Profile a rendered frame
Amoris exports the render thread's CPU spans and timestamped GPU passes as Chrome Trace Event JSON. Each pass carries a frame ID; clock calibration places GPU work on the CPU timeline. Use this to separate encoding, submission, GPU work, and presentation costs. The data page keeps recorded results and their limits.
Record a repeatable workload
From the repository root, build the example and capture 300 frames after 60 warm-up frames. This shell example selects native Metal on macOS:
cargo build --release -p amoris-render --example many_cubes
AMORIS_BACKEND=metal AMORIS_AA=msaa AMORIS_GTAO=off \
target/release/examples/many_cubes --dense --count 200000 --size 1280x720 \
--headless-bench 300 --occlusion off --prepass auto --settle 2 \
--trace frames=300,skip=60,out=out/profiler/dense-auto.json
python3 tools/frame_trace.py stats out/profiler/dense-auto.json
On Windows, use PowerShell environment assignments and the .exe binary; choose
dx12 or vulkan. For example, after the same Cargo build:
$env:AMORIS_BACKEND = 'dx12'
$env:AMORIS_AA = 'msaa'
$env:AMORIS_GTAO = 'off'
.\target\release\examples\many_cubes.exe --dense --count 200000 --size 1280x720 --headless-bench 300 --occlusion off --prepass auto --settle 5 --trace frames=300,skip=60,out=out/profiler/dense-auto.json
python tools/frame_trace.py stats out/profiler/dense-auto.json
--headless-bench waits for GPU completion after each offscreen frame. This is a
controlled render workload, not game FPS: it excludes window presentation. --bench
instead runs a native window. Keep resolution, antialiasing, GTAO, occlusion, prepass,
scene, build and adapter fixed when comparing runs. On multi-GPU machines,
AMORIS_ADAPTER accepts an index or a name fragment such as nvidia or 780m.
To inspect a game in a native window, set AMORIS_TRACE before launching a built host:
AMORIS_TRACE=frames=300,skip=60,out=out/profiler/sailing.json \
target/release/amoris play samples/sailing
The default trace specification is 300 frames after 60 skipped frames, written to
out/profiler/trace-<backend>.json. Tracing stops after that window. Export drains
pending work and writes the file; the recorded Windows exports cost 45–95 ms.
many_cubes exports outside its measured frames. A game window exporting during play
can visibly pause once. Clock calibration also waits for completion outside the
measured capture window.
Read and compare the trace
Open the JSON in Perfetto using Open trace file,
or in Chrome's chrome://tracing. Inspect the CPU render-thread lane, GPU pass lanes,
and GPU frame spans together. Select an event to see its frame ID. The JSON's
otherData records the backend, adapter, capture settings, missing/dropped GPU frames,
clock uncertainty and whether placement is calibrated. An uncalibrated trace positions
GPU work at submit completion for display; it cannot establish CPU-to-GPU latency.
python3 tools/frame_trace.py stats out/profiler/dense-auto.json
python3 tools/frame_trace.py diff out/profiler/dense-off.json out/profiler/dense-auto.json
For the second command, first repeat the capture with --prepass off and save it as
dense-off.json. stats reports medians, p95, extrema and spreads; diff compares
B against A. Alternate run order and repeat runs before interpreting small changes.
| Metric | What it measures |
|---|---|
| CPU frame / headless wall time | The host's frame bracket, including submission and its explicit wait |
| Frame interval | Time between consecutive CPU frame starts; window pacing differs from headless waits |
| GPU busy | Sum of timestamped pass durations; coverage depends on enabled pass markers |
| GPU frame span | First timestamped pass start to last pass end, including intervening gaps |
| Submit to GPU start | Calibrated signed offset from CPU submit completion to the first GPU pass; it can be negative |
These metrics are not interchangeable. A pass sum does not measure all device activity,
and a browser's uncapped frame interval can reflect submission pace rather than GPU
completion. Tracing enables a timestamp scope around postprocessing; older benchmarks
without that scope exclude it.
GPU execution can begin before the CPU's queue.submit call returns, so a negative
submit-to-start offset is valid.
Backend checks and deeper captures
Metal, Vulkan through MoltenVK and Vulkan on AMD adapters defer query resolve until the sampling submission has completed. Slots retain their queries, buffers, labels and frame IDs until asynchronous readback finishes. D3D12 and the other native Vulkan drivers retain same-submission resolve. On the Radeon 780M with AMD's Vulkan driver, a resolve in the sampling submission read zeros or earlier values, so every AMD Vulkan adapter takes the deferred path as a precaution. The Metal method describes the regression checks, calibration and measured uncertainty.
On Windows, RenderDoc can inspect a single D3D12 or Vulkan frame. Install RenderDoc
and set RENDERDOC_DIR to its installation if the tool cannot find it. Example:
python tools/renderdoc_frame.py capture --backend vulkan --adapter nvidia --frame 70 \
--name dense-vulkan -- many_cubes --dense --bench 120
python tools/renderdoc_frame.py analyze out/profiler/rdc/dense-vulkan.rdc
The analysis reports per-event durations and shader statistics. D3D12 counter replay requires
Windows Developer Mode and an explicit analyze --d3d12-counters; without it, the tool skips those
counters. The recorded per-event replay timings cover Vulkan. Static shader instruction counts are
not counts of instructions executed by a frame. See the Windows profiling
method
for the capture limitations.
Keep raw traces, captures and logs in ignored out/. Reviewed, privacy-cleaned runs can be
published in amoris-benchmarks-results;
the engine repository keeps methods, test fixtures and concise result tables.