diff --git a/CMakeLists.txt b/CMakeLists.txt index 26a243af..cb569903 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -148,6 +148,7 @@ set(GRAPHICS_OPENGL_SOURCES src/Graphics/OpenGL/OpenGLCubemapTexture.cpp src/Graphics/OpenGL/OpenGLIBL.cpp src/Graphics/OpenGL/OpenGLRenderer.cpp + src/Graphics/OpenGL/OpenGLSceneRead.cpp src/Graphics/OpenGL/OpenGLShader.cpp src/Graphics/OpenGL/OpenGLTexture.cpp ) @@ -155,13 +156,16 @@ set(GRAPHICS_OPENGL_SOURCES set(GRAPHICS_VULKAN_SOURCES src/Graphics/Vulkan/VulkanBloom.cpp src/Graphics/Vulkan/VulkanBuffer.cpp + src/Graphics/Vulkan/VulkanCapture.cpp src/Graphics/Vulkan/VulkanCubemapTexture.cpp src/Graphics/Vulkan/VulkanDeferred.cpp src/Graphics/Vulkan/VulkanDescriptors.cpp src/Graphics/Vulkan/VulkanDevice.cpp src/Graphics/Vulkan/VulkanIBL.cpp src/Graphics/Vulkan/VulkanPipelines.cpp + src/Graphics/Vulkan/VulkanProfiler.cpp src/Graphics/Vulkan/VulkanRenderer.cpp + src/Graphics/Vulkan/VulkanSceneRead.cpp src/Graphics/Vulkan/VulkanShader.cpp src/Graphics/Vulkan/VulkanShadow.cpp src/Graphics/Vulkan/VulkanSSAO.cpp diff --git a/assets/shaders/bloom_composite.frag b/assets/shaders/bloom_composite.frag index 2489c725..2209626b 100644 --- a/assets/shaders/bloom_composite.frag +++ b/assets/shaders/bloom_composite.frag @@ -16,9 +16,42 @@ layout(push_constant) uniform BloomCompositePC { float bloomStrength; // bloom mix amount (typical 0.04..0.08 UE-style) float exposure; // exposure multiplier for HDR scene float gamma; // 1.0 unless tonemapping is enabled + uint ssrEnabled; // 0 skips the SSR fetch + float sharpenStrength; // post-TAA RCAS strength, 0 = off, 1 = strongest + float _pad0; float _pad1; + float _pad2; }; +// AMD FidelityFX FSR1 RCAS: caps the negative lobe so sharpening never rings +const float RCAS_LIMIT = 0.25 - 1.0 / 16.0; +const float RCAS_EPSILON = 1e-4; +const float PERCEPTUAL_MAX = 1.0 - 1e-4; + +// Reversible per-channel Reinhard: RCAS expects values in [0, 1] +vec3 ToPerceptual(vec3 c) { return c / (1.0 + c); } +vec3 FromPerceptual(vec3 c) { return c / (1.0 - min(c, vec3(PERCEPTUAL_MAX))); } + +/// Robust contrast-adaptive sharpening of the centre texel e over its 4 neighbours. +vec3 SharpenRCAS(vec3 e, vec2 uv) { + vec3 b = ToPerceptual(textureLodOffset(sceneHDR, uv, 0.0, ivec2( 0, -1)).rgb); + vec3 d = ToPerceptual(textureLodOffset(sceneHDR, uv, 0.0, ivec2(-1, 0)).rgb); + vec3 f = ToPerceptual(textureLodOffset(sceneHDR, uv, 0.0, ivec2( 1, 0)).rgb); + vec3 h = ToPerceptual(textureLodOffset(sceneHDR, uv, 0.0, ivec2( 0, 1)).rgb); + vec3 c = ToPerceptual(e); + + vec3 mn4 = min(min(b, d), min(f, h)); + vec3 mx4 = max(max(b, d), max(f, h)); + vec3 hitMin = min(mn4, c) / max(4.0 * mx4, vec3(RCAS_EPSILON)); + vec3 hitMax = (1.0 - max(mx4, c)) / min(4.0 * mn4 - 4.0, vec3(-RCAS_EPSILON)); + vec3 lobeRGB = max(-hitMin, hitMax); + float lobe = max(-RCAS_LIMIT, min(max(lobeRGB.r, max(lobeRGB.g, lobeRGB.b)), 0.0)) + * sharpenStrength; + + vec3 sharp = (lobe * (b + d + f + h) + c) / (4.0 * lobe + 1.0); + return FromPerceptual(clamp(sharp, vec3(0.0), vec3(PERCEPTUAL_MAX))); +} + // ACES Filmic tonemap (Narkowicz 2015) vec3 ACESFilm(vec3 x) { const float a = 2.51; @@ -31,23 +64,27 @@ vec3 ACESFilm(vec3 x) { } void main() { - vec3 hdr = texture(sceneHDR, fragUV).rgb; - vec3 bloom = texture(bloomTex, fragUV).rgb; + vec3 hdr = texture(sceneHDR, fragUV).rgb; + if (sharpenStrength > 0.0) hdr = SharpenRCAS(hdr, fragUV); // SSR: ssr.rgb = reflectedColor * Fresnel * fades (premultiplied), ssr.a = fades only. // Unpremultiply to get reflectedColor * Fresnel, then lerp over the base scene using // ssr.a as coverage. This replaces (rather than adds to) the IBL-approximated specular, // so reflections are visible even on bright surfaces. - vec4 ssr = texture(ssrTex, fragUV); - if (ssr.a > 0.001) { - hdr = mix(hdr, ssr.rgb / max(ssr.a, 1e-4), min(ssr.a, 1.0)); + if (ssrEnabled != 0u) { + vec4 ssr = texture(ssrTex, fragUV); + if (ssr.a > 0.001) { + hdr = mix(hdr, ssr.rgb / max(ssr.a, 1e-4), min(ssr.a, 1.0)); + } } // Add bloom on top of the HDR scene. Bloom is a soft glow derived // from the bright parts of the scene; adding it preserves the base // luminance and only brightens hot spots (unlike mix, which dims // the scene by the bloom factor in non-glowing regions). - vec3 combined = hdr + bloom * clamp(bloomStrength, 0.0, 1.0); + vec3 combined = hdr; + if (bloomStrength > 0.0) + combined += texture(bloomTex, fragUV).rgb * clamp(bloomStrength, 0.0, 1.0); // Apply exposure before tone mapping. combined *= exposure; diff --git a/assets/shaders/bloom_composite.frag.spv b/assets/shaders/bloom_composite.frag.spv index c6baf0b3..94fa1b67 100644 Binary files a/assets/shaders/bloom_composite.frag.spv and b/assets/shaders/bloom_composite.frag.spv differ diff --git a/assets/shaders/lighting_pass.frag b/assets/shaders/lighting_pass.frag index 06da48bb..a8b99749 100644 --- a/assets/shaders/lighting_pass.frag +++ b/assets/shaders/lighting_pass.frag @@ -80,12 +80,16 @@ float InterleavedGradientNoise(vec2 screenPos) { return fract(magic.z * fract(dot(screenPos, magic.xy))); } -vec2 VogelDisk(int i, int count, float phi) { - const float goldenAngle = 2.39996323; - float r = sqrt((float(i) + 0.5) / float(count)); - float theta = float(i) * goldenAngle + phi; - return vec2(r * cos(theta), r * sin(theta)); -} +// r_i * (cos, sin)(i * 2.39996323), r_i = sqrt((i + 0.5) / 16) +const vec2 kVogel16[16] = vec2[]( + vec2( 0.176776692, 0.000000000), vec2(-0.225772178, 0.206825846), + vec2( 0.034557982, -0.393771189), vec2( 0.284571416, 0.371172631), + vec2(-0.522223197, -0.092373738), vec2( 0.494695337, -0.314684824), + vec2(-0.165465304, 0.615525170), vec2(-0.315562004, -0.607594155), + vec2( 0.684642316, 0.250029722), vec2(-0.712255912, 0.294009357), + vec2( 0.343354186, -0.733728746), vec2( 0.253730458, 0.808931929), + vec2(-0.764746789, -0.443184328), vec2( 0.897133617, -0.197234073), + vec2(-0.547505567, 0.778773187), vec2(-0.126488309, -0.976089476)); // Fixed-radius rotated PCF — crisp shadows like a classic forward renderer, // at a fraction of PCSS cost (no blocker search). Hardware comparison sampler @@ -93,10 +97,12 @@ vec2 VogelDisk(int i, int count, float phi) { const int PCF_SAMPLES = 16; float PCFFilter(vec2 uv, float zRef, float filterRadius, float phi) { + vec2 cs = vec2(cos(phi), sin(phi)) * filterRadius; float shadow = 0.0; for (int i = 0; i < PCF_SAMPLES; ++i) { - vec2 off = VogelDisk(i, PCF_SAMPLES, phi) * filterRadius; - shadow += texture(gShadow, vec3(uv + off, zRef)); + vec2 d = kVogel16[i]; + vec2 off = vec2(d.x * cs.x - d.y * cs.y, d.x * cs.y + d.y * cs.x); + shadow += textureLod(gShadow, vec3(uv + off, zRef), 0.0); } return shadow / float(PCF_SAMPLES); } @@ -209,16 +215,17 @@ vec3 ReconstructWorldPos(vec2 uv, float depth) { // ================================================================== void main() { // Early-out for sky pixels (depth == 1.0 means nothing was drawn) - float depth = texture(gDepth, fragUV).r; + ivec2 px = ivec2(gl_FragCoord.xy); + float depth = texelFetch(gDepth, px, 0).r; if (depth >= 1.0) { outColor = vec4(0.0, 0.0, 0.0, 1.0); return; } // Reconstruct world position from depth instead of a GBuffer RT. vec3 worldPos = ReconstructWorldPos(fragUV, depth); vec4 ndcPos = vec4(fragUV.x * 2.0 - 1.0, 1.0 - 2.0 * fragUV.y, depth, 1.0); - vec4 albedoAO = texture(gAlbedoAO, fragUV); - vec4 normalRg = texture(gNormalRough, fragUV); - vec4 metalEmit = texture(gMetalEmit, fragUV); // R16G16B16A16: .r=metallic, .gba=emissive + vec4 albedoAO = texelFetch(gAlbedoAO, px, 0); + vec4 normalRg = texelFetch(gNormalRough, px, 0); + vec4 metalEmit = texelFetch(gMetalEmit, px, 0); // R16G16B16A16: .r=metallic, .gba=emissive vec3 albedo = albedoAO.rgb; float bakedAO = albedoAO.a; // vertex AO baked into GBuffer — always [0.4, 1.0] @@ -308,15 +315,18 @@ void main() { vec3 Ldir = normalize(-uLightDir.xyz); float rawNdL = dot(N, Ldir); float wrapNdL = clamp(rawNdL * 0.85 + 0.15, 0.0, 1.0); - float shad = CalcShadow(worldPos, N); - shad *= smoothstep(-0.15, 0.0, rawNdL); + float shad = 0.0; + if (rawNdL > -0.15) + shad = CalcShadow(worldPos, N) * smoothstep(-0.15, 0.0, rawNdL); vec3 direct = uLightColor.rgb * uLightColor.a * wrapNdL * shad; for (uint li = 0u; li < uNumExtraLights; ++li) { vec3 Lx = normalize(-uExtraDir[li].xyz); float NdLx = max(dot(N, Lx), 0.0); direct += uExtraColor[li].rgb * uExtraColor[li].a * NdLx; } - ambient = albedo * mix(groundAmbient, skyAmbient, hemisphere) * bakedAO; + // SSAO remapped like GL + float aoGL = bakedAO * (0.5 + 0.5 * ssao); + ambient = albedo * mix(groundAmbient, skyAmbient, hemisphere) * aoGL; Lo = albedo * direct; } diff --git a/assets/shaders/lighting_pass.frag.spv b/assets/shaders/lighting_pass.frag.spv index 8ed6ac24..dbe8ef1f 100644 Binary files a/assets/shaders/lighting_pass.frag.spv and b/assets/shaders/lighting_pass.frag.spv differ diff --git a/assets/shaders/taa.frag b/assets/shaders/taa.frag index 5f83b7b4..4dd2b4d2 100644 --- a/assets/shaders/taa.frag +++ b/assets/shaders/taa.frag @@ -22,16 +22,65 @@ layout(set = 0, binding = 1) uniform sampler2D historyTex; layout(set = 0, binding = 2) uniform sampler2D gDepth; layout(set = 1, binding = 0) uniform TAAParams { - mat4 InvCurrentVP; // inverse of unjittered current view-projection - mat4 PrevVP; // previous frame unjittered view-projection + mat4 CurrentToPrevClip; // inverse(current VP) * previous VP, unjittered vec2 ScreenSize; - float BlendFactor; // 0.1 = 10% current, 90% history (1.0 = disable history) + float BlendFactor; // current-frame weight when still (1.0 = history reset) float _pad; }; +// NOTE: Do not change this. Global constant for standard sRGB/BT.709 human perception weights +const vec3 LUMA_WEIGHTS = vec3(0.2126, 0.7152, 0.0722); + +const float SKY_DEPTH = 0.9999; +// Variance box half-size in standard deviations: lower = sharper, more flicker +const float VARIANCE_GAMMA = 1.0; +// Current-frame weight once a pixel moves MOTION_PX_FOR_MOVING pixels per frame +const float BLEND_MOVING = 0.2; +// Current-frame weight on fast-moving reactive pixels (transparent draws zero +// the scene alpha). Refraction/reflection parallax grows with camera motion, +// so a still or slow view keeps full history and steady glints. +const float BLEND_REACTIVE = 0.5; +const float MOTION_PX_FOR_MOVING = 2.0; +const float NEIGHBOURHOOD_SIZE = 9.0; +const float TONEMAP_INVERSE_FLOOR = 1e-3; +const float CLIP_EPSILON = 1e-5; + +float Luma(vec3 color) { return dot(color, LUMA_WEIGHTS); } + +// Karis weighting: blend in a compressed range so bright sky cannot dominate +// the average at leaf/sky edges. +vec3 Tonemap(vec3 color) { return color / (1.0 + Luma(color)); } +vec3 TonemapInverse(vec3 color) { + return color / max(1.0 - Luma(color), TONEMAP_INVERSE_FLOOR); +} + +vec3 RGBToYCoCg(vec3 c) { + return vec3( 0.25 * c.r + 0.5 * c.g + 0.25 * c.b, + 0.5 * c.r - 0.5 * c.b, + -0.25 * c.r + 0.5 * c.g - 0.25 * c.b); +} +vec3 YCoCgToRGB(vec3 c) { + return vec3(c.x + c.y - c.z, c.x + c.z, c.x - c.y - c.z); +} + +vec3 FetchTonemappedYCoCg(ivec2 p, ivec2 maxIp) { + vec3 c = texelFetch(currentTex, clamp(p, ivec2(0), maxIp), 0).rgb; + return RGBToYCoCg(Tonemap(c)); +} + +// Pulls hist toward the box center until it touches the box. Unlike a +// per-channel clamp this never invents a colour that was not there. +vec3 ClipToAABB(vec3 hist, vec3 boxMin, vec3 boxMax) { + vec3 center = 0.5 * (boxMax + boxMin); + vec3 extents = max(0.5 * (boxMax - boxMin), vec3(CLIP_EPSILON)); + vec3 d = hist - center; + vec3 u = abs(d / extents); + float m = max(u.x, max(u.y, u.z)); + return m > 1.0 ? center + d / m : hist; +} + // Catmull-Rom history resample (Karis 5-tap): repeated bilinear resampling -// of the history buffer low-passes the whole image — this is what made the -// frame progressively blurry under camera motion. +// of the history buffer low-passes the whole image under camera motion. vec3 SampleHistoryCatmullRom(vec2 uv) { vec2 samplePos = uv * ScreenSize; vec2 texPos1 = floor(samplePos - 0.5) + 0.5; @@ -43,103 +92,105 @@ vec3 SampleHistoryCatmullRom(vec2 uv) { vec2 w3 = f * f * (-0.5 + 0.5 * f); vec2 w12 = w1 + w2; - vec2 tc0 = (texPos1 - 1.0) / ScreenSize; - vec2 tc3 = (texPos1 + 2.0) / ScreenSize; - vec2 tc12 = (texPos1 + w2 / w12) / ScreenSize; + vec2 tc0 = (texPos1 - 1.0) / ScreenSize; + vec2 tc3 = (texPos1 + 2.0) / ScreenSize; + vec2 tc12 = (texPos1 + w2 / w12) / ScreenSize; vec3 result = - texture(historyTex, vec2(tc12.x, tc0.y )).rgb * (w12.x * w0.y ) + - texture(historyTex, vec2(tc0.x, tc12.y)).rgb * (w0.x * w12.y) + - texture(historyTex, vec2(tc12.x, tc12.y)).rgb * (w12.x * w12.y) + - texture(historyTex, vec2(tc3.x, tc12.y)).rgb * (w3.x * w12.y) + - texture(historyTex, vec2(tc12.x, tc3.y )).rgb * (w12.x * w3.y ); + textureLod(historyTex, vec2(tc12.x, tc0.y ), 0.0).rgb * (w12.x * w0.y ) + + textureLod(historyTex, vec2(tc0.x, tc12.y), 0.0).rgb * (w0.x * w12.y) + + textureLod(historyTex, vec2(tc12.x, tc12.y), 0.0).rgb * (w12.x * w12.y) + + textureLod(historyTex, vec2(tc3.x, tc12.y), 0.0).rgb * (w3.x * w12.y) + + textureLod(historyTex, vec2(tc12.x, tc3.y ), 0.0).rgb * (w12.x * w3.y ); float wsum = (w12.x * w0.y) + (w0.x * w12.y) + (w12.x * w12.y) + (w3.x * w12.y) + (w12.x * w3.y); return max(result / wsum, vec3(0.0)); } void main() { - vec2 texelSize = 1.0 / ScreenSize; - - vec3 current = texture(currentTex, fragUV).rgb; - - ivec2 ip = ivec2(fragUV * ScreenSize); - ivec2 maxIp = ivec2(ScreenSize) - 1; - float depth = texelFetch(gDepth, clamp(ip, ivec2(0), maxIp), 0).r; - if (depth >= 0.9999) { + vec2 texelSize = 1.0 / ScreenSize; + ivec2 ip = ivec2(fragUV * ScreenSize); + ivec2 maxIp = ivec2(ScreenSize) - 1; + vec4 currentA = texelFetch(currentTex, clamp(ip, ivec2(0), maxIp), 0); + vec3 current = currentA.rgb; + float reactive = 1.0 - clamp(currentA.a, 0.0, 1.0); + + // 3x3 depth via gathers around the corner shared with the up-left texel + vec2 cuv = vec2(ip) * texelSize; + vec4 g00 = textureGather(gDepth, cuv); + vec4 g10 = textureGatherOffset(gDepth, cuv, ivec2(1, 0)); + vec4 g01 = textureGatherOffset(gDepth, cuv, ivec2(0, 1)); + vec4 g11 = textureGatherOffset(gDepth, cuv, ivec2(1, 1)); + + // Closest-depth dilation with unfiltered depth: edges follow the + // foreground surface, including sky pixels that border a leaf. + vec2 closestOff = vec2(0.0); + float closestDepth = g00.y; + if (g00.w < closestDepth) { closestDepth = g00.w; closestOff = vec2(-1.0, -1.0); } + if (g00.x < closestDepth) { closestDepth = g00.x; closestOff = vec2(-1.0, 0.0); } + if (g01.x < closestDepth) { closestDepth = g01.x; closestOff = vec2(-1.0, 1.0); } + if (g00.z < closestDepth) { closestDepth = g00.z; closestOff = vec2( 0.0, -1.0); } + if (g01.y < closestDepth) { closestDepth = g01.y; closestOff = vec2( 0.0, 1.0); } + if (g10.z < closestDepth) { closestDepth = g10.z; closestOff = vec2( 1.0, -1.0); } + if (g10.y < closestDepth) { closestDepth = g10.y; closestOff = vec2( 1.0, 0.0); } + if (g11.y < closestDepth) { closestDepth = g11.y; closestOff = vec2( 1.0, 1.0); } + + // Whole neighbourhood is sky: nothing to reproject + if (closestDepth >= SKY_DEPTH) { outColor = vec4(current, 1.0); return; } - // Closest-depth dilation with UNFILTERED depth reads. Bilinear-filtered - // depth at silhouettes yields positions on neither surface — the source - // of edge shimmer at distance where every texel is a silhouette. - vec2 closestOff = vec2(0.0); - float closestDepth = depth; - for (int x = -1; x <= 1; ++x) { - for (int y = -1; y <= 1; ++y) { - float d = texelFetch(gDepth, - clamp(ip + ivec2(x, y), ivec2(0), maxIp), - 0).r; - if (d < closestDepth) { - closestDepth = d; - closestOff = vec2(x, y); - } - } - } - - // Reproject the closest surface in the neighborhood and apply its motion - // to this pixel — edges follow the foreground surface. - // GBuffer stores UV.y=0 at screen top, so NDC.y = 1 - 2*uv.y + // Reproject the closest surface with the previous frame's camera vec2 dilatedUV = fragUV + closestOff * texelSize; - vec4 clipPos = vec4(dilatedUV.x * 2.0 - 1.0, 1.0 - 2.0 * dilatedUV.y, - closestDepth, 1.0); - vec4 world4 = InvCurrentVP * clipPos; - vec3 worldPos = world4.xyz / world4.w; - - vec4 prevClip = PrevVP * vec4(worldPos, 1.0); - vec3 prevNDC = prevClip.xyz / prevClip.w; - - vec2 prevUVd; - prevUVd.x = prevNDC.x * 0.5 + 0.5; - prevUVd.y = -prevNDC.y * 0.5 + 0.5; + vec4 clipPos = vec4(dilatedUV.x * 2.0 - 1.0, 1.0 - 2.0 * dilatedUV.y, + closestDepth, 1.0); + vec4 prevClip = CurrentToPrevClip * clipPos; + vec3 prevNDC = prevClip.xyz / prevClip.w; + vec2 prevUVd = vec2(prevNDC.x * 0.5 + 0.5, -prevNDC.y * 0.5 + 0.5); vec2 velocity = dilatedUV - prevUVd; vec2 prevUV = fragUV - velocity; - // Disocclusion / off-screen — use current frame only - if (prevUV.x < 0.0 || prevUV.x > 1.0 || - prevUV.y < 0.0 || prevUV.y > 1.0) { + // Came from off-screen: no history exists + if (any(lessThan(prevUV, vec2(0.0))) || any(greaterThan(prevUV, vec2(1.0)))) { outColor = vec4(current, 1.0); return; } - // 3x3 neighborhood AABB to prevent ghosting. - // History samples outside this range are clamped — they indicate - // a surface that changed between frames (disocclusion, fast motion). - vec3 nMin = vec3(1e10); - vec3 nMax = vec3(-1e10); - for (int x = -1; x <= 1; ++x) { - for (int y = -1; y <= 1; ++y) { - vec3 s = texture(currentTex, fragUV + vec2(x, y) * texelSize).rgb; + // Neighbourhood statistics in tonemapped YCoCg + vec3 currentYC = vec3(0.0); + vec3 m1 = vec3(0.0); + vec3 m2 = vec3(0.0); + vec3 nMin = vec3(1e10); + vec3 nMax = vec3(-1e10); + for (int y = -1; y <= 1; ++y) { + for (int x = -1; x <= 1; ++x) { + vec3 s = FetchTonemappedYCoCg(ip + ivec2(x, y), maxIp); + if (x == 0 && y == 0) currentYC = s; + m1 += s; + m2 += s * s; nMin = min(nMin, s); nMax = max(nMax, s); } } - - vec3 rawHistory = SampleHistoryCatmullRom(prevUV); - vec3 history = clamp(rawHistory, nMin, nMax); - - // Adaptive blend: when history was clamped hard (disocclusion / fast motion), - // trust the current frame more. clampFactor approaches 1 when the raw history - // was far outside the neighborhood AABB. 0.5 floor: pure 100% current would - // let sub-pixel jitter shimmer through on hard-clamped pixels every frame. - // (Was 0.35 to hide shadow-edge shimmer — that is now handled by the wider - // PCF, and 0.35 smeared distant sub-pixel detail that clamps every frame.) - float aabbRange = max(length(nMax - nMin), 0.001); - float clampDist = length(rawHistory - history); - float clampFactor = clamp(clampDist / aabbRange * 2.0, 0.0, 1.0); - float adaptBlend = mix(BlendFactor, 0.5, clampFactor); - - vec3 resolved = mix(history, current, adaptBlend); - outColor = vec4(resolved, 1.0); + vec3 mean = m1 / NEIGHBOURHOOD_SIZE; + vec3 sigma = sqrt(max(m2 / NEIGHBOURHOOD_SIZE - mean * mean, vec3(0.0))); + + // Variance box, never wider than the real min/max of the neighbourhood + vec3 boxMin = max(nMin, mean - VARIANCE_GAMMA * sigma); + vec3 boxMax = min(nMax, mean + VARIANCE_GAMMA * sigma); + + vec3 historyYC = RGBToYCoCg(Tonemap(SampleHistoryCatmullRom(prevUV))); + historyYC = ClipToAABB(historyYC, boxMin, boxMax); + + // Still: lean on history for a converged, stable image. + // Moving: take more of the current frame so reprojection blur cannot build up. + float motionPx = length(velocity * ScreenSize); + float motion = clamp(motionPx / MOTION_PX_FOR_MOVING, 0.0, 1.0); + float motionBlend = mix(BlendFactor, max(BlendFactor, BLEND_MOVING), motion); + motionBlend = max(motionBlend, reactive * motion * BLEND_REACTIVE); + + vec3 resolvedYC = mix(historyYC, currentYC, motionBlend); + vec3 resolved = max(YCoCgToRGB(resolvedYC), vec3(0.0)); + outColor = vec4(TonemapInverse(resolved), 1.0); } diff --git a/assets/shaders/taa.frag.spv b/assets/shaders/taa.frag.spv index 6ed9d260..21e2bbb3 100644 Binary files a/assets/shaders/taa.frag.spv and b/assets/shaders/taa.frag.spv differ diff --git a/include/private/Core/Window.hpp b/include/private/Core/Window.hpp index e4b09e1e..33f87372 100644 --- a/include/private/Core/Window.hpp +++ b/include/private/Core/Window.hpp @@ -67,6 +67,7 @@ class ENGINE_API Window { bool bIsFullScreen = false; bool bShouldClose = false; bool m_imguiReady = false; + bool m_inputLocked = false; std::string WindowName; SDL_Window* SDLWindow = nullptr; diff --git a/include/private/Graphics/Common/ConstantBuffer.hpp b/include/private/Graphics/Common/ConstantBuffer.hpp index d96c6ebb..b68ddd3a 100644 --- a/include/private/Graphics/Common/ConstantBuffer.hpp +++ b/include/private/Graphics/Common/ConstantBuffer.hpp @@ -1,8 +1,9 @@ #ifndef _CONSTANTBUFFER_H_ #define _CONSTANTBUFFER_H_ -#include #include +#include +#include #include namespace Sleak { @@ -327,6 +328,20 @@ namespace Sleak { DeferredCBData gpuData; }; + + /// Camera block for transparent formats that read the scene behind + /// them (VertexLayoutDesc::transparentReadsScene), std140 layout. + struct alignas(16) SceneReadUBO { + float ViewProj[16]; + float InvViewProj[16]; + float ScreenWidth, ScreenHeight, InvScreenWidth, InvScreenHeight; + float JitterX, JitterY, NearPlane, FarPlane; // jitter in NDC + float SceneValid, SkyValid, _pad0, _pad1; + }; + static_assert( + offsetof(SceneReadUBO, _pad1) + sizeof(float) == + sizeof(SceneReadUBO), + "SceneReadUBO must match the std140 block without padding"); } } diff --git a/include/private/Graphics/Common/Renderer.hpp b/include/private/Graphics/Common/Renderer.hpp index 33325a08..13203f02 100644 --- a/include/private/Graphics/Common/Renderer.hpp +++ b/include/private/Graphics/Common/Renderer.hpp @@ -7,6 +7,8 @@ namespace Sleak { +class Benchmark; + namespace RenderEngine { /// Selects which graphics API a Renderer instance targets. @@ -63,6 +65,18 @@ class ENGINE_API Renderer { /// Returns the backend's command-recording interface. virtual RenderContext* GetContext() = 0; + /// Adds the backend's GPU timing columns to a benchmark session. + virtual void RegisterBenchmarkMetrics(Benchmark& bench) { (void)bench; } + + /// Arms a readback of the next rendered frame, UI excluded, into a + /// binary PPM. Returns false when the backend cannot capture. + virtual bool RequestFrameCapture(const std::string& path) { + (void)path; + return false; + } + /// True while an armed frame capture has not been written yet. + virtual bool IsFrameCapturePending() const { return false; } + inline RendererType GetType() const { return Type; @@ -178,6 +192,10 @@ class ENGINE_API Renderer { void SetTAAEnabled(bool enabled) { m_taaEnabled = enabled; } bool IsTAAEnabled() const { return m_taaEnabled; } + /// Contrast-adaptive sharpening applied after TAA, 0 = off, 1 = strongest. + void SetSharpenStrength(float strength) { m_sharpenStrength = strength; } + float GetSharpenStrength() const { return m_sharpenStrength; } + // Post-process: Bloom void SetBloomEnabled(bool enabled) { m_bloomEnabled = enabled; } bool IsBloomEnabled() const { return m_bloomEnabled; } @@ -310,6 +328,7 @@ class ENGINE_API Renderer { int m_ssrBinarySteps = 6; float m_ssrRoughnessThreshold = 0.9f; float m_taaBlendFactor = 0.1f; + float m_sharpenStrength = 0.0f; float m_bloomRadius = 1.0f; float m_bloomIntensity = 1.0f; diff --git a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp index eec29d7d..566a1156 100644 --- a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp +++ b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp @@ -123,9 +123,14 @@ class ENGINE_API OpenGLRenderer : public Renderer, public RenderContext { float m_lightVP[16] = {}; float m_pendingLightVP[16] = {}; bool m_hasPendingLightVP = false; + // Matrix the shadow map was last rendered with + float m_shadowMapLightVP[16] = {}; + bool m_shadowMapRendered = false; bool m_inShadowPass = false; GLuint m_shadowTransformUBO = 0; void SetLightVP(const float* mat) override; + /// Uploads light and shadow data, sampling with the matrix the shadow + /// map was rendered with. void UpdateShadowLightUBO(const void* data, uint32_t size) override; /// Allocates the shadow-pass depth FBO and texture at the configured resolution. bool CreateShadowMapResources(); @@ -201,6 +206,29 @@ class ENGINE_API OpenGLRenderer : public Renderer, public RenderContext { /// Lazily (re)bakes IBL data when the bound skybox cubemap changes. void EnsureIBL(); void CleanupIBL(); + + // ---- Scene read for transparent formats ---- + static constexpr GLuint SCENE_COLOR_UNIT = 4; + static constexpr GLuint SCENE_DEPTH_UNIT = 5; + static constexpr GLuint SCENE_SKY_UNIT = 6; + static constexpr GLuint SCENE_READ_UBO_BINDING = 4; + GLuint m_sceneCopyTex = 0; // RGBA8 snapshot of the default framebuffer + int m_sceneCopyWidth = 0; + int m_sceneCopyHeight = 0; + GLuint m_sceneReadUBO = 0; + GLuint m_sceneFallbackCube = 0; + GLuint m_sceneSkyCube = 0; // skybox cubemap found this frame, 0 if none + bool m_sceneReadPrepared = false; // snapshot taken this frame + float m_cachedInvViewProj[16] = {}; + float m_cachedNearPlane = 0.0f; + float m_cachedFarPlane = 0.0f; + /// Returns the active scene's skybox cubemap, or 0 when there is none. + GLuint FindSkyboxCubemap() const; + /// Snapshots the scene once per frame on the first scene-reading draw + /// and binds the snapshot, depth, sky, and SceneReadUBO. + void PrepareSceneRead(); + /// Deletes the snapshot texture, stand-in cubemap, and UBO. + void CleanupSceneRead(); }; } // namespace RenderEngine diff --git a/include/private/Graphics/Vulkan/VulkanBuffer.hpp b/include/private/Graphics/Vulkan/VulkanBuffer.hpp index 02eddf31..5d5d4a01 100644 --- a/include/private/Graphics/Vulkan/VulkanBuffer.hpp +++ b/include/private/Graphics/Vulkan/VulkanBuffer.hpp @@ -22,6 +22,8 @@ class ENGINE_API VulkanBuffer : public BufferBase { void Update() override; /// Overwrites buffer contents, staging through a temporary buffer for device-local memory. void Update(void* data, size_t size) override; + /// Hands the GPU buffer to deferred deletion, which keeps it alive until + /// every frame and upload batch that may reference it has completed. void Cleanup() override; /// Maps host-visible memory for direct CPU writes. @@ -34,13 +36,17 @@ class ENGINE_API VulkanBuffer : public BufferBase { // VMA allocator lifecycle (owned by the renderer; created after the // logical device, destroyed after all buffers are drained). + /// Creates the VMA allocator plus a staging pool whose blocks stay + /// resident for the allocator's lifetime. static void InitAllocator(VkInstance instance, VkPhysicalDevice physicalDevice, VkDevice device); + /// Destroys the staging pool and the allocator once every buffer is freed. static void DestroyAllocator(); static VmaAllocator GetAllocator() { return s_allocator; } - // Flush all pending buffer copies in a single batched submission (synchronous). - static void FlushPendingCopies(); + /// Frees an upload batch that was recorded but never submitted, along + /// with its staging buffers. Only valid while the device is idle. + static void DiscardPendingCopies(); // Async flush: submit pending copies signaling the given semaphore. // No CPU wait — the caller must wait on the semaphore before using data. @@ -66,64 +72,65 @@ class ENGINE_API VulkanBuffer : public BufferBase { static void SetBatchingEnabled(bool enabled) { s_batchingEnabled = enabled; } private: - /// Allocates a VMA-backed VkBuffer with the given usage/memory properties. - void CreateBuffer(VkDeviceSize size, VkBufferUsageFlags usage, - VkMemoryPropertyFlags properties, - VkBuffer& buffer, VmaAllocation& memory, - VkDeviceSize* outAllocSize = nullptr, - uint32_t* outMemTypeIdx = nullptr); - - /// Records a GPU-side copy between buffers, batched or submitted immediately per SetBatchingEnabled. - void CopyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, - VkDeviceSize size); - - /// Finds a physical device memory type matching the filter and required properties. - uint32_t FindMemoryType(uint32_t typeFilter, - VkMemoryPropertyFlags properties); - - VkDevice m_device = VK_NULL_HANDLE; - VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; - VkCommandPool m_commandPool = VK_NULL_HANDLE; - VkQueue m_graphicsQueue = VK_NULL_HANDLE; - - VkBuffer m_buffer = VK_NULL_HANDLE; - VmaAllocation m_memory = VK_NULL_HANDLE; - VkDeviceSize m_allocSize = 0; // backing memory size (memReq) - VkDeviceSize m_bufferSize = 0; // logical VkBuffer size (bucketed) - VkBufferUsageFlags m_usage = 0; - uint32_t m_memoryTypeIndex = 0; - - VkBuffer m_stagingBuffer = VK_NULL_HANDLE; - VmaAllocation m_stagingMemory = VK_NULL_HANDLE; - - void* m_mappedData = nullptr; - bool m_pendingInBatch = false; - - // --- Batched transfer state --- - static bool s_batchingEnabled; - static bool s_batchActive; - static VkCommandBuffer s_batchCommandBuffer; - static VkDevice s_batchDevice; - static VkCommandPool s_batchCommandPool; - static VkQueue s_batchQueue; - static std::vector s_pendingCleanup; - - /// Opens the shared batch command buffer if one isn't already recording. - static void EnsureBatchStarted(VkDevice device, VkCommandPool pool, - VkQueue queue); - - // --- Deferred buffer deletion --- - /// A buffer queued for destruction once the GPU is guaranteed done referencing it. - struct DeferredBufferDelete { - VkBuffer buffer; - VmaAllocation memory; - VkDevice device; - VkDeviceSize allocSize; - VkBufferUsageFlags usage; - uint32_t memoryTypeIndex; - uint64_t frameNumber; - VkDeviceSize bufferSize; - }; + /// Allocates a VMA-backed VkBuffer with the given usage/memory properties. + /// Constant buffers get CPU-cached memory since every bind reads them, and + /// staging buffers come from the resident staging pool. + void CreateBuffer(VkDeviceSize size, VkBufferUsageFlags usage, + VkMemoryPropertyFlags properties, VkBuffer& buffer, + VmaAllocation& memory, VkDeviceSize* outAllocSize = nullptr, + uint32_t* outMemTypeIdx = nullptr); + + /// Records a GPU-side copy between buffers, batched or submitted immediately + /// per SetBatchingEnabled. + void CopyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size); + + /// Finds a physical device memory type matching the filter and required + /// properties. + uint32_t FindMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties); + + VkDevice m_device = VK_NULL_HANDLE; + VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; + VkCommandPool m_commandPool = VK_NULL_HANDLE; + VkQueue m_graphicsQueue = VK_NULL_HANDLE; + + VkBuffer m_buffer = VK_NULL_HANDLE; + VmaAllocation m_memory = VK_NULL_HANDLE; + VkDeviceSize m_allocSize = 0; // backing memory size (memReq) + VkDeviceSize m_bufferSize = 0; // logical VkBuffer size (bucketed) + VkBufferUsageFlags m_usage = 0; + uint32_t m_memoryTypeIndex = 0; + + VkBuffer m_stagingBuffer = VK_NULL_HANDLE; + VmaAllocation m_stagingMemory = VK_NULL_HANDLE; + + void* m_mappedData = nullptr; + + // --- Batched transfer state --- + static bool s_batchingEnabled; + static bool s_batchActive; + static VkCommandBuffer s_batchCommandBuffer; + static VkDevice s_batchDevice; + static VkCommandPool s_batchCommandPool; + static VkQueue s_batchQueue; + static std::vector s_pendingCleanup; + + /// Opens the shared batch command buffer if one isn't already recording. + static void EnsureBatchStarted(VkDevice device, VkCommandPool pool, + VkQueue queue); + + // --- Deferred buffer deletion --- + /// A buffer queued for destruction once the GPU is guaranteed done referencing + /// it. + struct DeferredBufferDelete { + VkBuffer buffer; + VmaAllocation memory; + VkDevice device; + VkDeviceSize allocSize; + VkBufferUsageFlags usage; + uint32_t memoryTypeIndex; + uint64_t frameNumber; + VkDeviceSize bufferSize; + }; static std::vector s_deferredDeletions; static uint64_t s_frameNumber; @@ -156,18 +163,20 @@ class ENGINE_API VulkanBuffer : public BufferBase { static void EvictPoolOverBudget(); public: - // Called by the renderer each frame after fence wait to safely - // destroy buffers that are no longer referenced by the GPU. - static void ProcessDeferredDeletions(uint32_t maxFramesInFlight); - static void FlushAllDeferredDeletions(); - static void AdvanceDeletionFrame() { s_frameNumber++; } - - // VRAM tracking - static VkDeviceSize GetTotalAllocatedBytes(); - static VkDeviceSize GetDeviceLocalHeapSize(); - static VkDeviceSize GetDeviceLocalAllocatedBytes(); - static void SetPhysicalDevice(VkPhysicalDevice device); - static void UntrackAllocation(VkDeviceSize size) { s_totalAllocatedBytes -= size; } + // Called by the renderer once per submitted frame, after its fence wait, + // to safely destroy buffers that are no longer referenced by the GPU. + static void ProcessDeferredDeletions(uint32_t maxFramesInFlight); + static void FlushAllDeferredDeletions(); + static void AdvanceDeletionFrame() { s_frameNumber++; } + + // VRAM tracking + static VkDeviceSize GetTotalAllocatedBytes(); + static VkDeviceSize GetDeviceLocalHeapSize(); + static VkDeviceSize GetDeviceLocalAllocatedBytes(); + static void SetPhysicalDevice(VkPhysicalDevice device); + static void UntrackAllocation(VkDeviceSize size) { + s_totalAllocatedBytes -= size; + } static void UntrackAllocation(VkDeviceSize size, uint32_t memTypeIdx) { s_totalAllocatedBytes -= size; if (memTypeIdx < s_memTypeCount) diff --git a/include/private/Graphics/Vulkan/VulkanInternal.hpp b/include/private/Graphics/Vulkan/VulkanInternal.hpp index 2d1518a8..20f14996 100644 --- a/include/private/Graphics/Vulkan/VulkanInternal.hpp +++ b/include/private/Graphics/Vulkan/VulkanInternal.hpp @@ -19,35 +19,39 @@ inline float HaltonSeq(int index, int base) { } /// Row-major mat4 multiply: C = A * B. -inline void MatMul4(const float A[16], const float B[16], float C[16]) { +template +void MatMul4(const T A[16], const T B[16], T C[16]) { for (int r = 0; r < 4; ++r) for (int c = 0; c < 4; ++c) { - float s = 0.0f; + T s = T(0); for (int k = 0; k < 4; ++k) s += A[r * 4 + k] * B[k * 4 + c]; C[r * 4 + c] = s; } } /// Row-major 4x4 inverse via Gauss-Jordan elimination. -inline bool InvertMat4(const float M[16], float out[16]) { - float m[4][8]; +template +bool InvertMat4(const T M[16], T out[16]) { + constexpr T kSingularEpsilon = T(1e-7); + T m[4][8]; for (int r = 0; r < 4; ++r) { for (int c = 0; c < 4; ++c) m[r][c] = M[r * 4 + c]; - for (int c = 0; c < 4; ++c) m[r][4 + c] = (r == c) ? 1.0f : 0.0f; + for (int c = 0; c < 4; ++c) m[r][4 + c] = (r == c) ? T(1) : T(0); } for (int col = 0; col < 4; ++col) { - int pivot = -1; float maxv = 0.0f; + int pivot = -1; + T maxv = T(0); for (int row = col; row < 4; ++row) { - float v = std::abs(m[row][col]); + T v = std::abs(m[row][col]); if (v > maxv) { maxv = v; pivot = row; } } - if (pivot < 0 || maxv < 1e-7f) return false; + if (pivot < 0 || maxv < kSingularEpsilon) return false; if (pivot != col) std::swap(m[col], m[pivot]); - float inv = 1.0f / m[col][col]; + T inv = T(1) / m[col][col]; for (int c = 0; c < 8; ++c) m[col][c] *= inv; for (int row = 0; row < 4; ++row) { if (row == col) continue; - float f = m[row][col]; + T f = m[row][col]; for (int c = 0; c < 8; ++c) m[row][c] -= f * m[col][c]; } } diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 46b8170a..cde5d94c 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -1,21 +1,29 @@ #ifndef VULKANRENDERER_HPP #define VULKANRENDERER_HPP -#include "../Common/Renderer.hpp" -#include "../Common/RenderContext.hpp" -#include "Graphics/Vulkan/VulkanShader.hpp" -#include "Graphics/Vulkan/VulkanTexture.hpp" -#include "Core/Logger.hpp" +#include +#include #include -#include "Graphics/Vulkan/VulkanBuffer.hpp" + #include +#include +#include #include -#include #include -#include -#include -#include -#include +#include + +#include "../Common/RenderContext.hpp" +#include "../Common/Renderer.hpp" +#include "Core/Logger.hpp" +#include "Graphics/Vulkan/VulkanBuffer.hpp" +#include "Graphics/Vulkan/VulkanShader.hpp" +#include "Graphics/Vulkan/VulkanTexture.hpp" + +// Shadow caster depth bias +#define SHADOW_DEPTH_BIAS_CONSTANT 1.25f +#define SHADOW_DEPTH_BIAS_SLOPE 1.75f +// WVP + World matrices +#define PUSH_CONSTANT_BYTES (sizeof(float) * 16 * 2) namespace Sleak { class ENGINE_API Window; @@ -57,8 +65,8 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { /// Runs the full Vulkan bring-up sequence: instance, device, swapchain, /// pipelines, and sync objects. virtual bool Initialize() override; - /// Prepares the command buffer and begins the shadow/GBuffer/forward - /// render pass; drawing happens via the RenderContext methods below. + /// Acquires a swapchain image and begins the shadow/GBuffer/forward + /// passes; drawing happens via the RenderContext methods below. virtual void BeginRender() override; /// Ends the active render pass, submits the command buffer, and presents. virtual void EndRender() override; @@ -66,7 +74,8 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { virtual void Cleanup() override; /// Blocks until the device finishes all submitted GPU work. virtual void WaitIdle() override; - /// Kicks off the current frame's async buffer upload batch. + /// Submits the open upload batch for this frame. A frame that failed to + /// start leaves it open, so it rides along with the next submitted frame. virtual void FlushPendingTransfers() override; // GPU memory tracking @@ -165,7 +174,7 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { virtual void BeginSkyboxPass() override; /// Restores the previous pipeline and descriptor set after the skybox draw. virtual void EndSkyboxPass() override; - /// Copies bone matrices into the current frame's UBO and binds its descriptor set. + /// Copies bone matrices into their own bone ring slot and binds it. virtual void BindBoneBuffer(RefPtr buffer) override; /// Binds the skinned pipeline matching the currently active render pass. virtual void BeginSkinnedPass() override; @@ -219,371 +228,453 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { /// Recreates the extent-dependent shadow map objects at the queued resolution. void ApplyShadowResolutionChange() override; + // Measurement + /// Adds per-pass GPU millisecond columns when --gpuprofile is on. + void RegisterBenchmarkMetrics(Benchmark& bench) override; + /// Arms a UI-free readback of the next presented frame into a binary PPM. + bool RequestFrameCapture(const std::string& path) override; + /// True while an armed capture has not been written yet. + bool IsFrameCapturePending() const override { + return !m_capturePath.empty(); + } + private: - /// Compiles the skybox shaders and creates the skybox descriptor set and pipeline. - bool CreateSkyboxPipeline(); - /// Compiles the skinned shaders and creates the forward skinned pipeline. - bool CreateSkinnedPipeline(); - - // Deferred rendering - /// Creates the GBuffer color images, render passes, descriptors, and - /// pipelines that make up the deferred lighting path. - bool CreateGBufferResources(); - /// Creates the GBuffer render pass with its three color attachments and depth. - bool CreateGBufferRenderPass(); - /// Creates the GBuffer framebuffer binding the GBuffer images and depth. - bool CreateGBufferFramebuffer(); - /// Compiles the GBuffer shaders and creates the geometry pipeline, reusing pipelineLay. - bool CreateGBufferPipeline(); - /// Compiles the skinned GBuffer shaders so skinned meshes write into the GBuffer. - bool CreateSkinnedGbufferPipeline(); - /// Creates the deferred lighting render pass with a single color attachment. - bool CreateLightingRenderPass(); - /// Creates one lighting pass framebuffer per swapchain image, all aliasing the HDR target. - bool CreateLightingFramebuffers(); - /// Compiles the lighting shaders and creates the fullscreen lighting pipeline. - bool CreateLightingPipeline(); - /// Creates the forward transparent render pass writing into the HDR scene image. - bool CreateForwardRenderPass(); - /// Creates one forward transparent framebuffer per swapchain image, all aliasing the HDR target. - bool CreateForwardFramebuffers(); - /// Creates the GBuffer sampler descriptor set layout, pool, and per-frame sets. - bool CreateGBufferDescriptorSets(); - /// Creates the per-frame deferred constant buffer holding InvViewProj and screen size. - bool CreateDeferredCBResources(); - /// Creates the PBR material descriptor layout, pool, ring of sets, and GBuffer geometry pipeline layout. - bool CreatePBRMaterialResources(); - /// Appends one chunk of PBR material sets and UBO slots to a frame's ring. - bool AddPBRMaterialChunk(uint32_t frame); - /// Destroys every PBR material chunk of every frame. - void DestroyPBRMaterialChunks(); - /// Creates stub IBL irradiance, prefilter, and BRDF LUT images, samplers, and descriptor set. - bool CreateIBLResources(); - /// Destroys all GBuffer, lighting, and forward transparent pass resources. - void CleanupGBufferResources(); - /// Destroys the IBL images, samplers, and descriptor resources. - void CleanupIBLResources(); - /// Writes the GBuffer, depth, and shadow images into the sampler descriptor sets before the lighting pass. - void UpdateGBufferDescriptors(); - - // Shadow mapping - /// Creates the shadow depth image, sampler, render pass, and framebuffer. - bool CreateShadowResources(); - /// Compiles the shadow depth shader and creates the shadow pass pipeline. - bool CreateShadowPipeline(); - /// Creates the per-frame light and shadow UBO buffers and descriptor sets. - bool CreateShadowLightUBOResources(); - /// Destroys the shadow map image, pipeline, render pass, and light UBO resources. - void CleanupShadowResources(); - /// Creates the Vulkan instance with validation layers when available. - bool InitVulkan(); - /// Creates the SDL-backed Vulkan presentation surface. - bool CreateSurface(); - /// Selects the physical GPU and creates the logical device and queues. - bool CreateDevice(); - /// Creates the swapchain from the queried surface capabilities. - bool CreateSwapChain(); - /// Rebuilds the swapchain and its dependents after resize or resolution change. - bool RecreateSwapChain(); - /// Creates an image view for each swapchain image. - bool CreateImageViews(); - /// Creates the main forward graphics pipeline and its pipeline layout. - bool CreateGraphicsPipeline(); - /// Creates the main forward render pass with optional MSAA color and resolve attachments. - bool CreateRenderPass(); - /// Creates one framebuffer per swapchain image for the main render pass. - bool CreateFrameBuffer(); - /// Creates the graphics command pool. - bool CreateCommandPool(); - /// Allocates one primary command buffer per frame in flight. - bool CreateCommandBuffer(); - /// Creates the per-swapchain-image semaphores and per-frame fences and - /// transfer semaphores. - bool CreateSyncObjects(); - /// Creates the depth image, memory, and image view. - bool CreateDepthResources(); - /// Creates the texture, bone UBO, light UBO, and shadow sampler descriptor set layouts. - bool CreateDescriptorSetLayout(); - /// Creates the descriptor pool backing the per-texture descriptor sets. - bool CreateDescriptorPool(); - /// Allocates one texture descriptor set per swapchain image. - bool AllocateDescriptorSets(); - /// Creates the fallback 1x1 white texture and writes it into the global descriptor sets. - bool CreateDefaultTexture(); - /// Allocates and writes a per-texture descriptor set for the given texture. - void WriteTextureDescriptors(VulkanTexture* texture); - /// Registers the debug messenger callback for validation output. - bool SetupDebugMessenger(); - - /// Destroys the swapchain, its image views, and framebuffers. - void CleanupSwapChain(); - /// Destroys the depth image, memory, and image view. - void CleanupDepthResources(); - - // MSAA resources - /// Creates the MSAA color image used as the multisampled render target. - bool CreateMSAAColorResources(); - /// Destroys the MSAA color image, view, and memory. - void CleanupMSAAColorResources(); - /// Queries the highest MSAA sample count the GPU supports. - VkSampleCountFlagBits GetMaxUsableSampleCount(); - - /// No-op; Vulkan polygon mode changes require pipeline recreation. - virtual void ConfigureRenderMode() override; - /// No-op; Vulkan cull mode changes require pipeline recreation. - virtual void ConfigureRenderFace() override; - - /// Builds one queue create info per unique queue family index. - std::vector - GetUniqueQueueCreateInfos(); - - /// Queries surface capabilities, formats, and present modes. - std::optional QuerySwapchain(); - - /// Picks a UNORM surface format to avoid double sRGB encoding. - VkSurfaceFormatKHR ChooseFormat( - const std::vector& formats); - /// Picks FIFO when VSync is on, otherwise MAILBOX or IMMEDIATE. - VkPresentModeKHR ChoosePresentMode( - const std::vector& modes); - /// Clamps the window size to the surface's supported extent. - VkExtent2D ChooseExtend(SwapchainDetails details); - - /// Picks the first supported depth-stencil format from the candidate list. - VkFormat FindDepthFormat(); - /// Finds a memory type index matching the filter and property flags. - uint32_t FindMemoryType(uint32_t typeFilter, - VkMemoryPropertyFlags properties); - - /// Fills the debug messenger create info with severity and callback. - void PopulateDebugMessengerCreateInfo( - VkDebugUtilsMessengerCreateInfoEXT& createInfo); - - /// Debug messenger callback that routes Vulkan messages to the logger. - static VkBool32 Validation( - VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, - VkDebugUtilsMessageTypeFlagsEXT messageTypes, - const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, - void* pUserData); - - bool bRender = true; - bool bFrameStarted = false; - - static constexpr uint32_t MAX_FRAMES_IN_FLIGHT = 2; - uint32_t currentFrame = 0; - uint32_t CurrentFrameIndex = 0; - uint32_t m_semaphoreIndex = 0; // cycles through swapchain image count - VkInstance instance = VK_NULL_HANDLE; - VkSurfaceKHR surface = VK_NULL_HANDLE; - VkRenderPass renderPass = VK_NULL_HANDLE; - VkSwapchainKHR swapChain = VK_NULL_HANDLE; - VkCommandPool commands = VK_NULL_HANDLE; - std::vector commandBuffers; - VkCommandBuffer command = VK_NULL_HANDLE; // alias for commandBuffers[currentFrame] - std::vector swapChainFramebuffers; - std::vector swapChainImages; - std::vector swapChainImageViews; - VkFormat scImageFormat; - VkExtent2D scExtent; - VkDevice device = VK_NULL_HANDLE; - VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; - std::vector GPUs; - VkPipeline pipeline = VK_NULL_HANDLE; - VkPipelineLayout pipelineLay = VK_NULL_HANDLE; - std::vector imageAvailableSemaphores; - std::vector renderFinishedSemaphores; - std::vector inFlightFences; - std::vector imagesInFlight; - VulkanShader* simpleShader = nullptr; - VkClearValue clearColor; - - // Depth buffer - VkImage depthImage = VK_NULL_HANDLE; - VkDeviceMemory depthImageMemory = VK_NULL_HANDLE; - VkImageView depthImageView = VK_NULL_HANDLE; - VkFormat depthFormat; - - // MSAA color buffer (multisample resolve target) - VkSampleCountFlagBits m_msaaSamples = VK_SAMPLE_COUNT_1_BIT; - VkImage m_msaaColorImage = VK_NULL_HANDLE; - VkDeviceMemory m_msaaColorImageMemory = VK_NULL_HANDLE; - VkImageView m_msaaColorImageView = VK_NULL_HANDLE; - - // Descriptor sets for uniform buffers - VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE; - VkDescriptorPool descriptorPool = VK_NULL_HANDLE; - std::vector descriptorSets; - - QueueIndices QueueIDs; - VkQueue graphicsQueue = VK_NULL_HANDLE; - VkQueue computeQueue = VK_NULL_HANDLE; - VkQueue transferQueue = VK_NULL_HANDLE; - VkQueue presentQueue = VK_NULL_HANDLE; - - VkDebugUtilsMessengerEXT debugMessenger = VK_NULL_HANDLE; - PFN_vkDestroyDebugUtilsMessengerEXT vkDestroyDebugUtilsMessengerEXT = - nullptr; - - Window* sdlWindow; - - // Texture binding - bool m_textureDescriptorsWritten = false; - VulkanTexture* m_defaultTexture = nullptr; - - // Skybox pipeline - VkPipeline skyboxPipeline = VK_NULL_HANDLE; - VulkanShader* skyboxShader = nullptr; - VkDescriptorPool skyboxDescriptorPool = VK_NULL_HANDLE; - std::vector skyboxDescriptorSets; - bool m_skyboxDescriptorsWritten = false; - VkImageView m_skyboxCubemapView = VK_NULL_HANDLE; // cached for MSAA re-bind - VkSampler m_skyboxCubemapSampler = VK_NULL_HANDLE; // cached for MSAA re-bind - - // Skinned pipeline (uses skinned shaders with bone UBO) - VkPipeline skinnedPipeline = VK_NULL_HANDLE; - VulkanShader* skinnedShader = nullptr; - - // Debug line pipeline - VkPipeline debugLinePipeline = VK_NULL_HANDLE; - VulkanShader* debugLineShader = nullptr; - /// Compiles the debug line shaders and creates the line-list pipeline. - bool CreateDebugLinePipeline(); - - // Custom vertex format pipelines (built lazily per registered VertexFormatHandle) - /// The four pipeline variants a registered vertex layout can drive. - /// A failed flag marks a variant as permanently absent so draws skip it - /// instead of retrying compilation every frame. - struct CustomFormatPipelines { - VkPipeline main = VK_NULL_HANDLE; - VkPipeline shadow = VK_NULL_HANDLE; - VkPipeline gbuffer = VK_NULL_HANDLE; - VkPipeline transparent = VK_NULL_HANDLE; - bool mainFailed = false; - bool shadowFailed = false; - bool gbufferFailed = false; - bool transparentFailed = false; - }; - std::unordered_map - m_customFormatPipelines; - VertexFormatHandle m_activeCustomFormat = 0; - /// Set while the active custom format has no pipeline for the current pass; - /// draws are dropped rather than issued against a mismatched vertex layout. - bool m_customFormatUnbound = false; - /// True while the bound vertex buffer's format has no pipeline for this pass. - bool CustomFormatDrawsSuppressed() const; - /// Creates any missing pipeline variant for a registered format; returns false when the main variant is unusable. - bool CreateCustomFormatPipelines(VertexFormatHandle format); - /// Destroys every cached custom-format pipeline (all variants, all formats). - void DestroyCustomFormatPipelines(); - - // Bone UBO (for skeletal animation — set 1, binding 0) - VkDescriptorSetLayout boneDescriptorSetLayout = VK_NULL_HANDLE; - VkDescriptorPool boneDescriptorPool = VK_NULL_HANDLE; - std::array boneUBOBuffers = {}; - std::array boneUBOMemory = {}; - std::array boneUBOMapped = {}; - std::array boneDescriptorSets = {}; - bool m_boneUBOCreated = false; - /// Creates the per-frame bone UBO buffers and their descriptor sets. - bool CreateBoneUBOResources(); - /// Destroys the bone UBO buffers, memory, and descriptor pool. - void CleanupBoneUBOResources(); - - // ImGUI - VkDescriptorPool imguiDescriptorPool = VK_NULL_HANDLE; - - // Shadow mapping resources - VkImage m_shadowImage = VK_NULL_HANDLE; - VkDeviceMemory m_shadowImageMemory = VK_NULL_HANDLE; - VkImageView m_shadowImageView = VK_NULL_HANDLE; - VkSampler m_shadowSampler = VK_NULL_HANDLE; // compare sampler (hardware PCF) - VkSampler m_shadowRawSampler = VK_NULL_HANDLE; // non-compare sampler (PCSS blocker search) - VkRenderPass m_shadowRenderPass = VK_NULL_HANDLE; - VkFramebuffer m_shadowFramebuffer = VK_NULL_HANDLE; - VkPipeline m_shadowPipeline = VK_NULL_HANDLE; - VulkanShader* m_shadowShader = nullptr; - bool m_shadowPassActive = false; - // m_shadowResourcesCreated lives in the Renderer base (shared with the - // shadow-resolution change-request logic) - - // Shadow push-constant memo: LightVP*World is frame-constant per unique - // World, so cache it and skip the matmul when consecutive casters (all - // draws share the identity transform) reuse the same World matrix. - float m_shadowWorldCache[16] = {}; - float m_shadowPCCache[32] = {}; - bool m_shadowPCCacheValid = false; - - // Light VP matrix (stored as raw floats for push constant computation) - float m_lightVP[16] = {}; - // Staging slot: SetLightVP writes here; BeginRender copies it to - // m_lightVP before the shadow pass. Keeps m_lightVP stable for the - // entire frame so shadow pass and main pass agree on the transform. - float m_pendingLightVP[16] = {}; - bool m_hasPendingLightVP = false; - - // Light/Shadow UBO (set 2, binding 0) - VkDescriptorSetLayout m_lightUBODescriptorSetLayout = VK_NULL_HANDLE; - VkDescriptorPool m_lightUBODescriptorPool = VK_NULL_HANDLE; - std::array m_lightUBOBuffers = {}; - std::array m_lightUBOMemory = {}; - std::array m_lightUBOMapped = {}; - std::array m_lightUBODescriptorSets = {}; - - // Shadow map sampler descriptor (set 3, binding 0) - VkDescriptorSetLayout m_shadowSamplerDescriptorSetLayout = VK_NULL_HANDLE; - VkDescriptorPool m_shadowSamplerDescriptorPool = VK_NULL_HANDLE; - std::array m_shadowSamplerDescriptorSets = {}; - bool m_lightUBOCreated = false; - - // Async buffer transfer (zero-CPU-blocking GPU uploads) - std::array m_transferSemaphores = {}; - std::array m_asyncFlush; - - // ---- Deferred GBuffer ---- - // 3 color attachments: RT0=AlbedoAO, RT1=NormalRough, RT2=MetalEmit. - // World position is reconstructed from the depth buffer + InvViewProj in - // the lighting/SSAO/SSR passes (no RGBA32F worldpos RT) to cut GBuffer - // bandwidth on a fill-bound renderer. - static constexpr uint32_t GBUFFER_COUNT = 3; - VkImage m_gbufferImages[GBUFFER_COUNT] = {}; - VkDeviceMemory m_gbufferMemory[GBUFFER_COUNT] = {}; - VkImageView m_gbufferViews[GBUFFER_COUNT] = {}; - /// GBuffer attachment formats: RT0 AlbedoAO, RT1 NormalRough, RT2 MetalEmit. Defined in VulkanDeferred.cpp. - static const VkFormat m_gbufferFormats[GBUFFER_COUNT]; - VkRenderPass m_gbufferRenderPass = VK_NULL_HANDLE; - VkFramebuffer m_gbufferFramebuffer = VK_NULL_HANDLE; - VkPipeline m_gbufferPipeline = VK_NULL_HANDLE; - VkPipeline m_skinnedGbufferPipeline = VK_NULL_HANDLE; - VkPipelineLayout m_gbufferPipelineLayout = VK_NULL_HANDLE; - VulkanShader* m_gbufferShader = nullptr; - bool m_gbufferResourcesCreated = false; - bool m_inGeometryPass = false; - - // Lighting pass - VkRenderPass m_lightingRenderPass = VK_NULL_HANDLE; - std::vector m_lightingFramebuffers; // one per swapchain image - VkPipeline m_lightingPipeline = VK_NULL_HANDLE; - VkPipelineLayout m_lightingPipelineLayout = VK_NULL_HANDLE; - VulkanShader* m_lightingShader = nullptr; - - // Forward transparent pass - VkRenderPass m_forwardRenderPass = VK_NULL_HANDLE; - std::vector m_forwardFramebuffers; // one per swapchain image (color+depth, non-MSAA) - bool m_inForwardTransparentPass = false; - bool m_forwardPassOpen = false; // true when forward RP is currently recording - - // GBuffer sampler descriptor set (set 0 in lighting pass) - VkDescriptorSetLayout m_gbufferSamplerDSL = VK_NULL_HANDLE; - VkDescriptorPool m_gbufferSamplerPool = VK_NULL_HANDLE; - std::array m_gbufferSamplerSets = {}; - VkSampler m_gbufferSampler = VK_NULL_HANDLE; - VkSampler m_depthSampler = VK_NULL_HANDLE; - - // Deferred CB UBO (set 1 in lighting pass): InvViewProj + screenSize + near/far - struct DeferredCBData { - float InvViewProj[16]; - float ScreenW, ScreenH, NearP, FarP; - }; + /// Compiles the skybox shaders on first use, sizes the skybox descriptor + /// sets to the swapchain image count, and creates the skybox pipeline. + bool CreateSkyboxPipeline(); + /// Compiles the skinned shaders on first use and creates the forward + /// skinned pipeline. + bool CreateSkinnedPipeline(); + + // Deferred rendering + /// Creates the GBuffer color images, render passes, descriptors, and + /// pipelines that make up the deferred lighting path. + bool CreateGBufferResources(); + /// Creates the GBuffer render pass with its three color attachments and depth. + bool CreateGBufferRenderPass(); + /// Creates the GBuffer framebuffer binding the GBuffer images and depth. + bool CreateGBufferFramebuffer(); + /// Compiles the GBuffer shaders and creates the geometry pipeline, reusing + /// pipelineLay. + bool CreateGBufferPipeline(); + /// Compiles the skinned GBuffer shaders so skinned meshes write into the + /// GBuffer. + bool CreateSkinnedGbufferPipeline(); + /// Creates the deferred lighting render pass with a single color attachment. + bool CreateLightingRenderPass(); + /// Creates one lighting pass framebuffer per swapchain image, all aliasing the + /// HDR target. + bool CreateLightingFramebuffers(); + /// Compiles the lighting shaders and creates the fullscreen lighting pipeline. + bool CreateLightingPipeline(); + /// Creates the forward transparent render pass writing into the HDR scene + /// image. + bool CreateForwardRenderPass(); + /// Creates one forward transparent framebuffer per swapchain image, all + /// aliasing the HDR target. + bool CreateForwardFramebuffers(); + /// Creates the GBuffer sampler descriptor set layout, pool, and per-frame + /// sets. + bool CreateGBufferDescriptorSets(); + /// Creates the per-frame deferred constant buffer holding InvViewProj and + /// screen size. + bool CreateDeferredCBResources(); + /// Creates the PBR material descriptor layout, pool, ring of sets, and GBuffer + /// geometry pipeline layout. + bool CreatePBRMaterialResources(); + /// Appends one chunk of PBR material sets and UBO slots to a frame's ring. + bool AddPBRMaterialChunk(uint32_t frame); + /// Destroys every PBR material chunk of every frame. + void DestroyPBRMaterialChunks(); + /// Creates stub IBL irradiance, prefilter, and BRDF LUT images, samplers, and + /// descriptor set. + bool CreateIBLResources(); + /// Destroys all GBuffer, lighting, and forward transparent pass resources. + void CleanupGBufferResources(); + /// Destroys the IBL images, samplers, and descriptor resources. + void CleanupIBLResources(); + /// Writes the GBuffer, depth, and shadow images into the sampler descriptor + /// sets before the lighting pass. + void UpdateGBufferDescriptors(); + + // Shadow mapping + /// Creates the shadow depth image, sampler, render pass, and framebuffer. + bool CreateShadowResources(); + /// Compiles the shadow depth shader and creates the shadow pass pipeline. + bool CreateShadowPipeline(); + /// Creates the per-frame light and shadow UBO buffers and descriptor sets. + bool CreateShadowLightUBOResources(); + /// Destroys the shadow map image, pipeline, render pass, and light UBO + /// resources. + void CleanupShadowResources(); + /// Creates the Vulkan instance. The validation layer is on in Debug builds + /// or with --validate, and --syncval adds synchronization validation. + bool InitVulkan(); + /// Creates the SDL-backed Vulkan presentation surface. + bool CreateSurface(); + /// Selects the physical GPU and creates the logical device and queues. + bool CreateDevice(); + /// Creates the swapchain from the queried surface capabilities. + bool CreateSwapChain(); + /// Rebuilds the swapchain and its dependents after resize or resolution + /// change. + bool RecreateSwapChain(); + /// True when the surface wants a different extent than the swapchain. + bool SurfaceExtentChanged(); + /// Creates an image view for each swapchain image. + bool CreateImageViews(); + /// Creates the layout shared by the forward, skybox, debug line, skinned, + /// and shadow pipelines. + bool CreateMainPipelineLayout(); + /// Creates the main forward graphics pipeline, plus its layout and shader + /// on first use. + bool CreateGraphicsPipeline(); + /// Creates the pipeline cache, seeded from disk when the saved blob was + /// written by this device and driver. + void CreatePipelineCache(); + /// Saves the pipeline cache to disk when it changed, then destroys it. + void DestroyPipelineCache(); + /// Creates the main forward render pass with optional MSAA color and resolve + /// attachments. + bool CreateRenderPass(); + /// Creates one framebuffer per swapchain image for the main render pass. + bool CreateFrameBuffer(); + /// Creates the graphics command pool. + bool CreateCommandPool(); + /// Allocates one primary command buffer per frame in flight. + bool CreateCommandBuffer(); + /// Creates the per-swapchain-image semaphores and per-frame fences and + /// transfer semaphores. + bool CreateSyncObjects(); + /// Grows the per-image semaphores to the swapchain image count and + /// clears the image-to-fence table. + bool CreateImageSyncObjects(); + /// Creates the depth image, memory, and image view. + bool CreateDepthResources(); + /// Creates the texture, bone UBO, light UBO, and shadow sampler descriptor set + /// layouts. + bool CreateDescriptorSetLayout(); + /// Creates the descriptor pool backing the per-texture descriptor sets. + bool CreateDescriptorPool(); + /// Allocates one texture descriptor set per swapchain image. + bool AllocateDescriptorSets(); + /// Creates the fallback 1x1 white texture and writes it into the global + /// descriptor sets. + bool CreateDefaultTexture(); + /// Allocates and writes a per-texture descriptor set for the given texture. + void WriteTextureDescriptors(VulkanTexture* texture); + /// Registers the debug messenger callback when the validation layer is on. + bool SetupDebugMessenger(); + + /// Destroys the swapchain, its image views, and framebuffers. + void CleanupSwapChain(); + /// Destroys the depth image, memory, and image view. + void CleanupDepthResources(); + + // MSAA resources + /// Creates the MSAA color image used as the multisampled render target. + bool CreateMSAAColorResources(); + /// Destroys the MSAA color image, view, and memory. + void CleanupMSAAColorResources(); + /// Queries the highest MSAA sample count the GPU supports. + VkSampleCountFlagBits GetMaxUsableSampleCount(); + + /// No-op; Vulkan polygon mode changes require pipeline recreation. + virtual void ConfigureRenderMode() override; + /// No-op; Vulkan cull mode changes require pipeline recreation. + virtual void ConfigureRenderFace() override; + + /// Builds one queue create info per unique queue family index. + std::vector GetUniqueQueueCreateInfos(); + + /// Queries surface capabilities, formats, and present modes. + std::optional QuerySwapchain(); + + /// Picks a UNORM surface format to avoid double sRGB encoding. + VkSurfaceFormatKHR ChooseFormat( + const std::vector& formats); + /// Picks FIFO when VSync is on, otherwise MAILBOX or IMMEDIATE. + VkPresentModeKHR ChoosePresentMode(const std::vector& modes); + /// Clamps the window size to the surface's supported extent. + VkExtent2D ChooseExtend(const VkSurfaceCapabilitiesKHR& caps); + + /// Picks the first supported depth-stencil format from the candidate list. + VkFormat FindDepthFormat(); + /// Finds a memory type index matching the filter and property flags. + uint32_t FindMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties); + + /// Fills the debug messenger create info with severity and callback. + void PopulateDebugMessengerCreateInfo( + VkDebugUtilsMessengerCreateInfoEXT& createInfo); + + /// Debug messenger callback that routes Vulkan messages to the logger. + static VkBool32 Validation( + VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, + VkDebugUtilsMessageTypeFlagsEXT messageTypes, + const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, + void* pUserData); + + bool bRender = true; + bool bFrameStarted = false; + + static constexpr uint32_t MAX_FRAMES_IN_FLIGHT = 2; + uint32_t currentFrame = 0; + uint32_t CurrentFrameIndex = 0; + uint32_t m_semaphoreIndex = 0; // cycles through swapchain image count + VkInstance instance = VK_NULL_HANDLE; + VkSurfaceKHR surface = VK_NULL_HANDLE; + VkRenderPass renderPass = VK_NULL_HANDLE; + VkSwapchainKHR swapChain = VK_NULL_HANDLE; + VkCommandPool commands = VK_NULL_HANDLE; + std::vector commandBuffers; + VkCommandBuffer command = + VK_NULL_HANDLE; // alias for commandBuffers[currentFrame] + std::vector swapChainFramebuffers; + std::vector swapChainImages; + std::vector swapChainImageViews; + VkFormat scImageFormat; + VkExtent2D scExtent; + VkDevice device = VK_NULL_HANDLE; + VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; + std::vector GPUs; + VkPipeline pipeline = VK_NULL_HANDLE; + VkPipelineLayout pipelineLay = VK_NULL_HANDLE; + VkPipelineCache m_pipelineCache = VK_NULL_HANDLE; + // Loaded blob size and hash, so an unchanged cache is not rewritten + size_t m_pipelineCacheLoadedSize = 0; + uint64_t m_pipelineCacheLoadedHash = 0; + std::vector imageAvailableSemaphores; + std::vector renderFinishedSemaphores; + std::vector inFlightFences; + std::vector imagesInFlight; + // Submission serial of the last frame recorded in each frame slot + std::array m_slotSerial{}; + uint64_t m_submitSerial = 0; + VulkanShader* simpleShader = nullptr; + VkClearValue clearColor; + + // Depth buffer + VkImage depthImage = VK_NULL_HANDLE; + VkDeviceMemory depthImageMemory = VK_NULL_HANDLE; + VkImageView depthImageView = VK_NULL_HANDLE; + VkFormat depthFormat; + + // MSAA color buffer (multisample resolve target) + VkSampleCountFlagBits m_msaaSamples = VK_SAMPLE_COUNT_1_BIT; + VkImage m_msaaColorImage = VK_NULL_HANDLE; + VkDeviceMemory m_msaaColorImageMemory = VK_NULL_HANDLE; + VkImageView m_msaaColorImageView = VK_NULL_HANDLE; + + // Descriptor sets for uniform buffers + VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE; + VkDescriptorPool descriptorPool = VK_NULL_HANDLE; + std::vector descriptorSets; + + QueueIndices QueueIDs; + VkQueue graphicsQueue = VK_NULL_HANDLE; + VkQueue computeQueue = VK_NULL_HANDLE; + VkQueue transferQueue = VK_NULL_HANDLE; + VkQueue presentQueue = VK_NULL_HANDLE; + + VkDebugUtilsMessengerEXT debugMessenger = VK_NULL_HANDLE; + PFN_vkDestroyDebugUtilsMessengerEXT vkDestroyDebugUtilsMessengerEXT = nullptr; + bool m_validationEnabled = false; + + Window* sdlWindow; + + // Texture binding + bool m_textureDescriptorsWritten = false; + VulkanTexture* m_defaultTexture = nullptr; + + // Skybox pipeline + VkPipeline skyboxPipeline = VK_NULL_HANDLE; + VulkanShader* skyboxShader = nullptr; + VkDescriptorPool skyboxDescriptorPool = VK_NULL_HANDLE; + std::vector skyboxDescriptorSets; + bool m_skyboxDescriptorsWritten = false; + VkImageView m_skyboxCubemapView = VK_NULL_HANDLE; // cached for MSAA re-bind + VkSampler m_skyboxCubemapSampler = VK_NULL_HANDLE; // cached for MSAA re-bind + uint32_t m_skyboxViewSerial = 0; // bumped whenever the cached view changes + + // Skinned pipeline (uses skinned shaders with bone UBO) + VkPipeline skinnedPipeline = VK_NULL_HANDLE; + VulkanShader* skinnedShader = nullptr; + + // Debug line pipeline + VkPipeline debugLinePipeline = VK_NULL_HANDLE; + VulkanShader* debugLineShader = nullptr; + /// Compiles the debug line shaders and creates the line-list pipeline. + bool CreateDebugLinePipeline(); + + // Custom vertex format pipelines (one per registered VertexFormatHandle) + /// Render pass a custom-format pipeline variant is built for. + enum class CustomPass : uint8_t { Main, Shadow, GBuffer, Transparent, Count }; + static constexpr size_t CUSTOM_PASS_COUNT = + static_cast(CustomPass::Count); + /// The pipeline variants a registered vertex layout can drive, indexed by + /// CustomPass. A failed flag marks a variant as permanently absent so + /// draws skip it instead of retrying compilation every frame. + struct CustomFormatPipelines { + std::array pipelines{}; + std::array failed{}; + }; + // Indexed by handle - 1 (handles are dense and start at 1) + std::vector m_customFormatPipelines; + // Registered formats whose active-path variants were prebuilt + VertexFormatHandle m_customFormatsPrebuilt = 0; + VertexFormatHandle m_activeCustomFormat = 0; + /// Set while the active custom format has no pipeline for the current pass; + /// draws are dropped rather than issued against a mismatched vertex layout. + bool m_customFormatUnbound = false; + /// True while the bound vertex buffer's format has no pipeline for this pass. + bool CustomFormatDrawsSuppressed() const; + /// True when the render pass a custom-format variant targets exists. + bool CustomPassAvailable(CustomPass pass) const; + /// Returns a format's pipeline for a pass, compiling it on first request. + VkPipeline GetCustomFormatPipeline(VertexFormatHandle format, CustomPass pass); + /// Compiles one pipeline variant of a registered layout; returns + /// VK_NULL_HANDLE when its stem is empty or compilation fails. + VkPipeline BuildCustomFormatPipeline(VertexFormatHandle format, + const VertexLayoutDesc& desc, + CustomPass pass); + /// Compiles the variants the active render path draws with for every + /// registered format not prebuilt yet. + void PrebuildCustomFormatPipelines(); + /// Destroys every cached custom-format pipeline (all variants, all formats). + void DestroyCustomFormatPipelines(); + + // Bone palette ring (set 1, binding 0, dynamic offset per bind) + struct BoneUBOChunk { + VkDescriptorPool pool = VK_NULL_HANDLE; + VkDescriptorSet set = VK_NULL_HANDLE; + VkBuffer buffer = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + void* mapped = nullptr; + }; + static constexpr uint32_t BONE_MATRIX_BYTES = sizeof(float) * 16; + static constexpr uint32_t BONE_PALETTE_BYTES = MAX_BONES * BONE_MATRIX_BYTES; + static constexpr uint32_t BONE_SLOTS_PER_CHUNK = 32; + VkDescriptorSetLayout boneDescriptorSetLayout = VK_NULL_HANDLE; + std::array, MAX_FRAMES_IN_FLIGHT> m_boneChunks; + VkDeviceSize m_boneSlotStride = BONE_PALETTE_BYTES; + uint32_t m_boneSlot[MAX_FRAMES_IN_FLIGHT] = {}; + bool m_boneUBOCreated = false; + /// Creates each frame's first bone ring chunk; slot 0 holds identity. + bool CreateBoneUBOResources(); + /// Appends BONE_SLOTS_PER_CHUNK palette slots to one frame's bone ring. + bool AddBoneUBOChunk(uint32_t frame); + /// Destroys every bone ring chunk of every frame. + void CleanupBoneUBOResources(); + /// Binds the identity palette (ring slot 0) at set 1. + void BindIdentityBonePalette(VkPipelineLayout layout); + + // ImGUI + VkDescriptorPool imguiDescriptorPool = VK_NULL_HANDLE; + + // Shadow mapping resources + VkImage m_shadowImage = VK_NULL_HANDLE; + VkDeviceMemory m_shadowImageMemory = VK_NULL_HANDLE; + VkImageView m_shadowImageView = VK_NULL_HANDLE; + VkSampler m_shadowSampler = VK_NULL_HANDLE; // compare sampler (hardware PCF) + VkSampler m_shadowRawSampler = + VK_NULL_HANDLE; // non-compare sampler (PCSS blocker search) + VkRenderPass m_shadowRenderPass = VK_NULL_HANDLE; + VkFramebuffer m_shadowFramebuffer = VK_NULL_HANDLE; + VkPipeline m_shadowPipeline = VK_NULL_HANDLE; + VulkanShader* m_shadowShader = nullptr; + bool m_shadowPassActive = false; + // m_shadowResourcesCreated lives in the Renderer base (shared with the + // shadow-resolution change-request logic) + + // Shadow push-constant memo: LightVP*World is frame-constant per unique + // World, so cache it and skip the matmul when consecutive casters (all + // draws share the identity transform) reuse the same World matrix. + float m_shadowWorldCache[16] = {}; + float m_shadowPCCache[32] = {}; + bool m_shadowPCCacheValid = false; + // Layout m_shadowPCCache was last pushed with in this shadow pass, or + // null once anything else overwrote the push constants. + VkPipelineLayout m_shadowPCPushedLayout = VK_NULL_HANDLE; + + // Light VP matrix (stored as raw floats for push constant computation) + float m_lightVP[16] = {}; + // Staging slot: SetLightVP writes here; BeginRender copies it to + // m_lightVP before the shadow pass. Keeps m_lightVP stable for the + // entire frame so shadow pass and main pass agree on the transform. + float m_pendingLightVP[16] = {}; + bool m_hasPendingLightVP = false; + // Matrix the shadow map was last rendered with + float m_shadowMapLightVP[16] = {}; + bool m_shadowMapRendered = false; + + // Light/Shadow UBO (set 2, binding 0) + VkDescriptorSetLayout m_lightUBODescriptorSetLayout = VK_NULL_HANDLE; + VkDescriptorPool m_lightUBODescriptorPool = VK_NULL_HANDLE; + std::array m_lightUBOBuffers = {}; + std::array m_lightUBOMemory = {}; + std::array m_lightUBOMapped = {}; + std::array m_lightUBODescriptorSets = + {}; + + // Shadow map sampler descriptor (set 3, binding 0) + VkDescriptorSetLayout m_shadowSamplerDescriptorSetLayout = VK_NULL_HANDLE; + VkDescriptorPool m_shadowSamplerDescriptorPool = VK_NULL_HANDLE; + std::array + m_shadowSamplerDescriptorSets = {}; + bool m_lightUBOCreated = false; + + // Async buffer transfer (zero-CPU-blocking GPU uploads) + std::array m_transferSemaphores = {}; + std::array m_asyncFlush; + + // ---- Deferred GBuffer ---- + // 3 color attachments: RT0=AlbedoAO, RT1=NormalRough, RT2=MetalEmit. + // World position is reconstructed from the depth buffer + InvViewProj in + // the lighting/SSAO/SSR passes (no RGBA32F worldpos RT) to cut GBuffer + // bandwidth on a fill-bound renderer. + static constexpr uint32_t GBUFFER_COUNT = 3; + VkImage m_gbufferImages[GBUFFER_COUNT] = {}; + VkDeviceMemory m_gbufferMemory[GBUFFER_COUNT] = {}; + VkImageView m_gbufferViews[GBUFFER_COUNT] = {}; + /// GBuffer attachment formats: RT0 AlbedoAO, RT1 NormalRough, RT2 MetalEmit. + /// Defined in VulkanDeferred.cpp. + static const VkFormat m_gbufferFormats[GBUFFER_COUNT]; + VkRenderPass m_gbufferRenderPass = VK_NULL_HANDLE; + VkFramebuffer m_gbufferFramebuffer = VK_NULL_HANDLE; + VkPipeline m_gbufferPipeline = VK_NULL_HANDLE; + VkPipeline m_skinnedGbufferPipeline = VK_NULL_HANDLE; + VkPipelineLayout m_gbufferPipelineLayout = VK_NULL_HANDLE; + VulkanShader* m_gbufferShader = nullptr; + bool m_gbufferResourcesCreated = false; + bool m_inGeometryPass = false; + + // Lighting pass + VkRenderPass m_lightingRenderPass = VK_NULL_HANDLE; + std::vector m_lightingFramebuffers; // one per swapchain image + VkPipeline m_lightingPipeline = VK_NULL_HANDLE; + VkPipelineLayout m_lightingPipelineLayout = VK_NULL_HANDLE; + VulkanShader* m_lightingShader = nullptr; + + // Forward transparent pass + VkRenderPass m_forwardRenderPass = VK_NULL_HANDLE; + std::vector + m_forwardFramebuffers; // one per swapchain image (color+depth, non-MSAA) + bool m_inForwardTransparentPass = false; + bool m_forwardPassOpen = false; // true when forward RP is currently recording + + // GBuffer sampler descriptor set (set 0 in lighting pass) + VkDescriptorSetLayout m_gbufferSamplerDSL = VK_NULL_HANDLE; + VkDescriptorPool m_gbufferSamplerPool = VK_NULL_HANDLE; + std::array m_gbufferSamplerSets = {}; + VkSampler m_gbufferSampler = VK_NULL_HANDLE; + VkSampler m_depthSampler = VK_NULL_HANDLE; + + // Deferred CB UBO (set 1 in lighting pass): InvViewProj + screenSize + + // near/far + struct DeferredCBData { + float InvViewProj[16]; + float ScreenW, ScreenH, NearP, FarP; + }; VkDescriptorSetLayout m_deferredCBDSL = VK_NULL_HANDLE; VkDescriptorPool m_deferredCBPool = VK_NULL_HANDLE; std::array m_deferredCBBuffers = {}; @@ -795,10 +886,10 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { // ---- TAA (Temporal Anti-Aliasing) resources ---- // Ping-pong history accumulation with depth-based reprojection - // and 3x3 neighborhood AABB clamping. + // and 3x3 YCoCg variance clipping. struct alignas(16) TAAParams { - float InvCurrentVP[16]; // inverse of unjittered current VP - float PrevVP[16]; // previous frame unjittered VP + float CurrentToPrevClip[16]; // inverse(current VP) * previous VP, + // unjittered float ScreenW, ScreenH; float BlendFactor; float _pad; @@ -807,6 +898,7 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { bool m_taaResourcesCreated = false; // m_taaEnabled is inherited from RenderEngine::Renderer (base class). uint64_t m_taaFrameIdx = 0; // global counter; ping-pong = idx % 2 + bool m_taaRanLastFrame = false; // false restarts accumulation VkImage m_taaImages[2] = {}; VkDeviceMemory m_taaMemory[2] = {}; @@ -815,6 +907,8 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { VkFramebuffer m_taaFramebufs[2] = {}; VkSampler m_taaSampler = VK_NULL_HANDLE; + VkImageView m_taaResolvedView = VK_NULL_HANDLE; // null if TAA is off + VkDescriptorSetLayout m_taaInputDSL = VK_NULL_HANDLE; VkDescriptorSetLayout m_taaUboDSL = VK_NULL_HANDLE; VkDescriptorPool m_taaPool = VK_NULL_HANDLE; @@ -829,7 +923,7 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { VkPipeline m_taaPipeline = VK_NULL_HANDLE; VulkanShader* m_taaShader = nullptr; - float m_prevViewProj[16] = {}; // previous frame unjittered VP (row-major) + double m_prevViewProj[16] = {}; // previous frame unjittered VP (row-major) float m_taaJitter[2] = {}; // current frame jitter in UV space /// Creates the ping-pong TAA history images, render pass, framebuffers, descriptors, and pipeline. @@ -838,8 +932,10 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { void CleanupTAAResources(); /// Computes the inverse current view-projection and reprojection blend factor into the TAA UBO. void UpdateTAAUBO(); - /// Resolves the current frame against TAA history and copies the result back into the HDR scene image. + /// Resolves the frame against TAA history into the ping-pong target. void RenderTAAPass(); + /// HDR scene color for the post passes: the TAA resolve when TAA ran. + VkImageView PostSceneView() const; // Per-image "fallback content is valid" flags. Set true once the image is // primed (static black/white in SHADER_READ_ONLY) by either @@ -931,6 +1027,151 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { /// Sets the dynamic viewport and scissor to fill the given extent. Defined in VulkanBloom.cpp (most call sites of the four post-effect TUs). static void FillFullscreenViewportScissor(VkCommandBuffer cmd, VkExtent2D ext); + + // ---- GPU timestamp profiler (--gpuprofile) ---- + /// Frame passes bracketed by timestamp pairs, in log-table order. + enum class GpuPass : uint8_t { + Shadow, + GBuffer, + SSAO, + Lighting, + Forward, + TAA, + SSR, + Bloom, + Tonemap, + UI, + Count + }; + static constexpr uint32_t GPU_PASS_COUNT = + static_cast(GpuPass::Count); + static constexpr uint32_t GPU_ROW_OTHER = GPU_PASS_COUNT; + static constexpr uint32_t GPU_ROW_TOTAL = GPU_PASS_COUNT + 1; + static constexpr uint32_t GPU_ROW_COUNT = GPU_PASS_COUNT + 2; + static constexpr uint32_t GPU_MAX_PAIRS = 16; + static constexpr uint32_t GPU_QUERIES_PER_FRAME = 2 + 2 * GPU_MAX_PAIRS; + + /// Timestamp pairs recorded into one frame-in-flight slot of the pool. + struct GpuProfileSlot { + std::array pairPass{}; + uint32_t pairCount = 0; + bool pairOpen = false; + bool recording = false; + bool submitted = false; + }; + + VkQueryPool m_gpuQueryPool = VK_NULL_HANDLE; + bool m_gpuProfilerEnabled = false; + double m_gpuTickNs = 0.0; + uint64_t m_gpuTickMask = 0; + std::array m_gpuSlots{}; + std::array m_gpuSumMs{}; + std::array m_gpuMaxMs{}; + std::array m_gpuRuns{}; + std::array m_gpuLastMs{}; + uint32_t m_gpuWindowFrames = 0; + Timer m_gpuWindowTimer; + + /// Creates the timestamp query pool; leaves profiling off when the + /// graphics queue has no timestamp support. + bool CreateGpuProfiler(); + /// Destroys the timestamp query pool. + void CleanupGpuProfiler(); + /// Folds this slot's finished timestamps into the running window and + /// logs the table once the window elapses. Call after the slot's fence. + void CollectGpuProfile(); + /// Resets this slot's queries and stamps the frame start. + void BeginGpuFrame(); + /// Closes any pass left open and stamps the frame end. + void EndGpuFrame(); + /// Flags this slot's queries for readback on its next fence wait. + void MarkGpuFrameSubmitted(); + /// Stamps the start of a pass. + void BeginGpuPass(GpuPass pass); + /// Stamps the end of the pass opened by the matching BeginGpuPass. + void EndGpuPass(GpuPass pass); + /// Logs the averaged per-pass table for the current window. + void LogGpuProfile(float seconds) const; + + // ---- Frame capture (-capture) ---- + bool m_captureMode = false; + bool m_swapchainTransferSrc = false; + std::string m_capturePath; + bool m_captureRecorded = false; + VkBuffer m_captureBuffer = VK_NULL_HANDLE; + VkDeviceMemory m_captureMemory = VK_NULL_HANDLE; + VkExtent2D m_captureExtent = {0, 0}; + VkFormat m_captureFormat = VK_FORMAT_UNDEFINED; + + /// Records the swapchain-image-to-buffer copy for an armed capture. + void RecordFrameCaptureCopy(); + /// Waits for the captured frame, writes the PPM, and disarms the capture. + void FinishFrameCapture(); + /// Frees the capture readback buffer. + void CleanupFrameCapture(); + + // ---- Scene read for transparent formats (set 3, bindings 2-5) ---- + // Set 3 holds the shadow map samplers first, then the scene-read inputs + static constexpr uint32_t SCENE_COLOR_BINDING = 2; + static constexpr uint32_t SCENE_DEPTH_BINDING = 3; + static constexpr uint32_t SCENE_SKY_BINDING = 4; + static constexpr uint32_t SCENE_UBO_BINDING = 5; + static constexpr uint32_t SET3_BINDING_COUNT = SCENE_UBO_BINDING + 1; + // Every set 3 binding but the UBO is a combined image sampler + static constexpr uint32_t SET3_SAMPLER_COUNT = SET3_BINDING_COUNT - 1; + static constexpr uint32_t SCENE_READ_IMAGE_COUNT = + SCENE_UBO_BINDING - SCENE_COLOR_BINDING; + // Light pool uniform buffers per frame: set 2 light UBO, set 3 SceneReadUBO + static constexpr uint32_t LIGHT_POOL_UBOS_PER_FRAME = 2; + // Snapshots of the HDR scene and depth taken mid forward pass + VkImage m_sceneColorCopyImage = VK_NULL_HANDLE; + VkDeviceMemory m_sceneColorCopyMemory = VK_NULL_HANDLE; + VkImageView m_sceneColorCopyView = VK_NULL_HANDLE; + VkImage m_sceneDepthCopyImage = VK_NULL_HANDLE; + VkDeviceMemory m_sceneDepthCopyMemory = VK_NULL_HANDLE; + VkImageView m_sceneDepthCopyView = VK_NULL_HANDLE; + // 1x1 stand-ins bound while there is no snapshot or sky + VkImage m_sceneFallbackImage = VK_NULL_HANDLE; + VkDeviceMemory m_sceneFallbackMemory = VK_NULL_HANDLE; + VkImageView m_sceneFallbackView = VK_NULL_HANDLE; + VkImage m_sceneFallbackCubeImage = VK_NULL_HANDLE; + VkDeviceMemory m_sceneFallbackCubeMemory = VK_NULL_HANDLE; + VkImageView m_sceneFallbackCubeView = VK_NULL_HANDLE; + VkSampler m_sceneLinearSampler = VK_NULL_HANDLE; + VkSampler m_scenePointSampler = VK_NULL_HANDLE; + std::array m_sceneReadUBOBuffers{}; + std::array m_sceneReadUBOMemory{}; + std::array m_sceneReadUBOMapped{}; + /// What one frame's set 3 bindings 2-4 last pointed at; a cleared entry + /// forces a rewrite. + struct SceneReadViews { + VkImageView color = VK_NULL_HANDLE; + VkImageView depth = VK_NULL_HANDLE; + VkImageView sky = VK_NULL_HANDLE; + uint32_t skySerial = 0; + }; + std::array m_sceneReadWritten{}; + bool m_sceneReadCreated = false; + bool m_sceneReadPrepared = false; // UBO filled (and copy taken) this frame + float m_cachedNearPlane = 0.0f; + float m_cachedFarPlane = 0.0f; + + /// Creates the scene-read samplers, stand-in images, and per-frame UBOs + /// and writes them into both frames' set 3. + bool CreateSceneReadResources(); + /// Destroys everything CreateSceneReadResources made plus the snapshots. + void CleanupSceneReadResources(); + /// Creates the extent-sized scene color and depth snapshot images. + bool CreateSceneCopyImages(); + /// Destroys the snapshot images and forces a set 3 rewrite. + void CleanupSceneCopyImages(); + /// Points a frame's set 3 bindings 2-4 at the current snapshot and sky. + void UpdateSceneReadDescriptors(uint32_t frame); + /// Fills this frame's SceneReadUBO and, inside the forward pass, splits it + /// to copy the scene. Runs once per frame on the first scene-reading draw. + void PrepareSceneRead(); + /// Records the scene color and depth copies between two forward passes. + void RecordSceneCopy(); }; } // namespace RenderEngine diff --git a/include/private/Graphics/Vulkan/VulkanTexture.hpp b/include/private/Graphics/Vulkan/VulkanTexture.hpp index eea2a970..c9cff5d4 100644 --- a/include/private/Graphics/Vulkan/VulkanTexture.hpp +++ b/include/private/Graphics/Vulkan/VulkanTexture.hpp @@ -43,12 +43,24 @@ class ENGINE_API VulkanTexture : public Texture { VkImageView GetImageView() const { return m_imageView; } VkSampler GetSampler() const { return m_sampler; } - // Per-texture descriptor sets (one per swapchain image) - void SetDescriptorSets(std::vector sets) { m_descriptorSets = std::move(sets); } + /// Takes ownership of per-swapchain-image descriptor sets allocated from + /// pool, releasing any sets the texture held before. + void SetDescriptorSets(std::vector sets, + VkDescriptorPool pool); const std::vector& GetDescriptorSets() const { return m_descriptorSets; } bool HasDescriptorSets() const { return !m_descriptorSets.empty(); } -private: + /// Stamps textures released from now on with the serial of the next + /// frame submission, the last one that may still reference them. + static void SetReleaseSerial(uint64_t serial); + /// Destroys released textures whose stamped submission has completed. + static void DestroyReleased(uint64_t completedSerial); + /// Destroys every released texture; the caller guarantees the GPU is idle. + static void DestroyAllReleased(); + + private: + /// Hands the descriptor sets and GPU objects to the release queue so they + /// outlive any frame still in flight. void Cleanup(); /// Writes this texture's image view/sampler into each of its per-frame descriptor sets. void UpdateDescriptorSets(); @@ -78,6 +90,7 @@ class ENGINE_API VulkanTexture : public Texture { VkSampler m_sampler = VK_NULL_HANDLE; std::vector m_descriptorSets; + VkDescriptorPool m_descriptorPool = VK_NULL_HANDLE; mutable VkDescriptorSet m_imguiDescriptorSet = VK_NULL_HANDLE; uint32_t m_width = 0; diff --git a/include/public/Core/Application.hpp b/include/public/Core/Application.hpp index 896cafaa..b847a51f 100644 --- a/include/public/Core/Application.hpp +++ b/include/public/Core/Application.hpp @@ -154,6 +154,10 @@ class ENGINE_API Application { bool GetVSync() const; void SetVSync(bool enabled); + /// Deferred rendering + const bool GetIsDeferredEnabled() const; + void SetDeferredEnabled(bool enabled); + // Screen-space ambient occlusion (deferred path only) bool IsSSAOEnabled() const; void SetSSAOEnabled(bool enabled); @@ -178,6 +182,11 @@ class ENGINE_API Application { bool IsTAAEnabled() const; void SetTAAEnabled(bool enabled); + /// Post-TAA contrast-adaptive sharpening strength, 0 = off, 1 = strongest. + float GetSharpenStrength() const; + /// Sets post-TAA sharpening strength; only applied on frames TAA resolves. + void SetSharpenStrength(float strength); + // Bloom (deferred path only) bool IsBloomEnabled() const; void SetBloomEnabled(bool enabled); diff --git a/include/public/Lighting/LightManager.hpp b/include/public/Lighting/LightManager.hpp index 02887700..cffded7e 100644 --- a/include/public/Lighting/LightManager.hpp +++ b/include/public/Lighting/LightManager.hpp @@ -72,6 +72,12 @@ namespace Sleak { /// Refreshes the deferred-pass constant buffer (fog, ambient) independent of the per-light data. void UpdateDeferredCB(); + /// Light view-projection the next shadow pass renders with (row-major, + /// row vectors), or nullptr when no light casts shadows. + const float* GetShadowLightVP() const { + return m_hasShadowLightVP ? m_shadowLightVP : nullptr; + } + void SetAmbientColor(float r, float g, float b); void SetAmbientIntensity(float intensity) { m_ambientIntensity = intensity; @@ -124,6 +130,10 @@ namespace Sleak { float m_ambientB = 0.1f; float m_ambientIntensity = 1.0f; + // Last light VP staged on the renderer + float m_shadowLightVP[16] = {}; + bool m_hasShadowLightVP = false; + // Fog bool m_fogEnabled = true; float m_fogR = 0.62f, m_fogG = 0.74f, m_fogB = 0.88f; // horizon (warm-cool sky tint) diff --git a/include/public/Runtime/VertexLayout.hpp b/include/public/Runtime/VertexLayout.hpp index a37a3a98..5e97d5be 100644 --- a/include/public/Runtime/VertexLayout.hpp +++ b/include/public/Runtime/VertexLayout.hpp @@ -69,6 +69,33 @@ struct VertexAttribute { /// Sleak::VertexFormatRegistry::Register(desc); /// @endcode /// +/// Set `transparentReadsScene` for transparent geometry that refracts or +/// reflects what lies behind it, such as water. The first transparent draw +/// of the format in a frame snapshots the scene drawn so far, and its +/// shaders can then read it: +/// - Vulkan, descriptor set 3: binding 2 scene color (linear HDR), +/// binding 3 scene depth, binding 4 sky cubemap, binding 5 the +/// SceneReadUBO block below. +/// - OpenGL: texture unit 4 scene color (tonemapped, as shown), unit 5 +/// scene depth, unit 6 sky cubemap, uniform block binding 4 SceneReadUBO. +/// +/// @code{.glsl} +/// uniform SceneReadUBO { +/// mat4 ViewProj; // unjittered, row-major engine matrix +/// mat4 InvViewProj; +/// vec4 ScreenSize; // width, height, 1/width, 1/height +/// vec4 Camera; // TAA jitter in NDC (xy), near, far +/// vec4 Flags; // x: scene textures valid, y: sky valid +/// }; +/// @endcode +/// +/// Add `Camera.xy * gl_Position.w` to the clip position after the Vulkan Y +/// flip so the draw lines up with the jittered opaque geometry. +/// +/// On Vulkan, transparent draws set the HDR scene alpha to 0. TAA reads that +/// as a reactive mask and trusts history less there, since animated or +/// refracted content is not described by the depth buffer. +/// /// @see VertexFormatRegistry, VertexAttribute, VertexAttribFormat, MeshBatch /// @ingroup vertexformats struct VertexLayoutDesc { @@ -81,6 +108,8 @@ struct VertexLayoutDesc { std::string gbufferShaderStem; // Forward transparent variant; empty string skips the transparent pass. std::string transparentShaderStem; + // Transparent draws sample the opaque scene; see above. + bool transparentReadsScene = false; }; /// 0 is reserved for the engine's default vertex layout; registered diff --git a/src/Core/Application.cpp b/src/Core/Application.cpp index 83c0eae5..86e21a6d 100644 --- a/src/Core/Application.cpp +++ b/src/Core/Application.cpp @@ -34,6 +34,8 @@ #include #include +#define DEFAULT_CAPTURE_FRAME 600 + using namespace Sleak; using namespace Sleak::Math; @@ -141,6 +143,7 @@ namespace Sleak { m_benchmark = new Benchmark(); m_benchmark->Initialize(renderer); + renderer->RegisterBenchmarkMetrics(*m_benchmark); { auto& cfg = m_DebugOverlay->GetConfig(); cfg.ShowCameraPanel = false; @@ -152,7 +155,27 @@ namespace Sleak { float lastTime = FrameTimer.Elapsed(); float accumulator = 0.0f; - const float fixedTimestep = 1.0f / 60.0f; + const float fixedTimestep = 1.0f / 60.0f; + + // Frame capture + const std::string capturePath = CommandLine::GetValue("-capture"); + const bool captureMode = !capturePath.empty(); + uint64_t captureFrame = DEFAULT_CAPTURE_FRAME; + const std::string frameArg = + CommandLine::GetValue("-capture-frame"); + if (!frameArg.empty()) { + try { + captureFrame = std::stoull(frameArg); + } catch (...) { + } + } + if (captureMode) + SLEAK_INFO( + "Frame capture: frame {} -> {} (fixed 1/60 s step, " + "resizes ignored)", + captureFrame, capturePath); + uint64_t frameIndex = 0; + bool captureArmed = false; // Initialize and begin the game (and scene) if (Game) { @@ -187,7 +210,8 @@ namespace Sleak { while(!CoreWindow->ShouldClose()) { float currentTime = FrameTimer.Elapsed(); - DeltaTime = currentTime - lastTime; + DeltaTime = + captureMode ? fixedTimestep : currentTime - lastTime; lastTime = currentTime; #if defined(_DEBUG) && defined(COUNT_FRAME) @@ -197,7 +221,7 @@ namespace Sleak { CoreWindow->Update(); // Apply any pending resize (deferred from event handler to avoid GPU hang) - if (m_pendingResize) { + if (m_pendingResize && !captureMode) { renderer->Resize(m_pendingResizeW, m_pendingResizeH); width = static_cast(m_pendingResizeW); height = static_cast(m_pendingResizeH); @@ -212,6 +236,16 @@ namespace Sleak { } } + if (captureMode && !captureArmed && + frameIndex >= captureFrame) { + captureArmed = true; + if (!renderer->RequestFrameCapture(capturePath)) { + SLEAK_ERROR("Frame capture unavailable on {}", + renderer->GetTypeStr()); + CoreWindow->Close(); + } + } + renderer->BeginRender(); // Update active scene if present @@ -247,6 +281,10 @@ namespace Sleak { queue->ExecuteCommands(context); renderer->EndRender(); + + if (captureArmed && !renderer->IsFrameCapturePending()) + CoreWindow->Close(); + ++frameIndex; } } else{ @@ -387,6 +425,13 @@ namespace Sleak { bool Application::GetVSync() const { return renderer->GetVSync(); } void Application::SetVSync(bool enabled) { renderer->SetVSync(enabled); } + const bool Application::GetIsDeferredEnabled() const { + return renderer->GetDeferredEnabled(); + } + void Application::SetDeferredEnabled(bool enable) { + renderer->SetDeferredEnabled(enable); + } + bool Application::IsSSAOEnabled() const { return renderer->IsSSAOEnabled(); } void Application::SetSSAOEnabled(bool e) { renderer->SetSSAOEnabled(e); } float Application::GetSSAORadius() const { return renderer->GetSSAORadius(); } @@ -407,6 +452,13 @@ namespace Sleak { bool Application::IsTAAEnabled() const { return renderer->IsTAAEnabled(); } void Application::SetTAAEnabled(bool e) { renderer->SetTAAEnabled(e); } + float Application::GetSharpenStrength() const { + return renderer->GetSharpenStrength(); + } + void Application::SetSharpenStrength(float s) { + renderer->SetSharpenStrength(s); + } + bool Application::IsBloomEnabled() const { return renderer->IsBloomEnabled(); } void Application::SetBloomEnabled(bool e) { renderer->SetBloomEnabled(e); } @@ -421,6 +473,7 @@ namespace Sleak { renderer->SetIBLEnabled(cfg.iblEnabled); renderer->SetIBLIntensity(cfg.iblIntensity); renderer->SetTAAEnabled(cfg.taaEnabled); + renderer->SetSharpenStrength(cfg.sharpenStrength); renderer->SetShadowMapResolution(cfg.shadowMapResolution); renderer->SetMSAASampleCount(cfg.msaaSamples); renderer->SetTonemappingEnabled(cfg.tonemapEnabled); diff --git a/src/Core/Window.cpp b/src/Core/Window.cpp index e8c18cbf..d82b8d88 100644 --- a/src/Core/Window.cpp +++ b/src/Core/Window.cpp @@ -1,16 +1,43 @@ #include "../../include/private/Core/Window.hpp" -#include "../../include/private/Graphics/Common/RendererFactory.hpp" -#include -#include +#include #include #include #include #include +#include +#include +#include "../../include/private/Graphics/Common/RendererFactory.hpp" namespace Sleak { +namespace { + +/// True for keyboard and mouse events that carry user input. +bool IsUserInputEvent(Uint32 type) { + switch (type) { + case SDL_EVENT_KEY_DOWN: + case SDL_EVENT_KEY_UP: + case SDL_EVENT_TEXT_INPUT: + case SDL_EVENT_MOUSE_MOTION: + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: + case SDL_EVENT_MOUSE_WHEEL: + return true; + default: + return false; + } +} + +/// SDL event filter that drops user input while a capture runs. +bool SDLCALL DropUserInput(void* userdata, SDL_Event* event) { + (void)userdata; + return !IsUserInputEvent(event->type); +} + +} // namespace + int Window::Width = 0; int Window::Height = 0; @@ -37,6 +64,8 @@ bool Window::InitializeWindow() { if(bIsInitialized) return false; + m_inputLocked = !CommandLine::GetValue("-capture").empty(); + auto type = RenderEngine::RendererFactory::GetRendererType(); int GraphicsAPI = 0; @@ -50,13 +79,15 @@ bool Window::InitializeWindow() { SLEAK_FATAL("Failed to initialize SDL: {0}", SDL_GetError()); return false; } + if (m_inputLocked) SDL_SetEventFilter(DropUserInput, nullptr); - SDLWindow = SDL_CreateWindow(WindowName.c_str(), Width, Height, GraphicsAPI | SDL_WINDOW_RESIZABLE); + SDLWindow = SDL_CreateWindow(WindowName.c_str(), Width, Height, + GraphicsAPI | SDL_WINDOW_RESIZABLE); - if (!SDLWindow) { - SLEAK_FATAL("Failed to create window! {0} ", SDL_GetError()); - return false; - } + if (!SDLWindow) { + SLEAK_FATAL("Failed to create window! {0} ", SDL_GetError()); + return false; + } if (SDL_Surface* icon = SDL_LoadBMP("assets/branding/icon.bmp")) { SDL_SetWindowIcon(SDLWindow, icon); @@ -180,6 +211,8 @@ void Window::Update() { } } } + + if (m_inputLocked) SDL_GetRelativeMouseState(nullptr, nullptr); } void Window::SetFullScreen(bool isFullscreen) { diff --git a/src/Graphics/OpenGL/OpenGLRenderer.cpp b/src/Graphics/OpenGL/OpenGLRenderer.cpp index 9f9dfdac..48c316d3 100644 --- a/src/Graphics/OpenGL/OpenGLRenderer.cpp +++ b/src/Graphics/OpenGL/OpenGLRenderer.cpp @@ -117,6 +117,7 @@ void OpenGLRenderer::SetupVertexLayout() { } void OpenGLRenderer::BeginRender() { + m_sceneReadPrepared = false; if (m_vsyncChangeRequested) ApplyVSyncChange(); if (m_msaaChangeRequested) @@ -189,12 +190,14 @@ void OpenGLRenderer::EndRender() { void OpenGLRenderer::Cleanup() { if (m_Initialized) { + CleanupSceneRead(); CleanupGBufferResources(); if (m_shadowTransformUBO) { glDeleteBuffers(1, &m_shadowTransformUBO); m_shadowTransformUBO = 0; } if (m_shadowUBO) { glDeleteBuffers(1, &m_shadowUBO); m_shadowUBO = 0; } if (m_shadowDepthTex) { glDeleteTextures(1, &m_shadowDepthTex); m_shadowDepthTex = 0; } if (m_shadowFBO) { glDeleteFramebuffers(1, &m_shadowFBO); m_shadowFBO = 0; } m_shadowMapCreated = false; + m_shadowMapRendered = false; m_shadowUBOCreated = false; CleanupMSAAFramebuffer(); if (bImInitialized) { @@ -369,6 +372,10 @@ void OpenGLRenderer::BindVertexBuffer(RefPtr buffer, const VertexLayoutDesc* customDesc = customFormat != 0 ? VertexFormatRegistry::Get(customFormat) : nullptr; + if (customDesc && customDesc->transparentReadsScene && + m_inForwardTransparentPass) + PrepareSceneRead(); + if (customDesc) { // One attribute pointer per registry entry, tracking which slots stay live const GLsizei stride = static_cast(customDesc->stride); @@ -688,7 +695,10 @@ void OpenGLRenderer::UpdateShadowLightUBO(const void* data, uint32_t size) { const auto* ubo = static_cast(data); RenderEngine::PCSSShadowGPUData shadowData{}; - memcpy(shadowData.LightVP, ubo->LightVP, sizeof(float) * 16); + // Sample with the map's own matrix + memcpy(shadowData.LightVP, + m_shadowMapRendered ? m_shadowMapLightVP : ubo->LightVP, + sizeof(shadowData.LightVP)); memcpy(shadowData.NdcToShadow, ubo->NdcToShadow, sizeof(float) * 16); shadowData.ShadowBias = ubo->ShadowBias; shadowData.ShadowStrength = ubo->ShadowStrength; @@ -780,6 +790,8 @@ void OpenGLRenderer::RenderShadowPass() { queue->ExecuteShadowPass(this); } m_inShadowPass = false; + memcpy(m_shadowMapLightVP, m_lightVP, sizeof(m_shadowMapLightVP)); + m_shadowMapRendered = true; // Restore state glDisable(GL_POLYGON_OFFSET_FILL); @@ -933,20 +945,7 @@ void OpenGLRenderer::CleanupGBufferResources() { // IBL — lazy bake from active scene's skybox cubemap. // First call (or when the skybox cubemap changes) reruns the precompute. void OpenGLRenderer::EnsureIBL() { - auto* app = Application::GetInstance(); - if (!app) return; - auto* game = app->GetGame(); - if (!game) return; - auto* scene = game->GetActiveScene(); - if (!scene) return; - auto* skybox = scene->GetSkybox(); - if (!skybox || !skybox->IsInitialized()) return; - auto* cube = skybox->GetCubemapTexture(); - if (!cube) return; - auto* glCube = dynamic_cast(cube); - if (!glCube) return; - - GLuint cubeHandle = glCube->GetGLTexture(); + GLuint cubeHandle = FindSkyboxCubemap(); if (cubeHandle == 0) return; if (!m_ibl) m_ibl = new OpenGLIBL(); @@ -1048,6 +1047,13 @@ void OpenGLRenderer::BindPBRMaterial(Sleak::Material* material) { void OpenGLRenderer::UpdateDeferredCB(const void* data, uint32_t size) { if (!m_deferredCBCreated) return; + if (data && size >= sizeof(DeferredCBData)) { + const auto* cb = static_cast(data); + memcpy(m_cachedInvViewProj, cb->InvViewProj, + sizeof(m_cachedInvViewProj)); + m_cachedNearPlane = cb->NearPlane; + m_cachedFarPlane = cb->FarPlane; + } glBindBuffer(GL_UNIFORM_BUFFER, m_deferredCBUBO); glBufferSubData(GL_UNIFORM_BUFFER, 0, size, data); glBindBuffer(GL_UNIFORM_BUFFER, 0); diff --git a/src/Graphics/OpenGL/OpenGLSceneRead.cpp b/src/Graphics/OpenGL/OpenGLSceneRead.cpp new file mode 100644 index 00000000..6df289b9 --- /dev/null +++ b/src/Graphics/OpenGL/OpenGLSceneRead.cpp @@ -0,0 +1,137 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "../../include/private/Graphics/OpenGL/OpenGLCubemapTexture.hpp" +#include "../../include/private/Graphics/OpenGL/OpenGLRenderer.hpp" +#include "Graphics/Common/ConstantBuffer.hpp" + +namespace Sleak { +namespace RenderEngine { + +namespace { + +constexpr GLenum CUBE_FACE_COUNT = 6; +constexpr GLsizei STAND_IN_SIZE = 1; + +} // namespace + +// ================================================================== +// Scene read for transparent formats (VertexLayoutDesc::transparentReadsScene) +// ================================================================== + +/// Returns the active scene's skybox cubemap, or 0 when there is none. +GLuint OpenGLRenderer::FindSkyboxCubemap() const { + auto* app = Application::GetInstance(); + auto* game = app ? app->GetGame() : nullptr; + auto* scene = game ? game->GetActiveScene() : nullptr; + auto* skybox = scene ? scene->GetSkybox() : nullptr; + if (!skybox || !skybox->IsInitialized()) return 0; + auto* cube = + dynamic_cast(skybox->GetCubemapTexture()); + return cube ? cube->GetGLTexture() : 0; +} + +/// Snapshots the scene once per frame on the first scene-reading draw and +/// binds the snapshot, depth, sky, and SceneReadUBO for every such draw. +void OpenGLRenderer::PrepareSceneRead() { + // Creation binds go to the units rebound below + if (m_sceneFallbackCube == 0) { + glGenTextures(1, &m_sceneFallbackCube); + glActiveTexture(GL_TEXTURE0 + SCENE_SKY_UNIT); + glBindTexture(GL_TEXTURE_CUBE_MAP, m_sceneFallbackCube); + const uint32_t black = 0; + for (GLenum face = 0; face < CUBE_FACE_COUNT; ++face) + glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, GL_RGBA8, + STAND_IN_SIZE, STAND_IN_SIZE, 0, GL_RGBA, + GL_UNSIGNED_BYTE, &black); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAX_LEVEL, 0); + } + if (m_sceneCopyTex == 0) { + glGenTextures(1, &m_sceneCopyTex); + glActiveTexture(GL_TEXTURE0 + SCENE_COLOR_UNIT); + glBindTexture(GL_TEXTURE_2D, m_sceneCopyTex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + } + if (m_sceneReadUBO == 0) { + glGenBuffers(1, &m_sceneReadUBO); + glBindBuffer(GL_UNIFORM_BUFFER, m_sceneReadUBO); + glBufferData(GL_UNIFORM_BUFFER, sizeof(SceneReadUBO), nullptr, + GL_DYNAMIC_DRAW); + } + + if (!m_sceneReadPrepared) { + m_sceneReadPrepared = true; + m_sceneSkyCube = FindSkyboxCubemap(); + const int w = m_gbufferWidth; + const int h = m_gbufferHeight; + const bool valid = m_gbufferCreated && IsDeferredEnabled() && + m_gbufferDepth != 0 && w > 0 && h > 0; + + // Copy of the default framebuffer as drawn so far + if (valid) { + glActiveTexture(GL_TEXTURE0 + SCENE_COLOR_UNIT); + glBindTexture(GL_TEXTURE_2D, m_sceneCopyTex); + if (w != m_sceneCopyWidth || h != m_sceneCopyHeight) { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, + GL_UNSIGNED_BYTE, nullptr); + m_sceneCopyWidth = w; + m_sceneCopyHeight = h; + } + glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, w, h); + } + + // Camera block + const Math::Matrix4 viewProj = + Camera::GetMainViewMatrix() * Camera::GetMainProjectionMatrix(); + SceneReadUBO ubo{}; + std::memcpy(ubo.ViewProj, &viewProj(0, 0), sizeof(ubo.ViewProj)); + std::memcpy(ubo.InvViewProj, m_cachedInvViewProj, + sizeof(ubo.InvViewProj)); + ubo.ScreenWidth = static_cast(std::max(w, 1)); + ubo.ScreenHeight = static_cast(std::max(h, 1)); + ubo.InvScreenWidth = 1.0f / ubo.ScreenWidth; + ubo.InvScreenHeight = 1.0f / ubo.ScreenHeight; + ubo.NearPlane = m_cachedNearPlane; + ubo.FarPlane = m_cachedFarPlane; + ubo.SceneValid = static_cast(valid); + ubo.SkyValid = static_cast(m_sceneSkyCube != 0); + glBindBuffer(GL_UNIFORM_BUFFER, m_sceneReadUBO); + glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(ubo), &ubo); + } + + glBindBufferBase(GL_UNIFORM_BUFFER, SCENE_READ_UBO_BINDING, m_sceneReadUBO); + glActiveTexture(GL_TEXTURE0 + SCENE_COLOR_UNIT); + glBindTexture(GL_TEXTURE_2D, m_sceneCopyTex); + glActiveTexture(GL_TEXTURE0 + SCENE_DEPTH_UNIT); + glBindTexture(GL_TEXTURE_2D, m_gbufferDepth); + glActiveTexture(GL_TEXTURE0 + SCENE_SKY_UNIT); + glBindTexture(GL_TEXTURE_CUBE_MAP, + m_sceneSkyCube != 0 ? m_sceneSkyCube : m_sceneFallbackCube); + glActiveTexture(GL_TEXTURE0); +} + +/// Deletes the snapshot texture, stand-in cubemap, and UBO. +void OpenGLRenderer::CleanupSceneRead() { + if (m_sceneCopyTex) glDeleteTextures(1, &m_sceneCopyTex); + if (m_sceneFallbackCube) glDeleteTextures(1, &m_sceneFallbackCube); + if (m_sceneReadUBO) glDeleteBuffers(1, &m_sceneReadUBO); + m_sceneCopyTex = 0; + m_sceneFallbackCube = 0; + m_sceneReadUBO = 0; + m_sceneCopyWidth = 0; + m_sceneCopyHeight = 0; +} + +} // namespace RenderEngine +} // namespace Sleak diff --git a/src/Graphics/Vulkan/VulkanBloom.cpp b/src/Graphics/Vulkan/VulkanBloom.cpp index f1e50b57..0bef3477 100644 --- a/src/Graphics/Vulkan/VulkanBloom.cpp +++ b/src/Graphics/Vulkan/VulkanBloom.cpp @@ -45,12 +45,9 @@ bool VulkanRenderer::CreateHDRSceneResources() { info.format = m_hdrSceneFormat; info.tiling = VK_IMAGE_TILING_OPTIMAL; info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - // TRANSFER_DST_BIT: TAA copies its resolved result back into hdrScene. // TRANSFER_SRC_BIT: MSAA resolve from the forward render pass. - info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT - | VK_IMAGE_USAGE_SAMPLED_BIT - | VK_IMAGE_USAGE_TRANSFER_DST_BIT - | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; info.samples = VK_SAMPLE_COUNT_1_BIT; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; if (vkCreateImage(device, &info, nullptr, &m_hdrSceneImage) != VK_SUCCESS) return false; @@ -332,7 +329,7 @@ bool VulkanRenderer::CreateBloomPipelines() { VkPushConstantRange compPushRange{}; compPushRange.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; compPushRange.offset = 0; - compPushRange.size = 16; + compPushRange.size = 32; VkPipelineLayoutCreateInfo cliInfo{}; cliInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; @@ -433,7 +430,9 @@ bool VulkanRenderer::CreateBloomPipelines() { gpi.layout = m_bloomFilterPipelineLayout; gpi.renderPass = m_bloomRenderPass; gpi.subpass = 0; - if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpi, nullptr, &m_bloomThresholdPipeline) != VK_SUCCESS) return false; + if (vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &gpi, nullptr, + &m_bloomThresholdPipeline) != VK_SUCCESS) + return false; // ---- Downsample pipeline ---- VkPipelineShaderStageCreateInfo downStages[] = { @@ -441,7 +440,9 @@ bool VulkanRenderer::CreateBloomPipelines() { m_bloomDownsampleShader->GetFragInfo() }; gpi.pStages = downStages; - if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpi, nullptr, &m_bloomDownsamplePipeline) != VK_SUCCESS) return false; + if (vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &gpi, nullptr, + &m_bloomDownsamplePipeline) != VK_SUCCESS) + return false; // ---- Upsample pipeline (additive blend, LOAD render pass) ---- VkPipelineShaderStageCreateInfo upStages[] = { @@ -451,7 +452,9 @@ bool VulkanRenderer::CreateBloomPipelines() { gpi.pStages = upStages; gpi.pColorBlendState = &cbAdd; gpi.renderPass = m_bloomAddRenderPass; - if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpi, nullptr, &m_bloomUpsamplePipeline) != VK_SUCCESS) return false; + if (vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &gpi, nullptr, + &m_bloomUpsamplePipeline) != VK_SUCCESS) + return false; // ---- Composite pipeline ---- VkPipelineShaderStageCreateInfo compStages[] = { @@ -462,7 +465,9 @@ bool VulkanRenderer::CreateBloomPipelines() { gpi.pColorBlendState = &cbOpaque; gpi.layout = m_bloomCompositePipelineLayout; gpi.renderPass = m_bloomCompositeRenderPass; - if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpi, nullptr, &m_bloomCompositePipeline) != VK_SUCCESS) return false; + if (vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &gpi, nullptr, + &m_bloomCompositePipeline) != VK_SUCCESS) + return false; return true; } @@ -609,11 +614,13 @@ void VulkanRenderer::RenderBloomPass() { // [MIP..MIP*2-2] : upsamples (bloomMip[MIP-1-k] -> bloomMip[MIP-2-k]) // BLOOM_TRANSITION_COUNT = 1 + (MIP-1) + (MIP-1) = 11 for MIP=6. auto& setArray = m_bloomFilterSets[currentFrame]; + BeginGpuPass(GpuPass::Bloom); // ---------- 1. Threshold / prefilter pass ---------- - // Input: sceneHDR (already SHADER_READ_ONLY_OPTIMAL via forward RP finalLayout) + // Input: scene color (TAA resolve or HDR scene, SHADER_READ_ONLY_OPTIMAL) // Output: bloomMip[0] - WriteSingleImageSampler(device, setArray[0], m_hdrSceneView, m_bloomSampler, + WriteSingleImageSampler(device, setArray[0], PostSceneView(), + m_bloomSampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); { @@ -695,6 +702,7 @@ void VulkanRenderer::RenderBloomPass() { vkCmdDraw(command, 3, 1, 0, 0); vkCmdEndRenderPass(command); } + EndGpuPass(GpuPass::Bloom); } /// Tonemaps and composites the HDR scene, bloom, and SSR into the swapchain image, then draws ImGui. @@ -708,7 +716,7 @@ void VulkanRenderer::RenderBloomCompositePass() { VkDescriptorSet compSet = m_bloomCompositeSets[currentFrame]; std::array infos{}; infos[0].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - infos[0].imageView = m_hdrSceneView; + infos[0].imageView = PostSceneView(); infos[0].sampler = m_bloomSampler; infos[1].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; @@ -720,7 +728,8 @@ void VulkanRenderer::RenderBloomCompositePass() { // is disabled). Fall back to the bloom mip view only when SSR resources // haven't been created yet (should never happen once init completes). infos[2].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - if (m_ssrResourcesCreated && m_ssrView != VK_NULL_HANDLE) { + const bool ssrReady = m_ssrResourcesCreated && m_ssrView != VK_NULL_HANDLE; + if (ssrReady) { infos[2].imageView = m_ssrView; infos[2].sampler = m_ssrSampler ? m_ssrSampler : m_bloomSampler; } else { @@ -748,6 +757,7 @@ void VulkanRenderer::RenderBloomCompositePass() { rp.renderArea.extent = scExtent; rp.clearValueCount = 0; + BeginGpuPass(GpuPass::Tonemap); vkCmdBeginRenderPass(command, &rp, VK_SUBPASS_CONTENTS_INLINE); FillFullscreenViewportScissor(command, scExtent); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_bloomCompositePipeline); @@ -755,24 +765,38 @@ void VulkanRenderer::RenderBloomCompositePass() { m_bloomCompositePipelineLayout, 0, 1, &compSet, 0, nullptr); struct { - float bloomStrength, exposure, gamma, p1; - } pc; + float bloomStrength, exposure, gamma; + uint32_t ssrEnabled; + float sharpenStrength; + float _pad[3]; + } pc{}; // bloom disabled -> 0 so the (black) mip contributes nothing pc.bloomStrength = m_bloomEnabled ? 0.06f : 0.0f; // UE4-style soft bloom pc.exposure = m_exposure; pc.gamma = m_tonemapEnabled ? m_gamma : 1.0f; - pc.p1 = 0.0f; + pc.ssrEnabled = (ssrReady && m_ssrEnabled) ? 1u : 0u; + // Sharpening an aliased (non-TAA) frame only amplifies the stair-steps + pc.sharpenStrength = + (m_taaResolvedView != VK_NULL_HANDLE) ? m_sharpenStrength : 0.0f; vkCmdPushConstants(command, m_bloomCompositePipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(pc), &pc); vkCmdDraw(command, 3, 1, 0, 0); // ImGui draws inside the composite pass, AFTER the HDR → LDR tonemap. + // Captures skip UI + const bool drawUI = bImFrameActive && m_capturePath.empty(); + if (drawUI) { + EndGpuPass(GpuPass::Tonemap); + BeginGpuPass(GpuPass::UI); + } if (bImFrameActive) { ImGui::Render(); - ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), command); + if (drawUI) + ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), command); } vkCmdEndRenderPass(command); + EndGpuPass(drawUI ? GpuPass::UI : GpuPass::Tonemap); } } // namespace RenderEngine diff --git a/src/Graphics/Vulkan/VulkanBuffer.cpp b/src/Graphics/Vulkan/VulkanBuffer.cpp index 9b25c97e..98d6cd9c 100644 --- a/src/Graphics/Vulkan/VulkanBuffer.cpp +++ b/src/Graphics/Vulkan/VulkanBuffer.cpp @@ -15,6 +15,13 @@ namespace RenderEngine { VmaAllocator VulkanBuffer::s_allocator = VK_NULL_HANDLE; +// Staging pool whose blocks stay resident, so upload bursts never pay for +// fresh vkAllocateMemory calls. +static VmaPool g_stagingPool = VK_NULL_HANDLE; +#define STAGING_PROBE_BUFFER_BYTES (64 * 1024) +static constexpr VkDeviceSize kStagingBlockSize = 64ull * 1024 * 1024; +static constexpr size_t kStagingMinBlocks = 2; + void VulkanBuffer::InitAllocator(VkInstance instance, VkPhysicalDevice physicalDevice, VkDevice device) { @@ -27,11 +34,38 @@ void VulkanBuffer::InitAllocator(VkInstance instance, if (vmaCreateAllocator(&aci, &s_allocator) != VK_SUCCESS) { SLEAK_ERROR("Failed to create VMA allocator!"); s_allocator = VK_NULL_HANDLE; + return; + } + + // Staging pool + VkBufferCreateInfo stagingInfo{}; + stagingInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + stagingInfo.size = STAGING_PROBE_BUFFER_BYTES; + stagingInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + stagingInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + VmaAllocationCreateInfo stagingAlloc{}; + stagingAlloc.usage = VMA_MEMORY_USAGE_AUTO; + stagingAlloc.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; + stagingAlloc.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + VmaPoolCreateInfo poolInfo{}; + poolInfo.blockSize = kStagingBlockSize; + poolInfo.minBlockCount = kStagingMinBlocks; + if (vmaFindMemoryTypeIndexForBufferInfo( + s_allocator, &stagingInfo, &stagingAlloc, + &poolInfo.memoryTypeIndex) != VK_SUCCESS || + vmaCreatePool(s_allocator, &poolInfo, &g_stagingPool) != VK_SUCCESS) { + SLEAK_WARN("VMA staging pool unavailable, using the default pools"); + g_stagingPool = VK_NULL_HANDLE; } } void VulkanBuffer::DestroyAllocator() { if (s_allocator == VK_NULL_HANDLE) return; + if (g_stagingPool != VK_NULL_HANDLE) { + vmaDestroyPool(s_allocator, g_stagingPool); + g_stagingPool = VK_NULL_HANDLE; + } vmaDestroyAllocator(s_allocator); s_allocator = VK_NULL_HANDLE; } @@ -308,14 +342,6 @@ void VulkanBuffer::Update(void* data, size_t size) { void VulkanBuffer::Cleanup() { if (m_device == VK_NULL_HANDLE) return; - // Only flush if THIS buffer has a pending copy in the active batch. - // Normally old buffers are destroyed before the batch starts (two-pass - // column rebuild), so this only triggers during scene transitions. - if (m_pendingInBatch && s_batchActive) { - FlushPendingCopies(); - m_pendingInBatch = false; - } - if (Type == BufferType::Constant && m_mappedData) { vmaUnmapMemory(s_allocator, m_memory); m_mappedData = nullptr; @@ -399,8 +425,14 @@ void VulkanBuffer::CreateBuffer(VkDeviceSize size, VmaAllocationCreateInfo allocCI{}; allocCI.usage = VMA_MEMORY_USAGE_AUTO; allocCI.requiredFlags = properties; // preserve exact memory-property semantics - if (properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) - allocCI.flags |= VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; + if (properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) { + // Constants are read back + allocCI.flags |= + Type == BufferType::Constant + ? VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT + : VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; + } + if (usage == VK_BUFFER_USAGE_TRANSFER_SRC_BIT) allocCI.pool = g_stagingPool; VmaAllocationInfo info{}; VkResult r = vmaCreateBuffer(s_allocator, &bufferInfo, &allocCI, @@ -466,8 +498,8 @@ void VulkanBuffer::CopyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, if (s_batchActive) { VkBufferCopy copyRegion{}; copyRegion.size = size; - vkCmdCopyBuffer(s_batchCommandBuffer, srcBuffer, dstBuffer, 1, ©Region); - m_pendingInBatch = true; + vkCmdCopyBuffer(s_batchCommandBuffer, srcBuffer, dstBuffer, 1, + ©Region); return; } @@ -543,28 +575,13 @@ void VulkanBuffer::EnsureBatchStarted(VkDevice device, VkCommandPool pool, s_batchActive = true; } -void VulkanBuffer::FlushPendingCopies() { - if (!s_batchActive) return; - - vkEndCommandBuffer(s_batchCommandBuffer); - - // Single submit + single fence for ALL batched copies - VkFenceCreateInfo fenceInfo{}; - fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - VkFence batchFence; - vkCreateFence(s_batchDevice, &fenceInfo, nullptr, &batchFence); - - VkSubmitInfo submitInfo{}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &s_batchCommandBuffer; - - vkQueueSubmit(s_batchQueue, 1, &submitInfo, batchFence); - vkWaitForFences(s_batchDevice, 1, &batchFence, VK_TRUE, UINT64_MAX); - - // Cleanup - vkDestroyFence(s_batchDevice, batchFence, nullptr); - vkFreeCommandBuffers(s_batchDevice, s_batchCommandPool, 1, &s_batchCommandBuffer); +void VulkanBuffer::DiscardPendingCopies() { + if (s_batchActive) { + vkFreeCommandBuffers(s_batchDevice, s_batchCommandPool, 1, + &s_batchCommandBuffer); + s_batchCommandBuffer = VK_NULL_HANDLE; + s_batchActive = false; + } for (auto& pending : s_pendingCleanup) { vmaDestroyBuffer(s_allocator, pending.buffer, pending.memory); @@ -573,9 +590,6 @@ void VulkanBuffer::FlushPendingCopies() { s_perTypeBytes[pending.memoryTypeIndex] -= pending.allocSize; } s_pendingCleanup.clear(); - - s_batchCommandBuffer = VK_NULL_HANDLE; - s_batchActive = false; } VulkanBuffer::AsyncFlushResult diff --git a/src/Graphics/Vulkan/VulkanCapture.cpp b/src/Graphics/Vulkan/VulkanCapture.cpp new file mode 100644 index 00000000..7bf5fe6a --- /dev/null +++ b/src/Graphics/Vulkan/VulkanCapture.cpp @@ -0,0 +1,215 @@ +#include +#include +#include +#include +#include +#include + +#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" +#include "Core/Logger.hpp" + +namespace Sleak { +namespace RenderEngine { + +namespace { + +/// Byte offsets of red, green, and blue inside one 4-byte texel; false when +/// the format is not an 8-bit RGBA or BGRA layout. +bool ChannelOffsets(VkFormat format, uint32_t& r, uint32_t& g, uint32_t& b) { + switch (format) { + case VK_FORMAT_B8G8R8A8_UNORM: + case VK_FORMAT_B8G8R8A8_SRGB: + r = 2; + g = 1; + b = 0; + return true; + case VK_FORMAT_R8G8B8A8_UNORM: + case VK_FORMAT_R8G8B8A8_SRGB: + case VK_FORMAT_A8B8G8R8_UNORM_PACK32: + case VK_FORMAT_A8B8G8R8_SRGB_PACK32: + r = 0; + g = 1; + b = 2; + return true; + default: + return false; + } +} + +/// Picks a host-visible, host-coherent memory type, preferring cached ones. +uint32_t FindReadbackMemoryType(VkPhysicalDevice gpu, uint32_t typeBits) { + VkPhysicalDeviceMemoryProperties props; + vkGetPhysicalDeviceMemoryProperties(gpu, &props); + const VkMemoryPropertyFlags coherent = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + for (VkMemoryPropertyFlags wanted : + {coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, coherent}) { + for (uint32_t i = 0; i < props.memoryTypeCount; ++i) { + if ((typeBits & (1u << i)) && + (props.memoryTypes[i].propertyFlags & wanted) == wanted) + return i; + } + } + return UINT32_MAX; +} + +} // namespace + +/// Arms a UI-free readback of the next presented frame into a binary PPM. +bool VulkanRenderer::RequestFrameCapture(const std::string& path) { + if (path.empty()) return false; + if (!m_swapchainTransferSrc) { + SLEAK_ERROR( + "Frame capture: swapchain was created without TRANSFER_SRC usage"); + return false; + } + uint32_t r, g, b; + if (!ChannelOffsets(scImageFormat, r, g, b)) { + SLEAK_ERROR("Frame capture: unsupported swapchain format {}", + static_cast(scImageFormat)); + return false; + } + m_capturePath = path; + return true; +} + +/// Records the swapchain-image-to-buffer copy for an armed capture. Runs after +/// the final render pass, so the image is in PRESENT_SRC_KHR. +void VulkanRenderer::RecordFrameCaptureCopy() { + CleanupFrameCapture(); + + VkBufferCreateInfo bufferInfo{}; + bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufferInfo.size = + static_cast(scExtent.width) * scExtent.height * 4; + bufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (vkCreateBuffer(device, &bufferInfo, nullptr, &m_captureBuffer) != + VK_SUCCESS) + m_captureBuffer = VK_NULL_HANDLE; + bool ok = m_captureBuffer != VK_NULL_HANDLE; + if (ok) { + VkMemoryRequirements req; + vkGetBufferMemoryRequirements(device, m_captureBuffer, &req); + VkMemoryAllocateInfo alloc{}; + alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + alloc.allocationSize = req.size; + alloc.memoryTypeIndex = + FindReadbackMemoryType(physicalDevice, req.memoryTypeBits); + if (alloc.memoryTypeIndex == UINT32_MAX || + vkAllocateMemory(device, &alloc, nullptr, &m_captureMemory) != + VK_SUCCESS) + m_captureMemory = VK_NULL_HANDLE; + ok = m_captureMemory != VK_NULL_HANDLE && + vkBindBufferMemory(device, m_captureBuffer, m_captureMemory, 0) == + VK_SUCCESS; + } + if (!ok) { + SLEAK_ERROR("Frame capture: readback buffer allocation failed"); + CleanupFrameCapture(); + m_capturePath.clear(); + return; + } + + const VkImage image = swapChainImages[CurrentFrameIndex]; + + VkImageMemoryBarrier toCopy{}; + toCopy.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + toCopy.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + toCopy.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + toCopy.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + toCopy.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + toCopy.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toCopy.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toCopy.image = image; + toCopy.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vkCmdPipelineBarrier(command, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, + nullptr, 1, &toCopy); + + VkBufferImageCopy region{}; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageExtent = {scExtent.width, scExtent.height, 1}; + vkCmdCopyImageToBuffer(command, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + m_captureBuffer, 1, ®ion); + + VkImageMemoryBarrier toPresent = toCopy; + toPresent.srcAccessMask = 0; + toPresent.dstAccessMask = 0; + toPresent.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + toPresent.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + + VkMemoryBarrier toHost{}; + toHost.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER; + toHost.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toHost.dstAccessMask = VK_ACCESS_HOST_READ_BIT; + vkCmdPipelineBarrier( + command, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 1, + &toHost, 0, nullptr, 1, &toPresent); + + m_captureExtent = scExtent; + m_captureFormat = scImageFormat; + m_captureRecorded = true; +} + +/// Waits for the captured frame, writes the PPM, and disarms the capture. +void VulkanRenderer::FinishFrameCapture() { + m_captureRecorded = false; + std::string path; + path.swap(m_capturePath); + + const VkResult wait = vkWaitForFences( + device, 1, &inFlightFences[currentFrame], VK_TRUE, 10'000'000'000ull); + void* mapped = nullptr; + if (wait != VK_SUCCESS || + vkMapMemory(device, m_captureMemory, 0, VK_WHOLE_SIZE, 0, &mapped) != + VK_SUCCESS) { + SLEAK_ERROR("Frame capture: readback of {} failed ({})", path, + static_cast(wait)); + CleanupFrameCapture(); + return; + } + + uint32_t r = 0, g = 1, b = 2; + ChannelOffsets(m_captureFormat, r, g, b); + const size_t pixels = + static_cast(m_captureExtent.width) * m_captureExtent.height; + const auto* src = static_cast(mapped); + std::vector rgb(pixels * 3); + for (size_t i = 0; i < pixels; ++i) { + rgb[i * 3 + 0] = src[i * 4 + r]; + rgb[i * 3 + 1] = src[i * 4 + g]; + rgb[i * 3 + 2] = src[i * 4 + b]; + } + vkUnmapMemory(device, m_captureMemory); + CleanupFrameCapture(); + + std::ofstream out(path, std::ios::binary); + out << "P6\n" + << m_captureExtent.width << " " << m_captureExtent.height << "\n255\n"; + out.write(reinterpret_cast(rgb.data()), + static_cast(rgb.size())); + if (!out) { + SLEAK_ERROR("Frame capture: could not write {}", path); + return; + } + SLEAK_INFO("Frame capture: wrote {}x{} to {}", m_captureExtent.width, + m_captureExtent.height, path); +} + +/// Frees the capture readback buffer. +void VulkanRenderer::CleanupFrameCapture() { + if (m_captureBuffer != VK_NULL_HANDLE) { + vkDestroyBuffer(device, m_captureBuffer, nullptr); + m_captureBuffer = VK_NULL_HANDLE; + } + if (m_captureMemory != VK_NULL_HANDLE) { + vkFreeMemory(device, m_captureMemory, nullptr); + m_captureMemory = VK_NULL_HANDLE; + } +} + +} // namespace RenderEngine +} // namespace Sleak diff --git a/src/Graphics/Vulkan/VulkanDeferred.cpp b/src/Graphics/Vulkan/VulkanDeferred.cpp index eca45981..63eb4f32 100644 --- a/src/Graphics/Vulkan/VulkanDeferred.cpp +++ b/src/Graphics/Vulkan/VulkanDeferred.cpp @@ -237,7 +237,7 @@ bool VulkanRenderer::CreateGBufferPipeline() { pipelineInfo.basePipelineIndex = -1; VkResult result = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &m_gbufferPipeline); + device, m_pipelineCache, 1, &pipelineInfo, nullptr, &m_gbufferPipeline); if (result != VK_SUCCESS) { SLEAK_ERROR("GBuffer: Failed to create GBuffer pipeline!"); return false; @@ -249,6 +249,8 @@ bool VulkanRenderer::CreateGBufferPipeline() { /// Compiles the skinned GBuffer shaders so skinned meshes write into the GBuffer. bool VulkanRenderer::CreateSkinnedGbufferPipeline() { + if (m_skinnedGbufferPipeline != VK_NULL_HANDLE) return true; + // Load skinned vert + gbuffer frag (SPIR-V already on disk) VulkanShader* sh = new VulkanShader(device); if (!sh->compile("assets/shaders/skinned_shader.vert.spv", @@ -349,7 +351,7 @@ bool VulkanRenderer::CreateSkinnedGbufferPipeline() { pi.basePipelineIndex = -1; VkResult res = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &pi, nullptr, &m_skinnedGbufferPipeline); + device, m_pipelineCache, 1, &pi, nullptr, &m_skinnedGbufferPipeline); delete sh; if (res != VK_SUCCESS) { SLEAK_ERROR("GBuffer: Failed to create skinned GBuffer pipeline!"); @@ -548,8 +550,9 @@ bool VulkanRenderer::CreateLightingPipeline() { pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; pipelineInfo.basePipelineIndex = -1; - VkResult result = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &m_lightingPipeline); + VkResult result = + vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &pipelineInfo, + nullptr, &m_lightingPipeline); if (result != VK_SUCCESS) { SLEAK_ERROR("GBuffer: Failed to create lighting pipeline!"); return false; @@ -575,7 +578,7 @@ bool VulkanRenderer::CreateForwardRenderPass() { depthAtt.format = depthFormat; depthAtt.samples = VK_SAMPLE_COUNT_1_BIT; depthAtt.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; - depthAtt.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + depthAtt.storeOp = VK_ATTACHMENT_STORE_OP_STORE; depthAtt.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; depthAtt.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; depthAtt.initialLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; @@ -595,15 +598,26 @@ bool VulkanRenderer::CreateForwardRenderPass() { subpass.pColorAttachments = &colorRef; subpass.pDepthStencilAttachment = &depthRef; - VkSubpassDependency dep{}; - dep.srcSubpass = VK_SUBPASS_EXTERNAL; - dep.dstSubpass = 0; - dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | - VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; - dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | - VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + std::array deps{}; + deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; + deps[0].dstSubpass = 0; + deps[0].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + deps[0].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + + // Feeds the post passes + deps[1].srcSubpass = 0; + deps[1].dstSubpass = VK_SUBPASS_EXTERNAL; + deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + deps[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; std::array attachments = {colorAtt, depthAtt}; @@ -613,8 +627,8 @@ bool VulkanRenderer::CreateForwardRenderPass() { rpInfo.pAttachments = attachments.data(); rpInfo.subpassCount = 1; rpInfo.pSubpasses = &subpass; - rpInfo.dependencyCount = 1; - rpInfo.pDependencies = &dep; + rpInfo.dependencyCount = static_cast(deps.size()); + rpInfo.pDependencies = deps.data(); return vkCreateRenderPass(device, &rpInfo, nullptr, &m_forwardRenderPass) == VK_SUCCESS; } @@ -1088,6 +1102,7 @@ void VulkanRenderer::CleanupGBufferResources() { CleanupSSAOResources(); CleanupSSRResources(); CleanupBloomResources(); + CleanupSceneCopyImages(); m_gbufferResourcesCreated = false; m_inGeometryPass = false; @@ -1113,6 +1128,7 @@ void VulkanRenderer::ExecuteDeferredLightingPass() { // 1. End GBuffer render pass — transitions color RTs → SHADER_READ_ONLY, // depth → DEPTH_STENCIL_READ_ONLY via finalLayout in CreateGBufferRenderPass vkCmdEndRenderPass(command); + EndGpuPass(GpuPass::GBuffer); m_inGeometryPass = false; m_activeCustomFormat = 0; // geometry pass is over; pipeline state doesn't survive across render passes @@ -1136,6 +1152,7 @@ void VulkanRenderer::ExecuteDeferredLightingPass() { rpBegin.clearValueCount = 1; rpBegin.pClearValues = &clearVal; + BeginGpuPass(GpuPass::Lighting); vkCmdBeginRenderPass(command, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); // 4. Set viewport and scissor @@ -1156,6 +1173,13 @@ void VulkanRenderer::ExecuteDeferredLightingPass() { // 5. Bind lighting pipeline vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_lightingPipeline); + // IBL is not baked here + static bool iblWarned = false; + if (m_iblEnabled && !iblWarned) { + SLEAK_WARN("Vulkan: IBL is not implemented, using hemisphere ambient"); + iblWarned = true; + } + // 6. Bind descriptor sets: // set 0: GBuffer samplers (RTs + shadow maps) // set 1: DeferredCB (InvViewProj + screen size) @@ -1176,6 +1200,7 @@ void VulkanRenderer::ExecuteDeferredLightingPass() { // 8. End lighting render pass vkCmdEndRenderPass(command); + EndGpuPass(GpuPass::Lighting); // 9. Transition depth back to DEPTH_STENCIL_ATTACHMENT_OPTIMAL for forward pass VkImageMemoryBarrier depthBarrier{}; @@ -1208,6 +1233,7 @@ void VulkanRenderer::BeginForwardTransparentPass() { rpBegin.renderArea.extent = scExtent; rpBegin.clearValueCount = 0; // LOAD_OP — no clear needed + BeginGpuPass(GpuPass::Forward); vkCmdBeginRenderPass(command, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); VkViewport viewport{}; @@ -1234,11 +1260,7 @@ void VulkanRenderer::BeginForwardTransparentPass() { pipelineLay, 0, 1, &descriptorSets[CurrentFrameIndex], 0, nullptr); } - if (m_boneUBOCreated) { - vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, - pipelineLay, 1, 1, - &boneDescriptorSets[currentFrame], 0, nullptr); - } + BindIdentityBonePalette(pipelineLay); if (m_lightUBOCreated) { vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLay, 2, 1, @@ -1275,6 +1297,11 @@ void VulkanRenderer::UpdateDeferredCB(const void* data, uint32_t size) { // exact inverse the lighting pass uses to reconstruct world from depth. if (copySize >= sizeof(m_cachedInvViewProj)) memcpy(m_cachedInvViewProj, data, sizeof(m_cachedInvViewProj)); + if (copySize >= sizeof(DeferredCBData)) { + const auto* cb = static_cast(data); + m_cachedNearPlane = cb->NearP; + m_cachedFarPlane = cb->FarP; + } // Compute sub-pixel Halton jitter for this frame (UV space). // Applied to WVP push constants in BindConstantBuffer during the geometry pass diff --git a/src/Graphics/Vulkan/VulkanDescriptors.cpp b/src/Graphics/Vulkan/VulkanDescriptors.cpp index 153ae44a..67199c0d 100644 --- a/src/Graphics/Vulkan/VulkanDescriptors.cpp +++ b/src/Graphics/Vulkan/VulkanDescriptors.cpp @@ -34,7 +34,7 @@ bool VulkanRenderer::CreateDescriptorSetLayout() { // Set 1: bone UBO (for skeletal animation) VkDescriptorSetLayoutBinding boneUBOBinding{}; boneUBOBinding.binding = 0; - boneUBOBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + boneUBOBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; boneUBOBinding.descriptorCount = 1; boneUBOBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; boneUBOBinding.pImmutableSamplers = nullptr; @@ -67,19 +67,23 @@ bool VulkanRenderer::CreateDescriptorSetLayout() { SLEAK_RETURN_ERR("Failed to create light UBO descriptor set layout!"); // Set 3: shadow map samplers — binding 0 = compare sampler (PCF), - // binding 1 = raw sampler (PCSS blocker search) - std::array shadowSamplerBindings{}; - shadowSamplerBindings[0].binding = 0; - shadowSamplerBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - shadowSamplerBindings[0].descriptorCount = 1; - shadowSamplerBindings[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - shadowSamplerBindings[0].pImmutableSamplers = nullptr; - - shadowSamplerBindings[1].binding = 1; - shadowSamplerBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - shadowSamplerBindings[1].descriptorCount = 1; - shadowSamplerBindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - shadowSamplerBindings[1].pImmutableSamplers = nullptr; + // binding 1 = raw sampler (PCSS blocker + // search) + // Bindings 2-5: scene color, depth, sky, SceneReadUBO (scene-read formats) + std::array + shadowSamplerBindings{}; + for (uint32_t b = 0; b < shadowSamplerBindings.size(); ++b) { + shadowSamplerBindings[b].binding = b; + shadowSamplerBindings[b].descriptorType = + VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + shadowSamplerBindings[b].descriptorCount = 1; + shadowSamplerBindings[b].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + shadowSamplerBindings[b].pImmutableSamplers = nullptr; + } + shadowSamplerBindings[SCENE_UBO_BINDING].descriptorType = + VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + shadowSamplerBindings[SCENE_UBO_BINDING].stageFlags = + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; VkDescriptorSetLayoutCreateInfo shadowSamplerLayoutInfo{}; shadowSamplerLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; @@ -109,6 +113,7 @@ bool VulkanRenderer::CreateDescriptorPool() { VkDescriptorPoolCreateInfo poolInfo{}; poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; poolInfo.poolSizeCount = 1; poolInfo.pPoolSizes = &poolSize; poolInfo.maxSets = totalSets; @@ -229,7 +234,7 @@ void VulkanRenderer::WriteTextureDescriptors(VulkanTexture* texture) { vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr); } - texture->SetDescriptorSets(std::move(sets)); + texture->SetDescriptorSets(std::move(sets), descriptorPool); } /// Initializes ImGui and its Vulkan backend against the active render pass. @@ -285,6 +290,7 @@ bool VulkanRenderer::CreateImGUI() { initInfo.Device = device; initInfo.QueueFamily = QueueIDs.GraphicsIndex; initInfo.Queue = graphicsQueue; + initInfo.PipelineCache = m_pipelineCache; initInfo.DescriptorPool = imguiDescriptorPool; initInfo.MinImageCount = 2; initInfo.ImageCount = @@ -301,134 +307,151 @@ bool VulkanRenderer::CreateImGUI() { return true; } -/// Creates the per-frame bone UBO buffers and their descriptor sets. +/// Creates each frame's first bone ring chunk; slot 0 holds identity. bool VulkanRenderer::CreateBoneUBOResources() { if (m_boneUBOCreated) return true; - static constexpr uint32_t MAX_BONES = 256; - static constexpr VkDeviceSize boneUBOSize = MAX_BONES * 64; // 256 mat4 = 16384 bytes + VkPhysicalDeviceProperties props{}; + vkGetPhysicalDeviceProperties(physicalDevice, &props); + const VkDeviceSize align = props.limits.minUniformBufferOffsetAlignment; + m_boneSlotStride = BONE_PALETTE_BYTES; + if (align > 0) + m_boneSlotStride = (m_boneSlotStride + align - 1) / align * align; - // Create per-frame UBO buffers (host-visible, coherent for fast CPU writes) for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { - VkBufferCreateInfo bufferInfo{}; - bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; - bufferInfo.size = boneUBOSize; - bufferInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; - bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - - if (vkCreateBuffer(device, &bufferInfo, nullptr, &boneUBOBuffers[i]) != VK_SUCCESS) { - SLEAK_ERROR("Failed to create bone UBO buffer!"); - return false; - } - - VkMemoryRequirements memReqs; - vkGetBufferMemoryRequirements(device, boneUBOBuffers[i], &memReqs); - - VkMemoryAllocateInfo allocInfo{}; - allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; - allocInfo.allocationSize = memReqs.size; - allocInfo.memoryTypeIndex = FindMemoryType(memReqs.memoryTypeBits, - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); - - if (vkAllocateMemory(device, &allocInfo, nullptr, &boneUBOMemory[i]) != VK_SUCCESS) { - SLEAK_ERROR("Failed to allocate bone UBO memory!"); + if (!AddBoneUBOChunk(i)) { + CleanupBoneUBOResources(); return false; } - vkBindBufferMemory(device, boneUBOBuffers[i], boneUBOMemory[i], 0); - vkMapMemory(device, boneUBOMemory[i], 0, boneUBOSize, 0, &boneUBOMapped[i]); - - // Initialize with identity matrices - auto* matrices = static_cast(boneUBOMapped[i]); - for (uint32_t b = 0; b < MAX_BONES; ++b) { - // Identity matrix in column-major order + // Identity palette in slot 0 + auto* matrices = static_cast(m_boneChunks[i][0].mapped); + for (uint32_t b = 0; b < BONE_PALETTE_BYTES / BONE_MATRIX_BYTES; ++b) { for (int c = 0; c < 16; ++c) matrices[b * 16 + c] = (c % 5 == 0) ? 1.0f : 0.0f; } + m_boneSlot[i] = 1; } - // Create descriptor pool for bone UBO + m_boneUBOCreated = true; + SLEAK_INFO( + "VulkanRenderer: Bone UBO ring created ({} x {} bytes per frame)", + BONE_SLOTS_PER_CHUNK, m_boneSlotStride); + return true; +} + +/// Appends BONE_SLOTS_PER_CHUNK palette slots to one frame's bone ring: its +/// own buffer and dynamic-UBO set, so sets bound earlier stay valid. +bool VulkanRenderer::AddBoneUBOChunk(uint32_t frame) { + BoneUBOChunk chunk{}; + VkDescriptorPoolSize poolSize{}; - poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; - poolSize.descriptorCount = MAX_FRAMES_IN_FLIGHT; + poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; + poolSize.descriptorCount = 1; VkDescriptorPoolCreateInfo poolInfo{}; poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; poolInfo.poolSizeCount = 1; poolInfo.pPoolSizes = &poolSize; - poolInfo.maxSets = MAX_FRAMES_IN_FLIGHT; - - if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &boneDescriptorPool) != VK_SUCCESS) { + poolInfo.maxSets = 1; + if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &chunk.pool) != + VK_SUCCESS) { SLEAK_ERROR("Failed to create bone descriptor pool!"); return false; } - // Allocate descriptor sets - std::array layouts; - layouts.fill(boneDescriptorSetLayout); - VkDescriptorSetAllocateInfo dsAllocInfo{}; dsAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; - dsAllocInfo.descriptorPool = boneDescriptorPool; - dsAllocInfo.descriptorSetCount = MAX_FRAMES_IN_FLIGHT; - dsAllocInfo.pSetLayouts = layouts.data(); - - if (vkAllocateDescriptorSets(device, &dsAllocInfo, boneDescriptorSets.data()) != VK_SUCCESS) { - SLEAK_ERROR("Failed to allocate bone descriptor sets!"); + dsAllocInfo.descriptorPool = chunk.pool; + dsAllocInfo.descriptorSetCount = 1; + dsAllocInfo.pSetLayouts = &boneDescriptorSetLayout; + if (vkAllocateDescriptorSets(device, &dsAllocInfo, &chunk.set) != + VK_SUCCESS) { + SLEAK_ERROR("Failed to allocate bone descriptor set!"); + vkDestroyDescriptorPool(device, chunk.pool, nullptr); return false; } - // Write descriptor sets pointing to UBO buffers - for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { - VkDescriptorBufferInfo bufInfo{}; - bufInfo.buffer = boneUBOBuffers[i]; - bufInfo.offset = 0; - bufInfo.range = boneUBOSize; - - VkWriteDescriptorSet descriptorWrite{}; - descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - descriptorWrite.dstSet = boneDescriptorSets[i]; - descriptorWrite.dstBinding = 0; - descriptorWrite.dstArrayElement = 0; - descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; - descriptorWrite.descriptorCount = 1; - descriptorWrite.pBufferInfo = &bufInfo; + // Host-visible, coherent for fast CPU writes + const VkDeviceSize size = m_boneSlotStride * BONE_SLOTS_PER_CHUNK; + VkBufferCreateInfo bufferInfo{}; + bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufferInfo.size = size; + bufferInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + if (vkCreateBuffer(device, &bufferInfo, nullptr, &chunk.buffer) != + VK_SUCCESS) { + SLEAK_ERROR("Failed to create bone UBO buffer!"); + vkDestroyDescriptorPool(device, chunk.pool, nullptr); + return false; + } - vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr); + VkMemoryRequirements memReqs; + vkGetBufferMemoryRequirements(device, chunk.buffer, &memReqs); + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memReqs.size; + allocInfo.memoryTypeIndex = FindMemoryType( + memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + if (vkAllocateMemory(device, &allocInfo, nullptr, &chunk.memory) != + VK_SUCCESS) { + SLEAK_ERROR("Failed to allocate bone UBO memory!"); + vkDestroyBuffer(device, chunk.buffer, nullptr); + vkDestroyDescriptorPool(device, chunk.pool, nullptr); + return false; } + vkBindBufferMemory(device, chunk.buffer, chunk.memory, 0); + vkMapMemory(device, chunk.memory, 0, size, 0, &chunk.mapped); - m_boneUBOCreated = true; - SLEAK_INFO("VulkanRenderer: Bone UBO resources created ({} bytes per frame)", boneUBOSize); + // One palette range; binds select the slot by dynamic offset + VkDescriptorBufferInfo bufInfo{}; + bufInfo.buffer = chunk.buffer; + bufInfo.offset = 0; + bufInfo.range = BONE_PALETTE_BYTES; + + VkWriteDescriptorSet descriptorWrite{}; + descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrite.dstSet = chunk.set; + descriptorWrite.dstBinding = 0; + descriptorWrite.dstArrayElement = 0; + descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; + descriptorWrite.descriptorCount = 1; + descriptorWrite.pBufferInfo = &bufInfo; + vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr); + + m_boneChunks[frame].push_back(chunk); + if (m_boneChunks[frame].size() > 1) + SLEAK_INFO("VulkanRenderer: frame {} bone ring grew to {} slots", frame, + m_boneChunks[frame].size() * BONE_SLOTS_PER_CHUNK); return true; } - -/// Destroys the bone UBO buffers, memory, and descriptor pool. +/// Destroys every bone ring chunk of every frame. void VulkanRenderer::CleanupBoneUBOResources() { - if (!m_boneUBOCreated) return; - - for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { - if (boneUBOMapped[i]) { - vkUnmapMemory(device, boneUBOMemory[i]); - boneUBOMapped[i] = nullptr; - } - if (boneUBOBuffers[i]) { - vkDestroyBuffer(device, boneUBOBuffers[i], nullptr); - boneUBOBuffers[i] = VK_NULL_HANDLE; - } - if (boneUBOMemory[i]) { - vkFreeMemory(device, boneUBOMemory[i], nullptr); - boneUBOMemory[i] = VK_NULL_HANDLE; + for (auto& chunks : m_boneChunks) { + for (BoneUBOChunk& chunk : chunks) { + if (chunk.mapped) vkUnmapMemory(device, chunk.memory); + if (chunk.buffer) vkDestroyBuffer(device, chunk.buffer, nullptr); + if (chunk.memory) vkFreeMemory(device, chunk.memory, nullptr); + if (chunk.pool) + vkDestroyDescriptorPool(device, chunk.pool, nullptr); } - } - if (boneDescriptorPool) { - vkDestroyDescriptorPool(device, boneDescriptorPool, nullptr); - boneDescriptorPool = VK_NULL_HANDLE; + chunks.clear(); } m_boneUBOCreated = false; } -/// Copies bone matrices into the current frame's UBO and binds its descriptor set. +/// Binds the identity palette (ring slot 0) at set 1. +void VulkanRenderer::BindIdentityBonePalette(VkPipelineLayout layout) { + if (!m_boneUBOCreated || m_boneChunks[currentFrame].empty()) return; + const uint32_t offset = 0; + vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 1, + 1, &m_boneChunks[currentFrame][0].set, 1, &offset); +} + +/// Copies bone matrices into this bind's own slot of the frame's bone ring +/// and binds set 1 at that slot, so every skinned draw keeps its palette. void VulkanRenderer::BindBoneBuffer(RefPtr buffer) { if (!bFrameStarted) return; if (!buffer) return; @@ -445,12 +468,19 @@ void VulkanRenderer::BindBoneBuffer(RefPtr buffer) { if (!data) return; uint32_t size = static_cast(vkBuf->GetSize()); - static constexpr uint32_t MAX_BONE_UBO_SIZE = 256 * 64; // MAX_BONES * sizeof(mat4) - if (size > MAX_BONE_UBO_SIZE) size = MAX_BONE_UBO_SIZE; + if (size > BONE_PALETTE_BYTES) size = BONE_PALETTE_BYTES; - // Copy bone matrices to mapped UBO (use currentFrame, not CurrentFrameIndex + // Claim the next ring slot (use currentFrame, not CurrentFrameIndex // which is the swapchain image index and can exceed MAX_FRAMES_IN_FLIGHT) - memcpy(boneUBOMapped[currentFrame], data, size); + const uint32_t slot = m_boneSlot[currentFrame]; + auto& chunks = m_boneChunks[currentFrame]; + const uint32_t chunkIdx = slot / BONE_SLOTS_PER_CHUNK; + if (chunkIdx >= chunks.size() && !AddBoneUBOChunk(currentFrame)) return; + const BoneUBOChunk& chunk = chunks[chunkIdx]; + const uint32_t offset = + static_cast((slot % BONE_SLOTS_PER_CHUNK) * m_boneSlotStride); + memcpy(static_cast(chunk.mapped) + offset, data, size); + m_boneSlot[currentFrame] = slot + 1; // Bind bone descriptor set at set index 1. // In the GBuffer geometry pass the active layout is m_gbufferGeomLayout; @@ -460,9 +490,7 @@ void VulkanRenderer::BindBoneBuffer(RefPtr buffer) { VkPipelineLayout boneBindLayout = (m_inGeometryPass && m_gbufferGeomLayout != VK_NULL_HANDLE) ? m_gbufferGeomLayout : pipelineLay; vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, - boneBindLayout, 1, 1, - &boneDescriptorSets[currentFrame], - 0, nullptr); + boneBindLayout, 1, 1, &chunk.set, 1, &offset); } /// Creates the per-frame PBR material descriptor set (set 0 in GBuffer pass): diff --git a/src/Graphics/Vulkan/VulkanDevice.cpp b/src/Graphics/Vulkan/VulkanDevice.cpp index 83ce61e4..f341a080 100644 --- a/src/Graphics/Vulkan/VulkanDevice.cpp +++ b/src/Graphics/Vulkan/VulkanDevice.cpp @@ -1,7 +1,5 @@ -#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" -#include "../../include/private/Graphics/Vulkan/VulkanBuffer.hpp" - #include + #include #include #include @@ -11,6 +9,10 @@ #include #include #include + +#include "../../include/private/Graphics/Vulkan/VulkanBuffer.hpp" +#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" +#include "Core/CommandLine.hpp" #include "Core/Logger.hpp" #include "SDL3/SDL_error.h" #ifdef PLATFORM_LINUX @@ -22,124 +24,168 @@ namespace Sleak { namespace RenderEngine { -/// Creates the Vulkan instance with validation layers when available. -bool VulkanRenderer::InitVulkan() { - try { - instance = VK_NULL_HANDLE; - - std::vector requiredExtensions = { - VK_KHR_SURFACE_EXTENSION_NAME, - VK_EXT_DEBUG_UTILS_EXTENSION_NAME - }; - - #ifdef PLATFORM_LINUX - { - // Only request surface extensions actually available - // (blindly requesting both breaks capture tools like RenderDoc) - uint32_t surfExtCount = 0; - vkEnumerateInstanceExtensionProperties(nullptr, &surfExtCount, nullptr); - std::vector surfExts(surfExtCount); - vkEnumerateInstanceExtensionProperties(nullptr, &surfExtCount, surfExts.data()); - auto hasSurfExt = [&](const char* name) { - for (auto& e : surfExts) - if (strcmp(e.extensionName, name) == 0) return true; - return false; - }; - if (hasSurfExt("VK_KHR_wayland_surface")) - requiredExtensions.push_back("VK_KHR_wayland_surface"); - if (hasSurfExt("VK_KHR_xlib_surface")) - requiredExtensions.push_back("VK_KHR_xlib_surface"); - } - #elif defined(PLATFORM_WIN) + /// Creates the Vulkan instance. The validation layer is on in Debug builds + /// or with --validate, and --syncval adds synchronization validation. + bool VulkanRenderer::InitVulkan() { + try { + instance = VK_NULL_HANDLE; + + std::vector requiredExtensions = { + VK_KHR_SURFACE_EXTENSION_NAME, + VK_EXT_DEBUG_UTILS_EXTENSION_NAME}; + +#ifdef PLATFORM_LINUX + { + // Only request surface extensions actually available + // (blindly requesting both breaks capture tools like RenderDoc) + uint32_t surfExtCount = 0; + vkEnumerateInstanceExtensionProperties(nullptr, &surfExtCount, + nullptr); + std::vector surfExts(surfExtCount); + vkEnumerateInstanceExtensionProperties(nullptr, &surfExtCount, + surfExts.data()); + auto hasSurfExt = [&](const char* name) { + for (auto& e : surfExts) + if (strcmp(e.extensionName, name) == 0) return true; + return false; + }; + if (hasSurfExt("VK_KHR_wayland_surface")) + requiredExtensions.push_back("VK_KHR_wayland_surface"); + if (hasSurfExt("VK_KHR_xlib_surface")) + requiredExtensions.push_back("VK_KHR_xlib_surface"); + } +#elif defined(PLATFORM_WIN) requiredExtensions.push_back( VK_KHR_WIN32_SURFACE_EXTENSION_NAME); - #elif defined(TARGET_OS_MAC) || defined(TARGET_OS_IOS) +#elif defined(TARGET_OS_MAC) || defined(TARGET_OS_IOS) requiredExtensions.push_back(VK_MVK_MOLTENVK_EXTENSION_NAME); - #endif - - // Always attempt to enable validation layers so GPU errors are - // reported via the debug messenger as SLEAK_ERROR messages rather - // than silent VK_ERROR_DEVICE_LOST crashes. If the layer is not - // installed the instance still creates successfully (empty list). - std::vector enabledLayers; - { - uint32_t layerCount = 0; - vkEnumerateInstanceLayerProperties(&layerCount, nullptr); - std::vector availableLayers(layerCount); - vkEnumerateInstanceLayerProperties(&layerCount, - availableLayers.data()); +#endif + // Validation layer gating + const bool syncValidation = CommandLine::HasFlag("--syncval"); + bool wantValidation = + syncValidation || CommandLine::HasFlag("--validate"); +#ifdef _DEBUG + wantValidation = true; +#endif const char* desiredLayer = "VK_LAYER_KHRONOS_validation"; - for (const auto& layer : availableLayers) { - if (strcmp(layer.layerName, desiredLayer) == 0) { - enabledLayers.push_back(desiredLayer); - SLEAK_INFO("Vulkan validation layer enabled"); - break; + std::vector enabledLayers; + if (wantValidation) { + uint32_t layerCount = 0; + vkEnumerateInstanceLayerProperties(&layerCount, nullptr); + std::vector availableLayers(layerCount); + vkEnumerateInstanceLayerProperties(&layerCount, + availableLayers.data()); + + for (const auto& layer : availableLayers) { + if (strcmp(layer.layerName, desiredLayer) == 0) { + enabledLayers.push_back(desiredLayer); + SLEAK_INFO("Vulkan validation layer enabled"); + break; + } + } + if (enabledLayers.empty()) { + SLEAK_WARN( + "VK_LAYER_KHRONOS_validation not available — GPU " + "errors will not be reported"); } } + m_validationEnabled = !enabledLayers.empty(); if (enabledLayers.empty()) { - SLEAK_WARN("VK_LAYER_KHRONOS_validation not available — GPU errors will not be reported"); + auto it = std::find_if( + requiredExtensions.begin(), requiredExtensions.end(), + [](const char* ext) { + return strcmp(ext, VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == + 0; + }); + if (it != requiredExtensions.end()) { + requiredExtensions.erase(it); + } } - } - if (enabledLayers.empty()) { - auto it = std::find_if( - requiredExtensions.begin(), requiredExtensions.end(), - [](const char* ext) { - return strcmp(ext, - VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == 0; - }); - if (it != requiredExtensions.end()) { - requiredExtensions.erase(it); + + // Sync validation settings + const VkBool32 syncEnabled = VK_TRUE; + VkLayerSettingEXT syncSetting{desiredLayer, "validate_sync", + VK_LAYER_SETTING_TYPE_BOOL32_EXT, 1, + &syncEnabled}; + VkLayerSettingsCreateInfoEXT layerSettings{}; + layerSettings.sType = + VK_STRUCTURE_TYPE_LAYER_SETTINGS_CREATE_INFO_EXT; + layerSettings.settingCount = 1; + layerSettings.pSettings = &syncSetting; + bool useLayerSettings = false; + if (m_validationEnabled && syncValidation) { + uint32_t layerExtCount = 0; + vkEnumerateInstanceExtensionProperties(desiredLayer, + &layerExtCount, nullptr); + std::vector layerExts(layerExtCount); + vkEnumerateInstanceExtensionProperties( + desiredLayer, &layerExtCount, layerExts.data()); + for (const auto& ext : layerExts) { + if (strcmp(ext.extensionName, + VK_EXT_LAYER_SETTINGS_EXTENSION_NAME) == 0) { + useLayerSettings = true; + break; + } + } + if (useLayerSettings) { + requiredExtensions.push_back( + VK_EXT_LAYER_SETTINGS_EXTENSION_NAME); + SLEAK_INFO("Vulkan synchronization validation enabled"); + } else { + SLEAK_WARN( + "Vulkan synchronization validation needs " + "VK_EXT_layer_settings, which this layer lacks"); + } } - } - // Check and list vulkan extensions - uint32_t extensionCount = 0; - vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, - nullptr); - std::vector extensions(extensionCount); - vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, - extensions.data()); - - VkApplicationInfo appInfo{}; - appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; - appInfo.pApplicationName = "SleakEngine"; - appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); - appInfo.pEngineName = "Sleak Engine"; - appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); - appInfo.apiVersion = VK_API_VERSION_1_1; - - VkInstanceCreateInfo createInfo{}; - createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; - createInfo.pApplicationInfo = &appInfo; - createInfo.enabledExtensionCount = - static_cast(requiredExtensions.size()); - createInfo.ppEnabledExtensionNames = requiredExtensions.data(); - createInfo.enabledLayerCount = - static_cast(enabledLayers.size()); - createInfo.ppEnabledLayerNames = enabledLayers.data(); - - VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{}; - if (!enabledLayers.empty()) { - PopulateDebugMessengerCreateInfo(debugCreateInfo); - createInfo.pNext = &debugCreateInfo; - } + // Check and list vulkan extensions + uint32_t extensionCount = 0; + vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, + nullptr); + std::vector extensions(extensionCount); + vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, + extensions.data()); + + VkApplicationInfo appInfo{}; + appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + appInfo.pApplicationName = "SleakEngine"; + appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); + appInfo.pEngineName = "Sleak Engine"; + appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); + appInfo.apiVersion = VK_API_VERSION_1_1; + + VkInstanceCreateInfo createInfo{}; + createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + createInfo.pApplicationInfo = &appInfo; + createInfo.enabledExtensionCount = + static_cast(requiredExtensions.size()); + createInfo.ppEnabledExtensionNames = requiredExtensions.data(); + createInfo.enabledLayerCount = + static_cast(enabledLayers.size()); + createInfo.ppEnabledLayerNames = enabledLayers.data(); + + VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{}; + if (m_validationEnabled) { + PopulateDebugMessengerCreateInfo(debugCreateInfo); + if (useLayerSettings) debugCreateInfo.pNext = &layerSettings; + createInfo.pNext = &debugCreateInfo; + } - VkResult result = - vkCreateInstance(&createInfo, nullptr, &instance); - if (result != VK_SUCCESS) { - SLEAK_ERROR("Failed to create Vulkan instance!"); - return false; - } + VkResult result = vkCreateInstance(&createInfo, nullptr, &instance); + if (result != VK_SUCCESS) { + SLEAK_ERROR("Failed to create Vulkan instance!"); + return false; + } - SLEAK_INFO("Vulkan instance created successfully."); - return true; + SLEAK_INFO("Vulkan instance created successfully."); + return true; - } catch (const std::exception& e) { - SLEAK_ERROR("Exception in InitVulkan: {}", e.what()); - return false; + } catch (const std::exception& e) { + SLEAK_ERROR("Exception in InitVulkan: {}", e.what()); + return false; + } } -} /// Selects the physical GPU and creates the logical device and queues. bool VulkanRenderer::CreateDevice() { @@ -256,13 +302,20 @@ bool VulkanRenderer::CreateDevice() { // Build unique queue create infos (no duplicates!) auto queueCreateInfos = GetUniqueQueueCreateInfos(); - // Get supported features + // Enable only used features + VkPhysicalDeviceFeatures supported{}; + vkGetPhysicalDeviceFeatures(physicalDevice, &supported); VkPhysicalDeviceFeatures features{}; - vkGetPhysicalDeviceFeatures(physicalDevice, &features); + features.samplerAnisotropy = supported.samplerAnisotropy; + features.fullDrawIndexUint32 = supported.fullDrawIndexUint32; + if (CommandLine::HasFlag("--validate")) + features.robustBufferAccess = supported.robustBufferAccess; // Query max MSAA sample count VkPhysicalDeviceProperties deviceProperties; vkGetPhysicalDeviceProperties(physicalDevice, &deviceProperties); + SLEAK_INFO("Selected GPU: {} (driver {:#x})", deviceProperties.deviceName, + deviceProperties.driverVersion); VkSampleCountFlags counts = deviceProperties.limits.framebufferColorSampleCounts & deviceProperties.limits.framebufferDepthSampleCounts; m_maxMsaaSampleCount = 1; @@ -325,8 +378,10 @@ VulkanRenderer::GetUniqueQueueCreateInfos() { return result; } -/// Registers the debug messenger callback for validation output. +/// Registers the debug messenger callback when the validation layer is on. bool VulkanRenderer::SetupDebugMessenger() { + if (!m_validationEnabled) return true; + VkDebugUtilsMessengerCreateInfoEXT createInfo{}; PopulateDebugMessengerCreateInfo(createInfo); diff --git a/src/Graphics/Vulkan/VulkanPipelines.cpp b/src/Graphics/Vulkan/VulkanPipelines.cpp index 01c3e353..8d5d7685 100644 --- a/src/Graphics/Vulkan/VulkanPipelines.cpp +++ b/src/Graphics/Vulkan/VulkanPipelines.cpp @@ -1,15 +1,171 @@ -#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" - #include #include #include +#include +#include +#include #include +#include +#include +#include #include + +#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" #include "Core/Logger.hpp" namespace Sleak { namespace RenderEngine { + namespace { + + // Relative to the working directory, which is the executable's directory + constexpr const char* kPipelineCacheDir = "cache"; + constexpr const char* kPipelineCachePath = "cache/vulkan_pipelines.bin"; + constexpr const char* kPipelineCacheTempPath = + "cache/vulkan_pipelines.bin.tmp"; + constexpr size_t kPipelineCacheHeaderSize = + sizeof(VkPipelineCacheHeaderVersionOne); +#define FNV1A_OFFSET_BASIS 14695981039346656037ull +#define FNV1A_PRIME 1099511628211ull + + /// FNV-1a hash, used to skip rewriting an unchanged pipeline cache. + uint64_t HashBytes(const std::vector& data) { + uint64_t hash = FNV1A_OFFSET_BASIS; + for (char c : data) { + hash ^= static_cast(c); + hash *= FNV1A_PRIME; + } + return hash; + } + + /// Reads one little-endian field of a pipeline cache header. + uint32_t ReadLE32(const char* bytes) { + const auto* b = reinterpret_cast(bytes); + return static_cast(b[0]) | static_cast(b[1]) << 8 | + static_cast(b[2]) << 16 | + static_cast(b[3]) << 24; + } + + } // namespace + + /// Creates the pipeline cache, seeded from disk when the saved blob was + /// written by this device and driver. + void VulkanRenderer::CreatePipelineCache() { + std::vector blob; + std::ifstream file(kPipelineCachePath, + std::ios::binary | std::ios::ate); + if (file) { + const std::streamoff size = file.tellg(); + if (size > 0) { + blob.resize(static_cast(size)); + file.seekg(0); + if (!file.read(blob.data(), size)) blob.clear(); + } + } + file.close(); + + // Header check + if (!blob.empty()) { + VkPhysicalDeviceProperties props{}; + vkGetPhysicalDeviceProperties(physicalDevice, &props); + const bool valid = + blob.size() >= kPipelineCacheHeaderSize && + ReadLE32(blob.data()) >= kPipelineCacheHeaderSize && + ReadLE32(blob.data()) <= blob.size() && + ReadLE32(blob.data() + offsetof(VkPipelineCacheHeaderVersionOne, + headerVersion)) == + static_cast( + VK_PIPELINE_CACHE_HEADER_VERSION_ONE) && + ReadLE32(blob.data() + offsetof(VkPipelineCacheHeaderVersionOne, + vendorID)) == props.vendorID && + ReadLE32(blob.data() + offsetof(VkPipelineCacheHeaderVersionOne, + deviceID)) == props.deviceID && + std::memcmp( + blob.data() + offsetof(VkPipelineCacheHeaderVersionOne, + pipelineCacheUUID), + props.pipelineCacheUUID, VK_UUID_SIZE) == 0; + if (!valid) { + SLEAK_INFO( + "Pipeline cache: ignoring blob from another device or " + "driver"); + blob.clear(); + } + } + + VkPipelineCacheCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO; + info.initialDataSize = blob.size(); + info.pInitialData = blob.empty() ? nullptr : blob.data(); + VkResult result = + vkCreatePipelineCache(device, &info, nullptr, &m_pipelineCache); + if (result != VK_SUCCESS && !blob.empty()) { + blob.clear(); + info.initialDataSize = 0; + info.pInitialData = nullptr; + result = + vkCreatePipelineCache(device, &info, nullptr, &m_pipelineCache); + } + if (result != VK_SUCCESS) { + m_pipelineCache = VK_NULL_HANDLE; + SLEAK_WARN( + "Pipeline cache: creation failed, pipelines compile uncached"); + return; + } + + m_pipelineCacheLoadedSize = blob.size(); + m_pipelineCacheLoadedHash = blob.empty() ? 0 : HashBytes(blob); + SLEAK_INFO("Pipeline cache: {} ({} bytes)", + blob.empty() ? "empty" : "loaded", blob.size()); + } + + /// Saves the pipeline cache to disk when it changed, then destroys it. + /// Writes a temp file and renames it so a failed write never leaves a torn + /// cache. + void VulkanRenderer::DestroyPipelineCache() { + if (m_pipelineCache == VK_NULL_HANDLE) return; + + std::vector data; + size_t size = 0; + if (vkGetPipelineCacheData(device, m_pipelineCache, &size, nullptr) == + VK_SUCCESS && + size > 0) { + data.resize(size); + if (vkGetPipelineCacheData(device, m_pipelineCache, &size, + data.data()) == VK_SUCCESS) + data.resize(size); + else + data.clear(); + } + vkDestroyPipelineCache(device, m_pipelineCache, nullptr); + m_pipelineCache = VK_NULL_HANDLE; + + if (data.empty()) return; + if (data.size() == m_pipelineCacheLoadedSize && + HashBytes(data) == m_pipelineCacheLoadedHash) + return; + + std::error_code ec; + std::filesystem::create_directories(kPipelineCacheDir, ec); + std::ofstream out(kPipelineCacheTempPath, + std::ios::binary | std::ios::trunc); + out.write(data.data(), static_cast(data.size())); + out.close(); + if (!out) { + SLEAK_WARN("Pipeline cache: could not write {}", + kPipelineCacheTempPath); + std::filesystem::remove(kPipelineCacheTempPath, ec); + return; + } + std::filesystem::rename(kPipelineCacheTempPath, kPipelineCachePath, ec); + if (ec) { + SLEAK_WARN("Pipeline cache: could not replace {}: {}", + kPipelineCachePath, ec.message()); + std::filesystem::remove(kPipelineCacheTempPath, ec); + return; + } + SLEAK_INFO("Pipeline cache: saved {} bytes", data.size()); + } + /// Binds the skybox pipeline and its descriptor set for the current frame. void VulkanRenderer::BeginSkyboxPass() { if (!bFrameStarted) return; @@ -54,16 +210,50 @@ void VulkanRenderer::EndSkyboxPass() { } } -/// Creates the main forward graphics pipeline and its pipeline layout. -bool VulkanRenderer::CreateGraphicsPipeline() { - VkResult result; +/// Creates the layout shared by the forward, skybox, debug line, skinned, and +/// shadow pipelines. +bool VulkanRenderer::CreateMainPipelineLayout() { + if (pipelineLay != VK_NULL_HANDLE) return true; + + // Push constants + VkPushConstantRange pushConstantRange{}; + pushConstantRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + pushConstantRange.offset = 0; + pushConstantRange.size = PUSH_CONSTANT_BYTES; + + // Descriptor set layouts + std::array setLayouts = { + descriptorSetLayout, boneDescriptorSetLayout, + m_lightUBODescriptorSetLayout, m_shadowSamplerDescriptorSetLayout}; + + VkPipelineLayoutCreateInfo layoutInfo{}; + layoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + layoutInfo.setLayoutCount = static_cast(setLayouts.size()); + layoutInfo.pSetLayouts = setLayouts.data(); + layoutInfo.pushConstantRangeCount = 1; + layoutInfo.pPushConstantRanges = &pushConstantRange; - simpleShader = new VulkanShader(device); - bool isShader = - simpleShader->compile("assets/shaders/default_shader"); + if (vkCreatePipelineLayout(device, &layoutInfo, nullptr, &pipelineLay) != + VK_SUCCESS) + SLEAK_RETURN_ERR("Failed to create graphics pipeline layout!"); - if (!isShader) - SLEAK_RETURN_ERR("Cannot compile shaders!") + return true; +} + +/// Creates the main forward graphics pipeline, plus its layout and shader on +/// first use. +bool VulkanRenderer::CreateGraphicsPipeline() { + if (pipeline != VK_NULL_HANDLE) return true; + if (!CreateMainPipelineLayout()) return false; + + if (!simpleShader) { + simpleShader = new VulkanShader(device); + if (!simpleShader->compile("assets/shaders/default_shader")) { + delete simpleShader; + simpleShader = nullptr; + SLEAK_RETURN_ERR("Cannot compile shaders!") + } + } VkPipelineShaderStageCreateInfo shaderStages[] = { simpleShader->GetVertexInfo(), simpleShader->GetFragInfo()}; @@ -79,13 +269,13 @@ bool VulkanRenderer::CreateGraphicsPipeline() { static_cast(dynamicStates.size()); dynamicState.pDynamicStates = dynamicStates.data(); - // Vertex input — matches Sleak::Vertex (64 bytes) + // Vertex input (locations 0-4) VkVertexInputBindingDescription bindingDescription{}; bindingDescription.binding = 0; bindingDescription.stride = sizeof(Vertex); bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; - std::array attributeDescs{}; + std::array attributeDescs{}; // Position: float3 at offset 0 attributeDescs[0].binding = 0; @@ -117,18 +307,6 @@ bool VulkanRenderer::CreateGraphicsPipeline() { attributeDescs[4].format = VK_FORMAT_R32G32_SFLOAT; attributeDescs[4].offset = offsetof(Vertex, u); - // BoneIDs: int4 - attributeDescs[5].binding = 0; - attributeDescs[5].location = 5; - attributeDescs[5].format = VK_FORMAT_R32G32B32A32_SINT; - attributeDescs[5].offset = offsetof(Vertex, boneIDs); - - // BoneWeights: float4 - attributeDescs[6].binding = 0; - attributeDescs[6].location = 6; - attributeDescs[6].format = VK_FORMAT_R32G32B32A32_SFLOAT; - attributeDescs[6].offset = offsetof(Vertex, boneWeights); - VkPipelineVertexInputStateCreateInfo vertexInputInfo{}; vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; @@ -202,33 +380,6 @@ bool VulkanRenderer::CreateGraphicsPipeline() { colorBlendInfo.attachmentCount = 1; colorBlendInfo.pAttachments = &colorBlendAttachment; - // Pipeline layout — push constants for WVP matrix + descriptor set - // for texture sampler - VkPushConstantRange pushConstantRange{}; - pushConstantRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; - pushConstantRange.offset = 0; - pushConstantRange.size = 128; // sizeof(mat4) * 2 = 128 bytes (WVP + World) - - // Four descriptor set layouts: - // set 0 = texture sampler, set 1 = bone UBO, - // set 2 = light/shadow UBO, set 3 = shadow map sampler - std::array setLayouts = { - descriptorSetLayout, boneDescriptorSetLayout, - m_lightUBODescriptorSetLayout, m_shadowSamplerDescriptorSetLayout - }; - - VkPipelineLayoutCreateInfo layoutInfo{}; - layoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; - layoutInfo.setLayoutCount = static_cast(setLayouts.size()); - layoutInfo.pSetLayouts = setLayouts.data(); - layoutInfo.pushConstantRangeCount = 1; - layoutInfo.pPushConstantRanges = &pushConstantRange; - - result = vkCreatePipelineLayout(device, &layoutInfo, nullptr, - &pipelineLay); - if (result != VK_SUCCESS) - SLEAK_RETURN_ERR("Failed to create graphics pipeline layout!"); - // Create pipeline VkGraphicsPipelineCreateInfo pipelineInfo{}; pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; @@ -249,15 +400,14 @@ bool VulkanRenderer::CreateGraphicsPipeline() { pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; pipelineInfo.basePipelineIndex = -1; - result = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, - &pipelineInfo, nullptr, &pipeline); + VkResult result = vkCreateGraphicsPipelines( + device, m_pipelineCache, 1, &pipelineInfo, nullptr, &pipeline); if (result != VK_SUCCESS) SLEAK_ERROR("Failed to create graphics pipeline!!"); return true; } - /// Creates the main forward render pass with optional MSAA color and resolve attachments. bool VulkanRenderer::CreateRenderPass() { const bool msaaEnabled = (m_msaaSamples != VK_SAMPLE_COUNT_1_BIT); @@ -389,52 +539,65 @@ bool VulkanRenderer::CreateFrameBuffer() { return true; } -/// Compiles the skybox shaders and creates the skybox descriptor set and pipeline. +/// Compiles the skybox shaders on first use, sizes the skybox descriptor sets +/// to the swapchain image count, and creates the skybox pipeline. bool VulkanRenderer::CreateSkyboxPipeline() { // 1. Compile skybox shaders - skyboxShader = new VulkanShader(device); - if (!skyboxShader->compile("assets/shaders/skybox")) { - SLEAK_ERROR("VulkanRenderer: Failed to compile skybox shaders"); - delete skyboxShader; - skyboxShader = nullptr; - return false; + if (!skyboxShader) { + skyboxShader = new VulkanShader(device); + if (!skyboxShader->compile("assets/shaders/skybox")) { + SLEAK_ERROR("VulkanRenderer: Failed to compile skybox shaders"); + delete skyboxShader; + skyboxShader = nullptr; + return false; + } } // 2. Create skybox descriptor pool and sets (same layout as main) uint32_t imageCount = static_cast(swapChainImages.size()); - VkDescriptorPoolSize poolSize{}; - poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - poolSize.descriptorCount = imageCount; - - VkDescriptorPoolCreateInfo poolInfo{}; - poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; - poolInfo.poolSizeCount = 1; - poolInfo.pPoolSizes = &poolSize; - poolInfo.maxSets = imageCount; - - if (vkCreateDescriptorPool(device, &poolInfo, nullptr, - &skyboxDescriptorPool) != VK_SUCCESS) { - SLEAK_ERROR("VulkanRenderer: Failed to create skybox descriptor pool"); - return false; - } - - std::vector layouts(imageCount, - descriptorSetLayout); - - VkDescriptorSetAllocateInfo allocInfo{}; - allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; - allocInfo.descriptorPool = skyboxDescriptorPool; - allocInfo.descriptorSetCount = imageCount; - allocInfo.pSetLayouts = layouts.data(); + if (skyboxDescriptorSets.size() != imageCount) { + if (skyboxDescriptorPool) { + vkDestroyDescriptorPool(device, skyboxDescriptorPool, nullptr); + skyboxDescriptorPool = VK_NULL_HANDLE; + } + skyboxDescriptorSets.clear(); + + VkDescriptorPoolSize poolSize{}; + poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + poolSize.descriptorCount = imageCount; + + VkDescriptorPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + poolInfo.maxSets = imageCount; + + if (vkCreateDescriptorPool(device, &poolInfo, nullptr, + &skyboxDescriptorPool) != VK_SUCCESS) { + SLEAK_ERROR( + "VulkanRenderer: Failed to create skybox descriptor pool"); + return false; + } - skyboxDescriptorSets.resize(imageCount); - if (vkAllocateDescriptorSets(device, &allocInfo, - skyboxDescriptorSets.data()) != - VK_SUCCESS) { - SLEAK_ERROR("VulkanRenderer: Failed to allocate skybox descriptor sets"); - return false; + std::vector layouts(imageCount, + descriptorSetLayout); + + VkDescriptorSetAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = skyboxDescriptorPool; + allocInfo.descriptorSetCount = imageCount; + allocInfo.pSetLayouts = layouts.data(); + + std::vector sets(imageCount); + if (vkAllocateDescriptorSets(device, &allocInfo, sets.data()) != + VK_SUCCESS) { + SLEAK_ERROR( + "VulkanRenderer: Failed to allocate skybox descriptor sets"); + return false; + } + skyboxDescriptorSets = std::move(sets); } // 3. Create skybox pipeline (same as main but with skybox shaders @@ -452,13 +615,13 @@ bool VulkanRenderer::CreateSkyboxPipeline() { static_cast(dynamicStates.size()); dynamicState.pDynamicStates = dynamicStates.data(); - // Same vertex layout as main pipeline + // Same vertex layout as main pipeline (locations 0-4) VkVertexInputBindingDescription bindingDescription{}; bindingDescription.binding = 0; bindingDescription.stride = sizeof(Vertex); bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; - std::array attributeDescs{}; + std::array attributeDescs{}; attributeDescs[0].binding = 0; attributeDescs[0].location = 0; attributeDescs[0].format = VK_FORMAT_R32G32B32_SFLOAT; @@ -484,16 +647,6 @@ bool VulkanRenderer::CreateSkyboxPipeline() { attributeDescs[4].format = VK_FORMAT_R32G32_SFLOAT; attributeDescs[4].offset = offsetof(Vertex, u); - attributeDescs[5].binding = 0; - attributeDescs[5].location = 5; - attributeDescs[5].format = VK_FORMAT_R32G32B32A32_SINT; - attributeDescs[5].offset = offsetof(Vertex, boneIDs); - - attributeDescs[6].binding = 0; - attributeDescs[6].location = 6; - attributeDescs[6].format = VK_FORMAT_R32G32B32A32_SFLOAT; - attributeDescs[6].offset = offsetof(Vertex, boneWeights); - VkPipelineVertexInputStateCreateInfo vertexInputInfo{}; vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; @@ -575,7 +728,7 @@ bool VulkanRenderer::CreateSkyboxPipeline() { pipelineInfo.basePipelineIndex = -1; VkResult result = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &skyboxPipeline); + device, m_pipelineCache, 1, &pipelineInfo, nullptr, &skyboxPipeline); if (result != VK_SUCCESS) { SLEAK_ERROR("VulkanRenderer: Failed to create skybox pipeline!"); return false; @@ -590,12 +743,14 @@ bool VulkanRenderer::CreateSkyboxPipeline() { bool VulkanRenderer::CreateDebugLinePipeline() { if (debugLinePipeline != VK_NULL_HANDLE) return true; - debugLineShader = new VulkanShader(device); - if (!debugLineShader->compile("assets/shaders/debug_line")) { - SLEAK_ERROR("VulkanRenderer: Failed to compile debug line shaders"); - delete debugLineShader; - debugLineShader = nullptr; - return false; + if (!debugLineShader) { + debugLineShader = new VulkanShader(device); + if (!debugLineShader->compile("assets/shaders/debug_line")) { + SLEAK_ERROR("VulkanRenderer: Failed to compile debug line shaders"); + delete debugLineShader; + debugLineShader = nullptr; + return false; + } } VkPipelineShaderStageCreateInfo shaderStages[] = { @@ -616,14 +771,12 @@ bool VulkanRenderer::CreateDebugLinePipeline() { bindingDescription.stride = sizeof(Vertex); bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; - std::array attributeDescs{}; + std::array attributeDescs{}; attributeDescs[0] = {0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, px)}; attributeDescs[1] = {1, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, nx)}; attributeDescs[2] = {2, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(Vertex, tx)}; attributeDescs[3] = {3, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(Vertex, r)}; attributeDescs[4] = {4, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(Vertex, u)}; - attributeDescs[5] = {5, 0, VK_FORMAT_R32G32B32A32_SINT, offsetof(Vertex, boneIDs)}; - attributeDescs[6] = {6, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(Vertex, boneWeights)}; VkPipelineVertexInputStateCreateInfo vertexInputInfo{}; vertexInputInfo.sType = @@ -705,7 +858,7 @@ bool VulkanRenderer::CreateDebugLinePipeline() { pipelineInfo.basePipelineIndex = -1; VkResult result = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &debugLinePipeline); + device, m_pipelineCache, 1, &pipelineInfo, nullptr, &debugLinePipeline); if (result != VK_SUCCESS) { SLEAK_ERROR("VulkanRenderer: Failed to create debug line pipeline!"); return false; @@ -734,445 +887,246 @@ static VkFormat ToVkVertexFormat(VertexAttribFormat format) { return VK_FORMAT_R32G32B32_SFLOAT; } -/// Creates any missing pipeline variant (main, shadow, GBuffer, transparent) -/// for a registered vertex layout. Variants whose shader fails to load are -/// marked absent so draws skip them without retrying every frame. -bool VulkanRenderer::CreateCustomFormatPipelines(VertexFormatHandle format) { +/// True when the render pass a custom-format variant targets exists. +bool VulkanRenderer::CustomPassAvailable(CustomPass pass) const { + switch (pass) { + case CustomPass::Shadow: + return m_shadowRenderPass != VK_NULL_HANDLE && + m_shadowShader != nullptr; + case CustomPass::GBuffer: + return m_gbufferResourcesCreated && + m_gbufferRenderPass != VK_NULL_HANDLE; + default: + return true; + } +} + +/// Returns a format's pipeline for a pass, compiling it on first request. +/// A variant that failed once stays absent instead of recompiling per frame. +VkPipeline VulkanRenderer::GetCustomFormatPipeline(VertexFormatHandle format, + CustomPass pass) { + if (format == 0) return VK_NULL_HANDLE; + if (format > m_customFormatPipelines.size()) { + if (!VertexFormatRegistry::Get(format)) return VK_NULL_HANDLE; + m_customFormatPipelines.resize(format); + } + CustomFormatPipelines& pipes = m_customFormatPipelines[format - 1]; + const size_t slot = static_cast(pass); + if (pipes.pipelines[slot] != VK_NULL_HANDLE || pipes.failed[slot]) + return pipes.pipelines[slot]; + + // Lazy fallback + if (!CustomPassAvailable(pass)) return VK_NULL_HANDLE; const VertexLayoutDesc* desc = VertexFormatRegistry::Get(format); - if (!desc || desc->stride == 0 || desc->attributes.empty()) return false; + if (!desc) return VK_NULL_HANDLE; + pipes.pipelines[slot] = BuildCustomFormatPipeline(format, *desc, pass); + pipes.failed[slot] = pipes.pipelines[slot] == VK_NULL_HANDLE; + return pipes.pipelines[slot]; +} - CustomFormatPipelines& pipes = m_customFormatPipelines[format]; +/// Compiles one pipeline variant of a registered layout; returns +/// VK_NULL_HANDLE when its stem is empty or compilation fails. +VkPipeline VulkanRenderer::BuildCustomFormatPipeline( + VertexFormatHandle format, const VertexLayoutDesc& desc, CustomPass pass) { + static constexpr const char* kPassNames[CUSTOM_PASS_COUNT] = { + "main", "shadow", "GBuffer", "transparent"}; + const char* passName = kPassNames[static_cast(pass)]; + + if (desc.stride == 0 || desc.attributes.empty()) { + SLEAK_ERROR("VulkanRenderer: Vertex format {} has an empty layout", + format); + return VK_NULL_HANDLE; + } - // Vertex input state shared by all four variants - VkVertexInputBindingDescription bindingDescription{}; - bindingDescription.binding = 0; - bindingDescription.stride = desc->stride; - bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + const std::string* stem = &desc.shaderStem; + if (pass == CustomPass::Shadow) stem = &desc.shadowShaderStem; + if (pass == CustomPass::GBuffer) stem = &desc.gbufferShaderStem; + if (pass == CustomPass::Transparent) stem = &desc.transparentShaderStem; + if (stem->empty()) { + if (pass == CustomPass::Main) + SLEAK_ERROR( + "VulkanRenderer: Vertex format {} has no main shader stem", + format); + return VK_NULL_HANDLE; + } - std::vector attributeDescs; - attributeDescs.reserve(desc->attributes.size()); - for (const auto& attr : desc->attributes) { - attributeDescs.push_back({attr.location, 0, - ToVkVertexFormat(attr.format), attr.offset}); + // Shader stages + const std::string base = "assets/shaders/" + *stem; + std::string shaderPath = base; + VulkanShader shader(device); + bool compiled = false; + if (pass == CustomPass::Shadow) { + shaderPath = base + ".vert.spv"; + compiled = shader.compileVertexOnly(shaderPath); + } else if (pass == CustomPass::GBuffer) { + shaderPath = base + ".vert.spv"; + compiled = + shader.compile(shaderPath, "assets/shaders/gbuffer.frag.spv"); + } else { + compiled = shader.compile(base); + } + if (!compiled) { + SLEAK_ERROR( + "VulkanRenderer: Failed to compile '{}' for vertex format {}", + shaderPath, format); + return VK_NULL_HANDLE; + } + VkPipelineShaderStageCreateInfo stages[] = {shader.GetVertexInfo(), + shader.GetFragInfo()}; + + // Vertex input + VkVertexInputBindingDescription binding{}; + binding.binding = 0; + binding.stride = desc.stride; + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector attributes; + attributes.reserve(desc.attributes.size()); + for (const auto& attr : desc.attributes) { + attributes.push_back( + {attr.location, 0, ToVkVertexFormat(attr.format), attr.offset}); } - VkPipelineVertexInputStateCreateInfo vertexInputInfo{}; - vertexInputInfo.sType = + VkPipelineVertexInputStateCreateInfo vertexInput{}; + vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; - vertexInputInfo.vertexBindingDescriptionCount = 1; - vertexInputInfo.pVertexBindingDescriptions = &bindingDescription; - vertexInputInfo.vertexAttributeDescriptionCount = - static_cast(attributeDescs.size()); - vertexInputInfo.pVertexAttributeDescriptions = attributeDescs.data(); + vertexInput.vertexBindingDescriptionCount = 1; + vertexInput.pVertexBindingDescriptions = &binding; + vertexInput.vertexAttributeDescriptionCount = + static_cast(attributes.size()); + vertexInput.pVertexAttributeDescriptions = attributes.data(); + + VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; + inputAssembly.sType = + VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; + inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + inputAssembly.primitiveRestartEnable = VK_FALSE; - std::vector dynamicStates = { - VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + VkPipelineViewportStateCreateInfo viewportState{}; + viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + viewportState.viewportCount = 1; + viewportState.scissorCount = 1; + const VkDynamicState dynamicStates[] = {VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR}; VkPipelineDynamicStateCreateInfo dynamicState{}; dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; - dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); - dynamicState.pDynamicStates = dynamicStates.data(); + dynamicState.dynamicStateCount = 2; + dynamicState.pDynamicStates = dynamicStates; - // Forward/main variant — opaque geometry in the forward pass - if (pipes.main == VK_NULL_HANDLE && !pipes.mainFailed) { - if (desc->shaderStem.empty()) { - SLEAK_ERROR("VulkanRenderer: Vertex format {} has no main shader stem", format); - pipes.mainFailed = true; - } else { - const std::string stemPath = "assets/shaders/" + desc->shaderStem; - auto* shader = new VulkanShader(device); - if (!shader->compile(stemPath)) { - SLEAK_ERROR("VulkanRenderer: Failed to compile '{}' for vertex format {}", - stemPath, format); - pipes.mainFailed = true; - delete shader; - } else { - VkPipelineShaderStageCreateInfo shaderStages[] = { - shader->GetVertexInfo(), shader->GetFragInfo()}; - - VkPipelineInputAssemblyStateCreateInfo inputAssemblyInfo{}; - inputAssemblyInfo.sType = - VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; - inputAssemblyInfo.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - inputAssemblyInfo.primitiveRestartEnable = VK_FALSE; - - VkPipelineViewportStateCreateInfo viewportInfo{}; - viewportInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; - viewportInfo.viewportCount = 1; - viewportInfo.scissorCount = 1; - - VkPipelineRasterizationStateCreateInfo rasterizer{}; - rasterizer.sType = - VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; - rasterizer.depthClampEnable = VK_FALSE; - rasterizer.rasterizerDiscardEnable = VK_FALSE; - rasterizer.polygonMode = VK_POLYGON_MODE_FILL; - rasterizer.lineWidth = 1.0f; - rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; - rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; - rasterizer.depthBiasEnable = VK_FALSE; - - VkPipelineMultisampleStateCreateInfo msaa{}; - msaa.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; - msaa.sampleShadingEnable = VK_FALSE; - msaa.rasterizationSamples = m_msaaSamples; - - VkPipelineDepthStencilStateCreateInfo depthStencil{}; - depthStencil.sType = - VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; - depthStencil.depthTestEnable = VK_TRUE; - depthStencil.depthWriteEnable = VK_TRUE; - depthStencil.depthCompareOp = VK_COMPARE_OP_LESS; - depthStencil.depthBoundsTestEnable = VK_FALSE; - depthStencil.stencilTestEnable = VK_FALSE; - - VkPipelineColorBlendAttachmentState colorBlendAttachment{}; - colorBlendAttachment.blendEnable = VK_TRUE; - colorBlendAttachment.colorWriteMask = - VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | - VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; - colorBlendAttachment.dstColorBlendFactor = - VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; - colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; - colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; - colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; - colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; - - VkPipelineColorBlendStateCreateInfo colorBlendInfo{}; - colorBlendInfo.sType = - VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; - colorBlendInfo.logicOpEnable = VK_FALSE; - colorBlendInfo.attachmentCount = 1; - colorBlendInfo.pAttachments = &colorBlendAttachment; - - VkGraphicsPipelineCreateInfo pipelineInfo{}; - pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; - pipelineInfo.stageCount = 2; - pipelineInfo.pStages = shaderStages; - pipelineInfo.pVertexInputState = &vertexInputInfo; - pipelineInfo.pInputAssemblyState = &inputAssemblyInfo; - pipelineInfo.pViewportState = &viewportInfo; - pipelineInfo.pRasterizationState = &rasterizer; - pipelineInfo.pMultisampleState = &msaa; - pipelineInfo.pDepthStencilState = &depthStencil; - pipelineInfo.pColorBlendState = &colorBlendInfo; - pipelineInfo.pDynamicState = &dynamicState; - pipelineInfo.layout = pipelineLay; // reuse main layout (same descriptor sets) - pipelineInfo.subpass = 0; - pipelineInfo.renderPass = - (m_deferredEnabled && m_forwardRenderPass != VK_NULL_HANDLE) - ? m_forwardRenderPass : renderPass; - pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; - pipelineInfo.basePipelineIndex = -1; - - VkResult result = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipes.main); - delete shader; - - if (result != VK_SUCCESS) { - SLEAK_ERROR("VulkanRenderer: Failed to create main pipeline for vertex format {}", - format); - pipes.main = VK_NULL_HANDLE; - pipes.mainFailed = true; - } else { - SLEAK_INFO("VulkanRenderer: Custom format {} main pipeline created", format); - } - } - } + // Rasterization + VkPipelineRasterizationStateCreateInfo rasterizer{}; + rasterizer.sType = + VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + rasterizer.polygonMode = VK_POLYGON_MODE_FILL; + rasterizer.lineWidth = 1.0f; + rasterizer.cullMode = pass == CustomPass::Transparent + ? VK_CULL_MODE_NONE + : VK_CULL_MODE_BACK_BIT; + rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + if (pass == CustomPass::Shadow) { + rasterizer.depthBiasEnable = VK_TRUE; + rasterizer.depthBiasConstantFactor = SHADOW_DEPTH_BIAS_CONSTANT; + rasterizer.depthBiasSlopeFactor = SHADOW_DEPTH_BIAS_SLOPE; } - // Shadow variant — depth-only, rendered into the shadow map - if (pipes.shadow == VK_NULL_HANDLE && !pipes.shadowFailed && - !desc->shadowShaderStem.empty() && - m_shadowRenderPass != VK_NULL_HANDLE && m_shadowShader) { - const std::string shadowPath = - "assets/shaders/" + desc->shadowShaderStem + ".vert.spv"; - auto* shadowShader = new VulkanShader(device); - if (!shadowShader->compileVertexOnly(shadowPath)) { - SLEAK_ERROR("VulkanRenderer: Failed to compile '{}' for vertex format {}", - shadowPath, format); - pipes.shadowFailed = true; - delete shadowShader; - } else { - VkPipelineShaderStageCreateInfo shaderStage = shadowShader->GetVertexInfo(); - - VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; - inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; - inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - inputAssembly.primitiveRestartEnable = VK_FALSE; - - VkPipelineViewportStateCreateInfo viewportState{}; - viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; - viewportState.viewportCount = 1; - viewportState.scissorCount = 1; - - VkPipelineRasterizationStateCreateInfo rasterizer{}; - rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; - rasterizer.depthClampEnable = VK_FALSE; - rasterizer.rasterizerDiscardEnable = VK_FALSE; - rasterizer.polygonMode = VK_POLYGON_MODE_FILL; - rasterizer.lineWidth = 1.0f; - rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; - rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; - rasterizer.depthBiasEnable = VK_TRUE; - rasterizer.depthBiasConstantFactor = 1.25f; - rasterizer.depthBiasSlopeFactor = 1.75f; - rasterizer.depthBiasClamp = 0.0f; - - VkPipelineMultisampleStateCreateInfo msaa{}; - msaa.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; - msaa.sampleShadingEnable = VK_FALSE; - msaa.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; - - VkPipelineDepthStencilStateCreateInfo depthStencil{}; - depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; - depthStencil.depthTestEnable = VK_TRUE; - depthStencil.depthWriteEnable = VK_TRUE; - depthStencil.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL; - depthStencil.depthBoundsTestEnable = VK_FALSE; - depthStencil.stencilTestEnable = VK_FALSE; - - VkPipelineColorBlendStateCreateInfo colorBlendInfo{}; - colorBlendInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; - colorBlendInfo.logicOpEnable = VK_FALSE; - colorBlendInfo.attachmentCount = 0; - - VkGraphicsPipelineCreateInfo pipelineInfo{}; - pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; - pipelineInfo.stageCount = 1; - pipelineInfo.pStages = &shaderStage; - pipelineInfo.pVertexInputState = &vertexInputInfo; - pipelineInfo.pInputAssemblyState = &inputAssembly; - pipelineInfo.pViewportState = &viewportState; - pipelineInfo.pRasterizationState = &rasterizer; - pipelineInfo.pMultisampleState = &msaa; - pipelineInfo.pDepthStencilState = &depthStencil; - pipelineInfo.pColorBlendState = &colorBlendInfo; - pipelineInfo.pDynamicState = &dynamicState; - pipelineInfo.layout = pipelineLay; - pipelineInfo.renderPass = m_shadowRenderPass; - pipelineInfo.subpass = 0; - pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; - pipelineInfo.basePipelineIndex = -1; - - VkResult result = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipes.shadow); - delete shadowShader; - - if (result != VK_SUCCESS) { - SLEAK_ERROR("VulkanRenderer: Failed to create shadow pipeline for vertex format {}", - format); - pipes.shadow = VK_NULL_HANDLE; - pipes.shadowFailed = true; - } else { - SLEAK_INFO("VulkanRenderer: Custom format {} shadow pipeline created", format); - } - } - } + // Multisampling + const bool forwardTarget = + m_deferredEnabled && m_forwardRenderPass != VK_NULL_HANDLE; + VkPipelineMultisampleStateCreateInfo multisample{}; + multisample.sType = + VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + if (pass == CustomPass::Main || + (pass == CustomPass::Transparent && !forwardTarget)) + multisample.rasterizationSamples = m_msaaSamples; - // GBuffer variant — deferred geometry pass, writes the GBuffer attachments - if (pipes.gbuffer == VK_NULL_HANDLE && !pipes.gbufferFailed && - !desc->gbufferShaderStem.empty() && m_gbufferResourcesCreated && - m_gbufferRenderPass != VK_NULL_HANDLE) { - const std::string gbufVertPath = - "assets/shaders/" + desc->gbufferShaderStem + ".vert.spv"; - auto* gbufShader = new VulkanShader(device); - if (!gbufShader->compile(gbufVertPath, "assets/shaders/gbuffer.frag.spv")) { - SLEAK_ERROR("VulkanRenderer: Failed to compile '{}' for vertex format {}", - gbufVertPath, format); - pipes.gbufferFailed = true; - delete gbufShader; - } else { - VkPipelineShaderStageCreateInfo gbufStages[] = { - gbufShader->GetVertexInfo(), gbufShader->GetFragInfo()}; - - VkPipelineInputAssemblyStateCreateInfo ia{}; - ia.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; - ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - ia.primitiveRestartEnable = VK_FALSE; - - VkPipelineViewportStateCreateInfo vp{}; - vp.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; - vp.viewportCount = 1; - vp.scissorCount = 1; - - VkPipelineRasterizationStateCreateInfo rs{}; - rs.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; - rs.polygonMode = VK_POLYGON_MODE_FILL; - rs.lineWidth = 1.0f; - rs.cullMode = VK_CULL_MODE_BACK_BIT; - rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; - - VkPipelineMultisampleStateCreateInfo ms{}; - ms.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; - ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; - - VkPipelineDepthStencilStateCreateInfo ds{}; - ds.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; - ds.depthTestEnable = VK_TRUE; - ds.depthWriteEnable = VK_TRUE; - ds.depthCompareOp = VK_COMPARE_OP_LESS; - - VkPipelineColorBlendAttachmentState opaqueBlend{}; - opaqueBlend.blendEnable = VK_FALSE; - opaqueBlend.colorWriteMask = - VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | - VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - std::array gbAtts; - gbAtts.fill(opaqueBlend); - - VkPipelineColorBlendStateCreateInfo cb{}; - cb.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; - cb.attachmentCount = static_cast(gbAtts.size()); - cb.pAttachments = gbAtts.data(); - - VkGraphicsPipelineCreateInfo gbPipeInfo{}; - gbPipeInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; - gbPipeInfo.stageCount = 2; - gbPipeInfo.pStages = gbufStages; - gbPipeInfo.pVertexInputState = &vertexInputInfo; - gbPipeInfo.pInputAssemblyState = &ia; - gbPipeInfo.pViewportState = &vp; - gbPipeInfo.pRasterizationState = &rs; - gbPipeInfo.pMultisampleState = &ms; - gbPipeInfo.pDepthStencilState = &ds; - gbPipeInfo.pColorBlendState = &cb; - gbPipeInfo.pDynamicState = &dynamicState; - // gbuffer.frag reads set 0 as m_pbrMaterialDSL, so the GBuffer - // variant must use m_gbufferGeomLayout, never pipelineLay. - gbPipeInfo.layout = m_gbufferGeomLayout; - gbPipeInfo.renderPass = m_gbufferRenderPass; - gbPipeInfo.subpass = 0; - - VkResult gbResult = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &gbPipeInfo, nullptr, &pipes.gbuffer); - delete gbufShader; - - if (gbResult != VK_SUCCESS) { - SLEAK_ERROR("VulkanRenderer: Failed to create GBuffer pipeline for vertex format {}", - format); - pipes.gbuffer = VK_NULL_HANDLE; - pipes.gbufferFailed = true; - } else { - SLEAK_INFO("VulkanRenderer: Custom format {} GBuffer pipeline created", format); - } - } + VkPipelineDepthStencilStateCreateInfo depthStencil{}; + depthStencil.sType = + VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; + depthStencil.depthTestEnable = VK_TRUE; + depthStencil.depthWriteEnable = VK_TRUE; + depthStencil.depthCompareOp = pass == CustomPass::Shadow + ? VK_COMPARE_OP_LESS_OR_EQUAL + : VK_COMPARE_OP_LESS; + + // Color blending + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | + VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + if (pass == CustomPass::Main || pass == CustomPass::Transparent) { + blend.blendEnable = VK_TRUE; + blend.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + blend.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blend.colorBlendOp = VK_BLEND_OP_ADD; + // Transparent draws zero the HDR scene alpha: TAA's reactive mask + const bool transparent = pass == CustomPass::Transparent; + blend.srcAlphaBlendFactor = + transparent ? VK_BLEND_FACTOR_ZERO : VK_BLEND_FACTOR_ONE; + blend.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + blend.alphaBlendOp = VK_BLEND_OP_ADD; } + std::array blends; + blends.fill(blend); + + VkPipelineColorBlendStateCreateInfo colorBlend{}; + colorBlend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; + colorBlend.attachmentCount = 1; + if (pass == CustomPass::Shadow) colorBlend.attachmentCount = 0; + if (pass == CustomPass::GBuffer) colorBlend.attachmentCount = GBUFFER_COUNT; + colorBlend.pAttachments = + colorBlend.attachmentCount > 0 ? blends.data() : nullptr; + + // Pipeline + VkGraphicsPipelineCreateInfo pipelineInfo{}; + pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + pipelineInfo.stageCount = pass == CustomPass::Shadow ? 1 : 2; + pipelineInfo.pStages = stages; + pipelineInfo.pVertexInputState = &vertexInput; + pipelineInfo.pInputAssemblyState = &inputAssembly; + pipelineInfo.pViewportState = &viewportState; + pipelineInfo.pRasterizationState = &rasterizer; + pipelineInfo.pMultisampleState = &multisample; + pipelineInfo.pDepthStencilState = &depthStencil; + pipelineInfo.pColorBlendState = &colorBlend; + pipelineInfo.pDynamicState = &dynamicState; + pipelineInfo.layout = + pass == CustomPass::GBuffer ? m_gbufferGeomLayout : pipelineLay; + pipelineInfo.renderPass = forwardTarget ? m_forwardRenderPass : renderPass; + if (pass == CustomPass::Shadow) + pipelineInfo.renderPass = m_shadowRenderPass; + if (pass == CustomPass::GBuffer) + pipelineInfo.renderPass = m_gbufferRenderPass; + pipelineInfo.subpass = 0; + pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; + pipelineInfo.basePipelineIndex = -1; - // Transparent variant — alpha-blended, two-sided, runs in the forward - // transparent pass over the deferred scene image. - if (pipes.transparent == VK_NULL_HANDLE && !pipes.transparentFailed && - !desc->transparentShaderStem.empty()) { - const std::string transparentPath = - "assets/shaders/" + desc->transparentShaderStem; - auto* transparentShader = new VulkanShader(device); - if (!transparentShader->compile(transparentPath)) { - SLEAK_ERROR("VulkanRenderer: Failed to compile '{}' for vertex format {}", - transparentPath, format); - pipes.transparentFailed = true; - delete transparentShader; - } else { - VkPipelineShaderStageCreateInfo trStages[] = { - transparentShader->GetVertexInfo(), - transparentShader->GetFragInfo()}; - - VkPipelineInputAssemblyStateCreateInfo ia{}; - ia.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; - ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - ia.primitiveRestartEnable = VK_FALSE; - - VkPipelineViewportStateCreateInfo vp{}; - vp.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; - vp.viewportCount = 1; - vp.scissorCount = 1; - - VkPipelineRasterizationStateCreateInfo rs{}; - rs.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; - rs.depthClampEnable = VK_FALSE; - rs.rasterizerDiscardEnable = VK_FALSE; - rs.polygonMode = VK_POLYGON_MODE_FILL; - rs.lineWidth = 1.0f; - // Transparent surfaces are viewed from both sides - rs.cullMode = VK_CULL_MODE_NONE; - rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; - rs.depthBiasEnable = VK_FALSE; - - VkPipelineMultisampleStateCreateInfo ms{}; - ms.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; - ms.sampleShadingEnable = VK_FALSE; - // The forward transparent pass runs on m_forwardRenderPass (1 sample) - // when deferred is enabled; use the main render pass samples otherwise. - ms.rasterizationSamples = - (m_deferredEnabled && m_forwardRenderPass != VK_NULL_HANDLE) - ? VK_SAMPLE_COUNT_1_BIT - : m_msaaSamples; - - VkPipelineDepthStencilStateCreateInfo ds{}; - ds.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; - ds.depthTestEnable = VK_TRUE; - ds.depthWriteEnable = VK_TRUE; - ds.depthCompareOp = VK_COMPARE_OP_LESS; - ds.depthBoundsTestEnable = VK_FALSE; - ds.stencilTestEnable = VK_FALSE; - - VkPipelineColorBlendAttachmentState blendAtt{}; - blendAtt.blendEnable = VK_TRUE; - blendAtt.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; - blendAtt.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; - blendAtt.colorBlendOp = VK_BLEND_OP_ADD; - blendAtt.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; - blendAtt.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; - blendAtt.alphaBlendOp = VK_BLEND_OP_ADD; - blendAtt.colorWriteMask = - VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | - VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - - VkPipelineColorBlendStateCreateInfo cb{}; - cb.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; - cb.logicOpEnable = VK_FALSE; - cb.attachmentCount = 1; - cb.pAttachments = &blendAtt; - - VkGraphicsPipelineCreateInfo trPipeInfo{}; - trPipeInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; - trPipeInfo.stageCount = 2; - trPipeInfo.pStages = trStages; - trPipeInfo.pVertexInputState = &vertexInputInfo; - trPipeInfo.pInputAssemblyState = &ia; - trPipeInfo.pViewportState = &vp; - trPipeInfo.pRasterizationState = &rs; - trPipeInfo.pMultisampleState = &ms; - trPipeInfo.pDepthStencilState = &ds; - trPipeInfo.pColorBlendState = &cb; - trPipeInfo.pDynamicState = &dynamicState; - trPipeInfo.layout = pipelineLay; // reuse main layout (same descriptor sets) - trPipeInfo.subpass = 0; - trPipeInfo.renderPass = - (m_deferredEnabled && m_forwardRenderPass != VK_NULL_HANDLE) - ? m_forwardRenderPass - : renderPass; - trPipeInfo.basePipelineHandle = VK_NULL_HANDLE; - trPipeInfo.basePipelineIndex = -1; - - VkResult trResult = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &trPipeInfo, nullptr, &pipes.transparent); - delete transparentShader; - - if (trResult != VK_SUCCESS) { - SLEAK_ERROR("VulkanRenderer: Failed to create transparent pipeline for vertex format {}", - format); - pipes.transparent = VK_NULL_HANDLE; - pipes.transparentFailed = true; - } else { - SLEAK_INFO("VulkanRenderer: Custom format {} transparent pipeline created", format); - } - } + VkPipeline created = VK_NULL_HANDLE; + if (vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &pipelineInfo, + nullptr, &created) != VK_SUCCESS) { + SLEAK_ERROR( + "VulkanRenderer: Failed to create {} pipeline for vertex format {}", + passName, format); + return VK_NULL_HANDLE; } - - return pipes.main != VK_NULL_HANDLE; + SLEAK_INFO("VulkanRenderer: Custom format {} {} pipeline created", format, + passName); + return created; } +/// Compiles the variants the active render path draws with for every +/// registered format not prebuilt yet; the forward main variant is skipped +/// while deferred is active and only built if a draw asks for it. +void VulkanRenderer::PrebuildCustomFormatPipelines() { + while (VertexFormatRegistry::Get(m_customFormatsPrebuilt + 1)) { + const VertexFormatHandle format = ++m_customFormatsPrebuilt; + GetCustomFormatPipeline(format, CustomPass::Shadow); + GetCustomFormatPipeline(format, IsDeferredEnabled() + ? CustomPass::GBuffer + : CustomPass::Main); + GetCustomFormatPipeline(format, CustomPass::Transparent); + } +} /// Binds the custom-format pipeline matching the currently active render pass. void VulkanRenderer::BeginCustomFormatPass(VertexFormatHandle format) { @@ -1180,30 +1134,30 @@ void VulkanRenderer::BeginCustomFormatPass(VertexFormatHandle format) { if (format == 0) return; if (m_activeCustomFormat == format) return; m_activeCustomFormat = format; - m_customFormatUnbound = false; - if (!CreateCustomFormatPipelines(format)) { - m_customFormatUnbound = true; - return; - } - const CustomFormatPipelines& pipes = m_customFormatPipelines[format]; - - VkPipeline target; + CustomPass pass = CustomPass::Main; if (m_shadowPassActive) { - target = pipes.shadow; + pass = CustomPass::Shadow; } else if (m_inGeometryPass) { - target = pipes.gbuffer; + pass = CustomPass::GBuffer; } else if (m_inForwardTransparentPass) { - target = pipes.transparent; - } else { - target = pipes.main; + pass = CustomPass::Transparent; } // No variant for this pass: drop the draws rather than rasterize this // layout's vertices through a pipeline built for another one. - if (target == VK_NULL_HANDLE) { - m_customFormatUnbound = true; - return; + const VkPipeline target = GetCustomFormatPipeline(format, pass); + m_customFormatUnbound = target == VK_NULL_HANDLE; + if (m_customFormatUnbound) return; + + // Scene-reading shaders need set 3 bindings 2-5 written + if (pass == CustomPass::Transparent) { + const VertexLayoutDesc* desc = VertexFormatRegistry::Get(format); + if (desc && desc->transparentReadsScene) { + m_customFormatUnbound = !m_sceneReadCreated; + if (m_customFormatUnbound) return; + PrepareSceneRead(); + } } vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, target); @@ -1239,18 +1193,15 @@ void VulkanRenderer::EndCustomFormatPass() { } } - -/// Destroys every cached custom-format pipeline and clears the map. +/// Destroys every cached custom-format pipeline and clears the table. void VulkanRenderer::DestroyCustomFormatPipelines() { - for (auto& entry : m_customFormatPipelines) { - CustomFormatPipelines& pipes = entry.second; - if (pipes.main) vkDestroyPipeline(device, pipes.main, nullptr); - if (pipes.shadow) vkDestroyPipeline(device, pipes.shadow, nullptr); - if (pipes.gbuffer) vkDestroyPipeline(device, pipes.gbuffer, nullptr); - if (pipes.transparent) - vkDestroyPipeline(device, pipes.transparent, nullptr); + for (const CustomFormatPipelines& pipes : m_customFormatPipelines) { + for (VkPipeline pipe : pipes.pipelines) { + if (pipe) vkDestroyPipeline(device, pipe, nullptr); + } } m_customFormatPipelines.clear(); + m_customFormatsPrebuilt = 0; m_activeCustomFormat = 0; } @@ -1291,15 +1242,20 @@ void VulkanRenderer::EndDebugLinePass() { } } -/// Compiles the skinned shaders and creates the forward skinned pipeline. +/// Compiles the skinned shaders on first use and creates the forward skinned +/// pipeline. bool VulkanRenderer::CreateSkinnedPipeline() { + if (skinnedPipeline != VK_NULL_HANDLE) return true; + // 1. Compile skinned shaders - skinnedShader = new VulkanShader(device); - if (!skinnedShader->compile("assets/shaders/skinned_shader")) { - SLEAK_ERROR("VulkanRenderer: Failed to compile skinned shaders"); - delete skinnedShader; - skinnedShader = nullptr; - return false; + if (!skinnedShader) { + skinnedShader = new VulkanShader(device); + if (!skinnedShader->compile("assets/shaders/skinned_shader")) { + SLEAK_ERROR("VulkanRenderer: Failed to compile skinned shaders"); + delete skinnedShader; + skinnedShader = nullptr; + return false; + } } // 2. Create pipeline (same as main but with skinned shaders) @@ -1443,7 +1399,7 @@ bool VulkanRenderer::CreateSkinnedPipeline() { pipelineInfo.basePipelineIndex = -1; VkResult result = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &skinnedPipeline); + device, m_pipelineCache, 1, &pipelineInfo, nullptr, &skinnedPipeline); if (result != VK_SUCCESS) { SLEAK_ERROR("VulkanRenderer: Failed to create skinned pipeline!"); return false; diff --git a/src/Graphics/Vulkan/VulkanProfiler.cpp b/src/Graphics/Vulkan/VulkanProfiler.cpp new file mode 100644 index 00000000..1937d8a9 --- /dev/null +++ b/src/Graphics/Vulkan/VulkanProfiler.cpp @@ -0,0 +1,238 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" +#include "Core/Logger.hpp" + +namespace Sleak { +namespace RenderEngine { + +namespace { + +constexpr float kGpuLogIntervalSeconds = 2.0f; + +constexpr std::array kGpuRowNames = {"shadow", "gbuffer", "ssao", "lighting", + "forward", "taa", "ssr", "bloom", + "tonemap", "ui", "other", "total"}; + +} // namespace + +/// Creates the timestamp query pool; leaves profiling off when the graphics +/// queue has no timestamp support. +bool VulkanRenderer::CreateGpuProfiler() { + static_assert(kGpuRowNames.size() == GPU_ROW_COUNT); + + VkPhysicalDeviceProperties props; + vkGetPhysicalDeviceProperties(physicalDevice, &props); + + uint32_t familyCount = 0; + vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &familyCount, + nullptr); + std::vector families(familyCount); + vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &familyCount, + families.data()); + + const uint32_t validBits = + QueueIDs.GraphicsIndex < familyCount + ? families[QueueIDs.GraphicsIndex].timestampValidBits + : 0; + if (validBits == 0 || props.limits.timestampPeriod <= 0.0f) { + SLEAK_WARN( + "GPU profiler: graphics queue has no timestamp support, " + "--gpuprofile ignored"); + return false; + } + + VkQueryPoolCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; + info.queryType = VK_QUERY_TYPE_TIMESTAMP; + info.queryCount = MAX_FRAMES_IN_FLIGHT * GPU_QUERIES_PER_FRAME; + if (vkCreateQueryPool(device, &info, nullptr, &m_gpuQueryPool) != + VK_SUCCESS) { + SLEAK_WARN("GPU profiler: query pool creation failed"); + m_gpuQueryPool = VK_NULL_HANDLE; + return false; + } + + m_gpuTickNs = static_cast(props.limits.timestampPeriod); + m_gpuTickMask = validBits >= std::numeric_limits::digits + ? ~0ull + : ((1ull << validBits) - 1); + m_gpuSlots = {}; + m_gpuSumMs = {}; + m_gpuMaxMs = {}; + m_gpuRuns = {}; + m_gpuLastMs = {}; + m_gpuWindowFrames = 0; + m_gpuWindowTimer.Reset(); + m_gpuProfilerEnabled = true; + + SLEAK_INFO("GPU profiler: on ({} timestamp bits, {:.3f} ns/tick)", + validBits, m_gpuTickNs); + return true; +} + +/// Destroys the timestamp query pool. +void VulkanRenderer::CleanupGpuProfiler() { + m_gpuProfilerEnabled = false; + if (m_gpuQueryPool != VK_NULL_HANDLE) { + vkDestroyQueryPool(device, m_gpuQueryPool, nullptr); + m_gpuQueryPool = VK_NULL_HANDLE; + } +} + +/// Folds this slot's finished timestamps into the running window and logs the +/// table once the window elapses. Call after the slot's fence. +void VulkanRenderer::CollectGpuProfile() { + if (!m_gpuProfilerEnabled) return; + GpuProfileSlot& slot = m_gpuSlots[currentFrame]; + if (!slot.submitted) return; + slot.submitted = false; + + // value + availability per query + const uint32_t queryCount = 2 + 2 * slot.pairCount; + std::array data{}; + const VkResult res = vkGetQueryPoolResults( + device, m_gpuQueryPool, currentFrame * GPU_QUERIES_PER_FRAME, + queryCount, queryCount * 2 * sizeof(uint64_t), data.data(), + 2 * sizeof(uint64_t), + VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT); + if (res != VK_SUCCESS || data[1] == 0 || data[3] == 0) return; + + auto ticksToMs = [this](uint64_t begin, uint64_t end) { + const uint64_t ticks = (end - begin) & m_gpuTickMask; + return static_cast(static_cast(ticks) * m_gpuTickNs * + 1e-6); + }; + + std::array frameMs{}; + std::array ran{}; + float passSum = 0.0f; + for (uint32_t i = 0; i < slot.pairCount; ++i) { + const uint64_t* begin = &data[(2 + 2 * i) * 2]; + const uint64_t* end = &data[(3 + 2 * i) * 2]; + if (begin[1] == 0 || end[1] == 0) continue; + const float ms = ticksToMs(begin[0], end[0]); + const auto row = static_cast(slot.pairPass[i]); + frameMs[row] += ms; + ran[row] = true; + passSum += ms; + } + frameMs[GPU_ROW_TOTAL] = ticksToMs(data[0], data[2]); + frameMs[GPU_ROW_OTHER] = std::max(0.0f, frameMs[GPU_ROW_TOTAL] - passSum); + ran[GPU_ROW_TOTAL] = true; + ran[GPU_ROW_OTHER] = true; + + for (uint32_t row = 0; row < GPU_ROW_COUNT; ++row) { + m_gpuLastMs[row] = ran[row] ? frameMs[row] : 0.0f; + if (!ran[row]) continue; + m_gpuSumMs[row] += frameMs[row]; + m_gpuMaxMs[row] = std::max(m_gpuMaxMs[row], frameMs[row]); + ++m_gpuRuns[row]; + } + ++m_gpuWindowFrames; + + const float elapsed = m_gpuWindowTimer.Elapsed(); + if (elapsed < kGpuLogIntervalSeconds) return; + LogGpuProfile(elapsed); + m_gpuSumMs = {}; + m_gpuMaxMs = {}; + m_gpuRuns = {}; + m_gpuWindowFrames = 0; + m_gpuWindowTimer.Reset(); +} + +/// Resets this slot's queries and stamps the frame start. +void VulkanRenderer::BeginGpuFrame() { + if (!m_gpuProfilerEnabled) return; + GpuProfileSlot& slot = m_gpuSlots[currentFrame]; + slot = {}; + slot.recording = true; + const uint32_t base = currentFrame * GPU_QUERIES_PER_FRAME; + vkCmdResetQueryPool(command, m_gpuQueryPool, base, GPU_QUERIES_PER_FRAME); + vkCmdWriteTimestamp(command, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + m_gpuQueryPool, base); +} + +/// Closes any pass left open and stamps the frame end. +void VulkanRenderer::EndGpuFrame() { + if (!m_gpuProfilerEnabled) return; + GpuProfileSlot& slot = m_gpuSlots[currentFrame]; + if (!slot.recording) return; + if (slot.pairOpen) EndGpuPass(slot.pairPass[slot.pairCount]); + vkCmdWriteTimestamp(command, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + m_gpuQueryPool, + currentFrame * GPU_QUERIES_PER_FRAME + 1); + slot.recording = false; +} + +/// Flags this slot's queries for readback on its next fence wait. +void VulkanRenderer::MarkGpuFrameSubmitted() { + if (!m_gpuProfilerEnabled) return; + m_gpuSlots[currentFrame].submitted = true; +} + +/// Stamps the start of a pass. Timestamps use BOTTOM_OF_PIPE so each pair +/// measures the time the pass adds once earlier work has drained. +void VulkanRenderer::BeginGpuPass(GpuPass pass) { + if (!m_gpuProfilerEnabled) return; + GpuProfileSlot& slot = m_gpuSlots[currentFrame]; + if (!slot.recording || slot.pairOpen || slot.pairCount >= GPU_MAX_PAIRS) + return; + vkCmdWriteTimestamp( + command, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, m_gpuQueryPool, + currentFrame * GPU_QUERIES_PER_FRAME + 2 + 2 * slot.pairCount); + slot.pairPass[slot.pairCount] = pass; + slot.pairOpen = true; +} + +/// Stamps the end of the pass opened by the matching BeginGpuPass. +void VulkanRenderer::EndGpuPass(GpuPass pass) { + if (!m_gpuProfilerEnabled) return; + GpuProfileSlot& slot = m_gpuSlots[currentFrame]; + if (!slot.recording || !slot.pairOpen || + slot.pairPass[slot.pairCount] != pass) + return; + vkCmdWriteTimestamp( + command, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, m_gpuQueryPool, + currentFrame * GPU_QUERIES_PER_FRAME + 3 + 2 * slot.pairCount); + slot.pairOpen = false; + ++slot.pairCount; +} + +/// Logs the averaged per-pass table for the current window. +void VulkanRenderer::LogGpuProfile(float seconds) const { + SLEAK_INFO("GPU profile: {} frames in {:.1f} s, avg / max ms", + m_gpuWindowFrames, seconds); + for (uint32_t row = 0; row < GPU_ROW_COUNT; ++row) { + if (m_gpuRuns[row] == 0) continue; + const double avg = m_gpuSumMs[row] / m_gpuRuns[row]; + const std::string runs = + m_gpuRuns[row] < m_gpuWindowFrames + ? std::format(" ({}/{} frames)", m_gpuRuns[row], + m_gpuWindowFrames) + : std::string(); + SLEAK_INFO(" {:<9}{:8.3f}{:8.3f}{}", kGpuRowNames[row], avg, + m_gpuMaxMs[row], runs); + } +} + +/// Adds per-pass GPU millisecond columns when --gpuprofile is on. Values lag +/// the CSV row by MAX_FRAMES_IN_FLIGHT frames. +void VulkanRenderer::RegisterBenchmarkMetrics(Benchmark& bench) { + if (!m_gpuProfilerEnabled) return; + for (uint32_t row = 0; row < GPU_ROW_COUNT; ++row) { + if (row == GPU_ROW_OTHER) continue; + bench.RegisterMetric(std::format("GPU_{}_ms", kGpuRowNames[row]), + [this, row] { return m_gpuLastMs[row]; }); + } +} + +} // namespace RenderEngine +} // namespace Sleak diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index 3aaf87ef..177a1db2 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -78,6 +78,8 @@ VulkanRenderer::~VulkanRenderer() { /// Runs the full Vulkan bring-up sequence: instance, device, swapchain, /// pipelines, and sync objects. bool VulkanRenderer::Initialize() { + m_captureMode = !CommandLine::GetValue("-capture").empty(); + if (!InitVulkan()) SLEAK_RETURN_ERR("Failed to initialize Vulkan Instance!"); @@ -91,6 +93,8 @@ bool VulkanRenderer::Initialize() { if (!CreateDevice()) SLEAK_RETURN_ERR("Failed to initialize devices!"); + CreatePipelineCache(); + if (!CreateSwapChain()) SLEAK_RETURN_ERR("Failed to create swap chain!"); @@ -124,12 +128,16 @@ bool VulkanRenderer::Initialize() { if (!CreateDefaultTexture()) SLEAK_WARN("Failed to create default white texture for Vulkan"); - if (!CreateGraphicsPipeline()) - SLEAK_RETURN_ERR("Failed to create graphics pipeline!"); + if (!CreateMainPipelineLayout()) + SLEAK_RETURN_ERR("Failed to create graphics pipeline layout!"); if (!CreateShadowLightUBOResources()) SLEAK_WARN("Failed to create light UBO resources — dynamic lighting disabled"); + if (!CreateSceneReadResources()) + SLEAK_WARN( + "Failed to create scene-read resources for transparent formats"); + if (!CreateShadowResources()) SLEAK_WARN("Failed to create shadow mapping resources — shadows disabled"); @@ -142,6 +150,10 @@ bool VulkanRenderer::Initialize() { SLEAK_WARN("Failed to create GBuffer resources — deferred rendering disabled"); } + // Forward path + if (pipeline == VK_NULL_HANDLE && !CreateGraphicsPipeline()) + SLEAK_RETURN_ERR("Failed to create graphics pipeline!"); + // Eagerly create bone UBO resources so set 1 is always bound at pass start. // Must happen after CreateDescriptorSetLayout() (boneDescriptorSetLayout is ready) // and after GBuffer init (m_deferredEnabled is known). @@ -151,6 +163,8 @@ bool VulkanRenderer::Initialize() { if (!CreateSyncObjects()) SLEAK_RETURN_ERR("Failed to synchronization objects of renderer!"); + if (CommandLine::HasFlag("--gpuprofile")) CreateGpuProfiler(); + SetPerformanceCounter(true); SLEAK_INFO("Vulkan renderer has been initialized successfully!"); @@ -158,15 +172,16 @@ bool VulkanRenderer::Initialize() { return true; } -/// Prepares the command buffer and begins the shadow/GBuffer/forward render -/// pass. Does not end the command buffer; the RenderCommandQueue records -/// draw commands via the RenderContext interface after this returns. +/// Acquires a swapchain image and begins the shadow/GBuffer/forward passes +/// for the RenderCommandQueue to record into. A failed acquire returns early +/// with only a UI frame open, leaving deletions unaged and uploads batched. void VulkanRenderer::BeginRender() { bFrameStarted = false; m_inGeometryPass = false; m_inForwardTransparentPass = false; m_forwardPassOpen = false; m_activeCustomFormat = 0; + m_sceneReadPrepared = false; if (!bRender) return; @@ -183,13 +198,17 @@ void VulkanRenderer::BeginRender() { ApplyMSAAChange(); if (m_shadowResChangeRequested) ApplyShadowResolutionChange(); + PrebuildCustomFormatPipelines(); VkResult result; if (device && !inFlightFences.empty()) { vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); + VulkanTexture::DestroyReleased(m_slotSerial[currentFrame]); } + CollectGpuProfile(); + UpdateSceneReadDescriptors(currentFrame); // Clean up staging buffers from the previous use of this frame slot. // The fence wait above guarantees the GPU finished both the transfer @@ -210,26 +229,39 @@ void VulkanRenderer::BeginRender() { flush = {}; } - VulkanBuffer::ProcessDeferredDeletions(MAX_FRAMES_IN_FLIGHT); - VulkanBuffer::AdvanceDeletionFrame(); - // Enable batched buffer uploads for this frame (async, zero CPU blocking). // This is disabled during init/scene transitions where buffers may be // created and destroyed before a flush. VulkanBuffer::SetBatchingEnabled(true); + // UI frame for skipped frames + auto beginSkippedUIFrame = [this]() { + bImFrameActive = false; + if (!bImInitialized) return; + ImGui_ImplVulkan_NewFrame(); + ImGui_ImplSDL3_NewFrame(); + ImGui::NewFrame(); + bImFrameActive = true; + }; + // Acquire the next image from the swapchain result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[m_semaphoreIndex], VK_NULL_HANDLE, &CurrentFrameIndex); if (result == VK_ERROR_OUT_OF_DATE_KHR) { RecreateSwapChain(); + beginSkippedUIFrame(); return; } else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { SLEAK_ERROR("Failed to acquire swapchain image!"); + beginSkippedUIFrame(); return; } + // Submitting frames age deletions + VulkanBuffer::ProcessDeferredDeletions(MAX_FRAMES_IN_FLIGHT); + VulkanBuffer::AdvanceDeletionFrame(); + // Wait if this swapchain image is still in use by a DIFFERENT frame slot if (CurrentFrameIndex < imagesInFlight.size() && imagesInFlight[CurrentFrameIndex] != VK_NULL_HANDLE && @@ -259,6 +291,8 @@ void VulkanRenderer::BeginRender() { bFrameStarted = true; m_pbrMaterialSlot[currentFrame] = 0; // reset PBR material ring for this frame + m_boneSlot[currentFrame] = 1; // slot 0 is the identity palette + BeginGpuFrame(); // Skip shadow pass if no cached draws — preserve previous frame's shadow map auto* shadowQueue = RenderCommandQueue::GetInstance(); @@ -277,6 +311,7 @@ void VulkanRenderer::BeginRender() { shadowPassInfo.clearValueCount = 1; shadowPassInfo.pClearValues = &shadowClear; + BeginGpuPass(GpuPass::Shadow); vkCmdBeginRenderPass(command, &shadowPassInfo, VK_SUBPASS_CONTENTS_INLINE); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_shadowPipeline); @@ -284,11 +319,7 @@ void VulkanRenderer::BeginRender() { // (skinning conditioned on boneWeights). The shader must have set 1 bound even for // non-skinned casters, otherwise vkCmdDrawIndexed fires VUID-vkCmdDrawIndexed-None-08600. // Bind the bone UBO once at pass start so all shadow draws (skinned or static) are legal. - if (m_boneUBOCreated) { - vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, - pipelineLay, 1, 1, - &boneDescriptorSets[currentFrame], 0, nullptr); - } + BindIdentityBonePalette(pipelineLay); VkViewport shadowViewport{}; shadowViewport.x = 0.0f; @@ -306,6 +337,9 @@ void VulkanRenderer::BeginRender() { m_shadowPassActive = true; m_shadowPCCacheValid = false; + m_shadowPCPushedLayout = VK_NULL_HANDLE; + memcpy(m_shadowMapLightVP, m_lightVP, sizeof(m_shadowMapLightVP)); + m_shadowMapRendered = true; auto* queue = RenderCommandQueue::GetInstance(); if (queue) { queue->ExecuteShadowPass(this); @@ -313,6 +347,7 @@ void VulkanRenderer::BeginRender() { m_shadowPassActive = false; vkCmdEndRenderPass(command); + EndGpuPass(GpuPass::Shadow); } // ---- Deferred path: begin GBuffer render pass ---- @@ -333,6 +368,7 @@ void VulkanRenderer::BeginRender() { gbufferPassInfo.clearValueCount = GBUFFER_COUNT + 1; gbufferPassInfo.pClearValues = gbufferClears; + BeginGpuPass(GpuPass::GBuffer); vkCmdBeginRenderPass(command, &gbufferPassInfo, VK_SUBPASS_CONTENTS_INLINE); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_gbufferPipeline); @@ -353,15 +389,11 @@ void VulkanRenderer::BeginRender() { // Bind descriptor sets for GBuffer geometry pass. // Set 0 (PBR material) is bound per-material by BindPBRMaterial(). - // Set 1 (bone matrices) is frame-constant: bound here so m_skinnedGbufferPipeline - // can always find a valid set 1, even for frames where no skinned draw fires. + // Set 1 (bone matrices) starts at the identity palette so + // m_skinnedGbufferPipeline always finds a valid set 1. // Sets 2-3 are frame-constant: light/shadow UBO and shadow samplers. if (m_gbufferGeomLayout != VK_NULL_HANDLE && m_lightUBOCreated) { - if (m_boneUBOCreated) { - vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, - m_gbufferGeomLayout, 1, 1, - &boneDescriptorSets[currentFrame], 0, nullptr); - } + BindIdentityBonePalette(m_gbufferGeomLayout); vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_gbufferGeomLayout, 2, 1, &m_lightUBODescriptorSets[currentFrame], 0, nullptr); @@ -407,6 +439,7 @@ void VulkanRenderer::BeginRender() { passInfo.clearValueCount = clearValueCount; passInfo.pClearValues = clearValues; + BeginGpuPass(GpuPass::Forward); vkCmdBeginRenderPass(command, &passInfo, VK_SUBPASS_CONTENTS_INLINE); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); @@ -460,9 +493,14 @@ void VulkanRenderer::BeginRender() { } /// Ends the active render pass, submits the command buffer, and presents. +/// A skipped frame only closes its UI frame. void VulkanRenderer::EndRender() { - if (!bRender || !bFrameStarted) + if (!bRender || !bFrameStarted) { + // Skipped frame, no UI draw + if (bImFrameActive && bImInitialized) ImGui::EndFrame(); + bImFrameActive = false; return; + } const bool deferredPath = m_gbufferResourcesCreated && m_deferredEnabled && m_bloomResourcesCreated; @@ -471,6 +509,7 @@ void VulkanRenderer::EndRender() { // end it now so we don't have a dangling render pass. if (m_gbufferResourcesCreated && m_deferredEnabled && m_inGeometryPass) { vkCmdEndRenderPass(command); + EndGpuPass(GpuPass::GBuffer); m_inGeometryPass = false; } @@ -486,6 +525,7 @@ void VulkanRenderer::EndRender() { rpBegin.renderArea.offset = {0, 0}; rpBegin.renderArea.extent = scExtent; rpBegin.clearValueCount = 0; + BeginGpuPass(GpuPass::Forward); vkCmdBeginRenderPass(command, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); m_forwardPassOpen = true; } @@ -493,15 +533,16 @@ void VulkanRenderer::EndRender() { // Close the forward pass (HDR scene → SHADER_READ_ONLY_OPTIMAL via finalLayout). if (m_forwardPassOpen) { vkCmdEndRenderPass(command); + EndGpuPass(GpuPass::Forward); m_forwardPassOpen = false; } if (deferredPath) { - // TAA: accumulate current HDR frame with history, write resolved result - // back into hdrScene. Also handles the depth barrier (ATTACHMENT → READ_ONLY) - // so SSR can skip its own barrier when TAA is enabled. + // TAA: accumulate current HDR frame with history into a ping-pong + // target that SSR, bloom and the composite read. Also handles the depth + // barrier (ATTACHMENT → READ_ONLY) so SSR can skip its own barrier. RenderTAAPass(); - // Screen-space reflections — reads TAA-resolved hdrScene + GBuffer. + // Screen-space reflections — reads the TAA-resolved scene + GBuffer. RenderSSRPass(); // Bloom pyramid generates the bloom result from the HDR scene. RenderBloomPass(); @@ -510,13 +551,23 @@ void VulkanRenderer::EndRender() { RenderBloomCompositePass(); } else { // Forward (non-deferred) path — ImGui inside main render pass. + const bool drawUI = bImFrameActive && m_capturePath.empty(); + if (drawUI) { + EndGpuPass(GpuPass::Forward); + BeginGpuPass(GpuPass::UI); + } if (bImFrameActive) { ImGui::Render(); - ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), command); + if (drawUI) + ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), command); } vkCmdEndRenderPass(command); + EndGpuPass(drawUI ? GpuPass::UI : GpuPass::Forward); } + if (!m_capturePath.empty()) RecordFrameCaptureCopy(); + EndGpuFrame(); + if (vkEndCommandBuffer(command) != VK_SUCCESS) { SLEAK_ERROR("Failed to end command buffer!"); return; @@ -558,6 +609,10 @@ void VulkanRenderer::EndRender() { if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) { SLEAK_ERROR("Failed to submit draw command buffer!"); + } else { + MarkGpuFrameSubmitted(); + m_slotSerial[currentFrame] = ++m_submitSerial; + VulkanTexture::SetReleaseSerial(m_submitSerial + 1); } // Present @@ -573,13 +628,18 @@ void VulkanRenderer::EndRender() { VkResult presentResult = vkQueuePresentKHR(presentQueue, &presentInfo); + // Capture pins the extent if (presentResult == VK_ERROR_OUT_OF_DATE_KHR || - presentResult == VK_SUBOPTIMAL_KHR) { + (presentResult == VK_SUBOPTIMAL_KHR && !m_captureMode && + SurfaceExtentChanged())) { RecreateSwapChain(); - } else if (presentResult != VK_SUCCESS) { + } else if (presentResult != VK_SUCCESS && + presentResult != VK_SUBOPTIMAL_KHR) { SLEAK_ERROR("Failed to present render!"); } + if (m_captureRecorded) FinishFrameCapture(); + // Reset per-frame deferred state flags m_forwardPassOpen = false; m_inForwardTransparentPass = false; @@ -721,8 +781,9 @@ void VulkanRenderer::BindIndexBuffer(RefPtr buffer, VK_INDEX_TYPE_UINT32); } -/// Pushes constant-buffer data via push constants, applying TAA jitter or -/// the shadow push-constant cache as needed. +/// Pushes constant-buffer data via push constants, applying TAA jitter or the +/// shadow push-constant cache. A shadow push that would repeat the bytes +/// already pushed in this pass is skipped. void VulkanRenderer::BindConstantBuffer(RefPtr buffer, uint32_t slot) { if (!bFrameStarted) return; @@ -734,7 +795,7 @@ void VulkanRenderer::BindConstantBuffer(RefPtr buffer, if (!data) return; uint32_t size = static_cast(vkBuf->GetSize()); - if (size > 128) size = 128; // Vulkan guarantees at least 128 bytes + if (size > PUSH_CONSTANT_BYTES) size = PUSH_CONSTANT_BYTES; // Choose the pipeline layout that owns the currently bound pipeline. // GBuffer geometry pass uses m_gbufferGeomLayout; all other passes use pipelineLay. @@ -758,10 +819,11 @@ void VulkanRenderer::BindConstantBuffer(RefPtr buffer, } vkCmdPushConstants(command, activeLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, size, jdata); + m_shadowPCPushedLayout = VK_NULL_HANDLE; return; } - if (m_shadowPassActive && slot == 0 && size >= 128) { + if (m_shadowPassActive && slot == 0 && size >= PUSH_CONSTANT_BYTES) { // Shadow mode: push [LightVP*World (64)][World (64)]. // Buffer layout: [WVP (64 bytes)][World (64 bytes)]. // LightVP is frame-constant, so memoize on World and reuse the result @@ -784,13 +846,20 @@ void VulkanRenderer::BindConstantBuffer(RefPtr buffer, memcpy(&m_shadowPCCache[16], srcWorld, 64); memcpy(m_shadowWorldCache, srcWorld, 64); m_shadowPCCacheValid = true; + m_shadowPCPushedLayout = VK_NULL_HANDLE; } - vkCmdPushConstants(command, activeLayout, - VK_SHADER_STAGE_VERTEX_BIT, 0, 128, m_shadowPCCache); + // Pushes survive pipeline binds + if (m_shadowPCPushedLayout != activeLayout) { + vkCmdPushConstants(command, activeLayout, + VK_SHADER_STAGE_VERTEX_BIT, 0, + PUSH_CONSTANT_BYTES, m_shadowPCCache); + m_shadowPCPushedLayout = activeLayout; + } } else { vkCmdPushConstants(command, activeLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, size, data); + m_shadowPCPushedLayout = VK_NULL_HANDLE; } } @@ -860,6 +929,7 @@ ::Sleak::Texture* VulkanRenderer::CreateCubemapTexture( // Write cubemap to skybox descriptor sets m_skyboxCubemapView = texture->GetImageView(); m_skyboxCubemapSampler = texture->GetSampler(); + ++m_skyboxViewSerial; for (size_t i = 0; i < skyboxDescriptorSets.size(); i++) { VkDescriptorImageInfo imageInfo{}; imageInfo.imageLayout = @@ -908,6 +978,7 @@ ::Sleak::Texture* VulkanRenderer::CreateCubemapTextureFromPanorama( // Write cubemap to skybox descriptor sets m_skyboxCubemapView = texture->GetImageView(); m_skyboxCubemapSampler = texture->GetSampler(); + ++m_skyboxViewSerial; for (size_t i = 0; i < skyboxDescriptorSets.size(); i++) { VkDescriptorImageInfo imageInfo{}; imageInfo.imageLayout = @@ -1020,8 +1091,10 @@ void VulkanRenderer::WaitIdle() { if (device) vkDeviceWaitIdle(device); } -/// Kicks off the current frame's async buffer upload batch. +/// Submits the open upload batch for this frame. A frame that failed to +/// start leaves it open, so it rides along with the next submitted frame. void VulkanRenderer::FlushPendingTransfers() { + if (!bFrameStarted) return; m_asyncFlush[currentFrame] = VulkanBuffer::FlushPendingCopiesAsync( m_transferSemaphores[currentFrame]); } @@ -1045,6 +1118,7 @@ void VulkanRenderer::Cleanup() { // Wait for the device to finish all work if (device) { vkDeviceWaitIdle(device); + VulkanBuffer::DiscardPendingCopies(); } RenderCommandQueue::Shutdown(); @@ -1064,6 +1138,15 @@ void VulkanRenderer::Cleanup() { imguiDescriptorPool = VK_NULL_HANDLE; } + DestroyPipelineCache(); + + // Drain released textures + if (device) { + delete m_defaultTexture; + m_defaultTexture = nullptr; + VulkanTexture::DestroyAllReleased(); + } + // Destroy descriptor pool (frees descriptor sets too) if (descriptorPool) { vkDestroyDescriptorPool(device, descriptorPool, nullptr); @@ -1111,6 +1194,7 @@ void VulkanRenderer::Cleanup() { // Destroy shadow mapping resources CleanupShadowResources(); + CleanupSceneReadResources(); // Backstop: free any shader modules whose resource-guarded cleanup was // skipped (guard false while shader non-null). Cleanups null after delete, @@ -1233,12 +1317,6 @@ void VulkanRenderer::Cleanup() { renderPass = VK_NULL_HANDLE; } - // Destroy default texture - if (m_defaultTexture) { - delete m_defaultTexture; - m_defaultTexture = nullptr; - } - // Destroy command pool (this will also free command buffers) if (commands) { vkDestroyCommandPool(device, commands, nullptr); @@ -1257,6 +1335,9 @@ void VulkanRenderer::Cleanup() { debugMessenger = VK_NULL_HANDLE; } + CleanupGpuProfiler(); + CleanupFrameCapture(); + // Drain any buffers freed during teardown, then destroy the VMA allocator // (it must outlive every vmaDestroyBuffer, and both precede vkDestroyDevice). VulkanBuffer::FlushAllDeferredDeletions(); @@ -1275,9 +1356,9 @@ void VulkanRenderer::Cleanup() { } } -/// Recreates the swapchain for the new window dimensions. +/// Recreates the swapchain when the surface extent no longer matches it. void VulkanRenderer::Resize(uint32_t width, uint32_t height) { - if (device) { + if (device && (swapChain == VK_NULL_HANDLE || SurfaceExtentChanged())) { RecreateSwapChain(); } } @@ -1316,8 +1397,10 @@ bool VulkanRenderer::RecreateSwapChain() { return false; } - // Resize imagesInFlight in case swapchain image count changed - imagesInFlight.resize(swapChainImages.size(), VK_NULL_HANDLE); + if (!CreateImageSyncObjects()) { + SLEAK_ERROR("Failed to recreate swapchain image semaphores!"); + return false; + } // Recreate GBuffer resources if they were previously created if (hadGBuffer && m_deferredEnabled) { @@ -1388,6 +1471,10 @@ void VulkanRenderer::ApplyMSAAChange() { ImGui::DestroyContext(); bImInitialized = false; } + if (imguiDescriptorPool) { + vkDestroyDescriptorPool(device, imguiDescriptorPool, nullptr); + imguiDescriptorPool = VK_NULL_HANDLE; + } // Cleanup swapchain-related resources CleanupMSAAColorResources(); @@ -1395,6 +1482,7 @@ void VulkanRenderer::ApplyMSAAChange() { // Recreate everything CreateSwapChain(); + CreateImageSyncObjects(); CreateImageViews(); CreateDepthResources(); CreateMSAAColorResources(); @@ -1465,17 +1553,37 @@ bool VulkanRenderer::CreateCommandPool() { return true; } +/// Grows the per-image semaphores to the swapchain image count and clears +/// the image-to-fence table. Existing semaphores are kept: a present may +/// still reference them, and indices beyond the image count are harmless. +bool VulkanRenderer::CreateImageSyncObjects() { + const size_t imageCount = swapChainImages.size(); + imagesInFlight.assign(imageCount, VK_NULL_HANDLE); + + VkSemaphoreCreateInfo semaphoreInfo{}; + semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + + while (imageAvailableSemaphores.size() < imageCount) { + VkSemaphore available = VK_NULL_HANDLE; + VkSemaphore finished = VK_NULL_HANDLE; + if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &available) != + VK_SUCCESS || + vkCreateSemaphore(device, &semaphoreInfo, nullptr, &finished) != + VK_SUCCESS) { + if (available) vkDestroySemaphore(device, available, nullptr); + SLEAK_RETURN_ERR("Failed to create synchronization objects!"); + } + imageAvailableSemaphores.push_back(available); + renderFinishedSemaphores.push_back(finished); + } + return true; +} + /// Creates the per-swapchain-image semaphores and per-frame fences and /// transfer semaphores. bool VulkanRenderer::CreateSyncObjects() { - uint32_t imageCount = static_cast(swapChainImages.size()); - - // Semaphores sized to swapchain image count to prevent reuse - // while the presentation engine still holds a reference. - imageAvailableSemaphores.resize(imageCount); - renderFinishedSemaphores.resize(imageCount); + if (!CreateImageSyncObjects()) return false; inFlightFences.resize(MAX_FRAMES_IN_FLIGHT); - imagesInFlight.resize(imageCount, VK_NULL_HANDLE); VkSemaphoreCreateInfo semaphoreInfo{}; semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; @@ -1484,16 +1592,6 @@ bool VulkanRenderer::CreateSyncObjects() { fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; - // Create per-swapchain-image semaphores - for (uint32_t i = 0; i < imageCount; i++) { - if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, - &imageAvailableSemaphores[i]) != VK_SUCCESS || - vkCreateSemaphore(device, &semaphoreInfo, nullptr, - &renderFinishedSemaphores[i]) != VK_SUCCESS) { - SLEAK_RETURN_ERR("Failed to create synchronization objects!"); - } - } - // Create per-frame-in-flight fences and transfer semaphores for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, @@ -1530,6 +1628,7 @@ void VulkanRenderer::ApplyShadowResolutionChange() { m_shadowImageMemory = VK_NULL_HANDLE; m_shadowMapResolution = m_pendingShadowMapResolution; + m_shadowMapRendered = false; VkImageCreateInfo imageInfo{}; imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; @@ -1619,24 +1718,19 @@ void VulkanRenderer::ApplyShadowResolutionChange() { cmdBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; vkBeginCommandBuffer(cmdBuf, &cmdBeginInfo); - VkImageMemoryBarrier barrier{}; - barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - barrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; - barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.image = m_shadowImage; - barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; - barrier.subresourceRange.baseMipLevel = 0; - barrier.subresourceRange.levelCount = 1; - barrier.subresourceRange.baseArrayLayer = 0; - barrier.subresourceRange.layerCount = 1; - barrier.srcAccessMask = 0; - barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - - vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, - nullptr, 0, nullptr, 1, &barrier); + // Clear to far depth + VkClearValue clear{}; + clear.depthStencil = {1.0f, 0}; + VkRenderPassBeginInfo clearPass{}; + clearPass.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + clearPass.renderPass = m_shadowRenderPass; + clearPass.framebuffer = m_shadowFramebuffer; + clearPass.renderArea.extent = {m_shadowMapResolution, + m_shadowMapResolution}; + clearPass.clearValueCount = 1; + clearPass.pClearValues = &clear; + vkCmdBeginRenderPass(cmdBuf, &clearPass, VK_SUBPASS_CONTENTS_INLINE); + vkCmdEndRenderPass(cmdBuf); vkEndCommandBuffer(cmdBuf); @@ -1764,6 +1858,10 @@ bool VulkanRenderer::CreateGBufferResources() { // GBuffer sampler set 0 binding 7 samples the SSAO blur result. if (!CreateSSAOResources()) { SLEAK_ERROR("GBuffer: SSAO resources failed!"); return false; } if (!CreateBloomResources()) { SLEAK_ERROR("GBuffer: bloom/HDR resources failed!"); return false; } + if (!CreateSceneCopyImages()) + SLEAK_WARN( + "GBuffer: scene snapshot images failed, transparent formats cannot " + "read the scene"); // SSR needs the HDR scene view (created by CreateBloomResources), so it // must come after that. The bloom composite pass later samples SSR. if (!CreateSSRResources()) { SLEAK_ERROR("GBuffer: SSR resources failed!"); return false; } @@ -1799,23 +1897,16 @@ bool VulkanRenderer::CreateGBufferResources() { if (pipeline) { vkDestroyPipeline(device, pipeline, nullptr); pipeline = VK_NULL_HANDLE; } if (skyboxPipeline) { vkDestroyPipeline(device, skyboxPipeline, nullptr); skyboxPipeline = VK_NULL_HANDLE; } if (debugLinePipeline) { vkDestroyPipeline(device, debugLinePipeline, nullptr); debugLinePipeline = VK_NULL_HANDLE; } - if (skinnedPipeline) { vkDestroyPipeline(device, skinnedPipeline, nullptr); skinnedPipeline = VK_NULL_HANDLE; } - if (m_skinnedGbufferPipeline) { vkDestroyPipeline(device, m_skinnedGbufferPipeline, nullptr); m_skinnedGbufferPipeline = VK_NULL_HANDLE; } - DestroyCustomFormatPipelines(); - // Destroy old skybox descriptor pool (CreateSkyboxPipeline allocates new ones) - if (skyboxDescriptorPool) { - vkDestroyDescriptorPool(device, skyboxDescriptorPool, nullptr); - skyboxDescriptorPool = VK_NULL_HANDLE; + if (skinnedPipeline) { + vkDestroyPipeline(device, skinnedPipeline, nullptr); + skinnedPipeline = VK_NULL_HANDLE; } - skyboxDescriptorSets.clear(); - delete skyboxShader; - skyboxShader = nullptr; + DestroyCustomFormatPipelines(); CreateGraphicsPipeline(); CreateSkyboxPipeline(); CreateDebugLinePipeline(); CreateSkinnedPipeline(); - CreateSkinnedGbufferPipeline(); m_gbufferResourcesCreated = true; @@ -1840,6 +1931,8 @@ bool VulkanRenderer::CreateGBufferResources() { } m_skyboxDescriptorsWritten = true; } + + PrebuildCustomFormatPipelines(); } SLEAK_INFO("VulkanRenderer: Deferred GBuffer resources created ({}x{})", diff --git a/src/Graphics/Vulkan/VulkanSSAO.cpp b/src/Graphics/Vulkan/VulkanSSAO.cpp index ac2d3d08..f1e1aeeb 100644 --- a/src/Graphics/Vulkan/VulkanSSAO.cpp +++ b/src/Graphics/Vulkan/VulkanSSAO.cpp @@ -519,7 +519,8 @@ bool VulkanRenderer::CreateSSAOPipelines() { gpi.renderPass = m_ssaoRenderPass; gpi.subpass = 0; - if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpi, nullptr, &m_ssaoPipeline) != VK_SUCCESS) { + if (vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &gpi, nullptr, + &m_ssaoPipeline) != VK_SUCCESS) { SLEAK_ERROR("SSAO: failed to create ssao pipeline"); return false; } @@ -547,7 +548,8 @@ bool VulkanRenderer::CreateSSAOPipelines() { }; gpi.pStages = blurStages; gpi.layout = m_ssaoBlurPipelineLayout; - if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpi, nullptr, &m_ssaoBlurPipeline) != VK_SUCCESS) { + if (vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &gpi, nullptr, + &m_ssaoBlurPipeline) != VK_SUCCESS) { SLEAK_ERROR("SSAO: failed to create ssao blur pipeline"); return false; } @@ -860,6 +862,7 @@ void VulkanRenderer::RenderSSAOPasses() { rp.renderArea.extent = m_ssaoExtent; rp.clearValueCount = 0; + BeginGpuPass(GpuPass::SSAO); vkCmdBeginRenderPass(command, &rp, VK_SUBPASS_CONTENTS_INLINE); FillFullscreenViewportScissor(command, m_ssaoExtent); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_ssaoPipeline); @@ -879,6 +882,7 @@ void VulkanRenderer::RenderSSAOPasses() { &m_ssaoBlurSets[currentFrame], 0, nullptr); vkCmdDraw(command, 3, 1, 0, 0); vkCmdEndRenderPass(command); + EndGpuPass(GpuPass::SSAO); } } // namespace RenderEngine diff --git a/src/Graphics/Vulkan/VulkanSSR.cpp b/src/Graphics/Vulkan/VulkanSSR.cpp index 2bddc6e1..6728e67d 100644 --- a/src/Graphics/Vulkan/VulkanSSR.cpp +++ b/src/Graphics/Vulkan/VulkanSSR.cpp @@ -350,7 +350,8 @@ bool VulkanRenderer::CreateSSRResources() { gpi.renderPass = m_ssrRenderPass; gpi.subpass = 0; - if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpi, nullptr, &m_ssrPipeline) != VK_SUCCESS) { + if (vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &gpi, nullptr, + &m_ssrPipeline) != VK_SUCCESS) { SLEAK_ERROR("SSR: failed to create pipeline"); return false; } @@ -426,7 +427,7 @@ void VulkanRenderer::UpdateSSRDescriptors() { infos[3].imageView = m_gbufferViews[0]; infos[3].sampler = m_gbufferSampler ? m_gbufferSampler : m_ssrSampler; - // sceneHDR — forward pass finalLayout is SHADER_READ_ONLY_OPTIMAL. + // sceneHDR — RenderSSRPass retargets it to the TAA resolve. infos[4].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; infos[4].imageView = m_hdrSceneView; infos[4].sampler = m_ssrSampler; @@ -546,6 +547,20 @@ void VulkanRenderer::RenderSSRPass() { UpdateSSRUBO(); + // Scene input for this frame + VkDescriptorImageInfo sceneInfo{}; + sceneInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + sceneInfo.imageView = PostSceneView(); + sceneInfo.sampler = m_ssrSampler; + VkWriteDescriptorSet sceneWrite{}; + sceneWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + sceneWrite.dstSet = m_ssrInputSets[currentFrame]; + sceneWrite.dstBinding = 4; + sceneWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + sceneWrite.descriptorCount = 1; + sceneWrite.pImageInfo = &sceneInfo; + vkUpdateDescriptorSets(device, 1, &sceneWrite, 0, nullptr); + VkRenderPassBeginInfo rp{}; rp.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; rp.renderPass = m_ssrRenderPass; @@ -554,6 +569,7 @@ void VulkanRenderer::RenderSSRPass() { rp.renderArea.extent = scExtent; rp.clearValueCount = 0; // DONT_CARE load — we write every pixel + BeginGpuPass(GpuPass::SSR); vkCmdBeginRenderPass(command, &rp, VK_SUBPASS_CONTENTS_INLINE); FillFullscreenViewportScissor(command, scExtent); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_ssrPipeline); @@ -563,6 +579,7 @@ void VulkanRenderer::RenderSSRPass() { m_ssrPipelineLayout, 0, 2, sets, 0, nullptr); vkCmdDraw(command, 3, 1, 0, 0); vkCmdEndRenderPass(command); + EndGpuPass(GpuPass::SSR); } } // namespace RenderEngine diff --git a/src/Graphics/Vulkan/VulkanSceneRead.cpp b/src/Graphics/Vulkan/VulkanSceneRead.cpp new file mode 100644 index 00000000..a49e37d2 --- /dev/null +++ b/src/Graphics/Vulkan/VulkanSceneRead.cpp @@ -0,0 +1,522 @@ +#include +#include +#include +#include +#include + +#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" +#include "Core/Logger.hpp" +#include "Graphics/Common/ConstantBuffer.hpp" + +namespace Sleak { +namespace RenderEngine { + +// ================================================================== +// Scene read for transparent formats (VertexLayoutDesc::transparentReadsScene) +// ================================================================== + +namespace { + +constexpr uint32_t SINGLE_LAYER = 1; +constexpr uint32_t CUBE_FACE_COUNT = 6; +constexpr VkExtent2D STAND_IN_EXTENT = {1, 1}; +constexpr float FAR_DEPTH = 1.0f; +// UV offsets span half the NDC range +constexpr float UV_TO_NDC = 2.0f; + +/// True when the depth format carries a stencil aspect. +bool HasStencil(VkFormat format) { + return format == VK_FORMAT_D32_SFLOAT_S8_UINT || + format == VK_FORMAT_D24_UNORM_S8_UINT || + format == VK_FORMAT_D16_UNORM_S8_UINT; +} + +/// Aspect mask covering every aspect of a depth format. +VkImageAspectFlags DepthAspects(VkFormat format) { + return VK_IMAGE_ASPECT_DEPTH_BIT | + (HasStencil(format) ? VK_IMAGE_ASPECT_STENCIL_BIT : 0); +} + +/// Picks a memory type matching the filter and property flags. +bool FindMemory(VkPhysicalDevice gpu, uint32_t typeBits, + VkMemoryPropertyFlags flags, uint32_t& index) { + VkPhysicalDeviceMemoryProperties props; + vkGetPhysicalDeviceMemoryProperties(gpu, &props); + for (uint32_t i = 0; i < props.memoryTypeCount; ++i) { + if ((typeBits & (1u << i)) && + (props.memoryTypes[i].propertyFlags & flags) == flags) { + index = i; + return true; + } + } + return false; +} + +/// Creates a device-local 2D image, or a cube when given six layers, and +/// its view. +bool CreateSampledImage(VkDevice device, VkPhysicalDevice gpu, VkFormat format, + VkExtent2D extent, uint32_t layers, + VkImageUsageFlags usage, VkImageAspectFlags aspect, + VkImage& image, VkDeviceMemory& memory, + VkImageView& view) { + const bool cube = layers == CUBE_FACE_COUNT; + + VkImageCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + info.flags = cube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0; + info.imageType = VK_IMAGE_TYPE_2D; + info.extent = {extent.width, extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = layers; + info.format = format; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + info.usage = usage; + info.samples = VK_SAMPLE_COUNT_1_BIT; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + if (vkCreateImage(device, &info, nullptr, &image) != VK_SUCCESS) + return false; + + VkMemoryRequirements req; + vkGetImageMemoryRequirements(device, image, &req); + VkMemoryAllocateInfo alloc{}; + alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + alloc.allocationSize = req.size; + if (!FindMemory(gpu, req.memoryTypeBits, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + alloc.memoryTypeIndex) || + vkAllocateMemory(device, &alloc, nullptr, &memory) != VK_SUCCESS) + return false; + vkBindImageMemory(device, image, memory, 0); + + VkImageViewCreateInfo vinfo{}; + vinfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + vinfo.image = image; + vinfo.viewType = cube ? VK_IMAGE_VIEW_TYPE_CUBE : VK_IMAGE_VIEW_TYPE_2D; + vinfo.format = format; + vinfo.subresourceRange = {aspect, 0, 1, 0, layers}; + return vkCreateImageView(device, &vinfo, nullptr, &view) == VK_SUCCESS; +} + +/// Destroys an image, its view, and its memory, nulling the handles. +void DestroyImage(VkDevice device, VkImage& image, VkDeviceMemory& memory, + VkImageView& view) { + if (view) vkDestroyImageView(device, view, nullptr); + if (image) vkDestroyImage(device, image, nullptr); + if (memory) vkFreeMemory(device, memory, nullptr); + view = VK_NULL_HANDLE; + image = VK_NULL_HANDLE; + memory = VK_NULL_HANDLE; +} + +/// Image barrier over every layer of a single-mip image. +VkImageMemoryBarrier MakeBarrier(VkImage image, VkImageAspectFlags aspect, + uint32_t layers, VkImageLayout from, + VkImageLayout to, VkAccessFlags src, + VkAccessFlags dst) { + VkImageMemoryBarrier b{}; + b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + b.oldLayout = from; + b.newLayout = to; + b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.srcAccessMask = src; + b.dstAccessMask = dst; + b.image = image; + b.subresourceRange = {aspect, 0, 1, 0, layers}; + return b; +} + +/// One image to clear and leave in SHADER_READ_ONLY_OPTIMAL. +struct ClearTarget { + VkImage image; + VkImageAspectFlags aspect; + uint32_t layers; +}; + +/// Clears images to zero (depth to 1) and leaves them shader readable. +void ClearToShaderRead(VkDevice device, VkCommandPool pool, VkQueue queue, + const ClearTarget* targets, uint32_t count) { + VkCommandBufferAllocateInfo ca{}; + ca.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + ca.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + ca.commandPool = pool; + ca.commandBufferCount = 1; + VkCommandBuffer cmd = VK_NULL_HANDLE; + if (vkAllocateCommandBuffers(device, &ca, &cmd) != VK_SUCCESS) return; + + VkCommandBufferBeginInfo bi{}; + bi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cmd, &bi); + + for (uint32_t i = 0; i < count; ++i) { + const ClearTarget& t = targets[i]; + const VkImageMemoryBarrier toDst = + MakeBarrier(t.image, t.aspect, t.layers, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, + VK_ACCESS_TRANSFER_WRITE_BIT); + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, + nullptr, 1, &toDst); + + const VkImageSubresourceRange range = {t.aspect, 0, 1, 0, t.layers}; + if (t.aspect & VK_IMAGE_ASPECT_DEPTH_BIT) { + // windows.h defines `far` as an empty macro + const VkClearDepthStencilValue farDepth = {FAR_DEPTH, 0}; + vkCmdClearDepthStencilImage(cmd, t.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + &farDepth, 1, &range); + } else { + const VkClearColorValue zero = {}; + vkCmdClearColorImage(cmd, t.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &zero, 1, + &range); + } + + const VkImageMemoryBarrier toRead = MakeBarrier( + t.image, t.aspect, t.layers, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &toRead); + } + vkEndCommandBuffer(cmd); + + VkSubmitInfo submit{}; + submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submit.commandBufferCount = 1; + submit.pCommandBuffers = &cmd; + vkQueueSubmit(queue, 1, &submit, VK_NULL_HANDLE); + vkQueueWaitIdle(queue); + vkFreeCommandBuffers(device, pool, 1, &cmd); +} + +} // namespace + +/// Creates the scene-read samplers, stand-in images, and per-frame UBOs and +/// writes them into both frames' set 3 so the bindings are always valid. +bool VulkanRenderer::CreateSceneReadResources() { + if (m_sceneReadCreated) return true; + if (!m_lightUBOCreated) return false; + + // Samplers + VkSamplerCreateInfo sinfo{}; + sinfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + sinfo.magFilter = VK_FILTER_LINEAR; + sinfo.minFilter = VK_FILTER_LINEAR; + sinfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + sinfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sinfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sinfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sinfo.maxLod = 0.0f; + if (vkCreateSampler(device, &sinfo, nullptr, &m_sceneLinearSampler) != + VK_SUCCESS) + return false; + sinfo.magFilter = VK_FILTER_NEAREST; + sinfo.minFilter = VK_FILTER_NEAREST; + if (vkCreateSampler(device, &sinfo, nullptr, &m_scenePointSampler) != + VK_SUCCESS) + return false; + + // Stand-ins + const VkImageUsageFlags standInUsage = + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (!CreateSampledImage(device, physicalDevice, VK_FORMAT_R8G8B8A8_UNORM, + STAND_IN_EXTENT, SINGLE_LAYER, standInUsage, + VK_IMAGE_ASPECT_COLOR_BIT, m_sceneFallbackImage, + m_sceneFallbackMemory, m_sceneFallbackView) || + !CreateSampledImage(device, physicalDevice, VK_FORMAT_R8G8B8A8_UNORM, + STAND_IN_EXTENT, CUBE_FACE_COUNT, standInUsage, + VK_IMAGE_ASPECT_COLOR_BIT, m_sceneFallbackCubeImage, + m_sceneFallbackCubeMemory, + m_sceneFallbackCubeView)) { + SLEAK_ERROR("VulkanRenderer: scene-read stand-in images failed"); + return false; + } + const ClearTarget standIns[] = { + {m_sceneFallbackImage, VK_IMAGE_ASPECT_COLOR_BIT, SINGLE_LAYER}, + {m_sceneFallbackCubeImage, VK_IMAGE_ASPECT_COLOR_BIT, CUBE_FACE_COUNT}}; + ClearToShaderRead(device, commands, graphicsQueue, standIns, + static_cast(std::size(standIns))); + + // Per-frame UBOs + for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { + VkBufferCreateInfo binfo{}; + binfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + binfo.size = sizeof(SceneReadUBO); + binfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; + binfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + if (vkCreateBuffer(device, &binfo, nullptr, + &m_sceneReadUBOBuffers[i]) != VK_SUCCESS) + return false; + + VkMemoryRequirements req; + vkGetBufferMemoryRequirements(device, m_sceneReadUBOBuffers[i], &req); + VkMemoryAllocateInfo alloc{}; + alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + alloc.allocationSize = req.size; + if (!FindMemory(physicalDevice, req.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + alloc.memoryTypeIndex) || + vkAllocateMemory(device, &alloc, nullptr, + &m_sceneReadUBOMemory[i]) != VK_SUCCESS) + return false; + vkBindBufferMemory(device, m_sceneReadUBOBuffers[i], + m_sceneReadUBOMemory[i], 0); + vkMapMemory(device, m_sceneReadUBOMemory[i], 0, sizeof(SceneReadUBO), 0, + &m_sceneReadUBOMapped[i]); + std::memset(m_sceneReadUBOMapped[i], 0, sizeof(SceneReadUBO)); + + VkDescriptorBufferInfo bufInfo{}; + bufInfo.buffer = m_sceneReadUBOBuffers[i]; + bufInfo.range = sizeof(SceneReadUBO); + + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = m_shadowSamplerDescriptorSets[i]; + write.dstBinding = SCENE_UBO_BINDING; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + write.pBufferInfo = &bufInfo; + vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); + } + + m_sceneReadCreated = true; + m_sceneReadWritten = {}; + for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) + UpdateSceneReadDescriptors(i); + return true; +} + +/// Destroys everything CreateSceneReadResources made plus the snapshots. +void VulkanRenderer::CleanupSceneReadResources() { + CleanupSceneCopyImages(); + for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { + if (m_sceneReadUBOMapped[i]) + vkUnmapMemory(device, m_sceneReadUBOMemory[i]); + if (m_sceneReadUBOBuffers[i]) + vkDestroyBuffer(device, m_sceneReadUBOBuffers[i], nullptr); + if (m_sceneReadUBOMemory[i]) + vkFreeMemory(device, m_sceneReadUBOMemory[i], nullptr); + m_sceneReadUBOMapped[i] = nullptr; + m_sceneReadUBOBuffers[i] = VK_NULL_HANDLE; + m_sceneReadUBOMemory[i] = VK_NULL_HANDLE; + } + DestroyImage(device, m_sceneFallbackImage, m_sceneFallbackMemory, + m_sceneFallbackView); + DestroyImage(device, m_sceneFallbackCubeImage, m_sceneFallbackCubeMemory, + m_sceneFallbackCubeView); + if (m_sceneLinearSampler) + vkDestroySampler(device, m_sceneLinearSampler, nullptr); + if (m_scenePointSampler) + vkDestroySampler(device, m_scenePointSampler, nullptr); + m_sceneLinearSampler = VK_NULL_HANDLE; + m_scenePointSampler = VK_NULL_HANDLE; + m_sceneReadWritten = {}; + m_sceneReadCreated = false; +} + +/// Creates the extent-sized scene color and depth snapshot images, cleared +/// and shader readable until the first copy lands. +bool VulkanRenderer::CreateSceneCopyImages() { + CleanupSceneCopyImages(); + const VkImageUsageFlags usage = + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (!CreateSampledImage(device, physicalDevice, m_hdrSceneFormat, scExtent, + SINGLE_LAYER, usage, VK_IMAGE_ASPECT_COLOR_BIT, + m_sceneColorCopyImage, m_sceneColorCopyMemory, + m_sceneColorCopyView) || + !CreateSampledImage(device, physicalDevice, depthFormat, scExtent, + SINGLE_LAYER, usage, VK_IMAGE_ASPECT_DEPTH_BIT, + m_sceneDepthCopyImage, m_sceneDepthCopyMemory, + m_sceneDepthCopyView)) { + CleanupSceneCopyImages(); + return false; + } + const ClearTarget targets[] = { + {m_sceneColorCopyImage, VK_IMAGE_ASPECT_COLOR_BIT, SINGLE_LAYER}, + {m_sceneDepthCopyImage, DepthAspects(depthFormat), SINGLE_LAYER}}; + ClearToShaderRead(device, commands, graphicsQueue, targets, + static_cast(std::size(targets))); + return true; +} + +/// Destroys the snapshot images and forces a set 3 rewrite on both frames. +void VulkanRenderer::CleanupSceneCopyImages() { + DestroyImage(device, m_sceneColorCopyImage, m_sceneColorCopyMemory, + m_sceneColorCopyView); + DestroyImage(device, m_sceneDepthCopyImage, m_sceneDepthCopyMemory, + m_sceneDepthCopyView); + m_sceneReadWritten = {}; +} + +/// Points a frame's set 3 bindings 2-4 at the current snapshot and sky. +/// Call only while that frame's command buffer is not pending. +void VulkanRenderer::UpdateSceneReadDescriptors(uint32_t frame) { + if (!m_sceneReadCreated) return; + + SceneReadViews want; + want.color = + m_sceneColorCopyView ? m_sceneColorCopyView : m_sceneFallbackView; + want.depth = + m_sceneDepthCopyView ? m_sceneDepthCopyView : m_sceneFallbackView; + want.sky = + m_skyboxCubemapView ? m_skyboxCubemapView : m_sceneFallbackCubeView; + want.skySerial = m_skyboxViewSerial; + + SceneReadViews& have = m_sceneReadWritten[frame]; + std::array infos{}; + std::array writes{}; + uint32_t count = 0; + auto add = [&](uint32_t binding, VkImageView view, VkSampler sampler) { + infos[count].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + infos[count].imageView = view; + infos[count].sampler = sampler; + writes[count].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[count].dstSet = m_shadowSamplerDescriptorSets[frame]; + writes[count].dstBinding = binding; + writes[count].descriptorCount = 1; + writes[count].descriptorType = + VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[count].pImageInfo = &infos[count]; + ++count; + }; + if (have.color != want.color) + add(SCENE_COLOR_BINDING, want.color, m_sceneLinearSampler); + if (have.depth != want.depth) + add(SCENE_DEPTH_BINDING, want.depth, m_scenePointSampler); + if (have.sky != want.sky || have.skySerial != want.skySerial) + add(SCENE_SKY_BINDING, want.sky, + m_skyboxCubemapView ? m_skyboxCubemapSampler + : m_sceneLinearSampler); + if (count > 0) + vkUpdateDescriptorSets(device, count, writes.data(), 0, nullptr); + have = want; +} + +/// Fills this frame's SceneReadUBO and, inside the deferred forward pass, +/// snapshots the scene. Runs once per frame on the first scene-reading draw. +void VulkanRenderer::PrepareSceneRead() { + if (m_sceneReadPrepared || !m_sceneReadCreated) return; + m_sceneReadPrepared = true; + + const SceneReadViews& bound = m_sceneReadWritten[currentFrame]; + const bool sceneValid = m_forwardPassOpen && m_inForwardTransparentPass && + m_sceneColorCopyImage != VK_NULL_HANDLE && + bound.color == m_sceneColorCopyView && + bound.depth == m_sceneDepthCopyView; + if (sceneValid) RecordSceneCopy(); + + SceneReadUBO ubo{}; + Math::Matrix4 view, projection; + std::memcpy(&view(0, 0), m_cachedView, sizeof(m_cachedView)); + std::memcpy(&projection(0, 0), m_cachedProjection, + sizeof(m_cachedProjection)); + const Math::Matrix4 viewProj = view * projection; + std::memcpy(ubo.ViewProj, &viewProj(0, 0), sizeof(ubo.ViewProj)); + std::memcpy(ubo.InvViewProj, m_cachedInvViewProj, sizeof(ubo.InvViewProj)); + ubo.ScreenWidth = static_cast(std::max(scExtent.width, 1u)); + ubo.ScreenHeight = static_cast(std::max(scExtent.height, 1u)); + ubo.InvScreenWidth = 1.0f / ubo.ScreenWidth; + ubo.InvScreenHeight = 1.0f / ubo.ScreenHeight; + // Same UV-to-NDC scale the GBuffer push constants use + const bool jitter = + m_taaEnabled && m_taaResourcesCreated && m_inForwardTransparentPass; + ubo.JitterX = jitter ? m_taaJitter[0] * UV_TO_NDC : 0.0f; + ubo.JitterY = jitter ? m_taaJitter[1] * UV_TO_NDC : 0.0f; + ubo.NearPlane = m_cachedNearPlane; + ubo.FarPlane = m_cachedFarPlane; + ubo.SceneValid = static_cast(sceneValid); + ubo.SkyValid = static_cast(bound.sky != m_sceneFallbackCubeView); + std::memcpy(m_sceneReadUBOMapped[currentFrame], &ubo, sizeof(ubo)); +} + +/// Ends the forward pass, copies the HDR scene and depth into the +/// snapshots, and reopens the pass with its contents loaded. +void VulkanRenderer::RecordSceneCopy() { + vkCmdEndRenderPass(command); + + const VkImageAspectFlags depthAspects = DepthAspects(depthFormat); + const VkImageMemoryBarrier before[] = { + MakeBarrier(m_hdrSceneImage, VK_IMAGE_ASPECT_COLOR_BIT, SINGLE_LAYER, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_ACCESS_TRANSFER_READ_BIT), + MakeBarrier(depthImage, depthAspects, SINGLE_LAYER, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, + VK_ACCESS_TRANSFER_READ_BIT), + MakeBarrier(m_sceneColorCopyImage, VK_IMAGE_ASPECT_COLOR_BIT, + SINGLE_LAYER, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, + VK_ACCESS_TRANSFER_WRITE_BIT), + MakeBarrier(m_sceneDepthCopyImage, depthAspects, SINGLE_LAYER, + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, + VK_ACCESS_TRANSFER_WRITE_BIT)}; + vkCmdPipelineBarrier(command, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, + nullptr, static_cast(std::size(before)), + before); + + VkImageCopy region{}; + region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.dstSubresource = region.srcSubresource; + region.extent = {scExtent.width, scExtent.height, 1}; + vkCmdCopyImage(command, m_hdrSceneImage, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, m_sceneColorCopyImage, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + region.dstSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + vkCmdCopyImage(command, depthImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + m_sceneDepthCopyImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, ®ion); + + const VkImageMemoryBarrier after[] = { + MakeBarrier(m_hdrSceneImage, VK_IMAGE_ASPECT_COLOR_BIT, SINGLE_LAYER, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, 0, + VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT), + MakeBarrier(depthImage, depthAspects, SINGLE_LAYER, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, 0, + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT), + MakeBarrier(m_sceneColorCopyImage, VK_IMAGE_ASPECT_COLOR_BIT, + SINGLE_LAYER, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT), + MakeBarrier(m_sceneDepthCopyImage, depthAspects, SINGLE_LAYER, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT)}; + vkCmdPipelineBarrier(command, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, + static_cast(std::size(after)), after); + + VkRenderPassBeginInfo rpBegin{}; + rpBegin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rpBegin.renderPass = m_forwardRenderPass; + rpBegin.framebuffer = m_forwardFramebuffers[CurrentFrameIndex]; + rpBegin.renderArea.extent = scExtent; + vkCmdBeginRenderPass(command, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); + FillFullscreenViewportScissor(command, scExtent); +} + +} // namespace RenderEngine +} // namespace Sleak diff --git a/src/Graphics/Vulkan/VulkanShadow.cpp b/src/Graphics/Vulkan/VulkanShadow.cpp index 3ff9a4d6..0d105537 100644 --- a/src/Graphics/Vulkan/VulkanShadow.cpp +++ b/src/Graphics/Vulkan/VulkanShadow.cpp @@ -1,11 +1,12 @@ -#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" -#include "../../include/private/Graphics/Common/RenderCommandQueue.hpp" - #include #include #include +#include #include #include + +#include "../../include/private/Graphics/Common/RenderCommandQueue.hpp" +#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" #include "Core/Logger.hpp" namespace Sleak { @@ -187,25 +188,19 @@ bool VulkanRenderer::CreateShadowResources() { cmdBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; vkBeginCommandBuffer(cmdBuf, &cmdBeginInfo); - VkImageMemoryBarrier barrier{}; - barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - barrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; - barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.image = m_shadowImage; - barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; - barrier.subresourceRange.baseMipLevel = 0; - barrier.subresourceRange.levelCount = 1; - barrier.subresourceRange.baseArrayLayer = 0; - barrier.subresourceRange.layerCount = 1; - barrier.srcAccessMask = 0; - barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - - vkCmdPipelineBarrier(cmdBuf, - VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - 0, 0, nullptr, 0, nullptr, 1, &barrier); + // Clear to far depth + VkClearValue clear{}; + clear.depthStencil = {1.0f, 0}; + VkRenderPassBeginInfo clearPass{}; + clearPass.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + clearPass.renderPass = m_shadowRenderPass; + clearPass.framebuffer = m_shadowFramebuffer; + clearPass.renderArea.extent = {m_shadowMapResolution, + m_shadowMapResolution}; + clearPass.clearValueCount = 1; + clearPass.pClearValues = &clear; + vkCmdBeginRenderPass(cmdBuf, &clearPass, VK_SUBPASS_CONTENTS_INLINE); + vkCmdEndRenderPass(cmdBuf); vkEndCommandBuffer(cmdBuf); @@ -333,8 +328,8 @@ bool VulkanRenderer::CreateShadowPipeline() { rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; // same winding as main pass rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rasterizer.depthBiasEnable = VK_TRUE; - rasterizer.depthBiasConstantFactor = 1.25f; - rasterizer.depthBiasSlopeFactor = 1.75f; + rasterizer.depthBiasConstantFactor = SHADOW_DEPTH_BIAS_CONSTANT; + rasterizer.depthBiasSlopeFactor = SHADOW_DEPTH_BIAS_SLOPE; rasterizer.depthBiasClamp = 0.0f; VkPipelineMultisampleStateCreateInfo msaa{}; @@ -375,7 +370,7 @@ bool VulkanRenderer::CreateShadowPipeline() { pipelineInfo.basePipelineIndex = -1; VkResult result = vkCreateGraphicsPipelines( - device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &m_shadowPipeline); + device, m_pipelineCache, 1, &pipelineInfo, nullptr, &m_shadowPipeline); if (result != VK_SUCCESS) { SLEAK_ERROR("VulkanRenderer: Failed to create shadow pipeline!"); return false; @@ -422,12 +417,15 @@ bool VulkanRenderer::CreateShadowLightUBOResources() { } // Create descriptor pool for light UBO + shadow samplers. - // Two shadow samplers per frame now (compare + raw for PCSS blocker search). + // Two shadow samplers per frame now (compare + raw for PCSS blocker + // search). Set 3 also carries the scene-read color, depth, sky, and UBO + // bindings. std::array poolSizes{}; poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; - poolSizes[0].descriptorCount = MAX_FRAMES_IN_FLIGHT; + poolSizes[0].descriptorCount = + MAX_FRAMES_IN_FLIGHT * LIGHT_POOL_UBOS_PER_FRAME; poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - poolSizes[1].descriptorCount = MAX_FRAMES_IN_FLIGHT * 2; + poolSizes[1].descriptorCount = MAX_FRAMES_IN_FLIGHT * SET3_SAMPLER_COUNT; VkDescriptorPoolCreateInfo poolInfo{}; poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; @@ -591,6 +589,7 @@ void VulkanRenderer::CleanupShadowResources() { void VulkanRenderer::BeginShadowPass() { m_shadowPassActive = true; m_shadowPCCacheValid = false; + m_shadowPCPushedLayout = VK_NULL_HANDLE; } /// Marks the shadow pass inactive. @@ -598,11 +597,21 @@ void VulkanRenderer::EndShadowPass() { m_shadowPassActive = false; } -/// Copies light and shadow data into the current frame's mapped UBO. +/// Copies light and shadow data into the current frame's mapped UBO. LightVP +/// is replaced by the matrix the shadow map was rendered with, since the map +/// is recorded in BeginRender before the caller stages a newer one. void VulkanRenderer::UpdateShadowLightUBO(const void* data, uint32_t size) { if (!m_lightUBOCreated || !data) return; uint32_t copySize = std::min(size, static_cast(sizeof(ShadowLightUBO))); memcpy(m_lightUBOMapped[currentFrame], data, copySize); + + constexpr size_t kLightVPOffset = offsetof(ShadowLightUBO, LightVP); + if (m_shadowMapRendered && + copySize >= kLightVPOffset + sizeof(m_shadowMapLightVP)) { + char* dst = static_cast(m_lightUBOMapped[currentFrame]); + memcpy(dst + kLightVPOffset, m_shadowMapLightVP, + sizeof(m_shadowMapLightVP)); + } } /// Stages the light view-projection matrix for commit at the next BeginRender. diff --git a/src/Graphics/Vulkan/VulkanSwapchain.cpp b/src/Graphics/Vulkan/VulkanSwapchain.cpp index 6d9ddd60..b5ca6150 100644 --- a/src/Graphics/Vulkan/VulkanSwapchain.cpp +++ b/src/Graphics/Vulkan/VulkanSwapchain.cpp @@ -12,6 +12,26 @@ namespace Sleak { namespace RenderEngine { + namespace { + + /// Short name of a present mode for logs. + const char* PresentModeName(VkPresentModeKHR mode) { + switch (mode) { + case VK_PRESENT_MODE_IMMEDIATE_KHR: + return "IMMEDIATE"; + case VK_PRESENT_MODE_MAILBOX_KHR: + return "MAILBOX"; + case VK_PRESENT_MODE_FIFO_KHR: + return "FIFO"; + case VK_PRESENT_MODE_FIFO_RELAXED_KHR: + return "FIFO_RELAXED"; + default: + return "OTHER"; + } + } + + } // namespace + /// Creates the swapchain from the queried surface capabilities. bool VulkanRenderer::CreateSwapChain() { auto details = QuerySwapchain(); @@ -23,7 +43,7 @@ bool VulkanRenderer::CreateSwapChain() { auto format = ChooseFormat(details->formats); auto mode = ChoosePresentMode(details->presentModes); - auto extent = ChooseExtend(details.value()); + auto extent = ChooseExtend(details->caps); uint32_t imageCount = details->caps.minImageCount + 1; if (details->caps.maxImageCount > 0 && @@ -39,6 +59,11 @@ bool VulkanRenderer::CreateSwapChain() { info.imageArrayLayers = 1; info.presentMode = mode; info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + m_swapchainTransferSrc = + m_captureMode && + (details->caps.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + if (m_swapchainTransferSrc) + info.imageUsage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; uint32_t indices[] = {QueueIDs.GraphicsIndex, QueueIDs.PresentIndex}; @@ -70,6 +95,9 @@ bool VulkanRenderer::CreateSwapChain() { scImageFormat = format.format; scExtent = extent; + SLEAK_INFO("Swapchain: {}x{}, {} images, format {}, present mode {}", + extent.width, extent.height, scImageCount, + static_cast(format.format), PresentModeName(mode)); return true; } @@ -226,10 +254,12 @@ bool VulkanRenderer::CreateDepthResources() { imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; // Always add SAMPLED_BIT so deferred lighting pass can read depth. // When deferred is enabled, force 1x samples (GBuffer is non-MSAA). + // TRANSFER_SRC_BIT: scene-read snapshot for transparent formats. VkSampleCountFlagBits depthSamples = m_deferredEnabled ? VK_SAMPLE_COUNT_1_BIT : m_msaaSamples; - imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT - | VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | + VK_IMAGE_USAGE_SAMPLED_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT; imageInfo.samples = depthSamples; imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; @@ -411,10 +441,9 @@ VkPresentModeKHR VulkanRenderer::ChoosePresentMode( /// Clamps the window size to the surface's supported extent. // Fixed: clamp height using height, not width -VkExtent2D VulkanRenderer::ChooseExtend(SwapchainDetails details) { - if (details.caps.currentExtent.width != - std::numeric_limits::max()) - return details.caps.currentExtent; +VkExtent2D VulkanRenderer::ChooseExtend(const VkSurfaceCapabilitiesKHR& caps) { + if (caps.currentExtent.width != std::numeric_limits::max()) + return caps.currentExtent; int width, height; SDL_GetWindowSizeInPixels(sdlWindow->GetSDLWindow(), &width, &height); @@ -423,17 +452,25 @@ VkExtent2D VulkanRenderer::ChooseExtend(SwapchainDetails details) { static_cast(height)}; actualExtent.width = - std::clamp(actualExtent.width, - details.caps.minImageExtent.width, - details.caps.maxImageExtent.width); + std::clamp(actualExtent.width, caps.minImageExtent.width, + caps.maxImageExtent.width); actualExtent.height = - std::clamp(actualExtent.height, - details.caps.minImageExtent.height, - details.caps.maxImageExtent.height); + std::clamp(actualExtent.height, caps.minImageExtent.height, + caps.maxImageExtent.height); return actualExtent; } +/// True when the surface now wants a different extent than the swapchain +/// has. A failed query keeps the current swapchain. +bool VulkanRenderer::SurfaceExtentChanged() { + VkSurfaceCapabilitiesKHR caps{}; + if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, + &caps) != VK_SUCCESS) + return false; + const VkExtent2D extent = ChooseExtend(caps); + return extent.width != scExtent.width || extent.height != scExtent.height; +} } } diff --git a/src/Graphics/Vulkan/VulkanTAA.cpp b/src/Graphics/Vulkan/VulkanTAA.cpp index 49e5c443..9cdc7877 100644 --- a/src/Graphics/Vulkan/VulkanTAA.cpp +++ b/src/Graphics/Vulkan/VulkanTAA.cpp @@ -30,10 +30,8 @@ bool VulkanRenderer::CreateTAAResources() { ic.format = fmt; ic.tiling = VK_IMAGE_TILING_OPTIMAL; ic.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - ic.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT - | VK_IMAGE_USAGE_SAMPLED_BIT - | VK_IMAGE_USAGE_TRANSFER_SRC_BIT - | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + ic.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; ic.samples = VK_SAMPLE_COUNT_1_BIT; ic.sharingMode = VK_SHARING_MODE_EXCLUSIVE; if (vkCreateImage(device, &ic, nullptr, &m_taaImages[i]) != VK_SUCCESS) return false; @@ -149,13 +147,21 @@ bool VulkanRenderer::CreateTAAResources() { sub.colorAttachmentCount = 1; sub.pColorAttachments = &ref; - VkSubpassDependency dep{}; - dep.srcSubpass = VK_SUBPASS_EXTERNAL; - dep.dstSubpass = 0; - dep.srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; - dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - dep.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; - dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + std::array deps{}; + deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; + deps[0].dstSubpass = 0; + deps[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + deps[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + + // Feeds history and the post passes + deps[1].srcSubpass = 0; + deps[1].dstSubpass = VK_SUBPASS_EXTERNAL; + deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + deps[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; VkRenderPassCreateInfo rpi{}; rpi.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; @@ -163,8 +169,8 @@ bool VulkanRenderer::CreateTAAResources() { rpi.pAttachments = &att; rpi.subpassCount = 1; rpi.pSubpasses = ⊂ - rpi.dependencyCount = 1; - rpi.pDependencies = &dep; + rpi.dependencyCount = static_cast(deps.size()); + rpi.pDependencies = deps.data(); if (vkCreateRenderPass(device, &rpi, nullptr, &m_taaRenderPass) != VK_SUCCESS) return false; } @@ -346,8 +352,8 @@ bool VulkanRenderer::CreateTAAResources() { gpi.layout = m_taaPipelineLayout; gpi.renderPass = m_taaRenderPass; gpi.subpass = 0; - if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpi, nullptr, - &m_taaPipeline) != VK_SUCCESS) { + if (vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &gpi, nullptr, + &m_taaPipeline) != VK_SUCCESS) { SLEAK_ERROR("TAA: failed to create pipeline"); return false; } @@ -391,29 +397,45 @@ void VulkanRenderer::CleanupTAAResources() { if (m_taaMemory[i]) { vkFreeMemory(device, m_taaMemory[i], nullptr); m_taaMemory[i] = VK_NULL_HANDLE; } } + m_taaResolvedView = VK_NULL_HANDLE; m_taaResourcesCreated = false; } -/// Computes the inverse current view-projection and reprojection blend factor into the TAA UBO. +/// Computes the current-to-previous clip reprojection and blend factor into the +/// TAA UBO. Uses double: in float, inverse(VP) * VP drifts from identity far +/// from the origin. void VulkanRenderer::UpdateTAAUBO() { if (!m_taaResourcesCreated || !m_taaUboMapped[currentFrame]) return; - // VP = View * Projection (row-major) - float currentVP[16]; - MatMul4(m_cachedView, m_cachedProjection, currentVP); + double view[16], proj[16]; + for (int i = 0; i < 16; ++i) { + view[i] = static_cast(m_cachedView[i]); + proj[i] = static_cast(m_cachedProjection[i]); + } - float invCurrentVP[16]; - if (!InvertMat4(currentVP, invCurrentVP)) { - memset(invCurrentVP, 0, sizeof(invCurrentVP)); - for (int i = 0; i < 4; ++i) invCurrentVP[i * 4 + i] = 1.0f; // identity fallback + // VP = View * Projection (row-major) + double currentVP[16]; + MatMul4(view, proj, currentVP); + + // First frame: identity reprojection + if (m_taaFrameIdx == 0) + memcpy(m_prevViewProj, currentVP, sizeof(m_prevViewProj)); + + double invCurrentVP[16]; + double currentToPrev[16]; + if (InvertMat4(currentVP, invCurrentVP)) { + MatMul4(invCurrentVP, m_prevViewProj, currentToPrev); + } else { + for (int i = 0; i < 16; ++i) + currentToPrev[i] = (i / 4 == i % 4) ? 1.0 : 0.0; } TAAParams p{}; - memcpy(p.InvCurrentVP, invCurrentVP, sizeof(p.InvCurrentVP)); - memcpy(p.PrevVP, m_prevViewProj, sizeof(p.PrevVP)); + for (int i = 0; i < 16; ++i) + p.CurrentToPrevClip[i] = static_cast(currentToPrev[i]); p.ScreenW = static_cast(scExtent.width); p.ScreenH = static_cast(scExtent.height); - // First two frames skip history (prev VP is zero-initialized) + // First two frames skip history p.BlendFactor = (m_taaFrameIdx <= 1) ? 1.0f : m_taaBlendFactor; p._pad = 0.0f; @@ -423,12 +445,21 @@ void VulkanRenderer::UpdateTAAUBO() { memcpy(m_prevViewProj, currentVP, sizeof(m_prevViewProj)); } -/// Resolves the current frame against TAA history and copies the result back into the HDR scene image. +/// Resolves the frame against TAA history into the ping-pong target. void VulkanRenderer::RenderTAAPass() { - if (!m_taaResourcesCreated || !m_taaEnabled) return; + m_taaResolvedView = VK_NULL_HANDLE; + if (!m_taaResourcesCreated || !m_taaEnabled) { + m_taaRanLastFrame = false; + return; + } + // History and the previous VP are stale after TAA was off: restart + // accumulation + if (!m_taaRanLastFrame) m_taaFrameIdx = 0; + m_taaRanLastFrame = true; const uint32_t writeIdx = static_cast(m_taaFrameIdx % 2); const uint32_t readIdx = 1u - writeIdx; + BeginGpuPass(GpuPass::TAA); // ---- 1. Depth barrier: DEPTH_STENCIL_ATTACHMENT → READ_ONLY ---- // (SSR will skip its own depth barrier since TAA already handles it) @@ -518,58 +549,17 @@ void VulkanRenderer::RenderTAAPass() { // taaImages[writeIdx] is now SHADER_READ_ONLY_OPTIMAL (render pass finalLayout) } - // ---- 6. Copy TAA resolve → hdrScene ---- - // Both images are SHADER_READ_ONLY; transition for transfer then restore. - { - VkImageMemoryBarrier toTransfer[2]{}; - toTransfer[0].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - toTransfer[0].oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - toTransfer[0].newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; - toTransfer[0].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - toTransfer[0].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - toTransfer[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT; - toTransfer[0].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - toTransfer[0].image = m_taaImages[writeIdx]; - toTransfer[0].subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - - toTransfer[1] = toTransfer[0]; - toTransfer[1].newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - toTransfer[1].dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - toTransfer[1].image = m_hdrSceneImage; - - vkCmdPipelineBarrier(command, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, - 0, 0, nullptr, 0, nullptr, 2, toTransfer); - - VkImageCopy region{}; - region.srcSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; - region.dstSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; - region.extent = { scExtent.width, scExtent.height, 1 }; - vkCmdCopyImage(command, - m_taaImages[writeIdx], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - m_hdrSceneImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - 1, ®ion); - - VkImageMemoryBarrier toRead[2]{}; - toRead[0] = toTransfer[0]; - toRead[0].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; - toRead[0].newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - toRead[0].srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - toRead[0].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - - toRead[1] = toRead[0]; - toRead[1].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - toRead[1].srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - toRead[1].image = m_hdrSceneImage; - - vkCmdPipelineBarrier(command, - VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - 0, 0, nullptr, 0, nullptr, 2, toRead); - } + m_taaResolvedView = m_taaViews[writeIdx]; + EndGpuPass(GpuPass::TAA); // Advance ping-pong for next frame m_taaFrameIdx++; } +/// HDR scene color for the post passes: the TAA resolve when TAA ran. +VkImageView VulkanRenderer::PostSceneView() const { + return m_taaResolvedView ? m_taaResolvedView : m_hdrSceneView; +} + } // namespace RenderEngine } // namespace Sleak diff --git a/src/Graphics/Vulkan/VulkanTexture.cpp b/src/Graphics/Vulkan/VulkanTexture.cpp index 92d09cc7..a3328115 100644 --- a/src/Graphics/Vulkan/VulkanTexture.cpp +++ b/src/Graphics/Vulkan/VulkanTexture.cpp @@ -1,14 +1,78 @@ #include "../../include/private/Graphics/Vulkan/VulkanTexture.hpp" + #include -#include #include -#include + +#include #include #include +#include +#include +#include +#include namespace Sleak { namespace RenderEngine { +namespace { + +/// GPU objects of a destroyed texture, kept until no submitted frame can +/// still reference them. +struct ReleasedTexture { + VkDevice device = VK_NULL_HANDLE; + VkDescriptorPool pool = VK_NULL_HANDLE; + std::vector sets; + VkSampler sampler = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + uint64_t serial = 0; +}; + +std::mutex g_releaseMutex; +std::vector g_released; +uint64_t g_releaseSerial = 1; + +/// Frees a released texture's descriptor sets and destroys its objects. +void DestroyReleasedTexture(ReleasedTexture& entry) { + if (entry.pool != VK_NULL_HANDLE && !entry.sets.empty()) { + vkFreeDescriptorSets(entry.device, entry.pool, + static_cast(entry.sets.size()), + entry.sets.data()); + } + if (entry.sampler) vkDestroySampler(entry.device, entry.sampler, nullptr); + if (entry.view) vkDestroyImageView(entry.device, entry.view, nullptr); + if (entry.image) vkDestroyImage(entry.device, entry.image, nullptr); + if (entry.memory) vkFreeMemory(entry.device, entry.memory, nullptr); +} + +} // namespace + +/// Stamps textures released from now on with the serial of the next frame +/// submission, the last one that may still reference them. +void VulkanTexture::SetReleaseSerial(uint64_t serial) { + std::lock_guard lock(g_releaseMutex); + g_releaseSerial = serial; +} + +/// Destroys released textures whose stamped submission has completed. +void VulkanTexture::DestroyReleased(uint64_t completedSerial) { + std::lock_guard lock(g_releaseMutex); + auto done = std::stable_partition( + g_released.begin(), g_released.end(), + [&](const ReleasedTexture& e) { return e.serial > completedSerial; }); + for (auto it = done; it != g_released.end(); ++it) + DestroyReleasedTexture(*it); + g_released.erase(done, g_released.end()); +} + +/// Destroys every released texture; the caller guarantees the GPU is idle. +void VulkanTexture::DestroyAllReleased() { + std::lock_guard lock(g_releaseMutex); + for (ReleasedTexture& entry : g_released) DestroyReleasedTexture(entry); + g_released.clear(); +} + VulkanTexture::VulkanTexture(VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, @@ -274,6 +338,25 @@ uint64_t VulkanTexture::GetImGuiTextureID() const { return reinterpret_cast(m_imguiDescriptorSet); } +/// Takes ownership of per-swapchain-image descriptor sets allocated from +/// pool, releasing any sets the texture held before. +void VulkanTexture::SetDescriptorSets(std::vector sets, + VkDescriptorPool pool) { + if (!m_descriptorSets.empty()) { + ReleasedTexture entry; + entry.device = m_device; + entry.pool = m_descriptorPool; + entry.sets = std::move(m_descriptorSets); + std::lock_guard lock(g_releaseMutex); + entry.serial = g_releaseSerial; + g_released.push_back(std::move(entry)); + } + m_descriptorSets = std::move(sets); + m_descriptorPool = pool; +} + +/// Hands the descriptor sets and GPU objects to the release queue so they +/// outlive any frame still in flight. void VulkanTexture::Cleanup() { if (m_device == VK_NULL_HANDLE) return; @@ -281,22 +364,25 @@ void VulkanTexture::Cleanup() { ImGui_ImplVulkan_RemoveTexture(m_imguiDescriptorSet); m_imguiDescriptorSet = VK_NULL_HANDLE; } - if (m_sampler != VK_NULL_HANDLE) { - vkDestroySampler(m_device, m_sampler, nullptr); - m_sampler = VK_NULL_HANDLE; - } - if (m_imageView != VK_NULL_HANDLE) { - vkDestroyImageView(m_device, m_imageView, nullptr); - m_imageView = VK_NULL_HANDLE; - } - if (m_image != VK_NULL_HANDLE) { - vkDestroyImage(m_device, m_image, nullptr); - m_image = VK_NULL_HANDLE; - } - if (m_imageMemory != VK_NULL_HANDLE) { - vkFreeMemory(m_device, m_imageMemory, nullptr); - m_imageMemory = VK_NULL_HANDLE; - } + if (m_descriptorSets.empty() && m_sampler == VK_NULL_HANDLE && + m_imageView == VK_NULL_HANDLE && m_image == VK_NULL_HANDLE && + m_imageMemory == VK_NULL_HANDLE) + return; + + ReleasedTexture entry; + entry.device = m_device; + entry.pool = m_descriptorPool; + entry.sets = std::move(m_descriptorSets); + entry.sampler = std::exchange(m_sampler, VK_NULL_HANDLE); + entry.view = std::exchange(m_imageView, VK_NULL_HANDLE); + entry.image = std::exchange(m_image, VK_NULL_HANDLE); + entry.memory = std::exchange(m_imageMemory, VK_NULL_HANDLE); + m_descriptorSets.clear(); + m_descriptorPool = VK_NULL_HANDLE; + + std::lock_guard lock(g_releaseMutex); + entry.serial = g_releaseSerial; + g_released.push_back(std::move(entry)); } uint32_t VulkanTexture::FindMemoryType( diff --git a/src/Lighting/LightManager.cpp b/src/Lighting/LightManager.cpp index 8b46086e..6d377630 100644 --- a/src/Lighting/LightManager.cpp +++ b/src/Lighting/LightManager.cpp @@ -203,6 +203,7 @@ void LightManager::UpdateShadowData() { DirectionalLight* activeLight = shadowLight ? shadowLight : anyDirLight; if (!activeLight) { + m_hasShadowLightVP = false; static bool warned = false; if (!warned) { SLEAK_WARN("UpdateShadowData: No directional light found!"); warned = true; } return; @@ -314,6 +315,8 @@ void LightManager::UpdateShadowData() { // Set the light VP matrix on the renderer renderer->SetLightVP(&lightVP(0, 0)); + std::memcpy(m_shadowLightVP, &lightVP(0, 0), sizeof(m_shadowLightVP)); + m_hasShadowLightVP = shadowLight != nullptr; // Build shadow light UBO const auto& camPos = Camera::GetMainCameraPosition(); @@ -340,10 +343,14 @@ void LightManager::UpdateShadowData() { // nowhere else to stash time). Using a static Timer keeps this // self-contained and independent of Application state. static Sleak::Timer s_sceneClock; + static const bool s_fixedStep = !CommandLine::GetValue("-capture").empty(); + static uint64_t s_sceneFrames = 0; ubo.CameraPos[0] = camPos.GetX(); ubo.CameraPos[1] = camPos.GetY(); ubo.CameraPos[2] = camPos.GetZ(); - ubo.CameraPos[3] = s_sceneClock.Elapsed(); + // Capture: frame-driven clock + ubo.CameraPos[3] = s_fixedStep ? static_cast(s_sceneFrames++) / 60.0f + : s_sceneClock.Elapsed(); // CURRENT lightVP — renderers stage SetLightVP and commit at BeginRender, // so this frame's shadow map IS rendered with this matrix. The old