feat(framework): add Octane as the third first-class framework - #203
Merged
Conversation
Octane (React's programming model, compiled) joins Solid and Vue Vapor: universal-driver renderer over the native tree, full 15-component surface, use-prefixed frame hooks, app.octane.tsx variants for all 8 demos, playground in-browser compilation, byte-exact PPSSPP goldens, and a three-framework PPSSPP benchmark (docs/bench/) per the PR #6 methodology. The PSP host gains an arena-pressure GC; framework=octane builds stub octane/profiling to dodge a WeakMap pinning pathology in the pinned QuickJS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ly -O0 QuickJS builds Real-hardware testing showed hero-octane under 1 fps. Two root causes: 1. Every PSP QuickJS build was -O0: CRATE_CC_NO_DEFAULTS=1 drops the cc crate's synthesized flags including build.rs opt_level(), and the pin predated quickjs-rs PR #6 anyway. -O2 now lives in TARGET_CFLAGS and libquickjs-sys is repinned to master (ba5bdd0d) registry-wide (psp + vita Cargo, psp/symbian toolchain manifests, pin tests). 2. An Octane state commit replays the whole root regardless of where the state lives (~2-4ms desktop, engine-bound on PSP), so per-frame state is unaffordable there. Continuous motion now rides the native channels: sprite atlases (hero/gallery/library spinners via sprites.json + a new spinner-atlas.svg), baked keyframe timelines (music equalizer, apps/music/pocket.config.ts), animate()/jump() one-shots (stats systems reveal), ref-based phase timers (notifications, library loading), and a new setTextContent() escape hatch - the text-shaped sibling of animate() - for count-ups and percentages (stats tiles, music progress). useSpriteAnimation also stops committing state on ticks that do not change the visible sprite frame. PPSSPP avg frame work, before -> after (budget 16.7ms): hero 387.8 -> 6.5ms, stats 279.9 -> 9.2ms, music 282.7 -> 12.9ms, notifications 271.2 -> 13.8ms, library 50.8 -> 4.9ms, settings 27.4 -> 14.5ms, cards 9.7 -> 6.6ms. All three frameworks' e2e suites pass; only the six goldens whose pixels legitimately changed were re-baselined. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…+ blog to post-fix numbers - tools/gen-demo-covers.ts bakes the pocket app-family cover convention for every showcase demo: ICON0 as a geometry-drawn family tile, PIC1 as a real sim-pumped frame of the app with the XMB legibility dim. apps/<demo>/psp/ fragments apply to every framework build of that demo. - docs/bench: archive the 2026-07-30 post-optimization dataset (geomean avg_work octane 1.66x vs solid, was 15.58x; all 21 cells inside the 16.7ms budget) alongside the pre-fix 07-29 dataset. - blog: add "The hardware said no" chapter (the -O0-since-2021 discovery, replay-scope-independence, the native-channel rule, per-demo before/after), refresh the benchmark section to the new dataset with a linear-scale chart, and re-count byte-identical goldens (14/23). - playground compiler-entry merges the music demo's equalizer keyframes into the playground-wide animation theme. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No repo-internal references, leads with the machine and the three findings; post dated to its publish day. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
doodlewind
marked this pull request as ready for review
July 30, 2026 01:03
…pinner atlas The svg baker scans shapes flat, so the spinner atlas's per-cell <g transform="translate(...)"> was silently dropped - all eight frames baked into cell 0 and cells 1-7 shipped blank. On hardware the spinner sat invisible and "flashed" once per cycle when the frame index wrapped through the one painted cell; the golden re-baseline had baked the bug in. The atlas now carries absolute per-cell circle coordinates (no groups), and bake-svg refuses <g transform> loudly instead of mis-baking. All four affected goldens re-baked and visually verified: the spinner is present at a distinct phase in every capture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dent The post-fix builds are verified on the same physical PSP that failed the first run; the sprite-cycling bullet now carries the honest beat about the <g transform> atlas bake and the golden re-baseline that hid it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Octane (https://github.com/octanejs/octane — React's programming model, compiled; by Dominic Gannaway) joins Solid and Vue Vapor as a first-class PocketJS framework. This is the React-model path PR #6 concluded was impossible with original React: Octane's universal client target compiles JSX + hooks into static host plans + dynamic slot batches executed by a
UniversalHostDriver— no DOM, no VDOM — which maps directly onto the nativeui.*tree.What's in here
Adapter (
framework/src/*octane*)renderer-octane.ts— the "pocket" universal driver: host command batches (create/update/insert/move/remove/destroy) applied to the shared NodeMirror arena viaapplyProps;#texthosts throughcreateTextNode/replaceText; portals as a driver capability minting overlay hosts transactionally. Re-exports the host-neutral universal ABI (octane/universal/native— QuickJS-safe).components-octane.tsx— the full 15-component surface (View/Text/Image/Sprite as hand-lowered universal plans; Screen/Focusable/FocusScope/FocusGrid/ActionHandler/Portal/Modal/ActionBar/Grid/Lazy/Gallery as Octane function components).frame-octane.tsx/lifecycle-octane.ts—useFrame,useButtonPress,useSpriteAnimation(use-prefixed: the Octane compiler slot-keys custom hooks by theuse[A-Z]call-site convention; non-hook names silently collide their internal slots).index-octane.ts— render/mount; the frame handler drains Octane's microtask renders inside the frame viaflushUniversalSync, so state commits land in the same tick as their input.Compiler/build
jsx-plugin.ts: thirdFRAMEWORKSentry,OCTANE_RENDERER_DESCRIPTOR, octane compile branch (collector pass on pristine source →octane/compileruniversal lowering), generalizedframeworkVariantPath(app.octane.tsxsiblings), cache-key includes the octane version.pocket.jsonschema ("framework": "octane"), package exports./octane/*, tsconfig paths.The memory hunt (the fun part) — octane demos initially died mid-run on PPSSPP (
InternalError: out of memorywith megabytes of arena free, opaquenullexceptions, wedged frame loops). A scratch cargo probe pinned to the exact libquickjs-sys revision (eval the real bundle + stubui+ frame loop + heap histograms + agc_decrefroot dump) unpicked a three-layer cause:trackedComponentsWeakMap is written even afterprofiler.stop()— with per-render closures as values.wm.set(key, {back: key})never collects; bellard master has the same code).Fixes, in order: framework=octane builds alias
octane/profilingto a no-op stub (framework/src/octane-profiling-stub.ts— also removes per-render profiler work from the frame budget); the PSP frame loop gained an arena-pressure GC (JS_RunGCwhen a frame leaves the bump pointer >256 KiB past the last collection — steady-state Solid/Vapor guests never trigger it); and the Octane ports keep always-animating state in leaf components, committing state on visible steps (music'sEqualizer/ProgressLinesplit, stats'StatTiles). Along the way the PSP host's exception logger learned to print tag + message + stack (anullexception is QuickJS's no-room-for-the-Error-object throw), and one solid golden (hero.boot, a single background pixel ±2) was re-baselined — it predates this branch; this is the first full solid e2e since the #149-era goldens.Result: all three frameworks pass byte-exact PPSSPP e2e. A real-hardware test then exposed the second act (commits
4e097c0,f0b8d12): hero-octane ran under 1 fps on a physical PSP. Two more root causes fell out — every PSP QuickJS build since 2021 had silently been-O0(CRATE_CC_NO_DEFAULTS=1drops the cc crate'sopt_level;-O2now lives inTARGET_CFLAGS, and libquickjs-sys is repinned registry-wide), and an Octane state commit replays the whole root at scope-independent cost, so per-frame state is unaffordable regardless of leaf placement. Continuous motion now rides the native channels (<Sprite>atlases, baked keyframe timelines,animate()/jump(), ref-based phase timers, and a newsetTextContent()escape hatch for per-frame text). Per-replay engine residue remains a tracked quickjs-rs follow-up, but replays are now interaction-rate, not frame-rate.Demos — all 8 Vue Vapor demos got
app.octane.tsxtwins (hero, cards, stats, library, settings, notifications, music, gallery), sim-verified text/pixel parity against the vapor variants, PPSSPP byte-exact goldens intests/goldens/psp-octane/(7 bench-table demos), plus newtests/goldens/psp-vue-vapor/coverage.Playground + docs — third framework toggle with in-browser octane compilation (pure-JS compiler, no WASM shim needed),
pg/runtime-octane.jsbundle, import-map entries;frameworks.mdreworked for three frameworks, octane fences added to all 29:::framework-codeblocks, README/DESIGN/RUNTIMES/home/INTEGRATION updated.Benchmark —
tools/bench-ppsspp.tsregained the PR #6 cross-engine axis:--frameworks=solid,vue-vapor,octane, per-(app, framework) grouping, geomean-vs-baseline ratios with optional--bootstrapCIs;tools/bench-chart.tsrenders the matrix SVG. Full three-framework results below.Benchmark (PPSSPP software renderer, per PR #6 methodology)
7 samples per (app, framework) cell across the 7 bench demos, deterministic PPSSPP headless (PPSSPP
676724ee5e, base4e097c0), geomean-vs-Solid ratios with mulberry32-seeded 5000-resample bootstrap 95% CIs. Full report + chart indocs/bench/(three-frameworks-ppsspp-2026-07-30.{md,json,svg,png}; the pre-fix 07-29 dataset is archived beside it); raw samples reproducible via the specs table intools/bench-ppsspp.ts, and the report is rebuildable from a raw dump with the new--from-raw=<raw.jsonl>mode.Average frame work by app:
Every cell of all three frameworks is inside the 16.7 ms 60 fps budget (the
-O2fix also roughly halved Solid's and Vue Vapor's own numbers). The honest remaining gaps: Octane runs 1.2–3.1× Solid per app, and a button press is one root replay ≈ 150–250 ms on the PSP — the replay walk's identity-path re-resolution (13k calls/replay) is the upstream Octane follow-up, alongside the quickjs-rs ephemeron repair.React status: unchanged from PR #6 — original React still has no viable PSP path; Octane is the React programming model that does run, measured above.
Authoring notes (also in docs/frameworks.md)
setState(a same-frame handler write would be clobbered by a stale read).stylewhose value changes across renders (unchanged values diff away and are safe).mount(App)— JSX in a call-argument arrow is a fail-closed universal-target error.Validation
bunx tsc --noEmitcleanbun run testgreen end-to-end (includes the newtests/octane-smoke.test.ts+hero --framework=octanebuild in the chain)tests/goldens/psp/,tests/goldens/psp-vue-vapor/(new),tests/goldens/psp-octane/(new);hero.bootre-baselined (single background pixel ±2, pre-dates this branch)docs/bench/tools/gen-demo-covers.tsbun site/build.tsgreen — playground ships currentpg/runtime.js/pg/runtime-vue-vapor.js/pg/runtime-octane.js4e097c0<g transform>silently dropped by bake-svg) fixed in569d626🤖 Generated with Claude Code