1264 lines
46 KiB
C++
1264 lines
46 KiB
C++
#include "renderer/renderer.h"
|
|
#include "ui/ui_core.h"
|
|
#include "ui/ui_theme.h"
|
|
|
|
#include <d3d12.h>
|
|
#include <dxgi1_5.h>
|
|
#include <d3dcompiler.h>
|
|
#include <math.h>
|
|
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
#include <windows.h>
|
|
|
|
#ifdef _DEBUG
|
|
#define DX12_ENABLE_DEBUG_LAYER
|
|
#endif
|
|
|
|
#ifdef DX12_ENABLE_DEBUG_LAYER
|
|
#include <dxgidebug.h>
|
|
#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 > 0.5) {
|
|
// Textured glyph mode: sample font atlas alpha
|
|
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);
|
|
}
|
|
}
|
|
|
|
if (col.a < 0.002) discard;
|
|
return col;
|
|
}
|
|
)";
|
|
|
|
////////////////////////////////
|
|
// Frame context
|
|
|
|
struct FrameContext {
|
|
ID3D12CommandAllocator *command_allocator;
|
|
UINT64 fence_value;
|
|
};
|
|
|
|
////////////////////////////////
|
|
// Renderer struct
|
|
|
|
struct Renderer {
|
|
HWND hwnd;
|
|
int32_t width;
|
|
int32_t height;
|
|
int32_t frame_count;
|
|
UINT frame_index;
|
|
|
|
ID3D12Device *device;
|
|
ID3D12CommandQueue *command_queue;
|
|
IDXGISwapChain3 *swap_chain;
|
|
HANDLE swap_chain_waitable;
|
|
bool swap_chain_occluded;
|
|
bool 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 (double-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;
|
|
|
|
// GDI text measurement
|
|
HDC measure_dc;
|
|
HFONT measure_font;
|
|
F32 measure_font_size;
|
|
};
|
|
|
|
////////////////////////////////
|
|
// DX12 infrastructure
|
|
|
|
static bool 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 bool 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 bool 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 (int 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 bool create_frame_resources(Renderer *r) {
|
|
for (int 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 bool 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
|
|
|
|
static void init_text_measurement(Renderer *r) {
|
|
r->measure_dc = CreateCompatibleDC(nullptr);
|
|
r->measure_font_size = 0;
|
|
r->measure_font = nullptr;
|
|
}
|
|
|
|
static void ensure_measure_font(Renderer *r, F32 font_size) {
|
|
if (r->measure_font && r->measure_font_size == font_size) return;
|
|
if (r->measure_font) DeleteObject(r->measure_font);
|
|
|
|
r->measure_font = CreateFontW(
|
|
-(int)(font_size + 0.5f), 0, 0, 0,
|
|
FW_NORMAL, FALSE, FALSE, FALSE,
|
|
DEFAULT_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS,
|
|
CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_DONTCARE,
|
|
L"Segoe UI"
|
|
);
|
|
r->measure_font_size = font_size;
|
|
SelectObject(r->measure_dc, r->measure_font);
|
|
}
|
|
|
|
static bool create_font_atlas(Renderer *r, F32 font_size) {
|
|
const int SS = 2; // supersample factor
|
|
F32 render_size = font_size * SS;
|
|
int render_w = FONT_ATLAS_W * SS;
|
|
int render_h = FONT_ATLAS_H * SS;
|
|
|
|
r->font_atlas_size = font_size;
|
|
|
|
// Create a GDI bitmap to render glyphs at supersampled resolution
|
|
HDC dc = CreateCompatibleDC(nullptr);
|
|
HFONT font = CreateFontW(
|
|
-(int)(render_size + 0.5f), 0, 0, 0,
|
|
FW_NORMAL, FALSE, FALSE, FALSE,
|
|
DEFAULT_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS,
|
|
ANTIALIASED_QUALITY, DEFAULT_PITCH | FF_DONTCARE,
|
|
L"Segoe UI"
|
|
);
|
|
SelectObject(dc, font);
|
|
|
|
// Get line height (at supersample scale, divide back to get 1x)
|
|
TEXTMETRICW tm;
|
|
GetTextMetricsW(dc, &tm);
|
|
r->font_line_height = (F32)tm.tmHeight / SS;
|
|
|
|
// Create DIB section at supersampled resolution
|
|
BITMAPINFO bmi = {};
|
|
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
|
bmi.bmiHeader.biWidth = render_w;
|
|
bmi.bmiHeader.biHeight = -render_h; // top-down
|
|
bmi.bmiHeader.biPlanes = 1;
|
|
bmi.bmiHeader.biBitCount = 32;
|
|
bmi.bmiHeader.biCompression = BI_RGB;
|
|
|
|
void *dib_bits = nullptr;
|
|
HBITMAP dib = CreateDIBSection(dc, &bmi, DIB_RGB_COLORS, &dib_bits, nullptr, 0);
|
|
SelectObject(dc, dib);
|
|
SelectObject(dc, font);
|
|
|
|
// Clear to black
|
|
memset(dib_bits, 0, render_w * render_h * 4);
|
|
|
|
SetTextColor(dc, RGB(255, 255, 255));
|
|
SetBkMode(dc, TRANSPARENT);
|
|
|
|
// Render each glyph at supersampled resolution
|
|
int pen_x = SS, pen_y = SS;
|
|
int row_height = 0;
|
|
|
|
for (int i = 0; i < GLYPH_COUNT; i++) {
|
|
char ch = (char)(GLYPH_FIRST + i);
|
|
SIZE ch_size = {};
|
|
GetTextExtentPoint32A(dc, &ch, 1, &ch_size);
|
|
|
|
int gw = ch_size.cx + 2 * SS; // padding scaled by SS
|
|
int gh = ch_size.cy + 2 * SS;
|
|
|
|
if (pen_x + gw >= render_w) {
|
|
pen_x = SS;
|
|
pen_y += row_height + SS;
|
|
row_height = 0;
|
|
}
|
|
|
|
if (pen_y + gh >= render_h) break; // out of space
|
|
|
|
TextOutA(dc, pen_x + SS, pen_y + SS, &ch, 1);
|
|
|
|
// UVs are fractional — pen_x/render_w == (pen_x/SS)/FONT_ATLAS_W
|
|
r->glyphs[i].u0 = (F32)pen_x / (F32)render_w;
|
|
r->glyphs[i].v0 = (F32)pen_y / (F32)render_h;
|
|
r->glyphs[i].u1 = (F32)(pen_x + gw) / (F32)render_w;
|
|
r->glyphs[i].v1 = (F32)(pen_y + gh) / (F32)render_h;
|
|
r->glyphs[i].w = (F32)gw / SS; // store at 1x scale
|
|
r->glyphs[i].h = (F32)gh / SS;
|
|
r->glyphs[i].x_advance = (F32)ch_size.cx / SS;
|
|
|
|
if (gh > row_height) row_height = gh;
|
|
pen_x += gw + SS;
|
|
}
|
|
|
|
GdiFlush();
|
|
|
|
// Box-filter downsample from supersampled resolution to atlas resolution
|
|
U8 *atlas_data = (U8 *)malloc(FONT_ATLAS_W * FONT_ATLAS_H);
|
|
U8 *src = (U8 *)dib_bits;
|
|
for (int y = 0; y < FONT_ATLAS_H; y++) {
|
|
for (int x = 0; x < FONT_ATLAS_W; x++) {
|
|
int sum = 0;
|
|
for (int sy = 0; sy < SS; sy++) {
|
|
for (int sx = 0; sx < SS; sx++) {
|
|
int src_idx = ((y * SS + sy) * render_w + (x * SS + sx)) * 4;
|
|
sum += src[src_idx + 2]; // R channel from BGRA
|
|
}
|
|
}
|
|
float a = (float)sum / (float)(SS * SS * 255);
|
|
a = powf(a, 0.55f);
|
|
atlas_data[y * FONT_ATLAS_W + x] = (U8)(a * 255.0f + 0.5f);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
DeleteObject(dib);
|
|
DeleteObject(font);
|
|
DeleteDC(dc);
|
|
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 (int 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);
|
|
DeleteObject(dib);
|
|
DeleteObject(font);
|
|
DeleteDC(dc);
|
|
|
|
// 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 bool 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 bool create_ui_buffers(Renderer *r) {
|
|
D3D12_HEAP_PROPERTIES heap_props = {};
|
|
heap_props.Type = D3D12_HEAP_TYPE_UPLOAD;
|
|
|
|
for (int 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 (int 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, int32_t length, float font_size, void *user_data) {
|
|
Renderer *r = (Renderer *)user_data;
|
|
if (!r || length == 0) return v2f32(0, font_size);
|
|
|
|
ensure_measure_font(r, font_size);
|
|
|
|
SIZE sz = {};
|
|
GetTextExtentPoint32A(r->measure_dc, text, length, &sz);
|
|
return v2f32((F32)sz.cx, (F32)sz.cy);
|
|
}
|
|
|
|
////////////////////////////////
|
|
// Quad emission helpers
|
|
|
|
struct DrawBatch {
|
|
UIVertex *vertices;
|
|
U32 *indices;
|
|
U32 vertex_count;
|
|
U32 index_count;
|
|
};
|
|
|
|
static void emit_quad(DrawBatch *batch,
|
|
float x0, float y0, float x1, float y1,
|
|
float u0, float v0, float u1, float v1,
|
|
float cr, float cg, float cb, float ca,
|
|
float rmin_x, float rmin_y, float rmax_x, float rmax_y,
|
|
float cr_tl, float cr_tr, float cr_br, float cr_bl,
|
|
float border_thickness, float softness, float 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
|
|
float px0 = x0, py0 = y0, px1 = x1, py1 = y1;
|
|
if (mode < 0.5f) {
|
|
float 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 (int 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_rect(DrawBatch *batch,
|
|
float x0, float y0, float x1, float y1,
|
|
float cr, float cg, float cb, float ca,
|
|
float cr_tl, float cr_tr, float cr_br, float cr_bl,
|
|
float border_thickness, float 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,
|
|
float x0, float y0, float x1, float y1,
|
|
float tr, float tg, float tb, float ta,
|
|
float br, float bg, float bb_, float ba,
|
|
float cr_tl, float cr_tr, float cr_br, float cr_bl,
|
|
float 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];
|
|
|
|
float pad = softness + 1.0f;
|
|
float 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 (int 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, int32_t text_len,
|
|
uint16_t font_size)
|
|
{
|
|
if (text_len == 0 || color.a < 0.1f) return;
|
|
|
|
// Color is 0-255 in Clay convention, normalize to 0-1
|
|
float cr = color.r / 255.f;
|
|
float cg = color.g / 255.f;
|
|
float cb = color.b / 255.f;
|
|
float 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 (int32_t i = 0; i < text_len; i++) {
|
|
char ch = text[i];
|
|
if (ch < GLYPH_FIRST || ch > GLYPH_LAST) {
|
|
if (ch == ' ') {
|
|
int gi = ' ' - GLYPH_FIRST;
|
|
if (gi >= 0 && gi < GLYPH_COUNT)
|
|
x += r->glyphs[gi].x_advance * scale;
|
|
continue;
|
|
}
|
|
ch = '?';
|
|
}
|
|
int 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) {
|
|
if (batch->index_count == 0) return;
|
|
|
|
r->command_list->SetPipelineState(r->pipeline_state);
|
|
r->command_list->SetGraphicsRootSignature(r->root_signature);
|
|
|
|
float constants[4] = { (float)r->width, (float)r->height, 0, 0 };
|
|
r->command_list->SetGraphicsRoot32BitConstants(0, 4, constants, 0);
|
|
|
|
// Bind font texture
|
|
r->command_list->SetDescriptorHeaps(1, &r->srv_heap);
|
|
r->command_list->SetGraphicsRootDescriptorTable(1,
|
|
r->srv_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(batch->index_count, 1, 0, 0, 0);
|
|
|
|
batch->vertex_count = 0;
|
|
batch->index_count = 0;
|
|
}
|
|
|
|
////////////////////////////////
|
|
// 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_text_measurement(r);
|
|
if (!create_font_atlas(r, 15.0f)) goto fail;
|
|
|
|
return r;
|
|
|
|
fail:
|
|
renderer_destroy(r);
|
|
return nullptr;
|
|
}
|
|
|
|
void renderer_destroy(Renderer *r) {
|
|
if (!r) return;
|
|
|
|
wait_for_pending(r);
|
|
|
|
for (int 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();
|
|
}
|
|
if (r->font_texture) r->font_texture->Release();
|
|
if (r->pipeline_state) r->pipeline_state->Release();
|
|
if (r->root_signature) r->root_signature->Release();
|
|
|
|
if (r->measure_font) DeleteObject(r->measure_font);
|
|
if (r->measure_dc) DeleteDC(r->measure_dc);
|
|
|
|
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 (int i = 0; i < r->frame_count; i++)
|
|
if (r->frames[i].command_allocator) r->frames[i].command_allocator->Release();
|
|
if (r->command_queue) r->command_queue->Release();
|
|
if (r->command_list) r->command_list->Release();
|
|
if (r->rtv_heap) r->rtv_heap->Release();
|
|
if (r->srv_heap) r->srv_heap->Release();
|
|
if (r->fence) r->fence->Release();
|
|
if (r->fence_event) CloseHandle(r->fence_event);
|
|
if (r->device) r->device->Release();
|
|
|
|
#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;
|
|
}
|
|
|
|
bool renderer_begin_frame(Renderer *r) {
|
|
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 float clear_color[4] = { 0.12f, 0.12f, 0.13f, 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;
|
|
|
|
for (int32_t 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;
|
|
float cr_norm = c.r / 255.f;
|
|
float cg_norm = c.g / 255.f;
|
|
float cb_norm = c.b / 255.f;
|
|
float ca_norm = c.a / 255.f;
|
|
|
|
// Draw individual border sides as thin rects
|
|
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: {
|
|
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
|
|
flush_batch(r, &batch, buf_idx);
|
|
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: {
|
|
flush_batch(r, &batch, buf_idx);
|
|
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) {
|
|
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);
|
|
}
|
|
}
|
|
} break;
|
|
|
|
case CLAY_RENDER_COMMAND_TYPE_IMAGE:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Flush remaining
|
|
flush_batch(r, &batch, buf_idx);
|
|
}
|
|
|
|
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_resize(Renderer *r, int32_t width, int32_t 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;
|
|
}
|