Files
MetaCrate/docs/renderer-performance.md
Chili Palmer cb9233c59c
Some checks failed
CI / rust-skia (Rust only) (push) Has been cancelled
CI / required (push) Has been cancelled
Profile and preinitialize viewport rendering
2026-08-23 12:16:52 +02:00

6.3 KiB

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.