Reference13 min read10 sections

Render placement and capability evidence

docs/RENDER_PLACEMENT.mdSource updated 2026-10-27
Markdown
On this page 10 sections

The development player now places its owned WebGPU renderer in a dedicated worker when worker WebGPU and canvas transfer are available. Simulation remains in its own worker. Unsupported render-worker placement uses the same backend on the main thread. This does not change the retained beta build or establish physical-GPU performance acceptance.

#Live player ownership and recovery

Room admission and simulation loading now overlap graphics creation. Simulation can restore its checkpoint, connect to the room and publish bounded UI state while GPU initialization is pending. It publishes no scene bytes until the graphics placement is bound exactly once; main-thread fallback explicitly binds without a port and requests a fresh baseline. Enter remains disabled until simulation, the graphics connection and (for shared play) room admission are ready. Startup failure terminates the simulation and graphics workers; a late uncancellable main-thread renderer is disposed before catalog installation. The existing 20-second startup and recovery limits are unchanged.

Exported reports include fixed startup stage timestamps and at most 64 graphics lifecycle entries, with worker timestamps translated to the page clock. Native adapter/device requests, resources, shader modules/validation/pipelines/identity, initial upload and model preparation/allocation/upload/installation now refine the original probe/device/catalog/actors/ready boundaries. Three supported device generations retain all 49 entries. These identify the unfinished phase without recording an unbounded event stream; their wall-clock intervals are not GPU timings. See recovery diagnostics. The serial-startup investigation preserves CI trace evidence: second-player simulation module requests began 17–20 seconds after document requests in three failed scenarios, and were absent in another. Those traces establish the serial critical path, but not the cause of slow GPU initialization.

apps/player/graphics.ts owns one render worker and transfers the visible canvas once. A capability probe runs before that transfer; missing canvas transfer, worker construction failure, missing worker WebGPU or an unavailable adapter selects main-thread rendering. Once the canvas has transferred, a worker failure requires the existing retry/reload flow. A transferred canvas cannot be silently reclaimed for main-thread rendering.

The worker installs the same Darkwind model catalog and traveler rig as the fallback. It renders on its own timer, with at most one native GPU frame outstanding. Main-thread camera, viewport and quality updates coalesce to the latest view with at most one view request outstanding. There are at most four tracked requests, each with a 20-second deadline. Replies are checked against their request generation. Failure or page teardown terminates the worker and rejects pending requests. The direct scene channel has its own 20-second receipt deadline in the simulation worker; duplicate or retired receipts cannot release a different pending buffer. A receipt error or deadline pauses simulation and enters the saved-journey error flow.

Presentation now travels directly between simulation and rendering over a transferred MessagePort. UI state has a separate one-outstanding-state acknowledgement; holding it cannot retain the scene buffer. The scene channel owns one reusable 4 MiB buffer, returned after admission independently of GPU completion. Holding a scene receipt cannot retain UI state or autosaves. The main-thread graphics fallback keeps the existing combined path. During device replacement, the renderer pauses scene publication and then announces a new graphics generation. This forces a fresh baseline even without a world mutation; a still-outstanding older buffer must return before the new baseline can leave simulation. Replacement succeeds only after the new renderer admits its baseline, renders it and observes GPU completion. Two device replacements reuse the same canvas and render worker without replacing simulation or the room session. A third loss, a failed replacement, validation failure or worker crash enters the saved-journey error flow.

Worker statistics carry their monotonic time origin. The proxy translates completion/submission timestamps to the page clock; durations remain unchanged. Projected nameplates use the logical viewport, while render targets use the existing quality/pixel-ratio policy. The page owns DOM, input, camera smoothing and room UI. Rendering and remote scene updates can continue from the latest accepted camera view without page animation frames or UI-state acknowledgements. Camera changes, DOM and local-storage writes still require the main thread. A blocked page is not a fully interactive page; physical-device responsiveness measurements remain open.

The live-player acceptance records the scoped local checks and source hashes. The native runtime reports separate main-page scheduling observations from the render worker’s GPU queue events. Software rendering and local passes do not close remote CI or physical-device gates.

