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/.claude/workflows/fps-perf.js +147 -0
- package/.claude/workflows/optimize-dna.js +201 -0
- package/.claude/workflows/shader-bottleneck-dna.js +168 -0
- package/.claude/workflows/speed-dna.js +209 -0
- package/.claude/workflows/startup-perf.js +117 -0
- package/.github/workflows/publish.yml +43 -0
- package/AGENTS.md +265 -0
- package/CHANGELOG.md +31 -0
- package/CLAUDE.md +1 -0
- package/README.md +82 -0
- package/examples/basic-sdk-usage.html +114 -0
- package/package.json +28 -0
- package/planet.html +2181 -0
- package/planet.zip +0 -0
- package/scripts/backend-ab.mjs +88 -0
- package/scripts/dev-chrome.cmd +22 -0
- package/scripts/verify.mjs +69 -0
- package/server.js +127 -0
- package/src/anchor-field.js +559 -0
- package/src/gl-render.js +944 -0
- package/src/index.js +41 -0
- package/src/planet-orchestrator.js +790 -0
- package/src/quadtree.js +160 -0
- package/src/shaders/atmosphere.glsl +215 -0
- package/src/shaders/terrain.glsl +2109 -0
- package/src/terrain-gen-controls.js +122 -0
- package/tests/run.js +58 -0
- package/textures/grass-color.jpg +0 -0
- package/textures/grass-displacement.jpg +0 -0
- package/textures/rock-color.jpg +0 -0
- package/textures/rock-displacement.jpg +0 -0
- package/textures/sand-color.jpg +0 -0
- package/textures/sand-displacement.jpg +0 -0
- package/textures/snow-color.jpg +0 -0
- package/textures/snow-displacement.jpg +0 -0
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
|
+
}
|