Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424;;; js-bridge.lisp — Bridge between QuickJS and CL graphics/audio/input;;;;;; This creates a QuickJS context, registers CL functions as the JS API;;; (wipe, ink, line, box, circle, sound.synth, etc.), loads .mjs pieces,;;; and drives the piece lifecycle (boot, paint, act, sim) each frame.
(in-package :ac-native.js-bridge)
(defvar *ctx* nil "Active QuickJS context")(defvar *rt* nil "Active QuickJS runtime")
(defmacro with-fp-traps-masked (&body body) "Disable SBCL floating-point exception traps around BODY.QuickJS does normal FP math (NaN, Inf) that triggers SBCL's traps." `(sb-int:with-float-traps-masked (:invalid :overflow :divide-by-zero :underflow :inexact) ,@body))(defvar *graph* nil "Current graph context for JS callbacks")(defvar *fb* nil "Current framebuffer for JS callbacks")(defvar *audio* nil "Audio engine for JS callbacks")(defvar *screen-w* 0 "Screen width")(defvar *screen-h* 0 "Screen height")(defvar *has-boot* nil)(defvar *has-paint* nil)(defvar *has-act* nil)(defvar *has-sim* nil)
;;; ── CFFI callbacks (called from JS via quickjs-shim) ──;;; These use the QuickJS JSCFunction signature:;;; JSValue callback(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
(defvar *jump-target* nil "Piece name to jump to (set by system.jump).")(defvar *poweroff-requested* nil "Set to T when JS calls system.poweroff().")
(cffi:defcallback cl-wipe :uint64 ((ctx :pointer) (this :uint64) (argc :int) (argv :pointer)) (declare (ignore this)) (let ((r (ac-native.quickjs:qjs-arg-int ctx argv 0)) (g (ac-native.quickjs:qjs-arg-int ctx argv 1)) (b (ac-native.quickjs:qjs-arg-int ctx argv 2))) (declare (ignore argc)) (ac-native.graph:graph-wipe *graph* (ac-native.color:make-color :r r :g g :b b)) 0)) ; JS_UNDEFINED
(cffi:defcallback cl-ink :uint64 ((ctx :pointer) (this :uint64) (argc :int) (argv :pointer)) (declare (ignore this)) (let ((r (ac-native.quickjs:qjs-arg-int ctx argv 0)) (g (ac-native.quickjs:qjs-arg-int ctx argv 1)) (b (ac-native.quickjs:qjs-arg-int ctx argv 2)) (a (if (> argc 3) (ac-native.quickjs:qjs-arg-int ctx argv 3) 255))) (ac-native.graph:graph-ink *graph* (ac-native.color:make-color :r r :g g :b b :a a)) 0))
(cffi:defcallback cl-line :uint64 ((ctx :pointer) (this :uint64) (argc :int) (argv :pointer)) (declare (ignore this argc)) (let ((x1 (ac-native.quickjs:qjs-arg-int ctx argv 0)) (y1 (ac-native.quickjs:qjs-arg-int ctx argv 1)) (x2 (ac-native.quickjs:qjs-arg-int ctx argv 2)) (y2 (ac-native.quickjs:qjs-arg-int ctx argv 3))) (ac-native.graph:graph-line *graph* x1 y1 x2 y2) 0))
(cffi:defcallback cl-box :uint64 ((ctx :pointer) (this :uint64) (argc :int) (argv :pointer)) (declare (ignore this argc)) (let ((x (ac-native.quickjs:qjs-arg-int ctx argv 0)) (y (ac-native.quickjs:qjs-arg-int ctx argv 1)) (w (ac-native.quickjs:qjs-arg-int ctx argv 2)) (h (ac-native.quickjs:qjs-arg-int ctx argv 3))) (ac-native.graph:graph-box *graph* x y w h) 0))
(cffi:defcallback cl-circle :uint64 ((ctx :pointer) (this :uint64) (argc :int) (argv :pointer)) (declare (ignore this argc)) (let ((x (ac-native.quickjs:qjs-arg-int ctx argv 0)) (y (ac-native.quickjs:qjs-arg-int ctx argv 1)) (r (ac-native.quickjs:qjs-arg-int ctx argv 2))) (ac-native.graph:graph-circle *graph* x y r) 0))
(cffi:defcallback cl-plot :uint64 ((ctx :pointer) (this :uint64) (argc :int) (argv :pointer)) (declare (ignore this argc)) (let ((x (ac-native.quickjs:qjs-arg-int ctx argv 0)) (y (ac-native.quickjs:qjs-arg-int ctx argv 1))) (ac-native.graph:graph-plot *graph* x y) 0))
(cffi:defcallback cl-write :uint64 ((ctx :pointer) (this :uint64) (argc :int) (argv :pointer)) (declare (ignore this argc)) (let ((text (ac-native.quickjs:qjs-arg-string ctx argv 0)) (x (ac-native.quickjs:qjs-arg-int ctx argv 1)) (y (ac-native.quickjs:qjs-arg-int ctx argv 2))) (ac-native.font:font-draw *graph* text x y) 0))
(cffi:defcallback cl-screen-w :uint64 ((ctx :pointer) (this :uint64) (argc :int) (argv :pointer)) (declare (ignore ctx this argc argv)) ;; Return screen width as NaN-boxed int — use shim helper instead 0)
(cffi:defcallback cl-screen-h :uint64 ((ctx :pointer) (this :uint64) (argc :int) (argv :pointer)) (declare (ignore ctx this argc argv)) 0)
(cffi:defcallback cl-synth :uint64 ((ctx :pointer) (this :uint64) (argc :int) (argv :pointer)) (declare (ignore this argc)) (when *audio* (let ((type (ac-native.quickjs:qjs-arg-int ctx argv 0)) (freq (ac-native.quickjs:qjs-arg-float ctx argv 1)) (vol (ac-native.quickjs:qjs-arg-float ctx argv 2)) (dur (ac-native.quickjs:qjs-arg-float ctx argv 3)) (attack (ac-native.quickjs:qjs-arg-float ctx argv 4)) (decay (ac-native.quickjs:qjs-arg-float ctx argv 5)) (pan (ac-native.quickjs:qjs-arg-float ctx argv 6))) (ac-native.audio:audio-synth *audio* :type type :tone freq :volume vol :duration dur :attack attack :decay decay :pan pan))) 0)
(cffi:defcallback cl-synth-kill :uint64 ((ctx :pointer) (this :uint64) (argc :int) (argv :pointer)) (declare (ignore this argc)) (when *audio* (let ((id (ac-native.quickjs:qjs-arg-int ctx argv 0))) (ac-native.audio:audio-synth-kill *audio* id))) 0)
(cffi:defcallback cl-log :uint64 ((ctx :pointer) (this :uint64) (argc :int) (argv :pointer)) (declare (ignore this argc)) (let ((msg (ac-native.quickjs:qjs-arg-string ctx argv 0))) (format *error-output* "[js] ~A~%" msg) (force-output *error-output*)) 0)
(cffi:defcallback cl-jump :uint64 ((ctx :pointer) (this :uint64) (argc :int) (argv :pointer)) (declare (ignore this argc)) (let ((piece (ac-native.quickjs:qjs-arg-string ctx argv 0))) (setf *jump-target* piece) (format *error-output* "[js-bridge] jump → ~A~%" piece) (force-output *error-output*)) 0)
(cffi:defcallback cl-poweroff :uint64 ((ctx :pointer) (this :uint64) (argc :int) (argv :pointer)) (declare (ignore ctx this argc argv)) (setf *poweroff-requested* t) 0)
;;; ── Initialize QuickJS and register the API ──
(defun js-init (graph fb audio screen-w screen-h) "Create QuickJS runtime and context, register CL functions as JS API." (setf *graph* graph *fb* fb *audio* audio *screen-w* screen-w *screen-h* screen-h *jump-target* nil *poweroff-requested* nil) (setf *rt* (ac-native.quickjs:js-new-runtime)) (setf *ctx* (ac-native.quickjs:js-new-context *rt*)) ;; Register native callbacks (ac-native.quickjs:qjs-register-func *ctx* "__cl_wipe" (cffi:callback cl-wipe) 3) (ac-native.quickjs:qjs-register-func *ctx* "__cl_ink" (cffi:callback cl-ink) 4) (ac-native.quickjs:qjs-register-func *ctx* "__cl_line" (cffi:callback cl-line) 4) (ac-native.quickjs:qjs-register-func *ctx* "__cl_box" (cffi:callback cl-box) 4) (ac-native.quickjs:qjs-register-func *ctx* "__cl_circle" (cffi:callback cl-circle) 3) (ac-native.quickjs:qjs-register-func *ctx* "__cl_plot" (cffi:callback cl-plot) 2) (ac-native.quickjs:qjs-register-func *ctx* "__cl_write" (cffi:callback cl-write) 3) (ac-native.quickjs:qjs-register-func *ctx* "__cl_screen_w" (cffi:callback cl-screen-w) 0) (ac-native.quickjs:qjs-register-func *ctx* "__cl_screen_h" (cffi:callback cl-screen-h) 0) (ac-native.quickjs:qjs-register-func *ctx* "__cl_synth" (cffi:callback cl-synth) 7) (ac-native.quickjs:qjs-register-func *ctx* "__cl_synth_kill" (cffi:callback cl-synth-kill) 2) (ac-native.quickjs:qjs-register-func *ctx* "__cl_log" (cffi:callback cl-log) 1) (ac-native.quickjs:qjs-register-func *ctx* "__cl_jump" (cffi:callback cl-jump) 1) (ac-native.quickjs:qjs-register-func *ctx* "__cl_poweroff" (cffi:callback cl-poweroff) 0) ;; Set screen dimensions (ac-native.quickjs:qjs-set-global-int *ctx* "__screen_w" screen-w) (ac-native.quickjs:qjs-set-global-int *ctx* "__screen_h" screen-h) ;; Eval the API bootstrap JS (let ((api-code (build-api-js))) (with-fp-traps-masked (ac-native.quickjs:qjs-eval *ctx* api-code (length api-code) "<api-init>" 0))) (format *error-output* "[js-bridge] QuickJS initialized (~Dx~D)~%" screen-w screen-h) (force-output *error-output*))
(defun js-destroy () "Free QuickJS context and runtime." (when *ctx* (ac-native.quickjs:js-free-context *ctx*) (setf *ctx* nil)) (when *rt* (ac-native.quickjs:js-free-runtime *rt*) (setf *rt* nil)))
;;; ── Load and run a piece ──
(defun js-load-piece (path) "Load a .mjs piece file and detect lifecycle functions." (let ((code (with-open-file (s path :direction :input :if-does-not-exist nil :external-format :utf-8) (when s (let* ((buf (make-string (file-length s))) (n (read-sequence buf s))) ;; Trim to actual bytes read (avoid trailing nulls) (subseq buf 0 n)))))) (unless code (format *error-output* "[js-bridge] Piece not found: ~A~%" path) (return-from js-load-piece nil)) ;; Strip export statement and eval as global script. ;; Use byte length (not char length) for C interop. (let* ((stripped (let ((pos (search "export " code))) (if pos ;; Replace all `export ` with empty to handle: ;; `export { boot, paint, act, sim };` and ;; `export function boot(...)` etc. (let ((s (copy-seq code)) (p pos)) (loop while p do ;; Find closing ; for `export {` (if (and (< (+ p 7) (length s)) (char= (char s (+ p 7)) #\{)) ;; export { ... }; → blank the whole thing (let ((semi (position #\; s :start p))) (when semi (fill s #\Space :start p :end (1+ semi)))) ;; export function/const/let → just blank "export " (fill s #\Space :start p :end (min (+ p 7) (length s)))) (setf p (search "export " s :start2 (1+ p)))) s) code))) (wrapper (format nil "~A~%~// Expose lifecycle functions as globals~%~if (typeof boot === 'function') globalThis.__piece_boot = boot;~%~if (typeof paint === 'function') globalThis.__piece_paint = paint;~%~if (typeof act === 'function') globalThis.__piece_act = act;~%~if (typeof sim === 'function') globalThis.__piece_sim = sim;~%~if (typeof leave === 'function') globalThis.__piece_leave = leave;~%" stripped)) (bytes (sb-ext:string-to-octets wrapper :external-format :utf-8))) (let ((rc (with-fp-traps-masked (ac-native.quickjs:qjs-eval *ctx* wrapper (length bytes) path 0)))) ; 0 = JS_EVAL_TYPE_GLOBAL (when (= rc -1) (format *error-output* "[js-bridge] Failed to load ~A~%" path) (return-from js-load-piece nil)))) ;; Detect which lifecycle functions exist (setf *has-boot* (= 1 (ac-native.quickjs:qjs-has-global-func *ctx* "__piece_boot"))) (setf *has-paint* (= 1 (ac-native.quickjs:qjs-has-global-func *ctx* "__piece_paint"))) (setf *has-act* (= 1 (ac-native.quickjs:qjs-has-global-func *ctx* "__piece_act"))) (setf *has-sim* (= 1 (ac-native.quickjs:qjs-has-global-func *ctx* "__piece_sim"))) (format *error-output* "[js-bridge] Loaded ~A (boot=~A paint=~A act=~A sim=~A)~%" path *has-boot* *has-paint* *has-act* *has-sim*) (force-output *error-output*) t))
;;; ── Lifecycle calls ──
(defun js-boot () "Call the piece's boot() with the API object." (when *has-boot* (with-fp-traps-masked (ac-native.quickjs:qjs-call-with-api *ctx* "__piece_boot") (ac-native.quickjs:qjs-execute-pending *ctx*))))
(defun js-paint (paint-count) "Call the piece's paint() with the API object." (when *has-paint* (with-fp-traps-masked (ac-native.quickjs:qjs-set-global-int *ctx* "__paintCount" paint-count) (ac-native.quickjs:qjs-call-with-api *ctx* "__piece_paint") (ac-native.quickjs:qjs-execute-pending *ctx*))))
(defun js-act (event-type event-key event-code) "Call the piece's act() with an event." (when *has-act* (with-fp-traps-masked (ac-native.quickjs:qjs-set-global-int *ctx* "__evType" event-type) (ac-native.quickjs:qjs-set-global-string *ctx* "__evKey" event-key) (ac-native.quickjs:qjs-set-global-int *ctx* "__evCode" event-code) (ac-native.quickjs:qjs-call-with-api *ctx* "__piece_act") (ac-native.quickjs:qjs-execute-pending *ctx*))))
(defun js-sim () "Call the piece's sim()." (when *has-sim* (with-fp-traps-masked (ac-native.quickjs:qjs-call-with-api *ctx* "__piece_sim") (ac-native.quickjs:qjs-execute-pending *ctx*))))
;;; ── Build the JS API object ──;;; This JS code creates __ac_api with all the graphics/audio/system;;; functions that call back into native CL via registered C functions.
(defun build-api-js () "Return JS source that builds the __ac_api object." (format nil "// ConsoleglobalThis.console = { log: function(...args) { __cl_log(args.join(' ')); }, warn: function(...args) { __cl_log('[warn] ' + args.join(' ')); }, error: function(...args) { __cl_log('[error] ' + args.join(' ')); },};
// Screen dimensions (updated each frame by CL)globalThis.__paintCount = 0;globalThis.__evType = 0;globalThis.__evKey = '';globalThis.__evCode = 0;
// Event helperfunction makeEvent() { return { _type: __evType, _key: __evKey, code: __evCode, key: __evKey, name: __evKey, x: 0, y: 0, repeat: false, velocity: 255, pressure: 0, is: function(pattern) { const parts = pattern.split(':'); if (parts[0] === 'keyboard') { const dir = parts[1]; // 'down' or 'up' const key = parts[2]; if (dir === 'down' && __evType === 1 && __evKey === key) return true; if (dir === 'up' && __evType === 0 && __evKey === key) return true; return false; } return false; } };}
// Build API object passed to lifecycle functionsglobalThis.__ac_api = { wipe: function(r, g, b) { __cl_wipe(r||0, g||0, b||0); }, ink: function(r, g, b, a) { __cl_ink(r||255, g||255, b||255, a===undefined?255:a); return __ac_api; }, line: function(x1, y1, x2, y2) { __cl_line(x1, y1, x2, y2); }, box: function(x, y, w, h) { __cl_box(x, y, w, h); }, circle: function(x, y, r) { __cl_circle(x, y, r); }, plot: function(x, y) { __cl_plot(x, y); }, write: function(text, opts) { var x = 0, y = 0; if (opts && typeof opts === 'object') { x = opts.x || 0; y = opts.y || 0; } __cl_write(String(text), x, y); }, screen: { get width() { return __screen_w; }, get height() { return __screen_h; } }, event: makeEvent(), paintCount: 0, num: { rand: function() { return Math.random(); }, randIntRange: function(a, b) { return Math.floor(Math.random() * (b - a + 1)) + a; }, clamp: function(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }, lerp: function(a, b, t) { return a + (b - a) * t; }, dist: function(x1, y1, x2, y2) { return Math.sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1)); }, abs: Math.abs, floor: Math.floor, ceil: Math.ceil, round: Math.round, sign: Math.sign, min: Math.min, max: Math.max, map: function(v, a, b, c, d) { return c + (v - a) / (b - a) * (d - c); }, }, sound: { synth: function(cfg) { var type = cfg.type || 0; var freq = cfg.tone || cfg.freq || 440; var vol = cfg.volume !== undefined ? cfg.volume : 0.7; var dur = cfg.duration || -1; var attack = cfg.attack || 0; var decay = cfg.decay || 0; var pan = cfg.pan || 0; var id = __cl_synth(type, freq, vol, dur, attack, decay, pan); return { id: id, kill: function(fade) { __cl_synth_kill(id, fade || 0); }, update: function(opts) { /* TODO */ }, }; }, kill: function(id, fade) { __cl_synth_kill(id, fade || 0); }, bpm: function(v) { if (v !== undefined) __cl_set_bpm(v); return __cl_get_bpm(); }, time: 0, speaker: { poll: function() {}, sampleRate: 48000, waveforms: { left: [] }, amplitudes: { left: 0, right: 0 }, frequencies: { left: [] }, systemVolume: 70, }, room: { toggle: function(){}, setMix: function(){}, mix: 0 }, fx: { setMix: function(){}, mix: 0 }, glitch: { toggle: function(){} }, microphone: { open: function(){}, close: function(){}, connected: false }, sample: { play: function(){}, kill: function(){} }, speak: function(text) { __cl_log('[tts] ' + text); }, }, system: { version: 'cl-bridge', jump: function(piece) { __cl_jump(piece); }, reboot: function() { __cl_reboot(); }, poweroff: function() { __cl_poweroff(); }, readFile: function(path) { return __cl_read_file(path); }, config: { handle: '', piece: '' }, brightness: -1, battery: { percent: -1, charging: false, status: 'Unknown' }, hw: { model: '', vendor: '', cpu: '', cores: 0 }, }, wifi: { scan: function() { __cl_log('[wifi] scan requested'); }, connect: function(ssid, pass) { __cl_log('[wifi] connect: ' + ssid); }, disconnect: function() { __cl_log('[wifi] disconnect'); }, }, store: { retrieve: function(k, d) { return Promise.resolve(d); }, persist: function() { return Promise.resolve(); }, delete: function() { return Promise.resolve(); }, }, net: { preload: function() { return Promise.resolve(); }, pieces: function() { return Promise.resolve([]); }, }, clock: { time: function() { return Date.now(); } }, params: [], colon: [], needsPaint: function() {}, help: { resampleArray: function(a,n) { return a; } },};
// Performance timerglobalThis.performance = { now: function() { return Date.now(); } };"))