perf(ui): build nodes without per-node marshalling tables

build() copied a node's children into a second table so it could nil them
out of the spec, and applyStyle() copied the style keys into a third. Both
existed only to hand C a table it was already holding: tree.create reads
named fields and never touches the array part, so children come straight
off the spec, and create now applies the style itself. That is one C call
per styled node instead of two, and the sparse-style decision is made by
the set mask rather than by a loop over key names in Lua.

Measured in the emulator, 12 sensors / 131 nodes: build 106ms -> 74ms, Lua
transient +18.1kB -> +12.0kB, free heap at build end 18572 -> 27936. Tree
footprint is byte-identical, so the same styles are applied. ui.lua stripped
bytecode 10024 -> 9621 bytes, saved in every app state.

An explicit fill now wins over background, where the Lua version had
background clobber it; no caller sets both.
This commit is contained in:
2026-08-08 11:31:06 -04:00
parent 5cec396697
commit 2104a91fc3
3 changed files with 58 additions and 66 deletions
+4 -36
View File
@@ -163,34 +163,6 @@ tree.setPainter(function(id, x, y, w, h, clipX, clipY, clipW, clipH)
end
end)
local STYLE_KEYS = {
"color",
"fill",
"border",
"face",
"pressedFace",
"pressedColor",
"focusColor",
"radius",
"font",
"textStyle",
}
local function applyStyle(id, spec)
local style, hasStyle = {}, false
for _, key in ipairs(STYLE_KEYS) do
if spec[key] ~= nil then
style[key], hasStyle = spec[key], true
end
end
if spec.background ~= nil then
style.background, style.fill, hasStyle = spec.background, spec.background, true
end
if hasStyle then
tree.setStyle(id, style)
end
end
local function build(spec, kind)
spec = spec or {}
-- Nodes outside a build would reset the arena under the screen already on the
@@ -199,20 +171,16 @@ local function build(spec, kind)
error("build nodes from the function ui.mount() was given, then ui.rebuild()", 3)
end
local children = {}
for index, child in ipairs(spec) do
children[index] = child
spec[index] = nil
end
spec.type = kind
spec.interactive = spec.on_enter ~= nil or spec.on_exit ~= nil or spec.on_click ~= nil
-- Children come straight off the spec's array part. tree.create reads only named
-- fields, so copying them into a second table cost one extra Lua table per node
-- -- roughly half again the spec's own cost, on the build path that peaks the heap.
local id = tree.create(nil, spec)
for _, child in ipairs(children) do
for _, child in ipairs(spec) do
tree.attach(id, child)
end
applyStyle(id, spec)
enterHandlers[id] = spec.on_enter
exitHandlers[id] = spec.on_exit
clickHandlers[id] = spec.on_click
+53 -29
View File
@@ -144,6 +144,50 @@ int reset(lua_State* state) {
return 0;
}
void readStyleColor(lua_State* state, int index, const char* key,
int32_t& field, uint16_t flag, uint16_t& set) {
lua_getfield(state, index, key);
if (!lua_isnoneornil(state, -1)) {
field = static_cast<int32_t>(luaL_checkinteger(state, -1));
set |= flag;
}
lua_pop(state, 1);
}
// Reads whatever style fields a spec carries, returning whether it named any. The
// Lua wrapper used to do this by copying the style keys into a second table and
// mapping background itself, which cost a table per styled node and a rehash of the
// caller's spec to express the mapping.
bool readStyleInto(lua_State* state, int index, ui::Style& style) {
const uint16_t before = style.set;
readStyleColor(state, index, "color", style.color, ui::S_COLOR, style.set);
readStyleColor(state, index, "background", style.bg, ui::S_BG, style.set);
readStyleColor(state, index, "fill", style.fill, ui::S_FILL, style.set);
readStyleColor(state, index, "border", style.border, ui::S_BORDER, style.set);
readStyleColor(state, index, "face", style.face, ui::S_FACE, style.set);
readStyleColor(state, index, "pressedFace", style.pressedFace,
ui::S_PRESSED_FACE, style.set);
readStyleColor(state, index, "pressedColor", style.pressedColor,
ui::S_PRESSED_COLOR, style.set);
readStyleColor(state, index, "focusColor", style.focusColor,
ui::S_FOCUS_COLOR, style.set);
readStyleColor(state, index, "font", style.font, ui::S_FONT, style.set);
readStyleColor(state, index, "textStyle", style.textStyle, ui::S_TEXT_STYLE,
style.set);
int32_t radius = style.radius;
readStyleColor(state, index, "radius", radius, ui::S_RADIUS, style.set);
style.radius = static_cast<uint8_t>(radius);
// A background is the surface its own corners blend into, so it implies fill.
// An explicit fill wins, which is the way round the Lua version had it backwards.
if ((style.set & ui::S_BG) && !(style.set & ui::S_FILL)) {
style.fill = style.bg;
style.set |= ui::S_FILL;
}
return style.set != before;
}
int create(lua_State* state) {
const bool hasParent = !lua_isnoneornil(state, 1);
const uint16_t parent = hasParent ? checkNode(state, 1) : ui::NONE;
@@ -199,6 +243,14 @@ int create(lua_State* state) {
if (label)
tree(state).setLabel(id, label);
lua_pop(state, 1);
// Styled here rather than by a second call from Lua: the spec is already on the
// stack, and a node naming no colours must cost no Style entry, which is a
// decision the set mask makes better than a loop over key names in Lua.
ui::Style style;
if (readStyleInto(state, 2, style))
tree(state).styleFor(id) = style;
lua_pushinteger(state, id);
return 1;
}
@@ -316,39 +368,11 @@ int getParent(lua_State* state) {
return 1;
}
void readStyleColor(lua_State* state, int index, const char* key,
int32_t& field, uint16_t flag, uint16_t& set) {
lua_getfield(state, index, key);
if (!lua_isnoneornil(state, -1)) {
field = static_cast<int32_t>(luaL_checkinteger(state, -1));
set |= flag;
}
lua_pop(state, 1);
}
int setStyle(lua_State* state) {
const uint16_t id = checkNode(state, 1);
luaL_checktype(state, 2, LUA_TTABLE);
ui::Style& style = tree(state).styleFor(id);
readStyleColor(state, 2, "color", style.color, ui::S_COLOR, style.set);
readStyleColor(state, 2, "background", style.bg, ui::S_BG, style.set);
readStyleColor(state, 2, "fill", style.fill, ui::S_FILL, style.set);
readStyleColor(state, 2, "border", style.border, ui::S_BORDER, style.set);
readStyleColor(state, 2, "face", style.face, ui::S_FACE, style.set);
readStyleColor(state, 2, "pressedFace", style.pressedFace, ui::S_PRESSED_FACE,
style.set);
readStyleColor(state, 2, "pressedColor", style.pressedColor,
ui::S_PRESSED_COLOR, style.set);
readStyleColor(state, 2, "focusColor", style.focusColor, ui::S_FOCUS_COLOR,
style.set);
readStyleColor(state, 2, "font", style.font, ui::S_FONT, style.set);
readStyleColor(state, 2, "textStyle", style.textStyle, ui::S_TEXT_STYLE,
style.set);
int32_t radius = style.radius;
readStyleColor(state, 2, "radius", radius, ui::S_RADIUS, style.set);
style.radius = static_cast<uint8_t>(radius);
readStyleInto(state, 2, tree(state).styleFor(id));
tree(state).nodes[id].flags |= ui::DIRTY;
return 0;
File diff suppressed because one or more lines are too long