Skip to content

feat: ocaml native backend - #235

Merged
tiensonqin merged 267 commits into
mainfrom
feat/ml-backend
Oct 15, 2026
Merged

tiensonqin merged 267 commits into
mainfrom
feat/ml-backend

Conversation

@tiensonqin

Copy link
Copy Markdown
Collaborator

No description provided.

Derives an accessibility semantic model from the retained Lui_store
node mirror for the native backend: role mapping for every standard
kind (plus ARIA-style role-prop overrides and extension nodes),
accessible name/description precedence with descendant-text
accumulation for composite widgets, tri-state checked and the full
state-flag set, value payloads, hidden-subtree exclusion, incremental
updates from drained dirty ids that report exactly the changed ids
for bridge notifications, and focus tracking.
New platform/native/lui_layout module: rebuilds a Lui_flex mirror from
a Lui_store on each sync, translates wire kinds/props into flex styles
(directions, overflow, popup-out-of-flow, shared-cell overlays, grid
cells, edge-inset ordering, container-relative frames), attaches an
injectable text measure to text leaves, and exposes run/layout_hook/rect
so the host can feed placements to Lui_paint in scene (device-px)
coordinates. 9 alcotest cases cover the required matrix.
Evaluates a Lui_scene.t into a premultiplied BGRA buffer, covering fills
(linear/oklab/stripes paints, borders incl. dashed), inset and cast
shadows, glyphs via the scene atlas with text coverage correction,
image ops (grayscale/opacity/src-rect), rounded clips, effects over a
blurred backdrop, and holes, with the continuous-corner SDF for
negative radii.

Damage tracks per-op bounds across scenes and merges dirty rects, so
the renderer repaints only what changed; backdrop areas of damaged
effects are forced back into the dirty set. Scanline bands split
across Domain.spawn workers stay bit-identical to one worker.

backdrop_sample's shared clamp can take texel rows to w-1, so bd_read
pads the buffer and repeats the last valid row to keep edge sampling
deterministic.
Real window backend for the native pipeline: SDL2 window (GL 3.3 core,
double-buffered, resizable, high-DPI) running an event loop that maps
SDL events to Lui_app.dispatch_event on a hit-tested node, repaints via
Lui_host + Lui_gl, and presents through SDL_GL_SwapWindow.

- lui_window.ml{i}: pure backend logic — input/event tables, rect hit
  testing (topmost-first, invisible/zero-area skipped), admission
  gating mirrored from Lui_runtime.dispatch, a deterministic flex-ish
  layout stub (padding/gap/width/height/grow/measure hooks, device-px
  output) until lui_flex lands, focus+caret text editing on a shadow
  buffer (store text lags a frame behind dispatched edits), and an
  FNV-1a scene checksum for headless verification.
- lui_window_text.ml: Lui_text shaping -> per-glyph subpixel-quantized
  rasterization -> scene atlas upload -> glyph op emission, plus the
  text measure hook for layout.
- demo_app.ml: modest demo Lui_app — heading, text-field with
  autofocus, add button, keyed list rows with remove buttons.
- main.ml: SDL driver. --headless N forces the dummy video driver and
  runs N forced repaints with a no-op renderer when no GL context is
  available (dummy refuses OpenGL windows), printing per-frame op
  counts and a final checksum. Frame stats (fps/frame-ms min/avg/max)
  print every 60 painted frames and at exit.
- test/: 16 alcotest cases over the pure parts — arg parsing, SDL
  constant tables, admission gating (incl. role=treeitem gates),
  layout math, hit testing, click/focus/text/key mapping, disabled
  swallow semantics, resize scale, frame pacing, checksum determinism.
Lui_host gains an optional layout engine slot: ~layout:(module
LAYOUT_ENGINE) builds an engine the host re-syncs inside every repaint
before paint runs, and hooks.layout swaps to the engine's placement
hook while it is still the zero-rect default (an explicit hooks.layout
always wins). Lui_host.layout_rect exposes the synced rects for
hit-testing. Lui_layout satisfies the signature, so production callers
pass ~layout:(module Lui_layout) plus a real ?measure.

The e2e harness drops the deterministic 304x20-row stub for that host
path, keeping a stub measure (8px/char, 16px tall) so text sizes stay
deterministic and cross-platform. rect_of now reads Lui_host.layout_rect.

e2e_test gains the first numeric real-layout assertion: a padded/gapped
column holding a padded/gapped row with exact child offsets. Existing
rect-relative assertions were already layout-agnostic via rect_of and
keep passing.
Mirror the Lui_a11y semantic tree as NSAccessibilityElement objects so
assistive technology sees the app: roles/subroles, titles and labels,
values (numeric, text, tri-state toggles), expanded/selected/focused
states, row sets on list/table/outline containers, and the action
surface VoiceOver drives (press, increment/decrement, focus, set-value,
scroll-to-visible, disclose).

The ObjC side keeps a flat node-id -> element table and posts
AXDestroyed/Value/Title/Focused/SelectedRows/Expanded plus one
AXLayoutChanged per sync; actions are enqueued and polled by the OCaml
side rather than called back into the runtime. Element frames come from
a host-supplied view-space rect callback, converted through the window
to screen coordinates; the header documents the container methods a
host NSView delegates (children, hit test, focused element).

Tests: alcotests for the role map, notification selection, action
masks and pure helpers; a manual executable that spins up an offscreen
NSWindow + host view, attaches an 11-node tree, dumps the reported AX
attributes (also after a focus/value sync) and performs legacy actions.
Pure-OCaml composition state machine in platform/native/lui_ime:
SDL text_editing/text_input/focus events feed a state of
{active, marked_text, cursor, sel_len, caret_rect}; feed_event returns
actions (Begin_composition/Update_composition/Commit_text/Cancel/
Move_candidate) for the driver to wire — never dispatches TextChanged
for marked text; focus loss cancels; caret rect updates candidate
position only while composing; key-driven edits gated while marked
text is non-empty.

