Squashed from work; the fine-grained history is under tag archive/work-2026-08-26:
- render+sim: the two hooks the model viewer already relies on
- viewport: Realtime gets the engine's cascaded shadow pass, the NOAA sun, metered exposure with sky ambient, a haze knob, a time-of-day slider in the header, and no grid — the building itself still waits for its casters and direct light
- render: the engine's receiver shader takes the sun with two-sided normals and the shadow term for the model batches
- fab: a 3D creation shell and the viewer built on it
- render: two shaders that never compiled — a let is not assignable, a var is
- sim: a declared map facing becomes a body's heading through one rule
- render: a HUD that already reads as a game's before anyone styles it
- asset+sim: the two modules their own commits already declared
Squashed from work; the fine-grained history is under tag archive/work-2026-08-26:
- audio: a FLAC decoder from the specification, beside the MP3 and Vorbis ones
Squashed from work; the fine-grained history is under tag archive/work-2026-08-26:
- vj: the tweener learns — RIFE runs on the Mac and feeds the warp
- asset-ai: the chat tells the truth while it works
- llm: a cold turn on the solo slot takes the session-native speculative path — think-mode turns no longer re-ingest the whole conversation through the draft head (66 → 122 tok/s on the four-lane box)
- llm: the step cost model is chosen per device — the RTX PRO 6000's measured verify curve (13.7 + 3.17·B ms) beside the 5090's; the bench warms every tail shape and times two windows
- vj: local store, lyrics and model plumbing, and the frame-interpolator's device parity check
Squashed from work; the fine-grained history is under tag archive/work-2026-08-26:
- vj: the thumbnail pipeline becomes one honest machine, and effects go livecodable
- repo: context_ladder scratch bin stays local, not shipped
- vj: thumbnails become mp4 — hardware-coded sheets at measured-4K cells, and the bake stops racing the GPU
- store: the ceremony dies — batch publish, one transaction, and the engine stops re-reading its own log
- vj: the console grows real transports, and the deck stops lying about reverse
- models: an interrupted install can never load broken
- importer: the classic worlds stop being mirror images
- asset-ai: the chat tells the truth while it works
- sqlite: derived tables get their real names, their predicates, and all their arms
- llm: the step cost model is chosen per device — the RTX PRO 6000's measured verify curve (13.7 + 3.17·B ms) beside the 5090's; the bench warms every tail shape and times two windows
- importer: a sound and a single-tile sprite publish a picture like everything else
- asset: hardware sha256 kernels, proved against the software oracle before they run
- sim: a declared map facing becomes a body's heading through one rule
- asset: the batch publish route, with the hostile cases it has to refuse
- asset: an example that asks a live store which assets carry a thumbnail
- asset+sim: the two modules their own commits already declared
- asset: ActorDef::scaled — every linear quantity follows the map's person height — plus the place-dump and retire-stale store examples, and the game chat context stops reporting work it did not do
Squashed from work; the fine-grained history is under tag archive/work-2026-08-26:
- store: the ceremony dies — batch publish, one transaction, and the engine stops re-reading its own log
- sqlite: derived tables get their real names, their predicates, and all their arms
Squashed from work; the fine-grained history is under tag archive/work-2026-08-26:
- vj: the console grows real transports, and the deck stops lying about reverse
- vj: video goes NV12 end to end, and the GPU does the unpacking
- widgets: boxed labels center on their ink, not on the font's line box
- metal: a fresh texture forgets nothing it never had — reallocated vec textures upload whole
- fab: a 3D creation shell and the viewer built on it
- widgets: a dock redraws its panels, not just its own frame
- draw: hsv2rgb takes vector bounds, so it compiles
- texcomp: the block codec and the container every texture will travel in
- widgets: a Splash keeps the walk its host declared across a body rebuild, and logs a body that fails to evaluate instead of drawing nothing
- vj: DJ-tab scroll knobs, title-click reset, crossfade fix, stem headroom — plus the pre-Ampere CUDA fix (#1193)
Co-authored-by: Rogier de Leeuw <vjroger@gmail.com>
Squashed from work; the fine-grained history is under tag archive/work-2026-08-26:
- vj: video goes NV12 end to end, and the GPU does the unpacking
- script: parser, with the case that caught it in the test suite
Squashed from work; the fine-grained history is under tag archive/work-2026-08-26:
- vj: thumbnails become mp4 — hardware-coded sheets at measured-4K cells, and the bake stops racing the GPU
- windows: the paint beat becomes the swapchain's own beat, and /g learns to see
Squashed from work; the fine-grained history is under tag archive/work-2026-08-26:
- vj: thumbnails become mp4 — hardware-coded sheets at measured-4K cells, and the bake stops racing the GPU
- vj: the console grows real transports, and the deck stops lying about reverse
- vj: the GPU learns to see motion — realtime frame tweening on every deck
- macos: the paint beat becomes the display's own beat
- windows: the paint beat becomes the swapchain's own beat, and /g learns to see
- metal: vec textures ride the command stream — the CPU stops overwriting what the GPU is still reading
- metal: instance buffers stop being rewritten under a live draw
- macos: next frames and draws are stamped with the flip they aim at
- metal: a fresh texture forgets nothing it never had — reallocated vec textures upload whole
- metal: a watchdog for stalled command buffers — it names the pass, and only aborts when asked
- macos: resetCursorRects no longer aborts the app when AppKit re-enters it
- macos: nothing panics across resetCursorRects — the callback is shielded and its cursors are retained
- vj: the next pair's fields are fetched ahead of the change under the capacity law — a pair change costs an ordinary beat; macos: the layer's own display link paces the frame when the system offers it, the old path stays as fallback
- macos: a drawable from the layer's display link is presented plainly — presenting it at a time is forbidden and raised in every visible window
- macos: a window paced by the layer's display link never asks the layer for a drawable — the beat waits for the link's update; ObjC exceptions are logged with their reason before they unwind
- macos: the layer's display link is opt-in (MAKEPAD_METAL_DISPLAY_LINK=1) until it paces at the display's rate — 11 fps visible against 62 on the proven path
- macos: the layer's display link asks for the screen's maximum rate, consumes every drawable it hands out, and traces updates/consumed/presented per second — still 75 ms per present under a drag, so it stays opt-in
- fab: a 3D creation shell and the viewer built on it
- raytrace: the traced pane starts coarse and doubles to native, with the raster underneath
- macos: a link-paced beat blocks till the next flip, and an armed paint clock means wait — the main thread no longer polls at 100% CPU between frames
Squashed from work; the fine-grained history is under tag archive/work-2026-08-26:
- vj: thumbnails become mp4 — hardware-coded sheets at measured-4K cells, and the bake stops racing the GPU
- vj: reverse earns a memory, and the effects stop aging
- vj: video goes NV12 end to end, and the GPU does the unpacking
- windows: the paint beat becomes the swapchain's own beat, and /g learns to see
- platform: the time repaint stops resurrecting passes their owner left behind
- metal: a fresh texture forgets nothing it never had — reallocated vec textures upload whole
- fab: a 3D creation shell and the viewer built on it
- raytrace: the traced pane starts coarse and doubles to native, with the raster underneath
A Turing box (RTX 2080 Ti, sm_75) lost ALL of CUDA because two kernel
files refused to compile for it, and one failed kernel build means the
stub store — surfaced in the VJ as "stems: model error: no compiled-graph
device" on the DJ tab.
diffusion_ops.cu used three sm_80-only pieces unguarded: bf16 wmma
fragments (the type itself is incomplete before Ampere), cp.async, and
the m16n8k16 mma shapes. The cp.async helpers now fall back to
synchronous copies below sm_80 — the f16 wmma flash/sdpa kernels lose
their prefetch overlap on Turing, not their contents — while the bf16
and FA2 kernels are compiled out and their launchers refuse pre-sm_80
devices with cudaErrorNotSupported instead of returning a buffer the
kernel never wrote.
fattn/common.cuh made mkllm_unused_vars constexpr: the no-cp.async
branch of ggml_cuda_fattn_mma_get_nstages calls it, and a non-constexpr
callee poisoned the constexpr config chain on exactly the pre-Ampere
device pass — the arch nobody had compiled for.
Stems verified on the 2080 Ti: stems-ops-check all green (SNR 137-147 dB
against the CPU reference), two tracks separated end to end, output
confirmed clean by ear.
Co-authored-by: vjroger <r.deleeuw@qogni.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
`current_view` only advances once a push or pop transition finishes, so
mid-transition it still reports the outgoing view. Callers that want to
address the view the app considers current, e.g. to retitle it, had to
either duplicate the id themselves or special-case `is_transitioning`.
* Add `destination_view()`, which reports the incoming view as soon as a
transition starts and falls back to `current_view` when settled.
- INSTALL MODELS under the music explorer: a download dialog naming both
MIT weight sets (BS-RoFormer splitter 527MB, whisper large-v3-turbo
1.6GB), where they land and their licenses; resumable sha256-pinned
downloads through the asset-ai downloader (featureless dep — the same
slice the asset UI links); cancel mid-flight (the button flips to
CANCEL, .part resumes later), MB progress, and the row disappears on a
provisioned machine. When the last model lands the loaded decks
separate immediately: the stems worker now re-probes the checkpoint
per job instead of latching its absence, and the lyrics transcriber
unlatches too (the Apple fallback yields to whisper mid-session).
- DrawWaveLane's stem palette moves from instance inputs to uniforms:
36 vertex inputs blew D3D11's vs_5_0 limit of 32 (error X4506), which
left the music decks with NO waveform at all on Windows.
- --remote HOST:PORT binds a named interface so another machine can
drive an app over the LAN (fleet-box testing); bare --remote stays
loopback.
- queue chip: the + glyph centres in its 26x18 box.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The VJ's EFFECT surface is a small renderer of its own. An effect is a
`.splash` DOCUMENT — engine choice, stages, fields, parameters and now the
SHADERS THEMSELVES live in the document rather than in Rust. That is the
shape this commit introduces (dev has never seen an intermediate one): a
document is the whole effect, and the Rust side is the engine families that
documents draw with.
The engine families: particles and emitters, GPU sim swarms and fluid, static
meshes (firefly synchrony, harmonograph loom, domino liturgy), tiles, flock,
clouds, city, pipes, stock charts, an SDF raymarcher with a subclassable
`scene_sdf`, and a mountain-jet endless range with a beat-pulsed fighter.
Three things make them a system rather than a demo reel:
- SIM FIELDS — a float simulation-texture primitive, so an engine can carry
GPU state across frames (wind, particles, fluid) instead of being a pure
function of the clock.
- CONTENT COUPLING — `content:` in a document and `input0` on every engine's
tex0, so an effect can consume the program video: drapes, backdrops,
mirrors, billboards, chrome, frescoes, canopy, glass, silk, wall, swarm,
pen and mosaic families all take the picture playing behind them.
- MUSIC BINDING — shaders read the beat, so the whole library moves with the
track rather than on its own clock.
Roughly a hundred seeded preset documents ship with it, each with a lazily
rendered animated thumbnail (rendered 4x and box-downscaled, 16-tap SSAA);
the thumbnail pass went from four minutes to eleven seconds. CONTRACT.md is
the document contract and its verify recipe; IDEAS.md is the campaign tracker.
A VJ console that is its own asset store: five surfaces (VIDEO program tiles,
MUSIC DJ decks, SFX, EFFECT slots, LIGHTS) over the asset server, driven by
APC40 hardware or the screen.
The transport is the centre of it. The SWEEP LAW: one direction, one beat
step, and a pacer that never hiccups — a loop wrap is never fast-forwarded and
the grid keeps no holes. On top of that: bracket-to-bracket scrub math so the
playhead learns the user's trim, SCRATCH as a sprung shuttle on every deck
transport, ping-pong and per-slot mute, hot-standby decks, one rate authority,
and beat sync that fits a whole clip into N bars at a musical rate while
cached loops keep real wrapping pts so the phase survives.
VARIABLE FRAMERATE VIDEO IMPORT measures the flow field a clip needs without a
model (libs/video_flow), which is what makes free-rate bounce-looping GPU
playback possible; the enhance pipe uprezes and tweens a deck clip in one
decode and one encode, with the motion vectors inside the mp4.
The console itself: makepad orange and the brand lockup, Blender-style BPM,
system tooltips, popover sliders, bracket trim handles, one button family and
a no-push band, doc-labeled slot dials with MIDI learn and strict typing, a
BEATS dropdown and slow-biased jog, an IMPORT panel that arms, runs and stops,
and BLAST — one press invents a visual for every parallel pipe. Generation
runs six jobs in flight across the fleet, and a job row names what it made.
The library grid fills as a relay — page, detail, manifest, blob, decode,
texture — and every hand-off used to be picked up by the 20Hz poll timer, so
each hop cost a tick however fast the store answered. The hand-offs now run on
the frame while anything is owed, manifests jump the queue like the details
that produced them, and resolve/decode/worker width all widen while nobody is
on stage and narrow together the moment the program window opens or a deck
plays. The visible page resolves first. Measured on the same warm cache: a
48-tile page 4.7s -> 1.9s, a tab's first visit 4.3s -> 1.1s, decode wait 48ms
-> 9ms, a second visit painted in the same frame with zero decodes, and no
UI hitches across the session.
Also here: wave analysis (tempo, grid, downbeat, tiles) with a judge suite
that scores the detector against click tracks and a human drummer, stem
separation, karaoke lyric display, Art-Net/DMX light control, and an output
window that projects without janking on Windows mouse movement.
466 tests, zero warnings.
- README: a build quick-start for macOS and Windows, the honest Linux
story, what CUDA is for and how to install the separation model.
- AGENTS.md: the `--remote` control surface protocol, so the harness it
documents is usable without reading platform/src/remote.rs.
- Cargo.toml: workspace membership for the crates this series adds and
removes.
- tools/: the Windows box scripts (wincmd, winps and friends, winrun) and
remote_smoke.sh — how a build gets driven on a remote machine.
- apps/asset-server: the standalone server binary and its README.
- Small follow-ups in libs/{windows,apple_sys,makepad_test,mbtile_reader,
converse} and apps/route, plus .gitignore and makepad.splash.
Four new examples, each of which is also the test surface for the thing it
demonstrates:
- datagrid — the DataGrid widget across five tabs (sheets with a formula
engine, big data, charts, pixels, widgets).
- portallist_hit — PortalList hit-testing, with a UI test suite.
- render_to_texture — the offscreen pass, with a UI test suite.
- splat_bench — Gaussian splat sorting and drawing under load.
uizoo gains a dropdown tab; the splash example's UI test follows the splash
host-services change.
The asset browser becomes the front end for everything the AI stack can do:
- analysis — a content analysis pass over imported assets.
- mask_paint — paint a mask over an image for inpainting.
- music_page — the music generation surface.
- webcam — live capture as a generation input.
- fast_presets, store_content — preset and store-content plumbing.
Around them the existing surfaces get the corrections the store and the video
widget forced: sprite-split and child-pass thumbnail fixes, one video widget
everywhere (opens stop fighting each other and the rail never reflows),
grouped classic/Duke import that lands 102 assets instead of a card flood,
chat that greets with silence rather than a banner, and a pipeline that
survives restarts.
libs/render picks up the two biggest new modules in the group: `level.rs`
(the imported-world runtime) and `player_nav.rs` (walkable-surface planning
with clearance bands). The renderer, shader set, GPU lightmap and skinning
paths all grow with them — a chart-edge texel is no longer trusted with a
skirt's light, a lamp only receives what the sky is not already delivering,
and lamp photometry comes from the fixture rather than the mesh scale. A
`walk_probe` example drives the navigation directly.
xr gets splat packing and a GPU splat sort (`splat_pack.rs`, `splat_sort.rs`,
with tests), and view_splat is largely rewritten on top of them.
remesh, xatlas, gltf, splat and sim carry the supporting work: the xatlas
unwrap hang is fixed and the pass is several times faster, the glTF writer
emits the rig and vehicle contracts, and sim grows the entity layer the
imported worlds drive.
Another dependency the app should not be asking the platform for:
- audio_decode — MP3 (layer 3, LSF tables, synthesis) and Ogg Vorbis
(codebooks, floor, residue, MDCT) decoders, with tag reading. Both are
checked against oracle fixtures rather than against our own expectations.
- audio_encode — an Ogg Vorbis encoder: MDCT, psychoacoustics, floor and
Huffman coding, setup tables, plus `oggenc` and `audiobench` binaries.
- audio_picture — waveform and spectrogram rendering, and compositing.
- audio_lyrics — word-level lyric alignment (DTW plus a DP snap) and the
baked schema behind karaoke timing.
- audio_sidechannels — the side-channel plumbing between them.
libs/voice grows a CUDA backend and an alignment path beside its CPU decoder,
with a `whisper_parity` binary to keep the two honest.
The model code was spread across eight crates that had grown into each other:
ggml and cuda and mlx each owned part of a tensor runtime, llama and tts and
voice2 each owned part of a model, and libs/diffusion owned everything else.
They are now one tree with an explicit shape:
libs/ai/cuda — kernels and launch surface
libs/ai/metal — Metal shaders and the shim
libs/ai/llm — the language-model runtime (sessions, lanes, contexts,
the CUDA and Metal executors, the compiled Metal path)
libs/ai/models/ — common, flux, h3, music, paint, speech, stems, vision
libs/diffusion is not deleted but demoted: what remains is the VALIDATOR
crate — several dozen `*_validate.rs` oracles that check a native
implementation against a reference, which is where they belong now that the
implementations live next door.
The functional work inside the move is mostly in the LLM runtime: N lanes that
draft while one verify batch serves all of them, per-slot prefill over a shared
folded attention arena, speculation that survives batching, and a scheduler
that reports rather than publishes. And in the CUDA build: a machine without
usable CUDA must still LINK (and say so), the default kernel arch is the
building machine's GPU, `NO_CUDA` forces the stub even where the toolkit
exists, and kernels compile in parallel with progress.
libs/video_flow is new here: classical optical flow estimation and the `mkfl`
motion-field payload — a flow field measured from a clip without a model,
which is what drives free-rate bounce-looping playback and the uprez/tween
enhance pipe.
The asset store now uses libs/sqlite_query as its ONLY engine — not a feature
flag, not a fallback. That closes the Windows gap (the embedded store starts
there now, and a SHARED->EXCLUSIVE upgrade is handled rather than assumed
free) and takes the C dependency out of the build everywhere else.
Around it:
- store: a garbage collector, catalogued content that is referenced in place
instead of copied, the `vjeffect` kind, and host/chat routes that keep up
with the chat wire below.
- importer: the unified map contract reaches quake2, quake3, doom and duke —
world placement, nav, welding, prelit maps and glTF node handling shared
rather than reimplemented per game. Music import, billboards and stateful
props move to the data crate so readers stop linking the importer.
- ai: the serving side of multi-lane chat — per-lane conversations, honest
progress and acceptance reporting, penalties and a watchdog, context as a
per-box number that compacts instead of erupting, a realtime session mode,
and inpaint/flux2 backends. `chat_bench` measures the rate the way the
client meter computes it.
- client / chat / chat_ui: a publication can NAME a file instead of carrying
it; the wire says whether a turn is warm and whether it is thinking, so a
client stops guessing; transcript and feed widgets render history the way
the model wrote it. `SessionConfig::catalog_runtime` lets a host size the
catalog runtime's lanes itself — a browsing UI puts every listing, every
per-tile resolve and every thumbnail blob through that one runtime and
wants a wider fast lane than the shared default, while media lanes keep
it (a few big transfers, not a thousand small ones).
- widgets: the shared asset widgets — one video view (knobbed seek,
transport, bracket trim, rail playback) used everywhere, plus thumb,
preview, scene view, walk-world and the lyric reader.
A from-scratch, dependency-free SQLite implementation: file format reader and
writer (b-tree read and write paths, pager, journal, WAL), a SQL lexer,
parser and AST, a planner, and an executor — plus locking, integrity checking
and a `sqlq` CLI.
It exists because the asset store needs a database on every platform the app
ships to, without a C toolchain in the build and without a system library
whose version is somebody else's decision. The test suite is the argument:
DML, DDL, concurrency, crash recovery, a query corpus and a DML fuzzer, all
checked against real SQLite behaviour rather than against our own reading of
the spec.
Six new widgets, all built for the console-density end of the spectrum:
- DataGrid — a 2D-virtualised grid (rows AND columns), the table/spreadsheet
counterpart to PortalList. Cells host arbitrary widgets from templates.
- Chart — trend and sparkline drawing that composes inside docks and grids,
which the old DrawVector-based chart could not.
- ComboBox — a text input with a filtered, keyboard-navigable popup list;
type-to-filter rather than pick-from-a-menu.
- Tip / TipLayer — system tooltips: a shared overlay layer, hover timing and
placement handled once instead of per widget.
- ValueInput — a numeric field you can also drag, Blender-style.
- DropSlider — a slider that lives in a popover, for consoles with no room
for a permanent one.
widget_tree gets the larger share of the changed lines: observation and
patching paths reworked so a structural rebuild is not the answer to every
change. PortalList picks up the scroll-distance readout and the dead-isolate
guard from upstream; window.rs grows maximize/restore forwarders; splitter
exposes a color hook; fold_header, scroll_bar, text_flow and drop_down2 get
follow-ups.
An app built with `--remote` now serves a localhost HTTP control surface:
window list, per-window PNG grabs, real mouse/key/text injection, widget
rects, a log ring buffer, and `/gq` (grab every window, then quit). It exists
so a test or an agent can DRIVE a running app instead of reasoning about it
from source — the protocol is documented in AGENTS.md. Grabs are targeted per
window (`/g?w=N`), so a multi-window app is captured window by window rather
than whichever pass happens to present first, and `log!` mirrors into the ring
buffer without anyone owning the app's stdout.
platform/video grows a streaming half beside the file half. StreamEncoder /
StreamDecoder with Apple VideoToolbox and Windows Media Foundation backends,
Annex-B framing, and all-intra bound through pEncodingParameters on Windows —
the only control that MFT actually honors, as the readbacks claim success for
everything else. The file decoder can now be asked for a SPECIFIC frame rather
than only the next one, which is what frame-exact seek and bounce playback
need. Tests cover file seek and the stream round trip.
Draw shaders gain `Rgba16F` and `Rgba32F` color formats to pair with the
float render textures: blending off, whole-texel writes, meant for GPU
simulation state (particle position/velocity, fluid fields) rather than
pictures.
Windowing and dialogs:
- `CxOsOp::SetChromelessWhenMaximized` drops the native maximized border
strip on Windows, so a maximized window reads as a clean picture.
- `Cx::open_select_folder_dialog` opens the native folder picker with a
title and start location, answered by a `FileDialogAction` in the actions
pass; cancelling is a first-class outcome, not an error.
- Windows reports a user close the way macos.rs already did.
- macOS swaps the titlebar container so WindowDragQuery alone decides window
drags, and the delegates carry a panic shield.
- `Windows::id_iter()` enumerates window slots generation-correctly.
Headless: the virtual GPU and its rasterizer are substantially rebuilt around
the shader runtime preamble, making `MAKEPAD=headless` render-to-PNG a real
test surface rather than a smoke check. `PerfMonitor::frames_painted()` lets a
scripted driver pace itself to PRESENTED frames instead of queueing passes
faster than the GPU retires them.
* Wayland: mark windows as created, fixing dpi override and pass dpi
Wayland was the only backend that never set `is_created = true` on the Cx
window; every other one does it in `CxOsOp::CreateWindow`.
That flag gates `Cx::dpi_override_scale()`, so every pointer event skipped
the native->layout remap and clicks missed their widgets by the UI zoom
factor. It also gates `get_delegated_dpi_factor()`, which was returning a
hardcoded 1.0 for every draw pass on Wayland, so pixel snapping and the
shader pixel size (SDF AA fringe) used the wrong scale on HiDPI screens.
`SetWindowVisuals` and `set_topmost` were dropped for the same reason.
Also seed the Cx window's geom at creation like the x11 backend does,
otherwise it sits at dpi_factor 0.0 until the first configure arrives.
* convert the seeded wayland geom to layout points and record os_dpi_factor
Seeding the raw native geom left window_geom in native units (and the
os_dpi_factor fallback unset) until the first configure arrived, which is
exactly the pre-configure window the dpi override needs to be correct in.
Do the same conversion the WindowGeomChange path already does.
When an area that already captured a touch sees another touch start,
hits() returns a FingerDown for it (so the owner can handle multi-touch
gestures like pinch) but never marked the touch as handled. Widgets
behind the owner could then capture that second touch themselves: the
second finger of a pinch atop a fullscreen overlay could drag-scroll a
list behind it, or even press a button back there.
* mark such a touch as handled if it actually hit-tests within the
area, mirroring the normal capture path below it
* only do so if nothing else has handled it yet, preserving the claim
of a child widget that captured it earlier in the same dispatch
Killing a Splash isolate while widgets it minted are still in the tree
could panic the whole process in the GC, somewhere else entirely:
platform/script/src/gc.rs:300
index out of bounds: the len is 12 but the index is 19
script_ref_vm_id() resolves a widget's owning VM from a ref the widget
holds. Isolate heaps live in heap_to_vm; anything else was assumed to be
the app VM. But an isolate's widgets outlive it by a frame or two — a
tile dropped mid-gesture, an app force-stopped while its buttons are
still on screen — and their refs were minted by a heap that is gone. So
those calls were routed INTO the app VM, which then stored a dead heap's
object ids in an args object of its own (make_call_args_object_with_
context). Nothing complained: the checked stores silently skip an index
they can't resolve. The next GC walked that object, indexed the app heap
with the other heap's index, and blew up with no trace of the cause.
Reclaimed heaps are now remembered, so a ref from one is told apart from
an app-VM ref and its call is dropped rather than redirected — the same
thing script_timer_dispatch_hook already does with a dead isolate's
timers. Three related tightenings while here:
- with_script_vm_id checks "is this VM already installed?" BEFORE the
main-VM shortcut, and debug_asserts in the main branch. with_vm runs
against whatever VM is parked on Cx, which during an isolate's own
execution is that isolate's — so the old order could silently run
app-VM work in an isolate's heap.
- gc_dead_splash_isolates purges the queues holding a dying isolate's
values before dropping the heap those values live in, matching what
gc_bridge above it already did.
- splash_host_respond checks it landed in the heap the request came
from before minting the answer object there. One usize compare turns
a future routing mistake into an undeliverable answer instead of a
corrupted heap.
Found by a launcher that force-stops a mini-app for hammering the host
bridge: ~40 requests in flight, isolate torn down in the same event
pass, reopened moments later. That reproduced it every time; with this
it no longer does.
The styled mod.widgets.DropDown2 block re-applied hover/active/up/enabled
as instance(...) markers onto the already-typed f32 fields of the draw
shaders, which fails at widget instantiation with "type mismatch:
expected f32, got object". The instance() declarations belong to the
script_shader type registrations; the style block just sets plain
float defaults.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds `PortalListRef::user_scroll_travel()`, the running sum of every
user-driven scroll delta: wheel/trackpad (including OS momentum), touch
drags, scroll-bar drags, and selection auto-scroll. Deltas absorbed at a
clamped edge still count, while the list's own movement (smooth scrolls,
fling coasting, bounce-back, tail-following) does not.
Sampling it at two points and subtracting tells a caller whether the
user scrolled in between and in which direction, which comparing
`first_id()` cannot do once items are inserted or removed.
A bare `if cond { ... }` statement whose final body statement is a
non-void expression compiled to an empty `if(cond){ }`: the expression
never reached the generated shader, so its side effects were lost. In
robrix this silently removed every border drawn as
if self.border_size > 0.0 {
sdf.stroke(self.border_color, self.border_size)
}
A call is not written to the output when it is compiled, it is pushed on
the stack as a string, and it only reaches the output via POP_TO_ME.
Since e0a5a23f2 the enclosing statement's POP_TO_ME is emitted as a
standalone opcode at the if's jump target instead of being fused onto the
body's last call, and the shader compiler closes an `IfBody` as soon as
`ip >= target_ip`, so the opcode sitting exactly at the target is never
seen while the body is open. The body's value was then dropped on the
floor by the `no outer phi` arm, whose comment assumed that could not
happen.
* Emit the leftover value as a statement inside the branch when nothing
consumes it, mirroring the void path a few lines above.
* Add `gpu_stage_4m`, which asserts the call survives into the generated
shader. It fails without the fix.
The parser side is deliberately untouched: `last_jump_target` is load
bearing for the widget-loss fix that `on_render_emission` guards.
* Splash: a host-services bridge so isolates can ask for brokered capabilities
Mini-apps are sandboxed hard (fs/run/res stripped, net gated), which also
means they can't do anything real. This adds the one doorway back: a
mod.host module in every isolate whose host.request(service, args, cb)
queues {app_tag, heap_key, req_id, service, args_json} on a thread-local
the EMBEDDING HOST drains and answers (splash_host_respond re-enters the
isolate under the normal budget and calls the callback with {ok, data,
error}). No policy lives in makepad: an undrained request never resolves,
tags are host-assigned (Splash::set_host_tag) so scripts can't spoof who
they are, and host.capabilities() just echoes whatever grant list the
host last pushed (set_host_caps). Callbacks are rooted ScriptFnRefs keyed
by heap, GC'd with the isolate alongside the storage-jail roots.
Also: call_script_fn_with_strings (string args must be minted in the
callee's own heap), 'let host = mod.host' in both Splash prefixes (line
offsets documented per prefix; the net prefix was already one line off),
and mod.cx.quit is now nil'd in isolates - a mini-app could quit the
whole host process with one call.
* script: stop validation from blessing scripts that failed to parse
The parser RECOVERS from errors (dangling else, missing expression), logs
them, sets had_error - which nothing ever read - and hands back a runnable
module. Nothing enters the trap queue, so a host validating with a
captured_errors sink + take_errors() got an empty list and reported
success; three freshly-written mini-apps shipped real parse errors straight
through host_launcher's validate this way, visible only as stray [E] log
lines.
report_error now also records the formatted message on the parser
(ScriptParser::errors), and both eval paths (eval_with_source and the
streaming eval_with_append_source) drain that into bx.captured_errors when
a sink is installed. No sink = logs only, exactly as before. Regression
tests in tests/parse_error_capture.rs, including the exact fn-final
if/else shape that slipped through.
* splash_host: review fixes — is_ok result field, silent surfaces, JSON hardening
Three classes of fixes from an adversarial review of the bridge:
- The result object's success field is now is_ok. 'ok' is the script
dialect's ok-test KEYWORD, so r.ok never parsed as a field access — every
callback that read it silently died. A pure-VM regression test
(fn_ref_callback.rs) now exercises the exact store-callback-then-answer
flow the bridge uses.
- SplashHostRequest carries may_prompt, set per isolate via
Splash::set_host_prompts: background surfaces (home-screen widget tiles)
are marked silent so a host can fail their permission-needing requests
instead of popping consent dialogs nobody asked for. splash_host_respond
also reports an outcome now (Delivered / NoCallback / IsolateGone) so
hosts can log undeliverable answers, and Splash::isolate_heap_key lets a
host relate a request to a specific widget (IPC fan-out skips the
sender's own isolate with it).
- heap.to_json hardening: a cyclic object graph (script-buildable, host-
serialized on every bridge request) recursed to a stack overflow — now a
depth cap emits null leaves; backslashes were mis-escaped as a single
backslash (invalid JSON downstream), tab and other control chars weren't
escaped at all, and a handle serialized as unquoted junk.
* script: a closure's captured varargs must not shadow its own parameters
A call binds positional args by INDEXING the fn object's vec, which holds
declared parameters — but also, past that, any varargs the call received
(unnamed_fn_arg pushes them with a NIL key). A closure captures the scope
it was minted in, so those leftovers ride along ahead of the closure's own
parameters.
Concretely: script timers invoke their callback with one number (the time).
Hand start_timeout a zero-arg closure and that number lands in the scope as
a NIL-keyed vararg; any closure created in that body then binds its FIRST
parameter against the leftover — first failing the typecheck ("arg 0 (nil)
type mismatch: expected number, got object"), and once that was relaxed,
binding the value under the NIL key so the real parameter stayed nil. It
cost a full debug cycle in host_launcher, where every host-service callback
created inside a boot timer silently never ran.
Both binding paths now walk the DECLARED (named) entries in order, so
captured varargs can never be mistaken for a parameter. Regression test in
tests/extra_arg_typecheck.rs reproduces the timer shape exactly.
(Pre-existing and unrelated: widget_tree's test_observe_and_find_single_node
and test_property_patch_no_structural_rebuild fail on upstream dev too.)
* script: stop parse_json silently dropping negative numbers
The tokenizer emits a leading `-` as its own Operator token, and none of
the three JSON value positions (object value, array element, root) had a
case for it. The sign was swallowed — and inside an object the KEY went
with it, because the minus consumed the value slot and the parser resynced
on the next token.
So `{"lat":37.7,"lon":-122.4}` parsed to `{"lat":37.7}`. No error, no
warning, just a missing field. That is how it was found: a mini-app asked
the host where it was, got coordinates with no longitude, and quietly fell
back to a default city. Sub-zero temperatures and negative UTC offsets
(New York is -14400) were being dropped the same way.
A pending-sign flag is applied to the next number in all three positions.
Bare scalar roots stay unsupported ("42" never parsed either) — separate
pre-existing gap, not touched here. Tests in
platform/script/tests/json_negative_numbers.rs.
* splash_storage: let the host raise a single isolate's jail quota
The jail's 16MB whole-app cap is a constant, so "this app may keep more
than the standard amount" had nowhere to live. A per-heap quota map beside
SANDBOX_ROOTS gives the host one, set through Splash::set_storage_quota
and cleared with the isolate like every other per-isolate binding. Script
still can't see or raise its own cap.
Lowering a quota never deletes anything — it just stops further growth —
so revoking the grant is safe on an app that already wrote past the
default.
host_launcher uses this for a `storage-large` permission (64MB), which is
the point: a capability the user can revoke and have it actually mean
something.
Drop the archived old/ tree, root AI notes, splashgame.md (Arcade lives
in the private sandbox), widgets-dll leftovers, and LAN/oracle helper
scripts. Move download_*.sh into tools/, and move sim/math out of
libs/game/ so the public repo no longer has a game/ directory.
Also quiet first-party compile noise and skip asset-ui tests that need
sandbox kits or uncommitted fixtures.
hotload_ui and xr pulled a dylib widgets wrapper that made
`cargo build --workspace` fail. XR now uses normal widgets.
The vendored weezl async test needed tokio, which we do not ship.
Drop the private sandbox clone from required workspace members
and Studio runnables. Finish the platform_ops VecDeque merge
(push_back / Option remove), land the mip-repeat texture API
the renderer already calls, and unbreak the Q3 importer plus
the godot example template that .gitignore had hidden.
Point the workspace at libs/asset and libs/render. Drop Arcade
frontend, gamemaker, rig, pack registry, physics, and the old
libs/game gameplay crates. Those live in the private sandbox repo.