diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 1371b6ceb..d7b0024a1 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -920,7 +920,7 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]); // Then draw background cells - try self.drawCells(encoder, frame, frame.cells_bg, self.cells_bg.items.len); + try self.drawCellBgs(encoder, frame, self.cells_bg.items.len); // Then draw images under text try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]); @@ -1215,6 +1215,50 @@ fn drawImagePlacement( // log.debug("drawImagePlacement: {}", .{p}); } +/// Draw the cell backgrounds. +fn drawCellBgs( + self: *Metal, + encoder: objc.Object, + frame: *const FrameState, + len: usize, +) !void { + // This triggers an assertion in the Metal API if we try to draw + // with an instance count of 0 so just bail. + if (len == 0) return; + + // Use our shader pipeline + encoder.msgSend( + void, + objc.sel("setRenderPipelineState:"), + .{self.shaders.cell_bg_pipeline.value}, + ); + + // Set our buffers + encoder.msgSend( + void, + objc.sel("setVertexBuffer:offset:atIndex:"), + .{ frame.cells_bg.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) }, + ); + encoder.msgSend( + void, + objc.sel("setVertexBuffer:offset:atIndex:"), + .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) }, + ); + + encoder.msgSend( + void, + objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"), + .{ + @intFromEnum(mtl.MTLPrimitiveType.triangle), + @as(c_ulong, 6), + @intFromEnum(mtl.MTLIndexType.uint16), + self.gpu_state.instance.buffer.value, + @as(c_ulong, 0), + @as(c_ulong, len), + }, + ); +} + /// Loads some set of cell data into our buffer and issues a draw call. /// This expects all the Metal command encoder state to be setup. /// diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index d5a6baccb..6d2e9addb 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -18,6 +18,10 @@ pub const Shaders = struct { /// foreground. cell_pipeline: objc.Object, + /// The cell background shader is the shader used to render the + /// background of terminal cells. + cell_bg_pipeline: objc.Object, + /// The image shader is the shader used to render images for things /// like the Kitty image protocol. image_pipeline: objc.Object, @@ -43,6 +47,9 @@ pub const Shaders = struct { const cell_pipeline = try initCellPipeline(device, library); errdefer cell_pipeline.msgSend(void, objc.sel("release"), .{}); + const cell_bg_pipeline = try initCellBgPipeline(device, library); + errdefer cell_bg_pipeline.msgSend(void, objc.sel("release"), .{}); + const image_pipeline = try initImagePipeline(device, library); errdefer image_pipeline.msgSend(void, objc.sel("release"), .{}); @@ -66,6 +73,7 @@ pub const Shaders = struct { return .{ .library = library, .cell_pipeline = cell_pipeline, + .cell_bg_pipeline = cell_bg_pipeline, .image_pipeline = image_pipeline, .post_pipelines = post_pipelines, }; @@ -74,6 +82,7 @@ pub const Shaders = struct { pub fn deinit(self: *Shaders, alloc: Allocator) void { // Release our primary shaders self.cell_pipeline.msgSend(void, objc.sel("release"), .{}); + self.cell_bg_pipeline.msgSend(void, objc.sel("release"), .{}); self.image_pipeline.msgSend(void, objc.sel("release"), .{}); self.library.msgSend(void, objc.sel("release"), .{}); @@ -493,6 +502,173 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { return pipeline_state; } +/// This is a single parameter for the cell bg shader. +pub const CellBg = extern struct { + mode: Mode, + grid_pos: [2]f32, + cell_width: u8, + color: [4]u8, + + pub const Mode = enum(u8) { + rgb = 1, + }; +}; + +/// Initialize the cell background render pipeline for our shader library. +fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object { + // Get our vertex and fragment functions + const func_vert = func_vert: { + const str = try macos.foundation.String.createWithBytes( + "cell_bg_vertex", + .utf8, + false, + ); + defer str.release(); + + const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); + break :func_vert objc.Object.fromId(ptr.?); + }; + defer func_vert.msgSend(void, objc.sel("release"), .{}); + const func_frag = func_frag: { + const str = try macos.foundation.String.createWithBytes( + "cell_bg_fragment", + .utf8, + false, + ); + defer str.release(); + + const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); + break :func_frag objc.Object.fromId(ptr.?); + }; + defer func_frag.msgSend(void, objc.sel("release"), .{}); + + // Create the vertex descriptor. The vertex descriptor describes the + // data layout of the vertex inputs. We use indexed (or "instanced") + // rendering, so this makes it so that each instance gets a single + // Cell as input. + const vertex_desc = vertex_desc: { + const desc = init: { + const Class = objc.getClass("MTLVertexDescriptor").?; + const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); + const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); + break :init id_init; + }; + + // Our attributes are the fields of the input + const attrs = objc.Object.fromId(desc.getProperty(?*anyopaque, "attributes")); + { + const attr = attrs.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 0)}, + ); + + attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar)); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellBg, "mode"))); + attr.setProperty("bufferIndex", @as(c_ulong, 0)); + } + { + const attr = attrs.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 1)}, + ); + + attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.float2)); + attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "grid_pos"))); + attr.setProperty("bufferIndex", @as(c_ulong, 0)); + } + { + const attr = attrs.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 2)}, + ); + + attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar)); + attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "cell_width"))); + attr.setProperty("bufferIndex", @as(c_ulong, 0)); + } + { + const attr = attrs.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 3)}, + ); + + attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar4)); + attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "color"))); + attr.setProperty("bufferIndex", @as(c_ulong, 0)); + } + + // The layout describes how and when we fetch the next vertex input. + const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts")); + { + const layout = layouts.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 0)}, + ); + + // Access each Cell per instance, not per vertex. + layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance)); + layout.setProperty("stride", @as(c_ulong, @sizeOf(Cell))); + } + + break :vertex_desc desc; + }; + defer vertex_desc.msgSend(void, objc.sel("release"), .{}); + + // Create our descriptor + const desc = init: { + const Class = objc.getClass("MTLRenderPipelineDescriptor").?; + const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); + const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); + break :init id_init; + }; + defer desc.msgSend(void, objc.sel("release"), .{}); + + // Set our properties + desc.setProperty("vertexFunction", func_vert); + desc.setProperty("fragmentFunction", func_frag); + desc.setProperty("vertexDescriptor", vertex_desc); + + // Set our color attachment + const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments")); + { + const attachment = attachments.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 0)}, + ); + + // Value is MTLPixelFormatBGRA8Unorm + attachment.setProperty("pixelFormat", @as(c_ulong, 80)); + + // Blending. This is required so that our text we render on top + // of our drawable properly blends into the bg. + attachment.setProperty("blendingEnabled", true); + attachment.setProperty("rgbBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add)); + attachment.setProperty("alphaBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add)); + attachment.setProperty("sourceRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one)); + attachment.setProperty("sourceAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one)); + attachment.setProperty("destinationRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha)); + attachment.setProperty("destinationAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha)); + } + + // Make our state + var err: ?*anyopaque = null; + const pipeline_state = device.msgSend( + objc.Object, + objc.sel("newRenderPipelineStateWithDescriptor:error:"), + .{ desc, &err }, + ); + try checkError(err); + errdefer pipeline_state.msgSend(void, objc.sel("release"), .{}); + + return pipeline_state; +} + /// Initialize the image render pipeline for our shader library. fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object { // Get our vertex and fragment functions diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index 10e5804cc..a1c4389b1 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -231,6 +231,78 @@ fragment float4 uber_fragment(VertexOut in [[stage_in]], } } +//------------------------------------------------------------------- +// Cell Background Shader +//------------------------------------------------------------------- +#pragma mark - Cell BG Shader + +// The possible modes that a cell bg entry can take. +enum CellbgMode : uint8_t { + MODE_RGB = 1u, +}; + +struct CellBgVertexIn { + // The mode for this cell. + uint8_t mode [[attribute(0)]]; + + // The grid coordinates (x, y) where x < columns and y < rows + float2 grid_pos [[attribute(1)]]; + + // The width of the cell in cells (i.e. 2 for double-wide). + uint8_t cell_width [[attribute(2)]]; + + // The color. For BG modes, this is the bg color, for FG modes this is + // the text color. For styles, this is the color of the style. + uchar4 color [[attribute(3)]]; +}; + +struct CellBgVertexOut { + float4 position [[position]]; + float4 color; +}; + +vertex CellBgVertexOut cell_bg_vertex(unsigned int vid [[vertex_id]], + CellBgVertexIn input [[stage_in]], + constant Uniforms& uniforms + [[buffer(1)]]) { + // Convert the grid x,y into world space x, y by accounting for cell size + float2 cell_pos = uniforms.cell_size * input.grid_pos; + + // Scaled cell size for the cell width + float2 cell_size_scaled = uniforms.cell_size; + cell_size_scaled.x = cell_size_scaled.x * input.cell_width; + + // Turn the cell position into a vertex point depending on the + // vertex ID. Since we use instanced drawing, we have 4 vertices + // for each corner of the cell. We can use vertex ID to determine + // which one we're looking at. Using this, we can use 1 or 0 to keep + // or discard the value for the vertex. + // + // 0 = top-right + // 1 = bot-right + // 2 = bot-left + // 3 = top-left + float2 position; + position.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f; + position.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f; + + // Calculate the final position of our cell in world space. + // We have to add our cell size since our vertices are offset + // one cell up and to the left. (Do the math to verify yourself) + cell_pos = cell_pos + cell_size_scaled * position; + + CellBgVertexOut out; + out.color = float4(input.color) / 255.0f; + out.position = + uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f); + + return out; +} + +fragment float4 cell_bg_fragment(CellBgVertexOut in [[stage_in]]) { + return in.color; +} + //------------------------------------------------------------------- // Image Shader //-------------------------------------------------------------------