lui_window: Input gains Text_editing/Caret, action gains Ime_rect;
Ui feeds SDL events through lui_ime and translates Commit_text into
the existing Text_input splice + Dispatch(TextChanged) path.
main.ml maps Sdl.Event.text_editing, starts SDL text input on window
creation/autofocus, calls Sdl.set_text_input_rect on Focus_changed and
caret moves, and renders marked text underlined via a paint hook
overlay that suppresses the placeholder while composing.

Tests: 12 alcotest cases on the state machine (lifecycle, cancel on
focus loss, edit churn, utf8 passthrough, candidate gating) plus 4
lui_window cases (marked never dispatched, key gating, blur cancel,
Ime_rect emission). Verified live on macOS with the Pinyin input
source: marked text renders underlined, candidate window anchors at
the field rect, 你好 commits and submits end to end, blur cancels.
Render every golden scene and authored edge cases through both
Lui_raster and Lui_gl and compare premultiplied BGRA frames:
exact equality where the math is shared, ±1–2 where float32 vs
float64 rounding legitimately lands either side of an LSB, and
named expectations where the renderers are known to diverge
(continuous corners, nested rounded clips, image src-rect bleed,
margin-less glyph/image quads, empty src rects, GLSL-less effects).

Damage parity: Lui_raster.Renderer's incremental composite vs a
full GL redraw across random mutated scenes, plus a backdrop-blur
damage case. Per-scene diff stats print the divergent count, max
channel delta and first-divergence coordinate.
Status item (tray), menus built from an OCaml tree spec,
notifications (UNUserNotificationCenter with legacy fallback),
clipboard with change-count polling, open/save panels, get-url
Apple-event handling, activation policy, dock badge, attention
requests and window title.

ObjC stubs compile to real implementations on macOS and failwith
stubs elsewhere, following the lui_text pattern. Callbacks into
OCaml go through fixed named values, dispatched on the main thread.
Alcotest suites: pure model tests everywhere, real-call tests
enabled_if macosx covering the headless-safe paths and graceful
degradation when unbundled.
Builds a deterministic scene exercising every standard kind's painted
form (plus a registered extension kind covering the hole op family),
renders through Lui_host + Lui_layout -> Lui_raster, and writes real
PNG evidence to platform/native/gallery_out/ (master gallery, one PNG
per labeled cell, and a CoreText-labeled variant on macOS).

Alcotest coverage: kind coverage against Lui_wire_schema, PNG decode,
per-cell painted pixels, and per-cell checksum determinism.
Exposes the Lui_a11y semantic tree to Linux assistive technologies via
the AT-SPI D-Bus protocol, following the sibling cross-platform stub
pattern.

OCaml side (pure, fully testable):
- Role mapping table covering all 39 Lui_a11y roles with documented
  fallbacks for roles AT-SPI lacks (e.g. Switch->SWITCH,
  Search_field->ENTRY, Radio_group->GROUPING, Navigation->LANDMARK).
- Derived AT-SPI state sets, interfaces (Accessible/Component/Action/
  Text/Value/Application/Socket) and actions per node; incremental
  Lui_a11y diffs translated into Object/Cache/Focus/Window signals.
- Full method dispatch for served objects, synthetic application root
  and cache object, including Text granularity replies and
  action/focus/caret/value callbacks into OCaml.
- Introspection XML generation; UTF-8 code-point <-> byte offset
  conversion for text interfaces.

C side: raw D-Bus transport bound to libdbus-1 via dlopen at runtime —
no link-time or dev-header dependency; degrades to a queued Offline
transport when no bus is reachable, so the library links and tests run
everywhere. Non-Linux builds failwith per the usual stub pattern.

22 alcotest cases: role-map completeness, state-set translation,
path/id wiring, cache event emission, method table, text calls,
action/value callbacks, update diffs, focus events, offline transport.
shader.glsl + lui_gpu + lui_gl now match the CPU renderer on all six
documented divergences; the parity Known list is empty.

- Continuous corners: ported the CPU squircle distance (cornerDist /
  contDist / contCoverage, quartic P(rho), 1/16-px finite difference)
  into the shader; rectCoverage dispatches on negative fitted radii.
- Multi-level clip: instances carry the innermost three clip frames
  (two new vec4 attr pairs, 15 total); coverage is the product of all
  stamped levels. Deeper levels still bound by integer scissor.
- Image src-rect bleed: builder writes the CPU's src-texel clamp bounds
  into the image instance's inner field; the shader clamps the
  continuous sample coordinate before the bilinear fetch.
- Empty eglsl: lui_gl picks a built-in GLSL twin by effect name
  (testfx/dim/tint/lens) so the GL renderer draws what the CPU twin
  computes for parameter-driven effects.
- Quad margin: fill/image/effect quads expand 1 px and derive uv from
  the unexpanded rect, rasterizing the fractional-rect outer ring.
- Empty src rect: builder now skips image ops whose isrc is empty,
  matching the CPU.

Parity: 46/46 scenes green; fill_cont, image_ops, image_empty_src,
effect, two_effects byte-exact; remaining flips are Within(1)
float32-vs-float64 rounding or Within(2) backdrop quantization.
…acement

- grid columns=n + gap: refine cell basis to (inner_width - (n-1)*gap)/n
  after first layout so n cells + gaps fit one line
- frame extent: measure content at unbounded height, then lay out at
  max(offered, content) — content is no longer silently clipped; new
  content_extent reports the used extent
- popup kinds with no inset*/x/y/anchor props default to centered in
  the containing block (flex parent border box; offered frame for
  store-root popups)
- lui_gallery: revert Row chunking workaround to real grid cells,
  regenerate gallery PNGs
