mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
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:
commit
c8219a54cd
37 files changed
+4174
-135
No files matched your search
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user