Vec::pop inverted CreateWindow/prepare/IME sequences; VecDeque pop_front matches causal order, and SetTopmost defer no longer livelocks on an empty Windows queue.
Co-authored-by: jasonqiu <jasonqiuchen@outlook.com>
A Rust panic raised while handling an iOS event unwinds out of the
`extern "C"` ObjC callback that delivered it, so the process aborts with
a bare SIGABRT. Phase-2 unwinding pops every frame between the panic and
that callback, and the default panic hook writes to stderr, which goes
nowhere on a device. Nothing about the panic survives: the TestFlight
report that prompted this shows only `abort` under
`-[UIWindow _sendTouchesForEvent:]`, with no message and no panic site.
* Install an iOS panic hook in `Cx::event_loop`, mirroring the Android
one, that logs payload, location, thread and backtrace through
`error!` (NSLog on iOS). Hooks run at the panic site before unwinding
starts, so this captures the location the abort would otherwise erase.
* Wrap the app callback in `IosApp::do_callback` in `catch_unwind`.
That's the one choke point every ObjC callback funnels through, so it
covers touches, presses, timers, draws and text input at once. It also
has to be the place: `do_callback` `take()`s the callback and only
restores it on the normal path, so catching any further out would
leave the app alive but permanently inert.
* Restore `Cx::executor` in `event_loop`'s callback even when a spawned
task panics. It's `take()`n the same way, so without this the catch
above would turn one abort into an `unwrap` panic on every later
event. The panic still propagates once the executor is back.
macOS and tvOS take the executor the same way, but neither catches, so
the process dies either way and there's nothing to restore it for.
The headless backend compiles each shader to a cdylib with `rustc -O` and
writes it to a path keyed by the hash of the generated source — then
recompiles all of them from scratch on the next process start, ignoring
what it just wrote. For host_launcher that is 68 shaders and 38.7s of
startup, paid again by every process. A headless test suite starts one
process per test, so it was paying it 55 times.
Reuse the dylib when it's already there, falling back to a compile if it
won't load or has no version symbol (truncated by a killed run, built by
another toolchain). Compiles now land on a pid-private path and get
renamed into place, so a crash can't leave a half-written dylib behind.
Cold start goes 38.7s -> 1.2s.
Two more things that only bite headless:
- The BGRA->RGBAf32 texture conversion cache was rebuilt per frame, so
the whole glyph atlas was re-converted on every draw: 430ms of a 840ms
frame. It already carries a signature and honours pending updates, so
it was always meant to outlive a frame — park it on CxOs. 430ms -> 29ms.
- Headless dpi was pinned at 2.0, i.e. 4x the pixels through a SOFTWARE
rasteriser. Still 2.0 by default, since a screenshot should match what a
display shows, but MAKEPAD_HEADLESS_DPI=1 lets a suite that only asserts
logical geometry do a quarter of the work. Raster 385ms -> 96ms.
Together a steady frame goes 840ms -> 127ms. Also reports texture time in
the MAKEPAD_HEADLESS_PROFILE line, which is how the atlas cost showed up.
* Android: silence unused VA/OpenXR warnings
Exclude desktop-only va_dmabuf_modifier from Android/OHOS builds, cfg-gate
gpu_texture pool imports, and fix OpenXR repaint locals unused without Vulkan.
* Move D3D11 texture COM calls into os/windows helpers for windows_strip
* regenerate windows-rs by windows-strip
* Windows: use overlapped custom chrome with extended client area
* Windows: fix overlapped chrome init sizing and avoid DWM work on every NCCALCSIZE
---------
Co-authored-by: jasonqiu <jasonqiuchen@outlook.com>
`Area::Rect` holds a `rect_id` into its draw list's `rect_areas`, plus the
`redraw_id` of the draw that created it. A widget that hands out its area and
is then redrawn with fewer rect areas leaves that id past the end, so
`clipped_rect()` panicked with an out-of-bounds index (seen from robrix as
"the len is 105 but the index is 173" while hit-testing a message's children).
`rect()` already guarded this with `redraw_id`; `clipped_rect()`, `abs_to_rel()`
and `set_rect()` did not. All four now check the generation first and use
`get`/`get_mut` instead of indexing, falling back to the same values they
already return for an unknown area.
* fix a pile of splash script-vm bugs: newline statements, short-circuit args, tail calls
went through the script VM and parser and fixed a batch of correctness bugs
that were biting the launcher's mini-apps:
- newline-delimited statements: a `(` or `[` at the start of the next line no
longer greedily glues onto the previous value as a call/index. leading infix
operators and `.` still continue the expression (the shader DSL needs that),
and the divert is suppressed inside ()/[] groupings.
- short-circuit `&&`/`||` used as a call argument no longer loses its value to
nil when the jump skips a multi-op right-hand side.
- a call as the very last statement of a script actually executes now, in both
end-of-parse unwind loops (also patched a zero-offset ShortCircuitEnd).
- custom widgets that deref to a base with a #[source] field now forward
script_source, so script_apply_eval works on them instead of silently no-op'ing.
plus regression tests for the newline and short-circuit cases.
* harden splash isolates: scoped timers, net gating, effective-visibility snapshots
isolate-safety work so mini-apps can't reach outside their sandbox:
- isolate-safe script-timer dispatch hook + gc for stale timers
- gate net.socket_stream on the net runtime being present
- widget-tree snapshot reports effective visibility (a widget counts as hidden
if any ancestor is hidden)
- macos_activate_app (plus a headless no-op) so the launcher can focus itself
* widen the host->splash surface: splash setters, view/glassbutton script calls
everything the host needs to poke into a running mini-app's script:
- Splash: call_script_fn, set_script_global, set_allow_net, and a cached body id
so host->script calls don't rescan for the body every time
- View.set_visible and GlassButton set_text/text are callable from script now
- makepad_test learned right-click (secondary button) so the headless tests can
exercise long-press / context menus
* fix small-size glass lens + sdf box degeneration, warn on missing glyphs
visual correctness fixes we kept tripping over:
- cap the gauss lens band at 35% of the surface's smaller side so tiny discs
degrade gracefully instead of smearing
- clamp the Sdf2d.box (and box_x/box_y/box_all) radius so an oversized radius
saturates at a circle instead of collapsing into a rotated diamond
- log once per codepoint when no loaded font can render it (was silently
drawing .notdef boxes)
* guard stale rect areas in clipped_rect/abs_to_rel/set_rect against out-of-bounds panics
* add switch_finger_capture to hand a live finger capture between widgets mid-drag
* add promote_finger_capture_over: hand a child-grabbed finger up to a co-capturing container
* splash: add validate_splash_body, a dry-run eval for externally-sourced scripts
evaluates a body in a throwaway isolate with the exact prefix/limits the
Splash widget uses and returns the captured script errors instead of logging
them. lets hosts that install source from outside (downloads, AI generation,
user input) reject bad scripts with real errors to show or feed back, where
the widget's own eval silently keeps the old view.
* strip mod.res from splash isolates; document validate_splash_body caveats
the res module's handles reach both the filesystem (abs_path loads) and the
network (web_url / http resources) without going through the gated net
runtime, so a 'no-net' isolate could still fetch and exfiltrate. found by an
adversarial review of AI-generated app installs, but it applies to any
untrusted splash source.
also note on validate_splash_body that the instruction limit bounds compute,
not heap growth, and that top-level timers live until isolate reclamation --
same exposure as actually installing the source, so validation adds nothing
new.
* splash: jailed per-app file storage (mod.fs inside isolates)
mini-apps get an OS-style private data directory, like an android app's
internal storage or an iOS container: the app sees a filesystem rooted at
"/", and that root IS its host-assigned sandbox directory
(Splash::set_sandbox_dir / SplashRef forwarder). registered as mod.fs in
isolates -- deliberately shadowing the stripped real fs module, so inside
an app "the filesystem" simply is the jail:
fs.read fs.write fs.append fs.exists fs.remove fs.mkdir fs.list
containment lives entirely in the host layer:
- lexical path resolution against the root; `..` above the root, NUL, deep
or overlong paths are errors before any I/O
- the per-VM root is rust state keyed by the isolate's heap -- script code
can neither read nor retarget it
- symlink defense in depth: nothing here can create links, and every
existing component under the root is verified non-symlink before use
- quotas: 1MB/file, 16MB/jail, 256 entries
- no root assigned (previews) -> every call errors cleanly
validate_splash_body gives dry runs a throwaway jail (temp dir, removed
after) so top-level fs.read boot loads validate instead of erroring. roots
are dropped with their isolates in the gc.
unit tests cover the containment: traversal/absolute/backslash escapes,
depth/name caps, and the symlink block.
* splash: put the jailed fs module in scope as a bare name
app scripts say fs.read("/x"), but the eval prefix only used the widgets
prelude, so bare fs resolved to a not-found error value and every storage
call failed silently. bind it in the prefix (let fs = mod.fs) for both the
plain and net variants; a script reassigning fs only shadows its own name,
the jail stays host-side.
* splash storage: quota + boundary hardening from adversarial review
three confirmed jail findings:
- mkdir bypassed every quota (target + create_dir_all, no jail_usage check)
-> unbounded inode/dir-metadata exhaustion on the shared host volume.
now charges new dirs against MAX_ENTRIES via missing_entries(); write's
entry check does the same so a deep write can't overshoot the cap either.
- write/append/mkdir lacked remove's root guard: fs.write("/", data)
resolved real == root and reached create_dir_all(root.parent()) -- one
dir above the jail (the shared app_data/). now rejected like remove does.
- validate_splash_body's scratch jail used a predictable temp name created
with create_dir_all (would follow a planted symlink out of temp). now an
exclusive create_dir on a per-process+vm name (EEXIST-safe against a
planted entry), reclaimed via gc before the dir is removed so a top-level
timer can't resurrect it.
unit tests added for missing_entries; the containment tests still pass.
* splash: empty set_text tears down the isolate instead of no-oping
set_text("") was a silent no-op (eval_body early-returns on an empty body),
so a reused Splash that goes back to empty -- the widget-gallery live preview
on Back -- left its old isolate running its timers (and holding a storage-jail
binding) behind a blank view. now an empty body reclaims the isolate: the
isolate-minted view is replaced with a fresh empty one built in the main vm
BEFORE the isolate heap is freed, then the isolate is gc'd (stopping its
timers, dropping its jail root); vm_id resets to MAIN so a later non-empty
set_text allocs a fresh isolate as before. the existing host_launcher
teardown call sites (widget picker back()/reset()) become correct unchanged.
* overlay: composite glass in draw order, not creation order
every gauss/glass surface opens its own draw list and registers it in the
window's single Overlay via store_sub_list, which hands out the first free
slot and keeps it for the life of the process. renderers walk that table in
index order, so the paint order of all glass in an app was the order the
surfaces were first *created* — permanently, with freed slots reused by
whatever registered next. draw order never came into it, so a widget rebuilt
after a layout change, or a panel opened later, could land on top of anything
drawn after it. the only workarounds available to apps were "don't draw the
thing that's winning", which looks like a bug.
the hook for fixing it was already there and unused: CxDrawList's
draw_item_reorder, honoured by every backend (metal, d3d11, opengl, vulkan,
web_gl, headless raster). so stamp each overlay sub-list with the position it
was begun in this frame (Cx2d::overlay_seq, reset in Overlay::begin) and have
Overlay::end stable-sort the table by that stamp.
this also gets parent-then-child right without special cases, which matters
because glass.GlassButton / glass.GlassSegmented call begin_overlay_reuse
unconditionally instead of checking is_drawing_overlay(), so they hold their
own slots rather than riding their parent's.
* glass.GlassSegmented: size segments to their labels, add set_selected
three things, all of them things that looked broken to a user:
- segments were width/count, so "Max" got the same room as "Default": the long
word crowded, the short one floated. each segment is now measured (DrawText
layout size_in_lpxs) and gets its text plus padding, with leftover width
shared equally so every label keeps the same margin. if the labels don't fit,
the padding shrinks (never the text) to a floor. the pill's x/width are
computed in rust and passed as uniforms since they can't come from a segment
count any more, and hit-testing is a boundary lookup rather than a division.
- `selected` was public but the pill is drawn from a private sel_pos that only
followed it via the click animation, so restoring a saved value from code
left the control showing one segment while reporting another — and a click on
the segment it really held was then ignored as "already selected". that reads
as the control eating your clicks. set_selected keeps both in step.
- the travel easing was 0.30, which arrived before the eye could follow it.
0.16.
* text_input: re-layout when max_lines changes
the laidout text was cached on width alone, so flipping draw_text.max_lines
at runtime (collapsing a composer to one line) kept the old multi-row layout
and the field never shrank. make max_lines part of the cache key.
* text_input: add set_max_lines instead of making callers script it
applying script to a TextInput re-applies its #[live] fields, and text is
one of them, so toggling max_lines through script_apply_eval! silently
wiped whatever the user had typed. give it a typed setter.
* text_input: don't drop the layout in set_max_lines
clearing laidout_text there leaves the field with no layout for the rest
of the event batch, so every cursor op in that window bails out with
"can't move cursor because layout was invalidated by an earlier event".
since set_max_lines gets called from focus/blur handling, that window is
exactly when you're clicking into the field — so the click placed no
caret at all. max_lines is already part of the layout cache key, so the
next draw re-lays out on its own.
* text_input: add scroll_to_top
for a field that folds to a fixed height when it loses focus: the scroll
offset survives the blur, so a draft last edited near its end folds
showing whichever line the caret had scrolled to rather than its first.
leaves laidout_text alone — scrolling doesn't change the layout, and
dropping it would break every cursor op for the rest of the event batch,
same trap as set_max_lines.
* text_input: add set_height
for a composer that folds to one line when it loses focus. pinning the
height is the safe way to fold — unlike clamping max_lines it leaves the
laid-out text alone, and the laid-out text is what maps a click to a
caret position. fold by re-layout and the press that re-focuses the
field resolves against the folded layout while the expanded one is on
screen, so the caret and any drag-selection land on the wrong text.
* text_input: add take_key_focus, which actually shows the caret
the caret draws as (1.0 - blink) * focus, and both come from animators
that only move when the widget is dealt a Hit::KeyFocus. setting key
focus on a field that ALREADY holds it dispatches no hit — so a field
that was focused, then hidden (hiding doesn't clear Cx's key focus) and
shown again comes back typable but with no caret and no selection
highlight, animators still parked where the last focus-lost left them.
plays focus.on unconditionally rather than only when focus changed:
repairing the case where it did NOT change is the entire point.
* splash: name scripts in errors, and stop using line as an identity slot
a runtime error from a Splash app logged `:1804943384:12 - widget has
no uid`: empty file, and a "line" that is really a pointer address.
the format is {file}:{line}:{col}, and both fields were casualties of
the same hack — ScriptMod.line carried self_id so the body could be
found again (m.line == self_id && m.file.is_empty()), while ip_to_loc
adds that same field to the script's real line when reporting. so every
location came out as real_line + a pointer, and nothing said WHICH app.
identity moves to module_path, which nothing else reads for these
bodies, freeing line to be a line. file gets a real name via a new
set_debug_name the host calls with the mini-app's id.
the validator's ScriptMod gets the same treatment — its errors are
shown to the user AND fed back to the agent as repair input, so a
location offset by a vm id was actively misleading there.
* splash: document the constant offset in reported script lines
the host prefix is two lines, so a reported line is two ahead of the
app's own file. it can't be zero — a zero-line prefix would share line 1
with the app's first line, and that line is always the // name: header,
which would comment the prefix out.
* script: stop silently losing widgets emitted from branches and loops
Splash mini-apps kept rendering nothing from on_render closures with zero
errors logged. Bisected live and in pure-VM probes, this was a pile of
separate bugs in the same corner:
- an if/else whose branch emits a widget lost the taken true branch: the
statement's POP_TO_ME got fused onto the else tail, which the IF_ELSE
jump skips. Generalized last_short_circuit_target into last_jump_target
and record every branch join (if/elif/else, match, try/err), so the
commit lands standalone AT the join and every path runs it. Same disease
as the short-circuit-argument bug, new jump sites.
- elif never patched its arm's IF_ELSE jump (relative 0), so a taken arm
spun the interpreter until the instruction limit killed the whole entry.
elif now desugars into else { if ... } via IfElseExpr, which also gives
its arms the join treatment.
- for x in <non-iterable> silently skipped the body where the equivalent
while errored; now raises "for loop source is not iterable" (nil and
empty sources stay silent). for k v in <number> passed key/index swapped
and lost k after the first iteration (the advance only rebound v).
- a line-leading { after a value-ending line glued onto the previous
expression as a proto instantiation; it now starts a new statement,
same divert rule the ( and [ newline fix added.
- int literals were second-class numbers: U40/I32/F32/F16/U32 didn't
collapse to the number bucket in to_redux, so 6 .is_number() missed
method dispatch entirely and an int arg against a float default failed
with "expected number, got number". They're all just numbers now.
Regression tests in tests/on_render_emission.rs cover each shape, incl.
the exact calendar/weather patterns that were blank in the launcher.
* splash: keep an on_render closure's final widget, and say when a render fails
Two host-side halves of the emission-loss story:
- the parser turns a closure's last statement into its return value, so a
render closure that ENDS with a widget literal built it and then threw it
away (this is why wrapping a whole render in one extra View{} produced
nothing). script_result now pushes a returned widget object into me as
the last child; non-widget returns still get skipped downstream.
- a render closure that errors mid-run used to have its output discarded
with no diagnostics at all, which is what made this whole bug family
cost days to find. Now it logs the error before dropping the result.
* script: auto-close still-open fn and let states at end of source
Both parse drivers dropped EndFnExpr/EndFnBlock/EmitLetDyn through their
auto-close catch-all when the source ended with them still open. A module
whose FINAL statement was let c = <lambda> got a body whose jump-over
stayed 0 — FN_BODY_DYN re-ran, found its me already popped, logged
"me stack is empty" and fell straight INTO the body, running it inline
at definition time and ending the module eval early — and the let itself
never emitted, so the binding silently didn't exist (same for
let c = <call>). Now those states close the way the live handlers do:
return + jump patch for the body, LET_DYN/LET_TYPED for the binding, and
the let's own EndStmt no longer marks a statement value (LET consumed
it; the final RETURN would pop an empty stack).
Fun consequence: the old test idiom of reading a result via
let out = r
out
only ever worked BECAUSE the trailing let was dropped — RETURN popped
the naked value off the stack. With the let actually binding, scripts
must end on a call (echo(r)); the emission tests are updated to do that.
Regression tests in tests/auto_close_eof.rs, including the exact
deferred-boot-timer closure shape the launcher apps use (which was
already fine — it just LOOKED guilty, see the splash commit).
* splash: probe optional script hooks without spamming the error log
call_script_fn checks whether the fn exists and bails quietly — but it
probed with a trapping scope_value, which had already queued a NotFound
by the time the miss was handled. Every host broadcast of an optional
hook (on_app_resize, on_widget_resize) against a script that doesn't
define it logged
variable 00001e93e419c77c not found in scope
— maximally misleading: the hex is just id!(on_app_resize) (Rust-side
ids aren't in the reverse-lookup table so they print raw), and the
line:col is the stale ip from the end of that script's eval, which
pointed at whatever closure happened to be compiled last. In the
launcher that was the boot-timer line of every generated app, sending
the investigation down a deferred-closure rabbit hole the pure-VM tests
then cleared. Probe with NoTrap.
* script derive: don't name the eval values vec 'v'
script_apply_eval!'s generated values block bound 'let mut v' and then
spliced #(expr) interpolations in verbatim — so a caller interpolating a
variable that happened to be called v got the macro's own half-built Vec
(borrow errors if you were lucky, the wrong value if not). Obscure name
instead.
* headless: don't compile the Apple video path
Upstream's zero-copy video work put CoreVideo/Metal code in
gpu_texture.rs (plus two consumers) behind cfg(target_os = "macos")
alone. Under cfg(headless) the apple backend isn't built at all, so
every one of those symbols — ObjcId, msg_send!, CVPixelBufferRef,
CVMetalTextureCache* — is missing and makepad-platform fails to
compile with 94 errors. That takes the headless harness down with it,
which is what host_launcher's UI tests run on.
Gate the Apple blocks on not(headless) too. Nothing is lost: headless
has no Metal device to import a CVPixelBuffer into, so the whole path
is inapplicable there.
Not caused by the rebase — pristine dev has it: its headless
CxOsTexture is an empty struct while gpu_texture.rs reads .os.texture.
* platform: expose the capturing area of a touch by uid
Raw `Event::LongPress` handlers (it has no `handled` field and is
broadcast to every widget) had no way to tell whether the press was
captured by an unrelated widget, so a geometric containment check
alone could steal a press that e.g. a dock `Splitter` owns.
* add `CxFingers::touch_capture_area(uid)`, mirroring the existing
public `is_area_captured`
* platform+widgets: add `Event::ClearHover` to reset stale hover state
An overlay (context menu, modal) claims all pointer hits while open, so
widgets that were hovered or pressed when it appeared never receive the
`FingerHoverOut`/`FingerUp` that would clear their visuals: makepad's
hover slot holds exactly one area, and once a widget loses it, nothing
is ever delivered to it again. Apps had no way to reset widgets they
don't own.
* `Cx::queue_clear_hover()` requests a one-shot `Event::ClearHover`
dispatch after the current event and its follow-up actions finish
(drained in `call_event_handler`, mirroring
`handle_pending_window_geom_changes`)
* `View` plays `hover.off`/`down.off` on it when its animator is
defined, `Button` cuts `hover.off`, `HtmlLink` cuts its link hover,
and `CalloutTooltip` hides itself
* overlays call `cx.queue_clear_hover()` when they close
* platform: rename `queue_clear_hover` to `clear_all_hovers`
Name the effect rather than the mechanism, matching `set_key_focus`
and friends, which also defer their commit internally.
* platform: expose `Event::pointer_claimed_area` for claim bracketing
A widget cannot tell whether the area that claimed a pointer event
belongs to one of its own descendants or to an unrelated widget drawn
on top of it; geometric containment is a false proxy (a floating
overlay's rect can sit entirely inside a larger widget's rect).
Dispatch order answers it exactly: dispatch IS the widget tree
traversal, and claims are first-wins, so a claim that appears between
a widget's own `handle_event` entry and its end was made by that
widget or a descendant, while a pre-existing claim belongs to a widget
drawn above it.
* add `Event::pointer_claimed_area()`, reading the claim cell of
MouseMove/MouseDown/TouchUpdate(Start); snapshot it at dispatch
entry and compare after
* macos: fix present-gate watchdog race that could re-wedge the runloop
The watchdog's `store(0)` assumed overdue presented-handlers were lost,
but they are usually just late: each one that fired after the reset
stole a decrement belonging to a newer frame, so `in_flight_presents`
could transiently read lower than the true number of outstanding
drawables. A deficit coinciding with a compositor consumption stall let
the gate open onto an exhausted pool, and with
`allowsNextDrawableTimeout:NO` the `nextDrawable` call blocked the
single runloop forever (paints, input, and the watchdog all share it),
until an external window resize freed a drawable.
* pack a reset generation into the high 32 bits of
`in_flight_presents` (count stays in the low 32)
* the watchdog reset now bumps the generation as it zeroes the count
* each presented-handler captures its generation at arm time and
no-ops if a reset happened since, so late handlers can no longer
steal decrements and the gate closes exactly when the pool is full
* macOS: gate the present pacer at 3 in-flight presents, not 2
The drawable pool holds three, so acquiring one with two still outstanding
can never block. Gating at 2 made the pipeline effectively single-buffered:
when a GPU frame ran longer than one beat it locked into a submit-wait-submit
resonance (~20fps at 0.6ms of CPU work), and it tripped the 250ms stuck-gate
watchdog a full frame earlier than it needed to.
`macos_freeze_android_gc` has gated at 3 since `ee96a23a3`; this brings the
branch that robrix pins in line, using a named `PRESENT_GATE_IN_FLIGHT` const
so both skip sites stay together.
* macOS: stop a stale `occlusionState` from freezing the UI forever
`handle_repaint` skips the beat while the window reports itself hidden, and
that skip was the one path with no timeout: its only exit was the flag
flipping back to visible. macOS can leave the visible bit clear on a window
that is really on screen (it comes back after a resize, which is why resizing
a wedged window has always cured it), and when that happens we skip every
beat forever. The runloop stays alive and input keeps working, so it looks
exactly like a frozen UI rather than a hung app.
* Bound the skip. After `OCCLUSION_PROBE_INTERVAL` (2s) of skipping we fall
through and present anyway. If the flag was stale we recover on our own; if
the window really is hidden we spend one frame per 2s to find that out.
* Make the pool rebuild actually rebuild. The watchdog nudged `drawableSize`
to `h + 1` and back to `h` in a single implicit CATransaction, so the layer
only ever observed the net change of nothing and could keep its stuck
drawables. `rebuild_drawable_pool()` now commits and flushes each size
separately, the way a real resize does. This also has to be right before
the occlusion probe is safe, since the probe deliberately presents a frame
the compositor may not consume.
* Log the abnormal paths. Every recurrence so far left no evidence of which
path fired, so the stuck gate now says so once, staying quiet while the
window is hidden (the probe trips that path by design every 2s).
Verified by running robrix against this branch: the probe path fires and
recovers with no panic, the UI renders normally, and a healthy foreground
run logs nothing.
* macOS: give back the in-flight count when a pass bails before presenting
`handle_repaint` acquires the drawable, bumps `in_flight_presents` and arms the
presented handler, and only then calls `draw_pass`. `draw_pass` has three early
returns (no draw list, nil MTKView descriptor, degenerate pass rect) that all
bail before `presentDrawable`, so the handler never fires and the count is over
by one forever. Two of those and the gate is permanently closed, leaving the
250ms watchdog holding the app up at a few frames a second.
`draw_pass` now returns whether it reached the present, and the caller undoes
its accounting when it didn't, guarded on the same generation as the handler so
a late handler still can't double-decrement. The other callers (iOS, tvOS,
stdin) ignore the result, which is fine since they don't count presents.
* macOS: fix two window delegate selectors that never fired
`windowChangedScreen:` and `windowChangedBackingProperties:` aren't
NSWindowDelegate selectors; the real names are `windowDidChangeScreen:` and
`windowDidChangeBackingProperties:`. AppKit never called either, so
`window_did_change_screen` and `window_did_change_backing_properties` have been
dead code, and moving a window between displays or changing its backing scale
never produced a geom event. Every other selector in that block already uses
the correct `windowDid` spelling.
- deep_eq returns false immediately when both sides are string-like and
the bit-compare failed (strings are interned, so bit-equality IS string
equality) — trims the type-check chain on the hot failed-compare path
of string-tag dispatch (a.kind == "...")
- pre-reserve thread stacks (value stack, scopes, calls, mes, loops)
- splash_bench: string_cmp workload, arith_hostmode (instruction limit +
run budget installed, the game-host configuration; measured: the two
per-instruction counters cost ~nothing, branch prediction hides them),
BENCH_ONLY profiler filter
Tried and rejected: batching consecutive value-pushes in run_core into
one dispatch iteration — the scan overhead outweighed the per-value
limit/budget checks it skipped (they were already free); reverted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Interpreter perf pass on platform/script, semantics-preserving:
- ValueMap hybrid small-map: object maps linear-scan a vec below 16
entries and spill to a HashMap above (scopes/call-args/small objects
never hash)
- compound scope assigns (+=, -=, ...) locate their slot with ONE
proto-chain walk instead of a read walk plus a write walk
- for-loop iterations clear and reuse the iteration scope in place when
nothing captured it (closure capture -> REFFED -> fresh scope); loop
scopes no longer copy the parent scope vec
- 'for i in a..b' captures end/step at loop entry when the range object
is un-REFFED; stored (mutable) ranges keep live per-iteration lookups
- trap error-queue + trap.on polled via one Cell<u8> bitfield per
instruction instead of a RefCell borrow + Option<enum> Cell read
- ADD checks the number fast path before the string check
Measured (splash_bench, medians of 3 A/B runs): array_iter -27%,
method_call -21%, array_index -21%, for-range loops -19%, object_churn
and field r/w -8..9%, fib -7%, sandbox-style composite tick -6%.
Adds platform/script/test/src/bin/splash_bench.rs (run with
cargo run -p makepad-script-test --bin splash_bench --release,
BENCH_ONLY=<name> loops one workload for profilers) and parity asserts
for the touched edges: mid-loop range end/step mutation stays live on
stored ranges, closures pin their iteration's scope, selective capture
does not leak across reused scopes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Windows: fix DPI-change problems: freezing, blank windows, drag-n-drop errors
- win32: do_callback queues re-entrant events (WM_DPICHANGED pumped inside
Present, nested WM_SIZE) instead of dropping them; the drops left dpi/geometry
stale, the window blank, and hit-testing offset after display-scale changes.
- win32: WM_DPICHANGED applies the OS-suggested rect and returns 0; removed the
duplicate-delivery compensations the lossless queue obsoletes.
- d3d11: no unbounded Present(1) block after a timed-out frame-latency wait
(DO_NOT_WAIT + paced retry); DXGI errors log instead of panicking.
- dnd: Drag/Drop events carry WindowId and are remapped via dpi_override_scale,
fixing drop-zone offset under UI zoom (Windows OLE, macOS external drops,
Wayland internal drags).
- win32: packaged-build window icons load at native shell sizes via LoadImageW.
* TextFlow: enforce `max_lines` for inline code wraps, list items, and inline widgets
`max_lines` + `text_overflow: Ellipsis` under-counted or skipped visual lines
whenever anything but a plain text run started one, so rich content (rooms-list
previews, anything with inline `<code>` or mention pills) could overflow its
line budget or hard-clip without an ellipsis.
* TextFlow: the inline `<code>` forced wrap left `is_continuation` stale, so a
wrapped code run was granted one extra row AND reported one fewer — a 2-line
overflow per wrap. The wrap now refreshes the continuation state and row
budget, is refused when no line remains, and its prediction probes measure
an unclamped layout (a clamped probe always claimed "fits on one line").
* Layouter: a non-wrapping layout truncated by `max_lines` never reported
`is_truncated` (detection was row-count only), so `max_lines: 1` labels
hard-clipped mid-word instead of showing "…". Width overflow on the
surviving last row now also triggers the ellipsis.
* TextFlow: list items charged two lines each (marker run + content run); the
marker now shares one visual line with the item's first content run.
* Inline child widgets (mention pills, images) bypassed the budget entirely:
Html now gates them on `is_content_truncated()`, charges the rows they open
via `track_inline_content()`, and holds a widget that lands on the last
allowed line in place (`Turtle::set_flow_wrap`) instead of letting the
turtle relocate it onto a row the budget can't pay for. A held widget that
still overruns the line is hidden behind a tracked clip and replaced with a
drawn "…" — the same contract text truncation has.
* TextFlow: fix row alignment and spacing for wrapped text beside inline widgets
On the single-batch text path (Windows/Linux), a multi-row text run never told
the turtle where its internal row boundaries were: every walk since the last
boundary — a bold sender on one visual row, a mention pill on the next — was
centered by `finish_row` against one merged row. Text sat a few px off the
pill baselines, rows around wrapped runs squeezed or overlapped, and an
up-centered pill on a padding-less first row rose above the clip and lost the
top of its rounded corners.
* DrawText: the single-walk path now allocates per visual row and runs
`turtle_new_line_with_spacing` at each internal boundary, keeping one glyph
instance batch. It emits a first-row walk owning the run's align entries and
a last-row walk with an empty range, so the run is never shifted twice.
* Turtle: `FinishedWalk` carries a `RowAlignRole`. A wrapped run's rows are
immovable (their glyphs share one batch): rows holding its visible text are
an `Anchor` — `finish_row_center` centers every shiftable walk on the
anchor's line, shifting a taller pill UP onto the text — while a
whitespace-only first row (a continuation that wrapped on a leading space)
is `Fixed`, so it cannot anchor a row to an invisible line. Anchor shifts
are clamped so no walk's top can rise above the turtle's clip.
* Layouter: `first_row_min_line_spacing_below_in_lpxs` is now honored (it was
stored and hashed but never read). Resumable draws pass the current row's
height plus wrap spacing plus the centering overhang, so a continuation's
second row lands where centered content beside it starts exactly one wrap
gap below the previous row — zero residual shift for uniform-height pills.
Selection capture and the `<code>` wrap probe pass the same floor so every
layout of a run shares one cache entry.
* Turtle: an anchored row returns its bottom forgiveness — the surplus its
allocation extends below the risen content — and `turtle_new_line`
subtracts it, keeping inter-row gaps uniform on both sides of anchored
rows. A turtle's final row keeps its full extent so nothing clips at the
bottom.
* uizoo: Html fixtures for line clamping and pill/text row alignment
Repro and regression fixtures mirroring a chat client's message surfaces:
max_lines clamping with inline <code> at several widths, atomic inline-widget
(pill) relocation/hold/ellipsis under a line budget, and timeline-style
pill+text rows at 9.3pt and 11pt metrics — including a CJK-titled pill and a
padding-0 first-row case that guards against clipped pill tops.
* Layouter: fill the last permitted row by grapheme when ellipsizing
When `max_rows` is set together with ellipsis truncation, word wrapping
would move a word that didn't fit off the final permitted row before
truncation ran, leaving that row ellipsized at the last word boundary
("@…") rather than at the last glyph that fits ("@quokka:…").
Word integrity is meaningless on a row that ends in an ellipsis, so lay
the final permitted row out by grapheme and let truncation cut at the
actual width limit.
* DrawText: account for walk margins when resolving a Fit max bound
The layout bound for a Fit-width walk with a relative max bound was the
raw resolved max, but the turtle's final width — and with it the clip
rect — gets clamped to that max minus the walk's outer margins. On top
of that, a Label passes the same margined walk to its inner DrawText,
re-applying those margins inside the turtle. Text laid out near the
bound therefore extended past the clamped clip, which sliced letters
off the end of untruncated text and cut the appended ellipsis down to
a single dot.
Subtract the walk's own horizontal margins from the resolved bound, and
when the enclosing turtle is itself an unresolved Fit with a max bound
(a Label sizing itself around this text), subtract that turtle's outer
margins too, so the layout bound matches the width the clip is actually
clamped to.
* uizoo: fixture for bounded pill labels ellipsizing at the width cap
Pill-like labels bounded to a fraction of the enclosing width, at
container widths that walk across the cap, plus one inline in an Html
flow. Truncation must always end in a visible trailing ellipsis, never
a bare mid-glyph cut.
* Layouter: keep word-boundary ellipsis on continuation rows
Grapheme-filling the last permitted row is wrong when that row is an
empty continuation: grapheme layout force-places a grapheme wider than
the row's remnant past the width limit with the truncation flag unset,
drawing overflowing text with no ellipsis. Finishing the row instead
truncates within bounds, so continuation rows keep the word-boundary
behavior.
* DrawText: scale the Fit max bound into layout units
The layouter works in unscaled units and row widths are multiplied by
font_scale on output, but the resolved Fit max bound was passed through
in physical units — the same mismatch max_layout_width_for_walk already
divides away. Any font_scale above 1 on a bounded label laid text out
past the clamped clip, reintroducing the sliced-tail bug the bound is
there to prevent.
* Turtle: add Base.Line, a Fit bound relative to the available line width
A static relative bound cannot express what an inline widget's inner
text actually has room for: that depends on the line the widget lands
on, its own leading geometry (icons, padding, spacing), and the
trailing insets after the text. Base.Line resolves a Fit max bound to
exactly that, with the enclosing line's flow selecting between two
measurements that are each final at the moment they are taken:
- A wrapping line can relocate the widget whole onto a fresh row, so
the bound is what a fresh row offers. Content sized to it either
fits where it is, or fits the row the widget is relocated to.
- A non-wrapping line — including one held non-wrapping by the
inline-content clamp on the last permitted row — keeps the widget
in place, so the bound is the remnant up to the line's right edge.
The measurement runs from the current turtle's content origin rather
than its pen, because a Fit max bound is evaluated twice: before the
content is laid out, and again in compute_final_size when the turtle
closes. A pen-relative measurement would collapse the closing clamp
to the leftover width and clip the content it just laid out.
* uizoo: fixtures for Base.Line-bounded pill labels
The four behaviors the line bound guarantees: a long name ellipsizing
at a narrow container's edge, a mid-line pill relocating whole to the
next row at full row width, a pill held on the last clamped row
squeezing visibly into the remnant, and a short name in a wide
container rendering untruncated.
* Turtle/DrawText: keep tiny Fit max bounds from slicing or inverting clips
A line-remnant bound can resolve to nearly zero when the last permitted
row is already full where an inline widget's text begins. Two guards
keep that degenerate range within the whole-glyph truncation contract:
- The Fit max clamp in compute_final_size floors at zero, so a bound
smaller than the walk's margins cannot produce a negative width and
an inverted clip (a pill rendering as bare chrome with no title and
no ellipsis anywhere).
- DrawText treats a bound too narrow for the truncation ellipsis
itself as no bound at all: the layouter appends the ellipsis glyph
unconditionally, so a narrower clip would slice it open. Left
unbounded, the text overflows honestly, letting an enclosing flow's
inline-content clamp hide the widget and draw the ellipsis itself.
* macos: fix UI freezing after the window has been fully hidden
The present-gated frame pacing from commit `c0b851a57`
introduce a bug here, which basically causes vsync or other paint
events to never reach to UI after the macOS window is re-shown.
(moved to the foreground after having been bkgd'd for a while)
Fixes for this:
* Skip presenting while the window's `occlusionState` lacks
`NSWindowOcclusionStateVisible`, keeping the pass dirty so the first
visible beat repaints. This also avoids `nextDrawable` hard-blocking
the main thread (we set `allowsNextDrawableTimeout: NO`) on a pool
exhausted by presents the compositor will never consume.
* Add a 250ms watchdog to force the event handlers to start again
if they locked up somehow.
* On a watchdog timer, nudge `drawableSize` to rebuild the drawable pool
the same way a manual resize does, reclaiming stuck drawables so the
recovery present can't block on an exhausted pool.
* android: run script-VM gc after paint, like every other backend
commit `77cacb846` added the post-paint `gc()` sweep to the iOS, Windows, X11,
and Wayland event loops to match macOS, but Android never got one.
Now we do it on Android too.
* android oes video zero-copy and gles shader fixes
* regenerate windows-rs by windows-strip
* fix windows video freezes with MF on an MTA worker
* regenerate windows-rs by windows-strip
* MTA MF video, COM notify, YUV texture reuse
* add local video file playback support to video-player example
* NV12 Metal present, seek warm-up gate, YUV full range
* fix Linux GStreamer video: A/V mute, HLS prepare, async teardown
* no-op SelectVideoTrack/SelectAudioTrack on non-Linux backends
* Linux ALSA/Pulse: bigger buffers, fix resample setup, report device rate
* fix some warnings
* Move XInput/DirectInput device discovery off the UI thread
* fix GLSL unpack4u8 for GLES 3.0 (#version 300 es)
* Linux GStreamer: DMA-Buf NV12 OES zero-copy, optional GLMemory path
* Linux MediaPlugin: DMA-Buf NV12 and GLMemory zero-copy present APIs
* Add Windows SourceReader DXGI NV12 zero-copy video path
* fix build error
* fix GLES: add highp precision for sampler2DArray in Linux shaders
* playback speed support for android
---------
Co-authored-by: jasonqiu <jasonqiuchen@outlook.com>
Co-authored-by: jasonqiu <jasonqiu@futunn.com>
The Feb websocket migration left windows_stdin.rs behind: hosted apps
got no Event::Timer at all (StartTimer/StopTimer fell into the catch-all,
nothing dispatched PollTimers), no HttpRequest/CancelHttpRequest, a
WindowGeomChange that never fired the event or the pass redraw, and an
unguarded stdin_windows index in the repaint path. Ported straight from
macos_stdin.rs / linux_x11_stdin.rs; compile-checked against
x86_64-pc-windows-msvc, still needs a live run on a Windows box.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CPU launches shells; the GPU animates every spark from a closed form. Once
a shell bursts nothing in the world can influence a spark — it flies from a
known point, at a known time, along a fixed arc — so position is a function
of (spark index, seed, age) and there is no state to step and nothing to
upload per frame. Twelve shells of 320 sparks is 3,840 particles for TWELVE
instances of CPU work, against 3,840 simulation updates and a 3,840-instance
upload for the stepped path.
**The style seam is the point.** Rust owns structure — trajectory, lifetime,
billboarding — and exposes four hooks a splash script overrides by
inheritance: `spark_motion` (swirl, fizzle, drift), `spark_size`,
`spark_color`, and `spark_pixel` (the sprite program). Arcade's own styling
lives in its `script_mod!`, not in Rust: a three-stage temperature burn,
hotter on the fast outer shell than the slow core, with sparse glitter
strobing. A generated game can restyle the sky without being able to reach
the simulation.
Two real bugs fixed on the way:
- **Vec3f instance fields silently misalign.** A `Vec3f` is tightly packed in
Rust but a `vec3` obeys 16-byte alignment in the shader ABI, so the burst
origin read back as zero and every shell rendered at the world origin, on
the ground. The other shaders here get away with `Vec3f` because theirs are
`uniform`, not instance. Everything is packed into `vec4`s now, which is
the shape the hardware wants anyway and removes the class of bug.
- **Eight bare `panic!()`s in the shader backend** now name the backend, the
stage and the IO type, and the commonest case — reading a geometry
attribute from `pixel:` — gets a message saying so and showing the varying
that fixes it. Previously a shader that tripped it aborted with no text at
all, which is not a barrier, it is a wall in the dark.
**Gated off by default.** The spark SIZE instance is not reaching the shader:
scaling it 25x changes nothing on screen, so every spark draws as a
screen-filling blob and the sky whites out. That is the same class as the
Vec3f bug above and I have not found the second instance of it. The launcher,
the trajectory, the placement annulus and the whole hook surface are finished
and tested; this is one plumbing bug from working. ARCADE_FIREWORKS=1 turns
it on to work on it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- win32: do_callback queues re-entrant events (WM_DPICHANGED pumped inside
Present, nested WM_SIZE) instead of dropping them; the drops left dpi/geometry
stale, the window blank, and hit-testing offset after display-scale changes.
- win32: WM_DPICHANGED applies the OS-suggested rect and returns 0; removed the
duplicate-delivery compensations the lossless queue obsoletes.
- d3d11: no unbounded Present(1) block after a timed-out frame-latency wait
(DO_NOT_WAIT + paced retry); DXGI errors log instead of panicking.
- dnd: Drag/Drop events carry WindowId and are remapped via dpi_override_scale,
fixing drop-zone offset under UI zoom (Windows OLE, macOS external drops,
Wayland internal drags).
- win32: packaged-build window icons load at native shell sizes via LoadImageW.
A game was playable standalone and completely dead under Studio — which is
exactly where it gets developed. The cause is structural, not a binding: an
app hosted by Studio is a child process with no window of its own, so the
OS hands controller input to Studio and never to it. On macOS the child
never even reaches `init_cx_os`, so `apple_game_input` is None and
`game_input_states()` returns an empty slice forever.
So controllers now travel the same road mouse and key events already do.
`StudioToApp::GameInput` carries the whole controller set; the run view
polls its own `game_input_states` on the tick it already uses to batch
messages, and the app lands them in `Cx::game_input_remote`, which
`game_input_states()` reads whenever `in_makepad_studio` is set.
Details worth knowing:
- Sent on change only. A pad at rest reports identical level state every
tick, and that is not worth the wire. The memo drops on focus loss so
regaining focus resends rather than assuming the app still holds it.
- Only the focused run view forwards, so two open run views cannot both be
driven by one stick. That is the rule the keyboard already follows.
- The set is replaced wholesale rather than merged, so a pad that unplugs
disappears by being absent instead of freezing at its last state.
- Wheels ride along with gamepads. Forwarding only pads would have left a
wheel silently dead under Studio while a pad worked, which is a worse
failure than both being dead.
- Linux gains controllers under Studio that it does not have standalone:
its native backend is a stub, but the forwarded path is platform-neutral.
The 21-field mapping lives in ONE place with both directions together.
Written per-side it would have drifted the first time a button was added —
one end gaining a field and the other quietly reporting zero for it. The
round-trip tests give every field a distinct value, because a fixture of
uniform numbers cannot detect two crossed wires, and the sticks are pinned
separately since flattening a Vec2 to scalars is where a swap would hide.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An indented code block in module docs is a doctest, and this one contained
a bare `...`, so `cargo test -p makepad-platform` has been failing on a
snippet that was only ever meant to be read. Fenced as `ignore`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The compiler is where hostile shaders get stopped, because this engine lowers
the script shader language to Metal/HLSL/GLSL/WGSL rather than passing source
to a driver. Two genuinely exploitable holes found and closed:
- Loop bounds were arbitrary EXPRESSION STRINGS written straight into the
emitted shader, so `for i in 0..some_uniform` compiled to an unbounded GPU
loop and `loop{}` emitted a bare `while(true)`. A hang triggers a driver
device reset that kills the app — ugly anywhere, worst in a headset. Rather
than reject (which would break legitimate code), the bound is now EMITTED: a
provably-small integer literal compiles unchanged, anything else gets a hard
65536-iteration cap. literal_bound() is deliberately conservative — a
uniform, arithmetic, a call, hex or a negative all count as unprovable
- compile_fn INLINES at every call site, so a branching call graph expands
exponentially with depth (recur_block stops self-recursion, but not f1
calling f2 twice calling f3 twice). MAX_EMITTED_BYTES (1 MB) bounds what was
an unbounded compile-time DoS
Recursion was already safe (recur_block errors); nothing added there.
Honest remaining gap: the cap bounds ITERATIONS, not cost per iteration — a
shader doing 65536 heavy texture samples is legal and slow. Bounding real GPU
time needs a cost model or driver watchdog; neither exists here and this does
not claim otherwise.
Verified empirically, not just by compiling: all 24 loop{} constructs in the
built-in shaders are guarded (48 emitted lines, 4 unique guard names per
shader confirming no shadowing), zero for( loops exist in any built-in, and
the capture shows rendering intact — including text, which is exactly where
those guarded loops live.
MEASURED FIRST, then declined to build: 34 shaders compile at Arcade boot in
~15 ms total on Metal (first 4.47 ms cold, rest 0.10-0.74 ms). A persistent
shader cache is NOT worth its invalidation-and-staleness surface to save 15 ms
of one-time boot, so it wasn't built. Vulkan/GLES on Quest may differ — that
needs on-device measurement before anyone builds speculatively. Benchmark
retained behind MAKEPAD_SHADER_BENCH (it was logging every boot);
MAKEPAD_SHADER_DUMP=<dir> writes generated source, which is what an AI has to
debug and was otherwise invisible.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
from_tcp_stream wrapped the socket in a BufReader that was dropped on
return, so body bytes read ahead into it vanished and handle_post blocked
forever on bytes that no longer existed — one wedged thread per request
that sent headers and body in the same TCP segment. Browsers split the two,
which is why nothing noticed. The function now owns its buffer, reads to
\r\n\r\n, and returns the remainder alongside the headers for handle_post
to consume first. The websocket upgrade path had the identical exposure (a
frame pipelined with the upgrade was silently dropped) and consumes the
same prefix now; EOF mid-head returns instead of spinning to the 4096-line
guard. New tests cover headers+body in ONE write (the case that hung, with
the connection held open afterwards so a regression blocks rather than
passing on EOF), the split case, and a plain GET.
game_net: Intent::Authoring{text} + MAX_AUTHORING_TEXT so a keyless client
in a hosted room can route a creation request to the host's agent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Entity lookup O(log n): shared entity_index_sorted helper, single
push_entity spawn path with ascending-id assert + per-tick sorted
debug_assert — the sorted-Vec invariant is now enforced, not hoped
- WorldSnapshot captures next_id: failed-eval rollback can no longer mint
colliding ids under surviving entities (review defect fixed)
- game.log buffered (1s/16KiB/eval-boundary flush), agent RPC poll gated
on one .agent dir-mtime stat — no per-tick file I/O left
- Camera rig authoritative on GameWorld (orbit/chase state); widget keeps
only device-input accumulation; mailbox drain order unchanged
- Per-tick cumulative script budget: with_instruction_limit reports
consumption (incl. trap-wipe subtlety), gamemaker tick holds ONE 500k
pool across on_tick + timers + touch events
- Tape probe verified BYTE-IDENTICAL vs pre-cleanup reference
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Escape barrier: every checked store funnel tags stored objects REFFED
(stored => REFFED structural), ret==args guards at native completion,
fn-arg bind-time barrier (closure-captured scopes retained args untagged
-> latent use-after-free under eager release)
- vm.release_transient() for host per-call objects; *_unchecked pushes are
the releasable-container path; call_with_scope native branch no longer
leaks one scope object per Rust->native vm.call()
- pump_widget_async: dead-isolate reclaim every pump + needs_gc-gated
round-robin mark/sweep — isolate heaps were never collected before
- res.rs: resource path cache was Cx-global and cached handle VALUES across
VMs, so isolate heaps held main-heap handle indices (exposed by the first
isolate mark pass ever to run). Now: data shared globally, handles minted
per-heap, cache keyed (heap_key, path), per-heap detach on GC
- vm.gc() no longer shrink_to_fits every run; explicit gc_and_compact()
- script-test: GC campaign suite (flat-heap tick loops, retained-input
survival, escaped-scope capture, isolate churn collection)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wrapper names printed as hex LiveId hashes on non-Metal backends
(identifiers starting with digits — GLSL compile failure at startup on
Linux/GLES). Metal had the lut entries; Glsl/Wgsl now do too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GLSL emits _mp_ wrappers over unpackHalf2x16/unpackUnorm4x8 (WebGL2
baseline has them native); WGSL names mapped for the future backend;
the headless runtime gets bit-exact CPU decodes. The packed map vertex
format is now portable to the wasm target.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One f32 geometry slot carries two f16s or four unorm8s; the vertex fn
unpacks via _mp_ helpers in the MSL preamble. Type checker: float ->
vec2f/vec4f. GLSL/WGSL mappings + headless Rust-backend impl to follow
with the packed layout itself.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Texture-mode passes append to one retained command buffer flushed at the
window pass (safety flush at repaint start for texture-only frames) —
the 12-pass gauss pyramid paid ~1ms commit/schedule latency PER PASS.
Profiling mode (MAKEPAD_GPU_PROFILE) keeps per-pass buffers so spans
stay attributable. Startup window 3400x2050 for pixel-heavy iteration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per command buffer: pass debug name, GPU start->end interval, draw calls,
vertices, instances, upload bytes — accumulated and printed as a 1Hz
table from the completion threads. Map frame stats gain gpu_ms/gpu_max
from the existing PERF_CHANNEL_GPU ring.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Regression attributed: the combined archive's six osm_* detail layers
were wholesale-ingested and STYLED by the base parse (roads double-
styled via streets + osm_lines: union input 946->3940 rings, stroke
prep 26x) -> 2.1-5.5x slower than the old two-archive pair. Layer-level
lazy skip (MvtSink::wants_layer): unconsumed layers skip as raw bytes
in both passes; all 24 profile cells improve 1.3-2.3x.
- Gray-tile keep-stale: three kill paths fixed — mark_tile_failed no
longer replaces drawable Ready entries with gray placeholders on batch
errors (archive rewrites), transient absent reads no longer evict
Ready meshes + 30s blacklist, retry backoff moved to TileEntry.
- Tilt-aware request priority (camera-projected screen distance with
toward-camera bias) — the near field builds first under tilt.
- Restyle bursts lift the 4-slot gesture throttle to 12; queued jobs for
dead zoom/bucket/mode are pruned (TagThreadPool::retain_queued), stale
results dropped pre-upload; uploads byte-budgeted (24MB/frame).
- Stage matrix: painter-order union cascade = 59-77% of every cell
(~320-350ms worst key) — per-bucket face bake is the path to <200ms.
- Baked-fill strips: A/B shows no measurable win post fill-tessellator
optimization -> removal recommended (dissolve stays).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Idle listening now runs PLAIN capture (no VPIO, zero ducking — music
untouched); the app arms the voice-processing unit only while its own
TTS is audibly playing (+0.8s tail), which is the only window where echo
cancellation matters. Options changes rebuild running input units
(AudioUnitAccess.last_input_options); WindowVoiceInput/VoiceWave gain
set_echo_cancellation; route app polls speech playback at 4Hz.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Advanced ducking dips other audio hard on voice activity even at Min;
standard ducking at Min is a light constant reduction. Also fixes the
IO-unit instantiation double-retain (av retained twice, au never) that
made every unit immortal — mic-off now really ends the voice session
and un-ducks. MAKEPAD_VPIO_DUCKING=min|mid|max|default and
MAKEPAD_VPIO_ADVANCED=1 for experimentation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Standalone harness (scratchpad/vpio_test.m) proved macOS VoiceProcessingIO
rejects ANY custom bus format with -10875 FailedInitialization; with the
native format untouched it allocates and accepts the min-ducking config.
- VoiceInput skips the bus-format override and reads back the native
sample rate/channel count after allocation (input handler now sizes its
buffer from input_channels instead of hardcoded 2ch).
- Retain balance fixed: the AU handle is retained once on SUCCESS (the
historic double av-retain made every IO unit immortal), and a FAILED
instantiation is released in the error path — leaked half-born VPIO
units were keeping system-wide ducking engaged until process exit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
release_audio_unit now stops hardware, deallocates render resources and
releases the retained render_block — the leaked VoiceProcessingIO unit
kept the system voice-chat session (and its ducking of all other app
audio) alive until process exit. The other-audio ducking configuration
(advanced + minimum) is applied after allocateRenderResources and logs
whether it took effect.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The SCK loopback device claimed is_default and default_input() fell
through to it when the real mic errored — the voice pipeline silently
became SYSTEM-AUDIO capture behind screen-recording privileges. Loopback
is no longer default and default_input() only auto-picks real Input
devices; system-audio capture is explicit-opt-in by device id.
- VoiceProcessingIO input failed FailedInitialization on macOS when pinned
via setDeviceID (it aggregates its own devices; follows system default
input now). If VPIO still fails, degrade to plain capture instead of
marking the DEVICE failed (voice_input_unusable flag + device-change
re-arm); input error log no longer says "output".
- VPIO voice-chat mode ducks all other app audio to a whisper: request
advanced ducking at minimum level (macOS 14+/iOS 17+) so music stays
near full volume while AEC keeps running. Links AudioToolbox.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- bridge-dz baked for the entire Netherlands (17026 tiles, 29MB):
AHN-refined over the 8 Amsterdam sheet pairs, solver-only elsewhere.
Baked as 4 north-south strips with 2-tile overlap — the full-bbox
global solve peaked past 60GB RSS — merged with the new mbtiles-merge
subcommand (later input wins on overlaps, block-major write order).
Route app bridge_dz path ams -> nl.
- audio_unit: the new VoiceInput (VoiceProcessingIO) kind takes the input
setup/handler paths (set_input_handler panicked and aborted the app);
VPIO capture forced mono on every platform.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
use_audio_inputs_with_options(devices, AudioInputOptions{echo_cancellation})
— platform-neutral trait default ignores the flag; Apple swaps the input
unit HAL->VoiceProcessingIO (system-wide AEC, the unit iOS input already
uses), so the assistant's own TTS no longer feeds back into the mic.
Voice capture (WindowVoiceInput) requests it always.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ggml: read-only mmap module (unix-gated) + two-region Context (mapped
weights / dirty caches) + segmented Metal buffer binding; llama loads
GGUF weights as file-backed clean pages (jetsam-exempt) with owned-arena
fallback (MAKEPAD_LLAMA_NO_MMAP=1). Route app dirty footprint 12GB -> 4-7GB;
model load becomes lazy page-in; A/B byte-identical on 4B + 9B.
- llama: fix graph-cache keying corruption — a graph keyed wider than the
KV cache corrupted attention for any prefill batch >= 2 (flash op reads
permute-node dims baked at build; view reconfigure never reached the
kernel; masks were written cache-narrow). Masks now always fill the full
graph key width and graphs key by 1024-buckets; reconfigure path removed.
Verified byte-exact vs per-length reference across batch 1/2/8/64/512,
short+long prompts, mmap on/off, plus a two-session concurrency probe.
- voice: passive VoiceWaves no longer register the global audio-input
callback (the invisible caption-bar wave stole mic audio — last
registrant wins — and spawned duplicate whisper workers); whisper back
to F16 default (voice Metal library has no quantized kernels; q5_0
failed every GPU matmul); raw transcripts render immediately.
- route: kokoro TTS voice output (speaker toggle; streams reply sentences,
announces nav maneuvers + arrival) with barge-in — voice activity on the
mic stops playback instantly; dispatcher context 8k -> 32k (hybrid KV is
12/48 layers, ~48KB/token); window caption bar suppressed under studio.
- llama-generate: --max-context/--prefill-batch-size + state fingerprints;
new llama_concurrent_probe bin.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deep gauss pyramid levels (3-5) are re-homed to 1/8 resolution via
progressive half-texel tent upsamples so no on-screen sample comes from
a texture coarser than 8 device px/texel; half-texel offsets keep the
exposed sigma ladder at ratio-2 per level (full-texel tents inflate deep
levels ~35% and open a visible blur band at the raw->re-homed boundary).
Per-pixel sampling replaces the 13-tap cross cascade with 4-tap bicubic
B-spline reconstruction: C2-smooth (kills the bilinear texel lattice)
and cheaper (glass 26->8 taps, chromatic variant 78->24).
Headless shader JIT fixes found while verifying: RenderCx always emits
vtx_pos (vertex fns that return the position have no VertexPosition io),
vec*vec MulAssign/DivAssign impls, and discard returns a value.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(linux): survive machines with no usable audio device
Makepad aborted inside libpulse on any machine without a working sound
server: get_updated_descs passed the null pa_operation returned by a
context that is not READY straight to pa_operation_get_state, which is
an assert in libpulse ("Assertion 'o' failed at pulse/operation.c:136").
Headless boxes and containers died on startup. Nearby paths were no
better - a sound server that advertises a sink it cannot open hung the
UI thread for good, and a device that failed to open was re-opened on
every device change, forever.
PulseAudio:
- never hand a null pa_operation to libpulse, and give up on a context
that is not ready instead of calling into it
- PulseAudioAccess::new returns None rather than panicking, so a
machine without a server simply runs on ALSA
- PA_CONTEXT_NOAUTOSPAWN: without it pa_context_connect forks and execs
a sound daemon synchronously, before the mainloop exists, blocking
the UI thread for as long as the spawn takes (measured 7s) and
leaving a daemon running on a machine that deliberately had none
- every wait is bounded by a mainloop timer, so a server that never
answers cannot freeze the app
- the context state callback no longer takes the access mutex, which
the UI thread holds while waiting on the mainloop (a deadlock)
- null info pointers, eol < 0 and absent default sink/source names are
handled instead of dereferenced
- streams still being created cannot be disconnected, so they are
parked and disconnected once the server answers rather than left
attached to the microphone for the life of the process
- callbacks no longer panic across the FFI boundary, and a stream the
server tears down is reaped and reported instead of going silent
ALSA:
- track input and output failures apart: both directions of one pcm
share a device id, so a card with no microphone disabled playback
- publish a device before opening it, not after. Opening takes ~200ms
and the check that decides to spawn reads that same list, so two
device changes inside the window - the normal startup - spawned a
second thread whose EBUSY marked a device that was playing fine as
failed
- a stream that dies mid-playback is reported, so the app can move to
another device instead of silently losing audio
Both backends:
- a device that failed to open is not retried on every device change.
It is retried when the device list changes, or when the app asks for
a different set than last time, so an explicit request - a user
picking a device, a widget toggling its microphone - is still
honoured
- AudioDevicesEvent::default_output falls back to another working
device instead of handing back one already known to have failed,
which is what made apps ask for it again on every event.
default_input deliberately does not: the next input in the list is
typically a monitor source, and silently recording the machine's
own output instead of a microphone would be a privacy breach
* fix(linux): enumerate and prefer the generic ALSA "default" pcm
Device enumeration called snd_device_name_hint once per card. Passing a
card number only returns that card's raw pcms, so the generic ones -
"default", "pipewire", "pulse" - were invisible to makepad, and it then
picked the first plughw: node as its default device. Raw nodes demand
exclusive access to the card, so makepad's chosen device failed with
EBUSY whenever anything else was playing, which on a desktop running
PipeWire is most of the time.
Ask for the whole system instead (card -1), which is what every other
alsa client does and what "aplay -L" prints. It returns the generic pcms
as well as every card's own, so nothing is lost. "default" is now
preferred when picking the default device, for both directions; the old
plughw:/dmix: chain remains for a system with no alsa configuration.
"null" is skipped: it accepts and discards audio and always opens, so
offering it would let the automatic fallback land on a device that looks
like working audio and is silent.
Also frees the hint array, which was leaked on every enumeration.
Measured with the raw card held by another process: before, makepad's
first pick failed; now it opens "default" and audio flows immediately.
- Opaque faces get boundary-straddling AA skirts (coverage 50% at the
edge, legacy convention); translucent faces ramp outward only and their
straddling rings pick one level part — premultiplied translucent color
must never paint over itself (double-blend darkened plaza boundaries).
- macos event loop: a foreign window class in the scroll-wheel path (the
screen-capture overlay's TUINSWindow) has no macos_window_ptr ivar —
skip instead of panicking. This was the random studio death whenever a
system screenshot overlay was active.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every window hardcoded `create_app_id = "Makepad"`. GNOME Shell matches Wayland
toplevels to their `.desktop` file by app id, so packaged apps looked for
`Makepad.desktop`, missed, and lost their icon. X11 was unaffected, having always
used argv[0] for WM_CLASS.
* Default `create_app_id` to the argv[0] basename via `window::default_app_id()`.
* Add a `window.app_id` DSL field to override it, for rDNS packaging like Flatpak.
* Route X11's WM_CLASS through `create_app_id` too, replacing its own inline copy
of the argv[0] chain, so both identities come from one field.
* PortalList: add `set_flow(cx, flow)` to switch a list between vertical and
horizontal layout flow at runtime. Much faster than using `script_apply_eval`,
and always fully correct because it updates the `vec_index` axis.
It also avoids a full ScriptReapply sequence, which is potentially expensive
across all of an app's widgets.
* AdaptiveView: add `active_variant()` getter so that widgets using AdaptiveView do not have to separately track which variant it should be in. Removes all ambiguity and possibility of divergence... finally!
* Also fixes long-standing TODO item in AdaptiveView about properly handling
weird window geom events, e.g., on macOS sometimes it spits out a 0-width
window update which is total b.s.
* DisplayContext: add `is_desktop_width()` helper.
* Window: ignore spurious zero-size window geom events.
- transit routes draw as per-line colored strokes (stable ref-hash
palette) on white casings, labeled "Tram 7"/"Metro 52"; stops get
name labels (stations z13+, local z15+)
- districts layer: tiered admin boundaries (gemeente/wijk/buurt) and
centroid name labels staged by zoom, muted admin purple
- upside-down labels fixed structurally: post-placement invariant
re-places any glyph run that reads net-leftward (hairpin ramps fooled
every pre-placement chord heuristic); vertical dead band ±4°
- overlay charger pins hold collision priority over base charging_station
icons, which are suppressed while a charger overlay is active; pins
reserve their bubble box so POI text places beside them
- per-icon zoom floors: overlay pins use kW tier floors (8/10/12) —
micro floor 16.5 no longer hides them; 0.6 zoom grace; fail-open when
the icon_zoom uniform is unset; oneway arrows from z15
- rail z-fighting: stroke merge drops duplicate quantized segments from
forked ways (switches drew shared segments twice, shimmering in tilt)
- 3D mode: stoplights (pole + red/amber/green lights) and taller
ellipsoid trees; live_reload names its override files
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>