Skip to content

WS2: performance — quality levels 0-3, faster step-down, 72 Hz, perf budget - #10

Merged
karanheart96 merged 6 commits into
competition-hardeningfrom
competition-hardening-ws2-performance
Nov 11, 2026
Merged

karanheart96 merged 6 commits into
competition-hardeningfrom
competition-hardening-ws2-performance

Conversation

@karanheart96

Copy link
Copy Markdown
Contributor

WS2 (P0): performance. The goal is to hold 72 Hz on Quest 3/3S and degrade quietly if a headset can't.

What changed

  • Quality levels 0–3 (step down only, never back up within a session):
    • 1: 512 px shadow map; the shadow surface is trimmed to the lit area. It follows the light pool and grows for the finale; before this it was 2.6× the stage width.
    • 2: no room-dimming layer; silhouette readbacks at 2.5 Hz.
    • 3 (new, last resort): 256 px shadow map; half the storm's raindrops.
  • Faster step-down. The level drops after 1.5 s (was 3 s) of frames averaging more than 3% over the display interval, which is 14.3 ms at 72 Hz. There is a 4 s warm-up and a 1 s settle between steps.
  • Bug fixed: the emulator and desktops always ran at reduced quality. The old fixed 15.2 ms threshold tripped at 60 Hz, so every emulator session (and every earlier emulator playtest) was quietly running at level 2. The display interval now comes from XRSession.frameRate on a real device. Under the emulator, which reports 72 Hz while Chromium draws at 60, it is measured at the end of warm-up. The emulator is detected by its IWER_DEVICE global, since it also spoofs the Quest user agent.
  • 72 Hz is requested for everyone on XR entry, not only in ?diag. The result (granted / refused / unsupported) goes to diagnostics and the summary.
  • Empty hand casters are hidden, so an untracked hand no longer sits in the shadow and depth passes with zero instances.
  • __sp.audit() reports visible meshes, shadow casters, transparent layers and shadow-map setup.
  • The playtest enforces a performance budget: the measured peaks plus 10% (79 draw calls, 45,100 triangles, 2 shadow casters). Exceeding it fails the run.

Numbers (emulator, GPU, The Moth, six personas)

Before After
Quality level the emulator actually ran at 2 (unintended step-down) 0
Peak draw calls / triangles ~70 / 39,082 (at level 2) 72 / 41,002 (at level 0)
Peak shadow casters hand casters incl. empty ones 2 (one per hand)
App time per frame 0.6 ms median 0.6 ms median, p95 0.7–0.9
Time to start stepping under sustained load 3 s + warm-up 1.5 s + warm-up; level 3 reached ~6 s into load
Shadow map at the lowest level 512 256

The emulator can't measure GPU time on a Quest, so the real frame-time gain from the trimmed surface and smaller maps is a device item.

Trade-offs (the conservative option was chosen in each case)

  • Threshold. Stepping at exactly 13.9 ms would trip on ordinary 72 Hz jitter, so I used 3% over the interval. A tighter margin would react sooner but risks needless step-downs.
  • Never step back up. Stepping up could recover quality after a transient load, but it risks visible flicker loops, so it isn't done.
  • Shadow filter. I kept PCFSoft. Plain PCF is a similar tap count, so switching saves nothing.
  • Emulator detection. I trust frameRate only off the emulator. The alternative, always measuring, would hide a Quest that is slow from the very first frames.

Verified

  • npm test, npm run build: pass.
  • playtest.mjs --gpu: all six personas, no stalls, 0 errors, 60 fps, budget ok.
  • live-family.mjs: pass; fps the same as the previous run (software rendering).
  • Each level screenshotted in the emulator: levels 0 and 1 look identical, so the trimmed surface leaves no visible edge; level 2 shows a brighter room, as designed.
  • Step-down tested with an injected 22 ms per-frame load: 0→1 after ~1.5 s, level 3 by ~6 s. With no load it holds at 0.
  • The silhouette readback is already async (readRenderTargetPixelsAsync).

Device items (docs/DEVICE-TEST.md)

  • A7 now covers levels q0..q3.
  • A7b (new): no pop when the level changes, and shadows not clipped.
  • Open questions: is 72 Hz granted, and does Quest 3S hold q0 through the storm and finale?

🤖 Generated with Claude Code

karanheart96 and others added 6 commits November 10, 2026 20:55
Competition hardening: diagnostics, cold start, recognition
…t on the stage

Level 1 trims the shadow-receiving surface to the lit area (it follows the
pool and grows for the finale). Level 3 is a 256 px shadow map and half the
storm's raindrops. The stage asks for 72 Hz on XR entry and records the answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he display rate

Slow means 3% over the refresh interval (14.3 ms at 72 Hz). The interval comes
from XRSession.frameRate on a device; under the emulator, which claims 72 Hz on
a 60 Hz desktop, it is measured at the end of warm-up. Previously any 60 Hz
emulator session was quietly stepped down to level 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…shadow map

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…checks for quality levels

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@karanheart96
karanheart96 merged commit 561374b into competition-hardening Nov 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant