SetNextWindowCached(key): opt-in cached windows on top of the refresh policy (skip contents while idle), t_cached_window test, README snippet
Build & Release / build (push) Canceled after 0s
Build & Release / build (push) Canceled after 0s
This commit is contained in:
@@ -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.
|
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
|
### Testing
|
||||||
|
|
||||||
`tools/test/run.py` runs any LocalScript headless against a mocked Roblox (`tools/test/roblox_mock.luau`):
|
`tools/test/run.py` runs any LocalScript headless against a mocked Roblox (`tools/test/roblox_mock.luau`):
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ jobs:
|
|||||||
luajit -bl dist/love-nodemo/imgui_love.lua > /dev/null
|
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
|
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
|
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
|
python3 tools/bundle.py --target love >/dev/null
|
||||||
luajit tools/test/love/t_drawhash.lua 60 Widgets,Basic
|
luajit tools/test/love/t_drawhash.lua 60 Widgets,Basic
|
||||||
luajit tools/test/love/t_examples.lua
|
luajit tools/test/love/t_examples.lua
|
||||||
|
|||||||
@@ -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.
|
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
|
### Testing
|
||||||
|
|
||||||
`tools/test/run.py` runs any LocalScript headless against a mocked Roblox (`tools/test/roblox_mock.luau`):
|
`tools/test/run.py` runs any LocalScript headless against a mocked Roblox (`tools/test/roblox_mock.luau`):
|
||||||
|
|||||||
@@ -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"]]
|
[[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"]]
|
||||||
+43
-1
@@ -8857,6 +8857,26 @@ function ImGui.UpdateWindowSkipRefresh(window)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if bit32.band(g.NextWindowData.RefreshFlagsVal, ImGuiWindowRefreshFlags.TryToAvoidRefresh) ~= 0 then
|
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.
|
-- FIXME-IDLE: Tests for e.g. mouse clicks or keyboard while focused.
|
||||||
if window.Appearing then -- If currently appearing
|
if window.Appearing then -- If currently appearing
|
||||||
return
|
return
|
||||||
@@ -9488,6 +9508,20 @@ function ImGui.SetNextWindowRefreshPolicy(flags)
|
|||||||
local g = GImGui
|
local g = GImGui
|
||||||
g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasRefreshPolicy)
|
g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasRefreshPolicy)
|
||||||
g.NextWindowData.RefreshFlagsVal = flags
|
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
|
end
|
||||||
|
|
||||||
function ImGui.GetWindowDpiScale()
|
function ImGui.GetWindowDpiScale()
|
||||||
@@ -10690,6 +10724,10 @@ function ImGui.End()
|
|||||||
IM_ASSERT(window.DrawList == nil)
|
IM_ASSERT(window.DrawList == nil)
|
||||||
window.DrawList = window.DrawListInst
|
window.DrawList = window.DrawListInst
|
||||||
end
|
end
|
||||||
|
if window.CacheSkipItemsBackup ~= nil then -- [Port] SetNextWindowCached()
|
||||||
|
window.SkipItems = window.CacheSkipItemsBackup
|
||||||
|
window.CacheSkipItemsBackup = nil
|
||||||
|
end
|
||||||
|
|
||||||
-- Stop logging
|
-- Stop logging
|
||||||
if g.LogWindow == window then -- FIXME: add more options for scope of logging
|
if g.LogWindow == window then -- FIXME: add more options for scope of logging
|
||||||
@@ -42163,7 +42201,9 @@ function ImGuiNextWindowData()
|
|||||||
SizeCallbackUserData = nil,
|
SizeCallbackUserData = nil,
|
||||||
BgAlphaVal = nil,
|
BgAlphaVal = nil,
|
||||||
MenuBarOffsetMinVal = ImVec2(),
|
MenuBarOffsetMinVal = ImVec2(),
|
||||||
RefreshFlagsVal = nil
|
RefreshFlagsVal = nil,
|
||||||
|
CacheKeySet = false, -- [Port] SetNextWindowCached()
|
||||||
|
CacheKeyVal = nil
|
||||||
}, IMGUI_NEXT_WINDOW_DATA)
|
}, IMGUI_NEXT_WINDOW_DATA)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -43335,6 +43375,8 @@ function ImGuiWindow(ctx, name)
|
|||||||
SkipItems = false,
|
SkipItems = false,
|
||||||
|
|
||||||
SkipRefresh = false,
|
SkipRefresh = false,
|
||||||
|
CacheSnap = nil, -- [Port] SetNextWindowCached(): state at the last refresh
|
||||||
|
CacheSkipItemsBackup = nil,
|
||||||
|
|
||||||
Appearing = false,
|
Appearing = false,
|
||||||
|
|
||||||
|
|||||||
@@ -516,7 +516,7 @@ export type ImGuiAPI = {
|
|||||||
NavUpdateWindowingTarget: (...any) -> ...any,
|
NavUpdateWindowingTarget: (...any) -> ...any,
|
||||||
NewFrame: () -> (),
|
NewFrame: () -> (),
|
||||||
NewLine: () -> (),
|
NewLine: () -> (),
|
||||||
NewTextBuffer: (...any) -> ...any,
|
NewTextBuffer: (str: string, size: number?) -> {number},
|
||||||
NextColumn: () -> (),
|
NextColumn: () -> (),
|
||||||
OpenPopup: (...any) -> ...any,
|
OpenPopup: (...any) -> ...any,
|
||||||
OpenPopupEx: (...any) -> ...any,
|
OpenPopupEx: (...any) -> ...any,
|
||||||
@@ -643,6 +643,7 @@ export type ImGuiAPI = {
|
|||||||
SetNextItemStorageID: (storage_id: number) -> (),
|
SetNextItemStorageID: (storage_id: number) -> (),
|
||||||
SetNextItemWidth: (item_width: number) -> (),
|
SetNextItemWidth: (item_width: number) -> (),
|
||||||
SetNextWindowBgAlpha: (alpha: number) -> (),
|
SetNextWindowBgAlpha: (alpha: number) -> (),
|
||||||
|
SetNextWindowCached: (key: any) -> (),
|
||||||
SetNextWindowChildFlags: (...any) -> ...any,
|
SetNextWindowChildFlags: (...any) -> ...any,
|
||||||
SetNextWindowClass: (window_class: any) -> (),
|
SetNextWindowClass: (window_class: any) -> (),
|
||||||
SetNextWindowCollapsed: (collapsed: boolean, cond: number?) -> (),
|
SetNextWindowCollapsed: (collapsed: boolean, cond: number?) -> (),
|
||||||
@@ -823,7 +824,7 @@ export type ImGuiAPI = {
|
|||||||
TestKeyOwner: (...any) -> ...any,
|
TestKeyOwner: (...any) -> ...any,
|
||||||
Text: (fmt: string, ...any) -> (),
|
Text: (fmt: string, ...any) -> (),
|
||||||
TextAligned: (...any) -> ...any,
|
TextAligned: (...any) -> ...any,
|
||||||
TextBufferToString: (...any) -> ...any,
|
TextBufferToString: (buf: {number}) -> string,
|
||||||
TextColored: (col: ImVec4, fmt: string, ...any) -> (),
|
TextColored: (col: ImVec4, fmt: string, ...any) -> (),
|
||||||
TextDisabled: (fmt: string, ...any) -> (),
|
TextDisabled: (fmt: string, ...any) -> (),
|
||||||
TextEx: (...any) -> ...any,
|
TextEx: (...any) -> ...any,
|
||||||
|
|||||||
@@ -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"]]
|
[[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"]]
|
||||||
+43
-1
@@ -8994,6 +8994,26 @@ function ImGui.UpdateWindowSkipRefresh(window)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if bit32.band(g.NextWindowData.RefreshFlagsVal, ImGuiWindowRefreshFlags.TryToAvoidRefresh) ~= 0 then
|
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.
|
-- FIXME-IDLE: Tests for e.g. mouse clicks or keyboard while focused.
|
||||||
if window.Appearing then -- If currently appearing
|
if window.Appearing then -- If currently appearing
|
||||||
return
|
return
|
||||||
@@ -9625,6 +9645,20 @@ function ImGui.SetNextWindowRefreshPolicy(flags)
|
|||||||
local g = GImGui
|
local g = GImGui
|
||||||
g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasRefreshPolicy)
|
g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasRefreshPolicy)
|
||||||
g.NextWindowData.RefreshFlagsVal = flags
|
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
|
end
|
||||||
|
|
||||||
function ImGui.GetWindowDpiScale()
|
function ImGui.GetWindowDpiScale()
|
||||||
@@ -10827,6 +10861,10 @@ function ImGui.End()
|
|||||||
IM_ASSERT(window.DrawList == nil)
|
IM_ASSERT(window.DrawList == nil)
|
||||||
window.DrawList = window.DrawListInst
|
window.DrawList = window.DrawListInst
|
||||||
end
|
end
|
||||||
|
if window.CacheSkipItemsBackup ~= nil then -- [Port] SetNextWindowCached()
|
||||||
|
window.SkipItems = window.CacheSkipItemsBackup
|
||||||
|
window.CacheSkipItemsBackup = nil
|
||||||
|
end
|
||||||
|
|
||||||
-- Stop logging
|
-- Stop logging
|
||||||
if g.LogWindow == window then -- FIXME: add more options for scope of logging
|
if g.LogWindow == window then -- FIXME: add more options for scope of logging
|
||||||
@@ -35105,7 +35143,9 @@ function ImGuiNextWindowData()
|
|||||||
SizeCallbackUserData = nil,
|
SizeCallbackUserData = nil,
|
||||||
BgAlphaVal = nil,
|
BgAlphaVal = nil,
|
||||||
MenuBarOffsetMinVal = ImVec2(),
|
MenuBarOffsetMinVal = ImVec2(),
|
||||||
RefreshFlagsVal = nil
|
RefreshFlagsVal = nil,
|
||||||
|
CacheKeySet = false, -- [Port] SetNextWindowCached()
|
||||||
|
CacheKeyVal = nil
|
||||||
}, IMGUI_NEXT_WINDOW_DATA)
|
}, IMGUI_NEXT_WINDOW_DATA)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -36277,6 +36317,8 @@ function ImGuiWindow(ctx, name)
|
|||||||
SkipItems = false,
|
SkipItems = false,
|
||||||
|
|
||||||
SkipRefresh = false,
|
SkipRefresh = false,
|
||||||
|
CacheSnap = nil, -- [Port] SetNextWindowCached(): state at the last refresh
|
||||||
|
CacheSkipItemsBackup = nil,
|
||||||
|
|
||||||
Appearing = false,
|
Appearing = false,
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ KNOWN ISSUES
|
|||||||
|
|
||||||
PERFORMANCE IDEAS (not done)
|
PERFORMANCE IDEAS (not done)
|
||||||
----------------------------
|
----------------------------
|
||||||
- Opt-in cached windows (skip user code for idle windows).
|
|
||||||
- Cheaper visible ColorEdit4 rows; cheaper Begin() for child windows.
|
- Cheaper visible ColorEdit4 rows; cheaper Begin() for child windows.
|
||||||
|
|
||||||
PLANNED EXTENSIONS
|
PLANNED EXTENSIONS
|
||||||
|
|||||||
@@ -8846,6 +8846,26 @@ function ImGui.UpdateWindowSkipRefresh(window)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if bit32.band(g.NextWindowData.RefreshFlagsVal, ImGuiWindowRefreshFlags.TryToAvoidRefresh) ~= 0 then
|
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.
|
-- FIXME-IDLE: Tests for e.g. mouse clicks or keyboard while focused.
|
||||||
if window.Appearing then -- If currently appearing
|
if window.Appearing then -- If currently appearing
|
||||||
return
|
return
|
||||||
@@ -9477,6 +9497,20 @@ function ImGui.SetNextWindowRefreshPolicy(flags)
|
|||||||
local g = GImGui
|
local g = GImGui
|
||||||
g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasRefreshPolicy)
|
g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasRefreshPolicy)
|
||||||
g.NextWindowData.RefreshFlagsVal = flags
|
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
|
end
|
||||||
|
|
||||||
function ImGui.GetWindowDpiScale()
|
function ImGui.GetWindowDpiScale()
|
||||||
@@ -10679,6 +10713,10 @@ function ImGui.End()
|
|||||||
IM_ASSERT(window.DrawList == nil)
|
IM_ASSERT(window.DrawList == nil)
|
||||||
window.DrawList = window.DrawListInst
|
window.DrawList = window.DrawListInst
|
||||||
end
|
end
|
||||||
|
if window.CacheSkipItemsBackup ~= nil then -- [Port] SetNextWindowCached()
|
||||||
|
window.SkipItems = window.CacheSkipItemsBackup
|
||||||
|
window.CacheSkipItemsBackup = nil
|
||||||
|
end
|
||||||
|
|
||||||
-- Stop logging
|
-- Stop logging
|
||||||
if g.LogWindow == window then -- FIXME: add more options for scope of logging
|
if g.LogWindow == window then -- FIXME: add more options for scope of logging
|
||||||
|
|||||||
+5
-1
@@ -1236,7 +1236,9 @@ function ImGuiNextWindowData()
|
|||||||
SizeCallbackUserData = nil,
|
SizeCallbackUserData = nil,
|
||||||
BgAlphaVal = nil,
|
BgAlphaVal = nil,
|
||||||
MenuBarOffsetMinVal = ImVec2(),
|
MenuBarOffsetMinVal = ImVec2(),
|
||||||
RefreshFlagsVal = nil
|
RefreshFlagsVal = nil,
|
||||||
|
CacheKeySet = false, -- [Port] SetNextWindowCached()
|
||||||
|
CacheKeyVal = nil
|
||||||
}, IMGUI_NEXT_WINDOW_DATA)
|
}, IMGUI_NEXT_WINDOW_DATA)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2408,6 +2410,8 @@ function ImGuiWindow(ctx, name)
|
|||||||
SkipItems = false,
|
SkipItems = false,
|
||||||
|
|
||||||
SkipRefresh = false,
|
SkipRefresh = false,
|
||||||
|
CacheSnap = nil, -- [Port] SetNextWindowCached(): state at the last refresh
|
||||||
|
CacheSkipItemsBackup = nil,
|
||||||
|
|
||||||
Appearing = false,
|
Appearing = false,
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -39,6 +39,11 @@ def upstream_decls():
|
|||||||
return decls
|
return decls
|
||||||
|
|
||||||
LOOSE = "(...any) -> ...any"
|
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):
|
def signature(port_params, ret, cparams):
|
||||||
if any(p == "..." for p in cparams):
|
if any(p == "..." for p in cparams):
|
||||||
@@ -103,7 +108,7 @@ def types_header(files):
|
|||||||
"export type ImGuiAPI = {"]
|
"export type ImGuiAPI = {"]
|
||||||
for name in sorted(fns):
|
for name in sorted(fns):
|
||||||
d = decls.get(name)
|
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.append(" %s: %s," % (name, sig))
|
||||||
lines += [" [string]: any,", "}"]
|
lines += [" [string]: any,", "}"]
|
||||||
for name, keys in sorted(enums(files).items()):
|
for name, keys in sorted(enums(files).items()):
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -124,12 +124,24 @@ local function MyTool()
|
|||||||
end
|
end
|
||||||
-- [/snippet]
|
-- [/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
|
local frame = 0
|
||||||
RunService.Stepped:Connect(function()
|
RunService.Stepped:Connect(function()
|
||||||
frame += 1
|
frame += 1
|
||||||
G.ImGui_ImplRoblox.NewFrame()
|
G.ImGui_ImplRoblox.NewFrame()
|
||||||
ImGui.NewFrame()
|
ImGui.NewFrame()
|
||||||
MyFirstTool(); MyTool(); Conventions(); MenusAndTable(); Plot(); CustomDrawing()
|
MyFirstTool(); MyTool(); Conventions(); MenusAndTable(); Plot(); CustomDrawing(); StatsWindow()
|
||||||
ImGui.Render()
|
ImGui.Render()
|
||||||
G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
|
G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
|
||||||
if frame == 20 then
|
if frame == 20 then
|
||||||
|
|||||||
Reference in New Issue
Block a user