diff --git a/backends/imgui_impl_roblox.lua b/backends/imgui_impl_roblox.lua index ac68be3..779443e 100644 --- a/backends/imgui_impl_roblox.lua +++ b/backends/imgui_impl_roblox.lua @@ -862,7 +862,8 @@ function ImGui_ImplRoblox_RenderDrawData(draw_data) end if rate > 0 then local now = os.clock() - if now + 0.002 < bd.NextRender then table.clear(bd.Overlays); PE(); return end + -- Skipped: kept pending, so if no new frame comes (the app stopped rendering) Heartbeat still draws it + if now + 0.002 < bd.NextRender then bd.Pending = draw_data; PE(); return end bd.NextRender = math.max(bd.NextRender + 1 / rate, now) end @@ -1129,6 +1130,16 @@ function ImGui_ImplRoblox_Init(parent, render_scale) bd.PendingChars = {} + -- Render rate cap trailing edge: the last frame submitted is always drawn, even if it was skipped and the app + -- then stopped calling NewFrame (e.g. it rendered one empty frame to hide its windows) + connect(game:GetService("RunService").Heartbeat, function() + local d = bd.Pending + if d and os.clock() + 0.002 >= bd.NextRender then + bd.Pending = nil + ImGui_ImplRoblox_RenderDrawData(d) + end + end) + connect(UserInputService.WindowFocused, function() ImGui.GetIO():AddFocusEvent(true) end) connect(UserInputService.WindowFocusReleased, function() ImGui.GetIO():AddFocusEvent(false) end) end @@ -1188,6 +1199,7 @@ function ImGui_ImplRoblox_NewFrame() PB("ImGui NewFrame (backend)") local io = ImGui.GetIO() local bd = ImGui_ImplRoblox_GetBackendData() + if bd.Pending then bd.Pending = nil; table.clear(bd.Overlays) end -- skipped frame superseded (its draw lists get reset) local screen = bd.Gui.AbsoluteSize local w, h = floor(screen.X * bd.Scale), floor(screen.Y * bd.Scale) diff --git a/build/ImGui.linemap.json b/build/ImGui.linemap.json index 938cd59..ba5877b 100644 --- a/build/ImGui.linemap.json +++ b/build/ImGui.linemap.json @@ -1 +1 @@ -[[12, "imgui.lua"], [18510, "imgui_demo.lua"], [25795, "imgui_draw.lua"], [31033, "imgui_h.lua"], [33231, "imgui_impl_roblox.lua"], [34571, "imgui_impl_roblox_fonts.lua"], [40974, "imgui_internal.lua"], [44150, "imgui_tables.lua"], [48497, "imgui_widgets.lua"], [58252, "imstb_rectpack.lua"], [58698, "imstb_textedit.lua"], [59939, "imstb_truetype.lua"], [62407, "imstd_minstdio.lua"]] \ No newline at end of file +[[12, "imgui.lua"], [18514, "imgui_demo.lua"], [25799, "imgui_draw.lua"], [31037, "imgui_h.lua"], [33235, "imgui_impl_roblox.lua"], [34587, "imgui_impl_roblox_fonts.lua"], [40990, "imgui_internal.lua"], [44166, "imgui_tables.lua"], [48513, "imgui_widgets.lua"], [58268, "imstb_rectpack.lua"], [58714, "imstb_textedit.lua"], [59955, "imstb_truetype.lua"], [62423, "imstd_minstdio.lua"]] \ No newline at end of file diff --git a/build/ImGui.luau b/build/ImGui.luau index c97d955..55d6f89 100644 --- a/build/ImGui.luau +++ b/build/ImGui.luau @@ -8637,9 +8637,13 @@ local function RenderWindowDecorations(window, title_bar_rect, title_bar_is_high end if override_alpha then - bg_col = bit32.band(bg_col, bit32.bnot(IM_COL32_A_MASK)) or bit32.lshift(IM_F32_TO_INT8_SAT(alpha), IM_COL32_A_SHIFT) + bg_col = bit32.bor(bit32.band(bg_col, bit32.bnot(IM_COL32_A_MASK)), bit32.lshift(IM_F32_TO_INT8_SAT(alpha), IM_COL32_A_SHIFT)) end + -- Render, for docked windows and host windows we ensure BG goes before decorations + if window.DockIsActive then window.DockNode.LastBgColor = bg_col end + if bit32.band(flags, ImGuiWindowFlags.DockNodeHost) ~= 0 then bg_col = 0 end + if bit32.band(bg_col, IM_COL32_A_MASK) ~= 0 then local bg_rect = ImRect(window.Pos + ImVec2(0, window.TitleBarHeight), window.Pos + window.Size) local bg_rounding_flags @@ -34092,7 +34096,8 @@ function ImGui_ImplRoblox_RenderDrawData(draw_data) end if rate > 0 then local now = os.clock() - if now + 0.002 < bd.NextRender then table.clear(bd.Overlays); PE(); return end + -- Skipped: kept pending, so if no new frame comes (the app stopped rendering) Heartbeat still draws it + if now + 0.002 < bd.NextRender then bd.Pending = draw_data; PE(); return end bd.NextRender = math.max(bd.NextRender + 1 / rate, now) end @@ -34359,6 +34364,16 @@ function ImGui_ImplRoblox_Init(parent, render_scale) bd.PendingChars = {} + -- Render rate cap trailing edge: the last frame submitted is always drawn, even if it was skipped and the app + -- then stopped calling NewFrame (e.g. it rendered one empty frame to hide its windows) + connect(game:GetService("RunService").Heartbeat, function() + local d = bd.Pending + if d and os.clock() + 0.002 >= bd.NextRender then + bd.Pending = nil + ImGui_ImplRoblox_RenderDrawData(d) + end + end) + connect(UserInputService.WindowFocused, function() ImGui.GetIO():AddFocusEvent(true) end) connect(UserInputService.WindowFocusReleased, function() ImGui.GetIO():AddFocusEvent(false) end) end @@ -34418,6 +34433,7 @@ function ImGui_ImplRoblox_NewFrame() PB("ImGui NewFrame (backend)") local io = ImGui.GetIO() local bd = ImGui_ImplRoblox_GetBackendData() + if bd.Pending then bd.Pending = nil; table.clear(bd.Overlays) end -- skipped frame superseded (its draw lists get reset) local screen = bd.Gui.AbsoluteSize local w, h = floor(screen.X * bd.Scale), floor(screen.Y * bd.Scale) diff --git a/imgui.lua b/imgui.lua index 82b698a..c059795 100644 --- a/imgui.lua +++ b/imgui.lua @@ -8626,9 +8626,13 @@ local function RenderWindowDecorations(window, title_bar_rect, title_bar_is_high end if override_alpha then - bg_col = bit32.band(bg_col, bit32.bnot(IM_COL32_A_MASK)) or bit32.lshift(IM_F32_TO_INT8_SAT(alpha), IM_COL32_A_SHIFT) + bg_col = bit32.bor(bit32.band(bg_col, bit32.bnot(IM_COL32_A_MASK)), bit32.lshift(IM_F32_TO_INT8_SAT(alpha), IM_COL32_A_SHIFT)) end + -- Render, for docked windows and host windows we ensure BG goes before decorations + if window.DockIsActive then window.DockNode.LastBgColor = bg_col end + if bit32.band(flags, ImGuiWindowFlags.DockNodeHost) ~= 0 then bg_col = 0 end + if bit32.band(bg_col, IM_COL32_A_MASK) ~= 0 then local bg_rect = ImRect(window.Pos + ImVec2(0, window.TitleBarHeight), window.Pos + window.Size) local bg_rounding_flags diff --git a/tools/test/scripts/t_dock_close.luau b/tools/test/scripts/t_dock_close.luau new file mode 100644 index 0000000..a0e4b94 --- /dev/null +++ b/tools/test/scripts/t_dock_close.luau @@ -0,0 +1,68 @@ +-- Docked windows (floating split node) stop being submitted: the host lingers one frame, its bodies must be filled +-- with the windows' bg colour (node.LastBgColor), not white, and everything must be gone within a few frames. +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") +local G = require(ReplicatedStorage:WaitForChild("ImGui")) +local ImGui, ImVec2 = G.ImGui, G.ImVec2 +ImGui.CreateContext() +local io = ImGui.GetIO() +io.ConfigFlags = bit32.bor(io.ConfigFlags, G.ImGuiConfigFlags.DockingEnable) +G.ImGui_ImplRoblox.Init() +local frame, closed_at, done = 0, 31, false +RunService.Stepped:Connect(function() + if done then return end + frame += 1 + G.ImGui_ImplRoblox.NewFrame() + ImGui.NewFrame() + if frame == 2 then + local id = ImGui.GetID("floating") + ImGui.DockBuilderRemoveNode(id) + ImGui.DockBuilderAddNode(id, 0) + ImGui.DockBuilderSetNodePos(id, ImVec2(100, 100)) + ImGui.DockBuilderSetNodeSize(id, ImVec2(600, 400)) + local left, right = ImGui.DockBuilderSplitNode(id, G.ImGuiDir.Left, 0.5) + ImGui.DockBuilderDockWindow("A", left) + ImGui.DockBuilderDockWindow("B", right) + ImGui.DockBuilderFinish(id) + end + if frame < closed_at then + for _, n in { "A", "B" } do + if ImGui.Begin(n) then ImGui.Text("window " .. n) end + ImGui.End() + end + end + if frame == closed_at - 1 then + local a, b = ImGui.FindWindowByName("A"), ImGui.FindWindowByName("B") + assert(a.DockNode and b.DockNode and a.DockNode ~= b.DockNode and a.DockNode.HostWindow, "A, B docked side by side") + end + ImGui.Render() + local dd = ImGui.GetDrawData() + if frame >= closed_at then + for _, dl in dd.CmdLists:iter() do + -- white vertices spanning a big area = a white body fill (white text glyphs are small) + local vb, x0, y0, x1, y1 = dl.VtxBuffer.Buf, math.huge, math.huge, -math.huge, -math.huge + for _, pcmd in dl.CmdBuffer:iter() do + for i = pcmd.IdxOffset + 1, pcmd.IdxOffset + pcmd.ElemCount do + local o = (pcmd.VtxOffset + dl.IdxBuffer.Data[i] - 1) * G.VTX_STRIDE + if buffer.readu32(vb, o + 32) == 0xFFFFFFFF then + local x, y = buffer.readf64(vb, o), buffer.readf64(vb, o + 8) + x0, y0, x1, y1 = math.min(x0, x), math.min(y0, y), math.max(x1, x), math.max(y1, y) + end + end + end + assert(not (x1 - x0 > 100 and y1 - y0 > 100), "white fill on frame " .. frame) + end + if dd.TotalVtxCount == 0 then + G.ImGui_ImplRoblox.RenderDrawData(dd) + for _, set in io.BackendPlatformUserData.Layers do + for kind = 1, 3 do assert(not (set[kind] and set[kind].Visible), "backend layer still visible after close") end + end + print("DOCK CLOSE OK after", frame - closed_at + 1, "frames") + done = true + return + else + assert(frame < closed_at + 3, "docked windows still drawn " .. (frame - closed_at + 1) .. " frames after close") + end + end + G.ImGui_ImplRoblox.RenderDrawData(dd) +end)