WIP: tabs, docking, dragdrop, settings, logging, harness
This commit is contained in:
@@ -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)))
|
||||
Reference in New Issue
Block a user