Three single-file HTML apps ported to Makepad 2.0 (dev b41e740) script_mod! DSL: - rider (ride.html): 21 unit + 9 integration + 9 headless UI tests - koboyo (koboyo.html): 29 + 14 + 8 - insurance (insurance.html): 28 + 14 + 8 All 140 tests green. Includes headless-Linux platform patch, full README, setup script, and three brutal codebase assessments with execution plans.
24 KiB
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.rsat 7,120 lines, a 4,626-line testmain.rsin script, 3.9kwidget_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-widgetsalone, including#[live(4.0)]picking up the deprecatedf64→f32fallback 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
#xhex prefix rule has a second edge nobody documents: it applies only when a digit directly precedese(#2ecc71fails,#e8926apasses — verified empirically here). Developers will hit the unlucky combinations at random. - Q5 — Leaky core abstractions.
PortalList::next_visible_itemover-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.
widgetshas ~1,846 doc lines for 1,079pub fns (fine);platform/os/headlessand 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.mdetc. — 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): missingcfggate ongl_render_bridge; missing headless arm incx_api::can_play_type_impl; three missing module shims (libc_sys,ipc,dma_buf) underos/mod.rs; unconditional imports inshared_framebuf.rs; unimplementedsymbol()onHeadlessLoadedModule; unimplementedCx::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→f32implicit 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 — norust-toolchain). - B3 (High) —
PortalListrange 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
Viewwith nocursorand 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
#xescape. - 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 (ViewSetvsWidgetSet) — a custom widget queryable via one and silently absent via the other. - B8 (Low) — Test-harness locator semantics:
makepad_testclick()always targets widget center,wait_visibledoesn'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.shpullsggml-large-v3-turbo.binfrom a HFresolve/mainURL — 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 checkof 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
unsafeoccurrences inplatform, 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(HuggingFaceresolve/main) — no pinned versions, no SHA-256, no signatures (verified in the scripts themselves: straightcurl/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 instudio/). 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 automatedcargo auditequivalent that can even see these. Vulnerabilities in the vendoredwindowsbindgen /rapier/stitchsnapshots 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
unwrapentirely 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),WidgetSetvsViewSetvsWidgetRef— the conceptual surface per trivial app is far above egui/dioxus-tier, and migration from 1.xlive_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.tomldual 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)
- Carry the five headless patches until Phase 0.5 lands (already done).
- 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.
- Gate any upstream re-sync on: clean
git applyof 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)
devcompiles on every platform it names, in CI, with-D warnings.- No memory-safety invariant lives only in documentation (S1 closed).
- A new example app can be built by reading rustdoc alone — 0 AGENTS.md rules required to avoid silent traps (DX1/DX2).
- Test culture shift measurable: core
#[test]count ×10 at minimum and CI-time <15 min with caching. - Clone-and-build to running
uizoofrom 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.