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/planet.zip ADDED
Binary file
@@ -0,0 +1,88 @@
1
+ // backend-ab.mjs -- the "is this GPU/backend-keyed?" one-command answer (2026-06-12 tooling; the
2
+ // question that cost a full day on the FXC per-callsite hunt). Launches TWO Chromes against the
3
+ // live server -- default ANGLE (d3d11 on Windows) and vulkan -- parks BOTH at the same pose, and
4
+ // prints renderer string + luminance stats + a verdict, saving side-by-side screenshots to .gm/.
5
+ //
6
+ // node scripts/backend-ab.mjs # deterministic lowland pose
7
+ // node scripts/backend-ab.mjs 0.21 0.43 0.87 5 # camDir x y z + altKm
8
+ //
9
+ // Needs: server.js on :8080, Chrome installed. Cold d3d11 compile is ONCE per profile (cached after).
10
+ import { spawn } from 'child_process';
11
+ import fs from 'fs';
12
+
13
+ const CHROME = ['C:/Program Files/Google/Chrome/Application/chrome.exe',
14
+ 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe'].find(p => fs.existsSync(p));
15
+ const [dx, dy, dz, altKm] = process.argv.slice(2).map(Number);
16
+ const havePose = [dx, dy, dz].every(Number.isFinite);
17
+
18
+ const CFG = [
19
+ { name: 'd3d11', port: 9231, args: [] },
20
+ { name: 'vulkan', port: 9232, args: ['--use-angle=vulkan'] },
21
+ ];
22
+
23
+ async function cdp(port) {
24
+ const ver = await (await fetch(`http://localhost:${port}/json/version`)).json();
25
+ const list = await (await fetch(`http://localhost:${port}/json`)).json();
26
+ const pg = list.find(t => t.type === 'page' && t.url.includes('localhost:8080'));
27
+ const ws = new WebSocket(ver.webSocketDebuggerUrl);
28
+ await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; });
29
+ let seq = 0; const pending = new Map();
30
+ ws.onmessage = (ev) => { const m = JSON.parse(ev.data);
31
+ if (m.id && pending.has(m.id)) { const { res, rej } = pending.get(m.id); pending.delete(m.id);
32
+ m.error ? rej(new Error(m.error.message)) : res(m.result); } };
33
+ const send = (method, params = {}, sessionId) => new Promise((res, rej) => {
34
+ const id = ++seq; pending.set(id, { res, rej });
35
+ ws.send(JSON.stringify(sessionId ? { id, method, params, sessionId } : { id, method, params })); });
36
+ const { sessionId } = await send('Target.attachToTarget', { targetId: pg.id, flatten: true });
37
+ await send('Runtime.enable', {}, sessionId);
38
+ // foreground so the compile poll runs at full rate (background rAF throttling)
39
+ await fetch(`http://localhost:${port}/json/activate/${pg.id}`).catch(() => {});
40
+ const evalIn = async (expression) => {
41
+ const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }, sessionId);
42
+ if (r.exceptionDetails) throw new Error((r.exceptionDetails.exception?.description || r.exceptionDetails.text).slice(0, 300));
43
+ return r.result.value; };
44
+ return { evalIn, send, sessionId, ws };
45
+ }
46
+
47
+ const POSE = havePose
48
+ ? `const u0=[${dx},${dy},${dz}];const l0=Math.hypot(...u0);const land={u:[u0[0]/l0,u0[1]/l0,u0[2]/l0]};`
49
+ : `let land=null;for(let i=0;i<3000&&!land;i++){const y=1-2*(i+0.5)/3000,rr=Math.sqrt(Math.max(0,1-y*y)),t=i*2.399963229;
50
+ const u=[Math.cos(t)*rr,y,Math.sin(t)*rr];const h=sg(u);if(h>200&&h<900&&Math.abs(y)<0.5)land={u,h};}`;
51
+
52
+ async function measure(name, port) {
53
+ const c = await cdp(port);
54
+ for (;;) { const st = await c.evalIn(`window.__planetOrchStatus||'init'`).catch(() => 'nav');
55
+ if (st === 'ready') break; await new Promise(r => setTimeout(r, 5000)); }
56
+ const out = await c.evalIn(`(async()=>{
57
+ const d=window.__diag; await d.probeWarm();
58
+ const sg=window.__planetOrch.render.sampleGroundM;
59
+ ${POSE}
60
+ const camC=window.__dbg.cam,latC=Math.asin(land.u[1]),lonC=Math.atan2(land.u[0],land.u[2]);
61
+ camC.sunLatBase=Math.max(-1.4,latC-0.5);camC.sunLonBase=lonC;camC.sunLonAccum=0;camC.timeScale=0;
62
+ await d.aimDir(land.u, ${Number.isFinite(altKm) ? altKm : 2}, 50);
63
+ const A=d._read(); let n=0,s=0,s2=0,grey=0;
64
+ for(let i=0;i<A.px.length;i+=28){const r0=A.px[i],g0=A.px[i+1],b0=A.px[i+2];
65
+ const lum=0.2126*r0+0.7152*g0+0.0722*b0;n++;s+=lum;s2+=lum*lum;
66
+ const mx=Math.max(r0,g0,b0),mn=Math.min(r0,g0,b0);if(mx>30&&(mx-mn)<18)grey++;}
67
+ const mean=s/n,sd=Math.sqrt(Math.max(0,s2/n-mean*mean));
68
+ return {gpu:(window.__gpuRenderer||'').slice(0,70),lumMean:+mean.toFixed(1),lumSD:+sd.toFixed(2),greyFrac:+(grey/n).toFixed(3)};
69
+ })()`);
70
+ const shot = await c.send('Page.captureScreenshot', { format: 'png' }, c.sessionId);
71
+ fs.writeFileSync(`.gm/ab-${name}.png`, Buffer.from(shot.data, 'base64'));
72
+ c.ws.close();
73
+ return out;
74
+ }
75
+
76
+ for (const cfg of CFG) {
77
+ spawn(CHROME, [`--user-data-dir=C:/dev/tv8/.gm/tmp/ab-${cfg.name}`, `--remote-debugging-port=${cfg.port}`,
78
+ '--no-first-run', '--no-default-browser-check', ...cfg.args, 'http://localhost:8080/planet.html'],
79
+ { detached: true, stdio: 'ignore' }).unref();
80
+ }
81
+ await new Promise(r => setTimeout(r, 8000));
82
+ const results = {};
83
+ for (const cfg of CFG) results[cfg.name] = await measure(cfg.name, cfg.port);
84
+ const dSD = Math.abs(results.d3d11.lumSD - results.vulkan.lumSD);
85
+ const dGrey = Math.abs(results.d3d11.greyFrac - results.vulkan.greyFrac);
86
+ console.log(JSON.stringify({ ...results,
87
+ verdict: (dSD > 5 || dGrey > 0.15) ? 'BACKEND-DIVERGENT (suspect FXC translation -- see AGENTS.md FXC section)' : 'backends agree',
88
+ screenshots: ['.gm/ab-d3d11.png', '.gm/ab-vulkan.png'] }, null, 1));
@@ -0,0 +1,22 @@
1
+ @echo off
2
+ rem TV8 dev Chrome launcher -- kills the 150s cold shader compile (measured 2026-06-10).
3
+ rem
4
+ rem The ~150s cold compile of terrain.glsl is the ANGLE D3D11 backend's FXC
5
+ rem HLSL-optimize pass (default Chrome on Windows). Switching the ANGLE backend
6
+ rem removes FXC entirely; same shader source, measured cold shaderCompileMs:
7
+ rem d3d11 (default) : 152379 ms
8
+ rem opengl : 4653 ms cold / 96 ms warm (NVIDIA GL driver cache)
9
+ rem vulkan : 140 ms
10
+ rem Every shader EDIT recompiles cold, so on vulkan the edit loop is ~0.1s not 150s.
11
+ rem
12
+ rem Usage: scripts\dev-chrome.cmd [gl|d3d11] (default: vulkan)
13
+ rem A persistent profile keeps driver/browser caches warm across runs.
14
+
15
+ set BACKEND=vulkan
16
+ if /i "%1"=="gl" set BACKEND=gl
17
+ if /i "%1"=="d3d11" set BACKEND=d3d11
18
+
19
+ set CHROME="C:\Program Files\Google\Chrome\Application\chrome.exe"
20
+ if not exist %CHROME% set CHROME="C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
21
+
22
+ %CHROME% --user-data-dir="%~dp0..\.gm\tmp\chrome-dev-%BACKEND%" --use-angle=%BACKEND% --remote-debugging-port=9222 --no-first-run --no-default-browser-check http://localhost:8080/
@@ -0,0 +1,69 @@
1
+ // verify.mjs -- TV8 permanent make-sure-it-works runner (2026-06-11 policy: every render/shader
2
+ // fix is verified against the LIVE page before commit; compile-clean alone never ships).
3
+ //
4
+ // Drives the in-page witness suite (window.__diag.verifyAll / coastWitness / materialWitness /
5
+ // shadeKeyWitness / limbScan / hazeProbe ... in planet.html) over RAW CDP using Node's built-in
6
+ // WebSocket -- no relay, no per-call execution cap, no session recycling, zero dependencies.
7
+ //
8
+ // Usage:
9
+ // node scripts/verify.mjs # full suite (__diag.verifyAll)
10
+ // node scripts/verify.mjs materialWitness # one probe
11
+ // node scripts/verify.mjs "expr" # any expression on the planet page (await'ed)
12
+ // Needs: dev server on :8080 and a chrome with --remote-debugging-port=9222 (headless ok):
13
+ // chrome --headless=new --remote-debugging-port=9222 --user-data-dir=.gm/.cdp-profile about:blank
14
+ // Exit code 0 = pass, 1 = fail/error. Prints the JSON verdict.
15
+
16
+ const CDP_HTTP = process.env.CDP_URL || 'http://localhost:9222';
17
+ const PAGE_URL = process.env.PAGE_URL || 'http://localhost:8080/planet.html';
18
+ const probe = process.argv[2] || 'verifyAll';
19
+
20
+ const ver = await (await fetch(CDP_HTTP + '/json/version')).json();
21
+ const ws = new WebSocket(ver.webSocketDebuggerUrl);
22
+ await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; });
23
+
24
+ let seq = 0; const pending = new Map();
25
+ ws.onmessage = (ev) => {
26
+ const m = JSON.parse(ev.data);
27
+ if (m.id && pending.has(m.id)) { const { res, rej } = pending.get(m.id); pending.delete(m.id);
28
+ m.error ? rej(new Error(m.error.message)) : res(m.result); }
29
+ };
30
+ const send = (method, params = {}, sessionId) => new Promise((res, rej) => {
31
+ const id = ++seq; pending.set(id, { res, rej });
32
+ ws.send(JSON.stringify(sessionId ? { id, method, params, sessionId } : { id, method, params }));
33
+ });
34
+
35
+ const { targetId } = await send('Target.createTarget', { url: PAGE_URL });
36
+ const { sessionId } = await send('Target.attachToTarget', { targetId, flatten: true });
37
+ await send('Runtime.enable', {}, sessionId);
38
+
39
+ const evalIn = async (expression, awaitPromise = true) => {
40
+ const r = await send('Runtime.evaluate', { expression, awaitPromise, returnByValue: true }, sessionId);
41
+ if (r.exceptionDetails) throw new Error(r.exceptionDetails.text + ' ' + (r.exceptionDetails.exception?.description || '').slice(0, 300));
42
+ return r.result.value;
43
+ };
44
+
45
+ // wait for the orchestrator (cold shader compile can take minutes on SwiftShader)
46
+ const deadline = Date.now() + 8 * 60 * 1000;
47
+ // Close the created page on EVERY exit path (perf sweep follow-up 2026-06-11: early-exit/killed runs
48
+ // leaked one headless planet.html per run; leaked pages poll /cmd and steal live-tab diagnostics).
49
+ const closeTarget = () => send('Target.closeTarget', { targetId }).catch(() => {});
50
+ process.on('SIGINT', async () => { await closeTarget(); process.exit(130); });
51
+ process.on('SIGTERM', async () => { await closeTarget(); process.exit(143); });
52
+ for (;;) {
53
+ const st = await evalIn('window.__planetOrchStatus || "init"', false).catch(() => 'navigating');
54
+ if (st === 'ready') break;
55
+ if (st === 'error') { console.log(JSON.stringify({ pass: false, err: 'orch-error' })); await closeTarget(); process.exit(1); }
56
+ if (Date.now() > deadline) { console.log(JSON.stringify({ pass: false, err: 'ready-timeout' })); await closeTarget(); process.exit(1); }
57
+ await new Promise(r => setTimeout(r, 4000));
58
+ }
59
+
60
+ const expr = /^[A-Za-z]\w*$/.test(probe)
61
+ ? `window.__diag.${probe}()`
62
+ : probe;
63
+ let verdict;
64
+ try { verdict = await evalIn(`(async()=>{ const r = await (${expr}); return r; })()`); }
65
+ catch (e) { verdict = { pass: false, err: String(e.message || e).slice(0, 500) }; }
66
+
67
+ await closeTarget();
68
+ console.log(JSON.stringify(verdict, null, 1));
69
+ process.exit(verdict && (verdict.pass || verdict.ok) ? 0 : 1);
package/server.js ADDED
@@ -0,0 +1,127 @@
1
+ const http = require('http');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const PORT = process.env.PORT ? parseInt(process.env.PORT) : 8080;
6
+ // Static file server for the GPU one-fractal planet (planet.html + src/*.js + shaders/*.glsl).
7
+ // No C++/wasm binary, no capture/diag sinks (the old GPU-capture pipeline is deleted).
8
+ const MIME_TYPES = {
9
+ '.html': 'text/html; charset=utf-8',
10
+ '.js': 'text/javascript; charset=utf-8',
11
+ '.mjs': 'text/javascript; charset=utf-8', // ES module imports require a JS MIME (browser rejects octet-stream)
12
+ '.json': 'application/json',
13
+ '.png': 'image/png',
14
+ '.css': 'text/css',
15
+ '.glsl': 'text/plain; charset=utf-8',
16
+ };
17
+
18
+ const server = http.createServer((req, res) => {
19
+ // Set COOP/COEP headers for SharedArrayBuffer support
20
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
21
+ res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
22
+ res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
23
+ // Disable caching so iterative dev edits show up on reload
24
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
25
+ res.setHeader('Pragma', 'no-cache');
26
+ res.setHeader('Expires', '0');
27
+
28
+ const urlPath = req.url.split('?')[0];
29
+
30
+ // DIAG SINK (re-hosted 2026-06-07, user: 'it wants to contact /diag, we're missing diagnostic info by
31
+ // not hosting that'). planet.html postDiag() POSTs a per-frame JSON line; without a handler it 404s and
32
+ // the live diagnostic stream is lost. The last 200 lines are kept in an in-memory ring so a headless
33
+ // agent can GET /diag/tail and read the live render state (face/alt/quads/glError/height) WITHOUT a
34
+ // browser tool. (The old capture/diag.ndjson file append was a dead ENOENT no-op after capture/ was
35
+ // deleted in c131284 -- removed 2026-06-11; the ring is the only consumer anyone reads.)
36
+ if (urlPath === '/diag' && req.method === 'POST') {
37
+ let body = '';
38
+ req.on('data', c => { body += c; if (body.length > 1e6) req.destroy(); });
39
+ req.on('end', () => {
40
+ try {
41
+ const line = body.trim();
42
+ if (line) {
43
+ server._diagRing = server._diagRing || [];
44
+ server._diagRing.push(line);
45
+ if (server._diagRing.length > 200) server._diagRing.shift();
46
+ }
47
+ } catch (_) {}
48
+ res.writeHead(204); res.end();
49
+ });
50
+ return;
51
+ }
52
+ if (urlPath === '/diag/tail') {
53
+ const ring = server._diagRing || [];
54
+ const n = Math.min(ring.length, parseInt((req.url.split('?')[1] || '').replace(/^n=/, '')) || 30);
55
+ res.writeHead(200, { 'Content-Type': 'application/x-ndjson; charset=utf-8' });
56
+ res.end(ring.slice(-n).join('\n') + '\n');
57
+ return;
58
+ }
59
+ if (urlPath === '/diag/clear') { server._diagRing = []; res.writeHead(204); res.end(); return; }
60
+
61
+ // AGENT COMMAND CHANNEL (2026-06-07, user: 'maximize live code execution + hot reloading'). Lets a
62
+ // headless agent drive the warm tab WITHOUT a reload or browser tool: POST /cmd {js} enqueues a JS
63
+ // snippet; the page polls GET /cmd/next, runs it, and POSTs the result to /diag (kind:'cmd-result').
64
+ // So toggling the atlas, hot-reloading the shader (window.__diag.recompile), and probing live state
65
+ // are all one curl away -- the simplest possible iterate loop given the cold compile blocks the tool.
66
+ if (urlPath === '/cmd' && req.method === 'POST') {
67
+ let body = '';
68
+ req.on('data', c => { body += c; if (body.length > 2e6) req.destroy(); });
69
+ req.on('end', () => {
70
+ try { const o = JSON.parse(body); server._cmdQ = server._cmdQ || []; server._cmdQ.push({ id: (server._cmdId = (server._cmdId || 0) + 1), js: o.js }); res.writeHead(200, {'Content-Type':'application/json'}); res.end(JSON.stringify({ queued: server._cmdId })); }
71
+ catch (e) { res.writeHead(400); res.end(String(e)); }
72
+ });
73
+ return;
74
+ }
75
+ if (urlPath === '/cmd/next') {
76
+ const q = server._cmdQ || [];
77
+ const cmd = q.shift() || null;
78
+ res.writeHead(200, { 'Content-Type': 'application/json' });
79
+ res.end(JSON.stringify(cmd));
80
+ return;
81
+ }
82
+ // Root IS the GPU planet (planet.html): one-fractal terrain on WebGL, no wasm. /rewrite.html
83
+ // kept as an alias. Everything else falls through to a static file (src/, shaders, etc.).
84
+ let filepath;
85
+ if (urlPath === '/' || urlPath === '/index.html' || urlPath === '/rewrite.html' || urlPath === '/rewrite') {
86
+ filepath = path.join(__dirname, 'planet.html');
87
+ } else {
88
+ filepath = path.join(__dirname, urlPath);
89
+ }
90
+
91
+ // Prevent directory traversal
92
+ if (!filepath.startsWith(__dirname)) {
93
+ res.writeHead(403);
94
+ res.end('Forbidden');
95
+ return;
96
+ }
97
+
98
+ fs.stat(filepath, (err, stats) => {
99
+ if (err) {
100
+ res.writeHead(404);
101
+ res.end('Not Found');
102
+ return;
103
+ }
104
+
105
+ if (stats.isDirectory()) {
106
+ filepath = path.join(filepath, 'index.html');
107
+ }
108
+
109
+ const ext = path.extname(filepath);
110
+ const mimeType = MIME_TYPES[ext] || 'application/octet-stream';
111
+
112
+ fs.readFile(filepath, (err, content) => {
113
+ if (err) {
114
+ res.writeHead(500);
115
+ res.end('Server Error');
116
+ return;
117
+ }
118
+
119
+ res.writeHead(200, { 'Content-Type': mimeType });
120
+ res.end(content);
121
+ });
122
+ });
123
+ });
124
+
125
+ server.listen(PORT, () => {
126
+ console.log(`Server running at http://localhost:${PORT}`);
127
+ });