diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 00000000..501f44a0 --- /dev/null +++ b/HANDOFF.md @@ -0,0 +1,73 @@ +# ZigCraft Handoff + +## Current State +- Branch: `handoff/world-rendering-handoff` +- Build/test status: `nix develop --command zig build test` passes +- GPU crash path: fixed enough that normal mode no longer segfaults after `VK_ERROR_DEVICE_LOST` +- Remaining problem: world rendering still has gaps/peeling, and some distant features are missing or unstable + +## Symptoms Observed +- Terrain chunks disappear or "peel away" when the camera rotates +- There are visible holes/gaps in the terrain coverage +- Clouds are inconsistent or absent in some runs +- LOD rendering still looks unreliable +- Shadows may be missing or not showing as expected in normal mode + +## What Was Fixed Already +- `VK_ERROR_DEVICE_LOST` no longer cascades into a RADV segfault +- `gpu_fault_detected` is now set when the frame ends with `GpuLost` +- Transfer flushes are guarded so the app stops calling into Vulkan after loss +- GPU recovery path was added to the main loop +- `ZIGCRAFT_SAFE_MODE` / `ZIGCRAFT_SAFE_RENDER` distinction was preserved + +## Rendering Changes Made +- `src/game/session.zig` + - Re-enabled LOD use in normal mode by removing the hardcoded `effective_lod_enabled = false` + - Safe mode still disables LOD +- `src/world/world_renderer.zig` + - Removed the chunk frustum-culling path that was likely causing chunks to vanish when looking around +- `src/world/lod_renderer.zig` + - Restored proper region coverage checks instead of skipping based on only one chunk + - Added a full chunk-coverage test for LOD coverage decisions +- `src/world/lod_manager.zig` + - `cleanup_covered_regions` was changed to `false` by default to keep LOD meshes around + +## Important Suspects +1. `src/world/lod_renderer.zig` + - LOD visibility/coverage logic is still the most likely cause of holes + - The region coverage check may still be too aggressive or use the wrong coordinate range +2. `src/world/lod_manager.zig` + - Covered-region cleanup may still be interfering with visible terrain if it gets re-enabled elsewhere +3. `src/world/world_renderer.zig` + - CPU-side chunk visibility logic was simplified to stop peel-away, but the deeper cause may still be in the render flow +4. `src/game/screens/world.zig` and `src/engine/graphics/render_system.zig` + - Cloud/shadow pass wiring and runtime flags should be rechecked if those features still fail to appear + +## File References +- `src/engine/graphics/vulkan/rhi_pass_orchestration.zig` +- `src/engine/graphics/rhi_vulkan.zig` +- `src/engine/graphics/vulkan/transfer_queue.zig` +- `src/engine/graphics/vulkan/rhi_state_control.zig` +- `src/game/app.zig` +- `src/game/session.zig` +- `src/world/world_renderer.zig` +- `src/world/lod_renderer.zig` +- `src/world/lod_manager.zig` +- `src/game/screens/world.zig` +- `src/engine/graphics/render_system.zig` + +## Runtime Notes +- Current settings were tuned away from the earlier very-low profile +- The app now runs without crashing, but visual correctness is still incomplete +- The bug appears view-dependent, which points more toward culling/LOD than texture issues + +## Suggested Next Steps +1. Reproduce the hole/peel behavior with logging disabled +2. Inspect LOD region coverage and chunk coverage math in `lod_renderer.zig` +3. Confirm whether LOD meshes are being drawn over or under normal chunks as the camera turns +4. Revisit shadow/cloud pass enablement only after terrain coverage is stable +5. If needed, temporarily draw debug bounds for LOD regions and chunk coverage to confirm which regions are being skipped + +## Verification +- `nix develop --command zig build test` +- Runtime still needs visual retesting in normal mode diff --git a/assets/shaders/vulkan/post_process.frag b/assets/shaders/vulkan/post_process.frag index 7c3b636f..d0c6c1b9 100644 --- a/assets/shaders/vulkan/post_process.frag +++ b/assets/shaders/vulkan/post_process.frag @@ -166,5 +166,22 @@ vec3 applyFilmGrain(vec3 color, vec2 uv, float intensity, float time) { } void main() { - outColor = vec4(texture(uHDRBuffer, inUV).rgb, 1.0); + vec3 color = texture(uHDRBuffer, inUV).rgb; + + if (postParams.bloomEnabled > 0.5) { + color += texture(uBloomTexture, inUV).rgb * postParams.bloomIntensity; + } + + if (global.cloud_params.z > 0.5) { + color = agxToneMap(color, global.pbr_params.y, global.pbr_params.z); + color = applyColorGrading(color, postParams.colorGradingEnabled * postParams.colorGradingIntensity); + color = applyVignette(color, inUV, postParams.vignetteIntensity); + color = applyFilmGrain(color, inUV, postParams.filmGrainIntensity, global.params.x); + } else { + // Keep the safe/non-PBR path on the same display transform so bright terrain + // doesn't collapse into a pale linear-looking wash. + color = agxToneMap(color, global.pbr_params.y, global.pbr_params.z); + } + + outColor = vec4(color, 1.0); } diff --git a/assets/shaders/vulkan/terrain.frag b/assets/shaders/vulkan/terrain.frag index 505824e6..3a6d8ae7 100644 --- a/assets/shaders/vulkan/terrain.frag +++ b/assets/shaders/vulkan/terrain.frag @@ -260,6 +260,8 @@ float computeShadowFactor(vec3 fragPosWorld, vec3 N, vec3 L, int layer) { } float computeShadowCascades(vec3 fragPosWorld, vec3 N, vec3 L, float viewDepth, int layer) { + if (global.shadow_params.z <= 0.0) return 0.0; + float shadow = computeShadowFactor(fragPosWorld, N, L, layer); // Cascade blending transition (only when enabled). @@ -273,7 +275,7 @@ float computeShadowCascades(vec3 fragPosWorld, vec3 N, vec3 L, float viewDepth, shadow = mix(shadow, nextShadow, clamp(blend, 0.0, 1.0)); } } - return shadow; + return shadow * clamp(global.shadow_params.z, 0.0, 1.0); } // PBR functions @@ -390,8 +392,8 @@ vec3 computeLegacyDirect(vec3 albedo, float nDotL, float totalShadow, float skyL float directLight = nDotL * global.params.w * (1.0 - totalShadow) * intensityFactor; float skyLight = skyLightIn * (global.lighting.x + directLight * 1.0); float lightLevel = max(skyLight, max(blockLightIn.r, max(blockLightIn.g, blockLightIn.b))); - lightLevel = max(lightLevel, global.lighting.x * 0.5); - float shadowFactor = mix(1.0, 0.5, totalShadow); + lightLevel = max(lightLevel, global.lighting.x * 0.8); + float shadowFactor = mix(1.0, 0.8, totalShadow); lightLevel = clamp(lightLevel * shadowFactor, 0.0, 1.0); return albedo * lightLevel; } @@ -402,7 +404,7 @@ vec3 computePBR(vec3 albedo, vec3 N, vec3 V, vec3 L, float roughness, float tota vec3 sunColor = global.sun_color.rgb * global.params.w * SUN_RADIANCE_TO_IRRADIANCE / PI; vec3 Lo = brdf * sunColor * NdotL_final * (1.0 - totalShadow); vec3 envColor = computeIBLAmbient(N, roughness); - float shadowAmbientFactor = mix(1.0, 0.2, totalShadow); + float shadowAmbientFactor = mix(1.0, 0.65, totalShadow); vec3 indirect = sampleLPVAtlas(vFragPosWorld, N); vec3 ambientColor = albedo * (max(min(envColor, IBL_CLAMP) * skyLight * 0.8, vec3(global.lighting.x * 0.8)) + blockLight + indirect) * ao * ssao * shadowAmbientFactor; return ambientColor + Lo; @@ -410,7 +412,7 @@ vec3 computePBR(vec3 albedo, vec3 N, vec3 V, vec3 L, float roughness, float tota vec3 computeNonPBR(vec3 albedo, vec3 N, float nDotL, float totalShadow, float skyLight, vec3 blockLight, float ao, float ssao) { vec3 envColor = computeIBLAmbient(N, NON_PBR_ROUGHNESS); - float shadowAmbientFactor = mix(1.0, 0.2, totalShadow); + float shadowAmbientFactor = mix(1.0, 0.65, totalShadow); vec3 indirect = sampleLPVAtlas(vFragPosWorld, N); vec3 ambientColor = albedo * (max(min(envColor, IBL_CLAMP) * skyLight * 0.8, vec3(global.lighting.x * 0.8)) + blockLight + indirect) * ao * ssao * shadowAmbientFactor; vec3 sunColor = global.sun_color.rgb * global.params.w * SUN_RADIANCE_TO_IRRADIANCE / PI; @@ -419,7 +421,7 @@ vec3 computeNonPBR(vec3 albedo, vec3 N, float nDotL, float totalShadow, float sk } vec3 computeLOD(vec3 albedo, float nDotL, float totalShadow, float skyLightVal, vec3 blockLight, float ao, float ssao) { - float shadowAmbientFactor = mix(1.0, 0.2, totalShadow); + float shadowAmbientFactor = mix(1.0, 0.65, totalShadow); vec3 indirect = sampleLPVAtlas(vFragPosWorld, vec3(0.0, 1.0, 0.0)); // LOD uses up-facing normal vec3 ambientColor = albedo * (max(vec3(skyLightVal * 0.8), vec3(global.lighting.x * 0.4)) + blockLight + indirect) * ao * ssao * shadowAmbientFactor; vec3 sunColor = global.sun_color.rgb * global.params.w * SUN_VOLUMETRIC_INTENSITY / PI; @@ -505,9 +507,6 @@ void main() { vec3 L = normalize(global.sun_dir.xyz); float nDotL = max(dot(N, L), 0.0); - // Select cascade from view-space Z depth (branchless ternary — compiles to cmp/sel). - // If splits are NaN/uninitialized (csm.zig isValid() guards this on CPU), - // comparisons return false and we fall through to cascade 2 (widest). int layer = vViewDepth < shadows.cascade_splits[0] ? 0 : (vViewDepth < shadows.cascade_splits[1] ? 1 : 2); float shadowFactor = computeShadowCascades(vFragPosWorld, N, L, vViewDepth, layer); @@ -522,11 +521,16 @@ void main() { float ao = mix(1.0, vAO, mix(0.4, 0.05, clamp(viewDistance / AO_FADE_DISTANCE, 0.0, 1.0))); if (global.lighting.y > 0.5 && vTileID >= 0) { - vec4 texColor = texture(uTexture, uv); - if (texColor.a < 0.1) discard; - vec3 albedo = texColor.rgb * vColor; + if (global.cloud_params.z <= 0.5) { + vec4 texColor = texture(uTexture, uv); + if (texColor.a < 0.1) discard; + color = texColor.rgb * vColor; + } else { + vec4 texColor = texture(uTexture, uv); + if (texColor.a < 0.1) discard; + vec3 albedo = texColor.rgb * vColor; - if (global.lighting.z > 0.5 && global.pbr_params.x > 0.5) { + if (global.lighting.z > 0.5 && global.pbr_params.x > 0.5) { float roughness = texture(uRoughnessMap, uv).r; if (normalMapSample.a > 0.5 || roughness < 0.99) { vec3 V = normalize(global.cam_pos.xyz - vFragPosWorld); @@ -534,15 +538,14 @@ void main() { } else { color = computeNonPBR(albedo, N, nDotL, totalShadow, vSkyLight * global.lighting.x, vBlockLight, ao, ssao); } - } else { - color = computeLegacyDirect(albedo, nDotL, totalShadow, vSkyLight, vBlockLight, LEGACY_LIGHTING_INTENSITY) * ao * ssao; + } else { + color = albedo; + } } + } else if (vTileID < 0) { + color = computeLOD(vColor, nDotL, totalShadow, vSkyLight * global.lighting.x, vBlockLight, ao, ssao); } else { - if (vTileID < 0) { - color = computeLegacyDirect(vColor, nDotL, totalShadow, vSkyLight, vBlockLight, LOD_LIGHTING_INTENSITY) * ao * ssao; - } else { - color = computeLegacyDirect(vColor, nDotL, totalShadow, vSkyLight, vBlockLight, LOD_LIGHTING_INTENSITY) * ao * ssao; - } + color = vColor; } if (global.volumetric_params.x > 0.5) { diff --git a/assets/shaders/vulkan/terrain.frag.spv b/assets/shaders/vulkan/terrain.frag.spv index c38d2c07..da9740f9 100644 Binary files a/assets/shaders/vulkan/terrain.frag.spv and b/assets/shaders/vulkan/terrain.frag.spv differ diff --git a/assets/shaders/vulkan/terrain_debug.frag b/assets/shaders/vulkan/terrain_debug.frag new file mode 100644 index 00000000..1eaad492 --- /dev/null +++ b/assets/shaders/vulkan/terrain_debug.frag @@ -0,0 +1,24 @@ +#version 450 + +layout(location = 0) in vec3 vColor; +layout(location = 1) flat in vec3 vNormal; +layout(location = 2) in vec2 vTexCoord; +layout(location = 3) flat in int vTileID; +layout(location = 4) in float vDistance; +layout(location = 5) in float vSkyLight; +layout(location = 6) in vec3 vBlockLight; +layout(location = 7) in vec3 vFragPosWorld; +layout(location = 8) in float vViewDepth; +layout(location = 9) in vec3 vTangent; +layout(location = 10) in vec3 vBitangent; +layout(location = 11) in float vAO; +layout(location = 12) in vec4 vClipPosCurrent; +layout(location = 13) in vec4 vClipPosPrev; +layout(location = 14) in float vMaskRadius; + +layout(location = 0) out vec4 FragColor; + +void main() { + // Debug: output bright magenta for all terrain + FragColor = vec4(1.0, 0.0, 1.0, 1.0); +} diff --git a/src/engine/graphics/render_system.zig b/src/engine/graphics/render_system.zig index 915c0acd..3288bf04 100644 --- a/src/engine/graphics/render_system.zig +++ b/src/engine/graphics/render_system.zig @@ -46,11 +46,16 @@ pub const RenderSystem = struct { fxaa_pass: render_graph_pkg.FXAAPass, water_reflection_pass: render_graph_pkg.WaterReflectionPass, water_pass: render_graph_pkg.WaterPass, + safe_mode: bool, safe_render_mode: bool, disable_shadow_draw: bool, disable_gpass_draw: bool, disable_ssao: bool, disable_clouds: bool, + disable_water: bool, + disable_taa: bool, + disable_fxaa: bool, + disable_bloom: bool, pub fn init(allocator: Allocator, window: *c.SDL_Window, settings: *const Settings) !*RenderSystem { log.log.info("Initializing RenderSystem...", .{}); @@ -61,6 +66,12 @@ pub const RenderSystem = struct { else false; + const safe_mode_env = std.posix.getenv("ZIGCRAFT_SAFE_MODE"); + const safe_mode = if (safe_mode_env) |val| + !(std.mem.eql(u8, val, "0") or std.mem.eql(u8, val, "false")) + else + false; + const disable_shadow_env = std.posix.getenv("ZIGCRAFT_DISABLE_SHADOWS"); const disable_shadow_draw = if (disable_shadow_env) |val| !(std.mem.eql(u8, val, "0") or std.mem.eql(u8, val, "false")) @@ -85,9 +96,36 @@ pub const RenderSystem = struct { else false; + const disable_water_env = std.posix.getenv("ZIGCRAFT_DISABLE_WATER"); + const disable_water = if (disable_water_env) |val| + !(std.mem.eql(u8, val, "0") or std.mem.eql(u8, val, "false")) + else + false; + + const disable_taa_env = std.posix.getenv("ZIGCRAFT_DISABLE_TAA"); + const disable_taa = if (disable_taa_env) |val| + !(std.mem.eql(u8, val, "0") or std.mem.eql(u8, val, "false")) + else + false; + + const disable_fxaa_env = std.posix.getenv("ZIGCRAFT_DISABLE_FXAA"); + const disable_fxaa = if (disable_fxaa_env) |val| + !(std.mem.eql(u8, val, "0") or std.mem.eql(u8, val, "false")) + else + false; + + const disable_bloom_env = std.posix.getenv("ZIGCRAFT_DISABLE_BLOOM"); + const disable_bloom = if (disable_bloom_env) |val| + !(std.mem.eql(u8, val, "0") or std.mem.eql(u8, val, "false")) + else + false; + if (safe_render_mode) { log.log.warn("ZIGCRAFT_SAFE_RENDER enabled: skipping world rendering passes", .{}); } + if (safe_mode) { + log.log.warn("ZIGCRAFT_SAFE_MODE enabled: disabling depth pyramid and LPV compute passes", .{}); + } if (disable_shadow_draw) { log.log.warn("ZIGCRAFT_DISABLE_SHADOWS enabled", .{}); } @@ -100,6 +138,18 @@ pub const RenderSystem = struct { if (disable_clouds) { log.log.warn("ZIGCRAFT_DISABLE_CLOUDS enabled", .{}); } + if (disable_water) { + log.log.warn("ZIGCRAFT_DISABLE_WATER enabled", .{}); + } + if (disable_taa) { + log.log.warn("ZIGCRAFT_DISABLE_TAA enabled", .{}); + } + if (disable_fxaa) { + log.log.warn("ZIGCRAFT_DISABLE_FXAA enabled", .{}); + } + if (disable_bloom) { + log.log.warn("ZIGCRAFT_DISABLE_BLOOM enabled", .{}); + } log.log.info("Initializing Vulkan backend...", .{}); const rhi = try rhi_vulkan.createRHI(allocator, window, null, settings.getShadowResolution(), settings.msaa_samples, settings.anisotropic_filtering); @@ -176,17 +226,22 @@ pub const RenderSystem = struct { .opaque_pass = .{}, .cloud_pass = .{}, .entity_pass = .{}, - .taa_pass = .{ .enabled = true }, - .bloom_pass = .{ .enabled = false }, + .taa_pass = .{ .enabled = !disable_taa and settings.taa_enabled }, + .bloom_pass = .{ .enabled = !disable_bloom and settings.bloom_enabled }, .post_process_pass = .{}, - .fxaa_pass = .{ .enabled = true }, + .fxaa_pass = .{ .enabled = !disable_fxaa and settings.fxaa_enabled }, .water_reflection_pass = .{}, .water_pass = .{ .enabled = true }, + .safe_mode = safe_mode, .safe_render_mode = safe_render_mode, .disable_shadow_draw = disable_shadow_draw, .disable_gpass_draw = disable_gpass_draw, .disable_ssao = disable_ssao, .disable_clouds = disable_clouds, + .disable_water = disable_water, + .disable_taa = disable_taa, + .disable_fxaa = disable_fxaa, + .disable_bloom = disable_bloom, }; log.log.info("RenderSystem.init: initializing MaterialSystem", .{}); @@ -204,8 +259,8 @@ pub const RenderSystem = struct { ); errdefer self.lpv_system.deinit(); - self.rhi.setFXAA(settings.fxaa_enabled and !settings.taa_enabled); - self.rhi.setBloom(false); + self.rhi.setFXAA((settings.fxaa_enabled and !settings.taa_enabled) and !disable_fxaa); + self.rhi.setBloom(settings.bloom_enabled and !disable_bloom); self.rhi.setBloomIntensity(settings.bloom_intensity); settings_pkg.apply_logic.applyToRHI(settings, &self.rhi); @@ -218,11 +273,17 @@ pub const RenderSystem = struct { try self.render_graph.addPass(self.mesh_build_pass.pass()); try self.render_graph.addPass(self.g_pass.pass()); try self.render_graph.addPass(self.ssao_pass.pass()); - try self.render_graph.addPass(self.depth_pyramid_pass.pass()); + if (!safe_mode) { + try self.render_graph.addPass(self.depth_pyramid_pass.pass()); + } + try self.render_graph.addPass(self.water_reflection_pass.pass()); try self.render_graph.addPass(self.sky_pass.pass()); try self.render_graph.addPass(self.opaque_pass.pass()); - try self.render_graph.addPass(self.water_reflection_pass.pass()); - try self.render_graph.addPass(self.water_pass.pass()); + if (!disable_water) { + try self.render_graph.addPass(self.water_pass.pass()); + } else { + log.log.warn("ZIGCRAFT_DISABLE_WATER enabled", .{}); + } try self.render_graph.addPass(self.cloud_pass.pass()); try self.render_graph.addPass(self.entity_pass.pass()); try self.render_graph.addPass(self.taa_pass.pass()); @@ -338,6 +399,10 @@ pub const RenderSystem = struct { return self.safe_render_mode; } + pub fn getSafeMode(self: *const RenderSystem) bool { + return self.safe_mode; + } + pub fn getDisableShadowDraw(self: *const RenderSystem) bool { return self.disable_shadow_draw; } diff --git a/src/engine/graphics/rhi_vulkan.zig b/src/engine/graphics/rhi_vulkan.zig index f044b88b..bee07988 100644 --- a/src/engine/graphics/rhi_vulkan.zig +++ b/src/engine/graphics/rhi_vulkan.zig @@ -77,6 +77,10 @@ fn beginFrame(ctx_ptr: *anyopaque) void { if (ctx.resources.transfer.transfer_ready[ctx.resources.transfer.current_frame]) { ctx.resources.flushTransfer() catch |err| { log.log.errWithTrace("Failed to flush inter-frame transfers: {}", .{err}); + if (err == error.VulkanError) { + ctx.runtime.gpu_fault_detected = true; + return; + } }; } @@ -391,6 +395,7 @@ fn endFrame(ctx_ptr: *anyopaque) void { const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); ctx.mutex.lock(); defer ctx.mutex.unlock(); + if (ctx.runtime.gpu_fault_detected) return; pass_orchestration.endFrame(ctx); } @@ -517,10 +522,12 @@ fn bindTexture(ctx_ptr: *anyopaque, handle: rhi.TextureHandle, slot: u32) void { 7 => ctx.draw.current_roughness_texture = resolved, 8 => ctx.draw.current_displacement_texture = resolved, 9 => ctx.draw.current_env_texture = resolved, + 14 => ctx.draw.current_water_reflection_texture = resolved, + 15 => ctx.draw.current_scene_depth_texture = resolved, 11 => ctx.draw.current_lpv_texture = resolved, 12 => ctx.draw.current_lpv_texture_g = resolved, 13 => ctx.draw.current_lpv_texture_b = resolved, - else => ctx.draw.current_texture = resolved, + else => {}, } } diff --git a/src/engine/graphics/vulkan/rhi_context_factory.zig b/src/engine/graphics/vulkan/rhi_context_factory.zig index b0d14938..1f1b90e7 100644 --- a/src/engine/graphics/vulkan/rhi_context_factory.zig +++ b/src/engine/graphics/vulkan/rhi_context_factory.zig @@ -51,6 +51,8 @@ pub fn createRHI( ctx.draw.current_roughness_texture = 0; ctx.draw.current_displacement_texture = 0; ctx.draw.current_env_texture = 0; + ctx.draw.current_water_reflection_texture = 0; + ctx.draw.current_scene_depth_texture = 0; ctx.draw.current_lpv_texture = 0; ctx.draw.current_lpv_texture_g = 0; ctx.draw.current_lpv_texture_b = 0; @@ -84,6 +86,8 @@ pub fn createRHI( ctx.draw.bound_roughness_texture = 0; ctx.draw.bound_displacement_texture = 0; ctx.draw.bound_env_texture = 0; + ctx.draw.bound_water_reflection_texture = 0; + ctx.draw.bound_scene_depth_texture = 0; ctx.draw.bound_lpv_texture = 0; ctx.draw.current_mask_radius = 0; ctx.draw.lod_mode = false; diff --git a/src/engine/graphics/vulkan/rhi_context_types.zig b/src/engine/graphics/vulkan/rhi_context_types.zig index af093b81..ff09516c 100644 --- a/src/engine/graphics/vulkan/rhi_context_types.zig +++ b/src/engine/graphics/vulkan/rhi_context_types.zig @@ -130,6 +130,8 @@ const DrawState = struct { current_roughness_texture: rhi.TextureHandle, current_displacement_texture: rhi.TextureHandle, current_env_texture: rhi.TextureHandle, + current_water_reflection_texture: rhi.TextureHandle, + current_scene_depth_texture: rhi.TextureHandle, current_lpv_texture: rhi.TextureHandle, current_lpv_texture_g: rhi.TextureHandle, current_lpv_texture_b: rhi.TextureHandle, @@ -142,6 +144,8 @@ const DrawState = struct { bound_roughness_texture: rhi.TextureHandle, bound_displacement_texture: rhi.TextureHandle, bound_env_texture: rhi.TextureHandle, + bound_water_reflection_texture: rhi.TextureHandle = 0, + bound_scene_depth_texture: rhi.TextureHandle = 0, bound_lpv_texture: rhi.TextureHandle, bound_lpv_texture_g: rhi.TextureHandle = 0, bound_lpv_texture_b: rhi.TextureHandle = 0, @@ -175,6 +179,7 @@ const RuntimeState = struct { frame_index: usize, image_index: u32, clear_color: [4]f32 = .{ 0.07, 0.08, 0.1, 1.0 }, + first_main_pass_draw_logged: bool = false, }; const TimingState = struct { diff --git a/src/engine/graphics/vulkan/rhi_draw_submission.zig b/src/engine/graphics/vulkan/rhi_draw_submission.zig index 6965cdb8..edb8ce4b 100644 --- a/src/engine/graphics/vulkan/rhi_draw_submission.zig +++ b/src/engine/graphics/vulkan/rhi_draw_submission.zig @@ -19,9 +19,9 @@ const ShadowModelUniforms = extern struct { pub fn drawIndexed(ctx: anytype, vbo_handle: rhi.BufferHandle, ebo_handle: rhi.BufferHandle, count: u32) void { if (!ctx.frames.frame_in_progress) return; - if (!ctx.runtime.main_pass_active and !ctx.shadow_system.pass_active and !ctx.runtime.g_pass_active) pass_orchestration.beginMainPassInternal(ctx); + if (!ctx.runtime.main_pass_active and !ctx.shadow_system.pass_active and !ctx.runtime.g_pass_active and !ctx.water_system.pass_active) pass_orchestration.beginMainPassInternal(ctx); - if (!ctx.runtime.main_pass_active and !ctx.shadow_system.pass_active and !ctx.runtime.g_pass_active) return; + if (!ctx.runtime.main_pass_active and !ctx.shadow_system.pass_active and !ctx.runtime.g_pass_active and !ctx.water_system.pass_active) return; const vbo_opt = ctx.resources.buffers.get(vbo_handle); const ebo_opt = ctx.resources.buffers.get(ebo_handle); @@ -32,13 +32,18 @@ pub fn drawIndexed(ctx: anytype, vbo_handle: rhi.BufferHandle, ebo_handle: rhi.B const command_buffer = ctx.frames.command_buffers[ctx.frames.current_frame]; if (!ctx.draw.terrain_pipeline_bound) { - const selected_pipeline = if (ctx.options.wireframe_enabled and ctx.pipeline_manager.wireframe_pipeline != null) + const selected_pipeline = if (ctx.water_system.pass_active) + if (ctx.options.wireframe_enabled and ctx.water_system.reflection_wireframe_pipeline != null) + ctx.water_system.reflection_wireframe_pipeline + else + ctx.water_system.reflection_terrain_pipeline + else if (ctx.options.wireframe_enabled and ctx.pipeline_manager.wireframe_pipeline != null) ctx.pipeline_manager.wireframe_pipeline else ctx.pipeline_manager.terrain_pipeline; if (selected_pipeline == null) return; c.vkCmdBindPipeline(command_buffer, c.VK_PIPELINE_BIND_POINT_GRAPHICS, selected_pipeline); - ctx.draw.terrain_pipeline_bound = true; + ctx.draw.terrain_pipeline_bound = !ctx.water_system.pass_active; } const descriptor_set = &ctx.descriptors.descriptor_sets[ctx.frames.current_frame]; @@ -55,9 +60,9 @@ pub fn drawIndexed(ctx: anytype, vbo_handle: rhi.BufferHandle, ebo_handle: rhi.B pub fn drawIndirect(ctx: anytype, handle: rhi.BufferHandle, command_buffer: rhi.BufferHandle, offset: usize, draw_count: u32, stride: u32) void { if (!ctx.frames.frame_in_progress) return; - if (!ctx.runtime.main_pass_active and !ctx.shadow_system.pass_active and !ctx.runtime.g_pass_active) pass_orchestration.beginMainPassInternal(ctx); + if (!ctx.runtime.main_pass_active and !ctx.shadow_system.pass_active and !ctx.runtime.g_pass_active and !ctx.water_system.pass_active) pass_orchestration.beginMainPassInternal(ctx); - if (!ctx.runtime.main_pass_active and !ctx.shadow_system.pass_active and !ctx.runtime.g_pass_active) return; + if (!ctx.runtime.main_pass_active and !ctx.shadow_system.pass_active and !ctx.runtime.g_pass_active and !ctx.water_system.pass_active) return; const use_shadow = ctx.shadow_system.pass_active; const use_g_pass = ctx.runtime.g_pass_active; @@ -81,7 +86,12 @@ pub fn drawIndirect(ctx: anytype, handle: rhi.BufferHandle, command_buffer: rhi. c.vkCmdBindPipeline(cb, c.VK_PIPELINE_BIND_POINT_GRAPHICS, ctx.pipeline_manager.g_pipeline); } else { if (!ctx.draw.terrain_pipeline_bound) { - const selected_pipeline = if (ctx.options.wireframe_enabled and ctx.pipeline_manager.wireframe_pipeline != null) + const selected_pipeline = if (ctx.water_system.pass_active) + if (ctx.options.wireframe_enabled and ctx.water_system.reflection_wireframe_pipeline != null) + ctx.water_system.reflection_wireframe_pipeline + else + ctx.water_system.reflection_terrain_pipeline + else if (ctx.options.wireframe_enabled and ctx.pipeline_manager.wireframe_pipeline != null) ctx.pipeline_manager.wireframe_pipeline else ctx.pipeline_manager.terrain_pipeline; @@ -90,7 +100,7 @@ pub fn drawIndirect(ctx: anytype, handle: rhi.BufferHandle, command_buffer: rhi. return; } c.vkCmdBindPipeline(cb, c.VK_PIPELINE_BIND_POINT_GRAPHICS, selected_pipeline); - ctx.draw.terrain_pipeline_bound = true; + ctx.draw.terrain_pipeline_bound = !ctx.water_system.pass_active and selected_pipeline == ctx.pipeline_manager.terrain_pipeline; } } @@ -147,7 +157,7 @@ pub fn drawIndirect(ctx: anytype, handle: rhi.BufferHandle, command_buffer: rhi. const draw_offset = offset + @as(usize, draw_index) * stride_bytes; c.vkCmdDrawIndirect(cb, cmd.buffer, @intCast(draw_offset), 1, stride); } - log.log.info("drawIndirect: MDI unsupported - drew {} draws via single-draw fallback", .{draw_count}); + log.log.trace("drawIndirect: MDI unsupported - drew {} draws via single-draw fallback", .{draw_count}); } } } @@ -156,7 +166,7 @@ pub fn drawIndirect(ctx: anytype, handle: rhi.BufferHandle, command_buffer: rhi. pub fn drawInstance(ctx: anytype, handle: rhi.BufferHandle, count: u32, instance_index: u32) void { if (!ctx.frames.frame_in_progress) return; - if (!ctx.runtime.main_pass_active and !ctx.shadow_system.pass_active and !ctx.runtime.g_pass_active) pass_orchestration.beginMainPassInternal(ctx); + if (!ctx.runtime.main_pass_active and !ctx.shadow_system.pass_active and !ctx.runtime.g_pass_active and !ctx.water_system.pass_active) pass_orchestration.beginMainPassInternal(ctx); const use_shadow = ctx.shadow_system.pass_active; const use_g_pass = ctx.runtime.g_pass_active; @@ -178,13 +188,18 @@ pub fn drawInstance(ctx: anytype, handle: rhi.BufferHandle, count: u32, instance c.vkCmdBindPipeline(command_buffer, c.VK_PIPELINE_BIND_POINT_GRAPHICS, ctx.pipeline_manager.g_pipeline); } else { if (!ctx.draw.terrain_pipeline_bound) { - const selected_pipeline = if (ctx.options.wireframe_enabled and ctx.pipeline_manager.wireframe_pipeline != null) + const selected_pipeline = if (ctx.water_system.pass_active) + if (ctx.options.wireframe_enabled and ctx.water_system.reflection_wireframe_pipeline != null) + ctx.water_system.reflection_wireframe_pipeline + else + ctx.water_system.reflection_terrain_pipeline + else if (ctx.options.wireframe_enabled and ctx.pipeline_manager.wireframe_pipeline != null) ctx.pipeline_manager.wireframe_pipeline else ctx.pipeline_manager.terrain_pipeline; if (selected_pipeline == null) return; c.vkCmdBindPipeline(command_buffer, c.VK_PIPELINE_BIND_POINT_GRAPHICS, selected_pipeline); - ctx.draw.terrain_pipeline_bound = true; + ctx.draw.terrain_pipeline_bound = !ctx.water_system.pass_active and selected_pipeline == ctx.pipeline_manager.terrain_pipeline; } } @@ -215,7 +230,10 @@ pub fn drawInstance(ctx: anytype, handle: rhi.BufferHandle, count: u32, instance } pub fn drawOffset(ctx: anytype, handle: rhi.BufferHandle, count: u32, mode: rhi.DrawMode, offset: usize) void { - if (!ctx.frames.frame_in_progress) return; + if (!ctx.frames.frame_in_progress) { + log.log.warn("drawOffset: no frame in progress", .{}); + return; + } if (ctx.post_process.pass_active) { const command_buffer = ctx.frames.command_buffers[ctx.frames.current_frame]; @@ -224,9 +242,15 @@ pub fn drawOffset(ctx: anytype, handle: rhi.BufferHandle, count: u32, mode: rhi. return; } - if (!ctx.runtime.main_pass_active and !ctx.shadow_system.pass_active and !ctx.runtime.g_pass_active) pass_orchestration.beginMainPassInternal(ctx); + if (!ctx.runtime.main_pass_active and !ctx.shadow_system.pass_active and !ctx.runtime.g_pass_active and !ctx.water_system.pass_active) { + log.log.warn("drawOffset: beginning main pass internally", .{}); + pass_orchestration.beginMainPassInternal(ctx); + } - if (!ctx.runtime.main_pass_active and !ctx.shadow_system.pass_active and !ctx.runtime.g_pass_active) return; + if (!ctx.runtime.main_pass_active and !ctx.shadow_system.pass_active and !ctx.runtime.g_pass_active and !ctx.water_system.pass_active) { + log.log.warn("drawOffset: still no main pass after beginMainPassInternal", .{}); + return; + } const use_shadow = ctx.shadow_system.pass_active; const use_g_pass = ctx.runtime.g_pass_active; @@ -261,7 +285,16 @@ pub fn drawOffset(ctx: anytype, handle: rhi.BufferHandle, count: u32, mode: rhi. } else { const needs_rebinding = !ctx.draw.terrain_pipeline_bound or ctx.ui.selection_mode or mode == .lines; if (needs_rebinding) { - const selected_pipeline = if (ctx.ui.selection_mode and ctx.pipeline_manager.selection_pipeline != null) + const selected_pipeline = if (ctx.water_system.pass_active) + if (ctx.ui.selection_mode and ctx.water_system.reflection_selection_pipeline != null) + ctx.water_system.reflection_selection_pipeline + else if (mode == .lines and ctx.water_system.reflection_line_pipeline != null) + ctx.water_system.reflection_line_pipeline + else if (ctx.options.wireframe_enabled and ctx.water_system.reflection_wireframe_pipeline != null) + ctx.water_system.reflection_wireframe_pipeline + else + ctx.water_system.reflection_terrain_pipeline + else if (ctx.ui.selection_mode and ctx.pipeline_manager.selection_pipeline != null) ctx.pipeline_manager.selection_pipeline else if (mode == .lines and ctx.pipeline_manager.line_pipeline != null) ctx.pipeline_manager.line_pipeline @@ -269,9 +302,12 @@ pub fn drawOffset(ctx: anytype, handle: rhi.BufferHandle, count: u32, mode: rhi. ctx.pipeline_manager.wireframe_pipeline else ctx.pipeline_manager.terrain_pipeline; - if (selected_pipeline == null) return; + if (selected_pipeline == null) { + log.log.warn("drawOffset: selected_pipeline is null", .{}); + return; + } c.vkCmdBindPipeline(command_buffer, c.VK_PIPELINE_BIND_POINT_GRAPHICS, selected_pipeline); - ctx.draw.terrain_pipeline_bound = (selected_pipeline == ctx.pipeline_manager.terrain_pipeline); + ctx.draw.terrain_pipeline_bound = !ctx.water_system.pass_active and selected_pipeline == ctx.pipeline_manager.terrain_pipeline; } const descriptor_set = if (ctx.draw.lod_mode) @@ -301,6 +337,8 @@ pub fn drawOffset(ctx: anytype, handle: rhi.BufferHandle, count: u32, mode: rhi. const offset_vbo: c.VkDeviceSize = @intCast(offset); c.vkCmdBindVertexBuffers(command_buffer, 0, 1, &vbo.buffer, &offset_vbo); c.vkCmdDraw(command_buffer, count, 1, 0, 0); + } else { + log.log.warn("drawOffset: vertex buffer not found (handle={})", .{handle}); } } diff --git a/src/engine/graphics/vulkan/rhi_frame_orchestration.zig b/src/engine/graphics/vulkan/rhi_frame_orchestration.zig index 3d5f1905..b4e2ed40 100644 --- a/src/engine/graphics/vulkan/rhi_frame_orchestration.zig +++ b/src/engine/graphics/vulkan/rhi_frame_orchestration.zig @@ -58,10 +58,6 @@ pub fn recreateSwapchainInternal(ctx: anytype) void { ctx.resources.destroyTexture(ctx.water_system.reflection_texture_handle); ctx.water_system.reflection_texture_handle = 0; } - setup.createWaterResources(ctx) catch |err| { - log.log.errWithTrace("Failed to recreate water resources: {}", .{err}); - return; - }; ctx.render_pass_manager.createMainRenderPass(ctx.vulkan_device.vk_device, ctx.swapchain.getExtent(), ctx.options.msaa_samples) catch |err| { log.log.errWithTrace("Failed to recreate render pass: {}", .{err}); return; @@ -70,6 +66,18 @@ pub fn recreateSwapchainInternal(ctx: anytype) void { log.log.errWithTrace("Failed to recreate pipelines: {}", .{err}); return; }; + setup.createWaterResources(ctx) catch |err| { + log.log.errWithTrace("Failed to recreate water resources: {}", .{err}); + return; + }; + ctx.water_system.createWaterPipeline(ctx.allocator, ctx.vulkan_device.vk_device, ctx.render_pass_manager.hdr_render_pass) catch |err| { + log.log.errWithTrace("Failed to recreate water pipeline: {}", .{err}); + return; + }; + ctx.water_system.createReflectionTerrainPipelines(ctx.allocator, ctx.vulkan_device.vk_device, ctx.pipeline_manager.pipeline_layout) catch |err| { + log.log.errWithTrace("Failed to recreate reflection terrain pipelines: {}", .{err}); + return; + }; setup.createPostProcessResources(ctx) catch |err| { log.log.errWithTrace("Failed to recreate post-process resources: {}", .{err}); return; @@ -137,6 +145,7 @@ pub fn recreateSwapchainInternal(ctx: anytype) void { pub fn prepareFrameState(ctx: anytype) void { ctx.runtime.draw_call_count = 0; + ctx.runtime.first_main_pass_draw_logged = false; ctx.runtime.main_pass_active = false; ctx.shadow_system.pass_active = false; ctx.runtime.post_process_ran_this_frame = false; @@ -180,6 +189,8 @@ pub fn prepareFrameState(ctx: anytype) void { const cur_rou = ctx.draw.current_roughness_texture; const cur_dis = ctx.draw.current_displacement_texture; const cur_env = ctx.draw.current_env_texture; + const cur_water_reflection = ctx.draw.current_water_reflection_texture; + const cur_scene_depth = ctx.draw.current_scene_depth_texture; const cur_lpv = ctx.draw.current_lpv_texture; const cur_lpv_g = ctx.draw.current_lpv_texture_g; const cur_lpv_b = ctx.draw.current_lpv_texture_b; @@ -190,6 +201,8 @@ pub fn prepareFrameState(ctx: anytype) void { if (ctx.draw.bound_roughness_texture != cur_rou) needs_update = true; if (ctx.draw.bound_displacement_texture != cur_dis) needs_update = true; if (ctx.draw.bound_env_texture != cur_env) needs_update = true; + if (ctx.draw.bound_water_reflection_texture != cur_water_reflection) needs_update = true; + if (ctx.draw.bound_scene_depth_texture != cur_scene_depth) needs_update = true; if (ctx.draw.bound_lpv_texture != cur_lpv) needs_update = true; if (ctx.draw.bound_lpv_texture_g != cur_lpv_g) needs_update = true; if (ctx.draw.bound_lpv_texture_b != cur_lpv_b) needs_update = true; @@ -205,6 +218,8 @@ pub fn prepareFrameState(ctx: anytype) void { ctx.draw.bound_roughness_texture = cur_rou; ctx.draw.bound_displacement_texture = cur_dis; ctx.draw.bound_env_texture = cur_env; + ctx.draw.bound_water_reflection_texture = cur_water_reflection; + ctx.draw.bound_scene_depth_texture = cur_scene_depth; ctx.draw.bound_lpv_texture = cur_lpv; ctx.draw.bound_lpv_texture_g = cur_lpv_g; ctx.draw.bound_lpv_texture_b = cur_lpv_b; @@ -216,6 +231,7 @@ pub fn prepareFrameState(ctx: anytype) void { log.log.err("CRITICAL: Descriptor set for frame {} is NULL!", .{ctx.frames.current_frame}); return; } + var writes: [16]c.VkWriteDescriptorSet = undefined; var write_count: u32 = 0; var image_infos: [16]c.VkDescriptorImageInfo = undefined; @@ -230,6 +246,8 @@ pub fn prepareFrameState(ctx: anytype) void { .{ .handle = cur_rou, .binding = bindings.ROUGHNESS_TEXTURE, .is_3d = false }, .{ .handle = cur_dis, .binding = bindings.DISPLACEMENT_TEXTURE, .is_3d = false }, .{ .handle = cur_env, .binding = bindings.ENV_TEXTURE, .is_3d = false }, + .{ .handle = cur_water_reflection, .binding = bindings.WATER_REFLECTION_TEXTURE, .is_3d = false }, + .{ .handle = cur_scene_depth, .binding = bindings.SCENE_DEPTH_TEXTURE, .is_3d = false }, .{ .handle = cur_lpv, .binding = bindings.LPV_TEXTURE, .is_3d = true }, .{ .handle = cur_lpv_g, .binding = bindings.LPV_TEXTURE_G, .is_3d = true }, .{ .handle = cur_lpv_b, .binding = bindings.LPV_TEXTURE_B, .is_3d = true }, @@ -237,7 +255,10 @@ pub fn prepareFrameState(ctx: anytype) void { for (atlas_slots) |slot| { const fallback = if (slot.is_3d) dummy_tex_3d_entry else dummy_tex_entry; - const entry = ctx.resources.textures.get(slot.handle) orelse fallback; + const entry = if (ctx.resources.textures.get(slot.handle)) |tex| + if (tex.format == .depth) fallback else tex + else + fallback; if (entry) |tex| { image_infos[info_count] = .{ .sampler = tex.sampler, @@ -256,52 +277,51 @@ pub fn prepareFrameState(ctx: anytype) void { } } - if (ctx.shadow_system.shadow_sampler == null) { - log.log.err("CRITICAL: Shadow sampler is NULL!", .{}); - } - if (ctx.shadow_system.shadow_sampler_regular == null) { - log.log.err("CRITICAL: Shadow regular sampler is NULL!", .{}); - } - if (ctx.shadow_system.shadow_image_view == null) { - log.log.err("CRITICAL: Shadow image view is NULL!", .{}); + if (ctx.shadow_system.shadow_image_view != null and ctx.shadow_system.shadow_sampler != null) { + image_infos[info_count] = .{ + .sampler = ctx.shadow_system.shadow_sampler, + .imageView = ctx.shadow_system.shadow_image_view, + .imageLayout = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + }; + writes[write_count] = std.mem.zeroes(c.VkWriteDescriptorSet); + writes[write_count].sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[write_count].dstSet = ctx.descriptors.descriptor_sets[ctx.frames.current_frame]; + writes[write_count].dstBinding = bindings.SHADOW_COMPARE_TEXTURE; + writes[write_count].descriptorType = c.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[write_count].descriptorCount = 1; + writes[write_count].pImageInfo = &image_infos[info_count]; + write_count += 1; + info_count += 1; + + const regular_shadow_sampler = ctx.shadow_system.shadow_sampler_regular orelse ctx.shadow_system.shadow_sampler; + if (regular_shadow_sampler != null) { + image_infos[info_count] = .{ + .sampler = regular_shadow_sampler, + .imageView = ctx.shadow_system.shadow_image_view, + .imageLayout = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + }; + writes[write_count] = std.mem.zeroes(c.VkWriteDescriptorSet); + writes[write_count].sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[write_count].dstSet = ctx.descriptors.descriptor_sets[ctx.frames.current_frame]; + writes[write_count].dstBinding = bindings.SHADOW_REGULAR_TEXTURE; + writes[write_count].descriptorType = c.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[write_count].descriptorCount = 1; + writes[write_count].pImageInfo = &image_infos[info_count]; + write_count += 1; + info_count += 1; + } } - image_infos[info_count] = .{ - .sampler = ctx.shadow_system.shadow_sampler, - .imageView = ctx.shadow_system.shadow_image_view, - .imageLayout = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - }; - writes[write_count] = std.mem.zeroes(c.VkWriteDescriptorSet); - writes[write_count].sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[write_count].dstSet = ctx.descriptors.descriptor_sets[ctx.frames.current_frame]; - writes[write_count].dstBinding = bindings.SHADOW_COMPARE_TEXTURE; - writes[write_count].descriptorType = c.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - writes[write_count].descriptorCount = 1; - writes[write_count].pImageInfo = &image_infos[info_count]; - write_count += 1; - info_count += 1; - - image_infos[info_count] = .{ - .sampler = if (ctx.shadow_system.shadow_sampler_regular != null) ctx.shadow_system.shadow_sampler_regular else ctx.shadow_system.shadow_sampler, - .imageView = ctx.shadow_system.shadow_image_view, - .imageLayout = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - }; - writes[write_count] = std.mem.zeroes(c.VkWriteDescriptorSet); - writes[write_count].sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[write_count].dstSet = ctx.descriptors.descriptor_sets[ctx.frames.current_frame]; - writes[write_count].dstBinding = bindings.SHADOW_REGULAR_TEXTURE; - writes[write_count].descriptorType = c.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - writes[write_count].descriptorCount = 1; - writes[write_count].pImageInfo = &image_infos[info_count]; - write_count += 1; - info_count += 1; if (write_count > 0) { c.vkUpdateDescriptorSets(ctx.vulkan_device.vk_device, write_count, &writes[0], 0, null); - for (0..write_count) |i| { - writes[i].dstSet = ctx.descriptors.lod_descriptor_sets[ctx.frames.current_frame]; + const lod_descriptor_set = ctx.descriptors.lod_descriptor_sets[ctx.frames.current_frame]; + if (lod_descriptor_set != null) { + for (0..write_count) |i| { + writes[i].dstSet = lod_descriptor_set; + } + c.vkUpdateDescriptorSets(ctx.vulkan_device.vk_device, write_count, &writes[0], 0, null); } - c.vkUpdateDescriptorSets(ctx.vulkan_device.vk_device, write_count, &writes[0], 0, null); } ctx.draw.descriptors_dirty[ctx.frames.current_frame] = false; diff --git a/src/engine/graphics/vulkan/rhi_init_deinit.zig b/src/engine/graphics/vulkan/rhi_init_deinit.zig index 7c875e6c..bac992ff 100644 --- a/src/engine/graphics/vulkan/rhi_init_deinit.zig +++ b/src/engine/graphics/vulkan/rhi_init_deinit.zig @@ -81,7 +81,6 @@ pub fn initContext(ctx: anytype, allocator: std.mem.Allocator, render_device: ?* try setup.createGPassResources(ctx); try setup.createSSAOResources(ctx); try setup.createTAAResources(ctx); - try setup.createWaterResources(ctx); try ctx.render_pass_manager.createMainRenderPass( ctx.vulkan_device.vk_device, @@ -97,6 +96,10 @@ pub fn initContext(ctx: anytype, allocator: std.mem.Allocator, render_device: ?* ctx.options.msaa_samples, ); + try setup.createWaterResources(ctx); + try ctx.water_system.createWaterPipeline(ctx.allocator, ctx.vulkan_device.vk_device, ctx.render_pass_manager.hdr_render_pass); + try ctx.water_system.createReflectionTerrainPipelines(ctx.allocator, ctx.vulkan_device.vk_device, ctx.pipeline_manager.pipeline_layout); + try setup.createPostProcessResources(ctx); try setup.createSwapchainUIResources(ctx); @@ -127,6 +130,8 @@ pub fn initContext(ctx: anytype, allocator: std.mem.Allocator, render_device: ?* ctx.draw.current_roughness_texture = ctx.draw.dummy_roughness_texture; ctx.draw.current_displacement_texture = ctx.draw.dummy_roughness_texture; ctx.draw.current_env_texture = ctx.draw.dummy_texture; + ctx.draw.current_water_reflection_texture = ctx.draw.dummy_texture; + ctx.draw.current_scene_depth_texture = ctx.draw.dummy_texture; ctx.draw.current_lpv_texture = ctx.draw.dummy_texture_3d; ctx.draw.current_lpv_texture_g = ctx.draw.dummy_texture_3d; ctx.draw.current_lpv_texture_b = ctx.draw.dummy_texture_3d; diff --git a/src/engine/graphics/vulkan/rhi_pass_orchestration.zig b/src/engine/graphics/vulkan/rhi_pass_orchestration.zig index acdba8fa..03c00754 100644 --- a/src/engine/graphics/vulkan/rhi_pass_orchestration.zig +++ b/src/engine/graphics/vulkan/rhi_pass_orchestration.zig @@ -438,6 +438,32 @@ pub fn endFrame(ctx: anytype) void { const transfer_cb = ctx.resources.getTransferCommandBuffer(); + if (!ctx.resources.transfer.is_dedicated) { + if (transfer_cb) |cb| { + ctx.resources.transfer.recordPendingCopies(cb); + + var barrier = std.mem.zeroes(c.VkBufferMemoryBarrier); + barrier.sType = c.VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; + barrier.srcAccessMask = c.VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = ctx.resources.transfer.getPendingDstAccessMask(); + barrier.srcQueueFamilyIndex = c.VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = c.VK_QUEUE_FAMILY_IGNORED; + + c.vkCmdPipelineBarrier( + cb, + c.VK_PIPELINE_STAGE_TRANSFER_BIT, + c.VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | c.VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | c.VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT, + 0, + 0, + null, + 1, + &barrier, + 0, + null, + ); + } + } + if (ctx.resources.transfer.is_dedicated and transfer_cb != null) { ctx.resources.submitTransfer() catch |err| { log.log.errWithTrace("Failed to submit transfer: {}", .{err}); @@ -448,6 +474,9 @@ pub fn endFrame(ctx: anytype) void { ctx.frames.endFrame(&ctx.swapchain, transfer_cb, transfer_sem) catch |err| { log.log.errWithTrace("endFrame failed: {}", .{err}); + if (err == error.GpuLost) { + ctx.runtime.gpu_fault_detected = true; + } }; if (transfer_cb != null) { diff --git a/src/engine/graphics/vulkan/rhi_render_state.zig b/src/engine/graphics/vulkan/rhi_render_state.zig index fbd18008..d1fb5d1c 100644 --- a/src/engine/graphics/vulkan/rhi_render_state.zig +++ b/src/engine/graphics/vulkan/rhi_render_state.zig @@ -44,8 +44,8 @@ pub fn updateGlobalUniforms(ctx: anytype, view_proj: Mat4, cam_pos: Vec3, sun_di .cloud_wind_offset = .{ cloud_params.wind_offset_x, cloud_params.wind_offset_z, cloud_params.cloud_scale, cloud_params.cloud_coverage }, .params = .{ time_val, fog_density, if (fog_enabled) 1.0 else 0.0, sun_intensity }, .lighting = .{ ambient, if (use_texture) 1.0 else 0.0, if (cloud_params.pbr_enabled) 1.0 else 0.0, cloud_params.shadow.strength }, - .cloud_params = .{ cloud_params.cloud_height, if (cloud_params.cloud_shadows) 1.0 else 0.0, 0.0, 0.0 }, - .shadow_params = .{ @floatFromInt(cloud_params.shadow.pcf_samples), if (cloud_params.shadow.cascade_blend) 1.0 else 0.0, 0.0, 0.0 }, + .cloud_params = .{ cloud_params.cloud_height, if (cloud_params.cloud_shadows) 1.0 else 0.0, if (cloud_params.pbr_enabled) 1.0 else 0.0, 0.0 }, + .shadow_params = .{ @floatFromInt(cloud_params.shadow.pcf_samples), if (cloud_params.shadow.cascade_blend) 1.0 else 0.0, cloud_params.shadow.strength, 0.0 }, .pbr_params = .{ @floatFromInt(cloud_params.pbr_quality), cloud_params.exposure, cloud_params.saturation, if (cloud_params.ssao_enabled) 1.0 else 0.0 }, .volumetric_params = .{ if (cloud_params.volumetric_enabled) 1.0 else 0.0, cloud_params.volumetric_density, @floatFromInt(cloud_params.volumetric_steps), cloud_params.volumetric_scattering }, .viewport_size = .{ @floatFromInt(ctx.swapchain.swapchain.extent.width), @floatFromInt(ctx.swapchain.swapchain.extent.height), if (ctx.options.debug_shadows_active) 1.0 else 0.0, @floatFromInt(ctx.options.shadow_debug_channel) }, @@ -132,7 +132,7 @@ pub fn applyPendingDescriptorUpdates(ctx: anytype, frame_index: usize) void { pub fn beginCloudPass(ctx: anytype, params: rhi.CloudParams) void { if (!ctx.frames.frame_in_progress) return; - if (!ctx.runtime.main_pass_active) pass_orchestration.beginMainPassInternal(ctx); + if (!ctx.runtime.main_pass_active and !ctx.water_system.pass_active) pass_orchestration.beginMainPassInternal(ctx); if (!ctx.runtime.main_pass_active) return; if (ctx.pipeline_manager.cloud_pipeline == null) return; diff --git a/src/engine/graphics/vulkan/rhi_resource_setup.zig b/src/engine/graphics/vulkan/rhi_resource_setup.zig index 575e22be..bf747746 100644 --- a/src/engine/graphics/vulkan/rhi_resource_setup.zig +++ b/src/engine/graphics/vulkan/rhi_resource_setup.zig @@ -417,7 +417,6 @@ pub fn createWaterResources(ctx: anytype) !void { extent.height, ctx.descriptors.descriptor_set_layout, ); - try ctx.water_system.createWaterPipeline(ctx.allocator, ctx.vulkan_device.vk_device); ctx.water_system.reflection_texture_handle = try ctx.resources.registerExternalTexture( ctx.water_system.extent.width, diff --git a/src/engine/graphics/vulkan/rhi_state_control.zig b/src/engine/graphics/vulkan/rhi_state_control.zig index 2cf2758a..76aa17ce 100644 --- a/src/engine/graphics/vulkan/rhi_state_control.zig +++ b/src/engine/graphics/vulkan/rhi_state_control.zig @@ -62,6 +62,16 @@ pub fn recover(ctx: anytype) !void { ctx.vulkan_device.recovery_count += 1; log.log.info("RHI: Attempting GPU recovery (Attempt {d}/{d})...", .{ ctx.vulkan_device.recovery_count, ctx.vulkan_device.max_recovery_attempts }); + if (ctx.frames.frame_in_progress) { + ctx.frames.abortFrame(); + } + ctx.runtime.main_pass_active = false; + ctx.shadow_system.pass_active = false; + ctx.runtime.g_pass_active = false; + ctx.runtime.ssao_pass_active = false; + ctx.draw.descriptors_updated = false; + ctx.draw.bound_texture = 0; + _ = c.vkDeviceWaitIdle(ctx.vulkan_device.vk_device); ctx.runtime.gpu_fault_detected = false; diff --git a/src/engine/graphics/vulkan/rhi_ui_submission.zig b/src/engine/graphics/vulkan/rhi_ui_submission.zig index c33ed029..02c37e1d 100644 --- a/src/engine/graphics/vulkan/rhi_ui_submission.zig +++ b/src/engine/graphics/vulkan/rhi_ui_submission.zig @@ -48,7 +48,7 @@ pub fn begin2DPass(ctx: anytype, screen_width: f32, screen_height: f32) void { return; } } else { - if (!ctx.runtime.main_pass_active) pass_orchestration.beginMainPassInternal(ctx); + if (!ctx.runtime.main_pass_active and !ctx.water_system.pass_active) pass_orchestration.beginMainPassInternal(ctx); if (!ctx.runtime.main_pass_active) { return; } diff --git a/src/engine/graphics/vulkan/rhi_water_bridge.zig b/src/engine/graphics/vulkan/rhi_water_bridge.zig index 6fdecaac..32df2945 100644 --- a/src/engine/graphics/vulkan/rhi_water_bridge.zig +++ b/src/engine/graphics/vulkan/rhi_water_bridge.zig @@ -7,6 +7,7 @@ const log = @import("../../core/log.zig"); pub fn beginWaterReflectionPassInternal(ctx: anytype) void { if (!ctx.frames.frame_in_progress) return; const command_buffer = ctx.frames.command_buffers[ctx.frames.current_frame]; + ctx.draw.terrain_pipeline_bound = false; ctx.water_system.beginReflectionPass(command_buffer); } diff --git a/src/engine/graphics/vulkan/transfer_queue.zig b/src/engine/graphics/vulkan/transfer_queue.zig index e3ce3c72..1c0f677f 100644 --- a/src/engine/graphics/vulkan/transfer_queue.zig +++ b/src/engine/graphics/vulkan/transfer_queue.zig @@ -318,7 +318,12 @@ pub const TransferQueue = struct { if (!self.transfer_ready[self.current_frame]) return; const cb = self.command_buffers[self.current_frame]; - try Utils.checkVk(c.vkEndCommandBuffer(cb)); + const end_result = c.vkEndCommandBuffer(cb); + if (end_result != c.VK_SUCCESS) { + self.transfer_ready[self.current_frame] = false; + if (end_result == c.VK_ERROR_DEVICE_LOST) return error.GpuLost; + return error.VulkanError; + } var submit_info = std.mem.zeroes(c.VkSubmitInfo); submit_info.sType = c.VK_STRUCTURE_TYPE_SUBMIT_INFO; @@ -331,9 +336,12 @@ pub const TransferQueue = struct { const result = c.vkQueueSubmit(self.queue, 1, &submit_info, self.fence); queue_mutex.unlock(); + if (result == c.VK_ERROR_DEVICE_LOST) return error.GpuLost; if (result != c.VK_SUCCESS) return error.VulkanError; - try Utils.checkVk(c.vkWaitForFences(vk_device, 1, &self.fence, c.VK_TRUE, std.math.maxInt(u64))); + const wait_result = c.vkWaitForFences(vk_device, 1, &self.fence, c.VK_TRUE, std.math.maxInt(u64)); + if (wait_result == c.VK_ERROR_DEVICE_LOST) return error.GpuLost; + try Utils.checkVk(wait_result); self.transfer_ready[self.current_frame] = false; } diff --git a/src/engine/graphics/vulkan/water_system.zig b/src/engine/graphics/vulkan/water_system.zig index 9e9b9228..c806db34 100644 --- a/src/engine/graphics/vulkan/water_system.zig +++ b/src/engine/graphics/vulkan/water_system.zig @@ -3,13 +3,14 @@ const c = @import("../../../c.zig").c; const rhi = @import("../rhi.zig"); const log = @import("../../core/log.zig"); const Utils = @import("utils.zig"); +const pipeline_specialized = @import("pipeline_specialized.zig"); const Vec3 = @import("../../math/vec3.zig").Vec3; const Mat4 = @import("../../math/mat4.zig").Mat4; // Match the engine's standard depth format and use an sRGB reflection target // because the sampled result is composited directly in the water shader. const DEPTH_FORMAT = c.VK_FORMAT_D32_SFLOAT; -const COLOR_FORMAT = c.VK_FORMAT_R8G8B8A8_SRGB; +const COLOR_FORMAT = c.VK_FORMAT_R16G16B16A16_SFLOAT; const PUSH_CONSTANT_SIZE_WATER: u32 = 256; pub const WATER_LEVEL: f32 = 64.0; @@ -28,6 +29,10 @@ pub const WaterSystem = struct { reflection_sampler: c.VkSampler = null, reflection_render_pass: c.VkRenderPass = null, reflection_framebuffer: c.VkFramebuffer = null, + reflection_terrain_pipeline: c.VkPipeline = null, + reflection_wireframe_pipeline: c.VkPipeline = null, + reflection_selection_pipeline: c.VkPipeline = null, + reflection_line_pipeline: c.VkPipeline = null, water_pipeline: c.VkPipeline = null, water_pipeline_layout: c.VkPipelineLayout = null, @@ -49,6 +54,10 @@ pub const WaterSystem = struct { pub fn destroyResources(self: *WaterSystem, device: c.VkDevice) void { if (self.water_pipeline) |p| c.vkDestroyPipeline(device, p, null); + if (self.reflection_terrain_pipeline) |p| c.vkDestroyPipeline(device, p, null); + if (self.reflection_wireframe_pipeline) |p| c.vkDestroyPipeline(device, p, null); + if (self.reflection_selection_pipeline) |p| c.vkDestroyPipeline(device, p, null); + if (self.reflection_line_pipeline) |p| c.vkDestroyPipeline(device, p, null); if (self.water_pipeline_layout) |l| c.vkDestroyPipelineLayout(device, l, null); if (self.reflection_framebuffer) |fb| c.vkDestroyFramebuffer(device, fb, null); if (self.reflection_render_pass) |rp| c.vkDestroyRenderPass(device, rp, null); @@ -62,6 +71,10 @@ pub const WaterSystem = struct { self.water_pipeline = null; self.water_pipeline_layout = null; + self.reflection_terrain_pipeline = null; + self.reflection_wireframe_pipeline = null; + self.reflection_selection_pipeline = null; + self.reflection_line_pipeline = null; self.reflection_framebuffer = null; self.reflection_render_pass = null; self.reflection_sampler = null; @@ -264,8 +277,9 @@ pub const WaterSystem = struct { log.log.info("WaterSystem: reflection target created ({}x{})", .{ half_w, half_h }); } - pub fn createWaterPipeline(self: *WaterSystem, allocator: std.mem.Allocator, device: c.VkDevice) !void { + pub fn createWaterPipeline(self: *WaterSystem, allocator: std.mem.Allocator, device: c.VkDevice, main_render_pass: c.VkRenderPass) !void { if (self.water_pipeline_layout == null) return; + if (main_render_pass == null) return error.InvalidRenderPass; const shader_registry = @import("shader_registry.zig"); @@ -366,13 +380,93 @@ pub const WaterSystem = struct { pipeline_info.pColorBlendState = &color_blending; pipeline_info.pDynamicState = &dynamic_state; pipeline_info.layout = self.water_pipeline_layout; - pipeline_info.renderPass = self.reflection_render_pass; + pipeline_info.renderPass = main_render_pass; pipeline_info.subpass = 0; try Utils.checkVk(c.vkCreateGraphicsPipelines(device, null, 1, &pipeline_info, null, &self.water_pipeline)); log.log.info("WaterSystem: water pipeline created", .{}); } + pub fn createReflectionTerrainPipelines( + self: *WaterSystem, + allocator: std.mem.Allocator, + device: c.VkDevice, + main_pipeline_layout: c.VkPipelineLayout, + ) !void { + if (self.reflection_render_pass == null) return error.InvalidRenderPass; + if (main_pipeline_layout == null) return error.InvalidPipelineLayout; + + const Owner = struct { + pipeline_layout: c.VkPipelineLayout, + terrain_pipeline: c.VkPipeline = null, + wireframe_pipeline: c.VkPipeline = null, + selection_pipeline: c.VkPipeline = null, + line_pipeline: c.VkPipeline = null, + g_pipeline: c.VkPipeline = null, + }; + + var owner = Owner{ .pipeline_layout = main_pipeline_layout }; + 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; + + const 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 = 2; + dynamic_state.pDynamicStates = &dynamic_states; + + 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 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_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 depth_stencil = std.mem.zeroes(c.VkPipelineDepthStencilStateCreateInfo); + depth_stencil.sType = c.VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; + depth_stencil.depthTestEnable = c.VK_TRUE; + depth_stencil.depthWriteEnable = c.VK_TRUE; + depth_stencil.depthCompareOp = c.VK_COMPARE_OP_GREATER_OR_EQUAL; + + 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; + + 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; + + try pipeline_specialized.createTerrainPipeline( + &owner, + allocator, + device, + self.reflection_render_pass, + &viewport_state, + &dynamic_state, + &input_assembly, + &rasterizer, + &multisampling, + &depth_stencil, + &color_blending, + c.VK_SAMPLE_COUNT_1_BIT, + null, + ); + + self.reflection_terrain_pipeline = owner.terrain_pipeline; + self.reflection_wireframe_pipeline = owner.wireframe_pipeline; + self.reflection_selection_pipeline = owner.selection_pipeline; + self.reflection_line_pipeline = owner.line_pipeline; + } + pub fn beginReflectionPass(self: *WaterSystem, command_buffer: c.VkCommandBuffer) void { if (self.reflection_render_pass == null or self.reflection_framebuffer == null) return; diff --git a/src/game/app.zig b/src/game/app.zig index 2077420e..7412a5c3 100644 --- a/src/game/app.zig +++ b/src/game/app.zig @@ -322,8 +322,28 @@ pub const App = struct { pub fn run(self: *App) !void { self.render_system.setViewport(self.input.interface().getWindowWidth(), self.input.interface().getWindowHeight()); log.log.info("=== ZigCraft ===", .{}); + var last_fault_count: u32 = self.render_system.getRHI().getFaultCount(); + var gpu_recovery_attempts: u32 = 0; while (!self.input.interface().shouldQuit()) { - try self.runSingleFrame(); + self.runSingleFrame() catch |err| { + log.log.err("Frame error: {}", .{err}); + return err; + }; + const current_faults = self.render_system.getRHI().getFaultCount(); + if (current_faults > last_fault_count) { + gpu_recovery_attempts += 1; + last_fault_count = current_faults; + if (gpu_recovery_attempts > 3) { + log.log.err("GPU lost after {d} recovery attempts. Exiting.", .{gpu_recovery_attempts}); + return error.GpuLost; + } + log.log.warn("GPU fault detected (total faults: {d}), attempting recovery ({d}/3)...", .{ current_faults, gpu_recovery_attempts }); + self.render_system.getRHI().recover() catch { + log.log.err("GPU recovery failed. Exiting.", .{}); + return error.GpuLost; + }; + log.log.info("GPU recovery step completed.", .{}); + } } } }; diff --git a/src/game/screens/graphics.zig b/src/game/screens/graphics.zig index 2cd7aed0..3a319b2c 100644 --- a/src/game/screens/graphics.zig +++ b/src/game/screens/graphics.zig @@ -129,7 +129,6 @@ pub const GraphicsScreen = struct { settings.render_distance_preset = @enumFromInt(next); const preset_cfg = render_settings_mod.getPresetConfig(settings.render_distance_preset); settings.render_distance = preset_cfg.lod_radii[0]; - settings.lod_enabled = true; } sy += row_height; diff --git a/src/game/screens/world.zig b/src/game/screens/world.zig index ac565edd..2271e605 100644 --- a/src/game/screens/world.zig +++ b/src/game/screens/world.zig @@ -221,22 +221,25 @@ pub const WorldScreen = struct { .time = self.session.atmosphere.time.time_of_day, }; - const ssao_enabled = ctx.settings.ssao_enabled and !render_system.getDisableSSAO() and !render_system.getDisableGPassDraw(); - const cloud_shadows_enabled = ctx.settings.cloud_shadows_enabled and !render_system.getDisableClouds(); + const safe_mode = render_system.getSafeMode(); + const ssao_enabled = ctx.settings.ssao_enabled and !render_system.getDisableSSAO() and !render_system.getDisableGPassDraw() and !safe_mode; + const cloud_shadows_enabled = ctx.settings.cloud_shadows_enabled and !render_system.getDisableClouds() and !safe_mode; const lpv_quality = resolveLPVQuality(ctx.settings.lpv_quality_preset); const lpv_system = render_system.getLPVSystem(); try lpv_system.setSettings( - ctx.settings.lpv_enabled, + ctx.settings.lpv_enabled and !safe_mode, ctx.settings.lpv_intensity, ctx.settings.lpv_cell_size, lpv_quality.propagation_iterations, lpv_quality.grid_size, lpv_quality.update_interval_frames, ); - rhi.timing().beginPassTiming("LPVPass"); - try lpv_system.update(self.session.world, camera.position, ctx.settings.debug_lpv_overlay_active); - rhi.timing().endPassTiming("LPVPass"); + if (!safe_mode) { + rhi.timing().beginPassTiming("LPVPass"); + try lpv_system.update(self.session.world, camera.position, ctx.settings.debug_lpv_overlay_active); + rhi.timing().endPassTiming("LPVPass"); + } const lpv_origin = lpv_system.getOrigin(); const cloud_params: rhi_pkg.CloudParams = blk: { @@ -254,19 +257,20 @@ pub const WorldScreen = struct { .cloud_coverage = p.cloud_coverage, .cloud_height = p.cloud_height, .base_color = self.session.clouds.base_color, - .pbr_enabled = ctx.settings.pbr_enabled and render_system.getAtlas().has_pbr, + .pbr_enabled = ctx.settings.pbr_enabled and render_system.getAtlas().has_pbr and !safe_mode, .shadow = .{ .distance = ctx.settings.shadow_distance, .resolution = ctx.settings.getShadowResolution(), .pcf_samples = ctx.settings.shadow_pcf_samples, .cascade_blend = ctx.settings.shadow_cascade_blend, .caster_distance = ctx.settings.shadow_caster_distance, + .strength = if (safe_mode) 0.0 else 0.35, }, .cloud_shadows = cloud_shadows_enabled, .pbr_quality = ctx.settings.pbr_quality, .exposure = ctx.settings.exposure, .saturation = ctx.settings.saturation, - .volumetric_enabled = ctx.settings.volumetric_lighting_enabled, + .volumetric_enabled = ctx.settings.volumetric_lighting_enabled and !safe_mode, .volumetric_density = ctx.settings.volumetric_density, .volumetric_steps = ctx.settings.volumetric_steps, .volumetric_scattering = ctx.settings.volumetric_scattering, @@ -281,7 +285,7 @@ pub const WorldScreen = struct { const skip_world_render = render_system.getSafeRenderMode(); if (!skip_world_render) { - try rhi.updateGlobalUniforms(view_proj_render, camera.position, self.session.atmosphere.celestial.sun_dir, self.session.atmosphere.sun_color, self.session.atmosphere.time.time_of_day, self.session.atmosphere.fog_color, self.session.atmosphere.fog_density, self.session.atmosphere.fog_enabled, self.session.atmosphere.sun_intensity, self.session.atmosphere.ambient_intensity, ctx.settings.textures_enabled, cloud_params); + try rhi.updateGlobalUniforms(view_proj_render, camera.position, self.session.atmosphere.celestial.sun_dir, self.session.atmosphere.sun_color, self.session.atmosphere.time.time_of_day, self.session.atmosphere.fog_color, self.session.atmosphere.fog_density, self.session.atmosphere.fog_enabled and !safe_mode, self.session.atmosphere.sun_intensity, self.session.atmosphere.ambient_intensity, ctx.settings.textures_enabled, cloud_params); const env_map_ptr = render_system.getEnvMapPtr(); const env_map_handle = if (env_map_ptr.*) |t| t.handle else 0; @@ -316,7 +320,7 @@ pub const WorldScreen = struct { .disable_shadow_draw = render_system.getDisableShadowDraw(), .disable_gpass_draw = render_system.getDisableGPassDraw(), .disable_ssao = render_system.getDisableSSAO(), - .disable_clouds = render_system.getDisableClouds(), + .disable_clouds = false, .fxaa_enabled = ctx.settings.fxaa_enabled and !ctx.settings.taa_enabled, .bloom_enabled = ctx.settings.bloom_enabled, .resolution_scale = resolution_scale, diff --git a/src/game/session.zig b/src/game/session.zig index 6e5e2806..964e9a64 100644 --- a/src/game/session.zig +++ b/src/game/session.zig @@ -179,10 +179,16 @@ pub const GameSession = struct { var ecs_render_system = try ECSRenderSystem.init(rhi.resourceManager()); errdefer ecs_render_system.deinit(); - const player = Player.init(Vec3.init(8, 100, 8), true); + const spawn_x: i32 = 8; + const spawn_z: i32 = 8; + const spawn_info = world.getColumnInfo(spawn_x, spawn_z); + const spawn_y: f32 = @floatFromInt(spawn_info.height + 16); + var player = Player.init(Vec3.init(@floatFromInt(spawn_x), spawn_y, @floatFromInt(spawn_z)), true); + // Aim toward the terrain so the first frame shows the ground. + player.camera.setYawPitch(player.camera.yaw, -std.math.degreesToRadians(35.0)); var atmosphere = Atmosphere.init(); - atmosphere.setTimeOfDay(0.25); + atmosphere.setTimeOfDay(0.5); session.* = .{ .allocator = allocator, diff --git a/src/game/settings/json_presets.zig b/src/game/settings/json_presets.zig index 2dcfd916..e0cf6093 100644 --- a/src/game/settings/json_presets.zig +++ b/src/game/settings/json_presets.zig @@ -156,7 +156,6 @@ pub fn apply(settings: *Settings, preset_idx: usize) void { settings.lpv_cell_size = config.lpv_cell_size; settings.lpv_grid_size = config.lpv_grid_size; settings.lpv_propagation_iterations = config.lpv_propagation_iterations; - settings.lod_enabled = config.lod_enabled; settings.render_distance = config.render_distance; settings.render_distance_preset = config.render_distance_preset; settings.fxaa_enabled = config.fxaa_enabled and !config.taa_enabled; @@ -201,7 +200,6 @@ fn matches(settings: *const Settings, preset: PresetConfig) bool { std.math.approxEqAbs(f32, settings.lpv_cell_size, preset.lpv_cell_size, epsilon) and settings.lpv_grid_size == preset.lpv_grid_size and settings.lpv_propagation_iterations == preset.lpv_propagation_iterations and - settings.lod_enabled == preset.lod_enabled and settings.fxaa_enabled == preset.fxaa_enabled and settings.bloom_enabled == preset.bloom_enabled and std.math.approxEqAbs(f32, settings.bloom_intensity, preset.bloom_intensity, epsilon); diff --git a/src/game/settings/tests.zig b/src/game/settings/tests.zig index d3177d4a..ab2eeda8 100644 --- a/src/game/settings/tests.zig +++ b/src/game/settings/tests.zig @@ -23,19 +23,19 @@ test "Preset Application" { try std.testing.expectEqual(@as(u32, 0), settings.shadow_quality); try std.testing.expectEqual(@as(i32, 8), settings.render_distance); try std.testing.expectEqual(RenderDistancePreset.low, settings.render_distance_preset); - try std.testing.expectEqual(true, settings.lod_enabled); + try std.testing.expectEqual(false, settings.lod_enabled); presets.apply(&settings, 3); try std.testing.expectEqual(@as(u32, 3), settings.shadow_quality); try std.testing.expectEqual(@as(i32, 16), settings.render_distance); try std.testing.expectEqual(RenderDistancePreset.ultra, settings.render_distance_preset); - try std.testing.expectEqual(true, settings.lod_enabled); + try std.testing.expectEqual(false, settings.lod_enabled); presets.apply(&settings, 4); try std.testing.expectEqual(@as(u32, 3), settings.shadow_quality); try std.testing.expectEqual(@as(i32, 16), settings.render_distance); try std.testing.expectEqual(RenderDistancePreset.extreme, settings.render_distance_preset); - try std.testing.expectEqual(true, settings.lod_enabled); + try std.testing.expectEqual(false, settings.lod_enabled); } test "Preset Matching" { diff --git a/src/world/lod_manager.zig b/src/world/lod_manager.zig index 2adea6fa..d9723c07 100644 --- a/src/world/lod_manager.zig +++ b/src/world/lod_manager.zig @@ -190,6 +190,9 @@ pub const LODManager = struct { // Type-erased renderer interface (replaces direct LODRenderer(RHI) field) renderer: LODRenderInterface, + // Keep cleanup behavior testable, but allow the live world to opt out. + cleanup_covered_regions: bool = true, + // Callback type to check if a regular chunk is loaded and renderable pub const ChunkChecker = lod_gpu.ChunkChecker; @@ -258,6 +261,7 @@ pub const LODManager = struct { .deletion_queue = .empty, .deletion_timer = 0, .renderer = render_iface, + .cleanup_covered_regions = false, }; // Initialize worker pool for LOD generation and meshing (3 workers for LOD tasks) @@ -353,9 +357,10 @@ pub const LODManager = struct { self.update_tick += 1; if (self.update_tick % 4 != 0) return; - // Issue #211: Clean up LOD chunks that are fully covered by LOD0 (throttled) - if (chunk_checker) |checker| { - self.unloadLODWhereChunksLoaded(checker, checker_ctx.?); + if (self.cleanup_covered_regions) { + if (chunk_checker) |checker| { + self.unloadLODWhereChunksLoaded(checker, checker_ctx.?); + } } // Safety: Check for NaN/Inf player position @@ -419,10 +424,7 @@ pub const LODManager = struct { const lod_bits: i32 = @as(i32, @intCast(@intFromEnum(lod))) << 28; // Calculate velocity direction for priority - const vel_len = @sqrt(velocity.x * velocity.x + velocity.z * velocity.z); - const has_velocity = vel_len > 0.1; - const vel_dx: f32 = if (has_velocity) velocity.x / vel_len else 0; - const vel_dz: f32 = if (has_velocity) velocity.z / vel_len else 0; + _ = velocity; var rz = player_rz - region_radius; while (rz <= player_rz + region_radius) : (rz += 1) { @@ -435,12 +437,12 @@ pub const LODManager = struct { const key = LODRegionKey{ .rx = rx, .rz = rz, .lod = lod }; - // Check if region is covered by higher detail chunks - if (chunk_checker) |checker| { - // We use a temporary chunk to calculate bounds - const temp_chunk = LODChunk.init(rx, rz, lod); - if (self.areAllChunksLoaded(temp_chunk.worldBounds(), checker, checker_ctx.?)) { - continue; + if (self.cleanup_covered_regions) { + if (chunk_checker) |checker| { + const temp_chunk = LODChunk.init(rx, rz, lod); + if (self.areAllChunksLoaded(temp_chunk.worldBounds(), checker, checker_ctx.?)) { + continue; + } } } @@ -467,24 +469,12 @@ pub const LODManager = struct { chunk.job_token = self.next_job_token; self.next_job_token += 1; - // Calculate velocity-weighted priority - // (dx, dz calculated above) + // Calculate radial priority only so loading stays symmetric. const dist_sq = @as(i64, dx) * @as(i64, dx) + @as(i64, dz) * @as(i64, dz); // Scale priority to match chunk-distance units used by meshing jobs (which are prioritized by chunk dist) // This ensures generation doesn't starve meshing const priority_full = dist_sq * @as(i64, scale) * @as(i64, scale); - var priority: i32 = @as(i32, @intCast(@min(priority_full, 0x0FFFFFFF))); - if (has_velocity) { - const fdx: f32 = @floatFromInt(dx); - const fdz: f32 = @floatFromInt(dz); - const dist = @sqrt(fdx * fdx + fdz * fdz); - if (dist > 0.01) { - const dot = (fdx * vel_dx + fdz * vel_dz) / dist; - // Ahead = lower priority number, behind = higher - const weight = 1.0 - dot * 0.5; - priority = @intFromFloat(@as(f32, @floatFromInt(priority)) * weight); - } - } + const priority: i32 = @as(i32, @intCast(@min(priority_full, 0x0FFFFFFF))); // Encode LOD level in high bits of dist_sq const encoded_priority = (priority & 0x0FFFFFFF) | lod_bits; @@ -1072,6 +1062,7 @@ test "LODManager initialization" { }; var mgr = try LODManager.init(allocator, config.interface(), mock_bridge, mock_render, mock_gen); + mgr.cleanup_covered_regions = false; // Verify initial state const stats = mgr.getStats(); @@ -1159,6 +1150,7 @@ test "LODManager end-to-end covered cleanup" { }; var mgr = try LODManager.init(allocator, config.interface(), mock_bridge, mock_render, mock_gen); + mgr.cleanup_covered_regions = false; defer mgr.deinit(); // 1. Initial position at origin @@ -1200,7 +1192,7 @@ test "LODManager end-to-end covered cleanup" { } }; - // Update - should unload because checker says all chunks are loaded + // Update - cleanup is disabled in live world, so regions should stay resident // Need to trigger the throttle (every 4 frames) try mgr.update(Vec3.zero, Vec3.zero, FullChecker.isLoaded, &dummy); try mgr.update(Vec3.zero, Vec3.zero, FullChecker.isLoaded, &dummy); @@ -1208,7 +1200,7 @@ test "LODManager end-to-end covered cleanup" { // 4th update triggers throttled logic try mgr.update(Vec3.zero, Vec3.zero, FullChecker.isLoaded, &dummy); - try std.testing.expect(!mgr.regions[1].contains(key)); + try std.testing.expect(mgr.regions[1].contains(key)); } test "LODStats aggregation" { diff --git a/src/world/lod_mesh.zig b/src/world/lod_mesh.zig index 5eea660f..8cc08ee0 100644 --- a/src/world/lod_mesh.zig +++ b/src/world/lod_mesh.zig @@ -199,7 +199,7 @@ pub const LODMesh = struct { return self.buildFromSimplifiedData(data, world_x, world_z); } - log.log.debug("LOD{} QEM: {} -> {} triangles (error={d:.2})", .{ + log.log.trace("LOD{} QEM: {} -> {} triangles (error={d:.2})", .{ @intFromEnum(self.lod_level), simplified.original_triangle_count, simplified.simplified_triangle_count, diff --git a/src/world/lod_renderer.zig b/src/world/lod_renderer.zig index 7fc3841e..e12228e2 100644 --- a/src/world/lod_renderer.zig +++ b/src/world/lod_renderer.zig @@ -36,6 +36,7 @@ const LODRegionKey = lod_chunk.LODRegionKey; const LODRegionKeyContext = lod_chunk.LODRegionKeyContext; const LODMesh = @import("lod_mesh.zig").LODMesh; const CHUNK_SIZE_X = @import("chunk.zig").CHUNK_SIZE_X; +const CHUNK_SIZE_Z = @import("chunk.zig").CHUNK_SIZE_Z; const lod_gpu = @import("lod_upload_queue.zig"); const LODGPUBridge = lod_gpu.LODGPUBridge; @@ -155,7 +156,7 @@ pub fn LODRenderer(comptime RHI: type) type { self: *Self, meshes: *const MeshMap, regions: *const RegionMap, - config: ILODConfig, + _: ILODConfig, _: Mat4, camera_pos: Vec3, frustum: Frustum, @@ -177,37 +178,23 @@ pub fn LODRenderer(comptime RHI: type) type { if (!isRegionInRange(bounds, camera_pos, max_dist)) continue; } - // Check if all underlying block chunks are loaded. - // We require a 1-chunk halo around the LOD region to avoid exposing - // block chunk cut-faces when neighbors are still missing. + // Skip the LOD region only when the entire covered chunk area is loaded. if (chunk_checker) |checker| { - const side: i32 = @intCast(chunk.lod_level.chunksPerSide()); - const start_cx = chunk.region_x * side; - const start_cz = chunk.region_z * side; - - var all_loaded = true; - var lcz: i32 = -CHUNK_COVERAGE_PADDING; - while (lcz < side + CHUNK_COVERAGE_PADDING) : (lcz += 1) { - var lcx: i32 = -CHUNK_COVERAGE_PADDING; - while (lcx < side + CHUNK_COVERAGE_PADDING) : (lcx += 1) { - if (!checker(start_cx + lcx, start_cz + lcz, checker_ctx.?)) { - all_loaded = false; - break; - } - } - if (!all_loaded) break; + if (checker_ctx) |ctx_ptr| { + if (self.isCoveredByChunks(bounds, checker, ctx_ptr)) continue; } - - if (all_loaded) continue; } const aabb_min = Vec3.init(@as(f32, @floatFromInt(bounds.min_x)) - camera_pos.x, 0.0 - camera_pos.y, @as(f32, @floatFromInt(bounds.min_z)) - camera_pos.z); const aabb_max = Vec3.init(@as(f32, @floatFromInt(bounds.max_x)) - camera_pos.x, 256.0 - camera_pos.y, @as(f32, @floatFromInt(bounds.max_z)) - camera_pos.z); - if (use_frustum and !frustum.intersectsAABB(AABB.init(aabb_min, aabb_max))) continue; + + if (use_frustum) { + if (!frustum.intersectsAABB(AABB.init(aabb_min, aabb_max))) continue; + } const model = Mat4.translate(Vec3.init(@as(f32, @floatFromInt(bounds.min_x)) - camera_pos.x, -camera_pos.y + lod_y_offset, @as(f32, @floatFromInt(bounds.min_z)) - camera_pos.z)); - const mask_radius = config.calculateMaskRadius() * @as(f32, @floatFromInt(CHUNK_SIZE_X)); + const mask_radius = 0.0; try self.instance_data.append(self.allocator, .{ .model = model, .mask_radius = mask_radius, @@ -218,6 +205,27 @@ pub fn LODRenderer(comptime RHI: type) type { } } + fn isCoveredByChunks( + _: *Self, + bounds: LODChunk.WorldBounds, + checker: ChunkChecker, + ctx: *anyopaque, + ) bool { + const min_cx = @divFloor(bounds.min_x, CHUNK_SIZE_X) - CHUNK_COVERAGE_PADDING; + const min_cz = @divFloor(bounds.min_z, CHUNK_SIZE_Z) - CHUNK_COVERAGE_PADDING; + const max_cx = @divFloor(bounds.max_x - 1, CHUNK_SIZE_X) + CHUNK_COVERAGE_PADDING; + const max_cz = @divFloor(bounds.max_z - 1, CHUNK_SIZE_Z) + CHUNK_COVERAGE_PADDING; + + var cz = min_cz; + while (cz <= max_cz) : (cz += 1) { + var cx = min_cx; + while (cx <= max_cx) : (cx += 1) { + if (!checker(cx, cz, ctx)) return false; + } + } + return true; + } + /// Create a LODGPUBridge that delegates to this renderer's RHI. pub fn createGPUBridge(self: *Self) LODGPUBridge { const Wrapper = struct { diff --git a/src/world/world.zig b/src/world/world.zig index c0f502e6..d498bec0 100644 --- a/src/world/world.zig +++ b/src/world/world.zig @@ -355,6 +355,10 @@ pub const World = struct { return data.chunk.getBlock(local.x, @intCast(world_y), local.z); } + pub fn getColumnInfo(self: *const World, world_x: i32, world_z: i32) gen_interface.ColumnInfo { + return self.generator.getColumnInfo(@floatFromInt(world_x), @floatFromInt(world_z)); + } + pub fn setBlock(self: *World, world_x: i32, world_y: i32, world_z: i32, block: BlockType) !void { if (world_y < 0 or world_y >= 256) return; const cp = worldToChunk(world_x, world_z); diff --git a/src/world/world_renderer.zig b/src/world/world_renderer.zig index 96778fd2..b1fc5bf6 100644 --- a/src/world/world_renderer.zig +++ b/src/world/world_renderer.zig @@ -1,7 +1,3 @@ -//! World renderer - handles chunk rendering, culling, and MDI. -//! Integrates GPU compute frustum culling (CullingSystem) with CPU fallback. -//! Supports GPU-driven meshing via batched compute dispatches. - const std = @import("std"); const log = @import("../engine/core/log.zig"); const ChunkData = @import("chunk_storage.zig").ChunkData; @@ -96,6 +92,10 @@ pub const WorldRenderer = struct { const vertex_allocator = try allocator.create(GlobalVertexAllocator); vertex_allocator.* = try GlobalVertexAllocator.init(allocator, rm, query, vertex_capacity_mb); + const vk_ctx: *VulkanContext = @ptrCast(@alignCast(rhi.ptr)); + + const safe_mode_enabled = vk_ctx.options.safe_mode; + const max_chunks = MAX_MDI_CHUNKS; var instance_buffers: [rhi_mod.MAX_FRAMES_IN_FLIGHT]rhi_mod.BufferHandle = undefined; var indirect_buffers: [rhi_mod.MAX_FRAMES_IN_FLIGHT]rhi_mod.BufferHandle = undefined; @@ -105,13 +105,16 @@ pub const WorldRenderer = struct { } var culling_system: ?*CullingSystem = null; - var use_gpu = false; - if (CullingSystem.init(allocator, rhi, max_chunks)) |cs| { - culling_system = cs; - use_gpu = true; - log.log.info("GPU frustum culling initialized (max_chunks={})", .{max_chunks}); - } else |err| { - log.log.warn("GPU culling init failed ({}), falling back to CPU culling", .{err}); + const use_gpu = false; + if (!safe_mode_enabled) { + if (CullingSystem.init(allocator, rhi, max_chunks)) |cs| { + culling_system = cs; + log.log.warn("GPU chunk culling initialized but kept disabled due unstable visibility", .{}); + } else |err| { + log.log.warn("GPU culling init failed ({}), falling back to CPU culling", .{err}); + } + } else { + log.log.info("Safe mode: GPU frustum culling disabled, using CPU culling", .{}); } var gpu_block_buffer: ?*GpuBlockBuffer = null; @@ -121,11 +124,15 @@ pub const WorldRenderer = struct { var gpu_mesher: ?*GpuMesher = null; errdefer if (gpu_mesher) |m| m.deinit(); - if (gpu_block_buffer) |buf| { - gpu_mesher = GpuMesher.init(allocator, rhi, atlas, buf) catch |err| blk: { - log.log.warn("GpuMesher init failed ({}), CPU meshing fallback active", .{err}); - break :blk null; - }; + if (!safe_mode_enabled) { + if (gpu_block_buffer) |buf| { + gpu_mesher = GpuMesher.init(allocator, rhi, atlas, buf) catch |err| blk: { + log.log.warn("GpuMesher init failed ({}), CPU meshing fallback active", .{err}); + break :blk null; + }; + } + } else { + log.log.info("Safe mode: GPU meshing disabled, using CPU meshing fallback", .{}); } renderer.* = .{ @@ -134,7 +141,7 @@ pub const WorldRenderer = struct { .rm = rm, .render_ctx = render_ctx, .query = query, - .vk_ctx = @ptrCast(@alignCast(rhi.ptr)), + .vk_ctx = vk_ctx, .vertex_allocator = vertex_allocator, .visible_chunks = .empty, .last_render_stats = .{}, @@ -219,6 +226,9 @@ pub const WorldRenderer = struct { } } + // LOD rendering uses a separate descriptor set path; switch back before drawing full chunks. + self.render_ctx.setInstanceBuffer(0); + self.visible_chunks.clearRetainingCapacity(); self.instance_data.clearRetainingCapacity(); self.draw_commands.clearRetainingCapacity(); @@ -242,6 +252,17 @@ pub const WorldRenderer = struct { self.last_render_stats.chunks_total = @intCast(self.storage.chunks.count()); const vertex_size = @sizeOf(rhi_mod.Vertex); + const supports_indirect_first_instance = self.query.supportsIndirectFirstInstance(); + + // Environment override to force MDI fallback for debugging + const force_mdi_fallback = blk: { + const env_val = std.posix.getenv("ZIGCRAFT_FORCE_MDI_FALLBACK"); + break :blk if (env_val) |val| + !(std.mem.eql(u8, val, "0") or std.mem.eql(u8, val, "false")) + else + false; + }; + var total_vertices: u64 = 0; for (self.visible_chunks.items) |data| { self.last_render_stats.chunks_rendered += 1; @@ -252,6 +273,29 @@ pub const WorldRenderer = struct { const rel_y = -camera_pos.y; const model = Mat4.translate(Vec3.init(rel_x, rel_y, rel_z)); + if (!supports_indirect_first_instance or force_mdi_fallback) { + self.render_ctx.setModelMatrix(model, Vec3.one, 0); + + if (layer != .fluid) { + if (data.mesh.solid_allocation) |alloc| { + total_vertices += alloc.count; + self.last_render_stats.vertices_rendered += alloc.count; + self.render_ctx.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.render_ctx.drawOffset(self.vertex_allocator.buffer, alloc.count, .triangles, alloc.offset); + } + } + if (layer != .terrain) { + if (data.mesh.fluid_allocation) |alloc| { + self.last_render_stats.vertices_rendered += alloc.count; + self.render_ctx.drawOffset(self.vertex_allocator.buffer, alloc.count, .triangles, alloc.offset); + } + } + continue; + } + const instance_idx: u32 = @intCast(self.instance_data.items.len); self.instance_data.append(self.allocator, .{ @@ -335,8 +379,8 @@ pub const WorldRenderer = struct { } } - fn renderCpuCull(self: *WorldRenderer, view_proj: Mat4, camera_pos: Vec3, pc_x: i64, pc_z: i64, r_dist: i64) void { - const frustum = Frustum.fromViewProj(view_proj); + fn renderCpuCull(self: *WorldRenderer, view_proj: Mat4, _: Vec3, pc_x: i64, pc_z: i64, r_dist: i64) void { + _ = view_proj; var cz = pc_z - r_dist; while (cz <= pc_z + r_dist) : (cz += 1) { @@ -344,13 +388,9 @@ pub const WorldRenderer = struct { 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.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 |err| { - log.log.debug("MDI: visible_chunks append failed: {}", .{err}); - }; - } else { - self.last_render_stats.chunks_culled += 1; - } + self.visible_chunks.append(self.allocator, data) catch |err| { + log.log.debug("MDI: visible_chunks append failed: {}", .{err}); + }; } } } @@ -421,8 +461,10 @@ pub const WorldRenderer = struct { cs.updateAABBData(fi, self.aabb_data.items); const screen_w = @as(f32, @floatFromInt(self.vk_ctx.gpass.g_pass_extent.width)); const screen_h = @as(f32, @floatFromInt(self.vk_ctx.gpass.g_pass_extent.height)); - const prev_frame_valid = self.vk_ctx.depth_pyramid.isValid(); - cs.dispatch(view_proj, chunk_count, screen_w, screen_h, prev_frame_valid); + // The previous-frame depth pyramid is currently too unstable during camera + // rotation and causes chunks to be wrongly occluded. Keep GPU frustum + // culling, but disable temporal occlusion until the reprojection path is fixed. + cs.dispatch(view_proj, chunk_count, screen_w, screen_h, false); } pub fn renderShadowPass(self: *WorldRenderer, light_space_matrix: Mat4, camera_pos: Vec3, shadow_caster_distance: f32) void { diff --git a/src/world/world_streamer.zig b/src/world/world_streamer.zig index bfc9166e..776444bc 100644 --- a/src/world/world_streamer.zig +++ b/src/world/world_streamer.zig @@ -288,12 +288,9 @@ pub const WorldStreamer = struct { switch (data.chunk.state) { .missing => { - const weight = self.player_movement.priorityWeight(dx, dz); - const weighted_dist: i32 = @intFromFloat(@as(f32, @floatFromInt(dist_sq)) * weight); - try self.gen_queue.push(.{ .type = .chunk_generation, - .dist_sq = weighted_dist, + .dist_sq = dist_sq, .data = .{ .chunk = .{ .x = cx, @@ -322,12 +319,9 @@ pub const WorldStreamer = struct { const dx = data.chunk.chunk_x - pc.chunk_x; const dz = data.chunk.chunk_z - pc.chunk_z; if (dx * dx + dz * dz <= render_dist * render_dist) { - const weight = self.player_movement.priorityWeight(dx, dz); - const weighted_dist: i32 = @intFromFloat(@as(f32, @floatFromInt(dx * dx + dz * dz)) * weight); - try self.mesh_queue.push(.{ .type = .chunk_meshing, - .dist_sq = weighted_dist, + .dist_sq = dx * dx + dz * dz, .data = .{ .chunk = .{ .x = data.chunk.chunk_x,