open-standards/wai/wai-web/demo/world.js
Transaction Science 1f5bfbd1e0 wai: no modelled or zero figure shown as a measurement
- The C settlement kernel graded every receipt measured, from hardware, by
  hardware_meter, whatever its meter; it now writes the grade and device the
  meter states. Its host harness's figures are constants: projected, from a
  simulation, by model_estimated (oer-receipts §2: an Estimator figure is
  projected). The Zephyr skeleton states projected and no longer seals a
  compute figure of 0. firmware_vectors.json is regenerated; both verifiers
  pass it.
- wai-web: the world demo seals the page's estimate labelled Estimator and says
  so on screen (label_page_figure, tested); the quantum demos seal the unmetered
  marker instead of invented constants, and their getters say a 0 is unmetered.
- The quantum studio shows a figure with its class, coverage and uncertainty;
  the quantum CLI prints the unmetered marker as unmetered (tested); the
  metering tools head a bracket as a bracket and print the marker on stdout when
  they seal it; a provenance total names the class it reads at (tested).
- energy-measurement §2.8 and §6 record which demos still seal an illustrative
  figure the page passes in.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 2a8d45aa0d)
2026-10-26 08:30:49 -04:00

925 lines
40 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// WAI interactive-worlds browser sink — WebGPU renderer over the
// WorldSink WASM binding (the same `wai` engine native sinks run).
//
// Two modes, decided by the payload itself:
// - sealed replay (`wai.world.replay`): play the op log, then verify
// the BLAKE3 state hash against the publisher's Ed25519 seal.
// - live (`wai.world.live`): this tab becomes a signed ACTOR. Clicks
// author real CausalOps (Ed25519-signed in WASM); a BroadcastChannel
// carries them to other tabs — transport is out of the extension's
// scope, and this is the smallest honest one. N tabs converge on the
// identical state hash because the math does, not because a server
// says so.
import init, { WorldSink, Actor, op_lamport } from './pkg/wai_web.js';
const WORLD = { xMin: -9, xMax: 9, yMin: -1, yMax: 11 }; // ortho camera box
const TICK_HZ = 60;
const SHOVE = 18.0; // base impulse; scaled by 1/distance — point-blank clicks snap a 20%-yield vertical, distant clicks wobble
const OP_LEAD_TICKS = 8; // author ops slightly in the future
const HASH_EVERY = 60; // live-mode hash exchange cadence (ticks)
const $ = (id) => document.getElementById(id);
const ui = {
vector: $('vector'), restart: $('restart'), pause: $('pause'), seal: $('seal'), badge: $('badge'),
sim: $('sim'), tick: $('tick'), final: $('final'), nodes: $('nodes'),
hash: $('hash'), bytes: $('bytes'), actors: $('actors'), sync: $('sync'), receipt: $('receipt'),
assets: $('assets'),
};
// Decoded wai.mesh.glb asset (gallery scene): {positions, edges,
// triIndices} — presentation only, never part of the state hash.
let assetMesh = null;
// 'sim' (2D), 'mesh' (3D PBR), or 'splat' (3D Gaussian splats) — set per vector.
let renderMode = 'sim';
const toHex = (u8) => Array.from(u8, b => b.toString(16).padStart(2, '0')).join('');
const fromHex = (s) => new Uint8Array(s.match(/.{2}/g).map(h => parseInt(h, 16)));
// ---------------------------------------------------------------------------
// WebGPU setup
// ---------------------------------------------------------------------------
const canvas = $('c');
if (!navigator.gpu) {
document.body.insertAdjacentHTML('beforeend',
'<p style="color:#ff6b6b">WebGPU unavailable — this sink declares capability by the core dispatch rule: fall back to the recorded rendition.</p>');
throw new Error('no webgpu');
}
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const ctx = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
ctx.configure({ device, format, alphaMode: 'premultiplied' });
const sx = 2 / (WORLD.xMax - WORLD.xMin), sy = 2 / (WORLD.yMax - WORLD.yMin);
const ox = -(WORLD.xMax + WORLD.xMin) / 2, oy = -(WORLD.yMax + WORLD.yMin) / 2;
const uniform = device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
device.queue.writeBuffer(uniform, 0, new Float32Array([sx, sy, ox, oy]));
const shader = device.createShaderModule({ code: /* wgsl */`
struct Cam { scale: vec2f, offset: vec2f }
@group(0) @binding(0) var<uniform> cam: Cam;
fn toClip(p: vec2f) -> vec2f { return (p + cam.offset) * cam.scale; }
struct CircOut {
@builtin(position) pos: vec4f,
@location(0) uv: vec2f,
@location(1) kind: f32,
}
@vertex
fn circVs(@builtin(vertex_index) vi: u32,
@location(0) center: vec3f,
@location(1) r: f32,
@location(2) kind: f32) -> CircOut {
var corners = array<vec2f, 6>(
vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(-1.0, 1.0),
vec2f(-1.0, 1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0));
let c = corners[vi];
var out: CircOut;
out.pos = vec4f(toClip(center.xy + c * r), 0.0, 1.0);
out.uv = c;
out.kind = kind;
return out;
}
@fragment
fn circFs(in: CircOut) -> @location(0) vec4f {
let d = length(in.uv);
if (d > 1.0) { discard; }
let glow = smoothstep(1.0, 0.55, d);
let free = vec3f(0.0, 0.898, 0.627);
let anch = vec3f(0.941, 0.706, 0.161);
let col = mix(free, anch, in.kind);
return vec4f(col * glow, glow);
}
@vertex
fn lineVs(@location(0) p: vec3f) -> @builtin(position) vec4f {
return vec4f(toClip(p.xy), 0.0, 1.0);
}
@fragment
fn lineFs() -> @location(0) vec4f { return vec4f(0.0, 0.72, 0.83, 0.9); }
@vertex
fn groundVs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
let x = select(-9.0, 9.0, vi == 1u);
return vec4f(toClip(vec2f(x, 0.0)), 0.0, 1.0);
}
@fragment
fn groundFs() -> @location(0) vec4f { return vec4f(0.18, 0.23, 0.32, 1.0); }
`});
const circPipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: shader, entryPoint: 'circVs',
buffers: [{
arrayStride: 20, stepMode: 'instance',
attributes: [
{ shaderLocation: 0, offset: 0, format: 'float32x3' },
{ shaderLocation: 1, offset: 12, format: 'float32' },
{ shaderLocation: 2, offset: 16, format: 'float32' },
],
}],
},
fragment: { module: shader, entryPoint: 'circFs', targets: [{
format,
blend: { color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' },
alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' } },
}] },
primitive: { topology: 'triangle-list' },
});
const linePipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: shader, entryPoint: 'lineVs',
buffers: [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] }],
},
fragment: { module: shader, entryPoint: 'lineFs', targets: [{ format }] },
primitive: { topology: 'line-list' },
});
const groundPipeline = device.createRenderPipeline({
layout: 'auto',
vertex: { module: shader, entryPoint: 'groundVs' },
fragment: { module: shader, entryPoint: 'groundFs', targets: [{ format }] },
primitive: { topology: 'line-list' },
});
const circBind = device.createBindGroup({ layout: circPipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: uniform } }] });
const lineBind = device.createBindGroup({ layout: linePipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: uniform } }] });
const groundBind = device.createBindGroup({ layout: groundPipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: uniform } }] });
function gpuBuf() { return { buf: null, cap: 0 }; }
const circles = gpuBuf(), lines = gpuBuf();
function upload(slot, data) {
const bytes = data.byteLength;
if (bytes > slot.cap) {
slot.buf?.destroy();
slot.cap = Math.max(256, bytes * 2);
slot.buf = device.createBuffer({ size: slot.cap, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
}
if (bytes > 0) device.queue.writeBuffer(slot.buf, 0, data);
}
// ---------------------------------------------------------------------------
// 3D render path (gallery asset rendering: PBR mesh + Gaussian splats)
// ---------------------------------------------------------------------------
// Minimal column-major mat4 helpers (no dependency).
const mat4 = {
perspective(fovy, aspect, near, far) {
const f = 1 / Math.tan(fovy / 2), nf = 1 / (near - far);
return [f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, (far + near) * nf, -1, 0, 0, 2 * far * near * nf, 0];
},
mul(a, b) {
const o = new Array(16);
for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) {
o[c * 4 + r] = a[r] * b[c * 4] + a[4 + r] * b[c * 4 + 1] + a[8 + r] * b[c * 4 + 2] + a[12 + r] * b[c * 4 + 3];
}
return o;
},
lookAt(eye, ctr, up) {
const z = norm3(sub3(eye, ctr)), x = norm3(cross3(up, z)), y = cross3(z, x);
return [x[0], y[0], z[0], 0, x[1], y[1], z[1], 0, x[2], y[2], z[2], 0,
-dot3(x, eye), -dot3(y, eye), -dot3(z, eye), 1];
},
};
const sub3 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
const cross3 = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
const dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
const norm3 = (a) => { const l = Math.hypot(a[0], a[1], a[2]) || 1; return [a[0] / l, a[1] / l, a[2] / l]; };
// Orbiting camera for an object at the origin. Returns vp + view + proj
// separately: the splat renderer needs view & proj apart to build the
// EWA covariance-projection Jacobian.
const FOVY = 0.9;
function camera(angle) {
const r = 3.2, eye = [Math.sin(angle) * r, 1.1, Math.cos(angle) * r];
const proj = mat4.perspective(FOVY, canvas.width / canvas.height, 0.1, 100);
const view = mat4.lookAt(eye, [0, 0, 0], [0, 1, 0]);
return { vp: mat4.mul(proj, view), view, proj, eye };
}
// Cam uniform (256 B): vp[16] view[16] proj[16] eye(vec4) params(vec4:
// vpW, vpH, focalX, focalY).
const cam3d = device.createBuffer({ size: 256, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
const CAM_WGSL = `struct Cam {
vp: mat4x4f, view: mat4x4f, proj: mat4x4f, eye: vec4f, params: vec4f,
}
@group(0) @binding(0) var<uniform> cam: Cam;`;
let depthTex = device.createTexture({
size: [canvas.width, canvas.height], format: 'depth24plus',
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
// ---- PBR-ish mesh: solid triangles, flat normals from derivatives ----
const meshShader = device.createShaderModule({ code: /* wgsl */`
${CAM_WGSL}
struct VOut { @builtin(position) clip: vec4f, @location(0) world: vec3f }
@vertex fn vs(@location(0) p: vec3f) -> VOut {
var o: VOut; o.world = p; o.clip = cam.vp * vec4f(p, 1.0); return o;
}
@fragment fn fs(in: VOut) -> @location(0) vec4f {
let n = normalize(cross(dpdx(in.world), dpdy(in.world))); // flat normal, no attribute needed
let L = normalize(vec3f(0.5, 0.9, 0.4));
let V = normalize(cam.eye.xyz - in.world);
let H = normalize(L + V);
let base = vec3f(0.83, 0.62, 0.36); // warm metal (site accent family)
let diff = max(dot(n, L), 0.0);
let spec = pow(max(dot(n, H), 0.0), 48.0) * 0.6;
let amb = 0.18 + 0.12 * (n.y * 0.5 + 0.5); // soft sky ambient
let col = base * (amb + diff * 0.9) + vec3f(spec);
return vec4f(pow(col, vec3f(0.4545)), 1.0); // gamma
}`});
const meshPipeline = device.createRenderPipeline({
layout: 'auto',
vertex: { module: meshShader, entryPoint: 'vs',
buffers: [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] }] },
fragment: { module: meshShader, entryPoint: 'fs', targets: [{ format }] },
primitive: { topology: 'triangle-list', cullMode: 'none' },
depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' },
});
const mesh3dBind = device.createBindGroup({ layout: meshPipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: cam3d } }] });
const meshVerts = gpuBuf(), meshIdx = gpuBuf();
function uploadIdx(slot, data) {
const bytes = data.byteLength;
if (bytes > slot.cap) {
slot.buf?.destroy();
slot.cap = Math.max(256, bytes * 2);
slot.buf = device.createBuffer({ size: slot.cap, usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST });
}
if (bytes > 0) device.queue.writeBuffer(slot.buf, 0, data);
}
// ---- Gaussian splats: real EWA splatting. Each splat is an oriented
// 3D Gaussian (anisotropic covariance); the vertex shader projects its
// covariance to a 2D screen-space conic (Σ' = J·W·Σ·Wᵀ·Jᵀ), emits a
// tight bounding quad, and the fragment evaluates the conic. Sorted
// back-to-front on the CPU, alpha-blended. The textbook 3DGS pipeline.
const splatShader = device.createShaderModule({ code: /* wgsl */`
${CAM_WGSL}
struct VOut {
@builtin(position) clip: vec4f,
@location(0) d: vec2f, // pixel offset from the splat center
@location(1) conic: vec3f, // inverse 2D covariance (a, b, c)
@location(2) col: vec4f,
}
@vertex fn vs(@builtin(vertex_index) vi: u32,
@location(0) center: vec3f,
@location(1) covA: vec3f, // cxx, cxy, cxz
@location(2) covB: vec3f, // cyy, cyz, czz
@location(3) col: vec4f) -> VOut {
var corners = array<vec2f,6>(vec2f(-1,-1),vec2f(1,-1),vec2f(-1,1),vec2f(-1,1),vec2f(1,-1),vec2f(1,1));
let q = corners[vi];
var o: VOut;
let tv = cam.view * vec4f(center, 1.0);
let t = tv.xyz;
if (t.z >= -0.05) { o.clip = vec4f(2.0, 2.0, 2.0, 1.0); o.d = vec2f(0); o.conic = vec3f(1,0,1); o.col = vec4f(0); return o; }
let fx = cam.params.z; let fy = cam.params.w;
let z = t.z; let z2 = z * z;
// world→view rotation and the perspective Jacobian (3rd row 0).
let W = mat3x3f(cam.view[0].xyz, cam.view[1].xyz, cam.view[2].xyz);
let J = mat3x3f(
vec3f(fx / (-z), 0.0, 0.0),
vec3f(0.0, fy / (-z), 0.0),
vec3f(fx * t.x / z2, fy * t.y / z2, 0.0));
let cov3 = mat3x3f(
vec3f(covA.x, covA.y, covA.z),
vec3f(covA.y, covB.x, covB.y),
vec3f(covA.z, covB.y, covB.z));
let camCov = W * cov3 * transpose(W);
let c2 = J * camCov * transpose(J);
var a = c2[0][0] + 0.3; var b = c2[1][0]; var cc = c2[1][1] + 0.3; // low-pass
let det = a * cc - b * b;
if (det <= 0.0) { o.clip = vec4f(2.0, 2.0, 2.0, 1.0); o.d = vec2f(0); o.conic = vec3f(1,0,1); o.col = vec4f(0); return o; }
o.conic = vec3f(cc, -b, a) / det;
let mid = 0.5 * (a + cc);
let l1 = mid + sqrt(max(0.1, mid * mid - det));
let radius = ceil(3.0 * sqrt(l1)); // pixels
let clip = cam.proj * tv;
o.d = q * radius;
o.clip = vec4f(clip.xy + q * radius * 2.0 / cam.params.xy * clip.w, clip.zw);
o.col = col;
return o;
}
@fragment fn fs(in: VOut) -> @location(0) vec4f {
let power = -0.5 * (in.conic.x * in.d.x * in.d.x + in.conic.z * in.d.y * in.d.y)
- in.conic.y * in.d.x * in.d.y;
if (power > 0.0) { discard; }
let alpha = min(0.99, in.col.a * exp(power));
return vec4f(in.col.rgb * alpha, alpha); // premultiplied
}`});
const splatPipeline = device.createRenderPipeline({
layout: 'auto',
vertex: { module: splatShader, entryPoint: 'vs',
buffers: [{ arrayStride: 52, stepMode: 'instance', attributes: [
{ shaderLocation: 0, offset: 0, format: 'float32x3' },
{ shaderLocation: 1, offset: 12, format: 'float32x3' },
{ shaderLocation: 2, offset: 24, format: 'float32x3' },
{ shaderLocation: 3, offset: 36, format: 'float32x4' },
] }] },
fragment: { module: splatShader, entryPoint: 'fs', targets: [{
format,
blend: { color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' },
alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' } },
}] },
primitive: { topology: 'triangle-list' },
});
const splat3dBind = device.createBindGroup({ layout: splatPipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: cam3d } }] });
const splatBuf = gpuBuf();
const SPLAT_FLOATS = 13; // center3, cov3d(6), rgba4
// Derive an ANISOTROPIC Gaussian-splat cloud from the verified mesh:
// each barycentric sample becomes a flat disc oriented to the triangle
// (wide in the surface tangent plane, thin along the normal) — the way
// a real splat capture of a flat surface looks. Per-splat 3×3 covariance
// Σ = sₜ²(t1t1ᵀ + t2t2ᵀ) + sₙ²(nnᵀ). Same content-addressed asset, a
// different sink-side rendering.
let splatCloud = null;
function buildSplatCloud(mesh, perTri = 64) {
const pos = mesh.positions, idx = mesh.triIndices;
const tris = idx.length / 3;
const n = tris * perTri;
const centers = new Float32Array(n * 3), inst = new Float32Array(n * SPLAT_FLOATS);
const st = 0.11, sn = 0.018, st2 = st * st, sn2 = sn * sn; // tangent / normal sigmas
let s = 12345 >>> 0; const rnd = () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296;
let k = 0;
for (let t = 0; t < tris; t++) {
const a = idx[t * 3], b = idx[t * 3 + 1], c = idx[t * 3 + 2];
const pa = [pos[a * 3], pos[a * 3 + 1], pos[a * 3 + 2]];
const pb = [pos[b * 3], pos[b * 3 + 1], pos[b * 3 + 2]];
const pc = [pos[c * 3], pos[c * 3 + 1], pos[c * 3 + 2]];
const t1 = norm3(sub3(pb, pa));
const nrm = norm3(cross3(sub3(pb, pa), sub3(pc, pa)));
const t2 = norm3(cross3(nrm, t1));
// symmetric covariance entries
const cxx = st2 * (t1[0] * t1[0] + t2[0] * t2[0]) + sn2 * nrm[0] * nrm[0];
const cyy = st2 * (t1[1] * t1[1] + t2[1] * t2[1]) + sn2 * nrm[1] * nrm[1];
const czz = st2 * (t1[2] * t1[2] + t2[2] * t2[2]) + sn2 * nrm[2] * nrm[2];
const cxy = st2 * (t1[0] * t1[1] + t2[0] * t2[1]) + sn2 * nrm[0] * nrm[1];
const cxz = st2 * (t1[0] * t1[2] + t2[0] * t2[2]) + sn2 * nrm[0] * nrm[2];
const cyz = st2 * (t1[1] * t1[2] + t2[1] * t2[2]) + sn2 * nrm[1] * nrm[2];
for (let j = 0; j < perTri; j++) {
let u = rnd(), v = rnd(); if (u + v > 1) { u = 1 - u; v = 1 - v; }
const w = 1 - u - v;
const x = pa[0] * w + pb[0] * u + pc[0] * v;
const y = pa[1] * w + pb[1] * u + pc[1] * v;
const z = pa[2] * w + pb[2] * u + pc[2] * v;
centers[k * 3] = x; centers[k * 3 + 1] = y; centers[k * 3 + 2] = z;
const o = k * SPLAT_FLOATS;
inst[o] = x; inst[o + 1] = y; inst[o + 2] = z;
inst[o + 3] = cxx; inst[o + 4] = cxy; inst[o + 5] = cxz;
inst[o + 6] = cyy; inst[o + 7] = cyz; inst[o + 8] = czz;
inst[o + 9] = 0.5 + x; inst[o + 10] = 0.6 + y * 0.8; inst[o + 11] = 0.7 - z; inst[o + 12] = 0.9;
k++;
}
}
return { centers, instances: inst, count: n };
}
// ---------------------------------------------------------------------------
// Sink lifecycle
// ---------------------------------------------------------------------------
await init();
let sink = null, playing = true, done = false;
let live = null; // live-session state (playground mode), else null
function closeLive() {
if (live) { live.transport?.close(); live = null; }
ui.actors.textContent = '—';
ui.sync.textContent = '—';
}
async function load(name) {
closeLive();
assetMesh = null;
splatCloud = null;
computeMs = 0;
ui.assets.textContent = '—';
// gallery / gallery_splat both load the same verified scene; the
// suffix only chooses the sink-side rendering.
const file = name.startsWith('gallery') ? 'gallery' : name;
renderMode = name === 'gallery' ? 'mesh' : name === 'gallery_splat' ? 'splat' : 'sim';
const resp = await fetch(`vectors/${file}.wwld`);
const bytes = new Uint8Array(await resp.arrayBuffer());
sink = new WorldSink(bytes);
done = false;
playing = true;
ui.badge.className = '';
ui.badge.textContent = '';
ui.pause.textContent = '⏸ pause';
ui.sim.textContent = sink.sim().replace('wai.world.sim.', '');
ui.final.textContent = sink.final_tick().toString();
ui.bytes.textContent = bytes.length.toString();
// Only the playground is a live, authored session; other scenes
// (e.g. gallery) are static — they render, they don't open a transport.
if (name === 'playground') await startLive(name);
await loadAssets();
}
// Fetch, content-hash-verify, and decode the world's wai.mesh.glb
// assets (extension §4.1). The verify happens in WASM (asset_glb
// rejects a hash mismatch); the GLB decode is the page's job — WAI
// dispatches, it does not decode meshes.
async function loadAssets() {
let list;
try { list = JSON.parse(sink.assets()); } catch { list = []; }
if (!list.length) return;
const fetchAsset = async (a, cap) => {
const env = new Uint8Array(await (await fetch(`vectors/assets/${a.name}.wai`)).arrayBuffer());
return sink.asset_payload(a.name, env, cap); // throws on hash/capability mismatch
};
const glb = list.find(a => a.name.endsWith('.glb'));
const spz = list.find(a => a.name.endsWith('.spz'));
try {
if (renderMode === 'splat') {
// Prefer the real wai.splat.spz asset: fetch, hash-verify, gunzip
// + decode the actual SPZ bitstream. Fall back to a mesh-derived
// cloud only if no splat asset is present.
if (spz) {
splatCloud = await decodeSpz(await fetchAsset(spz, 'wai.splat.spz'));
upload(splatBuf, splatCloud.instances);
ui.assets.textContent = `1/1 verified ✓ · ${spz.name} · ${splatCloud.count} real Gaussians (SPZ decoded)`;
return;
}
assetMesh = decodeGlb(await fetchAsset(glb, 'wai.mesh.glb'));
splatCloud = buildSplatCloud(assetMesh);
upload(splatBuf, splatCloud.instances);
ui.assets.textContent = `verified ✓ · ${splatCloud.count} splats (mesh-derived)`;
return;
}
// mesh mode
assetMesh = decodeGlb(await fetchAsset(glb, 'wai.mesh.glb'));
upload(meshVerts, assetMesh.positions);
uploadIdx(meshIdx, assetMesh.triIndices);
ui.assets.textContent = `1/1 verified ✓ · ${glb.name} · PBR mesh`;
} catch (e) {
console.warn('asset:', e);
ui.assets.textContent = '✗ ' + (e.message || e);
}
}
// Real SPZ v3 decoder: gunzip (DecompressionStream), parse the
// 16-byte header, unpack the non-interleaved arrays (24-bit fixed-point
// positions, log scales, smallest-three quaternion, opacity/color =
// byte/255), and build per-Gaussian world covariance Σ = R·diag(σ²)·Rᵀ
// for the EWA splat pipeline. sh_degree 0 (no SH bands).
async function decodeSpz(u8) {
const stream = new Blob([u8]).stream().pipeThrough(new DecompressionStream('gzip'));
const buf = new Uint8Array(await new Response(stream).arrayBuffer());
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
if (dv.getInt32(0, true) !== 0x5053474e) throw new Error('not an SPZ file');
const n = dv.getInt32(8, true);
const fbits = buf[13];
let o = 16;
const pos = buf.subarray(o, o += n * 9);
const alpha = buf.subarray(o, o += n);
const col = buf.subarray(o, o += n * 3);
const scl = buf.subarray(o, o += n * 3);
const rot = buf.subarray(o, o += n * 4);
const fixedScale = 1 / (1 << fbits);
const centers = new Float32Array(n * 3);
const inst = new Float32Array(n * SPLAT_FLOATS);
for (let i = 0; i < n; i++) {
const p = [0, 0, 0];
for (let k = 0; k < 3; k++) {
let f = pos[i * 9 + k * 3] | (pos[i * 9 + k * 3 + 1] << 8) | (pos[i * 9 + k * 3 + 2] << 16);
if (f & 0x800000) f |= ~0xffffff; // sign-extend 24-bit
p[k] = f * fixedScale;
}
const sg = [
Math.exp(scl[i * 3] / 16 - 10),
Math.exp(scl[i * 3 + 1] / 16 - 10),
Math.exp(scl[i * 3 + 2] / 16 - 10),
];
const q = unpackQuat(rot, i * 4);
const cov = covFromQuatScale(q, sg);
centers[i * 3] = p[0]; centers[i * 3 + 1] = p[1]; centers[i * 3 + 2] = p[2];
const b = i * SPLAT_FLOATS;
inst[b] = p[0]; inst[b + 1] = p[1]; inst[b + 2] = p[2];
inst[b + 3] = cov[0]; inst[b + 4] = cov[1]; inst[b + 5] = cov[2];
inst[b + 6] = cov[3]; inst[b + 7] = cov[4]; inst[b + 8] = cov[5];
inst[b + 9] = col[i * 3] / 255; inst[b + 10] = col[i * 3 + 1] / 255;
inst[b + 11] = col[i * 3 + 2] / 255; inst[b + 12] = alpha[i] / 255;
}
return { centers, instances: inst, count: n };
}
// SPZ smallest-three quaternion (4 bytes) → [x, y, z, w].
function unpackQuat(r, off) {
let comp = (r[off] | (r[off + 1] << 8) | (r[off + 2] << 16) | (r[off + 3] << 24)) >>> 0;
const iLargest = comp >>> 30;
const q = [0, 0, 0, 0];
let sumsq = 0;
for (let i = 3; i >= 0; i--) {
if (i === iLargest) continue;
const mag = comp & 0x1ff;
const neg = (comp >>> 9) & 1;
comp >>>= 10;
let v = (Math.SQRT1_2 * mag) / 511;
if (neg) v = -v;
q[i] = v;
sumsq += v * v;
}
q[iLargest] = Math.sqrt(Math.max(0, 1 - sumsq));
return q;
}
// World covariance Σ = R·diag(σ²)·Rᵀ → 6 symmetric floats (cxx,cxy,cxz,cyy,cyz,czz).
function covFromQuatScale(q, sg) {
const [x, y, z, w] = q;
const r = [
1 - 2 * (y * y + z * z), 2 * (x * y - w * z), 2 * (x * z + w * y),
2 * (x * y + w * z), 1 - 2 * (x * x + z * z), 2 * (y * z - w * x),
2 * (x * z - w * y), 2 * (y * z + w * x), 1 - 2 * (x * x + y * y),
];
const s = [sg[0] * sg[0], sg[1] * sg[1], sg[2] * sg[2]];
const m = (a, b) => r[a] * r[b] * s[0] + r[a + 1] * r[b + 1] * s[1] + r[a + 2] * r[b + 2] * s[2];
// rows 0,1,2 start at 0,3,6
return [m(0, 0), m(0, 3), m(0, 6), m(3, 3), m(3, 6), m(6, 6)];
}
// Minimal glTF 2.0 Binary decoder: first mesh primitive's POSITION +
// indices. Slices into fresh buffers so typed-array alignment is never
// an issue regardless of where the WASM payload sits.
function decodeGlb(u8) {
const dv = new DataView(u8.buffer, u8.byteOffset, u8.byteLength);
if (dv.getUint32(0, true) !== 0x46546c67) throw new Error('not a glb');
const jsonLen = dv.getUint32(12, true);
const json = JSON.parse(new TextDecoder().decode(u8.subarray(20, 20 + jsonLen)));
const binStart = 20 + jsonLen + 8; // chunk0 data + chunk1 (len+type)
const bin = u8.subarray(binStart);
const prim = json.meshes[0].primitives[0];
const acc = json.accessors, bv = json.bufferViews;
const pAcc = acc[prim.attributes.POSITION], iAcc = acc[prim.indices];
const pBV = bv[pAcc.bufferView], iBV = bv[iAcc.bufferView];
const pOff = (pBV.byteOffset || 0), iOff = (iBV.byteOffset || 0);
const positions = new Float32Array(bin.slice(pOff, pOff + pAcc.count * 12).buffer);
const indices = new Uint16Array(bin.slice(iOff, iOff + iAcc.count * 2).buffer);
// unique edges from the triangle soup
const seen = new Set(), edges = [];
for (let t = 0; t < indices.length; t += 3) {
for (const [x, y] of [[0, 1], [1, 2], [2, 0]]) {
const a = indices[t + x], b = indices[t + y];
const key = a < b ? a * 65536 + b : b * 65536 + a;
if (!seen.has(key)) { seen.add(key); edges.push(a, b); }
}
}
return { positions, edges: new Uint16Array(edges), triIndices: indices };
}
// ---------------------------------------------------------------------------
// Live mode: this tab is an actor; tabs converge over BroadcastChannel
// ---------------------------------------------------------------------------
/// Transport selection: the relay (wss — cross-device, room in the URL
/// hash) when reachable, BroadcastChannel (same-browser tabs) otherwise.
/// Either way the payload is identical signed-op envelopes — transport
/// is out of the extension's scope, and convergence never depends on it.
function connectTransport(name, onMessage) {
return new Promise((resolve) => {
const room = ((location.hash.slice(1) || 'lobby').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 48) || 'lobby') + '-' + name;
let settled = false;
const fallback = () => {
if (settled) return; settled = true;
const chan = new BroadcastChannel(`wai-world-${name}`);
chan.onmessage = (ev) => onMessage(ev.data);
resolve({ kind: 'tabs', send: (m) => chan.postMessage(m), close: () => chan.close() });
};
let ws;
try {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
ws = new WebSocket(`${proto}://${location.host}/worlds-ws/${room}`);
} catch { return fallback(); }
const timer = setTimeout(() => { try { ws.close(); } catch {} fallback(); }, 2500);
ws.onerror = () => { if (!settled) { clearTimeout(timer); fallback(); } };
ws.onopen = () => {
clearTimeout(timer); settled = true;
ws.onmessage = (ev) => onMessage(JSON.parse(ev.data));
resolve({
kind: 'relay',
send: (m) => { if (ws.readyState === 1) ws.send(JSON.stringify(m)); },
close: () => ws.close(),
});
};
});
}
async function startLive(name) {
const seed = crypto.getRandomValues(new Uint8Array(32));
const actor = new Actor(seed);
live = {
actor,
pub: actor.pubkey_hex(),
epoch: Date.now(), // provisional; the smallest epoch wins (the relay's, if present)
send: () => {},
transport: null,
ops: [], // hex op log (for late joiners on the tabs fallback)
actors: new Set(), // pubkeys seen (incl. self)
hashes: new Map(), // our boundary-tick hashes (tick → hex)
peerOk: null,
rewound: false,
};
live.actors.add(live.pub);
const onMessage = (m) => {
if (!live) return;
if (m.t === 'hello') {
live.send({ t: 'sync', epoch: live.epoch, ops: live.ops, from: live.pub });
} else if (m.t === 'sync') {
if (m.from && m.from !== 'relay') live.actors.add(m.from);
if (m.epoch < live.epoch) { live.epoch = m.epoch; live.hashes.clear(); }
for (const hex of m.ops) ingestHex(hex, m.from);
} else if (m.t === 'op') {
live.actors.add(m.from);
ingestHex(m.bytes, m.from);
} else if (m.t === 'hash') {
live.actors.add(m.from);
const ours = live.hashes.get(m.tick);
if (ours !== undefined) {
live.peerOk = ours === m.hash ? { tick: m.tick } : false;
}
}
};
const transport = await connectTransport(name, onMessage);
if (!live) { transport.close(); return; } // vector switched mid-connect
live.transport = transport;
live.send = transport.send;
live.send({ t: 'hello' });
ui.badge.className = 'ok';
ui.badge.textContent = transport.kind === 'relay'
? '● live via relay — open this URL on ANY device and shove the tower'
: '● live (tab-local) — open this page in a second tab and shove the tower';
}
function ingestHex(hexOp, from) {
const bytes = fromHex(hexOp);
try {
live.actor.observe_lamport(op_lamport(bytes));
sink.ingest_op(bytes); // signature verified inside the engine
if (!live.ops.includes(hexOp)) live.ops.push(hexOp);
live.rewound = true; // conservatively drop stale boundary hashes
live.hashes.clear();
live.actors.add(from);
} catch (e) {
console.warn('op rejected:', e);
}
}
function liveTargetTick() {
return BigInt(Math.max(1, Math.floor((Date.now() - live.epoch) / (1000 / TICK_HZ))));
}
canvas.addEventListener('pointerdown', (ev) => {
if (!live || !sink) return;
// canvas pixel → world coordinates (account for CSS scaling)
const rect = canvas.getBoundingClientRect();
const wx = WORLD.xMin + ((ev.clientX - rect.left) / rect.width) * (WORLD.xMax - WORLD.xMin);
const wy = WORLD.yMax - ((ev.clientY - rect.top) / rect.height) * (WORLD.yMax - WORLD.yMin);
// nearest particle
const p = sink.particles();
const ids = sink.particle_ids();
let best = -1, bestD = Infinity;
for (let i = 0; i * 5 < p.length; i++) {
const dx = p[i * 5] - wx, dy = p[i * 5 + 1] - wy;
const d = dx * dx + dy * dy;
if (d < bestD) { bestD = d; best = i; }
}
if (best < 0 || bestD > 9) return; // ignore clicks far from the structure
const id = ids.slice(best * 16, best * 16 + 16);
// shove the particle away from the click point
const px = p[best * 5], py = p[best * 5 + 1];
let dx = px - wx, dy = py - wy;
const len = Math.hypot(dx, dy) || 1;
// Demolition is precision work: force falls off with distance. A
// point-blank click out-runs the member yield (PBD projection resets
// velocity each tick, so only the instantaneous impulse matters);
// a distant click just makes the structure sway.
const mag = Math.min(34, SHOVE / Math.max(len, 0.5));
dx = (dx / len) * mag; dy = (dy / len) * mag;
const effect = liveTargetTick() + BigInt(OP_LEAD_TICKS);
const opBytes = live.actor.sign_truss_impulse(effect, id, dx, dy, 0);
const hexOp = toHex(opBytes);
ingestHex(hexOp, live.pub);
live.send({ t: 'op', bytes: hexOp, from: live.pub });
});
// ---------------------------------------------------------------------------
// UI wiring + frame loop
// ---------------------------------------------------------------------------
ui.vector.addEventListener('change', () => load(ui.vector.value));
ui.restart.addEventListener('click', () => load(ui.vector.value));
ui.pause.addEventListener('click', () => {
playing = !playing;
ui.pause.textContent = playing ? '⏸ pause' : '▶ play';
});
function download(name, bytes, mime) {
const url = URL.createObjectURL(new Blob([bytes], { type: mime }));
const a = document.createElement('a');
a.href = url; a.download = name; a.click();
setTimeout(() => URL.revokeObjectURL(url), 2000);
}
// Seal the session as it stands into a wai.world.replay + its §8
// receipt, prove the replay re-verifies in a throwaway sink, and
// download both. The whole point: a live, shared, edited world
// becomes a portable artifact anyone can re-derive and check.
// Per-world receipt chain head, persisted so successive seals of the
// same world form one append-only signed timeline (parent_receipt_hash).
const CHAIN_KEY = (worldHash) => `wai-chain-${worldHash}`;
function chainHead(worldHash) {
try { return JSON.parse(localStorage.getItem(CHAIN_KEY(worldHash))) || null; } catch { return null; }
}
// Honest energy figure: work_steps is the EXACT signed quantity; joules
// is an ESTIMATE, wall-clock compute times a declared power coefficient
// (a page reads no energy counter). The receipt seals it with the class
// Estimator and the page shows it as an estimate, so it is never
// mistaken for a measurement.
const WATTS = 4.0; // declared coefficient for the estimate
let computeMs = 0; // accumulated wall-clock in the deterministic step loop
ui.seal.addEventListener('click', () => {
if (!sink) return;
try {
const seed = crypto.getRandomValues(new Uint8Array(32));
const signerId = live ? `did:key:wai-${live.pub.slice(0, 16)}` : 'did:example:wai-world';
const worldHash = sink.world_hash_hex();
const head = chainHead(worldHash);
const groupId = head ? BigInt(head.group_id + 1) : 0n;
const parentHex = head ? head.receipt_hash : '';
const joulesMicro = BigInt(Math.round((computeMs / 1000) * WATTS * 1e6));
const sealed = sink.seal_session(seed, signerId, joulesMicro, groupId, parentHex);
const tag = `${ui.vector.value}-t${sealed.final_tick()}-g${groupId}`;
// Proof: re-open the freshly sealed replay in a fresh sink and verify.
const replay = sealed.replay_bytes();
const reverified = new WorldSink(replay).verify_seal();
download(`${tag}.wwld`, replay, 'application/octet-stream');
download(`${tag}.receipt.json`, sealed.receipt_json(), 'application/json');
// Advance the world's chain head.
localStorage.setItem(CHAIN_KEY(worldHash),
JSON.stringify({ receipt_hash: sealed.receipt_hash(), group_id: Number(groupId) }));
const mJ = (Number(joulesMicro) / 1000).toFixed(1);
ui.receipt.textContent =
`chain #${groupId}${head ? ' ↳linked' : ''} · root ${sealed.root_hash().slice(0, 10)}… · `
+ `${sealed.work_steps()} steps · ≈${mJ} mJ (Estimator: wall time × ${WATTS} W, not measured) · ${reverified ? '✓ re-verified' : '✗ FAILED'}`;
} catch (e) {
ui.receipt.textContent = '✗ ' + (e.message || e);
}
});
await load(ui.vector.value);
function stepReplay() {
const t = sink.current_tick();
const finalTick = sink.final_tick();
if (t < finalTick) {
{ const _t = performance.now(); sink.advance_to(t + 1n); computeMs += performance.now() - _t; }
} else {
const ok = sink.verify_seal();
ui.badge.className = ok ? 'ok' : 'bad';
ui.badge.textContent = ok
? '✓ replay verified — state hash matches the publisher seal'
: '✗ HASH MISMATCH — nonconformant replay';
done = true;
if (ok) setTimeout(() => { if (done) load(ui.vector.value); }, 3000);
}
}
function stepLive() {
const target = liveTargetTick();
let t = sink.current_tick();
// Land exactly on hash-exchange boundaries as we pass them.
while (t < target) {
const nextBoundary = ((t / BigInt(HASH_EVERY)) + 1n) * BigInt(HASH_EVERY);
const stop = nextBoundary < target ? nextBoundary : target;
{ const _t = performance.now(); sink.advance_to(stop); computeMs += performance.now() - _t; }
t = stop;
if (t % BigInt(HASH_EVERY) === 0n) {
const h = sink.state_hash_hex();
live.hashes.set(t.toString(), h);
if (live.hashes.size > 12) live.hashes.delete(live.hashes.keys().next().value);
live.send({ t: 'hash', tick: t.toString(), hash: h, from: live.pub });
}
}
ui.actors.textContent = live.actors.size.toString();
ui.sync.textContent =
live.actors.size < 2 ? 'waiting for a second tab'
: live.peerOk === null ? 'comparing…'
: live.peerOk === false ? '✗ diverged'
: `✓ hashes agree @ tick ${live.peerOk.tick}`;
}
function frame() {
if (sink && playing && !done) {
// live → step the multiplayer clock; sealed → replay+verify;
// static scene (final_tick 0, no live) → nothing to advance.
if (live) stepLive();
else if (sink.final_tick() > 0n) stepReplay();
ui.tick.textContent = sink.current_tick().toString();
ui.nodes.textContent = sink.node_count().toString();
ui.hash.textContent = sink.state_hash_hex().slice(0, 16) + '…';
}
const enc = device.createCommandEncoder();
const view = ctx.getCurrentTexture().createView();
const clear = { r: 0.016, g: 0.024, b: 0.04, a: 1 };
if (renderMode === 'mesh' || renderMode === 'splat') {
// 3D path: the verified asset, rendered as a solid PBR cube or a
// Gaussian-splat cloud — presentation only, state hash untouched.
const cam = camera(performance.now() / 2500);
const eye = cam.eye;
const fy = canvas.height / (2 * Math.tan(FOVY / 2)); // focal in px
const fx = fy; // square pixels; perspective uses aspect, focals match in px
const camData = new Float32Array(64); // 256 bytes
camData.set(cam.vp, 0);
camData.set(cam.view, 16);
camData.set(cam.proj, 32);
camData[48] = eye[0]; camData[49] = eye[1]; camData[50] = eye[2];
camData[52] = canvas.width; camData[53] = canvas.height; camData[54] = fx; camData[55] = fy;
device.queue.writeBuffer(cam3d, 0, camData);
const pass = enc.beginRenderPass({
colorAttachments: [{ view, clearValue: clear, loadOp: 'clear', storeOp: 'store' }],
depthStencilAttachment: renderMode === 'mesh' ? {
view: depthTex.createView(), depthClearValue: 1.0,
depthLoadOp: 'clear', depthStoreOp: 'store',
} : undefined,
});
if (renderMode === 'mesh' && assetMesh && meshIdx.buf) {
pass.setPipeline(meshPipeline);
pass.setBindGroup(0, mesh3dBind);
pass.setVertexBuffer(0, meshVerts.buf);
pass.setIndexBuffer(meshIdx.buf, 'uint16');
pass.drawIndexed(assetMesh.triIndices.length);
} else if (renderMode === 'splat' && splatCloud) {
// Back-to-front sort by view-space depth each frame (painter's
// order for correct alpha-blended Gaussians).
sortSplats(eye);
pass.setPipeline(splatPipeline);
pass.setBindGroup(0, splat3dBind);
pass.setVertexBuffer(0, splatBuf.buf);
pass.draw(6, splatCloud.count);
}
pass.end();
ui.tick.textContent = sink ? sink.current_tick().toString() : '0';
device.queue.submit([enc.finish()]);
requestAnimationFrame(frame);
return;
}
// 2D sim path (playground / replay vectors).
const p = sink ? sink.particles() : new Float32Array(0);
const m = sink ? sink.members() : new Float32Array(0);
upload(circles, p);
upload(lines, m);
const pass = enc.beginRenderPass({
colorAttachments: [{ view, clearValue: clear, loadOp: 'clear', storeOp: 'store' }],
});
pass.setPipeline(groundPipeline);
pass.setBindGroup(0, groundBind);
pass.draw(2);
if (m.length > 0) {
pass.setPipeline(linePipeline);
pass.setBindGroup(0, lineBind);
pass.setVertexBuffer(0, lines.buf);
pass.draw(m.length / 3);
}
if (p.length > 0) {
pass.setPipeline(circPipeline);
pass.setBindGroup(0, circBind);
pass.setVertexBuffer(0, circles.buf);
pass.draw(6, p.length / 5);
}
pass.end();
device.queue.submit([enc.finish()]);
requestAnimationFrame(frame);
}
// Re-order the splat instance buffer back-to-front from the camera so
// alpha blending composites correctly (the painter's algorithm — the
// CPU half of a Gaussian-splat renderer; the GPU half is the shader).
let splatOrder = null;
function sortSplats(eye) {
const c = splatCloud.centers, n = splatCloud.count;
if (!splatOrder || splatOrder.length !== n) splatOrder = Array.from({ length: n }, (_, i) => i);
const d = new Float32Array(n);
for (let i = 0; i < n; i++) {
const dx = c[i * 3] - eye[0], dy = c[i * 3 + 1] - eye[1], dz = c[i * 3 + 2] - eye[2];
d[i] = dx * dx + dy * dy + dz * dz;
}
splatOrder.sort((a, b) => d[b] - d[a]); // far → near
const src = splatCloud.instances, F = SPLAT_FLOATS, out = new Float32Array(n * F);
for (let i = 0; i < n; i++) out.set(src.subarray(splatOrder[i] * F, splatOrder[i] * F + F), i * F);
upload(splatBuf, out);
}
requestAnimationFrame(frame);