Skip to content

feat: integrate Strata terrain, live editor and game handoff (PRDs 466–468) - #381

Merged
jonit-dev merged 442 commits into
developfrom
feat/prd-466-468-strata
Oct 10, 2026
Merged

jonit-dev merged 442 commits into
developfrom
feat/prd-466-468-strata

Conversation

@jonit-dev

@jonit-dev jonit-dev commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Merged — final status (2026-10-10)

PR381 squash-merged into develop at 2d212479255cf725f64343dc72aa2eafe9b7b92d, from final head ba284199ddbc8bd8b15574548ab5b35f75565eee. 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 31f813c72 includes latest fetched develop 1a5314a1d, its world streaming updates, remote CI/asset repairs through f32d791cf, 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 38043597475 covers f32d791cf, not this combined local candidate, and currently has six failed template jobs reporting SwiftShader device loss and Instance 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

Pose BEFORE AFTER
ground BEFORE run 1 ground AFTER run 1 ground
edge BEFORE run 1 edge AFTER run 1 edge
overview BEFORE run 1 overview AFTER run 1 overview
lake BEFORE run 1 lake AFTER run 1 lake

Combined candidate — run 2

Pose BEFORE AFTER
ground BEFORE run 2 ground AFTER run 2 ground
edge BEFORE run 2 edge AFTER run 2 edge
overview BEFORE run 2 overview AFTER run 2 overview
lake BEFORE run 2 lake AFTER run 2 lake

The optional @threenative/terrain addon 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%

PRD Result
PRD-466 terrain integration 4/4 phases, all retained boxes ticked. Moved to docs/PRDs/done/.
PRD-467 live editor 3/3 phases, 8/8 boxes. Moved to docs/PRDs/done/.
PRD-468 world controls and asset imports 3/3 phases, 7/7 boxes. Moved to docs/PRDs/done/.

pnpm prd:progress prints prd:100% — ready and 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.

View BEFORE AFTER — committed R7
Edge BEFORE edge AFTER R7 edge
Ground BEFORE ground AFTER R7 ground
Lake BEFORE lake AFTER R7 lake
Overview BEFORE overview AFTER R7 overview

What this PR keeps

  • Forest asset budgets currently fail on ece594ad8: baked world 34,312,316 bytes exceeds 25 MiB; packed starter assets 50,695,422 bytes exceeds 40 MiB. Both failures reproduce locally in starter-kit.spec.ts; correction is in progress.
  • Terrain, kit, runtime and playtest proofs in the three PRDs.
  • Evidence growth stopped: six canopy captures PNG to JPG, uncited generated verification captures removed, 468 captures palette-quantized. The evidence-budget spec passes under the shipped caps.

Verification run on the final head ece594ad8

  • pnpm check:docs and check-doc-links, evidence-budget, evidence-citations, sync-agent-docs specs: 39/39 pass.
  • Pre-push ci:fast drift checks: agents, docs, lint, drift pass.
  • Not re-run on this head: the full board, native build lane. CI runs them on this PR.
  • Kit proof (KIT=forest pnpm --filter strata-terrain-preview test:kit) produced the R7 captures. Its performance.maxPhaseMsP95.render assertion 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.

Pose / run BEFORE AFTER
ground / run1 BEFORE ground run1 AFTER ground run1
ground / run2 BEFORE ground run2 AFTER ground run2
edge / run1 BEFORE edge run1 AFTER edge run1
edge / run2 BEFORE edge run2 AFTER edge run2
overview / run1 BEFORE overview run1 AFTER overview run1
overview / run2 BEFORE overview run2 AFTER overview run2
lake / run1 BEFORE lake run1 AFTER lake run1
lake / run2 BEFORE lake run2 AFTER lake run2

Earlier six-GLB candidate — superseded by combined candidate above

Latest develop 1a5314a1d is merged locally in ee86f4757, 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.

