Add a handheld render rung and reach it with CARTO_QUALITY=steamdeck #293

Closed
opened 2026-09-09 21:52:40 +00:00 by viberfox-agent · 5 comments
Collaborator

Problem

quality.rs has three rungs and two machine classes. The rungs are Budget::phone(), Budget::balanced() and Budget::desktop() (crates/cartopolis/src/quality.rs:498, :439, :392); the machine classes are "Android" and "everything else" (quality.rs:901-907). A Steam Deck is neither: it lands in the else arm and takes Budget::desktop() (quality.rs:1137-1140) — a workstation's 4 shadow cascades to 3 km, full-size atmosphere LUTs re-baked every frame, the sky IBL, the space raymarch, a 512 px tile raster over the full 424-tile clipmap, and no ceiling at all on either the per-frame upload rate or resident streamed content (quality.rs:419-420).

The two words that would reach something cheaper by name do not exist: parse() accepts phone|low|mobile, balanced|medium, android|handset, desktop|high and nothing else (quality.rs:1037-1054).

balanced alone is not the answer, and quality.rs:1043-1049 already says why for the handset case: off the guarded platform it is the same rung with no upload ceiling, which is precisely the field that matters on a shared-memory part streaming a city. The guard that fixes that on Android — handset_guarded (quality.rs:1000-1022) — is applied only under cfg!(target_os = "android"), so it cannot reach a Deck.

Approach

One crate, cartopolis; principally one file.

crates/cartopolis/src/quality.rs

  1. Add handheld_guarded(budget: Budget) -> Budget, shaped exactly like handset_guarded (quality.rs:1000-1022): it may touch only the four memory fields, each as a min/max so a rung already asking for less keeps its own number —

    • upload_bytes_per_frame → Some(min(own, HANDHELD_UPLOAD_BYTES_PER_FRAME))
    • tile_px → min(own, HANDHELD_TILE_PX)
    • radius_bias → max(own, HANDHELD_RADIUS_BIAS)
    • resident_bytes_target → Some(min(own, HANDHELD_RESIDENT_BYTES))

    Everything else — tier, render_scale, shadows, bloom, detail_layers, MSAA, LUTs, cascades — passes through untouched, for the reason quality.rs:1359-1362 gives: a guard bounds what the driver is asked to hold, it does not second-guess the picture.

  2. Add pub(crate) fn steamdeck_budget() -> Budget { handheld_guarded(Budget::balanced()) }, doc-commented in the shape of android_budget (quality.rs:1116-1135) — including why it is its own function rather than a literal (so the tests can see it on every host).

  3. Each of the three/four constants gets its own doc block naming the measured row it came from, the way HANDSET_RESIDENT_BYTES does (quality.rs:1024-1029) and PHONE_UPLOAD_BYTES_PER_FRAME does (quality.rs:365-380). No number in this change may be typed without a row in the table below it.

  4. parse() gains "steamdeck" | "handheld" => Some(steamdeck_budget()), next to the "android" | "handset" arm, with a comment saying the same thing that arm says: the guard is what distinguishes it from bare balanced.

  5. Update the detect warning string at quality.rs:875-878, which still lists only phone|low|mobile or desktop|high and is already out of date.

crates/cartopolis/index.html:206-213 — add both words to the forced-tier chain, resolving to "balanced". The page only reduces its own resolution for the phone rung, so this changes no layout; it exists because that block's own comment (index.html:200-205) says the page must recognise every word or ?quality=steamdeck silently falls through to auto-detection there while Rust takes the rung.

docs/notes/steam-deck.md — new note with the standard front-matter (docs/notes/README.md:30-40), carrying the measured table, the date, the method, and the explicit statement of what is not established (see Verification).

No QualityPreset variant. resources.rs:218-259 and settings.rs:869 stay as they are; the ticket says so and the rung is reachable by name.

The one thing that will bite

resident_bytes_target is inert in a --shot run: load_control::observe is registered behind not(harness_active) (lib.rs:3023-3025, and the module note at load_control.rs:44-52 explains the choice). So the acceptance criterion "gpu_resident_kb below the desktop rung" can only be met by tile_px and/or radius_bias — the target is a controller set-point for interactive runs, not a lever the capture will show. Size it the way HANDSET_RESIDENT_BYTES was sized: just above what the chosen configuration actually measured.

