diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 00faa26e8c..e7fd540276 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -107,7 +107,7 @@ jobs: (github.event_name == 'pull_request_target' && !github.event.pull_request.draft)) name: Build plugin and addons - runs-on: windows-latest + runs-on: windows-2022 permissions: contents: read outputs: @@ -120,7 +120,71 @@ jobs: repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} submodules: recursive - - uses: ilammy/msvc-dev-cmd@v1.10.0 + - name: Setup MSVC environment (VS2022) + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + + - name: Get MSVC version + id: msvc_version + shell: pwsh + run: | + # Create a dummy source file for cl.exe /Bv + "int main() { return 0; }" | Out-File -FilePath "dummy.cpp" -Encoding ASCII + + $output = & cl.exe /Bv dummy.cpp 2>&1 + Write-Host "Raw cl.exe output:" + Write-Host $output + + # More robust version extraction + $version = $null + + # Split output into lines and search for version patterns + $lines = $output -split "`n|`r" + Write-Host "Number of lines: $($lines.Count)" + + foreach ($line in $lines) { + Write-Host "Processing line: '$line'" + + # Try different patterns on each line + if ($line -match 'Version ([0-9]+\.[0-9]+\.[0-9]+)') { + $version = $Matches[1] + Write-Host "Found version in line: $version" + break + } + elseif ($line -match '([0-9]+\.[0-9]+\.[0-9]+)') { + $version = $Matches[1] + Write-Host "Found generic version in line: $version" + break + } + } + + # If still no version, try the whole output as a single string + if (-not $version) { + Write-Host "Trying whole output as single string..." + if ($output -match 'Version ([0-9]+\.[0-9]+\.[0-9]+)') { + $version = $Matches[1] + Write-Host "Found version in whole output: $version" + } + elseif ($output -match '([0-9]+\.[0-9]+\.[0-9]+)') { + $version = $Matches[1] + Write-Host "Found generic version in whole output: $version" + } + } + + Write-Host "Final version result: $version" + + if ($version) { + Write-Host "MSVC version: $version" + Add-Content -Path $env:GITHUB_OUTPUT -Value "version=$version" -Encoding UTF8 + } else { + Write-Host "Failed to extract MSVC version from output:" + Write-Host $output + throw "MSVC version not found in output" + } + + # Clean up dummy file + Remove-Item "dummy.cpp" -ErrorAction SilentlyContinue - name: Setup vcpkg uses: lukka/run-vcpkg@v11.5 @@ -131,10 +195,28 @@ jobs: uses: actions/cache@v4 with: path: build/ALL - key: ${{ runner.os }}-cmake-${{ github.event.inputs.cache-key-suffix || 'default' }}-${{ hashFiles('.gitmodules', 'extern/**', 'CMakePresets.json', 'vcpkg.json', 'vcpkg-configuration.json') }} + key: ${{ runner.os }}-cmake-msvc-${{ steps.msvc_version.outputs.version }}-${{ github.event.inputs.cache-key-suffix || 'default' }}-${{ hashFiles('.gitmodules', 'extern/**', 'CMakePresets.json', 'vcpkg.json', 'vcpkg-configuration.json') }} restore-keys: | - ${{ runner.os }}-cmake-${{ github.event.inputs.cache-key-suffix || 'default' }}- - ${{ runner.os }}-cmake- + ${{ runner.os }}-cmake-msvc-${{ steps.msvc_version.outputs.version }}-${{ github.event.inputs.cache-key-suffix || 'default' }}- + ${{ runner.os }}-cmake-msvc-${{ steps.msvc_version.outputs.version }}- + + - name: Remove stale CMake cache if drive letter changed + shell: pwsh + run: | + $cacheFile = "build/ALL/CMakeCache.txt" + if (Test-Path $cacheFile) { + $expected = (Resolve-Path .).Path.Substring(0,1) # C or D + $content = Get-Content $cacheFile -Raw + if ($content -match 'CMAKE_HOME_DIRECTORY:INTERNAL=([A-Z]):') { + $actual = $Matches[1] + if ($actual -ne $expected) { + Write-Host "❌ Cache drive mismatch. Removing stale build/ALL directory." + Remove-Item -Recurse -Force "build/ALL" + } else { + Write-Host "✅ Cache path matches. Keeping build/ALL." + } + } + } - name: Build using run-cmake uses: lukka/run-cmake@v10 @@ -175,7 +257,7 @@ jobs: (github.event_name == 'workflow_dispatch' && github.event.inputs.validate-shaders == 'true')) name: Validate shader compilation - runs-on: windows-latest + runs-on: windows-2022 permissions: contents: read strategy: @@ -207,9 +289,71 @@ jobs: repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} submodules: recursive - - uses: ilammy/msvc-dev-cmd@v1.10.0 + - uses: ilammy/msvc-dev-cmd@v1 if: steps.check-hlsl.outputs.skip != 'true' + - name: Get MSVC version + if: steps.check-hlsl.outputs.skip != 'true' + id: msvc_version + shell: pwsh + run: | + # Create a dummy source file for cl.exe /Bv + "int main() { return 0; }" | Out-File -FilePath "dummy.cpp" -Encoding ASCII + + $output = & cl.exe /Bv dummy.cpp 2>&1 + Write-Host "Raw cl.exe output:" + Write-Host $output + + # More robust version extraction + $version = $null + + # Split output into lines and search for version patterns + $lines = $output -split "`n|`r" + Write-Host "Number of lines: $($lines.Count)" + + foreach ($line in $lines) { + Write-Host "Processing line: '$line'" + + # Try different patterns on each line + if ($line -match 'Version ([0-9]+\.[0-9]+\.[0-9]+)') { + $version = $Matches[1] + Write-Host "Found version in line: $version" + break + } + elseif ($line -match '([0-9]+\.[0-9]+\.[0-9]+)') { + $version = $Matches[1] + Write-Host "Found generic version in line: $version" + break + } + } + + # If still no version, try the whole output as a single string + if (-not $version) { + Write-Host "Trying whole output as single string..." + if ($output -match 'Version ([0-9]+\.[0-9]+\.[0-9]+)') { + $version = $Matches[1] + Write-Host "Found version in whole output: $version" + } + elseif ($output -match '([0-9]+\.[0-9]+\.[0-9]+)') { + $version = $Matches[1] + Write-Host "Found generic version in whole output: $version" + } + } + + Write-Host "Final version result: $version" + + if ($version) { + Write-Host "MSVC version: $version" + Add-Content -Path $env:GITHUB_OUTPUT -Value "version=$version" -Encoding UTF8 + } else { + Write-Host "Failed to extract MSVC version from output:" + Write-Host $output + throw "MSVC version not found in output" + } + + # Clean up dummy file + Remove-Item "dummy.cpp" -ErrorAction SilentlyContinue + - name: Setup vcpkg if: steps.check-hlsl.outputs.skip != 'true' uses: lukka/run-vcpkg@v11.5 @@ -221,11 +365,29 @@ jobs: uses: actions/cache@v4 with: path: build/ALL - key: ${{ runner.os }}-cmake-${{ matrix.config.name }}-${{ github.event.inputs.cache-key-suffix || 'default' }}-${{ hashFiles('.gitmodules', 'extern/**', 'CMakePresets.json', 'vcpkg.json', 'vcpkg-configuration.json') }} + key: ${{ runner.os }}-cmake-msvc-${{ steps.msvc_version.outputs.version }}-${{ matrix.config.name }}-${{ github.event.inputs.cache-key-suffix || 'default' }}-${{ hashFiles('.gitmodules', 'extern/**', 'CMakePresets.json', 'vcpkg.json', 'vcpkg-configuration.json') }} restore-keys: | - ${{ runner.os }}-cmake-${{ matrix.config.name }}-${{ github.event.inputs.cache-key-suffix || 'default' }}- - ${{ runner.os }}-cmake-${{ matrix.config.name }}- - ${{ runner.os }}-cmake- + ${{ runner.os }}-cmake-msvc-${{ steps.msvc_version.outputs.version }}-${{ matrix.config.name }}-${{ github.event.inputs.cache-key-suffix || 'default' }}- + ${{ runner.os }}-cmake-msvc-${{ steps.msvc_version.outputs.version }}-${{ matrix.config.name }}- + + - name: Remove stale CMake cache if drive letter changed + if: steps.check-hlsl.outputs.skip != 'true' + shell: pwsh + run: | + $cacheFile = "build/ALL/CMakeCache.txt" + if (Test-Path $cacheFile) { + $expected = (Resolve-Path .).Path.Substring(0,1) # C or D + $content = Get-Content $cacheFile -Raw + if ($content -match 'CMAKE_HOME_DIRECTORY:INTERNAL=([A-Z]):') { + $actual = $Matches[1] + if ($actual -ne $expected) { + Write-Host "❌ Cache drive mismatch. Removing stale build/ALL directory." + Remove-Item -Recurse -Force "build/ALL" + } else { + Write-Host "✅ Cache path matches. Keeping build/ALL." + } + } + } - name: Prepare shaders for validation if: steps.check-hlsl.outputs.skip != 'true' @@ -266,7 +428,7 @@ jobs: name: Post Prerelease from PR if: github.event_name == 'pull_request_target' needs: [cpp-build, shader-validation] - runs-on: windows-latest + runs-on: windows-2022 permissions: contents: write pull-requests: write @@ -358,7 +520,7 @@ jobs: if: > github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') - runs-on: windows-latest + runs-on: windows-2022 permissions: contents: write steps: diff --git a/package/Shaders/Common/BRDF.hlsli b/package/Shaders/Common/BRDF.hlsli new file mode 100644 index 0000000000..8f1faf4489 --- /dev/null +++ b/package/Shaders/Common/BRDF.hlsli @@ -0,0 +1,216 @@ +#ifndef __BRDF_DEPENDENCY_HLSL__ +#define __BRDF_DEPENDENCY_HLSL__ + +#include "Common/Math.hlsli" + +namespace BRDF +{ + // N = Normal of the macro surface + // H = Normal of the micro surface (halfway vector between L and V) + // L = Light direction from surface point to light + // V = View direction from surface point to camera + + // D = Distribution (Microfacet NDF) + // F = Fresnel + // Vis = Visibility (Self-shadowing and masking) + // Specular BRDF = D * Vis * F + + // Diffuse BRDFs + float Diffuse_Lambert() + { + return 1.0 / Math::PI; + } + + // [Burley 2012, "Physically-Based Shading at Disney"] + float3 Diffuse_Burley(float roughness, float NdotV, float NdotL, float VdotH) + { + float FD90 = 0.5 + 2.0 * VdotH * VdotH * roughness; + float FdV = 1.0 + (FD90 - 1.0) * pow(1.0 - NdotV, 5.0); + float FdL = 1.0 + (FD90 - 1.0) * pow(1.0 - NdotL, 5.0); + return (1.0 / Math::PI) * (FdV * FdL); + } + + // [Gotanda 2012, "Beyond a Simple Physically Based Blinn-Phong Model in Real-Time"] + float3 Diffuse_OrenNayar(float roughness, float3 N, float3 V, float3 L, float NdotV, float NdotL) + { + float a = roughness * roughness * 0.25; + float A = 1.0 - 0.5 * (a / (a + 0.33)); + float B = 0.45 * (a / (a + 0.09)); + + float gamma = dot(V - N * NdotV, L - N * NdotL) / (sqrt(saturate(1.0 - NdotV * NdotV)) * sqrt(saturate(1.0 - NdotL * NdotL))); + + float2 cos_alpha_beta = NdotV < NdotL ? float2(NdotV, NdotL) : float2(NdotL, NdotV); + float2 sin_alpha_beta = sqrt(saturate(1.0 - cos_alpha_beta * cos_alpha_beta)); + float C = sin_alpha_beta.x * sin_alpha_beta.y / (1e-6 + cos_alpha_beta.y); + + return (1 / Math::PI) * (A + B * max(0.0, gamma) * C); + } + + // [Gotanda 2014, "Designing Reflectance Models for New Consoles"] + float3 Diffuse_Gotanda(float roughness, float NdotV, float NdotL, float VdotL) + { + float a = roughness * roughness; + float a2 = a * a; + float F0 = 0.04; + float Cosri = VdotL - NdotV * NdotL; + float Fr = (1 - (0.542026 * a2 + 0.303573 * a) / (a2 + 1.36053)) * (1 - pow(1 - NdotV, 5 - 4 * a2) / (a2 + 1.36053)) * ((-0.733996 * a2 * a + 1.50912 * a2 - 1.16402 * a) * pow(1 - NdotV, 1 + rcp(39 * a2 * a2 + 1)) + 1); + float Lm = (max(1 - 2 * a, 0) * (1 - pow(1 - NdotL, 5)) + min(2 * a, 1)) * (1 - 0.5 * a * (NdotL - 1)) * NdotL; + float Vd = (a2 / ((a2 + 0.09) * (1.31072 + 0.995584 * NdotV))) * (1 - pow(1 - NdotL, (1 - 0.3726732 * NdotV * NdotV) / (0.188566 + 0.38841 * NdotV))); + float Bp = Cosri < 0 ? 1.4 * NdotV * NdotL * Cosri : Cosri; + float Lr = (21.0 / 20.0) * (1 - F0) * (Fr * Lm + Vd + Bp); + return (1 / Math::PI) * Lr; + } + + // [ Chan 2018, "Material Advances in Call of Duty: WWII" ] + float3 Diffuse_Chan(float roughness, float NdotV, float NdotL, float VdotH, float NdotH) + { + float a = roughness * roughness; + float a2 = a * a; + float g = saturate((1.0 / 18.0) * log2(2 * rcp(a2) - 1)); + + float F0 = VdotH + pow(1 - VdotH, 5); + float FdV = 1 - 0.75 * pow(1 - NdotV, 5); + float FdL = 1 - 0.75 * pow(1 - NdotL, 5); + + float Fd = lerp(F0, FdV * FdL, saturate(2.2 * g - 0.5)); + + float Fb = ((34.5 * g - 59) * g + 24.5) * VdotH * exp2(-max(73.2 * g - 21.2, 8.9) * sqrt(NdotH)); + + return (1 / Math::PI) * (Fd + Fb); + } + + // Specular BRDFs + // [Schlick 1994, "An Inexpensive BRDF Model for Physically-Based Rendering"] + float3 F_Schlick(float3 specularColor, float VdotH) + { + float Fc = pow(1 - VdotH, 5); + return Fc + (1 - Fc) * specularColor; + } + + float3 F_Schlick(float3 F0, float3 F90, float VdotH) + { + float Fc = pow(1 - VdotH, 5); + return F0 + (F90 - F0) * Fc; + } + + // [Kutz et al. 2021, "Novel aspects of the Adobe Standard Material" ] + float3 F_AdobeF82(float3 F0, float3 F82, float VdotH) + { + const float Fc = pow(1 - VdotH, 5); + const float K = 49.0 / 46656.0; + float3 b = (K - K * F82) * (7776.0 + 9031.0 * F0); + return saturate(F0 + Fc * ((1 - F0) - b * (VdotH - VdotH * VdotH))); + } + + // [Beckmann 1963, "The scattering of electromagnetic waves from rough surfaces"] + float D_Beckmann(float roughness, float NdotH) + { + float NdotH2 = NdotH * NdotH; + float a = roughness * roughness; + float a2 = a * a; + return exp((NdotH2 - 1.0) / (a2 * NdotH2)) / (Math::PI * a2 * NdotH2 * NdotH2); + } + + // [Walter et al. 2007, "Microfacet models for refraction through rough surfaces"] + float D_GGX(float roughness, float NdotH) + { + float NdotH2 = NdotH * NdotH; + float a = roughness * roughness; + float a2 = a * a; + float d = NdotH2 * (a2 - 1.0) + 1.0; + return (a2 / (Math::PI * d * d)); + } + + // [Burley 2012, "Physically-Based Shading at Disney"] + float D_AnisoGGX(float alphaX, float alphaY, float NdotH, float XdotH, float YdotH) + { + float d = XdotH * XdotH / (alphaX * alphaX) + YdotH * YdotH / (alphaY * alphaY) + NdotH * NdotH; + return rcp(Math::PI * alphaX * alphaY * d * d); + } + + // [Estevez et al. 2017, "Production Friendly Microfacet Sheen BRDF"] + float D_Charlie(float roughness, float NdotH) + { + float invAlpha = pow(roughness, -4); + float cos2h = NdotH * NdotH; + float sin2h = max(1.0 - cos2h, 1e-5); + return (2.0 + invAlpha) * pow(sin2h, invAlpha * 0.5) / Math::TAU; + } + + // Smith term for GGX + // [Smith 1967, "Geometrical shadowing of a random rough surface"] + float Vis_Smith(float roughness, float NdotV, float NdotL) + { + float a = roughness * roughness; + float a2 = a * a; + float Vis_SmithV = NdotV + sqrt(a2 + (1.0 - a2) * NdotV * NdotV); + float Vis_SmithL = NdotL + sqrt(a2 + (1.0 - a2) * NdotL * NdotL); + return rcp(Vis_SmithV * Vis_SmithL); + } + + // Appoximation of joint Smith term for GGX + // [Heitz 2014, "Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs"] + float Vis_SmithJointApprox(float roughness, float NdotV, float NdotL) + { + float a = roughness * roughness; + float Vis_SmithV = NdotL * (NdotV * (1.0 + a) + a); + float Vis_SmithL = NdotV * (NdotL * (1.0 + a) + a); + return rcp(Vis_SmithV + Vis_SmithL) * 0.5; + } + + float Vis_SmithJoint(float roughness, float NdotV, float NdotL) + { + float a = roughness * roughness; + float a2 = a * a; + float Vis_SmithV = NdotL * sqrt(a2 + (1.0 - a2) * NdotV * NdotV); + float Vis_SmithL = NdotV * sqrt(a2 + (1.0 - a2) * NdotL * NdotL); + return rcp(Vis_SmithV + Vis_SmithL) * 0.5; + } + + float Vis_SmithJointAniso(float alphaX, float alphaY, float NdotL, float NdotV, float XdotL, float YdotL, float XdotV, float YdotV) + { + float Vis_SmithV = NdotL * length(float3(alphaX * XdotV, alphaY * YdotV, NdotV)); + float Vis_SmithL = NdotV * length(float3(alphaX * XdotL, alphaY * YdotL, NdotL)); + return rcp(Vis_SmithV + Vis_SmithL) * 0.5; + } + + // [Estevez and Kulla 2017, "Production Friendly Microfacet Sheen BRDF"] + float Vis_Charlie_L(float x, float r) + { + r = saturate(r); + r = 1.0 - (1.0 - r) * (1.0 - r); + float a = lerp(25.3245 , 21.5473 , r); + float b = lerp( 3.32435, 3.82987, r); + float c = lerp( 0.16801, 0.19823, r); + float d = lerp(-1.27393, -1.97760, r); + float e = lerp(-4.85967, -4.32054, r); + + return a * rcp((1 + b * pow(x, c)) + d * x + e); + } + + float Vis_Charlie(float roughness, float NdotV, float NdotL) + { + float visV = NdotV < 0.5 ? exp(Vis_Charlie_L(NdotV, roughness)) : exp(2.0 * Vis_Charlie_L(0.5, roughness) - Vis_Charlie_L(1.0 - NdotV, roughness)); + float visL = NdotL < 0.5 ? exp(Vis_Charlie_L(NdotL, roughness)) : exp(2.0 * Vis_Charlie_L(0.5, roughness) - Vis_Charlie_L(1.0 - NdotL, roughness)); + return rcp(((1.0 + visV + visL) * (4.0 * NdotL * NdotV))); + } + + // [Neubelt et al. 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886"] + float Vis_Neubelt(float NdotV, float NdotL) + { + return rcp(4.0 * (NdotL + NdotV - NdotL * NdotV)); + } + + // [Lazarov 2013, "Getting More Physical in Call of Duty: Black Ops II"] + float2 EnvBRDFApproxLazarov(float roughness, float NdotV) + { + const float4 c0 = { -1, -0.0275, -0.572, 0.022 }; + const float4 c1 = { 1, 0.0425, 1.04, -0.04 }; + float4 r = roughness * c0 + c1; + float a004 = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y; + float2 AB = float2(-1.04, 1.04) * a004 + r.zw; + return AB; + } +} + +#endif // __BRDF_DEPENDENCY_HLSL__ \ No newline at end of file diff --git a/package/Shaders/Common/PBR.hlsli b/package/Shaders/Common/PBR.hlsli index 4139f89e7a..24205e1f6d 100644 --- a/package/Shaders/Common/PBR.hlsli +++ b/package/Shaders/Common/PBR.hlsli @@ -1,6 +1,7 @@ #ifndef __PBR_DEPENDENCY_HLSL__ #define __PBR_DEPENDENCY_HLSL__ +#include "Common/BRDF.hlsli" #include "Common/Color.hlsli" #include "Common/Math.hlsli" #include "Common/SharedData.hlsli" @@ -149,58 +150,19 @@ namespace PBR return saturate(pow(NdotV + ao, exp2(-16.0 * roughness - 1.0)) - 1.0 + ao); } - // [Schlick 1994, "An Inexpensive BRDF Model for Physically-Based Rendering"] - float3 GetFresnelFactorSchlick(float3 specularColor, float VdotH) - { - float Fc = pow(1 - VdotH, 5); - return Fc + (1 - Fc) * specularColor; - } - - // [Heitz 2014, "Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs"] - float GetVisibilityFunctionSmithJointApprox(float roughness, float NdotV, float NdotL) - { - float a = roughness * roughness; - float visSmithV = NdotL * (NdotV * (1 - a) + a); - float visSmithL = NdotV * (NdotL * (1 - a) + a); - return 0.5 * rcp(visSmithV + visSmithL); - } - - // [Neubelt et al. 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886"] - float GetVisibilityFunctionNeubelt(float NdotV, float NdotL) - { - return rcp(4 * (NdotL + NdotV - NdotL * NdotV)); - } - - // [Walter et al. 2007, "Microfacet models for refraction through rough surfaces"] - float GetNormalDistributionFunctionGGX(float roughness, float NdotH) - { - float a2 = pow(roughness, 4); - float d = max((NdotH * a2 - NdotH) * NdotH + 1, 1e-5); - return a2 / (Math::PI * d * d); - } - - // [Estevez et al. 2017, "Production Friendly Microfacet Sheen BRDF"] - float GetNormalDistributionFunctionCharlie(float roughness, float NdotH) - { - float invAlpha = pow(roughness, -4); - float cos2h = NdotH * NdotH; - float sin2h = max(1.0 - cos2h, 1e-5); - return (2.0 + invAlpha) * pow(sin2h, invAlpha * 0.5) / Math::TAU; - } - #if defined(GLINT) float3 GetSpecularDirectLightMultiplierMicrofacetWithGlint(float noise, float roughness, float3 specularColor, float NdotL, float NdotV, float NdotH, float VdotH, float glintH, float logDensity, float microfacetRoughness, float densityRandomization, Glints::GlintCachedVars glintCache, out float3 F) { - float D = GetNormalDistributionFunctionGGX(roughness, NdotH); + float D = BRDF::D_GGX(roughness, NdotH); [branch] if (logDensity > 1.1) { - float D_max = GetNormalDistributionFunctionGGX(roughness, 1); + float D_max = BRDF::D_GGX(roughness, 1); D = Glints::SampleGlints2023NDF(noise, logDensity, microfacetRoughness, densityRandomization, glintCache, glintH, D, D_max).x; } - float G = GetVisibilityFunctionSmithJointApprox(roughness, NdotV, NdotL); - F = GetFresnelFactorSchlick(specularColor, VdotH); + float G = BRDF::Vis_SmithJointApprox(roughness, NdotV, NdotL); + F = BRDF::F_Schlick(specularColor, VdotH); return D * G * F; } @@ -208,96 +170,22 @@ namespace PBR float3 GetSpecularDirectLightMultiplierMicrofacet(float roughness, float3 specularColor, float NdotL, float NdotV, float NdotH, float VdotH, out float3 F) { - float D = GetNormalDistributionFunctionGGX(roughness, NdotH); - float G = GetVisibilityFunctionSmithJointApprox(roughness, NdotV, NdotL); - F = GetFresnelFactorSchlick(specularColor, VdotH); + float D = BRDF::D_GGX(roughness, NdotH); + float G = BRDF::Vis_SmithJointApprox(roughness, NdotV, NdotL); + F = BRDF::F_Schlick(specularColor, VdotH); return D * G * F; } float3 GetSpecularDirectLightMultiplierMicroflakes(float roughness, float3 specularColor, float NdotL, float NdotV, float NdotH, float VdotH) { - float D = GetNormalDistributionFunctionCharlie(roughness, NdotH); - float G = GetVisibilityFunctionNeubelt(NdotV, NdotL); - float3 F = GetFresnelFactorSchlick(specularColor, VdotH); + float D = BRDF::D_Charlie(roughness, NdotH); + float G = BRDF::Vis_Neubelt(NdotV, NdotL); + float3 F = BRDF::F_Schlick(specularColor, VdotH); return D * G * F; } - float GetDiffuseDirectLightMultiplierLambert() - { - return 1 / Math::PI; - } - - // [Burley 2012, "Physically-Based Shading at Disney"] - float3 GetDiffuseDirectLightMultiplierBurley(float roughness, float NdotV, float NdotL, float VdotH) - { - float Fd90 = 0.5 + 2 * VdotH * VdotH * roughness; - float FdV = 1 + (Fd90 - 1) * pow(1 - NdotV, 5); - float FdL = 1 + (Fd90 - 1) * pow(1 - NdotL, 5); - return (1 / Math::PI) * FdV * FdL; - } - - // [Oren et al. 1994, "Generalization of Lambert’s Reflectance Model"] - float3 GetDiffuseDirectLightMultiplierOrenNayar(float roughness, float3 N, float3 V, float3 L, float NdotV, float NdotL) - { - float a = roughness * roughness * 0.25; - float A = 1.0 - 0.5 * (a / (a + 0.33)); - float B = 0.45 * (a / (a + 0.09)); - - float gamma = dot(V - N * NdotV, L - N * NdotL) / (sqrt(saturate(1.0 - NdotV * NdotV)) * sqrt(saturate(1.0 - NdotL * NdotL))); - - float2 cos_alpha_beta = NdotV < NdotL ? float2(NdotV, NdotL) : float2(NdotL, NdotV); - float2 sin_alpha_beta = sqrt(saturate(1.0 - cos_alpha_beta * cos_alpha_beta)); - float C = sin_alpha_beta.x * sin_alpha_beta.y / (1e-6 + cos_alpha_beta.y); - - return (1 / Math::PI) * (A + B * max(0.0, gamma) * C); - } - - // [Gotanda 2014, "Designing Reflectance Models for New Consoles"] - float3 GetDiffuseDirectLightMultiplierGotanda(float roughness, float NdotV, float NdotL, float VdotL) - { - float a = roughness * roughness; - float a2 = a * a; - float F0 = 0.04; - float Cosri = VdotL - NdotV * NdotL; - float Fr = (1 - (0.542026 * a2 + 0.303573 * a) / (a2 + 1.36053)) * (1 - pow(1 - NdotV, 5 - 4 * a2) / (a2 + 1.36053)) * ((-0.733996 * a2 * a + 1.50912 * a2 - 1.16402 * a) * pow(1 - NdotV, 1 + rcp(39 * a2 * a2 + 1)) + 1); - float Lm = (max(1 - 2 * a, 0) * (1 - pow(1 - NdotL, 5)) + min(2 * a, 1)) * (1 - 0.5 * a * (NdotL - 1)) * NdotL; - float Vd = (a2 / ((a2 + 0.09) * (1.31072 + 0.995584 * NdotV))) * (1 - pow(1 - NdotL, (1 - 0.3726732 * NdotV * NdotV) / (0.188566 + 0.38841 * NdotV))); - float Bp = Cosri < 0 ? 1.4 * NdotV * NdotL * Cosri : Cosri; - float Lr = (21.0 / 20.0) * (1 - F0) * (Fr * Lm + Vd + Bp); - return (1 / Math::PI) * Lr; - } - - // [Chan 2018, "Material Advances in Call of Duty: WWII"] - float3 GetDiffuseDirectLightMultiplierChan(float roughness, float NdotV, float NdotL, float VdotH, float NdotH) - { - float a = roughness * roughness; - float a2 = a * a; - float g = saturate((1.0 / 18.0) * log2(2 * rcp(a2) - 1)); - - float F0 = VdotH + pow(1 - VdotH, 5); - float FdV = 1 - 0.75 * pow(1 - NdotV, 5); - float FdL = 1 - 0.75 * pow(1 - NdotL, 5); - - float Fd = lerp(F0, FdV * FdL, saturate(2.2 * g - 0.5)); - - float Fb = ((34.5 * g - 59) * g + 24.5) * VdotH * exp2(-max(73.2 * g - 21.2, 8.9) * sqrt(NdotH)); - - return (1 / Math::PI) * (Fd + Fb); - } - - // [Lazarov 2013, "Getting More Physical in Call of Duty: Black Ops II"] - float2 GetEnvBRDFApproxLazarov(float roughness, float NdotV) - { - const float4 c0 = { -1, -0.0275, -0.572, 0.022 }; - const float4 c1 = { 1, 0.0425, 1.04, -0.04 }; - float4 r = roughness * c0 + c1; - float a004 = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y; - float2 AB = float2(-1.04, 1.04) * a004 + r.zw; - return AB; - } - float HairIOR() { const float n = 1.55; @@ -354,14 +242,14 @@ namespace PBR // R Mp = HairGaussian(B[0], ThetaH - Alpha[0]); Np = 0.25 * cosHalfPhi; - Fp = GetFresnelFactorSchlick(specularColor, sqrt(saturate(0.5 + 0.5 * VdotL))).x; + Fp = BRDF::F_Schlick(specularColor, sqrt(saturate(0.5 + 0.5 * VdotL))).x; S += (Mp * Np) * (Fp * lerp(1, backlit, saturate(-VdotL))); // TT Mp = HairGaussian(B[1], ThetaH - Alpha[1]); a = (1.55f / hairIOR) * rcp(n_prime); h = cosHalfPhi * (1 + a * (0.6 - 0.8 * cosPhi)); - f = GetFresnelFactorSchlick(specularColor, cosThetaD * sqrt(saturate(1 - h * h))).x; + f = BRDF::F_Schlick(specularColor, cosThetaD * sqrt(saturate(1 - h * h))).x; Fp = (1 - f) * (1 - f); Tp = pow(surfaceProperties.BaseColor, 0.5 * sqrt(1 - (h * a) * (h * a)) / cosThetaD); Np = exp(-3.65 * cosPhi - 3.98); @@ -369,7 +257,7 @@ namespace PBR // TRT Mp = HairGaussian(B[2], ThetaH - Alpha[2]); - f = GetFresnelFactorSchlick(specularColor, cosThetaD * 0.5f).x; + f = BRDF::F_Schlick(specularColor, cosThetaD * 0.5f).x; Fp = (1 - f) * (1 - f) * f; Tp = pow(surfaceProperties.BaseColor, 0.8 / cosThetaD); Np = exp(17 * cosPhi - 16.78); @@ -435,7 +323,7 @@ namespace PBR else #endif { - diffuse += lightProperties.LightColor * satNdotL * GetDiffuseDirectLightMultiplierLambert(); + diffuse += lightProperties.LightColor * satNdotL * BRDF::Diffuse_Lambert(); float3 F; #if defined(GLINT) @@ -446,7 +334,7 @@ namespace PBR specular += GetSpecularDirectLightMultiplierMicrofacet(surfaceProperties.Roughness, surfaceProperties.F0, satNdotL, satNdotV, satNdotH, satVdotH, F) * lightProperties.LightColor * satNdotL; #endif - float2 specularBRDF = GetEnvBRDFApproxLazarov(surfaceProperties.Roughness, satNdotV); + float2 specularBRDF = BRDF::EnvBRDFApproxLazarov(surfaceProperties.Roughness, satNdotV); specular *= 1 + surfaceProperties.F0 * (1 / (specularBRDF.x + specularBRDF.y) - 1); #if !defined(LANDSCAPE) && !defined(LODLANDSCAPE) @@ -464,7 +352,7 @@ namespace PBR float forwardScatter = exp2(saturate(-VdotL) * subsurfacePower - subsurfacePower); float backScatter = saturate(satNdotL * surfaceProperties.Thickness + (1.0 - surfaceProperties.Thickness)) * 0.5; float subsurface = lerp(backScatter, 1, forwardScatter) * (1.0 - surfaceProperties.Thickness); - transmission += surfaceProperties.SubsurfaceColor * subsurface * lightProperties.LightColor * GetDiffuseDirectLightMultiplierLambert(); + transmission += surfaceProperties.SubsurfaceColor * subsurface * lightProperties.LightColor * BRDF::Diffuse_Lambert(); } else if ((PBRFlags & Flags::TwoLayer) != 0) { @@ -489,7 +377,7 @@ namespace PBR diffuse *= layerAttenuation; specular *= layerAttenuation; - coatDiffuse += lightProperties.CoatLightColor * coatNdotL * GetDiffuseDirectLightMultiplierLambert(); + coatDiffuse += lightProperties.CoatLightColor * coatNdotL * BRDF::Diffuse_Lambert(); specular += coatSpecular * surfaceProperties.CoatStrength; } #endif @@ -544,7 +432,7 @@ namespace PBR } #endif - float2 specularBRDF = GetEnvBRDFApproxLazarov(surfaceProperties.Roughness, NdotV); + float2 specularBRDF = BRDF::EnvBRDFApproxLazarov(surfaceProperties.Roughness, NdotV); specularLobeWeight = surfaceProperties.F0 * specularBRDF.x + specularBRDF.y; diffuseLobeWeight *= (1 - specularLobeWeight); @@ -553,11 +441,11 @@ namespace PBR #if !defined(LANDSCAPE) && !defined(LODLANDSCAPE) [branch] if ((PBRFlags & Flags::TwoLayer) != 0) { - float2 coatSpecularBRDF = GetEnvBRDFApproxLazarov(surfaceProperties.CoatRoughness, NdotV); + float2 coatSpecularBRDF = BRDF::EnvBRDFApproxLazarov(surfaceProperties.CoatRoughness, NdotV); float3 coatSpecularLobeWeight = surfaceProperties.CoatF0 * coatSpecularBRDF.x + coatSpecularBRDF.y; coatSpecularLobeWeight *= 1 + surfaceProperties.CoatF0 * (1 / (coatSpecularBRDF.x + coatSpecularBRDF.y) - 1); - float3 coatF = GetFresnelFactorSchlick(surfaceProperties.CoatF0, NdotV); + float3 coatF = BRDF::F_Schlick(surfaceProperties.CoatF0, NdotV); float3 layerAttenuation = 1 - coatF * surfaceProperties.CoatStrength; diffuseLobeWeight *= layerAttenuation; @@ -596,7 +484,7 @@ namespace PBR const float wetnessF0 = 0.02; float NdotV = saturate(abs(dot(N, V)) + 1e-5); - float2 specularBRDF = GetEnvBRDFApproxLazarov(roughness, NdotV); + float2 specularBRDF = BRDF::EnvBRDFApproxLazarov(roughness, NdotV); float3 specularLobeWeight = wetnessF0 * specularBRDF.x + specularBRDF.y; // Horizon specular occlusion diff --git a/src/Features/LightLimitFix.cpp b/src/Features/LightLimitFix.cpp index 7b27eff3e7..80f6edd345 100644 --- a/src/Features/LightLimitFix.cpp +++ b/src/Features/LightLimitFix.cpp @@ -752,7 +752,7 @@ void LightLimitFix::UpdateLights() // Process point lights - roomNodes.empty(); + roomNodes.clear(); auto addRoom = [&](RE::NiNode* node, LightData& light) { uint8_t roomIndex = 0; diff --git a/src/Features/TerrainHelper.cpp b/src/Features/TerrainHelper.cpp index b7266ab4aa..a446b2522d 100644 --- a/src/Features/TerrainHelper.cpp +++ b/src/Features/TerrainHelper.cpp @@ -30,15 +30,21 @@ void TerrainHelper::DataLoaded() if (defaultLandTextureSet != nullptr) { logger::info("[Terrain Helper] LandscapeDefault EDID texture set found"); defaultLandTexture = defaultLandTextureSet; + // only enable if TerrainHelper.esp is loaded + enabled = true; } else { - logger::info("[Terrain Helper] LandscapeDefault EDID texture set not found, using default"); - const auto bgsDefaultLandTex = *REL::Relocation(RELOCATION_ID(514783, 400936)); - defaultLandTexture = bgsDefaultLandTex->textureSet; + logger::warn("[Terrain Helper] LandscapeDefault EDID texture set from TerrainHelper.esp not found. Terrain helper is disabled."); + enabled = false; } } bool TerrainHelper::TESObjectLAND_SetupMaterial(RE::TESObjectLAND* land) { + if (!enabled) { + // terrain helper is not enabled + return false; + } + if (land == nullptr || land->loadedData == nullptr || land->loadedData->mesh[0] == nullptr) { // this is not terrain or vanilla material failed return false; @@ -146,6 +152,11 @@ void TerrainHelper::SetShaderResouces(ID3D11DeviceContext* a_context) void TerrainHelper::BSLightingShader_SetupMaterial(RE::BSLightingShaderMaterialBase const* material) { + if (!enabled) { + // terrain helper is not enabled + return; + } + if (material == nullptr) { return; } diff --git a/src/Features/TerrainHelper.h b/src/Features/TerrainHelper.h index 4ec2e5d41c..abfac99997 100644 --- a/src/Features/TerrainHelper.h +++ b/src/Features/TerrainHelper.h @@ -41,6 +41,7 @@ struct TerrainHelper : Feature std::shared_mutex extendedSlotsMutex; std::unordered_map extendedSlots; RE::BGSTextureSet* defaultLandTexture; + bool enabled = false; virtual void DataLoaded() override; virtual bool SupportsVR() override { return true; }; diff --git a/src/Menu.cpp b/src/Menu.cpp index 25e09978a9..db84f6ad08 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -22,7 +22,6 @@ #include "Utils/UI.h" #include "Features/LightLimitFix/ParticleLights.h" -#include "Utils/UI.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Menu::ThemeSettings::PaletteColors, @@ -315,16 +314,6 @@ void Menu::DrawSettings() uiIcons.clearCache.texture || uiIcons.clearDiskCache.texture); - // Debug logging for icon availability - if (settings.Theme.ShowActionIcons) { - logger::debug("Icon status - Save: {}, Load: {}, Cache: {}, Disk: {}, Logo: {}", - uiIcons.saveSettings.texture ? "OK" : "NULL", - uiIcons.loadSettings.texture ? "OK" : "NULL", - uiIcons.clearCache.texture ? "OK" : "NULL", - uiIcons.clearDiskCache.texture ? "OK" : "NULL", - uiIcons.logo.texture ? "OK" : "NULL"); - } - // Always show logo if available, regardless of action icons setting bool showLogo = uiIcons.logo.texture != nullptr; @@ -629,34 +618,6 @@ void Menu::DrawSettings() // Restore original text color ImGui::PopStyleColor(); - // Show tooltip based on the state - if (isDisabled) { - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Disabled at boot. Reenable, save settings, and restart."); - } - } else if (!isLoaded) { - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text(hasFailedMessage ? feat->failedLoadedMessage.c_str() : "Feature pending restart."); - } - } else if (isLoaded) { - // Show feature summary tooltip for loaded features - if (auto _tt = Util::HoverTooltipWrapper()) { - auto [description, keyFeatures] = feat->GetFeatureSummary(); - if (!description.empty()) { - ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); - ImGui::Text("%s", description.c_str()); - if (!keyFeatures.empty()) { - ImGui::Spacing(); - ImGui::Text("Key Features:"); - for (const auto& feature : keyFeatures) { - ImGui::BulletText("%s", feature.c_str()); - } - } - ImGui::PopTextWrapPos(); - } - } - } - // Display version if loaded if (isLoaded) { ImGui::SameLine(); @@ -691,81 +652,146 @@ void Menu::DrawSettings() bool hasFailedMessage = !feat->failedLoadedMessage.empty(); auto& themeSettings = globals::menu->settings.Theme; - if (ImGui::BeginTable("##FeatureButtons", 2, ImGuiTableFlags_SizingStretchSame)) { - ImGui::TableNextColumn(); - - ImVec4 textColor; - - // Determine the text color based on the state - if (isDisabled) { - textColor = themeSettings.StatusPalette.Disable; - } else if (hasFailedMessage) { - textColor = themeSettings.StatusPalette.Error; - } else { - textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text); - } - ImGui::PushStyleColor(ImGuiCol_Text, textColor); - - if (ImGui::Button(isDisabled ? "Enable at Boot" : "Disable at Boot", { -1, 0 })) { - bool newState = feat->ToggleAtBootSetting(); - logger::info("{}: {} at boot.", featureName, newState ? "Enabled" : "Disabled"); - } + if (ImGui::BeginTabBar("##FeatureTabs")) { + if (ImGui::BeginTabItem("Settings")) { + if (ImGui::BeginChild("##FeatureSettingsFrame", { 0, 0 }, true)) { + ImGui::SeparatorText("Feature Management"); + + // Disable/Enable at boot + ImVec4 textColor; + if (isDisabled) { + textColor = themeSettings.StatusPalette.Disable; + } else if (hasFailedMessage) { + textColor = themeSettings.StatusPalette.Error; + } else { + textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text); + } + ImGui::PushStyleColor(ImGuiCol_Text, textColor); + if (ImGui::Button(isDisabled ? "Enable at Boot" : "Disable at Boot", { -1, 0 })) { + bool newState = feat->ToggleAtBootSetting(); + logger::info("{}: {} at boot.", featureName, newState ? "Enabled" : "Disabled"); + } + ImGui::PopStyleColor(); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Current State: %s\n" + "%s the feature settings at boot. " + "Restart will be required to reenable. " + "This is the same as deleting the ini file. " + "This should remove any performance impact for the feature.", + isDisabled ? "Disabled" : "Enabled", + isDisabled ? "Enable" : "Disable"); + } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Current State: %s\n" - "%s the feature settings at boot. " - "Restart will be required to reenable. " - "This is the same as deleting the ini file. " - "This should remove any performance impact for the feature.", - isDisabled ? "Disabled" : "Enabled", - isDisabled ? "Enable" : "Disable"); - } + // Restore Defaults buttons shows when feature is not disabled and is loaded + if (!isDisabled && isLoaded) { + ImGui::Spacing(); + if (ImGui::Button("Restore Defaults", { -1, 0 })) { + feat->RestoreDefaultSettings(); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Restores the feature's settings back to their default values. " + "You will still need to Save Settings to make these changes permanent."); + } + } - ImGui::PopStyleColor(); + ImGui::Spacing(); + ImGui::Spacing(); - ImGui::TableNextColumn(); + // Feature-specific settings section + ImGui::SeparatorText("Feature Settings"); + if (isDisabled) { + // Show disabled message + ImGui::TextColored(themeSettings.StatusPalette.Disable, "Feature settings are hidden because this feature is disabled at boot."); + ImGui::Spacing(); + ImGui::Text("Enable the feature above to access its configuration options."); + } else { + if (isLoaded) { + feat->DrawSettings(); + } else { + feat->DrawUnloadedUI(); + // Add download link if available + if (!feat->GetFeatureModLink().empty()) { + ImGui::Spacing(); + const auto downloadText = fmt::format("Click here to download this feature ({})", feat->GetFeatureModLink()); + if (ImGui::Selectable(downloadText.c_str())) { + ShellExecuteA(NULL, "open", feat->GetFeatureModLink().c_str(), NULL, NULL, SW_SHOWNORMAL); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Download the feature from the mod page."); + } + } + } + } - if (!isDisabled && isLoaded) { - if (ImGui::Button("Restore Defaults", { -1, 0 })) { - feat->RestoreDefaultSettings(); - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Restores the feature's settings back to their default values. " - "You will still need to Save Settings to make these changes permanent."); + // Error Messages + if (hasFailedMessage && feat->DrawFailLoadMessage()) { + ImGui::Spacing(); + ImGui::SeparatorText("Error"); + ImGui::TextColored(themeSettings.StatusPalette.Error, feat->failedLoadedMessage.c_str()); + } } + ImGui::EndChild(); + ImGui::EndTabItem(); } - ImGui::EndTable(); - } - - if (hasFailedMessage && feat->DrawFailLoadMessage()) { - ImGui::TextColored(themeSettings.StatusPalette.Error, feat->failedLoadedMessage.c_str()); - } + // About Tab - Information about the feature and how it works + if (ImGui::BeginTabItem("About")) { + if (ImGui::BeginChild("##FeatureAboutFrame", { 0, 0 }, true)) { + // Status Section + ImGui::SeparatorText("Status"); + + ImVec4 statusColor; + const char* statusText; + if (isDisabled) { + statusColor = themeSettings.StatusPalette.Disable; + statusText = "Disabled at boot."; + } else if (hasFailedMessage) { + statusColor = themeSettings.StatusPalette.Error; + statusText = "Failed to load."; + } else if (!isLoaded) { + statusColor = themeSettings.StatusPalette.RestartNeeded; + statusText = "Pending restart."; + } else { + statusColor = themeSettings.StatusPalette.SuccessColor; + statusText = "Active."; + } - if (!isDisabled) { - if (ImGui::BeginChild("##FeatureConfigFrame", { 0, 0 }, true)) { - if (isLoaded) { - // draw settings for loaded feature - feat->DrawSettings(); - } else { - // draw any unloaded UI elements like help text about the feature - feat->DrawUnloadedUI(); - - // draw download link if available - if (!feat->GetFeatureModLink().empty()) { - // print feature download info + ImGui::TextColored(statusColor, "Current State: %s", statusText); + + // Feature Info - Description and key features + if (isLoaded) { + auto [description, keyFeatures] = feat->GetFeatureSummary(); + if (!description.empty()) { + ImGui::Spacing(); + ImGui::SeparatorText("Description"); + ImGui::TextWrapped("%s", description.c_str()); + + if (!keyFeatures.empty()) { + ImGui::Spacing(); + ImGui::SeparatorText("Key Features"); + for (const auto& feature : keyFeatures) { + ImGui::BulletText("%s", feature.c_str()); + } + } + } + } else { + // For unloaded features, show basic info if available ImGui::Spacing(); - const auto downloadText = fmt::format("Click here to download this feature ({})", feat->GetFeatureModLink()); - if (ImGui::Selectable(downloadText.c_str())) { - ShellExecuteA(NULL, "open", feat->GetFeatureModLink().c_str(), NULL, NULL, SW_SHOWNORMAL); + ImGui::SeparatorText("Information"); + ImGui::Text("This feature is not currently loaded."); + if (hasFailedMessage) { + ImGui::Spacing(); + ImGui::TextColored(themeSettings.StatusPalette.Error, "%s", feat->failedLoadedMessage.c_str()); } } } + ImGui::EndChild(); + ImGui::EndTabItem(); } - ImGui::EndChild(); } + ImGui::EndTabBar(); } }; diff --git a/src/XSEPlugin.cpp b/src/XSEPlugin.cpp index 1484e04e12..9704b88a93 100644 --- a/src/XSEPlugin.cpp +++ b/src/XSEPlugin.cpp @@ -1,4 +1,3 @@ - #include "DX12SwapChain.h" #include "Deferred.h" #include "FrameAnnotations.h" @@ -91,16 +90,18 @@ void MessageHandler(SKSE::MessagingInterface::Message* message) auto shaderCache = globals::shaderCache; - shaderCache->ValidateDiskCache(); - - if (shaderCache->UseFileWatcher()) - shaderCache->StartFileWatcher(); - + // Run feature PostPostLoad() first so features can disable themselves if needed for (auto* feature : Feature::GetFeatureList()) { if (feature->loaded) { feature->PostPostLoad(); } } + + // Now validate disk cache after features have had a chance to modify their state + shaderCache->ValidateDiskCache(); + + if (shaderCache->UseFileWatcher()) + shaderCache->StartFileWatcher(); } break;