From e665fc6741a5610bef204f45bba5aa96e281f7a0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 22 Aug 2023 09:20:30 -0700 Subject: [PATCH] renderer/metal: first pass at an image shader --- src/renderer/Metal.zig | 2 + src/renderer/metal/shaders.zig | 130 +++++++++++++++++++++++++++++++- src/renderer/shaders/cell.metal | 70 +++++++++++++++++ 3 files changed, 201 insertions(+), 1 deletion(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 758f749db..3a6046fb2 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -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. diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index cf1125a51..c57613fd6 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -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( diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index 4e76f264d..678b318e3 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -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 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 image [[ texture(0) ]] +) { + constexpr sampler textureSampler(address::clamp_to_edge, filter::linear); + return image.sample(textureSampler, in.tex_coord); +}