Merge opt-in tracing and University performance improvements

# Conflicts:
#	Cargo.lock
#	crates/skate-game/Cargo.toml
#	crates/skate-game/src/retail_character.rs
This commit is contained in:
chazmm committed 2026-09-09 22:25:54 +01:00
commit c8219a54cd
37 files changed
+4174 -135

No files matched your search

+130
View File
@@ -0,0 +1,130 @@
# CPU follow-up after v8
The user confirmed substantially higher FPS in v8 and authorized all three next
targets. The new 20-second v8 capture completed without dropped events and confirmed
bindless materials enabled. Inclusive CPU scopes measured main opaque encoder
finish at 1.47 ms/frame, directional shadow visibility at 0.52 ms/frame, and
controller polling at 0.39 ms/frame. These costs overlap and are not promised savings.
## Resource reuse in material groups
The vendored Bevy allocator now chooses the fitting slab requiring the fewest new
resources, preferring an already fuller slab on ties. Previously it chose the
first fitting slab, even if a later slab already contained all the textures.
This targets duplicate texture references and the associated render-command
resource tracking. Capacity remains 64 for retail materials; shader code is
unchanged. Allocation, reference counting, retirement and GPU uploads retain the
existing implementation. Selection performs more work when materials are loaded
or replaced, not on steady-state rendered frames. An FPS benefit still requires
a matched capture; this is not a guarantee of fewer total slabs on every workload.
The explicit Vulkan test exercises the real allocator with GPU texture identities:
fill two slabs, free the first, reuse the second's resident textures instead of
duplicating them into the first, then retire and reallocate the resources.
## Static shadow spatial index
Retail map meshes receive a static-shadow marker. A cached binary bounding-volume
hierarchy rejects groups outside a directional shadow frustum. Each leaf still
uses Bevy's original per-mesh OBB test with near-plane rejection disabled and far
rejection enabled. Node bounds conservatively enclose transformed mesh bounds;
node render-layer unions also reject unrelated lights early.
The index rebuilds when relevant transforms, bounds, inherited visibility, render
layers or membership change. Camera movement changes queries, not the static tree.
Unmarked/dynamic meshes, missing bounds/transforms, explicit visibility ranges and
NoFrustumCulling use the ordinary per-mesh semantics. Shadow visibility is still
marked through deferred commands, retaining ordering relative to point lights and
newly hidden entities. No geometry, shadow distance, cascade count or visuals change.
Tests compare results with upstream Bevy across mesh movement, visibility and layer
changes, exclusion removal, disabled lights and entity retirement; another compares
the tree against flat OBB checks over 4,096 rotated/scaled boxes. Schedule replacement
is tested separately. A data-only University benchmark uses real material bounds
and representative shadow frusta; its result is not whole-frame performance.
## Controller metadata cache
XInputGetState still runs every host frame for every eligible slot. Raw packets,
packet-number handling, fixed-tick publication and simulation cadence are unchanged.
Only the controller subtype obtained from XInputGetCapabilities is cached. State
errors/disconnects and filtered-out slots invalidate it immediately. Metadata is
also refreshed once per second so a hot swap cannot retain a subtype indefinitely
if Windows never reports a disconnect. Failed refreshes remain errors and are
retried, never stored as successful data. Menu-only polling retains its original API.
This removes redundant capability queries; the 0.39 ms total polling cost also
includes state queries, so it is not all recoverable by this change.
## Verification scope
Headless Vulkan main/shadow tests retain the v8 pipeline-crash regression and 8x
MSAA pixel readback. The game and native gameplay are never launched by the agent.
Existing v8 and recovery binaries remain available. Final frame-time acceptance
requires the same scene/settings and route in the private v9 build and v8.
Final targeted checks passed: 47 ordinary tests (29 input, 3 shadow index/schedule,
11 world adapter, 2 scene lifecycle, 2 shader/descriptor), plus both GPU probes in
bindless and fallback modes. The real allocator reuse/retirement assertions run
inside those GPU probes. The explicit University inventory also passed geometry
preservation and indexed-versus-flat visibility equality.
For 4,840 static retail batches and 1,200 representative shadow queries, the final
single-thread kernel benchmark measured 22.1027 ms flat versus 4.1048 ms indexed
(81.4% lower query time), with a 1.8785 ms index build. It excludes ECS refresh,
dynamic casters, deferred visibility writes and the upstream parallel scheduler;
it is not an 81.4% reduction in whole shadow-system time or frame time.
The private v9 static-CRT release compiled from
`1c62e56fd62f5ffe7f4eee21d3b272db00d88c16`. Embedded revision, PDB presence and
system-only DLL imports were verified without running gameplay. EXE SHA-256:
`c83d68d2ccb344a353ffe9a27896966a2c0a9f541cb594e18adaf65693b4e4e2`.
`TRACE-UNIVERSITY-CPU-FOLLOWUP.bat` selects v9; the earlier CPU-FIX launcher keeps
v8 for comparison. The new capture is armed until F9 and records 20 seconds.
Existing compiler warnings remain. Runtime appearance and FPS acceptance are
pending the user's test; the agent's GPU runs were isolated headless tests only.
## v10: restore the measured shadow baseline
The completed user captures `university-cpu-bindings-v8-8702-22955.json` and
`university-cpu-followup-v9-5644-32244.json` each cover 20 seconds without dropped
trace events. Median frame time improved from 4.578 to 3.976 ms, but mean frame
time increased from 4.601 to 4.806 ms and p99 from 7.775 to 21.713 ms. These are
not matched camera routes; neither the aggregate improvement nor the hitches
can be attributed conclusively to a particular change.
Directional shadow system plus deferred-command time increased from approximately
0.525 + 0.036 = 0.561 ms/frame in v8 to 0.573 + 0.243 = 0.816 ms/frame in v9.
The indexed implementation repeats visibility publication across cascade lists.
A candidate using a reused entity set and a bulk mutable query passed visibility
parity tests but did not improve a complete headless schedule benchmark: 0.0610
versus 0.0590 ms/update for v9. That candidate was discarded.
The same fixture measured upstream at 0.1410 ms/update, which disagrees with the
relative result in gameplay. It uses actual University bounds but fixed frusta,
two overlapping views, no moving casters and an otherwise idle ECS schedule.
This makes it useful for detecting local overhead, not choosing the fastest
production path or predicting FPS. The inventory now includes full schedule
measurements, including deferred writes, alongside the older query-only kernel.
v10 restores upstream Bevy directional shadow visibility by removing the custom
system installation/replacement and the retail entity marker. The index remains
compiled only for headless tests and experiments. Material resource reuse,
controller metadata caching and the v8 bindless shader fix remain active.
Shadow geometry, quality and distance are unchanged. This is a conservative
return to the earlier measured path, not a claim that it resolves every hitch.
Visibility parity tests now also reset and verify per-entity ViewVisibility after
each lifecycle change, in addition to comparing cascade membership. The private
v10 launcher is `TRACE-UNIVERSITY-SHADOW-FIX.bat`; v8 and v9 remain available.
A matched user capture is required to determine the actual frame-time effect.
v10 verification: all three targeted shadow tests and the University data-only
inventory passed, including exact preservation of 1,645,617 triangles. The static
CRT release built from `54b8f989ca74b62b0c3ff6094f60f414f43a0983`; embedded revision,
PDB presence and system-only DLL imports were verified. EXE SHA-256:
`e21c5de6d17f117ee512a2197bcc6443bd2c1c9bb8de775e131b5368805d2154`.
No game was launched. GPU shaders were unchanged; v9 GPU probes were not repeated.
Existing compiler warnings remain. Runtime FPS acceptance awaits the user capture.
+142
View File
@@ -0,0 +1,142 @@
# CPU material binding experiment
## v8: repaired material-array shader
The v6 native access violation was reproduced without launching gameplay: an
isolated Vulkan pipeline compilation using the v6 material fragment shader exited
with `0xC0000005` at `Device::create_render_pipeline`. The test used the same RTX
5090, NVIDIA 616.56 and Vulkan backend reported by the user. The original shader
compiled in the same harness. Inlining the v6 shading body also compiled, narrowing
the trigger to that shader's function/resource-argument structure rather than map
simulation or residency. This does not establish a specific driver instruction
or rule out additional issues in the full game.
v8 retains material-array batching but replaces the large `shade` function taking
textures and samplers as arguments. Sampling helpers now receive only the material
slot and coordinates; they access global texture/sampler arrays at the sampling
site. One shading body serves both bindless and fallback paths. Existing shading
operations, texture selection, alpha rejection and user settings are retained.
Validation on that GPU passed for both bindless and fallback modes:
- Native compilation of the main and shadow pipelines.
- Offscreen main-pass drawing with 8x MSAA, command submission and pixel readback.
Material slots 0 and 1 produced the expected `[64,64,64,255]` and
`[191,191,191,255]` pixels; fallback produced the expected first value.
- Six Naga shader variants and the Rust material index/buffer contract.
- Eleven world adapter regressions.
The GPU probes use real retail shaders and Bevy vertex interfaces, with small
fixtures for unrelated lighting resources. They validate the reproduced pipeline
crash and basic material-array execution, not the entire Bevy render graph or game.
The two GPU tests are ignored by ordinary test runs and require explicit opt-in.
`SKATE_SHADER_PROBE_FALLBACK=1` selects the fallback probe; the default tests the
array path. The test-only `SKATE_SHADER_PROBE_SOURCE` can point to a directory with
`v6-retail_world.wgsl` and `v6-retail_material_bindings.wgsl` extracted from `bea625f`
to reproduce the old compiler crash using the main pipeline probe.
CPU batching is enabled again in v8. No FPS gain is claimed until a matched user
capture measures main opaque encoder finish, total command generation and frame
times. No gameplay was launched during this work.
Private v8 release build `de37c6e16722e2e68259e458cc12d5f78aeb71cb` compiled
with static CRT. The embedded revision, PDB presence and system-only DLL imports
were verified without executing the game. EXE SHA-256:
`b5c6d46b5861c3e34d75d47e061318f3ea3b69b7c8cb2aca87074fd7b2621532`.
`TRACE-UNIVERSITY-CPU-FIX.bat` launches this build against the existing assets and
records 20 seconds after F9. The former bindless launcher also selects v8;
the recovery launcher still selects v7 for comparison. Existing compiler warnings
remain. Headless GPU checks above passed; gameplay/FPS acceptance is still pending.
## v6/v7 history
**Withdrawn after the user's runtime test.** Build `bea625f` exited with native
access violation `0xC0000005` about 11.4 seconds after launch, shortly after map
initialization, on an RTX 5090 using Vulkan. The report contains no native stack
or module map, so it cannot establish the faulting subsystem or blame the driver.
The binding-array change is treated as a suspected regression, not a confirmed
root cause. Passing Naga tests did not establish runtime safety.
The recovery revision restores the exact pre-v6 main/depth shader source and
original uniform, texture and storage bindings. It removes the array shader
library and adds a regression assertion that RetailWorldMaterial has no bindless
descriptor or slot count. The v5 shadow proxies remain disabled. Recovery does
not claim an FPS improvement or a confirmed crash fix until the user runs it.
The remaining sections describe the withdrawn experiment for reference.
Recovery build `0b89cb8f47838bb1b7532a88f4bd3822c6d26ec6` compiled in release
with static CRT. Both shader regression tests passed (three original shader
variants plus the non-bindless material contract). Its embedded revision and
system-only DLL imports were checked without gameplay execution. SHA-256:
`ef9b2c8f0b51d80e43f2928eb3b7aba948fff4e4225d91c78435b38699a30b2e`.
The private v7 recovery launcher is ready; the former bindless launcher now also
points to v7. The v6 binary remains preserved but is no longer selected by it.
The user reports poor FPS while looking into University and good FPS looking out
of the map, still CPU bound. The map being resident does not establish that every
triangle is drawn. The captured CPU render work is the actionable bottleneck.
The v4 audit capture `university-audit-7802-28261.json` recorded 4,918 frames in
20 seconds without dropped trace events. Mean command generation was 1.782 ms per
frame; the main opaque encoder finish accounted for 0.995 ms, versus 0.349 ms in
opaque draw traversal. The v5 shadow batch capture
`university-shadow-batches-12344-4318.json` recorded 3,309 frames without dropped
events. Main encoder finish remained 1.012 ms per frame. Shadow visibility fell
from 0.466 to 0.349 ms, but that did not address the dominant main-pass cost.
These are different workloads, not a controlled performance comparison.
A discarded CPU wall-occlusion prototype split at most 256 coarse batches and
used conservative whole-AABB occlusion against real opaque wall triangles. Its
static inward view culled only 55 of 3,512 candidates while costing about 1.15 ms.
It was removed before shipping; adding that CPU cost was unjustified.
## Implementation
RetailWorldMaterial now uses Bevy's bindless material allocator, with a resource
limit of 64 per slab. Materials share texture/sampler arrays and a packed parameter
buffer. The existing shared frame/shadow state remains a buffer reference. This
reduces distinct material binding groups and allows compatible meshes to share
multidraw batches. Pipeline, mesh allocation and resource capacity still constrain
batching; this is not a promise of a 64-fold draw reduction.
The main and depth/shadow shaders use the material slot stored in Bevy's mesh
instance data. Both retain the original bindings when Bevy reports that bindless
resources are unsupported. The common shader module is permanently loaded using
Bevy's shader-library macro so custom imports resolve at runtime. The shading
body, alpha rejection, geometry and texture content are preserved. No user
graphics setting, native physics, input or simulation cadence changes.
The unsuccessful v5 extra shadow meshes and their light-layer adjustment are
removed from the active renderer. Normal world meshes cast shadows again. The
data-only shadow experiment remains test-only for historical reproducibility.
Trace configuration includes `retail_bindless`: true means the device supports
the selected material-array path, false means fallback, null means device data
was unavailable. This indicates capability selection, not measured draw counts.
## Validation and acceptance
GPU-free shader tests compose the actual material shaders and real Bevy vertex
interfaces, using fixtures for unrelated lighting resources/functions. They
validate six combinations: main, shadow depth and depth with normal/motion output,
each with bindless enabled and disabled. A separate test checks the Rust-generated
resource index table and buffer bindings against the shader contract.
The release shader tests passed (both tests, six shader variants). Eleven world
adapter tests and two scene lifecycle tests passed; the offline map inventory
test remains ignored in this ordinary regression run.
Four tracing regression tests also passed, for 19 targeted tests in total.
Runtime pipeline creation, appearance and FPS remain unverified until the user
runs the private executable. The agent does not launch gameplay. Compare v4 and
v6 from the same stationary inward-facing view with identical settings, then the
same route. Inspect `retail_bindless`, main opaque encoder finish, total command
generation and frame percentiles. Keep the earlier executables for rollback.
The private v6 static-CRT release executable compiled successfully from
`bea625f9878148347e0fda0529bd0d6ba7b1a327`. Its embedded revision, matching PDB
presence and Windows-system-only PE imports were checked without executing it.
EXE SHA-256: `cd52c92a709372f95a41d27f6fea794867c1e651f9b8f9d693662ff898dc7325`.
The local `TRACE-UNIVERSITY-BINDLESS.bat` launcher uses the existing extracted
University/assets and captures 20 seconds after F9. Earlier builds remain intact.
Existing compiler warnings remain; this build does not establish a measured FPS gain.
+180
View File
@@ -0,0 +1,180 @@
# Advanced performance capture
## Normal launch: tracing is off
Double-clicking `skate3rust.exe`, or running it without `--trace`, starts normal
play. It creates no performance capture file or export thread and installs no
per-frame trace sampling systems or GPU profiling plugin. Ordinary event logging
and crash reporting remain available. Trace support is compiled into the executable,
but its recording spans are disabled by default.
Tracing is an opt-in CLI diagnostic, not a graphics or gameplay setting. The private
`TRACE-*.bat` launchers enable it by supplying the flags below; launching the same
executable directly does not inherit those flags. F9/F10 capture controls are only
installed for a tracing launch. After a diagnostic session, relaunch without the
flags for normal play; completing a capture does not uninstall its instrumentation.
## Enable a capture through the CLI
Windows releases built from this revision include the recorder. No Rust, engine rebuild,
profiler installation, network listener or menu setting is needed to **record**.
From a terminal in the installed release directory:
```bat
skate3rust.exe --trace university-cpu.json --trace-wait --trace-seconds 30
```
Load University normally, settle at the area you want to measure, press **F9**, and
skate for 30 seconds. **F10** stops and exports early. Continue playing or close
normally; the capture does not terminate gameplay. Use a new output filename for
each launch: existing files are never overwritten. The destination directory must
already exist and be writable. Relative filenames resolve from the working directory.
For GPU pass diagnostics, repeat the same route and settings in a separate launch:
```bat
skate3rust.exe --trace university-gpu.json --trace-wait --trace-seconds 30 --trace-gpu
```
For startup/loading, omit `--trace-wait`. For unattended delayed recording use
`--trace-delay 60` instead of `--trace-wait`. Delay is measured from recorder
initialization, not map readiness. Duration defaults to 30 seconds (range 1–600);
delay range is 0–600 seconds. There is one capture per process. F9 cannot restart
a completed recording. Trace modifiers without `--trace` are rejected. Existing
`--assets` and `--map` options can select prepared data as usual.
CPU slices shorter than **25 microseconds** are omitted by default to keep high-FPS
captures useful within the file-size bound. Counters and metadata are not filtered.
Use `--trace-min-us 0` for an unfiltered, usually much shorter capture, or a value
up to 1000 for a coarser timeline. `capture_complete` reports `min_span_us` and
`filtered_short_spans`, separately from queue losses. Slice counts and summed slice
durations exclude those short scopes; do not treat the remainder as all CPU work.
Open the completed JSON using **Open trace file** in [Perfetto](https://ui.perfetto.dev/).
Select a CPU slice to see its duration; search for `physics::advance`,
`fixed_collision_and_solve`, `fixed_animation_graphs`, `prepare_mesh_bind_groups`,
`prepare_windows`, or `present_frames`. Expand counter tracks for frame intervals
and render pass diagnostics. Load after recording to avoid viewer GPU contention.
Nothing in the recorder uploads captures. Share the JSON file explicitly when needed.
## What the measurements mean
- CPU thread slices use Bevy 0.18.1's built-in ECS/system, schedule and render
instrumentation plus startup/map/physics subspans. They measure **wall time inside
a scope**, including waits and descheduling, not exclusively CPU execution time.
Worker threads have anonymous numeric IDs. Nested slices must not be summed as
independent work. Re-entered spans emit separate intervals. Scopes crossing a
capture boundary can be absent; partial frames should be excluded from analysis.
- `frame_interval_ms` measures successive main-app `First` samples. It includes
pacing and scheduling effects; it is not a monitor presentation/scanout measurement.
`fixed_schedule_iterations` counts fixed schedule loops, not successful physics
steps. `fixed_period_ms` reports the existing native-derived timestep. The main
sample duration stops at the trace system in `Last`; unordered `Last` systems,
including the FPS pacer, may fall outside it. Use their CPU spans separately.
- Render preparation, command encoding, queue submission and `present_frames`
**are CPU intervals**. A long `prepare_windows` can reflect waiting for the GPU
or presentation rather than expensive mesh preparation. Main and render apps
are pipelined, so their work need not correspond to the same displayed frame.
- With `--trace-gpu`, `render/.../elapsed_gpu` counters are **measured GPU timestamp
durations in milliseconds**, when the device supports them. `elapsed_cpu` is
CPU pass encoding time. Other render counters are pipeline invocation statistics.
Results arrive asynchronously and are sampled only when a new diagnostic arrives.
They are plotted at CPU receipt time, **not on a calibrated GPU timeline**; do not
infer exact GPU start times or associate them blindly with that main frame.
Anonymous series IDs distinguish views; only recognized pass names are retained.
- Bevy's instrumented passes are covered, with added diagnostics for the custom
retail exposure meter and tone pass. This is not complete GPU-frame coverage:
other custom passes may be uninstrumented, and nested pass durations can overlap.
Never add all counters to claim total GPU frame time. Missing GPU counters mean
unavailable measurements, not zero GPU cost.
- Asset counters show main-world asset entries, not VRAM usage. Map meshes/images
use `RENDER_WORLD` ownership and can leave those main-world collections after
upload. Treat counts as lifecycle clues, not resident memory or draw-call counts.
- Metadata includes the existing build ID/revision, engine versions, map fingerprint,
initial map counts, timestep, numeric adapter IDs/backend, window dimensions,
camera target sizes/MSAA and graphics settings. Map names, source filenames,
local asset paths, player names, environment dumps, CLI arguments and log payloads
are omitted. The map fingerprint identifies content without publishing its path.
## Bounds, overhead and failure behavior
The writer runs on a separate thread with an 8,192-event queue. Full queues drop
events instead of waiting for disk; `capture_complete.args.dropped_events` reports
loss. A nonzero count makes the timeline incomplete. The event body is limited to
128 MiB, plus a small JSON header/footer, and ends early with `size_limit` if reached.
The time limit also exports while gameplay continues. A normal return, startup
error or Rust unwind drops the guard and finishes the file. Forced process termination,
native crashes, power loss or disk errors can leave incomplete JSON; this is not a
crash-safe journal. The crash supervisor retains its existing independent behavior.
CPU recording costs timestamps, span bookkeeping, JSON values and queue operations;
export costs CPU and disk bandwidth. GPU queries/readback and pipeline statistics
add overhead. This implementation has **not** had its overhead benchmarked in-game.
Compare CPU-only and GPU-enabled runs, and treat heavily instrumented results as
diagnostic evidence rather than baseline FPS. GPU instrumentation remains installed
for the entire `--trace-gpu` launch, including while waiting and after export. CPU
span subscription also remains installed after export, although event creation stops.
Relaunch without tracing for normal play. With no `--trace`, no recorder, writer,
sampling systems or GPU diagnostics are installed; the log subscriber rejects spans
before recording. Built-in trace callsites remain compiled but disabled.
The application retains its existing Vulkan device configuration. GPU timestamp
and pipeline statistics support depends on adapter features and drivers; the trace
does not force unsupported features or change device limits. Bevy 0.18.1 supports
these diagnostics on Vulkan/DX12, and internally bounds timestamp/query capacity.
CPU capture works when GPU measurements are unavailable. No textures, shaders,
resolution, culling policy, density, animation cadence or simulation timestep are
changed by the launch flags. Do not combine this workflow with the older
`SKATE_PERF_REPORT`/camera-sweep benchmark environment options: that is a separate
benchmark mode with different controls and collection semantics.
## Developer support and tooling choice
The package always enables Bevy's `trace` and `debug` features; `debug` retains ECS
diagnostic names and does **not** select Cargo's debug profile or change rendering.
The normal static release build
(`cargo build --release --locked -p skate-game --bin skate3rust --no-default-features`)
therefore supports the CLI. No separate `trace_chrome` or `trace_tracy` build feature
is necessary. The custom subscriber retains event logging and the existing panic
hook while filtering timeline and log layers independently; `RUST_LOG` controls
logs and cannot silently remove CPU trace systems.
Bevy's stock Chrome backend is convenient but its default formatter includes span
fields and lacks our capture controls/privacy bounds. Tracy offers live CPU/GPU
correlation and richer analysis, but requires matching capture/viewer tooling and
an alternative instrumented build. It is a developer escalation, not a requirement
for distributed capture. RenderDoc is useful for inspecting actual draws, resources,
pipeline state and overdraw on Vulkan; replay/capture overhead means its numbers
should not be substituted for an ordinary frame-time profile. Capture only after
the user launches the game. wgpu API tracing is primarily reproduction/replay of
GPU API traffic, not a replacement for CPU or hardware duration profiling, and may
contain asset contents. It is intentionally not enabled by these flags.
Versioned official references: [Bevy 0.18.1 profiling guide](https://github.com/bevyengine/bevy/blob/v0.18.1/docs/profiling.md),
[Bevy 0.18.1 render diagnostics](https://docs.rs/bevy_render/0.18.1/bevy_render/diagnostic/struct.RenderDiagnosticsPlugin.html),
[wgpu 27 source](https://github.com/gfx-rs/wgpu/tree/v27.0.1), and
[RenderDoc documentation](https://renderdoc.org/docs/getting_started/quick_start.html).
The pinned local Bevy/wgpu source is the authority for this implementation; newer
Bevy documentation uses some different diagnostic names.
For static inventory without starting the game:
```text
cargo run --release --locked -p skate-data --example map-performance-inventory -- MAP.skate
```
This reports texture payload identity, material bounds and a spatial-cell estimate.
For exact renderer canonicalization, the ignored, data-only
`skate_world::performance_inventory::runtime_batches` test accepts
`SKATE_MAP_INVENTORY` and `SKATE_MAP_INVENTORY_OUT`. Neither initializes Bevy or a GPU.
Keep private inventory paths and personal launchers out of published docs/artifacts.
For a capture summary using only Python's standard library:
```text
python tools/analyse_performance_trace.py CAPTURE.json > summary.json
```
The analyser streams the recorder's one-event-per-line format, reports frame-interval
percentiles and inclusive CPU slice statistics, and flags truncation, missing system
names and CPU-only captures. It does not infer GPU execution time from CPU spans.
+97
View File
@@ -0,0 +1,97 @@
# University render batching experiment
**Superseded in the active renderer:** the user's v5 test did not improve the
reported bottleneck. The v6 [CPU material binding experiment](cpu-material-binding-experiment.md)
removes the extra shadow meshes and layer adjustment. The inventory below records
the historical v5 candidate, not the current production spawn path.
The user authorized engine/content visual tradeoffs after the initial exact-output
audit, while keeping resolution, MSAA, user settings and native gameplay timing
unchanged. This first structural experiment targets redundant shadow submission;
it does not yet simplify triangle geometry or lower shadow-map quality.
## What the inventory established
Read-only analysis used the same University v14 package and actual material builder
as the preceding captures. Synthetic image handles in the inventory preserve
canonical texture identity, sampler role and invalid-cube fallback; no GPU starts.
| Quantity | Before | Experiment |
|---|---:|---:|
| Main world batches before visibility | 4,870 | 4,868 |
| Opaque static world shadow casters/batches | 3,414 | 680 |
| Opaque shadow triangles | 1,142,084 | 1,142,084 |
| All main world triangles | 1,645,617 | 1,645,617 |
Exact main-material consolidation saves only two batches (0.04%). It is not the
large main-pass optimization suggested by the original material count. Further
main-pass gains would require a different binding/submission strategy or an
intentional content change, not merely removing unused material metadata.
Opaque shadow batch count falls by about 80%. The remaining 1,414 masked, 12 blended
and 28 fallback world batches retain their existing shadow paths. Consequently,
the static-world caster candidate count is approximately 2,134 rather than 4,868
after main consolidation, before per-view/cascade culling. These are resource
counts, not measured draws, GPU time or an FPS improvement.
The additional proxy meshes contain 1,358,304 vertices and 1,142,084 triangles:
46,304,304 bytes (44.2 MiB) of position/normal/index payload. Allocator and renderer
metadata are additional. This trades memory and some mesh preparation for fewer
shadow candidates and material switches; only a runtime comparison can establish
the net benefit. The 64 m cell size is a conservative initial experiment, not a
measured optimum.
## Implementation
`RetailWorldMaterial::batch_key` compares every current uniform lane by float bits,
texture/sampler-role handles, shared storage, alpha cutoff and culling state.
Source-only metadata no longer forces separate identical retail batches. Blended
geometry retains its old grouping and sorting centers. Batches needing generated
tangents remain separate to avoid changing averaging across old boundaries.
For opaque static retail meshes with no alpha rejection, `retail_shadow_geometry`
assigns complete triangles to 64 m cells by centroid and separates one-sided and
two-sided casters. Bounds come from the full vertex positions, including triangles
that cross cell boundaries. The shadow meshes retain authored positions, normals,
indices and winding. They use two shared opaque depth-capable StandardMaterials,
so they do not bind all the world's surface textures for depth rendering.
Original opaque main meshes receive `NotShadowCaster`; their shadow geometry is
on reserved internal render layer 27. World lights whose masks contain layer 0
also receive layer 27 before visibility processing. Main cameras do not receive
that layer. The layer-28 player-only shadow light is unchanged. This also handles
world point/spot lights; University itself contains no authored map lights.
Alpha-tested foliage, transparent surfaces, unsupported/fallback materials, sky,
backdrops and character meshes retain their prior paths. Collision, rails, physics,
input sampling and simulation cadence are untouched. All proxy entities and meshes
use the normal map asset ownership/retirement path, including abandoned preparation.
## Verification and runtime acceptance
Thirteen release tests passed for the map/render adapter, including new checks for
effective material identity, distinct lightmap/blend groups, full boundary-crossing
shadow triangles, exact normal bits, culling modes, reserved light/camera layers
and proxy retirement. Two scene lifecycle tests and two character-lighting tests
also passed. The separate ignored University inventory test passed exact triangle
multiset equality for both main geometry and the opaque shadow geometry.
No gameplay or GPU was started by the agent. Runtime acceptance still needs the
private build to load and render correctly, especially main-world lighting on the
skater, player shadows on the ground, foliage silhouettes, camera movement and
map transitions. Compare shadow visibility, shadow queue/draw costs, render command
generation, frame percentiles and memory against the preceding build. The previous
EXE/PDB and captures remain available for comparison and rollback.
The capture includes `main_opaque_pass_close` and `main_opaque_encoder_finish` CPU
scopes added in the preceding build. They should identify whether the dominant
main-pass command time is draw traversal or pass/encoder completion before a larger
material binding redesign is attempted. No FPS gain is claimed from the inventory.
Private static-CRT release build `e109a0a` compiled successfully with the existing
shared Cargo target. Its embedded revision and Windows-system-only PE imports were
verified without executing it. The EXE SHA-256 is
`c919bf6d8166c8f6ca65cdb41d598f37e24f10174282dc0e67af33f676099e35`.
The local v5 launcher records 20 seconds with GPU diagnostics after F9. For an
isolated comparison, use the preceding v4 audit launcher first, then v5 with the
same scene, settings and route. Existing compiler warnings remain.
+452
View File
@@ -0,0 +1,452 @@
# University: performance assessment, 9 September 2026
Follow-up: the user subsequently authorized engine/content visual tradeoffs.
See [the render batching experiment](render-batching-experiment.md) for the newer
material census and static shadow batching implementation. The original constraints
and capture findings below describe the preceding investigation.
This investigation preserves the current rendered image and native-derived gameplay
contract. It does not lower resolution, texture precision, sampler fidelity, density,
draw distance, shadows, animation quality or physics cadence. It introduces no LOD.
No game, Steam, recomp or automated gameplay was launched. There is **no measured
FPS gain** and no conclusion yet that University is CPU- or GPU-bound on the user's PC.
Baseline: `57742e0` (the available `origin/main` at worktree creation), Bevy 0.18.1,
wgpu 27.0.1, vendored PBR/core-pipeline patches. Static inventory used the existing
fresh installation's University v14 package, SHA-256
`b63e57a84884e639c5ce433a84834a840ae96bfdacad8f34c457d28211315f42`.
The local `main` ref was older; no checkout or remote ref was modified to change it.
## Evidence and scale
The data-only inventory decoded the existing package and the ignored renderer test
ran the actual `render_texture_ids`, `render_material_ids` and `render_groups`
functions. Neither initializes a window, Bevy application or GPU.
| Quantity | Observed |
|---|---:|
| Package bytes | 281,255,761 |
| Render vertices / triangles | 2,075,425 / 1,645,617 |
| Material records / actual material batches | 8,546 / 4,870 |
| Texture records / byte-identical unique payloads | 2,046 / 2,021 |
| Decoded texture bytes / unique bytes | 772,544,512 / 766,686,208 |
| Rail records | 4,201 |
| Batches wider than 50 m horizontally / their triangles | 2,021 / 1,226,659 |
| Wider than 100 m / their triangles | 624 / 435,401 |
| Wider than 250 m / their triangles | 54 / 35,027 |
| Wider than 500 m / their triangles | 6 / 10,191 |
| Estimated occupied 32 m material cells | 19,604 |
Extent buckets overlap. The cell estimate assigns triangle centroids to an XZ grid;
it is not a measured culling result or a proposed mesh implementation. Existing
installation validation logs separately report 1,133,649 collision triangles,
5,537 query clusters and 27,008 grind primitives. These are existing log evidence,
not new runtime profiles. Decoded texture bytes exclude lightmap half-float
expansion, sampler-role duplication, mip chains, sky/character textures and GPU
allocation overhead. They must not be called VRAM consumption.
## Rendering and map data path
`config.rs::from_env` reads the selected package and computes a map fingerprint.
`skate_world.rs::validate_runtime` validates supported extensions/collision.
`world.rs::setup` takes the decoded map out of `Config`; startup does not retain
the full package permanently. `skate_world.rs::spawn` canonicalizes resources,
reindexes material groups and emits static identity-transform mesh entities.
Textures and meshes use `RenderAssetUsages::RENDER_WORLD`, allowing main-world
payloads to leave after upload. No per-frame map reparse or map-wide transform
rewrite was found in this path.
`map_render.rs::PreparedScene` reserves handles, builds detached scene commands
and publishes at a schedule boundary. `map_transition.rs::start` reads/validates
on a loader thread and constructs rendering alongside physics on another worker.
`commit` retires map-owned entities/materials/images/meshes before publishing the
new world. The decoded transition package is dropped on the worker. Live old and
prepared new worlds coexist during preparation; a temporary memory peak is expected.
GPU extraction/removal is asynchronous, so counting just main-world assets is
insufficient to establish either a leak or complete retirement.
**Culling and batching — highest-priority structural experiment, conditional on
the trace.** Material grouping reduces 8,546 source groups to 4,870 draws' worth of
mesh/material entities before Bevy visibility and batching. It combines distant
geometry with identical material identity into one larger bound. Existing frustum
and optional hierarchical occlusion operate at mesh/instance granularity: they
cannot reject individual disconnected regions inside one visible material mesh.
`graphics_menu.rs::apply` already configures `DepthPrepass` and `OcclusionCulling`;
adding those again is not an optimisation. `vendor/bevy_pbr/src/render/gpu_preprocess.rs`
and the core 3D phase nodes own preprocessing and draw execution.
If GPU invocation counts are high while most geometry is behind buildings, evaluate
spatial subdivision **only for large opaque/masked batches**, with conservative
full-triangle bounds. Keep vertex bit patterns, indices, UVs, normals, tangent
frames, materials and shadow participation unchanged. Test camera cuts and fast
turns; do not accept occlusion false negatives or pop-in. A blanket grid raises the
group estimate about fourfold, potentially worsening queue/binning/submission cost.
Transparent batches cannot be split casually: changing mesh centers changes sort
order, and coplanar/decal order can affect pixels. No split is enabled here.
**Material changes, pipeline traffic and instancing — potentially substantial,
but not established by batch count.** `retail_render.rs::RetailWorldMaterial` has
eight texture/sampler roles, per-material uniforms and a shared storage binding;
it does not declare a bindless resource layout. `RetailKey` adds two-sidedness to
Bevy's pipeline keys; alpha/MSAA/prepass and mesh layout also affect specialization.
`vendor/bevy_pbr/src/material.rs` queues and bins using pipeline/material/mesh keys.
Distinct handles are not automatically one draw merely because they use one shader.
Prepared mesh and waiting-pipeline counters plus system spans now expose the cost.
Investigate bindless/texture-array material access only if queue or binding costs
dominate and hardware limits permit it. Preserve every sampling mode, mip policy,
color-space interpretation and fallback path. Atlas packing is not automatically
lossless: borders, repeat addressing and derivatives matter. The `.skate` path
already bakes world geometry; repeated source props are not currently retained as
one local mesh plus transforms. Recovering instancing must prove all vertex data,
per-instance lighting and material inputs match. No geometry welding, approximate
matching or loss of baked lightmap distinctions is justified.
**Textures/material duplication — existing wins, limited byte-identity headroom.**
`render_texture_ids` hashes width/height/color space/payload then confirms equality,
so hash collisions cannot incorrectly merge images. Image cache keys include the
canonical texture and role. Roles deliberately differ: lightmaps expand to linear
half floats, cube maps need six layers, some data remains mip zero, and other roles
build mips and use different address modes. Combining roles solely because bytes
match can change output. Existing exact payload dedup saves only 5,858,304 decoded
bytes in University. Material canonicalization includes retail definitions as well
as PBR fields; removing apparently unused definition fields needs a full proof that
the build consumes no distinction. Texture compression/reduction is outside this
zero-degradation task unless mathematically lossless and supported end-to-end.
**Foliage, alpha and shader cost — GPU capture required.** Retail materials preserve
two-sidedness; alpha mask/blend semantics and `retail_depth.wgsl` rejection are
significant. `retail_world.wgsl` samples diffuse/lightmap and conditional detail,
normal, macro/decal/specular resources. Samples precede some rejection to preserve
implicit derivatives. The family and texture flags are material-uniform, so these
branches are not necessarily divergent across pixels. Moving a sample after
discard, flattening detail or disabling backfaces can change pixels. Specializing
families could reduce instructions at the price of pipeline variants; measure
`elapsed_gpu`/fragment invocations and inspect generated draws first. Density,
alpha cutoff, texture precision and draw-distance reductions are disallowed.
**Uploads, lifetimes and exposure — narrow safe change made.** The shared shadow,
clock and ocean PCA state in `retail_shadow.rs::upload` already updates an existing
GPU buffer without invalidating every material. This change replaces its temporary
144-byte heap vector with a fixed stack array; scalar order, little-endian bytes,
buffer, write size and scheduling are unchanged. It saves one allocation per upload
call; no frame-time benefit is claimed. The clock/PCA changes each frame, so simply
skipping updates is unsafe. `retail_exposure.rs::upload` writes 32 settings bytes;
`ExposureNode::run` creates two bind groups per view/frame and executes the meter
and tone passes. Caching bind groups keyed by the actual source/target identity is
a candidate if CPU encoding cost matters, but must invalidate on resize/MSAA/target
changes and account for ping-pong views. Added pass diagnostics measure these custom
passes without changing shader math or their order. Exposure's temporal adaptation
must not be decimated.
`render_capacity.rs` already reserves instance capacity with bounded growth before
Bevy writes buffers. Do not replace that with exact-size per-frame reallocations.
Render preparation stalls can also be presentation backpressure; the official
[Bevy profiling guide](https://github.com/bevyengine/bevy/blob/v0.18.1/docs/profiling.md)
specifically discusses this ambiguity. CPU command time is not GPU execution time.
## Simulation, transforms and scheduling
`app.rs` orders fixed input → controls → physics and Update assets → physics
presentation → animation → verification. `physics.rs::advance` uses the native-derived
clock period, runs the monolithic fixed frame and republishes cadence. `physics/frame.rs`
preserves preceding-query consumption, animation/input/state selection, movement,
shared solve and output publication order. Added spans isolate animation graphs,
collision/solve and finishing work. They do not parallelize or reorder any phase.
`skate_world.rs` constructs `BoardWorld` from the embedded retail collision archive.
`skate-core/src/physics/board_world/query_index.rs` already builds a bounds hierarchy
over authored clusters, traverses intersecting nodes and sorts matches back into
source order. Narrow phase checks volumes/triangles; wheels, feet, camera and board/
skeleton assemblies have distinct query consumers. `physics/solve.rs` also handles
dynamic/network bodies. Retail map size alone does not establish fixed-step cost.
The query index allocates result/stack vectors; scratch reuse is a plausible CPU
optimisation if profiles show query pressure. Preserve result order, tie handling,
contact retention and delayed query visibility. Replacing the solver, simplifying
collision, reducing ticks, or changing contact ordering would violate the task.
`grind_world` retains 4,201 authored splines with 27,008 primitives and an octree
provider; it is not a naive all-rails scan for every normal query. Candidate counts
and selection work need focused subspans if the fixed-frame profile implicates it.
`retail_irradiance.rs::sample` already has a six-entry, 2.5-metre hysteresis cache,
then bounds-filtered nearest-probe selection. A spatial index could help cache misses,
but must retain nearest/tie selection and hysteresis exactly.
`presentation.rs::capture` only captures a new pose generation, retains previous/
current snapshots and records replay. `animation.rs` interpolates bone transforms
for display. Snapshot copying and hierarchy propagation are candidates if those
systems dominate, not grounds to reduce bone update rate or interpolation quality.
Static map transforms are separate from these moving character hierarchies.
Multiplayer/mod/vehicle systems are additional workload; profiles should state
whether mods or online play were active rather than attributing all time to University.
`graphics_menu.rs::pace` sleeps in `Last` when capped; window presentation may add
another wait. The new frame-interval counter and CPU spans preserve both behaviors.
Do not remove waits, alter fixed cadence or infer GPU saturation from a capped FPS
number. Timing variation must be investigated without modifying native input/physics.
## Next decision requires a user capture
Use [the capture workflow](performance-tracing.md). Record 30 seconds after loading:
stand briefly, rotate the camera, then skate through a representative University
area. Repeat the same settings/route with `--trace-gpu`; record a separate startup
trace if the problem is loading or first-use hitching. Include where the slowdown
occurred, approximate time in the capture, and whether mods/multiplayer were active.
Do not change quality settings between captures.
Analyse steady-state frame-interval p50/p95/p99, fixed iteration counts, dominant
CPU slices, waits, prepared-resource/pipeline counts, and asynchronously received
GPU pass durations/invocations. A high CPU queue cost favors batching/material work;
high GPU opaque/foliage work favors conservative spatial subdivision or exact shader
specialization; dominant fixed solves favor order-preserving scratch/query work.
First-use-only stalls favor upload/pipeline cache work. Any nonzero dropped-event
count limits conclusions. Broad redesign waits for this evidence and manual visual
verification. No percentage gain or final bottleneck is asserted in this assessment.
## Verification
The Windows static-CRT release compiled successfully using the existing shared
Cargo target cache and an explicit worktree-private EXE/PDB output. The final
executable's embedded source revision was checked against `c709b5d`; PE import
inspection found only Windows system libraries, with no Bevy/Rust DLL dependency.
Four release unit tests passed: option bounds/dependencies, disabled span filtering,
nonblocking full-queue drops, and F9-style activation/stop/valid JSON export with
secret/path/log canaries omitted. The data-only exact-runtime-batch inventory test
also passed. Existing compiler warnings remain; no game or GPU device was started.
Runtime frame timings, capture overhead and visual validation remain user tests.
## First user capture: limited CPU evidence
The user recorded a CPU-only capture with build `c709b5d`. Capture SHA-256:
`6ab11b1e174049507be2506f17d35ef6cd111fea63180ceb82a147c2067cf8d3`.
It contains 1,194,743 events and 668 frame-interval samples covering approximately
3.05 seconds of active recording. The 128 MiB byte bound terminated it early;
the queue reported zero drops. The earlier timestamp range includes the armed
waiting period and must not be mistaken for recorded gameplay duration.
| Measurement | Median | 95th percentile | Interpretation |
|---|---:|---:|---|
| Main-frame interval | 3.59 ms | 7.50 ms | Instrumented, short sample; not scanout or baseline FPS |
| CPU command-buffer generation | 0.83 ms | 4.25 ms | Includes waits/descheduling; not GPU execution |
| CPU main opaque pass | 0.53 ms | 4.18 ms | Nested in command generation; do not add to it |
| Fixed collision/solve | 0.36 ms | 0.45 ms | 183 recorded fixed-step subspans |
| Fixed animation graphs | 0.12 ms | 0.22 ms | Fixed-step subspan, not all animation/presentation |
Frame-interval p99 was 8.20 ms, maximum 9.21 ms and mean 4.57 ms. These values cannot
describe a full skating route or quantify recorder overhead. Prepared mesh/image
counts stayed at 4,927/2,173 over 11 sampled observations; waiting pipelines stayed
at zero. This window supplies no evidence of asset churn or first-use pipeline
compilation. The actual window/camera target was 2560×1369 while requested settings
were 2560×1440, 100% scale, 8× MSAA, uncapped, occlusion off. No setting was changed
to obtain those measurements. Device timestamp support was present, but GPU
diagnostics were not requested, so GPU costs remain unmeasured.
The recording exposed a release instrumentation defect: system names appeared as
`<Enable the debug feature to see the name>`. Consequently, the aggregate anonymous
system time cannot identify particular game or renderer systems. The recorder now
retains Bevy's diagnostic names in release builds and tests a real headless ECS
schedule to catch this regression. A 25 µs default slice threshold limits tiny
scheduler events; applying that filter offline to this old recording would reduce
its retained events from 1,194,743 to 61,607 (about 6.4 MB with the old labels).
That is a file-volume calculation, not a measured improvement in runtime overhead.
Next: repeat with the corrected build and `--trace-gpu`, using the same scene and
settings. CPU command generation is a candidate for further investigation, but
this short trace does not yet support material/culling redesign or a physics change.
Corrected build `2a74581` passed all four release recorder tests, including actual
ECS system/schedule name assertions and the duration-filter boundary. The streaming
analyser reproduced the capture's event/sample counts and limitation flags. The
new private static-CRT release EXE/PDB built successfully; embedded revision and PE
imports were verified. The original recording and executable were retained. No
gameplay or GPU capture was launched by the agent.
## Second user capture: named CPU systems and measured GPU durations
Build `2a74581`, GPU diagnostics enabled, SHA-256
`03d97c8b8cd3980123ad6ea7c6e7c36d9d41ff2c5877077851ce93086b1747de`.
The useful recording covers 24.62 seconds and 6,997 frame intervals. It ended at
the byte bound, with zero queue drops and 12,213,517 intentionally filtered short
spans. This is sufficient for the observations below despite not reaching 30 seconds.
Another small file from this session contains only metadata and no active capture.
Frame interval: mean **3.52 ms**, median **3.11 ms**, p95 **6.33 ms**, p99 **7.11 ms**,
maximum **9.71 ms**. The mean corresponds to about 284 main-app updates per second,
not verified display FPS. Settings and actual target size match the first capture.
The samples cover a different interval and instrumentation configuration, so their
lower timings are **not an A/B performance improvement**.
| Scope | Mean | p95 | Measurement |
|---|---:|---:|---|
| Render schedule | 3.27 ms | 6.07 ms | Inclusive CPU wall time |
| Main app | 2.25 ms | 2.98 ms | Inclusive CPU wall time; overlaps render work |
| Command-buffer generation tasks | 1.27 ms | 3.59 ms | CPU work/waits |
| Directional-light mesh visibility | 0.448 ms | 0.974 ms | CPU system |
| Controller polling | 0.342 ms | 0.443 ms | CPU/OS calls |
| Character-material preparation | 0.124 ms | 0.160 ms | CPU system |
| Physics advance | 0.644 ms | 0.880 ms | Per fixed tick: 1,477 ticks, not per display frame |
| Main opaque pass | 0.191 ms | 0.259 ms | GPU timestamp duration |
| Retail exposure meter | 0.00781 ms | 0.00794 ms | GPU timestamp duration |
| Retail exposed tone | 0.0157 ms | 0.0159 ms | GPU timestamp duration |
CPU rendering/visibility is a stronger optimisation lead than main-pass shading
for this scene. GPU figures cover instrumented passes, not the entire GPU frame;
some anonymized shadow/other pass labels cannot be attributed individually from
this export. CPU scope durations include scheduler and profiler overhead. The
recorder's `end_frame` system itself averaged 0.115 ms; this is only one component
of capture overhead, not its total. No GPU saturation claim follows from these data.
Across 117 resource snapshots there were consistently 4,936 prepared meshes,
2,195 prepared images, and zero waiting pipelines. There is still no indication
of resource churn or shader compilation driving this interval. These counts
differ from the first capture, so the two recordings are not identical workloads.
The visibility source checks two distinct shadow sources: world/skater shadows
for character lighting, and layer-28 player-only shadows received by the baked
world. Removing casters or reducing cascades would change the image. It remains a
substantial lead, but no visibility shortcut is applied without an exact equivalent.
Controller polling calls XInput state and capabilities at the existing cadence;
throttling absent slots or caching subtype data could alter reconnect/input semantics,
so it is not changed under the timing constraint.
One directly supported change is implemented in `retail_character.rs::update`.
It previously used `Assets::iter_mut` to assign SH lighting to every character
material every frame. Bevy emits `AssetEvent::Modified` for those mutable accesses,
including identical assignments, causing needless preparation/binding work after
lighting has settled. The new path reads each material first and only obtains a
mutable reference if any SH float's **bits** differ. It still computes probe choice,
smoothing and shadow state every frame and updates every changing/new material in
the same frame. It uses no tolerance, cadence reduction or shader change. Signed
zeros and NaN payloads are compared explicitly. Reusable scratch storage avoids a
new per-frame allocation. This is expected to help settled-lighting frames; it may
save nothing while every material's lighting changes. No FPS gain is claimed before
a matching post-change capture and user visual test.
Validation of `d3f964b`: both character-lighting release tests passed, covering
unchanged asset events, changed values, newly bound pieces, float bit preservation
and existing shadow-source separation. The private static-CRT EXE/PDB built and
its embedded revision/system-only PE imports were verified. The local comparison
launcher records 20 seconds with unchanged settings; the preceding executable and
capture remain available. No post-change gameplay result is claimed.
## Third user capture: character-lighting change observed
Build `d3f964b`, GPU diagnostics enabled, SHA-256
`eb8a736ec1ad5d453f9b872525d619e1c8a37818b1ad97a51590df4e1a680ac3`.
The recording completed its requested 20 seconds: 785,631 events, 4,750 frame
intervals, zero queue drops and 8,314,179 intentionally filtered short spans.
The file is 89,548,748 bytes, below the capture byte limit.
| Measurement | Previous capture | Post-change capture |
|---|---:|---:|
| Main-frame mean | 3.52 ms | 4.21 ms |
| Main-frame median | 3.11 ms | 4.62 ms |
| Main-frame p95 | 6.33 ms | 6.06 ms |
| Main-frame p99 | 7.11 ms | 6.87 ms |
| Main-frame maximum | 9.71 ms | 74.70 ms |
| Retained character-material preparation time per main frame | 0.124 ms | 0.064 ms |
| Retained material-bind-group preparation time per main frame | 0.070 ms | 0.034 ms |
| Character-material preparation spans at least 25 us / main frames | 6,997 / 6,997 | 2,962 / 4,750 |
| Main opaque GPU mean | 0.191 ms | 0.248 ms |
The reduced retained material work is consistent with suppressing identical SH
asset modifications. It does not mean that the system stopped executing: slices
below 25 us are omitted. During the first five seconds, only 125 preparation
spans were retained across 892 main frames; during the last five seconds there
were 1,232 across 1,232 frames. This fits the intended benefit for settled lighting
and continued preparation when values change, but the trace does not independently
record player movement or SH equality. Headless asset-event tests establish the
bit-exact update behavior.
These are observations across different capture intervals, not a controlled FPS
gain. Graphics settings and the 2560x1369 target are unchanged, but prepared
resources are now 4,939 meshes and 2,187 images instead of 4,936 and 2,195. All 79
render-resource snapshots have those same counts and zero waiting pipelines.
Command-generation CPU mean increased from 1.27 to 2.00 ms, while directional-light
visibility averaged 0.421 ms and controller polling 0.242 ms. Different scene work,
timing and instrumentation overhead prevent attributing aggregate differences to
the small lighting change. No overall speedup or regression is established.
The 74.70 ms frame interval occurs approximately 8.87 seconds into active capture.
Its preceding `PreUpdate` schedule contains a 70.68 ms
`bevy_gilrs::gilrs_system::gilrs_event_system` CPU slice. This locates the delay in
controller-event processing; a wall-time trace cannot distinguish OS blocking,
descheduling or internal processing as the root cause. It is not evidence of a
character-material stall. Input behavior remains unchanged under the native-input
constraint. No polling/cadence workaround is applied.
The streaming analyser successfully processed the complete recording and the
comparison was checked against event-level five-second windows and long slices.
This capture supports retaining the exact lighting-update optimization, with no
claim of an overall FPS improvement. User visual confirmation remains separate
from timing evidence; the agent did not launch gameplay or alter quality settings.
## Follow-up source audit and optimization order
The third capture ranks the next work as follows. Times below are inclusive CPU
wall time amortized over main frames, unless labeled GPU; nested and parallel
rows must not be added. The 25 us filter omits short executions.
| Priority | Area and observed cost | Concrete opportunity / decision |
|---|---|---|
| 1 | Command generation 2.004 ms/frame; main opaque scope 1.772 ms/frame | Draw traversal accounts for 0.418 ms opaque plus 0.141 ms masked. Added `main_opaque_pass_close` and `main_opaque_encoder_finish` scopes to locate the remaining cost before changing batching or submission. |
| 2 | Directional shadow visibility 0.421 ms/frame | Bevy scans eligible meshes for each light/view and tests every cascade. A conservative static-mesh hierarchy or layer-based candidate index could reduce scans without changing final tests. It needs equivalence tests against the existing entity sets across moving cameras, map swaps, dynamic casters and near-plane exceptions. No culling replacement yet. |
| 3 | Controller-event hitch: 70.68 ms in one call | Disable the unused Gilrs backend. Both gameplay and menu navigation use `input::platform::poll`; the repository has no Bevy gamepad-event or rumble consumer. Raw XInput state/capability calls, slot selection, errors and fixed publication remain unchanged. |
| 4 | Raw controller polling 0.242 ms/frame; menu navigation 0.028 ms/frame | Menu and gameplay make separate OS polls. Sharing a snapshot would change their sample time; throttling disconnected slots or caching capabilities changes reconnect/error semantics. Keep this path unchanged under the native-input constraint. |
| 5 | Character preparation 0.064 ms/frame; material bindings 0.034 ms/frame | Retain the existing bit-exact SH update guard. Further gains during changing lighting would require a separate per-character uniform/storage update path, preserving distinct materials and current-frame lighting. That is a renderer design change, not a safe skipped update. |
| 6 | Hidden session effect: material preparation 0.020 ms/frame; specialization checks 0.035 ms/frame | `present` modified its material, scale and visibility every frame even when hidden. Skip hidden material publication; keep noise/time advancement and publish current values before making it visible. Guard identical visible parameters and unchanged scale/visibility. |
| 7 | Render buffer preparation: indirect parameters 0.098 ms/frame; instances 0.086 ms/frame | Existing capacity reservation already prevents routine capacity churn. Static/dynamic partitioning could reduce uploads but needs explicit removal, previous-frame and camera-cut handling. Do not mark moving data static or disable motion history. |
| 8 | Main visibility 0.134 ms/frame; transforms 0.059 ms/frame | Avoid unnecessary component change ticks first (session effect fixed). A shared static hierarchy is a future option; freezing visibility or transforms is not equivalent. |
| 9 | Physics advance 0.152 ms/frame, including collision/solve 0.089 ms/frame | Existing collision hierarchy preserves source ordering. Reusing query scratch is possible but needs per-call ownership/reentrancy and result-order tests. These costs do not justify changing solver behavior or fixed cadence. |
| 10 | Exposure GPU meter 0.00770 ms; tone 0.01548 ms | Preserve both passes every frame. Reuse their CPU bind groups and replace the per-frame 32-byte heap payload with a stack array. No change to dispatch count, uniform bytes or exposure adaptation. |
The source review also checked map construction/transitions, texture deduplication,
world/sky material writes and existing render-capacity management. The world and
sky material loops run at scene construction or an explicit diagnostic keypress,
not continuously. There is no evidence here for per-frame map parsing, ongoing
texture reloads or pipeline compilation. Loading/memory peaks require a separate
startup/transition capture; steady-state timings cannot rank those costs.
### Implemented follow-up changes and limits
Gilrs removal follows the additional consumer audit above. The earlier decision
to leave input unchanged concerned the *used raw XInput path*, which remains
untouched. Removing an unused backend removes this specific stall source; it does
not guarantee that OS/controller stalls can never occur elsewhere. Keyboard and
mouse input plugins remain enabled.
The session effect retains the same 60 Hz noise advancement, including hidden
frames and multi-step deltas. A headless regression test compares its sequence
with continuous reference advancement through hidden/visible transitions and
checks material modification events and the first visible payload. Visible
parameters use exact float-bit comparison; no epsilon or effect quality reduction
is introduced. Hidden materials may deliberately retain older parameters until
the frame that displays them.
Exposure caches at most eight `(source TextureViewId, meter bind group, tone bind
group)` entries inside its Pipeline resource. Buffers, sampler and layouts are
immutable for that resource's lifetime, and replacing the resource drops the cache.
Alternating post-process sources have separate entries. Before rendering, prune
entries against both current source views of every exposure camera, so camera
removal/resizing releases retired textures rather than retaining large targets.
Resized/recreated targets have new source identities. The
destination is a render attachment, not a bind-group input, so it does not belong
in the key. Buffer contents still update every frame. GPU execution/resize checks
remain manual because this task does not authorize the agent to start the game.
The strongest larger opportunity remains reducing CPU rendering work with exact
output equivalence. A blanket 32 m map split would quadruple the estimated groups,
so it is not an appropriate default for this capture. Bindless material access is
a possible later experiment, but must preserve each texture's format, sampler,
role and alpha ordering and cannot be inferred to help from material counts alone.
No FPS gain is claimed for the follow-up changes before a matched user capture.
Validation of `842b4ad`: 14 targeted release tests passed (two session-effect,
four recorder, two character-lighting and six raw-controller tests). The session
tests were rerun successfully after adding exposure-cache retirement. The private
static-CRT release EXE/PDB built successfully using the shared Cargo target; its
embedded revision and Windows-system-only PE imports were verified without running
the game. Existing compiler warnings remain. The ignored v4 comparison launcher
records 20 seconds with GPU diagnostics and the same settings/assets; earlier
executables and captures are retained. Renderer runtime validation, visual checks
and post-change frame timing await a user capture.