Add ability to render non-Clay primitives + proper layering support #8

Merged
shan merged 3 commits from updates into master 2025-07-29 06:18:17 +00:00
10 changed files with 160 additions and 48 deletions
Showing only changes of commit f11f688ea0 - Show all commits

View File

@@ -1,12 +1,12 @@
package main package main
import clay "../clay"
import "../renderer" import "../renderer"
import "core:c" import "core:c"
import "core:fmt" import "core:fmt"
import "core:log" import "core:log"
import "core:mem" import "core:mem"
import "core:os" import "core:os"
import clay "library:clay"
import sdl "vendor:sdl3" import sdl "vendor:sdl3"
WINDOW_WIDTH :: 1024 WINDOW_WIDTH :: 1024
@@ -165,8 +165,43 @@ destroy :: proc() {
update :: proc(cmd_buffer: ^sdl.GPUCommandBuffer, delta_time: u64) -> bool { update :: proc(cmd_buffer: ^sdl.GPUCommandBuffer, delta_time: u64) -> bool {
frame_time := f32(delta_time) / 1000.0 frame_time := f32(delta_time) / 1000.0
input := input() input := input()
render_cmds := layout() mouse_x, mouse_y: f32
renderer.prepare(device, window, cmd_buffer, &render_cmds, input.mouse_delta, frame_time) mouse_flags := sdl.GetMouseState(&mouse_x, &mouse_y)
width, height: c.int
sdl.GetWindowSize(window, &width, &height)
layer := renderer.begin_prepare()
// ===== Begin processing primitives for GPU upload =====
// Everything after begin_prepare() is uploaded in-order. We pass the layer down
// until we need a new one, after which we call new_layer()
// Render raw primitive setup
//renderer.prepare_batch(device, window, cmd_buffer, &layer, &primitives) //TODO
clay_layer_bounds := renderer.Rectangle {
x = f32(width) / 2.0,
y = 0.0,
w = f32(width) / 2.0,
h = f32(height),
}
// Create a new layer, because these two scenes cannot be renderer in the same batch due to overlap
layer = renderer.new_layer(&layer, clay_layer_bounds)
clay_batch := clay_layout(clay_layer_bounds)
renderer.prepare_clay_batch(
device,
window,
cmd_buffer,
&layer,
{mouse_x, mouse_y},
mouse_flags,
input.mouse_delta,
frame_time,
&clay_batch,
)
// This uploads the primitive data to the GPU
renderer.end_prepare(device, cmd_buffer, &layer)
return input.should_quit return input.should_quit
} }
@@ -210,7 +245,8 @@ draw :: proc(cmd_buffer: ^sdl.GPUCommandBuffer) {
} }
} }
layout :: proc() -> clay.ClayArray(clay.RenderCommand) { clay_layout :: proc(bounds: renderer.Rectangle) -> renderer.ClayBatch {
clay.SetLayoutDimensions(clay.Dimensions{bounds.w, bounds.h})
clay.BeginLayout() clay.BeginLayout()
if clay.UI()( if clay.UI()(
@@ -247,5 +283,5 @@ layout :: proc() -> clay.ClayArray(clay.RenderCommand) {
clay.Text("Test Text", &body_text) clay.Text("Test Text", &body_text)
} }
return clay.EndLayout() return renderer.ClayBatch{bounds, clay.EndLayout()}
} }

View File

@@ -37,7 +37,6 @@ create_quad_pipeline :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> Qua
log.debug("Loaded", len(frag_raw), "frag bytes") log.debug("Loaded", len(frag_raw), "frag bytes")
log.debug("ShaderType:", SHADER_TYPE) log.debug("ShaderType:", SHADER_TYPE)
vert_info := sdl.GPUShaderCreateInfo { vert_info := sdl.GPUShaderCreateInfo {
code_size = len(vert_raw), code_size = len(vert_raw),
code = raw_data(vert_raw), code = raw_data(vert_raw),

View File

@@ -1,11 +1,11 @@
package renderer package renderer
import clay "../clay"
import "base:runtime" import "base:runtime"
import "core:c" import "core:c"
import "core:log" import "core:log"
import "core:os" import "core:os"
import "core:strings" import "core:strings"
import clay "library:clay"
import sdl "vendor:sdl3" import sdl "vendor:sdl3"
import sdl_ttf "vendor:sdl3/ttf" import sdl_ttf "vendor:sdl3/ttf"
@@ -25,8 +25,35 @@ quad_pipeline: QuadPipeline
text_pipeline: TextPipeline text_pipeline: TextPipeline
odin_context: runtime.Context odin_context: runtime.Context
// TODO New layer for each z-index/batch // I need to make it so that I can
// a) Add primitives directly to a layer
// b) Create small nested clay-layouts that can be batched with other shit
// Some colletion of items, where its just primtiives, but collections of primitives? And each collection has a type,
// either raw or clay? Or something
// Prepare - upload to GPU
// With clay commands, this requires converting to my custom primitive types
// With raw commands, this would require no conversion
// Need to go from UI declaration -> processing render cmds
// I want to process & upload in the same loop, don't want to add an additional pass where I transform
// clay cmds -> my primitives
// 1) I need to be able to process all clay cmds & all raw primitive cmds in a single iteration
// 2) I need to be able to define (at a layout level) which primitives can be rendered in the same batch
//
// Some kind of queue? Each UI element adds to a temp queue that is reset every frame
// Queue is like
// [ chunk of primitives ] [ chunk of clay primitives ] [ new layer indicator ] [ chunk ]
Rectangle :: struct {
x: f32,
y: f32,
w: f32,
h: f32,
}
Layer :: struct { Layer :: struct {
bounds: Rectangle,
quad_instance_start: u32, quad_instance_start: u32,
quad_len: u32, quad_len: u32,
text_instance_start: u32, text_instance_start: u32,
@@ -35,6 +62,7 @@ Layer :: struct {
text_vertex_len: u32, text_vertex_len: u32,
text_index_start: u32, text_index_start: u32,
text_index_len: u32, text_index_len: u32,
curr_scissor_index: u32,
scissors: [dynamic]Scissor, scissors: [dynamic]Scissor,
} }
@@ -68,6 +96,7 @@ init :: proc(
text_pipeline = create_text_pipeline(device, window) text_pipeline = create_text_pipeline(device, window)
} }
@(private = "file")
clay_error_handler :: proc "c" (errorData: clay.ErrorData) { clay_error_handler :: proc "c" (errorData: clay.ErrorData) {
context = odin_context context = odin_context
log.error("Clay error:", errorData.errorType, errorData.errorText) log.error("Clay error:", errorData.errorType, errorData.errorText)
@@ -95,26 +124,9 @@ destroy :: proc(device: ^sdl.GPUDevice) {
destroy_text_pipeline(device) destroy_text_pipeline(device)
} }
/// Upload data to the GPU /// Sets up renderer to begin upload to the GPU. Returns starting `Layer` to begin processing primitives for
prepare :: proc( begin_prepare :: proc() -> Layer {
device: ^sdl.GPUDevice, // Prepare to upload to GPU
window: ^sdl.Window,
cmd_buffer: ^sdl.GPUCommandBuffer,
render_commands: ^clay.ClayArray(clay.RenderCommand),
mouse_delta: [2]f32,
frame_time: f32,
) {
mouse_x, mouse_y: f32
mouse_flags := sdl.GetMouseState(&mouse_x, &mouse_y)
// Currently MacOS blocks main thread when resizing, this will be fixed with next SDL3 release
window_w, window_h: c.int
window_size := sdl.GetWindowSize(window, &window_w, &window_h)
// Update clay internals
clay.SetPointerState(clay.Vector2{mouse_x, mouse_y}, .LEFT in mouse_flags)
clay.UpdateScrollContainers(true, transmute(clay.Vector2)mouse_delta, frame_time)
clay.SetLayoutDimensions({f32(window_w), f32(window_h)})
clear(&layers) clear(&layers)
clear(&tmp_quads) clear(&tmp_quads)
clear(&tmp_text) clear(&tmp_text)
@@ -125,12 +137,82 @@ prepare :: proc(
layer := Layer { layer := Layer {
scissors = make([dynamic]Scissor, 0, 10, context.temp_allocator), scissors = make([dynamic]Scissor, 0, 10, context.temp_allocator),
} }
return layer
}
/// Creates a new layer, appending the old one to `layers`
new_layer :: proc(layer: ^Layer, bounds: Rectangle) -> Layer {
append(&layers, layer^)
layer := Layer {
bounds = bounds,
scissors = make([dynamic]Scissor, 0, 10, context.temp_allocator),
}
return layer
}
end_prepare :: proc(device: ^sdl.GPUDevice, cmd_buffer: ^sdl.GPUCommandBuffer, layer: ^Layer) {
// Commit last layer worked on
append(&layers, layer^)
// Upload primitives to GPU
copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
upload_quads(device, copy_pass)
upload_text(device, copy_pass)
sdl.EndGPUCopyPass(copy_pass)
}
// ===== Built-in primitive processing =====
//prepare_batch :: proc(
// device: ^sdl.GPUDevice,
// window: ^sdl.Window,
// cmd_buffer: ^sdl.GPUCommandBuffer,
// layer: ^Layer,
// primitives: ^[]Primitive,
//) {
// scissor := Scissor{}
//
//
//}
// ====== Clay-specific processing ======
ClayBatch :: struct {
bounds: Rectangle,
cmds: clay.ClayArray(clay.RenderCommand),
}
/// Upload data to the GPU
prepare_clay_batch :: proc(
device: ^sdl.GPUDevice,
window: ^sdl.Window,
cmd_buffer: ^sdl.GPUCommandBuffer,
layer: ^Layer,
mouse_pos: [2]f32,
mouse_flags: sdl.MouseButtonFlags,
mouse_wheel_delta: [2]f32,
frame_time: f32,
batch: ^ClayBatch,
) {
// Update clay internals
clay.SetPointerState(
clay.Vector2{mouse_pos.x - layer.bounds.x, mouse_pos.y - layer.bounds.y},
.LEFT in mouse_flags,
)
clay.UpdateScrollContainers(true, transmute(clay.Vector2)mouse_wheel_delta, frame_time)
scissor := Scissor{} scissor := Scissor{}
// Parse render commands // Parse render commands
for i in 0 ..< int(render_commands.length) { for i in 0 ..< int(batch.cmds.length) {
render_command := clay.RenderCommandArray_Get(render_commands, cast(i32)i) render_command := clay.RenderCommandArray_Get(&batch.cmds, cast(i32)i)
bounds := render_command.boundingBox // Translate bounding box of the primitive by the layer position
bounds := Rectangle {
x = render_command.boundingBox.x + layer.bounds.x,
y = render_command.boundingBox.y + layer.bounds.y,
w = render_command.boundingBox.width,
h = render_command.boundingBox.height,
}
switch (render_command.commandType) { switch (render_command.commandType) {
case clay.RenderCommandType.None: case clay.RenderCommandType.None:
@@ -173,27 +255,28 @@ prepare :: proc(
bounds := sdl.Rect { bounds := sdl.Rect {
c.int(bounds.x * dpi_scaling), c.int(bounds.x * dpi_scaling),
c.int(bounds.y * dpi_scaling), c.int(bounds.y * dpi_scaling),
c.int(bounds.width * dpi_scaling), c.int(bounds.w * dpi_scaling),
c.int(bounds.height * dpi_scaling), c.int(bounds.h * dpi_scaling),
} }
new := new_scissor(&scissor)
if scissor.quad_len != 0 || scissor.text_len != 0 { if scissor.quad_len != 0 || scissor.text_len != 0 {
new := new_scissor(&scissor)
append(&layer.scissors, scissor) append(&layer.scissors, scissor)
scissor = new
} }
scissor = new
scissor.bounds = bounds scissor.bounds = bounds
case clay.RenderCommandType.ScissorEnd: case clay.RenderCommandType.ScissorEnd:
new := new_scissor(&scissor)
if scissor.quad_len != 0 || scissor.text_len != 0 { if scissor.quad_len != 0 || scissor.text_len != 0 {
new := new_scissor(&scissor)
append(&layer.scissors, scissor) append(&layer.scissors, scissor)
scissor = new
} }
scissor = new
case clay.RenderCommandType.Rectangle: case clay.RenderCommandType.Rectangle:
render_data := render_command.renderData.rectangle render_data := render_command.renderData.rectangle
color := f32_color(render_data.backgroundColor) color := f32_color(render_data.backgroundColor)
cr := render_data.cornerRadius cr := render_data.cornerRadius
quad := Quad { quad := Quad {
position_scale = {bounds.x, bounds.y, bounds.width, bounds.height}, position_scale = {bounds.x, bounds.y, bounds.w, bounds.h},
corner_radii = {cr.bottomRight, cr.topRight, cr.bottomLeft, cr.topLeft}, corner_radii = {cr.bottomRight, cr.topRight, cr.bottomLeft, cr.topLeft},
color = color, color = color,
} }
@@ -205,7 +288,7 @@ prepare :: proc(
cr := render_data.cornerRadius cr := render_data.cornerRadius
//TODO dedicated border pipeline //TODO dedicated border pipeline
quad := Quad { quad := Quad {
position_scale = {bounds.x, bounds.y, bounds.width, bounds.height}, position_scale = {bounds.x, bounds.y, bounds.w, bounds.h},
corner_radii = {cr.bottomRight, cr.topRight, cr.bottomLeft, cr.topLeft}, corner_radii = {cr.bottomRight, cr.topRight, cr.bottomLeft, cr.topLeft},
color = f32_color(clay.Color{0.0, 0.0, 0.0, 0.0}), color = f32_color(clay.Color{0.0, 0.0, 0.0, 0.0}),
border_color = f32_color(render_data.color), border_color = f32_color(render_data.color),
@@ -221,15 +304,9 @@ prepare :: proc(
} }
} }
//TODO start new layers with z-index changes if scissor.quad_len != 0 || scissor.text_len != 0 {
append(&layer.scissors, scissor) append(&layer.scissors, scissor)
append(&layers, layer) }
// Upload primitives to GPU
copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
upload_quads(device, copy_pass)
upload_text(device, copy_pass)
sdl.EndGPUCopyPass(copy_pass)
} }
/// Render primitives /// Render primitives