Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7e81ac3
perf(native): incremental forest roots and indexed child access
tiensonqin Oct 12, 2026
5393c32
perf(native): real virtualized lists — mounted window drives layout, …
tiensonqin Oct 12, 2026
e4e2d5a
perf(native): feed vlist viewports in perf harness, assert mounted bound
tiensonqin Oct 12, 2026
7c5dee7
Merge feat/ml-backend into devin/vlist
tiensonqin Oct 12, 2026
51007a8
perf(native): variable-height virtual lists — measured extents, Fenwi…
tiensonqin Oct 12, 2026
f87e0d5
Merge remote-tracking branch 'origin/feat/ml-backend' into ci-215
tiensonqin Oct 12, 2026
e5712d4
Merge remote-tracking branch 'origin/feat/ml-backend' into ci-215
tiensonqin Oct 12, 2026
8df675f
Merge remote-tracking branch 'origin/perf/ml-m3-virtual-list' into ci…
tiensonqin Oct 12, 2026
3d48b2f
Merge remote-tracking branch 'origin/feat/ml-backend' into ci-215
tiensonqin Oct 12, 2026
a8f38b8
Merge remote-tracking branch 'origin/perf/ml-m3-virtual-list' into ci…
tiensonqin Oct 12, 2026
ef343da
Merge feat/ml-backend into perf/ml-m3-virtual-list
tiensonqin Oct 12, 2026
fdc4909
Merge perf/ml-m3-virtual-list into perf/ml-m5-variable-vlist
tiensonqin Oct 12, 2026
9df2801
Merge feat/ml-backend into perf/ml-m3-virtual-list
tiensonqin Oct 12, 2026
1979f0b
Merge remote-tracking branch 'origin/perf/ml-m3-virtual-list' into ci…
tiensonqin Oct 12, 2026
9ef1f7f
Merge remote-tracking branch 'origin/perf/ml-m3-virtual-list' into ci…
tiensonqin Oct 12, 2026
e6f2938
Merge feat/ml-backend (#228) into perf/ml-m3-virtual-list
tiensonqin Oct 13, 2026
8f06538
Merge remote-tracking branch 'origin/perf/ml-m3-virtual-list' into fi…
tiensonqin Oct 13, 2026
ae905a8
Merge feat/ml-backend (after #215 squash) into perf/ml-m5-variable-vlist
tiensonqin Oct 13, 2026
a3af639
Merge feat/ml-backend (#226) into perf/ml-m5-variable-vlist
tiensonqin Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions platform/native/lui_host/lui_host.ml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ module type LAYOUT_ENGINE = sig
val virtual_index_of_key : t -> Lui_store.t -> int -> string -> int option
val virtual_index_of_id : t -> Lui_store.t -> int -> int -> int option
val virtual_view : t -> int -> (float * float) option
val virtual_take_drift : t -> int -> float

(** Store ids whose absolute rect changed under the last {!sync};
drained by the paint pass to re-emit only moved subtrees. *)
Expand All @@ -71,6 +72,7 @@ type layout_state = {
lkey : int -> string -> int option;
lid : int -> int -> int option;
lview : int -> (float * float) option;
ldrift : int -> float;
lchanges : unit -> (int, unit) Hashtbl.t;
}

Expand Down Expand Up @@ -128,6 +130,7 @@ let create ~hooks ?layout ?(measure = no_measure) ~renderer ~width ~height ~scal
lkey = (fun id key -> L.virtual_index_of_key engine store id key);
lid = (fun id cid -> L.virtual_index_of_id engine store id cid);
lview = (fun id -> L.virtual_view engine id);
ldrift = (fun id -> L.virtual_take_drift engine id);
lchanges = (fun () -> L.take_rect_changes engine) }
in
let mask_atlas = Atlas.create ~bpp:1 ~w:512 ~h:512 in
Expand Down Expand Up @@ -187,6 +190,11 @@ let layout_virtual_index_of_id h id cid =
let layout_virtual_view h id =
match h.layout with Some l -> l.lview id | None -> None

(* Consume-on-read scroll correction from virtual-list measurement
drift — the driver adds it to the list's scroll offset. *)
let layout_take_drift h id =
match h.layout with Some l -> l.ldrift id | None -> 0.

(* The layout engine memoizes on (store revision, frame, input
identities); a measure/hidden closure whose answers changed under a
stable identity — e.g. a font stack that finished loading — needs
Expand Down
10 changes: 10 additions & 0 deletions platform/native/lui_host/lui_host.mli
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ module type LAYOUT_ENGINE = sig
val virtual_index_of_id : t -> Lui_store.t -> int -> int -> int option
val virtual_view : t -> int -> (float * float) option

(** Pending anchor correction in device px, consume-on-read: how far
the row at the viewport top moved because rows above it measured
off-estimate. Add it to the list's scroll offset. *)
val virtual_take_drift : t -> int -> float

(** Store ids whose absolute rect changed under the last {!sync};
drained by the paint pass to re-emit only moved subtrees. *)
val take_rect_changes : t -> (int, unit) Hashtbl.t
Expand Down Expand Up @@ -170,4 +175,9 @@ val layout_virtual_index_of_key : t -> int -> string -> int option
val layout_virtual_index_of_id : t -> int -> int -> int option
val layout_virtual_view : t -> int -> (float * float) option

(** Consume-on-read anchor correction in device px; the driver adds it
to the list's scroll offset so the top row keeps its screen
position while variable heights settle. *)
val layout_take_drift : t -> int -> float

val backend : t -> Lui_protocol.platform_profile -> Lui_protocol.backend
38 changes: 37 additions & 1 deletion platform/native/lui_layout/lui_layout.ml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type vlist = {
mutable vs : float; (* last viewport fed (content space) *)
mutable ve : float;
mutable windowed : bool; (* a viewport was supplied → slice *)
mutable drift : float; (* pending anchor correction, device px *)
rows : (int, vrow) Hashtbl.t; (* index → mounted row subtree *)
nodes : (int, Lui_flex.node) Hashtbl.t; (* store id → flex node, rows *)
mutable mounted_ids : int list; (* ids whose rects are registered *)
Expand Down Expand Up @@ -879,6 +880,7 @@ let vlist_state ctx id =
vs = 0.;
ve = 0.;
windowed = false;
drift = 0.;
rows = Hashtbl.create 16;
nodes = Hashtbl.create 64;
mounted_ids = [];
Expand Down Expand Up @@ -1737,9 +1739,27 @@ let vlists_mount (t : t) ctx store ~vport =
let f, l = vlist_range v in
if f <> v.first || l <> v.last || Hashtbl.length v.rows < l - f
then begin
(* Anchor the item at the viewport top: measuring freshly
mounted overscan rows above it shifts every offset below —
record the prefix delta as drift the scroller applies so
the anchored row keeps its screen position. *)
let anchor =
if v.windowed && v.count > 0 then
let ai =
min (v.count - 1)
(bit_lower v.bit
(Float.max 0. (v.vs -. v.bor0 -. v.pad0)))
in
Some (ai, bit_sum v.bit ai)
else None
in
v.first <- f;
v.last <- l;
build_vrows t ctx store v ~first:f ~last:l;
(match anchor with
| Some (ai, y0) ->
v.drift <- v.drift +. (bit_sum v.bit ai -. y0)
| None -> ());
changed := true
end)
t.vlists;
Expand Down Expand Up @@ -1916,7 +1936,11 @@ let vlists_update (t : t) ~vport =
in
t.extent <- (ew, eh);
Hashtbl.iter (fun _ v -> vlist_geometry ctx v) t.vlists;
collect t
collect t;
(* Freshly measured rows can widen the window the first
pass computed — a second mount covers what the
post-measure range now reaches. *)
ignore (vlists_mount t ctx sc.sc_store ~vport)
| None -> ())
end;
vlists_collect t
Expand Down Expand Up @@ -2557,6 +2581,18 @@ let virtual_view (t : t) id =
| Some v when v.windowed -> Some (v.vs, v.ve)
| _ -> None

(* Pending scroll correction from measurement drift, device px —
consume-on-read. The scroller adds it to the list's offset so the
row at the viewport top keeps its screen position when rows above
measure off-estimate. *)
let virtual_take_drift (t : t) id =
match Hashtbl.find_opt t.vlists id with
| Some v ->
let d = v.drift in
v.drift <- 0.;
d
| None -> 0.

(* key → item index, lazily built once per list shape. *)
let virtual_index_of_key (t : t) store id key =
match Hashtbl.find_opt t.vlists id with
Expand Down
6 changes: 6 additions & 0 deletions platform/native/lui_layout/lui_layout.mli
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ val virtual_item_rect : t -> int -> index:int -> Lui_scene.rect option
what the interaction layer reports as the visible range. *)
val virtual_view : t -> int -> viewport option

(** [virtual_take_drift t id] is the pending anchor correction in
device px — how far the row at the viewport top moved because
rows above it measured off-estimate. Consume-on-read: the caller
(the scroller) adds it to the list's scroll offset. *)
val virtual_take_drift : t -> int -> float

(** [virtual_index_of_key t store id key] resolves a child [key] prop
to its item index — O(n) once per list shape, O(1) after. *)
val virtual_index_of_key : t -> Lui_store.t -> int -> string -> int option
Expand Down
60 changes: 60 additions & 0 deletions platform/native/lui_layout/test/lui_layout_test.ml
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,63 @@ let test_vlist_unwindowed () =
check int "mounted" 50 (Lui_layout.mounted_count t 2);
check_some_rect "row49" 0. 980. 100. 20. (Lui_layout.rect t 59)

(* The mounted window's store ids — what paint and hit-test slice to. *)
let test_vlist_mounted_children () =
let store = vlist_store ~n:1000 () in
let t = Lui_layout.create () in
Lui_layout.sync ~measure:no_measure4 ~width:100. ~height:800.
~vport:(vport 500. 200.) t store;
let f, l = Option.get (Lui_layout.mounted_range t 2) in
let ids = Option.get (Lui_layout.mounted_children t 2) in
check int "window size" (l - f) (List.length ids);
check int "first id" (10 + f) (List.hd ids);
check int "last id" (10 + l - 1) (List.nth ids (l - f - 1))

(* Variable heights settle through measured extents: overscan rows
above the anchor that differ from the estimate shift the prefix
sums — the drift the scroller folds back into the offset so the
anchored row does not visibly jump. *)
let test_vlist_anchor_drift () =
let store = Lui_store.create () in
let ops =
ref
[ InsertChild (1, 2, 0);
SetProp
(2, DataAttrs,
StringValue
(Lui_protocol.data_attrs_encode
[ ("data-item-extent", "20") ]));
CreateNode (2, VirtualList); CreateNode (1, Root) ]
in
for i = 0 to 99 do
let id = 10 + i in
(* Rows 46-49 are really 60 px against the 20-px estimate. *)
ops :=
InsertChild (2, id, i)
:: SetProp
(id, HeightValue,
FloatValue (if i >= 46 && i < 50 then 60. else 20.))
:: CreateNode (id, ListItem) :: !ops
done;
Lui_store.apply_batch store { generation = 1; ops = List.rev !ops };
let t = Lui_layout.create () in
Lui_layout.sync ~measure:no_measure4 ~width:100. ~height:800.
~vport:(vport 0. 300.) t store;
(* Anchor is item 0 — nothing above it can move. *)
check (float eps) "no drift at top" 0.
(Lui_layout.virtual_take_drift t 2);
(* Jump mid-list: rows 46-49 mount in the overscan band and measure
60 instead of the 20 estimate — prefix(anchor=50) grew by 160. *)
Lui_layout.sync ~measure:no_measure4 ~width:100. ~height:800.
~vport:(vport 1000. 300.) t store;
check (float eps) "drift" 160. (Lui_layout.virtual_take_drift t 2);
(* consume-on-read *)
check (float eps) "consumed" 0. (Lui_layout.virtual_take_drift t 2);
(* the model reports corrected positions: item 50 sits at
46*20 + 4*60 = 1160 *)
check_some_rect "item50" 0. 1160. 100. 20.
(Lui_layout.virtual_item_rect t 2 ~index:50)

(* Pixel snapping contract: at a fractional backing scale the exported
rects land on device-pixel edges, and siblings stacked without a
gap share their boundary exactly — the same snapped geometry feeds
Expand Down Expand Up @@ -714,6 +771,9 @@ let () =
test_case "vlist measured" `Quick test_vlist_measured_extents;
test_case "vlist gap" `Quick test_vlist_gap;
test_case "vlist unwindowed" `Quick test_vlist_unwindowed;
test_case "vlist mounted children" `Quick
test_vlist_mounted_children;
test_case "vlist anchor drift" `Quick test_vlist_anchor_drift;
test_case "vlist incremental child" `Quick
test_vlist_incremental_child;
test_case "snapped edges" `Quick test_snapped_edges;
Expand Down
25 changes: 20 additions & 5 deletions platform/native/lui_window/lui_window.ml
Original file line number Diff line number Diff line change
Expand Up @@ -2033,7 +2033,11 @@ module Assembly = struct
engine_rect : int -> rect;
(** Unshifted engine rect — content-space input for
[scroll_show]/visible-range. *)
populate : unit -> unit; (** rebuild the hit mirror + scroll caps *)
populate : unit -> action list;
(** rebuild the hit mirror + scroll caps, fold measured anchor
drift back into scroll offsets — the VisibleRange
dispatches those corrections produce come back for the
driver to apply *)
caret_rect : unit -> rect;
(** Focused field's caret rect, device px — candidate-window
anchor while composing. *)
Expand All @@ -2053,7 +2057,8 @@ module Assembly = struct
(* One frame's paint path: the engine re-syncs inside
[Lui_host.repaint], then the hit mirror and scroll caps are
rebuilt from the fresh rects — always in this order, on every
platform. *)
platform. Returns the frame bytes plus the actions populate's
anchor-drift corrections produced. *)
let repaint t =
(* Interaction-state diffs reach the fragment pass through the
host's hint table — the store journal cannot see them. *)
Expand All @@ -2065,8 +2070,8 @@ module Assembly = struct
(fun id () -> Lui_host.note_moved t.host id)
soft;
let px = Lui_host.repaint t.host in
t.populate ();
px
let acts = t.populate () in
(px, acts)

let backend t profile = Lui_host.backend t.host profile

Expand Down Expand Up @@ -2417,7 +2422,17 @@ module Assembly = struct
sids;
(* Rects were just rebuilt — visible-range memos key on the
epoch. *)
Ui.notify_layout ui
Ui.notify_layout ui;
(* Anchor correction: rows that measured off-estimate above the
viewport shifted everything below — fold the drift back into
the scroll offset so the top row does not visibly jump. *)
List.concat_map
(fun id ->
let d = Lui_host.layout_take_drift host id in
if Float.abs d > 0.001 then
Ui.scroll_to ui store id (Ui.scroll_offset ui id +. d)
else [])
(Lui_host.layout_vlists host)
in
(* Caret rect of the focused field, device px: the node's layout
rect shifted to the caret's real position inside the laid-out
Expand Down
13 changes: 8 additions & 5 deletions platform/native/lui_window/lui_window.mli
Original file line number Diff line number Diff line change
Expand Up @@ -781,15 +781,18 @@ module Assembly : sig
(** {!Ui.store_changed} over this surface — run it after every
[Lui_app.flush]. *)

val populate : t -> unit
val populate : t -> action list
(** Rebuild the hit mirror and the scroll caps from the last layout
sync. {!repaint} already calls it; expose only for drivers that
keep paint and hit-refresh on separate schedules. *)
sync, then fold each virtual list's measured anchor drift back
into its scroll offset. {!repaint} already calls it; expose only
for drivers that keep paint and hit-refresh on separate
schedules. The returned actions are the VisibleRange dispatches
the corrections produced — apply them like {!handle}'s. *)

val repaint : t -> Bytes.t
val repaint : t -> Bytes.t * action list
(** One frame's paint path: [Lui_host.repaint] (which re-syncs the
engine) then {!populate} — always in this order, on every
platform. *)
platform. Returns the frame bytes and populate's actions. *)

val caret_rect : t -> rect
(** Focused field's caret rect, device px — the IME candidate-window
Expand Down
4 changes: 3 additions & 1 deletion platform/native/lui_window/main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,9 @@ let tick (s : surf) ~t0 =
(match decision with
| Frame.Present ->
(* Assembly.repaint repaints and refreshes the hit-test mirror *)
ignore (Assembly.repaint s.driver.asm);
(* populate's anchor-drift corrections surface as VisibleRange
dispatches — apply them like any other Ui action *)
apply_actions s (snd (Assembly.repaint s.driver.asm));
ignore (Lui_a11y.sync d.a11y);
if !(d.gl_live_cell) then begin
(match !(d.gctx_cell) with
Expand Down
1 change: 1 addition & 0 deletions platform/native/lui_window/test/lui_window_test.ml
Original file line number Diff line number Diff line change
Expand Up @@ -2429,6 +2429,7 @@ module Fake_layout = struct
let virtual_index_of_key _ _ _ _ = None
let virtual_index_of_id _ _ _ _ = None
let virtual_view _ _ = None
let virtual_take_drift _ _ = 0.

(* The stub re-places the whole tree every sync — every rect is
potentially fresh. *)
Expand Down
4 changes: 3 additions & 1 deletion platform/native/lui_window_linux/main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,9 @@ let () =
the frame counter and checksum actually run. *)
let dirty = headless || Ui.want_frame ui host in
if dirty then begin
ignore (Assembly.repaint asm);
(* populate's anchor-drift corrections surface as VisibleRange
dispatches — apply them like any other Ui action *)
apply_actions (snd (Assembly.repaint asm));
if gl_live () then Sdl.gl_swap_window !win;
Ui.frame_done ui;
incr frame;
Expand Down
4 changes: 3 additions & 1 deletion platform/native/lui_window_windows/main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,9 @@ let () =
the frame counter and checksum actually run. *)
let dirty = headless || Ui.want_frame ui host in
if dirty then begin
ignore (Assembly.repaint asm);
(* populate's anchor-drift corrections surface as VisibleRange
dispatches — apply them like any other Ui action *)
apply_actions (snd (Assembly.repaint asm));
Ui.frame_done ui;
incr frame;
Checksum.add_scene checksum (Lui_host.scene host);
Expand Down
9 changes: 9 additions & 0 deletions platform/native/perf/harness.ml
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,15 @@ let scroll_shift t id =

let set_scroll t id off = Hashtbl.replace t.scroll id off

(* Anchor correction, the way the window driver applies it in
populate: measurement drift folds back into the scroll offset so
the top row keeps its screen position. *)
let apply_vlist_drift t id =
let d = Lui_layout.virtual_take_drift t.engine id in
if Float.abs d > 0.001 then
set_scroll t id
(Option.value ~default:0. (Hashtbl.find_opt t.scroll id) +. d)

(* Engine rect shifted by ancestor scroll offsets — what paint and hit
tests see. *)
let rect_of t id =
Expand Down
Loading
Loading