render-zig

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

main.zig (20114B)


      1 const std = @import("std");
      2 const glfw = @import("mach_glfw");
      3 const gpu = @import("mach_gpu");
      4 const builtin = @import("builtin");
      5 
      6 const RenderPass = @import("render_pass.zig").RenderPass;
      7 const ForwardScreenPass = @import("render_pass.zig").ForwardScreenPass;
      8 const RenderPipeline = @import("render_pipeline.zig").RenderPipeline;
      9 const MeshRenderPipeline = @import("mesh_render_pipeline.zig");
     10 const LineRenderPipeline = @import("line_render_pipeline.zig");
     11 const SkyBoxRenderPipeline = @import("render_pipeline.zig").SkyBoxRenderPipeline;
     12 const CubeMap = @import("cubemap.zig");
     13 const Textures = @import("textures.zig");
     14 const Material = @import("material.zig");
     15 const Mesh = @import("mesh.zig");
     16 const RenderData = @import("render_data.zig");
     17 
     18 const Camera = @import("camera.zig");
     19 const input = @import("input.zig");
     20 const Transform = @import("transform.zig");
     21 
     22 const load_obj = @import("load_obj.zig");
     23 
     24 const objc = @import("objc_message.zig");
     25 
     26 fn glfwErrorCallback(error_code: glfw.ErrorCode, description: [:0]const u8) void {
     27     std.log.err("glfw: {}: {s}\n", .{ error_code, description });
     28 }
     29 
     30 inline fn gpuErrorCallback(_: void, error_type: gpu.ErrorType, message: [*:0]const u8) void {
     31     switch (error_type) {
     32         .validation => std.log.err("gpu: validation error: {s}\n", .{message}),
     33         .out_of_memory => std.log.err("gpu: out of memory: {s}\n", .{message}),
     34         .device_lost => std.log.err("gpu: device lost: {s}\n", .{message}),
     35         .unknown => std.log.err("gpu: unknown error: {s}\n", .{message}),
     36         else => unreachable,
     37     }
     38     std.process.exit(1);
     39 }
     40 
     41 fn gpuDetectBackendType() !gpu.BackendType {
     42     return .metal;
     43 }
     44 
     45 fn glfwWindowHintsForBackend(backend: gpu.BackendType) glfw.Window.Hints {
     46     return switch (backend) {
     47         else => .{
     48             .client_api = .no_api,
     49         },
     50     };
     51 }
     52 
     53 fn glfwDetectBackendOptions() glfw.BackendOptions {
     54     if (builtin.target.isDarwin()) return .{ .cocoa = true };
     55     return switch (builtin.target.os.tag) {
     56         .windows => .{ .win32 = true },
     57         .linux => .{ .x11 = true, .wayland = true },
     58         else => .{},
     59     };
     60 }
     61 
     62 pub const App = struct {
     63     window: glfw.Window,
     64     instance: *gpu.Instance,
     65     surface: *gpu.Surface,
     66     adapter: *gpu.Adapter,
     67     device: *gpu.Device,
     68     queue: *gpu.Queue,
     69     swap_chain: *AppSwapChain,
     70     depth_texture: *gpu.Texture,
     71     depth_texture_view: *gpu.TextureView,
     72     inputs: std.ArrayList(input.UserInput),
     73 
     74     pub fn init(app: *App, allocator: std.mem.Allocator) !void {
     75         try gpu.Impl.init(allocator, .{});
     76 
     77         glfw.setErrorCallback(glfwErrorCallback);
     78         if (!glfw.init(.{})) {
     79             const msg = "failed to initialize GLFW: {?s}";
     80             std.log.err(msg, .{glfw.getErrorString()});
     81             std.process.exit(1);
     82         }
     83 
     84         const backend_type = try gpuDetectBackendType();
     85         const hints = glfwWindowHintsForBackend(backend_type);
     86         const title = "Hello, World!";
     87         app.window = glfw.Window.create(640, 480, title, null, null, hints) orelse {
     88             const msg = "failed to create GLFW window: {?s}";
     89             std.log.err(msg, .{glfw.getErrorString()});
     90             std.process.exit(1);
     91         };
     92 
     93         app.instance = gpu.createInstance(null) orelse {
     94             std.log.err("failed to create GPU instance", .{});
     95             std.process.exit(1);
     96         };
     97 
     98         app.surface = try app.createSurfaceForWindow();
     99         app.adapter = try app.createAdapter();
    100 
    101         var props = std.mem.zeroes(gpu.Adapter.Properties);
    102         app.adapter.getProperties(&props);
    103         std.debug.print("found {s} backend on {s} adapter: {s}, {s}\n", .{
    104             props.backend_type.name(),
    105             props.adapter_type.name(),
    106             props.name,
    107             props.driver_description,
    108         });
    109 
    110         const features = [_]gpu.FeatureName{
    111             gpu.FeatureName.indirect_first_instance,
    112         };
    113         const device_descriptor = gpu.Device.Descriptor{
    114             .required_features_count = 1,
    115             .required_features = &features,
    116             .device_lost_callback = undefined,
    117             .device_lost_userdata = undefined,
    118         };
    119         app.device = app.adapter.createDevice(&device_descriptor) orelse {
    120             std.log.err("failed to create GPU device", .{});
    121             std.process.exit(1);
    122         };
    123         app.device.setUncapturedErrorCallback({}, gpuErrorCallback);
    124 
    125         app.queue = app.device.getQueue();
    126 
    127         app.swap_chain = try app.createSwapChain(allocator);
    128 
    129         app.inputs = std.ArrayList(input.UserInput).init(allocator);
    130         app.window.setKeyCallback(glfwKeyCallback);
    131 
    132         app.window.setUserPointer(app);
    133 
    134         const framebuffer_size = app.window.getFramebufferSize();
    135         app.depth_texture = app.device.createTexture(&gpu.Texture.Descriptor{
    136             .size = gpu.Extent3D{
    137                 .width = framebuffer_size.width,
    138                 .height = framebuffer_size.height,
    139             },
    140             .format = .depth24_plus,
    141             .usage = .{
    142                 .render_attachment = true,
    143             },
    144         });
    145         app.depth_texture_view = app.depth_texture.createView(&gpu.TextureView.Descriptor{
    146             .format = .depth24_plus,
    147             .dimension = .dimension_2d,
    148             .array_layer_count = 1,
    149             .mip_level_count = 1,
    150         });
    151     }
    152 
    153     pub fn deinit(app: *App) void {
    154         app.window.destroy();
    155         glfw.terminate();
    156         app.inputs.deinit();
    157     }
    158 
    159     fn createSurfaceForWindow(app: *App) !*gpu.Surface {
    160         const glfw_options = comptime glfwDetectBackendOptions();
    161         const glfw_native = glfw.Native(glfw_options);
    162         if (glfw_options.cocoa) {
    163             const ns_window = glfw_native.getCocoaWindow(app.window);
    164             const ns_view = msgSend(ns_window, "contentView", .{}, *anyopaque);
    165 
    166             // Create a CAMetalLayer that covers the whole window.
    167             msgSend(ns_view, "setWantsLayer:", .{true}, void);
    168             const layer = msgSend(objc.objc_getClass("CAMetalLayer"), "layer", .{}, ?*anyopaque) orelse {
    169                 @panic("failed to create Metal layer");
    170             };
    171             msgSend(ns_view, "setLayer:", .{layer}, void);
    172 
    173             // Use retina if the window was created with retina support.
    174             const scale_factor = msgSend(ns_window, "backingScaleFactor", .{}, f64);
    175             msgSend(layer, "setContentsScale:", .{scale_factor}, void);
    176 
    177             return app.instance.createSurface(&gpu.Surface.Descriptor{
    178                 .next_in_chain = .{
    179                     .from_metal_layer = &.{ .layer = layer },
    180                 },
    181             });
    182         }
    183     }
    184 
    185     fn createAdapter(app: *App) !*gpu.Adapter {
    186         const RequestAdapterResponse = struct {
    187             status: gpu.RequestAdapterStatus,
    188             adapter: ?*gpu.Adapter,
    189             message: ?[*:0]const u8,
    190 
    191             pub inline fn callback(
    192                 context: *@This(),
    193                 status: gpu.RequestAdapterStatus,
    194                 adapter: ?*gpu.Adapter,
    195                 message: ?[*:0]const u8,
    196             ) void {
    197                 context.* = @This(){
    198                     .status = status,
    199                     .adapter = adapter,
    200                     .message = message,
    201                 };
    202             }
    203         };
    204 
    205         var response: RequestAdapterResponse = undefined;
    206         app.instance.requestAdapter(&gpu.RequestAdapterOptions{
    207             .compatible_surface = app.surface,
    208             .power_preference = .undefined,
    209             .force_fallback_adapter = .false,
    210         }, &response, RequestAdapterResponse.callback);
    211         if (response.status != .success) {
    212             const msg = "failed to create GPU adapter: {s}\n";
    213             std.debug.print(msg, .{response.message.?});
    214             std.process.exit(1);
    215         }
    216         return response.adapter.?;
    217     }
    218 
    219     fn createSwapChain(app: *App, allocator: std.mem.Allocator) !*AppSwapChain {
    220         const format = .bgra8_unorm;
    221         const framebuffer_size = app.window.getFramebufferSize();
    222         const descriptor = gpu.SwapChain.Descriptor{
    223             .label = "basic swap chain",
    224             .usage = .{ .render_attachment = true },
    225             .format = format,
    226             .width = framebuffer_size.width,
    227             .height = framebuffer_size.height,
    228             .present_mode = .fifo,
    229         };
    230         const swap_chain = try allocator.create(AppSwapChain);
    231         swap_chain.* = .{
    232             .gpu_swap_chain = null,
    233             .format = format,
    234             .current_descriptor = descriptor,
    235             .target_descriptor = descriptor,
    236         };
    237         return swap_chain;
    238     }
    239 
    240     fn getCurrentSwapChain(app: *App) *gpu.SwapChain {
    241         const sc = app.swap_chain;
    242         const not_exists = sc.gpu_swap_chain == null;
    243         const changed = !std.meta.eql(sc.current_descriptor, sc.target_descriptor);
    244         if (not_exists or changed) {
    245             sc.gpu_swap_chain = app.device.createSwapChain(app.surface, &sc.target_descriptor);
    246             sc.current_descriptor = sc.target_descriptor;
    247         }
    248         return sc.gpu_swap_chain.?;
    249     }
    250 
    251     pub fn frame(app: *App, passes: []const RenderPass) !void {
    252         app.device.tick();
    253         const swap_chain = app.getCurrentSwapChain();
    254         defer swap_chain.present();
    255 
    256         const back_buffer_view = swap_chain.getCurrentTextureView().?;
    257         defer back_buffer_view.release();
    258 
    259         {
    260             const encoder = app.device.createCommandEncoder(null);
    261             defer encoder.release();
    262             for (passes) |pass| {
    263                 pass.frame(encoder, back_buffer_view);
    264             }
    265 
    266             {
    267                 var command = encoder.finish(null);
    268                 defer command.release();
    269 
    270                 app.queue.submit(&[_]*gpu.CommandBuffer{command});
    271             }
    272         }
    273     }
    274 
    275     pub fn keyCallback(app: *App, args: input.KeyCallbackArgs) void {
    276         for (app.inputs.items) |user_input| {
    277             user_input.keyCallback(args);
    278         }
    279     }
    280 };
    281 
    282 const AppSwapChain = struct {
    283     gpu_swap_chain: ?*gpu.SwapChain,
    284     format: gpu.Texture.Format,
    285     current_descriptor: gpu.SwapChain.Descriptor,
    286     target_descriptor: gpu.SwapChain.Descriptor,
    287 };
    288 
    289 pub const f32x3 = @Vector(3, f32);
    290 pub const f32x4 = @Vector(4, f32);
    291 pub const mat4 = [4]@Vector(4, f32);
    292 
    293 pub const UniformData = struct {
    294     pw_camera: f32x3,
    295     view_matrix: mat4,
    296     proj_matrix: mat4,
    297 };
    298 
    299 pub fn Buffer(comptime T: type) type {
    300     return struct {
    301         const Self = @This();
    302         const attrib_size = @sizeOf(T);
    303 
    304         data: *gpu.Buffer,
    305         size: u32,
    306         offset: u32,
    307         attrib_size: u32,
    308 
    309         pub fn init(
    310             max_attribs: u32,
    311             device: *gpu.Device,
    312             usage: gpu.Buffer.UsageFlags,
    313             mapped_at_creation: bool,
    314         ) Self {
    315             std.log.info("buffer init: {} {}", .{ max_attribs, T });
    316             const size = max_attribs * attrib_size;
    317             const descriptor = gpu.Buffer.Descriptor{
    318                 .size = size,
    319                 .usage = usage,
    320                 .mapped_at_creation = gpu.Bool32.from(mapped_at_creation),
    321             };
    322             const data = device.createBuffer(&descriptor);
    323             return .{
    324                 .data = data,
    325                 .size = size,
    326                 .offset = 0,
    327                 .attrib_size = attrib_size,
    328             };
    329         }
    330 
    331         pub fn alloc(self: *Self, n_attribs: u32) !u32 {
    332             const size = n_attribs * attrib_size;
    333             const offset_start = self.offset;
    334             const offset_after = self.offset + size;
    335             std.log.info("buffer alloc: {} {}", .{ n_attribs, T });
    336             if (offset_after > self.size) {
    337                 std.log.err("buffer out of memory", .{});
    338                 return error.BufferEmpty;
    339             }
    340             self.offset = offset_after;
    341             return offset_start / attrib_size;
    342         }
    343 
    344         pub fn write(
    345             self: *Self,
    346             data_slice: anytype,
    347             queue: *gpu.Queue,
    348             offset: u32,
    349         ) void {
    350             queue.writeBuffer(self.data, offset * attrib_size, data_slice);
    351         }
    352 
    353         pub fn allocWrite(
    354             self: *Self,
    355             data_slice: anytype,
    356             queue: *gpu.Queue,
    357         ) !u32 {
    358             const offset = try self.alloc(@intCast(data_slice.len));
    359             queue.writeBuffer(self.data, offset * attrib_size, data_slice);
    360             return offset;
    361         }
    362 
    363         pub fn count(self: *const Self) u32 {
    364             return self.offset / attrib_size;
    365         }
    366     };
    367 }
    368 
    369 pub fn main() !void {
    370     var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    371     const allocator = gpa.allocator();
    372 
    373     var app: App = undefined;
    374     try app.init(allocator);
    375     defer app.deinit();
    376 
    377     var textures = Textures.init(allocator, app.device);
    378 
    379     //const images = .{
    380     //    .pos_x = "cube/cube-pos-x.png",
    381     //    .neg_x = "cube/cube-neg-x.png",
    382     //    .pos_y = "cube/cube-pos-y.png",
    383     //    .neg_y = "cube/cube-neg-y.png",
    384     //    .pos_z = "cube/cube-pos-z.png",
    385     //    .neg_z = "cube/cube-neg-z.png",
    386     //};
    387     const images = .{
    388         .pos_x = "alps_field_4k/px.png",
    389         .neg_x = "alps_field_4k/nx.png",
    390         .pos_y = "alps_field_4k/py.png",
    391         .neg_y = "alps_field_4k/ny.png",
    392         .pos_z = "alps_field_4k/pz.png",
    393         .neg_z = "alps_field_4k/nz.png",
    394     };
    395     const cube_map = try CubeMap.init(images, allocator, &app);
    396 
    397     var camera = Camera{};
    398     camera.logParams();
    399 
    400     var render_data = RenderData.init(
    401         .{
    402             .device = app.device,
    403             .queue = app.queue,
    404             .n_instances = 2000,
    405             .n_objects = 500,
    406             .n_vertices = 60000,
    407             .n_indices = 60000,
    408         },
    409         allocator,
    410     );
    411 
    412     var mesh = try load_obj.loadFile(.{
    413         .allocator = allocator,
    414         .path = "Cottage_Clean/cottage.obj",
    415     });
    416 
    417     var bunny_mesh = try load_obj.loadFile(.{
    418         .allocator = allocator,
    419         .path = "teapot.obj",
    420     });
    421 
    422     const optimizeMesh = @import("forsyth_optimize.zig").optimize;
    423     try optimizeMesh(allocator, &mesh, 32, .{});
    424     try optimizeMesh(allocator, &bunny_mesh, 32, .{});
    425 
    426     const buildClusters = @import("meshlets.zig").buildClusters;
    427     const cluster_mesh = try buildClusters(allocator, &mesh, 64, 126);
    428     const cbunny_mesh = try buildClusters(allocator, &bunny_mesh, 64, 126);
    429     std.log.info("num clusters: {}", .{cluster_mesh.descriptors.?.len});
    430 
    431     const generateUVSphere = @import("uvsphere.zig").generateUVSphere;
    432     const sphere_mesh = try generateUVSphere(16, 16, allocator);
    433 
    434     const generateCube = @import("cube.zig").generateCube;
    435     const cube_mesh = try generateCube(1, allocator);
    436 
    437     const generateCubeSphere = @import("cubesphere.zig").generateCubeSphere;
    438     const cubesphere_mesh = try generateCubeSphere(16, allocator);
    439 
    440     var rp = try MeshRenderPipeline.init(
    441         &app,
    442         .{
    443             .positions = &render_data.positions,
    444             .normals = &render_data.normals,
    445             .tex_coords = &render_data.tex_coords,
    446             .tangents = &render_data.tangents,
    447             .bitangents = &render_data.bitangents,
    448             .indices = &render_data.indices,
    449             .instance_data = &render_data.instance_data,
    450             .object_data = &render_data.object_data,
    451             .indirect = &render_data.indirect_mesh,
    452         },
    453         &textures,
    454         "shaders/mesh_clusters.wgsl",
    455     );
    456 
    457     var lrp = try LineRenderPipeline.init(
    458         &app,
    459         .{
    460             .positions = &render_data.positions,
    461             .indices = &render_data.indices,
    462             .object_data = &render_data.object_data,
    463             .indirect = &render_data.indirect_line,
    464         },
    465         "shaders/lines.wgsl",
    466     );
    467 
    468     var transform_1 = Transform{};
    469     var transform_2 = Transform{};
    470     var transform_3 = Transform{ .offset = .{ 3, 3, 3 } };
    471     var transform_4 = Transform{ .offset = .{ -3, -3, 0 } };
    472     var transform_5 = Transform{ .offset = .{ 3, -3, 1 } };
    473 
    474     var render_mesh = try render_data.addMesh(
    475         &transform_1,
    476         &cluster_mesh,
    477         allocator,
    478     );
    479 
    480     _ = try render_data.addMesh(
    481         &transform_2,
    482         &cbunny_mesh,
    483         allocator,
    484     );
    485 
    486     _ = try render_data.addMesh(
    487         &transform_3,
    488         &sphere_mesh,
    489         allocator,
    490     );
    491 
    492     _ = try render_data.addMesh(
    493         &transform_4,
    494         &cube_mesh,
    495         allocator,
    496     );
    497 
    498     _ = try render_data.addMesh(
    499         &transform_5,
    500         &cubesphere_mesh,
    501         allocator,
    502     );
    503 
    504     const corners = mesh.bboxVertices();
    505     const bbox_indices = Mesh.bboxIndices();
    506 
    507     var lines = try render_data.addLines(&corners, &bbox_indices, &transform_1);
    508 
    509     const material = try Material.init(
    510         &textures,
    511         "Cottage_Clean/Cottage_Clean_Base_Color_fixed.png",
    512         "Cottage_Clean/Cottage_Clean_Normal_fixed.png",
    513         "Cottage_Clean/Cottage_Clean_Roughness_fixed.png",
    514         "Cottage_Clean/Cottage_Clean_Metallic_fixed.png",
    515         app.queue,
    516     );
    517 
    518     render_mesh.setMaterial(&material);
    519     render_data.updateObjects();
    520 
    521     var skybox = SkyBoxRenderPipeline.init(&app, cube_map);
    522 
    523     var fp = ForwardScreenPass.init(.{
    524         .depth = app.depth_texture_view,
    525         .pipelines = &.{ rp.pipeline(), lrp.pipeline(), skybox.pipeline() },
    526     });
    527 
    528     const passes = [_]RenderPass{fp.renderPass()};
    529 
    530     try app.inputs.append(camera.input.userInput());
    531 
    532     var uniform_data = UniformData{
    533         .pw_camera = undefined,
    534         .view_matrix = undefined,
    535         .proj_matrix = undefined,
    536     };
    537 
    538     var skybox_uniform = SkyBoxRenderPipeline.UniformData{
    539         .inv_view = undefined,
    540         .inv_proj = undefined,
    541     };
    542 
    543     while (!app.window.shouldClose()) {
    544         glfw.pollEvents();
    545 
    546         camera.update(0.01);
    547         camera.view(&uniform_data.view_matrix);
    548         camera.proj(&uniform_data.proj_matrix);
    549         uniform_data.pw_camera = camera.position;
    550         app.queue.writeBuffer(rp.uniform_buffer, 0, &[1]UniformData{uniform_data});
    551         app.queue.writeBuffer(lrp.uniform_buffer, 0, &[1]UniformData{uniform_data});
    552 
    553         camera.invView(&skybox_uniform.inv_view);
    554         camera.invProj(&skybox_uniform.inv_proj);
    555         app.queue.writeBuffer(skybox.uniform_buffer, 0, &[1]SkyBoxRenderPipeline.UniformData{skybox_uniform});
    556 
    557         transform_1.translate(.{ 0.01, 0, 0 });
    558         render_mesh.dirty = true;
    559         lines.dirty = true;
    560 
    561         render_data.updateObjects();
    562 
    563         try app.frame(&passes);
    564     }
    565 }
    566 
    567 // Borrowed from https://github.com/hexops/mach-gpu
    568 // Borrowed from https://github.com/hazeycode/zig-objcrt
    569 pub fn msgSend(obj: anytype, sel_name: [:0]const u8, args: anytype, comptime ReturnType: type) ReturnType {
    570     const args_meta = @typeInfo(@TypeOf(args)).Struct.fields;
    571 
    572     const FnType = switch (args_meta.len) {
    573         0 => *const fn (@TypeOf(obj), ?*objc.SEL) callconv(.C) ReturnType,
    574         1 => *const fn (@TypeOf(obj), ?*objc.SEL, args_meta[0].type) callconv(.C) ReturnType,
    575         2 => *const fn (@TypeOf(obj), ?*objc.SEL, args_meta[0].type, args_meta[1].type) callconv(.C) ReturnType,
    576         3 => *const fn (@TypeOf(obj), ?*objc.SEL, args_meta[0].type, args_meta[1].type, args_meta[2].type) callconv(.C) ReturnType,
    577         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,
    578         else => @compileError("Unsupported number of args"),
    579     };
    580 
    581     const func = @as(FnType, @ptrCast(&objc.objc_msgSend));
    582     const sel = objc.sel_getUid(@as([*c]const u8, @ptrCast(sel_name)));
    583 
    584     return @call(.auto, func, .{ obj, sel } ++ args);
    585 }
    586 
    587 fn glfwKeyCallback(
    588     window: glfw.Window,
    589     key: glfw.Key,
    590     scancode: i32,
    591     action: glfw.Action,
    592     mods: glfw.Mods,
    593 ) void {
    594     const app = window.getUserPointer(App).?;
    595     app.keyCallback(.{
    596         .window = window,
    597         .key = key,
    598         .scancode = scancode,
    599         .action = action,
    600         .mods = mods,
    601     });
    602 }