- regression tests for all three fixes
Status items (AppIndicator/StatusNotifier), tray menus from the
lui_shell menu model, freedesktop notifications and the Unity
launcher entry over the session D-Bus, clipboard/dialogs through
GTK 3, and XDG open-url registration (.desktop + mimeapps.list +
org.freedesktop.Application). Platform libraries bind at runtime
via dlopen — no dev headers or link-time deps; every API degrades
to false/None offline rather than crashing.
Server-side UI Automation provider exposing the Lui_a11y semantic
tree to assistive technologies. Each a11y node becomes a COM object
implementing IRawElementProviderSimple/Fragment plus the pattern
interfaces its role advertises (Invoke, Toggle, SelectionItem,
Selection, RangeValue, Value, ExpandCollapse, ScrollItem); a
synthetic frame node is the fragment root handed out on WM_GETOBJECT
via UiaReturnRawElementProvider.

- OCaml surface mirrors lui_ax (attach/detach/sync/set_focused/
  clear_focused/refresh_frames/drain_actions/wm_getobject) so the
  host can pick a bridge per platform.
- Full 38-role control-type table with localized-type fallbacks,
  pattern bitmasking, state enums, and action mapping; incremental
  diffs raise property-change, automation, and structure-change
  events.
- In-process verification path (probe_* externals) walks the
  provider through the real client API: UiaHasServerSideProvider,
  UiaNodeFromProvider, UiaNavigate, UiaGetRuntimeId,
  UiaGetPropertyValue, and direct pattern calls.
- Tests: pure-OCaml mapping/state/notification/utf16 suites plus a
  mingw64-gated UIA suite covering provider detection, tree
  enumeration, properties, sync, action draining, focus, and hit
  testing.
Headless offscreen BGRA8 render + readback (instance/device/queue,
command pool, render pass, staging readback) over dlopen'd
libvulkan.so.1 with hand-rolled declarations so the stubs build
without Vulkan headers. Instance draws consume the shared builder
layout (15 float4s/quad) through instanced vertex attributes; push
constants carry viewport size. Pipelines: premul-alpha blend,
dynamic scissor, optional dual-source blend when the device
supports it; effect pipelines compile runtime GLSL via libshaderc
or glslangValidator.

Shaders port the shared SDF/gradient/shadow/glyph/backdrop math to
GLSL 450, precompiled to SPIR-V and embedded (sources + binaries
committed). NDC-y down is handled by a negative viewport height.
The corner flag chain uses inout so a corner that returns early
preserves the accumulated value — an out parameter is undefined on
unwritten paths under SPIR-V, which broke continuous-corner
coverage on this device.

Test: renders every op kind, uploads/reads frames, and runs the
shared scene corpus vs the CPU renderer on a real Vulkan device
(lavapipe here; tests document-skip when no driver). Byte-exact
where the float paths agree; the float64-CPU-vs-float32-GPU
rounding class is tolerated at ±1 as documented.
Draws Lui_gpu's instance batches on Windows through D3D11, matching
Lui_raster's pixels: same SDF math (rounded rects, continuous corners,
borders, gradients, shadows), mask/color/subpixel atlas glyphs,
bilinear images, and backdrop effects compiled per-effect through
D3DCompile with the GLSL compatibility shim.

Hardware device with WARP fallback, offscreen render targets for the
parity tests and a flip-model swapchain for windows, atlas/image
texture upload tracking, and a test that diffs every op kind against
the CPU rasterizer.
- app_spec record (identity, entitlements, notarization via env/keychain creds only)
- bundle: .app assembly, Info.plist gen, iconset->icns, dylib/framework enumeration
- sign: codesign --deep --options runtime, nested-then-bundle ordering, verify w/ codesign -vvv
- notarize: notarytool submit --wait + staple; Skipped w/o creds, never fakes success
- dmg: hdiutil w/ Applications symlink + .DS_Store layout, zip fallback
- update: file-level sha256 delta (bsdiff when smaller, moved-file detection) + verifier
- pure-OCaml sha256/crc32/adler32/deflate/bsdiff/bplist/.DS_Store/varints (no new deps)
- CLI: lui_pkg bundle|sign|notarize|dmg|update|apply
- linux/windows declared stubs returning Unsupported via packager record
…l, tests

Extend demo_app into a real application exercising the whole native
chain (Lui_app -> patches -> store -> flex layout -> paint ->
Metal/GL/CPU render -> events -> IME -> a11y):

- five bottom-tab sections: inputs (text fields, secure field, search,
  textarea, combobox, IME), controls (checkbox, switch, radio group,
  slider, stepper, select, progress, spinner, icons, kbd), lists
  (scrollable keyed list with per-row controls, table, timeline,
  draft field + add flow), overlays (dialog, popover, dropdown menu,
  context menu, sheet, toast, drawer, tooltip, toolbar + menu),
  deco (gradients, shadows, per-edge borders, glass, opacity, stack,
  split + resizable)
- live FPS + renderer badge, footer status line, on-screen shortcut
  legend; interactive-only sends stay out of the headless path so the
  checksum doubles as a regression
- scroll: Ui tracks scrollable kinds + overflow, wheel events walk the
  hit path to the deepest scrollable ancestor and clamp to the content
  span the scroll mirror reports
- shell: status-item tray, window menu items routing into the pending
  queue, notify/clipboard/a11y-dump demo buttons — all guarded to the
  interactive path
- tests: wheel/scroll-offset case, dispatch-flow through the demo's
  own event paths (tab switch -> draft -> add -> toggles -> dialog
  open/close -> every section mounts), headless checksum regression
…2e input suite

- new platform/native/e2e/input: 13 headless tests driving Lui_window.Ui
  (IME composition + textarea, menu button/context/innermost menus,
  observed appear events, a11y list/table trees, input-follows-focus,
  tab focus, hover-while-pressed, resize-first-frame)
- new lui_scene test: atlas put padding clear, make-room/growth,
  full-frame repack integrity, text-coverage curve
- lui_raster: shadow outside cast, shadow formula profile, inset shadow
  pixels, thin-line coverage, opaque-mask coverage ramp, oklab gradient
  checked against an independent reference
