898 lines
33 KiB
Plaintext
898 lines
33 KiB
Plaintext
#include "renderer/renderer.h"
|
|
#include "ui/ui_core.h"
|
|
#include "ui/ui_icons.h"
|
|
|
|
#import <Metal/Metal.h>
|
|
#import <QuartzCore/CAMetalLayer.h>
|
|
#import <Cocoa/Cocoa.h>
|
|
#import <CoreText/CoreText.h>
|
|
#import <CoreGraphics/CoreGraphics.h>
|
|
#include <math.h>
|
|
|
|
#define NUM_BACK_BUFFERS 2
|
|
#define MAX_VERTICES (64 * 1024)
|
|
#define MAX_INDICES (MAX_VERTICES * 3)
|
|
|
|
// Font atlas
|
|
#define FONT_ATLAS_W 1024
|
|
#define FONT_ATLAS_H 1024
|
|
#define GLYPH_FIRST 32
|
|
#define GLYPH_LAST 126
|
|
#define GLYPH_COUNT (GLYPH_LAST - GLYPH_FIRST + 1)
|
|
|
|
////////////////////////////////
|
|
// Vertex format — matches DX12 UIVertex exactly
|
|
|
|
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;
|
|
F32 w, h;
|
|
F32 x_advance;
|
|
};
|
|
|
|
////////////////////////////////
|
|
// Metal shader (MSL) — port of HLSL SDF shader
|
|
|
|
static const char *g_shader_msl = R"(
|
|
#include <metal_stdlib>
|
|
using namespace metal;
|
|
|
|
struct Vertex {
|
|
float2 pos [[attribute(0)]];
|
|
float2 uv [[attribute(1)]];
|
|
float4 col [[attribute(2)]];
|
|
float2 rect_min [[attribute(3)]];
|
|
float2 rect_max [[attribute(4)]];
|
|
float4 corner_radii [[attribute(5)]];
|
|
float border_thickness [[attribute(6)]];
|
|
float softness [[attribute(7)]];
|
|
float mode [[attribute(8)]];
|
|
};
|
|
|
|
struct Fragment {
|
|
float4 pos [[position]];
|
|
float2 uv;
|
|
float4 col;
|
|
float2 rect_min;
|
|
float2 rect_max;
|
|
float4 corner_radii;
|
|
float border_thickness;
|
|
float softness;
|
|
float mode;
|
|
};
|
|
|
|
struct Constants {
|
|
float2 viewport_size;
|
|
};
|
|
|
|
vertex Fragment vertex_main(Vertex in [[stage_in]],
|
|
constant Constants &cb [[buffer(1)]]) {
|
|
Fragment out;
|
|
float2 ndc;
|
|
ndc.x = (in.pos.x / cb.viewport_size.x) * 2.0 - 1.0;
|
|
ndc.y = 1.0 - (in.pos.y / cb.viewport_size.y) * 2.0;
|
|
out.pos = float4(ndc, 0.0, 1.0);
|
|
out.uv = in.uv;
|
|
out.col = in.col;
|
|
out.rect_min = in.rect_min;
|
|
out.rect_max = in.rect_max;
|
|
out.corner_radii = in.corner_radii;
|
|
out.border_thickness = in.border_thickness;
|
|
out.softness = in.softness;
|
|
out.mode = in.mode;
|
|
return out;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
fragment float4 fragment_main(Fragment in [[stage_in]],
|
|
texture2d<float> font_tex [[texture(0)]],
|
|
sampler font_smp [[sampler(0)]]) {
|
|
float4 col = in.col;
|
|
|
|
if (in.mode > 0.5) {
|
|
float alpha = font_tex.sample(font_smp, in.uv).r;
|
|
col.a *= alpha;
|
|
} else {
|
|
float2 pixel_pos = in.pos.xy;
|
|
float2 rect_center = (in.rect_min + in.rect_max) * 0.5;
|
|
float2 rect_half_size = (in.rect_max - in.rect_min) * 0.5;
|
|
float radius = (pixel_pos.x < rect_center.x)
|
|
? ((pixel_pos.y < rect_center.y) ? in.corner_radii.x : in.corner_radii.w)
|
|
: ((pixel_pos.y < rect_center.y) ? in.corner_radii.y : in.corner_radii.z);
|
|
float softness = max(in.softness, 0.5);
|
|
float dist = rounded_rect_sdf(pixel_pos, rect_center, rect_half_size, radius);
|
|
|
|
if (in.border_thickness > 0) {
|
|
float inner_dist = dist + in.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_fragment();
|
|
return col;
|
|
}
|
|
)";
|
|
|
|
////////////////////////////////
|
|
// Renderer struct
|
|
|
|
struct Renderer {
|
|
int32_t width;
|
|
int32_t height;
|
|
int32_t frame_count;
|
|
uint32_t frame_index;
|
|
F32 backing_scale;
|
|
|
|
id<MTLDevice> device;
|
|
id<MTLCommandQueue> command_queue;
|
|
CAMetalLayer *metal_layer;
|
|
id<MTLRenderPipelineState> pipeline_state;
|
|
|
|
dispatch_semaphore_t frame_semaphore;
|
|
|
|
// Double-buffered vertex/index buffers
|
|
id<MTLBuffer> vertex_buffers[NUM_BACK_BUFFERS];
|
|
id<MTLBuffer> index_buffers[NUM_BACK_BUFFERS];
|
|
|
|
// Font atlas
|
|
id<MTLTexture> font_texture;
|
|
id<MTLSamplerState> font_sampler;
|
|
GlyphInfo glyphs[GLYPH_COUNT];
|
|
F32 font_atlas_size;
|
|
F32 font_line_height;
|
|
|
|
// Icon atlas
|
|
id<MTLTexture> icon_texture;
|
|
|
|
// Text measurement (Core Text)
|
|
CTFontRef measure_font;
|
|
F32 measure_font_size;
|
|
|
|
// Current drawable (acquired in begin_frame)
|
|
id<CAMetalDrawable> current_drawable;
|
|
|
|
// Clear color
|
|
F32 clear_r, clear_g, clear_b;
|
|
};
|
|
|
|
////////////////////////////////
|
|
// Font atlas (Core Text + CoreGraphics)
|
|
|
|
static bool create_font_atlas(Renderer *r, F32 font_size) {
|
|
const int SS = 2;
|
|
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 Core Text font (system font = SF Pro on macOS)
|
|
CTFontRef font = CTFontCreateUIFontForLanguage(kCTFontUIFontSystem, render_size, nullptr);
|
|
if (!font) return false;
|
|
|
|
// Get line height
|
|
F32 ascent = (F32)CTFontGetAscent(font);
|
|
F32 descent = (F32)CTFontGetDescent(font);
|
|
F32 leading = (F32)CTFontGetLeading(font);
|
|
r->font_line_height = (ascent + descent + leading) / SS;
|
|
|
|
// Create bitmap context at supersampled resolution
|
|
CGColorSpaceRef color_space = CGColorSpaceCreateDeviceGray();
|
|
CGContextRef ctx = CGBitmapContextCreate(nullptr, render_w, render_h, 8, render_w,
|
|
color_space, kCGImageAlphaNone);
|
|
CGColorSpaceRelease(color_space);
|
|
if (!ctx) { CFRelease(font); return false; }
|
|
|
|
// Clear to black
|
|
CGContextSetGrayFillColor(ctx, 0.0, 1.0);
|
|
CGContextFillRect(ctx, CGRectMake(0, 0, render_w, render_h));
|
|
|
|
// White text
|
|
CGContextSetGrayFillColor(ctx, 1.0, 1.0);
|
|
|
|
// Flip CG context so (0,0) is top-left, matching our pen_y convention
|
|
CGContextTranslateCTM(ctx, 0, render_h);
|
|
CGContextScaleCTM(ctx, 1.0, -1.0);
|
|
|
|
// In the flipped context, Core Text draws upside down unless we undo the
|
|
// text matrix flip. Set the text matrix to flip Y back for glyph rendering.
|
|
CGContextSetTextMatrix(ctx, CGAffineTransformMake(1, 0, 0, -1, 0, 0));
|
|
|
|
// Render each glyph
|
|
int pen_x = SS, pen_y = SS;
|
|
int row_height = 0;
|
|
|
|
NSDictionary *attrs = @{
|
|
(id)kCTFontAttributeName: (__bridge id)font,
|
|
(id)kCTForegroundColorFromContextAttributeName: @YES
|
|
};
|
|
|
|
for (int i = 0; i < GLYPH_COUNT; i++) {
|
|
char ch = (char)(GLYPH_FIRST + i);
|
|
NSString *str = [[NSString alloc] initWithBytes:&ch length:1 encoding:NSASCIIStringEncoding];
|
|
NSAttributedString *astr = [[NSAttributedString alloc] initWithString:str attributes:attrs];
|
|
CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)astr);
|
|
|
|
CGRect bounds = CTLineGetBoundsWithOptions(line, 0);
|
|
int gw = (int)ceilf((float)bounds.size.width) + 2 * SS;
|
|
int gh = (int)ceilf((float)(ascent + descent)) + 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) { CFRelease(line); break; }
|
|
|
|
// Context is flipped to top-left origin. Position baseline:
|
|
// pen_y + SS is the top of the glyph cell, baseline is ascent below that.
|
|
F32 draw_x = (F32)(pen_x + SS) - (F32)bounds.origin.x;
|
|
F32 draw_y = (F32)(pen_y + SS) + ascent;
|
|
|
|
CGContextSetTextPosition(ctx, draw_x, draw_y);
|
|
CTLineDraw(line, ctx);
|
|
|
|
// UVs (same fractional math as DX12)
|
|
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;
|
|
r->glyphs[i].h = (F32)gh / SS;
|
|
r->glyphs[i].x_advance = (F32)bounds.size.width / SS;
|
|
|
|
if (gh > row_height) row_height = gh;
|
|
pen_x += gw + SS;
|
|
|
|
CFRelease(line);
|
|
}
|
|
|
|
// Box-filter downsample (context is flipped, so bitmap is already top-down)
|
|
uint8_t *src = (uint8_t *)CGBitmapContextGetData(ctx);
|
|
uint8_t *atlas_data = (uint8_t *)malloc(FONT_ATLAS_W * FONT_ATLAS_H);
|
|
|
|
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);
|
|
sum += src[src_idx];
|
|
}
|
|
}
|
|
float a = (float)sum / (float)(SS * SS * 255);
|
|
a = powf(a, 0.55f);
|
|
atlas_data[y * FONT_ATLAS_W + x] = (uint8_t)(a * 255.0f + 0.5f);
|
|
}
|
|
}
|
|
|
|
CGContextRelease(ctx);
|
|
CFRelease(font);
|
|
|
|
// Create Metal texture
|
|
MTLTextureDescriptor *tex_desc = [[MTLTextureDescriptor alloc] init];
|
|
tex_desc.pixelFormat = MTLPixelFormatR8Unorm;
|
|
tex_desc.width = FONT_ATLAS_W;
|
|
tex_desc.height = FONT_ATLAS_H;
|
|
tex_desc.usage = MTLTextureUsageShaderRead;
|
|
|
|
r->font_texture = [r->device newTextureWithDescriptor:tex_desc];
|
|
[r->font_texture replaceRegion:MTLRegionMake2D(0, 0, FONT_ATLAS_W, FONT_ATLAS_H)
|
|
mipmapLevel:0
|
|
withBytes:atlas_data
|
|
bytesPerRow:FONT_ATLAS_W];
|
|
|
|
free(atlas_data);
|
|
|
|
// Create sampler
|
|
MTLSamplerDescriptor *samp_desc = [[MTLSamplerDescriptor alloc] init];
|
|
samp_desc.minFilter = MTLSamplerMinMagFilterLinear;
|
|
samp_desc.magFilter = MTLSamplerMinMagFilterLinear;
|
|
samp_desc.sAddressMode = MTLSamplerAddressModeClampToEdge;
|
|
samp_desc.tAddressMode = MTLSamplerAddressModeClampToEdge;
|
|
r->font_sampler = [r->device newSamplerStateWithDescriptor:samp_desc];
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////
|
|
// Text measurement
|
|
|
|
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) CFRelease(r->measure_font);
|
|
r->measure_font = CTFontCreateUIFontForLanguage(kCTFontUIFontSystem, font_size, nullptr);
|
|
r->measure_font_size = font_size;
|
|
}
|
|
|
|
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);
|
|
|
|
NSString *str = [[NSString alloc] initWithBytes:text length:length encoding:NSUTF8StringEncoding];
|
|
if (!str) return v2f32(0, font_size);
|
|
|
|
NSDictionary *attrs = @{ (id)kCTFontAttributeName: (__bridge id)r->measure_font };
|
|
NSAttributedString *astr = [[NSAttributedString alloc] initWithString:str attributes:attrs];
|
|
CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)astr);
|
|
|
|
CGRect bounds = CTLineGetBoundsWithOptions(line, 0);
|
|
CFRelease(line);
|
|
|
|
return v2f32((F32)bounds.size.width, (F32)bounds.size.height);
|
|
}
|
|
|
|
////////////////////////////////
|
|
// Draw batch and quad emission (same logic as DX12)
|
|
|
|
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];
|
|
|
|
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;
|
|
|
|
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;
|
|
|
|
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;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////
|
|
// Public API
|
|
|
|
Renderer *renderer_create(RendererDesc *desc) {
|
|
Renderer *r = new Renderer();
|
|
memset(r, 0, sizeof(*r));
|
|
|
|
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;
|
|
|
|
// Get the NSView and attach a CAMetalLayer
|
|
NSView *view = (__bridge NSView *)desc->window_handle;
|
|
[view setWantsLayer:YES];
|
|
|
|
r->device = MTLCreateSystemDefaultDevice();
|
|
if (!r->device) { delete r; return nullptr; }
|
|
|
|
r->command_queue = [r->device newCommandQueue];
|
|
|
|
CAMetalLayer *layer = [CAMetalLayer layer];
|
|
layer.device = r->device;
|
|
layer.pixelFormat = MTLPixelFormatBGRA8Unorm;
|
|
layer.framebufferOnly = YES;
|
|
|
|
NSWindow *window = [view window];
|
|
r->backing_scale = (F32)[window backingScaleFactor];
|
|
layer.contentsScale = r->backing_scale;
|
|
layer.drawableSize = CGSizeMake(r->width, r->height);
|
|
|
|
[view setLayer:layer];
|
|
r->metal_layer = layer;
|
|
|
|
r->frame_semaphore = dispatch_semaphore_create(NUM_BACK_BUFFERS);
|
|
|
|
// Compile shaders
|
|
NSError *error = nil;
|
|
id<MTLLibrary> library = [r->device newLibraryWithSource:
|
|
[NSString stringWithUTF8String:g_shader_msl] options:nil error:&error];
|
|
if (!library) {
|
|
NSLog(@"Metal shader compile error: %@", error);
|
|
delete r;
|
|
return nullptr;
|
|
}
|
|
|
|
id<MTLFunction> vert_fn = [library newFunctionWithName:@"vertex_main"];
|
|
id<MTLFunction> frag_fn = [library newFunctionWithName:@"fragment_main"];
|
|
|
|
// Vertex descriptor
|
|
MTLVertexDescriptor *vtx_desc = [[MTLVertexDescriptor alloc] init];
|
|
vtx_desc.attributes[0].format = MTLVertexFormatFloat2;
|
|
vtx_desc.attributes[0].offset = offsetof(UIVertex, pos);
|
|
vtx_desc.attributes[0].bufferIndex = 0;
|
|
vtx_desc.attributes[1].format = MTLVertexFormatFloat2;
|
|
vtx_desc.attributes[1].offset = offsetof(UIVertex, uv);
|
|
vtx_desc.attributes[1].bufferIndex = 0;
|
|
vtx_desc.attributes[2].format = MTLVertexFormatFloat4;
|
|
vtx_desc.attributes[2].offset = offsetof(UIVertex, col);
|
|
vtx_desc.attributes[2].bufferIndex = 0;
|
|
vtx_desc.attributes[3].format = MTLVertexFormatFloat2;
|
|
vtx_desc.attributes[3].offset = offsetof(UIVertex, rect_min);
|
|
vtx_desc.attributes[3].bufferIndex = 0;
|
|
vtx_desc.attributes[4].format = MTLVertexFormatFloat2;
|
|
vtx_desc.attributes[4].offset = offsetof(UIVertex, rect_max);
|
|
vtx_desc.attributes[4].bufferIndex = 0;
|
|
vtx_desc.attributes[5].format = MTLVertexFormatFloat4;
|
|
vtx_desc.attributes[5].offset = offsetof(UIVertex, corner_radii);
|
|
vtx_desc.attributes[5].bufferIndex = 0;
|
|
vtx_desc.attributes[6].format = MTLVertexFormatFloat;
|
|
vtx_desc.attributes[6].offset = offsetof(UIVertex, border_thickness);
|
|
vtx_desc.attributes[6].bufferIndex = 0;
|
|
vtx_desc.attributes[7].format = MTLVertexFormatFloat;
|
|
vtx_desc.attributes[7].offset = offsetof(UIVertex, softness);
|
|
vtx_desc.attributes[7].bufferIndex = 0;
|
|
vtx_desc.attributes[8].format = MTLVertexFormatFloat;
|
|
vtx_desc.attributes[8].offset = offsetof(UIVertex, mode);
|
|
vtx_desc.attributes[8].bufferIndex = 0;
|
|
vtx_desc.layouts[0].stride = sizeof(UIVertex);
|
|
vtx_desc.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;
|
|
|
|
// Pipeline state
|
|
MTLRenderPipelineDescriptor *pipe_desc = [[MTLRenderPipelineDescriptor alloc] init];
|
|
pipe_desc.vertexFunction = vert_fn;
|
|
pipe_desc.fragmentFunction = frag_fn;
|
|
pipe_desc.vertexDescriptor = vtx_desc;
|
|
pipe_desc.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;
|
|
pipe_desc.colorAttachments[0].blendingEnabled = YES;
|
|
pipe_desc.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha;
|
|
pipe_desc.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
|
|
pipe_desc.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd;
|
|
pipe_desc.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne;
|
|
pipe_desc.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
|
|
pipe_desc.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd;
|
|
|
|
r->pipeline_state = [r->device newRenderPipelineStateWithDescriptor:pipe_desc error:&error];
|
|
if (!r->pipeline_state) {
|
|
NSLog(@"Metal pipeline error: %@", error);
|
|
delete r;
|
|
return nullptr;
|
|
}
|
|
|
|
// Create double-buffered vertex/index buffers
|
|
for (int i = 0; i < NUM_BACK_BUFFERS; i++) {
|
|
r->vertex_buffers[i] = [r->device newBufferWithLength:MAX_VERTICES * sizeof(UIVertex)
|
|
options:MTLResourceStorageModeShared];
|
|
r->index_buffers[i] = [r->device newBufferWithLength:MAX_INDICES * sizeof(U32)
|
|
options:MTLResourceStorageModeShared];
|
|
}
|
|
|
|
// Font atlas
|
|
if (!create_font_atlas(r, 15.0f)) {
|
|
delete r;
|
|
return nullptr;
|
|
}
|
|
|
|
// Init text measurement
|
|
r->measure_font = nullptr;
|
|
r->measure_font_size = 0;
|
|
|
|
// Default clear color (dark theme bg_dark)
|
|
r->clear_r = 0.12f;
|
|
r->clear_g = 0.12f;
|
|
r->clear_b = 0.13f;
|
|
|
|
return r;
|
|
}
|
|
|
|
void renderer_destroy(Renderer *r) {
|
|
if (!r) return;
|
|
if (r->measure_font) CFRelease(r->measure_font);
|
|
delete r;
|
|
}
|
|
|
|
bool renderer_begin_frame(Renderer *r) {
|
|
if (r->width <= 0 || r->height <= 0) return false;
|
|
|
|
// Wait for an in-flight frame to finish, then acquire a drawable.
|
|
// Doing this BEFORE input sampling ensures the freshest mouse position
|
|
// is used for rendering, reducing perceived drag latency.
|
|
dispatch_semaphore_wait(r->frame_semaphore, DISPATCH_TIME_FOREVER);
|
|
r->current_drawable = [r->metal_layer nextDrawable];
|
|
if (!r->current_drawable) {
|
|
dispatch_semaphore_signal(r->frame_semaphore);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) {
|
|
@autoreleasepool {
|
|
id<CAMetalDrawable> drawable = r->current_drawable;
|
|
r->current_drawable = nil;
|
|
|
|
uint32_t buf_idx = r->frame_index % NUM_BACK_BUFFERS;
|
|
|
|
MTLRenderPassDescriptor *pass = [MTLRenderPassDescriptor renderPassDescriptor];
|
|
pass.colorAttachments[0].texture = drawable.texture;
|
|
pass.colorAttachments[0].loadAction = MTLLoadActionClear;
|
|
pass.colorAttachments[0].storeAction = MTLStoreActionStore;
|
|
pass.colorAttachments[0].clearColor = MTLClearColorMake(r->clear_r, r->clear_g, r->clear_b, 1.0);
|
|
|
|
id<MTLCommandBuffer> cmd_buf = [r->command_queue commandBuffer];
|
|
id<MTLRenderCommandEncoder> encoder = [cmd_buf renderCommandEncoderWithDescriptor:pass];
|
|
|
|
[encoder setRenderPipelineState:r->pipeline_state];
|
|
[encoder setFragmentTexture:r->font_texture atIndex:0];
|
|
[encoder setFragmentSamplerState:r->font_sampler atIndex:0];
|
|
|
|
// Viewport
|
|
MTLViewport viewport = {};
|
|
viewport.width = (double)r->width;
|
|
viewport.height = (double)r->height;
|
|
viewport.zfar = 1.0;
|
|
[encoder setViewport:viewport];
|
|
|
|
// Full scissor
|
|
MTLScissorRect full_scissor = { 0, 0, (NSUInteger)r->width, (NSUInteger)r->height };
|
|
[encoder setScissorRect:full_scissor];
|
|
|
|
// Constants
|
|
float constants[2] = { (float)r->width, (float)r->height };
|
|
[encoder setVertexBytes:constants length:sizeof(constants) atIndex:1];
|
|
|
|
// Process Clay render commands
|
|
if (render_commands.length > 0) {
|
|
DrawBatch batch = {};
|
|
batch.vertices = (UIVertex *)[r->vertex_buffers[buf_idx] contents];
|
|
batch.indices = (U32 *)[r->index_buffers[buf_idx] contents];
|
|
batch.vertex_count = 0;
|
|
batch.index_count = 0;
|
|
|
|
// Track which texture is currently bound (0 = font, 1 = icon)
|
|
int bound_texture = 0;
|
|
|
|
auto flush_batch = [&]() {
|
|
if (batch.index_count == 0) return;
|
|
|
|
[encoder setVertexBuffer:r->vertex_buffers[buf_idx] offset:0 atIndex:0];
|
|
[encoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle
|
|
indexCount:batch.index_count
|
|
indexType:MTLIndexTypeUInt32
|
|
indexBuffer:r->index_buffers[buf_idx]
|
|
indexBufferOffset:0];
|
|
|
|
batch.vertex_count = 0;
|
|
batch.index_count = 0;
|
|
};
|
|
|
|
auto bind_font_texture = [&]() {
|
|
if (bound_texture != 0) {
|
|
flush_batch();
|
|
[encoder setFragmentTexture:r->font_texture atIndex:0];
|
|
bound_texture = 0;
|
|
}
|
|
};
|
|
|
|
auto bind_icon_texture = [&]() {
|
|
if (bound_texture != 1 && r->icon_texture) {
|
|
flush_batch();
|
|
[encoder setFragmentTexture:r->icon_texture atIndex:0];
|
|
bound_texture = 1;
|
|
}
|
|
};
|
|
|
|
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;
|
|
|
|
if (border->width.top > 0) {
|
|
emit_rect(&batch, bb.x, bb.y, bb.x + bb.width, bb.y + border->width.top,
|
|
cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f);
|
|
}
|
|
if (border->width.bottom > 0) {
|
|
emit_rect(&batch, bb.x, bb.y + bb.height - border->width.bottom, bb.x + bb.width, bb.y + bb.height,
|
|
cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f);
|
|
}
|
|
if (border->width.left > 0) {
|
|
emit_rect(&batch, bb.x, bb.y, bb.x + border->width.left, bb.y + bb.height,
|
|
cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f);
|
|
}
|
|
if (border->width.right > 0) {
|
|
emit_rect(&batch, bb.x + bb.width - border->width.right, bb.y, bb.x + bb.width, bb.y + bb.height,
|
|
cr_norm, cg_norm, cb_norm, ca_norm, 0, 0, 0, 0, 0, 1.0f);
|
|
}
|
|
} break;
|
|
|
|
case CLAY_RENDER_COMMAND_TYPE_TEXT: {
|
|
bind_font_texture();
|
|
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_batch();
|
|
NSUInteger sx = (NSUInteger)Max(bb.x, 0.f);
|
|
NSUInteger sy = (NSUInteger)Max(bb.y, 0.f);
|
|
NSUInteger sw = (NSUInteger)Min(bb.width, (F32)r->width - (F32)sx);
|
|
NSUInteger sh = (NSUInteger)Min(bb.height, (F32)r->height - (F32)sy);
|
|
if (sw == 0) sw = 1;
|
|
if (sh == 0) sh = 1;
|
|
MTLScissorRect clip = { sx, sy, sw, sh };
|
|
[encoder setScissorRect:clip];
|
|
} break;
|
|
|
|
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: {
|
|
flush_batch();
|
|
[encoder setScissorRect: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_texture();
|
|
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_texture();
|
|
CustomIconData *icon = (CustomIconData *)custom->customData;
|
|
Clay_Color c = icon->color;
|
|
float cr = c.r / 255.f, cg = c.g / 255.f;
|
|
float 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, 1.0f);
|
|
}
|
|
}
|
|
} break;
|
|
|
|
case CLAY_RENDER_COMMAND_TYPE_IMAGE:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
flush_batch();
|
|
}
|
|
|
|
[encoder endEncoding];
|
|
[cmd_buf presentDrawable:drawable];
|
|
|
|
__block dispatch_semaphore_t sem = r->frame_semaphore;
|
|
[cmd_buf addCompletedHandler:^(id<MTLCommandBuffer> _Nonnull) {
|
|
dispatch_semaphore_signal(sem);
|
|
}];
|
|
|
|
[cmd_buf commit];
|
|
}
|
|
|
|
r->frame_index++;
|
|
}
|
|
|
|
void renderer_create_icon_atlas(Renderer *r, const uint8_t *data, int32_t w, int32_t h) {
|
|
MTLTextureDescriptor *tex_desc = [[MTLTextureDescriptor alloc] init];
|
|
tex_desc.pixelFormat = MTLPixelFormatR8Unorm;
|
|
tex_desc.width = w;
|
|
tex_desc.height = h;
|
|
tex_desc.usage = MTLTextureUsageShaderRead;
|
|
|
|
r->icon_texture = [r->device newTextureWithDescriptor:tex_desc];
|
|
[r->icon_texture replaceRegion:MTLRegionMake2D(0, 0, w, h)
|
|
mipmapLevel:0
|
|
withBytes:data
|
|
bytesPerRow:w];
|
|
}
|
|
|
|
void renderer_set_clear_color(Renderer *r, float cr, float cg, float cb) {
|
|
r->clear_r = cr;
|
|
r->clear_g = cg;
|
|
r->clear_b = cb;
|
|
}
|
|
|
|
void renderer_set_font_scale(Renderer *r, float scale) {
|
|
float target_size = 15.0f * scale;
|
|
if (fabsf(target_size - r->font_atlas_size) < 0.1f) return;
|
|
r->font_texture = nil;
|
|
r->font_sampler = nil;
|
|
create_font_atlas(r, target_size);
|
|
}
|
|
|
|
void renderer_resize(Renderer *r, int32_t width, int32_t height) {
|
|
if (width <= 0 || height <= 0) return;
|
|
r->width = width;
|
|
r->height = height;
|
|
|
|
NSWindow *window = [(__bridge NSView *)r->metal_layer.delegate window];
|
|
if (window) {
|
|
r->backing_scale = (F32)[window backingScaleFactor];
|
|
r->metal_layer.contentsScale = r->backing_scale;
|
|
}
|
|
r->metal_layer.drawableSize = CGSizeMake(width, height);
|
|
}
|