Compare commits

..

5 Commits

Author SHA1 Message Date
Zachary Levy 1e2c2936a8 Platform checks 2026-04-20 10:13:36 -07:00
Zachary Levy 90fba74243 Custom draw
tuneup
2026-04-19 22:10:01 -07:00
Zachary Levy 0953462b3b Massive renaming 2026-04-19 19:42:37 -07:00
Zachary Levy 7a21d6f253 Added improved non-clay text handling along with consistent origin and rotation API 2026-04-19 18:28:42 -07:00
Zachary Levy 30b72128b2 Foramatting 2026-04-19 07:37:29 -07:00
14 changed files with 1673 additions and 713 deletions
+10
View File
@@ -55,6 +55,16 @@
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- hellope-shapes", "command": "odin run draw/examples -debug -out=out/debug/draw-examples -- hellope-shapes",
"cwd": "$ZED_WORKTREE_ROOT", "cwd": "$ZED_WORKTREE_ROOT",
}, },
{
"label": "Run draw hellope-text example",
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- hellope-text",
"cwd": "$ZED_WORKTREE_ROOT",
},
{
"label": "Run draw hellope-custom example",
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- hellope-custom",
"cwd": "$ZED_WORKTREE_ROOT",
},
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// ----- Other ------------------------ // ----- Other ------------------------
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
+23
View File
@@ -555,3 +555,26 @@ odin run meta -- gen-shaders
``` ```
Requires `glslangValidator` and `spirv-cross` on PATH. Requires `glslangValidator` and `spirv-cross` on PATH.
### Shader format selection
The library embeds shader bytecode per compile target — MSL + `main0` entry point on Darwin (via
`spirv-cross --msl`, which renames `main` because it is reserved in Metal), SPIR-V + `main` entry
point elsewhere. Three compile-time constants in `draw.odin` expose the build's shader configuration:
| Constant | Type | Darwin | Other |
| ----------------------------- | ------------------------- | --------- | ---------- |
| `PLATFORM_SHADER_FORMAT_FLAG` | `sdl.GPUShaderFormatFlag` | `.MSL` | `.SPIRV` |
| `PLATFORM_SHADER_FORMAT` | `sdl.GPUShaderFormat` | `{.MSL}` | `{.SPIRV}` |
| `SHADER_ENTRY` | `cstring` | `"main0"` | `"main"` |
Pass `PLATFORM_SHADER_FORMAT` to `sdl.CreateGPUDevice` so SDL selects a backend compatible with the
embedded bytecode:
```
gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
```
At init time the library calls `sdl.GetGPUShaderFormats(device)` to verify the active backend
accepts `PLATFORM_SHADER_FORMAT_FLAG`. If it does not, `draw.init` returns `false` with a
descriptive log message showing both the embedded and active format sets.
+292 -113
View File
@@ -1,21 +1,27 @@
package draw package draw
import clay "../vendor/clay"
import "base:runtime" import "base:runtime"
import "core:c" import "core:c"
import "core:log" import "core:log"
import "core:math"
import "core:strings" import "core:strings"
import sdl "vendor:sdl3" import sdl "vendor:sdl3"
import sdl_ttf "vendor:sdl3/ttf" import sdl_ttf "vendor:sdl3/ttf"
import clay "../vendor/clay"
when ODIN_OS == .Darwin { when ODIN_OS == .Darwin {
SHADER_TYPE :: sdl.GPUShaderFormat{.MSL} PLATFORM_SHADER_FORMAT_FLAG :: sdl.GPUShaderFormatFlag.MSL
ENTRY_POINT :: "main0" SHADER_ENTRY :: cstring("main0")
BASE_VERT_2D_RAW :: #load("shaders/generated/base_2d.vert.metal")
BASE_FRAG_2D_RAW :: #load("shaders/generated/base_2d.frag.metal")
} else { } else {
SHADER_TYPE :: sdl.GPUShaderFormat{.SPIRV} PLATFORM_SHADER_FORMAT_FLAG :: sdl.GPUShaderFormatFlag.SPIRV
ENTRY_POINT :: "main" SHADER_ENTRY :: cstring("main")
BASE_VERT_2D_RAW :: #load("shaders/generated/base_2d.vert.spv")
BASE_FRAG_2D_RAW :: #load("shaders/generated/base_2d.frag.spv")
} }
PLATFORM_SHADER_FORMAT :: sdl.GPUShaderFormat{PLATFORM_SHADER_FORMAT_FLAG}
BUFFER_INIT_SIZE :: 256 BUFFER_INIT_SIZE :: 256
INITIAL_LAYER_SIZE :: 5 INITIAL_LAYER_SIZE :: 5
@@ -35,14 +41,14 @@ BLUE :: Color{0, 0, 255, 255}
BLANK :: Color{0, 0, 0, 0} BLANK :: Color{0, 0, 0, 0}
// Convert clay.Color ([4]c.float in 0255 range) to Color. // Convert clay.Color ([4]c.float in 0255 range) to Color.
color_from_clay :: proc(cc: clay.Color) -> Color { color_from_clay :: proc(clay_color: clay.Color) -> Color {
return Color{u8(cc[0]), u8(cc[1]), u8(cc[2]), u8(cc[3])} return Color{u8(clay_color[0]), u8(clay_color[1]), u8(clay_color[2]), u8(clay_color[3])}
} }
// Convert Color to [4]f32 in 0.01.0 range. Useful for SDL interop (e.g. clear color). // Convert Color to [4]f32 in 0.01.0 range. Useful for SDL interop (e.g. clear color).
color_to_f32 :: proc(c: Color) -> [4]f32 { color_to_f32 :: proc(color: Color) -> [4]f32 {
INV :: 1.0 / 255.0 INV :: 1.0 / 255.0
return {f32(c[0]) * INV, f32(c[1]) * INV, f32(c[2]) * INV, f32(c[3]) * INV} return {f32(color[0]) * INV, f32(color[1]) * INV, f32(color[2]) * INV, f32(color[3]) * INV}
} }
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@@ -50,16 +56,16 @@ color_to_f32 :: proc(c: Color) -> [4]f32 {
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
Rectangle :: struct { Rectangle :: struct {
x: f32, x: f32,
y: f32, y: f32,
w: f32, width: f32,
h: f32, height: f32,
} }
Sub_Batch_Kind :: enum u8 { Sub_Batch_Kind :: enum u8 {
Shapes, // non-indexed, white texture, mode 0 Shapes, // non-indexed, white texture, mode 0
Text, // indexed, atlas texture, mode 0 Text, // indexed, atlas texture, mode 0
SDF, // instanced unit quad, white texture, mode 1 SDF, // instanced unit quad, white texture, mode 1
} }
Sub_Batch :: struct { Sub_Batch :: struct {
@@ -100,7 +106,8 @@ Global :: struct {
tmp_text_batches: [dynamic]TextBatch, tmp_text_batches: [dynamic]TextBatch,
tmp_primitives: [dynamic]Primitive, tmp_primitives: [dynamic]Primitive,
tmp_sub_batches: [dynamic]Sub_Batch, tmp_sub_batches: [dynamic]Sub_Batch,
clay_mem: [^]u8, tmp_uncached_text: [dynamic]^sdl_ttf.Text, // Uncached TTF_Text objects to destroy after end()
clay_memory: [^]u8,
msaa_texture: ^sdl.GPUTexture, msaa_texture: ^sdl.GPUTexture,
curr_layer_index: uint, curr_layer_index: uint,
max_layers: int, max_layers: int,
@@ -112,8 +119,8 @@ Global :: struct {
max_primitives: int, max_primitives: int,
max_sub_batches: int, max_sub_batches: int,
dpi_scaling: f32, dpi_scaling: f32,
msaa_w: u32, msaa_width: u32,
msaa_h: u32, msaa_height: u32,
sample_count: sdl.GPUSampleCount, sample_count: sdl.GPUSampleCount,
clay_z_index: i16, clay_z_index: i16,
cleared: bool, cleared: bool,
@@ -161,33 +168,34 @@ init :: proc(
} }
GLOB = Global { GLOB = Global {
layers = make([dynamic]Layer, 0, INITIAL_LAYER_SIZE, allocator = allocator), layers = make([dynamic]Layer, 0, INITIAL_LAYER_SIZE, allocator = allocator),
scissors = make([dynamic]Scissor, 0, INITIAL_SCISSOR_SIZE, allocator = allocator), scissors = make([dynamic]Scissor, 0, INITIAL_SCISSOR_SIZE, allocator = allocator),
tmp_shape_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator), tmp_shape_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator),
tmp_text_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator), tmp_text_verts = make([dynamic]Vertex, 0, BUFFER_INIT_SIZE, allocator = allocator),
tmp_text_indices = make([dynamic]c.int, 0, BUFFER_INIT_SIZE, allocator = allocator), tmp_text_indices = make([dynamic]c.int, 0, BUFFER_INIT_SIZE, allocator = allocator),
tmp_text_batches = make([dynamic]TextBatch, 0, BUFFER_INIT_SIZE, allocator = allocator), tmp_text_batches = make([dynamic]TextBatch, 0, BUFFER_INIT_SIZE, allocator = allocator),
tmp_primitives = make([dynamic]Primitive, 0, BUFFER_INIT_SIZE, allocator = allocator), tmp_primitives = make([dynamic]Primitive, 0, BUFFER_INIT_SIZE, allocator = allocator),
tmp_sub_batches = make([dynamic]Sub_Batch, 0, BUFFER_INIT_SIZE, allocator = allocator), tmp_sub_batches = make([dynamic]Sub_Batch, 0, BUFFER_INIT_SIZE, allocator = allocator),
odin_context = odin_context, tmp_uncached_text = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator),
dpi_scaling = sdl.GetWindowDisplayScale(window), odin_context = odin_context,
clay_mem = make([^]u8, min_memory_size, allocator = allocator), dpi_scaling = sdl.GetWindowDisplayScale(window),
sample_count = resolved_sample_count, clay_memory = make([^]u8, min_memory_size, allocator = allocator),
pipeline_2d_base = pipeline, sample_count = resolved_sample_count,
text_cache = text_cache, pipeline_2d_base = pipeline,
text_cache = text_cache,
} }
log.debug("Window DPI scaling:", GLOB.dpi_scaling) log.debug("Window DPI scaling:", GLOB.dpi_scaling)
arena := clay.CreateArenaWithCapacityAndMemory(min_memory_size, GLOB.clay_mem) arena := clay.CreateArenaWithCapacityAndMemory(min_memory_size, GLOB.clay_memory)
window_width, window_height: c.int window_width, window_height: c.int
sdl.GetWindowSize(window, &window_width, &window_height) sdl.GetWindowSize(window, &window_width, &window_height)
clay.Initialize(arena, {f32(window_width), f32(window_height)}, {handler = clay_error_handler}) clay.Initialize(arena, {f32(window_width), f32(window_height)}, {handler = clay_error_handler})
clay.SetMeasureTextFunction(measure_text, nil) clay.SetMeasureTextFunction(measure_text_clay, nil)
return true return true
} }
// TODO every x frames nuke max values in case of edge cases where max gets set very high // TODO Either every x frames nuke max values in case of edge cases where max gets set very high
// Called at the end of every frame // or leave to application code to decide the right time for resize
resize_global :: proc() { resize_global :: proc() {
if len(GLOB.layers) > GLOB.max_layers do GLOB.max_layers = len(GLOB.layers) if len(GLOB.layers) > GLOB.max_layers do GLOB.max_layers = len(GLOB.layers)
shrink(&GLOB.layers, GLOB.max_layers) shrink(&GLOB.layers, GLOB.max_layers)
@@ -216,7 +224,9 @@ destroy :: proc(device: ^sdl.GPUDevice, allocator := context.allocator) {
delete(GLOB.tmp_text_batches) delete(GLOB.tmp_text_batches)
delete(GLOB.tmp_primitives) delete(GLOB.tmp_primitives)
delete(GLOB.tmp_sub_batches) delete(GLOB.tmp_sub_batches)
free(GLOB.clay_mem, allocator) for ttf_text in GLOB.tmp_uncached_text do sdl_ttf.DestroyText(ttf_text)
delete(GLOB.tmp_uncached_text)
free(GLOB.clay_memory, allocator)
if GLOB.msaa_texture != nil { if GLOB.msaa_texture != nil {
sdl.ReleaseGPUTexture(device, GLOB.msaa_texture) sdl.ReleaseGPUTexture(device, GLOB.msaa_texture)
} }
@@ -229,6 +239,9 @@ clear_global :: proc() {
GLOB.curr_layer_index = 0 GLOB.curr_layer_index = 0
GLOB.clay_z_index = 0 GLOB.clay_z_index = 0
GLOB.cleared = false GLOB.cleared = false
// Destroy uncached TTF_Text objects from the previous frame (after end() has submitted draw data)
for ttf_text in GLOB.tmp_uncached_text do sdl_ttf.DestroyText(ttf_text)
clear(&GLOB.tmp_uncached_text)
clear(&GLOB.layers) clear(&GLOB.layers)
clear(&GLOB.scissors) clear(&GLOB.scissors)
clear(&GLOB.tmp_shape_verts) clear(&GLOB.tmp_shape_verts)
@@ -244,7 +257,7 @@ clear_global :: proc() {
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@(private = "file") @(private = "file")
measure_text :: proc "c" ( measure_text_clay :: proc "c" (
text: clay.StringSlice, text: clay.StringSlice,
config: ^clay.TextElementConfig, config: ^clay.TextElementConfig,
user_data: rawptr, user_data: rawptr,
@@ -252,12 +265,12 @@ measure_text :: proc "c" (
context = GLOB.odin_context context = GLOB.odin_context
text := string(text.chars[:text.length]) text := string(text.chars[:text.length])
c_text := strings.clone_to_cstring(text, context.temp_allocator) c_text := strings.clone_to_cstring(text, context.temp_allocator)
w, h: c.int width, height: c.int
if !sdl_ttf.GetStringSize(get_font(config.fontId, config.fontSize), c_text, 0, &w, &h) { if !sdl_ttf.GetStringSize(get_font(config.fontId, config.fontSize), c_text, 0, &width, &height) {
log.panicf("Failed to measure text: %s", sdl.GetError()) log.panicf("Failed to measure text: %s", sdl.GetError())
} }
return clay.Dimensions{width = f32(w) / GLOB.dpi_scaling, height = f32(h) / GLOB.dpi_scaling} return clay.Dimensions{width = f32(width) / GLOB.dpi_scaling, height = f32(height) / GLOB.dpi_scaling}
} }
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@@ -274,8 +287,8 @@ begin :: proc(bounds: Rectangle) -> ^Layer {
bounds = sdl.Rect { bounds = sdl.Rect {
x = i32(bounds.x * GLOB.dpi_scaling), x = i32(bounds.x * GLOB.dpi_scaling),
y = i32(bounds.y * GLOB.dpi_scaling), y = i32(bounds.y * GLOB.dpi_scaling),
w = i32(bounds.w * GLOB.dpi_scaling), w = i32(bounds.width * GLOB.dpi_scaling),
h = i32(bounds.h * GLOB.dpi_scaling), h = i32(bounds.height * GLOB.dpi_scaling),
}, },
} }
append(&GLOB.scissors, scissor) append(&GLOB.scissors, scissor)
@@ -305,8 +318,8 @@ new_layer :: proc(prev_layer: ^Layer, bounds: Rectangle) -> ^Layer {
bounds = sdl.Rect { bounds = sdl.Rect {
x = i32(bounds.x * GLOB.dpi_scaling), x = i32(bounds.x * GLOB.dpi_scaling),
y = i32(bounds.y * GLOB.dpi_scaling), y = i32(bounds.y * GLOB.dpi_scaling),
w = i32(bounds.w * GLOB.dpi_scaling), w = i32(bounds.width * GLOB.dpi_scaling),
h = i32(bounds.h * GLOB.dpi_scaling), h = i32(bounds.height * GLOB.dpi_scaling),
}, },
} }
append(&GLOB.scissors, scissor) append(&GLOB.scissors, scissor)
@@ -336,14 +349,19 @@ prepare_sdf_primitive :: proc(layer: ^Layer, prim: Primitive) {
// Submit a text element to the given layer for rendering. // Submit a text element to the given layer for rendering.
// Copies SDL_ttf vertices directly (with baked position) and copies indices for indexed drawing. // Copies SDL_ttf vertices directly (with baked position) and copies indices for indexed drawing.
prepare_text :: proc(layer: ^Layer, txt: Text) { prepare_text :: proc(layer: ^Layer, text: Text) {
data := sdl_ttf.GetGPUTextDrawData(txt.ref) data := sdl_ttf.GetGPUTextDrawData(text.sdl_text)
if data == nil { if data == nil {
return // nil is normal for empty text return // nil is normal for empty text
} }
scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1] scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
// Snap base position to integer physical pixels to avoid atlas sub-pixel
// sampling blur (and the off-by-one bottom-row clip that comes with it).
base_x := math.round(text.position[0] * GLOB.dpi_scaling)
base_y := math.round(text.position[1] * GLOB.dpi_scaling)
for data != nil { for data != nil {
vertex_start := u32(len(GLOB.tmp_text_verts)) vertex_start := u32(len(GLOB.tmp_text_verts))
index_start := u32(len(GLOB.tmp_text_indices)) index_start := u32(len(GLOB.tmp_text_indices))
@@ -354,11 +372,7 @@ prepare_text :: proc(layer: ^Layer, txt: Text) {
uv := data.uv[i] uv := data.uv[i]
append( append(
&GLOB.tmp_text_verts, &GLOB.tmp_text_verts,
Vertex { Vertex{position = {pos.x + base_x, -pos.y + base_y}, uv = {uv.x, uv.y}, color = text.color},
position = {pos.x + txt.position[0] * GLOB.dpi_scaling, -pos.y + txt.position[1] * GLOB.dpi_scaling},
uv = {uv.x, uv.y},
color = txt.color,
},
) )
} }
@@ -384,6 +398,54 @@ prepare_text :: proc(layer: ^Layer, txt: Text) {
} }
} }
// Submit a text element with a 2D affine transform applied to vertices.
// Used by the high-level `text` proc when rotation or a non-zero origin is specified.
// NOTE: xform must be in physical (DPI-scaled) pixel space — the caller pre-scales
// pos and origin by GLOB.dpi_scaling before building the transform.
prepare_text_transformed :: proc(layer: ^Layer, text: Text, transform: Transform_2D) {
data := sdl_ttf.GetGPUTextDrawData(text.sdl_text)
if data == nil {
return
}
scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
for data != nil {
vertex_start := u32(len(GLOB.tmp_text_verts))
index_start := u32(len(GLOB.tmp_text_indices))
for i in 0 ..< data.num_vertices {
pos := data.xy[i]
uv := data.uv[i]
// SDL_ttf gives glyph positions in physical pixels relative to text origin.
// The transform is already in physical-pixel space (caller pre-scaled),
// so we apply directly — no per-vertex DPI divide/multiply.
append(
&GLOB.tmp_text_verts,
Vertex{position = apply_transform(transform, {pos.x, -pos.y}), uv = {uv.x, uv.y}, color = text.color},
)
}
append(&GLOB.tmp_text_indices, ..data.indices[:data.num_indices])
batch_idx := u32(len(GLOB.tmp_text_batches))
append(
&GLOB.tmp_text_batches,
TextBatch {
atlas_texture = data.atlas_texture,
vertex_start = vertex_start,
vertex_count = u32(data.num_vertices),
index_start = index_start,
index_count = u32(data.num_indices),
},
)
append_or_extend_sub_batch(scissor, layer, .Text, batch_idx, 1)
data = data.next
}
}
// Append a new sub-batch or extend the last one if same kind and contiguous. // Append a new sub-batch or extend the last one if same kind and contiguous.
@(private) @(private)
append_or_extend_sub_batch :: proc( append_or_extend_sub_batch :: proc(
@@ -415,6 +477,19 @@ clay_error_handler :: proc "c" (errorData: clay.ErrorData) {
log.error("Clay error:", errorData.errorType, errorData.errorText) log.error("Clay error:", errorData.errorType, errorData.errorText)
} }
// Called for each Clay `RenderCommandType.Custom` render command that
// `prepare_clay_batch` encounters.
//
// - `layer` is the layer the command belongs to (post-z-index promotion).
// - `bounds` is already translated into the active layer's coordinate system
// and pre-DPI, matching what the built-in shape procs expect.
// - `render_data` is Clay's `CustomRenderData` for the element, exposing
// `backgroundColor`, `cornerRadius`, and the `customData` pointer the caller
// attached to `clay.CustomElementConfig.customData`.
//
// The callback must not call `new_layer` or `prepare_clay_batch`.
Custom_Draw :: #type proc(layer: ^Layer, bounds: Rectangle, render_data: clay.CustomRenderData)
ClayBatch :: struct { ClayBatch :: struct {
bounds: Rectangle, bounds: Rectangle,
cmds: clay.ClayArray(clay.RenderCommand), cmds: clay.ClayArray(clay.RenderCommand),
@@ -426,6 +501,7 @@ prepare_clay_batch :: proc(
batch: ^ClayBatch, batch: ^ClayBatch,
mouse_wheel_delta: [2]f32, mouse_wheel_delta: [2]f32,
frame_time: f32 = 0, frame_time: f32 = 0,
custom_draw: Custom_Draw = nil,
) { ) {
mouse_pos: [2]f32 mouse_pos: [2]f32
mouse_flags := sdl.GetMouseState(&mouse_pos.x, &mouse_pos.y) mouse_flags := sdl.GetMouseState(&mouse_pos.x, &mouse_pos.y)
@@ -445,10 +521,10 @@ prepare_clay_batch :: proc(
// Translate bounding box of the primitive by the layer position // Translate bounding box of the primitive by the layer position
bounds := Rectangle { bounds := Rectangle {
x = render_command.boundingBox.x + layer.bounds.x, x = render_command.boundingBox.x + layer.bounds.x,
y = render_command.boundingBox.y + layer.bounds.y, y = render_command.boundingBox.y + layer.bounds.y,
w = render_command.boundingBox.width, width = render_command.boundingBox.width,
h = render_command.boundingBox.height, height = render_command.boundingBox.height,
} }
if render_command.zIndex > GLOB.clay_z_index { if render_command.zIndex > GLOB.clay_z_index {
@@ -466,32 +542,17 @@ prepare_clay_batch :: proc(
render_data := render_command.renderData.text render_data := render_command.renderData.text
txt := string(render_data.stringContents.chars[:render_data.stringContents.length]) txt := string(render_data.stringContents.chars[:render_data.stringContents.length])
c_text := strings.clone_to_cstring(txt, context.temp_allocator) c_text := strings.clone_to_cstring(txt, context.temp_allocator)
sdl_text := GLOB.text_cache.cache[render_command.id] // Clay render-command IDs are derived via Clay's internal HashNumber (Jenkins-family)
// and namespaced with .Clay so they can never collide with user-provided custom text IDs.
if sdl_text == nil { sdl_text := cache_get_or_update(
// Cache a SDL text object Cache_Key{render_command.id, .Clay},
sdl_text = sdl_ttf.CreateText( c_text,
GLOB.text_cache.engine, get_font(render_data.fontId, render_data.fontSize),
get_font(render_data.fontId, render_data.fontSize), )
c_text,
0,
)
if sdl_text == nil {
log.panicf("Failed to create SDL text for clay render command: %s", sdl.GetError())
}
GLOB.text_cache.cache[render_command.id] = sdl_text
} else {
if !sdl_ttf.SetTextString(sdl_text, c_text, 0) {
log.panicf("Failed to update SDL text string: %s", sdl.GetError())
}
}
prepare_text(layer, Text{sdl_text, {bounds.x, bounds.y}, color_from_clay(render_data.textColor)}) prepare_text(layer, Text{sdl_text, {bounds.x, bounds.y}, color_from_clay(render_data.textColor)})
case clay.RenderCommandType.Image: case clay.RenderCommandType.Image:
case clay.RenderCommandType.ScissorStart: case clay.RenderCommandType.ScissorStart:
if bounds.w == 0 || bounds.h == 0 { if bounds.width == 0 || bounds.height == 0 do continue
continue
}
curr_scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1] curr_scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
@@ -502,8 +563,8 @@ prepare_clay_batch :: proc(
bounds = sdl.Rect { bounds = sdl.Rect {
c.int(bounds.x * GLOB.dpi_scaling), c.int(bounds.x * GLOB.dpi_scaling),
c.int(bounds.y * GLOB.dpi_scaling), c.int(bounds.y * GLOB.dpi_scaling),
c.int(bounds.w * GLOB.dpi_scaling), c.int(bounds.width * GLOB.dpi_scaling),
c.int(bounds.h * GLOB.dpi_scaling), c.int(bounds.height * GLOB.dpi_scaling),
}, },
} }
append(&GLOB.scissors, new) append(&GLOB.scissors, new)
@@ -512,8 +573,8 @@ prepare_clay_batch :: proc(
curr_scissor.bounds = sdl.Rect { curr_scissor.bounds = sdl.Rect {
c.int(bounds.x * GLOB.dpi_scaling), c.int(bounds.x * GLOB.dpi_scaling),
c.int(bounds.y * GLOB.dpi_scaling), c.int(bounds.y * GLOB.dpi_scaling),
c.int(bounds.w * GLOB.dpi_scaling), c.int(bounds.width * GLOB.dpi_scaling),
c.int(bounds.h * GLOB.dpi_scaling), c.int(bounds.height * GLOB.dpi_scaling),
} }
} }
case clay.RenderCommandType.ScissorEnd: case clay.RenderCommandType.ScissorEnd:
@@ -532,25 +593,23 @@ prepare_clay_batch :: proc(
render_data := render_command.renderData.border render_data := render_command.renderData.border
cr := render_data.cornerRadius cr := render_data.cornerRadius
color := color_from_clay(render_data.color) color := color_from_clay(render_data.color)
thick := f32(render_data.width.top) thickness := f32(render_data.width.top)
radii := [4]f32{cr.topLeft, cr.topRight, cr.bottomRight, cr.bottomLeft} radii := [4]f32{cr.topLeft, cr.topRight, cr.bottomRight, cr.bottomLeft}
if radii == {0, 0, 0, 0} { if radii == {0, 0, 0, 0} {
rectangle_lines(layer, bounds, color, thick) rectangle_lines(layer, bounds, color, thickness)
} else { } else {
rectangle_corners_lines(layer, bounds, radii, color, thick) rectangle_corners_lines(layer, bounds, radii, color, thickness)
} }
case clay.RenderCommandType.Custom: case clay.RenderCommandType.Custom: if custom_draw != nil {
custom_draw(layer, bounds, render_command.renderData.custom)
}
} }
} }
} }
// Render primitives. clear_color is the background fill before any layers are drawn. // Render primitives. clear_color is the background fill before any layers are drawn.
end :: proc( end :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, clear_color: Color = BLACK) {
device: ^sdl.GPUDevice,
window: ^sdl.Window,
clear_color: Color = BLACK,
) {
cmd_buffer := sdl.AcquireGPUCommandBuffer(device) cmd_buffer := sdl.AcquireGPUCommandBuffer(device)
if cmd_buffer == nil { if cmd_buffer == nil {
log.panicf("Failed to acquire GPU command buffer: %s", sdl.GetError()) log.panicf("Failed to acquire GPU command buffer: %s", sdl.GetError())
@@ -561,13 +620,9 @@ end :: proc(
upload(device, copy_pass) upload(device, copy_pass)
sdl.EndGPUCopyPass(copy_pass) sdl.EndGPUCopyPass(copy_pass)
// Resize dynamic arrays
// TODO: This should only be called occasionally, not every frame.
resize_global()
swapchain_texture: ^sdl.GPUTexture swapchain_texture: ^sdl.GPUTexture
w, h: u32 width, height: u32
if !sdl.WaitAndAcquireGPUSwapchainTexture(cmd_buffer, window, &swapchain_texture, &w, &h) { if !sdl.WaitAndAcquireGPUSwapchainTexture(cmd_buffer, window, &swapchain_texture, &width, &height) {
log.panicf("Failed to acquire swapchain texture: %s", sdl.GetError()) log.panicf("Failed to acquire swapchain texture: %s", sdl.GetError())
} }
@@ -583,16 +638,16 @@ end :: proc(
render_texture := swapchain_texture render_texture := swapchain_texture
if use_msaa { if use_msaa {
ensure_msaa_texture(device, sdl.GetGPUSwapchainTextureFormat(device, window), w, h) ensure_msaa_texture(device, sdl.GetGPUSwapchainTextureFormat(device, window), width, height)
render_texture = GLOB.msaa_texture render_texture = GLOB.msaa_texture
} }
cc := color_to_f32(clear_color) clear_color_f32 := color_to_f32(clear_color)
// Draw layers. One render pass per layer; sub-batches draw in submission order within each scissor. // Draw layers. One render pass per layer; sub-batches draw in submission order within each scissor.
for &layer, index in GLOB.layers { for &layer, index in GLOB.layers {
log.debug("Drawing layer", index) log.debug("Drawing layer", index)
draw_layer(device, window, cmd_buffer, render_texture, w, h, cc, &layer) draw_layer(device, window, cmd_buffer, render_texture, width, height, clear_color_f32, &layer)
} }
// Resolve MSAA render texture to the swapchain. // Resolve MSAA render texture to the swapchain.
@@ -624,15 +679,15 @@ end :: proc(
max_sample_count :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> sdl.GPUSampleCount { max_sample_count :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> sdl.GPUSampleCount {
format := sdl.GetGPUSwapchainTextureFormat(device, window) format := sdl.GetGPUSwapchainTextureFormat(device, window)
counts := [?]sdl.GPUSampleCount{._8, ._4, ._2} counts := [?]sdl.GPUSampleCount{._8, ._4, ._2}
for sc in counts { for count in counts {
if sdl.GPUTextureSupportsSampleCount(device, format, sc) do return sc if sdl.GPUTextureSupportsSampleCount(device, format, count) do return count
} }
return ._1 return ._1
} }
@(private = "file") @(private = "file")
ensure_msaa_texture :: proc(device: ^sdl.GPUDevice, format: sdl.GPUTextureFormat, w, h: u32) { ensure_msaa_texture :: proc(device: ^sdl.GPUDevice, format: sdl.GPUTextureFormat, width, height: u32) {
if GLOB.msaa_texture != nil && GLOB.msaa_w == w && GLOB.msaa_h == h { if GLOB.msaa_texture != nil && GLOB.msaa_width == width && GLOB.msaa_height == height {
return return
} }
if GLOB.msaa_texture != nil { if GLOB.msaa_texture != nil {
@@ -644,18 +699,18 @@ ensure_msaa_texture :: proc(device: ^sdl.GPUDevice, format: sdl.GPUTextureFormat
type = .D2, type = .D2,
format = format, format = format,
usage = {.COLOR_TARGET}, usage = {.COLOR_TARGET},
width = w, width = width,
height = h, height = height,
layer_count_or_depth = 1, layer_count_or_depth = 1,
num_levels = 1, num_levels = 1,
sample_count = GLOB.sample_count, sample_count = GLOB.sample_count,
}, },
) )
if GLOB.msaa_texture == nil { if GLOB.msaa_texture == nil {
log.panicf("Failed to create MSAA texture (%dx%d): %s", w, h, sdl.GetError()) log.panicf("Failed to create MSAA texture (%dx%d): %s", width, height, sdl.GetError())
} }
GLOB.msaa_w = w GLOB.msaa_width = width
GLOB.msaa_h = h GLOB.msaa_height = height
} }
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
@@ -683,9 +738,21 @@ Vertex_Uniforms :: struct {
} }
// Push projection, dpi scale, and rendering mode as a single uniform block (slot 0). // Push projection, dpi scale, and rendering mode as a single uniform block (slot 0).
push_globals :: proc(cmd_buffer: ^sdl.GPUCommandBuffer, w: f32, h: f32, mode: Draw_Mode = .Tessellated) { push_globals :: proc(
cmd_buffer: ^sdl.GPUCommandBuffer,
width: f32,
height: f32,
mode: Draw_Mode = .Tessellated,
) {
globals := Vertex_Uniforms { globals := Vertex_Uniforms {
projection = ortho_rh(left = 0.0, top = 0.0, right = f32(w), bottom = f32(h), near = -1.0, far = 1.0), projection = ortho_rh(
left = 0.0,
top = 0.0,
right = f32(width),
bottom = f32(height),
near = -1.0,
far = 1.0,
),
scale = GLOB.dpi_scaling, scale = GLOB.dpi_scaling,
mode = mode, mode = mode,
} }
@@ -757,3 +824,115 @@ destroy_buffer :: proc(device: ^sdl.GPUDevice, buffer: ^Buffer) {
sdl.ReleaseGPUBuffer(device, buffer.gpu) sdl.ReleaseGPUBuffer(device, buffer.gpu)
sdl.ReleaseGPUTransferBuffer(device, buffer.transfer) sdl.ReleaseGPUTransferBuffer(device, buffer.transfer)
} }
// ---------------------------------------------------------------------------------------------------------------------
// ----- Transform ------------------------
// ---------------------------------------------------------------------------------------------------------------------
// 2x3 affine transform for 2D pivot-rotation.
// Used internally by rotation-aware drawing procs.
Transform_2D :: struct {
m00, m01: f32, // row 0: rotation/scale
m10, m11: f32, // row 1: rotation/scale
tx, ty: f32, // translation
}
// Build a pivot-rotation transform.
//
// Semantics (raylib-style):
// The point whose local coordinates equal `origin` lands at `pos` in world space.
// The rest of the shape rotates around that pivot.
//
// Formula: p_world = pos + R(θ) · (p_local - origin)
//
// Parameters:
// pos world-space position where the pivot lands.
// origin pivot point in local space (measured from the shape's natural reference point).
// rotation_deg rotation in degrees, counter-clockwise.
//
build_pivot_rotation :: proc(position: [2]f32, origin: [2]f32, rotation_deg: f32) -> Transform_2D {
radians := math.to_radians(rotation_deg)
cos_angle := math.cos(radians)
sin_angle := math.sin(radians)
return Transform_2D {
m00 = cos_angle,
m01 = -sin_angle,
m10 = sin_angle,
m11 = cos_angle,
tx = position.x - (cos_angle * origin.x - sin_angle * origin.y),
ty = position.y - (sin_angle * origin.x + cos_angle * origin.y),
}
}
// Apply the transform to a local-space point, producing a world-space point.
apply_transform :: #force_inline proc(transform: Transform_2D, point: [2]f32) -> [2]f32 {
return {
transform.m00 * point.x + transform.m01 * point.y + transform.tx,
transform.m10 * point.x + transform.m11 * point.y + transform.ty,
}
}
// Fast-path check callers use BEFORE building a transform.
// Returns true if either the origin is non-zero or rotation is non-zero,
// meaning a transform actually needs to be computed.
needs_transform :: #force_inline proc(origin: [2]f32, rotation: f32) -> bool {
return origin != {0, 0} || rotation != 0
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Procedure Groups ------------------------
// ---------------------------------------------------------------------------------------------------------------------
center_of :: proc {
center_of_rectangle,
center_of_triangle,
center_of_text,
}
top_left_of :: proc {
top_left_of_rectangle,
top_left_of_triangle,
top_left_of_text,
}
top_of :: proc {
top_of_rectangle,
top_of_triangle,
top_of_text,
}
top_right_of :: proc {
top_right_of_rectangle,
top_right_of_triangle,
top_right_of_text,
}
left_of :: proc {
left_of_rectangle,
left_of_triangle,
left_of_text,
}
right_of :: proc {
right_of_rectangle,
right_of_triangle,
right_of_text,
}
bottom_left_of :: proc {
bottom_left_of_rectangle,
bottom_left_of_triangle,
bottom_left_of_text,
}
bottom_of :: proc {
bottom_of_rectangle,
bottom_of_triangle,
bottom_of_text,
}
bottom_right_of :: proc {
bottom_right_of_rectangle,
bottom_right_of_triangle,
bottom_right_of_text,
}
+244 -40
View File
@@ -2,10 +2,9 @@ package examples
import "../../draw" import "../../draw"
import "../../vendor/clay" import "../../vendor/clay"
import "core:c" import "core:math"
import "core:os" import "core:os"
import sdl "vendor:sdl3" import sdl "vendor:sdl3"
import sdl_ttf "vendor:sdl3/ttf"
JETBRAINS_MONO_REGULAR_RAW :: #load("fonts/JetBrainsMono-Regular.ttf") JETBRAINS_MONO_REGULAR_RAW :: #load("fonts/JetBrainsMono-Regular.ttf")
JETBRAINS_MONO_REGULAR: draw.Font_Id = max(draw.Font_Id) // Max so we crash if registration is forgotten JETBRAINS_MONO_REGULAR: draw.Font_Id = max(draw.Font_Id) // Max so we crash if registration is forgotten
@@ -13,25 +12,28 @@ JETBRAINS_MONO_REGULAR: draw.Font_Id = max(draw.Font_Id) // Max so we crash if r
hellope_shapes :: proc() { hellope_shapes :: proc() {
if !sdl.Init({.VIDEO}) do os.exit(1) if !sdl.Init({.VIDEO}) do os.exit(1)
window := sdl.CreateWindow("Hellope!", 500, 500, {.HIGH_PIXEL_DENSITY}) window := sdl.CreateWindow("Hellope!", 500, 500, {.HIGH_PIXEL_DENSITY})
gpu := sdl.CreateGPUDevice({.MSL}, true, nil) gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1) if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
if !draw.init(gpu, window) do os.exit(1) if !draw.init(gpu, window) do os.exit(1)
spin_angle: f32 = 0
for { for {
defer free_all(context.temp_allocator) defer free_all(context.temp_allocator)
ev: sdl.Event ev: sdl.Event
for sdl.PollEvent(&ev) { for sdl.PollEvent(&ev) {
if ev.type == .QUIT do return if ev.type == .QUIT do return
} }
base_layer := draw.begin({w = 500, h = 500}) spin_angle += 1
base_layer := draw.begin({width = 500, height = 500})
// Background // Background
draw.rectangle(base_layer, {0, 0, 500, 500}, {40, 40, 40, 255}) draw.rectangle(base_layer, {0, 0, 500, 500}, {40, 40, 40, 255})
// Shapes demo // ----- Shapes without rotation (existing demo) -----
draw.rectangle(base_layer, {20, 20, 200, 120}, {80, 120, 200, 255}) draw.rectangle(base_layer, {20, 20, 200, 120}, {80, 120, 200, 255})
draw.rectangle_lines(base_layer, {20, 20, 200, 120}, draw.WHITE, thick = 2) draw.rectangle_lines(base_layer, {20, 20, 200, 120}, draw.WHITE, thickness = 2)
draw.rectangle_rounded(base_layer, {240, 20, 240, 120}, 0.3, {200, 80, 80, 255}) draw.rectangle(base_layer, {240, 20, 240, 120}, {200, 80, 80, 255}, roundness = 0.3)
draw.rectangle_gradient( draw.rectangle_gradient(
base_layer, base_layer,
{20, 160, 460, 60}, {20, 160, 460, 60},
@@ -41,33 +43,86 @@ hellope_shapes :: proc() {
{255, 255, 0, 255}, {255, 255, 0, 255},
) )
draw.circle(base_layer, {120, 320}, 60, {100, 200, 100, 255}) // ----- Rotation demos -----
draw.circle_lines(base_layer, {120, 320}, 60, draw.WHITE, thick = 2)
draw.circle_gradient(base_layer, {300, 320}, 60, {255, 200, 50, 255}, {200, 50, 50, 255})
draw.ring(base_layer, {430, 320}, 30, 55, 0, 270, {100, 100, 220, 255})
draw.triangle(base_layer, {60, 420}, {180, 480}, {20, 480}, {220, 180, 60, 255}) // Rectangle rotating around its center
draw.line(base_layer, {220, 420}, {460, 480}, {255, 255, 100, 255}, thick = 3) rect := draw.Rectangle{100, 320, 80, 50}
draw.poly(base_layer, {350, 450}, 6, 40, {180, 100, 220, 255}, rotation = 30) draw.rectangle(
draw.poly_lines(base_layer, {350, 450}, 6, 40, draw.WHITE, rotation = 30, thick = 2) base_layer,
rect,
{100, 200, 100, 255},
origin = draw.center_of(rect),
rotation = spin_angle,
)
draw.rectangle_lines(
base_layer,
rect,
draw.WHITE,
thickness = 2,
origin = draw.center_of(rect),
rotation = spin_angle,
)
// Rounded rectangle rotating around its center
rrect := draw.Rectangle{230, 300, 100, 80}
draw.rectangle(
base_layer,
rrect,
{200, 100, 200, 255},
roundness = 0.4,
origin = draw.center_of(rrect),
rotation = spin_angle,
)
// Ellipse rotating around its center (tilted ellipse)
draw.ellipse(base_layer, {410, 340}, 50, 30, {255, 200, 50, 255}, rotation = spin_angle)
// Circle orbiting a point (moon orbiting planet)
planet_pos := [2]f32{100, 450}
moon_pos := planet_pos + {0, -40}
draw.circle(base_layer, planet_pos, 8, {200, 200, 200, 255}) // planet (stationary)
draw.circle(base_layer, moon_pos, 5, {100, 150, 255, 255}, origin = {0, 40}, rotation = spin_angle) // moon orbiting
// Ring arc rotating in place
draw.ring(base_layer, {250, 450}, 15, 30, 0, 270, {100, 100, 220, 255}, rotation = spin_angle)
// Triangle rotating around its center
tv1 := [2]f32{350, 420}
tv2 := [2]f32{420, 480}
tv3 := [2]f32{340, 480}
draw.triangle(
base_layer,
tv1,
tv2,
tv3,
{220, 180, 60, 255},
origin = draw.center_of(tv1, tv2, tv3),
rotation = spin_angle,
)
// Polygon rotating around its center (already had rotation; now with origin for orbit)
draw.polygon(base_layer, {460, 450}, 6, 30, {180, 100, 220, 255}, rotation = spin_angle)
draw.polygon_lines(base_layer, {460, 450}, 6, 30, draw.WHITE, rotation = spin_angle, thickness = 2)
draw.end(gpu, window) draw.end(gpu, window)
} }
} }
hellope_text :: proc() { hellope_text :: proc() {
HELLOPE_ID :: 1
ROTATING_SENTENCE_ID :: 2
MEASURED_ID :: 3
CORNER_SPIN_ID :: 4
if !sdl.Init({.VIDEO}) do os.exit(1) if !sdl.Init({.VIDEO}) do os.exit(1)
window := sdl.CreateWindow("Hellope!", 500, 500, {.HIGH_PIXEL_DENSITY}) window := sdl.CreateWindow("Hellope!", 600, 600, {.HIGH_PIXEL_DENSITY})
gpu := sdl.CreateGPUDevice({.MSL}, true, nil) gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1) if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
if !draw.init(gpu, window) do os.exit(1) if !draw.init(gpu, window) do os.exit(1)
JETBRAINS_MONO_REGULAR = draw.register_font(JETBRAINS_MONO_REGULAR_RAW) JETBRAINS_MONO_REGULAR = draw.register_font(JETBRAINS_MONO_REGULAR_RAW)
FONT_SIZE :: u16(24) FONT_SIZE :: u16(24)
TEXT_ID :: u32(1) spin_angle: f32 = 0
font := draw.get_font(JETBRAINS_MONO_REGULAR, FONT_SIZE)
dpi := sdl.GetWindowDisplayScale(window)
for { for {
defer free_all(context.temp_allocator) defer free_all(context.temp_allocator)
@@ -75,28 +130,74 @@ hellope_text :: proc() {
for sdl.PollEvent(&ev) { for sdl.PollEvent(&ev) {
if ev.type == .QUIT do return if ev.type == .QUIT do return
} }
base_layer := draw.begin({w = 500, h = 500}) spin_angle += 0.5
base_layer := draw.begin({width = 600, height = 600})
// Grey background // Grey background
draw.rectangle(base_layer, {0, 0, 500, 500}, {127, 127, 127, 255}) draw.rectangle(base_layer, {0, 0, 600, 600}, {127, 127, 127, 255})
// Measure and center text // ----- Text API demos -----
tw, th: c.int
sdl_ttf.GetStringSize(font, "Hellope!", 0, &tw, &th)
text_w := f32(tw) / dpi
text_h := f32(th) / dpi
pos_x := (500.0 - text_w) / 2.0
pos_y := (500.0 - text_h) / 2.0
txt := draw.text( // Cached text with id — TTF_Text reused across frames (good for text-heavy apps)
TEXT_ID, draw.text(
base_layer,
"Hellope!", "Hellope!",
{pos_x, pos_y}, {300, 80},
JETBRAINS_MONO_REGULAR,
FONT_SIZE,
color = draw.WHITE, color = draw.WHITE,
font_id = JETBRAINS_MONO_REGULAR, origin = draw.center_of("Hellope!", JETBRAINS_MONO_REGULAR, FONT_SIZE),
font_size = FONT_SIZE, id = HELLOPE_ID,
)
// Rotating sentence — verifies multi-word text rotation around center
draw.text(
base_layer,
"Hellope World!",
{300, 250},
JETBRAINS_MONO_REGULAR,
FONT_SIZE,
color = {255, 200, 50, 255},
origin = draw.center_of("Hellope World!", JETBRAINS_MONO_REGULAR, FONT_SIZE),
rotation = spin_angle,
id = ROTATING_SENTENCE_ID,
)
// Uncached text (no id) — created and destroyed each frame, simplest usage
draw.text(
base_layer,
"Top-left anchored",
{20, 450},
JETBRAINS_MONO_REGULAR,
FONT_SIZE,
color = draw.WHITE,
)
// Measure text for manual layout
size := draw.measure_text("Measured!", JETBRAINS_MONO_REGULAR, FONT_SIZE)
draw.rectangle(base_layer, {300 - size.x / 2, 380, size.x, size.y}, {60, 60, 60, 200})
draw.text(
base_layer,
"Measured!",
{300, 380},
JETBRAINS_MONO_REGULAR,
FONT_SIZE,
color = draw.WHITE,
origin = draw.top_of("Measured!", JETBRAINS_MONO_REGULAR, FONT_SIZE),
id = MEASURED_ID,
)
// Rotating text anchored at top-left (no origin offset) — spins around top-left corner
draw.text(
base_layer,
"Corner spin",
{150, 530},
JETBRAINS_MONO_REGULAR,
FONT_SIZE,
color = {100, 200, 255, 255},
rotation = spin_angle,
id = CORNER_SPIN_ID,
) )
draw.prepare_text(base_layer, txt)
draw.end(gpu, window) draw.end(gpu, window)
} }
@@ -105,14 +206,14 @@ hellope_text :: proc() {
hellope_clay :: proc() { hellope_clay :: proc() {
if !sdl.Init({.VIDEO}) do os.exit(1) if !sdl.Init({.VIDEO}) do os.exit(1)
window := sdl.CreateWindow("Hellope!", 500, 500, {.HIGH_PIXEL_DENSITY}) window := sdl.CreateWindow("Hellope!", 500, 500, {.HIGH_PIXEL_DENSITY})
gpu := sdl.CreateGPUDevice({.MSL}, true, nil) gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1) if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
if !draw.init(gpu, window) do os.exit(1) if !draw.init(gpu, window) do os.exit(1)
JETBRAINS_MONO_REGULAR = draw.register_font(JETBRAINS_MONO_REGULAR_RAW) JETBRAINS_MONO_REGULAR = draw.register_font(JETBRAINS_MONO_REGULAR_RAW)
text_config := clay.TextElementConfig { text_config := clay.TextElementConfig {
fontId = JETBRAINS_MONO_REGULAR, fontId = JETBRAINS_MONO_REGULAR,
fontSize = 24, fontSize = 36,
textColor = {255, 255, 255, 255}, textColor = {255, 255, 255, 255},
} }
@@ -122,8 +223,8 @@ hellope_clay :: proc() {
for sdl.PollEvent(&ev) { for sdl.PollEvent(&ev) {
if ev.type == .QUIT do return if ev.type == .QUIT do return
} }
base_layer := draw.begin({w = 500, h = 500}) base_layer := draw.begin({width = 500, height = 500})
clay.SetLayoutDimensions({width = base_layer.bounds.w, height = base_layer.bounds.h}) clay.SetLayoutDimensions({width = base_layer.bounds.width, height = base_layer.bounds.height})
clay.BeginLayout() clay.BeginLayout()
if clay.UI()( if clay.UI()(
{ {
@@ -145,3 +246,106 @@ hellope_clay :: proc() {
draw.end(gpu, window) draw.end(gpu, window)
} }
} }
hellope_custom :: proc() {
if !sdl.Init({.VIDEO}) do os.exit(1)
window := sdl.CreateWindow("Hellope Custom!", 600, 400, {.HIGH_PIXEL_DENSITY})
gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
if !draw.init(gpu, window) do os.exit(1)
JETBRAINS_MONO_REGULAR = draw.register_font(JETBRAINS_MONO_REGULAR_RAW)
text_config := clay.TextElementConfig {
fontId = JETBRAINS_MONO_REGULAR,
fontSize = 24,
textColor = {255, 255, 255, 255},
}
gauge := Gauge {
value = 0.73,
color = {50, 200, 100, 255},
}
gauge2 := Gauge {
value = 0.45,
color = {200, 100, 50, 255},
}
spin_angle: f32 = 0
for {
defer free_all(context.temp_allocator)
ev: sdl.Event
for sdl.PollEvent(&ev) {
if ev.type == .QUIT do return
}
spin_angle += 1
gauge.value = (math.sin(spin_angle * 0.02) + 1) * 0.5
gauge2.value = (math.cos(spin_angle * 0.03) + 1) * 0.5
base_layer := draw.begin({width = 600, height = 400})
clay.SetLayoutDimensions({width = base_layer.bounds.width, height = base_layer.bounds.height})
clay.BeginLayout()
if clay.UI()(
{
id = clay.ID("outer"),
layout = {
sizing = {clay.SizingGrow({}), clay.SizingGrow({})},
childAlignment = {x = .Center, y = .Center},
layoutDirection = .TopToBottom,
childGap = 20,
},
backgroundColor = {50, 50, 50, 255},
},
) {
if clay.UI()({id = clay.ID("title"), layout = {sizing = {clay.SizingFit({}), clay.SizingFit({})}}}) {
clay.Text("Custom Draw Demo", &text_config)
}
if clay.UI()(
{
id = clay.ID("gauge"),
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
custom = {customData = &gauge},
backgroundColor = {80, 80, 80, 255},
},
) {}
if clay.UI()(
{
id = clay.ID("gauge2"),
layout = {sizing = {clay.SizingFixed(300), clay.SizingFixed(30)}},
custom = {customData = &gauge2},
backgroundColor = {80, 80, 80, 255},
},
) {}
}
clay_batch := draw.ClayBatch {
bounds = base_layer.bounds,
cmds = clay.EndLayout(),
}
draw.prepare_clay_batch(base_layer, &clay_batch, {0, 0}, custom_draw = draw_custom)
draw.end(gpu, window)
}
Gauge :: struct {
value: f32,
color: draw.Color,
}
draw_custom :: proc(layer: ^draw.Layer, bounds: draw.Rectangle, render_data: clay.CustomRenderData) {
gauge := cast(^Gauge)render_data.customData
// Background from clay's backgroundColor
draw.rectangle(layer, bounds, draw.color_from_clay(render_data.backgroundColor), roundness = 0.25)
// Fill bar
fill := bounds
fill.width *= gauge.value
draw.rectangle(layer, fill, gauge.color, roundness = 0.25)
// Border
draw.rectangle_lines(layer, bounds, draw.WHITE, thickness = 2, roundness = 0.25)
}
}
+3 -2
View File
@@ -57,17 +57,18 @@ main :: proc() {
args := os.args args := os.args
if len(args) < 2 { if len(args) < 2 {
fmt.eprintln("Usage: examples <example_name>") fmt.eprintln("Usage: examples <example_name>")
fmt.eprintln("Available examples: hellope-shapes, hellope-text, hellope-clay") fmt.eprintln("Available examples: hellope-shapes, hellope-text, hellope-clay, hellope-custom")
os.exit(1) os.exit(1)
} }
switch args[1] { switch args[1] {
case "hellope-clay": hellope_clay() case "hellope-clay": hellope_clay()
case "hellope-custom": hellope_custom()
case "hellope-shapes": hellope_shapes() case "hellope-shapes": hellope_shapes()
case "hellope-text": hellope_text() case "hellope-text": hellope_text()
case: case:
fmt.eprintf("Unknown example: %v\n", args[1]) fmt.eprintf("Unknown example: %v\n", args[1])
fmt.eprintln("Available examples: hellope-shapes, hellope-text, hellope-clay") fmt.eprintln("Available examples: hellope-shapes, hellope-text, hellope-clay, hellope-custom")
os.exit(1) os.exit(1)
} }
} }
+128 -154
View File
@@ -100,11 +100,12 @@ Shape_Params :: struct #raw_union {
// GPU layout: 64 bytes, std430-compatible. The shader declares this as a storage buffer struct. // GPU layout: 64 bytes, std430-compatible. The shader declares this as a storage buffer struct.
Primitive :: struct { Primitive :: struct {
bounds: [4]f32, // 0: min_x, min_y, max_x, max_y (world-space, pre-DPI) bounds: [4]f32, // 0: min_x, min_y, max_x, max_y (world-space, pre-DPI)
color: Color, // 16: u8x4, unpacked in shader via unpackUnorm4x8 color: Color, // 16: u8x4, unpacked in shader via unpackUnorm4x8
kind_flags: u32, // 20: (kind as u32) | (flags as u32 << 8) kind_flags: u32, // 20: (kind as u32) | (flags as u32 << 8)
_pad: [2]f32, // 24: alignment to vec4 boundary rotation: f32, // 24: shader self-rotation in radians (used by RRect, Ellipse)
params: Shape_Params, // 32: two vec4s of shape params _pad: f32, // 28: alignment to vec4 boundary
params: Shape_Params, // 32: two vec4s of shape params
} }
#assert(size_of(Primitive) == 64) #assert(size_of(Primitive) == 64)
@@ -143,32 +144,34 @@ create_pipeline_2d_base :: proc(
if pipeline.sdl_pipeline != nil do sdl.ReleaseGPUGraphicsPipeline(device, pipeline.sdl_pipeline) if pipeline.sdl_pipeline != nil do sdl.ReleaseGPUGraphicsPipeline(device, pipeline.sdl_pipeline)
} }
when ODIN_OS == .Darwin { active_shader_formats := sdl.GetGPUShaderFormats(device)
base_2d_vert_raw := #load("shaders/generated/base_2d.vert.metal") if PLATFORM_SHADER_FORMAT_FLAG not_in active_shader_formats {
base_2d_frag_raw := #load("shaders/generated/base_2d.frag.metal") log.errorf(
} else { "draw: no embedded shader matches active GPU formats; this build supports %v but device reports %v",
base_2d_vert_raw := #load("shaders/generated/base_2d.vert.spv") PLATFORM_SHADER_FORMAT,
base_2d_frag_raw := #load("shaders/generated/base_2d.frag.spv") active_shader_formats,
)
return pipeline, false
} }
log.debug("Loaded", len(base_2d_vert_raw), "vert bytes") log.debug("Loaded", len(BASE_VERT_2D_RAW), "vert bytes")
log.debug("Loaded", len(base_2d_frag_raw), "frag bytes") log.debug("Loaded", len(BASE_FRAG_2D_RAW), "frag bytes")
vert_info := sdl.GPUShaderCreateInfo { vert_info := sdl.GPUShaderCreateInfo {
code_size = len(base_2d_vert_raw), code_size = len(BASE_VERT_2D_RAW),
code = raw_data(base_2d_vert_raw), code = raw_data(BASE_VERT_2D_RAW),
entrypoint = ENTRY_POINT, entrypoint = SHADER_ENTRY,
format = SHADER_TYPE, format = {PLATFORM_SHADER_FORMAT_FLAG},
stage = .VERTEX, stage = .VERTEX,
num_uniform_buffers = 1, num_uniform_buffers = 1,
num_storage_buffers = 1, num_storage_buffers = 1,
} }
frag_info := sdl.GPUShaderCreateInfo { frag_info := sdl.GPUShaderCreateInfo {
code_size = len(base_2d_frag_raw), code_size = len(BASE_FRAG_2D_RAW),
code = raw_data(base_2d_frag_raw), code = raw_data(BASE_FRAG_2D_RAW),
entrypoint = ENTRY_POINT, entrypoint = SHADER_ENTRY,
format = SHADER_TYPE, format = {PLATFORM_SHADER_FORMAT_FLAG},
stage = .FRAGMENT, stage = .FRAGMENT,
num_samplers = 1, num_samplers = 1,
} }
@@ -239,31 +242,31 @@ create_pipeline_2d_base :: proc(
} }
// Create vertex buffer // Create vertex buffer
vb_ok: bool vert_buf_ok: bool
pipeline.vertex_buffer, vb_ok = create_buffer( pipeline.vertex_buffer, vert_buf_ok = create_buffer(
device, device,
size_of(Vertex) * BUFFER_INIT_SIZE, size_of(Vertex) * BUFFER_INIT_SIZE,
sdl.GPUBufferUsageFlags{.VERTEX}, sdl.GPUBufferUsageFlags{.VERTEX},
) )
if !vb_ok do return pipeline, false if !vert_buf_ok do return pipeline, false
// Create index buffer (used by text) // Create index buffer (used by text)
ib_ok: bool idx_buf_ok: bool
pipeline.index_buffer, ib_ok = create_buffer( pipeline.index_buffer, idx_buf_ok = create_buffer(
device, device,
size_of(c.int) * BUFFER_INIT_SIZE, size_of(c.int) * BUFFER_INIT_SIZE,
sdl.GPUBufferUsageFlags{.INDEX}, sdl.GPUBufferUsageFlags{.INDEX},
) )
if !ib_ok do return pipeline, false if !idx_buf_ok do return pipeline, false
// Create primitive storage buffer (used by SDF instanced drawing) // Create primitive storage buffer (used by SDF instanced drawing)
pb_ok: bool prim_buf_ok: bool
pipeline.primitive_buffer, pb_ok = create_buffer( pipeline.primitive_buffer, prim_buf_ok = create_buffer(
device, device,
size_of(Primitive) * BUFFER_INIT_SIZE, size_of(Primitive) * BUFFER_INIT_SIZE,
sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ}, sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ},
) )
if !pb_ok do return pipeline, false if !prim_buf_ok do return pipeline, false
// Create static 6-vertex unit quad buffer (two triangles, TRIANGLELIST) // Create static 6-vertex unit quad buffer (two triangles, TRIANGLELIST)
pipeline.unit_quad_buffer = sdl.CreateGPUBuffer( pipeline.unit_quad_buffer = sdl.CreateGPUBuffer(
@@ -279,14 +282,14 @@ create_pipeline_2d_base :: proc(
pipeline.white_texture = sdl.CreateGPUTexture( pipeline.white_texture = sdl.CreateGPUTexture(
device, device,
sdl.GPUTextureCreateInfo { sdl.GPUTextureCreateInfo {
type = .D2, type = .D2,
format = .R8G8B8A8_UNORM, format = .R8G8B8A8_UNORM,
usage = {.SAMPLER}, usage = {.SAMPLER},
width = 1, width = 1,
height = 1, height = 1,
layer_count_or_depth = 1, layer_count_or_depth = 1,
num_levels = 1, num_levels = 1,
sample_count = ._1, sample_count = ._1,
}, },
) )
if pipeline.white_texture == nil { if pipeline.white_texture == nil {
@@ -296,73 +299,73 @@ create_pipeline_2d_base :: proc(
// Upload white pixel and unit quad data in a single command buffer // Upload white pixel and unit quad data in a single command buffer
white_pixel := [4]u8{255, 255, 255, 255} white_pixel := [4]u8{255, 255, 255, 255}
white_transfer := sdl.CreateGPUTransferBuffer( white_transfer_buf := sdl.CreateGPUTransferBuffer(
device, device,
sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = size_of(white_pixel)}, sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = size_of(white_pixel)},
) )
if white_transfer == nil { if white_transfer_buf == nil {
log.errorf("Failed to create white pixel transfer buffer: %s", sdl.GetError()) log.errorf("Failed to create white pixel transfer buffer: %s", sdl.GetError())
return pipeline, false return pipeline, false
} }
defer sdl.ReleaseGPUTransferBuffer(device, white_transfer) defer sdl.ReleaseGPUTransferBuffer(device, white_transfer_buf)
white_ptr := sdl.MapGPUTransferBuffer(device, white_transfer, false) white_ptr := sdl.MapGPUTransferBuffer(device, white_transfer_buf, false)
if white_ptr == nil { if white_ptr == nil {
log.errorf("Failed to map white pixel transfer buffer: %s", sdl.GetError()) log.errorf("Failed to map white pixel transfer buffer: %s", sdl.GetError())
return pipeline, false return pipeline, false
} }
mem.copy(white_ptr, &white_pixel, size_of(white_pixel)) mem.copy(white_ptr, &white_pixel, size_of(white_pixel))
sdl.UnmapGPUTransferBuffer(device, white_transfer) sdl.UnmapGPUTransferBuffer(device, white_transfer_buf)
quad_verts := [6]Vertex{ quad_verts := [6]Vertex {
{position = {0, 0}}, {position = {1, 0}}, {position = {0, 1}}, {position = {0, 0}},
{position = {0, 1}}, {position = {1, 0}}, {position = {1, 1}}, {position = {1, 0}},
{position = {0, 1}},
{position = {0, 1}},
{position = {1, 0}},
{position = {1, 1}},
} }
quad_transfer := sdl.CreateGPUTransferBuffer( quad_transfer_buf := sdl.CreateGPUTransferBuffer(
device, device,
sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = size_of(quad_verts)}, sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = size_of(quad_verts)},
) )
if quad_transfer == nil { if quad_transfer_buf == nil {
log.errorf("Failed to create unit quad transfer buffer: %s", sdl.GetError()) log.errorf("Failed to create unit quad transfer buffer: %s", sdl.GetError())
return pipeline, false return pipeline, false
} }
defer sdl.ReleaseGPUTransferBuffer(device, quad_transfer) defer sdl.ReleaseGPUTransferBuffer(device, quad_transfer_buf)
quad_ptr := sdl.MapGPUTransferBuffer(device, quad_transfer, false) quad_ptr := sdl.MapGPUTransferBuffer(device, quad_transfer_buf, false)
if quad_ptr == nil { if quad_ptr == nil {
log.errorf("Failed to map unit quad transfer buffer: %s", sdl.GetError()) log.errorf("Failed to map unit quad transfer buffer: %s", sdl.GetError())
return pipeline, false return pipeline, false
} }
mem.copy(quad_ptr, &quad_verts, size_of(quad_verts)) mem.copy(quad_ptr, &quad_verts, size_of(quad_verts))
sdl.UnmapGPUTransferBuffer(device, quad_transfer) sdl.UnmapGPUTransferBuffer(device, quad_transfer_buf)
upload_cmd := sdl.AcquireGPUCommandBuffer(device) upload_cmd_buffer := sdl.AcquireGPUCommandBuffer(device)
if upload_cmd == nil { if upload_cmd_buffer == nil {
log.errorf("Failed to acquire command buffer for init upload: %s", sdl.GetError()) log.errorf("Failed to acquire command buffer for init upload: %s", sdl.GetError())
return pipeline, false return pipeline, false
} }
upload_pass := sdl.BeginGPUCopyPass(upload_cmd) upload_pass := sdl.BeginGPUCopyPass(upload_cmd_buffer)
sdl.UploadToGPUTexture( sdl.UploadToGPUTexture(
upload_pass, upload_pass,
sdl.GPUTextureTransferInfo{transfer_buffer = white_transfer}, sdl.GPUTextureTransferInfo{transfer_buffer = white_transfer_buf},
sdl.GPUTextureRegion{texture = pipeline.white_texture, w = 1, h = 1, d = 1}, sdl.GPUTextureRegion{texture = pipeline.white_texture, w = 1, h = 1, d = 1},
false, false,
) )
sdl.UploadToGPUBuffer( sdl.UploadToGPUBuffer(
upload_pass, upload_pass,
sdl.GPUTransferBufferLocation{transfer_buffer = quad_transfer}, sdl.GPUTransferBufferLocation{transfer_buffer = quad_transfer_buf},
sdl.GPUBufferRegion{ sdl.GPUBufferRegion{buffer = pipeline.unit_quad_buffer, offset = 0, size = size_of(quad_verts)},
buffer = pipeline.unit_quad_buffer,
offset = 0,
size = size_of(quad_verts),
},
false, false,
) )
sdl.EndGPUCopyPass(upload_pass) sdl.EndGPUCopyPass(upload_pass)
if !sdl.SubmitGPUCommandBuffer(upload_cmd) { if !sdl.SubmitGPUCommandBuffer(upload_cmd_buffer) {
log.errorf("Failed to submit init upload command buffer: %s", sdl.GetError()) log.errorf("Failed to submit init upload command buffer: %s", sdl.GetError())
return pipeline, false return pipeline, false
} }
@@ -373,9 +376,9 @@ create_pipeline_2d_base :: proc(
pipeline.sampler = sdl.CreateGPUSampler( pipeline.sampler = sdl.CreateGPUSampler(
device, device,
sdl.GPUSamplerCreateInfo { sdl.GPUSamplerCreateInfo {
min_filter = .LINEAR, min_filter = .LINEAR,
mag_filter = .LINEAR, mag_filter = .LINEAR,
mipmap_mode = .LINEAR, mipmap_mode = .LINEAR,
address_mode_u = .CLAMP_TO_EDGE, address_mode_u = .CLAMP_TO_EDGE,
address_mode_v = .CLAMP_TO_EDGE, address_mode_v = .CLAMP_TO_EDGE,
address_mode_w = .CLAMP_TO_EDGE, address_mode_w = .CLAMP_TO_EDGE,
@@ -409,16 +412,16 @@ upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
sdl.GPUBufferUsageFlags{.VERTEX}, sdl.GPUBufferUsageFlags{.VERTEX},
) )
v_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_base.vertex_buffer.transfer, false) vert_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_base.vertex_buffer.transfer, false)
if v_array == nil { if vert_array == nil {
log.panicf("Failed to map vertex transfer buffer: %s", sdl.GetError()) log.panicf("Failed to map vertex transfer buffer: %s", sdl.GetError())
} }
if shape_vert_size > 0 { if shape_vert_size > 0 {
mem.copy(v_array, raw_data(GLOB.tmp_shape_verts), int(shape_vert_size)) mem.copy(vert_array, raw_data(GLOB.tmp_shape_verts), int(shape_vert_size))
} }
if text_vert_size > 0 { if text_vert_size > 0 {
mem.copy( mem.copy(
rawptr(uintptr(v_array) + uintptr(shape_vert_size)), rawptr(uintptr(vert_array) + uintptr(shape_vert_size)),
raw_data(GLOB.tmp_text_verts), raw_data(GLOB.tmp_text_verts),
int(text_vert_size), int(text_vert_size),
) )
@@ -428,11 +431,7 @@ upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
sdl.UploadToGPUBuffer( sdl.UploadToGPUBuffer(
pass, pass,
sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_base.vertex_buffer.transfer}, sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_base.vertex_buffer.transfer},
sdl.GPUBufferRegion{ sdl.GPUBufferRegion{buffer = GLOB.pipeline_2d_base.vertex_buffer.gpu, offset = 0, size = total_vert_size},
buffer = GLOB.pipeline_2d_base.vertex_buffer.gpu,
offset = 0,
size = total_vert_size,
},
false, false,
) )
} }
@@ -449,21 +448,17 @@ upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
sdl.GPUBufferUsageFlags{.INDEX}, sdl.GPUBufferUsageFlags{.INDEX},
) )
i_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_base.index_buffer.transfer, false) idx_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_base.index_buffer.transfer, false)
if i_array == nil { if idx_array == nil {
log.panicf("Failed to map index transfer buffer: %s", sdl.GetError()) log.panicf("Failed to map index transfer buffer: %s", sdl.GetError())
} }
mem.copy(i_array, raw_data(GLOB.tmp_text_indices), int(index_size)) mem.copy(idx_array, raw_data(GLOB.tmp_text_indices), int(index_size))
sdl.UnmapGPUTransferBuffer(device, GLOB.pipeline_2d_base.index_buffer.transfer) sdl.UnmapGPUTransferBuffer(device, GLOB.pipeline_2d_base.index_buffer.transfer)
sdl.UploadToGPUBuffer( sdl.UploadToGPUBuffer(
pass, pass,
sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_base.index_buffer.transfer}, sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_base.index_buffer.transfer},
sdl.GPUBufferRegion{ sdl.GPUBufferRegion{buffer = GLOB.pipeline_2d_base.index_buffer.gpu, offset = 0, size = index_size},
buffer = GLOB.pipeline_2d_base.index_buffer.gpu,
offset = 0,
size = index_size,
},
false, false,
) )
} }
@@ -480,25 +475,17 @@ upload :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) {
sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ}, sdl.GPUBufferUsageFlags{.GRAPHICS_STORAGE_READ},
) )
p_array := sdl.MapGPUTransferBuffer( prim_array := sdl.MapGPUTransferBuffer(device, GLOB.pipeline_2d_base.primitive_buffer.transfer, false)
device, GLOB.pipeline_2d_base.primitive_buffer.transfer, false, if prim_array == nil {
)
if p_array == nil {
log.panicf("Failed to map primitive transfer buffer: %s", sdl.GetError()) log.panicf("Failed to map primitive transfer buffer: %s", sdl.GetError())
} }
mem.copy(p_array, raw_data(GLOB.tmp_primitives), int(prim_size)) mem.copy(prim_array, raw_data(GLOB.tmp_primitives), int(prim_size))
sdl.UnmapGPUTransferBuffer(device, GLOB.pipeline_2d_base.primitive_buffer.transfer) sdl.UnmapGPUTransferBuffer(device, GLOB.pipeline_2d_base.primitive_buffer.transfer)
sdl.UploadToGPUBuffer( sdl.UploadToGPUBuffer(
pass, pass,
sdl.GPUTransferBufferLocation{ sdl.GPUTransferBufferLocation{transfer_buffer = GLOB.pipeline_2d_base.primitive_buffer.transfer},
transfer_buffer = GLOB.pipeline_2d_base.primitive_buffer.transfer, sdl.GPUBufferRegion{buffer = GLOB.pipeline_2d_base.primitive_buffer.gpu, offset = 0, size = prim_size},
},
sdl.GPUBufferRegion{
buffer = GLOB.pipeline_2d_base.primitive_buffer.gpu,
offset = 0,
size = prim_size,
},
false, false,
) )
} }
@@ -510,8 +497,8 @@ draw_layer :: proc(
window: ^sdl.Window, window: ^sdl.Window,
cmd_buffer: ^sdl.GPUCommandBuffer, cmd_buffer: ^sdl.GPUCommandBuffer,
render_texture: ^sdl.GPUTexture, render_texture: ^sdl.GPUTexture,
swapchain_w: u32, swapchain_width: u32,
swapchain_h: u32, swapchain_height: u32,
clear_color: [4]f32, clear_color: [4]f32,
layer: ^Layer, layer: ^Layer,
) { ) {
@@ -521,10 +508,8 @@ draw_layer :: proc(
cmd_buffer, cmd_buffer,
&sdl.GPUColorTargetInfo { &sdl.GPUColorTargetInfo {
texture = render_texture, texture = render_texture,
clear_color = sdl.FColor { clear_color = sdl.FColor{clear_color[0], clear_color[1], clear_color[2], clear_color[3]},
clear_color[0], clear_color[1], clear_color[2], clear_color[3], load_op = .CLEAR,
},
load_op = .CLEAR,
store_op = .STORE, store_op = .STORE,
}, },
1, 1,
@@ -540,10 +525,8 @@ draw_layer :: proc(
cmd_buffer, cmd_buffer,
&sdl.GPUColorTargetInfo { &sdl.GPUColorTargetInfo {
texture = render_texture, texture = render_texture,
clear_color = sdl.FColor { clear_color = sdl.FColor{clear_color[0], clear_color[1], clear_color[2], clear_color[3]},
clear_color[0], clear_color[1], clear_color[2], clear_color[3], load_op = GLOB.cleared ? .LOAD : .CLEAR,
},
load_op = GLOB.cleared ? .LOAD : .CLEAR,
store_op = .STORE, store_op = .STORE,
}, },
1, 1,
@@ -569,21 +552,19 @@ draw_layer :: proc(
) )
// Shorthand aliases for frequently-used pipeline resources // Shorthand aliases for frequently-used pipeline resources
main_vbuf := GLOB.pipeline_2d_base.vertex_buffer.gpu main_vert_buf := GLOB.pipeline_2d_base.vertex_buffer.gpu
unit_quad := GLOB.pipeline_2d_base.unit_quad_buffer unit_quad := GLOB.pipeline_2d_base.unit_quad_buffer
white := GLOB.pipeline_2d_base.white_texture white_texture := GLOB.pipeline_2d_base.white_texture
sampler := GLOB.pipeline_2d_base.sampler sampler := GLOB.pipeline_2d_base.sampler
w := f32(swapchain_w) width := f32(swapchain_width)
h := f32(swapchain_h) height := f32(swapchain_height)
// Initial GPU state: tessellated mode, main vertex buffer, no atlas bound yet // Initial GPU state: tessellated mode, main vertex buffer, no atlas bound yet
push_globals(cmd_buffer, w, h, .Tessellated) push_globals(cmd_buffer, width, height, .Tessellated)
sdl.BindGPUVertexBuffers( sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1)
render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vbuf, offset = 0}, 1,
)
current_mode: Draw_Mode = .Tessellated current_mode: Draw_Mode = .Tessellated
current_vbuf := main_vbuf current_vert_buf := main_vert_buf
current_atlas: ^sdl.GPUTexture current_atlas: ^sdl.GPUTexture
// Text vertices live after shape vertices in the GPU vertex buffer // Text vertices live after shape vertices in the GPU vertex buffer
@@ -596,76 +577,69 @@ draw_layer :: proc(
switch batch.kind { switch batch.kind {
case .Shapes: case .Shapes:
if current_mode != .Tessellated { if current_mode != .Tessellated {
push_globals(cmd_buffer, w, h, .Tessellated) push_globals(cmd_buffer, width, height, .Tessellated)
current_mode = .Tessellated current_mode = .Tessellated
} }
if current_vbuf != main_vbuf { if current_vert_buf != main_vert_buf {
sdl.BindGPUVertexBuffers( sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1)
render_pass, 0, current_vert_buf = main_vert_buf
&sdl.GPUBufferBinding{buffer = main_vbuf, offset = 0}, 1,
)
current_vbuf = main_vbuf
} }
if current_atlas != white { if current_atlas != white_texture {
sdl.BindGPUFragmentSamplers( sdl.BindGPUFragmentSamplers(
render_pass, 0, render_pass,
&sdl.GPUTextureSamplerBinding{texture = white, sampler = sampler}, 1, 0,
&sdl.GPUTextureSamplerBinding{texture = white_texture, sampler = sampler},
1,
) )
current_atlas = white current_atlas = white_texture
} }
sdl.DrawGPUPrimitives(render_pass, batch.count, 1, batch.offset, 0) sdl.DrawGPUPrimitives(render_pass, batch.count, 1, batch.offset, 0)
case .Text: case .Text:
if current_mode != .Tessellated { if current_mode != .Tessellated {
push_globals(cmd_buffer, w, h, .Tessellated) push_globals(cmd_buffer, width, height, .Tessellated)
current_mode = .Tessellated current_mode = .Tessellated
} }
if current_vbuf != main_vbuf { if current_vert_buf != main_vert_buf {
sdl.BindGPUVertexBuffers( sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = main_vert_buf, offset = 0}, 1)
render_pass, 0, current_vert_buf = main_vert_buf
&sdl.GPUBufferBinding{buffer = main_vbuf, offset = 0}, 1,
)
current_vbuf = main_vbuf
} }
chunk := &GLOB.tmp_text_batches[batch.offset] text_batch := &GLOB.tmp_text_batches[batch.offset]
if current_atlas != chunk.atlas_texture { if current_atlas != text_batch.atlas_texture {
sdl.BindGPUFragmentSamplers( sdl.BindGPUFragmentSamplers(
render_pass, 0, render_pass,
&sdl.GPUTextureSamplerBinding { 0,
texture = chunk.atlas_texture, &sdl.GPUTextureSamplerBinding{texture = text_batch.atlas_texture, sampler = sampler},
sampler = sampler,
},
1, 1,
) )
current_atlas = chunk.atlas_texture current_atlas = text_batch.atlas_texture
} }
sdl.DrawGPUIndexedPrimitives( sdl.DrawGPUIndexedPrimitives(
render_pass, render_pass,
chunk.index_count, text_batch.index_count,
1, 1,
chunk.index_start, text_batch.index_start,
i32(text_vertex_gpu_base + chunk.vertex_start), i32(text_vertex_gpu_base + text_batch.vertex_start),
0, 0,
) )
case .SDF: case .SDF:
if current_mode != .SDF { if current_mode != .SDF {
push_globals(cmd_buffer, w, h, .SDF) push_globals(cmd_buffer, width, height, .SDF)
current_mode = .SDF current_mode = .SDF
} }
if current_vbuf != unit_quad { if current_vert_buf != unit_quad {
sdl.BindGPUVertexBuffers( sdl.BindGPUVertexBuffers(render_pass, 0, &sdl.GPUBufferBinding{buffer = unit_quad, offset = 0}, 1)
render_pass, 0, current_vert_buf = unit_quad
&sdl.GPUBufferBinding{buffer = unit_quad, offset = 0}, 1,
)
current_vbuf = unit_quad
} }
if current_atlas != white { if current_atlas != white_texture {
sdl.BindGPUFragmentSamplers( sdl.BindGPUFragmentSamplers(
render_pass, 0, render_pass,
&sdl.GPUTextureSamplerBinding{texture = white, sampler = sampler}, 1, 0,
&sdl.GPUTextureSamplerBinding{texture = white_texture, sampler = sampler},
1,
) )
current_atlas = white current_atlas = white_texture
} }
sdl.DrawGPUPrimitives(render_pass, 6, batch.count, 0, batch.offset) sdl.DrawGPUPrimitives(render_pass, 6, batch.count, 0, batch.offset)
} }
+69 -54
View File
@@ -24,32 +24,41 @@ struct main0_in
float4 f_params [[user(locn2)]]; float4 f_params [[user(locn2)]];
float4 f_params2 [[user(locn3)]]; float4 f_params2 [[user(locn3)]];
uint f_kind_flags [[user(locn4)]]; uint f_kind_flags [[user(locn4)]];
float f_rotation [[user(locn5), flat]];
}; };
static inline __attribute__((always_inline))
float2 apply_rotation(thread const float2& p, thread const float& angle)
{
float cr = cos(-angle);
float sr = sin(-angle);
return float2x2(float2(cr, sr), float2(-sr, cr)) * p;
}
static inline __attribute__((always_inline)) static inline __attribute__((always_inline))
float sdRoundedBox(thread const float2& p, thread const float2& b, thread float4& r) float sdRoundedBox(thread const float2& p, thread const float2& b, thread float4& r)
{ {
float2 _56; float2 _61;
if (p.x > 0.0) if (p.x > 0.0)
{ {
_56 = r.xy; _61 = r.xy;
} }
else else
{ {
_56 = r.zw; _61 = r.zw;
} }
r.x = _56.x; r.x = _61.x;
r.y = _56.y; r.y = _61.y;
float _73; float _78;
if (p.y > 0.0) if (p.y > 0.0)
{ {
_73 = r.x; _78 = r.x;
} }
else else
{ {
_73 = r.y; _78 = r.y;
} }
r.x = _73; r.x = _78;
float2 q = (abs(p) - b) + float2(r.x); float2 q = (abs(p) - b) + float2(r.x);
return (fast::min(fast::max(q.x, q.y), 0.0) + length(fast::max(q, float2(0.0)))) - r.x; return (fast::min(fast::max(q.x, q.y), 0.0) + length(fast::max(q, float2(0.0)))) - r.x;
} }
@@ -142,16 +151,23 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
float4 r = float4(in.f_params.zw, in.f_params2.xy); float4 r = float4(in.f_params.zw, in.f_params2.xy);
soft = fast::max(in.f_params2.z, 1.0); soft = fast::max(in.f_params2.z, 1.0);
float stroke_px = in.f_params2.w; float stroke_px = in.f_params2.w;
float2 param = in.f_local_or_uv; float2 p_local = in.f_local_or_uv;
float2 param_1 = b; if (in.f_rotation != 0.0)
float4 param_2 = r; {
float _453 = sdRoundedBox(param, param_1, param_2); float2 param = p_local;
d = _453; float param_1 = in.f_rotation;
p_local = apply_rotation(param, param_1);
}
float2 param_2 = p_local;
float2 param_3 = b;
float4 param_4 = r;
float _491 = sdRoundedBox(param_2, param_3, param_4);
d = _491;
if ((flags & 1u) != 0u) if ((flags & 1u) != 0u)
{ {
float param_3 = d; float param_5 = d;
float param_4 = stroke_px; float param_6 = stroke_px;
d = sdf_stroke(param_3, param_4); d = sdf_stroke(param_5, param_6);
} }
} }
else else
@@ -161,14 +177,14 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
float radius = in.f_params.x; float radius = in.f_params.x;
soft = fast::max(in.f_params.y, 1.0); soft = fast::max(in.f_params.y, 1.0);
float stroke_px_1 = in.f_params.z; float stroke_px_1 = in.f_params.z;
float2 param_5 = in.f_local_or_uv; float2 param_7 = in.f_local_or_uv;
float param_6 = radius; float param_8 = radius;
d = sdCircle(param_5, param_6); d = sdCircle(param_7, param_8);
if ((flags & 1u) != 0u) if ((flags & 1u) != 0u)
{ {
float param_7 = d; float param_9 = d;
float param_8 = stroke_px_1; float param_10 = stroke_px_1;
d = sdf_stroke(param_7, param_8); d = sdf_stroke(param_9, param_10);
} }
} }
else else
@@ -178,15 +194,22 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
float2 ab = in.f_params.xy; float2 ab = in.f_params.xy;
soft = fast::max(in.f_params.z, 1.0); soft = fast::max(in.f_params.z, 1.0);
float stroke_px_2 = in.f_params.w; float stroke_px_2 = in.f_params.w;
float2 param_9 = in.f_local_or_uv; float2 p_local_1 = in.f_local_or_uv;
float2 param_10 = ab; if (in.f_rotation != 0.0)
float _511 = sdEllipse(param_9, param_10); {
d = _511; float2 param_11 = p_local_1;
float param_12 = in.f_rotation;
p_local_1 = apply_rotation(param_11, param_12);
}
float2 param_13 = p_local_1;
float2 param_14 = ab;
float _560 = sdEllipse(param_13, param_14);
d = _560;
if ((flags & 1u) != 0u) if ((flags & 1u) != 0u)
{ {
float param_11 = d; float param_15 = d;
float param_12 = stroke_px_2; float param_16 = stroke_px_2;
d = sdf_stroke(param_11, param_12); d = sdf_stroke(param_15, param_16);
} }
} }
else else
@@ -197,10 +220,10 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
float2 b_1 = in.f_params.zw; float2 b_1 = in.f_params.zw;
float width = in.f_params2.x; float width = in.f_params2.x;
soft = fast::max(in.f_params2.y, 1.0); soft = fast::max(in.f_params2.y, 1.0);
float2 param_13 = in.f_local_or_uv; float2 param_17 = in.f_local_or_uv;
float2 param_14 = a; float2 param_18 = a;
float2 param_15 = b_1; float2 param_19 = b_1;
d = sdSegment(param_13, param_14, param_15) - (width * 0.5); d = sdSegment(param_17, param_18, param_19) - (width * 0.5);
} }
else else
{ {
@@ -218,26 +241,18 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
{ {
angle += 6.283185482025146484375; angle += 6.283185482025146484375;
} }
float ang_start = start_rad; float ang_start = mod(start_rad, 6.283185482025146484375);
float ang_end = end_rad; float ang_end = mod(end_rad, 6.283185482025146484375);
if (ang_start < 0.0) float _654;
{
ang_start += 6.283185482025146484375;
}
if (ang_end < 0.0)
{
ang_end += 6.283185482025146484375;
}
float _615;
if (ang_end > ang_start) if (ang_end > ang_start)
{ {
_615 = float((angle >= ang_start) && (angle <= ang_end)); _654 = float((angle >= ang_start) && (angle <= ang_end));
} }
else else
{ {
_615 = float((angle >= ang_start) || (angle <= ang_end)); _654 = float((angle >= ang_start) || (angle <= ang_end));
} }
float in_arc = _615; float in_arc = _654;
if (abs(ang_end - ang_start) >= 6.282185077667236328125) if (abs(ang_end - ang_start) >= 6.282185077667236328125)
{ {
in_arc = 1.0; in_arc = 1.0;
@@ -262,9 +277,9 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
d = (length(p) * cos(bn)) - radius_1; d = (length(p) * cos(bn)) - radius_1;
if ((flags & 1u) != 0u) if ((flags & 1u) != 0u)
{ {
float param_16 = d; float param_20 = d;
float param_17 = stroke_px_3; float param_21 = stroke_px_3;
d = sdf_stroke(param_16, param_17); d = sdf_stroke(param_20, param_21);
} }
} }
} }
@@ -272,9 +287,9 @@ fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[textur
} }
} }
} }
float param_18 = d; float param_22 = d;
float param_19 = soft; float param_23 = soft;
float alpha = sdf_alpha(param_18, param_19); float alpha = sdf_alpha(param_22, param_23);
out.out_color = float4(in.f_color.xyz, in.f_color.w * alpha); out.out_color = float4(in.f_color.xyz, in.f_color.w * alpha);
return out; return out;
} }
Binary file not shown.
+15 -9
View File
@@ -15,7 +15,8 @@ struct Primitive
float4 bounds; float4 bounds;
uint color; uint color;
uint kind_flags; uint kind_flags;
float2 _pad; float rotation;
float _pad;
float4 params; float4 params;
float4 params2; float4 params2;
}; };
@@ -25,7 +26,8 @@ struct Primitive_1
float4 bounds; float4 bounds;
uint color; uint color;
uint kind_flags; uint kind_flags;
float2 _pad; float rotation;
float _pad;
float4 params; float4 params;
float4 params2; float4 params2;
}; };
@@ -42,6 +44,7 @@ struct main0_out
float4 f_params [[user(locn2)]]; float4 f_params [[user(locn2)]];
float4 f_params2 [[user(locn3)]]; float4 f_params2 [[user(locn3)]];
uint f_kind_flags [[user(locn4)]]; uint f_kind_flags [[user(locn4)]];
float f_rotation [[user(locn5)]];
float4 gl_Position [[position]]; float4 gl_Position [[position]];
}; };
@@ -52,7 +55,7 @@ struct main0_in
float4 v_color [[attribute(2)]]; float4 v_color [[attribute(2)]];
}; };
vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer(0)]], const device Primitives& _70 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]]) vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer(0)]], const device Primitives& _72 [[buffer(1)]], uint gl_InstanceIndex [[instance_id]])
{ {
main0_out out = {}; main0_out out = {};
if (_12.mode == 0u) if (_12.mode == 0u)
@@ -62,17 +65,19 @@ vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer
out.f_params = float4(0.0); out.f_params = float4(0.0);
out.f_params2 = float4(0.0); out.f_params2 = float4(0.0);
out.f_kind_flags = 0u; out.f_kind_flags = 0u;
out.f_rotation = 0.0;
out.gl_Position = _12.projection * float4(in.v_position * _12.dpi_scale, 0.0, 1.0); out.gl_Position = _12.projection * float4(in.v_position * _12.dpi_scale, 0.0, 1.0);
} }
else else
{ {
Primitive p; Primitive p;
p.bounds = _70.primitives[int(gl_InstanceIndex)].bounds; p.bounds = _72.primitives[int(gl_InstanceIndex)].bounds;
p.color = _70.primitives[int(gl_InstanceIndex)].color; p.color = _72.primitives[int(gl_InstanceIndex)].color;
p.kind_flags = _70.primitives[int(gl_InstanceIndex)].kind_flags; p.kind_flags = _72.primitives[int(gl_InstanceIndex)].kind_flags;
p._pad = _70.primitives[int(gl_InstanceIndex)]._pad; p.rotation = _72.primitives[int(gl_InstanceIndex)].rotation;
p.params = _70.primitives[int(gl_InstanceIndex)].params; p._pad = _72.primitives[int(gl_InstanceIndex)]._pad;
p.params2 = _70.primitives[int(gl_InstanceIndex)].params2; p.params = _72.primitives[int(gl_InstanceIndex)].params;
p.params2 = _72.primitives[int(gl_InstanceIndex)].params2;
float2 corner = in.v_position; float2 corner = in.v_position;
float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner); float2 world_pos = mix(p.bounds.xy, p.bounds.zw, corner);
float2 center = (p.bounds.xy + p.bounds.zw) * 0.5; float2 center = (p.bounds.xy + p.bounds.zw) * 0.5;
@@ -81,6 +86,7 @@ vertex main0_out main0(main0_in in [[stage_in]], constant Uniforms& _12 [[buffer
out.f_params = p.params; out.f_params = p.params;
out.f_params2 = p.params2; out.f_params2 = p.params2;
out.f_kind_flags = p.kind_flags; out.f_kind_flags = p.kind_flags;
out.f_rotation = p.rotation;
out.gl_Position = _12.projection * float4(world_pos * _12.dpi_scale, 0.0, 1.0); out.gl_Position = _12.projection * float4(world_pos * _12.dpi_scale, 0.0, 1.0);
} }
return out; return out;
Binary file not shown.
+28 -10
View File
@@ -6,6 +6,7 @@ layout(location = 1) in vec2 f_local_or_uv;
layout(location = 2) in vec4 f_params; layout(location = 2) in vec4 f_params;
layout(location = 3) in vec4 f_params2; layout(location = 3) in vec4 f_params2;
layout(location = 4) flat in uint f_kind_flags; layout(location = 4) flat in uint f_kind_flags;
layout(location = 5) flat in float f_rotation;
// --- Output --- // --- Output ---
layout(location = 0) out vec4 out_color; layout(location = 0) out vec4 out_color;
@@ -82,6 +83,15 @@ float sdf_stroke(float d, float stroke_width) {
return abs(d) - stroke_width * 0.5; return abs(d) - stroke_width * 0.5;
} }
// Rotate a 2D point by the negative of the given angle (inverse rotation).
// Used to rotate the sampling frame opposite to the shape's rotation so that
// the SDF evaluates correctly for the rotated shape.
vec2 apply_rotation(vec2 p, float angle) {
float cr = cos(-angle);
float sr = sin(-angle);
return mat2(cr, sr, -sr, cr) * p;
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// main // main
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -113,11 +123,16 @@ void main() {
soft = max(f_params2.z, 1.0); soft = max(f_params2.z, 1.0);
float stroke_px = f_params2.w; float stroke_px = f_params2.w;
d = sdRoundedBox(f_local_or_uv, b, r); vec2 p_local = f_local_or_uv;
if (f_rotation != 0.0) {
p_local = apply_rotation(p_local, f_rotation);
}
d = sdRoundedBox(p_local, b, r);
if ((flags & 1u) != 0u) d = sdf_stroke(d, stroke_px); if ((flags & 1u) != 0u) d = sdf_stroke(d, stroke_px);
} }
else if (kind == 2u) { else if (kind == 2u) {
// Circle // Circle — rotationally symmetric, no rotation needed
float radius = f_params.x; float radius = f_params.x;
soft = max(f_params.y, 1.0); soft = max(f_params.y, 1.0);
float stroke_px = f_params.z; float stroke_px = f_params.z;
@@ -131,11 +146,16 @@ void main() {
soft = max(f_params.z, 1.0); soft = max(f_params.z, 1.0);
float stroke_px = f_params.w; float stroke_px = f_params.w;
d = sdEllipse(f_local_or_uv, ab); vec2 p_local = f_local_or_uv;
if (f_rotation != 0.0) {
p_local = apply_rotation(p_local, f_rotation);
}
d = sdEllipse(p_local, ab);
if ((flags & 1u) != 0u) d = sdf_stroke(d, stroke_px); if ((flags & 1u) != 0u) d = sdf_stroke(d, stroke_px);
} }
else if (kind == 4u) { else if (kind == 4u) {
// Segment (capsule line) // Segment (capsule line) — no rotation (excluded)
vec2 a = f_params.xy; // already in local physical pixels vec2 a = f_params.xy; // already in local physical pixels
vec2 b = f_params.zw; vec2 b = f_params.zw;
float width = f_params2.x; float width = f_params2.x;
@@ -144,7 +164,7 @@ void main() {
d = sdSegment(f_local_or_uv, a, b) - width * 0.5; d = sdSegment(f_local_or_uv, a, b) - width * 0.5;
} }
else if (kind == 5u) { else if (kind == 5u) {
// Ring / Arc // Ring / Arc — rotation handled by CPU angle offset, no shader rotation
float inner = f_params.x; float inner = f_params.x;
float outer = f_params.y; float outer = f_params.y;
float start_rad = f_params.z; float start_rad = f_params.z;
@@ -157,10 +177,8 @@ void main() {
// Angular clip // Angular clip
float angle = atan(f_local_or_uv.y, f_local_or_uv.x); float angle = atan(f_local_or_uv.y, f_local_or_uv.x);
if (angle < 0.0) angle += 2.0 * PI; if (angle < 0.0) angle += 2.0 * PI;
float ang_start = start_rad; float ang_start = mod(start_rad, 2.0 * PI);
float ang_end = end_rad; float ang_end = mod(end_rad, 2.0 * PI);
if (ang_start < 0.0) ang_start += 2.0 * PI;
if (ang_end < 0.0) ang_end += 2.0 * PI;
float in_arc = (ang_end > ang_start) float in_arc = (ang_end > ang_start)
? ((angle >= ang_start && angle <= ang_end) ? 1.0 : 0.0) : ((angle >= ang_start || angle <= ang_end) ? 1.0 : 0.0); ? ((angle >= ang_start && angle <= ang_end) ? 1.0 : 0.0) : ((angle >= ang_start || angle <= ang_end) ? 1.0 : 0.0);
@@ -169,7 +187,7 @@ void main() {
d = in_arc > 0.5 ? d_ring : 1e30; d = in_arc > 0.5 ? d_ring : 1e30;
} }
else if (kind == 6u) { else if (kind == 6u) {
// Regular N-gon // Regular N-gon — has its own rotation in params, no Primitive.rotation used
float radius = f_params.x; float radius = f_params.x;
float rotation = f_params.y; float rotation = f_params.y;
float sides = f_params.z; float sides = f_params.z;
+5 -1
View File
@@ -11,6 +11,7 @@ layout(location = 1) out vec2 f_local_or_uv;
layout(location = 2) out vec4 f_params; layout(location = 2) out vec4 f_params;
layout(location = 3) out vec4 f_params2; layout(location = 3) out vec4 f_params2;
layout(location = 4) flat out uint f_kind_flags; layout(location = 4) flat out uint f_kind_flags;
layout(location = 5) flat out float f_rotation;
// ---------- Uniforms (single block — avoids spirv-cross reordering on Metal) ---------- // ---------- Uniforms (single block — avoids spirv-cross reordering on Metal) ----------
layout(set = 1, binding = 0) uniform Uniforms { layout(set = 1, binding = 0) uniform Uniforms {
@@ -24,7 +25,8 @@ struct Primitive {
vec4 bounds; // 0-15: min_x, min_y, max_x, max_y vec4 bounds; // 0-15: min_x, min_y, max_x, max_y
uint color; // 16-19: packed u8x4 (unpack with unpackUnorm4x8) uint color; // 16-19: packed u8x4 (unpack with unpackUnorm4x8)
uint kind_flags; // 20-23: kind | (flags << 8) uint kind_flags; // 20-23: kind | (flags << 8)
vec2 _pad; // 24-31: padding float rotation; // 24-27: shader self-rotation in radians
float _pad; // 28-31: alignment padding
vec4 params; // 32-47: shape params part 1 vec4 params; // 32-47: shape params part 1
vec4 params2; // 48-63: shape params part 2 vec4 params2; // 48-63: shape params part 2
}; };
@@ -42,6 +44,7 @@ void main() {
f_params = vec4(0.0); f_params = vec4(0.0);
f_params2 = vec4(0.0); f_params2 = vec4(0.0);
f_kind_flags = 0u; f_kind_flags = 0u;
f_rotation = 0.0;
gl_Position = projection * vec4(v_position * dpi_scale, 0.0, 1.0); gl_Position = projection * vec4(v_position * dpi_scale, 0.0, 1.0);
} else { } else {
@@ -57,6 +60,7 @@ void main() {
f_params = p.params; f_params = p.params;
f_params2 = p.params2; f_params2 = p.params2;
f_kind_flags = p.kind_flags; f_kind_flags = p.kind_flags;
f_rotation = p.rotation;
gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0); gl_Position = projection * vec4(world_pos * dpi_scale, 0.0, 1.0);
} }
+665 -311
View File
File diff suppressed because it is too large Load Diff
+191 -19
View File
@@ -1,6 +1,8 @@
package draw package draw
import "core:c"
import "core:log" import "core:log"
import "core:strings"
import sdl "vendor:sdl3" import sdl "vendor:sdl3"
import sdl_ttf "vendor:sdl3/ttf" import sdl_ttf "vendor:sdl3/ttf"
@@ -11,11 +13,21 @@ Font_Key :: struct {
size: u16, size: u16,
} }
Cache_Source :: enum u8 {
Custom,
Clay,
}
Cache_Key :: struct {
id: u32,
source: Cache_Source,
}
Text_Cache :: struct { Text_Cache :: struct {
engine: ^sdl_ttf.TextEngine, engine: ^sdl_ttf.TextEngine,
font_bytes: [dynamic][]u8, font_bytes: [dynamic][]u8,
sdl_fonts: map[Font_Key]^sdl_ttf.Font, sdl_fonts: map[Font_Key]^sdl_ttf.Font,
cache: map[u32]^sdl_ttf.Text, cache: map[Cache_Key]^sdl_ttf.Text,
} }
// Internal for fetching SDL TTF font pointer for rendering // Internal for fetching SDL TTF font pointer for rendering
@@ -66,37 +78,194 @@ register_font :: proc(bytes: []u8) -> (id: Font_Id, ok: bool) #optional_ok {
} }
Text :: struct { Text :: struct {
ref: ^sdl_ttf.Text, sdl_text: ^sdl_ttf.Text,
position: [2]f32, position: [2]f32,
color: Color, color: Color,
} }
text :: proc( // ---------------------------------------------------------------------------------------------------------------------
id: u32, // ----- Text cache lookup -------------
txt: cstring, // ---------------------------------------------------------------------------------------------------------------------
pos: [2]f32,
font_id: Font_Id, // Shared cache lookup/create/update logic used by both the `text` proc and the Clay render path.
font_size: u16 = 44, // Returns the cached (or newly created) TTF_Text pointer.
color: Color = {0, 0, 0, 255}, @(private)
) -> Text { cache_get_or_update :: proc(key: Cache_Key, c_str: cstring, font: ^sdl_ttf.Font) -> ^sdl_ttf.Text {
sdl_text := GLOB.text_cache.cache[id] existing, found := GLOB.text_cache.cache[key]
if sdl_text == nil { if !found {
sdl_text = sdl_ttf.CreateText(GLOB.text_cache.engine, get_font(font_id, font_size), txt, 0) sdl_text := sdl_ttf.CreateText(GLOB.text_cache.engine, font, c_str, 0)
if sdl_text == nil { if sdl_text == nil {
log.panicf("Failed to create SDL text: %s", sdl.GetError()) log.panicf("Failed to create SDL text: %s", sdl.GetError())
} }
GLOB.text_cache.cache[id] = sdl_text GLOB.text_cache.cache[key] = sdl_text
return sdl_text
} else { } else {
//TODO if IDs are always unique and never change the underlying text if !sdl_ttf.SetTextString(existing, c_str, 0) {
// can get rid of this
if !sdl_ttf.SetTextString(sdl_text, txt, 0) {
log.panicf("Failed to update SDL text string: %s", sdl.GetError()) log.panicf("Failed to update SDL text string: %s", sdl.GetError())
} }
return existing
}
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Text drawing ------------------
// ---------------------------------------------------------------------------------------------------------------------
// Draw text at a position with optional rotation and origin.
//
// When `id` is nil (the default), the text is created and destroyed each frame — simple and
// leak-free, appropriate for HUDs and moderate UI (up to ~50 text elements per frame).
//
// When `id` is set, the TTF_Text object is cached across frames keyed by the provided u32.
// This avoids per-frame HarfBuzz shaping and allocation, which matters for text-heavy apps
// (editors, terminals, chat). The user is responsible for choosing unique IDs per logical text
// element and calling `clear_text_cache` or `clear_text_cache_entry` when cached entries are
// no longer needed. Custom text IDs occupy a separate namespace from Clay text IDs, so
// collisions between the two are impossible.
//
// `origin` is in pixels from the text block's top-left corner (raylib convention).
// The point whose local coords equal `origin` lands at `pos` in world space.
// `rotation` is in degrees, counter-clockwise.
text :: proc(
layer: ^Layer,
text_string: string,
position: [2]f32,
font_id: Font_Id,
font_size: u16 = 44,
color: Color = BLACK,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
id: Maybe(u32) = nil,
temp_allocator := context.temp_allocator,
) {
c_str := strings.clone_to_cstring(text_string, temp_allocator)
sdl_text: ^sdl_ttf.Text
cached := false
if cache_id, ok := id.?; ok {
cached = true
sdl_text = cache_get_or_update(Cache_Key{cache_id, .Custom}, c_str, get_font(font_id, font_size))
} else {
sdl_text = sdl_ttf.CreateText(GLOB.text_cache.engine, get_font(font_id, font_size), c_str, 0)
if sdl_text == nil {
log.panicf("Failed to create SDL text: %s", sdl.GetError())
}
} }
return Text{sdl_text, pos, color} if needs_transform(origin, rotation) {
dpi_scale := GLOB.dpi_scaling
transform := build_pivot_rotation(position * dpi_scale, origin * dpi_scale, rotation)
prepare_text_transformed(layer, Text{sdl_text, {0, 0}, color}, transform)
} else {
prepare_text(layer, Text{sdl_text, position, color})
}
if !cached {
// Don't destroy now — the draw data (atlas texture, vertices) is still referenced
// by the batch buffers until end() submits to the GPU. Deferred to clear_global().
append(&GLOB.tmp_uncached_text, sdl_text)
}
} }
// ---------------------------------------------------------------------------------------------------------------------
// ----- Public text measurement -------
// ---------------------------------------------------------------------------------------------------------------------
// Measure a string in logical pixels (pre-DPI-scaling) using the same font backend as the renderer.
measure_text :: proc(
text_string: string,
font_id: Font_Id,
font_size: u16 = 44,
allocator := context.temp_allocator,
) -> [2]f32 {
c_str := strings.clone_to_cstring(text_string, allocator)
width, height: c.int
if !sdl_ttf.GetStringSize(get_font(font_id, font_size), c_str, 0, &width, &height) {
log.panicf("Failed to measure text: %s", sdl.GetError())
}
return {f32(width) / GLOB.dpi_scaling, f32(height) / GLOB.dpi_scaling}
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Text anchor helpers -----------
// ---------------------------------------------------------------------------------------------------------------------
center_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
size := measure_text(text_string, font_id, font_size)
return size * 0.5
}
top_left_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
return {0, 0}
}
top_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
size := measure_text(text_string, font_id, font_size)
return {size.x * 0.5, 0}
}
top_right_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
size := measure_text(text_string, font_id, font_size)
return {size.x, 0}
}
left_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
size := measure_text(text_string, font_id, font_size)
return {0, size.y * 0.5}
}
right_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
size := measure_text(text_string, font_id, font_size)
return {size.x, size.y * 0.5}
}
bottom_left_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
size := measure_text(text_string, font_id, font_size)
return {0, size.y}
}
bottom_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
size := measure_text(text_string, font_id, font_size)
return {size.x * 0.5, size.y}
}
bottom_right_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = 44) -> [2]f32 {
size := measure_text(text_string, font_id, font_size)
return size
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Cache management --------------
// ---------------------------------------------------------------------------------------------------------------------
// Destroy all cached text objects (both custom and Clay entries). Call on scene transitions,
// view changes, or periodically in apps that produce many distinct cached text entries over time.
// After calling this, subsequent text draws with an `id` will re-create their cache entries.
clear_text_cache :: proc() {
for _, sdl_text in GLOB.text_cache.cache {
sdl_ttf.DestroyText(sdl_text)
}
clear(&GLOB.text_cache.cache)
}
// Destroy a specific cached custom text entry by its u32 id (the same value passed to the
// `text` proc's `id` parameter). This only affects custom text entries — Clay text entries
// are managed internally and are not addressable by the user.
// No-op if the id is not in the cache.
clear_text_cache_entry :: proc(id: u32) {
key := Cache_Key{id, .Custom}
sdl_text, ok := GLOB.text_cache.cache[key]
if ok {
sdl_ttf.DestroyText(sdl_text)
delete_key(&GLOB.text_cache.cache, key)
}
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Internal cache lifecycle ------
// ---------------------------------------------------------------------------------------------------------------------
@(private, require_results) @(private, require_results)
init_text_cache :: proc( init_text_cache :: proc(
device: ^sdl.GPUDevice, device: ^sdl.GPUDevice,
@@ -121,7 +290,7 @@ init_text_cache :: proc(
text_cache = Text_Cache { text_cache = Text_Cache {
engine = engine, engine = engine,
cache = make(map[u32]^sdl_ttf.Text, allocator = allocator), cache = make(map[Cache_Key]^sdl_ttf.Text, allocator = allocator),
} }
log.debug("Done initializing text cache") log.debug("Done initializing text cache")
@@ -132,6 +301,9 @@ destroy_text_cache :: proc() {
for _, font in GLOB.text_cache.sdl_fonts { for _, font in GLOB.text_cache.sdl_fonts {
sdl_ttf.CloseFont(font) sdl_ttf.CloseFont(font)
} }
for _, sdl_text in GLOB.text_cache.cache {
sdl_ttf.DestroyText(sdl_text)
}
delete(GLOB.text_cache.sdl_fonts) delete(GLOB.text_cache.sdl_fonts)
delete(GLOB.text_cache.font_bytes) delete(GLOB.text_cache.font_bytes)
delete(GLOB.text_cache.cache) delete(GLOB.text_cache.cache)