Commit graph

530 commits

Author SHA1 Message Date
a9af660e45 android: send BeforeStartup/AfterStartup over websocket
The Android platform never sent BeforeStartup or AfterStartup messages
via the studio websocket. Desktop platforms send these through their
stdin event loops, but Android uses websockets instead of stdin.

Without AfterStartup, the hub never broadcasts AppStarted to UI
clients, causing makepad-test to time out waiting for app startup.
2026-08-20 07:41:10 +03:00
ce899827a9 feat(test): extend protocol for touch, long-press, paste, IME composition
Add RemoteTouchState, RemoteTouchPoint, RemoteTouchUpdate, RemoteLongPress,
RemoteTextPaste, RemoteIMEComposition wire structs to StudioToApp enum.

Dispatch new events through cx_shared.rs (TouchUpdate→Event::TouchUpdate,
LongPress→MouseUp+MouseDown, TextPaste/IMEComposition→Event::TextInput).

TestApp: touch_down/move/up, long_press, paste_text, ime_composition.
Locator: touch_down/move/up, long_press, paste, ime_composition.
2026-08-19 00:50:17 +03:00
7de0f215d9 feat(test): Android makepad_test via adb + in-process hub (legacy Java path)
Adds the Android test runtime to makepad_test: builds the APK with
cargo-makepad's standard Java path, installs and launches via adb with
makepad.STUDIO_* intent extras (incl. STUDIO_BUILD), connects the app to an
in-process hub over adb reverse, and waits for startup + responsiveness.
Adds clean in-process hub shutdown (HttpServerHandle + GatewayHandle Drop)
and the STUDIO_BUILD intent parsing on the app side. No native-activity or
NDK APK compilation code is included.
2026-08-16 14:36:10 +03:00
Jason Yau
41b41f1d11
Drain platform_ops FIFO so host commands run in enqueue order. (#1183)
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>
2026-08-15 20:34:16 +02:00
Kevin Boos
941a3de88f
iOS: log panics to NSLog, and don't abort when one escapes (#1178)
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.
2026-08-14 20:08:52 +02:00
Kevin Boos
4f7abea39e
headless: stop recompiling every shader on every start (#1175)
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.
2026-08-13 10:03:01 +02:00
Jason Yau
42a61ce7d7
Windows: overlapped main window (#1168)
* 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>
2026-08-13 10:01:52 +02:00
Kevin Boos
457e75a7ef
Area: don't panic on a stale Area::Rect (#1171)
`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.
2026-08-12 21:20:37 +02:00
Kevin Boos
e0a5a23f2f
Splash improvements for running untrusted mini-apps (#1139)
* 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.
2026-08-12 01:55:46 +02:00
Kevin Boos
dd6498a459
Add Event::ClearHover, and expose the pointer's claim and capture areas (#1170)
* 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
2026-08-11 20:19:29 +02:00
Kevin Boos
5f74ffec15
macos: fix present-gate watchdog race that could block the runloop (#1167)
* 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.
2026-08-11 20:19:16 +02:00
Jason Yau
259b84ed23
fix: resize swap chain on DPI change (#1169)
Co-authored-by: jasonqiu <jasonqiuchen@outlook.com>
2026-08-11 19:33:44 +02:00
Admin
ff604e53a3 splash: string-compare early-out in deep_eq, thread stack prealloc, bench additions
- 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>
2026-08-11 15:59:05 +02:00
Admin
0cd882f793 splash optimisation work
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>
2026-08-10 13:07:40 +02:00
Kevin Boos
50bfc8ad16
macos: fix UI freezing after the window has been fully hidden (#1166)
* 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.
2026-08-10 12:30:20 +02:00
Jason Yau
947f815181
Video playback fixes and improvements (#1155)
* 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>
2026-08-10 12:29:33 +02:00
Sabin Regmi
105158e08c
Shader: update unpacking function for better precision in GLSL shaders (#1162) 2026-08-05 19:34:39 +02:00
Admin
fbb6b3c5bf Windows stdin-loop: port the mac/x11 hosting parity bits
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>
2026-08-05 17:19:25 +02:00
Admin
362c96d63b Fireworks: parametric GPU sparks with a splash-side style seam (gated off)
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>
2026-08-05 16:59:45 +02:00
Kevin Boos
aab6002e7e
Windows: fix DPI-change problems: freezing, blank windows, drag-n-drop errors (#1159)
- 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.
2026-08-04 09:39:41 +02:00
Admin
303945ea57 Studio: forward game controllers to the hosted app
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>
2026-08-03 20:30:29 +02:00
Admin
d933cc242a platform: fix the perf_monitor doctest
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>
2026-08-03 20:30:12 +02:00
Admin
0c8b5357cf Shader compiler: bound emitted loops and inlining — the AI-shader security barrier
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>
2026-08-03 09:19:01 +02:00
Admin
88183a924b Fix http_server dropping pipelined body bytes; add Intent::Authoring to game_net
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>
2026-08-03 02:20:54 +02:00
Admin
d41b6e43ec Arcade M0r: parity-neutral sim internals cleanup
- 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>
2026-08-02 23:34:45 +02:00
Admin
0ebe270c20 Script VM GC campaign: isolate GC, host-transient release, cross-VM resource handle fix
- 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>
2026-08-02 22:27:27 +02:00
Admin
292a47d7e6 Shader: register _mp_unpack ids in the GLSL/WGSL luts — Linux vertex compile fix
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>
2026-08-02 19:53:19 +02:00
Admin
882cf6e440 Shader: unpack2f16/unpack4u8 on all backends — GLSL ES3 built-ins, WGSL mapping, headless Rust impls
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>
2026-08-02 17:48:44 +02:00
Admin
ee96a23a3e macOS: present gate at 3 in flight — the >=2 gate single-buffered the pipeline (50ms lock)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 17:48:43 +02:00
Admin
1c563efc20 Shader: unpack2f16/unpack4u8 registered in shader scope + CPU impls (bit-exact)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 17:48:43 +02:00
Admin
196539995c Shader compiler: unpack2f16/unpack4u8 intrinsics (Metal) — packed map vertex format foundation
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>
2026-08-02 17:48:43 +02:00
Admin
c073c08596 Revert to known-good: pass batching opt-in (MAKEPAD_BATCH_PASSES=1), AI loading opt-in (MAKEPAD_AI=1)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 17:48:43 +02:00
Admin
b387a6d5be Metal: flush batched offscreen buffer at present-pass ENTRY — restore GPU/CPU pipelining
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 17:48:43 +02:00
Admin
c59fe0b0eb Metal: frame-batched command buffer for offscreen passes + big benchmark window
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>
2026-08-02 17:48:43 +02:00
Admin
097a57ab70 Metal: MAKEPAD_GPU_PROFILE=1 per-pass GPU profiler + gpu_ms in map frame log
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>
2026-08-02 17:48:43 +02:00
Admin
b1329124eb Map: layer-skip, keep-stale fixes, tilt priority, cancellation, budgets
- 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>
2026-08-02 17:48:42 +02:00
Admin
419d77c652 Audio: half-duplex echo control — duck only while the assistant speaks
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>
2026-08-02 17:48:41 +02:00
Admin
1288ed7471 Audio: gentler VPIO ducking — standard mode at Min level, env-tunable
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>
2026-08-02 17:48:41 +02:00
Admin
cff99f0a22 Audio: fix VPIO for real — native format, retain balance, error release
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>
2026-08-02 17:48:41 +02:00
Admin
72c0330d95 Audio: fully tear down VPIO on mic-off; set ducking config post-allocate
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>
2026-08-02 17:48:41 +02:00
Admin
aad72dfe2b Audio: never auto-select loopback; fix VPIO init + minimize ducking
- 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>
2026-08-02 17:48:41 +02:00
Admin
a17f745ccb Add missing platform/src/event/location.rs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 12:56:19 +02:00
Admin
1afcedd8f6 NL-wide bridge-dz overlay + mbtiles-merge; fix VoiceInput unit handling
- 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>
2026-07-31 12:38:48 +02:00
Admin
22316f9b8c Audio: echo-cancelled capture option (Apple VoiceProcessingIO)
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>
2026-07-31 12:16:29 +02:00
Admin
723b7928bb Route assistant: mmap LLM weights, fix batched-attention corruption, kokoro voice out
- 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>
2026-07-31 12:13:31 +02:00
Admin
676255dfc2 Land route app + platform geo/permission APIs, map nav layer, geodata radar
apps/route: AI trip planner on MapView — tool broker + local/cloud agent
dispatch, nav session + simulated drive, layers/theme state, DDG image
search, trip history, tilt-shift DOF layer (linear circle-of-confusion:
level = log2 of radius, constant growth rate, tilt raises only the
ceiling; tilt-shift on by default).

Platform: Cx geo location API (macOS/iOS/Android/web) + permission
plumbing, memory watchdog, headless build cfg. Map: nav layer M0 API,
landcover drape, bridge dz. Geodata: 250m dual-radar compositor
(radar_volume) + KNMI sync. Docs for the route/glass/blur/shiny work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 00:10:52 +02:00
Admin
0d8c4773d8 Glass blur: 1/8-res floor for deep mips, bicubic sampling, headless JIT fixes
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>
2026-07-31 00:10:52 +02:00
Kevin Boos
8fa33c86fc fix(linux): don't abort on machines with no usable audio device (#1154)
* 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.
2026-07-30 21:25:38 +02:00
Admin
7f3f568933 map: opacity-aware AA fringes, single-level translucent rings; macos: skip foreign windows in scroll path
- 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>
2026-07-30 10:42:24 +02:00
Kevin Boos
1b558cefd9 Linux: derive window app id from the binary name, or app override (#1151)
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.
2026-07-30 08:51:06 +02:00