diff --git a/.github/README.md b/.github/README.md index 8581e41..afe5704 100644 --- a/.github/README.md +++ b/.github/README.md @@ -244,6 +244,20 @@ Measured headless (Luau CLI, Studio-like native code generation), demo window op Most of the remaining cost is widget logic (your UI code and Dear ImGui's per-widget work), which runs every frame by design. Use `ImGuiListClipper` for long lists. +Windows that rarely change can opt in to caching: `ImGui.SetNextWindowCached(key)` keeps last frame's drawing and makes `Begin` return `visible = false`, so your code for that window doesn't run. It refreshes when `key` changes, while the window is hovered, focused or active (plus one frame after), and when it appears, moves, resizes, scrolls, collapses or is docked. An idle 200-widget window goes from ~2.2 ms to ~0.15 ms per frame. + +```lua +local stats = { version = 0, kills = 0 } +local function StatsWindow() + ImGui.SetNextWindowCached(stats.version) -- bump stats.version whenever stats change + local _, visible = ImGui.Begin("Stats") + if visible then -- false while the cached contents are reused + ImGui.Text("Kills: %d", stats.kills) + end + ImGui.End() +end +``` + ### Testing `tools/test/run.py` runs any LocalScript headless against a mocked Roblox (`tools/test/roblox_mock.luau`): diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ff1a53..138f718 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,6 +47,8 @@ jobs: luajit -bl dist/love-nodemo/imgui_love.lua > /dev/null python3 tools/test/run.py --main tools/test/scripts/t_readme.luau --frames 30 --quiet --roblox --timeout 300 | tee /tmp/r.txt grep -q "README OK" /tmp/r.txt + python3 tools/test/run.py --main tools/test/scripts/t_cached_window.luau --frames 130 --quiet --roblox --timeout 300 | tee /tmp/c.txt + grep -q "CACHED OK" /tmp/c.txt python3 tools/bundle.py --target love >/dev/null luajit tools/test/love/t_drawhash.lua 60 Widgets,Basic luajit tools/test/love/t_examples.lua diff --git a/README.md b/README.md index 4759422..532a15f 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,20 @@ Measured headless (Luau CLI, Studio-like native code generation), demo window op Most of the remaining cost is widget logic (your UI code and Dear ImGui's per-widget work), which runs every frame by design. Use `ImGuiListClipper` for long lists. +Windows that rarely change can opt in to caching: `ImGui.SetNextWindowCached(key)` keeps last frame's drawing and makes `Begin` return `visible = false`, so your code for that window doesn't run. It refreshes when `key` changes, while the window is hovered, focused or active (plus one frame after), and when it appears, moves, resizes, scrolls, collapses or is docked. An idle 200-widget window goes from ~2.2 ms to ~0.15 ms per frame. + +```lua +local stats = { version = 0, kills = 0 } +local function StatsWindow() + ImGui.SetNextWindowCached(stats.version) -- bump stats.version whenever stats change + local _, visible = ImGui.Begin("Stats") + if visible then -- false while the cached contents are reused + ImGui.Text("Kills: %d", stats.kills) + end + ImGui.End() +end +``` + ### Testing `tools/test/run.py` runs any LocalScript headless against a mocked Roblox (`tools/test/roblox_mock.luau`): diff --git a/build/ImGui.linemap.json b/build/ImGui.linemap.json index 6a77b25..170517f 100644 --- a/build/ImGui.linemap.json +++ b/build/ImGui.linemap.json @@ -1 +1 @@ -[[12, "imgui.lua"], [18472, "imgui_demo.lua"], [25757, "imgui_draw.lua"], [30995, "imgui_h.lua"], [33193, "imgui_impl_roblox.lua"], [34525, "imgui_impl_roblox_fonts.lua"], [40928, "imgui_internal.lua"], [44100, "imgui_tables.lua"], [48447, "imgui_widgets.lua"], [58202, "imstb_rectpack.lua"], [58648, "imstb_textedit.lua"], [59889, "imstb_truetype.lua"], [62357, "imstd_minstdio.lua"]] \ No newline at end of file +[[12, "imgui.lua"], [18510, "imgui_demo.lua"], [25795, "imgui_draw.lua"], [31033, "imgui_h.lua"], [33231, "imgui_impl_roblox.lua"], [34563, "imgui_impl_roblox_fonts.lua"], [40966, "imgui_internal.lua"], [44142, "imgui_tables.lua"], [48489, "imgui_widgets.lua"], [58244, "imstb_rectpack.lua"], [58690, "imstb_textedit.lua"], [59931, "imstb_truetype.lua"], [62399, "imstd_minstdio.lua"]] \ No newline at end of file diff --git a/build/ImGui.luau b/build/ImGui.luau index 402d16b..b791df6 100644 --- a/build/ImGui.luau +++ b/build/ImGui.luau @@ -8857,6 +8857,26 @@ function ImGui.UpdateWindowSkipRefresh(window) end if bit32.band(g.NextWindowData.RefreshFlagsVal, ImGuiWindowRefreshFlags.TryToAvoidRefresh) ~= 0 then + -- [Port] SetNextWindowCached(): refresh on any change since the last refresh (and one frame after being busy) + if g.NextWindowData.CacheKeySet then + local function related(w) return w ~= nil and (w.RootWindow == window.RootWindow or ImGui.IsWindowWithinBeginStackOf(w.RootWindow, window)) end + local busy = window.Appearing or window.Hidden or window.DockIsActive or related(g.HoveredWindow) or related(g.NavWindow) or related(g.ActiveIdWindow) or (g.MovingWindow ~= nil and related(g.MovingWindow)) + local c, key, ds, tex = window.CacheSnap, g.NextWindowData.CacheKeyVal, g.IO.DisplaySize, g.IO.Fonts.TexData + if busy or c == nil or c.Busy or c.Key ~= key or c.Tex ~= tex or c.Collapsed ~= window.Collapsed + or c.PosX ~= window.Pos.x or c.PosY ~= window.Pos.y or c.SizeX ~= window.SizeFull.x or c.SizeY ~= window.SizeFull.y + or c.ScrollX ~= window.Scroll.x or c.ScrollY ~= window.Scroll.y or window.ScrollTarget.x < FLT_MAX or window.ScrollTarget.y < FLT_MAX + or c.DisplayX ~= ds.x or c.DisplayY ~= ds.y then + c = c or {} + window.CacheSnap = c + c.Busy, c.Key, c.Tex, c.Collapsed = busy, key, tex, window.Collapsed + c.PosX, c.PosY, c.SizeX, c.SizeY = window.Pos.x, window.Pos.y, window.SizeFull.x, window.SizeFull.y + c.ScrollX, c.ScrollY, c.DisplayX, c.DisplayY = window.Scroll.x, window.Scroll.y, ds.x, ds.y + return + end + -- skip: widgets early-out on SkipItems and Begin() returns visible=false; End() restores it + window.CacheSkipItemsBackup = window.SkipItems + window.SkipItems = true + end -- FIXME-IDLE: Tests for e.g. mouse clicks or keyboard while focused. if window.Appearing then -- If currently appearing return @@ -9488,6 +9508,20 @@ function ImGui.SetNextWindowRefreshPolicy(flags) local g = GImGui g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasRefreshPolicy) g.NextWindowData.RefreshFlagsVal = flags + g.NextWindowData.CacheKeySet = false +end + +-- [Port] Opt-in cached window (built on the refresh policy above). While nothing that could change the window +-- happened, Begin() keeps last frame's draw list and returns visible=false, so `if visible then ... end` skips your code. +-- It refreshes when `key` changes (pass e.g. a version number of the data shown), while the window (or a popup/child +-- of it) is hovered, focused or active and for one frame after, and when it appears, moves, resizes, scrolls, +-- collapses, is docked, or the display size or font atlas texture changes. +-- Anything else that changes the contents (style, fonts, time) must be part of `key`. +function ImGui.SetNextWindowCached(key) + local g = GImGui + ImGui.SetNextWindowRefreshPolicy(bit32.bor(ImGuiWindowRefreshFlags.TryToAvoidRefresh, ImGuiWindowRefreshFlags.RefreshOnHover, ImGuiWindowRefreshFlags.RefreshOnFocus)) + g.NextWindowData.CacheKeySet = true + g.NextWindowData.CacheKeyVal = key end function ImGui.GetWindowDpiScale() @@ -10690,6 +10724,10 @@ function ImGui.End() IM_ASSERT(window.DrawList == nil) window.DrawList = window.DrawListInst end + if window.CacheSkipItemsBackup ~= nil then -- [Port] SetNextWindowCached() + window.SkipItems = window.CacheSkipItemsBackup + window.CacheSkipItemsBackup = nil + end -- Stop logging if g.LogWindow == window then -- FIXME: add more options for scope of logging @@ -42163,7 +42201,9 @@ function ImGuiNextWindowData() SizeCallbackUserData = nil, BgAlphaVal = nil, MenuBarOffsetMinVal = ImVec2(), - RefreshFlagsVal = nil + RefreshFlagsVal = nil, + CacheKeySet = false, -- [Port] SetNextWindowCached() + CacheKeyVal = nil }, IMGUI_NEXT_WINDOW_DATA) end @@ -43335,6 +43375,8 @@ function ImGuiWindow(ctx, name) SkipItems = false, SkipRefresh = false, + CacheSnap = nil, -- [Port] SetNextWindowCached(): state at the last refresh + CacheSkipItemsBackup = nil, Appearing = false, diff --git a/build/ImGuiTyped.luau b/build/ImGuiTyped.luau index f22324f..8f85466 100644 --- a/build/ImGuiTyped.luau +++ b/build/ImGuiTyped.luau @@ -516,7 +516,7 @@ export type ImGuiAPI = { NavUpdateWindowingTarget: (...any) -> ...any, NewFrame: () -> (), NewLine: () -> (), - NewTextBuffer: (...any) -> ...any, + NewTextBuffer: (str: string, size: number?) -> {number}, NextColumn: () -> (), OpenPopup: (...any) -> ...any, OpenPopupEx: (...any) -> ...any, @@ -643,6 +643,7 @@ export type ImGuiAPI = { SetNextItemStorageID: (storage_id: number) -> (), SetNextItemWidth: (item_width: number) -> (), SetNextWindowBgAlpha: (alpha: number) -> (), + SetNextWindowCached: (key: any) -> (), SetNextWindowChildFlags: (...any) -> ...any, SetNextWindowClass: (window_class: any) -> (), SetNextWindowCollapsed: (collapsed: boolean, cond: number?) -> (), @@ -823,7 +824,7 @@ export type ImGuiAPI = { TestKeyOwner: (...any) -> ...any, Text: (fmt: string, ...any) -> (), TextAligned: (...any) -> ...any, - TextBufferToString: (...any) -> ...any, + TextBufferToString: (buf: {number}) -> string, TextColored: (col: ImVec4, fmt: string, ...any) -> (), TextDisabled: (fmt: string, ...any) -> (), TextEx: (...any) -> ...any, diff --git a/build/imgui_love.linemap.json b/build/imgui_love.linemap.json index 01b6734..968f0ac 100644 --- a/build/imgui_love.linemap.json +++ b/build/imgui_love.linemap.json @@ -1 +1 @@ -[[149, "imgui.lua"], [18609, "imgui_demo.lua"], [25894, "imgui_draw.lua"], [31132, "imgui_h.lua"], [33330, "imgui_impl_love.lua"], [33870, "imgui_internal.lua"], [37042, "imgui_tables.lua"], [41389, "imgui_widgets.lua"], [51144, "imstb_rectpack.lua"], [51590, "imstb_textedit.lua"], [52831, "imstb_truetype.lua"], [55299, "imstd_minstdio.lua"]] \ No newline at end of file +[[149, "imgui.lua"], [18647, "imgui_demo.lua"], [25932, "imgui_draw.lua"], [31170, "imgui_h.lua"], [33368, "imgui_impl_love.lua"], [33908, "imgui_internal.lua"], [37084, "imgui_tables.lua"], [41431, "imgui_widgets.lua"], [51186, "imstb_rectpack.lua"], [51632, "imstb_textedit.lua"], [52873, "imstb_truetype.lua"], [55341, "imstd_minstdio.lua"]] \ No newline at end of file diff --git a/build/imgui_love.lua b/build/imgui_love.lua index c2c0ba7..a9566a6 100644 --- a/build/imgui_love.lua +++ b/build/imgui_love.lua @@ -8994,6 +8994,26 @@ function ImGui.UpdateWindowSkipRefresh(window) end if bit32.band(g.NextWindowData.RefreshFlagsVal, ImGuiWindowRefreshFlags.TryToAvoidRefresh) ~= 0 then + -- [Port] SetNextWindowCached(): refresh on any change since the last refresh (and one frame after being busy) + if g.NextWindowData.CacheKeySet then + local function related(w) return w ~= nil and (w.RootWindow == window.RootWindow or ImGui.IsWindowWithinBeginStackOf(w.RootWindow, window)) end + local busy = window.Appearing or window.Hidden or window.DockIsActive or related(g.HoveredWindow) or related(g.NavWindow) or related(g.ActiveIdWindow) or (g.MovingWindow ~= nil and related(g.MovingWindow)) + local c, key, ds, tex = window.CacheSnap, g.NextWindowData.CacheKeyVal, g.IO.DisplaySize, g.IO.Fonts.TexData + if busy or c == nil or c.Busy or c.Key ~= key or c.Tex ~= tex or c.Collapsed ~= window.Collapsed + or c.PosX ~= window.Pos.x or c.PosY ~= window.Pos.y or c.SizeX ~= window.SizeFull.x or c.SizeY ~= window.SizeFull.y + or c.ScrollX ~= window.Scroll.x or c.ScrollY ~= window.Scroll.y or window.ScrollTarget.x < FLT_MAX or window.ScrollTarget.y < FLT_MAX + or c.DisplayX ~= ds.x or c.DisplayY ~= ds.y then + c = c or {} + window.CacheSnap = c + c.Busy, c.Key, c.Tex, c.Collapsed = busy, key, tex, window.Collapsed + c.PosX, c.PosY, c.SizeX, c.SizeY = window.Pos.x, window.Pos.y, window.SizeFull.x, window.SizeFull.y + c.ScrollX, c.ScrollY, c.DisplayX, c.DisplayY = window.Scroll.x, window.Scroll.y, ds.x, ds.y + return + end + -- skip: widgets early-out on SkipItems and Begin() returns visible=false; End() restores it + window.CacheSkipItemsBackup = window.SkipItems + window.SkipItems = true + end -- FIXME-IDLE: Tests for e.g. mouse clicks or keyboard while focused. if window.Appearing then -- If currently appearing return @@ -9625,6 +9645,20 @@ function ImGui.SetNextWindowRefreshPolicy(flags) local g = GImGui g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasRefreshPolicy) g.NextWindowData.RefreshFlagsVal = flags + g.NextWindowData.CacheKeySet = false +end + +-- [Port] Opt-in cached window (built on the refresh policy above). While nothing that could change the window +-- happened, Begin() keeps last frame's draw list and returns visible=false, so `if visible then ... end` skips your code. +-- It refreshes when `key` changes (pass e.g. a version number of the data shown), while the window (or a popup/child +-- of it) is hovered, focused or active and for one frame after, and when it appears, moves, resizes, scrolls, +-- collapses, is docked, or the display size or font atlas texture changes. +-- Anything else that changes the contents (style, fonts, time) must be part of `key`. +function ImGui.SetNextWindowCached(key) + local g = GImGui + ImGui.SetNextWindowRefreshPolicy(bit32.bor(ImGuiWindowRefreshFlags.TryToAvoidRefresh, ImGuiWindowRefreshFlags.RefreshOnHover, ImGuiWindowRefreshFlags.RefreshOnFocus)) + g.NextWindowData.CacheKeySet = true + g.NextWindowData.CacheKeyVal = key end function ImGui.GetWindowDpiScale() @@ -10827,6 +10861,10 @@ function ImGui.End() IM_ASSERT(window.DrawList == nil) window.DrawList = window.DrawListInst end + if window.CacheSkipItemsBackup ~= nil then -- [Port] SetNextWindowCached() + window.SkipItems = window.CacheSkipItemsBackup + window.CacheSkipItemsBackup = nil + end -- Stop logging if g.LogWindow == window then -- FIXME: add more options for scope of logging @@ -35105,7 +35143,9 @@ function ImGuiNextWindowData() SizeCallbackUserData = nil, BgAlphaVal = nil, MenuBarOffsetMinVal = ImVec2(), - RefreshFlagsVal = nil + RefreshFlagsVal = nil, + CacheKeySet = false, -- [Port] SetNextWindowCached() + CacheKeyVal = nil }, IMGUI_NEXT_WINDOW_DATA) end @@ -36277,6 +36317,8 @@ function ImGuiWindow(ctx, name) SkipItems = false, SkipRefresh = false, + CacheSnap = nil, -- [Port] SetNextWindowCached(): state at the last refresh + CacheSkipItemsBackup = nil, Appearing = false, diff --git a/docs/TODO.txt b/docs/TODO.txt index 8b4c2e2..2af3d22 100644 --- a/docs/TODO.txt +++ b/docs/TODO.txt @@ -45,7 +45,6 @@ KNOWN ISSUES PERFORMANCE IDEAS (not done) ---------------------------- -- Opt-in cached windows (skip user code for idle windows). - Cheaper visible ColorEdit4 rows; cheaper Begin() for child windows. PLANNED EXTENSIONS diff --git a/imgui.lua b/imgui.lua index 5ed1faa..82b698a 100644 --- a/imgui.lua +++ b/imgui.lua @@ -8846,6 +8846,26 @@ function ImGui.UpdateWindowSkipRefresh(window) end if bit32.band(g.NextWindowData.RefreshFlagsVal, ImGuiWindowRefreshFlags.TryToAvoidRefresh) ~= 0 then + -- [Port] SetNextWindowCached(): refresh on any change since the last refresh (and one frame after being busy) + if g.NextWindowData.CacheKeySet then + local function related(w) return w ~= nil and (w.RootWindow == window.RootWindow or ImGui.IsWindowWithinBeginStackOf(w.RootWindow, window)) end + local busy = window.Appearing or window.Hidden or window.DockIsActive or related(g.HoveredWindow) or related(g.NavWindow) or related(g.ActiveIdWindow) or (g.MovingWindow ~= nil and related(g.MovingWindow)) + local c, key, ds, tex = window.CacheSnap, g.NextWindowData.CacheKeyVal, g.IO.DisplaySize, g.IO.Fonts.TexData + if busy or c == nil or c.Busy or c.Key ~= key or c.Tex ~= tex or c.Collapsed ~= window.Collapsed + or c.PosX ~= window.Pos.x or c.PosY ~= window.Pos.y or c.SizeX ~= window.SizeFull.x or c.SizeY ~= window.SizeFull.y + or c.ScrollX ~= window.Scroll.x or c.ScrollY ~= window.Scroll.y or window.ScrollTarget.x < FLT_MAX or window.ScrollTarget.y < FLT_MAX + or c.DisplayX ~= ds.x or c.DisplayY ~= ds.y then + c = c or {} + window.CacheSnap = c + c.Busy, c.Key, c.Tex, c.Collapsed = busy, key, tex, window.Collapsed + c.PosX, c.PosY, c.SizeX, c.SizeY = window.Pos.x, window.Pos.y, window.SizeFull.x, window.SizeFull.y + c.ScrollX, c.ScrollY, c.DisplayX, c.DisplayY = window.Scroll.x, window.Scroll.y, ds.x, ds.y + return + end + -- skip: widgets early-out on SkipItems and Begin() returns visible=false; End() restores it + window.CacheSkipItemsBackup = window.SkipItems + window.SkipItems = true + end -- FIXME-IDLE: Tests for e.g. mouse clicks or keyboard while focused. if window.Appearing then -- If currently appearing return @@ -9477,6 +9497,20 @@ function ImGui.SetNextWindowRefreshPolicy(flags) local g = GImGui g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasRefreshPolicy) g.NextWindowData.RefreshFlagsVal = flags + g.NextWindowData.CacheKeySet = false +end + +-- [Port] Opt-in cached window (built on the refresh policy above). While nothing that could change the window +-- happened, Begin() keeps last frame's draw list and returns visible=false, so `if visible then ... end` skips your code. +-- It refreshes when `key` changes (pass e.g. a version number of the data shown), while the window (or a popup/child +-- of it) is hovered, focused or active and for one frame after, and when it appears, moves, resizes, scrolls, +-- collapses, is docked, or the display size or font atlas texture changes. +-- Anything else that changes the contents (style, fonts, time) must be part of `key`. +function ImGui.SetNextWindowCached(key) + local g = GImGui + ImGui.SetNextWindowRefreshPolicy(bit32.bor(ImGuiWindowRefreshFlags.TryToAvoidRefresh, ImGuiWindowRefreshFlags.RefreshOnHover, ImGuiWindowRefreshFlags.RefreshOnFocus)) + g.NextWindowData.CacheKeySet = true + g.NextWindowData.CacheKeyVal = key end function ImGui.GetWindowDpiScale() @@ -10679,6 +10713,10 @@ function ImGui.End() IM_ASSERT(window.DrawList == nil) window.DrawList = window.DrawListInst end + if window.CacheSkipItemsBackup ~= nil then -- [Port] SetNextWindowCached() + window.SkipItems = window.CacheSkipItemsBackup + window.CacheSkipItemsBackup = nil + end -- Stop logging if g.LogWindow == window then -- FIXME: add more options for scope of logging diff --git a/imgui_internal.lua b/imgui_internal.lua index 33c16e2..57c327e 100644 --- a/imgui_internal.lua +++ b/imgui_internal.lua @@ -1236,7 +1236,9 @@ function ImGuiNextWindowData() SizeCallbackUserData = nil, BgAlphaVal = nil, MenuBarOffsetMinVal = ImVec2(), - RefreshFlagsVal = nil + RefreshFlagsVal = nil, + CacheKeySet = false, -- [Port] SetNextWindowCached() + CacheKeyVal = nil }, IMGUI_NEXT_WINDOW_DATA) end @@ -2408,6 +2410,8 @@ function ImGuiWindow(ctx, name) SkipItems = false, SkipRefresh = false, + CacheSnap = nil, -- [Port] SetNextWindowCached(): state at the last refresh + CacheSkipItemsBackup = nil, Appearing = false, diff --git a/tools/gen_types.py b/tools/gen_types.py index 37dc95f..80fa169 100644 --- a/tools/gen_types.py +++ b/tools/gen_types.py @@ -39,6 +39,11 @@ def upstream_decls(): return decls LOOSE = "(...any) -> ...any" +PORT_ONLY = { # port additions that aren't in imgui.h + "SetNextWindowCached": "(key: any) -> ()", + "NewTextBuffer": "(str: string, size: number?) -> {number}", + "TextBufferToString": "(buf: {number}) -> string", +} def signature(port_params, ret, cparams): if any(p == "..." for p in cparams): @@ -103,7 +108,7 @@ def types_header(files): "export type ImGuiAPI = {"] for name in sorted(fns): d = decls.get(name) - sig = signature(fns[name], *d[0]) if d and len(d) == 1 else LOOSE + sig = PORT_ONLY.get(name) or (signature(fns[name], *d[0]) if d and len(d) == 1 else LOOSE) lines.append(" %s: %s," % (name, sig)) lines += [" [string]: any,", "}"] for name, keys in sorted(enums(files).items()): diff --git a/tools/test/scripts/t_cached_window.luau b/tools/test/scripts/t_cached_window.luau new file mode 100644 index 0000000..e04a223 --- /dev/null +++ b/tools/test/scripts/t_cached_window.luau @@ -0,0 +1,58 @@ +-- SetNextWindowCached: the same scripted scenario runs twice, window "Win##A" uncached then "Win##B" cached. +-- Every frame the window's draw list must hash identically in both phases, while B's body runs far less often. +-- run: python3 tools/test/run.py --main tools/test/scripts/t_cached_window.luau --frames 130 --quiet --roblox +local G = require(game:GetService("ReplicatedStorage"):WaitForChild("ImGui")) +local ImGui, ImVec2 = G.ImGui, G.ImVec2 +ImGui.CreateContext() +G.ImGui_ImplRoblox.Init() +G.ImGui_ImplRoblox.SetRenderRate(0) +local io = ImGui.GetIO() +local P = 60 -- frames per phase +local frame, hashes, runs = 0, {}, { A = 0, B = 0 } + +local function hash(dl) + local h = 0 + local function mix(v) h = (h * 31 + math.floor(v * 64 + 0.5)) % 2147483647 end + local vb, id = dl.VtxBuffer.Buf, dl.IdxBuffer.Data + for i = 1, dl.VtxBuffer.Size do local o = (i - 1) * 36; mix(buffer.readf64(vb, o)); mix(buffer.readf64(vb, o + 8)); mix(buffer.readf64(vb, o + 16) * 4096); mix(buffer.readf64(vb, o + 24) * 4096); mix(buffer.readu32(vb, o + 32) / 64) end + for i = 1, dl.IdxBuffer.Size do mix(id[i]) end + for _, c in dl.CmdBuffer:iter() do mix(c.ClipRect.x); mix(c.ClipRect.y); mix(c.ClipRect.z); mix(c.ClipRect.w); mix(c.ElemCount) end + return h +end + +game:GetService("RunService").Stepped:Connect(function() + frame += 1 + if frame > 2 * P then return end + local phase = frame <= P and "A" or "B" + local t = (frame - 1) % P + 1 -- frame within the phase + G.ImGui_ImplRoblox.NewFrame() + io.DeltaTime = 1 / 60 + -- mouse: away, over the button (hover highlight), away again + if t >= 20 and t < 26 then io:AddMousePosEvent(60, 75) else io:AddMousePosEvent(700, 500) end + local key = math.floor(t / 15) -- data version: changes at t = 15, 30, 45 + ImGui.NewFrame() + if t == 3 then ImGui.SetWindowFocus(nil) end -- focused -> unfocused title bar must not stay stale + if t == 1 then ImGui.SetNextWindowFocus() end + ImGui.SetNextWindowPos(ImVec2(20, 20), G.ImGuiCond.Always) + ImGui.SetNextWindowSize(ImVec2(220, 140), G.ImGuiCond.Always) + if phase == "B" then ImGui.SetNextWindowCached(key) end + local _, visible = ImGui.Begin("Win##" .. phase) + if visible then + runs[phase] += 1 + ImGui.Button("Button") + ImGui.Text("value %d", key) + ImGui.SliderFloat("slider", key * 0.1, 0, 1) + end + ImGui.End() + ImGui.Render() + local h = hash(ImGui.FindWindowByName("Win##" .. phase).DrawList) + if phase == "A" then hashes[t] = h + elseif h ~= hashes[t] then error(("frame %d: cached draw list differs from uncached"):format(t)) end + G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData()) + if frame == 2 * P then + print(("runs: uncached %d, cached %d"):format(runs.A, runs.B)) + assert(runs.A == P, "uncached window must run every frame") + assert(runs.B < P / 3, "cached window ran too often") + print("CACHED OK") + end +end) diff --git a/tools/test/scripts/t_readme.luau b/tools/test/scripts/t_readme.luau index d5b11e6..3c1b66e 100644 --- a/tools/test/scripts/t_readme.luau +++ b/tools/test/scripts/t_readme.luau @@ -124,12 +124,24 @@ local function MyTool() end -- [/snippet] +-- [snippet:cached] +local stats = { version = 0, kills = 0 } +local function StatsWindow() + ImGui.SetNextWindowCached(stats.version) -- bump stats.version whenever stats change + local _, visible = ImGui.Begin("Stats") + if visible then -- false while the cached contents are reused + ImGui.Text("Kills: %d", stats.kills) + end + ImGui.End() +end +-- [/snippet] + local frame = 0 RunService.Stepped:Connect(function() frame += 1 G.ImGui_ImplRoblox.NewFrame() ImGui.NewFrame() - MyFirstTool(); MyTool(); Conventions(); MenusAndTable(); Plot(); CustomDrawing() + MyFirstTool(); MyTool(); Conventions(); MenusAndTable(); Plot(); CustomDrawing(); StatsWindow() ImGui.Render() G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData()) if frame == 20 then