Pose / run BEFORE Corrected AFTER
ground / run1 BEFORE ground run1 AFTER corrected ground run1
ground / run2 BEFORE ground run2 AFTER corrected ground run2
edge / run1 BEFORE edge run1 AFTER corrected edge run1
edge / run2 BEFORE edge run2 AFTER corrected edge run2
overview / run1 BEFORE overview run1 AFTER corrected overview run1
overview / run2 BEFORE overview run2 AFTER corrected overview run2
lake / run1 BEFORE lake run1 AFTER corrected lake run1
lake / run2 BEFORE lake run2 AFTER corrected lake run2

Merged develop verification — rejected visual set

Latest fetched develop (6202a1c10, including PRD-567 height blending and PRD-572 shadow classification) is merged locally as 094493120; CI repairs are committed locally as dfe45b1fe. 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.

Pose / run BEFORE AFTER
ground / run1 BEFORE merged develop ground run1 AFTER merged develop ground run1
ground / run2 BEFORE merged develop ground run2 AFTER merged develop ground run2
edge / run1 BEFORE merged develop edge run1 AFTER merged develop edge run1
edge / run2 BEFORE merged develop edge run2 AFTER merged develop edge run2
overview / run1 BEFORE merged develop overview run1 AFTER merged develop overview run1
overview / run2 BEFORE merged develop overview run2 AFTER merged develop overview run2
lake / run1 BEFORE merged develop lake run1 AFTER merged develop lake run1
lake / run2 BEFORE merged develop lake run2 AFTER merged develop lake run2

@jonit-dev jonit-dev added prd:0% PRD implementation has no verified phase yet prd:25% PRD phase 1 verified and removed prd:0% PRD implementation has no verified phase yet labels Oct 1, 2026
@jonit-dev

Copy link
Copy Markdown
Contributor Author

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 036e8bf4b.

Browser terrain and road, after a 50-metre walk. Textures, vegetation and river rendering are still pending.

Browser terrain and playable road

Browser coastal terrain with the installed spectral ocean. Shoreline foam and the final lighting/art pass are pending.

Browser coastal terrain and spectral ocean

Linux desktop coastal preview, captured in the native host. The ocean clock now advances during fixed simulation ticks.

Linux native coastal preview

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.

@jonit-dev

Copy link
Copy Markdown
Contributor Author

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 4994d6505.

Browser coast, with moving spectral waves and shoreline foam:

Browser shoreline

Linux native coast, using the same game-owned surface:

Native shoreline

Linux native view after moving the actual sun:

Native alternate sunlight

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.

@jonit-dev

Copy link
Copy Markdown
Contributor Author

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):

Live terrain editor before agent edit

After the agent increases hill amplitude (80):

Live terrain editor after agent edit

Human GUI edit saved to the shared document:

Recovered terrain controls with saved GUI edit

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.

@jonit-dev

Copy link
Copy Markdown
Contributor Author

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.

Live terrain editor with 100 rendered pine props

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.

@jonit-dev

Copy link
Copy Markdown
Contributor Author

Individual prop gizmos — fc6a5c3f09. Progress: 3 of 9 phases verified. These are actual 1440 × 900 browser WebGPU captures on NVIDIA/turing RTX 2080.

  1. Select one pine by a real mesh click or the object list. Numeric fields edit its position, rotation and nonuniform scale.

Selected pine and numeric transform inspector

  1. Drag ordinary Three.js rotation and scale handles. Only that instance matrix changes; each completed drag saves once.

Pine after actual rotation and scale drags

  1. Reload restores the saved transforms. A Y-handle lift explicitly disables grounding and keeps measured clearance visible: 2.5549 m in this capture.

Reloaded pine with explicit grounding override and measured clearance

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.

@jonit-dev

Copy link
Copy Markdown
Contributor Author

Retained-preview fix — 1b5224730f. Progress: 3 of 9 phases verified.

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.

Actual retained preview with an unrendered saved recipe and named missing-asset error

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.

@jonit-dev

Copy link
Copy Markdown
Contributor Author

Landform transforms — c2134b83d8. Progress: 3 of 9 phases verified; PRD-467 AC-3 now passes. These are actual 1440 × 900 browser WebGPU captures on NVIDIA/turing RTX 2080.

  1. Select the stable recipe layer. Its footprint follows the rendered terrain; Y rotation is available and overhanging X/Z rotations are disabled with a reason.

