Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

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) <noreply@anthropic.com>

+154 -4
+14
fedac/native/ac-os
··· 113 113 sudo cp /tmp/kidlisp-bundle.js "${INITRAMFS_ROOT}/jslib/kidlisp-bundle.js" 114 114 fi 115 115 116 + # FPS system bundle (Camera + Dolly + CamDoll) for pieces that export 117 + # `system = "fps"` (arena.mjs etc.). Mirrors the docker-build.sh path. 118 + local FPS_ENTRY="${SCRIPT_DIR}/scripts/fps-bundle-entry.mjs" 119 + if [ -f "${FPS_ENTRY}" ] && command -v npx &>/dev/null; then 120 + log "Bundling FPS system (Camera + Dolly + CamDoll)..." 121 + npx esbuild "${FPS_ENTRY}" --bundle --format=iife \ 122 + --global-name=__FpsSystem --platform=neutral \ 123 + --outfile=/tmp/fps-system-bundle.js 2>&1 | grep -v "^npm warn" || true 124 + if [ -f /tmp/fps-system-bundle.js ]; then 125 + sudo mkdir -p "${INITRAMFS_ROOT}/jslib" 126 + sudo cp /tmp/fps-system-bundle.js "${INITRAMFS_ROOT}/jslib/fps-system-bundle.js" 127 + fi 128 + fi 129 + 116 130 # Copy fresh binary into initramfs 117 131 sudo cp "${BUILD_DIR}/ac-native" "${INITRAMFS_ROOT}/ac-native" 118 132
+24
fedac/native/docker-build.sh
··· 446 446 cd "$NATIVE" 447 447 fi 448 448 449 + # ── 2o2: FPS system bundle (CamDoll + Camera + Dolly for `system="fps"` pieces) ── 450 + # Tree-shakes graph.mjs down to just Camera + Dolly and pulls in CamDoll. 451 + # Pieces like arena.mjs that export `system = "fps"` get this injected 452 + # by the piece loader (see js_load_piece in js-bindings.c) so web and 453 + # native share the exact same FPS camera/input source. 454 + if command -v npx &>/dev/null && [ -f "$NATIVE/scripts/fps-bundle-entry.mjs" ]; then 455 + log " Bundling FPS system (Camera + Dolly + CamDoll)..." 456 + cd "$SRC" 457 + # IIFE format so the bundle self-executes at load and exposes the 458 + # classes as `globalThis.__FpsSystem.{Camera,Dolly,CamDoll}` — lets 459 + # the piece loader wire them in synchronously without ES module 460 + # resolution. Mirrors the kidlisp-bundle.js pattern. 461 + npx esbuild "$NATIVE/scripts/fps-bundle-entry.mjs" \ 462 + --bundle --format=iife --global-name=__FpsSystem --platform=neutral \ 463 + --external:https --external:http --external:net --external:fs --external:path \ 464 + --outfile=/tmp/fps-system-bundle.js 2>&1 || true 465 + if [ -f /tmp/fps-system-bundle.js ]; then 466 + mkdir -p "$IROOT/jslib" 467 + cp /tmp/fps-system-bundle.js "$IROOT/jslib/fps-system-bundle.js" 468 + log " fps-system-bundle.js: $(stat -c%s /tmp/fps-system-bundle.js) bytes" 469 + fi 470 + cd "$NATIVE" 471 + fi 472 + 449 473 # ── 2p: ES module lib files for pieces (clock.mjs needs these) ── 450 474 # These are pure JS with no DOM/browser deps — work in QuickJS as-is. 451 475 # The module loader resolves "../lib/X.mjs" → "/lib/X.mjs" in the initramfs.
+15
fedac/native/scripts/fps-bundle-entry.mjs
··· 1 + // fps-bundle-entry.mjs — esbuild entry for the FPS system bundle. 2 + // 3 + // Shipped to /lib/fps-system.mjs in the initramfs. ac-native's piece 4 + // loader imports from this when a piece exports `system = "fps"` 5 + // (arena.mjs etc.). The bundle contains the subset of the web runtime 6 + // needed: the Camera + Dolly 3D math classes from graph.mjs, and the 7 + // CamDoll input+physics controller from cam-doll.mjs. esbuild 8 + // tree-shakes everything else out of graph.mjs. 9 + // 10 + // Kept deliberately tiny so the bundle size stays small and the 11 + // native/web runtimes use LITERALLY the same source for FPS — any fix 12 + // to cam-doll.mjs or Camera/Dolly applies to both targets at once. 13 + 14 + export { Camera, Dolly } from "../../../system/public/aesthetic.computer/lib/graph.mjs"; 15 + export { CamDoll } from "../../../system/public/aesthetic.computer/lib/cam-doll.mjs";
+101 -4
fedac/native/src/js-bindings.c
··· 2267 2267 fprintf(stderr, "[js] KidLisp bundle not found at /jslib/kidlisp-bundle.js (optional)\n"); 2268 2268 } 2269 2269 2270 + // Load FPS system bundle (Camera + Dolly + CamDoll as globalThis.__FpsSystem) 2271 + // so pieces that export `system = "fps"` (arena.mjs etc.) can be wrapped 2272 + // synchronously in the piece-load shim without async module resolution. 2273 + FILE *fps_f = fopen("/jslib/fps-system-bundle.js", "r"); 2274 + if (fps_f) { 2275 + fseek(fps_f, 0, SEEK_END); 2276 + long fps_len = ftell(fps_f); 2277 + fseek(fps_f, 0, SEEK_SET); 2278 + char *fps_src = malloc(fps_len + 1); 2279 + fread(fps_src, 1, fps_len, fps_f); 2280 + fps_src[fps_len] = '\0'; 2281 + fclose(fps_f); 2282 + JSValue fps_result = JS_Eval(ctx, fps_src, fps_len, "<fps-system-bundle>", JS_EVAL_TYPE_GLOBAL); 2283 + free(fps_src); 2284 + if (JS_IsException(fps_result)) { 2285 + JSValue exc = JS_GetException(ctx); 2286 + const char *str = JS_ToCString(ctx, exc); 2287 + fprintf(stderr, "[js] FPS bundle error: %s\n", str); 2288 + JS_FreeCString(ctx, str); 2289 + JS_FreeValue(ctx, exc); 2290 + } else { 2291 + fprintf(stderr, "[js] FPS system loaded (%ld bytes)\n", fps_len); 2292 + } 2293 + JS_FreeValue(ctx, fps_result); 2294 + } else { 2295 + fprintf(stderr, "[js] FPS bundle not found at /jslib/fps-system-bundle.js (optional)\n"); 2296 + } 2297 + 2270 2298 JS_FreeValue(ctx, global); 2271 2299 return rt; 2272 2300 } ··· 6630 6658 fclose(f); 6631 6659 6632 6660 // Append globalThis export assignments so we can find lifecycle functions 6633 - // after module evaluation (QuickJS modules don't expose exports publicly) 6661 + // after module evaluation (QuickJS modules don't expose exports publicly). 6662 + // For pieces with `export const system = "fps"` (arena.mjs etc.) this 6663 + // ALSO instantiates CamDoll from the preloaded FPS bundle and wraps 6664 + // boot/sim/act/paint so `api.system.fps.doll` is live on every call 6665 + // without the piece needing to know anything about the native runtime. 6634 6666 const char *export_shim = 6635 6667 "\n;if(typeof boot==='function')globalThis.boot=boot;" 6636 6668 "if(typeof paint==='function')globalThis.paint=paint;" ··· 6639 6671 "if(typeof leave==='function')globalThis.leave=leave;" 6640 6672 "if(typeof beat==='function')globalThis.beat=beat;" 6641 6673 "if(typeof configureAutopat==='function')globalThis.configureAutopat=configureAutopat;" 6642 - "if(typeof system!=='undefined')globalThis.__pieceSystem=system;\n"; 6674 + "if(typeof system!=='undefined')globalThis.__pieceSystem=system;" 6675 + "if(typeof fpsOpts!=='undefined')globalThis.__pieceFpsOpts=fpsOpts;" 6676 + // FPS system wiring — runs only when piece opts in. 6677 + "if(globalThis.__pieceSystem==='fps'&&globalThis.__FpsSystem){" 6678 + "try{" 6679 + "const FS=globalThis.__FpsSystem;" 6680 + "const opts=globalThis.__pieceFpsOpts||{fov:80};" 6681 + "const doll=new FS.CamDoll(FS.Camera,FS.Dolly,opts);" 6682 + "globalThis.__fpsDoll=doll;" 6683 + "const _b=globalThis.boot,_s=globalThis.sim,_a=globalThis.act,_p=globalThis.paint;" 6684 + "const inject=(api)=>{if(api){if(!api.system)api.system={};api.system.fps={doll};}};" 6685 + "globalThis.boot=(api)=>{inject(api);return _b?.(api);};" 6686 + "globalThis.sim=(api)=>{inject(api);try{doll.sim?.();}catch(e){}return _s?.(api);};" 6687 + "globalThis.act=(api)=>{inject(api);try{if(api?.event)doll.act?.(api.event);}catch(e){}return _a?.(api);};" 6688 + "globalThis.paint=(api)=>{inject(api);return _p?.(api);};" 6689 + "}catch(e){console.error('[fps] wire-up failed:',e.message);}" 6690 + "}\n"; 6643 6691 size_t shim_len = strlen(export_shim); 6644 6692 char *patched = malloc(len + shim_len + 1); 6645 6693 memcpy(patched, src, len); ··· 6904 6952 void js_call_sim(ACRuntime *rt) { 6905 6953 current_rt = rt; 6906 6954 6907 - // Update FPS camera from key state + trackpad delta 6908 - if (rt->pen_locked && rt->fps_system_active) { 6955 + // If the piece's FPS bundle installed a CamDoll (via the export 6956 + // shim in js_load_piece), the doll's own sim() — invoked from the 6957 + // wrapped piece sim() — is the camera authority. We just sync its 6958 + // cam state back to rt->camera3d AFTER the piece sim runs (below). 6959 + // Otherwise, fall back to the native WASD + trackpad driver. 6960 + int have_doll = 0; 6961 + { 6962 + JSValue global = JS_GetGlobalObject(rt->ctx); 6963 + JSValue doll = JS_GetPropertyStr(rt->ctx, global, "__fpsDoll"); 6964 + have_doll = !JS_IsUndefined(doll) && !JS_IsNull(doll); 6965 + JS_FreeValue(rt->ctx, doll); 6966 + JS_FreeValue(rt->ctx, global); 6967 + } 6968 + if (!have_doll && rt->pen_locked && rt->fps_system_active) { 6909 6969 camera3d_update(&rt->camera3d, 6910 6970 rt->keys_held[KEY_W], 6911 6971 rt->keys_held[KEY_S], ··· 6937 6997 } 6938 6998 JS_FreeValue(rt->ctx, result); 6939 6999 JS_FreeValue(rt->ctx, api); 7000 + 7001 + // FPS camera sync — after the wrapped piece sim() runs (which 7002 + // invoked doll.sim() via the shim), copy doll.cam.{x,y,z,rotX/Y/Z} 7003 + // back into rt->camera3d so the next frame's graph3d rendering 7004 + // uses the doll's camera. Only syncs when the doll is present. 7005 + if (have_doll) { 7006 + JSValue global = JS_GetGlobalObject(rt->ctx); 7007 + JSValue doll = JS_GetPropertyStr(rt->ctx, global, "__fpsDoll"); 7008 + if (!JS_IsUndefined(doll) && !JS_IsNull(doll)) { 7009 + JSValue cam = JS_GetPropertyStr(rt->ctx, doll, "cam"); 7010 + if (!JS_IsUndefined(cam) && !JS_IsNull(cam)) { 7011 + double x, y, z, rx, ry, rz; 7012 + JSValue v; 7013 + v = JS_GetPropertyStr(rt->ctx, cam, "x"); 7014 + if (JS_ToFloat64(rt->ctx, &x, v) == 0) rt->camera3d.x = (float)x; 7015 + JS_FreeValue(rt->ctx, v); 7016 + v = JS_GetPropertyStr(rt->ctx, cam, "y"); 7017 + if (JS_ToFloat64(rt->ctx, &y, v) == 0) rt->camera3d.y = (float)y; 7018 + JS_FreeValue(rt->ctx, v); 7019 + v = JS_GetPropertyStr(rt->ctx, cam, "z"); 7020 + if (JS_ToFloat64(rt->ctx, &z, v) == 0) rt->camera3d.z = (float)z; 7021 + JS_FreeValue(rt->ctx, v); 7022 + v = JS_GetPropertyStr(rt->ctx, cam, "rotX"); 7023 + if (JS_ToFloat64(rt->ctx, &rx, v) == 0) rt->camera3d.rotX = (float)rx; 7024 + JS_FreeValue(rt->ctx, v); 7025 + v = JS_GetPropertyStr(rt->ctx, cam, "rotY"); 7026 + if (JS_ToFloat64(rt->ctx, &ry, v) == 0) rt->camera3d.rotY = (float)ry; 7027 + JS_FreeValue(rt->ctx, v); 7028 + v = JS_GetPropertyStr(rt->ctx, cam, "rotZ"); 7029 + if (JS_ToFloat64(rt->ctx, &rz, v) == 0) rt->camera3d.rotZ = (float)rz; 7030 + JS_FreeValue(rt->ctx, v); 7031 + } 7032 + JS_FreeValue(rt->ctx, cam); 7033 + } 7034 + JS_FreeValue(rt->ctx, doll); 7035 + JS_FreeValue(rt->ctx, global); 7036 + } 6940 7037 6941 7038 // Execute pending jobs (promises) 6942 7039 JSContext *pctx;