Repository navigation
perf(ml): P2-3 scene batch cache — memoized GPU frame resources - #226
Merged
Merged
Conversation
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>
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>
# Conflicts: # platform/native/lui_layout/lui_layout.ml # platform/native/lui_layout/lui_layout.mli
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>
… (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>
…d 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>
Adopts the Ui state-machine refactor (lui_input: pointer/focus/ session/selection/gesture/scroll/drag/overlay composers) and ports the retained-paint hint plumbing onto it: set_scroll diffs the machine's offset/cap maps to mark scrolled vs metrics-only containers, and paint_hints snapshots the new accessors. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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>
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>
Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
# Conflicts: # platform/native/lui_host/lui_host.ml # platform/native/lui_host/lui_host.mli # platform/native/lui_paint/lui_retained.ml # platform/native/lui_paint/test/lui_retained_test.ml # platform/native/lui_window/lui_window.ml
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>
tiensonqin
added a commit
that referenced
this pull request
Oct 13, 2026
lui_window_linux/main.ml: keep drift-action apply from Assembly.repaint, adopt renderer-ladder gl_live ().
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
P2-3 from the ml-backend plan: stop rebuilding all GPU-side frame resources every frame. Originally stacked on #224 (merged) — this PR is rebased onto
feat/ml-backend. Supersedes the auto-closed #225.Lui_scenegains a paint revision (rev), bumped byreset; out-of-band writers callLui_scene.touch(glass/terminal plugin emitters, the fuzz testkit'sMaker.change).Lui_gpu.batch_cache: memoizes a scene's compiledbatch listplus a grow-only packed instance buffer. The key is(scene identity, rev, wide flag, frame size, text coverage params, atlas dims)— everythingbuildfolds into instances.Lui_gpu.frameinstead of rebuilding batches + packing + re-uploading every frame. On a hit — a paint that changed nothing, e.g. retained-pass clean frames — GL skipsglBufferData, Vulkan/D3D11 skip the instance memcpy into the persistent vertex buffer, and Metal reuses the OCaml-side compile+pack (its stub still stages the bytes per render — a follow-up can keep a persistentMTLBuffer).Lui_paint.resolve_in: perf(ml): P0-3 retained scene fragments — O(dirty) incremental paint #224's squash merge keptlui_retained.ml's call sites but lost the ctx wrapper whenresolve_rectgrewrect_snapunder it —feat/ml-backend@ 206eff8 does not compile without it (visible in the branch's own native-tests failure).Benchmarks
lui_gpu_testnewbenchcase — text-500 profile, 500 Glyphs ops indexing 24k glyph records, per frame:The
platform/native/perfsuite is raster-path (GPU renderers aren't in it); re-run before/after shows no regression (within noise).Test plan
lui_gpu_test: 6 new cache cases — hit/rebuild,touchcontract, scene identity, key inputs (size, text params, atlas growth re-bakes glyph UVs, wide flag), grow-only buffer.lui_gl_testrender checksum: repeated renders of the same scene draw identical pixels through the hit path (real GL context).paritydamage suites: mutations outsideresetnowtouch; CPU-composited vs GL full redraws stay pixel-parity.dune build @runtestgreen locally.Generated with Devin