#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)); }