diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d8b57a73..184e0cce 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,6 +2,9 @@ version: 2 updates: - package-ecosystem: github-actions directory: / + # Updates are tested in staging like every other change; main only takes + # pull requests from staging. + target-branch: staging schedule: interval: monthly commit-message: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7b379491..b32f1b7d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,3 +1,5 @@ + + ## What this changes diff --git a/.github/rulesets/approval.json b/.github/rulesets/approval.json new file mode 100644 index 00000000..6a80923b --- /dev/null +++ b/.github/rulesets/approval.json @@ -0,0 +1,27 @@ +{ + "name": "Maintainer approval", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["refs/heads/main", "refs/heads/staging"], + "exclude": [] + } + }, + "bypass_actors": [ + { "actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "pull_request" } + ], + "rules": [ + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 1, + "dismiss_stale_reviews_on_push": true, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": false, + "allowed_merge_methods": ["merge", "squash", "rebase"] + } + } + ] +} diff --git a/.github/rulesets/main.json b/.github/rulesets/main.json new file mode 100644 index 00000000..f08bece3 --- /dev/null +++ b/.github/rulesets/main.json @@ -0,0 +1,70 @@ +{ + "name": "Protect main", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": [ + "refs/heads/main" + ], + "exclude": [] + } + }, + "bypass_actors": [], + "rules": [ + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": false, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": true, + "allowed_merge_methods": [ + "merge" + ] + } + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": false, + "required_status_checks": [ + { + "context": "Only staging merges into main" + }, + { + "context": "Format (changed lines)" + }, + { + "context": "Linux Debug" + }, + { + "context": "Linux Release" + }, + { + "context": "Linux Clang Release" + }, + { + "context": "Windows Debug" + }, + { + "context": "Windows Release" + }, + { + "context": "Shaders compile and are committed" + }, + { + "context": "Downstream game still builds" + } + ] + } + } + ] +} diff --git a/.github/rulesets/staging.json b/.github/rulesets/staging.json new file mode 100644 index 00000000..2bb3d750 --- /dev/null +++ b/.github/rulesets/staging.json @@ -0,0 +1,69 @@ +{ + "name": "Protect staging", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": [ + "refs/heads/staging" + ], + "exclude": [] + } + }, + "bypass_actors": [], + "rules": [ + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": false, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": true, + "allowed_merge_methods": [ + "merge", + "squash", + "rebase" + ] + } + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": false, + "required_status_checks": [ + { + "context": "Format (changed lines)" + }, + { + "context": "Linux Debug" + }, + { + "context": "Linux Release" + }, + { + "context": "Linux Clang Release" + }, + { + "context": "Windows Debug" + }, + { + "context": "Windows Release" + }, + { + "context": "Shaders compile and are committed" + }, + { + "context": "Downstream game still builds" + } + ] + } + } + ] +} diff --git a/.github/workflows/branch-policy.yml b/.github/workflows/branch-policy.yml new file mode 100644 index 00000000..e2196fcc --- /dev/null +++ b/.github/workflows/branch-policy.yml @@ -0,0 +1,43 @@ +name: Branch policy + +# staging is the testing layer: every change lands there first, and main only +# takes pull requests from staging. This check fails any other pull request +# into main. It also runs on pull requests into staging, where it always +# passes, so retargeting a pull request from main to staging clears the +# failure it picked up there. +# +# pull_request_target runs this file as it exists on the base branch, so a +# pull request cannot edit the check into passing. Nothing from the pull +# request is checked out or run, and the job holds no token permissions. +on: + pull_request_target: + branches: [main, staging] + types: [opened, reopened, synchronize, edited] + +permissions: {} + +jobs: + # The job name is a required status check in .github/rulesets/main.json. + # Keep the verdict inside the step: a job skipped by an `if:` reports + # success, which would satisfy the required check. + staging-to-main: + name: Only staging merges into main + runs-on: ubuntu-latest + steps: + - name: Check where the pull request comes from + env: + BASE_REF: ${{ github.event.pull_request.base.ref }} + HEAD_REF: ${{ github.event.pull_request.head.ref }} + HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} + run: | + if [ "$BASE_REF" != "main" ]; then + echo "Targets $BASE_REF, which accepts pull requests from any branch." + exit 0 + fi + if [ "$HEAD_REPO" = "$GITHUB_REPOSITORY" ] && [ "$HEAD_REF" = "staging" ]; then + echo "Release pull request from staging into main." + exit 0 + fi + echo "This pull request comes from ${HEAD_REPO:-a deleted fork}:${HEAD_REF}." + echo "::error::main only accepts pull requests from staging. Edit this pull request and change its base branch to staging." + exit 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f289cd9..e4545f31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [main] + branches: [main, staging] pull_request: - branches: [main] + branches: [main, staging] workflow_dispatch: # A newer push to the same branch makes the older run irrelevant. @@ -245,12 +245,19 @@ jobs: submodules: recursive path: SleakEngine + # Engine staging pairs with game staging, engine main with game main - name: Check out the game uses: actions/checkout@v7 with: repository: CanReader/SleakCraft + ref: ${{ github.base_ref }} path: SleakCraft + - name: Point the game at this engine revision + run: | + rm -rf SleakCraft/Engine + ln -s ../SleakEngine SleakCraft/Engine + - name: Install dependencies run: | sudo apt-get update diff --git a/CMakeLists.txt b/CMakeLists.txt index 26a243af..c74dc440 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -113,6 +113,7 @@ set(CORE_SOURCES src/Core/Application.cpp src/Core/CommandLine.cpp src/Core/File.cpp + src/Core/FileWatcher.cpp src/Core/Logger.cpp src/Core/ScopedTimer.cpp src/Core/Serializable.cpp @@ -128,6 +129,7 @@ set(DEBUG_SOURCES src/Debug/Benchmark.cpp src/Debug/DebugLineRenderer.cpp src/Debug/DebugOverlay.cpp + src/Debug/ShaderHotReload.cpp src/Debug/SystemMetrics.cpp ) @@ -148,6 +150,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 +158,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 @@ -369,6 +375,7 @@ endforeach() if(GLSLC) set(ENGINE_SPIRV_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders") file(MAKE_DIRECTORY "${ENGINE_SPIRV_DIR}") + target_compile_definitions(Engine PRIVATE SLEAK_GLSLC_PATH="${GLSLC}") foreach(_src ${ENGINE_VULKAN_SHADERS}) get_filename_component(_name "${_src}" NAME) set(_spv "${ENGINE_SPIRV_DIR}/${_name}.spv") @@ -425,6 +432,10 @@ add_dependencies(Engine CompileShaders) set_property(GLOBAL PROPERTY ENGINE_SPIRV_FILES ${ENGINE_SPIRV_FILES}) set_property(GLOBAL PROPERTY ENGINE_COMMITTED_SPIRV ${ENGINE_COMMITTED_SPIRV}) +target_compile_definitions(Engine + PUBLIC $<$:SLEAK_HOT_RELOAD> + PRIVATE SLEAK_ENGINE_SHADER_DIR="${CMAKE_CURRENT_SOURCE_DIR}/assets/shaders") + function(_engine_collect_targets dir out) get_property(_targets DIRECTORY "${dir}" PROPERTY BUILDSYSTEM_TARGETS) get_property(_subdirs DIRECTORY "${dir}" PROPERTY SUBDIRECTORIES) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..c103fb02 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# Contributing + +## Branches + +`staging` is the testing layer between a change and `main`. Everything lands in `staging` first, gets tested there, and reaches `main` only when `staging` is merged into it. Every game built on the engine follows `main`, so nothing reaches them untested. + +| Branch | Holds | Accepts | +|--------|-------|---------| +| `staging` | Changes being tested | Pull requests from any branch, once CI passes and a maintainer approves | +| `main` | Tested, released code | Pull requests from `staging` only, once CI passes and a maintainer approves | + +Open every pull request against `staging`. A pull request into `main` from any other branch fails the **Only staging merges into main** check and cannot be merged. To fix it, edit the pull request and change its base branch to `staging`. + +--- + +## Testing in staging + +Every pull request into `staging` runs the full CI: the format check, the Linux and Windows build matrix, the shader check, and the downstream game build. + +The downstream game build uses the SleakCraft branch of the same tier: pull requests into `staging` build SleakCraft `staging`, and pull requests into `main` build SleakCraft `main`. SleakCraft's own CI pairs the same way, so a change that spans both repos goes into both `staging` branches and is tested together. + +--- + +## Releasing staging to main + +1. Open a pull request from `staging` into `main`. +2. Wait for CI to pass. +3. Merge it with **Create a merge commit**. +4. When SleakCraft has matching changes, release its `staging` right after, so the game's `main` builds against the engine's `main` again. + +`main` only allows merge commits. Squash and rebase copy staging's commits into new ones, so the two branches would stop sharing history, and every later release would replay old commits and hit conflicts that aren't real. + +After a release, GitHub shows `staging` as behind `main`. The commits it is missing are the release merge commits, which change nothing on their own and need no action. + +--- + +## Branch protection + +The rules are the JSON files in `.github/rulesets/`, which GitHub imports as they are: **Settings → Rules → Rulesets → New ruleset → Import a ruleset**. + +| Rule | `main` | `staging` | +|------|--------|-----------| +| Changes arrive by pull request only | ✓ | ✓ | +| No force pushes, no deletion | ✓ | ✓ | +| CI must pass | ✓ | ✓ | +| Review threads resolved | ✓ | ✓ | +| A maintainer approves | ✓ | ✓ | +| Source must be `staging` | ✓ | | +| Merge method | Merge commit | Any | + +> **Setting it up?** Import `main.json` only after `.github/workflows/branch-policy.yml` is on `main`. The ruleset requires the check that workflow reports, so until the workflow exists nothing can merge into `main`. **Allow merge commits** must stay on under Settings → General → Pull Requests. + +The approval rule lives in its own ruleset, `approval.json`, because GitHub does not let authors approve their own pull requests. Repository admins can bypass that one ruleset, and only through a pull request, so a maintainer can merge their own work once CI passes. Everyone else needs a maintainer's approval, and a new push dismisses an earlier approval. + +Nobody can bypass the other two rulesets, admins included. To get out of a jam, set the ruleset's enforcement to **Disabled**, fix the problem, then set it back to **Active**. + +Required checks are matched by name, so renaming a job in `ci.yml` or `branch-policy.yml` means updating the rulesets to match. 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..e6c936c7 100644 --- a/include/private/Graphics/Common/Renderer.hpp +++ b/include/private/Graphics/Common/Renderer.hpp @@ -1,12 +1,16 @@ #pragma once - -#include "RenderContext.hpp" #include #include +#include +#include + +#include "RenderContext.hpp" namespace Sleak { +class Benchmark; + namespace RenderEngine { /// Selects which graphics API a Renderer instance targets. @@ -47,6 +51,12 @@ class ENGINE_API Renderer { virtual void Cleanup() = 0; /// Blocks until the GPU has finished all outstanding work. virtual void WaitIdle() {} + /// Rebuilds what uses the given shader stems from the files on disk. + /// Returns false when the backend has no hot reload support. + virtual bool ReloadShaders(const std::vector& stems) { + (void)stems; + return false; + } /// Flushes any buffered upload/staging transfers still in flight. virtual void FlushPendingTransfers() {} @@ -63,6 +73,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 +200,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 +336,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/DirectX11/DirectX11Renderer.hpp b/include/private/Graphics/DirectX11/DirectX11Renderer.hpp index 22767ab6..be8d4612 100644 --- a/include/private/Graphics/DirectX11/DirectX11Renderer.hpp +++ b/include/private/Graphics/DirectX11/DirectX11Renderer.hpp @@ -30,6 +30,10 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext { void BeginRender() override; void EndRender() override; void Cleanup() override; +#ifdef SLEAK_HOT_RELOAD + /// Recompiles every live shader and fixed pass built from one of the stems. + bool ReloadShaders(const std::vector& stems) override; +#endif /// Allocates the depth/stencil texture and view sized to the swapchain. bool CreateDepthStencilBuffer(uint32_t width, uint32_t height); @@ -200,6 +204,12 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext { bool m_tonemapResourcesCreated = false; /// Allocates the HDR offscreen target and tonemap shaders/sampler. bool CreateTonemapResources(); + /// Compiles tonemap.hlsl, writing the outputs only on success. + bool CompileTonemapShaders(ID3D11VertexShader** vs, ID3D11PixelShader** ps); +#ifdef SLEAK_HOT_RELOAD + /// Swaps in recompiled tonemap shaders, keeping the old ones on failure. + void ReloadTonemapShaders(); +#endif void CleanupTonemapResources(); /// Runs the ACES tonemap full-screen pass from the HDR target to the back buffer. void ExecuteTonemapPass(); diff --git a/include/private/Graphics/DirectX11/DirectX11Shader.hpp b/include/private/Graphics/DirectX11/DirectX11Shader.hpp index 493ccb01..26c5842c 100644 --- a/include/private/Graphics/DirectX11/DirectX11Shader.hpp +++ b/include/private/Graphics/DirectX11/DirectX11Shader.hpp @@ -9,6 +9,9 @@ #include #include #include +#ifdef SLEAK_HOT_RELOAD +#include +#endif #include "../Common/Shader.hpp" @@ -20,6 +23,8 @@ class DirectX11Shader : public Shader { public: DirectX11Shader(ID3D11Device* device); ~DirectX11Shader() override; + DirectX11Shader(const DirectX11Shader&) = delete; + DirectX11Shader& operator=(const DirectX11Shader&) = delete; /// Compiles the combined-path convention (vs_main/ps_main entry points) from a single HLSL file. bool compile(const std::string& shaderPath) override; @@ -33,7 +38,41 @@ class DirectX11Shader : public Shader { ID3D11InputLayout* createInputLayout(); ID3D11InputLayout* getInputLayout() const { return Layout.Get(); } +#ifdef SLEAK_HOT_RELOAD + /// Rebuilds stages and input layout, keeping the old ones on failure. + bool Reload(); + /// Shares the stages and layout of a shader built from the same files. + void AdoptStages(const DirectX11Shader& source); + /// Returns true when both shaders compile the same files and entry points. + bool HasSameSources(const DirectX11Shader& other) const { + return m_sources == other.m_sources; + } + /// Returns the stem of the vertex source, e.g. skybox for skybox.hlsl. + const std::string& GetStem() const { return m_vertStem; } + /// Returns true when the vertex or pixel source has the given stem. + bool UsesStem(const std::string& stem) const { + return stem == m_vertStem || stem == m_fragStem; + } + /// Returns every live DirectX11Shader instance. + static const std::unordered_set& GetLiveShaders(); +#endif + private: + /// Files and entry points the two stages compile from. + struct StageSources { + std::string vertPath; + std::string vertEntry; + std::string fragPath; + std::string fragEntry; + + bool operator==(const StageSources&) const = default; + }; + + /// Compiles both stages into new objects and swaps them in on success. + bool BuildStages(StageSources sources); + /// Creates an input layout matching the inputs of the vertex bytecode. + bool CreateLayoutFor(ID3DBlob* vertexBlob, + Microsoft::WRL::ComPtr& layout); /// Compiles one HLSL entry point/profile via D3DCompile into a shader object and bytecode blob. template bool compileShader(const std::string& filePath, @@ -54,6 +93,12 @@ class DirectX11Shader : public Shader { m_vertexShaderBlob; // For input layout creation Microsoft::WRL::ComPtr m_pixelShaderBlob; +#ifdef SLEAK_HOT_RELOAD + StageSources m_sources; + std::string m_vertStem; + std::string m_fragStem; +#endif + D3D11_INPUT_ELEMENT_DESC DefaultLayout[5] = { // Semantic Index Format Slot Offset Class Step Rate {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0}, diff --git a/include/private/Graphics/DirectX12/DirectX12Renderer.hpp b/include/private/Graphics/DirectX12/DirectX12Renderer.hpp index fdbb5a95..9adc6df4 100644 --- a/include/private/Graphics/DirectX12/DirectX12Renderer.hpp +++ b/include/private/Graphics/DirectX12/DirectX12Renderer.hpp @@ -40,6 +40,10 @@ class ENGINE_API DirectX12Renderer : public Renderer, public RenderContext { void EndRender() override; void Cleanup() override; void WaitIdle() override { WaitForGPU(); } +#ifdef SLEAK_HOT_RELOAD + /// Rebuilds the PSOs of live shaders and fixed passes using the stems. + bool ReloadShaders(const std::vector& stems) override; +#endif virtual void Resize(uint32_t width, uint32_t height) override; @@ -136,6 +140,23 @@ class ENGINE_API DirectX12Renderer : public Renderer, public RenderContext { /// Returns the cached PSO for the key, building it on a miss. ID3D12PipelineState* GetPipeline(const PipelineKey& key, ID3DBlob* vs, ID3DBlob* ps); + /// Returns the default-state PSO for a vertex/pixel shader pair. + ID3D12PipelineState* GetShaderPipeline(ID3DBlob* vs, ID3DBlob* ps); +#ifdef SLEAK_HOT_RELOAD + /// Recompiles live shaders using the stems, collecting their old PSOs. + void ReloadLiveShaders(const std::vector& stems, + std::vector& replaced); + /// Rebuilds a fixed pass PSO, keeping the old one on failure. + void ReloadPassPipeline(ID3D12PipelineState*& pso, bool& failed, + bool (DirectX12Renderer::*create)(), + const char* stem, + std::vector& replaced); + /// Recompiles the shadow vertex shader and rebuilds the shadow PSO. + void ReloadShadowPassPSO(std::vector& replaced); + /// Drops cached PSOs that were replaced and that nothing binds anymore. + void PruneReplacedPipelines( + const std::vector& replaced); +#endif /// Reapplies the last shader PSO after a pass override ends. void RestoreShaderPipeline(); /// Copies the light/fog constants into the ring and binds them at b2. diff --git a/include/private/Graphics/DirectX12/DirectX12Shader.hpp b/include/private/Graphics/DirectX12/DirectX12Shader.hpp index b0dad66b..aab32feb 100644 --- a/include/private/Graphics/DirectX12/DirectX12Shader.hpp +++ b/include/private/Graphics/DirectX12/DirectX12Shader.hpp @@ -8,6 +8,9 @@ #include #include #include +#ifdef SLEAK_HOT_RELOAD +#include +#endif #include "../Common/Shader.hpp" @@ -20,6 +23,8 @@ class DirectX12Shader : public Shader { public: DirectX12Shader(ID3D12Device* device); ~DirectX12Shader() override; + DirectX12Shader(const DirectX12Shader&) = delete; + DirectX12Shader& operator=(const DirectX12Shader&) = delete; /// Compiles the combined-path convention (vs_main/ps_main entry points) from a single HLSL file. bool compile(const std::string& shaderPath) override; @@ -39,12 +44,50 @@ class DirectX12Shader : public Shader { /// Routes bind() through the renderer so pass-specific PSOs are respected. void SetRenderer(DirectX12Renderer* renderer); +#ifdef SLEAK_HOT_RELOAD + /// Recompiles both stages from their files without touching the live ones. + bool RecompileStages(Microsoft::WRL::ComPtr& vs, + Microsoft::WRL::ComPtr& ps) const; + /// Swaps in new bytecode together with the PSO built from it. + void ReplaceStages(Microsoft::WRL::ComPtr vs, + Microsoft::WRL::ComPtr ps, + ID3D12PipelineState* pso); + /// Returns true when both shaders compile the same files and entry points. + bool HasSameSources(const DirectX12Shader& other) const { + return m_sources == other.m_sources; + } + /// Returns the stem of the vertex source, e.g. skybox for skybox_dx12.hlsl. + const std::string& GetStem() const { return m_vertStem; } + /// Returns true when the vertex or pixel source has the given stem. + bool UsesStem(const std::string& stem) const { + return stem == m_vertStem || stem == m_fragStem; + } + /// Returns every live DirectX12Shader instance. + static const std::unordered_set& GetLiveShaders(); +#endif + private: + /// Files and entry points the two stages compile from. + struct StageSources { + std::string vertPath; + std::string vertEntry; + std::string fragPath; + std::string fragEntry; + + bool operator==(const StageSources&) const = default; + }; + + /// Compiles both stages and swaps them in only when both succeed. + bool BuildStages(StageSources sources); + /// Compiles both stages of the given sources into new blobs. + bool CompileSources(const StageSources& sources, + Microsoft::WRL::ComPtr& vs, + Microsoft::WRL::ComPtr& ps) const; /// Compiles one HLSL entry point/profile via D3DCompileFromFile into bytecode. bool compileShader(const std::string& filePath, - const std::string& entryPoint, - const std::string& profile, - Microsoft::WRL::ComPtr& blob); + const std::string& entryPoint, + const std::string& profile, + Microsoft::WRL::ComPtr& blob) const; Microsoft::WRL::ComPtr m_device; @@ -53,6 +96,11 @@ class DirectX12Shader : public Shader { Microsoft::WRL::ComPtr m_pipelineState; ID3D12GraphicsCommandList* m_commandList = nullptr; DirectX12Renderer* m_renderer = nullptr; +#ifdef SLEAK_HOT_RELOAD + StageSources m_sources; + std::string m_vertStem; + std::string m_fragStem; +#endif }; } // namespace RenderEngine diff --git a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp index eec29d7d..4f926baf 100644 --- a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp +++ b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp @@ -25,6 +25,10 @@ class ENGINE_API OpenGLRenderer : public Renderer, public RenderContext { void BeginRender() override; void EndRender() override; void Cleanup() override; +#ifdef SLEAK_HOT_RELOAD + /// Relinks every live shader built from one of the given stems. + bool ReloadShaders(const std::vector& stems) override; +#endif virtual uint32_t GetFeatureCaps() const override { return CapDeferred | CapSSAO | CapIBL | CapShadows; @@ -123,9 +127,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 +210,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/OpenGL/OpenGLShader.hpp b/include/private/Graphics/OpenGL/OpenGLShader.hpp index 37537319..da7bdf86 100644 --- a/include/private/Graphics/OpenGL/OpenGLShader.hpp +++ b/include/private/Graphics/OpenGL/OpenGLShader.hpp @@ -1,9 +1,15 @@ #ifndef OPENGLSHADER_HPP_ #define OPENGLSHADER_HPP_ -#include "../Common/Shader.hpp" #include +#include + +#include "../Common/Shader.hpp" +#ifdef SLEAK_HOT_RELOAD +#include +#endif + namespace Sleak { namespace RenderEngine { @@ -12,6 +18,8 @@ class ENGINE_API OpenGLShader : public Shader { public: OpenGLShader(); ~OpenGLShader() override; + OpenGLShader(const OpenGLShader&) = delete; + OpenGLShader& operator=(const OpenGLShader&) = delete; /// Splits a combined shader file into vertex/fragment stages and links them. bool compile(const std::string& shaderPath) override; @@ -21,13 +29,33 @@ class ENGINE_API OpenGLShader : public Shader { GLuint GetProgram() const { return m_program; } -private: + /// Rebuilds the program from its files, keeping the old one on failure. + bool Reload(); + /// Returns the stem of the vertex source, e.g. gbuffer for gbuffer_gl.vert. + const std::string& GetStem() const { return m_vertStem; } + /// Returns true when the vertex or fragment source has the given stem. + bool UsesStem(const std::string& stem) const { + return stem == m_vertStem || stem == m_fragStem; + } + +#ifdef SLEAK_HOT_RELOAD + /// Returns every live OpenGLShader instance. + static const std::unordered_set& GetLiveShaders(); +#endif + + private: /// Compiles a single shader stage from source text. bool CompileShaderSource(const std::string& source, GLenum type, - GLuint& shader); + const std::string& path, GLuint& shader); + /// Links a new program from the stored paths and swaps it in on success. + bool BuildProgram(); std::string ReadFile(const std::string& path); GLuint m_program = 0; + std::string m_vertPath; + std::string m_fragPath; + std::string m_vertStem; + std::string m_fragStem; }; } // 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..95b56fae 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,478 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { /// Recreates the extent-dependent shadow map objects at the queued resolution. void ApplyShadowResolutionChange() override; +#ifdef SLEAK_HOT_RELOAD + /// Rebuilds every pipeline from the SPIR-V on disk; a pipeline that fails + /// to rebuild keeps its previous version. + bool ReloadShaders(const std::vector& stems) override; +#endif + + // 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(); + +#ifdef SLEAK_HOT_RELOAD + // Shader hot reload + /// The pipelines, layouts, and shaders one create function writes, swapped + /// as a unit when shaders reload. + struct PipelineGroup { + const char* name; + std::vector pipelines; + std::vector layouts; + std::vector shaders; + bool (VulkanRenderer::*create)(); + }; + /// Rebuilds a live group through its create function, keeping the old + /// objects when the rebuild fails. + void ReloadPipelineGroup(const PipelineGroup& group); + /// Rebuilds every cached custom-format variant, keeping the old pipeline of + /// a variant that fails. + void ReloadCustomFormatPipelines(); +#endif + + // 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 = {}; @@ -784,6 +900,8 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { /// Creates the SSR image, render pass, framebuffer, descriptors, UBOs, and pipeline. bool CreateSSRResources(); + /// Compiles the SSR shaders and creates the SSR pipeline. + bool CreateSSRPipeline(); /// Destroys the SSR pipeline, framebuffer, descriptors, image, and sampler. void CleanupSSRResources(); /// Fills the SSR UBO with the cached camera matrices, camera position, and ray march parameters. @@ -795,10 +913,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 +925,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 +934,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,17 +950,21 @@ 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. bool CreateTAAResources(); + /// Compiles the TAA shaders and creates the TAA resolve pipeline. + bool CreateTAAPipeline(); /// Destroys the TAA pipeline, framebuffers, descriptors, images, and sampler. 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 +1056,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/Core/FileWatcher.hpp b/include/public/Core/FileWatcher.hpp new file mode 100644 index 00000000..27ba0e13 --- /dev/null +++ b/include/public/Core/FileWatcher.hpp @@ -0,0 +1,34 @@ +#ifndef _FILE_WATCHER_HPP_ +#define _FILE_WATCHER_HPP_ + +#include +#include +#include +#include + +namespace Sleak { + +/// Reports files saved inside watched directories. Not recursive. +/// Generic: shader, library and asset hot reload all build on it. +/// @ingroup core +class ENGINE_API FileWatcher { + public: + FileWatcher(); + ~FileWatcher(); + FileWatcher(const FileWatcher&) = delete; + FileWatcher& operator=(const FileWatcher&) = delete; + + /// Starts watching a directory. Returns false if it can't be watched. + bool AddDirectory(const std::string& dir); + + /// Appends full paths of files saved since the last call. Never blocks. + void Poll(std::vector& outChanged); + + private: + struct Impl; + std::unique_ptr m_impl; +}; + +} // namespace Sleak + +#endif diff --git a/include/public/Debug/ShaderHotReload.hpp b/include/public/Debug/ShaderHotReload.hpp new file mode 100644 index 00000000..d1fbfd92 --- /dev/null +++ b/include/public/Debug/ShaderHotReload.hpp @@ -0,0 +1,25 @@ +#ifndef _SHADER_HOT_RELOAD_HPP_ +#define _SHADER_HOT_RELOAD_HPP_ + +#include +#include + +namespace Sleak { +namespace Debug { + +/// Recompiles and reloads shaders when their source files are saved. +/// Debug builds only; every call is a no-op in Release. +/// @ingroup debug +class ENGINE_API ShaderHotReload { + public: + /// Watches a shader source folder. Later folders override earlier ones. + static void AddSourceDirectory(const std::string& dir); + + /// Rebuilds changed shaders. Called once per frame by Application. + static void Poll(); +}; + +} // namespace Debug +} // namespace Sleak + +#endif 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/Math/Vector.hpp b/include/public/Math/Vector.hpp index ad3ae8a8..bf256e56 100644 --- a/include/public/Math/Vector.hpp +++ b/include/public/Math/Vector.hpp @@ -43,17 +43,17 @@ namespace Sleak } // Access elements (read/write) - [[nodiscard]] T& operator[](size_t index) { + [[nodiscard]] constexpr T& operator[](size_t index) { assert(index < N && "Vector index out of range"); return data[index]; } // Read-only access - [[nodiscard]] const T& operator[](size_t index) const { + [[nodiscard]] constexpr const T& operator[](size_t index) const { assert(index < N && "Vector index out of range"); return data[index]; } - + // Basic operations Vector operator+(const Vector& other) const { Vector result; 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..c7e7dc73 100644 --- a/src/Core/Application.cpp +++ b/src/Core/Application.cpp @@ -1,38 +1,41 @@ #include "../../include/public/Core/Application.hpp" -#include "../../include/public/Core/CommandLine.hpp" -#include "../../include/private/Graphics/Common/RendererFactory.hpp" -#include "../../include/private/Graphics/Common/RenderCommandQueue.hpp" -#include -#include -#include -#include -#include -#include -#include -#include "Graphics/Vulkan/VulkanRenderer.hpp" -#include "Core/Logger.hpp" -#include #include -#include #include - #include -#include -#include -#include #include -#include -#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include + +#include "../../include/private/Graphics/Common/RenderCommandQueue.hpp" +#include "../../include/private/Graphics/Common/RendererFactory.hpp" +#include "../../include/public/Core/CommandLine.hpp" +#include "Core/Logger.hpp" +#include "ECS/Components/FirstPersonController.hpp" +#include "ECS/Components/FreeLookCameraController.hpp" #include "ECS/Components/MeshComponent.hpp" #include "ECS/Components/TransformComponent.hpp" -#include "ECS/Components/FreeLookCameraController.hpp" -#include "ECS/Components/FirstPersonController.hpp" -#include -#include -#include +#include "Graphics/Vulkan/VulkanRenderer.hpp" + +#define DEFAULT_CAPTURE_FRAME 600 using namespace Sleak; using namespace Sleak::Math; @@ -141,6 +144,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 +156,29 @@ 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; + + Debug::ShaderHotReload::AddSourceDirectory(SLEAK_ENGINE_SHADER_DIR); // Initialize and begin the game (and scene) if (Game) { @@ -187,7 +213,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 +224,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 +239,18 @@ namespace Sleak { } } + if (captureMode && !captureArmed && + frameIndex >= captureFrame) { + captureArmed = true; + if (!renderer->RequestFrameCapture(capturePath)) { + SLEAK_ERROR("Frame capture unavailable on {}", + renderer->GetTypeStr()); + CoreWindow->Close(); + } + } + + Debug::ShaderHotReload::Poll(); + renderer->BeginRender(); // Update active scene if present @@ -247,6 +286,10 @@ namespace Sleak { queue->ExecuteCommands(context); renderer->EndRender(); + + if (captureArmed && !renderer->IsFrameCapturePending()) + CoreWindow->Close(); + ++frameIndex; } } else{ @@ -387,6 +430,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 +457,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 +478,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/FileWatcher.cpp b/src/Core/FileWatcher.cpp new file mode 100644 index 00000000..c0177ee2 --- /dev/null +++ b/src/Core/FileWatcher.cpp @@ -0,0 +1,188 @@ +#include +#include +#include + +#if defined(PLATFORM_LINUX) +#include +#include + +#include +#include +#include +#elif defined(PLATFORM_WIN) +#define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +namespace Sleak { + +namespace { +constexpr size_t EVENT_BUFFER_SIZE = 4096; +} + +#if defined(PLATFORM_LINUX) + +struct FileWatcher::Impl { + int fd = -1; + std::unordered_map dirs; +}; + +FileWatcher::FileWatcher() : m_impl(std::make_unique()) { + m_impl->fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + if (m_impl->fd < 0) + SLEAK_ERROR("FileWatcher: inotify_init1 failed: {}", + std::strerror(errno)); +} + +FileWatcher::~FileWatcher() { + if (m_impl->fd >= 0) close(m_impl->fd); +} + +bool FileWatcher::AddDirectory(const std::string& dir) { + if (m_impl->fd < 0) return false; + const int wd = inotify_add_watch(m_impl->fd, dir.c_str(), + IN_CLOSE_WRITE | IN_MOVED_TO); + if (wd < 0) { + SLEAK_ERROR("FileWatcher: can't watch {}: {}", dir, + std::strerror(errno)); + return false; + } + m_impl->dirs[wd] = dir; + return true; +} + +void FileWatcher::Poll(std::vector& outChanged) { + if (m_impl->fd < 0) return; + alignas(inotify_event) char buf[EVENT_BUFFER_SIZE]; + for (;;) { + const ssize_t len = read(m_impl->fd, buf, sizeof(buf)); + if (len <= 0) break; + for (char* p = buf; p < buf + len;) { + const auto* ev = reinterpret_cast(p); + p += sizeof(inotify_event) + ev->len; + if (ev->len == 0) continue; + const auto dir = m_impl->dirs.find(ev->wd); + if (dir == m_impl->dirs.end()) continue; + outChanged.push_back(dir->second + "/" + ev->name); + } + } +} + +#elif defined(PLATFORM_WIN) + +namespace { +constexpr DWORD WATCH_FILTER = + FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_FILE_NAME; + +struct WatchedDirectory { + std::string path; + HANDLE handle = INVALID_HANDLE_VALUE; + OVERLAPPED overlapped = {}; + alignas(DWORD) char buffer[EVENT_BUFFER_SIZE]; +}; + +/// Queues the next asynchronous change read for a directory. +bool Arm(WatchedDirectory& dir) { + ResetEvent(dir.overlapped.hEvent); + return ReadDirectoryChangesW(dir.handle, dir.buffer, sizeof(dir.buffer), + FALSE, WATCH_FILTER, nullptr, &dir.overlapped, + nullptr) != 0; +} + +/// Converts a counted UTF-16 file name to UTF-8. +std::string ToUtf8(const WCHAR* name, int length) { + const int size = WideCharToMultiByte(CP_UTF8, 0, name, length, nullptr, 0, + nullptr, nullptr); + std::string out(size, '\0'); + WideCharToMultiByte(CP_UTF8, 0, name, length, out.data(), size, nullptr, + nullptr); + return out; +} +} // namespace + +struct FileWatcher::Impl { + std::vector > dirs; +}; + +FileWatcher::FileWatcher() : m_impl(std::make_unique()) {} + +FileWatcher::~FileWatcher() { + for (auto& dir : m_impl->dirs) { + DWORD bytes = 0; + CancelIoEx(dir->handle, &dir->overlapped); + GetOverlappedResult(dir->handle, &dir->overlapped, &bytes, TRUE); + CloseHandle(dir->overlapped.hEvent); + CloseHandle(dir->handle); + } +} + +bool FileWatcher::AddDirectory(const std::string& dir) { + auto watched = std::make_unique(); + watched->path = dir; + watched->handle = + CreateFileA(dir.c_str(), FILE_LIST_DIRECTORY, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, nullptr); + if (watched->handle == INVALID_HANDLE_VALUE) { + SLEAK_ERROR("FileWatcher: can't open {} (error {})", dir, + GetLastError()); + return false; + } + watched->overlapped.hEvent = CreateEventA(nullptr, TRUE, FALSE, nullptr); + if (!watched->overlapped.hEvent || !Arm(*watched)) { + SLEAK_ERROR("FileWatcher: can't watch {} (error {})", dir, + GetLastError()); + if (watched->overlapped.hEvent) CloseHandle(watched->overlapped.hEvent); + CloseHandle(watched->handle); + return false; + } + m_impl->dirs.push_back(std::move(watched)); + return true; +} + +void FileWatcher::Poll(std::vector& outChanged) { + for (auto& dir : m_impl->dirs) { + DWORD bytes = 0; + if (!GetOverlappedResult(dir->handle, &dir->overlapped, &bytes, FALSE)) + continue; + for (DWORD offset = 0; bytes > 0;) { + const auto* info = reinterpret_cast( + dir->buffer + offset); + if (info->Action != FILE_ACTION_REMOVED && + info->Action != FILE_ACTION_RENAMED_OLD_NAME) { + const int length = + static_cast(info->FileNameLength / sizeof(WCHAR)); + outChanged.push_back(dir->path + "/" + + ToUtf8(info->FileName, length)); + } + if (info->NextEntryOffset == 0) break; + offset += info->NextEntryOffset; + } + Arm(*dir); + } +} + +#else + +struct FileWatcher::Impl {}; + +FileWatcher::FileWatcher() : m_impl(std::make_unique()) {} + +FileWatcher::~FileWatcher() = default; + +bool FileWatcher::AddDirectory(const std::string& dir) { + SLEAK_WARN("FileWatcher: not supported on this platform ({})", dir); + return false; +} + +void FileWatcher::Poll(std::vector& outChanged) { + (void)outChanged; +} + +#endif + +} // namespace Sleak 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/Debug/ShaderHotReload.cpp b/src/Debug/ShaderHotReload.cpp new file mode 100644 index 00000000..cee15336 --- /dev/null +++ b/src/Debug/ShaderHotReload.cpp @@ -0,0 +1,298 @@ +#include + +#ifdef SLEAK_HOT_RELOAD +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef PLATFORM_WIN +#include +#else +#include +#endif +#endif + +namespace Sleak { +namespace Debug { + +#ifdef SLEAK_HOT_RELOAD +namespace { + +namespace fs = std::filesystem; + +constexpr float DEBOUNCE_SECONDS = 0.15f; +constexpr float MS_PER_SECOND = 1000.0f; +constexpr size_t PROCESS_LINE_SIZE = 512; +constexpr const char* RUNTIME_SHADER_DIR = "assets/shaders"; +constexpr const char* SPIRV_EXTENSION = ".spv"; +constexpr const char* TEMP_EXTENSION = ".tmp"; +constexpr std::string_view GL_SUFFIX = "_gl"; +constexpr std::string_view DX12_SUFFIX = "_dx12"; + +struct State { + FileWatcher watcher; + std::vector directories; + std::set pending; + Timer quietTimer; +}; + +/// Lazily created hot reload state shared by all calls. +State& GetState() { + static State state; + return state; +} + +/// Per-process temp name next to a target, so two running games never clash. +fs::path TempPathFor(const fs::path& target) { +#ifdef PLATFORM_WIN + const int pid = _getpid(); +#else + const int pid = static_cast(getpid()); +#endif + fs::path tmp = target; + tmp += "." + std::to_string(pid) + TEMP_EXTENSION; + return tmp; +} + +/// Atomically replaces a target with a finished temp file. +bool ReplaceWith(const fs::path& tmp, const fs::path& target) { + std::error_code ec; + fs::rename(tmp, target, ec); + if (!ec) return true; + SLEAK_ERROR("Hot reload: can't replace {}: {}", target.filename().string(), + ec.message()); + fs::remove(tmp, ec); + return false; +} + +/// True for shader sources the watcher reacts to. +bool IsShaderSource(const fs::path& file) { + const fs::path ext = file.extension(); + return ext == ".vert" || ext == ".frag" || ext == ".hlsl"; +} + +/// True for GLSL that glslc compiles to SPIR-V for Vulkan. +bool IsVulkanSource(const fs::path& file) { + const fs::path ext = file.extension(); + if (ext != ".vert" && ext != ".frag") return false; + return !file.stem().string().ends_with(GL_SUFFIX); +} + +/// Stem shared by all backend variants, e.g. lighting_pass_gl.frag. +std::string StemOf(const fs::path& file) { + std::string name = file.filename().string(); + name = name.substr(0, name.find('.')); + for (std::string_view suffix : {GL_SUFFIX, DX12_SUFFIX}) { + if (name.ends_with(suffix)) { + name.resize(name.size() - suffix.size()); + break; + } + } + return name; +} + +/// Backend that loads this source file, picked from its suffix. +RenderEngine::RendererType BackendOf(const fs::path& file) { + using RenderEngine::RendererType; + const std::string stem = file.stem().string(); + if (file.extension() == ".hlsl") + return stem.ends_with(DX12_SUFFIX) ? RendererType::DirectX12 + : RendererType::DirectX11; + return stem.ends_with(GL_SUFFIX) ? RendererType::OpenGL + : RendererType::Vulkan; +} + +/// File name pattern a backend loads for a stem. +std::string VariantFor(const std::string& stem, + RenderEngine::RendererType type) { + using RenderEngine::RendererType; + switch (type) { + case RendererType::Vulkan: + return stem + ".vert / " + stem + ".frag"; + case RendererType::OpenGL: + return stem + "_gl.vert / " + stem + "_gl.frag"; + case RendererType::DirectX11: + return stem + ".hlsl"; + case RendererType::DirectX12: + return stem + "_dx12.hlsl"; + } + return stem; +} + +/// True when a later source folder has a file with the same name. +bool IsOverridden(const fs::path& file, const State& state) { + const auto dir = std::find(state.directories.begin(), + state.directories.end(), file.parent_path()); + if (dir == state.directories.end()) return false; + for (auto later = dir + 1; later != state.directories.end(); ++later) { + std::error_code ec; + if (fs::exists(*later / file.filename(), ec)) return true; + } + return false; +} + +/// Compiles GLSL to SPIR-V next to the source. Keeps the old .spv on error. +bool CompileSpirv(const fs::path& src, const fs::path& spv) { +#ifndef SLEAK_GLSLC_PATH + (void)spv; + SLEAK_WARN("Hot reload: glslc was not found at configure time, {} skipped", + src.filename().string()); + return false; +#else + const fs::path tmp = TempPathFor(spv); + std::string cmd = "\"" SLEAK_GLSLC_PATH "\" \"" + src.string() + + "\" -o \"" + tmp.string() + "\" 2>&1"; +#ifdef PLATFORM_WIN + cmd = "\"" + cmd + "\""; + FILE* pipe = _popen(cmd.c_str(), "r"); +#else + FILE* pipe = popen(cmd.c_str(), "r"); +#endif + if (!pipe) { + SLEAK_ERROR("Hot reload: can't run glslc for {}", + src.filename().string()); + return false; + } + std::string output; + char line[PROCESS_LINE_SIZE]; + while (std::fgets(line, sizeof(line), pipe)) output += line; +#ifdef PLATFORM_WIN + const int status = _pclose(pipe); +#else + const int status = pclose(pipe); +#endif + if (status != 0) { + std::error_code ec; + fs::remove(tmp, ec); + SLEAK_ERROR( + "Hot reload: {} failed to compile, keeping the old " + "shader\n{}", + src.filename().string(), output); + return false; + } + return ReplaceWith(tmp, spv); +#endif +} + +/// Copies a file into the runtime shader folder the backends load from. +bool CopyToRuntime(const fs::path& file) { + const fs::path target = fs::path(RUNTIME_SHADER_DIR) / file.filename(); + const fs::path tmp = TempPathFor(target); + std::error_code ec; + fs::copy_file(file, tmp, fs::copy_options::overwrite_existing, ec); + if (ec) { + SLEAK_ERROR("Hot reload: can't copy {}: {}", file.filename().string(), + ec.message()); + return false; + } + return ReplaceWith(tmp, target); +} + +/// Compiles and stages one saved file. Returns its stem, or empty to skip. +std::string ProcessFile(const fs::path& file, const State& state) { + std::error_code ec; + if (!fs::exists(file, ec)) return {}; + if (IsOverridden(file, state)) { + SLEAK_WARN( + "Hot reload: {} is overridden by a later shader folder, " + "edit that copy instead", + file.filename().string()); + return {}; + } + if (IsVulkanSource(file)) { + fs::path spv = file; + spv += SPIRV_EXTENSION; + if (!CompileSpirv(file, spv) || !CopyToRuntime(spv)) return {}; + } + if (!CopyToRuntime(file)) return {}; + return StemOf(file); +} + +} // namespace +#endif + +void ShaderHotReload::AddSourceDirectory(const std::string& dir) { +#ifdef SLEAK_HOT_RELOAD + std::error_code ec; + const fs::path path = fs::canonical(dir, ec); + if (ec) { + SLEAK_WARN("Hot reload: shader folder {} not found", dir); + return; + } + State& state = GetState(); + if (std::find(state.directories.begin(), state.directories.end(), path) != + state.directories.end()) + return; + if (!state.watcher.AddDirectory(path.string())) return; + state.directories.push_back(path); + SLEAK_INFO("Hot reload: watching {}", path.string()); +#else + (void)dir; +#endif +} + +void ShaderHotReload::Poll() { +#ifdef SLEAK_HOT_RELOAD + State& state = GetState(); + if (state.directories.empty()) return; + + std::vector changed; + state.watcher.Poll(changed); + for (const std::string& path : changed) { + const fs::path file(path); + if (!IsShaderSource(file)) continue; + state.pending.insert(file); + state.quietTimer.Reset(); + } + if (state.pending.empty() || state.quietTimer.Elapsed() < DEBOUNCE_SECONDS) + return; + + Application* app = Application::GetInstance(); + RenderEngine::Renderer* renderer = app ? app->GetRenderer() : nullptr; + if (!renderer) return; + const RenderEngine::RendererType active = renderer->GetType(); + + Timer reloadTimer; + std::vector stems; + for (const fs::path& file : state.pending) { + std::string stem = ProcessFile(file, state); + if (stem.empty()) continue; + if (BackendOf(file) != active) { + SLEAK_WARN( + "Hot reload: {} is not used by the {} renderer, edit {} " + "instead", + file.filename().string(), app->GetRendererTypeStr(), + VariantFor(stem, active)); + continue; + } + if (std::find(stems.begin(), stems.end(), stem) == stems.end()) + stems.push_back(std::move(stem)); + } + state.pending.clear(); + if (stems.empty()) return; + + app->WaitGPUIdle(); + if (!renderer->ReloadShaders(stems)) { + SLEAK_WARN("Hot reload: the {} backend can't reload shaders yet", + app->GetRendererTypeStr()); + return; + } + + std::string names; + for (const std::string& stem : stems) + names += (names.empty() ? "" : ", ") + stem; + SLEAK_INFO("Hot reload: reloaded {} in {:.0f} ms", names, + reloadTimer.Elapsed() * MS_PER_SECOND); +#endif +} + +} // namespace Debug +} // namespace Sleak diff --git a/src/Graphics/DirectX11/DirectX11Renderer.cpp b/src/Graphics/DirectX11/DirectX11Renderer.cpp index 727b28a1..3d96421b 100644 --- a/src/Graphics/DirectX11/DirectX11Renderer.cpp +++ b/src/Graphics/DirectX11/DirectX11Renderer.cpp @@ -1,25 +1,52 @@ #include "../../include/private/Graphics/DirectX11/DirectX11Renderer.hpp" #ifdef PLATFORM_WIN -#include #include +#include #include +#include +#include +#include +#include + +#include +#include #include #include -#include "Graphics/DirectX11/DirectX11Texture.hpp" -#include "Graphics/DirectX11/DirectX11CubemapTexture.hpp" #include -#include -#include -#include -#include -#include +#include #include +#include +#include + #include "Graphics/Common/RenderCommandQueue.hpp" +#include "Graphics/DirectX11/DirectX11CubemapTexture.hpp" +#include "Graphics/DirectX11/DirectX11Texture.hpp" namespace Sleak { namespace RenderEngine { - + + namespace { + + constexpr const char* TONEMAP_SHADER_PATH = "assets/shaders/tonemap.hlsl"; +#ifdef SLEAK_HOT_RELOAD + constexpr const char* TONEMAP_STEM = "tonemap"; +#endif + + /// Logs a failed D3DCompileFromFile call with the compiler output. + void LogCompileError(const char* path, const char* entry, HRESULT hr, + ID3DBlob* errors) { + if (errors) { + SLEAK_ERROR("{} ({}) compile error: {}", path, entry, + static_cast(errors->GetBufferPointer())); + } else { + SLEAK_ERROR("{} ({}) compile error: 0x{:08X}", path, entry, + static_cast(hr)); + } + } + + } // namespace + /// Registers this backend's buffer/shader/texture factories with ResourceManager. DirectX11Renderer::DirectX11Renderer(Window* window) : window(window), @@ -68,9 +95,9 @@ bool DirectX11Renderer::Initialize() { swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; UINT createDeviceFlags = 0; - #ifdef _DEBUG - createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG; - #endif +#ifdef _DEBUG + createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG; +#endif D3D_FEATURE_LEVEL featureLevels[] = { D3D_FEATURE_LEVEL_11_0 }; HRESULT hr = D3D11CreateDeviceAndSwapChain( @@ -88,7 +115,21 @@ bool DirectX11Renderer::Initialize() { &deviceContext // Device context ); +#ifdef _DEBUG + if (hr == DXGI_ERROR_SDK_COMPONENT_MISSING) { + SLEAK_WARN( + "D3D11 debug layer is not installed, creating device without it"); + createDeviceFlags &= ~D3D11_CREATE_DEVICE_DEBUG; + hr = D3D11CreateDeviceAndSwapChain( + nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, createDeviceFlags, + featureLevels, 1, D3D11_SDK_VERSION, &swapChainDesc, &swapChain, + &device, nullptr, &deviceContext); + } +#endif + if (FAILED(hr)) { + SLEAK_ERROR("Failed to create device and swap chain! HRESULT: 0x{:X}", + (unsigned)hr); return false; } @@ -103,43 +144,41 @@ bool DirectX11Renderer::Initialize() { SLEAK_INFO("DirectX 11 has successfully created!"); - #ifdef _DEBUG - ID3D11InfoQueue* infoQueue; - if (SUCCEEDED(device->QueryInterface(__uuidof(ID3D11InfoQueue), (void**)&infoQueue))) { - // Only break on corruption (critical), not on errors — breaking on - // errors without a debugger attached silently drops draw calls. - infoQueue->SetBreakOnSeverity(D3D11_MESSAGE_SEVERITY_CORRUPTION, true); +#ifdef _DEBUG + ID3D11InfoQueue* infoQueue; + if (SUCCEEDED(device->QueryInterface(__uuidof(ID3D11InfoQueue), + (void**)&infoQueue))) { + // Only break on corruption (critical), not on errors — breaking on + // errors without a debugger attached silently drops draw calls. + infoQueue->SetBreakOnSeverity(D3D11_MESSAGE_SEVERITY_CORRUPTION, true); - D3D11_MESSAGE_SEVERITY severities[] = { - D3D11_MESSAGE_SEVERITY_INFO - }; + D3D11_MESSAGE_SEVERITY severities[] = {D3D11_MESSAGE_SEVERITY_INFO}; - D3D11_INFO_QUEUE_FILTER filter = {}; - filter.DenyList.NumSeverities = _countof(severities); - filter.DenyList.pSeverityList = severities; + D3D11_INFO_QUEUE_FILTER filter = {}; + filter.DenyList.NumSeverities = _countof(severities); + filter.DenyList.pSeverityList = severities; - infoQueue->AddStorageFilterEntries(&filter); - infoQueue->Release(); - } - #endif + infoQueue->AddStorageFilterEntries(&filter); + infoQueue->Release(); + } +#endif - SetPerformanceCounter(true); - SetRenderMode(RenderMode::Fill); - SetRenderFace(RenderFace::Front); + SetPerformanceCounter(true); + SetRenderMode(RenderMode::Fill); + SetRenderFace(RenderFace::Front); - ConfigureRenderMode(); + ConfigureRenderMode(); + + SetDepthStencilState(true, true); - SetDepthStencilState(true, true); + msaaSampleCount = 1; - msaaSampleCount = 1; + D3D11_QUERY_DESC queryDesc; + queryDesc.Query = D3D11_QUERY::D3D11_QUERY_PIPELINE_STATISTICS; + queryDesc.MiscFlags = 0; + if (FAILED(device->CreateQuery(&queryDesc, &query))) + SLEAK_WARN("Could not create pipeline statics object!"); - D3D11_QUERY_DESC queryDesc; - queryDesc.Query = D3D11_QUERY::D3D11_QUERY_PIPELINE_STATISTICS; - queryDesc.MiscFlags = 0; - if(FAILED(device->CreateQuery(&queryDesc,&query))) - SLEAK_WARN("Could not create pipeline statics object!"); - - if(bEnabledPerformanceCounter){ if(query) while (deviceContext->GetData(query, nullptr, 0, 0) == S_FALSE); @@ -286,16 +325,16 @@ void DirectX11Renderer::Cleanup() { bImInitialized = false; } - #ifdef _DEBUG - if(device) { - ID3D11Debug* debugDevice; - device->QueryInterface(__uuidof(ID3D11Debug), (void**)&debugDevice); - if (debugDevice) { - debugDevice->ReportLiveDeviceObjects(D3D11_RLDO_DETAIL); - debugDevice->Release(); - } +#ifdef _DEBUG + if (device) { + ID3D11Debug* debugDevice; + device->QueryInterface(__uuidof(ID3D11Debug), (void**)&debugDevice); + if (debugDevice) { + debugDevice->ReportLiveDeviceObjects(D3D11_RLDO_DETAIL); + debugDevice->Release(); } - #endif + } +#endif if (device) { device->Release(); @@ -853,12 +892,75 @@ Shader* DirectX11Renderer::CreateShader(const std::string& shaderSource) { if (!bIsLayoutCreated && il) { layout = il; bIsLayoutCreated = true; + } else if (il) { + il->Release(); } return static_cast(shader); } + delete shader; return nullptr; } +#ifdef SLEAK_HOT_RELOAD +bool DirectX11Renderer::ReloadShaders(const std::vector& stems) { + const auto usesAnyStem = [&stems](const DirectX11Shader* shader) { + return std::ranges::any_of(stems, [shader](const std::string& stem) { + return shader->UsesStem(stem); + }); + }; + + // Shaders built from the same files compile once and share the result + std::vector rebuilt; + std::vector failed; + for (DirectX11Shader* shader : DirectX11Shader::GetLiveShaders()) { + if (!usesAnyStem(shader)) continue; + const auto sameSources = [shader](const DirectX11Shader* other) { + return shader->HasSameSources(*other); + }; + if (std::ranges::any_of(failed, sameSources)) continue; + + ID3D11InputLayout* oldLayout = shader->getInputLayout(); + const auto twin = std::ranges::find_if(rebuilt, sameSources); + if (twin != rebuilt.end()) { + shader->AdoptStages(**twin); + } else if (shader->Reload()) { + rebuilt.push_back(shader); + SLEAK_INFO("Reloaded DirectX 11 shader '{}'", shader->GetStem()); + } else { + failed.push_back(shader); + SLEAK_WARN("Shader '{}' failed to reload, keeping the old one", + shader->GetStem()); + continue; + } + + // The renderer holds its own reference to the first shader's layout + if (bIsLayoutCreated && oldLayout && oldLayout == layout) { + ID3D11InputLayout* fresh = shader->getInputLayout(); + if (fresh) fresh->AddRef(); + layout->Release(); + layout = fresh; + } + } + + if (std::find(stems.begin(), stems.end(), TONEMAP_STEM) != stems.end()) + ReloadTonemapShaders(); + + for (const std::string& stem : stems) { + const bool used = + stem == TONEMAP_STEM || + std::ranges::any_of(DirectX11Shader::GetLiveShaders(), + [&stem](const DirectX11Shader* shader) { + return shader->UsesStem(stem); + }); + if (!used) + SLEAK_WARN( + "Hot reload: no loaded DirectX 11 shader is built from '{}'", + stem); + } + return true; +} +#endif + bool DirectX11Renderer::CreateBlendState() { //(+) : Binary Operator // Color = SourceP (X) SourceFactor (+) DestP (X) DestFactor @@ -1308,52 +1410,72 @@ bool DirectX11Renderer::CreateTonemapResources() { hr = device->CreateBuffer(&cbDesc, nullptr, &m_postProcessCB); if (FAILED(hr)) return false; - // Compile tonemap shaders - ID3DBlob* vsBlob = nullptr; - ID3DBlob* psBlob = nullptr; - ID3DBlob* errorBlob = nullptr; + if (!CompileTonemapShaders(&m_tonemapVS, &m_tonemapPS)) return false; - // Build path to shader (relative to working directory = bin/) - std::string narrowPath = "assets/shaders/tonemap.hlsl"; - std::wstring shaderPath(narrowPath.begin(), narrowPath.end()); + m_tonemapResourcesCreated = true; + SLEAK_INFO("Tonemapping post-process resources created successfully"); + return true; +} - hr = D3DCompileFromFile(shaderPath.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, +bool DirectX11Renderer::CompileTonemapShaders(ID3D11VertexShader** vs, + ID3D11PixelShader** ps) { + // Path is relative to the working directory (bin/) + const std::string narrowPath = TONEMAP_SHADER_PATH; + const std::wstring shaderPath(narrowPath.begin(), narrowPath.end()); + + Microsoft::WRL::ComPtr vsBlob; + Microsoft::WRL::ComPtr psBlob; + Microsoft::WRL::ComPtr errorBlob; + HRESULT hr = D3DCompileFromFile( + shaderPath.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, "VS_Main", "vs_5_0", D3DCOMPILE_DEBUG, 0, &vsBlob, &errorBlob); if (FAILED(hr)) { - if (errorBlob) { - SLEAK_ERROR("Tonemap VS compile error: {}", - (const char*)errorBlob->GetBufferPointer()); - errorBlob->Release(); - } + LogCompileError(TONEMAP_SHADER_PATH, "VS_Main", hr, errorBlob.Get()); return false; } - - hr = D3DCompileFromFile(shaderPath.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, - "PS_Main", "ps_5_0", D3DCOMPILE_DEBUG, 0, &psBlob, &errorBlob); + hr = D3DCompileFromFile(shaderPath.c_str(), nullptr, + D3D_COMPILE_STANDARD_FILE_INCLUDE, "PS_Main", + "ps_5_0", D3DCOMPILE_DEBUG, 0, &psBlob, &errorBlob); if (FAILED(hr)) { - if (errorBlob) { - SLEAK_ERROR("Tonemap PS compile error: {}", - (const char*)errorBlob->GetBufferPointer()); - errorBlob->Release(); - } - vsBlob->Release(); + LogCompileError(TONEMAP_SHADER_PATH, "PS_Main", hr, errorBlob.Get()); return false; } - hr = device->CreateVertexShader(vsBlob->GetBufferPointer(), - vsBlob->GetBufferSize(), nullptr, &m_tonemapVS); - vsBlob->Release(); - if (FAILED(hr)) { psBlob->Release(); return false; } + Microsoft::WRL::ComPtr vertexShader; + Microsoft::WRL::ComPtr pixelShader; + if (FAILED(device->CreateVertexShader(vsBlob->GetBufferPointer(), + vsBlob->GetBufferSize(), nullptr, + &vertexShader)) || + FAILED(device->CreatePixelShader(psBlob->GetBufferPointer(), + psBlob->GetBufferSize(), nullptr, + &pixelShader))) { + SLEAK_ERROR("Failed to create tonemap shader objects"); + return false; + } + *vs = vertexShader.Detach(); + *ps = pixelShader.Detach(); + return true; +} - hr = device->CreatePixelShader(psBlob->GetBufferPointer(), - psBlob->GetBufferSize(), nullptr, &m_tonemapPS); - psBlob->Release(); - if (FAILED(hr)) return false; +#ifdef SLEAK_HOT_RELOAD +void DirectX11Renderer::ReloadTonemapShaders() { + // Not built yet, the first tonemap pass compiles the new file + if (!m_tonemapResourcesCreated) return; - m_tonemapResourcesCreated = true; - SLEAK_INFO("Tonemapping post-process resources created successfully"); - return true; + ID3D11VertexShader* vs = nullptr; + ID3D11PixelShader* ps = nullptr; + if (!CompileTonemapShaders(&vs, &ps)) { + SLEAK_WARN("Shader '{}' failed to reload, keeping the old one", + TONEMAP_STEM); + return; + } + if (m_tonemapVS) m_tonemapVS->Release(); + if (m_tonemapPS) m_tonemapPS->Release(); + m_tonemapVS = vs; + m_tonemapPS = ps; + SLEAK_INFO("Reloaded DirectX 11 shader '{}'", TONEMAP_STEM); } +#endif void DirectX11Renderer::CleanupTonemapResources() { if (m_tonemapVS) { m_tonemapVS->Release(); m_tonemapVS = nullptr; } diff --git a/src/Graphics/DirectX11/DirectX11Shader.cpp b/src/Graphics/DirectX11/DirectX11Shader.cpp index 2df09543..19a72123 100644 --- a/src/Graphics/DirectX11/DirectX11Shader.cpp +++ b/src/Graphics/DirectX11/DirectX11Shader.cpp @@ -8,53 +8,111 @@ #include #include #include +#include +#ifdef SLEAK_HOT_RELOAD +#include +#endif namespace Sleak { namespace RenderEngine { +namespace { + +constexpr const char* VERTEX_ENTRY = "VS_Main"; +constexpr const char* PIXEL_ENTRY = "PS_Main"; +constexpr const char* SPLIT_FILE_ENTRY = "Main"; +constexpr const char* VERTEX_PROFILE = "vs_5_0"; +constexpr const char* PIXEL_PROFILE = "ps_5_0"; + +#ifdef SLEAK_HOT_RELOAD +/// Returns the file name up to its first '.', e.g. skybox for skybox.hlsl. +std::string StemFromPath(const std::string& path) { + const std::string name = std::filesystem::path(path).filename().string(); + return name.substr(0, name.find('.')); +} + +/// Returns the live shader registry, leaked to survive static teardown. +std::unordered_set& LiveShaders() { + static auto* shaders = new std::unordered_set(); + return *shaders; +} +#endif + +} // namespace + DirectX11Shader::DirectX11Shader(ID3D11Device* device) : m_device(device) { assert(m_device != nullptr); // Ensure the device is valid m_device->GetImmediateContext(m_deviceContext.GetAddressOf()); +#ifdef SLEAK_HOT_RELOAD + LiveShaders().insert(this); +#endif } DirectX11Shader::~DirectX11Shader() { - // Resources are automatically released by ComPtr +#ifdef SLEAK_HOT_RELOAD + LiveShaders().erase(this); +#endif } -bool DirectX11Shader::compile(const std::string& shaderPath) { - // Compile vertex shader - if (!compileShader(shaderPath, "VS_Main", "vs_5_0", m_vertexShader, - m_vertexShaderBlob)) { - SLEAK_ERROR("Failed to compile vertex shader!"); - return false; - } +#ifdef SLEAK_HOT_RELOAD +const std::unordered_set& DirectX11Shader::GetLiveShaders() { + return LiveShaders(); +} - // Compile pixel shader - if (!compileShader(shaderPath, "PS_Main", "ps_5_0", m_pixelShader, - m_pixelShaderBlob)) { - SLEAK_ERROR("Failed to compile pixel shader!"); - return false; - } +bool DirectX11Shader::Reload() { + if (m_sources.vertPath.empty() || m_sources.fragPath.empty()) return false; + return BuildStages(m_sources); +} - return true; +void DirectX11Shader::AdoptStages(const DirectX11Shader& source) { + m_vertexShader = source.m_vertexShader; + m_pixelShader = source.m_pixelShader; + m_vertexShaderBlob = source.m_vertexShaderBlob; + m_pixelShaderBlob = source.m_pixelShaderBlob; + Layout = source.Layout; +} +#endif + +bool DirectX11Shader::compile(const std::string& shaderPath) { + return BuildStages({shaderPath, VERTEX_ENTRY, shaderPath, PIXEL_ENTRY}); } bool DirectX11Shader::compile(const std::string& vertPath, const std::string& fragPath) { - // Compile vertex shader - if (!compileShader(vertPath, "Main", "vs_5_0", m_vertexShader, - m_vertexShaderBlob)) { - SLEAK_ERROR("Failed to compile vertex shader!"); + return BuildStages( + {vertPath, SPLIT_FILE_ENTRY, fragPath, SPLIT_FILE_ENTRY}); +} + +bool DirectX11Shader::BuildStages(StageSources sources) { + Microsoft::WRL::ComPtr vertexShader; + Microsoft::WRL::ComPtr pixelShader; + Microsoft::WRL::ComPtr vertexBlob; + Microsoft::WRL::ComPtr pixelBlob; + if (!compileShader(sources.vertPath, sources.vertEntry, VERTEX_PROFILE, + vertexShader, vertexBlob)) { + SLEAK_ERROR("Failed to compile vertex shader {}", sources.vertPath); return false; } - - // Compile pixel shader - if (!compileShader(fragPath, "Main", "ps_5_0", m_pixelShader, - m_pixelShaderBlob)) { - SLEAK_ERROR("Failed to compile pixel shader!"); + if (!compileShader(sources.fragPath, sources.fragEntry, PIXEL_PROFILE, + pixelShader, pixelBlob)) { + SLEAK_ERROR("Failed to compile pixel shader {}", sources.fragPath); return false; } + // A rebuild needs a layout validated against the new vertex inputs + Microsoft::WRL::ComPtr layout; + if (Layout && !CreateLayoutFor(vertexBlob.Get(), layout)) return false; + + m_vertexShader = std::move(vertexShader); + m_pixelShader = std::move(pixelShader); + m_vertexShaderBlob = std::move(vertexBlob); + m_pixelShaderBlob = std::move(pixelBlob); + if (layout) Layout = std::move(layout); +#ifdef SLEAK_HOT_RELOAD + m_vertStem = StemFromPath(sources.vertPath); + m_fragStem = StemFromPath(sources.fragPath); + m_sources = std::move(sources); +#endif return true; } @@ -74,13 +132,25 @@ ID3DBlob* DirectX11Shader::getVertexShaderBlob() const { } ID3D11InputLayout* DirectX11Shader::createInputLayout() { + Microsoft::WRL::ComPtr layout; + if (!m_vertexShaderBlob || + !CreateLayoutFor(m_vertexShaderBlob.Get(), layout)) + return nullptr; + + // Store per-shader so bind() can set it, and keep a ref for the caller + Layout = layout; + return layout.Detach(); +} + +bool DirectX11Shader::CreateLayoutFor( + ID3DBlob* vertexBlob, Microsoft::WRL::ComPtr& layout) { // Check via reflection if the shader uses BLENDINDICES (skinned shader) bool isSkinned = false; Microsoft::WRL::ComPtr reflector; - HRESULT hr = D3DReflect(m_vertexShaderBlob->GetBufferPointer(), - m_vertexShaderBlob->GetBufferSize(), - IID_ID3D11ShaderReflection, - reinterpret_cast(reflector.GetAddressOf())); + HRESULT hr = + D3DReflect(vertexBlob->GetBufferPointer(), vertexBlob->GetBufferSize(), + IID_ID3D11ShaderReflection, + reinterpret_cast(reflector.GetAddressOf())); if (SUCCEEDED(hr)) { D3D11_SHADER_DESC shaderDesc; reflector->GetDesc(&shaderDesc); @@ -97,22 +167,16 @@ ID3D11InputLayout* DirectX11Shader::createInputLayout() { D3D11_INPUT_ELEMENT_DESC* layoutDesc = isSkinned ? SkinnedLayout : DefaultLayout; UINT layoutCount = isSkinned ? ARRAYSIZE(SkinnedLayout) : ARRAYSIZE(DefaultLayout); - ID3D11InputLayout* inputLayout = nullptr; hr = m_device->CreateInputLayout( - layoutDesc, layoutCount, - m_vertexShaderBlob->GetBufferPointer(), - m_vertexShaderBlob->GetBufferSize(), &inputLayout); + layoutDesc, layoutCount, vertexBlob->GetBufferPointer(), + vertexBlob->GetBufferSize(), layout.ReleaseAndGetAddressOf()); if (FAILED(hr)) { - SLEAK_ERROR("Failed to create input layout: 0x{:08X}", hr); - return nullptr; + SLEAK_ERROR("Failed to create input layout: 0x{:08X}", + static_cast(hr)); + return false; } - - // Store per-shader so bind() can set it - Layout.Attach(inputLayout); - inputLayout->AddRef(); // Keep a ref for the caller too - - return inputLayout; + return true; } /// Compiles a shader stage from an HLSL file and creates the matching D3D11 shader object. @@ -137,8 +201,11 @@ bool DirectX11Shader::compileShader(const std::string& filePath, if (FAILED(hr)) { if (errorBlob) { - SLEAK_ERROR("Shader compilation error: {}", - (char*)errorBlob->GetBufferPointer()); + SLEAK_ERROR("Shader compilation error in {} ({}): {}", filePath, + entryPoint, (char*)errorBlob->GetBufferPointer()); + } else { + SLEAK_ERROR("Shader compilation error in {} ({}): 0x{:08X}", + filePath, entryPoint, static_cast(hr)); } return false; } @@ -154,6 +221,10 @@ bool DirectX11Shader::compileShader(const std::string& filePath, reinterpret_cast(shader.GetAddressOf())); } + if (FAILED(hr)) { + SLEAK_ERROR("Failed to create shader object for {} ({}): 0x{:08X}", + filePath, entryPoint, static_cast(hr)); + } return SUCCEEDED(hr); } diff --git a/src/Graphics/DirectX12/DirectX12Renderer.cpp b/src/Graphics/DirectX12/DirectX12Renderer.cpp index eb900ec1..7c18ed92 100644 --- a/src/Graphics/DirectX12/DirectX12Renderer.cpp +++ b/src/Graphics/DirectX12/DirectX12Renderer.cpp @@ -10,12 +10,15 @@ #include #include #include +#include #include #include #include #include #include #include +#include +#include #include "Graphics/Common/RenderCommandQueue.hpp" @@ -52,21 +55,48 @@ uint64_t HashBytecode(ID3DBlob* blob) { return hash; } -Microsoft::WRL::ComPtr CompileStage(const wchar_t* path, +constexpr const char* VERTEX_ENTRY = "VS_Main"; +constexpr const char* PIXEL_ENTRY = "PS_Main"; +constexpr const char* VERTEX_PROFILE = "vs_5_0"; +constexpr const char* PIXEL_PROFILE = "ps_5_0"; +constexpr const char* SHADOW_SHADER_PATH = + "assets/shaders/default_shader_dx12.hlsl"; +constexpr const char* SKYBOX_SHADER_PATH = "assets/shaders/skybox_dx12.hlsl"; +constexpr const char* DEBUG_LINE_SHADER_PATH = + "assets/shaders/debug_line_dx12.hlsl"; +#ifdef SLEAK_HOT_RELOAD +constexpr const char* SHADOW_SHADER_STEM = "default_shader"; +constexpr const char* SKYBOX_STEM = "skybox"; +constexpr const char* DEBUG_LINE_STEM = "debug_line"; +#endif + +/// Compiles one fixed-pass stage, logging the compiler output on failure. +Microsoft::WRL::ComPtr CompileStage(const char* path, const char* entry, const char* profile) { + const std::wstring widePath(path, path + std::strlen(path)); Microsoft::WRL::ComPtr blob, errorBlob; - HRESULT hr = D3DCompileFromFile(path, nullptr, nullptr, entry, profile, 0, - 0, &blob, &errorBlob); + HRESULT hr = D3DCompileFromFile(widePath.c_str(), nullptr, nullptr, entry, + profile, 0, 0, &blob, &errorBlob); if (FAILED(hr)) { if (errorBlob) { - SLEAK_ERROR("{} ({}) compile error: {}", entry, profile, + SLEAK_ERROR("{} {} ({}) compile error: {}", path, entry, profile, (char*)errorBlob->GetBufferPointer()); + } else { + SLEAK_ERROR("{} {} ({}) compile error: 0x{:08X}", path, entry, + profile, static_cast(hr)); } return nullptr; } return blob; } + +#ifdef SLEAK_HOT_RELOAD +/// True when the reload batch contains the stem. +bool HasStem(const std::vector& stems, const char* stem) { + return std::find(stems.begin(), stems.end(), stem) != stems.end(); +} +#endif } // namespace /// Registers this backend's buffer/shader/texture factories with ResourceManager. @@ -572,13 +602,172 @@ ID3D12PipelineState* DirectX12Renderer::GetPipeline(const PipelineKey& key, return pso.Get(); } +ID3D12PipelineState* DirectX12Renderer::GetShaderPipeline(ID3DBlob* vs, + ID3DBlob* ps) { + PipelineKey key; + key.vsHash = HashBytecode(vs); + key.psHash = HashBytecode(ps); + return GetPipeline(key, vs, ps); +} + +#ifdef SLEAK_HOT_RELOAD +bool DirectX12Renderer::ReloadShaders(const std::vector& stems) { + // Old PSOs are released below, so no frame may still use them + WaitForGPU(); + + std::vector replaced; + ReloadLiveShaders(stems, replaced); + if (HasStem(stems, SHADOW_SHADER_STEM)) ReloadShadowPassPSO(replaced); + if (HasStem(stems, SKYBOX_STEM)) + ReloadPassPipeline(m_skyboxPipelineState, m_skyboxPipelineFailed, + &DirectX12Renderer::CreateSkyboxPipelineState, + SKYBOX_STEM, replaced); + if (HasStem(stems, DEBUG_LINE_STEM)) + ReloadPassPipeline(m_debugLinePipelineState, m_debugLinePipelineFailed, + &DirectX12Renderer::CreateDebugLinePipelineState, + DEBUG_LINE_STEM, replaced); + PruneReplacedPipelines(replaced); + m_shaderPipeline = nullptr; + m_passPipeline = nullptr; + + for (const std::string& stem : stems) { + const bool used = + stem == SHADOW_SHADER_STEM || stem == SKYBOX_STEM || + stem == DEBUG_LINE_STEM || + std::ranges::any_of(DirectX12Shader::GetLiveShaders(), + [&stem](const DirectX12Shader* shader) { + return shader->UsesStem(stem); + }); + if (!used) + SLEAK_WARN( + "Hot reload: no loaded DirectX 12 shader is built from '{}'", + stem); + } + return true; +} + +void DirectX12Renderer::ReloadLiveShaders( + const std::vector& stems, + std::vector& replaced) { + const auto usesAnyStem = [&stems](const DirectX12Shader* shader) { + return std::ranges::any_of(stems, [shader](const std::string& stem) { + return shader->UsesStem(stem); + }); + }; + + // Shaders built from the same files compile once and share the result + std::vector rebuilt; + std::vector failed; + for (DirectX12Shader* shader : DirectX12Shader::GetLiveShaders()) { + if (!usesAnyStem(shader)) continue; + const auto sameSources = [shader](const DirectX12Shader* other) { + return shader->HasSameSources(*other); + }; + if (std::ranges::any_of(failed, sameSources)) continue; + + Microsoft::WRL::ComPtr vs; + Microsoft::WRL::ComPtr ps; + ID3D12PipelineState* pso = nullptr; + const auto twin = std::ranges::find_if(rebuilt, sameSources); + if (twin != rebuilt.end()) { + vs = (*twin)->getVertexShaderBlob(); + ps = (*twin)->getPixelShaderBlob(); + pso = (*twin)->GetPipelineState(); + } else if (shader->RecompileStages(vs, ps)) { + pso = GetShaderPipeline(vs.Get(), ps.Get()); + } + if (!pso) { + failed.push_back(shader); + SLEAK_WARN("Shader '{}' failed to reload, keeping the old pipeline", + shader->GetStem()); + continue; + } + if (twin == rebuilt.end()) { + rebuilt.push_back(shader); + SLEAK_INFO("Reloaded DirectX 12 shader '{}'", shader->GetStem()); + } + + // The first shader also provides the default PSO and shadow fallback + if (m_cachedVSBlob && + m_cachedVSBlob.Get() == shader->getVertexShaderBlob()) { + m_cachedVSBlob = vs; + m_defaultPipelineState = pso; + } + if (ID3D12PipelineState* old = shader->GetPipelineState()) + replaced.push_back(old); + shader->ReplaceStages(std::move(vs), std::move(ps), pso); + } +} + +void DirectX12Renderer::ReloadPassPipeline( + ID3D12PipelineState*& pso, bool& failed, + bool (DirectX12Renderer::*create)(), const char* stem, + std::vector& replaced) { + ID3D12PipelineState* old = std::exchange(pso, nullptr); + failed = false; + // Not built yet, the first pass compiles the new file + if (!old) return; + + if ((this->*create)()) { + replaced.push_back(old); + SLEAK_INFO("Reloaded DirectX 12 pipeline '{}'", stem); + return; + } + pso = old; + failed = false; + SLEAK_WARN("Pipeline '{}' failed to reload, keeping the old one", stem); +} + +void DirectX12Renderer::ReloadShadowPassPSO( + std::vector& replaced) { + // Not built yet, the first shadow pass compiles the new file + if (!m_shadowVSCompiled) return; + + Microsoft::WRL::ComPtr vs = + CompileStage(SHADOW_SHADER_PATH, VERTEX_ENTRY, VERTEX_PROFILE); + if (!vs) { + SLEAK_WARN("Shadow pass shader failed to reload, keeping the old one"); + return; + } + ID3D12PipelineState* oldPSO = std::exchange(m_shadowPassPSO, nullptr); + Microsoft::WRL::ComPtr oldVS = std::exchange(m_shadowVSBlob, vs); + if (CreateShadowPassPSO()) { + if (oldPSO) replaced.push_back(oldPSO); + SLEAK_INFO("Reloaded DirectX 12 pipeline 'shadow pass'"); + return; + } + m_shadowPassPSO = oldPSO; + m_shadowVSBlob = std::move(oldVS); + SLEAK_WARN("Shadow pass PSO failed to rebuild, keeping the old one"); +} + +void DirectX12Renderer::PruneReplacedPipelines( + const std::vector& replaced) { + const auto inUse = [this](ID3D12PipelineState* pso) { + if (pso == m_defaultPipelineState || pso == m_shadowPassPSO || + pso == m_skyboxPipelineState || pso == m_debugLinePipelineState) + return true; + return std::ranges::any_of(DirectX12Shader::GetLiveShaders(), + [pso](const DirectX12Shader* shader) { + return shader->GetPipelineState() == pso; + }); + }; + std::erase_if(m_pipelineCache, [&](const auto& entry) { + ID3D12PipelineState* pso = entry.second.Get(); + return std::find(replaced.begin(), replaced.end(), pso) != + replaced.end() && + !inUse(pso); + }); +} +#endif + bool DirectX12Renderer::CreateShadowPassPSO() { if (m_shadowPassPSO) return true; if (!m_shadowVSCompiled) { m_shadowVSCompiled = true; - m_shadowVSBlob = CompileStage( - L"assets/shaders/default_shader_dx12.hlsl", "VS_Main", "vs_5_0"); + m_shadowVSBlob = + CompileStage(SHADOW_SHADER_PATH, VERTEX_ENTRY, VERTEX_PROFILE); } ID3DBlob* vs = m_shadowVSBlob ? m_shadowVSBlob.Get() : m_cachedVSBlob.Get(); if (!vs) { @@ -1095,10 +1284,7 @@ Shader* DirectX12Renderer::CreateShader(const std::string& shaderSource) { ID3DBlob* vs = shader->getVertexShaderBlob(); ID3DBlob* ps = shader->getPixelShaderBlob(); - PipelineKey key; - key.vsHash = HashBytecode(vs); - key.psHash = HashBytecode(ps); - if (ID3D12PipelineState* pso = GetPipeline(key, vs, ps)) { + if (ID3D12PipelineState* pso = GetShaderPipeline(vs, ps)) { shader->SetPipelineState( Microsoft::WRL::ComPtr(pso)); if (!m_defaultPipelineState) { @@ -1358,10 +1544,8 @@ bool DirectX12Renderer::CreateSkyboxPipelineState() { if (m_skyboxPipelineFailed) return false; m_skyboxPipelineFailed = true; - auto vs = - CompileStage(L"assets/shaders/skybox_dx12.hlsl", "VS_Main", "vs_5_0"); - auto ps = - CompileStage(L"assets/shaders/skybox_dx12.hlsl", "PS_Main", "ps_5_0"); + auto vs = CompileStage(SKYBOX_SHADER_PATH, VERTEX_ENTRY, VERTEX_PROFILE); + auto ps = CompileStage(SKYBOX_SHADER_PATH, PIXEL_ENTRY, PIXEL_PROFILE); if (!vs || !ps) return false; // No culling, depth write off with LEQUAL, and depth clip off so the @@ -1414,10 +1598,9 @@ bool DirectX12Renderer::CreateDebugLinePipelineState() { if (m_debugLinePipelineFailed) return false; m_debugLinePipelineFailed = true; - auto vs = CompileStage(L"assets/shaders/debug_line_dx12.hlsl", "VS_Main", - "vs_5_0"); - auto ps = CompileStage(L"assets/shaders/debug_line_dx12.hlsl", "PS_Main", - "ps_5_0"); + auto vs = + CompileStage(DEBUG_LINE_SHADER_PATH, VERTEX_ENTRY, VERTEX_PROFILE); + auto ps = CompileStage(DEBUG_LINE_SHADER_PATH, PIXEL_ENTRY, PIXEL_PROFILE); if (!vs || !ps) return false; PipelineKey key; diff --git a/src/Graphics/DirectX12/DirectX12Shader.cpp b/src/Graphics/DirectX12/DirectX12Shader.cpp index 05c4a32a..8d00e6b6 100644 --- a/src/Graphics/DirectX12/DirectX12Shader.cpp +++ b/src/Graphics/DirectX12/DirectX12Shader.cpp @@ -5,17 +5,80 @@ #include #include +#include +#ifdef SLEAK_HOT_RELOAD +#include +#include +#endif #include "../../include/private/Graphics/DirectX12/DirectX12Renderer.hpp" namespace Sleak { namespace RenderEngine { +namespace { + +constexpr const char* VERTEX_ENTRY = "VS_Main"; +constexpr const char* PIXEL_ENTRY = "PS_Main"; +constexpr const char* SPLIT_FILE_ENTRY = "Main"; +constexpr const char* VERTEX_PROFILE = "vs_5_0"; +constexpr const char* PIXEL_PROFILE = "ps_5_0"; + +#ifdef SLEAK_HOT_RELOAD +constexpr std::string_view DX12_STEM_SUFFIX = "_dx12"; + +/// Returns the file name up to its first '.', minus a trailing _dx12. +std::string StemFromPath(const std::string& path) { + std::string stem = std::filesystem::path(path).filename().string(); + stem = stem.substr(0, stem.find('.')); + if (stem.ends_with(DX12_STEM_SUFFIX)) { + stem.resize(stem.size() - DX12_STEM_SUFFIX.size()); + } + return stem; +} + +/// Returns the live shader registry, leaked to survive static teardown. +std::unordered_set& LiveShaders() { + static auto* shaders = new std::unordered_set(); + return *shaders; +} +#endif + +} // namespace + DirectX12Shader::DirectX12Shader(ID3D12Device* device) : m_device(device) { assert(m_device != nullptr); +#ifdef SLEAK_HOT_RELOAD + LiveShaders().insert(this); +#endif +} + +DirectX12Shader::~DirectX12Shader() { +#ifdef SLEAK_HOT_RELOAD + LiveShaders().erase(this); +#endif } -DirectX12Shader::~DirectX12Shader() = default; +#ifdef SLEAK_HOT_RELOAD +const std::unordered_set& DirectX12Shader::GetLiveShaders() { + return LiveShaders(); +} + +bool DirectX12Shader::RecompileStages( + Microsoft::WRL::ComPtr& vs, + Microsoft::WRL::ComPtr& ps) const { + if (m_sources.vertPath.empty() || m_sources.fragPath.empty()) return false; + return CompileSources(m_sources, vs, ps); +} + +void DirectX12Shader::ReplaceStages(Microsoft::WRL::ComPtr vs, + Microsoft::WRL::ComPtr ps, + ID3D12PipelineState* pso) { + m_vertexShaderBlob = std::move(vs); + m_pixelShaderBlob = std::move(ps); + m_pipelineState = pso; +} +#endif bool DirectX12Shader::compile(const std::string& shaderPath) { // Strip .hlsl extension if present, then use _dx12.hlsl @@ -26,33 +89,43 @@ bool DirectX12Shader::compile(const std::string& shaderPath) { } dx12Path += "_dx12.hlsl"; - // Compile shaders - if (!compileShader(dx12Path, "VS_Main", "vs_5_0", m_vertexShaderBlob)) { - SLEAK_ERROR("Failed to compile vertex shader!"); - return false; - } + return BuildStages({dx12Path, VERTEX_ENTRY, dx12Path, PIXEL_ENTRY}); +} - if (!compileShader(dx12Path, "PS_Main", "ps_5_0", m_pixelShaderBlob)) { - SLEAK_ERROR("Failed to compile pixel shader!"); - return false; - } +bool DirectX12Shader::compile(const std::string& vertPath, + const std::string& fragPath) { + return BuildStages( + {vertPath, SPLIT_FILE_ENTRY, fragPath, SPLIT_FILE_ENTRY}); +} +bool DirectX12Shader::BuildStages(StageSources sources) { + Microsoft::WRL::ComPtr vs; + Microsoft::WRL::ComPtr ps; + if (!CompileSources(sources, vs, ps)) return false; + + m_vertexShaderBlob = std::move(vs); + m_pixelShaderBlob = std::move(ps); +#ifdef SLEAK_HOT_RELOAD + m_vertStem = StemFromPath(sources.vertPath); + m_fragStem = StemFromPath(sources.fragPath); + m_sources = std::move(sources); +#endif return true; } -bool DirectX12Shader::compile(const std::string& vertPath, - const std::string& fragPath) { - // Compile shaders - if (!compileShader(vertPath, "Main", "vs_5_0", m_vertexShaderBlob)) { - SLEAK_ERROR("Failed to compile vertex shader!"); +bool DirectX12Shader::CompileSources( + const StageSources& sources, Microsoft::WRL::ComPtr& vs, + Microsoft::WRL::ComPtr& ps) const { + if (!compileShader(sources.vertPath, sources.vertEntry, VERTEX_PROFILE, + vs)) { + SLEAK_ERROR("Failed to compile vertex shader {}", sources.vertPath); return false; } - - if (!compileShader(fragPath, "Main", "ps_5_0", m_pixelShaderBlob)) { - SLEAK_ERROR("Failed to compile pixel shader!"); + if (!compileShader(sources.fragPath, sources.fragEntry, PIXEL_PROFILE, + ps)) { + SLEAK_ERROR("Failed to compile pixel shader {}", sources.fragPath); return false; } - return true; } @@ -89,10 +162,9 @@ void DirectX12Shader::SetRenderer(DirectX12Renderer* renderer) { m_renderer = renderer; } -bool DirectX12Shader::compileShader(const std::string& filePath, - const std::string& entryPoint, - const std::string& profile, - Microsoft::WRL::ComPtr& blob) { +bool DirectX12Shader::compileShader( + const std::string& filePath, const std::string& entryPoint, + const std::string& profile, Microsoft::WRL::ComPtr& blob) const { Microsoft::WRL::ComPtr errorBlob; HRESULT hr = D3DCompileFromFile( std::wstring(filePath.begin(), filePath.end()).c_str(), @@ -107,7 +179,11 @@ bool DirectX12Shader::compileShader(const std::string& filePath, if (FAILED(hr)) { if (errorBlob) { - SLEAK_ERROR("Shader error: {}", (char*)errorBlob->GetBufferPointer()); + SLEAK_ERROR("Shader error in {} ({}): {}", filePath, entryPoint, + (char*)errorBlob->GetBufferPointer()); + } else { + SLEAK_ERROR("Shader error in {} ({}): 0x{:08X}", filePath, + entryPoint, static_cast(hr)); } return false; } diff --git a/src/Graphics/OpenGL/OpenGLRenderer.cpp b/src/Graphics/OpenGL/OpenGLRenderer.cpp index 9f9dfdac..225ca347 100644 --- a/src/Graphics/OpenGL/OpenGLRenderer.cpp +++ b/src/Graphics/OpenGL/OpenGLRenderer.cpp @@ -1,25 +1,29 @@ #include "../../include/private/Graphics/OpenGL/OpenGLRenderer.hpp" -#include "../../include/private/Graphics/OpenGL/OpenGLBuffer.hpp" -#include "../../include/private/Graphics/OpenGL/OpenGLShader.hpp" -#include "../../include/private/Graphics/OpenGL/OpenGLTexture.hpp" -#include "../../include/private/Graphics/OpenGL/OpenGLCubemapTexture.hpp" -#include "../../include/private/Graphics/OpenGL/OpenGLIBL.hpp" -#include -#include -#include "Graphics/Common/ResourceManager.hpp" -#include "Graphics/Common/ConstantBuffer.hpp" -#include "Graphics/Common/RenderCommandQueue.hpp" -#include "Graphics/Common/SSAOKernel.hpp" + +#include + #include #include +#include #include -#include #include -#include -#include -#include +#include +#include +#include +#include #include #include +#include + +#include "../../include/private/Graphics/OpenGL/OpenGLBuffer.hpp" +#include "../../include/private/Graphics/OpenGL/OpenGLCubemapTexture.hpp" +#include "../../include/private/Graphics/OpenGL/OpenGLIBL.hpp" +#include "../../include/private/Graphics/OpenGL/OpenGLShader.hpp" +#include "../../include/private/Graphics/OpenGL/OpenGLTexture.hpp" +#include "Graphics/Common/ConstantBuffer.hpp" +#include "Graphics/Common/RenderCommandQueue.hpp" +#include "Graphics/Common/ResourceManager.hpp" +#include "Graphics/Common/SSAOKernel.hpp" namespace Sleak { namespace RenderEngine { @@ -117,6 +121,7 @@ void OpenGLRenderer::SetupVertexLayout() { } void OpenGLRenderer::BeginRender() { + m_sceneReadPrepared = false; if (m_vsyncChangeRequested) ApplyVSyncChange(); if (m_msaaChangeRequested) @@ -189,12 +194,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) { @@ -215,6 +222,35 @@ void OpenGLRenderer::Cleanup() { } } +#ifdef SLEAK_HOT_RELOAD +bool OpenGLRenderer::ReloadShaders(const std::vector& stems) { + for (OpenGLShader* shader : OpenGLShader::GetLiveShaders()) { + const bool changed = + std::ranges::any_of(stems, [shader](const std::string& stem) { + return shader->UsesStem(stem); + }); + if (!changed) continue; + if (shader->Reload()) { + SLEAK_INFO("Reloaded OpenGL shader '{}'", shader->GetStem()); + } else { + SLEAK_WARN("Shader '{}' failed to reload, keeping the old program", + shader->GetStem()); + } + } + for (const std::string& stem : stems) { + const bool used = + std::ranges::any_of(OpenGLShader::GetLiveShaders(), + [&stem](const OpenGLShader* shader) { + return shader->UsesStem(stem); + }); + if (!used) + SLEAK_WARN("Hot reload: no loaded OpenGL shader is built from '{}'", + stem); + } + return true; +} +#endif + void OpenGLRenderer::Resize(uint32_t width, uint32_t height) { glViewport(0, 0, width, height); // Recreate MSAA FBO at new size @@ -369,6 +405,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 +728,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 +823,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 +978,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 +1080,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/OpenGL/OpenGLShader.cpp b/src/Graphics/OpenGL/OpenGLShader.cpp index 65984ac4..0d0c55d6 100644 --- a/src/Graphics/OpenGL/OpenGLShader.cpp +++ b/src/Graphics/OpenGL/OpenGLShader.cpp @@ -1,20 +1,76 @@ #include "../../include/private/Graphics/OpenGL/OpenGLShader.hpp" + #include +#include #include #include +#include namespace Sleak { namespace RenderEngine { -OpenGLShader::OpenGLShader() {} +namespace { + +constexpr std::string_view GL_STEM_SUFFIX = "_gl"; + +/// Returns the file name up to its first '.', minus a trailing _gl. +std::string StemFromPath(const std::string& path) { + std::string stem = std::filesystem::path(path).filename().string(); + stem = stem.substr(0, stem.find('.')); + if (stem.ends_with(GL_STEM_SUFFIX)) { + stem.resize(stem.size() - GL_STEM_SUFFIX.size()); + } + return stem; +} + +/// Returns the info log of a shader or program object. +std::string ReadInfoLog(GLuint object, PFNGLGETSHADERIVPROC getParam, + PFNGLGETSHADERINFOLOGPROC getLog) { + GLint length = 0; + getParam(object, GL_INFO_LOG_LENGTH, &length); + if (length <= 0) return {}; + std::string log(static_cast(length), '\0'); + GLsizei written = 0; + getLog(object, length, &written, log.data()); + log.resize(static_cast(written)); + while (!log.empty() && (log.back() == '\n' || log.back() == '\r')) { + log.pop_back(); + } + return log; +} + +#ifdef SLEAK_HOT_RELOAD +/// Returns the live shader registry, leaked to survive static teardown. +std::unordered_set& LiveShaders() { + static auto* shaders = new std::unordered_set(); + return *shaders; +} +#endif + +} // namespace + +OpenGLShader::OpenGLShader() { +#ifdef SLEAK_HOT_RELOAD + LiveShaders().insert(this); +#endif +} OpenGLShader::~OpenGLShader() { +#ifdef SLEAK_HOT_RELOAD + LiveShaders().erase(this); +#endif if (m_program != 0) { glDeleteProgram(m_program); m_program = 0; } } +#ifdef SLEAK_HOT_RELOAD +const std::unordered_set& OpenGLShader::GetLiveShaders() { + return LiveShaders(); +} +#endif + bool OpenGLShader::compile(const std::string& shaderPath) { // Strip .hlsl extension if present, then use _gl.vert/_gl.frag std::string basePath = shaderPath; @@ -27,46 +83,61 @@ bool OpenGLShader::compile(const std::string& shaderPath) { bool OpenGLShader::compile(const std::string& vertPath, const std::string& fragPath) { - std::string vertSource = ReadFile(vertPath); - std::string fragSource = ReadFile(fragPath); + m_vertPath = vertPath; + m_fragPath = fragPath; + m_vertStem = StemFromPath(vertPath); + m_fragStem = StemFromPath(fragPath); + return BuildProgram(); +} - if (vertSource.empty() || fragSource.empty()) { - SLEAK_ERROR("Failed to read shader files: {} and {}", vertPath, - fragPath); - return false; - } +bool OpenGLShader::Reload() { + if (m_vertPath.empty() || m_fragPath.empty()) return false; + return BuildProgram(); +} - GLuint vertShader = 0, fragShader = 0; +bool OpenGLShader::BuildProgram() { + const std::string vertSource = ReadFile(m_vertPath); + const std::string fragSource = ReadFile(m_fragPath); - if (!CompileShaderSource(vertSource, GL_VERTEX_SHADER, vertShader)) { + if (vertSource.empty() || fragSource.empty()) { + SLEAK_ERROR("Failed to read shader files: {} and {}", m_vertPath, + m_fragPath); return false; } - if (!CompileShaderSource(fragSource, GL_FRAGMENT_SHADER, fragShader)) { - glDeleteShader(vertShader); + + GLuint vertShader = 0; + GLuint fragShader = 0; + if (!CompileShaderSource(vertSource, GL_VERTEX_SHADER, m_vertPath, + vertShader)) { return false; } - - m_program = glCreateProgram(); - glAttachShader(m_program, vertShader); - glAttachShader(m_program, fragShader); - glLinkProgram(m_program); - - GLint success = 0; - glGetProgramiv(m_program, GL_LINK_STATUS, &success); - if (!success) { - char infoLog[512]; - glGetProgramInfoLog(m_program, 512, nullptr, infoLog); - SLEAK_ERROR("Shader program linking failed: {}", infoLog); - glDeleteProgram(m_program); - m_program = 0; + if (!CompileShaderSource(fragSource, GL_FRAGMENT_SHADER, m_fragPath, + fragShader)) { glDeleteShader(vertShader); - glDeleteShader(fragShader); return false; } + GLuint program = glCreateProgram(); + glAttachShader(program, vertShader); + glAttachShader(program, fragShader); + glLinkProgram(program); + glDetachShader(program, vertShader); + glDetachShader(program, fragShader); glDeleteShader(vertShader); glDeleteShader(fragShader); + GLint linked = GL_FALSE; + glGetProgramiv(program, GL_LINK_STATUS, &linked); + if (linked != GL_TRUE) { + SLEAK_ERROR("Shader program linking failed ({} + {}): {}", m_vertPath, + m_fragPath, + ReadInfoLog(program, glGetProgramiv, glGetProgramInfoLog)); + glDeleteProgram(program); + return false; + } + + if (m_program != 0) glDeleteProgram(m_program); + m_program = program; return true; } @@ -76,19 +147,19 @@ void OpenGLShader::bind() { } } -bool OpenGLShader::CompileShaderSource(const std::string& source, - GLenum type, GLuint& shader) { +bool OpenGLShader::CompileShaderSource(const std::string& source, GLenum type, + const std::string& path, + GLuint& shader) { shader = glCreateShader(type); const char* src = source.c_str(); glShaderSource(shader, 1, &src, nullptr); glCompileShader(shader); - GLint success = 0; + GLint success = GL_FALSE; glGetShaderiv(shader, GL_COMPILE_STATUS, &success); - if (!success) { - char infoLog[512]; - glGetShaderInfoLog(shader, 512, nullptr, infoLog); - SLEAK_ERROR("Shader compilation failed: {}", infoLog); + if (success != GL_TRUE) { + SLEAK_ERROR("Shader compilation failed ({}): {}", path, + ReadInfoLog(shader, glGetShaderiv, glGetShaderInfoLog)); glDeleteShader(shader); shader = 0; return false; 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..59f64313 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; + + if (vkCreatePipelineLayout(device, &layoutInfo, nullptr, &pipelineLay) != + VK_SUCCESS) + SLEAK_RETURN_ERR("Failed to create graphics pipeline layout!"); - simpleShader = new VulkanShader(device); - bool isShader = - simpleShader->compile("assets/shaders/default_shader"); + return true; +} - if (!isShader) - SLEAK_RETURN_ERR("Cannot compile shaders!") +/// 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; @@ -1490,5 +1446,147 @@ void VulkanRenderer::EndSkinnedPass() { vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); } +#ifdef SLEAK_HOT_RELOAD +/// Rebuilds every pipeline from the SPIR-V on disk; this version ignores the +/// stems and rebuilds them all. +bool VulkanRenderer::ReloadShaders(const std::vector& stems) { + (void)stems; + vkDeviceWaitIdle(device); + + const PipelineGroup groups[] = { + {"default_shader", + {&pipeline}, + {}, + {&simpleShader}, + &VulkanRenderer::CreateGraphicsPipeline}, + {"skybox", + {&skyboxPipeline}, + {}, + {&skyboxShader}, + &VulkanRenderer::CreateSkyboxPipeline}, + {"debug_line", + {&debugLinePipeline}, + {}, + {&debugLineShader}, + &VulkanRenderer::CreateDebugLinePipeline}, + {"skinned_shader", + {&skinnedPipeline}, + {}, + {&skinnedShader}, + &VulkanRenderer::CreateSkinnedPipeline}, + {"shadow_depth", + {&m_shadowPipeline}, + {}, + {&m_shadowShader}, + &VulkanRenderer::CreateShadowPipeline}, + {"gbuffer", + {&m_gbufferPipeline}, + {}, + {&m_gbufferShader}, + &VulkanRenderer::CreateGBufferPipeline}, + {"skinned gbuffer", + {&m_skinnedGbufferPipeline}, + {}, + {}, + &VulkanRenderer::CreateSkinnedGbufferPipeline}, + {"lighting_pass", + {&m_lightingPipeline}, + {&m_lightingPipelineLayout}, + {&m_lightingShader}, + &VulkanRenderer::CreateLightingPipeline}, + {"ssao", + {&m_ssaoPipeline, &m_ssaoBlurPipeline}, + {&m_ssaoPipelineLayout, &m_ssaoBlurPipelineLayout}, + {&m_ssaoShader, &m_ssaoBlurShader}, + &VulkanRenderer::CreateSSAOPipelines}, + {"ssr", + {&m_ssrPipeline}, + {}, + {&m_ssrShader}, + &VulkanRenderer::CreateSSRPipeline}, + {"taa", + {&m_taaPipeline}, + {}, + {&m_taaShader}, + &VulkanRenderer::CreateTAAPipeline}, + {"bloom", + {&m_bloomThresholdPipeline, &m_bloomDownsamplePipeline, + &m_bloomUpsamplePipeline, &m_bloomCompositePipeline}, + {&m_bloomFilterPipelineLayout, &m_bloomCompositePipelineLayout}, + {&m_bloomThresholdShader, &m_bloomDownsampleShader, + &m_bloomUpsampleShader, &m_bloomCompositeShader}, + &VulkanRenderer::CreateBloomPipelines}, + }; + for (const PipelineGroup& group : groups) ReloadPipelineGroup(group); + ReloadCustomFormatPipelines(); + return true; +} + +/// Rebuilds a live group through its create function, keeping the old +/// objects when the rebuild fails. +void VulkanRenderer::ReloadPipelineGroup(const PipelineGroup& group) { + // Pass is off or still builds lazily from disk + if (*group.pipelines.front() == VK_NULL_HANDLE) return; + + // Detach the live objects so the create function builds new ones + std::vector pipelines; + std::vector layouts; + std::vector shaders; + for (VkPipeline* pipe : group.pipelines) + pipelines.push_back(std::exchange(*pipe, VK_NULL_HANDLE)); + for (VkPipelineLayout* layout : group.layouts) + layouts.push_back(std::exchange(*layout, VK_NULL_HANDLE)); + for (VulkanShader** shader : group.shaders) + shaders.push_back(std::exchange(*shader, nullptr)); + + bool built = (this->*group.create)(); + for (VkPipeline* pipe : group.pipelines) + built = built && *pipe != VK_NULL_HANDLE; + if (!built) + SLEAK_ERROR("Hot reload: {} rebuild failed, keeping the previous one", + group.name); + + // Destroy the losing generation + for (size_t i = 0; i < pipelines.size(); ++i) { + if (!built) std::swap(*group.pipelines[i], pipelines[i]); + if (pipelines[i]) vkDestroyPipeline(device, pipelines[i], nullptr); + } + for (size_t i = 0; i < layouts.size(); ++i) { + if (!built) std::swap(*group.layouts[i], layouts[i]); + if (layouts[i]) vkDestroyPipelineLayout(device, layouts[i], nullptr); + } + for (size_t i = 0; i < shaders.size(); ++i) { + if (!built) std::swap(*group.shaders[i], shaders[i]); + delete shaders[i]; + } +} + +/// Rebuilds every cached custom-format variant, keeping the old pipeline of +/// a variant that fails. +void VulkanRenderer::ReloadCustomFormatPipelines() { + for (size_t i = 0; i < m_customFormatPipelines.size(); ++i) { + const auto format = static_cast(i + 1); + CustomFormatPipelines& pipes = m_customFormatPipelines[i]; + // Variants that failed before retry on next use + pipes.failed.fill(false); + const VertexLayoutDesc* desc = VertexFormatRegistry::Get(format); + if (!desc) continue; + for (size_t slot = 0; slot < CUSTOM_PASS_COUNT; ++slot) { + VkPipeline& live = pipes.pipelines[slot]; + if (live == VK_NULL_HANDLE) continue; + const VkPipeline rebuilt = BuildCustomFormatPipeline( + format, *desc, static_cast(slot)); + if (rebuilt == VK_NULL_HANDLE) { + SLEAK_ERROR( + "Hot reload: vertex format {} keeps its previous pipeline", + format); + continue; + } + vkDestroyPipeline(device, live, nullptr); + live = rebuilt; + } + } +} +#endif } } 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..b55e45a7 100644 --- a/src/Graphics/Vulkan/VulkanSSR.cpp +++ b/src/Graphics/Vulkan/VulkanSSR.cpp @@ -280,6 +280,16 @@ bool VulkanRenderer::CreateSSRResources() { } // ---- 8. Pipeline ---- + if (!CreateSSRPipeline()) return false; + + m_ssrResourcesCreated = true; + SLEAK_INFO("SSR resources created ({}x{})", scExtent.width, + scExtent.height); + return true; +} + +/// Compiles the SSR shaders and creates the SSR pipeline. +bool VulkanRenderer::CreateSSRPipeline() { { m_ssrShader = new VulkanShader(device); if (!m_ssrShader->compile("assets/shaders/ssr.vert.spv", @@ -350,14 +360,12 @@ 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; } } - - m_ssrResourcesCreated = true; - SLEAK_INFO("SSR resources created ({}x{})", scExtent.width, scExtent.height); return true; } @@ -426,7 +434,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 +554,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 +576,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 +586,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..ea402d92 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; } @@ -282,6 +288,16 @@ bool VulkanRenderer::CreateTAAResources() { } // ---- 10. Pipeline ---- + if (!CreateTAAPipeline()) return false; + + m_taaResourcesCreated = true; + SLEAK_INFO("TAA resources created ({}x{})", scExtent.width, + scExtent.height); + return true; +} + +/// Compiles the TAA shaders and creates the TAA resolve pipeline. +bool VulkanRenderer::CreateTAAPipeline() { { m_taaShader = new VulkanShader(device); if (!m_taaShader->compile("assets/shaders/taa.vert.spv", @@ -346,15 +362,12 @@ 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; } } - - m_taaResourcesCreated = true; - SLEAK_INFO("TAA resources created ({}x{})", scExtent.width, scExtent.height); return true; } @@ -391,29 +404,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 +452,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 +556,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