WIP: tabs, docking, dragdrop, settings, logging, harness

This commit is contained in:
4DBug
2026-10-08 05:35:25 -05:00
parent 693ced0511
commit fee9a84902
26 changed files with 128896 additions and 1836 deletions
+54
View File
@@ -0,0 +1,54 @@
# Porting rules (Dear ImGui docking branch -> this Lua/Luau port)
Upstream reference: `tools/test/upstream/*.cpp|h` (ocornut/imgui `docking` branch, 1.93 WIP). Port semantics 1:1 from
there, but keep this port's existing data structures and conventions. Read the surrounding existing Lua code first.
## Runtime
- Target is **Luau (Roblox)** only. All files are concatenated into one ModuleScript by `tools/bundle.py`; every
file shares one global namespace. Globals defined by one file are visible to the others at call time.
- Each `imgui/*.lua` file is a function body in the bundle. New files must be loaded with `IM_INCLUDE"file.lua"`
from an existing file (files named `imgui_demo*.lua` are loaded automatically, after `imgui.lua`).
- No `goto`, no `::labels::`, no `setfenv/getfenv`, no LuaJIT `bit`/`ffi`. Use `bit32`. For `continue` use
`repeat ... until true` with `break`, and an explicit flag when the loop itself must break out of the wrapper.
- Luau has a limit of 200 locals per function and 255 upvalues; split huge functions (like demo sections) into
several local/global functions.
## Conventions (follow what the port already does)
- `ImVector`: 1-based `.Data`, `.Size`, methods `push_back/pop_back/resize/back/iter/erase/...` (imgui_h.lua).
Indices that C++ stores as 0-based *values* (e.g. column index, tab order) stay 0-based unless the surrounding
port code already shifted them; document `-- 0-based` / `-- 1-based` next to anything ambiguous.
- `ImVec2(x, y)` stores `[1],[2]` with `.x/.y` aliases (metatable). Arithmetic operators exist. Prefer
`ImVec2_Copy(dst, src)` / `ImVec2_CopyV(dst, x, y)` when C++ assigns into an existing struct field.
`ImVec4`, `ImRect` (`.Min/.Max`) likewise.
- Structs: constructor function `ImGuiFoo()` returning a table, methods on `MT.ImGuiFoo` with
`MT.ImGuiFoo.__index = MT.ImGuiFoo`. `local MT = ImGui.GetMetatables()`.
- Flags/enums: tables like `ImGuiTableFlags.Resizable`; combine with `bit32.bor`, test with `bit32.band(...) ~= 0`.
- Context: `local g = GImGui` (each file has its own local `GImGui` kept in sync; follow how the file you are in
does it). New fields on `ImGuiContext`/`ImGuiWindow`: add them to the constructor in imgui_internal.lua.
- Strings: Lua strings + 1-based byte positions; `text_end` is exclusive. Editable text buffers are `char[]`
(byte tables), like `ImGui.InputText(label, buf, buf_size)`.
- **Output parameters** (C++ `bool* p_open`, `float* v`, `int* current_item`...) become return values:
- bool-like widgets: `return pressed, new_v` (Checkbox, RadioButton, Selectable, MenuItem, CheckboxFlags)
- scalar widgets: `return new_v, changed` (Slider*/Drag*/Input* scalar, SliderAngle, Combo -> `new_idx, changed`)
- multi-component (`float v[3]`, ImVec4 colors, `char[]` buffers): table mutated in place, return `changed`
- `Begin(name, p_open)` style: `return open, visible` / follow existing `ImGui.Begin`.
When an existing similar function already exists, copy its convention exactly.
- `static` locals in C++ become file-level upvalue tables / fields (demo uses a `static` table per section).
- `IM_ASSERT(cond, msg)` exists. `IM_COL32(r,g,b,a)`, `ImMax/ImMin/ImClamp/ImLerp/ImFloor/ImTrunc` etc. exist.
- Keep function names identical to upstream (`ImGui.TableNextRow`, `ImGui.TabBarLayout`, ...). Internal helpers
that are `static` in C++ become `local function` in the file.
- Roblox can't do: OS clipboard (internal only), files/URLs (VFS `ImStd.ImFileLoadToMemory` only), multi-viewports,
gamepad. Port the code paths anyway where cheap; demo shows "(not supported on Roblox)" text instead of
widgets that cannot work.
## Testing
- `python3 tools/test/run.py` — runs main.client.luau 400 frames with scripted random input (mouse, clicks,
keys, typing). Errors print with `imgui/<file>.lua:<line>` mapped stack traces.
- `--open` forces every CollapsingHeader/TreeNode open (exercises the whole demo). `--big` makes the demo
window 900x1000. `--png out.png` writes a screenshot (Read the PNG to look at it). `--check` asserts the
incremental renderer is pixel identical to a full redraw. `--main file.luau` runs your own test LocalScript
(copy main.client.luau as a template: `local G = require(ReplicatedStorage:WaitForChild("ImGui"))`, all
library globals are fields of `G`).
- `python3 tools/test/missing.py [tables|widgets|imgui.cpp]` lists upstream functions not yet ported.
- Several people edit the repo concurrently. Keep every file syntactically valid after each edit. If a run fails
because of an error in a file you don't own, wait a minute and retry instead of editing it.
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env python3
"""Lists upstream ImGui:: functions (tools/test/upstream/*.cpp, docking branch) that the Lua port doesn't define.
python3 tools/test/missing.py # summary per upstream file
python3 tools/test/missing.py tables # names missing from imgui_tables.cpp
"""
import os, re, sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(HERE))
port = set()
for f in os.listdir(os.path.join(ROOT, "imgui")):
if f.endswith(".lua"):
port.update(re.findall(r"function ImGui[.:](\w+)", open(os.path.join(ROOT, "imgui", f)).read()))
filt = sys.argv[1] if len(sys.argv) > 1 else None
for f in sorted(os.listdir(os.path.join(HERE, "upstream"))):
if not f.endswith(".cpp") or f == "imgui_demo.cpp": continue
if filt and filt not in f: continue
src = open(os.path.join(HERE, "upstream", f)).read()
names = sorted(set(re.findall(r"^[\w:<>*& ]+?\bImGui::(\w+)\(", src, re.M)))
miss = [n for n in names if n not in port]
print("%-20s %4d/%4d ported, missing %d" % (f, len(names) - len(miss), len(names), len(miss)))
if filt: print(" " + "\n ".join(miss))
+245
View File
@@ -0,0 +1,245 @@
-- Minimal Roblox mock for running the imgui Roblox backend + the real CanvasDraw module in the Luau CLI.
local realG = {} -- (no getfenv: it would mark the global env unsafe and deoptimize everything)
FAKE_TIME = 0
local SCREEN_W, SCREEN_H = 1920, 1080
local NFRAMES = tonumber(ARG_FRAMES) or 400
local FORCE = ARG_FORCE == true
local DUMP_FRAMES = { [60] = true, [200] = true, [NFRAMES] = true }
local function Signal()
local s = { conns = {} }
function s:Connect(fn)
local c = { fn = fn, Connected = true }
function c:Disconnect() c.Connected = false end
table.insert(s.conns, c)
return c
end
function s:Fire(...) for _, c in ipairs(s.conns) do if c.Connected then c.fn(...) end end end
return s
end
local V2 = {}
V2.__index = V2
local function v2(x, y) return setmetatable({ X = x or 0, Y = y or 0 }, V2) end
V2.__add = function(a, b) return v2(a.X + b.X, a.Y + b.Y) end
V2.__sub = function(a, b) return v2(a.X - b.X, a.Y - b.Y) end
V2.__mul = function(a, b) if type(b) == "number" then return v2(a.X * b, a.Y * b) end return v2(a.X * b.X, a.Y * b.Y) end
V2.__div = function(a, b) if type(b) == "number" then return v2(a.X / b, a.Y / b) end return v2(a.X / b.X, a.Y / b.Y) end
V2.__eq = function(a, b) return a.X == b.X and a.Y == b.Y end
local Vector2 = { new = v2, zero = v2(0, 0), one = v2(1, 1) }
-- Enums: KeyCode values as in Roblox; unknown KeyCode names error (catches typos in the backend)
local KEYCODES = {
Unknown = 0, Backspace = 8, Tab = 9, Return = 13, Escape = 27, Space = 32, Quote = 39, Comma = 44, Minus = 45, Period = 46, Slash = 47,
Semicolon = 59, Equals = 61, LeftBracket = 91, BackSlash = 92, RightBracket = 93, Backquote = 96, Delete = 127,
KeypadPeriod = 266, KeypadDivide = 267, KeypadMultiply = 268, KeypadMinus = 269, KeypadPlus = 270, KeypadEnter = 271, KeypadEquals = 272,
Up = 273, Down = 274, Right = 275, Left = 276, Insert = 277, Home = 278, End = 279, PageUp = 280, PageDown = 281,
CapsLock = 301, RightShift = 303, LeftShift = 304, RightControl = 305, LeftControl = 306, RightAlt = 307, LeftAlt = 308,
LeftSuper = 311, RightSuper = 312,
}
local digits = { "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine" }
for i = 0, 9 do KEYCODES[digits[i + 1]] = 48 + i; KEYCODES["Keypad" .. digits[i + 1]] = 256 + i end
for i = 0, 25 do KEYCODES[string.char(65 + i)] = 97 + i end
for i = 1, 15 do KEYCODES["F" .. i] = 281 + i end
local Enum = setmetatable({}, { __index = function(t, enum_name)
local e = setmetatable({}, { __index = function(e, item)
local value = 0
if enum_name == "KeyCode" then value = KEYCODES[item]; assert(value, "invalid KeyCode " .. item) end
local it = { Name = item, Value = value, EnumType = enum_name }
rawset(e, item, it)
return it
end })
rawset(t, enum_name, e)
return e
end })
-- Instances
local InstMethods = {}
local InstMT = {}
InstMT.__index = function(t, k)
if k == "Parent" then return rawget(t, "_parent") end
local m = InstMethods[k]; if m then return m end
for _, c in ipairs(rawget(t, "_children")) do if c.Name == k then return c end end
return nil
end
InstMT.__newindex = function(t, k, v)
if k == "Parent" then
local old = rawget(t, "_parent")
if old then local ch = rawget(old, "_children"); for i, c in ipairs(ch) do if c == t then table.remove(ch, i) break end end end
rawset(t, "_parent", v)
if v then table.insert(rawget(v, "_children"), t) end
else
rawset(t, k, v)
end
end
local function newInst(class, name, parent)
local o = setmetatable({ ClassName = class, Name = name or class, _children = {} }, InstMT)
if class == "ScreenGui" or class == "Frame" or class == "ImageLabel" then o.AbsoluteSize = v2(SCREEN_W, SCREEN_H) end
o.Visible = true
if class == "BindableEvent" then local sig = Signal(); rawset(o, "Event", sig); rawset(o, "Fire", function(_, ...) sig:Fire(...) end) end
if parent then o.Parent = parent end
return o
end
function InstMethods:FindFirstChild(name, recursive)
for _, c in ipairs(rawget(self, "_children")) do if c.Name == name then return c end end
if recursive then for _, c in ipairs(rawget(self, "_children")) do local r = c:FindFirstChild(name, true); if r then return r end end end
return nil
end
function InstMethods:WaitForChild(name) return assert(self:FindFirstChild(name), "WaitForChild would hang: " .. name) end
function InstMethods:FindFirstChildOfClass(class) for _, c in ipairs(rawget(self, "_children")) do if c.ClassName == class then return c end end end
function InstMethods:GetChildren() return table.clone(rawget(self, "_children")) end
function InstMethods:IsA(class)
if class == "GuiObject" then return self.ClassName == "Frame" or self.ClassName == "ImageLabel" end
return self.ClassName == class
end
function InstMethods:Destroy() self.Parent = nil end
-- Services
local mouse = v2(0, 0)
local keys_down = {}
local UIS = {
InputBegan = Signal(), InputEnded = Signal(), InputChanged = Signal(), WindowFocused = Signal(), WindowFocusReleased = Signal(),
}
function UIS:GetMouseLocation() return mouse end
function UIS:IsKeyDown(kc) return keys_down[kc] == true end
local RunService = { Heartbeat = Signal(), Stepped = Signal(), PreAnimation = Signal(), RenderStepped = Signal() }
local last_pixels -- last buffer written to an EditableImage
local AssetService = {}
function AssetService:CreateEditableImage(opts)
local img = { Size = opts.Size, Pixels = buffer.create(opts.Size.X * opts.Size.Y * 4) }
UPLOADS = 0
function img:WritePixelsBuffer(origin, size, buf) buffer.copy(img.Pixels, 0, buf); _G_UPLOADS = (_G_UPLOADS or 0) + 1 end
function img:Destroy() end
return img
end
local ReplicatedStorage = newInst("ReplicatedStorage")
local PlayerGui = newInst("PlayerGui")
local LocalPlayer = newInst("Player"); PlayerGui.Parent = LocalPlayer
local services = {
UserInputService = UIS, RunService = RunService, AssetService = AssetService, ReplicatedStorage = ReplicatedStorage,
Players = { LocalPlayer = LocalPlayer }, HttpService = {}, GuiService = { GetGuiInset = function() return v2(0, 0), v2(0, 0) end },
}
local game = {}
function game:GetService(name) services[name] = services[name] or {}; return services[name] end
local fakeG = {} -- Roblox's shared _G
local cache = {}
local ROBLOX
local function makeEnv(inst) return setmetatable({ script = inst }, { __index = ROBLOX }) end
local GLOBAL_NAMES = { "game", "workspace", "Instance", "Enum", "Vector2", "Vector3", "UDim2", "Color3", "Content", "warn", "require", "script", "os" }
-- Load like Roblox does but WITHOUT setfenv (which would disable Luau optimizations): Roblox globals become chunk locals.
local function loadAsScript(src_name, inst)
local fn = SOURCE_FNS[src_name]
local vals = {}
for i, n in ipairs(GLOBAL_NAMES) do vals[i] = (n == "script") and inst or ROBLOX[n] end
return function() return fn(table.unpack(vals, 1, #GLOBAL_NAMES)) end
end
local function rbxrequire(inst)
assert(type(inst) == "table" and inst.ClassName == "ModuleScript", "require() expects a ModuleScript")
if cache[inst] then return cache[inst].v end
local r = table.pack(loadAsScript(rawget(inst, "_src"), inst)())
assert(r.n == 1, "Module code did not return exactly one value")
cache[inst] = { v = r[1] }
return r[1]
end
ROBLOX = setmetatable({
game = game, workspace = { CurrentCamera = { ViewportSize = v2(SCREEN_W, SCREEN_H) } }, require = rbxrequire, _G = fakeG,
Instance = { new = function(class) return newInst(class) end },
Enum = Enum, Vector2 = Vector2,
Vector3 = { new = function(x, y, z) return { X = x, Y = y, Z = z } end },
UDim2 = { new = function(...) return { ... } end, fromScale = function(...) return { ... } end, fromOffset = function(x, y) return { X = x, Y = y } end },
Color3 = { new = function(r, g, b) return { R = r or 0, G = g or 0, B = b or 0 } end },
Content = { fromObject = function(o) return { Object = o } end },
warn = function(...) print("WARN", ...) end,
os = { clock = function() return FAKE_TIME end, time = os.time },
}, { __index = realG })
local mod = newInst("ModuleScript", "ImGui", ReplicatedStorage); rawset(mod, "_src", "ImGui.luau")
-- Run main.lua as a LocalScript
local main = loadAsScript("main.client.luau", newInst("LocalScript", "main"))
main()
local G = cache[mod].v
local render_time = 0
local realDemo = G.ImGui.ShowDemoWindow
local realCH = G.ImGui.CollapsingHeader
G.ImGui.CollapsingHeader = function(...)
if ARG_OPEN then G.ImGui.SetNextItemOpen(true, G.ImGuiCond.Always) end
return realCH(...)
end
local realTN = G.ImGui.TreeNode
G.ImGui.TreeNode = function(...)
if ARG_OPEN then G.ImGui.SetNextItemOpen(true, G.ImGuiCond.Always) end
return realTN(...)
end
G.ImGui.ShowDemoWindow = function(...)
if ARG_BIG then G.ImGui.SetNextWindowSize(G.ImVec2(900, 1000), G.ImGuiCond.Always) end
return realDemo(...)
end
local realRender = G.ImGui_ImplRoblox.RenderDrawData
G.ImGui_ImplRoblox.RenderDrawData = function(dd)
if FORCE then G.ImGui.GetIO().BackendPlatformUserData.ForceRedraw = true end
local t0 = os.clock(); realRender(dd); render_time = render_time + os.clock() - t0
end
-- Drive frames with scripted (deterministic) input
local seed = 12345
local function rnd(n) seed = (seed * 1103515245 + 12345) % 2147483648; return seed % n end
local function input(t, kc, pos) return { UserInputType = Enum.UserInputType[t], KeyCode = Enum.KeyCode[kc or "Unknown"], Position = pos or { X = 0, Y = 0, Z = 0 } } end
local held = false
local typing = { "H", "E", "L", "L", "O", "Space", "One", "Two", "Minus", "Backspace", "Left", "Return" }
local hex = {}
for i = 0, 255 do hex[i] = string.format("%02x", i) end
local total = 0
for frame = 1, NFRAMES do
local r = rnd(100)
if r < 40 then
mouse = v2(math.clamp(mouse.X + rnd(81) - 40, 0, SCREEN_W - 1), math.clamp(mouse.Y + rnd(81) - 40, 0, SCREEN_H - 1))
elseif r < 55 then
held = not held
if held then UIS.InputBegan:Fire(input("MouseButton1"), false) else UIS.InputEnded:Fire(input("MouseButton1"), false) end
elseif r < 60 then
UIS.InputChanged:Fire(input("MouseWheel", nil, { X = 0, Y = 0, Z = rnd(3) - 1 }), false)
elseif r < 75 then
local k = typing[rnd(#typing) + 1]
local shift = rnd(4) == 0
if shift then keys_down[Enum.KeyCode.LeftShift] = true; UIS.InputBegan:Fire(input("Keyboard", "LeftShift"), false) end
UIS.InputBegan:Fire(input("Keyboard", k), false)
UIS.InputEnded:Fire(input("Keyboard", k), false)
if shift then keys_down[Enum.KeyCode.LeftShift] = nil; UIS.InputEnded:Fire(input("Keyboard", "LeftShift"), false) end
elseif r < 90 then
mouse = v2(rnd(700), rnd(500))
end
FAKE_TIME = frame / 60
local t0 = os.clock()
RunService.Stepped:Fire(frame / 60, 1 / 60)
local dt = os.clock() - t0
if frame > 10 then total = total + dt end
if frame == 10 then render_time = 0; _G_UPLOADS = 0 end
if DUMP_FRAMES[frame] and ARG_DUMP then
local W, H = SCREEN_W, SCREEN_H
local screen = buffer.create(W * H * 4)
local gui = PlayerGui:FindFirstChild("ImGui")
for _, label in ipairs(gui:GetChildren()) do
if label.Visible then
local img, px, py = label.ImageContent.Object, label.Position.X, label.Position.Y
local tw, th = img.Size.X, img.Size.Y
for y = 0, th - 1 do
if py + y < H then
local n = math.min(tw, W - px)
buffer.copy(screen, ((py + y) * W + px) * 4, img.Pixels, y * tw * 4, n * 4)
end
end
end
end
local parts = {}
for i = 0, W * H * 4 - 1 do parts[#parts + 1] = hex[buffer.readu8(screen, i)] end
print(string.format("IMG %d %d %d %s", frame, W, H, table.concat(parts)))
end
end
print(string.format("avg frame total %.2f ms, of which render %.2f ms, uploads/frame %.2f", total / (NFRAMES - 10) * 1000, render_time / (NFRAMES - 10) * 1000, _G_UPLOADS / (NFRAMES - 10)))
+36490
View File
File diff suppressed because it is too large Load Diff
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Headless test harness: runs build/ImGui.luau + a main script in the Luau CLI against a mocked Roblox
(roblox_mock.luau). Rebuilds the bundle first.
python3 tools/test/run.py # 400 frames of main.client.luau, scripted random input
python3 tools/test/run.py --open # force every CollapsingHeader/TreeNode open (exercises whole demo)
python3 tools/test/run.py --big --open --bench # timing, best of 5
python3 tools/test/run.py --check # incremental vs forced redraw must be pixel identical
python3 tools/test/run.py --png out.png # screenshot of the last frame
python3 tools/test/run.py --main my_test.luau # different LocalScript
"""
import argparse, os, shutil, subprocess, sys, zlib, struct, tempfile
TMP = tempfile.mkdtemp(prefix="imgui_test_") # per process: parallel runs must not share files
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(HERE))
LUAU = shutil.which("luau") or "/nix/store/2m2ja613nndihc8qm10f2rhq6znp1nh4-luau-0.734/bin/luau"
G = "game, workspace, Instance, Enum, Vector2, Vector3, UDim2, Color3, Content, warn, require, script, os"
def build_run(args, flags):
subprocess.run([sys.executable, os.path.join(ROOT, "tools", "bundle.py"), TMP], check=True, stdout=subprocess.DEVNULL)
out = ["--!native", "--!optimize 2", flags, "local SOURCE_FNS = {}"]
def add(key, path):
s = "\n".join(l for l in open(path).read().split("\n") if not l.startswith("--!"))
out.append("SOURCE_FNS[%r] = function(%s)\n%s\nend" % (key, G, s))
add("ImGui.luau", os.path.join(TMP, "ImGui.luau"))
add("main.client.luau", args.main)
out.append(open(os.path.join(HERE, "roblox_mock.luau")).read())
global OFFSET, LAST_LINE
LAST_LINE = open(os.path.join(TMP, "ImGui.luau")).read().count("\n")
OFFSET = 5 # lines before the ImGui function body in run.luau (flags/header lines + "function(...)" line)
path = os.path.join(TMP, "run.luau")
open(path, "w").write("\n".join(out))
return path
def run(args, extra=""):
flags = "ARG_FRAMES=%d; ARG_BIG=%s; ARG_OPEN=%s; %s" % (args.frames, str(args.big).lower(), str(args.open).lower(), extra)
try:
p = subprocess.run([LUAU, "-O2", "--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)
def remap(text):
"""run.luau:N -> imgui/<file>.lua:M"""
import json, re
lm = json.load(open(os.path.join(TMP, "ImGui.linemap.json")))
def sub(m):
n = int(m.group(1)) - OFFSET # line inside build/ImGui.luau (minus its 2 stripped --! lines)
n += 2
best = None
if n > LAST_LINE: return "<harness>:%d" % n
for start, name in lm:
if start <= n: best = (start, name)
if not best: return m.group(0)
return "%s:%d" % (best[1], n - best[0] + 1)
return re.sub(r"\S*run\.luau:(\d+)", sub, text)
def images(out):
return [l.split() for l in out.split("\n") if l.startswith("IMG ")]
def write_png(img, path, crop=None):
_, fr, w, h, hx = img; w, h = int(w), int(h); px = bytes.fromhex(hx)
W, H = crop or (w, h)
raw = bytearray()
for y in range(H):
raw.append(0)
for x in range(W):
o = (y * w + x) * 4; r, g, b, a = px[o:o + 4]; k = a / 255
raw += bytes([int(r * k + 90 * (1 - k)), int(g * k + 90 * (1 - k)), int(b * k + 90 * (1 - k))])
def chunk(t, d): return struct.pack('>I', len(d)) + t + d + struct.pack('>I', zlib.crc32(t + d) & 0xffffffff)
open(path, 'wb').write(b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', W, H, 8, 2, 0, 0, 0))
+ chunk(b'IDAT', zlib.compress(bytes(raw))) + chunk(b'IEND', b''))
def main():
ap = argparse.ArgumentParser()
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("--open", action="store_true", help="force all CollapsingHeader/TreeNode open")
ap.add_argument("--bench", action="store_true")
ap.add_argument("--check", action="store_true")
ap.add_argument("--png")
ap.add_argument("--crop", default="1100x1080")
ap.add_argument("--timeout", type=int, default=120)
args = ap.parse_args()
if args.bench:
for mode in ["", "ARG_FORCE=true"]:
best = None
for _ in range(5):
code, out = run(args, mode)
if code: print(out[-3000:]); sys.exit(1)
line = [l for l in out.split("\n") if l.startswith("avg frame")][-1]
if best is None or float(line.split()[3]) < float(best.split()[3]): best = line
print("%-12s %s" % (mode or "incremental", best))
return
if args.check:
code, a = run(args, "ARG_DUMP=true")
code2, b = run(args, "ARG_DUMP=true; ARG_FORCE=true")
if code or code2: print((a if code else b)[-3000:]); sys.exit(1)
ok = [x[4] for x in images(a)] == [x[4] for x in images(b)]
print("PIXELS-IDENTICAL" if ok else "PIXELS-DIFFER"); sys.exit(0 if ok else 1)
code, out = run(args, "ARG_DUMP=true" if args.png else "")
print("\n".join(l if not l.startswith("IMG ") else l[:40] + "..." for l in out.split("\n"))[-4000:])
if args.png and images(out):
write_png(images(out)[-1], args.png, tuple(map(int, args.crop.split("x"))))
print("wrote", args.png)
sys.exit(code)
try:
main()
finally:
shutil.rmtree(TMP, ignore_errors=True)
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""Static check: ImGui.X / ImGui:X / global ImFoo() calls in imgui/*.lua that nothing defines.
python3 tools/test/undefined.py
"""
import os, re, collections
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
SRC = os.path.join(ROOT, "imgui")
files = {}
for dp, _, fn in os.walk(SRC):
for f in fn:
if f.endswith(".lua"):
files[os.path.relpath(os.path.join(dp, f), SRC)] = open(os.path.join(dp, f)).read()
def strip(s): # drop comments and strings (roughly) so we don't report text
s = re.sub(r"--\[(=*)\[.*?\]\1\]", "", s, flags=re.S)
s = re.sub(r"\[(=*)\[.*?\]\1\]", '""', s, flags=re.S)
s = re.sub(r'"(\\.|[^"\\\n])*"', '""', s)
s = re.sub(r"'(\\.|[^'\\\n])*'", "''", s)
return re.sub(r"--[^\n]*", "", s)
defined_imgui, defined_global, used = set(), set(), collections.defaultdict(list)
for name, src in files.items():
s = strip(src)
defined_imgui.update(re.findall(r"function\s+ImGui[.:](\w+)", s))
defined_imgui.update(re.findall(r"\bImGui\.(\w+)\s*=[^=]", s))
defined_global.update(re.findall(r"^\s*function\s+([A-Za-z_]\w*)\s*\(", s, re.M))
defined_global.update(re.findall(r"^\s*([A-Za-z_]\w*)\s*=[^=]", s, re.M))
defined_global.update(re.findall(r"^\s*local\s+function\s+([A-Za-z_]\w*)", s, re.M))
defined_global.update(re.findall(r"^\s*local\s+([A-Za-z_][\w, ]*)", s, re.M) and
[x.strip() for l in re.findall(r"^\s*local\s+([A-Za-z_][\w, ]*)", s, re.M) for x in l.split(",")])
for i, line in enumerate(s.split("\n"), 1):
for m in re.findall(r"\bImGui\.(\w+)\s*\(", line):
used[("ImGui." + m, m, True)].append("%s:%d" % (name, i))
for m in re.findall(r"(?<![.:\w])(Im[A-Z]\w*)\s*\(", line):
used[(m, m, False)].append("%s:%d" % (name, i))
missing = [(k, v) for k, v in used.items() if (k[1] not in defined_imgui if k[2] else k[1] not in defined_global)]
for (full, _, _), locs in sorted(missing):
print("%-45s %d uses, e.g. %s" % (full, len(locs), ", ".join(locs[:3])))
print("total undefined:", len(missing))
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff