From 28e8ed44d3b924430e9a4e84c8da31a5e5520684 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sat, 10 Oct 2026 13:35:30 -0700 Subject: [PATCH] =?UTF-8?q?KidLisp=20on=20the=20console:=20the=20native=20?= =?UTF-8?q?live=20script,=20measured=20on=20the=20Xbox=20devkit=20and=20th?= =?UTF-8?q?e=20Mac=20shell;=20=C2=A710=20in=20the=20design?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + kidlisp/PIECE-IL.md | 49 ++++++++ .../whistlegraph-benchmark-2026-10-10.md | 15 +++ kidlisp/tools/native-tv-script.mjs | 108 ++++++++++++++++++ kidlisp/tools/native/entry.mjs | 6 + 5 files changed, 181 insertions(+) create mode 100644 kidlisp/tools/native-tv-script.mjs create mode 100644 kidlisp/tools/native/entry.mjs diff --git a/.gitignore b/.gitignore index 46f09fcfd3..12ef456022 100644 --- a/.gitignore +++ b/.gitignore @@ -618,3 +618,6 @@ xbox/builds/ios/ # Pulled Menu Band takes (menuband_take_pull); backed up to @jeffrey, not git pop/menuband/takes/ + +# Generated native live scripts (node kidlisp/tools/native-tv-script.mjs) +kidlisp/build/ diff --git a/kidlisp/PIECE-IL.md b/kidlisp/PIECE-IL.md index d6a97ce2bb..09ccb5ed42 100644 --- a/kidlisp/PIECE-IL.md +++ b/kidlisp/PIECE-IL.md @@ -270,3 +270,52 @@ What is left after this is evaluation and the runtime's own per-frame work. Kernels (§8) take the data-parallel arithmetic; the whole program to Wasm takes the rest, and a host that keeps kernel outputs on the GPU and draws from them never pays a readback at all. + + +## 10. The console: the same bundle in the native bios + +Measured 2026-10-10. `node kidlisp/tools/native-tv-script.mjs` bundles the +evaluator, the closure compiler and the frame builder (esbuild, one classic +script, global `KidLispNative`), embeds the pieces, and adds the +boot/sim/paint/act lifecycle the native bios shells call, with a shim that +walks each frame buffer (§9) into the host's `wipe`, `box`, `line` and +`triangle`. The same file runs in the Mac shell (JavaScriptCore, JIT) and +on the Xbox devkit through the live-piece lane the bios already has +(`node xbox/tools/live.mjs hot-deploy kidlisp/build/kidlisp-tv.js`, no +msix rebuild: the host reloads `LocalState/live-piece.js` and logs +`AC_NATIVE_LIVE_READY`). Nothing in the evaluator needed changing for +either engine. + +| per frame | Xbox Series X, QuickJS-ng, no JIT | Mac shell, JavaScriptCore, JIT | +|---|---|---| +| shooter (7,100 triangles) | 1,200 ms | 15 ms | +| Fía (2,250 lines, ovals) | 1,010 ms | 28 ms | +| starfield (4,000 boxes, one kernel) | 645 ms | 10 ms | +| walking the frame buffer into host calls | 0 to 23 ms | 2 to 14 ms | +| 10 million `sin`-multiply-adds | 1,909 ms | 308 ms (local QuickJS) | + +The drawing contract holds: ten thousand boxes cost the Xbox 3 ms. The +cost is the evaluation, and the reason is the engine, not the pieces: +the console's QuickJS is six times slower than QuickJS on an M-series +Mac and has no JIT, so the compiled closures that give 25 to 60 fps +under a JIT give one frame a second there. Oskiewar reaches 60 fps on +the same engine only because its JavaScript is written tight by hand. + +So on the console the piece has to be compiled below JavaScript. The +closure compiler already lowers a piece to typed arithmetic, slots and +pools (§7), and the kernel subset already emits Wasm and WGSL from one +plan (§8); the next emitter is C, from the same lowered forms, compiled +into the native bios by AppVeyor (`appveyor.yml` builds the msix on any +commit touching `xbox/native-bios/`). That is the "msix like oskiewar" +route: a KidLisp console package whose pieces are C, with the frame +buffer as the only drawing interface and the Device Portal install +(`xbox/tools/live.mjs install`) as the installer. The JavaScript bundle +stays the development path: hot-deploy a piece in seconds, read its +`KIDLISP` telemetry lines, then compile it when it is done. + +Open on the Xbox: the frame's CLEAR maps to the host `wipe`, which oskiewar +uses for its dark sky, yet the screen comes up white with the post shader's +vignette; and the shooter's 7,100 filled triangles are counted but not +seen, where the Mac shell draws them (the host's `triangle` puts every +vertex at z = 0 under a depth buffer). Both are host-side questions for +the C++ lane, not evaluator faults. diff --git a/kidlisp/reports/whistlegraph-benchmark-2026-10-10.md b/kidlisp/reports/whistlegraph-benchmark-2026-10-10.md index 50534468a6..d033bfff80 100644 --- a/kidlisp/reports/whistlegraph-benchmark-2026-10-10.md +++ b/kidlisp/reports/whistlegraph-benchmark-2026-10-10.md @@ -93,3 +93,18 @@ arithmetic and pools to the numeric plan and the Wasm frame (`lib/kidlisp-plan.mjs`, `kidlisp/benchmarks/raytrace-frame.c`), keep the drawing calls as a command list the host rasterizes, and measure these two pieces again. 60 fps at 1280×720 for the shooter is the bar. + +## The native shells (evening) + +Same bundle, same pieces, run by the native bios lifecycle (PIECE-IL.md §10). + +| per frame | Xbox Series X (QuickJS-ng, no JIT) | Mac shell (JavaScriptCore, JIT) | +|---|---|---| +| shooter | 1,200 ms | 15 ms (28 to 42 fps) | +| Fía | 1,010 ms | 28 ms (25 fps) | +| starfield | 645 ms | 10 ms (54 to 60 fps) | +| frame buffer → host calls | 0 to 23 ms | 2 to 14 ms | +| 10 million sin-multiply-adds | 1,909 ms | 308 ms in local QuickJS, 482 in Node | + +The Mac shell re-signed without the JIT entitlement ran the same script at +130 to 230 ms a frame: the entitlement is the difference, not the engine. diff --git a/kidlisp/tools/native-tv-script.mjs b/kidlisp/tools/native-tv-script.mjs new file mode 100644 index 0000000000..6202d16a96 --- /dev/null +++ b/kidlisp/tools/native-tv-script.mjs @@ -0,0 +1,108 @@ +#!/usr/bin/env node +// A live script for the native bios shells (Xbox QuickJS, the Mac +// JavaScriptCore shell): the KidLisp bundle, a shim that draws a gpu-frame +// buffer with the host's wipe/box/line/triangle, the pieces embedded, and +// the boot/sim/paint/act lifecycle the shells call. PIECE-IL.md §10. +// +// node kidlisp/tools/native-tv-script.mjs [draw|nodraw] [switchMs] [out.js] [piece.lisp ...] +// node xbox/tools/live.mjs hot-deploy kidlisp/build/kidlisp-tv.js # the Xbox devkit +// +// Every 10th frame the script writes a `KIDLISP` telemetry line (piece, +// frame, ms, drawMs, op counts) and at boot a `KIDLISP_BENCH` line, so the +// host log says what the engine costs. The Mac shell reads it from +// live/oskiewar.js: copy the built app, replace that file, re-sign with the +// JIT entitlement (apple/oskiewar-mac/Oskiewar-macOS.entitlements), run with +// OSKIEWAR_PERF=1. +import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; +import { resolve, dirname, basename } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRequire } from 'node:module'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); +const require = createRequire(resolve(root, 'system/package.json')); +const esbuild = require('esbuild'); + +const [mode = 'draw', switchArg = '20000', outArg = 'kidlisp/build/kidlisp-tv.js', ...pieceArgs] = process.argv.slice(2); +const pieceFiles = pieceArgs.length ? pieceArgs : ['kidlisp/examples/whistlegraph/shooter.lisp', 'kidlisp/examples/kernels/starfield.lisp', 'kidlisp/examples/whistlegraph/fia.lisp']; + +const bundle = (await esbuild.build({ + entryPoints: [resolve(root, 'kidlisp/tools/native/entry.mjs')], bundle: true, write: false, format: 'iife', globalName: 'KidLispNative', + platform: 'neutral', target: 'es2020', external: ['url', 'module', 'https', 'node:*'], logLevel: 'silent', +})).outputFiles[0].text; + +const directives = (source) => (/^\s*;\s*@compile\b/m.test(source) ? '' : '; @compile\n') + (/^\s*;\s*@gpu\b/m.test(source) ? '' : '; @gpu\n') + source; +const pieces = Object.fromEntries(pieceFiles.map((file) => [basename(file, '.lisp'), directives(readFileSync(resolve(root, file), 'utf8'))])); + +const script = `const buildVersion = 1; +globalThis.console = globalThis.console || { log() {}, warn() {}, error() {}, info() {}, debug() {} }; +${bundle} +globalThis.performance = globalThis.performance || { now: () => Date.now() }; +const PIECES = ${JSON.stringify(pieces)}; +const ORDER = ${JSON.stringify(Object.keys(pieces))}; +const SWITCH_EVERY = ${Number(switchArg) || 20000}; +const DRAW = ${mode === 'nodraw' ? 'false' : 'true'}; +const { KidLisp, compileProgram, readFrame } = KidLispNative; +let lisp = null, piece = null, current = -1, frames = 0, lastSwitch = 0, W = 390, H = 520, fpsCount = 0, fpsAt = 0, fps = 0; +let lastOps = 0, lastErr = "", lastDrawMs = 0, lastCounts = ""; +const say = (event, detail) => { if (typeof telemetry === "function") telemetry(event, detail); }; +function screen() { try { const c = capabilities(); if (c && c.width) { W = c.width; H = c.height; } } catch (_) {} } +let ink = [255, 255, 255, 255]; +const api = { + screen: { width: W, height: H }, params: [], colon: [], system: { fps: 60 }, paintCount: 0, + needsPaint() {}, fps() {}, toggleHUD() {}, page() {}, unmask() {}, mask() {}, blend() {}, + inkrn: () => ink.slice(), clock: { time: () => new Date(), resync() {} }, sound: {}, + gpuFrame: { available: true, probe() {}, sent: false, send: (buffer) => drawFrame(buffer) }, + wipe() {}, line() {}, box() {}, circle() {}, oval() {}, tri() {}, shape() {}, write() {}, plot() {}, + ink(...a) { if (typeof a[0] === "number") ink = [a[0], a[1] ?? a[0], a[2] ?? a[0], a[3] ?? 255]; return api; }, +}; +// The hosts: wipe(r g b) · box(x y w h r g b) · line(x1 y1 x2 y2 width r g b) · triangle(x1 y1 x2 y2 x3 y3 r g b). No alpha, so faint ops are skipped. +const vis = (a) => a >= 48; +const L = (x1, y1, x2, y2, r, g, b) => line(x1, y1, x2, y2, 1, r, g, b); +const fan = (pts, r, g, b) => { for (let k = 1; k + 1 < pts.length / 2; k++) triangle(pts[0], pts[1], pts[k * 2], pts[k * 2 + 1], pts[k * 2 + 2], pts[k * 2 + 3], r, g, b); }; +const ring = (pts, r, g, b) => { const n = pts.length / 2; for (let k = 0; k < n; k++) { const j = (k + 1) % n; L(pts[k * 2], pts[k * 2 + 1], pts[j * 2], pts[j * 2 + 1], r, g, b); } }; +const visit = { + clear: (r, g, b) => wipe(r, g, b), + line: (x1, y1, x2, y2, th, r, g, b, a) => { if (vis(a)) L(x1, y1, x2, y2, r, g, b); }, + box: (x, y, w, h, fill, r, g, b, a) => { if (!vis(a)) return; if (fill) box(x, y, w, h, r, g, b); else ring([x, y, x + w, y, x + w, y + h, x, y + h], r, g, b); }, + oval: (cx, cy, rx, ry, fill, r, g, b, a) => { if (!vis(a)) return; const n = Math.max(8, Math.min(32, Math.round(Math.max(rx, ry)))); const pts = []; for (let k = 0; k < n; k++) { const t = (k / n) * Math.PI * 2; pts.push(cx + Math.cos(t) * rx, cy + Math.sin(t) * ry); } if (fill) fan(pts, r, g, b); else ring(pts, r, g, b); }, + tri: (x1, y1, x2, y2, x3, y3, fill, r, g, b, a) => { if (!vis(a)) return; if (fill) triangle(x1, y1, x2, y2, x3, y3, r, g, b); else ring([x1, y1, x2, y2, x3, y3], r, g, b); }, + shape: (pts, fill, r, g, b, a) => { if (!vis(a)) return; const p = Array.from(pts); if (fill) fan(p, r, g, b); else ring(p, r, g, b); }, +}; +const COUNTS = { clear: 0, line: 0, box: 0, oval: 0, tri: 0, shape: 0 }; +const counting = { clear: () => COUNTS.clear++, line: () => COUNTS.line++, box: () => COUNTS.box++, oval: () => COUNTS.oval++, tri: () => COUNTS.tri++, shape: () => COUNTS.shape++ }; +function drawFrame(buffer) { + lastOps = buffer.length; for (const k in COUNTS) COUNTS[k] = 0; readFrame(buffer, counting); lastCounts = JSON.stringify(COUNTS); + const d0 = Date.now(); if (DRAW) readFrame(buffer, visit); 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; + const t1 = Date.now(); for (let i = 0; i < 10000; i++) box(i % 100, 0, 1, 1, 0, 0, 0); const boxMs = Date.now() - t1; + const t2 = Date.now(); for (let i = 0; i < 10000; i++) triangle(0, 0, 1, 0, 0, 1, 0, 0, 0); const triMs = Date.now() - t2; + say("KIDLISP_BENCH", JSON.stringify({ loop1e7Ms: loopMs, box10kMs: boxMs, tri10kMs: triMs, acc: Math.round(acc) })); +} +function quietly(fn) { const log = console.log; console.log = () => {}; try { return fn(); } finally { console.log = log; } } +function load(index) { + current = index; lisp = new KidLisp(); + quietly(() => { piece = lisp.module(PIECES[ORDER[index]], true); lisp.setAPI(api); piece.boot(api); }); + frames = 0; lastSwitch = Date.now(); +} +function boot() { try { bench(); } catch (_) {} screen(); api.screen.width = W; api.screen.height = H; load(0); } +function sim() {} +function paint() { + screen(); api.screen.width = W; api.screen.height = H; + if (Date.now() - lastSwitch > SWITCH_EVERY) load((current + 1) % ORDER.length); + api.paintCount = frames++; + const t0 = Date.now(); + try { quietly(() => { piece.sim && piece.sim(api); piece.paint(api); }); } + catch (e) { lastErr = String((e && e.message) || e).slice(0, 200); if (frames % 10 === 1) say("KIDLISP_ERR", lastErr); box(0, 0, W, 40, 120, 0, 0); return; } + fpsCount++; if (Date.now() - fpsAt >= 1000) { fps = fpsCount; fpsCount = 0; fpsAt = Date.now(); } + const ms = Date.now() - t0; + if (frames % 10 === 1) say("KIDLISP", JSON.stringify({ piece: ORDER[current], frame: frames, ms, drawMs: lastDrawMs, ops: lastOps, counts: lastCounts, w: W, h: H, err: lastErr })); + box(6, 6, Math.min(W - 12, fps * 3), 4, 255, 255, 255); box(6, 12, Math.min(W - 12, ms), 4, 255, 120, 60); // fps and ms, as bars +} +function act() {} +`; +const out = resolve(root, outArg); +mkdirSync(dirname(out), { recursive: true }); +writeFileSync(out, script); +console.log(JSON.stringify({ out: outArg, mode, switchMs: Number(switchArg) || 20000, pieces: Object.keys(pieces), kb: Math.round(script.length / 1024) })); diff --git a/kidlisp/tools/native/entry.mjs b/kidlisp/tools/native/entry.mjs new file mode 100644 index 0000000000..5f1e8f1554 --- /dev/null +++ b/kidlisp/tools/native/entry.mjs @@ -0,0 +1,6 @@ +// The entry for the native bundle: the evaluator, the compiler, the frame +// builder. esbuild rolls these into one classic script (global KidLispNative) +// that QuickJS and JavaScriptCore run without modules. +export { KidLisp } from '../../../system/public/aesthetic.computer/lib/kidlisp.mjs'; +export { compileProgram } from '../../../system/public/aesthetic.computer/lib/kidlisp-compile.mjs'; +export { GpuFrame, readFrame } from '../../../system/public/aesthetic.computer/lib/gpu-frame.mjs'; -- 2.51.2