renderer/metal: dedicated cell fg shader

This commit is contained in:
Mitchell Hashimoto
2024-04-22 10:58:23 -07:00
parent e8b623e829
commit d12e3db599
3 changed files with 225 additions and 247 deletions

View File

@ -36,7 +36,6 @@ const Image = mtl_image.Image;
const ImageMap = mtl_image.ImageMap;
const Shaders = mtl_shaders.Shaders;
const CellBuffer = mtl_buffer.Buffer(mtl_shaders.Cell);
const ImageBuffer = mtl_buffer.Buffer(mtl_shaders.Image);
const InstanceBuffer = mtl_buffer.Buffer(u16);
@ -90,8 +89,8 @@ current_background_color: terminal.color.RGB,
/// The current set of cells to render. This is rebuilt on every frame
/// but we keep this around so that we don't reallocate. Each set of
/// cells goes into a separate shader.
cells_bg: std.ArrayListUnmanaged(mtl_shaders.Cell),
cells: std.ArrayListUnmanaged(mtl_shaders.Cell),
cells_bg: std.ArrayListUnmanaged(mtl_shaders.CellBg),
cells: std.ArrayListUnmanaged(mtl_shaders.CellText),
/// The current GPU uniform values.
uniforms: mtl_shaders.Uniforms,
@ -205,8 +204,8 @@ pub const GPUState = struct {
/// This is used to implement double/triple buffering.
pub const FrameState = struct {
uniforms: UniformBuffer,
cells: CellBuffer,
cells_bg: CellBuffer,
cells: CellTextBuffer,
cells_bg: CellBgBuffer,
greyscale: objc.Object, // MTLTexture
greyscale_modified: usize = 0,
@ -215,6 +214,8 @@ pub const FrameState = struct {
/// A buffer containing the uniform data.
const UniformBuffer = mtl_buffer.Buffer(mtl_shaders.Uniforms);
const CellBgBuffer = mtl_buffer.Buffer(mtl_shaders.CellBg);
const CellTextBuffer = mtl_buffer.Buffer(mtl_shaders.CellText);
pub fn init(device: objc.Object) !FrameState {
// Uniform buffer contains exactly 1 uniform struct. The
@ -225,9 +226,9 @@ pub const FrameState = struct {
// Create the buffers for our vertex data. The preallocation size
// is likely too small but our first frame update will resize it.
var cells = try CellBuffer.init(device, 10 * 10);
var cells = try CellTextBuffer.init(device, 10 * 10);
errdefer cells.deinit();
var cells_bg = try CellBuffer.init(device, 10 * 10);
var cells_bg = try CellBgBuffer.init(device, 10 * 10);
errdefer cells_bg.deinit();
// Initialize our textures for our font atlas.
@ -1268,7 +1269,7 @@ fn drawCells(
self: *Metal,
encoder: objc.Object,
frame: *const FrameState,
buf: CellBuffer,
buf: FrameState.CellTextBuffer,
len: usize,
) !void {
// This triggers an assertion in the Metal API if we try to draw
@ -1279,7 +1280,7 @@ fn drawCells(
encoder.msgSend(
void,
objc.sel("setRenderPipelineState:"),
.{self.shaders.cell_pipeline.value},
.{self.shaders.cell_text_pipeline.value},
);
// Set our buffers
@ -1694,7 +1695,7 @@ fn rebuildCells(
// This is the cell that has [mode == .fg] and is underneath our cursor.
// We keep track of it so that we can invert the colors so the character
// remains visible.
var cursor_cell: ?mtl_shaders.Cell = null;
var cursor_cell: ?mtl_shaders.CellText = null;
// Build each cell
var row_it = screen.pages.rowIterator(.right_down, .{ .viewport = .{} }, null);
@ -1847,12 +1848,6 @@ fn rebuildCells(
self.cells.appendAssumeCapacity(cell.*);
}
}
// Some debug mode safety checks
if (std.debug.runtime_safety) {
for (self.cells_bg.items) |cell| assert(cell.mode == .bg);
for (self.cells.items) |cell| assert(cell.mode != .bg);
}
}
fn updateCell(
@ -1964,11 +1959,10 @@ fn updateCell(
};
self.cells_bg.appendAssumeCapacity(.{
.mode = .bg,
.mode = .rgb,
.grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) },
.cell_width = cell.gridWidth(),
.color = .{ rgb.r, rgb.g, rgb.b, bg_alpha },
.bg_color = .{ 0, 0, 0, 0 },
});
break :bg .{ rgb.r, rgb.g, rgb.b, bg_alpha };
@ -1992,7 +1986,7 @@ fn updateCell(
},
);
const mode: mtl_shaders.Cell.Mode = switch (try fgMode(
const mode: mtl_shaders.CellText.Mode = switch (try fgMode(
render.presentation,
cell_pin,
)) {
@ -2080,7 +2074,7 @@ fn addCursor(
self: *Metal,
screen: *terminal.Screen,
cursor_style: renderer.CursorStyle,
) ?*const mtl_shaders.Cell {
) ?*const mtl_shaders.CellText {
// Add the cursor. We render the cursor over the wide character if
// we're on the wide characer tail.
const wide, const x = cell: {
@ -2166,11 +2160,10 @@ fn addPreeditCell(
// Add our opaque background cell
self.cells_bg.appendAssumeCapacity(.{
.mode = .bg,
.mode = .rgb,
.grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) },
.cell_width = if (cp.wide) 2 else 1,
.color = .{ bg.r, bg.g, bg.b, 255 },
.bg_color = .{ bg.r, bg.g, bg.b, 255 },
});
// Add our text

View File

@ -16,7 +16,7 @@ pub const Shaders = struct {
/// The cell shader is the shader used to render the terminal cells.
/// It is a single shader that is used for both the background and
/// foreground.
cell_pipeline: objc.Object,
cell_text_pipeline: objc.Object,
/// The cell background shader is the shader used to render the
/// background of terminal cells.
@ -44,8 +44,8 @@ pub const Shaders = struct {
const library = try initLibrary(device);
errdefer library.msgSend(void, objc.sel("release"), .{});
const cell_pipeline = try initCellPipeline(device, library);
errdefer cell_pipeline.msgSend(void, objc.sel("release"), .{});
const cell_text_pipeline = try initCellTextPipeline(device, library);
errdefer cell_text_pipeline.msgSend(void, objc.sel("release"), .{});
const cell_bg_pipeline = try initCellBgPipeline(device, library);
errdefer cell_bg_pipeline.msgSend(void, objc.sel("release"), .{});
@ -72,7 +72,7 @@ pub const Shaders = struct {
return .{
.library = library,
.cell_pipeline = cell_pipeline,
.cell_text_pipeline = cell_text_pipeline,
.cell_bg_pipeline = cell_bg_pipeline,
.image_pipeline = image_pipeline,
.post_pipelines = post_pipelines,
@ -81,7 +81,7 @@ pub const Shaders = struct {
pub fn deinit(self: *Shaders, alloc: Allocator) void {
// Release our primary shaders
self.cell_pipeline.msgSend(void, objc.sel("release"), .{});
self.cell_text_pipeline.msgSend(void, objc.sel("release"), .{});
self.cell_bg_pipeline.msgSend(void, objc.sel("release"), .{});
self.image_pipeline.msgSend(void, objc.sel("release"), .{});
self.library.msgSend(void, objc.sel("release"), .{});
@ -96,25 +96,6 @@ pub const Shaders = struct {
}
};
/// This is a single parameter for the terminal cell shader.
pub const Cell = extern struct {
mode: Mode,
grid_pos: [2]f32,
glyph_pos: [2]u32 = .{ 0, 0 },
glyph_size: [2]u32 = .{ 0, 0 },
glyph_offset: [2]i32 = .{ 0, 0 },
color: [4]u8,
bg_color: [4]u8,
cell_width: u8,
pub const Mode = enum(u8) {
bg = 1,
fg = 2,
fg_constrained = 3,
fg_color = 7,
};
};
/// Single parameter for the image shader. See shader for field details.
pub const Image = extern struct {
grid_pos: [2]f32,
@ -303,12 +284,31 @@ fn initPostPipeline(
return pipeline_state;
}
/// This is a single parameter for the terminal cell shader.
pub const CellText = extern struct {
mode: Mode,
grid_pos: [2]f32,
glyph_pos: [2]u32 = .{ 0, 0 },
glyph_size: [2]u32 = .{ 0, 0 },
glyph_offset: [2]i32 = .{ 0, 0 },
color: [4]u8,
bg_color: [4]u8,
cell_width: u8,
pub const Mode = enum(u8) {
bg = 1,
fg = 2,
fg_constrained = 3,
fg_color = 7,
};
};
/// Initialize the cell render pipeline for our shader library.
fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
fn initCellTextPipeline(device: objc.Object, library: objc.Object) !objc.Object {
// Get our vertex and fragment functions
const func_vert = func_vert: {
const str = try macos.foundation.String.createWithBytes(
"uber_vertex",
"cell_text_vertex",
.utf8,
false,
);
@ -319,7 +319,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
};
const func_frag = func_frag: {
const str = try macos.foundation.String.createWithBytes(
"uber_fragment",
"cell_text_fragment",
.utf8,
false,
);
@ -353,7 +353,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar));
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "mode")));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "mode")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
@ -364,7 +364,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.float2));
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "grid_pos")));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "grid_pos")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
@ -375,7 +375,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uint2));
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "glyph_pos")));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "glyph_pos")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
@ -386,7 +386,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uint2));
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "glyph_size")));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "glyph_size")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
@ -397,7 +397,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.int2));
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "glyph_offset")));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "glyph_offset")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
@ -408,7 +408,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar4));
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "color")));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "color")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
@ -419,7 +419,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar4));
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "bg_color")));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "bg_color")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
@ -430,7 +430,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar));
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "cell_width")));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "cell_width")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
@ -445,7 +445,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
// Access each Cell per instance, not per vertex.
layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance));
layout.setProperty("stride", @as(c_ulong, @sizeOf(Cell)));
layout.setProperty("stride", @as(c_ulong, @sizeOf(CellText)));
}
break :vertex_desc desc;
@ -506,8 +506,8 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
pub const CellBg = extern struct {
mode: Mode,
grid_pos: [2]f32,
cell_width: u8,
color: [4]u8,
cell_width: u8,
pub const Mode = enum(u8) {
rgb = 1,
@ -575,7 +575,7 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object {
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.float2));
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "grid_pos")));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellBg, "grid_pos")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
@ -586,7 +586,7 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object {
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar));
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "cell_width")));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellBg, "cell_width")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
@ -597,7 +597,7 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object {
);
attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar4));
attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "color")));
attr.setProperty("offset", @as(c_ulong, @offsetOf(CellBg, "color")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
@ -612,7 +612,7 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object {
// Access each Cell per instance, not per vertex.
layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance));
layout.setProperty("stride", @as(c_ulong, @sizeOf(Cell)));
layout.setProperty("stride", @as(c_ulong, @sizeOf(CellBg)));
}
break :vertex_desc desc;
@ -840,11 +840,20 @@ fn checkError(err_: ?*anyopaque) !void {
// on macOS 12 or Apple Silicon macOS 13.
//
// To be safe, we put this test in here.
test "Cell offsets" {
test "CellText offsets" {
const testing = std.testing;
const alignment = @alignOf(Cell);
inline for (@typeInfo(Cell).Struct.fields) |field| {
const offset = @offsetOf(Cell, field.name);
const alignment = @alignOf(CellText);
inline for (@typeInfo(CellText).Struct.fields) |field| {
const offset = @offsetOf(CellText, field.name);
try testing.expectEqual(0, @mod(offset, alignment));
}
}
test "CellBg offsets" {
const testing = std.testing;
const alignment = @alignOf(CellBg);
inline for (@typeInfo(CellBg).Struct.fields) |field| {
const offset = @offsetOf(CellBg, field.name);
try testing.expectEqual(0, @mod(offset, alignment));
}
}

View File

@ -1,58 +1,11 @@
using namespace metal;
// The possible modes that a shader can take.
enum Mode : uint8_t {
MODE_BG = 1u,
MODE_FG = 2u,
MODE_FG_CONSTRAINED = 3u,
MODE_FG_COLOR = 7u,
};
struct Uniforms {
float4x4 projection_matrix;
float2 cell_size;
float min_contrast;
};
struct VertexIn {
// The mode for this cell.
uint8_t mode [[attribute(0)]];
// The grid coordinates (x, y) where x < columns and y < rows
float2 grid_pos [[attribute(1)]];
// The width of the cell in cells (i.e. 2 for double-wide).
uint8_t cell_width [[attribute(6)]];
// The color. For BG modes, this is the bg color, for FG modes this is
// the text color. For styles, this is the color of the style.
uchar4 color [[attribute(5)]];
// The fields below are present only when rendering text (fg mode)
// The background color of the cell. This is used to determine if
// we need to render the text with a different color to ensure
// contrast.
uchar4 bg_color [[attribute(7)]];
// The position of the glyph in the texture (x,y)
uint2 glyph_pos [[attribute(2)]];
// The size of the glyph in the texture (w,h)
uint2 glyph_size [[attribute(3)]];
// The left and top bearings for the glyph (x,y)
int2 glyph_offset [[attribute(4)]];
};
struct VertexOut {
float4 position [[position]];
float2 cell_size;
uint8_t mode;
float4 color;
float2 tex_coord;
};
//-------------------------------------------------------------------
// Color Functions
//-------------------------------------------------------------------
@ -102,142 +55,13 @@ float4 contrasted_color(float min, float4 fg, float4 bg) {
return fg;
}
//-------------------------------------------------------------------
// Terminal Grid Cell Shader
//-------------------------------------------------------------------
#pragma mark - Terminal Grid Cell Shader
vertex VertexOut uber_vertex(unsigned int vid [[vertex_id]],
VertexIn input [[stage_in]],
constant Uniforms& uniforms [[buffer(1)]]) {
// Convert the grid x,y into world space x, y by accounting for cell size
float2 cell_pos = uniforms.cell_size * input.grid_pos;
// Scaled cell size for the cell width
float2 cell_size_scaled = uniforms.cell_size;
cell_size_scaled.x = cell_size_scaled.x * input.cell_width;
// Turn the cell position into a vertex point depending on the
// vertex ID. Since we use instanced drawing, we have 4 vertices
// for each corner of the cell. We can use vertex ID to determine
// which one we're looking at. Using this, we can use 1 or 0 to keep
// or discard the value for the vertex.
//
// 0 = top-right
// 1 = bot-right
// 2 = bot-left
// 3 = top-left
float2 position;
position.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f;
position.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f;
VertexOut out;
out.mode = input.mode;
out.cell_size = uniforms.cell_size;
out.color = float4(input.color) / 255.0f;
switch (input.mode) {
case MODE_BG:
// Calculate the final position of our cell in world space.
// We have to add our cell size since our vertices are offset
// one cell up and to the left. (Do the math to verify yourself)
cell_pos = cell_pos + cell_size_scaled * position;
out.position = uniforms.projection_matrix *
float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
break;
case MODE_FG:
case MODE_FG_CONSTRAINED:
case MODE_FG_COLOR: {
float2 glyph_size = float2(input.glyph_size);
float2 glyph_offset = float2(input.glyph_offset);
// The glyph_offset.y is the y bearing, a y value that when added
// to the baseline is the offset (+y is up). Our grid goes down.
// So we flip it with `cell_size.y - glyph_offset.y`.
glyph_offset.y = cell_size_scaled.y - glyph_offset.y;
// If we're constrained then we need to scale the glyph.
// We also always constrain colored glyphs since we should have
// their scaled cell size exactly correct.
if (input.mode == MODE_FG_CONSTRAINED || input.mode == MODE_FG_COLOR) {
if (glyph_size.x > cell_size_scaled.x) {
float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x);
glyph_offset.y += (glyph_size.y - new_y) / 2;
glyph_size.y = new_y;
glyph_size.x = cell_size_scaled.x;
}
}
// Calculate the final position of the cell which uses our glyph size
// and glyph offset to create the correct bounding box for the glyph.
cell_pos = cell_pos + glyph_size * position + glyph_offset;
out.position = uniforms.projection_matrix *
float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
// Calculate the texture coordinate in pixels. This is NOT normalized
// (between 0.0 and 1.0) and must be done in the fragment shader.
out.tex_coord =
float2(input.glyph_pos) + float2(input.glyph_size) * position;
// If we have a minimum contrast, we need to check if we need to
// change the color of the text to ensure it has enough contrast
// with the background.
if (uniforms.min_contrast > 1.0f && input.mode == MODE_FG) {
float4 bg_color = float4(input.bg_color) / 255.0f;
out.color =
contrasted_color(uniforms.min_contrast, out.color, bg_color);
}
break;
}
}
return out;
}
fragment float4 uber_fragment(VertexOut in [[stage_in]],
texture2d<float> textureGreyscale [[texture(0)]],
texture2d<float> textureColor [[texture(1)]]) {
constexpr sampler textureSampler(address::clamp_to_edge, filter::linear);
switch (in.mode) {
case MODE_BG:
return in.color;
case MODE_FG_CONSTRAINED:
case MODE_FG: {
// Normalize the texture coordinates to [0,1]
float2 size =
float2(textureGreyscale.get_width(), textureGreyscale.get_height());
float2 coord = in.tex_coord / size;
// We premult the alpha to our whole color since our blend function
// uses One/OneMinusSourceAlpha to avoid blurry edges.
// We first premult our given color.
float4 premult = float4(in.color.rgb * in.color.a, in.color.a);
// Then premult the texture color
float a = textureGreyscale.sample(textureSampler, coord).r;
premult = premult * a;
return premult;
}
case MODE_FG_COLOR: {
// Normalize the texture coordinates to [0,1]
float2 size = float2(textureColor.get_width(), textureColor.get_height());
float2 coord = in.tex_coord / size;
return textureColor.sample(textureSampler, coord);
}
}
}
//-------------------------------------------------------------------
// Cell Background Shader
//-------------------------------------------------------------------
#pragma mark - Cell BG Shader
// The possible modes that a cell bg entry can take.
enum CellbgMode : uint8_t {
enum CellBgMode : uint8_t {
MODE_RGB = 1u,
};
@ -248,12 +72,12 @@ struct CellBgVertexIn {
// The grid coordinates (x, y) where x < columns and y < rows
float2 grid_pos [[attribute(1)]];
// The width of the cell in cells (i.e. 2 for double-wide).
uint8_t cell_width [[attribute(2)]];
// The color. For BG modes, this is the bg color, for FG modes this is
// the text color. For styles, this is the color of the style.
uchar4 color [[attribute(3)]];
// The width of the cell in cells (i.e. 2 for double-wide).
uint8_t cell_width [[attribute(2)]];
};
struct CellBgVertexOut {
@ -303,6 +127,158 @@ fragment float4 cell_bg_fragment(CellBgVertexOut in [[stage_in]]) {
return in.color;
}
//-------------------------------------------------------------------
// Cell Text Shader
//-------------------------------------------------------------------
#pragma mark - Cell Text Shader
// The possible modes that a cell fg entry can take.
enum CellTextMode : uint8_t {
MODE_TEXT = 2u,
MODE_TEXT_CONSTRAINED = 3u,
MODE_TEXT_COLOR = 7u,
};
struct CellTextVertexIn {
// The mode for this cell.
uint8_t mode [[attribute(0)]];
// The grid coordinates (x, y) where x < columns and y < rows
float2 grid_pos [[attribute(1)]];
// The width of the cell in cells (i.e. 2 for double-wide).
uint8_t cell_width [[attribute(6)]];
// The color of the rendered text glyph.
uchar4 color [[attribute(5)]];
// The background color of the cell. This is used to determine if
// we need to render the text with a different color to ensure
// contrast.
uchar4 bg_color [[attribute(7)]];
// The position of the glyph in the texture (x,y)
uint2 glyph_pos [[attribute(2)]];
// The size of the glyph in the texture (w,h)
uint2 glyph_size [[attribute(3)]];
// The left and top bearings for the glyph (x,y)
int2 glyph_offset [[attribute(4)]];
};
struct CellTextVertexOut {
float4 position [[position]];
float2 cell_size;
uint8_t mode;
float4 color;
float2 tex_coord;
};
vertex CellTextVertexOut cell_text_vertex(unsigned int vid [[vertex_id]],
CellTextVertexIn input [[stage_in]],
constant Uniforms& uniforms
[[buffer(1)]]) {
// Convert the grid x,y into world space x, y by accounting for cell size
float2 cell_pos = uniforms.cell_size * input.grid_pos;
// Scaled cell size for the cell width
float2 cell_size_scaled = uniforms.cell_size;
cell_size_scaled.x = cell_size_scaled.x * input.cell_width;
// Turn the cell position into a vertex point depending on the
// vertex ID. Since we use instanced drawing, we have 4 vertices
// for each corner of the cell. We can use vertex ID to determine
// which one we're looking at. Using this, we can use 1 or 0 to keep
// or discard the value for the vertex.
//
// 0 = top-right
// 1 = bot-right
// 2 = bot-left
// 3 = top-left
float2 position;
position.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f;
position.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f;
CellTextVertexOut out;
out.mode = input.mode;
out.cell_size = uniforms.cell_size;
out.color = float4(input.color) / 255.0f;
float2 glyph_size = float2(input.glyph_size);
float2 glyph_offset = float2(input.glyph_offset);
// The glyph_offset.y is the y bearing, a y value that when added
// to the baseline is the offset (+y is up). Our grid goes down.
// So we flip it with `cell_size.y - glyph_offset.y`.
glyph_offset.y = cell_size_scaled.y - glyph_offset.y;
// If we're constrained then we need to scale the glyph.
// We also always constrain colored glyphs since we should have
// their scaled cell size exactly correct.
if (input.mode == MODE_TEXT_CONSTRAINED || input.mode == MODE_TEXT_COLOR) {
if (glyph_size.x > cell_size_scaled.x) {
float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x);
glyph_offset.y += (glyph_size.y - new_y) / 2;
glyph_size.y = new_y;
glyph_size.x = cell_size_scaled.x;
}
}
// Calculate the final position of the cell which uses our glyph size
// and glyph offset to create the correct bounding box for the glyph.
cell_pos = cell_pos + glyph_size * position + glyph_offset;
out.position =
uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
// Calculate the texture coordinate in pixels. This is NOT normalized
// (between 0.0 and 1.0) and must be done in the fragment shader.
out.tex_coord = float2(input.glyph_pos) + float2(input.glyph_size) * position;
// If we have a minimum contrast, we need to check if we need to
// change the color of the text to ensure it has enough contrast
// with the background.
if (uniforms.min_contrast > 1.0f && input.mode == MODE_TEXT) {
float4 bg_color = float4(input.bg_color) / 255.0f;
out.color = contrasted_color(uniforms.min_contrast, out.color, bg_color);
}
return out;
}
fragment float4 cell_text_fragment(CellTextVertexOut in [[stage_in]],
texture2d<float> textureGreyscale
[[texture(0)]],
texture2d<float> textureColor
[[texture(1)]]) {
constexpr sampler textureSampler(address::clamp_to_edge, filter::linear);
switch (in.mode) {
case MODE_TEXT_CONSTRAINED:
case MODE_TEXT: {
// Normalize the texture coordinates to [0,1]
float2 size =
float2(textureGreyscale.get_width(), textureGreyscale.get_height());
float2 coord = in.tex_coord / size;
// We premult the alpha to our whole color since our blend function
// uses One/OneMinusSourceAlpha to avoid blurry edges.
// We first premult our given color.
float4 premult = float4(in.color.rgb * in.color.a, in.color.a);
// Then premult the texture color
float a = textureGreyscale.sample(textureSampler, coord).r;
premult = premult * a;
return premult;
}
case MODE_TEXT_COLOR: {
// Normalize the texture coordinates to [0,1]
float2 size = float2(textureColor.get_width(), textureColor.get_height());
float2 coord = in.tex_coord / size;
return textureColor.sample(textureSampler, coord);
}
}
}
//-------------------------------------------------------------------
// Image Shader
//-------------------------------------------------------------------