The parallel batch machinery from 369de6929 only ever existed to chase a
default-on multi-threaded win that never came (washer w8 still +54% with
the hybrid on). At w1 — the only configuration this opt-in flag is for —
parallel_for runs inline, so the serial path gives the identical result
for ~340 fewer lines. Wire up the previously-dead
dynamic_tree_self_pairs/cross_pairs into a serial collect_batch_candidates
(three BVTT self/cross traversals -> canonical (a,b,child) sort -> serial
filter into move_results[0]) and delete BatchWork, BatchCtx, batch_drain_*,
BatchFilterCtx, batch_filter_*, bvtt_step, dynamic_tree_bvtt_drain/expand,
and the batch_frontier/worker_* scratch fields.
Determinism preserved exactly: OFF 0x61E35C31/step314 bit-identical, ON
0xBE99C5F7/step313 identical across workers 1/2/4. The debug SET-equality
oracle and the determinism_broad_phase_hybrid_across_worker_counts test
are unchanged and still pass; zero warnings.
PGO: pgo.sh never trained -bp=1, so an off-path-only profile laid the
hybrid branch out cold and collapsed the win to ~-5%. Add one -b=8 -bp=1
training run (neutral for the default path — counts merge, the OFF branch
stays hot) and retrain.
Corrected README numbers to measured values (hybrid-trained profile,
paired -bp toggle, washer w1): pair-finding stage 7.6k -> 3.6k ms/1000
(-52%); total ~-17% (~19.0k vs ~22.6k), which beats C (20661) and narrows
Rapier's lead from ~23% to ~12% — not the "17.7k / within 5% / -19%"
369de6929 claimed. Still default-off (w8 regresses ~+54%), opt-in
single-threaded accelerator for churn-heavy scenes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Broad-phase-hybrid subsection (why washer loses, the batch design, the
determinism proof, the single-thread win / multi-thread floor tradeoff,
and the default-off rationale). Washer row in the single-thread rapier
table annotated with the opt-in number (~17.7s, beats C, ~5% behind
rapier → box3d ahead-or-even on all nine single-threaded).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WorldDef.enable_broad_phase_hybrid (default false; -bp=0/1 bench toggle):
an adaptive batch broad phase for high-churn scenes. When
move_count*4 > proxy_count, replaces the per-moved-proxy tree queries
(8k proxies x 3 root-descents on washer) with three BVTT self/cross
traversals (dynamic self + dynamic x static + dynamic x kinematic) that
share the upper-tree descent, plus an O(n) bottom-up refit instead of
the median rebuild. Both traversal and candidate-filter are parallelized
across the task system (per-worker buffers → merge → canonical sort by
(shape_a,shape_b,child) → deterministic contact creation).
Correctness: a #[cfg(debug_assertions)] SET-equality assertion (batch
candidate set == per-mover set) runs in every test and never fires — the
proof the BVTT finds identical contacts (the hash can't prove it since
creation order legitimately re-baselines). New test
determinism_broad_phase_hybrid_across_worker_counts. OFF hash 0x61E35C31
bit-identical; ON hash 0xBE99C5F7 identical across workers 1/2/4 +
external tasks. 180/186/180/180 tests, zero warnings, profile retrained.
Single-threaded washer -18.7% (17715 vs 21780, broad phase -51%) — beats
C (20661), within ~6% of Rapier (16844). DEFAULT OFF because it regresses
multi-threaded (washer w8 +52%): the batch materializes ~40-50k
candidates/step and serially merges+sorts them (a fundamental floor the
inline per-mover path avoids by filtering in the query callback), so at
w8 the parallel per-mover queries win. Cannot be worker-gated (would
break cross-worker determinism). Correct, deterministic, zero-cost when
off — an opt-in single-threaded accelerator for churn-heavy scenes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the C Box3D column to the nine-scene rapier table, all from one
same-window interleaved single-thread run (2026-07-06). Percentages vs
the box3d Rust column. box3d Rust beats Rapier on 8/9 (junkyard flipped
to a +5% win post-tier-2; only washer lost, Rapier's incremental-BVH
broad phase). vs C: within ~7% geomean, ahead on both pyramid scenes,
worst is junkyard +18%. Replaces the derived † junkyard cell with a
direct measurement. Headline updated 7/9→8/9.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
query_tree_for_pairs was materializing every tree-query hit into a Vec
then re-iterating; C runs try_add_pair inline in the query callback
(b3PairQueryCallback). Rewrote to filter inline like C (both world
borrows are shared, so it compiles); only the rare compound inner-query
still uses a child_hits scratch (no compounds in washer/junkyard/
pyramids/trees, so pair-discovery order is unchanged). Removed the
now-dead PairScratch.hits field.
query_tree_for_pairs is washer's single hottest symbol (8k dynamic cubes
churned by a rotating drum re-query the whole tree every step). washer
broad phase -3% (paired plain + retrained-PGO), junkyard -1.4%,
pyramids/trees neutral. Hash bit-identical (0x61E35C31) — pure
structural, same discovery order. 179/185/179/179 tests, zero warnings.
Profile retrained dual-mode.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Instrumentation used to decompose the washer scene (which phase holds
rapier's ~24% advantage over box3d). Timer.rs drops the profiler-feature
gate so PhysicsPipeline's per-stage counters always measure (std Instant
instead of web_time); bench gains --stages (per-phase ms split) and
--probe (per-step contact/pair/sleep counts). Diagnostic tooling only;
no effect on simulation. Findings: washer's rapier advantage is entirely
broad phase (box3d ~8.5s vs rapier 1.7s), box3d's narrow phase is
actually faster than rapier's.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Derived cells marked with a dagger and the derivation stated: cold-window
baseline x the same-binary paired improvement (thermal-drift-immune);
direct cold-window rerun will replace them. vs C: junkyard +17%->+7% w1,
+22%->+12% w8, geomeans +6%/+8%. vs rapier: junkyard -8% -> ~parity,
geomean +34%, washer now rapier's only win.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New middle tier between full manifold recycling and the full SAT, behind
WorldDef.enable_feature_recycling (default true; OFF path bit-identical,
hash 0x61E35C31 verified):
- Case A, separated-witness early-out: a previously-non-touching contact
revalidates only the cached winning axis; still separating beyond the
speculative distance means done in one test. Sound structurally: any
cached axis is a valid separation witness (understates only, which
falls through to the full SAT). Carried junkyard: 24k skips/step,
full SATs 25.4k -> 4.5k per step.
- Case B, touching feature rebuild: re-clips the cached winning feature
under explicit staleness bounds (SATCache::sat_pose at last full SAT,
translation < 4x recycle distance, rotation < ~4.6 deg, forced refresh
every 8 steps); degenerate rebuilds and touching<->separated
transitions fall through same-step.
Probe-driven (junkyard: 89k full SATs/step on 105k pairs, 13k touching;
rapier maintains 3.4x fewer pairs): paired same-binary -fr=0/1 A/Bs show
junkyard -8% in every pairing (collide phase -15%), washer neutral to
-5%, pyramid/rain guards neutral, OFF costs nothing. Same-session
cross-engine junkyard: rapier's -8% lead closes to ~-3%.
pgo.sh now trains BOTH modes (single-mode training starved the remaining
full-SAT path); checked-in profile retrained dual-mode. SATCache pose
serialized in snapshots. feature_recycled_contact_count in Counters.
README: port-extension subsection, soundness argument, updated notes.
Also: rapier bench --probe mode from the workload-probe session.
179/185/179/179 tests green with the tier ON, zero warnings,
determinism suite passes across runs/workers/task systems.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- libs/rapier/crates/bench now mirrors every box3d benchmark scene
(trees100/50/25, junkyard, rain, washer added to large_pyramid/
many_pyramids/joint_grid), same -b indices as the box3d benchmark.
Geometry, densities, filters, spawn cadence and joint counts match;
body/collider/joint counts verified equal on all nine scenes.
- Fix an index-out-of-bounds panic in the vendored rapier simd-stable
constraint grouping (interaction_groups.rs): bodies in a different
island than the interaction (kinematic drivers, dynamics mid
island-merge) indexed the wrong island's conflict masks. Out-of-island
bodies are now exempt from conflict tracking, matching the solver's
existing boundary treatment (bounds-checked gathers, dropped scatters).
Original three scenes reproduce their previous timings after the fix.
- README: full nine-scene single-threaded matrix vs rapier (box3d wins
7/9, +33% geomean; rapier wins the hull-churn scenes junkyard/washer)
with comparability caveats for the extended scenes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Geomeans over the nine real scenes: +7% w=1 / +9% w=8. The scene stays
in the benchmark binaries (upstream suite parity); its fixed-overhead
story is kept in the known-remainder notes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
junkyard now shows its real post-fix +17%; footnote lattice replaced by
one measurement-conditions note. Geomeans +7% w=1 / +13% w=8.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pure full-update pipeline is +38% vs C (both engines, recycling forced
off), diluted to +17% by the at-parity recycle path. Gap is diffuse
(1.3-1.5x per pipeline function); all concentrated hypotheses measured
~zero, including a staging rewrite that halved build_face_a_contact's
instruction count with zero wall-clock effect (the bloat was cold code).
Note: sample attribution unreliable on PGO binaries (hot/cold splits).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Disassembly attribution on current binaries: junkyard's +23% sits
entirely in the NON-SAT narrow phase (2.06x C) — the edge SAT is now
FASTER than C. Cause: LLVM+PGO mega-inlining (third occurrence) —
update_contact compiled to 5.5x C's instruction count, collide_hulls had
no symbol at all. inline(never) on collide_hulls /
compute_convex_manifold / query_face_directions restores C's layout:
junkyard -3.6% paired (retrained profile), washer neutral, others
untouched.
hull_at coverage extended to build_face_a_contact / build_polygon /
clip_segment_to_hull_face / find_incident_face for contract completeness
— measured NEUTRAL beyond the boundary fix, and the feature's earlier
-3.6% is now captured by the safe attribute instead. README documents
the demotion honestly (the safe fix superseded the unsafe one).
Gates: 179/179/185/179 tests (default/unchecked-hulls/dp/nosimd), hash
0x61E35C31 everywhere, zero warnings. PGO profile retrained.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Matrix cells stay default-build (cold-window run); the hull scenes'
opt-in feature gains are annotated as paired deltas rather than absolute
cross-session numbers (thermal windows differ ~10%, mixing them would
misstate both). Algebraic-float-ops evaluation moved to a dedicated
'Evaluated ideas' section at the bottom.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
unchecked-hulls (off by default): elides bounds checks on hull-topology
indexing in the three SAT hot loops via a cfg'd accessor. Safety contract
= hull connectivity invariants validated at construction (hull.rs
is_valid_hull_impl + create_hull asserts), immutable behind Arc; debug
builds always assert, so every test run exercises the contract. Measured
(paired, retrained PGO): junkyard -3.6%, washer -2%, nothing elsewhere —
documented honestly that the checks were NOT most of the hull residue.
Tests 179 green with and without the feature; hash 0x61E35C31 both.
README: evaluation of the newly-stabilized algebraic float ops idea —
incompatible with the determinism contract as a default (compiler-
version/ISA-dependent results break cross-arch equality and cross-build
replay), modest expected upside since hot paths are already hand-
contracted; possible future opt-in, not planned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A sweep of every C b3ParallelFor/enqueue dispatch against the port found
the finalize-bodies pass (per-body transforms, AABB updates, sleep
accounting, continuous/TOI) and the bullet pass were left serial when
threading was ported — C runs both under b3ParallelFor. rain's w=8 gap
was almost entirely this serial fraction (Amdahl decomposition showed
its parallel portion already at C parity).
FinalizeCtx mirrors the collide pass's pattern: taken arrays + SyncSlice
disjoint per-body access, per-worker task contexts, deterministic merges
(bitset OR, split-candidate max like C), bullet list via atomic cursor
(C's b3AtomicFetchAddInt mirror). No new unsafe primitives. Pre-solve/
custom-filter callbacks force single-worker like collide.
w=8: rain +42% -> +8%, joint_grid +36% -> +11%, large_pyramid and
many_pyramids and trees25 at parity; geomean +11%. Serial geomean +5%
(Rust wins joint_grid/large_pyramid/many_pyramids outright). PGO profile
retrained; README tables + narrative updated. Remaining known
serial-vs-C difference: the split-island enqueue overlap (documented).
Hash 0x61E35C31 bit-identical (runs, workers 1/2/4, external tasks);
179/185/179 tests, zero warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Training set was missing washer and trees50/25 (washer -1.5% with
coverage, large_pyramid unchanged — no dilution). Profile refreshed.
Small-stage main-only fast path (generalizing C's single-block
shortcut): swept cutoffs 32/64/256 at w=8 — only large_world benefited
(-8% of ~11ms); rain regressed at every cutoff (its small-count stages
are mesh-contact stages with heavy per-item cost — serializing them
starves real parallelism) and joint_grid has few fat stages (grid
coloring = 2-4 colors), so its w=8 gap is NOT thin-stage sync. Reverted
per the measurable-win rule; negative result documented in the README.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Disassembly census showed the joint solvers at exact FMA parity with C
but +110 loads/+54 stores per joint: the full 56-byte BodyState get/set
round trip keeps untouched fields live across the ~1000-instruction
solve bodies. StateAccess::set_velocities (same unsafe contract as set,
velocities only, like C's in-place stores) + get_ref field extraction
across all 16 warm-start/solve functions in the 8 joint types.
joint_grid: 817 vs C 801 ms (was -11%). Full fresh matrix in README:
serial geomean +7% vs C with Rust WINNING large_pyramid (-6%) and
many_pyramids (-3%); w=8 geomean +28%. Checked-in PGO profile retrained
for the new code (stale profile cost ~13% on joint scenes).
Same change was measured neutral for contact scatter and correctly
dropped there (state live ~40 instrs vs ~1000) — both verdicts in the
README as a paired case study.
Hash 0x61E35C31 bit-identical everywhere; 179/185/179 tests, zero
warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cargo build --release on anything in the workspace now gets the PGO'd
box3d automatically (-Cprofile-use=libs/box3d/box3d.profdata; verified:
default build runs at the explicit-PGO binary's speed). The profile is
target-independent — x86_64 cross-build with the ARM-trained profile
compiles clean — and degrades gracefully when stale (unmatched functions
keep normal heuristics). Retrain with libs/box3d/pgo.sh.
Fresh four-way interleaved matrix in the README: default box3d is now
faster than or equal to rapier-simd on all three scenes (1118 vs 1451,
1510 vs 1690, 912 vs 914 ms) and faster than non-PGO C on two of three
(C keeps joint_grid 816 vs 912). 179/185/179 tests green with the
config active.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pgo.sh: instrument -> train on the benchmark scenes -> merge -> rebuild.
Paired same-machine runs: large_pyramid 1177 vs 1457 ms (-19%, now 15%
faster than the non-PGO C build), junkyard -14%, many_pyramids -11%.
Determinism hash unchanged under the PGO binary (0x61E35C31 across
runs/workers/task systems) — PGO changes layout/inlining, never
arithmetic. README notes the C-reference fairness caveat.
Also documented as tried-and-dropped (noise-floor in paired A/B, per
the keep-only-measurable-wins rule): cache-line padding of stage-sync
atomics, narrow velocity-only scatter writes, compound child Arc clone
(already eliminated by the earlier scratch fix).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Disassembly-driven (fork agents confirmed the stalls, killed the
bounds-check and recycle-rate hypotheses with instruction-level and
runtime-counter evidence — recycle counts are bit-identical to C):
- Manifolds inline-when-single store: Contact.manifolds Vec<Manifold> ->
enum { None, One(Manifold), Many(Vec) } with deref-as-slice. Convex
contacts keep their manifold inline (the Rust equivalent of C's block-
allocator arena locality — the per-contact heap chase was the main
stall in collide/prepare/store). Contact is #[repr(C)] with manifolds
last so hot header fields stay on the leading cache lines. Public
ContactData API unchanged via Deref; contact_solver.rs needed zero
changes. Pure storage change: determinism hash identical (0x61E35C31).
- #[inline(never)] on update_contact + the four convex stage functions:
C compiles these standalone; LLVM had inlined all of them into one
13.6 KB execute_block paying constant register-spill traffic.
Definitive cold-machine matrix: serial geomean 1.15x -> 1.12x vs C
(many_pyramids 2071 vs 1949 ms, large_pyramid 1501 vs 1392); 8-worker
geomean 1.35x -> 1.30x. vs rapier-simd (adjacent runs): box3d ahead 16%
on large_pyramid and 3% on joint_grid, behind 5% on many_pyramids (was
21%). README grids updated.
179/185/179 tests green, zero warnings, hash unchanged across workers/
arch/task systems.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-vendors wide 0.7 + safe_arch, restores the upstream simd-stable
wiring in rapier3d/parry3d manifests and the cfg-simd source, and drops
the added 'stripped build does not support SIMD' guards (upstream's
simd-vs-enhanced-determinism exclusivity guard kept).
Interleaved single-thread retest (min of 4): SIMD buys rapier 1.8-2.2x;
box3d vs rapier-simd is now near parity — large_pyramid 1579 vs 1638 ms,
joint_grid 957 vs 1008 ms (box3d ahead), many_pyramids 2389 vs 1970 ms
(rapier ahead). box3d README grid updated with the honest three-column
table; box3d keeps cross-arch determinism + zero deps at that speed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same scenes/geometry/materials/dt, matched solver budget (4 substeps vs
4 solver iterations), interleaved min-of-4 runs: large_pyramid 2.23x,
many_pyramids 1.82x, joint_grid 1.88x (geomean ~1.97x). Table + fairness
notes at the top of the box3d README (vendored rapier has no SIMD;
enhanced-determinism measured free on these scenes).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- joint prepare: read BodySim through references (was deref-copying
220 bytes twice per joint per step; prepare_joint now at C parity)
- FMA contraction extended to joint solvers (32 sites; hash re-baselined
to 0x61E35C31, still bit-identical across workers/arch/task systems)
- scheduler: workers spin ~tens of us before committing to a kernel
sleep (semaphore try_acquire spin phase; A/B: large_world w=8
24 -> 11.5 ms, joint_grid w=8 1.58x -> 1.49x, other scenes neutral;
intentional deviation from C documented in README)
- tried and reverted: chunks_exact twin-pair edge SAT (won 5% on
junkyard compounds, cost box-box scenes 4-8%; keeps C 1:1 loop shape)
- README: fresh benchmark matrix, second-round notes, stale external
task-hook claim fixed
179/185/179 tests green, zero warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Profiling-driven (sample + disasm comparison vs clang -O3). All changes
safe Rust except one debug_assert-guarded extension of the existing
SyncSlice unsafe contract. Determinism preserved: hash bit-identical
across workers 1/2/4, NEON/SSE2/scalar, internal/external task systems
(new baseline 0x9018E2D8 after approved FMA contraction).
- f32/f64::mul_add contraction in hot scalar math (= C's -ffp-contract=on;
89 sites; wide SIMD ops untouched like C intrinsics). large_pyramid
now at parity with C (1387 vs 1373 ms serial)
- FloatW::get/set: direct lane load/store instead of vector-through-stack
round trip; layout asserted at compile time
- gather_bodies by reference (removes 20-register spill storm)
- per-worker capacity-preserving scratch for convex + mesh collide paths
(C-arena equivalent; mesh path allocated per triangle and serialized
the parallel collide pass on allocator locks)
- update_contact: borrow shapes instead of cloning (deep compound
geometry clones + cross-worker Arc traffic; junkyard w=8 -39%)
- scheduler semaphore: two-level atomic fast path (C uses
dispatch_semaphore_t; old Mutex+Condvar locked every enqueue)
- SyncSlice::get_ref/get_mut unchecked indexing under the existing
unsafe contract, debug_assert-guarded (-6% serial)
- README/PORTING: new numbers, FMA sync conventions, known remainders
179/185/179 tests green (default/double-precision/disable-simd), zero
warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full op-stream player in recording_replay.rs (b3RecPlayer port):
opcode dispatch for ~150 ops, StateHash verification at every step
marker, query replay with bitwise comparison, keyframe ring with
budget-driven interval doubling, seek/restart/scrub, validate_replay.
tests/test_recording.rs ports test_recording.c (17 tests incl.
record-at-4-workers/replay-at-1-and-4 hash equality). 179/185/179
tests green across default/double-precision/disable-simd, zero
warnings, determinism hash unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All ~140 opcodes from recording_ops.inl with exact C values, capture hooks
in every mutator and query (~137 sites across body/shape/joint/world),
48-byte header with registry locator backpatch, snapshot seed, query tag
interning, state-hash anchors per step. Recording is observer-only
(bit-identical world state with and without a recording attached).
Replay/player side lands separately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- WorldDef enqueue_task/finish_task/user_task_context (C contract incl.
null-return-means-inline); TaskSystem dispatch (Serial/Internal/External)
replaces the bare scheduler; determinism hash bit-identical through an
external thread-per-task system.
- examples/box3d: makepad app rendering the live simulation (offscreen 3D
pass with depth, orbit/zoom camera, instanced lit boxes/spheres, 204-box
pyramid + spheres, 4-worker solver, Space to reset).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port of the C threading design: worker threads with a fixed task ring and
help-while-waiting finish (scheduler.rs), atomic block-claiming parallel_for,
and the solver's stage machinery (per-block syncIndex CAS, sync-bits stage
advancement, mainClaimed race). Parallel narrow phase, broad-phase pairs,
sensors, finalize. Shared access goes through documented disjointness
primitives (sync.rs: SyncPtr/SyncSlice/AtomicIndex); worker_count 1 keeps the
serial path bit-identically. Results are bit-identical at any worker count
(determinism hash 0x7A796F4F asserted at 1/2/4 workers). 8 workers: 3.4-5.7x
over serial on heavy scenes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Persist solver constraint arrays/spans/stage blocks and broad-phase pair
query buffers across steps instead of reallocating each world_step.
Bit-identical results (determinism hash unchanged); washer -7.6%, small
wins on trees/rain, pairs stage -6% on junkyard. A contact-manifold
reuse attempt regressed pyramid scenes and was dropped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- contact solver wide ops + V32 now have real SSE2 and NEON paths selected
by target arch; scalar fallback behind the disable-simd feature. All three
paths are bit-identical (cross-arch determinism verified: same ragdoll
hash on NEON, SSE2 under Rosetta, and scalar).
- double-precision feature (C BOX3D_DOUBLE_PRECISION): f64 world positions
with the exact C boundary-function semantics; enables the far-from-origin
test halves (157 tests in DP mode, 151 default).
- world snapshots: recording substrate subset (buffer/writers/geometry
registry/readers) + world_snapshot.c port; bit-identical continuation
after restore, corrupt-image rejection.
- examples/benchmark.rs: all 10 C benchmark scenarios; serial Rust runs
1.05-1.55x slower than C -O2 at one worker (geomean ~1.3x with fat LTO).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full engine port in libs/box3d: math, geometry, GJK/TOI, hull builder,
dynamic tree, manifolds, constraint graph, solver (serial, scalar SIMD
path), all 8 joint types, sensors, mover, world API. 147 ported C unit
tests green in debug and release. See libs/box3d/README.md for the
upstream revision and sync notes, PORTING.md for conventions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Support standard keyboard navg shortcuts/keys in TextInput
Implement platform-standard TextInput navigation and deletion behavior,
including Home, End, PageUp, PageDown, word movement, line/document
boundaries, and Shift-based selection.
* Use Apple Option/Cmd conventions on Apple targets
* Use Ctrl conventions on non-Apple targets
* Web accepts both shortcut styles for now, since we don't have a way
to query the host OS from within a makepad web env.
Also, be extremely careful to ensure that we respect Unicode grapheme boundaries
when doing all the selection/navigation logic.
Fix `Delete`, which was erroneously handled before.
Add lots of missing keys in Linux X11 & Wayland backends, e.g.,
Home, End, Delete, Insert, PageUp/PageDown, and arrow keys
* Add `CropToFill` image fit variant, improve ImageFit docs
This allows you to easily achieve the "centered cropped fit" that most apps
want for things like avatars or small thubmnails that get masked.
* Detect and support hardware keyboards, distinguish from soft/virtual kbd
Mimic desktop behavior on mobile systems as much as possible.
This is esp important for tablets like iPad OS where you're more likely
to have a real physical keyboard attached.
For iOS:
* Arrow keys and Home/End/PageUp/PageDown navigate and auto-repeat
at the system-defined rate (connected via `UIKeyCommand`)
* Cmd+Enter to submit a `TextInput` and Cmd+C/X/V clipboard shortcuts now work.
* Ensure the pop-up diacritic/accent menu is properly placed using a hidden
`UITextInput` native widget, which acts as a sort of "proxy"
* Proactively drain `ShowTextIME` after each draw so the IME position will be
properly updated after each keystroke.
* Importnatly, don't mark the IME dismissed when a hardware keyboard is attached.
For both iOS & Android:
* Add a `has_physical_keyboard()` detection mechanism across both backends,
and fix platform-specific key repeat behavior
For Android:
* Ensure clipboard cut/copy works using the same Ctrl shortcuts (API 26+)
Soft/virtual keyboard/IME changes:
* For multiline TExtInputs, a soft keyboard Enter/Return key will always just
insert a new line, to avoid complexity with keyboard shortcut cfgs.
* CJK keyboard character selection should also be properly positioned now
* minor optimization to avoid re-setting IME pos if it didn't change
* Ensure inline composition is still shown in TextInput on all platforms
This is mostly relevant when using CJK and other similar IMEs.
Previously Makepad didn't shown any "echoes" of the single latin chars
that the user would type, but it would correctly input the selected CJK
glyph. So if you typed `nihao` and then selected `你哈`, then you would
see the proper chinese characters but not the latin "nihao".
Tha't s a bit confusing while typing.
Summary of the fixes per platform:
macOS:
- `set_marked_text` now forwards the marked (composition) text to the
focused TextInput with `replace_last = true`; previously it only stored
it in an ivar. `unmark_text` clears the preview, and both it and
`insert_text` share a new `clear_marked_text_ivar` helper so a commit
doesn't emit a second, destructive text-input event.
Windows:
- Add `WM_IME_COMPOSITION` handling: commit `GCS_RESULTSTR`
(`replace_last = false`) and show `GCS_COMPSTR` inline
(`replace_last = true`), and clear on `WM_IME_ENDCOMPOSITION`. The
message is consumed so DefWindowProc neither draws its own composition
window nor synthesizes a duplicate WM_CHAR for the result.
- Extend the vendored `windows` binding with `ImmGetCompositionStringW`,
`GCS_COMPSTR`/`GCS_RESULTSTR`, and `WM_IME_COMPOSITION`/
`WM_IME_ENDCOMPOSITION`, which it didn't previously generate.
Linux (Wayland):
- Handle the `zwp_text_input_v3` `PreeditString` event (previously empty)
and apply the double-buffered preedit/commit state on `Done`, in the
protocol-mandated order (commit, then preedit), clearing the preview
when a cycle carries no preedit.
Linux (X11):
- Create the input context with XIM on-the-spot (`XIMPreeditCallbacks`)
and forward the preedit string from the draw/start/done callbacks,
falling back to `XIMPreeditNothing` if the IM server doesn't support
callbacks. Callbacks run inside `XFilterEvent`, so they only mutate a
thread-local that the event loop drains into the widget afterward,
avoiding re-entrant access to the app.
Android:
- Don't mark the IME dismissed when a physical keyboard is attached
(mirrors the iOS guard). Android was unconditionally calling
`text_ime_was_dismissed()` on soft-keyboard hide, which tore down the
IME connection that hardware-key composition relies on.
* iOS: replace custom `UITextInput` with a native `UITextView`
`UITextView` is a full system-native keyboard client, so we get all the
major features for free: language HUD pill and the complete globe/Ctrl+Space
shortcut to cycle between IMEs/languages.
Makepad basically just mirrors the state of the system native text view,
via the `full_state_sync`, but the actual native text view is kept invisible
so it doesn't interfere with what we render in Makepad's TextInput.
Notably, the Full Keyboard Accessibility setting now does work properly,
whereas it did not before with our UITextInput-based approach.
We also make sure that arrow keys, nav keys, auto-repeat, and modifiers
are properly hanlded so we can retain the expected kbd shortcuts,
like other desktop platforms.
* iOS: remove the old `UITextInput` connection with the Makepad TextInput
We've now switched to the native UITextview, so we don't need this any more.
* iOS: fix desync during fast typing
Ensure there's no race between the native UITextView and
Makepad's TextInput, as the Enter/REturn key needs special handling
w.r.t. how `pressesBegan` gets it (From a real hardware kbd).
* TextInput: more iOS integration, and text input types
more native integration for things like username/password,
new password fields, email, address, URLs, etc.
These tell iOS to change the keyboard layout/type for the text input.
* iOS: don't let Full Keyboard Access focus on our hidden native cursor
* cleanup
* iOS/TextInput: fix perf issues
* iOS TextInput: more fixes for read-only efficiency, and filtered input
Also port some of these fixes to Android's IME integration layer
* iOS/TextInput: hide the native caret iOS draws during autocorrect
but still allow the "decline autocorrect" bubble to popup where that
hidden caret is located (and the CJK candidate window in the same spot)
* Avoid script VM re-entrant panic: defer animator_cut/play if script VM is held
`animator_cut` / `animator_play` call `cx.with_vm`, which panics
(*"Script VM swapped off"*) when invoked during an apply walk — e.g. a
widget's `on_after_apply` on `ScriptReapply` / `Reload` — because the VM
is already taken for the duration of that walk's enclosing `cx.with_vm`.
- The derive macro's `animator_cut_scoped` / `animator_play_scoped` now
check `cx.is_script_vm_held()`; when held, they queue the op
(`defer_cut` / `defer_play`) and return instead of re-entering the VM.
- `animator_handle_event_scoped` replays the queue via `flush_deferred`
on the next frame, once the VM is free.
The defer path runs **only** in the formerly-panicking case, so VM-free
animations are byte-for-byte unchanged.
Also adds a re-entrancy-naming panic (`VmHolderGuard`) plus
`Cx::try_with_vm` / `Cx::is_script_vm_held` for diagnosing and handling
this class of bug.
* Better spacing/positioning for IME popups like the CJK candidate menu
applied to all platforms, but primarily an issue on macOS/iOS.
The candidate/conversion window (e.g. CJK pinyin) was covering the line of
text being composed. Carry the caret-line rect (not just a point) through
ShowTextIME and feed each backend its native "keep clear of this line" API,
so the OS places the candidate directly above/below the line with a small gap:
- macOS: firstRectForCharacterRange returns the line rect via AppKit
convertRect:toView:nil + convertRectToScreen (drops the hand-rolled
screen-coord math + fudge offsets); invalidate on caret move.
- Windows: ImmSetCandidateWindow with a CFS_EXCLUDE line rect.
- Wayland: set_cursor_rectangle with the real line rect.
- X11: XNSpotLocation/XNArea at the line.
- iOS: return the true composing-line box from firstRectForRange so iOS flips
around the real edges (consistent at any screen position) instead of a
degenerate point; only while marked text is active, to avoid an oversized
autocorrect highlight when typing normally.
* Fix Linux X11 behavior: Ctrl-based kbd shortcuts didn't work in TextInput
also trying to fix X11 behavior for positioning the CJK candidate window,
turns out there was an X11 bug for Ubuntu 22 and older so it's not always
possible, but we can attempt a workaround if errors occur (based on that,
we try to auto-detect the version of X11)
* fix X11 event loop latency by draining only a max of 64 events before redrawing
still working on X11 CJK candidate window positioning...
* add logs to X11 ime to figure out wtf is going on
* more robust fallbacks for X11 CJK candidate window positioning... grr
* maybe try to set the XFontSet attribute? for CJK candidate positioning
* positioning works now but there is a bit of overlap still
* now that X11 CJK candidate positioning works in some cases,
we need to pass the full rectangle containing the current line of text
to the X11 library so that it can position the window both on top
and beneath the current line of text, if needed.
* tweaking X11 CJK candidate positioning
* abandon the screen-positioning heuristic
Instead, we just send the bounding rect of the current text character
and hopefully let the X11 platform libs decide where to put the
CJK candidate popup
* add more spacing to the bounding rect on X11
* tweak for a bit more space between CJK candidate window
* more tweaks, rect height isn't being respected for some reason...
* attempting to add more instrumentation to figure out wtf is going on with X11 CJK positioning
* remove bad instrumentation that was causing freezes. ugh
* different approach for IME placement on X11
* previous positioning attempts for X11 didn't work.
New strategy: let it be positioned, and then try to move it
* still trying to fix X11 CJK candidate window positoining...
* trying to find CJK candidate window with X11 queries (To move it)
* abandon window scanning approach
* better approach, now just tweaking it
* fix one case where the candidate window was flipped but it pointing too low
* tweaking more
* trying to fix above-text line positioning
* still trying to tweak CJK candidates ABOVE the text line
* be more conservative when guessing whether X11 will show the CJK candidate above or below
* improve size heuristic for CJK candidate height
* calling X11 as complete now. jfc. Cleanup, remove debug logs, etc
* iOS: replace custom `UITextInput` with a native `UITextView`
`UITextView` is a full system-native keyboard client, so we get all the
major features for free: language HUD pill and the complete globe/Ctrl+Space
shortcut to cycle between IMEs/languages.
Makepad basically just mirrors the state of the system native text view,
via the `full_state_sync`, but the actual native text view is kept invisible
so it doesn't interfere with what we render in Makepad's TextInput.
Notably, the Full Keyboard Accessibility setting now does work properly,
whereas it did not before with our UITextInput-based approach.
We also make sure that arrow keys, nav keys, auto-repeat, and modifiers
are properly hanlded so we can retain the expected kbd shortcuts,
like other desktop platforms.
* iOS: remove the old `UITextInput` connection with the Makepad TextInput
We've now switched to the native UITextview, so we don't need this any more.
* iOS: fix desync during fast typing
Ensure there's no race between the native UITextView and
Makepad's TextInput, as the Enter/REturn key needs special handling
w.r.t. how `pressesBegan` gets it (From a real hardware kbd).
* TextInput: more iOS integration, and text input types
more native integration for things like username/password,
new password fields, email, address, URLs, etc.
These tell iOS to change the keyboard layout/type for the text input.
* iOS: don't let Full Keyboard Access focus on our hidden native cursor
* cleanup
* iOS/TextInput: fix perf issues
* iOS TextInput: more fixes for read-only efficiency, and filtered input
Also port some of these fixes to Android's IME integration layer
* iOS/TextInput: hide the native caret iOS draws during autocorrect
but still allow the "decline autocorrect" bubble to popup where that
hidden caret is located (and the CJK candidate window in the same spot)
* Image support: add bmp/qoi,ico, webp, SVG in `Image` widget, 16-bit png
Generally, this commit makes improvements to image decoding and rendering.
Added a bunch of functions for image discovery / metadata gathering:
`decode_image_from_data()`, `image_size_by_data()`, `looks_like_svg()`
Added more `Image[Ref]` functions for other image formats:
`ImageRef::load_{bmp,qoi,ico,gif,webp,svg}_from_data()`, plus a nice
convenience fn for auto-detec+load: `load_image_from_data()`.
Added cheap, lazily-init'd support for SVGs within the `Image` widget.
Fixed some issues with aspect ratio being clobbered during image rotation.
* Audit and harden image decoding stuff against huge inputs (DoS)
Bound the size of the decoded image, pixel count, frame counts (for animated),
range of SVG sniffing, and encoded file size.
Only once we run those checks do we actually alloc a buffer for the decoded image. before allocating decode buffers. Validate
Add various other checks within the vendored image decoding libraries too.
Generally, this commit makes improvements to image decoding and rendering.
Added a bunch of functions for image discovery / metadata gathering:
`decode_image_from_data()`, `image_size_by_data()`, `looks_like_svg()`
Added more `Image[Ref]` functions for other image formats:
`ImageRef::load_{bmp,qoi,ico,gif,webp,svg}_from_data()`, plus a nice
convenience fn for auto-detec+load: `load_image_from_data()`.
Added cheap, lazily-init'd support for SVGs within the `Image` widget.
Fixed some issues with aspect ratio being clobbered during image rotation.
* Support standard keyboard navg shortcuts/keys in TextInput
Implement platform-standard TextInput navigation and deletion behavior,
including Home, End, PageUp, PageDown, word movement, line/document
boundaries, and Shift-based selection.
* Use Apple Option/Cmd conventions on Apple targets
* Use Ctrl conventions on non-Apple targets
* Web accepts both shortcut styles for now, since we don't have a way
to query the host OS from within a makepad web env.
Also, be extremely careful to ensure that we respect Unicode grapheme boundaries
when doing all the selection/navigation logic.
Fix `Delete`, which was erroneously handled before.
Add lots of missing keys in Linux X11 & Wayland backends, e.g.,
Home, End, Delete, Insert, PageUp/PageDown, and arrow keys
* Add `CropToFill` image fit variant, improve ImageFit docs
This allows you to easily achieve the "centered cropped fit" that most apps
want for things like avatars or small thubmnails that get masked.
* Detect and support hardware keyboards, distinguish from soft/virtual kbd
Mimic desktop behavior on mobile systems as much as possible.
This is esp important for tablets like iPad OS where you're more likely
to have a real physical keyboard attached.
For iOS:
* Arrow keys and Home/End/PageUp/PageDown navigate and auto-repeat
at the system-defined rate (connected via `UIKeyCommand`)
* Cmd+Enter to submit a `TextInput` and Cmd+C/X/V clipboard shortcuts now work.
* Ensure the pop-up diacritic/accent menu is properly placed using a hidden
`UITextInput` native widget, which acts as a sort of "proxy"
* Proactively drain `ShowTextIME` after each draw so the IME position will be
properly updated after each keystroke.
* Importnatly, don't mark the IME dismissed when a hardware keyboard is attached.
For both iOS & Android:
* Add a `has_physical_keyboard()` detection mechanism across both backends,
and fix platform-specific key repeat behavior
For Android:
* Ensure clipboard cut/copy works using the same Ctrl shortcuts (API 26+)
Soft/virtual keyboard/IME changes:
* For multiline TExtInputs, a soft keyboard Enter/Return key will always just
insert a new line, to avoid complexity with keyboard shortcut cfgs.
* CJK keyboard character selection should also be properly positioned now
* minor optimization to avoid re-setting IME pos if it didn't change
* Dock: avoid ID collisions in drag/drop; never delete dock root in unsplit_tabs
* Clean up and further harden dock logic around splitting/dragging
* cargo_makepad: Android App Bundle builds, API 26 support, stable toolchain
Overhaul the Android build pipeline. Three related build-tooling
changes that share compile.rs/sdk.rs and so are committed together.
Android App Bundle (.aab) support — required for Google Play uploads:
- New `build-aab` command: compile resources with aapt2, link a
proto-format APK, assemble the base module, run bundletool, and sign
with jarsigner.
- New `keystore-create` command wrapping keytool, with a reusable
keystore sidecar file; new `--keystore*`, `--no-sign`,
`--version-code`, `--version-name` flags.
- Version codes may be explicit or auto-generated as a monotonic
YYYYMMDDHH UTC integer.
- Read app id, version, and signing metadata from
`[package.metadata.packager]` / `[package.metadata.makepad.android]`
in Cargo.toml; support a custom AndroidManifest.xml template.
- Upgrade the bundled TOML parser for the dotted keys, inline tables,
and multi-line strings those metadata sections use.
- Download bundletool and copy jarsigner/keytool/aapt2 into the SDK.
minSdkVersion 26:
- Lower the default Android minimum SDK from 33 to 26 and track the
target SDK (35) separately, emitting minSdkVersion and
targetSdkVersion independently in the generated manifest; add a
`--min-sdk-version` override.
Stable Rust toolchain:
- Build Android and iOS on stable instead of nightly. tvOS still needs
nightly for `-Z build-std`, so the channel is resolved per target.
- Add `ensure_rust_toolchain_installed` (install only when missing).
* Android: load newer NDK symbols at runtime to support API 26
With the minimum SDK lowered to 26, NDK entry points that only exist
on newer API levels can no longer be declared with `extern "C"` —
doing so breaks `dlopen`/startup on API 26-28. Resolve them at
runtime instead:
- amidi_sys: lazily `dlopen` libamidi.so (API 29+) into a cached
vtable; the wrappers degrade to error/zero returns when the library
is absent on older devices.
- android_jni: `dlsym` the AChoreographer vsync callbacks, gated on
the running API level.
- ndk_sys: drop the `extern "C"` declarations for
`ANativeWindow_setFrameRate` and the Choreographer callbacks;
android.rs drops the now-unused frame-rate call.
- MakepadActivity: guard `setInitialSurroundingSubText` (API 30+) and
`layoutInDisplayCutoutMode` (API 28+) behind version checks.
- android_jni: the fallback render-loop thread now exits cleanly when
the app is torn down.
* Android: automatic and app-controlled system bar appearance
Add a way to control the tint of the status and navigation bar icons,
fixing white-on-white (invisible) icons when an app draws a light
background under a system dark-mode theme.
- New `Cx::set_system_bar_appearance(SystemBarAppearance)`. The default
`Auto` mode picks dark or light icons from the window background
luminance; `DarkIcons`/`LightIcons` force the choice.
- The `Window` widget resolves the setting each event cycle — for
`Auto`, the Rec.709 luma of `pass.clear_color` — and emits
`CxOsOp::SetSystemBarDarkIcons` only when the resolved value changes.
- On Android this drives `WindowInsetsController.setSystemBarsAppearance`
(API 30+) or the `SYSTEM_UI_FLAG_LIGHT_*` flags (API 26-29). The tint
is re-asserted after fullscreen toggles, since the legacy path
rewrites the whole `systemUiVisibility` bitmask.
* Android: fix soft-keyboard handling and edge-to-edge insets
Several related window-inset and IME fixes, mostly affecting devices
that are not edge-to-edge (Android versions before 15).
- Report safe-area and IME insets as the overlap with the render
surface, not the raw window-edge insets. On a non-edge-to-edge
window the surface already sits inside the system bars, so the raw
insets double-counted — leaving oversized gaps around content and
above the keyboard.
- Also drive safe-area insets from `onGlobalLayout`, so the app is
inset correctly from launch instead of drawing under the status bar
until the first keyboard show or rotation.
- While the keyboard animates, treat the `WindowInsetsAnimation`
callback as the authoritative per-frame inset source and have the
layout-driven callbacks defer to it. Read target IME visibility from
`getRootWindowInsets()` so a show animation is not misread as an
instant dismissal.
- Only reconfigure the Java IME when the `TextInputConfig` actually
changes, instead of on every show.
- `KeyboardView`: compute and apply the content shift at keyboard-show
event time, removing a one-frame lag and a tail-end jump; only
reconcile post-draw when the focused field actually redrew.
- `Modal::close()`: skip the focus revert when the modal is already
closed — it was stealing focus from a just-tapped text input and
causing a first-tap keyboard flicker.
- Hide the keyboard via `WindowInsetsController.hide(ime())` on API 30+.
* platform: don't panic posting actions during shutdown
post_action no longer unwraps the global action sender. It now
silently drops the action if the sender mutex is poisoned, no Cx
sender is installed, or the receiver has been dropped during app
teardown, and only raises the UI signal when the send succeeds.
(Also shortens an over-long field doc comment in cx.rs; no behavior
change.)
* cargo-makepad: link std statically in AAB builds (16 KB page-size fix)
`-C prefer-dynamic` ships std as a separate, 4 KB-aligned libstd.so that
fails Play's 16 KB page-size rule. AAB builds now link std statically;
APK/dev builds keep prefer-dynamic. Also documents {min_sdk_version} in help.