renderer/metal: dedicated cell bg shader

This commit is contained in:
Mitchell Hashimoto
2024-04-22 10:01:59 -07:00
parent 7a6a2b0752
commit e8b623e829
3 changed files with 293 additions and 1 deletions

View File

@ -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.
///

View File

@ -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

View File

@ -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
//-------------------------------------------------------------------