render-zig

A 3D rendering engine written in Zig
git clone git://git.christianermann.dev/render-zig
Log | Files | Refs

commit bc9c9cb43fb639a35195dc334ce1ae3a68ecdebc
parent e7a7f0e33865a00e05e3fbafe46e61c69ca2c34c
Author: Christian Ermann <christianermann@gmail.com>
Date:   Thu, 25 Apr 2024 17:27:01 -0400

Clear screen

Diffstat:
Msrc/main.zig | 280+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Asrc/objc_message.zig | 8++++++++
2 files changed, 269 insertions(+), 19 deletions(-)

diff --git a/src/main.zig b/src/main.zig @@ -1,34 +1,276 @@ const std = @import("std"); const glfw = @import("mach_glfw"); const gpu = @import("mach_gpu"); +const builtin = @import("builtin"); -fn errorCallback(error_code: glfw.ErrorCode, description: [:0]const u8) void { +const objc = @import("objc_message.zig"); + +fn glfwErrorCallback(error_code: glfw.ErrorCode, description: [:0]const u8) void { std.log.err("glfw: {}: {s}\n", .{ error_code, description }); } -pub fn main() !void { - glfw.setErrorCallback(errorCallback); - if (!glfw.init(.{})) { - std.log.err("failed to initialize GLFW: {?s}", .{glfw.getErrorString()}); - std.process.exit(1); - } - defer glfw.terminate(); - - const title = "Hello, World!"; - const window = glfw.Window.create(640, 480, title, null, null, .{}) orelse { - const msg = "failed to create GLFW window: {?s}"; - std.log.err(msg, .{glfw.getErrorString()}); - std.process.exit(1); +inline fn gpuErrorCallback(_: void, error_type: gpu.ErrorType, message: [*:0]const u8) void { + switch (error_type) { + .validation => std.log.err("gpu: validation error: {s}\n", .{message}), + .out_of_memory => std.log.err("gpu: out of memory: {s}\n", .{message}), + .device_lost => std.log.err("gpu: device lost: {s}\n", .{message}), + .unknown => std.log.err("gpu: unknown error: {s}\n", .{message}), + else => unreachable, + } + std.process.exit(1); +} + +fn gpuDetectBackendType() !gpu.BackendType { + return .metal; +} + +fn glfwWindowHintsForBackend(backend: gpu.BackendType) glfw.Window.Hints { + return switch (backend) { + else => .{ + .client_api = .no_api, + }, }; - defer window.destroy(); +} - while (!window.shouldClose()) { - window.swapBuffers(); - glfw.pollEvents(); +fn glfwDetectBackendOptions() glfw.BackendOptions { + if (builtin.target.isDarwin()) return .{ .cocoa = true }; + return switch (builtin.target.os.tag) { + .windows => .{ .win32 = true }, + .linux => .{ .x11 = true, .wayland = true }, + else => .{}, + }; +} + +const AppSwapChain = struct { + gpu_swap_chain: ?*gpu.SwapChain, + format: gpu.Texture.Format, + current_descriptor: gpu.SwapChain.Descriptor, + target_descriptor: gpu.SwapChain.Descriptor, +}; + +const App = struct { + window: glfw.Window, + instance: *gpu.Instance, + surface: *gpu.Surface, + adapter: *gpu.Adapter, + device: *gpu.Device, + queue: *gpu.Queue, + swap_chain: *AppSwapChain, + + pub fn init(app: *App, allocator: std.mem.Allocator) !void { + try gpu.Impl.init(allocator, .{}); + + glfw.setErrorCallback(glfwErrorCallback); + if (!glfw.init(.{})) { + const msg = "failed to initialize GLFW: {?s}"; + std.log.err(msg, .{glfw.getErrorString()}); + std.process.exit(1); + } + + const backend_type = try gpuDetectBackendType(); + const hints = glfwWindowHintsForBackend(backend_type); + const title = "Hello, World!"; + app.window = glfw.Window.create(640, 480, title, null, null, hints) orelse { + const msg = "failed to create GLFW window: {?s}"; + std.log.err(msg, .{glfw.getErrorString()}); + std.process.exit(1); + }; + + app.instance = gpu.createInstance(null) orelse { + std.log.err("failed to create GPU instance", .{}); + std.process.exit(1); + }; + + app.surface = try app.createSurfaceForWindow(); + app.adapter = try app.createAdapter(); + + var props = std.mem.zeroes(gpu.Adapter.Properties); + app.adapter.getProperties(&props); + std.debug.print("found {s} backend on {s} adapter: {s}, {s}\n", .{ + props.backend_type.name(), + props.adapter_type.name(), + props.name, + props.driver_description, + }); + + app.device = app.adapter.createDevice(null) orelse { + std.log.err("failed to create GPU device", .{}); + std.process.exit(1); + }; + app.device.setUncapturedErrorCallback({}, gpuErrorCallback); + + app.queue = app.device.getQueue(); + + app.swap_chain = try app.createSwapChain(allocator); + app.window.setUserPointer(app.swap_chain); + } + + pub fn deinit(app: *App) void { + app.window.destroy(); + glfw.terminate(); } + fn createSurfaceForWindow(app: *App) !*gpu.Surface { + const glfw_options = comptime glfwDetectBackendOptions(); + const glfw_native = glfw.Native(glfw_options); + if (glfw_options.cocoa) { + const ns_window = glfw_native.getCocoaWindow(app.window); + const ns_view = msgSend(ns_window, "contentView", .{}, *anyopaque); + + // Create a CAMetalLayer that covers the whole window. + msgSend(ns_view, "setWantsLayer:", .{true}, void); + const layer = msgSend(objc.objc_getClass("CAMetalLayer"), "layer", .{}, ?*anyopaque) orelse { + @panic("failed to create Metal layer"); + }; + msgSend(ns_view, "setLayer:", .{layer}, void); + + // Use retina if the window was created with retina support. + const scale_factor = msgSend(ns_window, "backingScaleFactor", .{}, f64); + msgSend(layer, "setContentsScale:", .{scale_factor}, void); + + return app.instance.createSurface(&gpu.Surface.Descriptor{ + .next_in_chain = .{ + .from_metal_layer = &.{ .layer = layer }, + }, + }); + } + } + + fn createAdapter(app: *App) !*gpu.Adapter { + const RequestAdapterResponse = struct { + status: gpu.RequestAdapterStatus, + adapter: ?*gpu.Adapter, + message: ?[*:0]const u8, + + pub inline fn callback( + context: *@This(), + status: gpu.RequestAdapterStatus, + adapter: ?*gpu.Adapter, + message: ?[*:0]const u8, + ) void { + context.* = @This(){ + .status = status, + .adapter = adapter, + .message = message, + }; + } + }; + + var response: RequestAdapterResponse = undefined; + app.instance.requestAdapter(&gpu.RequestAdapterOptions{ + .compatible_surface = app.surface, + .power_preference = .undefined, + .force_fallback_adapter = .false, + }, &response, RequestAdapterResponse.callback); + if (response.status != .success) { + const msg = "failed to create GPU adapter: {s}\n"; + std.debug.print(msg, .{response.message.?}); + std.process.exit(1); + } + return response.adapter.?; + } + + fn createSwapChain(app: *App, allocator: std.mem.Allocator) !*AppSwapChain { + const format = .bgra8_unorm; + const framebuffer_size = app.window.getFramebufferSize(); + const descriptor = gpu.SwapChain.Descriptor{ + .label = "basic swap chain", + .usage = .{ .render_attachment = true }, + .format = format, + .width = framebuffer_size.width, + .height = framebuffer_size.height, + .present_mode = .fifo, + }; + const swap_chain = try allocator.create(AppSwapChain); + swap_chain.* = .{ + .gpu_swap_chain = null, + .format = format, + .current_descriptor = descriptor, + .target_descriptor = descriptor, + }; + return swap_chain; + } + + fn getCurrentSwapChain(app: *App) *gpu.SwapChain { + const sc = app.window.getUserPointer(AppSwapChain).?; + const not_exists = sc.gpu_swap_chain == null; + const changed = !std.meta.eql(sc.current_descriptor, sc.target_descriptor); + if (not_exists or changed) { + sc.gpu_swap_chain = app.device.createSwapChain(app.surface, &sc.target_descriptor); + sc.current_descriptor = sc.target_descriptor; + } + return sc.gpu_swap_chain.?; + } + + pub fn frame(app: *App) !void { + app.device.tick(); + const swap_chain = app.getCurrentSwapChain(); + defer swap_chain.present(); + + const back_buffer_view = swap_chain.getCurrentTextureView().?; + defer back_buffer_view.release(); + + { + const encoder = app.device.createCommandEncoder(null); + defer encoder.release(); + + const color_attachment = gpu.RenderPassColorAttachment{ + .view = back_buffer_view, + .resolve_target = null, + .clear_value = gpu.Color{ .r = 1, .g = 1, .b = 1, .a = 1 }, + .load_op = .clear, + .store_op = .store, + }; + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{color_attachment}, + }); + + { + const pass = encoder.beginRenderPass(&render_pass_info); + defer pass.release(); + defer pass.end(); + } + + { + var command = encoder.finish(null); + defer command.release(); + + app.queue.submit(&[_]*gpu.CommandBuffer{command}); + } + } + } +}; + +pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); - try gpu.Impl.init(allocator, .{}); + var app: App = undefined; + try app.init(allocator); + defer app.deinit(); + + while (!app.window.shouldClose()) { + glfw.pollEvents(); + try app.frame(); + } +} + +// Borrowed from https://github.com/hexops/mach-gpu +// Borrowed from https://github.com/hazeycode/zig-objcrt +pub fn msgSend(obj: anytype, sel_name: [:0]const u8, args: anytype, comptime ReturnType: type) ReturnType { + const args_meta = @typeInfo(@TypeOf(args)).Struct.fields; + + const FnType = switch (args_meta.len) { + 0 => *const fn (@TypeOf(obj), ?*objc.SEL) callconv(.C) ReturnType, + 1 => *const fn (@TypeOf(obj), ?*objc.SEL, args_meta[0].type) callconv(.C) ReturnType, + 2 => *const fn (@TypeOf(obj), ?*objc.SEL, args_meta[0].type, args_meta[1].type) callconv(.C) ReturnType, + 3 => *const fn (@TypeOf(obj), ?*objc.SEL, args_meta[0].type, args_meta[1].type, args_meta[2].type) callconv(.C) ReturnType, + 4 => *const fn (@TypeOf(obj), ?*objc.SEL, args_meta[0].type, args_meta[1].type, args_meta[2].type, args_meta[3].type) callconv(.C) ReturnType, + else => @compileError("Unsupported number of args"), + }; + + const func = @as(FnType, @ptrCast(&objc.objc_msgSend)); + const sel = objc.sel_getUid(@as([*c]const u8, @ptrCast(sel_name))); + + return @call(.auto, func, .{ obj, sel } ++ args); } diff --git a/src/objc_message.zig b/src/objc_message.zig @@ -0,0 +1,8 @@ +// Borrowed from https://github.com/hexops/mach-gpu + +pub const SEL = opaque {}; +pub const Class = opaque {}; + +pub extern fn sel_getUid(str: [*c]const u8) ?*SEL; +pub extern fn objc_getClass(name: [*c]const u8) ?*Class; +pub extern fn objc_msgSend() void;