diff --git a/.gitignore b/.gitignore index fc91f02..3a3a9ed 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ nob.ilk # Generated source *.gen.h *.gen.cpp +*.spv.h # Misc *.exe diff --git a/README.md b/README.md index 2ea786f..9f2fe7b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Graphical interface for automatically recording samples from existing analog aud ### Windows -Requires MSVC (Visual Studio 2019 Build Tools or later) with the Windows SDK and C++20 support. +Requires MSVC (Visual Studio 2019 Build Tools or later) with the Windows SDK and C++20 support, and the [Vulkan SDK](https://vulkan.lunarg.com/) (for headers, `vulkan-1.lib`, and `glslc`). The build system looks for `%VULKAN_SDK%` or falls back to `C:\VulkanSDK\1.4.341.1`. Open a Developer Command Prompt, then: @@ -74,7 +74,7 @@ Interactive elements use subtle visual effects for a DAW-style look: Custom SDF-based pipeline for UI rendering. Processes Clay's `Clay_RenderCommandArray` output directly — no intermediate scene graph. -- **Windows**: `renderer_dx12.cpp` — DirectX 12, HLSL shaders +- **Windows**: `renderer_vulkan.cpp` — Vulkan, GLSL shaders (compiled to SPIR-V at build time) - **macOS**: `renderer_metal.mm` — Metal, MSL shaders Both renderers share the same vertex format (18 floats), SDF rounded-rect shader, and Clay command processing logic. Font rasterization and text measurement use FreeType on both platforms with the embedded [Inter](https://rsms.me/inter/) typeface. The font atlas is a 1024x1024 R8 texture containing ASCII glyphs 32–126 rasterized at a fixed pixel size; text at other sizes is scaled from the atlas. The Inter TTF is embedded at build time as a C byte array (`font_inter.gen.h`) so there are no runtime font file dependencies. Font scale is multiplied by the platform DPI scale factor for crisp rendering on high-DPI displays. diff --git a/build.c b/build.c index 98673b9..56c1f7f 100644 --- a/build.c +++ b/build.c @@ -445,10 +445,14 @@ int main(int argc, char **argv) { //////////////////////////////// // Windows build (MSVC cl.exe) +// Vulkan SDK path — detected from environment or default install location +static const char *get_vulkan_sdk_path(void) { + const char *env = getenv("VULKAN_SDK"); + if (env && env[0]) return env; + return "C:\\VulkanSDK\\1.4.341.1"; +} + static const char *link_libs[] = { - "d3d12.lib", - "dxgi.lib", - "d3dcompiler.lib", "user32.lib", "gdi32.lib", "shell32.lib", @@ -458,6 +462,78 @@ static const char *link_libs[] = { "winmm.lib", }; +//////////////////////////////// +// SPIR-V shader compilation — compiles .glsl to .spv, then embeds as C header + +static bool compile_shader(const char *glslc_path, const char *src, const char *spv_path, const char *stage) { + Nob_Cmd cmd = {0}; + nob_cmd_append(&cmd, glslc_path, nob_temp_sprintf("-fshader-stage=%s", stage), "-o", spv_path, src); + Nob_Cmd_Opt opt = {0}; + return nob_cmd_run_opt(&cmd, opt); +} + +static bool embed_spirv(const char *spv_path, const char *header_path, const char *array_name) { + Nob_String_Builder sb = {0}; + if (!nob_read_entire_file(spv_path, &sb)) return false; + + FILE *out = fopen(header_path, "wb"); + if (!out) { + nob_log(NOB_ERROR, "Could not open %s for writing", header_path); + nob_sb_free(sb); + return false; + } + + fprintf(out, "// Auto-generated from %s — do not edit\n", spv_path); + fprintf(out, "#pragma once\n\n"); + // SPIR-V is U32-aligned, emit as uint32_t array + size_t word_count = sb.count / 4; + fprintf(out, "#include \n\n"); + fprintf(out, "static const uint32_t %s[] = {\n", array_name); + const uint32_t *words = (const uint32_t *)sb.items; + for (size_t i = 0; i < word_count; i++) { + if (i % 8 == 0) fprintf(out, " "); + fprintf(out, "0x%08x,", words[i]); + if (i % 8 == 7 || i == word_count - 1) fprintf(out, "\n"); + } + fprintf(out, "};\n"); + + fclose(out); + nob_sb_free(sb); + nob_log(NOB_INFO, "Generated %s (%zu bytes)", header_path, sb.count); + return true; +} + +static bool compile_and_embed_shaders(const char *build_dir) { + const char *vk_sdk = get_vulkan_sdk_path(); + const char *glslc = nob_temp_sprintf("%s\\Bin\\glslc.exe", vk_sdk); + + const char *vert_src = "src/renderer/ui.v.glsl"; + const char *frag_src = "src/renderer/ui.f.glsl"; + const char *vert_spv = nob_temp_sprintf("%s/ui_vert.spv", build_dir); + const char *frag_spv = nob_temp_sprintf("%s/ui_frag.spv", build_dir); + const char *vert_hdr = "src/renderer/ui_vert.spv.h"; + const char *frag_hdr = "src/renderer/ui_frag.spv.h"; + + // Check if rebuild needed + if (nob_needs_rebuild1(vert_hdr, vert_src)) { + nob_log(NOB_INFO, "Compiling vertex shader"); + if (!compile_shader(glslc, vert_src, vert_spv, "vertex")) return false; + if (!embed_spirv(vert_spv, vert_hdr, "ui_vert_spv")) return false; + } else { + nob_log(NOB_INFO, "Vertex shader is up to date"); + } + + if (nob_needs_rebuild1(frag_hdr, frag_src)) { + nob_log(NOB_INFO, "Compiling fragment shader"); + if (!compile_shader(glslc, frag_src, frag_spv, "fragment")) return false; + if (!embed_spirv(frag_spv, frag_hdr, "ui_frag_spv")) return false; + } else { + nob_log(NOB_INFO, "Fragment shader is up to date"); + } + + return true; +} + static bool build_lunasvg_lib(const char *build_dir, bool debug) { const char *obj_dir = nob_temp_sprintf("%s\\lunasvg_obj", build_dir); const char *lib_path = debug ? "vendor\\lunasvg\\lunasvg_d.lib" : "vendor\\lunasvg\\lunasvg.lib"; @@ -626,6 +702,8 @@ int main(int argc, char **argv) { remove("vendor\\freetype\\freetype.lib"); remove("vendor\\freetype\\freetype_d.lib"); remove("src\\renderer\\font_inter.gen.h"); + remove("src\\renderer\\ui_vert.spv.h"); + remove("src\\renderer\\ui_frag.spv.h"); return 0; } @@ -639,7 +717,14 @@ int main(int argc, char **argv) { if (!embed_font_file("assets/fonts/Inter-Regular.ttf", "src\\renderer\\font_inter.gen.h")) return 1; - // Unity build: single cl.exe invocation compiles main.cpp (which #includes everything) + // Compile GLSL shaders to SPIR-V and embed as C headers + if (!compile_and_embed_shaders(build_dir)) return 1; + + const char *vk_sdk = get_vulkan_sdk_path(); + const char *vk_include = nob_temp_sprintf("/I%s/Include", vk_sdk); + const char *vk_lib = nob_temp_sprintf("%s/Lib/vulkan-1.lib", vk_sdk); + + // Unity build: single cl.exe invocation compiles main.cpp (which #includes everything including the vulkan renderer) { Nob_Cmd cmd = {0}; nob_cmd_append(&cmd, "cl.exe"); @@ -647,6 +732,7 @@ int main(int argc, char **argv) { nob_cmd_append(&cmd, "/Isrc", "/Ivendor/clay"); nob_cmd_append(&cmd, "/Ivendor/lunasvg/include"); nob_cmd_append(&cmd, "/Ivendor/freetype/include"); + nob_cmd_append(&cmd, vk_include); nob_cmd_append(&cmd, "/DLUNASVG_BUILD_STATIC"); if (debug) { @@ -668,6 +754,7 @@ int main(int argc, char **argv) { nob_cmd_append(&cmd, "/DEBUG"); nob_cmd_append(&cmd, debug ? "vendor/lunasvg/lunasvg_d.lib" : "vendor/lunasvg/lunasvg.lib"); nob_cmd_append(&cmd, debug ? "vendor/freetype/freetype_d.lib" : "vendor/freetype/freetype.lib"); + nob_cmd_append(&cmd, vk_lib); { size_t i; for (i = 0; i < NOB_ARRAY_LEN(link_libs); i++) diff --git a/src/main.cpp b/src/main.cpp index c43ae98..d4fc0b6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -29,7 +29,7 @@ #include "audio/audio_coreaudio.cpp" #else #include "platform/platform_win32.cpp" -#include "renderer/renderer_dx12.cpp" +#include "renderer/renderer_vulkan.cpp" #include "midi/midi_win32.cpp" #include "audio/audio_asio.cpp" #endif diff --git a/src/renderer/renderer.h b/src/renderer/renderer.h index d804fe9..6b0d210 100644 --- a/src/renderer/renderer.h +++ b/src/renderer/renderer.h @@ -2,15 +2,19 @@ #include "base/base_core.h" -struct Renderer; +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct Renderer Renderer; struct Clay_RenderCommandArray; -struct RendererDesc { - void *window_handle = nullptr; - S32 width = 1280; - S32 height = 720; - S32 frame_count = 2; -}; +typedef struct RendererDesc { + void *window_handle; + S32 width; + S32 height; + S32 frame_count; +} RendererDesc; Renderer *renderer_create(RendererDesc *desc); @@ -31,8 +35,11 @@ void renderer_set_clear_color(Renderer *renderer, F32 r, F32 g, F32 b); // Text measurement callback compatible with UI_MeasureTextFn // Measures text of given length (NOT necessarily null-terminated) at font_size pixels. // user_data should be the Renderer pointer. -struct Vec2F32; Vec2F32 renderer_measure_text(const char *text, S32 length, F32 font_size, void *user_data); // Upload an RGBA8 icon atlas texture for icon rendering (4 bytes per pixel) void renderer_create_icon_atlas(Renderer *renderer, const U8 *data, S32 w, S32 h); + +#ifdef __cplusplus +} +#endif diff --git a/src/renderer/renderer_dx12.cpp b/src/renderer/renderer_dx12.cpp deleted file mode 100644 index bfbecf2..0000000 --- a/src/renderer/renderer_dx12.cpp +++ /dev/null @@ -1,1548 +0,0 @@ -#include "renderer/renderer.h" -#include "ui/ui_core.h" -#include "ui/ui_icons.h" - -#include -#include -#include -#include - -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#include - -// FreeType headers — temporarily undefine `internal` macro (base_core.h: #define internal static) -// because FreeType uses `internal` as a struct field name. -#undef internal -#include -#include FT_FREETYPE_H -#include FT_BITMAP_H -#define internal static -#include "renderer/font_inter.gen.h" - -#ifdef _DEBUG -#define DX12_ENABLE_DEBUG_LAYER -#endif - -#ifdef DX12_ENABLE_DEBUG_LAYER -#include -#pragma comment(lib, "dxguid.lib") -#endif - -#define NUM_BACK_BUFFERS 2 -#define MAX_VERTICES (64 * 1024) -#define MAX_INDICES (MAX_VERTICES * 3) - -// Font atlas -#define FONT_ATLAS_W 1024 -#define FONT_ATLAS_H 1024 -#define GLYPH_FIRST 32 -#define GLYPH_LAST 126 -#define GLYPH_COUNT (GLYPH_LAST - GLYPH_FIRST + 1) - -//////////////////////////////// -// Vertex format for 2D UI rendering -// mode: 0 = SDF rounded rect, 1 = textured glyph - -struct UIVertex { - float pos[2]; - float uv[2]; - float col[4]; - float rect_min[2]; - float rect_max[2]; - float corner_radii[4]; // TL, TR, BR, BL - float border_thickness; - float softness; - float mode; // 0 = rect SDF, 1 = textured -}; - -//////////////////////////////// -// Glyph info - -struct GlyphInfo { - F32 u0, v0, u1, v1; // UV coords in atlas - F32 w, h; // pixel size - F32 x_advance; // how far to move cursor -}; - -//////////////////////////////// -// Shaders (inline HLSL) - -static const char *g_shader_hlsl = R"( -struct VSInput { - float2 pos : POSITION; - float2 uv : TEXCOORD0; - float4 col : COLOR0; - float2 rect_min : TEXCOORD1; - float2 rect_max : TEXCOORD2; - float4 corner_radii : TEXCOORD3; - float border_thickness : TEXCOORD4; - float softness : TEXCOORD5; - float mode : TEXCOORD6; -}; - -struct PSInput { - float4 pos : SV_POSITION; - float2 uv : TEXCOORD0; - float4 col : COLOR0; - float2 rect_min : TEXCOORD1; - float2 rect_max : TEXCOORD2; - float4 corner_radii : TEXCOORD3; - float border_thickness : TEXCOORD4; - float softness : TEXCOORD5; - float mode : TEXCOORD6; -}; - -cbuffer ConstantBuffer : register(b0) { - float2 viewport_size; - float2 _padding; -}; - -Texture2D font_tex : register(t0); -SamplerState font_smp : register(s0); - -PSInput VSMain(VSInput input) { - PSInput output; - float2 ndc; - ndc.x = (input.pos.x / viewport_size.x) * 2.0 - 1.0; - ndc.y = 1.0 - (input.pos.y / viewport_size.y) * 2.0; - output.pos = float4(ndc, 0.0, 1.0); - output.uv = input.uv; - output.col = input.col; - output.rect_min = input.rect_min; - output.rect_max = input.rect_max; - output.corner_radii = input.corner_radii; - output.border_thickness = input.border_thickness; - output.softness = input.softness; - output.mode = input.mode; - return output; -} - -float rounded_rect_sdf(float2 sample_pos, float2 rect_center, float2 rect_half_size, float radius) { - float2 d = abs(sample_pos - rect_center) - rect_half_size + float2(radius, radius); - return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - radius; -} - -float4 PSMain(PSInput input) : SV_TARGET { - float4 col = input.col; - - if (input.mode > 1.5) { - // RGBA textured mode: sample all channels, multiply by vertex color - float4 tex = font_tex.Sample(font_smp, input.uv); - col *= tex; - } else if (input.mode > 0.5) { - // Alpha-only textured mode: sample R channel as alpha (font atlas) - float alpha = font_tex.Sample(font_smp, input.uv).r; - col.a *= alpha; - } else { - // SDF rounded rect mode - float2 pixel_pos = input.pos.xy; - float2 rect_center = (input.rect_min + input.rect_max) * 0.5; - float2 rect_half_size = (input.rect_max - input.rect_min) * 0.5; - // corner_radii = (TL, TR, BR, BL) — select radius by quadrant - float radius = (pixel_pos.x < rect_center.x) - ? ((pixel_pos.y < rect_center.y) ? input.corner_radii.x : input.corner_radii.w) - : ((pixel_pos.y < rect_center.y) ? input.corner_radii.y : input.corner_radii.z); - float softness = max(input.softness, 0.5); - float dist = rounded_rect_sdf(pixel_pos, rect_center, rect_half_size, radius); - - if (input.border_thickness > 0) { - float inner_dist = dist + input.border_thickness; - float outer_alpha = 1.0 - smoothstep(-softness, softness, dist); - float inner_alpha = smoothstep(-softness, softness, inner_dist); - col.a *= outer_alpha * inner_alpha; - } else { - col.a *= 1.0 - smoothstep(-softness, softness, dist); - } - } - - // Dither to reduce gradient banding (interleaved gradient noise) - float dither = frac(52.9829189 * frac(dot(input.pos.xy, float2(0.06711056, 0.00583715)))) - 0.5; - col.rgb += dither / 255.0; - - if (col.a < 0.002) discard; - return col; -} -)"; - -//////////////////////////////// -// Frame context - -struct FrameContext { - ID3D12CommandAllocator *command_allocator; - UINT64 fence_value; -}; - -//////////////////////////////// -// Renderer struct - -struct Renderer { - Renderer *parent; // non-null for shared renderers - HWND hwnd; - S32 width; - S32 height; - S32 frame_count; - UINT frame_index; - - ID3D12Device *device; - ID3D12CommandQueue *command_queue; - IDXGISwapChain3 *swap_chain; - HANDLE swap_chain_waitable; - B32 swap_chain_occluded; - B32 tearing_support; - - ID3D12DescriptorHeap *rtv_heap; - ID3D12DescriptorHeap *srv_heap; - - FrameContext frames[NUM_BACK_BUFFERS]; - ID3D12Resource *render_targets[NUM_BACK_BUFFERS]; - D3D12_CPU_DESCRIPTOR_HANDLE rtv_descriptors[NUM_BACK_BUFFERS]; - - ID3D12GraphicsCommandList *command_list; - ID3D12Fence *fence; - HANDLE fence_event; - UINT64 fence_last_signaled; - - // UI rendering pipeline - ID3D12RootSignature *root_signature; - ID3D12PipelineState *pipeline_state; - - // Per-frame vertex/index buffers (F64-buffered) - ID3D12Resource *vertex_buffers[NUM_BACK_BUFFERS]; - ID3D12Resource *index_buffers[NUM_BACK_BUFFERS]; - void *vb_mapped[NUM_BACK_BUFFERS]; - void *ib_mapped[NUM_BACK_BUFFERS]; - - // Font atlas - ID3D12Resource *font_texture; - GlyphInfo glyphs[GLYPH_COUNT]; - F32 font_atlas_size; // font size the atlas was built at - F32 font_line_height; - - // Icon atlas - ID3D12Resource *icon_texture; - ID3D12DescriptorHeap *icon_srv_heap; - - // FreeType - FT_Library ft_lib; - FT_Face ft_face; - - // Clear color - F32 clear_r = 0.12f; - F32 clear_g = 0.12f; - F32 clear_b = 0.13f; -}; - -//////////////////////////////// -// DX12 infrastructure - -static B32 create_device(Renderer *r) { -#ifdef DX12_ENABLE_DEBUG_LAYER - ID3D12Debug *debug = nullptr; - if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debug)))) { - debug->EnableDebugLayer(); - debug->Release(); - } -#endif - - if (D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&r->device)) != S_OK) - return false; - -#ifdef DX12_ENABLE_DEBUG_LAYER - { - ID3D12InfoQueue *info_queue = nullptr; - if (SUCCEEDED(r->device->QueryInterface(IID_PPV_ARGS(&info_queue)))) { - info_queue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_ERROR, true); - info_queue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_CORRUPTION, true); - info_queue->Release(); - } - } -#endif - - return true; -} - -static B32 create_command_queue(Renderer *r) { - D3D12_COMMAND_QUEUE_DESC desc = {}; - desc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; - desc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; - desc.NodeMask = 1; - return r->device->CreateCommandQueue(&desc, IID_PPV_ARGS(&r->command_queue)) == S_OK; -} - -static B32 create_descriptor_heaps(Renderer *r) { - // RTV heap - { - D3D12_DESCRIPTOR_HEAP_DESC desc = {}; - desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; - desc.NumDescriptors = NUM_BACK_BUFFERS; - desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; - desc.NodeMask = 1; - if (r->device->CreateDescriptorHeap(&desc, IID_PPV_ARGS(&r->rtv_heap)) != S_OK) - return false; - - SIZE_T rtv_size = r->device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); - D3D12_CPU_DESCRIPTOR_HANDLE handle = r->rtv_heap->GetCPUDescriptorHandleForHeapStart(); - for (S32 i = 0; i < NUM_BACK_BUFFERS; i++) { - r->rtv_descriptors[i] = handle; - handle.ptr += rtv_size; - } - } - - // SRV heap (for font texture) - { - D3D12_DESCRIPTOR_HEAP_DESC desc = {}; - desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; - desc.NumDescriptors = 1; - desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; - if (r->device->CreateDescriptorHeap(&desc, IID_PPV_ARGS(&r->srv_heap)) != S_OK) - return false; - } - - return true; -} - -static B32 create_frame_resources(Renderer *r) { - for (S32 i = 0; i < r->frame_count; i++) { - if (r->device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, - IID_PPV_ARGS(&r->frames[i].command_allocator)) != S_OK) - return false; - r->frames[i].fence_value = 0; - } - - if (r->device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, - r->frames[0].command_allocator, nullptr, IID_PPV_ARGS(&r->command_list)) != S_OK) - return false; - if (r->command_list->Close() != S_OK) - return false; - - if (r->device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&r->fence)) != S_OK) - return false; - - r->fence_event = CreateEventW(nullptr, FALSE, FALSE, nullptr); - return r->fence_event != nullptr; -} - -static B32 create_swap_chain(Renderer *r) { - DXGI_SWAP_CHAIN_DESC1 sd = {}; - sd.BufferCount = NUM_BACK_BUFFERS; - sd.Width = 0; - sd.Height = 0; - sd.Format = DXGI_FORMAT_R8G8B8A8_UNORM; - sd.Flags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT; - sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; - sd.SampleDesc.Count = 1; - sd.SampleDesc.Quality = 0; - sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; - sd.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED; - sd.Scaling = DXGI_SCALING_STRETCH; - sd.Stereo = FALSE; - - IDXGIFactory5 *factory = nullptr; - if (CreateDXGIFactory1(IID_PPV_ARGS(&factory)) != S_OK) - return false; - - BOOL allow_tearing = FALSE; - factory->CheckFeatureSupport(DXGI_FEATURE_PRESENT_ALLOW_TEARING, &allow_tearing, sizeof(allow_tearing)); - r->tearing_support = (allow_tearing == TRUE); - if (r->tearing_support) - sd.Flags |= DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING; - - IDXGISwapChain1 *swap_chain1 = nullptr; - if (factory->CreateSwapChainForHwnd(r->command_queue, r->hwnd, &sd, nullptr, nullptr, &swap_chain1) != S_OK) { - factory->Release(); - return false; - } - - if (swap_chain1->QueryInterface(IID_PPV_ARGS(&r->swap_chain)) != S_OK) { - swap_chain1->Release(); - factory->Release(); - return false; - } - - if (r->tearing_support) - factory->MakeWindowAssociation(r->hwnd, DXGI_MWA_NO_ALT_ENTER); - - swap_chain1->Release(); - factory->Release(); - - r->swap_chain->SetMaximumFrameLatency(NUM_BACK_BUFFERS); - r->swap_chain_waitable = r->swap_chain->GetFrameLatencyWaitableObject(); - return true; -} - -static void create_render_targets(Renderer *r) { - for (UINT i = 0; i < NUM_BACK_BUFFERS; i++) { - ID3D12Resource *back_buffer = nullptr; - r->swap_chain->GetBuffer(i, IID_PPV_ARGS(&back_buffer)); - r->device->CreateRenderTargetView(back_buffer, nullptr, r->rtv_descriptors[i]); - r->render_targets[i] = back_buffer; - } -} - -static void cleanup_render_targets(Renderer *r) { - for (UINT i = 0; i < NUM_BACK_BUFFERS; i++) { - if (r->render_targets[i]) { - r->render_targets[i]->Release(); - r->render_targets[i] = nullptr; - } - } -} - -static void wait_for_pending(Renderer *r) { - r->command_queue->Signal(r->fence, ++r->fence_last_signaled); - r->fence->SetEventOnCompletion(r->fence_last_signaled, r->fence_event); - WaitForSingleObject(r->fence_event, INFINITE); -} - -static FrameContext *wait_for_next_frame(Renderer *r) { - FrameContext *fc = &r->frames[r->frame_index % r->frame_count]; - if (r->fence->GetCompletedValue() < fc->fence_value) { - r->fence->SetEventOnCompletion(fc->fence_value, r->fence_event); - HANDLE waitables[] = { r->swap_chain_waitable, r->fence_event }; - WaitForMultipleObjects(2, waitables, TRUE, INFINITE); - } else { - WaitForSingleObject(r->swap_chain_waitable, INFINITE); - } - return fc; -} - -//////////////////////////////// -// Font atlas (FreeType) - -static void init_freetype(Renderer *r) { - FT_Init_FreeType(&r->ft_lib); - FT_New_Memory_Face(r->ft_lib, font_inter_data, font_inter_size, 0, &r->ft_face); -} - -static B32 create_font_atlas(Renderer *r, F32 font_size) { - S32 pixel_size = (S32)(font_size + 0.5f); - r->font_atlas_size = font_size; - - FT_Set_Pixel_Sizes(r->ft_face, 0, pixel_size); - r->font_line_height = (F32)(r->ft_face->size->metrics.height >> 6); - - U8 *atlas_data = (U8 *)calloc(1, FONT_ATLAS_W * FONT_ATLAS_H); - - S32 pen_x = 1, pen_y = 1; - S32 row_height = 0; - F32 ascender = (F32)(r->ft_face->size->metrics.ascender >> 6); - - for (S32 i = 0; i < GLYPH_COUNT; i++) { - char ch = (char)(GLYPH_FIRST + i); - if (FT_Load_Char(r->ft_face, ch, FT_LOAD_RENDER)) continue; - - FT_GlyphSlot g = r->ft_face->glyph; - S32 bw = (S32)g->bitmap.width; - S32 bh = (S32)g->bitmap.rows; - S32 pad = 2; - S32 cell_w = bw + pad; - S32 cell_h = (S32)r->font_line_height + pad; - - if (pen_x + cell_w >= FONT_ATLAS_W) { - pen_x = 1; - pen_y += row_height + 1; - row_height = 0; - } - if (pen_y + cell_h >= FONT_ATLAS_H) break; - - // Copy glyph bitmap into atlas at baseline-relative position - S32 y_off = (S32)ascender - g->bitmap_top; - for (S32 y = 0; y < bh; y++) { - S32 dst_y = pen_y + y_off + y; - if (dst_y < 0 || dst_y >= FONT_ATLAS_H) continue; - for (S32 x = 0; x < bw; x++) { - S32 dst_x = pen_x + g->bitmap_left + x; - if (dst_x < 0 || dst_x >= FONT_ATLAS_W) continue; - atlas_data[dst_y * FONT_ATLAS_W + dst_x] = g->bitmap.buffer[y * g->bitmap.pitch + x]; - } - } - - r->glyphs[i].u0 = (F32)pen_x / (F32)FONT_ATLAS_W; - r->glyphs[i].v0 = (F32)pen_y / (F32)FONT_ATLAS_H; - r->glyphs[i].u1 = (F32)(pen_x + cell_w) / (F32)FONT_ATLAS_W; - r->glyphs[i].v1 = (F32)(pen_y + cell_h) / (F32)FONT_ATLAS_H; - r->glyphs[i].w = (F32)cell_w; - r->glyphs[i].h = (F32)cell_h; - r->glyphs[i].x_advance = (F32)(g->advance.x >> 6); - - if (cell_h > row_height) row_height = cell_h; - pen_x += cell_w + 1; - } - - // Create D3D12 texture - D3D12_HEAP_PROPERTIES heap_props = {}; - heap_props.Type = D3D12_HEAP_TYPE_DEFAULT; - - D3D12_RESOURCE_DESC tex_desc = {}; - tex_desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; - tex_desc.Width = FONT_ATLAS_W; - tex_desc.Height = FONT_ATLAS_H; - tex_desc.DepthOrArraySize = 1; - tex_desc.MipLevels = 1; - tex_desc.Format = DXGI_FORMAT_R8_UNORM; - tex_desc.SampleDesc.Count = 1; - tex_desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; - - if (r->device->CreateCommittedResource(&heap_props, D3D12_HEAP_FLAG_NONE, - &tex_desc, D3D12_RESOURCE_STATE_COPY_DEST, nullptr, - IID_PPV_ARGS(&r->font_texture)) != S_OK) { - free(atlas_data); - return false; - } - - // Upload via staging buffer - D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint = {}; - UINT64 total_bytes = 0; - r->device->GetCopyableFootprints(&tex_desc, 0, 1, 0, &footprint, nullptr, nullptr, &total_bytes); - - D3D12_HEAP_PROPERTIES upload_heap = {}; - upload_heap.Type = D3D12_HEAP_TYPE_UPLOAD; - - D3D12_RESOURCE_DESC upload_desc = {}; - upload_desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; - upload_desc.Width = total_bytes; - upload_desc.Height = 1; - upload_desc.DepthOrArraySize = 1; - upload_desc.MipLevels = 1; - upload_desc.SampleDesc.Count = 1; - upload_desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; - - ID3D12Resource *upload_buf = nullptr; - r->device->CreateCommittedResource(&upload_heap, D3D12_HEAP_FLAG_NONE, - &upload_desc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, - IID_PPV_ARGS(&upload_buf)); - - // Copy atlas data into upload buffer - void *mapped = nullptr; - D3D12_RANGE read_range = {0, 0}; - upload_buf->Map(0, &read_range, &mapped); - U8 *dst = (U8 *)mapped; - for (S32 y = 0; y < FONT_ATLAS_H; y++) { - memcpy(dst + y * footprint.Footprint.RowPitch, - atlas_data + y * FONT_ATLAS_W, - FONT_ATLAS_W); - } - upload_buf->Unmap(0, nullptr); - - // Record copy command - r->frames[0].command_allocator->Reset(); - r->command_list->Reset(r->frames[0].command_allocator, nullptr); - - D3D12_TEXTURE_COPY_LOCATION src_loc = {}; - src_loc.pResource = upload_buf; - src_loc.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; - src_loc.PlacedFootprint = footprint; - - D3D12_TEXTURE_COPY_LOCATION dst_loc = {}; - dst_loc.pResource = r->font_texture; - dst_loc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; - dst_loc.SubresourceIndex = 0; - - r->command_list->CopyTextureRegion(&dst_loc, 0, 0, 0, &src_loc, nullptr); - - // Transition to shader resource - D3D12_RESOURCE_BARRIER barrier = {}; - barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; - barrier.Transition.pResource = r->font_texture; - barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST; - barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; - barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; - r->command_list->ResourceBarrier(1, &barrier); - - r->command_list->Close(); - r->command_queue->ExecuteCommandLists(1, (ID3D12CommandList *const *)&r->command_list); - wait_for_pending(r); - - upload_buf->Release(); - free(atlas_data); - - // Create SRV - D3D12_SHADER_RESOURCE_VIEW_DESC srv_desc = {}; - srv_desc.Format = DXGI_FORMAT_R8_UNORM; - srv_desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; - srv_desc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; - srv_desc.Texture2D.MipLevels = 1; - - r->device->CreateShaderResourceView(r->font_texture, - &srv_desc, r->srv_heap->GetCPUDescriptorHandleForHeapStart()); - - return true; -} - -//////////////////////////////// -// UI rendering pipeline setup - -static B32 create_ui_pipeline(Renderer *r) { - ID3DBlob *vs_blob = nullptr; - ID3DBlob *ps_blob = nullptr; - ID3DBlob *error_blob = nullptr; - - UINT compile_flags = 0; -#ifdef _DEBUG - compile_flags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; -#endif - - HRESULT hr = D3DCompile(g_shader_hlsl, strlen(g_shader_hlsl), "ui_shader", - nullptr, nullptr, "VSMain", "vs_5_0", compile_flags, 0, &vs_blob, &error_blob); - if (FAILED(hr)) { - if (error_blob) { - OutputDebugStringA((char *)error_blob->GetBufferPointer()); - error_blob->Release(); - } - return false; - } - - hr = D3DCompile(g_shader_hlsl, strlen(g_shader_hlsl), "ui_shader", - nullptr, nullptr, "PSMain", "ps_5_0", compile_flags, 0, &ps_blob, &error_blob); - if (FAILED(hr)) { - if (error_blob) { - OutputDebugStringA((char *)error_blob->GetBufferPointer()); - error_blob->Release(); - } - vs_blob->Release(); - return false; - } - - // Root signature: root constants + descriptor table for font texture + static sampler - D3D12_ROOT_PARAMETER root_params[2] = {}; - - // [0] Root constants: viewport_size - root_params[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS; - root_params[0].Constants.ShaderRegister = 0; - root_params[0].Constants.RegisterSpace = 0; - root_params[0].Constants.Num32BitValues = 4; - root_params[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX; - - // [1] Descriptor table: font texture SRV - D3D12_DESCRIPTOR_RANGE srv_range = {}; - srv_range.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV; - srv_range.NumDescriptors = 1; - srv_range.BaseShaderRegister = 0; - srv_range.RegisterSpace = 0; - srv_range.OffsetInDescriptorsFromTableStart = 0; - - root_params[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; - root_params[1].DescriptorTable.NumDescriptorRanges = 1; - root_params[1].DescriptorTable.pDescriptorRanges = &srv_range; - root_params[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; - - // Static sampler for font texture - D3D12_STATIC_SAMPLER_DESC sampler = {}; - sampler.Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR; - sampler.AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - sampler.AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - sampler.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - sampler.ShaderRegister = 0; - sampler.RegisterSpace = 0; - sampler.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; - - D3D12_ROOT_SIGNATURE_DESC rs_desc = {}; - rs_desc.NumParameters = 2; - rs_desc.pParameters = root_params; - rs_desc.NumStaticSamplers = 1; - rs_desc.pStaticSamplers = &sampler; - rs_desc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT; - - ID3DBlob *signature_blob = nullptr; - if (D3D12SerializeRootSignature(&rs_desc, D3D_ROOT_SIGNATURE_VERSION_1, &signature_blob, &error_blob) != S_OK) { - if (error_blob) error_blob->Release(); - vs_blob->Release(); - ps_blob->Release(); - return false; - } - - hr = r->device->CreateRootSignature(0, signature_blob->GetBufferPointer(), - signature_blob->GetBufferSize(), IID_PPV_ARGS(&r->root_signature)); - signature_blob->Release(); - if (FAILED(hr)) { - vs_blob->Release(); - ps_blob->Release(); - return false; - } - - // Input layout - D3D12_INPUT_ELEMENT_DESC input_layout[] = { - { "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, offsetof(UIVertex, pos), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, - { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, offsetof(UIVertex, uv), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, - { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, offsetof(UIVertex, col), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, - { "TEXCOORD", 1, DXGI_FORMAT_R32G32_FLOAT, 0, offsetof(UIVertex, rect_min), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, - { "TEXCOORD", 2, DXGI_FORMAT_R32G32_FLOAT, 0, offsetof(UIVertex, rect_max), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, - { "TEXCOORD", 3, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, offsetof(UIVertex, corner_radii), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, - { "TEXCOORD", 4, DXGI_FORMAT_R32_FLOAT, 0, offsetof(UIVertex, border_thickness), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, - { "TEXCOORD", 5, DXGI_FORMAT_R32_FLOAT, 0, offsetof(UIVertex, softness), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, - { "TEXCOORD", 6, DXGI_FORMAT_R32_FLOAT, 0, offsetof(UIVertex, mode), D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, - }; - - D3D12_GRAPHICS_PIPELINE_STATE_DESC pso_desc = {}; - pso_desc.InputLayout = { input_layout, _countof(input_layout) }; - pso_desc.pRootSignature = r->root_signature; - pso_desc.VS = { vs_blob->GetBufferPointer(), vs_blob->GetBufferSize() }; - pso_desc.PS = { ps_blob->GetBufferPointer(), ps_blob->GetBufferSize() }; - - pso_desc.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID; - pso_desc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; - pso_desc.RasterizerState.DepthClipEnable = TRUE; - - D3D12_RENDER_TARGET_BLEND_DESC rt_blend = {}; - rt_blend.BlendEnable = TRUE; - rt_blend.SrcBlend = D3D12_BLEND_SRC_ALPHA; - rt_blend.DestBlend = D3D12_BLEND_INV_SRC_ALPHA; - rt_blend.BlendOp = D3D12_BLEND_OP_ADD; - rt_blend.SrcBlendAlpha = D3D12_BLEND_ONE; - rt_blend.DestBlendAlpha = D3D12_BLEND_INV_SRC_ALPHA; - rt_blend.BlendOpAlpha = D3D12_BLEND_OP_ADD; - rt_blend.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL; - pso_desc.BlendState.RenderTarget[0] = rt_blend; - - pso_desc.DepthStencilState.DepthEnable = FALSE; - pso_desc.DepthStencilState.StencilEnable = FALSE; - pso_desc.SampleMask = UINT_MAX; - pso_desc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; - pso_desc.NumRenderTargets = 1; - pso_desc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; - pso_desc.SampleDesc.Count = 1; - - hr = r->device->CreateGraphicsPipelineState(&pso_desc, IID_PPV_ARGS(&r->pipeline_state)); - - vs_blob->Release(); - ps_blob->Release(); - - return SUCCEEDED(hr); -} - -static B32 create_ui_buffers(Renderer *r) { - D3D12_HEAP_PROPERTIES heap_props = {}; - heap_props.Type = D3D12_HEAP_TYPE_UPLOAD; - - for (S32 i = 0; i < NUM_BACK_BUFFERS; i++) { - D3D12_RESOURCE_DESC buf_desc = {}; - buf_desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; - buf_desc.Width = MAX_VERTICES * sizeof(UIVertex); - buf_desc.Height = 1; - buf_desc.DepthOrArraySize = 1; - buf_desc.MipLevels = 1; - buf_desc.SampleDesc.Count = 1; - buf_desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; - - if (r->device->CreateCommittedResource(&heap_props, D3D12_HEAP_FLAG_NONE, - &buf_desc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, - IID_PPV_ARGS(&r->vertex_buffers[i])) != S_OK) - return false; - - D3D12_RANGE read_range = {0, 0}; - r->vertex_buffers[i]->Map(0, &read_range, &r->vb_mapped[i]); - } - - for (S32 i = 0; i < NUM_BACK_BUFFERS; i++) { - D3D12_RESOURCE_DESC buf_desc = {}; - buf_desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; - buf_desc.Width = MAX_INDICES * sizeof(U32); - buf_desc.Height = 1; - buf_desc.DepthOrArraySize = 1; - buf_desc.MipLevels = 1; - buf_desc.SampleDesc.Count = 1; - buf_desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; - - if (r->device->CreateCommittedResource(&heap_props, D3D12_HEAP_FLAG_NONE, - &buf_desc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, - IID_PPV_ARGS(&r->index_buffers[i])) != S_OK) - return false; - - D3D12_RANGE read_range = {0, 0}; - r->index_buffers[i]->Map(0, &read_range, &r->ib_mapped[i]); - } - - return true; -} - -//////////////////////////////// -// Text measurement callback for UI system - -Vec2F32 renderer_measure_text(const char *text, S32 length, F32 font_size, void *user_data) { - Renderer *r = (Renderer *)user_data; - if (!r || length == 0) return v2f32(0, font_size); - - FT_Set_Pixel_Sizes(r->ft_face, 0, (FT_UInt)(font_size + 0.5f)); - - F32 width = 0; - for (S32 i = 0; i < length; i++) { - if (FT_Load_Char(r->ft_face, (FT_ULong)(unsigned char)text[i], FT_LOAD_DEFAULT)) continue; - width += (F32)(r->ft_face->glyph->advance.x >> 6); - } - F32 height = (F32)(r->ft_face->size->metrics.height >> 6); - return v2f32(width, height); -} - -//////////////////////////////// -// Quad emission helpers - -struct DrawBatch { - UIVertex *vertices; - U32 *indices; - U32 vertex_count; - U32 index_count; -}; - -static void emit_quad(DrawBatch *batch, - F32 x0, F32 y0, F32 x1, F32 y1, - F32 u0, F32 v0, F32 u1, F32 v1, - F32 cr, F32 cg, F32 cb, F32 ca, - F32 rmin_x, F32 rmin_y, F32 rmax_x, F32 rmax_y, - F32 cr_tl, F32 cr_tr, F32 cr_br, F32 cr_bl, - F32 border_thickness, F32 softness, F32 mode) -{ - if (batch->vertex_count + 4 > MAX_VERTICES || batch->index_count + 6 > MAX_INDICES) - return; - - U32 base = batch->vertex_count; - UIVertex *v = &batch->vertices[base]; - - // For SDF mode, expand quad slightly for anti-aliasing - F32 px0 = x0, py0 = y0, px1 = x1, py1 = y1; - if (mode < 0.5f) { - F32 pad = softness + 1.0f; - px0 -= pad; py0 -= pad; px1 += pad; py1 += pad; - } - - v[0].pos[0] = px0; v[0].pos[1] = py0; v[0].uv[0] = u0; v[0].uv[1] = v0; - v[1].pos[0] = px1; v[1].pos[1] = py0; v[1].uv[0] = u1; v[1].uv[1] = v0; - v[2].pos[0] = px1; v[2].pos[1] = py1; v[2].uv[0] = u1; v[2].uv[1] = v1; - v[3].pos[0] = px0; v[3].pos[1] = py1; v[3].uv[0] = u0; v[3].uv[1] = v1; - - for (S32 i = 0; i < 4; i++) { - v[i].col[0] = cr; v[i].col[1] = cg; v[i].col[2] = cb; v[i].col[3] = ca; - v[i].rect_min[0] = rmin_x; v[i].rect_min[1] = rmin_y; - v[i].rect_max[0] = rmax_x; v[i].rect_max[1] = rmax_y; - v[i].corner_radii[0] = cr_tl; v[i].corner_radii[1] = cr_tr; - v[i].corner_radii[2] = cr_br; v[i].corner_radii[3] = cr_bl; - v[i].border_thickness = border_thickness; - v[i].softness = softness; - v[i].mode = mode; - } - - U32 *idx = &batch->indices[batch->index_count]; - idx[0] = base; idx[1] = base + 1; idx[2] = base + 2; - idx[3] = base; idx[4] = base + 2; idx[5] = base + 3; - - batch->vertex_count += 4; - batch->index_count += 6; -} - -static void emit_quad_rotated(DrawBatch *batch, - F32 x0, F32 y0, F32 x1, F32 y1, - F32 u0, F32 v0, F32 u1, F32 v1, - F32 cr, F32 cg, F32 cb, F32 ca, - F32 angle_rad) -{ - if (batch->vertex_count + 4 > MAX_VERTICES || batch->index_count + 6 > MAX_INDICES) - return; - - U32 base = batch->vertex_count; - UIVertex *v = &batch->vertices[base]; - - F32 cx = (x0 + x1) * 0.5f; - F32 cy = (y0 + y1) * 0.5f; - F32 cosA = cosf(angle_rad); - F32 sinA = sinf(angle_rad); - - F32 dx0 = x0 - cx, dy0 = y0 - cy; - F32 dx1 = x1 - cx, dy1 = y1 - cy; - - v[0].pos[0] = cx + dx0 * cosA - dy0 * sinA; - v[0].pos[1] = cy + dx0 * sinA + dy0 * cosA; - v[0].uv[0] = u0; v[0].uv[1] = v0; - - v[1].pos[0] = cx + dx1 * cosA - dy0 * sinA; - v[1].pos[1] = cy + dx1 * sinA + dy0 * cosA; - v[1].uv[0] = u1; v[1].uv[1] = v0; - - v[2].pos[0] = cx + dx1 * cosA - dy1 * sinA; - v[2].pos[1] = cy + dx1 * sinA + dy1 * cosA; - v[2].uv[0] = u1; v[2].uv[1] = v1; - - v[3].pos[0] = cx + dx0 * cosA - dy1 * sinA; - v[3].pos[1] = cy + dx0 * sinA + dy1 * cosA; - v[3].uv[0] = u0; v[3].uv[1] = v1; - - for (S32 i = 0; i < 4; i++) { - v[i].col[0] = cr; v[i].col[1] = cg; v[i].col[2] = cb; v[i].col[3] = ca; - v[i].rect_min[0] = 0; v[i].rect_min[1] = 0; - v[i].rect_max[0] = 0; v[i].rect_max[1] = 0; - v[i].corner_radii[0] = 0; v[i].corner_radii[1] = 0; - v[i].corner_radii[2] = 0; v[i].corner_radii[3] = 0; - v[i].border_thickness = 0; - v[i].softness = 0; - v[i].mode = 2.0f; - } - - U32 *idx = &batch->indices[batch->index_count]; - idx[0] = base; idx[1] = base + 1; idx[2] = base + 2; - idx[3] = base; idx[4] = base + 2; idx[5] = base + 3; - - batch->vertex_count += 4; - batch->index_count += 6; -} - -static void emit_rect(DrawBatch *batch, - F32 x0, F32 y0, F32 x1, F32 y1, - F32 cr, F32 cg, F32 cb, F32 ca, - F32 cr_tl, F32 cr_tr, F32 cr_br, F32 cr_bl, - F32 border_thickness, F32 softness) -{ - emit_quad(batch, x0, y0, x1, y1, - 0, 0, 0, 0, - cr, cg, cb, ca, - x0, y0, x1, y1, - cr_tl, cr_tr, cr_br, cr_bl, - border_thickness, softness, 0.0f); -} - -static void emit_rect_vgradient(DrawBatch *batch, - F32 x0, F32 y0, F32 x1, F32 y1, - F32 tr, F32 tg, F32 tb, F32 ta, - F32 br, F32 bg, F32 bb_, F32 ba, - F32 cr_tl, F32 cr_tr, F32 cr_br, F32 cr_bl, - F32 softness) -{ - if (batch->vertex_count + 4 > MAX_VERTICES || batch->index_count + 6 > MAX_INDICES) - return; - - U32 base = batch->vertex_count; - UIVertex *v = &batch->vertices[base]; - - F32 pad = softness + 1.0f; - F32 px0 = x0 - pad, py0 = y0 - pad, px1 = x1 + pad, py1 = y1 + pad; - - v[0].pos[0] = px0; v[0].pos[1] = py0; v[0].uv[0] = 0; v[0].uv[1] = 0; - v[1].pos[0] = px1; v[1].pos[1] = py0; v[1].uv[0] = 0; v[1].uv[1] = 0; - v[2].pos[0] = px1; v[2].pos[1] = py1; v[2].uv[0] = 0; v[2].uv[1] = 0; - v[3].pos[0] = px0; v[3].pos[1] = py1; v[3].uv[0] = 0; v[3].uv[1] = 0; - - // Top vertices get top color, bottom vertices get bottom color - v[0].col[0] = tr; v[0].col[1] = tg; v[0].col[2] = tb; v[0].col[3] = ta; - v[1].col[0] = tr; v[1].col[1] = tg; v[1].col[2] = tb; v[1].col[3] = ta; - v[2].col[0] = br; v[2].col[1] = bg; v[2].col[2] = bb_; v[2].col[3] = ba; - v[3].col[0] = br; v[3].col[1] = bg; v[3].col[2] = bb_; v[3].col[3] = ba; - - for (S32 i = 0; i < 4; i++) { - v[i].rect_min[0] = x0; v[i].rect_min[1] = y0; - v[i].rect_max[0] = x1; v[i].rect_max[1] = y1; - v[i].corner_radii[0] = cr_tl; v[i].corner_radii[1] = cr_tr; - v[i].corner_radii[2] = cr_br; v[i].corner_radii[3] = cr_bl; - v[i].border_thickness = 0; - v[i].softness = softness; - v[i].mode = 0; - } - - U32 *idx = &batch->indices[batch->index_count]; - idx[0] = base; idx[1] = base + 1; idx[2] = base + 2; - idx[3] = base; idx[4] = base + 2; idx[5] = base + 3; - - batch->vertex_count += 4; - batch->index_count += 6; -} - -static void emit_text_glyphs(DrawBatch *batch, Renderer *r, - Clay_BoundingBox bbox, Clay_Color color, const char *text, S32 text_len, - U16 font_size) -{ - if (text_len == 0 || color.a < 0.1f) return; - - // Color is 0-255 in Clay convention, normalize to 0-1 - F32 cr = color.r / 255.f; - F32 cg = color.g / 255.f; - F32 cb = color.b / 255.f; - F32 ca = color.a / 255.f; - - F32 scale = (F32)font_size / r->font_atlas_size; - F32 text_h = r->font_line_height * scale; - - // Vertically center text in bounding box, snapped to pixel grid to avoid blurry glyphs - F32 x = floorf(bbox.x + 0.5f); - F32 y = floorf(bbox.y + (bbox.height - text_h) * 0.5f + 0.5f); - - for (S32 i = 0; i < text_len; i++) { - char ch = text[i]; - if (ch < GLYPH_FIRST || ch > GLYPH_LAST) { - if (ch == ' ') { - S32 gi = ' ' - GLYPH_FIRST; - if (gi >= 0 && gi < GLYPH_COUNT) - x += r->glyphs[gi].x_advance * scale; - continue; - } - ch = '?'; - } - S32 gi = ch - GLYPH_FIRST; - if (gi < 0 || gi >= GLYPH_COUNT) continue; - - GlyphInfo *g = &r->glyphs[gi]; - F32 gw = g->w * scale; - F32 gh = g->h * scale; - - emit_quad(batch, - x, y, x + gw, y + gh, - g->u0, g->v0, g->u1, g->v1, - cr, cg, cb, ca, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 1.0f); - - x += g->x_advance * scale; - } -} - -//////////////////////////////// -// Flush helper: issues a draw call for accumulated vertices, then resets batch - -static void flush_batch(Renderer *r, DrawBatch *batch, UINT buf_idx, U32 *flush_index_start, ID3D12DescriptorHeap *tex_heap = nullptr) { - U32 draw_index_count = batch->index_count - *flush_index_start; - if (draw_index_count == 0) return; - - r->command_list->SetPipelineState(r->pipeline_state); - r->command_list->SetGraphicsRootSignature(r->root_signature); - - F32 constants[4] = { (F32)r->width, (F32)r->height, 0, 0 }; - r->command_list->SetGraphicsRoot32BitConstants(0, 4, constants, 0); - - // Bind texture (font or icon) - ID3D12DescriptorHeap *heap = tex_heap ? tex_heap : r->srv_heap; - r->command_list->SetDescriptorHeaps(1, &heap); - r->command_list->SetGraphicsRootDescriptorTable(1, - heap->GetGPUDescriptorHandleForHeapStart()); - - r->command_list->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); - - D3D12_VERTEX_BUFFER_VIEW vbv = {}; - vbv.BufferLocation = r->vertex_buffers[buf_idx]->GetGPUVirtualAddress(); - vbv.SizeInBytes = batch->vertex_count * sizeof(UIVertex); - vbv.StrideInBytes = sizeof(UIVertex); - r->command_list->IASetVertexBuffers(0, 1, &vbv); - - D3D12_INDEX_BUFFER_VIEW ibv = {}; - ibv.BufferLocation = r->index_buffers[buf_idx]->GetGPUVirtualAddress(); - ibv.SizeInBytes = batch->index_count * sizeof(U32); - ibv.Format = DXGI_FORMAT_R32_UINT; - r->command_list->IASetIndexBuffer(&ibv); - - r->command_list->DrawIndexedInstanced(draw_index_count, 1, *flush_index_start, 0, 0); - - *flush_index_start = batch->index_count; -} - -//////////////////////////////// -// Public API - -Renderer *renderer_create(RendererDesc *desc) { - Renderer *r = new Renderer(); - memset(r, 0, sizeof(*r)); - - r->hwnd = (HWND)desc->window_handle; - r->width = desc->width; - r->height = desc->height; - r->frame_count = desc->frame_count; - if (r->frame_count > NUM_BACK_BUFFERS) r->frame_count = NUM_BACK_BUFFERS; - - if (!create_device(r)) goto fail; - if (!create_command_queue(r)) goto fail; - if (!create_descriptor_heaps(r)) goto fail; - if (!create_frame_resources(r)) goto fail; - if (!create_swap_chain(r)) goto fail; - create_render_targets(r); - - if (!create_ui_pipeline(r)) goto fail; - if (!create_ui_buffers(r)) goto fail; - - init_freetype(r); - if (!create_font_atlas(r, 22.0f)) goto fail; - - return r; - -fail: - renderer_destroy(r); - return nullptr; -} - -Renderer *renderer_create_shared(Renderer *parent, RendererDesc *desc) { - if (!parent) return nullptr; - - Renderer *r = new Renderer(); - memset(r, 0, sizeof(*r)); - - r->parent = parent; - r->hwnd = (HWND)desc->window_handle; - r->width = desc->width; - r->height = desc->height; - r->frame_count = desc->frame_count; - if (r->frame_count > NUM_BACK_BUFFERS) r->frame_count = NUM_BACK_BUFFERS; - - // Share from parent (not ref-counted — parent must outlive children) - r->device = parent->device; - r->command_queue = parent->command_queue; - r->root_signature = parent->root_signature; - r->pipeline_state = parent->pipeline_state; - r->font_texture = parent->font_texture; - r->icon_texture = parent->icon_texture; - r->icon_srv_heap = parent->icon_srv_heap; - r->srv_heap = parent->srv_heap; - r->ft_lib = parent->ft_lib; - r->ft_face = parent->ft_face; - memcpy(r->glyphs, parent->glyphs, sizeof(r->glyphs)); - r->font_atlas_size = parent->font_atlas_size; - r->font_line_height = parent->font_line_height; - - // Create own RTV heap (we don't need own SRV heap — use parent's) - { - D3D12_DESCRIPTOR_HEAP_DESC rtv_desc = {}; - rtv_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; - rtv_desc.NumDescriptors = NUM_BACK_BUFFERS; - rtv_desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; - rtv_desc.NodeMask = 1; - if (r->device->CreateDescriptorHeap(&rtv_desc, IID_PPV_ARGS(&r->rtv_heap)) != S_OK) - goto fail; - - SIZE_T rtv_size = r->device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); - D3D12_CPU_DESCRIPTOR_HANDLE handle = r->rtv_heap->GetCPUDescriptorHandleForHeapStart(); - for (S32 i = 0; i < NUM_BACK_BUFFERS; i++) { - r->rtv_descriptors[i] = handle; - handle.ptr += rtv_size; - } - } - - if (!create_frame_resources(r)) goto fail; - if (!create_swap_chain(r)) goto fail; - create_render_targets(r); - if (!create_ui_buffers(r)) goto fail; - - r->clear_r = parent->clear_r; - r->clear_g = parent->clear_g; - r->clear_b = parent->clear_b; - - return r; - -fail: - renderer_destroy(r); - return nullptr; -} - -void renderer_destroy(Renderer *r) { - if (!r) return; - - wait_for_pending(r); - - // Per-window resources (always owned) - for (S32 i = 0; i < NUM_BACK_BUFFERS; i++) { - if (r->vertex_buffers[i]) r->vertex_buffers[i]->Release(); - if (r->index_buffers[i]) r->index_buffers[i]->Release(); - } - - cleanup_render_targets(r); - - if (r->swap_chain) { r->swap_chain->SetFullscreenState(false, nullptr); r->swap_chain->Release(); } - if (r->swap_chain_waitable) CloseHandle(r->swap_chain_waitable); - for (S32 i = 0; i < r->frame_count; i++) - if (r->frames[i].command_allocator) r->frames[i].command_allocator->Release(); - if (r->command_list) r->command_list->Release(); - if (r->rtv_heap) r->rtv_heap->Release(); - if (r->fence) r->fence->Release(); - if (r->fence_event) CloseHandle(r->fence_event); - - // Shared resources (only freed by root renderer) - if (!r->parent) { - if (r->font_texture) r->font_texture->Release(); - if (r->icon_texture) r->icon_texture->Release(); - if (r->icon_srv_heap) r->icon_srv_heap->Release(); - if (r->pipeline_state) r->pipeline_state->Release(); - if (r->root_signature) r->root_signature->Release(); - if (r->srv_heap) r->srv_heap->Release(); - if (r->command_queue) r->command_queue->Release(); - if (r->device) r->device->Release(); - - if (r->ft_face) FT_Done_Face(r->ft_face); - if (r->ft_lib) FT_Done_FreeType(r->ft_lib); - -#ifdef DX12_ENABLE_DEBUG_LAYER - IDXGIDebug1 *dxgi_debug = nullptr; - if (SUCCEEDED(DXGIGetDebugInterface1(0, IID_PPV_ARGS(&dxgi_debug)))) { - dxgi_debug->ReportLiveObjects(DXGI_DEBUG_ALL, DXGI_DEBUG_RLO_SUMMARY); - dxgi_debug->Release(); - } -#endif - } - - delete r; -} - -B32 renderer_begin_frame(Renderer *r) { - // Sync shared resources from parent (font atlas may have been rebuilt) - if (r->parent) { - r->font_texture = r->parent->font_texture; - r->icon_texture = r->parent->icon_texture; - r->icon_srv_heap = r->parent->icon_srv_heap; - r->srv_heap = r->parent->srv_heap; - memcpy(r->glyphs, r->parent->glyphs, sizeof(r->glyphs)); - r->font_atlas_size = r->parent->font_atlas_size; - r->font_line_height = r->parent->font_line_height; - } - - if ((r->swap_chain_occluded && r->swap_chain->Present(0, DXGI_PRESENT_TEST) == DXGI_STATUS_OCCLUDED) - || IsIconic(r->hwnd)) - { - Sleep(10); - return false; - } - r->swap_chain_occluded = false; - return true; -} - -void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) { - FrameContext *fc = wait_for_next_frame(r); - UINT back_buffer_idx = r->swap_chain->GetCurrentBackBufferIndex(); - UINT buf_idx = r->frame_index % NUM_BACK_BUFFERS; - - fc->command_allocator->Reset(); - r->command_list->Reset(fc->command_allocator, nullptr); - - D3D12_RESOURCE_BARRIER barrier = {}; - barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; - barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; - barrier.Transition.pResource = r->render_targets[back_buffer_idx]; - barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; - barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT; - barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; - r->command_list->ResourceBarrier(1, &barrier); - - const F32 clear_color[4] = { r->clear_r, r->clear_g, r->clear_b, 1.0f }; - r->command_list->ClearRenderTargetView(r->rtv_descriptors[back_buffer_idx], clear_color, 0, nullptr); - r->command_list->OMSetRenderTargets(1, &r->rtv_descriptors[back_buffer_idx], FALSE, nullptr); - - D3D12_VIEWPORT viewport = {}; - viewport.Width = (FLOAT)r->width; - viewport.Height = (FLOAT)r->height; - viewport.MaxDepth = 1.0f; - r->command_list->RSSetViewports(1, &viewport); - - D3D12_RECT scissor = { 0, 0, (LONG)r->width, (LONG)r->height }; - r->command_list->RSSetScissorRects(1, &scissor); - - // Process Clay render commands - if (render_commands.length > 0) { - DrawBatch batch = {}; - batch.vertices = (UIVertex *)r->vb_mapped[buf_idx]; - batch.indices = (U32 *)r->ib_mapped[buf_idx]; - batch.vertex_count = 0; - batch.index_count = 0; - - // Track which texture is currently bound (0 = font, 1 = icon) - S32 bound_texture = 0; - U32 flush_index_start = 0; - - auto bind_font = [&]() { - if (bound_texture != 0) { - ID3D12DescriptorHeap *heap = bound_texture == 1 ? r->icon_srv_heap : r->srv_heap; - flush_batch(r, &batch, buf_idx, &flush_index_start, heap); - bound_texture = 0; - } - }; - - auto bind_icon = [&]() { - if (bound_texture != 1 && r->icon_srv_heap) { - flush_batch(r, &batch, buf_idx, &flush_index_start, r->srv_heap); - bound_texture = 1; - } - }; - - for (S32 i = 0; i < render_commands.length; i++) { - Clay_RenderCommand *cmd = Clay_RenderCommandArray_Get(&render_commands, i); - Clay_BoundingBox bb = cmd->boundingBox; - - switch (cmd->commandType) { - case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: { - Clay_RectangleRenderData *rect = &cmd->renderData.rectangle; - Clay_Color c = rect->backgroundColor; - emit_rect(&batch, - bb.x, bb.y, bb.x + bb.width, bb.y + bb.height, - c.r / 255.f, c.g / 255.f, c.b / 255.f, c.a / 255.f, - rect->cornerRadius.topLeft, rect->cornerRadius.topRight, - rect->cornerRadius.bottomRight, rect->cornerRadius.bottomLeft, - 0, 1.0f); - } break; - - case CLAY_RENDER_COMMAND_TYPE_BORDER: { - Clay_BorderRenderData *border = &cmd->renderData.border; - Clay_Color c = border->color; - F32 cr_norm = c.r / 255.f; - F32 cg_norm = c.g / 255.f; - F32 cb_norm = c.b / 255.f; - F32 ca_norm = c.a / 255.f; - - // Use SDF rounded border when corner radius is present and widths are uniform - Clay_CornerRadius cr = border->cornerRadius; - B32 has_radius = cr.topLeft > 0 || cr.topRight > 0 || cr.bottomLeft > 0 || cr.bottomRight > 0; - B32 uniform = border->width.top == border->width.bottom && - border->width.top == border->width.left && - border->width.top == border->width.right && - border->width.top > 0; - - if (has_radius && uniform) { - emit_rect(&batch, - bb.x, bb.y, bb.x + bb.width, bb.y + bb.height, - cr_norm, cg_norm, cb_norm, ca_norm, - cr.topLeft, cr.topRight, cr.bottomRight, cr.bottomLeft, - (F32)border->width.top, 1.0f); - } else { - if (border->width.top > 0) { - emit_rect(&batch, bb.x, bb.y, bb.x + bb.width, bb.y + border->width.top, - cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f); - } - if (border->width.bottom > 0) { - emit_rect(&batch, bb.x, bb.y + bb.height - border->width.bottom, bb.x + bb.width, bb.y + bb.height, - cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f); - } - if (border->width.left > 0) { - emit_rect(&batch, bb.x, bb.y, bb.x + border->width.left, bb.y + bb.height, - cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f); - } - if (border->width.right > 0) { - emit_rect(&batch, bb.x + bb.width - border->width.right, bb.y, bb.x + bb.width, bb.y + bb.height, - cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f); - } - } - } break; - - case CLAY_RENDER_COMMAND_TYPE_TEXT: { - bind_font(); - Clay_TextRenderData *text = &cmd->renderData.text; - emit_text_glyphs(&batch, r, bb, text->textColor, - text->stringContents.chars, text->stringContents.length, - text->fontSize); - } break; - - case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: { - // Flush before changing scissor - ID3D12DescriptorHeap *heap = bound_texture == 1 ? r->icon_srv_heap : r->srv_heap; - flush_batch(r, &batch, buf_idx, &flush_index_start, heap); - D3D12_RECT clip = {}; - clip.left = (LONG)Max(bb.x, 0.f); - clip.top = (LONG)Max(bb.y, 0.f); - clip.right = (LONG)Min(bb.x + bb.width, (F32)r->width); - clip.bottom = (LONG)Min(bb.y + bb.height, (F32)r->height); - if (clip.right <= clip.left) clip.right = clip.left + 1; - if (clip.bottom <= clip.top) clip.bottom = clip.top + 1; - r->command_list->RSSetScissorRects(1, &clip); - } break; - - case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: { - ID3D12DescriptorHeap *heap = bound_texture == 1 ? r->icon_srv_heap : r->srv_heap; - flush_batch(r, &batch, buf_idx, &flush_index_start, heap); - D3D12_RECT full_scissor = { 0, 0, (LONG)r->width, (LONG)r->height }; - r->command_list->RSSetScissorRects(1, &full_scissor); - } break; - - case CLAY_RENDER_COMMAND_TYPE_CUSTOM: { - Clay_CustomRenderData *custom = &cmd->renderData.custom; - if (custom->customData) { - CustomRenderType type = *(CustomRenderType *)custom->customData; - if (type == CUSTOM_RENDER_VGRADIENT) { - bind_font(); - CustomGradientData *grad = (CustomGradientData *)custom->customData; - Clay_Color tc = grad->top_color; - Clay_Color bc = grad->bottom_color; - emit_rect_vgradient(&batch, - bb.x, bb.y, bb.x + bb.width, bb.y + bb.height, - tc.r / 255.f, tc.g / 255.f, tc.b / 255.f, tc.a / 255.f, - bc.r / 255.f, bc.g / 255.f, bc.b / 255.f, bc.a / 255.f, - custom->cornerRadius.topLeft, custom->cornerRadius.topRight, - custom->cornerRadius.bottomRight, custom->cornerRadius.bottomLeft, - 1.0f); - } else if (type == CUSTOM_RENDER_ICON) { - bind_icon(); - CustomIconData *icon = (CustomIconData *)custom->customData; - Clay_Color c = icon->color; - F32 cr = c.r / 255.f, cg = c.g / 255.f; - F32 cb = c.b / 255.f, ca = c.a / 255.f; - UI_IconInfo *info = &g_icons[icon->icon_id]; - emit_quad(&batch, - bb.x, bb.y, bb.x + bb.width, bb.y + bb.height, - info->u0, info->v0, info->u1, info->v1, - cr, cg, cb, ca, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 2.0f); - } else if (type == CUSTOM_RENDER_ROTATED_ICON) { - bind_icon(); - CustomRotatedIconData *ri = (CustomRotatedIconData *)custom->customData; - Clay_Color c = ri->color; - F32 cr = c.r / 255.f, cg = c.g / 255.f; - F32 cb = c.b / 255.f, ca = c.a / 255.f; - UI_IconInfo *info = &g_icons[ri->icon_id]; - emit_quad_rotated(&batch, - bb.x, bb.y, bb.x + bb.width, bb.y + bb.height, - info->u0, info->v0, info->u1, info->v1, - cr, cg, cb, ca, - ri->angle_rad); - } - } - } break; - - case CLAY_RENDER_COMMAND_TYPE_IMAGE: - default: - break; - } - } - - // Flush remaining - ID3D12DescriptorHeap *heap = bound_texture == 1 ? r->icon_srv_heap : r->srv_heap; - flush_batch(r, &batch, buf_idx, &flush_index_start, heap); - } - - barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; - barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT; - r->command_list->ResourceBarrier(1, &barrier); - r->command_list->Close(); - - r->command_queue->ExecuteCommandLists(1, (ID3D12CommandList *const *)&r->command_list); - r->command_queue->Signal(r->fence, ++r->fence_last_signaled); - fc->fence_value = r->fence_last_signaled; - - HRESULT hr = r->swap_chain->Present(1, 0); - r->swap_chain_occluded = (hr == DXGI_STATUS_OCCLUDED); - r->frame_index++; -} - -void renderer_create_icon_atlas(Renderer *r, const U8 *data, S32 w, S32 h) { - // Create texture resource - D3D12_HEAP_PROPERTIES heap_props = {}; - heap_props.Type = D3D12_HEAP_TYPE_DEFAULT; - - D3D12_RESOURCE_DESC tex_desc = {}; - tex_desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; - tex_desc.Width = w; - tex_desc.Height = h; - tex_desc.DepthOrArraySize = 1; - tex_desc.MipLevels = 1; - tex_desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; - tex_desc.SampleDesc.Count = 1; - tex_desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; - - r->device->CreateCommittedResource(&heap_props, D3D12_HEAP_FLAG_NONE, - &tex_desc, D3D12_RESOURCE_STATE_COPY_DEST, nullptr, - IID_PPV_ARGS(&r->icon_texture)); - - // Upload via staging buffer - D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint = {}; - UINT64 total_bytes = 0; - r->device->GetCopyableFootprints(&tex_desc, 0, 1, 0, &footprint, nullptr, nullptr, &total_bytes); - - D3D12_HEAP_PROPERTIES upload_heap = {}; - upload_heap.Type = D3D12_HEAP_TYPE_UPLOAD; - - D3D12_RESOURCE_DESC upload_desc = {}; - upload_desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; - upload_desc.Width = total_bytes; - upload_desc.Height = 1; - upload_desc.DepthOrArraySize = 1; - upload_desc.MipLevels = 1; - upload_desc.SampleDesc.Count = 1; - upload_desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; - - ID3D12Resource *upload_buf = nullptr; - r->device->CreateCommittedResource(&upload_heap, D3D12_HEAP_FLAG_NONE, - &upload_desc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, - IID_PPV_ARGS(&upload_buf)); - - void *mapped = nullptr; - D3D12_RANGE read_range = {0, 0}; - upload_buf->Map(0, &read_range, &mapped); - U8 *dst = (U8 *)mapped; - for (S32 y = 0; y < h; y++) { - memcpy(dst + y * footprint.Footprint.RowPitch, data + y * w * 4, w * 4); - } - upload_buf->Unmap(0, nullptr); - - r->frames[0].command_allocator->Reset(); - r->command_list->Reset(r->frames[0].command_allocator, nullptr); - - D3D12_TEXTURE_COPY_LOCATION src_loc = {}; - src_loc.pResource = upload_buf; - src_loc.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; - src_loc.PlacedFootprint = footprint; - - D3D12_TEXTURE_COPY_LOCATION dst_loc = {}; - dst_loc.pResource = r->icon_texture; - dst_loc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; - dst_loc.SubresourceIndex = 0; - - r->command_list->CopyTextureRegion(&dst_loc, 0, 0, 0, &src_loc, nullptr); - - D3D12_RESOURCE_BARRIER barrier = {}; - barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; - barrier.Transition.pResource = r->icon_texture; - barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST; - barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; - barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; - r->command_list->ResourceBarrier(1, &barrier); - - r->command_list->Close(); - r->command_queue->ExecuteCommandLists(1, (ID3D12CommandList *const *)&r->command_list); - wait_for_pending(r); - upload_buf->Release(); - - // Create separate SRV heap for icon texture - D3D12_DESCRIPTOR_HEAP_DESC srv_desc = {}; - srv_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; - srv_desc.NumDescriptors = 1; - srv_desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; - r->device->CreateDescriptorHeap(&srv_desc, IID_PPV_ARGS(&r->icon_srv_heap)); - - D3D12_SHADER_RESOURCE_VIEW_DESC srv_view = {}; - srv_view.Format = DXGI_FORMAT_R8G8B8A8_UNORM; - srv_view.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; - srv_view.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; - srv_view.Texture2D.MipLevels = 1; - - r->device->CreateShaderResourceView(r->icon_texture, - &srv_view, r->icon_srv_heap->GetCPUDescriptorHandleForHeapStart()); -} - -void renderer_resize(Renderer *r, S32 width, S32 height) { - if (width <= 0 || height <= 0) return; - - wait_for_pending(r); - cleanup_render_targets(r); - - DXGI_SWAP_CHAIN_DESC1 desc = {}; - r->swap_chain->GetDesc1(&desc); - r->swap_chain->ResizeBuffers(0, (UINT)width, (UINT)height, desc.Format, desc.Flags); - - create_render_targets(r); - - r->width = width; - r->height = height; -} - -void renderer_set_clear_color(Renderer *r, F32 cr, F32 cg, F32 cb) { - r->clear_r = cr; - r->clear_g = cg; - r->clear_b = cb; -} - -void renderer_set_font_scale(Renderer *r, F32 scale) { - // Build atlas at the largest font size used in the UI (22px clock display) - // so all smaller sizes scale down (crisper) rather than up (blurry). - F32 target_size = 22.0f * scale; - if (fabsf(target_size - r->font_atlas_size) < 0.1f) return; - wait_for_pending(r); - if (r->font_texture) { r->font_texture->Release(); r->font_texture = nullptr; } - create_font_atlas(r, target_size); -} - -void renderer_sync_from_parent(Renderer *r) { - if (!r || !r->parent) return; - Renderer *p = r->parent; - r->font_texture = p->font_texture; - r->font_atlas_size = p->font_atlas_size; - r->font_line_height = p->font_line_height; - memcpy(r->glyphs, p->glyphs, sizeof(r->glyphs)); -} diff --git a/src/renderer/renderer_vulkan.cpp b/src/renderer/renderer_vulkan.cpp new file mode 100644 index 0000000..ad74960 --- /dev/null +++ b/src/renderer/renderer_vulkan.cpp @@ -0,0 +1,1772 @@ +#include "renderer/renderer.h" +#include "ui/ui_core.h" +#include "ui/ui_icons.h" + +#include +#include +#include + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +#define VK_USE_PLATFORM_WIN32_KHR +#include + +// VMA — single-file implementation in this translation unit +#define VMA_IMPLEMENTATION +#define VMA_STATIC_VULKAN_FUNCTIONS 1 +#define VMA_DYNAMIC_VULKAN_FUNCTIONS 0 +#include + +// FreeType headers — temporarily undefine `internal` macro (base_core.h: #define internal static) +// because FreeType uses `internal` as a struct field name. +#undef internal +#include +#include FT_FREETYPE_H +#include FT_BITMAP_H +#define internal static +#include "renderer/font_inter.gen.h" + +// Embedded SPIR-V shaders (generated at build time by glslc) +#include "renderer/ui_vert.spv.h" +#include "renderer/ui_frag.spv.h" + +#define NUM_BACK_BUFFERS 2 +#define MAX_VERTICES (64 * 1024) +#define MAX_INDICES (MAX_VERTICES * 3) + +// Font atlas +#define FONT_ATLAS_W 1024 +#define FONT_ATLAS_H 1024 +#define GLYPH_FIRST 32 +#define GLYPH_LAST 126 +#define GLYPH_COUNT (GLYPH_LAST - GLYPH_FIRST + 1) + +//////////////////////////////// +// Vertex format for 2D UI rendering +// mode: 0 = SDF rounded rect, 1 = textured glyph, 2 = RGBA icon + +typedef struct UIVertex { + float pos[2]; + float uv[2]; + float col[4]; + float rect_min[2]; + float rect_max[2]; + float corner_radii[4]; // TL, TR, BR, BL + float border_thickness; + float softness; + float mode; +} UIVertex; + +//////////////////////////////// +// Glyph info + +typedef struct GlyphInfo { + F32 u0, v0, u1, v1; + F32 w, h; + F32 x_advance; +} GlyphInfo; + +//////////////////////////////// +// Frame context + +typedef struct FrameContext { + VkCommandBuffer command_buffer; + VkFence fence; + U64 fence_value; +} FrameContext; + +//////////////////////////////// +// Renderer struct + +struct Renderer { + Renderer *parent; + HWND hwnd; + S32 width; + S32 height; + S32 frame_count; + U32 frame_index; + + // Vulkan core + VkInstance instance; + VkPhysicalDevice physical_device; + VkDevice device; + VkQueue graphics_queue; + U32 queue_family; + VmaAllocator allocator; + + // Surface & swap chain + VkSurfaceKHR surface; + VkSwapchainKHR swap_chain; + VkFormat swap_chain_format; + VkImage swap_chain_images[NUM_BACK_BUFFERS]; + VkImageView swap_chain_views[NUM_BACK_BUFFERS]; + VkFramebuffer framebuffers[NUM_BACK_BUFFERS]; + B32 swap_chain_occluded; + + // Render pass + VkRenderPass render_pass; + + // Command pool & per-frame resources + VkCommandPool command_pool; + FrameContext frames[NUM_BACK_BUFFERS]; + + // Synchronization + VkSemaphore image_available_sema[NUM_BACK_BUFFERS]; + VkSemaphore render_finished_sema[NUM_BACK_BUFFERS]; + + // Pipeline + VkPipelineLayout pipeline_layout; + VkPipeline pipeline; + VkDescriptorSetLayout descriptor_set_layout; + + // Descriptor pool & sets + VkDescriptorPool descriptor_pool; + VkDescriptorSet font_descriptor_set; + VkDescriptorSet icon_descriptor_set; + VkSampler sampler; + + // Per-frame vertex/index buffers (double-buffered, persistently mapped) + VkBuffer vertex_buffers[NUM_BACK_BUFFERS]; + VmaAllocation vertex_allocs[NUM_BACK_BUFFERS]; + void *vb_mapped[NUM_BACK_BUFFERS]; + + VkBuffer index_buffers[NUM_BACK_BUFFERS]; + VmaAllocation index_allocs[NUM_BACK_BUFFERS]; + void *ib_mapped[NUM_BACK_BUFFERS]; + + // Font atlas + VkImage font_image; + VmaAllocation font_alloc; + VkImageView font_view; + GlyphInfo glyphs[GLYPH_COUNT]; + F32 font_atlas_size; + F32 font_line_height; + + // Icon atlas + VkImage icon_image; + VmaAllocation icon_alloc; + VkImageView icon_view; + + // FreeType + FT_Library ft_lib; + FT_Face ft_face; + + // Clear color + F32 clear_r; + F32 clear_g; + F32 clear_b; + +#ifdef _DEBUG + VkDebugUtilsMessengerEXT debug_messenger; +#endif +}; + +//////////////////////////////// +// Debug callback + +#ifdef _DEBUG +static VKAPI_ATTR VkBool32 VKAPI_CALL vk_debug_callback( + VkDebugUtilsMessageSeverityFlagBitsEXT severity, + VkDebugUtilsMessageTypeFlagsEXT type, + const VkDebugUtilsMessengerCallbackDataEXT *data, + void *user_data) +{ + (void)type; (void)user_data; + if (severity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { + OutputDebugStringA("VK: "); + OutputDebugStringA(data->pMessage); + OutputDebugStringA("\n"); + } + return VK_FALSE; +} +#endif + +//////////////////////////////// +// Helper: begin/end one-shot command buffer + +static VkCommandBuffer begin_one_shot(Renderer *r) { + VkCommandBufferAllocateInfo ai = {0}; + ai.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + ai.commandPool = r->command_pool; + ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + ai.commandBufferCount = 1; + VkCommandBuffer cb; + vkAllocateCommandBuffers(r->device, &ai, &cb); + + VkCommandBufferBeginInfo bi = {0}; + bi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cb, &bi); + return cb; +} + +static void end_one_shot(Renderer *r, VkCommandBuffer cb) { + vkEndCommandBuffer(cb); + VkSubmitInfo si = {0}; + si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + si.commandBufferCount = 1; + si.pCommandBuffers = &cb; + vkQueueSubmit(r->graphics_queue, 1, &si, VK_NULL_HANDLE); + vkQueueWaitIdle(r->graphics_queue); + vkFreeCommandBuffers(r->device, r->command_pool, 1, &cb); +} + +//////////////////////////////// +// Helper: transition image layout + +static void transition_image(VkCommandBuffer cb, VkImage image, + VkImageLayout old_layout, VkImageLayout new_layout, + VkAccessFlags src_access, VkAccessFlags dst_access, + VkPipelineStageFlags src_stage, VkPipelineStageFlags dst_stage) +{ + VkImageMemoryBarrier barrier = {0}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.oldLayout = old_layout; + barrier.newLayout = new_layout; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + barrier.srcAccessMask = src_access; + barrier.dstAccessMask = dst_access; + vkCmdPipelineBarrier(cb, src_stage, dst_stage, 0, 0, NULL, 0, NULL, 1, &barrier); +} + +//////////////////////////////// +// Vulkan infrastructure + +static B32 create_instance(Renderer *r) { + VkApplicationInfo app_info = {0}; + app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + app_info.pApplicationName = "autosample"; + app_info.applicationVersion = VK_MAKE_VERSION(0, 1, 0); + app_info.pEngineName = "autosample"; + app_info.engineVersion = VK_MAKE_VERSION(0, 1, 0); + app_info.apiVersion = VK_API_VERSION_1_2; + + const char *extensions[] = { + VK_KHR_SURFACE_EXTENSION_NAME, + VK_KHR_WIN32_SURFACE_EXTENSION_NAME, +#ifdef _DEBUG + VK_EXT_DEBUG_UTILS_EXTENSION_NAME, +#endif + }; + + const char *layers[] = { +#ifdef _DEBUG + "VK_LAYER_KHRONOS_validation", +#endif + }; + + VkInstanceCreateInfo ci = {0}; + ci.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + ci.pApplicationInfo = &app_info; + ci.enabledExtensionCount = ArrayCount(extensions); + ci.ppEnabledExtensionNames = extensions; + ci.enabledLayerCount = ArrayCount(layers); + ci.ppEnabledLayerNames = layers; + + if (vkCreateInstance(&ci, NULL, &r->instance) != VK_SUCCESS) + return 0; + +#ifdef _DEBUG + { + VkDebugUtilsMessengerCreateInfoEXT dbg = {0}; + dbg.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; + dbg.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT + | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; + dbg.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT + | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT + | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; + dbg.pfnUserCallback = vk_debug_callback; + + PFN_vkCreateDebugUtilsMessengerEXT func = + (PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(r->instance, "vkCreateDebugUtilsMessengerEXT"); + if (func) func(r->instance, &dbg, NULL, &r->debug_messenger); + } +#endif + + return 1; +} + +static B32 create_surface(Renderer *r) { + VkWin32SurfaceCreateInfoKHR ci = {0}; + ci.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR; + ci.hinstance = GetModuleHandle(NULL); + ci.hwnd = r->hwnd; + return vkCreateWin32SurfaceKHR(r->instance, &ci, NULL, &r->surface) == VK_SUCCESS; +} + +static B32 pick_physical_device(Renderer *r) { + U32 count = 0; + vkEnumeratePhysicalDevices(r->instance, &count, NULL); + if (count == 0) return 0; + + VkPhysicalDevice devices[16]; + if (count > 16) count = 16; + vkEnumeratePhysicalDevices(r->instance, &count, devices); + + // Prefer discrete GPU + for (U32 i = 0; i < count; i++) { + VkPhysicalDeviceProperties props; + vkGetPhysicalDeviceProperties(devices[i], &props); + if (props.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { + r->physical_device = devices[i]; + return 1; + } + } + r->physical_device = devices[0]; + return 1; +} + +static B32 find_queue_family(Renderer *r) { + U32 count = 0; + vkGetPhysicalDeviceQueueFamilyProperties(r->physical_device, &count, NULL); + VkQueueFamilyProperties props[64]; + if (count > 64) count = 64; + vkGetPhysicalDeviceQueueFamilyProperties(r->physical_device, &count, props); + + for (U32 i = 0; i < count; i++) { + VkBool32 present = VK_FALSE; + vkGetPhysicalDeviceSurfaceSupportKHR(r->physical_device, i, r->surface, &present); + if ((props[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && present) { + r->queue_family = i; + return 1; + } + } + return 0; +} + +static B32 create_device(Renderer *r) { + float priority = 1.0f; + VkDeviceQueueCreateInfo queue_ci = {0}; + queue_ci.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queue_ci.queueFamilyIndex = r->queue_family; + queue_ci.queueCount = 1; + queue_ci.pQueuePriorities = &priority; + + const char *extensions[] = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; + + VkDeviceCreateInfo ci = {0}; + ci.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + ci.queueCreateInfoCount = 1; + ci.pQueueCreateInfos = &queue_ci; + ci.enabledExtensionCount = 1; + ci.ppEnabledExtensionNames = extensions; + + if (vkCreateDevice(r->physical_device, &ci, NULL, &r->device) != VK_SUCCESS) + return 0; + + vkGetDeviceQueue(r->device, r->queue_family, 0, &r->graphics_queue); + return 1; +} + +static B32 create_vma(Renderer *r) { + VmaAllocatorCreateInfo ci = {0}; + ci.physicalDevice = r->physical_device; + ci.device = r->device; + ci.instance = r->instance; + ci.vulkanApiVersion = VK_API_VERSION_1_2; + return vmaCreateAllocator(&ci, &r->allocator) == VK_SUCCESS; +} + +static B32 create_swap_chain(Renderer *r) { + VkSurfaceCapabilitiesKHR caps; + vkGetPhysicalDeviceSurfaceCapabilitiesKHR(r->physical_device, r->surface, &caps); + + // Pick format + U32 fmt_count = 0; + vkGetPhysicalDeviceSurfaceFormatsKHR(r->physical_device, r->surface, &fmt_count, NULL); + VkSurfaceFormatKHR formats[64]; + if (fmt_count > 64) fmt_count = 64; + vkGetPhysicalDeviceSurfaceFormatsKHR(r->physical_device, r->surface, &fmt_count, formats); + + VkSurfaceFormatKHR chosen = formats[0]; + for (U32 i = 0; i < fmt_count; i++) { + if (formats[i].format == VK_FORMAT_B8G8R8A8_UNORM && + formats[i].colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { + chosen = formats[i]; + break; + } + } + r->swap_chain_format = chosen.format; + + // Pick present mode (mailbox preferred, fallback fifo) + U32 pm_count = 0; + vkGetPhysicalDeviceSurfacePresentModesKHR(r->physical_device, r->surface, &pm_count, NULL); + VkPresentModeKHR modes[16]; + if (pm_count > 16) pm_count = 16; + vkGetPhysicalDeviceSurfacePresentModesKHR(r->physical_device, r->surface, &pm_count, modes); + + VkPresentModeKHR present_mode = VK_PRESENT_MODE_FIFO_KHR; + for (U32 i = 0; i < pm_count; i++) { + if (modes[i] == VK_PRESENT_MODE_MAILBOX_KHR) { + present_mode = VK_PRESENT_MODE_MAILBOX_KHR; + break; + } + } + + VkExtent2D extent; + if (caps.currentExtent.width != 0xFFFFFFFF) { + extent = caps.currentExtent; + } else { + extent.width = (U32)r->width; + extent.height = (U32)r->height; + } + + U32 image_count = NUM_BACK_BUFFERS; + if (image_count < caps.minImageCount) image_count = caps.minImageCount; + if (caps.maxImageCount > 0 && image_count > caps.maxImageCount) + image_count = caps.maxImageCount; + + VkSwapchainCreateInfoKHR ci = {0}; + ci.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; + ci.surface = r->surface; + ci.minImageCount = image_count; + ci.imageFormat = chosen.format; + ci.imageColorSpace = chosen.colorSpace; + ci.imageExtent = extent; + ci.imageArrayLayers = 1; + ci.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + ci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; + ci.preTransform = caps.currentTransform; + ci.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + ci.presentMode = present_mode; + ci.clipped = VK_TRUE; + ci.oldSwapchain = r->swap_chain; // for recreation + + if (vkCreateSwapchainKHR(r->device, &ci, NULL, &r->swap_chain) != VK_SUCCESS) + return 0; + + // Get swap chain images + U32 sc_count = NUM_BACK_BUFFERS; + vkGetSwapchainImagesKHR(r->device, r->swap_chain, &sc_count, r->swap_chain_images); + + // Create image views + for (U32 i = 0; i < NUM_BACK_BUFFERS; i++) { + VkImageViewCreateInfo vi = {0}; + vi.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + vi.image = r->swap_chain_images[i]; + vi.viewType = VK_IMAGE_VIEW_TYPE_2D; + vi.format = r->swap_chain_format; + vi.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + vi.subresourceRange.levelCount = 1; + vi.subresourceRange.layerCount = 1; + if (vkCreateImageView(r->device, &vi, NULL, &r->swap_chain_views[i]) != VK_SUCCESS) + return 0; + } + + r->width = (S32)extent.width; + r->height = (S32)extent.height; + return 1; +} + +static B32 create_render_pass(Renderer *r) { + VkAttachmentDescription color_attach = {0}; + color_attach.format = r->swap_chain_format; + color_attach.samples = VK_SAMPLE_COUNT_1_BIT; + color_attach.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + color_attach.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + color_attach.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + color_attach.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + color_attach.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + color_attach.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + + VkAttachmentReference color_ref = {0}; + color_ref.attachment = 0; + color_ref.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + + VkSubpassDescription subpass = {0}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &color_ref; + + VkSubpassDependency dep = {0}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; + dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.srcAccessMask = 0; + dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + + VkRenderPassCreateInfo ci = {0}; + ci.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + ci.attachmentCount = 1; + ci.pAttachments = &color_attach; + ci.subpassCount = 1; + ci.pSubpasses = &subpass; + ci.dependencyCount = 1; + ci.pDependencies = &dep; + + return vkCreateRenderPass(r->device, &ci, NULL, &r->render_pass) == VK_SUCCESS; +} + +static B32 create_framebuffers(Renderer *r) { + for (U32 i = 0; i < NUM_BACK_BUFFERS; i++) { + VkFramebufferCreateInfo ci = {0}; + ci.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + ci.renderPass = r->render_pass; + ci.attachmentCount = 1; + ci.pAttachments = &r->swap_chain_views[i]; + ci.width = (U32)r->width; + ci.height = (U32)r->height; + ci.layers = 1; + if (vkCreateFramebuffer(r->device, &ci, NULL, &r->framebuffers[i]) != VK_SUCCESS) + return 0; + } + return 1; +} + +static B32 create_command_resources(Renderer *r) { + VkCommandPoolCreateInfo pool_ci = {0}; + pool_ci.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + pool_ci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + pool_ci.queueFamilyIndex = r->queue_family; + if (vkCreateCommandPool(r->device, &pool_ci, NULL, &r->command_pool) != VK_SUCCESS) + return 0; + + VkCommandBufferAllocateInfo alloc_info = {0}; + alloc_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + alloc_info.commandPool = r->command_pool; + alloc_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + alloc_info.commandBufferCount = NUM_BACK_BUFFERS; + + VkCommandBuffer cbs[NUM_BACK_BUFFERS]; + if (vkAllocateCommandBuffers(r->device, &alloc_info, cbs) != VK_SUCCESS) + return 0; + + for (S32 i = 0; i < NUM_BACK_BUFFERS; i++) { + r->frames[i].command_buffer = cbs[i]; + + VkFenceCreateInfo fence_ci = {0}; + fence_ci.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fence_ci.flags = VK_FENCE_CREATE_SIGNALED_BIT; + if (vkCreateFence(r->device, &fence_ci, NULL, &r->frames[i].fence) != VK_SUCCESS) + return 0; + } + + for (S32 i = 0; i < NUM_BACK_BUFFERS; i++) { + VkSemaphoreCreateInfo sem_ci = {0}; + sem_ci.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + if (vkCreateSemaphore(r->device, &sem_ci, NULL, &r->image_available_sema[i]) != VK_SUCCESS) + return 0; + if (vkCreateSemaphore(r->device, &sem_ci, NULL, &r->render_finished_sema[i]) != VK_SUCCESS) + return 0; + } + + return 1; +} + +static B32 create_sampler(Renderer *r) { + VkSamplerCreateInfo ci = {0}; + ci.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + ci.magFilter = VK_FILTER_LINEAR; + ci.minFilter = VK_FILTER_LINEAR; + ci.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + ci.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + ci.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + ci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + return vkCreateSampler(r->device, &ci, NULL, &r->sampler) == VK_SUCCESS; +} + +static B32 create_descriptor_resources(Renderer *r) { + // Descriptor set layout: single combined image sampler at binding 0 + VkDescriptorSetLayoutBinding binding = {0}; + binding.binding = 0; + binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + binding.descriptorCount = 1; + binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + + VkDescriptorSetLayoutCreateInfo layout_ci = {0}; + layout_ci.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + layout_ci.bindingCount = 1; + layout_ci.pBindings = &binding; + if (vkCreateDescriptorSetLayout(r->device, &layout_ci, NULL, &r->descriptor_set_layout) != VK_SUCCESS) + return 0; + + // Descriptor pool: 2 sets (font + icon) + VkDescriptorPoolSize pool_size = {0}; + pool_size.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + pool_size.descriptorCount = 2; + + VkDescriptorPoolCreateInfo pool_ci = {0}; + pool_ci.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + pool_ci.maxSets = 2; + pool_ci.poolSizeCount = 1; + pool_ci.pPoolSizes = &pool_size; + if (vkCreateDescriptorPool(r->device, &pool_ci, NULL, &r->descriptor_pool) != VK_SUCCESS) + return 0; + + // Allocate font descriptor set + VkDescriptorSetAllocateInfo alloc_info = {0}; + alloc_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + alloc_info.descriptorPool = r->descriptor_pool; + alloc_info.descriptorSetCount = 1; + alloc_info.pSetLayouts = &r->descriptor_set_layout; + if (vkAllocateDescriptorSets(r->device, &alloc_info, &r->font_descriptor_set) != VK_SUCCESS) + return 0; + + return 1; +} + +static VkShaderModule create_shader_module(Renderer *r, const U32 *code, size_t size) { + VkShaderModuleCreateInfo ci = {0}; + ci.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + ci.codeSize = size; + ci.pCode = code; + VkShaderModule module = VK_NULL_HANDLE; + vkCreateShaderModule(r->device, &ci, NULL, &module); + return module; +} + +static B32 create_pipeline(Renderer *r) { + VkShaderModule vert_module = create_shader_module(r, ui_vert_spv, sizeof(ui_vert_spv)); + VkShaderModule frag_module = create_shader_module(r, ui_frag_spv, sizeof(ui_frag_spv)); + if (!vert_module || !frag_module) return 0; + + VkPipelineShaderStageCreateInfo stages[2] = {{0}, {0}}; + stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; + stages[0].module = vert_module; + stages[0].pName = "main"; + stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; + stages[1].module = frag_module; + stages[1].pName = "main"; + + // Vertex input + VkVertexInputBindingDescription binding = {0}; + binding.binding = 0; + binding.stride = sizeof(UIVertex); + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + VkVertexInputAttributeDescription attrs[9] = {}; + attrs[0].location = 0; attrs[0].binding = 0; attrs[0].format = VK_FORMAT_R32G32_SFLOAT; attrs[0].offset = offsetof(UIVertex, pos); + attrs[1].location = 1; attrs[1].binding = 0; attrs[1].format = VK_FORMAT_R32G32_SFLOAT; attrs[1].offset = offsetof(UIVertex, uv); + attrs[2].location = 2; attrs[2].binding = 0; attrs[2].format = VK_FORMAT_R32G32B32A32_SFLOAT; attrs[2].offset = offsetof(UIVertex, col); + attrs[3].location = 3; attrs[3].binding = 0; attrs[3].format = VK_FORMAT_R32G32_SFLOAT; attrs[3].offset = offsetof(UIVertex, rect_min); + attrs[4].location = 4; attrs[4].binding = 0; attrs[4].format = VK_FORMAT_R32G32_SFLOAT; attrs[4].offset = offsetof(UIVertex, rect_max); + attrs[5].location = 5; attrs[5].binding = 0; attrs[5].format = VK_FORMAT_R32G32B32A32_SFLOAT; attrs[5].offset = offsetof(UIVertex, corner_radii); + attrs[6].location = 6; attrs[6].binding = 0; attrs[6].format = VK_FORMAT_R32_SFLOAT; attrs[6].offset = offsetof(UIVertex, border_thickness); + attrs[7].location = 7; attrs[7].binding = 0; attrs[7].format = VK_FORMAT_R32_SFLOAT; attrs[7].offset = offsetof(UIVertex, softness); + attrs[8].location = 8; attrs[8].binding = 0; attrs[8].format = VK_FORMAT_R32_SFLOAT; attrs[8].offset = offsetof(UIVertex, mode); + + VkPipelineVertexInputStateCreateInfo vertex_input = {0}; + vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + vertex_input.vertexBindingDescriptionCount = 1; + vertex_input.pVertexBindingDescriptions = &binding; + vertex_input.vertexAttributeDescriptionCount = 9; + vertex_input.pVertexAttributeDescriptions = attrs; + + VkPipelineInputAssemblyStateCreateInfo input_assembly = {0}; + input_assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; + input_assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + + // Dynamic viewport/scissor + VkDynamicState dynamic_states[] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; + VkPipelineDynamicStateCreateInfo dynamic_state = {0}; + dynamic_state.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; + dynamic_state.dynamicStateCount = 2; + dynamic_state.pDynamicStates = dynamic_states; + + VkPipelineViewportStateCreateInfo viewport_state = {0}; + viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + viewport_state.viewportCount = 1; + viewport_state.scissorCount = 1; + + VkPipelineRasterizationStateCreateInfo rasterizer = {0}; + rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + rasterizer.polygonMode = VK_POLYGON_MODE_FILL; + rasterizer.lineWidth = 1.0f; + rasterizer.cullMode = VK_CULL_MODE_NONE; + rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE; + + VkPipelineMultisampleStateCreateInfo multisample = {0}; + multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + + VkPipelineColorBlendAttachmentState blend_attach = {0}; + blend_attach.blendEnable = VK_TRUE; + blend_attach.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + blend_attach.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blend_attach.colorBlendOp = VK_BLEND_OP_ADD; + blend_attach.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + blend_attach.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blend_attach.alphaBlendOp = VK_BLEND_OP_ADD; + blend_attach.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT + | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + + VkPipelineColorBlendStateCreateInfo blend = {0}; + blend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; + blend.attachmentCount = 1; + blend.pAttachments = &blend_attach; + + VkPipelineDepthStencilStateCreateInfo depth_stencil = {0}; + depth_stencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; + + // Pipeline layout: push constants + 1 descriptor set + VkPushConstantRange push_range = {0}; + push_range.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + push_range.offset = 0; + push_range.size = 4 * sizeof(float); // viewport_size + padding + + VkPipelineLayoutCreateInfo layout_ci = {0}; + layout_ci.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + layout_ci.setLayoutCount = 1; + layout_ci.pSetLayouts = &r->descriptor_set_layout; + layout_ci.pushConstantRangeCount = 1; + layout_ci.pPushConstantRanges = &push_range; + if (vkCreatePipelineLayout(r->device, &layout_ci, NULL, &r->pipeline_layout) != VK_SUCCESS) { + vkDestroyShaderModule(r->device, vert_module, NULL); + vkDestroyShaderModule(r->device, frag_module, NULL); + return 0; + } + + VkGraphicsPipelineCreateInfo pipeline_ci = {0}; + pipeline_ci.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + pipeline_ci.stageCount = 2; + pipeline_ci.pStages = stages; + pipeline_ci.pVertexInputState = &vertex_input; + pipeline_ci.pInputAssemblyState = &input_assembly; + pipeline_ci.pViewportState = &viewport_state; + pipeline_ci.pRasterizationState = &rasterizer; + pipeline_ci.pMultisampleState = &multisample; + pipeline_ci.pDepthStencilState = &depth_stencil; + pipeline_ci.pColorBlendState = &blend; + pipeline_ci.pDynamicState = &dynamic_state; + pipeline_ci.layout = r->pipeline_layout; + pipeline_ci.renderPass = r->render_pass; + pipeline_ci.subpass = 0; + + VkResult result = vkCreateGraphicsPipelines(r->device, VK_NULL_HANDLE, 1, &pipeline_ci, NULL, &r->pipeline); + + vkDestroyShaderModule(r->device, vert_module, NULL); + vkDestroyShaderModule(r->device, frag_module, NULL); + + return result == VK_SUCCESS; +} + +static B32 create_ui_buffers(Renderer *r) { + for (S32 i = 0; i < NUM_BACK_BUFFERS; i++) { + VkBufferCreateInfo buf_ci = {0}; + buf_ci.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + buf_ci.size = MAX_VERTICES * sizeof(UIVertex); + buf_ci.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; + + VmaAllocationCreateInfo alloc_ci = {0}; + alloc_ci.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + alloc_ci.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + + VmaAllocationInfo alloc_info; + if (vmaCreateBuffer(r->allocator, &buf_ci, &alloc_ci, + &r->vertex_buffers[i], &r->vertex_allocs[i], &alloc_info) != VK_SUCCESS) + return 0; + r->vb_mapped[i] = alloc_info.pMappedData; + + buf_ci.size = MAX_INDICES * sizeof(U32); + buf_ci.usage = VK_BUFFER_USAGE_INDEX_BUFFER_BIT; + if (vmaCreateBuffer(r->allocator, &buf_ci, &alloc_ci, + &r->index_buffers[i], &r->index_allocs[i], &alloc_info) != VK_SUCCESS) + return 0; + r->ib_mapped[i] = alloc_info.pMappedData; + } + return 1; +} + +//////////////////////////////// +// Helper: update a descriptor set to point to an image view + +static void update_descriptor_set(Renderer *r, VkDescriptorSet set, VkImageView view) { + VkDescriptorImageInfo img_info = {0}; + img_info.sampler = r->sampler; + img_info.imageView = view; + img_info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + VkWriteDescriptorSet write = {0}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = set; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &img_info; + vkUpdateDescriptorSets(r->device, 1, &write, 0, NULL); +} + +//////////////////////////////// +// Font atlas (FreeType) + +static void init_freetype(Renderer *r) { + FT_Init_FreeType(&r->ft_lib); + FT_New_Memory_Face(r->ft_lib, font_inter_data, font_inter_size, 0, &r->ft_face); +} + +static B32 create_font_atlas(Renderer *r, F32 font_size) { + S32 pixel_size = (S32)(font_size + 0.5f); + r->font_atlas_size = font_size; + + FT_Set_Pixel_Sizes(r->ft_face, 0, pixel_size); + r->font_line_height = (F32)(r->ft_face->size->metrics.height >> 6); + + U8 *atlas_data = (U8 *)calloc(1, FONT_ATLAS_W * FONT_ATLAS_H); + + S32 pen_x = 1, pen_y = 1; + S32 row_height = 0; + F32 ascender = (F32)(r->ft_face->size->metrics.ascender >> 6); + + for (S32 i = 0; i < GLYPH_COUNT; i++) { + char ch = (char)(GLYPH_FIRST + i); + if (FT_Load_Char(r->ft_face, ch, FT_LOAD_RENDER)) continue; + + FT_GlyphSlot g = r->ft_face->glyph; + S32 bw = (S32)g->bitmap.width; + S32 bh = (S32)g->bitmap.rows; + S32 pad = 2; + S32 cell_w = bw + pad; + S32 cell_h = (S32)r->font_line_height + pad; + + if (pen_x + cell_w >= FONT_ATLAS_W) { + pen_x = 1; + pen_y += row_height + 1; + row_height = 0; + } + if (pen_y + cell_h >= FONT_ATLAS_H) break; + + S32 y_off = (S32)ascender - g->bitmap_top; + for (S32 y = 0; y < bh; y++) { + S32 dst_y = pen_y + y_off + y; + if (dst_y < 0 || dst_y >= FONT_ATLAS_H) continue; + for (S32 x = 0; x < bw; x++) { + S32 dst_x = pen_x + g->bitmap_left + x; + if (dst_x < 0 || dst_x >= FONT_ATLAS_W) continue; + atlas_data[dst_y * FONT_ATLAS_W + dst_x] = g->bitmap.buffer[y * g->bitmap.pitch + x]; + } + } + + r->glyphs[i].u0 = (F32)pen_x / (F32)FONT_ATLAS_W; + r->glyphs[i].v0 = (F32)pen_y / (F32)FONT_ATLAS_H; + r->glyphs[i].u1 = (F32)(pen_x + cell_w) / (F32)FONT_ATLAS_W; + r->glyphs[i].v1 = (F32)(pen_y + cell_h) / (F32)FONT_ATLAS_H; + r->glyphs[i].w = (F32)cell_w; + r->glyphs[i].h = (F32)cell_h; + r->glyphs[i].x_advance = (F32)(g->advance.x >> 6); + + if (cell_h > row_height) row_height = cell_h; + pen_x += cell_w + 1; + } + + // Create Vulkan image + VkImageCreateInfo img_ci = {0}; + img_ci.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + img_ci.imageType = VK_IMAGE_TYPE_2D; + img_ci.format = VK_FORMAT_R8_UNORM; + img_ci.extent.width = FONT_ATLAS_W; + img_ci.extent.height = FONT_ATLAS_H; + img_ci.extent.depth = 1; + img_ci.mipLevels = 1; + img_ci.arrayLayers = 1; + img_ci.samples = VK_SAMPLE_COUNT_1_BIT; + img_ci.tiling = VK_IMAGE_TILING_OPTIMAL; + img_ci.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + + VmaAllocationCreateInfo alloc_ci = {0}; + alloc_ci.usage = VMA_MEMORY_USAGE_GPU_ONLY; + + if (vmaCreateImage(r->allocator, &img_ci, &alloc_ci, &r->font_image, &r->font_alloc, NULL) != VK_SUCCESS) { + free(atlas_data); + return 0; + } + + // Staging buffer + VkBufferCreateInfo staging_ci = {0}; + staging_ci.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + staging_ci.size = FONT_ATLAS_W * FONT_ATLAS_H; + staging_ci.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + + VmaAllocationCreateInfo staging_alloc_ci = {0}; + staging_alloc_ci.usage = VMA_MEMORY_USAGE_CPU_ONLY; + staging_alloc_ci.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + + VkBuffer staging_buf; + VmaAllocation staging_alloc; + VmaAllocationInfo staging_info; + vmaCreateBuffer(r->allocator, &staging_ci, &staging_alloc_ci, &staging_buf, &staging_alloc, &staging_info); + memcpy(staging_info.pMappedData, atlas_data, FONT_ATLAS_W * FONT_ATLAS_H); + free(atlas_data); + + // Copy via command buffer + VkCommandBuffer cb = begin_one_shot(r); + + transition_image(cb, r->font_image, + VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 0, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); + + VkBufferImageCopy region = {0}; + region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.imageSubresource.layerCount = 1; + region.imageExtent.width = FONT_ATLAS_W; + region.imageExtent.height = FONT_ATLAS_H; + region.imageExtent.depth = 1; + vkCmdCopyBufferToImage(cb, staging_buf, r->font_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + + transition_image(cb, r->font_image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + + end_one_shot(r, cb); + + vmaDestroyBuffer(r->allocator, staging_buf, staging_alloc); + + // Image view + VkImageViewCreateInfo view_ci = {0}; + view_ci.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + view_ci.image = r->font_image; + view_ci.viewType = VK_IMAGE_VIEW_TYPE_2D; + view_ci.format = VK_FORMAT_R8_UNORM; + view_ci.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + view_ci.subresourceRange.levelCount = 1; + view_ci.subresourceRange.layerCount = 1; + if (vkCreateImageView(r->device, &view_ci, NULL, &r->font_view) != VK_SUCCESS) + return 0; + + // Update descriptor + update_descriptor_set(r, r->font_descriptor_set, r->font_view); + + return 1; +} + +//////////////////////////////// +// Text measurement callback for UI system + +Vec2F32 renderer_measure_text(const char *text, S32 length, F32 font_size, void *user_data) { + Renderer *r = (Renderer *)user_data; + if (!r || length == 0) return v2f32(0, font_size); + + FT_Set_Pixel_Sizes(r->ft_face, 0, (FT_UInt)(font_size + 0.5f)); + + F32 width = 0; + for (S32 i = 0; i < length; i++) { + if (FT_Load_Char(r->ft_face, (FT_ULong)(unsigned char)text[i], FT_LOAD_DEFAULT)) continue; + width += (F32)(r->ft_face->glyph->advance.x >> 6); + } + F32 height = (F32)(r->ft_face->size->metrics.height >> 6); + return v2f32(width, height); +} + +//////////////////////////////// +// Quad emission helpers + +typedef struct DrawBatch { + UIVertex *vertices; + U32 *indices; + U32 vertex_count; + U32 index_count; +} DrawBatch; + +static void emit_quad(DrawBatch *batch, + F32 x0, F32 y0, F32 x1, F32 y1, + F32 u0, F32 v0, F32 u1, F32 v1, + F32 cr, F32 cg, F32 cb, F32 ca, + F32 rmin_x, F32 rmin_y, F32 rmax_x, F32 rmax_y, + F32 cr_tl, F32 cr_tr, F32 cr_br, F32 cr_bl, + F32 border_thickness, F32 softness, F32 mode) +{ + if (batch->vertex_count + 4 > MAX_VERTICES || batch->index_count + 6 > MAX_INDICES) + return; + + U32 base = batch->vertex_count; + UIVertex *v = &batch->vertices[base]; + + F32 px0 = x0, py0 = y0, px1 = x1, py1 = y1; + if (mode < 0.5f) { + F32 pad = softness + 1.0f; + px0 -= pad; py0 -= pad; px1 += pad; py1 += pad; + } + + v[0].pos[0] = px0; v[0].pos[1] = py0; v[0].uv[0] = u0; v[0].uv[1] = v0; + v[1].pos[0] = px1; v[1].pos[1] = py0; v[1].uv[0] = u1; v[1].uv[1] = v0; + v[2].pos[0] = px1; v[2].pos[1] = py1; v[2].uv[0] = u1; v[2].uv[1] = v1; + v[3].pos[0] = px0; v[3].pos[1] = py1; v[3].uv[0] = u0; v[3].uv[1] = v1; + + for (S32 i = 0; i < 4; i++) { + v[i].col[0] = cr; v[i].col[1] = cg; v[i].col[2] = cb; v[i].col[3] = ca; + v[i].rect_min[0] = rmin_x; v[i].rect_min[1] = rmin_y; + v[i].rect_max[0] = rmax_x; v[i].rect_max[1] = rmax_y; + v[i].corner_radii[0] = cr_tl; v[i].corner_radii[1] = cr_tr; + v[i].corner_radii[2] = cr_br; v[i].corner_radii[3] = cr_bl; + v[i].border_thickness = border_thickness; + v[i].softness = softness; + v[i].mode = mode; + } + + U32 *idx = &batch->indices[batch->index_count]; + idx[0] = base; idx[1] = base + 1; idx[2] = base + 2; + idx[3] = base; idx[4] = base + 2; idx[5] = base + 3; + + batch->vertex_count += 4; + batch->index_count += 6; +} + +static void emit_quad_rotated(DrawBatch *batch, + F32 x0, F32 y0, F32 x1, F32 y1, + F32 u0, F32 v0, F32 u1, F32 v1, + F32 cr, F32 cg, F32 cb, F32 ca, + F32 angle_rad) +{ + if (batch->vertex_count + 4 > MAX_VERTICES || batch->index_count + 6 > MAX_INDICES) + return; + + U32 base = batch->vertex_count; + UIVertex *v = &batch->vertices[base]; + + F32 cx = (x0 + x1) * 0.5f; + F32 cy = (y0 + y1) * 0.5f; + F32 cosA = cosf(angle_rad); + F32 sinA = sinf(angle_rad); + + F32 dx0 = x0 - cx, dy0 = y0 - cy; + F32 dx1 = x1 - cx, dy1 = y1 - cy; + + v[0].pos[0] = cx + dx0 * cosA - dy0 * sinA; + v[0].pos[1] = cy + dx0 * sinA + dy0 * cosA; + v[0].uv[0] = u0; v[0].uv[1] = v0; + + v[1].pos[0] = cx + dx1 * cosA - dy0 * sinA; + v[1].pos[1] = cy + dx1 * sinA + dy0 * cosA; + v[1].uv[0] = u1; v[1].uv[1] = v0; + + v[2].pos[0] = cx + dx1 * cosA - dy1 * sinA; + v[2].pos[1] = cy + dx1 * sinA + dy1 * cosA; + v[2].uv[0] = u1; v[2].uv[1] = v1; + + v[3].pos[0] = cx + dx0 * cosA - dy1 * sinA; + v[3].pos[1] = cy + dx0 * sinA + dy1 * cosA; + v[3].uv[0] = u0; v[3].uv[1] = v1; + + for (S32 i = 0; i < 4; i++) { + v[i].col[0] = cr; v[i].col[1] = cg; v[i].col[2] = cb; v[i].col[3] = ca; + v[i].rect_min[0] = 0; v[i].rect_min[1] = 0; + v[i].rect_max[0] = 0; v[i].rect_max[1] = 0; + v[i].corner_radii[0] = 0; v[i].corner_radii[1] = 0; + v[i].corner_radii[2] = 0; v[i].corner_radii[3] = 0; + v[i].border_thickness = 0; + v[i].softness = 0; + v[i].mode = 2.0f; + } + + U32 *idx = &batch->indices[batch->index_count]; + idx[0] = base; idx[1] = base + 1; idx[2] = base + 2; + idx[3] = base; idx[4] = base + 2; idx[5] = base + 3; + + batch->vertex_count += 4; + batch->index_count += 6; +} + +static void emit_rect(DrawBatch *batch, + F32 x0, F32 y0, F32 x1, F32 y1, + F32 cr, F32 cg, F32 cb, F32 ca, + F32 cr_tl, F32 cr_tr, F32 cr_br, F32 cr_bl, + F32 border_thickness, F32 softness) +{ + emit_quad(batch, x0, y0, x1, y1, + 0, 0, 0, 0, + cr, cg, cb, ca, + x0, y0, x1, y1, + cr_tl, cr_tr, cr_br, cr_bl, + border_thickness, softness, 0.0f); +} + +static void emit_rect_vgradient(DrawBatch *batch, + F32 x0, F32 y0, F32 x1, F32 y1, + F32 tr, F32 tg, F32 tb, F32 ta, + F32 br, F32 bg, F32 bb_, F32 ba, + F32 cr_tl, F32 cr_tr, F32 cr_br, F32 cr_bl, + F32 softness) +{ + if (batch->vertex_count + 4 > MAX_VERTICES || batch->index_count + 6 > MAX_INDICES) + return; + + U32 base = batch->vertex_count; + UIVertex *v = &batch->vertices[base]; + + F32 pad = softness + 1.0f; + F32 px0 = x0 - pad, py0 = y0 - pad, px1 = x1 + pad, py1 = y1 + pad; + + v[0].pos[0] = px0; v[0].pos[1] = py0; v[0].uv[0] = 0; v[0].uv[1] = 0; + v[1].pos[0] = px1; v[1].pos[1] = py0; v[1].uv[0] = 0; v[1].uv[1] = 0; + v[2].pos[0] = px1; v[2].pos[1] = py1; v[2].uv[0] = 0; v[2].uv[1] = 0; + v[3].pos[0] = px0; v[3].pos[1] = py1; v[3].uv[0] = 0; v[3].uv[1] = 0; + + v[0].col[0] = tr; v[0].col[1] = tg; v[0].col[2] = tb; v[0].col[3] = ta; + v[1].col[0] = tr; v[1].col[1] = tg; v[1].col[2] = tb; v[1].col[3] = ta; + v[2].col[0] = br; v[2].col[1] = bg; v[2].col[2] = bb_; v[2].col[3] = ba; + v[3].col[0] = br; v[3].col[1] = bg; v[3].col[2] = bb_; v[3].col[3] = ba; + + for (S32 i = 0; i < 4; i++) { + v[i].rect_min[0] = x0; v[i].rect_min[1] = y0; + v[i].rect_max[0] = x1; v[i].rect_max[1] = y1; + v[i].corner_radii[0] = cr_tl; v[i].corner_radii[1] = cr_tr; + v[i].corner_radii[2] = cr_br; v[i].corner_radii[3] = cr_bl; + v[i].border_thickness = 0; + v[i].softness = softness; + v[i].mode = 0; + } + + U32 *idx = &batch->indices[batch->index_count]; + idx[0] = base; idx[1] = base + 1; idx[2] = base + 2; + idx[3] = base; idx[4] = base + 2; idx[5] = base + 3; + + batch->vertex_count += 4; + batch->index_count += 6; +} + +static void emit_text_glyphs(DrawBatch *batch, Renderer *r, + Clay_BoundingBox bbox, Clay_Color color, const char *text, S32 text_len, + U16 font_size) +{ + if (text_len == 0 || color.a < 0.1f) return; + + F32 cr = color.r / 255.f; + F32 cg = color.g / 255.f; + F32 cb = color.b / 255.f; + F32 ca = color.a / 255.f; + + F32 scale = (F32)font_size / r->font_atlas_size; + F32 text_h = r->font_line_height * scale; + + F32 x = floorf(bbox.x + 0.5f); + F32 y = floorf(bbox.y + (bbox.height - text_h) * 0.5f + 0.5f); + + for (S32 i = 0; i < text_len; i++) { + char ch = text[i]; + if (ch < GLYPH_FIRST || ch > GLYPH_LAST) { + if (ch == ' ') { + S32 gi = ' ' - GLYPH_FIRST; + if (gi >= 0 && gi < GLYPH_COUNT) + x += r->glyphs[gi].x_advance * scale; + continue; + } + ch = '?'; + } + S32 gi = ch - GLYPH_FIRST; + if (gi < 0 || gi >= GLYPH_COUNT) continue; + + GlyphInfo *g = &r->glyphs[gi]; + F32 gw = g->w * scale; + F32 gh = g->h * scale; + + emit_quad(batch, + x, y, x + gw, y + gh, + g->u0, g->v0, g->u1, g->v1, + cr, cg, cb, ca, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 1.0f); + + x += g->x_advance * scale; + } +} + +//////////////////////////////// +// Flush helper + +static void flush_batch(Renderer *r, DrawBatch *batch, U32 buf_idx, + U32 *flush_index_start, VkDescriptorSet tex_set) +{ + U32 draw_index_count = batch->index_count - *flush_index_start; + if (draw_index_count == 0) return; + + VkCommandBuffer cb = r->frames[r->frame_index % NUM_BACK_BUFFERS].command_buffer; + + vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, r->pipeline); + + F32 constants[4] = { (F32)r->width, (F32)r->height, 0, 0 }; + vkCmdPushConstants(cb, r->pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(constants), constants); + + vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, r->pipeline_layout, + 0, 1, &tex_set, 0, NULL); + + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cb, 0, 1, &r->vertex_buffers[buf_idx], &offset); + vkCmdBindIndexBuffer(cb, r->index_buffers[buf_idx], 0, VK_INDEX_TYPE_UINT32); + + vkCmdDrawIndexed(cb, draw_index_count, 1, *flush_index_start, 0, 0); + + *flush_index_start = batch->index_count; +} + +//////////////////////////////// +// Public API + +Renderer *renderer_create(RendererDesc *desc) { + Renderer *r = (Renderer *)calloc(1, sizeof(Renderer)); + + r->hwnd = (HWND)desc->window_handle; + r->width = desc->width; + r->height = desc->height; + r->frame_count = desc->frame_count > 0 ? desc->frame_count : NUM_BACK_BUFFERS; + r->clear_r = 0.12f; + r->clear_g = 0.12f; + r->clear_b = 0.13f; + if (r->frame_count > NUM_BACK_BUFFERS) r->frame_count = NUM_BACK_BUFFERS; + + if (!create_instance(r)) goto fail; + if (!create_surface(r)) goto fail; + if (!pick_physical_device(r)) goto fail; + if (!find_queue_family(r)) goto fail; + if (!create_device(r)) goto fail; + if (!create_vma(r)) goto fail; + if (!create_command_resources(r)) goto fail; + if (!create_sampler(r)) goto fail; + if (!create_descriptor_resources(r)) goto fail; + if (!create_swap_chain(r)) goto fail; + if (!create_render_pass(r)) goto fail; + if (!create_framebuffers(r)) goto fail; + if (!create_pipeline(r)) goto fail; + if (!create_ui_buffers(r)) goto fail; + + init_freetype(r); + if (!create_font_atlas(r, 22.0f)) goto fail; + + return r; + +fail: + renderer_destroy(r); + return NULL; +} + +Renderer *renderer_create_shared(Renderer *parent, RendererDesc *desc) { + if (!parent) return NULL; + + Renderer *r = (Renderer *)calloc(1, sizeof(Renderer)); + + r->parent = parent; + r->hwnd = (HWND)desc->window_handle; + r->width = desc->width; + r->height = desc->height; + r->frame_count = desc->frame_count > 0 ? desc->frame_count : NUM_BACK_BUFFERS; + if (r->frame_count > NUM_BACK_BUFFERS) r->frame_count = NUM_BACK_BUFFERS; + + // Share from parent + r->instance = parent->instance; + r->physical_device = parent->physical_device; + r->device = parent->device; + r->graphics_queue = parent->graphics_queue; + r->queue_family = parent->queue_family; + r->allocator = parent->allocator; + r->render_pass = parent->render_pass; + r->pipeline_layout = parent->pipeline_layout; + r->pipeline = parent->pipeline; + r->descriptor_set_layout = parent->descriptor_set_layout; + r->sampler = parent->sampler; + r->font_image = parent->font_image; + r->font_alloc = parent->font_alloc; + r->font_view = parent->font_view; + r->font_descriptor_set = parent->font_descriptor_set; + r->icon_image = parent->icon_image; + r->icon_alloc = parent->icon_alloc; + r->icon_view = parent->icon_view; + r->icon_descriptor_set = parent->icon_descriptor_set; + r->descriptor_pool = parent->descriptor_pool; + r->ft_lib = parent->ft_lib; + r->ft_face = parent->ft_face; + memcpy(r->glyphs, parent->glyphs, sizeof(r->glyphs)); + r->font_atlas_size = parent->font_atlas_size; + r->font_line_height = parent->font_line_height; + + // Own surface, swap chain, command resources, buffers + if (!create_surface(r)) goto fail; + if (!create_command_resources(r)) goto fail; + if (!create_swap_chain(r)) goto fail; + // Shared renderers reuse parent render_pass but format must match + if (!create_framebuffers(r)) goto fail; + if (!create_ui_buffers(r)) goto fail; + + r->clear_r = parent->clear_r; + r->clear_g = parent->clear_g; + r->clear_b = parent->clear_b; + + return r; + +fail: + renderer_destroy(r); + return NULL; +} + +void renderer_destroy(Renderer *r) { + if (!r) return; + + if (r->device) vkDeviceWaitIdle(r->device); + + // Per-window resources + for (S32 i = 0; i < NUM_BACK_BUFFERS; i++) { + if (r->vertex_buffers[i]) + vmaDestroyBuffer(r->allocator, r->vertex_buffers[i], r->vertex_allocs[i]); + if (r->index_buffers[i]) + vmaDestroyBuffer(r->allocator, r->index_buffers[i], r->index_allocs[i]); + } + + for (S32 i = 0; i < NUM_BACK_BUFFERS; i++) { + if (r->framebuffers[i]) vkDestroyFramebuffer(r->device, r->framebuffers[i], NULL); + if (r->swap_chain_views[i]) vkDestroyImageView(r->device, r->swap_chain_views[i], NULL); + } + + if (r->swap_chain) vkDestroySwapchainKHR(r->device, r->swap_chain, NULL); + + for (S32 i = 0; i < NUM_BACK_BUFFERS; i++) { + if (r->frames[i].fence) vkDestroyFence(r->device, r->frames[i].fence, NULL); + if (r->image_available_sema[i]) vkDestroySemaphore(r->device, r->image_available_sema[i], NULL); + if (r->render_finished_sema[i]) vkDestroySemaphore(r->device, r->render_finished_sema[i], NULL); + } + if (r->command_pool) vkDestroyCommandPool(r->device, r->command_pool, NULL); + + if (r->surface) vkDestroySurfaceKHR(r->instance, r->surface, NULL); + + // Shared resources only freed by root renderer + if (!r->parent) { + if (r->font_view) vkDestroyImageView(r->device, r->font_view, NULL); + if (r->font_image) vmaDestroyImage(r->allocator, r->font_image, r->font_alloc); + if (r->icon_view) vkDestroyImageView(r->device, r->icon_view, NULL); + if (r->icon_image) vmaDestroyImage(r->allocator, r->icon_image, r->icon_alloc); + + if (r->pipeline) vkDestroyPipeline(r->device, r->pipeline, NULL); + if (r->pipeline_layout) vkDestroyPipelineLayout(r->device, r->pipeline_layout, NULL); + if (r->descriptor_pool) vkDestroyDescriptorPool(r->device, r->descriptor_pool, NULL); + if (r->descriptor_set_layout) vkDestroyDescriptorSetLayout(r->device, r->descriptor_set_layout, NULL); + if (r->sampler) vkDestroySampler(r->device, r->sampler, NULL); + if (r->render_pass) vkDestroyRenderPass(r->device, r->render_pass, NULL); + + if (r->ft_face) FT_Done_Face(r->ft_face); + if (r->ft_lib) FT_Done_FreeType(r->ft_lib); + + if (r->allocator) vmaDestroyAllocator(r->allocator); + if (r->device) vkDestroyDevice(r->device, NULL); + +#ifdef _DEBUG + if (r->debug_messenger) { + PFN_vkDestroyDebugUtilsMessengerEXT func = + (PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(r->instance, "vkDestroyDebugUtilsMessengerEXT"); + if (func) func(r->instance, r->debug_messenger, NULL); + } +#endif + if (r->instance) vkDestroyInstance(r->instance, NULL); + } + + free(r); +} + +B32 renderer_begin_frame(Renderer *r) { + // Sync shared resources from parent + if (r->parent) { + r->font_image = r->parent->font_image; + r->font_view = r->parent->font_view; + r->font_descriptor_set = r->parent->font_descriptor_set; + r->icon_image = r->parent->icon_image; + r->icon_view = r->parent->icon_view; + r->icon_descriptor_set = r->parent->icon_descriptor_set; + memcpy(r->glyphs, r->parent->glyphs, sizeof(r->glyphs)); + r->font_atlas_size = r->parent->font_atlas_size; + r->font_line_height = r->parent->font_line_height; + } + + if (IsIconic(r->hwnd)) { + Sleep(10); + return 0; + } + + return 1; +} + +void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) { + U32 frame_idx = r->frame_index % NUM_BACK_BUFFERS; + FrameContext *fc = &r->frames[frame_idx]; + + // Wait for this frame's fence + vkWaitForFences(r->device, 1, &fc->fence, VK_TRUE, UINT64_MAX); + vkResetFences(r->device, 1, &fc->fence); + + // Acquire next image + U32 image_index; + VkResult acquire_result = vkAcquireNextImageKHR(r->device, r->swap_chain, UINT64_MAX, + r->image_available_sema[frame_idx], VK_NULL_HANDLE, &image_index); + + if (acquire_result == VK_ERROR_OUT_OF_DATE_KHR) { + renderer_resize(r, r->width, r->height); + return; + } + + VkCommandBuffer cb = fc->command_buffer; + vkResetCommandBuffer(cb, 0); + + VkCommandBufferBeginInfo begin_info = {0}; + begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cb, &begin_info); + + VkClearValue clear_value = {0}; + clear_value.color.float32[0] = r->clear_r; + clear_value.color.float32[1] = r->clear_g; + clear_value.color.float32[2] = r->clear_b; + clear_value.color.float32[3] = 1.0f; + + VkRenderPassBeginInfo rp_begin = {0}; + rp_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rp_begin.renderPass = r->render_pass; + rp_begin.framebuffer = r->framebuffers[image_index]; + rp_begin.renderArea.extent.width = (U32)r->width; + rp_begin.renderArea.extent.height = (U32)r->height; + rp_begin.clearValueCount = 1; + rp_begin.pClearValues = &clear_value; + + vkCmdBeginRenderPass(cb, &rp_begin, VK_SUBPASS_CONTENTS_INLINE); + + VkViewport viewport = {0}; + viewport.width = (float)r->width; + viewport.height = (float)r->height; + viewport.maxDepth = 1.0f; + vkCmdSetViewport(cb, 0, 1, &viewport); + + VkRect2D scissor = {0}; + scissor.extent.width = (U32)r->width; + scissor.extent.height = (U32)r->height; + vkCmdSetScissor(cb, 0, 1, &scissor); + + // Process Clay render commands + if (render_commands.length > 0) { + DrawBatch batch = {0}; + batch.vertices = (UIVertex *)r->vb_mapped[frame_idx]; + batch.indices = (U32 *)r->ib_mapped[frame_idx]; + + S32 bound_texture = 0; // 0 = font, 1 = icon + U32 flush_index_start = 0; + + for (S32 i = 0; i < render_commands.length; i++) { + Clay_RenderCommand *cmd = Clay_RenderCommandArray_Get(&render_commands, i); + Clay_BoundingBox bb = cmd->boundingBox; + + switch (cmd->commandType) { + case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: { + Clay_RectangleRenderData *rect = &cmd->renderData.rectangle; + Clay_Color c = rect->backgroundColor; + emit_rect(&batch, + bb.x, bb.y, bb.x + bb.width, bb.y + bb.height, + c.r / 255.f, c.g / 255.f, c.b / 255.f, c.a / 255.f, + rect->cornerRadius.topLeft, rect->cornerRadius.topRight, + rect->cornerRadius.bottomRight, rect->cornerRadius.bottomLeft, + 0, 1.0f); + } break; + + case CLAY_RENDER_COMMAND_TYPE_BORDER: { + Clay_BorderRenderData *border = &cmd->renderData.border; + Clay_Color c = border->color; + F32 cr_norm = c.r / 255.f; + F32 cg_norm = c.g / 255.f; + F32 cb_norm = c.b / 255.f; + F32 ca_norm = c.a / 255.f; + + Clay_CornerRadius cr = border->cornerRadius; + B32 has_radius = cr.topLeft > 0 || cr.topRight > 0 || cr.bottomLeft > 0 || cr.bottomRight > 0; + B32 uniform = border->width.top == border->width.bottom && + border->width.top == border->width.left && + border->width.top == border->width.right && + border->width.top > 0; + + if (has_radius && uniform) { + emit_rect(&batch, + bb.x, bb.y, bb.x + bb.width, bb.y + bb.height, + cr_norm, cg_norm, cb_norm, ca_norm, + cr.topLeft, cr.topRight, cr.bottomRight, cr.bottomLeft, + (F32)border->width.top, 1.0f); + } else { + if (border->width.top > 0) { + emit_rect(&batch, bb.x, bb.y, bb.x + bb.width, bb.y + border->width.top, + cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f); + } + if (border->width.bottom > 0) { + emit_rect(&batch, bb.x, bb.y + bb.height - border->width.bottom, bb.x + bb.width, bb.y + bb.height, + cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f); + } + if (border->width.left > 0) { + emit_rect(&batch, bb.x, bb.y, bb.x + border->width.left, bb.y + bb.height, + cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f); + } + if (border->width.right > 0) { + emit_rect(&batch, bb.x + bb.width - border->width.right, bb.y, bb.x + bb.width, bb.y + bb.height, + cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f); + } + } + } break; + + case CLAY_RENDER_COMMAND_TYPE_TEXT: { + // Bind font texture + if (bound_texture != 0) { + VkDescriptorSet heap = bound_texture == 1 ? r->icon_descriptor_set : r->font_descriptor_set; + flush_batch(r, &batch, frame_idx, &flush_index_start, heap); + bound_texture = 0; + } + Clay_TextRenderData *text = &cmd->renderData.text; + emit_text_glyphs(&batch, r, bb, text->textColor, + text->stringContents.chars, text->stringContents.length, + text->fontSize); + } break; + + case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: { + VkDescriptorSet heap = bound_texture == 1 ? r->icon_descriptor_set : r->font_descriptor_set; + flush_batch(r, &batch, frame_idx, &flush_index_start, heap); + VkRect2D clip = {0}; + clip.offset.x = (S32)Max(bb.x, 0.f); + clip.offset.y = (S32)Max(bb.y, 0.f); + clip.extent.width = (U32)Min(bb.width, (F32)r->width - clip.offset.x); + clip.extent.height = (U32)Min(bb.height, (F32)r->height - clip.offset.y); + if (clip.extent.width == 0) clip.extent.width = 1; + if (clip.extent.height == 0) clip.extent.height = 1; + vkCmdSetScissor(cb, 0, 1, &clip); + } break; + + case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: { + VkDescriptorSet heap = bound_texture == 1 ? r->icon_descriptor_set : r->font_descriptor_set; + flush_batch(r, &batch, frame_idx, &flush_index_start, heap); + VkRect2D full_scissor = {0}; + full_scissor.extent.width = (U32)r->width; + full_scissor.extent.height = (U32)r->height; + vkCmdSetScissor(cb, 0, 1, &full_scissor); + } break; + + case CLAY_RENDER_COMMAND_TYPE_CUSTOM: { + Clay_CustomRenderData *custom = &cmd->renderData.custom; + if (custom->customData) { + CustomRenderType type = *(CustomRenderType *)custom->customData; + if (type == CUSTOM_RENDER_VGRADIENT) { + if (bound_texture != 0) { + VkDescriptorSet heap = bound_texture == 1 ? r->icon_descriptor_set : r->font_descriptor_set; + flush_batch(r, &batch, frame_idx, &flush_index_start, heap); + bound_texture = 0; + } + CustomGradientData *grad = (CustomGradientData *)custom->customData; + Clay_Color tc = grad->top_color; + Clay_Color bc = grad->bottom_color; + emit_rect_vgradient(&batch, + bb.x, bb.y, bb.x + bb.width, bb.y + bb.height, + tc.r / 255.f, tc.g / 255.f, tc.b / 255.f, tc.a / 255.f, + bc.r / 255.f, bc.g / 255.f, bc.b / 255.f, bc.a / 255.f, + custom->cornerRadius.topLeft, custom->cornerRadius.topRight, + custom->cornerRadius.bottomRight, custom->cornerRadius.bottomLeft, + 1.0f); + } else if (type == CUSTOM_RENDER_ICON) { + if (bound_texture != 1 && r->icon_descriptor_set) { + flush_batch(r, &batch, frame_idx, &flush_index_start, r->font_descriptor_set); + bound_texture = 1; + } + CustomIconData *icon = (CustomIconData *)custom->customData; + Clay_Color c = icon->color; + F32 icon_cr = c.r / 255.f, icon_cg = c.g / 255.f; + F32 icon_cb = c.b / 255.f, icon_ca = c.a / 255.f; + UI_IconInfo *info = &g_icons[icon->icon_id]; + emit_quad(&batch, + bb.x, bb.y, bb.x + bb.width, bb.y + bb.height, + info->u0, info->v0, info->u1, info->v1, + icon_cr, icon_cg, icon_cb, icon_ca, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 2.0f); + } else if (type == CUSTOM_RENDER_ROTATED_ICON) { + if (bound_texture != 1 && r->icon_descriptor_set) { + flush_batch(r, &batch, frame_idx, &flush_index_start, r->font_descriptor_set); + bound_texture = 1; + } + CustomRotatedIconData *ri = (CustomRotatedIconData *)custom->customData; + Clay_Color c = ri->color; + F32 ri_cr = c.r / 255.f, ri_cg = c.g / 255.f; + F32 ri_cb = c.b / 255.f, ri_ca = c.a / 255.f; + UI_IconInfo *info = &g_icons[ri->icon_id]; + emit_quad_rotated(&batch, + bb.x, bb.y, bb.x + bb.width, bb.y + bb.height, + info->u0, info->v0, info->u1, info->v1, + ri_cr, ri_cg, ri_cb, ri_ca, + ri->angle_rad); + } + } + } break; + + case CLAY_RENDER_COMMAND_TYPE_IMAGE: + default: + break; + } + } + + // Flush remaining + VkDescriptorSet heap = bound_texture == 1 ? r->icon_descriptor_set : r->font_descriptor_set; + flush_batch(r, &batch, frame_idx, &flush_index_start, heap); + } + + vkCmdEndRenderPass(cb); + vkEndCommandBuffer(cb); + + // Submit + VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + VkSubmitInfo submit = {0}; + submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submit.waitSemaphoreCount = 1; + submit.pWaitSemaphores = &r->image_available_sema[frame_idx]; + submit.pWaitDstStageMask = &wait_stage; + submit.commandBufferCount = 1; + submit.pCommandBuffers = &cb; + submit.signalSemaphoreCount = 1; + submit.pSignalSemaphores = &r->render_finished_sema[frame_idx]; + vkQueueSubmit(r->graphics_queue, 1, &submit, fc->fence); + + // Present + VkPresentInfoKHR present = {0}; + present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + present.waitSemaphoreCount = 1; + present.pWaitSemaphores = &r->render_finished_sema[frame_idx]; + present.swapchainCount = 1; + present.pSwapchains = &r->swap_chain; + present.pImageIndices = &image_index; + VkResult present_result = vkQueuePresentKHR(r->graphics_queue, &present); + + if (present_result == VK_ERROR_OUT_OF_DATE_KHR || present_result == VK_SUBOPTIMAL_KHR) { + renderer_resize(r, r->width, r->height); + } + + r->frame_index++; +} + +void renderer_create_icon_atlas(Renderer *r, const U8 *data, S32 w, S32 h) { + // Create image + VkImageCreateInfo img_ci = {0}; + img_ci.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + img_ci.imageType = VK_IMAGE_TYPE_2D; + img_ci.format = VK_FORMAT_R8G8B8A8_UNORM; + img_ci.extent.width = (U32)w; + img_ci.extent.height = (U32)h; + img_ci.extent.depth = 1; + img_ci.mipLevels = 1; + img_ci.arrayLayers = 1; + img_ci.samples = VK_SAMPLE_COUNT_1_BIT; + img_ci.tiling = VK_IMAGE_TILING_OPTIMAL; + img_ci.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + + VmaAllocationCreateInfo alloc_ci = {0}; + alloc_ci.usage = VMA_MEMORY_USAGE_GPU_ONLY; + + vmaCreateImage(r->allocator, &img_ci, &alloc_ci, &r->icon_image, &r->icon_alloc, NULL); + + // Staging buffer + VkBufferCreateInfo staging_ci = {0}; + staging_ci.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + staging_ci.size = (VkDeviceSize)w * h * 4; + staging_ci.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + + VmaAllocationCreateInfo staging_alloc_ci = {0}; + staging_alloc_ci.usage = VMA_MEMORY_USAGE_CPU_ONLY; + staging_alloc_ci.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + + VkBuffer staging_buf; + VmaAllocation staging_alloc; + VmaAllocationInfo staging_info; + vmaCreateBuffer(r->allocator, &staging_ci, &staging_alloc_ci, &staging_buf, &staging_alloc, &staging_info); + memcpy(staging_info.pMappedData, data, (size_t)w * h * 4); + + VkCommandBuffer cb = begin_one_shot(r); + + transition_image(cb, r->icon_image, + VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 0, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); + + VkBufferImageCopy region = {0}; + region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.imageSubresource.layerCount = 1; + region.imageExtent.width = (U32)w; + region.imageExtent.height = (U32)h; + region.imageExtent.depth = 1; + vkCmdCopyBufferToImage(cb, staging_buf, r->icon_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + + transition_image(cb, r->icon_image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + + end_one_shot(r, cb); + vmaDestroyBuffer(r->allocator, staging_buf, staging_alloc); + + // Image view + VkImageViewCreateInfo view_ci = {0}; + view_ci.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + view_ci.image = r->icon_image; + view_ci.viewType = VK_IMAGE_VIEW_TYPE_2D; + view_ci.format = VK_FORMAT_R8G8B8A8_UNORM; + view_ci.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + view_ci.subresourceRange.levelCount = 1; + view_ci.subresourceRange.layerCount = 1; + vkCreateImageView(r->device, &view_ci, NULL, &r->icon_view); + + // Allocate icon descriptor set + VkDescriptorSetAllocateInfo ds_alloc = {0}; + ds_alloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + ds_alloc.descriptorPool = r->descriptor_pool; + ds_alloc.descriptorSetCount = 1; + ds_alloc.pSetLayouts = &r->descriptor_set_layout; + vkAllocateDescriptorSets(r->device, &ds_alloc, &r->icon_descriptor_set); + + update_descriptor_set(r, r->icon_descriptor_set, r->icon_view); +} + +void renderer_resize(Renderer *r, S32 width, S32 height) { + if (width <= 0 || height <= 0) return; + + vkDeviceWaitIdle(r->device); + + // Clean up old swap chain resources + for (U32 i = 0; i < NUM_BACK_BUFFERS; i++) { + if (r->framebuffers[i]) { vkDestroyFramebuffer(r->device, r->framebuffers[i], NULL); r->framebuffers[i] = VK_NULL_HANDLE; } + if (r->swap_chain_views[i]) { vkDestroyImageView(r->device, r->swap_chain_views[i], NULL); r->swap_chain_views[i] = VK_NULL_HANDLE; } + } + + VkSwapchainKHR old_swap = r->swap_chain; + r->width = width; + r->height = height; + + create_swap_chain(r); + + if (old_swap && old_swap != r->swap_chain) { + vkDestroySwapchainKHR(r->device, old_swap, NULL); + } + + create_framebuffers(r); +} + +void renderer_set_clear_color(Renderer *r, F32 cr, F32 cg, F32 cb) { + r->clear_r = cr; + r->clear_g = cg; + r->clear_b = cb; +} + +void renderer_set_font_scale(Renderer *r, F32 scale) { + F32 target_size = 22.0f * scale; + if (fabsf(target_size - r->font_atlas_size) < 0.1f) return; + vkDeviceWaitIdle(r->device); + if (r->font_view) { vkDestroyImageView(r->device, r->font_view, NULL); r->font_view = VK_NULL_HANDLE; } + if (r->font_image) { vmaDestroyImage(r->allocator, r->font_image, r->font_alloc); r->font_image = VK_NULL_HANDLE; } + create_font_atlas(r, target_size); +} + +void renderer_sync_from_parent(Renderer *r) { + if (!r || !r->parent) return; + Renderer *p = r->parent; + r->font_image = p->font_image; + r->font_view = p->font_view; + r->font_descriptor_set = p->font_descriptor_set; + r->font_atlas_size = p->font_atlas_size; + r->font_line_height = p->font_line_height; + memcpy(r->glyphs, p->glyphs, sizeof(r->glyphs)); +} diff --git a/src/renderer/ui.f.glsl b/src/renderer/ui.f.glsl new file mode 100644 index 0000000..54ade14 --- /dev/null +++ b/src/renderer/ui.f.glsl @@ -0,0 +1,61 @@ +#version 450 + +layout(set = 0, binding = 0) uniform sampler2D tex; + +layout(location = 0) in vec2 frag_uv; +layout(location = 1) in vec4 frag_col; +layout(location = 2) in vec2 frag_rect_min; +layout(location = 3) in vec2 frag_rect_max; +layout(location = 4) in vec4 frag_corner_radii; +layout(location = 5) in float frag_border_thickness; +layout(location = 6) in float frag_softness; +layout(location = 7) in float frag_mode; +layout(location = 8) in vec2 frag_pixel_pos; + +layout(location = 0) out vec4 out_color; + +float rounded_rect_sdf(vec2 sample_pos, vec2 rect_center, vec2 rect_half_size, float radius) { + vec2 d = abs(sample_pos - rect_center) - rect_half_size + vec2(radius, radius); + return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - radius; +} + +void main() { + vec4 col = frag_col; + + if (frag_mode > 1.5) { + // RGBA textured mode: sample all channels, multiply by vertex color + vec4 tex_sample = texture(tex, frag_uv); + col *= tex_sample; + } else if (frag_mode > 0.5) { + // Alpha-only textured mode: sample R channel as alpha (font atlas) + float alpha = texture(tex, frag_uv).r; + col.a *= alpha; + } else { + // SDF rounded rect mode + vec2 pixel_pos = frag_pixel_pos; + vec2 rect_center = (frag_rect_min + frag_rect_max) * 0.5; + vec2 rect_half_size = (frag_rect_max - frag_rect_min) * 0.5; + // corner_radii = (TL, TR, BR, BL) - select radius by quadrant + float radius = (pixel_pos.x < rect_center.x) + ? ((pixel_pos.y < rect_center.y) ? frag_corner_radii.x : frag_corner_radii.w) + : ((pixel_pos.y < rect_center.y) ? frag_corner_radii.y : frag_corner_radii.z); + float softness = max(frag_softness, 0.5); + float dist = rounded_rect_sdf(pixel_pos, rect_center, rect_half_size, radius); + + if (frag_border_thickness > 0) { + float inner_dist = dist + frag_border_thickness; + float outer_alpha = 1.0 - smoothstep(-softness, softness, dist); + float inner_alpha = smoothstep(-softness, softness, inner_dist); + col.a *= outer_alpha * inner_alpha; + } else { + col.a *= 1.0 - smoothstep(-softness, softness, dist); + } + } + + // Dither to reduce gradient banding (interleaved gradient noise) + float dither = fract(52.9829189 * fract(dot(frag_pixel_pos, vec2(0.06711056, 0.00583715)))) - 0.5; + col.rgb += dither / 255.0; + + if (col.a < 0.002) discard; + out_color = col; +} diff --git a/src/renderer/ui.v.glsl b/src/renderer/ui.v.glsl new file mode 100644 index 0000000..338e5b7 --- /dev/null +++ b/src/renderer/ui.v.glsl @@ -0,0 +1,43 @@ +#version 450 + +layout(push_constant) uniform PushConstants { + vec2 viewport_size; + vec2 _padding; +} pc; + +layout(location = 0) in vec2 in_pos; +layout(location = 1) in vec2 in_uv; +layout(location = 2) in vec4 in_col; +layout(location = 3) in vec2 in_rect_min; +layout(location = 4) in vec2 in_rect_max; +layout(location = 5) in vec4 in_corner_radii; +layout(location = 6) in float in_border_thickness; +layout(location = 7) in float in_softness; +layout(location = 8) in float in_mode; + +layout(location = 0) out vec2 frag_uv; +layout(location = 1) out vec4 frag_col; +layout(location = 2) out vec2 frag_rect_min; +layout(location = 3) out vec2 frag_rect_max; +layout(location = 4) out vec4 frag_corner_radii; +layout(location = 5) out float frag_border_thickness; +layout(location = 6) out float frag_softness; +layout(location = 7) out float frag_mode; +layout(location = 8) out vec2 frag_pixel_pos; + +void main() { + vec2 ndc; + ndc.x = (in_pos.x / pc.viewport_size.x) * 2.0 - 1.0; + ndc.y = (in_pos.y / pc.viewport_size.y) * 2.0 - 1.0; // Vulkan Y is top-down already + gl_Position = vec4(ndc, 0.0, 1.0); + + frag_uv = in_uv; + frag_col = in_col; + frag_rect_min = in_rect_min; + frag_rect_max = in_rect_max; + frag_corner_radii = in_corner_radii; + frag_border_thickness = in_border_thickness; + frag_softness = in_softness; + frag_mode = in_mode; + frag_pixel_pos = in_pos; +}