makepad-ports/makepad_assessment.md
andodeki 7ddd75fb70 Makepad 2.0 ports: rider, koboyo, insurance (+ HTML sources, assessments)
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.
2026-08-04 05:07:18 +00:00

24 KiB
Raw Permalink Blame History

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 (010):

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 fns (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 ~1020 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 (23 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 (36 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 (48 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 (26 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.