diff --git a/src/engine/graphics/rhi_vulkan.zig b/src/engine/graphics/rhi_vulkan.zig index 334540e3..0a94040e 100644 --- a/src/engine/graphics/rhi_vulkan.zig +++ b/src/engine/graphics/rhi_vulkan.zig @@ -94,6 +94,11 @@ const TextureResource = struct { height: u32, }; +const ZombieBuffer = struct { + buffer: c.VkBuffer, + memory: c.VkDeviceMemory, +}; + /// Core Vulkan context containing all renderer state. /// Owns Vulkan objects and manages their lifecycle. const VulkanContext = struct { @@ -109,6 +114,8 @@ const VulkanContext = struct { transfer_command_pool: c.VkCommandPool, transfer_command_buffer: c.VkCommandBuffer, + buffer_deletion_queue: [MAX_FRAMES_IN_FLIGHT]std.ArrayListUnmanaged(ZombieBuffer), + // Sync image_available_semaphores: [MAX_FRAMES_IN_FLIGHT]c.VkSemaphore, render_finished_semaphores: [MAX_FRAMES_IN_FLIGHT]c.VkSemaphore, @@ -1347,6 +1354,10 @@ fn init(ctx_ptr: *anyopaque, allocator: std.mem.Allocator) anyerror!void { ctx.shadow_image_layouts[si] = c.VK_IMAGE_LAYOUT_UNDEFINED; } + for (0..MAX_FRAMES_IN_FLIGHT) |frame_i| { + ctx.buffer_deletion_queue[frame_i] = .empty; + } + std.log.info("Vulkan initialized successfully!", .{}); } @@ -1438,6 +1449,14 @@ fn deinit(ctx_ptr: *anyopaque) void { } ctx.textures.deinit(); + for (0..MAX_FRAMES_IN_FLIGHT) |frame_i| { + for (ctx.buffer_deletion_queue[frame_i].items) |zombie| { + if (zombie.buffer != null) c.vkDestroyBuffer(ctx.device, zombie.buffer, null); + if (zombie.memory != null) c.vkFreeMemory(ctx.device, zombie.memory, null); + } + ctx.buffer_deletion_queue[frame_i].deinit(ctx.allocator); + } + c.vkDestroyDevice(ctx.device, null); } if (ctx.surface != null) c.vkDestroySurfaceKHR(ctx.instance, ctx.surface, null); @@ -1549,8 +1568,9 @@ fn destroyBuffer(ctx_ptr: *anyopaque, handle: rhi.BufferHandle) void { ctx.mutex.unlock(); if (entry_opt) |entry| { - c.vkDestroyBuffer(ctx.device, entry.value.buffer, null); - c.vkFreeMemory(ctx.device, entry.value.memory, null); + ctx.buffer_deletion_queue[ctx.current_sync_frame].append(ctx.allocator, .{ .buffer = entry.value.buffer, .memory = entry.value.memory }) catch { + std.log.err("Failed to queue buffer deletion (OOM). Leaking buffer.", .{}); + }; } } @@ -1762,6 +1782,13 @@ fn beginFrame(ctx_ptr: *anyopaque) void { _ = c.vkWaitForFences(ctx.device, 1, &fence, c.VK_TRUE, std.math.maxInt(u64)); + // Process deletion queue for this frame + for (ctx.buffer_deletion_queue[ctx.current_sync_frame].items) |zombie| { + c.vkDestroyBuffer(ctx.device, zombie.buffer, null); + c.vkFreeMemory(ctx.device, zombie.memory, null); + } + ctx.buffer_deletion_queue[ctx.current_sync_frame].clearRetainingCapacity(); + var image_index: u32 = 0; const result = c.vkAcquireNextImageKHR(ctx.device, ctx.swapchain, 1000000000, acquire_semaphore, null, &image_index); diff --git a/src/game/app.zig b/src/game/app.zig index cf48abbe..d1170d97 100644 --- a/src/game/app.zig +++ b/src/game/app.zig @@ -27,6 +27,7 @@ const AppState = @import("state.zig").AppState; const Settings = @import("state.zig").Settings; const Menus = @import("menus.zig"); const RenderSystem = @import("render_system.zig").RenderSystem; +const MapController = @import("map_controller.zig").MapController; pub const App = struct { allocator: std.mem.Allocator, @@ -51,14 +52,7 @@ pub const App = struct { world: ?*World, world_map: ?WorldMap, - show_map: bool, - map_needs_update: bool, - map_zoom: f32, - map_target_zoom: f32, - map_pos_x: f32, - map_pos_z: f32, - last_mouse_x: f32, - last_mouse_y: f32, + map_controller: MapController, pub fn init(allocator: std.mem.Allocator) !*App { var use_vulkan = false; @@ -114,14 +108,7 @@ pub const App = struct { .seed_focused = false, .world = null, .world_map = null, - .show_map = false, - .map_needs_update = true, - .map_zoom = 4.0, - .map_target_zoom = 4.0, - .map_pos_x = 0.0, - .map_pos_z = 0.0, - .last_mouse_x = 0.0, - .last_mouse_y = 0.0, + .map_controller = .{}, }; return app; @@ -163,8 +150,8 @@ pub const App = struct { continue; }; if (self.world_map == null) self.world_map = WorldMap.init(self.render_system.rhi, 256, 256); - self.show_map = false; - self.map_needs_update = true; + self.map_controller.show_map = false; + self.map_controller.map_needs_update = true; self.camera = Camera.init(.{ .position = Vec3.init(8, 100, 8), .pitch = -0.3, .move_speed = 50.0 }); } @@ -183,8 +170,8 @@ pub const App = struct { const mouse_clicked = self.input.isMouseButtonPressed(.left); if (self.input.isKeyPressed(.escape)) { - if (self.show_map) { - self.show_map = false; + if (self.map_controller.show_map) { + self.map_controller.show_map = false; if (self.app_state == .world) self.input.setMouseCapture(self.window_manager.window, true); } else { switch (self.app_state) { @@ -227,77 +214,11 @@ pub const App = struct { self.render_system.rhi.setVSync(self.settings.vsync); } if (self.input.isKeyPressed(.u)) self.debug_shadows = !self.debug_shadows; - if (self.input.isKeyPressed(.m)) { - self.show_map = !self.show_map; - log.log.info("Toggle map: show={}", .{self.show_map}); - if (self.show_map) { - self.map_pos_x = self.camera.position.x; - self.map_pos_z = self.camera.position.z; - self.map_target_zoom = self.map_zoom; - self.map_needs_update = true; - self.input.setMouseCapture(self.window_manager.window, false); - } else if (self.app_state == .world) self.input.setMouseCapture(self.window_manager.window, true); - } - if (self.show_map) { - const dt = @min(self.time.delta_time, 0.033); - // ... map input logic (omitted for brevity, same as before) ... - // Wait, I need to keep this logic or extract it. - // For now, I'll copy-paste the map input logic as it's coupled to App state. - if (self.input.isKeyDown(.plus) or self.input.isKeyDown(.kp_plus)) { - self.map_target_zoom /= @exp(1.2 * dt); - self.map_needs_update = true; - } - if (self.input.isKeyDown(.minus) or self.input.isKeyDown(.kp_minus)) { - self.map_target_zoom *= @exp(1.2 * dt); - self.map_needs_update = true; - } - if (self.input.scroll_y != 0) { - self.map_target_zoom *= @exp(-self.input.scroll_y * 0.12); - self.map_needs_update = true; - } - self.map_target_zoom = std.math.clamp(self.map_target_zoom, 0.05, 128.0); - const old_zoom = self.map_zoom; - self.map_zoom = std.math.lerp(self.map_zoom, self.map_target_zoom, 20.0 * dt); - if (@abs(self.map_zoom - old_zoom) > 0.001 * self.map_zoom) self.map_needs_update = true; - if (self.input.isKeyPressed(.space)) { - self.map_pos_x = self.camera.position.x; - self.map_pos_z = self.camera.position.z; - self.map_needs_update = true; - } - const map_ui_size: f32 = @min(screen_w, screen_h) * 0.8; - const world_to_screen_ratio = if (self.world_map) |m| @as(f32, @floatFromInt(m.width)) / map_ui_size else 1.0; - if (self.input.isMouseButtonPressed(.left)) { - self.last_mouse_x = mouse_x; - self.last_mouse_y = mouse_y; - } - if (self.input.isMouseButtonDown(.left)) { - const drag_dx = mouse_x - self.last_mouse_x; - const drag_dz = mouse_y - self.last_mouse_y; - if (@abs(drag_dx) > 0.1 or @abs(drag_dz) > 0.1) { - self.map_pos_x -= drag_dx * self.map_zoom * world_to_screen_ratio; - self.map_pos_z -= drag_dz * self.map_zoom * world_to_screen_ratio; - self.map_needs_update = true; - } - self.last_mouse_x = mouse_x; - self.last_mouse_y = mouse_y; - } else { - const pan_kb_speed = 800.0 * self.map_zoom; - var dx: f32 = 0; - var dz: f32 = 0; - if (self.input.isKeyDown(.w)) dz -= 1; - if (self.input.isKeyDown(.s)) dz += 1; - if (self.input.isKeyDown(.a)) dx -= 1; - if (self.input.isKeyDown(.d)) dx += 1; - if (dx != 0 or dz != 0) { - self.map_pos_x += dx * pan_kb_speed * dt; - self.map_pos_z += dz * pan_kb_speed * dt; - self.map_needs_update = true; - } - } - } + self.map_controller.update(&self.input, &self.camera, self.time.delta_time, self.window_manager.window, screen_w, screen_h, if (self.world_map) |m| m.width else 256); if (self.debug_shadows and self.input.isKeyPressed(.k)) self.debug_cascade_idx = (self.debug_cascade_idx + 1) % 3; + if (self.input.isKeyPressed(.@"1")) if (self.render_system.atmosphere) |*a| a.setTimeOfDay(0.0); if (self.input.isKeyPressed(.@"2")) if (self.render_system.atmosphere) |*a| a.setTimeOfDay(0.25); if (self.input.isKeyPressed(.@"3")) if (self.render_system.atmosphere) |*a| a.setTimeOfDay(0.5); @@ -307,7 +228,7 @@ pub const App = struct { }; if (in_world) { - if (!self.show_map and !in_pause) { + if (!self.map_controller.show_map and !in_pause) { self.camera.update(&self.input, self.time.delta_time); } @@ -466,27 +387,9 @@ pub const App = struct { } if (self.ui) |*u| { u.begin(); - if (self.show_map) if (self.world_map) |*m| { - if (self.map_needs_update) { - try m.update(&active_world.generator, self.map_pos_x, self.map_pos_z, self.map_zoom); - self.map_needs_update = false; - } - const sz: f32 = @min(screen_w, screen_h) * 0.8; - const mx = (screen_w - sz) * 0.5; - const my = (screen_h - sz) * 0.5; - u.drawRect(.{ .x = 0, .y = 0, .width = screen_w, .height = screen_h }, Color.rgba(0, 0, 0, 0.5)); - u.drawTexture(@intCast(m.texture.handle), .{ .x = mx, .y = my, .width = sz, .height = sz }); - u.drawRectOutline(.{ .x = mx, .y = my, .width = sz, .height = sz }, Color.white, 2.0); - Font.drawTextCentered(u, "WORLD MAP", screen_w * 0.5, my - 40.0, 3.0, Color.white); - const rx = (self.camera.position.x - self.map_pos_x) / (self.map_zoom * @as(f32, @floatFromInt(m.width))); - const rz = (self.camera.position.z - self.map_pos_z) / (self.map_zoom * @as(f32, @floatFromInt(m.height))); - const px = mx + (rx + 0.5) * sz; - const pz = my + (rz + 0.5) * sz; - if (px >= mx and px <= mx + sz and pz >= my and pz <= my + sz) { - u.drawRect(.{ .x = px - 5, .y = pz - 1, .width = 10, .height = 2 }, Color.red); - u.drawRect(.{ .x = px - 1, .y = pz - 5, .width = 2, .height = 10 }, Color.red); - } - }; + if (self.world_map) |*m| { + try self.map_controller.draw(u, screen_w, screen_h, m, &active_world.generator, self.camera.position); + } u.drawRect(.{ .x = 10, .y = 10, .width = 80, .height = 30 }, Color.rgba(0, 0, 0, 0.7)); Font.drawNumber(u, @intFromFloat(self.time.fps), 15, 15, Color.white); const stats = active_world.getStats(); diff --git a/src/game/map_controller.zig b/src/game/map_controller.zig new file mode 100644 index 00000000..861ef2e3 --- /dev/null +++ b/src/game/map_controller.zig @@ -0,0 +1,128 @@ +const std = @import("std"); +const c = @import("../c.zig").c; +const Input = @import("../engine/input/input.zig").Input; +const WorldMap = @import("../world/worldgen/world_map.zig").WorldMap; +const Camera = @import("../engine/graphics/camera.zig").Camera; +const TerrainGenerator = @import("../world/worldgen/generator.zig").TerrainGenerator; +const UISystem = @import("../engine/ui/ui_system.zig").UISystem; +const Color = @import("../engine/ui/ui_system.zig").Color; +const Font = @import("../engine/ui/font.zig"); +const log = @import("../engine/core/log.zig"); +const Vec3 = @import("../engine/math/vec3.zig").Vec3; + +pub const MapController = struct { + show_map: bool = false, + map_needs_update: bool = true, + map_zoom: f32 = 4.0, + map_target_zoom: f32 = 4.0, + map_pos_x: f32 = 0.0, + map_pos_z: f32 = 0.0, + last_mouse_x: f32 = 0.0, + last_mouse_y: f32 = 0.0, + + pub fn update(self: *MapController, input: *Input, camera: *const Camera, time_delta: f32, window: *c.SDL_Window, screen_w: f32, screen_h: f32, world_map_width: u32) void { + if (input.isKeyPressed(.m)) { + self.show_map = !self.show_map; + log.log.info("Toggle map: show={}", .{self.show_map}); + if (self.show_map) { + self.map_pos_x = camera.position.x; + self.map_pos_z = camera.position.z; + self.map_target_zoom = self.map_zoom; + self.map_needs_update = true; + input.setMouseCapture(window, false); + } else { + input.setMouseCapture(window, true); + } + } + + if (!self.show_map) return; + + const dt = @min(time_delta, 0.033); + if (input.isKeyDown(.plus) or input.isKeyDown(.kp_plus)) { + self.map_target_zoom /= @exp(1.2 * dt); + self.map_needs_update = true; + } + if (input.isKeyDown(.minus) or input.isKeyDown(.kp_minus)) { + self.map_target_zoom *= @exp(1.2 * dt); + self.map_needs_update = true; + } + if (input.scroll_y != 0) { + self.map_target_zoom *= @exp(-input.scroll_y * 0.12); + self.map_needs_update = true; + } + self.map_target_zoom = std.math.clamp(self.map_target_zoom, 0.05, 128.0); + const old_zoom = self.map_zoom; + self.map_zoom = std.math.lerp(self.map_zoom, self.map_target_zoom, 20.0 * dt); + if (@abs(self.map_zoom - old_zoom) > 0.001 * self.map_zoom) self.map_needs_update = true; + + if (input.isKeyPressed(.space)) { + self.map_pos_x = camera.position.x; + self.map_pos_z = camera.position.z; + self.map_needs_update = true; + } + + const mouse_pos = input.getMousePosition(); + const mouse_x: f32 = @floatFromInt(mouse_pos.x); + const mouse_y: f32 = @floatFromInt(mouse_pos.y); + + const map_ui_size: f32 = @min(screen_w, screen_h) * 0.8; + const world_to_screen_ratio = @as(f32, @floatFromInt(world_map_width)) / map_ui_size; + + if (input.isMouseButtonPressed(.left)) { + self.last_mouse_x = mouse_x; + self.last_mouse_y = mouse_y; + } + + if (input.isMouseButtonDown(.left)) { + const drag_dx = mouse_x - self.last_mouse_x; + const drag_dz = mouse_y - self.last_mouse_y; + if (@abs(drag_dx) > 0.1 or @abs(drag_dz) > 0.1) { + self.map_pos_x -= drag_dx * self.map_zoom * world_to_screen_ratio; + self.map_pos_z -= drag_dz * self.map_zoom * world_to_screen_ratio; + self.map_needs_update = true; + } + self.last_mouse_x = mouse_x; + self.last_mouse_y = mouse_y; + } else { + const pan_kb_speed = 800.0 * self.map_zoom; + var dx: f32 = 0; + var dz: f32 = 0; + if (input.isKeyDown(.w)) dz -= 1; + if (input.isKeyDown(.s)) dz += 1; + if (input.isKeyDown(.a)) dx -= 1; + if (input.isKeyDown(.d)) dx += 1; + if (dx != 0 or dz != 0) { + self.map_pos_x += dx * pan_kb_speed * dt; + self.map_pos_z += dz * pan_kb_speed * dt; + self.map_needs_update = true; + } + } + } + + pub fn draw(self: *MapController, u: *UISystem, screen_w: f32, screen_h: f32, world_map: *WorldMap, generator: *const TerrainGenerator, camera_pos: Vec3) !void { + if (!self.show_map) return; + + if (self.map_needs_update) { + try world_map.update(generator, self.map_pos_x, self.map_pos_z, self.map_zoom); + self.map_needs_update = false; + } + + const sz: f32 = @min(screen_w, screen_h) * 0.8; + const mx = (screen_w - sz) * 0.5; + const my = (screen_h - sz) * 0.5; + u.drawRect(.{ .x = 0, .y = 0, .width = screen_w, .height = screen_h }, Color.rgba(0, 0, 0, 0.5)); + u.drawTexture(@intCast(world_map.texture.handle), .{ .x = mx, .y = my, .width = sz, .height = sz }); + u.drawRectOutline(.{ .x = mx, .y = my, .width = sz, .height = sz }, Color.white, 2.0); + Font.drawTextCentered(u, "WORLD MAP", screen_w * 0.5, my - 40.0, 3.0, Color.white); + + const rx = (camera_pos.x - self.map_pos_x) / (self.map_zoom * @as(f32, @floatFromInt(world_map.width))); + const rz = (camera_pos.z - self.map_pos_z) / (self.map_zoom * @as(f32, @floatFromInt(world_map.height))); + const px = mx + (rx + 0.5) * sz; + const pz = my + (rz + 0.5) * sz; + + if (px >= mx and px <= mx + sz and pz >= my and pz <= my + sz) { + u.drawRect(.{ .x = px - 5, .y = pz - 1, .width = 10, .height = 2 }, Color.red); + u.drawRect(.{ .x = px - 1, .y = pz - 5, .width = 2, .height = 10 }, Color.red); + } + } +};