Adds feature-gated optional deps and re-exports (makepad-test, makepad-csg,
makepad-gltf, makepad-mbtile-reader, makepad-fast-inflate) so a downstream
workspace can depend on makepad-widgets as its sole Makepad source.
Measured first: the tilted start view carried 15.1 MiB of u16 and
11.6 MiB of u32 indices against 77.3 MiB of vertices, the u32 premium
5.7 MiB, all on the big tiles (fill up to 149 k and casing up to 98 k
vertices). Streams now split at bake time into triangle-safe chunks of
fewer than 65,536 vertices, one geometry per chunk (at most four on any
tile, 191 duplicated vertices in 10.9 M), so u32 leaves the typed
streams. Amsterdam start view: tilted 114.3 -> 108.6 MiB (fill 28.4 ->
24.7, casing 40.4 -> 38.4), flat 311.7 -> 287.7 (fill 83.9 -> 67.3,
fringe 57.1 -> 51.6); the harness prints vertex/index counts and widths
per stream. Same triangles in the same order; Metal hidden grab within
label noise of the head without it, web GPU gate clean (IDX1 lane).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A round cap was a hard disc fan of up to 32 vertices per road end with no
anti-aliasing; a Butt/Square start pushed a feather pair it never
indexed. The cap is now carried by the last body pair extended by half a
width, with uv.y = 1 + cap_axis, and the fragment computes the circle's
coverage with the same derivative-based AA as the band edge. Amsterdam
start view: casing 42.5 -> 40.4 MiB (shape 100 17.1 -> 16.5, shape 110
15.2 -> 14.5), tilted total 116.4 -> 114.3 MiB, flat 313.9 -> 311.7.
Cap edges gain AA; Metal hidden grab vs the head without it 1.23 % /
0.27 % start (label placement in the crops), web GPU gate clean
(CAP1 lane).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A wall edge was eleven f32 (44 B): both ends, base, height, normal, AO,
colour, z-bias. It is now i16 ends, u16 centimetre heights (max error
8 um in the fixtures), f16 AO and z-bias ticks and a UNorm8x4 colour; the
normal is derived from the edge in the shader with the courtyard winding
preserved. Both the wall draw and the projected wall-shadow draw read the
new record. Amsterdam start view: wall_inst 19.3 -> 8.8 MiB, tilted total
127.0 -> 116.4 MiB, flat unchanged. Metal hidden grab within noise of the
head without it (0.99 % / 0.27 %); web GPU gate clean (WALL2 lane).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* remote: honor MAKEPAD_REMOTE in requested(), and hush the close notices when the bridge is off
`requested()` only scanned argv, while `requested_bind()` also reads
MAKEPAD_REMOTE. So `MAKEPAD_REMOTE=1` started the bridge but everything
keyed off `requested()` still said no. Just delegate, so there's one
answer to "did this process ask for the remote bridge".
Also stop printing `[makepad-remote] user closed window ...` to stdout
from every app on every window close -- that line is for the agent
driving the app, so only print it when the bridge is actually up. The
log ring still gets it either way, for /log.
* devtools: put the F10/F12/Shift+F12 overlays behind an opt-in
Three dev tools are currently wired into every app with no way to turn
them off, each on a bare function key:
F10 the exploded draw-list view. Intercepted in
Cx::call_event_handler *before* the app's handler, and once
it's up it also eats Escape, the arrows, +/-/0, I and H --
no modifier needed -- plus every drag outside the flat band.
F12 the design tweaker, a child of every Window. Once it's up it
swallows every pointer event over the body, so the app looks
frozen to the mouse.
Shift+F12 the screen recorder, which starts writing mp4s to disk.
None of that is something a shipped app wants a user to find by accident,
and there was no flag, env var or property to stop it.
So: one gate, platform/src/devtools.rs. `--devtools`, or
MAKEPAD_DEVTOOLS=1, and --remote implies it since the /snap + /click loop
drives the tweaker. An explicit MAKEPAD_DEVTOOLS=0 wins over all of it,
which also keeps the off path testable under --remote.
Only the hotkeys are gated, not the tools. Cx::sploded_toggle,
set_tweak_on and ScreenCap::toggle are untouched and still public, so an
app that wants any of this puts it on a key of its own choosing -- that's
the app deciding, rather than a key nobody knew was bound.
Gating F10 and F12 is enough to reach all of it: everything else these
two claim sits behind `sploded.active` / `tweak_is_on()`, and with the
hotkeys gated the only remaining ways in are the /tweak routes (already
--remote, which implies devtools) and an app's own call.
* text_input: drop the Ctrl+Enter submit clause again
`|| mods.control` made Ctrl+Enter submit a multiline input. On
Linux/Windows that's already what is_primary() means, so it changed
nothing; on macOS it turned Ctrl+Enter from "insert a newline" into
"send", which is a surprise in the middle of a chat app's composer.
The comment right above it already described the old behavior, so this
puts the code back in line with it.
Every floating POI or charger marker in 3D mode drew two crossed wall
quads on the 48-byte generic layout, and every signal a pole plus three
light quads; together they were the whole 17.1 MiB fill_3d_misc stream.
They are now one 24-byte instance each (anchor, arm/height, colour,
z-bias) over one shared stalk template and one shared stoplight template
per tile, drawn by a prop draw struct on the same lighting path.
Amsterdam start view: fill_3d_misc 17.1 -> 0.0 MiB, stalk_inst 0.1 MiB
(4,887), stoplight_inst 0.2 MiB (1,866); tilted total 143.7 -> 127.0 MiB,
flat unchanged (313.9). Metal hidden grab within the label-noise band of
the head without it (1.78 % / 0.42 % start); web GPU gate clean
(STALK1 lane).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Cx::memory_budget_bytes() is set once at startup from the device
(ProcessInfo on macOS/iOS, ActivityManager on Android, GlobalMemoryStatusEx
on Windows, /proc/meminfo on Linux, the wasm maximum on the web) through
one policy: desktop keeps 1536 MiB unless RAM is under 8 GiB (then RAM/4),
mobile takes RAM/4 clamped to 384..1536 MiB. The map's four byte budgets
are fractions of it (upload 1/64, pending 1/4, tile cache 25/32, HTTP
cache 5/32; the tile cache caps at 1/2 on web heaps under 1 GiB) instead
of constants. At 1536 MiB they are the binary sizes 24/384/1200/240 MiB,
about 5 % above the old decimal constants. Harness totals unchanged; Metal
grab and web GPU gate within noise (BUDGET1 lane).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Shape-0 Boolean union faces (plazas, road bodies, tunnel and bridge
faces) leave the 28-byte road vertex for FaceVertexTyped: i16 anchor,
UNorm8x4 colour, F16x2 class/material + depth tick, F16x2 deck lift +
coverage. The road shader draws them through a second vertex format on
the same pass; face streams use u16 indices. Amsterdam start view:
casing 73.6 -> 58.7 MiB (42.5 casing + 16.2 face), tilted total
158.6 -> 143.7 MiB, flat 328.8 -> 313.9, 433 ms/tile. Metal hidden grab
within the noise floor of the head without it (1.26 % / 0.23 %, label
placement); web GPU gate clean. Faces now draw as one batch per tile
before the stroke bands, so a translucent shadowed face over a crossing
casing blends ground instead of stroke at that pixel — not visible in
the crops (FACE1 lane).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Per-layer tag arenas and views replace a HashMap per feature, the tag
whitelist is applied while parsing, and the worker's output scratch
buffers are reused. Release, Amsterdam: mvt-parse 377,074 -> 3,071 and
detail-merge 2,713,553 -> 19,817 allocations (135x fewer); parse median
5.9 -> 1.8 ms, merge 63.6 -> 9.7 ms; bake 429 -> 344 ms/tile, totals
unchanged (158.6 MiB tilted). Buffers, labels and icons byte-identical
across the 25 fixtures; Metal grab within the noise floor (0.9 % / 0.1 %)
of the head without it; web GPU gate clean (ALLOC1 lane).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The worker drain runs once per frame and uploads until a 6 ms / 24 MB
budget is spent (nearest tiles first). Its continuation was a redraw,
and a Draw never re-enters handle_event, so the rest of the queue waited
for an unrelated timer (2-16 s in the web demo). When tiles remain the
drain now also asks for a next frame; that event arrives after the pass
and drains again. Real GPU, no probe, no input: all 25 start-view tiles
inserted between 2 and 4 s at 60 fps (DRAIN1 lane + continuation fix).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A new binary in tools/map_tiles streams an .mkmap shard by shard into a
new archive: every tile's protobuf is decoded, the osm_* tag tables are
rewritten to the reader's key whitelist, the field-101 shadow sections are
stubbed (regions and building groups stay byte-identical), field 100 is
kept, and the tile is re-brotli'd with the archive's own codec and
dictionary; leaf directories and root.mkidx are rebuilt, output is
deterministic and resumable per shard with a sidecar manifest, --tiles
limits a run to a Hilbert range or a z/x/y list, --dry-run reports and
--verify decodes both archives and checks the policy. On the 25 Amsterdam
start-view tiles: 95.1 -> 47.6 MB decoded, 30.6 -> 16.7 MB compressed.
A label sort tie-break makes the bake byte-deterministic for the parity
test.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The fringe stream (AA skirts around the road-union faces) is drawn only
below 25 degrees of tilt, yet every tile baked it: 57 MiB of the Amsterdam
start view that the tilted demo never drew. The bake now takes a fringe
flag the way it takes the 3D flag: wanted while the tilt is under 30
degrees, dropped only above 40, decided in the frame loop next to the
2D/3D flag and re-baked through the same restyle path with a full road
core rebuild; the harness prints both the tilted and the flat totals.
Harness: 215.5 -> 158.6 MiB for the 25 start-view tiles tilted (fringe 0),
328.8 MiB flat (fringe 188.3), 429 ms/tile.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Roads move from eight f32 lanes to a 28-byte RoadVertexTyped (i16 anchor
at 1/64 tile unit, f16 offset / depth / uv pairs, unorm8 colour, f16
params, exact f32 deck), ground fills and roofs to 16 bytes (i16 anchor,
unorm8 colour, f16 params; u16 depth ticks for fills, exact f32 height for
roofs); indices are u16 whenever a stream has fewer than 65,536 vertices.
The three shaders read the typed POD fields directly (the fetch converts),
uploads go through Geometry::update_typed with the shader's layout, the
space-warp subdivision decodes / interpolates / re-encodes the typed
records, and a tile that lands before its shader has drawn waits in the
pending queue instead of panicking. The shader compiler types a packed
field access as vec2f / vec4f, and the Metal name table gains the packed
short / ushort / uchar / char vector names.
Harness: 249.6 -> 215.5 MiB for the 25 start-view tiles, 384 ms/tile.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Draw shaders can declare geometry POD fields as F16x2/F16x4, U16x2/I16x2
(plain or normalised) and UNorm8x4/SNorm8x4; the fetch converts them to
vec2f/vec4f on Metal and WebGL2 (per-attribute vertexAttribPointer with
type, normalised flag, byte stride and offset), the headless JIT decodes
the same formats, and Vulkan/OpenGL/D3D11 refuse compact layouts with a
logged skip rather than drawing garbage. Attribute packing computes
physical offsets with natural alignment for the whole record; the all-F32
path stays byte-identical (stride = slots * 4, packed_geometry_N on GL).
Geometry::update_typed / update_typed_with_recycled_buffers take
IndexData::{U16,U32} plus a byte vector and the shader's input layout;
updates are validated (stride, whole vertices, index range) and every
geometry carries its index width and a layout signature that survive
releasing the CPU staging, so a draw checks layout and index type against
the resident buffers, never the staging enum. Compact fields are rejected
in instance PODs and in nested aggregates at shader validation; SNORM
minima clamp to -1 like WebGL2.
On Metal the decoded (logical) vertex is reached through the shader
context like the instance is, so every shader function that reads
geometry compiles.
No map stream is converted yet; that is the next cut.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Lifted shape-0 roofs leave the 48-byte universal vertex for a five-slot
RoofVertexPacked (anchor, unorm8 colour, exact metre height, f16
material | zbias ticks); DrawMapRoof reconstructs the fixed vector
channels and shares the colour pass, the fade, the LOD band and the
shadow-mask projection / footprint cut-out. Roof records that need more
(parapet AO depth variants, marker stalks) stay on the generic layout in
a fill_3d_misc stream. The tree and signal contact shadows are no longer
tessellated ten-segment discs in a vertex stream but four floats per disc
drawn as an instanced unit quad into the shadow mask, with the same
radial ramp.
Harness: 281.5 -> 249.6 MiB for the 25 start-view tiles, 387 ms/tile.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
std's wasm allocator spins, so the Atomics.wait that killed one WebGL2
run in four came from std Mutex / Once contended between the browser main
thread and a tile worker. Every lock the UI thread shares with a worker
(task pool state, cancellation generation, task results, the scheduler,
the web request table, the archive read states and shard file cache) now
goes through lock_from_ui, which spins on try_lock on wasm32 and is a
plain lock elsewhere. The icon mesh cache, icon slot table and tag-key
whitelist are warmed on the UI thread before the tile pool starts so no
Once can wait either.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Casing, stroke and fringe records leave the 48-byte universal vertex for
an 8-slot road layout: full-precision anchor and deck height, f16 offset /
depth / tessellator uv pairs, unorm8 colour, and one f16 integer that
packs class, material, dash id, kind (stroke / union face / fringe) and an
explicit expanded flag. DrawMapRoad reproduces DrawMapVector's expansion,
terrain, tilt, space-warp and shadow-mask behaviour for those streams;
casing/stroke draw through it in the colour passes, the fade cross-fade
and the shadow-mask projection, and the space-warp subdivision has a
road-stride midpoint.
Harness: 339.0 -> 281.5 MiB for the 25 start-view tiles, 437 ms/tile.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The bake no longer unions every building's projected roof and silhouette
into a material-6 ground fill per tile (122.5 MiB of the Amsterdam start
view, plus the b-shadow bake laps). MapView renders one child pass per
frame: wall records swept along the sun as instanced quads, roof/deck
projections of the lifted geometry, footprint cut-outs, and the tree/signal
contact discs (split from the icon stream); every ground shader samples the
mask at its screen position with the same alpha the decals used.
Metal returns the pass texture bottom-up, so the sample is flipped per OS
(WebGL2 flips its render-to-texture projection instead). The draws inside
the mask pass unbind the mask sampler (feedback loop). The compact fill
shader sets the shadow varyings so land and water are shadowed too.
Harness: 475.5 -> 339.0 MiB for the 25 start-view tiles, 411 ms/tile.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The omarchy bar is the caption strip and the frame is fully
client-sized there, so a WM window had no window controls at all. The
bar's right cluster now ends in three of our SVG buttons after the
status modules — minimise, maximise (restore once maximised), close —
wired to the window's own minimize, maximize, restore and close;
hover washes the slot, the close carries the accent, tooltips name
them, and the drag query answers Client over them. macOS keeps its
traffic lights on the left. The gallery shows them on every platform.
wm 155; the WM checks for windows-msvc, linux-gnu and wasm32. A real
click on Windows is the box's to prove.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Decision 17: the chat never sits over the app. Under the WM the pane is
a left column of the desk row — its walk width is the reserved strip,
eased from nothing to the card plus a gap as it slides, the card
right-aligned in the strip so it comes in from off the desk's left edge,
its edge on the right — and the desk's layout takes what is left, tiles
snapping to it while the strip moves the way they follow a drag. In a
standalone Window the F10 slot does the same: it reserves the strip as
the body's left inset every frame, lays the body out beside it, and
reads the app's own ground colour after its retint. The background
switcher's index lives on App, not in a static.
Verified by hidden grabs: a terminal full width; F10 → the pane on the
left and the desk narrowed by 450 px with the terminal filling it; F10
→ full width again. sheets standalone: F10 → the grid starts right of
the card, nothing over cells; F10 → back. wm 152, widgets 138 (+ the
two known widget_tree failures), sheets 84; the WM checks for wasm32.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
libs/app_module (makepad-app-module) is the contract (aicontrol §3):
AppModule (id, label, register into an isolate, an OpenSchema, create
into InstanceParts, capabilities), InstanceHandles (an InstanceScope
owner token, the storage jail as a cx.storage namespace, the viewport,
a ReplySink for calls that finish later), a ServiceExecutor addressed
apart from the root, and an OpenSchema that refuses raw paths — a file
is a handle the host issued or nothing. Only types live there.
The WM hosts one (module_host.rs): allocate the isolate, retint its
theme from the palette, register, create — one trusted entry into the
isolate, never a second &mut Cx beside the VM; teardown drops the root
first, runs shutdown in the isolate, frees it. MpModuleView is the tile
for an instance's root, drawing and dispatching with the isolate
INSTALLED on Cx (enter_isolate/leave_isolate, new in widgets) and
gating keys on the WM's focus; a TileHost trait covers both tile kinds
so the desk and the focus logic never ask which. The registry overlay
(apps.rs) links modules per app-* feature; the desktop default stays
Process, switched per app in ~/.makepad/wm/apps.splash or by a dev
--module flag. The bus gains its in-process leg: an instance is an
m<id> endpoint the pane addresses like any other — the leg the web
superbuild runs everything on.
sheets is lib + bin: SheetsModule mints MpSheets{} in the isolate and
answers sheets.summary on its executor; the standalone binary keeps its
Window and F10 overlay behind the standalone feature.
The drive found a platform bug: a shader compiled on a widget minted in
an isolate hit the draw-shader object cache by object index alone, so
an isolate's Button reused the main heap's entry and drew with zero
instance slots. The cache is keyed by (heap, object) in every backend,
and add_instance refuses a zero-slot draw call instead of dividing by
it. Isolate cost, measured (the P2 entry gate): 12 ms and ~8 MB per
isolate, flat to 32 — no pre-warming needed.
Verified by hidden grabs under the WM: /os.launch sheets → an
in-process Sheets tile (no child process), the apps row Desktop ·
Sheets, /sheets.summary answered through the in-process leg, a warm
terminal beside it, F10, close → the instance torn down and its isolate
freed. wm 145, sheets 84, app-module 3, services 33; platform and draw
check for windows-msvc, linux-gnu and wasm32.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every Window now carries an AiChatSlot beside the tweaker: zero cost
while off, inert when the window manager hosts the process (the WM's
pane is the chat then), and on the first bare F10 it instantiates the
chat's module root BY NAME — mod.widgets.AiChatOverlay — which exists
when the app links makepad-aichat and calls its script_mod; an app that
does not gets one log line. The overlay slides in from the right over
the body with the WM pane's motion, owns the pointer inside its rect and
the keyboard through its composer, keeps ticking while hidden, and draws
in the window's retained overlay list so it composites over the deferred
body. Requests from outside the tree ride Cx globals, never statics.
An app exposes itself with one call: AiServicePort::open(cx, manifest)
is the hosted transport under the WM and, standalone, an in-process link
parked on Cx (PendingServiceLinks) that whichever chat root is up adopts
into its registry — the overlay today, the superbuild's pane later. The
registry wakes the UI when it sends, so an in-process call is answered
without waiting for a pointer event. NoModel leaves the engine feature
so a build without a model runtime still answers honestly and keeps the
tool console; aichat's engine is a default-on feature.
The bridge: Cx::ai_callback beside tweak_callback, /ai?on=1|0, /ai?say=…
and /ai/transcript (the overlay publishes the transcript as JSON after
each state change), installed by makepad_aichat::script_mod.
sheets links aichat and exposes sheets.summary (name, used range, header
row, selection) through the port: standalone the overlay answers it,
under the WM the bus does.
The local model no longer loads on a registration: its session starts
on the first user line, so an app joining the bus costs nothing.
Verified by hidden grabs: sheets standalone /ai?on=1 → the overlay with
the summary card and no model; F10 closes it; under the WM the sheets
tile shows no overlay and F10 toggles the WM pane. widgets 135 + 4
(two widget_tree find_within tests fail before and after this change),
aichat 33, sheets 83 + 2, services 29; widgets and aichat check for
wasm32.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>