commit 16918451996fdb1e9718015490444282ba9a34d5 Author: shan Date: Sun Apr 27 19:51:48 2025 -0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0563865 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.bin +/renderer/res/shaders/compiled/ +/run.sh +/examples.bin.dSYM/ +/bin/ diff --git a/examples/main.odin b/examples/main.odin new file mode 100644 index 0000000..236a3a8 --- /dev/null +++ b/examples/main.odin @@ -0,0 +1,234 @@ +package main + +import "../renderer" +import "core:c" +import "core:fmt" +import "core:log" +import "core:mem" +import "core:os" +import clay "library:clay" +import sdl "vendor:sdl3" + +WINDOW_WIDTH :: 1024 +WINDOW_HEIGHT :: 728 +WINDOW_FLAGS :: sdl.WindowFlags{.RESIZABLE, .HIGH_PIXEL_DENSITY} + +window: ^sdl.Window +device: ^sdl.GPUDevice +debug_enabled := false + +body_text := clay.TextElementConfig { + fontId = renderer.JETBRAINS_MONO_REGULAR, + fontSize = 44, + textColor = { 1.0, 1.0, 1.0, 1.0 }, +} + +main :: proc() { + defer destroy() + + when ODIN_DEBUG == true { + context.logger = log.create_console_logger(lowest = .Debug) + + //----- Tracking allocator ---------------------------------- + // Temp + track_temp: mem.Tracking_Allocator + mem.tracking_allocator_init(&track_temp, context.temp_allocator) + context.temp_allocator = mem.tracking_allocator(&track_temp) + // Default + track: mem.Tracking_Allocator + mem.tracking_allocator_init(&track, context.allocator) + context.allocator = mem.tracking_allocator(&track) + // Log a warning about any memory that was not freed by the end of the program. + // This could be fine for some global state or it could be a memory leak. + defer { + // Temp allocator + if len(track_temp.allocation_map) > 0 { + fmt.eprintf( + "=== %v allocations not freed - temp allocator: ===\n", + len(track_temp.allocation_map), + ) + for _, entry in track_temp.allocation_map { + fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location) + } + } + if len(track_temp.bad_free_array) > 0 { + fmt.eprintf( + "=== %v incorrect frees - temp allocator: ===\n", + len(track_temp.bad_free_array), + ) + for entry in track_temp.bad_free_array { + fmt.eprintf("- %p @ %v\n", entry.memory, entry.location) + } + } + mem.tracking_allocator_destroy(&track_temp) + // Default allocator + if len(track.allocation_map) > 0 { + fmt.eprintf( + "=== %v allocations not freed - main allocator: ===\n", + len(track.allocation_map), + ) + for _, entry in track.allocation_map { + fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location) + } + } + if len(track.bad_free_array) > 0 { + fmt.eprintf( + "=== %v incorrect frees - main allocator: ===\n", + len(track.bad_free_array), + ) + for entry in track.bad_free_array { + fmt.eprintf("- %p @ %v\n", entry.memory, entry.location) + } + } + mem.tracking_allocator_destroy(&track) + } + } + + if !sdl.Init(sdl.InitFlags{.VIDEO}) { + log.error("Failed to initialize SDL:", sdl.GetError()) + } + + window = sdl.CreateWindow("System Controller", WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_FLAGS) + + if window == nil { + log.error("Failed to create window:", sdl.GetError()) + os.exit(1) + } + + device = sdl.CreateGPUDevice(renderer.SHADER_TYPE, true, nil) + if device == nil { + log.error("Failed to create GPU device:", sdl.GetError()) + os.exit(1) + } + driver := sdl.GetGPUDeviceDriver(device) + log.info("Created GPU device:", driver) + + if !sdl.ClaimWindowForGPUDevice(device, window) { + log.error("Failed to claim GPU device for window:", sdl.GetError()) + os.exit(1) + } + + renderer.init(device, window, WINDOW_WIDTH, WINDOW_HEIGHT, context) + + // debug + FPS_REFRESH_INTERVAL :: 1000.0 // 1 second + fps_time := sdl.GetTicks() + frame_count: int + fps: f32 + + last_frame_time := sdl.GetTicks() + + program: for { + defer free_all(context.temp_allocator) + + // Update debug FPS + frame_time := sdl.GetTicks() + when ODIN_DEBUG == true { + frame_count += 1 + if frame_time - fps_time >= FPS_REFRESH_INTERVAL { + new_fps := f32(frame_count) + if new_fps != fps { + log.info("FPS:", new_fps) + } + fps = new_fps + frame_count = 0 + fps_time = frame_time + } + } + + cmd_buffer := sdl.AcquireGPUCommandBuffer(device) + if cmd_buffer == nil { + log.error("Failed to acquire command buffer") + os.exit(1) + } + + if update(cmd_buffer, frame_time - last_frame_time) { + log.debug("User command to quit") + break program + } + + draw(cmd_buffer) + + last_frame_time = frame_time + } +} + +destroy :: proc() { + renderer.destroy(device) + sdl.ReleaseWindowFromGPUDevice(device, window) + sdl.DestroyWindow(window) + sdl.DestroyGPUDevice(device) +} + +update :: proc(cmd_buffer: ^sdl.GPUCommandBuffer, delta_time: u64) -> bool { + frame_time := f32(delta_time) / 1000.0 + input := input() + + render_cmds: clay.ClayArray(clay.RenderCommand) = layout() + + renderer.prepare(device, window, cmd_buffer, &render_cmds, input.mouse_delta, frame_time) + + return input.should_quit +} + +Input :: struct { + mouse_delta: [2]f32, + should_quit: bool, +} + +input :: proc() -> Input { + result := Input{} + + event: sdl.Event + for sdl.PollEvent(&event) == true { + #partial switch event.type { + case .KEY_DOWN: + switch event.key.key { + case sdl.K_ESCAPE: + result.should_quit = true + case sdl.K_D: + if .LSHIFT in event.key.mod { + debug_enabled = !debug_enabled + clay.SetDebugModeEnabled(debug_enabled) + } + } + case .QUIT: + result.should_quit = true + case .MOUSE_WHEEL: + result.mouse_delta[0] = event.wheel.x + result.mouse_delta[1] = event.wheel.y + } + } + + return result +} + +draw :: proc(cmd_buffer: ^sdl.GPUCommandBuffer) { + renderer.draw(device, window, cmd_buffer) + submit_ok := sdl.SubmitGPUCommandBuffer(cmd_buffer) + if !submit_ok { + log.debug("Failed to submit command buffer:", sdl.GetError()) + } +} + +layout :: proc() -> clay.ClayArray(clay.RenderCommand) { + clay.BeginLayout() + + if clay.UI()( + { + id = clay.ID("OuterContainer"), + layout = { + layoutDirection = .TopToBottom, + sizing = {clay.SizingGrow({}), clay.SizingGrow({})}, + childAlignment = {x = .Center, y = .Center}, + childGap = 16, + }, + backgroundColor = {0.2, 0.2, 0.2, 1.0}, + }, + ) { + clay.Text("3D SCENE", &body_text) + } + + return clay.EndLayout() +} + diff --git a/library/clay/clay.odin b/library/clay/clay.odin new file mode 100644 index 0000000..9d7a73a --- /dev/null +++ b/library/clay/clay.odin @@ -0,0 +1,470 @@ +package clay + +import "core:c" + +when ODIN_OS == .Windows { + foreign import Clay "windows/clay.lib" +} else when ODIN_OS == .Linux { + foreign import Clay "linux/clay.a" +} else when ODIN_OS == .Darwin { + when ODIN_ARCH == .arm64 { + foreign import Clay "macos-arm64/clay.a" + } else { + foreign import Clay "macos/clay.a" + } +} else when ODIN_ARCH == .wasm32 || ODIN_ARCH == .wasm64p32 { + foreign import Clay "wasm/clay.o" +} + +String :: struct { + isStaticallyAllocated: c.bool, + length: c.int32_t, + chars: [^]c.char, +} + +StringSlice :: struct { + length: c.int32_t, + chars: [^]c.char, + baseChars: [^]c.char, +} + +Vector2 :: [2]c.float + +Dimensions :: struct { + width: c.float, + height: c.float, +} + +Arena :: struct { + nextAllocation: uintptr, + capacity: c.size_t, + memory: [^]c.char, +} + +BoundingBox :: struct { + x: c.float, + y: c.float, + width: c.float, + height: c.float, +} + +Color :: [4]c.float + +CornerRadius :: struct { + topLeft: c.float, + topRight: c.float, + bottomLeft: c.float, + bottomRight: c.float, +} + +BorderData :: struct { + width: u32, + color: Color, +} + +ElementId :: struct { + id: u32, + offset: u32, + baseId: u32, + stringId: String, +} + +when ODIN_OS == .Windows { + EnumBackingType :: u32 +} else { + EnumBackingType :: u8 +} + +RenderCommandType :: enum EnumBackingType { + None, + Rectangle, + Border, + Text, + Image, + ScissorStart, + ScissorEnd, + Custom, +} + +RectangleElementConfig :: struct { + color: Color, +} + +TextWrapMode :: enum EnumBackingType { + Words, + Newlines, + None, +} + +TextAlignment :: enum EnumBackingType { + Left, + Center, + Right, +} + +TextElementConfig :: struct { + userData: rawptr, + textColor: Color, + fontId: u16, + fontSize: u16, + letterSpacing: u16, + lineHeight: u16, + wrapMode: TextWrapMode, + textAlignment: TextAlignment, +} + +ImageElementConfig :: struct { + imageData: rawptr, + sourceDimensions: Dimensions, +} + +CustomElementConfig :: struct { + customData: rawptr, +} + +BorderWidth :: struct { + left: u16, + right: u16, + top: u16, + bottom: u16, + betweenChildren: u16, +} + +BorderElementConfig :: struct { + color: Color, + width: BorderWidth, +} + +ScrollElementConfig :: struct { + horizontal: bool, + vertical: bool, +} + +FloatingAttachPointType :: enum EnumBackingType { + LeftTop, + LeftCenter, + LeftBottom, + CenterTop, + CenterCenter, + CenterBottom, + RightTop, + RightCenter, + RightBottom, +} + +FloatingAttachPoints :: struct { + element: FloatingAttachPointType, + parent: FloatingAttachPointType, +} + +PointerCaptureMode :: enum EnumBackingType { + Capture, + Passthrough, +} + +FloatingAttachToElement :: enum EnumBackingType { + None, + Parent, + ElementWithId, + Root, +} + +FloatingElementConfig :: struct { + offset: Vector2, + expand: Dimensions, + parentId: u32, + zIndex: i16, + attachment: FloatingAttachPoints, + pointerCaptureMode: PointerCaptureMode, + attachTo: FloatingAttachToElement, +} + +TextRenderData :: struct { + stringContents: StringSlice, + textColor: Color, + fontId: u16, + fontSize: u16, + letterSpacing: u16, + lineHeight: u16, +} + +RectangleRenderData :: struct { + backgroundColor: Color, + cornerRadius: CornerRadius, +} + +ImageRenderData :: struct { + backgroundColor: Color, + cornerRadius: CornerRadius, + sourceDimensions: Dimensions, + imageData: rawptr, +} + +CustomRenderData :: struct { + backgroundColor: Color, + cornerRadius: CornerRadius, + customData: rawptr, +} + +BorderRenderData :: struct { + color: Color, + cornerRadius: CornerRadius, + width: BorderWidth, +} + +RenderCommandData :: struct #raw_union { + rectangle: RectangleRenderData, + text: TextRenderData, + image: ImageRenderData, + custom: CustomRenderData, + border: BorderRenderData, +} + +RenderCommand :: struct { + boundingBox: BoundingBox, + renderData: RenderCommandData, + userData: rawptr, + id: u32, + zIndex: i16, + commandType: RenderCommandType, +} + +ScrollContainerData :: struct { + // Note: This is a pointer to the real internal scroll position, mutating it may cause a change in final layout. + // Intended for use with external functionality that modifies scroll position, such as scroll bars or auto scrolling. + scrollPosition: ^Vector2, + scrollContainerDimensions: Dimensions, + contentDimensions: Dimensions, + config: ScrollElementConfig, + // Indicates whether an actual scroll container matched the provided ID or if the default struct was returned. + found: bool, +} + +ElementData :: struct { + boundingBox: BoundingBox, + found: bool, +} + +PointerDataInteractionState :: enum EnumBackingType { + PressedThisFrame, + Pressed, + ReleasedThisFrame, + Released, +} + +PointerData :: struct { + position: Vector2, + state: PointerDataInteractionState, +} + +SizingType :: enum EnumBackingType { + Fit, + Grow, + Percent, + Fixed, +} + +SizingConstraintsMinMax :: struct { + min: c.float, + max: c.float, +} + +SizingConstraints :: struct #raw_union { + sizeMinMax: SizingConstraintsMinMax, + sizePercent: c.float, +} + +SizingAxis :: struct { + // Note: `min` is used for CLAY_SIZING_PERCENT, slightly different to clay.h due to lack of C anonymous unions + constraints: SizingConstraints, + type: SizingType, +} + +Sizing :: struct { + width: SizingAxis, + height: SizingAxis, +} + +Padding :: struct { + left: u16, + right: u16, + top: u16, + bottom: u16, +} + +LayoutDirection :: enum EnumBackingType { + LeftToRight, + TopToBottom, +} + +LayoutAlignmentX :: enum EnumBackingType { + Left, + Right, + Center, +} + +LayoutAlignmentY :: enum EnumBackingType { + Top, + Bottom, + Center, +} + +ChildAlignment :: struct { + x: LayoutAlignmentX, + y: LayoutAlignmentY, +} + +LayoutConfig :: struct { + sizing: Sizing, + padding: Padding, + childGap: u16, + childAlignment: ChildAlignment, + layoutDirection: LayoutDirection, +} + +ClayArray :: struct($type: typeid) { + capacity: i32, + length: i32, + internalArray: [^]type, +} + +ElementDeclaration :: struct { + id: ElementId, + layout: LayoutConfig, + backgroundColor: Color, + cornerRadius: CornerRadius, + image: ImageElementConfig, + floating: FloatingElementConfig, + custom: CustomElementConfig, + scroll: ScrollElementConfig, + border: BorderElementConfig, + userData: rawptr, +} + +ErrorType :: enum EnumBackingType { + TextMeasurementFunctionNotProvided, + ArenaCapacityExceeded, + ElementsCapacityExceeded, + TextMeasurementCapacityExceeded, + DuplicateId, + FloatingContainerParentNotFound, + PercentageOver1, + InternalError, +} + +ErrorData :: struct { + errorType: ErrorType, + errorText: String, + userData: rawptr, +} + +ErrorHandler :: struct { + handler: proc "c" (errorData: ErrorData), + userData: rawptr, +} + +Context :: struct {} // opaque structure, only use as a pointer + +@(link_prefix = "Clay_", default_calling_convention = "c") +foreign Clay { + _OpenElement :: proc() --- + _CloseElement :: proc() --- + MinMemorySize :: proc() -> u32 --- + CreateArenaWithCapacityAndMemory :: proc(capacity: c.size_t, offset: [^]u8) -> Arena --- + SetPointerState :: proc(position: Vector2, pointerDown: bool) --- + Initialize :: proc(arena: Arena, layoutDimensions: Dimensions, errorHandler: ErrorHandler) -> ^Context --- + GetCurrentContext :: proc() -> ^Context --- + SetCurrentContext :: proc(ctx: ^Context) --- + UpdateScrollContainers :: proc(enableDragScrolling: bool, scrollDelta: Vector2, deltaTime: c.float) --- + SetLayoutDimensions :: proc(dimensions: Dimensions) --- + BeginLayout :: proc() --- + EndLayout :: proc() -> ClayArray(RenderCommand) --- + GetElementId :: proc(id: String) -> ElementId --- + GetElementIdWithIndex :: proc(id: String, index: u32) -> ElementId --- + GetElementData :: proc(id: ElementId) -> ElementData --- + Hovered :: proc() -> bool --- + OnHover :: proc(onHoverFunction: proc "c" (id: ElementId, pointerData: PointerData, userData: rawptr), userData: rawptr) --- + PointerOver :: proc(id: ElementId) -> bool --- + GetScrollContainerData :: proc(id: ElementId) -> ScrollContainerData --- + SetMeasureTextFunction :: proc(measureTextFunction: proc "c" (text: StringSlice, config: ^TextElementConfig, userData: rawptr) -> Dimensions, userData: rawptr) --- + SetQueryScrollOffsetFunction :: proc(queryScrollOffsetFunction: proc "c" (elementId: u32, userData: rawptr) -> Vector2, userData: rawptr) --- + RenderCommandArray_Get :: proc(array: ^ClayArray(RenderCommand), index: i32) -> ^RenderCommand --- + SetDebugModeEnabled :: proc(enabled: bool) --- + IsDebugModeEnabled :: proc() -> bool --- + SetCullingEnabled :: proc(enabled: bool) --- + GetMaxElementCount :: proc() -> i32 --- + SetMaxElementCount :: proc(maxElementCount: i32) --- + GetMaxMeasureTextCacheWordCount :: proc() -> i32 --- + SetMaxMeasureTextCacheWordCount :: proc(maxMeasureTextCacheWordCount: i32) --- + ResetMeasureTextCache :: proc() --- +} + +@(link_prefix = "Clay_", default_calling_convention = "c", private) +foreign Clay { + _ConfigureOpenElement :: proc(config: ElementDeclaration) --- + _HashString :: proc(key: String, offset: u32, seed: u32) -> ElementId --- + _OpenTextElement :: proc(text: String, textConfig: ^TextElementConfig) --- + _StoreTextElementConfig :: proc(config: TextElementConfig) -> ^TextElementConfig --- + _GetParentElementId :: proc() -> u32 --- +} + +ConfigureOpenElement :: proc(config: ElementDeclaration) -> bool { + _ConfigureOpenElement(config) + return true +} + +@(deferred_none = _CloseElement) +UI :: proc() -> proc (config: ElementDeclaration) -> bool { + _OpenElement() + return ConfigureOpenElement +} + +Text :: proc($text: string, config: ^TextElementConfig) { + wrapped := MakeString(text) + wrapped.isStaticallyAllocated = true + _OpenTextElement(wrapped, config) +} + +TextDynamic :: proc(text: string, config: ^TextElementConfig) { + _OpenTextElement(MakeString(text), config) +} + +TextConfig :: proc(config: TextElementConfig) -> ^TextElementConfig { + return _StoreTextElementConfig(config) +} + +PaddingAll :: proc(allPadding: u16) -> Padding { + return { left = allPadding, right = allPadding, top = allPadding, bottom = allPadding } +} + +CornerRadiusAll :: proc(radius: f32) -> CornerRadius { + return CornerRadius{radius, radius, radius, radius} +} + +SizingFit :: proc(sizeMinMax: SizingConstraintsMinMax) -> SizingAxis { + return SizingAxis{type = SizingType.Fit, constraints = {sizeMinMax = sizeMinMax}} +} + +SizingGrow :: proc(sizeMinMax: SizingConstraintsMinMax) -> SizingAxis { + return SizingAxis{type = SizingType.Grow, constraints = {sizeMinMax = sizeMinMax}} +} + +SizingFixed :: proc(size: c.float) -> SizingAxis { + return SizingAxis{type = SizingType.Fixed, constraints = {sizeMinMax = {size, size}}} +} + +SizingPercent :: proc(sizePercent: c.float) -> SizingAxis { + return SizingAxis{type = SizingType.Percent, constraints = {sizePercent = sizePercent}} +} + +MakeString :: proc(label: string) -> String { + return String{chars = raw_data(label), length = cast(c.int)len(label)} +} + +ID :: proc(label: string, index: u32 = 0) -> ElementId { + return _HashString(MakeString(label), index, 0) +} + +ID_LOCAL :: proc(label: string, index: u32 = 0) -> ElementId { + return _HashString(MakeString(label), index, _GetParentElementId()) +} diff --git a/library/clay/linux-arm64/clay.a b/library/clay/linux-arm64/clay.a new file mode 100644 index 0000000..d40da7c Binary files /dev/null and b/library/clay/linux-arm64/clay.a differ diff --git a/library/clay/linux/clay.a b/library/clay/linux/clay.a new file mode 100644 index 0000000..8352290 Binary files /dev/null and b/library/clay/linux/clay.a differ diff --git a/library/clay/macos-arm64/clay.a b/library/clay/macos-arm64/clay.a new file mode 100644 index 0000000..8c91161 Binary files /dev/null and b/library/clay/macos-arm64/clay.a differ diff --git a/library/clay/macos/clay.a b/library/clay/macos/clay.a new file mode 100644 index 0000000..e56ff15 Binary files /dev/null and b/library/clay/macos/clay.a differ diff --git a/library/clay/wasm/clay.o b/library/clay/wasm/clay.o new file mode 100644 index 0000000..fc3cf41 Binary files /dev/null and b/library/clay/wasm/clay.o differ diff --git a/library/clay/windows/clay.lib b/library/clay/windows/clay.lib new file mode 100644 index 0000000..8bc2215 Binary files /dev/null and b/library/clay/windows/clay.lib differ diff --git a/library/sdl3_ttf/sdl3_ttf.odin b/library/sdl3_ttf/sdl3_ttf.odin new file mode 100644 index 0000000..71187fb --- /dev/null +++ b/library/sdl3_ttf/sdl3_ttf.odin @@ -0,0 +1,184 @@ +package sdl3_ttf + +import sdl "vendor:sdl3" +import "core:c" + +foreign import lib "system:SDL3_ttf" + +Font :: struct {} + +Text :: struct { + text: cstring, + num_lines: c.int, + refcount: c.int, + internal: rawptr, +} + +TextEngine :: struct {} + +Direction :: enum c.int { + LTR = 0, + RTL, + TTB, + BTT, +} + +// Normal == empty +FontStyleFlag :: enum u32 { + BOLD = 0, + ITALIC = 1, + UNDERLINE = 2, + STRIKETHROUGH = 3, +} + +FontStyleFlags :: bit_set[FontStyleFlag;u32] +FONT_STYLE_NORMAL :: FontStyleFlags{} +FONT_STYLE_BOLD :: FontStyleFlags{.BOLD} +FONT_STYLE_ITALIC :: FontStyleFlags{.ITALIC} +FONT_STYLE_UNDERLINE :: FontStyleFlags{.UNDERLINE} +FONT_STYLE_STRIKETHROUGH :: FontStyleFlags{.STRIKETHROUGH} + +HintingFlags :: enum c.int { + NORMAL = 0, + LIGHT, + MONO, + NONE, + LIGHT_SUBPIXEL, +} + +TTF_PROP_FONT_OUTLINE_LINE_CAP_NUMBER :: "SDL_ttf.font.outline.line_cap" +TTF_PROP_FONT_OUTLINE_LINE_JOIN_NUMBER :: "SDL_ttf.font.outline.line_join" +TTF_PROP_FONT_OUTLINE_MITER_LIMIT_NUMBER :: "SDL_ttf.font.outline.miter_limit" + +HorizontalAlignment :: enum c.int { + INVALID = -1, + LEFT, + CENTER, + RIGHT, +} + +GPUAtlasDrawSequence :: struct { + atlas_texture: ^sdl.GPUTexture, + vertex_positions: [^]sdl.FPoint, + uvs: [^]sdl.FPoint, // Normalized + num_verticies: c.int, + indices: [^]c.int, + num_indices: c.int, + next: ^GPUAtlasDrawSequence, // If nil, this is the last text in the sequence +} + +GPUTextEngineWinding :: enum c.int { + INVALID = -1, + CLOCKWISE, + COUNTERCLOCKWISE, +} + +SubStringFlag :: enum u32 { + TEXT_START, + LINE_START, + LINE_END, + TEXT_END, +} + +SubString :: struct { + flags: SubStringFlag, + offset: c.int, + length: c.int, + line_index: c.int, + cluster_index: c.int, + rect: sdl.Rect, +} + +/// General +@(default_calling_convention = "c", link_prefix = "TTF_") +foreign lib { + Init :: proc() -> bool --- + CreateGPUTextEngine :: proc(device: ^sdl.GPUDevice) -> ^TextEngine --- + DestroyGPUTextEngine :: proc(engine: ^TextEngine) --- + Quit :: proc() --- +} + +/// Fonts +@(default_calling_convention = "c", link_prefix = "TTF_") +foreign lib { + CloseFont :: proc(font: ^Font) --- + FontHasGlyph :: proc(font: ^Font, glyph: u32) -> bool --- + FontIsFixedWidth :: proc(font: ^Font) -> bool --- + GetFontAscent :: proc(font: ^Font) -> c.int --- + GetFontDescent :: proc(font: ^Font) -> c.int --- + GetFontDirection :: proc(font: ^Font) -> Direction --- + GetFontDPI :: proc(font: ^Font, hdpi: ^c.int, vdpi: ^c.int) -> bool --- + GetFontFamilyName :: proc(font: ^Font) -> cstring --- + GetFontGeneration :: proc(font: ^Font) -> u32 --- + GetFontHeight :: proc(font: ^Font) -> c.int --- + GetFontHinting :: proc(font: ^Font) -> HintingFlags --- + GetFontKerning :: proc(font: ^Font) -> bool --- + /// Returns the font's recommended spacing + GetFontLineSkip :: proc(font: ^Font) -> c.int --- + GetFontOutline :: proc(font: ^Font) -> c.int --- + GetFontProperties :: proc(font: ^Font) -> sdl.PropertiesID --- + GetFontSize :: proc(font: ^Font) -> f32 --- + GetFontStyle :: proc(font: ^Font) -> FontStyleFlags --- + GetFontStyleName :: proc(font: ^Font) -> cstring --- + GetFontWrapAlignment :: proc(font: ^Font) -> HorizontalAlignment --- + GetFreeTypeVersion :: proc(major: ^c.int, minor: ^c.int, patch: ^c.int) --- + GetGlyphMetrics :: proc(font: ^Font, glyph: u32, min_x: ^c.int, max_x: ^c.int, min_y: ^c.int, max_y: ^c.int, advance: ^c.int) -> bool --- + GetGlyphScript :: proc(glyph: u32, script: ^c.char, script_size: c.size_t) -> bool --- + /// `stream`: A `sdl.IOStream` to provide a font's file data + /// `close_io`: Close src when the font is closed, false to leave it open + /// `point_size`: Font point size to use for the newly-opened font + OpenFontIO :: proc(stream: ^sdl.IOStream, close_io: bool, point_size: f32) -> ^Font --- + OpenFont :: proc(file: cstring, point_size: f32) -> ^Font --- + SetFontDirection :: proc(font: ^Font, direction: Direction) -> bool --- + SetFontHinting :: proc(font: ^Font, hinting_flags: HintingFlags) --- + SetFontKerning :: proc(font: ^Font, enabled: bool) --- + SetFontLineSkip :: proc(font: ^Font, lineskip: c.int) --- + SetFontOutline :: proc(font: ^Font, outline: c.int) -> bool --- + SetFontScript :: proc(font: ^Font, script: cstring) -> bool --- + SetFontSize :: proc(font: ^Font, pt_size: f32) -> bool --- + SetFontSizeDPI :: proc(font: ^Font, pt_size: f32, hdpi: c.int, vdpi: c.int) -> bool --- + SetFontStyle :: proc(font: ^Font, style: FontStyleFlags) --- + SetFontWrapAlignment :: proc(font: ^Font, horizontal_alignment: HorizontalAlignment) --- + SetGPUTextEngineWinding :: proc(engine: ^TextEngine, winding: GPUTextEngineWinding) --- +} + +/// Text +@(default_calling_convention = "c", link_prefix = "TTF_") +foreign lib { + AppendTextString :: proc(text: ^Text, str: cstring, length: c.size_t) -> bool --- + CreateText :: proc(engine: ^TextEngine, font: ^Font, text: cstring, length: c.size_t) -> ^Text --- + DeleteTextString :: proc(text: ^Text, offset: c.int, length: c.int) -> bool --- + DestroyText :: proc(text: ^Text) --- + GetGPUTextDrawData :: proc(text: ^Text) -> ^GPUAtlasDrawSequence --- + GetGPUTextEngineWinding :: proc(engine: ^TextEngine) -> GPUTextEngineWinding --- + GetNextTextSubString :: proc(text: ^Text, substring: ^SubString, next: ^SubString) -> bool --- + GetPreviousTextSubString :: proc(text: ^Text, substring: ^SubString, previous: ^SubString) -> bool --- + /// Calculate the dimensions of a rendered string of UTF-8 text. + GetStringSize :: proc(font: ^Font, text: cstring, length: c.size_t, w: ^c.int, h: ^c.int) -> bool --- + GetStringSizeWrapped :: proc(font: ^Font, text: cstring, length: c.size_t, wrap_width: c.int, w: ^c.int, h: ^c.int) -> bool --- + GetTextColor :: proc(text: ^Text, r: ^u8, g: ^u8, b: ^u8, a: ^u8) -> bool --- + GetTextColorFloat :: proc(text: ^Text, r: ^f32, g: ^f32, b: ^f32, a: ^f32) -> bool --- + GetTextEngine :: proc(text: ^Text) -> ^TextEngine --- + GetTextFont :: proc(text: ^Text) -> ^Font --- + GetTextPosition :: proc(text: ^Text, x: ^c.int, y: ^c.int) -> bool --- + GetTextProperties :: proc(text: ^Text) -> sdl.PropertiesID --- + GetTextSize :: proc(text: ^Text, width: ^c.int, height: ^c.int) -> bool --- + GetTextSubString :: proc(text: ^Text, offset: c.int, substring: ^SubString) -> bool --- + GetTextSubStringForLine :: proc(text: ^Text, line: c.int, substring: ^SubString) -> bool --- + GetTextSubStringForPoint :: proc(text: ^Text, x: c.int, y: c.int, substring: ^SubString) -> bool --- + GetTextSubStringsForRange :: proc(text: ^Text, offset: c.int, length: c.int, count: ^c.int) -> [^]^SubString --- + GetTextWrapping :: proc(text: ^Text, wrap_length: ^c.int) -> bool --- + GetTextWrapWidth :: proc(text: ^Text, wrap_width: ^c.int) -> bool --- + InsertTextString :: proc(text: ^Text, offset: c.int, str: cstring, length: c.size_t) -> bool --- + // Calculate how much of a UTF-8 string will fit in a given width. + MeasureString :: proc(font: ^Font, text: cstring, length: c.size_t, max_width: c.int, measured_width: ^c.int, measured_length: ^c.size_t) -> bool --- + SetTextColor :: proc(text: ^Text, r: u8, g: u8, b: u8, a: u8) -> bool --- + SetTextColorFloat :: proc(text: ^Text, r: f32, g: f32, b: f32, a: f32) -> bool --- + SetTextEngine :: proc(text: ^Text, engine: ^TextEngine) -> bool --- + SetTextFont :: proc(text: ^Text, font: ^Font) -> bool --- + SetTextPosition :: proc(text: ^Text, x: c.int, y: c.int) -> bool --- + SetTextString :: proc(text: ^Text, str: cstring, length: c.size_t) -> bool --- + SetTextWrapping :: proc(text: ^Text, wrap_length: c.int) -> bool --- + SetTextWrapWhitespaceVisible :: proc(text: ^Text, visible: bool) -> bool --- + SetTextWrapWidth :: proc(text: ^Text, wrap_width: c.int) -> bool --- +} diff --git a/renderer/buffer.odin b/renderer/buffer.odin new file mode 100644 index 0000000..1841a43 --- /dev/null +++ b/renderer/buffer.odin @@ -0,0 +1,51 @@ +package renderer + +import "core:log" +import sdl "vendor:sdl3" + +Buffer :: struct { + gpu: ^sdl.GPUBuffer, + transfer: ^sdl.GPUTransferBuffer, + size: u32, +} + +create_buffer :: proc( + device: ^sdl.GPUDevice, + size: u32, + gpu_usage: sdl.GPUBufferUsageFlags, +) -> Buffer { + return Buffer { + gpu = sdl.CreateGPUBuffer(device, sdl.GPUBufferCreateInfo{usage = gpu_usage, size = size}), + transfer = sdl.CreateGPUTransferBuffer( + device, + sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = size}, + ), + size = size, + } +} + +resize_buffer :: proc( + device: ^sdl.GPUDevice, + buffer: ^Buffer, + new_size: u32, + gpu_usage: sdl.GPUBufferUsageFlags, +) { + if new_size > buffer.size { + log.debug("Resizing buffer from", buffer.size, "to", new_size) + destroy_buffer(device, buffer) + buffer.gpu = sdl.CreateGPUBuffer( + device, + sdl.GPUBufferCreateInfo{usage = gpu_usage, size = new_size}, + ) + buffer.transfer = sdl.CreateGPUTransferBuffer( + device, + sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = new_size}, + ) + buffer.size = new_size + } +} + +destroy_buffer :: proc(device: ^sdl.GPUDevice, buffer: ^Buffer) { + sdl.ReleaseGPUBuffer(device, buffer.gpu) + sdl.ReleaseGPUTransferBuffer(device, buffer.transfer) +} diff --git a/renderer/compile_shaders.sh b/renderer/compile_shaders.sh new file mode 100755 index 0000000..c4cdc18 --- /dev/null +++ b/renderer/compile_shaders.sh @@ -0,0 +1,35 @@ +#!/bin/sh +if ! command -v glslangValidator 2>&1 > /dev/null +then + echo "glslangValidator not found" + exit 1 +fi + +if ! command -v spirv-cross 2>&1 > /dev/null +then + echo "spirv-cross not found" + exit 1 +fi + +# Convert GLSL to SPIRV +echo "Converting GLSL shaders to SPIRV..." +mkdir -p renderer/res/shaders/compiled +cd renderer/res/shaders/raw || exit +glslangValidator -V quad.vert -o ../compiled/quad.vert.spv +glslangValidator -V quad.frag -o ../compiled/quad.frag.spv +glslangValidator -V text.vert -o ../compiled/text.vert.spv +glslangValidator -V text.frag -o ../compiled/text.frag.spv +glslangValidator -V scene.vert -o ../compiled/scene.vert.spv +glslangValidator -V scene.frag -o ../compiled/scene.frag.spv + +# Convert SPIRV to MSL +echo "Done converting GLSL to SPIRV. Converting SPIRV to MSL..." +cd ../compiled || exit +spirv-cross --msl quad.vert.spv --output quad.vert.metal +spirv-cross --msl quad.frag.spv --output quad.frag.metal +spirv-cross --msl text.vert.spv --output text.vert.metal +spirv-cross --msl text.frag.spv --output text.frag.metal +spirv-cross --msl scene.vert.spv --output scene.vert.metal +spirv-cross --msl scene.frag.spv --output scene.frag.metal + +echo "Done processing shaders." \ No newline at end of file diff --git a/renderer/quad.odin b/renderer/quad.odin new file mode 100644 index 0000000..5baf4b9 --- /dev/null +++ b/renderer/quad.odin @@ -0,0 +1,243 @@ +package renderer + +import "core:log" +import "core:mem" +import "core:os" +import sdl "vendor:sdl3" + +tmp_quads: [dynamic]Quad + +QuadPipeline :: struct { + instance_buffer: Buffer, + num_instances: u32, + sdl_pipeline: ^sdl.GPUGraphicsPipeline, +} + +Quad :: struct { + position_scale: [4]f32, + corner_radii: [4]f32, + color: [4]f32, + border_color: [4]f32, + border_width: f32, + _: [3]f32, +} + +@(private) +create_quad_pipeline :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> QuadPipeline { + log.debug("Creating quad pipeline") + when ODIN_OS == .Darwin { + vert_raw := #load("res/shaders/compiled/quad.vert.metal") + frag_raw := #load("res/shaders/compiled/quad.frag.metal") + } else { + vert_raw := #load("res/shaders/compiled/quad.vert.spv") + frag_raw := #load("res/shaders/compiled/quad.frag.spv") + } + + log.debug("Loaded", len(vert_raw), "vert bytes") + log.debug("Loaded", len(frag_raw), "frag bytes") + log.debug("ShaderType:", SHADER_TYPE) + + + vert_info := sdl.GPUShaderCreateInfo { + code_size = len(vert_raw), + code = raw_data(vert_raw), + entrypoint = ENTRY_POINT, + format = SHADER_TYPE, + stage = sdl.GPUShaderStage.VERTEX, + num_uniform_buffers = 1, + } + + frag_info := sdl.GPUShaderCreateInfo { + code_size = len(frag_raw), + code = raw_data(frag_raw), + entrypoint = ENTRY_POINT, + format = SHADER_TYPE, + stage = sdl.GPUShaderStage.FRAGMENT, + } + + vert_shader := sdl.CreateGPUShader(device, vert_info) + if vert_shader == nil { + log.error("Could not create vertex shader:", sdl.GetError()) + os.exit(1) + } + + frag_shader := sdl.CreateGPUShader(device, frag_info) + if frag_shader == nil { + log.error("Could not create fragment shader:", sdl.GetError()) + os.exit(1) + } + + vertex_attributes: [5]sdl.GPUVertexAttribute = { + // position and scale + sdl.GPUVertexAttribute { + buffer_slot = 0, + location = 0, + format = sdl.GPUVertexElementFormat.FLOAT4, + offset = 0, + }, + // corner radii + sdl.GPUVertexAttribute { + buffer_slot = 0, + location = 1, + format = sdl.GPUVertexElementFormat.FLOAT4, + offset = size_of(f32) * 4, + }, + // color + sdl.GPUVertexAttribute { + buffer_slot = 0, + location = 2, + format = sdl.GPUVertexElementFormat.FLOAT4, + offset = size_of(f32) * 8, + }, + // border color + sdl.GPUVertexAttribute { + buffer_slot = 0, + location = 3, + format = sdl.GPUVertexElementFormat.FLOAT4, + offset = size_of(f32) * 12, + }, + // border width + sdl.GPUVertexAttribute { + buffer_slot = 0, + location = 4, + format = sdl.GPUVertexElementFormat.FLOAT, + offset = size_of(f32) * 16, + }, + } + + pipeline_info := sdl.GPUGraphicsPipelineCreateInfo { + vertex_shader = vert_shader, + fragment_shader = frag_shader, + primitive_type = .TRIANGLELIST, + target_info = sdl.GPUGraphicsPipelineTargetInfo { + color_target_descriptions = &sdl.GPUColorTargetDescription { + format = sdl.GetGPUSwapchainTextureFormat(device, window), + blend_state = sdl.GPUColorTargetBlendState { + src_color_blendfactor = .SRC_ALPHA, + dst_color_blendfactor = .ONE_MINUS_SRC_ALPHA, + color_blend_op = .ADD, + src_alpha_blendfactor = .ONE, + dst_alpha_blendfactor = .ONE_MINUS_SRC_ALPHA, + alpha_blend_op = .ADD, + color_write_mask = sdl.GPUColorComponentFlags{.R, .G, .B, .A}, + enable_blend = true, + enable_color_write_mask = true, + }, + }, + num_color_targets = 1, + }, + vertex_input_state = sdl.GPUVertexInputState { + vertex_buffer_descriptions = &sdl.GPUVertexBufferDescription { + slot = 0, + input_rate = sdl.GPUVertexInputRate.INSTANCE, + instance_step_rate = 1, + pitch = size_of(Quad), + }, + num_vertex_buffers = 1, + vertex_attributes = raw_data(vertex_attributes[:]), + num_vertex_attributes = 5, + }, + } + + sdl_pipeline := sdl.CreateGPUGraphicsPipeline(device, pipeline_info) + if sdl_pipeline == nil { + log.error("Failed to create quad graphics pipeline:", sdl.GetError()) + os.exit(1) + } + + sdl.ReleaseGPUShader(device, vert_shader) + sdl.ReleaseGPUShader(device, frag_shader) + + // Create buffers + instance_buffer := create_buffer( + device, + size_of(Quad) * BUFFER_INIT_SIZE, + sdl.GPUBufferUsageFlags { .VERTEX }, + ) + + pipeline := QuadPipeline{instance_buffer, BUFFER_INIT_SIZE, sdl_pipeline} + + log.debug("Done creating quad pipeline") + return pipeline +} + +@(private) +upload_quads :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) { + num_quads := u32(len(tmp_quads)) + size := num_quads * size_of(Quad) + + resize_buffer(device, &quad_pipeline.instance_buffer, size, sdl.GPUBufferUsageFlags { .VERTEX }) + + // Write data + i_array := sdl.MapGPUTransferBuffer(device, quad_pipeline.instance_buffer.transfer, false) + mem.copy(i_array, raw_data(tmp_quads), int(size)) + sdl.UnmapGPUTransferBuffer(device, quad_pipeline.instance_buffer.transfer) + + // Upload + sdl.UploadToGPUBuffer( + pass, + sdl.GPUTransferBufferLocation{transfer_buffer = quad_pipeline.instance_buffer.transfer}, + sdl.GPUBufferRegion{buffer = quad_pipeline.instance_buffer.gpu, offset = 0, size = size}, + false, // TODO figure out what cycling actually does + ) +} + +@(private) +draw_quads :: proc( + device: ^sdl.GPUDevice, + window: ^sdl.Window, + cmd_buffer: ^sdl.GPUCommandBuffer, + swapchain_texture: ^sdl.GPUTexture, + swapchain_w: u32, + swapchain_h: u32, + layer: ^Layer, + load_op: sdl.GPULoadOp, +) { + if layer.quad_len == 0 { + return + } + + render_pass := sdl.BeginGPURenderPass( + cmd_buffer, + &sdl.GPUColorTargetInfo { + texture = swapchain_texture, + clear_color = sdl.FColor{1.0, 1.0, 1.0, 1.0}, + load_op = load_op, + store_op = sdl.GPUStoreOp.STORE, + }, + 1, + nil, + ) + sdl.BindGPUGraphicsPipeline(render_pass, quad_pipeline.sdl_pipeline) + + sdl.BindGPUVertexBuffers( + render_pass, + 0, + &sdl.GPUBufferBinding{buffer = quad_pipeline.instance_buffer.gpu, offset = 0}, + 1, + ) + push_globals(cmd_buffer, f32(swapchain_w), f32(swapchain_h)) + + quad_offset := layer.quad_instance_start + + for &scissor, index in layer.scissors { + if scissor.quad_len == 0 { + continue + } + + if scissor.bounds.w == 0 || scissor.bounds.h == 0 { + sdl.SetGPUScissor(render_pass, sdl.Rect{0, 0, i32(swapchain_w), i32(swapchain_h)}) + } else { + sdl.SetGPUScissor(render_pass, scissor.bounds) + } + + sdl.DrawGPUPrimitives(render_pass, 6, scissor.quad_len, 0, quad_offset) + quad_offset += scissor.quad_len + } + sdl.EndGPURenderPass(render_pass) +} + +destroy_quad_pipeline :: proc(device: ^sdl.GPUDevice) { + destroy_buffer(device, &quad_pipeline.instance_buffer) + sdl.ReleaseGPUGraphicsPipeline(device, quad_pipeline.sdl_pipeline) +} diff --git a/renderer/renderer.odin b/renderer/renderer.odin new file mode 100644 index 0000000..6ab6b53 --- /dev/null +++ b/renderer/renderer.odin @@ -0,0 +1,307 @@ +package renderer + +import "base:runtime" +import "core:c" +import "core:log" +import "core:os" +import "core:strings" +import clay "library:clay" +import sdl_ttf "library:sdl3_ttf" +import sdl "vendor:sdl3" + +when ODIN_OS == .Darwin { + SHADER_TYPE :: sdl.GPUShaderFormat{.MSL} + ENTRY_POINT :: "main0" +} else { + SHADER_TYPE :: sdl.GPUShaderFormat{.SPIRV} + ENTRY_POINT :: "main" +} + +BUFFER_INIT_SIZE: u32 : 256 + +dpi_scaling: f32 = 1.0 +layers: [dynamic]Layer +quad_pipeline: QuadPipeline +text_pipeline: TextPipeline +odin_context: runtime.Context + +// TODO New layer for each z-index/batch +Layer :: struct { + quad_instance_start: u32, + quad_len: u32, + text_instance_start: u32, + text_instance_len: u32, + text_vertex_start: u32, + text_vertex_len: u32, + text_index_start: u32, + text_index_len: u32, + scissors: [dynamic]Scissor, +} + +Scissor :: struct { + bounds: sdl.Rect, + quad_start: u32, + quad_len: u32, + text_start: u32, + text_len: u32, +} + +/// Initialize the renderer. +init :: proc( + device: ^sdl.GPUDevice, + window: ^sdl.Window, + window_width: f32, + window_height: f32, + ctx: runtime.Context, +) { + odin_context = ctx + dpi_scaling = sdl.GetWindowDisplayScale(window) + log.debug("Window DPI scaling:", dpi_scaling) + + min_memory_size: c.size_t = cast(c.size_t)clay.MinMemorySize() + memory := make([^]u8, min_memory_size) + arena := clay.CreateArenaWithCapacityAndMemory(min_memory_size, memory) + + clay.Initialize(arena, {window_width, window_height}, {handler = clay_error_handler}) + clay.SetMeasureTextFunction(measure_text, nil) + quad_pipeline = create_quad_pipeline(device, window) + text_pipeline = create_text_pipeline(device, window) +} + +clay_error_handler :: proc "c" (errorData: clay.ErrorData) { + context = odin_context + log.error("Clay error:", errorData.errorType, errorData.errorText) +} + +@(private = "file") +measure_text :: proc "c" ( + text: clay.StringSlice, + config: ^clay.TextElementConfig, + user_data: rawptr, +) -> clay.Dimensions { + context = odin_context + text := string(text.chars[:text.length]) + c_text := strings.clone_to_cstring(text, context.temp_allocator) + w, h: c.int + if !sdl_ttf.GetStringSize(get_font(config.fontId, config.fontSize), c_text, 0, &w, &h) { + log.error("Failed to measure text", sdl.GetError()) + } + + return clay.Dimensions{width = f32(w) / dpi_scaling, height = f32(h) / dpi_scaling} +} + +destroy :: proc(device: ^sdl.GPUDevice) { + destroy_quad_pipeline(device) + destroy_text_pipeline(device) +} + +/// Upload data to the GPU +prepare :: proc( + device: ^sdl.GPUDevice, + window: ^sdl.Window, + cmd_buffer: ^sdl.GPUCommandBuffer, + render_commands: ^clay.ClayArray(clay.RenderCommand), + mouse_delta: [2]f32, + 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(&tmp_quads) + clear(&tmp_text) + + tmp_quads = make([dynamic]Quad, 0, quad_pipeline.num_instances, context.temp_allocator) + tmp_text = make([dynamic]Text, 0, 20, context.temp_allocator) + + layer := Layer { + scissors = make([dynamic]Scissor, 0, 10, context.temp_allocator), + } + scissor := Scissor{} + + // Parse render commands + for i in 0 ..< int(render_commands.length) { + render_command := clay.RenderCommandArray_Get(render_commands, cast(i32)i) + bounds := render_command.boundingBox + + switch (render_command.commandType) { + case clay.RenderCommandType.None: + case clay.RenderCommandType.Text: + render_data := render_command.renderData.text + text := string(render_data.stringContents.chars[:render_data.stringContents.length]) + c_text := strings.clone_to_cstring(text, context.temp_allocator) + sdl_text := text_pipeline.cache[render_command.id] + + if sdl_text == nil { + // Cache a SDL text object + sdl_text = sdl_ttf.CreateText( + text_pipeline.engine, + get_font(render_data.fontId, render_data.fontSize), + c_text, + 0, + ) + text_pipeline.cache[render_command.id] = sdl_text + } else { + // Update text with c_string + sdl_ttf.SetTextString(sdl_text, c_text, 0) + } + + data := sdl_ttf.GetGPUTextDrawData(sdl_text) + + if sdl_text == nil { + log.error("Could not create SDL text:", sdl.GetError()) + } else { + append( + &tmp_text, + Text{sdl_text, {bounds.x, bounds.y}, f32_color(render_data.textColor)}, + ) + layer.text_instance_len += 1 + layer.text_vertex_len += u32(data.num_verticies) + layer.text_index_len += u32(data.num_indices) + scissor.text_len += 1 + } + case clay.RenderCommandType.Image: + case clay.RenderCommandType.ScissorStart: + bounds := sdl.Rect { + c.int(bounds.x * dpi_scaling), + c.int(bounds.y * dpi_scaling), + c.int(bounds.width * dpi_scaling), + c.int(bounds.height * dpi_scaling), + } + new := new_scissor(&scissor) + if scissor.quad_len != 0 || scissor.text_len != 0 { + append(&layer.scissors, scissor) + } + scissor = new + scissor.bounds = bounds + case clay.RenderCommandType.ScissorEnd: + new := new_scissor(&scissor) + if scissor.quad_len != 0 || scissor.text_len != 0 { + append(&layer.scissors, scissor) + } + scissor = new + case clay.RenderCommandType.Rectangle: + render_data := render_command.renderData.rectangle + color := f32_color(render_data.backgroundColor) + cr := render_data.cornerRadius + quad := Quad { + position_scale = {bounds.x, bounds.y, bounds.width, bounds.height}, + corner_radii = {cr.topLeft, cr.topRight, cr.bottomRight, cr.bottomLeft}, + color = color, + } + append(&tmp_quads, quad) + layer.quad_len += 1 + scissor.quad_len += 1 + case clay.RenderCommandType.Border: + render_data := render_command.renderData.border + cr := render_data.cornerRadius + quad := Quad { + position_scale = {bounds.x, bounds.y, bounds.width, bounds.height}, + corner_radii = {cr.topLeft, cr.topRight, cr.bottomRight, cr.bottomLeft}, + //TODO: I was using a hack here to get the underlying color of the quad in the layout and then pass it into the + // right border color, but Clay got rid of multi color support for borders so I need to just make a dedicated border pipeline + color = f32_color( + clay.Color{render_data.color.r, render_data.color.g, render_data.color.b, 0.0}, + ), + border_color = f32_color(render_data.color), + // We only support one border width at the moment + border_width = f32(render_data.width.top), + } + // Technically these should be drawn on top of everything else including children, but + // for our use case we can just chuck these in with the quad pipeline + append(&tmp_quads, quad) + layer.quad_len += 1 + scissor.quad_len += 1 + case clay.RenderCommandType.Custom: + } + } + + //TODO start new layers with z-index changes + append(&layer.scissors, scissor) + append(&layers, layer) + + // Upload primitives to GPU + copy_pass := sdl.BeginGPUCopyPass(cmd_buffer) + upload_quads(device, copy_pass) + upload_text(device, copy_pass) + sdl.EndGPUCopyPass(copy_pass) +} + +/// Render primitives +draw :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window, cmd_buffer: ^sdl.GPUCommandBuffer) { + swapchain_texture: ^sdl.GPUTexture + w, h: u32 + if !sdl.WaitAndAcquireGPUSwapchainTexture(cmd_buffer, window, &swapchain_texture, &w, &h) { + log.error("Failed to acquire swapchain texture:", sdl.GetError()) + os.exit(1) + } + + if swapchain_texture == nil { + log.error("Failed to acquire swapchain texture:", sdl.GetError()) + os.exit(1) + } + + for &layer, index in layers { + draw_quads( + device, + window, + cmd_buffer, + swapchain_texture, + w, + h, + &layer, + index == 0 ? sdl.GPULoadOp.CLEAR : sdl.GPULoadOp.LOAD, + ) + draw_text(device, window, cmd_buffer, swapchain_texture, w, h, &layer) + //TODO draw other primitives in layer + } +} + +ortho_rh :: proc( + left: f32, + right: f32, + bottom: f32, + top: f32, + near: f32, + far: f32, +) -> matrix[4, 4]f32 { + return matrix[4, 4]f32{ + 2.0 / (right - left), 0.0, 0.0, -(right + left) / (right - left), + 0.0, 2.0 / (top - bottom), 0.0, -(top + bottom) / (top - bottom), + 0.0, 0.0, -2.0 / (far - near), -(far + near) / (far - near), + 0.0, 0.0, 0.0, 1.0, + } +} + +f32_color :: proc(color: clay.Color) -> [4]f32 { + return [4]f32{color.x / 255.0, color.y / 255.0, color.z / 255.0, color.w / 255.0} +} + +Globals :: struct { + projection: matrix[4, 4]f32, + scale: f32, +} + +push_globals :: proc(cmd_buffer: ^sdl.GPUCommandBuffer, w: f32, h: f32) { + globals := Globals { + ortho_rh(left = 0.0, top = 0.0, right = f32(w), bottom = f32(h), near = -1.0, far = 1.0), + dpi_scaling, + } + + sdl.PushGPUVertexUniformData(cmd_buffer, 0, &globals, size_of(Globals)) +} + +new_scissor :: proc(old: ^Scissor) -> Scissor { + return Scissor { + quad_start = old.quad_start + old.quad_len, + text_start = old.text_start + old.text_len, + } +} diff --git a/renderer/res/fonts/JetBrainsMono-Bold.ttf b/renderer/res/fonts/JetBrainsMono-Bold.ttf new file mode 100644 index 0000000..8c93043 Binary files /dev/null and b/renderer/res/fonts/JetBrainsMono-Bold.ttf differ diff --git a/renderer/res/fonts/JetBrainsMono-Regular.ttf b/renderer/res/fonts/JetBrainsMono-Regular.ttf new file mode 100644 index 0000000..dff66cc Binary files /dev/null and b/renderer/res/fonts/JetBrainsMono-Regular.ttf differ diff --git a/renderer/res/shaders/raw/quad.frag b/renderer/res/shaders/raw/quad.frag new file mode 100644 index 0000000..f61599a --- /dev/null +++ b/renderer/res/shaders/raw/quad.frag @@ -0,0 +1,33 @@ +#version 450 core + +layout(location = 0) in vec4 color; +layout(location = 1) in vec4 corners; +layout(location = 2) in vec4 center_scale; +layout(location = 3) in vec4 border_color; +layout(location = 4) in float border_width; + +layout(location = 0) out vec4 out_color; + +const float AA_THRESHOLD = 1.0; + +float rounded_box(vec2 p, vec2 b, in vec4 r) { + r.xy = (p.x > 0.0) ? r.xy : r.zw; + r.x = (p.y > 0.0) ? r.x : r.y; + vec2 q = abs(p) - b + r.x; + return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x; +} + +void main() { + if (corners == vec4(0.0) && border_width == 0.0) { + out_color = color; + } else { + float d = rounded_box(gl_FragCoord.xy - center_scale.xy, center_scale.zw, corners); + if (d > AA_THRESHOLD) { + discard; + } + float alpha = 1.0 - smoothstep(-AA_THRESHOLD, AA_THRESHOLD, d); + vec4 border_mixed = mix(color, border_color, 1.0 - smoothstep(0.0, AA_THRESHOLD * 2.0, abs(d) - border_width - AA_THRESHOLD)); + + out_color = vec4(border_mixed.rgb, border_mixed.a * alpha); + } +} diff --git a/renderer/res/shaders/raw/quad.vert b/renderer/res/shaders/raw/quad.vert new file mode 100644 index 0000000..c46773f --- /dev/null +++ b/renderer/res/shaders/raw/quad.vert @@ -0,0 +1,54 @@ +#version 450 core + +layout(location = 0) in vec4 v_pos_scale; +layout(location = 1) in vec4 v_corners; +layout(location = 2) in vec4 v_color; +layout(location = 3) in vec4 v_border_color; +layout(location = 4) in float v_border_width; + +layout(location = 0) out vec4 color; +layout(location = 1) out vec4 corners; +layout(location = 2) out vec4 center_scale; +layout(location = 3) out vec4 border_color; +layout(location = 4) out float border_width; + +layout(set = 1, binding = 0) uniform Uniforms { + mat4 projection; + float dpi_scale; +}; + +const vec2 positions[6] = vec2[]( + vec2(1.0, 1.0), // top left + vec2(1.0, 0.0), // top right + vec2(0.0, 0.0), // bottom right + vec2(0.0, 0.0), // bottom right + vec2(0.0, 1.0), // bottom left + vec2(1.0, 1.0) // top left +); + +void main() { + float min_corner_radius = min(v_pos_scale.z, v_pos_scale.w) * 0.5; + vec4 corner_radii = vec4( + min(v_corners.x, min_corner_radius), + min(v_corners.y, min_corner_radius), + min(v_corners.z, min_corner_radius), + min(v_corners.w, min_corner_radius) + ); + + // Extract position and scale from position_scale + vec2 position = v_pos_scale.xy * dpi_scale; + vec2 scale = v_pos_scale.zw * dpi_scale; + + vec2 local_pos = positions[gl_VertexIndex]; + local_pos *= scale; + local_pos += position; + + // Pass values to fragment shader + color = v_color; + corners = corner_radii * dpi_scale; + center_scale = vec4(position + scale * 0.5, v_pos_scale.zw); + border_color = v_border_color; + border_width = v_border_width * dpi_scale; + + gl_Position = projection * vec4(local_pos, 0.0, 1.0); +} diff --git a/renderer/res/shaders/raw/scene.frag b/renderer/res/shaders/raw/scene.frag new file mode 100644 index 0000000..ba7afb0 --- /dev/null +++ b/renderer/res/shaders/raw/scene.frag @@ -0,0 +1,10 @@ +#version 450 core + +layout(location = 0) in vec4 color; +layout(location = 1) in vec4 roughness_metallic_uv; + +layout(location = 0) out vec4 out_color; + +void main() { + out_color = color; +} diff --git a/renderer/res/shaders/raw/scene.vert b/renderer/res/shaders/raw/scene.vert new file mode 100644 index 0000000..b3e7dfe --- /dev/null +++ b/renderer/res/shaders/raw/scene.vert @@ -0,0 +1,28 @@ +#version 450 + +layout (location = 0) in vec4 v_pos; +layout (location = 1) in vec4 v_normal; +layout (location = 2) in vec2 v_uv; + +layout(location = 0) out vec4 color; +layout(location = 1) out vec4 roughness_metallic_uv; + +struct Material { + vec4 base_color; + float roughness; + float metallic; +}; + +layout(binding = 0) uniform UniformBlock { + mat4 projection; + float dpi_scale; + Material material; +}; + +void main() { + vec3 local_pos = v_pos.xyz; + gl_Position = projection * vec4(local_pos, 1.0); + + color = material.base_color; + roughness_metallic_uv = vec4(material.roughness, material.metallic, v_uv); +} \ No newline at end of file diff --git a/renderer/res/shaders/raw/text.frag b/renderer/res/shaders/raw/text.frag new file mode 100644 index 0000000..380bc48 --- /dev/null +++ b/renderer/res/shaders/raw/text.frag @@ -0,0 +1,12 @@ +#version 450 + +layout (location = 0) in vec4 color; +layout (location = 1) in vec2 uv; + +layout (location = 0) out vec4 out_color; + +layout (set = 2, binding = 0) uniform sampler2D atlas; + +void main() { + out_color = color * texture(atlas, uv); +} \ No newline at end of file diff --git a/renderer/res/shaders/raw/text.vert b/renderer/res/shaders/raw/text.vert new file mode 100644 index 0000000..56b828f --- /dev/null +++ b/renderer/res/shaders/raw/text.vert @@ -0,0 +1,23 @@ +#version 450 + +layout (location = 0) in vec4 v_pos_uv; +layout (location = 1) in vec4 v_color; +layout (location = 2) in vec2 text_pos; + +layout (location = 0) out vec4 color; +layout (location = 1) out vec2 uv; + +layout(set = 1, binding = 0) uniform Uniforms { + mat4 projection; + float dpi_scale; +}; + +void main() { + color = v_color; + uv = v_pos_uv.zw; + + vec2 local_pos = v_pos_uv.xy; + local_pos += text_pos * dpi_scale; + + gl_Position = projection * vec4(local_pos, 0.0, 1.0); +} \ No newline at end of file diff --git a/renderer/text.odin b/renderer/text.odin new file mode 100644 index 0000000..793528b --- /dev/null +++ b/renderer/text.odin @@ -0,0 +1,440 @@ +package renderer + +import "core:c" +import "core:log" +import "core:mem" +import "core:os" +import sdl_ttf "library:sdl3_ttf" +import sdl "vendor:sdl3" + +JETBRAINS_MONO_REGULAR: u16 : 0 +JETBRAINS_MONO_BOLD: u16 : 1 +NUM_FONTS :: 2 +MAX_FONT_SIZE :: 120 + +tmp_text: [dynamic]Text + +@(private = "file") +jetbrains_mono_regular := #load("res/fonts/JetBrainsMono-Regular.ttf") +@(private = "file") +jetbrains_mono_bold := #load("res/fonts/JetBrainsMono-Bold.ttf") + +TextPipeline :: struct { + engine: ^sdl_ttf.TextEngine, + fonts: [NUM_FONTS][MAX_FONT_SIZE]^sdl_ttf.Font, + sdl_pipeline: ^sdl.GPUGraphicsPipeline, + vertex_buffer: Buffer, + index_buffer: Buffer, + instance_buffer: Buffer, + sampler: ^sdl.GPUSampler, + cache: map[u32]^sdl_ttf.Text, +} + +get_font :: proc(id: u16, size: u16) -> ^sdl_ttf.Font { + font := text_pipeline.fonts[id > 1 ? 0 : id][size > 0 ? size : 16] + + if font == nil { + log.debug("Font not found for size", size, "+ adding") + jb_mono_reg_rwops := sdl.IOFromConstMem( + raw_data(jetbrains_mono_regular[:]), + len(jetbrains_mono_regular), + ) + f := sdl_ttf.OpenFontIO(jb_mono_reg_rwops, true, f32(size)) + if f == nil { + log.error("Failed to font with size:", size, sdl.GetError()) + os.exit(1) + } + font = f + sdl_ttf.SetFontSizeDPI(f, f32(size), 72 * i32(dpi_scaling), 72 * i32(dpi_scaling)) + text_pipeline.fonts[id][size] = f + } + + return font +} + +Text :: struct { + ref: ^sdl_ttf.Text, + position: [2]f32, + color: [4]f32, +} + +// For upload +TextVert :: struct { + pos_uv: [4]f32, + color: [4]f32, +} + +@(private) +create_text_pipeline :: proc(device: ^sdl.GPUDevice, window: ^sdl.Window) -> TextPipeline { + log.debug("Creating text pipeline") + if !sdl_ttf.Init() { + log.error("Failed to initialize TTF", sdl.GetError()) + os.exit(1) + } + + when ODIN_OS == .Darwin { + vert_raw := #load("res/shaders/compiled/text.vert.metal") + frag_raw := #load("res/shaders/compiled/text.frag.metal") + } else { + vert_raw := #load("res/shaders/compiled/text.vert.spv") + frag_raw := #load("res/shaders/compiled/text.frag.spv") + } + + log.debug("Loaded", len(vert_raw), "vert bytes") + log.debug("Loaded", len(frag_raw), "frag bytes") + + vert_info := sdl.GPUShaderCreateInfo { + code_size = len(vert_raw), + code = raw_data(vert_raw), + entrypoint = ENTRY_POINT, + format = SHADER_TYPE, + stage = sdl.GPUShaderStage.VERTEX, + num_uniform_buffers = 1, + } + + frag_info := sdl.GPUShaderCreateInfo { + code_size = len(frag_raw), + code = raw_data(frag_raw), + entrypoint = ENTRY_POINT, + format = SHADER_TYPE, + stage = sdl.GPUShaderStage.FRAGMENT, + num_samplers = 1, + } + + vert_shader := sdl.CreateGPUShader(device, vert_info) + if vert_shader == nil { + log.error("Could not create vertex shader:", sdl.GetError()) + os.exit(1) + } + + frag_shader := sdl.CreateGPUShader(device, frag_info) + if frag_shader == nil { + log.error("Could not create fragment shader:", sdl.GetError()) + os.exit(1) + } + + vertex_attributes: [3]sdl.GPUVertexAttribute = { + // vertex position & uv + sdl.GPUVertexAttribute { + buffer_slot = 0, + location = 0, + format = sdl.GPUVertexElementFormat.FLOAT4, + offset = 0, + }, + // color + sdl.GPUVertexAttribute { + buffer_slot = 0, + location = 1, + format = sdl.GPUVertexElementFormat.FLOAT4, + offset = size_of(f32) * 4, + }, + // Instance position data + sdl.GPUVertexAttribute { + buffer_slot = 1, + location = 2, + format = sdl.GPUVertexElementFormat.FLOAT2, + offset = 0, + }, + } + + buffer_descriptions: [2]sdl.GPUVertexBufferDescription = { + sdl.GPUVertexBufferDescription{slot = 0, input_rate = .VERTEX, pitch = size_of(TextVert)}, + sdl.GPUVertexBufferDescription { + slot = 1, + input_rate = .INSTANCE, + pitch = size_of([2]f32), + instance_step_rate = 1, + }, + } + + sampler_info := sdl.GPUSamplerCreateInfo { + min_filter = .LINEAR, + mag_filter = .LINEAR, + mipmap_mode = .LINEAR, + address_mode_u = .CLAMP_TO_EDGE, + address_mode_v = .CLAMP_TO_EDGE, + address_mode_w = .CLAMP_TO_EDGE, + } + + sampler := sdl.CreateGPUSampler(device, sampler_info) + if sampler == nil { + log.error("Could not create GPU sampler:", sdl.GetError()) + os.exit(1) + } + + pipeline_info := sdl.GPUGraphicsPipelineCreateInfo { + vertex_shader = vert_shader, + fragment_shader = frag_shader, + primitive_type = .TRIANGLELIST, + target_info = sdl.GPUGraphicsPipelineTargetInfo { + color_target_descriptions = &sdl.GPUColorTargetDescription { + format = sdl.GetGPUSwapchainTextureFormat(device, window), + blend_state = sdl.GPUColorTargetBlendState { + enable_blend = true, + color_write_mask = sdl.GPUColorComponentFlags{.R, .G, .B, .A}, + alpha_blend_op = sdl.GPUBlendOp.ADD, + src_alpha_blendfactor = sdl.GPUBlendFactor.SRC_ALPHA, + dst_alpha_blendfactor = sdl.GPUBlendFactor.ONE_MINUS_SRC_ALPHA, + color_blend_op = sdl.GPUBlendOp.ADD, + src_color_blendfactor = sdl.GPUBlendFactor.SRC_ALPHA, + dst_color_blendfactor = sdl.GPUBlendFactor.ONE_MINUS_SRC_ALPHA, + }, + }, + num_color_targets = 1, + }, + vertex_input_state = sdl.GPUVertexInputState { + vertex_buffer_descriptions = raw_data(buffer_descriptions[:]), + num_vertex_buffers = 2, + vertex_attributes = raw_data(vertex_attributes[:]), + num_vertex_attributes = 3, + }, + } + + sdl_pipeline := sdl.CreateGPUGraphicsPipeline(device, pipeline_info) + if sdl_pipeline == nil { + log.error("Failed to create quad graphics pipeline:", sdl.GetError()) + os.exit(1) + } + + sdl.ReleaseGPUShader(device, vert_shader) + sdl.ReleaseGPUShader(device, frag_shader) + + // Create engine + engine := sdl_ttf.CreateGPUTextEngine(device) + if engine == nil { + log.error("Could not create text engine") + os.exit(1) + } + sdl_ttf.SetGPUTextEngineWinding(engine, .COUNTERCLOCKWISE) + + // Create buffers + vertex_buffer := create_buffer( + device, + size_of(TextVert) * BUFFER_INIT_SIZE, + sdl.GPUBufferUsageFlags{.VERTEX}, + ) + index_buffer := create_buffer( + device, + size_of(c.int) * BUFFER_INIT_SIZE, + sdl.GPUBufferUsageFlags{.INDEX}, + ) + instance_buffer := create_buffer( + device, + size_of([2]f32) * BUFFER_INIT_SIZE, + sdl.GPUBufferUsageFlags{.VERTEX}, + ) + + pipeline := TextPipeline { + engine, + [NUM_FONTS][MAX_FONT_SIZE]^sdl_ttf.Font{}, + sdl_pipeline, + vertex_buffer, + index_buffer, + instance_buffer, + sampler, + make(map[u32]^sdl_ttf.Text), + } + + log.debug("Done creating text pipeline") + return pipeline +} + +@(private) +upload_text :: proc(device: ^sdl.GPUDevice, pass: ^sdl.GPUCopyPass) { + vertices := make([dynamic]TextVert, 0, BUFFER_INIT_SIZE, context.temp_allocator) + indices := make([dynamic]c.int, 0, BUFFER_INIT_SIZE, context.temp_allocator) + instances := make([dynamic][2]f32, 0, BUFFER_INIT_SIZE, context.temp_allocator) + + for &text, index in tmp_text { + append(&instances, text.position) + data := sdl_ttf.GetGPUTextDrawData(text.ref) + + for data != nil { + for i in 0 ..< data.num_verticies { + pos := data.vertex_positions[i] + uv := data.uvs[i] + color := text.color + append(&vertices, TextVert{{pos.x, -pos.y, uv.x, uv.y}, color}) + } + append(&indices, ..data.indices[:data.num_indices]) + data = data.next + } + } + + // Resize buffers if needed + vertices_size := u32(len(vertices) * size_of(TextVert)) + indices_size := u32(len(indices) * size_of(c.int)) + instances_size := u32(len(instances) * size_of([2]f32)) + + resize_buffer( + device, + &text_pipeline.vertex_buffer, + vertices_size, + sdl.GPUBufferUsageFlags{.VERTEX}, + ) + resize_buffer( + device, + &text_pipeline.index_buffer, + indices_size, + sdl.GPUBufferUsageFlags{.INDEX}, + ) + resize_buffer( + device, + &text_pipeline.instance_buffer, + instances_size, + sdl.GPUBufferUsageFlags{.VERTEX}, + ) + + vertex_array := sdl.MapGPUTransferBuffer(device, text_pipeline.vertex_buffer.transfer, true) + mem.copy(vertex_array, raw_data(vertices), int(vertices_size)) + sdl.UnmapGPUTransferBuffer(device, text_pipeline.vertex_buffer.transfer) + + index_array := sdl.MapGPUTransferBuffer(device, text_pipeline.index_buffer.transfer, true) + mem.copy(index_array, raw_data(indices), int(indices_size)) + sdl.UnmapGPUTransferBuffer(device, text_pipeline.index_buffer.transfer) + + instance_array := sdl.MapGPUTransferBuffer( + device, + text_pipeline.instance_buffer.transfer, + true, + ) + mem.copy(instance_array, raw_data(instances), int(instances_size)) + sdl.UnmapGPUTransferBuffer(device, text_pipeline.instance_buffer.transfer) + + sdl.UploadToGPUBuffer( + pass, + sdl.GPUTransferBufferLocation{transfer_buffer = text_pipeline.vertex_buffer.transfer}, + sdl.GPUBufferRegion { + buffer = text_pipeline.vertex_buffer.gpu, + offset = 0, + size = vertices_size, + }, + true, + ) + + sdl.UploadToGPUBuffer( + pass, + sdl.GPUTransferBufferLocation{transfer_buffer = text_pipeline.index_buffer.transfer}, + sdl.GPUBufferRegion { + buffer = text_pipeline.index_buffer.gpu, + offset = 0, + size = indices_size, + }, + true, + ) + + sdl.UploadToGPUBuffer( + pass, + sdl.GPUTransferBufferLocation{transfer_buffer = text_pipeline.instance_buffer.transfer}, + sdl.GPUBufferRegion { + buffer = text_pipeline.instance_buffer.gpu, + offset = 0, + size = instances_size, + }, + true, + ) +} + +@(private) +draw_text :: proc( + device: ^sdl.GPUDevice, + window: ^sdl.Window, + cmd_buffer: ^sdl.GPUCommandBuffer, + swapchain_texture: ^sdl.GPUTexture, + swapchain_w: u32, + swapchain_h: u32, + layer: ^Layer, +) { + if layer.text_instance_len == 0 { + return + } + + render_pass := sdl.BeginGPURenderPass( + cmd_buffer, + &sdl.GPUColorTargetInfo { + texture = swapchain_texture, + load_op = sdl.GPULoadOp.LOAD, + store_op = sdl.GPUStoreOp.STORE, + }, + 1, + nil, + ) + sdl.BindGPUGraphicsPipeline(render_pass, text_pipeline.sdl_pipeline) + + v_bindings: [2]sdl.GPUBufferBinding = { + sdl.GPUBufferBinding{buffer = text_pipeline.vertex_buffer.gpu, offset = 0}, + sdl.GPUBufferBinding{buffer = text_pipeline.instance_buffer.gpu, offset = 0}, + } + + sdl.BindGPUVertexBuffers(render_pass, 0, raw_data(v_bindings[:]), 2) + sdl.BindGPUIndexBuffer( + render_pass, + sdl.GPUBufferBinding{buffer = text_pipeline.index_buffer.gpu, offset = 0}, + ._32BIT, + ) + + push_globals(cmd_buffer, f32(swapchain_w), f32(swapchain_h)) + + atlas: ^sdl.GPUTexture + + layer_text := tmp_text[layer.text_instance_start:layer.text_instance_start + + layer.text_instance_len] + index_offset: u32 = layer.text_instance_start + vertex_offset: i32 = i32(layer.text_vertex_start) + instance_offset: u32 = layer.text_instance_start + + for &scissor, index in layer.scissors { + if scissor.text_len == 0 { + continue + } + + if scissor.bounds.w == 0 || scissor.bounds.h == 0 { + sdl.SetGPUScissor(render_pass, sdl.Rect{0, 0, i32(swapchain_w), i32(swapchain_h)}) + } else { + sdl.SetGPUScissor(render_pass, scissor.bounds) + } + + for &text in layer_text[scissor.text_start:scissor.text_start + scissor.text_len] { + data := sdl_ttf.GetGPUTextDrawData(text.ref) + + for data != nil { + if data.atlas_texture != atlas { + sdl.BindGPUFragmentSamplers( + render_pass, + 0, + &sdl.GPUTextureSamplerBinding { + texture = data.atlas_texture, + sampler = text_pipeline.sampler, + }, + 1, + ) + atlas = data.atlas_texture + } + + sdl.DrawGPUIndexedPrimitives( + render_pass, + u32(data.num_indices), + 1, + index_offset, + vertex_offset, + instance_offset, + ) + + index_offset += u32(data.num_indices) + vertex_offset += data.num_verticies + + data = data.next + } + + instance_offset += 1 + } + } + + sdl.EndGPURenderPass(render_pass) +} + +destroy_text_pipeline :: proc(device: ^sdl.GPUDevice) { + destroy_buffer(device, &text_pipeline.vertex_buffer) + destroy_buffer(device, &text_pipeline.index_buffer) + sdl.ReleaseGPUGraphicsPipeline(device, text_pipeline.sdl_pipeline) +}