Threat Radar Cost Redesign — Research
Threat Radar Cost Redesign — Research
Section titled “Threat Radar Cost Redesign — Research”Context: Live D1 telemetry analysis (55K samples rows, 253 real Apple TV sessions) found
the Apple TV — this project’s primary playtest device — has never sustained a stable 60fps, and
recent builds show a fresh, measurable regression: mean FPS 42.8 → 39.6 → 38.8 across builds
172→173→174. Sim time is consistently negligible (0.3-3.8ms/frame) across the whole history —
100% of the performance story is rendering. Controlling for enemy count (bucketed comparison),
build 173+ shows dramatically higher prims/draws even at LOW enemy counts than build 172 —
not explained by more enemies on screen.
Root cause, confirmed against source: build 173 shipped the per-enemy threat radar mist
rework (ui/threat_radar.gd, spec docs/superpowers/specs/2026-07-17-per-enemy-threat-radar-mist-design.md).
It replaced a fixed-cost 8-sector radar with one MultiMesh light instance PER ENEMY (up to
MAX_LIGHTS=400) across two layers (a full-res “sharp” layer + a low-res-then-bilinear-upscaled
“mist” layer rendered via a second SubViewport with UPDATE_ALWAYS). Two compounding cost
drivers: (1) primitive count now scales directly with live enemy count (up to 800 quads across
both layers), where the old sector version was a small fixed constant; (2) a second full render
pass (SubViewport: clear + scene traversal + composite) exists where none did before, adding
overhead independent of resolution — MIST_RESOLUTION_SCALE=0.18 already minimizes the mist
layer’s pixel-fill cost, but does nothing about instance count or the extra render pass itself.
Chris: “lets do an adversarial investigation into all the options we have available to get a radar system that is very low performance cost but still alerts the player to the information they need, we dont definitely need every single dot, but before we did that the other glowing dots didnt seem to indicate accurately what was going on so it needed adjustment but it doesnt have to be the way we have done it.”
External genre precedent
Section titled “External genre precedent”Checked Vampire Survivors, Brotato, Halls of Torment, Nova Drift, 20 Minutes Till Dawn, Soulstone Survivors, and Deep Rock Galactic Survivor. None have an explicit off-screen danger-direction/threat-radar UI element at all — every one of them relies purely on in-frame enemy density, screen shake/hit-flash, HP bar, and audio to communicate danger. The closest things found in any of them were unrelated (off-screen pickup-item arrows in Halls of Torment, a stage-select map in Vampire Survivors, a “find your ship after wraparound” indicator in Nova Drift) — nothing threat-related.
Implication: this game’s escalating swarm size (up to 200-345 concurrent enemies per its own telemetry) is a genuinely unusual scale problem this genre’s other games don’t hit as hard — this isn’t a missing standard convention, it’s a novel problem for the genre.
The closest useful outside precedent is the FPS “directional damage indicator”: a single aggregated signal (screen-edge highlight or vignette wedge) showing broad direction of incoming damage, not per-attacker markers. UX research on this pattern (analysis) found players miss both center-screen-only AND edge-only indicators roughly half the time in isolation; best practice combines both. Battlefield-style progressive edge vignette/desaturation (deliberately vague/aggregated, not a literal per-enemy readout) is praised in the genre for immersion. This suggests the ORIGINAL 8-sector radar’s real flaw was likely being driven by raw enemy count/proximity per sector rather than a properly threat-weighted, continuously-blended signal (flat discrete buckets, not smooth blending across sector boundaries) — not that sector aggregation itself is the wrong idea.
Candidate techniques evaluated against the real codebase
Section titled “Candidate techniques evaluated against the real codebase”Top-K sharp dots + sector-aggregate data reuse
Section titled “Top-K sharp dots + sector-aggregate data reuse”ThreatMap.enemy_weight(sim, pilot, i)(ai/threat_map.gd:30-40) is O(1) per enemy (onedistance_to, two flag checks, one division, oneexp()) — already called once per enemy per frame today by bothThreatRadar.build_light_data()andThreatMap.compute().ThreatMap.compute(sim, pilot, sectors)(ai/threat_map.gd:45-67) ALREADY returns, per sector,score(summed weight) andtarget_pos/angle— a sector-based renderer needs ZERO new sim-side computation, this data already exists every frame.- No existing top-K/bounded-selection helper exists anywhere in the codebase (
ThreatMap.best()only tracks a running max of 1) — selecting the top ~20-30 individually-marked threats needs new (but cheap, O(N·K) for small K) code. - Estimated cost: top-K=24 sharp instances + up to 16 sector-glow instances = a HARD CEILING of ~40 quads / ~2 draw calls, completely independent of live enemy count — versus the current design’s up to 800 quads / 400+ draws that scale directly with swarm size (confirmed via live telemetry at high-enemy-count samples).
Soft glow without a second SubViewport — two DIFFERENT techniques, not equivalent
Section titled “Soft glow without a second SubViewport — two DIFFERENT techniques, not equivalent”- Additive billboard overlap (more instances of
GlowTexture’s pre-baked radial-falloff texture, the same textureSwarmRenderer’s halo and the CURRENT radar’s MultiMeshes already use): this was ALREADY investigated and explicitly REJECTED for this exact feature, per the original mist design spec (docs/superpowers/specs/2026-07-17-per-enemy-threat-radar-mist-design.md), citing two published sources (2D lighting techniques, 2D metaballs): pure additive overlap brightens intersection zones but does NOT smooth the outer silhouette — each sprite’s own gradient edge stays visible, so a dense cluster reads as “many overlapping bright dots,” not one continuous cloud. This is why the SubViewport-blur approach was chosen in the first place. - A true analytic per-pixel gradient shader — a categorically DIFFERENT technique, already
shipped and proven cheap in this exact codebase:
fx/screen_feedback.gd’sBORDER_SHADER(the low-HP screen vignette) does genuine soft falloff viasmoothstep-based distance math computed continuously across a singleColorRect, with ZERO SubViewport and zero discrete sprite compositing. Because it’s a continuous mathematical function (not composited billboard edges), it is NOT subject to the “visible seams in dense clusters” problem the additive-overlap rejection above found — that rejection was specifically about sprite compositing, not about analytic shaders in general. Extending this exact falloff math from edge-distance to angular/sector wedges (fed byThreatMap.compute()’s already-available per-sector score/angle) is the same technique, reparameterized, not new territory. - No official Godot documentation quantifies a fixed per-SubViewport-instance cost independent of resolution/content — but this project’s own telemetry (elevated draw calls even at LOW enemy counts once the second SubViewport was added) is itself real-world evidence that a second render pass carries overhead beyond pure pixel-fill cost, consistent with (if not officially documented by) general driver-bound-mobile-GPU guidance that render-pass/draw-call count matters independently of resolution.
Decision
Section titled “Decision”Chosen: Option B — analytic sector-shader aggregate glow (no SubViewport) + top-K sharp dots for the single worst individual threats, replacing the current per-enemy sharp+mist system entirely. Combines: a fixed, swarm-size-independent cost ceiling (~40 quads/~2 draws regardless of enemy count, vs. the current uncapped-with-swarm-size design); the FPS-damage-indicator external precedent’s “one continuously-blended aggregate signal” pattern (addressing the ORIGINAL 8-sector version’s likely real flaw — flat, non-blended, non-threat-weighted buckets); and genuine individual-threat callouts for the handful of enemies that actually matter, via already-existing O(1)-per-enemy weight data.
Needs a real visual prototype before committing to an implementation plan — a described shader does not guarantee it reads as a “gentle wide mist,” the same lesson already learned once this session building the CURRENT (soon to be replaced) per-enemy version.