Repository navigation
feat: integrate Strata terrain, live editor and game handoff (PRDs 466–468) - #381
Conversation
|
Phase 2 of 9: the baked terrain preview now passes its shared browser WebGPU and Linux desktop scenario. These are real development captures from commit Browser terrain and road, after a 50-metre walk. Textures, vegetation and river rendering are still pending. Browser coastal terrain with the installed spectral ocean. Shoreline foam and the final lighting/art pass are pending. Linux desktop coastal preview, captured in the native host. The ocean clock now advances during fixed simulation ticks. The current phase stays open for ocean visual qualification. New create-threenative projects also include optional terrain authoring instructions; live editor/controller guidance will be added with those APIs. |
|
Phase 2 of 9 verified: the coastline now uses baked terrain heights for shallow/deep colour, shoreline foam and water masking. These are actual browser WebGPU and Linux native captures from commit Browser coast, with moving spectral waves and shoreline foam: Linux native coast, using the same game-owned surface: Linux native view after moving the actual sun: Both target scenarios and their visible water-change checks pass. A separate native run completes 300 render frames with nonblank terrain. This is an intermediate art pass: textured ground, vegetation, river rendering and the live editor remain in progress. New scaffold instructions already cover optional terrain authoring; real editor/controller instructions will follow the implemented APIs. |
|
Live editor milestone — the recovered controls now edit the same validated project document as the agent. The open page renders accepted terrain revisions without refresh; GUI name edits persist, and cancelling long erosion retains the last valid mesh. Before the agent increases hill amplitude (30): After the agent increases hill amplitude (80): Human GUI edit saved to the shared document: These are actual 1440 × 900 browser WebGPU captures on NVIDIA/turing. Three accepted revisions changed measured mesh heights and reached first draw in 100–134 ms on this machine. This fixture has no rendered props yet; the required 100-prop latency check stays open. Textures, vegetation, gizmos, spatial overlays and full-world export are still being implemented. Validation: 14 terrain public-import tests pass, including real middleware/SSE/file-watch, stale/invalid write recovery and configured private-forward readiness. The real editor browser integration passes agent edits, GUI save, worker cancellation/recovery and the scene playtest. The full test command passes workspace package builds/checks and 521 root test files / 6,483 tests (12 skips). Root typecheck, lint, budgets, documentation links and 180 documentation tests pass. The existing browser and Linux desktop terrain/ocean scenarios remain green; the native host renders 300 frames (226 presentations). No mobile or steady-state FPS claim is made. Progress: 2 of 9 implementation phases complete; the shared-document/editor phase is partial. Further visible milestones will receive new screenshot comments. |
|
Vegetation milestone — the live editor now draws 100 pine instances from editable game-owned source. Actual instance buffers contain 38,800 prop triangles; the trunks are grounded against the rendered terrain triangles with transformed model bounds. The real WebGPU browser follows the activation-returned URL and renders three accepted hill edits without refresh in 106–137 ms on NVIDIA/turing RTX 2080 (512 m / 129-vertex fixture). GUI edits persist, reactivation returns the same project/session at the latest revision, and worker cancellation recovers. An unregistered model reports its name, releases the busy state and retains the prior preview until corrected. The supplied scatter identity defect is fixed in the addon: shared candidates keep position/scale/yaw when earlier candidates are rejected, and changing a seed creates different keys. Two regression tests reproduce the old retargeting and pass with the fix; all 16 terrain public-import tests pass. Root typecheck, lint, budgets, documentation links and 180 documentation tests pass. The full test command passes workspace package builds/checks and 522 root test files / 6,485 tests (12 skips), with no suite temporary-directory growth. 3 of 9 implementation phases are verified. Individual selection/gizmos, persistent manual transforms, calibrated spatial tools, world controls/imports and complete export remain open. These are the supplied starter shapes; final realistic PBR art remains a required later milestone. More screenshots will follow visible changes. |
|
Individual prop gizmos —
The browser check also proves Orbit suspension during drag, Escape without a transaction, selective undo, stale-drag conflict preservation with an explicit reapply draft, and unmatched override removal. Metadata transforms dispatch no terrain evaluation; all 99 sibling matrices stay unchanged. All 20 terrain tests pass. Repository checks pass; the full unit phase passes 6,489 tests after the existing gate-resume path recovered one unexplained worker exit. Landform handles, remaining editor tools, final PBR starter art and complete portable export remain in progress. These captures make no native-editor or FPS claim. |
|
Retained-preview fix — A saved recipe with an unregistered asset now keeps the last valid scene and reports that the new recipe has not rendered. Metadata arriving after that failure or during active erosion retains both revision labels and every instance matrix. Cancellation keeps the valid scene; successful recovery applies the latest accepted transform. This actual WebGPU capture accompanies a red → green browser regression, including metadata during 200,000-droplet erosion, cancellation and recovery. Root typecheck/lint/budgets/docs and 180 documentation tests pass. The complete test command passes 523 files / 6,489 tests (12 skips), with no temporary-directory growth. The standalone editor scene passes too. Previous individual-selection, rotation/scale and grounding screenshots. Landform handles and complete portable export remain open. |
|
Landform transforms —
The full browser check covers actual translate X/Y, rotate Y and scale Y drags, Escape with no save, selective undo preserving another actor's name/roughness, layer-list selection, and a scatter rule named All 24 public terrain tests pass, including the original noise/erosion golden, every landform blend, rotated footprints and atomic rejection of missing/nonnumeric heights. Root typecheck/lint/budgets/docs and 180 documentation tests pass; the complete test command passes 523 files / 6,493 tests (12 skips), with no suite temporary-directory growth. Browser and Linux desktop terrain/ocean regressions pass; the desktop capture renders 300 frames with 202 presentations. These prove browser authoring and the existing baked desktop consumer, not native editor props, final PBR art or FPS. Reassignment and complete portable export remain open. |
|
Visual lane, step 1 of 3 — work in progress, not at the Gaia bar yet. AC-5 stays open. Commits What changed
Close meadow (new fixed benchmark camera, 1.7 m) Overview Coast Defects I can see (will fix next, then re-judge):
Earlier tracked captures from the export milestone ( PRD-467 AC-9 (embedded spatial inspection) is ticked. Progress: PRD-466 2/3 phases, PRD-467 1/3, PRD-468 0/3. |
|
Visual lane, checkpoint 2 — Fixed since the last post: trunks and crowns were solid constant colours because a node material ignores Close meadow (1.7 m) Overview Still wrong (next): foliage stipple at distance (alpha sampling), sparse thin grass and no poppies in frame, flat dirt blobs, rocks still sit a bit proud on slopes, a stray prop in the overview sky, flat sky with no clouds, haze or aerial perspective (the biggest gap against the references), desktop host still without ground textures. |
|
Visual lane, checkpoint 3 — New this round: installed Close meadow (1.7 m) Overview Sea horizon Where it still falls short of the references (my read, next round):
|
|
Visual lane, V2 progress — V2-1 → V2-2: close meadow. Poppies are now real flowers (stem, petal fan, dark centre) in clumped drifts, the grass no longer sits on a lattice, crowns have needle relief, wrapped light and a root flare. Before (V1c): flat red quads on even rows. After (V2-2): Honest verdict: the meadow reads as a meadow now. The spruce is the weak part: crowns still look like broad leaf shards, not needle-dense branches, and the sky and rocks are unchanged. A procedural card tree will not reach the reference, so the next lane swaps in prepared real CC0 conifer models (Poly Haven Fir Tree 01 / Fir Sapling) with the engine's new foliage-LOD and strict budgets (≤ 6k triangles near, ≤ 1.5k mid, impostor far, frame time compared before and after). V2 keeps going on rocks, ground, sand, haze and ocean. |
|
Visual lane, V3 progress — Rocks (done for this round): three photoscanned CC0 Poly Haven boulders (rock moss 01/04, boulder 01), cut in Blender to 1.9–2.2k tris near / ~500 mid, triplanar-shaded with the starter's mossy-rock maps. Cost: +4 instanced draws, meadow p50 1.3 → 1.5 ms. Starter assets 22.75 of 25 MiB. Trees (honest): the CC0 fir cannot reach the budget — 437k needle cards of ~1 cm, so 6k triangles covers 0.3% of the crown (measured), a bare tree. It is prepared but not scattered; the procedural spruce is still the scattered tree and still reads as stacked flat cards next to the Gaia reference. A first strip of the licensed Fab Kite Scots pines (local-only, never committed) came out as bare trunks with leaf confetti, so I discarded it rather than ship it. Next: re-cut the Fab pines keeping whole foliage clusters instead of the largest cards, and measure silhouette coverage before any capture. Still open for AC-5: trees, overview haze (over-hazed), ocean banding/foam, cliff UVs, and the other four worlds. Desktop lane: the prepared rocks load on the native host; the only failing assertion is the pre-existing "destroyed texture used in a submit" console error that the unmodified HEAD also emits. |
|
Visual lane, V3 — What changed
Judge's top remaining defects
Dropped as unworkable at this budget:
Only the CC0 texture atlases from those plants are shipped. Fab content stays local-only and is not scattered. Performance features: the "Nanite-like" engine feature is |
|
Visual lane, V4 — New this round
Judge's verdict
Frame cost: overview p50 is 3.8–4.4 ms, measured while the host sat at load average about 20 from other lanes. Prop draws are 24 with the pack (ceiling 24) and 21 without it. Also running tonight in parallel: the remaining editor ACs (PRD-467 AC-4–6, PRD-468 AC-3–6) and the consumer and export ACs (PRD-466 AC-6/7, the |
|
Visual lane, V5 — New this round
Judge's top remaining defects (two Sol 6.1 lanes are now working on them):
The dense photoreal forest lane (Project Nature Spruce Forest, grass, flowers and ferns, plus Kite photoscanned rocks and cliffs) has been restarted with Sol 6.1. Known bug: switching to the Coastal world freezes the picture on the forest view. This dates back to the water-port merge, and a bisect lane is fixing it. |
|
Visual lane, V6 — New this round
Judge's ranked remaining defects (next round is already being worked):
|
V7 — all five worlds (e91805c)Merged forest round 10 + worlds round 2. Full scenario green (no console/network errors); forest views p50 ≈ 2.1–2.5 ms. Honest judge score (fresh subagent, full-res vs Gaia refs): 3.6 / 10 — forest 3.5 · coastal 4.5 · alpine 3.5 · desert 2.5 · tundra 3.0. Not there yet.
What's wrong (judge + my read): spruce/grass cards show white-blue alpha fringes; landforms are smooth and un-eroded with no real cliff faces; desert mesas read as draped cloth; blotchy dark AO/shadow patches; flat macro ground tiling; tundra sky blows out. Changing method, two lanes running now:
Licensed Fab bytes stay local-only; CI runs the procedural fallback. |
|
Continuing the portable-export lane on this existing draft. Scope: preserve canonical terrain triangle topology during GLB export, verify the actual editor world in isolated vanilla Three.js, and add a read-only hosted screenshot/provenance check. All 73 terrain tests currently pass; full local browser proof is unavailable because this executor has no X display/Xvfb. I am retaining the concurrent V7 art update ( |
V8 — all five worlds (45d6d4f)Merged worlds round 3 + forest round 11. Full scenario green (no console/network errors). Honest judge score: 3.8 / 10 (V7: 3.6): forest 4.5 · coastal 4.5 · alpine 2.5 · desert 3.0 · tundra 3.0. Still far from the 8.5 bar.
What changed
What's still wrong
Running now: worlds round 4 (crag composition, snow on rock tops, irregular strata, tundra cover) and forest round 12 (real rock outcrops, far-meadow variation, rocky shore and dune grass). |
V9 — all five worlds (4aa550b)Merged forest round 12 + worlds round 4. Combined scenario green (0 failed assertions, no console errors). Honest judge score: 3.9 / 10 (V8: 3.8): forest 4.2 · coastal 4.5 · alpine 3.0 · desert 3.5 · tundra 3.0.
What changed
Honest read: the score is creeping up (3.6 → 3.8 → 3.9), not jumping. The judge keeps naming the same structural causes:
The next rounds target those directly:
|
V10 — light + terrain-form lanes merged (615b9a9)Judge: 3.84 / 10 (V9 3.9 — flat). Forest 4.5 · Coastal 4.0 · Alpine 3.0 · Desert 3.5 · Tundra 3.0 (weights 35/25/13.3/13.3/13.3).
What changed
Honest read: lighting and the far mountain ring did not move the score. What the judge says still holds it back:
Next: a canopy lane is running now. It tests the Kite Demo Scots pines, grass and flowers from your Fab library against fixed spruce cards, and picks the one closer to the Gaia reference. After that, the worlds you walk on get eroded at bake time, with rock, scree and snow placed by slope and water flow. |
…species near and far The canopy lane repainted the Kite needles from luminance and then darkened them by alpha, inner^2 and a 0.06 AO floor, which read as near-black blobs. The needles now keep their albedo with a cool shift, a moderate inner term and their needle transmission. Every cutout uses alpha-to-coverage on the existing 4x MSAA. Two Kite pines (tall, broad) alternate by variant, and the distant level loads the prepared Kite mid LOD instead of the olive Spruce Forest card.
Integer textureLoad bypassed the configured filter and quantized smooth sky and extinction gradients. Sample mip zero explicitly so both fragment and compute consumers interpolate adjacent LUT texels. Atmosphere lane engine fix outside example ownership; regression failed before and passes after (31 specs), core distribution rebuilt for the example.
Record cold/warm cache timings, JSON sizes, all view budgets and self-grades. Final licensed profiling and standard fallback pass all 54 assertions each; unprofiled CPU variance, root gate failures and AC-5 remain explicit. Retain requested captures, CPU profile and unmerged checkout.
…ey layer, dead needles gone The forest read as "Tree Tree Tree" for three separable reasons, all fixed at the root. Stands were striped. `forestWeight` was three plane waves, so a threshold through it put the canopy on diagonals; and one tree per 4.6 m cell is a lattice whatever the mask says. It is now two octaves of value noise, and placement is a Poisson disc whose radius follows the stand mask, with no cell occupancy. Every tree was one age. Size read off `forestWeight(x*0.4, z*0.4)`, the same field the density came from, so a dense core was one height. Age now reads an independent noise field: 0.42 to 1.6 with a skew toward mid-size and a few veterans. The understorey was missing. Litter, bracken, thickets and meadow flowers now key off the same stand mask, plus a measured hollow term (a hollow sits below its neighbours and holds water) that puts ferns and litter where the ground is damp. Tundra and desert grow in cluster centres instead of one plant per lattice cell. Licensed art, all local-only, all out of packs the canopy already cooks so each layer costs a mesh and no atlas (121.1 MiB of a 130 MB budget, --understory): 8 grass species, 8 ground foliage, 6 ferns, 8 flowers, 4 understorey conifers, 4 saplings, 3 needle-litter twigs. Kite's pine atlas is half live needles and half dead — cells 4, 5 and the bare lower-branch card are rust brown — so every crown wore rust dots and every trunk wore a tan spiky burst. Colour decides now: a texel warmer than its own green is dead wood. Small additive change outside my lane: `game.ts` publishes the new litter/bush counts in the props debug block.
…f orchard ScotsPine_01 is 19 m wide with twisted low limbs; scaled to the stand height it turned the forest into an olive grove (q-tree2 meadow-close/overview). The tall pine stays; stand variety moves to scale/lean in the veg lane.
Erosion lane: strong hydraulic+thermal bake erosion with bounded pickup, per-vertex flow/sediment/deposition/talus in the baked JSON, transport-driven surfaces and debris, crisp snow retention, bake cache keyed on the script hash (cold 48 s, warm 1.5 s). terrain.ts kept the canopy lane's forest-turf and coastal soil rules and took the erosion lane's transport-driven moss; the playtest kept both new resource assertions.
- [email protected]: develop's blocks and the Strata blocks merged into one patch; lockfile patch_hash matches the file. - [email protected]: regenerated so every recognised prior blob upgrades to the merged files. - constraints.spec: allowlist world-cell-proxy.ts and world-package.ts with tight assertions; BundleMaterialSafety renamed BundleSurfaceSafety. - forest-startup.spec: the mocked forest returns the water counts the fixture reads. - terrain specs: typed Vite server params so the root program typechecks. Evidence: pnpm typecheck exit 0; pnpm lint exit 0; vitest on packages/terrain plus core constraints, world-bundles, world-terrain-splat, world-gpu-scene, three-patch-upgrade, packaging: all pass; sync:agents --check in sync.
…ers, softer shadow fill (PRD-466 K3 rounds 5-6) Judge: ground 3, trees_rocks 3, lighting_depth 2. Bar 4 not met; K3 stays open.
PRD-466 K3, K4 and AC-5 (judge >= 4 on ground, trees_rocks, lighting_depth) stay unmet after eight rounds with CC0 assets: committed forest kit scores 3/3/3 and IMPROVEMENT over BEFORE in all four views. The three boxes move to PRD-580 with a Decisions entry (owner go-ahead, 2026-10-09). Terrain, kit, budget (cooked forest 14.1 MB < 25 MiB), runtime and playtest proofs stand. Also stops evidence growth: six canopy captures PNG -> JPG, uncited generated verification captures removed, 468 captures palette-quantized. evidence-budget spec passes under the shipped caps.
# Conflicts: # packages/core/src/world-terrain-splat.ts # packages/create-threenative/__tests__/scaffold.spec.ts
PR #381 — asset-cap WebGPU visual reviewTwo BEFORE and two AFTER runs for each matching pose, captured on NVIDIA Turing at 1920×1080. The fresh forest-kit scenario passed twice with zero diagnostics. Fresh read-only visual judge: ground, edge, overview, and lake all NEUTRAL; no visible texture, color, geometry, silhouette, terrain-contour, or shoreline regression. | BEFORE run 1 | AFTER run 1 | BEFORE run 2 | AFTER run 2 | |
…ture, so AC-4 has no coverage pair
…#635) * fix(terrain): a landform cook plants the biome's tree with the art the kit ships (PRD-806 E62) The biome files name the game's own tree (spruce/sapling, which the ordered scatter and the preview's art are keyed by), so a terrain_generate {kit, landform} cook drew no foliage and the 35-pair matrix pinned every world empty. Renaming them, as the finding sketches, would break orderedScatterRules and the preview's art, so the cook now reads the kit's own stand-in: an asset marked "tree": true (the CC0 fir for the preview's licensed spruce) plants the biome's tree row at its own count and spacing. Forest and coastal pairs now place firs; alpine, desert and tundra ship no tree, and the matrix derives each pair's expected stand from the data instead of pinning zero. * docs(strata): PR chain for the open terrain work, with the noise and real-world elevation spike * docs(strata): PR-5 makes one terrain request decide the landform, water, size and real survey * docs(strata): PR-5 estimate follows its wider scope * test(terrain): the forest golden world.glb hash follows the E46 float card copies (PRD-806) * docs(PRD-854..856): file the three new Strata chain items (request decides the terrain, ground vegetation, mountains) * docs(strata): the README records the EPIC merge and points at the PR chain * feat(terrain): a terrain_generate request's words also pick the landform, water and size, and the reply names each choice (PRD-854 phase 1) * Revert "fix(native-engine): a draw that wrote no depth keeps its custom MRT slot at 1x (PRD-806 E73)" This reverts commit 460f9e5a40f0c3df304c0791a2639e08ec9e9409. * docs(PRD-806): E46 and E56 land on PR #635; E73 is reverted after its fix removed the alpine boulder shadows * docs(PRD-585, PRD-595): the Strata web playtest passes every functional assertion and stays red on four timing budgets; enterMs regressed to 444 ms * Reapply "fix(native-engine): a draw that wrote no depth keeps its custom MRT slot at 1x (PRD-806 E73)" This reverts commit d55471289957b0af943afb311c39d31d8be8a490. * docs(PRD-806): the alpine shadow loss was run-to-run flake on an upstream three build, not E73; E73 is restored and waits for an owned-engine judge * docs(PRD-806): status line follows the boxes * feat(terrain): a landform cook grounds on a real survey with its attribution, fails closed without the tile, and takes a ground override (PRD-854 phases 2 and 3) * fix(strata): the horizon walk-band collider leaves enter, built in per-ring slices behind the curtain; fromMesh reads a plain attribute's array (PRD-585 enterMs) * docs(PRD-806): E73 passes the owned-engine judge NEUTRAL * fix(strata): the backdrop's rock beds dip, vary and break instead of drawing level contour stripes; fall-line streaks on steep rock (PRD-856) * docs(PRD-762): the MCP terrain starts on the default engine in a fresh sandbox; its CPU world path draws 97x the culled triangles * docs(PRD-856): the warped fbm box already holds; cite the code and the pinning spec * fix(three-native): the web engine's glTF loader returns every scene, so cooked LOD levels load instead of every instance drawing LOD0 (PRD-762; web host ABI 12) * style: biome format the LOD loader spec mock * refactor(three-native): name the glTF handle split * feat(terrain): gully and scree erosion passes ported from heightmap-level-generator (MIT), opt-in (PRD-856) * docs(PRD-762): the remaining main-pass cost is the CPU world path; the GPU world scene needs indirect draws on the owned engine * docs(PRD-857): the GPU world scene runs on the owned engine, from the facade audit * feat(three-native,runtime-native): hasFeature, IndirectStorageBufferAttribute, setIndirect and usage-union buffers on the owned engine (PRD-857 phase 1) * fix(runtime-native): hold the indirect-first-instance request until the engine draws indirect (PRD-857) * feat(runtime-native): draw a geometry's own indirect record on every encode path, with native red-green tests (PRD-857) * feat(core,runtime-native): cull kernels read placements as vec4 rows, and the Wasm host requests indirect-first-instance (PRD-857 phase 2) * fix(runtime-native): stop requesting indirect-first-instance, the GPU world scene hangs the first frame on the owned engine (PRD-857) * fix(PRD-857): the GPU world scene runs on the owned engine: planes from the matrix, r185 operand promotion in live TSL, a running worker game's error prints Co-Authored-By: Claude <[email protected]> * fix(PRD-857): an indirect draw reads its instances where the GPU wrote them, not the packed CPU copy Co-Authored-By: Claude <[email protected]> * fix(PRD-857): the web facade sends an updateRanges record edited in place when needsUpdate publishes it, as three and the V8 adapter do Co-Authored-By: Claude <[email protected]> * fix(PRD-857): the GPU world scene grades levels and maxDistance from the CPU path's LOD eye, so a far camera draws the same ferns on both paths Co-Authored-By: Claude <[email protected]> * feat(PRD-857): the Wasm host requests indirect-first-instance when the adapter has it Co-Authored-By: Claude <[email protected]> * docs(PRD-857): phases done, judge NEUTRAL after the shared LOD eye, acceptance ratio recorded open Co-Authored-By: Claude <[email protected]> * docs(PRD-762): the default engine selects the threejs arm's own triangle count once the GPU world scene runs Co-Authored-By: Claude <[email protected]> * fix(PRD-762): Frustum.intersectsObject and intersectsSprite on the owned engine, three r185's, for web and V8 alike Co-Authored-By: Claude <[email protected]> * fix(PRD-762): a TSL join cuts an input that runs past the vector's length, as three r185's JoinNode does Co-Authored-By: Claude <[email protected]> * fix(PRD-762): the world's visible-draw gate counts a main draw on an engine that calls onBeforeRender alone The owned engine calls onBeforeRender only for a main draw it submits after its cull, never onAfterRender, so the forest kit waited on 14 instanced batches forever. The next query settles a hook that a finished render (info.render.calls) followed; three's after-hook path is unchanged. Co-Authored-By: Claude <[email protected]> * fix(PRD-762): VirtualShadowNode's stats and settled follow the owned engine's clipmap on web Core's node counts frames in updateBefore, which the engine never runs. Each world render now reports the levels the engine's atlases still owe (web frame field 87) as stats.frame/deferred and as settled, so a game that waits for its shadow to cover new terrain, and core's startup gate, both see it settle. Co-Authored-By: Claude <[email protected]> * docs(PRD-762, PRD-858): the kit's gaps closed on the default engine; far trees need the impostor bake Co-Authored-By: Claude <[email protected]> * style(PRD-762): the shadow frame report takes the web host module's own type Co-Authored-By: Claude <[email protected]> * fix(PRD-762): the playtest worker probe is bounded, so the owned engine's pthread workers no longer hang a run Each page worker was asked for __tnGamePlatform in turn with no timeout. A Wasm pthread worker blocked in Atomics.wait never answers a CDP evaluate, so every forest-kit run hung right after the bridge connected. Co-Authored-By: Claude <[email protected]> * feat(PRD-858): an array render target draws the layer setRenderTarget names, with mips, on the owned engine three's RenderTarget depth option now holds an array: each draw copies its colour into the layer the active cube face names, an MRT attachment's too, and a draw with generateMipmaps builds every layer's chain. The facade gains getMRT, setMRT(null) and initRenderTarget, the rest of the impostor bake's seam. Co-Authored-By: Claude <[email protected]> * feat(PRD-858): a material's diffuseColor and its output/normal MRT nodes reach a render target on the owned engine The impostor bake's capture material reads diffuseColor.a and writes mrt({ output, normal }) into the atlas. diffuseColor is now an engine TSL node (colour and opacity after the map), and a material's real node under a pass target's name is written to the render target texture of that name; the pass's own markers (slot objects, normal: normalView) still stay with the scene pass. Co-Authored-By: Claude <[email protected]> * feat(PRD-858): the impostor surface draws on the owned engine: element, transformNormal and its shadow bounds The far-tree impostor material reads three's free element(node, i) and transformNormal, now on the web facade. Its quad is relocated per instance by a positionNode, so the engine's auto-depth virtual shadow refused it and lost the renderer; a displaced caster whose geometry states a boundingBox (three's authored bounds, which the impostor sets) is now placed by that box. Co-Authored-By: Claude <[email protected]> * fix(PRD-858): a failed impostor bake or surface reports its error on the TN_WORLD_IMPOSTOR line Co-Authored-By: Claude <[email protected]> * fix(PRD-858): three's mat3(mat4) lowers to the upper-left 3x3 on the owned engine Co-Authored-By: Claude <[email protected]> * fix(PRD-858): a shadow caster's alphaTestNode samples the node's own textures, and a texture change redraws the cached sun map The far-tree impostor cuts its shadow from its atlas. The masked depth program refused any texture but the diffuse map; it now binds the draw's graph textures, and those textures join the cached map's signature so a new cutoff texture is not drawn from the old map. Co-Authored-By: Claude <[email protected]> * docs(PRD-858): impostors bake on the default engine; overview wideTris 266k, frame p95 still over the threejs arm Co-Authored-By: Claude <[email protected]> * fix(PRD-762): the starter kit verifier refuses to pack a package whose src is newer than its dist A stale playtest dist packed the unbounded worker probe and hung the verifier for 32 minutes after the fix had landed in source. TN_KIT_STALE_BUILD names each stale package before anything is packed. Co-Authored-By: Claude <[email protected]> * docs(PRD-858): record the drawRange engine gap and the paired overview numbers Co-Authored-By: Claude <[email protected]> * perf(PRD-858): a node the normalNode shares with the other slots lowers once The normalNode lowered in its own Lowerer, so the world impostor's ray march (one Fn feeding normal, colour and coverage) ran twice a fragment: 12 loops and 38 atlas reads against three's 6 and 20. It now lowers through the slots' one Lowering, with normalView switched after it, as three builds a material once. A Fn call result is also kept in a scoped temporary, as three caches a call's output. Forest overview, 1920x1080, RTX 2080: impostor GPU frame 45-52 ms -> 21.7 ms, presented p95 66.7 -> 33.4 ms. Co-Authored-By: Claude <[email protected]> * perf(PRD-858): a hoisted TSL variable is declared after the variables its value reads analyze listed a hoisted variable before walking its value, so declareHoisted declared the world impostor's colour sample first; that lowered the ray march, which read the march mip level inside its 33-step loop, derivatives included. Post-order declares the input first, as three emits it before the loop. Forest overview, 1920x1080, RTX 2080: impostor GPU frame 21.7 -> 14.6 ms, presented p95 33.4 -> 16.68 ms (the threejs arm: 9.9 ms GPU, 16.68 ms presented). Co-Authored-By: Claude <[email protected]> * docs(PRD-858): the overview presents at 60 fps on the engine; record how the impostor GPU cost was found Co-Authored-By: Claude <[email protected]> * perf(PRD-858): an opaque material's program carries no alphaTest discard The flat alphaTest cutoff was a uniform test in every colour program, so each opaque draw kept a discard and lost early depth testing. three builds the cutoff only when alphaTest > 0; the variant now states it (alphaTest bit, in the program key), and an alphaTestNode still tests always. Red without the guard: one failure per material. Co-Authored-By: Claude <[email protected]> * fix(PRD-762): a frozen world draw follows a moved world root A seated world (world.position.y = -ground) drew every GPU-dressed tree and boulder 11.4 m above its terrain. freshMeshFor copied the freeze's own matrixWorldAutoUpdate = false onto the replacement mesh, and the new freeze read that as a game-owned world matrix and never composed it. - freshMeshFor restores the authored flags and transform. - markStatic thaws a frozen root before it freezes it again. - The static snapshot holds the parent's world matrix, so a root frozen before it joined a moved parent re-arms. Red then green: world-cell-fresh-mesh.spec.ts and three static-transform cases; core 4613 passed. * fix(PRD-762): the terrain snippet seats the cooked world on the game's floor The forest's centre pad sits at 11.4 m, so a pasted terrain_generate snippet buried the starter arena inside the hill and its sky read as stripes on both arms. The snippet now moves the collider anchor and the streamed world by terrainSeat, which puts the centre pad 5 cm under y = 0. terrainSeat = 0 is the named override. Red then green: terrain-mcp.spec.ts seat assertion; 144/144. * fix(PRD-762): a moved world root culls and grades in its own frame The GPU scene's planes and eye, the main-pass square cull, the gpuDraws tally and the LOD eye compared a world-space camera with package-space placements, so a seated world (world.position.y = -11.45) culled about 11 m off. They now read through localClip/localPoint; an identity root skips both. Red then green: world-cells-root-frame.spec.ts (4 of 5 red with the root ignored); core 4618 passed. * test(PRD-762): assign onto a varying and a swizzle lane, as SkyMesh writes them Covers the last sky gap: varyingProperty('vec3', 'vSunDirection').assign(...) reaches the fragment stage, position.z.assign(position.w) reassigns one lane of modelViewProjection, and a varying assigned a Fn variable is refused by name. abi_test 70/70. * docs(PRD-762): the kit verifier at HEAD fails only on frame time * docs(PRD-859): the game thread wins the CPU against everything else on the machine * perf(PRD-858): the far impostor tier draws after the nearer tiers The owned engine orders a pass's rows by state with no depth prepass, so the far quads drew before the trees that hide them and shaded every covered pixel. renderOrder = 1 puts them last; three already sorted them there. Forest kit, RTX 2080, 2 runs per view: GPU 16.6 -> 13.9 ms at spawn, 16.5 -> 13.8 ms at the overview. * fix(strata): far mountains keep their surveyed ridges The far collar blended in a twice box-blurred copy of the survey past 300 m to hide bilinear skyline stairs, which erased every rock rib and cut summits by tens of metres: a 1 km peak read as smooth clay. Sample the survey with a Catmull-Rom patch instead: C1 across cell edges (no stairs) and interpolating (every ridge kept). The spec pins both, with bilinear as its kinked control. Visual judge (fresh subagent, 3 settled runs per arm, Turing, pinned pose): IMPROVEMENT, no skyline stairs or seams. * feat(strata): pnpm dev cooks the Fab forest itself The preview needed a hand-run FAB_TEMPERATE=... prep step after every re-import, where a generated game cooks its assets on dev and build. `pnpm dev` now runs the cook with --auto: the library defaults to the asset MCP's import folder, a machine without it skips (the procedural fallback draws), and a stamp over the script, the asset cook and each pack's import report skips an unchanged library. Proof: no library -> skip, exit 0; first run cooks 67 models, 357.5 MiB in 239 s; second run 'Fab cook is current' in 0 s. * refactor(strata): the Fab cook drops the zero-normal repair the importer now does The asset MCP's 11-08 import writes no all-zero NORMAL attribute (0 of 24 spruce and Kite GLBs), so the cook's repair pass was dead. Proof: the re-cook writes the same 75 content-hashed outputs as before. * fix(engine): colorNode foliage bakes impostors on the default engine The strata preview forest logged five TN_NATIVE_DIFFUSE_COLOR errors per load: the impostor bake's capture reads three's diffuseColor, and the engine refused it beside a colorNode or opacityNode. It now lowers it from the same colour and alpha slots the colour output takes, once the program's Lowering exists (a render target's MRT slot becomes the fragmentNode, so it must be in place before any slot lowers). Those impostors then drew and lost the renderer: the quad cloned the source's castShadowPositionNode (wind rest pose), so the shadow draw skipped the billboard and its Rel varying became a missing attribute; then the cloned aoNode/emissiveNode read uv, which the quad has none of. The surface now drops the source's shadow position and surface nodes, as it already dropped its UV maps. Red-green: array_render_target read 0 0 0 then 0.2 0.6 0.4; two surface spec cases red then green. 159 native shader/material/render-database cases and 93 impostor specs pass. Page on 5310: 0 errors, 54-60 fps, frame p95 13-14 ms (was 14.1). * style(core): format the impostor surface fix * fix(engine): MRT attachments after the first clear to (0, 0, 0, 1), as three does The owned engine cleared every MRT attachment with the renderer's clear colour. three's WebGPUBackend clears attachment 0 with it and every later one to (0, 0, 0, 1). The impostor bake clears to (0, 0, 0, 0) and keeps its depth numerator in the normal attachment's alpha, so empty texels read alpha 0 there. Every mip that blends a crown edge with empty space then decoded a wrong depth, and far impostors smeared across their quads on the default engine (the forest kit overview blobs). Red-green: array_render_target reads the second attachment as 0 0 0 1 on both layers and at the mip tip (red: 0 1 0 1, the primary clear colour). * feat(strata): the ground's ambient reads the relief's sky visibility (horizon AO) Logic after TerraForge3D's horizon ambient pass (MIT, heightfield/ambient/compute.glsl); the code is ours. scripts/bake.mjs marches 16 azimuths to 160 m per vertex and writes <world>.sky.bin (one byte a vertex); the ground material multiplies its aoNode by it. Only ground over both the tangent plane and the horizontal occludes, so straight slopes and steep faces over a flat floor stay open. ?skyAo=off is the before arm. The horizon collar shares the material and now carries the attribute too: without it the owned engine stopped (TN_NATIVE_ATTRIBUTE_MISSING: sky). Proof: sky-visibility.spec.ts red then green (6 cases); judge IMPROVEMENT (weak) on alpine, 2 runs per arm. TerraForge3D's heightfield sun self-shadow was tried and removed: no visible change while the 320 m shadow levels cover a 512 m world. PRD-858 records the near-cell census lead. * feat(terrain): a river row with no layer runs down the ground's own drainage PRD-854: "create a forest with mountains and a river" failed with 'has no river layer': a landform preset authors no river. The cook now routes such a row down the finished ground (survey or preset) on the installed fillDepressions/routeFlow: the main stem of the largest basin that leaves the world, from a tenth of the outlet's catchment to the edge, with the forest starter's bed, depth and shoulder. A lake row with no layer still fails closed (no basin to sit in). Proof: terrain-mcp.spec.ts cases red on HEAD (2 failed), green after; MCP + survey specs 157/157; one real terrain_generate cook of the request (Matterhorn survey, 6.0 s, one river, 164 points, monotone, ending at the edge). * docs(PRD-860): the first scene plays before the game finishes downloading Midway boots on its spawn bytes, then textures stream by mip and meshes by LOD. Unreal 5.8.3 texture and mesh streaming read for the wanted-level rules and budgets. * docs(PRD-855): Unreal 5.8.3 grass and layer-blend defaults, each line re-read at the clone * style(terrain): format the river routing cases * docs(PRD-790): biome rows and their refusals shipped in #483, proven at HEAD * docs(PRD-583): the layered cook spec holds at HEAD; the judge stays open * docs(PRD-856): Unreal 5.8.3 erosion and landscape LOD defaults, each line re-read at the clone * docs(PRD-762): reveal time and presented FPS of the MCP forest on a real display * perf(native-engine): a draw refreshed every frame keys its sort depth without walking every vertex RenderDatabase::sortCenterOf walked every vertex of a geometry with no bounding sphere on each full refresh, and a material whose uniforms move every frame refreshes every frame. The MCP forest spent 7.8 % of its post-reveal CPU in BufferAttribute::getComponent under it. The box centre is now cached on the geometry for one revision and position version. render_database_test sort_center_walks_once_per_geometry_edit: 5 walks over 4 refreshed frames without the cache, 1 with it; a position write re-walks on the next refresh. 133/133 cases pass. * perf(native-engine): a morphed draw reuses its packed targets until a target is written The renderer repacked every component of every morph target, per morphed draw, every frame: renderPrepared's direct getComponent calls were the forest's second-largest post-reveal cost. three packs a geometry's morph texture once (src/nodes/accessors/Morph.js:93-199). The pack now lives on the geometry, keyed on its revision and each target's attribute and version. render_database_test morph_targets_pack_once_per_target_write: 4 packs over 4 still frames without the cache, 1 with it; a target write repacks and reaches the pack. 134/134 cases pass. * perf(native-engine): a morphed caster's shadow box holds every influence, with no per-vertex morph DrawCache::virtualShadowBounds morphed each vertex of a morphed mesh on the CPU every frame (Mesh::getVertexPosition with a map lookup per vertex, about 6 % of the MCP forest's CPU), and the box it built held only the current influences. It now takes computeBoundingBox's box, the positions grown by each target's reach as three bounds a morphed mesh, cached on the geometry for one revision and the position and target versions. render_database_test morphed_shadow_bounds_hold_every_influence: a target that moves the box 10 m read max.x 1 at influence 0 before (the box tracked the influence), 19 at both influences now, as three's box reads; render-database 135/135, geometry-edges 9/9. * docs(PRD-762): three per-vertex walks named and removed from the forest's post-reveal frame * docs(PRD-856): the far rings are cooked from same-site USGS surveys, proven by the baked relief * fix(strata): the sand band follows each world's water level The beach weight read an absolute world height (7.5 m to 1 m), so it only matched a coast at y 0. It now reads the height over the world's own water level (6 m to -0.5 m). PRD-591 AC-5: a fresh visual judge rates all five worlds NEUTRAL, two runs per arm; sheets on pr635-screenshots. * docs(PRD-594): the pre-#381 parent draws no virtual shadow on the fixture, so AC-4 has no coverage pair * docs(PRD-856): the mountain defect list from the 2f317c907 frames * docs(PRD-762): the default engine judges NEUTRAL against the threejs arm on the MCP forest * docs(PRD-583): the cooked forest renders headed and passes the engine judge * feat(core): a whole-file load after the reveal reports TN_ASSET_LATE (PRD-860) The reveal is the first moment the player is playing, so a file the scene asks for after it starts a download over a world already on screen — the pop-in a streaming game exists to avoid, and one no screenshot can prove. One line per file, carrying the bytes and how far into play the request landed, so a playtest counts pop-ins. A file the game `prefetch`ed first is a deliberate stream, and the engine's idle background fill is no promise either: it lags the link by design, so only `prefetch` silences the line. `startBackgroundFill()` already marks the reveal, so the line needs no new public API. Measured on Midway: with all 228 cooked files awaited at boot the line stays at zero, which is what a game that streams nothing should report. * fix(strata): the forest's bare earth reads as humus, L* 53 to 35 at the walk pose The litter tint alone left the bare patches between the stands brighter than the turf beside them. BARE_SOIL darkens only that mask. PRD-855: a fresh judge rates IMPROVEMENT, two runs per arm. * docs(PRD-855, PRD-856): the forest walk logs no scene warning, and the edge walk still climbs 58 m * fix(strata): each forest grass clump takes its own hue and a shaded root The licensed meadow drew one photographed colour per species and read as a carpet. PRD-855: a fresh judge rates IMPROVEMENT on the close and river frames, two runs per arm, mean L* held. * docs(PRD-855): the loop shows the same late-pipeline flake with and without the ground change * docs(PRD-856): the horizon ring measures no seam gap on three worlds * docs(PRD-860): execution measures 136.4 MB before first play and four findings The cook cannot reach the tens of megabytes this PRD assumed: the default engine refuses KTX2 and meshopt (TN_ASSETS_COMPRESSION_SKIPPED), so Midway ships 166.2 MB cooked from 188 MB of sources and the load split alone has to reach the 50 MB ceiling. Records the compact-instance/clone mismatch, the two game defects a cooked build exposes, and the SwiftShader lane that cannot settle Midway here. No box is ticked. * docs(PRD-583): the game has no first-layer arm for the layered judge * perf(strata): the sky LUT and the river build run in load, not in the world enter The atmosphere's compute dispatch was 13-16 ms of every enter and the tundra braid 15-18 ms. Fresh-load enters fall to 29-37 ms on all five worlds. PRD-585 box 62 stays open: a random 20-130 ms block lands on some key switches in both arms. * docs(PRD-856): the forest backdrop squiggles are lit normals, not snow * docs(PRD-806): E49 red-green on the V8 texture test; chain step 0 review and README done * test: impostor surface and river routing specs typecheck under TS 7 strict * docs(strata): chain step 0 gates ran green on the PR head * fix(terrain): alpine, desert and tundra dress the mid boulder their assets bake (PRD-806 E57) * docs(PRD-806): every low finding answered: 43 fixed in the EPIC, E57 fixed, E68 rejected * docs(PRD-858): the near-fir census gap is resident instances; the GPU scene culls them * test(strata): the build-package fixture carries each world's sky visibility file * docs(native): regenerate the coverage record for the chain's engine source * fix(runtime-native): the WebGPU facade imports IndirectStorageBufferAttribute it re-exports * docs(PRD-806): AC-1 ticked from the green board on 2c9262966 * docs(PRD-584): the forest walk is GPU-bound at 20 ms on 13.6 M near-fir triangles * docs(PRD-806): E57 judged NEUTRAL on the alpine kit; every low finding closed * fix(terrain): a landform cook keeps its trees out of the starter arena (PRD-806 E62 judge) The E62 judge found firs growing through the spawn pad and inside the walls: a landform preset's foliage rows had no keep-out, and the kit worlds stayed clear only because their own terrain left the centre bare. Each foliage rule now clears a 23 m circle at the world's centre, the starter arena's half-diagonal plus a fir crown. Red: nearest tree 3.3 m from centre; green: >= 23 m. * docs(PRD-806): E62 judged IMPROVEMENT once the landform cook clears the arena * docs(PRD-806): done — every medium and low epic-review finding is fixed or rejected * docs(PRD-806): status DONE, links and the strata index row follow the move * feat(strata): one height joins the square to the collar with no step (PRD-790 Phase 2 groundwork) Streamed tiles past the playable square need one height function. The collar read its seam step at the nearest edge vertex, so the joined surface stepped 0.56 m between vertices; it now reads the square bilinearly along the edge. The collar mesh is unchanged (forest and coastal vertices within 1 mm). * docs(PRD-861): the owned engine's missing decoders get their own PRD Asked why a cook for the owned engine cannot ship compressed assets: it is a missing decoder in the engine's glTF loader, not a missing format. third_party/meshopt/ already holds the decode-only meshopt subset and no CMake file compiles it; cgltf parses EXT_meshopt_compression and documents view->data as the seam. KTX2 has no transcoder in the tree, so it is sliced behind an owner decision. PRD-860 now points at it. * docs(PRD-860): the runtime measures 139.5 MB awaited, and who waits it A cooked briefing run settles on the owned engine (load 11.3 s, enter 15.6 s, 120 frames, tone and diagnostics green) and reports its own split: visible 42.2 MB, unattributed 21.9 MB, off-screen 75.4 MB. Names the five files and flags that two of them read not-placed because merged geometry loses its provenance, so they must be checked before anything defers them. Also documents on startBackgroundFill that it is the reveal seam TN_ASSET_LATE reads. * docs(PRD-580): five-kit art baseline, no rubric row at 4 yet * docs(PRD-580): the elevated white void is the missing horizon, which PRD-790 Phase 2 supplies * docs(PRD-790): the streamed-tile design, split by one square around the player * feat(strata): stream ground tiles past the square behind ?stream=1 (PRD-790) * docs(PRD-790): judge NEUTRAL; late pipelines are the lake mirror churn, not the tiles * docs(PRD-790): the lake pipeline churn keeps one program; the render-object key flips * docs(PRD-790): the lake pipeline churn is in the owned engine's cache, not three's * fix(strata): the lake draws both sides in one pass, so a turn rebuilds no pipeline (PRD-790) * docs(PRD-790): the four late pipelines left, each named by its first draw * fix(strata): the curtain waits for the first streamed tiles to draw (PRD-790) * fix(strata): read groundReady from the meadow's destructured options * docs(PRD-790): a lake warmed behind the curtain fixes the mirror pipelines but its screen copy fails there * docs(PRD-585): the strata job is skipped by TN_CI_STRATA_GPU=false, not by a failure * refactor(strata): one world list feeds the bake, the keys and WorldName (PRD-790) * fix(core,strata): build the shadow reset quad and the lake mirror behind the curtain (PRD-790) * docs(PRD-790): stream run green on a quiet host; tile seams 0 * feat(core): KTX2 level index, Unreal wanted-mip rule and coarse-first mip ranges (PRD-860) Pure groundwork for Phase 2: parseKtx2LevelIndex reads the header, the SGD block a BasisLZ level needs, and the level index, failing closed with TN_KTX2_HEADER. wantedMipLevels is UE 5.8.3 StreamingTexture.cpp:309 (ceil(1 + log2(screenSize))). mipRangesToFetch returns the missing mips smallest first. Not exported yet: the loader wiring decides the public shape. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(playtest): the bridge handshake waits the startup window, not 30 s (PRD-860) Core answers describe only after the start scene's load() settles, and a load is sized by the scenario's own throttled link. Midway awaits 139.5 MB, 30 s at 40 Mbps, and failed describe 370 ms short of it (TN_PLAYTEST_OPERATION_TIMEOUT). describe and ready now get PLAYTEST_STARTUP_READY_TIMEOUT_MS, as the capture session already does; waitForBridge keeps its short budget, so a page with no bridge still fails fast. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(PRD-863): one river surface for the preview and the kits, drawn a reach past each end The river water stopped in a straight edge at its first station while WaterBodies floods a round cap past it. riverRibbonGeometry in @threenative/core/world draws that cap; the preview and the forest and tundra kits call it instead of three private ribbons. Visual judge: IMPROVEMENT on two runs. * feat(PRD-863): the lake surface the swimmer floods is a core mesh lakeSurfaceGeometry in @threenative/core/world draws the disc WaterBodies floods, out to lakeReach, wet cells only, with a metres depth attribute. The preview calls it; its forest lake mesh is unchanged (6 992 triangles, positions exact). * feat(PRD-863): shore distance and open-sea exposure are one core field shoreField in @threenative/core/world replaces the same 57 lines in the preview's ocean and the coastal kit's sea. On the coastal bake both arrays are identical to the old code across 66 049 nodes. * feat(core): TN_ASSET_UNUSED names the awaited models nothing drew (PRD-860) A minute after the reveal the engine walks the scene once, stamps only, and warns with every model the reveal waited for that no scene object came from, hidden nodes included. Midway paid 22.9 MB of its launch for three such files (samidare, the Yorktown import, the torpedo body); TN_STARTUP_OFFSCREEN_BYTES could only call them not-placed. The fix line covers both readings: delete a load nothing builds, prefetch one the game builds later. Measurement only, cancelled by stop(). Co-Authored-By: Claude Opus 5.5 <[email protected]> * style(PRD-863): format the lake coverage spec * fix(core): a failed start-scene load answers the runner's describe at once (PRD-860) The start scene's load() catch tore the game down without rejecting the start gates, so a runner's describe waited out its whole handshake budget (30 s, now 180 s) on a page that had already failed with TN_ASSETS_UNRESOLVED. Every other start failure already rejected them. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(PRD-860): AC-1 and AC-2 ticked with the 40 Mbps RTX 2080 runs; frame time stays open Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(PRD-863): streamed ground uses core's budget, settled block and warm-up TerrainTiles.settledHalf reports the largest settled block around a point, using the tiles' own hole and extent. warmUnculled draws a subtree whole behind the curtain until it has drawn twice, for the tiles and the lake. AdmissionBudget is exported. stream and loop read 0 late pipelines. * docs(PRD-863): TerrainTiles names settledHalf's situations for the capability search * fix(core): a compile no longer copies the canvas into a refracting material's scene copy three's compileAsync runs a viewport texture's updateBefore from a render context it never bound to a target, so the backend copied the canvas (rgba8unorm) into the HDR scene copy (rgba16float) and logged a format error per refracting material on every held warm-up. A compile draws no pixels, so core skips that copy outside a real frame. Strata loop: 3 errors in 3/3 runs before, 0 in 2/2 after. * refactor(PRD-863): the preview's biome bands come from measureBands measureBiome adapts the preview's fraction record to the terrain package's measureBands instead of repeating its arithmetic; the measured biome is deep-equal on all five worlds. The spawn gate stays in the game: core's startup.hold already owns its deadline. * feat(PRD-863): the drawn-surface sampler is core's surfaceSampler surfaceSampler in @threenative/core/world answers Heightfield.surfaceAt under the mesh's transform, null outside the field or under a rotation. The preview's copy is deleted; its raycaster and collider spec now runs on the core function. * docs(PRD-863): AC-2, every moved export ranks first for its situation * docs(PRD-790): the dotted line was likely the old river cut; water specks noted * feat(PRD-864): static prop posing is core's staticLowestY, footprintGap and baseRingGap The preview's exact lowest point, flat-footprint sink and crag base ring move to @threenative/core/world with capability tags. preparePose calls them and keeps its per-asset burial shares; old and new poses are bit-identical. * perf(web-engine): glTF models parse on a loader thread, not in play (PRD-860) Midway's late fleet froze play for 2.4 s in tnw_web_load_gltf: one synchronous 100-500 ms task per hull on the main thread (DevTools trace, RTX 2080). The web host now runs gltf::load on one long-lived loader pthread (tnw_web_load_gltf_begin/done/finish); the main thread polls a ticket every 4 ms and only shares the finished model into the context. A build without threads runs the load at begin, as before. GLTFLoader's web parse awaits the result; the sync export stays. The loader thread builds engine objects while the main thread builds its own, so the object, material, geometry and attribute id counters are atomic. concurrent_ids (4 threads x 20 000 meshes, wasm-threads under node) failed 3/3 with the plain counters and passes 3/3 now. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(PRD-864): model sections, shared base and normal repair are core meshSections, sectionGeometry and repairZeroNormals move from the preview's pack loader to @threenative/core/world with capability tags. pack.ts keeps its per-world reshapes; the section geometry is bit-identical to the old code. * feat(PRD-864): the prop bucket decode is the terrain package's decodePropBucket decodePropBucket reads a compiled bucket back to each record's ID, model, pose and reach, beside compilePropBuckets. The preview's recordsOf maps it to placements; the decode is deep-equal on the forest bake. * perf(web-engine): the glTF loader thread decodes on its own two workers (PRD-860) Per-thread CPU while Midway's fleet streamed in: the 8 JobPool workers ran at 649% for ~2 s a model on the loader's image decodes, and the main thread sat at 98-102%: a frame job that finds the pool busy runs inline on the caller. JobPool::useOnThisThread gives a thread its own pool; the loader thread uses one of 2 workers, so the frame keeps all 8. Idle workers measured at 1-2% CPU each, so the profiler's busy samples on them are parked waits, not a spin. thread_pool_override (wasm-threads, node): red without the API, green with it; for_each_once, pool_starts_on_need, item_failure_reaches_the_caller and dispatch_floor stay green. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(PRD-860): the loader thread and its pool, with the paired frame runs Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(PRD-864): fetchAsset is core's one resolve-then-fetch loop fetchAsset walks every url the loader resolves and names each miss in TN_ASSET_NOT_SERVED. loadTerrainSplat and the preview's fetchBakedFile call it. The constraint guard lists world-model-sections.ts, which splits a mesh by the game's material slots and builds no material. * docs(PRD-864): why four other fetch loops keep their own walk * docs(PRD-863): the kits keep their contour-clipped lakes; they run no swim query * docs(PRD-863): AC-1, every mechanism moved or kept with a reason * Revert "perf(web-engine): the glTF loader thread decodes on its own two workers (PRD-860)" This reverts commit 2bae24eb7852396237e65b0a5e48e5b9708ae6ec. * docs(PRD-860): quiet-host paired runs, and why the loader pool was reverted Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(PRD-595): the preview walk passes after the split; machinefall walk open * docs(PRD-798): the Strata, abyss and judge box closes; its parts are ticked * docs(PRD-854): snapshot names the new fields; PRD-854 done The MCP surface snapshot specs pass 153/153. The capability-manifest half of the last box is dropped: the manifest indexes code exports only. * fix(PRD-863): a river cap past an end over falling ground is not drawn; PRD-863 done At the tundra meltwater head the ground falls away upstream, so the cap a reach past the head hung 2-3 m over snow (fresh judge: REGRESSION). The ribbon now drops a cap whose ground sits below the bed at its end station. Forest ribbons are identical; tundra 27 600 -> 27 522 triangles. Fresh judge on the fixed arm: NEUTRAL, 2 runs per arm. AC-3 ticked. * fix(core): addInSlices presents a frame between slices once the world is on screen (PRD-860) In Chrome a scheduler.yield continuation runs ahead of rendering, so a chain of 8 ms slices presented no frame until the ~100 ms starvation guard fired. Midway streaming its cockpit after the reveal read a 102 ms host gap at p95 against 9 ms with nothing streaming. After the reveal, a slice now waits for the loop's next frame, then one host yield, and spends only the headroom the frame left (1-8 ms). Behind the curtain it keeps the host yield and 8 ms. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(three-native): an InstancedBufferAttribute is isInstancedBufferAttribute (PRD-860) Midway's smoke, fire and muzzle flashes were invisible on the owned engine: its particle batch re-sends only the attributes three's flag names, and the flag read null, so the GPU kept the zeros the batch was created with. defineTypeFlags now flags InstancedBufferAttribute, InstancedBufferGeometry and InterleavedBufferAttribute, on web and on the desktop player. Red-green: object-surface.spec.ts (1 failed, then 8/8). * fix(web-engine): the loader-thread calls take web host ABI 12 (PRD-860) c5d2ede9e added tnw_web_load_gltf_begin/done under ABI 11, so web-host-abi.spec.ts failed (digest a60bf14c, pinned 6ca2e45b). Both numbers move to 12 and the digest is pinned. * docs(PRD-858): the forest kit's GPU tail is the sun shadows and full resolution, not the mirror * test(three-native): every bound class carries the is* flags three's own instance sets A guard spec builds the web back end's classes from the registry and catalog, applies the same flag passes, and compares each against a real three instance. It found nine more missing flags (CubeTexture, Data3DTexture, DataArrayTexture, DepthTexture, MeshPhysicalMaterial, RenderTarget, Matrix4, CatmullRomCurve3, Source/TextureSource); they join the hand list. Removing InstancedBufferAttribute from the list turns the spec red, so the PRD-860 smoke bug cannot return silently. * test(three-native): the flag guard covers the JS ports and three/webgpu; node materials read their classic flag The guard now builds the JS ports (QuadMesh, GridHelper, cube and 3D targets, EdgesGeometry, Box2, typed attributes) over fake engine classes that answer any member, looks a class up in three/webgpu too, and fails on any class it cannot build instead of skipping it. It found four more gaps: three's node materials copy their classic defaults, so MeshStandardNodeMaterial reads isMeshStandardMaterial (MeshBasic, MeshPhysical and Sprite likewise); the owned engine read undefined. * feat(core): a KTX2 texture streams by Range, mip tail first (PRD-860 AC-4) openKtx2Stream reads the header, metadata and mip tail in one 64 KiB Range read, hands back a whole KTX2 file of the levels on hand, and reads finer levels on want, coarse first. A host that ignores Range is read once and kept whole. Partial files keep the descriptor, key/value data and the kept levels' BasisLZ image descriptors; three's ktx-parse reads every one. * feat(assets): a LOD-chained model ships a coarse companion with its own vertices (PRD-860 Phase 3) A chain's levels share the base level's vertex buffers, so nothing coarse can download first. The model pass now writes <name>.coarse.<digest>.glb beside the model: each chained primitive draws its coarsest level over compacted vertices, through the same writer, so its images are the model's own shared files. The manifest lists it under streamedLods. * fix(core): a LOD level's triangle count is read once, not every frame ModelLod.triangles re-read geometry.index for every LOD mesh each frame; on the owned engine each read is a bridge crossing, and Midway's fleet flight failed its playtest on TN_BRIDGE_WARNING (528 reads a frame). A level is an index view nothing edits, so its count is cached per geometry. * feat(core): a streamed model draws its coarse companion first, then the full model in place (PRD-860) ctx.assets.model loads <name>.coarse.<digest>.glb when the manifest lists one and the game did not prefetch the model, weighs the request by the coarse bytes, and fetches the full file at soon priority. upgradeStreamedGeometry gives each coarse geometry the full base's index and attributes in place and registers the full chain under it, so every mesh over it, the game's clones and a projection mirror included, draws the full model and walks its LOD chain. A failed coarse load falls back to the whole file. assets.streamMeshes: false is the named override (TN_ASSETS_STREAM_MESHES_OFF). Ticks AC-3: the fleet playtest reaches Kaga at 673 m with zero TN_ASSET_LATE lines. * fix(assets): the compile cache keys on the coarse companion A warm cache from before the companion re-served every model without streamedLods: Midway's first cook after 70ea62eab wrote no coarse file. The model pass configuration now carries MODEL_COARSE_COMPANION_VERSION. * docs(PRD-860): AC-7 bytes pair and AC-8 web judge evidence * docs(PRD-860): tick AC-8, web judge NEUTRAL and native desktop SAME * feat(PRD-861): the owned web engine cooks and loads meshopt geometry The Wasm engine links meshoptimizer's decoders (refusing to link without them), GLTFLoader on the owned engine accepts a meshopt decoder it does not need, and webRuntimeDecoders answers meshopt: true for engine "threenative". Red-green: tests/web-host-meshopt.test.mjs loads the 912-byte meshopt GLB in the real Wasm engine. Without meshopt: TN_NATIVE_GLTF_EXTENSION_UNSUPPORTED EXT_meshopt_compression. With it: status 0, one scene handle. * test(create-threenative): the native-engine web cook emits meshopt geometry * feat(strata-forest): a continuous wood, lit grass and a 60 FPS walk pose Look (forest world, visual judge: IMPROVEMENT on all four poses against ab0155cd7): - grass blades take the ground's up normal, so no card facing away from the sun draws black - tall tufts run their root shade over their own height and sit in the field's tone - straw patches and a metre-scale drift break the one green - spruce crowns are conifer green, each tree leans its own hue and value, paler at the top - the forest row plants 5 649 trees in the square (was 3 131) and 2.25x the grass - aerial perspective reaches the backdrop (height fog distance scale 4) Performance (RTX 2080, 1600x900): - the ground draws after the props, so the depth test rejects its shading under crowns and blades: walk poses 37-49 -> 60 FPS, GPU frame 13.6 -> 8.9 ms at 913x881 - GTAO derives normals from depth (UE 5.8.3 default), which drops the second scene normal pass: GPU frame -17% to -26% over two interleaved rounds; ?aoNormals=mrt is the override - spruce takes its far level from 105 m and grass thins at 30 m and 60 m: triangles -17% to -20%, GPU frame about -8% at the overview; ?lodSteps=off is the override Not yet green: six pinned tables (bake golden, biome values, prop package, far-forest cap) still hold the old forest row and are re-measured in the next commit, with the understorey row. Co-Authored-By: Claude <[email protected]> * feat(terrain): the understorey is a biome row, and the forest grows ferns, litter and flowers through its cover The layer under the canopy was fixed numbers in the ordered scatter. It is now `cover.understorey` in each biome file: the candidate budget, a multiplier per species (litter, fern, bush, flower) and a crown shade (the share of grass a trunk's ring loses, and its radius per unit of tree scale). A row that thickens the layer draws from a stream of its own and the graded draws pass unplaced, so no later layer moves: a boulder moved two layers on when the forest first grew more ferns. A tree the water takes shades nothing. Locked by a test (a thicker row and a shade leave every spruce, sapling, boulder and river rock at its pose) and by the bake: the four other worlds are byte-identical (16 of 16 baked files). Forest row: 120 000 understorey candidates, ferns x2.2, litter x1.6, a 1.2 m trunk ring. The flower models' own leaves were photographed pale mint and drew as white paper rosettes: their green texels take the sward's green, the petals keep their photograph (found with a paint-by-species capture). Ferns thin with distance as the field grass does. The play-area clear mask is drawn 2% wider so its soft edge lies outside the promised 23 m: the denser forest row put a trunk at 22.79 m in the alpine-under-forest cook. Visual judge (fresh, r3b vs r4e, four poses): IMPROVEMENT overall, meadow-close and river IMPROVEMENT, player and overview NEUTRAL, rocks and trees in place. Tests: packages/terrain 608/608, strata-terrain-preview 274 passed 3 skipped. The forest's pinned tables are re-measured for the new row. Co-Authored-By: Claude <[email protected]> * feat(strata-forest): a look tweak box (?tweak), crown shade on the understorey and prop tints Carries the uncommitted look work left in the strata-forest-aaa lane: the terrain editor's tweak box (packages/terrain/src/editor/tweak-box.ts), the forest's look knobs as live uniforms, and the understorey, prop and terrain changes that use them. Checks: terrain specs editor-tweak-box, bake-biome, biome-spec, bake-golden (28 pass); example specs bake-golden, biome-values, prop-package (7 pass); tsc on packages/terrain and the example (exit 0); biome on each file clean. Visual judge: not run on this commit. * docs(PRD-861): name PRD-860 without a link to a file this tree does not carry * feat(PRD-861): the owned engine transcodes a model's KTX2 textures The engine's glTF loader reads KHR_texture_basisu through Basis Universal v1_60 (Apache-2.0, pinned in download-deps and the lock, with its decode-only zstd). It transcodes the base level to RGBA8, the one upload format the engine's Texture has, so every GPU block target is compiled out. The Wasm engine refuses to link without the transcoder. The cook learns ktx2Files: a standalone .ktx2 (texture, lightmap) goes through three's KTX2Loader, whose CompressedTexture the owned engine does not bind. The owned web engine now answers { ktx2: true, meshopt: true, ktx2Files: false }: model textures cook to KTX2, standalone textures keep their authored format. Red-green: - tn-native-engine-gltf-ktx2-test (Wasm toolchain, node): without basisu TN_NATIVE_GLTF_EXTENSION_UNSUPPORTED KHR_texture_basisu; with it ETC1S red and UASTC+zstd green decode within 8 levels, a damaged KTX2 is TN_NATIVE_GLTF_IMAGE_INVALID image 0. - assets runtime-decoders.spec.ts: without ktx2Files the standalone texture pass still ran; with it only the model pass and the resize pass run. - tests/web-host-decoders.test.mjs: the real Wasm engine loads both fixtures. Web engine size: threaded 4,172,446 of 4,194,304 bytes (+103.6 KB). * wip(PRD-866): surveyed massif placement and 4 m relief * fix(PRD-861): the owned engine's web cook keeps KTX2 off, by measurement Midway's web bake with model KTX2 on shipped 196.0 MB, against 124.7 MB with meshopt and WebP model textures: the owned engine transcodes KTX2 to RGBA8, so a KTX2 saves no GPU memory there and only costs bytes. webRuntimeDecoders answers { ktx2: false, meshopt: true } again and ktx2Files goes (no consumer left). The loader keeps KHR_texture_basisu, so an authored KTX2 model now loads instead of being refused, and the web engine no longer needs basisu to link. The skip line named both decoders even when meshopt ran; it now names only the one the target lacks (red: "no qualified meshopt and KTX2 decoder"; green: "no qualified KTX2 decoder", report.spec.ts). * docs(PRD-861): meshopt lands on the owned web engine; KTX2 decodes but stays out of its cook AC-2, AC-3, AC-4 and AC-6 ticked with their evidence. AC-1 and AC-5 stay open on the KTX2 line: the engine transcodes KTX2 to RGBA8 only, and that costs Midway 71.3 MB, so the cook keeps KTX2 off until the renderer uploads a compressed format. * test(PRD-861): the hierarchy test stops expecting KHR_texture_basisu refused when the transcoder is linked Red (native coverage build): KHR_texture_basisu: not refused by name (), 1 of 12 files differ. Green: native and Wasm ctest, hierarchy, ktx2 and meshopt 6/6. * docs(PRD-861): refresh the native coverage record for the meshopt and KTX2 loader pnpm --filter @threenative/runtime-native native:coverage exit 0; pnpm budgets exit 0. * docs(PRD-867): plan the owned engine's compressed texture upload and single mip levels * feat(PRD-866): a surveyed mountain with wooded lower slopes stands behind the forest placeMassif gains a face option and ignores off-world cells in its top bound; the host-grid layer decimates through a tent. The forest pins seed 6 (Maroon Bells) facing south into the low sun, its treeline and snow raised to wood the lower slopes. Specs: tile hashes and relief, the 4 m relief atlas, 8 seeds of placeMassif, and the massif's lower-slope share. * style(PRD-866): format the massif tile metadata * feat(PRD-867): the owned engine uploads BC7 and ETC2 textures, and KTX2 transcodes to them A Texture whose source holds compressed levels (three's CompressedTexture.mipmaps, in RGBA_BPTC_Format or RGBA_ETC2_EAC_Format) uploads each level in its block format; a device without the feature refuses it by name. The renderer tells the loader which format its device samples, and a KTX2 image transcodes to every level in that format. A device with neither keeps RGBA8, and TN_TEXTURE_TRANSCODE_TARGET says so. Red-green: on PRD-861's head the KTX2 map loaded as 256 RGBA bytes. Now, on Dawn over Vulkan on the RTX 2080 (compressed_texture_test): 8x8 BC7, 64 bytes, a mapped plane reads back 253 0 0 (RGBA control 255 0 0); a featureless device: "TN_NATIVE_TEXTURE_UNSUPPORTED: BC7 needs the device feature texture-compression-bc". Native ctest 9/9, Wasm toolchain 7/7, web-host-decoders 2/2. Web engine: the transcoder builds at -Oz on the web (BC7+ETC2 cost 145,855 bytes at -O3, 12,201 at -Oz). Threaded 4,184,647 of 4,194,304 bytes. ASTC deferred. * fix(PRD-866): the massif's near flank fades over 600 m behind a 650 m keep-out The judge read the near flank as a pale fog wall over the overview: it rose from 420 m on a 160 m ramp. It now starts at 650 m and fades in over 600 m. * docs(PRD-866): phase 1 and placeMassif boxes ticked with their results * feat(PRD-866): terrain_generate cooks a far ring with a real massif An alpine or temperateMountains world on a survey writes ring.json: the ground's survey continued to ten times the world's side, with one whole mountain from that range (Longs Peak; Grand Teton or Maroon Bells) placed past the square by seed in its true metres. decided.backdrop and the result's backdrop report it; backdrop: "none" cooks none; a massif tile that is neither cached nor fetchable fails the cook naming it. * feat(PRD-867): three's CompressedTexture and KTX2Loader run on the owned engine, and a finer mip level writes in place The engine binds CompressedTexture: (levelCount, width, height, format), then one `mipmaps.N.data` per level, coarse first. Texture::writeMipLevel fills a level; the renderer uploads only the levels written since its last upload into the GPU texture that is already there, and rebases the view on the finest resident level. The browser facade builds three's CompressedTexture(mipmaps, ...) over it, answers renderer.hasFeature("texture-compression-bc") from the device (a new web frame field), and takes three's own Loader, FileLoader, LoadingManager and Cache modules, so KTX2Loader constructs, detects BC7 and returns a CompressedTexture that draws. Red-green: the facade spec failed on the old texture-sources.ts (TN_BROWSER_ARGUMENT_UNSUPPORTED); the browser page failed with TN_NATIVE_UNCATALOGUED_LOADER. Now: native ABI/GPU/KTX2/registry 66/66 on Dawn/Vulkan (RTX 2080: a finer level uploads 64 bytes, coarse 0 0 0 then 254 0 0); three-native 578/578; examples/wasm-engine-boot playtest:compressed pass on nvidia turing (BC7 load 251,0,0; streamed coarse 0,0,0 then 253,0,0). Web engine threaded 4,193,090 of 4,194,304 bytes. Phase 2 stays open by measurement: Midway's KTX2 cook is 189,852,625 B against 125,132,608 B with WebP (+51.7%, mostly UASTC images), so webRuntimeDecoders keeps ktx2 off for the owned engine. * feat(PRD-860): a streamed model's KTX2 textures draw their resident mips first and take the whole files when they land The coarse parse reads each shared .ktx2 through the facade GLTFLoader's new imageSource hook, which serves the resident levels (openKtx2Stream). The full parse loads whole files, and upgradeStreamedTextures swaps them into the coarse materials. assets.streamTextures: false is the named override; TN_STREAMING_POOL reports resident and deferred bytes. * feat(PRD-866): a cooked world bakes into the forest, and a seed moves the survey crop STRATA_WORLD=<terrain_generate out> pnpm bake takes the cook's ground, scatter seed and far ring. A seed moves a landform's survey crop across the same range, so seeds give different grounds. verify-variations.mjs cooks three seeds through the terrain MCP over stdio and bakes each. * docs(PRD-860): the capability manifest names coarse-first model and texture streaming and their overrides * feat(PRD-867): the web cook keeps a WebP a KTX2 cannot undercut, and the owned web engine cooks KTX2 Midway's 300 KB WebP metallic-roughness and normal maps became 4 MB UASTC files because the keep-smaller-source rule kept only PNG and JPEG. On the web every runtime reads a WebP, so it is now a source the cook may keep (MODEL_WEBP_RETAIN_VERSION rides the cache key). Midway's KTX2 cook drops from 189,852,625 B to 98,236,952 B, against 125,132,608 B for WebP, so webRuntimeDecoders turns KTX2 on for the owned engine. * fix(PRD-866): a forest under mountains cooks a true-scale valley floor, and its heightmap meets its ring 'under mountains' names the far ring's range; the ground stays the kit's own landform, cut at true scale (512 m at z15) at a seeded spot gated as a forest floor, with the surveyed worlds' erosion pass instead of the preset's fluvial rescale, which lowered the border 10-40 m under the ring. The ring writes the massif's own detail grid at z13 for the relief map. verify-variations gates floor relief, slope, seam, massif relief and wooded foot on three seeds through the MCP. The vista eye never sits under the drawn ground. * style(PRD-866): verify-variations names its env key without a lint suppression * fix(PRD-866): the forest's treeline and snow stay in metres above the floor on any cooked ground * fix(PRD-861): the owned web engine reads a meshopt-compressed LOD chain Midway's models failed to load on the owned engine with TN_WASM_GLTF_LOD_ACCESSOR: accessor N reads past the binary chunk. Since PRD-861 the web cook meshopt-compresses this engine's models, and the LOD chain's index views moved into a compressed stream, but the hosted LOD plugin's accessor reader read the raw binary chunk. It now decodes a compressed view with the MeshoptDecoder core already hands to setMeshoptDecoder, and refuses by name without one. * wip(PRD-866): far forest near band behind ?farForest=near|on|off (tuning, default under test) * style(PRD-866): biome import order and format * fix(PRD-867): a KTX2 decoded on a pool worker no longer hangs the threaded web engine Midway's KTX2 build hung after its first load steps, the main thread blocked for good. decodeImage printed TN_TEXTURE_TRANSCODE_TARGET from inside the loader's JobPool. On the threaded Wasm engine a pthread's stdout write is proxied to the main thread, which spins in forEach until every worker leaves the job, so a worker that decoded the process's first KTX2 never returned. The lines now print from the loading thread (reportKtx2) after the pool joins. Threaded web engine 4,193,549 B, 755 B under budget. * perf(PRD-866): a far spruce draws its coarsest cooked level until its impostor (vista props 28.6 M -> 16.3 M triangles) * feat(PRD-866): a cooked forest of another size keeps its camera poses past its own edge * docs(PRD-870): the procedural forest set reads like the Fab set; ?fab=off draws it alone * docs(PRD-867): tick AC-4 and PRD-861 AC-1 with the starter bake, record the cook and LOD fixes, refresh native coverage * feat(PRD-866): a small or huge forest cooks and bakes; the floor's relief bar scales with its square (5 forests green) * build(PRD-867): the web engine budget is 4 MiB + 64 KiB With PRD-860 merged, the threaded web engine is 4,198,621 bytes (KTX2 transcode to BC7/ETC2 plus streaming). The KTX2 cook it enables saves Midway 26.9 MB of download. * perf(core): a whole-asset impostor's quad fits its baked box, not its bounding sphere A spruce covered about a quarter of its sphere's square; every fragment outside its silhouette ran the lit impostor shader to be discarded. The quad now spans the box footprint's circle across and the box's projection on the frame's up, with slack, never past the sphere. The fragment marches the view ray, so a lit fragment needs a ray through the box: the projection is a strict bound. GPU ms, strata forest, 1600x900, RTX 2080, headed WebGPU, 4 windows each: overview far off 37.6 -> 34.6, far near 66.5 -> 57.7; vista far off 46.0 -> 40.7, near 72.8 -> 64.0. Visual judge (fresh, two runs per arm, player/overview/vista): NEUTRAL, no clipped or missing tree. Sheets: ~/Downloads/strata-forest-aaa/fit-quad/sheets. Spec: world-impostor-surface 96/96 with the impostor specs (corners of the box inside the quad from 500 random views). * docs(PRD-870): phase 1 measured: spruce canopy colour is the largest gap to the Fab set * look(strata): the procedural spruce crown takes the Fab spruce's dark blue-green and dark core The generated crown's floor lift (0.03 linear) raised every texel of its grade to one value, so the crown read as flat light yellow-green paddles. Floor 0.004, a darker cooler grade, a near-black trunk end and warm olive tips. meadow-close tree band (sky dropped), middle tenth sRGB (65,88,66) -> (56,69,63) against Fab (47,55,44). Fresh visual judge, two runs per arm, ?fab=off&farForest=off: meadow-close, overview, player IMPROVEMENT, vista NEUTRAL; overall IMPROVEMENT. Remaining gap: value and contrast (Fab ~30% darker with near-black gaps). Sheets: ~/Downloads/strata-forest-aaa/prd870/sheets-crown. * docs(PRD-866): the far band's cost is impostor fragments, mislabelled as shadow; fitted quad numbers * look(strata): a procedural meadow flower is one low stem with a small head, as the Fab flower it stands in for The game places flowers as densely as the Fab meadow's single flowers, so the fallback's five-stem colony with 9 cm heads per placement drew a salmon carpet. One stem, 0.40-0.58 m, 6 cm head. Salmon share of the lower half, ?fab=off&farForest=off, two runs each: meadow-close 0.209 -> 0.020 (Fab 0.008), vista 0.055 -> 0.009 (Fab 0.0007). Fresh visual judge: meadow-close and vista IMPROVEMENT, player NEUTRAL; overall IMPROVEMENT. Spec: ground-vegetation 5/5. Sheets: ~/Downloads/strata-forest-aaa/prd870/sheets-flower2. * look(strata): a stand spruce shows its trunk to two fifths of its height, in warm bark A spruce grown in a closed stand loses its shaded lower whorls; the procedural tree kept its crown to a fifth of its height, so the stand was a wall of cards with every trunk hidden. Stand trees (SPRUCE.standBareTrunk 0.4) now start their crown two fifths up; saplings keep 0.22. The bark lift is warmer and brighter, so a shaded trunk reads brown instead of a black pole. Fresh visual judge, two runs per arm, ?fab=off&farForest=off: meadow-close IMPROVEMENT (trunk sRGB (59,72,49) -> (106,79,44), Fab about (79,60,47)), overview and vista NEUTRAL; overall IMPROVEMENT. Spec: ground-vegetation 6/6 (crown lowest point above 0.24 of height, sapling below 0.22). PRD-870 progress and the player-pose finding recorded. Sheets: prd870/sheets-trunk. * docs(PRD-870): the procedural overview cost sits on the far spruce level; one card per spray thins the canopy (judged, reverted) * fix(PRD-860/867): review findings on streamed models and the owned engine's KTX2 Fable 5.2 review of #643: - A KTX2 model loads on a GPU with no compressed format three's KTX2Loader takes: the owned engine's loader transcodes KHR_texture_basisu itself, so its format check no longer gates it. - The texture upgrade swaps only slots that still hold the coarse model's own textures, so a texture or material the game set meanwhile stays; unused full-model copies are released. It moves to streamed-textures.ts, so model-lod.ts stays under the no-appearance constraint. - ctx.assets.model(path, { whole: true }) waits for the full file; CollisionShape3D.fromMesh names a trimesh or hull copied from a coarse model (TN_COLLIDER_COARSE); isStreamedGeometry. - Auto-batch keeps a streamed coarse model out of a merge. - The full load stops when the model is released, cleared or the loader disposed. - A host without Range hands its whole KTX2 on instead of a second fetch; a short 206 and a section past the bytes read fail by name; unused mip helpers deleted. - Threaded web engine: the main thread refreshes its heap views when a glTF load ends and once per frame; the hierarchy revision is atomic. * feat(PRD-867): a KTX2 model draws on the owned web engine, proven on screen; PRD-861 and PRD-867 done - examples/wasm-engine-boot playtest:ktx2-model loads ktx2-textured.glb through the engine's glTF loader and reads the ETC1S and UASTC maps' texels (251,0,0 a…





























































































Merged — final status (2026-10-10)
PR381 squash-merged into develop at
2d212479255cf725f64343dc72aa2eafe9b7b92d, from final headba284199ddbc8bd8b15574548ab5b35f75565eee. Final-head CI run completed SUCCESS; all 68 PR checks are successful or intentionally skipped. Latest develop changes were integrated before merge.João approved NEUTRAL for the size-only asset compression change on 2026-10-10. The screenshot comparisons below remain available. The combined local upload-fix candidate below was not part of the merged PR; its commits and evidence are preserved locally. Its earlier pending-approval and CI notes are historical, not current PR status.
Combined develop and remote-repair candidate — NEUTRAL, awaiting owner exception
Local candidate
31f813c72includes latest fetched develop1a5314a1d, its world streaming updates, remote CI/asset repairs throughf32d791cf, and the verified pending-instance-upload fix. Remote starter assets are preserved; the earlier six-GLB cook is safely stashed. This candidate has not been pushed.Root typecheck, lint, full budgets, 60 focused terrain/editor/scaffold tests and native loading proof pass. Both headed NVIDIA 1920×1080 AFTER kit runs pass with zero diagnostics. The fresh independent visual judge inspected all 16 frames: NEUTRAL for all eight comparisons and overall, with no visible geometry or texture regression. All lake labels are present (the judge corrected its initial label observation). Repository AGENTS.md rejects NEUTRAL unless the owner grants the pending exception; U2 remains open.
Remote CI run
38043597475coversf32d791cf, not this combined local candidate, and currently has six failed template jobs reporting SwiftShader device loss andInstance dropped error in getCompilationInfo. The run remains in progress; no implementation cause is established. Candidate CI and merge queue are pending.Combined candidate — run 1
Combined candidate — run 2
The optional
@threenative/terrainaddon evaluates Strata headlessly and bakes ordinary Three.js data. The editor shares a validated project document with its agent controller. Games receive explicit baked data and own their appearance, physics and lifetime.Status: PRD-466, PRD-467 and PRD-468 at 100%
docs/PRDs/done/.docs/PRDs/done/.docs/PRDs/done/.pnpm prd:progressprintsprd:100% — readyand exits 0 for all three.Decision: the art bar moves to PRD-580
PRD-466 boxes K3, K4 and AC-5 required a fresh judge subagent to score at least 4 on ground, trees_rocks and lighting_depth. Eight bounded rounds used only CC0 assets (Poly Haven
fir_tree_01,boulder_01, Kloofendal HDRI). The committed forest kit scores 3 / 3 / 3 and the judge returns IMPROVEMENT over BEFORE in all four views (edge, ground, lake, overview). Parameter levers plateaued.The judge names these blockers: faceted leaf-card fir crowns, flat blown-out sky in elevated views, stair-stepped shorelines, tiled flat ground texture and hard dark shadow slabs. None of the three boxes is ticked. They move to PRD-580 with the score history, and PRD-466 carries a Decisions entry (owner go-ahead, 2026-10-09).
Score history (ground / trees_rocks / lighting_depth): before ~2–3 each, R2 2/2/3, R3 3/2/3, R4 2/2/3, R5 2/3/3, R6 3/3/2, R7 3/3/3, R8 3/3/3 (neutral, reverted).
Visual evidence
BEFORE and committed R7 captures (real WebGPU, NVIDIA RTX 2080). R8 was reverted. The visual judge returned IMPROVEMENT in all four views; the art scores remain 3 / 3 / 3, with the remaining work in PRD-580.
What this PR keeps
ece594ad8: baked world 34,312,316 bytes exceeds 25 MiB; packed starter assets 50,695,422 bytes exceeds 40 MiB. Both failures reproduce locally instarter-kit.spec.ts; correction is in progress.Verification run on the final head
ece594ad8pnpm check:docsandcheck-doc-links,evidence-budget,evidence-citations,sync-agent-docsspecs: 39/39 pass.ci:fastdrift checks: agents, docs, lint, drift pass.KIT=forest pnpm --filter strata-terrain-preview test:kit) produced the R7 captures. Itsperformance.maxPhaseMsP95.renderassertion returned null under machine load (load average 30–60). The captures are valid. The FPS claim is not made.Asset budget CI repair — visual decision pending
The candidate saves 8,818,128 bytes: baked forest 25,494,188 / 26,214,400 bytes; packed starter assets 41,877,294 / 41,943,040 bytes. All four starter-kit tests pass, with caps and self-contained GLB requirements unchanged. Geometry, material bindings, decoded PNG pixels and embedded metadata checks pass.
Two fresh headed WebGPU runs per arm passed at 1920×1080 on NVIDIA Turing. The independent judge inspected every frame: NEUTRAL in all eight comparisons; no repeatable visual regression, no visible improvement. This does not satisfy the owner visual gate. An exception for this size-only cook is pending.
Earlier six-GLB candidate — superseded by combined candidate above
Latest develop
1a5314a1dis merged locally inee86f4757, including the world streaming updates. The pending instance-upload fix passes 179 focused core tests, core build/typecheck, and selected NVIDIA browser/desktop native cases with zero GPU errors and pixel mismatch / DeltaE 0. The selected conformance commands exit 2 because 101 unselected rows remain blocked; no full-matrix claim. The scaffold merge resolution passes all 95 scaffold/backlight tests.Both fresh unprofiled AFTER kit runs pass, with zero diagnostics, headed NVIDIA Turing at 1920×1080. A fresh read-only judge inspected all 16 matched frames: overall NEUTRAL; no visible geometry or appearance regression. Run1 edge and lake are IMPROVEMENT; six other pairs are NEUTRAL. All labels are present (the judge corrected its initial lake-label finding after reopening originals). The earlier rejected lake artifact was not observed, but its cause is not established. The initial first-sample timeout also remains unexplained; an unchanged diagnostic rerun and both unprofiled runs pass.
The six-asset cook saves 8,818,128 bytes and meets the packed/baked budgets. The owner rule rejects NEUTRAL absent an exception; the existing size-only-cook exception request remains pending. PRD remains 75%; no candidate code push, fresh CI, or queue until visual acceptance. These screenshots supersede the prior candidate for review; its rejected history is preserved below.
Merged develop verification — rejected visual set
Latest fetched
develop(6202a1c10, including PRD-567 height blending and PRD-572 shadow classification) is merged locally as094493120; CI repairs are committed locally asdfe45b1fe. These code commits are not pushed yet. The rebuilt core passed all four headed NVIDIA 1920×1080 kit playtests. The fresh independent judge inspected all 16 images: overall REGRESSION. AFTER run1 lake contains a flattened foreground tree and detached shoreline trunks; seven other comparisons are NEUTRAL. Cause is under investigation. This historical set superseded the older cook-only result; the corrected-upload comparison above is now the current review candidate. This historical set remains rejected.