#include "renderer/renderer.h" #include "ui/ui_core.h" #include "ui/ui_theme.h" #import #import #import #import #import #include #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 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 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 device; id command_queue; CAMetalLayer *metal_layer; id pipeline_state; dispatch_semaphore_t frame_semaphore; // Double-buffered vertex/index buffers id vertex_buffers[NUM_BACK_BUFFERS]; id index_buffers[NUM_BACK_BUFFERS]; // Font atlas id font_texture; id font_sampler; GlyphInfo glyphs[GLYPH_COUNT]; F32 font_atlas_size; F32 font_line_height; // Text measurement (Core Text) CTFontRef measure_font; F32 measure_font_size; }; //////////////////////////////// // 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 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 vert_fn = [library newFunctionWithName:@"vertex_main"]; id 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; 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; return true; } void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) { dispatch_semaphore_wait(r->frame_semaphore, DISPATCH_TIME_FOREVER); @autoreleasepool { id drawable = [r->metal_layer nextDrawable]; if (!drawable) { dispatch_semaphore_signal(r->frame_semaphore); return; } 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(0.12, 0.12, 0.13, 1.0); id cmd_buf = [r->command_queue commandBuffer]; id 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; 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; }; 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: { 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) { 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_batch(); } [encoder endEncoding]; [cmd_buf presentDrawable:drawable]; __block dispatch_semaphore_t sem = r->frame_semaphore; [cmd_buf addCompletedHandler:^(id _Nonnull) { dispatch_semaphore_signal(sem); }]; [cmd_buf commit]; } r->frame_index++; } 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); }