- lui_text: glyph shades/thick coverage, many-size shaping
- gaps.md: re-audit all 495 rows — 70 marked covered with evidence,
  425 annotated TODO with the missing capability/module
…kend

SDL window + GPU renderer ladder (Lui_gl default, Lui_vulkan via
LUI_GPU=vulkan, raster+blit via LUI_GPU=0|cpu, no-op tail, never
crashes) + Pango text engine + Lui_a11y/Lui_ax_linux bridge +
Lui_shell_linux tray/notify/clipboard + Lui_ime marked-text path,
mirroring the macOS platform/native/lui_window host.

LUI_GPU unset or other -> [Gl; Noop]; =0|cpu -> [Cpu; Noop];
=vulkan -> [Vulkan; Gl; Noop]; --headless without override forces
[Noop] so the checksum stays deterministic.

Known degradation: the lui_ax_linux c_emit_* stubs for >5-arg
externals are declared with the bytecode calling convention and
segfault from native code on a live transport, so the event queue is
drained instead of flushed (incoming AT actions still work).
Check a per-channel JSON feed, plan delta-first, download with
size+SHA-256 verification, stage the new tree via Lui_pkg.apply_delta
or a full archive, swap by rename dance with a rollback backup, and
relaunch through a detached re-exec. Fetching goes through an injected
fetcher (default: hardened curl) so headless tests never hit the
network; the scheme policy allows https/file and loopback http only.
tiensonqin and others added 29 commits October 13, 2026 07:45
…t-test (#215)

* perf(native): incremental forest roots and indexed child access

- root_ids now reads an incrementally maintained roots set instead of
  folding every node + probing each parent per call — a paint over a
  100k-node store no longer pays O(n) just to enumerate roots.
- child_count / child_id_at / child_index / children_range expose
  indexed slices of the child vector so virtualized containers can
  materialize a mounted window without building the full child list.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(native): real virtualized lists — mounted window drives layout, paint and hit-test (Milestone 3)

A virtual-list's store children stay the logical model; the layout
engine materializes flex trees only for the mounted window of item
indices (visible range + overscan):

- Lui_layout: per-list extent model (Fenwick tree over stride =
  measured extent + gap) giving O(log n) index<->offset; the list is
  a leaf in the main mirror whose intrinsic main size is the virtual
  content extent; mounted row subtrees live in a side forest and get
  composed absolute rects in t.rects. Sync feeds a ?vport accessor
  (content-space visible range per list); on a memo hit only the rows
  that slid are remounted — scroll frames pay no mirror rebuild.
- Config rides data-attrs (no schema change): data-item-extent,
  data-overscan, data-track-visible-range, data-scroll-target/
  token/anchor.
- Lui_paint: hooks.mounted_range slices the child walk to the mounted
  window; the paint pass no longer touches unmounted rows.
- Lui_host: forwards the vport feed into every engine sync and
  exposes the virtual-list query surface (mounted range, virtual
  extent, item rects incl. unmounted indices, key/id -> index).
- Ui/main: hit-test mirror and scroll caps slice to the mounted
  window and span the virtual extent; scroll-target resolves keys
  through the model so unmounted rows are reachable; visible-range
  reports answer via the fed viewport + binary search; the
  scrollable-id list is memoized on store revision so populate does
  not re-walk all rows per scroll frame.
- Lui_elements.virtual_list gains ?overscan / ?item_extent.

Tests: layout window mount/move/measured/gap/unwindowed, paint
mounted-range slicing, Ui vlist scroll-request.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(native): feed vlist viewports in perf harness, assert mounted bound

- Harness feeds each virtual-list's content-space visible range into
  layout sync and wires mounted_range into paint hooks — the same
  composition the window driver makes.
- vlist scenarios report "mounted rows"; the perf test asserts the
  milestone bound (10k rows mount <= 200).

Results: vlist-100k scroll frame p50 = 7.3ms (paint 0.13ms), mounted
rows = 38 at 10k/50k/100k alike.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* perf(ml): lui_flex resolved-spec fingerprinting and clean-child bookkeeping

Cache the inputs the flex engine re-derives per child on every parent
layout pass:

- resolve_specs gains a fingerprint: percent-free specs never re-resolve,
  percent specs only when the owner's inner dims actually changed.
- Non-auto margin sums per axis are computed once at resolution
  (marg_row/marg_col) instead of re-walking the edge cascade.
- set_position stores the seeded slot values (pos_seed_*), letting a
  later pass place a child without re-running the seed math.
- Each node records the offer that produced its current measured dims
  (fin_*) plus the offers the flex-line call sites last made
  (prev_a_*/prev_b_*), the context those were taken under (ctx_*), and
  a resolution generation (res_gen) guarding staleness.

The skip gates are present but disabled in this commit; all
per-child work still runs, so layout results are bit-identical.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(ml): lui_flex clean-child fast lanes in the flex pass

Turn the bookkeeping on: when a child is provably unaffected by its
siblings' changes (identical spec resolution, identical owner context,
relative flow, no auto margins, no wrapping), the parent skips the work
that would re-derive identical results:

- flex-basis reuse for definite or context-stable bases (guarded by
  was_sflex since the single-flex zero-basis depends on the sibling set)
- pass-1 freeze scan skipped when there is no space to distribute
- offer derivation + recursive call skipped at both flex-line sites
  when the offer equals the one that produced the current measured
  dims (fin/prev tracking, guarded by resolution generation and parent
  direction)
- auto-margin scans skipped when no relative child has auto margins