Selected recipe footprint with supported Y rotation

  1. Move, rotate and change vertical gain/offset through the same ordinary Three.js gizmo used for props. The footprint previews during drag; the heightfield rebuilds after the single committed gesture.

Landform after real translation rotation and vertical scale drags

  1. Imported height data has a bounded, translated, rotated and anisotropic footprint. Numeric fields use metres and degrees.

Imported heightmap after footprint and height gain edits

  1. Pasted height data uses the same recipe transform contract.

Pasted height data with saved transform parameters

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 landform without identity collisions. Prop gizmos, stale conflicts, reload, grounding, pending/failed revisions and cancellation remain green. The 100-prop fixture's three terrain revisions draw in 164–188 ms.

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.

@jonit-dev

Copy link
Copy Markdown
Contributor Author

Visual lane, step 1 of 3 — work in progress, not at the Gaia bar yet. AC-5 stays open. Commits cbc008ab8 (ground), 12477abe0 + 25785c5d9 (props), ce3d6d450. Real 1920 × 1080 browser WebGPU captures (NVIDIA/turing RTX 2080, headed). Reference target: temperate spruce meadow + alpine snow ridge.

What changed

  • Ground: six CC0 Poly Haven PBR sets (grass, forest ground, cliff, moss rock, sand, snow; 17 MiB of the 25 MiB budget, per-file sha256 and authors in credits.json), blended by height/slope/painted weights with triplanar cliffs and anti-tiling.
  • Props: seeded spruce (bark-textured trunk, drooping needle-card whorls, 3 variants), angular boulders, blade-grass clumps, placement read from the baked ground.
  • Ocean verifier now scales to the 1920 × 1080 captures (it hard-coded 1280 × 720); both ocean checks pass (66% / 51% of water pixels change).

Close meadow (new fixed benchmark camera, 1.7 m)

Meadow close

Overview

Overview

Coast

Coast

Defects I can see (will fix next, then re-judge):

  1. Spruce foliage is stippled (alpha-test noise) and reads pale/washed-out at distance — no real needle silhouette yet.
  2. Trunks render near-white: the bark map is not reaching the trunk material.
  3. Some boulders float above the ground; one stray prop hangs in the sky in the overview.
  4. Grass is sparse thin blades, no poppies yet, and the dirt bands are flat blobs.
  5. Sky is a flat colour: no clouds, haze or aerial perspective. Ocean/horizon unchanged.
  6. The desktop host does not load the ground textures yet (flat fallback), so this is web only until fixed.

Earlier tracked captures from the export milestone (cca803e08): vanilla GLTFLoader, native ordinary game, reassigned override.

PRD-467 AC-9 (embedded spatial inspection) is ticked. Progress: PRD-466 2/3 phases, PRD-467 1/3, PRD-468 0/3.

@jonit-dev

Copy link
Copy Markdown
Contributor Author

Visual lane, checkpoint 2 — 4a01edece. Still not at the Gaia bar; AC-5 stays open. Real 1920 × 1080 WebGPU captures (RTX 2080, headed).

Fixed since the last post: trunks and crowns were solid constant colours because a node material ignores map once colorNode is set. They now multiply the sampled texture, so the spruce has real brown bark and green needle bundles. Boulder bases are buried 20% of their height. The browser scenario and both ocean checks pass.

Close meadow (1.7 m)

Meadow close

Overview

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.

@jonit-dev

Copy link
Copy Markdown
Contributor Author

Visual lane, checkpoint 3 — 277220896. Closer, still not Gaia; AC-5 stays open. Real 1920 × 1080 WebGPU captures (RTX 2080, headed). typecheck, root lint and the browser scenario pass; both ocean checks pass (waves 0.72, sun 0.83), 0 console errors, 1.2 ms p50 frame with 752k prop triangles. Desktop lane not run for this step.

