Profile and preinitialize viewport rendering
Some checks failed
CI / rust-skia (Rust only) (push) Has been cancelled
CI / required (push) Has been cancelled

This commit is contained in:
2026-08-23 12:16:52 +02:00
parent b59bc4c3cd
commit cb9233c59c
8 changed files with 444 additions and 84 deletions

View File

@@ -0,0 +1,100 @@
# Renderer and viewport performance
Measured on 2026-08-23 with the primary account in Broceliande, a 1024 m x
1024 m varregion, using the default 64 m view distance. The visible scene had
211 objects, 427,829 triangles, and five unavailable grid textures. Release
numbers are the performance baseline; debug timings are intentionally omitted.
## Current capture path
| Phase | Warm release time | Can run ahead? | Required change |
|---|---:|---|---|
| Read current simulator objects and discover assets | 8 ms | Yes | Apply incoming object/avatar/terrain events to a persistent world instead of scanning the simulator maps per capture. |
| Fetch cached/missing assets | 151 ms | Yes | Bounded asset workers continuously fetch visible dirty assets. Raw immutable assets remain in the 2 GiB disk LRU. |
| Decode textures and derive scene geometry | 176 ms warm; 8.03 s on the first scene | Yes | Retain decoded visible textures and derived meshes in memory. Add a versioned persistent derived-mesh cache only for the measured 8 s cold-start cost; decoded RGBA textures should normally remain memory-only because they expand substantially. |
| Collect and sort renderables | 1 ms | Yes | Persistent renderer entities remove this per-frame list build. |
| Validate scene | 1 ms | Yes | Validate assets and changes when admitted, not every frame. |
| Copy render command | 21 ms | Yes | Send stable IDs and dirty updates; do not clone the complete scene for a frame. |
| Re-identify cached textures by content hash | 30 ms | Yes | Key immutable texture resources by grid asset UUID. |
| Re-identify cached meshes by content hash | 14 ms | Yes | Key derived meshes by stable object/asset signature and LOD. |
| Recreate frame entities | <1 ms | Yes | Keep Bevy entities and material handles alive; update only dirty components. |
| Render plus synchronous readback | 46 ms | Partly | Render continuously into a persistent target. A GUI presents that target directly; readback happens only for CPU consumers. |
| JPEG encoding | 41 ms | Yes, and not part of viewport FPS | A separate latest-frame worker encodes only when an LLM/image consumer asks for it. |
The existing end-to-end warm capture takes 525 ms. A repeated renderer call
alone takes 113 ms (8.8 FPS), including 67 ms of avoidable copying, hashing,
and cache synchronization. Its measured render/readback segment is 46 ms
(21.9 FPS equivalent). A persistent GPU scene therefore makes the initial
10 FPS viewport target realistic on the measured hardware.
The 46 ms value still includes synchronous CPU readback. It is not a pure GPU
timestamp. Continuous viewport rendering without readback should be faster and
must be measured separately once the persistent render target exists.
## Startup
The optimized cold-start profile was:
| Phase | Time | Scheduling |
|---|---:|---|
| Client owner initialization | 2 ms | Startup thread |
| Grid login | 3.37 s | Parallel with renderer initialization |
| wgpu/Bevy initialization | 452 ms | Parallel with grid login; completed before agent readiness |
| Test scene convergence window | 30.04 s | World events continue asynchronously; this is not a system-readiness gate. |
| First full visible-scene build from persistent raw assets | 8.35 s | Background asset/scene workers; publish partial complete frames as content converges. |
System readiness must require configuration, renderer initialization (or an
explicitly reported fallback), grid login, region dimensions, and the running
world/update loops. It must not wait until every user-created asset inside the
view radius has decoded: that would turn missing or slow grid assets into a
permanent login stall. Visual readiness is a separate completeness signal.
## Game loop
The agent needs four independent paths:
1. Grid callbacks enqueue compact object, avatar, terrain, region, and camera
changes immediately. They never perform asset I/O or rendering.
2. A fixed update loop drains those events, updates the authoritative CPU world,
recalculates 64 m visibility, and emits stable dirty IDs. Grid event handling
must remain faster than the render tick.
3. Bounded asset workers fetch, decode, and derive only dirty visible content.
Completed resources update the CPU world and enqueue GPU changes.
4. A render loop applies GPU changes and updates a persistent viewport at an
initial 10 FPS target. It retains the latest complete frame. GUI presentation
uses the GPU target directly; snapshot readback, resize, JPEG encoding, and
LLM upload are independent latest-frame consumers.
Simulation/update and rendering use separate clocks. Slow asset downloads,
JPEG encoding, LLM requests, and subscribers must never hold either loop.
### Frame publication and readback
The persistent viewport renders into a GPU texture. A GUI presents it without
CPU readback. When a CPU image is requested, the render graph schedules a copy
after that frame's render commands into the next free staging buffer. GPU queue
ordering makes the copied frame coherent even when the render target is reused
for the following frame.
Use a ring of three staging buffers. Mapping and CPU consumption happen
asynchronously; a buffer is reused only after its completion signal. If all
three are busy, skip that readback rather than stall rendering. Publish only the
latest completed immutable CPU frame, tagged with frame sequence, camera pose,
and observation time. JPEG and LLM workers use that published frame and discard
superseded work. The current synchronous Bevy screenshot plus `device.poll(Wait)`
path is only a diagnostic bridge and must not remain in the game loop.
## Instrumentation
Hot paths record only monotonic start/end timestamps and emit fixed-size,
fixed-cardinality timing signals with `try_send`. A bounded receiver owned by a
dedicated profiling worker aggregates counts, totals, maxima, and percentile
histograms and publishes observability records. A full queue drops profiling
signals and increments a drop counter; it never backpressures grid or rendering.
Formatting, serialization, journal I/O, and subscriber delivery remain outside
the measured thread.
Required fixed phases are startup/config, client creation, renderer creation,
login, grid readiness, event ingest, update tick, visibility, asset disk read,
asset network fetch, texture decode, geometry derive, GPU change application,
render submission, GPU completion, optional readback, and optional JPEG encode.