renderer: metal shaders rework

- Significant changes to optimize memory usage.
- Adjusted formatting of the metal shader code to improve readability.
- Normalized naming conventions in shader code.
- Abstracted repetitive code for attribute descriptors to a helper
function.
This commit is contained in:
Qwerasd
2024-08-07 17:39:31 -04:00
parent df7409f4f3
commit 6339f9bae9
5 changed files with 286 additions and 466 deletions

View File

@ -1084,7 +1084,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 +1179,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, self.cells.size.columns * self.cells.size.rows);
// Then draw images under text
try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]);
@ -1519,25 +1519,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 +1568,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:"),
@ -2411,7 +2413,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,21 +2442,17 @@ 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 },
});
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)))),
self.cells.bg_cells[coord.y * self.cells.size.columns + coord.x] = .{
rgb.r, rgb.g, rgb.b, bg_alpha,
};
if (cell.gridWidth() > 1 and coord.x < self.cells.size.columns - 1) {
self.cells.bg_cells[coord.y * self.cells.size.columns + coord.x + 1] = .{
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: {
// Render
@ -2487,14 +2485,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 +2521,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 +2546,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 +2608,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 +2644,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.bg_cells[coord.y * self.cells.size.columns + coord.x] = .{
bg.r, bg.g, bg.b, 255,
};
if (cp.wide and coord.x < self.cells.size.columns - 1) {
self.cells.bg_cells[coord.y * self.cells.size.columns + 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),
},
});
}

View File

@ -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,
};

View File

@ -75,22 +75,7 @@ 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.
///
/// 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.
///
/// 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) = .{},
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 +101,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 +114,10 @@ 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 = size.columns * size.rows;
const bg_cells = (try alloc.alloc(mtl_shaders.CellBg, cell_count))[0..cell_count];
errdefer alloc.free(bg_cells);
// The foreground lists can hold 3 types of items:
// - Glyphs
@ -154,10 +134,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 +150,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();
}
@ -197,7 +177,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 +193,10 @@ 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();
for (self.bg_cells[y * self.size.columns ..][0..self.size.columns]) |*cell| {
cell.* = .{ 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.

View File

@ -246,7 +246,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 +307,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 +322,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 +372,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 +443,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 +494,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 +609,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 +678,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 +723,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));
}
}

View File

@ -64,93 +64,75 @@ 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((in.position.xy - uniforms.grid_padding.wx) / uniforms.cell_size);
// Clamp x position, extends edge bg colors in to padding on sides.
grid_pos.x = clamp(grid_pos.x, 0, uniforms.grid_size.x - 1);
// Clamp y position if we should extend, otherwise discard if out of bounds.
if (grid_pos.y < 0) {
if (uniforms.padding_extend_top) {
grid_pos.y = 0;
} else {
return float4(0.0);
}
}
if (grid_pos.y > uniforms.grid_size.y - 1) {
if (uniforms.padding_extend_bottom) {
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 +150,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 +198,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 +268,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<float> textureGreyscale
[[texture(0)]],
texture2d<float> textureColor
[[texture(1)]]) {
constexpr sampler textureSampler(address::clamp_to_edge, filter::linear);
fragment float4 cell_text_fragment(
CellTextVertexOut in [[stage_in]],
texture2d<float> textureGreyscale [[texture(0)]],
texture2d<float> textureColor [[texture(1)]]
) {
constexpr sampler textureSampler(
coord::pixel,
address::clamp_to_edge,
filter::nearest
);
switch (in.mode) {
default:
@ -298,26 +302,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 +327,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 +345,12 @@ struct ImageVertexOut {
float2 tex_coord;
};
vertex ImageVertexOut image_vertex(unsigned int vid [[vertex_id]],
ImageVertexIn input [[stage_in]],
vertex ImageVertexOut image_vertex(
uint vid [[vertex_id]],
ImageVertexIn in [[stage_in]],
texture2d<uint> image [[texture(0)]],
constant Uniforms& uniforms [[buffer(1)]]) {
constant Uniforms& uniforms [[buffer(1)]]
) {
// The size of the image in pixels
float2 image_size = float2(image.get_width(), image.get_height());
@ -364,22 +364,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 +387,10 @@ vertex ImageVertexOut image_vertex(unsigned int vid [[vertex_id]],
return out;
}
fragment float4 image_fragment(ImageVertexOut in [[stage_in]],
texture2d<uint> image [[texture(0)]]) {
fragment float4 image_fragment(
ImageVertexOut in [[stage_in]],
texture2d<uint> 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 +405,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;
}