New this round: installed Daylight Preetham sky + game-owned cumulus, sun shadows (virtual clipmaps), sky-coloured aerial haze, AgX tonemapping; dense spruce crowns at every distance (stipple gone), red poppies, thick blade grass with a focus-following density plateau, frayed dirt edges with litter, surf that is not a contour, and a new sea-horizon camera. A floating prop in the overview was a leftover query fixture, now undrawn.

Close meadow (1.7 m)

Meadow close

Overview

Overview

Sea horizon

Sea horizon

Where it still falls short of the references (my read, next round):

  1. Poppies are flat red quads, not flowers; grass sits in even rows with no clumping or colour variation.
  2. Spruce crowns read as cards, not needle-dense branches; trunks too thin/dark at the base.
  3. Rocks are smooth low-poly lumps: no stratification, no moss/lichen, no contact darkening.
  4. Overview is over-hazed, flattening the terrain; the ridge has no snow/rock transition.
  5. Coastal foreground shows 2 m heightfield facets and stretched sand texture; surf near the horizon is streaky.
  6. Desktop host still lacks the ground textures (flat fallback) — web-only until fixed.

@jonit-dev

Copy link
Copy Markdown
Contributor Author

Visual lane, V2 progress — c2e7e2828. Better, still not AAA; AC-5 stays open. A fresh judge scored checkpoint 3 at ~4.5/10 (below shippable-indie), so V2 works its ranked defect list item by item. Real 1600 px WebGPU captures (RTX 2080, headed); JPEG q85 to respect the repo's evidence cap.

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.

Before: V1c meadow

After (V2-2):

After: V2-2 meadow

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.

@jonit-dev

Copy link
Copy Markdown
Contributor Author

Visual lane, V3 progress — 31e9406ff. Rocks are now real; trees are still not good enough. AC-5 stays open. Real 1600 px WebGPU captures (RTX 2080, headed), JPEG.

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.

Close meadow
Overview
Player

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.

@jonit-dev

Copy link
Copy Markdown
Contributor Author

Visual lane, V3 — 6e76f8414. Real progress, still not Gaia: a fresh judge scored the last round 2.5/10 and this one 3.5/10 against the Gaia references. AC-5 stays open. 1600 px WebGPU captures (RTX 2080, headed). Web scenario: everything passes except diagnostics, which fails on two "ShadowDepthTexture used in a submit" errors that the unmodified HEAD also emits. Meadow p50 is 1.2 ms, overview 1.2 ms, 20 prop draws.

What changed

  • Spruces: the branch droop only ever existed in the wind weight, so every branch was dead level, which made the stacked flat tiers. Branches now really droop, each branch is three small rolled sprays instead of one paddle, crowns are lit as a volume, and the colour is a darker blue-green. A needle "floor" bug that flattened the atlas to one brightness is fixed.
  • Water: a stream follows the terrain's real drainage into a lake. The lake has a planar reflection through the engine's WaterSurface3D.
  • Terrain (engine fixes, unit-tested): roads and pads now follow the ground. The "sausage" was a road pinned 6–19 m above the slope. Hydraulic erosion left 167 single-cell spikes up to 10.6 m tall, the brown cones; there is now 1 left, which is the summit.
  • Terrain blend: rocks and trunk bases fade into soil. The engine has no terrain-blend feature, so this is game-side in src/render/.
  • Ferns: bracken under the spruces, from a CC0 frond atlas.
  • Atmosphere: a clearer sky and less haze.
  • Performance: markStatic on the terrain and props.

Meadow
Stream and lake
Overview
Walk

Judge's top remaining defects

  1. Flat lighting: no strong sun, weak shadows, a grey haze wall.
  2. Bare ground: the grass covers only the meadow focus, and the brown blotches ignore the topography.
  3. Spruces up close: noisy, with pale trunk-flare cones.
  4. Water edges: a hard polygon edge on the grass, and a ribbon-straight stream.
  5. Terrain macro form and the world edge.

