diff --git a/fedac/native/ac-os b/fedac/native/ac-os index 705915b48..70c1be004 100755 --- a/fedac/native/ac-os +++ b/fedac/native/ac-os @@ -113,6 +113,20 @@ build_initramfs() { 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 index 73d0753b9..b54d4d8dd 100644 --- a/fedac/native/docker-build.sh +++ b/fedac/native/docker-build.sh @@ -446,6 +446,30 @@ if command -v npx &>/dev/null && [ -f "$SRC/system/public/aesthetic.computer/lib 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 index 000000000..4d2a926e9 --- /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 index 297efc2ad..6460dc682 100644 --- a/fedac/native/src/js-bindings.c +++ b/fedac/native/src/js-bindings.c @@ -2267,6 +2267,34 @@ ACRuntime *js_init(ACGraph *graph, ACInput *input, ACAudio *audio, ACWifi *wifi, 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 @@ int js_load_piece(ACRuntime *rt, const char *path) { 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 @@ int js_load_piece(ACRuntime *rt, const char *path) { "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_act(ACRuntime *rt) { 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], @@ -6938,6 +6998,43 @@ void js_call_sim(ACRuntime *rt) { 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; while (JS_ExecutePendingJob(rt->rt, &pctx) > 0) {}