100k-column, one child height change: ~85ms -> ~45ms per relayout
pass; the two-pass frame relayout drops ~205ms -> ~91ms.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…) (#229)

* fix(native): mask secure-field text before it reaches the renderer

A secure-field's real text went straight into the text hook and
painted as plaintext; the a11y password flag only marks the tree.
The paint pass now substitutes one bullet per grapheme before the
string reaches the text engine, and the window text engines apply the
same mask to any string that bypasses the paint resolver (the IME
composition overlay's marked text) and to every geometry answer —
measure, caret prefix, pointer-to-offset — so caret/selection/IME
geometry is computed on the masked text. Masked hit offsets map back
to real byte offsets through the same grapheme boundaries.

The grapheme segmenter is a compact UAX #29 subset kept engine-free
in Lui_paint so paint and all three window engines share one mask
shape. The gallery cell no longer hides the bug behind a literal
"********" — it passes a real value and relies on the mask.

Tests: lui_paint_test asserts the text hook only ever receives the
mask (fails on the old code, which handed it the plaintext), plus
grapheme/mask/offset-mapping unit cases.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(native): share the stacking/clip walk between paint and hit-test

Paint and hit-test resolved stacking independently and disagreed:
paint sorted siblings by z-index and promoted popup kinds out of
ancestor clips into a deferred overlay pass, while hit_path walked
raw child order and the drivers' hit mirrors baked an approximate
scroll-ancestor clip into stored rects. A z-ordered node could paint
on top yet lose the click to a DOM-later sibling, and a popover
inside a scroll container could paint outside the clip yet never be
hit.

Lui_paint gains walk — the traversal producing each node's resolved
stacking/clip frame (z-ordered siblings, popup promotion with a fresh
clip context, display:contents pass-through, accumulated ancestor
clip). paint now runs as enter/leave callbacks on the walk emitting
the same op stream, and Ui.hit_path runs the same walk over the
driver's resolved rects (fed back as placement overrides) with only
the per-node contains check on top.

Driver hit mirrors in all three main.ml files stop baking
ancestor-clip intersections into stored rects — each node keeps its
full resolved box and the walk recomputes the clip with the exact
clips rule.

Tests: three new hit cases in lui_window_test — z-index order wins
the click, a popover inside a clipped scroll container is hittable
outside the clip, and a mid-tree popup wins over a later sibling
covering the same spot. All fail on the old code.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(macos): re-pin headless golden for masked demo secure field

* fix(macos): drop rect_intersect left unused by the shared hit walk

* fix(native): distinguish node-not-drawn from subtree-invisible in culling

The viewport cull dropped a node's whole subtree when its own box
missed the frame, losing overflow:visible children, position:fixed
descendants (which resolve against the frame) and promoted popups.

paint_enter now skips only the node's own chrome and keeps walking;
the true subtree cull lives in the shared walk as a dead-clip path —
a clip that misses the viewport can contain no output, so the walk
switches to a ghost pass that skips enter/chrome cost and only hunts
promoted popups, still threading alpha and hit flags down the path.

hit_path gives the walk the pointer point as its viewport so clips
that miss it prune; popup escapes keep their ancestors' flags.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(native): assert actions/state in headless runs; share the ladder walk

C8: the Linux/Windows "interaction" tests drove Ui.handle but dropped
the returned actions and only compared two runs' scene checksums —
inputs whose dispatches never reached the model could still "pass".
Likewise the renderer-fallback coverage only asserted the ladder
ordering and the no-op renderer's frame size, a CPU→CPU comparison
that never touched the driver's real step-down path.

- Lui_window.Renderer_ladder now owns the walk/demote/renderer_live
  sequencing both drivers wrote by hand: try_rung init failures step
  down, the untried tail is kept for runtime demotion, release and a
  per-demote window re-fit hook come from the platform. The SDL and
  Win32 mains wire their rung inits into it unchanged.
- The headless harness primes the hit-test mirror before the first
  scripted event (previously frame 1's click tested against empty
  rects and always missed), records every action list, and feeds
  Dispatch events back into the app the way apply_actions does.
- test_headless_actions asserts the actions themselves (Focus_changed,
  Press, TextChanged, Submit) and the resulting store mutations:
  on Linux the submitted draft mounts as a new item and clears the
  field; on Windows the Name field's text and the last-event line
  update. The Windows variant addresses the autofocused field by
  prop and its laid-out rect rather than guessed coordinates.
- test_ladder_demote injects init failures on the GPU rungs and a
  mid-render device loss on the CPU rung through the same
  Renderer_ladder the drivers use, asserting walk order, release,
  and that renderer_live retries on the next rung inside the same
  render call.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(windows): assert scripted headless runs replay the same actions, focus and store

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* perf(ml): typed dirty flags and O(1) wide child removal (Milestone 1)

Lui_store now classifies every applied op into typed dirty masks
(structure/layout/paint/semantics/input, plus layout-kids for
parent-driven props like grid columns, split value, edge-inset edge)
and tracks a separate layout_rev bumped only by geometry-relevant
ops. Lui_layout.sync keys its memo on layout_rev, so a paint-only
prop change (background, opacity, border-*, z-index, ...) no longer
triggers a wholesale flex mirror rebuild.

The store also keeps a bounded op journal (changes_since) that
incremental consumers replay instead of rescanning the tree.

Children move from a compacting array to an intrusive doubly-linked
list: detach is O(1) via the child's own sibling links, so removing
N children from a wide parent is O(N) total instead of O(N^2); drop
skips unlinking children of a doomed parent. Index inserts walk from
the nearer end.

Tests: masked-drain classification, layout_rev gating, journal
replay, paint-only prop skips the mirror rebuild, and a 20k-child
removal complexity check (linear; the array needed seconds).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(ml): persistent incremental layout mirror (Milestone 2)

The flex mirror no longer rebuilds per sync. node_id -> flex_node
mappings persist across frames in a shadow of produced expansions
(pkids/sparent/owner), and Lui_store.changes_since replays the op
journal onto them:

- SetProp/RemoveProp classified layout re-styles the node's flex node
  in place (new Lui_flex.set_measure/set_children swap style, measure
  and child arrays without recreating the node); layout-kids props
  restyle the produced children.
- display/structure props re-expand the node's produced set in place.
- Insert/Move/Remove splice the raw-parent shadow and re-set only the
  affected owner's flex children; contents/hidden nodes lift their
  produced set through the raw chain to the nearest flex owner.
- The flex engine's own dirty propagation confines relayout to the
  touched path, and rect_dirty scopes the rect re-read to the shifted
  produced subtrees (top affected owner plus any later frame siblings).

sync now has three lanes: paint/semantics-only churn returns early on
an unmoved layout_rev, the journal replays layout-relevant ops, and a
wholesale rebuild remains for input changes, invalidation, or a
journal that no longer reaches our cursor.

Differential tests rebuild-oracle the mirror after every op pattern
(moves, reorders, display toggles, detach-to-root, drops, prop churn,
two-root shifts, an 80-round seeded fuzz, and a 10k-sibling tree).
Single text edit on a 10k flat column: ~73 ms -> ~17 ms (mirror
rebuild eliminated; the residual is the flex line re-flow, P2-1
territory). Paint-only prop sync: 0 ms.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(ml): track changed rects across incremental layout refreshes

take_rect_changes hands the retained paint pass the set of store ids
whose absolute rect moved, so it can re-emit only moved subtrees
instead of repainting everything.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(ml): retained scene fragments — journal-driven incremental paint (P0-3)

Adds Lui_retained: a per-node paint cache that keeps each node's
emitted ops plus the glyph/wide/effect records they index. The store
op journal (replayed at a paint-owned cursor), engine rect changes,
and host state hints mark fragments for re-derivation; clean frames
skip assembly entirely and a single-node update re-derives one
fragment while the flat scene buffers the renderers consume are
re-materialized.

Wiring: Lui_host.repaint drives the retained pass with the layout
engine's take_rect_changes as the moved set (purged rects now mark
changed too, so hidden/dropped nodes re-derive) and a drained hint
table the window fills from a Ui interaction snapshot — hover, press,
focus, caret/selection/marked, menus, and per-container scroll moves
(a moved offset shifts all descendant rects, so the subtree marks).

Tests: 15-case suite cross-checking the retained scene against a full
repaint after every op batch (random equivalence, structure, z-index,
display cycles, opacity cascade, popup deferral, state hints), plus a
perf scenario: 10k static tree single-color flip repaints exactly one
node.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(ml): subtree-piece segments + cached root set — O(dirty) retained paint

Reworks the retained engine's materialize from a per-node visit
(~34 ms at 100k) to piece references: each frag's seg is a list of
own ops + Sub references to emitting children, so a child's content
edit never touches ancestor lists. Ancestors re-derive only on
emit-status flips or child-set changes, driven by a deepest-first
worklist (pend_repaint / pend_kids / pend_segs) instead of a tree
walk; marks under culled branches persist until the branch surfaces.

Also memoizes Lui_store.root_ids on layout_rev — the forest scan was
an O(N log N) hidden cost inside every frame (helps layout too).

Bench (perf scenario retained-*): 100k static tree, one color flip —
104.9 ms full -> 0.04 ms p50 / 0.40 ms p95, 1 node repainted.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(ml): scene revision + Lui_gpu batch cache skeleton (P2-3)

Adds a paint revision to Lui_scene (bumped by reset; touch for
out-of-band writers) and Lui_gpu.batch_cache: memoized batch list
plus a grow-only packed instance buffer, keyed on the scene's rev and
the inputs build folds into instances (wide flag, frame size, text
coverage params, atlas dims). A paint that changed nothing — the
retained pass's clean frames — reuses the batches and the staging
buffer wholesale.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(ml): wire scene batch cache into all four GPU renderers

Each renderer now holds a Lui_gpu.batch_cache and calls Lui_gpu.frame
for its batches and packed instances instead of rebuilding them
frame-locally. On a hit — a paint that changed nothing — GL skips
glBufferData, Vulkan and D3D11 skip the instance-buffer memcpy (their
persistent vertex buffers already hold the contents), and Metal reuses
the OCaml-side compile+pack (its stub still stages the buffer each
render; noted as follow-up). Per-batch draw state (scissor, image,
pipeline, attribute pointers) still re-issues every frame: pointers
encode the batch offset, they are draw setup not one-time state.

Writers mutating scene buffers outside the reset–seal window now bump
the revision (Lui_scene.touch): the glass and terminal plugin emit
helpers, the fuzz testkit's Maker.change, and the backdrop damage
parity case.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* docs: frame compiles the instances in the GL renderer header

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ml): restore Lui_paint.resolve_in dropped by #224's squash merge

