Centralize world and viewport game loops
This commit is contained in:
@@ -1,100 +1,116 @@
|
||||
# Renderer and viewport performance
|
||||
# Agent runtime 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.
|
||||
1024 m varregion, using the default 64 m view distance. The rendered scene had
|
||||
211 objects, 427,253 triangles, and five unavailable grid textures. All timing
|
||||
numbers below are optimized release builds.
|
||||
|
||||
## Current capture path
|
||||
## Runtime ownership
|
||||
|
||||
| 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. |
|
||||
- `metacrate-grid-world` owns native OpenSim/Second Life event subscriptions,
|
||||
distance filtering, attachment world transforms, terse movement application,
|
||||
event-drop accounting, and immutable published world snapshots.
|
||||
- `metacrate-game-loop` owns the fixed update schedule, renderer lifetime,
|
||||
persistent GPU scene, fixed-rate viewport loop, completed-frame buffers, and
|
||||
timing signals. Readers receive an immutable `Arc<[u8]>`; they never read a
|
||||
render target while Bevy writes it.
|
||||
- `metacrate-rendering-wgpu` owns Bevy/wgpu resources and supports separate
|
||||
scene loading and camera/frame rendering. Shared immutable scene arrays cross
|
||||
the renderer thread without copying their contents.
|
||||
- `metacrate-grid-agent` supplies policy, conversation, tools, and lifecycle
|
||||
triggers. It consumes published world/viewport state instead of synchronizing
|
||||
render buffers or individual native event channels.
|
||||
|
||||
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.
|
||||
Grid callbacks never perform asset I/O or render work. The world loop drains a
|
||||
bounded 4,096-event queue in batches, coalesces dirty IDs, and publishes only
|
||||
complete snapshots. Object, avatar, attachment, and terrain admission uses the
|
||||
configured view distance; 64 m is the default. The live reference contained
|
||||
280 visible prims, one avatar, and 51 nearby terrain patches, with zero dropped
|
||||
events.
|
||||
|
||||
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.
|
||||
## Readiness
|
||||
|
||||
## Startup
|
||||
Transport connection is not agent readiness. A generation remains degraded and
|
||||
chat, tools, movement, and autonomous behavior remain fenced while the terminal
|
||||
reports:
|
||||
|
||||
The optimized cold-start profile was:
|
||||
```text
|
||||
INITIALIZING generation=N waiting_for=world_state,viewport target_fps=10
|
||||
```
|
||||
|
||||
| Phase | Time | Scheduling |
|
||||
The framework gate opens only after the region-scoped world state exists, the
|
||||
first scene/assets have converged, and at least two complete viewport frames
|
||||
have been published without render errors. The terminal then emits
|
||||
`INITIALIZED ...` followed by one unambiguous `READY ...` line when the session
|
||||
and agent services are released. The default readiness allowance is two
|
||||
minutes; the measured scene convergence window was about 30.05 seconds.
|
||||
|
||||
## Measured steady state
|
||||
|
||||
| Phase | Measured time |
|
||||
|---|---:|
|
||||
| Bevy/wgpu initialization | 476 ms |
|
||||
| First persistent GPU scene load and frame | 1.02 s |
|
||||
| Cached complete scene refresh | 86 ms |
|
||||
| Stable warm render plus coherent CPU readback | 31–36 ms |
|
||||
| JPEG encoding on demand | 37 ms |
|
||||
| Repeated capture using the already published viewport, including JPEG | 45 ms |
|
||||
|
||||
The viewport uses a 90 ms scheduling interval, providing margin above the
|
||||
minimum 10 FPS requirement. The live two-second soak completed 22 frames at
|
||||
10.99 FPS with zero missed deadlines and zero render errors. JPEG encoding and
|
||||
LLM transfer are not in this loop.
|
||||
|
||||
A cached full-scene refresh completed in 86 ms, inside the 90 ms interval:
|
||||
validation 1.08 ms, shared command handoff 0.001 ms, texture cache sync 30.47
|
||||
ms, mesh cache sync 15.21 ms, scene setup 0.47 ms, and render/readback 35.70 ms.
|
||||
Unchanged frames skip validation, command transfer, texture sync, and mesh sync.
|
||||
|
||||
## Startup work
|
||||
|
||||
| Phase | Reference 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. |
|
||||
| Bevy/wgpu initialization | 0.49 s | Completed before agent readiness |
|
||||
| Initial world/asset convergence | 30.05 s | Asynchronous Grid and asset workers |
|
||||
| First persistent GPU scene and frame | 1.02 s | Before the readiness gate opens |
|
||||
|
||||
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.
|
||||
The slower first start is intentional: the agent does not claim readiness until
|
||||
it can provide stable world and visual state. Raw immutable assets persist in
|
||||
the configured 2 GiB LRU under the platform cache directory
|
||||
(`$HOME/.cache/metacrate` on Linux), so later starts reuse them.
|
||||
|
||||
## Game loop
|
||||
## Frame publication and future GUI path
|
||||
|
||||
The agent needs four independent paths:
|
||||
The current agent path continuously renders and publishes the latest complete
|
||||
RGBA frame. A requested snapshot reads that immutable frame and performs JPEG
|
||||
encoding independently. Camera or scene submissions replace stale pending
|
||||
input; consumers never block the viewport loop.
|
||||
|
||||
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.
|
||||
The current Bevy screenshot API still performs synchronous CPU readback, which
|
||||
accounts for most of the 31–36 ms warm frame cost. This meets the current
|
||||
headless-agent target, but a GUI viewer should present the GPU target directly.
|
||||
For higher-rate CPU capture, use a ring of three staging buffers: render, enqueue
|
||||
GPU copy, map asynchronously, and publish only after completion. If all staging
|
||||
buffers are occupied, skip that readback instead of stalling rendering.
|
||||
|
||||
Simulation/update and rendering use separate clocks. Slow asset downloads,
|
||||
JPEG encoding, LLM requests, and subscribers must never hold either loop.
|
||||
## Instrumentation and observed limits
|
||||
|
||||
### Frame publication and readback
|
||||
Hot paths record monotonic timestamps and fixed-cardinality counters only.
|
||||
World event drops, completed frames, missed deadlines, render errors, and last
|
||||
render duration are available without formatting or I/O on the loop threads.
|
||||
|
||||
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.
|
||||
Observed limiting factors:
|
||||
|
||||
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.
|
||||
- Initial scene/asset convergence, about 30 seconds, dominates readiness.
|
||||
- First GPU admission is about one second; it occurs before `READY`.
|
||||
- A cached full-scene refresh is 86 ms and is currently the closest operation to
|
||||
the 90 ms frame budget. Incremental per-entity GPU updates are the next useful
|
||||
optimization if denser or highly animated regions exceed that budget.
|
||||
- Synchronous readback costs roughly 31–36 ms per frame. A direct GUI path or
|
||||
asynchronous staging ring removes it from presentation.
|
||||
- Five referenced textures were unavailable from the test grid. Missing assets
|
||||
do not deadlock readiness; completeness remains explicit.
|
||||
- The successful live run used 280 visible prims, one avatar, 51 terrain
|
||||
patches, 427,253 render triangles, 4,096 event slots, and dropped zero events.
|
||||
|
||||
Reference in New Issue
Block a user