Dropped as unworkable at this budget:

  • Decimated scan trees: the Fab and CC0 Poly Haven scans keep only 0.3–20% of their crown.
  • Decimated scan plants: they keep a single blade or frond.

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 ClusteredMesh/ClusteredBatch (virtual geometry baked by the asset cook), plus assets.lod cooked LOD chains. This example loads raw GLBs outside the asset cook, so it uses neither yet. Routing the rocks through ClusteredBatch is the next performance step.

@jonit-dev

Copy link
Copy Markdown
Contributor Author

Visual lane, V4 — dff98349d. A fresh judge scored V3 3.5/10. V4 scores 4.0/10 on the owner's machine (licensed pack) and 3.5/10 for the committed fallback. Still not Gaia; AC-5 stays open.

New this round

  • Real trees and plants: the owner's licensed Fab pack Landscape Pro 2.0 (pines, bush, grass clumps, rocks), loaded through Wildwood's own material and wind code. It is local-only and gitignored, with the importer committed, the same way Wildwood handles it. CI and clones fall back to the procedural spruce and the CC0 rocks.
  • Water: Wildwood's water on the stream and lake: waves, depth colour, soft shore.
  • Sky (engine fix): Daylight was fogging its own sky dome, so every game on it drew a flat grey sky. The sky is now blue with white clouds. Unit-tested red then green. Contrast is 0.70 against the Gaia reference's 0.69.
  • Three new landforms: Alpine, Desert/Canyon and Snow/Tundra are baked but not rendered yet.

Meadow
Stream and lake
Walk
Overview

Judge's verdict

  • Better: near pines (photoreal bark and branches), the sky at ground level, and no raised road or spike cones.
  • Worse: the water. Its ripples read as black blobs, which a fix in progress addresses.
  • Still wrong:
    • Distant trees dither into blue-grey blobs.
    • There is no ground cover outside the meadow.
    • The flowers are cartoon cutouts next to photoreal pines, which breaks the single-style rule.
    • The overview still ends in a white wall at the world edge.
    • The committed fallback still uses the stylized procedural spruce.

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 test:consumer harness).

@jonit-dev

Copy link
Copy Markdown
Contributor Author

Visual lane, V5 — 15bc23b5f. A fresh judge scores this round 4.3/10, up from 4.0. Real progress on terrain and horizon, but still not Unreal-level; AC-5 stays open.

New this round

  • Erosion algorithm (owner: "double check your algorithm"). Four engine bugs were fixed in the terrain package, each red-green. The droplet count didn't scale with the terrain grid. The per-step bite was so deep that more droplets carved dimples, not drainage. A layer with no brush did nothing because its weights were all zeros. The spike counter reported zero without counting.
    Now: forest relief 76 → 91 m, slopes over 30° 3.8% → 22.4%, alpine 21% → 36%, tundra 0.6% → 22%.
  • Ground (Sol 6.1 lane). The brown blob decals are gone. Layers now follow slope, height and curvature, with large-scale grass variation and triplanar rock.
  • Horizon. The landscape now continues 2.1 km past the 512 m world, so there's no edge or void.
  • Lighting. Stronger sun and shadows. Ground shadow luminance is 0.065 against Gaia's 0.061.
  • Editor. PRD-467 AC-4, AC-5 and AC-6 are green and ticked.

Meadow
Walk
Overview
Stream

Judge's top remaining defects (two Sol 6.1 lanes are now working on them):

  1. The rock layer reads as pale plaster.
  2. Ground cover almost disappeared in this round.
  3. Low-poly boulders.
  4. Gullies read as dug trenches.
  5. Distant mountains are plain cones.
    Also: trees are sparse and too dark.

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.

@jonit-dev

Copy link
Copy Markdown
Contributor Author

Visual lane, V6 — 1b72e2caf. A fresh judge scores the forest world 5.0/10 (up from 4.3) and the coastal world 2.5/10. The forest now reads as good indie up close, but neither world is Unreal-level, so AC-5 stays open.

