// 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', '

WebGPU unavailable — this sink declares capability by the core dispatch rule: fall back to the recorded rendition.

'); 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 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(-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 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(-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 from measured wall-clock compute at a declared power // coefficient (a browser can't read RAPL — native sinks supply real // joules via the sink-metering path). Labeled "est" so it's 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 est@${WATTS}W · ${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);