renderer/metal: first pass at an image shader

This commit is contained in:
Mitchell Hashimoto
2023-08-22 09:20:30 -07:00
parent 5229cb93d2
commit e665fc6741
3 changed files with 201 additions and 1 deletions

View File

@ -28,7 +28,9 @@ const mtl_shaders = @import("metal/shaders.zig");
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);
// Get native API access on certain platforms so we can do more customization.

View File

@ -13,6 +13,7 @@ const log = std.log.scoped(.metal);
pub const Shaders = struct {
library: objc.Object,
cell_pipeline: objc.Object,
image_pipeline: objc.Object,
pub fn init(device: objc.Object) !Shaders {
const library = try initLibrary(device);
@ -21,14 +22,19 @@ pub const Shaders = struct {
const cell_pipeline = try initCellPipeline(device, library);
errdefer cell_pipeline.msgSend(void, objc.sel("release"), .{});
const image_pipeline = try initImagePipeline(device, library);
errdefer image_pipeline.msgSend(void, objc.sel("release"), .{});
return .{
.library = library,
.cell_pipeline = cell_pipeline,
.image_pipeline = image_pipeline,
};
}
pub fn deinit(self: *Shaders) void {
self.cell_pipeline.msgSend(void, objc.sel("release"), .{});
self.image_pipeline.msgSend(void, objc.sel("release"), .{});
self.library.msgSend(void, objc.sel("release"), .{});
}
};
@ -51,6 +57,11 @@ pub const Cell = extern struct {
};
};
/// Single parameter for the image shader.
pub const Image = extern struct {
grid_pos: [2]f32,
};
/// The uniforms that are passed to the terminal cell shader.
pub const Uniforms = extern struct {
/// The projection matrix for turning world coordinates to normalized.
@ -117,7 +128,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
break :func_frag objc.Object.fromId(ptr.?);
};
// Create the vertex descriptor. The vertex descriptor describves the
// Create the vertex descriptor. The vertex descriptor describes the
// data layout of the vertex inputs. We use indexed (or "instanced")
// rendering, so this makes it so that each instance gets a single
// Cell as input.
@ -274,6 +285,123 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object {
return pipeline_state;
}
/// Initialize the image render pipeline for our shader library.
fn initImagePipeline(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(
"image_vertex",
.utf8,
false,
);
defer str.release();
const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
break :func_vert objc.Object.fromId(ptr.?);
};
const func_frag = func_frag: {
const str = try macos.foundation.String.createWithBytes(
"image_fragment",
.utf8,
false,
);
defer str.release();
const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
break :func_frag objc.Object.fromId(ptr.?);
};
// Create the vertex descriptor. The vertex descriptor describes the
// data layout of the vertex inputs. We use indexed (or "instanced")
// rendering, so this makes it so that each instance gets a single
// Image as input.
const vertex_desc = vertex_desc: {
const desc = init: {
const Class = objc.Class.getClass("MTLVertexDescriptor").?;
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;
};
// 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));
}
// The layout describes how and when we fetch the next vertex input.
const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts"));
{
const layout = layouts.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, 0)},
);
// Access each Image per instance, not per vertex.
layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance));
layout.setProperty("stride", @as(c_ulong, @sizeOf(Image)));
}
break :vertex_desc desc;
};
// Create our descriptor
const desc = init: {
const Class = objc.Class.getClass("MTLRenderPipelineDescriptor").?;
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("vertexFunction", func_vert);
desc.setProperty("fragmentFunction", func_frag);
desc.setProperty("vertexDescriptor", vertex_desc);
// Set our color attachment
const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments"));
{
const attachment = attachments.msgSend(
objc.Object,
objc.sel("objectAtIndexedSubscript:"),
.{@as(c_ulong, 0)},
);
// Value is MTLPixelFormatBGRA8Unorm
attachment.setProperty("pixelFormat", @as(c_ulong, 80));
// Blending. This is required so that our text we render on top
// of our drawable properly blends into the bg.
attachment.setProperty("blendingEnabled", true);
attachment.setProperty("rgbBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
attachment.setProperty("alphaBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
attachment.setProperty("sourceRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
attachment.setProperty("sourceAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
attachment.setProperty("destinationRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
attachment.setProperty("destinationAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
}
// Make our state
var err: ?*anyopaque = null;
const pipeline_state = device.msgSend(
objc.Object,
objc.sel("newRenderPipelineStateWithDescriptor:error:"),
.{ desc, &err },
);
try checkError(err);
return pipeline_state;
}
fn checkError(err_: ?*anyopaque) !void {
const nserr = objc.Object.fromId(err_ orelse return);
const str = @as(

View File

@ -49,6 +49,11 @@ struct VertexOut {
float2 tex_coord;
};
//-------------------------------------------------------------------
// Terminal Grid Cell Shader
//-------------------------------------------------------------------
#pragma mark - Terminal Grid Cell Shader
vertex VertexOut uber_vertex(
unsigned int vid [[ vertex_id ]],
VertexIn input [[ stage_in ]],
@ -179,3 +184,68 @@ fragment float4 uber_fragment(
return in.color;
}
}
//-------------------------------------------------------------------
// Image Shader
//-------------------------------------------------------------------
#pragma mark - Image Shader
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) ]];
};
struct ImageVertexOut {
float4 position [[ position ]];
float2 tex_coord;
};
vertex ImageVertexOut image_vertex(
unsigned int vid [[ vertex_id ]],
ImageVertexIn input [[ stage_in ]],
texture2d<float> image [[ texture(0) ]],
constant Uniforms &uniforms [[ buffer(1) ]]
) {
// The position of our image starts at the top-left of the grid cell.
float2 image_pos = uniforms.cell_size * input.grid_pos;
// The size of the image in pixels
float2 image_size = float2(image.get_width(), image.get_height());
// Turn the image 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;
ImageVertexOut out;
// Our final position is our image position multiplied by the on/off
// position based on corners above.
image_pos = image_pos + image_size * position;
// Output position is just our cell top-left.
out.position = uniforms.projection_matrix * float4(image_pos.x, image_pos.y, 0.0f, 1.0f);
// Calculate the texture coordinate in pixels and normalize it to [0, 1]
out.tex_coord = position;
return out;
}
fragment float4 image_fragment(
ImageVertexOut in [[ stage_in ]],
texture2d<float> image [[ texture(0) ]]
) {
constexpr sampler textureSampler(address::clamp_to_edge, filter::linear);
return image.sample(textureSampler, in.tex_coord);
}