open-standards/wai/wai-web/demo/world.js
Transaction Science 46c5688b86 wai: the WAI open standard — full cargo-class surface + provenance
Commit the WAI standard source: a content + capability-dispatch standard
whose thesis is "carries whatever it can reconstruct, verifies whatever
it carries." Source only — large vendored/model assets and build output
are gitignored.

Reconstruct classes (each: spec + conformance corpus + in-tab browser
sink that re-derives the corpus hash, one wai.det.fixed64 fixed-point
floor, one *-equivalence apiece):
  - interactive worlds  (replay-equivalence)   + 3 sim registrants,
    multiplayer over a relay, session receipts, glTF + real SPZ assets
  - spatial audio        (mixdown-equivalence)
  - haptics              (sample-equivalence)
  - state-feeds          (replay-equivalence, non-physics sim)
  - avatars / digital humans (pose-equivalence) + telepresence over the relay
  - volumetric / 4D splats   (frame-state-equivalence) + WebGPU EWA render,
    real-SPZ-derived clip

Verify half (lever 4):
  - asset provenance: a signed, energy-accounted lineage for opaque
    captured content WAI does not reconstruct; tamper-evident, in-tab.

Cross-cutting: JWP receipt profiles per class; a native energy meter
(IOReport via macmon, no sudo) emitting receipts with REAL measured
joules; the learned-image/neural codec family and JPEG AI entropy port
from earlier work.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-13 22:28:51 -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 Niantic-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:web:wai.transaction.science';
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);