diff --git a/build/ImGui.linemap.json b/build/ImGui.linemap.json index ed71a5d..7da51a3 100644 --- a/build/ImGui.linemap.json +++ b/build/ImGui.linemap.json @@ -1 +1 @@ -[[12, "imgui.lua"], [13741, "imgui_demo.lua"], [14674, "imgui_demo_2.lua"], [17749, "imgui_demo_apps.lua"], [19008, "imgui_demo_tools.lua"], [20334, "imgui_demo_widgets.lua"], [22258, "imgui_docking.lua"], [25386, "imgui_dragdrop.lua"], [25678, "imgui_draw.lua"], [30866, "imgui_h.lua"], [33079, "imgui_impl_roblox.lua"], [33903, "imgui_impl_roblox_fonts.lua"], [40306, "imgui_internal.lua"], [43476, "imgui_tables.lua"], [47823, "imgui_tabs.lua"], [48990, "imgui_widgets.lua"], [57514, "imstb_rectpack.lua"], [57960, "imstb_textedit.lua"], [59201, "imstb_truetype.lua"], [61669, "imstd_minstdio.lua"]] \ No newline at end of file +[[12, "imgui.lua"], [13741, "imgui_demo.lua"], [14674, "imgui_demo_2.lua"], [17749, "imgui_demo_apps.lua"], [19008, "imgui_demo_tools.lua"], [20334, "imgui_demo_widgets.lua"], [22258, "imgui_docking.lua"], [25386, "imgui_dragdrop.lua"], [25678, "imgui_draw.lua"], [30900, "imgui_h.lua"], [33113, "imgui_impl_roblox.lua"], [33937, "imgui_impl_roblox_fonts.lua"], [40340, "imgui_internal.lua"], [43510, "imgui_tables.lua"], [47857, "imgui_tabs.lua"], [49024, "imgui_widgets.lua"], [57548, "imstb_rectpack.lua"], [57994, "imstb_textedit.lua"], [59235, "imstb_truetype.lua"], [61703, "imstd_minstdio.lua"]] \ No newline at end of file diff --git a/build/ImGui.luau b/build/ImGui.luau index 9a1f60c..6d7df7e 100644 --- a/build/ImGui.luau +++ b/build/ImGui.luau @@ -28289,7 +28289,25 @@ end --- @param flags ImDrawTextFlags --- @return ImVec2 text_size --- @return int remaining +local TEXT_SIZE_CACHE_MAX = 4096 + function ImFontCalcTextSizeEx(font, size, max_width, wrap_width, text, text_begin, text_end_display, text_end, out_offset, flags) + -- [Luau] cache for the common case (whole string, single pass, no wrap/limit): per baked font, text -> unscaled width, line count. + -- Advances of a baked font never change once loaded, so entries stay valid. ponytail: whole-cache reset at TEXT_SIZE_CACHE_MAX entries, LRU if dynamic text churns it + local cache_key = nil + if text_begin == 1 and out_offset == nil and wrap_width <= 0.0 and max_width >= FLT_MAX and type(text) == "string" + and (text_end == nil or text_end == #text + 1) and (text_end_display == nil or text_end_display == #text + 1) and (flags == nil or bit32.band(flags, ImDrawTextFlags.StopOnNewLine) == 0) then + local baked = font:GetFontBaked(size) + local cache = baked._TextSizeCache + if cache == nil or baked._TextSizeCacheN >= TEXT_SIZE_CACHE_MAX then + cache = {}; baked._TextSizeCache = cache; baked._TextSizeCacheN = 0 + end + local e = cache[text] + if e then + return ImVec2(e[1] * (size / baked.Size), e[2] * size), #text + 1 + end + cache_key = text + end if not text_end then text_end = #text + 1 end @@ -28389,6 +28407,11 @@ function ImFontCalcTextSizeEx(font, size, max_width, wrap_width, text, text_begi local out_remaining = s + if cache_key then + baked._TextSizeCache[cache_key] = { text_size.x / scale, text_size.y / line_height } + baked._TextSizeCacheN += 1 + end + return text_size, out_remaining end @@ -29737,6 +29760,17 @@ function MT.ImDrawList:AddRect(p_min, p_max, col, rounding, thickness, flags) if bit32.band(col, IM_COL32_A_MASK) == 0 then return end + -- [Luau] square corners + whole-pixel thickness: 4 filled rects instead of a stroked path (same pixels, far cheaper here) + if (rounding == nil or rounding < 0.5 or bit32.band(flags, ImDrawFlags.RoundCornersMask_) == ImDrawFlags.RoundCornersNone) + and thickness >= 1.0 and thickness % 1 == 0 and p_max.x - p_min.x > 2 * thickness and p_max.y - p_min.y > 2 * thickness then + local x0, y0, x1, y1, t = p_min.x, p_min.y, p_max.x, p_max.y, thickness + self:PrimReserve(24, 16) + self:PrimRect(ImVec2(x0, y0), ImVec2(x1, y0 + t), col) + self:PrimRect(ImVec2(x0, y1 - t), ImVec2(x1, y1), col) + self:PrimRect(ImVec2(x0, y0 + t), ImVec2(x0 + t, y1 - t), col) + self:PrimRect(ImVec2(x1 - t, y0 + t), ImVec2(x1, y1 - t), col) + return + end if bit32.band(self.Flags, ImDrawListFlags.AntiAliasedLines) ~= 0 then self:PathRect(p_min + ImVec2(0.50, 0.50), p_max - ImVec2(0.50, 0.50), rounding, flags) else diff --git a/imgui/imgui_draw.lua b/imgui/imgui_draw.lua index 5104b14..af14945 100644 --- a/imgui/imgui_draw.lua +++ b/imgui/imgui_draw.lua @@ -2612,7 +2612,25 @@ end --- @param flags ImDrawTextFlags --- @return ImVec2 text_size --- @return int remaining +local TEXT_SIZE_CACHE_MAX = 4096 + function ImFontCalcTextSizeEx(font, size, max_width, wrap_width, text, text_begin, text_end_display, text_end, out_offset, flags) + -- [Luau] cache for the common case (whole string, single pass, no wrap/limit): per baked font, text -> unscaled width, line count. + -- Advances of a baked font never change once loaded, so entries stay valid. ponytail: whole-cache reset at TEXT_SIZE_CACHE_MAX entries, LRU if dynamic text churns it + local cache_key = nil + if text_begin == 1 and out_offset == nil and wrap_width <= 0.0 and max_width >= FLT_MAX and type(text) == "string" + and (text_end == nil or text_end == #text + 1) and (text_end_display == nil or text_end_display == #text + 1) and (flags == nil or bit32.band(flags, ImDrawTextFlags.StopOnNewLine) == 0) then + local baked = font:GetFontBaked(size) + local cache = baked._TextSizeCache + if cache == nil or baked._TextSizeCacheN >= TEXT_SIZE_CACHE_MAX then + cache = {}; baked._TextSizeCache = cache; baked._TextSizeCacheN = 0 + end + local e = cache[text] + if e then + return ImVec2(e[1] * (size / baked.Size), e[2] * size), #text + 1 + end + cache_key = text + end if not text_end then text_end = #text + 1 end @@ -2712,6 +2730,11 @@ function ImFontCalcTextSizeEx(font, size, max_width, wrap_width, text, text_begi local out_remaining = s + if cache_key then + baked._TextSizeCache[cache_key] = { text_size.x / scale, text_size.y / line_height } + baked._TextSizeCacheN += 1 + end + return text_size, out_remaining end @@ -4060,6 +4083,17 @@ function MT.ImDrawList:AddRect(p_min, p_max, col, rounding, thickness, flags) if bit32.band(col, IM_COL32_A_MASK) == 0 then return end + -- [Luau] square corners + whole-pixel thickness: 4 filled rects instead of a stroked path (same pixels, far cheaper here) + if (rounding == nil or rounding < 0.5 or bit32.band(flags, ImDrawFlags.RoundCornersMask_) == ImDrawFlags.RoundCornersNone) + and thickness >= 1.0 and thickness % 1 == 0 and p_max.x - p_min.x > 2 * thickness and p_max.y - p_min.y > 2 * thickness then + local x0, y0, x1, y1, t = p_min.x, p_min.y, p_max.x, p_max.y, thickness + self:PrimReserve(24, 16) + self:PrimRect(ImVec2(x0, y0), ImVec2(x1, y0 + t), col) + self:PrimRect(ImVec2(x0, y1 - t), ImVec2(x1, y1), col) + self:PrimRect(ImVec2(x0, y0 + t), ImVec2(x0 + t, y1 - t), col) + self:PrimRect(ImVec2(x1 - t, y0 + t), ImVec2(x1, y1 - t), col) + return + end if bit32.band(self.Flags, ImDrawListFlags.AntiAliasedLines) ~= 0 then self:PathRect(p_min + ImVec2(0.50, 0.50), p_max - ImVec2(0.50, 0.50), rounding, flags) else diff --git a/tools/test/prof.py b/tools/test/prof.py new file mode 100644 index 0000000..5a4e9e9 --- /dev/null +++ b/tools/test/prof.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# Summarise profile.out from `run.py --profile`: top functions by self and inclusive samples, mapped to imgui/*.lua lines +import collections, json, os, re, sys, glob +path = sys.argv[1] if len(sys.argv) > 1 else "profile.out" +tmp = max(glob.glob("/tmp/imgui_test_*/ImGui.linemap.json"), key=os.path.getmtime) +lm = json.load(open(tmp)) +def loc(n): + if not n.isdigit(): return "C" + n = int(n) - 5 + 2; best = None + for start, name in lm: + if start <= n: best = (start, name) + return "%s:%d" % (best[1], n - best[0] + 1) if best else "?" +self_, incl, total = collections.Counter(), collections.Counter(), 0 +for line in open(path): + cnt, stack = line.split(" ", 1); cnt = int(cnt); total += cnt + frames = [f.rsplit(",", 2) for f in stack.strip().split(";")] + keys = ["%s (%s)" % (f[1] or "?", loc(f[2])) for f in frames] + self_[keys[0]] += cnt + for k in set(keys): incl[k] += cnt +for title, c in (("SELF", self_), ("INCLUSIVE", incl)): + print("== %s (%% of %d)" % (title, total)) + for k, v in c.most_common(int(os.environ.get("N", 30))): print("%5.1f%% %s" % (100 * v / total, k)) diff --git a/tools/test/run.py b/tools/test/run.py index f121434..d0cb055 100644 --- a/tools/test/run.py +++ b/tools/test/run.py @@ -33,7 +33,7 @@ def build_run(args, flags): def run(args, extra=""): flags = "ARG_FRAMES=%d; ARG_BIG=%s; ARG_OPEN=%s; ARG_QUIET=%s; %s" % (args.frames, str(args.big).lower(), str(args.open).lower(), str(args.quiet).lower(), extra) try: - p = subprocess.run([LUAU, "-O2", "--codegen", build_run(args, flags)], capture_output=True, text=True, timeout=args.timeout) + p = subprocess.run([LUAU, "-O2"] + (["--profile=2000"] if args.profile else ["--codegen"]) + [build_run(args, flags)], capture_output=True, text=True, timeout=args.timeout) except subprocess.TimeoutExpired as e: return 124, "TIMEOUT (infinite loop?)\n" + remap((e.stdout or b"").decode(errors="replace")) return p.returncode, remap(p.stdout + p.stderr) @@ -58,6 +58,7 @@ def main(): ap.add_argument("--frames", type=int, default=400) ap.add_argument("--main", default=os.path.join(ROOT, "main.client.luau")) ap.add_argument("--big", action="store_true", help="demo window forced to 900x1000") + ap.add_argument("--profile", action="store_true", help="sampling profile (no codegen) -> profile.out; see tools/test/prof.py") ap.add_argument("--open", action="store_true", help="force all CollapsingHeader/TreeNode open") ap.add_argument("--timeout", type=int, default=120) ap.add_argument("--quiet", action="store_true", help="no random input (scripted tests drive io themselves)")