From 4f79a93dfded993464e4e7b06b9c95d0478d2011 Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Sat, 7 Feb 2026 17:00:52 +0000 Subject: [PATCH 01/11] feat(lighting): Complete Phases A & C of Modern Lighting Overhaul (#143) This commit implements colored block lights and post-processing effects, completing phases A and C of issue #143. Phase B (LPV) is tracked in #260. ## Phase A: Colored Block Light System - Add torch block (ID: 45) with warm orange emission (RGB: 15, 11, 6) - Add lava block (ID: 46) with red-orange emission (RGB: 15, 8, 3) - Extend existing RGB-packed light propagation system - Update block registry with textures, transparency, and render pass settings - Torch and lava now emit colored light visible in-game ## Phase C: Post-Processing Stack - Implement vignette effect with configurable intensity - Implement film grain effect with time-based animation - Add settings: vignette_enabled, vignette_intensity, film_grain_enabled, film_grain_intensity - Full UI integration in Graphics Settings screen - Real-time parameter updates via RHI interface ## Technical Changes - Modified: src/world/block.zig (new block types) - Modified: src/world/block_registry.zig (light emission, properties) - Modified: src/engine/graphics/resource_pack.zig (texture mappings) - Modified: assets/shaders/vulkan/post_process.frag (vignette, grain) - Modified: src/engine/graphics/vulkan/post_process_system.zig (push constants) - Modified: src/game/settings/data.zig (new settings) - Modified: src/game/screens/graphics.zig (UI handlers) - Modified: src/engine/graphics/rhi.zig (new RHI methods) - Modified: src/engine/graphics/rhi_vulkan.zig (implementations) - Modified: src/engine/graphics/vulkan/rhi_context_types.zig (state storage) - Modified: src/engine/graphics/vulkan/rhi_pass_orchestration.zig (push constants) Closes #143 (partially - Phase B tracked in #260) --- assets/shaders/vulkan/post_process.frag | 48 ++++++++++++++++++- src/engine/graphics/resource_pack.zig | 2 + src/engine/graphics/rhi.zig | 16 +++++++ src/engine/graphics/rhi_tests.zig | 4 ++ src/engine/graphics/rhi_vulkan.zig | 24 ++++++++++ .../graphics/vulkan/post_process_system.zig | 2 + .../graphics/vulkan/rhi_context_types.zig | 8 ++++ .../vulkan/rhi_pass_orchestration.zig | 2 + src/game/screens/graphics.zig | 8 ++++ src/game/settings/data.zig | 24 ++++++++++ src/world/block.zig | 2 + src/world/block_registry.zig | 27 +++++++++-- 12 files changed, 160 insertions(+), 7 deletions(-) diff --git a/assets/shaders/vulkan/post_process.frag b/assets/shaders/vulkan/post_process.frag index 67b9824a..da5e1694 100644 --- a/assets/shaders/vulkan/post_process.frag +++ b/assets/shaders/vulkan/post_process.frag @@ -7,8 +7,10 @@ layout(set = 0, binding = 0) uniform sampler2D uHDRBuffer; layout(set = 0, binding = 2) uniform sampler2D uBloomTexture; layout(push_constant) uniform PostProcessParams { - float bloomEnabled; // 0.0 = disabled, 1.0 = enabled - float bloomIntensity; // Final bloom blend intensity + float bloomEnabled; // 0.0 = disabled, 1.0 = enabled + float bloomIntensity; // Final bloom blend intensity + float vignetteIntensity; // 0.0 = none, 1.0 = full vignette + float filmGrainIntensity;// 0.0 = none, 1.0 = heavy grain } postParams; layout(set = 0, binding = 1) uniform GlobalUniforms { @@ -107,6 +109,42 @@ vec3 ACESFilm(vec3 x) { return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0); } +// Vignette effect - darkens edges of the screen +vec3 applyVignette(vec3 color, vec2 uv, float intensity) { + if (intensity <= 0.0) return color; + + // Convert UV from [0,1] to [-1,1] range, centered at (0.5, 0.5) + vec2 centered = uv * 2.0 - 1.0; + + // Calculate distance from center (circular vignette) + float dist = length(centered); + + // Smooth vignette falloff + float vignette = smoothstep(1.0, 0.4, dist * (1.0 + intensity)); + + // Apply vignette - darker at edges + return color * mix(0.3, 1.0, vignette * (1.0 - intensity * 0.5) + intensity * 0.5); +} + +// Pseudo-random function for film grain +float random(vec2 uv) { + return fract(sin(dot(uv, vec2(12.9898, 78.233))) * 43758.5453); +} + +// Film grain effect - adds animated noise +vec3 applyFilmGrain(vec3 color, vec2 uv, float intensity, float time) { + if (intensity <= 0.0) return color; + + // Generate grain using UV and time for animation + float grain = random(uv + time * 0.01); + + // Convert to signed noise centered around 0 + grain = (grain - 0.5) * 2.0; + + // Apply grain with intensity - subtle effect + return color + grain * intensity * 0.05; +} + void main() { vec3 hdrColor = texture(uHDRBuffer, inUV).rgb; @@ -125,5 +163,11 @@ void main() { color = ACESFilm(hdrColor * global.pbr_params.y); } + // Apply vignette effect + color = applyVignette(color, inUV, postParams.vignetteIntensity); + + // Apply film grain effect + color = applyFilmGrain(color, inUV, postParams.filmGrainIntensity, global.params.x); + outColor = vec4(color, 1.0); } diff --git a/src/engine/graphics/resource_pack.zig b/src/engine/graphics/resource_pack.zig index f96d96b4..790c930e 100644 --- a/src/engine/graphics/resource_pack.zig +++ b/src/engine/graphics/resource_pack.zig @@ -89,6 +89,8 @@ pub const BLOCK_TEXTURES = [_]TextureMapping{ .{ .name = "flower_red", .files = &.{ "flower_red.png", "flower_rose.png", "poppy.png" } }, .{ .name = "flower_yellow", .files = &.{ "flower_yellow.png", "flower_dandelion.png", "dandelion.png" } }, .{ .name = "dead_bush", .files = &.{ "dead_bush.png", "deadbush.png" } }, + .{ .name = "torch", .files = &.{ "torch.png", "torch_on.png" } }, + .{ .name = "lava", .files = &.{ "lava.png", "lava_still.png" } }, }; pub const LoadedTexture = struct { diff --git a/src/engine/graphics/rhi.zig b/src/engine/graphics/rhi.zig index bd7c94cf..c898545a 100644 --- a/src/engine/graphics/rhi.zig +++ b/src/engine/graphics/rhi.zig @@ -472,6 +472,10 @@ pub const RHI = struct { setFXAA: *const fn (ctx: *anyopaque, enabled: bool) void, setBloom: *const fn (ctx: *anyopaque, enabled: bool) void, setBloomIntensity: *const fn (ctx: *anyopaque, intensity: f32) void, + setVignetteEnabled: *const fn (ctx: *anyopaque, enabled: bool) void, + setVignetteIntensity: *const fn (ctx: *anyopaque, intensity: f32) void, + setFilmGrainEnabled: *const fn (ctx: *anyopaque, enabled: bool) void, + setFilmGrainIntensity: *const fn (ctx: *anyopaque, intensity: f32) void, }; pub fn factory(self: RHI) IResourceFactory { @@ -706,4 +710,16 @@ pub const RHI = struct { pub fn setBloomIntensity(self: RHI, intensity: f32) void { self.vtable.setBloomIntensity(self.ptr, intensity); } + pub fn setVignetteEnabled(self: RHI, enabled: bool) void { + self.vtable.setVignetteEnabled(self.ptr, enabled); + } + pub fn setVignetteIntensity(self: RHI, intensity: f32) void { + self.vtable.setVignetteIntensity(self.ptr, intensity); + } + pub fn setFilmGrainEnabled(self: RHI, enabled: bool) void { + self.vtable.setFilmGrainEnabled(self.ptr, enabled); + } + pub fn setFilmGrainIntensity(self: RHI, intensity: f32) void { + self.vtable.setFilmGrainIntensity(self.ptr, intensity); + } }; diff --git a/src/engine/graphics/rhi_tests.zig b/src/engine/graphics/rhi_tests.zig index ea52b812..8445317a 100644 --- a/src/engine/graphics/rhi_tests.zig +++ b/src/engine/graphics/rhi_tests.zig @@ -323,6 +323,10 @@ const MockContext = struct { .setFXAA = undefined, .setBloom = undefined, .setBloomIntensity = undefined, + .setVignetteEnabled = undefined, + .setVignetteIntensity = undefined, + .setFilmGrainEnabled = undefined, + .setFilmGrainIntensity = undefined, }; const MOCK_ENCODER_VTABLE = rhi.IGraphicsCommandEncoder.VTable{ diff --git a/src/engine/graphics/rhi_vulkan.zig b/src/engine/graphics/rhi_vulkan.zig index 075782b9..8a9bba77 100644 --- a/src/engine/graphics/rhi_vulkan.zig +++ b/src/engine/graphics/rhi_vulkan.zig @@ -197,6 +197,26 @@ fn setBloomIntensity(ctx_ptr: *anyopaque, intensity: f32) void { ctx.bloom.intensity = intensity; } +fn setVignetteEnabled(ctx_ptr: *anyopaque, enabled: bool) void { + const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); + ctx.post_process_state.vignette_enabled = enabled; +} + +fn setVignetteIntensity(ctx_ptr: *anyopaque, intensity: f32) void { + const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); + ctx.post_process_state.vignette_intensity = intensity; +} + +fn setFilmGrainEnabled(ctx_ptr: *anyopaque, enabled: bool) void { + const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); + ctx.post_process_state.film_grain_enabled = enabled; +} + +fn setFilmGrainIntensity(ctx_ptr: *anyopaque, intensity: f32) void { + const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); + ctx.post_process_state.film_grain_intensity = intensity; +} + fn endFrame(ctx_ptr: *anyopaque) void { const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); ctx.mutex.lock(); @@ -705,6 +725,10 @@ const VULKAN_RHI_VTABLE = rhi.RHI.VTable{ .setFXAA = setFXAA, .setBloom = setBloom, .setBloomIntensity = setBloomIntensity, + .setVignetteEnabled = setVignetteEnabled, + .setVignetteIntensity = setVignetteIntensity, + .setFilmGrainEnabled = setFilmGrainEnabled, + .setFilmGrainIntensity = setFilmGrainIntensity, }; fn beginPassTiming(ctx_ptr: *anyopaque, pass_name: []const u8) void { diff --git a/src/engine/graphics/vulkan/post_process_system.zig b/src/engine/graphics/vulkan/post_process_system.zig index 0738eb1d..47a90677 100644 --- a/src/engine/graphics/vulkan/post_process_system.zig +++ b/src/engine/graphics/vulkan/post_process_system.zig @@ -8,6 +8,8 @@ const VulkanBuffer = @import("resource_manager.zig").VulkanBuffer; pub const PostProcessPushConstants = extern struct { bloom_enabled: f32, bloom_intensity: f32, + vignette_intensity: f32, + film_grain_intensity: f32, }; pub const PostProcessSystem = struct { diff --git a/src/engine/graphics/vulkan/rhi_context_types.zig b/src/engine/graphics/vulkan/rhi_context_types.zig index 0f36a5e1..50a58b44 100644 --- a/src/engine/graphics/vulkan/rhi_context_types.zig +++ b/src/engine/graphics/vulkan/rhi_context_types.zig @@ -98,6 +98,13 @@ const ShadowRuntime = struct { shadow_resolution: u32, }; +const PostProcessState = struct { + vignette_enabled: bool = true, + vignette_intensity: f32 = 0.3, + film_grain_enabled: bool = true, + film_grain_intensity: f32 = 0.15, +}; + const RenderOptions = struct { wireframe_enabled: bool = false, textures_enabled: bool = true, @@ -193,6 +200,7 @@ pub const VulkanContext = struct { debug_shadow: DebugShadowResources = .{}, fxaa: FXAASystem = .{}, bloom: BloomSystem = .{}, + post_process_state: PostProcessState = .{}, velocity: VelocityResources = .{}, timing: TimingState = .{}, diff --git a/src/engine/graphics/vulkan/rhi_pass_orchestration.zig b/src/engine/graphics/vulkan/rhi_pass_orchestration.zig index c2e9ff6c..7f513bdd 100644 --- a/src/engine/graphics/vulkan/rhi_pass_orchestration.zig +++ b/src/engine/graphics/vulkan/rhi_pass_orchestration.zig @@ -324,6 +324,8 @@ pub fn beginPostProcessPassInternal(ctx: anytype) void { const push = PostProcessPushConstants{ .bloom_enabled = if (ctx.bloom.enabled) 1.0 else 0.0, .bloom_intensity = ctx.bloom.intensity, + .vignette_intensity = if (ctx.post_process_state.vignette_enabled) ctx.post_process_state.vignette_intensity else 0.0, + .film_grain_intensity = if (ctx.post_process_state.film_grain_enabled) ctx.post_process_state.film_grain_intensity else 0.0, }; c.vkCmdPushConstants(command_buffer, ctx.post_process.pipeline_layout, c.VK_SHADER_STAGE_FRAGMENT_BIT, 0, @sizeOf(PostProcessPushConstants), &push); diff --git a/src/game/screens/graphics.zig b/src/game/screens/graphics.zig index 8909ac70..cbf91d8f 100644 --- a/src/game/screens/graphics.zig +++ b/src/game/screens/graphics.zig @@ -220,6 +220,14 @@ pub const GraphicsScreen = struct { ctx.rhi.*.setBloom(settings.bloom_enabled); } else if (std.mem.eql(u8, decl.name, "bloom_intensity")) { ctx.rhi.*.setBloomIntensity(settings.bloom_intensity); + } else if (std.mem.eql(u8, decl.name, "vignette_enabled")) { + ctx.rhi.*.setVignetteEnabled(settings.vignette_enabled); + } else if (std.mem.eql(u8, decl.name, "vignette_intensity")) { + ctx.rhi.*.setVignetteIntensity(settings.vignette_intensity); + } else if (std.mem.eql(u8, decl.name, "film_grain_enabled")) { + ctx.rhi.*.setFilmGrainEnabled(settings.film_grain_enabled); + } else if (std.mem.eql(u8, decl.name, "film_grain_intensity")) { + ctx.rhi.*.setFilmGrainIntensity(settings.film_grain_intensity); } } diff --git a/src/game/settings/data.zig b/src/game/settings/data.zig index 794d6c7f..8d206b2a 100644 --- a/src/game/settings/data.zig +++ b/src/game/settings/data.zig @@ -74,6 +74,12 @@ pub const Settings = struct { bloom_enabled: bool = true, bloom_intensity: f32 = 0.5, + // Post-Processing Settings (Phase 6) + vignette_enabled: bool = true, + vignette_intensity: f32 = 0.3, + film_grain_enabled: bool = true, + film_grain_intensity: f32 = 0.15, + // Texture Settings max_texture_resolution: u32 = 512, // 16, 32, 64, 128, 256, 512 @@ -191,6 +197,24 @@ pub const Settings = struct { .label = "BLOOM INTENSITY", .kind = .{ .slider = .{ .min = 0.0, .max = 2.0, .step = 0.1 } }, }; + pub const vignette_enabled = SettingMetadata{ + .label = "VIGNETTE", + .description = "Darkens screen edges for cinematic effect", + .kind = .toggle, + }; + pub const vignette_intensity = SettingMetadata{ + .label = "VIGNETTE INTENSITY", + .kind = .{ .slider = .{ .min = 0.0, .max = 1.0, .step = 0.05 } }, + }; + pub const film_grain_enabled = SettingMetadata{ + .label = "FILM GRAIN", + .description = "Adds subtle noise for film-like appearance", + .kind = .toggle, + }; + pub const film_grain_intensity = SettingMetadata{ + .label = "GRAIN INTENSITY", + .kind = .{ .slider = .{ .min = 0.0, .max = 1.0, .step = 0.05 } }, + }; pub const volumetric_density = SettingMetadata{ .label = "FOG DENSITY", .kind = .{ .slider = .{ .min = 0.0, .max = 0.5, .step = 0.05 } }, diff --git a/src/world/block.zig b/src/world/block.zig index 8062e307..26b541f7 100644 --- a/src/world/block.zig +++ b/src/world/block.zig @@ -119,6 +119,8 @@ pub const BlockType = enum(u8) { spruce_log = 42, spruce_leaves = 43, vine = 44, + torch = 45, + lava = 46, _, }; diff --git a/src/world/block_registry.zig b/src/world/block_registry.zig index 316cd4ed..239639b8 100644 --- a/src/world/block_registry.zig +++ b/src/world/block_registry.zig @@ -207,6 +207,16 @@ pub const BLOCK_REGISTRY = blk: { def.texture_bottom = "spruce_log_top"; def.texture_side = "spruce_log_side"; }, + .torch => { + def.texture_top = "torch"; + def.texture_bottom = "torch"; + def.texture_side = "torch"; + }, + .lava => { + def.texture_top = "lava"; + def.texture_bottom = "lava"; + def.texture_side = "lava"; + }, else => {}, } @@ -257,18 +267,20 @@ pub const BLOCK_REGISTRY = blk: { .spruce_log => .{ 0.35, 0.25, 0.15 }, .spruce_leaves => .{ 0.15, 0.4, 0.15 }, .vine => .{ 0.2, 0.5, 0.1 }, + .torch => .{ 1.0, 0.8, 0.4 }, + .lava => .{ 1.0, 0.4, 0.1 }, else => .{ 1, 0, 1 }, }; // 2. Solid def.is_solid = switch (id) { - .air, .water => false, + .air, .water, .torch => false, else => true, }; // 3. Transparent def.is_transparent = switch (id) { - .air, .water, .glass, .leaves, .mangrove_leaves, .mangrove_roots, .jungle_leaves, .bamboo, .acacia_leaves, .acacia_sapling, .birch_leaves, .spruce_leaves, .vine, .tall_grass, .flower_red, .flower_yellow, .dead_bush, .cactus, .melon => true, + .air, .water, .glass, .leaves, .mangrove_leaves, .mangrove_roots, .jungle_leaves, .bamboo, .acacia_leaves, .acacia_sapling, .birch_leaves, .spruce_leaves, .vine, .tall_grass, .flower_red, .flower_yellow, .dead_bush, .cactus, .melon, .torch => true, else => false, }; @@ -288,12 +300,17 @@ pub const BLOCK_REGISTRY = blk: { def.render_pass = switch (id) { .water => .fluid, .glass => .translucent, - .leaves, .mangrove_leaves, .jungle_leaves, .acacia_leaves, .birch_leaves, .spruce_leaves, .mangrove_roots, .bamboo, .acacia_sapling, .vine, .tall_grass, .flower_red, .flower_yellow, .dead_bush, .cactus, .melon => .cutout, + .leaves, .mangrove_leaves, .jungle_leaves, .acacia_leaves, .birch_leaves, .spruce_leaves, .mangrove_roots, .bamboo, .acacia_sapling, .vine, .tall_grass, .flower_red, .flower_yellow, .dead_bush, .cactus, .melon, .torch => .cutout, else => .solid, }; - // 7. Light Emission - def.light_emission = if (id == .glowstone) .{ 15, 14, 10 } else .{ 0, 0, 0 }; + // 7. Light Emission (RGB values 0-15) + def.light_emission = switch (id) { + .glowstone => .{ 15, 14, 10 }, // Warm yellow + .torch => .{ 15, 11, 6 }, // Warm orange + .lava => .{ 15, 8, 3 }, // Red-orange + else => .{ 0, 0, 0 }, + }; definitions[int_id] = def; } From 29f7f3680d746aa0ca16a1d576d7dfe6977c7893 Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Sat, 7 Feb 2026 17:29:52 +0000 Subject: [PATCH 02/11] fix(settings): disable vignette and film grain by default Users expect a clean, modern look by default. These effects are now opt-in via the Graphics Settings menu. Fixes review feedback on PR #261 --- src/engine/graphics/vulkan/rhi_context_types.zig | 4 ++-- src/game/settings/data.zig | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/engine/graphics/vulkan/rhi_context_types.zig b/src/engine/graphics/vulkan/rhi_context_types.zig index 50a58b44..4a9840fc 100644 --- a/src/engine/graphics/vulkan/rhi_context_types.zig +++ b/src/engine/graphics/vulkan/rhi_context_types.zig @@ -99,9 +99,9 @@ const ShadowRuntime = struct { }; const PostProcessState = struct { - vignette_enabled: bool = true, + vignette_enabled: bool = false, vignette_intensity: f32 = 0.3, - film_grain_enabled: bool = true, + film_grain_enabled: bool = false, film_grain_intensity: f32 = 0.15, }; diff --git a/src/game/settings/data.zig b/src/game/settings/data.zig index 8d206b2a..794e38c1 100644 --- a/src/game/settings/data.zig +++ b/src/game/settings/data.zig @@ -75,9 +75,9 @@ pub const Settings = struct { bloom_intensity: f32 = 0.5, // Post-Processing Settings (Phase 6) - vignette_enabled: bool = true, + vignette_enabled: bool = false, vignette_intensity: f32 = 0.3, - film_grain_enabled: bool = true, + film_grain_enabled: bool = false, film_grain_intensity: f32 = 0.15, // Texture Settings From d8c0eef384d0568bf4f3ed86c1d650a3cfc99025 Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Sat, 7 Feb 2026 17:34:34 +0000 Subject: [PATCH 03/11] fix(blocks): add missing textures and fix lava properties Critical fixes for PR #261: ## Missing Textures (CRITICAL) - Add assets/textures/default/torch.png (orange placeholder) - Add assets/textures/default/lava.png (red placeholder) - Prevents runtime texture loading failures ## Lava Block Properties (HIGH) Fix lava to behave as a proper fluid/light source: - is_solid: false (was true) - allows light propagation and player movement - is_transparent: true - allows light to pass through - is_fluid: true - enables fluid physics and rendering - render_pass: .fluid - proper fluid rendering pipeline These changes ensure lava: - Emits colored light correctly (RGB: 15, 8, 3) - Propagates light to surrounding blocks - Renders with proper fluid transparency - Behaves consistently with water fluid mechanics Fixes review feedback on PR #261 --- assets/textures/default/lava.png | Bin 0 -> 313 bytes assets/textures/default/torch.png | Bin 0 -> 313 bytes src/world/block_registry.zig | 8 ++++---- 3 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 assets/textures/default/lava.png create mode 100644 assets/textures/default/torch.png diff --git a/assets/textures/default/lava.png b/assets/textures/default/lava.png new file mode 100644 index 0000000000000000000000000000000000000000..dcc4906dbb52ed8f93d524ef0439a6a04052eddb GIT binary patch literal 313 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!D3?x-;bCrOULb6AYF9SoB8UsT^3j@P1pisjL z28L1t28LG&3=CE?7#PG0=Ijcz0ZOn1_=LFrH)i<%|Nrti-`s&Bj7i?^E{y+~bngK< z>?NMQuI#Uv*aa0d6i@DH0t%^?xJHzuB$lLFB^RXvDF!10BQsqCBV7aY5JPh-VQ#<_AR8glbfGSez?Yp9P=T?Evi0k@$fGdH!kBr&%Dw;l~omRg`59#0p? f5RU7~2@1SGo&f{n@l~yTKo*0itDnm{r-UW|gqKYV literal 0 HcmV?d00001 diff --git a/assets/textures/default/torch.png b/assets/textures/default/torch.png new file mode 100644 index 0000000000000000000000000000000000000000..bed8a77769fd51875911292415eb4e4381e3ed3c GIT binary patch literal 313 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!D3?x-;bCrOULb6AYF9SoB8UsT^3j@P1pisjL z28L1t28LG&3=CE?7#PG0=Ijcz0ZOn1_=LFrPhs01O;9o&wzpP_^Q@EAdA7%)z4*}Q$iB}scua+ literal 0 HcmV?d00001 diff --git a/src/world/block_registry.zig b/src/world/block_registry.zig index 239639b8..a57980f8 100644 --- a/src/world/block_registry.zig +++ b/src/world/block_registry.zig @@ -274,13 +274,13 @@ pub const BLOCK_REGISTRY = blk: { // 2. Solid def.is_solid = switch (id) { - .air, .water, .torch => false, + .air, .water, .lava, .torch => false, else => true, }; // 3. Transparent def.is_transparent = switch (id) { - .air, .water, .glass, .leaves, .mangrove_leaves, .mangrove_roots, .jungle_leaves, .bamboo, .acacia_leaves, .acacia_sapling, .birch_leaves, .spruce_leaves, .vine, .tall_grass, .flower_red, .flower_yellow, .dead_bush, .cactus, .melon, .torch => true, + .air, .water, .lava, .glass, .leaves, .mangrove_leaves, .mangrove_roots, .jungle_leaves, .bamboo, .acacia_leaves, .acacia_sapling, .birch_leaves, .spruce_leaves, .vine, .tall_grass, .flower_red, .flower_yellow, .dead_bush, .cactus, .melon, .torch => true, else => false, }; @@ -292,13 +292,13 @@ pub const BLOCK_REGISTRY = blk: { // 5. Is Fluid def.is_fluid = switch (id) { - .water => true, + .water, .lava => true, else => false, }; // 6. Render Pass def.render_pass = switch (id) { - .water => .fluid, + .water, .lava => .fluid, .glass => .translucent, .leaves, .mangrove_leaves, .jungle_leaves, .acacia_leaves, .birch_leaves, .spruce_leaves, .mangrove_roots, .bamboo, .acacia_sapling, .vine, .tall_grass, .flower_red, .flower_yellow, .dead_bush, .cactus, .melon, .torch => .cutout, else => .solid, From 4bfd13b7195239238d09b384ff6246dfe8a6b9db Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Sat, 7 Feb 2026 21:52:42 +0000 Subject: [PATCH 04/11] feat(lighting): add LPV compute GI with debug profiling --- assets/shaders/vulkan/g_pass.frag | 2 + assets/shaders/vulkan/lpv_inject.comp | 49 ++ assets/shaders/vulkan/lpv_propagate.comp | 47 + assets/shaders/vulkan/terrain.frag | 53 +- assets/shaders/vulkan/terrain.frag.spv | Bin 46596 -> 52988 bytes assets/shaders/vulkan/terrain.vert | 2 + assets/shaders/vulkan/terrain.vert.spv | Bin 6104 -> 6200 bytes build.zig | 6 +- src/engine/graphics/lpv_system.zig | 805 ++++++++++++++++++ src/engine/graphics/render_graph.zig | 2 + src/engine/graphics/rhi.zig | 4 + src/engine/graphics/rhi_tests.zig | 11 + src/engine/graphics/rhi_types.zig | 6 + src/engine/graphics/rhi_vulkan.zig | 12 +- .../graphics/vulkan/descriptor_bindings.zig | 1 + .../graphics/vulkan/descriptor_manager.zig | 4 + .../graphics/vulkan/resource_manager.zig | 12 +- .../graphics/vulkan/resource_texture_ops.zig | 161 ++++ .../graphics/vulkan/rhi_context_factory.zig | 2 + .../graphics/vulkan/rhi_context_types.zig | 2 + .../vulkan/rhi_frame_orchestration.zig | 8 +- .../graphics/vulkan/rhi_init_deinit.zig | 4 +- .../graphics/vulkan/rhi_render_state.zig | 4 + src/engine/graphics/vulkan/rhi_timing.zig | 23 +- src/engine/ui/debug_lpv_overlay.zig | 30 + src/engine/ui/timing_overlay.zig | 3 +- src/game/app.zig | 20 + src/game/input_mapper.zig | 3 + src/game/screen.zig | 2 + src/game/screens/graphics.zig | 13 + src/game/screens/world.zig | 65 ++ src/game/settings/data.zig | 29 + src/game/settings/json_presets.zig | 38 + 33 files changed, 1404 insertions(+), 19 deletions(-) create mode 100644 assets/shaders/vulkan/lpv_inject.comp create mode 100644 assets/shaders/vulkan/lpv_propagate.comp create mode 100644 src/engine/graphics/lpv_system.zig create mode 100644 src/engine/ui/debug_lpv_overlay.zig diff --git a/assets/shaders/vulkan/g_pass.frag b/assets/shaders/vulkan/g_pass.frag index f14f09ea..cc4f0b01 100644 --- a/assets/shaders/vulkan/g_pass.frag +++ b/assets/shaders/vulkan/g_pass.frag @@ -31,6 +31,8 @@ layout(set = 0, binding = 0) uniform GlobalUniforms { vec4 pbr_params; vec4 volumetric_params; vec4 viewport_size; + vec4 lpv_params; + vec4 lpv_origin; } global; // 4x4 Bayer matrix for dithered LOD transitions diff --git a/assets/shaders/vulkan/lpv_inject.comp b/assets/shaders/vulkan/lpv_inject.comp new file mode 100644 index 00000000..138f1957 --- /dev/null +++ b/assets/shaders/vulkan/lpv_inject.comp @@ -0,0 +1,49 @@ +#version 450 + +layout(local_size_x = 4, local_size_y = 4, local_size_z = 4) in; + +struct LightData { + vec4 pos_radius; + vec4 color; +}; + +layout(set = 0, binding = 0, rgba32f) uniform writeonly image2D lpv_out; +layout(set = 0, binding = 1) readonly buffer Lights { + LightData lights[]; +} light_buffer; + +layout(push_constant) uniform InjectPush { + vec4 grid_origin_cell; + vec4 grid_params; + uint light_count; +} push_data; + +ivec2 atlasUV(ivec3 cell, int gridSize) { + return ivec2(cell.x, cell.y + cell.z * gridSize); +} + +void main() { + int gridSize = int(push_data.grid_params.x); + ivec3 cell = ivec3(gl_GlobalInvocationID.xyz); + if (any(greaterThanEqual(cell, ivec3(gridSize)))) { + return; + } + + vec3 world_pos = push_data.grid_origin_cell.xyz + vec3(cell) * push_data.grid_origin_cell.w + vec3(0.5 * push_data.grid_origin_cell.w); + + vec3 accum = vec3(0.0); + for (uint i = 0; i < push_data.light_count; i++) { + vec3 light_pos = light_buffer.lights[i].pos_radius.xyz; + float radius = max(light_buffer.lights[i].pos_radius.w, 0.001); + vec3 light_color = light_buffer.lights[i].color.rgb; + + float d = length(world_pos - light_pos); + if (d < radius) { + float att = 1.0 - (d / radius); + att *= att; + accum += light_color * att; + } + } + + imageStore(lpv_out, atlasUV(cell, gridSize), vec4(accum, 1.0)); +} diff --git a/assets/shaders/vulkan/lpv_propagate.comp b/assets/shaders/vulkan/lpv_propagate.comp new file mode 100644 index 00000000..40329399 --- /dev/null +++ b/assets/shaders/vulkan/lpv_propagate.comp @@ -0,0 +1,47 @@ +#version 450 + +layout(local_size_x = 4, local_size_y = 4, local_size_z = 4) in; + +layout(set = 0, binding = 0, rgba32f) uniform readonly image2D lpv_src; +layout(set = 0, binding = 1, rgba32f) uniform writeonly image2D lpv_dst; + +layout(push_constant) uniform PropPush { + uint grid_size; + vec4 propagation; +} push_data; + +ivec2 atlasUV(ivec3 cell, int gridSize) { + return ivec2(cell.x, cell.y + cell.z * gridSize); +} + +vec3 sampleCell(ivec3 cell, int gridSize) { + return imageLoad(lpv_src, atlasUV(cell, gridSize)).rgb; +} + +void main() { + int gridSize = int(push_data.grid_size); + ivec3 cell = ivec3(gl_GlobalInvocationID.xyz); + if (any(greaterThanEqual(cell, ivec3(gridSize)))) { + return; + } + + vec3 center = sampleCell(cell, gridSize) * 0.82; + vec3 accum = center; + float f = push_data.propagation.x; + + ivec3 off[6] = ivec3[6]( + ivec3(-1, 0, 0), ivec3(1, 0, 0), + ivec3(0, -1, 0), ivec3(0, 1, 0), + ivec3(0, 0, -1), ivec3(0, 0, 1) + ); + + for (int i = 0; i < 6; i++) { + ivec3 n = cell + off[i]; + if (any(lessThan(n, ivec3(0))) || any(greaterThanEqual(n, ivec3(gridSize)))) { + continue; + } + accum += sampleCell(n, gridSize) * f; + } + + imageStore(lpv_dst, atlasUV(cell, gridSize), vec4(accum, 1.0)); +} diff --git a/assets/shaders/vulkan/terrain.frag b/assets/shaders/vulkan/terrain.frag index c5b4a268..2d3c61a1 100644 --- a/assets/shaders/vulkan/terrain.frag +++ b/assets/shaders/vulkan/terrain.frag @@ -32,6 +32,8 @@ layout(set = 0, binding = 0) uniform GlobalUniforms { vec4 pbr_params; // x = pbr_quality, y = exposure, z = saturation, w = ssao_strength vec4 volumetric_params; // x = enabled, y = density, z = steps, w = scattering vec4 viewport_size; // xy = width/height + vec4 lpv_params; // x = enabled, y = intensity, z = cell_size, w = grid_size + vec4 lpv_origin; // xyz = world origin } global; // Constants @@ -100,6 +102,7 @@ layout(set = 0, binding = 7) uniform sampler2D uRoughnessMap; // Roughness ma layout(set = 0, binding = 8) uniform sampler2D uDisplacementMap; // Displacement map (unused for now) layout(set = 0, binding = 9) uniform sampler2D uEnvMap; // Environment Map (EXR) layout(set = 0, binding = 10) uniform sampler2D uSSAOMap; // SSAO Map +layout(set = 0, binding = 11) uniform sampler2D uLPVGrid; // LPV 3D atlas (Z slices packed in Y) layout(set = 0, binding = 2) uniform ShadowUniforms { mat4 light_space_matrices[4]; @@ -277,6 +280,47 @@ vec3 computeIBLAmbient(vec3 N, float roughness) { return textureLod(uEnvMap, envUV, envMipLevel).rgb; } +vec3 sampleLPVVoxel(vec3 voxel, float gridSize) { + float u = (voxel.x + 0.5) / gridSize; + float v = (voxel.y + voxel.z * gridSize + 0.5) / (gridSize * gridSize); + return texture(uLPVGrid, vec2(u, v)).rgb; +} + +vec3 sampleLPVAtlas(vec3 worldPos) { + if (global.lpv_params.x < 0.5) return vec3(0.0); + + float gridSize = max(global.lpv_params.w, 1.0); + float cellSize = max(global.lpv_params.z, 0.001); + vec3 local = (worldPos - global.lpv_origin.xyz) / cellSize; + + if (any(lessThan(local, vec3(0.0))) || any(greaterThanEqual(local, vec3(gridSize)))) { + return vec3(0.0); + } + + vec3 base = floor(local); + vec3 frac = fract(local); + + vec3 p0 = clamp(base, vec3(0.0), vec3(gridSize - 1.0)); + vec3 p1 = clamp(base + vec3(1.0), vec3(0.0), vec3(gridSize - 1.0)); + + vec3 c000 = sampleLPVVoxel(vec3(p0.x, p0.y, p0.z), gridSize); + vec3 c100 = sampleLPVVoxel(vec3(p1.x, p0.y, p0.z), gridSize); + vec3 c010 = sampleLPVVoxel(vec3(p0.x, p1.y, p0.z), gridSize); + vec3 c110 = sampleLPVVoxel(vec3(p1.x, p1.y, p0.z), gridSize); + vec3 c001 = sampleLPVVoxel(vec3(p0.x, p0.y, p1.z), gridSize); + vec3 c101 = sampleLPVVoxel(vec3(p1.x, p0.y, p1.z), gridSize); + vec3 c011 = sampleLPVVoxel(vec3(p0.x, p1.y, p1.z), gridSize); + vec3 c111 = sampleLPVVoxel(vec3(p1.x, p1.y, p1.z), gridSize); + + vec3 c00 = mix(c000, c100, frac.x); + vec3 c10 = mix(c010, c110, frac.x); + vec3 c01 = mix(c001, c101, frac.x); + vec3 c11 = mix(c011, c111, frac.x); + vec3 c0 = mix(c00, c10, frac.y); + vec3 c1 = mix(c01, c11, frac.y); + return mix(c0, c1, frac.z) * global.lpv_params.y; +} + vec3 computeBRDF(vec3 albedo, vec3 N, vec3 V, vec3 L, float roughness) { vec3 H = normalize(V + L); vec3 F0 = mix(vec3(DIELECTRIC_F0), albedo, 0.0); @@ -307,14 +351,16 @@ vec3 computePBR(vec3 albedo, vec3 N, vec3 V, vec3 L, float roughness, float tota vec3 Lo = brdf * sunColor * NdotL_final * (1.0 - totalShadow); vec3 envColor = computeIBLAmbient(N, roughness); float shadowAmbientFactor = mix(1.0, 0.2, totalShadow); - vec3 ambientColor = albedo * (max(min(envColor, IBL_CLAMP) * skyLight * 0.8, vec3(global.lighting.x * 0.8)) + blockLight) * ao * ssao * shadowAmbientFactor; + vec3 indirect = sampleLPVAtlas(vFragPosWorld); + 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); - vec3 ambientColor = albedo * (max(min(envColor, IBL_CLAMP) * skyLight * 0.8, vec3(global.lighting.x * 0.8)) + blockLight) * ao * ssao * shadowAmbientFactor; + vec3 indirect = sampleLPVAtlas(vFragPosWorld); + 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; vec3 directColor = albedo * sunColor * nDotL * (1.0 - totalShadow); return ambientColor + directColor; @@ -322,7 +368,8 @@ 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); - vec3 ambientColor = albedo * (max(vec3(skyLightVal * 0.8), vec3(global.lighting.x * 0.4)) + blockLight) * ao * ssao * shadowAmbientFactor; + vec3 indirect = sampleLPVAtlas(vFragPosWorld); + 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; vec3 directColor = albedo * sunColor * nDotL * (1.0 - totalShadow); return ambientColor + directColor; diff --git a/assets/shaders/vulkan/terrain.frag.spv b/assets/shaders/vulkan/terrain.frag.spv index 30588452bbb7511442fab78792a6e46949d60563..e1a64e16663d95319f3d0bfdccc6d1f4bba8e834 100644 GIT binary patch literal 52988 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 literal 46596 zcma*Q2bf+}8MS?2Wpxd^_FC(C_TJAfXP-8cbS=Nd3RSgS zwQ@DGItk>lX0<#@g*Kwj51Bk=@=gneX6&@bp1WvYt?DwXZL3%7Ro!6I+}{3q>MJf^ zRqK+L^it=k#q}b673#AnoK&9bQEa~+OSR_sEdfI>Ry8C zK`qDN`Qgg&37&uZ~GYM_5XU&~3KRogx*RO`m) z;AwMfkN#b$sa4n7ZNKhn9q_>Pq25J(3#faAY6JMR-o<@`6Z?l|_YLlG&K{}1r&`}~ z0}J|x`UmQ<>0z8VLz~q%G{KOk%4M zv%!wnuH?(&Sh3m%KCyqn&|v?xh0etxhn(DwfY$3i?TJt7-wS*w$uZoUd~jgltl9JW z7C0e2)r!>t^c3@&$LRavbI(X=10vK7&@`$J=Ol?!|Pe8nuyk} zXD;if9hI|?y1J^d_$9V+;8vYo)p&5mVFGw~{UfWx&}I(yEtuCgrwP*->!nur?GWtK z2NoPZyKkr$h}Kp2`(OsuwR$All-{}X=k!gP&)V*v-aF^W-uZQqwXbb2s>`id9Y@~G z^DkDEKq6&G$pwEEFg9Sa^F?@HC1men!-xQPc38;y*J^Lqk*y>q7Z%@}CK z?095+P6B6qP6lUuP5}>ZZ`Eo#S`+i+zFED~7juU7O>aj{;LUKgxuU7A6O{~X^KkoI0W>~m&#Kt?5`j*ApRn3BDtUO;bR{h}g;SBKbvFfSL zMC8?Bv%RUXkTYc)T7J|$9aD8^heKuNW+~=Tm#=RIValh3v?YLL2E{13A96d1$y%lpA zu<@Q}J?-_{U7ZgvV_X97jPU}r#CT%woO*2Efz}!0g=mTKBFnU699dlk=UQ{(K(iPV zB{KDORhN>tu8}>}yTHTu(G{!rqRsA`x43WdA%lIa$f3Uec~sWk=&a*C__(HA=a-WY z-%nPmuCBHEdPC8L?wS45+sij|+Ecv`zro(cQ-*p6ola`&uHN6$Ce7o@BmV$=MtzxW z#eW;zb!!}7CU>oizeeu)WtrQ_=l2fwx-PZdLGC%G?OPqTZ^PXaw0*C`_5-*VKy43p z*nSLm-D!J@d{OV5g?%;ess2jt7TQ0wa7MJhp|LNYwXkpAbk~@1{hhqkm+tC+;4JI>tUurV&J;ZFy|dd*@D_&uvPyE;Q~_ zrq19-C3SZ<+Vp`r1Kt-z?*Y#om^F2oKC-TtTf)};+|;w_+SGxWGZ*x^AkxMP;CgI| z_(!76adYjT=TgMC75|Fh`rdGAN-S%CC2aGj4Yn;KxZc?>?mFpbu@Z?e|Pem;62qT@cI4c^v$Vf z%CcN9EL&f9TcX-bw7M^B1}-k^nr+$k+U~B-7{+_5Iq;#`+yKlTsBhk^=d9FoWbXp^ zbn7`EZEo+Z{?^)Gp}G>@+=JCurz!J$r)Pb=A6x6vR<5HJD|L^09;Pm^#MHUHp6uKx zb2umZ=r+A|8;BRDMLmxrYnyATHD*(X`WTXWe6btj)f8;myzlO*Zh*U7>wDYUXGHY~ z+Vq8kgWP%KcChyS4SHSSl+4$N>T$Gby$kw|x1mCR!a7>YgwP+?u$>Y>w}swqbiqPjxQZ!q((-ALu+VFKA^OAA~Q9v%9*sgMX-le|Q-0sXhsx*LTj) z6xT-GzY*0^Gvge8JYUPHv)?;_I27K16b7;L4-L_>_ z=M>9NqEPRA{JW|*!&_tARhnqJh-lN7PzdVzk{DSjCWP@;Dy7M{&}_PxV;~3 z!Ti4I3+MFeR*%EAXlG7rjq8n;sUKD9?HPMdb*p8XXN*>vp6YhAdOvOrKVBoM@9S5e zAI-x>U4M7=5c;&i8QwzS*IoSrUXJO*9sH3F{%8k(Y#8sU9*5Wa=hT_>v~@rGCp7LC zCN#GOwA)oZi#EA_R_Lkz17`gccJ@=WGOqs)<119J!dv}oo^YSQs7Q2gU<0UZn>O3vC*5fPi z`uW9|EY14q?(v@DoLaHWNH`A=Ts2x{R&024>7`zta?yrAKXg~?qRpSpg}SMCJ$Z4@ zv*9q_Q*8ntKE@-ftHu)-Ich{T0X)0U0WHt3p6Up6`#-T=Uw1V{t+-Yl zZ#_ldP8`NZRHrm`lpQes^U&*OH-^6npLj>q?_d{sC&9Gl1)k&Vds#Nv1!o@Dx*5>- zy=XHifAHYI+~(F0PoBBZXY$UDuGV#4SNr*@bJ@jMzYn38v409a#BI(Dc6!_NJ@6uK zwwAq%?5aMG*1A@7Ro?*T8Q%BBi;m(MrQRn-RNscrt0`B7)(fe+&R<%-<>%!Bjq5Jj z@#BxG?Y?tfbUb%g+z__nXCG1<-_JO;RLi`Cmh0%N;APL|-BlM4B<1XIY+k99lTo*BaCA>Rihg?>BtE zpIzIze-C{4`=k-o<<@gtUaYo)aW1Y$&%MDXz|)(?t;cT$i>55Q$f@}aVbS>Zi!_w( z>N9BNTzsyBf1!il*1^Bj!N1(WzuLjS*1^Bt!SCqc-|pb|b@1&W`#bmp9sEZf z{3jj!7ajcJ4*p07f3$-?*1><*!Jp~i|LEY)b?|?8@aH@Diyi!>4*p69f31Tr$4i59 zJ#=;Oo({gkFy2$G1z+|~z;mX%+HjapceQZ`-=u?YI*fC!!dnk{t&_{O?02w5UNPI( zpq~1>*rF4cec*7f_B+|433K}Av#(ERJ?~O_ME&h7`Z4v}8e?6dIvW3l)H{C;Pvvvj z4BO}a3e^;Ctvv>H!Et@FT6dk+cRaQu2WIpYuZ&Q8YTa{xt~PR??e*nRw{^Zw9az*i zIN0x1!}mSfc%0#WV8NMFn+H()dm?&W$J80UK0qA8`JZ=7hcd=u54L2U0b1{!`DjTeRly!)~l& zYkODweT?IocRLx+@Nzt()v-~BvE_YF8LwK#*m#Y_cRQmgqaMC2wyye}$!d+w_a}Z2 z?P|F1OYHyZ4fh>|b;T~<9PVM=K3{5gY{N_Y5;|%9_D#F@O7gVteTd0GB6?3TCx`g?8GKkds)f3M5>d+6^q zSpW1@Uiy2z)t|6xf3LN2vYNYxmG!xQ$vro$-*5VI*S6e!OYYi~`>kKTYQx>9j3@r` zG9LFT?WteB%-+@BXI!)HIUdIDS`1fSqeH9LXQ4TMYGVs+`9||TacyQ_KAQP=or%{2 zIWD6}>(jpN$hD1v+b1>CXt;B(U431+F^wYGw*G69C)Ux#zZN|H+H7N^Ml%M>Y)oz$ zb<1o*ZW(pcXslayXzJEy`R&OqZ<*0VwnKwc-drF3N8`0ehh3ZP?%U|{mbOB5P={~o z(*Iy=#;mWp<8uVLv9x$kbu?I?W#vXz#}}G7yQ>kAW|KdgOT5-%a3ABoFQKr8Qf|Z-Fn@*lpv^+P>tV z)OT0SE7nb`&AS^P^=S8j<(7?hf5X*M*U!OKHFj+HLp873{OK02+xFafrp5XBoaL(* zz?M@VLBM6qt4XRQbv^cRJ-B|G;J-fDeyOG14Ql`D!0-)gKC0d4jcPu&%{Q+3lD7XQ zH9x&=e?!ghY1=n#m2dOSYQ9U`e^kwv9FX=$*L;n(ee;^nZ}Tl`{&>5-Eo**E+rCxJ zZ#ZxVb}{?AHOcvKJngIfvrqQJ*p1g1jmubU-!^T>x~(r`y&c%K>Rz$~*mY+c8^HG< zckOE5l&iJ(c52bJ{r^eUy%*)&SFK~ehI@abmRR;_xc*}rZ2hs1Lvs(-KEAO#_ZjCS z&>no?fqLB6KtCQ{+TRRkeU8)Lbu_K1BlXXMyD!H+x8T;d81CMm_LjgWob|5uJYN96 za?;)V#r_WXJD!;`eg~p(@3|0e8U3``k1NO>^YqiWu0nInUEkj0Io@iUa*ld0rl!Bi zIz9k*9JM*Na_`S_9lNH%)|1$t1h4clf|s7w#MWGJK_nw`lj}75GipPi*to z;8k_Q*!Yj2u`BLfxWDm?1Z#H8j7RQ0pj!M_hu?JlclOhNoyKN=^p|^&So&`Se{}W3 z#=0)I06S)6mF2f=xO(c}1x)>Qe~e>axVd9`VB=rz83&=gyv6eb7%O$@H785w_WBCxIXIfITU=+ zM{jNK4~M}=o&7=fgVvsXI9wm~lsN)?-q~lg`!gAS=_!A0mpKxyk9x`+1%BwBW7=ho zh99$9Z#%YQ;QFYi%(39puUOn3gX7?TyLsF}iERp8AN7NTe`xpT1o#dgU)*lz zM7TccDRUCIx_ZTSJ14^rzx=uO{G0;UM?GcU1Rip#{WgL!a$)V(HoJ`IpOM_lu+Vb`Q<%r)a0a88{g$J{Y=OzfjE z8-sOvE}up2xow>rvfH0S?jG)5kOZu;`Nr(ni}BIV@^2;gP;)Pl`#kB9{#{w~CG~Z{ zx<3tn@PWzgIe)(8-?oqOqEIbqkH>-VM_2#BSmQeeocQ$0VXPj;cPu{o8Q*c_9%{xX z_xaW%@%6&ldk@HUV;Wp7Df)G2-hU-o-aT$9x?`C7UW3y==C5@f>fz$zy;@TI-h$5c zv%OY+1|L|ftG(y{9?l-z9+#)!#JPXUKMk*{d+KpCj(@^eCf|W*t|WgE?tM#AdG9#F zhrVhL?tNP^ZUc{>dD=J>oxU94Se_4doV~U>mhvTyru~uzJ3rdxz8kPi>iZy^n8)dV zV`Fpd^q2eYp!B~5e)e7GAC&fdf1o{a-wQWh`(~cLe+S+BHB)TH;5hvdo9_#()Aisx z2M@1xzXsbsxUP&#?z;`O^vib}Y1cS>w-N3=hVM2??z@d}`{la~kNP|Z8>V)T&b&U)@i~TM ze8)9qk0r0ik=%D59^S|KZo~UA=hk{Ha{`+8x7sWt_kF9{UX*cLs!uU;`}rRf$4TGq z$^E8?dz<@xu;hLhEV-WEK?_>Su7w$UsyI#2dezOZV zUccLg`+majb|qiC;QIR=FLvwOw1bc8;C{c0zvcaY7jAogzYEvy_q&phDY)(VEiZQY zfd#kxqz>+Py!dPPn_al$<+r+U>+_pjxc+{F3)k*k_~E4km$O71tblKTxU z-1hv2R&u|gmE3P=;l|@Pv~bJ&4XxyULkrj6Z)hdIui%#V8(QpgzoC`fZ)oAx=Qp%) z{r!eka=)R4>+d(TaQ*#;R&u|gmE3P=;g+d(TaQ*#;R&u|gmE3P=;g;_yxaIwZ7Q5@yZ)oA}4}L!jx4hra zO78cwaO?B?nfw6uC-=DvINz6$R$<2@)ao5znyJP&wgcJK+{4r@oKy-{pEw)HNs*Zyxb^;`y4^Lp|UzA0n=JW<=$yTR7U zpGtr0v(3vDIBZj&_mI?VQ=I-@0k*$KQ=j8-C0Nbm;eMX6xC$*}pW=v*$UPi$Z67D8 zIp*U35s6sM`OJKn+lJ%66nqoOF_+&AHh%BDv>E58$kkKk(_pn@h|4mc0sF4R6z#KU z-WQr2WBon{wr3A=jT?q+U2wD~Y`-sP`h7cCKlS%E+SkGAqo~w=d4Hg09iH#s z1m8ijefgbW+n?0fEaN?ddir%2xIEwQhO6a#{}$N8b)fAYlA3cbPM!CHT?et5Yx5rE zJEU>MG@fAY1N$D*cYD)F#^IQM7hPNSi|>Kep2p^S`~ld*IJAAAq-GrA)PFzN`hQ5i z6#vx!09;$@e-NzZGn8?sF7xb1KLXp9ebL``jO8cf9>${2k4b9ABF@@h^_nEV~hxQ)}e zqCW#RCS$P8=+A;}&-$%T{nzCGAX%5?^l|*XXIFQ;^!X=AP40X>2lgB~nq9S-G$Dpli#%G6Jk- zou0QN!5*Hs+EyT`dESas|BB$$?>9vIhg7Y^U)oat%3!tJN2M>;awZOi%4-*${; zHF6JQ(PveXnz4w}=IY?{D4W>UfG4)H?`xuK%RR(eV9To8-n!&zPun`A^+>iScAS0Y z%zmi9HhmJy`rtB_4d98T9H$M@wI!B~z?N12G$ZMCe`Bz^bLiZreRbDy+Bdg-%WMKR z#;ggsz8N=jeLbHx1219R^z~U;9@}WJ>r~q)uspUcz^++sn}g-r$f~XQ%l>Qgd0MW` z*tP+?PPO@LEg!?%3$kik{^Cc`>o{!BdM&S?{yy95lR17P*z?tIetw5b``e>y%lg{^ ztd{HYPGAq$l(rp7YR;oL_3sR}e&3ar_3whNE%omTRy&kFJI<-gJnMTmux+hI(%*Jm zZ+nm@7JYUnsTqqnvFr)X-c+ukz0kEKmc7Ahi6wQJCzgG{wq<Y844ds} za~+H!S9h$9+4zhzw(K*pnP+WG1Y2HT-{r_-I~eR53qJ&`kN3A^)uH^Qk9zjy!@$PB zRb%&hW!;m|wON<NOxki^aspW0BkS%&xNUoHTAl+Zp=-lp!+==8ESeHJI z#o^@Ijom%q6mWU}{wBCu?%&@6_VE1F_GXfr^DIuCw&NOz%{+5A6?|I3PY1gv<=JN% zT&JO2cO5ZyN+xl_8E=cKKdQRbIdZa_Z4=_8G~hv_f&H28P}O$$JJ+w@_e3! zt}W;DY_MAPt~09&KREwF7M;#z}0fyN*VL)3v=QxaQ zG1$ZL&~^?<&Dg|w=2tUL&z-k{Z72KD+rerk57&VEk^WvI&qqIxbTM(tmw+v!Za>Z? zSIgX90M6V_qcZD3s@}n0+A_Bng4Hs&7lS?Qi?)kMYW78(`rir8Ia%J@T!OAG^DvduYW6MTdo|p;vR1AEt9_7k6LaVD?6qLaYO|jo zBKNSL+O8wrMY5mb?7<%fJLcYl*`Dj@BWUVbM;`^NnLIkz(Z|r2(62np+yJ+XdY&IX z4z}DRl6J@EdUEyj^OIovnSJL|V9R(8X>(t4|B3%iXg89wewKo>KFjrUGrBg{Ia&2- z{<5sPb-DIG3wFI5*JsG(v3(w#`^C?J<+0rgwjY-J0=Yc4FM@L~c^g=+-IfB8*A#j9ehh;`x?1Cb$kPSYGeC4xjeRSf)8(OcaY1+@KfOS`%dtD za{FxG?1Qlyqp=u+ZCR&fjq4NSmh;?mow=r5TWRlZa5*pcz+Z1(zJ;zW^KviPvg+yc zcfgsKZ-eEreHWa0xeqK)4BrR8%6c*0?}6p9{Sch-{sCC7-~Hsqkoq44XS^Q(%TvdX z!5Qx#f#tFN6rAz?30OXcRqc5H416xReKBTZGX~qX4ePQ@###SzoE>lZyU88*pOYuv zUxG7MzW~b<)33l8kB7nX*nSPpcsv4@$MzfWjU?lFlw6+nehbd{JO-BQ_dD{3NMm@r zY@Z(oPbW7f+qMnsvW&iI>-S*CH1ALT0Jf}pe4YS1w(fQnOZqUj+K0X+%fwT zx%GL^_9VExXZtf;E%$6sfj!(uwf%+kKa%x`6W3qCj#Kucr@^nc=KqGSEo=VoV9Tmo zm*e#;_&k#Rd4^n`cK!*@JpKbLkL_RJ%;R%lxpn-T+;&p`3*gM#^I&=E_zyVi`bDrj zwwJ+-4sE+4~Bxft_*!55PouW{MF?OMO}=%4w~*ZFX49Y@DAvAqgT-(LaC)4n(T z>HBNw^4OLKr|*9IlgHK#c5ZCn?||~OHv*io@SC7qKfee1K00f7B-}M@Otx(s)>VH; zBu`r_g3C3$65O)t@mU${IA#s60@qJHW29D&kz-Y^Vb^lj+N$`v*8G{HrNo$aRzug8 zdyCbgUe^Sb>M0y53kXQWnDC5@!i5w^u)3ry0(0G zSs$!sx`694S+xOwS+_RF!Edf=`L4DRcxC+5wfhZr8}@kHa1Fiz?tO?hzsJhOev7s2 z7+SGEn}PjzzAUewcK!V3t1b170)L_&=hl7vXt+M*{oCen^~~iKV9Tjz{I&#JF85Pg zfz?bNj-U7Cj)VQ!8r^#IGcIFLbN(|w<{9&Cz!~#tRF*N{7F}EBe><>R=6`#zhx4!P zjU+YaUz~B+0i1F0eezCl+w*6Y(#M^_>gnSyU^SCR`k2^uML&x7e|!RL zUD{IDC&AX8x;_Qg&y>1u1nZ|Q@u`iYU#{DmN!GEHWWA2jr%CGZ`3zW}TS)QwELc6~ z(&xaIQ_s2dd2q%@`>TwxS~(x9!9B|P@OLDg4}Z_qpV4!UZX-G7;a@Je{qWyGEbX@! zT>Bjbx4v%`T>G~RuKjxj*ZzZoYk#nV|G43qn=iucN7l-hz>bAwwK*5|Nj+u00xrvZ z6`t|aW*Pgfo-$ttTaPyTEYCIX8{m~m>e?M6wUqrP*s@tG_kh*>T{it4qkBlH?_RL= zscUyk)l%Pgz-4{kg{$31(%&&vOMTx5Tc5gi=Rhs>{SaK%_W)e&ev$_}Adl;Ud3HN%T->*pO`ltR! zz_y{y_*^q;w&i*EYm##m{wUZn&UyG7uw|B#;`10-J=dh)f?YE^k*v=;^ojrPz>ahH z<6y@rZTueG*~TBx)YHZj;Iv_VkCXIC9e)H{N4a1839gHD?_4ESyizI#2y@t#G zL9%Y^5@$~T12#^%WAGBm@rwOru(4-N{}-%}x_z|oYB{f70k1|<&wl?Z*mks~-q*m6 zmHUb7*$GzDH{+-l|K-5OnL3w;t7Uw(sl~qwY~1nhhO0f))UQp=F?PHi7jw%y#y!{^ z<0Zr=UjeS>cVy$M*8sSjL&rDmkHlvGT%+Q0jc$PMu_7sJbbYWfY)I}pbe;L{+h$$L z{WosI{XNnh8=iA%6}a^*B_)nk(bV&EZmWaU{C(efO<(=@Ce-bhZOT*UT43wErKodl zH1*WE4p^>vK^v)^7x>`Fpb&|Lwt+(`KFaeQQ$U+zG6v zU;4f?Slu}gmvgWg{qfj@_Q>-1=r0-1+w3txf&<&*|W2HQfGggnf6oeQ->Ce%hDh{eSt+eZNM}^V9xtHIs+g z@}vu>!#WQ@uwMNfcgH|2Z5{-6tnwXoEL_dxVO@D1nSf?I{=7lCMkm6xWiBUyZKqsw z2gB9#jCBaua_ZTe4h7r3w$ybP*!ar24o6eZH|WV={?tDkr0ob&?vsxMYd7V4@KIpf z(&m`B4%9Lx$AFDLV{$B5&E(;j=zq);J9#0@SZpV`>cO`x&HhS7P z39e@HFiV{$L#$UH<8_^?C5AVF)zas;fc-sgQ~G==SU+vKPdyFHpZY#kd*)#(SexZ- z&+|Yn?e&7yvM#2B)4yqrpVx#LU~Q?l4{TX=>ph)ZE%o+;)wFpo$;F;Wvq<@wh%>?7 zC+MS1AJ0Yg#550_nC60Q!+u{_i~k^4E&dC@YOb;P41x7=zHDP5 zSgrI~1UJUq6P*p_PyK&-Sf^$5F$VVq?WzA9uv+@|RReSpTZZS!FeBT4+PwlJSaZoGAVLR*|Tat1u z*{Z->7kHZnyT5Nsa;)A+?pR$xy;qX#bH?t5f;(Q1bnxGF@W(s&6CM1i4*qNhf4+mi z)WO$g^GbYPv;6nB!;Q~>f4k(P3ce+HtAgvlQwQI<;EvJm1-HHZ3-0eR`|okvo@>MV zv=0)m!yowK|@x*o3P5$(f`rX1}fjYb&l_e>u} zv%LP=-5;)Ey}NIJ3~t-{cs@AaYQ~}c<6yO1pFaUsGkKWVR?eSKBIsY9$De|$=e^vG zV6`OobkC!k(5zp(vAEXMGA2vGYB|qt2CJDo%(6Fs8lwGav|LASX}DUhMV~>_S6lk< zS+MOa#V_OfIk>(a#-@*2%6uMd8L#Up^98tN_*0kBM=fP;1*aYR=w5ppntFcb;fr9k zyRhAikIz$If~&ts9A6>#@VQF;%Oo|=V{zL0D!8+)uc4{uz3c5@HP?sF5vj*KKVR~7 zux-0X=x-Z|>l2|)IOCQvxC>1^V{kWE%`q^p)MK8w?g87j zvFdLdiR)Wn?Z)+Oau4HDzn7$DT;jy_9dKt{_o1n0yuJ%o%Xp<8^ThQ%ux%Tw{Ter~>&Ia2 z#`RP3#HIcdlA3Xe6W7ncopJpfO+DlF5Lhkam3qt**Dt`fZLIp+hH*Sho;dXRB}vUV z#9jyOuUtKG{TkdE*Q03ap2v@n%N&L*^ug8ObOS+o04@q0b{C8k&*6|y1dG2R^ z4^A2Pkd*lYx;D!^PA-q_k6`Nw{}WiBJQMsG%%3_oZBLT&vyXoPTV5aQeu7-hy4+X) z3ig^G{xp0WcVD?L7nLPu-sOzmu%{DROOz^B-Wf@P9Tu_b|`F^-<3~=fA+N z0d2Xj`8Sw9_0I-tx2HiV72fU!M2@o_zzegb<00bt``6Qf*qU0_CN5;B=z{b z3btIvf#p=k<+WJbYowH44%;hmeNujTaM?ze2`WtwK`soDrw})xfqNz6QMP)0*(r zN$QDlEwC|q?pR)*#Hh`EJ8hX;xAzz8gUfI18^F~}9@c3+-q&f%d%X?8n`2YYd~6I> z->A?w0jqCJMb_i}qFVa4DOfG*a5J!Nu1eBo8Sf$0Q)V={EVDV>GQMZjW|>js>M64| zSS@@Tu=R#-+wgoB-VUx7`y0WQ&%Mm{VC%G=Xgh!{r?1);Z8Sh2DH_1c$K8@Ynwp^=Yz}9gaae7a) zA6RW)lIv5xC&{|3TWneTbO6bIq^|j3>oTrE^7`K;qF(RM7r@o@(cgRXA+Wml!19IK zNb34{AGQc=e{(;1HdsG(+ws0%P5;H@ZzG-CaQED|gKuC_ttDSCVpVdLP(plQ!=uMF3yX6k;ir&*m2TkzvQu94|d$N*+;oH z&w-DC9XoCIS3ZWb+%@-6u*lq%AGZyDX9^1`ewOqq)0jqtQq`&i~7XQzH zT}$Dgh3k`h?9YMqQMWH&Aos8@+CEQGvoGS*a~rs<_lt1r&3W-9us-T3^JTEHhkpfJ zmia1NAN8DbUjtj0Hv8u~RI?7}`0HTDH0Ss?;A)PkKCZ2A!n3wqr}EhD0%vWxX63Qn z1J2rV{mPBOwQD8;&LC`hx$GCAY~uhv%q^b z*u7+LlIvv*dG@ROsW1DLK0of@Kkwkb>fpcW;J!hhQEl>ZrAA9c$czncE8v4_BpNBA$`j!B-GehJq{J@>#5gPmKi)7qU|>vk-( zC6-6PYT>_bcw&1Lu8+Fq9Xqx7KL$2lZH}Wnw%>vE4}To2Z_eZ2gVkJfY3~nk%W2EC z`3bPPezxQMsHNUNg4I&TpTHZD)Z_Ccxb*omTtD^HsTTjgfbCECQ(((yeE$m0_-gZ+ zH2ccaaLeoCdY6~${Q&ymu`kK>?wZ}N!25$;;|JDy>ze!wb(CxTpB?&=+{1Fn`awSKkuKL=I||5w8^#{Y)vqn>`h09JpVLo%~Rn&``Xw7k1O!_2D=s}kevTXQD;Qn8Z zWqJQE$C7W@aL2_xXa)FK^6-&xRJU{*1am+@krgP z!Szv3-K&FBxAxTSxM<6Kt_8L|=iV`q$F>f50_|vXeB}8Kx-QuB)H2%iag1Af>w3EZ z-1f9Ne)8145qLsVw__YzbRNBb=RIeYtQ*Q zjO1}JDSONz4fdY;P?BrxaB|zQ%xLOR58u4u`Bt(8+;cF=bJKELqHEJX@oojSy_~;W z!!4(t`-^SB&Y^2wyK|^d=6Ku2ZX5dgzRPD|`F3#I)yH#P?Tujjpv^InXG~n%N0K~_ zAmx0V40arjBG2=V|8Hq|F6`35{r^j2A4xg?|5ACzZ6~<(`X0x)c81$e&u4AUw{578 zBX;}0D_CtBjv2$<;Og<&z42L!e|+{pQ_q;~3AQbNR?70)ZO=YvOI>?|ZO?B+scRp& zdgfvh9bB#Di8_9pe*m?85bu3u@X>j5j z2X|cUtM$tDOTFX4wv~D(fYrS2r;dqm>#&d3A=l43oUciZ-L)dlT5-;fBY7N6%33+5 z!H&nVB-hOp^6b%vQit`*T^C0b+;i@zhNlmQ!L29f^x<%|TtBT(O@CvW3^smpW_`HH!exQS&)xj4O-1-(4-16rXT>IM#uKm0YenG+Yzog*$zrTZD z+wiQRx4<3G@KfQAspq0=>@>JO>RD$~!C7b8T}O_ydg_@DcK&j$m;rY`PjXMO{yuc; z@|kiP*mC+>r*ow}b^4QJ*8-q6IOs>uSWDeN1q0RY|`+H~F=7N3Rv_8ve z*UxpKE%na_r)}q09@`+;F|?fXEst#o?09N(?&XPV5jb(V4&-^xKO5}*qHS5HW%aQh z*PQmWu^4RM!`})vp4=~;3)e?I*M+x%9rtZX+H=3;defG=&Ig}PvR&7n-1`>iZVA}? z3d?BI$Mvh8KE4C&ScYE+Hva5!7lHLrPn#EmttZ#^cY@XQwGG$2TKq2sJ2&C)f|vcj z46cuQ`u}cl`mf#oC#LtnSHq}J>bx9Ww((xL^O`Zf0Ejfwd2$VyCZy}|C2y@fsv>~&-Yd9LL*(q^tB)_H3OzrBNhzv1@D zxw;8%9C;smGkk9r$5QgSbp|2FXH_$THs!i{;caihoYOX%9dzYMmnwDT3PnlWare-&;yZSngW z*fzBpk7KNsdcO`<8^=0x4!!~IB`;&S15G_X-)wx!``SCv)Z=qk<1?+8hr7|#b1vTl z#$4~0me(HJw+fr>>l@p>h0XZ&wQsH$ZRy*$!D<=n?|@%#tnWkDmNEM-SnX+IOPk+= zCsym#Cv9p=n?C@%j>hL%8L<7CTqgsgGq{&)QS=ez5U|KLFMzaXkpuM?Llb2yFe% zop$#_+evKNt@AyC=xYJz+j&J^D!56J{28R)J?1xWB>1 zct(NGEbyELyKl@b^mzpy0K12rMV@=nhlnwIh-H7#!5{A6k96=y3vPUmb@0avZvB7i z;LmpO|8($|3U2-X?clE#-1?XAZuKW~_Dk%JvGZx))E;=2WxYG8H2-s>GYimol^#nWKxQjhjGu)6m&xgP%=o@=pruE)=y>#yCuxQ5g+F8=^KcH#d7>*IOv zxIYKhM_nK1>0e;SB=P(kte<*(o(Fr5<(l>a+`U0PM2iG>iDvkEeCgwvRA%_uRfObJkg%Gy1y?7o@4tPR&sJwEGz z-B07QE?htL_^bzZ+~c!8Tt9W^#Jx%_@ooTC3*QjzdP@8o!Szx1`XVo{FXs}E#~>-! zmjwkLYOwq0!U8WU@YxM^FFmKw7Z>=gV6P)@BhPi@4a8DjM>Z|E?TzZ-n-|=+w(8*f zcJPS}&)94Rw?B@fZOO&9^E7(SqtS5BBYm{#W31|#r!ByaLC&Ks;hpD^K9)6Z?TKS+ zuwxv)4cz*39&HQGd8Dm8kG6w5-dP`SgzK*!ZF{hKc^>Tmf4%c)M|5pDk9Gp9mFLmU zaP_3bxeHp(Biqz3ecBc5TF5%w4X(d>&ZFJIj)k_2>mFd&P5QDYTtD^r>;-l`#b(5^E>ez2G>tL zK8J%{^SQPi0r%Rbo^q4Hmepo#o`Y(M?MSd=AAS^ApRALk!TPAXU&+h;>SEgScsnWk z)p-qe&p5xpOA35JgWWgYQRo*I_#&`-)H})ZynGz(mwVI+9lXDTFKBqiYzlk{^<|$p z9`3p+uebU*R<@-*V{;dA1m@?1RyuAY?Z=$p`9@BDu=y0+ALDp<{b zt2fut)4=LUIsd1k*%z;)wyR%aKOO8iXI^{Z`m1LhP6OLVZRz85uyd02Gy|@mdVKo8 z7ZpA;;rgk^XBOD;Og*!~`l&nC&ZAn+|1-dy=l_{#>N)@CfYnSM_Psp+=c2#f`9BX` zTVfdiTbFvY`C#>*Q)kBgEcol4|AXlIYqu}1LA8wi5IF0``?7^_eKPKg!1}0Xyv_!D z?j)Xb;QFb@XEAsQJ~{v23ite1Pq}l!mepo#o)2n??QLMkKK$)qeX>r@1M8#i`7h7; z?>+csB#%o-IsY$hu;>4~NS?#*Cifin8{P%fk#qP8ChdxXyB}QFaO2h2pHWPo-vPJI zoJSYJ^Ye#G(c^a!y0-j&(8XZOs=r9Rmymn-GlJUQNxGC&?>FRW`x$)vSu_2$>613z z1ukQ`44znSp`@q`duFdy@oxl97^9SH+9?`CDG>W&i4>p?TUw(%A8Z^u6 zuidqI75DKzr(O%UZGG&YYe3C7w0{V!mUI1ju$sxkwp-_V>z&<)(e*Fid42@0p7Zyk zV6~(?n|=(<`n4O2>rX9XasyZ`KiB?ou$sxkSUlfc)1N@o?)__iN8ytVSM$8m?^9^{ zYID!?T)7c!JFer5^-XYnJ&a8swUk*3wv6j7Wp0LB#<(n_k6Oxn8k~0Qqv!E0XzE#` zp8>1+?jgS;_gT2QYxeWx9^|y`0^(C-&7h;aKvQ zACM;weZEgpGY+xW9_y8>8^dBYfcwGclFQw{9)KHX&eaFOY9x%!wVC}p* zX#bAMui*AqUAtqVR$hJ8yplI~U$7Yk#V+{jIUNp7eW~l=}Y;{yj+_uOHU+4A?$ub57)$ z6R%}glRT~>IVaxZysyCTZ?JRq0h06cLGrwpd5QX*pUl@Q9lX0|nce%Vkp;KT6+8H9 z9emvmzFr64u;9kNNeAD$;I_AI2j8yX#jkifd#ASNNovlY*tPIt!_}?V zHT5#sIS>CY-1Xpk)&4)YKI)!d&c9mxUj?h>_cvbyyOzh3v{}YAqMp7`b*-h2<>1zl zd!yyy`XuGvs0&SB?TJM#_4a^mE9cb;aAVoZs=y;imaErjgI%BdHhZ?`&q{FZ*6F^y zGFYGN%d3FZa=xt!wv4)UyS~-Zx7ES+&3)YOBx}H(-&rJmJSW#gQ_ub7T3|JkhkYQ# zdW@Zuwb9ikk@U^?n{~jpr`=dQx74z>)(2;8mG`C_plh>?=bJoZx)IoVw0Z8y+dtC_ zc8|A=HhnyQHzuXOZvfk7`CVvJH1+g%Gq9S;BmGU>qtLBmDHotJ?$PMl@_e~D*t*oC zZ2?v#c#v1nq6T1)P`EDn$=dHQF&$|oxt|Sldv36_h=2@q^gRLWc53uX@HrCw>l-m=mkGgw= zd}oq%+m<+G_Xb;6*8DzTwXBITV6P3iUhWImM?F6Kfjt+}_x-{8soP)oSvCD#(+7gx zcft>Xd)}nJv2e>|UdMs;Q8y0j7!S4{ZMNawt>#*E@0|!%E9YnuTs?cn!C=>)*J5qn zZ|8Ty4}oh>pAG}79ZJgncR1Mc?(N!*V*gn^nzV` z@tFqJPu*BgAy?DiahMKPOZgdK&+(M+1M8!nGBd&M30Zryz-s#1hBmd-IU8)>vX=V6 zYO$XIHs-8>Gr{_(XKl>^XKiUu*|}ijP1$*1HT{i6n_Bz_z-qadm=9L-zSH$H2=;LO zXgiCf=K2x42kp{ub$Qm8xqR2gZf?6?ONYQal9q5j$`^vwe3xXL+SDw+6S?i#j^&-F wMPSD@{Wu$JKk}S%4p<+LXp0-oaf|lWM)R7Q=bv-YEU&+I>-L^W-TUYN0|&wQm;e9( diff --git a/assets/shaders/vulkan/terrain.vert b/assets/shaders/vulkan/terrain.vert index 9f5d9405..54d42ec2 100644 --- a/assets/shaders/vulkan/terrain.vert +++ b/assets/shaders/vulkan/terrain.vert @@ -39,6 +39,8 @@ layout(set = 0, binding = 0) uniform GlobalUniforms { vec4 pbr_params; // x = pbr_quality, y = exposure, z = saturation, w = ssao_strength vec4 volumetric_params; // x = enabled, y = density, z = steps, w = scattering vec4 viewport_size; // xy = width/height + vec4 lpv_params; // x = enabled, y = intensity, z = cell_size, w = grid_size + vec4 lpv_origin; // xyz = world origin } global; layout(push_constant) uniform ModelUniforms { diff --git a/assets/shaders/vulkan/terrain.vert.spv b/assets/shaders/vulkan/terrain.vert.spv index b08cac531d7200f855a314dd446ad5c1d94c8afb..dc977470a569e6dfd94f8c151193f62f9a3267fe 100644 GIT binary patch delta 117 zcmcbizr$d|4JJJ{1~vvc1_lORAkHZ$i!VqlO3W>00E$Ay`Jm$YMVaZDd7G~>#j&V* rFtEb)C~EPE1t-gMSx$D~;@BL))xZk?Z9o;L delta 28 kcmdmCa6^B?4W`W;%rPvRd)QwwG4f9?=d#?qgR6lT0HBo$DF6Tf diff --git a/build.zig b/build.zig index 63401c76..d1a85430 100644 --- a/build.zig +++ b/build.zig @@ -52,7 +52,7 @@ pub fn build(b: *std.Build) void { b.installArtifact(exe); - const shader_cmd = b.addSystemCommand(&.{ "sh", "-c", "for f in assets/shaders/vulkan/*.vert assets/shaders/vulkan/*.frag; do glslangValidator -V \"$f\" -o \"$f.spv\"; done" }); + const shader_cmd = b.addSystemCommand(&.{ "sh", "-c", "for f in assets/shaders/vulkan/*.vert assets/shaders/vulkan/*.frag assets/shaders/vulkan/*.comp; do glslangValidator -V \"$f\" -o \"$f.spv\"; done" }); const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); @@ -176,6 +176,8 @@ pub fn build(b: *std.Build) void { const validate_vulkan_ssao_frag = b.addSystemCommand(&.{ "glslangValidator", "-V", "assets/shaders/vulkan/ssao.frag" }); const validate_vulkan_ssao_blur_frag = b.addSystemCommand(&.{ "glslangValidator", "-V", "assets/shaders/vulkan/ssao_blur.frag" }); const validate_vulkan_g_pass_frag = b.addSystemCommand(&.{ "glslangValidator", "-V", "assets/shaders/vulkan/g_pass.frag" }); + const validate_vulkan_lpv_inject_comp = b.addSystemCommand(&.{ "glslangValidator", "-V", "assets/shaders/vulkan/lpv_inject.comp" }); + const validate_vulkan_lpv_propagate_comp = b.addSystemCommand(&.{ "glslangValidator", "-V", "assets/shaders/vulkan/lpv_propagate.comp" }); test_step.dependOn(&validate_vulkan_terrain_vert.step); test_step.dependOn(&validate_vulkan_terrain_frag.step); @@ -195,4 +197,6 @@ pub fn build(b: *std.Build) void { test_step.dependOn(&validate_vulkan_ssao_frag.step); test_step.dependOn(&validate_vulkan_ssao_blur_frag.step); test_step.dependOn(&validate_vulkan_g_pass_frag.step); + test_step.dependOn(&validate_vulkan_lpv_inject_comp.step); + test_step.dependOn(&validate_vulkan_lpv_propagate_comp.step); } diff --git a/src/engine/graphics/lpv_system.zig b/src/engine/graphics/lpv_system.zig new file mode 100644 index 00000000..bf71b84c --- /dev/null +++ b/src/engine/graphics/lpv_system.zig @@ -0,0 +1,805 @@ +const std = @import("std"); +const c = @import("../../c.zig").c; +const rhi_pkg = @import("rhi.zig"); +const Vec3 = @import("../math/vec3.zig").Vec3; +const World = @import("../../world/world.zig").World; +const CHUNK_SIZE_X = @import("../../world/chunk.zig").CHUNK_SIZE_X; +const CHUNK_SIZE_Y = @import("../../world/chunk.zig").CHUNK_SIZE_Y; +const CHUNK_SIZE_Z = @import("../../world/chunk.zig").CHUNK_SIZE_Z; +const block_registry = @import("../../world/block_registry.zig"); +const VulkanContext = @import("vulkan/rhi_context_types.zig").VulkanContext; +const Utils = @import("vulkan/utils.zig"); + +const MAX_LIGHTS_PER_UPDATE: usize = 2048; + +const GpuLight = extern struct { + pos_radius: [4]f32, + color: [4]f32, +}; + +const InjectPush = extern struct { + grid_origin: [4]f32, + grid_params: [4]f32, + light_count: u32, + _pad0: [3]u32, +}; + +const PropagatePush = extern struct { + grid_size: u32, + _pad0: [3]u32, + propagation_factor: f32, + _pad1: [3]f32, +}; + +pub const LPVSystem = struct { + pub const Stats = struct { + updated_this_frame: bool = false, + light_count: u32 = 0, + cpu_update_ms: f32 = 0.0, + grid_size: u32 = 0, + propagation_iterations: u32 = 0, + update_interval_frames: u32 = 0, + }; + + allocator: std.mem.Allocator, + rhi: rhi_pkg.RHI, + vk_ctx: *VulkanContext, + + grid_texture_a: rhi_pkg.TextureHandle = 0, + grid_texture_b: rhi_pkg.TextureHandle = 0, + active_grid_texture: rhi_pkg.TextureHandle = 0, + debug_overlay_texture: rhi_pkg.TextureHandle = 0, + grid_size: u32, + cell_size: f32, + intensity: f32, + propagation_iterations: u32, + enabled: bool, + update_interval_frames: u32 = 6, + + origin: Vec3 = Vec3.zero, + current_frame: u32 = 0, + was_enabled_last_frame: bool = true, + + image_layout_a: c.VkImageLayout = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + image_layout_b: c.VkImageLayout = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + + debug_overlay_pixels: []f32, + stats: Stats, + + light_buffer: Utils.VulkanBuffer = .{}, + + descriptor_pool: c.VkDescriptorPool = null, + inject_set_layout: c.VkDescriptorSetLayout = null, + propagate_set_layout: c.VkDescriptorSetLayout = null, + inject_descriptor_set: c.VkDescriptorSet = null, + propagate_ab_descriptor_set: c.VkDescriptorSet = null, + propagate_ba_descriptor_set: c.VkDescriptorSet = null, + inject_pipeline_layout: c.VkPipelineLayout = null, + propagate_pipeline_layout: c.VkPipelineLayout = null, + inject_pipeline: c.VkPipeline = null, + propagate_pipeline: c.VkPipeline = null, + + pub fn init( + allocator: std.mem.Allocator, + rhi: rhi_pkg.RHI, + grid_size: u32, + cell_size: f32, + intensity: f32, + propagation_iterations: u32, + enabled: bool, + ) !*LPVSystem { + const self = try allocator.create(LPVSystem); + errdefer allocator.destroy(self); + + const vk_ctx: *VulkanContext = @ptrCast(@alignCast(rhi.ptr)); + const clamped_grid = std.math.clamp(grid_size, 16, 64); + + self.* = .{ + .allocator = allocator, + .rhi = rhi, + .vk_ctx = vk_ctx, + .grid_size = clamped_grid, + .cell_size = @max(cell_size, 0.5), + .intensity = std.math.clamp(intensity, 0.0, 4.0), + .propagation_iterations = std.math.clamp(propagation_iterations, 1, 8), + .enabled = enabled, + .was_enabled_last_frame = enabled, + .debug_overlay_pixels = &.{}, + .stats = .{ + .grid_size = clamped_grid, + .propagation_iterations = std.math.clamp(propagation_iterations, 1, 8), + .update_interval_frames = 6, + }, + }; + + try self.createGridTextures(); + errdefer self.destroyGridTextures(); + + const light_buffer_size = MAX_LIGHTS_PER_UPDATE * @sizeOf(GpuLight); + self.light_buffer = try Utils.createVulkanBuffer( + &vk_ctx.vulkan_device, + light_buffer_size, + c.VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, + c.VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | c.VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + ); + errdefer self.destroyLightBuffer(); + + try self.initComputeResources(); + errdefer self.deinitComputeResources(); + + return self; + } + + pub fn deinit(self: *LPVSystem) void { + self.deinitComputeResources(); + self.destroyLightBuffer(); + self.destroyGridTextures(); + self.allocator.destroy(self); + } + + pub fn setSettings(self: *LPVSystem, enabled: bool, intensity: f32, cell_size: f32, propagation_iterations: u32, grid_size: u32, update_interval_frames: u32) !void { + self.enabled = enabled; + self.intensity = std.math.clamp(intensity, 0.0, 4.0); + self.cell_size = @max(cell_size, 0.5); + self.propagation_iterations = std.math.clamp(propagation_iterations, 1, 8); + self.update_interval_frames = std.math.clamp(update_interval_frames, 1, 16); + self.stats.propagation_iterations = self.propagation_iterations; + self.stats.update_interval_frames = self.update_interval_frames; + + const clamped_grid = std.math.clamp(grid_size, 16, 64); + if (clamped_grid == self.grid_size) return; + + self.destroyGridTextures(); + self.grid_size = clamped_grid; + self.stats.grid_size = clamped_grid; + self.origin = Vec3.zero; + try self.createGridTextures(); + try self.updateDescriptorSets(); + } + + pub fn getTextureHandle(self: *const LPVSystem) rhi_pkg.TextureHandle { + return self.active_grid_texture; + } + + pub fn getDebugOverlayTextureHandle(self: *const LPVSystem) rhi_pkg.TextureHandle { + return self.debug_overlay_texture; + } + + pub fn getStats(self: *const LPVSystem) Stats { + return self.stats; + } + + pub fn getOrigin(self: *const LPVSystem) Vec3 { + return self.origin; + } + + pub fn getGridSize(self: *const LPVSystem) u32 { + return self.grid_size; + } + + pub fn getCellSize(self: *const LPVSystem) f32 { + return self.cell_size; + } + + pub fn update(self: *LPVSystem, world: *World, camera_pos: Vec3) !void { + self.current_frame +%= 1; + var timer = std.time.Timer.start() catch unreachable; + self.stats.updated_this_frame = false; + self.stats.grid_size = self.grid_size; + self.stats.propagation_iterations = self.propagation_iterations; + self.stats.update_interval_frames = self.update_interval_frames; + + if (!self.enabled) { + self.active_grid_texture = self.grid_texture_a; + if (self.was_enabled_last_frame) { + self.buildDebugOverlay(&.{}, 0); + try self.uploadDebugOverlay(); + } + self.was_enabled_last_frame = false; + self.stats.light_count = 0; + self.stats.cpu_update_ms = 0.0; + return; + } + + const half_extent = (@as(f32, @floatFromInt(self.grid_size)) * self.cell_size) * 0.5; + const next_origin = Vec3.init( + quantizeToCell(camera_pos.x - half_extent, self.cell_size), + quantizeToCell(camera_pos.y - half_extent, self.cell_size), + quantizeToCell(camera_pos.z - half_extent, self.cell_size), + ); + + const moved = @abs(next_origin.x - self.origin.x) >= self.cell_size or + @abs(next_origin.y - self.origin.y) >= self.cell_size or + @abs(next_origin.z - self.origin.z) >= self.cell_size; + + const tick_update = (self.current_frame % self.update_interval_frames) == 0; + if (!moved and !tick_update and self.was_enabled_last_frame) { + self.stats.cpu_update_ms = 0.0; + return; + } + + self.origin = next_origin; + self.was_enabled_last_frame = true; + + var lights: [MAX_LIGHTS_PER_UPDATE]GpuLight = undefined; + const light_count = self.collectLights(world, lights[0..]); + if (self.light_buffer.mapped_ptr) |ptr| { + const bytes = std.mem.sliceAsBytes(lights[0..light_count]); + @memcpy(@as([*]u8, @ptrCast(ptr))[0..bytes.len], bytes); + } + + self.buildDebugOverlay(lights[0..], light_count); + try self.uploadDebugOverlay(); + + try self.dispatchCompute(light_count); + + const elapsed_ns = timer.read(); + const delta_ms: f32 = @floatCast(@as(f64, @floatFromInt(elapsed_ns)) / @as(f64, std.time.ns_per_ms)); + self.stats.updated_this_frame = true; + self.stats.light_count = @intCast(light_count); + self.stats.cpu_update_ms = delta_ms; + } + + fn collectLights(self: *LPVSystem, world: *World, out: []GpuLight) usize { + const grid_world_size = @as(f32, @floatFromInt(self.grid_size)) * self.cell_size; + const min_x = self.origin.x; + const min_y = self.origin.y; + const min_z = self.origin.z; + const max_x = min_x + grid_world_size; + const max_y = min_y + grid_world_size; + const max_z = min_z + grid_world_size; + + var emitted_lights: usize = 0; + + world.storage.chunks_mutex.lockShared(); + defer world.storage.chunks_mutex.unlockShared(); + + var iter = world.storage.iteratorUnsafe(); + while (iter.next()) |entry| { + const chunk_data = entry.value_ptr.*; + const chunk = &chunk_data.chunk; + + const chunk_min_x = @as(f32, @floatFromInt(chunk.chunk_x * CHUNK_SIZE_X)); + const chunk_min_z = @as(f32, @floatFromInt(chunk.chunk_z * CHUNK_SIZE_Z)); + const chunk_max_x = chunk_min_x + @as(f32, @floatFromInt(CHUNK_SIZE_X)); + const chunk_max_z = chunk_min_z + @as(f32, @floatFromInt(CHUNK_SIZE_Z)); + + if (chunk_max_x < min_x or chunk_min_x > max_x or chunk_max_z < min_z or chunk_min_z > max_z) { + continue; + } + + var y: u32 = 0; + while (y < CHUNK_SIZE_Y) : (y += 1) { + var z: u32 = 0; + while (z < CHUNK_SIZE_Z) : (z += 1) { + var x: u32 = 0; + while (x < CHUNK_SIZE_X) : (x += 1) { + const block = chunk.getBlock(x, y, z); + if (block == .air) continue; + + const def = block_registry.getBlockDefinition(block); + const r_u4 = def.light_emission[0]; + const g_u4 = def.light_emission[1]; + const b_u4 = def.light_emission[2]; + if (r_u4 == 0 and g_u4 == 0 and b_u4 == 0) continue; + + const world_x = chunk_min_x + @as(f32, @floatFromInt(x)) + 0.5; + const world_y = @as(f32, @floatFromInt(y)) + 0.5; + const world_z = chunk_min_z + @as(f32, @floatFromInt(z)) + 0.5; + if (world_x < min_x or world_x >= max_x or world_y < min_y or world_y >= max_y or world_z < min_z or world_z >= max_z) { + continue; + } + + const emission_r = @as(f32, @floatFromInt(r_u4)) / 15.0; + const emission_g = @as(f32, @floatFromInt(g_u4)) / 15.0; + const emission_b = @as(f32, @floatFromInt(b_u4)) / 15.0; + const max_emission = @max(emission_r, @max(emission_g, emission_b)); + const radius_cells = @max(1.0, max_emission * 6.0); + + out[emitted_lights] = .{ + .pos_radius = .{ world_x, world_y, world_z, radius_cells }, + .color = .{ emission_r, emission_g, emission_b, 1.0 }, + }; + + emitted_lights += 1; + if (emitted_lights >= out.len) return emitted_lights; + } + } + } + } + + return emitted_lights; + } + + fn dispatchCompute(self: *LPVSystem, light_count: usize) !void { + const cmd = self.vk_ctx.frames.command_buffers[self.vk_ctx.frames.current_frame]; + if (cmd == null) return; + + const tex_a = self.vk_ctx.resources.textures.get(self.grid_texture_a) orelse return; + const tex_b = self.vk_ctx.resources.textures.get(self.grid_texture_b) orelse return; + + try self.transitionImage(cmd, tex_a.image.?, self.image_layout_a, c.VK_IMAGE_LAYOUT_GENERAL, c.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | c.VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, c.VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, c.VK_ACCESS_SHADER_READ_BIT, c.VK_ACCESS_SHADER_READ_BIT | c.VK_ACCESS_SHADER_WRITE_BIT); + self.image_layout_a = c.VK_IMAGE_LAYOUT_GENERAL; + try self.transitionImage(cmd, tex_b.image.?, self.image_layout_b, c.VK_IMAGE_LAYOUT_GENERAL, c.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | c.VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, c.VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, c.VK_ACCESS_SHADER_READ_BIT, c.VK_ACCESS_SHADER_READ_BIT | c.VK_ACCESS_SHADER_WRITE_BIT); + self.image_layout_b = c.VK_IMAGE_LAYOUT_GENERAL; + + const groups = divCeil(self.grid_size, 4); + + const inject_push = InjectPush{ + .grid_origin = .{ self.origin.x, self.origin.y, self.origin.z, self.cell_size }, + .grid_params = .{ @floatFromInt(self.grid_size), 0.0, 0.0, 0.0 }, + .light_count = @intCast(light_count), + ._pad0 = .{ 0, 0, 0 }, + }; + + c.vkCmdBindPipeline(cmd, c.VK_PIPELINE_BIND_POINT_COMPUTE, self.inject_pipeline); + c.vkCmdBindDescriptorSets(cmd, c.VK_PIPELINE_BIND_POINT_COMPUTE, self.inject_pipeline_layout, 0, 1, &self.inject_descriptor_set, 0, null); + c.vkCmdPushConstants(cmd, self.inject_pipeline_layout, c.VK_SHADER_STAGE_COMPUTE_BIT, 0, @sizeOf(InjectPush), &inject_push); + c.vkCmdDispatch(cmd, groups, groups, groups); + + var mem_barrier = std.mem.zeroes(c.VkMemoryBarrier); + mem_barrier.sType = c.VK_STRUCTURE_TYPE_MEMORY_BARRIER; + mem_barrier.srcAccessMask = c.VK_ACCESS_SHADER_WRITE_BIT; + mem_barrier.dstAccessMask = c.VK_ACCESS_SHADER_READ_BIT | c.VK_ACCESS_SHADER_WRITE_BIT; + c.vkCmdPipelineBarrier(cmd, c.VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, c.VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &mem_barrier, 0, null, 0, null); + + c.vkCmdBindPipeline(cmd, c.VK_PIPELINE_BIND_POINT_COMPUTE, self.propagate_pipeline); + const prop_push = PropagatePush{ + .grid_size = self.grid_size, + ._pad0 = .{ 0, 0, 0 }, + .propagation_factor = 0.14, + ._pad1 = .{ 0, 0, 0 }, + }; + + var use_ab = true; + var i: u32 = 0; + while (i < self.propagation_iterations) : (i += 1) { + const descriptor_set = if (use_ab) self.propagate_ab_descriptor_set else self.propagate_ba_descriptor_set; + c.vkCmdBindDescriptorSets(cmd, c.VK_PIPELINE_BIND_POINT_COMPUTE, self.propagate_pipeline_layout, 0, 1, &descriptor_set, 0, null); + c.vkCmdPushConstants(cmd, self.propagate_pipeline_layout, c.VK_SHADER_STAGE_COMPUTE_BIT, 0, @sizeOf(PropagatePush), &prop_push); + c.vkCmdDispatch(cmd, groups, groups, groups); + + c.vkCmdPipelineBarrier(cmd, c.VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, c.VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &mem_barrier, 0, null, 0, null); + use_ab = !use_ab; + } + + const final_is_a = (self.propagation_iterations % 2) == 0; + const final_tex = if (final_is_a) tex_a else tex_b; + const final_image = final_tex.image.?; + + try self.transitionImage(cmd, final_image, c.VK_IMAGE_LAYOUT_GENERAL, c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, c.VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, c.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, c.VK_ACCESS_SHADER_WRITE_BIT, c.VK_ACCESS_SHADER_READ_BIT); + + if (final_is_a) { + self.image_layout_a = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + self.active_grid_texture = self.grid_texture_a; + } else { + self.image_layout_b = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + self.active_grid_texture = self.grid_texture_b; + } + } + + fn transitionImage( + self: *LPVSystem, + cmd: c.VkCommandBuffer, + image: c.VkImage, + old_layout: c.VkImageLayout, + new_layout: c.VkImageLayout, + src_stage: c.VkPipelineStageFlags, + dst_stage: c.VkPipelineStageFlags, + src_access: c.VkAccessFlags, + dst_access: c.VkAccessFlags, + ) !void { + _ = self; + if (old_layout == new_layout) return; + var barrier = std.mem.zeroes(c.VkImageMemoryBarrier); + barrier.sType = c.VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.oldLayout = old_layout; + barrier.newLayout = new_layout; + barrier.srcQueueFamilyIndex = c.VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = c.VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange.aspectMask = c.VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + barrier.srcAccessMask = src_access; + barrier.dstAccessMask = dst_access; + + c.vkCmdPipelineBarrier(cmd, src_stage, dst_stage, 0, 0, null, 0, null, 1, &barrier); + } + + fn createGridTextures(self: *LPVSystem) !void { + const empty = try self.allocator.alloc(f32, @as(usize, self.grid_size) * @as(usize, self.grid_size) * @as(usize, self.grid_size) * 4); + defer self.allocator.free(empty); + @memset(empty, 0.0); + const bytes = std.mem.sliceAsBytes(empty); + + self.grid_texture_a = try self.rhi.createTexture( + self.grid_size, + self.grid_size * self.grid_size, + .rgba32f, + .{ + .min_filter = .linear, + .mag_filter = .linear, + .wrap_s = .clamp_to_edge, + .wrap_t = .clamp_to_edge, + .generate_mipmaps = false, + .is_render_target = false, + }, + bytes, + ); + + self.grid_texture_b = try self.rhi.createTexture( + self.grid_size, + self.grid_size * self.grid_size, + .rgba32f, + .{ + .min_filter = .linear, + .mag_filter = .linear, + .wrap_s = .clamp_to_edge, + .wrap_t = .clamp_to_edge, + .generate_mipmaps = false, + .is_render_target = false, + }, + bytes, + ); + + const debug_size = @as(usize, self.grid_size) * @as(usize, self.grid_size) * 4; + self.debug_overlay_pixels = try self.allocator.alloc(f32, debug_size); + @memset(self.debug_overlay_pixels, 0.0); + + self.debug_overlay_texture = try self.rhi.createTexture( + self.grid_size, + self.grid_size, + .rgba32f, + .{ + .min_filter = .linear, + .mag_filter = .linear, + .wrap_s = .clamp_to_edge, + .wrap_t = .clamp_to_edge, + .generate_mipmaps = false, + .is_render_target = false, + }, + std.mem.sliceAsBytes(self.debug_overlay_pixels), + ); + + self.buildDebugOverlay(&.{}, 0); + try self.uploadDebugOverlay(); + + self.active_grid_texture = self.grid_texture_a; + self.image_layout_a = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + self.image_layout_b = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + } + + fn destroyGridTextures(self: *LPVSystem) void { + if (self.grid_texture_a != 0) { + self.rhi.destroyTexture(self.grid_texture_a); + self.grid_texture_a = 0; + } + if (self.grid_texture_b != 0) { + self.rhi.destroyTexture(self.grid_texture_b); + self.grid_texture_b = 0; + } + if (self.debug_overlay_texture != 0) { + self.rhi.destroyTexture(self.debug_overlay_texture); + self.debug_overlay_texture = 0; + } + if (self.debug_overlay_pixels.len > 0) { + self.allocator.free(self.debug_overlay_pixels); + self.debug_overlay_pixels = &.{}; + } + self.active_grid_texture = 0; + } + + fn buildDebugOverlay(self: *LPVSystem, lights: []const GpuLight, light_count: usize) void { + const gs = @as(usize, self.grid_size); + var y: usize = 0; + while (y < gs) : (y += 1) { + var x: usize = 0; + while (x < gs) : (x += 1) { + const idx = (y * gs + x) * 4; + const checker: f32 = if (((x / 4) + (y / 4)) % 2 == 0) @as(f32, 1.5) else @as(f32, 2.0); + self.debug_overlay_pixels[idx + 0] = checker; + self.debug_overlay_pixels[idx + 1] = checker; + self.debug_overlay_pixels[idx + 2] = checker; + self.debug_overlay_pixels[idx + 3] = 1.0; + + if (x == 0 or y == 0 or x + 1 == gs or y + 1 == gs) { + self.debug_overlay_pixels[idx + 0] = 4.0; + self.debug_overlay_pixels[idx + 1] = 4.0; + self.debug_overlay_pixels[idx + 2] = 4.0; + } + } + } + + for (lights[0..light_count]) |light| { + const cx: f32 = ((light.pos_radius[0] - self.origin.x) / self.cell_size); + const cz: f32 = ((light.pos_radius[2] - self.origin.z) / self.cell_size); + const radius = @max(light.pos_radius[3], 0.5); + + var ty: usize = 0; + while (ty < gs) : (ty += 1) { + var tx: usize = 0; + while (tx < gs) : (tx += 1) { + const dx = @as(f32, @floatFromInt(tx)) - cx; + const dz = @as(f32, @floatFromInt(ty)) - cz; + const dist = @sqrt(dx * dx + dz * dz); + if (dist > radius) continue; + + const falloff = std.math.pow(f32, 1.0 - (dist / radius), 2.0); + const idx = (ty * gs + tx) * 4; + self.debug_overlay_pixels[idx + 0] += light.color[0] * falloff * 6.0; + self.debug_overlay_pixels[idx + 1] += light.color[1] * falloff * 6.0; + self.debug_overlay_pixels[idx + 2] += light.color[2] * falloff * 6.0; + } + } + } + + for (0..gs * gs) |i| { + const idx = i * 4; + self.debug_overlay_pixels[idx + 0] = toneMap(self.debug_overlay_pixels[idx + 0]); + self.debug_overlay_pixels[idx + 1] = toneMap(self.debug_overlay_pixels[idx + 1]); + self.debug_overlay_pixels[idx + 2] = toneMap(self.debug_overlay_pixels[idx + 2]); + } + } + + fn uploadDebugOverlay(self: *LPVSystem) !void { + if (self.debug_overlay_texture == 0 or self.debug_overlay_pixels.len == 0) return; + try self.rhi.updateTexture(self.debug_overlay_texture, std.mem.sliceAsBytes(self.debug_overlay_pixels)); + } + + fn destroyLightBuffer(self: *LPVSystem) void { + if (self.light_buffer.buffer != null) { + if (self.light_buffer.mapped_ptr != null) { + c.vkUnmapMemory(self.vk_ctx.vulkan_device.vk_device, self.light_buffer.memory); + } + c.vkDestroyBuffer(self.vk_ctx.vulkan_device.vk_device, self.light_buffer.buffer, null); + c.vkFreeMemory(self.vk_ctx.vulkan_device.vk_device, self.light_buffer.memory, null); + self.light_buffer = .{}; + } + } + + fn initComputeResources(self: *LPVSystem) !void { + const vk = self.vk_ctx.vulkan_device.vk_device; + + var pool_sizes = [_]c.VkDescriptorPoolSize{ + .{ .type = c.VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .descriptorCount = 8 }, + .{ .type = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, .descriptorCount = 2 }, + }; + + var pool_info = std.mem.zeroes(c.VkDescriptorPoolCreateInfo); + pool_info.sType = c.VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + pool_info.maxSets = 4; + pool_info.poolSizeCount = pool_sizes.len; + pool_info.pPoolSizes = &pool_sizes; + try Utils.checkVk(c.vkCreateDescriptorPool(vk, &pool_info, null, &self.descriptor_pool)); + + const inject_bindings = [_]c.VkDescriptorSetLayoutBinding{ + .{ .binding = 0, .descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .descriptorCount = 1, .stageFlags = c.VK_SHADER_STAGE_COMPUTE_BIT, .pImmutableSamplers = null }, + .{ .binding = 1, .descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, .descriptorCount = 1, .stageFlags = c.VK_SHADER_STAGE_COMPUTE_BIT, .pImmutableSamplers = null }, + }; + var inject_layout_info = std.mem.zeroes(c.VkDescriptorSetLayoutCreateInfo); + inject_layout_info.sType = c.VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + inject_layout_info.bindingCount = inject_bindings.len; + inject_layout_info.pBindings = &inject_bindings; + try Utils.checkVk(c.vkCreateDescriptorSetLayout(vk, &inject_layout_info, null, &self.inject_set_layout)); + + const prop_bindings = [_]c.VkDescriptorSetLayoutBinding{ + .{ .binding = 0, .descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .descriptorCount = 1, .stageFlags = c.VK_SHADER_STAGE_COMPUTE_BIT, .pImmutableSamplers = null }, + .{ .binding = 1, .descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .descriptorCount = 1, .stageFlags = c.VK_SHADER_STAGE_COMPUTE_BIT, .pImmutableSamplers = null }, + }; + var prop_layout_info = std.mem.zeroes(c.VkDescriptorSetLayoutCreateInfo); + prop_layout_info.sType = c.VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + prop_layout_info.bindingCount = prop_bindings.len; + prop_layout_info.pBindings = &prop_bindings; + try Utils.checkVk(c.vkCreateDescriptorSetLayout(vk, &prop_layout_info, null, &self.propagate_set_layout)); + + try self.allocateDescriptorSets(); + try self.updateDescriptorSets(); + + try self.createComputePipelines(); + } + + fn allocateDescriptorSets(self: *LPVSystem) !void { + const vk = self.vk_ctx.vulkan_device.vk_device; + + var inject_alloc = std.mem.zeroes(c.VkDescriptorSetAllocateInfo); + inject_alloc.sType = c.VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + inject_alloc.descriptorPool = self.descriptor_pool; + inject_alloc.descriptorSetCount = 1; + inject_alloc.pSetLayouts = &self.inject_set_layout; + try Utils.checkVk(c.vkAllocateDescriptorSets(vk, &inject_alloc, &self.inject_descriptor_set)); + + const layouts = [_]c.VkDescriptorSetLayout{ self.propagate_set_layout, self.propagate_set_layout }; + var prop_alloc = std.mem.zeroes(c.VkDescriptorSetAllocateInfo); + prop_alloc.sType = c.VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + prop_alloc.descriptorPool = self.descriptor_pool; + prop_alloc.descriptorSetCount = 2; + prop_alloc.pSetLayouts = &layouts; + var prop_sets: [2]c.VkDescriptorSet = .{ null, null }; + try Utils.checkVk(c.vkAllocateDescriptorSets(vk, &prop_alloc, &prop_sets)); + self.propagate_ab_descriptor_set = prop_sets[0]; + self.propagate_ba_descriptor_set = prop_sets[1]; + } + + fn updateDescriptorSets(self: *LPVSystem) !void { + const vk = self.vk_ctx.vulkan_device.vk_device; + _ = vk; + + const tex_a = self.vk_ctx.resources.textures.get(self.grid_texture_a) orelse return error.ResourceNotFound; + const tex_b = self.vk_ctx.resources.textures.get(self.grid_texture_b) orelse return error.ResourceNotFound; + + var img_a = c.VkDescriptorImageInfo{ .sampler = null, .imageView = tex_a.view, .imageLayout = c.VK_IMAGE_LAYOUT_GENERAL }; + var img_b = c.VkDescriptorImageInfo{ .sampler = null, .imageView = tex_b.view, .imageLayout = c.VK_IMAGE_LAYOUT_GENERAL }; + var light_info = c.VkDescriptorBufferInfo{ .buffer = self.light_buffer.buffer, .offset = 0, .range = @sizeOf(GpuLight) * MAX_LIGHTS_PER_UPDATE }; + + var writes: [6]c.VkWriteDescriptorSet = undefined; + var n: usize = 0; + + writes[n] = std.mem.zeroes(c.VkWriteDescriptorSet); + writes[n].sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[n].dstSet = self.inject_descriptor_set; + writes[n].dstBinding = 0; + writes[n].descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + writes[n].descriptorCount = 1; + writes[n].pImageInfo = &img_a; + n += 1; + + writes[n] = std.mem.zeroes(c.VkWriteDescriptorSet); + writes[n].sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[n].dstSet = self.inject_descriptor_set; + writes[n].dstBinding = 1; + writes[n].descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + writes[n].descriptorCount = 1; + writes[n].pBufferInfo = &light_info; + n += 1; + + writes[n] = std.mem.zeroes(c.VkWriteDescriptorSet); + writes[n].sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[n].dstSet = self.propagate_ab_descriptor_set; + writes[n].dstBinding = 0; + writes[n].descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + writes[n].descriptorCount = 1; + writes[n].pImageInfo = &img_a; + n += 1; + + writes[n] = std.mem.zeroes(c.VkWriteDescriptorSet); + writes[n].sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[n].dstSet = self.propagate_ab_descriptor_set; + writes[n].dstBinding = 1; + writes[n].descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + writes[n].descriptorCount = 1; + writes[n].pImageInfo = &img_b; + n += 1; + + writes[n] = std.mem.zeroes(c.VkWriteDescriptorSet); + writes[n].sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[n].dstSet = self.propagate_ba_descriptor_set; + writes[n].dstBinding = 0; + writes[n].descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + writes[n].descriptorCount = 1; + writes[n].pImageInfo = &img_b; + n += 1; + + writes[n] = std.mem.zeroes(c.VkWriteDescriptorSet); + writes[n].sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[n].dstSet = self.propagate_ba_descriptor_set; + writes[n].dstBinding = 1; + writes[n].descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + writes[n].descriptorCount = 1; + writes[n].pImageInfo = &img_a; + n += 1; + + c.vkUpdateDescriptorSets(self.vk_ctx.vulkan_device.vk_device, @intCast(n), &writes[0], 0, null); + } + + fn createComputePipelines(self: *LPVSystem) !void { + const vk = self.vk_ctx.vulkan_device.vk_device; + + const inject_module = try createShaderModule(vk, "assets/shaders/vulkan/lpv_inject.comp.spv", self.allocator); + defer c.vkDestroyShaderModule(vk, inject_module, null); + const propagate_module = try createShaderModule(vk, "assets/shaders/vulkan/lpv_propagate.comp.spv", self.allocator); + defer c.vkDestroyShaderModule(vk, propagate_module, null); + + var inject_pc = std.mem.zeroes(c.VkPushConstantRange); + inject_pc.stageFlags = c.VK_SHADER_STAGE_COMPUTE_BIT; + inject_pc.offset = 0; + inject_pc.size = @sizeOf(InjectPush); + + var inject_layout_info = std.mem.zeroes(c.VkPipelineLayoutCreateInfo); + inject_layout_info.sType = c.VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + inject_layout_info.setLayoutCount = 1; + inject_layout_info.pSetLayouts = &self.inject_set_layout; + inject_layout_info.pushConstantRangeCount = 1; + inject_layout_info.pPushConstantRanges = &inject_pc; + try Utils.checkVk(c.vkCreatePipelineLayout(vk, &inject_layout_info, null, &self.inject_pipeline_layout)); + + var prop_pc = std.mem.zeroes(c.VkPushConstantRange); + prop_pc.stageFlags = c.VK_SHADER_STAGE_COMPUTE_BIT; + prop_pc.offset = 0; + prop_pc.size = @sizeOf(PropagatePush); + + var prop_layout_info = std.mem.zeroes(c.VkPipelineLayoutCreateInfo); + prop_layout_info.sType = c.VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + prop_layout_info.setLayoutCount = 1; + prop_layout_info.pSetLayouts = &self.propagate_set_layout; + prop_layout_info.pushConstantRangeCount = 1; + prop_layout_info.pPushConstantRanges = &prop_pc; + try Utils.checkVk(c.vkCreatePipelineLayout(vk, &prop_layout_info, null, &self.propagate_pipeline_layout)); + + var inject_stage = std.mem.zeroes(c.VkPipelineShaderStageCreateInfo); + inject_stage.sType = c.VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + inject_stage.stage = c.VK_SHADER_STAGE_COMPUTE_BIT; + inject_stage.module = inject_module; + inject_stage.pName = "main"; + + var inject_info = std.mem.zeroes(c.VkComputePipelineCreateInfo); + inject_info.sType = c.VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; + inject_info.stage = inject_stage; + inject_info.layout = self.inject_pipeline_layout; + try Utils.checkVk(c.vkCreateComputePipelines(vk, null, 1, &inject_info, null, &self.inject_pipeline)); + + var prop_stage = std.mem.zeroes(c.VkPipelineShaderStageCreateInfo); + prop_stage.sType = c.VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + prop_stage.stage = c.VK_SHADER_STAGE_COMPUTE_BIT; + prop_stage.module = propagate_module; + prop_stage.pName = "main"; + + var prop_info = std.mem.zeroes(c.VkComputePipelineCreateInfo); + prop_info.sType = c.VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; + prop_info.stage = prop_stage; + prop_info.layout = self.propagate_pipeline_layout; + try Utils.checkVk(c.vkCreateComputePipelines(vk, null, 1, &prop_info, null, &self.propagate_pipeline)); + } + + fn deinitComputeResources(self: *LPVSystem) void { + const vk = self.vk_ctx.vulkan_device.vk_device; + if (self.inject_pipeline != null) c.vkDestroyPipeline(vk, self.inject_pipeline, null); + if (self.propagate_pipeline != null) c.vkDestroyPipeline(vk, self.propagate_pipeline, null); + if (self.inject_pipeline_layout != null) c.vkDestroyPipelineLayout(vk, self.inject_pipeline_layout, null); + if (self.propagate_pipeline_layout != null) c.vkDestroyPipelineLayout(vk, self.propagate_pipeline_layout, null); + if (self.inject_set_layout != null) c.vkDestroyDescriptorSetLayout(vk, self.inject_set_layout, null); + if (self.propagate_set_layout != null) c.vkDestroyDescriptorSetLayout(vk, self.propagate_set_layout, null); + if (self.descriptor_pool != null) c.vkDestroyDescriptorPool(vk, self.descriptor_pool, null); + + self.inject_pipeline = null; + self.propagate_pipeline = null; + self.inject_pipeline_layout = null; + self.propagate_pipeline_layout = null; + self.inject_set_layout = null; + self.propagate_set_layout = null; + self.descriptor_pool = null; + self.inject_descriptor_set = null; + self.propagate_ab_descriptor_set = null; + self.propagate_ba_descriptor_set = null; + } +}; + +fn quantizeToCell(value: f32, cell_size: f32) f32 { + return @floor(value / cell_size) * cell_size; +} + +fn divCeil(v: u32, d: u32) u32 { + return @divFloor(v + d - 1, d); +} + +fn toneMap(v: f32) f32 { + const x = @max(v, 0.0); + return x / (1.0 + x); +} + +fn createShaderModule(vk: c.VkDevice, path: []const u8, allocator: std.mem.Allocator) !c.VkShaderModule { + const bytes = try std.fs.cwd().readFileAlloc(path, allocator, @enumFromInt(16 * 1024 * 1024)); + defer allocator.free(bytes); + if (bytes.len % 4 != 0) return error.InvalidState; + + var info = std.mem.zeroes(c.VkShaderModuleCreateInfo); + info.sType = c.VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + info.codeSize = bytes.len; + info.pCode = @ptrCast(@alignCast(bytes.ptr)); + + var module: c.VkShaderModule = null; + try Utils.checkVk(c.vkCreateShaderModule(vk, &info, null, &module)); + return module; +} diff --git a/src/engine/graphics/render_graph.zig b/src/engine/graphics/render_graph.zig index 42a3c66e..9b0cfd55 100644 --- a/src/engine/graphics/render_graph.zig +++ b/src/engine/graphics/render_graph.zig @@ -35,6 +35,7 @@ pub const SceneContext = struct { bloom_enabled: bool = true, overlay_renderer: ?*const fn (ctx: SceneContext) void = null, overlay_ctx: ?*anyopaque = null, + lpv_texture_handle: rhi_pkg.TextureHandle = 0, // Pointer to frame-local cascade storage, computed once per frame by the first // ShadowPass and reused by subsequent cascade passes to guarantee consistency. cached_cascades: *?CSM.ShadowCascades, @@ -282,6 +283,7 @@ pub const OpaquePass = struct { const rhi = ctx.rhi; rhi.bindShader(ctx.main_shader); ctx.material_system.bindTerrainMaterial(ctx.env_map_handle); + rhi.bindTexture(ctx.lpv_texture_handle, 11); const view_proj = Mat4.perspectiveReverseZ(ctx.camera.fov, ctx.aspect, ctx.camera.near, ctx.camera.far).multiply(ctx.camera.getViewMatrixOriginCentered()); ctx.world.render(view_proj, ctx.camera.position, true); } diff --git a/src/engine/graphics/rhi.zig b/src/engine/graphics/rhi.zig index c898545a..66436394 100644 --- a/src/engine/graphics/rhi.zig +++ b/src/engine/graphics/rhi.zig @@ -52,6 +52,7 @@ pub const IResourceFactory = struct { updateBuffer: *const fn (ptr: *anyopaque, handle: BufferHandle, offset: usize, data: []const u8) RhiError!void, destroyBuffer: *const fn (ptr: *anyopaque, handle: BufferHandle) void, createTexture: *const fn (ptr: *anyopaque, width: u32, height: u32, format: TextureFormat, config: TextureConfig, data: ?[]const u8) RhiError!TextureHandle, + createTexture3D: *const fn (ptr: *anyopaque, width: u32, height: u32, depth: u32, format: TextureFormat, config: TextureConfig, data: ?[]const u8) RhiError!TextureHandle, destroyTexture: *const fn (ptr: *anyopaque, handle: TextureHandle) void, updateTexture: *const fn (ptr: *anyopaque, handle: TextureHandle, data: []const u8) RhiError!void, createShader: *const fn (ptr: *anyopaque, vertex_src: [*c]const u8, fragment_src: [*c]const u8) RhiError!ShaderHandle, @@ -75,6 +76,9 @@ pub const IResourceFactory = struct { pub fn createTexture(self: IResourceFactory, width: u32, height: u32, format: TextureFormat, config: TextureConfig, data: ?[]const u8) RhiError!TextureHandle { return self.vtable.createTexture(self.ptr, width, height, format, config, data); } + pub fn createTexture3D(self: IResourceFactory, width: u32, height: u32, depth: u32, format: TextureFormat, config: TextureConfig, data: ?[]const u8) RhiError!TextureHandle { + return self.vtable.createTexture3D(self.ptr, width, height, depth, format, config, data); + } pub fn destroyTexture(self: IResourceFactory, handle: TextureHandle) void { self.vtable.destroyTexture(self.ptr, handle); } diff --git a/src/engine/graphics/rhi_tests.zig b/src/engine/graphics/rhi_tests.zig index 8445317a..961250ed 100644 --- a/src/engine/graphics/rhi_tests.zig +++ b/src/engine/graphics/rhi_tests.zig @@ -203,6 +203,7 @@ const MockContext = struct { .updateBuffer = updateBuffer, .destroyBuffer = destroyBuffer, .createTexture = createTexture, + .createTexture3D = createTexture3D, .destroyTexture = destroyTexture, .updateTexture = updateTexture, .createShader = createShader, @@ -241,6 +242,16 @@ const MockContext = struct { _ = data; return 1; } + fn createTexture3D(ptr: *anyopaque, width: u32, height: u32, depth: u32, format: rhi.TextureFormat, config: rhi.TextureConfig, data: ?[]const u8) rhi.RhiError!rhi.TextureHandle { + _ = ptr; + _ = width; + _ = height; + _ = depth; + _ = format; + _ = config; + _ = data; + return 1; + } fn destroyTexture(ptr: *anyopaque, handle: rhi.TextureHandle) void { _ = ptr; _ = handle; diff --git a/src/engine/graphics/rhi_types.zig b/src/engine/graphics/rhi_types.zig index 6d22e856..3ac517c8 100644 --- a/src/engine/graphics/rhi_types.zig +++ b/src/engine/graphics/rhi_types.zig @@ -199,6 +199,11 @@ pub const CloudParams = struct { exposure: f32 = 0.9, saturation: f32 = 1.3, ssao_enabled: bool = true, + lpv_enabled: bool = true, + lpv_intensity: f32 = 0.5, + lpv_cell_size: f32 = 2.0, + lpv_grid_size: u32 = 32, + lpv_origin: Vec3 = Vec3.init(0.0, 0.0, 0.0), }; pub const Color = struct { @@ -233,6 +238,7 @@ pub const GpuTimingResults = struct { shadow_pass_ms: [SHADOW_CASCADE_COUNT]f32, g_pass_ms: f32, ssao_pass_ms: f32, + lpv_pass_ms: f32, sky_pass_ms: f32, opaque_pass_ms: f32, cloud_pass_ms: f32, diff --git a/src/engine/graphics/rhi_vulkan.zig b/src/engine/graphics/rhi_vulkan.zig index 8a9bba77..8c4503ab 100644 --- a/src/engine/graphics/rhi_vulkan.zig +++ b/src/engine/graphics/rhi_vulkan.zig @@ -15,8 +15,9 @@ const shadow_bridge = @import("vulkan/rhi_shadow_bridge.zig"); const native_access = @import("vulkan/rhi_native_access.zig"); const render_state = @import("vulkan/rhi_render_state.zig"); const init_deinit = @import("vulkan/rhi_init_deinit.zig"); +const rhi_timing = @import("vulkan/rhi_timing.zig"); -const QUERY_COUNT_PER_FRAME = 22; +const QUERY_COUNT_PER_FRAME = rhi_timing.QUERY_COUNT_PER_FRAME; const VulkanContext = @import("vulkan/rhi_context_types.zig").VulkanContext; @@ -315,6 +316,13 @@ fn createTexture(ctx_ptr: *anyopaque, width: u32, height: u32, format: rhi.Textu return ctx.resources.createTexture(width, height, format, config, data_opt); } +fn createTexture3D(ctx_ptr: *anyopaque, width: u32, height: u32, depth: u32, format: rhi.TextureFormat, config: rhi.TextureConfig, data_opt: ?[]const u8) rhi.RhiError!rhi.TextureHandle { + const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); + ctx.mutex.lock(); + defer ctx.mutex.unlock(); + return ctx.resources.createTexture3D(width, height, depth, format, config, data_opt); +} + fn destroyTexture(ctx_ptr: *anyopaque, handle: rhi.TextureHandle) void { const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); ctx.resources.destroyTexture(handle); @@ -338,6 +346,7 @@ 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, + 11 => ctx.draw.current_lpv_texture = resolved, else => ctx.draw.current_texture = resolved, } } @@ -661,6 +670,7 @@ const VULKAN_RHI_VTABLE = rhi.RHI.VTable{ .updateBuffer = updateBuffer, .destroyBuffer = destroyBuffer, .createTexture = createTexture, + .createTexture3D = createTexture3D, .destroyTexture = destroyTexture, .updateTexture = updateTexture, .createShader = createShader, diff --git a/src/engine/graphics/vulkan/descriptor_bindings.zig b/src/engine/graphics/vulkan/descriptor_bindings.zig index 1b8dc231..33e743a4 100644 --- a/src/engine/graphics/vulkan/descriptor_bindings.zig +++ b/src/engine/graphics/vulkan/descriptor_bindings.zig @@ -9,3 +9,4 @@ pub const ROUGHNESS_TEXTURE = 7; pub const DISPLACEMENT_TEXTURE = 8; pub const ENV_TEXTURE = 9; pub const SSAO_TEXTURE = 10; +pub const LPV_TEXTURE = 11; diff --git a/src/engine/graphics/vulkan/descriptor_manager.zig b/src/engine/graphics/vulkan/descriptor_manager.zig index e3b5c648..3ffc1701 100644 --- a/src/engine/graphics/vulkan/descriptor_manager.zig +++ b/src/engine/graphics/vulkan/descriptor_manager.zig @@ -22,6 +22,8 @@ const GlobalUniforms = extern struct { pbr_params: [4]f32, volumetric_params: [4]f32, viewport_size: [4]f32, + lpv_params: [4]f32, + lpv_origin: [4]f32, }; const ShadowUniforms = extern struct { @@ -154,6 +156,8 @@ pub const DescriptorManager = struct { .{ .binding = 9, .descriptorType = c.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = 1, .stageFlags = c.VK_SHADER_STAGE_FRAGMENT_BIT }, // 10: SSAO Map .{ .binding = 10, .descriptorType = c.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = 1, .stageFlags = c.VK_SHADER_STAGE_FRAGMENT_BIT }, + // 11: LPV Grid Atlas + .{ .binding = 11, .descriptorType = c.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = 1, .stageFlags = c.VK_SHADER_STAGE_FRAGMENT_BIT }, }; var layout_info = std.mem.zeroes(c.VkDescriptorSetLayoutCreateInfo); diff --git a/src/engine/graphics/vulkan/resource_manager.zig b/src/engine/graphics/vulkan/resource_manager.zig index 2b70063b..242ba8df 100644 --- a/src/engine/graphics/vulkan/resource_manager.zig +++ b/src/engine/graphics/vulkan/resource_manager.zig @@ -16,8 +16,10 @@ pub const TextureResource = struct { sampler: c.VkSampler, width: u32, height: u32, + depth: u32, format: rhi.TextureFormat, config: rhi.TextureConfig, + is_3d: bool = false, is_owned: bool = true, }; @@ -373,6 +375,10 @@ pub const ResourceManager = struct { return resource_texture_ops.createTexture(self, width, height, format, config, data_opt); } + pub fn createTexture3D(self: *ResourceManager, width: u32, height: u32, depth: u32, format: rhi.TextureFormat, config: rhi.TextureConfig, data_opt: ?[]const u8) rhi.RhiError!rhi.TextureHandle { + return resource_texture_ops.createTexture3D(self, width, height, depth, format, config, data_opt); + } + pub fn destroyTexture(self: *ResourceManager, handle: rhi.TextureHandle) void { const tex = self.textures.get(handle) orelse return; _ = self.textures.remove(handle); @@ -398,8 +404,10 @@ pub const ResourceManager = struct { .sampler = sampler, .width = width, .height = height, + .depth = 1, .format = format, .config = .{}, // Default config + .is_3d = false, .is_owned = false, }); @@ -419,8 +427,10 @@ pub const ResourceManager = struct { .sampler = sampler, .width = width, .height = height, + .depth = 1, .format = format, .config = .{}, + .is_3d = false, .is_owned = false, }); return handle; @@ -459,7 +469,7 @@ pub const ResourceManager = struct { region.bufferOffset = offset; region.imageSubresource.aspectMask = c.VK_IMAGE_ASPECT_COLOR_BIT; region.imageSubresource.layerCount = 1; - region.imageExtent = .{ .width = tex.width, .height = tex.height, .depth = 1 }; + region.imageExtent = .{ .width = tex.width, .height = tex.height, .depth = tex.depth }; c.vkCmdCopyBufferToImage(transfer_cb, staging.buffer, tex.image.?, c.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); diff --git a/src/engine/graphics/vulkan/resource_texture_ops.zig b/src/engine/graphics/vulkan/resource_texture_ops.zig index 1c42fa9c..ca49ad29 100644 --- a/src/engine/graphics/vulkan/resource_texture_ops.zig +++ b/src/engine/graphics/vulkan/resource_texture_ops.zig @@ -30,6 +30,7 @@ pub fn createTexture(self: anytype, width: u32, height: u32, format: rhi.Texture if (mip_levels > 1) usage_flags |= c.VK_IMAGE_USAGE_TRANSFER_SRC_BIT; if (config.is_render_target) usage_flags |= c.VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + if (format == .rgba32f) usage_flags |= c.VK_IMAGE_USAGE_STORAGE_BIT; var staging_offset: u64 = 0; if (data_opt) |data| { @@ -212,8 +213,168 @@ pub fn createTexture(self: anytype, width: u32, height: u32, format: rhi.Texture .sampler = sampler, .width = width, .height = height, + .depth = 1, .format = format, .config = config, + .is_3d = false, + .is_owned = true, + }); + + return handle; +} + +pub fn createTexture3D(self: anytype, width: u32, height: u32, depth: u32, format: rhi.TextureFormat, config: rhi.TextureConfig, data_opt: ?[]const u8) rhi.RhiError!rhi.TextureHandle { + _ = config; + const vk_format: c.VkFormat = switch (format) { + .rgba => c.VK_FORMAT_R8G8B8A8_UNORM, + .rgba_srgb => c.VK_FORMAT_R8G8B8A8_SRGB, + .rgb => c.VK_FORMAT_R8G8B8_UNORM, + .red => c.VK_FORMAT_R8_UNORM, + .depth => c.VK_FORMAT_D32_SFLOAT, + .rgba32f => c.VK_FORMAT_R32G32B32A32_SFLOAT, + }; + + if (format == .depth) return error.FormatNotSupported; + if (depth == 0) return error.InvalidState; + + var usage_flags: c.VkImageUsageFlags = c.VK_IMAGE_USAGE_TRANSFER_DST_BIT | c.VK_IMAGE_USAGE_SAMPLED_BIT; + if (format == .rgba32f) usage_flags |= c.VK_IMAGE_USAGE_STORAGE_BIT; + + var staging_offset: u64 = 0; + if (data_opt) |data| { + const staging = &self.staging_buffers[self.current_frame_index]; + const offset = staging.allocate(data.len) orelse return error.OutOfMemory; + if (staging.mapped_ptr == null) return error.OutOfMemory; + staging_offset = offset; + } + + const device = self.vulkan_device.vk_device; + + var image: c.VkImage = null; + var image_info = std.mem.zeroes(c.VkImageCreateInfo); + image_info.sType = c.VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + image_info.imageType = c.VK_IMAGE_TYPE_3D; + image_info.extent.width = width; + image_info.extent.height = height; + image_info.extent.depth = depth; + image_info.mipLevels = 1; + image_info.arrayLayers = 1; + image_info.format = vk_format; + image_info.tiling = c.VK_IMAGE_TILING_OPTIMAL; + image_info.initialLayout = c.VK_IMAGE_LAYOUT_UNDEFINED; + image_info.usage = usage_flags; + image_info.samples = c.VK_SAMPLE_COUNT_1_BIT; + image_info.sharingMode = c.VK_SHARING_MODE_EXCLUSIVE; + + try Utils.checkVk(c.vkCreateImage(device, &image_info, null, &image)); + errdefer c.vkDestroyImage(device, image, null); + + var mem_reqs: c.VkMemoryRequirements = undefined; + c.vkGetImageMemoryRequirements(device, image, &mem_reqs); + + var memory: c.VkDeviceMemory = null; + var alloc_info = std.mem.zeroes(c.VkMemoryAllocateInfo); + alloc_info.sType = c.VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + alloc_info.allocationSize = mem_reqs.size; + alloc_info.memoryTypeIndex = try Utils.findMemoryType(self.vulkan_device.physical_device, mem_reqs.memoryTypeBits, c.VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + try Utils.checkVk(c.vkAllocateMemory(device, &alloc_info, null, &memory)); + errdefer c.vkFreeMemory(device, memory, null); + try Utils.checkVk(c.vkBindImageMemory(device, image, memory, 0)); + + const transfer_cb = try self.prepareTransfer(); + var barrier = std.mem.zeroes(c.VkImageMemoryBarrier); + barrier.sType = c.VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.oldLayout = c.VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = if (data_opt != null) c.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL else c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier.srcQueueFamilyIndex = c.VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = c.VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange.aspectMask = c.VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + barrier.srcAccessMask = 0; + barrier.dstAccessMask = if (data_opt != null) c.VK_ACCESS_TRANSFER_WRITE_BIT else c.VK_ACCESS_SHADER_READ_BIT; + + c.vkCmdPipelineBarrier( + transfer_cb, + c.VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + if (data_opt != null) c.VK_PIPELINE_STAGE_TRANSFER_BIT else c.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, + 0, + null, + 0, + null, + 1, + &barrier, + ); + + if (data_opt) |data| { + const staging = &self.staging_buffers[self.current_frame_index]; + if (staging.mapped_ptr == null) return error.OutOfMemory; + const dest = @as([*]u8, @ptrCast(staging.mapped_ptr.?)) + staging_offset; + @memcpy(dest[0..data.len], data); + + var region = std.mem.zeroes(c.VkBufferImageCopy); + region.bufferOffset = staging_offset; + region.imageSubresource.aspectMask = c.VK_IMAGE_ASPECT_COLOR_BIT; + region.imageSubresource.layerCount = 1; + region.imageExtent = .{ .width = width, .height = height, .depth = depth }; + c.vkCmdCopyBufferToImage(transfer_cb, staging.buffer, image, c.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + + barrier.oldLayout = c.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier.srcAccessMask = c.VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = c.VK_ACCESS_SHADER_READ_BIT; + c.vkCmdPipelineBarrier(transfer_cb, c.VK_PIPELINE_STAGE_TRANSFER_BIT, c.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, null, 0, null, 1, &barrier); + } + + var view: c.VkImageView = null; + var view_info = std.mem.zeroes(c.VkImageViewCreateInfo); + view_info.sType = c.VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + view_info.image = image; + view_info.viewType = c.VK_IMAGE_VIEW_TYPE_3D; + view_info.format = vk_format; + view_info.subresourceRange.aspectMask = c.VK_IMAGE_ASPECT_COLOR_BIT; + view_info.subresourceRange.baseMipLevel = 0; + view_info.subresourceRange.levelCount = 1; + view_info.subresourceRange.baseArrayLayer = 0; + view_info.subresourceRange.layerCount = 1; + + const sampler = try Utils.createSampler(self.vulkan_device, .{ + .min_filter = .linear, + .mag_filter = .linear, + .wrap_s = .clamp_to_edge, + .wrap_t = .clamp_to_edge, + .generate_mipmaps = false, + .is_render_target = false, + }, 1, self.vulkan_device.max_anisotropy); + errdefer c.vkDestroySampler(device, sampler, null); + + try Utils.checkVk(c.vkCreateImageView(device, &view_info, null, &view)); + errdefer c.vkDestroyImageView(device, view, null); + + const handle = self.next_texture_handle; + self.next_texture_handle += 1; + try self.textures.put(handle, .{ + .image = image, + .memory = memory, + .view = view, + .sampler = sampler, + .width = width, + .height = height, + .depth = depth, + .format = format, + .config = .{ + .min_filter = .linear, + .mag_filter = .linear, + .wrap_s = .clamp_to_edge, + .wrap_t = .clamp_to_edge, + .generate_mipmaps = false, + .is_render_target = false, + }, + .is_3d = true, .is_owned = true, }); diff --git a/src/engine/graphics/vulkan/rhi_context_factory.zig b/src/engine/graphics/vulkan/rhi_context_factory.zig index 4493808f..00b368e9 100644 --- a/src/engine/graphics/vulkan/rhi_context_factory.zig +++ b/src/engine/graphics/vulkan/rhi_context_factory.zig @@ -50,6 +50,7 @@ pub fn createRHI( ctx.draw.current_roughness_texture = 0; ctx.draw.current_displacement_texture = 0; ctx.draw.current_env_texture = 0; + ctx.draw.current_lpv_texture = 0; ctx.draw.dummy_texture = 0; ctx.draw.dummy_normal_texture = 0; ctx.draw.dummy_roughness_texture = 0; @@ -79,6 +80,7 @@ pub fn createRHI( ctx.draw.bound_roughness_texture = 0; ctx.draw.bound_displacement_texture = 0; ctx.draw.bound_env_texture = 0; + ctx.draw.bound_lpv_texture = 0; ctx.draw.current_mask_radius = 0; ctx.draw.lod_mode = false; ctx.draw.pending_instance_buffer = 0; diff --git a/src/engine/graphics/vulkan/rhi_context_types.zig b/src/engine/graphics/vulkan/rhi_context_types.zig index 4a9840fc..5aaee469 100644 --- a/src/engine/graphics/vulkan/rhi_context_types.zig +++ b/src/engine/graphics/vulkan/rhi_context_types.zig @@ -122,6 +122,7 @@ const DrawState = struct { current_roughness_texture: rhi.TextureHandle, current_displacement_texture: rhi.TextureHandle, current_env_texture: rhi.TextureHandle, + current_lpv_texture: rhi.TextureHandle, dummy_texture: rhi.TextureHandle, dummy_normal_texture: rhi.TextureHandle, dummy_roughness_texture: rhi.TextureHandle, @@ -130,6 +131,7 @@ const DrawState = struct { bound_roughness_texture: rhi.TextureHandle, bound_displacement_texture: rhi.TextureHandle, bound_env_texture: rhi.TextureHandle, + bound_lpv_texture: rhi.TextureHandle, bound_ssao_handle: rhi.TextureHandle = 0, bound_shadow_views: [rhi.SHADOW_CASCADE_COUNT]c.VkImageView, descriptors_dirty: [MAX_FRAMES_IN_FLIGHT]bool, diff --git a/src/engine/graphics/vulkan/rhi_frame_orchestration.zig b/src/engine/graphics/vulkan/rhi_frame_orchestration.zig index ad61513c..59458cf3 100644 --- a/src/engine/graphics/vulkan/rhi_frame_orchestration.zig +++ b/src/engine/graphics/vulkan/rhi_frame_orchestration.zig @@ -153,6 +153,7 @@ 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_lpv = ctx.draw.current_lpv_texture; var needs_update = false; if (ctx.draw.bound_texture != cur_tex) needs_update = true; @@ -160,6 +161,7 @@ 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_lpv_texture != cur_lpv) needs_update = true; for (0..rhi.SHADOW_CASCADE_COUNT) |si| { if (ctx.draw.bound_shadow_views[si] != ctx.shadow_system.shadow_image_views[si]) needs_update = true; @@ -172,6 +174,7 @@ 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_lpv_texture = cur_lpv; for (0..rhi.SHADOW_CASCADE_COUNT) |si| ctx.draw.bound_shadow_views[si] = ctx.shadow_system.shadow_image_views[si]; } @@ -180,9 +183,9 @@ pub fn prepareFrameState(ctx: anytype) void { std.log.err("CRITICAL: Descriptor set for frame {} is NULL!", .{ctx.frames.current_frame}); return; } - var writes: [10]c.VkWriteDescriptorSet = undefined; + var writes: [12]c.VkWriteDescriptorSet = undefined; var write_count: u32 = 0; - var image_infos: [10]c.VkDescriptorImageInfo = undefined; + var image_infos: [12]c.VkDescriptorImageInfo = undefined; var info_count: u32 = 0; const dummy_tex_entry = ctx.resources.textures.get(ctx.draw.dummy_texture); @@ -193,6 +196,7 @@ pub fn prepareFrameState(ctx: anytype) void { .{ .handle = cur_rou, .binding = bindings.ROUGHNESS_TEXTURE }, .{ .handle = cur_dis, .binding = bindings.DISPLACEMENT_TEXTURE }, .{ .handle = cur_env, .binding = bindings.ENV_TEXTURE }, + .{ .handle = cur_lpv, .binding = bindings.LPV_TEXTURE }, }; for (atlas_slots) |slot| { diff --git a/src/engine/graphics/vulkan/rhi_init_deinit.zig b/src/engine/graphics/vulkan/rhi_init_deinit.zig index 5a28e7fa..7e6e1511 100644 --- a/src/engine/graphics/vulkan/rhi_init_deinit.zig +++ b/src/engine/graphics/vulkan/rhi_init_deinit.zig @@ -13,9 +13,10 @@ const ShadowSystem = @import("shadow_system.zig").ShadowSystem; const Utils = @import("utils.zig"); const lifecycle = @import("rhi_resource_lifecycle.zig"); const setup = @import("rhi_resource_setup.zig"); +const rhi_timing = @import("rhi_timing.zig"); const MAX_FRAMES_IN_FLIGHT = rhi.MAX_FRAMES_IN_FLIGHT; -const TOTAL_QUERY_COUNT = 22 * MAX_FRAMES_IN_FLIGHT; +const TOTAL_QUERY_COUNT = rhi_timing.QUERY_COUNT_PER_FRAME * MAX_FRAMES_IN_FLIGHT; pub fn initContext(ctx: anytype, allocator: std.mem.Allocator, render_device: ?*RenderDevice) !void { ctx.allocator = allocator; @@ -108,6 +109,7 @@ 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_lpv_texture = ctx.draw.dummy_texture; const cloud_vbo_handle = try ctx.resources.createBuffer(8 * @sizeOf(f32), .vertex); std.log.info("Cloud VBO handle: {}, map count: {}", .{ cloud_vbo_handle, ctx.resources.buffers.count() }); diff --git a/src/engine/graphics/vulkan/rhi_render_state.zig b/src/engine/graphics/vulkan/rhi_render_state.zig index 7ee6b0be..d6db6b3a 100644 --- a/src/engine/graphics/vulkan/rhi_render_state.zig +++ b/src/engine/graphics/vulkan/rhi_render_state.zig @@ -20,6 +20,8 @@ const GlobalUniforms = extern struct { pbr_params: [4]f32, volumetric_params: [4]f32, viewport_size: [4]f32, + lpv_params: [4]f32, + lpv_origin: [4]f32, }; const CloudPushConstants = extern struct { @@ -45,6 +47,8 @@ pub fn updateGlobalUniforms(ctx: anytype, view_proj: Mat4, cam_pos: Vec3, sun_di .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, 0.0 }, + .lpv_params = .{ if (cloud_params.lpv_enabled) 1.0 else 0.0, cloud_params.lpv_intensity, cloud_params.lpv_cell_size, @floatFromInt(cloud_params.lpv_grid_size) }, + .lpv_origin = .{ cloud_params.lpv_origin.x, cloud_params.lpv_origin.y, cloud_params.lpv_origin.z, 0.0 }, }; try ctx.descriptors.updateGlobalUniforms(ctx.frames.current_frame, &global_uniforms); diff --git a/src/engine/graphics/vulkan/rhi_timing.zig b/src/engine/graphics/vulkan/rhi_timing.zig index ad143b33..22d84d19 100644 --- a/src/engine/graphics/vulkan/rhi_timing.zig +++ b/src/engine/graphics/vulkan/rhi_timing.zig @@ -8,6 +8,7 @@ const GpuPass = enum { shadow_2, g_pass, ssao, + lpv_compute, sky, opaque_pass, cloud, @@ -15,10 +16,10 @@ const GpuPass = enum { fxaa, post_process, - pub const COUNT = 11; + pub const COUNT = 12; }; -const QUERY_COUNT_PER_FRAME = GpuPass.COUNT * 2; +pub const QUERY_COUNT_PER_FRAME = GpuPass.COUNT * 2; fn mapPassName(name: []const u8) ?GpuPass { if (std.mem.eql(u8, name, "ShadowPass0")) return .shadow_0; @@ -26,6 +27,7 @@ fn mapPassName(name: []const u8) ?GpuPass { if (std.mem.eql(u8, name, "ShadowPass2")) return .shadow_2; if (std.mem.eql(u8, name, "GPass")) return .g_pass; if (std.mem.eql(u8, name, "SSAOPass")) return .ssao; + if (std.mem.eql(u8, name, "LPVPass")) return .lpv_compute; if (std.mem.eql(u8, name, "SkyPass")) return .sky; if (std.mem.eql(u8, name, "OpaquePass")) return .opaque_pass; if (std.mem.eql(u8, name, "CloudPass")) return .cloud; @@ -84,12 +86,13 @@ pub fn processTimingResults(ctx: anytype) void { ctx.timing.timing_results.shadow_pass_ms[2] = @as(f32, @floatFromInt(results[5] -% results[4])) * period / 1e6; ctx.timing.timing_results.g_pass_ms = @as(f32, @floatFromInt(results[7] -% results[6])) * period / 1e6; ctx.timing.timing_results.ssao_pass_ms = @as(f32, @floatFromInt(results[9] -% results[8])) * period / 1e6; - ctx.timing.timing_results.sky_pass_ms = @as(f32, @floatFromInt(results[11] -% results[10])) * period / 1e6; - ctx.timing.timing_results.opaque_pass_ms = @as(f32, @floatFromInt(results[13] -% results[12])) * period / 1e6; - ctx.timing.timing_results.cloud_pass_ms = @as(f32, @floatFromInt(results[15] -% results[14])) * period / 1e6; - ctx.timing.timing_results.bloom_pass_ms = @as(f32, @floatFromInt(results[17] -% results[16])) * period / 1e6; - ctx.timing.timing_results.fxaa_pass_ms = @as(f32, @floatFromInt(results[19] -% results[18])) * period / 1e6; - ctx.timing.timing_results.post_process_pass_ms = @as(f32, @floatFromInt(results[21] -% results[20])) * period / 1e6; + ctx.timing.timing_results.lpv_pass_ms = @as(f32, @floatFromInt(results[11] -% results[10])) * period / 1e6; + ctx.timing.timing_results.sky_pass_ms = @as(f32, @floatFromInt(results[13] -% results[12])) * period / 1e6; + ctx.timing.timing_results.opaque_pass_ms = @as(f32, @floatFromInt(results[15] -% results[14])) * period / 1e6; + ctx.timing.timing_results.cloud_pass_ms = @as(f32, @floatFromInt(results[17] -% results[16])) * period / 1e6; + ctx.timing.timing_results.bloom_pass_ms = @as(f32, @floatFromInt(results[19] -% results[18])) * period / 1e6; + ctx.timing.timing_results.fxaa_pass_ms = @as(f32, @floatFromInt(results[21] -% results[20])) * period / 1e6; + ctx.timing.timing_results.post_process_pass_ms = @as(f32, @floatFromInt(results[23] -% results[22])) * period / 1e6; ctx.timing.timing_results.main_pass_ms = ctx.timing.timing_results.sky_pass_ms + ctx.timing.timing_results.opaque_pass_ms + ctx.timing.timing_results.cloud_pass_ms; ctx.timing.timing_results.validate(); @@ -100,17 +103,19 @@ pub fn processTimingResults(ctx: anytype) void { ctx.timing.timing_results.total_gpu_ms += ctx.timing.timing_results.shadow_pass_ms[2]; ctx.timing.timing_results.total_gpu_ms += ctx.timing.timing_results.g_pass_ms; ctx.timing.timing_results.total_gpu_ms += ctx.timing.timing_results.ssao_pass_ms; + ctx.timing.timing_results.total_gpu_ms += ctx.timing.timing_results.lpv_pass_ms; ctx.timing.timing_results.total_gpu_ms += ctx.timing.timing_results.main_pass_ms; ctx.timing.timing_results.total_gpu_ms += ctx.timing.timing_results.bloom_pass_ms; ctx.timing.timing_results.total_gpu_ms += ctx.timing.timing_results.fxaa_pass_ms; ctx.timing.timing_results.total_gpu_ms += ctx.timing.timing_results.post_process_pass_ms; if (ctx.timing.timing_enabled) { - std.debug.print("GPU Frame Time: {d:.2}ms (Shadow: {d:.2}, G-Pass: {d:.2}, SSAO: {d:.2}, Main: {d:.2}, Bloom: {d:.2}, FXAA: {d:.2}, Post: {d:.2})\n", .{ + std.debug.print("GPU Frame Time: {d:.2}ms (Shadow: {d:.2}, G-Pass: {d:.2}, SSAO: {d:.2}, LPV: {d:.2}, Main: {d:.2}, Bloom: {d:.2}, FXAA: {d:.2}, Post: {d:.2})\n", .{ ctx.timing.timing_results.total_gpu_ms, ctx.timing.timing_results.shadow_pass_ms[0] + ctx.timing.timing_results.shadow_pass_ms[1] + ctx.timing.timing_results.shadow_pass_ms[2], ctx.timing.timing_results.g_pass_ms, ctx.timing.timing_results.ssao_pass_ms, + ctx.timing.timing_results.lpv_pass_ms, ctx.timing.timing_results.main_pass_ms, ctx.timing.timing_results.bloom_pass_ms, ctx.timing.timing_results.fxaa_pass_ms, diff --git a/src/engine/ui/debug_lpv_overlay.zig b/src/engine/ui/debug_lpv_overlay.zig new file mode 100644 index 00000000..97da827a --- /dev/null +++ b/src/engine/ui/debug_lpv_overlay.zig @@ -0,0 +1,30 @@ +const rhi = @import("../graphics/rhi.zig"); +const IUIContext = rhi.IUIContext; + +pub const DebugLPVOverlay = struct { + pub const Config = struct { + width: f32 = 220.0, + height: f32 = 220.0, + spacing: f32 = 10.0, + }; + + pub fn rect(screen_height: f32, config: Config) rhi.Rect { + return .{ + .x = config.spacing, + .y = screen_height - config.height - config.spacing, + .width = config.width, + .height = config.height, + }; + } + + pub fn draw(ui: IUIContext, lpv_texture: rhi.TextureHandle, screen_width: f32, screen_height: f32, config: Config) void { + if (lpv_texture == 0) return; + + const r = rect(screen_height, config); + + ui.beginPass(screen_width, screen_height); + defer ui.endPass(); + + ui.drawTexture(lpv_texture, r); + } +}; diff --git a/src/engine/ui/timing_overlay.zig b/src/engine/ui/timing_overlay.zig index 140c26fc..421d9d5e 100644 --- a/src/engine/ui/timing_overlay.zig +++ b/src/engine/ui/timing_overlay.zig @@ -16,7 +16,7 @@ pub const TimingOverlay = struct { const width: f32 = 280; const line_height: f32 = 15; const scale: f32 = 1.0; - const num_lines = 13; // Title + 11 passes + Total + const num_lines = 14; // Title + 12 passes + Total const padding = 20; // Spacers and margins // Background @@ -31,6 +31,7 @@ pub const TimingOverlay = struct { drawTimingLine(ui, "SHADOW 2:", results.shadow_pass_ms[2], x + 10, &y, scale, Color.gray); drawTimingLine(ui, "G-PASS:", results.g_pass_ms, x + 10, &y, scale, Color.gray); drawTimingLine(ui, "SSAO:", results.ssao_pass_ms, x + 10, &y, scale, Color.gray); + drawTimingLine(ui, "LPV:", results.lpv_pass_ms, x + 10, &y, scale, Color.gray); drawTimingLine(ui, "SKY:", results.sky_pass_ms, x + 10, &y, scale, Color.gray); drawTimingLine(ui, "OPAQUE:", results.opaque_pass_ms, x + 10, &y, scale, Color.gray); drawTimingLine(ui, "CLOUDS:", results.cloud_pass_ms, x + 10, &y, scale, Color.gray); diff --git a/src/game/app.zig b/src/game/app.zig index 35764cdd..0bcf8573 100644 --- a/src/game/app.zig +++ b/src/game/app.zig @@ -20,6 +20,7 @@ const render_graph_pkg = @import("../engine/graphics/render_graph.zig"); const RenderGraph = render_graph_pkg.RenderGraph; const AtmosphereSystem = @import("../engine/graphics/atmosphere_system.zig").AtmosphereSystem; const MaterialSystem = @import("../engine/graphics/material_system.zig").MaterialSystem; +const LPVSystem = @import("../engine/graphics/lpv_system.zig").LPVSystem; const ResourcePackManager = @import("../engine/graphics/resource_pack.zig").ResourcePackManager; const AudioSystem = @import("../engine/audio/system.zig").AudioSystem; const TimingOverlay = @import("../engine/ui/timing_overlay.zig").TimingOverlay; @@ -46,6 +47,7 @@ pub const App = struct { render_graph: RenderGraph, atmosphere_system: *AtmosphereSystem, material_system: *MaterialSystem, + lpv_system: *LPVSystem, audio_system: *AudioSystem, shadow_passes: [4]render_graph_pkg.ShadowPass, g_pass: render_graph_pkg.GPass, @@ -233,6 +235,7 @@ pub const App = struct { .render_graph = RenderGraph.init(allocator), .atmosphere_system = atmosphere_system, .material_system = undefined, + .lpv_system = undefined, .audio_system = audio_system, .shadow_passes = .{ render_graph_pkg.ShadowPass.init(0), @@ -272,6 +275,16 @@ pub const App = struct { app.material_system = try MaterialSystem.init(allocator, rhi, &app.atlas); errdefer app.material_system.deinit(); + app.lpv_system = try LPVSystem.init( + allocator, + rhi, + settings.lpv_grid_size, + settings.lpv_cell_size, + settings.lpv_intensity, + settings.lpv_propagation_iterations, + settings.lpv_enabled, + ); + errdefer app.lpv_system.deinit(); // Sync FXAA and Bloom settings to RHI after initialization app.rhi.setFXAA(settings.fxaa_enabled); @@ -327,6 +340,7 @@ pub const App = struct { self.render_graph.deinit(); self.atmosphere_system.deinit(); self.material_system.deinit(); + self.lpv_system.deinit(); self.audio_system.deinit(); self.atlas.deinit(); if (self.env_map) |*t| t.deinit(); @@ -352,6 +366,7 @@ pub const App = struct { .render_graph = &self.render_graph, .atmosphere_system = self.atmosphere_system, .material_system = self.material_system, + .lpv_system = self.lpv_system, .audio_system = self.audio_system, .env_map_ptr = &self.env_map, .shader = self.shader, @@ -427,6 +442,11 @@ pub const App = struct { .volumetric_steps = 0, .volumetric_scattering = 0, .ssao_enabled = false, + .lpv_enabled = false, + .lpv_intensity = 0, + .lpv_cell_size = 2.0, + .lpv_grid_size = 32, + .lpv_origin = Vec3.zero, }); // Update current screen. Transitions happen here. diff --git a/src/game/input_mapper.zig b/src/game/input_mapper.zig index b3be8edb..49a77823 100644 --- a/src/game/input_mapper.zig +++ b/src/game/input_mapper.zig @@ -155,6 +155,8 @@ pub const GameAction = enum(u8) { toggle_clouds, /// Toggle fog toggle_fog, + /// Toggle LPV debug overlay + toggle_lpv_overlay, pub const count = @typeInfo(GameAction).@"enum".fields.len; }; @@ -347,6 +349,7 @@ pub const DEFAULT_BINDINGS = blk: { bindings[@intFromEnum(GameAction.toggle_ssao)] = ActionBinding.init(.{ .key = .f8 }); bindings[@intFromEnum(GameAction.toggle_clouds)] = ActionBinding.init(.{ .key = .f9 }); bindings[@intFromEnum(GameAction.toggle_fog)] = ActionBinding.init(.{ .key = .f10 }); + bindings[@intFromEnum(GameAction.toggle_lpv_overlay)] = ActionBinding.init(.{ .key = .f11 }); // Map controls bindings[@intFromEnum(GameAction.toggle_map)] = ActionBinding.init(.{ .key = .m }); diff --git a/src/game/screen.zig b/src/game/screen.zig index de179f4f..a7c80fe5 100644 --- a/src/game/screen.zig +++ b/src/game/screen.zig @@ -15,6 +15,7 @@ const TextureAtlas = @import("../engine/graphics/texture_atlas.zig").TextureAtla const RenderGraph = @import("../engine/graphics/render_graph.zig").RenderGraph; const AtmosphereSystem = @import("../engine/graphics/atmosphere_system.zig").AtmosphereSystem; const MaterialSystem = @import("../engine/graphics/material_system.zig").MaterialSystem; +const LPVSystem = @import("../engine/graphics/lpv_system.zig").LPVSystem; const Texture = @import("../engine/graphics/texture.zig").Texture; const AudioSystem = @import("../engine/audio/system.zig").AudioSystem; const rhi_pkg = @import("../engine/graphics/rhi.zig"); @@ -28,6 +29,7 @@ pub const EngineContext = struct { render_graph: *RenderGraph, atmosphere_system: *AtmosphereSystem, material_system: *MaterialSystem, + lpv_system: *LPVSystem, audio_system: *AudioSystem, env_map_ptr: ?*?Texture, shader: rhi_pkg.ShaderHandle, diff --git a/src/game/screens/graphics.zig b/src/game/screens/graphics.zig index cbf91d8f..e877e9e4 100644 --- a/src/game/screens/graphics.zig +++ b/src/game/screens/graphics.zig @@ -231,6 +231,11 @@ pub const GraphicsScreen = struct { } } + if (std.mem.eql(u8, decl.name, "lpv_quality_preset")) { + const legend = getLPVQualityLegend(settings.lpv_quality_preset); + Font.drawText(ui, legend, vx - 90.0 * ui_scale, sy + row_height - 10.0 * ui_scale, 1.2 * ui_scale, Color.rgba(0.72, 0.86, 0.98, 1.0)); + } + sy += row_height; } @@ -255,3 +260,11 @@ fn getPresetLabel(idx: usize) []const u8 { if (idx >= settings_pkg.json_presets.graphics_presets.items.len) return "CUSTOM"; return settings_pkg.json_presets.graphics_presets.items[idx].name; } + +fn getLPVQualityLegend(preset: u32) []const u8 { + return switch (preset) { + 0 => "GRID16 ITER2 TICK8", + 2 => "GRID64 ITER5 TICK3", + else => "GRID32 ITER3 TICK6", + }; +} diff --git a/src/game/screens/world.zig b/src/game/screens/world.zig index fa003049..dd5073c4 100644 --- a/src/game/screens/world.zig +++ b/src/game/screens/world.zig @@ -10,6 +10,8 @@ const rhi_pkg = @import("../../engine/graphics/rhi.zig"); const render_graph_pkg = @import("../../engine/graphics/render_graph.zig"); const PausedScreen = @import("paused.zig").PausedScreen; const DebugShadowOverlay = @import("../../engine/ui/debug_shadow_overlay.zig").DebugShadowOverlay; +const DebugLPVOverlay = @import("../../engine/ui/debug_lpv_overlay.zig").DebugLPVOverlay; +const Font = @import("../../engine/ui/font.zig"); const log = @import("../../engine/core/log.zig"); pub const WorldScreen = struct { @@ -110,6 +112,11 @@ pub const WorldScreen = struct { log.log.info("Fog {s}", .{if (self.session.atmosphere.fog_enabled) "enabled" else "disabled"}); self.last_debug_toggle_time = now; } + if (can_toggle_debug and ctx.input_mapper.isActionPressed(ctx.input, .toggle_lpv_overlay)) { + ctx.settings.debug_lpv_overlay_active = !ctx.settings.debug_lpv_overlay_active; + log.log.info("LPV overlay {s}", .{if (ctx.settings.debug_lpv_overlay_active) "enabled" else "disabled"}); + self.last_debug_toggle_time = now; + } // Update Audio Listener const cam = &self.session.player.camera; @@ -150,6 +157,21 @@ pub const WorldScreen = struct { const ssao_enabled = ctx.settings.ssao_enabled and !ctx.disable_ssao and !ctx.disable_gpass_draw; const cloud_shadows_enabled = ctx.settings.cloud_shadows_enabled and !ctx.disable_clouds; + + const lpv_quality = resolveLPVQuality(ctx.settings.lpv_quality_preset); + try ctx.lpv_system.setSettings( + ctx.settings.lpv_enabled, + ctx.settings.lpv_intensity, + ctx.settings.lpv_cell_size, + lpv_quality.propagation_iterations, + lpv_quality.grid_size, + lpv_quality.update_interval_frames, + ); + ctx.rhi.timing().beginPassTiming("LPVPass"); + try ctx.lpv_system.update(self.session.world, camera.position); + ctx.rhi.timing().endPassTiming("LPVPass"); + + const lpv_origin = ctx.lpv_system.getOrigin(); const cloud_params: rhi_pkg.CloudParams = blk: { const p = self.session.clouds.getShadowParams(); break :blk .{ @@ -181,6 +203,11 @@ pub const WorldScreen = struct { .volumetric_steps = ctx.settings.volumetric_steps, .volumetric_scattering = ctx.settings.volumetric_scattering, .ssao_enabled = ssao_enabled, + .lpv_enabled = ctx.settings.lpv_enabled, + .lpv_intensity = ctx.settings.lpv_intensity, + .lpv_cell_size = ctx.lpv_system.getCellSize(), + .lpv_grid_size = ctx.lpv_system.getGridSize(), + .lpv_origin = lpv_origin, }; }; @@ -216,6 +243,7 @@ pub const WorldScreen = struct { .overlay_renderer = renderOverlay, .overlay_ctx = self, .cached_cascades = &frame_cascades, + .lpv_texture_handle = ctx.lpv_system.getTextureHandle(), }; try ctx.render_graph.execute(render_ctx); } @@ -233,6 +261,29 @@ pub const WorldScreen = struct { if (ctx.settings.debug_shadows_active) { DebugShadowOverlay.draw(ctx.rhi.ui(), ctx.rhi.shadow(), screen_w, screen_h, .{}); } + if (ctx.settings.debug_lpv_overlay_active) { + const cfg = DebugLPVOverlay.Config{}; + const r = DebugLPVOverlay.rect(screen_h, cfg); + DebugLPVOverlay.draw(ctx.rhi.ui(), ctx.lpv_system.getDebugOverlayTextureHandle(), screen_w, screen_h, cfg); + + const stats = ctx.lpv_system.getStats(); + const timing_results = ctx.rhi.timing().getTimingResults(); + var line0_buf: [64]u8 = undefined; + var line1_buf: [64]u8 = undefined; + var line2_buf: [64]u8 = undefined; + var line3_buf: [64]u8 = undefined; + const line0 = std.fmt.bufPrint(&line0_buf, "LPV GRID:{d} ITER:{d}", .{ stats.grid_size, stats.propagation_iterations }) catch "LPV"; + const line1 = std.fmt.bufPrint(&line1_buf, "LIGHTS:{d} UPDATE:{d:.2}MS", .{ stats.light_count, stats.cpu_update_ms }) catch "LIGHTS"; + const line2 = std.fmt.bufPrint(&line2_buf, "TICK:{d} UPDATED:{d}", .{ stats.update_interval_frames, if (stats.updated_this_frame) @as(u8, 1) else @as(u8, 0) }) catch "TICK"; + const line3 = std.fmt.bufPrint(&line3_buf, "LPV GPU:{d:.2}MS", .{timing_results.lpv_pass_ms}) catch "GPU"; + + const text_x = r.x; + const text_y = r.y - 28.0; + Font.drawText(ui, line0, text_x, text_y, 1.5, .{ .r = 0.95, .g = 0.98, .b = 1.0, .a = 1.0 }); + Font.drawText(ui, line1, text_x, text_y + 10.0, 1.5, .{ .r = 0.95, .g = 0.98, .b = 1.0, .a = 1.0 }); + Font.drawText(ui, line2, text_x, text_y + 20.0, 1.5, .{ .r = 0.95, .g = 0.98, .b = 1.0, .a = 1.0 }); + Font.drawText(ui, line3, text_x, text_y + 30.0, 1.5, .{ .r = 0.95, .g = 0.98, .b = 1.0, .a = 1.0 }); + } } pub fn onEnter(ptr: *anyopaque) void { @@ -256,3 +307,17 @@ pub const WorldScreen = struct { self.session.hand_renderer.draw(scene_ctx.camera.position, scene_ctx.camera.yaw, scene_ctx.camera.pitch); } }; + +const LPVQualityResolved = struct { + grid_size: u32, + propagation_iterations: u32, + update_interval_frames: u32, +}; + +fn resolveLPVQuality(preset: u32) LPVQualityResolved { + return switch (preset) { + 0 => .{ .grid_size = 16, .propagation_iterations = 2, .update_interval_frames = 8 }, + 2 => .{ .grid_size = 64, .propagation_iterations = 5, .update_interval_frames = 3 }, + else => .{ .grid_size = 32, .propagation_iterations = 3, .update_interval_frames = 6 }, + }; +} diff --git a/src/game/settings/data.zig b/src/game/settings/data.zig index 794e38c1..5580de78 100644 --- a/src/game/settings/data.zig +++ b/src/game/settings/data.zig @@ -36,6 +36,7 @@ pub const Settings = struct { textures_enabled: bool = true, wireframe_enabled: bool = false, debug_shadows_active: bool = false, // Reverted to false for normal gameplay + debug_lpv_overlay_active: bool = false, shadow_quality: u32 = 2, // 0=Low, 1=Medium, 2=High, 3=Ultra shadow_distance: f32 = 250.0, anisotropic_filtering: u8 = 16, @@ -67,6 +68,15 @@ pub const Settings = struct { volumetric_scattering: f32 = 0.8, // Mie scattering anisotropy (G) ssao_enabled: bool = true, + // LPV Settings (Issue #260) + lpv_enabled: bool = true, + lpv_quality_preset: u32 = 1, // 0=Fast, 1=Balanced, 2=Quality + lpv_intensity: f32 = 0.5, + lpv_cell_size: f32 = 2.0, + lpv_grid_size: u32 = 32, // Derived from lpv_quality_preset at runtime + lpv_propagation_iterations: u32 = 3, // Derived from lpv_quality_preset at runtime + lpv_update_interval_frames: u32 = 6, // Derived from lpv_quality_preset at runtime + // FXAA Settings (Phase 3) fxaa_enabled: bool = true, @@ -227,6 +237,25 @@ pub const Settings = struct { .label = "VOLUMETRIC SCATTERING", .kind = .{ .slider = .{ .min = 0.0, .max = 1.0, .step = 0.05 } }, }; + pub const lpv_enabled = SettingMetadata{ + .label = "LPV GI", + .kind = .toggle, + }; + pub const lpv_quality_preset = SettingMetadata{ + .label = "LPV QUALITY", + .kind = .{ .choice = .{ + .labels = &[_][]const u8{ "FAST", "BALANCED", "QUALITY" }, + .values = &[_]u32{ 0, 1, 2 }, + } }, + }; + pub const lpv_intensity = SettingMetadata{ + .label = "LPV INTENSITY", + .kind = .{ .slider = .{ .min = 0.0, .max = 2.0, .step = 0.1 } }, + }; + pub const lpv_cell_size = SettingMetadata{ + .label = "LPV CELL SIZE", + .kind = .{ .slider = .{ .min = 1.0, .max = 4.0, .step = 0.25 } }, + }; }; pub fn getShadowResolution(self: *const Settings) u32 { diff --git a/src/game/settings/json_presets.zig b/src/game/settings/json_presets.zig index 82ba0b3c..e7fa7d27 100644 --- a/src/game/settings/json_presets.zig +++ b/src/game/settings/json_presets.zig @@ -22,6 +22,12 @@ pub const PresetConfig = struct { volumetric_steps: u32, volumetric_scattering: f32, ssao_enabled: bool, + lpv_quality_preset: u32 = 1, + lpv_enabled: bool = true, + lpv_intensity: f32 = 0.5, + lpv_cell_size: f32 = 2.0, + lpv_grid_size: u32 = 32, + lpv_propagation_iterations: u32 = 3, lod_enabled: bool, render_distance: i32, fxaa_enabled: bool, @@ -71,6 +77,26 @@ pub fn initPresets(allocator: std.mem.Allocator) !void { std.log.warn("Skipping preset '{s}': invalid bloom_intensity {}", .{ p.name, p.bloom_intensity }); continue; } + if (p.lpv_intensity < 0.0 or p.lpv_intensity > 2.0) { + std.log.warn("Skipping preset '{s}': invalid lpv_intensity {}", .{ p.name, p.lpv_intensity }); + continue; + } + if (p.lpv_quality_preset > 2) { + std.log.warn("Skipping preset '{s}': invalid lpv_quality_preset {}", .{ p.name, p.lpv_quality_preset }); + continue; + } + if (p.lpv_cell_size < 1.0 or p.lpv_cell_size > 4.0) { + std.log.warn("Skipping preset '{s}': invalid lpv_cell_size {}", .{ p.name, p.lpv_cell_size }); + continue; + } + if (p.lpv_grid_size != 16 and p.lpv_grid_size != 32 and p.lpv_grid_size != 64) { + std.log.warn("Skipping preset '{s}': invalid lpv_grid_size {}", .{ p.name, p.lpv_grid_size }); + continue; + } + if (p.lpv_propagation_iterations < 1 or p.lpv_propagation_iterations > 8) { + std.log.warn("Skipping preset '{s}': invalid lpv_propagation_iterations {}", .{ p.name, p.lpv_propagation_iterations }); + continue; + } // Duplicate name because parsed.deinit() will free strings p.name = try allocator.dupe(u8, preset.name); errdefer allocator.free(p.name); @@ -106,6 +132,12 @@ pub fn apply(settings: *Settings, preset_idx: usize) void { settings.volumetric_steps = config.volumetric_steps; settings.volumetric_scattering = config.volumetric_scattering; settings.ssao_enabled = config.ssao_enabled; + settings.lpv_quality_preset = config.lpv_quality_preset; + settings.lpv_enabled = config.lpv_enabled; + settings.lpv_intensity = config.lpv_intensity; + 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.fxaa_enabled = config.fxaa_enabled; @@ -139,6 +171,12 @@ fn matches(settings: *const Settings, preset: PresetConfig) bool { std.math.approxEqAbs(f32, settings.volumetric_density, preset.volumetric_density, epsilon) and settings.volumetric_steps == preset.volumetric_steps and std.math.approxEqAbs(f32, settings.volumetric_scattering, preset.volumetric_scattering, epsilon) and + settings.lpv_quality_preset == preset.lpv_quality_preset and + settings.lpv_enabled == preset.lpv_enabled and + std.math.approxEqAbs(f32, settings.lpv_intensity, preset.lpv_intensity, epsilon) and + 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 From 0d225e62cba2cf51b8c5aadd2577f83be879085a Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Sat, 7 Feb 2026 22:13:39 +0000 Subject: [PATCH 05/11] fix(lighting): harden LPV shader loading and propagation tuning --- assets/shaders/vulkan/lpv_propagate.comp | 3 ++- src/engine/graphics/lpv_system.zig | 33 +++++++++++++++++++----- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/assets/shaders/vulkan/lpv_propagate.comp b/assets/shaders/vulkan/lpv_propagate.comp index 40329399..e2b22939 100644 --- a/assets/shaders/vulkan/lpv_propagate.comp +++ b/assets/shaders/vulkan/lpv_propagate.comp @@ -25,7 +25,8 @@ void main() { return; } - vec3 center = sampleCell(cell, gridSize) * 0.82; + // propagation.x = neighbor propagation factor, propagation.y = center retention + vec3 center = sampleCell(cell, gridSize) * push_data.propagation.y; vec3 accum = center; float f = push_data.propagation.x; diff --git a/src/engine/graphics/lpv_system.zig b/src/engine/graphics/lpv_system.zig index bf71b84c..e0e5a319 100644 --- a/src/engine/graphics/lpv_system.zig +++ b/src/engine/graphics/lpv_system.zig @@ -11,6 +11,10 @@ const VulkanContext = @import("vulkan/rhi_context_types.zig").VulkanContext; const Utils = @import("vulkan/utils.zig"); const MAX_LIGHTS_PER_UPDATE: usize = 2048; +const DEFAULT_PROPAGATION_FACTOR: f32 = 0.14; +const DEFAULT_CENTER_RETENTION: f32 = 0.82; +const INJECT_SHADER_PATH = "assets/shaders/vulkan/lpv_inject.comp.spv"; +const PROPAGATE_SHADER_PATH = "assets/shaders/vulkan/lpv_propagate.comp.spv"; const GpuLight = extern struct { pos_radius: [4]f32, @@ -27,8 +31,7 @@ const InjectPush = extern struct { const PropagatePush = extern struct { grid_size: u32, _pad0: [3]u32, - propagation_factor: f32, - _pad1: [3]f32, + propagation: [4]f32, }; pub const LPVSystem = struct { @@ -53,6 +56,8 @@ pub const LPVSystem = struct { cell_size: f32, intensity: f32, propagation_iterations: u32, + propagation_factor: f32, + center_retention: f32, enabled: bool, update_interval_frames: u32 = 6, @@ -102,6 +107,8 @@ pub const LPVSystem = struct { .cell_size = @max(cell_size, 0.5), .intensity = std.math.clamp(intensity, 0.0, 4.0), .propagation_iterations = std.math.clamp(propagation_iterations, 1, 8), + .propagation_factor = DEFAULT_PROPAGATION_FACTOR, + .center_retention = DEFAULT_CENTER_RETENTION, .enabled = enabled, .was_enabled_last_frame = enabled, .debug_overlay_pixels = &.{}, @@ -124,6 +131,9 @@ pub const LPVSystem = struct { ); errdefer self.destroyLightBuffer(); + try ensureShaderFileExists(INJECT_SHADER_PATH); + try ensureShaderFileExists(PROPAGATE_SHADER_PATH); + try self.initComputeResources(); errdefer self.deinitComputeResources(); @@ -347,8 +357,7 @@ pub const LPVSystem = struct { const prop_push = PropagatePush{ .grid_size = self.grid_size, ._pad0 = .{ 0, 0, 0 }, - .propagation_factor = 0.14, - ._pad1 = .{ 0, 0, 0 }, + .propagation = .{ self.propagation_factor, self.center_retention, 0, 0 }, }; var use_ab = true; @@ -551,8 +560,12 @@ pub const LPVSystem = struct { fn destroyLightBuffer(self: *LPVSystem) void { if (self.light_buffer.buffer != null) { + if (self.light_buffer.memory == null) { + std.log.warn("LPV light buffer has VkBuffer but null VkDeviceMemory during teardown", .{}); + } if (self.light_buffer.mapped_ptr != null) { c.vkUnmapMemory(self.vk_ctx.vulkan_device.vk_device, self.light_buffer.memory); + self.light_buffer.mapped_ptr = null; } c.vkDestroyBuffer(self.vk_ctx.vulkan_device.vk_device, self.light_buffer.buffer, null); c.vkFreeMemory(self.vk_ctx.vulkan_device.vk_device, self.light_buffer.memory, null); @@ -697,9 +710,9 @@ pub const LPVSystem = struct { fn createComputePipelines(self: *LPVSystem) !void { const vk = self.vk_ctx.vulkan_device.vk_device; - const inject_module = try createShaderModule(vk, "assets/shaders/vulkan/lpv_inject.comp.spv", self.allocator); + const inject_module = try createShaderModule(vk, INJECT_SHADER_PATH, self.allocator); defer c.vkDestroyShaderModule(vk, inject_module, null); - const propagate_module = try createShaderModule(vk, "assets/shaders/vulkan/lpv_propagate.comp.spv", self.allocator); + const propagate_module = try createShaderModule(vk, PROPAGATE_SHADER_PATH, self.allocator); defer c.vkDestroyShaderModule(vk, propagate_module, null); var inject_pc = std.mem.zeroes(c.VkPushConstantRange); @@ -803,3 +816,11 @@ fn createShaderModule(vk: c.VkDevice, path: []const u8, allocator: std.mem.Alloc try Utils.checkVk(c.vkCreateShaderModule(vk, &info, null, &module)); return module; } + +fn ensureShaderFileExists(path: []const u8) !void { + std.fs.cwd().access(path, .{}) catch |err| { + std.log.err("LPV shader artifact missing: {s} ({})", .{ path, err }); + std.log.err("Run `nix develop --command zig build` to regenerate Vulkan SPIR-V shaders.", .{}); + return err; + }; +} From b5982b2d4b9e30766b55b0cb6390265ce6f4725d Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Sat, 7 Feb 2026 22:24:44 +0000 Subject: [PATCH 06/11] fix(lighting): address LPV review blockers --- .gitignore | 2 ++ assets/shaders/vulkan/lpv_inject.comp.spv | Bin 0 -> 4036 bytes assets/shaders/vulkan/lpv_propagate.comp.spv | Bin 0 -> 3936 bytes src/engine/graphics/lpv_system.zig | 6 +++-- .../graphics/vulkan/resource_texture_ops.zig | 23 +++++------------- 5 files changed, 12 insertions(+), 19 deletions(-) create mode 100644 assets/shaders/vulkan/lpv_inject.comp.spv create mode 100644 assets/shaders/vulkan/lpv_propagate.comp.spv diff --git a/.gitignore b/.gitignore index 0398fc41..4a1ce0be 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ test_output.txt *.swp .DS_Store *.spv +!assets/shaders/vulkan/lpv_inject.comp.spv +!assets/shaders/vulkan/lpv_propagate.comp.spv wiki/ *.exr *.hdr diff --git a/assets/shaders/vulkan/lpv_inject.comp.spv b/assets/shaders/vulkan/lpv_inject.comp.spv new file mode 100644 index 0000000000000000000000000000000000000000..1ea075519aae1f9545e20c28ad2af12a08de264f GIT binary patch literal 4036 zcmZvdX>(LX6ozk>31Qzi1!@vxQ4~RTBLWJBC`wpeP$!c~2!lf=BojbHWmViz5fO0% z|A2nqMO3UZDx4XC>=&HBQIq%uJPfy=zX4mdfru z2llKUYYePjzhR9L(~_1n(3t7O6p*dpM7cVwV>OroFE7vNyNw9_o-Or{lt;@a#$Yqvz1ONk#~P(deSEl)?qwO=2-Qmi zWUITPjGSkan*kzcj$9FR)7Lwh3>E=noQW%6 zOD7 z)pswpHMxuJd5oXUggkEtHQbx}Tyo;R?KdBr$DeY>6tUZpazcAz(9JV`5>h>@T}b=M znP;p$oOuzrRt4MdfGgT_(A`6{=c8M59=q*87J>abk=lMImW2D36Iq|uJL4`Q zwSALv)_1?lf&Dzw3Zy>k+T&5Awc-pOLpN65dfNV0_RtR+gi^9*0yNx%WU@6me$>mEf-&oZJuw}8gkA>+uyI8{-L0E2Kz0f`{)SvFw$7%rceyFvCi#r;5#^n zZS6IIGuC-%8>??EkkhvA6QBrsvGlJ4auL5UZQs*IV6Oh#%jNrg2FM9l6KDqUJkO$go;vx)?*wvj zzR#h@9=p)Z>p*q`eb?BFdAotR@)rWvgKmF)vEB>l_p7%DUf&gRt^XqM8zJv(?eP+j zkBEKt!r4o|cjj+aPTk+@{-FC?^*8c26F7u)p2|Ns)62+LfJ^_YNc~FBJOlX}u!i&Z zTk{6+EJr|$c{A|865}10fO+!Xt@dGHKkKW<+@t7O{R!ze=NR@N@P75>@41StKi>0k zbUERQ_v{S%u=U42C(yG#?Ys}~{$3w__Q~J%NQjxjFWkEs1=e2)^qmCu@vZBNeH!Tb z^NeH1^XOY1Vmpzifc|)%(}9zB<5V`-U$dUEZ_4^FYpfR*!Wqqvz{h!M5&I z>e}}MV2r$Hkoz#zGoO3C7`RyTDz;p-KMHnyPak8;85i^AjCY2g0OuO>KSj4*eBYm; z8zUccuA%$B;ye5tUCvm0=#%q3#W^UoeQVA|X}uZ9FMz+7^PE}x%fR_Nv7SD;{QLDC znSUMmHMkLMzs28xRlwYv!8W!H{aa83`fde#3!X{nx4|@Ej=p%_@6bKdzHlesqszG> z_XE0IQ;z#FaLveG-cTp2e2keJV&>#x=Ap~Sm<1tbelBJqx_qqr0J?j)OmAoWATUPW ZJC$1mtgWv%oNF<(Ln5QcA-Az@#`B8UlC1;`?(VKD+m(121B#NciiCJUoO5;K#ih#C~PQgK&A zMO3Wvo8@2dKe<-9JkOomc!{^_o$h|S`<(7``rc`3T-%Z)4aw}JHTgcNKMRvam;`P{ zDtitcJ+!4-8`yGN=T;-;B~59dG4qKjAe+IdVtGVIH&_5Kw-6`;P2@IW|4ecLHlJ4@ zXCA&{tx~M^9a}$HzO}2ott*Y2178}fR1nyOWG-@eygYEUe0s3XwIppQ)#9nK%HTdC zucoK@i}B_2m;5ikIoSYL8Jp~{j+fG$jo4$w@!~0@wQjz~E+n1U*Va%-w!`&~kB;?D zR8OMP(TtQ+H@3f8FF1J5aE$okaIsb%9bunBvQv)y{()kxi0c5YzL~fH2%Ug|HYi$!=B#7a?r!t>K(u*>@{^0yD2?i zXX*eed;UIa>*6ZsIm;~ouF;L04OXRgQ(}E9QoT9V-On6j_5HVBb8-dST;mrr&Fle{w<6_)_RLH-&-htL_4?X{bU!)sj9toZ_RyYlDKmX{Lvyb(e!`QCO_WpdV zb1Tv{eD7ED>wQ_5y!ktj_9t(?w(o+j9&nQIl-56YnT)KQ#sz8O>}CVPXlXmfA?}- z*DyYMsG{3*taS!G>Qu%|_)htl=b2qft*O0ncHdz)1K08Ht^m2fc4pkUGsdS0{0`%P zk$ZSIoxpR{ZbkYH4cq(m8yU9#-kfc|-^lPA?>91R@7r%=*xqL;XPfW0GW@PTG0pay z8GhsaW`=FN-^j4_`;FA@rc39(3;1q$uDj{cci;{ne+Juo*qw2EI3dqN+gQ)^F3=5( zb=`d#XRK$XZLGfiKu+6r?*^{di>3dbjFT5XQsV*SL13=_i{$no?*;O|vCko-&!70@ z?*nq)rTSmIaq4nW%Mo;Iu}<$yJL)=$Ze9AkKkZ%A=zIJiXanZx>xTC|eh7GueZcRZ zYdMn-1AXV|#r#KrYskC4^CM@x{>OlvK4(Qc_IMIK_E?S{d-S90Gsk(-R(FbYzCKki!Qs+;S*PJ;Zs zKa1^J&Y&~;9I#gT$ay~Fr-*Tl)4+A)?Pnt6BK`$zIqP#~UIcP}J7Q*D%J?6s%N||^ za?a3Mq>sJGPXRf5QICCIL3bZ*XZTgn31WuNWt_Z-8Ga4jT>Vkc>*(^HdF-Y9g^sQ7 z4PbqqrLj}MTHiwYSgSE_0y%3{cRlY@TRv)k2i@8|KffE=(Z_jo`_Sh%L_22wJ@htU zjy`jo?e~H67PI{Uy0P-cIL{vf=lCr5qWw|E8S7s9i*C9Ux{!eJPAU_ArqPZ6`JNEhlyEF4$L~45mvyfkcdB7Zf=Gf0yz#fiJ zpJ(!Q#v!h9mou&*$96+obGqsvE52f8(! zV{i9a1KNRn-050$*VfmY^>rh?Pu}k~;2!!9b5r(l Q3y>E+4WJR&vp#$N7hPIJ-~a#s literal 0 HcmV?d00001 diff --git a/src/engine/graphics/lpv_system.zig b/src/engine/graphics/lpv_system.zig index e0e5a319..d9a3e314 100644 --- a/src/engine/graphics/lpv_system.zig +++ b/src/engine/graphics/lpv_system.zig @@ -11,7 +11,9 @@ const VulkanContext = @import("vulkan/rhi_context_types.zig").VulkanContext; const Utils = @import("vulkan/utils.zig"); const MAX_LIGHTS_PER_UPDATE: usize = 2048; +// Tuned for stable 6-neighbor propagation without runaway energy. const DEFAULT_PROPAGATION_FACTOR: f32 = 0.14; +// Retain most energy in the current cell to prevent over-blur. const DEFAULT_CENTER_RETENTION: f32 = 0.82; const INJECT_SHADER_PATH = "assets/shaders/vulkan/lpv_inject.comp.spv"; const PROPAGATE_SHADER_PATH = "assets/shaders/vulkan/lpv_propagate.comp.spv"; @@ -41,7 +43,7 @@ pub const LPVSystem = struct { cpu_update_ms: f32 = 0.0, grid_size: u32 = 0, propagation_iterations: u32 = 0, - update_interval_frames: u32 = 0, + update_interval_frames: u32 = 6, }; allocator: std.mem.Allocator, @@ -134,8 +136,8 @@ pub const LPVSystem = struct { try ensureShaderFileExists(INJECT_SHADER_PATH); try ensureShaderFileExists(PROPAGATE_SHADER_PATH); - try self.initComputeResources(); errdefer self.deinitComputeResources(); + try self.initComputeResources(); return self; } diff --git a/src/engine/graphics/vulkan/resource_texture_ops.zig b/src/engine/graphics/vulkan/resource_texture_ops.zig index ca49ad29..2c06bacd 100644 --- a/src/engine/graphics/vulkan/resource_texture_ops.zig +++ b/src/engine/graphics/vulkan/resource_texture_ops.zig @@ -224,7 +224,9 @@ pub fn createTexture(self: anytype, width: u32, height: u32, format: rhi.Texture } pub fn createTexture3D(self: anytype, width: u32, height: u32, depth: u32, format: rhi.TextureFormat, config: rhi.TextureConfig, data_opt: ?[]const u8) rhi.RhiError!rhi.TextureHandle { - _ = config; + var texture_config = config; + texture_config.generate_mipmaps = false; + const vk_format: c.VkFormat = switch (format) { .rgba => c.VK_FORMAT_R8G8B8A8_UNORM, .rgba_srgb => c.VK_FORMAT_R8G8B8A8_SRGB, @@ -239,6 +241,7 @@ pub fn createTexture3D(self: anytype, width: u32, height: u32, depth: u32, forma var usage_flags: c.VkImageUsageFlags = c.VK_IMAGE_USAGE_TRANSFER_DST_BIT | c.VK_IMAGE_USAGE_SAMPLED_BIT; if (format == .rgba32f) usage_flags |= c.VK_IMAGE_USAGE_STORAGE_BIT; + if (texture_config.is_render_target) usage_flags |= c.VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; var staging_offset: u64 = 0; if (data_opt) |data| { @@ -342,14 +345,7 @@ pub fn createTexture3D(self: anytype, width: u32, height: u32, depth: u32, forma view_info.subresourceRange.baseArrayLayer = 0; view_info.subresourceRange.layerCount = 1; - const sampler = try Utils.createSampler(self.vulkan_device, .{ - .min_filter = .linear, - .mag_filter = .linear, - .wrap_s = .clamp_to_edge, - .wrap_t = .clamp_to_edge, - .generate_mipmaps = false, - .is_render_target = false, - }, 1, self.vulkan_device.max_anisotropy); + const sampler = try Utils.createSampler(self.vulkan_device, texture_config, 1, self.vulkan_device.max_anisotropy); errdefer c.vkDestroySampler(device, sampler, null); try Utils.checkVk(c.vkCreateImageView(device, &view_info, null, &view)); @@ -366,14 +362,7 @@ pub fn createTexture3D(self: anytype, width: u32, height: u32, depth: u32, forma .height = height, .depth = depth, .format = format, - .config = .{ - .min_filter = .linear, - .mag_filter = .linear, - .wrap_s = .clamp_to_edge, - .wrap_t = .clamp_to_edge, - .generate_mipmaps = false, - .is_render_target = false, - }, + .config = texture_config, .is_3d = true, .is_owned = true, }); From 89bb72c156ed9c2c63f8738a396b10dec932ea68 Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Sat, 7 Feb 2026 22:29:39 +0000 Subject: [PATCH 07/11] fix(lighting): clarify 3D texture config and LPV debug scaling --- src/engine/graphics/lpv_system.zig | 4 ++++ src/engine/graphics/vulkan/resource_texture_ops.zig | 5 ++++- src/game/screens/world.zig | 7 ++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/engine/graphics/lpv_system.zig b/src/engine/graphics/lpv_system.zig index d9a3e314..ad95fed7 100644 --- a/src/engine/graphics/lpv_system.zig +++ b/src/engine/graphics/lpv_system.zig @@ -240,6 +240,7 @@ pub const LPVSystem = struct { @memcpy(@as([*]u8, @ptrCast(ptr))[0..bytes.len], bytes); } + // Keep debug overlay generation on LPV update ticks only (not every frame). self.buildDebugOverlay(lights[0..], light_count); try self.uploadDebugOverlay(); @@ -426,6 +427,9 @@ pub const LPVSystem = struct { @memset(empty, 0.0); const bytes = std.mem.sliceAsBytes(empty); + // Atlas fallback: store Z slices stacked in Y (height = grid_size * grid_size). + // This stays until terrain/material sampling fully migrates to native 3D textures. + self.grid_texture_a = try self.rhi.createTexture( self.grid_size, self.grid_size * self.grid_size, diff --git a/src/engine/graphics/vulkan/resource_texture_ops.zig b/src/engine/graphics/vulkan/resource_texture_ops.zig index 2c06bacd..47ef4a2b 100644 --- a/src/engine/graphics/vulkan/resource_texture_ops.zig +++ b/src/engine/graphics/vulkan/resource_texture_ops.zig @@ -225,7 +225,10 @@ pub fn createTexture(self: anytype, width: u32, height: u32, format: rhi.Texture pub fn createTexture3D(self: anytype, width: u32, height: u32, depth: u32, format: rhi.TextureFormat, config: rhi.TextureConfig, data_opt: ?[]const u8) rhi.RhiError!rhi.TextureHandle { var texture_config = config; - texture_config.generate_mipmaps = false; + if (texture_config.generate_mipmaps) { + std.log.warn("3D texture mipmaps are not supported yet; disabling generate_mipmaps", .{}); + texture_config.generate_mipmaps = false; + } const vk_format: c.VkFormat = switch (format) { .rgba => c.VK_FORMAT_R8G8B8A8_UNORM, diff --git a/src/game/screens/world.zig b/src/game/screens/world.zig index dd5073c4..67d1b254 100644 --- a/src/game/screens/world.zig +++ b/src/game/screens/world.zig @@ -262,7 +262,12 @@ pub const WorldScreen = struct { DebugShadowOverlay.draw(ctx.rhi.ui(), ctx.rhi.shadow(), screen_w, screen_h, .{}); } if (ctx.settings.debug_lpv_overlay_active) { - const cfg = DebugLPVOverlay.Config{}; + const overlay_size = std.math.clamp(220.0 * ctx.settings.ui_scale, 160.0, screen_h * 0.4); + const cfg = DebugLPVOverlay.Config{ + .width = overlay_size, + .height = overlay_size, + .spacing = 10.0 * ctx.settings.ui_scale, + }; const r = DebugLPVOverlay.rect(screen_h, cfg); DebugLPVOverlay.draw(ctx.rhi.ui(), ctx.lpv_system.getDebugOverlayTextureHandle(), screen_w, screen_h, cfg); From cc434378189b188e275190956e8939163eb0a34f Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Sat, 7 Feb 2026 22:34:30 +0000 Subject: [PATCH 08/11] chore(lighting): polish LPV docs and overlay sizing --- src/engine/graphics/lpv_system.zig | 4 ++-- src/engine/ui/debug_lpv_overlay.zig | 15 ++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/engine/graphics/lpv_system.zig b/src/engine/graphics/lpv_system.zig index ad95fed7..21f06c2f 100644 --- a/src/engine/graphics/lpv_system.zig +++ b/src/engine/graphics/lpv_system.zig @@ -11,9 +11,9 @@ const VulkanContext = @import("vulkan/rhi_context_types.zig").VulkanContext; const Utils = @import("vulkan/utils.zig"); const MAX_LIGHTS_PER_UPDATE: usize = 2048; -// Tuned for stable 6-neighbor propagation without runaway energy. +// Approximate 1/7 energy spread for 6-neighbor propagation, tuned to avoid runaway amplification. const DEFAULT_PROPAGATION_FACTOR: f32 = 0.14; -// Retain most energy in the current cell to prevent over-blur. +// Keep most energy in the center cell to reduce over-blur and preserve local contrast. const DEFAULT_CENTER_RETENTION: f32 = 0.82; const INJECT_SHADER_PATH = "assets/shaders/vulkan/lpv_inject.comp.spv"; const PROPAGATE_SHADER_PATH = "assets/shaders/vulkan/lpv_propagate.comp.spv"; diff --git a/src/engine/ui/debug_lpv_overlay.zig b/src/engine/ui/debug_lpv_overlay.zig index 97da827a..06bb801e 100644 --- a/src/engine/ui/debug_lpv_overlay.zig +++ b/src/engine/ui/debug_lpv_overlay.zig @@ -1,19 +1,24 @@ +const std = @import("std"); const rhi = @import("../graphics/rhi.zig"); const IUIContext = rhi.IUIContext; pub const DebugLPVOverlay = struct { pub const Config = struct { - width: f32 = 220.0, - height: f32 = 220.0, + // Optional explicit size; <= 0 uses screen-relative fallback. + width: f32 = 0.0, + height: f32 = 0.0, spacing: f32 = 10.0, }; pub fn rect(screen_height: f32, config: Config) rhi.Rect { + const fallback_size = std.math.clamp(screen_height * 0.28, 160.0, 280.0); + const width = if (config.width > 0.0) config.width else fallback_size; + const height = if (config.height > 0.0) config.height else fallback_size; return .{ .x = config.spacing, - .y = screen_height - config.height - config.spacing, - .width = config.width, - .height = config.height, + .y = screen_height - height - config.spacing, + .width = width, + .height = height, }; } From c8b88824d4ff0320d34758ef46ffe6d085c135b5 Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Sat, 7 Feb 2026 22:37:01 +0000 Subject: [PATCH 09/11] docs(lighting): clarify LPV constants and 3D mipmap behavior --- src/engine/graphics/lpv_system.zig | 5 +++-- src/engine/graphics/vulkan/resource_texture_ops.zig | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/engine/graphics/lpv_system.zig b/src/engine/graphics/lpv_system.zig index 21f06c2f..9d937a49 100644 --- a/src/engine/graphics/lpv_system.zig +++ b/src/engine/graphics/lpv_system.zig @@ -11,9 +11,10 @@ const VulkanContext = @import("vulkan/rhi_context_types.zig").VulkanContext; const Utils = @import("vulkan/utils.zig"); const MAX_LIGHTS_PER_UPDATE: usize = 2048; -// Approximate 1/7 energy spread for 6-neighbor propagation, tuned to avoid runaway amplification. +// Approximate 1/7 spread for 6-neighbor propagation (close to 1/6 with extra damping) +// to keep indirect light stable and avoid runaway amplification. const DEFAULT_PROPAGATION_FACTOR: f32 = 0.14; -// Keep most energy in the center cell to reduce over-blur and preserve local contrast. +// Retain 82% of center-cell energy so propagation does not over-blur local contrast. const DEFAULT_CENTER_RETENTION: f32 = 0.82; const INJECT_SHADER_PATH = "assets/shaders/vulkan/lpv_inject.comp.spv"; const PROPAGATE_SHADER_PATH = "assets/shaders/vulkan/lpv_propagate.comp.spv"; diff --git a/src/engine/graphics/vulkan/resource_texture_ops.zig b/src/engine/graphics/vulkan/resource_texture_ops.zig index 47ef4a2b..4e6e6cef 100644 --- a/src/engine/graphics/vulkan/resource_texture_ops.zig +++ b/src/engine/graphics/vulkan/resource_texture_ops.zig @@ -223,6 +223,8 @@ pub fn createTexture(self: anytype, width: u32, height: u32, format: rhi.Texture return handle; } +/// Creates a 3D texture resource. +/// Note: `config.generate_mipmaps` is currently forced off for 3D textures. pub fn createTexture3D(self: anytype, width: u32, height: u32, depth: u32, format: rhi.TextureFormat, config: rhi.TextureConfig, data_opt: ?[]const u8) rhi.RhiError!rhi.TextureHandle { var texture_config = config; if (texture_config.generate_mipmaps) { From 3a619bea399c611057fde7a6dbde0913cd000d42 Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Sat, 7 Feb 2026 23:11:10 +0000 Subject: [PATCH 10/11] docs(vulkan): clarify createTexture3D config handling --- src/engine/graphics/vulkan/resource_texture_ops.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/engine/graphics/vulkan/resource_texture_ops.zig b/src/engine/graphics/vulkan/resource_texture_ops.zig index 4e6e6cef..04d3cf8a 100644 --- a/src/engine/graphics/vulkan/resource_texture_ops.zig +++ b/src/engine/graphics/vulkan/resource_texture_ops.zig @@ -225,6 +225,7 @@ pub fn createTexture(self: anytype, width: u32, height: u32, format: rhi.Texture /// Creates a 3D texture resource. /// Note: `config.generate_mipmaps` is currently forced off for 3D textures. +/// Other config parameters (filtering, wrapping, render-target flag) are respected. pub fn createTexture3D(self: anytype, width: u32, height: u32, depth: u32, format: rhi.TextureFormat, config: rhi.TextureConfig, data_opt: ?[]const u8) rhi.RhiError!rhi.TextureHandle { var texture_config = config; if (texture_config.generate_mipmaps) { From f3640c9a4cd2e37eace7685946f33a9bbec82248 Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Sun, 8 Feb 2026 01:11:08 +0000 Subject: [PATCH 11/11] perf(lighting): gate LPV debug overlay work behind toggle --- src/engine/graphics/lpv_system.zig | 19 +++++++++++++------ src/game/screens/world.zig | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/engine/graphics/lpv_system.zig b/src/engine/graphics/lpv_system.zig index 9d937a49..6c71e03b 100644 --- a/src/engine/graphics/lpv_system.zig +++ b/src/engine/graphics/lpv_system.zig @@ -67,6 +67,7 @@ pub const LPVSystem = struct { origin: Vec3 = Vec3.zero, current_frame: u32 = 0, was_enabled_last_frame: bool = true, + debug_overlay_was_enabled: bool = false, image_layout_a: c.VkImageLayout = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, image_layout_b: c.VkImageLayout = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, @@ -194,7 +195,7 @@ pub const LPVSystem = struct { return self.cell_size; } - pub fn update(self: *LPVSystem, world: *World, camera_pos: Vec3) !void { + pub fn update(self: *LPVSystem, world: *World, camera_pos: Vec3, debug_overlay_enabled: bool) !void { self.current_frame +%= 1; var timer = std.time.Timer.start() catch unreachable; self.stats.updated_this_frame = false; @@ -204,11 +205,12 @@ pub const LPVSystem = struct { if (!self.enabled) { self.active_grid_texture = self.grid_texture_a; - if (self.was_enabled_last_frame) { + if (self.was_enabled_last_frame and debug_overlay_enabled) { self.buildDebugOverlay(&.{}, 0); try self.uploadDebugOverlay(); } self.was_enabled_last_frame = false; + self.debug_overlay_was_enabled = debug_overlay_enabled; self.stats.light_count = 0; self.stats.cpu_update_ms = 0.0; return; @@ -226,7 +228,10 @@ pub const LPVSystem = struct { @abs(next_origin.z - self.origin.z) >= self.cell_size; const tick_update = (self.current_frame % self.update_interval_frames) == 0; - if (!moved and !tick_update and self.was_enabled_last_frame) { + const debug_toggle_on = debug_overlay_enabled and !self.debug_overlay_was_enabled; + self.debug_overlay_was_enabled = debug_overlay_enabled; + + if (!moved and !tick_update and !debug_toggle_on and self.was_enabled_last_frame) { self.stats.cpu_update_ms = 0.0; return; } @@ -241,9 +246,11 @@ pub const LPVSystem = struct { @memcpy(@as([*]u8, @ptrCast(ptr))[0..bytes.len], bytes); } - // Keep debug overlay generation on LPV update ticks only (not every frame). - self.buildDebugOverlay(lights[0..], light_count); - try self.uploadDebugOverlay(); + if (debug_overlay_enabled) { + // Keep debug overlay generation only when overlay is active. + self.buildDebugOverlay(lights[0..], light_count); + try self.uploadDebugOverlay(); + } try self.dispatchCompute(light_count); diff --git a/src/game/screens/world.zig b/src/game/screens/world.zig index 67d1b254..4e7b0c8a 100644 --- a/src/game/screens/world.zig +++ b/src/game/screens/world.zig @@ -168,7 +168,7 @@ pub const WorldScreen = struct { lpv_quality.update_interval_frames, ); ctx.rhi.timing().beginPassTiming("LPVPass"); - try ctx.lpv_system.update(self.session.world, camera.position); + try ctx.lpv_system.update(self.session.world, camera.position, ctx.settings.debug_lpv_overlay_active); ctx.rhi.timing().endPassTiming("LPVPass"); const lpv_origin = ctx.lpv_system.getOrigin();