packages/presentation consumes an independent EntityStore journal cursor and produces version-2 presentation messages. It imports the core, schema, math and render protocol packages, without importing Simulation, browser APIs or game content. The existing spatial forest resolver lives in packages/math/spatial.ts; Simulation re-exports the same API and keeps its installed transactional spatial invariant.
The production player now consumes canonical Simulation ECS rows through this bridge in its simulation worker. All 1,480 Darkwind authored objects and the local player occupy the real store. The primitive compiler supplies stable place-local identity mappings and immutable asset descriptions; checkpoint profile 4 preserves those rows. games/engine-zoo/presentation.ts (ecs-presentation.1) remains the independent six-object hierarchy fixture for main-thread and OffscreenCanvas diagnostics.
#Publication boundary
The bridge reads complete journal batches. It stages entity creation, component changes/removals and deletion into a private cache containing only the five required render/spatial components, validates the changed component values, resolves affected transforms and asset references, then validates a staged presentation patch. Baselines still validate a complete scene. Only successful publication advances its cursor/cache. Missing assets or unsupported policies leave the prior accepted scene and cursor intact; a later repair can retry from that point. If history is no longer retained, the store supplies a complete baseline instead.
Initial reads, store replacement, journal reset/overflow and place changes start a new render generation. Ordinary changes become one structural render delta. ECS IDs are copied as render IDs, never process-local handles; deletion and reuse of a handle slot cannot update the prior render entity. Each reader advances independently. The cache is bounded by the store's 65,536-entity limit and publication by the 4,096-instance scene budget. Unchanged or exclusively non-render component changes do not copy the cache, emit revisions or recompute the hierarchy. Other component payloads are never retained in the bridge.
Projection samples final committed state at each poll. It is not an event stream for gameplay actions or a resource eviction queue. Render lifecycle messages, once produced, still require reliable ordered delivery and baseline recovery. The player uses an acknowledged one-frame channel that coalesces pending requests; pooled transferable-buffer transport remains unfinished.
#Supported data
Transform,PlaceMembershipandParentresolve to world matrices with the canonical float64 hierarchy algorithm, including nonuniform-scale shear. Parent changes therefore update descendant render matrices.RenderMeshselects a primitive asset. Invisible entities and other places produce no draw record. Current primitive policy supports casting/receiving shadows with default LOD bias; unsupported policies fail explicitly.MaterialRefmust name exactly onedefaultslot. Primitive mesh/material catalogs own validated immutable descriptions. Missing resources fail before any render update becomes visible.- Environment, procedural traveler descriptions and optional decorative motion remain explicit presentation-view inputs. ECS light/animation projection and general asset residency are not implemented by this bridge.
PrimitiveAssets maps caller-owned asset IDs to primitive/material descriptions. It is not the planned immutable asset compiler or content-hash/GPU-resource manager. Accepted draw records own their material/matrix values independently of upstream ECS rows. The four primitive geometry buffers stay resident; replacement reuses them.
#Rendered reference checks
The worker probe first edits a nonuniformly scaled parent and a rotated wall child, checks that pixels change, and records the composed matrix. Node and browser compare that matrix against tests/fixtures/ecs-presentation-v1.json. It then rejects a missing material while retaining the accepted frame, renders eight alternating primitive/material replacements while checking six buffers and three textures, deletes the wall and overruns the reader's journal, applies the new baseline with five visible objects, and restores the original ECS snapshot. The restored frame must match the initial PNG bytes exactly and contain all six original objects.
The protocol also retains its separate deliberately dropped-structural-message recovery check. Headless tests cover two independent readers, atomic multi-component updates, stale handles, place filtering, component removal, immutable assets, unsupported policies and explicit version-1 envelope migration. The fixture itself does not add entities to saved games or change the authored world fingerprints.
Local pose and material edits now use incremental projection. Complete baselines and topology edits still validate/compose the full forest. Bounded transferable buffers, imported resource identity/lifetime management and broader device acceptance remain open. No whole engine milestone is complete.
#Incremental invalidation
The bridge retains accepted spatial nodes, composed matrices, a parent-to-children index and current draw records. Final no-op edits advance the journal cursor without allocating replacement caches. Changed component values are validated before equality checks, so omitted JSON values cannot hide an invalid record from a loosely registered store.
A local pose edit marks the entity and its descendant closure dirty. Recomposition runs in accepted depth order, using already updated parent matrices. Material/mesh/visibility edits evaluate only their draw records and do not recompose transforms. A moving non-rendered leaf updates its pose cache without rebuilding draw records. Environment/actor changes do not invalidate object transforms; changed decorative metadata reevaluates the affected view's render records.
Changes to spatial entity presence, parent edges or place membership rebuild/validate the complete spatial forest. This handles cycles, dangling parents and depth limits before publication and replaces the child index for subsequent incremental edits. Baseline, place changes and journal recovery remain complete publication barriers. A parent pose whose subtree contains the entire world still legitimately touches that complete subtree.
PresentationSource.patch() validates and copies only supplied records. Unchanged frozen records remain owned by the accepted scene. Object insertion order is preserved by deltas and the full-scene compatibility adapter; an explicit baseline reset changes ordering. Unknown removals, conflicts, unsupported data and combined instance-budget failures leave the previous source baseline unchanged. The bridge commits its row cache, graph, draws and cursor only after patch publication succeeds.
The work result reports path, composed transforms and evaluated draw records. The worker fixture proves that its parent/child edit composes two transforms and evaluates one draw, while each of eight mesh/material replacements composes zero transforms and evaluates one draw. Headless coverage includes a 240-step deterministic comparison with fresh baseline projections across creation, deletion, visibility, parenting, view/actor changes and journal overflow, plus failures and repair with no accepted-cache corruption.
This is not allocation-free or strictly O(changed entities): staged Map copies, journal scanning, scene-array construction and budget checks remain bounded by retained state. Topology changes still take the full-validation path. Timings exclude canonical ECS mutation/invariant cost and renderer/GPU submission; those costs need separate measurement before live-world integration.
#Projection measurement
Run npm run bench:presentation -- <output.json> after browser and other load tests stop. The ecs-presentation-grid.1 workload measures 64, 512, 2,048 and 4,096 flat primitive entities, with five warmups and 30 measured samples per size. It separates complete baseline projection, one changed transform, and an unchanged read. It verifies that a one-entity edit emits exactly one draw update. Timings include journal consumption, composition, asset resolution and owned protocol validation; entity mutation preparation, JSON encoding and all GPU/network work are excluded. Machine, runtime and Git provenance accompany the results. The current affected-subtrees.1 implementation additionally asserts that each measured edit composes/evaluates one transform/draw, and an unchanged read does neither. Small outgoing deltas alone do not establish cheap projection.
Historical full-recomposition evidence: 105 headless tests, 19 browser scenarios, worker report, and projection benchmark. The headless runtime source is 04b2df2; the browser/benchmark source is c372760, which only corrects Playwright fixture loading. The initial JSON import failure and exact provenance are retained in the evidence.
| Entities | Baseline p95 (ms) | One-transform edit p95 (ms) | Unchanged p95 (ms) |
|---|---|---|---|
| 64 | 1.788 | 1.108 | 0.0256 |
| 512 | 5.896 | 3.639 | 0.0294 |
| 2,048 | 25.915 | 14.503 | 0.0680 |
| 4,096 | 47.747 | 38.484 | 0.0767 |
This historical 4,096-entity edit cost exceeded a 30 Hz tick budget before physics or networking. It motivated the incremental path above; a new clean-source comparison is recorded separately rather than overwriting this baseline.
#Affected-subtree results
Clean source c232cafab8b1b26c0e3c643dbb0f53d2634afea0 passed 114 headless tests and 19 browser scenarios, with no skips or retries. The worker report records actual incremental hierarchy/replacement work. The benchmark ran after the local browser and headless processes stopped, on the same recorded CPU/runtime as the historical comparison.
| Entities | Baseline p95 (ms) | One-transform edit p95 (ms) | Unchanged p95 (ms) |
|---|---|---|---|
| 64 | 1.733 | 0.201 | 0.0173 |
| 512 | 7.076 | 0.209 | 0.0227 |
| 2,048 | 31.860 | 0.567 | 0.0246 |
| 4,096 | 63.370 | 1.867 | 0.0491 |
At 4,096 entities, single-transform projection fell from the historical 38.484 ms to 1.867 ms p95 (about 20.6× in these runs). Full baselines increased from 47.747 ms to 63.370 ms p95; cache construction and validation still need work. These are individual synthetic runs, not a statistically established universal speedup. Mutation/invariant CPU cost, whole-scene topology edits, transport and GPU work remain outside this timing. Measure those separately before canonical live-world integration.
#Canonical mutation cost before live-world migration
The canonical mutation benchmark, source 3ae872363a8cd29341cba367ed5994540a203a0e, measures the previously excluded commit path. Run npm run bench:mutation -- <output.json> with other local load tests stopped. A flat primitive population registers all 17 built-in schemas; one actor commits Transform and CharacterMotor together, matching the player's normal component write pattern. The production identity and spatial invariants validate the complete candidate state. A no-invariant arm isolates staging/journal cost; disabling those contracts is not an acceptable runtime optimization. Five warmups and 30 samples run per case. Invalid identity and missing-membership commits must still fail without changing the accepted snapshot or journal.
| Entities | Motor commit with invariants p95 (ms) | No-invariant control p95 (ms) |
|---|---|---|
| 64 | 1.992 | 0.207 |
| 512 | 7.948 | 0.080 |
| 1,481 | 25.191 | 0.166 |
| 4,096 | 73.213 | 0.835 |
The 1,481-entity case approximates 1,480 authored Darkwind objects plus one player in count only. Its 25.191 ms p95 commit cost consumes most of a 30 Hz tick before physics, presentation or networking. At 4,096 entities, 73.213 ms p95 exceeds that entire budget. At that historical source, the live Simulation contained the player alone, so this was a prerequisite integration measurement, not an observed beta slowdown. Full snapshot materialization, isolated copies for both invariants and complete spatial validation require optimization before adding the authored world to that store. Any replacement must preserve atomic rejection, direct-write and restore checks, exact hierarchy/identity semantics and independent journal readers. Fixed workload order, JIT/GC effects and 30-sample tails limit comparisons; these are not hardware-independent capacity guarantees.
#Scoped validation follow-up
Source 70f7f024204910aca230ac206c814ed6309df9b3 replaces complete snapshots on ordinary commits with scoped candidate views. The 119 headless checks and 21 browser scenarios pass, including full checkpoint/worker replay and a 600-step differential comparison with the original snapshot rules. The clean benchmark ran after local acceptance stopped, using the same workload, sample count and recorded machine/runtime as the historical measurement.
| Entities | Scoped invariants p95 (ms) | Historical full-snapshot invariants p95 (ms) | Current no-invariant control p95 (ms) |
|---|---|---|---|
| 64 | 0.307 | 1.992 | 0.101 |
| 512 | 0.142 | 7.948 | 0.119 |
| 1,481 | 0.121 | 25.191 | 0.194 |
| 4,096 | 0.572 | 73.213 | 0.607 |
The formerly dominant full-snapshot work is absent from the ordinary flat-scene pose path, with both production invariants still installed. Small differences between controls and validated cases reflect fixed run order, JIT/GC and sampled tails; they do not imply that validation has negative cost. The benchmark has no Parent edges. Parented scenes still scan their sparse Parent column, and affected descendants/ancestors still require validation. Topology edits and restores take the complete-forest path. Remaining identity/column copies, compiler integration, checkpoint growth, transport and renderer costs must be measured in the actual world before a broader capacity claim.
#Live authored-world acceptance
The primitive compiler now feeds actual Darkwind Simulation rows into this bridge in the production worker. Clean source cbb9ec34f56b0d5d868dc99c2fdf21d6ff6144ef passed 129 headless and 22 browser checks, including a stalled real-worker acknowledgement, canvas-pixel verification and the complete shared journey. The actual 1,481-entity simulation cost is measured separately from projection: 0.187 ms p95 over 1,800 ticks. Full checkpoint growth/cost is recorded in checkpoint evidence. This closes the earlier player-only integration gap; it does not establish generic ECS-driven physics, imported-asset residency or pooled transferable transport.
#Transfer-buffer ownership
The live simulation-to-player presentation path and the Observatory render-worker path now use packages/render/transfer.ts. Each channel owns one reusable, non-resizable 4 MiB ArrayBuffer. The payload is UTF-8 JSON using the existing validated presentation formats 2–5, inside local transfer-envelope format 1. This establishes bounded ownership and transfer; it is not the compact binary scene codec or a zero-allocation tick loop. JSON strings and validated scene objects remain transient allocations. Frames with no presentation change do not borrow the buffer.
A prepared transfer has a random channel epoch, increasing uint64 sequence, used byte length and fixed-capacity buffer. postMessage detaches the sender's buffer. The consumer validates bounds, UTF-8, JSON and the existing presentation schema before rendering; returning the same buffer detaches the consumer. The producer admits a return only for its pending epoch/sequence, capacity and used length. Duplicate and stale returns cannot release a newer transfer. Receiver sequence admission prevents replay within the channel. These checks bind trusted worker ownership; they are not authentication against a malicious peer.
The scene producer returns to its acknowledged-delivery boundary only after reclaiming the buffer. In worker placement this boundary is independent of the UI-state acknowledgement; the main-thread fallback retains the original combined delivery. There is no additional unsent-message queue: while the consumer is stalled, ECS changes stay at the source and coalesce through the existing journal/baseline rules. Structural edits are retained through a fresh baseline after overflow. Oversized or malformed messages do not consume an available buffer or sequence. A failed synchronous send may cancel only while the buffer remains attached. Terminate a failed peer before disposing/replacing its pool; a new epoch cannot reuse stale returns. Frame state and room metadata still use bounded structured cloning.
The 1,000-edit stalled-consumer test proves one outstanding buffer and latest-state recovery after journal overflow. Native transfer tests cover detachment in both directions, Unicode, repeated reuse, duplicate/stale receipts, byte limits, malformed UTF-8/JSON, cancellation and disposal. The real render-worker probe transfers its structural-gap, baseline, stale-delta and new-generation messages and retains exact frame comparisons. The live-player browser test withholds an actual acknowledgement and then verifies returned-buffer detachment as movement catches up.
The development player now transfers this same buffer directly from simulation into its live render worker, then returns it over the same MessagePort after scene admission. UI state and room metadata have a separate acknowledged delivery. Renderer pause/ready generations force a new baseline after device replacement, and only the matching old receipt can reclaim an outstanding old-generation buffer. The renderer/catalog/recovery lifecycle has a main-thread placement fallback before canvas transfer. Compact binary encoding, shared memory and device-specific acceptance remain open. This transport and placement work do not by themselves resolve the recorded CI scheduling and screenshot stalls.
npm run bench:transfer -- <output.json> compares the actual 674-object Darkwind place plus one peer through structured cloning versus the admitted transfer codec, including the receiver and buffer return. The recorded Node 22 measurement, after local browser/type checks stopped, reports full-baseline p95 5.196 ms for transferred JSON versus 3.295 ms for structured clone; peer-motion p95 is 0.0197 ms versus 0.0076 ms. The baseline uses 183,463 bytes of the fixed buffer; the measured motion packet uses 233. The added admission/JSON work has a CPU cost. The purpose of this increment is explicit bounded ownership on the worker boundary; it does not establish a frame-rate improvement. One hundred measured samples follow 20 warmups per case; fixed ordering and GC limit comparison, and event-loop/GPU/network time is excluded.