metal: populate the greyscale texture, prep ubershader

This commit is contained in:
Mitchell Hashimoto
2022-10-30 10:32:13 -07:00
parent 89610f9b8d
commit a7c1f63ad8
2 changed files with 245 additions and 26 deletions

View File

@ -6,6 +6,7 @@ const builtin = @import("builtin");
const glfw = @import("glfw");
const objc = @import("objc");
const macos = @import("macos");
const Atlas = @import("../Atlas.zig");
const font = @import("../font/main.zig");
const terminal = @import("../terminal/main.zig");
const renderer = @import("../renderer.zig");
@ -51,9 +52,14 @@ swapchain: objc.Object, // CAMetalLayer
buf_cells: objc.Object, // MTLBuffer
buf_instance: objc.Object, // MTLBuffer
pipeline: objc.Object, // MTLRenderPipelineState
texture_greyscale: objc.Object, // MTLTexture
const GPUCell = extern struct {
mode: GPUCellMode,
grid_pos: [2]f32,
glyph_pos: [2]u32 = .{ 0, 0 },
glyph_size: [2]u32 = .{ 0, 0 },
glyph_offset: [2]i32 = .{ 0, 0 },
};
const GPUUniforms = extern struct {
@ -61,6 +67,17 @@ const GPUUniforms = extern struct {
cell_size: [2]f32,
};
const GPUCellMode = enum(u8) {
bg = 1,
fg = 2,
fg_color = 7,
cursor_rect = 3,
cursor_rect_hollow = 4,
cursor_bar = 5,
underline = 6,
strikethrough = 8,
};
/// Returns the hints that we want for this
pub fn windowHints() glfw.Window.Hints {
return .{
@ -145,6 +162,7 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal {
// Initialize our shader (MTLLibrary)
const library = try initLibrary(device, @embedFile("../shaders/cell.metal"));
const pipeline_state = try initPipelineState(device, library);
const texture_greyscale = try initAtlasTexture(device, &font_group.atlas_greyscale);
return Metal{
.alloc = alloc,
@ -167,6 +185,7 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal {
.buf_cells = buf_cells,
.buf_instance = buf_instance,
.pipeline = pipeline_state,
.texture_greyscale = texture_greyscale,
};
}
@ -283,9 +302,15 @@ pub fn render(
// Get our surface (CAMetalDrawable)
const surface = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{});
// Setup our buffer
// Setup our buffers
try self.syncCells();
// If our font atlas changed, sync the texture data
if (self.font_group.atlas_greyscale.modified) {
try syncAtlasTexture(&self.font_group.atlas_greyscale, &self.texture_greyscale);
self.font_group.atlas_greyscale.modified = false;
}
// MTLRenderPassDescriptor
const desc = desc: {
const MTLRenderPassDescriptor = objc.Class.getClass("MTLRenderPassDescriptor").?;
@ -351,6 +376,14 @@ pub fn render(
@as(c_ulong, 1),
},
);
encoder.msgSend(
void,
objc.sel("setFragmentTexture:atIndex:"),
.{
self.texture_greyscale.value,
@as(c_ulong, 0),
},
);
encoder.msgSend(
void,
@ -489,6 +522,7 @@ pub fn updateCell(
if (colors.bg) |rgb| {
_ = rgb;
self.cells.appendAssumeCapacity(.{
.mode = .bg,
.grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) },
// .grid_col = @intCast(u16, x),
// .grid_row = @intCast(u16, y),
@ -516,20 +550,16 @@ pub fn updateCell(
shaper_cell.glyph_index,
@floatToInt(u16, @ceil(self.cell_size.height)),
);
_ = glyph;
self.cells.appendAssumeCapacity(.{
.mode = .fg,
.grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) },
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
.glyph_size = .{ glyph.width, glyph.height },
.glyph_offset = .{ glyph.offset_x, glyph.offset_y },
// .mode = mode,
// .grid_col = @intCast(u16, x),
// .grid_row = @intCast(u16, y),
// .grid_width = cell.widthLegacy(),
// .glyph_x = glyph.atlas_x,
// .glyph_y = glyph.atlas_y,
// .glyph_width = glyph.width,
// .glyph_height = glyph.height,
// .glyph_offset_x = glyph.offset_x,
// .glyph_offset_y = glyph.offset_y,
// .fg_r = colors.fg.r,
// .fg_g = colors.fg.g,
// .fg_b = colors.fg.b,
@ -565,6 +595,34 @@ fn syncCells(self: *Metal) !void {
@memcpy(ptr, @ptrCast([*]const u8, self.cells.items.ptr), req_bytes);
}
/// Sync the atlas data to the given texture. This copies the bytes
/// associated with the atlas to the given texture. If the atlas no longer
/// fits into the texture, the texture will be resized.
fn syncAtlasTexture(atlas: *const Atlas, texture: *objc.Object) !void {
const width = texture.getProperty(c_ulong, "width");
if (atlas.size > width) {
@panic("TODO: reallocate texture");
}
texture.msgSend(
void,
objc.sel("replaceRegion:mipmapLevel:withBytes:bytesPerRow:"),
.{
MTLRegion{
.origin = .{ .x = 0, .y = 0, .z = 0 },
.size = .{
.width = @intCast(c_ulong, atlas.size),
.height = @intCast(c_ulong, atlas.size),
.depth = 1,
},
},
@as(c_ulong, 0),
atlas.data.ptr,
@as(c_ulong, atlas.format.depth() * atlas.size),
},
);
}
/// Initialize the shader library.
fn initLibrary(device: objc.Object, data: []const u8) !objc.Object {
const source = try macos.foundation.String.createWithBytes(
@ -594,7 +652,7 @@ fn initPipelineState(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(
"basic_vertex",
"uber_vertex",
.utf8,
false,
);
@ -605,7 +663,7 @@ fn initPipelineState(device: objc.Object, library: objc.Object) !objc.Object {
};
const func_frag = func_frag: {
const str = try macos.foundation.String.createWithBytes(
"basic_fragment",
"uber_fragment",
.utf8,
false,
);
@ -636,8 +694,52 @@ fn initPipelineState(device: objc.Object, library: objc.Object) !objc.Object {
.{@as(c_ulong, 0)},
);
attr.setProperty("format", @enumToInt(MTLVertexFormat.uchar));
attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "mode")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
{
const attr = attrs.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, 1)},
);
attr.setProperty("format", @enumToInt(MTLVertexFormat.float2));
attr.setProperty("offset", @as(c_ulong, 0));
attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "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", @enumToInt(MTLVertexFormat.uint2));
attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "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", @enumToInt(MTLVertexFormat.uint2));
attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "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", @enumToInt(MTLVertexFormat.int2));
attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "glyph_offset")));
attr.setProperty("bufferIndex", @as(c_ulong, 0));
}
@ -696,6 +798,44 @@ fn initPipelineState(device: objc.Object, library: objc.Object) !objc.Object {
return pipeline_state;
}
/// Initialize a MTLTexture object for the given atlas.
fn initAtlasTexture(device: objc.Object, atlas: *const Atlas) !objc.Object {
// Determine our pixel format
const pixel_format: MTLPixelFormat = switch (atlas.format) {
.greyscale => .r8unorm,
else => @panic("unsupported atlas format for Metal texture"),
};
// Create our descriptor
const desc = init: {
const Class = objc.Class.getClass("MTLTextureDescriptor").?;
const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
break :init id_init;
};
// Set our properties
desc.setProperty("pixelFormat", @enumToInt(pixel_format));
desc.setProperty("width", @intCast(c_ulong, atlas.size));
desc.setProperty("height", @intCast(c_ulong, atlas.size));
// Initialize
const id = device.msgSend(
?*anyopaque,
objc.sel("newTextureWithDescriptor:"),
.{desc},
) orelse return error.MetalFailed;
return objc.Object.fromId(id);
}
/// Deinitialize a metal resource (buffer, texture, etc.) and free the
/// memory associated with it.
fn deinitMTLResource(obj: objc.Object) void {
obj.msgSend(void, objc.sel("setPurgeableState:"), .{@enumToInt(MTLPurgeableState.empty)});
obj.msgSend(void, objc.sel("release"), .{});
}
fn checkError(err_: ?*anyopaque) !void {
if (err_) |err| {
const nserr = objc.Object.fromId(err);
@ -748,6 +888,9 @@ const MTLIndexType = enum(c_ulong) {
/// https://developer.apple.com/documentation/metal/mtlvertexformat?language=objc
const MTLVertexFormat = enum(c_ulong) {
float2 = 29,
int2 = 33,
uint2 = 37,
uchar = 45,
};
/// https://developer.apple.com/documentation/metal/mtlvertexstepfunction?language=objc
@ -757,6 +900,16 @@ const MTLVertexStepFunction = enum(c_ulong) {
per_instance = 2,
};
/// https://developer.apple.com/documentation/metal/mtlpixelformat?language=objc
const MTLPixelFormat = enum(c_ulong) {
r8unorm = 10,
};
/// https://developer.apple.com/documentation/metal/mtlpurgeablestate?language=objc
const MTLPurgeableState = enum(c_ulong) {
empty = 4,
};
/// https://developer.apple.com/documentation/metal/mtlresourceoptions?language=objc
/// (incomplete, we only use this mode so we just hardcode it)
const MTLResourceStorageModeShared: c_ulong = @enumToInt(MTLStorageMode.shared) << 4;
@ -777,6 +930,23 @@ const MTLViewport = extern struct {
zfar: f64,
};
const MTLRegion = extern struct {
origin: MTLOrigin,
size: MTLSize,
};
const MTLOrigin = extern struct {
x: c_ulong,
y: c_ulong,
z: c_ulong,
};
const MTLSize = extern struct {
width: c_ulong,
height: c_ulong,
depth: c_ulong,
};
extern "c" fn MTLCreateSystemDefaultDevice() ?*anyopaque;
extern "c" fn objc_autoreleasePoolPush() ?*anyopaque;
extern "c" fn objc_autoreleasePoolPop(?*anyopaque) void;

View File

@ -1,26 +1,46 @@
using namespace metal;
// The possible modes that a shader can take.
enum Mode : uint8_t {
MODE_BG = 1u,
MODE_FG = 2u,
};
struct Uniforms {
float4x4 projection_matrix;
float2 cell_size;
};
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(0) ]];
float2 grid_pos [[ attribute(1) ]];
// The fields below are present only when rendering text.
// 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) ]];
};
vertex float4 basic_vertex(
struct VertexOut {
float4 position [[ position ]];
};
vertex VertexOut uber_vertex(
unsigned int vid [[ vertex_id ]],
VertexIn input [[ stage_in ]],
constant Uniforms &uniforms [[ buffer(1) ]]
) {
// Where we are in the grid (x, y) where top-left is origin
// float2 grid_coord = float2(5.0f, 0.0f);
float2 grid_coord = input.grid_pos;
// Convert the grid x,y into world space x, y by accounting for cell size
float2 cell_pos = uniforms.cell_size * grid_coord;
float2 cell_pos = uniforms.cell_size * input.grid_pos;
// Turn the cell position into a vertex point depending on the
// vertex ID. Since we use instanced drawing, we have 4 vertices
@ -36,14 +56,43 @@ vertex float4 basic_vertex(
position.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f;
position.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f;
// 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 + uniforms.cell_size * position;
// TODO: scale
float2 cell_size = uniforms.cell_size;
return uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
VertexOut out;
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 + uniforms.cell_size * position;
out.position = uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
break;
case MODE_FG:
float2 glyph_size = float2(input.glyph_size);
float2 glyph_offset = float2(input.glyph_offset);
// TODO: downsampling
// 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.y - glyph_offset.y;
// Calculate the final position of the cell.
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);
break;
}
return out;
}
fragment half4 basic_fragment() {
fragment half4 uber_fragment(
VertexOut in [[ stage_in ]]
) {
return half4(1.0, 0.0, 0.0, 1.0);
}