New this round

  • Engine fix: scene switches no longer freeze. VirtualShadowNode registered its shadow depth targets before three.js had created them. three then destroyed textures that bind groups still held, every submit was rejected, and the picture froze. That is why Coastal showed the forest frame. The fix registers each target as soon as it exists. It was red-green proved with a unit test against three's real texture manager: 4 destroys before, 0 after.
    The full web scenario now passes every assertion, including diagnostics, with 0 console errors. That is the first green run of this scenario on this PR. The four templates that use VirtualShadowNode get the fix through core.
  • Forest (Sol 6.1 lane). It is now built from Project Nature Spruce Forest photoscans:
    • 3,200 trees and 4,144 saplings.
    • About 180k grass and ground-foliage clumps, plus photographed flowers.
    • Kite photoscanned boulders, river rocks, scree and cliffs.
    • Frame cost: meadow 2.3 ms, overview 2.1 ms (p50 engine work).
      Licensed art stays local-only; the procedural fallback was checked with it removed.
  • Ground and atmosphere (Sol 6.1 lane).
    • Grey-brown steep rock with detail normals.
    • Softer, curved drainage.
    • The lake now sits in its basin, with planar reflection and wet banks.
    • Layered distant mountains.
    • Half-resolution contact AO.
  • PRD-466: AC-6 (custom assets) and AC-7 (discoverability) are proven and ticked, and the new test:consumer harness is green. All five worlds match hash-for-hash outside the workspace.

Meadow
Lake
Walk
Overview
Coast
Coast horizon

Judge's ranked remaining defects (next round is already being worked):

  1. Spruce crowns have a frosted mint sheen; the references are deep green.
  2. Mid and far ground is one flat olive fill, and the overview still shows gully streaks.
  3. Coastal water: opaque foam blobs, including in lagoons, no Fresnel or sun highlight, stray ellipse sprites, an aliased horizon, and a slab-like cliff.
  4. Cut-out clouds and matte-card distant mountains.
  5. Hard, sheared crown shadows; rocks not blended into the ground; poppies with black stems.

@jonit-dev

Copy link
Copy Markdown
Contributor Author

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.

Forest meadow Forest walk Forest overview
Coast Alpine Desert Tundra

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:

  • Worlds r3: Kite Demo mountain-scale photoscans (SM_MountainRock, LargeVolcanicRock_001–004) cladding steep faces, as UE/Gaia build cliffs; RockFace003 macro+detail rock material; slope/exposure snow; real buttes with vertical walls and strata; tundra fringe and exposure fixes.
  • Forest/coast r11: atlas dilation for the card fringe, crown depth darkening, size and age variation, macro ground variation, eroded horizon mountains, shore foam and wet sand.

Licensed Fab bytes stay local-only; CI runs the procedural fallback.

Copy link
Copy Markdown
Contributor Author

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 (e91805c) and will reconcile the current develop base without force-pushing. No ownership of the active art/biome lane is implied; AC-5 and complete five-world AC-8 remain open.

@jonit-dev

Copy link
Copy Markdown
Contributor Author

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.

Forest meadow Forest walk Forest overview
Coast Alpine Desert Tundra

What changed

  • Crowns: the cyan/white crown fringe is fixed. The root cause was ambient occlusion multiplying the foliage cards' alpha, not atlas bleed. Crowns are darker and more varied.
  • Horizon: mountains now have real peaks.
  • Alpine and desert: Kite mountain photoscans are on steep faces. Desert mesas have vertical walls.
  • Tundra: the sky no longer blows out.

What's still wrong

  • Alpine: the crags read as a repeated boulder pile on a smooth cone (score dropped to 2.5).
  • Snow: hard stencil edges instead of slope- and height-driven blending.
  • Desert: strata are evenly spaced stripes, and rocks sit on vertical walls.
  • Tundra: a Voronoi crack pattern is tiled over the ground.
  • Forest and coast: painted rock patches, a lawn-like far meadow, clay-like coast land, and weak contact shadows where props meet the ground.

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).

@jonit-dev

Copy link
Copy Markdown
Contributor Author

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.

Forest meadow Forest walk Forest overview
Coast Alpine Desert Tundra

