diff --git a/assets/config/presets.json b/assets/config/presets.json index 6ac4450..6eac50a 100644 --- a/assets/config/presets.json +++ b/assets/config/presets.json @@ -8,6 +8,9 @@ "pbr_enabled": false, "pbr_quality": 0, "msaa_samples": 1, + "taa_enabled": false, + "taa_blend_factor": 0.85, + "taa_velocity_rejection": 0.03, "anisotropic_filtering": 1, "max_texture_resolution": 64, "cloud_shadows_enabled": false, @@ -32,7 +35,10 @@ "shadow_cascade_blend": false, "pbr_enabled": true, "pbr_quality": 1, - "msaa_samples": 2, + "msaa_samples": 1, + "taa_enabled": true, + "taa_blend_factor": 0.9, + "taa_velocity_rejection": 0.02, "anisotropic_filtering": 4, "max_texture_resolution": 128, "cloud_shadows_enabled": true, @@ -45,7 +51,7 @@ "ssao_enabled": true, "lod_enabled": true, "render_distance": 12, - "fxaa_enabled": true, + "fxaa_enabled": false, "bloom_enabled": true, "bloom_intensity": 0.5 }, @@ -57,7 +63,10 @@ "shadow_cascade_blend": true, "pbr_enabled": true, "pbr_quality": 2, - "msaa_samples": 4, + "msaa_samples": 1, + "taa_enabled": true, + "taa_blend_factor": 0.93, + "taa_velocity_rejection": 0.015, "anisotropic_filtering": 8, "max_texture_resolution": 256, "cloud_shadows_enabled": true, @@ -82,7 +91,10 @@ "shadow_cascade_blend": true, "pbr_enabled": true, "pbr_quality": 2, - "msaa_samples": 4, + "msaa_samples": 1, + "taa_enabled": true, + "taa_blend_factor": 0.95, + "taa_velocity_rejection": 0.01, "anisotropic_filtering": 16, "max_texture_resolution": 512, "cloud_shadows_enabled": true, diff --git a/assets/shaders/vulkan/taa.frag b/assets/shaders/vulkan/taa.frag new file mode 100644 index 0000000..50c203d --- /dev/null +++ b/assets/shaders/vulkan/taa.frag @@ -0,0 +1,50 @@ +#version 450 + +layout(location = 0) in vec2 outUV; +layout(location = 0) out vec4 outColor; + +layout(set = 0, binding = 0) uniform sampler2D uCurrentHdr; +layout(set = 0, binding = 1) uniform sampler2D uHistory; +layout(set = 0, binding = 2) uniform sampler2D uVelocity; + +layout(push_constant) uniform TAAPush { + float blend_factor; + float velocity_rejection; + float reset_history; + float _pad; +} taa; + +vec3 sampleCurrent(vec2 uv) { + return texture(uCurrentHdr, uv).rgb; +} + +void main() { + vec2 velocity = texture(uVelocity, outUV).xy; + vec2 history_uv = outUV - velocity; + + vec3 current = sampleCurrent(outUV); + + if (taa.reset_history > 0.5 || history_uv.x < 0.0 || history_uv.y < 0.0 || history_uv.x > 1.0 || history_uv.y > 1.0) { + outColor = vec4(current, 1.0); + return; + } + + vec3 history = texture(uHistory, history_uv).rgb; + + vec2 texel = 1.0 / vec2(textureSize(uCurrentHdr, 0)); + vec3 c1 = sampleCurrent(clamp(outUV + vec2(texel.x, 0.0), 0.0, 1.0)); + vec3 c2 = sampleCurrent(clamp(outUV + vec2(-texel.x, 0.0), 0.0, 1.0)); + vec3 c3 = sampleCurrent(clamp(outUV + vec2(0.0, texel.y), 0.0, 1.0)); + vec3 c4 = sampleCurrent(clamp(outUV + vec2(0.0, -texel.y), 0.0, 1.0)); + + vec3 min_color = min(current, min(min(c1, c2), min(c3, c4))); + vec3 max_color = max(current, max(max(c1, c2), max(c3, c4))); + vec3 clamped_history = clamp(history, min_color, max_color); + + float speed = length(velocity); + float stable = 1.0 - smoothstep(taa.velocity_rejection, taa.velocity_rejection * 4.0, speed); + float history_weight = taa.blend_factor * stable; + vec3 resolved = mix(current, clamped_history, history_weight); + + outColor = vec4(resolved, 1.0); +} diff --git a/assets/shaders/vulkan/taa.vert b/assets/shaders/vulkan/taa.vert new file mode 100644 index 0000000..ebae3af --- /dev/null +++ b/assets/shaders/vulkan/taa.vert @@ -0,0 +1,8 @@ +#version 450 + +layout(location = 0) out vec2 outUV; + +void main() { + outUV = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); + gl_Position = vec4(outUV * 2.0f - 1.0f, 0.0f, 1.0f); +} diff --git a/build.zig b/build.zig index d1a8543..7d92fac 100644 --- a/build.zig +++ b/build.zig @@ -176,6 +176,8 @@ pub fn build(b: *std.Build) void { const validate_vulkan_ssao_frag = b.addSystemCommand(&.{ "glslangValidator", "-V", "assets/shaders/vulkan/ssao.frag" }); const validate_vulkan_ssao_blur_frag = b.addSystemCommand(&.{ "glslangValidator", "-V", "assets/shaders/vulkan/ssao_blur.frag" }); const validate_vulkan_g_pass_frag = b.addSystemCommand(&.{ "glslangValidator", "-V", "assets/shaders/vulkan/g_pass.frag" }); + const validate_vulkan_taa_vert = b.addSystemCommand(&.{ "glslangValidator", "-V", "assets/shaders/vulkan/taa.vert" }); + const validate_vulkan_taa_frag = b.addSystemCommand(&.{ "glslangValidator", "-V", "assets/shaders/vulkan/taa.frag" }); const validate_vulkan_lpv_inject_comp = b.addSystemCommand(&.{ "glslangValidator", "-V", "assets/shaders/vulkan/lpv_inject.comp" }); const validate_vulkan_lpv_propagate_comp = b.addSystemCommand(&.{ "glslangValidator", "-V", "assets/shaders/vulkan/lpv_propagate.comp" }); @@ -197,6 +199,8 @@ pub fn build(b: *std.Build) void { test_step.dependOn(&validate_vulkan_ssao_frag.step); test_step.dependOn(&validate_vulkan_ssao_blur_frag.step); test_step.dependOn(&validate_vulkan_g_pass_frag.step); + test_step.dependOn(&validate_vulkan_taa_vert.step); + test_step.dependOn(&validate_vulkan_taa_frag.step); test_step.dependOn(&validate_vulkan_lpv_inject_comp.step); test_step.dependOn(&validate_vulkan_lpv_propagate_comp.step); } diff --git a/src/engine/graphics/camera.zig b/src/engine/graphics/camera.zig index 10a1d63..3bc2407 100644 --- a/src/engine/graphics/camera.zig +++ b/src/engine/graphics/camera.zig @@ -9,6 +9,16 @@ const Key = @import("../core/interfaces.zig").Key; const InputMapper = @import("../../game/input_mapper.zig").InputMapper; pub const Camera = struct { + // 4-tap Halton(2,3) sequence centered to [-0.5, 0.5] pixel offsets. + // We keep this to 4 samples to keep temporal convergence fast while matching + // the current low-latency TAA target (minimal history lag and ghosting). + const JITTER_SEQUENCE = [_][2]f32{ + .{ 0.0, -0.16666667 }, + .{ -0.25, 0.16666667 }, + .{ 0.25, -0.3888889 }, + .{ -0.375, -0.055555556 }, + }; + position: Vec3, /// Yaw in radians (rotation around Y axis) @@ -36,6 +46,7 @@ pub const Camera = struct { forward: Vec3, right: Vec3, up: Vec3, + jitter_index: usize, pub const Config = struct { position: Vec3 = Vec3.init(0, 0, 3), @@ -61,11 +72,25 @@ pub const Camera = struct { .forward = Vec3.zero, .right = Vec3.zero, .up = Vec3.zero, + .jitter_index = 0, }; cam.updateVectors(); return cam; } + pub fn resetJitter(self: *Camera) void { + self.jitter_index = 0; + } + + pub fn advanceJitter(self: *Camera) void { + self.jitter_index = (self.jitter_index + 1) % JITTER_SEQUENCE.len; + } + + fn currentJitterPixel(self: *const Camera, enabled: bool) [2]f32 { + if (!enabled) return .{ 0.0, 0.0 }; + return JITTER_SEQUENCE[self.jitter_index]; + } + /// Update camera from input (call once per frame) pub fn update(self: *Camera, input: *const Input, mapper: *const InputMapper, delta_time: f32) void { // Mouse look @@ -126,6 +151,23 @@ pub const Camera = struct { return Mat4.perspective(self.fov, aspect_ratio, self.near, self.far); } + pub fn getProjectionMatrixReverseZ(self: *const Camera, aspect_ratio: f32) Mat4 { + return Mat4.perspectiveReverseZ(self.fov, aspect_ratio, self.near, self.far); + } + + pub fn getJitteredProjectionMatrixReverseZ(self: *const Camera, aspect_ratio: f32, viewport_width: f32, viewport_height: f32, jitter_enabled: bool) Mat4 { + const base_projection = self.getProjectionMatrixReverseZ(aspect_ratio); + if (!jitter_enabled or viewport_width <= 0.0 or viewport_height <= 0.0) { + return base_projection; + } + + const jitter = self.currentJitterPixel(jitter_enabled); + const jitter_x_ndc = (jitter[0] * 2.0) / viewport_width; + const jitter_y_ndc = (jitter[1] * 2.0) / viewport_height; + const jitter_matrix = Mat4.translate(Vec3.init(jitter_x_ndc, jitter_y_ndc, 0.0)); + return jitter_matrix.multiply(base_projection); + } + /// Get view matrix centered at origin (for floating origin rendering) /// Camera is conceptually at origin looking in the forward direction pub fn getViewMatrixOriginCentered(self: *const Camera) Mat4 { diff --git a/src/engine/graphics/render_graph.zig b/src/engine/graphics/render_graph.zig index 216b10f..bc8f8fd 100644 --- a/src/engine/graphics/render_graph.zig +++ b/src/engine/graphics/render_graph.zig @@ -5,7 +5,6 @@ const World = @import("../../world/world.zig").World; const shadow_scene = @import("shadow_scene.zig"); const RHI = @import("rhi.zig").RHI; const rhi_pkg = @import("rhi.zig"); -const Mat4 = @import("../math/mat4.zig").Mat4; const Vec3 = @import("../math/vec3.zig").Vec3; const log = @import("../core/log.zig"); const CSM = @import("csm.zig"); @@ -22,6 +21,9 @@ pub const SceneContext = struct { aspect: f32, sky_params: rhi_pkg.SkyParams, cloud_params: rhi_pkg.CloudParams, + taa_enabled: bool, + viewport_width: f32, + viewport_height: f32, main_shader: rhi_pkg.ShaderHandle, env_map_handle: rhi_pkg.TextureHandle, shadow: rhi_pkg.ShadowConfig, @@ -212,7 +214,7 @@ pub const GPass = struct { ctx.rhi.beginGPass(); const atlas = ctx.material_system.getAtlasHandles(ctx.env_map_handle); ctx.rhi.bindTexture(atlas.diffuse, 1); - const view_proj = Mat4.perspectiveReverseZ(ctx.camera.fov, ctx.aspect, ctx.camera.near, ctx.camera.far).multiply(ctx.camera.getViewMatrixOriginCentered()); + const view_proj = ctx.camera.getJitteredProjectionMatrixReverseZ(ctx.aspect, ctx.viewport_width, ctx.viewport_height, ctx.taa_enabled).multiply(ctx.camera.getViewMatrixOriginCentered()); ctx.world.render(view_proj, ctx.camera.position, false); ctx.rhi.endGPass(); } @@ -234,7 +236,7 @@ pub const SSAOPass = struct { fn execute(ptr: *anyopaque, ctx: SceneContext) anyerror!void { _ = ptr; if (!ctx.ssao_enabled or ctx.disable_ssao) return; - const proj = Mat4.perspectiveReverseZ(ctx.camera.fov, ctx.aspect, ctx.camera.near, ctx.camera.far); + const proj = ctx.camera.getJitteredProjectionMatrixReverseZ(ctx.aspect, ctx.viewport_width, ctx.viewport_height, ctx.taa_enabled); const inv_proj = proj.inverse(); ctx.rhi.ssao().compute(proj, inv_proj); } @@ -288,7 +290,7 @@ pub const OpaquePass = struct { rhi.bindTexture(ctx.lpv_texture_handle, 11); rhi.bindTexture(ctx.lpv_texture_handle_g, 12); rhi.bindTexture(ctx.lpv_texture_handle_b, 13); - const view_proj = Mat4.perspectiveReverseZ(ctx.camera.fov, ctx.aspect, ctx.camera.near, ctx.camera.far).multiply(ctx.camera.getViewMatrixOriginCentered()); + const view_proj = ctx.camera.getJitteredProjectionMatrixReverseZ(ctx.aspect, ctx.viewport_width, ctx.viewport_height, ctx.taa_enabled).multiply(ctx.camera.getViewMatrixOriginCentered()); ctx.world.render(view_proj, ctx.camera.position, true); } }; @@ -309,7 +311,7 @@ pub const CloudPass = struct { fn execute(ptr: *anyopaque, ctx: SceneContext) anyerror!void { _ = ptr; if (ctx.disable_clouds) return; - const view_proj = Mat4.perspectiveReverseZ(ctx.camera.fov, ctx.aspect, ctx.camera.near, ctx.camera.far).multiply(ctx.camera.getViewMatrixOriginCentered()); + const view_proj = ctx.camera.getJitteredProjectionMatrixReverseZ(ctx.aspect, ctx.viewport_width, ctx.viewport_height, ctx.taa_enabled).multiply(ctx.camera.getViewMatrixOriginCentered()); ctx.atmosphere_system.renderClouds(ctx.cloud_params, view_proj) catch |err| { if (err != error.ResourceNotReady and err != error.CloudPipelineNotReady and @@ -386,6 +388,28 @@ pub const BloomPass = struct { } }; +// TAA pass - reserved temporal AA stage between scene rendering and bloom/post. +pub const TAAPass = struct { + enabled: bool = true, + const VTABLE = IRenderPass.VTable{ + .name = "TAAPass", + .needs_main_pass = false, + .execute = execute, + }; + pub fn pass(self: *TAAPass) IRenderPass { + return .{ + .ptr = self, + .vtable = &VTABLE, + }; + } + + fn execute(ptr: *anyopaque, ctx: SceneContext) anyerror!void { + const self: *TAAPass = @ptrCast(@alignCast(ptr)); + if (!self.enabled or !ctx.taa_enabled) return; + ctx.rhi.computeTAA(); + } +}; + // FXAA pass - applies anti-aliasing to LDR output pub const FXAAPass = struct { enabled: bool = true, diff --git a/src/engine/graphics/rhi.zig b/src/engine/graphics/rhi.zig index 9fc1ea8..362e10f 100644 --- a/src/engine/graphics/rhi.zig +++ b/src/engine/graphics/rhi.zig @@ -286,6 +286,8 @@ pub const IRenderContext = struct { endFXAAPass: *const fn (ptr: *anyopaque) void, // Bloom pass computeBloom: *const fn (ptr: *anyopaque) void, + // TAA pass + computeTAA: *const fn (ptr: *anyopaque) void, getEncoder: *const fn (ptr: *anyopaque) IGraphicsCommandEncoder, getStateContext: *const fn (ptr: *anyopaque) IRenderStateContext, @@ -347,6 +349,9 @@ pub const IRenderContext = struct { pub fn computeBloom(self: IRenderContext) void { self.vtable.computeBloom(self.ptr); } + pub fn computeTAA(self: IRenderContext) void { + self.vtable.computeTAA(self.ptr); + } pub fn getEncoder(self: IRenderContext) IGraphicsCommandEncoder { return self.vtable.getEncoder(self.ptr); } @@ -482,6 +487,8 @@ pub const RHI = struct { setFilmGrainIntensity: *const fn (ctx: *anyopaque, intensity: f32) void, setColorGradingEnabled: *const fn (ctx: *anyopaque, enabled: bool) void, setColorGradingIntensity: *const fn (ctx: *anyopaque, intensity: f32) void, + setTAABlendFactor: *const fn (ctx: *anyopaque, value: f32) void, + setTAAVelocityRejection: *const fn (ctx: *anyopaque, value: f32) void, }; pub fn factory(self: RHI) IResourceFactory { @@ -672,6 +679,9 @@ pub const RHI = struct { pub fn computeBloom(self: RHI) void { self.vtable.render.computeBloom(self.ptr); } + pub fn computeTAA(self: RHI) void { + self.vtable.render.computeTAA(self.ptr); + } pub fn setTextureUniforms(self: RHI, enabled: bool, handles: [SHADOW_CASCADE_COUNT]TextureHandle) void { self.vtable.render.setTextureUniforms(self.ptr, enabled, handles); } @@ -734,4 +744,10 @@ pub const RHI = struct { pub fn setColorGradingIntensity(self: RHI, intensity: f32) void { self.vtable.setColorGradingIntensity(self.ptr, intensity); } + pub fn setTAABlendFactor(self: RHI, value: f32) void { + self.vtable.setTAABlendFactor(self.ptr, value); + } + pub fn setTAAVelocityRejection(self: RHI, value: f32) void { + self.vtable.setTAAVelocityRejection(self.ptr, value); + } }; diff --git a/src/engine/graphics/rhi_tests.zig b/src/engine/graphics/rhi_tests.zig index a497199..c9f75d7 100644 --- a/src/engine/graphics/rhi_tests.zig +++ b/src/engine/graphics/rhi_tests.zig @@ -178,6 +178,7 @@ const MockContext = struct { .beginFXAAPass = undefined, .endFXAAPass = undefined, .computeBloom = undefined, + .computeTAA = undefined, .getEncoder = MockContext.getEncoder, .getStateContext = MockContext.getStateContext, .setClearColor = undefined, @@ -340,6 +341,8 @@ const MockContext = struct { .setFilmGrainIntensity = undefined, .setColorGradingEnabled = undefined, .setColorGradingIntensity = undefined, + .setTAABlendFactor = undefined, + .setTAAVelocityRejection = undefined, }; const MOCK_ENCODER_VTABLE = rhi.IGraphicsCommandEncoder.VTable{ diff --git a/src/engine/graphics/rhi_vulkan.zig b/src/engine/graphics/rhi_vulkan.zig index 5a6855c..c1ff739 100644 --- a/src/engine/graphics/rhi_vulkan.zig +++ b/src/engine/graphics/rhi_vulkan.zig @@ -173,11 +173,41 @@ fn computeBloom(ctx_ptr: *anyopaque) void { if (!ctx.frames.frame_in_progress) return; pass_orchestration.ensureNoRenderPassActiveInternal(ctx); + var bloom_source_image = ctx.hdr.hdr_image; + if (ctx.taa.ran_this_frame and ctx.taa.output_texture != 0) { + if (ctx.resources.textures.get(ctx.taa.output_texture)) |tex| { + if (tex.image) |img| { + bloom_source_image = img; + } + } + } + const command_buffer = ctx.frames.command_buffers[ctx.frames.current_frame]; ctx.bloom.compute( command_buffer, ctx.frames.current_frame, - ctx.hdr.hdr_image, + bloom_source_image, + ctx.swapchain.getExtent(), + &ctx.runtime.draw_call_count, + ); +} + +fn computeTAA(ctx_ptr: *anyopaque) void { + const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); + ctx.mutex.lock(); + defer ctx.mutex.unlock(); + if (!ctx.frames.frame_in_progress) return; + if (!ctx.taa.enabled) return; + pass_orchestration.ensureNoRenderPassActiveInternal(ctx); + + const command_buffer = ctx.frames.command_buffers[ctx.frames.current_frame]; + ctx.taa.compute( + ctx.vulkan_device.vk_device, + command_buffer, + ctx.frames.current_frame, + &ctx.resources, + ctx.hdr.hdr_view, + ctx.velocity.velocity_view, ctx.swapchain.getExtent(), &ctx.runtime.draw_call_count, ); @@ -228,6 +258,16 @@ fn setColorGradingIntensity(ctx_ptr: *anyopaque, intensity: f32) void { ctx.post_process_state.color_grading_intensity = intensity; } +fn setTAABlendFactor(ctx_ptr: *anyopaque, value: f32) void { + const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); + ctx.taa.blend_factor = std.math.clamp(value, 0.0, 0.98); +} + +fn setTAAVelocityRejection(ctx_ptr: *anyopaque, value: f32) void { + const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); + ctx.taa.velocity_rejection = std.math.clamp(value, 0.0, 0.25); +} + fn endFrame(ctx_ptr: *anyopaque) void { const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); ctx.mutex.lock(); @@ -703,6 +743,7 @@ const VULKAN_RHI_VTABLE = rhi.RHI.VTable{ .beginFXAAPass = beginFXAAPass, .endFXAAPass = endFXAAPass, .computeBloom = computeBloom, + .computeTAA = computeTAA, .getEncoder = getEncoder, .getStateContext = getStateContext, .getNativeSkyPipeline = getNativeSkyPipeline, @@ -753,6 +794,8 @@ const VULKAN_RHI_VTABLE = rhi.RHI.VTable{ .setFilmGrainIntensity = setFilmGrainIntensity, .setColorGradingEnabled = setColorGradingEnabled, .setColorGradingIntensity = setColorGradingIntensity, + .setTAABlendFactor = setTAABlendFactor, + .setTAAVelocityRejection = setTAAVelocityRejection, }; fn beginPassTiming(ctx_ptr: *anyopaque, pass_name: []const u8) void { diff --git a/src/engine/graphics/vulkan/post_process_system.zig b/src/engine/graphics/vulkan/post_process_system.zig index 36b3eaa..1e87c7e 100644 --- a/src/engine/graphics/vulkan/post_process_system.zig +++ b/src/engine/graphics/vulkan/post_process_system.zig @@ -198,6 +198,27 @@ pub const PostProcessSystem = struct { } } + pub fn updateSourceDescriptors(self: *PostProcessSystem, vk: c.VkDevice, source_view: c.VkImageView, source_sampler: c.VkSampler) void { + for (0..rhi.MAX_FRAMES_IN_FLIGHT) |i| { + if (self.descriptor_sets[i] == null) continue; + + var source_image_info = std.mem.zeroes(c.VkDescriptorImageInfo); + source_image_info.imageLayout = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + source_image_info.imageView = source_view; + source_image_info.sampler = source_sampler; + + var write = std.mem.zeroes(c.VkWriteDescriptorSet); + write.sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = self.descriptor_sets[i]; + write.dstBinding = 0; + write.descriptorType = c.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.descriptorCount = 1; + write.pImageInfo = &source_image_info; + + c.vkUpdateDescriptorSets(vk, 1, &write, 0, null); + } + } + pub fn updateLUTDescriptor(self: *PostProcessSystem, vk: c.VkDevice, lut_view: c.VkImageView, lut_sampler: c.VkSampler) void { for (0..rhi.MAX_FRAMES_IN_FLIGHT) |i| { if (self.descriptor_sets[i] == null) continue; diff --git a/src/engine/graphics/vulkan/rhi_context_types.zig b/src/engine/graphics/vulkan/rhi_context_types.zig index 1e7ec1a..2b5b0a1 100644 --- a/src/engine/graphics/vulkan/rhi_context_types.zig +++ b/src/engine/graphics/vulkan/rhi_context_types.zig @@ -18,6 +18,7 @@ const SSAOSystem = @import("ssao_system.zig").SSAOSystem; const PostProcessSystem = @import("post_process_system.zig").PostProcessSystem; const FXAASystem = @import("fxaa_system.zig").FXAASystem; const BloomSystem = @import("bloom_system.zig").BloomSystem; +const TAASystem = @import("taa_system.zig").TAASystem; const VulkanDevice = @import("device.zig").VulkanDevice; const MAX_FRAMES_IN_FLIGHT = rhi.MAX_FRAMES_IN_FLIGHT; @@ -208,6 +209,7 @@ pub const VulkanContext = struct { post_process: PostProcessSystem = .{}, debug_shadow: DebugShadowResources = .{}, fxaa: FXAASystem = .{}, + taa: TAASystem = .{}, bloom: BloomSystem = .{}, post_process_state: PostProcessState = .{}, velocity: VelocityResources = .{}, diff --git a/src/engine/graphics/vulkan/rhi_frame_orchestration.zig b/src/engine/graphics/vulkan/rhi_frame_orchestration.zig index 232a299..c943027 100644 --- a/src/engine/graphics/vulkan/rhi_frame_orchestration.zig +++ b/src/engine/graphics/vulkan/rhi_frame_orchestration.zig @@ -17,6 +17,7 @@ pub fn recreateSwapchainInternal(ctx: anytype) void { setup.destroyMainRenderPassAndPipelines(ctx); lifecycle.destroyHDRResources(ctx); lifecycle.destroyFXAAResources(ctx); + lifecycle.destroyTAAResources(ctx); lifecycle.destroyBloomResources(ctx); lifecycle.destroyPostProcessResources(ctx); lifecycle.destroyGPassResources(ctx); @@ -43,11 +44,15 @@ pub fn recreateSwapchainInternal(ctx: anytype) void { std.log.err("Failed to recreate SSAO resources: {}", .{err}); return; }; - ctx.render_pass_manager.createMainRenderPass(ctx.vulkan_device.vk_device, ctx.swapchain.getExtent(), ctx.options.msaa_samples) catch |err| { + setup.createTAAResources(ctx) catch |err| { + std.log.err("Failed to recreate TAA resources: {}", .{err}); + return; + }; + ctx.render_pass_manager.createMainRenderPass(ctx.vulkan_device.vk_device, ctx.swapchain.getExtent(), 1) catch |err| { std.log.err("Failed to recreate render pass: {}", .{err}); return; }; - ctx.pipeline_manager.createMainPipelines(ctx.allocator, ctx.vulkan_device.vk_device, ctx.render_pass_manager.hdr_render_pass, ctx.render_pass_manager.g_render_pass, ctx.options.msaa_samples) catch |err| { + ctx.pipeline_manager.createMainPipelines(ctx.allocator, ctx.vulkan_device.vk_device, ctx.render_pass_manager.hdr_render_pass, ctx.render_pass_manager.g_render_pass, 1) catch |err| { std.log.err("Failed to recreate pipelines: {}", .{err}); return; }; @@ -115,6 +120,7 @@ pub fn prepareFrameState(ctx: anytype) void { ctx.shadow_system.pass_active = false; ctx.runtime.post_process_ran_this_frame = false; ctx.runtime.fxaa_ran_this_frame = false; + ctx.taa.ran_this_frame = false; ctx.ui.ui_using_swapchain = false; ctx.draw.terrain_pipeline_bound = false; diff --git a/src/engine/graphics/vulkan/rhi_init_deinit.zig b/src/engine/graphics/vulkan/rhi_init_deinit.zig index 3924279..47eda8d 100644 --- a/src/engine/graphics/vulkan/rhi_init_deinit.zig +++ b/src/engine/graphics/vulkan/rhi_init_deinit.zig @@ -77,11 +77,12 @@ pub fn initContext(ctx: anytype, allocator: std.mem.Allocator, render_device: ?* try lifecycle.createHDRResources(ctx); try setup.createGPassResources(ctx); try setup.createSSAOResources(ctx); + try setup.createTAAResources(ctx); try ctx.render_pass_manager.createMainRenderPass( ctx.vulkan_device.vk_device, ctx.swapchain.getExtent(), - ctx.options.msaa_samples, + 1, ); try ctx.pipeline_manager.createMainPipelines( @@ -89,7 +90,7 @@ pub fn initContext(ctx: anytype, allocator: std.mem.Allocator, render_device: ?* ctx.vulkan_device.vk_device, ctx.render_pass_manager.hdr_render_pass, ctx.render_pass_manager.g_render_pass, - ctx.options.msaa_samples, + 1, ); try setup.createPostProcessResources(ctx); @@ -206,6 +207,7 @@ pub fn deinit(ctx: anytype) void { lifecycle.destroyHDRResources(ctx); lifecycle.destroyFXAAResources(ctx); + lifecycle.destroyTAAResources(ctx); lifecycle.destroyBloomResources(ctx); lifecycle.destroyVelocityResources(ctx); lifecycle.destroyPostProcessResources(ctx); diff --git a/src/engine/graphics/vulkan/rhi_pass_orchestration.zig b/src/engine/graphics/vulkan/rhi_pass_orchestration.zig index e11a1cc..5c41f66 100644 --- a/src/engine/graphics/vulkan/rhi_pass_orchestration.zig +++ b/src/engine/graphics/vulkan/rhi_pass_orchestration.zig @@ -190,7 +190,7 @@ pub fn beginMainPassInternal(ctx: anytype) void { if (ctx.swapchain.getExtent().width == 0 or ctx.swapchain.getExtent().height == 0) return; if (ctx.render_pass_manager.hdr_render_pass == null) { - ctx.render_pass_manager.createMainRenderPass(ctx.vulkan_device.vk_device, ctx.swapchain.getExtent(), ctx.options.msaa_samples) catch |err| { + ctx.render_pass_manager.createMainRenderPass(ctx.vulkan_device.vk_device, ctx.swapchain.getExtent(), 1) catch |err| { std.log.err("beginMainPass: failed to recreate render pass: {}", .{err}); return; }; @@ -231,19 +231,12 @@ pub fn beginMainPassInternal(ctx: anytype) void { render_pass_info.renderArea.offset = .{ .x = 0, .y = 0 }; render_pass_info.renderArea.extent = ctx.swapchain.getExtent(); - var clear_values: [3]c.VkClearValue = undefined; + var clear_values: [2]c.VkClearValue = undefined; clear_values[0] = std.mem.zeroes(c.VkClearValue); clear_values[0].color = .{ .float32 = ctx.runtime.clear_color }; clear_values[1] = std.mem.zeroes(c.VkClearValue); clear_values[1].depthStencil = .{ .depth = 0.0, .stencil = 0 }; - - if (ctx.options.msaa_samples > 1) { - clear_values[2] = std.mem.zeroes(c.VkClearValue); - clear_values[2].color = .{ .float32 = ctx.runtime.clear_color }; - render_pass_info.clearValueCount = 3; - } else { - render_pass_info.clearValueCount = 2; - } + render_pass_info.clearValueCount = 2; render_pass_info.pClearValues = &clear_values[0]; c.vkCmdBeginRenderPass(command_buffer, &render_pass_info, c.VK_SUBPASS_CONTENTS_INLINE); @@ -314,6 +307,16 @@ pub fn beginPostProcessPassInternal(ctx: anytype) void { c.vkCmdBindPipeline(command_buffer, c.VK_PIPELINE_BIND_POINT_GRAPHICS, ctx.post_process.pipeline); + var source_view = ctx.hdr.hdr_view; + var source_sampler = ctx.post_process.sampler; + if (ctx.taa.ran_this_frame and ctx.taa.output_texture != 0) { + if (ctx.resources.textures.get(ctx.taa.output_texture)) |tex| { + source_view = tex.view; + source_sampler = tex.sampler; + } + } + ctx.post_process.updateSourceDescriptors(ctx.vulkan_device.vk_device, source_view, source_sampler); + const pp_ds = ctx.post_process.descriptor_sets[ctx.frames.current_frame]; if (pp_ds == null) { std.log.err("Post-process descriptor set is null for frame {}", .{ctx.frames.current_frame}); diff --git a/src/engine/graphics/vulkan/rhi_resource_lifecycle.zig b/src/engine/graphics/vulkan/rhi_resource_lifecycle.zig index 66624a0..4f133f7 100644 --- a/src/engine/graphics/vulkan/rhi_resource_lifecycle.zig +++ b/src/engine/graphics/vulkan/rhi_resource_lifecycle.zig @@ -131,6 +131,10 @@ pub fn destroyBloomResources(ctx: anytype) void { ctx.bloom.deinit(ctx.vulkan_device.vk_device, ctx.allocator, ctx.descriptors.descriptor_pool); } +pub fn destroyTAAResources(ctx: anytype) void { + ctx.taa.deinit(ctx.vulkan_device.vk_device, ctx.descriptors.descriptor_pool, &ctx.resources); +} + pub fn destroyVelocityResources(ctx: anytype) void { const vk = ctx.vulkan_device.vk_device; if (vk == null) return; @@ -192,19 +196,9 @@ pub fn transitionImagesToShaderRead(ctx: anytype, images: []const c.VkImage, is_ c.vkFreeCommandBuffers(ctx.vulkan_device.vk_device, ctx.frames.command_pool, 1, &cmd); } -fn getMSAASampleCountFlag(samples: u8) c.VkSampleCountFlagBits { - return switch (samples) { - 2 => c.VK_SAMPLE_COUNT_2_BIT, - 4 => c.VK_SAMPLE_COUNT_4_BIT, - 8 => c.VK_SAMPLE_COUNT_8_BIT, - else => c.VK_SAMPLE_COUNT_1_BIT, - }; -} - pub fn createHDRResources(ctx: anytype) !void { const extent = ctx.swapchain.getExtent(); const format = c.VK_FORMAT_R16G16B16A16_SFLOAT; - const sample_count = getMSAASampleCountFlag(ctx.options.msaa_samples); var image_info = std.mem.zeroes(c.VkImageCreateInfo); image_info.sType = c.VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; @@ -237,18 +231,4 @@ pub fn createHDRResources(ctx: anytype) !void { view_info.format = format; view_info.subresourceRange = .{ .aspectMask = c.VK_IMAGE_ASPECT_COLOR_BIT, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1 }; try Utils.checkVk(c.vkCreateImageView(ctx.vulkan_device.vk_device, &view_info, null, &ctx.hdr.hdr_view)); - - if (ctx.options.msaa_samples > 1) { - image_info.samples = sample_count; - image_info.usage = c.VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | c.VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; - try Utils.checkVk(c.vkCreateImage(ctx.vulkan_device.vk_device, &image_info, null, &ctx.hdr.hdr_msaa_image)); - c.vkGetImageMemoryRequirements(ctx.vulkan_device.vk_device, ctx.hdr.hdr_msaa_image, &mem_reqs); - alloc_info.allocationSize = mem_reqs.size; - alloc_info.memoryTypeIndex = try Utils.findMemoryType(ctx.vulkan_device.physical_device, mem_reqs.memoryTypeBits, c.VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - try Utils.checkVk(c.vkAllocateMemory(ctx.vulkan_device.vk_device, &alloc_info, null, &ctx.hdr.hdr_msaa_memory)); - try Utils.checkVk(c.vkBindImageMemory(ctx.vulkan_device.vk_device, ctx.hdr.hdr_msaa_image, ctx.hdr.hdr_msaa_memory, 0)); - - view_info.image = ctx.hdr.hdr_msaa_image; - try Utils.checkVk(c.vkCreateImageView(ctx.vulkan_device.vk_device, &view_info, null, &ctx.hdr.hdr_msaa_view)); - } } diff --git a/src/engine/graphics/vulkan/rhi_resource_setup.zig b/src/engine/graphics/vulkan/rhi_resource_setup.zig index 961057b..50a7c5a 100644 --- a/src/engine/graphics/vulkan/rhi_resource_setup.zig +++ b/src/engine/graphics/vulkan/rhi_resource_setup.zig @@ -373,6 +373,16 @@ pub fn createSSAOResources(ctx: anytype) !void { try lifecycle.transitionImagesToShaderRead(ctx, &ssao_images, false); } +pub fn createTAAResources(ctx: anytype) !void { + try ctx.taa.ensureResources( + ctx.vulkan_device.vk_device, + ctx.allocator, + ctx.descriptors.descriptor_pool, + &ctx.resources, + ctx.swapchain.getExtent(), + ); +} + pub fn createPostProcessResources(ctx: anytype) !void { const vk = ctx.vulkan_device.vk_device; @@ -453,9 +463,9 @@ pub fn createMainFramebuffers(ctx: anytype) !void { ctx.vulkan_device.vk_device, ctx.swapchain.getExtent(), ctx.hdr.hdr_view, - if (ctx.options.msaa_samples > 1) ctx.hdr.hdr_msaa_view else null, + null, ctx.swapchain.swapchain.depth_image_view, - ctx.options.msaa_samples, + 1, ); } diff --git a/src/engine/graphics/vulkan/shader_registry.zig b/src/engine/graphics/vulkan/shader_registry.zig index 37de764..d0d21d3 100644 --- a/src/engine/graphics/vulkan/shader_registry.zig +++ b/src/engine/graphics/vulkan/shader_registry.zig @@ -12,6 +12,9 @@ pub const FXAA_FRAG = "assets/shaders/vulkan/fxaa.frag.spv"; pub const POST_PROCESS_VERT = "assets/shaders/vulkan/post_process.vert.spv"; pub const POST_PROCESS_FRAG = "assets/shaders/vulkan/post_process.frag.spv"; +pub const TAA_VERT = "assets/shaders/vulkan/post_process.vert.spv"; +pub const TAA_FRAG = "assets/shaders/vulkan/taa.frag.spv"; + pub const SHADOW_VERT = "assets/shaders/vulkan/shadow.vert.spv"; pub const SHADOW_FRAG = "assets/shaders/vulkan/shadow.frag.spv"; diff --git a/src/engine/graphics/vulkan/taa_system.zig b/src/engine/graphics/vulkan/taa_system.zig new file mode 100644 index 0000000..bc1e4b5 --- /dev/null +++ b/src/engine/graphics/vulkan/taa_system.zig @@ -0,0 +1,397 @@ +const std = @import("std"); +const c = @import("../../../c.zig").c; +const rhi = @import("../rhi.zig"); +const Utils = @import("utils.zig"); +const shader_registry = @import("shader_registry.zig"); + +pub const TAAPushConstants = extern struct { + blend_factor: f32, + velocity_rejection: f32, + reset_history: f32, + _pad: f32, +}; + +pub const TAASystem = struct { + enabled: bool = true, + pass_active: bool = false, + ran_this_frame: bool = false, + history_valid: bool = false, + blend_factor: f32 = 0.9, + velocity_rejection: f32 = 0.02, + + render_pass: c.VkRenderPass = null, + pipeline: c.VkPipeline = null, + pipeline_layout: c.VkPipelineLayout = null, + descriptor_set_layout: c.VkDescriptorSetLayout = null, + descriptor_sets: [rhi.MAX_FRAMES_IN_FLIGHT]c.VkDescriptorSet = .{null} ** rhi.MAX_FRAMES_IN_FLIGHT, + sampler: c.VkSampler = null, + + history_textures: [2]rhi.TextureHandle = .{ 0, 0 }, + output_texture: rhi.TextureHandle = 0, + framebuffers: [2]c.VkFramebuffer = .{ null, null }, + extent: c.VkExtent2D = .{ .width = 0, .height = 0 }, + history_index: usize = 0, + + pub fn ensureResources( + self: *TAASystem, + vk: c.VkDevice, + allocator: std.mem.Allocator, + descriptor_pool: c.VkDescriptorPool, + resources: anytype, + extent: c.VkExtent2D, + ) !void { + if (extent.width == 0 or extent.height == 0) return; + + try self.ensureRenderState(vk, allocator, descriptor_pool); + + if (self.extent.width == extent.width and self.extent.height == extent.height and self.history_textures[0] != 0 and self.history_textures[1] != 0) { + return; + } + + self.destroyFramebuffers(vk); + self.destroyHistoryTextures(resources); + + const config = rhi.TextureConfig{ + .min_filter = .linear, + .mag_filter = .linear, + .wrap_s = .clamp_to_edge, + .wrap_t = .clamp_to_edge, + .generate_mipmaps = false, + .is_render_target = true, + }; + + self.history_textures[0] = try resources.createTexture(extent.width, extent.height, .rgba32f, config, null); + errdefer self.destroyHistoryTextures(resources); + self.history_textures[1] = try resources.createTexture(extent.width, extent.height, .rgba32f, config, null); + + try self.createFramebuffers(vk, resources, extent); + + self.extent = extent; + self.history_index = 0; + self.history_valid = false; + self.output_texture = self.history_textures[0]; + } + + fn ensureRenderState(self: *TAASystem, vk: c.VkDevice, allocator: std.mem.Allocator, descriptor_pool: c.VkDescriptorPool) !void { + if (self.render_pass == null) { + var color_attachment = std.mem.zeroes(c.VkAttachmentDescription); + color_attachment.format = c.VK_FORMAT_R32G32B32A32_SFLOAT; + color_attachment.samples = c.VK_SAMPLE_COUNT_1_BIT; + color_attachment.loadOp = c.VK_ATTACHMENT_LOAD_OP_CLEAR; + color_attachment.storeOp = c.VK_ATTACHMENT_STORE_OP_STORE; + color_attachment.initialLayout = c.VK_IMAGE_LAYOUT_UNDEFINED; + color_attachment.finalLayout = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + var color_ref = c.VkAttachmentReference{ .attachment = 0, .layout = c.VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; + var subpass = std.mem.zeroes(c.VkSubpassDescription); + subpass.pipelineBindPoint = c.VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &color_ref; + + var dependency = std.mem.zeroes(c.VkSubpassDependency); + dependency.srcSubpass = c.VK_SUBPASS_EXTERNAL; + dependency.dstSubpass = 0; + dependency.srcStageMask = c.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + dependency.dstStageMask = c.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dependency.srcAccessMask = c.VK_ACCESS_SHADER_READ_BIT; + dependency.dstAccessMask = c.VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + + var rp_info = std.mem.zeroes(c.VkRenderPassCreateInfo); + rp_info.sType = c.VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + rp_info.attachmentCount = 1; + rp_info.pAttachments = &color_attachment; + rp_info.subpassCount = 1; + rp_info.pSubpasses = &subpass; + rp_info.dependencyCount = 1; + rp_info.pDependencies = &dependency; + + try Utils.checkVk(c.vkCreateRenderPass(vk, &rp_info, null, &self.render_pass)); + } + + if (self.sampler == null) { + var sampler_info = std.mem.zeroes(c.VkSamplerCreateInfo); + sampler_info.sType = c.VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + sampler_info.magFilter = c.VK_FILTER_LINEAR; + sampler_info.minFilter = c.VK_FILTER_LINEAR; + sampler_info.addressModeU = c.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sampler_info.addressModeV = c.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sampler_info.addressModeW = c.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sampler_info.mipmapMode = c.VK_SAMPLER_MIPMAP_MODE_LINEAR; + try Utils.checkVk(c.vkCreateSampler(vk, &sampler_info, null, &self.sampler)); + } + + if (self.descriptor_set_layout == null) { + var layout_bindings = [_]c.VkDescriptorSetLayoutBinding{ + .{ .binding = 0, .descriptorType = c.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = 1, .stageFlags = c.VK_SHADER_STAGE_FRAGMENT_BIT }, + .{ .binding = 1, .descriptorType = c.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = 1, .stageFlags = c.VK_SHADER_STAGE_FRAGMENT_BIT }, + .{ .binding = 2, .descriptorType = c.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = 1, .stageFlags = c.VK_SHADER_STAGE_FRAGMENT_BIT }, + }; + + var layout_info = std.mem.zeroes(c.VkDescriptorSetLayoutCreateInfo); + layout_info.sType = c.VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + layout_info.bindingCount = layout_bindings.len; + layout_info.pBindings = &layout_bindings; + try Utils.checkVk(c.vkCreateDescriptorSetLayout(vk, &layout_info, null, &self.descriptor_set_layout)); + } + + if (self.pipeline_layout == null) { + var push_constant = std.mem.zeroes(c.VkPushConstantRange); + push_constant.stageFlags = c.VK_SHADER_STAGE_FRAGMENT_BIT; + push_constant.offset = 0; + push_constant.size = @sizeOf(TAAPushConstants); + + var pipe_layout_info = std.mem.zeroes(c.VkPipelineLayoutCreateInfo); + pipe_layout_info.sType = c.VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + pipe_layout_info.setLayoutCount = 1; + pipe_layout_info.pSetLayouts = &self.descriptor_set_layout; + pipe_layout_info.pushConstantRangeCount = 1; + pipe_layout_info.pPushConstantRanges = &push_constant; + try Utils.checkVk(c.vkCreatePipelineLayout(vk, &pipe_layout_info, null, &self.pipeline_layout)); + } + + if (self.pipeline == null) { + const vert_code = try std.fs.cwd().readFileAlloc(shader_registry.TAA_VERT, allocator, @enumFromInt(1024 * 1024)); + defer allocator.free(vert_code); + const frag_code = try std.fs.cwd().readFileAlloc(shader_registry.TAA_FRAG, allocator, @enumFromInt(1024 * 1024)); + defer allocator.free(frag_code); + + const vert_module = try Utils.createShaderModule(vk, vert_code); + defer c.vkDestroyShaderModule(vk, vert_module, null); + const frag_module = try Utils.createShaderModule(vk, frag_code); + defer c.vkDestroyShaderModule(vk, frag_module, null); + + var stages = [_]c.VkPipelineShaderStageCreateInfo{ + .{ .sType = c.VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = c.VK_SHADER_STAGE_VERTEX_BIT, .module = vert_module, .pName = "main" }, + .{ .sType = c.VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = c.VK_SHADER_STAGE_FRAGMENT_BIT, .module = frag_module, .pName = "main" }, + }; + + var vertex_input = std.mem.zeroes(c.VkPipelineVertexInputStateCreateInfo); + vertex_input.sType = c.VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + + var input_assembly = std.mem.zeroes(c.VkPipelineInputAssemblyStateCreateInfo); + input_assembly.sType = c.VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; + input_assembly.topology = c.VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + + var viewport_state = std.mem.zeroes(c.VkPipelineViewportStateCreateInfo); + viewport_state.sType = c.VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + viewport_state.viewportCount = 1; + viewport_state.scissorCount = 1; + + var rasterizer = std.mem.zeroes(c.VkPipelineRasterizationStateCreateInfo); + rasterizer.sType = c.VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + rasterizer.lineWidth = 1.0; + rasterizer.cullMode = c.VK_CULL_MODE_NONE; + rasterizer.frontFace = c.VK_FRONT_FACE_COUNTER_CLOCKWISE; + + var multisampling = std.mem.zeroes(c.VkPipelineMultisampleStateCreateInfo); + multisampling.sType = c.VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + multisampling.rasterizationSamples = c.VK_SAMPLE_COUNT_1_BIT; + + var color_blend_attachment = std.mem.zeroes(c.VkPipelineColorBlendAttachmentState); + color_blend_attachment.colorWriteMask = c.VK_COLOR_COMPONENT_R_BIT | c.VK_COLOR_COMPONENT_G_BIT | c.VK_COLOR_COMPONENT_B_BIT | c.VK_COLOR_COMPONENT_A_BIT; + color_blend_attachment.blendEnable = c.VK_FALSE; + + var color_blending = std.mem.zeroes(c.VkPipelineColorBlendStateCreateInfo); + color_blending.sType = c.VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; + color_blending.attachmentCount = 1; + color_blending.pAttachments = &color_blend_attachment; + + var dynamic_states = [_]c.VkDynamicState{ c.VK_DYNAMIC_STATE_VIEWPORT, c.VK_DYNAMIC_STATE_SCISSOR }; + var dynamic_state = std.mem.zeroes(c.VkPipelineDynamicStateCreateInfo); + dynamic_state.sType = c.VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; + dynamic_state.dynamicStateCount = dynamic_states.len; + dynamic_state.pDynamicStates = &dynamic_states; + + var pipeline_info = std.mem.zeroes(c.VkGraphicsPipelineCreateInfo); + pipeline_info.sType = c.VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + pipeline_info.stageCount = stages.len; + pipeline_info.pStages = &stages[0]; + pipeline_info.pVertexInputState = &vertex_input; + pipeline_info.pInputAssemblyState = &input_assembly; + pipeline_info.pViewportState = &viewport_state; + pipeline_info.pRasterizationState = &rasterizer; + pipeline_info.pMultisampleState = &multisampling; + pipeline_info.pColorBlendState = &color_blending; + pipeline_info.pDynamicState = &dynamic_state; + pipeline_info.layout = self.pipeline_layout; + pipeline_info.renderPass = self.render_pass; + pipeline_info.subpass = 0; + + try Utils.checkVk(c.vkCreateGraphicsPipelines(vk, null, 1, &pipeline_info, null, &self.pipeline)); + } + + if (self.descriptor_sets[0] == null) { + var layouts: [rhi.MAX_FRAMES_IN_FLIGHT]c.VkDescriptorSetLayout = undefined; + for (0..rhi.MAX_FRAMES_IN_FLIGHT) |i| { + layouts[i] = self.descriptor_set_layout; + } + + var alloc_info = std.mem.zeroes(c.VkDescriptorSetAllocateInfo); + alloc_info.sType = c.VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + alloc_info.descriptorPool = descriptor_pool; + alloc_info.descriptorSetCount = rhi.MAX_FRAMES_IN_FLIGHT; + alloc_info.pSetLayouts = &layouts[0]; + try Utils.checkVk(c.vkAllocateDescriptorSets(vk, &alloc_info, &self.descriptor_sets[0])); + } + } + + fn createFramebuffers(self: *TAASystem, vk: c.VkDevice, resources: anytype, extent: c.VkExtent2D) !void { + for (0..2) |i| { + const tex = resources.textures.get(self.history_textures[i]) orelse return error.InvalidTexture; + + var fb_info = std.mem.zeroes(c.VkFramebufferCreateInfo); + fb_info.sType = c.VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fb_info.renderPass = self.render_pass; + fb_info.attachmentCount = 1; + fb_info.pAttachments = &tex.view; + fb_info.width = extent.width; + fb_info.height = extent.height; + fb_info.layers = 1; + try Utils.checkVk(c.vkCreateFramebuffer(vk, &fb_info, null, &self.framebuffers[i])); + } + } + + fn destroyFramebuffers(self: *TAASystem, vk: c.VkDevice) void { + for (0..2) |i| { + if (self.framebuffers[i] != null) { + c.vkDestroyFramebuffer(vk, self.framebuffers[i], null); + self.framebuffers[i] = null; + } + } + } + + fn destroyHistoryTextures(self: *TAASystem, resources: anytype) void { + for (self.history_textures) |handle| { + if (handle != 0) { + resources.destroyTexture(handle); + } + } + self.history_textures = .{ 0, 0 }; + self.output_texture = 0; + } + + pub fn compute( + self: *TAASystem, + vk: c.VkDevice, + command_buffer: c.VkCommandBuffer, + frame_index: usize, + resources: anytype, + hdr_view: c.VkImageView, + velocity_view: c.VkImageView, + extent: c.VkExtent2D, + draw_call_count: *u32, + ) void { + if (!self.enabled) return; + if (self.pipeline == null or self.pipeline_layout == null or self.render_pass == null) return; + if (hdr_view == null or velocity_view == null) return; + + const write_idx = self.history_index; + const read_idx = (self.history_index + 1) % 2; + if (self.framebuffers[write_idx] == null) return; + + const history_tex = resources.textures.get(self.history_textures[read_idx]) orelse return; + + var image_infos = [_]c.VkDescriptorImageInfo{ + .{ .sampler = self.sampler, .imageView = hdr_view, .imageLayout = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }, + .{ .sampler = self.sampler, .imageView = history_tex.view, .imageLayout = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }, + .{ .sampler = self.sampler, .imageView = velocity_view, .imageLayout = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }, + }; + + var writes = [_]c.VkWriteDescriptorSet{ + .{ .sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = self.descriptor_sets[frame_index], .dstBinding = 0, .descriptorType = c.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = 1, .pImageInfo = &image_infos[0] }, + .{ .sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = self.descriptor_sets[frame_index], .dstBinding = 1, .descriptorType = c.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = 1, .pImageInfo = &image_infos[1] }, + .{ .sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = self.descriptor_sets[frame_index], .dstBinding = 2, .descriptorType = c.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = 1, .pImageInfo = &image_infos[2] }, + }; + c.vkUpdateDescriptorSets(vk, writes.len, &writes[0], 0, null); + + var clear = std.mem.zeroes(c.VkClearValue); + clear.color.float32 = .{ 0.0, 0.0, 0.0, 1.0 }; + + var rp_begin = std.mem.zeroes(c.VkRenderPassBeginInfo); + rp_begin.sType = c.VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rp_begin.renderPass = self.render_pass; + rp_begin.framebuffer = self.framebuffers[write_idx]; + rp_begin.renderArea = .{ .offset = .{ .x = 0, .y = 0 }, .extent = extent }; + rp_begin.clearValueCount = 1; + rp_begin.pClearValues = &clear; + + c.vkCmdBeginRenderPass(command_buffer, &rp_begin, c.VK_SUBPASS_CONTENTS_INLINE); + self.pass_active = true; + + const viewport = c.VkViewport{ + .x = 0, + .y = 0, + .width = @floatFromInt(extent.width), + .height = @floatFromInt(extent.height), + .minDepth = 0.0, + .maxDepth = 1.0, + }; + c.vkCmdSetViewport(command_buffer, 0, 1, &viewport); + + const scissor = c.VkRect2D{ .offset = .{ .x = 0, .y = 0 }, .extent = extent }; + c.vkCmdSetScissor(command_buffer, 0, 1, &scissor); + + c.vkCmdBindPipeline(command_buffer, c.VK_PIPELINE_BIND_POINT_GRAPHICS, self.pipeline); + c.vkCmdBindDescriptorSets(command_buffer, c.VK_PIPELINE_BIND_POINT_GRAPHICS, self.pipeline_layout, 0, 1, &self.descriptor_sets[frame_index], 0, null); + + const push = TAAPushConstants{ + .blend_factor = self.blend_factor, + .velocity_rejection = self.velocity_rejection, + .reset_history = if (self.history_valid) 0.0 else 1.0, + ._pad = 0.0, + }; + c.vkCmdPushConstants(command_buffer, self.pipeline_layout, c.VK_SHADER_STAGE_FRAGMENT_BIT, 0, @sizeOf(TAAPushConstants), &push); + + c.vkCmdDraw(command_buffer, 3, 1, 0, 0); + draw_call_count.* += 1; + + c.vkCmdEndRenderPass(command_buffer); + self.pass_active = false; + + self.output_texture = self.history_textures[write_idx]; + self.history_index = read_idx; + self.history_valid = true; + self.ran_this_frame = true; + } + + pub fn deinit(self: *TAASystem, vk: c.VkDevice, descriptor_pool: c.VkDescriptorPool, resources: anytype) void { + self.destroyFramebuffers(vk); + self.destroyHistoryTextures(resources); + + if (descriptor_pool != null) { + for (0..rhi.MAX_FRAMES_IN_FLIGHT) |i| { + if (self.descriptor_sets[i] != null) { + _ = c.vkFreeDescriptorSets(vk, descriptor_pool, 1, &self.descriptor_sets[i]); + self.descriptor_sets[i] = null; + } + } + } + + if (self.pipeline != null) { + c.vkDestroyPipeline(vk, self.pipeline, null); + self.pipeline = null; + } + if (self.pipeline_layout != null) { + c.vkDestroyPipelineLayout(vk, self.pipeline_layout, null); + self.pipeline_layout = null; + } + if (self.descriptor_set_layout != null) { + c.vkDestroyDescriptorSetLayout(vk, self.descriptor_set_layout, null); + self.descriptor_set_layout = null; + } + if (self.sampler != null) { + c.vkDestroySampler(vk, self.sampler, null); + self.sampler = null; + } + if (self.render_pass != null) { + c.vkDestroyRenderPass(vk, self.render_pass, null); + self.render_pass = null; + } + + self.extent = .{ .width = 0, .height = 0 }; + self.history_index = 0; + self.history_valid = false; + self.ran_this_frame = false; + self.pass_active = false; + } +}; diff --git a/src/game/app.zig b/src/game/app.zig index 0bcf857..ca79589 100644 --- a/src/game/app.zig +++ b/src/game/app.zig @@ -56,6 +56,7 @@ pub const App = struct { opaque_pass: render_graph_pkg.OpaquePass, cloud_pass: render_graph_pkg.CloudPass, entity_pass: render_graph_pkg.EntityPass, + taa_pass: render_graph_pkg.TAAPass, bloom_pass: render_graph_pkg.BloomPass, post_process_pass: render_graph_pkg.PostProcessPass, fxaa_pass: render_graph_pkg.FXAAPass, @@ -249,6 +250,7 @@ pub const App = struct { .opaque_pass = .{}, .cloud_pass = .{}, .entity_pass = .{}, + .taa_pass = .{ .enabled = true }, .bloom_pass = .{ .enabled = true }, .post_process_pass = .{}, .fxaa_pass = .{ .enabled = true }, @@ -287,7 +289,7 @@ pub const App = struct { errdefer app.lpv_system.deinit(); // Sync FXAA and Bloom settings to RHI after initialization - app.rhi.setFXAA(settings.fxaa_enabled); + app.rhi.setFXAA(settings.fxaa_enabled and !settings.taa_enabled); app.rhi.setBloom(settings.bloom_enabled); app.rhi.setBloomIntensity(settings.bloom_intensity); @@ -310,6 +312,7 @@ pub const App = struct { try app.render_graph.addPass(app.opaque_pass.pass()); try app.render_graph.addPass(app.cloud_pass.pass()); try app.render_graph.addPass(app.entity_pass.pass()); + try app.render_graph.addPass(app.taa_pass.pass()); try app.render_graph.addPass(app.bloom_pass.pass()); try app.render_graph.addPass(app.post_process_pass.pass()); try app.render_graph.addPass(app.fxaa_pass.pass()); diff --git a/src/game/screens/graphics.zig b/src/game/screens/graphics.zig index e877e9e..5019eb5 100644 --- a/src/game/screens/graphics.zig +++ b/src/game/screens/graphics.zig @@ -99,8 +99,15 @@ pub const GraphicsScreen = struct { if (next_idx < settings_pkg.json_presets.graphics_presets.items.len) { settings_pkg.json_presets.apply(settings, next_idx); ctx.rhi.*.setAnisotropicFiltering(settings.anisotropic_filtering); - ctx.rhi.*.setMSAA(settings.msaa_samples); ctx.rhi.*.setTexturesEnabled(settings.textures_enabled); + ctx.rhi.*.setTAABlendFactor(settings.taa_blend_factor); + ctx.rhi.*.setTAAVelocityRejection(settings.taa_velocity_rejection); + if (settings.taa_enabled) { + settings.fxaa_enabled = false; + ctx.rhi.*.setFXAA(false); + } else { + ctx.rhi.*.setFXAA(settings.fxaa_enabled); + } } else { // Custom selected, nothing changes in values but UI label updates to CUSTOM (via getPresetIndex next frame) } @@ -114,6 +121,8 @@ pub const GraphicsScreen = struct { // Auto-generated UI from metadata inline for (comptime std.meta.declarations(Settings.metadata)) |decl| { + if (comptime std.mem.eql(u8, decl.name, "msaa_samples")) continue; + const meta = @field(Settings.metadata, decl.name); const val_ptr = &@field(settings, decl.name); const val_type = @TypeOf(val_ptr.*); @@ -206,16 +215,28 @@ pub const GraphicsScreen = struct { if (val_ptr.* != old_val) { if (std.mem.eql(u8, decl.name, "anisotropic_filtering")) { ctx.rhi.*.setAnisotropicFiltering(settings.anisotropic_filtering); - } else if (std.mem.eql(u8, decl.name, "msaa_samples")) { - ctx.rhi.*.setMSAA(settings.msaa_samples); } else if (std.mem.eql(u8, decl.name, "textures_enabled")) { ctx.rhi.*.setTexturesEnabled(settings.textures_enabled); } else if (std.mem.eql(u8, decl.name, "vsync")) { ctx.rhi.*.setVSync(settings.vsync); } else if (std.mem.eql(u8, decl.name, "volumetric_density")) { ctx.rhi.*.setVolumetricDensity(settings.volumetric_density); + } else if (std.mem.eql(u8, decl.name, "taa_enabled")) { + if (settings.taa_enabled) { + settings.fxaa_enabled = false; + ctx.rhi.*.setFXAA(false); + } + } else if (std.mem.eql(u8, decl.name, "taa_blend_factor")) { + ctx.rhi.*.setTAABlendFactor(settings.taa_blend_factor); + } else if (std.mem.eql(u8, decl.name, "taa_velocity_rejection")) { + ctx.rhi.*.setTAAVelocityRejection(settings.taa_velocity_rejection); } else if (std.mem.eql(u8, decl.name, "fxaa_enabled")) { - ctx.rhi.*.setFXAA(settings.fxaa_enabled); + if (settings.taa_enabled and settings.fxaa_enabled) { + settings.fxaa_enabled = false; + ctx.rhi.*.setFXAA(false); + } else { + ctx.rhi.*.setFXAA(settings.fxaa_enabled); + } } else if (std.mem.eql(u8, decl.name, "bloom_enabled")) { ctx.rhi.*.setBloom(settings.bloom_enabled); } else if (std.mem.eql(u8, decl.name, "bloom_intensity")) { diff --git a/src/game/screens/world.zig b/src/game/screens/world.zig index c190003..6dd5135 100644 --- a/src/game/screens/world.zig +++ b/src/game/screens/world.zig @@ -4,7 +4,6 @@ const Screen = @import("../screen.zig"); const IScreen = Screen.IScreen; const EngineContext = Screen.EngineContext; const GameSession = @import("../session.zig").GameSession; -const Mat4 = @import("../../engine/math/mat4.zig").Mat4; const Vec3 = @import("../../engine/math/vec3.zig").Vec3; const rhi_pkg = @import("../../engine/graphics/rhi.zig"); const render_graph_pkg = @import("../../engine/graphics/render_graph.zig"); @@ -138,7 +137,8 @@ pub const WorldScreen = struct { const screen_h: f32 = @floatFromInt(ctx.input.getWindowHeight()); const aspect = screen_w / screen_h; - const view_proj_render = Mat4.perspectiveReverseZ(camera.fov, aspect, camera.near, camera.far).multiply(camera.getViewMatrixOriginCentered()); + const taa_enabled = ctx.settings.taa_enabled; + const view_proj_render = camera.getJitteredProjectionMatrixReverseZ(aspect, screen_w, screen_h, taa_enabled).multiply(camera.getViewMatrixOriginCentered()); const sky_params = rhi_pkg.SkyParams{ .cam_pos = camera.position, @@ -228,6 +228,9 @@ pub const WorldScreen = struct { .atmosphere_system = ctx.atmosphere_system, .material_system = ctx.material_system, .aspect = aspect, + .taa_enabled = taa_enabled, + .viewport_width = screen_w, + .viewport_height = screen_h, .sky_params = sky_params, .cloud_params = cloud_params, .main_shader = ctx.shader, @@ -238,7 +241,7 @@ pub const WorldScreen = struct { .disable_gpass_draw = ctx.disable_gpass_draw, .disable_ssao = ctx.disable_ssao, .disable_clouds = ctx.disable_clouds, - .fxaa_enabled = ctx.settings.fxaa_enabled, + .fxaa_enabled = ctx.settings.fxaa_enabled and !ctx.settings.taa_enabled, .bloom_enabled = ctx.settings.bloom_enabled, .overlay_renderer = renderOverlay, .overlay_ctx = self, @@ -250,6 +253,12 @@ pub const WorldScreen = struct { try ctx.render_graph.execute(render_ctx); } + if (taa_enabled) { + camera.advanceJitter(); + } else { + camera.resetJitter(); + } + ui.begin(); defer ui.end(); diff --git a/src/game/settings/apply.zig b/src/game/settings/apply.zig index e548eea..4ed1136 100644 --- a/src/game/settings/apply.zig +++ b/src/game/settings/apply.zig @@ -9,6 +9,8 @@ const RHI = @import("../../engine/graphics/rhi.zig").RHI; /// - `textures_enabled` - Texture sampling toggle /// - `anisotropic_filtering` - Sampler anisotropy level /// - `msaa_samples` - Multisample anti-aliasing sample count +/// - `taa_blend_factor` - TAA history accumulation factor +/// - `taa_velocity_rejection` - TAA motion rejection threshold /// /// ## Settings NOT Applied Here (consumed elsewhere): /// These settings take effect without requiring this function because they are @@ -27,6 +29,7 @@ const RHI = @import("../../engine/graphics/rhi.zig").RHI; /// | `max_texture_resolution` | TextureLoader on texture load | On asset reload | /// | `fov`, `mouse_sensitivity` | Camera / InputMapper | Next frame | /// | `window_*`, `fullscreen` | WindowManager | On explicit apply | +/// | `taa_enabled` | TAA render graph stage toggle | Next frame | /// /// This separation exists because RHI exposes setters only for GPU pipeline state, /// while other settings are architectural concerns handled by their respective systems. @@ -37,4 +40,6 @@ pub fn applyToRHI(settings: *const Settings, rhi: *RHI) void { rhi.setDebugShadowView(settings.debug_shadows_active); rhi.setAnisotropicFiltering(settings.anisotropic_filtering); rhi.setMSAA(settings.msaa_samples); + rhi.setTAABlendFactor(settings.taa_blend_factor); + rhi.setTAAVelocityRejection(settings.taa_velocity_rejection); } diff --git a/src/game/settings/data.zig b/src/game/settings/data.zig index 5580de7..f76e266 100644 --- a/src/game/settings/data.zig +++ b/src/game/settings/data.zig @@ -41,6 +41,9 @@ pub const Settings = struct { shadow_distance: f32 = 250.0, anisotropic_filtering: u8 = 16, msaa_samples: u8 = 4, + taa_enabled: bool = false, + taa_blend_factor: f32 = 0.9, + taa_velocity_rejection: f32 = 0.02, ui_scale: f32 = 1.0, // Manual UI scale multiplier (0.5 to 2.0) window_width: u32 = 1920, window_height: u32 = 1080, @@ -167,12 +170,28 @@ pub const Settings = struct { } }, }; pub const msaa_samples = SettingMetadata{ - .label = "ANTI-ALIASING (MSAA)", + .label = "ANTI-ALIASING (LEGACY)", + .description = "Legacy setting retained for compatibility while TAA rollout completes", .kind = .{ .choice = .{ .labels = &[_][]const u8{ "OFF", "2X", "4X", "8X" }, .values = &[_]u32{ 1, 2, 4, 8 }, } }, }; + pub const taa_enabled = SettingMetadata{ + .label = "TEMPORAL AA (TAA)", + .description = "Experimental temporal anti-aliasing pipeline", + .kind = .toggle, + }; + pub const taa_blend_factor = SettingMetadata{ + .label = "TAA HISTORY BLEND", + .description = "Higher values favor temporal stability over responsiveness", + .kind = .{ .slider = .{ .min = 0.50, .max = 0.98, .step = 0.02 } }, + }; + pub const taa_velocity_rejection = SettingMetadata{ + .label = "TAA VELOCITY REJECT", + .description = "Lower values reject history sooner on motion", + .kind = .{ .slider = .{ .min = 0.0, .max = 0.25, .step = 0.01 } }, + }; pub const max_texture_resolution = SettingMetadata{ .label = "MAX TEXTURE RES", .kind = .{ .choice = .{ diff --git a/src/game/settings/json_presets.zig b/src/game/settings/json_presets.zig index e7fa7d2..3091568 100644 --- a/src/game/settings/json_presets.zig +++ b/src/game/settings/json_presets.zig @@ -12,6 +12,9 @@ pub const PresetConfig = struct { pbr_enabled: bool, pbr_quality: u8, msaa_samples: u8, + taa_enabled: bool = false, + taa_blend_factor: f32 = 0.9, + taa_velocity_rejection: f32 = 0.02, anisotropic_filtering: u8, max_texture_resolution: u32, cloud_shadows_enabled: bool, @@ -122,6 +125,9 @@ pub fn apply(settings: *Settings, preset_idx: usize) void { settings.pbr_enabled = config.pbr_enabled; settings.pbr_quality = config.pbr_quality; settings.msaa_samples = config.msaa_samples; + settings.taa_enabled = config.taa_enabled; + settings.taa_blend_factor = config.taa_blend_factor; + settings.taa_velocity_rejection = config.taa_velocity_rejection; settings.anisotropic_filtering = config.anisotropic_filtering; settings.max_texture_resolution = config.max_texture_resolution; settings.cloud_shadows_enabled = config.cloud_shadows_enabled; @@ -140,7 +146,7 @@ pub fn apply(settings: *Settings, preset_idx: usize) void { settings.lpv_propagation_iterations = config.lpv_propagation_iterations; settings.lod_enabled = config.lod_enabled; settings.render_distance = config.render_distance; - settings.fxaa_enabled = config.fxaa_enabled; + settings.fxaa_enabled = config.fxaa_enabled and !config.taa_enabled; settings.bloom_enabled = config.bloom_enabled; settings.bloom_intensity = config.bloom_intensity; } @@ -161,6 +167,9 @@ fn matches(settings: *const Settings, preset: PresetConfig) bool { settings.pbr_enabled == preset.pbr_enabled and settings.pbr_quality == preset.pbr_quality and settings.msaa_samples == preset.msaa_samples and + settings.taa_enabled == preset.taa_enabled and + std.math.approxEqAbs(f32, settings.taa_blend_factor, preset.taa_blend_factor, epsilon) and + std.math.approxEqAbs(f32, settings.taa_velocity_rejection, preset.taa_velocity_rejection, epsilon) and settings.anisotropic_filtering == preset.anisotropic_filtering and settings.max_texture_resolution == preset.max_texture_resolution and settings.cloud_shadows_enabled == preset.cloud_shadows_enabled and diff --git a/src/world/chunk_mesh.zig b/src/world/chunk_mesh.zig index d6bebeb..d3161f8 100644 --- a/src/world/chunk_mesh.zig +++ b/src/world/chunk_mesh.zig @@ -1,7 +1,7 @@ //! Chunk mesh orchestrator — coordinates meshing stages and manages GPU lifecycle. //! //! Vertices are built per-subchunk via the greedy mesher, then merged into -//! single solid/fluid buffers for minimal draw calls. Meshing logic is +//! single solid/cutout/fluid buffers for minimal draw calls. Meshing logic is //! delegated to modules in `meshing/`. const std = @import("std"); @@ -28,14 +28,16 @@ pub const NUM_SUBCHUNKS = boundary.NUM_SUBCHUNKS; pub const Pass = enum { solid, + cutout, fluid, }; -/// Merged chunk mesh with single solid/fluid buffers for minimal draw calls. +/// Merged chunk mesh with single solid/cutout/fluid buffers for minimal draw calls. /// Subchunk data is only used during mesh building, then merged. pub const ChunkMesh = struct { // Merged GPU allocations from GlobalVertexAllocator solid_allocation: ?VertexAllocation = null, + cutout_allocation: ?VertexAllocation = null, fluid_allocation: ?VertexAllocation = null, ready: bool = false, @@ -45,10 +47,12 @@ pub const ChunkMesh = struct { // Pending merged vertex data (built on worker thread, uploaded on main thread) pending_solid: ?[]Vertex = null, + pending_cutout: ?[]Vertex = null, pending_fluid: ?[]Vertex = null, // Temporary per-subchunk data during building (not stored after merge) subchunk_solid: [NUM_SUBCHUNKS]?[]Vertex = [_]?[]Vertex{null} ** NUM_SUBCHUNKS, + subchunk_cutout: [NUM_SUBCHUNKS]?[]Vertex = [_]?[]Vertex{null} ** NUM_SUBCHUNKS, subchunk_fluid: [NUM_SUBCHUNKS]?[]Vertex = [_]?[]Vertex{null} ** NUM_SUBCHUNKS, pub fn init(allocator: std.mem.Allocator) ChunkMesh { @@ -64,15 +68,19 @@ pub const ChunkMesh = struct { defer self.mutex.unlock(); if (self.solid_allocation) |alloc| allocator.free(alloc); + if (self.cutout_allocation) |alloc| allocator.free(alloc); if (self.fluid_allocation) |alloc| allocator.free(alloc); self.solid_allocation = null; + self.cutout_allocation = null; self.fluid_allocation = null; if (self.pending_solid) |p| self.allocator.free(p); + if (self.pending_cutout) |p| self.allocator.free(p); if (self.pending_fluid) |p| self.allocator.free(p); for (0..NUM_SUBCHUNKS) |i| { if (self.subchunk_solid[i]) |p| self.allocator.free(p); + if (self.subchunk_cutout[i]) |p| self.allocator.free(p); if (self.subchunk_fluid[i]) |p| self.allocator.free(p); } } @@ -82,10 +90,12 @@ pub const ChunkMesh = struct { defer self.mutex.unlock(); if (self.pending_solid) |p| self.allocator.free(p); + if (self.pending_cutout) |p| self.allocator.free(p); if (self.pending_fluid) |p| self.allocator.free(p); for (0..NUM_SUBCHUNKS) |i| { if (self.subchunk_solid[i]) |p| self.allocator.free(p); + if (self.subchunk_cutout[i]) |p| self.allocator.free(p); if (self.subchunk_fluid[i]) |p| self.allocator.free(p); } } @@ -105,6 +115,8 @@ pub const ChunkMesh = struct { fn buildSubchunk(self: *ChunkMesh, chunk: *const Chunk, neighbors: NeighborChunks, si: u32, atlas: *const TextureAtlas) !void { var solid_verts = std.ArrayListUnmanaged(Vertex).empty; defer solid_verts.deinit(self.allocator); + var cutout_verts = std.ArrayListUnmanaged(Vertex).empty; + defer cutout_verts.deinit(self.allocator); var fluid_verts = std.ArrayListUnmanaged(Vertex).empty; defer fluid_verts.deinit(self.allocator); @@ -114,17 +126,17 @@ pub const ChunkMesh = struct { // Mesh horizontal slices (top/bottom faces) var sy: i32 = y0; while (sy <= y1) : (sy += 1) { - try greedy_mesher.meshSlice(self.allocator, chunk, neighbors, .top, sy, si, &solid_verts, &fluid_verts, atlas); + try greedy_mesher.meshSlice(self.allocator, chunk, neighbors, .top, sy, si, &solid_verts, &cutout_verts, &fluid_verts, atlas); } // Mesh east/west face slices var sx: i32 = 0; while (sx <= CHUNK_SIZE_X) : (sx += 1) { - try greedy_mesher.meshSlice(self.allocator, chunk, neighbors, .east, sx, si, &solid_verts, &fluid_verts, atlas); + try greedy_mesher.meshSlice(self.allocator, chunk, neighbors, .east, sx, si, &solid_verts, &cutout_verts, &fluid_verts, atlas); } // Mesh south/north face slices var sz: i32 = 0; while (sz <= CHUNK_SIZE_Z) : (sz += 1) { - try greedy_mesher.meshSlice(self.allocator, chunk, neighbors, .south, sz, si, &solid_verts, &fluid_verts, atlas); + try greedy_mesher.meshSlice(self.allocator, chunk, neighbors, .south, sz, si, &solid_verts, &cutout_verts, &fluid_verts, atlas); } // Store subchunk data temporarily (will be merged later) @@ -132,12 +144,17 @@ pub const ChunkMesh = struct { defer self.mutex.unlock(); if (self.subchunk_solid[si]) |p| self.allocator.free(p); + if (self.subchunk_cutout[si]) |p| self.allocator.free(p); if (self.subchunk_fluid[si]) |p| self.allocator.free(p); self.subchunk_solid[si] = if (solid_verts.items.len > 0) try self.allocator.dupe(Vertex, solid_verts.items) else null; + self.subchunk_cutout[si] = if (cutout_verts.items.len > 0) + try self.allocator.dupe(Vertex, cutout_verts.items) + else + null; self.subchunk_fluid[si] = if (fluid_verts.items.len > 0) try self.allocator.dupe(Vertex, fluid_verts.items) else @@ -152,14 +169,17 @@ pub const ChunkMesh = struct { // Count total vertices var total_solid: usize = 0; + var total_cutout: usize = 0; var total_fluid: usize = 0; for (0..NUM_SUBCHUNKS) |i| { if (self.subchunk_solid[i]) |v| total_solid += v.len; + if (self.subchunk_cutout[i]) |v| total_cutout += v.len; if (self.subchunk_fluid[i]) |v| total_fluid += v.len; } // Free old pending data if (self.pending_solid) |p| self.allocator.free(p); + if (self.pending_cutout) |p| self.allocator.free(p); if (self.pending_fluid) |p| self.allocator.free(p); // Merge solid vertices @@ -179,6 +199,23 @@ pub const ChunkMesh = struct { self.pending_solid = null; } + // Merge cutout vertices + if (total_cutout > 0) { + var merged = try self.allocator.alloc(Vertex, total_cutout); + var offset: usize = 0; + for (0..NUM_SUBCHUNKS) |i| { + if (self.subchunk_cutout[i]) |v_slice| { + @memcpy(merged[offset..][0..v_slice.len], v_slice); + offset += v_slice.len; + self.allocator.free(v_slice); + self.subchunk_cutout[i] = null; + } + } + self.pending_cutout = merged; + } else { + self.pending_cutout = null; + } + // Merge fluid vertices if (total_fluid > 0) { var merged = try self.allocator.alloc(Vertex, total_fluid); @@ -226,6 +263,28 @@ pub const ChunkMesh = struct { self.ready = true; } + // Handle cutout pass + if (self.pending_cutout) |v| { + if (self.cutout_allocation) |alloc| { + allocator.free(alloc); + self.cutout_allocation = null; + } + + if (v.len > 0) { + self.cutout_allocation = allocator.allocate(v) catch |err| { + std.log.err("Failed to allocate chunk cutout vertices (will retry): {}", .{err}); + return; + }; + } + self.allocator.free(v); + self.pending_cutout = null; + self.ready = true; + } else if (self.cutout_allocation != null) { + allocator.free(self.cutout_allocation.?); + self.cutout_allocation = null; + self.ready = true; + } + // Handle fluid pass if (self.pending_fluid) |v| { if (self.fluid_allocation) |alloc| { @@ -260,6 +319,11 @@ pub const ChunkMesh = struct { rhi.drawOffset(alloc.handle, alloc.count, .triangles, alloc.offset); } }, + .cutout => { + if (self.cutout_allocation) |alloc| { + rhi.drawOffset(alloc.handle, alloc.count, .triangles, alloc.offset); + } + }, .fluid => { if (self.fluid_allocation) |alloc| { rhi.drawOffset(alloc.handle, alloc.count, .triangles, alloc.offset); diff --git a/src/world/chunk_storage.zig b/src/world/chunk_storage.zig index 4303f7b..4a0b876 100644 --- a/src/world/chunk_storage.zig +++ b/src/world/chunk_storage.zig @@ -129,7 +129,7 @@ pub const ChunkStorage = struct { defer self.chunks_mutex.unlockShared(); if (self.chunks.get(.{ .x = cx, .z = cz })) |data| { - return data.chunk.state == .renderable or data.mesh.solid_allocation != null or data.mesh.fluid_allocation != null; + return data.chunk.state == .renderable or data.mesh.solid_allocation != null or data.mesh.cutout_allocation != null or data.mesh.fluid_allocation != null; } return false; } diff --git a/src/world/meshing/greedy_mesher.zig b/src/world/meshing/greedy_mesher.zig index 3f047ba..cb56429 100644 --- a/src/world/meshing/greedy_mesher.zig +++ b/src/world/meshing/greedy_mesher.zig @@ -43,7 +43,7 @@ const FaceKey = struct { }; /// Process a single 16x16 slice along the given axis, producing greedy-merged quads. -/// Populates solid_list and fluid_list with generated vertices. +/// Populates solid_list, cutout_list, and fluid_list with generated vertices. pub fn meshSlice( allocator: std.mem.Allocator, chunk: *const Chunk, @@ -52,6 +52,7 @@ pub fn meshSlice( s: i32, si: u32, solid_list: *std.ArrayListUnmanaged(Vertex), + cutout_list: *std.ArrayListUnmanaged(Vertex), fluid_list: *std.ArrayListUnmanaged(Vertex), atlas: *const TextureAtlas, ) !void { @@ -143,7 +144,11 @@ pub fn meshSlice( } const k_def = block_registry.getBlockDefinition(k.block); - const target = if (k_def.render_pass == .fluid) fluid_list else solid_list; + const target = switch (k_def.render_pass) { + .fluid => fluid_list, + .cutout => cutout_list, + else => solid_list, + }; try addGreedyFace(allocator, target, axis, s, su, sv, width, height, k_def, k.side, si, k.light, k.color, chunk, neighbors, atlas); var dy: u32 = 0; diff --git a/src/world/world.zig b/src/world/world.zig index d9051f5..0a6f88f 100644 --- a/src/world/world.zig +++ b/src/world/world.zig @@ -277,7 +277,7 @@ pub const World = struct { defer storage.chunks_mutex.unlockShared(); if (storage.chunks.get(.{ .x = chunk_x, .z = chunk_z })) |data| { - return data.chunk.state == .renderable or data.mesh.solid_allocation != null or data.mesh.fluid_allocation != null; + return data.chunk.state == .renderable or data.mesh.solid_allocation != null or data.mesh.cutout_allocation != null or data.mesh.fluid_allocation != null; } return false; } @@ -317,6 +317,7 @@ pub const World = struct { var iter = self.storage.iteratorUnsafe(); while (iter.next()) |entry| { if (entry.value_ptr.*.mesh.solid_allocation) |alloc| total_verts += alloc.count; + if (entry.value_ptr.*.mesh.cutout_allocation) |alloc| total_verts += alloc.count; if (entry.value_ptr.*.mesh.fluid_allocation) |alloc| total_verts += alloc.count; } diff --git a/src/world/world_renderer.zig b/src/world/world_renderer.zig index 7ed5776..ebd3b7f 100644 --- a/src/world/world_renderer.zig +++ b/src/world/world_renderer.zig @@ -136,7 +136,7 @@ pub const WorldRenderer = struct { var cx = pc_x - r_dist; while (cx <= pc_x + r_dist) : (cx += 1) { if (self.storage.chunks.get(.{ .x = @as(i32, @intCast(cx)), .z = @as(i32, @intCast(cz)) })) |data| { - if (data.chunk.state == .renderable or data.mesh.solid_allocation != null or data.mesh.fluid_allocation != null) { + if (data.chunk.state == .renderable or data.mesh.solid_allocation != null or data.mesh.cutout_allocation != null or data.mesh.fluid_allocation != null) { if (frustum.intersectsChunkRelative(@as(i32, @intCast(cx)), @as(i32, @intCast(cz)), camera_pos.x, camera_pos.y, camera_pos.z)) { self.visible_chunks.append(self.allocator, data) catch {}; } else { @@ -164,6 +164,10 @@ pub const WorldRenderer = struct { self.last_render_stats.vertices_rendered += alloc.count; self.rhi.drawOffset(self.vertex_allocator.buffer, alloc.count, .triangles, alloc.offset); } + if (data.mesh.cutout_allocation) |alloc| { + self.last_render_stats.vertices_rendered += alloc.count; + self.rhi.drawOffset(self.vertex_allocator.buffer, alloc.count, .triangles, alloc.offset); + } if (data.mesh.fluid_allocation) |alloc| { self.last_render_stats.vertices_rendered += alloc.count; self.rhi.drawOffset(self.vertex_allocator.buffer, alloc.count, .triangles, alloc.offset); @@ -205,7 +209,7 @@ pub const WorldRenderer = struct { var cx = pc_x - r_dist; while (cx <= pc_x + r_dist) : (cx += 1) { if (self.storage.chunks.get(.{ .x = @as(i32, @intCast(cx)), .z = @as(i32, @intCast(cz)) })) |data| { - if (data.chunk.state == .renderable or data.mesh.solid_allocation != null or data.mesh.fluid_allocation != null) { + if (data.chunk.state == .renderable or data.mesh.solid_allocation != null or data.mesh.cutout_allocation != null or data.mesh.fluid_allocation != null) { const chunk_world_x: f32 = @floatFromInt(data.chunk.chunk_x * CHUNK_SIZE_X); const chunk_world_z: f32 = @floatFromInt(data.chunk.chunk_z * CHUNK_SIZE_Z); @@ -223,6 +227,10 @@ pub const WorldRenderer = struct { self.rhi.drawOffset(self.vertex_allocator.buffer, alloc.count, .triangles, alloc.offset); } + if (data.mesh.cutout_allocation) |alloc| { + self.rhi.setModelMatrix(model, Vec3.one, 0); + self.rhi.drawOffset(self.vertex_allocator.buffer, alloc.count, .triangles, alloc.offset); + } } } }