// 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);