Add a handheld render rung and reach it with CARTO_QUALITY=steamdeck #293
Labels
No labels
agent
agent:ci
agent:done
agent:failed
agent:needs-input
agent:refined
agent:refining
agent:running
agent:shipped
agent:skip
autonomous
autopilot
driven
local
plan
proposal
qa
qa-gap
research
retro
ship
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jeroen/cartopolis#293
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
quality.rshas three rungs and two machine classes. The rungs areBudget::phone(),Budget::balanced()andBudget::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 theelsearm and takesBudget::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()acceptsphone|low|mobile,balanced|medium,android|handset,desktop|highand nothing else (quality.rs:1037-1054).balancedalone is not the answer, andquality.rs:1043-1049already 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 undercfg!(target_os = "android"), so it cannot reach a Deck.Approach
One crate,
cartopolis; principally one file.crates/cartopolis/src/quality.rsAdd
handheld_guarded(budget: Budget) -> Budget, shaped exactly likehandset_guarded(quality.rs:1000-1022): it may touch only the four memory fields, each as amin/maxso 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 reasonquality.rs:1359-1362gives: a guard bounds what the driver is asked to hold, it does not second-guess the picture.Add
pub(crate) fn steamdeck_budget() -> Budget { handheld_guarded(Budget::balanced()) }, doc-commented in the shape ofandroid_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).Each of the three/four constants gets its own doc block naming the measured row it came from, the way
HANDSET_RESIDENT_BYTESdoes (quality.rs:1024-1029) andPHONE_UPLOAD_BYTES_PER_FRAMEdoes (quality.rs:365-380). No number in this change may be typed without a row in the table below it.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 barebalanced.Update the
detectwarning string atquality.rs:875-878, which still lists onlyphone|low|mobile or desktop|highand 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=steamdecksilently 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
QualityPresetvariant.resources.rs:218-259andsettings.rs:869stay as they are; the ticket says so and the rung is reachable by name.The one thing that will bite
resident_bytes_targetis inert in a--shotrun:load_control::observeis registered behindnot(harness_active)(lib.rs:3023-3025, and the module note atload_control.rs:44-52explains the choice). So the acceptance criterion "gpu_resident_kbbelow the desktop rung" can only be met bytile_pxand/orradius_bias— the target is a controller set-point for interactive runs, not a lever the capture will show. Size it the wayHANDSET_RESIDENT_BYTESwas sized: just above what the chosen configuration actually measured.Correspondingly,
upload_bytes_per_frameis also partly inert under the harness: bevy'sRenderAssetBytesPerFrameis forced toNonein a scripted run unlessCARTO_UPLOAD_KBis set (lib.rs:1864-1880). That is what makes the capture useful for sizing the ceiling —upload_peak_kb/upload_peak_asset_kbreport what an unthrottled queue hands over.map_stream's own tile cap does honourupload_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 byTierand assertslower.tier < upper.tier. The new rung isTier::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 thatsteamdeck_budget()costs no more thanBudget::balanced()on every field where cheaper is defined, with the tier equal rather than lower.Acceptance criteria
quality::handheld_guardedexists, ispub(crate), is compiled on every target (a plain function, no#[cfg]— the trap recorded atquality.rs:1118-1126), and modifies onlyupload_bytes_per_frame,tile_px,radius_bias,resident_bytes_target.quality::steamdeck_budget()returnshandheld_guarded(Budget::balanced()).CARTO_QUALITY=steamdeckandCARTO_QUALITY=handheldboth 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::QualityPresetandsystems/gui/settings.rsare unchanged;user.jsongains no new value.docs/notes/steam-deck.md.the_tier_vocabulary_matches_the_pagecovers the two new words.steamdeck_budget()≤Budget::balanced()field by field, withtierequal; the existing three-rung ladder loop still passes unchanged in meaning.a_handset_keeps_the_burst_guard_on_every_rung-style assertions exist forhandheld_guarded: a ceiling is always present, a rung asking for less keeps its own numbers, andtier/render_scale/shadows/bloom/detail_layersare 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_forof the new rung is ≤ its own ceiling.cargo test -p cartopolispasses.--size 1280x800), thesteamdeckrung reportsdraws,tris_shadowandgpu_resident_kball strictly below thedesktoprung at the same viewpoint, withsettled == truein every capture.docs/notes/steam-deck.mdcarries the four numbers per configuration in a table likehandset_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.docs/direction.mdthat was applied.Verification
cargo test -p cartopoliscargo fmt --checkover 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):--dump-statewrites 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-979states outright thatgpu_resident_kbis 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-998uses about the handset.Out of scope
on_this_machineguards only Android (quality.rs:901-907), nocfgdistinguishes a Deck from any other Linux desktop, andinitruns 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 substitutionCapabilities' 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 byCARTO_QUALITY=steamdeckand by nothing else, and picking "Full detail" in Settings there still yields an unguardedBudget::desktop(). Value 6 ofdocs/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.QualityPresetvariant / a Settings entry for it. Ruled out by the ticket.fps_cap/Capabilities::vsync_pacedquestion (quality.rs:230-258,:809), untouched here.render_scale.balancedrenders 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.handset_guardedor 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
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-rungOriginal request
From the maintainer's focus for this lane:
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.mdexist to settleexactly 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.
🤖 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.
🤖 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:skipstops me.🤖 viberfox issue agent on the VPS — 30 turns · $2.96 · session
138fd6d0-58f9-4601-a05e-e1f7a378e931. Reply with @agent to continue this session.🤖 Picked this up — working on branch
perf/293-handheld-render-rungin a fresh worktree on the VPS. I'll report back here when I'm done.Label an issue
agent:skipto keep me off it, ordrivento keep it in a session you are driving.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.
alreadyFiledonly ever sees 50 issues, so every autopilot dedupe key silently expires — #206 was QA'd twice #281alreadyFiledonly ever sees 50 issues, so every autopilot dedupe key silently expires — #206 was QA'd twice #281Nightly review (2026-09-29), not built: most of this landed another way. Since
90f6c7d,CARTO_QUALITY=balanced CARTO_HANDSET=1applieshandset_guardedtobalancedon any machine, so a Steam Deck can already run exactly the guarded rung this ticket describes. What remains is asteamdeckalias with Deck-specific constants, and those need a measurement on the device. Suggest closing it, or re-scoping it to that measurement.