The split of an image variant into `payload` and `usage` is now what sharing keys on. A registry holds the texels an archive entry decodes to — the mip chain in one allocation, laid out as an upload wants it — and the `Image` is built over them where the row is placed. Two materials that want the same file under a different colour space or sampler cost one read, one inflate and one decode, and the second of them a copy of the chain; the last asker copies nothing, because the decode's own buffer becomes the image's. Asking whether a name is a cubemap reads the thirty-two byte IWI header out of the entry instead of inflating the megabyte behind it. The resident-bytes ceiling is charged per payload rather than per batch, so two batches pointing at one buffer hold one allocation between them. Load rows split `produced_bytes` into `prepared_bytes` and `reused_bytes`: only the first is work, and their sum is what the plan served. The merge census names who won a disputed claim by plan id, not only that somebody did. The frame clock closes the row and opens the next at the top of `First`, before anything else runs, so every main-phase interval falls wholly inside one frame; the frame the process exits from is closed at exit and its row carries `partial`. Which spans are a frame's coverage roots is declared by `Span::coverage_root` rather than read off a thread's stack — an executor is free to run a schedule's `begin` and its `end` on two workers — and spans a migration left behind are pruned. A counter measured on another thread is charged to the frame it ran in through `emit_at`, and a sample that finds no row is counted rather than moved to whichever frame is open. `IW4L_PIPELINED_RENDERING=1` runs the render world a frame behind on its own thread, and it is the only thing the switch moves; the manifest records which branch a run took. The report gains `main/render overlap`, read off the intervals rather than off the frame a render stage names as its origin. A `bevy-trace` build writes bevy's own spans as a Chrome trace, which is the only place the render graph's submit can be timed from, and `scheduling.bevy_tracing` says whether a run was one. Glass keeps its shattered material while damaged, scales the authored UV to it, picks its shatter effect by whether the piece is landing, and carries the surface normal on the presentation event. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4.1 KiB
make bench — three reports, one run, one package
IW4L_BENCH=1 turns on an in-process recorder. At exit it prints three
independent reports to iw4l-artifacts/bench/<stamp>.txt. None needs a trace
file; any can be MISS while the others stand.
make bench demo0011 # play a demo (a goal is always a demo name)
make bench ZONE=mp_boneyard # a live map instead
IW4L_BENCH_JOBS=assets make bench demo0011 # a load_jobs row per asset
IW4L_BENCH_FRAMES=0 make bench demo0011 # no per-frame table
[1/3] Map load — the waterfall from the shell command to a playable map, the
load-pool stages, then each image plan's wait split from its decode. A stage's
own duration says nothing on a parallel walk, so the table carries sole_open:
how much of the window that stage was the only thing running. It is not a
bound on what deleting it would save. A stage's RSS delta is the whole process,
so the deltas do not add up. Match audio is split by decoder, in worker time.
[2/3] Frame time — every typed span from PERF.md as a tree, for
the frames after the first playable one, then the rows: first seconds against
steady state (exact percentiles), the worst frames, and unclassified — wall
minus the union of the frame's declared top-level spans, so two threads
inside one nanosecond cover it once and the remainder is never clamped. A span
is charged by intersection: a task spanning four frames shows its part in each
and the rest as carried in. The frame clock closes and reopens at the top of
First, before anything else runs, so every main-phase interval falls wholly
inside one frame; the frame the process exits from is closed at exit and its
row carries partial. Which spans are roots is declared, not read off a
thread's stack — an executor is free to run a schedule's begin and its end
on different workers, and the report counts those as migrated. Nesting is
still observed, and the report counts frames where a span outlives its parent.
main/render overlap is the part of the wall a main schedule and the render
thread were both inside, from their intervals. It is zero whenever rendering is
serialised, which is the default; IW4L_PIPELINED_RENDERING=1 runs the render
world a frame behind on its own thread instead, and scheduling in the
manifest says which branch a run took.
[3/3] Counters — render stages, the bodies inside Present and Ui, the
HUD schedule gaps, GPU passes, per-frame work. A *_schedule_interval is a gap
between two systems, which the executor is free to fill: compare it against
hud_tess_body, never add the two. A counter nobody sampled prints MISS, never
0.00; GPU passes overlap and resolve late, so they are never summed.
The run's own directory, beside the trace.pftrace:
report.txt— the three reports above;manifest.json— what the run was: revision and dirty-patch hash, binary and demo SHA-256, toolchain, adapter, window, render target and the viewport the passes were sized by, pools, affinity, env toggles, cache state by kind. An absent fact isnull, never a default;summary.json— spans, counters and audio totals as data;frames.csv— one row per frame: each span's overlap with it, every counter,covered_ns,main_covered_ns,carried_in_ns, flags, thread slots. GPU counters aredelivered_*: a timestamp arrives frames after the work that queued it;load_jobs.csv— one row per load job:discovered → plan_ready → enqueued → started → decode_started → finished → joined.budget_wait_msis a worker held by the image memory threshold,decode_msthe work;prepared_bytesis what the job decoded itself andreused_byteswhat it served out of another job's payload without decoding anything, against what the merge kept and threw away. Onlyprepared_bytesis work.
A diagnostic build with --features bevy-trace writes Bevy's own tracing
spans — the ones inside the render graph, queue_submit among them — as a
Chrome trace. It costs the frame it measures, so it is never a timed run;
scheduling.bevy_tracing in the manifest says whether a run was one.