diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index b5e281900..caedce13d 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -625,8 +625,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .cell_size = undefined, .grid_size = undefined, .grid_padding = undefined, - .padding_extend_top = true, - .padding_extend_bottom = true, + .padding_extend = .{}, .min_contrast = options.config.min_contrast, .cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) }, .cursor_color = undefined, @@ -1084,7 +1083,7 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { // Setup our frame data try frame.uniforms.sync(self.gpu_state.device, &.{self.uniforms}); - const bg_count = try frame.cells_bg.syncFromArrayLists(self.gpu_state.device, self.cells.bg_rows.lists); + try frame.cells_bg.sync(self.gpu_state.device, self.cells.bg_cells); const fg_count = try frame.cells.syncFromArrayLists(self.gpu_state.device, self.cells.fg_rows.lists); // If we have custom shaders, update the animation time. @@ -1179,7 +1178,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.drawCellBgs(encoder, frame, bg_count); + try self.drawCellBgs(encoder, frame); // Then draw images under text try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]); @@ -1371,9 +1370,9 @@ fn drawPostShader( void, objc.sel("drawPrimitives:vertexStart:vertexCount:"), .{ - @intFromEnum(mtl.MTLPrimitiveType.triangle_strip), + @intFromEnum(mtl.MTLPrimitiveType.triangle), @as(c_ulong, 0), - @as(c_ulong, 4), + @as(c_ulong, 3), }, ); } @@ -1503,12 +1502,7 @@ 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, @@ -1519,25 +1513,22 @@ fn drawCellBgs( // Set our buffers encoder.msgSend( void, - objc.sel("setVertexBuffer:offset:atIndex:"), + objc.sel("setFragmentBuffer:offset:atIndex:"), .{ frame.cells_bg.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) }, ); encoder.msgSend( void, - objc.sel("setVertexBuffer:offset:atIndex:"), + objc.sel("setFragmentBuffer: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:"), + objc.sel("drawPrimitives:vertexStart:vertexCount:"), .{ @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), + @as(c_ulong, 3), }, ); } @@ -1571,6 +1562,11 @@ fn drawCellFgs( objc.sel("setVertexBuffer:offset:atIndex:"), .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) }, ); + encoder.msgSend( + void, + objc.sel("setVertexBuffer:offset:atIndex:"), + .{ frame.cells_bg.buffer.value, @as(c_ulong, 0), @as(c_ulong, 2) }, + ); encoder.msgSend( void, objc.sel("setFragmentTexture:atIndex:"), @@ -1953,16 +1949,20 @@ pub fn setScreenSize( const padded_dim = dim.subPadding(padding); // Blank space around the grid. - const blank: renderer.Padding = switch (self.config.padding_color) { - // We can use zero padding because the background color is our - // clear color. - .background => .{}, + const blank: renderer.Padding = dim.blankPadding(padding, grid_size, .{ + .width = self.grid_metrics.cell_width, + .height = self.grid_metrics.cell_height, + }).add(padding); - .extend => dim.blankPadding(padding, grid_size, .{ - .width = self.grid_metrics.cell_width, - .height = self.grid_metrics.cell_height, - }).add(padding), - }; + var padding_extend = self.uniforms.padding_extend; + if (self.config.padding_color == .extend) { + // If padding extension is enabled, we extend left and right always. + padding_extend.left = true; + padding_extend.right = true; + } else { + // Otherwise, disable all padding extension. + padding_extend = .{}; + } // Set the size of the drawable surface to the bounds self.layer.setProperty("drawableSize", macos.graphics.Size{ @@ -1993,8 +1993,7 @@ pub fn setScreenSize( @floatFromInt(blank.bottom), @floatFromInt(blank.left), }, - .padding_extend_top = old.padding_extend_top, - .padding_extend_bottom = old.padding_extend_bottom, + .padding_extend = padding_extend, .min_contrast = old.min_contrast, .cursor_pos = old.cursor_pos, .cursor_color = old.cursor_color, @@ -2139,8 +2138,10 @@ fn rebuildCells( self.cells.reset(); // We also reset our padding extension depending on the screen type - self.uniforms.padding_extend_top = screen_type == .alternate; - self.uniforms.padding_extend_bottom = screen_type == .alternate; + if (self.config.padding_color == .extend) { + self.uniforms.padding_extend.up = screen_type == .alternate; + self.uniforms.padding_extend.down = screen_type == .alternate; + } } // Go row-by-row to build the cells. We go row by row because we do @@ -2177,10 +2178,12 @@ fn rebuildCells( // under certain conditions we feel are safe. This helps make some // scenarios look better while avoiding scenarios we know do NOT look // good. - if (y == 0 and screen_type == .primary) { - self.uniforms.padding_extend_top = !row.neverExtendBg(); - } else if (y == self.cells.size.rows - 1 and screen_type == .primary) { - self.uniforms.padding_extend_bottom = !row.neverExtendBg(); + if (self.config.padding_color == .extend) { + if (y == 0 and screen_type == .primary) { + self.uniforms.padding_extend.up = !row.neverExtendBg(); + } else if (y == self.cells.size.rows - 1 and screen_type == .primary) { + self.uniforms.padding_extend.down = !row.neverExtendBg(); + } } // Split our row into runs and shape each one. @@ -2411,7 +2414,7 @@ fn updateCell( const alpha: u8 = if (style.flags.faint) 175 else 255; // If the cell has a background, we always draw it. - const bg: [4]u8 = if (colors.bg) |rgb| bg: { + if (colors.bg) |rgb| { // Determine our background alpha. If we have transparency configured // then this is dynamic depending on some situations. This is all // in an attempt to make transparency look the best for various @@ -2440,20 +2443,16 @@ fn updateCell( break :bg_alpha @intFromFloat(bg_alpha); }; - try self.cells.add(self.alloc, .bg, .{ - .mode = .rgb, - .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, - .cell_width = cell.gridWidth(), - .color = .{ rgb.r, rgb.g, rgb.b, bg_alpha }, - }); + self.cells.bgCell(coord.y, coord.x).* = .{ + rgb.r, rgb.g, rgb.b, bg_alpha, + }; - break :bg .{ rgb.r, rgb.g, rgb.b, bg_alpha }; - } else .{ - self.current_background_color.r, - self.current_background_color.g, - self.current_background_color.b, - @intFromFloat(@max(0, @min(255, @round(self.config.background_opacity * 255)))), - }; + if (cell.gridWidth() > 1 and coord.x < self.cells.size.columns - 1) { + self.cells.bgCell(coord.y, coord.x).* = .{ + rgb.r, rgb.g, rgb.b, bg_alpha, + }; + } + } // If the shaper cell has a glyph, draw it. if (shaper_cell.glyph_index) |glyph_index| glyph: { @@ -2487,14 +2486,13 @@ fn updateCell( try self.cells.add(self.alloc, .text, .{ .mode = mode, .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, - .cell_width = cell.gridWidth(), + .constraint_width = cell.gridWidth(), .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, - .bg_color = bg, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, .glyph_size = .{ render.glyph.width, render.glyph.height }, - .glyph_offset = .{ - render.glyph.offset_x + shaper_cell.x_offset, - render.glyph.offset_y + shaper_cell.y_offset, + .bearings = .{ + @intCast(render.glyph.offset_x + shaper_cell.x_offset), + @intCast(render.glyph.offset_y + shaper_cell.y_offset), }, }); } @@ -2524,12 +2522,14 @@ fn updateCell( try self.cells.add(self.alloc, .underline, .{ .mode = .fg, .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, - .cell_width = cell.gridWidth(), + .constraint_width = cell.gridWidth(), .color = .{ color.r, color.g, color.b, alpha }, - .bg_color = bg, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, .glyph_size = .{ render.glyph.width, render.glyph.height }, - .glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y }, + .bearings = .{ + @intCast(render.glyph.offset_x), + @intCast(render.glyph.offset_y), + }, }); } @@ -2547,12 +2547,14 @@ fn updateCell( try self.cells.add(self.alloc, .strikethrough, .{ .mode = .fg, .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, - .cell_width = cell.gridWidth(), + .constraint_width = cell.gridWidth(), .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, - .bg_color = bg, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, .glyph_size = .{ render.glyph.width, render.glyph.height }, - .glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y }, + .bearings = .{ + @intCast(render.glyph.offset_x), + @intCast(render.glyph.offset_y), + }, }); } @@ -2607,12 +2609,13 @@ fn addCursor( self.cells.setCursor(.{ .mode = .cursor, .grid_pos = .{ x, screen.cursor.y }, - .cell_width = if (wide) 2 else 1, .color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha }, - .bg_color = .{ 0, 0, 0, 0 }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, .glyph_size = .{ render.glyph.width, render.glyph.height }, - .glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y }, + .bearings = .{ + @intCast(render.glyph.offset_x), + @intCast(render.glyph.offset_y), + }, }); } @@ -2642,23 +2645,26 @@ fn addPreeditCell( }; // Add our opaque background cell - try self.cells.add(self.alloc, .bg, .{ - .mode = .rgb, - .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, - .cell_width = if (cp.wide) 2 else 1, - .color = .{ bg.r, bg.g, bg.b, 255 }, - }); + self.cells.bgCell(coord.y, coord.x).* = .{ + bg.r, bg.g, bg.b, 255, + }; + if (cp.wide and coord.x < self.cells.size.columns - 1) { + self.cells.bgCell(coord.y, coord.x + 1).* = .{ + bg.r, bg.g, bg.b, 255, + }; + } // Add our text try self.cells.add(self.alloc, .text, .{ .mode = .fg, .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, - .cell_width = if (cp.wide) 2 else 1, .color = .{ fg.r, fg.g, fg.b, 255 }, - .bg_color = .{ bg.r, bg.g, bg.b, 255 }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, .glyph_size = .{ render.glyph.width, render.glyph.height }, - .glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y }, + .bearings = .{ + @intCast(render.glyph.offset_x), + @intCast(render.glyph.offset_y), + }, }); } diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig index 3ec04e367..0781812ac 100644 --- a/src/renderer/metal/api.zig +++ b/src/renderer/metal/api.zig @@ -51,11 +51,13 @@ pub const MTLIndexType = enum(c_ulong) { pub const MTLVertexFormat = enum(c_ulong) { uchar4 = 3, ushort2 = 13, + short2 = 16, float2 = 29, float4 = 31, int2 = 33, uint = 36, uint2 = 37, + uint4 = 39, uchar = 45, }; diff --git a/src/renderer/metal/cell.zig b/src/renderer/metal/cell.zig index e0a3f797e..94b8b39bb 100644 --- a/src/renderer/metal/cell.zig +++ b/src/renderer/metal/cell.zig @@ -75,22 +75,13 @@ fn ArrayListPool(comptime T: type) type { pub const Contents = struct { size: renderer.GridSize = .{ .rows = 0, .columns = 0 }, - /// The ArrayListPool which holds all of the background cells. When sized - /// with Contents.resize the individual ArrayLists SHOULD be given enough - /// capacity that appendAssumeCapacity may be used, since it should be - /// impossible for a row to have more background cells than columns. + /// Flat array containing cell background colors for the terminal grid. /// - /// HOWEVER, the initial capacity can be exceeded due to multi-glyph - /// composites each adding a background cell for the same position. - /// This should probably be considered a bug, but for now it means - /// that sometimes allocations might happen, so appendAssumeCapacity - /// MUST NOT be used. + /// Indexed as `bg_cells[row * size.columns + col]`. /// - /// Rows are indexed as Contents.bg_rows[y]. - /// - /// Must be initialized by calling resize on the Contents struct before - /// calling any operations. - bg_rows: ArrayListPool(mtl_shaders.CellBg) = .{}, + /// Prefer accessing with `Contents.bgCell(row, col).*` instead + /// of directly indexing in order to avoid integer size bugs. + bg_cells: []mtl_shaders.CellBg = undefined, /// The ArrayListPool which holds all of the foreground cells. When sized /// with Contents.resize the individual ArrayLists are given enough room @@ -116,7 +107,7 @@ pub const Contents = struct { fg_rows: ArrayListPool(mtl_shaders.CellText) = .{}, pub fn deinit(self: *Contents, alloc: Allocator) void { - self.bg_rows.deinit(alloc); + alloc.free(self.bg_cells); self.fg_rows.deinit(alloc); } @@ -129,15 +120,12 @@ pub const Contents = struct { ) !void { self.size = size; - // When we create our bg_rows pool, we give the lists an initial - // capacity of size.columns. This is to account for the usual case - // where you have a row with normal text and background colors. - // This can be exceeded due to multi-glyph composites each adding - // a background cell for the same position. This should probably be - // considered a bug, but for now it means that sometimes allocations - // might happen, and appendAssumeCapacity MUST NOT be used. - var bg_rows = try ArrayListPool(mtl_shaders.CellBg).init(alloc, size.rows, size.columns); - errdefer bg_rows.deinit(alloc); + const cell_count = @as(usize, size.columns) * @as(usize, size.rows); + + const bg_cells = try alloc.alloc(mtl_shaders.CellBg, cell_count); + errdefer alloc.free(bg_cells); + + @memset(bg_cells, .{0, 0, 0, 0}); // The foreground lists can hold 3 types of items: // - Glyphs @@ -154,10 +142,10 @@ pub const Contents = struct { var fg_rows = try ArrayListPool(mtl_shaders.CellText).init(alloc, size.rows + 1, size.columns * 3); errdefer fg_rows.deinit(alloc); - self.bg_rows.deinit(alloc); + alloc.free(self.bg_cells); self.fg_rows.deinit(alloc); - self.bg_rows = bg_rows; + self.bg_cells = bg_cells; self.fg_rows = fg_rows; // We don't need 3*cols worth of cells for the cursor list, so we can @@ -170,7 +158,7 @@ pub const Contents = struct { /// Reset the cell contents to an empty state without resizing. pub fn reset(self: *Contents) void { - self.bg_rows.reset(); + @memset(self.bg_cells, .{ 0, 0, 0, 0 }); self.fg_rows.reset(); } @@ -183,6 +171,12 @@ pub const Contents = struct { } } + /// Access a background cell. Prefer this function over direct indexing + /// of `bg_cells` in order to avoid integer size bugs causing overflows. + pub inline fn bgCell(self: *Contents, row: usize, col: usize) *mtl_shaders.CellBg { + return &self.bg_cells[row * self.size.columns + col]; + } + /// Add a cell to the appropriate list. Adding the same cell twice will /// result in duplication in the vertex buffer. The caller should clear /// the corresponding row with Contents.clear to remove old cells first. @@ -197,7 +191,7 @@ pub const Contents = struct { assert(y < self.size.rows); switch (key) { - .bg => try self.bg_rows.lists[y].append(alloc, cell), + .bg => comptime unreachable, .text, .underline, @@ -213,7 +207,8 @@ pub const Contents = struct { pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void { assert(y < self.size.rows); - self.bg_rows.lists[y].clearRetainingCapacity(); + @memset(self.bg_cells[@as(usize, y) * self.size.columns ..][0..self.size.columns], .{ 0, 0, 0, 0 }); + // We have a special list containing the cursor cell at the start // of our fg row pool, so we need to add 1 to the y to get the // correct index. @@ -234,47 +229,42 @@ test Contents { // We should start off empty after resizing. for (0..rows) |y| { - try testing.expect(c.bg_rows.lists[y].items.len == 0); try testing.expect(c.fg_rows.lists[y + 1].items.len == 0); + for (0..cols) |x| { + try testing.expectEqual(.{0, 0, 0, 0}, c.bgCell(y, x).*); + } } // And the cursor row should have a capacity of 1 and also be empty. try testing.expect(c.fg_rows.lists[0].capacity == 1); try testing.expect(c.fg_rows.lists[0].items.len == 0); // Add some contents. - const bg_cell: mtl_shaders.CellBg = .{ - .mode = .rgb, - .grid_pos = .{ 4, 1 }, - .cell_width = 1, - .color = .{ 0, 0, 0, 1 }, - }; + const bg_cell: mtl_shaders.CellBg = .{ 0, 0, 0, 1 }; const fg_cell: mtl_shaders.CellText = .{ .mode = .fg, .grid_pos = .{ 4, 1 }, - .cell_width = 1, .color = .{ 0, 0, 0, 1 }, - .bg_color = .{ 0, 0, 0, 1 }, }; - try c.add(alloc, .bg, bg_cell); + c.bgCell(1, 4).* = bg_cell; try c.add(alloc, .text, fg_cell); - try testing.expectEqual(bg_cell, c.bg_rows.lists[1].items[0]); + try testing.expectEqual(bg_cell, c.bgCell(1, 4).*); // The fg row index is offset by 1 because of the cursor list. try testing.expectEqual(fg_cell, c.fg_rows.lists[2].items[0]); // And we should be able to clear it. c.clear(1); for (0..rows) |y| { - try testing.expect(c.bg_rows.lists[y].items.len == 0); try testing.expect(c.fg_rows.lists[y + 1].items.len == 0); + for (0..cols) |x| { + try testing.expectEqual(.{0, 0, 0, 0}, c.bgCell(y, x).*); + } } // Add a cursor. const cursor_cell: mtl_shaders.CellText = .{ .mode = .cursor, .grid_pos = .{ 2, 3 }, - .cell_width = 1, .color = .{ 0, 0, 0, 1 }, - .bg_color = .{ 0, 0, 0, 1 }, }; c.setCursor(cursor_cell); try testing.expectEqual(cursor_cell, c.fg_rows.lists[0].items[0]); @@ -296,24 +286,32 @@ test "Contents clear retains other content" { defer c.deinit(alloc); // Set some contents - const cell1: mtl_shaders.CellBg = .{ - .mode = .rgb, + // bg and fg cells in row 1 + const bg_cell_1: mtl_shaders.CellBg = .{ 0, 0, 0, 1 }; + const fg_cell_1: mtl_shaders.CellText = .{ + .mode = .fg, .grid_pos = .{ 4, 1 }, - .cell_width = 1, .color = .{ 0, 0, 0, 1 }, }; - const cell2: mtl_shaders.CellBg = .{ - .mode = .rgb, + c.bgCell(1, 4).* = bg_cell_1; + try c.add(alloc, .text, fg_cell_1); + // bg and fg cells in row 2 + const bg_cell_2: mtl_shaders.CellBg = .{ 0, 0, 0, 1 }; + const fg_cell_2: mtl_shaders.CellText = .{ + .mode = .fg, .grid_pos = .{ 4, 2 }, - .cell_width = 1, .color = .{ 0, 0, 0, 1 }, }; - try c.add(alloc, .bg, cell1); - try c.add(alloc, .bg, cell2); + c.bgCell(2, 4).* = bg_cell_2; + try c.add(alloc, .text, fg_cell_2); + + // Clear row 1, this should leave row 2 untouched c.clear(1); - // Row 2 should still contain its cell. - try testing.expectEqual(cell2, c.bg_rows.lists[2].items[0]); + // Row 2 should still contain its cells. + try testing.expectEqual(bg_cell_2, c.bgCell(2, 4).*); + // Fg row index is +1 because of cursor list at start + try testing.expectEqual(fg_cell_2, c.fg_rows.lists[3].items[0]); } test "Contents clear last added content" { @@ -328,22 +326,30 @@ test "Contents clear last added content" { defer c.deinit(alloc); // Set some contents - const cell1: mtl_shaders.CellBg = .{ - .mode = .rgb, + // bg and fg cells in row 1 + const bg_cell_1: mtl_shaders.CellBg = .{ 0, 0, 0, 1 }; + const fg_cell_1: mtl_shaders.CellText = .{ + .mode = .fg, .grid_pos = .{ 4, 1 }, - .cell_width = 1, .color = .{ 0, 0, 0, 1 }, }; - const cell2: mtl_shaders.CellBg = .{ - .mode = .rgb, + c.bgCell(1, 4).* = bg_cell_1; + try c.add(alloc, .text, fg_cell_1); + // bg and fg cells in row 2 + const bg_cell_2: mtl_shaders.CellBg = .{ 0, 0, 0, 1 }; + const fg_cell_2: mtl_shaders.CellText = .{ + .mode = .fg, .grid_pos = .{ 4, 2 }, - .cell_width = 1, .color = .{ 0, 0, 0, 1 }, }; - try c.add(alloc, .bg, cell1); - try c.add(alloc, .bg, cell2); + c.bgCell(2, 4).* = bg_cell_2; + try c.add(alloc, .text, fg_cell_2); + + // Clear row 2, this should leave row 1 untouched c.clear(2); - // Row 1 should still contain its cell. - try testing.expectEqual(cell1, c.bg_rows.lists[1].items[0]); + // Row 1 should still contain its cells. + try testing.expectEqual(bg_cell_1, c.bgCell(1, 4).*); + // Fg row index is +1 because of cursor list at start + try testing.expectEqual(fg_cell_1, c.fg_rows.lists[2].items[0]); } diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 721cc6e10..6708a8bec 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -124,9 +124,10 @@ pub const Uniforms = extern struct { /// top, right, bottom, left. grid_padding: [4]f32 align(16), - /// True if vertical padding gets the extended color of the nearest row. - padding_extend_top: bool align(1), - padding_extend_bottom: bool align(1), + /// Bit mask defining which directions to + /// extend cell colors in to the padding. + /// Order, LSB first: left, right, up, down + padding_extend: PaddingExtend align(1), /// The minimum contrast ratio for text. The contrast ratio is calculated /// according to the WCAG 2.0 spec. @@ -135,6 +136,14 @@ pub const Uniforms = extern struct { /// The cursor position and color. cursor_pos: [2]u16 align(4), cursor_color: [4]u8 align(4), + + const PaddingExtend = packed struct(u8) { + left: bool = false, + right: bool = false, + up: bool = false, + down: bool = false, + _padding: u4 = 0, + }; }; /// The uniforms used for custom postprocess shaders. @@ -246,7 +255,7 @@ fn initPostPipeline( // Get our vertex and fragment functions const func_vert = func_vert: { const str = try macos.foundation.String.createWithBytes( - "post_vertex", + "full_screen_vertex", .utf8, false, ); @@ -307,14 +316,13 @@ fn initPostPipeline( /// This is a single parameter for the terminal cell shader. pub const CellText = extern struct { - mode: Mode, - 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, - grid_pos: [2]u16, - cell_width: u8, + glyph_pos: [2]u32 align(8) = .{ 0, 0 }, + glyph_size: [2]u32 align(8) = .{ 0, 0 }, + bearings: [2]i16 align(4) = .{ 0, 0 }, + grid_pos: [2]u16 align(4), + color: [4]u8 align(4), + mode: Mode align(1), + constraint_width: u8 align(1) = 0, pub const Mode = enum(u8) { fg = 1, @@ -323,6 +331,12 @@ pub const CellText = extern struct { cursor = 4, fg_powerline = 5, }; + + test { + // Minimizing the size of this struct is important, + // so we test it in order to be aware of any changes. + try std.testing.expectEqual(32, @sizeOf(CellText)); + } }; /// Initialize the cell render pipeline for our shader library. @@ -367,94 +381,7 @@ fn initCellTextPipeline(device: objc.Object, library: objc.Object) !objc.Object // 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(CellText, "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.ushort2)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "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.uint2)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "glyph_pos"))); - 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.uint2)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "glyph_size"))); - attr.setProperty("bufferIndex", @as(c_ulong, 0)); - } - { - const attr = attrs.msgSend( - objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, 4)}, - ); - - attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.int2)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "glyph_offset"))); - attr.setProperty("bufferIndex", @as(c_ulong, 0)); - } - { - const attr = attrs.msgSend( - objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, 5)}, - ); - - attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar4)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "color"))); - attr.setProperty("bufferIndex", @as(c_ulong, 0)); - } - { - const attr = attrs.msgSend( - objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, 7)}, - ); - - attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar4)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "bg_color"))); - attr.setProperty("bufferIndex", @as(c_ulong, 0)); - } - { - const attr = attrs.msgSend( - objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, 6)}, - ); - - attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "cell_width"))); - attr.setProperty("bufferIndex", @as(c_ulong, 0)); - } + autoAttribute(CellText, attrs); // The layout describes how and when we fetch the next vertex input. const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts")); @@ -525,23 +452,14 @@ fn initCellTextPipeline(device: objc.Object, library: objc.Object) !objc.Object } /// This is a single parameter for the cell bg shader. -pub const CellBg = extern struct { - mode: Mode, - grid_pos: [2]u16, - color: [4]u8, - cell_width: u8, - - pub const Mode = enum(u8) { - rgb = 1, - }; -}; +pub const CellBg = [4]u8; /// 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", + "full_screen_vertex", .utf8, false, ); @@ -585,41 +503,8 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object { .{@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.ushort2)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(CellBg, "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(CellBg, "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(CellBg, "color"))); + attr.setProperty("offset", @as(c_ulong, 0)); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } @@ -733,50 +618,7 @@ fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object { // 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, 1)}, - ); - - attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.float2)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Image, "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.float2)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Image, "cell_offset"))); - 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.float4)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Image, "source_rect"))); - attr.setProperty("bufferIndex", @as(c_ulong, 0)); - } - { - const attr = attrs.msgSend( - objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, 4)}, - ); - - attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.float2)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Image, "dest_size"))); - attr.setProperty("bufferIndex", @as(c_ulong, 0)); - } + autoAttribute(Image, attrs); // The layout describes how and when we fetch the next vertex input. const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts")); @@ -845,6 +687,41 @@ fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object { return pipeline_state; } +fn autoAttribute(T: type, attrs: objc.Object) void { + inline for (@typeInfo(T).Struct.fields, 0..) |field, i| { + const offset = @offsetOf(T, field.name); + + const FT = switch (@typeInfo(field.type)) { + .Enum => |e| e.tag_type, + else => field.type, + }; + + const format = switch (FT) { + [4]u8 => mtl.MTLVertexFormat.uchar4, + [2]u16 => mtl.MTLVertexFormat.ushort2, + [2]i16 => mtl.MTLVertexFormat.short2, + [2]f32 => mtl.MTLVertexFormat.float2, + [4]f32 => mtl.MTLVertexFormat.float4, + [2]i32 => mtl.MTLVertexFormat.int2, + u32 => mtl.MTLVertexFormat.uint, + [2]u32 => mtl.MTLVertexFormat.uint2, + [4]u32 => mtl.MTLVertexFormat.uint4, + u8 => mtl.MTLVertexFormat.uchar, + else => comptime unreachable, + }; + + const attr = attrs.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, i)}, + ); + + attr.setProperty("format", @intFromEnum(format)); + attr.setProperty("offset", @as(c_ulong, offset)); + attr.setProperty("bufferIndex", @as(c_ulong, 0)); + } +} + fn checkError(err_: ?*anyopaque) !void { const nserr = objc.Object.fromId(err_ orelse return); const str = @as( @@ -855,27 +732,3 @@ fn checkError(err_: ?*anyopaque) !void { log.err("metal error={s}", .{str.cstringPtr(.ascii).?}); return error.MetalFailed; } - -// Intel macOS 13 doesn't like it when any field in a vertex buffer is not -// aligned on the alignment of the struct. I don't understand it, I think -// this must be some macOS 13 Metal GPU driver bug because it doesn't matter -// on macOS 12 or Apple Silicon macOS 13. -// -// To be safe, we put this test in here. -test "CellText offsets" { - const testing = std.testing; - 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 9719dea81..b58e6600e 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -2,13 +2,19 @@ using namespace metal; +enum Padding : uint8_t { + EXTEND_LEFT = 1u, + EXTEND_RIGHT = 2u, + EXTEND_UP = 4u, + EXTEND_DOWN = 8u, +}; + struct Uniforms { float4x4 projection_matrix; float2 cell_size; ushort2 grid_size; float4 grid_padding; - bool padding_extend_top; - bool padding_extend_bottom; + uint8_t padding_extend; float min_contrast; ushort2 cursor_pos; uchar4 cursor_color; @@ -64,93 +70,86 @@ float4 contrasted_color(float min, float4 fg, float4 bg) { } //------------------------------------------------------------------- -// Cell Background Shader +// Full Screen Vertex Shader //------------------------------------------------------------------- -#pragma mark - Cell BG Shader +#pragma mark - Full Screen Vertex 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 - ushort2 grid_pos [[attribute(1)]]; - - // 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 { +struct FullScreenVertexOut { 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 * float2(input.grid_pos); +vertex FullScreenVertexOut full_screen_vertex( + uint vid [[vertex_id]] +) { + FullScreenVertexOut out; - // 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; + float4 position; + position.x = (vid == 2) ? 3.0 : -1.0; + position.y = (vid == 0) ? -3.0 : 1.0; + position.zw = 1.0; - // If we're at the edge of the grid, we add our padding to the background - // to extend it. Note: grid_padding is top/right/bottom/left. We always - // extend horiziontally because there is no downside but there are various - // heuristics to disable vertical extension. - if (input.grid_pos.y == 0 && uniforms.padding_extend_top) { - cell_pos.y -= uniforms.grid_padding.r; - cell_size_scaled.y += uniforms.grid_padding.r; - } else if (input.grid_pos.y == uniforms.grid_size.y - 1 && - uniforms.padding_extend_bottom) { - cell_size_scaled.y += uniforms.grid_padding.b; - } - if (input.grid_pos.x == 0) { - cell_pos.x -= uniforms.grid_padding.a; - cell_size_scaled.x += uniforms.grid_padding.a; - } else if (input.grid_pos.x == uniforms.grid_size.x - 1) { - cell_size_scaled.x += uniforms.grid_padding.g; - } - - // 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. + // Single triangle is clipped to viewport. // - // 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; + // X <- vid == 0: (-1, -3) + // |\ + // | \ + // | \ + // |###\ + // |#+# \ `+` is (0, 0). `#`s are viewport area. + // |### \ + // X------X <- vid == 2: (3, 1) + // ^ + // vid == 1: (-1, 1) - // 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); + out.position = position; return out; } -fragment float4 cell_bg_fragment(CellBgVertexOut in [[stage_in]]) { - return in.color; +//------------------------------------------------------------------- +// Cell Background Shader +//------------------------------------------------------------------- +#pragma mark - Cell BG Shader + +fragment float4 cell_bg_fragment( + FullScreenVertexOut in [[stage_in]], + constant uchar4 *cells [[buffer(0)]], + constant Uniforms& uniforms [[buffer(1)]] +) { + int2 grid_pos = int2(floor((in.position.xy - uniforms.grid_padding.wx) / uniforms.cell_size)); + + // Clamp x position, extends edge bg colors in to padding on sides. + if (grid_pos.x < 0) { + if (uniforms.padding_extend & EXTEND_LEFT) { + grid_pos.x = 0; + } else { + return float4(0.0); + } + } else if (grid_pos.x > uniforms.grid_size.x - 1) { + if (uniforms.padding_extend & EXTEND_RIGHT) { + grid_pos.x = uniforms.grid_size.x - 1; + } else { + return float4(0.0); + } + } + + // Clamp y position if we should extend, otherwise discard if out of bounds. + if (grid_pos.y < 0) { + if (uniforms.padding_extend & EXTEND_UP) { + grid_pos.y = 0; + } else { + return float4(0.0); + } + } else if (grid_pos.y > uniforms.grid_size.y - 1) { + if (uniforms.padding_extend & EXTEND_DOWN) { + grid_pos.y = uniforms.grid_size.y - 1; + } else { + return float4(0.0); + } + } + + // Retrieve color for cell and return it. + return float4(cells[grid_pos.y * uniforms.grid_size.x + grid_pos.x]) / 255.0; } //------------------------------------------------------------------- @@ -168,51 +167,43 @@ enum CellTextMode : uint8_t { }; struct CellTextVertexIn { - // The mode for this cell. - uint8_t mode [[attribute(0)]]; + // The position of the glyph in the texture (x, y) + uint2 glyph_pos [[attribute(0)]]; + + // The size of the glyph in the texture (w, h) + uint2 glyph_size [[attribute(1)]]; + + // The left and top bearings for the glyph (x, y) + int2 bearings [[attribute(2)]]; // The grid coordinates (x, y) where x < columns and y < rows - ushort2 grid_pos [[attribute(1)]]; - - // The width of the cell in cells (i.e. 2 for double-wide). - uint8_t cell_width [[attribute(6)]]; + ushort2 grid_pos [[attribute(3)]]; // The color of the rendered text glyph. - uchar4 color [[attribute(5)]]; + uchar4 color [[attribute(4)]]; - // 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 mode for this cell. + uint8_t mode [[attribute(5)]]; - // 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)]]; + // The width to constrain the glyph to, in cells, or 0 for no constraint. + uint8_t constraint_width [[attribute(6)]]; }; 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 * float2(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; +vertex CellTextVertexOut cell_text_vertex( + uint vid [[vertex_id]], + CellTextVertexIn in [[stage_in]], + constant Uniforms& uniforms [[buffer(1)]], + constant uchar4 *bg_colors [[buffer(2)]] +) { + // Convert the grid x, y into world space x, y by accounting for cell size + float2 cell_pos = uniforms.cell_size * float2(in.grid_pos); // Turn the cell position into a vertex point depending on the // vertex ID. Since we use instanced drawing, we have 4 vertices @@ -224,44 +215,68 @@ vertex CellTextVertexOut cell_text_vertex(unsigned int vid [[vertex_id]], // 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; + float2 corner; + corner.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f; + corner.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; + out.mode = in.mode; + out.color = float4(in.color) / 255.0f; - float2 glyph_size = float2(input.glyph_size); - float2 glyph_offset = float2(input.glyph_offset); + // === Grid Cell === + // + // offset.x = bearings.x + // .|. + // | | + // +-------+_. + // ._| | | + // | | .###. | | + // | | #...# | +- bearings.y + // glyph_size.y -+ | ##### | | + // | | #.... | | + // ^ |_| .#### |_| _. + // | | | +- offset.y = cell_size.y - bearings.y + // . cell_pos -> +-------+ -' + // +Y. |_._| + // . | + // | glyph_size.x + // 0,0--...-> + // +X + // + // In order to get the bottom left of the glyph, we compute an offset based + // on the bearings. The Y bearing is the distance from the top of the cell + // to the bottom of the glyph, so we subtract it from the cell height to get + // the y offset. The X bearing is the distance from the left of the cell to + // the left of the glyph, so it works as the x offset directly. - // 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; + float2 size = float2(in.glyph_size); + float2 offset = float2(in.bearings); + + offset.y = uniforms.cell_size.y - 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; + if (in.mode == MODE_TEXT_CONSTRAINED || in.mode == MODE_TEXT_COLOR) { + float max_width = uniforms.cell_size.x * in.constraint_width; + if (size.x > max_width) { + float new_y = size.y * (max_width / size.x); + offset.y += (size.y - new_y) / 2; + size.y = new_y; + size.x = max_width; } } // 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; + cell_pos = cell_pos + size * corner + 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; + // (between 0.0 and 1.0), and does not need to be, since the texture will + // be sampled with pixel coordinate mode. + out.tex_coord = float2(in.glyph_pos) + float2(in.glyph_size) * corner; // 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 @@ -270,27 +285,33 @@ vertex CellTextVertexOut cell_text_vertex(unsigned int vid [[vertex_id]], // since we want color glyphs to appear in their original color // and Powerline glyphs to be unaffected (else parts of the line would // have different colors as some parts are displayed via background colors). - if (uniforms.min_contrast > 1.0f && input.mode == MODE_TEXT) { - float4 bg_color = float4(input.bg_color) / 255.0f; + if (uniforms.min_contrast > 1.0f && in.mode == MODE_TEXT) { + float4 bg_color = float4(bg_colors[in.grid_pos.y * uniforms.grid_size.x + in.grid_pos.x]) / 255.0f; out.color = contrasted_color(uniforms.min_contrast, out.color, bg_color); } // If this cell is the cursor cell, then we need to change the color. - if (input.mode != MODE_TEXT_CURSOR && - input.grid_pos.x == uniforms.cursor_pos.x && - input.grid_pos.y == uniforms.cursor_pos.y) { + if ( + in.mode != MODE_TEXT_CURSOR && + in.grid_pos.x == uniforms.cursor_pos.x && + in.grid_pos.y == uniforms.cursor_pos.y + ) { out.color = float4(uniforms.cursor_color) / 255.0f; } 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); +fragment float4 cell_text_fragment( + CellTextVertexOut in [[stage_in]], + texture2d textureGreyscale [[texture(0)]], + texture2d textureColor [[texture(1)]] +) { + constexpr sampler textureSampler( + coord::pixel, + address::clamp_to_edge, + filter::nearest + ); switch (in.mode) { default: @@ -298,26 +319,20 @@ fragment float4 cell_text_fragment(CellTextVertexOut in [[stage_in]], case MODE_TEXT_CONSTRAINED: case MODE_TEXT_POWERLINE: 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; + float a = textureGreyscale.sample(textureSampler, in.tex_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); + return textureColor.sample(textureSampler, in.tex_coord); } } } @@ -329,17 +344,17 @@ fragment float4 cell_text_fragment(CellTextVertexOut in [[stage_in]], struct ImageVertexIn { // The grid coordinates (x, y) where x < columns and y < rows where // the image will be rendered. It will be rendered from the top left. - float2 grid_pos [[attribute(1)]]; + float2 grid_pos [[attribute(0)]]; // Offset in pixels from the top-left of the cell to make the top-left // corner of the image. - float2 cell_offset [[attribute(2)]]; + float2 cell_offset [[attribute(1)]]; // The source rectangle of the texture to sample from. - float4 source_rect [[attribute(3)]]; + float4 source_rect [[attribute(2)]]; // The final width/height of the image in pixels. - float2 dest_size [[attribute(4)]]; + float2 dest_size [[attribute(3)]]; }; struct ImageVertexOut { @@ -347,10 +362,12 @@ struct ImageVertexOut { float2 tex_coord; }; -vertex ImageVertexOut image_vertex(unsigned int vid [[vertex_id]], - ImageVertexIn input [[stage_in]], - texture2d image [[texture(0)]], - constant Uniforms& uniforms [[buffer(1)]]) { +vertex ImageVertexOut image_vertex( + uint vid [[vertex_id]], + ImageVertexIn in [[stage_in]], + texture2d image [[texture(0)]], + constant Uniforms& uniforms [[buffer(1)]] +) { // The size of the image in pixels float2 image_size = float2(image.get_width(), image.get_height()); @@ -364,22 +381,22 @@ vertex ImageVertexOut image_vertex(unsigned int vid [[vertex_id]], // 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; + float2 corner; + corner.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f; + corner.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f; // The texture coordinates start at our source x/y, then add the width/height // as enabled by our instance id, then normalize to [0, 1] - float2 tex_coord = input.source_rect.xy; - tex_coord += input.source_rect.zw * position; + float2 tex_coord = in.source_rect.xy; + tex_coord += in.source_rect.zw * corner; tex_coord /= image_size; ImageVertexOut out; // The position of our image starts at the top-left of the grid cell and // adds the source rect width/height components. - float2 image_pos = (uniforms.cell_size * input.grid_pos) + input.cell_offset; - image_pos += input.dest_size * position; + float2 image_pos = (uniforms.cell_size * in.grid_pos) + in.cell_offset; + image_pos += in.dest_size * corner; out.position = uniforms.projection_matrix * float4(image_pos.x, image_pos.y, 0.0f, 1.0f); @@ -387,8 +404,10 @@ vertex ImageVertexOut image_vertex(unsigned int vid [[vertex_id]], return out; } -fragment float4 image_fragment(ImageVertexOut in [[stage_in]], - texture2d image [[texture(0)]]) { +fragment float4 image_fragment( + ImageVertexOut in [[stage_in]], + texture2d image [[texture(0)]] +) { constexpr sampler textureSampler(address::clamp_to_edge, filter::linear); // Ehhhhh our texture is in RGBA8Uint but our color attachment is @@ -403,19 +422,3 @@ fragment float4 image_fragment(ImageVertexOut in [[stage_in]], return result; } -//------------------------------------------------------------------- -// Post Shader -//------------------------------------------------------------------- -#pragma mark - Post Shader - -struct PostVertexOut { - float4 position [[position]]; -}; - -constant float2 post_pos[4] = {{-1, -1}, {1, -1}, {-1, 1}, {1, 1}}; - -vertex PostVertexOut post_vertex(uint id [[vertex_id]]) { - PostVertexOut out; - out.position = float4(post_pos[id], 0, 1); - return out; -}