gui framework

This commit is contained in:
Richard Ward
2025-12-19 02:56:59 +00:00
parent eb95a296ec
commit 302b2e49a8
15 changed files with 1861 additions and 2 deletions

View File

@@ -19,7 +19,7 @@ find_package(CLI11 CONFIG REQUIRED)
find_package(rapidjson CONFIG REQUIRED)
if(BUILD_SDL3_APP)
add_executable(sdl3_app
add_executable(sdl3_app
src/main.cpp
src/app/sdl3_app_core.cpp
src/app/sdl3_app_device.cpp
@@ -27,6 +27,7 @@ if(BUILD_SDL3_APP)
src/app/sdl3_app_pipeline.cpp
src/app/sdl3_app_build.cpp
src/app/sdl3_app_buffers.cpp
src/gui/gui_renderer.cpp
src/app/sdl3_app_render.cpp
src/app/vulkan_api.cpp
src/script/cube_script.cpp

View File

@@ -33,6 +33,9 @@ If you need the Conan runtime environment (e.g., because dependencies set env va
`main.cpp` now uses a JSON-driven entrypoint. Use `sdl3_app --json-file-in <path>` to load a configuration that points at the Lua script and captures window dimensions, or run `sdl3_app --create-seed-json config/seed_runtime.json` to write a starter JSON file (based on the executables `scripts/cube_logic.lua` location). You can also use `sdl3_app --set-default-json` (optionally followed by an existing JSON path) to copy the runtime JSON to the platform default directory (APPDATA on Windows, `XDG_CONFIG_HOME`/`$HOME/.config` elsewhere); when that default file exists, the app picks it up automatically when launched without extra CLI options. If no JSON input is provided and no default exists, the app falls back to discovering `scripts/cube_logic.lua` next to the binary.
### GUI Demo
`scripts/gui_demo.lua` shows off the Lua GUI framework (buttons, text boxes, list views, and SVG icons) rendered on top of the Vulkan scene. Launch it with `./build/sdl3_app --json-file-in config/gui_runtime.json` (or use the new config as input to `sdl3_app --set-default-json`) to run the interactive overlay in the window.
## Dependency automation
This project ships a `renovate.json` configuration so Renovate can open PRs that bump the Conan packages listed in `conanfile.py`. Either install Renovate locally (`npm install -g renovate` or `npx renovate`) and run it from the repo root, or enable the Renovate bot on your GitHub/GitLab install to pick up the configuration automatically.

12
config/gui_runtime.json Normal file
View File

@@ -0,0 +1,12 @@
{
window_width: 1024,
window_height: 768,
lua_script: scripts/gui_demo.lua,
scripts_directory: scripts,
project_root: .,
shaders_directory: shaders,
device_extensions: [
VK_KHR_swapchain
],
config_file: config/gui_runtime.json
}

7
scripts/assets/logo.svg Normal file
View File

@@ -0,0 +1,7 @@
<svg width=\ 128\ height=\128\ viewBox=\0 0 128 128\ xmlns=\http://www.w3.org/2000/svg\>
<circle cx=\64\ cy=\64\ r=\60\ fill=\#1f7ae0\ />
<circle cx=\64\ cy=\64\ r=\42\ fill=\#ffffff\ />
<circle cx=\64\ cy=\64\ r=\32\ fill=\#0f3d91\ />
<circle cx=\64\ cy=\30\ r=\12\ fill=\#f4b400\ />
<circle cx=\96\ cy=\92\ r=\10\ fill=\#34a853\ />
</svg>

535
scripts/gui.lua Normal file
View File

@@ -0,0 +1,535 @@
-- Lightweight Lua-based 2D GUI framework that emits draw commands
-- and handles interaction for buttons, textboxes, and list views.
local Gui = {}
-- {r,g,b,a} colors
local DEFAULT_STYLE = {
spacing = 8,
radius = 6,
fontSize = 16,
button = {
background = {0.2, 0.24, 0.28, 1.0},
hovered = {0.26, 0.3, 0.36, 1.0},
active = {0.16, 0.22, 0.28, 1.0},
border = {0.45, 0.52, 0.6, 1.0},
text = {1.0, 1.0, 1.0, 1.0},
},
textbox = {
background = {0.05, 0.06, 0.08, 1.0},
border = {0.38, 0.4, 0.45, 1.0},
focusedBorder = {0.35, 0.55, 0.9, 1.0},
text = {0.97, 0.97, 0.97, 1.0},
placeholder = {0.5, 0.5, 0.5, 1.0},
cursor = {0.98, 0.98, 0.98, 1.0},
},
listview = {
background = {0.04, 0.05, 0.07, 1.0},
border = {0.25, 0.28, 0.32, 1.0},
alternate = {0.06, 0.07, 0.09, 1.0},
selection = {0.12, 0.35, 0.65, 1.0},
text = {0.93, 0.94, 0.96, 1.0},
},
}
local function clamp(value, minValue, maxValue)
if minValue and value < minValue then
return minValue
end
if maxValue and value > maxValue then
return maxValue
end
return value
end
local function rectContains(rect, x, y)
return x >= rect.x and x <= rect.x + rect.width and y >= rect.y and y <= rect.y + rect.height
end
local InputState = {}
InputState.__index = InputState
function InputState:new()
local instance = {
mouseX = 0,
mouseY = 0,
mouseDown = false,
mouseDownPrevious = false,
wheel = 0,
textInput = "",
keys = {},
}
return setmetatable(instance, self)
end
function InputState:setMouse(x, y, isDown)
self.mouseDownPrevious = self.mouseDown
self.mouseX = x
self.mouseY = y
self.mouseDown = isDown
end
function InputState:setWheel(deltaY)
self.wheel = deltaY
end
function InputState:mouseJustPressed()
return self.mouseDown and not self.mouseDownPrevious
end
function InputState:mouseJustReleased()
return not self.mouseDown and self.mouseDownPrevious
end
function InputState:setKey(keyName, isDown)
self.keys[keyName] = isDown
end
function InputState:isKeyDown(keyName)
return self.keys[keyName]
end
function InputState:addTextInput(text)
self.textInput = (self.textInput or "") .. (text or "")
end
function InputState:resetTransient()
self.textInput = ""
self.wheel = 0
end
local Context = {}
Context.__index = Context
function Context:new(options)
options = options or {}
local style = options.style or DEFAULT_STYLE
local instance = {
commands = {},
input = nil,
hotWidget = nil,
activeWidget = nil,
focusWidget = nil,
nextFocus = nil,
style = style,
mousePressed = false,
mouseReleased = false,
}
return setmetatable(instance, self)
end
function Context:beginFrame(input)
if not input then
error("Context requires an InputState for each frame")
end
self.input = input
self.commands = {}
self.hotWidget = nil
self.nextFocus = nil
self.mousePressed = input:mouseJustPressed()
self.mouseReleased = input:mouseJustReleased()
end
function Context:endFrame()
if self.mouseReleased then
self.activeWidget = nil
end
if self.nextFocus ~= nil then
self.focusWidget = self.nextFocus
elseif self.mousePressed and not self.hotWidget then
self.focusWidget = nil
end
end
function Context:requestFocus(widgetId)
self.nextFocus = widgetId
end
function Context:addCommand(command)
table.insert(self.commands, command)
end
function Context:getCommands()
return self.commands
end
function Context:isMouseOver(rect)
return rectContains(rect, self.input.mouseX, self.input.mouseY)
end
function Context:markHot(widgetId, hovered)
if hovered then
self.hotWidget = widgetId
end
end
function Context:pushRect(rect, params)
params = params or {}
local command = {
type = "rect",
x = rect.x,
y = rect.y,
width = rect.width,
height = rect.height,
color = params.color,
borderColor = params.borderColor,
borderWidth = params.borderWidth or (params.borderColor and 1 or 0),
radius = params.radius or self.style.radius,
}
self:addCommand(command)
end
function Context:pushText(rect, params)
params = params or {}
local padding = params.padding or self.style.spacing / 2
local alignX = params.alignX or "left"
local alignY = params.alignY or "center"
local x = rect.x + padding
local y = rect.y
if alignX == "center" then
x = rect.x + rect.width / 2
elseif alignX == "right" then
x = rect.x + rect.width - padding
end
if alignY == "center" then
y = rect.y + rect.height / 2
elseif alignY == "bottom" then
y = rect.y + rect.height - padding
else
y = rect.y + padding
end
local command = {
type = "text",
text = params.text or "",
x = x,
y = y,
color = params.color,
fontSize = params.fontSize or self.style.fontSize,
alignX = alignX,
alignY = alignY,
bounds = {x = rect.x, y = rect.y, width = rect.width, height = rect.height},
}
if params.clip then
command.clipRect = {
x = rect.x,
y = rect.y,
width = rect.width,
height = rect.height,
}
end
self:addCommand(command)
end
function Context:pushSvg(svgPath, rect, opts)
opts = opts or {}
local command = {
type = "svg",
path = svgPath,
x = rect.x,
y = rect.y,
width = rect.width,
height = rect.height,
color = opts.color,
tint = opts.tint,
}
self:addCommand(command)
end
function Context:pushClip(rect)
self:addCommand({type = "clip_push", rect = rect})
end
function Context:popClip()
self:addCommand({type = "clip_pop"})
end
function Context:processInteraction(widgetId, rect)
local hovered = self:isMouseOver(rect)
self:markHot(widgetId, hovered)
if hovered and self.mousePressed then
self.activeWidget = widgetId
end
local clicked = hovered and self.mouseReleased and self.activeWidget == widgetId
return hovered, clicked
end
local function ensureRect(rect)
return {
x = rect.x or rect[1] or 0,
y = rect.y or rect[2] or 0,
width = rect.width or rect[3] or 0,
height = rect.height or rect[4] or 0,
}
end
function Gui.newContext(options)
return Context:new(options)
end
function Gui.newInputState()
return InputState:new()
end
function Gui.button(context, widgetId, rectDef, label, opts)
opts = opts or {}
local rect = ensureRect(rectDef)
local style = context.style.button
local hovered, clicked = context:processInteraction(widgetId, rect)
local active = context.activeWidget == widgetId
local fillColor = style.background
if active then
fillColor = style.active
elseif hovered then
fillColor = style.hovered
end
context:pushRect(rect, {
color = fillColor,
borderColor = style.border,
radius = opts.radius,
})
context:pushText(rect, {
text = label or "",
color = style.text,
alignX = "center",
alignY = "center",
fontSize = opts.fontSize,
})
if clicked and opts.onClick then
opts.onClick()
end
return clicked
end
function Gui.text(context, rectDef, text, opts)
opts = opts or {}
local rect = ensureRect(rectDef)
context:pushText(rect, {
text = text or "",
color = opts.color,
alignX = opts.alignX,
alignY = opts.alignY,
padding = opts.padding,
fontSize = opts.fontSize,
clip = opts.clip,
})
end
function Gui.svg(context, rectDef, svgPath, opts)
opts = opts or {}
local rect = ensureRect(rectDef)
context:pushSvg(svgPath, rect, opts)
end
function Gui.textbox(context, widgetId, rectDef, state, opts)
opts = opts or {}
state = state or {}
state.text = state.text or ""
state.cursor = clamp(state.cursor or #state.text, 0, #state.text)
state.offset = clamp(state.offset or 0, 0, math.max(0, #state.text))
local rect = ensureRect(rectDef)
local style = context.style.textbox
local hovered, clicked = context:processInteraction(widgetId, rect)
local focused = context.focusWidget == widgetId
if clicked then
context:requestFocus(widgetId)
end
local background = style.background
local borderColor = focused and style.focusedBorder or style.border
context:pushRect(rect, {
color = background,
borderColor = borderColor,
})
local pad = opts.padding or context.style.spacing / 2
local charWidth = opts.charWidth or (context.style.fontSize * 0.55)
local innerWidth = math.max(0, rect.width - pad * 2)
local maxChars = math.max(1, math.floor(innerWidth / charWidth))
if focused then
local function deleteRange(startIdx, count)
if startIdx < 1 or startIdx > #state.text then
return
end
local before = state.text:sub(1, startIdx - 1)
local after = state.text:sub(startIdx + count)
state.text = before .. after
end
local textInput = context.input.textInput or ""
if textInput ~= "" then
local before = state.text:sub(1, state.cursor)
local after = state.text:sub(state.cursor + 1)
state.text = before .. textInput .. after
state.cursor = state.cursor + #textInput
context.input.textInput = ""
end
if context.input:isKeyDown("backspace") and state.cursor > 0 then
deleteRange(state.cursor, 1)
state.cursor = state.cursor - 1
context.input.keys.backspace = false
end
if context.input:isKeyDown("delete") and state.cursor < #state.text then
deleteRange(state.cursor + 1, 1)
context.input.keys.delete = false
end
if context.input:isKeyDown("left") and state.cursor > 0 then
state.cursor = state.cursor - 1
context.input.keys.left = false
end
if context.input:isKeyDown("right") and state.cursor < #state.text then
state.cursor = state.cursor + 1
context.input.keys.right = false
end
if context.input:isKeyDown("home") then
state.cursor = 0
context.input.keys.home = false
end
if context.input:isKeyDown("end") then
state.cursor = #state.text
context.input.keys.end = false
end
if context.input:isKeyDown("enter") then
if opts.onSubmit then
opts.onSubmit(state.text)
end
context.input.keys.enter = false
end
end
state.cursor = clamp(state.cursor, 0, #state.text)
if state.cursor < state.offset then
state.offset = state.cursor
end
if state.cursor > state.offset + maxChars then
state.offset = state.cursor - maxChars
end
local offset = clamp(state.offset, 0, math.max(0, #state.text - maxChars))
state.offset = offset
local visibleText = state.text:sub(offset + 1, offset + maxChars)
local display = visibleText
if display == "" and opts.placeholder and not focused then
context:pushText(rect, {
text = opts.placeholder,
color = style.placeholder,
alignX = "left",
alignY = "center",
padding = pad,
})
else
context:pushText(rect, {
text = display,
color = style.text,
alignX = "left",
alignY = "center",
padding = pad,
clip = true,
})
end
if focused then
context:pushRect({
x = rect.x + pad + (state.cursor - offset) * charWidth - 1,
y = rect.y + pad / 2,
width = opts.caretWidth or 2,
height = rect.height - pad,
}, {
color = style.cursor,
})
end
return state
end
function Gui.listView(context, widgetId, rectDef, items, state, opts)
opts = opts or {}
state = state or {}
items = items or {}
state.scroll = state.scroll or 0
local itemCount = #items
if itemCount == 0 then
state.selectedIndex = 0
else
state.selectedIndex = clamp(state.selectedIndex or 1, 1, itemCount)
end
local rect = ensureRect(rectDef)
local style = context.style.listview
local hovered, clicked = context:processInteraction(widgetId, rect)
local focus = context.focusWidget == widgetId
if clicked then
context:requestFocus(widgetId)
end
context:pushRect(rect, {
color = style.background,
borderColor = style.border,
})
local itemHeight = opts.itemHeight or (context.style.fontSize + context.style.spacing)
local contentHeight = #items * itemHeight
state.scroll = clamp(state.scroll, 0, math.max(0, contentHeight - rect.height))
if state.selectedIndex > 0 and opts.scrollToSelection then
local scrollTarget = (state.selectedIndex - 1) * itemHeight
if scrollTarget < state.scroll then
state.scroll = scrollTarget
elseif scrollTarget + itemHeight > state.scroll + rect.height then
state.scroll = scrollTarget + itemHeight - rect.height
end
end
local scrollDelta = context.input.wheel or 0
if (hovered or focus) and scrollDelta ~= 0 then
state.scroll = clamp(state.scroll - scrollDelta * (opts.scrollSpeed or 20), 0, math.max(0, contentHeight - rect.height))
end
context:pushClip(rect)
local baseY = rect.y - state.scroll
for index, item in ipairs(items) do
local rowY = baseY + (index - 1) * itemHeight
if rowY + itemHeight >= rect.y and rowY <= rect.y + rect.height then
local rowRect = {x = rect.x, y = rowY, width = rect.width, height = itemHeight}
local rowColor = style.alternate
if index % 2 == 0 then
rowColor = style.background
end
if index == state.selectedIndex then
rowColor = style.selection
end
context:pushRect(rowRect, {color = rowColor})
context:pushText(rowRect, {
text = (opts.itemFormatter and opts.itemFormatter(item, index)) or tostring(item),
color = style.text,
alignX = "left",
alignY = "center",
padding = context.style.spacing,
clip = true,
})
end
end
context:popClip()
if clicked then
local relativeY = context.input.mouseY - rect.y + state.scroll
local clickedIndex = math.floor(relativeY / itemHeight) + 1
if clickedIndex >= 1 and clickedIndex <= #items then
state.selectedIndex = clickedIndex
if opts.onSelect then
opts.onSelect(clickedIndex, items[clickedIndex])
end
end
end
if focus then
if context.input:isKeyDown("up") and state.selectedIndex > 1 then
state.selectedIndex = state.selectedIndex - 1
end
if context.input:isKeyDown("down") and state.selectedIndex < #items then
state.selectedIndex = state.selectedIndex + 1
end
end
return state
end
function Gui.newTextState(initial)
return {
text = initial or "",
cursor = #initial,
offset = 0,
}
end
function Gui.newListState()
return {
scroll = 0,
selectedIndex = 1,
}
end
Gui.style = DEFAULT_STYLE
return Gui

134
scripts/gui_demo.lua Normal file
View File

@@ -0,0 +1,134 @@
local Gui = require(\ gui\)
local math3d = require(\math3d\)
local ctx = Gui.newContext()
local input = Gui.newInputState()
local textState = Gui.newTextState(\\)
local listState = Gui.newListState()
local items = {
\Dashboard Setup\,
\Input Streams\,
\Telemetry\,
\Power Profile\,
\Diagnostics\,
\Release Notes\,
}
local statusMessage = \Idle\
local selectedItem = items[1]
local rotationSpeeds = {x = 0.45, y = 0.65}
local cubeVertices = {
{ position = {-1.0, -1.0, -1.0}, color = {1.0, 0.2, 0.4} },
{ position = {1.0, -1.0, -1.0}, color = {0.2, 0.9, 0.4} },
{ position = {1.0, 1.0, -1.0}, color = {0.3, 0.4, 0.9} },
{ position = {-1.0, 1.0, -1.0}, color = {1.0, 0.8, 0.2} },
{ position = {-1.0, -1.0, 1.0}, color = {0.9, 0.4, 0.5} },
{ position = {1.0, -1.0, 1.0}, color = {0.4, 0.9, 0.5} },
{ position = {1.0, 1.0, 1.0}, color = {0.6, 0.8, 1.0} },
{ position = {-1.0, 1.0, 1.0}, color = {0.4, 0.4, 0.4} },
}
local cubeIndices = {
1, 2, 3, 3, 4, 1,
5, 6, 7, 7, 8, 5,
1, 5, 8, 8, 4, 1,
2, 6, 7, 7, 3, 2,
4, 3, 7, 7, 8, 4,
1, 2, 6, 6, 5, 1,
}
local shaderVariants = {
default = {
vertex = \shaders/cube.vert.spv\,
fragment = \shaders/cube.frag.spv\,
},
}
local camera = {
eye = {2.0, 2.0, 3.0},
center = {0.0, 0.0, 0.0},
up = {0.0, 1.0, 0.0},
fov = 0.78,
near = 0.1,
far = 10.0,
}
local function buildModel(time)
local y = math3d.rotation_y(time * rotationSpeeds.y)
local x = math3d.rotation_x(time * rotationSpeeds.x)
return math3d.multiply(y, x)
end
local function createCube(position)
local function computeModel(time)
local base = buildModel(time)
local offset = math3d.translation(position[1], position[2], position[3])
return math3d.multiply(offset, base)
end
return {
vertices = cubeVertices,
indices = cubeIndices,
compute_model_matrix = computeModel,
shader_key = \default\,
}
end
gui_context = ctx
gui_input = input
function get_scene_objects()
return { createCube({0.0, 0.0, -4.0}) }
end
function get_shader_paths()
return shaderVariants
end
function get_view_projection(aspect)
local view = math3d.look_at(camera.eye, camera.center, camera.up)
local projection = math3d.perspective(camera.fov, aspect, camera.near, camera.far)
return math3d.multiply(projection, view)
end
local function drawPanel()
ctx:pushRect({x = 10, y = 10, width = 460, height = 520}, {
color = {0.06, 0.07, 0.09, 0.9},
borderColor = {0.35, 0.38, 0.42, 1.0},
})
Gui.text(ctx, {x = 30, y = 30, width = 420, height = 30}, \Command Console\, {
fontSize = 24,
alignX = \left\,
color = {0.95, 0.95, 0.95, 1.0},
})
Gui.svg(ctx, {x = 320, y = 30, width = 120, height = 120}, \assets/logo.svg\)
textState = Gui.textbox(ctx, \search_field\, {x = 30, y = 80, width = 420, height = 40}, textState, {
placeholder = \Filter modules...\,
onSubmit = function(text)
statusMessage = \Searching for: \ .. (text ~= \\ and text or \anything\)
end,
})
listState = Gui.listView(ctx, \menu_list\, {x = 30, y = 140, width = 420, height = 240}, items, listState, {
onSelect = function(idx, item)
selectedItem = item
statusMessage = \Ready to adjust \ .. item
end,
scrollToSelection = true,
})
if Gui.button(ctx, \apply\, {x = 30, y = 400, width = 200, height = 38}, \Apply Settings\) then
statusMessage = \Applied configuration for \ .. (selectedItem or \\)
end
Gui.text(ctx, {x = 30, y = 448, width = 420, height = 24}, \Status: \ .. statusMessage, {
color = {0.6, 0.9, 1.0, 1.0},
alignY = \top\,
})
end
function get_gui_commands()
ctx:beginFrame(input)
drawPanel()
ctx:endFrame()
return ctx:getCommands()
end

View File

@@ -7,6 +7,7 @@
#include <array>
#include <filesystem>
#include <memory>
#include <optional>
#include <set>
#include <string>
@@ -19,6 +20,7 @@
#include "core/vertex.hpp"
#include "script/cube_script.hpp"
#include "gui/gui_renderer.hpp"
namespace sdl3cpp::app {
@@ -87,6 +89,8 @@ private:
const std::array<float, 16>& viewProj);
void CreateSyncObjects();
void DrawFrame(float time);
void SetupGuiRenderer();
void ProcessGuiEvent(const SDL_Event& event);
QueueFamilyIndices FindQueueFamilies(VkPhysicalDevice device);
bool CheckDeviceExtensionSupport(VkPhysicalDevice device);
@@ -126,6 +130,10 @@ private:
std::string defaultShaderKey_;
VkFence inFlightFence_ = VK_NULL_HANDLE;
bool framebufferResized_ = false;
script::GuiInputSnapshot guiInputSnapshot_;
std::vector<script::CubeScript::GuiCommand> guiCommands_;
std::unique_ptr<gui::GuiRenderer> guiRenderer_;
bool guiHasCommands_ = false;
std::vector<RenderObject> renderObjects_;
};

View File

@@ -36,6 +36,7 @@ void Sdl3App::InitSDL() {
if (!window_) {
throw std::runtime_error(std::string("SDL_CreateWindow failed: ") + SDL_GetError());
}
SDL_StartTextInput();
}
void Sdl3App::InitVulkan() {
@@ -44,6 +45,7 @@ void Sdl3App::InitVulkan() {
PickPhysicalDevice();
CreateLogicalDevice();
CreateSwapChain();
SetupGuiRenderer();
CreateImageViews();
CreateRenderPass();
LoadSceneData();
@@ -66,9 +68,26 @@ void Sdl3App::MainLoop() {
running = false;
} else if (event.type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED) {
framebufferResized_ = true;
} else if (guiHasCommands_) {
ProcessGuiEvent(event);
}
}
if (guiHasCommands_) {
int mouseX = 0;
int mouseY = 0;
SDL_GetMouseState(&mouseX, &mouseY);
guiInputSnapshot_.mouseX = static_cast<float>(mouseX);
guiInputSnapshot_.mouseY = static_cast<float>(mouseY);
cubeScript_.UpdateGuiInput(guiInputSnapshot_);
if (guiRenderer_) {
guiCommands_ = cubeScript_.LoadGuiCommands();
guiRenderer_->Prepare(guiCommands_, swapChainExtent_.width, swapChainExtent_.height);
}
guiInputSnapshot_.wheel = 0.0f;
guiInputSnapshot_.textInput.clear();
}
auto now = std::chrono::steady_clock::now();
float time = std::chrono::duration<float>(now - start).count();
DrawFrame(time);
@@ -97,6 +116,7 @@ void Sdl3App::Cleanup() {
window_ = nullptr;
}
SDL_Vulkan_UnloadLibrary();
SDL_StopTextInput();
SDL_Quit();
}

View File

@@ -2,6 +2,21 @@
#include <limits>
#include <stdexcept>
#include <unordered_map>
namespace {
const std::unordered_map<SDL_Keycode, std::string> kGuiKeyNames = {
{SDLK_BACKSPACE, "backspace"},
{SDLK_DELETE, "delete"},
{SDLK_LEFT, "left"},
{SDLK_RIGHT, "right"},
{SDLK_HOME, "home"},
{SDLK_END, "end"},
{SDLK_RETURN, "enter"},
{SDLK_UP, "up"},
{SDLK_DOWN, "down"},
};
} // namespace
namespace sdl3cpp::app {
@@ -60,9 +75,57 @@ void Sdl3App::RecordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageI
vkCmdDrawIndexed(commandBuffer, object.indexCount, 1, object.indexOffset, object.vertexOffset, 0);
}
vkCmdEndRenderPass(commandBuffer);
if (guiRenderer_) {
guiRenderer_->BlitToSwapchain(commandBuffer, swapChainImages_[imageIndex]);
}
vkEndCommandBuffer(commandBuffer);
}
void Sdl3App::ProcessGuiEvent(const SDL_Event& event) {
switch (event.type) {
case SDL_EVENT_MOUSE_MOTION:
guiInputSnapshot_.mouseX = static_cast<float>(event.motion.x);
guiInputSnapshot_.mouseY = static_cast<float>(event.motion.y);
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
if (event.button.button == SDL_BUTTON_LEFT) {
guiInputSnapshot_.mouseDown = (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN);
}
break;
case SDL_EVENT_MOUSE_WHEEL:
guiInputSnapshot_.wheel += static_cast<float>(event.wheel.y);
break;
case SDL_EVENT_TEXT_INPUT:
guiInputSnapshot_.textInput.append(event.text.text);
break;
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP: {
auto it = kGuiKeyNames.find(event.key.keysym.sym);
if (it != kGuiKeyNames.end()) {
guiInputSnapshot_.keyStates[it->second] = (event.type == SDL_EVENT_KEY_DOWN);
}
break;
}
default:
break;
}
}
void Sdl3App::SetupGuiRenderer() {
guiHasCommands_ = cubeScript_.HasGuiCommands();
if (!guiHasCommands_) {
guiRenderer_.reset();
return;
}
if (!guiRenderer_) {
guiRenderer_ =
std::make_unique<gui::GuiRenderer>(device_, physicalDevice_, swapChainImageFormat_,
cubeScript_.GetScriptDirectory());
}
guiRenderer_->Resize(swapChainExtent_.width, swapChainExtent_.height, swapChainImageFormat_);
}
void Sdl3App::DrawFrame(float time) {
vkWaitForFences(device_, 1, &inFlightFence_, VK_TRUE, std::numeric_limits<uint64_t>::max());
vkResetFences(device_, 1, &inFlightFence_);

View File

@@ -151,6 +151,7 @@ void Sdl3App::RecreateSwapChain() {
CleanupSwapChain();
CreateSwapChain();
CreateImageViews();
SetupGuiRenderer();
CreateRenderPass();
CreateGraphicsPipeline();
CreateFramebuffers();

565
src/gui/gui_renderer.cpp Normal file
View File

@@ -0,0 +1,565 @@
#include "gui/gui_renderer.hpp"
#include "app/vulkan_api.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <cctype>
#include <cstdio>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include "../../third_party/font8x8_basic.h"
namespace script = sdl3cpp::script;
namespace {
bool ExtractAttribute(const std::string& source, const char* name, std::string& outValue) {
std::string key = name;
size_t pos = source.find(key);
while (pos != std::string::npos) {
size_t eq = source.find('=', pos + key.size());
if (eq == std::string::npos) {
break;
}
size_t valueStart = eq + 1;
while (valueStart < source.size() &&
std::isspace(static_cast<unsigned char>(source[valueStart]))) {
valueStart++;
}
if (valueStart >= source.size()) {
break;
}
char quote = source[valueStart];
if (quote != '\"' && quote != '\'') {
break;
}
size_t valueEnd = source.find(quote, valueStart + 1);
if (valueEnd == std::string::npos) {
break;
}
outValue = source.substr(valueStart + 1, valueEnd - valueStart - 1);
return true;
}
return false;
}
float ParseFloatValue(const std::string& text) {
try {
size_t idx = 0;
return std::stof(text, &idx);
} catch (...) {
return 0.0f;
}
}
script::GuiColor ParseColorString(const std::string& text, const script::GuiColor& fallback) {
if (text.empty() || text[0] != '#') {
return fallback;
}
try {
if (text.size() == 7) {
unsigned int rgb = std::stoul(text.substr(1), nullptr, 16);
return {((rgb >> 16) & 0xFF) / 255.0f, ((rgb >> 8) & 0xFF) / 255.0f,
(rgb & 0xFF) / 255.0f, 1.0f};
}
if (text.size() == 9) {
unsigned int rgba = std::stoul(text.substr(1), nullptr, 16);
return {((rgba >> 24) & 0xFF) / 255.0f, ((rgba >> 16) & 0xFF) / 255.0f,
((rgba >> 8) & 0xFF) / 255.0f, (rgba & 0xFF) / 255.0f};
}
} catch (...) {
}
return fallback;
}
ParsedSvg ParseSvgFile(const std::filesystem::path& path) {
std::ifstream file(path);
if (!file) {
throw std::runtime_error("Failed to open SVG file: " + path.string());
}
std::string data((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
ParsedSvg result;
std::string value;
if (ExtractAttribute(data, "viewBox", value)) {
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
std::sscanf(value.c_str(), "%f %f %f %f", &x, &y, &w, &h);
if (w > 0.0f && h > 0.0f) {
result.viewWidth = w;
result.viewHeight = h;
}
}
if (ExtractAttribute(data, "width", value)) {
result.viewWidth = ParseFloatValue(value);
}
if (ExtractAttribute(data, "height", value)) {
result.viewHeight = ParseFloatValue(value);
}
if (result.viewWidth <= 0.0f) {
result.viewWidth = 128.0f;
}
if (result.viewHeight <= 0.0f) {
result.viewHeight = 128.0f;
}
size_t search = 0;
while (true) {
size_t tagStart = data.find("<circle", search);
if (tagStart == std::string::npos) {
break;
}
size_t tagEnd = data.find('>', tagStart);
if (tagEnd == std::string::npos) {
break;
}
std::string tag = data.substr(tagStart, tagEnd - tagStart);
SvgCircle circle;
std::string attr;
if (ExtractAttribute(tag, "cx", attr)) {
circle.cx = ParseFloatValue(attr);
}
if (ExtractAttribute(tag, "cy", attr)) {
circle.cy = ParseFloatValue(attr);
}
if (ExtractAttribute(tag, "r", attr)) {
circle.r = ParseFloatValue(attr);
}
if (ExtractAttribute(tag, "fill", attr)) {
circle.color = ParseColorString(attr, circle.color);
}
result.circles.push_back(circle);
search = tagEnd + 1;
}
return result;
}
script::CubeScript::GuiCommand::RectData IntersectRect(const script::CubeScript::GuiCommand::RectData& a,
const script::CubeScript::GuiCommand::RectData& b) {
script::CubeScript::GuiCommand::RectData result;
result.x = std::max(a.x, b.x);
result.y = std::max(a.y, b.y);
float right = std::min(a.x + a.width, b.x + b.width);
float bottom = std::min(a.y + a.height, b.y + b.height);
result.width = std::max(0.0f, right - result.x);
result.height = std::max(0.0f, bottom - result.y);
return result;
}
int ClampToRange(int value, int minimum, int maximum) {
return std::min(std::max(value, minimum), maximum);
}
class Canvas {
public:
using RectData = script::CubeScript::GuiCommand::RectData;
void Resize(uint32_t width, uint32_t height) {
width_ = width;
height_ = height;
pixels_.assign(static_cast<size_t>(width_) * static_cast<size_t>(height_) * 4, 0);
clipStack_.clear();
clipStack_.push_back({0.0f, 0.0f, static_cast<float>(width_), static_cast<float>(height_)});
}
void Clear() {
std::fill(pixels_.begin(), pixels_.end(), 0);
clipStack_.clear();
clipStack_.push_back({0.0f, 0.0f, static_cast<float>(width_), static_cast<float>(height_)});
}
void PushClip(const RectData& rect) {
clipStack_.push_back(rect);
}
void PopClip() {
if (clipStack_.size() > 1) {
clipStack_.pop_back();
}
}
void FillRect(const RectData& rect, const script::GuiColor& fillColor,
const script::GuiColor& borderColor, float borderWidth) {
DrawFilledRect(rect, fillColor);
if (borderWidth > 0.0f && borderColor.a > 0.0f) {
DrawFilledRect({rect.x, rect.y, rect.width, borderWidth}, borderColor);
DrawFilledRect({rect.x, rect.y + rect.height - borderWidth, rect.width, borderWidth}, borderColor);
DrawFilledRect({rect.x, rect.y + borderWidth, borderWidth, rect.height - borderWidth * 2.0f}, borderColor);
DrawFilledRect({rect.x + rect.width - borderWidth, rect.y + borderWidth, borderWidth,
rect.height - borderWidth * 2.0f}, borderColor);
}
}
void DrawText(const std::string& text, const script::GuiColor& color, const RectData& bounds,
const std::string& alignX, const std::string& alignY, float fontSize) {
if (text.empty() || width_ == 0 || height_ == 0) {
return;
}
float scale = std::max(1.0f, fontSize / 8.0f);
float glyphWidth = 8.0f * scale;
float glyphHeight = 8.0f * scale;
float textWidth = glyphWidth * static_cast<float>(text.size());
float x = bounds.x;
if (alignX == "center") {
x += (bounds.width - textWidth) * 0.5f;
} else if (alignX == "right") {
x += bounds.width - textWidth;
}
float y = bounds.y;
if (alignY == "center") {
y += (bounds.height - glyphHeight) * 0.5f;
} else if (alignY == "bottom") {
y += bounds.height - glyphHeight;
}
for (size_t i = 0; i < text.size(); ++i) {
unsigned char code = static_cast<unsigned char>(text[i]);
if (code >= 128) {
continue;
}
float glyphX = x + glyphWidth * static_cast<float>(i);
for (int row = 0; row < 8; ++row) {
uint8_t pattern = static_cast<uint8_t>(font8x8_basic[code][row]);
for (int col = 0; col < 8; ++col) {
if ((pattern & (1 << col)) == 0) {
continue;
}
RectData pixelRect{
glyphX + static_cast<float>(col) * scale,
y + static_cast<float>(row) * scale,
scale,
scale,
};
DrawFilledRect(pixelRect, color);
}
}
}
}
void DrawSvg(const ParsedSvg& svg, const RectData& target, const script::GuiColor& tint) {
if (svg.circles.empty() || svg.viewWidth <= 0.0f || svg.viewHeight <= 0.0f || width_ == 0 ||
height_ == 0) {
return;
}
RectData clipped = ClipRect(target);
if (clipped.width <= 0.0f || clipped.height <= 0.0f) {
return;
}
float scaleX = clipped.width / svg.viewWidth;
float scaleY = clipped.height / svg.viewHeight;
float scale = std::min(scaleX, scaleY);
for (const auto& circle : svg.circles) {
float cx = clipped.x + circle.cx * scaleX;
float cy = clipped.y + circle.cy * scaleY;
float radius = circle.r * scale;
script::GuiColor color = circle.color;
if (tint.a > 0.0f) {
color.r *= tint.r;
color.g *= tint.g;
color.b *= tint.b;
color.a *= tint.a;
}
int yStart = ClampToRange(static_cast<int>(std::floor(cy - radius)), 0, static_cast<int>(height_));
int yEnd = ClampToRange(static_cast<int>(std::ceil(cy + radius)), 0, static_cast<int>(height_));
for (int row = yStart; row < yEnd; ++row) {
float dy = (static_cast<float>(row) + 0.5f) - cy;
float horizontalSpan = radius * radius - dy * dy;
if (horizontalSpan <= 0.0f) {
continue;
}
float span = std::sqrt(horizontalSpan);
RectData slice{
cx - span,
static_cast<float>(row),
2.0f * span,
1.0f,
};
DrawFilledRect(slice, color);
}
}
}
const std::vector<uint8_t>& Pixels() const {
return pixels_;
}
private:
RectData ClipRect(const RectData& rect) const {
RectData clipped = rect;
for (const auto& entry : clipStack_) {
clipped = IntersectRect(clipped, entry);
}
return clipped;
}
void DrawFilledRect(const RectData& rect, const script::GuiColor& color) {
if (rect.width <= 0.0f || rect.height <= 0.0f) {
return;
}
RectData clipped = ClipRect(rect);
if (clipped.width <= 0.0f || clipped.height <= 0.0f) {
return;
}
int startX = ClampToRange(static_cast<int>(std::floor(clipped.x)), 0, static_cast<int>(width_));
int startY = ClampToRange(static_cast<int>(std::floor(clipped.y)), 0, static_cast<int>(height_));
int endX = ClampToRange(static_cast<int>(std::ceil(clipped.x + clipped.width)), 0, static_cast<int>(width_));
int endY = ClampToRange(static_cast<int>(std::ceil(clipped.y + clipped.height)), 0, static_cast<int>(height_));
for (int y = startY; y < endY; ++y) {
for (int x = startX; x < endX; ++x) {
BlendPixel(x, y, color);
}
}
}
void BlendPixel(int x, int y, const script::GuiColor& color) {
size_t index = (static_cast<size_t>(y) * width_ + static_cast<size_t>(x)) * 4;
auto clampByte = [](float value) -> uint8_t {
return static_cast<uint8_t>(std::clamp(value, 0.0f, 1.0f) * 255.0f);
};
float destR = pixels_[index] / 255.0f;
float destG = pixels_[index + 1] / 255.0f;
float destB = pixels_[index + 2] / 255.0f;
float destA = pixels_[index + 3] / 255.0f;
float srcA = std::clamp(color.a, 0.0f, 1.0f);
float invSrc = 1.0f - srcA;
float outR = color.r * srcA + destR * invSrc;
float outG = color.g * srcA + destG * invSrc;
float outB = color.b * srcA + destB * invSrc;
float outA = srcA + destA * invSrc;
pixels_[index] = clampByte(outR);
pixels_[index + 1] = clampByte(outG);
pixels_[index + 2] = clampByte(outB);
pixels_[index + 3] = clampByte(outA);
}
uint32_t width_ = 0;
uint32_t height_ = 0;
std::vector<uint8_t> pixels_;
std::vector<RectData> clipStack_;
};
} // namespace
namespace sdl3cpp::gui {
GuiRenderer::GuiRenderer(VkDevice device, VkPhysicalDevice physicalDevice, VkFormat swapchainFormat,
const std::filesystem::path& scriptDirectory)
: device_(device),
physicalDevice_(physicalDevice),
swapchainFormat_(swapchainFormat),
scriptDirectory_(scriptDirectory),
canvas_(std::make_unique<Canvas>()) {}
GuiRenderer::~GuiRenderer() {
DestroyStagingBuffer();
}
bool GuiRenderer::IsReady() const {
return canvasWidth_ > 0 && canvasHeight_ > 0 && stagingBuffer_ != VK_NULL_HANDLE;
}
void GuiRenderer::Prepare(const std::vector<script::CubeScript::GuiCommand>& commands, uint32_t width,
uint32_t height) {
if (width == 0 || height == 0 || !canvas_) {
return;
}
EnsureCanvas(width, height);
canvas_->Clear();
for (const auto& command : commands) {
switch (command.type) {
case script::CubeScript::GuiCommand::Type::Rect:
canvas_->FillRect(command.rect, command.color, command.borderColor, command.borderWidth);
break;
case script::CubeScript::GuiCommand::Type::Text: {
if (command.hasClipRect) {
canvas_->PushClip(command.clipRect);
}
if (command.hasBounds) {
canvas_->DrawText(command.text, command.color, command.bounds, command.alignX,
command.alignY, command.fontSize);
} else {
script::CubeScript::GuiCommand::RectData fallback{
command.rect.x, command.rect.y,
command.fontSize * static_cast<float>(std::max<size_t>(1, command.text.size())), command.fontSize};
canvas_->DrawText(command.text, command.color, fallback, command.alignX,
command.alignY, command.fontSize);
}
if (command.hasClipRect) {
canvas_->PopClip();
}
break;
}
case script::CubeScript::GuiCommand::Type::ClipPush:
canvas_->PushClip(command.rect);
break;
case script::CubeScript::GuiCommand::Type::ClipPop:
canvas_->PopClip();
break;
case script::CubeScript::GuiCommand::Type::Svg:
if (command.svgPath.empty()) {
break;
}
if (const ParsedSvg* svg = LoadSvg(command.svgPath)) {
canvas_->DrawSvg(*svg, command.rect, command.svgTint);
}
break;
}
}
UpdateStagingBuffer();
}
void GuiRenderer::BlitToSwapchain(VkCommandBuffer commandBuffer, VkImage image) {
if (!IsReady()) {
return;
}
VkImageMemoryBarrier barrier{};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.image = image;
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
barrier.subresourceRange.baseMipLevel = 0;
barrier.subresourceRange.levelCount = 1;
barrier.subresourceRange.baseArrayLayer = 0;
barrier.subresourceRange.layerCount = 1;
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
VkBufferImageCopy region{};
region.bufferOffset = 0;
region.bufferRowLength = 0;
region.bufferImageHeight = 0;
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.imageSubresource.mipLevel = 0;
region.imageSubresource.baseArrayLayer = 0;
region.imageSubresource.layerCount = 1;
region.imageOffset = {0, 0, 0};
region.imageExtent = {canvasWidth_, canvasHeight_, 1};
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer_, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1,
&region);
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = 0;
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0,
0, nullptr, 0, nullptr, 1, &barrier);
}
void GuiRenderer::Resize(uint32_t width, uint32_t height, VkFormat format) {
if (width == canvasWidth_ && height == canvasHeight_ && format == swapchainFormat_) {
return;
}
UpdateFormat(format);
EnsureCanvas(width, height);
}
void GuiRenderer::EnsureCanvas(uint32_t width, uint32_t height) {
if (width == canvasWidth_ && height == canvasHeight_) {
return;
}
canvasWidth_ = width;
canvasHeight_ = height;
if (canvas_) {
canvas_->Resize(width, height);
}
size_t bufferSize = static_cast<size_t>(canvasWidth_) * canvasHeight_ * 4;
CreateStagingBuffer(bufferSize);
}
void GuiRenderer::UpdateStagingBuffer() {
if (!stagingMapped_ || !canvas_) {
return;
}
const auto& pixels = canvas_->Pixels();
size_t pixelCount = static_cast<size_t>(canvasWidth_) * canvasHeight_;
uint8_t* dest = reinterpret_cast<uint8_t*>(stagingMapped_);
for (size_t i = 0; i < pixelCount; ++i) {
size_t offset = i * 4;
uint8_t r = pixels[offset];
uint8_t g = pixels[offset + 1];
uint8_t b = pixels[offset + 2];
uint8_t a = pixels[offset + 3];
switch (swapchainFormat_) {
case VK_FORMAT_B8G8R8A8_UNORM:
case VK_FORMAT_B8G8R8A8_SRGB:
dest[offset] = b;
dest[offset + 1] = g;
dest[offset + 2] = r;
dest[offset + 3] = a;
break;
case VK_FORMAT_R8G8B8A8_UNORM:
case VK_FORMAT_R8G8B8A8_SRGB:
default:
dest[offset] = r;
dest[offset + 1] = g;
dest[offset + 2] = b;
dest[offset + 3] = a;
break;
}
}
}
void GuiRenderer::CreateStagingBuffer(size_t size) {
DestroyStagingBuffer();
if (size == 0) {
return;
}
vulkan::CreateBuffer(device_, physicalDevice_, static_cast<VkDeviceSize>(size),
VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
stagingBuffer_, stagingMemory_);
stagingSize_ = size;
vkMapMemory(device_, stagingMemory_, 0, stagingSize_, 0, &stagingMapped_);
}
void GuiRenderer::DestroyStagingBuffer() {
if (stagingMapped_) {
vkUnmapMemory(device_, stagingMemory_);
stagingMapped_ = nullptr;
}
if (stagingBuffer_ != VK_NULL_HANDLE) {
vkDestroyBuffer(device_, stagingBuffer_, nullptr);
stagingBuffer_ = VK_NULL_HANDLE;
}
if (stagingMemory_ != VK_NULL_HANDLE) {
vkFreeMemory(device_, stagingMemory_, nullptr);
stagingMemory_ = VK_NULL_HANDLE;
}
stagingSize_ = 0;
}
void GuiRenderer::UpdateFormat(VkFormat format) {
if (swapchainFormat_ == format) {
return;
}
swapchainFormat_ = format;
DestroyStagingBuffer();
if (canvasWidth_ > 0 && canvasHeight_ > 0) {
size_t bufferSize = static_cast<size_t>(canvasWidth_) * canvasHeight_ * 4;
CreateStagingBuffer(bufferSize);
}
}
const ParsedSvg* GuiRenderer::LoadSvg(const std::string& relativePath) {
auto it = svgCache_.find(relativePath);
if (it != svgCache_.end()) {
return &it->second;
}
std::filesystem::path path = scriptDirectory_ / relativePath;
try {
ParsedSvg parsed = ParseSvgFile(path);
auto inserted = svgCache_.emplace(relativePath, std::move(parsed));
return &inserted.first->second;
} catch (...) {
return nullptr;
}
}
} // namespace sdl3cpp::gui

69
src/gui/gui_renderer.hpp Normal file
View File

@@ -0,0 +1,69 @@
#ifndef SDL3CPP_GUI_GUI_RENDERER_HPP
#define SDL3CPP_GUI_GUI_RENDERER_HPP
#include <filesystem>
#include <memory>
#include <vector>
#include <vulkan/vulkan.h>
#include script/cube_script.hpp
namespace sdl3cpp::gui {
struct SvgCircle {
float cx = 0.0f;
float cy = 0.0f;
float r = 0.0f;
script::GuiColor color{1.0f, 1.0f, 1.0f, 1.0f};
};
struct ParsedSvg {
float viewWidth = 1.0f;
float viewHeight = 1.0f;
std::vector<SvgCircle> circles;
};
class GuiRenderer {
public:
GuiRenderer(VkDevice device, VkPhysicalDevice physicalDevice, VkFormat swapchainFormat,
const std::filesystem::path& scriptDirectory);
~GuiRenderer();
GuiRenderer(const GuiRenderer&) = delete;
GuiRenderer& operator=(const GuiRenderer&) = delete;
void Prepare(const std::vector<script::CubeScript::GuiCommand>& commands, uint32_t width,
uint32_t height);
void BlitToSwapchain(VkCommandBuffer commandBuffer, VkImage image);
void Resize(uint32_t width, uint32_t height, VkFormat format);
bool IsReady() const;
private:
struct Canvas;
const ParsedSvg* LoadSvg(const std::string& relativePath);
void EnsureCanvas(uint32_t width, uint32_t height);
void UpdateStagingBuffer();
void CreateStagingBuffer(size_t size);
void DestroyStagingBuffer();
void UpdateFormat(VkFormat format);
VkDevice device_;
VkPhysicalDevice physicalDevice_;
VkFormat swapchainFormat_;
std::filesystem::path scriptDirectory_;
VkBuffer stagingBuffer_ = VK_NULL_HANDLE;
VkDeviceMemory stagingMemory_ = VK_NULL_HANDLE;
void* stagingMapped_ = nullptr;
size_t stagingSize_ = 0;
uint32_t canvasWidth_ = 0;
uint32_t canvasHeight_ = 0;
std::unique_ptr<Canvas> canvas_;
std::unordered_map<std::string, ParsedSvg> svgCache_;
};
} // namespace sdl3cpp::gui
#endif // SDL3CPP_GUI_GUI_RENDERER_HPP

View File

@@ -1,5 +1,6 @@
#include "script/cube_script.hpp"
#include <cstring>
#include <stdexcept>
#include <string>
#include <utility>
@@ -17,7 +18,8 @@ std::array<float, 16> IdentityMatrix() {
} // namespace
CubeScript::CubeScript(const std::filesystem::path& scriptPath) : L_(luaL_newstate()) {
CubeScript::CubeScript(const std::filesystem::path& scriptPath)
: L_(luaL_newstate()), scriptDirectory_(scriptPath.parent_path()) {
if (!L_) {
throw std::runtime_error("Failed to create Lua state");
}
@@ -45,10 +47,30 @@ CubeScript::CubeScript(const std::filesystem::path& scriptPath) : L_(luaL_newsta
L_ = nullptr;
throw std::runtime_error("Failed to load Lua script: " + message);
}
lua_getglobal(L_, "gui_input");
if (!lua_isnil(L_, -1)) {
guiInputRef_ = luaL_ref(L_, LUA_REGISTRYINDEX);
} else {
lua_pop(L_, 1);
}
lua_getglobal(L_, "get_gui_commands");
if (lua_isfunction(L_, -1)) {
guiCommandsFnRef_ = luaL_ref(L_, LUA_REGISTRYINDEX);
} else {
lua_pop(L_, 1);
}
}
CubeScript::~CubeScript() {
if (L_) {
if (guiInputRef_ != LUA_REFNIL) {
luaL_unref(L_, LUA_REGISTRYINDEX, guiInputRef_);
}
if (guiCommandsFnRef_ != LUA_REFNIL) {
luaL_unref(L_, LUA_REGISTRYINDEX, guiCommandsFnRef_);
}
lua_close(L_);
}
}
@@ -328,4 +350,205 @@ std::string CubeScript::LuaErrorMessage(lua_State* L) {
return message ? message : "unknown lua error";
}
std::vector<CubeScript::GuiCommand> CubeScript::LoadGuiCommands() {
std::vector<GuiCommand> commands;
if (guiCommandsFnRef_ == LUA_REFNIL) {
return commands;
}
lua_rawgeti(L_, LUA_REGISTRYINDEX, guiCommandsFnRef_);
if (lua_pcall(L_, 0, 1, 0) != LUA_OK) {
std::string message = LuaErrorMessage(L_);
lua_pop(L_, 1);
throw std::runtime_error("Lua get_gui_commands failed: " + message);
}
if (!lua_istable(L_, -1)) {
lua_pop(L_, 1);
throw std::runtime_error("'get_gui_commands' did not return a table");
}
size_t count = lua_rawlen(L_, -1);
commands.reserve(count);
for (size_t i = 1; i <= count; ++i) {
lua_rawgeti(L_, -1, static_cast<int>(i));
if (!lua_istable(L_, -1)) {
lua_pop(L_, 1);
throw std::runtime_error("GUI command at index " + std::to_string(i) + " is not a table");
}
int commandIndex = lua_gettop(L_);
lua_getfield(L_, commandIndex, "type");
const char* typeName = lua_tostring(L_, -1);
if (!typeName) {
lua_pop(L_, 2);
throw std::runtime_error("GUI command at index " + std::to_string(i) + " is missing a type");
}
GuiCommand command{};
if (std::strcmp(typeName, "rect") == 0) {
command.type = GuiCommand::Type::Rect;
command.rect = ReadRect(L_, commandIndex);
command.color = ReadColor(L_, commandIndex, GuiColor{0.0f, 0.0f, 0.0f, 1.0f});
command.borderColor = ReadColor(L_, commandIndex, GuiColor{0.0f, 0.0f, 0.0f, 0.0f});
lua_getfield(L_, commandIndex, "borderWidth");
if (lua_isnumber(L_, -1)) {
command.borderWidth = static_cast<float>(lua_tonumber(L_, -1));
}
lua_pop(L_, 1);
} else if (std::strcmp(typeName, "text") == 0) {
command.type = GuiCommand::Type::Text;
ReadStringField(L_, commandIndex, "text", command.text);
lua_getfield(L_, commandIndex, "fontSize");
if (lua_isnumber(L_, -1)) {
command.fontSize = static_cast<float>(lua_tonumber(L_, -1));
}
lua_pop(L_, 1);
std::string align;
if (ReadStringField(L_, commandIndex, "alignX", align)) {
command.alignX = align;
}
if (ReadStringField(L_, commandIndex, "alignY", align)) {
command.alignY = align;
}
lua_getfield(L_, commandIndex, "clipRect");
if (lua_istable(L_, -1)) {
command.clipRect = ReadRect(L_, -1);
command.hasClipRect = true;
}
lua_pop(L_, 1);
lua_getfield(L_, commandIndex, "bounds");
if (lua_istable(L_, -1)) {
command.bounds = ReadRect(L_, -1);
command.hasBounds = true;
}
lua_pop(L_, 1);
command.color = ReadColor(L_, commandIndex, GuiColor{1.0f, 1.0f, 1.0f, 1.0f});
} else if (std::strcmp(typeName, "clip_push") == 0) {
command.type = GuiCommand::Type::ClipPush;
command.rect = ReadRect(L_, commandIndex);
} else if (std::strcmp(typeName, "clip_pop") == 0) {
command.type = GuiCommand::Type::ClipPop;
} else if (std::strcmp(typeName, "svg") == 0) {
command.type = GuiCommand::Type::Svg;
ReadStringField(L_, commandIndex, "path", command.svgPath);
command.rect = ReadRect(L_, commandIndex);
command.svgTint = ReadColor(L_, commandIndex, GuiColor{1.0f, 1.0f, 1.0f, 0.0f});
lua_getfield(L_, commandIndex, "tint");
if (lua_istable(L_, -1)) {
command.svgTint = ReadColor(L_, -1, command.svgTint);
}
lua_pop(L_, 1);
}
lua_pop(L_, 1); // pop type
lua_pop(L_, 1); // pop command table
commands.push_back(std::move(command));
}
lua_pop(L_, 1);
return commands;
}
void CubeScript::UpdateGuiInput(const GuiInputSnapshot& input) {
if (guiInputRef_ == LUA_REFNIL) {
return;
}
lua_rawgeti(L_, LUA_REGISTRYINDEX, guiInputRef_);
int stateIndex = lua_gettop(L_);
lua_getfield(L_, stateIndex, "resetTransient");
lua_pushvalue(L_, stateIndex);
lua_call(L_, 1, 0);
lua_getfield(L_, stateIndex, "setMouse");
lua_pushvalue(L_, stateIndex);
lua_pushnumber(L_, input.mouseX);
lua_pushnumber(L_, input.mouseY);
lua_pushboolean(L_, input.mouseDown);
lua_call(L_, 4, 0);
lua_getfield(L_, stateIndex, "setWheel");
lua_pushvalue(L_, stateIndex);
lua_pushnumber(L_, input.wheel);
lua_call(L_, 2, 0);
if (!input.textInput.empty()) {
lua_getfield(L_, stateIndex, "addTextInput");
lua_pushvalue(L_, stateIndex);
lua_pushstring(L_, input.textInput.c_str());
lua_call(L_, 2, 0);
}
for (const auto& [key, pressed] : input.keyStates) {
lua_getfield(L_, stateIndex, "setKey");
lua_pushvalue(L_, stateIndex);
lua_pushstring(L_, key.c_str());
lua_pushboolean(L_, pressed);
lua_call(L_, 3, 0);
}
lua_pop(L_, 1);
}
bool CubeScript::HasGuiCommands() const {
return guiCommandsFnRef_ != LUA_REFNIL;
}
std::filesystem::path CubeScript::GetScriptDirectory() const {
return scriptDirectory_;
}
GuiRect CubeScript::ReadRect(lua_State* L, int index) {
GuiRect rect{};
if (!lua_istable(L, index)) {
return rect;
}
int absIndex = lua_absindex(L, index);
auto readField = [&](const char* name, float defaultValue) -> float {
lua_getfield(L, absIndex, name);
float value = defaultValue;
if (lua_isnumber(L, -1)) {
value = static_cast<float>(lua_tonumber(L, -1));
}
lua_pop(L, 1);
return value;
};
rect.x = readField("x", rect.x);
rect.y = readField("y", rect.y);
rect.width = readField("width", rect.width);
rect.height = readField("height", rect.height);
return rect;
}
GuiColor CubeScript::ReadColor(lua_State* L, int index, const GuiColor& defaultColor) {
GuiColor color = defaultColor;
if (!lua_istable(L, index)) {
return color;
}
int absIndex = lua_absindex(L, index);
for (int component = 0; component < 4; ++component) {
lua_rawgeti(L, absIndex, component + 1);
if (lua_isnumber(L, -1)) {
float value = static_cast<float>(lua_tonumber(L, -1));
switch (component) {
case 0: color.r = value; break;
case 1: color.g = value; break;
case 2: color.b = value; break;
case 3: color.a = value; break;
}
}
lua_pop(L, 1);
}
return color;
}
bool CubeScript::ReadStringField(lua_State* L, int index, const char* name, std::string& outString) {
int absIndex = lua_absindex(L, index);
lua_getfield(L, absIndex, name);
if (lua_isstring(L, -1)) {
outString = lua_tostring(L, -1);
lua_pop(L, 1);
return true;
}
lua_pop(L, 1);
return false;
}
} // namespace sdl3cpp::script

View File

@@ -13,6 +13,62 @@
namespace sdl3cpp::script {
struct GuiInputSnapshot {
float mouseX = 0.0f;
float mouseY = 0.0f;
bool mouseDown = false;
float wheel = 0.0f;
std::string textInput;
std::unordered_map<std::string, bool> keyStates;
};
struct GuiRect {
float x = 0;
float y = 0;
float width = 0;
float height = 0;
};
struct GuiColor {
float r = 0;
float g = 0;
float b = 0;
float a = 1.0f;
};
struct GuiCommand {
enum class Type {
Rect,
Text,
ClipPush,
ClipPop,
Svg,
};
struct RectData {
float x = 0;
float y = 0;
float width = 0;
float height = 0;
};
Type type = Type::Rect;
RectData rect;
GuiColor color;
GuiColor borderColor;
float borderWidth = 0.0f;
bool hasClipRect = false;
RectData clipRect{};
std::string text;
float fontSize = 16.0f;
std::string alignX = \"left\";
std::string alignY = \"center\";
std::string svgPath;
GuiColor svgTint;
RectData bounds{};
bool hasBounds = false;
};
class CubeScript {
public:
explicit CubeScript(const std::filesystem::path& scriptPath);
@@ -34,6 +90,10 @@ public:
std::array<float, 16> ComputeModelMatrix(int functionRef, float time);
std::array<float, 16> GetViewProjectionMatrix(float aspect);
std::unordered_map<std::string, ShaderPaths> LoadShaderPathsMap();
std::vector<GuiCommand> LoadGuiCommands();
void UpdateGuiInput(const GuiInputSnapshot& input);
bool HasGuiCommands() const;
std::filesystem::path GetScriptDirectory() const;
private:
static std::array<float, 3> ReadVector3(lua_State* L, int index);
@@ -42,8 +102,14 @@ private:
static std::vector<uint16_t> ReadIndexArray(lua_State* L, int index);
static std::string LuaErrorMessage(lua_State* L);
static ShaderPaths ReadShaderPathsTable(lua_State* L, int index);
static GuiRect ReadRect(lua_State* L, int index);
static GuiColor ReadColor(lua_State* L, int index, const GuiColor& defaultColor);
static bool ReadStringField(lua_State* L, int index, const char* name, std::string& outString);
lua_State* L_ = nullptr;
int guiInputRef_ = LUA_REFNIL;
int guiCommandsFnRef_ = LUA_REFNIL;
std::filesystem::path scriptDirectory_;
};
} // namespace sdl3cpp::script

152
third_party/font8x8_basic.h vendored Normal file
View File

@@ -0,0 +1,152 @@
/**
* 8x8 monochrome bitmap fonts for rendering
* Author: Daniel Hepper <daniel@hepper.net>
*
* License: Public Domain
*
* Based on:
* // Summary: font8x8.h
* // 8x8 monochrome bitmap fonts for rendering
* //
* // Author:
* // Marcel Sondaar
* // International Business Machines (public domain VGA fonts)
* //
* // License:
* // Public Domain
*
* Fetched from: http://dimensionalrift.homelinux.net/combuster/mos3/?p=viewsource&file=/modules/gfx/font8_8.asm
**/
// Constant: font8x8_basic
// Contains an 8x8 font map for unicode points U+0000 - U+007F (basic latin)
char font8x8_basic[128][8] = {
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0000 (nul)
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0001
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0002
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0003
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0004
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0005
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0006
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0007
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0008
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0009
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000A
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000B
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000C
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000D
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000E
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000F
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0010
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0011
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0012
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0013
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0014
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0015
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0016
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0017
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0018
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0019
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001A
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001B
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001C
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001D
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001E
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001F
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0020 (space)
{ 0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00}, // U+0021 (!)
{ 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0022 (")
{ 0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00}, // U+0023 (#)
{ 0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00}, // U+0024 ($)
{ 0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00}, // U+0025 (%)
{ 0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00}, // U+0026 (&)
{ 0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0027 (')
{ 0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00}, // U+0028 (()
{ 0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00}, // U+0029 ())
{ 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00}, // U+002A (*)
{ 0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00}, // U+002B (+)
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // U+002C (,)
{ 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00}, // U+002D (-)
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // U+002E (.)
{ 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00}, // U+002F (/)
{ 0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00}, // U+0030 (0)
{ 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00}, // U+0031 (1)
{ 0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00}, // U+0032 (2)
{ 0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00}, // U+0033 (3)
{ 0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00}, // U+0034 (4)
{ 0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00}, // U+0035 (5)
{ 0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00}, // U+0036 (6)
{ 0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00}, // U+0037 (7)
{ 0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00}, // U+0038 (8)
{ 0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00}, // U+0039 (9)
{ 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // U+003A (:)
{ 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // U+003B (;)
{ 0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00}, // U+003C (<)
{ 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00}, // U+003D (=)
{ 0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00}, // U+003E (>)
{ 0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00}, // U+003F (?)
{ 0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00}, // U+0040 (@)
{ 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00}, // U+0041 (A)
{ 0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00}, // U+0042 (B)
{ 0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00}, // U+0043 (C)
{ 0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00}, // U+0044 (D)
{ 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00}, // U+0045 (E)
{ 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00}, // U+0046 (F)
{ 0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00}, // U+0047 (G)
{ 0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00}, // U+0048 (H)
{ 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0049 (I)
{ 0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00}, // U+004A (J)
{ 0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00}, // U+004B (K)
{ 0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00}, // U+004C (L)
{ 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00}, // U+004D (M)
{ 0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00}, // U+004E (N)
{ 0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00}, // U+004F (O)
{ 0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00}, // U+0050 (P)
{ 0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00}, // U+0051 (Q)
{ 0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00}, // U+0052 (R)
{ 0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00}, // U+0053 (S)
{ 0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0054 (T)
{ 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00}, // U+0055 (U)
{ 0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0056 (V)
{ 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00}, // U+0057 (W)
{ 0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00}, // U+0058 (X)
{ 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00}, // U+0059 (Y)
{ 0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00}, // U+005A (Z)
{ 0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00}, // U+005B ([)
{ 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00}, // U+005C (\)
{ 0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00}, // U+005D (])
{ 0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00}, // U+005E (^)
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF}, // U+005F (_)
{ 0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0060 (`)
{ 0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00}, // U+0061 (a)
{ 0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00}, // U+0062 (b)
{ 0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00}, // U+0063 (c)
{ 0x38, 0x30, 0x30, 0x3e, 0x33, 0x33, 0x6E, 0x00}, // U+0064 (d)
{ 0x00, 0x00, 0x1E, 0x33, 0x3f, 0x03, 0x1E, 0x00}, // U+0065 (e)
{ 0x1C, 0x36, 0x06, 0x0f, 0x06, 0x06, 0x0F, 0x00}, // U+0066 (f)
{ 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0067 (g)
{ 0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00}, // U+0068 (h)
{ 0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0069 (i)
{ 0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E}, // U+006A (j)
{ 0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00}, // U+006B (k)
{ 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+006C (l)
{ 0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00}, // U+006D (m)
{ 0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00}, // U+006E (n)
{ 0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00}, // U+006F (o)
{ 0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F}, // U+0070 (p)
{ 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78}, // U+0071 (q)
{ 0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00}, // U+0072 (r)
{ 0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00}, // U+0073 (s)
{ 0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00}, // U+0074 (t)
{ 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00}, // U+0075 (u)
{ 0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0076 (v)
{ 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00}, // U+0077 (w)
{ 0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00}, // U+0078 (x)
{ 0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0079 (y)
{ 0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00}, // U+007A (z)
{ 0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00}, // U+007B ({)
{ 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, // U+007C (|)
{ 0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00}, // U+007D (})
{ 0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+007E (~)
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // U+007F
};