What changed

  • Alpine: the crags are now embedded rock ribs, sunk by probing a ring of points around each base.
  • Desert: layered sandstone beds with talus at the foot of the walls.
  • Coast: a rocky shoreline built from Kite rocks.
  • Tundra: the white-fringed grass is fixed. It was the same cause as the forest crowns: ambient occlusion multiplying the cards' alpha.
  • Forest: rock outcrops replace the painted patches.

Honest read: the score is creeping up (3.6 → 3.8 → 3.9), not jumping. The judge keeps naming the same structural causes:

  • Spruces: olive cards with no light passing through the needles.
  • Background mountains: smooth, un-eroded continuation cones.
  • Snow: noise blobs instead of snow driven by slope and altitude.
  • Shadows: a coarse far cascade causes stair-stepping.
  • Light: flat overall.
  • Player capsule: a debug capsule is visible in the shots.

The next rounds target those directly:

  • Already running: a light, atmosphere and colour-grade lane across all five worlds.
  • Next: eroded continuation mountains and normal-driven snow, then the shading of the spruce foliage cards.

@jonit-dev

Copy link
Copy Markdown
Contributor Author

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).

Forest meadow Forest walk Forest overview
Coastal Alpine Desert Tundra

What changed

  • Per-world sun and sky, haze that thickens in valleys and brightens toward the sun, contact shadows where props meet the ground, and a colour grade per world.
  • Snow blends by slope and height.
  • Eroded mountain ring around the forest (baked in 14 s), irregular mesas with buttes, and a higher-resolution far shadow map for alpine and desert.
  • The player capsule is hidden in showcase views; the tundra grass grid and texture seam are fixed.
  • Scenario: 0 failed checks, ocean check green, no console errors.

Honest read: lighting and the far mountain ring did not move the score. What the judge says still holds it back:

  1. The landforms you walk on are smooth and un-eroded.
  2. The spruces read olive and card-like.
  3. Ground cover is thin over bare soil, with a hard shadow edge on forest-walk.

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.
jonit-dev and others added 4 commits October 9, 2026 20:28
- [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.
@jonit-dev jonit-dev added prd:75% PRD phases in, acceptance open and removed prd:50% PRD half the phases verified labels Oct 10, 2026
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.
@jonit-dev jonit-dev added prd:100% — ready PRD complete, ready to merge and removed prd:75% PRD phases in, acceptance open labels Oct 10, 2026
@jonit-dev
jonit-dev marked this pull request as ready for review October 10, 2026 05:26
@jonit-dev jonit-dev added prd:75% PRD phases in, acceptance open and removed prd:100% — ready PRD complete, ready to merge labels Oct 10, 2026
# Conflicts:
#	packages/core/src/world-terrain-splat.ts
#	packages/create-threenative/__tests__/scaffold.spec.ts
@jonit-dev

jonit-dev commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor Author

PR #381 — asset-cap WebGPU visual review

Two 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 |
|---|---|---|---|---|
| Ground | BEFORE run 1 ground | AFTER run 1 ground | BEFORE run 2 ground | AFTER run 2 ground |
| Edge | BEFORE run 1 edge | AFTER run 1 edge | BEFORE run 2 edge | AFTER run 2 edge |
| Overview | BEFORE run 1 overview | AFTER run 1 overview | BEFORE run 2 overview | AFTER run 2 overview |
| Lake | BEFORE run 1 lake | AFTER run 1 lake | BEFORE run 2 lake | AFTER run 2 lake |

@jonit-dev jonit-dev added prd:100% and removed prd:75% PRD phases in, acceptance open labels Oct 10, 2026
@jonit-dev
jonit-dev added this pull request to the merge queue Oct 10, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 10, 2026
@jonit-dev
jonit-dev enabled auto-merge October 10, 2026 12:33
@jonit-dev
jonit-dev added this pull request to the merge queue Oct 10, 2026
Merged via the queue into develop with commit 2d21247 Oct 10, 2026
66 checks passed
@jonit-dev jonit-dev mentioned this pull request Oct 10, 2026
12 tasks done
jonit-dev added a commit that referenced this pull request Nov 9, 2026
jonit-dev added a commit that referenced this pull request Nov 10, 2026
…#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…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant