# Makepad 2.0 (dev @ `b41e740`) — Brutal Professional Assessment **Scope & evidence base.** This reviews the dev branch as attached in the workspace (sparse clone: `AGENTS.md`, `examples/splash`, `examples/uizoo`, `libs/makepad_test`) **plus a full fresh clone used for metrics** and compiler/test telemetry from building and testing three example apps against the complete tree this week. It is *not* a line-by-line audit of all 5,548 files — where a claim is telemetry-based rather than line-read, it says so. Hard numbers, all measured on `b41e740`: | Metric | Value | Source | |---|---|---| | Tree size / files | 209 MB (excl. .git) / 5,548 files | fresh clone | | Core Rust LOC (`platform+draw+widgets`) | ~283k lines in 505 `.rs` files | `wc -l` | | `platform/script` (the DSL language) | 80 files / 52,822 lines (parser.rs alone: 4,291) | `wc -l` | | `unsafe` in `platform/src` | **2,565** (~7.4 per file) | grep | | `#[test]` across the entire core | **157, in only 27 files** | grep | | `.unwrap()` in core sources | 1,562 | grep | | Compiler warnings on stock toolchain | 14 (`makepad-widgets`) + ~40 more in `draw`, incl. `f64→f32` fallbacks that rustc flags as **future hard errors** (rust-lang issue #154024) | build logs from this session | | CI configuration | **None. No `.github/`, no workflows anywhere in the tree** | `find` | | Toolchain pin / committed lockfile | **Neither** (`rust-toolchain*` absent, `Cargo.lock` gitignored) | tree | | Vendored code in `libs/` | **1.4M lines / 2,811 files** (rapier 18 MB, vulkan 13 MB, stitch 13 MB, CEF, CUDA, MLX, TTS, windows-bindgen…) | `du` | | Root-directory clutter | 9 planning `.md` docs + 5 floating `download_*.sh` blobs at repo root | `ls` | | Registration duplication | 161 `workspace.members` vs 20 `RunStudioRelease` lines in `makepad.splash` — both must be hand-edited per example | grep | | Mega-files | `vulkan.rs` 7,120 lines, `widget_tree.rs` 3,917, `text_input.rs` 3,574, `portal_list.rs` 3,463 | `wc -l` | Scorecard (0–10): | Axis | Score | One-liner | |---|---|---| | Architecture | 6.5 | Ambitious, coherent layers — undermined by an interpreted DSL whose correctness lives in a markdown file | | Code quality | 5.0 | Competent, idiomatic Rust in places; crushed by scale per file, warning debt, and test scarcity | | Bugs / correctness | 4.0 | **dev head does not compile headless-Linux at all**; emulator-grade traps in the widget kit | | Performance | 7.5 | Rendering engine is world-class; repo/tooling ergonomics (clone size, boot, build) are not | | Design (API/DX) | 4.5 | Ten undocumented-or-markdown-documented sharp edges, several of them silent-failure | | Security / safety | 3.5 | Documented memory-layout footgun for GPU buffers, floating multi-GB downloads without checksums, massive unaudited vendored surface | | Tooling & CI | 2.0 | No CI, no pin, no lockfile, warning debt — the supply side of this project is running on vibes | | **Overall** | **4.7** | **A research-grade engine with production-grade ambition and zero production guardrails** | The good, stated once: the render stack (shader VM, retained areas, batching, hot reload) is genuinely the most advanced in the Rust-UI space; the cross-platform breadth (desktop, mobile, WASM, OpenXR, CEF) is real code, not vaporware; `examples/splash`/`uizoo` contain zero `.unwrap()` (the demos are cleaner than the framework); the new `makepad_test` studio-protocol harness is a legitimately good idea; and publishing an `AGENTS.md` contract for AI contributors is forward-thinking. None of that excuses what follows. --- ## 1. Architecture **A1 — The framework's correctness is enforced by a Markdown file, not the type system.** `script_mod!` waits until *app startup* to parse the UI: every DSL mistake (`=` instead of `:`, wrong key name, missing scope) is a runtime boot error. That's the deliberate hot-reload tradeoff, but the safety fallback is **855 lines of rules for humans/agents** instead of compiler guidance. Concretely, `AGENTS.md` rule 16 states that field order in `#[repr(C)]` draw-shader structs is a **memory-safety contract**: the system "uses an unsafe pointer trick in `DrawVars::as_slice()` that reads contiguously past the end of `dyn_instances` into the subsequent `#[live]` fields… [misordering] will **corrupt the GPU instance buffer**." A "Rust is safe" framework whose GPU buffer integrity depends on developers reading a rulebook is a documented footgun, full stop. The fix (derive-time layout assertions) is a weekend of proc-macro work; it hasn't been done. **A2 — A 53k-line bespoke language is the price of admission.** `platform/script` (parser.rs 4,291 lines + VM + test app) is the core of 2.0: a whole scripting language whose tokenizer quirks are normalized as usage rules — `#2ecc71` fails because `2e` parses as scientific notation ("write `#x2ecc71`"), and comments before the first token shift all error columns because "Rust's proc macro token stream strips comments entirely." These are *bugs documented as features*; every downstream team will rediscover them one cryptic error at a time. **A3 — Split-brain event architecture.** Two lanes: low-level `event.hits_with_capture_overload(cx, area, …)` vs high-level widget `finger_down(actions)` — and **`Event::FingerDown` doesn't exist at all** on the enum; you must know to ask for `Hit::FingerDown`. Worse, a `View` only *emits* `FingerDown` if a `cursor` is set: **cursor styling gates input semantics**. A tooltip changed your clickability. This cost real debugging time in two separate ports this week. **A4 — Layering discipline inverts at the edges.** `libs/` — nominally third-party vendored code — contains 1.4M lines including ML inference (`diffusion`, `mlx`, `tts`, `cuda`), a physics engine (`rapier`), and a browser (`cef`). These are *example-app dependencies living in the platform repo*: every `cargo metadata`, clone, index, and audit pays for demo assets. Meanwhile `studio/` (the design tool) is compiled into the same workspace as `widgets`, so example apps that import widgets transitively configure the studio protocol — which is also the test harness's control channel. **A5 — Duplicated registries.** Example crates must be added to `Cargo.toml workspace.members` **and** `makepad.splash` **and** use a `#[makepad_test]` package convention; 161 members vs 20 splash items shows the two lists already drift arbitrarily. One source of truth generated from Cargo metadata is the obvious fix AGENTS.md already half-implements by policy. **A6 — Headless is a figurative platform, not a real one.** There *is* an `os/headless` backend — and dev `@ b41e740` **does not compile it on Linux**: `gl_render_bridge` un-gated, `can_play_type_impl` missing the headless arm, `libc_sys`/`ipc`/`dma_buf` modules absent under headless, `shared_framebuf` importing them unconditionally, `HeadlessLoadedModule` missing `symbol()`, `Cx::is_draw_shader_window_ready` unimplemented. Five separate compile breaks = nobody has built this configuration in CI. (See §3, B1.) ## 2. Code quality - **Q1 — File-scale pathology.** `vulkan.rs` at 7,120 lines, a 4,626-line *test* `main.rs` in script, 3.9k `widget_tree.rs`. These aren't modules, they're walls. Review and blame archaeology are dead on arrival. - **Q2 — Warning debt on the hot path.** 14 warnings in `makepad-widgets` alone, including `#[live(4.0)]` picking up the deprecated `f64→f32` fallback the compiler explicitly schedules as a future **hard error** (chart.rs:383-385, per rust-lang#154024). When rustc flips that switch this crate stops compiling. This is known, dated breakage sitting in tree. - **Q3 — Panic-as-control-flow at scale.** 1,562 `.unwrap()` in core sources. Not fatal in demo apps; in a rendering framework each is a potential app-crash under user data — and with no fuzzing or property tests in tree, they're uncharacterized. - **Q4 — Tokenizer debt is user-facing.** The `#x` hex prefix rule has a second edge nobody documents: it applies only when a *digit directly precedes `e`* (`#2ecc71` fails, `#e8926a` passes — verified empirically here). Developers will hit the unlucky combinations at random. - **Q5 — Leaky core abstractions.** `PortalList::next_visible_item` **over-requests one item past the declared range end**; index blindly and panic (verified in the rider chat list — the crash is in *their* pattern, not user code). `ids!(var)` in a loop silently hashes the *literal* string `"var"` — compiles clean, returns the wrong widget, no error. Dynamic lookup requires `&[var]`, discovered via "unused variable" warnings, not docs. - **Q6 — Doc coverage is uneven by an order of magnitude.** `widgets` has ~1,846 doc lines for 1,079 `pub fn`s (fine); `platform/os/headless` and the script VM's public surface are essentially uncommented. The README is marketing + socials; the deep contract lives in a file addressed to language models. - **Q7 — Repo hygiene as quality signal.** Root hosts `aigame.md`, `handoff.md`, `talk.md`, `splashgame.md` etc. — sprint notes shipped as project surface, next to 5 floating download scripts. This is what "no release process" looks like. ## 3. Bugs (ranked — upstream, verified this session unless noted) - **B1 (Critical) — dev head does not build for headless Linux.** Six independent failures (listed in `patches/makepad-dev-headless-linux.patch`, which this workspace ships): missing `cfg` gate on `gl_render_bridge`; missing headless arm in `cx_api::can_play_type_impl`; three missing module shims (`libc_sys`, `ipc`, `dma_buf`) under `os/mod.rs`; unconditional imports in `shared_framebuf.rs`; unimplemented `symbol()` on `HeadlessLoadedModule`; unimplemented `Cx::is_draw_shader_window_ready()`. Shipping a workspace where a documented platform doesn't compile means the platform matrix isn't gated *at all*. - **B2 (High) — Warning class scheduled to become fatal.** `f64→f32` implicit fallback in `#[live]` defaults (≥2 sites in chart.rs, plus draw-glyph/draw-pbr warnings). Silence today, hard error on a future toolchain the project doesn't pin (and can't pin — no `rust-toolchain`). - **B3 (High) — `PortalList` range overrun** (see Q5): core list widget emits item ids outside the declared range; every consumer must defensively `.get()` or panic. This belongs in the widget, not in 100 downstream guards. - **B4 (Medium) — Cursor-gated hit testing** (A3): a `View` with no `cursor` and no animator is click-invisible. Undocumented in code; breaks the mental model "drawn area == hittable area". - **B5 (Medium) — Comment-stripping breaks diagnostics**: leading comments or blank lines inside `script_mod!` desynchronize error spans (the fix "start with real code" is legislated in AGENTS.md rule 18 instead of the tokenizer tracking trivia). - **B6 (Medium) — Hex/scientific-notation collision** (rule 19): tokenizer wart preserved as permanent API surface via the `#x` escape. - **B7 (Medium) — Boot-order fragility**: the widget tree builds lazily, so boot-time `ui.widget(cx, …)`/`view(cx, …)` borrows return nothing and any unconditional unwrapping crashes; consumers must poll/guard. Also: `.view()` and `.widget()` return *different* sets (`ViewSet` vs `WidgetSet`) — a custom widget queryable via one and silently absent via the other. - **B8 (Low) — Test-harness locator semantics**: `makepad_test` `click()` always targets widget center, `wait_visible` doesn't check viewport containment, no offset/click-at-point primitive; failure artifacts are good (`failure.txt`/`widget-tree.txt`) but the panic line (`runtime.rs:1374`) is the only stack context. - **B9 (Low) — Floating example assets**: `download_tts.sh` pulls `ggml-large-v3-turbo.bin` from a HF `resolve/main` URL — content changes underneath you; reproducible example builds are impossible by construction. ## 4. Performance - **P1 (Strength) — The render architecture earns its reputation.** Shader- driven immediate-mode-over-retained-areas, draw-call batching, per-area invalidation, hot reload, GL/Vulkan/DX/Metal backends, headless raster. UI rendering performance is not the problem. - **P2 — Everyone pays the ML tax.** `libs/` vendoring (`cuda`, `diffusion`, `mlx`, `tts`, `rapier`, `cef`, `windows-bindgen`) inflates clone to 209 MB and metadata/graph resolution for *every* consumer, including the phone- sized demo apps that use none of it. - **P3 — Boot cost discipline is absent.** Because the DSL is interpreted at startup, app boot includes full script parse+link; in the headless harness each app instance (with protocol handshake) costs **~10–20 s** cold. That also defines the floor for any startup-time-sensitive product — and it's unmeasured anywhere in tree (no startup benchmark exists). - **P4 — Build-time is the real DX bottleneck.** From-scratch `cargo check` of a two-example workspace on this sandbox ran into tens of minutes (instrumented indirectly via long build phases this session); with no CI caching artifacts and no sccache config committed, every contributor eats it raw. - **P5 — Test parallelism defeat**: `#[makepad_test]` boots one child app per test, serially in practice — the 9-test rider suite needs ~3 minutes. With ~zero pure unit tests upstream (§2), the only green/red loop is slow by construction. - **P6 — No perf guardrails**: no frame-time harness, no allocation tracking, no per-commit renderer benchmarks. A framework that sells performance should have a `benches/` that CI enforces; there isn't one. ## 5. Security & safety - **S1 — GPU buffer integrity by convention** (A1, AGENTS.md rule 16). `DrawVars::as_slice()` performs a documented out-of-slice contiguous read into trailing fields; correctness depends on field *order* in user structs. This is memory-unsafety reachable from perfectly ordinary app code, mitigated only by "read the doc". Any safety-branded framework must encode this invariant in the derive macro — compile error, not markdown. - **S2 — Unsafe surface with no audit scaffolding.** 2,565 `unsafe` occurrences in `platform`, no `#[deny(unsafe_op_in_unsafe_fn)]` policy, no Miri/ASAN config in tree, no security.md, no audit comments linking to invariants. For OS interop some of this is necessary — but there's no evidence of a systematic review trail. - **S3 — Supply chain: floating, unverified multi-GB fetches.** `download_cef.sh` (spotify CDN), `download_map.sh` (geofabrik), `download_tts.sh`/`download_voice.sh` (HuggingFace `resolve/main`) — no pinned versions, no SHA-256, no signatures (verified in the scripts themselves: straight `curl`/`wget`). One poisoned CDN account away from shipping blobs into user apps. - **S4 — Studio protocol exposure.** The studio hub and the test harness speak a control protocol into the app process (stdin/stdout in `makepad_test`, hub channels in `studio/`). There's no visible auth or scoping in-tree; treating a design-time channel as implicitly-safe is how debug endpoints end up in production builds (cf. every framework that learned this the hard way). - **S5 — Vendored 1.4M lines of third-party code without provenance tracking.** No `vendor.lock`, no audit metadata, no automated `cargo audit` equivalent that can even *see* these. Vulnerabilities in the vendored `windows` bindgen / `rapier` / `stitch` snapshots are invisible to the entire Rust security tooling ecosystem by construction. - **S6 — No lockfile committed for a 161-member workspace** (`.gitignore`: `Cargo.lock`). Consumers resolve fresh dependency graphs per build; combined with no toolchain pin, "works on my machine" is the *designed* state. - **S7 (positive)** — Example app code (splash/uizoo) avoids `unwrap` entirely and the script engine has its own dedicated test app (`platform/script/test`, 4.6k lines): the language authors test their language. The rest of the org does not test theirs. ## 6. Design (API & developer experience) - **DX1 — The 855-line AGENTS.md is both the project's best document and its worst smell.** It exists because the failure modes are real: it's a catalog of every sharp edge formalized as user obligations (rules 16/18/19 quoted above, the `let X = #(X::register_widget(vm))` incantation, `instance()`/`uniform()` shader-only, `theme.` prefixes, `+:` merge). Good frameworks remove edges; this one documents them and continues. - **DX2 — Silent-failure ergonomics.** `ids!(var)` (Q5), `.view()` vs `.widget()` set difference (B7), mod.widgets assignment *not* bringing a widget into scope (triageable only via "variable X not found in scope" much later), cursor-gated actions (B4): a consistent pattern — the wrong code compiles and runs, just misbehaves. - **DX3 — Naming/macro ceremony.** `live_id!`, `ids!`, `id!`, `live_id_unchecked`, `#[makepad_test]`, `app_main!`, `script_mod!`, `script_shader!`, `register_widget(vm)`, `WidgetSet` vs `ViewSet` vs `WidgetRef` — the conceptual surface per trivial app is far above egui/dioxus-tier, and migration from 1.x `live_design!` is breaking with no codemod in tree. - **DX4 — Test authoring is the best DX in the repo** (once discovered): `Selector::id(...).click(); app.press_key(...)` — but it needs click-at-point, viewport checks, and faster boots to graduate from "usable" to "good". Notably, the only real test culture visible on dev lives in *example apps*, not the framework. - **DX5 — 1.x→2.0 doc gap.** Search engines serve makepad 1.x `live_design!` answers; 2.0 changed the shape of everything and the only authoritative source is in-repo. Every external LLM, blog, or tutorial mis-teaches 2.0 users today. - **DX6 — `makepad.splash`/`Cargo.toml` dual registration** (A5) and the 20-of-161 splash coverage: the example discovery story is itself unenforced. --- ## 7. Execution plan — make this codebase actually improvable Written to be actionable from two seats: **the Makepad team** (upstream) and **a consumer running a private fork** (what this workspace can do today). Rough effort assumes a small team. ### Phase 0 — Supply-side hygiene (2–3 days; fork can do all of it) | # | Task | Acceptance | |---|---|---| | 0.1 | Commit `rust-toolchain.toml`, un-ignore `Cargo.lock` for workspace+examples, add `audit.toml`/`cargo-deny` | Reproducible builds; `cargo deny check` green | | 0.2 | **Stand up CI** (GitHub Actions matrix: Linux/macOS/Windows × {x11, wayland, **headless**}): `cargo check --workspace`, run `platform/script/test`, run `makepad_test` suites for 3 example apps | dev head must compile every declared platform; B1 class of bug becomes impossible to merge | | 0.3 | Zero-warning policy: fix the 54 warnings incl. `f64→f32` live defaults (B2); add `-D warnings` to CI | Tree compiles clean; future-hard-error class extinguished | | 0.4 | Pin + checksum all `download_*.sh` assets (URL → version + SHA-256 manifest), move to `xtask` | Tamper-evident asset fetches (S3) | | 0.5 | **Upstream the five headless-Linux fixes** from `patches/makepad-dev-headless-linux.patch` as one PR with a headless CI leg proving it | Patch deleted locally | ### Phase 1 — Kill the documented footguns (3–6 weeks) | # | Task | Acceptance | |---|---|---| | 1.1 | Encode the draw-shader layout invariant **in the derive**: generated `const _: ()` asserting no non-instance data after `DrawVars`/instance fields; `DrawVars::as_slice` gets `unsafe_op_in_unsafe_fn` + SAFETY comments (S1) | Misordered struct = compile error; AGENTS.md rule 16 deleted as obsolete | | 1.2 | Tokenizer fixes: treat trivia (comments) in span tracking (B5); disambiguate `e` in hex without `#x` — and make `#x` a warning-free alias for one release (B4/DX4) | `#2ecc71` parses; rule 19 removed | | 1.3 | Compile-time DSL validation: parse `script_mod!` in the proc-macro as far as possible (head/token/`=` vs `:` checks) with rustc diagnostics; runtime errors only for true dynamic features | Common DSL mistakes fail at `cargo check`, citing file:line | | 1.4 | Hard-error the silent failures: `ids!` of a non-literal when a same-named var is in scope = error suggesting `&[var]`; `View` without cursor that has finger handlers = warning; `.view()` vs `.widget()` hints in error text | Every trap from §2 produces a message at the point of mistake | | 1.5 | `PortalList` contract: never yield out-of-range item ids (B3) + regression test in-tree | `.get()` guard no longer required downstream | ### Phase 2 — Test & quality architecture (4–8 weeks, overlapping) | # | Task | Acceptance | |---|---| | 2.1 | Test-floor policy: every `widgets/src` widget gets at least smoke coverage; grow core `#[test]` count from 157 toward a per-file baseline; property tests for script parser/VM (corpus exists in `platform/script/test` — generalize) | CI reports coverage; trend enforced non-decreasing | | 2.2 | `makepad_test` v2: click-at-point, viewport-visibility semantics for `wait_visible`, parallel app boots, boot-time budget alert (~>5 s/instance fails perf gate), richer failure diffing (current text artifact is fine, add tree diff vs expectation) | Rider-scale suite (9 tests) < 60 s wall | | 2.3 | Renderer benchmark harness (`benches/`): frame time on canonical scenes, allocation regression gate, startup-time measurement (P3/P6) | Perf numbers on every PR | | 2.4 | Unsafe audit program: categorize the 2,565 with `cargo geiger`, Miri on `platform/script` VM + `shared_framebuf` + draw-vars paths; SAFETY-comment lint | Audit report committed; Miri leg in CI | | 2.5 | Clippy pedantic on changed code baseline; mega-file roadmap (split `vulkan.rs`, `widget_tree.rs`, `portal_list.rs` behind submodules — no behavior change) | Largest file < 2.5k lines | ### Phase 3 — Repo & ecosystem surgery (2–6 weeks) | # | Task | Acceptance | |---|---| | 3.1 | Move `libs/` ML/CEF/demo stacks into a separate `makepad-extras` repo (or crates.io deps); core clone back to <60 MB | Clone time / `cargo metadata` drop visibly (P2) | | 3.2 | Vendor manifest for what remains (`vendor.lock` with upstream commit + license), license audit of the whole tree | SBOM generated in CI (S5) | | 3.3 | Root cleanup: planning docs → `docs/internal/` or wiki; single registration source (`xtask` generates splash run-items from workspace metadata) | A5 resolved; no dual entry (DX6) | | 3.4 | Authoritative docs: rustdoc for `widgets` + `platform/script`, published per-commit; AGENTS.md content rendered into the human docs (it's currently agent-only); 1.x→2.0 migration guide + codemod script | DX5 closed; `docs.rs`-grade reference | | 3.5 | Studio protocol: audit surface, default-off in release profiles, scoped loopback + token for test channel (S4) | Release builds ship no open control plane | ### Phase 4 — What the fork in this workspace should do *now* (days) 1. Carry the five headless patches until Phase 0.5 lands (already done). 2. Keep the three test-suffixed example crates as the project's accidental "integration lab": they already exercise PageFlip, PortalList, custom draw shaders, script_apply_eval, hit capture, and the test harness — formalize them as the fork's regression suite for upstream bumps. 3. Gate any upstream re-sync on: clean `git apply` of patches, all 140 port tests green (currently: rider 21+9+9, koboyo 29+14+8, insurance 28+14+8), zero new warnings. ### Definition of done (framework-level) 1. `dev` compiles on every platform it names, in CI, with `-D warnings`. 2. No memory-safety invariant lives only in documentation (S1 closed). 3. A new example app can be built by reading rustdoc alone — 0 AGENTS.md rules required to avoid silent traps (DX1/DX2). 4. Test culture shift measurable: core `#[test]` count ×10 at minimum and CI-time <15 min with caching. 5. Clone-and-build to running `uizoo` from a cold machine <10 min. --- ### Bottom line Makepad is **the best Rust-UI render stack wrapped in research-lab engineering discipline**. The engine deserves the ambition; the repository doesn't currently deserve users. The single highest-leverage change costs almost nothing — CI + warnings + the headless fix (Phase 0) — and would have prevented every upstream bug hit in this workspace. Everything durable beyond that is one theme: convert obligations currently printed in `AGENTS.md` into compiler-enforced invariants, and the framework grows from "impressive demo platform" into something teams can bet on.