From 48a8509dcfdd3e4ae6c3379d5632dbd44aaa1a25 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sat, 10 Oct 2026 14:42:30 -0700 Subject: [PATCH] KidLisp 3D: faces sorted once a frame across placements, near and guard-band clipping, fixed-arity frame writes, the corridor's loops use now --- kidlisp/examples/meshes/corridor.lisp | 14 +- kidlisp/tools/native-tv-script.mjs | 37 ++-- kidlisp/tools/native/entry.mjs | 2 +- .../lib/disk-worker-manifest.json | 8 +- ...0cef1.mjs => disk.worker.5bf59277e25b.mjs} | 182 +++++++++++++++--- .../lib/gpu-frame-renderer.mjs | 7 +- .../aesthetic.computer/lib/gpu-frame.mjs | 19 +- .../aesthetic.computer/lib/kidlisp-mesh.mjs | 66 +++++-- xbox/tools/kidlisp-bench.mjs | 19 +- 9 files changed, 260 insertions(+), 94 deletions(-) rename system/public/aesthetic.computer/lib/{disk.worker.e3c760c0cef1.mjs => disk.worker.5bf59277e25b.mjs} (99%) diff --git a/kidlisp/examples/meshes/corridor.lisp b/kidlisp/examples/meshes/corridor.lisp index cf5c6da98e..113738cadb 100644 --- a/kidlisp/examples/meshes/corridor.lisp +++ b/kidlisp/examples/meshes/corridor.lisp @@ -22,21 +22,23 @@ (camera camx 60 camz yaw 0 70 NEAR_PLANE) (place floor 0 -150 2000) (place ceiling 0 150 2000) -; two long walls of bricks, staggered rows +; two long walls of bricks, staggered rows. Inside a loop a value is set +; with now: def defines a name once. +(def wx 0) (def by 0) (def stag 0) (def bz 0) (def cz 0) (repeat 2 side - (def wx (- (* side 1200) 600)) + (now wx (- (* side 1200) 600)) (place mortar wx 0 2000 1.5708) (repeat 11 row - (def by (- (* row 28) 140)) - (def stag (* (% row 2) 32)) + (now by (- (* row 28) 140)) + (now stag (* (% row 2) 32)) (repeat 60 col - (def bz (+ (* col 66) stag -40)) + (now bz (+ (* col 66) stag -40)) (place brick wx by bz 1.5708)))) (repeat 8 pi (place pillar -520 0 (+ (* pi 520) 300)) (place pillar 520 0 (+ (* pi 520) 300))) (repeat 6 ci - (def cz (+ (* ci 650) 500)) + (now cz (+ (* ci 650) 500)) (place crate (- (* (% ci 3) 200) 200) -120 cz (* ci 0.4)) (place guard (+ (* (% ci 2) 300) -150) -105 (+ cz 200) (* tt 0.8))) (ink 255 255 255 120) diff --git a/kidlisp/tools/native-tv-script.mjs b/kidlisp/tools/native-tv-script.mjs index cb4b890185..6a07b4e137 100644 --- a/kidlisp/tools/native-tv-script.mjs +++ b/kidlisp/tools/native-tv-script.mjs @@ -103,8 +103,10 @@ const visit = { // host's retained meshes where it has them (the Xbox: meshUpload/meshDraw // with the 27-float camera) and through the projector otherwise, far to near, // as triangle3d with depth when the host has it, else as plain triangles. -const { sceneCamera, placeMesh, projectMesh } = KidLispNative; -let camera27 = null; const handles = new Map(); let meshFaces = 0; +const { sceneCamera, placeMesh, projectMesh, FaceList } = KidLispNative; +const faceList = new FaceList(); +const emitFace = (x1, y1, x2, y2, x3, y3, depth, r, g, b) => { if (host3d) triangle3d(C(x1), C(y1), depth, C(x2), C(y2), depth, C(x3), C(y3), depth, r, g, b); else triangle(C(x1), C(y1), C(x2), C(y2), C(x3), C(y3), r, g, b); }; +let camera27 = null; const handles = new Map(), missesById = new Map(); let meshFaces = 0, uploadsRefused = 0; const hostMeshes = typeof meshUpload === "function" && typeof meshDraw === "function"; const host3d = typeof triangle3d === "function"; visit.camera = (x, y, z, yaw, pitch, fov, near) => { camera27 = sceneCamera(x * S, y * S, z * S, yaw, pitch, fov, HW, HH, camera27 || new Float32Array(27), (near || 1) * S); }; @@ -114,24 +116,35 @@ visit.place = (id, x, y, z, yaw, pitch, roll, scale, a) => { // meshDraw has no pose of its own: a mesh is uploaded in world space, so // each distinct pose of a mesh is uploaded once and kept (static scenes // cost one upload; a moving mesh re-uploads when its pose changes). - const key = id + ":" + (x * S) + "," + (y * S) + "," + (z * S) + "," + yaw + "," + pitch + "," + roll + "," + scale; - let handle = handles.get(key); - if (handle === undefined) { + // keyed by a numeric hash of the pose (no string per placement), verified against the stored pose + let hsh = Math.imul(id, 2654435761); + hsh = Math.imul(hsh ^ ((x * 1000) | 0), 2246822519); hsh = Math.imul(hsh ^ ((y * 1000) | 0), 3266489917); hsh = Math.imul(hsh ^ ((z * 1000) | 0), 668265263); + hsh = Math.imul(hsh ^ ((yaw * 100000) | 0), 2246822519); hsh = Math.imul(hsh ^ ((pitch * 100000) | 0), 3266489917); hsh = Math.imul(hsh ^ ((roll * 100000) | 0), 668265263); hsh = (hsh ^ ((scale * 1000) | 0)) >>> 0; + let entry = handles.get(hsh); + if (entry !== undefined && (entry.id !== id || entry.x !== x || entry.y !== y || entry.z !== z || entry.yaw !== yaw || entry.pitch !== pitch || entry.roll !== roll || entry.scale !== scale)) { if (typeof meshFree === "function") meshFree(entry.handle); handles.delete(hsh); entry = undefined; } + if (entry === undefined) { + // A mesh whose pose keeps changing is not worth an upload a frame: it + // goes through the projector instead (the host's store holds 4096). + const misses = (missesById.get(id) || 0) + 1; missesById.set(id, misses); + if (misses > 8 && frames > 2) { + meshFaces += projectMesh(camera27, placeMesh(mesh, x * S, y * S, z * S, yaw, pitch, roll, scale * S), faceList, a); + return; + } const world = placeMesh(mesh, x * S, y * S, z * S, yaw, pitch, roll, scale * S); - handle = meshUpload(world.verts, world.faces); handles.set(key, handle); - if (handles.size > 2048) { for (const [k, h] of handles) { if (typeof meshFree === "function") meshFree(h); handles.delete(k); if (handles.size <= 1024) break; } } + entry = { id, x, y, z, yaw, pitch, roll, scale, handle: meshUpload(world.verts, world.faces) }; handles.set(hsh, entry); + if (entry.handle < 0) uploadsRefused++; + if (handles.size > 3000) { for (const [k, e] of handles) { if (typeof meshFree === "function") meshFree(e.handle); handles.delete(k); if (handles.size <= 2000) break; } } } - if (handle >= 0) meshFaces += meshDraw(handle, camera27, 1, 1, 1, 1, a / 255) | 0; + if (entry.handle >= 0) meshFaces += meshDraw(entry.handle, camera27, 1, 1, 1, 1, a / 255) | 0; return; } - const world = placeMesh(mesh, x * S, y * S, z * S, yaw, pitch, roll, scale * S); - meshFaces += projectMesh(camera27, world, (x1, y1, x2, y2, x3, y3, depth, r, g, b) => { if (host3d) triangle3d(C(x1), C(y1), depth, C(x2), C(y2), depth, C(x3), C(y3), depth, r, g, b); else triangle(C(x1), C(y1), C(x2), C(y2), C(x3), C(y3), r, g, b); }, a); + meshFaces += projectMesh(camera27, placeMesh(mesh, x * S, y * S, z * S, yaw, pitch, roll, scale * S), faceList, a); }; const COUNTS = { clear: 0, line: 0, box: 0, oval: 0, tri: 0, shape: 0, camera: 0, place: 0 }; const counting = { clear: () => COUNTS.clear++, line: () => COUNTS.line++, box: () => COUNTS.box++, oval: () => COUNTS.oval++, tri: () => COUNTS.tri++, shape: () => COUNTS.shape++, camera: () => COUNTS.camera++, place: () => COUNTS.place++ }; function drawFrame(buffer) { lastOps = buffer.length; for (const k in COUNTS) COUNTS[k] = 0; meshFaces = 0; readFrame(buffer, counting); lastCounts = JSON.stringify(COUNTS); - S = HW / W; const d0 = Date.now(); if (DRAW) readFrame(buffer, visit); lastDrawMs = Date.now() - d0; + S = HW / W; const d0 = Date.now(); faceList.reset(); if (DRAW) { readFrame(buffer, visit); faceList.flush(emitFace); } lastDrawMs = Date.now() - d0; } function bench() { const t0 = Date.now(); let acc = 0; for (let i = 0; i < 10000000; i++) acc += Math.sin(i) * 0.5; const loopMs = Date.now() - t0; @@ -157,7 +170,7 @@ function paint() { fpsCount++; if (Date.now() - fpsAt >= 1000) { fps = fpsCount; fpsCount = 0; fpsAt = Date.now(); } const ms = Date.now() - t0; if (DENSITY_AUTO) { if (ms > BUDGET_MS) density = Math.max(0.25, density * Math.max(0.7, Math.sqrt(BUDGET_MS / ms))); else if (ms < BUDGET_MS * 0.7) density = Math.min(1, density * 1.04); } - if (frames % 10 === 1) say("KIDLISP", JSON.stringify({ piece: ORDER[current], frame: frames, ms, density: Math.round(density * 100) / 100, drawMs: lastDrawMs, ops: lastOps, counts: lastCounts, meshFaces, w: W, h: H, err: lastErr })); + if (frames % 10 === 1) say("KIDLISP", JSON.stringify({ piece: ORDER[current], frame: frames, ms, density: Math.round(density * 100) / 100, drawMs: lastDrawMs, ops: lastOps, counts: lastCounts, meshFaces, uploadsRefused, handles: handles.size, w: W, h: H, err: lastErr })); box(6, 6, Math.min(HW - 12, fps * 3), 4, 255, 255, 255); box(6, 12, Math.min(HW - 12, ms), 4, 255, 120, 60); // fps and ms, as bars, in host pixels } function act() {} diff --git a/kidlisp/tools/native/entry.mjs b/kidlisp/tools/native/entry.mjs index 304e7fee43..8aaa9811c6 100644 --- a/kidlisp/tools/native/entry.mjs +++ b/kidlisp/tools/native/entry.mjs @@ -6,4 +6,4 @@ export { KidLisp } from '../../../system/public/aesthetic.computer/lib/kidlisp.m export { compileProgram } from '../../../system/public/aesthetic.computer/lib/kidlisp-compile.mjs'; export { programHelpers } from '../../../system/public/aesthetic.computer/lib/kidlisp-emit.mjs'; export { GpuFrame, readFrame } from '../../../system/public/aesthetic.computer/lib/gpu-frame.mjs'; -export { sceneCamera, placeMesh, projectMesh, buildMesh } from '../../../system/public/aesthetic.computer/lib/kidlisp-mesh.mjs'; +export { sceneCamera, placeMesh, projectMesh, buildMesh, FaceList } from '../../../system/public/aesthetic.computer/lib/kidlisp-mesh.mjs'; diff --git a/system/public/aesthetic.computer/lib/disk-worker-manifest.json b/system/public/aesthetic.computer/lib/disk-worker-manifest.json index ded182d264..c5e83b4bfa 100644 --- a/system/public/aesthetic.computer/lib/disk-worker-manifest.json +++ b/system/public/aesthetic.computer/lib/disk-worker-manifest.json @@ -1,8 +1,8 @@ { - "filename": "disk.worker.e3c760c0cef1.mjs", - "sha256": "e3c760c0cef1632dadfe4b776c8297b3ab4f57a6417bc7ded729ed735dfd3fc4", - "bytes": 1798037, - "sourceSha256": "45a89673abef879aad030b4ec7793c17404cd87a883ef14fc5bfa8f73b4a2cb8", + "filename": "disk.worker.5bf59277e25b.mjs", + "sha256": "5bf59277e25b6b642baaa7cfc4d7ea3e2cbac596ac05a24b0a8e17bbc31a53f6", + "bytes": 1800534, + "sourceSha256": "8caabacbb02473531ddc9148e1d5e92a51f7c0c05a0031e4923ec0cfd56857e8", "sources": [ "public/aesthetic.computer/dep/@akamfoad/qr/qr.mjs", "public/aesthetic.computer/dep/gl-matrix/common.mjs", diff --git a/system/public/aesthetic.computer/lib/disk.worker.e3c760c0cef1.mjs b/system/public/aesthetic.computer/lib/disk.worker.5bf59277e25b.mjs similarity index 99% rename from system/public/aesthetic.computer/lib/disk.worker.e3c760c0cef1.mjs rename to system/public/aesthetic.computer/lib/disk.worker.5bf59277e25b.mjs index 66a66c8eb8..3b330c2f53 100644 --- a/system/public/aesthetic.computer/lib/disk.worker.e3c760c0cef1.mjs +++ b/system/public/aesthetic.computer/lib/disk.worker.5bf59277e25b.mjs @@ -16842,7 +16842,7 @@ function sceneCamera(x, y, z, yaw, pitch, fov, width2, height2, out = new Float3 const cy = Math.cos(yaw), sy = Math.sin(yaw), cp = Math.cos(pitch), sp = Math.sin(pitch); const fx = sy * cp, fy = -sp, fz = cy * cp; const rx = cy, ry = 0, rz = -sy; - const ux = ry * fz - rz * fy, uy = rz * fx - rx * fz, uz = rx * fy - ry * fx; + const ux = fy * rz - fz * ry, uy = fz * rx - fx * rz, uz = fx * ry - fy * rx; out[0] = x; out[1] = y; out[2] = z; @@ -16893,7 +16893,28 @@ function placeMesh(mesh, px, py, pz, yaw = 0, pitch = 0, roll = 0, scale7 = 1) { } return { verts, faces }; } -function projectMesh(m, world2, emit, alpha = 255) { +var FaceList = class { + constructor() { + this.faces = []; + } + reset() { + this.faces.length = 0; + } + // emit(x1, y1, x2, y2, x3, y3, depth, r, g, b, alpha), far to near + flush(emit) { + const faces = this.faces; + faces.sort((a2, b2) => b2.z - a2.z); + for (const face of faces) { + const s2 = face.screen; + for (let k = 1; k + 1 < s2.length; k++) emit(s2[0][0], s2[0][1], s2[k][0], s2[k][1], s2[k + 1][0], s2[k + 1][1], (s2[0][2] + s2[k][2] + s2[k + 1][2]) / 3, face.r, face.g, face.b, face.alpha); + } + const n2 = faces.length; + faces.length = 0; + return n2; + } +}; +function projectMesh(m, world2, emitOrList, alpha = 255) { + const list = emitOrList instanceof FaceList ? emitOrList : new FaceList(); const verts = world2.verts, faces = world2.faces, n2 = verts.length / 3; const view = new Float64Array(n2 * 3); for (let i2 = 0; i2 < n2; i2++) { @@ -16902,7 +16923,10 @@ function projectMesh(m, world2, emit, alpha = 255) { view[i2 * 3 + 1] = x * m[6] + y * m[7] + z * m[8]; view[i2 * 3 + 2] = x * m[9] + y * m[10] + z * m[11]; } - const near = m[17], out = []; + const near = m[17]; + const guard = Math.max(m[20] - m[18], m[21] - m[19]) * 4; + const gl = m[18] - guard, gt = m[19] - guard, gr = m[20] + guard, gb = m[21] + guard; + let count = 0; for (let f2 = 0; f2 < faces.length; f2 += 10) { const ids = [faces[f2], faces[f2 + 1], faces[f2 + 2], faces[f2 + 3]]; const nx = faces[f2 + 7], ny = faces[f2 + 8], nz = faces[f2 + 9]; @@ -16911,36 +16935,42 @@ function projectMesh(m, world2, emit, alpha = 255) { let poly2 = []; for (const id of ids) { const p = [view[id * 3], view[id * 3 + 1], view[id * 3 + 2]]; - if (!poly2.length || poly2[poly2.length - 1][0] !== p[0] || poly2[poly2.length - 1][1] !== p[1] || poly2[poly2.length - 1][2] !== p[2]) poly2.push(p); + const last = poly2[poly2.length - 1]; + if (!last || last[0] !== p[0] || last[1] !== p[1] || last[2] !== p[2]) poly2.push(p); } if (poly2.length < 3) continue; - poly2 = clipNear(poly2, near); + poly2 = clip(poly2, (p) => p[2] - near); if (poly2.length < 3) continue; const light = 0.72 + Math.max(0, -(nx * m[24] + ny * m[25] + nz * m[26])) * 0.28; const r2 = Math.round(faces[f2 + 4] * light), g = Math.round(faces[f2 + 5] * light), b2 = Math.round(faces[f2 + 6] * light); let depthSum = 0; - const screen2 = poly2.map((p) => { + let screen2 = poly2.map((p) => { const k = m[14] + (m[15] / p[2] - m[14]) * m[16]; depthSum += p[2]; return [m[12] + p[0] * k, m[13] - p[1] * k, Math.max(-1.499, Math.min(1.4, m[22] + p[2] * m[23]))]; }); - out.push({ z: depthSum / poly2.length, screen: screen2, r: r2, g, b: b2 }); - } - out.sort((a2, b2) => b2.z - a2.z); - for (const face of out) { - const s2 = face.screen; - for (let k = 1; k + 1 < s2.length; k++) emit(s2[0][0], s2[0][1], s2[k][0], s2[k][1], s2[k + 1][0], s2[k + 1][1], (s2[0][2] + s2[k][2] + s2[k + 1][2]) / 3, face.r, face.g, face.b, alpha); - } - return out.length; -} -function clipNear(poly2, near) { + screen2 = clip(screen2, (p) => p[0] - gl); + if (screen2.length < 3) continue; + screen2 = clip(screen2, (p) => gr - p[0]); + if (screen2.length < 3) continue; + screen2 = clip(screen2, (p) => p[1] - gt); + if (screen2.length < 3) continue; + screen2 = clip(screen2, (p) => gb - p[1]); + if (screen2.length < 3) continue; + list.faces.push({ z: depthSum / poly2.length, screen: screen2, r: r2, g, b: b2, alpha }); + count++; + } + if (emitOrList instanceof FaceList) return count; + return list.flush(emitOrList); +} +function clip(poly2, dist5) { const out = []; for (let i2 = 0; i2 < poly2.length; i2++) { - const a2 = poly2[i2], b2 = poly2[(i2 + 1) % poly2.length], da = a2[2] - near, db = b2[2] - near; + const a2 = poly2[i2], b2 = poly2[(i2 + 1) % poly2.length], da = dist5(a2), db = dist5(b2); if (da >= 0) out.push(a2); if (da >= 0 !== db >= 0) { const t2 = da / (da - db); - out.push([a2[0] + (b2[0] - a2[0]) * t2, a2[1] + (b2[1] - a2[1]) * t2, a2[2] + (b2[2] - a2[2]) * t2]); + out.push(a2.map((v2, k) => v2 + (b2[k] - v2) * t2)); } } return out; @@ -16966,10 +16996,36 @@ var GpuFrame = class { this.meshVersion++; } camera(x, y, z, yaw, pitch, fov, near = 1) { - this.push(OP.CAMERA, x, y, z, yaw, pitch, fov, near); + this.grow(8); + const o2 = this.buffer; + let i2 = this.length; + o2[i2++] = OP.CAMERA; + o2[i2++] = x; + o2[i2++] = y; + o2[i2++] = z; + o2[i2++] = yaw; + o2[i2++] = pitch; + o2[i2++] = fov; + o2[i2++] = near; + this.length = i2; + this.ops++; } place(mesh, x, y, z, yaw = 0, pitch = 0, roll = 0, scale7 = 1) { - this.push(OP.PLACE, mesh, x, y, z, yaw, pitch, roll, scale7, this.a); + this.grow(10); + const o2 = this.buffer; + let i2 = this.length; + o2[i2++] = OP.PLACE; + o2[i2++] = mesh; + o2[i2++] = x; + o2[i2++] = y; + o2[i2++] = z; + o2[i2++] = yaw; + o2[i2++] = pitch; + o2[i2++] = roll; + o2[i2++] = scale7; + o2[i2++] = this.a; + this.length = i2; + this.ops++; } reset() { this.length = 0; @@ -16988,28 +17044,92 @@ var GpuFrame = class { this.b = b2; this.a = a2; } - push(...values2) { - this.grow(values2.length); - for (let i2 = 0; i2 < values2.length; i2++) this.buffer[this.length++] = values2[i2]; - this.ops++; - } + // Fixed-arity writes (no rest arguments): an engine without a JIT allocates + // an array per spread, and a frame is thousands of these. clear(r2 = this.r, g = this.g, b2 = this.b, a2 = 255) { - this.push(OP.CLEAR, r2, g, b2, a2); + this.grow(5); + const o2 = this.buffer; + let i2 = this.length; + o2[i2++] = OP.CLEAR; + o2[i2++] = r2; + o2[i2++] = g; + o2[i2++] = b2; + o2[i2++] = a2; + this.length = i2; + this.ops++; } line(x1, y1, x2, y2, thickness = 1) { - this.push(OP.LINE, x1, y1, x2, y2, thickness, this.r, this.g, this.b, this.a); + this.grow(10); + const o2 = this.buffer; + let i2 = this.length; + o2[i2++] = OP.LINE; + o2[i2++] = x1; + o2[i2++] = y1; + o2[i2++] = x2; + o2[i2++] = y2; + o2[i2++] = thickness; + o2[i2++] = this.r; + o2[i2++] = this.g; + o2[i2++] = this.b; + o2[i2++] = this.a; + this.length = i2; + this.ops++; } box(x, y, w, h, fill = 1) { - this.push(OP.BOX, x, y, w, h, fill, this.r, this.g, this.b, this.a); + this.grow(10); + const o2 = this.buffer; + let i2 = this.length; + o2[i2++] = OP.BOX; + o2[i2++] = x; + o2[i2++] = y; + o2[i2++] = w; + o2[i2++] = h; + o2[i2++] = fill; + o2[i2++] = this.r; + o2[i2++] = this.g; + o2[i2++] = this.b; + o2[i2++] = this.a; + this.length = i2; + this.ops++; } oval(cx, cy, rx, ry, fill = 1) { - this.push(OP.OVAL, cx, cy, rx, ry, fill, this.r, this.g, this.b, this.a); + this.grow(10); + const o2 = this.buffer; + let i2 = this.length; + o2[i2++] = OP.OVAL; + o2[i2++] = cx; + o2[i2++] = cy; + o2[i2++] = rx; + o2[i2++] = ry; + o2[i2++] = fill; + o2[i2++] = this.r; + o2[i2++] = this.g; + o2[i2++] = this.b; + o2[i2++] = this.a; + this.length = i2; + this.ops++; } circle(cx, cy, r2, fill = 1) { - this.push(OP.OVAL, cx, cy, r2, r2, fill, this.r, this.g, this.b, this.a); + this.oval(cx, cy, r2, r2, fill); } tri(x1, y1, x2, y2, x3, y3, fill = 1) { - this.push(OP.TRI, x1, y1, x2, y2, x3, y3, fill, this.r, this.g, this.b, this.a); + this.grow(12); + const o2 = this.buffer; + let i2 = this.length; + o2[i2++] = OP.TRI; + o2[i2++] = x1; + o2[i2++] = y1; + o2[i2++] = x2; + o2[i2++] = y2; + o2[i2++] = x3; + o2[i2++] = y3; + o2[i2++] = fill; + o2[i2++] = this.r; + o2[i2++] = this.g; + o2[i2++] = this.b; + o2[i2++] = this.a; + this.length = i2; + this.ops++; } shape(points, fill = 1) { const n2 = points.length >> 1; diff --git a/system/public/aesthetic.computer/lib/gpu-frame-renderer.mjs b/system/public/aesthetic.computer/lib/gpu-frame-renderer.mjs index a9c1a972c7..9b1f56fe2b 100644 --- a/system/public/aesthetic.computer/lib/gpu-frame-renderer.mjs +++ b/system/public/aesthetic.computer/lib/gpu-frame-renderer.mjs @@ -5,7 +5,7 @@ // text and anything the GPU path does not draw still show. Runs in bios on // its own canvas, sized like the main canvas. import { readFrame } from "./gpu-frame.mjs"; -import { sceneCamera, placeMesh, projectMesh } from "./kidlisp-mesh.mjs"; +import { sceneCamera, placeMesh, projectMesh, FaceList } from "./kidlisp-mesh.mjs"; const SEGMENTS = 24; // an oval's polygon const FLOATS_PER_VERTEX = 6; // x y r g b a @@ -98,10 +98,11 @@ export async function createFrameRenderer(canvas) { // triangles, far to near, through the same vertex buffer. The camera is in // canvas pixels; meshes come from the frame's table. let camera27 = null, meshes = null, placed = 0; + const faceList = new FaceList(); // every placed face of the frame, sorted once at the end visit.camera = (x, y, z, yaw, pitch, fov, near) => { camera27 = sceneCamera(x, y, z, yaw, pitch, fov, canvas.width, canvas.height, camera27 || new Float32Array(27), near || 1); }; visit.place = (id, x, y, z, yaw, pitch, roll, scale, a) => { const mesh = meshes?.get(id); if (!mesh || !camera27) return; - placed += projectMesh(camera27, placeMesh(mesh, x, y, z, yaw, pitch, roll, scale), (x1, y1, x2, y2, x3, y3, depth, r, g, b, alpha) => triangle(x1, y1, x2, y2, x3, y3, r, g, b, alpha), a); + placed += projectMesh(camera27, placeMesh(mesh, x, y, z, yaw, pitch, roll, scale), faceList, a); }; return { @@ -113,7 +114,9 @@ export async function createFrameRenderer(canvas) { if (!configured.w) return; count = 0; clearColor = { r: 0, g: 0, b: 0, a: 1 }; placed = 0; if (meshTable) meshes = meshTable; + faceList.reset(); readFrame(frame, visit); + faceList.flush((x1, y1, x2, y2, x3, y3, depth, r, g, b, alpha) => triangle(x1, y1, x2, y2, x3, y3, r, g, b, alpha)); const bytes = count * FLOATS_PER_VERTEX * 4; if (bytes > vertexCapacity) { vertexBuffer?.destroy(); vertexCapacity = Math.max(bytes, 1 << 16); vertexBuffer = device.createBuffer({ size: vertexCapacity, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST }); } if (bytes) device.queue.writeBuffer(vertexBuffer, 0, vertices.buffer, 0, bytes); diff --git a/system/public/aesthetic.computer/lib/gpu-frame.mjs b/system/public/aesthetic.computer/lib/gpu-frame.mjs index 741e0a722f..52af995d05 100644 --- a/system/public/aesthetic.computer/lib/gpu-frame.mjs +++ b/system/public/aesthetic.computer/lib/gpu-frame.mjs @@ -29,8 +29,8 @@ export class GpuFrame { this.meshVersion = 0; } defineMesh(id, mesh) { this.meshes.set(id, mesh); this.meshVersion++; } - camera(x, y, z, yaw, pitch, fov, near = 1) { this.push(OP.CAMERA, x, y, z, yaw, pitch, fov, near); } - place(mesh, x, y, z, yaw = 0, pitch = 0, roll = 0, scale = 1) { this.push(OP.PLACE, mesh, x, y, z, yaw, pitch, roll, scale, this.a); } + camera(x, y, z, yaw, pitch, fov, near = 1) { this.grow(8); const o = this.buffer; let i = this.length; o[i++] = OP.CAMERA; o[i++] = x; o[i++] = y; o[i++] = z; o[i++] = yaw; o[i++] = pitch; o[i++] = fov; o[i++] = near; this.length = i; this.ops++; } + place(mesh, x, y, z, yaw = 0, pitch = 0, roll = 0, scale = 1) { this.grow(10); const o = this.buffer; let i = this.length; o[i++] = OP.PLACE; o[i++] = mesh; o[i++] = x; o[i++] = y; o[i++] = z; o[i++] = yaw; o[i++] = pitch; o[i++] = roll; o[i++] = scale; o[i++] = this.a; this.length = i; this.ops++; } reset() { this.length = 0; this.ops = 0; this.overlay = false; } grow(extra) { if (this.length + extra <= this.buffer.length) return; @@ -38,13 +38,14 @@ export class GpuFrame { next.set(this.buffer.subarray(0, this.length)); this.buffer = next; } ink(r, g, b, a = 255) { this.r = r; this.g = g; this.b = b; this.a = a; } - push(...values) { this.grow(values.length); for (let i = 0; i < values.length; i++) this.buffer[this.length++] = values[i]; this.ops++; } - clear(r = this.r, g = this.g, b = this.b, a = 255) { this.push(OP.CLEAR, r, g, b, a); } - line(x1, y1, x2, y2, thickness = 1) { this.push(OP.LINE, x1, y1, x2, y2, thickness, this.r, this.g, this.b, this.a); } - box(x, y, w, h, fill = 1) { this.push(OP.BOX, x, y, w, h, fill, this.r, this.g, this.b, this.a); } - oval(cx, cy, rx, ry, fill = 1) { this.push(OP.OVAL, cx, cy, rx, ry, fill, this.r, this.g, this.b, this.a); } - circle(cx, cy, r, fill = 1) { this.push(OP.OVAL, cx, cy, r, r, fill, this.r, this.g, this.b, this.a); } - tri(x1, y1, x2, y2, x3, y3, fill = 1) { this.push(OP.TRI, x1, y1, x2, y2, x3, y3, fill, this.r, this.g, this.b, this.a); } + // Fixed-arity writes (no rest arguments): an engine without a JIT allocates + // an array per spread, and a frame is thousands of these. + clear(r = this.r, g = this.g, b = this.b, a = 255) { this.grow(5); const o = this.buffer; let i = this.length; o[i++] = OP.CLEAR; o[i++] = r; o[i++] = g; o[i++] = b; o[i++] = a; this.length = i; this.ops++; } + line(x1, y1, x2, y2, thickness = 1) { this.grow(10); const o = this.buffer; let i = this.length; o[i++] = OP.LINE; o[i++] = x1; o[i++] = y1; o[i++] = x2; o[i++] = y2; o[i++] = thickness; o[i++] = this.r; o[i++] = this.g; o[i++] = this.b; o[i++] = this.a; this.length = i; this.ops++; } + box(x, y, w, h, fill = 1) { this.grow(10); const o = this.buffer; let i = this.length; o[i++] = OP.BOX; o[i++] = x; o[i++] = y; o[i++] = w; o[i++] = h; o[i++] = fill; o[i++] = this.r; o[i++] = this.g; o[i++] = this.b; o[i++] = this.a; this.length = i; this.ops++; } + oval(cx, cy, rx, ry, fill = 1) { this.grow(10); const o = this.buffer; let i = this.length; o[i++] = OP.OVAL; o[i++] = cx; o[i++] = cy; o[i++] = rx; o[i++] = ry; o[i++] = fill; o[i++] = this.r; o[i++] = this.g; o[i++] = this.b; o[i++] = this.a; this.length = i; this.ops++; } + circle(cx, cy, r, fill = 1) { this.oval(cx, cy, r, r, fill); } + tri(x1, y1, x2, y2, x3, y3, fill = 1) { this.grow(12); const o = this.buffer; let i = this.length; o[i++] = OP.TRI; o[i++] = x1; o[i++] = y1; o[i++] = x2; o[i++] = y2; o[i++] = x3; o[i++] = y3; o[i++] = fill; o[i++] = this.r; o[i++] = this.g; o[i++] = this.b; o[i++] = this.a; this.length = i; this.ops++; } shape(points, fill = 1) { const n = points.length >> 1; if (n < 2) return; this.grow(7 + n * 2); diff --git a/system/public/aesthetic.computer/lib/kidlisp-mesh.mjs b/system/public/aesthetic.computer/lib/kidlisp-mesh.mjs index 0fe7d8ba20..4dc2db09ee 100644 --- a/system/public/aesthetic.computer/lib/kidlisp-mesh.mjs +++ b/system/public/aesthetic.computer/lib/kidlisp-mesh.mjs @@ -69,7 +69,7 @@ export function sceneCamera(x, y, z, yaw, pitch, fov, width, height, out = new F // forward = R_y(yaw) R_x(pitch) (0,0,1); right = (cy, 0, -sy); up = right × forward const fx = sy * cp, fy = -sp, fz = cy * cp; const rx = cy, ry = 0, rz = -sy; - const ux = ry * fz - rz * fy, uy = rz * fx - rx * fz, uz = rx * fy - ry * fx; + const ux = fy * rz - fz * ry, uy = fz * rx - fx * rz, uz = fx * ry - fy * rx; // up = forward × right out[0] = x; out[1] = y; out[2] = z; out[3] = rx; out[4] = ry; out[5] = rz; out[6] = ux; out[7] = uy; out[8] = uz; out[9] = fx; out[10] = fy; out[11] = fz; out[12] = left + width / 2; out[13] = top + height / 2; @@ -107,10 +107,30 @@ export function placeMesh(mesh, px, py, pz, yaw = 0, pitch = 0, roll = 0, scale } // ---- projecting (the Xbox's SceneMesh, in JavaScript) ------------------------ -// Emits each visible face as screen-space triangles with depth and the lit -// colour: emit(x1, y1, x2, y2, x3, y3, depth, r, g, b). Faces come out -// far-to-near so a host without a depth buffer can paint in order. -export function projectMesh(m, world, emit, alpha = 255) { +// A frame's faces are collected across every placement, then sorted once, +// far to near, so a host without a depth buffer paints in the right order +// between objects as well as within them. Clipping: against the near plane +// in view space (Sutherland-Hodgman), then against a guard band around the +// viewport in screen space, so nothing projected from just in front of the +// eye reaches a host as a coordinate it refuses. Facing: a face whose +// normal points away from the eye is dropped (closed meshes only; an open +// surface should be built with both faces). +export class FaceList { + constructor() { this.faces = []; } + reset() { this.faces.length = 0; } + // emit(x1, y1, x2, y2, x3, y3, depth, r, g, b, alpha), far to near + flush(emit) { + const faces = this.faces; + faces.sort((a, b) => b.z - a.z); + for (const face of faces) { + const s = face.screen; + for (let k = 1; k + 1 < s.length; k++) emit(s[0][0], s[0][1], s[k][0], s[k][1], s[k + 1][0], s[k + 1][1], (s[0][2] + s[k][2] + s[k + 1][2]) / 3, face.r, face.g, face.b, face.alpha); + } + const n = faces.length; faces.length = 0; return n; + } +} +export function projectMesh(m, world, emitOrList, alpha = 255) { + const list = emitOrList instanceof FaceList ? emitOrList : new FaceList(); const verts = world.verts, faces = world.faces, n = verts.length / 3; const view = new Float64Array(n * 3); for (let i = 0; i < n; i++) { @@ -119,37 +139,41 @@ export function projectMesh(m, world, emit, alpha = 255) { view[i * 3 + 1] = x * m[6] + y * m[7] + z * m[8]; view[i * 3 + 2] = x * m[9] + y * m[10] + z * m[11]; } - const near = m[17], out = []; + const near = m[17]; + const guard = Math.max(m[20] - m[18], m[21] - m[19]) * 4; + const gl = m[18] - guard, gt = m[19] - guard, gr = m[20] + guard, gb = m[21] + guard; + let count = 0; for (let f = 0; f < faces.length; f += 10) { const ids = [faces[f], faces[f + 1], faces[f + 2], faces[f + 3]]; - // back faces: the lit normal points away when its dot with the view ray is positive const nx = faces[f + 7], ny = faces[f + 8], nz = faces[f + 9]; const i0 = ids[0] * 3, cx = verts[i0] - m[0], cy = verts[i0 + 1] - m[1], cz = verts[i0 + 2] - m[2]; - if (nx * cx + ny * cy + nz * cz > 0) continue; + if (nx * cx + ny * cy + nz * cz > 0) continue; // facing away let poly = []; - for (const id of ids) { const p = [view[id * 3], view[id * 3 + 1], view[id * 3 + 2]]; if (!poly.length || poly[poly.length - 1][0] !== p[0] || poly[poly.length - 1][1] !== p[1] || poly[poly.length - 1][2] !== p[2]) poly.push(p); } + for (const id of ids) { const p = [view[id * 3], view[id * 3 + 1], view[id * 3 + 2]]; const last = poly[poly.length - 1]; if (!last || last[0] !== p[0] || last[1] !== p[1] || last[2] !== p[2]) poly.push(p); } if (poly.length < 3) continue; - poly = clipNear(poly, near); + poly = clip(poly, (p) => p[2] - near); // near plane, view space if (poly.length < 3) continue; const light = 0.72 + Math.max(0, -(nx * m[24] + ny * m[25] + nz * m[26])) * 0.28; const r = Math.round(faces[f + 4] * light), g = Math.round(faces[f + 5] * light), b = Math.round(faces[f + 6] * light); let depthSum = 0; - const screen = poly.map((p) => { const k = m[14] + (m[15] / p[2] - m[14]) * m[16]; depthSum += p[2]; return [m[12] + p[0] * k, m[13] - p[1] * k, Math.max(-1.499, Math.min(1.4, m[22] + p[2] * m[23]))]; }); - out.push({ z: depthSum / poly.length, screen, r, g, b }); - } - out.sort((a, b) => b.z - a.z); - for (const face of out) { - const s = face.screen; - for (let k = 1; k + 1 < s.length; k++) emit(s[0][0], s[0][1], s[k][0], s[k][1], s[k + 1][0], s[k + 1][1], (s[0][2] + s[k][2] + s[k + 1][2]) / 3, face.r, face.g, face.b, alpha); + let screen = poly.map((p) => { const k = m[14] + (m[15] / p[2] - m[14]) * m[16]; depthSum += p[2]; return [m[12] + p[0] * k, m[13] - p[1] * k, Math.max(-1.499, Math.min(1.4, m[22] + p[2] * m[23]))]; }); + screen = clip(screen, (p) => p[0] - gl); if (screen.length < 3) continue; // the guard band, screen space + screen = clip(screen, (p) => gr - p[0]); if (screen.length < 3) continue; + screen = clip(screen, (p) => p[1] - gt); if (screen.length < 3) continue; + screen = clip(screen, (p) => gb - p[1]); if (screen.length < 3) continue; + list.faces.push({ z: depthSum / poly.length, screen, r, g, b, alpha }); + count++; } - return out.length; + if (emitOrList instanceof FaceList) return count; + return list.flush(emitOrList); } -function clipNear(poly, near) { +// Sutherland-Hodgman against one half-space: dist(p) >= 0 is inside. +function clip(poly, dist) { const out = []; for (let i = 0; i < poly.length; i++) { - const a = poly[i], b = poly[(i + 1) % poly.length], da = a[2] - near, db = b[2] - near; + const a = poly[i], b = poly[(i + 1) % poly.length], da = dist(a), db = dist(b); if (da >= 0) out.push(a); - if ((da >= 0) !== (db >= 0)) { const t = da / (da - db); out.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]); } + if ((da >= 0) !== (db >= 0)) { const t = da / (da - db); out.push(a.map((v, k) => v + (b[k] - v) * t)); } } return out; } diff --git a/xbox/tools/kidlisp-bench.mjs b/xbox/tools/kidlisp-bench.mjs index d320b9a16f..359e5b5a60 100755 --- a/xbox/tools/kidlisp-bench.mjs +++ b/xbox/tools/kidlisp-bench.mjs @@ -22,15 +22,18 @@ const run = (args, opts = {}) => execFileSync('node', args, { cwd: root, encodin const perPiece = Math.floor((seconds * 1000) / Math.max(1, pieces.length || 3)); const built = JSON.parse(run(['kidlisp/tools/native-tv-script.mjs', 'draw', String(perPiece), 'kidlisp/build/kidlisp-tv.js', density, ...pieces])); console.log(`built ${built.out} (${built.kb} KB) for ${built.pieces.join(', ')}, ${perPiece} ms each, density ${density}`); -// publish, then wait for the host to log the new live generation (hot-deploy's -// own byte check does not match a script with multibyte characters) -const lastGeneration = (text) => Number((text.match(/AC_NATIVE_LIVE_READY bytes=\d+ generation=(\d+)(?![\s\S]*AC_NATIVE_LIVE_READY)/) || [])[1] || 0); -const before = lastGeneration(run(['xbox/tools/live.mjs', 'logs', '400'])); +// publish, then restart the app so the host starts clean (its mesh store and +// the rest of its state outlive a hot reload), and wait for the live script +// to be logged after the new boot. +const boots = (text) => text.split('AC_NATIVE_BIOS_READY').length - 1; +const liveAfterBoot = (text) => { const at = text.lastIndexOf('AC_NATIVE_BIOS_READY'); return at >= 0 && text.indexOf('AC_NATIVE_LIVE_READY', at) >= 0; }; +const bootsBefore = boots(run(['xbox/tools/live.mjs', 'logs', '2000'])); run(['xbox/tools/live.mjs', 'publish', 'kidlisp/build/kidlisp-tv.js']); -let generation = before; -for (let attempt = 0; attempt < 60 && generation === before; attempt++) { await sleep(1000); generation = lastGeneration(run(['xbox/tools/live.mjs', 'logs', '400'])); } -if (generation === before) throw new Error('the Xbox did not log a new live generation; is Native BIOS running?'); -console.log(`live generation ${generation}`); +try { run(['xbox/tools/live.mjs', 'restart']); } catch { try { run(['xbox/tools/live.mjs', 'launch']); } catch (error) { console.log('restart failed: ' + (error.message || error)); } } +let ready = false, generation = 0; +for (let attempt = 0; attempt < 90 && !ready; attempt++) { await sleep(1000); const log = run(['xbox/tools/live.mjs', 'logs', '2000']); ready = boots(log) > bootsBefore && liveAfterBoot(log); if (ready) generation = Number((log.slice(log.lastIndexOf('AC_NATIVE_LIVE_READY')).match(/generation=(\d+)/) || [])[1] || 0); } +if (!ready) throw new Error('the Xbox did not boot the live script after the restart'); +console.log(`restarted; live generation ${generation}`); mkdirSync(resolve(root, 'kidlisp/build/xbox-bench'), { recursive: true }); const shots = new Set(); -- 2.51.2