Qwerasd 371d62a82c renderer: big rework, graphics API abstraction layers, unified logic
This commit is very large, representing about a month of work with many
interdependent changes that don't separate cleanly in to atomic commits.

The main change here is unifying the renderer logic to a single generic
renderer, implemented on top of an abstraction layer over OpenGL/Metal.

I'll write a more complete summary of the changes in the description of
the PR.
2025-06-20 15:18:41 -06:00

138 lines
3.9 KiB
Zig

//! Wrapper for handling render passes.
const Self = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const builtin = @import("builtin");
const objc = @import("objc");
const mtl = @import("api.zig");
const Renderer = @import("../generic.zig").Renderer(Metal);
const Metal = @import("../Metal.zig");
const Target = @import("Target.zig");
const Pipeline = @import("Pipeline.zig");
const RenderPass = @import("RenderPass.zig");
const Buffer = @import("buffer.zig").Buffer;
const Health = @import("../../renderer.zig").Health;
const log = std.log.scoped(.metal);
/// Options for beginning a frame.
pub const Options = struct {
/// MTLCommandQueue
queue: objc.Object,
};
/// MTLCommandBuffer
buffer: objc.Object,
block: CompletionBlock,
/// Begin encoding a frame.
pub fn begin(
opts: Options,
/// Once the frame has been completed, the `frameCompleted` method
/// on the renderer is called with the health status of the frame.
renderer: *Renderer,
/// The target is presented via the provided renderer's API when completed.
target: *Target,
) !Self {
const buffer = opts.queue.msgSend(
objc.Object,
objc.sel("commandBuffer"),
.{},
);
// Create our block to register for completion updates.
// The block is deallocated by the objC runtime on success.
const block = try CompletionBlock.init(
.{
.renderer = renderer,
.target = target,
.sync = false,
},
&bufferCompleted,
);
errdefer block.deinit();
return .{ .buffer = buffer, .block = block };
}
/// This is the block type used for the addCompletedHandler callback.
const CompletionBlock = objc.Block(struct {
renderer: *Renderer,
target: *Target,
sync: bool,
}, .{
objc.c.id, // MTLCommandBuffer
}, void);
fn bufferCompleted(
block: *const CompletionBlock.Context,
buffer_id: objc.c.id,
) callconv(.c) void {
const buffer = objc.Object.fromId(buffer_id);
// Get our command buffer status to pass back to the generic renderer.
const status = buffer.getProperty(mtl.MTLCommandBufferStatus, "status");
const health: Health = switch (status) {
.@"error" => .unhealthy,
else => .healthy,
};
// If the frame is healthy, present it.
if (health == .healthy) {
block.renderer.api.present(
block.target.*,
block.sync,
) catch |err| {
log.err("Failed to present render target: err={}", .{err});
};
}
block.renderer.frameCompleted(health);
}
/// Add a render pass to this frame with the provided attachments.
/// Returns a RenderPass which allows render steps to be added.
pub inline fn renderPass(
self: *const Self,
attachments: []const RenderPass.Options.Attachment,
) RenderPass {
return RenderPass.begin(.{
.attachments = attachments,
.command_buffer = self.buffer,
});
}
/// Complete this frame and present the target.
///
/// If `sync` is true, this will block until the frame is presented.
pub inline fn complete(self: *Self, sync: bool) void {
// If we don't need to complete synchronously,
// we add our block as a completion handler.
//
// It will be deallocated by the objc runtime on success.
if (!sync) {
self.buffer.msgSend(
void,
objc.sel("addCompletedHandler:"),
.{self.block.context},
);
}
self.buffer.msgSend(void, objc.sel("commit"), .{});
// If we need to complete synchronously, we wait until
// the buffer is completed and call the callback directly,
// deiniting the block after we're done.
if (sync) {
self.buffer.msgSend(void, "waitUntilCompleted", .{});
self.block.context.sync = true;
bufferCompleted(self.block.context, self.buffer.value);
self.block.deinit();
}
}