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>
CAMetalLayer with display sync throttles nextDrawable when the
compositor consumes frames unevenly (hardware-mirrored / scaled
displays): the main thread blocks 10-25ms in phases, felt as hiccups
in any sustained-animation app. Track in-flight presents per window
via addPresentedHandler; when two of the three pool drawables have
not reached glass, skip the paint beat and keep the pass dirty — the
next timer beat retries with the pool drained and event handling
never stalls behind vsync.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fix rendering, gradient, sampling, etc issues on older GPUs
* Fix bug in `box_y` sdf function, which caused gradients to be split
into two bands incorrectly. Mostly a problem on lower-res screens.
* Use per-texture filtering instead of GL sampler objects on Linux,
especially for Mesa drivers that ignore min filter samplers.
This should help prevent blocky/pixellated things like emoji/avatars
* Improve rendering sharpness on low-DPI screens (icons, emoji, images)
On 1.0-DPI screens, emoji/SVG-icons/avatars were minified without
adequate sampling and SVG AA was sub-pixel, producing blocky/aliased
output. This reworks each path and adds optional full-window SSAA.
SVG icons (device-aware AA + round caps):
- draw_svg.rs/draw_vector.rs/render.rs: size the fill & stroke AA
fringe and the curve-flatten tolerance in DEVICE pixels (≈constant
regardless of icon size), so edges resolve via the analytic
d/fwidth coverage and curves stay smooth at any scale; re-tessellate
on scale change.
- triangulate.rs: thread the flatten tolerance through path fill/stroke.
- tessellate.rs: emit round caps as a solid disc (u=0.5) instead of a
radial fade that collapsed to a square at small sizes.
- widgets/icon.rs: don't clip the Icon to its Fit bounds, so round
caps that extend past the box render fully instead of being sheared.
Emoji:
- glyph_raster_image.rs/rasterizer.rs: rasterize color emoji near the
on-screen size with an alpha-weighted box downscale (geometric-mean
scale factor) instead of the font's native PNG strike.
Images / avatars (mipmaps):
- image_cache.rs/texture.rs/draw_list.rs/lib.rs: optionally emit a CPU
mipmap chain (VecMipBGRAu8_32) for non-animated images so minified
avatars/thumbnails sample cleanly. Env-gated MAKEPAD_IMAGE_MIPMAPS;
default on for GL on Linux.
- metal.rs: real per-level mip upload. d3d11.rs/vulkan.rs/web_gl.rs:
safe single-level fallback (no crash; real mips TODO).
Full-window supersampling (optional):
- window.rs: render the whole UI into an offscreen target at
MAKEPAD_SUPERSAMPLE× device resolution and downscale-resolve into the
window. Default 2×, env-tunable, 1× disables. Modeled on the existing
GaussStack render-to-texture path.
* don't unconditionally enable supersampling SSAA of 2x by default
it's too expensive and too slow for most older devices
* cleanup, reduce comment verbosity
* improve SVG anti aliasing
* Windows: fix laggy/juddery scroll performance
- Pace the render loop to the display refresh using a DXGI frame-latency
waitable object and present with vsync, replacing the free-spinning,
uncapped Poll loop that caused uneven scroll cadence.
- Coalesce consecutive WM_MOUSEMOVE messages and paint once per loop pass
to stop the judder when moving the mouse during fling deceleration.
- Cache get_dpi_factor() and the WM_NCHITTEST WindowDragQuery result to
avoid per-mouse-move GetDeviceCaps syscalls and widget-tree hit-tests.
- Throttle XInput/DirectInput polling of empty/disconnected controller
slots, which was stalling the UI thread.
- Rework the momentum fling to a native exponential model with a
frame-interval EMA, and stop the tail auto-scroll from fighting an
active fling/drag.
- D3D11: update the glyph atlas and image textures in place via
UpdateSubresource instead of recreating them on every change, and
spread D3D11 shader-object creation across frames.
- Slug atlas: only force a full re-layout on a width change; append rows
on height growth.
* Windows: correctness fixes from review (off the scroll hot path)
- d3d11: close the DXGI frame-latency waitable HANDLE in Drop (it was
leaked once per main-window lifecycle and the field comment was wrong);
keep popup swap chains at frame-latency 1; track the waitable-swapchain
flag for ResizeBuffers instead of inferring it from the handle; present
without the vsync interval during a live resize.
- windows.rs: poll game input on the idle signal tick so a gamepad button
can be serviced while the app is otherwise idle.
- win32_window / window: invalidate the WM_NCHITTEST / WindowDragQuery
caches on window move and on a caption relayout, with a generation
counter guarding against a reentrant invalidation being clobbered.
- windows_game_input: detect a controller already plugged in at launch via
a one-shot full scan on the first poll, probe slot 0 (Player 1) first,
and offset the DirectInput enumeration so it never stacks with the
XInput probe.
- comment/doc corrections.
* Image cache: accept any Arc<D: AsRef<[u8]> + ?Sized> for async image data
The load_image_from_data_async family required Arc<Vec<u8>>, forcing callers
that already hold the bytes as Arc<[u8]> (e.g. a content-addressed media cache)
to copy the whole buffer via .to_vec() just to satisfy the type. Generalize the
data parameter to Arc<D> where D: AsRef<[u8]> + ?Sized, so those callers can pass
their existing Arc by refcount-clone with no byte copy. The decode path only ever
borrowed the bytes (&[u8]), so this is purely a signature relaxation; existing
Arc<Vec<u8>> callers are unaffected (D = Vec<u8>).
* Linux: GL glyph-atlas in-place texture update + X11/Wayland mouse-move coalescing
* Scroll: unified fling model + native trackpad momentum deceleration
Share one kinetic-scroll model between PortalList and ScrollBar (and thus
ScrollXView/ScrollYView/ScrollXYView) via a new widgets/src/scroll_motion.rs:
- Touch-drag flicks use an iOS-style exponential self-decay, frame-rate
independent via a per-frame integrator with dt smoothing.
- Trackpad scrolling applies the OS momentum directly while fast (responsive,
full native speed), then hands off to a gentler self-decaying tail once it
slows past a threshold, so the deceleration is longer and smoother than the
OS's short, choppy tail. Handoff is seeded at the current speed for a
continuous transition; the seed is clamped against degenerate event timing.
- Add ScrollPhase to scroll events, mapped from NSEventPhase/momentumPhase on
macOS and wl_pointer AxisStop on Wayland; None elsewhere (wheels/X11/Windows
behave as before). MAKEPAD_RAW_TRACKPAD_MOMENTUM=1 bypasses the smoothed tail.
- A press catches an in-progress fling (stops the scroll, consumes the press so
it doesn't also activate a child), matching iOS/Android/macOS.
* Shader codegen: prefix Metal/WGSL locals to avoid reserved-word collisions
The Metal/WGSL backends emitted user-declared shader locals verbatim, so a
local named after a reserved type keyword (e.g. `half`) produced invalid
shader source and failed to compile at runtime. Prefix them with `l_` like the
HLSL/GLSL backends already do.
* TextFlow: don't panic on unbalanced HTML close tags
end_code/end_quote unwrapped the area stack, so a stray `</pre>` or
`</blockquote>` in untrusted content (e.g. a chat message) panicked. Return
early instead, and drop a vestigial per-list-item area-stack push that leaked
an entry and could hand a stray close tag the wrong block's area.
* Scroll: expose fling decel, handoff threshold, & tail-decel as `#[live]` fields
This allows app devs to override the scroll feel per-widget in the DSL,
or globally by overriding the base widget's defaults — verified that a DSL
override takes effect.
* Windows: fix frame pacing, paste crash, and wheel input backlog
- Wait on the frame-latency waitable right before each window's vsync
present instead of on every Paint, so input no longer stalls behind
waits that have no matching present. Drain leftover credits after a
live resize.
- Pasting when the clipboard has no text no longer panics.
- The poll loop now handles up to 32 messages (2 ms) per frame and
merges consecutive mouse-wheel messages, so fast wheels can't build a
backlog that keeps scrolling after the gesture ends. Sleep 1 ms when a
frame presents nothing so animation polling doesn't spin a core.
* Image: don't build unused mip chains; fix stale images in recycled widgets
- Only build the CPU mip chain when a backend actually uploads it
(Metal, behind its env var), and build it on the decode thread instead
of the UI thread. Linux GL still gets its mipmaps via glGenerateMipmap
and now retains less CPU memory per image.
- Recycled Image widgets no longer show the previous item's image or
apply an old decode result. Placeholder textures set via set_texture
(like blurhashes) stay visible while the real image decodes, and a
failed load clears the widget instead of leaving old content up.
* Widgets: avoid needless caption redraws; cheaper PortalList height tracking
- Label::set_text does nothing when the text hasn't changed, and the
window caption title is only synced when it actually changes, so mouse
moves and animation ticks no longer redraw the whole window every
event. The caption centering padding requests its own redraw now.
- PortalList records item heights only when new or changed, and only
re-applies the default height after it drifts by half a pixel, so big
lists don't walk every unmeasured item on every scroll frame.
* Text: cache layouts of long texts; stop cloning glyph outlines every frame
- The layout cache now accepts texts of any length (long messages and
code blocks used to re-layout on every scroll frame). It is a real LRU
with a byte budget on top of the entry cap, and texts drawn in the
current frame are never evicted, so one heavy frame can't thrash the
cache into a permanent miss cycle. The shaper cache is LRU now too.
- Glyph outlines are shared via Rc, so drawing a cached glyph no longer
copies its command list, and outline complexity is computed once when
the outline is built instead of every frame.
* Html: fix stale links/spans in recycled widgets and <details> renumbering
- set_text only rebuilds when the content actually changed, so a
recycled link can't open the previous message's URL, and re-setting
identical content keeps the user's <details> open/closed state.
- Custom widgets and <details> are keyed by their node index instead of
a visit-order counter, so toggling a collapsed <details> can't
renumber the widgets after it and rebind them to the wrong nodes.
item_with_scope also recreates its widget when the template changes.
* Linux: fixed-distance wheel scrolling; Wayland frame-callback pacing
- Wheel scrolling moves a fixed 60 px per detent on X11 and Wayland
instead of a timing-based guess that flipped between 12 px and 240 px
depending on how events batched. Wayland reads real detent counts via
AxisValue120 (wl_seat v9, with AxisDiscrete as the older fallback) and
maps keymaps MAP_PRIVATE as v7+ requires. Touchpads are unchanged.
- Wayland frames are paced with wl_surface frame callbacks and swap
interval 0, so redrawing a hidden or minimized window can no longer
hang the whole app inside eglSwapBuffers (compositors withhold frame
callbacks for hidden windows). Windows with a callback in flight skip
presenting and stay dirty; X11 keeps vsync exactly as before.
* Text: bigger layout cache budget, reclaimed at the end of each frame
A maximal ~60 KB message lays out to roughly 4 MB of glyphs, so the 4 MB
budget couldn't hold even one alongside a normal screen. Raise it to
16 MB, and run eviction at the end of every frame so memory over the
budget is freed one frame after its content leaves the screen, instead
of lingering until some later layout happens to insert a new entry.
* Fix oversized uniform slices: the array lengths were in bytes, not f32 elements
* Wayland: flush buffered mouse motion before scroll events, and drop motion for closed windows
* GL: fall back to non-mipmapped filtering when glGenerateMipmap fails on strict GLES3 drivers
* Text: bucket emoji raster scales so zooming reuses atlas slots instead of re-decoding every step
* Image: add has_content() and record texture provenance on cache-hit loads too
* Scroll: native trackpad momentum, Chrome-model bounce; presses catch motion, never click children
* Dock: redraw the newly selected tab immediately when the active tab is closed
* Html: standard link colors with pressed precedence; skip re-parsing unchanged text; color setters
* Image: don't redraw on cache-hit loads of the already-bound image (per-draw reloaders looped forever)
* Scroll: log macOS momentum-end phase bits to check Cancelled (touch-cut) vs Ended (natural fade)
* Scroll: momentum state machine; flicks survive pagination; edge sentinels & once-per-frame actions
* Scroll: remove the MAKEPAD_SCROLL_DEBUG diagnostics
* Scroll: time-based fling velocity window; pointer fan-out guard; parked flings survive pagination
* PortalList: optional reached-start/end margins (Some(0) default); repositioning re-announces the edges
* Linux/Wayland: honor UI zoom across window resizes; scale caption bar with zoom
Keep the wayland-side window geom in native units so the zoomed dpi isn't
read back as "native" on the next Configure — UI zoom no longer resets or
flickers on maximize/tile. Also let the caption bar height scale with the
zoom (pin to native only on macOS, where the buttons are OS traffic lights).
* Windows: D3D11 fixes for drawlist mgmt
trying to help with the `new_batch` bugs
* draw_list.rs — `set_zbias` now returns whether it changed
* d3d11.rs — uploads draw_call_uniforms on the given condition:
`uniforms_dirty || zbias_changed || buffer.is_none()`
and the zbias advance is hoisted above the early-continues
* Scroll: hard flicks carry farther and boost on re-flick; iOS-style touch rubber band with per-edge bounce gating; Android fling spline behind a flag
* Splitter: redraw both pane subtrees on drag; cached views keep fresh fixed sizes in the dirty check
* iOS: re-deliver window-geometry changes dropped by re-entrant UIKit callbacks; Init only ever from the first draw
* final cleanup fixes. Ensure all examples, experiments, `studio` all work
* Image: skip re-parsing an SVG that is already shown, keyed on the caller's shared bytes
* CachedView: fix upside-down offscreen texture on GL/GL-ES
GL/GL-ES store offscreen FBOs bottom-up, unlike Metal/D3D/WGSL.
The DSL->script-shader migration switched the CachedView composite to plain
.sample() (non-flipping sample2d on GL), so cached views rendered
vertically mirrored on GL/GL-ES; Metal was fine on macOS.
Add a `sample_rt` script sampler (emits the V-flipping sample2d_rt on
GLSL, plain no-flip sample elsewhere) and use it in the CachedView and
CachedRoundedView composites.
* Fix `AdaptiveView::redraw()` to actually do something
Verdict on the gamemaker judder (measured, 25s A/B runs, same window size):
gauss OFF -> unchanged (25-33ms gaps) [pyramid exonerated]
empty game world -> unchanged [game exonerated]
vsync OFF -> FIXED (119.8fps steady, worst 9ms; slow callbacks 124->2)
The stall is CAMetalLayer display-sync throttling: the compositor (hardware-
mirrored + SwitchResX-scaled 7680x2160) returns drawables unevenly, and
nextDrawable blocks the main thread 10-25ms in phases. The durable fix is
present-gated pacing: track in-flight presents via addPresentedHandler and
skip the paint when the pool is busy instead of blocking the event loop.
MAKEPAD_TIMER_TRACE=1 logs paint-clock fire-to-fire gaps >20ms and slow
callbacks >10ms with a per-step breakdown (live_edit/net/pad/paint/gc).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fixed 8ms timer0 presented ~125Hz into a 120Hz vsync queue: the drawable
pool drifted full and nextDrawable blocked the main thread in a ~25-frame
sawtooth (PerfGraph 'wait' ramps). Timer now arms at 1.002/max_fps across
attached NSScreens — the +0.2% makes NSTimer lateness drain the queue instead
of accumulating. Sawtooth verified gone.
perf_monitor: 'gpu' channel — presented-frame GPU interval tapped from the
existing command-buffer completion aggregation (atomic hand-off, folded at
frame_boundary; concurrent with CPU channels, plotted violet).
metal: MAKEPAD_GPU_PASS_TRACE=1 logs per-pass GPU time ([gpu-pass] name ms)
for frame-budget hunts.
Measured (gamemaker, empty world vs full racing game — identical ~4.7ms GPU):
the frame cost is the UI glass pipeline, not the game — scene capture +
6 mip downsamples + 3 smooth upsamples re-run every frame at 120Hz because
the game pane dirties the window; at low GPU clocks the 11-pass chain's
latency swings past the 8.3ms budget in phases.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
platform: Cx.perf_monitor — per-frame ring (240) of paint-to-paint gap +
per-channel CPU us. Built-in channels: event dispatch (outermost, minus
app-attributed time), script exec, GC, pass encode, nextDrawable wait.
Apps register custom channels: cx.perf_monitor.channel("physics", rgb).
Off until enabled; hooks in event dispatch, macos repaint, metal draw_pass.
widgets: PerfGraph — corner-pinned live panel (DrawVector strips): frame-gap
bars colored against 120/60Hz budgets with guide lines, stacked per-channel
CPU, legend with averages. Self-positions bottom-right (DrawVector geometry
+ deferred turtle alignment don't mix — no aligning parent).
gamemaker: PerfGraph hovers the game pane (F3 toggles), engine feeds script
+ physics channels (incl. hot-reload evals); engine text overlay moved to
F4; per-phase engine window kept for ag perf / AIGAME_PERF=1; new 'ag perf'
harness verb + template guidance (template CLAUDE.md force-added: runtime
resource, blanket CLAUDE.md gitignore had kept it untracked).
Measured on my-game-5: engine frame CPU ~0.3ms; the hiccup is frame pacing —
the 8ms NSTimer paint clock beats against the 120Hz display, the drawable
pool drifts full and nextDrawable blocks the main thread in a ~25-frame
sawtooth (avg 2-3.4ms, spikes 20-30ms). The graph shows it as red ramps.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>