diff --git a/box.odin b/box.odin index d2d7f85..db83cec 100644 --- a/box.odin +++ b/box.odin @@ -61,4 +61,4 @@ box_set_style :: proc(box: ^Box, style: ^Box_Style_Override, use_transitions: bo box_apply_style :: proc(box: ^Box, style: Box_Style_Override, use_transitions: bool = true) { layout_box_apply_style(box, style, use_transitions) -} \ No newline at end of file +} diff --git a/examples/assets/Lora.ttf b/examples/assets/Lora.ttf new file mode 100644 index 0000000..f9eb7ce Binary files /dev/null and b/examples/assets/Lora.ttf differ diff --git a/examples/box_sdf.fs b/examples/assets/box_sdf.fs similarity index 100% rename from examples/box_sdf.fs rename to examples/assets/box_sdf.fs diff --git a/examples/vertex.vs b/examples/assets/vertex.vs similarity index 100% rename from examples/vertex.vs rename to examples/assets/vertex.vs diff --git a/examples/counters.odin b/examples/counters.odin index a8cb085..4c7e5ee 100644 --- a/examples/counters.odin +++ b/examples/counters.odin @@ -1,54 +1,7 @@ package main import syl "../" -import renderer "../renderer/raylib" import "core:math/ease" -import "core:fmt" -import rl "vendor:raylib" - -GREEN :: [4]u8{175,194,30, 255} -BLUE :: [4]u8{0, 0, 255, 255} -YELLOW :: [4]u8{255,255,40, 255} -WHITE :: [4]u8{255,255,255, 255} -BLACK :: [4]u8{0,0,0, 255} -BLANK :: [4]u8{0, 0, 0, 0} -RED :: [4]u8{255, 0, 0, 255} -DARK :: [4]u8{31,31,33, 255} -CELESTE :: [4]u8{80,99,132, 255} - -SCREEN_W :: 800 -SCREEN_H :: 500 - -style_sheet := syl.Style_Sheet { - box = { - padding = 0 - }, - button = { - normal = { - text_color = BLACK, - font_size = 18, - background_color = GREEN, - padding = {10,15,10,15}, - border_radius = 8, - transitions = { - background_color = {0.05, .Linear}, - }, - }, - hover = { - background_color = YELLOW, - }, - press = { - background_color = GREEN, - transitions = syl.Box_Transitions{ - background_color = {0, .Linear} - } - } - }, - text = { - color = WHITE, - font_size = 18 - } -} primary_button := syl.Button_Styles_Override { normal = { @@ -72,7 +25,7 @@ primary_button := syl.Button_Styles_Override { } } -Message :: enum { +Message_Counter :: enum { Increment, Decrement } @@ -83,7 +36,7 @@ Counter :: struct { counter_label: ^syl.Text, } -counter_update:: proc(using counter: ^Counter, message: ^Message) { +counter_update:: proc(using counter: ^Counter, message: ^Message_Counter) { switch message^ { case .Increment: count += 1 case .Decrement: count -= 1 @@ -99,26 +52,22 @@ counter_destroy :: proc(counter: ^Counter) { counter :: proc() -> ^Counter { c := new(Counter) syl.box( - handler = syl.make_handler(c, syl.Box, Message, counter_update, counter_destroy), + handler = syl.make_handler(c, syl.Box, Message_Counter, counter_update, counter_destroy), layout_direction = .Left_To_Right, gap = 10, children = { - syl.button(text_content = "-", on_click = Message.Decrement, style=&primary_button), + syl.button(text_content = "-", on_click = Message_Counter.Decrement, style=&primary_button), syl.box( syl.text("0", ref = &c.counter_label), sizing=syl.Expand, ), - syl.button(text_content = "+", on_click = Message.Increment), + syl.button(text_content = "+", on_click = Message_Counter.Increment), } ) return c } -main :: proc() { - rl.SetConfigFlags({.MSAA_4X_HINT}) - rl.InitWindow(SCREEN_W, SCREEN_H, "Syl in Raylib") - rl.SetTargetFPS(60) - +counter_app :: proc () -> ^syl.Box{ app := syl.box( counter(), counter(), @@ -131,15 +80,6 @@ main :: proc() { gap=4, ) - renderer.init() - for !rl.WindowShouldClose() { - renderer.update(app) - - rl.BeginDrawing() - rl.ClearBackground(cast(rl.Color)BLACK) - renderer.render(app) - rl.EndDrawing() - } - - rl.CloseWindow() + return app } + diff --git a/examples/game_menu.odin b/examples/game_menu.odin index 30b9086..bb86dac 100644 --- a/examples/game_menu.odin +++ b/examples/game_menu.odin @@ -1,47 +1,45 @@ package main import syl "../" -import renderer "../renderer/raylib" import "core:math/ease" -import rl "vendor:raylib" SCREEN_W :: 800 SCREEN_H :: 500 -BACKGROUND_COLOR :: [4]u8{17,45,58, 255} -BLACK :: [4]u8{0,0,0, 255} +BACKGROUND_COLOR :: [4]u8{17, 45, 58, 255} +BLACK :: [4]u8{0, 0, 0, 255} WHITE :: [4]u8{255, 255, 255, 255} -PRIMARY_COLOR :: [4]u8{236,155,92, 255} // orange -SECONDARY_COLOR :: [4]u8{160,223,227, 255} -PRIMARY_TEXT_COLOR :: [4]u8{25,16,0, 255} // black -MUTED :: [4]u8{46,79,90, 255} +PRIMARY_COLOR :: [4]u8{236, 155, 92, 255} // orange +SECONDARY_COLOR :: [4]u8{160, 223, 227, 255} +PRIMARY_TEXT_COLOR :: [4]u8{25, 16, 0, 255} // black +MUTED :: [4]u8{46, 79, 90, 255} + +GREEN :: [4]u8{175, 194, 30, 255} +BLUE :: [4]u8{0, 0, 255, 255} +YELLOW :: [4]u8{255, 255, 40, 255} +BLANK :: [4]u8{0, 0, 0, 0} +RED :: [4]u8{255, 0, 0, 255} +DARK :: [4]u8{31, 31, 33, 255} +CELESTE :: [4]u8{80, 99, 132, 255} style_sheet := syl.Style_Sheet { - box = { - padding = {0,0,0,10}, - transitions = { - padding = {0.15, .Linear}, - }, - }, + box = {padding = {0, 0, 0, 10}, transitions = {padding = {0.15, .Linear}}}, button = { normal = { text_color = SECONDARY_COLOR, font_size = 18, background_color = BACKGROUND_COLOR, - padding = {10,30,10,30}, + padding = {10, 30, 10, 30}, // border with different colors border_color = { PRIMARY_COLOR, // top - MUTED, // right - MUTED, // bottom + MUTED, // right + MUTED, // bottom PRIMARY_COLOR, // left }, - border_thickness = {2,2,2,12}, - border_radius = {0,0,20,0}, - transitions = { - background_color = {0.2, .Cubic_Out}, - padding = {0.1, .Cubic_Out}, - }, + border_thickness = {2, 2, 2, 12}, + border_radius = {20, 20, 20, 20}, + transitions = {background_color = {0.2, .Cubic_Out}, padding = {0.1, .Cubic_Out}}, }, hover = { background_color = PRIMARY_COLOR, @@ -51,30 +49,25 @@ style_sheet := syl.Style_Sheet { press = { background_color = WHITE, border_color = WHITE, - padding = [4]f32{13,30,7,30}, - transitions = syl.Box_Transitions{ - background_color = {0, .Linear} - } - } + padding = [4]f32{13, 30, 7, 30}, + transitions = syl.Box_Transitions{background_color = {0, .Linear}}, + }, }, - text = { - color = SECONDARY_COLOR, - font_size = 18 - } + text = {color = SECONDARY_COLOR, font_size = 18}, } Game_UI :: struct { - count: int, + count: int, using base: syl.Box, - container: ^syl.Box, - sub_menus: struct { - start: ^syl.Box, + container: ^syl.Box, + sub_menus: struct { + start: ^syl.Box, settings: ^syl.Box, - network: ^syl.Box, - credits: ^syl.Box, - exit: ^syl.Box, + network: ^syl.Box, + credits: ^syl.Box, + exit: ^syl.Box, }, - current: ^syl.Box, + current: ^syl.Box, } Message :: enum { @@ -92,11 +85,16 @@ game_ui_update :: proc(using game_ui: ^Game_UI, msg: ^Message) { } switch msg^ { - case .Start: current = sub_menus.start - case .Network: current = sub_menus.network - case .Settings: current = sub_menus.settings - case .Credits: current = sub_menus.credits - case .Exit: current = sub_menus.exit + case .Start: + current = sub_menus.start + case .Network: + current = sub_menus.network + case .Settings: + current = sub_menus.settings + case .Credits: + current = sub_menus.credits + case .Exit: + current = sub_menus.exit } syl.element_add_child(container, current, use_transitions = true) @@ -113,94 +111,114 @@ game_menu_ui :: proc() -> ^Game_UI { game_ui.sub_menus.exit = exit() // With a handler syl.box will initialize Game_UI instead creating a new Box - syl.box(size = {SCREEN_H, SCREEN_H}, style_sheet = &style_sheet, handler = handler, children = { - syl.center( - syl.box( - syl.box(gap=10, children = { - syl.button(text_content="START", size={200,40}, on_mouse_over = Message.Start), - syl.button(text_content="SETTINGS", size={200,40}, on_mouse_over = Message.Settings), - syl.button(text_content="NETWORK", size={200,40}, on_mouse_over = Message.Network), - syl.button(text_content="CREDITS", size={200,40}, on_mouse_over = Message.Credits), - syl.button(text_content="EXIT", size={200,40}, on_mouse_over = Message.Exit), - }), - syl.box(ref=&game_ui.container, width=200, height_sizing=.Expand), - layout_direction = .Left_To_Right - ) - ) - }) + syl.box( + size = {SCREEN_H, SCREEN_H}, + style_sheet = &style_sheet, + handler = handler, + children = { + syl.center( + syl.box( + syl.box( + gap = 10, + children = { + syl.button( + text_content = "START", + size = {200, 40}, + on_mouse_over = Message.Start, + ), + syl.button( + text_content = "SETTINGS", + size = {200, 40}, + on_mouse_over = Message.Settings, + ), + syl.button( + text_content = "NETWORK", + size = {200, 40}, + on_mouse_over = Message.Network, + ), + syl.button( + text_content = "CREDITS", + size = {200, 40}, + on_mouse_over = Message.Credits, + ), + syl.button( + text_content = "EXIT", + size = {200, 40}, + on_mouse_over = Message.Exit, + ), + }, + ), + syl.box(ref = &game_ui.container, width = 200, height_sizing = .Expand), + layout_direction = .Left_To_Right, + ), + ), + }, + ) return game_ui } start :: proc() -> ^syl.Box { - return syl.box(gap=10, children = { - syl.button(text_content="CONTINUE", size={200,40}), - syl.button(text_content="NEW GAME", size={200,40}), - }), + return syl.box( + gap = 10, + children = { + syl.button(text_content = "CONTINUE", size = {200, 40}), + syl.button(text_content = "NEW GAME", size = {200, 40}), + }, + ) } settings :: proc() -> ^syl.Box { - return syl.box(gap=10, children = { - syl.button(text_content="AUDIO", size={200,40}), - syl.button(text_content="VIDEO", size={200,40}), - syl.button(text_content="CONTROLS", size={200,40}), - syl.button(text_content="GAMEPLAY", size={200,40}), - }), + return syl.box( + gap = 10, + children = { + syl.button(text_content = "AUDIO", size = {200, 40}), + syl.button(text_content = "VIDEO", size = {200, 40}), + syl.button(text_content = "CONTROLS", size = {200, 40}), + syl.button(text_content = "GAMEPLAY", size = {200, 40}), + }, + ) } network :: proc() -> ^syl.Box { - return syl.box(gap=10, children = { - syl.button(text_content="CONNECT TO SERVER", size={240,40}), - syl.button(text_content="HOST GAME", size={240,40}), - syl.button(text_content="DISCONNECT", size={240,40}), - }), + return syl.box( + gap = 10, + children = { + syl.button(text_content = "CONNECT TO SERVER", size = {240, 40}), + syl.button(text_content = "HOST GAME", size = {240, 40}), + syl.button(text_content = "DISCONNECT", size = {240, 40}), + }, + ) } credits :: proc() -> ^syl.Box { - return syl.box(gap=10, sizing=syl.Expand, children = { - syl.center( - syl.text("Programmer: CRSOLVER", wrap = false), - ), - }), + return syl.box( + gap = 10, + sizing = syl.Expand, + children = {syl.center(syl.text("Programmer: CRSOLVER", wrap = false))}, + ) } exit :: proc() -> ^syl.Box { - return syl.box(sizing=syl.Expand, children = { - syl.center( - syl.box( - syl.text("ARE YOU SURE?", wrap = false), + return syl.box( + sizing = syl.Expand, + children = { + syl.center( syl.box( - syl.button(text_content="YES", size={100,40}), - syl.button(text_content="NO", size={100,40}), - layout_direction = .Left_To_Right, + syl.text("ARE YOU SURE?", wrap = false), + syl.box( + syl.button(text_content = "YES", size = {100, 40}), + syl.button(text_content = "NO", size = {100, 40}), + layout_direction = .Left_To_Right, + padding = 0, + gap = 10, + ), + gap = 10, padding = 0, - gap = 10 ), - gap=10, - padding=0, - ) - ) - }), -} - -main :: proc() { - //rl.SetConfigFlags({.VSYNC_HINT}) - rl.InitWindow(SCREEN_W, SCREEN_H, "Game Settings") - ui := game_menu_ui() - - renderer.init() - defer renderer.deinit() - - for !rl.WindowShouldClose() { - renderer.update(ui) - - rl.BeginDrawing() - rl.ClearBackground(cast(rl.Color)BACKGROUND_COLOR) - renderer.render(ui) - rl.EndDrawing() - } - - rl.CloseWindow() + ), + }, + ) } game_ui_destroy :: proc(game_ui: ^Game_UI) { @@ -210,4 +228,4 @@ game_ui_destroy :: proc(game_ui: ^Game_UI) { syl.element_destroy(game_ui.sub_menus.credits) syl.element_destroy(game_ui.sub_menus.exit) free(game_ui) -} \ No newline at end of file +} diff --git a/examples/main.odin b/examples/main.odin new file mode 100644 index 0000000..f411ec5 --- /dev/null +++ b/examples/main.odin @@ -0,0 +1,104 @@ +package main + +import "core:fmt" +import "core:mem" + +import syl "../" + +import sdl_renderer "../renderer/sdl_gpu/" +import sdl "vendor:sdl3" + +import rl_renderer "../renderer/raylib/" +import rl "vendor:raylib" + +main :: proc() { + when ODIN_DEBUG { + track: mem.Tracking_Allocator + mem.tracking_allocator_init(&track, context.allocator) + context.allocator = mem.tracking_allocator(&track) + + defer { + if len(track.allocation_map) > 0 { + fmt.eprintf("=== %v allocations not freed: ===\n", len(track.allocation_map)) + for _, entry in track.allocation_map { + fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location) + } + } + mem.tracking_allocator_destroy(&track) + } + } + + run_sdl_backend() +} + +run_raylib_backend :: proc() { + rl.InitWindow(SCREEN_W, SCREEN_H, "Game Settings") + ui := game_menu_ui() + + rl_renderer.init() + defer rl_renderer.deinit() + + for !rl.WindowShouldClose() { + rl_renderer.update(ui) + + rl.BeginDrawing() + rl.ClearBackground(cast(rl.Color)BACKGROUND_COLOR) + rl_renderer.render(ui) + rl.EndDrawing() + } + + rl.CloseWindow() + +} + +run_sdl_backend :: proc() { + r := sdl_renderer.init( + context.allocator, + "Game Menu", + { + .VERTEX = "../renderer/sdl_gpu/compiled/main.vert.sprv", + .FRAGMENT = "../renderer/sdl_gpu/compiled/main.frag.sprv", + }, + ) + + defer sdl_renderer.destroy(&r) + + f := sdl_renderer.add_font(&r, "./assets/Lora.ttf", 16) + + sdl_renderer.default_font = f + r.clear_color = sdl_renderer.color_syl_to_sdl(BACKGROUND_COLOR) + + syl.create_context(sdl_renderer.measure_text) + + ui := game_menu_ui() + + defer syl.destroy_context() + + run: for { + + event := sdl.Event{} + + for sdl.PollEvent(&event) { + #partial switch event.type { + case .QUIT: + break run + case .KEY_DOWN: + if event.key.scancode == .ESCAPE { + break run + } + } + } + + x, y: f32 + _ = sdl.GetMouseState(&x, &y) + + syl.input_mouse_move({x, y}) + + syl.update(ui) + syl.clear_context() + + sdl_renderer.begin(&r) + sdl_renderer.feed_validate(&r, sdl_renderer.feed_renderer(&r, ui, 0, nil)) + sdl_renderer.render(&r) + } +} diff --git a/layout_box.odin b/layout_box.odin index a6cc6f0..ed61f28 100644 --- a/layout_box.odin +++ b/layout_box.odin @@ -547,4 +547,4 @@ layout_box_apply_style:: proc(box: ^Layout_Box, new: Box_Style_Override, use_tra if bc, ok := new.border_color.?; ok && !(.Border_Color in box.overrides) { box.border_color = bc } -} \ No newline at end of file +} diff --git a/renderer/raylib/raylib.odin b/renderer/raylib/raylib.odin index 89156b0..3b6f6f2 100644 --- a/renderer/raylib/raylib.odin +++ b/renderer/raylib/raylib.odin @@ -3,7 +3,6 @@ package raylib_renderer import rl "vendor:raylib" import syl "../.." import "core:strings" -import "core:fmt" mouse_buttons_map := [syl.Mouse]rl.MouseButton{ .LEFT = .LEFT, @@ -37,7 +36,7 @@ deinit :: proc() { init_layout_box_shader :: proc() { // Load shaders from renderer/raylib relative to the process working directory // Use the alternative fragment shader `alt.fs` (converted for Raylib). - shader := rl.LoadShader("vertex.vs", "box_sdf.fs") + shader := rl.LoadShader("assets/vertex.vs", "assets/box_sdf.fs") box_shader = Box_Shader{ shader = shader, loc_position = rl.GetShaderLocation(shader, "position"), diff --git a/renderer/sdl_gpu/compile_shader.sh b/renderer/sdl_gpu/compile_shader.sh new file mode 100644 index 0000000..88878a2 --- /dev/null +++ b/renderer/sdl_gpu/compile_shader.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +SRC_DIR="$SCRIPT_DIR/source" +OUT_DIR="$SCRIPT_DIR/compiled" + +for file in "$SRC_DIR"/*; do + if [[ -f "$file" ]]; then + filename=$(basename -- "$file") + extension="${filename##*.}" + name="${filename%.*}" + glslc "$file" -o "$OUT_DIR/$name.$extension.sprv" + echo "Compiled $filename -> $name.$extension.sprv" + fi +done diff --git a/renderer/sdl_gpu/compiled/main.frag.sprv b/renderer/sdl_gpu/compiled/main.frag.sprv new file mode 100644 index 0000000..410eef5 Binary files /dev/null and b/renderer/sdl_gpu/compiled/main.frag.sprv differ diff --git a/renderer/sdl_gpu/compiled/main.vert.sprv b/renderer/sdl_gpu/compiled/main.vert.sprv new file mode 100644 index 0000000..95739ee Binary files /dev/null and b/renderer/sdl_gpu/compiled/main.vert.sprv differ diff --git a/renderer/sdl_gpu/main.odin b/renderer/sdl_gpu/main.odin new file mode 100644 index 0000000..fb9f03e --- /dev/null +++ b/renderer/sdl_gpu/main.odin @@ -0,0 +1,482 @@ +package sdl_renderer + +import "base:runtime" +import "core:c" +import "core:fmt" +import "core:mem" +import "core:os/os2" +import sdl "vendor:sdl3" +import ttf "vendor:sdl3/ttf" + +import syl "../.." + +Rect :: struct #packed { + position_and_size: [4]f32, + color: [4]f32, + f1: [4]f32, // uv_x for text, border radius for rects + f2: [4]f32, // uv_y for text, border thickness for rects + border_color: [4][4]f32, + flags: [4]i32, +} + +Command_Batch :: struct { + start, end: int, + font_texture: ^sdl.GPUTexture, +} + +default_font: ^ttf.Font = nil + +Renderer :: struct { + allocator: runtime.Allocator, + gpu: ^sdl.GPUDevice, + window: ^sdl.Window, + pipeline: ^sdl.GPUGraphicsPipeline, + font_engine: ^ttf.TextEngine, + font_sampler: ^sdl.GPUSampler, + dummy_texture: ^sdl.GPUTexture, + fonts: [dynamic]^ttf.Font, + rects: [dynamic]Rect, + batch: [dynamic]Command_Batch, + rect_gpu_buf: Gpu_Dynamic_Buffer, + clear_color: [4]f32, +} + +Gpu_Dynamic_Buffer :: struct { + data: ^sdl.GPUBuffer, + tansfer: ^sdl.GPUTransferBuffer, + byte_size: int, + prev_byte_size: int, +} + +init :: proc( + allocator: runtime.Allocator, + window_title: cstring, + shader_paths: [sdl.GPUShaderStage]string, + window_size: [2]c.int = {800, 640}, + window_flags: sdl.WindowFlags = {.RESIZABLE}, + shader_format: sdl.GPUShaderFormat = {.SPIRV}, + shader_debug: bool = false, +) -> Renderer { + + r := Renderer{} + + ok := sdl.Init({.VIDEO}); assert(ok) + + r.window = sdl.CreateWindow(window_title, window_size.x, window_size.y, window_flags) + r.gpu = sdl.CreateGPUDevice(shader_format, shader_debug, nil) + + assert(r.window != nil) + assert(r.gpu != nil) + + ok = sdl.ClaimWindowForGPUDevice(r.gpu, r.window); assert(ok) + + vert_shader := load_shader(r.gpu, shader_paths[.VERTEX], .VERTEX, shader_format, 1, 0, 1) + frag_shader := load_shader(r.gpu, shader_paths[.FRAGMENT], .FRAGMENT, shader_format, 0, 1, 0) + + r.pipeline = sdl.CreateGPUGraphicsPipeline( + r.gpu, + { + fragment_shader = frag_shader, + vertex_shader = vert_shader, + target_info = { + color_target_descriptions = &sdl.GPUColorTargetDescription { + blend_state = { + src_alpha_blendfactor = .ONE, + dst_alpha_blendfactor = .ONE_MINUS_SRC_ALPHA, + alpha_blend_op = .ADD, + src_color_blendfactor = .SRC_ALPHA, + dst_color_blendfactor = .ONE_MINUS_SRC_ALPHA, + color_blend_op = .ADD, + enable_blend = true, + enable_color_write_mask = true, + color_write_mask = ~{}, + }, + format = sdl.GetGPUSwapchainTextureFormat(r.gpu, r.window), + }, + num_color_targets = 1, + }, + primitive_type = .TRIANGLELIST, + }, + ) + assert(r.pipeline != nil) + + sdl.ReleaseGPUShader(r.gpu, vert_shader) + sdl.ReleaseGPUShader(r.gpu, frag_shader) + + r.rect_gpu_buf = init_gpu_dynamic_buffer(&r) + + ok = ttf.Init(); assert(ok) + + r.font_engine = ttf.CreateGPUTextEngine(r.gpu) + assert(r.font_engine != nil) + + r.font_sampler = sdl.CreateGPUSampler( + r.gpu, + { + address_mode_u = .REPEAT, + address_mode_v = .REPEAT, + address_mode_w = .REPEAT, + mag_filter = .LINEAR, + min_filter = .LINEAR, + mipmap_mode = .LINEAR, + }, + ) + assert(r.font_sampler != nil) + + r.dummy_texture = sdl.CreateGPUTexture( + r.gpu, + { + height = 1, + width = 1, + format = .R8G8B8A8_UNORM, + usage = {.SAMPLER}, + layer_count_or_depth = 1, + num_levels = 1, + }, + ) + assert(r.dummy_texture != nil) + + r.allocator = allocator + + r.batch = make([dynamic]Command_Batch, r.allocator) + r.fonts = make([dynamic]^ttf.Font, r.allocator) + r.rects = make([dynamic]Rect, r.allocator) + + return r +} + +add_font :: proc(renderer: ^Renderer, path: cstring, size: f32) -> ^ttf.Font { + font := ttf.OpenFont(path, size) + assert(font != nil) + append(&renderer.fonts, font) + return font +} + +init_gpu_dynamic_buffer :: proc(renderer: ^Renderer) -> Gpu_Dynamic_Buffer { + data_buffer := sdl.CreateGPUBuffer(renderer.gpu, {size = 64, usage = {.GRAPHICS_STORAGE_READ}}) + transfer_buffer := sdl.CreateGPUTransferBuffer(renderer.gpu, {size = 64, usage = .UPLOAD}) + + assert(data_buffer != nil) + assert(transfer_buffer != nil) + + dyn_buf: Gpu_Dynamic_Buffer + dyn_buf.byte_size = 64 // can't initialize a buffer with zero size, so reserve 64 bytes in advance + dyn_buf.prev_byte_size = 64 + dyn_buf.data = data_buffer + dyn_buf.tansfer = transfer_buffer + + return dyn_buf +} + +destroy :: proc(renderer: ^Renderer) { + + destroy_gpu_dynamic_buffer(renderer, &renderer.rect_gpu_buf) + + sdl.ReleaseGPUGraphicsPipeline(renderer.gpu, renderer.pipeline) + + sdl.ReleaseGPUTexture(renderer.gpu, renderer.dummy_texture) + sdl.ReleaseGPUSampler(renderer.gpu, renderer.font_sampler) + + for f in renderer.fonts { + ttf.CloseFont(f) + } + + ttf.DestroyGPUTextEngine(renderer.font_engine) + + sdl.ReleaseWindowFromGPUDevice(renderer.gpu, renderer.window) + + sdl.DestroyWindow(renderer.window) + sdl.DestroyGPUDevice(renderer.gpu) + + delete(renderer.batch) + delete(renderer.rects) + delete(renderer.fonts) +} + +destroy_gpu_dynamic_buffer :: proc(renderer: ^Renderer, buffer: ^Gpu_Dynamic_Buffer) { + sdl.ReleaseGPUBuffer(renderer.gpu, buffer.data) + sdl.ReleaseGPUTransferBuffer(renderer.gpu, buffer.tansfer) +} + +update_dynamic_buffer :: proc( + renderer: ^Renderer, + buf: ^Gpu_Dynamic_Buffer, + command_buffer: ^sdl.GPUCommandBuffer, + data: rawptr, +) { + if buf.byte_size > buf.prev_byte_size { + sdl.ReleaseGPUBuffer(renderer.gpu, buf.data) + buf.data = sdl.CreateGPUBuffer( + renderer.gpu, + {size = u32(buf.byte_size), usage = {.GRAPHICS_STORAGE_READ}}, + ) + + sdl.ReleaseGPUTransferBuffer(renderer.gpu, buf.tansfer) + buf.tansfer = sdl.CreateGPUTransferBuffer( + renderer.gpu, + {size = u32(buf.byte_size), usage = .UPLOAD}, + ) + } + + if buf.byte_size > 0 { + tmem := sdl.MapGPUTransferBuffer(renderer.gpu, buf.tansfer, false) + mem.copy(tmem, data, buf.byte_size) + sdl.UnmapGPUTransferBuffer(renderer.gpu, buf.tansfer) + + copy_pass := sdl.BeginGPUCopyPass(command_buffer) + sdl.UploadToGPUBuffer( + copy_pass, + {transfer_buffer = buf.tansfer}, + {size = u32(buf.byte_size), buffer = buf.data}, + false, + ) + sdl.EndGPUCopyPass(copy_pass) + } +} + +begin :: proc(renderer: ^Renderer) { + clear(&renderer.rects) + clear(&renderer.batch) +} + +feed_renderer :: proc( + renderer: ^Renderer, + elem: ^syl.Element, + start: int, + bound_texture: ^sdl.GPUTexture, +) -> ( + int, + ^sdl.GPUTexture, +) { + start := start + bound_texture := bound_texture + + switch elem.type { + case .Box, .Button: + box := cast(^syl.Box)elem + + r := Rect{} + r.position_and_size.xy = box.global_position + r.position_and_size.zw = box.size + r.f1 = box.border_radius + r.f2 = box.border_thickness + r.color = color_syl_to_sdl(box.background_color) + r.flags = 0 + r.border_color = { + color_syl_to_sdl(box.border_color.x), + color_syl_to_sdl(box.border_color.y), + color_syl_to_sdl(box.border_color.z), + color_syl_to_sdl(box.border_color.w), + } + append(&renderer.rects, r) + + case .Text: + text := cast(^syl.Text)elem + font := cast(^ttf.Font)text.font + + if font == nil { + font = default_font + if font == nil { + break + } + } + + for line in text.lines { + ttf.SetFontSize(font, f32(text.font_size)) + font_text := ttf.CreateText( + renderer.font_engine, + font, + cast(cstring)raw_data(line.content), + len(line.content), + ) + defer ttf.DestroyText(font_text) + draw_data := ttf.GetGPUTextDrawData(font_text) + + for seq := draw_data; seq != nil; seq = seq.next { + for idx: i32 = 0; idx < seq.num_indices; idx += 6 { + i0 := seq.indices[idx + 0] + i1 := seq.indices[idx + 1] + i2 := seq.indices[idx + 2] + i3 := seq.indices[idx + 5] + + v0 := seq.xy[i0] + v1 := seq.xy[i1] + v2 := seq.xy[i2] + v3 := seq.xy[i3] + + uv0 := seq.uv[i0] + uv1 := seq.uv[i1] + uv2 := seq.uv[i2] + uv3 := seq.uv[i3] + + x_min := min(min(v0.x, v1.x), min(v2.x, v3.x)) + y_min := min(min(v0.y, v1.y), min(v2.y, v3.y)) + x_max := max(max(v0.x, v1.x), max(v2.x, v3.x)) + y_max := max(max(v0.y, v1.y), max(v2.y, v3.y)) + + width := x_max - x_min + height := y_max - y_min + + r := Rect{} + r.position_and_size.xy = line.global_position + {x_min, 0} + r.position_and_size.zw = {width, height} + r.f1 = {uv0.x, uv1.x, uv2.x, uv3.x} + r.f2 = {uv2.y, uv3.y, uv0.y, uv1.y} + r.color = color_syl_to_sdl(text.color) + r.flags = {1, 0, 0, 0} + + index := len(renderer.rects) + append(&renderer.rects, r) + + if bound_texture == nil { + bound_texture = draw_data.atlas_texture + } + + if draw_data.next != nil && draw_data.next.atlas_texture != bound_texture { + append( + &renderer.batch, + Command_Batch { + start = start, + end = index, + font_texture = bound_texture, + }, + ) + bound_texture = draw_data.atlas_texture + start = index + } + } + } + } + } + + for child in elem.children { + start, bound_texture = feed_renderer(renderer, child, start, bound_texture) + } + + return start, bound_texture +} + +feed_validate :: proc(renderer: ^Renderer, start: int, bound_texture: ^sdl.GPUTexture) { + if len(renderer.batch) == 0 || + renderer.batch[len(renderer.batch) - 1].end < len(renderer.rects) { + append( + &renderer.batch, + Command_Batch{start = start, end = len(renderer.rects), font_texture = bound_texture}, + ) + } +} + +render :: proc(renderer: ^Renderer) { + window_w, window_h: i32 + sdl.GetWindowSize(renderer.window, &window_w, &window_h) + + cmd_buf := sdl.AcquireGPUCommandBuffer(renderer.gpu) + + renderer.rect_gpu_buf.prev_byte_size = renderer.rect_gpu_buf.byte_size + renderer.rect_gpu_buf.byte_size = len(renderer.rects) * size_of(Rect) + update_dynamic_buffer(renderer, &renderer.rect_gpu_buf, cmd_buf, raw_data(renderer.rects)) + + swapchain_tex := &sdl.GPUTexture{} + ok := sdl.WaitAndAcquireGPUSwapchainTexture( + cmd_buf, + renderer.window, + &swapchain_tex, + nil, + nil, + ); assert(ok) + + swapchain_target := sdl.GPUColorTargetInfo { + clear_color = cast(sdl.FColor)renderer.clear_color, + load_op = .CLEAR, + store_op = .STORE, + texture = swapchain_tex, + } + + storage_bufs := []^sdl.GPUBuffer{renderer.rect_gpu_buf.data} + + projection_mat := matrix[4, 4]f32{ + 2.0 / f32(window_w), 0, 0, -1, + 0, -2.0 / f32(window_h), 0, 1, + 0, 0, 1, 0, + 0, 0, 0, 1, + } + + render_pass := sdl.BeginGPURenderPass(cmd_buf, &swapchain_target, 1, nil) + + sdl.BindGPUGraphicsPipeline(render_pass, renderer.pipeline) + sdl.PushGPUVertexUniformData(cmd_buf, 0, &projection_mat, u32(size_of(projection_mat))) + sdl.BindGPUVertexStorageBuffers(render_pass, 0, raw_data(storage_bufs), u32(len(storage_bufs))) + sdl.BindGPUFragmentSamplers( + render_pass, + 0, + &sdl.GPUTextureSamplerBinding { + sampler = renderer.font_sampler, + texture = renderer.dummy_texture, + }, + 1, + ) + + for b in renderer.batch { + if b.font_texture != nil { + sdl.BindGPUFragmentSamplers( + render_pass, + 0, + &sdl.GPUTextureSamplerBinding { + sampler = renderer.font_sampler, + texture = b.font_texture, + }, + 1, + ) + } + sdl.DrawGPUPrimitives(render_pass, 6, u32(b.end - b.start), 0, u32(b.start)) + } + + sdl.EndGPURenderPass(render_pass) + + ok = sdl.SubmitGPUCommandBuffer(cmd_buf); assert(ok) +} + +load_shader :: proc( + gpu: ^sdl.GPUDevice, + path: string, + stage: sdl.GPUShaderStage, + format: sdl.GPUShaderFormat, + num_ubo, num_samplers, num_storage_buffers: u32, +) -> ^sdl.GPUShader { + source, read_err := os2.read_entire_file_from_path(path, context.allocator) + defer delete(source, context.allocator) + assert(read_err == nil) + shader := sdl.CreateGPUShader( + gpu, + { + code_size = len(source), + entrypoint = "main", + code = raw_data(source), + stage = stage, + format = format, + num_uniform_buffers = num_ubo, + num_samplers = num_samplers, + num_storage_buffers = num_storage_buffers, + }, + ) + assert(shader != nil) + return shader +} + +measure_text :: proc(s: string, font: rawptr, size: int, spacing: f32) -> int { + font_ := cast(^ttf.Font)(font) + + if font == nil { + font_ = default_font + } + + ttf.SetFontSize(font_, f32(size)) + w, h: i32 + ttf.GetStringSize(font_, cast(cstring)raw_data(s), len(s), &w, &h) + + return int(w) +} + +color_syl_to_sdl :: proc(c: [4]u8) -> [4]f32 { + return {f32(c.r), f32(c.g), f32(c.b), f32(c.a)} / 255.0 +} diff --git a/renderer/sdl_gpu/source/main.frag b/renderer/sdl_gpu/source/main.frag new file mode 100644 index 0000000..4020cab --- /dev/null +++ b/renderer/sdl_gpu/source/main.frag @@ -0,0 +1,90 @@ +#version 460 + +layout(location = 0) in vec4 in_color; +layout(location = 1) in vec4 in_radius; +layout(location = 2) in vec4 in_border_thickness; +layout(location = 3) in vec2 in_size; +layout(location = 4) in vec2 in_uv; +layout(location = 5) in vec4 in_border_color[4]; +layout(location = 9) flat in ivec4 in_flags; + +layout(location = 0) out vec4 out_color; + +layout(set = 2, binding = 0) uniform sampler2D font_sampler; + +float rect_select_side(vec2 pos, vec4 s) { + s.xy = pos.x < 0 ? s.xw : s.yz; + s.x = pos.y > 0 ? s.x : s.y; + return s.x; +} + +vec4 rect_get_blended_border_color(vec2 p, vec2 half_size) { + float d_left = abs(p.x + half_size.x); + float d_right = abs(p.x - half_size.x); + float d_top = abs(p.y + half_size.y); + float d_bottom = abs(p.y - half_size.y); + + float wl = 1.0 / (d_left + 1e-9); + float wr = 1.0 / (d_right + 1e-9); + float wt = 1.0 / (d_top + 1e-9); + float wb = 1.0 / (d_bottom + 1e-9); + + vec4 color = (in_border_color[3] * wl + in_border_color[0] * wt + + in_border_color[1] * wr + in_border_color[2] * wb) / + (wl + wt + wr + wb); + + return color; +} + +float sdf_rect(vec2 pos, vec2 half_size, float radius) { + vec2 q = abs(pos) - half_size + radius; + return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - + radius; // NEGATIVE inside, POSITIVE outside +} + +float sdf_inner_rect(vec2 pos, vec2 half_size, vec4 thickness, + float rad_outer) { + vec2 inner_half = + half_size - + vec2(thickness.w + thickness.y, thickness.x + thickness.z) * 0.5; + + vec2 offset = + vec2(thickness.w - thickness.y, thickness.x - thickness.z) * 0.5; + + vec2 s = sign(pos); + float t_corner = (s.x < 0.0) ? ((s.y < 0.0) ? max(thickness.w, thickness.x) + : max(thickness.w, thickness.z)) + : ((s.y < 0.0) ? max(thickness.y, thickness.x) + : max(thickness.y, thickness.z)); + + float rad_inner = max(0.0, rad_outer - t_corner); + + return sdf_rect(pos - offset, inner_half, rad_inner); +} + +void main() { + if (in_flags.x == 0) { + vec2 h_size = in_size * 0.5; + vec2 s_pos = in_uv * in_size - h_size; + + float rad = rect_select_side(s_pos, in_radius); + + vec4 border_color_blended = rect_get_blended_border_color(s_pos, h_size); + + float sdf_outer = sdf_rect(s_pos, h_size, rad); + float sdf_inner = sdf_inner_rect(s_pos, h_size, in_border_thickness, rad); + + float aa = 1.0; + float outer_mask = smoothstep(aa, -aa, sdf_outer); + float inner_mask = smoothstep(-aa, aa, sdf_inner); + + float fill = smoothstep(aa, -aa, sdf_inner); + + vec4 fill_color = in_color * fill; + vec4 border_color = border_color_blended * outer_mask * inner_mask; + + out_color = fill_color + border_color; + } else { + out_color = texture(font_sampler, in_uv) * in_color; + } +} diff --git a/renderer/sdl_gpu/source/main.vert b/renderer/sdl_gpu/source/main.vert new file mode 100644 index 0000000..72642ee --- /dev/null +++ b/renderer/sdl_gpu/source/main.vert @@ -0,0 +1,53 @@ +#version 460 + +struct Rect { + vec4 position_and_size; + vec4 color; + vec4 f1; + vec4 f2; + vec4 border_color[4]; + ivec4 flags; +}; + +layout(set = 0, binding = 0) readonly buffer Rects { Rect rects[]; }; +layout(set = 1, binding = 0) uniform mats { mat4 proj; }; + +layout(location = 0) out vec4 out_color; +layout(location = 1) out vec4 out_radius; +layout(location = 2) out vec4 out_thickness; +layout(location = 3) out vec2 out_size; +layout(location = 4) out vec2 out_uv; +layout(location = 5) out vec4 out_border_color[4]; +layout(location = 9) out ivec4 out_flags; + +const vec2 positions[6] = + vec2[](vec2(0.5, 0.5), vec2(-0.5, 0.5), vec2(-0.5, -0.5), vec2(0.5, 0.5), + vec2(-0.5, -0.5), vec2(0.5, -0.5)); + +const vec2 uvs[6] = vec2[](vec2(1.0, 0.0), vec2(0.0, 0.0), vec2(0.0, 1.0), + vec2(1.0, 0.0), vec2(0.0, 1.0), vec2(1.0, 1.0)); + +const int uv_map[6] = int[](1, 0, 3, 1, 3, 2); + +void main() { + vec2 vert_pos = positions[gl_VertexIndex]; + + Rect rect = rects[gl_InstanceIndex]; + vec2 size = rect.position_and_size.zw; + vec2 pos = rect.position_and_size.xy; + + out_color = rect.color; + out_radius = rect.f1; + out_thickness = rect.f2; + out_size = size; + out_border_color = rect.border_color; + out_flags = rect.flags; + + if (rect.flags.x == 1) { + out_uv = vec2(rect.f1[uv_map[gl_VertexIndex]], rect.f2[uv_map[gl_VertexIndex]]); + } else { + out_uv = uvs[gl_VertexIndex]; + } + + gl_Position = proj * vec4(vert_pos * size + pos + size / 2, 1.0, 1.0); +}