Merge pull request #2062 from qwerasd205/metal-optimizations

Metal optimizations
This commit is contained in:
Mitchell Hashimoto
2024-08-08 18:56:09 -07:00
committed by GitHub
5 changed files with 399 additions and 529 deletions

View File

@ -625,8 +625,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
.cell_size = undefined, .cell_size = undefined,
.grid_size = undefined, .grid_size = undefined,
.grid_padding = undefined, .grid_padding = undefined,
.padding_extend_top = true, .padding_extend = .{},
.padding_extend_bottom = true,
.min_contrast = options.config.min_contrast, .min_contrast = options.config.min_contrast,
.cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) }, .cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) },
.cursor_color = undefined, .cursor_color = undefined,
@ -1084,7 +1083,7 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
// Setup our frame data // Setup our frame data
try frame.uniforms.sync(self.gpu_state.device, &.{self.uniforms}); 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); 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. // 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]); try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]);
// Then draw background cells // Then draw background cells
try self.drawCellBgs(encoder, frame, bg_count); try self.drawCellBgs(encoder, frame);
// Then draw images under text // Then draw images under text
try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]); try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]);
@ -1371,9 +1370,9 @@ fn drawPostShader(
void, void,
objc.sel("drawPrimitives:vertexStart:vertexCount:"), objc.sel("drawPrimitives:vertexStart:vertexCount:"),
.{ .{
@intFromEnum(mtl.MTLPrimitiveType.triangle_strip), @intFromEnum(mtl.MTLPrimitiveType.triangle),
@as(c_ulong, 0), @as(c_ulong, 0),
@as(c_ulong, 4), @as(c_ulong, 3),
}, },
); );
} }
@ -1503,12 +1502,7 @@ fn drawCellBgs(
self: *Metal, self: *Metal,
encoder: objc.Object, encoder: objc.Object,
frame: *const FrameState, frame: *const FrameState,
len: usize,
) !void { ) !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 // Use our shader pipeline
encoder.msgSend( encoder.msgSend(
void, void,
@ -1519,25 +1513,22 @@ fn drawCellBgs(
// Set our buffers // Set our buffers
encoder.msgSend( encoder.msgSend(
void, void,
objc.sel("setVertexBuffer:offset:atIndex:"), objc.sel("setFragmentBuffer:offset:atIndex:"),
.{ frame.cells_bg.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) }, .{ frame.cells_bg.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) },
); );
encoder.msgSend( encoder.msgSend(
void, void,
objc.sel("setVertexBuffer:offset:atIndex:"), objc.sel("setFragmentBuffer:offset:atIndex:"),
.{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) }, .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) },
); );
encoder.msgSend( encoder.msgSend(
void, void,
objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"), objc.sel("drawPrimitives:vertexStart:vertexCount:"),
.{ .{
@intFromEnum(mtl.MTLPrimitiveType.triangle), @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, 0),
@as(c_ulong, len), @as(c_ulong, 3),
}, },
); );
} }
@ -1571,6 +1562,11 @@ fn drawCellFgs(
objc.sel("setVertexBuffer:offset:atIndex:"), objc.sel("setVertexBuffer:offset:atIndex:"),
.{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) }, .{ 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( encoder.msgSend(
void, void,
objc.sel("setFragmentTexture:atIndex:"), objc.sel("setFragmentTexture:atIndex:"),
@ -1953,16 +1949,20 @@ pub fn setScreenSize(
const padded_dim = dim.subPadding(padding); const padded_dim = dim.subPadding(padding);
// Blank space around the grid. // Blank space around the grid.
const blank: renderer.Padding = switch (self.config.padding_color) { const blank: renderer.Padding = dim.blankPadding(padding, grid_size, .{
// We can use zero padding because the background color is our
// clear color.
.background => .{},
.extend => dim.blankPadding(padding, grid_size, .{
.width = self.grid_metrics.cell_width, .width = self.grid_metrics.cell_width,
.height = self.grid_metrics.cell_height, .height = self.grid_metrics.cell_height,
}).add(padding), }).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 // Set the size of the drawable surface to the bounds
self.layer.setProperty("drawableSize", macos.graphics.Size{ self.layer.setProperty("drawableSize", macos.graphics.Size{
@ -1993,8 +1993,7 @@ pub fn setScreenSize(
@floatFromInt(blank.bottom), @floatFromInt(blank.bottom),
@floatFromInt(blank.left), @floatFromInt(blank.left),
}, },
.padding_extend_top = old.padding_extend_top, .padding_extend = padding_extend,
.padding_extend_bottom = old.padding_extend_bottom,
.min_contrast = old.min_contrast, .min_contrast = old.min_contrast,
.cursor_pos = old.cursor_pos, .cursor_pos = old.cursor_pos,
.cursor_color = old.cursor_color, .cursor_color = old.cursor_color,
@ -2139,8 +2138,10 @@ fn rebuildCells(
self.cells.reset(); self.cells.reset();
// We also reset our padding extension depending on the screen type // We also reset our padding extension depending on the screen type
self.uniforms.padding_extend_top = screen_type == .alternate; if (self.config.padding_color == .extend) {
self.uniforms.padding_extend_bottom = screen_type == .alternate; 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 // 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 // under certain conditions we feel are safe. This helps make some
// scenarios look better while avoiding scenarios we know do NOT look // scenarios look better while avoiding scenarios we know do NOT look
// good. // good.
if (self.config.padding_color == .extend) {
if (y == 0 and screen_type == .primary) { if (y == 0 and screen_type == .primary) {
self.uniforms.padding_extend_top = !row.neverExtendBg(); self.uniforms.padding_extend.up = !row.neverExtendBg();
} else if (y == self.cells.size.rows - 1 and screen_type == .primary) { } else if (y == self.cells.size.rows - 1 and screen_type == .primary) {
self.uniforms.padding_extend_bottom = !row.neverExtendBg(); self.uniforms.padding_extend.down = !row.neverExtendBg();
}
} }
// Split our row into runs and shape each one. // 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; const alpha: u8 = if (style.flags.faint) 175 else 255;
// If the cell has a background, we always draw it. // 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 // Determine our background alpha. If we have transparency configured
// then this is dynamic depending on some situations. This is all // then this is dynamic depending on some situations. This is all
// in an attempt to make transparency look the best for various // in an attempt to make transparency look the best for various
@ -2440,21 +2443,17 @@ fn updateCell(
break :bg_alpha @intFromFloat(bg_alpha); break :bg_alpha @intFromFloat(bg_alpha);
}; };
try self.cells.add(self.alloc, .bg, .{ self.cells.bgCell(coord.y, coord.x).* = .{
.mode = .rgb, rgb.r, rgb.g, rgb.b, bg_alpha,
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
.cell_width = cell.gridWidth(),
.color = .{ 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 the shaper cell has a glyph, draw it.
if (shaper_cell.glyph_index) |glyph_index| glyph: { if (shaper_cell.glyph_index) |glyph_index| glyph: {
// Render // Render
@ -2487,14 +2486,13 @@ fn updateCell(
try self.cells.add(self.alloc, .text, .{ try self.cells.add(self.alloc, .text, .{
.mode = mode, .mode = mode,
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, .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 }, .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_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height }, .glyph_size = .{ render.glyph.width, render.glyph.height },
.glyph_offset = .{ .bearings = .{
render.glyph.offset_x + shaper_cell.x_offset, @intCast(render.glyph.offset_x + shaper_cell.x_offset),
render.glyph.offset_y + shaper_cell.y_offset, @intCast(render.glyph.offset_y + shaper_cell.y_offset),
}, },
}); });
} }
@ -2524,12 +2522,14 @@ fn updateCell(
try self.cells.add(self.alloc, .underline, .{ try self.cells.add(self.alloc, .underline, .{
.mode = .fg, .mode = .fg,
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
.cell_width = cell.gridWidth(), .constraint_width = cell.gridWidth(),
.color = .{ color.r, color.g, color.b, alpha }, .color = .{ color.r, color.g, color.b, alpha },
.bg_color = bg,
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height }, .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, .{ try self.cells.add(self.alloc, .strikethrough, .{
.mode = .fg, .mode = .fg,
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, .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 }, .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_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height }, .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(.{ self.cells.setCursor(.{
.mode = .cursor, .mode = .cursor,
.grid_pos = .{ x, screen.cursor.y }, .grid_pos = .{ x, screen.cursor.y },
.cell_width = if (wide) 2 else 1,
.color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha }, .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_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height }, .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 // Add our opaque background cell
try self.cells.add(self.alloc, .bg, .{ self.cells.bgCell(coord.y, coord.x).* = .{
.mode = .rgb, bg.r, bg.g, bg.b, 255,
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, };
.cell_width = if (cp.wide) 2 else 1, if (cp.wide and coord.x < self.cells.size.columns - 1) {
.color = .{ bg.r, bg.g, bg.b, 255 }, self.cells.bgCell(coord.y, coord.x + 1).* = .{
}); bg.r, bg.g, bg.b, 255,
};
}
// Add our text // Add our text
try self.cells.add(self.alloc, .text, .{ try self.cells.add(self.alloc, .text, .{
.mode = .fg, .mode = .fg,
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
.cell_width = if (cp.wide) 2 else 1,
.color = .{ fg.r, fg.g, fg.b, 255 }, .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_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height }, .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),
},
}); });
} }

View File

@ -51,11 +51,13 @@ pub const MTLIndexType = enum(c_ulong) {
pub const MTLVertexFormat = enum(c_ulong) { pub const MTLVertexFormat = enum(c_ulong) {
uchar4 = 3, uchar4 = 3,
ushort2 = 13, ushort2 = 13,
short2 = 16,
float2 = 29, float2 = 29,
float4 = 31, float4 = 31,
int2 = 33, int2 = 33,
uint = 36, uint = 36,
uint2 = 37, uint2 = 37,
uint4 = 39,
uchar = 45, uchar = 45,
}; };

View File

@ -75,22 +75,13 @@ fn ArrayListPool(comptime T: type) type {
pub const Contents = struct { pub const Contents = struct {
size: renderer.GridSize = .{ .rows = 0, .columns = 0 }, size: renderer.GridSize = .{ .rows = 0, .columns = 0 },
/// The ArrayListPool which holds all of the background cells. When sized /// Flat array containing cell background colors for the terminal grid.
/// 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.
/// ///
/// HOWEVER, the initial capacity can be exceeded due to multi-glyph /// Indexed as `bg_cells[row * size.columns + col]`.
/// 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.
/// ///
/// Rows are indexed as Contents.bg_rows[y]. /// Prefer accessing with `Contents.bgCell(row, col).*` instead
/// /// of directly indexing in order to avoid integer size bugs.
/// Must be initialized by calling resize on the Contents struct before bg_cells: []mtl_shaders.CellBg = undefined,
/// calling any operations.
bg_rows: ArrayListPool(mtl_shaders.CellBg) = .{},
/// The ArrayListPool which holds all of the foreground cells. When sized /// The ArrayListPool which holds all of the foreground cells. When sized
/// with Contents.resize the individual ArrayLists are given enough room /// with Contents.resize the individual ArrayLists are given enough room
@ -116,7 +107,7 @@ pub const Contents = struct {
fg_rows: ArrayListPool(mtl_shaders.CellText) = .{}, fg_rows: ArrayListPool(mtl_shaders.CellText) = .{},
pub fn deinit(self: *Contents, alloc: Allocator) void { pub fn deinit(self: *Contents, alloc: Allocator) void {
self.bg_rows.deinit(alloc); alloc.free(self.bg_cells);
self.fg_rows.deinit(alloc); self.fg_rows.deinit(alloc);
} }
@ -129,15 +120,12 @@ pub const Contents = struct {
) !void { ) !void {
self.size = size; self.size = size;
// When we create our bg_rows pool, we give the lists an initial const cell_count = @as(usize, size.columns) * @as(usize, size.rows);
// capacity of size.columns. This is to account for the usual case
// where you have a row with normal text and background colors. const bg_cells = try alloc.alloc(mtl_shaders.CellBg, cell_count);
// This can be exceeded due to multi-glyph composites each adding errdefer alloc.free(bg_cells);
// a background cell for the same position. This should probably be
// considered a bug, but for now it means that sometimes allocations @memset(bg_cells, .{0, 0, 0, 0});
// 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);
// The foreground lists can hold 3 types of items: // The foreground lists can hold 3 types of items:
// - Glyphs // - 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); var fg_rows = try ArrayListPool(mtl_shaders.CellText).init(alloc, size.rows + 1, size.columns * 3);
errdefer fg_rows.deinit(alloc); errdefer fg_rows.deinit(alloc);
self.bg_rows.deinit(alloc); alloc.free(self.bg_cells);
self.fg_rows.deinit(alloc); self.fg_rows.deinit(alloc);
self.bg_rows = bg_rows; self.bg_cells = bg_cells;
self.fg_rows = fg_rows; self.fg_rows = fg_rows;
// We don't need 3*cols worth of cells for the cursor list, so we can // 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. /// Reset the cell contents to an empty state without resizing.
pub fn reset(self: *Contents) void { pub fn reset(self: *Contents) void {
self.bg_rows.reset(); @memset(self.bg_cells, .{ 0, 0, 0, 0 });
self.fg_rows.reset(); 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 /// Add a cell to the appropriate list. Adding the same cell twice will
/// result in duplication in the vertex buffer. The caller should clear /// result in duplication in the vertex buffer. The caller should clear
/// the corresponding row with Contents.clear to remove old cells first. /// the corresponding row with Contents.clear to remove old cells first.
@ -197,7 +191,7 @@ pub const Contents = struct {
assert(y < self.size.rows); assert(y < self.size.rows);
switch (key) { switch (key) {
.bg => try self.bg_rows.lists[y].append(alloc, cell), .bg => comptime unreachable,
.text, .text,
.underline, .underline,
@ -213,7 +207,8 @@ pub const Contents = struct {
pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void { pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void {
assert(y < self.size.rows); 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 // 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 // of our fg row pool, so we need to add 1 to the y to get the
// correct index. // correct index.
@ -234,47 +229,42 @@ test Contents {
// We should start off empty after resizing. // We should start off empty after resizing.
for (0..rows) |y| { 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); 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. // 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].capacity == 1);
try testing.expect(c.fg_rows.lists[0].items.len == 0); try testing.expect(c.fg_rows.lists[0].items.len == 0);
// Add some contents. // Add some contents.
const bg_cell: mtl_shaders.CellBg = .{ const bg_cell: mtl_shaders.CellBg = .{ 0, 0, 0, 1 };
.mode = .rgb,
.grid_pos = .{ 4, 1 },
.cell_width = 1,
.color = .{ 0, 0, 0, 1 },
};
const fg_cell: mtl_shaders.CellText = .{ const fg_cell: mtl_shaders.CellText = .{
.mode = .fg, .mode = .fg,
.grid_pos = .{ 4, 1 }, .grid_pos = .{ 4, 1 },
.cell_width = 1,
.color = .{ 0, 0, 0, 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 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. // 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]); try testing.expectEqual(fg_cell, c.fg_rows.lists[2].items[0]);
// And we should be able to clear it. // And we should be able to clear it.
c.clear(1); c.clear(1);
for (0..rows) |y| { 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); 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. // Add a cursor.
const cursor_cell: mtl_shaders.CellText = .{ const cursor_cell: mtl_shaders.CellText = .{
.mode = .cursor, .mode = .cursor,
.grid_pos = .{ 2, 3 }, .grid_pos = .{ 2, 3 },
.cell_width = 1,
.color = .{ 0, 0, 0, 1 }, .color = .{ 0, 0, 0, 1 },
.bg_color = .{ 0, 0, 0, 1 },
}; };
c.setCursor(cursor_cell); c.setCursor(cursor_cell);
try testing.expectEqual(cursor_cell, c.fg_rows.lists[0].items[0]); 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); defer c.deinit(alloc);
// Set some contents // Set some contents
const cell1: mtl_shaders.CellBg = .{ // bg and fg cells in row 1
.mode = .rgb, const bg_cell_1: mtl_shaders.CellBg = .{ 0, 0, 0, 1 };
const fg_cell_1: mtl_shaders.CellText = .{
.mode = .fg,
.grid_pos = .{ 4, 1 }, .grid_pos = .{ 4, 1 },
.cell_width = 1,
.color = .{ 0, 0, 0, 1 }, .color = .{ 0, 0, 0, 1 },
}; };
const cell2: mtl_shaders.CellBg = .{ c.bgCell(1, 4).* = bg_cell_1;
.mode = .rgb, 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 }, .grid_pos = .{ 4, 2 },
.cell_width = 1,
.color = .{ 0, 0, 0, 1 }, .color = .{ 0, 0, 0, 1 },
}; };
try c.add(alloc, .bg, cell1); c.bgCell(2, 4).* = bg_cell_2;
try c.add(alloc, .bg, cell2); try c.add(alloc, .text, fg_cell_2);
// Clear row 1, this should leave row 2 untouched
c.clear(1); c.clear(1);
// Row 2 should still contain its cell. // Row 2 should still contain its cells.
try testing.expectEqual(cell2, c.bg_rows.lists[2].items[0]); 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" { test "Contents clear last added content" {
@ -328,22 +326,30 @@ test "Contents clear last added content" {
defer c.deinit(alloc); defer c.deinit(alloc);
// Set some contents // Set some contents
const cell1: mtl_shaders.CellBg = .{ // bg and fg cells in row 1
.mode = .rgb, const bg_cell_1: mtl_shaders.CellBg = .{ 0, 0, 0, 1 };
const fg_cell_1: mtl_shaders.CellText = .{
.mode = .fg,
.grid_pos = .{ 4, 1 }, .grid_pos = .{ 4, 1 },
.cell_width = 1,
.color = .{ 0, 0, 0, 1 }, .color = .{ 0, 0, 0, 1 },
}; };
const cell2: mtl_shaders.CellBg = .{ c.bgCell(1, 4).* = bg_cell_1;
.mode = .rgb, 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 }, .grid_pos = .{ 4, 2 },
.cell_width = 1,
.color = .{ 0, 0, 0, 1 }, .color = .{ 0, 0, 0, 1 },
}; };
try c.add(alloc, .bg, cell1); c.bgCell(2, 4).* = bg_cell_2;
try c.add(alloc, .bg, cell2); try c.add(alloc, .text, fg_cell_2);
// Clear row 2, this should leave row 1 untouched
c.clear(2); c.clear(2);
// Row 1 should still contain its cell. // Row 1 should still contain its cells.
try testing.expectEqual(cell1, c.bg_rows.lists[1].items[0]); 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]);
} }

View File

@ -124,9 +124,10 @@ pub const Uniforms = extern struct {
/// top, right, bottom, left. /// top, right, bottom, left.
grid_padding: [4]f32 align(16), grid_padding: [4]f32 align(16),
/// True if vertical padding gets the extended color of the nearest row. /// Bit mask defining which directions to
padding_extend_top: bool align(1), /// extend cell colors in to the padding.
padding_extend_bottom: bool align(1), /// Order, LSB first: left, right, up, down
padding_extend: PaddingExtend align(1),
/// The minimum contrast ratio for text. The contrast ratio is calculated /// The minimum contrast ratio for text. The contrast ratio is calculated
/// according to the WCAG 2.0 spec. /// according to the WCAG 2.0 spec.
@ -135,6 +136,14 @@ pub const Uniforms = extern struct {
/// The cursor position and color. /// The cursor position and color.
cursor_pos: [2]u16 align(4), cursor_pos: [2]u16 align(4),
cursor_color: [4]u8 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. /// The uniforms used for custom postprocess shaders.
@ -246,7 +255,7 @@ fn initPostPipeline(
// Get our vertex and fragment functions // Get our vertex and fragment functions
const func_vert = func_vert: { const func_vert = func_vert: {
const str = try macos.foundation.String.createWithBytes( const str = try macos.foundation.String.createWithBytes(
"post_vertex", "full_screen_vertex",
.utf8, .utf8,
false, false,
); );
@ -307,14 +316,13 @@ fn initPostPipeline(
/// This is a single parameter for the terminal cell shader. /// This is a single parameter for the terminal cell shader.
pub const CellText = extern struct { pub const CellText = extern struct {
mode: Mode, glyph_pos: [2]u32 align(8) = .{ 0, 0 },
glyph_pos: [2]u32 = .{ 0, 0 }, glyph_size: [2]u32 align(8) = .{ 0, 0 },
glyph_size: [2]u32 = .{ 0, 0 }, bearings: [2]i16 align(4) = .{ 0, 0 },
glyph_offset: [2]i32 = .{ 0, 0 }, grid_pos: [2]u16 align(4),
color: [4]u8, color: [4]u8 align(4),
bg_color: [4]u8, mode: Mode align(1),
grid_pos: [2]u16, constraint_width: u8 align(1) = 0,
cell_width: u8,
pub const Mode = enum(u8) { pub const Mode = enum(u8) {
fg = 1, fg = 1,
@ -323,6 +331,12 @@ pub const CellText = extern struct {
cursor = 4, cursor = 4,
fg_powerline = 5, 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. /// 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 // Our attributes are the fields of the input
const attrs = objc.Object.fromId(desc.getProperty(?*anyopaque, "attributes")); const attrs = objc.Object.fromId(desc.getProperty(?*anyopaque, "attributes"));
{ autoAttribute(CellText, attrs);
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));
}
// The layout describes how and when we fetch the next vertex input. // The layout describes how and when we fetch the next vertex input.
const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts")); 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. /// This is a single parameter for the cell bg shader.
pub const CellBg = extern struct { pub const CellBg = [4]u8;
mode: Mode,
grid_pos: [2]u16,
color: [4]u8,
cell_width: u8,
pub const Mode = enum(u8) {
rgb = 1,
};
};
/// Initialize the cell background render pipeline for our shader library. /// Initialize the cell background render pipeline for our shader library.
fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object { fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object {
// Get our vertex and fragment functions // Get our vertex and fragment functions
const func_vert = func_vert: { const func_vert = func_vert: {
const str = try macos.foundation.String.createWithBytes( const str = try macos.foundation.String.createWithBytes(
"cell_bg_vertex", "full_screen_vertex",
.utf8, .utf8,
false, false,
); );
@ -585,41 +503,8 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object {
.{@as(c_ulong, 0)}, .{@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("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)); 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 // Our attributes are the fields of the input
const attrs = objc.Object.fromId(desc.getProperty(?*anyopaque, "attributes")); const attrs = objc.Object.fromId(desc.getProperty(?*anyopaque, "attributes"));
{ autoAttribute(Image, attrs);
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));
}
// The layout describes how and when we fetch the next vertex input. // The layout describes how and when we fetch the next vertex input.
const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts")); 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; 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 { fn checkError(err_: ?*anyopaque) !void {
const nserr = objc.Object.fromId(err_ orelse return); const nserr = objc.Object.fromId(err_ orelse return);
const str = @as( const str = @as(
@ -855,27 +732,3 @@ fn checkError(err_: ?*anyopaque) !void {
log.err("metal error={s}", .{str.cstringPtr(.ascii).?}); log.err("metal error={s}", .{str.cstringPtr(.ascii).?});
return error.MetalFailed; 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));
}
}

View File

@ -2,13 +2,19 @@
using namespace metal; using namespace metal;
enum Padding : uint8_t {
EXTEND_LEFT = 1u,
EXTEND_RIGHT = 2u,
EXTEND_UP = 4u,
EXTEND_DOWN = 8u,
};
struct Uniforms { struct Uniforms {
float4x4 projection_matrix; float4x4 projection_matrix;
float2 cell_size; float2 cell_size;
ushort2 grid_size; ushort2 grid_size;
float4 grid_padding; float4 grid_padding;
bool padding_extend_top; uint8_t padding_extend;
bool padding_extend_bottom;
float min_contrast; float min_contrast;
ushort2 cursor_pos; ushort2 cursor_pos;
uchar4 cursor_color; 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. struct FullScreenVertexOut {
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 {
float4 position [[position]]; float4 position [[position]];
float4 color;
}; };
vertex CellBgVertexOut cell_bg_vertex(unsigned int vid [[vertex_id]], vertex FullScreenVertexOut full_screen_vertex(
CellBgVertexIn input [[stage_in]], uint vid [[vertex_id]]
constant Uniforms& uniforms ) {
[[buffer(1)]]) { FullScreenVertexOut out;
// 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 float4 position;
float2 cell_size_scaled = uniforms.cell_size; position.x = (vid == 2) ? 3.0 : -1.0;
cell_size_scaled.x = cell_size_scaled.x * input.cell_width; 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 // Single triangle is clipped to viewport.
// 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.
// //
// 0 = top-right // X <- vid == 0: (-1, -3)
// 1 = bot-right // |\
// 2 = bot-left // | \
// 3 = top-left // | \
float2 position; // |###\
position.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f; // |#+# \ `+` is (0, 0). `#`s are viewport area.
position.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f; // |### \
// X------X <- vid == 2: (3, 1)
// ^
// vid == 1: (-1, 1)
// Calculate the final position of our cell in world space. out.position = position;
// 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; 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 { struct CellTextVertexIn {
// 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 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) // The position of the glyph in the texture (x, y)
uint2 glyph_pos [[attribute(2)]]; uint2 glyph_pos [[attribute(0)]];
// The size of the glyph in the texture (w, h) // The size of the glyph in the texture (w, h)
uint2 glyph_size [[attribute(3)]]; uint2 glyph_size [[attribute(1)]];
// The left and top bearings for the glyph (x, y) // The left and top bearings for the glyph (x, y)
int2 glyph_offset [[attribute(4)]]; int2 bearings [[attribute(2)]];
// The grid coordinates (x, y) where x < columns and y < rows
ushort2 grid_pos [[attribute(3)]];
// The color of the rendered text glyph.
uchar4 color [[attribute(4)]];
// The mode for this cell.
uint8_t mode [[attribute(5)]];
// The width to constrain the glyph to, in cells, or 0 for no constraint.
uint8_t constraint_width [[attribute(6)]];
}; };
struct CellTextVertexOut { struct CellTextVertexOut {
float4 position [[position]]; float4 position [[position]];
float2 cell_size;
uint8_t mode; uint8_t mode;
float4 color; float4 color;
float2 tex_coord; float2 tex_coord;
}; };
vertex CellTextVertexOut cell_text_vertex(unsigned int vid [[vertex_id]], vertex CellTextVertexOut cell_text_vertex(
CellTextVertexIn input [[stage_in]], uint vid [[vertex_id]],
constant Uniforms& uniforms CellTextVertexIn in [[stage_in]],
[[buffer(1)]]) { 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 // 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); float2 cell_pos = uniforms.cell_size * float2(in.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 // Turn the cell position into a vertex point depending on the
// vertex ID. Since we use instanced drawing, we have 4 vertices // 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 // 1 = bot-right
// 2 = bot-left // 2 = bot-left
// 3 = top-left // 3 = top-left
float2 position; float2 corner;
position.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f; corner.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f;
position.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f; corner.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f;
CellTextVertexOut out; CellTextVertexOut out;
out.mode = input.mode; out.mode = in.mode;
out.cell_size = uniforms.cell_size; out.color = float4(in.color) / 255.0f;
out.color = float4(input.color) / 255.0f;
float2 glyph_size = float2(input.glyph_size); // === Grid Cell ===
float2 glyph_offset = float2(input.glyph_offset); //
// 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 float2 size = float2(in.glyph_size);
// to the baseline is the offset (+y is up). Our grid goes down. float2 offset = float2(in.bearings);
// So we flip it with `cell_size.y - glyph_offset.y`.
glyph_offset.y = cell_size_scaled.y - glyph_offset.y; offset.y = uniforms.cell_size.y - offset.y;
// If we're constrained then we need to scale the glyph. // If we're constrained then we need to scale the glyph.
// We also always constrain colored glyphs since we should have // We also always constrain colored glyphs since we should have
// their scaled cell size exactly correct. // their scaled cell size exactly correct.
if (input.mode == MODE_TEXT_CONSTRAINED || input.mode == MODE_TEXT_COLOR) { if (in.mode == MODE_TEXT_CONSTRAINED || in.mode == MODE_TEXT_COLOR) {
if (glyph_size.x > cell_size_scaled.x) { float max_width = uniforms.cell_size.x * in.constraint_width;
float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x); if (size.x > max_width) {
glyph_offset.y += (glyph_size.y - new_y) / 2; float new_y = size.y * (max_width / size.x);
glyph_size.y = new_y; offset.y += (size.y - new_y) / 2;
glyph_size.x = cell_size_scaled.x; size.y = new_y;
size.x = max_width;
} }
} }
// Calculate the final position of the cell which uses our glyph size // Calculate the final position of the cell which uses our glyph size
// and glyph offset to create the correct bounding box for the glyph. // 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 = out.position =
uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f); uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
// Calculate the texture coordinate in pixels. This is NOT normalized // Calculate the texture coordinate in pixels. This is NOT normalized
// (between 0.0 and 1.0) and must be done in the fragment shader. // (between 0.0 and 1.0), and does not need to be, since the texture will
out.tex_coord = float2(input.glyph_pos) + float2(input.glyph_size) * position; // 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 // 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 // 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 // since we want color glyphs to appear in their original color
// and Powerline glyphs to be unaffected (else parts of the line would // and Powerline glyphs to be unaffected (else parts of the line would
// have different colors as some parts are displayed via background colors). // have different colors as some parts are displayed via background colors).
if (uniforms.min_contrast > 1.0f && input.mode == MODE_TEXT) { if (uniforms.min_contrast > 1.0f && in.mode == MODE_TEXT) {
float4 bg_color = float4(input.bg_color) / 255.0f; 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); 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 this cell is the cursor cell, then we need to change the color.
if (input.mode != MODE_TEXT_CURSOR && if (
input.grid_pos.x == uniforms.cursor_pos.x && in.mode != MODE_TEXT_CURSOR &&
input.grid_pos.y == uniforms.cursor_pos.y) { in.grid_pos.x == uniforms.cursor_pos.x &&
in.grid_pos.y == uniforms.cursor_pos.y
) {
out.color = float4(uniforms.cursor_color) / 255.0f; out.color = float4(uniforms.cursor_color) / 255.0f;
} }
return out; return out;
} }
fragment float4 cell_text_fragment(CellTextVertexOut in [[stage_in]], fragment float4 cell_text_fragment(
texture2d<float> textureGreyscale CellTextVertexOut in [[stage_in]],
[[texture(0)]], texture2d<float> textureGreyscale [[texture(0)]],
texture2d<float> textureColor texture2d<float> textureColor [[texture(1)]]
[[texture(1)]]) { ) {
constexpr sampler textureSampler(address::clamp_to_edge, filter::linear); constexpr sampler textureSampler(
coord::pixel,
address::clamp_to_edge,
filter::nearest
);
switch (in.mode) { switch (in.mode) {
default: default:
@ -298,26 +319,20 @@ fragment float4 cell_text_fragment(CellTextVertexOut in [[stage_in]],
case MODE_TEXT_CONSTRAINED: case MODE_TEXT_CONSTRAINED:
case MODE_TEXT_POWERLINE: case MODE_TEXT_POWERLINE:
case MODE_TEXT: { 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 // We premult the alpha to our whole color since our blend function
// uses One/OneMinusSourceAlpha to avoid blurry edges. // uses One/OneMinusSourceAlpha to avoid blurry edges.
// We first premult our given color. // We first premult our given color.
float4 premult = float4(in.color.rgb * in.color.a, in.color.a); float4 premult = float4(in.color.rgb * in.color.a, in.color.a);
// Then premult the texture color // Then premult the texture color
float a = textureGreyscale.sample(textureSampler, coord).r; float a = textureGreyscale.sample(textureSampler, in.tex_coord).r;
premult = premult * a; premult = premult * a;
return premult; return premult;
} }
case MODE_TEXT_COLOR: { case MODE_TEXT_COLOR: {
// Normalize the texture coordinates to [0,1] return textureColor.sample(textureSampler, in.tex_coord);
float2 size = float2(textureColor.get_width(), textureColor.get_height());
float2 coord = in.tex_coord / size;
return textureColor.sample(textureSampler, coord);
} }
} }
} }
@ -329,17 +344,17 @@ fragment float4 cell_text_fragment(CellTextVertexOut in [[stage_in]],
struct ImageVertexIn { struct ImageVertexIn {
// The grid coordinates (x, y) where x < columns and y < rows where // 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. // 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 // Offset in pixels from the top-left of the cell to make the top-left
// corner of the image. // corner of the image.
float2 cell_offset [[attribute(2)]]; float2 cell_offset [[attribute(1)]];
// The source rectangle of the texture to sample from. // 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. // The final width/height of the image in pixels.
float2 dest_size [[attribute(4)]]; float2 dest_size [[attribute(3)]];
}; };
struct ImageVertexOut { struct ImageVertexOut {
@ -347,10 +362,12 @@ struct ImageVertexOut {
float2 tex_coord; float2 tex_coord;
}; };
vertex ImageVertexOut image_vertex(unsigned int vid [[vertex_id]], vertex ImageVertexOut image_vertex(
ImageVertexIn input [[stage_in]], uint vid [[vertex_id]],
ImageVertexIn in [[stage_in]],
texture2d<uint> image [[texture(0)]], texture2d<uint> image [[texture(0)]],
constant Uniforms& uniforms [[buffer(1)]]) { constant Uniforms& uniforms [[buffer(1)]]
) {
// The size of the image in pixels // The size of the image in pixels
float2 image_size = float2(image.get_width(), image.get_height()); 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 // 1 = bot-right
// 2 = bot-left // 2 = bot-left
// 3 = top-left // 3 = top-left
float2 position; float2 corner;
position.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f; corner.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f;
position.y = (vid == 0 || vid == 3) ? 0.0f : 1.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 // 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] // as enabled by our instance id, then normalize to [0, 1]
float2 tex_coord = input.source_rect.xy; float2 tex_coord = in.source_rect.xy;
tex_coord += input.source_rect.zw * position; tex_coord += in.source_rect.zw * corner;
tex_coord /= image_size; tex_coord /= image_size;
ImageVertexOut out; ImageVertexOut out;
// The position of our image starts at the top-left of the grid cell and // The position of our image starts at the top-left of the grid cell and
// adds the source rect width/height components. // adds the source rect width/height components.
float2 image_pos = (uniforms.cell_size * input.grid_pos) + input.cell_offset; float2 image_pos = (uniforms.cell_size * in.grid_pos) + in.cell_offset;
image_pos += input.dest_size * position; image_pos += in.dest_size * corner;
out.position = out.position =
uniforms.projection_matrix * float4(image_pos.x, image_pos.y, 0.0f, 1.0f); 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; return out;
} }
fragment float4 image_fragment(ImageVertexOut in [[stage_in]], fragment float4 image_fragment(
texture2d<uint> image [[texture(0)]]) { ImageVertexOut in [[stage_in]],
texture2d<uint> image [[texture(0)]]
) {
constexpr sampler textureSampler(address::clamp_to_edge, filter::linear); constexpr sampler textureSampler(address::clamp_to_edge, filter::linear);
// Ehhhhh our texture is in RGBA8Uint but our color attachment is // 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; 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;
}