renderer/metal: emit draw calls for images

This commit is contained in:
Mitchell Hashimoto
2023-08-22 10:03:15 -07:00
parent e665fc6741
commit cb70d86c00
3 changed files with 196 additions and 61 deletions

View File

@ -33,6 +33,8 @@ const CellBuffer = mtl_buffer.Buffer(mtl_shaders.Cell);
const ImageBuffer = mtl_buffer.Buffer(mtl_shaders.Image); const ImageBuffer = mtl_buffer.Buffer(mtl_shaders.Image);
const InstanceBuffer = mtl_buffer.Buffer(u16); const InstanceBuffer = mtl_buffer.Buffer(u16);
const ImagePlacementList = std.ArrayListUnmanaged(mtl_image.Placement);
// Get native API access on certain platforms so we can do more customization. // Get native API access on certain platforms so we can do more customization.
const glfwNative = glfw.Native(.{ const glfwNative = glfw.Native(.{
.cocoa = builtin.os.tag == .macos, .cocoa = builtin.os.tag == .macos,
@ -82,6 +84,7 @@ font_shaper: font.Shaper,
/// The images that we may render. /// The images that we may render.
images: ImageMap = .{}, images: ImageMap = .{},
image_placements: ImagePlacementList = .{},
/// Metal state /// Metal state
shaders: Shaders, // Compiled shaders shaders: Shaders, // Compiled shaders
@ -285,6 +288,7 @@ pub fn deinit(self: *Metal) void {
while (it.next()) |kv| kv.value_ptr.deinit(self.alloc); while (it.next()) |kv| kv.value_ptr.deinit(self.alloc);
self.images.deinit(self.alloc); self.images.deinit(self.alloc);
} }
self.image_placements.deinit(self.alloc);
self.buf_cells_bg.deinit(); self.buf_cells_bg.deinit();
self.buf_cells.deinit(); self.buf_cells.deinit();
@ -632,49 +636,151 @@ pub fn render(
); );
defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); defer encoder.msgSend(void, objc.sel("endEncoding"), .{});
// Use our shader pipeline // Terminal grid
encoder.msgSend( {
void, // Use our shader pipeline
objc.sel("setRenderPipelineState:"), encoder.msgSend(
.{self.shaders.cell_pipeline.value}, void,
); objc.sel("setRenderPipelineState:"),
.{self.shaders.cell_pipeline.value},
);
// Set our buffers // Set our buffers
encoder.msgSend( encoder.msgSend(
void, void,
objc.sel("setVertexBytes:length:atIndex:"), objc.sel("setVertexBytes:length:atIndex:"),
.{ .{
@as(*const anyopaque, @ptrCast(&self.uniforms)), @as(*const anyopaque, @ptrCast(&self.uniforms)),
@as(c_ulong, @sizeOf(@TypeOf(self.uniforms))), @as(c_ulong, @sizeOf(@TypeOf(self.uniforms))),
@as(c_ulong, 1), @as(c_ulong, 1),
}, },
); );
encoder.msgSend( encoder.msgSend(
void, void,
objc.sel("setFragmentTexture:atIndex:"), objc.sel("setFragmentTexture:atIndex:"),
.{ .{
self.texture_greyscale.value, self.texture_greyscale.value,
@as(c_ulong, 0), @as(c_ulong, 0),
}, },
); );
encoder.msgSend( encoder.msgSend(
void, void,
objc.sel("setFragmentTexture:atIndex:"), objc.sel("setFragmentTexture:atIndex:"),
.{ .{
self.texture_color.value, self.texture_color.value,
@as(c_ulong, 1), @as(c_ulong, 1),
}, },
); );
// Issue the draw calls for this shader // Issue the draw calls for this shader
try self.drawCells(encoder, &self.buf_cells_bg, self.cells_bg); try self.drawCells(encoder, &self.buf_cells_bg, self.cells_bg);
try self.drawCells(encoder, &self.buf_cells, self.cells); try self.drawCells(encoder, &self.buf_cells, self.cells);
}
// Images
// TODO: these should not go above text
if (self.image_placements.items.len > 0) {
// Use our image shader pipeline
encoder.msgSend(
void,
objc.sel("setRenderPipelineState:"),
.{self.shaders.image_pipeline.value},
);
// Set our uniform, which is the only shared buffer
encoder.msgSend(
void,
objc.sel("setVertexBytes:length:atIndex:"),
.{
@as(*const anyopaque, @ptrCast(&self.uniforms)),
@as(c_ulong, @sizeOf(@TypeOf(self.uniforms))),
@as(c_ulong, 1),
},
);
for (self.image_placements.items) |placement| {
try self.drawImagePlacement(encoder, placement);
}
}
} }
buffer.msgSend(void, objc.sel("presentDrawable:"), .{drawable.value}); buffer.msgSend(void, objc.sel("presentDrawable:"), .{drawable.value});
buffer.msgSend(void, objc.sel("commit"), .{}); buffer.msgSend(void, objc.sel("commit"), .{});
} }
fn drawImagePlacement(
self: *Metal,
encoder: objc.Object,
p: mtl_image.Placement,
) !void {
// Look up the image
const image = self.images.get(p.image_id) orelse {
log.warn("image not found for placement image_id={}", .{p.image_id});
return;
};
// Get the texture
const texture = switch (image) {
.ready => |t| t,
else => {
log.warn("image not ready for placement image_id={}", .{p.image_id});
return;
},
};
// Create our vertex buffer, which is always exactly one item.
// future(mitchellh): we can group rendering multiple instances of a single image
const Buffer = mtl_buffer.Buffer(mtl_shaders.Image);
var buf = try Buffer.initFill(self.device, &.{.{
.grid_pos = .{
@as(f32, @floatFromInt(p.x)),
@as(f32, @floatFromInt(p.y)),
},
}});
defer buf.deinit();
// Set our buffer
encoder.msgSend(
void,
objc.sel("setVertexBuffer:offset:atIndex:"),
.{ buf.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) },
);
// Set our texture
encoder.msgSend(
void,
objc.sel("setVertexTexture:atIndex:"),
.{
texture.value,
@as(c_ulong, 0),
},
);
encoder.msgSend(
void,
objc.sel("setFragmentTexture:atIndex:"),
.{
texture.value,
@as(c_ulong, 0),
},
);
// Draw!
encoder.msgSend(
void,
objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"),
.{
@intFromEnum(mtl.MTLPrimitiveType.triangle),
@as(c_ulong, 6),
@intFromEnum(mtl.MTLIndexType.uint16),
self.buf_instance.buffer.value,
@as(c_ulong, 0),
@as(c_ulong, 1),
},
);
// log.debug("drawImagePlacement: {}", .{p});
}
/// Loads some set of cell data into our buffer and issues a draw call. /// Loads some set of cell data into our buffer and issues a draw call.
/// This expects all the Metal command encoder state to be setup. /// This expects all the Metal command encoder state to be setup.
/// ///
@ -718,6 +824,10 @@ fn prepKittyGraphics(
) !void { ) !void {
defer screen.kitty_images.dirty = false; defer screen.kitty_images.dirty = false;
// We always clear our previous placements no matter what because
// we rebuild them from scratch.
self.image_placements.clearRetainingCapacity();
// Go through our known images and if there are any that are no longer // Go through our known images and if there are any that are no longer
// in use then mark them to be freed. // in use then mark them to be freed.
// //
@ -737,33 +847,42 @@ fn prepKittyGraphics(
while (it.next()) |kv| { while (it.next()) |kv| {
// If we already know about this image then do nothing // If we already know about this image then do nothing
const gop = try self.images.getOrPut(self.alloc, kv.key_ptr.image_id); const gop = try self.images.getOrPut(self.alloc, kv.key_ptr.image_id);
if (gop.found_existing) continue; if (!gop.found_existing) {
// Find the image in storage
const image = screen.kitty_images.imageById(kv.key_ptr.image_id) orelse {
log.warn(
"missing image for placement, ignoring image_id={}",
.{kv.key_ptr.image_id},
);
continue;
};
// Find the image in storage // Copy the data into the pending state.
const image = screen.kitty_images.imageById(kv.key_ptr.image_id) orelse { const data = try self.alloc.dupe(u8, image.data);
log.warn( errdefer self.alloc.free(data);
"missing image for placement, ignoring image_id={}",
.{kv.key_ptr.image_id},
);
continue;
};
// Copy the data into the pending state. // Store it in the map
const data = try self.alloc.dupe(u8, image.data); const p: Image.Pending = .{
errdefer self.alloc.free(data); .width = image.width,
.height = image.height,
.data = data.ptr,
};
// Store it in the map gop.value_ptr.* = switch (image.format) {
const p: Image.Pending = .{ .rgb => .{ .pending_rgb = p },
.width = image.width, .rgba => .{ .pending_rgba = p },
.height = image.height, .png => unreachable, // should be decoded by now
.data = data.ptr, };
}; }
gop.value_ptr.* = switch (image.format) { // Accumulate the placement
.rgb => .{ .pending_rgb = p }, try self.image_placements.append(self.alloc, .{
.rgba => .{ .pending_rgba = p }, .image_id = kv.key_ptr.image_id,
.png => unreachable, // should be decoded by now
}; // TODO
.x = 0,
.y = 0,
});
} }
} }