The direct channel is implemented in packages/presentation/channel.ts, with one acknowledgement boundary for scene ownership and explicit pause/ready generations. Its local acceptance records 334 headless tests and 28 player/multiplayer scenarios. The native-port evidence shows scene receipts and GPU completion notifications during a blocked page task, and local saves while a scene receipt is withheld. Earlier worker-placement acceptance remains attached to its own source.

Connection Owned data Bound and recovery
Page ↔ simulation Semantic input, UI state, room metadata and save checkpoints One outstanding UI-state frame; latest state coalesces until its acknowledgement
Page ↔ renderer Camera, viewport, quality, initialization and recovery verification One pending view plus its latest replacement; four tracked commands total, each with a 20-second deadline
Simulation ↔ renderer Validated scene baselines/deltas and the reusable byte buffer One outstanding scene and one 4 MiB allocation; 20-second receipt deadline; graphics generations pause delivery and force reconstruction

The direct channel carries pure-data scene envelopes. MessagePort ownership stays in the player application, so the presentation package imports no DOM globals and remains usable in headless tests. Graphics-device generations are separate from the scene protocol's canonical generation/revision. An old device receipt can reclaim its own outstanding buffer, but cannot satisfy the new renderer's baseline or reclaim a different transfer. Producer errors close publication once and pause simulation; teardown closes ports and disposes both delivery boundaries.

#Initial entry readiness and software-browser launch

The player keeps its entry button disabled until simulation, room admission and presentation are ready and the current renderer has accepted a baseline and completed a scene frame. Completion is the renderer's observed GPU queue completion, not merely submission or successful resource allocation. The existing 20-second startup deadline still bounds a held startup. This gate concerns initial entry; staged travel and renderer/native recovery retain their own publication/verification boundaries.

The headless launcher uses the Vulkan/SwiftShader path and preserves Vulkan surface support. The retired --disable-vulkan-surface switch produced missing adapters or blank composited output on the locally installed Chromium 155. Removing it restored isolated main/worker clear pixels and real single-player composition in the measured configuration. See the Chromium SwiftShader documentation for software-driver modes; local acceptance records this specific experiment and the engine tests.

Chromium 141 remains the measured Playwright reference for the complete persistent journey. Chromium 155 passed main/worker first-frame gating and real visible/invisible canvas controls, but the second traveler in the persistent acceptance hit the unchanged 15-second shader compilation deadline. That failure remains open; successful queue completion and a solo screenshot do not establish multi-client or physical-GPU qualification.

#Repeatable probe

Build and start the application normally, then open /diagnostics.html on localhost or HTTPS. The Engine Observatory runs without room admission, a WebSocket, or writes to local game saves. It is also available at /diagnostics on the production relay. Vite development uses /diagnostics.html.

Run the checks exercises the actual engine-zoo motor.1 fixture through the same shadow, HDR forward and postprocessing passes as the player:

  1. Render on an HTML canvas, read nonblank opaque pixels, resize from 480 × 300 to 640 × 360, then dispose twice and verify a later render is inert.
  2. Transfer a fresh OffscreenCanvas and sentinel buffer to a dedicated worker. Verify the sender loses ownership and the sentinel returns intact. Read the rendered PNG, resize and return to the original size, checking dimensions and resource counts each time.
  3. Stop the worker normally, repeat with actual GPUDevice.destroy() and the asynchronous device-loss callback, then create another worker and render again. Every cycle owns fresh transferable objects.

Each worker command has a 20-second deadline. Timeout, worker error, cancellation and page exit terminate the owned worker and reject pending requests. Main-thread initialization has the same deadline; if the browser resolves an uncancellable adapter/device request late, the renderer is immediately disposed. Device loss and uncaptured graphics errors stop rendering and release the renderer's resources before invoking the application callback.

Check input & audio must be clicked separately. It requests pointer lock and resumes a silent audio context inside the user gesture. The pointer check observes both acquisition and release. The audio check closes its context before returning success. Denied, missing and timed-out capabilities are reported explicitly.

The additional Runtime data contracts check uses a separate short-lived worker to validate the built-in catalog and a five-entity transform hierarchy against its pinned headless fixture. The full browser test compares this complete result with a fresh Node capture. See the component catalog for the exact scope; this is not a complete physics replay test. The original four-check graphics evidence below remains tied to its recorded source commit.

