Franken Threed
Three.js-compatible application specializer with a Rust/Wasm + WebGPU execution core: compiles ordinary Three.js apps into specialized update kernels, packed GPU state, and persistent render schedules. Full r186 compatibility, zero feature cuts, honest 2x/3x benchmark gates.
#FrankenThreeD
FrankenThreeD is a Three.js-compatible application specializer with a Rust/Wasm and WebGPU execution core.
You give it an ordinary Three.js project. It gives you back a full-compatibility application package — a self-contained HTML file when the code and resource graph closes, or a normal networked/module build when it does not — in which the expensive, provably-closed parts of the frame loop have been compiled into specialized Rust kernels, packed GPU state, and a persistent render schedule.
Existing authoring knowledge stays useful. You keep writing Three.js.
#Honest status
Phase 0 foundation work has begun; core specializer/renderer functionality is not yet implemented.
| Question | Answer |
|---|---|
| Is there a Cargo workspace? | Yes — workspace with f3d-core and f3d-runtime crates (b43f846, 4ab9611, 8910299) |
| Is the upstream oracle pinned? | Yes — Three.js r186 pinned at commit 148ef33ecb6d2502ff796d4554abd1549c95d519 with package reconciliation evidence (5c2776d, 7a0ad38) |
| Are test evidence conventions in place? | Yes — structured event schema and test evidence helper (3772bbc) |
| Does Rust code execute in real browsers? | Yes — a real Rust program executes through the Asupersync browser host in HeadlessChrome 152 and Safari 26.5 with eight probes passing (timer wakeups, oneshot join, 1000 host turns, reentry rejection, 10k self-wakes, cooperative cancellation, region drain before close, fetch abort with server-observed disconnect) as of commits 8910299, 7cd379e, 8f1ca58 |
| Is the browser runtime dependency reproducible? | Yes — published Asupersync =0.5.0 with a checked-in registry checksum replaces the developer checkout. The browser results above predate this pin and still require a fresh browser run. |
| Is the WebGPU bridge implemented? | No — WebGPU bridge, device loss handling, and all-turn burst measurement are not done |
| Is there a compiler, renderer, or CLI? | No — compiler, WebGPU renderer, and CLI orchestration do not exist yet |
| Has the symbol/export census been run? | No — the plan defines the census; it has not been executed |
| Have benchmarks or hardware targets run? | No — physical M5 and iPhone runs are not done; zero speedup has been measured |
The plan is deliberately written to be falsifiable. It specifies kill gates that can stop this project early, and it says so explicitly. Treat every number below as a target or a budget, never as a result.
#TL;DR
The problem. A general-purpose renderer must answer the same questions on every frame: which objects share a program, which values changed, which changes affect which passes, what can be batched. A Three.js application usually has one stable answer to most of those questions, and pays to rediscover it 120 times a second.
The non-solution. Translating object.position.x = value into an equally
fine-grained Wasm call just relocates the overhead. Language substitution is not
an optimization.
The idea. Recover information across ordinary library boundaries at build time. Turn thousands of fine-grained property writes into one contiguous update kernel, legally versioned bulk uploads, and a persistent GPU schedule. Ship the answer plus a small invalidation condition instead of the question.
The most important optimization is removing a question from the frame loop.
The honesty constraint. For a simplified serial critical path, if fraction
p is accelerated by factor k and new overhead is fraction h:
speedup = 1 / ((1 - p) + p/k + h)
Even infinitely fast replacement of one part cannot produce 3× unless roughly two-thirds of the relevant critical path is removable before new overhead. That is why this project's gates are measured end to end, on real devices, against a strong reference — and why they can fail.
#The pin
| Upstream release | Three.js r186 (September 8, 2026) |
| Source commit | 148ef33ecb6d2502ff796d4554abd1549c95d519 |
The annotated tag object hash is not the source commit. Release updates are deliberate compatibility events, not automatic movements of the oracle. A new upstream version gets its own source, asset, test, and performance manifests, and old results are retained.
#What full compatibility actually means here
The scope is all existing features and functionality of r186 — the complete
source surface, not a convenient subset. That includes the ESM/CommonJS roots,
three/webgpu, three/tsl, three/addons, wildcard addon and source paths,
every loader and exporter, WebXR, WebAudio, CSS2D/CSS3D/SVG renderers, physics
wrappers, the editor, the Inspector, devtools, and TSL tooling.
#The no-cut rule
An optimization refusal is not an application-feature refusal.
"Not in our demos," "not tested upstream," "uncommon," "dynamic," and "not yet ported" are not release exemptions. Sequencing is allowed. Permanent omission is not.
#Four promises, never conflated
The single most important discipline in the project. These four statuses are tracked separately in code, tests, reports, and documentation:
| Promise | Meaning |
|---|---|
| API compatibility | Every pinned-release API contract has a working production path under its source-supported host prerequisites |
| Application equivalence | The complete application feature and control surface is preserved |
| Implementation ownership | Whether behavior executes in new Rust, retained JavaScript, or a browser adapter |
| Acceleration | The actual matched benchmark result on a named device, browser, and workload |
Passing a test through retained JavaScript is legitimate compositional compatibility. It is not evidence that a Rust implementation exists. A fast renderer does not imply that every application callback was accelerated.
#Four execution routes
| Route | May legitimately claim |
|---|---|
| Specialized WebGPU | Verified accelerated Rust/Wasm/WebGPU execution |
| General WebGPU | Working WebGPU execution when specialization is unavailable; speed measured separately |
| Retained JS / host component | Full component functionality — not a Rust rewrite |
| Exact backend component | Exact backend behavior and functional compatibility; never credited as acceleration |
The fourth route exists because some things cannot be faked. getContext()
returning a real WebGL context, shared-context resetState(), synchronous
render-target readback, native program handles, and construction-time XR
fallbacks are genuine features. A WebGPU-only imitation cannot promise their
identity or semantics — so the pinned upstream backend is retained, isolated,
version-pinned, and reported, rather than the feature being refused.
The mirror-image rule keeps that from becoming an excuse: every portable standard rendering family must also pass through the new WebGPU backend with retained renderer submission disabled. A release containing only a renamed upstream renderer fails, no matter how good its compatibility numbers are.
#Architecture
#Pipeline
Original application + pinned Three.js + declared assets
|
module resolution / source analysis
|
Three.js semantic and effect analysis
|
+------------------+---------------------+
| |
retained JavaScript compilable numeric islands
DOM / callbacks / escapes typed updates / geometry / animation
| |
+------------ semantic scene IR ----------+
|
render/compute pass graph
|
resource and capability specialization
|
application Wasm + WGSL + static host code + asset pack
|
one self-contained HTML
#Three representations, not a tower of frameworks
- Semantic IR — object identity, ownership, numeric types, update dependencies, effects, observations, source locations.
- Pass graph — resource reads/writes, attachment operations, required order, history dependencies, pass provenance.
- Execution plan — packed buffers, shader variants, resource assignments, draw/dispatch buckets, reusable bundles, a small per-frame command sequence.
Typed IDs into owned arrays. No heap-allocated trait-object graphs in the hot path. A bounded worklist optimizer, not a theorem prover.
#The unit of specialization: a closed update island
An island is admitted only when the compiler can answer: who can mutate each value including aliases; who can read intermediate state and when; which operations are pure; which numeric semantics are required; and what invalidates the specialization.
Observing a property stay constant for 1,000 frames is not proof that it is constant. A trace can suggest an island; it cannot justify erasing a possible behavior. Guards validate assumptions before affected work runs, and every optimized kernel ships alongside its conservative production path.
Write barriers are instrumented at build time on provably covered writes — not by wrapping the application in JavaScript Proxies, which would change observable behavior and add overhead to exactly the paths we want to remove.
#Workspace and budget
Eleven crates. Not dozens of tiny architectural layers.
| Crate | Responsibility | Planned Rust lines |
|---|---|---|
f3d-core |
Typed IDs/layouts, capabilities/epochs, errors, the feature/route manifest | 10,000 |
f3d-math |
Fixed-size math, SIMD batches, robust geometry primitives | 12,000 |
f3d-scene |
Packed scene state, dirty propagation, animation, query structures | 25,000 |
f3d-assets |
Asset pack, safe decoders/preprocessing, resource metadata | 20,000 |
f3d-shader |
Material/TSL/ESSL semantics and dynamic lowering | 30,000 |
f3d-graph |
Pass semantics, hazards, lifetime planning, schedule specialization | 14,000 |
f3d-gpu |
New-WebGPU resource/render/compute paths, backend boundary tests | 27,000 |
f3d-runtime |
Asupersync, lifecycle/host protocol, exact-component and tool integration | 12,000 |
f3d-compiler |
Route analysis, import closure, islands/guards/codegen | 24,000 |
f3d-cli |
build / inspect / verify / bench / serve orchestration | 7,000 |
f3d-conformance |
Feature closure, upstream/host/tool integration, performance tests | 24,000 |
| Subtotal | 205,000 | |
| Charged Asupersync browser-host work | 10,000 | |
| Planned | 215,000 | |
| Contingency reserve | 30,000 | |
| Ceiling | 245,000 |
Plus roughly 12,000 JS/TS lines for adapters, host bindings, and harness integration, and roughly 15,000 authored WGSL lines — tracked openly rather than hidden outside the Rust budget.
These are estimates, not measured implementation sizes. A forecast above the ceiling triggers a reuse decision, not a scope cut. Retained upstream modules and third-party codec/compiler code are reported separately with size and ownership.
#Dependency direction
core <- math core/math <- scene core <- assets core <- shader core/scene/shader <- graph core/graph <- gpu scene/assets/gpu + asupersync <- runtime core/scene/shader/graph <- compiler compiler/runtime <- cli public crate APIs <- conformance
Browser bindings do not leak into numerical or compiler modules. Native asset/build tooling never becomes a dependency of the deployed Wasm.
#Proposed command surface
These commands do not exist yet.
f3d inspect ./src/main.ts --format json f3d build ./index.html --compat full --standalone --out ./dist/demo.html f3d build ./src/main.ts --compat full --deployment networked --out ./dist f3d verify ./index.html --candidate ./dist/demo.html f3d verify-upstream --ref r186 --features all --tests all f3d bench ./benchmarks/headline.toml --device local f3d explain ./dist/demo.f3d-report.json f3d serve ./dist --port 8080
--compat full is the default, not a premium tier or a later phase.
--require-accelerated-webgpu is a diagnostic strict mode that fails when an
acceleration requirement cannot be met; it does not redefine the product's
functional scope.
Every build emits a report, in human-readable text and stable JSON, covering: the Three.js version and asset/module hashes; what was specialized, generally executed, retained, or kept on an exact backend; functional gaps versus host limitations versus optimization-only barriers, with no conversion between those statuses; estimated structural costs clearly distinguished from measured timings; precise source spans for specialization barriers; and what would unlock a fast path without weakening behavior.
No LLM is needed in the deployed application or the compiler correctness path.
#asupersync is the async foundation
asupersync is the sole async programming foundation for FrankenThreeD-owned work. No Tokio, no parallel executor, no unstructured promise pool.
The plan treats this as a critical-path risk, not a checked box. Before a large rendering system gets built, a real Rust-authored browser program must demonstrate: futures actually polled and producing observed results; timers and host-turn wakeups without native threads; a nonreentrant scheduler pump with bounded microtask bursts; cancellation delivered to running cooperative tasks; real fetch cancellation and cleanup; child tasks drained before scene-region teardown; no late publication into a cancelled scene; explicit behavior for unsupported hosts.
Where the browser host-services seam is incomplete, the missing host is implemented upstream in asupersync, not forked into FrankenThreeD. A closed ownership ledger is not proof that a browser callback stopped executing.
#The correctness rules that make this hard
These are the failure modes the architecture is explicitly built to catch. Each has a mandatory regression test that stays enabled in benchmark mode.
Queue writes are not per-draw snapshots. Two writeBuffer calls before one
submission can make both recorded draws read the second value. The regression:
render red to target A, mutate the shared material to blue, render to target B,
submit one combined schedule — A must stay red and B blue. The planner tracks
per-use data versions, not a last value per object per frame.
Bundles reset render-pass state. After executeBundles, cached bindings and
pipeline state must be invalidated before subsequent direct draws — even for an
empty bundle sequence. A falsely warm state cache is a rendering defect, not an
optimization.
CPU mutation and GPU-visible mutation are different histories. An attribute can change in a CPU array while the source deliberately has not requested an upload. Coalescing across gaps must not expose edits that the reference would leave GPU-stale. A scan detecting a CPU mutation is not permission to upload it early.
Synchronous work cannot acquire invisible yield points. A cooperative runtime does not authorize splitting an originally synchronous update across browser turns — that can expose half-updated state, alter Promise ordering, or deliver input inside a computation that previously ran to completion.
One authoritative owner per state region. JavaScript-owned, Wasm-owned, or mirrored under an explicit epoch contract. A mirror is not a license for two writers.
Layout is explicit. AffineRows is three vec4<f32> rows — 48 bytes. WGSL's
mat4x3<f32> is four padded columns — 64 bytes. Writing twelve tightly packed
floats into that type is wrong. Rust struct layout is not WGSL layout.
dispose() releases a residency, not necessarily the public object. A legally
reused material, texture, or geometry gets fresh backend residency without
changing its JavaScript identity, while GPU handle generations prevent late work
from targeting a recycled allocation.
The backend choice is irreversible. A renderer route is selected before its canvas, context, or device is bound. If the renderer or its native state can escape to opaque code that may call GL-specific methods, the exact component is preserved from construction.
Numeric fidelity is not negotiable for the headline score. f64 or retained
JavaScript for observable scalar operations; signed zero, exceptional values, and
observable ordering preserved. FP16, relaxed arithmetic, and changed reduction
order go in a separately labeled optional mode.
#Demonstration corpus
Eight preregistered headline workloads, chosen from the pinned example inventory before any candidate performance is known:
| ID | Upstream example | What it forces |
|---|---|---|
| H1 | webgpu_performance_renderbundle |
Existing bundles, heterogeneous geometry, per-object material data, static/dynamic toggles, a genuine WebGL backend mode |
| H2 | webgl_marchingcubes |
Rebuilt procedural geometry, normals/colors, four legacy ShaderMaterial variants |
| H3 | webgl_animation_multiple |
Multiple animated assets, independent poses |
| H4 | webgl_shadowmap_performance |
Repeated scene/shadow work, many objects |
| H5 | webgl_instancing_dynamic |
Already-instanced geometry with dynamic instance data |
| H6 | webgl_postprocessing_advanced |
Multiple effects, render targets, pass ordering |
| H7 | webgpu_skinning_instancing_individual |
Compute skinning, per-instance skeleton/morph data |
| H8 | webgpu_compute_particles_fluid |
MLS-MPM compute, atomics, indirect dispatch, runtime particle counts |
H1 already defaults to 4,000 objects with render bundles. H8 already implements multi-kernel GPU compute with atomic cell data. Render bundles, compute shaders, instancing, and WebGPU are baseline techniques, not this project's novelty. The baseline is strong on purpose.
H7 and H8 exist specifically so the suite is not just legacy object-submission bottlenecks. They may prove hard to accelerate. That difficulty is part of the go/no-go test, not a reason to remove them after disappointing results.
Additional required fidelity and control cases — published, but scored
separately — include webgpu_vxgi_sponza, SSGI/TRAA temporal effects, glTF
transmission and dispersion, clipping/stencil, compute cloth, video textures, a
deliberately GPU-bound full-screen shader as a negative control, and a small
ordinary scene as a startup/fixed-overhead control.
#Performance gates
What is measured: completed logical frames per wall-clock second in a bounded
steady-state pipeline — application updates, scene preparation, bridge work,
uploads, command preparation, GPU execution, and completion. Not the time to call
queue.submit. Not a GPU-only kernel timer. Not a matrix microbenchmark.
| Gate | Requirement |
|---|---|
| Equivalence | Visual, state, control, and lifecycle profile passes before any speedup counts |
| Eligibility | All reference-valid H1–H8 cells stay required; candidate failures never shrink the denominator |
| Headline floor | ≥ 2× median paired completed-frame throughput in every required cell |
| Stratum target | ≥ 3× geometric mean across required headline cells per device/browser stratum |
| Confidence | Paired 95% lower bounds meet the 2× floor and exceed 2× for the stratum geomean |
| Modern coverage | H7 and H8 stay in the required score |
| Tail | Frozen p95/p99 frame-time, missed-deadline, and input-response budgets |
| Controls | ≤ 5% sustained-throughput regression on valid control cases |
| Startup & memory | Frozen absolute device budgets; bounded caches; no OOM |
| Thermal | Phone floors hold in the sustained segment, not only the cool start |
| Deployment | Portable standalone passes independently |
| Ownership | Scored GPU work goes through the new renderer, not retained submission |
| Functional closure | All feature gates pass independently of the benchmark corpus |
The reference is chosen to be strong: R0 as authored, R1 a competent Three.js configuration under a frozen bounded optimization recipe, R2 an equivalent current Three.js WebGPU implementation. Selection happens by independent reference-only calibration and is frozen before scored runs, so the denominator cannot be cherry-picked from noisy trials.
Difficult control cases, regressions, startup costs, thermal behavior, and failures get published alongside the wins.
#Device matrix
| Stratum | Browser evidence |
|---|---|
| iPhone 17 Pro Max | Safari on a pinned OS/browser build; Chrome checked separately |
| M5 MacBook | Safari and Chrome, separate runs on the same machine |
| NVIDIA discrete GPU | Chrome on supported Windows; Linux as a separate lane |
| AMD discrete GPU | Chrome on supported Windows; Linux as a separate lane |
Physical devices, not emulation. No fictional "Safari on Windows NVIDIA" target. The upstream screenshot lane selects a software Vulkan ICD and special launch flags — it is a regression oracle and can never substantiate a hardware speedup claim.
#Kill gates
The project is designed to be stoppable early, before the full budget is spent.
- Foundation — real browser task execution, wakeups, and cancellation through asupersync on Safari and Chrome, plus a legal WebGPU bridge. No ledger-only "execution."
- Bridge — measure direct JS WebGPU, simple Wasm→host calls, bulk command transfer, and generated host submission on matched workloads. The chosen path must show an advantage after its own overhead.
- First product — automatically transform complete H1 and H2 with every control, backend mode, and material variant intact, and hit ≥ 2× on the phone and M5 strata against an independently selected strong reference. If H1 is not viable, the project decision is reopened rather than swapping in an easier demo.
- Generalization — repeat on held-out applications and at least one discrete-GPU stratum, with a measured end-to-end route for H7 and H8.
- Final — exhaustive functional and accelerated-rendering closure, all upstream and integration tests, all performance conditions.
If these fail, the project does not launch as a performance replacement. A foundation gate can stop development long before full compatibility is implemented.
#Phases
| Phase | Goal |
|---|---|
| 0 | Full source/feature inventory and executable foundation. Pin r186, census every export and registration, profile the strong references, freeze budgets, prove real browser task execution and the bridge counterexamples |
| 1 | Complete H1 and H2 as real applications with real, independently validated acceleration |
| 2 | Generalize aliases, effects, guards, ownership, dirty propagation, animation, and the full loader/exporter/codec inventory without losing public functionality |
| 3 | Full new-backend rendering and dynamic shader coverage, including portable legacy GLSL and runtime node registration |
| 4 | Feature, environment, and tool closure — exporters, media, XR, physics wrappers, editor/Inspector/devtools — plus demanding performance |
| 5 | Deployment and release validation: strict standalone closure, CSP, cold startup, device loss, mobile thermals, held-out workloads |
Phase 0 first. Frontier work must earn its complexity; optional research (geometric algebra, neural rendering, differentiable scene optimization, general JavaScript compilation) must never consume the compatibility reserve before the core gates pass.
#Scope boundaries
Not built here: a JavaScript VM, a browser or driver stack, a general WebGL-emulation layer, a native Metal/Vulkan/D3D backend, a physics engine, a new editor, a marketplace, a render farm, or an ML optimization service.
These are reuse decisions, not feature exclusions. Existing editor operations, physics wrappers, and legacy backend APIs are expressly in scope — they are preserved by reusing the appropriate upstream, browser, or external component, with the same engine version, inputs, solver semantics, and public wrapper behavior. An acceleration pass cannot replace Rapier/Ammo/Jolt simulation with a visually plausible approximation.
There are no permanent "core only," "no exporters," "no XR," "static shaders only," or "all the tests we happen to pass" release variants labeled full compatibility.
#Dependencies
Small, pinned, audited. First-party semantic, numerical, compiler, scene, and
resource code uses #![forbid(unsafe_code)]; the browser binding family is a
named external boundary rather than a claim that nothing beneath it is unsafe.
Allowed: wasm-bindgen/js-sys/web-sys aligned with asupersync's
boundary; shared serialization for build manifests and diagnostics (never
per-frame JSON); Naga as a normally build-time
shader compiler and validator; the existing JavaScript bundler/parser toolchain
for source ingestion; required asset codecs selected by dependency closure;
version-pinned upstream Three.js components for exact compatibility; and
test/fuzz/browser-automation tools outside the shipping path.
Not in the execution core: Bevy, a general wgpu stack, a new generic ECS,
Tokio, a mandatory Rayon pool, native C/C++ FFI, BLAS, or a Python interpreter.
Sibling Franken libraries — FrankenNumPy, FrankenSciPy, FrankenTorch, FrankenLibC — are resources, not assumptions. Reuse is decided per crate or per kernel with target and dependency checks. Importing everything would undermine both the size budget and the foundation requirement.
A note on the shader frontend: "run all Three.js GLSL through Naga" is not a plan. Naga's GLSL frontend targets Vulkan GLSL, not a turnkey WebGL GLSL ES 1.00/3.00 entrance. The ESSL preprocessing, semantic normalization, stage interface reconstruction, and uniformity work are first-party, and they are the largest single uncertainty in the budget.
#Repository layout
franken_threed/ ├── COMPREHENSIVE_PLAN_FOR_THE_DESIGN_OF_FRANKENTHREED.md # the technical constitution ├── AGENTS.md # operating contract for AI coding agents ├── README.md ├── LICENSE # MIT + OpenAI/Anthropic rider ├── .gitignore ├── .gitattributes └── .beads/ # br issue graph: the plan as beads (see AGENTS.md)
Everything else — the Cargo workspace, the pinned upstream checkout, the feature
manifest, the conformance harness, the benchmark corpus — is future work. The
work itself is tracked as beads: run br ready --json to see what is unblocked.
#Reading paths
- Evaluating whether this is worth doing: plan §1 (executive decision), §2 (what source inspection changes), §16.7 (kill gates), §20 (risks).
- Understanding the compiler: §4 (compilation strategy), §6 (IR and execution architecture), §4.3–4.5 (islands, write barriers, guards).
- Understanding the compatibility contract: §5 in full, especially §5.1 (four promises), §5.11 (full-surface scope), §5.12 (feature-family map), §5.16 (functional vs accelerated closure).
- Understanding the renderer: §8 (WebGPU renderer), §9 (materials, TSL, legacy shaders), §6.7 (queue ordering), §8.5 (hazards).
- Understanding how claims get verified: §14 (testing), §16 (performance methodology), §22 (definition of done).
- Working in this repo as an agent:
AGENTS.md.
#FAQ
Can I use this today? No. There is no code.
Is this a fork of Three.js? No. It is a compiler and execution core that consumes an unmodified pinned Three.js, retains upstream components where exact behavior demands them, and reports exactly what it retained.
Why not just use the Three.js WebGPU renderer? You should — it is one of the required baselines. Three.js already ships a WebGPU renderer, render bundles, compute-based fluid simulation, and compute-driven instanced skinning. This project has to beat that, not rediscover it.
Why not compile all of JavaScript to Rust? Because that is a different, larger, worse project. The compiler understands Three.js semantics and a restricted set of numeric operations. Dynamic UI and general language features stay in the browser's JavaScript engine, where they belong.
Why is the plan so preoccupied with what it cannot claim? Because the difference between a real result and a demo is entirely in the parts that are easy to leave out: the failed cells, the retained components, the startup cost, the thermal segment, the untested API, the control case that got slower.
Will it hit 3×? Unknown. Nothing has been measured. The plan states the gates and states that failing them means no performance-replacement launch, even if the implementation is otherwise impressive.
#License
MIT with an OpenAI/Anthropic rider. See LICENSE.