mapspinner 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,265 @@
1
+ # mapspinner — agent working rules
2
+
3
+ ## SDK Validation Policy
4
+
5
+ SDK changes must be validated against the test suite and verified in both the dev demo (planet.html) and external consumer examples. No changes ship without passing tests.
6
+
7
+ ## Architecture (GPU one-fractal)
8
+ - Height pool: **R16F**, **1024 layers**, **4 mip levels**, 130-texel tiles. The ONLY runtime branch is a
9
+ FORMAT capability probe (`_useR16F` = half-float color+storage+linear) that falls back to the
10
+ numerically-equivalent R32F when the GPU lacks half-float — correctness, not a quality tier.
11
+ (`THC_CACHE_LAYERS` clamps to `MAX_ARRAY_TEXTURE_LAYERS`; 1024 was tuned from a live eviction
12
+ measurement — 512 evicted ~3155/s at the deck where ~594 leaves are visible, 1024 → 0/s. ~35MB vs
13
+ the old 138MB R32F/2048 = ~90% VRAM cut.)
14
+ - Shader precision: **global `mediump float`** with explicit **highp islands** on every planet-scale
15
+ quantity (`vRel`/`vWorld`/`vH`, `defRadius`/`defViewProj*`/`defCam*`, all noise lattices + their args,
16
+ `broadShapeM`/`broadShapeLowM`/`faceWarp`/`vtxDisplace` metre accumulators, coordinate-snapped noise
17
+ anchors `rockO`/`wOrigin`). Validated SAFE live: `d.assertElevLinear()` pass, `d.pxPerPoly()` ~238k
18
+ tris rasterized. fp16 leaking onto a planet-scale value collapses the geometry — keep the islands.
19
+ - Reduced octaves: `broadShapeM` 14→12, `vtxDisplace` 9→6, single-octave rock detail; tanh ceiling
20
+ `tanh(x/8000)` gives pointed peaks (CLI `shapeReport allGatesPass`). Distance-gated cheap FS far path.
21
+ Tightened LOD (`planet-orchestrator` splitFactor + near-radius). Dead atlas apparatus deleted.
22
+ - The two new FS/VS uniforms `uHeightPoolTexSize` (=tile res) and `uFloatLinearOK` (=half-float-linear
23
+ probe) MUST be set on the main render program (`gl-render.js`) or the height-pool UV math goes to
24
+ `vec2(0)` → NaN displacement.
25
+
26
+ WITNESS HEADLESS WITHOUT A FRAMED SCREENSHOT: don't judge "flat render" from a close-zoom shot whose
27
+ camera you can't aim. Use the DATA diagnostics — `__diag.pxPerPoly()` (on-screen quads/tris rasterized),
28
+ `__diag.assertElevLinear()`, `__diag.landWitness()`, and `window.__terrainConfig`/`__tcEvictRate`. Start
29
+ `node server.js` (port 8080) yourself if it's down, then drive a fresh headless session to it.
30
+
31
+ ---
32
+
33
+ ## The terrain pipeline in one page (read this before touching terrain)
34
+
35
+ Earth-scale terrain SDK, WebGL2, served at `http://localhost:8080/` (entry `planet.html`, `server.js`).
36
+ GPU one-fractal: no Proland tile producer, no cascade. A finer
37
+ LOD is a denser sample of the SAME field. The procedural broadShapeM fractal is the LIVE render
38
+ path (hasAtlas==0, the default). The baked atlas is opt-in (planet-orchestrator.js opts.atlas===true;
39
+ live `window.__toggleAtlas(on)` / `__forceAtlas`); its bake is CLI-validated faithful (atlas-bake.mjs
40
+ `interiorExact` gate) and `broadShapeMD` central-differences `atlasHeight` so atlas-on land shades with
41
+ relief. Do NOT delete the procedural path while the atlas is opt-in. Recall
42
+ "tv8-atlas-flat-is-flat-normals-root-2026-06-07" + "tv8-reliable-visual-witness-method-2026-06-03".
43
+
44
+ THE SPOOL `browser` VERB IS A FULL BROWSER, NOT HEADLESS (user correction 2026-06-11): it drives a
45
+ locally-profiled Chromium with the REAL GPU + real ANGLE backend (witnessed: ANGLE AMD D3D11 -- the
46
+ user's exact stack). The 9222 headless chrome that scripts/verify.mjs targets runs a DIFFERENT
47
+ backend, so a look defect can pass every verify.mjs witness and still be broken on the user's
48
+ screen; any look/material/normal judgment MUST be confirmed on the spool browser verb (or the
49
+ user's warm tab via /cmd). Never write a note calling the spool browser tool headless.
50
+
51
+ LIVE WITNESS VIA SERVER: the cold shader compile (~110s on source change, ~150s on d3d11/FXC)
52
+ makes fresh sessions slow, so witness the warm tab through the server when possible. `server.js`
53
+ hosts `POST /diag` (per-frame
54
+ render state, ringed) read via `GET /diag/tail`, and a `POST /cmd {js}` command channel the page polls
55
+ and runs live (result to `/diag`, kind:`cmd-result`). Drive `window.__toggleAtlas`, `window.__diag`
56
+ probes, and `window.__diag.reloadShaders()` hot-reload through `/cmd` — no page reload. Recall
57
+ "tv8-diag-sink-rehosted-2026-06-07".
58
+
59
+ Data flow, each stage names its one file:
60
+ 1. QUADTREE picks which cube-sphere patches to draw per camera altitude — `src/quadtree.js`
61
+ (cube-sphere quadtree in JS, ported from the deleted Proland C++), driven per frame by
62
+ `src/planet-orchestrator.js`.
63
+ 2. MESH per patch is a GRID+2 grid (`src/gl-render.js`) whose outer ring is a SKIRT (terrain.glsl
64
+ drops `vertex.z>0.5` verts radially below the surface) hiding LOD T-junction cracks. The outer
65
+ ring is LOAD-BEARING — do not "just not draw it".
66
+ 3. HEIGHT assembled per-vertex in the VS (`src/shaders/terrain.glsl`): `h = cbias + bShape +
67
+ vDisp(land) + lake/river/canyon carves`. `bShape = broadShapeM(worldDir,reliefMul,ridgeMul)` =
68
+ THE shape (one continuous 14-oct world-dir fBm, LOD-invariant by construction). `cbias` = anchor
69
+ continental swell (`src/anchor-field.js`); `vDisp` = LOD-invariant micro-relief; carves via
70
+ `inciseRidgeField`. Collision = a GPU `_PROBE_` variant of the SAME shader (1px readback,
71
+ `gl-render.sampleGroundM`), no CPU mirror. — full design: recall "TV8 GPU-TERRAIN ARCHITECTURE
72
+ DECISION" in rs-learn.
73
+ 4. DEFORM: direct per-vertex sphere projection — `vWorld = dir0 * (R + h)` (replaced the Proland
74
+ corner-blend deform; round at any tessellation, no flat patches at high GRID).
75
+ 5. FS shades from height+slope+climate (`terrainAlbedoClimate`) + per-vertex seamless normal +
76
+ ocean/lake/river. No FS detail TEXTURE (a tiled image would moire + UV-scroll); closeup MACRO
77
+ relief comes from the mesh subdividing into a denser sample of the one VS fractal. The FS DOES
78
+ carry a procedural cliff DETAIL-NORMAL (biplanar + 2-scale RNM rock bump, world-anchored so it
79
+ never reseeds on camera move) on steep faces only, plus object-space slope/gorge AO and inline
80
+ analytic single-scatter AERIAL PERSPECTIVE (distance-gated space->ground depth cue). Unified
81
+ FS shading model: recall "tv8-fs-shading-bundle-shipped-2026-06-05" + "tv8-shading-unification-decision-2026-06-05".
82
+ CLOSE-UP ROCK ENGAGEMENT (2026-06-05b, workflow w5gywvug1): all rock/cliff/strata/detail-normal
83
+ gates now key off `rockSlope` = clamp(1-dot(ngGeo,uz), slope, 1) where ngGeo = the RAW geometric
84
+ normal cross(dFdx,dFdy) hoisted BEFORE the lit-normal compression (the macro `slope` caps ~0.6 on
85
+ vertical faces -> rock gates never fired at the deck = lit clay). The biplanar bump is promoted to a
86
+ material+AO signal (microSlope/microCurv from existing taps), all pxWorld/nearFade-faded so orbit is
87
+ identical. ZERO VS octaves added (VS is 96%-bound; the VS derivative-fBm was rejected). recall
88
+ "tv8-closeup-rock-engagement-2026-06-05". The deck VISUAL is the user's-eye gate (driven browser
89
+ can't render the deck: low-alt tile-fetch returns black).
90
+
91
+ DETAIL-ON-APPROACH: relief must grow (or hold), never drop, as you descend — guaranteed by
92
+ construction (finer LOD = denser sample of the one field). The lit-luma metric is near-blind to
93
+ relief at nadir; judge closeup BY EYE (screenshot) at an oblique pose.
94
+
95
+ RUNTIME FPS is VERTEX-SHADER-bound (96% VS+raster at the deck, FS is a dead lever): measure with the
96
+ headed-browser `window.__diag.gpuTimer` (EXT_disjoint_timer_query_webgl2 + a `uFsCheap` FS short-circuit).
97
+ Levers shipped: `broadShapeFD` reduced-octave FD, GRID 24->16, and the low-alt `altSplitMul` PEAK 2.0->1.4
98
+ (removes a sub-pixel over-tessellation tail; 1.68x at 6km). BEFORE trusting gpuTimer, read `__pageErr` and
99
+ confirm `dbg.quads()>0` — a frozen `__altM`+0-quads+overlay-up is a CRASHED render loop, not a slow one.
100
+ Full numbers, the live `__splitFactor` sweep method, the crash class, and gotchas in recall
101
+ "tv8-fps-splitfactor-peak-cut-2026-06-04b". Workflow `.claude/workflows/fps-perf.js`.
102
+
103
+ ## Headless shader verification (USE scripts/verify.mjs)
104
+
105
+ The render/lint/watch CLI toolchain and src/lab (terrain-lab mirrors, glslang lint) were DELETED
106
+ 2026-06-12 (user: "get rid of the lab and glslang"). The single headless verification surface is
107
+ `node scripts/verify.mjs [probeName|expr]` (raw CDP against the live planet.html -- see the
108
+ PERMANENT VERIFY POLICY at the top): shader compile errors surface as orch 'error'/ready-timeout
109
+ on the real two-stage compile, and the in-page `__diag` probes assert behavior. The lab's LOD
110
+ knot solver pattern (node bisection over the real quadtree) lives in recall
111
+ "tv8-separate-water-surface-2026-06-11" if a re-solve is ever needed -- rebuild it inline, do not
112
+ resurrect the lab mirror (byte-sync drift was a recurring failure class). NO node-WebGL2 binding
113
+ exists on win32 (headless-gl/@kmamal are WebGL1).
114
+
115
+ ## The efficient terrain-debug loop (USE THIS, never restart-and-eyeball)
116
+
117
+ Debug LIVE in the browser through `window.__diag` / `window.__dbg`, NOT by restarting the server
118
+ and guessing from screenshots. One `page.evaluate` dispatch reads the actual runtime state.
119
+
120
+ 1. Serve: `PORT=8081 node server.js`. Drive the page with the spool `browser` verb (Playwriter
121
+ script in `.gm/exec-spool/in/browser/N.txt`; raw JS, `page` global). Reuse ONE persistent
122
+ session — do NOT re-navigate per probe (~10 navs crashes chrome).
123
+ 2. Gate: a shader compile fail leaves `window.__diag` undefined / `window.__pageErr` set and the
124
+ page LOOKS like a slow load. Check `hasDiag` + `pageErr` in one dispatch before measuring.
125
+ 3. Park: `await __diag.parkOblique(altKm, aimFrac)` / `parkAt(rung)` — deterministic, reproducible
126
+ viewpoints (eyeballed positions made comparisons noisy).
127
+ 4. Measure: numeric probes return a metric + pass/coverage: `_read()` (pixel buffer), relief/
128
+ albedo SD, `limbScan`, `seamProbe`, `speckleProbe`, the `__lastGLQuads` leaf set + quad counts,
129
+ `glError`. Keep probes light — many frames + several parkOblique in one dispatch can exceed the
130
+ ~14s browser timeout; split into multiple dispatches.
131
+ **LIVE-TAB CAMERA CONTENTION (load-bearing):** the witness browser is CDP-attached to the USER's
132
+ live tab whose rAF loop overwrites `__planet.cam.pos`, so scripted poses are reverted — `__lastGLQuads`/
133
+ center-pixel/`page.screenshot()` read the user's contested camera, NOT yours. Pose-independent (reliable):
134
+ `reloadShaders`, `pageErr`-null-after-load (compile witness), `seamProbe`, `sampleGroundM`/`hpf.sampleDir`
135
+ scans, split-math trace. **LOD/SHAPE validation = the HEADLESS LAB** (`cd src/lab && node terrain-lab.mjs`;
136
+ mirror is STALE-prone, sync on shader massif/peak/ceiling edits). If the `browser` verb returns "could
137
+ not allocate free port" mid-session (resource accumulation), the fix is a FULL plugkit node-tree
138
+ kill+reboot (kill every `*plugkit*`/`*supervisor*`/`*relay*` node proc — spare server.js + the user's
139
+ Chrome/other projects — then `bun x gm-plugkit@latest spool &`); killing only the relay/duplicates does
140
+ NOT recover it. — recall "TV8 witness contention lod traces correct" + "TV8 wasm renamed to src and
141
+ spooler restart recovery 2026-06-02".
142
+ 5. Tune live, NO rebuild: `window.__gen` shader globals; `window.__displayMode` debug views —
143
+ recall "TV8 live-debug display modes" in rs-learn for the full mode list.
144
+ 6. Re-measure. Repeat. The whole loop is browser-side; the server stays up.
145
+
146
+ GEN-CONTROLS OVERRIDE + JS CACHE TRAP (cost many turns of invisible edits): the LIVE biome material
147
+ values (bcRock, slopeRock, bandEdgesHi/Lo, snowEdges...) come from `window.__gen.state.biome` in
148
+ `src/terrain-gen-controls.js` (~L35-41), which OVERRIDES the gl-render.js defaults — edit the
149
+ gen-controls state, not the gl-render default. AND the browser HTTP-caches JS modules, so reloads
150
+ serve stale code; force fresh via a CDP `Network.setCacheDisabled` before `page.goto`, and VERIFY any
151
+ change took with `gl.getUniform(orch.render.prog, name)` — never trust an edit is live. Recall "TV8
152
+ gen-controls overrides gl-render defaults CRITICAL 2026-06-03".
153
+
154
+ CLIENT-EDIT WITNESS: any edit to `terrain.glsl` / `*.js` / `planet.html` must be witnessed in the
155
+ SAME turn via a `browser` dispatch asserting the invariant (compile clean + glError 0 + the metric
156
+ the edit targets). Port-exhausted browser → contention-free headless fallbacks (lab for LOD/shape;
157
+ NODE MODULE PROFILE for JS hot loops, how bakeHpf 4027ms→1137ms was found) — recall "TV8 node module
158
+ profile bakehpf bottleneck 2026-06-02" + "TV8 patches view loadtime distribution 2026-06-02c".
159
+
160
+ ## RECURRING CLASS: "rocks everywhere + normals gone + height-keyed shading" -- THE SOLUTION (user order 2026-06-12)
161
+
162
+ PRIMARY ROOT = ANGLE D3D11/FXC MIS-TRANSLATION OF THE UNROLLED FRACTAL (default Chrome on Windows
163
+ = d3d11 backend = FXC). Proven by backend split on the SAME AMD GPU: vulkan renders correctly,
164
+ d3d11 renders the triad (blotchy rocks on flat ground + relief normals gone + shading reads as
165
+ height); the GPU/driver is innocent. FXC fully unrolls constant-bound loops and reorders math
166
+ across the unrolled 12-octave broadShapeM body. THE FIX (THE SOLUTION, do not regress it):
167
+ broadShapeM's octave loop is RUNTIME-BOUNDED -- `uniform int uOctMax` (set 12 by
168
+ gl-render setComposeHeightUniforms; shader guards <=0 -> 12) so FXC CANNOT unroll. Side proof the
169
+ de-unroll engages: d3d11 cold shaderCompileMs 152379 -> 63259. If this triad ever returns on
170
+ default Chrome/AMD: check the loop is still runtime-bounded FIRST. Never reintroduce a constant
171
+ 12 bound; if a new VS fractal loop is added, give it the same runtime bound.
172
+
173
+ SECONDARY (real but transient/exotic) modes with the same look, check in this order via /diag tail:
174
+ (a) `swgl`/`ctxLostAt` non-null = software-WebGL after a GPU-process crash -> restart the BROWSER;
175
+ (b) `bakePending` non-zero = unbaked/zero-HPF window (keep whole-planet bake <1s, ee7d72e).
176
+ Rock gates themselves are SLOPE-ONLY by design (terrain.glsl macro ~1093-1100, splat ~1574;
177
+ height-band rock REMOVED 2026-06-03) -- rock on flat ground means broken INPUTS, never the gates.
178
+
179
+ ## FXC (ANGLE d3d11 / default Chrome on Windows) -- THE SOLUTION RECORD (user order, 2026-06-12)
180
+
181
+ The recurring "rocks everywhere + normals gone + dark daylit ground, AMD/default-Chrome only" class
182
+ is FXC SHADER MIS-TRANSLATION, never the GPU/driver (vulkan on the same AMD silicon renders
183
+ correctly). Two proven mechanisms + their fixes (commits f062365 + d56a202):
184
+ 1. CONSTANT-BOUND LOOPS get fully unrolled + cross-iteration reordered -> `uniform int uOctMax`
185
+ runtime-bounds the broadShapeM octave loop (side effect: cold compile 152s -> 63s).
186
+ 2. PER-CALLSITE INLINING: composeHeight inlined at separate call sites gets optimized DIFFERENTLY
187
+ per copy -- the lit-normal FD taps then disagree by tens of metres on FLAT ground (fake slope ->
188
+ rock material + slope-AO dark + dead normals, in patches). Fix: ALL FD taps evaluate through ONE
189
+ runtime-bounded loop (fdIters keyed on uNrmStepM) = a single instance, errors cancel exactly.
190
+ RULES: never reintroduce a constant bound on a VS fractal loop; never difference composeHeight (or
191
+ any big field fn) across separate call sites -- route every tap through the single-instance loop.
192
+ DIAGNOSIS ORDER for the symptom triad: (1) loop-bound/call-site regression, (2) HUD/diag `SWGL!`/
193
+ `CTXLOST!` (software fallback after a GPU crash -- restart the BROWSER), (3) `bake:` pending
194
+ (zero-HPF window). COMPILE COST CORRECTED (2026-06-12, user caught it): the earlier ">40min cold
195
+ compile" reading was machine-HIBERNATION wall-clock contamination; a clean-profile awake measure
196
+ (shader disk cache disabled, real AMD d3d11) shows shaderCompileMs 31455 -- the single-instance-loop
197
+ shape compiles ~31s cold, 5x FASTER than the original 152s unrolled shape, cached ~250ms after.
198
+ No cheapening needed. Timing rule: never trust wall-clock perf numbers across a sleep/hibernate;
199
+ re-measure awake with the in-page shaderCompileMs.
200
+ Memory keys: tv8-fxc-percallsite-divergence-TRUE-ROOT-2026-06-12, tv8-fxc-unroll-amd-root-SOLUTION-2026-06-12.
201
+
202
+ ## DEBUGGING PLAYBOOK (2026-06-12, distilled from the day the FXC hunt cost)
203
+
204
+ - BACKEND SPLIT FIRST for any "looks wrong on X's machine" report: `node scripts/backend-ab.mjs`
205
+ launches d3d11 + vulkan side-by-side at the same pose and prints a divergence verdict.
206
+ - USER-FLOWN EVIDENCE: open a CDP-driven window (`.gm/drive.mjs` pattern), let the USER fly it to
207
+ the defect, then dissect THAT frame -- their eyes pick the evidence, probes name the term.
208
+ - ONE-CALL CARRIER FINDER: `__diag.bisect()` A/Bs splat/texNormals/warp/litNormal/slopeRock at the
209
+ current pose and returns per-toggle pixel-diff fractions; `__diag.groundTruth()` gives probe
210
+ h+slope under the camera. WIREFRAME checkbox = the geometry-truth tiebreaker (flat mesh under a
211
+ "rocky" render = the shading path is the liar).
212
+ - CLOSE EVERY OTHER PLANET WINDOW before judging look/fps on the iGPU: leaked pages render at full
213
+ rAF, crush the GPU, steal /cmd commands, and pollute every measurement (witnessed repeatedly).
214
+ - HUD tail now shows `vendor/backend`, `SWGL!`, `CTXLOST!`, `bake:N`, and `DBGVIEW <state>` when a
215
+ debug displayMode is requested but its program is still compiling or failed -- a debug view can
216
+ no longer silently fall back to the lit render.
217
+ - BACKGROUNDED TABS throttle rAF to ~1/min: the compile poll now falls back to setTimeout when
218
+ `document.hidden`, but remember the mechanism -- a hidden tab that seems "stuck at init" may just
219
+ be starved, foreground it.
220
+
221
+ ## Hard-won invariants
222
+
223
+ ONE FRACTAL PER CARVE (user hard rule): every carve = ONE world-dir field fn called in BOTH the VS
224
+ geometry carve AND any FS mask at `normalize(worldPos)`; never a divergent FS inline loop — recall
225
+ "TV8 one fractal per carve invariant 2026-06-02".
226
+
227
+ FP32 PRECISION + GRAZING-AA + per-pixel-moire + SHARED-PREAMBLE (snoise3/carves/broadShapeM must be
228
+ outside `#ifdef _VERTEX_` so the FS + `_PROBE_` program link) — recall "TV8 glsl fp32 grazing moire
229
+ invariants 2026-06-02".
230
+
231
+ LOD must INCREASE detail monotonically on descent + the LOD CENTER tracks the camera (worldToFaceLocal
232
+ applies the atan inverse of faceWarp; witness with `window.__diag.lodCenterProbe()` -> tracksCamera
233
+ <0.5deg). Far-LOD falloff coarsens only beyond the horizon; cull ON by default (screen-AABB). Debug
234
+ displayModes 0-12 (6 elevation, 10 canyon, 11 cliffs, 12 patches/LOD) — recall "TV8 lod monotone
235
+ descent cull debugviews 2026-06-02".
236
+
237
+ CLIFFS/CANYONS/STRATA/CLIFF-LIGHTING (terrain.glsl mesa cliffTerraceM coherent-rim redesign + FS
238
+ strata + RNB normal + slope-AO; levers `__canyonDepth __cliffAmt __strataM __cliffStrata __aoAmt
239
+ __macroNrm __biomeBandBias`; realism WIP, user visual is the gate) — recall "TV8 cliffs canyons
240
+ strata levers 2026-06-02".
241
+
242
+ LIGHTING / "FLAT GREEN" ROOT (recurring complaint): the "terrain reads flat green / normals not
243
+ affecting lit" was NOT the normal pipeline (A/B fsNormal proven, normalDiff 36-38 on gentle AND steep
244
+ land). It was the DEFAULT SUN sitting high (near-overhead) -> minimal slope self-shading on gentle
245
+ rolling terrain (correct for noon, but reads flat). FIX (planet.html cam.sunLatBase 0.6->0.35, commit
246
+ 333f3ba): a lower default sun = longer shadows -> even gentle hills self-shade -> default view reads
247
+ 3D. When judging relief, use an OBLIQUE sun + frame ACTUAL LAND (a sea-level dir at 45deg oblique from
248
+ +5km frames mostly ocean — verify the screenshot is land, not water). fsNormal default stays 0 (gated
249
+ to steep faces, no-op on gentle land; gentle relief is carried by pvNormal, default on).
250
+
251
+ VERTICAL ROCKFACE MATERIAL (user hard rule; distinct dark cool-grey cliffRock gated on verticality,
252
+ slope gates calibrated to the 0.3-0.6 SMOOTHED-normal band not ~1.0, witness by image, warm-cast caveat) —
253
+ recall "tv8-vertical-rockface-material-calibration".
254
+
255
+ ROCK DETAIL-NORMAL = JUMP-FREE (biplanar pow-softmax weight, never a dominant-plane pick or fwidth fade
256
+ = both JUMP on move; keep FS snoise3 taps down; cold-compile fix is CACHE PERSISTENCE not source-trim,
257
+ 181s one-time/machine) — recall "tv8-rock-detail-normal-triplanar-jumpfree" + "tv8-closeup-rock-engagement-2026-06-05".
258
+ Workflow `/startup-perf`.
259
+
260
+ ## HPF adaptive quadtree (anchor-field.js)
261
+
262
+ Per-band adaptive quadtrees (additive detail-hat overlay) + the BAKED-vs-FULL-BAND elevAmp gotcha —
263
+ recall "TV8 hpf adaptive quadtree anchor field 2026-06-02".
264
+
265
+ @.gm/next-step.md
package/CHANGELOG.md ADDED
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ ## 2026-06-08
4
+
5
+ - **Single comprehensive mobile-first terrain version** (`ab68667`) — rearchitected to run performantly on phones with drastically fewer GPU resources, one version only (no device tiers, no desktop/procedural/clipmap fallbacks). Driven by two subagent workflows (an 8-dimension analysis fan-out, then a 13-item per-file fixing-run fan-out), each step lint-witnessed.
6
+ - `gl-render.js`: height pool **R32F→R16F**, **2048→1024 layers** (tuned from a live eviction measurement: 512 evicted ~3155/s at the deck → 1024 gives 0/s), **8→4 mip levels**, with a format-only capability probe (`_useR16F`) that falls back to R32F when half-float is unavailable. **~90% VRAM cut** (138MB → ~35MB). Set the previously-missing `uHeightPoolTexSize` + `uFloatLinearOK` uniforms (were `vec2(0)` → NaN displacement). Added a `__tcEvictRate` counter + `window.__terrainConfig` diagnostics readout.
7
+ - `terrain.glsl` + `atmosphere.glsl`: global **`mediump float`** with explicit **highp islands** on all planet-scale quantities (validated safe — `assertElevLinear` pass, ~238k tris rasterized). Octave cuts (`broadShapeM` 14→12, `vtxDisplace` 9→6, single-octave rock), `aw*aw*aw*aw` for `pow(aw,4)`, distance-gated cheap FS far path, tanh ceiling `x/8000` (pointed peaks, no flat clipped tops). Overflow leaves shade with the geometric macro normal (no pale/colored flash) + macro-slope rock gate.
8
+ - `planet-orchestrator.js`: tightened LOD (splitFactor + near-radius) so peak visible leaves drop under the smaller cap; HPF continental field packed to RG16F+RG8.
9
+ - `quadtree.js`, `terrain-lab.mjs`: matched LOD + lab-mirror updates. Dead atlas apparatus deleted across files. 6-stage GLSL lint + `node --check` clean; CLI `shapeReport allGatesPass`; live glError 0 (R16F active, `bakeErr null`).
10
+
11
+ ## 2026-06-07
12
+
13
+ - `terrain.glsl` + `gl-render.js` (`7ee7cb0`): unified lit-normal slope gain (`uNrmGain`) so the normal is the true gradient of the rendered height; camera moveStep + collision both use the GPU-exact `sampleGroundM` height — fixes the camera hitting zero speed before the surface.
14
+ - `planet-orchestrator.js` + `terrain-lab.mjs` (`69c2a86`): LOD `distFactor` `sf*3.6 -> sf*8.0` — the detail seen at 5.5km now displays at ~12km (each LOD pop ~2.2x farther in altitude).
15
+ - `terrain.glsl` (`06a9bd9`): rockface/canyon detail texture ~10x larger (FS noise freq `1800->180`, `1200->120`).
16
+ - `terrain.glsl` (`db4dae0`): four normal-slope defects — full fine band restored, cbias continental-swell gradient added to the normal, peak-lift smoothstep chain rule, normalized pole tangent frame.
17
+ - `terrain.glsl` + `lint-shader.mjs` (`7cb6eac`): guard the VS-only fractal + debug-only functions out of the render FS to shrink the ANGLE cold compile; lint now covers the PROBE and DEBUG-FS programs.
18
+ - `terrain.glsl` + `planet-orchestrator.js` + `atlas-bake.mjs` (`b73465e`, `89f381e`): the baked atlas shades with relief — `broadShapeMD` central-differences `atlasHeight` so atlas-on land is no longer flat-shaded; `interiorExact` bake-validation gate added. Atlas default-off, live A/B via `window.__toggleAtlas`/`__forceAtlas`.
19
+ - `server.js` + `planet.html` (`61135c6`, `4b116ad`): re-host the `/diag` sink and add a `/cmd` command channel — a headless agent reads the warm tab's live render state and drives it (toggle atlas, hot-reload shaders, probe GPU state) with no page reload, routing around the cold-compile browser-tool block.
20
+ - `atlas.js` (`a2b2ce0`): `PLANET_R` default `6371000 -> 6360000` to match the system radius; closes the coordinate-system audit sweep (face-UV convention, vH<->normal term parity, tangent-frame guards, fp32 coord-scale, quadtree R, camera-height AGL — all pass).
21
+ - `terrain.glsl` (`e3e4572`): bounded quantization — break the `vtxDisplace` octave loop below the Nyquist fade floor; ~2-4 fewer noise taps/vertex at deep LOD, geometry pixel-identical.
22
+ - `quadtree.js` (`6bbf6e1`): flatten the near-far detail gradient as altitude rises above fps height (gradient 8 at the deck -> 2 at 300km).
23
+ - `planet-orchestrator.js` + `terrain.glsl` + `gl-render.js` (`7b7dcd6`): matched HPF bake+shader seam-inset (`window.__hpfInset`) collapses the 985m cube-face seam to 0m; gated off by default.
24
+
25
+ ## 2026-05-23
26
+
27
+ - `terrain-phase2.js` (commit `d87c51b`): Add `allocateTileSlot`, `computePipelines`, `computeBindGroups` to `ProlandProducer` — stopped TypeError crash every render frame when `terrain-phase3-integration.js` called these missing methods.
28
+ - `shader-loader.js` (commit `1e61b7b` session): Fix `#if` numeric literal handling — `#if 1` was treated as a flag lookup (always false), stripping always-true blocks and causing GPU validation errors and black canvas.
29
+ - `terrain-phase1.js`: Change `normTexArray` format from `rg32float` to `rgba8unorm` to match `normal_producer.wgsl` storageTexture output.
30
+ - `terrain-phase2.js`: Remove unused bind group entries for upsample (b5), normal producer (b0), ortho producer (b0,b1,b2,b6) — WebGPU `layout:'auto'` strips unreachable bindings, causing validation errors when they were supplied.
31
+ - `.gitignore`: Add `--session-id` stale runtime artifact.
package/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ @AGENTS.md
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # mapspinner — WebGL2 Earth-scale Terrain SDK
2
+
3
+ A performant, production-ready WebGL2 rendering SDK for interactive Earth-scale globe applications. Designed as a composable rendering layer that integrates seamlessly into projects like spoint.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npm install mapspinner
9
+ ```
10
+
11
+ ```javascript
12
+ import { createPlanet } from 'mapspinner';
13
+
14
+ // In your WebGL2 application
15
+ const planet = await createPlanet(gl, {
16
+ radius: 6360000, // Earth radius in meters
17
+ gridMeshSize: 16 // Mesh subdivision level
18
+ });
19
+
20
+ // Per-frame render
21
+ planet.frame();
22
+ ```
23
+
24
+ ## Integration Example
25
+
26
+ For integration into external projects (e.g., spoint), see `examples/basic-sdk-usage.js` for a complete setup including peer dependencies, camera control, and WebGL context initialization.
27
+
28
+ ## Development
29
+
30
+ ```bash
31
+ PORT=8080 npm run dev
32
+ # open http://localhost:8080/ in any WebGL2 browser
33
+ ```
34
+
35
+ ## Controls
36
+
37
+ - **WASD** / mouse drag — yaw + pitch
38
+ - **Q / E** / mouse wheel — zoom in / out
39
+ - **R** — reset camera
40
+
41
+ The camera flies continuously from orbit to first-person on the surface; the terrain LOD
42
+ refines as you descend.
43
+
44
+ ## Architecture
45
+
46
+ The terrain is a single continuous world-direction fractal evaluated per-vertex on the GPU — a lean, portable design with no procedural tile generation or offline preprocessing.
47
+
48
+ - **`src/shaders/terrain.glsl`** — the core fractal. Height per vertex =
49
+ `cbias` (continental elevation bias) + `broadShapeM` (silhouette + relief) + `vtxDisplace` (micro-relief) + carves. The fragment stage shades via biome ramp + seamless normal + ocean/lake/river.
50
+ - **`src/quadtree.js`** — cube-sphere quadtree LOD in JS. Selects visible patches based on camera altitude.
51
+ - **`src/planet-orchestrator.js`** — per-frame quadtree drive, mesh generation, and render dispatch.
52
+ - **`src/anchor-field.js`** — world-direction climate/elevation modulation (biome, temperature).
53
+ - **`src/gl-render.js`** — WebGL2 program compile, mesh generation, per-quad draw.
54
+ - **`src/index.js`** — SDK entry point for external consumers.
55
+
56
+ ## Layout
57
+
58
+ ```
59
+ planet.html dev demo entry + __diag witness harness
60
+ server.js dev static server (COOP/COEP, no-cache)
61
+ src/shaders/terrain.glsl terrain fractal (VS + FS)
62
+ src/shaders/atmosphere.glsl analytic sky/limb shading
63
+ src/quadtree.js cube-sphere quadtree LOD
64
+ src/planet-orchestrator.js per-frame drive + render dispatch
65
+ src/anchor-field.js elevation anchor field
66
+ src/gl-render.js WebGL2 render layer
67
+ src/index.js SDK entry point
68
+ examples/ integration examples
69
+ tests/ SDK validation tests
70
+ ```
71
+
72
+ ## Testing
73
+
74
+ ```bash
75
+ npm test
76
+ ```
77
+
78
+ Tests validate SDK geometry output, shader compilation, and rendering invariants.
79
+
80
+ ## License
81
+
82
+ MIT
@@ -0,0 +1,114 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>mapspinner SDK Integration Example</title>
6
+ <style>
7
+ body { margin: 0; overflow: hidden; font-family: sans-serif; }
8
+ canvas { display: block; }
9
+ #hud {
10
+ position: absolute;
11
+ top: 10px;
12
+ left: 10px;
13
+ background: rgba(0,0,0,0.7);
14
+ color: #0f0;
15
+ padding: 10px;
16
+ font-family: monospace;
17
+ font-size: 12px;
18
+ z-index: 10;
19
+ white-space: pre;
20
+ }
21
+ </style>
22
+ </head>
23
+ <body>
24
+ <canvas id="canvas"></canvas>
25
+ <div id="hud">
26
+ mapspinner SDK example
27
+ Controls: WASD fly, QE zoom, R reset, mouse drag rotate
28
+ </div>
29
+
30
+ <script type="module">
31
+ // mapspinner SDK integration example for external consumers (e.g., spoint)
32
+
33
+ import { createPlanet } from '../src/index.js';
34
+
35
+ const canvas = document.getElementById('canvas');
36
+ const hud = document.getElementById('hud');
37
+
38
+ canvas.width = window.innerWidth;
39
+ canvas.height = window.innerHeight;
40
+
41
+ const gl = canvas.getContext('webgl2', { antialias: false, preserveDrawingBuffer: false });
42
+ if (!gl) {
43
+ alert('WebGL2 not supported');
44
+ throw new Error('No WebGL2 context');
45
+ }
46
+
47
+ // Enable required extensions
48
+ gl.getExtension('OES_texture_float');
49
+ gl.getExtension('OES_texture_float_linear');
50
+ gl.getExtension('EXT_color_buffer_float');
51
+ gl.getExtension('KHR_parallel_shader_compile');
52
+
53
+ let planet = null;
54
+ let frameCount = 0;
55
+ let lastTime = Date.now();
56
+
57
+ // Initialize mapspinner SDK
58
+ async function init() {
59
+ try {
60
+ planet = await createPlanet(gl, {
61
+ radius: 6360000, // Earth radius (meters)
62
+ gridMeshSize: 16 // Mesh subdivision
63
+ });
64
+ console.log('mapspinner SDK initialized');
65
+ render();
66
+ } catch (e) {
67
+ hud.textContent = `Initialization error: ${e.message}`;
68
+ console.error(e);
69
+ }
70
+ }
71
+
72
+ function render() {
73
+ requestAnimationFrame(render);
74
+
75
+ if (!planet) return;
76
+
77
+ // Update camera and render one frame
78
+ try {
79
+ const frameResult = planet.frame();
80
+ frameCount++;
81
+
82
+ // Update HUD
83
+ const now = Date.now();
84
+ const elapsed = now - lastTime;
85
+ if (elapsed > 500) {
86
+ const fps = (frameCount / elapsed * 1000).toFixed(1);
87
+ hud.textContent =
88
+ `mapspinner SDK example\n` +
89
+ `FPS: ${fps}\n` +
90
+ `Quads: ${frameResult.quadCount || 0}\n` +
91
+ `Controls: WASD fly, QE zoom, R reset`;
92
+ frameCount = 0;
93
+ lastTime = now;
94
+ }
95
+ } catch (e) {
96
+ hud.textContent = `Render error: ${e.message}`;
97
+ console.error(e);
98
+ }
99
+ }
100
+
101
+ // Handle window resize
102
+ window.addEventListener('resize', () => {
103
+ canvas.width = window.innerWidth;
104
+ canvas.height = window.innerHeight;
105
+ if (planet && planet.render && planet.render.cullMatrix) {
106
+ // Notify render layer of viewport change if needed
107
+ }
108
+ });
109
+
110
+ // Start initialization
111
+ init();
112
+ </script>
113
+ </body>
114
+ </html>
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "mapspinner",
3
+ "version": "0.1.1",
4
+ "description": "WebGL2 Earth-scale terrain rendering SDK for interactive globe applications",
5
+ "main": "src/index.js",
6
+ "exports": {
7
+ ".": "./src/index.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "test": "node tests/run.js",
12
+ "dev": "PORT=8080 node server.js"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/AnEntrypoint/mapspinner.git"
17
+ },
18
+ "keywords": [
19
+ "WebGL2",
20
+ "terrain",
21
+ "rendering",
22
+ "SDK",
23
+ "globe",
24
+ "3D"
25
+ ],
26
+ "author": "",
27
+ "license": "MIT"
28
+ }