Correspondingly, upload_bytes_per_frame is also partly inert under the harness: bevy's RenderAssetBytesPerFrame is forced to None in a scripted run unless CARTO_UPLOAD_KB is set (lib.rs:1864-1880). That is what makes the capture useful for sizing the ceiling — upload_peak_kb / upload_peak_asset_kb report what an unthrottled queue hands over. map_stream's own tile cap does honour upload_ceiling() during a capture (quality.rs:738-746), so expect settle time to move.

Test extension

every_rung_costs_no_more_than_the_one_above_it (quality.rs:1200) walks an array ordered by Tier and asserts lower.tier < upper.tier. The new rung is Tier::Balanced, so it cannot be appended to that array. Extract the per-field body into a helper (assert_no_more_expensive_than(lower, upper, name)), leave the existing loop calling it, and add one assertion that steamdeck_budget() costs no more than Budget::balanced() on every field where cheaper is defined, with the tier equal rather than lower.

Acceptance criteria

  • quality::handheld_guarded exists, is pub(crate), is compiled on every target (a plain function, no #[cfg] — the trap recorded at quality.rs:1118-1126), and modifies only upload_bytes_per_frame, tile_px, radius_bias, resident_bytes_target.
  • quality::steamdeck_budget() returns handheld_guarded(Budget::balanced()).
  • CARTO_QUALITY=steamdeck and CARTO_QUALITY=handheld both resolve to it; case and surrounding whitespace are tolerated, as the existing vocabulary test requires (quality.rs:1176-1183).
  • index.html's forced-tier chain recognises both words and maps them to "balanced".
  • resources::QualityPreset and systems/gui/settings.rs are unchanged; user.json gains no new value.
  • Every constant the new rung introduces carries a doc comment naming the measured row it came from, and that row appears in docs/notes/steam-deck.md.
  • the_tier_vocabulary_matches_the_page covers the two new words.
  • A new assertion pins steamdeck_budget() ≤ Budget::balanced() field by field, with tier equal; the existing three-rung ladder loop still passes unchanged in meaning.
  • a_handset_keeps_the_burst_guard_on_every_rung-style assertions exist for handheld_guarded: a ceiling is always present, a rung asking for less keeps its own numbers, and tier / render_scale / shadows / bloom / detail_layers are not moved by the guard.
  • no_rung_permits_an_asset_larger_than_its_own_frame (quality.rs:1385) gains ("steamdeck", steamdeck_budget()), and passes — i.e. lod22::max_cell_mesh_bytes_for of the new rung is ≤ its own ceiling.
  • cargo test -p cartopolis passes.
  • At both viewpoints (Groningen, 150 m and 1700 m, --size 1280x800), the steamdeck rung reports draws, tris_shadow and gpu_resident_kb all strictly below the desktop rung at the same viewpoint, with settled == true in every capture.
  • docs/notes/steam-deck.md carries the four numbers per configuration in a table like handset_guarded's (quality.rs:975-984), with the date, the tile source, the cache state, and an explicit sentence about what the measurement does not establish.
  • The commit body names the value from docs/direction.md that was applied.

Verification

cargo test -p cartopolis
cargo fmt --check over the five owned crates.

The measurement pair — two captures per launch, one launch per configuration. Both runs need a vector tile source or the LoD2.2 cells that dominate residency never arrive (docs/shots.toml:17-23):

export WGPU_BACKEND=vulkan
export CARTO_TILE_URL='https://tiles.cartopolis.org/tiles/osm/{z}/{x}/{y}'

for q in desktop steamdeck; do
  CARTO_QUALITY=$q cargo run -p cartopolis -- \
    --shot /tmp/deck-$q.png --size 1280x800 \
    --at 53.2194,6.5665,150 --at 53.2194,6.5665,1700 \
    --look=-25,35 --look=-25,35 --time 12 --settle 1.5 --wait 45 \
    --dump-state /tmp/deck-$q.json --csv /tmp/deck-$q.csv
done

--dump-state writes an array for a two-capture run. The columns to read into the note: draws, tris_total, tris_shadow, gpu_resident_kb, upload_peak_kb, upload_peak_asset_kb, settled, settle time, radius_bias. Run each configuration twice and report the second (warm cache), and say which it is — handset_guarded's table is a warm-cache measurement and a cold one is not comparable to it.

This is runnable here. The agent container has Mesa lavapipe (docs/notes/headless-shots-software-renderer.md), and every number above is a geometry, asset-byte or timing-of-streaming count, not colour or lighting — quality.rs:977-979 states outright that gpu_resident_kb is asset bytes and therefore machine-independent. Frame time and anything about the picture's appearance are not evidence on that adapter and must not appear in the note as if they were.

What no harness here can check: that a Steam Deck actually holds a frame rate on this rung, or that the ceiling is the right one for its shared-memory bus. Nobody in this loop has the device. The note must say so, in the same terms quality.rs:995-998 uses about the handset.

Out of scope

  • Auto-detecting a Steam Deck. on_this_machine guards only Android (quality.rs:901-907), no cfg distinguishes a Deck from any other Linux desktop, and init runs before an adapter exists (quality.rs:1100-1104) so nothing can be asked of the device — any detector is a name, an env var or a DMI string, which is exactly the substitution Capabilities' own rule rejects (quality.rs:781-785) and which nobody here can verify without the hardware. Consequence to state plainly in the commit body: on a Deck today the rung is reached by CARTO_QUALITY=steamdeck and by nothing else, and picking "Full detail" in Settings there still yields an unguarded Budget::desktop(). Value 6 of docs/direction.md ("ship the smallest thing a machine can judge") is what settles it — a detector is judged by hardware behaviour, and this ticket's evidence is a metrics diff.
  • A QualityPreset variant / a Settings entry for it. Ruled out by the ticket.
  • Frame pacing. The Deck's 60 Hz LCD vs 90 Hz OLED and gamescope's own vsync are a fps_cap / Capabilities::vsync_paced question (quality.rs:230-258, :809), untouched here.
  • render_scale. balanced renders native (quality.rs:466-468) and this change leaves it there; a Deck render scale is a picture judgement that needs the panel in front of someone.
  • Any change to handset_guarded or the Android rung. The new function is a sibling, not a refactor of it; extracting a shared helper is fine only if neither Android number moves.

The other tickets this one is not

  1. Reach the rung automatically on a Deck — needs a signal (SteamOS session env, DMI product name) plus a device to confirm the signal actually fires. Blocked on hardware, not on design.
  2. Pace for the Deck's panel — fps_cap / target_fps / presentation mode for a 60 Hz and a 90 Hz variant. Also needs the device.

Open questions

None.


Branch: perf/293-handheld-render-rung

Original request

From the maintainer's focus for this lane:

Improving performance on the steamdeck

Add a handheld render rung and reach it with CARTO_QUALITY=steamdeck

Lands in crates/cartopolis/src/quality.rs. Add handheld_guarded(Budget::balanced()) in the same shape as android_budget/handset_guarded, with the upload ceiling and resident-bytes target measured rather than guessed, and add the words steamdeck and handheld to parse(). No new QualityPreset variant, so the settings picker and user.json are untouched and the rung is reached by name or by auto-detection. Judged by cargo test -p cartopolis (the existing rung-ladder and tier-vocabulary tests, extended) and by a --shot --size 1280x800 --dump-state pair over Groningen at 150 m and 1700 m: draws, tris_shadow and gpu_resident_kb all below the desktop rung at the same viewpoint, with settled still true. Write the measured table into docs/notes/steam-deck.md the way handset_guarded's table is written.

This is item 1 of 8 of the plan under that focus. The queue is ordered by a person in the
admin console, so build this one rather than the one you would have picked — and keep it
the size it is. A ticket that turns out to be three tickets is one ticket: finish this
part and say on it what the other two are.

If the item is simply wrong — already done, impossible, or a bad idea against the values
— say so here and close it. That is a result, not a failure.

Decide it yourself. This ticket is not being watched, so a question asked here is a
ticket that stops. The seven values at the top of docs/direction.md exist to settle
exactly that kind of ambiguity: pick the reading they support, say in the commit body
which one you applied, and build. Only a decision needing something nobody can derive
from the repository — a credential, a licence somebody must accept, a choice about what
the project is for — is a reason to stop.

Filed by the autopilot.

🤖 Refined by the viberfox issue agent. Reply with @agent refine and what is wrong to have this rewritten.

## Problem `quality.rs` has three rungs and two machine classes. The rungs are `Budget::phone()`, `Budget::balanced()` and `Budget::desktop()` (`crates/cartopolis/src/quality.rs:498`, `:439`, `:392`); the machine classes are "Android" and "everything else" (`quality.rs:901-907`). A Steam Deck is neither: it lands in the `else` arm and takes `Budget::desktop()` (`quality.rs:1137-1140`) — a workstation's 4 shadow cascades to 3 km, full-size atmosphere LUTs re-baked every frame, the sky IBL, the space raymarch, a 512 px tile raster over the full 424-tile clipmap, and **no ceiling at all** on either the per-frame upload rate or resident streamed content (`quality.rs:419-420`). The two words that would reach something cheaper by name do not exist: `parse()` accepts `phone|low|mobile`, `balanced|medium`, `android|handset`, `desktop|high` and nothing else (`quality.rs:1037-1054`). `balanced` alone is not the answer, and `quality.rs:1043-1049` already says why for the handset case: off the guarded platform it is the same rung with **no upload ceiling**, which is precisely the field that matters on a shared-memory part streaming a city. The guard that fixes that on Android — `handset_guarded` (`quality.rs:1000-1022`) — is applied only under `cfg!(target_os = "android")`, so it cannot reach a Deck. ## Approach One crate, `cartopolis`; principally one file. **`crates/cartopolis/src/quality.rs`** 1. Add `handheld_guarded(budget: Budget) -> Budget`, shaped exactly like `handset_guarded` (`quality.rs:1000-1022`): it may touch **only** the four memory fields, each as a `min`/`max` so a rung already asking for less keeps its own number — - `upload_bytes_per_frame` → `Some(min(own, HANDHELD_UPLOAD_BYTES_PER_FRAME))` - `tile_px` → `min(own, HANDHELD_TILE_PX)` - `radius_bias` → `max(own, HANDHELD_RADIUS_BIAS)` - `resident_bytes_target` → `Some(min(own, HANDHELD_RESIDENT_BYTES))` Everything else — `tier`, `render_scale`, `shadows`, `bloom`, `detail_layers`, MSAA, LUTs, cascades — passes through untouched, for the reason `quality.rs:1359-1362` gives: a guard bounds what the driver is asked to hold, it does not second-guess the picture. 2. Add `pub(crate) fn steamdeck_budget() -> Budget { handheld_guarded(Budget::balanced()) }`, doc-commented in the shape of `android_budget` (`quality.rs:1116-1135`) — including *why* it is its own function rather than a literal (so the tests can see it on every host). 3. Each of the three/four constants gets its own doc block naming the measured row it came from, the way `HANDSET_RESIDENT_BYTES` does (`quality.rs:1024-1029`) and `PHONE_UPLOAD_BYTES_PER_FRAME` does (`quality.rs:365-380`). **No number in this change may be typed without a row in the table below it.** 4. `parse()` gains `"steamdeck" | "handheld" => Some(steamdeck_budget())`, next to the `"android" | "handset"` arm, with a comment saying the same thing that arm says: the guard is what distinguishes it from bare `balanced`. 5. Update the `detect` warning string at `quality.rs:875-878`, which still lists only `phone|low|mobile or desktop|high` and is already out of date. **`crates/cartopolis/index.html:206-213`** — add both words to the forced-tier chain, resolving to `"balanced"`. The page only reduces its own resolution for the phone rung, so this changes no layout; it exists because that block's own comment (`index.html:200-205`) says the page must *recognise* every word or `?quality=steamdeck` silently falls through to auto-detection there while Rust takes the rung. **`docs/notes/steam-deck.md`** — new note with the standard front-matter (`docs/notes/README.md:30-40`), carrying the measured table, the date, the method, and the explicit statement of what is *not* established (see Verification). **No `QualityPreset` variant.** `resources.rs:218-259` and `settings.rs:869` stay as they are; the ticket says so and the rung is reachable by name. ### The one thing that will bite `resident_bytes_target` is **inert in a `--shot` run**: `load_control::observe` is registered behind `not(harness_active)` (`lib.rs:3023-3025`, and the module note at `load_control.rs:44-52` explains the choice). So the acceptance criterion "`gpu_resident_kb` below the desktop rung" can only be met by `tile_px` and/or `radius_bias` — the target is a controller set-point for interactive runs, not a lever the capture will show. Size it the way `HANDSET_RESIDENT_BYTES` was sized: just above what the chosen configuration actually measured. Correspondingly, `upload_bytes_per_frame` is **also** partly inert under the harness: bevy's `RenderAssetBytesPerFrame` is forced to `None` in a scripted run unless `CARTO_UPLOAD_KB` is set (`lib.rs:1864-1880`). That is what makes the capture useful for *sizing* the ceiling — `upload_peak_kb` / `upload_peak_asset_kb` report what an unthrottled queue hands over. `map_stream`'s own tile cap does honour `upload_ceiling()` during a capture (`quality.rs:738-746`), so expect settle time to move. ### Test extension `every_rung_costs_no_more_than_the_one_above_it` (`quality.rs:1200`) walks an array ordered by `Tier` and asserts `lower.tier < upper.tier`. The new rung is `Tier::Balanced`, so **it cannot be appended to that array**. Extract the per-field body into a helper (`assert_no_more_expensive_than(lower, upper, name)`), leave the existing loop calling it, and add one assertion that `steamdeck_budget()` costs no more than `Budget::balanced()` on every field where cheaper is defined, with the tier equal rather than lower. ## Acceptance criteria - [ ] `quality::handheld_guarded` exists, is `pub(crate)`, is compiled on every target (a plain function, no `#[cfg]` — the trap recorded at `quality.rs:1118-1126`), and modifies only `upload_bytes_per_frame`, `tile_px`, `radius_bias`, `resident_bytes_target`. - [ ] `quality::steamdeck_budget()` returns `handheld_guarded(Budget::balanced())`. - [ ] `CARTO_QUALITY=steamdeck` and `CARTO_QUALITY=handheld` both resolve to it; case and surrounding whitespace are tolerated, as the existing vocabulary test requires (`quality.rs:1176-1183`). - [ ] `index.html`'s forced-tier chain recognises both words and maps them to `"balanced"`. - [ ] `resources::QualityPreset` and `systems/gui/settings.rs` are unchanged; `user.json` gains no new value. - [ ] Every constant the new rung introduces carries a doc comment naming the measured row it came from, and that row appears in `docs/notes/steam-deck.md`. - [ ] `the_tier_vocabulary_matches_the_page` covers the two new words. - [ ] A new assertion pins `steamdeck_budget()` ≤ `Budget::balanced()` field by field, with `tier` equal; the existing three-rung ladder loop still passes unchanged in meaning. - [ ] `a_handset_keeps_the_burst_guard_on_every_rung`-style assertions exist for `handheld_guarded`: a ceiling is always present, a rung asking for less keeps its own numbers, and `tier` / `render_scale` / `shadows` / `bloom` / `detail_layers` are not moved by the guard. - [ ] `no_rung_permits_an_asset_larger_than_its_own_frame` (`quality.rs:1385`) gains `("steamdeck", steamdeck_budget())`, and passes — i.e. `lod22::max_cell_mesh_bytes_for` of the new rung is ≤ its own ceiling. - [ ] `cargo test -p cartopolis` passes. - [ ] At **both** viewpoints (Groningen, 150 m and 1700 m, `--size 1280x800`), the `steamdeck` rung reports `draws`, `tris_shadow` and `gpu_resident_kb` all strictly below the `desktop` rung at the same viewpoint, with `settled == true` in every capture. - [ ] `docs/notes/steam-deck.md` carries the four numbers per configuration in a table like `handset_guarded`'s (`quality.rs:975-984`), with the date, the tile source, the cache state, and an explicit sentence about what the measurement does *not* establish. - [ ] The commit body names the value from `docs/direction.md` that was applied. ## Verification `cargo test -p cartopolis` `cargo fmt --check` over the five owned crates. The measurement pair — two captures per launch, one launch per configuration. Both runs need a **vector** tile source or the LoD2.2 cells that dominate residency never arrive (`docs/shots.toml:17-23`): ```bash export WGPU_BACKEND=vulkan export CARTO_TILE_URL='https://tiles.cartopolis.org/tiles/osm/{z}/{x}/{y}' for q in desktop steamdeck; do CARTO_QUALITY=$q cargo run -p cartopolis -- \ --shot /tmp/deck-$q.png --size 1280x800 \ --at 53.2194,6.5665,150 --at 53.2194,6.5665,1700 \ --look=-25,35 --look=-25,35 --time 12 --settle 1.5 --wait 45 \ --dump-state /tmp/deck-$q.json --csv /tmp/deck-$q.csv done ``` `--dump-state` writes an array for a two-capture run. The columns to read into the note: `draws`, `tris_total`, `tris_shadow`, `gpu_resident_kb`, `upload_peak_kb`, `upload_peak_asset_kb`, `settled`, settle time, `radius_bias`. Run each configuration twice and report the second (warm cache), and say which it is — `handset_guarded`'s table is a warm-cache measurement and a cold one is not comparable to it. **This is runnable here.** The agent container has Mesa lavapipe (`docs/notes/headless-shots-software-renderer.md`), and every number above is a geometry, asset-byte or timing-of-streaming count, not colour or lighting — `quality.rs:977-979` states outright that `gpu_resident_kb` is asset bytes and therefore machine-independent. Frame time and anything about the picture's appearance are **not** evidence on that adapter and must not appear in the note as if they were. **What no harness here can check:** that a Steam Deck actually holds a frame rate on this rung, or that the ceiling is the right one for its shared-memory bus. Nobody in this loop has the device. The note must say so, in the same terms `quality.rs:995-998` uses about the handset. ## Out of scope - **Auto-detecting a Steam Deck.** `on_this_machine` guards only Android (`quality.rs:901-907`), no `cfg` distinguishes a Deck from any other Linux desktop, and `init` runs before an adapter exists (`quality.rs:1100-1104`) so nothing can be *asked* of the device — any detector is a name, an env var or a DMI string, which is exactly the substitution `Capabilities`' own rule rejects (`quality.rs:781-785`) and which nobody here can verify without the hardware. Consequence to state plainly in the commit body: **on a Deck today the rung is reached by `CARTO_QUALITY=steamdeck` and by nothing else**, and picking "Full detail" in Settings there still yields an unguarded `Budget::desktop()`. Value 6 of `docs/direction.md` ("ship the smallest thing a machine can judge") is what settles it — a detector is judged by hardware behaviour, and this ticket's evidence is a metrics diff. - **A `QualityPreset` variant / a Settings entry for it.** Ruled out by the ticket. - **Frame pacing.** The Deck's 60 Hz LCD vs 90 Hz OLED and gamescope's own vsync are a `fps_cap` / `Capabilities::vsync_paced` question (`quality.rs:230-258`, `:809`), untouched here. - **`render_scale`.** `balanced` renders native (`quality.rs:466-468`) and this change leaves it there; a Deck render scale is a picture judgement that needs the panel in front of someone. - **Any change to `handset_guarded` or the Android rung.** The new function is a sibling, not a refactor of it; extracting a shared helper is fine only if neither Android number moves. ### The other tickets this one is not 1. **Reach the rung automatically on a Deck** — needs a signal (SteamOS session env, DMI product name) plus a device to confirm the signal actually fires. Blocked on hardware, not on design. 2. **Pace for the Deck's panel** — `fps_cap` / `target_fps` / presentation mode for a 60 Hz and a 90 Hz variant. Also needs the device. ## Open questions None. --- Branch: `perf/293-handheld-render-rung` <details><summary>Original request</summary> From the maintainer's focus for this lane: > Improving performance on the steamdeck **Add a handheld render rung and reach it with CARTO_QUALITY=steamdeck** Lands in crates/cartopolis/src/quality.rs. Add handheld_guarded(Budget::balanced()) in the same shape as android_budget/handset_guarded, with the upload ceiling and resident-bytes target measured rather than guessed, and add the words steamdeck and handheld to parse(). No new QualityPreset variant, so the settings picker and user.json are untouched and the rung is reached by name or by auto-detection. Judged by cargo test -p cartopolis (the existing rung-ladder and tier-vocabulary tests, extended) and by a --shot --size 1280x800 --dump-state pair over Groningen at 150 m and 1700 m: draws, tris_shadow and gpu_resident_kb all below the desktop rung at the same viewpoint, with settled still true. Write the measured table into docs/notes/steam-deck.md the way handset_guarded's table is written. This is item 1 of 8 of the plan under that focus. The queue is ordered by a person in the admin console, so build this one rather than the one you would have picked — and keep it the size it is. A ticket that turns out to be three tickets is one ticket: finish this part and say on it what the other two are. If the item is simply wrong — already done, impossible, or a bad idea against the values — say so here and close it. That is a result, not a failure. **Decide it yourself.** This ticket is not being watched, so a question asked here is a ticket that stops. The seven values at the top of `docs/direction.md` exist to settle exactly that kind of ambiguity: pick the reading they support, say in the commit body which one you applied, and build. Only a decision needing something nobody can derive from the repository — a credential, a licence somebody must accept, a choice about what the project is *for* — is a reason to stop. <sub>Filed by the autopilot. <!-- autopilot:focus-add-a-handheld-render-rung-and-reach-it --></sub> </details> <sub>🤖 Refined by the viberfox issue agent. Reply with **@agent refine** and what is wrong to have this rewritten.</sub>
Author
Collaborator

🤖 Refining this before anything gets built — reading the code, no branch, no commits. If it turns out I'd have to guess at the requirements I'll stop and ask instead.

🤖 Refining this before anything gets built — reading the code, no branch, no commits. If it turns out I'd have to guess at the requirements I'll stop and ask instead.
Author
Collaborator

🤖 Refined. The description above is now the spec; I'll pick it up for implementation on the next poll.

If the direction is wrong, say so now — @agent refine with a correction rewrites it, and agent:skip stops me.


🤖 viberfox issue agent on the VPS — 30 turns · $2.96 · session 138fd6d0-58f9-4601-a05e-e1f7a378e931. Reply with @agent to continue this session.

🤖 **Refined.** The description above is now the spec; I'll pick it up for implementation on the next poll. If the direction is wrong, say so now — **@agent refine** with a correction rewrites it, and `agent:skip` stops me. --- <sub>🤖 viberfox issue agent on the VPS — 30 turns · $2.96 · session `138fd6d0-58f9-4601-a05e-e1f7a378e931`. Reply with **@agent** to continue this session.</sub>
Author
Collaborator

🤖 Picked this up — working on branch perf/293-handheld-render-rung in a fresh worktree on the VPS. I'll report back here when I'm done.

Label an issue agent:skip to keep me off it, or driven to keep it in a session you are driving.

🤖 Picked this up — working on branch `perf/293-handheld-render-rung` in a fresh worktree on the VPS. I'll report back here when I'm done. <sub>Label an issue `agent:skip` to keep me off it, or `driven` to keep it in a session you are driving.</sub>
Author
Collaborator

The session was killed after hitting the 45 minute limit.

No commits were made.


🤖 viberfox issue agent on the VPS — . Reply with @agent to continue this session.

The session was killed after hitting the 45 minute limit. _No commits were made._ --- <sub>🤖 viberfox issue agent on the VPS — . Reply with **@agent** to continue this session.</sub>
Owner

Nightly review (2026-09-29), not built: most of this landed another way. Since 90f6c7d, CARTO_QUALITY=balanced CARTO_HANDSET=1 applies handset_guarded to balanced on any machine, so a Steam Deck can already run exactly the guarded rung this ticket describes. What remains is a steamdeck alias with Deck-specific constants, and those need a measurement on the device. Suggest closing it, or re-scoping it to that measurement.

Nightly review (2026-09-29), not built: most of this landed another way. Since 90f6c7d, `CARTO_QUALITY=balanced CARTO_HANDSET=1` applies `handset_guarded` to `balanced` on any machine, so a Steam Deck can already run exactly the guarded rung this ticket describes. What remains is a `steamdeck` alias with Deck-specific constants, and those need a measurement on the device. Suggest closing it, or re-scoping it to that measurement.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jeroen/cartopolis#293
No description provided.