Compare commits

...

7 Commits

19 changed files with 510 additions and 365 deletions
+26 -7
View File
@@ -113,9 +113,12 @@ TextElementConfig :: struct {
textAlignment: TextAlignment,
}
AspectRatioElementConfig :: struct {
aspectRatio: f32,
}
ImageElementConfig :: struct {
imageData: rawptr,
sourceDimensions: Dimensions,
}
CustomElementConfig :: struct {
@@ -135,9 +138,10 @@ BorderElementConfig :: struct {
width: BorderWidth,
}
ScrollElementConfig :: struct {
horizontal: bool,
vertical: bool,
ClipElementConfig :: struct {
horizontal: bool, // clip overflowing elements on the "X" axis
vertical: bool, // clip overflowing elements on the "Y" axis
childOffset: Vector2, // offsets the [X,Y] positions of all child elements, primarily for scrolling containers
}
FloatingAttachPointType :: enum EnumBackingType {
@@ -169,6 +173,11 @@ FloatingAttachToElement :: enum EnumBackingType {
Root,
}
FloatingClipToElement :: enum EnumBackingType {
None,
AttachedParent,
}
FloatingElementConfig :: struct {
offset: Vector2,
expand: Dimensions,
@@ -177,6 +186,7 @@ FloatingElementConfig :: struct {
attachment: FloatingAttachPoints,
pointerCaptureMode: PointerCaptureMode,
attachTo: FloatingAttachToElement,
clipTo: FloatingClipToElement,
}
TextRenderData :: struct {
@@ -196,7 +206,6 @@ RectangleRenderData :: struct {
ImageRenderData :: struct {
backgroundColor: Color,
cornerRadius: CornerRadius,
sourceDimensions: Dimensions,
imageData: rawptr,
}
@@ -235,7 +244,7 @@ ScrollContainerData :: struct {
scrollPosition: ^Vector2,
scrollContainerDimensions: Dimensions,
contentDimensions: Dimensions,
config: ScrollElementConfig,
config: ClipElementConfig,
// Indicates whether an actual scroll container matched the provided ID or if the default struct was returned.
found: bool,
}
@@ -333,10 +342,11 @@ ElementDeclaration :: struct {
layout: LayoutConfig,
backgroundColor: Color,
cornerRadius: CornerRadius,
aspectRatio: AspectRatioElementConfig,
image: ImageElementConfig,
floating: FloatingElementConfig,
custom: CustomElementConfig,
scroll: ScrollElementConfig,
clip: ClipElementConfig,
border: BorderElementConfig,
userData: rawptr,
}
@@ -385,6 +395,7 @@ foreign Clay {
Hovered :: proc() -> bool ---
OnHover :: proc(onHoverFunction: proc "c" (id: ElementId, pointerData: PointerData, userData: rawptr), userData: rawptr) ---
PointerOver :: proc(id: ElementId) -> bool ---
GetScrollOffset :: proc() -> Vector2 ---
GetScrollContainerData :: proc(id: ElementId) -> ScrollContainerData ---
SetMeasureTextFunction :: proc(measureTextFunction: proc "c" (text: StringSlice, config: ^TextElementConfig, userData: rawptr) -> Dimensions, userData: rawptr) ---
SetQueryScrollOffsetFunction :: proc(queryScrollOffsetFunction: proc "c" (elementId: u32, userData: rawptr) -> Vector2, userData: rawptr) ---
@@ -437,6 +448,14 @@ PaddingAll :: proc(allPadding: u16) -> Padding {
return { left = allPadding, right = allPadding, top = allPadding, bottom = allPadding }
}
BorderOutside :: proc(width: u16) -> BorderWidth {
return {width, width, width, width, 0}
}
BorderAll :: proc(width: u16) -> BorderWidth {
return {width, width, width, width, width}
}
CornerRadiusAll :: proc(radius: f32) -> CornerRadius {
return CornerRadius{radius, radius, radius, radius}
}
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+109 -13
View File
@@ -1,12 +1,12 @@
package main
import clay "../clay"
import "../renderer"
import "core:c"
import "core:fmt"
import "core:log"
import "core:mem"
import "core:os"
import clay "library:clay"
import sdl "vendor:sdl3"
WINDOW_WIDTH :: 1024
@@ -20,12 +20,10 @@ debug_enabled := false
body_text := clay.TextElementConfig {
fontId = renderer.JETBRAINS_MONO_REGULAR,
fontSize = 44,
textColor = { 1.0, 1.0, 1.0, 1.0 },
textColor = {0.0, 0.0, 0.0, 255.0},
}
main :: proc() {
defer destroy()
when ODIN_DEBUG == true {
context.logger = log.create_console_logger(lowest = .Debug)
@@ -88,7 +86,7 @@ main :: proc() {
log.error("Failed to initialize SDL:", sdl.GetError())
}
window = sdl.CreateWindow("System Controller", WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_FLAGS)
window = sdl.CreateWindow("Test", WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_FLAGS)
if window == nil {
log.error("Failed to create window:", sdl.GetError())
@@ -142,7 +140,9 @@ main :: proc() {
os.exit(1)
}
if update(cmd_buffer, frame_time - last_frame_time) {
should_quit := update(cmd_buffer, frame_time - last_frame_time)
if should_quit {
log.debug("User command to quit")
break program
}
@@ -151,9 +151,12 @@ main :: proc() {
last_frame_time = frame_time
}
destroy()
}
destroy :: proc() {
free_all(context.temp_allocator)
renderer.destroy(device)
sdl.ReleaseWindowFromGPUDevice(device, window)
sdl.DestroyWindow(window)
@@ -163,10 +166,46 @@ destroy :: proc() {
update :: proc(cmd_buffer: ^sdl.GPUCommandBuffer, delta_time: u64) -> bool {
frame_time := f32(delta_time) / 1000.0
input := input()
mouse_x, mouse_y: f32
mouse_flags := sdl.GetMouseState(&mouse_x, &mouse_y)
width, height: c.int
sdl.GetWindowSize(window, &width, &height)
window_bounds := renderer.Rectangle {
x = 0.0,
y = 0.0,
w = f32(width),
h = f32(height),
}
render_cmds: clay.ClayArray(clay.RenderCommand) = layout()
layer := renderer.begin_prepare(window_bounds)
// ===== Begin processing primitives for GPU upload =====
// Everything after begin_prepare() is uploaded in-order. We pass the layer down
// until we need a new one, after which we call new_layer()
renderer.prepare(device, window, cmd_buffer, &render_cmds, input.mouse_delta, frame_time)
// Process primitives on this layer
layout(layer)
// Process clay-specific primitives
clay_layer_bounds := renderer.Rectangle {
x = f32(width) / 2.0,
y = 0.0,
w = f32(width) / 2.0,
h = f32(height),
}
// Create a new layer, because these two scenes cannot be renderer in the same batch due to overlap
layer = renderer.new_layer(layer, clay_layer_bounds)
clay_batch := clay_layout(clay_layer_bounds)
renderer.prepare_clay_batch(
layer,
{mouse_x, mouse_y},
mouse_flags,
input.mouse_delta,
frame_time,
&clay_batch,
)
// This uploads the primitive data to the GPU
renderer.end_prepare(device, cmd_buffer)
return input.should_quit
}
@@ -211,7 +250,8 @@ draw :: proc(cmd_buffer: ^sdl.GPUCommandBuffer) {
}
}
layout :: proc() -> clay.ClayArray(clay.RenderCommand) {
clay_layout :: proc(bounds: renderer.Rectangle) -> renderer.ClayBatch {
clay.SetLayoutDimensions(clay.Dimensions{bounds.w, bounds.h})
clay.BeginLayout()
if clay.UI()(
@@ -221,14 +261,70 @@ layout :: proc() -> clay.ClayArray(clay.RenderCommand) {
layoutDirection = .TopToBottom,
sizing = {clay.SizingGrow({}), clay.SizingGrow({})},
childAlignment = {x = .Center, y = .Center},
childGap = 16,
childGap = 32,
},
backgroundColor = {0.2, 0.2, 0.2, 1.0},
backgroundColor = {200.0, 200.0, 200.0, 100.0},
},
) {
if clay.UI()(
{
id = clay.ID("RoundedRect"),
backgroundColor = {255.0, 100.0, 100.0, 255.0},
cornerRadius = clay.CornerRadius {
topLeft = 10,
topRight = 20,
bottomLeft = 40,
bottomRight = 0,
},
border = clay.BorderElementConfig {
color = {0.0, 0.0, 0.0, 255.0},
width = clay.BorderAll(5),
},
layout = {sizing = {clay.SizingFixed(240), clay.SizingFixed(80)}},
},
) {
clay.Text("3D SCENE", &body_text)
}
return clay.EndLayout()
if clay.UI()(
{
id = clay.ID("RoundedRect2"),
backgroundColor = {255.0, 100.0, 100.0, 255.0},
cornerRadius = clay.CornerRadius {
topLeft = 10,
topRight = 20,
bottomLeft = 40,
bottomRight = 0,
},
border = clay.BorderElementConfig {
color = {0.0, 0.0, 0.0, 255.0},
width = clay.BorderAll(5),
},
layout = {sizing = {clay.SizingFixed(240), clay.SizingFixed(80)}},
},
) {
}
clay.Text("Test Text", &body_text)
}
return renderer.ClayBatch{bounds, clay.EndLayout()}
}
layout :: proc(layer: ^renderer.Layer) {
bounds := layer.bounds
test_quad := renderer.quad(
pos = {bounds.x + 200, bounds.y + 200},
size = {bounds.w / 2.0, bounds.h / 2.0},
color = {0.2, 0.2, 0.8, 1},
corner_radii = {5, 10, 0, 20},
border_color = {0, 0, 0, 1},
border_width = 10,
)
renderer.prepare_quad(layer, test_quad)
text_ok, text := renderer.text(0, "Raw Text", {bounds.x + 80, bounds.y + 80})
if text_ok {
renderer.prepare_text(layer, text)
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-184
View File
@@ -1,184 +0,0 @@
package sdl3_ttf
import sdl "vendor:sdl3"
import "core:c"
foreign import lib "system:SDL3_ttf"
Font :: struct {}
Text :: struct {
text: cstring,
num_lines: c.int,
refcount: c.int,
internal: rawptr,
}
TextEngine :: struct {}
Direction :: enum c.int {
LTR = 0,
RTL,
TTB,
BTT,
}
// Normal == empty
FontStyleFlag :: enum u32 {
BOLD = 0,
ITALIC = 1,
UNDERLINE = 2,
STRIKETHROUGH = 3,
}
FontStyleFlags :: bit_set[FontStyleFlag;u32]
FONT_STYLE_NORMAL :: FontStyleFlags{}
FONT_STYLE_BOLD :: FontStyleFlags{.BOLD}
FONT_STYLE_ITALIC :: FontStyleFlags{.ITALIC}
FONT_STYLE_UNDERLINE :: FontStyleFlags{.UNDERLINE}
FONT_STYLE_STRIKETHROUGH :: FontStyleFlags{.STRIKETHROUGH}
HintingFlags :: enum c.int {
NORMAL = 0,
LIGHT,
MONO,
NONE,
LIGHT_SUBPIXEL,
}
TTF_PROP_FONT_OUTLINE_LINE_CAP_NUMBER :: "SDL_ttf.font.outline.line_cap"
TTF_PROP_FONT_OUTLINE_LINE_JOIN_NUMBER :: "SDL_ttf.font.outline.line_join"
TTF_PROP_FONT_OUTLINE_MITER_LIMIT_NUMBER :: "SDL_ttf.font.outline.miter_limit"
HorizontalAlignment :: enum c.int {
INVALID = -1,
LEFT,
CENTER,
RIGHT,
}
GPUAtlasDrawSequence :: struct {
atlas_texture: ^sdl.GPUTexture,
vertex_positions: [^]sdl.FPoint,
uvs: [^]sdl.FPoint, // Normalized
num_verticies: c.int,
indices: [^]c.int,
num_indices: c.int,
next: ^GPUAtlasDrawSequence, // If nil, this is the last text in the sequence
}
GPUTextEngineWinding :: enum c.int {
INVALID = -1,
CLOCKWISE,
COUNTERCLOCKWISE,
}
SubStringFlag :: enum u32 {
TEXT_START,
LINE_START,
LINE_END,
TEXT_END,
}
SubString :: struct {
flags: SubStringFlag,
offset: c.int,
length: c.int,
line_index: c.int,
cluster_index: c.int,
rect: sdl.Rect,
}
/// General
@(default_calling_convention = "c", link_prefix = "TTF_")
foreign lib {
Init :: proc() -> bool ---
CreateGPUTextEngine :: proc(device: ^sdl.GPUDevice) -> ^TextEngine ---
DestroyGPUTextEngine :: proc(engine: ^TextEngine) ---
Quit :: proc() ---
}
/// Fonts
@(default_calling_convention = "c", link_prefix = "TTF_")
foreign lib {
CloseFont :: proc(font: ^Font) ---
FontHasGlyph :: proc(font: ^Font, glyph: u32) -> bool ---
FontIsFixedWidth :: proc(font: ^Font) -> bool ---
GetFontAscent :: proc(font: ^Font) -> c.int ---
GetFontDescent :: proc(font: ^Font) -> c.int ---
GetFontDirection :: proc(font: ^Font) -> Direction ---
GetFontDPI :: proc(font: ^Font, hdpi: ^c.int, vdpi: ^c.int) -> bool ---
GetFontFamilyName :: proc(font: ^Font) -> cstring ---
GetFontGeneration :: proc(font: ^Font) -> u32 ---
GetFontHeight :: proc(font: ^Font) -> c.int ---
GetFontHinting :: proc(font: ^Font) -> HintingFlags ---
GetFontKerning :: proc(font: ^Font) -> bool ---
/// Returns the font's recommended spacing
GetFontLineSkip :: proc(font: ^Font) -> c.int ---
GetFontOutline :: proc(font: ^Font) -> c.int ---
GetFontProperties :: proc(font: ^Font) -> sdl.PropertiesID ---
GetFontSize :: proc(font: ^Font) -> f32 ---
GetFontStyle :: proc(font: ^Font) -> FontStyleFlags ---
GetFontStyleName :: proc(font: ^Font) -> cstring ---
GetFontWrapAlignment :: proc(font: ^Font) -> HorizontalAlignment ---
GetFreeTypeVersion :: proc(major: ^c.int, minor: ^c.int, patch: ^c.int) ---
GetGlyphMetrics :: proc(font: ^Font, glyph: u32, min_x: ^c.int, max_x: ^c.int, min_y: ^c.int, max_y: ^c.int, advance: ^c.int) -> bool ---
GetGlyphScript :: proc(glyph: u32, script: ^c.char, script_size: c.size_t) -> bool ---
/// `stream`: A `sdl.IOStream` to provide a font's file data
/// `close_io`: Close src when the font is closed, false to leave it open
/// `point_size`: Font point size to use for the newly-opened font
OpenFontIO :: proc(stream: ^sdl.IOStream, close_io: bool, point_size: f32) -> ^Font ---
OpenFont :: proc(file: cstring, point_size: f32) -> ^Font ---
SetFontDirection :: proc(font: ^Font, direction: Direction) -> bool ---
SetFontHinting :: proc(font: ^Font, hinting_flags: HintingFlags) ---
SetFontKerning :: proc(font: ^Font, enabled: bool) ---
SetFontLineSkip :: proc(font: ^Font, lineskip: c.int) ---
SetFontOutline :: proc(font: ^Font, outline: c.int) -> bool ---
SetFontScript :: proc(font: ^Font, script: cstring) -> bool ---
SetFontSize :: proc(font: ^Font, pt_size: f32) -> bool ---
SetFontSizeDPI :: proc(font: ^Font, pt_size: f32, hdpi: c.int, vdpi: c.int) -> bool ---
SetFontStyle :: proc(font: ^Font, style: FontStyleFlags) ---
SetFontWrapAlignment :: proc(font: ^Font, horizontal_alignment: HorizontalAlignment) ---
SetGPUTextEngineWinding :: proc(engine: ^TextEngine, winding: GPUTextEngineWinding) ---
}
/// Text
@(default_calling_convention = "c", link_prefix = "TTF_")
foreign lib {
AppendTextString :: proc(text: ^Text, str: cstring, length: c.size_t) -> bool ---
CreateText :: proc(engine: ^TextEngine, font: ^Font, text: cstring, length: c.size_t) -> ^Text ---
DeleteTextString :: proc(text: ^Text, offset: c.int, length: c.int) -> bool ---
DestroyText :: proc(text: ^Text) ---
GetGPUTextDrawData :: proc(text: ^Text) -> ^GPUAtlasDrawSequence ---
GetGPUTextEngineWinding :: proc(engine: ^TextEngine) -> GPUTextEngineWinding ---
GetNextTextSubString :: proc(text: ^Text, substring: ^SubString, next: ^SubString) -> bool ---
GetPreviousTextSubString :: proc(text: ^Text, substring: ^SubString, previous: ^SubString) -> bool ---
/// Calculate the dimensions of a rendered string of UTF-8 text.
GetStringSize :: proc(font: ^Font, text: cstring, length: c.size_t, w: ^c.int, h: ^c.int) -> bool ---
GetStringSizeWrapped :: proc(font: ^Font, text: cstring, length: c.size_t, wrap_width: c.int, w: ^c.int, h: ^c.int) -> bool ---
GetTextColor :: proc(text: ^Text, r: ^u8, g: ^u8, b: ^u8, a: ^u8) -> bool ---
GetTextColorFloat :: proc(text: ^Text, r: ^f32, g: ^f32, b: ^f32, a: ^f32) -> bool ---
GetTextEngine :: proc(text: ^Text) -> ^TextEngine ---
GetTextFont :: proc(text: ^Text) -> ^Font ---
GetTextPosition :: proc(text: ^Text, x: ^c.int, y: ^c.int) -> bool ---
GetTextProperties :: proc(text: ^Text) -> sdl.PropertiesID ---
GetTextSize :: proc(text: ^Text, width: ^c.int, height: ^c.int) -> bool ---
GetTextSubString :: proc(text: ^Text, offset: c.int, substring: ^SubString) -> bool ---
GetTextSubStringForLine :: proc(text: ^Text, line: c.int, substring: ^SubString) -> bool ---
GetTextSubStringForPoint :: proc(text: ^Text, x: c.int, y: c.int, substring: ^SubString) -> bool ---
GetTextSubStringsForRange :: proc(text: ^Text, offset: c.int, length: c.int, count: ^c.int) -> [^]^SubString ---
GetTextWrapping :: proc(text: ^Text, wrap_length: ^c.int) -> bool ---
GetTextWrapWidth :: proc(text: ^Text, wrap_width: ^c.int) -> bool ---
InsertTextString :: proc(text: ^Text, offset: c.int, str: cstring, length: c.size_t) -> bool ---
// Calculate how much of a UTF-8 string will fit in a given width.
MeasureString :: proc(font: ^Font, text: cstring, length: c.size_t, max_width: c.int, measured_width: ^c.int, measured_length: ^c.size_t) -> bool ---
SetTextColor :: proc(text: ^Text, r: u8, g: u8, b: u8, a: u8) -> bool ---
SetTextColorFloat :: proc(text: ^Text, r: f32, g: f32, b: f32, a: f32) -> bool ---
SetTextEngine :: proc(text: ^Text, engine: ^TextEngine) -> bool ---
SetTextFont :: proc(text: ^Text, font: ^Font) -> bool ---
SetTextPosition :: proc(text: ^Text, x: c.int, y: c.int) -> bool ---
SetTextString :: proc(text: ^Text, str: cstring, length: c.size_t) -> bool ---
SetTextWrapping :: proc(text: ^Text, wrap_length: c.int) -> bool ---
SetTextWrapWhitespaceVisible :: proc(text: ^Text, visible: bool) -> bool ---
SetTextWrapWidth :: proc(text: ^Text, wrap_width: c.int) -> bool ---
}
+2 -2
View File
@@ -13,8 +13,8 @@ fi
# Convert GLSL to SPIRV
echo "Converting GLSL shaders to SPIRV..."
mkdir -p renderer/res/shaders/compiled
cd renderer/res/shaders/raw || exit
mkdir -p res/shaders/compiled
cd res/shaders/raw || exit
glslangValidator -V quad.vert -o ../compiled/quad.vert.spv
glslangValidator -V quad.frag -o ../compiled/quad.frag.spv
glslangValidator -V text.vert -o ../compiled/text.vert.spv
+24 -10
View File
@@ -5,8 +5,6 @@ import "core:mem"
import "core:os"
import sdl "vendor:sdl3"
tmp_quads: [dynamic]Quad
QuadPipeline :: struct {
instance_buffer: Buffer,
num_instances: u32,
@@ -22,6 +20,24 @@ Quad :: struct {
_: [3]f32,
}
quad :: proc(
pos: [2]f32,
size: [2]f32,
color: [4]f32,
corner_radii := [4]f32{0, 0, 0, 0},
border_color := [4]f32{0, 0, 0, 0},
border_width: f32 = 0,
) -> Quad {
return Quad {
{pos.x, pos.y, size.x, size.y},
corner_radii,
color,
border_color,
border_width,
{0, 0, 0},
}
}
@(private)
create_quad_pipeline :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> QuadPipeline {
log.debug("Creating quad pipeline")
@@ -37,7 +53,6 @@ create_quad_pipeline :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> Qua
log.debug("Loaded", len(frag_raw), "frag bytes")
log.debug("ShaderType:", SHADER_TYPE)
vert_info := sdl.GPUShaderCreateInfo {
code_size = len(vert_raw),
code = raw_data(vert_raw),
@@ -163,6 +178,7 @@ create_quad_pipeline :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> Qua
@(private)
upload_quads :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
using global
num_quads := u32(len(tmp_quads))
size := num_quads * size_of(Quad)
@@ -193,7 +209,9 @@ draw_quads :: proc(
layer: ^Layer,
load_op: sdl.GPULoadOp,
) {
if layer.quad_len == 0 {
using global
if layer.quad_instance_len == 0 {
return
}
@@ -220,17 +238,12 @@ draw_quads :: proc(
quad_offset := layer.quad_instance_start
for &scissor, index in layer.scissors {
for &scissor, index in scissors[layer.scissor_start:][:layer.scissor_len] {
if scissor.quad_len == 0 {
continue
}
if scissor.bounds.w == 0 || scissor.bounds.h == 0 {
sdl.SetGPUScissor(render_pass, sdl.Rect{0, 0, i32(swapchain_w), i32(swapchain_h)})
} else {
sdl.SetGPUScissor(render_pass, scissor.bounds)
}
sdl.DrawGPUPrimitives(render_pass, 6, scissor.quad_len, 0, quad_offset)
quad_offset += scissor.quad_len
}
@@ -238,6 +251,7 @@ draw_quads :: proc(
}
destroy_quad_pipeline :: proc(device: ^sdl.GPUDevice) {
using global
destroy_buffer(device, &quad_pipeline.instance_buffer)
sdl.ReleaseGPUGraphicsPipeline(device, quad_pipeline.sdl_pipeline)
}
+262 -98
View File
@@ -1,13 +1,13 @@
package renderer
import clay "../clay"
import "base:runtime"
import "core:c"
import "core:log"
import "core:os"
import "core:strings"
import clay "library:clay"
import sdl_ttf "library:sdl3_ttf"
import sdl "vendor:sdl3"
import sdl_ttf "vendor:sdl3/ttf"
when ODIN_OS == .Darwin {
SHADER_TYPE :: sdl.GPUShaderFormat{.MSL}
@@ -18,24 +18,85 @@ when ODIN_OS == .Darwin {
}
BUFFER_INIT_SIZE: u32 : 256
INITIAL_LAYER_SIZE :: 5
INITIAL_SCISSOR_SIZE :: 10
dpi_scaling: f32 = 1.0
layers: [dynamic]Layer
quad_pipeline: QuadPipeline
text_pipeline: TextPipeline
odin_context: runtime.Context
Global :: struct {
dpi_scaling: f32,
curr_layer_index: uint,
layers: [dynamic]Layer,
max_layers: int,
scissors: [dynamic]Scissor,
max_scissors: int,
tmp_text: [dynamic]Text,
max_tmp_text: int,
tmp_quads: [dynamic]Quad,
max_tmp_quads: int,
clay_mem: [^]u8,
clay_z_index: i16,
odin_context: runtime.Context,
quad_pipeline: QuadPipeline,
text_pipeline: TextPipeline,
}
// TODO every x frames nuke max values in case of edge cases where max gets set very high
// Called at the end of every frame
resize_global :: proc() {
using global
if len(layers) > max_layers do max_layers = len(layers)
shrink(&layers, max_layers)
if len(scissors) > max_scissors do max_scissors = len(scissors)
shrink(&scissors, max_scissors)
if len(tmp_text) > max_tmp_text do max_tmp_text = len(tmp_text)
shrink(&tmp_text, max_tmp_text)
if len(tmp_quads) > max_tmp_quads do max_tmp_quads = len(tmp_quads)
shrink(&tmp_quads, max_tmp_quads)
}
destroy :: proc(device: ^sdl.GPUDevice) {
using global
delete(layers)
delete(scissors)
delete(tmp_text)
delete(tmp_quads)
free(clay_mem)
destroy_quad_pipeline(device)
destroy_text_pipeline(device)
}
clear_global :: proc() {
using global
curr_layer_index = 0
clay_z_index = 0
clear(&layers)
clear(&scissors)
clear(&tmp_text)
clear(&tmp_quads)
}
global: Global
Rectangle :: struct {
x: f32,
y: f32,
w: f32,
h: f32,
}
// TODO New layer for each z-index/batch
Layer :: struct {
bounds: Rectangle,
quad_instance_start: u32,
quad_len: u32,
quad_instance_len: u32,
text_instance_start: u32,
text_instance_len: u32,
text_vertex_start: u32,
text_vertex_len: u32,
text_index_start: u32,
text_index_len: u32,
scissors: [dynamic]Scissor,
scissor_start: u32,
scissor_len: u32,
}
Scissor :: struct {
@@ -54,22 +115,29 @@ init :: proc(
window_height: f32,
ctx: runtime.Context,
) {
odin_context = ctx
dpi_scaling = sdl.GetWindowDisplayScale(window)
log.debug("Window DPI scaling:", dpi_scaling)
min_memory_size: c.size_t = cast(c.size_t)clay.MinMemorySize()
memory := make([^]u8, min_memory_size)
arena := clay.CreateArenaWithCapacityAndMemory(min_memory_size, memory)
global = Global {
layers = make([dynamic]Layer, 0, INITIAL_LAYER_SIZE),
scissors = make([dynamic]Scissor, 0, INITIAL_SCISSOR_SIZE),
tmp_quads = make([dynamic]Quad, 0, BUFFER_INIT_SIZE),
tmp_text = make([dynamic]Text, 0, BUFFER_INIT_SIZE),
odin_context = ctx,
dpi_scaling = sdl.GetWindowDisplayScale(window),
clay_mem = make([^]u8, min_memory_size),
quad_pipeline = create_quad_pipeline(device, window),
text_pipeline = create_text_pipeline(device, window),
}
log.debug("Window DPI scaling:", global.dpi_scaling)
arena := clay.CreateArenaWithCapacityAndMemory(min_memory_size, global.clay_mem)
clay.Initialize(arena, {window_width, window_height}, {handler = clay_error_handler})
clay.SetMeasureTextFunction(measure_text, nil)
quad_pipeline = create_quad_pipeline(device, window)
text_pipeline = create_text_pipeline(device, window)
}
@(private = "file")
clay_error_handler :: proc "c" (errorData: clay.ErrorData) {
context = odin_context
context = global.odin_context
log.error("Clay error:", errorData.errorType, errorData.errorText)
}
@@ -79,6 +147,7 @@ measure_text :: proc "c" (
config: ^clay.TextElementConfig,
user_data: rawptr,
) -> clay.Dimensions {
using global
context = odin_context
text := string(text.chars[:text.length])
c_text := strings.clone_to_cstring(text, context.temp_allocator)
@@ -90,47 +159,146 @@ measure_text :: proc "c" (
return clay.Dimensions{width = f32(w) / dpi_scaling, height = f32(h) / dpi_scaling}
}
destroy :: proc(device: ^sdl.GPUDevice) {
destroy_quad_pipeline(device)
destroy_text_pipeline(device)
/// Sets up renderer to begin upload to the GPU. Returns starting `Layer` to begin processing primitives for
begin_prepare :: proc(bounds: Rectangle) -> ^Layer {
using global
// Cleanup
clear_global()
// Begin new layer
// Start a new scissor
scissor := Scissor {
bounds = sdl.Rect {
x = i32(bounds.x * dpi_scaling),
y = i32(bounds.y * dpi_scaling),
w = i32(bounds.w * dpi_scaling),
h = i32(bounds.h * dpi_scaling),
},
}
append(&scissors, scissor)
layer := Layer {
bounds = bounds,
scissor_len = 1,
}
append(&layers, layer)
return &layers[curr_layer_index]
}
/// Creates a new layer
new_layer :: proc(prev_layer: ^Layer, bounds: Rectangle) -> ^Layer {
using global
layer := Layer {
bounds = bounds,
quad_instance_start = prev_layer.quad_instance_start + prev_layer.quad_instance_len,
text_instance_start = prev_layer.text_instance_start + prev_layer.text_instance_len,
text_vertex_start = prev_layer.text_vertex_start + prev_layer.text_vertex_len,
text_index_start = prev_layer.text_index_start + prev_layer.text_index_len,
scissor_start = prev_layer.scissor_start + prev_layer.scissor_len,
scissor_len = 1,
}
append(&layers, layer)
curr_layer_index += 1
log.debug("Added new layer; curr index", curr_layer_index)
scissor := Scissor {
bounds = sdl.Rect {
x = i32(bounds.x * dpi_scaling),
y = i32(bounds.y * dpi_scaling),
w = i32(bounds.w * dpi_scaling),
h = i32(bounds.h * dpi_scaling),
},
}
append(&scissors, scissor)
return &layers[curr_layer_index]
}
end_prepare :: proc(device: ^sdl.GPUDevice, cmd_buffer: ^sdl.GPUCommandBuffer) {
// Upload primitives to GPU
copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
upload_quads(device, copy_pass)
upload_text(device, copy_pass)
sdl.EndGPUCopyPass(copy_pass)
// Resize my dynamic arrays
resize_global()
}
// ===== Built-in primitive processing =====
// TODO scissoring support if I need it for primitives
prepare_quad :: proc(layer: ^Layer, quad: Quad) {
using global
append(&tmp_quads, quad)
layer.quad_instance_len += 1
scissors[layer.scissor_start + layer.scissor_len - 1].quad_len += 1
}
// TODO need to make sure no overlap with clay render command IDs
prepare_text :: proc(layer: ^Layer, text: Text) {
using global
data := sdl_ttf.GetGPUTextDrawData(text.ref)
if data == nil {
log.error("Failed to find GPUTextDrawData for sdl_text")
}
append(&tmp_text, text)
layer.text_instance_len += 1
layer.text_vertex_len += u32(data.num_vertices)
layer.text_index_len += u32(data.num_indices)
scissors[layer.scissor_start + layer.scissor_len - 1].text_len += 1
}
// ====== Clay-specific processing ======
ClayBatch :: struct {
bounds: Rectangle,
cmds: clay.ClayArray(clay.RenderCommand),
}
/// Upload data to the GPU
prepare :: proc(
device: ^sdl.GPUDevice,
window: ^sdl.Window,
cmd_buffer: ^sdl.GPUCommandBuffer,
render_commands: ^clay.ClayArray(clay.RenderCommand),
mouse_delta: [2]f32,
prepare_clay_batch :: proc(
base_layer: ^Layer,
mouse_pos: [2]f32,
mouse_flags: sdl.MouseButtonFlags,
mouse_wheel_delta: [2]f32,
frame_time: f32,
batch: ^ClayBatch,
) {
mouse_x, mouse_y: f32
mouse_flags := sdl.GetMouseState(&mouse_x, &mouse_y)
// Currently MacOS blocks main thread when resizing, this will be fixed with next SDL3 release
window_w, window_h: c.int
window_size := sdl.GetWindowSize(window, &window_w, &window_h)
using global
// Update clay internals
clay.SetPointerState(clay.Vector2{mouse_x, mouse_y}, .LEFT in mouse_flags)
clay.UpdateScrollContainers(true, transmute(clay.Vector2)mouse_delta, frame_time)
clay.SetLayoutDimensions({f32(window_w), f32(window_h)})
clay.SetPointerState(
clay.Vector2{mouse_pos.x - base_layer.bounds.x, mouse_pos.y - base_layer.bounds.y},
.LEFT in mouse_flags,
)
clay.UpdateScrollContainers(true, transmute(clay.Vector2)mouse_wheel_delta, frame_time)
clear(&layers)
clear(&tmp_quads)
clear(&tmp_text)
tmp_quads = make([dynamic]Quad, 0, quad_pipeline.num_instances, context.temp_allocator)
tmp_text = make([dynamic]Text, 0, 20, context.temp_allocator)
layer := Layer {
scissors = make([dynamic]Scissor, 0, 10, context.temp_allocator),
}
scissor := Scissor{}
layer := base_layer
// Parse render commands
for i in 0 ..< int(render_commands.length) {
render_command := clay.RenderCommandArray_Get(render_commands, cast(i32)i)
bounds := render_command.boundingBox
for i in 0 ..< int(batch.cmds.length) {
render_command := clay.RenderCommandArray_Get(&batch.cmds, cast(i32)i)
// Translate bounding box of the primitive by the layer position
bounds := Rectangle {
x = render_command.boundingBox.x + layer.bounds.x,
y = render_command.boundingBox.y + layer.bounds.y,
w = render_command.boundingBox.width,
h = render_command.boundingBox.height,
}
if render_command.zIndex > clay_z_index {
log.debug(
"Higher zIndex found, creating new layer & setting z_index to",
render_command.zIndex,
)
layer = new_layer(layer, bounds)
// Update bounds to new layer offset
bounds.x = render_command.boundingBox.x + layer.bounds.x
bounds.y = render_command.boundingBox.y + layer.bounds.y
clay_z_index = render_command.zIndex
}
switch (render_command.commandType) {
case clay.RenderCommandType.None:
@@ -151,10 +319,13 @@ prepare :: proc(
text_pipeline.cache[render_command.id] = sdl_text
} else {
// Update text with c_string
sdl_ttf.SetTextString(sdl_text, c_text, 0)
_ = sdl_ttf.SetTextString(sdl_text, c_text, 0)
}
data := sdl_ttf.GetGPUTextDrawData(sdl_text)
if data == nil {
log.error("Failed to find GPUTextDrawData for sdl_text:", c_text)
}
if sdl_text == nil {
log.error("Could not create SDL text:", sdl.GetError())
@@ -164,53 +335,61 @@ prepare :: proc(
Text{sdl_text, {bounds.x, bounds.y}, f32_color(render_data.textColor)},
)
layer.text_instance_len += 1
layer.text_vertex_len += u32(data.num_verticies)
layer.text_vertex_len += u32(data.num_vertices)
layer.text_index_len += u32(data.num_indices)
scissor.text_len += 1
scissors[layer.scissor_start + layer.scissor_len - 1].text_len += 1
}
case clay.RenderCommandType.Image:
case clay.RenderCommandType.ScissorStart:
bounds := sdl.Rect {
if bounds.w == 0 || bounds.h == 0 {
continue
}
curr_scissor := &scissors[layer.scissor_start + layer.scissor_len - 1]
if curr_scissor.quad_len != 0 || curr_scissor.text_len != 0 {
// Scissor has some content, need to make a new scissor
new := Scissor {
quad_start = curr_scissor.quad_start + curr_scissor.quad_len,
text_start = curr_scissor.text_start + curr_scissor.text_len,
bounds = sdl.Rect {
c.int(bounds.x * dpi_scaling),
c.int(bounds.y * dpi_scaling),
c.int(bounds.width * dpi_scaling),
c.int(bounds.height * dpi_scaling),
c.int(bounds.w * dpi_scaling),
c.int(bounds.h * dpi_scaling),
},
}
append(&scissors, new)
layer.scissor_len += 1
} else {
curr_scissor.bounds = sdl.Rect {
c.int(bounds.x * dpi_scaling),
c.int(bounds.y * dpi_scaling),
c.int(bounds.w * dpi_scaling),
c.int(bounds.h * dpi_scaling),
}
new := new_scissor(&scissor)
if scissor.quad_len != 0 || scissor.text_len != 0 {
append(&layer.scissors, scissor)
}
scissor = new
scissor.bounds = bounds
case clay.RenderCommandType.ScissorEnd:
new := new_scissor(&scissor)
if scissor.quad_len != 0 || scissor.text_len != 0 {
append(&layer.scissors, scissor)
}
scissor = new
case clay.RenderCommandType.Rectangle:
render_data := render_command.renderData.rectangle
color := f32_color(render_data.backgroundColor)
cr := render_data.cornerRadius
quad := Quad {
position_scale = {bounds.x, bounds.y, bounds.width, bounds.height},
corner_radii = {cr.topLeft, cr.topRight, cr.bottomRight, cr.bottomLeft},
position_scale = {bounds.x, bounds.y, bounds.w, bounds.h},
corner_radii = {cr.bottomRight, cr.topRight, cr.bottomLeft, cr.topLeft},
color = color,
}
append(&tmp_quads, quad)
layer.quad_len += 1
scissor.quad_len += 1
layer.quad_instance_len += 1
scissors[layer.scissor_start + layer.scissor_len - 1].quad_len += 1
case clay.RenderCommandType.Border:
render_data := render_command.renderData.border
cr := render_data.cornerRadius
quad := Quad {
position_scale = {bounds.x, bounds.y, bounds.width, bounds.height},
corner_radii = {cr.topLeft, cr.topRight, cr.bottomRight, cr.bottomLeft},
//TODO: I was using a hack here to get the underlying color of the quad in the layout and then pass it into the
// right border color, but Clay got rid of multi color support for borders so I need to just make a dedicated border pipeline
color = f32_color(
clay.Color{render_data.color.r, render_data.color.g, render_data.color.b, 0.0},
),
position_scale = {bounds.x, bounds.y, bounds.w, bounds.h},
corner_radii = {cr.bottomRight, cr.topRight, cr.bottomLeft, cr.topLeft},
color = f32_color(clay.Color{0.0, 0.0, 0.0, 0.0}),
border_color = f32_color(render_data.color),
// We only support one border width at the moment
border_width = f32(render_data.width.top),
@@ -218,25 +397,16 @@ prepare :: proc(
// Technically these should be drawn on top of everything else including children, but
// for our use case we can just chuck these in with the quad pipeline
append(&tmp_quads, quad)
layer.quad_len += 1
scissor.quad_len += 1
layer.quad_instance_len += 1
scissors[layer.scissor_start + layer.scissor_len - 1].quad_len += 1
case clay.RenderCommandType.Custom:
}
}
//TODO start new layers with z-index changes
append(&layer.scissors, scissor)
append(&layers, layer)
// Upload primitives to GPU
copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
upload_quads(device, copy_pass)
upload_text(device, copy_pass)
sdl.EndGPUCopyPass(copy_pass)
}
/// Render primitives
draw :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, cmd_buffer: ^sdl.GPUCommandBuffer) {
using global
swapchain_texture: ^sdl.GPUTexture
w, h: u32
if !sdl.WaitAndAcquireGPUSwapchainTexture(cmd_buffer, window, &swapchain_texture, &w, &h) {
@@ -250,6 +420,7 @@ draw :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, cmd_buffer: ^sdl.GPUCo
}
for &layer, index in layers {
log.debug("Drawing layer", index)
draw_quads(
device,
window,
@@ -261,7 +432,7 @@ draw :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, cmd_buffer: ^sdl.GPUCo
index == 0 ? sdl.GPULoadOp.CLEAR : sdl.GPULoadOp.LOAD,
)
draw_text(device, window, cmd_buffer, swapchain_texture, w, h, &layer)
//TODO draw other primitives in layer
//TODO draw other primitives in layer once I add support for them :)
}
}
@@ -293,15 +464,8 @@ Globals :: struct {
push_globals :: proc(cmd_buffer: ^sdl.GPUCommandBuffer, w: f32, h: f32) {
globals := Globals {
ortho_rh(left = 0.0, top = 0.0, right = f32(w), bottom = f32(h), near = -1.0, far = 1.0),
dpi_scaling,
global.dpi_scaling,
}
sdl.PushGPUVertexUniformData(cmd_buffer, 0, &globals, size_of(Globals))
}
new_scissor :: proc(old: ^Scissor) -> Scissor {
return Scissor {
quad_start = old.quad_start + old.quad_len,
text_start = old.text_start + old.text_len,
}
}
+58 -22
View File
@@ -4,16 +4,14 @@ import "core:c"
import "core:log"
import "core:mem"
import "core:os"
import sdl_ttf "library:sdl3_ttf"
import sdl "vendor:sdl3"
import sdl_ttf "vendor:sdl3/ttf"
JETBRAINS_MONO_REGULAR: u16 : 0
JETBRAINS_MONO_BOLD: u16 : 1
NUM_FONTS :: 2
MAX_FONT_SIZE :: 120
tmp_text: [dynamic]Text
@(private = "file")
jetbrains_mono_regular := #load("res/fonts/JetBrainsMono-Regular.ttf")
@(private = "file")
@@ -31,7 +29,7 @@ TextPipeline :: struct {
}
get_font :: proc(id: u16, size: u16) -> ^sdl_ttf.Font {
font := text_pipeline.fonts[id > 1 ? 0 : id][size > 0 ? size : 16]
font := global.text_pipeline.fonts[id > 1 ? 0 : id][size > 0 ? size : 16]
if font == nil {
log.debug("Font not found for size", size, "+ adding")
@@ -45,8 +43,13 @@ get_font :: proc(id: u16, size: u16) -> ^sdl_ttf.Font {
os.exit(1)
}
font = f
sdl_ttf.SetFontSizeDPI(f, f32(size), 72 * i32(dpi_scaling), 72 * i32(dpi_scaling))
text_pipeline.fonts[id][size] = f
_ = sdl_ttf.SetFontSizeDPI(
f,
f32(size),
72 * i32(global.dpi_scaling),
72 * i32(global.dpi_scaling),
)
global.text_pipeline.fonts[id][size] = f
}
return font
@@ -58,6 +61,38 @@ Text :: struct {
color: [4]f32,
}
text :: proc(
id: u32,
txt: cstring,
pos: [2]f32,
color: [4]f32 = {0.0, 0.0, 0.0, 1.0},
font_id: u16 = JETBRAINS_MONO_REGULAR,
font_size: u16 = 44,
) -> (bool, Text) {
using global
sdl_text := text_pipeline.cache[id]
if sdl_text == nil {
sdl_text = sdl_ttf.CreateText(text_pipeline.engine, get_font(font_id, font_size), txt, 0)
text_pipeline.cache[id] = sdl_text
} else {
//TODO if IDs are always unique and never change the underlying text
// can get rid of this
_ = sdl_ttf.SetTextString(sdl_text, txt, 0)
}
if sdl_text == nil {
log.error("Could not create SDL text:", sdl.GetError())
return false, Text {}
} else {
return true, Text {
sdl_text,
pos,
color,
}
}
}
// For upload
TextVert :: struct {
pos_uv: [4]f32,
@@ -205,7 +240,7 @@ create_text_pipeline :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> Tex
log.error("Could not create text engine")
os.exit(1)
}
sdl_ttf.SetGPUTextEngineWinding(engine, .COUNTERCLOCKWISE)
sdl_ttf.SetGPUTextEngineWinding(engine, .COUNTER_CLOCKWISE)
// Create buffers
vertex_buffer := create_buffer(
@@ -241,18 +276,20 @@ create_text_pipeline :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> Tex
@(private)
upload_text :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
using global
// TODO maybe don't use tmp here
vertices := make([dynamic]TextVert, 0, BUFFER_INIT_SIZE, context.temp_allocator)
indices := make([dynamic]c.int, 0, BUFFER_INIT_SIZE, context.temp_allocator)
instances := make([dynamic][2]f32, 0, BUFFER_INIT_SIZE, context.temp_allocator)
for &text, index in tmp_text {
for &text, index in global.tmp_text {
append(&instances, text.position)
data := sdl_ttf.GetGPUTextDrawData(text.ref)
for data != nil {
for i in 0 ..< data.num_verticies {
pos := data.vertex_positions[i]
uv := data.uvs[i]
for i in 0 ..< data.num_vertices {
pos := data.xy[i]
uv := data.uv[i]
color := text.color
append(&vertices, TextVert{{pos.x, -pos.y, uv.x, uv.y}, color})
}
@@ -345,6 +382,7 @@ draw_text :: proc(
swapchain_h: u32,
layer: ^Layer,
) {
using global
if layer.text_instance_len == 0 {
return
}
@@ -377,24 +415,19 @@ draw_text :: proc(
atlas: ^sdl.GPUTexture
layer_text := tmp_text[layer.text_instance_start:layer.text_instance_start +
layer.text_instance_len]
index_offset: u32 = layer.text_instance_start
layer_text := tmp_text[layer.text_instance_start:][:layer.text_instance_len]
index_offset: u32 = layer.text_index_start
vertex_offset: i32 = i32(layer.text_vertex_start)
instance_offset: u32 = layer.text_instance_start
for &scissor, index in layer.scissors {
for &scissor, index in scissors[layer.scissor_start:][:layer.scissor_len] {
if scissor.text_len == 0 {
continue
}
if scissor.bounds.w == 0 || scissor.bounds.h == 0 {
sdl.SetGPUScissor(render_pass, sdl.Rect{0, 0, i32(swapchain_w), i32(swapchain_h)})
} else {
sdl.SetGPUScissor(render_pass, scissor.bounds)
}
for &text in layer_text[scissor.text_start:scissor.text_start + scissor.text_len] {
for &text in layer_text[scissor.text_start:][:scissor.text_len] {
data := sdl_ttf.GetGPUTextDrawData(text.ref)
for data != nil {
@@ -421,7 +454,7 @@ draw_text :: proc(
)
index_offset += u32(data.num_indices)
vertex_offset += data.num_verticies
vertex_offset += data.num_vertices
data = data.next
}
@@ -434,7 +467,10 @@ draw_text :: proc(
}
destroy_text_pipeline :: proc(device: ^sdl.GPUDevice) {
using global
destroy_buffer(device, &text_pipeline.vertex_buffer)
destroy_buffer(device, &text_pipeline.index_buffer)
destroy_buffer(device, &text_pipeline.instance_buffer)
delete(text_pipeline.cache)
sdl.ReleaseGPUGraphicsPipeline(device, text_pipeline.sdl_pipeline)
}