feat/ml-backend @ 206eff8 does not compile: the squash merge kept
lui_retained.ml's resolve_in call sites but lost the three-line
ctx-wrapper in lui_paint.ml when the surrounding resolve_rect
changed under it (C2/C4 shared walk + rect_snap). Re-add the wrapper
delegating to the new snapping resolve_rect.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…drift (#216)

* perf(native): incremental forest roots and indexed child access

- root_ids now reads an incrementally maintained roots set instead of
  folding every node + probing each parent per call — a paint over a
  100k-node store no longer pays O(n) just to enumerate roots.
- child_count / child_id_at / child_index / children_range expose
  indexed slices of the child vector so virtualized containers can
  materialize a mounted window without building the full child list.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(native): real virtualized lists — mounted window drives layout, paint and hit-test (Milestone 3)

A virtual-list's store children stay the logical model; the layout
engine materializes flex trees only for the mounted window of item
indices (visible range + overscan):

- Lui_layout: per-list extent model (Fenwick tree over stride =
  measured extent + gap) giving O(log n) index<->offset; the list is
  a leaf in the main mirror whose intrinsic main size is the virtual
  content extent; mounted row subtrees live in a side forest and get
  composed absolute rects in t.rects. Sync feeds a ?vport accessor
  (content-space visible range per list); on a memo hit only the rows
  that slid are remounted — scroll frames pay no mirror rebuild.
- Config rides data-attrs (no schema change): data-item-extent,
  data-overscan, data-track-visible-range, data-scroll-target/
  token/anchor.
- Lui_paint: hooks.mounted_range slices the child walk to the mounted
  window; the paint pass no longer touches unmounted rows.
- Lui_host: forwards the vport feed into every engine sync and
  exposes the virtual-list query surface (mounted range, virtual
  extent, item rects incl. unmounted indices, key/id -> index).
- Ui/main: hit-test mirror and scroll caps slice to the mounted
  window and span the virtual extent; scroll-target resolves keys
  through the model so unmounted rows are reachable; visible-range
  reports answer via the fed viewport + binary search; the
  scrollable-id list is memoized on store revision so populate does
  not re-walk all rows per scroll frame.
- Lui_elements.virtual_list gains ?overscan / ?item_extent.

Tests: layout window mount/move/measured/gap/unwindowed, paint
mounted-range slicing, Ui vlist scroll-request.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(native): feed vlist viewports in perf harness, assert mounted bound

- Harness feeds each virtual-list's content-space visible range into
  layout sync and wires mounted_range into paint hooks — the same
  composition the window driver makes.
- vlist scenarios report "mounted rows"; the perf test asserts the
  milestone bound (10k rows mount <= 200).

Results: vlist-100k scroll frame p50 = 7.3ms (paint 0.13ms), mounted
rows = 38 at 10k/50k/100k alike.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(native): variable-height virtual lists — measured extents, Fenwick positions, anchor drift (Milestone 5)

- Per-row measured main extents feed the Fenwick tree already in the
  vlist model (stride = extent + gap): index→offset and offset→index
  stay O(log n) as real row heights replace the estimate; the leaf's
  virtual extent repatches and relayouts when measurements move the
  total.
- Anchor correction: when freshly mounted overscan rows above the
  viewport top measure off-estimate, the model records the anchor
  item's prefix delta as drift; the driver consumes it each populate
  (virtual_take_drift → Ui.scroll_set) so the top row keeps its
  screen position instead of visibly jumping. populate now returns
  the correction's actions for dispatch.
- Memo-hit remount runs a second mount pass after the extent patch
  so rows the post-measure window now reaches are covered the same
  frame.
- Perf: vlist-var-100k scenario (every 7th row 60px vs 28px estimate)
  exercises the measurement path; harness applies drift like the
  driver.

Tests: vlist mounted_children slice, anchor drift (+160 correction on
a mid-list jump, consume-on-read), corrected item positions.

Bench: vlist-var-100k scroll frame p50 = 3.55 ms, mounted rows = 41 —
as flat as the fixed-height runs.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
lui_retained was written against the pre-C2 paint API (resolve_in,
paint_node_body, ordered_children, node_shown) that #207 C2 and #217
C4 replaced with the shared Lui_paint.walk. Re-plumb the fragment
cache onto the same decisions the walk makes — geometry/clip
threading via a new Lui_paint.frame_of, dead-clip culling via
Lui_paint.clip_dead, chrome and clip brackets via
paint_enter/paint_leave, ghost traversal via paint_ghost_enter — so
retained and full paint produce identical scenes by construction.

Popup handling now mirrors the deferred drain: popup-kind roots
promote too, and each subtree's sub_pops lists only the next drain
level so popups nested inside popups surface level-by-level in
forest order.

Invalidation gaps the journal replay missed:

- seg_dirty flags outlive the per-frame worklist on culled branches;
  a fresh mark on one never re-entered pend_segs, so stale pieces
  kept emitting through a parent's memoized Sub.
- Reparenting changes every descendant's inherited context
  (foreground, inverse-surface background), but only the moved child
  was repainted; InsertChild/RemoveChild now repaint the subtree, and
  foreground / tooltip-background prop changes mark their readers.
- Child-list mutations can flip a sibling's last-sibling chrome
  without touching the node itself; was_last tracking repaints both
  sides of the flip.

Correctness fixes outside the cache:

- The geometry-translation fast path assumed every absolute
  coordinate in cached ops tracks the node box; the modal scrim
  fills the viewport and must never translate. New
  Lui_paint.viewport_anchored gates the path.
- Lui_store RemoveChild's defensive scan cleared c.parent even when
  the child was not in the named list, leaving it dual-membershipped
  (root + live list) — the seed of sib-list cycles and hangs.

The random differential test now mutates across 14 op kinds —
z-index, nested popups, scroll/overflow clips, offscreen placement,
display:contents, reparents, prop removal — and asserts scene
equality with a full paint every frame.

Generated with [Devin](https://devin.ai)

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* perf(ml): P2-2 spatial index — subtree bounds + interval arrays

New lui_spatial library: a snapshot index over resolved node rects
that both the paint pass's viewport cull and hit-testing prune with.
Per node it records the shown subtree's bound [b], its clip-escaping
part [eb] (promoted popups), and a zero-area flag [z]; parents with
>= 32 children get a bound-top-sorted array with prefix-max bottoms
so a query touches O(log n + candidates) siblings instead of every
child. The env is caller-supplied so the module stays free of
Lui_paint dependencies.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(ml): P2-2 indexed walk — paint viewport cull via spatial query

Lui_paint.walk takes an optional (index, query) pair: roots come from
the index's snapshot (avoiding the O(n) root_ids fold per walk),
wide parents enumerate candidates through the interval arrays, and
small parents fall back to a per-child keep test that preserves the
walk's exact clip/popup/z-order semantics. Lui_paint.paint gains
?spatial supplying a Region query over the viewport; without it the
walk is byte-for-byte the old path. spatial_env publishes the
paint-semantics env index builders share.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(ml): P2-2 wire the spatial index through host and window

- Lui_query.hit_path/hit take ?spatial and serve point queries
  through it — same walk, same path order.
- Lui_host gains an invalidation counter (bumped by resize,
  set_hidden, set_ctx_anchor, invalidate_layout) plus a spatial
  provider thunk consulted inside repaint.
- Lui_window.Assembly builds the index lazily over the populate-time
  hit mirror, stamps each mirror with a geometry key (store revision,
  host invalidations, scroll epoch, live context anchor, scale,
  viewport) and hands the paint pass an index only when the key
  proves the mirror still matches the frame being resolved; hit
  paths get the index whenever it matches the current mirror.
  Stale or absent keys fall back to the unindexed walk, so the index
  can never serve a wrong answer.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(ml): P2-2 spatial tests + hit benchmark

- lui_paint tests: indexed-vs-unindexed paint produces identical op
  streams (z-order, clip, promoted popup, contents, hidden, spacer),
  a 300-row column visits <= 12 nodes under a 100px viewport vs 302
  unindexed, and zero-area children stay reachable.
- lui_input tests: a point-grid sweep asserts hit_path indexed ==
  unindexed on a tree covering clip escape, display:contents, hidden
  subtrees, spacers and z-overlap; plus promoted-popover and
  wide-list hits.
- perf scenario hit-*k: hit_path p50 unindexed vs indexed, indexed
  static-frame paint, and index build cost. At 100k rows: hit_path
  320ms -> ~0.002ms; indexed static paint 0.06ms vs ~120ms; index
  build ~250ms once per geometry epoch.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
menu-item's box branch already paints the glyph in the fixed leading
slot via control_icon_ops and text_placement_of reserves the slot in
the label pad; labeled_content_ops painted a second side-slot copy,
so every icon in a Linux/SDL overlay menu appeared twice. Adds a
lui_paint regression test.
invalidate (journal truncation past the retained cursor, viewport/scale
change, invalidate_paint) reset every fragment, but fresh fragments
start with no kid list and nothing re-marked them, so the next frame
painted childless roots: a blank window. In Logseq, opening the
Keymap settings pane (~2.5k nodes in one batch, > the 8192-op journal)
blanked the whole UI. Re-mark every node's kids on the next paint.

Adds regression tests for journal overflow and explicit invalidate.
Reuse flex measurements, classify extension invalidation, and bound asynchronous Metal presentation. Optimize fill and glyph rendering, report actual presentation timings, and hide idle scrollbars. Add native performance and pixel parity regression coverage.
Preserve layout mirrors across resize and transient editor replacements, reuse retained glyph and Metal buffers, and correct pointer activation and popup visibility. Add allocation, lifetime, geometry, and presentation regressions.
Retain glyph instances only for the current frame and render Metal windows directly into drawables. Release spare drawable capacity during idle.

Stream software redraws through bounded bands and reduce pixel-loop allocation. Add split transitions and reactive column widths.

Validation: release @ALL and @runtest pass, including raster pixels, ownership, allocation, and transition regressions.
Use direct node lookup while preserving missing and dropped node behavior.

The regression reduces allocation for 10,000 lookups from 160,096 bytes to 96 bytes. Release @ALL, @runtest, and @install pass.
Inline float min/max to avoid boxing intermediate geometry, and reuse each
row's solid span instead of evaluating inset coverage for interior pixels.
Keep the 256 KiB allocation ceiling and cover both shadow kinds at two heights.

Validation: OCaml 5.5.1 classic and Flambda O3 builds and test suites pass,
including raster goldens and GPU parity checks.
Prevent constant lifting of the immutable glyph used by the weak-reference
check. Verify the cache owns current text and releases it after replacement,
while keeping the cache itself alive through both collections.

Validation: both compiler test suites pass. Temporarily removing glyph-cache
pruning makes the corrected lifetime assertion fail as expected.
Reuse shadow integration buffers and read cached clip intervals without allocating per-row tuples or closures. Return scalar shape edges to keep tall fills within the existing allocation budget.

Validate glyph and tall-shadow allocation budgets, raster goldens, multiworker pixels, and the full release build and test suite.
Pass the rooted OCaml source directly to the active OpenGL entry point instead of retaining only a temporary pointer cell.

Cover renderer initialization under concurrent major collection; release builds, GL rendering, and the complete parity suite pass on macOS.
Keep producers alive across frames and await every job before returning pixels or propagating upload failures. Join owned domains when the renderer is released.

A low-allocation 32-frame redraw falls from 85–109 forced minor collections to zero. Cover release and reuse, changing worker counts, pixel parity, and failure recovery; the full release build and test suite pass.
Replace per-pixel corner lists, tuples, and clip traversal closures with scalar coverage evaluation. Add failing-then-passing allocation regressions for continuous borders and clipped inset shadows while preserving reference pixels.
Snapshot the collection parent and removed subtrees while preserving queued row updates. Cover append and reorder allocation plus rollback after a removed descendant is followed by an invalid insertion.
Retain one fewer background domain for parallel band rendering while keeping uploads on the caller. Verify resident domain limits, complete pixels, release, and recovery from caller or producer failures.
Copy opaque spans in bulk, reuse covered row interiors, and remove redundant saturation from premultiplied blending. Reuse clip spans for sharp inset shadows and specialize integer bounds comparisons. Preserve pixel parity and the existing serial and parallel scrolling budgets with boundary and alpha coverage regressions.
Read live nodes and typed property values directly before constructing public optional results. Preserve missing and mistyped lookup behavior, and cover scalar, optional, and removed-node reads with allocation regressions.
Return final subtree facts instead of allocating mutable accumulators, result tuples, and child-id lists per node. Reuse equal immutable rectangles and fix negative subtree bounds that previously extended to the origin. Cover negative culling and a 1000-node allocation budget.
Keep clipping comparisons monomorphic and reuse immutable rectangles when the intersection is unchanged. Preserve empty, nonfinite, and rounded geometry with allocation regressions.
Recompute unchanged descendants only when inherited visibility changes, and avoid an intermediate child-node list. Cover incremental parity, reparenting, visibility, and allocation budgets.
@tiensonqin
tiensonqin merged commit 940100e into main Oct 15, 2026
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant