-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Shadow System Refactoring: SOLID Violations & Monolithic VulkanContext
Summary
The shadow implementation across src/engine/graphics/csm.zig, src/engine/graphics/render_graph.zig, src/engine/graphics/rhi_vulkan.zig, and src/world/world_renderer.zig has architectural issues affecting maintainability, testability, and adherence to SOLID principles. While the high-level design is sound, the Vulkan backend implementation suffers from severe SRP violations and monolithic structure.
Issues Found
🏗️ SOLID Violations
Critical: Single Responsibility Principle (SRP) Violations
-
Monolithic
VulkanContextContains Shadow Logic (src/engine/graphics/rhi_vulkan.zig)- 5000+ lines mixing shadow, UI, GPass, SSAO, and main pass responsibilities
- Shadow resources scattered across lines 337-344, 211-213, 310-328
- Shadow pipeline initialization mixed with other pipelines (~lines 2100-2300)
- Shadow pass rendering at lines 5151-5215 mixed with other pass functions
- Debug shadow visualization at lines 372-380, 3795-3875 embedded in production code
-
IRenderContextInterface Segregation Violation (src/engine/graphics/rhi.zig:272-352)- Mixes unrelated pass methods in single interface:
beginShadowPass/endShadowPass beginMainPass/endMainPass begin2DPass/end2DPass drawSky/drawDebugShadowMap
- Clients depending on shadow passes must drag in UI/2D rendering dependencies
- Violates Interface Segregation Principle (ISP)
- Mixes unrelated pass methods in single interface:
-
Dependency Inversion Violation (
src/engine/graphics/render_graph.zig:109)ShadowPass.execute()depends directly on concreteWorldandCameratypes- Cannot mock for testing shadow pass logic in isolation
- Tightly couples render graph to world implementation details
Medium Issues
-
Open/Closed Principle (OCP) Concerns
- Adding new shadow technique requires modifying
VulkanContextdirectly - No abstraction for shadow mapping algorithms (CSM vs VSM vs PCF)
- Adding new shadow technique requires modifying
-
State Duplication in
beginShadowPass()(src/engine/graphics/rhi_vulkan.zig:5159-5187)ctx.shadow_pass_active = true; // Line 5159 // ... some code ... ctx.shadow_pass_active = true; // Line 5185 (duplicate) ctx.shadow_pass_index = cascade_index; // Line 5186 (duplicate of line 5160) ctx.shadow_pass_matrix = light_space_matrix; // Line 5187 (duplicate of line 5161)
🧹 Code Cleanliness Problems
Major Issues
-
Shadow Resource Management Scattered
- Resource creation: throughout
rhi_vulkan.zig:2100-2300(no single location) - Destruction:
destroyShadowResources()at line 382 (isolated) - State tracking: lines 315-328 (
shadow_pass_active,shadow_pass_index, etc.) - Debug resources: lines 372-380 mixed with production resources
- Resource creation: throughout
-
Debug Shadow Code Embedded in Production
debug_shadow_pipeline,debug_shadow_pipeline_layoutat lines 372-374drawDebugShadowMap()function at lines 3795-3875- Debug descriptor pools at lines 375-377
- Should be behind conditional compilation or separate module
-
Manual Mutex Locking Without RAII (
src/engine/graphics/rhi_vulkan.zig:3821)ctx.mutex.lock(); const tex_entry = ctx.textures.get(depth_map_handle); ctx.mutex.unlock();
- No exception safety (though Zig doesn't have exceptions, this pattern is error-prone)
- Should use Zig's
deferpattern for guaranteed unlock
Medium Issues
-
Missing Documentation
- CSM cascade split algorithm (lambda=0.92) not explained
- No comments on shadow bias values (line 5183:
1.25, 0.0, 1.75) - No explanation of reverse-Z shadow mapping (line 5175 comment is helpful but sparse)
-
Hardcoded Shadow Constants
- Shadow cascade count:
SHADOW_CASCADE_COUNT = 3(line 32 ofrhi.zig) - Shadow distance hardcoded in
ShadowPass.execute()viactx.shadow_distance - Shadow resolution passed as parameter but not validated
- Shadow cascade count:
-
No Unit Tests
csm.zig:computeCascades()has no tests for matrix calculations- Cascade split distance calculations not validated
- Texel snapping logic not tested
✅ Positive Findings
-
Well-Structured High-Level Design
csm.zigis clean and focused (110 lines)ShadowPassinrender_graph.zigis well-scopedworld_renderer.zig:renderShadowPass()has clear responsibility
-
Good Use of Interface Pattern
IRenderPassenables pass extension without modificationRenderGraphfollows open/closed principle
-
Stable CSM Implementation
- Bounding sphere approach prevents shadow edge artifacts
- Texel snapping reduces shimmering
- Reverse-Z depth improves precision
Proposed Solution
Phase 1: Extract ShadowSystem (Critical)
// New file: src/engine/graphics/shadow_system.zig
pub const ShadowSystem = struct {
allocator: std.mem.Allocator,
rhi: *RHI,
// Resources
shadow_image: c.VkImage,
shadow_image_memory: c.VkDeviceMemory,
shadow_image_views: [rhi.SHADOW_CASCADE_COUNT]c.VkImageView,
shadow_framebuffers: [rhi.SHADOW_CASCADE_COUNT]c.VkFramebuffer,
shadow_sampler: c.VkSampler,
shadow_render_pass: c.VkRenderPass,
shadow_pipeline: c.VkPipeline,
// State
pass_active: bool = false,
pass_index: u32 = 0,
pass_matrix: Mat4,
pub fn init(allocator: std.mem.Allocator, rhi: *RHI, resolution: u32) !ShadowSystem { /* ... */ }
pub fn deinit(self: *ShadowSystem) void { /* ... */ }
pub fn beginPass(self: *ShadowSystem, cascade_index: u32, light_space_matrix: Mat4) void { /* ... */ }
pub fn endPass(self: *ShadowSystem) void { /* ... */ }
pub fn updateUniforms(self: *ShadowSystem, params: rhi.ShadowParams) void { /* ... */ }
};Phase 2: Interface Segregation
// Split IRenderContext into focused interfaces
pub const IShadowContext = struct {
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
beginPass: *const fn (ptr: *anyopaque, cascade_index: u32, light_space_matrix: Mat4) void,
endPass: *const fn (ptr: *anyopaque) void,
updateUniforms: *const fn (ptr: *anyopaque, params: ShadowParams) void,
};
// ... methods
};
pub const IMainPassContext = struct {
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
beginPass: *const fn (ptr: *anyopaque) void,
endPass: *const fn (ptr: *anyopaque) void,
// ... main pass only methods
};
// ... methods
};
// Compose RHI from interfaces
pub const RHI = struct {
shadow: IShadowContext,
main_pass: IMainPassContext,
// ... other contexts
};Phase 3: Dependency Inversion
// Refactor ShadowPass to depend on abstractions
pub const IShadowScene = struct {
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
renderShadowPass: *const fn (ptr: *anyopaque, light_space_matrix: Mat4, camera_pos: Vec3) void,
};
pub fn renderShadowPass(self: IShadowScene, light_space_matrix: Mat4, camera_pos: Vec3) void {
self.vtable.renderShadowPass(self.ptr, light_space_matrix, camera_pos);
}
};
// World implements IShadowScene
pub fn shadowScene(self: *World) IShadowScene {
return .{
.ptr = self,
.vtable = &.{
.renderPass = renderShadowPassWrapper,
},
};
}Phase 4: Debug Code Separation
// Use conditional compilation or separate module
const debug_shadows = @import("build_options").debug_shadows;
if (debug_shadows) {
// Debug shadow pipeline, resources, rendering
}Phase 5: Code Quality Improvements
- Extract
ShadowResourcesstruct for lifecycle management - Add RAII-style mutex handling:
ctx.mutex.lock(); defer ctx.mutex.unlock(); const tex_entry = ctx.textures.get(depth_map_handle);
- Add unit tests for CSM calculations
- Document magic constants (lambda=0.92, bias values, etc.)
- Create
ShadowConfigstruct for extensible configuration
Phase 6: Documentation & Testing
// src/engine/graphics/csm.zig
/// Computes cascaded shadow map matrices using stable CSM approach.
///
/// Parameters:
/// - resolution: Shadow map resolution in pixels
/// - camera_fov: Camera vertical field of view in radians
/// - aspect: Camera aspect ratio (width / height)
/// - near: Camera near plane distance
/// - far: Camera far plane distance
/// - sun_dir: Direction to sun (normalized)
/// - cam_view: Camera view matrix
/// - z_range_01: If true, maps depth to [0,1] (for reverse-Z)
///
/// Algorithm:
/// 1. Computes cascade splits using logarithmic/linear blend (lambda=0.92)
/// 2. For each cascade, computes bounding sphere of frustum slice
/// 3. Builds orthographic projection snapped to texel grid
/// 4. Returns light-space matrices and cascade metadata
pub fn computeCascades(resolution: u32, camera_fov: f32, aspect: f32, near: f32, far: f32, sun_dir: Vec3, cam_view: Mat4, z_range_01: bool) ShadowCascades { /* ... */ }
test "computeCascades generates valid matrices" {
// Test cascade splits are monotonic
// Test matrices are invertible
// Test texel snapping preserves alignment
}Expected Impact
- Maintainability: ~60% reduction in
VulkanContextsize (remove ~500 shadow-related lines) - Testability: Enable isolated unit testing of shadow system and CSM calculations
- Extensibility: Add new shadow techniques without modifying monolithic context
- Code Quality: Clearer responsibilities, better documentation, reduced coupling
Related Files
src/engine/graphics/rhi_vulkan.zig(lines 337-380: shadow resources, 5151-5215: shadow pass, 3795-3875: debug)src/engine/graphics/rhi.zig(lines 272-352: IRenderContext interface)src/engine/graphics/csm.zig(entire file - well-structured)src/engine/graphics/render_graph.zig(lines 91-141: ShadowPass)src/world/world_renderer.zig(lines 165-205: renderShadowPass)assets/shaders/vulkan/shadow.vertassets/shaders/vulkan/shadow.frag
Labels
enhancement, shaders, engine
Checklist
- Extract
ShadowSystemstruct fromVulkanContext - Split
IRenderContextinto focused interfaces - Implement
IShadowSceneabstraction for DIP - Remove debug shadow code from production (conditional compilation)
- Add unit tests for
computeCascades() - Document CSM algorithm and constants
- Create
ShadowConfigfor extensible configuration - Replace manual mutex locking with
deferpattern - Fix state duplication in
beginShadowPass()
Questions
- Should we support multiple shadow mapping techniques (VSM, PCF, PCSS)?
- Do we want runtime-configurable cascade count/split scheme?
- Should debug shadow visualization be toggleable at runtime or build-time?
- Is the current 3-cascade CSM configuration sufficient, or should we make it configurable?
- Should we add a shadow map cache to reduce per-frame reallocations?