Refactor: Decompose App and Fix Vulkan Parity/SOLID issues - #18
Conversation
This PR addresses remaining SOLID and parity issues: - **App Decomposition**: - Extracted `RenderSystem` to `src/game/render_system.zig` (handles RHI, resources). - Extracted `WindowManager` to `src/engine/core/window.zig` (handles SDL/GL context). - `App` now delegates to these subsystems. - **Vulkan/OpenGL Parity**: - Extracted Cascaded Shadow Map (CSM) math to `src/engine/graphics/csm.zig`. - Clarified that Vulkan handles shadow resources internally in RHI, while OpenGL uses `ShadowMap` struct. Both use common CSM math. - **SOLID/Clean Code**: - Introduced `MenuContext` in `menus.zig` to reduce parameter bloat (Interface Segregation). - Fixed `pending_new_world_seed` logic to prevent infinite retry loops on world creation failure. - **Fixes**: - Fixed build errors related to missing file tracking.
|
/oc can you do a indepth code review of this PR |
Code Review: Refactor - Decompose App and Fix Vulkan Parity/SOLID IssuesExecutive SummaryThe refactoring successfully decomposes the monolithic 1. Vulkan/OpenGL Parity Issues🔴 CRITICAL: Z-Range Inconsistency in CSM (src/game/app.zig)OpenGL Path (lines 342, 366): // Line 342: Shadow pass creation
sm.update(..., false); // z_range_01=false
// Line 366: Uniform update
const cascades = ShadowMap.computeCascades(..., true); // z_range_01=trueVulkan Path (line 414): // Both shadow pass and uniforms use z_range_01=true
const cascades = ShadowMap.computeCascades(..., true);Analysis: The OpenGL path uses different z_range modes for shadow pass creation vs uniform updates. Looking at
The shadow pass FBO uses GL's standard depth range, but uniforms compute Vulkan-style matrices. This will cause shadow sampling artifacts. Fix: Ensure both calls use // Line 366 should be:
const cascades = ShadowMap.computeCascades(..., false);🟡 MEDIUM: OpenGL beginShadowPass/endShadowPass Are No-Ops (src/engine/graphics/rhi_opengl.zig:432-439)fn beginShadowPass(ctx_ptr: *anyopaque, cascade_index: u32) void {
_ = ctx_ptr;
_ = cascade_index;
}Analysis: These methods exist in the RHI interface but do nothing for OpenGL. Shadow pass management is handled at a higher level ( Recommendation: This is acceptable given the different rendering models, but document it clearly in 🟢 GOOD: CSM Math Extraction (src/engine/graphics/csm.zig)The shared CSM math extraction is excellent. Both backends now use identical cascade computation logic, which ensures consistent shadow behavior. 2. SOLID Principles Analysis✅ Single Responsibility Principle (SRP) - IMPROVEDBefore: After:
Still problematic: ✅ Open/Closed Principle (OCP) - GOODThe RHI abstraction allows new backends without modifying ✅ Liskov Substitution Principle (LSP) - GOODBoth RHI backends implement the same vtable interface correctly. ✅ Interface Segregation Principle (ISP) - EXCELLENTBefore: Menu functions had 8-12 parameters each. After: pub const MenuContext = struct {
ui: *UISystem,
input: *const Input,
screen_w: f32,
screen_h: f32,
time: *const Time,
allocator: std.mem.Allocator,
};This is a perfect example of ISP - cohesive context grouping with reduced coupling. 🟡 Dependency Inversion Principle (DIP) - PARTIAL
3. Memory Management Issues🔴 CRITICAL: Potential Use-After-Free on World Init Failure (src/game/app.zig:158-164)if (self.pending_new_world_seed) |seed| {
self.pending_new_world_seed = null; // Cleared before validation
self.world = World.init(...) catch |err| {
log.log.err("Failed to create world: {}", .{err});
self.app_state = .home;
continue;
};
// ... world_map init continues
}Analysis: If
This is not a UAF, but it's inconsistent state. The app is in Fix: Use self.world = World.init(...) catch |err| {
log.log.err("Failed to create world: {}", .{err});
self.app_state = .home;
continue; // Skip to next frame
};🟡 MEDIUM: ShadowMap Init Failure Not Handled (src/game/render_system.zig:82)const shadow_map = if (!actual_is_vulkan) ShadowMap.init(rhi, settings.shadow_resolution) catch null else null;Analysis: If Risk: Fix: Add logging: const shadow_map = if (!actual_is_vulkan) blk: {
break :blk ShadowMap.init(rhi, settings.shadow_resolution) catch |err| {
log.log.warn("ShadowMap initialization failed: {}. Shadows disabled.", .{err});
break :blk null;
};
} else null;🟢 GOOD: Proper Cleanup Order (src/game/app.zig:130-141)pub fn deinit(self: *App) void {
if (self.world_map) |*m| m.deinit();
if (self.world) |w| w.deinit();
self.seed_input.deinit(self.allocator);
if (self.ui) |*u| u.deinit();
self.render_system.deinit();
self.input.deinit();
self.window_manager.deinit();
self.allocator.destroy(self);
}Analysis: Cleanup order is correct - dependent objects freed before their dependencies. 🟢 GOOD: Vulkan Memory Map Error Checked (src/engine/graphics/rhi_vulkan.zig:1663-1667)if (c.vkMapMemory(ctx.device, shadow_ubo.memory, 0, @sizeOf(ShadowUniforms), 0, &map_ptr) == c.VK_SUCCESS) {
const mapped: *ShadowUniforms = @ptrCast(@alignCast(map_ptr));
mapped.* = shadow_uniforms;
c.vkUnmapMemory(ctx.device, shadow_ubo.memory);
}Analysis: Properly checks the Vulkan return code before using the mapped pointer. 4. Additional Findings🟡 MEDIUM: GLEW Initialization Race (src/game/render_system.zig:41, 46)if (res) |v| {
break :blk RhiResult{ .rhi = v, .is_vulkan = true };
} else |err| {
log.log.err("Failed to initialize Vulkan: {}. Falling back to OpenGL.", .{err});
if (c.glewInit() != c.GLEW_OK) return error.GLEWInitFailed; // ← HERE
break :blk RhiResult{ .rhi = try rhi_opengl.createRHI(allocator), .is_vulkan = false };
}GLEW is initialized inside the Vulkan fallback path, but it's also initialized in the OpenGL path (line 46). This means GLEW could be initialized twice (no-op, but inefficient) or not at all if Vulkan succeeds (correct). Fix: Initialize GLEW once at the top if if (!is_vulkan) {
if (c.glewInit() != c.GLEW_OK) return error.GLEWInitFailed;
}🟡 MEDIUM: Unnecessary Shadow Map Shadowing (src/game/app.zig:337-349)if (self.render_system.shadow_map) |*sm| {
// ... 12 lines of shadow pass logic
}But then: if (!self.render_system.is_vulkan) {
self.render_system.rhi.beginMainPass();
// ...
if (self.render_system.shader) |*s| {
// ...
if (self.render_system.shadow_map) |*sm| {
// ... update uniforms
}
}
} else if (self.render_system.is_vulkan) {
// ... Vulkan path
if (light_active) {
// ... shadow pass using rhi.beginShadowPass()
}
}The OpenGL path uses 🟡 LOW: Dead Code in
|
…World init error handling - Fixed CSM Z-range inconsistency in app.zig (use z_range_01=false for OpenGL uniforms) - Added logging and null-safety for ShadowMap init in render_system.zig - Fixed control flow in app.zig to 'continue' on world init failure - Moved GLEW initialization to prevent race conditions
|
/oc can you do a indepth code review of this PR |
Code Review: Refactor - Decompose App and Fix Vulkan Parity/SOLID IssuesSummaryThis PR successfully achieves its primary goals: decomposing the monolithic 1. Vulkan/OpenGL Parity Issues🔴 CRITICAL: Duplicate Cascade Computation in
|
… viz safety - app.zig: Reuse computed cascades from ShadowMap struct instead of recomputing them in OpenGL path - app.zig: Explicitly check !is_vulkan for debug visualization to prevent potential errors - Verified cleanup logic in RenderSystem.deinit is correct
|
/oc can confirm the issues in these reviews are solved |
|
Looking at the current code, I can confirm that all critical issues from both previous reviews have been resolved: ✅ CRITICAL Issues - FIXED
✅ SOLID Principles - IMPROVED
All critical and medium-priority issues are resolved. The refactoring successfully achieves its stated goals with proper parity and memory safety. |



Summary
This PR completes the SOLID refactoring and addresses Vulkan parity concerns identified in code review.
Changes
App Decomposition: Split the monolithic
Appstruct into focused subsystems:WindowManager(src/engine/core/window.zig): Handles SDL window and context creation.RenderSystem(src/game/render_system.zig): Manages RHI, shaders, and rendering resources.App(src/game/app.zig): Orchestrates the game loop and state.Vulkan/OpenGL Parity:
src/engine/graphics/csm.zig. This clarifies that while OpenGL uses theShadowMapstruct for resource management, Vulkan uses the same math logic but manages resources internally inVulkanContext.pending_new_world_seedon initialization failure.Interface Segregation:
menus.zigto use aMenuContextstruct, significantly reducing parameter list size and coupling.Verification
zig build.zig build run -- --backend vulkan.