const Game = @This(); const std = @import("std"); const c = @import("const.zig"); const ctx = @import("AppContext.zig"); const glfw = @import("zglfw"); const math = @import("math.zig"); const media = @import("media"); const shaders = @import("shaders.zig"); const vk = @import("vulkan"); const vm = @import("vecmath"); const worldgen = @import("worldgen.zig"); const Blocks = @import("assets/Blocks.zig"); const Chunk = @import("Chunk.zig"); const Chunks = @import("Chunks.zig"); const CommandBuffer = @import("engine/CommandBuffer.zig"); const Engine = @import("engine/Engine.zig"); const Fonts = @import("engine/Fonts.zig"); const Iterator2 = math.Iterator2; const Materials = @import("engine/Materials.zig"); const Player = @import("Player.zig"); const Skybox = @import("engine/Skybox.zig"); const StagingBuffer = @import("engine/StagingBuffer.zig"); const Swapchain = @import("engine/Swapchain.zig"); const Texture = @import("engine/Texture.zig"); const Textures = @import("engine/Textures.zig"); global_descriptor_set_layout: vk.DescriptorSetLayout, per_batch_descriptor_set_layout: vk.DescriptorSetLayout, descriptor_pool: vk.DescriptorPool, global_descriptor_set: vk.DescriptorSet, pipeline_layout: vk.PipelineLayout, pipeline: vk.Pipeline, vertex_buffer: shaders.VertexBuffer, index_buffer: shaders.IndexBuffer, global_uniforms: shaders.GlobalUniformsBuffer, global_uniforms_staging_buffer: StagingBuffer, global_uniforms_transfer_semaphores: []vk.Semaphore, point_lights: shaders.PointLightBuffer, directional_lights: shaders.DirectionalLightBuffer, sampler: vk.Sampler, deferred_command_buffers: std.ArrayList(CommandBuffer), blocks: Blocks, chunks: Chunks, skybox: Skybox, player: Player, font: Fonts.Id, const max_textures = 1024; const max_point_lights = 1024; const max_directional_lights = 4; const chunk_descriptor_pool = 1024; const camera_near_plane = 0.1; const point_lights_data = [_]shaders.PointLight{}; const directional_lights_data = [_]shaders.DirectionalLight{ .{ .directionWS = .unit_nz, .color = .init(0.3, 0.3, 0.3), }, }; pub fn init() !Game { const allocator_general = ctx.allocator_general; const allocator_frame = ctx.allocator_frame; const engine = ctx.engine; const swapchain = ctx.swapchain; const textures = ctx.textures; const materials = ctx.materials; const fonts = ctx.fonts; const gui = ctx.gui; var blocks = try Blocks.init(); errdefer blocks.deinit(); // JANK HACK When this line is removed, capturing a frame with RenderDoc // will crash the game with segfault reading address 0x140 (presumably // within librenderdoc.so). blocks.loadAll(); const sampler = try engine.createSampler(.{ .mag_filter = .linear, .min_filter = .linear, .mipmap_mode = .linear, .address_mode_u = .repeat, .address_mode_v = .repeat, .address_mode_w = .repeat, .mip_lod_bias = 0, .anisotropy_enable = .false, .max_anisotropy = 0, .compare_enable = .false, .compare_op = .always, .min_lod = 0, .max_lod = vk.LOD_CLAMP_NONE, .border_color = .float_transparent_black, .unnormalized_coordinates = .false, }); errdefer engine.destroySampler(sampler); const global_descriptor_set_layout = try engine.createDescriptorSetLayout(.{ .bindings = &.{ .{ .binding = 0, .descriptor_type = .uniform_buffer, .descriptor_count = 1, .stage_flags = .{ .vertex_bit = true, .fragment_bit = true }, }, .{ .binding = 1, .descriptor_type = .storage_buffer, .descriptor_count = 1, .stage_flags = .{ .vertex_bit = true, .fragment_bit = true }, }, .{ .binding = 2, .descriptor_type = .storage_buffer, .descriptor_count = 1, .stage_flags = .{ .vertex_bit = true, .fragment_bit = true }, }, .{ .binding = 3, .descriptor_type = .storage_buffer, .descriptor_count = 1, .stage_flags = .{ .vertex_bit = true, .fragment_bit = true }, }, .{ .binding = 4, .descriptor_type = .sampler, .descriptor_count = 1, .stage_flags = .{ .vertex_bit = true, .fragment_bit = true }, .immutable_samplers = &.{sampler}, }, .{ .binding = 5, .descriptor_type = .sampled_image, .descriptor_count = max_textures, .stage_flags = .{ .vertex_bit = true, .fragment_bit = true }, .flags = .{ .variable_descriptor_count_bit = true }, // TODO Get rid of this AND somehow integrate with Textures module? }, }, }); errdefer engine.destroyDescriptorSetLayout(global_descriptor_set_layout); engine.setObjectName(global_descriptor_set_layout, "DSL Global", .{}); const per_batch_descriptor_set_layout = try engine.createDescriptorSetLayout(.{ .bindings = &.{ .{ .binding = 0, .descriptor_type = .storage_buffer, .descriptor_count = 1, .stage_flags = .{ .vertex_bit = true, .fragment_bit = true }, }, }, }); errdefer engine.destroyDescriptorSetLayout(per_batch_descriptor_set_layout); engine.setObjectName(per_batch_descriptor_set_layout, "DSL PerBatch", .{}); const pipeline_layout = try engine.createPipelineLayout(.{ .set_layouts = &.{ global_descriptor_set_layout, per_batch_descriptor_set_layout, }, }); errdefer engine.destroyPipelineLayout(pipeline_layout); engine.setObjectName(pipeline_layout, "PL Main", .{}); const shader = try engine.createShaderModule(.{ .code = &shaders.main_spv }); defer engine.destroyShaderModule(shader); engine.setObjectName(shader, "SM main", .{}); var vertex_buffer = try shaders.VertexBuffer.init(.{ .usage = .vertex, .target_queue = .graphics, .array_capacity = 4, .name = "QuadVB", }); errdefer vertex_buffer.deinit(); try vertex_buffer.write(.{ .elements = &.{ .init( .init(0, 0, 0), .init(0, 1), .init(0, 0, 1), .init(1, 0, 0, -1), ), .init( .init(1, 0, 0), .init(1, 1), .init(0, 0, 1), .init(1, 0, 0, -1), ), .init( .init(0, 1, 0), .init(0, 0), .init(0, 0, 1), .init(1, 0, 0, -1), ), .init( .init(1, 1, 0), .init(1, 0), .init(0, 0, 1), .init(1, 0, 0, -1), ), }, }); var index_buffer = try shaders.IndexBuffer.init(.{ .usage = .index, .target_queue = .graphics, .array_capacity = 6, .name = "QuadIB", }); errdefer index_buffer.deinit(); try index_buffer.write(.{ .elements = &.{ 0, 1, 2, 2, 1, 3 }, }); var global_uniforms = try shaders.GlobalUniformsBuffer.init(.{ .usage = .uniform, .target_queue = .graphics, .name = "GlobalUniforms", }); errdefer global_uniforms.deinit(); var global_uniforms_staging_buffer = try StagingBuffer.init(.{ .capacity = @sizeOf(shaders.GlobalUniforms), .target_queue = .graphics, }); errdefer global_uniforms_staging_buffer.deinit(); const global_uniforms_transfer_semaphores = blk: { var semaphores: std.ArrayList(vk.Semaphore) = try .initCapacity(allocator_general, swapchain.swapchain_images.len); errdefer semaphores.deinit(allocator_general); errdefer for (semaphores.items) |x| engine.destroySemaphore(x); for (0..swapchain.swapchain_images.len) |i| { const semaphore = try engine.createSemaphore(); engine.setObjectName(semaphore, "S Transfer[{d}]", .{i}); semaphores.appendAssumeCapacity(semaphore); } break :blk try semaphores.toOwnedSlice(allocator_general); }; errdefer { for (global_uniforms_transfer_semaphores) |semaphore| { engine.destroySemaphore(semaphore); } allocator_general.free(global_uniforms_transfer_semaphores); } var point_lights = try shaders.PointLightBuffer.init(.{ .usage = .storage, .target_queue = .graphics, .array_capacity = max_point_lights, .name = "PointLights", }); errdefer point_lights.deinit(); var directional_lights = try shaders.DirectionalLightBuffer.init(.{ .usage = .storage, .target_queue = .graphics, .array_capacity = max_directional_lights, .name = "DirectionalLights", }); errdefer directional_lights.deinit(); const pipeline = try engine.createGraphicsPipeline(.{ .stages = &.{ .{ .stage = .{ .vertex_bit = true }, .module = shader, .name = "vertexMain", }, .{ .stage = .{ .fragment_bit = true }, .module = shader, .name = "fragmentMain", }, }, .vertex_input_state = .{ .vertex_binding_descriptions = &.{ .{ .binding = 0, .stride = @sizeOf(shaders.Vertex), .input_rate = .vertex, }, }, .vertex_attribute_descriptions = &.{ .{ .location = 0, .binding = 0, .format = .r32g32b32_sfloat, .offset = @offsetOf(shaders.Vertex, "positionOS"), }, .{ .location = 1, .binding = 0, .format = .r16g16_unorm, .offset = @offsetOf(shaders.Vertex, "texCoord"), }, .{ .location = 2, .binding = 0, .format = .r8g8b8_snorm, .offset = @offsetOf(shaders.Vertex, "normalOS"), }, .{ .location = 3, .binding = 0, .format = .r8g8b8a8_snorm, .offset = @offsetOf(shaders.Vertex, "tangentOS"), }, }, }, .input_assembly_state = .{ .topology = .triangle_list, .primitive_restart_enable = .false, }, .viewport_state = .{ .viewports = &.{undefined}, // dynamic .scissors = &.{undefined}, // dynamic }, .rasterization_state = .{ .depth_clamp_enable = .false, .rasterizer_discard_enable = .false, .polygon_mode = .fill, .cull_mode = .{ .back_bit = true }, .front_face = .counter_clockwise, .depth_bias_enable = .false, .depth_bias_constant_factor = 0, .depth_bias_clamp = 0, .depth_bias_slope_factor = 0, .line_width = 1, }, .multisample_state = .{ .rasterization_samples = .{ .@"1_bit" = true }, .sample_shading_enable = .false, .min_sample_shading = 1, .alpha_to_coverage_enable = .false, .alpha_to_one_enable = .false, }, .depth_stencil_state = .{ .depth_test_enable = .true, .depth_write_enable = .true, .depth_compare_op = .greater, .depth_bounds_test_enable = .false, .stencil_test_enable = .false, .front = .{ .fail_op = .keep, .pass_op = .keep, .depth_fail_op = .keep, .compare_op = .never, .compare_mask = 0, .write_mask = 0, .reference = 0, }, .back = .{ .fail_op = .keep, .pass_op = .keep, .depth_fail_op = .keep, .compare_op = .never, .compare_mask = 0, .write_mask = 0, .reference = 0, }, .min_depth_bounds = 0, .max_depth_bounds = 1, }, .color_blend_state = .{ .logic_op_enable = .false, .logic_op = .copy, .attachments = &.{ .{ .blend_enable = .false, .src_color_blend_factor = .one, .dst_color_blend_factor = .zero, .color_blend_op = .add, .src_alpha_blend_factor = .one, .dst_alpha_blend_factor = .zero, .alpha_blend_op = .add, .color_write_mask = .{ .r_bit = true, .g_bit = true, .b_bit = true, .a_bit = true, }, }, }, .blend_constants = .{ 0, 0, 0, 0 }, }, .dynamic_state = .{ .dynamic_states = &.{ .viewport, .scissor }, }, .layout = pipeline_layout, .view_mask = 0, .color_attachment_formats = &.{swapchain.params.surface_format.format}, .depth_attachment_format = Texture.Usage.depth.vkFormat(), .stencil_attachment_format = .undefined, }); errdefer engine.destroyPipeline(pipeline); engine.setObjectName(pipeline, "P Main", .{}); const descriptor_pool = try engine.createDescriptorPool(.{ .flags = .{ .free_descriptor_set_bit = true, }, .max_sets = 1 + chunk_descriptor_pool, .pool_sizes = &.{ .{ .type = .sampler, .descriptor_count = 1, }, .{ .type = .sampled_image, .descriptor_count = max_textures, }, .{ .type = .uniform_buffer, .descriptor_count = 1, }, .{ .type = .storage_buffer, .descriptor_count = 3 + chunk_descriptor_pool, }, }, }); errdefer engine.destroyDescriptorPool(descriptor_pool); engine.setObjectName(descriptor_pool, "DP Main", .{}); const global_descriptor_set = try engine.allocateDescriptorSet(.{ .descriptor_pool = descriptor_pool, .set_layout = global_descriptor_set_layout, .variable_descriptor_count = @intCast(textures.array.items.len), }); engine.setObjectName(global_descriptor_set, "DS Global", .{}); const block_grass = try blocks.getOrLoad("Grass.json"); const block_dirt = try blocks.getOrLoad("Dirt.json"); const block_stone = try blocks.getOrLoad("Stone.json"); const block_bedrock = try blocks.getOrLoad("Bedrock.json"); const font_id = try fonts.load("FiraMono-Regular.ttf", .{ .codepoint_ranges = &.{ .{ .start = 32, .end = 127 }, }, .name = "Fira Mono", }); // VOLATILE Load all assets before this point const descriptor_images = try allocator_frame.alloc(vk.DescriptorImageInfo, textures.array.items.len); for (textures.array.items, descriptor_images) |texture, *info| { info.* = .{ .sampler = .null_handle, .image_view = texture.image_view, .image_layout = .shader_read_only_optimal, }; } try engine.updateDescriptorSets(.{ .writes = &.{ .{ .dst_set = global_descriptor_set, .dst_binding = 0, .dst_array_element = 0, .descriptor_type = .uniform_buffer, .descriptor_infos = .{ .buffer = &.{ .{ .buffer = global_uniforms.buffer, .offset = 0, .range = vk.WHOLE_SIZE, }, }, }, }, .{ .dst_set = global_descriptor_set, .dst_binding = 1, .dst_array_element = 0, .descriptor_type = .storage_buffer, .descriptor_infos = .{ .buffer = &.{ .{ .buffer = point_lights.buffer, .offset = 0, .range = vk.WHOLE_SIZE, }, }, }, }, .{ .dst_set = global_descriptor_set, .dst_binding = 2, .dst_array_element = 0, .descriptor_type = .storage_buffer, .descriptor_infos = .{ .buffer = &.{ .{ .buffer = directional_lights.buffer, .offset = 0, .range = vk.WHOLE_SIZE, }, }, }, }, .{ .dst_set = global_descriptor_set, .dst_binding = 3, .dst_array_element = 0, .descriptor_type = .storage_buffer, .descriptor_infos = .{ .buffer = &.{ .{ .buffer = materials.material_buffer.buffer, .offset = 0, .range = vk.WHOLE_SIZE, }, }, }, }, .{ .dst_set = global_descriptor_set, .dst_binding = 5, .dst_array_element = 0, .descriptor_type = .sampled_image, .descriptor_infos = .{ .image = descriptor_images }, }, }, }); var chunks: std.AutoHashMapUnmanaged([3]i16, Chunk) = .empty; errdefer { var it = chunks.valueIterator(); while (it.next()) |chunk| { chunk.deinit(descriptor_pool); } chunks.deinit(allocator_general); } const world_seed = engine.random.int(u64); std.log.info("Using world seed 0x{x:0<16}", .{world_seed}); var it = Iterator2(i16).init(.{ .min = .{ -8, -8 }, .max = .{ 8, 8 }, }); while (it.next()) |chunk_coords2| { const chunk_coords3 = chunk_coords2 ++ [_]i16{0}; const origin = vm.Vector3.init( @floatFromInt(chunk_coords3[0]), @floatFromInt(chunk_coords3[1]), @floatFromInt(chunk_coords3[2]), ).mulScalar(16); try chunks.ensureUnusedCapacity(allocator_general, 1); chunks.putAssumeCapacityNoClobber( chunk_coords3, try Chunk.init(.{ .origin = origin, .descriptor_pool = descriptor_pool, .per_batch_descriptor_set_layout = per_batch_descriptor_set_layout, }), ); const chunk = chunks.getPtr(chunk_coords3).?; var y: usize = 0; while (y < 16) : (y += 1) { const fpos0 = vm.Vector2x8.init( .{ 0, 1, 2, 3, 4, 5, 6, 7 }, @splat(@floatFromInt(y)), ).add(.splat(origin.dropZ())); const fpos1 = vm.Vector2x8.init( .{ 8, 9, 10, 11, 12, 13, 14, 15 }, @splat(@floatFromInt(y)), ).add(.splat(origin.dropZ())); const iheight0 = worldgen.heightIv(world_seed, fpos0); const iheight1 = worldgen.heightIv(world_seed, fpos1); const iheightv: [16]i32 = @as([8]i32, iheight0) ++ @as([8]i32, iheight1); var x: usize = 0; while (x < 16) : (x += 1) { chunk.blocks[0][y][x] = block_bedrock; const iheight = iheightv[x]; var i: i32 = 0; while (i < iheight) : (i += 1) { const iz = i + 1; const block = if (i + 1 == iheight) block_grass else if (i + 4 >= iheight) block_dirt else block_stone; chunk.blocks[@intCast(iz)][y][x] = block; } } } } const player_position_vx = vm.Vector3Int.init(0, 0, worldgen.heightI(world_seed, .zero) + 1); const player_position_sv = player_position_vx .mulScalar(c.sv_per_vx) .add(.init(c.sv_per_vx / 2, c.sv_per_vx / 2, 0)); var chunk_it = chunks.iterator(); while (chunk_it.next()) |entry| { const x, const y, const z = entry.key_ptr.*; const chunk = entry.value_ptr; try chunk.refresh(&blocks, .{ .positive_x = chunks.getPtr(.{ x + 1, y, z }), .negative_x = chunks.getPtr(.{ x - 1, y, z }), .positive_y = chunks.getPtr(.{ x, y + 1, z }), .negative_y = chunks.getPtr(.{ x, y - 1, z }), .positive_z = chunks.getPtr(.{ x, y, z + 1 }), .negative_z = chunks.getPtr(.{ x, y, z - 1 }), }); } try point_lights.write(.{ .elements = &point_lights_data }); try directional_lights.write(.{ .elements = &directional_lights_data }); var skybox = try Skybox.load("skybox.hdr", 512, global_uniforms.buffer); errdefer skybox.deinit(); const font = &fonts.array.items[@intFromEnum(font_id)]; // TODO This coupling is not elegant - the game must provide the GUI with // uniform buffer for globals. Maybe GUI should just use their own global // uniform buffer not for any other reason than decoupling. After all, GUI // only needs a single 3×2 matrix and does not need to depend on game's // rendering pipeline. try engine.updateDescriptorSets(.{ .writes = &.{ .{ .dst_set = gui.box_descriptor_set, .dst_binding = 0, .dst_array_element = 0, .descriptor_type = .uniform_buffer, .descriptor_infos = .{ .buffer = &.{ .{ .buffer = global_uniforms.buffer, .offset = 0, .range = vk.WHOLE_SIZE, }, }, }, }, .{ .dst_set = gui.text_descriptor_set, .dst_binding = 0, .dst_array_element = 0, .descriptor_type = .uniform_buffer, .descriptor_infos = .{ .buffer = &.{ .{ .buffer = global_uniforms.buffer, .offset = 0, .range = vk.WHOLE_SIZE, }, }, }, }, .{ .dst_set = gui.text_descriptor_set, .dst_binding = 2, .dst_array_element = @intFromEnum(font_id), .descriptor_type = .storage_buffer, .descriptor_infos = .{ .buffer = &.{ .{ .buffer = font.glyph_data.buffer, .offset = 0, .range = vk.WHOLE_SIZE, }, }, }, }, .{ .dst_set = gui.text_descriptor_set, .dst_binding = 3, .dst_array_element = @intFromEnum(font_id), .descriptor_type = .storage_buffer, .descriptor_infos = .{ .buffer = &.{ .{ .buffer = font.control_points.buffer, .offset = 0, .range = vk.WHOLE_SIZE, }, }, }, }, .{ .dst_set = gui.text_descriptor_set, .dst_binding = 4, .dst_array_element = @intFromEnum(font_id), .descriptor_type = .storage_buffer, .descriptor_infos = .{ .buffer = &.{ .{ .buffer = font.band_data.buffer, .offset = 0, .range = vk.WHOLE_SIZE, }, }, }, }, }, }); return .{ .global_descriptor_set_layout = global_descriptor_set_layout, .per_batch_descriptor_set_layout = per_batch_descriptor_set_layout, .descriptor_pool = descriptor_pool, .global_descriptor_set = global_descriptor_set, .pipeline_layout = pipeline_layout, .pipeline = pipeline, .vertex_buffer = vertex_buffer, .index_buffer = index_buffer, .global_uniforms = global_uniforms, .global_uniforms_staging_buffer = global_uniforms_staging_buffer, .global_uniforms_transfer_semaphores = global_uniforms_transfer_semaphores, .point_lights = point_lights, .directional_lights = directional_lights, .sampler = sampler, .deferred_command_buffers = try .initCapacity(allocator_general, 4), .blocks = blocks, .chunks = .{ .chunks = chunks }, .skybox = skybox, .player = .init(player_position_sv, 0, 0), .font = font_id, }; } pub fn deinit(self: *Game) void { const allocator_general = ctx.allocator_general; const engine = ctx.engine; std.log.scoped(.deinit).debug("Deinitializing {*}", .{self}); for (self.deferred_command_buffers.items) |*command_buffer| { command_buffer.deinit(); } self.deferred_command_buffers.deinit(allocator_general); self.vertex_buffer.deinit(); self.index_buffer.deinit(); self.chunks.deinit(self.descriptor_pool); self.skybox.deinit(); self.global_uniforms.deinit(); self.global_uniforms_staging_buffer.deinit(); self.point_lights.deinit(); self.directional_lights.deinit(); for (self.global_uniforms_transfer_semaphores) |semaphore| { engine.destroySemaphore(semaphore); } allocator_general.free(self.global_uniforms_transfer_semaphores); engine.destroyDescriptorPool(self.descriptor_pool); engine.destroySampler(self.sampler); engine.destroyPipeline(self.pipeline); engine.destroyPipelineLayout(self.pipeline_layout); engine.destroyDescriptorSetLayout(self.per_batch_descriptor_set_layout); engine.destroyDescriptorSetLayout(self.global_descriptor_set_layout); self.blocks.deinit(); self.* = undefined; } pub fn update(self: *Game, dt: f32) void { const swapchain = ctx.swapchain; const gui = ctx.gui; self.player.update(dt, &self.chunks); const extent = swapchain.extent; const framebuffer_size = vm.Vector2.init( @floatFromInt(extent.width), @floatFromInt(extent.height), ); const crosshair_half_extent_px = 8; const crosshair_half_width_px = 1; gui.pushBox(.{ .background_color = .init(1, 1, 1, 0.5), .border_color = .init(0, 0, 0, 1), .position_sspx = framebuffer_size.mulScalar(0.5).add(.init(-crosshair_half_extent_px, -crosshair_half_width_px)), .size_px = .init(2.0 * crosshair_half_extent_px, 2.0 * crosshair_half_width_px), .border_width_px = 0, .border_radius_px = 0, }); gui.pushBox(.{ .background_color = .init(1, 1, 1, 0.5), .border_color = .init(0, 0, 0, 1), .position_sspx = framebuffer_size.mulScalar(0.5).add(.init(-crosshair_half_width_px, -crosshair_half_extent_px)), .size_px = .init(2.0 * crosshair_half_width_px, 2.0 * crosshair_half_extent_px), .border_width_px = 0, .border_radius_px = 0, }); { const margin: f16 = 8; const font_size: f32 = 16; const player_position_sv = self.player.position_sv; gui.pushBox(.{ .background_color = .init(0, 0, 0, 0.75), .border_color = .init(0, 0, 0, 1), .position_sspx = .init(0, 0), .size_px = .init(157, 4 * margin + 3 * font_size), .border_width_px = 0, .border_radius_px = 0, }); gui.printLine("X {X:0>8}", .{@as(u32, @bitCast(player_position_sv.x))}, .{ .position_sspx = .init(margin, margin + font_size), .font_id = self.font, .cap_height_px = font_size, .color = .init(1, 1, 1, 1), }); gui.printLine("Y {X:0>8}", .{@as(u32, @bitCast(player_position_sv.y))}, .{ .position_sspx = .init(margin, 2 * (margin + font_size)), .font_id = self.font, .cap_height_px = font_size, .color = .init(1, 1, 1, 1), }); gui.printLine("Z {X:0>8}", .{@as(u32, @bitCast(player_position_sv.z))}, .{ .position_sspx = .init(margin, 3 * (margin + font_size)), .font_id = self.font, .cap_height_px = font_size, .color = .init(1, 1, 1, 1), }); } self.render() catch |err| { std.log.err("Failed to render: {s}", .{@errorName(err)}); @panic("Frame update failed"); }; } pub fn onKeyDown(self: *Game, key: glfw.Key) void { self.player.onKeyDown(key); } pub fn onKeyUp(self: *Game, key: glfw.Key) void { self.player.onKeyUp(key); } pub fn onMouseMove(self: *Game, dx: f32, dy: f32) void { self.player.onMouseMove(dx, dy); } pub fn onMouseDown(self: *Game, button: glfw.MouseButton) void { self.player.onMouseDown(button, self); } pub fn onMouseUp(self: *Game, button: glfw.MouseButton) void { _ = self; _ = button; } fn render(self: *Game) !void { const allocator_general = ctx.allocator_general; const window = ctx.window; const swapchain = ctx.swapchain; const gui = ctx.gui; const framebuffer_width, const framebuffer_height = blk: { const w, const h = window.getFramebufferSize(); break :blk [_]u32{ @intCast(w), @intCast(h) }; }; if (framebuffer_width == 0 or framebuffer_height == 0) { return; } if (framebuffer_width != swapchain.extent.width or framebuffer_height != swapchain.extent.height) { try self.recreateSwapchain(); } if (swapchain.swapchain == .null_handle) { return; } swapchain.acquire() catch |err| switch (err) { error.OutOfDateKHR => return self.recreateSwapchain(), else => return err, }; for (self.deferred_command_buffers.items) |*deferred_command_buffer| { deferred_command_buffer.deinit(); } self.deferred_command_buffers.clearRetainingCapacity(); const extent = swapchain.extent; const framebuffer_size = vm.Vector2.init( @floatFromInt(extent.width), @floatFromInt(extent.height), ); const camera_position = self.player.position_sv .asFloat() .divScalar(c.sv_per_vx) .add(.init(0, 0, Player.camera_height_vx)); const camera_rotation = vm.Quaternion.mulQuaternion( .initRotation(.XY, self.player.yaw_turns), .initRotation(.YZ, self.player.pitch_turns), ); const camera_aspect_ratio = framebuffer_size.x / framebuffer_size.y; const camera_yscale = 1.0 / @tan(0.5 * self.player.vertical_fov_deg * std.math.rad_per_deg); const camera_xscale = camera_yscale / camera_aspect_ratio; const matrix_ws_to_vs = vm.Matrix4x4 .initTranslationRotation(camera_position, camera_rotation) .inverseOrthonormal(); // zig fmt: off const matrix_vs_to_cs = vm.Matrix4x4.init( camera_xscale, 0, 0, 0, 0, 0, 0, 1, 0, -camera_yscale, 0, 0, 0, 0, camera_near_plane, 0, ); // zig fmt: on // zig fmt: off const matrix_sspx_to_cs = vm.Matrix3x2.init( 2.0 / framebuffer_size.x, 0, 0, 2.0 / framebuffer_size.y, -1, -1, ); // zig fmt: on const ambient_light = vm.Vector3.init(0.01, 0.01, 0.01); const global_uniforms_data: shaders.GlobalUniforms = .{ .matrixWStoVS = matrix_ws_to_vs, .matrixVStoCS = matrix_vs_to_cs, .matrixSSPXtoCS = matrix_sspx_to_cs, .pointLightCount = point_lights_data.len, .directionalLightCount = directional_lights_data.len, .ambientLight = ambient_light, }; const staging_memory = try self.global_uniforms_staging_buffer.map(); @memcpy(staging_memory, std.mem.asBytes(&global_uniforms_data)); self.global_uniforms_staging_buffer.unmap(); try self.deferred_command_buffers.ensureUnusedCapacity(allocator_general, 1); const global_uniforms_transfer_command_buffer = try CommandBuffer.init(.transfer); self.deferred_command_buffers.appendAssumeCapacity(global_uniforms_transfer_command_buffer); try global_uniforms_transfer_command_buffer.beginCommandBuffer(); global_uniforms_transfer_command_buffer.copyBuffer( self.global_uniforms_staging_buffer.buffer, self.global_uniforms.buffer, &.{ .{ .src_offset = 0, .dst_offset = 0, .size = @sizeOf(shaders.GlobalUniforms), }, }, ); try global_uniforms_transfer_command_buffer.endCommandBuffer(); try global_uniforms_transfer_command_buffer.submit(.{ .signal_semaphores = &.{self.global_uniforms_transfer_semaphores[swapchain.image_index.?]}, }); try self.deferred_command_buffers.ensureUnusedCapacity(allocator_general, 1); const command_buffer: CommandBuffer = try .init(.graphics); self.deferred_command_buffers.appendAssumeCapacity(command_buffer); try command_buffer.beginCommandBuffer(); { const swapchain_image = swapchain.getCurrentSwapchainImage(); command_buffer.pipelineBarrier(.{ .src_stage_mask = .{ .color_attachment_output_bit = true }, .dst_stage_mask = .{ .color_attachment_output_bit = true }, .image_memory_barriers = &.{ .{ .src_access_mask = .{}, .dst_access_mask = .{ .color_attachment_read_bit = true, .color_attachment_write_bit = true, }, .old_layout = .undefined, .new_layout = .color_attachment_optimal, .src_queue_family_index = vk.QUEUE_FAMILY_IGNORED, .dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED, .image = swapchain_image.image, .subresource_range = .{ .aspect_mask = .{ .color_bit = true }, .base_mip_level = 0, .level_count = 1, .base_array_layer = 0, .layer_count = 1, }, }, }, }); command_buffer.pipelineBarrier(.{ .src_stage_mask = .{ .late_fragment_tests_bit = true }, .dst_stage_mask = .{ .early_fragment_tests_bit = true }, .image_memory_barriers = &.{ .{ .src_access_mask = .{ .depth_stencil_attachment_write_bit = true, }, .dst_access_mask = .{ .depth_stencil_attachment_write_bit = true, }, .old_layout = .undefined, .new_layout = .depth_stencil_attachment_optimal, .src_queue_family_index = vk.QUEUE_FAMILY_IGNORED, .dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED, .image = swapchain.depth_texture.?.image, .subresource_range = .{ .aspect_mask = .{ .depth_bit = true }, .base_mip_level = 0, .level_count = 1, .base_array_layer = 0, .layer_count = 1, }, }, }, }); try command_buffer.beginRendering(.{ .render_area = .{ .offset = .{ .x = 0, .y = 0 }, .extent = extent, }, .layer_count = 1, .view_mask = 0, .color_attachments = &.{ .{ .image_view = swapchain_image.image_view, .image_layout = .color_attachment_optimal, .load_op = .clear, .store_op = .store, .clear_value = .{ .color = .{ .float_32 = .{ 0, 0, 0, 0 }, }, }, }, }, .depth_attachment = .{ .image_view = swapchain.depth_texture.?.image_view, .image_layout = .depth_attachment_optimal, .load_op = .clear, .store_op = .dont_care, .clear_value = .{ .depth_stencil = .{ .depth = 0, .stencil = 0, }, }, }, }); command_buffer.setViewport(0, .{ .x = 0, .y = 0, .width = @floatFromInt(extent.width), .height = @floatFromInt(extent.height), .min_depth = 0, .max_depth = 1, }); command_buffer.setScissor(0, .{ .offset = .{ .x = 0, .y = 0 }, .extent = extent, }); // --- RENDER 3D SCENE --- command_buffer.bindPipeline(.graphics, self.pipeline); try command_buffer.bindVertexBuffer(0, .{ .buffer = self.vertex_buffer.buffer, .offset = 0, }); command_buffer.bindIndexBuffer(self.index_buffer.buffer, 0, .uint16); command_buffer.bindDescriptorSet(.graphics, self.pipeline_layout, 0, self.global_descriptor_set, null); var it = self.chunks.chunks.valueIterator(); while (it.next()) |chunk| { chunk.draw(self.pipeline_layout, command_buffer); } try self.skybox.draw(command_buffer); // --- RENDER GUI --- try gui.draw(command_buffer); // --- FINALIZE --- command_buffer.endRendering(); command_buffer.pipelineBarrier(.{ .src_stage_mask = .{ .color_attachment_output_bit = true }, .dst_stage_mask = .{ .bottom_of_pipe_bit = true }, .image_memory_barriers = &.{ .{ .src_access_mask = .{ .color_attachment_write_bit = true }, .dst_access_mask = .{}, .old_layout = .color_attachment_optimal, .new_layout = .present_src_khr, .src_queue_family_index = vk.QUEUE_FAMILY_IGNORED, .dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED, .image = swapchain_image.image, .subresource_range = .{ .aspect_mask = .{ .color_bit = true }, .base_mip_level = 0, .level_count = 1, .base_array_layer = 0, .layer_count = 1, }, }, }, }); } try command_buffer.endCommandBuffer(); swapchain.present(.{ .command_buffer = command_buffer, .wait_semaphores = &.{ .{ .semaphore = self.global_uniforms_transfer_semaphores[swapchain.image_index.?], .stage_flags = .{ .vertex_shader_bit = true }, }, }, }) catch |err| switch (err) { error.OutOfDateKHR => return self.recreateSwapchain(), else => return err, }; } fn recreateSwapchain(self: *Game) !void { const engine = ctx.engine; const swapchain = ctx.swapchain; try swapchain.recreate(); for (self.global_uniforms_transfer_semaphores, 0..) |*semaphore, i| { engine.destroySemaphore(semaphore.*); semaphore.* = try engine.createSemaphore(); engine.setObjectName(semaphore.*, "S Transfer[{d}]", .{i}); } }