Repository navigation
Strata terrain PR chain: PRD-806 leftovers, then the MCP terrain path - #635
Conversation
…e 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.
…real-world elevation spike
…er, size and real survey
… card copies (PRD-806)
…cides the terrain, ground vegetation, mountains)
…orm, water and size, and the reply names each choice (PRD-854 phase 1)
…om MRT slot at 1x (PRD-806 E73)" This reverts commit 460f9e5.
… fix removed the alpine boulder shadows
Step 0 visual judge: before and after (PRD-806 E46, E56, E73)Headed WebGPU, NVIDIA RTX 2080 (adapter checked), same poses, 2 runs per arm. Rows top to bottom: BEFORE-1, BEFORE-2, AFTER-1, AFTER-2. Columns: meadow-close, overview, player, river. Verdict from a fresh judge: ❌ REGRESSION on alpine player and river: the boulders lose their cast shadows. The other 18 frames are NEUTRAL. Isolation: the same build with only E73 reverted brings the shadows back. So E73 is reverted on this branch ( |
…al assertion and stays red on four timing budgets; enterMs regressed to 444 ms
…tom MRT slot at 1x (PRD-806 E73)" This reverts commit d554712.
…ream three build, not E73; E73 is restored and waits for an owned-engine judge
Correction: E73 did not remove the alpine shadowsThe capture builds above ship upstream three: the preview's
|
…ibution, fails closed without the tile, and takes a ground override (PRD-854 phases 2 and 3)
…r-ring slices behind the curtain; fromMesh reads a plain attribute's array (PRD-585 enterMs)
…drawing level contour stripes; fall-line streaks on steep rock (PRD-856)
Mountain stripes removed (PRD-856), and E73 judged on the owned engineOwner report: level, artificial lines on the backdrop mountains. Cause:
E73 (PRD-806): the owned-engine judge returned NEUTRAL over 5 worlds × 4 poses × 2 runs per arm. E73 is ticked. |
…h sandbox; its CPU world path draws 97x the culled triangles
…7-compressed-upload
…t, so native_engine_no_blocking_waits passes The loader's queue moves into a small struct whose wait has the one TN_WORKER_ONLY shape CheckNoBlockingWaits.cmake accepts; begin only notifies. The check failed on the old gltfQueueWake.wait and blocked the native coverage refresh, which is regenerated here. Proof: the check passes on 237 engine sources and fails on the old file; the threaded web engine loads the KTX2 model playtest (bc7, zero console errors); pnpm budgets exit 0.
…s knolls - The cook aims the massif along the kit's spawn camera (+-25 deg) and retries up to six placements for a wooded foot (woodedFoot >= 0.6). - A floor spot with over 2% dead-flat cells (a hydro-flattened lake) is skipped. - The forest grass row has no height ceiling: its 66 m fade stopped trees at 57.6 m, which left seed 1's knoll bare. The pinned world tops out at 38.4 m. - far-forest.spec follows the near-band default. Judge: seed 1 IMPROVEMENT (overview, vista), seed 0 NEUTRAL; seeds 2 and 3 CONSISTENT, NO REGRESSION on the massif bearing cook.
…he owner AC-1: judge PASS on the shipped frames (overview and vista IMPROVEMENT against the commit before this PRD). AC-3: the default near band costs about 23 ms GPU on the RTX 2080 (overview 35.5 -> 57.5 ms, vista 40.4 -> 64.2 ms). Every lossless lever tried moved 0-2.6 ms, and the coarse impostor march that cut it darkened the canopy (judge REGRESSION, reverted). Only the owner's call is left, so the PRD moves to BLOCKED/requires-owner-decision.
…gates do not help
… full parse takes the coarse parse's whole file Review of #643, finding 7. The coarse parse already held the whole file; the full model's parse downloaded it again. The test was red ([22256, undefined]) before the fix.
…rap and transform the game set The AC-6 judge returned REGRESSION: Midway sets anisotropy on each loaded texture, the upgrade swapped in the whole file with the default 1, and the SBD's wing star smeared at grazing angles. The test was red (expected 1 to be 8) before the fix.
…hich its loader now decodes
The lane flipped webRuntimeDecoders to { ktx2: true, meshopt: true } and left this test on the old
refusal; the laptop unit run found it red.
…, streamed, with a NEUTRAL judge AC-5, AC-6, AC-7 and the end-to-end box ticked with their evidence; the PRD moves to done/.
… search by its own words 'an asset pops in after the reveal', 'a model texture looks blurry right after load' and TN_COLLIDER_COARSE found nothing in the first three; search.spec now pins 16 PR queries (3 were red).
# Conflicts: # docs/verification/native-coverage-2026-08-28.md # packages/create-threenative/agent-docs/references/capability-reference.md # packages/create-threenative/capabilities.json # packages/runtime-native/src/engine/renderer/render_database.cpp # packages/runtime-native/src/engine/renderer/renderer.cpp # packages/runtime-native/src/engine/renderer/renderer.h # packages/runtime-native/src/engine/wasm/web_host.cpp # packages/runtime-native/tests/native-engine/abi_test.cpp # packages/runtime-native/tests/native-engine/material_test.cpp # packages/runtime-native/tests/native-engine/render_database_test.cpp # packages/three-native/src/browser-renderer.ts # packages/three-native/src/render-target.ts
…lTF loader) into strata-chain Web host ABI 13 = the Strata lane's 12/13 over PRD-860's 12; tnw_web_load_gltf_finish also returns every scene. Frame fields: textureCompressionBc 87, virtualShadowPendingLevels 88. core-webgpu: drop develop's IndirectStorageBufferAttribute stub, the lane implements it (PRD-857). Native C++ tests pass (basisu fetched); related unit tests 505 files green.
…longer races the others - measureBiome validates the ground's samples before a bandSpan world re-measures its bands; a bandSpan world's bands follow its floor, not its range (the spec now says so). - strata-loading carries posesFor and SHADOW_REACH into the buildProps sandbox. - prop-lod and prop-streaming follow the 13 m variant-0 spruce and the procedural spruce's new look. - STRATA_BAKE_OUT sends a bake to another folder; cooked-world.spec bakes there, so src/world, which bake-golden and the game read, never changes under a parallel run. baked.json follows outputDir. - stamp.d.mts and recipe.d.mts type the two scripts the spec imports (TS7016 in pnpm typecheck).
…t three times The heaviest blended frame marches; the other two read the same 3D hit point from their own view. Overview 58 -> 38 ms GPU, vista 64 -> 42 ms GPU (RTX 2080, band near). Visual judge: PASS, NEUTRAL on overview and vista, sky and rock byte-identical, interleaved A/B from one tree.
…apability search points agents to it - A request with no biome word takes the biome its sea or shape suits (island -> coastal, canyon -> desert, rolling hills -> forest), else the forest, reported in decided.biome; it no longer throws. Unread words still come back in unused. An empty call (no request, kit or biome) still fails. - New words: alps, fjord(s), glacier, snow, tropical, shore, lagoon, canyon(s), arid, badlands, mesa, spruce, fir, trees, jungle, rainforest, grove, frozen, permafrost, glacial; volcanic reads before cliffs, so a volcanic island is a volcano in the sea. The starters' aliases carry the same words. - The tool text tells the agent to pass a real place as ground.location. - bakeBiome's situations route 'generate terrain with mountains and a river', 'open world map', 'procedural terrain' and 'a volcanic island level' to it (was Heightfield or nothing), and its summary names the threenative-terrain tool. Kit AGENTS: 'any terrain, from words'. - Regenerated: capabilities.json, terrain-mcp-tools.json, kit AGENTS/CLAUDE mirrors, scaffold pins.
#635 widened each starter's aliases in biome-words.ts and biome.data.json but left the pinned table in biome-values.spec.ts on the old lists, so the strata-terrain-preview test job is red on develop and on every PR.
…oading (streamed meshes and textures, meshopt, compressed upload) (#643) * docs(PRD-824): the mystral desktop host confines untrusted game JS Store audit (threenative-store docs/security/engine-capabilities-audit.md): playtest.respond/receive write, read and delete any path; file reads, module loads and screenshots take any path on desktop; curl has no scheme limit. Fixes keep dev, CLI, benchmark and playtest behavior unchanged. * docs(PRD-828): JS keeps only game logic, engine work moves into C++, measured on the stress lab against three.js * docs(PRD-832): untangle report and the urgent single-sources slice Eight read-only DeepSeek scouts mapped hidden links, needless links, surplus and 365 special cases across core, render, world, the three-native seam, playtest, the scaffolder, scripts and the C++ engine. PRD-832 batches the urgent items: kit inventory (recall skips three kits), template render drift guard, one launch-flag reader, one tone curve path, one core version and named tnw_web_frame fields. * docs(PRD-833..842): one PRD per root fix in the untangle report R1 template mechanism lives once (833), R2 cross-language contracts are generated (834), R3 one registry entry per assertion (835), R4 one committed capability manifest (836), R5 core owns each shared fact once (837), N1 scripts share one runtime layer (838), N2 the CI contract is a model (839), N3 platform difference is a value (840), N4 game.ts stops being the hub (841), N5 native tests check behaviour (842). * docs(PRD-843..852): Tailwind parity batch, one scene scored on web and native Exploration: Tailwind 4.3.3 gives 490 coverage units (402 utility units + 88 variants); the corpus proves 56 of 276 emitted properties; 14 of 14 kits have native-css findings; every filter is dropped because anyrender_vello_cpu builds without its filters feature. PRD-843 builds the scene, the meter, judge sheets and the ratchet; PRD-844..851 own every unit by family; PRD-852 closes at 490/490 with the denylist gone and a CI ratchet. * 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 * docs(PRD-859): the game thread wins the CPU against everything else on the machine * 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. * 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. * 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-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. * 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(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]> * 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]> * 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]> * 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]> * Revert "perf(web-engine): the glTF loader thread decodes on its own two workers (PRD-860)" This reverts commit 2bae24e. * docs(PRD-860): quiet-host paired runs, and why the loader pool was reverted Co-Authored-By: Claude Opus 5.5 <[email protected]> * 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) c5d2ede 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. * 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 70ea62e 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 * 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). * 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-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. * 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. * 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-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. * 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. * docs(PRD-867): tick AC-4 and PRD-861 AC-1 with the starter bake, record the cook and LOD fixes, refresh native coverage * 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. * 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 and 0,255,0, bc7 on nvidia turing); red on an engine without the transcoder (TN_NATIVE_GLTF_EXTENSION_UNSUPPORTED KHR_texture_basisu). - Capability manifest: createAssetLoader names the owned engine's KTX2 upload, its report and refusal lines, coarse-first levels and webRuntimeDecoders; modelPass names the codec ladder and its textures.overrides. - PRD-867 AC-5 (bytes -21.5%, judge NEUTRAL, sea check) and PRD-861 AC-5 ticked; both moved to done. * fix(PRD-860): the glTF loader thread parks in the reviewed worker wait, so native_engine_no_blocking_waits passes The loader's queue moves into a small struct whose wait has the one TN_WORKER_ONLY shape CheckNoBlockingWaits.cmake accepts; begin only notifies. The check failed on the old gltfQueueWake.wait and blocked the native coverage refresh, which is regenerated here. Proof: the check passes on 237 engine sources and fails on the old file; the threaded web engine loads the KTX2 model playtest (bc7, zero console errors); pnpm budgets exit 0. * fix(PRD-860): a host without Range sends each streamed KTX2 once; the full parse takes the coarse parse's whole file Review of #643, finding 7. The coarse parse already held the whole file; the full model's parse downloaded it again. The test was red ([22256, undefined]) before the fix. * fix(PRD-860): a streamed texture's whole file keeps the anisotropy, wrap and transform the game set The AC-6 judge returned REGRESSION: Midway sets anisotropy on each loaded texture, the upgrade swapped in the whole file with the default 1, and the SBD's wing star smeared at grazing angles. The test was red (expected 1 to be 8) before the fix. * test(PRD-861): the web native engine's cook keeps Meshopt and KTX2, which its loader now decodes The lane flipped webRuntimeDecoders to { ktx2: true, meshopt: true } and left this test on the old refusal; the laptop unit run found it red. * docs(PRD-860): done — Midway plays after 22.4 MB in 10.4 s at 40 Mbps, streamed, with a NEUTRAL judge AC-5, AC-6, AC-7 and the end-to-end box ticked with their evidence; the PRD moves to done/. * docs(PRD-860): every streaming abstraction of #643 answers capability search by its own words 'an asset pops in after the reveal', 'a model texture looks blurry right after load' and TN_COLLIDER_COARSE found nothing in the first three; search.spec now pins 16 PR queries (3 were red). * fix(PRD-860): the branch's 14 capability aliases become situations, so each alias keeps a corpus row CI test-unit red: 'maps every authored alias to a named predecessor corpus row' rejected aliases with no recall-corpus row. As situations they stay searchable; search.spec's 16 PR queries and the gold-set floor still pass (140/140). * test(PRD-867): source hygiene treats .ktx2 as a binary asset, like .webp and .glb CI test-unit red: the NUL-byte check read examples/wasm-engine-boot/src/red-etc1s.ktx2, the compressed-upload fixture, as a text source. * fix(capabilities): parse .ts sources as TS so fetchAsset gets its manifest entry The manifest builder parsed every source as TSX, so the generic arrow `const cached = <T>(...) =>` in assets.ts read as JSX and every export after it was dropped. fetchAsset had no entry and `pnpm budgets` failed on its missing doc tags. Also add the missing createTweakBox @example. * chore: format the manifest spec and record fetchAsset in the api surface * test(strata): pin the biome aliases #635 added to biome-words #635 widened each starter's aliases in biome-words.ts and biome.data.json but left the pinned table in biome-values.spec.ts on the old lists, so the strata-terrain-preview test job is red on develop and on every PR. * test(runtime-native): pass ABI 13's scenes pointer in the web decoder test ABI 13 added a scenes out-pointer to tnw_web_load_gltf and counts the default scene, the clips, then every scene. The PRD-861 decoder test still passed six arguments, so the host refused it with 'TN_WASM_GLTF: invalid arguments' and test-native was red on develop. * test(terrain): give the savanna cook-time check a noise margin AC-3 compared one savanna cook with one forest cook using a strict <=. On a shared CI runner the two vary by several percent either way (savanna 6026 ms against forest 5654 ms on #643; locally 4267 against 4908), so the test failed on noise twice. A 25% margin keeps the paired comparison and still fails a savanna that is really slower. * fix(ci): the suite temp-directory gate waits for late cleanup and names what stayed The test job on #643 passed every test and still failed: before 1, after 2. --suite-finish counted once, at once, and named nothing, while a test's async cleanup can still be removing its directory when the last package ends. It now gets the browser run's settle window (TN_ORPHAN_SETTLE_SECONDS, 30 s) and prints each surviving directory, so a real leak points at its owner. --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
…658) #635 (PRD-861, PRD-867) added native_engine_gltf_ktx2_* ctest cases that need third_party/basisu. The native-engine job, its Wasm-under-node step and the web engine action fetched only stb, cgltf, webp-source and meshopt, so the loader refused KTX2 by name and the three cases failed in 0.00 s (test-native red on every develop push since 39223c1). Local core-only build, CI's dependency list: 0 of 3 pass. With basisu: 3 of 3 pass.
…n where Wasm has no threads native_engine_scene_concurrent_ids (PRD-860, merged in #635) started four std::thread producers. CI's "Build and test the engine core as Wasm under node" step builds without -pthread, where a thread cannot start: node died with an unhandled "#<Exception>" and the step failed on every pull request run since the KTX2 failure stopped earlier steps from reaching it (runs 42940567205 and 42943256476). Without pthreads the producers now run one after another and the ids must still be unique; threaded builds are unchanged. Proof: the test fails on the old code and passes on the new under core-only Wasm (emcmake, node) and passes natively; scene tests 15/15 under Wasm.
strata-script-census.spec.ts requires every file under the example's scripts/ and playtests/ in its AGENTS.md. #635 (Strata terrain chain) added scripts/dem/massif.mjs, the three massif-*.bin/.json tiles, recipe.d.mts, stamp.d.mts and verify-variations.mjs without entries, so test-unit (2/4) failed (run 42958453742). Each file now has a one-line entry; CLAUDE.md is the regenerated mirror. Proof: strata-script-census red on origin/develop (10 files), green with the entries; check:docs exit 0; sync-agent-docs, primary-docs and ci-structure specs 217/217.









What is this PR about?
It runs the Strata terrain PR chain (
docs/PRDs/strata-terrain-hardening/PR-CHAIN.md) in one branch: the leftover PRD-806 epic-review fixes first, then the chain steps in order.Why does it matter?
After the EPIC merge (#483), four reviewed fixes still sat on unmerged branches, and the MCP terrain path (PRD-762, P0) gives an agent a game that does not start.
Done
depthWrite=falsedraws), E46 (scaled far cards on a quantized POSITION), E56 (crown normals bend once), E62 (landform cook plants the kit's tree). E49 and E52 were already on develop.Still open