Download report saves engine/fixture versions, source revision and dirty state when Git was available at build time, build/test timestamps, user agent, browser-exposed adapter capabilities, image measurements and owned resource counts. A Docker build without .git reports unknown source identity; deployment/CI metadata supplies that identity. No report is uploaded automatically.

Run the automated probe with:

Terminal
npm run test:browser -- tests/browser/probe.spec.ts

The browser suite also injects worker exceptions, cancels an active worker, retries with fresh workers and exercises an unavailable WebGPU API. Successful runs produce test-results/capabilities.json and test-results/engine-observatory.png; CI retains them with its browser artifacts.

The first committed-source capture is f54535c: capabilities, all six probe results, desktop capture, and narrow-layout capture. The report records a clean build source. The timeout scenario advances the browser clock against a deliberately silent worker; it proves deadline handling without spending 20 real seconds waiting.

#What the measurements establish

The local Chromium 141 software-WebGPU path renders the six-object engine-zoo fixture on both threads. Its active owned census is six buffers and three textures; resizing preserves that census, and normal/device-loss disposal reduces it to zero. The fixture uses one primitive batch, so it issues three draw calls across shadow, forward and post passes. Pixel contrast and opacity checks detect blank outputs; they are not a full material or color reference comparison.

These counters measure the engine's explicit buffer/texture ownership. They do not measure driver allocations or prove a long-session memory plateau. frameMs records CPU submission work, not completed GPU time or interactive frame rate. The probe demonstrates restart with a new worker/canvas/device; it does not reconstruct a live player's renderer automatically after GPU loss.

#Placement decision

Worker placement is now integrated with versioned canonical presentations, bounded transferable ownership and the player recovery lifecycle. The main-thread fallback retains the same graphics features and quality. Software-WebGPU tests establish scoped behavior; they do not show a performance advantage on a supported physical GPU or close milestones 2/4. The original Observatory measurements below remain attached to their recorded source.

#Device evidence ledger

Configuration Verified evidence Support decision
Linux test workstation; Intel Core Ultra X7 358H; Node 22.23.4; Playwright 1.56.1 / Chromium 141.0.7390.37; SwiftShader Vulkan; browser viewport 960 × 640 Automated engine/player/network checks; main/worker probe at 480 × 300 and 640 × 360 Behavioral reference only; software GPU does not establish hardware performance
Ubuntu 24.04 GitHub runner; pinned Node/Playwright; SwiftShader Vulkan Source-specific CI artifacts Reproducibility reference; not a physical preview device
Local Intel Core Ultra X7 358H / Arc B390; Linux 7.2.8-9-omarchy; xe; Mesa/vulkan-intel 26.2.5; Chromium 141 Vulkan 12 physical-GPU functional checks: editor restoration/reload, main/Worker daylight/weather/portals, device reconstruction and startup failure cleanup; editor 1440 × 1000, native fixture 800 × 500 Named short-run functional reference. Sustained frame-time/memory, broader device coverage and full-plan acceptance remain open
Phone hardware GPU No physical-device evidence yet Pending named device and touch/input/performance acceptance; no phone support claim

The full milestone 0 gate stays open until physical reference configurations are named and measured and all initial engine decisions are recorded. Milestone 2 also retains its culling, render graph, hot replacement, protocol-gap and automatic recovery requirements.

#Presentation protocol increment

The original version-1 render-only scene increment and its clean-source acceptance report cover an intentionally missed structural revision, byte-identical preservation of the last frame, baseline recovery, late-delta rejection and a generation reset. That historical player rendered on the main thread. Current canonical ECS and pooled presentation ownership are described in ECS presentation; live placement is described above. Resource-residency and physical-GPU acceptance remain open.

#Startup scheduling

Independent weather, foliage, lettering, particle and forward shader builds start together on each renderer's device, including portal renderers. Startup waits for all sibling results before accepting the renderer or propagating a build failure. In-flight effect builds are included in the renderer's native compilation drain. An injected precipitation failure with a held foliage build verifies that cleanup does not destroy the device early and that a subsequent startup succeeds.

