diff --git a/platform/native/lui_d3d11/lui_d3d11.ml b/platform/native/lui_d3d11/lui_d3d11.ml index 579fdbb2..fbb10fd2 100644 --- a/platform/native/lui_d3d11/lui_d3d11.ml +++ b/platform/native/lui_d3d11/lui_d3d11.ml @@ -174,36 +174,12 @@ let effect_source eglsl = (* {1 Instance packing} *) -type pack = { data : f32_ba; spans : (int * int) array } - let attribute_count = Lui_gpu.instance_floats / 4 let instance_bytes = Lui_gpu.instance_floats * 4 let attribute_offset start attr = start * instance_bytes + attr * 16 -let pack_batches batches = - let total = - List.fold_left (fun n b -> n + List.length b.Lui_gpu.instances) 0 batches - in - let data = - A1.create Bigarray.float32 Bigarray.c_layout - (total * Lui_gpu.instance_floats) - in - let spans = Array.make (List.length batches) (0, 0) in - let ofs = ref 0 in - List.iteri - (fun bi b -> - let start = !ofs / Lui_gpu.instance_floats in - List.iter - (fun inst -> - Lui_gpu.pack_instance inst data ~ofs:!ofs; - ofs := !ofs + Lui_gpu.instance_floats) - b.Lui_gpu.instances; - spans.(bi) <- (start, List.length b.Lui_gpu.instances)) - batches; - { data; spans } - (* {1 Textures} *) (* What a texture tracking an atlas must do to catch up. *) @@ -234,6 +210,10 @@ type t = { mutable grab : int * int; mutable backdrop_tex : int * int; mutable effects : (fx * int) list; + (* The scene's compiled batches and packed staging; a frame whose + display list did not move reuses them and skips the instance + upload — the vertex buffer still holds what was drawn. *) + fc : Lui_gpu.batch_cache; } and texture = { mutable gen : int; mutable ver : int; mutable w : int; @@ -376,7 +356,7 @@ let read_backdrop r (bk : backdrop) = (* {1 Drawing} *) let draw_batch r s pack bi (b : Lui_gpu.batch) bound program hole = - let start, count = pack.spans.(bi) in + let start, count = pack.Lui_gpu.spans.(bi) in let sc = clamp_scissor b.Lui_gpu.scissor ~w:s.width ~h:s.height in if count > 0 && not (Lui_gpu.scissor_empty sc) then ( let want = @@ -418,13 +398,15 @@ let render r s = r.frame <- r.frame + 1; sync_atlas r 0 r.mask s.mask_atlas; sync_atlas r 1 r.color s.color_atlas; - let batches = Lui_gpu.build s in + let batches, pack, rebuilt = Lui_gpu.frame r.fc s in fit_backdrop r batches; let cr, cg, cb, ca = premul s.clear 1. in c_begin r.ctx w h cr cg cb ca; - let pack = pack_batches batches in - if A1.dim pack.data > 0 then ( - c_instances r.ctx pack.data; + if pack.Lui_gpu.floats > 0 then ( + (* A scene the cache already compiled reuses the vertex + buffer's contents as uploaded — no second upload. *) + if rebuilt then + c_instances r.ctx (A1.sub pack.data 0 pack.floats); let bound = ref (-1) and program = ref 0 and hole = ref false in List.iteri (fun i b -> draw_batch r s pack i b bound program hole) batches); @@ -450,7 +432,8 @@ let init hwnd ~w ~h = images = Hashtbl.create 16; grab = (0, 0); backdrop_tex = (0, 0); - effects = [] } + effects = []; + fc = Lui_gpu.batch_cache () } let init_offscreen ~w ~h = init 0n ~w ~h diff --git a/platform/native/lui_d3d11/lui_d3d11.mli b/platform/native/lui_d3d11/lui_d3d11.mli index 3e6ab0d1..39b9f933 100644 --- a/platform/native/lui_d3d11/lui_d3d11.mli +++ b/platform/native/lui_d3d11/lui_d3d11.mli @@ -67,15 +67,6 @@ val attribute_offset : int -> int -> int (** [attribute_offset index attribute] is the byte offset of [attribute] of instance [index]. *) -type pack = { - data : (float, Bigarray.float32_elt, Bigarray.c_layout) Bigarray.Array1.t; - spans : (int * int) array; (** instance (start, count) per batch *) -} - -val pack_batches : Lui_gpu.batch list -> pack -(** The instance data of [batches], packed as the vertex shader reads - them, plus each batch's span. *) - val clamp_scissor : Lui_gpu.scissor -> w:int -> h:int -> Lui_gpu.scissor type atlas_plan = Recreate | Upload of Lui_scene.irect list | Nothing diff --git a/platform/native/lui_gl/lui_gl.ml b/platform/native/lui_gl/lui_gl.ml index 92d195aa..1e622f35 100644 --- a/platform/native/lui_gl/lui_gl.ml +++ b/platform/native/lui_gl/lui_gl.ml @@ -1,6 +1,6 @@ (* OpenGL renderer for Lui_scene display lists, through tgls: every op of a scene is an instanced quad drawn by the one shader - (Lui_gpu.shader_source) from the instances Lui_gpu.build produces; + (Lui_gpu.shader_source) from the instances Lui_gpu.frame compiles; clips are scissor rectangles, with the innermost rounded clip computed in the shader. @@ -26,8 +26,6 @@ module A1 = Bigarray.Array1 type char_ba = (char, Bigarray.int8_unsigned_elt, Bigarray.c_layout) Bigarray.Array1.t -type f32_ba = - (float, Bigarray.float32_elt, Bigarray.c_layout) Bigarray.Array1.t external blit_bytes_to_ba : Bytes.t -> int -> char_ba -> int -> int -> unit = "lui_gl_blit_bytes_to_ba" @@ -284,10 +282,12 @@ type t = { backdrop_fb : int array; header : string; mutable effects : (fx * effect_program) list; + (* The scene's compiled batches and packed staging; a frame whose + display list did not move reuses them and skips the instance + upload — [buf] still holds what was drawn. *) + fc : Lui_gpu.batch_cache; } -type pack = { data : f32_ba; spans : (int * int) array } - (* {1 Instance packing} *) let attribute_count = Lui_gpu.instance_floats / 4 @@ -296,28 +296,6 @@ let instance_bytes = Lui_gpu.instance_floats * 4 let attribute_offset start attr = start * instance_bytes + attr * 16 -let pack_batches batches = - let total = - List.fold_left (fun n b -> n + List.length b.Lui_gpu.instances) 0 batches - in - let data = - A1.create Bigarray.float32 Bigarray.c_layout - (total * Lui_gpu.instance_floats) - in - let spans = Array.make (List.length batches) (0, 0) in - let ofs = ref 0 in - List.iteri - (fun bi b -> - let start = !ofs / Lui_gpu.instance_floats in - List.iter - (fun inst -> - Lui_gpu.pack_instance inst data ~ofs:!ofs; - ofs := !ofs + Lui_gpu.instance_floats) - b.Lui_gpu.instances; - spans.(bi) <- (start, List.length b.Lui_gpu.instances)) - batches; - { data; spans } - (* {1 Textures} *) let scratch = ref (A1.create Bigarray.char Bigarray.c_layout 0) @@ -577,7 +555,8 @@ let init () = backdrop_tex = [| texture (); texture () |]; backdrop_fb = [| 0; 0 |]; header; - effects = [] })))) + effects = []; + fc = Lui_gpu.batch_cache () })))) (* {1 Backdrop passes} *) @@ -710,7 +689,7 @@ let bind_state r s = Gl.enable Gl.scissor_test let draw_batch r s fb pack bi (b : Lui_gpu.batch) bound program hole = - let start, count = pack.spans.(bi) in + let start, count = pack.Lui_gpu.spans.(bi) in let sc = clamp_scissor b.Lui_gpu.scissor ~w:s.width ~h:s.height in if count > 0 && not (Lui_gpu.scissor_empty sc) then ( let want = @@ -796,7 +775,7 @@ let render r s = Gl.active_texture Gl.texture0; sync_atlas r r.mask s.mask_atlas ~internal:Gl.r8 ~format:Gl.red; sync_atlas r r.color s.color_atlas ~internal:Gl.rgba8 ~format:Gl.rgba; - let batches = Lui_gpu.build s in + let batches, pack, rebuilt = Lui_gpu.frame r.fc s in fit_backdrop r batches; (* The framebuffer drawn into, which the backdrop passes leave for their own. *) @@ -806,17 +785,20 @@ let render r s = let cr, cg, cb, ca = premul s.clear 1. in Gl.clear_color cr cg cb ca; Gl.clear Gl.color_buffer_bit; - let pack = pack_batches batches in - if A1.dim pack.data > 0 then ( - Gl.bind_buffer Gl.array_buffer r.buf; - Gl.buffer_data Gl.array_buffer - (Gl.bigarray_byte_size pack.data) - (Some pack.data) Gl.stream_draw; + if pack.Lui_gpu.floats > 0 then ( + (* A scene the cache already compiled reuses the instance + buffer's contents as uploaded — no second upload. *) + if rebuilt then ( + let live = A1.sub pack.data 0 pack.floats in + Gl.bind_buffer Gl.array_buffer r.buf; + Gl.buffer_data Gl.array_buffer + (Gl.bigarray_byte_size live) (Some live) Gl.stream_draw); bind_state r s; let bound = ref r.empty and program = ref r.program and hole = ref false in - List.iteri (fun bi b -> draw_batch r s fb pack bi b bound program hole) + List.iteri + (fun bi b -> draw_batch r s fb pack bi b bound program hole) batches; teardown (); gc_images r); diff --git a/platform/native/lui_gl/lui_gl.mli b/platform/native/lui_gl/lui_gl.mli index a7e14bbc..e176c614 100644 --- a/platform/native/lui_gl/lui_gl.mli +++ b/platform/native/lui_gl/lui_gl.mli @@ -59,18 +59,6 @@ val attribute_offset : int -> int -> int [attr]'s first component in the instance buffer, for a batch whose instances start at index [start]. *) -type pack = { - data : (float, Bigarray.float32_elt, Bigarray.c_layout) Bigarray.Array1.t; - spans : (int * int) array; -} -(** A frame's instances packed for one buffer upload: [data] is the - float32 buffer and [spans.(i)] is the (first index, count) of - batch [i]'s instances in it. *) - -val pack_batches : Lui_gpu.batch list -> pack -(** [pack_batches batches] packs every batch's instances into one - float32 bigarray. *) - val clamp_scissor : Lui_gpu.scissor -> w:int -> h:int -> Lui_gpu.scissor (** [clamp_scissor sc ~w ~h] is [sc] clamped to the frame's pixels. *) diff --git a/platform/native/lui_gl/test/lui_gl_test.ml b/platform/native/lui_gl/test/lui_gl_test.ml index 18f8193e..caa91bdb 100644 --- a/platform/native/lui_gl/test/lui_gl_test.ml +++ b/platform/native/lui_gl/test/lui_gl_test.ml @@ -48,7 +48,7 @@ let test_pack_batches () = let s = make_scene ~w:64 ~h:64 in s.ops <- [ fill_op () ]; let batches = Lui_gpu.build s in - let pack = Lui_gl.pack_batches batches in + let pack = Lui_gpu.pack_batches batches in Alcotest.(check int) "one batch" 1 (List.length batches); Alcotest.(check int) "one instance" 60 (Bigarray.Array1.dim pack.data); @@ -76,7 +76,7 @@ let test_pack_spans () = Lui_scene.Pop_clip; fill_op ~x:40. ~y:40. ~w:8. ~h:8. () ]; let batches = Lui_gpu.build s in - let pack = Lui_gl.pack_batches batches in + let pack = Lui_gpu.pack_batches batches in Alcotest.(check int) "batches" 3 (List.length batches); Alcotest.(check int) "spans" 3 (Array.length pack.spans); Alcotest.(check (pair int int)) "span 0" (0, 1) pack.spans.(0); diff --git a/platform/native/lui_gpu/lui_gpu.ml b/platform/native/lui_gpu/lui_gpu.ml index 371800b4..5a6c1381 100644 --- a/platform/native/lui_gpu/lui_gpu.ml +++ b/platform/native/lui_gpu/lui_gpu.ml @@ -559,4 +559,113 @@ let to_float32_array (i : instance) : float array = pack_instance i b ~ofs:0; Array.init instance_floats (fun j -> Bigarray.Array1.get b j) +(* {1 Scene batch cache} + + A renderer compiles a scene to a batch list and a packed instance + buffer every frame, even when the display list did not move — the + retained paint pass already reports that: an unchanged scene keeps + its revision. The cache memoizes both artifacts under a key of what + [build] folds into instances, so a clean frame reuses the batch + structure, the CPU staging buffer and the GPU-side contents it was + uploaded to — renderers skip the re-upload on a hit. *) + +type f32_ba = + (float, Bigarray.float32_elt, Bigarray.c_layout) Bigarray.Array1.t + +(* The buffer a renderer uploads: [data] is the grow-only staging, + [floats] the live prefix, [spans] each batch's (first, count) in + instances. *) +type pack = { data : f32_ba; floats : int; spans : (int * int) array } + +(* The inputs [build] folds into instances besides the display list: + the revision identifying it, [wide] selecting the gamut, the frame + size (the root scissor), the text coverage params, and the atlas + dimensions glyph UVs divide by. A change to any recompiles. *) +type key = + int * bool * int * int * text_params * (int * int) * (int * int) + +type batch_cache = { + mutable c_scene : t option; + mutable c_key : key option; + mutable c_batches : batch list; + mutable c_pack : pack; + mutable c_rebuilds : int; + mutable c_reuses : int; +} + +let batch_cache () = + { c_scene = None; c_key = None; c_batches = []; + c_pack = + { data = Bigarray.(Array1.create float32 c_layout 0); + floats = 0; spans = [||] }; + c_rebuilds = 0; c_reuses = 0 } + +let stat_rebuilds c = c.c_rebuilds +let stat_reuses c = c.c_reuses + +(* Snapshot the key; the text params are copied so a later in-place + edit cannot mutate the stored key along with them. *) +let key_of ~wide (s : t) : key = + ( s.rev, wide, s.width, s.height, + { gamma_ratios = s.text.gamma_ratios; + contrast = s.text.contrast; + subpixel_contrast = s.text.subpixel_contrast }, + (s.mask_atlas.w, s.mask_atlas.h), + (s.color_atlas.w, s.color_atlas.h) ) + +let pack_into (batches : batch list) (data : f32_ba) = + let spans = Array.make (List.length batches) (0, 0) in + let ofs = ref 0 in + List.iteri + (fun bi b -> + let start = !ofs / instance_floats in + List.iter + (fun inst -> + pack_instance inst data ~ofs:!ofs; + ofs := !ofs + instance_floats) + b.instances; + spans.(bi) <- (start, List.length b.instances)) + batches; + spans + +(* The one-shot packer: an exact-size buffer, for callers without a + cache to grow into (tests). *) +let pack_batches (batches : batch list) : pack = + let total = + List.fold_left (fun n b -> n + List.length b.instances) 0 batches + in + let floats = total * instance_floats in + let data = Bigarray.(Array1.create float32 c_layout floats) in + { data; floats; spans = pack_into batches data } + +let rebuild c ~wide s = + let batches = build ~wide s in + let total = + List.fold_left (fun n b -> n + List.length b.instances) 0 batches + in + let floats = total * instance_floats in + let data = + if Bigarray.Array1.dim c.c_pack.data >= floats then c.c_pack.data + else + Bigarray.(Array1.create float32 c_layout (floats + floats / 2)) + in + let spans = pack_into batches data in + c.c_scene <- Some s; + c.c_key <- Some (key_of ~wide s); + c.c_batches <- batches; + c.c_pack <- { data; floats; spans }; + c.c_rebuilds <- c.c_rebuilds + 1 + +(* The scene's compiled batches and packed buffer, rebuilt only when + the key moved; [rebuilt] tells the renderer whether the GPU-side + buffer contents are stale. *) +let frame ?(wide = false) c (s : t) : batch list * pack * bool = + match (c.c_scene, c.c_key) with + | Some sc, Some k when sc == s && k = key_of ~wide s -> + c.c_reuses <- c.c_reuses + 1; + (c.c_batches, c.c_pack, false) + | _ -> + rebuild c ~wide s; + (c.c_batches, c.c_pack, true) + let shader_source = Shader_source.source diff --git a/platform/native/lui_gpu/lui_gpu.mli b/platform/native/lui_gpu/lui_gpu.mli index 639dddc0..f0c60a6b 100644 --- a/platform/native/lui_gpu/lui_gpu.mli +++ b/platform/native/lui_gpu/lui_gpu.mli @@ -139,5 +139,42 @@ val to_float32_array : instance -> float array params, clip2, clip2_radii, clip3, clip3_radii — rounded to float32. *) +(** {1 Scene batch cache} *) + +type f32_ba = + (float, Bigarray.float32_elt, Bigarray.c_layout) Bigarray.Array1.t + +(** The buffer a renderer uploads: [data] is the grow-only staging, + [floats] the live prefix, [spans] each batch's (first, count) + instance offset in that prefix. *) +type pack = { data : f32_ba; floats : int; spans : (int * int) array } + +val pack_batches : batch list -> pack +(** [pack_batches batches] packs every batch's instances into one + exact-size float32 buffer — the one-shot packer for callers + without a cache to grow into (tests). *) + +type batch_cache +(** A renderer's memoized batches and instance buffer for one scene. + The key is the scene's revision plus the other inputs {!build} + folds into instances: the wide-gamut flag, frame size, text + coverage parameters, and atlas dimensions. A paint that changed + nothing — the retained pass's clean frames — reuses the batches, + the staging buffer, and the GPU-side contents it was uploaded to, + so the renderer may skip the re-upload entirely. *) + +val batch_cache : unit -> batch_cache + +val frame : + ?wide:bool -> batch_cache -> Lui_scene.t -> batch list * pack * bool +(** The scene's compiled batches and packed instance buffer, rebuilt + only when the cache key moved. The flag tells whether this call + rebuilt — on [false] the returned [pack]'s GPU-side contents are + still current and the renderer skips re-uploading them. *) + +val stat_rebuilds : batch_cache -> int +val stat_reuses : batch_cache -> int +(** Cache accounting for tests and benchmarks. *) + val shader_source : string (** The shader of shader.glsl, verbatim, as data. *) diff --git a/platform/native/lui_gpu/test/lui_gpu_test.ml b/platform/native/lui_gpu/test/lui_gpu_test.ml index 81ca3bdb..9f4ce52e 100644 --- a/platform/native/lui_gpu/test/lui_gpu_test.ml +++ b/platform/native/lui_gpu/test/lui_gpu_test.ml @@ -437,6 +437,165 @@ let test_shader_source () = (String.length G.shader_source > 1000 && String.sub G.shader_source 0 2 = "//") +(* ---------- scene batch cache ---------- *) + +let ops_scene ?(w = 200) ?(h = 100) ops = + let s = scene ~w ~h () in + s.ops <- ops; + s + +let frame3 c ?(wide = false) s = G.frame ~wide c s + +let test_cache_hit () = + let s = ops_scene [ fill () ] in + let c = G.batch_cache () in + let b1, p1, r1 = frame3 c s in + check bool "first builds" true r1; + let b2, p2, r2 = frame3 c s in + check bool "second reuses" false r2; + check bool "same batches" true (b1 == b2); + check bool "same buffer" true (p1.data == p2.data); + check int "rebuilds" 1 (G.stat_rebuilds c); + check int "reuses" 1 (G.stat_reuses c) + +let test_cache_touch () = + let s = ops_scene [ fill () ] in + let c = G.batch_cache () in + let b1, _, _ = frame3 c s in + s.ops <- [ fill ~r:(rect 5. 5. 20. 20.) () ]; + (* Writers that mutate outside reset must touch: the cache only + rebuilds because the revision moved. *) + Lui_scene.touch s; + let b2, _, r = frame3 c s in + check bool "touch rebuilds" true r; + check bool "fresh batches" true (b1 != b2); + check int "rebuilds" 2 (G.stat_rebuilds c) + +(* A scene object the cache has never compiled always rebuilds, even + one byte-identical to the compiled scene — the key pairs identity + with revision. *) +let test_cache_identity () = + let ops () = [ fill () ] in + let s1 = ops_scene (ops ()) and s2 = ops_scene (ops ()) in + let c = G.batch_cache () in + ignore (frame3 c s1); + let _, _, r2 = frame3 c s2 in + check bool "another scene rebuilds" true r2; + let _, _, r3 = frame3 c s2 in + check bool "then reuses" false r3; + let _, _, r4 = frame3 c s1 in + check bool "back to first rebuilds" true r4 + +(* The inputs build folds into instances that the revision does not + capture — frame size, text coverage, atlas size, the gamut flag — + are in the key: each change recompiles. *) +let test_cache_key_inputs () = + let s = scene ~w:200 ~h:100 () in + s.glyphs <- [ glyph () ]; + s.ops <- + [ Glyphs + { gstart = 0; gend = 1; gpaint = Solid; gcolor = red; + gcolor2 = red; ggradient = (0., 0., 0., 0.); gwide2 = 0; + gopacity = 0. } ]; + let c = G.batch_cache () in + let _, _, r0 = frame3 c s in + check bool "build" true r0; + s.width <- 300; + let _, _, r1 = frame3 c s in + check bool "width rebuilds" true r1; + let _, _, r1b = frame3 c s in + check bool "width then reuses" false r1b; + s.text <- { s.text with contrast = 1.5 }; + let _, _, r2 = frame3 c s in + check bool "text params rebuild" true r2; + ignore (Atlas.grow s.mask_atlas); + let _, p, r3 = frame3 c s in + check bool "atlas growth rebuilds" true r3; + (* Glyph UVs are baked over the doubled atlas: 16/128 = 0.125. + [uv] is the instance's eighth float4, floats 28-31. *) + check (float 1e-3) "uv re-based" 0.125 p.data.{28}; + let _, _, r4 = frame3 ~wide:true c s in + check bool "wide flag rebuilds" true r4; + let _, _, r5 = frame3 ~wide:true c s in + check bool "wide then reuses" false r5 + +(* The staging buffer is grow-only: a smaller later scene reuses it + with a shorter live prefix. *) +let test_cache_buffer_growth () = + let s = ops_scene [ fill (); fill (); fill () ] in + let c = G.batch_cache () in + let _, p1, _ = frame3 c s in + check int "three instances" (3 * G.instance_floats) p1.floats; + let cap = Bigarray.Array1.dim p1.data in + s.ops <- [ fill () ]; + Lui_scene.touch s; + let _, p2, _ = frame3 c s in + check int "one instance" G.instance_floats p2.floats; + check bool "same buffer" true (p1.data == p2.data); + check bool "capacity kept" true (Bigarray.Array1.dim p2.data = cap) + +(* ---------- cache benchmark ---------- *) + +(* What the GPU marshalling pays per frame on a glyph-heavy scene, the + text-500 profile: 500 Glyphs ops indexing 24k glyph records. + Reports uncached build+pack against cached frames, hit and miss. *) +let bench_cache () = + let n_ops, n_glyphs = 500, 48 in + let s = scene ~w:1280 ~h:800 () in + s.glyphs <- + List.init (n_ops * n_glyphs) (fun i -> + glyph ~x:(Float.of_int (i mod 1000)) + ~y:(Float.of_int ((i / 50) mod 700)) ()); + s.ops <- + List.init n_ops (fun i -> + Glyphs + { gstart = i * n_glyphs; gend = (i + 1) * n_glyphs; + gpaint = Solid; gcolor = red; gcolor2 = red; + ggradient = (0., 0., 0., 0.); gwide2 = 0; gopacity = 0. }); + let insts = + List.fold_left + (fun n (b : G.batch) -> n + List.length b.instances) + 0 (G.build s) + in + let iters = 200 in + let timed f = + let t0 = Sys.time () in + ignore (f ()); + (Sys.time () -. t0) *. 1000. /. Float.of_int iters + in + let uncached = + timed (fun () -> + for _ = 1 to iters do + ignore (G.pack_batches (G.build s)) + done) + in + let c = G.batch_cache () in + ignore (frame3 c s); + let hit = + timed (fun () -> + for _ = 1 to iters do + ignore (frame3 c s) + done) + in + let c2 = G.batch_cache () in + let miss = + timed (fun () -> + for _ = 1 to iters do + (* Every frame a repaint: touch, compile, pack. *) + Lui_scene.touch s; + ignore (frame3 c2 s) + done) + in + Printf.printf + "\n gpu-frame cache, %d instances/frame\n\ + \ build+pack uncached : %8.3f ms\n\ + \ cache hit : %8.4f ms\n\ + \ cache miss : %8.3f ms\n\ + %!" + insts uncached hit miss; + check int "one hit per frame" iters (G.stat_reuses c); + check int "misses rebuild" iters (G.stat_rebuilds c2) + let () = run "lui_gpu" [ ( "build", @@ -451,4 +610,11 @@ let () = test_case "wide gamut" `Quick test_wide ] ); ( "packing", [ test_case "to_float32_array" `Quick test_float32_packing; - test_case "shader source" `Quick test_shader_source ] ) ] + test_case "shader source" `Quick test_shader_source ] ); + ( "batch cache", + [ test_case "hit reuses" `Quick test_cache_hit; + test_case "touch rebuilds" `Quick test_cache_touch; + test_case "scene identity" `Quick test_cache_identity; + test_case "key inputs" `Quick test_cache_key_inputs; + test_case "buffer growth" `Quick test_cache_buffer_growth; + test_case "bench" `Quick bench_cache ] ) ] diff --git a/platform/native/lui_metal/lui_metal.ml b/platform/native/lui_metal/lui_metal.ml index 524c258d..6755c650 100644 --- a/platform/native/lui_metal/lui_metal.ml +++ b/platform/native/lui_metal/lui_metal.ml @@ -50,6 +50,11 @@ type t = { images : (int, mtex) Hashtbl.t; mutable frame : int; mutable effects : (fx * effect_prog) list; + (* The scene's compiled batches and packed staging; a frame whose + display list did not move reuses them — the stub still copies + the staging each render, so the hit saves the OCaml compile + and pack. *) + fc : Lui_gpu.batch_cache; } (* An atlas or image's texture: gen/version track the source's, so uploads are incremental; last_frame lets render release textures @@ -116,30 +121,6 @@ let clamp_scissor (s : Lui_gpu.scissor) ~w ~h = (* A frame's instances packed for one buffer upload: data is the whole array, spans the (first, count) instance of each batch. *) -type pack = { data : f32_ba; spans : (int * int) array } - -let pack_batches (batches : Lui_gpu.batch list) = - let spans = - Array.of_list - (List.map (fun (b : Lui_gpu.batch) -> (0, List.length b.instances)) batches) - in - let total = Array.fold_left (fun n (_, c) -> n + c) 0 spans in - let data = - A1.create Bigarray.float32 Bigarray.c_layout (total * Lui_gpu.instance_floats) - in - let ofs = ref 0 in - List.iteri - (fun i (b : Lui_gpu.batch) -> - let start = !ofs / Lui_gpu.instance_floats in - List.iter - (fun inst -> - Lui_gpu.pack_instance inst data ~ofs:!ofs; - ofs := !ofs + Lui_gpu.instance_floats) - b.instances; - spans.(i) <- (start, !ofs / Lui_gpu.instance_floats - start)) - batches; - { data; spans } - (* Atlas texture sync: recreate when the atlas grew, upload the changed rects when the same atlas was drawn on, skip the upload when it is unchanged. *) @@ -297,7 +278,8 @@ let renderer ctx ~dual ~lib ~pipe_main ~pipe_hole ~pipe_down ~pipe_blur = { ctx; dual; lib; pipe_main; pipe_hole; pipe_down; pipe_blur; empty = make_empty ctx; mask = new_mtex (); color = new_mtex (); - images = Hashtbl.create 17; frame = 0; effects = [] } + images = Hashtbl.create 17; frame = 0; effects = []; + fc = Lui_gpu.batch_cache () } (* With dual-source blending, subpixel glyphs blend each destination channel by the coverage of the matching source channel; without it @@ -380,8 +362,7 @@ let render (t : t) (s : Lui_scene.t) : unit = t.frame <- t.frame + 1; sync_atlas t t.mask s.mask_atlas ~fmt:0; sync_atlas t t.color s.color_atlas ~fmt:1; - let batches = Lui_gpu.build s in - let pack = pack_batches batches in + let batches, pack, _rebuilt = Lui_gpu.frame t.fc s in (* Drop the image textures unseen for 64 frames. *) Hashtbl.filter_map_inplace (fun _i (m : mtex) -> @@ -394,7 +375,7 @@ let render (t : t) (s : Lui_scene.t) : unit = Array.of_list (List.mapi (fun bi (b : Lui_gpu.batch) -> - let first, count = pack.spans.(bi) in + let first, count = pack.Lui_gpu.spans.(bi) in let sc = clamp_scissor b.scissor ~w ~h in let pipe = match b.fx with @@ -422,7 +403,7 @@ let render (t : t) (s : Lui_scene.t) : unit = let mask_tex = match t.mask.tex with Some x -> x | None -> t.empty in let color_tex = match t.color.tex with Some x -> x | None -> t.empty in metal_render t.ctx - ( w, h, clear, pack.data, draws, + ( w, h, clear, A1.sub pack.data 0 pack.floats, draws, mask_tex, color_tex, t.empty, t.pipe_down, t.pipe_blur ) end diff --git a/platform/native/lui_metal/lui_metal.mli b/platform/native/lui_metal/lui_metal.mli index c785559c..c794d2c4 100644 --- a/platform/native/lui_metal/lui_metal.mli +++ b/platform/native/lui_metal/lui_metal.mli @@ -65,18 +65,6 @@ val attribute_offset : int -> int -> int [attr]'s first component in the instance buffer, for a batch whose instances start at index [start]. *) -type pack = { - data : (float, Bigarray.float32_elt, Bigarray.c_layout) Bigarray.Array1.t; - spans : (int * int) array; -} -(** A frame's instances packed for one buffer upload: [data] is the - float32 buffer and [spans.(i)] is the (first index, count) of - batch [i]'s instances in it. *) - -val pack_batches : Lui_gpu.batch list -> pack -(** [pack_batches batches] packs every batch's instances into one - float32 bigarray. *) - val clamp_scissor : Lui_gpu.scissor -> w:int -> h:int -> Lui_gpu.scissor (** [clamp_scissor sc ~w ~h] is [sc] clamped to the frame's pixels. *) diff --git a/platform/native/lui_paint/lui_paint.ml b/platform/native/lui_paint/lui_paint.ml index d679a49a..e51dfe14 100644 --- a/platform/native/lui_paint/lui_paint.ml +++ b/platform/native/lui_paint/lui_paint.ml @@ -2313,8 +2313,8 @@ let resolve_rect ~scale ~viewport store id kind p parent_r = in rect_snap r -(* The ctx-taking form of [resolve_rect], used by the retained paint - pass (Lui_retained). *) +(* The ctx-taking form {!Lui_retained} calls; the labeled-args one + above is the shared entry point. *) let resolve_in ctx = resolve_rect ~scale:ctx.scale ~viewport:ctx.viewport let zindex store id = diff --git a/platform/native/lui_plugin_glass/lui_plugin_glass.ml b/platform/native/lui_plugin_glass/lui_plugin_glass.ml index 96a91d3b..61f1e445 100644 --- a/platform/native/lui_plugin_glass/lui_plugin_glass.ml +++ b/platform/native/lui_plugin_glass/lui_plugin_glass.ml @@ -552,7 +552,8 @@ let emit s ee ~eblur eparams ?(radii = (0., 0., 0., 0.)) s.ops @ [ Effect { edrect = rc; edradii = radii; edcontinuous = continuous; - edindex = List.length s.effects - 1; edopacity = opacity } ] + edindex = List.length s.effects - 1; edopacity = opacity } ]; + Lui_scene.touch s let emit_fill s ?(radii = (0., 0., 0., 0.)) ?(continuous = false) ?(paint_ = Solid) ?(c2 = transparent) ?(g = (0., 0., 0., 0.)) rc c1 = @@ -562,7 +563,8 @@ let emit_fill s ?(radii = (0., 0., 0., 0.)) ?(continuous = false) { frect = rc; fradii = radii; fcontinuous = continuous; fcolor = c1; fpaint = paint_; fcolor2 = c2; fgradient = g; fborder = (0., 0., 0., 0.); fborder_color = transparent; - fdashed = false; fwide = 0; fopacity = 0. } ] + fdashed = false; fwide = 0; fopacity = 0. } ]; + Lui_scene.touch s (* A pane of glass over rect, rounded by radii: what was painted under it shows through, blurred and lensed and rim-lit per the material. diff --git a/platform/native/lui_plugin_terminal/terminal_render.ml b/platform/native/lui_plugin_terminal/terminal_render.ml index 7208459b..6f95d780 100644 --- a/platform/native/lui_plugin_terminal/terminal_render.ml +++ b/platform/native/lui_plugin_terminal/terminal_render.ml @@ -457,4 +457,5 @@ let render ?(blink_on = true) t grid scene ~w ~h = render_scrollbar t grid ~w ~h ops; (* scene.ops is reversed paint order; !ops already holds ours newest first, so a plain append keeps the invariant *) - scene.Lui_scene.ops <- !ops @ scene.Lui_scene.ops + scene.Lui_scene.ops <- !ops @ scene.Lui_scene.ops; + Lui_scene.touch scene diff --git a/platform/native/lui_raster/test/testkit.ml b/platform/native/lui_raster/test/testkit.ml index 47f7c195..a54d9d86 100644 --- a/platform/native/lui_raster/test/testkit.ml +++ b/platform/native/lui_raster/test/testkit.ml @@ -470,6 +470,9 @@ module Maker = struct done; image_changed m.img | _ -> ()); + (* The mutations above bypass reset — the renderer-facing + revision must move with them. *) + touch s; s end diff --git a/platform/native/lui_scene/lui_scene.ml b/platform/native/lui_scene/lui_scene.ml index 17e14dee..cb52fe38 100644 --- a/platform/native/lui_scene/lui_scene.ml +++ b/platform/native/lui_scene/lui_scene.ml @@ -504,7 +504,10 @@ and effect_draw = { edrect : rect; edradii : float * float * float * float; edco and hole_op = { hrect : rect; hradii : float * float * float * float; hcontinuous : bool; hopacity : float } -(* Scene: one frame's display list. *) +(* Scene: one frame's display list. [rev] identifies its contents: + {!reset} bumps it, and a writer mutating the buffers outside a + reset–seal window calls {!touch}, so a renderer can tell a scene it + has already compiled from one whose display list moved. *) type t = { mutable width : int; mutable height : int; @@ -515,6 +518,7 @@ type t = { mutable effects : effect_op list; mutable wide : wide_colors list; mutable text : text_params; + mutable rev : int; mask_atlas : Atlas.t; color_atlas : Atlas.t; } @@ -522,10 +526,13 @@ type t = { let create ~mask_atlas ~color_atlas = { width = 0; height = 0; scale = 1.; clear = color 0 0 0 0; ops = []; glyphs = []; effects = []; wide = []; - text = default_text_params; mask_atlas; color_atlas } + text = default_text_params; rev = 0; mask_atlas; color_atlas } + +let touch s = s.rev <- s.rev + 1 let reset s ~width ~height ~scale ~clear = s.width <- width; s.height <- height; s.scale <- scale; s.clear <- clear; - s.ops <- []; s.glyphs <- []; s.effects <- []; s.wide <- [] + s.ops <- []; s.glyphs <- []; s.effects <- []; s.wide <- []; + s.rev <- s.rev + 1 let add_wide s wc = s.wide <- wc :: s.wide; List.length s.wide diff --git a/platform/native/lui_vulkan/lui_vulkan.ml b/platform/native/lui_vulkan/lui_vulkan.ml index de9dbb44..b228d11d 100644 --- a/platform/native/lui_vulkan/lui_vulkan.ml +++ b/platform/native/lui_vulkan/lui_vulkan.ml @@ -84,10 +84,12 @@ type t = { backdrop_tex : texture array; (* two: the blur passes ping-pong *) mutable effects : ((fx * bool) * int) list; (* effect pipelines keyed by the effect and the hole blend *) + (* The scene's compiled batches and packed staging; a frame whose + display list did not move reuses them and skips the instance + upload — the vertex buffer still holds what was drawn. *) + fc : Lui_gpu.batch_cache; } -type pack = { data : f32_ba; spans : (int * int) array } - (* {1 Instance packing} *) let attribute_count = Lui_gpu.instance_floats / 4 @@ -96,28 +98,6 @@ let instance_bytes = Lui_gpu.instance_floats * 4 let attribute_offset start attr = start * instance_bytes + attr * 16 -let pack_batches batches = - let total = - List.fold_left (fun n b -> n + List.length b.Lui_gpu.instances) 0 batches - in - let data = - A1.create Bigarray.float32 Bigarray.c_layout - (total * Lui_gpu.instance_floats) - in - let spans = Array.make (List.length batches) (0, 0) in - let ofs = ref 0 in - List.iteri - (fun bi b -> - let start = !ofs / Lui_gpu.instance_floats in - List.iter - (fun inst -> - Lui_gpu.pack_instance inst data ~ofs:!ofs; - ofs := !ofs + Lui_gpu.instance_floats) - b.Lui_gpu.instances; - spans.(bi) <- (start, List.length b.Lui_gpu.instances)) - batches; - { data; spans } - let clamp_scissor (sc : Lui_gpu.scissor) ~w ~h : Lui_gpu.scissor = { left = max sc.left 0; top = max sc.top 0; @@ -349,7 +329,7 @@ let fit_backdrop r batches = (* Draw batch bi. ds_sel: 0 uses ds_id's own set (an image's), 1 the backdrop set. *) let draw_batch r s pack bi (b : Lui_gpu.batch) = - let start, count = pack.spans.(bi) in + let start, count = pack.Lui_gpu.spans.(bi) in let sc = clamp_scissor b.Lui_gpu.scissor ~w:s.width ~h:s.height in if count > 0 && not (Lui_gpu.scissor_empty sc) then ( let hole = b.Lui_gpu.hole in @@ -399,7 +379,7 @@ let render r s = c_begin r.ctx; sync_atlas r r.mask s.mask_atlas ~fmt:tex_r8 ~slot:0; sync_atlas r r.color s.color_atlas ~fmt:tex_rgba8 ~slot:1; - let batches = Lui_gpu.build s in + let batches, pack, rebuilt = Lui_gpu.frame r.fc s in (* Uploads record into the frame's command buffer, so every image the batches draw is uploaded before the render pass begins. *) List.iter @@ -408,8 +388,10 @@ let render r s = | None -> ()) batches; fit_backdrop r batches; - let pack = pack_batches batches in - if A1.dim pack.data > 0 then c_instances r.ctx pack.data; + (* A scene the cache already compiled reuses the vertex buffer's + contents as uploaded — no second upload. *) + if rebuilt && pack.Lui_gpu.floats > 0 then + c_instances r.ctx (A1.sub pack.data 0 pack.floats); let cr, cg, cb, ca = premul s.clear 1. in c_frame r.ctx cr cg cb ca; List.iteri (fun bi b -> draw_batch r s pack bi b) batches; @@ -470,7 +452,8 @@ let init ~w ~h = frame = 0; grab = texture (); backdrop_tex = [| texture (); texture () |]; - effects = [] } + effects = []; + fc = Lui_gpu.batch_cache () } with | Failure m -> c_destroy ctx; diff --git a/platform/native/lui_vulkan/lui_vulkan.mli b/platform/native/lui_vulkan/lui_vulkan.mli index 045ff157..d113e8ff 100644 --- a/platform/native/lui_vulkan/lui_vulkan.mli +++ b/platform/native/lui_vulkan/lui_vulkan.mli @@ -76,18 +76,6 @@ val attribute_offset : int -> int -> int [attr]'s first component in the instance buffer, for a batch whose instances start at index [start]. *) -type pack = { - data : (float, Bigarray.float32_elt, Bigarray.c_layout) Bigarray.Array1.t; - spans : (int * int) array; -} -(** A frame's instances packed for one buffer upload: [data] is the - float32 buffer and [spans.(i)] is the (first index, count) of - batch [i]'s instances in it. *) - -val pack_batches : Lui_gpu.batch list -> pack -(** [pack_batches batches] packs every batch's instances into one - float32 bigarray. *) - val clamp_scissor : Lui_gpu.scissor -> w:int -> h:int -> Lui_gpu.scissor (** [clamp_scissor sc ~w ~h] is [sc] clamped to the frame's pixels. *) diff --git a/platform/native/parity/test/parity_test.ml b/platform/native/parity/test/parity_test.ml index 3a9f4a1a..ad421e28 100644 --- a/platform/native/parity/test/parity_test.ml +++ b/platform/native/parity/test/parity_test.ml @@ -306,11 +306,12 @@ let test_damage_backdrop () = (fun () -> for step = 0 to 1 do (* step 1 changes the fill inside the backdrop *) - if step = 1 then + if step = 1 then ( s.ops <- [ Tk.ifill (rect 0. 0. 96. 64.) (color 37 99 235 255); Tk.ifill (rect 20. 10. 40. 30.) (color 220 38 38 255); Tk.ieffect ~radii:(Tk.radii 6.) (rect 12. 8. 60. 40.) 0 ]; + Lui_scene.touch s); ignore (Lui_raster.Renderer.render dr s); let cpu = (Lui_raster.Renderer.image dr).Lui_raster.Image.pix