Quest performance: the budget, the lie, and the capture
Performance work on Horizon OS fails in one of two ways: optimising without a
capture, or reading the capture wrong. This skill fixes the second first, because
the most-quoted number on the platform —
— is misleading exactly when it
matters.
For a whole-product roadmap or stage audit, use
; a single technical task stays with this owner. If absent, identify the current stage, its evidence and the next prerequisite inline.
Read
references/mobile-rendering.md
when choosing the render pipeline or investigating tile, render-pass, geometry, overdraw and asset costs.
The budget is a deadline, not a target
| Refresh rate | Time per frame |
|---|
| 72 Hz | 13.9 ms |
| 90 Hz | 11.1 ms |
| 120 Hz | 8.3 ms |
CPU, render-thread, GPU and compositor work form a pipeline and can overlap
across frames. Compare the critical path and synchronization against the
deadline; do not add independent CPU/GPU samples as a universal frame-time
formula. Missed submissions may invoke reprojection, with visible artifacts
that depend on motion, depth and the selected runtime path.
The GPU% lie, and the 50% threshold
When an app drops to half rate the GPU finishes early and idles, so utilisation
falls while the app is firmly GPU-bound.
text
FPS=36/72 Stale=36 GPU%=0.65 App=18.05ms
- — half rate: every frame shown twice.
- — the real per-frame GPU time, against a 13.9 ms budget.
- — measured across the doubled 27.8 ms interval, which is why it
reads 65% while the app needs a 23% cut, not the 35% "headroom" it implies.
Rule: read in milliseconds against the refresh-rate budget. Only trust
near saturation. To climb back out of half rate, GPU work must fit one
refresh interval — i.e. utilisation must fall
below ~50% while at half rate.
If
is inside budget and
remains non-zero, inspect CPU submission,
synchronization and compositor timing in a trace. That symptom alone does not
prove a CPU bottleneck. The worked half-rate example above assumes the stated
metric definitions and no intentional SpaceWarp mode; verify both first.
Select a lever from the measured bottleneck
Read
references/rendering-playbook.md
when choosing multiview, render scale,
foveation, layers, assets, shaders or reprojection. It links each experiment to
its prerequisites, visual failure cases and target-device evidence.
- Dynamic Resolution — tune render scale through the engine/SDK's supported
control path; verify actual behavior rather than assuming the OS enables it.
- Foveation — distinguish fixed from eye-tracked; test supported levels and
quality. A simple shader can cost more with FFR than without it.
- Refresh rate — request a rate the representative workload sustains; observe
actual runtime and thermal behavior rather than promising an automatic rate change.
- Application SpaceWarp — a supported depth/motion-vector integration with
artifact and latency tests, not an automatic rescue or guaranteed percentage gain.
Capture chain — what each tool answers
| Question | Tool | How |
|---|
| Am I missing frames at all, and where | logcat VrApi stats (, , , ) | / |
| Live numbers inside the headset | OVR Metrics Tool ( APK) | metavr tools install ovrmetric
, then enable its overlay |
| CPU vs GPU, thread timeline, stalls | Perfetto | MCP → → , or the CLI equivalents |
| Which C/C++ functions burn the CPU | simpleperf | Android NDK's simpleperf against the running package |
| Which draw call, shader or render stage burns the GPU | RenderDoc Meta fork | metavr tools install renderdoc
; draw-call trace, render-stage trace, Vulkan shader stats |
Meta ships task skills for two of these —
and
, plus
for general on-device debugging. When
they are installed, this skill decides
what to measure and they drive the tool.
references/capture-playbook.md
holds the exact sequences, including what to
capture for a report that another agent can act on.
Method, so a second capture means something
- Reproduce on a release build on a real headset. A simulator run
(, ) checks supported simulated behavior, not headset performance.
- Note refresh rate and the resulting budget before looking at anything else.
- Capture with the app in the worst scene, not the menu.
- Change one thing; re-capture; compare ms, not impressions.
- Record the pair (before/after , , scene, build id) in the
project's own notes — a performance claim with no capture is an opinion.
Gotchas that cost a debugging round
- A thermally throttled headset invalidates the comparison. Long sessions
drift; let it cool or state the temperature state with the number.
- Dev-mode overlays and logging cost frames. Measure with them off, then
turn them on only to read the numbers you cannot get otherwise.
- Counter semantics depend on capture/runtime mode. Inspect timestamps and
reprojection state before interpreting a stale-frame count as dropped work
or pipeline latency.
- Draw calls are a CPU cost and fill is a GPU cost. Cutting draw calls on a
fill-bound frame moves nothing, which is why the capture comes first.
- WebXR has its own counters — draw-call metrics in the browser docs; see
before applying native advice to a web build.
Frame budgets, the 65%/18.05 ms worked example and the recovery mechanisms were
read from documentation/native/android/os-missed-frames.md
on 2026-09-20;
re-verify with metavr docs search "missed frames"
.
Triggers
More phrases that route here: "profile the headset" / "профилировать шлем", "app is hot" /
"шлем греется", "judder" / "джаддер", "GPU bound" / "упёрлись в GPU", "perfetto trace" /
"трейс perfetto", "simpleperf", "RenderDoc", "OVR Metrics", "stale frames", "SpaceWarp",
"foveated rendering" / "фовеальный рендеринг", "72 vs 90 Hz".