The startup comparison records a partial improvement with two existing players on SwiftShader. It retains the unchanged five-second published-restoration reload failure. Pipeline batching does not establish physical-GPU frame rates, remove adapter-request contention, or close the loading gate. The diagnostic source is retained in the measurement fixture; its longer diagnostic waits must not be substituted for acceptance deadlines.

#Physical GPU test mode

The default browser profile continues to force SwiftShader for portable behavioral checks. Set DW_GPU_PROFILE=hardware to run on a physical adapter. Before any test starts, the runner requests a real WebGPU device, rejects missing/fallback adapters, and records adapter capabilities, browser, CPU, OS and launch arguments in the JSON report's config.metadata.hardware. A deliberately forced SwiftShader adapter is verified to fail qualification. Unknown profile names fail configuration.

The current physical reference uses Linux Vulkan and the pinned Playwright/Chromium version. The acceptance command is:

Terminal
DW_GPU_PROFILE=hardware DW_PLAYTEST=garden-ferry VITE_DW_PLAYTEST=garden-ferry \
DW_EDITOR_TOKEN=editor-browser-test-only-key-123456 \
npm run test:browser -- tests/browser/editor-startup.spec.ts \
  tests/browser/editor-restoration.spec.ts tests/browser/editor.spec.ts \
  tests/browser/renderer-initialization.spec.ts tests/browser/weather.spec.ts \
  tests/browser/day-cycle.spec.ts

PLAYWRIGHT_EXECUTABLE_PATH can select an installed Chromium executable; the recorded run used pinned headless Chromium 141. The server owns an isolated data directory and test port. This profile does not change the user's editor, deployment, application quality, or test deadlines. Hardware evidence is scoped to the recorded machine and selected cases; it does not establish mobile support, physical display/input behavior, sustained frame rates or long-session memory bounds. The software profile's five-second restoration reload failure remains in the evidence ledger.

The same Arc B390 reference subsequently passes the ten existing Observatory scenarios plus an injected sRGB-decoding fault check: DW_GPU_PROFILE=hardware npm run test:browser -- tests/browser/probe.spec.ts. This covers renderer/data checks, resize and reconstruction, worker failure/deadline/cancellation/retry, and pointer-lock/silent-audio API cleanup. The complete renderer scenario and color fault also pass on SwiftShader. Acceptance retains the initial strict sRGB reference failure and its measured precision correction. This is headless functional evidence; no physical input, audible output, sustained performance or additional device qualification is implied.

#Two-traveler delivery · 2026-10-27

The player sends tab activity with its camera view. Focused views retain the normal cadence, unfocused views submit at most four frames per second, and hidden views at most one. Focus/visibility events notify the independent render worker even when browser animation callbacks stop. The main-thread renderer applies the same policy. Simulation, room messages, first-frame readiness and recovery frame verification retain their existing paths. This reduces background rendering work; it does not resolve the SwiftShader adapter stall.

Focused acceptance passed npm run check and both browser journeys below on an Intel xe-3lpg physical adapter (fallback refused), Chromium 155.0.8059.84, Linux, Node 22.23.4. The gate journey uses two independent sessions in one browser process, shared opening/closing and normal rendering. The motor journey covers authoring, save, undo/redo, publication, shared movement and second-player reload. The journeys completed in 10.6s and 18.0s. The gate and motor screenshots were visually reviewed.

Terminal
DW_GPU_PROFILE=hardware PLAYWRIGHT_EXECUTABLE_PATH=/usr/bin/chromium \
DW_EDITOR_TOKEN=editor-browser-test-only-key-123456 \
npm run test:browser -- tests/browser/garden-threshold-shared.spec.ts \
  tests/browser/planar-motor.spec.ts

To try the delivered features, run npm run dev:editor under Node 22, create a Garden threshold world, save and publish, then open the release link in a regular and private browser window. Enter both travelers; walk onto and off the rose pressure stone and observe the shared gate. Use a hardware-accelerated WebGPU browser.

Deferred qualification: SwiftShader still times out at the second graphics adapter probe while the first traveler renders, despite successful room admission. A speculative per-frame yield did not fix it and was removed. Neither longer startup deadlines nor separate browser processes were used to claim this case passed. Background frame timing, additional physical devices, sustained performance and the broader graphics recovery matrix remain to review. Final character, environment and arrival art remains required.

End of Observatory & capabilities