View File

@ -5,6 +5,17 @@ const objc = @import("objc");
const mtl = @import("api.zig"); const mtl = @import("api.zig");
/// Represents a single image placement on the grid. A placement is a
/// request to render an instance of an image.
pub const Placement = struct {
/// The image being rendered. This MUST be in the image map.
image_id: u32,
/// The grid x/y where this placement is located.
x: u32,
y: u32,
};
/// The map used for storing images. /// The map used for storing images.
pub const ImageMap = std.AutoHashMapUnmanaged(u32, Image); pub const ImageMap = std.AutoHashMapUnmanaged(u32, Image);

View File

@ -204,7 +204,7 @@ struct ImageVertexOut {
vertex ImageVertexOut image_vertex( vertex ImageVertexOut image_vertex(
unsigned int vid [[ vertex_id ]], unsigned int vid [[ vertex_id ]],
ImageVertexIn input [[ stage_in ]], ImageVertexIn input [[ stage_in ]],
texture2d<float> image [[ texture(0) ]], texture2d<uint> image [[ texture(0) ]],
constant Uniforms &uniforms [[ buffer(1) ]] constant Uniforms &uniforms [[ buffer(1) ]]
) { ) {
// The position of our image starts at the top-left of the grid cell. // The position of our image starts at the top-left of the grid cell.
@ -244,8 +244,13 @@ vertex ImageVertexOut image_vertex(
fragment float4 image_fragment( fragment float4 image_fragment(
ImageVertexOut in [[ stage_in ]], ImageVertexOut in [[ stage_in ]],
texture2d<float> image [[ texture(0) ]] texture2d<uint> image [[ texture(0) ]]
) { ) {
constexpr sampler textureSampler(address::clamp_to_edge, filter::linear); constexpr sampler textureSampler(address::clamp_to_edge, filter::linear);
return image.sample(textureSampler, in.tex_coord);
// Ehhhhh our texture is in RGBA8Uint but our color attachment is
// BGRA8Unorm. So we need to convert it. We should really be converting
// our texture to BGRA8Unorm.
uint4 rgba = image.sample(textureSampler, in.tex_coord);
return float4(rgba) / 255.0f;
} }