Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions HANDOFF.md
Original file line number Diff line number Diff line change
@@ -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
19 changes: 18 additions & 1 deletion assets/shaders/vulkan/post_process.frag
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
43 changes: 23 additions & 20 deletions assets/shaders/vulkan/terrain.frag
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
Expand All @@ -402,15 +404,15 @@ 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;
}

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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -522,27 +521,31 @@ 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);
color = computePBR(albedo, N, V, L, clamp(roughness, 0.05, 1.0), totalShadow, vSkyLight * global.lighting.x, vBlockLight, ao, ssao);
} 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) {
Expand Down
Binary file modified assets/shaders/vulkan/terrain.frag.spv
Binary file not shown.
24 changes: 24 additions & 0 deletions assets/shaders/vulkan/terrain_debug.frag
Original file line number Diff line number Diff line change
@@ -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);
}
81 changes: 73 additions & 8 deletions src/engine/graphics/render_system.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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...", .{});
Expand All @@ -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"))
Expand All @@ -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", .{});
}
Expand All @@ -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);
Expand Down Expand Up @@ -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", .{});
Expand All @@ -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);
Expand All @@ -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());
Expand Down Expand Up @@ -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;
}
Expand Down
Loading
Loading