diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index d7b0024a1..f6b0d1ee1 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -36,7 +36,6 @@ const Image = mtl_image.Image; const ImageMap = mtl_image.ImageMap; const Shaders = mtl_shaders.Shaders; -const CellBuffer = mtl_buffer.Buffer(mtl_shaders.Cell); const ImageBuffer = mtl_buffer.Buffer(mtl_shaders.Image); const InstanceBuffer = mtl_buffer.Buffer(u16); @@ -90,8 +89,8 @@ current_background_color: terminal.color.RGB, /// The current set of cells to render. This is rebuilt on every frame /// but we keep this around so that we don't reallocate. Each set of /// cells goes into a separate shader. -cells_bg: std.ArrayListUnmanaged(mtl_shaders.Cell), -cells: std.ArrayListUnmanaged(mtl_shaders.Cell), +cells_bg: std.ArrayListUnmanaged(mtl_shaders.CellBg), +cells: std.ArrayListUnmanaged(mtl_shaders.CellText), /// The current GPU uniform values. uniforms: mtl_shaders.Uniforms, @@ -205,8 +204,8 @@ pub const GPUState = struct { /// This is used to implement double/triple buffering. pub const FrameState = struct { uniforms: UniformBuffer, - cells: CellBuffer, - cells_bg: CellBuffer, + cells: CellTextBuffer, + cells_bg: CellBgBuffer, greyscale: objc.Object, // MTLTexture greyscale_modified: usize = 0, @@ -215,6 +214,8 @@ pub const FrameState = struct { /// A buffer containing the uniform data. const UniformBuffer = mtl_buffer.Buffer(mtl_shaders.Uniforms); + const CellBgBuffer = mtl_buffer.Buffer(mtl_shaders.CellBg); + const CellTextBuffer = mtl_buffer.Buffer(mtl_shaders.CellText); pub fn init(device: objc.Object) !FrameState { // Uniform buffer contains exactly 1 uniform struct. The @@ -225,9 +226,9 @@ pub const FrameState = struct { // Create the buffers for our vertex data. The preallocation size // is likely too small but our first frame update will resize it. - var cells = try CellBuffer.init(device, 10 * 10); + var cells = try CellTextBuffer.init(device, 10 * 10); errdefer cells.deinit(); - var cells_bg = try CellBuffer.init(device, 10 * 10); + var cells_bg = try CellBgBuffer.init(device, 10 * 10); errdefer cells_bg.deinit(); // Initialize our textures for our font atlas. @@ -1268,7 +1269,7 @@ fn drawCells( self: *Metal, encoder: objc.Object, frame: *const FrameState, - buf: CellBuffer, + buf: FrameState.CellTextBuffer, len: usize, ) !void { // This triggers an assertion in the Metal API if we try to draw @@ -1279,7 +1280,7 @@ fn drawCells( encoder.msgSend( void, objc.sel("setRenderPipelineState:"), - .{self.shaders.cell_pipeline.value}, + .{self.shaders.cell_text_pipeline.value}, ); // Set our buffers @@ -1694,7 +1695,7 @@ fn rebuildCells( // This is the cell that has [mode == .fg] and is underneath our cursor. // We keep track of it so that we can invert the colors so the character // remains visible. - var cursor_cell: ?mtl_shaders.Cell = null; + var cursor_cell: ?mtl_shaders.CellText = null; // Build each cell var row_it = screen.pages.rowIterator(.right_down, .{ .viewport = .{} }, null); @@ -1847,12 +1848,6 @@ fn rebuildCells( self.cells.appendAssumeCapacity(cell.*); } } - - // Some debug mode safety checks - if (std.debug.runtime_safety) { - for (self.cells_bg.items) |cell| assert(cell.mode == .bg); - for (self.cells.items) |cell| assert(cell.mode != .bg); - } } fn updateCell( @@ -1964,11 +1959,10 @@ fn updateCell( }; self.cells_bg.appendAssumeCapacity(.{ - .mode = .bg, + .mode = .rgb, .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, .cell_width = cell.gridWidth(), .color = .{ rgb.r, rgb.g, rgb.b, bg_alpha }, - .bg_color = .{ 0, 0, 0, 0 }, }); break :bg .{ rgb.r, rgb.g, rgb.b, bg_alpha }; @@ -1992,7 +1986,7 @@ fn updateCell( }, ); - const mode: mtl_shaders.Cell.Mode = switch (try fgMode( + const mode: mtl_shaders.CellText.Mode = switch (try fgMode( render.presentation, cell_pin, )) { @@ -2080,7 +2074,7 @@ fn addCursor( self: *Metal, screen: *terminal.Screen, cursor_style: renderer.CursorStyle, -) ?*const mtl_shaders.Cell { +) ?*const mtl_shaders.CellText { // Add the cursor. We render the cursor over the wide character if // we're on the wide characer tail. const wide, const x = cell: { @@ -2166,11 +2160,10 @@ fn addPreeditCell( // Add our opaque background cell self.cells_bg.appendAssumeCapacity(.{ - .mode = .bg, + .mode = .rgb, .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, .cell_width = if (cp.wide) 2 else 1, .color = .{ bg.r, bg.g, bg.b, 255 }, - .bg_color = .{ bg.r, bg.g, bg.b, 255 }, }); // Add our text diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 6d2e9addb..40e048984 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -16,7 +16,7 @@ pub const Shaders = struct { /// The cell shader is the shader used to render the terminal cells. /// It is a single shader that is used for both the background and /// foreground. - cell_pipeline: objc.Object, + cell_text_pipeline: objc.Object, /// The cell background shader is the shader used to render the /// background of terminal cells. @@ -44,8 +44,8 @@ pub const Shaders = struct { const library = try initLibrary(device); errdefer library.msgSend(void, objc.sel("release"), .{}); - const cell_pipeline = try initCellPipeline(device, library); - errdefer cell_pipeline.msgSend(void, objc.sel("release"), .{}); + const cell_text_pipeline = try initCellTextPipeline(device, library); + errdefer cell_text_pipeline.msgSend(void, objc.sel("release"), .{}); const cell_bg_pipeline = try initCellBgPipeline(device, library); errdefer cell_bg_pipeline.msgSend(void, objc.sel("release"), .{}); @@ -72,7 +72,7 @@ pub const Shaders = struct { return .{ .library = library, - .cell_pipeline = cell_pipeline, + .cell_text_pipeline = cell_text_pipeline, .cell_bg_pipeline = cell_bg_pipeline, .image_pipeline = image_pipeline, .post_pipelines = post_pipelines, @@ -81,7 +81,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_text_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"), .{}); @@ -96,25 +96,6 @@ pub const Shaders = struct { } }; -/// This is a single parameter for the terminal cell shader. -pub const Cell = extern struct { - mode: Mode, - grid_pos: [2]f32, - glyph_pos: [2]u32 = .{ 0, 0 }, - glyph_size: [2]u32 = .{ 0, 0 }, - glyph_offset: [2]i32 = .{ 0, 0 }, - color: [4]u8, - bg_color: [4]u8, - cell_width: u8, - - pub const Mode = enum(u8) { - bg = 1, - fg = 2, - fg_constrained = 3, - fg_color = 7, - }; -}; - /// Single parameter for the image shader. See shader for field details. pub const Image = extern struct { grid_pos: [2]f32, @@ -303,12 +284,31 @@ fn initPostPipeline( return pipeline_state; } +/// This is a single parameter for the terminal cell shader. +pub const CellText = extern struct { + mode: Mode, + grid_pos: [2]f32, + glyph_pos: [2]u32 = .{ 0, 0 }, + glyph_size: [2]u32 = .{ 0, 0 }, + glyph_offset: [2]i32 = .{ 0, 0 }, + color: [4]u8, + bg_color: [4]u8, + cell_width: u8, + + pub const Mode = enum(u8) { + bg = 1, + fg = 2, + fg_constrained = 3, + fg_color = 7, + }; +}; + /// Initialize the cell render pipeline for our shader library. -fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { +fn initCellTextPipeline(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( - "uber_vertex", + "cell_text_vertex", .utf8, false, ); @@ -319,7 +319,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { }; const func_frag = func_frag: { const str = try macos.foundation.String.createWithBytes( - "uber_fragment", + "cell_text_fragment", .utf8, false, ); @@ -353,7 +353,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "mode"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "mode"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } { @@ -364,7 +364,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.float2)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "grid_pos"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "grid_pos"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } { @@ -375,7 +375,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uint2)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "glyph_pos"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "glyph_pos"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } { @@ -386,7 +386,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uint2)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "glyph_size"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "glyph_size"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } { @@ -397,7 +397,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.int2)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "glyph_offset"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "glyph_offset"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } { @@ -408,7 +408,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar4)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "color"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "color"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } { @@ -419,7 +419,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar4)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "bg_color"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "bg_color"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } { @@ -430,7 +430,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "cell_width"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "cell_width"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } @@ -445,7 +445,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { // Access each Cell per instance, not per vertex. layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance)); - layout.setProperty("stride", @as(c_ulong, @sizeOf(Cell))); + layout.setProperty("stride", @as(c_ulong, @sizeOf(CellText))); } break :vertex_desc desc; @@ -506,8 +506,8 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { pub const CellBg = extern struct { mode: Mode, grid_pos: [2]f32, - cell_width: u8, color: [4]u8, + cell_width: u8, pub const Mode = enum(u8) { rgb = 1, @@ -575,7 +575,7 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.float2)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "grid_pos"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellBg, "grid_pos"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } { @@ -586,7 +586,7 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "cell_width"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellBg, "cell_width"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } { @@ -597,7 +597,7 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar4)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "color"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellBg, "color"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } @@ -612,7 +612,7 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object { // Access each Cell per instance, not per vertex. layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance)); - layout.setProperty("stride", @as(c_ulong, @sizeOf(Cell))); + layout.setProperty("stride", @as(c_ulong, @sizeOf(CellBg))); } break :vertex_desc desc; @@ -840,11 +840,20 @@ fn checkError(err_: ?*anyopaque) !void { // on macOS 12 or Apple Silicon macOS 13. // // To be safe, we put this test in here. -test "Cell offsets" { +test "CellText offsets" { const testing = std.testing; - const alignment = @alignOf(Cell); - inline for (@typeInfo(Cell).Struct.fields) |field| { - const offset = @offsetOf(Cell, field.name); + const alignment = @alignOf(CellText); + inline for (@typeInfo(CellText).Struct.fields) |field| { + const offset = @offsetOf(CellText, field.name); + try testing.expectEqual(0, @mod(offset, alignment)); + } +} + +test "CellBg offsets" { + const testing = std.testing; + const alignment = @alignOf(CellBg); + inline for (@typeInfo(CellBg).Struct.fields) |field| { + const offset = @offsetOf(CellBg, field.name); try testing.expectEqual(0, @mod(offset, alignment)); } } diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index a1c4389b1..dd9ace86a 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -1,58 +1,11 @@ using namespace metal; -// The possible modes that a shader can take. -enum Mode : uint8_t { - MODE_BG = 1u, - MODE_FG = 2u, - MODE_FG_CONSTRAINED = 3u, - MODE_FG_COLOR = 7u, -}; - struct Uniforms { float4x4 projection_matrix; float2 cell_size; float min_contrast; }; -struct VertexIn { - // 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(6)]]; - - // 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(5)]]; - - // The fields below are present only when rendering text (fg mode) - - // The background color of the cell. This is used to determine if - // we need to render the text with a different color to ensure - // contrast. - uchar4 bg_color [[attribute(7)]]; - - // The position of the glyph in the texture (x,y) - uint2 glyph_pos [[attribute(2)]]; - - // The size of the glyph in the texture (w,h) - uint2 glyph_size [[attribute(3)]]; - - // The left and top bearings for the glyph (x,y) - int2 glyph_offset [[attribute(4)]]; -}; - -struct VertexOut { - float4 position [[position]]; - float2 cell_size; - uint8_t mode; - float4 color; - float2 tex_coord; -}; - //------------------------------------------------------------------- // Color Functions //------------------------------------------------------------------- @@ -102,142 +55,13 @@ float4 contrasted_color(float min, float4 fg, float4 bg) { return fg; } -//------------------------------------------------------------------- -// Terminal Grid Cell Shader -//------------------------------------------------------------------- -#pragma mark - Terminal Grid Cell Shader - -vertex VertexOut uber_vertex(unsigned int vid [[vertex_id]], - VertexIn 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; - - VertexOut out; - out.mode = input.mode; - out.cell_size = uniforms.cell_size; - out.color = float4(input.color) / 255.0f; - switch (input.mode) { - case MODE_BG: - // 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; - - out.position = uniforms.projection_matrix * - float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f); - break; - - case MODE_FG: - case MODE_FG_CONSTRAINED: - case MODE_FG_COLOR: { - float2 glyph_size = float2(input.glyph_size); - float2 glyph_offset = float2(input.glyph_offset); - - // The glyph_offset.y is the y bearing, a y value that when added - // to the baseline is the offset (+y is up). Our grid goes down. - // So we flip it with `cell_size.y - glyph_offset.y`. - glyph_offset.y = cell_size_scaled.y - glyph_offset.y; - - // If we're constrained then we need to scale the glyph. - // We also always constrain colored glyphs since we should have - // their scaled cell size exactly correct. - if (input.mode == MODE_FG_CONSTRAINED || input.mode == MODE_FG_COLOR) { - if (glyph_size.x > cell_size_scaled.x) { - float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x); - glyph_offset.y += (glyph_size.y - new_y) / 2; - glyph_size.y = new_y; - glyph_size.x = cell_size_scaled.x; - } - } - - // Calculate the final position of the cell which uses our glyph size - // and glyph offset to create the correct bounding box for the glyph. - cell_pos = cell_pos + glyph_size * position + glyph_offset; - out.position = uniforms.projection_matrix * - float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f); - - // Calculate the texture coordinate in pixels. This is NOT normalized - // (between 0.0 and 1.0) and must be done in the fragment shader. - out.tex_coord = - float2(input.glyph_pos) + float2(input.glyph_size) * position; - - // If we have a minimum contrast, we need to check if we need to - // change the color of the text to ensure it has enough contrast - // with the background. - if (uniforms.min_contrast > 1.0f && input.mode == MODE_FG) { - float4 bg_color = float4(input.bg_color) / 255.0f; - out.color = - contrasted_color(uniforms.min_contrast, out.color, bg_color); - } - - break; - } - } - - return out; -} - -fragment float4 uber_fragment(VertexOut in [[stage_in]], - texture2d textureGreyscale [[texture(0)]], - texture2d textureColor [[texture(1)]]) { - constexpr sampler textureSampler(address::clamp_to_edge, filter::linear); - - switch (in.mode) { - case MODE_BG: - return in.color; - - case MODE_FG_CONSTRAINED: - case MODE_FG: { - // Normalize the texture coordinates to [0,1] - float2 size = - float2(textureGreyscale.get_width(), textureGreyscale.get_height()); - float2 coord = in.tex_coord / size; - - // We premult the alpha to our whole color since our blend function - // uses One/OneMinusSourceAlpha to avoid blurry edges. - // We first premult our given color. - float4 premult = float4(in.color.rgb * in.color.a, in.color.a); - // Then premult the texture color - float a = textureGreyscale.sample(textureSampler, coord).r; - premult = premult * a; - return premult; - } - - case MODE_FG_COLOR: { - // Normalize the texture coordinates to [0,1] - float2 size = float2(textureColor.get_width(), textureColor.get_height()); - float2 coord = in.tex_coord / size; - return textureColor.sample(textureSampler, coord); - } - } -} - //------------------------------------------------------------------- // Cell Background Shader //------------------------------------------------------------------- #pragma mark - Cell BG Shader // The possible modes that a cell bg entry can take. -enum CellbgMode : uint8_t { +enum CellBgMode : uint8_t { MODE_RGB = 1u, }; @@ -248,12 +72,12 @@ struct CellBgVertexIn { // 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)]]; + + // The width of the cell in cells (i.e. 2 for double-wide). + uint8_t cell_width [[attribute(2)]]; }; struct CellBgVertexOut { @@ -303,6 +127,158 @@ fragment float4 cell_bg_fragment(CellBgVertexOut in [[stage_in]]) { return in.color; } +//------------------------------------------------------------------- +// Cell Text Shader +//------------------------------------------------------------------- +#pragma mark - Cell Text Shader + +// The possible modes that a cell fg entry can take. +enum CellTextMode : uint8_t { + MODE_TEXT = 2u, + MODE_TEXT_CONSTRAINED = 3u, + MODE_TEXT_COLOR = 7u, +}; + +struct CellTextVertexIn { + // 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(6)]]; + + // The color of the rendered text glyph. + uchar4 color [[attribute(5)]]; + + // The background color of the cell. This is used to determine if + // we need to render the text with a different color to ensure + // contrast. + uchar4 bg_color [[attribute(7)]]; + + // The position of the glyph in the texture (x,y) + uint2 glyph_pos [[attribute(2)]]; + + // The size of the glyph in the texture (w,h) + uint2 glyph_size [[attribute(3)]]; + + // The left and top bearings for the glyph (x,y) + int2 glyph_offset [[attribute(4)]]; +}; + +struct CellTextVertexOut { + float4 position [[position]]; + float2 cell_size; + uint8_t mode; + float4 color; + float2 tex_coord; +}; + +vertex CellTextVertexOut cell_text_vertex(unsigned int vid [[vertex_id]], + CellTextVertexIn 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; + + CellTextVertexOut out; + out.mode = input.mode; + out.cell_size = uniforms.cell_size; + out.color = float4(input.color) / 255.0f; + + float2 glyph_size = float2(input.glyph_size); + float2 glyph_offset = float2(input.glyph_offset); + + // The glyph_offset.y is the y bearing, a y value that when added + // to the baseline is the offset (+y is up). Our grid goes down. + // So we flip it with `cell_size.y - glyph_offset.y`. + glyph_offset.y = cell_size_scaled.y - glyph_offset.y; + + // If we're constrained then we need to scale the glyph. + // We also always constrain colored glyphs since we should have + // their scaled cell size exactly correct. + if (input.mode == MODE_TEXT_CONSTRAINED || input.mode == MODE_TEXT_COLOR) { + if (glyph_size.x > cell_size_scaled.x) { + float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x); + glyph_offset.y += (glyph_size.y - new_y) / 2; + glyph_size.y = new_y; + glyph_size.x = cell_size_scaled.x; + } + } + + // Calculate the final position of the cell which uses our glyph size + // and glyph offset to create the correct bounding box for the glyph. + cell_pos = cell_pos + glyph_size * position + glyph_offset; + out.position = + uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f); + + // Calculate the texture coordinate in pixels. This is NOT normalized + // (between 0.0 and 1.0) and must be done in the fragment shader. + out.tex_coord = float2(input.glyph_pos) + float2(input.glyph_size) * position; + + // If we have a minimum contrast, we need to check if we need to + // change the color of the text to ensure it has enough contrast + // with the background. + if (uniforms.min_contrast > 1.0f && input.mode == MODE_TEXT) { + float4 bg_color = float4(input.bg_color) / 255.0f; + out.color = contrasted_color(uniforms.min_contrast, out.color, bg_color); + } + + return out; +} + +fragment float4 cell_text_fragment(CellTextVertexOut in [[stage_in]], + texture2d textureGreyscale + [[texture(0)]], + texture2d textureColor + [[texture(1)]]) { + constexpr sampler textureSampler(address::clamp_to_edge, filter::linear); + + switch (in.mode) { + case MODE_TEXT_CONSTRAINED: + case MODE_TEXT: { + // Normalize the texture coordinates to [0,1] + float2 size = + float2(textureGreyscale.get_width(), textureGreyscale.get_height()); + float2 coord = in.tex_coord / size; + + // We premult the alpha to our whole color since our blend function + // uses One/OneMinusSourceAlpha to avoid blurry edges. + // We first premult our given color. + float4 premult = float4(in.color.rgb * in.color.a, in.color.a); + // Then premult the texture color + float a = textureGreyscale.sample(textureSampler, coord).r; + premult = premult * a; + return premult; + } + + case MODE_TEXT_COLOR: { + // Normalize the texture coordinates to [0,1] + float2 size = float2(textureColor.get_width(), textureColor.get_height()); + float2 coord = in.tex_coord / size; + return textureColor.sample(textureSampler, coord); + } + } +} //------------------------------------------------------------------- // Image Shader //-------------------------------------------------------------------