From a3701d4f5b94e6f0a588b37da04e52bac4de7f13 Mon Sep 17 00:00:00 2001 From: prompt.ac/@jeffrey Date: Tue, 14 Apr 2026 02:11:28 +0000 Subject: [PATCH] native: bring CamDoll FPS system to ac-native so arena.mjs runs unmodified Pieces that export `system = "fps"` (arena.mjs) now get a live CamDoll instance attached to api.system.fps.doll on the native runtime — same class, same source, same API surface as the web build. No piece-side changes required. Pipeline: 1. scripts/fps-bundle-entry.mjs re-exports Camera + Dolly from graph.mjs and CamDoll from cam-doll.mjs. esbuild tree-shakes graph.mjs down to just the two classes we need (~75 KB total). 2. docker-build.sh + ac-os bundle it as an IIFE (format=iife, global-name=__FpsSystem) into /jslib/fps-system-bundle.js — mirrors the kidlisp bundle pattern so it self-executes and exposes the classes at globalThis.__FpsSystem.{Camera,Dolly,CamDoll}. 3. js_load_piece loads the bundle once at runtime init (beside kidlisp) and extends the piece export shim: when a piece sets `system = "fps"`, the shim synchronously constructs a CamDoll from the piece's fpsOpts, stores it at globalThis.__fpsDoll, and wraps globalThis.{boot,sim,act,paint} to (a) inject api.system.fps.doll, (b) drive doll.sim() / doll.act(event) before the piece's own handler runs. 4. js_call_sim skips the native WASD/trackpad camera driver when a doll is present (it's authoritative), and — after the piece sim returns — copies doll.cam.{x,y,z,rotX,rotY,rotZ} into rt->camera3d so the next frame's graph3d rendering uses the doll's view. arena.mjs should now be launchable via the native prompt. Same source as the web piece; if a bug shows up, fix it in arena.mjs or cam-doll.mjs and both targets inherit the fix. Co-Authored-By: Claude Opus 4.6 (1M context) --- fedac/native/ac-os | 14 ++++++++++++++ fedac/native/docker-build.sh | 24 ++++++++++++++++++++++++ fedac/native/scripts/fps-bundle-entry.mjs | 15 +++++++++++++++ fedac/native/src/js-bindings.c | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 4 file(s) changed, 154 insertion(s)(+), 4 deletion(s)(-) diff --git a/fedac/native/ac-os b/fedac/native/ac-os --- a/fedac/native/ac-os +++ b/fedac/native/ac-os @@ -113,6 +113,20 @@ sudo mkdir -p "${INITRAMFS_ROOT}/jslib" sudo cp /tmp/kidlisp-bundle.js "${INITRAMFS_ROOT}/jslib/kidlisp-bundle.js" fi + # FPS system bundle (Camera + Dolly + CamDoll) for pieces that export + # `system = "fps"` (arena.mjs etc.). Mirrors the docker-build.sh path. + local FPS_ENTRY="${SCRIPT_DIR}/scripts/fps-bundle-entry.mjs" + if [ -f "${FPS_ENTRY}" ] && command -v npx &>/dev/null; then + log "Bundling FPS system (Camera + Dolly + CamDoll)..." + npx esbuild "${FPS_ENTRY}" --bundle --format=iife \ + --global-name=__FpsSystem --platform=neutral \ + --outfile=/tmp/fps-system-bundle.js 2>&1 | grep -v "^npm warn" || true + if [ -f /tmp/fps-system-bundle.js ]; then + sudo mkdir -p "${INITRAMFS_ROOT}/jslib" + sudo cp /tmp/fps-system-bundle.js "${INITRAMFS_ROOT}/jslib/fps-system-bundle.js" + fi + fi + # Copy fresh binary into initramfs sudo cp "${BUILD_DIR}/ac-native" "${INITRAMFS_ROOT}/ac-native" diff --git a/fedac/native/docker-build.sh b/fedac/native/docker-build.sh --- a/fedac/native/docker-build.sh +++ b/fedac/native/docker-build.sh @@ -446,6 +446,30 @@ fi cd "$NATIVE" fi +# ── 2o2: FPS system bundle (CamDoll + Camera + Dolly for `system="fps"` pieces) ── +# Tree-shakes graph.mjs down to just Camera + Dolly and pulls in CamDoll. +# Pieces like arena.mjs that export `system = "fps"` get this injected +# by the piece loader (see js_load_piece in js-bindings.c) so web and +# native share the exact same FPS camera/input source. +if command -v npx &>/dev/null && [ -f "$NATIVE/scripts/fps-bundle-entry.mjs" ]; then + log " Bundling FPS system (Camera + Dolly + CamDoll)..." + cd "$SRC" + # IIFE format so the bundle self-executes at load and exposes the + # classes as `globalThis.__FpsSystem.{Camera,Dolly,CamDoll}` — lets + # the piece loader wire them in synchronously without ES module + # resolution. Mirrors the kidlisp-bundle.js pattern. + npx esbuild "$NATIVE/scripts/fps-bundle-entry.mjs" \ + --bundle --format=iife --global-name=__FpsSystem --platform=neutral \ + --external:https --external:http --external:net --external:fs --external:path \ + --outfile=/tmp/fps-system-bundle.js 2>&1 || true + if [ -f /tmp/fps-system-bundle.js ]; then + mkdir -p "$IROOT/jslib" + cp /tmp/fps-system-bundle.js "$IROOT/jslib/fps-system-bundle.js" + log " fps-system-bundle.js: $(stat -c%s /tmp/fps-system-bundle.js) bytes" + fi + cd "$NATIVE" +fi + # ── 2p: ES module lib files for pieces (clock.mjs needs these) ── # These are pure JS with no DOM/browser deps — work in QuickJS as-is. # The module loader resolves "../lib/X.mjs" → "/lib/X.mjs" in the initramfs. diff --git a/fedac/native/scripts/fps-bundle-entry.mjs b/fedac/native/scripts/fps-bundle-entry.mjs new file mode 100644 --- /dev/null +++ b/fedac/native/scripts/fps-bundle-entry.mjs @@ -0,0 +1,15 @@ +// fps-bundle-entry.mjs — esbuild entry for the FPS system bundle. +// +// Shipped to /lib/fps-system.mjs in the initramfs. ac-native's piece +// loader imports from this when a piece exports `system = "fps"` +// (arena.mjs etc.). The bundle contains the subset of the web runtime +// needed: the Camera + Dolly 3D math classes from graph.mjs, and the +// CamDoll input+physics controller from cam-doll.mjs. esbuild +// tree-shakes everything else out of graph.mjs. +// +// Kept deliberately tiny so the bundle size stays small and the +// native/web runtimes use LITERALLY the same source for FPS — any fix +// to cam-doll.mjs or Camera/Dolly applies to both targets at once. + +export { Camera, Dolly } from "../../../system/public/aesthetic.computer/lib/graph.mjs"; +export { CamDoll } from "../../../system/public/aesthetic.computer/lib/cam-doll.mjs"; diff --git a/fedac/native/src/js-bindings.c b/fedac/native/src/js-bindings.c --- a/fedac/native/src/js-bindings.c +++ b/fedac/native/src/js-bindings.c @@ -2267,6 +2267,34 @@ } else { fprintf(stderr, "[js] KidLisp bundle not found at /jslib/kidlisp-bundle.js (optional)\n"); } + // Load FPS system bundle (Camera + Dolly + CamDoll as globalThis.__FpsSystem) + // so pieces that export `system = "fps"` (arena.mjs etc.) can be wrapped + // synchronously in the piece-load shim without async module resolution. + FILE *fps_f = fopen("/jslib/fps-system-bundle.js", "r"); + if (fps_f) { + fseek(fps_f, 0, SEEK_END); + long fps_len = ftell(fps_f); + fseek(fps_f, 0, SEEK_SET); + char *fps_src = malloc(fps_len + 1); + fread(fps_src, 1, fps_len, fps_f); + fps_src[fps_len] = '\0'; + fclose(fps_f); + JSValue fps_result = JS_Eval(ctx, fps_src, fps_len, "", JS_EVAL_TYPE_GLOBAL); + free(fps_src); + if (JS_IsException(fps_result)) { + JSValue exc = JS_GetException(ctx); + const char *str = JS_ToCString(ctx, exc); + fprintf(stderr, "[js] FPS bundle error: %s\n", str); + JS_FreeCString(ctx, str); + JS_FreeValue(ctx, exc); + } else { + fprintf(stderr, "[js] FPS system loaded (%ld bytes)\n", fps_len); + } + JS_FreeValue(ctx, fps_result); + } else { + fprintf(stderr, "[js] FPS bundle not found at /jslib/fps-system-bundle.js (optional)\n"); + } + JS_FreeValue(ctx, global); return rt; } @@ -6630,7 +6658,11 @@ src[len] = '\0'; fclose(f); // Append globalThis export assignments so we can find lifecycle functions - // after module evaluation (QuickJS modules don't expose exports publicly) + // after module evaluation (QuickJS modules don't expose exports publicly). + // For pieces with `export const system = "fps"` (arena.mjs etc.) this + // ALSO instantiates CamDoll from the preloaded FPS bundle and wraps + // boot/sim/act/paint so `api.system.fps.doll` is live on every call + // without the piece needing to know anything about the native runtime. const char *export_shim = "\n;if(typeof boot==='function')globalThis.boot=boot;" "if(typeof paint==='function')globalThis.paint=paint;" @@ -6639,7 +6671,23 @@ "if(typeof sim==='function')globalThis.sim=sim;" "if(typeof leave==='function')globalThis.leave=leave;" "if(typeof beat==='function')globalThis.beat=beat;" "if(typeof configureAutopat==='function')globalThis.configureAutopat=configureAutopat;" - "if(typeof system!=='undefined')globalThis.__pieceSystem=system;\n"; + "if(typeof system!=='undefined')globalThis.__pieceSystem=system;" + "if(typeof fpsOpts!=='undefined')globalThis.__pieceFpsOpts=fpsOpts;" + // FPS system wiring — runs only when piece opts in. + "if(globalThis.__pieceSystem==='fps'&&globalThis.__FpsSystem){" + "try{" + "const FS=globalThis.__FpsSystem;" + "const opts=globalThis.__pieceFpsOpts||{fov:80};" + "const doll=new FS.CamDoll(FS.Camera,FS.Dolly,opts);" + "globalThis.__fpsDoll=doll;" + "const _b=globalThis.boot,_s=globalThis.sim,_a=globalThis.act,_p=globalThis.paint;" + "const inject=(api)=>{if(api){if(!api.system)api.system={};api.system.fps={doll};}};" + "globalThis.boot=(api)=>{inject(api);return _b?.(api);};" + "globalThis.sim=(api)=>{inject(api);try{doll.sim?.();}catch(e){}return _s?.(api);};" + "globalThis.act=(api)=>{inject(api);try{if(api?.event)doll.act?.(api.event);}catch(e){}return _a?.(api);};" + "globalThis.paint=(api)=>{inject(api);return _p?.(api);};" + "}catch(e){console.error('[fps] wire-up failed:',e.message);}" + "}\n"; size_t shim_len = strlen(export_shim); char *patched = malloc(len + shim_len + 1); memcpy(patched, src, len); @@ -6904,8 +6952,20 @@ void js_call_sim(ACRuntime *rt) { current_rt = rt; - // Update FPS camera from key state + trackpad delta - if (rt->pen_locked && rt->fps_system_active) { + // If the piece's FPS bundle installed a CamDoll (via the export + // shim in js_load_piece), the doll's own sim() — invoked from the + // wrapped piece sim() — is the camera authority. We just sync its + // cam state back to rt->camera3d AFTER the piece sim runs (below). + // Otherwise, fall back to the native WASD + trackpad driver. + int have_doll = 0; + { + JSValue global = JS_GetGlobalObject(rt->ctx); + JSValue doll = JS_GetPropertyStr(rt->ctx, global, "__fpsDoll"); + have_doll = !JS_IsUndefined(doll) && !JS_IsNull(doll); + JS_FreeValue(rt->ctx, doll); + JS_FreeValue(rt->ctx, global); + } + if (!have_doll && rt->pen_locked && rt->fps_system_active) { camera3d_update(&rt->camera3d, rt->keys_held[KEY_W], rt->keys_held[KEY_S], @@ -6937,6 +6997,43 @@ JS_FreeValue(rt->ctx, exc); } JS_FreeValue(rt->ctx, result); JS_FreeValue(rt->ctx, api); + + // FPS camera sync — after the wrapped piece sim() runs (which + // invoked doll.sim() via the shim), copy doll.cam.{x,y,z,rotX/Y/Z} + // back into rt->camera3d so the next frame's graph3d rendering + // uses the doll's camera. Only syncs when the doll is present. + if (have_doll) { + JSValue global = JS_GetGlobalObject(rt->ctx); + JSValue doll = JS_GetPropertyStr(rt->ctx, global, "__fpsDoll"); + if (!JS_IsUndefined(doll) && !JS_IsNull(doll)) { + JSValue cam = JS_GetPropertyStr(rt->ctx, doll, "cam"); + if (!JS_IsUndefined(cam) && !JS_IsNull(cam)) { + double x, y, z, rx, ry, rz; + JSValue v; + v = JS_GetPropertyStr(rt->ctx, cam, "x"); + if (JS_ToFloat64(rt->ctx, &x, v) == 0) rt->camera3d.x = (float)x; + JS_FreeValue(rt->ctx, v); + v = JS_GetPropertyStr(rt->ctx, cam, "y"); + if (JS_ToFloat64(rt->ctx, &y, v) == 0) rt->camera3d.y = (float)y; + JS_FreeValue(rt->ctx, v); + v = JS_GetPropertyStr(rt->ctx, cam, "z"); + if (JS_ToFloat64(rt->ctx, &z, v) == 0) rt->camera3d.z = (float)z; + JS_FreeValue(rt->ctx, v); + v = JS_GetPropertyStr(rt->ctx, cam, "rotX"); + if (JS_ToFloat64(rt->ctx, &rx, v) == 0) rt->camera3d.rotX = (float)rx; + JS_FreeValue(rt->ctx, v); + v = JS_GetPropertyStr(rt->ctx, cam, "rotY"); + if (JS_ToFloat64(rt->ctx, &ry, v) == 0) rt->camera3d.rotY = (float)ry; + JS_FreeValue(rt->ctx, v); + v = JS_GetPropertyStr(rt->ctx, cam, "rotZ"); + if (JS_ToFloat64(rt->ctx, &rz, v) == 0) rt->camera3d.rotZ = (float)rz; + JS_FreeValue(rt->ctx, v); + } + JS_FreeValue(rt->ctx, cam); + } + JS_FreeValue(rt->ctx, doll); + JS_FreeValue(rt->ctx, global); + } // Execute pending jobs (promises) JSContext *pctx; -- tangled.sh