Skip to content

perf(ml): M3 real VirtualList — mounted window drives layout/paint/hit-test - #215

Merged
tiensonqin merged 11 commits into
feat/ml-backendfrom
perf/ml-m3-virtual-list
Oct 13, 2026
Merged

tiensonqin merged 11 commits into
feat/ml-backendfrom
perf/ml-m3-virtual-list

Conversation

@tiensonqin

Copy link
Copy Markdown
Collaborator

Summary

Plan P0-2 / Milestone 3: a virtual-list keeps all children in the store as the logical model, but the backend materializes flex trees only for the mounted window (visible range + overscan).

  • Lui_store: incremental roots set makes root_ids O(#roots) instead of a full-table fold per paint; child_id_at indexed lookup (nearer-end walk).
  • Lui_layout: per-list extent model — Fenwick tree over stride = measured extent + gap gives O(log n) index↔offset; the list is a leaf in the main mirror sized by the virtual content extent; mounted row subtrees live in a side forest and get composed absolute rects. sync ~vport feeds each list's content-space visible range; on a memo hit only rows that slid are remounted — a scroll frame pays no mirror rebuild. Config rides data-attrs (data-item-extent, data-overscan, data-track-visible-range, data-scroll-target/token/anchor) — no schema change.
  • Lui_paint: hooks.mounted_children slices the child walk to the mounted window; unmounted rows are never touched.
  • Lui_host: forwards the vport feed into every engine sync and exposes the virtual-list query surface (mounted range/children, virtual extent, item rects incl. unmounted indices, key/id → index).
  • Ui / driver: hit-test mirror and scroll caps slice to the mounted window and span the virtual extent; scroll-target resolves keys through the model (target row need not be mounted); visible-range reports answer via fed viewport + binary search; scrollable-id list memoized on store revision so populate does not re-walk all rows per scroll frame.
  • Lui_elements.virtual_list: ?overscan / ?item_extent params.

Test plan

  • dune build clean, dune runtest all green (incl. merged M1 dirty-flags + shared text layout work)
  • New tests: layout window mount/move/measured-extents/gap/unwindowed; paint mounted_children slicing; Ui vlist scroll-target via data-attrs
  • Perf harness feeds vports + asserts mounted bound

Bench (post-merge, this branch)

n frame layout p50 frame paint p50 frame total p50 / p95 mounted rows
10k 0.02ms 0.06ms 3.33 / 3.79ms 38
50k 0.02ms 0.07ms 3.26 / 3.54ms 38
100k 0.02ms 0.07ms 3.47 / 4.50ms 38

Milestone targets met: scroll p50 < 8ms at 100k, mounted render rows < 200, paint cost independent of item count.

Base: feat/ml-backend (first milestone in the vlist stack; includes merge of feat/ml-backend @ cc6ac2a + later).

Generated with Devin

tiensonqin and others added 4 commits October 13, 2026 05:10
- 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>
…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>
- 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>
Conflicts resolved keeping both sides:
- lui_store: upstream intrusive child list + typed dirty masks +
  change journal, plus the incremental roots set; indexed child
  access reimplemented on the linked list (nearer-end walk).
- lui_layout: sync memo keys on layout_rev (upstream) with ?vport
  feed preserved; the vlist keeps its own kid-id array snapshot so
  per-row window work stays O(1) despite the linked child list.
- paint/hit-test slicing switched from a (first,last) range +
  store-level indexed access to engine-provided mounted_children id
  lists (O(window), no linked-list walk).
- lui_window/main: upstream caret_ops + vlist hook and unmounted-row
  content_rect fallback both kept.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
tiensonqin and others added 7 commits October 13, 2026 05:58
# Conflicts:
#	platform/native/lui_layout/lui_layout.ml
#	platform/native/lui_layout/test/lui_layout_test.ml
Resolve conflicts with M4 retained paint / rect-change tracking:
- host: keep both virtual-list surface and take_rect_changes
- paint: adopt shared stacking walk; walk takes ?mounted so virtual
  lists only walk the mounted window; restore resolve_in
- retained: kids lists honour mounted_children and rebuild when a
  list's mounted window slides (host passes vlists)
- layout: vlist row rects and purged rows feed rect_changed
- store: keep incrementally maintained root set
- window: hit-test visit without anc_clip, over mounted children
Resolve conflicts keeping both features — virtual lists on top of
retained scene fragments, frame scheduler, subtree culling, text
input session and pixel snapping:

- host: keep both the virtual-list engine surface and
  take_rect_changes; repaint hands the vlist ids to paint_retained
- paint: adopt the shared stacking walk; walk takes ?mounted so
  virtual lists enumerate only the mounted window slice; restore
  resolve_in
- retained: kids lists honour mounted_children and rebuild when a
  list's mounted window slides (vwin snapshot + ?vlists channel)
- layout: vlist row rects go through the snapped scene_rect and feed
  rect_changed; purged rows mark rect_changed too
- store: keep the incrementally maintained root set
- window: hit-test visits the mounted children without anc_clip

Adds a retained test that scrolls a virtual list and cross-checks
the painted scene against a fresh full paint on every frame.

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

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

Same-parent merge: the remote tip already carries the feat/ml-backend
merge; this keeps our resolutions on top — vlist row rects snapped
through scene_rect and the retained vlist-scroll cross-check test.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@tiensonqin
tiensonqin merged commit 8a3a5ad into feat/ml-backend Oct 13, 2026
8 checks passed
tiensonqin added a commit that referenced this pull request Oct 13, 2026
Base tree equals the already-merged M3 tip; conflicts are squash
artifacts, resolved to this branch's side.
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