diff --git a/CMakeLists.txt b/CMakeLists.txt index c05cbf4f1b..a6f91f6931 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,6 +90,8 @@ find_package(unordered_dense CONFIG REQUIRED) find_package(efsw CONFIG REQUIRED) find_package(Tracy CONFIG REQUIRED) find_package(directx-headers CONFIG REQUIRED) +find_package(effects11 CONFIG REQUIRED) + add_subdirectory(${CMAKE_SOURCE_DIR}/cmake/Streamline) find_path(DETOURS_INCLUDE_DIRS "detours/detours.h") @@ -142,6 +144,8 @@ target_link_libraries( d3d12.lib Microsoft::DirectX-Headers ${DETOURS_LIBRARY} + Microsoft::Effects11 + windowsapp ) # Some third-party libs (e.g. FFX backend) are built with /GL. diff --git a/features/Cloud Shadows/Shaders/CloudShadows/CloudShadows.hlsli b/features/Cloud Shadows/Shaders/CloudShadows/CloudShadows.hlsli index e72e1c556d..9e6325ae8f 100644 --- a/features/Cloud Shadows/Shaders/CloudShadows/CloudShadows.hlsli +++ b/features/Cloud Shadows/Shaders/CloudShadows/CloudShadows.hlsli @@ -4,11 +4,7 @@ namespace CloudShadows { TextureCube CloudShadowsTexture : register(t25); - // 2 km cloud altitude / 6371 km planet radius converted from metres - // to game units. The original 1.428e-2 is GAME_UNIT_TO_M (1.428f / 100), - // so dividing by GAME_UNIT_TO_M is the exact same value. Game.hlsli - // parenthesizes the macro, so the operator precedence is preserved. - const static float CloudHeight = (2e3f / GAME_UNIT_TO_M) * 0.25; + const static float CloudHeight = (2e3f / GAME_UNIT_TO_M); const static float PlanetRadius = (6371e3f / GAME_UNIT_TO_M); const static float RcpHPlusR = (1.0 / (CloudHeight + PlanetRadius)); @@ -27,6 +23,13 @@ namespace CloudShadows { float3 cloudSampleDir = GetCloudShadowSampleDir(worldPosition, SharedData::DirLightDirection.xyz).xyz; float cloudCubeSample = CloudShadowsTexture.SampleLevel(textureSampler, cloudSampleDir, 0).x; - return lerp(1.0, 1.0 - cloudCubeSample, SharedData::cloudShadowsSettings.Opacity); + return saturate(1.0 - cloudCubeSample * SharedData::cloudShadowsSettings.Opacity); + } + + float GetTrueCloudShadowMult(float3 worldPosition, SamplerState textureSampler) + { + float3 cloudSampleDir = GetCloudShadowSampleDir(worldPosition, SharedData::SunDirection.xyz).xyz; + float cloudCubeSample = CloudShadowsTexture.SampleLevel(textureSampler, cloudSampleDir, 0).x; + return 1.0 - cloudCubeSample; } } diff --git a/features/Dynamic Cubemaps/Shaders/Features/DynamicCubemaps.ini b/features/Dynamic Cubemaps/Shaders/Features/DynamicCubemaps.ini index 82f2c91940..52d59c5c3a 100644 --- a/features/Dynamic Cubemaps/Shaders/Features/DynamicCubemaps.ini +++ b/features/Dynamic Cubemaps/Shaders/Features/DynamicCubemaps.ini @@ -1,5 +1,5 @@ [Info] -Version = 2-3-1 +Version = 2-4-0 [Nexus] autoupload = false diff --git a/features/Effect11/Shaders/Effect11/ApplyVolumetricRaysPS.hlsl b/features/Effect11/Shaders/Effect11/ApplyVolumetricRaysPS.hlsl new file mode 100644 index 0000000000..5443c0168c --- /dev/null +++ b/features/Effect11/Shaders/Effect11/ApplyVolumetricRaysPS.hlsl @@ -0,0 +1,42 @@ +#include "Common/SharedData.hlsli" + +#if defined(IBL) +# define IBL_DEFERRED +# include "IBL/IBL.hlsli" +#endif + +Texture2D BlurredShadowTexture : register(t0); + +struct VS_OUTPUT_POST +{ + float4 pos : SV_POSITION; + float2 txcoord0 : TEXCOORD0; +}; + +float4 main(VS_OUTPUT_POST input) : SV_Target0 +{ + float2 uv = input.txcoord0; + uint eyeIndex = 0; + + float volumetricShadow = BlurredShadowTexture.Load(int3(input.pos.xy, 0)); + + float depth = SharedData::GetDepth(uv); + float4 positionCS = float4(2 * float2(uv.x, -uv.y + 1) - 1, depth, 1); + float4 positionMS = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], positionCS); + positionMS.xyz /= positionMS.w; + + float3 viewDirection = normalize(positionMS.xyz); + + float phase = dot(viewDirection, SharedData::SunDirection.xyz) * 0.5 + 0.5; + float3 lightColor = SharedData::SunColor.xyz * phase; + +#if defined(IBL) + float3 ibl = ImageBasedLighting::GetSkyIBL(float3(0, 0, -1)); + ibl = lerp(dot(ibl, 1.0 / 3.0), ibl, 2.0); + lightColor += ibl * SharedData::enbSettings.VolumetricRaysSkyColorAmount; +#endif + + float3 volumetricColor = volumetricShadow * lightColor * SharedData::enbSettings.VolumetricRaysIntensity * SharedData::SunColor.w; + + return float4(volumetricColor, 0); +} diff --git a/features/Effect11/Shaders/Effect11/ColorCorrectionCS.hlsl b/features/Effect11/Shaders/Effect11/ColorCorrectionCS.hlsl new file mode 100644 index 0000000000..2ddd9ff5fa --- /dev/null +++ b/features/Effect11/Shaders/Effect11/ColorCorrectionCS.hlsl @@ -0,0 +1,44 @@ +cbuffer ColorCorrectionParams : register(b0) +{ + float Brightness; + float GammaCurve; + uint FrameCount; +}; + +RWTexture2D OutputTexture : register(u0); + +uint3 pcg3d(uint3 v) +{ + v = v * 1664525u + 1013904223u; + v.x += v.y * v.z; + v.y += v.z * v.x; + v.z += v.x * v.y; + v ^= v >> 16u; + v.x += v.y * v.z; + v.y += v.z * v.x; + v.z += v.x * v.y; + return v; +} + +float3 TriDither(float2 screenPos, uint frameCount) +{ + uint3 seed1 = uint3(screenPos, frameCount); + uint3 seed2 = uint3(screenPos, frameCount + 4729u); + return (pcg3d(seed1) - pcg3d(seed2)) / float(0xFFFFFFFFu); +} + +[numthreads(8, 8, 1)] void main(uint3 id + : SV_DispatchThreadID) +{ + uint width, height; + OutputTexture.GetDimensions(width, height); + if (id.x >= width || id.y >= height) { + return; + } + + float4 color = OutputTexture[id.xy]; + color.rgb = pow(abs(color.rgb), GammaCurve); + color.rgb *= Brightness; + color.rgb += TriDither(float2(id.xy), FrameCount) / 1023.0; + OutputTexture[id.xy] = color; +} diff --git a/features/Effect11/Shaders/Effect11/CopyPS.hlsl b/features/Effect11/Shaders/Effect11/CopyPS.hlsl new file mode 100644 index 0000000000..4f50079609 --- /dev/null +++ b/features/Effect11/Shaders/Effect11/CopyPS.hlsl @@ -0,0 +1,39 @@ +Texture2D SourceTexture : register(t0); + +cbuffer DitherParams : register(b0) +{ + uint FrameCount; +}; + +struct PS_INPUT +{ + float4 pos : SV_POSITION; + float2 txcoord0 : TEXCOORD0; +}; + +uint3 pcg3d(uint3 v) +{ + v = v * 1664525u + 1013904223u; + v.x += v.y * v.z; + v.y += v.z * v.x; + v.z += v.x * v.y; + v ^= v >> 16u; + v.x += v.y * v.z; + v.y += v.z * v.x; + v.z += v.x * v.y; + return v; +} + +float3 TriDither(float2 screenPos, uint frameCount) +{ + uint3 seed1 = uint3(screenPos, frameCount); + uint3 seed2 = uint3(screenPos, frameCount + 4729u); + return (pcg3d(seed1) - pcg3d(seed2)) / float(0xFFFFFFFFu); +} + +float4 main(PS_INPUT input) : SV_TARGET +{ + float3 color = SourceTexture.Load(int3(input.pos.xy, 0)).rgb; + color += TriDither(input.pos.xy, FrameCount) / 255.0; + return float4(color, 1.0); +} diff --git a/features/Effect11/Shaders/Effect11/DownsamplePS.hlsl b/features/Effect11/Shaders/Effect11/DownsamplePS.hlsl new file mode 100644 index 0000000000..edc0ae440f --- /dev/null +++ b/features/Effect11/Shaders/Effect11/DownsamplePS.hlsl @@ -0,0 +1,13 @@ +Texture2D SourceTexture : register(t0); +SamplerState LinearSampler : register(s0); + +struct PS_INPUT +{ + float4 pos : SV_POSITION; + float2 txcoord0 : TEXCOORD0; +}; + +float4 main(PS_INPUT input) : SV_Target +{ + return SourceTexture.SampleLevel(LinearSampler, input.txcoord0.xy, 0); +} diff --git a/features/Effect11/Shaders/Effect11/KawaseBlurPS.hlsl b/features/Effect11/Shaders/Effect11/KawaseBlurPS.hlsl new file mode 100644 index 0000000000..32703be76b --- /dev/null +++ b/features/Effect11/Shaders/Effect11/KawaseBlurPS.hlsl @@ -0,0 +1,22 @@ +Texture2D SourceTexture : register(t0); +SamplerState LinearSampler : register(s0); + +struct PS_INPUT +{ + float4 pos : SV_POSITION; + float2 txcoord0 : TEXCOORD0; +}; + +float4 main(PS_INPUT input) : SV_Target +{ + float2 texelSize = 1.0 / 1024.0; + float2 o = texelSize * 0.5; + + float4 sum = 0.0; + sum += SourceTexture.SampleLevel(LinearSampler, input.txcoord0.xy + float2(-o.x, -o.y), 0); + sum += SourceTexture.SampleLevel(LinearSampler, input.txcoord0.xy + float2( o.x, -o.y), 0); + sum += SourceTexture.SampleLevel(LinearSampler, input.txcoord0.xy + float2(-o.x, o.y), 0); + sum += SourceTexture.SampleLevel(LinearSampler, input.txcoord0.xy + float2( o.x, o.y), 0); + + return sum * 0.25; +} diff --git a/features/Effect11/Shaders/Effect11/QuadVS.hlsl b/features/Effect11/Shaders/Effect11/QuadVS.hlsl new file mode 100644 index 0000000000..1ffd605a85 --- /dev/null +++ b/features/Effect11/Shaders/Effect11/QuadVS.hlsl @@ -0,0 +1,19 @@ +struct VS_INPUT_POST +{ + float3 pos : POSITION; + float2 txcoord : TEXCOORD0; +}; + +struct VS_OUTPUT_POST +{ + float4 pos : SV_POSITION; + float2 txcoord0 : TEXCOORD0; +}; + +VS_OUTPUT_POST main(VS_INPUT_POST IN) +{ + VS_OUTPUT_POST OUT; + OUT.pos = float4(IN.pos, 1.0); + OUT.txcoord0 = IN.txcoord; + return OUT; +} diff --git a/features/Effect11/Shaders/Effect11/RaymarchVolumetricRaysPS.hlsl b/features/Effect11/Shaders/Effect11/RaymarchVolumetricRaysPS.hlsl new file mode 100644 index 0000000000..a9c94a50e4 --- /dev/null +++ b/features/Effect11/Shaders/Effect11/RaymarchVolumetricRaysPS.hlsl @@ -0,0 +1,55 @@ +#include "Common/FrameBuffer.hlsli" +#include "Common/Random.hlsli" +#include "Common/SharedData.hlsli" + +#define LinearSampler defaultSampler +SamplerState defaultSampler : register(s0); + +#include "Common/ShadowSampling.hlsli" + +struct VS_OUTPUT_POST +{ + float4 pos : SV_POSITION; + float2 txcoord0 : TEXCOORD0; +}; + +float main(VS_OUTPUT_POST input) : SV_Target0 +{ + float2 uv = input.txcoord0; + uint eyeIndex = 0; + + float depth = SharedData::GetDepth(uv); + float4 positionCS = float4(2 * float2(uv.x, -uv.y + 1) - 1, depth, 1); + float4 positionMS = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], positionCS); + positionMS.xyz /= positionMS.w; + + float extinction = SharedData::enbSettings.VolumetricRaysExtinction; + float totalRayLength = length(positionMS.xyz); + + const uint sampleCount = 16; + float noise = Random::InterleavedGradientNoise(input.pos.xy, SharedData::FrameCount); + float3 cameraOffset = FrameBuffer::CameraPosAdjust[eyeIndex].xyz; + + float scattering = 0.0; + float transmittance = 1.0; + + for (uint i = 0; i < sampleCount; i++) { + float t0 = float(i) / float(sampleCount); + float t1 = float(i + 1) / float(sampleCount); + + t0 *= t0; + t1 *= t1; + + float t = lerp(t0, t1, noise); + float stepLength = (t1 - t0) * totalRayLength; + + float3 samplePos = positionMS.xyz * t; + float shadow = ShadowSampling::GetTrueWorldShadow(samplePos, cameraOffset, eyeIndex); + + float stepTransmittance = exp(-extinction * stepLength); + scattering += shadow * (1.0 - stepTransmittance) * transmittance; + transmittance *= stepTransmittance; + } + + return scattering; +} diff --git a/features/Effect11/Shaders/Effect11/SettingsPatches.json b/features/Effect11/Shaders/Effect11/SettingsPatches.json new file mode 100644 index 0000000000..d26d846d39 --- /dev/null +++ b/features/Effect11/Shaders/Effect11/SettingsPatches.json @@ -0,0 +1,142 @@ +[ + { + "file": "enbeffectpostpass.fx", + "patches": [ + { "variable": "AA.|- SMAA Enable", "value": "false" }, + { "variable": "AA.|- FXAA Enable", "value": "false" }, + { "variable": "CinBorders.|- Enable", "value": "false" }, + { "variable": "KiSharp.|- Enable", "value": "false" }, + { "variable": "Blur Suite.|- Enable", "value": "false" }, + { "variable": "Blur Suite.|:: Enable", "value": "false" }, + { "variable": "Blur Suite.Depth.|- Enable", "value": "false" }, + + { "variable": "Enable Sharpening", "value": "false" }, + { "variable": "Enable Depth Sharpening", "value": "false" }, + { "variable": "Enable Luma Sharpening", "value": "false" }, + { "variable": "Enable LUMA Sharpen", "value": "false" }, + { "variable": "Enable CAS Sharp by rhellct", "value": "false" }, + { "variable": "Enable CAS", "value": "false" }, + { "variable": "Enable Contrast Adaptive Sharpener", "value": "false" }, + { "variable": "Enable ENB Sharpen", "value": "false" }, + { "variable": "Enable - Kitsune Sharpen", "value": "false" }, + { "variable": "Enable Kitsuune Sharpen", "value": "false" }, + { "variable": "Use Sharpening", "value": "false" }, + { "variable": "Use Luma Sharp", "value": "false" }, + { "variable": "Use Xtra Sharpening", "value": "false" }, + { "variable": "Use Sharpening by Color", "value": "false" }, + { "variable": "Sharp Enable", "value": "false" }, + { "variable": "Sharp enabled", "value": "false" }, + { "variable": "Contrast Adaptive Sharpener", "value": "false" }, + { "variable": "Contrast Adaptive Sharpener by rhellct", "value": "false" }, + { "variable": "|- Sharp: Enable", "value": "false" }, + { "variable": "|- Sharp - Enable Unsharp Mask", "value": "false" }, + { "variable": "| Enable CAS Sharpening", "value": "false" }, + { "variable": "| Enable Contrast Adaptive Sharpening", "value": "false" }, + { "variable": "SHARPEN SHADER.CAS Shapen.Enable CAS Sharp by rhellct", "value": "false" }, + { "variable": "SHARPEN SHADER.Kitsune Sharpen.Enable - Kitsune Sharpen", "value": "false" }, + { "variable": "-------------- FilmicSharpen --------------", "value": "false" }, + { "variable": "-------------- LumaSharpen --------------", "value": "false" }, + { "variable": "--------------- Sharpening ----------------", "value": "false" }, + { "variable": "------ Luma Sharpen (1st) ------", "value": "false" }, + + { "variable": "Enable Blurring", "value": "false" }, + { "variable": "Enable ENB Blur", "value": "false" }, + { "variable": "Enable Kitsune Blur", "value": "false" }, + { "variable": "Enable Kitsuune Blur", "value": "false" }, + { "variable": "Enable -Kitsune Blur", "value": "false" }, + { "variable": "Use Blurring", "value": "false" }, + { "variable": "Blur", "value": "false" }, + { "variable": "|- Blur - Enable", "value": "false" }, + { "variable": "|- Blur - Depth - Enable", "value": "false" }, + { "variable": "| Enable Gaussian Blur", "value": "false" }, + { "variable": "Blur.Enable -Kitsune Blur", "value": "false" }, + { "variable": "Blur.Depth - Enable", "value": "false" }, + { "variable": "----------------- Blurring ------------------", "value": "false" }, + + { "variable": "Enable Letterbox Bars", "value": "false" }, + { "variable": "Use Letterbox", "value": "false" }, + { "variable": "Toggle Letterbox", "value": "false" }, + { "variable": "Letterbox", "value": "false" }, + { "variable": "Letterbox Enable", "value": "false" }, + { "variable": "Letterbox by Ceejay.dk", "value": "false" }, + { "variable": "|- Letterbox: Enable", "value": "false" }, + { "variable": "|--- Toggle Letterbox ---|", "value": "false" }, + { "variable": "|=== Toggle Letterbox ===|", "value": "false" }, + { "variable": "|- Box - Enable Letterbox / Pillarbox", "value": "false" }, + { "variable": "| Enable Letterbox", "value": "false" }, + { "variable": "Cam: Toggle Letterbox", "value": "false" }, + { "variable": "enable border", "value": "false" }, + { "variable": "Film Effect.LETTERBOX.Enable Letterbox Bars", "value": "false" }, + { "variable": "------------------ Border ------------------", "value": "false" }, + { "variable": "---------------- Letterbox -----------------", "value": "false" }, + + { "variable": "Enable Grain", "value": "false" }, + { "variable": "|- Grain - Enable", "value": "false" }, + { "variable": "|- Grain: Enable", "value": "false" }, + { "variable": "| Enable Grain", "value": "false" }, + { "variable": "| - Enable Grain", "value": "false" }, + { "variable": "Grain Enable", "value": "false" }, + { "variable": "KiGrain.|- Enable", "value": "false" }, + { "variable": "Distortion.GRAIN.Enable Grain", "value": "false" }, + + { "variable": "Enable Vignette", "value": "false" }, + { "variable": "|- Vignette - Enable", "value": "false" }, + { "variable": "|- Vignette: Enable", "value": "false" }, + { "variable": "V: Enable Vignette", "value": "false" }, + { "variable": "Vignette Enable", "value": "false" }, + { "variable": "Vignette enabled", "value": "false" }, + { "variable": "EnableVignette", "value": "false" }, + { "variable": "NE: Vignette Enable", "value": "false" }, + { "variable": "Vignette.|- Enable", "value": "false" }, + { "variable": "Enable : Vignette Effect", "value": "false" }, + { "variable": "Film Effect.Vignette.Enable Vignette", "value": "false" }, + { "variable": "Blur.Enable : Vignette Effect", "value": "false" }, + { "variable": "|- Blur - Vignette - Enable", "value": "false" }, + + { "variable": "|- Enable Chromatic Aberration", "value": "false" }, + { "variable": "|- CA - Enable Chromatic Aberration", "value": "false" }, + { "variable": "| Enable Chromatic Aberration", "value": "false" }, + { "variable": "EnableChromaticAberration", "value": "false" }, + { "variable": "Chromatic Aberration enabled", "value": "false" }, + { "variable": "Chromatic Aberration.|- Enable Chromatic Aberration", "value": "false" }, + { "variable": "Distortion.ChromaticAberration.Enable C.A", "value": "false" }, + + { "variable": "Enable Dither", "value": "false" }, + { "variable": "Enable Dithering", "value": "false" }, + { "variable": "Dither.|- Enable", "value": "false" }, + { "variable": "Dither.|- Enable (screen out)", "value": "false" }, + + { "variable": "| Enable Lens Distortion", "value": "false" }, + + { "variable": "Enable SMAA", "value": "false" }, + { "variable": "Use SMAA", "value": "false" }, + { "variable": "SMAA", "value": "false" }, + { "variable": "SMAAby kingeric1992", "value": "false" }, + { "variable": "Disable SMAA", "value": "true" }, + { "variable": "|- AA - Enable FXAA", "value": "false" }, + { "variable": "|- AA - Enable SMAA", "value": "false" }, + { "variable": "|- AA - Enable CREAA", "value": "false" }, + { "variable": "---------- FXAA: Use FXAA ----------", "value": "false" }, + { "variable": "ANTIALIASING.SMAA.Enable SMAA", "value": "false" }, + { "variable": "ANTIALIASING.CREAA.Enable CREAA", "value": "false" } + ] + }, + { + "file": "enblens.fx", + "patches": [ + { "variable": "|- Enable Chromatic Aberration", "value": "false" }, + { "variable": "Chromatic Aberration.|- Enable Chromatic Aberration", "value": "false" } + ] + }, + { + "file": "enbeffect.fx", + "patches": [ + { "variable": "Enable Letterbox", "value": "false" }, + { "variable": "Letterbox", "value": "0.0" }, + { "variable": "Sharpening", "value": "0" }, + { "variable": "---- EENE Blur ----", "value": "false" }, + { "variable": "---- EENE Sharpening ----", "value": "false" }, + { "variable": "------------ Sharpening ------------", "value": "false" } + ] + } +] diff --git a/features/Effect11/Shaders/Effect11/ShaderPatches.json b/features/Effect11/Shaders/Effect11/ShaderPatches.json new file mode 100644 index 0000000000..7f71e8550e --- /dev/null +++ b/features/Effect11/Shaders/Effect11/ShaderPatches.json @@ -0,0 +1,11 @@ +[ + { + "file": "enbeffect.fx", + "patches": [ + { + "find": "lerp( TOD(a), a##_Interior, EInteriorFactor )", + "replace": "(EInteriorFactor ? a##_Interior : TOD(a))" + } + ] + } +] diff --git a/features/Effect11/Shaders/Features/Effect11.ini b/features/Effect11/Shaders/Features/Effect11.ini new file mode 100644 index 0000000000..19f01444dc --- /dev/null +++ b/features/Effect11/Shaders/Features/Effect11.ini @@ -0,0 +1,2 @@ +[Info] +Version = 1-0-0 \ No newline at end of file diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli index c2e6bba23f..6992fd8677 100644 --- a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli @@ -62,13 +62,14 @@ namespace ExponentialHeightFog float expFogFactor = saturate(exp2(-exponentialHeightLineIntegral)); float3 fogInscatteringColor = fogColor * SharedData::exponentialHeightFogSettings.originalFogColorAmount; + fogInscatteringColor += SharedData::exponentialHeightFogSettings.fogInscatteringColor.rgb * SharedData::exponentialHeightFogSettings.fogInscatteringColor.a; #if defined(DYNAMIC_CUBEMAPS) - if (SharedData::exponentialHeightFogSettings.useDynamicCubemaps > 0) { - float3 cubemapColor = DynamicCubemaps::EnvReflectionsTexture.SampleLevel(SampColorSampler, normalize(lerp(positionWS, float3(0, 0, 1), saturate((SharedData::exponentialHeightFogSettings.cubemapMipLevel + 1) / 8))), SharedData::exponentialHeightFogSettings.cubemapMipLevel).xyz; - fogInscatteringColor += cubemapColor * SharedData::exponentialHeightFogSettings.inscatteringTint.rgb * SharedData::exponentialHeightFogSettings.inscatteringTint.a; - } + if (SharedData::exponentialHeightFogSettings.useDynamicCubemaps > 0) { + float3 cubemapColor = DynamicCubemaps::EnvReflectionsTexture.SampleLevel(SampColorSampler, normalize(lerp(positionWS, float3(0, 0, 1), saturate((SharedData::exponentialHeightFogSettings.cubemapMipLevel + 1) / 8))), SharedData::exponentialHeightFogSettings.cubemapMipLevel).xyz; + fogInscatteringColor += cubemapColor * SharedData::exponentialHeightFogSettings.inscatteringTint.rgb * SharedData::exponentialHeightFogSettings.inscatteringTint.a; + } #endif fogColor = fogInscatteringColor * (1.0f - expFogFactor); diff --git a/features/Grass Collision/Shaders/Features/GrassCollision.ini b/features/Grass Collision/Shaders/Features/GrassCollision.ini index 060fc3ead0..0c77b1a61f 100644 --- a/features/Grass Collision/Shaders/Features/GrassCollision.ini +++ b/features/Grass Collision/Shaders/Features/GrassCollision.ini @@ -1,5 +1,5 @@ [Info] -Version = 3-0-4 +Version = 3-1-0 [Nexus] nexusmodid = 87816 diff --git a/features/Grass Lighting/Shaders/GrassLighting/GrassLighting.hlsli b/features/Grass Lighting/Shaders/GrassLighting/GrassLighting.hlsli index f85808a56d..1e6f310baa 100644 --- a/features/Grass Lighting/Shaders/GrassLighting/GrassLighting.hlsli +++ b/features/Grass Lighting/Shaders/GrassLighting/GrassLighting.hlsli @@ -4,14 +4,15 @@ namespace GrassLighting { float3 H = normalize(V + L); float HdotN = saturate(dot(H, N)); - + if (SharedData::enbSettings.Enable) + shininess *= SharedData::enbSettings.ColorPow; float lightColorMultiplier = exp2(shininess * log2(HdotN)); - return lightColor * lightColorMultiplier.xxx; + return lightColor * lightColorMultiplier; } float3 TransformNormal(float3 normal) { - return normal * 2 + -1.0.xxx; + return normal * 2.0 - 1.0; } // http://www.thetenthplanet.de/archives/1180 diff --git a/features/HDR Display/Shaders/Features/HDRDisplay.ini b/features/HDR Display/Shaders/Features/HDRDisplay.ini index b5d2506d9e..a8f2e7bc43 100644 --- a/features/HDR Display/Shaders/Features/HDRDisplay.ini +++ b/features/HDR Display/Shaders/Features/HDRDisplay.ini @@ -1,5 +1,5 @@ [Info] -Version = 1-0-1 +Version = 1-1-0 [Nexus] nexusmodid = 179371 diff --git a/features/HDR Display/Shaders/HDRDisplay/HDROutputCS.hlsl b/features/HDR Display/Shaders/HDRDisplay/HDROutputCS.hlsl index 892a4ba94c..065f33723a 100644 --- a/features/HDR Display/Shaders/HDRDisplay/HDROutputCS.hlsl +++ b/features/HDR Display/Shaders/HDRDisplay/HDROutputCS.hlsl @@ -4,6 +4,7 @@ */ #include "Common/Color.hlsli" +#include "Common/DisplayMapping.hlsli" #include "Common/SharedData.hlsli" Texture2D SceneTex : register(t0); @@ -38,6 +39,11 @@ cbuffer PerFrame : register(b0) if (hdrEnabled) { bool sceneIsLinear = isSceneLinear > 0.5; + + float3 outputColor = sceneIsLinear ? scene.xyz : Color::GammaToLinearSafe(scene.xyz); + outputColor = DisplayMapping::PumboAutoHDR(outputColor, SharedData::HDRData.z, SharedData::HDRData.y, 2.75, 1.0); + scene.xyz = sceneIsLinear ? outputColor : Color::LinearToGammaSafe(outputColor); + float3 compositedColorLinear; if (sceneIsLinear) { diff --git a/features/Hair Specular/Shaders/Features/HairSpecular.ini b/features/Hair Specular/Shaders/Features/HairSpecular.ini index 04cf130b39..c2d14c26d2 100644 --- a/features/Hair Specular/Shaders/Features/HairSpecular.ini +++ b/features/Hair Specular/Shaders/Features/HairSpecular.ini @@ -1,5 +1,5 @@ [Info] -Version = 1-1-0 +Version = 1-1-1 [Nexus] nexusmodid = 149011 diff --git a/features/Hair Specular/Shaders/Hair/Hair.hlsli b/features/Hair Specular/Shaders/Hair/Hair.hlsli index d7d1ea07bb..d9deeab7df 100644 --- a/features/Hair Specular/Shaders/Hair/Hair.hlsli +++ b/features/Hair Specular/Shaders/Hair/Hair.hlsli @@ -261,6 +261,7 @@ namespace Hair return saturate(lerp(float3(luminance, luminance, luminance), color, saturation)); } +#if defined(PSHADER) float HairSelfShadow(float3 positionWS, float3 lightDirWS, float noise, uint eyeIndex) { if (!SharedData::hairSpecularSettings.EnableSelfShadow) { @@ -297,5 +298,6 @@ namespace Hair } return lerp(1.0, shadow, SharedData::hairSpecularSettings.SelfShadowStrength); } +#endif } #endif //__HAIR_DEPENDENCY_HLSL__ diff --git a/features/IBL/Shaders/Features/ImageBasedLighting.ini b/features/IBL/Shaders/Features/ImageBasedLighting.ini index 0bc0292971..9e325f8475 100644 --- a/features/IBL/Shaders/Features/ImageBasedLighting.ini +++ b/features/IBL/Shaders/Features/ImageBasedLighting.ini @@ -1,5 +1,5 @@ [Info] -Version = 1-1-0 +Version = 1-2-0 [Nexus] autoupload = false diff --git a/features/IBL/Shaders/IBL/DiffuseIBLCS.hlsl b/features/IBL/Shaders/IBL/DiffuseIBLCS.hlsl index 7432e22d45..5a3602e17f 100644 --- a/features/IBL/Shaders/IBL/DiffuseIBLCS.hlsl +++ b/features/IBL/Shaders/IBL/DiffuseIBLCS.hlsl @@ -33,9 +33,9 @@ groupshared sh2 sharedB[TOTAL_SAMPLES]; float2 sampleCoord = (float2(az, ze) + 0.5) * rcpAxisSampleCount; float3 rayDir = SphericalHarmonics::GetUniformSphereSample(sampleCoord.x, sampleCoord.y); - // Sample cubemap with optimized direction + // Sample cubemap float3 color = EnvTexture.SampleLevel(LinearSampler, -rayDir, 0).xyz; - + // Compute spherical harmonics basis for this direction sh2 sh = SphericalHarmonics::Evaluate(rayDir); @@ -69,4 +69,4 @@ groupshared sh2 sharedB[TOTAL_SAMPLES]; IBLTexture[int2(1, 0)] = sharedG[0]; IBLTexture[int2(2, 0)] = sharedB[0]; } -} \ No newline at end of file +} diff --git a/features/IBL/Shaders/IBL/IBL.hlsli b/features/IBL/Shaders/IBL/IBL.hlsli index e5dcb0f787..f8805ccf39 100644 --- a/features/IBL/Shaders/IBL/IBL.hlsli +++ b/features/IBL/Shaders/IBL/IBL.hlsli @@ -29,10 +29,10 @@ namespace ImageBasedLighting sh2 shR = EnvIBLTexture.Load(int3(0, 0, 0)); sh2 shG = EnvIBLTexture.Load(int3(1, 0, 0)); sh2 shB = EnvIBLTexture.Load(int3(2, 0, 0)); - float colorR = SphericalHarmonics::SHHallucinateZH3Irradiance(shR, rayDir); - float colorG = SphericalHarmonics::SHHallucinateZH3Irradiance(shG, rayDir); - float colorB = SphericalHarmonics::SHHallucinateZH3Irradiance(shB, rayDir); - return float3(colorR, colorG, colorB) / Math::PI; + float colorR = SphericalHarmonics::Unproject(shR, rayDir); + float colorG = SphericalHarmonics::Unproject(shG, rayDir); + float colorB = SphericalHarmonics::Unproject(shB, rayDir); + return float3(colorR, colorG, colorB); } /// Get Sky-only IBL color from game's native reflections cubemap SH @@ -41,15 +41,10 @@ namespace ImageBasedLighting sh2 shR = SkyIBLTexture.Load(int3(0, 0, 0)); sh2 shG = SkyIBLTexture.Load(int3(1, 0, 0)); sh2 shB = SkyIBLTexture.Load(int3(2, 0, 0)); - float colorR = SphericalHarmonics::SHHallucinateZH3Irradiance(shR, rayDir); - float colorG = SphericalHarmonics::SHHallucinateZH3Irradiance(shG, rayDir); - float colorB = SphericalHarmonics::SHHallucinateZH3Irradiance(shB, rayDir); - return max(0, float3(colorR, colorG, colorB) / Math::PI); - } - - float3 GetSkyIBLOccluded(float3 rayDir, float visibility) - { - return GetSkyIBL(rayDir) * visibility; + float colorR = SphericalHarmonics::Unproject(shR, rayDir); + float colorG = SphericalHarmonics::Unproject(shG, rayDir); + float colorB = SphericalHarmonics::Unproject(shB, rayDir); + return max(0, float3(colorR, colorG, colorB)); } // ============================================================================ @@ -65,10 +60,10 @@ namespace ImageBasedLighting sh2 iblSHG = EnvIBLTexture.Load(int3(1, 0, 0)); sh2 iblSHB = EnvIBLTexture.Load(int3(2, 0, 0)); - float colorR = SphericalHarmonics::SHHallucinateZH3Irradiance(iblSHR, float3(0, 0, 0)); - float colorG = SphericalHarmonics::SHHallucinateZH3Irradiance(iblSHG, float3(0, 0, 0)); - float colorB = SphericalHarmonics::SHHallucinateZH3Irradiance(iblSHB, float3(0, 0, 0)); - float3 ibl0 = max(0, float3(colorR, colorG, colorB) / Math::PI); + float colorR = SphericalHarmonics::Unproject(iblSHR, float3(0, 0, 0)); + float colorG = SphericalHarmonics::Unproject(iblSHG, float3(0, 0, 0)); + float colorB = SphericalHarmonics::Unproject(iblSHB, float3(0, 0, 0)); + float3 ibl0 = max(0, float3(colorR, colorG, colorB)); if (SharedData::iblSettings.DALCMode == 1) { float3 ratio = dalc0 / max(ibl0, 0.001); @@ -96,11 +91,6 @@ namespace ImageBasedLighting return Color::Saturation(GetSkyIBL(rayDir), SharedData::iblSettings.SkyIBLSaturation) * SharedData::iblSettings.SkyIBLScale; } - float3 GetSkyIBLColorOccluded(float3 rayDir, float visibility) - { - return Color::Saturation(GetSkyIBLOccluded(rayDir, visibility), SharedData::iblSettings.SkyIBLSaturation) * SharedData::iblSettings.SkyIBLScale; - } - // ============================================================================ // High-level: compute the full diffuse ambient replacement // ============================================================================ @@ -110,48 +100,15 @@ namespace ImageBasedLighting { float3 linEnv, linSky; if (SharedData::iblSettings.DALCMode >= 2) { - linEnv = Color::IrradianceToLinear(vanillaDALC * SharedData::iblSettings.DALCAmount); + linEnv = vanillaDALC * SharedData::iblSettings.DALCAmount; linSky = GetSkyIBLColor(rayDir); } else { linEnv = GetEnvIBLColor(rayDir); linSky = GetSkyIBLColor(rayDir); } - return Color::IrradianceToGamma(linEnv + linSky); - } - - /// Compute diffuse IBL ambient (gamma-space) with visibility applied per DALCMode. - /// visibility: scalar skylighting factor (already computed in Lighting.hlsl). - float3 GetDiffuseIBLOccluded(float3 vanillaDALC, float3 rayDir, float visibility) - { - float3 linEnv, linSky; - if (SharedData::iblSettings.DALCMode == 3) { - // Mode 3: Skylighting dims both DALC and sky - linEnv = Color::IrradianceToLinear(vanillaDALC * SharedData::iblSettings.DALCAmount) * visibility; - linSky = GetSkyIBLColorOccluded(rayDir, visibility); - } else if (SharedData::iblSettings.DALCMode == 2) { - // Mode 2: Skylighting only dims sky, DALC unaffected - linEnv = Color::IrradianceToLinear(vanillaDALC * SharedData::iblSettings.DALCAmount); - linSky = GetSkyIBLColorOccluded(rayDir, visibility); - } else { - // Mode 0/1: Skylighting only dims sky, env IBL unaffected - linEnv = GetEnvIBLColor(rayDir); - linSky = GetSkyIBLColorOccluded(rayDir, visibility); - } - return Color::IrradianceToGamma(linEnv + linSky); - } - - // ============================================================================ - // Convenience: combined IBL (for simple contexts) - // ============================================================================ - - float3 GetIBLColor(float3 rayDir) - { - return GetEnvIBLColor(rayDir) + GetSkyIBLColor(rayDir); - } - - float3 GetIBLColorOccluded(float3 rayDir, float visibility) - { - return GetEnvIBLColor(rayDir) + GetSkyIBLColorOccluded(rayDir, visibility); + if (SharedData::enbSettings.Enable) + linSky *= saturate(-rayDir.z * 0.65 + 0.35); + return linEnv + linSky; } #if defined(LIGHTING) @@ -166,7 +123,7 @@ namespace ImageBasedLighting float3 iblColor; if (SharedData::iblSettings.DALCMode >= 2) { float3 dalc0 = Color::Ambient(SharedData::GetAmbient(0.f)); - iblColor = Color::IrradianceToLinear(dalc0 * SharedData::iblSettings.DALCAmount) + GetSkyIBLColor(float3(0, 0, 0)); + iblColor = dalc0 * SharedData::iblSettings.DALCAmount + GetSkyIBLColor(float3(0, 0, 0)); } else { iblColor = GetEnvIBLColor(float3(0, 0, 0)) + GetSkyIBLColor(float3(0, 0, 0)); } diff --git a/features/Light Limit Fix/Shaders/Features/LightLimitFix.ini b/features/Light Limit Fix/Shaders/Features/LightLimitFix.ini index 7b3c6c9135..21a23ad267 100644 --- a/features/Light Limit Fix/Shaders/Features/LightLimitFix.ini +++ b/features/Light Limit Fix/Shaders/Features/LightLimitFix.ini @@ -1,5 +1,5 @@ [Info] -Version = 3-0-3 +Version = 3-1-0 [Nexus] autoupload = false diff --git a/features/Linear Lighting/Shaders/Features/LinearLighting.ini b/features/Linear Lighting/Shaders/Features/LinearLighting.ini index d4094c367b..9e325f8475 100644 --- a/features/Linear Lighting/Shaders/Features/LinearLighting.ini +++ b/features/Linear Lighting/Shaders/Features/LinearLighting.ini @@ -1,5 +1,5 @@ [Info] -Version = 1-1-1 +Version = 1-2-0 [Nexus] autoupload = false diff --git a/features/Performance Overlay/Shaders/Features/PerformanceOverlay.ini b/features/Performance Overlay/Shaders/Features/PerformanceOverlay.ini index 0bc0292971..9e325f8475 100644 --- a/features/Performance Overlay/Shaders/Features/PerformanceOverlay.ini +++ b/features/Performance Overlay/Shaders/Features/PerformanceOverlay.ini @@ -1,5 +1,5 @@ [Info] -Version = 1-1-0 +Version = 1-2-0 [Nexus] autoupload = false diff --git a/features/Screen Space GI/Shaders/Features/ScreenSpaceGI.ini b/features/Screen Space GI/Shaders/Features/ScreenSpaceGI.ini index 0accf2f262..5f19c243ea 100644 --- a/features/Screen Space GI/Shaders/Features/ScreenSpaceGI.ini +++ b/features/Screen Space GI/Shaders/Features/ScreenSpaceGI.ini @@ -1,5 +1,5 @@ [Info] -Version = 4-1-0 +Version = 4-2-0 [Nexus] nexusmodid = 130375 diff --git a/features/Sky Sync/Shaders/Features/SkySync.ini b/features/Sky Sync/Shaders/Features/SkySync.ini index a59a9fffa3..8c5e0b48c3 100644 --- a/features/Sky Sync/Shaders/Features/SkySync.ini +++ b/features/Sky Sync/Shaders/Features/SkySync.ini @@ -1,5 +1,5 @@ [Info] -Version = 1-1-0 +Version = 1-2-0 [Nexus] nexusmodid = 153543 diff --git a/features/Skylighting/Shaders/Features/Skylighting.ini b/features/Skylighting/Shaders/Features/Skylighting.ini index 5611859742..5ccefced64 100644 --- a/features/Skylighting/Shaders/Features/Skylighting.ini +++ b/features/Skylighting/Shaders/Features/Skylighting.ini @@ -1,5 +1,5 @@ [Info] -Version = 1-3-0 +Version = 1-4-0 [Nexus] nexusmodid = 139352 diff --git a/features/Skylighting/Shaders/Skylighting/Skylighting.hlsli b/features/Skylighting/Shaders/Skylighting/Skylighting.hlsli index fbbe1fe448..b1e6869cd0 100644 --- a/features/Skylighting/Shaders/Skylighting/Skylighting.hlsli +++ b/features/Skylighting/Shaders/Skylighting/Skylighting.hlsli @@ -2,6 +2,7 @@ #define __SKYLIGHTING_DEPENDENCY_HLSL__ #include "Common/Math.hlsli" +#include "Common/Random.hlsli" #include "Common/Shading.hlsli" #include "Common/SharedData.hlsli" #include "Common/Spherical Harmonics/SphericalHarmonics.hlsli" @@ -14,6 +15,10 @@ namespace Skylighting Texture3D SkylightingProbeArray : register(t50); #endif +#if defined(PSHADER) + Texture3D ShadowVisibilityProbeArray : register(t53); +#endif + const static sh2 UNIT_SH = float4(sqrt(4.0 * Math::PI), 0, 0, 0); const static uint3 ARRAY_DIM = uint3(256, 256, 128); @@ -75,7 +80,7 @@ namespace Skylighting #endif #if defined(PSHADER) || defined(SKYLIGHTING_PROBE_REGISTER) - sh2 Sample(float3 positionMS, float3 normalWS) + sh2 Sample(float3 positionMS, float3 normalWS, float2 screenPosition) { sh2 scaledUnitSH = UNIT_SH / 1e-10; @@ -84,6 +89,11 @@ namespace Skylighting positionMS.xyz += normalWS * CELL_SIZE * 0.5; // Receiver normal bias + if (SharedData::FrameCount) { + float3 offset = float3(Random::pcg3d(uint3(screenPosition.xy, SharedData::FrameCount))) / 4294967295.0 * 2.0 - 1.0; + positionMS.xyz += offset * CELL_SIZE * 0.5; + } + float3 positionMSAdjusted = positionMS - SharedData::skylightingSettings.PosOffset.xyz; float3 uvw = positionMSAdjusted / ARRAY_SIZE + .5; @@ -125,26 +135,20 @@ namespace Skylighting return SphericalHarmonics::Scale(sum, rcp(wsum + EPSILON_WEIGHT_SUM)); } - // Compute skylighting diffuse for a receiver biased to face upward (grass/foliage). - // The result is pre-divided by vertexAO so that a subsequent multiply by vertexAO - // yields min(skylightingDiffuse, vertexAO). Pass vertexAO = 1 to skip this compensation. - float GetVertexSkylightingDiffuse(float3 positionMS, float3 normalWS, float vertexAO) + float GetSkylightingDiffuse(sh2 skylightingSH, float3 positionMS, float3 evalNormal, float vertexAO = 1.0) { if (SharedData::InInterior) return 1.0; + float3 biasedNormal = normalize(float3(evalNormal.xy, max(0.0, evalNormal.z))); float fadeOutFactor = GetFadeOutFactor(positionMS); - - float3 biasedNormal = normalWS; - biasedNormal.z = max(0.0, biasedNormal.z); - biasedNormal = normalize(biasedNormal); - - sh2 skylightingSH = Sample(positionMS, normalWS); float skylightingDiffuse = EvaluateDiffuse(skylightingSH, biasedNormal, fadeOutFactor); - return saturate(skylightingDiffuse / max(vertexAO, 1e-5)); + return saturate(skylightingDiffuse / max(vertexAO, EPSILON_DIVISION)); } + + sh2 SampleNoBias(float3 positionMS) { sh2 scaledUnitSH = UNIT_SH / 1e-10; @@ -190,6 +194,55 @@ namespace Skylighting return SphericalHarmonics::Scale(sum, rcp(wsum + EPSILON_WEIGHT_SUM)); } #endif + +#if defined(PSHADER) + float SampleShadowVisibility(float3 positionMS, float3 normalWS, float2 screenPosition) + { + if (SharedData::InInterior) + return 1.0; + + positionMS.xyz += normalWS * CELL_SIZE * 0.5; + + if (SharedData::FrameCount) { + float3 offset = float3(Random::pcg3d(uint3(screenPosition.xy, SharedData::FrameCount))) / 4294967295.0 * 2.0 - 1.0; + positionMS.xyz += offset * CELL_SIZE * 0.5; + } + + float3 positionMSAdjusted = positionMS - SharedData::skylightingSettings.PosOffset.xyz; + float3 uvw = positionMSAdjusted / ARRAY_SIZE + .5; + + if (any(uvw < 0) || any(uvw > 1)) + return 1.0; + + float3 cellVxCoord = uvw * ARRAY_DIM; + int3 cell000 = floor(cellVxCoord - 0.5); + float3 trilinearPos = cellVxCoord - 0.5 - cell000; + + float sum = 0; + float wsum = 0; + [unroll] for (int i = 0; i < 2; i++) + [unroll] for (int j = 0; j < 2; j++) + [unroll] for (int k = 0; k < 2; k++) + { + int3 offset = int3(i, j, k); + int3 cellID = cell000 + offset; + + if (any(cellID < 0) || any((uint3)cellID >= ARRAY_DIM)) + continue; + + float3 trilinearWeights = 1 - abs(offset - trilinearPos); + float w = trilinearWeights.x * trilinearWeights.y * trilinearWeights.z; + + uint3 cellTexID = (cellID + SharedData::skylightingSettings.ArrayOrigin.xyz) % ARRAY_DIM; + sum += ShadowVisibilityProbeArray[cellTexID] * w; + wsum += w; + } + + float fadeOut = GetFadeOutFactor(positionMS); + float shadow = sum / max(wsum, 0.0001); + return lerp(1.0, shadow, fadeOut); + } +#endif } #endif diff --git a/features/Skylighting/Shaders/Skylighting/UpdateProbesCS.hlsl b/features/Skylighting/Shaders/Skylighting/UpdateProbesCS.hlsl index c2906f4619..6402287907 100644 --- a/features/Skylighting/Shaders/Skylighting/UpdateProbesCS.hlsl +++ b/features/Skylighting/Shaders/Skylighting/UpdateProbesCS.hlsl @@ -1,13 +1,62 @@ +#include "Common/FrameBuffer.hlsli" #include "Common/Math.hlsli" #include "Skylighting/Skylighting.hlsli" Texture2D srcOcclusionDepth : register(t0); +Texture2DArray ShadowCascadeMap : register(t1); + +struct DirectionalShadowLightData +{ + column_major float4x4 ShadowProj[2]; + column_major float4x4 InvShadowProj[2]; + float2 EndSplitDistances; + float2 StartSplitDistances; + float4 CascadeDepthParams; +}; +StructuredBuffer DirectionalShadowLights : register(t2); RWTexture3D outProbeArray : register(u0); RWTexture3D outAccumFramesArray : register(u1); +RWTexture3D outShadowBitmask : register(u2); +RWTexture3D outShadowVisibility : register(u3); SamplerComparisonState comparisonSampler : register(s0); +static const float3 noise3D[32] = { + float3(0.247, -0.583, 0.891), + float3(-0.672, 0.315, -0.428), + float3(0.934, 0.762, -0.153), + float3(-0.391, -0.847, 0.526), + float3(0.618, 0.094, 0.739), + float3(-0.825, -0.271, -0.683), + float3(0.152, 0.968, 0.347), + float3(0.503, -0.714, -0.592), + float3(-0.436, 0.629, 0.814), + float3(0.887, -0.198, 0.461), + float3(-0.759, 0.852, -0.305), + float3(0.321, -0.476, -0.921), + float3(-0.094, 0.543, -0.768), + float3(0.776, 0.418, 0.632), + float3(-0.538, -0.695, 0.279), + float3(0.649, -0.921, 0.186), + float3(-0.913, 0.127, 0.574), + float3(0.285, 0.806, -0.447), + float3(0.471, -0.352, 0.698), + float3(-0.627, -0.194, -0.856), + float3(0.834, 0.591, -0.712), + float3(-0.173, -0.968, -0.421), + float3(0.562, 0.239, -0.785), + float3(-0.745, 0.487, 0.316), + float3(0.108, -0.631, 0.894), + float3(0.926, -0.845, -0.267), + float3(-0.384, 0.712, -0.539), + float3(0.697, 0.163, 0.825), + float3(-0.851, -0.429, 0.641), + float3(0.214, 0.934, 0.372), + float3(0.578, -0.762, -0.614), + float3(-0.469, 0.381, 0.947) +}; + [numthreads(8, 8, 1)] void main(uint3 dtid : SV_DispatchThreadID) { const float fadeInThreshold = 15; const static sh2 unitSH = Skylighting::UNIT_SH; @@ -39,8 +88,60 @@ SamplerComparisonState comparisonSampler : register(s0); outProbeArray[dtid] = occlusionSH; outAccumFramesArray[dtid] = accumFrames; + + // Shadow cascade sampling with bitmask accumulation and Gaussian spatial blur + { + float shadowSample = 1.0; + + if (SharedData::HasDirectionalShadows && !SharedData::InInterior) { + DirectionalShadowLightData shadowData = DirectionalShadowLights[0]; + + uint bitIndex = (accumFrames - 1) % 32; + float3 jitteredMS = cellCentreMS + noise3D[bitIndex] * Skylighting::CELL_SIZE; + + float ndcDepth = FrameBuffer::GetShadowDepth(jitteredMS, 0); + float linearDepth = SharedData::GetScreenDepth(ndcDepth); + + if (linearDepth > 0 && linearDepth < shadowData.EndSplitDistances.y) { + float3 positionWS = jitteredMS + FrameBuffer::CameraPosAdjust[0].xyz; + + float cascadeSelect = saturate((linearDepth - shadowData.StartSplitDistances.y) / + (shadowData.EndSplitDistances.x - shadowData.StartSplitDistances.y)); + uint cascadeIndex = uint(cascadeSelect); + + float3 positionLS = mul(shadowData.ShadowProj[cascadeIndex], float4(positionWS, 1)).xyz; + + if (all(positionLS.xy >= 0) && all(positionLS.xy <= 1)) { + shadowSample = ShadowCascadeMap.SampleCmpLevelZero(comparisonSampler, float3(positionLS.xy, cascadeIndex), positionLS.z); + } + + float fade = saturate(linearDepth / shadowData.EndSplitDistances.y); + float fadeFactor = 1.0 - pow(fade * fade, 8); + shadowSample = lerp(1.0, shadowSample, fadeFactor); + } + + uint bitmask = isValid ? outShadowBitmask[dtid] : 0; + bitmask &= ~(1u << bitIndex); + if (shadowSample > 0.5) + bitmask |= (1u << bitIndex); + + outShadowBitmask[dtid] = bitmask; + + uint validBits = min(accumFrames, 32u); + float shadow = float(countbits(bitmask)) / float(validBits); + shadow = lerp(1.0, shadow, min(fadeInThreshold, accumFrames) / fadeInThreshold); + outShadowVisibility[dtid] = shadow; + } else { + if (!isValid) { + outShadowBitmask[dtid] = 0; + outShadowVisibility[dtid] = 1.0; + } + } + } } else if (!isValid) { outProbeArray[dtid] = unitSH; outAccumFramesArray[dtid] = 0; + outShadowBitmask[dtid] = 0; + outShadowVisibility[dtid] = 1.0; } -} \ No newline at end of file +} diff --git a/features/Subsurface Scattering/Shaders/Features/SubsurfaceScattering.ini b/features/Subsurface Scattering/Shaders/Features/SubsurfaceScattering.ini index cd764c96ce..241fb42a2f 100644 --- a/features/Subsurface Scattering/Shaders/Features/SubsurfaceScattering.ini +++ b/features/Subsurface Scattering/Shaders/Features/SubsurfaceScattering.ini @@ -1,5 +1,5 @@ [Info] -Version = 3-0-2 +Version = 3-1-0 [Nexus] nexusmodid = 114114 diff --git a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli index ea4a94f245..6fb969ff35 100644 --- a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli +++ b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli @@ -58,7 +58,7 @@ float4 BurleyNormalizedSS(uint2 DTid, float2 texCoord, uint eyeIndex, float sssA } float4 surfaceAlbedo = AlbedoTexture[DTid]; - float3 originalColor = Color::IrradianceToLinear(centerColor.xyz / max(surfaceAlbedo.xyz, EPSILON_SSS_ALBEDO)); + float3 originalColor = centerColor.xyz; float4 diffuseMeanFreePath = humanProfile ? MeanFreePathHuman : MeanFreePathBase; diffuseMeanFreePath.xyz = float3(max(diffuseMeanFreePath.x, 1e-5f), max(diffuseMeanFreePath.y, 1e-5f), max(diffuseMeanFreePath.z, 1e-5f)); @@ -112,7 +112,7 @@ float4 BurleyNormalizedSS(uint2 DTid, float2 texCoord, uint eyeIndex, float sssA if (!mask) continue; - float3 sampleColor = Color::IrradianceToLinear(ColorTexture[samplePixcoord].xyz * maskSample / max(AlbedoTexture[samplePixcoord].xyz, EPSILON_SSS_ALBEDO)); + float3 sampleColor = ColorTexture[samplePixcoord].xyz * maskSample; float sampleDepth = SharedData::GetScreenDepth(DepthTexture[samplePixcoord].x); float3 sampleNormalVS = GBuffer::DecodeNormal(NormalTexture[samplePixcoord].xy); float3 sampleNormalWS = normalize(mul(FrameBuffer::CameraViewInverse[eyeIndex], float4(sampleNormalVS, 0)).xyz); @@ -130,8 +130,11 @@ float4 BurleyNormalizedSS(uint2 DTid, float2 texCoord, uint eyeIndex, float sssA colorSum *= any(weightSum == 0.0f) ? 0.0f : (1.0f / weightSum); colorSum = lerp(colorSum, originalColor, saturate(centerWeight)); - float3 color = Color::IrradianceToGamma(colorSum) * AlbedoTexture[DTid.xy].xyz; - color = lerp(centerColor.xyz, color, saturate(sssAmount)); + + float3 albedo = AlbedoTexture[DTid.xy].xyz; + float3 color = SSSApplyAlbedo(Color::IrradianceToGamma(colorSum), albedo, SSS_SCATTER_MODE_POST); + float3 centerColorRestored = SSSApplyAlbedo(Color::IrradianceToGamma(originalColor), albedo, SSS_SCATTER_MODE_POST); + color = lerp(centerColorRestored, color, saturate(sssAmount > 0.0)); float4 outColor = float4(color, ColorTexture[DTid.xy].w); return outColor; diff --git a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/DiffuseExtractionCS.hlsl b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/DiffuseExtractionCS.hlsl new file mode 100644 index 0000000000..c4176fd893 --- /dev/null +++ b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/DiffuseExtractionCS.hlsl @@ -0,0 +1,18 @@ +RWTexture2D OutputRW : register(u0); + +Texture2D ColorTexture : register(t0); +Texture2D AlbedoTexture : register(t3); + +#include "Common/Color.hlsli" +#include "Common/SharedData.hlsli" +#include "SubsurfaceScattering/SSSCommon.hlsli" + +[numthreads(8, 8, 1)] void main(uint3 DTid : SV_DispatchThreadID) { + if (any(DTid.xy >= uint2(SharedData::BufferDim.xy))) + return; + + float4 color = ColorTexture[DTid.xy]; + color.rgb = SSSRemoveAlbedo(color.rgb, AlbedoTexture[DTid.xy].rgb, ScatterMode); + color.rgb = Color::IrradianceToLinear(color.rgb); + OutputRW[DTid.xy] = color; +} diff --git a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SSSCommon.hlsli b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SSSCommon.hlsli new file mode 100644 index 0000000000..7441f34083 --- /dev/null +++ b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SSSCommon.hlsli @@ -0,0 +1,41 @@ +#ifndef SSS_COMMON_HLSLI +#define SSS_COMMON_HLSLI + +#include "Common/Math.hlsli" + +#define SSSS_N_SAMPLES 21 + +#define SSS_SCATTER_MODE_PRE 0 +#define SSS_SCATTER_MODE_POST 1 +#define SSS_SCATTER_MODE_PRE_POST 2 + +cbuffer PerFrameSSS : register(b1) +{ + float4 Kernels[SSSS_N_SAMPLES + SSSS_N_SAMPLES]; + float4 BaseProfile; + float4 HumanProfile; + float SSSS_FOVY; + uint BurleySamples; + uint ScatterMode; + uint pad; + float4 MeanFreePathBase; + float4 MeanFreePathHuman; +}; + +float3 SSSRemoveAlbedo(float3 color, float3 albedo, uint mode) +{ + if (mode == SSS_SCATTER_MODE_PRE) + return color; + float3 divisor = (mode == SSS_SCATTER_MODE_PRE_POST) ? sqrt(albedo) : albedo; + return color / max(divisor, EPSILON_SSS_ALBEDO); +} + +float3 SSSApplyAlbedo(float3 irradiance, float3 albedo, uint mode) +{ + if (mode == SSS_SCATTER_MODE_PRE) + return irradiance; + float3 multiplier = (mode == SSS_SCATTER_MODE_PRE_POST) ? sqrt(albedo) : albedo; + return irradiance * multiplier; +} + +#endif diff --git a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSS.hlsli b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSS.hlsli index 65b89e7def..fee4261748 100644 --- a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSS.hlsli +++ b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSS.hlsli @@ -90,26 +90,27 @@ //----------------------------------------------------------------------------- #include "Common/Math.hlsli" +#include "Common/Random.hlsli" float4 SSSSBlurCS( - uint2 DTid, float2 texcoord, float2 dir, float sssAmount, bool humanProfile) { - // Fetch color of current pixel: - float4 colorM = ColorTexture[DTid.xy]; + // texcoord is in DR-space UVs (coming from DTid / full buffer dim), so it already + // addresses the rendered sub-rectangle of the texture. Non-DR UVs are derived for + // eye-half detection during clamping. + float2 texcoordNonDR = texcoord * FrameBuffer::DynamicResolutionParams2.xy; -#if defined(HORIZONTAL) - colorM.rgb = Color::IrradianceToLinear(colorM.rgb); -#endif + // Input is already linear and albedo-free from the pre-pass + float4 colorM = ColorTexture.SampleLevel(PointSampler, texcoord, 0); if (sssAmount == 0) return colorM; // Fetch linear depth of current pixel: - float depthM = DepthTexture[DTid.xy].r; + float depthM = DepthTexture.SampleLevel(PointSampler, texcoord, 0).r; depthM = SharedData::GetScreenDepth(depthM); float2 profile = humanProfile ? HumanProfile.xy : BaseProfile.xy; @@ -124,38 +125,32 @@ float4 SSSSBlurCS( float scale = distanceToProjectionWindow / depthM; // Calculate the final step to fetch the surrounding pixels: - float2 finalStep = scale * SharedData::BufferDim.xy * dir; + float2 finalStep = scale * dir; finalStep *= sssAmount; finalStep *= profile.x; // Modulate it using the profile - finalStep *= 1.0 / 3.0; // Divide by 3 as the kernels range from -3 to 3. - -#if defined(VR) - finalStep.x *= 0.5; // Halve horizontal screen resolution - uint eyeIndex = texcoord.x >= 0.5; // 0 = left 1 = right - uint bufferDimHalfX = uint(SharedData::BufferDim.x * 0.5); - uint2 minCoord = uint2(eyeIndex ? bufferDimHalfX : 0, 0); - uint2 maxCoord = uint2(eyeIndex ? SharedData::BufferDim.x : bufferDimHalfX, SharedData::BufferDim.y); -#else - uint2 minCoord = uint2(0, 0); - uint2 maxCoord = uint2(SharedData::BufferDim.x, SharedData::BufferDim.y); -#endif + // Scale the step into DR-UV space so blur width in rendered pixels stays consistent. + finalStep *= FrameBuffer::DynamicResolutionParams1.xy; + + // Per-pixel rotation to break separable axis-aligned banding + float jitter = Random::InterleavedGradientNoise(texcoord * SharedData::BufferDim.xy, SharedData::FrameCount) * Math::TAU; + float2x2 rotationMatrix = float2x2(cos(jitter), sin(jitter), -sin(jitter), cos(jitter)); + float2x2 identityMatrix = float2x2(1.0, 0.0, 0.0, 1.0); // Accumulate the other samples: for (uint i = kernelOffset + 1; i < kernelOffset + SSSS_N_SAMPLES; i++) { float2 offset = Kernels[i].a * finalStep; - uint2 coords = DTid.xy + int2(offset + 0.5); - - // Clamp for dynamic resolution - coords = clamp(coords, minCoord, maxCoord); + // Apply randomized rotation + offset = mul(offset, rotationMatrix); - float3 color = ColorTexture[coords].rgb; + float2 sampleCoord = texcoord + offset; + + // Clamp to the DR-rendered region (per-eye in VR) to avoid sampling outside it. + sampleCoord = FrameBuffer::ClampDynamicResolutionAdjustedScreenPosition(sampleCoord, texcoordNonDR); -#if defined(HORIZONTAL) - color.rgb = Color::IrradianceToLinear(color.rgb); -#endif + float3 color = ColorTexture.SampleLevel(PointSampler, sampleCoord, 0).rgb; - float depth = DepthTexture[coords].r; + float depth = DepthTexture.SampleLevel(PointSampler, sampleCoord, 0).r; depth = SharedData::GetScreenDepth(depth); // If the difference in depth is huge, we lerp color back to "colorM": diff --git a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSSCS.hlsl b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSSCS.hlsl index f4a955f504..e6457a05af 100644 --- a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSSCS.hlsl +++ b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSSCS.hlsl @@ -6,23 +6,12 @@ Texture2D MaskTexture : register(t2); Texture2D AlbedoTexture : register(t3); Texture2D NormalTexture : register(t4); -#define SSSS_N_SAMPLES 21 - -cbuffer PerFrameSSS : register(b1) -{ - float4 Kernels[SSSS_N_SAMPLES + SSSS_N_SAMPLES]; - float4 BaseProfile; - float4 HumanProfile; - float SSSS_FOVY; - uint BurleySamples; - uint2 pad; - float4 MeanFreePathBase; - float4 MeanFreePathHuman; -}; +SamplerState PointSampler : register(s0); #include "Common/Color.hlsli" #include "Common/Random.hlsli" #include "Common/SharedData.hlsli" +#include "SubsurfaceScattering/SSSCommon.hlsli" #if defined(BURLEY) # include "SubsurfaceScattering/Burley.hlsli" @@ -41,17 +30,20 @@ cbuffer PerFrameSSS : register(b1) #if defined(BURLEY) float sssAmount = MaskTexture[DTid.xy].x; - bool humanProfile = MaskTexture[DTid.xy].y > 0.0; - float4 color = BurleyNormalizedSS(DTid.xy, texCoord, eyeIndex, sssAmount, humanProfile); - SSSRW[DTid.xy] = max(0, color); + if (sssAmount > 0.0) { + bool humanProfile = MaskTexture[DTid.xy].y > 0.0; + + float4 color = BurleyNormalizedSS(DTid.xy, texCoord, eyeIndex, sssAmount, humanProfile); + SSSRW[DTid.xy] = max(0, color); + } #elif defined(HORIZONTAL) float sssAmount = MaskTexture[DTid.xy].x; bool humanProfile = MaskTexture[DTid.xy].y > 0.0; - float4 color = SSSSBlurCS(DTid.xy, texCoord, float2(1.0, 0.0), sssAmount, humanProfile); + float4 color = SSSSBlurCS(texCoord, float2(1.0, 0.0), sssAmount, humanProfile); SSSRW[DTid.xy] = max(0, color); #else @@ -61,8 +53,9 @@ cbuffer PerFrameSSS : register(b1) if (sssAmount > 0.0) { bool humanProfile = MaskTexture[DTid.xy].y > 0.0; - float4 color = SSSSBlurCS(DTid.xy, texCoord, float2(0.0, 1.0), sssAmount, humanProfile); + float4 color = SSSSBlurCS(texCoord, float2(0.0, 1.0), sssAmount, humanProfile); color.rgb = Color::IrradianceToGamma(color.rgb); + color.rgb = SSSApplyAlbedo(color.rgb, AlbedoTexture[DTid.xy].rgb, ScatterMode); SSSRW[DTid.xy] = float4(color.rgb, 1.0); } diff --git a/features/Terrain Blending/Shaders/Features/TerrainBlending.ini b/features/Terrain Blending/Shaders/Features/TerrainBlending.ini index e28d64a619..df0412c758 100644 --- a/features/Terrain Blending/Shaders/Features/TerrainBlending.ini +++ b/features/Terrain Blending/Shaders/Features/TerrainBlending.ini @@ -1,5 +1,5 @@ [Info] -Version = 1-1-0 +Version = 1-2-0 [Nexus] nexusmodid = 157076 diff --git a/features/Terrain Shadows/Shaders/Features/TerrainShadows.ini b/features/Terrain Shadows/Shaders/Features/TerrainShadows.ini index 0bc0292971..9e325f8475 100644 --- a/features/Terrain Shadows/Shaders/Features/TerrainShadows.ini +++ b/features/Terrain Shadows/Shaders/Features/TerrainShadows.ini @@ -1,5 +1,5 @@ [Info] -Version = 1-1-0 +Version = 1-2-0 [Nexus] autoupload = false diff --git a/features/Volumetric Shadows/Shaders/Features/VolumetricShadows.ini b/features/Volumetric Shadows/Shaders/Features/VolumetricShadows.ini index e9d66d302c..14a4f48fa2 100644 --- a/features/Volumetric Shadows/Shaders/Features/VolumetricShadows.ini +++ b/features/Volumetric Shadows/Shaders/Features/VolumetricShadows.ini @@ -1,5 +1,5 @@ [Info] -Version = 2-0-1 +Version = 2-1-0 [Nexus] autoupload = false diff --git a/features/Volumetric Shadows/Shaders/VolumetricShadows/BlurShadowCS.hlsl b/features/Volumetric Shadows/Shaders/VolumetricShadows/BlurShadowCS.hlsl index 266cd01fcc..1254ed085f 100644 --- a/features/Volumetric Shadows/Shaders/VolumetricShadows/BlurShadowCS.hlsl +++ b/features/Volumetric Shadows/Shaders/VolumetricShadows/BlurShadowCS.hlsl @@ -1,33 +1,29 @@ -// 11x11 separable Gaussian blur for VSM shadow map +// Separable Gaussian blur for shadow map moments // BLUR_HORIZONTAL - horizontal pass // BLUR_VERTICAL - vertical pass -Texture2D InputTexture : register(t0); -RWTexture2D OutputTexture : register(u0); - -// Gaussian weights for 11-tap kernel (sigma ~= 2.5) -static const float weights[6] = { - 0.198596, // center - 0.175713, // +/- 1 - 0.121703, // +/- 2 - 0.065984, // +/- 3 - 0.028002, // +/- 4 - 0.009302 // +/- 5 +Texture2D InputTexture : register(t0); +RWTexture2D OutputTexture : register(u0); + +cbuffer BlurCB : register(b0) +{ + uint BlurRadius; + uint _pad[3]; }; -#define KERNEL_RADIUS 5 +#define MAX_KERNEL_RADIUS 32 #define GROUP_SIZE 128 // Shared memory for efficient loading -// We need GROUP_SIZE + 2 * KERNEL_RADIUS elements -groupshared float2 g_cache[GROUP_SIZE + 2 * KERNEL_RADIUS]; +// We need GROUP_SIZE + 2 * MAX_KERNEL_RADIUS elements +groupshared float4 g_cache[GROUP_SIZE + 2 * MAX_KERNEL_RADIUS]; #if defined(BLUR_HORIZONTAL) [numthreads(GROUP_SIZE, 1, 1)] void main(uint3 groupID : SV_GroupID, uint3 groupThreadID : SV_GroupThreadID, uint3 dispatchThreadID : SV_DispatchThreadID) { uint width, height; InputTexture.GetDimensions(width, height); - int2 baseCoord = int2(groupID.x * GROUP_SIZE - KERNEL_RADIUS, groupID.y); + int2 baseCoord = int2(groupID.x * GROUP_SIZE - MAX_KERNEL_RADIUS, groupID.y); int localIdx = groupThreadID.x; // Load main data @@ -36,7 +32,7 @@ groupshared float2 g_cache[GROUP_SIZE + 2 * KERNEL_RADIUS]; g_cache[localIdx] = InputTexture[coord]; // Load extra data for kernel overlap - if (localIdx < 2 * KERNEL_RADIUS) { + if (localIdx < 2 * MAX_KERNEL_RADIUS) { coord = baseCoord + int2(GROUP_SIZE + localIdx, 0); coord.x = clamp(coord.x, 0, (int)width - 1); g_cache[GROUP_SIZE + localIdx] = InputTexture[coord]; @@ -48,16 +44,21 @@ groupshared float2 g_cache[GROUP_SIZE + 2 * KERNEL_RADIUS]; if (dispatchThreadID.x >= width || dispatchThreadID.y >= height) return; - // Apply horizontal blur - float2 result = g_cache[localIdx + KERNEL_RADIUS] * weights[0]; + // Apply horizontal blur with dynamic radius + uint radius = min(BlurRadius, (uint)MAX_KERNEL_RADIUS); + float sigma = max(float(radius) * 0.5, 0.5); + float rcpTwoSigma2 = rcp(2.0 * sigma * sigma); + + float4 result = g_cache[localIdx + MAX_KERNEL_RADIUS]; + float totalWeight = 1.0; - [unroll] for (int i = 1; i <= KERNEL_RADIUS; i++) - { - result += g_cache[localIdx + KERNEL_RADIUS - i] * weights[i]; - result += g_cache[localIdx + KERNEL_RADIUS + i] * weights[i]; + for (uint i = 1; i <= radius; i++) { + float w = exp(-float(i * i) * rcpTwoSigma2); + result += (g_cache[localIdx + MAX_KERNEL_RADIUS - i] + g_cache[localIdx + MAX_KERNEL_RADIUS + i]) * w; + totalWeight += 2.0 * w; } - OutputTexture[dispatchThreadID.xy] = result; + OutputTexture[dispatchThreadID.xy] = result * rcp(totalWeight); } #elif defined(BLUR_VERTICAL) @@ -65,7 +66,7 @@ groupshared float2 g_cache[GROUP_SIZE + 2 * KERNEL_RADIUS]; uint width, height; InputTexture.GetDimensions(width, height); - int2 baseCoord = int2(groupID.x, groupID.y * GROUP_SIZE - KERNEL_RADIUS); + int2 baseCoord = int2(groupID.x, groupID.y * GROUP_SIZE - MAX_KERNEL_RADIUS); int localIdx = groupThreadID.y; // Load main data @@ -74,7 +75,7 @@ groupshared float2 g_cache[GROUP_SIZE + 2 * KERNEL_RADIUS]; g_cache[localIdx] = InputTexture[coord]; // Load extra data for kernel overlap - if (localIdx < 2 * KERNEL_RADIUS) { + if (localIdx < 2 * MAX_KERNEL_RADIUS) { coord = baseCoord + int2(0, GROUP_SIZE + localIdx); coord.y = clamp(coord.y, 0, (int)height - 1); g_cache[GROUP_SIZE + localIdx] = InputTexture[coord]; @@ -86,15 +87,20 @@ groupshared float2 g_cache[GROUP_SIZE + 2 * KERNEL_RADIUS]; if (dispatchThreadID.x >= width || dispatchThreadID.y >= height) return; - // Apply vertical blur - float2 result = g_cache[localIdx + KERNEL_RADIUS] * weights[0]; + // Apply vertical blur with dynamic radius + uint radius = min(BlurRadius, (uint)MAX_KERNEL_RADIUS); + float sigma = max(float(radius) * 0.5, 0.5); + float rcpTwoSigma2 = rcp(2.0 * sigma * sigma); + + float4 result = g_cache[localIdx + MAX_KERNEL_RADIUS]; + float totalWeight = 1.0; - [unroll] for (int i = 1; i <= KERNEL_RADIUS; i++) - { - result += g_cache[localIdx + KERNEL_RADIUS - i] * weights[i]; - result += g_cache[localIdx + KERNEL_RADIUS + i] * weights[i]; + for (uint i = 1; i <= radius; i++) { + float w = exp(-float(i * i) * rcpTwoSigma2); + result += (g_cache[localIdx + MAX_KERNEL_RADIUS - i] + g_cache[localIdx + MAX_KERNEL_RADIUS + i]) * w; + totalWeight += 2.0 * w; } - OutputTexture[dispatchThreadID.xy] = result; + OutputTexture[dispatchThreadID.xy] = result * rcp(totalWeight); } #endif diff --git a/features/Volumetric Shadows/Shaders/VolumetricShadows/DownsampleShadowCS.hlsl b/features/Volumetric Shadows/Shaders/VolumetricShadows/DownsampleShadowCS.hlsl index b7f960281e..c0a0ea708d 100644 --- a/features/Volumetric Shadows/Shaders/VolumetricShadows/DownsampleShadowCS.hlsl +++ b/features/Volumetric Shadows/Shaders/VolumetricShadows/DownsampleShadowCS.hlsl @@ -1,19 +1,43 @@ Texture2DArray InputTexture : register(t0); Texture2DArray ESRAMShadow : register(t1); -RWTexture2D OutputTexture : register(u0); +RWTexture2D OutputTexture : register(u0); SamplerState LinearSampler : register(s0); -float2 GetVSMMoments(in float depth) +cbuffer VSMLinearizeCB : register(b0) { - return float2(depth, depth * depth); + float CascadeNear; + float CascadeFar; + float2 _pad; +}; + +float LinearizeDepth(float depth) +{ + float linZ = CascadeNear * CascadeFar / (CascadeFar - depth * (CascadeFar - CascadeNear)); + return (linZ - CascadeNear) / (CascadeFar - CascadeNear); +} + +// Compute 4 power moments with RGBA16 quantization optimization +// Reference: Peters, "Moment Shadow Mapping" (I3D 2015) +float4 GetOptimizedMoments(in float depth) +{ + float d = LinearizeDepth(depth); + float d2 = d * d; + float4 moments = float4(d, d2, d * d2, d2 * d2); + float4 optimized = mul(moments, float4x4( + -2.07224649, 13.7948857237, 0.105877704, 9.7924062118, + 32.23703778, -59.4683975703, -1.9077466311, -33.7652110555, + -68.571074599, 82.0359750338, 9.3496555107, 47.9456096605, + 39.3703274134, -35.364903257, -6.6543490743, -23.9728048165)); + optimized[0] += 0.035955884801; + return optimized; } -float2 ReduceMoments(float2 a, float2 b, float2 c, float2 d) +float4 ReduceMoments(float4 a, float4 b, float4 c, float4 d) { return (a + b + c + d) * 0.25; } -groupshared float2 g_scratchDepths[8][8]; +groupshared float4 g_scratchDepths[8][8]; #if defined(DOWNSAMPLE_SHADOW_MIP0) static const uint CASCADE = 1; @@ -47,12 +71,12 @@ static const uint CASCADE = 0; float4 esramDepths = ESRAMShadow.GatherRed(LinearSampler, float3(uv, CASCADE)); depths = min(depths, esramDepths); - float2 vsmDepth = 0; + float4 msmMoments = 0; for (uint i = 0; i < 4; i++) - vsmDepth += GetVSMMoments(depths[i]); - vsmDepth *= 0.25; + msmMoments += GetOptimizedMoments(depths[i]); + msmMoments *= 0.25; - g_scratchDepths[groupThreadID.x][groupThreadID.y] = vsmDepth; + g_scratchDepths[groupThreadID.x][groupThreadID.y] = msmMoments; GroupMemoryBarrierWithGroupSync(); diff --git a/features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli b/features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli index cdfb339ba2..b6f559bf46 100644 --- a/features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli +++ b/features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli @@ -1,33 +1,77 @@ #ifndef __VOLUMETRIC_SHADOWS_HLSLI__ #define __VOLUMETRIC_SHADOWS_HLSLI__ -// Variance Shadow Maps (VSM) -// Chebyshev's inequality on filtered depth moments - namespace VolumetricShadows { - Texture2D SharedShadowMap : register(t18); + Texture2D SharedShadowMap : register(t18); + + static const float MSM_MOMENT_BIAS = 0.003; - static const float VSM_MIN_VARIANCE = 0.00001; - static const float VSM_BLEEDING_REDUCTION = 0.2; + float LinearizeDepth(float depth, float cascadeNear, float cascadeFar) + { + float linZ = cascadeNear * cascadeFar / (cascadeFar - depth * (cascadeFar - cascadeNear)); + return (linZ - cascadeNear) / (cascadeFar - cascadeNear); + } - // Chebyshev upper bound on P(X >= t) - // moments.x = mean(z), moments.y = mean(z^2) - float ComputeVSM(float2 moments, float depth) + // Inverse RGBA16 quantization: recover power moments from optimized storage + // Reference: Peters, "Moment Shadow Mapping" (I3D 2015) + float4 ConvertOptimizedMoments(float4 optimizedMoments) { - float variance = max(moments.y - moments.x * moments.x, VSM_MIN_VARIANCE); - float d = depth - moments.x; - float pMax = variance / (variance + d * d); - return (depth <= moments.x) ? 1.0 : pMax; + optimizedMoments[0] -= 0.035955884801; + return mul(optimizedMoments, float4x4( + 0.2227744146, 0.1549679261, 0.1451988946, 0.163127443, + 0.0771972861, 0.1394629426, 0.2120202157, 0.2591432266, + 0.7926986636, 0.7963415838, 0.7258694464, 0.6539092497, + 0.0319417555, -0.1722823173, -0.2758014811, -0.3376131734)); } - // Reduces light bleeding by remapping shadow values below a threshold to zero - float ReduceBleeding(float shadow, float amount) + // Hamburger 4-moment shadow reconstruction + // Reference: Peters, "Moment Shadow Mapping" (I3D 2015) + float ComputeMSM(float4 optimizedMoments, float depth) { - return saturate((shadow - amount) / (1.0 - amount)); + float4 b = ConvertOptimizedMoments(optimizedMoments); + + // Bias moments to reduce light bleeding + b = lerp(b, 0.5, MSM_MOMENT_BIAS); + + float3 z; + z[0] = depth; + + // Cholesky factorization of the Hankel matrix + float L32D22 = mad(-b[0], b[1], b[2]); + float D22 = mad(-b[0], b[0], b[1]); + float squaredDepthVariance = mad(-b[1], b[1], b[3]); + float D33D22 = dot(float2(squaredDepthVariance, -L32D22), float2(D22, L32D22)); + float InvD22 = 1.0 / D22; + float L32 = L32D22 * InvD22; + + // Solve for the quadratic polynomial whose roots give the 2-point distribution + float3 c = float3(1.0, z[0], z[0] * z[0]); + c[1] -= b.x; + c[2] -= b.y + L32 * c[1]; + c[1] *= InvD22; + c[2] *= D22 / D33D22; + c[1] -= L32 * c[2]; + c[0] -= dot(c.yz, b.xy); + + float p = c[1] / c[2]; + float q = c[0] / c[2]; + float D = (p * p * 0.25) - q; + float r = sqrt(max(D, 0.0)); + z[1] = -p * 0.5 - r; + z[2] = -p * 0.5 + r; + + // Compute shadow intensity from the 2-point distribution + float4 switchVal = (z[2] < z[0]) ? float4(z[1], z[0], 1.0, 1.0) : + ((z[1] < z[0]) ? float4(z[0], z[1], 0.0, 1.0) : + float4(0.0, 0.0, 0.0, 0.0)); + float quotient = (switchVal[0] * z[2] - b[0] * (switchVal[0] + z[2]) + b[1]) + / ((z[2] - switchVal[1]) * (z[0] - z[1])); + float shadowIntensity = switchVal[2] + switchVal[3] * quotient; + return 1.0 - saturate(shadowIntensity); } - // Sample a single cascade for VSM shadow + // Sample a single cascade for MSM shadow float SampleVSMCascade3D( uint cascadeIndex, float noise, @@ -35,6 +79,8 @@ namespace VolumetricShadows float rcpSampleCount, float3 startPositionLS, float3 endPositionLS, + float cascadeNear, + float cascadeFar, out float firstSample) { float shadow = 0.0; @@ -45,8 +91,9 @@ namespace VolumetricShadows float t = (float(k) + noise) * rcpSampleCount; float3 samplePosLS = lerp(endPositionLS, startPositionLS, t); - float2 moments = SharedShadowMap.SampleLevel(LinearSampler, samplePosLS.xy, 1u - cascadeIndex); - float lit = ComputeVSM(moments, samplePosLS.z); + float4 moments = SharedShadowMap.SampleLevel(LinearSampler, samplePosLS.xy, 1u - cascadeIndex); + float depth = LinearizeDepth(samplePosLS.z, cascadeNear, cascadeFar); + float lit = ComputeMSM(moments, depth); // Last to set firstSample is start position firstSample = lit; @@ -88,6 +135,8 @@ namespace VolumetricShadows uint primaryCascade = uint(cascadeSelect); bool needsBlending = (cascadeSelect > 0.0) && (cascadeSelect < 1.0); + float4 depthParams = directionalShadowLightData.CascadeDepthParams; + // Transform ray to light space for primary cascade float4x4 shadowProj = directionalShadowLightData.ShadowProj[primaryCascade]; float3 startLS = mul(shadowProj, float4(startPosition, 1)).xyz; @@ -95,9 +144,12 @@ namespace VolumetricShadows startLS.xy = saturate(startLS.xy); endLS.xy = saturate(endLS.xy); + float primaryNear = primaryCascade == 0 ? depthParams.x : depthParams.z; + float primaryFar = primaryCascade == 0 ? depthParams.y : depthParams.w; + // Sample primary cascade float primaryFirstSample; - float shadow = SampleVSMCascade3D(primaryCascade, noise, sampleCount, rcpSampleCount, startLS, endLS, primaryFirstSample); + float shadow = SampleVSMCascade3D(primaryCascade, noise, sampleCount, rcpSampleCount, startLS, endLS, primaryNear, primaryFar, primaryFirstSample); surfaceShadow = primaryFirstSample; // Blend with secondary cascade if needed @@ -111,8 +163,11 @@ namespace VolumetricShadows startLS.xy = saturate(startLS.xy); endLS.xy = saturate(endLS.xy); + float secondaryNear = secondaryCascade == 0 ? depthParams.x : depthParams.z; + float secondaryFar = secondaryCascade == 0 ? depthParams.y : depthParams.w; + float secondaryFirstSample; - float shadowBlend = SampleVSMCascade3D(secondaryCascade, noise, sampleCount, rcpSampleCount, startLS, endLS, secondaryFirstSample); + float shadowBlend = SampleVSMCascade3D(secondaryCascade, noise, sampleCount, rcpSampleCount, startLS, endLS, secondaryNear, secondaryFar, secondaryFirstSample); shadow = lerp(shadow, shadowBlend, cascadeSelect); surfaceShadow = lerp(surfaceShadow, secondaryFirstSample, cascadeSelect); } @@ -123,11 +178,12 @@ namespace VolumetricShadows return lerp(1.0, shadow, fadeFactor); } - // Sample a single cascade for VSM shadow (2D point sample) - float SampleVSMCascade2D(uint cascadeIndex, float3 positionLS) + // Sample a single cascade for MSM shadow (2D point sample) + float SampleVSMCascade2D(uint cascadeIndex, float3 positionLS, float cascadeNear, float cascadeFar) { - float2 moments = SharedShadowMap.SampleLevel(LinearSampler, positionLS.xy, 1u - cascadeIndex); - return ComputeVSM(moments, positionLS.z); + float4 moments = SharedShadowMap.SampleLevel(LinearSampler, positionLS.xy, 1u - cascadeIndex); + float depth = LinearizeDepth(positionLS.z, cascadeNear, cascadeFar); + return ComputeMSM(moments, depth); } float GetVSMShadow2D(float3 position, uint eyeIndex, out float detailedShadow) @@ -155,12 +211,17 @@ namespace VolumetricShadows uint primaryCascade = uint(cascadeSelect); bool needsBlending = (cascadeSelect > 0.0) && (cascadeSelect < 1.0); + float4 depthParams = directionalShadowLightData.CascadeDepthParams; + // Transform position to light space for primary cascade float3 positionLS = mul(directionalShadowLightData.ShadowProj[primaryCascade], float4(positionWS, 1)).xyz; positionLS.xy = saturate(positionLS.xy); + float primaryNear = primaryCascade == 0 ? depthParams.x : depthParams.z; + float primaryFar = primaryCascade == 0 ? depthParams.y : depthParams.w; + // Sample primary cascade - float shadow = SampleVSMCascade2D(primaryCascade, positionLS); + float shadow = SampleVSMCascade2D(primaryCascade, positionLS, primaryNear, primaryFar); // Blend with secondary cascade if needed [branch] if (needsBlending) @@ -170,14 +231,17 @@ namespace VolumetricShadows positionLS = mul(directionalShadowLightData.ShadowProj[secondaryCascade], float4(positionWS, 1)).xyz; positionLS.xy = saturate(positionLS.xy); - float shadowBlend = SampleVSMCascade2D(secondaryCascade, positionLS); + float secondaryNear = secondaryCascade == 0 ? depthParams.x : depthParams.z; + float secondaryFar = secondaryCascade == 0 ? depthParams.y : depthParams.w; + + float shadowBlend = SampleVSMCascade2D(secondaryCascade, positionLS, secondaryNear, secondaryFar); shadow = lerp(shadow, shadowBlend, cascadeSelect); } // Apply distance fade float fadeFactor = 1.0 - pow(fade * fade, 8); - detailedShadow = lerp(1.0, ReduceBleeding(shadow, VSM_BLEEDING_REDUCTION), fadeFactor); - return lerp(1.0, shadow, fadeFactor); + detailedShadow = lerp(1.0, shadow, fadeFactor); + return detailedShadow; } } diff --git a/include/PCH.h b/include/PCH.h index e3a8d9461f..565fc4e157 100644 --- a/include/PCH.h +++ b/include/PCH.h @@ -205,6 +205,7 @@ using float4x4 = DirectX::SimpleMath::Matrix; using uint = uint32_t; #include "Globals.h" +#include "Profiler.h" #include "Util.h" #include "Feature.h" #include "Buffer.h" diff --git a/package/Shaders/Common/Color.hlsli b/package/Shaders/Common/Color.hlsli index 7147f5d492..aaed6fb2dd 100644 --- a/package/Shaders/Common/Color.hlsli +++ b/package/Shaders/Common/Color.hlsli @@ -2,7 +2,9 @@ #define __COLOR_DEPENDENCY_HLSL__ #include "Common/Math.hlsli" -#include "Common/SharedData.hlsli" +#ifndef UNIT_TEST +# include "Common/SharedData.hlsli" +#endif #define ENABLE_LL SharedData::linearLightingSettings.enableLinearLighting @@ -197,6 +199,9 @@ namespace Color float3 Diffuse(float3 color) { + if (SharedData::enbSettings.Enable) + color = pow(abs(color), SharedData::enbSettings.ColorPow); + # if defined(TRUE_PBR) return ENABLE_LL ? color : LinearToSrgb(color); # else diff --git a/package/Shaders/Common/DisplayMapping.hlsli b/package/Shaders/Common/DisplayMapping.hlsli index ba56504c5f..3aebbb5b60 100644 --- a/package/Shaders/Common/DisplayMapping.hlsli +++ b/package/Shaders/Common/DisplayMapping.hlsli @@ -217,6 +217,26 @@ namespace DisplayMapping #endif + // AdvancedAutoHDR pass to generate some HDR brightness out of an SDR signal. + // https://github.com/Filoppi/PumboAutoHDR + float3 PumboAutoHDR(float3 SDRColor, float MaxPeakWhiteNits, float PaperWhiteNits, float ShoulderPow = 2.75f, float SaturationExpansionIntensity = 0.2f) + { + float SDRRatio = average(SDRColor); + + float AutoHDRMaxWhite = max(min(MaxPeakWhiteNits / sRGB_WhiteLevelNits, 500 / PaperWhiteNits), 1.f); + + float AutoHDRExtraRatio = pow(saturate(SDRRatio), ShoulderPow) * (AutoHDRMaxWhite - 1.f); + float AutoHDRTotalRatio = SDRRatio + AutoHDRExtraRatio; + float SingleColorScale = safeDivision(AutoHDRTotalRatio, SDRRatio, 1); + + float3 SDRRatio3 = SDRColor; + float3 AutoHDRExtraRatio3 = pow(saturate(SDRRatio3), ShoulderPow) * (AutoHDRMaxWhite - 1.f); + float3 AutoHDRTotalRatio3 = SDRRatio3 + AutoHDRExtraRatio3; + float3 PerChannelColorScale = safeDivision(AutoHDRTotalRatio3, SDRRatio3, 1); + + return SDRColor * lerp(SingleColorScale, PerChannelColorScale, SaturationExpansionIntensity); + } + } // namespace DisplayMapping #endif // __DISPLAY_MAPPING_DEPENDENCY_HLSL__ \ No newline at end of file diff --git a/package/Shaders/Common/Permutation.hlsli b/package/Shaders/Common/Permutation.hlsli index d3e2005526..2caea4a722 100644 --- a/package/Shaders/Common/Permutation.hlsli +++ b/package/Shaders/Common/Permutation.hlsli @@ -65,6 +65,7 @@ namespace Permutation static const uint GrayscaleToColor = (1 << 19); static const uint GrayscaleToAlpha = (1 << 20); static const uint IgnoreTexAlpha = (1 << 21); + static const uint SkyObject = (1 << 24); } namespace ExtraFlags diff --git a/package/Shaders/Common/ShadowSampling.hlsli b/package/Shaders/Common/ShadowSampling.hlsli index 70e7143f54..19ac12702f 100644 --- a/package/Shaders/Common/ShadowSampling.hlsli +++ b/package/Shaders/Common/ShadowSampling.hlsli @@ -17,19 +17,20 @@ #if defined(IBL) # include "IBL/IBL.hlsli" #elif defined(SKYLIGHTING) -// sh2 type is needed for the ExtractLighting overload that accepts a visibility SH # include "Common/Spherical Harmonics/SphericalHarmonics.hlsli" #endif -// Populated once per frame by Deferred::CopyShadowLightData from BSShadowDirectionalLight. -// Column-major float4x4 projections so HLSL `mul(proj, float4(pos, 1))` matches the -// XMMATRIX layout written by XMStoreFloat4x4 on the C++ side. +#if defined(SKYLIGHTING) +# include "Skylighting/Skylighting.hlsli" +#endif + struct DirectionalShadowLightData { column_major float4x4 ShadowProj[2]; column_major float4x4 InvShadowProj[2]; float2 EndSplitDistances; float2 StartSplitDistances; + float4 CascadeDepthParams; }; StructuredBuffer DirectionalShadowLights : register(t98); @@ -65,6 +66,23 @@ namespace ShadowSampling return worldShadow; } + + float GetTrueWorldShadow(float3 positionWS, float3 offset, uint eyeIndex) + { + if (SharedData::InInterior || SharedData::HideSky || SharedData::InMapMenu) + return 1.0; + + float worldShadow = 1.0; +#if defined(TERRAIN_SHADOWS) + worldShadow = TerrainShadows::GetTerrainShadow(positionWS + offset, LinearSampler); +#endif + +#if defined(CLOUD_SHADOWS) + worldShadow *= CloudShadows::GetTrueCloudShadowMult(positionWS, LinearSampler); +#endif + + return worldShadow; + } float Get3DFilteredShadow(float3 positionWS, float3 viewDirection, float2 screenPosition, uint eyeIndex, out float surfaceShadow) { @@ -95,7 +113,7 @@ namespace ShadowSampling for (uint i = 0; i < sampleCount; i++) { float t = (float(i) + noise) * rcpSampleCount; float3 sampledPositionWS = lerp(endPosition, startPosition, t); - float worldShadowSample = ShadowSampling::GetWorldShadow(sampledPositionWS, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex); + float worldShadowSample = GetWorldShadow(sampledPositionWS, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex); surfaceShadow = worldShadowSample; worldShadow += worldShadowSample; } @@ -119,30 +137,69 @@ namespace ShadowSampling return worldShadow; } - float GetLightingShadow(float3 worldPosition, uint eyeIndex, out float detailedShadow) + float Get3DFilteredShadowVolumetric(float3 positionWS, float3 viewDirection, float2 screenPosition, uint eyeIndex, float extinction) + { + float totalRayLength = length(positionWS); + + const uint sampleCount = 8; + const float rcpSampleCount = 1.0 / float(sampleCount); + float stepLength = totalRayLength * rcpSampleCount; + + float noise = Random::InterleavedGradientNoise(screenPosition, SharedData::FrameCount); + + float stepTransmittance = exp(-extinction * stepLength); + float stepScatterCoeff = 1.0 - stepTransmittance; + + float3 cameraOffset = FrameBuffer::CameraPosAdjust[eyeIndex].xyz; + + float scattering = 0.0; + float transmittance = 1.0; + + for (uint i = 0; i < sampleCount; i++) { + float t = (float(i) + noise) * rcpSampleCount; + float3 sampledPositionWS = positionWS * t; + + float worldShadowSample = GetTrueWorldShadow(sampledPositionWS, cameraOffset, eyeIndex); + + scattering += worldShadowSample * stepScatterCoeff * transmittance; + transmittance *= stepTransmittance; + } + + return scattering; + } + + float GetLightingShadow(float3 worldPosition, float3 normal, float2 screenPosition, uint eyeIndex, out float detailedShadow) { if (!HasDirectionalShadows()) { detailedShadow = 1.0; return 1.0; } -#if defined(VOLUMETRIC_SHADOWS) - float shadow = VolumetricShadows::GetVSMShadow2D(worldPosition, eyeIndex, detailedShadow); - return shadow; -#else + float shadow = 1.0; detailedShadow = 1.0; - return 1.0; + +#if defined(SKYLIGHTING) && defined(PSHADER) + shadow = min(shadow, Skylighting::SampleShadowVisibility(worldPosition, normal, screenPosition)); +#endif + +#if defined(VOLUMETRIC_SHADOWS) + float vsmDetailedShadow; + shadow = min(shadow, VolumetricShadows::GetVSMShadow2D(worldPosition, eyeIndex, vsmDetailedShadow)); + detailedShadow = min(detailedShadow, vsmDetailedShadow); #endif + + detailedShadow = min(detailedShadow, shadow); + return shadow; } - float3 GetRawAmbientLighting(float3 normal) + float3 GetRawAmbientLighting() { - return max(0, SharedData::GetAmbient(normal)); + return max(0, SharedData::GetAmbient(LightingSampleNormal)); } - float3 GetAmbientLighting(float3 normal) + float3 GetAmbientLighting() { - float3 ambientColor = GetRawAmbientLighting(normal); + float3 ambientColor = GetRawAmbientLighting(); #if defined(IBL) if (SharedData::iblSettings.EnableIBL) { @@ -153,21 +210,6 @@ namespace ShadowSampling return ambientColor; } -#if defined(SKYLIGHTING) && !defined(INTERIOR) - float3 GetAmbientLighting(float3 normal, float skylightingDiffuse) - { - float3 ambientColor = GetRawAmbientLighting(normal); - -# if defined(IBL) - if (SharedData::iblSettings.EnableIBL) { - ambientColor = ImageBasedLighting::GetDiffuseIBLOccluded(ambientColor, ImageBasedLightingNormal, skylightingDiffuse); - } -# endif - - return ambientColor; - } -#endif - float3 GetDirectionalLighting() { float llDirLightMult = (SharedData::linearLightingSettings.enableLinearLighting && !SharedData::linearLightingSettings.isDirLightLinear) ? SharedData::linearLightingSettings.dirLightMult : 1.0f; @@ -176,21 +218,12 @@ namespace ShadowSampling float3 GetSceneLightingColor() { - return GetAmbientLighting(LightingSampleNormal) + GetDirectionalLighting(); + return GetAmbientLighting() + GetDirectionalLighting(); } -#if defined(SKYLIGHTING) && !defined(INTERIOR) - void ExtractLighting(float3 inputColor, out float3 dirColor, out float3 ambientColor, float skylightingDiffuse) -#else void ExtractLighting(float3 inputColor, out float3 dirColor, out float3 ambientColor) -#endif { -#if defined(SKYLIGHTING) && !defined(INTERIOR) - float3 ambientColorAmb = GetAmbientLighting(LightingSampleNormal, skylightingDiffuse); -#else - float3 ambientColorAmb = GetAmbientLighting(LightingSampleNormal); -#endif - + float3 ambientColorAmb = GetAmbientLighting(); float3 dirLightColorDir = GetDirectionalLighting(); float inputLuma = Color::RGBToLuminance(inputColor); @@ -199,7 +232,6 @@ namespace ShadowSampling float totalLuma = ambientLuma + dirLightLuma; - // Scale ambientColorAmb so total luma matches input luma if (totalLuma > 0.0 && ambientLuma > 0.0) ambientColorAmb *= inputLuma / totalLuma; diff --git a/package/Shaders/Common/SharedData.hlsli b/package/Shaders/Common/SharedData.hlsli index 8168507571..67e353b7f7 100644 --- a/package/Shaders/Common/SharedData.hlsli +++ b/package/Shaders/Common/SharedData.hlsli @@ -7,14 +7,18 @@ namespace SharedData { - -#if defined(PSHADER) || defined(CSHADER) || defined(COMPUTESHADER) cbuffer SharedData : register(b5) { float4 WaterData[25]; row_major float3x4 DirectionalAmbient; float4 DirLightDirection; float4 DirLightColor; + float4 SunDirection; + float4 SunColor; + float4 MasserDirection; + float4 MasserColor; + float4 SecundaDirection; + float4 SecundaColor; float4 CameraData; float4 BufferDim; float Timer; @@ -243,6 +247,79 @@ namespace SharedData uint pad0; }; + struct ENBSettings + { + uint Enable; + uint EnableSky; + float ColorPow; + float LightSpriteIntensity; + + float CloudsCurve; + float CloudsDesaturation; + float CloudsEdgeIntensity; + float CloudsEdgeMoonMultiplier; + + float VolumetricRaysDesaturation; + float3 VolumetricRaysColorFilter; + + uint UseProceduralGradientWeights; + float ProceduralGradientWeightCurve; + uint EnableProceduralSun; + float ProceduralSunDiskRadiusSq; + + float ProceduralSunDiskEdgeScale; + float ProceduralSunGlowIntensity; + float ProceduralSunCoronaFalloff; + float ProceduralSunCoronaScale; + + float ParticleIntensity; + float ParticleLightingInfluence; + float ParticleAmbientInfluence; + float ParticlePointLightingInfluence; + + uint EnableCloudsScattering; + uint EnableCloudsLightingFromMoon; + uint ScatteringColorHDRWeighting; + float SkyScatteringAtmosphereThickness; + + float SkyScatteringHorizonRange; + float SkyScatteringIntensity; + float SkyScatteringAmount; + float SkyScatteringDustVolume; + + float SkyScatteringDustDensity; + float SkyScatteringDustDarkening; + float SkyScatteringShadowAmount; + float SkyScatteringColorFromSun; + + float3 SkyScatteringColor; + float SkyScatteringAirGlowIntensity; + + float SkyScatteringAirGlowRange; + float SkyScatteringSunGlowIntensity; + float SkyScatteringSunGlowRange; + float SkyScatteringMoonGlowAmount; + + float SkyScatteringMoonGlowRange; + float SkyScatteringCloudsLightingSunMinIntensity; + float SkyScatteringCloudsLightingSunMultiplier; + float SkyScatteringCloudsLightingMoonIntensity; + + uint EnableVolumetricRays; + float VolumetricRaysIntensity; + float VolumetricRaysExtinction; + float VolumetricRaysSkyColorAmount; + + float FireIntensity; + float FireCurve; + float AuroraIntensity; + float AuroraCurve; + + uint EnableRain; + float RainMotionStretch; + float RainMotionTransparency; + float SkylightingAmbientMinLevel; + }; struct TerrainBlendingSettings { uint Enabled; @@ -291,6 +368,7 @@ namespace SharedData IBLSettings iblSettings; ExtendedTranslucencySettings extendedTranslucencySettings; LinearLightingSettings linearLightingSettings; + ENBSettings enbSettings; TerrainBlendingSettings terrainBlendingSettings; ExponentialHeightFogSettings exponentialHeightFogSettings; TruePBRSettings truePBRSettings; @@ -350,11 +428,11 @@ namespace SharedData [flatten] if (cellInt.x < 5 && cellInt.x >= 0 && cellInt.y < 5 && cellInt.y >= 0) waterData = WaterData[waterTile]; -# if defined(VR) +#if defined(VR) // Correct .w from eye-0 camera-relative Z to the current eye's camera-relative Z. // No-op when eyeIndex == 0 (both terms are identical). waterData.w += FrameBuffer::CameraPosAdjust[0].z - FrameBuffer::CameraPosAdjust[eyeIndex].z; -# endif +#endif return waterData; } @@ -363,7 +441,5 @@ namespace SharedData { return SphericalHarmonics::Unproject(AmbientSHR, AmbientSHG, AmbientSHB, normal); } - -#endif // PSHADER } #endif // __SHARED_DATA_DEPENDENCY_HLSL__ diff --git a/package/Shaders/DeferredCompositeCS.hlsl b/package/Shaders/DeferredCompositeCS.hlsl index 99132602a4..9a71c25fe3 100644 --- a/package/Shaders/DeferredCompositeCS.hlsl +++ b/package/Shaders/DeferredCompositeCS.hlsl @@ -13,6 +13,7 @@ Texture2D SpecularTexture : register(t0); Texture2D AlbedoTexture : register(t1); Texture2D NormalRoughnessTexture : register(t2); Texture2D MasksTexture : register(t3); +Texture2D Masks2Texture : register(t9); RWTexture2D MainRW : register(u0); RWTexture2D NormalTAAMaskSpecularMaskRW : register(u1); @@ -139,6 +140,9 @@ void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, inout float ao, out float3 il, float3 ssgiIl; SampleSSGI(dispatchID.xy, normalWS, ssgiAo, ssgiIl); + float vertexAO = Masks2Texture[dispatchID.xy].x; + ssgiAo = saturate(ssgiAo / max(vertexAO, EPSILON_DIVISION)); + float3 linAlbedo = Color::IrradianceToLinear(albedo / Color::PBRLightingScale); float3 multiBounceSSGIAo = MultiBounceAO(linAlbedo, ssgiAo); @@ -148,18 +152,7 @@ void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, inout float ao, out float3 il, if (SharedData::iblSettings.EnableIBL) { float3 vanillaDALC = Color::Ambient(max(0, SharedData::GetAmbient(normalWS))); -# if defined(SKYLIGHTING) -# if defined(VR) - float3 positionMS = positionWS.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; -# else - float3 positionMS = positionWS.xyz; -# endif - sh2 skylightingSH = Skylighting::Sample(positionMS.xyz, normalWS); - float skylightingDiffuse = Skylighting::EvaluateDiffuse(skylightingSH, normalWS); - directionalAmbientColor = ImageBasedLighting::GetDiffuseIBLOccluded(vanillaDALC, -normalWS, skylightingDiffuse) * albedo; -# else directionalAmbientColor = ImageBasedLighting::GetDiffuseIBL(vanillaDALC, -normalWS) * albedo; -# endif directionalAmbientColor = Color::RGBToYCoCg(directionalAmbientColor); directionalAmbientColor.x = MasksTexture[dispatchID.xy].z; @@ -224,7 +217,7 @@ void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, inout float ao, out float3 il, float3 positionMS = positionWS.xyz; # endif - sh2 skylightingSH = Skylighting::Sample(positionMS.xyz, R); + sh2 skylightingSH = Skylighting::Sample(positionMS.xyz, R, float2(dispatchID.xy)); float skylightingSpecular = Skylighting::EvaluateSpecular(skylightingSH, specularLobe); # endif diff --git a/package/Shaders/DistantTree.hlsl b/package/Shaders/DistantTree.hlsl index 669d61342e..b1457d85ea 100644 --- a/package/Shaders/DistantTree.hlsl +++ b/package/Shaders/DistantTree.hlsl @@ -115,6 +115,7 @@ struct PS_OUTPUT float4 Normal: SV_Target2; float4 Albedo: SV_Target3; float4 Masks: SV_Target6; + float4 Masks2: SV_Target7; # endif // DEFERRED #endif // !RENDER_DEPTH }; @@ -262,6 +263,7 @@ PS_OUTPUT main(PS_INPUT input) psout.Albedo = float4(baseColor.xyz, 1); psout.Masks = float4(0, 0, 1, 0); + psout.Masks2 = float4(1, 0, 0, 0); # else float dirShadow = ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex); diff --git a/package/Shaders/Effect.hlsl b/package/Shaders/Effect.hlsl index fcd9dfb2e8..f5d6840b4f 100644 --- a/package/Shaders/Effect.hlsl +++ b/package/Shaders/Effect.hlsl @@ -11,6 +11,13 @@ #define EFFECT +#if defined(SOFT) && defined(NORMALS) && defined(TEXTURE) && defined(FALLOFF) && defined(VC) && \ + !defined(LIGHTING) && !defined(PARTICLES) && !defined(STRIP_PARTICLES) && \ + !defined(BLOOD) && !defined(MEMBRANE) && !defined(ADDBLEND) && !defined(MULTBLEND) && \ + !defined(MULTBLEND_DECAL) && !defined(ALPHA_TEST) && !defined(DEFERRED) && !defined(SKINNED) +# define IS_VOLUMETRIC_FOG +#endif + #if !defined(DYNAMIC_CUBEMAPS) && defined(IBL) # undef IBL #endif @@ -435,6 +442,7 @@ struct PS_OUTPUT float4 Specular: SV_Target4; float4 Reflectance: SV_Target5; float4 Masks: SV_Target6; + float4 Masks2: SV_Target7; }; #else struct PS_OUTPUT @@ -550,11 +558,14 @@ float3 GetLightingColor(float3 msPosition, float3 worldPosition, float2 screenPo float3 dirColor; float3 ambientColor; -# if defined(SKYLIGHTING) - ShadowSampling::ExtractLighting(color, dirColor, ambientColor, skylightingDiffuse); -# else ShadowSampling::ExtractLighting(color, dirColor, ambientColor); -# endif + + if (SharedData::enbSettings.Enable) { + dirColor = ShadowSampling::GetDirectionalLighting(); + ambientColor = ShadowSampling::GetAmbientLighting(); + dirColor *= SharedData::enbSettings.ParticleLightingInfluence; + ambientColor *= SharedData::enbSettings.ParticleAmbientInfluence; + } float3 viewDirection = normalize(worldPosition.xyz); @@ -577,6 +588,7 @@ float3 GetLightingColor(float3 msPosition, float3 worldPosition, float2 screenPo # endif # if defined(SKYLIGHTING) + skylightingDiffuse = min(skylightingDiffuse, lerp(dirShadow, 1.0, SharedData::enbSettings.SkylightingAmbientMinLevel)); ambientColor = Color::IrradianceToLinear(ambientColor); ambientColor *= skylightingDiffuse; ambientColor = Color::IrradianceToGamma(ambientColor); @@ -590,9 +602,10 @@ float3 GetLightingColor(float3 msPosition, float3 worldPosition, float2 screenPo { float4 lightDistanceSquared = (PLightPositionX[eyeIndex] - msPosition.xxxx) * (PLightPositionX[eyeIndex] - msPosition.xxxx) + (PLightPositionY[eyeIndex] - msPosition.yyyy) * (PLightPositionY[eyeIndex] - msPosition.yyyy) + (PLightPositionZ[eyeIndex] - msPosition.zzzz) * (PLightPositionZ[eyeIndex] - msPosition.zzzz); float4 lightFadeMul = 1.0.xxxx - saturate(PLightingRadiusInverseSquared * lightDistanceSquared); - color.x += dot(Color::PointLight(PLightColorR.xxx).x * lightFadeMul * Color::EffectLightingMult(), 1.0.xxxx); - color.y += dot(Color::PointLight(PLightColorG.xxx).x * lightFadeMul * Color::EffectLightingMult(), 1.0.xxxx); - color.z += dot(Color::PointLight(PLightColorB.xxx).x * lightFadeMul * Color::EffectLightingMult(), 1.0.xxxx); + float pointScale = SharedData::enbSettings.Enable ? SharedData::enbSettings.ParticlePointLightingInfluence : 1.0; + color.x += dot(Color::PointLight(PLightColorR.xxx).x * lightFadeMul * Color::EffectLightingMult(), 1.0.xxxx) * pointScale; + color.y += dot(Color::PointLight(PLightColorG.xxx).x * lightFadeMul * Color::EffectLightingMult(), 1.0.xxxx) * pointScale; + color.z += dot(Color::PointLight(PLightColorB.xxx).x * lightFadeMul * Color::EffectLightingMult(), 1.0.xxxx) * pointScale; } return color; @@ -602,12 +615,7 @@ float3 GetLightingShadow(float3 color, float3 worldPosition, float2 screenPositi { float3 dirColor; float3 ambientColor; - float skylightingDiffuse = 1.0; -# if defined(SKYLIGHTING) - ShadowSampling::ExtractLighting(color, dirColor, ambientColor, skylightingDiffuse); -# else ShadowSampling::ExtractLighting(color, dirColor, ambientColor); -# endif static const uint sampleCount = 8; static const float rcpSampleCount = 1.0 / float(sampleCount); @@ -711,6 +719,11 @@ PS_OUTPUT main(PS_INPUT input) float3 propertyColor = Color::Effect(PropertyColor.xyz); float shadowVariance = 1.0; +# if !defined(IS_VOLUMETRIC_FOG) + if (SharedData::enbSettings.Enable && !(Permutation::VertexShaderDescriptor & Permutation::EffectFlags::SkyObject)) + propertyColor *= SharedData::enbSettings.ParticleIntensity; +# endif + # if defined(LIGHTING) propertyColor = GetLightingColor(input.MSPosition.xyz, input.WorldPosition.xyz, input.Position.xy, eyeIndex, shadowVariance); @@ -838,9 +851,8 @@ PS_OUTPUT main(PS_INPUT input) } # endif -# if !defined(LIGHTING) && defined(VC) && defined(TEXCOORD) && defined(NORMALS) && defined(TEXTURE) && defined(FALLOFF) && defined(SOFT) - if (Permutation::PixelShaderDescriptor & Permutation::EffectFlags::GrayscaleToAlpha && lightingInfluence == 1.0) - lightColor = GetLightingShadow(lightColor, input.WorldPosition.xyz, input.Position.xy, depth, eyeIndex, shadowVariance); +# if defined(IS_VOLUMETRIC_FOG) + lightColor = GetLightingShadow(lightColor, input.WorldPosition.xyz, input.Position.xy, depth, eyeIndex, shadowVariance); # endif lightColor = Color::EffectMult(lightColor); @@ -875,6 +887,8 @@ PS_OUTPUT main(PS_INPUT input) # if defined(ADDBLEND) # if defined(EXP_HEIGHT_FOG) float3 blendedColor = lightColor * (1 - vanillaFogFactor) * (1 - expFogFactor); + if (SharedData::enbSettings.Enable) + blendedColor *= SharedData::enbSettings.LightSpriteIntensity; # else float3 blendedColor = lightColor * (1 - fogFactor); # endif @@ -895,7 +909,7 @@ PS_OUTPUT main(PS_INPUT input) # else float3 blendedColor = lightColor.xyz; # endif - + alpha = Color::EffectAlpha(alpha); float4 finalColor = float4(blendedColor, alpha); @@ -935,11 +949,13 @@ PS_OUTPUT main(PS_INPUT input) psout.Albedo = float4(psout.Diffuse.xyz, finalColor.w); psout.Reflectance = float4(psout.Diffuse.xyz, finalColor.w); psout.Masks = float4(Color::RGBToLuminance(psout.Diffuse.xyz).xxx, finalColor.w); + psout.Masks2 = float4(1, 0, 0, finalColor.w); # else - psout.Albedo = float4(0, 0, 0, finalColor.w); + psout.Albedo = float4(psout.Diffuse.xyz * !(Permutation::VertexShaderDescriptor & Permutation::EffectFlags::SkyObject), finalColor.w); psout.Specular = float4(0, 0, 0, finalColor.w); psout.Reflectance = float4(0, 0, 0, finalColor.w); psout.Masks = float4(0, 0, 0, finalColor.w); + psout.Masks2 = float4(1, 0, 0, finalColor.w); # endif # elif defined(MOTIONVECTORS_NORMALS) diff --git a/package/Shaders/ISCompositeLensFlareVolumetricLighting.hlsl b/package/Shaders/ISCompositeLensFlareVolumetricLighting.hlsl index 63682300ae..392b6e4a07 100644 --- a/package/Shaders/ISCompositeLensFlareVolumetricLighting.hlsl +++ b/package/Shaders/ISCompositeLensFlareVolumetricLighting.hlsl @@ -31,7 +31,12 @@ PS_OUTPUT main(PS_INPUT input) # if defined(VOLUMETRIC_LIGHTING) float2 screenPosition = FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(input.TexCoord); float volumetricLightingPower = VLSourceTex.Sample(VLSourceSampler, screenPosition).x; - color += VolumetricLightingColor.xyz * Color::VolumetricLighting(volumetricLightingPower.xxx).x; + float3 volumetricLightingColor = VolumetricLightingColor.xyz; + if (SharedData::enbSettings.Enable) { + volumetricLightingColor = lerp(volumetricLightingColor, dot(volumetricLightingColor, 1.0 / 3.0), SharedData::enbSettings.VolumetricRaysDesaturation); + volumetricLightingColor *= SharedData::enbSettings.VolumetricRaysColorFilter; + } + color += volumetricLightingColor * Color::VolumetricLighting(volumetricLightingPower.xxx).x; # endif # if defined(LENS_FLARE) diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index 953b09e13d..17f343a00d 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -344,9 +344,7 @@ struct PS_OUTPUT float4 Specular: SV_Target4; float4 Reflectance: SV_Target5; float4 Masks: SV_Target6; -# if defined(SNOW) - float4 Parameters: SV_Target7; -# endif + float4 Masks2: SV_Target7; }; #else struct PS_OUTPUT @@ -870,6 +868,9 @@ float GetSnowParameterY(float texProjTmp, float alpha) # if defined(EYE) # undef WETNESS_EFFECTS +# undef SOFT_LIGHTING +# undef BACK_LIGHTING +# undef RIM_LIGHTING # endif # if defined(EXTENDED_MATERIALS) && !defined(LOD) && (defined(PARALLAX) || defined(LANDSCAPE) || defined(ENVMAP) || defined(TRUE_PBR)) @@ -2058,9 +2059,6 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) rawBaseColor = Triplanar::SampleStochasticBias(TexColorSampler, SampColorSampler, projWorldPos, triWeights, ProjectedUVParams2.y, SharedData::MipBias, screenNoise); baseColor = float4(Color::Diffuse(rawBaseColor.rgb), rawBaseColor.a); worldNormal.xyz = projectedNormal; -# if defined(SNOW) - psout.Parameters.y = 1; -# endif // SNOW # elif !defined(FACEGEN) && !defined(MULTI_LAYER_PARALLAX) && !defined(PARALLAX) && !defined(SPARKLE) if (ProjectedUVParams3.w > 0.5) { float diffuseNormalScale = ProjectedUVParams3.x * ProjectedUVParams.z; @@ -2089,18 +2087,12 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(SNOW) useSnowDecalSpecular = true; - psout.Parameters.y = GetSnowParameterY(projectedMaterialWeight, baseColor.w); # endif // SNOW } else { if (projWeight > 0) { baseColor.xyz = Color::Diffuse(ProjectedUVParams2.xyz); # if defined(SNOW) useSnowDecalSpecular = true; - psout.Parameters.y = GetSnowParameterY(projWeight, baseColor.w); -# endif // SNOW - } else { -# if defined(SNOW) - psout.Parameters.y = 0; # endif // SNOW } } @@ -2110,12 +2102,6 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif // SPECULAR # endif // SPARKLE -# elif defined(SNOW) -# if defined(LANDSCAPE) - psout.Parameters.y = landSnowMask; -# else - psout.Parameters.y = baseColor.w; -# endif // LANDSCAPE # endif // SNOW # if defined(WORLD_MAP) @@ -2369,9 +2355,9 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float3 positionMSSkylight = input.WorldPosition.xyz; # endif # if defined(DEFERRED) - sh2 skylightingSH = Skylighting::Sample(positionMSSkylight, worldNormal); + sh2 skylightingSH = Skylighting::Sample(positionMSSkylight, worldNormal, input.Position.xy); # else - sh2 skylightingSH = inWorld ? Skylighting::Sample(positionMSSkylight, worldNormal) : Skylighting::UNIT_SH; + sh2 skylightingSH = inWorld ? Skylighting::Sample(positionMSSkylight, worldNormal, input.Position.xy) : Skylighting::UNIT_SH; # endif # endif @@ -2501,9 +2487,9 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float dirSoftShadow = 1.0; float dirVSMDetailedShadow = 1.0; -# if defined(VOLUMETRIC_SHADOWS) +# if defined(VOLUMETRIC_SHADOWS) || defined(SKYLIGHTING) if (inWorld && !inReflection && ShadowSampling::HasDirectionalShadows()) - dirSoftShadow = ShadowSampling::GetLightingShadow(input.WorldPosition.xyz, eyeIndex, dirVSMDetailedShadow); + dirSoftShadow = ShadowSampling::GetLightingShadow(input.WorldPosition.xyz, worldNormal, input.Position.xy, eyeIndex, dirVSMDetailedShadow); # endif float dirDetailedShadow = 1.0; @@ -2511,7 +2497,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) if ((Permutation::PixelShaderDescriptor & Permutation::LightingFlags::DefShadow) && (Permutation::PixelShaderDescriptor & Permutation::LightingFlags::ShadowDir)) { dirDetailedShadow *= shadowColor.x; -# if !defined(VOLUMETRIC_SHADOWS) +# if !defined(VOLUMETRIC_SHADOWS) && !defined(SKYLIGHTING) dirSoftShadow = dirDetailedShadow; # endif } else { @@ -2873,15 +2859,6 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) } # endif -# if defined(SKYLIGHTING) - float skylightingDiffuse = 1; - float skylightingFadeOutFactor = 1.0; - if (!SharedData::InInterior) { - skylightingFadeOutFactor = Skylighting::GetFadeOutFactor(input.WorldPosition.xyz); - skylightingDiffuse = Skylighting::EvaluateDiffuse(skylightingSH, ambientNormal, skylightingFadeOutFactor); - } -# endif - # if defined(LANDSCAPE) if (SharedData::lodBlendingSettings.DisableTerrainVertexColors) input.Color.xyz = 1; @@ -2891,35 +2868,45 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(HAIR) float3 vertexColor = lerp(1, Color::ColorToLinear(TintColor.xyz), Color::ColorToLinear(input.Color.y)); + float vertexAO = 1; # if defined(CS_HAIR) if (SharedData::hairSpecularSettings.Enabled) vertexColor = 1; # endif +# if defined(SKYLIGHTING) + float skylightingDiffuse = Skylighting::GetSkylightingDiffuse(skylightingSH, input.WorldPosition.xyz, ambientNormal); + skylightingDiffuse = min(skylightingDiffuse, lerp(dirSoftShadow, 1.0, SharedData::enbSettings.SkylightingAmbientMinLevel)); +# endif # elif defined(SKYLIGHTING) float3 vertexColor = input.Color.xyz; - float vertexAO = max(max(vertexColor.r, vertexColor.g), vertexColor.b); +# if defined(FACEGEN) || defined(FACEGEN_RGB_TINT) || defined(EYE) + float vertexAO = 1; +# else + float vertexAO = Color::ColorToLinear(max(max(vertexColor.r, vertexColor.g), vertexColor.b).xxx).x; +# endif # if defined(TRUE_PBR) vertexAO = lerp(1, vertexAO, SharedData::truePBRSettings.VertexAOStrength); vertexColor = 1; # endif - // Modify skylightingDiffuse such that skylightingDiffuse * vertexAO = min(skylightingDiffuse, vertexAO) - skylightingDiffuse = saturate(skylightingDiffuse / max(vertexAO, 1e-5)); + float skylightingDiffuse = Skylighting::GetSkylightingDiffuse(skylightingSH, input.WorldPosition.xyz, ambientNormal, vertexAO); + skylightingDiffuse = min(skylightingDiffuse, lerp(dirSoftShadow, 1.0, SharedData::enbSettings.SkylightingAmbientMinLevel)); # else # if defined(TRUE_PBR) float3 vertexColor = 1; # else float3 vertexColor = input.Color.xyz; # endif +# if defined(FACEGEN) || defined(FACEGEN_RGB_TINT) || defined(EYE) + float vertexAO = 1; +# else + float vertexAO = Color::ColorToLinear(max(max(vertexColor.r, vertexColor.g), vertexColor.b).xxx).x; +# endif # endif // defined (HAIR) # if defined(IBL) if (SharedData::iblSettings.EnableIBL) { if (!(SharedData::iblSettings.UseStaticIBL && !inWorld && !inReflection)) { -# if defined(SKYLIGHTING) - directionalAmbientColor = ImageBasedLighting::GetDiffuseIBLOccluded(directionalAmbientColor, -ambientNormal, skylightingDiffuse); -# else directionalAmbientColor = ImageBasedLighting::GetDiffuseIBL(directionalAmbientColor, -ambientNormal); -# endif } } # endif @@ -3012,11 +2999,8 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float mlpBlendFactor = saturate(viewNormalAngle) * (1.0 - baseColor.w); -# if defined(SKYLIGHTING) - color.xyz = lerp(color.xyz, (diffuseColor + directionalAmbientColor * skylightingDiffuse) * vertexColor * layerColor, mlpBlendFactor); -# else color.xyz = lerp(color.xyz, (diffuseColor + directionalAmbientColor) * vertexColor * layerColor, mlpBlendFactor); -# endif + directionalAmbientColor = lerp(directionalAmbientColor, directionalAmbientColor * layerColor, mlpBlendFactor); indirectLobeWeights.diffuse *= 1.0 - mlpBlendFactor; # endif // MULTI_LAYER_PARALLAX @@ -3062,12 +3046,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) directionalAmbientColor *= outputAlbedo; # if defined(SKYLIGHTING) -# if defined(IBL) - if (!SharedData::iblSettings.EnableIBL) -# endif - { - Skylighting::ApplySkylighting(color.xyz, directionalAmbientColor, outputAlbedo, skylightingDiffuse); - } + Skylighting::ApplySkylighting(color.xyz, directionalAmbientColor, outputAlbedo, skylightingDiffuse); # endif # if !defined(DEFERRED) @@ -3302,16 +3281,6 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) PomOffsetTex[uint2(input.Position.xy)] = hasPOM ? pixelOffset : Stereo::POM_NO_DATA; # endif -# if defined(SNOW) -# if defined(TRUE_PBR) - psout.Parameters.x = Color::RGBToLuminanceAlternative(specularColor); - psout.Parameters.y = 0; -# else - psout.Parameters.x = Color::RGBToLuminanceAlternative(lightsSpecularColor); -# endif - psout.Parameters.w = psout.Diffuse.w; -# endif - float masksZ = Color::RGBToYCoCg(directionalAmbientColor).x; # if defined(SSS) && defined(SKIN) @@ -3320,6 +3289,8 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) psout.Masks = float4(0, 0, masksZ, psout.Diffuse.w); # endif + psout.Masks2 = float4(vertexAO, 0, 0, psout.Diffuse.w); + float stochasticBlend = (screenNoise * screenNoise) < psout.Diffuse.w ? 1.0 : 0.0; psout.NormalGlossiness.w = stochasticBlend; # endif diff --git a/package/Shaders/Particle.hlsl b/package/Shaders/Particle.hlsl index c35f68861c..6bce245c3e 100644 --- a/package/Shaders/Particle.hlsl +++ b/package/Shaders/Particle.hlsl @@ -3,6 +3,10 @@ #include "Common/SharedData.hlsli" #include "Common/VR.hlsli" +#if !defined(DYNAMIC_CUBEMAPS) && defined(IBL) +# undef IBL +#endif + struct VS_INPUT { float4 Position: POSITION0; @@ -29,6 +33,9 @@ struct VS_OUTPUT #if defined(ENVCUBE) float4 PrecipitationOcclusionTexCoord: TEXCOORD1; #endif +#if defined(ENVCUBE) && defined(RAIN) + float2 RaindropData: TEXCOORD2; +#endif #if defined(VR) float ClipDistance: SV_ClipDistance0; // o11 float CullDistance: SV_CullDistance0; // p11 @@ -103,7 +110,12 @@ VS_OUTPUT main(VS_INPUT input) float4 viewPosition = mul(WorldViewProj[eyeIndex], msPosition); # if defined(RAIN) - float4 adjustedMsPosition = msPosition - float4(Velocity.xyz, 0); + float3 rainVelocity = Velocity.xyz; + if (SharedData::enbSettings.EnableRain) { + float3 normVel = normalize(rainVelocity); + rainVelocity = lerp(normVel, rainVelocity, SharedData::enbSettings.RainMotionStretch); + } + float4 adjustedMsPosition = msPosition - float4(rainVelocity, 0); float positionBlendParam = 0.5 * (1 + input.TexCoord1.y); float4 adjustedViewPosition = mul(WorldViewProj[eyeIndex], adjustedMsPosition); float4 finalViewPosition = lerp(adjustedViewPosition, viewPosition, positionBlendParam); @@ -201,6 +213,14 @@ VS_OUTPUT main(VS_INPUT input) vsout.ClipDistance.x = VRout.ClipDistance; vsout.CullDistance.x = VRout.CullDistance; # endif // VR + +# if defined(RAIN) + float2 uv = input.TexCoord1.xy; + uv.y *= 1.25; // UV fix + uv.xy *= 0.5; // UV unfix + vsout.RaindropData.xy = uv * 0.5 + 0.5; +# endif + return vsout; } #endif @@ -240,6 +260,9 @@ Texture2D TexGrayscaleTexture : register(t1); Texture2D TexPrecipitationOcclusionTexture : register(t2); Texture2D TexUnderwaterMask : register(t3); # endif +# if defined(ENVCUBE) && defined(RAIN) +Texture2D TexRaindropNormals : register(t80); +# endif cbuffer PerGeometry : register(b2) { @@ -250,7 +273,16 @@ cbuffer PerGeometry : register(b2) # define LinearSampler SampSourceTexture # include "Common/ShadowSampling.hlsli" -PS_OUTPUT main(PS_INPUT input) +# if defined(IBL) +# include "IBL/IBL.hlsli" +# endif + +# if defined(DYNAMIC_CUBEMAPS) +# define SampColorSampler SampSourceTexture +# include "DynamicCubemaps/DynamicCubemaps.hlsli" +# endif + +PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) { PS_OUTPUT psout; @@ -261,11 +293,11 @@ PS_OUTPUT main(PS_INPUT input) # endif // !VR # if defined(ENVCUBE) - float2 precipitationOcclusionUV = (input.PrecipitationOcclusionTexCoord.xy * 0.5 + 0.5) * TextureSize.x; + float2 precipitationOcclusionUV = input.PrecipitationOcclusionTexCoord.xy * 0.5 + 0.5; # ifdef VR precipitationOcclusionUV *= FrameBuffer::DynamicResolutionParams1.x; // only difference in VR # endif - float precipitationOcclusion = -input.PrecipitationOcclusionTexCoord.z + TexPrecipitationOcclusionTexture.Load(float3(precipitationOcclusionUV, 0)).x; + float precipitationOcclusion = -input.PrecipitationOcclusionTexCoord.z + TexPrecipitationOcclusionTexture.SampleLevel(SampSourceTexture, precipitationOcclusionUV, 0.0); float2 underwaterMaskUv = TextureSize.yz * input.Position.xy; float underwaterMask = TexUnderwaterMask.Sample(SampUnderwaterMask, underwaterMaskUv).x; if (precipitationOcclusion - underwaterMask < 0) { @@ -273,6 +305,51 @@ PS_OUTPUT main(PS_INPUT input) } # endif +# if defined(RAIN) && defined(DYNAMIC_CUBEMAPS) +if (SharedData::enbSettings.EnableRain) { + float4 raindropNormal = TexRaindropNormals.Sample(SampSourceTexture, input.RaindropData.xy); + float alpha = saturate(raindropNormal.w * (1.0 - SharedData::enbSettings.RainMotionTransparency)); + clip(alpha - (4.0 / 255.0)); + raindropNormal.y = 1.0 - raindropNormal.y; + + // Reconstruct camera-relative worldspace position (camera at origin). + float2 uv = Stereo::ConvertFromStereoUV(input.Position.xy * SharedData::BufferDim.zw, eyeIndex); + float4 posCS = float4(2.0 * float2(uv.x, 1.0 - uv.y) - 1.0, input.Position.z, 1.0); + float4 posWS = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], posCS); + posWS.xyz /= posWS.w; + + // Build worldspace TBN from screen-space derivatives. The billboard is camera-aligned, + // so dPdx/dPdy lie in the billboard plane along screen X/Y. + float3 T = normalize(ddx(posWS.xyz)); + float3 N = normalize(cross(T, -ddy(posWS.xyz))); + float3 B = cross(N, T); + float3x3 TBN = float3x3(T, B, N); + + float3 normalTS = normalize(raindropNormal.xyz * 2.0 - 1.0); + float3 normalWS = normalize(mul(normalTS, TBN)); + + if (frontFace) + normalWS = -normalWS; + + float3 V = normalize(-posWS.xyz); + float NdotV = saturate(dot(normalWS, V)); + float fresnel = 0.02 + 0.98 * pow(1.0 - NdotV, 5.0); + + float3 reflectDir = reflect(-V, normalWS); + float3 refractDir = refract(-V, normalWS, 1.0 / 1.33); + if (dot(refractDir, refractDir) < 1e-4) + refractDir = -V; + + float3 reflectColor = DynamicCubemaps::EnvReflectionsTexture.SampleLevel(SampSourceTexture, reflectDir, 0).xyz; + float3 refractColor = DynamicCubemaps::EnvReflectionsTexture.SampleLevel(SampSourceTexture, refractDir, 0).xyz; + + psout.Color.xyz = lerp(refractColor, reflectColor, fresnel); + psout.Color.w = alpha; + psout.Normal = float4(0, 1, 0, alpha); + return psout; +} +# endif + float4 sourceColor = TexSourceTexture.Sample(SampSourceTexture, input.TexCoord0); float4 baseColor = input.Color * sourceColor; baseColor.xyz = Color::Diffuse(baseColor.xyz); @@ -295,9 +372,16 @@ PS_OUTPUT main(PS_INPUT input) positionWS = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], positionWS); positionWS.xyz = positionWS.xyz / positionWS.w; + float3 viewPosition = FrameBuffer::WorldToView(positionWS.xyz, true, eyeIndex); + float unusedDetailedShadow; - float3 dirLightColor = SharedData::DirLightColor.xyz * ShadowSampling::GetLightingShadow(positionWS.xyz, eyeIndex, unusedDetailedShadow); + float3 dirLightColor = SharedData::DirLightColor.xyz * ShadowSampling::GetLightingShadow(positionWS.xyz, float3(0, 0, 1), input.Position.xy, eyeIndex, unusedDetailedShadow); float3 ambientColor = max(0, SharedData::GetAmbient(float3(0, 0, 1))); +# if defined(IBL) + if (SharedData::iblSettings.EnableIBL) { + ambientColor = ImageBasedLighting::GetDiffuseIBL(ambientColor, float3(0, 0, -1)); + } +# endif propertyColor += dirLightColor; propertyColor += ambientColor; @@ -305,7 +389,6 @@ PS_OUTPUT main(PS_INPUT input) # if defined(LIGHT_LIMIT_FIX) uint lightCount = 0; { - float3 viewPosition = FrameBuffer::WorldToView(positionWS.xyz, true, eyeIndex); float2 screenUV = FrameBuffer::ViewToUV(viewPosition, true, eyeIndex); uint clusterIndex = 0; diff --git a/package/Shaders/RunGrass.hlsl b/package/Shaders/RunGrass.hlsl index e7ef06f9f8..63bd4a7eed 100644 --- a/package/Shaders/RunGrass.hlsl +++ b/package/Shaders/RunGrass.hlsl @@ -358,9 +358,7 @@ struct PS_OUTPUT float4 Reflectance: SV_Target5; # endif // TRUE_PBR float4 Masks: SV_Target6; -# if defined(TRUE_PBR) - float4 Parameters: SV_Target7; -# endif // TRUE_PBR + float4 Masks2: SV_Target7; # endif // RENDER_DEPTH }; #else @@ -374,6 +372,7 @@ struct PS_OUTPUT float4 Normal: SV_Target2; float4 Albedo: SV_Target3; float4 Masks: SV_Target6; + float4 Masks2: SV_Target7; # endif }; #endif @@ -600,11 +599,23 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) if (!SharedData::InInterior) dirLightColor *= ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex); + float dirSoftShadow = 1.0; float dirDetailedShadow = 1.0; +# if defined(VOLUMETRIC_SHADOWS) || defined(SKYLIGHTING) + if (!SharedData::InInterior && ShadowSampling::HasDirectionalShadows()) { + float vsmDetailedShadow; + dirSoftShadow = ShadowSampling::GetLightingShadow(input.WorldPosition.xyz, normal, input.HPosition.xy, eyeIndex, vsmDetailedShadow); + } +# endif + if (!SharedData::InInterior) dirDetailedShadow *= shadowColor.x; +# if !defined(VOLUMETRIC_SHADOWS) && !defined(SKYLIGHTING) + dirSoftShadow = dirDetailedShadow; +# endif + # if defined(SCREEN_SPACE_SHADOWS) if (!SharedData::InInterior && dirLightAngle >= 0.0) dirDetailedShadow *= ScreenSpaceShadows::GetScreenSpaceShadow(input.HPosition.xyz, screenUV, screenNoise, eyeIndex); @@ -633,7 +644,8 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) lightsDiffuseColor += dirLightColor * dirDetailedShadow * saturate(dirLightAngle) * Color::VanillaNormalization(); float3 vertexColor = Color::ColorToLinear(input.Color.xyz); - vertexColor /= max(max(max(vertexColor.r, vertexColor.g), vertexColor.b), EPSILON_DIVISION); + float vertexAO = max(max(vertexColor.r, vertexColor.g), vertexColor.b); + vertexColor /= max(vertexAO, EPSILON_DIVISION); # if defined(SKYLIGHTING) # if defined(VR) @@ -641,8 +653,9 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # else float3 positionMSSkylight = input.WorldPosition.xyz; # endif - float vertexAO = max(max(vertexColor.r, vertexColor.g), vertexColor.b); - float skylightingDiffuse = Skylighting::GetVertexSkylightingDiffuse(positionMSSkylight, normal, vertexAO); + sh2 skylightingSH = Skylighting::Sample(positionMSSkylight, normal, input.HPosition.xy); + float skylightingDiffuse = Skylighting::GetSkylightingDiffuse(skylightingSH, positionMSSkylight, normal, vertexAO); + skylightingDiffuse = min(skylightingDiffuse, lerp(dirSoftShadow, 1.0, SharedData::enbSettings.SkylightingAmbientMinLevel)); # endif // SKYLIGHTING float3 albedo = baseColor.xyz * vertexColor; @@ -738,13 +751,8 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float3 directionalAmbientColor = Color::Ambient(max(0, SharedData::GetAmbient(normal))); # if defined(IBL) - if (SharedData::iblSettings.EnableIBL) { -# if defined(SKYLIGHTING) - directionalAmbientColor = ImageBasedLighting::GetDiffuseIBLOccluded(directionalAmbientColor, -normal, skylightingDiffuse); -# else + if (SharedData::iblSettings.EnableIBL) directionalAmbientColor = ImageBasedLighting::GetDiffuseIBL(directionalAmbientColor, -normal); -# endif - } # endif diffuseColor += directionalAmbientColor; @@ -754,12 +762,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) directionalAmbientColor *= albedo; # if defined(SKYLIGHTING) -# if defined(IBL) - if (!SharedData::iblSettings.EnableIBL) -# endif - { - Skylighting::ApplySkylighting(diffuseColor, directionalAmbientColor, albedo, skylightingDiffuse); - } + Skylighting::ApplySkylighting(diffuseColor, directionalAmbientColor, albedo, skylightingDiffuse); # endif specularColor += lightsSpecularColor; @@ -787,7 +790,6 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) psout.Albedo = float4(Color::IrradianceToGamma(indirectDiffuseLobeWeight), 1); psout.NormalGlossiness = float4(GBuffer::EncodeNormal(normalVS), 1 - pbrSurfaceProperties.Roughness, 1); psout.Reflectance = float4(indirectSpecularLobeWeight, 1); - psout.Parameters = float4(0, 0, 1, 1); # else psout.Albedo = float4(albedo, 1); psout.NormalGlossiness = float4(GBuffer::EncodeNormal(normalVS), specColor.w, 1); @@ -795,6 +797,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) psout.Specular = float4(specularColor, 1); psout.Masks = float4(0, 0, Color::RGBToYCoCg(directionalAmbientColor).x, 0); + psout.Masks2 = float4(vertexAO, 0, 0, 0); # endif return psout; } @@ -835,11 +838,23 @@ PS_OUTPUT main(PS_INPUT input) if (!SharedData::InInterior) dirLightColor *= ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex); + float dirSoftShadow = 1.0; float dirDetailedShadow = 1.0; +# if defined(VOLUMETRIC_SHADOWS) || defined(SKYLIGHTING) + if (!SharedData::InInterior && ShadowSampling::HasDirectionalShadows()) { + float vsmDetailedShadow; + dirSoftShadow = ShadowSampling::GetLightingShadow(input.WorldPosition.xyz, normal, input.HPosition.xy, eyeIndex, vsmDetailedShadow); + } +# endif + if (!SharedData::InInterior) dirDetailedShadow = shadowColor.x; +# if !defined(VOLUMETRIC_SHADOWS) && !defined(SKYLIGHTING) + dirSoftShadow = dirDetailedShadow; +# endif + # if defined(SCREEN_SPACE_SHADOWS) if (!SharedData::InInterior) dirDetailedShadow *= ScreenSpaceShadows::GetScreenSpaceShadow(input.HPosition.xyz, screenUV, screenNoise, eyeIndex); @@ -900,7 +915,8 @@ PS_OUTPUT main(PS_INPUT input) float3 normal = -normalize(cross(ddx, ddy)); float3 vertexColor = Color::ColorToLinear(input.Color.xyz); - vertexColor /= max(max(max(vertexColor.r, vertexColor.g), vertexColor.b), EPSILON_DIVISION); + float vertexAO = max(max(vertexColor.r, vertexColor.g), vertexColor.b); + vertexColor /= max(vertexAO, EPSILON_DIVISION); # if defined(SKYLIGHTING) # if defined(VR) @@ -908,36 +924,27 @@ PS_OUTPUT main(PS_INPUT input) # else float3 positionMSSkylight = input.WorldPosition.xyz; # endif - float vertexAO = max(max(vertexColor.r, vertexColor.g), vertexColor.b); - float skylightingDiffuse = Skylighting::GetVertexSkylightingDiffuse(positionMSSkylight, normal, vertexAO); + sh2 skylightingSH = Skylighting::Sample(positionMSSkylight, normal, input.HPosition.xy); + float skylightingDiffuse = Skylighting::GetSkylightingDiffuse(skylightingSH, positionMSSkylight, normal, vertexAO); + skylightingDiffuse = min(skylightingDiffuse, lerp(dirSoftShadow, 1.0, SharedData::enbSettings.SkylightingAmbientMinLevel)); # endif // SKYLIGHTING float3 directionalAmbientColor = Color::Ambient(max(0, SharedData::GetAmbient(normal))); # if defined(IBL) - if (SharedData::iblSettings.EnableIBL) { -# if defined(SKYLIGHTING) - directionalAmbientColor = ImageBasedLighting::GetDiffuseIBLOccluded(directionalAmbientColor, -normal, skylightingDiffuse); -# else + if (SharedData::iblSettings.EnableIBL) directionalAmbientColor = ImageBasedLighting::GetDiffuseIBL(directionalAmbientColor, -normal); -# endif - } # endif - diffuseColor += directionalAmbientColor; - float3 albedo = baseColor.xyz * vertexColor; + diffuseColor += directionalAmbientColor; + diffuseColor *= albedo; directionalAmbientColor *= albedo; # if defined(SKYLIGHTING) -# if defined(IBL) - if (!SharedData::iblSettings.EnableIBL) -# endif - { - Skylighting::ApplySkylighting(diffuseColor, directionalAmbientColor, albedo, skylightingDiffuse); - } + Skylighting::ApplySkylighting(diffuseColor, directionalAmbientColor, albedo, skylightingDiffuse); # endif psout.Diffuse.xyz = diffuseColor; @@ -950,6 +957,7 @@ PS_OUTPUT main(PS_INPUT input) psout.Albedo = float4(albedo, 1); psout.Masks = float4(0, 0, Color::RGBToYCoCg(directionalAmbientColor).x, 0); + psout.Masks2 = float4(vertexAO, 0, 0, 0); # endif return psout; diff --git a/package/Shaders/Sky.hlsl b/package/Shaders/Sky.hlsl index a4c83b2f03..a3ea9f8325 100644 --- a/package/Shaders/Sky.hlsl +++ b/package/Shaders/Sky.hlsl @@ -1,5 +1,6 @@ #include "Common/Color.hlsli" #include "Common/FrameBuffer.hlsli" +#include "Common/Math.hlsli" #include "Common/Permutation.hlsli" #include "Common/Random.hlsli" #include "Common/SharedData.hlsli" @@ -43,6 +44,11 @@ struct VS_OUTPUT float4 Color: COLOR0; #endif +#if !defined(OCCLUSION) && !defined(MOONMASK) && !defined(HORIZFADE) + float4 SkyBlendColor0: TEXCOORD5; + float4 SkyBlendColor2: TEXCOORD6; +#endif + float4 WorldPosition: POSITION1; float4 PreviousWorldPosition: POSITION2; #if defined(VR) @@ -133,8 +139,9 @@ VS_OUTPUT main(VS_INPUT input) vsout.Color.xyz = VParams * skyColor; vsout.Color.w = BlendColor[0].w * input.Color.w; - -# endif // OCCLUSION MOONMASK HORIZFADE + vsout.SkyBlendColor0 = float4(BlendColor[0].xyz * VParams, 0); + vsout.SkyBlendColor2 = float4(BlendColor[2].xyz * VParams, 0); +# endif // OCCLUSION MOONMASK HORIZFADE vsout.Position = mul(WorldViewProj[eyeIndex], inputPosition).xyww; vsout.WorldPosition = mul(World[eyeIndex], inputPosition); @@ -186,6 +193,7 @@ cbuffer AlphaTestRefCB : register(b11) # include "Common/MotionBlur.hlsli" # include "Common/SharedData.hlsli" +# include "Common/Random.hlsli" # if defined(CLOUD_SHADOWS) # include "CloudShadows/CloudShadows.hlsli" @@ -193,17 +201,27 @@ cbuffer AlphaTestRefCB : register(b11) # ifdef HDR_OUTPUT # include "HDRDisplay/HDRSun.hlsli" -# include "Common/Random.hlsli" # endif Texture2D TexDepthSampler : register(t17); +float ComputeProceduralSun(float2 uv) +{ + float2 p = uv * 2.0 - 1.0; + float dist = dot(p, p) - SharedData::enbSettings.ProceduralSunDiskRadiusSq; + + float c = saturate(dist * SharedData::enbSettings.ProceduralSunCoronaScale); + float corona = (1.0 - c) * rcp(SharedData::enbSettings.ProceduralSunCoronaFalloff * c + 1.0) * SharedData::enbSettings.ProceduralSunGlowIntensity; + + float disk = saturate(-dist * SharedData::enbSettings.ProceduralSunDiskEdgeScale); + + return corona + disk; +} + PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT psout; - // Color::Sky is float3->float3 (per-channel sky gamma). PParams.yyy broadcasts the packed - // scalar in PParams.y to RGB; float3 matches output .xyz where skyScale is added. - float3 skyScale = Color::Sky(PParams.yyy); + float skyScale = Color::Sky(PParams.yyy).x; # if !defined(VR) uint eyeIndex = 0; # else @@ -225,41 +243,39 @@ PS_OUTPUT main(PS_INPUT input) baseColor = PParams.xxxx * (-baseColor + blendColor) + baseColor; # endif -# ifdef HDR_OUTPUT - float hdrSunGain = HDRSun::GetHdrSunGain( - input.TexCoord0.xy, - baseColor); +# if defined(HDR_OUTPUT) + float hdrSunGain = HDRSun::GetHdrSunGain(input.TexCoord0.xy, baseColor); baseColor.xyz *= hdrSunGain; - if (HDRSun::IsHdrSunActive()) { - // Dither bright output to reduce banding in high-boost sun path. - // Same baseColor/skyScale treatment for DITHER and non-DITHER; DITHER adds noiseGrad later. - baseColor.xyz += (Random::InterleavedGradientNoise(input.Position.xy) - 0.5f) * - (saturate(hdrSunGain - 1.0f) / 255.0f); - skyScale = 0.0f; - } +# endif -# if defined(CLOUD_SHADOWS) - if (HDRSun::IsHdrSunActive()) { - float cloudMult = CloudShadows::GetCloudShadowMult(input.WorldPosition.xyz, SampBaseSampler); - baseColor.xyz *= cloudMult; - baseColor.w *= cloudMult; +# if defined(TEX) + if (SharedData::enbSettings.EnableProceduralSun && (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::IsSun)) { + baseColor.xyz = ComputeProceduralSun(input.TexCoord0.xy); + baseColor.w = input.Color.w; + skyScale = 0.0; } -# endif # endif # if defined(DITHER) float2 noiseGradUv = float2(0.125, 0.125) * input.Position.xy; - float noiseGrad = - TexNoiseGradSampler.Sample(SampNoiseGradSampler, noiseGradUv).x * 0.03125 + -0.0078125; + float noiseGrad = TexNoiseGradSampler.Sample(SampNoiseGradSampler, noiseGradUv).x * 0.03125 - 0.0078125; + noiseGrad *= 10.0; # ifdef TEX - float3 skyVertColor = ENABLE_LL ? (input.Color.xyz + noiseGrad) : input.Color.xyz; - float3 sunGlareColor = Color::Sky(skyVertColor) * baseColor.xyz; - // Dither/noise term is the legacy sky path contribution for gradient smoothing. - psout.Color.xyz = (sunGlareColor + skyScale) + (ENABLE_LL ? 0.0 : noiseGrad); + psout.Color.xyz = Color::Sky(input.Color.xyz) * baseColor.xyz + skyScale; + psout.Color.xyz *= 1.0 + noiseGrad; psout.Color.w = baseColor.w * input.Color.w; # else - psout.Color.xyz = skyScale + Color::Sky(input.Color.xyz + noiseGrad); + float3 skyGradientColor = input.Color.xyz; + if (SharedData::enbSettings.UseProceduralGradientWeights) { + float3 viewDirection = normalize(input.WorldPosition.xyz); + float gradientPosition = pow(1.0 - saturate(viewDirection.z), SharedData::enbSettings.ProceduralGradientWeightCurve); + float3 labA = Color::Correct::BT709ToOKLab(input.SkyBlendColor2.xyz); + float3 labB = Color::Correct::BT709ToOKLab(input.SkyBlendColor0.xyz); + skyGradientColor = Color::Correct::OkLabToBT709(lerp(labA, labB, gradientPosition)); + } + psout.Color.xyz = Color::Sky(skyGradientColor) + skyScale; + psout.Color.xyz *= 1.0 + noiseGrad; psout.Color.w = input.Color.w; # endif // TEX @@ -273,9 +289,109 @@ PS_OUTPUT main(PS_INPUT input) # elif defined(HORIZFADE) psout.Color.xyz = float3(1.5, 1.5, 1.5) * (Color::Sky(input.Color.xyz) * baseColor.xyz + skyScale); psout.Color.w = input.TexCoord2.x * (baseColor.w * input.Color.w); -# else // not DITHER, not MOONMASK, not HORIZFADE +# else + psout.Color.w = input.Color.w * baseColor.w; psout.Color.xyz = Color::Sky(input.Color.xyz) * baseColor.xyz + skyScale; + +# if defined(CLOUDS) + if (SharedData::enbSettings.EnableSky) { + float3 cloudColor = psout.Color.xyz; + float3 viewDirection = normalize(input.WorldPosition.xyz); + + cloudColor.xyz = pow(abs(cloudColor.xyz), SharedData::enbSettings.CloudsCurve); + cloudColor.xyz = lerp(abs(cloudColor.xyz), dot(cloudColor.xyz, 1.0 / 3.0), SharedData::enbSettings.CloudsDesaturation); + + float cloudBaseLuminance = pow(abs(dot(baseColor.xyz, 1.0 / 3.0)), SharedData::enbSettings.CloudsCurve); + + float sunShadow = 1.0; + float masserShadow = 1.0; + float secundaShadow = 1.0; + + if (SharedData::enbSettings.EnableCloudsScattering){ + sunShadow = 0.0; + masserShadow = 0.0; + secundaShadow = 0.0; + + float screenNoise = Random::InterleavedGradientNoise(input.Position.xy, SharedData::FrameCount); + + const uint sampleCount = 8; + const float rcpSampleCount = 1.0 / float(sampleCount); + + { + for (uint i = 0; i < sampleCount; i++) { + float t = (float(i) + screenNoise) * rcpSampleCount; + float3 samplePosition = normalize(lerp(viewDirection, SharedData::SunDirection.xyz, t * 0.1)); + sunShadow += CloudShadows::CloudShadowsTexture.SampleLevel(SampBaseSampler, samplePosition, 0); + } + sunShadow = 1.0 - sunShadow * rcpSampleCount; + } + + if (SharedData::enbSettings.EnableCloudsLightingFromMoon) { + { + for (uint i = 0; i < sampleCount; i++) { + float t = (float(i) + screenNoise) * rcpSampleCount; + float3 samplePosition = normalize(lerp(viewDirection, SharedData::MasserDirection.xyz, t * 0.1)); + masserShadow += CloudShadows::CloudShadowsTexture.SampleLevel(SampBaseSampler, samplePosition, 0); + } + masserShadow = 1.0 - masserShadow * rcpSampleCount; + } + { + for (uint i = 0; i < sampleCount; i++) { + float t = (float(i) + screenNoise) * rcpSampleCount; + float3 samplePosition = normalize(lerp(viewDirection, SharedData::SecundaDirection.xyz, t * 0.1)); + secundaShadow += CloudShadows::CloudShadowsTexture.SampleLevel(SampBaseSampler, samplePosition, 0); + } + secundaShadow = 1.0 - secundaShadow * rcpSampleCount; + } + } + + float cloudLuminance = dot(cloudColor.xyz, 1.0 / 3.0); + + float3 sunScatterColor = SharedData::enbSettings.SkyScatteringColor * SharedData::enbSettings.SkyScatteringIntensity * lerp(1.0, SharedData::SunColor.xyz, SharedData::enbSettings.SkyScatteringColorFromSun); + float sunLighting = saturate(dot(viewDirection, SharedData::SunDirection.xyz) * 0.5 + 0.5); + float3 sunDirectLit = sunScatterColor * sunLighting * sunShadow; + + float3 moonDirectLit = 0.0; + if (SharedData::enbSettings.EnableCloudsLightingFromMoon) { + float3 masserScatterColor = SharedData::enbSettings.SkyScatteringColor * SharedData::enbSettings.SkyScatteringIntensity * lerp(1.0, SharedData::MasserColor.xyz, SharedData::enbSettings.SkyScatteringColorFromSun); + float masserLighting = dot(viewDirection, SharedData::MasserDirection.xyz) * 0.5 + 0.5; + + float3 secundaScatterColor = SharedData::enbSettings.SkyScatteringColor * SharedData::enbSettings.SkyScatteringIntensity * lerp(1.0, SharedData::SecundaColor.xyz, SharedData::enbSettings.SkyScatteringColorFromSun); + float secundaLighting = dot(viewDirection, SharedData::SecundaDirection.xyz) * 0.5 + 0.5; + + moonDirectLit = masserScatterColor * masserLighting * masserShadow + secundaScatterColor * secundaLighting * secundaShadow; + moonDirectLit *= SharedData::enbSettings.SkyScatteringCloudsLightingMoonIntensity; + } + + float3 directLit = sunDirectLit + moonDirectLit; + + float3 colorLit = cloudColor; + colorLit += directLit * cloudBaseLuminance * SharedData::enbSettings.SkyScatteringCloudsLightingSunMinIntensity; + colorLit += directLit * cloudLuminance * SharedData::enbSettings.SkyScatteringCloudsLightingSunMultiplier; + cloudColor = lerp(cloudColor, colorLit, SharedData::enbSettings.SkyScatteringAmount); + } + + if (SharedData::enbSettings.CloudsEdgeIntensity > 0.0) { + float cloudsEdgeAlpha = 1.0 - baseColor.w; + + float3 sunPhase = pow(abs(saturate(dot(viewDirection, SharedData::SunDirection.xyz))), 10.0) * SharedData::SunColor.xyz * sunShadow; + float3 masserPhase = pow(abs(saturate(dot(viewDirection, SharedData::MasserDirection.xyz))), 10.0) * SharedData::MasserColor.xyz * SharedData::enbSettings.CloudsEdgeMoonMultiplier * masserShadow; + float3 secundaPhase = pow(abs(saturate(dot(viewDirection, SharedData::SecundaDirection.xyz))), 10.0) * SharedData::SecundaColor.xyz * SharedData::enbSettings.CloudsEdgeMoonMultiplier * secundaShadow; + + float3 cloudsScatter = (sunPhase + masserPhase + secundaPhase) * SharedData::enbSettings.CloudsEdgeIntensity; + + if (SharedData::enbSettings.EnableCloudsScattering) + cloudsScatter *= 2.0; + + cloudColor += cloudBaseLuminance * cloudsScatter * cloudsEdgeAlpha; + } + + psout.Color.xyz = cloudColor; + + input.Color.w = saturate(input.Color.w); + } +# endif # endif # else @@ -288,7 +404,7 @@ PS_OUTPUT main(PS_INPUT input) psout.Normal = float4(0.5, 0.5, 0, psout.Color.w); # if defined(CLOUD_SHADOWS) && defined(CLOUDS) && !defined(DEFERRED) - psout.CloudShadows = float4(1, 1, 1, psout.Color.w); + psout.CloudShadows = psout.Color.w; // Keep sun behind scene depth to prevent halo leaks through geometry. float depth = TexDepthSampler.Load(int3(input.Position.xy, 0)); diff --git a/package/Shaders/Tests/TestHair.hlsl b/package/Shaders/Tests/TestHair.hlsl index a51e9d527c..c455c9ec64 100644 --- a/package/Shaders/Tests/TestHair.hlsl +++ b/package/Shaders/Tests/TestHair.hlsl @@ -6,6 +6,7 @@ // 3. Testing Hair namespace functions directly #define CS_HAIR #define HAIR +#define UNIT_TEST // ============================================================================ // STUBS FOR EXTERNAL DEPENDENCIES (must be defined BEFORE including Hair.hlsli) @@ -74,9 +75,6 @@ namespace SharedData // These are all real files from the codebase that will be included automatically #include "/Shaders/Hair/Hair.hlsli" -// Include common dependencies needed for tests (LightingCommon provides struct definitions) -#include "/Shaders/Common/LightingCommon.hlsli" - #include "/Test/STF/ShaderTestFramework.hlsli" // Test tolerance constants diff --git a/package/Shaders/Water.hlsl b/package/Shaders/Water.hlsl index 87927eaece..54f1913a9c 100644 --- a/package/Shaders/Water.hlsl +++ b/package/Shaders/Water.hlsl @@ -1182,15 +1182,12 @@ PS_OUTPUT main(PS_INPUT input) float3 dirColor; float3 ambientColor; -# if defined(SKYLIGHTING) && !defined(INTERIOR) - ShadowSampling::ExtractLighting(diffuseOutput.refractionDiffuseColor, dirColor, ambientColor, skylightingDiffuse); -# else ShadowSampling::ExtractLighting(diffuseOutput.refractionDiffuseColor, dirColor, ambientColor); -# endif dirColor *= dirShadow; # if defined(SKYLIGHTING) + skylightingDiffuse = min(skylightingDiffuse, lerp(dirShadow, 1.0, SharedData::enbSettings.SkylightingAmbientMinLevel)); ambientColor = Color::IrradianceToLinear(ambientColor); ambientColor *= skylightingDiffuse; ambientColor = Color::IrradianceToGamma(ambientColor); diff --git a/src/Deferred.cpp b/src/Deferred.cpp index 870d1354e3..87de509f60 100644 --- a/src/Deferred.cpp +++ b/src/Deferred.cpp @@ -6,13 +6,17 @@ #include "State.h" #include "Utils/D3D.h" +#include "Features/CloudShadows.h" #include "Features/DynamicCubemaps.h" +#include "Features/Effect11.h" #include "Features/IBL.h" #include "Features/ScreenSpaceGI.h" +#include "Features/TerrainShadows.h" #include "Features/Skylighting.h" #include "Features/SubsurfaceScattering.h" #include "Features/TerrainBlending.h" #include "Features/Upscaling.h" +#include "Features/VolumetricShadows.h" #include "Features/VR.h" #include "Features/WeatherEditor.h" @@ -109,6 +113,8 @@ void Deferred::SetupResources() SetupRenderTarget(NORMALROUGHNESS, texDesc, srvDesc, rtvDesc, uavDesc, DXGI_FORMAT_R10G10B10A2_UNORM, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE); // Masks SetupRenderTarget(MASKS, texDesc, srvDesc, rtvDesc, uavDesc, DXGI_FORMAT_R11G11B10_FLOAT, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE); + // Masks2 (vertexAO, supports blending) + SetupRenderTarget(MASKS2, texDesc, srvDesc, rtvDesc, uavDesc, DXGI_FORMAT_R16G16B16A16_FLOAT, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE); // TAA Water Buffers SetupRenderTarget(RE::RENDER_TARGETS::kWATER_1, texDesc, srvDesc, rtvDesc, uavDesc, DXGI_FORMAT_R16G16B16A16_FLOAT, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE); @@ -177,7 +183,9 @@ void Deferred::ReflectionsPrepasses() globals::game::stateUpdateFlags->set(RE::BSGraphics::ShaderFlags::DIRTY_RENDERTARGET); // Run OMSetRenderTargets again - Feature::ForEachLoadedFeature("ReflectionsPrepass", [](Feature* feature) { feature->ReflectionsPrepass(); }, true); + Feature::ForEachLoadedFeature("ReflectionsPrepass", [](Feature* feature) { + feature->ReflectionsPrepass(); + }, true); } void Deferred::EarlyPrepasses() @@ -200,7 +208,9 @@ void Deferred::EarlyPrepasses() // Shadow maps have just been rendered — upload BSShadowDirectionalLight data to t98. CopyShadowLightData(); - Feature::ForEachLoadedFeature("EarlyPrepass", [](Feature* feature) { feature->EarlyPrepass(); }, true); + Feature::ForEachLoadedFeature("EarlyPrepass", [](Feature* feature) { + feature->EarlyPrepass(); + }, true); } void Deferred::PrepassPasses() @@ -216,7 +226,9 @@ void Deferred::PrepassPasses() auto context = globals::d3d::context; context->OMSetRenderTargets(0, nullptr, nullptr); // Unbind all bound render targets - Feature::ForEachLoadedFeature("Prepass", [](Feature* feature) { feature->Prepass(); }, true); + Feature::ForEachLoadedFeature("Prepass", [](Feature* feature) { + feature->Prepass(); + }, true); } void Deferred::StartDeferred() @@ -243,7 +255,7 @@ void Deferred::StartDeferred() SPECULAR, REFLECTANCE, MASKS, - RE::RENDER_TARGET::kNONE + MASKS2 }; for (uint i = 2; i < 8; i++) { @@ -317,6 +329,7 @@ void Deferred::DeferredPasses() auto albedo = renderer->GetRuntimeData().renderTargets[ALBEDO]; auto normalRoughness = renderer->GetRuntimeData().renderTargets[NORMALROUGHNESS]; auto masks = renderer->GetRuntimeData().renderTargets[MASKS]; + auto masks2 = renderer->GetRuntimeData().renderTargets[MASKS2]; auto main = renderer->GetRuntimeData().renderTargets[forwardRenderTargets[0]]; auto normals = renderer->GetRuntimeData().renderTargets[forwardRenderTargets[2]]; @@ -329,6 +342,8 @@ void Deferred::DeferredPasses() auto& skylighting = globals::features::skylighting; + auto* profiler = globals::profiler; + auto& ssgi = globals::features::screenSpaceGI; if (ssgi.loaded) ssgi.DrawSSGI(); @@ -361,7 +376,7 @@ void Deferred::DeferredPasses() dynamicCubemaps.loaded ? dynamicCubemaps.envTexture->srv.get() : nullptr, // t6 EnvTexture dynamicCubemaps.loaded ? dynamicCubemaps.envReflectionsTexture->srv.get() : nullptr, // t7 EnvReflectionsTexture dynamicCubemaps.loaded && skylighting.loaded ? skylighting.texProbeArray->srv.get() : nullptr, // t8 SkylightingProbeArray - nullptr, // t9 unused + masks2.SRV, // t9 Masks2Texture ssgi_ao, // t10 SsgiAoTexture ssgi_hq_spec ? nullptr : ssgi_y, // t11 SsgiYTexture ssgi_hq_spec ? nullptr : ssgi_cocg, // t12 SsgiCoCgTexture @@ -391,7 +406,9 @@ void Deferred::DeferredPasses() { TracyD3D11Zone(globals::state->tracyCtx, "Deferred Composite - Dispatch"); + profiler->BeginPass("DeferredComposite"); context->Dispatch(dispatchCount.x, dispatchCount.y, 1); + profiler->EndPass(); } // Unbind mode texture SRV @@ -411,7 +428,9 @@ void Deferred::DeferredPasses() // VR: Stereo reprojection fills Eye 1 holes here (after DeferredComposite, before SSR/water/sky) // so that ISReflectionsRayTracing sees valid pixels in both eyes. if (globals::game::isVR) { + profiler->BeginPass("VR::StereoBlend"); globals::features::vr.DrawStereoBlend(); + profiler->EndPass(); } // Clear @@ -430,6 +449,8 @@ void Deferred::DeferredPasses() if (dynamicCubemaps.loaded) dynamicCubemaps.PostDeferred(); + + globals::features::effect11.DrawVolumetricRays(); } void Deferred::EndDeferred() @@ -591,6 +612,8 @@ void Deferred::CopyShadowLightData() else SetShadowCascadeParameters(sunShadowLight->GetRuntimeData(), dd); + dd.CascadeDepthParams = globals::features::volumetricShadows.GetCascadeDepthParams(); + D3D11_MAPPED_SUBRESOURCE mapped{}; DX::ThrowIfFailed(context->Map(directionalShadowLights->resource.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped)); memcpy(mapped.pData, &dd, sizeof(DirectionalShadowLightData)); @@ -680,6 +703,7 @@ ID3D11ComputeShader* Deferred::GetComputeMainCompositeInterior() return mainCompositeInteriorCS; } + void Deferred::Hooks::Main_RenderShadowMaps::thunk() { func(); diff --git a/src/Deferred.h b/src/Deferred.h index c95f28ed2a..9f358c81c5 100644 --- a/src/Deferred.h +++ b/src/Deferred.h @@ -27,6 +27,7 @@ class Deferred float4x4 InvShadowProj[2]; float2 EndSplitDistances; float2 StartSplitDistances; + float4 CascadeDepthParams; }; STATIC_ASSERT_ALIGNAS_16(DirectionalShadowLightData); diff --git a/src/Feature.cpp b/src/Feature.cpp index aa0e23992f..0b4f002529 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -4,6 +4,7 @@ #include "FeatureVersions.h" #include "Features/CloudShadows.h" #include "Features/DynamicCubemaps.h" +#include "Features/Effect11.h" #include "Features/ExponentialHeightFog.h" #include "Features/ExtendedMaterials.h" #include "Features/ExtendedTranslucency.h" @@ -37,6 +38,7 @@ #include "Features/WaterEffects.h" #include "Features/WeatherEditor.h" #include "Features/WetnessEffects.h" + #include "Menu.h" #include "SettingsOverrideManager.h" #include "Utils/Format.h" @@ -243,6 +245,7 @@ const std::vector& Feature::GetFeatureList() &globals::features::weatherEditor, &globals::features::screenshotFeature, &globals::features::linearLighting, + &globals::features::effect11, &globals::features::unifiedWater, &globals::features::exponentialHeightFog, &globals::features::hdrDisplay diff --git a/src/FeatureBuffer.cpp b/src/FeatureBuffer.cpp index 98f5aa834b..5f890a41ec 100644 --- a/src/FeatureBuffer.cpp +++ b/src/FeatureBuffer.cpp @@ -2,6 +2,7 @@ #include "Features/CloudShadows.h" #include "Features/DynamicCubemaps.h" +#include "Features/Effect11.h" #include "Features/ExponentialHeightFog.h" #include "Features/ExtendedMaterials.h" #include "Features/ExtendedTranslucency.h" @@ -44,14 +45,15 @@ std::pair GetFeatureBufferData(bool a_inWorld) globals::features::lightLimitFix.GetCommonBufferData(), globals::features::wetnessEffects.GetCommonBufferData(), globals::features::skylighting.GetCommonBufferData(a_inWorld), - globals::features::cloudShadows.settings, + globals::features::cloudShadows.GetCommonBufferData(), globals::features::lodBlending.settings, globals::features::hairSpecular.settings, globals::features::terrainVariation.settings, globals::features::ibl.GetCommonBufferData(), globals::features::extendedTranslucency.GetCommonBufferData(), globals::features::linearLighting.GetCommonBufferData(), + globals::features::effect11.GetCommonBufferData(), globals::features::terrainBlending.settings, - globals::features::exponentialHeightFog.settings, + globals::features::exponentialHeightFog.GetCommonBufferData(), globals::features::truePBR.settings); } \ No newline at end of file diff --git a/src/Features/CloudShadows.cpp b/src/Features/CloudShadows.cpp index 4cb8676686..43aeab48f6 100644 --- a/src/Features/CloudShadows.cpp +++ b/src/Features/CloudShadows.cpp @@ -1,5 +1,7 @@ #include "CloudShadows.h" +#include "Effect11.h" +#include "Effect11/SettingManager.h" #include "State.h" #include "Utils/D3D.h" @@ -9,7 +11,15 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void CloudShadows::DrawSettings() { - ImGui::SliderFloat("Opacity", &settings.Opacity, 0.0f, 1.0f, "%.1f"); + if (globals::features::effect11.loaded) { + auto& enb = globals::features::effect11; + if (enb.enableEffect) { + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Settings are currently managed by ENB."); + return; + } + } + + ImGui::SliderFloat("Opacity", &settings.Opacity, 0.0f, 4.0f, "%.1f"); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( "Higher values make cloud shadows darker."); @@ -31,16 +41,84 @@ void CloudShadows::RestoreDefaultSettings() settings = {}; } +CloudShadows::Settings CloudShadows::GetCommonBufferData() +{ + if (!loaded) + return settings; + + auto data = settings; + + if (globals::features::effect11.loaded) { + auto& enb = globals::features::effect11; + if (enb.enableEffect) { + auto& settingManager = SettingManager::GetSingleton(); + if (settingManager.GetValue("EnableCloudShadows", "EFFECT")) { + data.Opacity = settingManager.GetInterpolatedTimeOfDayValue("Amount", "CLOUDSHADOWS"); + } else { + data.Opacity = 0.0f; + } + } + } + + return data; +} + void CloudShadows::CheckResourcesSide(int side) { static Util::FrameChecker frame_checker[6]; if (!frame_checker[side].IsNewFrame()) return; + if (previouslyRenderedSide >= 0 && previouslyRenderedSide != side) + PropagateToCompletion(previouslyRenderedSide); + previouslyRenderedSide = side; + auto context = globals::d3d::context; float black[4] = { 0, 0, 0, 0 }; - context->ClearRenderTargetView(cubemapCloudOccRTVs[side], black); + context->ClearRenderTargetView(cloudShadowLayerRTVs[0][side], black); + renderedLayersMask[side] = 0; +} + +void CloudShadows::PropagateToCompletion(int side) +{ + uint32_t mask = renderedLayersMask[side]; + unsigned long highBit; + int fromLayer = _BitScanReverse(&highBit, mask) ? static_cast(highBit) : 0; + + auto context = globals::d3d::context; + + uint32_t newLayers = mask & ~globalRenderedMask; + if (newLayers) { + unsigned long bit; + uint32_t remaining = newLayers; + while (_BitScanForward(&bit, remaining)) { + int newLayer = static_cast(bit); + for (int otherSide = 0; otherSide < 6; otherSide++) { + if (otherSide == side) + continue; + if (renderedLayersMask[otherSide] & (1u << newLayer)) + continue; + uint32_t belowMask = renderedLayersMask[otherSide] & ((1u << newLayer) - 1); + unsigned long nearestBit; + int srcLayer = _BitScanReverse(&nearestBit, belowMask) ? static_cast(nearestBit) : 0; + UINT otherSub = D3D11CalcSubresource(0, otherSide, cubemapMipLevels); + context->CopySubresourceRegion( + texCloudShadowLayers[newLayer]->resource.get(), otherSub, 0, 0, 0, + texCloudShadowLayers[srcLayer]->resource.get(), otherSub, nullptr); + renderedLayersMask[otherSide] |= (1u << newLayer); + } + remaining &= ~(1u << bit); + } + globalRenderedMask |= mask; + } + + if (fromLayer < kMaxCloudLayers - 1) { + UINT subresource = D3D11CalcSubresource(0, side, cubemapMipLevels); + context->CopySubresourceRegion( + texCloudShadowLayers[kMaxCloudLayers - 1]->resource.get(), subresource, 0, 0, 0, + texCloudShadowLayers[fromLayer]->resource.get(), subresource, nullptr); + } } void CloudShadows::SkyShaderHacks() @@ -51,7 +129,6 @@ void CloudShadows::SkyShaderHacks() auto reflections = renderer->GetRendererData().cubemapRenderTargets[RE::RENDER_TARGET_CUBEMAP::kREFLECTIONS]; - // render targets ID3D11RenderTargetView* rtvs[4]; ID3D11DepthStencilView* dsv; context->OMGetRenderTargets(3, rtvs, &dsv); @@ -67,7 +144,27 @@ void CloudShadows::SkyShaderHacks() CheckResourcesSide(side); - rtvs[3] = cubemapCloudOccRTVs[side]; + int layer = currentLayerForDraw; + + unsigned long highBit; + int prevLayer = _BitScanReverse(&highBit, renderedLayersMask[side]) ? static_cast(highBit) : -1; + + UINT subresource = D3D11CalcSubresource(0, side, cubemapMipLevels); + + int fromLayer = std::max(prevLayer, 0); + + context->CopyResource(texSelfShadowCopy->resource.get(), texCloudShadowLayers[layer]->resource.get()); + + if (layer > 0) { + context->CopySubresourceRegion( + texCloudShadowLayers[layer]->resource.get(), subresource, 0, 0, 0, + texCloudShadowLayers[fromLayer]->resource.get(), subresource, nullptr); + } + + ID3D11ShaderResourceView* selfShadowSrv = texSelfShadowCopy->srv.get(); + context->PSSetShaderResources(26, 1, &selfShadowSrv); + + rtvs[3] = cloudShadowLayerRTVs[layer][side]; context->OMSetRenderTargets(4, rtvs, nullptr); float blendFactor[4] = { 1.0f, 1.0f, 1.0f, 1.0f }; @@ -78,7 +175,6 @@ void CloudShadows::SkyShaderHacks() auto cubemapDepth = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kCUBEMAP_REFLECTIONS]; context->PSSetShaderResources(17, 1, &cubemapDepth.depthSRV); - // Release COM objects to prevent memory leaks for (int i = 0; i < 3; ++i) { if (rtvs[i]) rtvs[i]->Release(); @@ -86,23 +182,47 @@ void CloudShadows::SkyShaderHacks() if (dsv) dsv->Release(); + renderedLayersMask[side] |= (1u << layer); + overrideSky = false; } } +int CloudShadows::FindCloudLayer(RE::BSRenderPass* Pass) +{ + auto sky = globals::game::sky; + if (!sky || !sky->clouds) + return -1; + + for (int i = 0; i < kMaxCloudLayers; i++) { + if (sky->clouds->clouds[i].get() == Pass->geometry) + return i; + } + return -1; +} + void CloudShadows::ModifySky(RE::BSRenderPass* Pass) { auto shadowState = globals::game::shadowState; GET_INSTANCE_MEMBER(cubeMapRenderTarget, shadowState); - if (cubeMapRenderTarget != RE::RENDER_TARGETS_CUBEMAP::kREFLECTIONS) + auto skyProperty = static_cast(Pass->shaderProperty); + + if (skyProperty->uiSkyObjectType != RE::BSSkyShaderProperty::SkyObject::SO_CLOUDS) return; - auto skyProperty = static_cast(Pass->shaderProperty); + int layer = FindCloudLayer(Pass); + if (layer < 0) + return; - if (skyProperty->uiSkyObjectType == RE::BSSkyShaderProperty::SkyObject::SO_CLOUDS) { + if (cubeMapRenderTarget == RE::RENDER_TARGETS_CUBEMAP::kREFLECTIONS) { + currentLayerForDraw = layer; overrideSky = true; + } else { + auto context = globals::d3d::context; + ID3D11ShaderResourceView* srv = texCloudShadowLayers[layer]->srv.get(); + context->PSSetShaderResources(26, 1, &srv); } } @@ -116,7 +236,7 @@ void CloudShadows::ReflectionsPrepass() auto context = globals::d3d::context; - context->CopyResource(texCubemapCloudOccCopy->resource.get(), texCubemapCloudOcc->resource.get()); + context->CopyResource(texCubemapCloudOccCopy->resource.get(), texCloudShadowLayers[kMaxCloudLayers - 1]->resource.get()); ID3D11ShaderResourceView* srv = texCubemapCloudOccCopy->srv.get(); context->PSSetShaderResources(25, 1, &srv); @@ -126,15 +246,10 @@ void CloudShadows::ReflectionsPrepass() void CloudShadows::EarlyPrepass() { - if ((globals::game::sky->mode.get() != RE::Sky::Mode::kFull) || - !globals::game::sky->currentClimate) - return; - - auto context = globals::d3d::context; - - ID3D11ShaderResourceView* srv = texCubemapCloudOcc->srv.get(); - context->PSSetShaderResources(25, 1, &srv); - context->CSSetShaderResources(25, 1, &srv); + if (previouslyRenderedSide >= 0) { + PropagateToCompletion(previouslyRenderedSide); + previouslyRenderedSide = -1; + } } void CloudShadows::SetupResources() @@ -153,26 +268,27 @@ void CloudShadows::SetupResources() reflections.SRV->GetDesc(&srvDesc); texDesc.Format = srvDesc.Format = DXGI_FORMAT_R8_UNORM; - - texCubemapCloudOcc = new Texture2D(texDesc, "CloudShadows::CubemapCloudOcc"); - texCubemapCloudOcc->CreateSRV(srvDesc); - - for (int i = 0; i < 6; ++i) { - reflections.cubeSideRTV[i]->GetDesc(&rtvDesc); - rtvDesc.Format = texDesc.Format; - DX::ThrowIfFailed(device->CreateRenderTargetView(texCubemapCloudOcc->resource.get(), &rtvDesc, cubemapCloudOccRTVs + i)); - Util::SetResourceName(cubemapCloudOccRTVs[i], "CloudShadows::CubemapCloudOcc RTV[%d]", i); + cubemapMipLevels = texDesc.MipLevels; + + for (int layer = 0; layer < kMaxCloudLayers; ++layer) { + char name[64]; + snprintf(name, sizeof(name), "CloudShadows::Layer[%d]", layer); + texCloudShadowLayers[layer] = new Texture2D(texDesc, name); + texCloudShadowLayers[layer]->CreateSRV(srvDesc); + + for (int face = 0; face < 6; ++face) { + reflections.cubeSideRTV[face]->GetDesc(&rtvDesc); + rtvDesc.Format = texDesc.Format; + DX::ThrowIfFailed(device->CreateRenderTargetView(texCloudShadowLayers[layer]->resource.get(), &rtvDesc, &cloudShadowLayerRTVs[layer][face])); + Util::SetResourceName(cloudShadowLayerRTVs[layer][face], "CloudShadows::Layer[%d] RTV[%d]", layer, face); + } } texCubemapCloudOccCopy = new Texture2D(texDesc, "CloudShadows::CubemapCloudOccCopy"); texCubemapCloudOccCopy->CreateSRV(srvDesc); - for (int i = 0; i < 6; ++i) { - reflections.cubeSideRTV[i]->GetDesc(&rtvDesc); - rtvDesc.Format = texDesc.Format; - DX::ThrowIfFailed(device->CreateRenderTargetView(texCubemapCloudOccCopy->resource.get(), &rtvDesc, cubemapCloudOccCopyRTVs + i)); - Util::SetResourceName(cubemapCloudOccCopyRTVs[i], "CloudShadows::CubemapCloudOccCopy RTV[%d]", i); - } + texSelfShadowCopy = new Texture2D(texDesc, "CloudShadows::SelfShadowCopy"); + texSelfShadowCopy->CreateSRV(srvDesc); } { D3D11_BLEND_DESC blendDesc = {}; diff --git a/src/Features/CloudShadows.h b/src/Features/CloudShadows.h index 3ab14cbfef..8cff0fb076 100644 --- a/src/Features/CloudShadows.h +++ b/src/Features/CloudShadows.h @@ -6,6 +6,8 @@ struct CloudShadows : Feature static constexpr std::string_view MOD_ID = "139185"; public: + static constexpr int kMaxCloudLayers = 32; + struct alignas(16) Settings { float Opacity = 0.8f; @@ -35,11 +37,17 @@ struct CloudShadows : Feature bool overrideSky = false; void SkyShaderHacks(); - Texture2D* texCubemapCloudOcc = nullptr; + Texture2D* texCloudShadowLayers[kMaxCloudLayers] = {}; + ID3D11RenderTargetView* cloudShadowLayerRTVs[kMaxCloudLayers][6] = {}; Texture2D* texCubemapCloudOccCopy = nullptr; + Texture2D* texSelfShadowCopy = nullptr; + + UINT cubemapMipLevels = 1; + int currentLayerForDraw = 0; - ID3D11RenderTargetView* cubemapCloudOccRTVs[6] = { nullptr }; - ID3D11RenderTargetView* cubemapCloudOccCopyRTVs[6] = { nullptr }; + uint32_t renderedLayersMask[6] = {}; + uint32_t globalRenderedMask = 0; + int previouslyRenderedSide = -1; ID3D11BlendState* cloudShadowBlendState = nullptr; @@ -52,7 +60,11 @@ struct CloudShadows : Feature virtual void RestoreDefaultSettings() override; + Settings GetCommonBufferData(); + void CheckResourcesSide(int side); + void PropagateToCompletion(int side); + int FindCloudLayer(RE::BSRenderPass* Pass); void ModifySky(RE::BSRenderPass* Pass); virtual void ReflectionsPrepass() override; diff --git a/src/Features/DynamicCubemaps.cpp b/src/Features/DynamicCubemaps.cpp index 560c902452..8fddc96e59 100644 --- a/src/Features/DynamicCubemaps.cpp +++ b/src/Features/DynamicCubemaps.cpp @@ -126,6 +126,7 @@ void DynamicCubemaps::DrawSettings() ImGui::TreePop(); } } + } void DynamicCubemaps::LoadSettings(json& o_json) @@ -374,7 +375,9 @@ void DynamicCubemaps::UpdateCubemapCapture(bool a_reflections) context->CSSetShader(a_reflections ? (fakeReflections ? GetComputeShaderUpdateFakeReflections() : GetComputeShaderUpdateReflections()) : GetComputeShaderUpdate(), nullptr, 0); + globals::profiler->BeginPass(a_reflections ? "DynamicCubemaps::CaptureReflections" : "DynamicCubemaps::Capture"); context->Dispatch((uint32_t)std::ceil(envCaptureTexture->desc.Width / 8.0f), (uint32_t)std::ceil(envCaptureTexture->desc.Height / 8.0f), 6); + globals::profiler->EndPass(); uavs[0] = nullptr; uavs[1] = nullptr; @@ -415,7 +418,9 @@ void DynamicCubemaps::Inferrence(bool a_reflections) context->CSSetShader(a_reflections ? (fakeReflections ? GetComputeShaderInferrenceFakeReflections() : GetComputeShaderInferrenceReflections()) : GetComputeShaderInferrence(), nullptr, 0); + globals::profiler->BeginPass(a_reflections ? "DynamicCubemaps::InferReflections" : "DynamicCubemaps::Infer"); context->Dispatch((uint32_t)std::ceil(envCaptureTexture->desc.Width / 8.0f), (uint32_t)std::ceil(envCaptureTexture->desc.Height / 8.0f), 6); + globals::profiler->EndPass(); srvs[0] = nullptr; srvs[1] = nullptr; @@ -458,6 +463,7 @@ void DynamicCubemaps::Irradiance(bool a_reflections) std::uint32_t size = std::max(envTexture->desc.Width, envTexture->desc.Height) / 2; + globals::profiler->BeginPass(a_reflections ? "DynamicCubemaps::IrradianceReflections" : "DynamicCubemaps::Irradiance"); for (std::uint32_t level = 1; level < MIPLEVELS; level++, size /= 2) { const UINT numGroups = (UINT)std::max(1u, size / 8); @@ -469,6 +475,7 @@ void DynamicCubemaps::Irradiance(bool a_reflections) context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); context->Dispatch(numGroups, numGroups, 6); } + globals::profiler->EndPass(); } ID3D11ShaderResourceView* nullSRV = { nullptr }; @@ -503,6 +510,7 @@ void DynamicCubemaps::CompressToBC6H(bool a_reflections) std::uint32_t mipDim = std::max(envTexture->desc.Width, envTexture->desc.Height); + globals::profiler->BeginPass(a_reflections ? "DynamicCubemaps::BC6HReflections" : "DynamicCubemaps::BC6H"); for (std::uint32_t level = 0; level < bc6hMipLevels; ++level) { std::uint32_t srcWidth = std::max(1u, mipDim >> level); std::uint32_t srcHeight = std::max(1u, mipDim >> level); @@ -521,6 +529,7 @@ void DynamicCubemaps::CompressToBC6H(bool a_reflections) std::uint32_t dispatchY = std::max(1u, (blocksY + 7) / 8); context->Dispatch(dispatchX, dispatchY, 6); } + globals::profiler->EndPass(); { ID3D11ShaderResourceView* nullSRV = nullptr; diff --git a/src/Features/Effect11.cpp b/src/Features/Effect11.cpp new file mode 100644 index 0000000000..0b8c770ff5 --- /dev/null +++ b/src/Features/Effect11.cpp @@ -0,0 +1,931 @@ +#include "Effect11.h" + +#include + +#include "Effect11/D3D11StateBackup.h" +#include "Effect11/ENBHelper.h" +#include "Effect11/EffectManager.h" +#include "Effect11/MenuManager.h" +#include "Effect11/PresetManager.h" +#include "Effect11/SettingManager.h" +#include "Effect11/WeatherManager.h" + +#include "CloudShadows.h" +#include "Deferred.h" +#include "IBL.h" +#include "ShaderCache.h" +#include "State.h" +#include "TerrainShadows.h" +#include "Utils/D3D.h" +#include "Utils/Game.h" + +Effect11::PerFrame Effect11::GetCommonBufferData() +{ + CheckCommonData(); + + auto& settingManager = SettingManager::GetSingleton(); + PerFrame data{}; + + data.Enable = enableEffect; + data.EnableSky = enableEffect && settingManager.GetValue("Enable", "SKY"); + data.ColorPow = settingManager.GetInterpolatedTimeOfDayValue("ColorPow", "ENVIRONMENT"); + + data.CloudsCurve = settingManager.GetInterpolatedTimeOfDayValue("CloudsCurve", "SKY"); + data.CloudsDesaturation = settingManager.GetInterpolatedTimeOfDayValue("CloudsDesaturation", "SKY"); + data.CloudsEdgeIntensity = settingManager.GetValue("CloudsEdgeIntensity", "SKY"); + data.CloudsEdgeMoonMultiplier = settingManager.GetValue("CloudsEdgeMoonMultiplier", "SKY"); + + data.VolumetricRaysDesaturation = settingManager.GetInterpolatedTimeOfDayValue("Desaturation", "GAMEVOLUMETRICRAYS"); + auto colorFilter = settingManager.GetInterpolatedColorTimeOfDayValue("ColorFilter", "GAMEVOLUMETRICRAYS"); + data.VolumetricRaysColorFilter = { colorFilter.x, colorFilter.y, colorFilter.z }; + + data.UseProceduralGradientWeights = enableEffect && settingManager.GetValue("UseProceduralGradientWeights", "SKY"); + data.ProceduralGradientWeightCurve = settingManager.GetInterpolatedTimeOfDayValue("ProceduralGradientWeightCurve", "SKY"); + + data.LightSpriteIntensity = settingManager.GetInterpolatedTimeOfDayValue("Intensity", "LIGHTSPRITE"); + + data.ParticleIntensity = settingManager.GetInterpolatedTimeOfDayValue("Intensity", "PARTICLE"); + data.ParticleLightingInfluence = settingManager.GetInterpolatedTimeOfDayValue("LightingInfluence", "PARTICLE"); + data.ParticleAmbientInfluence = settingManager.GetInterpolatedTimeOfDayValue("AmbientInfluence", "PARTICLE"); + data.ParticlePointLightingInfluence = settingManager.GetInterpolatedTimeOfDayValue("PointLightingInfluence", "PARTICLE"); + + data.EnableCloudsLightingFromMoon = settingManager.GetValue("EnableCloudsLightingFromMoon", "SKYSCATTERING"); + data.ScatteringColorHDRWeighting = settingManager.GetValue("ScatteringColorHDRWeighting", "SKYSCATTERING"); + data.SkyScatteringAtmosphereThickness = settingManager.GetInterpolatedTimeOfDayValue("AtmosphereThickness", "SKYSCATTERING"); + data.SkyScatteringHorizonRange = settingManager.GetInterpolatedTimeOfDayValue("HorizonRange", "SKYSCATTERING"); + data.SkyScatteringIntensity = settingManager.GetInterpolatedTimeOfDayValue("Intensity", "SKYSCATTERING"); + data.SkyScatteringAmount = settingManager.GetInterpolatedTimeOfDayValue("Amount", "SKYSCATTERING"); + data.SkyScatteringDustVolume = settingManager.GetInterpolatedTimeOfDayValue("DustVolume", "SKYSCATTERING"); + data.SkyScatteringDustDensity = settingManager.GetInterpolatedTimeOfDayValue("DustDensity", "SKYSCATTERING"); + data.SkyScatteringDustDarkening = settingManager.GetInterpolatedTimeOfDayValue("DustDarkening", "SKYSCATTERING"); + data.SkyScatteringShadowAmount = settingManager.GetInterpolatedTimeOfDayValue("ShadowAmount", "SKYSCATTERING"); + data.SkyScatteringColorFromSun = settingManager.GetInterpolatedTimeOfDayValue("ColorFromSun", "SKYSCATTERING"); + auto scatteringColor = settingManager.GetInterpolatedColorTimeOfDayValue("ScatteringColor", "SKYSCATTERING"); + data.SkyScatteringColor = { scatteringColor.x, scatteringColor.y, scatteringColor.z }; + data.SkyScatteringAirGlowIntensity = settingManager.GetInterpolatedTimeOfDayValue("AirGlowIntensity", "SKYSCATTERING"); + data.SkyScatteringAirGlowRange = settingManager.GetInterpolatedTimeOfDayValue("AirGlowRange", "SKYSCATTERING"); + data.SkyScatteringSunGlowIntensity = settingManager.GetInterpolatedTimeOfDayValue("SunGlowIntensity", "SKYSCATTERING"); + data.SkyScatteringSunGlowRange = settingManager.GetInterpolatedTimeOfDayValue("SunGlowRange", "SKYSCATTERING"); + data.SkyScatteringMoonGlowAmount = settingManager.GetInterpolatedTimeOfDayValue("MoonGlowAmount", "SKYSCATTERING"); + data.SkyScatteringMoonGlowRange = settingManager.GetInterpolatedTimeOfDayValue("MoonGlowRange", "SKYSCATTERING"); + data.SkyScatteringCloudsLightingSunMinIntensity = settingManager.GetInterpolatedTimeOfDayValue("CloudsLightingSunMinIntensity", "SKYSCATTERING"); + data.SkyScatteringCloudsLightingSunMultiplier = settingManager.GetInterpolatedTimeOfDayValue("CloudsLightingSunMultiplier", "SKYSCATTERING"); + data.SkyScatteringCloudsLightingMoonIntensity = settingManager.GetInterpolatedTimeOfDayValue("CloudsLightingMoonIntensity", "SKYSCATTERING"); + + data.EnableCloudsScattering = enableEffect && settingManager.GetValue("EnableCloudsScattering", "EFFECT"); + + data.EnableVolumetricRays = enableEffect && settingManager.GetValue("EnableVolumetricRays", "EFFECT"); + data.VolumetricRaysIntensity = settingManager.GetInterpolatedTimeOfDayValue("Intensity", "VOLUMETRICRAYS"); + { + float density = std::max(0.1f, settingManager.GetInterpolatedTimeOfDayValue("Density", "VOLUMETRICRAYS")); + data.VolumetricRaysExtinction = 0.000003f / density; + } + data.VolumetricRaysSkyColorAmount = settingManager.GetInterpolatedTimeOfDayValue("SkyColorAmount", "VOLUMETRICRAYS"); + + data.FireIntensity = settingManager.GetInterpolatedTimeOfDayValue("Intensity", "FIRE"); + data.FireCurve = settingManager.GetInterpolatedTimeOfDayValue("Curve", "FIRE"); + data.AuroraIntensity = settingManager.GetInterpolatedTimeOfDayValue("AuroraBorealisIntensity", "SKY"); + data.AuroraCurve = settingManager.GetInterpolatedTimeOfDayValue("AuroraBorealisCurve", "SKY"); + + data.EnableRain = enableEffect && raindropSRV && settingManager.GetValue("Enable", "RAIN"); + data.RainMotionStretch = settingManager.GetInterpolatedTimeOfDayValue("MotionStretch", "RAIN"); + data.RainMotionTransparency = settingManager.GetInterpolatedTimeOfDayValue("MotionTransparency", "RAIN"); + + data.SkylightingAmbientMinLevel = (enableEffect && settingManager.GetValue("EnableSkylighting", "EFFECT")) ? settingManager.GetInterpolatedTimeOfDayValue("AmbientMinLevel", "SKYLIGHTING") : 1.0f; + + data.EnableProceduralSun = enableEffect && settingManager.GetValue("EnableProceduralSun", "EFFECT"); + + { + float size = settingManager.GetValue("Size", "PROCEDURALSUN"); + float edgeSoftness = settingManager.GetValue("EdgeSoftness", "PROCEDURALSUN"); + float glowCurve = std::max(FLT_MIN, settingManager.GetInterpolatedTimeOfDayValue("GlowCurve", "PROCEDURALSUN")); + + float scaledSize = size * 0.04f; + float diskSq = scaledSize * scaledSize; + float outerSpan = std::max(1.0f - diskSq, FLT_MIN); + float softSq = std::max(edgeSoftness * edgeSoftness, FLT_MIN); + + data.ProceduralSunDiskRadiusSq = diskSq; + data.ProceduralSunCoronaScale = 1.0f / outerSpan; + data.ProceduralSunDiskEdgeScale = 1.0f / (std::max(diskSq, FLT_MIN) * softSq); + data.ProceduralSunCoronaFalloff = 100.0f / (outerSpan * glowCurve); + } + + data.ProceduralSunGlowIntensity = settingManager.GetInterpolatedTimeOfDayValue("GlowIntensity", "PROCEDURALSUN"); + + return data; +} + +void Effect11::DrawSettings() +{ + MenuManager::GetSingleton().RenderImGui(); +} + +void Effect11::LoadRaindropTexture() +{ + raindropTexture = nullptr; + raindropSRV = nullptr; + + auto& presetManager = PresetManager::GetSingleton(); + auto enbPath = presetManager.GetENBSeriesPath(); + auto raindropPath = enbPath / "enbraindrops.png"; + + if (!std::filesystem::exists(raindropPath)) { + logger::debug("[Effect11] Raindrop texture not found: {}", raindropPath.string()); + return; + } + + std::wstring widePath = raindropPath.wstring(); + + DirectX::ScratchImage image; + HRESULT hr = DirectX::LoadFromWICFile(widePath.c_str(), DirectX::WIC_FLAGS_IGNORE_SRGB, nullptr, image); + if (FAILED(hr)) { + logger::error("[Effect11] Failed to load raindrop texture: {}", raindropPath.string()); + return; + } + + DirectX::ScratchImage mipImage; + hr = DirectX::GenerateMipMaps(image.GetImages(), image.GetImageCount(), image.GetMetadata(), + DirectX::TEX_FILTER_DEFAULT, 0, mipImage); + if (FAILED(hr)) { + logger::error("[Effect11] Failed to generate mipmaps for raindrop texture"); + return; + } + + DirectX::ScratchImage bc7Image; + hr = DirectX::Compress(mipImage.GetImages(), mipImage.GetImageCount(), mipImage.GetMetadata(), + DXGI_FORMAT_BC7_UNORM, DirectX::TEX_COMPRESS_BC7_QUICK, 1.0f, bc7Image); + if (FAILED(hr)) { + logger::error("[Effect11] Failed to compress raindrop texture to BC7"); + return; + } + + auto device = globals::d3d::device; + hr = DirectX::CreateTexture(device, + bc7Image.GetImages(), bc7Image.GetImageCount(), bc7Image.GetMetadata(), + reinterpret_cast(raindropTexture.put())); + if (FAILED(hr)) { + logger::error("[Effect11] Failed to create raindrop GPU texture"); + return; + } + + Util::SetResourceName(raindropTexture.get(), "Effect11::RaindropTexture"); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc{}; + srvDesc.Format = DXGI_FORMAT_BC7_UNORM; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MipLevels = static_cast(bc7Image.GetMetadata().mipLevels); + srvDesc.Texture2D.MostDetailedMip = 0; + + hr = device->CreateShaderResourceView(raindropTexture.get(), &srvDesc, raindropSRV.put()); + if (FAILED(hr)) { + logger::error("[Effect11] Failed to create raindrop SRV"); + raindropTexture = nullptr; + return; + } + + Util::SetResourceName(raindropSRV.get(), "Effect11::RaindropTexture SRV"); + + logger::info("[Effect11] Loaded raindrop texture: {} ({}x{}, BC7, {} mips)", + raindropPath.string(), + bc7Image.GetMetadata().width, + bc7Image.GetMetadata().height, + bc7Image.GetMetadata().mipLevels); +} + +void Effect11::SetupResources() +{ + EffectManager::GetSingleton().Initialize(); + LoadRaindropTexture(); +} + +void Effect11::Reset() +{ + // Reset effect state if needed +} + +void Effect11::ClearShaderCache() +{ + if (raymarchVolumetricRaysPS) { + raymarchVolumetricRaysPS->Release(); + raymarchVolumetricRaysPS = nullptr; + } + if (applyVolumetricRaysPS) { + applyVolumetricRaysPS->Release(); + applyVolumetricRaysPS = nullptr; + } + if (blurHCS) { + blurHCS->Release(); + blurHCS = nullptr; + } + if (blurVCS) { + blurVCS->Release(); + blurVCS = nullptr; + } + + EffectManager::GetSingleton().ReloadShaders(); +} + +void Effect11::Prepass() +{ + if (!enableEffect) { + return; + } + + auto& settingManager = SettingManager::GetSingleton(); + + if (!settingManager.GetValue("Enable", "SKY")) { + return; + } + + auto imageSpaceManager = RE::ImageSpaceManager::GetSingleton(); + if (!imageSpaceManager) { + return; + } + + GET_INSTANCE_MEMBER(data, imageSpaceManager); + + float gradientIntensity = settingManager.GetInterpolatedTimeOfDayValue("GradientIntensity", "SKY"); + float skyScaleIntensity = settingManager.GetValue("DisableWrongSkyMath", "SKY") ? 0.0f : gradientIntensity; + + data.baseData.hdr.skyScale *= skyScaleIntensity; +} + +float3 Curve(float3 color, float power) +{ + color.x = pow(std::max(color.x, 0.0f), power); + color.y = pow(std::max(color.y, 0.0f), power); + color.z = pow(std::max(color.z, 0.0f), power); + + return color; +} + +float3 Desaturation(float3 color, float desaturation) +{ + float luminance = color.Dot({ 1.0f / 3.0f, 1.0f / 3.0f, 1.0f / 3.0f }); + + color.x = std::lerp(color.x, luminance, desaturation); + color.y = std::lerp(color.y, luminance, desaturation); + color.z = std::lerp(color.z, luminance, desaturation); + + return color; +} + +float3 Intensity(float3 color, float intensity) +{ + return color * intensity; +} + +float3 ColorFilter(float3 color, float3 colorFilter, float colorFilterAmount) +{ + color.x = std::lerp(color.x, 1.0f, colorFilterAmount); + color.y = std::lerp(color.y, 1.0f, colorFilterAmount); + color.z = std::lerp(color.z, 1.0f, colorFilterAmount); + + return color * colorFilter; +} + +float3 NiToF3(RE::NiColor color) +{ + return { color.red, color.green, color.blue }; +} + +RE::NiColor F3ToNi(float3 color) +{ + return { color.x, color.y, color.z }; +} + +void Effect11::OverrideWeather(RE::Sky* a_sky) +{ + if (!a_sky) { + return; + } + + auto& settingManager = SettingManager::GetSingleton(); + + auto& colors = a_sky->skyColor; + + { + auto& dirLightColor = colors[(uint)RE::TESWeather::ColorTypes::kSunlight]; + + auto dirLightColorF3 = NiToF3(dirLightColor); + + auto imageSpaceManager = RE::ImageSpaceManager::GetSingleton(); + if (!imageSpaceManager) { + return; + } + + GET_INSTANCE_MEMBER(data, imageSpaceManager); + float sunlightScale = std::max(data.baseData.hdr.sunlightScale, FLT_MIN); + dirLightColorF3 *= sunlightScale; + + dirLightColorF3 = Curve(dirLightColorF3, settingManager.GetInterpolatedTimeOfDayValue("DirectLightingCurve", "ENVIRONMENT")); + dirLightColorF3 = Desaturation(dirLightColorF3, settingManager.GetInterpolatedTimeOfDayValue("DirectLightingDesaturation", "ENVIRONMENT")); + dirLightColorF3 = ColorFilter(dirLightColorF3, settingManager.GetInterpolatedColorTimeOfDayValue("DirectLightingColorFilter", "ENVIRONMENT"), settingManager.GetInterpolatedTimeOfDayValue("DirectLightingColorFilterAmount", "ENVIRONMENT")); + dirLightColorF3 = Intensity(dirLightColorF3, settingManager.GetInterpolatedTimeOfDayValue("DirectLightingIntensity", "ENVIRONMENT")); + + dirLightColorF3 /= sunlightScale; + + dirLightColor = F3ToNi(dirLightColorF3); + } + + { + auto& fogFarColor = colors[(uint)RE::TESWeather::ColorTypes::kFogFar]; + + auto fogFarColorF3 = NiToF3(fogFarColor); + + auto fogColorCurve = settingManager.GetInterpolatedTimeOfDayValue("FogColorCurve", "ENVIRONMENT"); + auto fogColorMultiplier = settingManager.GetInterpolatedTimeOfDayValue("FogColorMultiplier", "ENVIRONMENT"); + + auto fogColorFilter = settingManager.GetInterpolatedColorTimeOfDayValue("FogColorFilter", "ENVIRONMENT"); + auto fogColorFilterAmount = settingManager.GetInterpolatedTimeOfDayValue("FogColorFilterAmount", "ENVIRONMENT"); + + fogFarColorF3 = Curve(fogFarColorF3, fogColorCurve); + fogFarColorF3 = ColorFilter(fogFarColorF3, fogColorFilter, fogColorFilterAmount); + fogFarColorF3 = Intensity(fogFarColorF3, fogColorMultiplier); + + fogFarColor = F3ToNi(fogFarColorF3); + + auto& fogNearColor = colors[(uint)RE::TESWeather::ColorTypes::kFogNear]; + + auto fogNearColorF3 = NiToF3(fogNearColor); + + fogNearColorF3 = Curve(fogNearColorF3, fogColorCurve); + fogNearColorF3 = ColorFilter(fogNearColorF3, fogColorFilter, fogColorFilterAmount); + fogNearColorF3 = Intensity(fogNearColorF3, fogColorMultiplier); + + fogNearColor = F3ToNi(fogNearColorF3); + } + + { + a_sky->fogPower *= settingManager.GetInterpolatedTimeOfDayValue("FogCurveMultiplier", "ENVIRONMENT"); + } + + { + auto fogAmountMultiplier = settingManager.GetInterpolatedTimeOfDayValue("FogAmountMultiplier", "ENVIRONMENT"); + fogAmountMultiplier = std::max(fogAmountMultiplier, FLT_MIN); + + a_sky->fogNear /= fogAmountMultiplier; + a_sky->fogFar /= fogAmountMultiplier; + } + + const bool enableSky = enableEffect && settingManager.GetValue("Enable", "SKY"); + + if (enableSky) { + { + auto& sunColor = colors[(uint)RE::TESWeather::ColorTypes::kSun]; + + auto sunColorF3 = NiToF3(sunColor); + + sunColorF3 = Desaturation(sunColorF3, settingManager.GetInterpolatedTimeOfDayValue("SunDesaturation", "SKY")); + sunColorF3 = ColorFilter(sunColorF3, settingManager.GetInterpolatedColorTimeOfDayValue("SunColorFilter", "SKY"), 0.0f); + sunColorF3 = Intensity(sunColorF3, settingManager.GetInterpolatedTimeOfDayValue("SunIntensity", "SKY")); + + sunColor = F3ToNi(sunColorF3); + } + + { + auto& moonColor = colors[(uint)RE::TESWeather::ColorTypes::kMoonGlare]; + + auto moonColorF3 = NiToF3(moonColor); + + moonColorF3 = Desaturation(moonColorF3, settingManager.GetInterpolatedTimeOfDayValue("MoonDesaturation", "SKY")); + moonColorF3 = ColorFilter(moonColorF3, settingManager.GetInterpolatedColorTimeOfDayValue("MoonColorFilter", "SKY"), 0.0f); + moonColorF3 = Intensity(moonColorF3, settingManager.GetInterpolatedTimeOfDayValue("MoonIntensity", "SKY")); + + moonColor = F3ToNi(moonColorF3); + } + + { + auto& starsColor = colors[(uint)RE::TESWeather::ColorTypes::kStars]; + + auto starsColorF3 = NiToF3(starsColor); + + starsColorF3 = Intensity(starsColorF3, settingManager.GetInterpolatedTimeOfDayValue("StarsIntensity", "SKY")); + + starsColor = F3ToNi(starsColorF3); + } + + { + auto& sunGlareColor = colors[(uint)RE::TESWeather::ColorTypes::kSunGlare]; + + auto sunGlareColorF3 = NiToF3(sunGlareColor); + + sunGlareColorF3 = Intensity(sunGlareColorF3, settingManager.GetInterpolatedTimeOfDayValue("GlowIntensity", "SUNGLARE")); + + sunGlareColor = F3ToNi(sunGlareColorF3); + } + + { + auto& skyStaticsColor = colors[(uint)RE::TESWeather::ColorTypes::kSkyStatics]; + + auto skyStaticsColorF3 = NiToF3(skyStaticsColor); + + skyStaticsColorF3 = ColorFilter(skyStaticsColorF3, settingManager.GetInterpolatedColorTimeOfDayValue("ColorFilter", "VOLUMETRICFOG"), 0.0f); + skyStaticsColorF3 = Intensity(skyStaticsColorF3, settingManager.GetInterpolatedTimeOfDayValue("Intensity", "VOLUMETRICFOG")); + + skyStaticsColor = F3ToNi(skyStaticsColorF3); + } + + float gradientIntensity = settingManager.GetInterpolatedTimeOfDayValue("GradientIntensity", "SKY"); + float gradientDesaturation = settingManager.GetInterpolatedTimeOfDayValue("GradientDesaturation", "SKY"); + + { + auto& horizonColor = colors[(uint)RE::TESWeather::ColorTypes::kHorizon]; + auto horizonColorF3 = NiToF3(horizonColor); + + horizonColorF3 = Curve(horizonColorF3, settingManager.GetInterpolatedTimeOfDayValue("GradientHorizonCurve", "SKY")); + horizonColorF3 = ColorFilter(horizonColorF3, settingManager.GetInterpolatedColorTimeOfDayValue("GradientHorizonColorFilter", "SKY"), 0.0f); + horizonColorF3 *= settingManager.GetInterpolatedTimeOfDayValue("GradientHorizonIntensity", "SKY") * gradientIntensity; + horizonColorF3 = Desaturation(horizonColorF3, gradientDesaturation); + + horizonColor = F3ToNi(horizonColorF3); + } + + { + auto& lowerColor = colors[(uint)RE::TESWeather::ColorTypes::kSkyLower]; + auto lowerColorF3 = NiToF3(lowerColor); + + lowerColorF3 = Curve(lowerColorF3, settingManager.GetInterpolatedTimeOfDayValue("GradientMiddleCurve", "SKY")); + lowerColorF3 = ColorFilter(lowerColorF3, settingManager.GetInterpolatedColorTimeOfDayValue("GradientMiddleColorFilter", "SKY"), 0.0f); + lowerColorF3 *= settingManager.GetInterpolatedTimeOfDayValue("GradientMiddleIntensity", "SKY") * gradientIntensity; + lowerColorF3 = Desaturation(lowerColorF3, gradientDesaturation); + + lowerColor = F3ToNi(lowerColorF3); + } + + { + auto& upperColor = colors[(uint)RE::TESWeather::ColorTypes::kSkyUpper]; + auto upperColorF3 = NiToF3(upperColor); + + upperColorF3 = Curve(upperColorF3, settingManager.GetInterpolatedTimeOfDayValue("GradientTopCurve", "SKY")); + upperColorF3 = ColorFilter(upperColorF3, settingManager.GetInterpolatedColorTimeOfDayValue("GradientTopColorFilter", "SKY"), 0.0f); + upperColorF3 *= settingManager.GetInterpolatedTimeOfDayValue("GradientTopIntensity", "SKY") * gradientIntensity; + upperColorF3 = Desaturation(upperColorF3, gradientDesaturation); + + upperColor = F3ToNi(upperColorF3); + } + + if (auto clouds = a_sky->clouds) { + auto cloudsColorFilter = settingManager.GetInterpolatedColorTimeOfDayValue("CloudsColorFilter", "SKY"); + auto cloudsIntensity = settingManager.GetInterpolatedTimeOfDayValue("CloudsIntensity", "SKY"); + auto cloudsOpacity = settingManager.GetInterpolatedTimeOfDayValue("CloudsOpacity", "SKY"); + + for (uint16_t i = 0; i < clouds->numLayers; i++) { + auto cloudColorF3 = NiToF3(clouds->colors[i]); + cloudColorF3 *= cloudsColorFilter * cloudsIntensity; + clouds->colors[i] = F3ToNi(cloudColorF3); + clouds->alphas[i] *= cloudsOpacity; + } + } + } + + { + static auto& volumetricLighting = (*(RE::BSVolumetricLightingRenderData*)(REL::RelocationID(527719, 414629).address() - offsetof(RE::BSVolumetricLightingRenderData, red))); + volumetricLighting.intensity *= settingManager.GetInterpolatedTimeOfDayValue("Intensity", "GAMEVOLUMETRICRAYS"); + volumetricLighting.samplingRepartition.rangeFactor *= settingManager.GetInterpolatedTimeOfDayValue("RangeFactor", "GAMEVOLUMETRICRAYS"); + } +} + +void Effect11::CheckCommonData() +{ + static Util::FrameChecker checker; + if (checker.IsNewFrame()) { + ENBHelper::Update(); + + auto& settingManager = SettingManager::GetSingleton(); + auto ui = globals::game::ui; + bool isMenuOpen = ui->IsMenuOpen(RE::MapMenu::MENU_NAME); + enableEffect = !isMenuOpen && settingManager.GetValue("UseEffect", "GLOBAL"); + + auto& effectManager = EffectManager::GetSingleton(); + auto& weatherManager = WeatherManager::GetSingleton(); + + effectManager.UpdateCommonData(); + + const auto& commonData = effectManager.GetCommonData(); + settingManager.SetTimeOfDayData(commonData.timeOfDay1, commonData.timeOfDay2); + + uint32_t currentWeatherID = weatherManager.GetEffectiveWeatherID(static_cast(commonData.weather[0])); + uint32_t lastWeatherID = weatherManager.GetEffectiveWeatherID(static_cast(commonData.weather[1])); + settingManager.SetWeatherBlendFactors(currentWeatherID, lastWeatherID, commonData.weather[2]); + } +} + +void Effect11::OverridePointLightColor(float3& a_color) +{ + auto& settingManager = SettingManager::GetSingleton(); + + a_color = Curve(a_color, settingManager.GetInterpolatedTimeOfDayValue("PointLightingCurve", "ENVIRONMENT")); + a_color = Desaturation(a_color, settingManager.GetInterpolatedTimeOfDayValue("PointLightingDesaturation", "ENVIRONMENT")); + a_color = Intensity(a_color, settingManager.GetInterpolatedTimeOfDayValue("PointLightingIntensity", "ENVIRONMENT")); +} + +void Effect11::OverrideAmbientLighting(DirectionalAmbientColors& DirectionalAmbientColors) +{ + auto& settingManager = SettingManager::GetSingleton(); + + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 2; j++) { + auto& ambientLightingColor = DirectionalAmbientColors.directionalAmbientColors[i][j]; + + float3 ambientLightingColorF3 = NiToF3(ambientLightingColor); + + int currentSide = i * 2 + j; + if (currentSide == 3) + ambientLightingColorF3 = Desaturation(ambientLightingColorF3, settingManager.GetInterpolatedTimeOfDayValue("AmbientLightingDesaturation", "ENVIRONMENT")); + + ambientLightingColorF3 = Intensity(ambientLightingColorF3, settingManager.GetInterpolatedTimeOfDayValue("AmbientLightingIntensity", "ENVIRONMENT")); + + ambientLightingColor = F3ToNi(ambientLightingColorF3); + } + } +} + +void Effect11::OnSkyUpdateColors(RE::Sky* a_sky) +{ + CheckCommonData(); + if (enableEffect) + OverrideWeather(a_sky); +} + +struct Sky_SetDirectionalAmbientColors +{ + static void thunk(Effect11::DirectionalAmbientColors& DirectionalAmbientColors, RE::NiColor* AmbientSpecularTint, float AmbientSpecularFresnel) + { + globals::features::effect11.CheckCommonData(); + if (globals::features::effect11.enableEffect) + globals::features::effect11.OverrideAmbientLighting(DirectionalAmbientColors); + func(DirectionalAmbientColors, AmbientSpecularTint, AmbientSpecularFresnel); + } + + static inline REL::Relocation func; +}; + +struct Main_HDRTonemapBlendCinematic_Render +{ + static void thunk(RE::ImageSpaceManager* a1, RE::ImageSpaceEffect* a2, uint32_t a3, uint32_t a4, RE::ImageSpaceShaderParam* a5) + { + globals::features::effect11.CheckCommonData(); + + auto& settingManager = SettingManager::GetSingleton(); + auto& effectManager = EffectManager::GetSingleton(); + + if (globals::features::effect11.enableEffect && !settingManager.GetValue("UseOriginalPostProcessing", "EFFECT")) { + effectManager.ExecuteEffects(); + } else { + func(a1, a2, a3, a4, a5); + } + } + + static inline REL::Relocation func; +}; + +void Effect11::ModifySky(RE::BSRenderPass* Pass) +{ + if (!Pass || !Pass->shaderProperty) { + return; + } + + auto skyProperty = static_cast(Pass->shaderProperty); + + auto state = globals::state; + + state->permutationData.ExtraShaderDescriptor &= ~static_cast(State::ExtraShaderDescriptors::IsSun); + + if (skyProperty->uiSkyObjectType == RE::BSSkyShaderProperty::SkyObject::SO_SUN) { + state->permutationData.ExtraShaderDescriptor |= static_cast(State::ExtraShaderDescriptors::IsSun); + } +} + +struct BSSkyShader_SetupMaterial +{ + static void thunk(RE::BSShader* This, RE::BSRenderPass* Pass, uint32_t RenderFlags) + { + globals::features::effect11.ModifySky(Pass); + func(This, Pass, RenderFlags); + } + + static inline REL::Relocation func; +}; + +void Effect11::ModifyParticle(RE::BSRenderPass* Pass) +{ + if (!enableEffect || !raindropSRV) + return; + + if (!Pass) + return; + + auto context = globals::d3d::context; + ID3D11ShaderResourceView* srv = raindropSRV.get(); + context->PSSetShaderResources(80, 1, &srv); + + ID3D11Buffer* cbs[] = { globals::state->sharedDataCB->CB(), globals::state->featureDataCB->CB() }; + context->VSSetConstantBuffers(5, 2, cbs); +} + +struct BSParticleShader_SetupGeometry +{ + static void thunk(RE::BSShader* This, RE::BSRenderPass* Pass, uint32_t RenderFlags) + { + func(This, Pass, RenderFlags); + globals::features::effect11.ModifyParticle(Pass); + } + + static inline REL::Relocation func; +}; + +void Effect11::ParticleShaderHacks() +{ + if (!enableEffect || !raindropSRV) + return; + + auto state = State::GetSingleton(); + if (!state->currentShader || state->currentShader->shaderType.get() != RE::BSShader::Type::Particle) + return; + if (state->currentPixelDescriptor != static_cast(SIE::ShaderCache::ParticleShaderTechniques::EnvCubeRain)) + return; + + auto context = globals::d3d::context; + + if (!alphaBlendState) { + D3D11_BLEND_DESC blendDesc{}; + blendDesc.RenderTarget[0].BlendEnable = TRUE; + blendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; + blendDesc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; + blendDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; + blendDesc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; + blendDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA; + blendDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; + blendDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; + globals::d3d::device->CreateBlendState(&blendDesc, &alphaBlendState); + } + + float blendFactor[4] = { 0, 0, 0, 0 }; + context->OMSetBlendState(alphaBlendState, blendFactor, 0xFFFFFFFF); +} + +void Effect11::DrawVolumetricRays() +{ + if (!enableEffect) + return; + + if (Util::IsInterior()) + return; + + auto& settingManager = SettingManager::GetSingleton(); + if (!settingManager.GetValue("EnableVolumetricRays", "EFFECT")) + return; + + auto& effectManager = EffectManager::GetSingleton(); + if (!effectManager.IsInitialized() || !effectManager.copyVertexShader) + return; + + if (!raymarchVolumetricRaysPS) { + std::vector> defines; + if (globals::features::cloudShadows.loaded) + defines.push_back({ "CLOUD_SHADOWS", nullptr }); + if (globals::features::terrainShadows.loaded) + defines.push_back({ "TERRAIN_SHADOWS", nullptr }); + if (REL::Module::IsVR()) + defines.push_back({ "FRAMEBUFFER", nullptr }); + + raymarchVolumetricRaysPS = static_cast(Util::CompileShader(L"Data\\Shaders\\Effect11\\RaymarchVolumetricRaysPS.hlsl", defines, "ps_5_0")); + if (!raymarchVolumetricRaysPS) + return; + } + + if (!applyVolumetricRaysPS) { + std::vector> defines; + if (globals::features::ibl.loaded) + defines.push_back({ "IBL", nullptr }); + if (REL::Module::IsVR()) + defines.push_back({ "FRAMEBUFFER", nullptr }); + + applyVolumetricRaysPS = static_cast(Util::CompileShader(L"Data\\Shaders\\Effect11\\ApplyVolumetricRaysPS.hlsl", defines, "ps_5_0")); + if (!applyVolumetricRaysPS) + return; + } + + if (!blurHCS) { + blurHCS = static_cast(Util::CompileShader(L"Data\\Shaders\\ISVolumetricLightingBlurHCS.hlsl", {}, "cs_5_0")); + if (!blurHCS) + return; + } + + if (!blurVCS) { + blurVCS = static_cast(Util::CompileShader(L"Data\\Shaders\\ISVolumetricLightingBlurVCS.hlsl", {}, "cs_5_0")); + if (!blurVCS) + return; + } + + if (!additiveBlendState) { + D3D11_BLEND_DESC blendDesc{}; + blendDesc.RenderTarget[0].BlendEnable = TRUE; + blendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_ONE; + blendDesc.RenderTarget[0].DestBlend = D3D11_BLEND_ONE; + blendDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; + blendDesc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ZERO; + blendDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ONE; + blendDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; + blendDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_RED | D3D11_COLOR_WRITE_ENABLE_GREEN | D3D11_COLOR_WRITE_ENABLE_BLUE; + globals::d3d::device->CreateBlendState(&blendDesc, &additiveBlendState); + } + + auto context = globals::d3d::context; + auto renderer = globals::game::renderer; + auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + + D3D11_TEXTURE2D_DESC mainTexDesc{}; + main.texture->GetDesc(&mainTexDesc); + float2 resolution = { static_cast(mainTexDesc.Width), static_cast(mainTexDesc.Height) }; + resolution = Util::ConvertToDynamic(resolution); + uint32_t dynWidth = static_cast(resolution.x); + uint32_t dynHeight = static_cast(resolution.y); + + if (!vrTexA || vrTexA->desc.Width != mainTexDesc.Width || vrTexA->desc.Height != mainTexDesc.Height) { + D3D11_TEXTURE2D_DESC desc{}; + desc.Width = mainTexDesc.Width; + desc.Height = mainTexDesc.Height; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_R16_FLOAT; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc{}; + srvDesc.Format = DXGI_FORMAT_R16_FLOAT; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MipLevels = 1; + + D3D11_RENDER_TARGET_VIEW_DESC rtvDesc{}; + rtvDesc.Format = DXGI_FORMAT_R16_FLOAT; + rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D; + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc{}; + uavDesc.Format = DXGI_FORMAT_R16_FLOAT; + uavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D; + + vrTexA = std::make_unique(desc, "Effect11::VRTexA"); + vrTexA->CreateSRV(srvDesc); + vrTexA->CreateRTV(rtvDesc); + vrTexA->CreateUAV(uavDesc); + + vrTexB = std::make_unique(desc, "Effect11::VRTexB"); + vrTexB->CreateSRV(srvDesc); + vrTexB->CreateUAV(uavDesc); + } + + if (!vrBlurCB) + vrBlurCB = std::make_unique(ConstantBufferDesc(16), "Effect11::VRBlurCB"); + + Effect11Util::D3D11FullStateBackup stateBackup; + stateBackup.Save(context); + + ID3D11SamplerState* sampler = Deferred::GetSingleton()->linearSampler; + D3D11_VIEWPORT viewport{ 0, 0, resolution.x, resolution.y, 0, 1 }; + + auto* profiler = globals::profiler; + + // Pass 1: Raymarch shadow → R16F texture + { + profiler->BeginPass("Effect11::VolumetricRays Pass 0"); + + ID3D11RenderTargetView* rtv = vrTexA->rtv.get(); + context->OMSetRenderTargets(1, &rtv, nullptr); + context->RSSetViewports(1, &viewport); + + context->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF); + context->RSSetState(effectManager.rasterizerState.get()); + context->OMSetDepthStencilState(nullptr, 0); + + UINT stride = 20; + UINT offset = 0; + ID3D11Buffer* vbs[] = { effectManager.quadVertexBuffer.get() }; + context->IASetVertexBuffers(0, 1, vbs, &stride, &offset); + context->IASetInputLayout(effectManager.inputLayout.get()); + context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + + context->VSSetShader(effectManager.copyVertexShader.get(), nullptr, 0); + context->PSSetShader(raymarchVolumetricRaysPS, nullptr, 0); + context->PSSetSamplers(0, 1, &sampler); + + context->Draw(4, 0); + + ID3D11RenderTargetView* nullRTV = nullptr; + context->OMSetRenderTargets(1, &nullRTV, nullptr); + + profiler->EndPass(); + } + + // Blur setup + auto depthSRV = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN].depthSRV; + + struct VLData + { + int32_t screenX, screenY, screenXMin1, screenYMin1; + }; + VLData vlData = { static_cast(dynWidth), static_cast(dynHeight), static_cast(dynWidth) - 1, static_cast(dynHeight) - 1 }; + vrBlurCB->Update(vlData); + + static constexpr uint32_t tgDim = 256; + static constexpr uint32_t blurWindow = 12; + static constexpr uint32_t effectiveGroupSize = tgDim - blurWindow * 2; + + // Pass 2: Blur horizontal (texA → texB) + { + profiler->BeginPass("Effect11::VolumetricRays Pass 1"); + context->CSSetShader(blurHCS, nullptr, 0); + + ID3D11ShaderResourceView* csSRVs[2] = { vrTexA->srv.get(), depthSRV }; + context->CSSetShaderResources(0, 2, csSRVs); + + ID3D11UnorderedAccessView* csUAVs[1] = { vrTexB->uav.get() }; + context->CSSetUnorderedAccessViews(0, 1, csUAVs, nullptr); + + ID3D11Buffer* csCBs[2] = { nullptr, vrBlurCB->CB() }; + context->CSSetConstantBuffers(0, 2, csCBs); + + uint32_t groupsX = (dynWidth + effectiveGroupSize - 1) / effectiveGroupSize; + context->Dispatch(groupsX, dynHeight, 1); + + ID3D11ShaderResourceView* nullSRVs[2] = { nullptr, nullptr }; + context->CSSetShaderResources(0, 2, nullSRVs); + ID3D11UnorderedAccessView* nullUAVs[1] = { nullptr }; + context->CSSetUnorderedAccessViews(0, 1, nullUAVs, nullptr); + profiler->EndPass(); + } + + // Pass 3: Blur vertical (texB → texA) + { + profiler->BeginPass("Effect11::VolumetricRays Pass 2"); + context->CSSetShader(blurVCS, nullptr, 0); + + ID3D11ShaderResourceView* csSRVs[2] = { vrTexB->srv.get(), depthSRV }; + context->CSSetShaderResources(0, 2, csSRVs); + + ID3D11UnorderedAccessView* csUAVs[1] = { vrTexA->uav.get() }; + context->CSSetUnorderedAccessViews(0, 1, csUAVs, nullptr); + + uint32_t groupsY = (dynHeight + effectiveGroupSize - 1) / effectiveGroupSize; + context->Dispatch(dynWidth, groupsY, 1); + + ID3D11ShaderResourceView* nullSRVs[2] = { nullptr, nullptr }; + context->CSSetShaderResources(0, 2, nullSRVs); + ID3D11UnorderedAccessView* nullUAVs[1] = { nullptr }; + context->CSSetUnorderedAccessViews(0, 1, nullUAVs, nullptr); + context->CSSetShader(nullptr, nullptr, 0); + profiler->EndPass(); + } + + // Pass 4: Apply blurred shadow with color → main RT (additive) + { + profiler->BeginPass("Effect11::VolumetricRays Pass 3"); + ID3D11RenderTargetView* rtv = main.RTV; + context->OMSetRenderTargets(1, &rtv, nullptr); + context->RSSetViewports(1, &viewport); + + context->OMSetBlendState(additiveBlendState, nullptr, 0xFFFFFFFF); + context->RSSetState(effectManager.rasterizerState.get()); + context->OMSetDepthStencilState(nullptr, 0); + + UINT stride = 20; + UINT offset = 0; + ID3D11Buffer* vbs[] = { effectManager.quadVertexBuffer.get() }; + context->IASetVertexBuffers(0, 1, vbs, &stride, &offset); + context->IASetInputLayout(effectManager.inputLayout.get()); + context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + + context->VSSetShader(effectManager.copyVertexShader.get(), nullptr, 0); + context->PSSetShader(applyVolumetricRaysPS, nullptr, 0); + + auto& ibl = globals::features::ibl; + ID3D11ShaderResourceView* srvs[16]{}; + srvs[0] = vrTexA->srv.get(); + if (ibl.loaded) { + srvs[14] = ibl.envIBLTexture->srv.get(); + srvs[15] = ibl.skyIBLTexture->srv.get(); + } + context->PSSetShaderResources(0, 16, srvs); + context->PSSetSamplers(0, 1, &sampler); + + context->Draw(4, 0); + profiler->EndPass(); + } + + stateBackup.Restore(context); + stateBackup.Release(); +} + +void Effect11::PostPostLoad() +{ + stl::write_thunk_call(REL::RelocationID(99023, 105674).address() + REL::Relocate(0x1EA, 0x178)); + if (REL::Module::IsSE()) + stl::write_thunk_call(REL::RelocationID(99023, 105674).address() + REL::Relocate(0x230, 0x178)); + + stl::detour_thunk(REL::RelocationID(98989, 105643)); + stl::write_vfunc<0x6, BSSkyShader_SetupMaterial>(RE::VTABLE_BSSkyShader[0]); + stl::write_vfunc<0x6, BSParticleShader_SetupGeometry>(RE::VTABLE_BSParticleShader[0]); +} diff --git a/src/Features/Effect11.h b/src/Features/Effect11.h new file mode 100644 index 0000000000..ac6c35cc28 --- /dev/null +++ b/src/Features/Effect11.h @@ -0,0 +1,144 @@ +#pragma once + +#include "Buffer.h" + +#include +#include + +struct Effect11 : Feature +{ +public: + virtual inline std::string GetName() override { return "Effect11"; } + virtual inline std::string GetShortName() override { return "Effect11"; } + virtual std::string_view GetCategory() const override { return "Post-Processing"; } + + virtual std::pair> GetFeatureSummary() override + { + return { + "Effect11 provides a framework for loading and executing ENBSeries-compatible FX effect files.\n" + "This allows for advanced post-processing effects and visual enhancements using DirectX 11 Effect (.fx) files.", + { "ENBSeries-compatible FX support", + "DirectX 11 Effect file loading", + "Advanced post-processing pipeline", + "Custom technique execution", + "Dynamic UI variable system" } + }; + } + + struct alignas(16) PerFrame + { + uint Enable; + uint EnableSky; + float ColorPow; + float LightSpriteIntensity; + + float CloudsCurve; + float CloudsDesaturation; + float CloudsEdgeIntensity; + float CloudsEdgeMoonMultiplier; + + float VolumetricRaysDesaturation; + float3 VolumetricRaysColorFilter; + + uint UseProceduralGradientWeights; + float ProceduralGradientWeightCurve; + uint EnableProceduralSun; + float ProceduralSunDiskRadiusSq; + + float ProceduralSunDiskEdgeScale; + float ProceduralSunGlowIntensity; + float ProceduralSunCoronaFalloff; + float ProceduralSunCoronaScale; + + float ParticleIntensity; + float ParticleLightingInfluence; + float ParticleAmbientInfluence; + float ParticlePointLightingInfluence; + + uint EnableCloudsScattering; + uint EnableCloudsLightingFromMoon; + uint ScatteringColorHDRWeighting; + float SkyScatteringAtmosphereThickness; + + float SkyScatteringHorizonRange; + float SkyScatteringIntensity; + float SkyScatteringAmount; + float SkyScatteringDustVolume; + + float SkyScatteringDustDensity; + float SkyScatteringDustDarkening; + float SkyScatteringShadowAmount; + float SkyScatteringColorFromSun; + + float3 SkyScatteringColor; + float SkyScatteringAirGlowIntensity; + + float SkyScatteringAirGlowRange; + float SkyScatteringSunGlowIntensity; + float SkyScatteringSunGlowRange; + float SkyScatteringMoonGlowAmount; + + float SkyScatteringMoonGlowRange; + float SkyScatteringCloudsLightingSunMinIntensity; + float SkyScatteringCloudsLightingSunMultiplier; + float SkyScatteringCloudsLightingMoonIntensity; + + uint EnableVolumetricRays; + float VolumetricRaysIntensity; + float VolumetricRaysExtinction; + float VolumetricRaysSkyColorAmount; + + float FireIntensity; + float FireCurve; + float AuroraIntensity; + float AuroraCurve; + + uint EnableRain; + float RainMotionStretch; + float RainMotionTransparency; + float SkylightingAmbientMinLevel; + }; + + bool enableEffect = false; + + ID3D11PixelShader* raymarchVolumetricRaysPS = nullptr; + ID3D11PixelShader* applyVolumetricRaysPS = nullptr; + ID3D11ComputeShader* blurHCS = nullptr; + ID3D11ComputeShader* blurVCS = nullptr; + ID3D11BlendState* additiveBlendState = nullptr; + ID3D11BlendState* alphaBlendState = nullptr; + + std::unique_ptr vrTexA; + std::unique_ptr vrTexB; + std::unique_ptr vrBlurCB; + + winrt::com_ptr raindropTexture; + winrt::com_ptr raindropSRV; + void LoadRaindropTexture(); + + PerFrame GetCommonBufferData(); + + virtual void DrawSettings() override; + virtual void SetupResources() override; + virtual void Reset() override; + virtual void Prepass() override; + virtual void ClearShaderCache() override; + + void DrawVolumetricRays(); + + void OnSkyUpdateColors(RE::Sky* a_sky); + void OverrideWeather(RE::Sky* a_sky); + void CheckCommonData(); + void OverridePointLightColor(float3& a_color); + + struct DirectionalAmbientColors + { + RE::NiColor directionalAmbientColors[3][2]; + }; + void OverrideAmbientLighting(DirectionalAmbientColors& DirectionalAmbientColors); + + void ModifySky(RE::BSRenderPass* Pass); + __declspec(noinline) void ModifyParticle(RE::BSRenderPass* Pass); + void ParticleShaderHacks(); + virtual void PostPostLoad() override; +}; \ No newline at end of file diff --git a/src/Features/Effect11/D3D11StateBackup.h b/src/Features/Effect11/D3D11StateBackup.h new file mode 100644 index 0000000000..b249a0d557 --- /dev/null +++ b/src/Features/Effect11/D3D11StateBackup.h @@ -0,0 +1,248 @@ +#pragma once + +#include + +namespace Effect11Util +{ + static constexpr UINT kMaxSRVs = D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + static constexpr UINT kMaxSamplers = D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + static constexpr UINT kMaxCBs = D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + static constexpr UINT kMaxUAVs = D3D11_1_UAV_SLOT_COUNT; + static constexpr UINT kMaxVBs = D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT; + static constexpr UINT kMaxRTVs = D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT; + static constexpr UINT kMaxViewports = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE; + static constexpr UINT kMaxSOTargets = 4; + + template + inline void SafeRelease(T*& ptr) + { + if (ptr) { + ptr->Release(); + ptr = nullptr; + } + } + + template + inline void SafeReleaseArray(T* (&arr)[N]) + { + for (size_t i = 0; i < N; ++i) + SafeRelease(arr[i]); + } + + struct D3D11FullStateBackup + { + // Input Assembler + ID3D11InputLayout* iaInputLayout = nullptr; + D3D11_PRIMITIVE_TOPOLOGY iaTopology = D3D11_PRIMITIVE_TOPOLOGY_UNDEFINED; + ID3D11Buffer* iaVertexBuffers[kMaxVBs] = {}; + UINT iaVBStrides[kMaxVBs] = {}; + UINT iaVBOffsets[kMaxVBs] = {}; + ID3D11Buffer* iaIndexBuffer = nullptr; + DXGI_FORMAT iaIndexFormat = DXGI_FORMAT_UNKNOWN; + UINT iaIndexOffset = 0; + + // Vertex Shader + ID3D11VertexShader* vs = nullptr; + ID3D11Buffer* vsCBs[kMaxCBs] = {}; + ID3D11ShaderResourceView* vsSRVs[kMaxSRVs] = {}; + ID3D11SamplerState* vsSamplers[kMaxSamplers] = {}; + + // Hull Shader + ID3D11HullShader* hs = nullptr; + ID3D11Buffer* hsCBs[kMaxCBs] = {}; + ID3D11ShaderResourceView* hsSRVs[kMaxSRVs] = {}; + ID3D11SamplerState* hsSamplers[kMaxSamplers] = {}; + + // Domain Shader + ID3D11DomainShader* ds = nullptr; + ID3D11Buffer* dsCBs[kMaxCBs] = {}; + ID3D11ShaderResourceView* dsSRVs[kMaxSRVs] = {}; + ID3D11SamplerState* dsSamplers[kMaxSamplers] = {}; + + // Geometry Shader + ID3D11GeometryShader* gs = nullptr; + ID3D11Buffer* gsCBs[kMaxCBs] = {}; + ID3D11ShaderResourceView* gsSRVs[kMaxSRVs] = {}; + ID3D11SamplerState* gsSamplers[kMaxSamplers] = {}; + + // Stream Output + ID3D11Buffer* soTargets[kMaxSOTargets] = {}; + + // Rasterizer + ID3D11RasterizerState* rs = nullptr; + UINT rsNumViewports = kMaxViewports; + D3D11_VIEWPORT rsViewports[kMaxViewports] = {}; + UINT rsNumScissorRects = kMaxViewports; + D3D11_RECT rsScissorRects[kMaxViewports] = {}; + + // Pixel Shader + ID3D11PixelShader* ps = nullptr; + ID3D11Buffer* psCBs[kMaxCBs] = {}; + ID3D11ShaderResourceView* psSRVs[kMaxSRVs] = {}; + ID3D11SamplerState* psSamplers[kMaxSamplers] = {}; + + // Output Merger + ID3D11RenderTargetView* omRTVs[kMaxRTVs] = {}; + ID3D11DepthStencilView* omDSV = nullptr; + ID3D11BlendState* omBlendState = nullptr; + FLOAT omBlendFactor[4] = {}; + UINT omSampleMask = 0; + ID3D11DepthStencilState* omDepthStencilState = nullptr; + UINT omStencilRef = 0; + + // Compute Shader + ID3D11ComputeShader* cs = nullptr; + ID3D11Buffer* csCBs[kMaxCBs] = {}; + ID3D11ShaderResourceView* csSRVs[kMaxSRVs] = {}; + ID3D11SamplerState* csSamplers[kMaxSamplers] = {}; + ID3D11UnorderedAccessView* csUAVs[kMaxUAVs] = {}; + + void Save(ID3D11DeviceContext* ctx) + { + ctx->IAGetInputLayout(&iaInputLayout); + ctx->IAGetPrimitiveTopology(&iaTopology); + ctx->IAGetVertexBuffers(0, kMaxVBs, iaVertexBuffers, iaVBStrides, iaVBOffsets); + ctx->IAGetIndexBuffer(&iaIndexBuffer, &iaIndexFormat, &iaIndexOffset); + + ctx->VSGetShader(&vs, nullptr, nullptr); + ctx->VSGetConstantBuffers(0, kMaxCBs, vsCBs); + ctx->VSGetShaderResources(0, kMaxSRVs, vsSRVs); + ctx->VSGetSamplers(0, kMaxSamplers, vsSamplers); + + ctx->HSGetShader(&hs, nullptr, nullptr); + ctx->HSGetConstantBuffers(0, kMaxCBs, hsCBs); + ctx->HSGetShaderResources(0, kMaxSRVs, hsSRVs); + ctx->HSGetSamplers(0, kMaxSamplers, hsSamplers); + + ctx->DSGetShader(&ds, nullptr, nullptr); + ctx->DSGetConstantBuffers(0, kMaxCBs, dsCBs); + ctx->DSGetShaderResources(0, kMaxSRVs, dsSRVs); + ctx->DSGetSamplers(0, kMaxSamplers, dsSamplers); + + ctx->GSGetShader(&gs, nullptr, nullptr); + ctx->GSGetConstantBuffers(0, kMaxCBs, gsCBs); + ctx->GSGetShaderResources(0, kMaxSRVs, gsSRVs); + ctx->GSGetSamplers(0, kMaxSamplers, gsSamplers); + + ctx->SOGetTargets(kMaxSOTargets, soTargets); + + ctx->RSGetState(&rs); + rsNumViewports = kMaxViewports; + ctx->RSGetViewports(&rsNumViewports, rsViewports); + rsNumScissorRects = kMaxViewports; + ctx->RSGetScissorRects(&rsNumScissorRects, rsScissorRects); + + ctx->PSGetShader(&ps, nullptr, nullptr); + ctx->PSGetConstantBuffers(0, kMaxCBs, psCBs); + ctx->PSGetShaderResources(0, kMaxSRVs, psSRVs); + ctx->PSGetSamplers(0, kMaxSamplers, psSamplers); + + ctx->OMGetRenderTargets(kMaxRTVs, omRTVs, &omDSV); + ctx->OMGetBlendState(&omBlendState, omBlendFactor, &omSampleMask); + ctx->OMGetDepthStencilState(&omDepthStencilState, &omStencilRef); + + ctx->CSGetShader(&cs, nullptr, nullptr); + ctx->CSGetConstantBuffers(0, kMaxCBs, csCBs); + ctx->CSGetShaderResources(0, kMaxSRVs, csSRVs); + ctx->CSGetSamplers(0, kMaxSamplers, csSamplers); + ctx->CSGetUnorderedAccessViews(0, kMaxUAVs, csUAVs); + } + + void Restore(ID3D11DeviceContext* ctx) + { + ctx->IASetInputLayout(iaInputLayout); + ctx->IASetPrimitiveTopology(iaTopology); + ctx->IASetVertexBuffers(0, kMaxVBs, iaVertexBuffers, iaVBStrides, iaVBOffsets); + ctx->IASetIndexBuffer(iaIndexBuffer, iaIndexFormat, iaIndexOffset); + + ctx->VSSetShader(vs, nullptr, 0); + ctx->VSSetConstantBuffers(0, kMaxCBs, vsCBs); + ctx->VSSetShaderResources(0, kMaxSRVs, vsSRVs); + ctx->VSSetSamplers(0, kMaxSamplers, vsSamplers); + + ctx->HSSetShader(hs, nullptr, 0); + ctx->HSSetConstantBuffers(0, kMaxCBs, hsCBs); + ctx->HSSetShaderResources(0, kMaxSRVs, hsSRVs); + ctx->HSSetSamplers(0, kMaxSamplers, hsSamplers); + + ctx->DSSetShader(ds, nullptr, 0); + ctx->DSSetConstantBuffers(0, kMaxCBs, dsCBs); + ctx->DSSetShaderResources(0, kMaxSRVs, dsSRVs); + ctx->DSSetSamplers(0, kMaxSamplers, dsSamplers); + + ctx->GSSetShader(gs, nullptr, 0); + ctx->GSSetConstantBuffers(0, kMaxCBs, gsCBs); + ctx->GSSetShaderResources(0, kMaxSRVs, gsSRVs); + ctx->GSSetSamplers(0, kMaxSamplers, gsSamplers); + + UINT soOffsets[kMaxSOTargets] = {}; + ctx->SOSetTargets(kMaxSOTargets, soTargets, soOffsets); + + ctx->RSSetState(rs); + ctx->RSSetViewports(rsNumViewports, rsViewports); + ctx->RSSetScissorRects(rsNumScissorRects, rsScissorRects); + + ctx->PSSetShader(ps, nullptr, 0); + ctx->PSSetConstantBuffers(0, kMaxCBs, psCBs); + ctx->PSSetShaderResources(0, kMaxSRVs, psSRVs); + ctx->PSSetSamplers(0, kMaxSamplers, psSamplers); + + ctx->OMSetRenderTargets(kMaxRTVs, omRTVs, omDSV); + ctx->OMSetBlendState(omBlendState, omBlendFactor, omSampleMask); + ctx->OMSetDepthStencilState(omDepthStencilState, omStencilRef); + + ctx->CSSetShader(cs, nullptr, 0); + ctx->CSSetConstantBuffers(0, kMaxCBs, csCBs); + ctx->CSSetShaderResources(0, kMaxSRVs, csSRVs); + ctx->CSSetSamplers(0, kMaxSamplers, csSamplers); + ctx->CSSetUnorderedAccessViews(0, kMaxUAVs, csUAVs, nullptr); + } + + void Release() + { + SafeRelease(iaInputLayout); + SafeReleaseArray(iaVertexBuffers); + SafeRelease(iaIndexBuffer); + + SafeRelease(vs); + SafeReleaseArray(vsCBs); + SafeReleaseArray(vsSRVs); + SafeReleaseArray(vsSamplers); + + SafeRelease(hs); + SafeReleaseArray(hsCBs); + SafeReleaseArray(hsSRVs); + SafeReleaseArray(hsSamplers); + + SafeRelease(ds); + SafeReleaseArray(dsCBs); + SafeReleaseArray(dsSRVs); + SafeReleaseArray(dsSamplers); + + SafeRelease(gs); + SafeReleaseArray(gsCBs); + SafeReleaseArray(gsSRVs); + SafeReleaseArray(gsSamplers); + + SafeReleaseArray(soTargets); + + SafeRelease(rs); + + SafeRelease(ps); + SafeReleaseArray(psCBs); + SafeReleaseArray(psSRVs); + SafeReleaseArray(psSamplers); + + SafeReleaseArray(omRTVs); + SafeRelease(omDSV); + SafeRelease(omBlendState); + SafeRelease(omDepthStencilState); + + SafeRelease(cs); + SafeReleaseArray(csCBs); + SafeReleaseArray(csSRVs); + SafeReleaseArray(csSamplers); + SafeReleaseArray(csUAVs); + } + }; +} diff --git a/src/Features/Effect11/ENBExtender.cpp b/src/Features/Effect11/ENBExtender.cpp new file mode 100644 index 0000000000..b1e22dec99 --- /dev/null +++ b/src/Features/Effect11/ENBExtender.cpp @@ -0,0 +1,1733 @@ +#include "ENBExtender.h" + +#include +#include +#include +#include + +#include "EffectManager.h" +#include "Utils/ShaderPatches.h" + +namespace ENBExtender +{ + static bool IsTruthy(const std::string& s) + { + return !s.empty() && s != "0" && s != "false"; + } + + static bool IsIdentChar(char c) + { + return std::isalnum(static_cast(c)) || c == '_'; + } + + static void Trim(std::string& s, const char* chars = " \t") + { + s.erase(0, s.find_first_not_of(chars)); + s.erase(s.find_last_not_of(chars) + 1); + } + + static std::string BuildGroupPath(const std::vector& stack) + { + std::string path; + for (size_t i = 0; i < stack.size(); ++i) { + if (i > 0) + path += "."; + path += stack[i]; + } + return path; + } + + static std::string ResolveGroup(const std::string& varName, const std::string& explicitGroup, + const std::vector& groupStack, const Effect& effect) + { + if (!explicitGroup.empty()) + return explicitGroup; + if (!groupStack.empty()) + return BuildGroupPath(groupStack); + auto it = effect.sourceGroupMap.find(varName); + return (it != effect.sourceGroupMap.end()) ? it->second : std::string{}; + } + + static int GetSourceOrder(const std::string& varName, const Effect& effect) + { + auto it = effect.sourceOrderMap.find(varName); + return (it != effect.sourceOrderMap.end()) ? it->second : INT_MAX; + } + + static std::string GetStringVariableValue(ID3DX11EffectVariable* variable) + { + auto* strVar = variable->AsString(); + if (strVar && strVar->IsValid()) { + LPCSTR val = nullptr; + if (SUCCEEDED(strVar->GetString(&val)) && val && val[0] != '\0') + return val; + } + return {}; + } + + static Effect::UIVariable MakeSeparator(const std::string& varName, + const std::vector& groupStack, const Effect& effect) + { + Effect::UIVariable sep = {}; + sep.isSeparator = true; + sep.name = varName; + sep.group = ResolveGroup(varName, {}, groupStack, effect); + sep.sourceOrder = GetSourceOrder(varName, effect); + return sep; + } + + int SafeStoi(const std::string& s, int fallback) + { + try { + return std::stoi(s); + } catch (...) { + if (!s.empty()) + logger::warn("[ENBExtender] Failed to parse int from '{}'", s); + return fallback; + } + } + + float SafeStof(const std::string& s, float fallback) + { + try { + return std::stof(s); + } catch (...) { + if (!s.empty()) + logger::warn("[ENBExtender] Failed to parse float from '{}'", s); + return fallback; + } + } + + static Effect::UIWidgetType ParseWidgetType(const std::string& widget) + { + std::string lower = widget; + std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); + if (lower == "dropdown") return Effect::UIWidgetType::Dropdown; + if (lower == "vector") return Effect::UIWidgetType::Vector; + if (lower == "quality") return Effect::UIWidgetType::Quality; + if (lower == "color") return Effect::UIWidgetType::Color; + return Effect::UIWidgetType::Default; + } + + static std::vector ParseDropdownList(const std::string& list) + { + std::vector items; + std::stringstream ss(list); + std::string item; + while (std::getline(ss, item, ',')) { + Trim(item); + items.push_back(item); + } + return items; + } + + // KIEFX + + static constexpr char kiefxMagic[] = "KIEFX\x00\x01"; + static constexpr size_t kiefxMagicSize = sizeof(kiefxMagic) - 1; + static constexpr size_t kiefxKeySize = 8; + static uint8_t kiefxKey[kiefxKeySize] = {}; + static bool kiefxKeyInitialized = false; + static std::string kiefxKeyError; + + static void InitializeKIEFXKey() + { + if (kiefxKeyInitialized) + return; + kiefxKeyInitialized = true; + + std::filesystem::path dllPath = "Data\\KiLoader\\Plugins\\KiENBExtender.dll"; + if (!std::filesystem::exists(dllPath)) { + kiefxKeyError = "KiENBExtender.dll not found at " + dllPath.string(); + logger::warn("[ENBExtender] {}", kiefxKeyError); + return; + } + + std::ifstream file(dllPath, std::ios::binary | std::ios::ate); + if (!file.is_open()) { + kiefxKeyError = "Failed to open " + dllPath.string(); + logger::warn("[ENBExtender] {}", kiefxKeyError); + return; + } + auto size = file.tellg(); + if (size <= 0) { + kiefxKeyError = "Empty or unreadable: " + dllPath.string(); + logger::warn("[ENBExtender] {}", kiefxKeyError); + return; + } + file.seekg(0, std::ios::beg); + std::vector data(static_cast(size)); + if (!file.read(reinterpret_cast(data.data()), size)) { + kiefxKeyError = "Failed to read " + dllPath.string(); + logger::warn("[ENBExtender] {}", kiefxKeyError); + return; + } + + for (size_t i = 0; i + kiefxMagicSize + 1 + kiefxKeySize <= data.size(); ++i) { + if (memcmp(&data[i], kiefxMagic, kiefxMagicSize) == 0) { + memcpy(kiefxKey, &data[i + kiefxMagicSize + 1], kiefxKeySize); + logger::info("[ENBExtender] Extracted KIEFX key from {}", dllPath.string()); + return; + } + } + + kiefxKeyError = "Could not extract KIEFX key from " + dllPath.string(); + logger::warn("[ENBExtender] {}", kiefxKeyError); + } + + bool IsKIEFX(const std::string& content) + { + return content.size() >= kiefxMagicSize && + memcmp(content.data(), kiefxMagic, kiefxMagicSize) == 0; + } + + std::string DecodeKIEFX(const std::string& content) + { + if (!IsKIEFX(content)) + return content; + InitializeKIEFXKey(); + if (!kiefxKeyError.empty()) + return "#error KIEFX decoding failed: " + kiefxKeyError + "\n"; + std::string decoded; + decoded.reserve(content.size() - kiefxMagicSize); + for (size_t i = kiefxMagicSize; i < content.size(); ++i) + decoded += static_cast(static_cast(content[i]) ^ kiefxKey[(i - kiefxMagicSize) % kiefxKeySize]); + return decoded; + } + + static std::string ExtractAnnotation(const std::string& annotations, const std::string& name) + { + for (size_t pos = 0;;) { + pos = annotations.find(name, pos); + if (pos == std::string::npos) + return {}; + size_t end = pos + name.size(); + if ((pos > 0 && IsIdentChar(annotations[pos - 1])) || + (end < annotations.size() && IsIdentChar(annotations[end]))) { + pos = end; + continue; + } + size_t eq = annotations.find('=', end); + if (eq == std::string::npos) + return {}; + size_t vs = annotations.find_first_not_of(" \t", eq + 1); + if (vs == std::string::npos) + return {}; + if (annotations[vs] == '"') { + std::string combined; + size_t cur = vs; + while (cur < annotations.size() && annotations[cur] == '"') { + size_t qe = annotations.find('"', cur + 1); + if (qe == std::string::npos) + return combined; + combined += annotations.substr(cur + 1, qe - cur - 1); + cur = qe + 1; + while (cur < annotations.size() && (annotations[cur] == ' ' || annotations[cur] == '\t')) + ++cur; + } + return combined; + } + size_t ve = annotations.find_first_of(";>", vs); + std::string val = (ve != std::string::npos) ? annotations.substr(vs, ve - vs) : annotations.substr(vs); + Trim(val); + return val; + } + } + + // ConvertExtenderSyntax + + static bool HandlePragmaUIDefine(std::string& line, std::istringstream& stream, + const std::string& iniPath, const std::string& iniSection, std::string& result, std::vector& uiDefines) + { + size_t pragmaPos = line.find("pragma"); + if (pragmaPos == std::string::npos) + return false; + size_t uidefPos = line.find("uidefine", pragmaPos + 6); + if (uidefPos == std::string::npos) + return false; + + for (auto t = line.find_last_not_of(" \t\r"); + t != std::string::npos && line[t] == '\\'; + t = line.find_last_not_of(" \t\r")) { + line.erase(t); + std::string next; + if (!std::getline(stream, next)) + break; + line += next; + } + + size_t openParen = line.find('(', uidefPos); + size_t closeParen = line.rfind(')'); + if (openParen == std::string::npos || closeParen == std::string::npos || closeParen <= openParen) + return false; + + std::string inner = line.substr(openParen + 1, closeParen - openParen - 1); + + size_t typeEnd = inner.find_first_of(" \t"); + if (typeEnd == std::string::npos) + return false; + std::string typeName = inner.substr(0, typeEnd); + Trim(typeName); + + size_t nameStart = inner.find_first_not_of(" \t", typeEnd); + if (nameStart == std::string::npos) + return false; + size_t nameEnd = inner.find_first_of("<=", nameStart); + std::string defineName = (nameEnd != std::string::npos) ? inner.substr(nameStart, nameEnd - nameStart) : inner.substr(nameStart); + Trim(defineName); + + std::string annotations; + size_t angleOpen = inner.find('<', nameStart); + size_t angleClose = inner.rfind('>'); + if (angleOpen != std::string::npos && angleClose != std::string::npos && angleClose > angleOpen) + annotations = inner.substr(angleOpen + 1, angleClose - angleOpen - 1); + + size_t equalsPos = (angleClose != std::string::npos) ? inner.find('=', angleClose) : inner.rfind('='); + std::string defaultVal = "0"; + if (equalsPos != std::string::npos) { + defaultVal = inner.substr(equalsPos + 1); + Trim(defaultVal, " \t;"); + if (defaultVal == "false") defaultVal = "0"; + else if (defaultVal == "true") defaultVal = "1"; + } + + auto ann = [&](const char* name) { return ExtractAnnotation(annotations, name); }; + std::string uiName = ann("UIName"); + std::string uiGroup = ann("UIGroup"); + + std::string finalVal = defaultVal; + if (!iniPath.empty() && !iniSection.empty() && !uiName.empty()) { + std::string iniKey = uiGroup.empty() ? uiName : (uiGroup + "." + uiName); + char buf[1024]; + if (GetPrivateProfileStringA(iniSection.c_str(), iniKey.c_str(), "", buf, sizeof(buf), iniPath.c_str()) > 0) { + std::string iniVal(buf); + Trim(iniVal); + if (iniVal == "false") iniVal = "0"; + else if (iniVal == "true") iniVal = "1"; + finalVal = iniVal; + } + } + + if (!uiName.empty()) { + bool alreadyExists = std::any_of(uiDefines.begin(), uiDefines.end(), + [&](const Effect::UIDefineInfo& existing) { return existing.defineName == defineName; }); + if (!alreadyExists) { + bool isInt = (typeName == "int"); + Effect::UIDefineInfo info; + info.defineName = defineName; + info.displayName = uiName; + info.group = uiGroup; + info.type = typeName; + info.value = finalVal; + info.widget = ann("UIWidget"); + info.list = ann("UIList"); + + auto minStr = ann("UIMin"), maxStr = ann("UIMax"); + if (!minStr.empty()) { if (isInt) info.intMin = SafeStoi(minStr); else info.floatMin = SafeStof(minStr); } + if (!maxStr.empty()) { if (isInt) info.intMax = SafeStoi(maxStr); else info.floatMax = SafeStof(maxStr); } + auto orderStr = ann("UIOrdering"); + if (!orderStr.empty()) { + info.ordering = SafeStoi(orderStr); + info.hasExplicitOrdering = true; + } + uiDefines.push_back(std::move(info)); + } + } + + result += "#define " + defineName + " " + finalVal + "\n"; + result += "string __uidef_" + defineName + ";\n"; + return true; + } + + void ConvertExtenderSyntax(std::string& content, const std::filesystem::path& enbseriesPath, std::vector& uiDefines, const std::string& iniPath, const std::string& iniSection) + { + std::string result; + result.reserve(content.size()); + result += "#define ENB_EXT_VER 1\n"; + std::istringstream stream(content); + std::string line; + + while (std::getline(stream, line)) { + size_t pragmaPos = line.find("#pragma"); + if (pragmaPos != std::string::npos) { + size_t existsPos = line.find("exists", pragmaPos + 7); + if (existsPos != std::string::npos) { + size_t op = line.find('(', existsPos), cp = line.rfind(')'); + if (op != std::string::npos && cp != std::string::npos && cp > op) { + std::string args = line.substr(op + 1, cp - op - 1); + size_t q1 = args.find('"'); + size_t q2 = (q1 != std::string::npos) ? args.find('"', q1 + 1) : std::string::npos; + size_t comma = (q2 != std::string::npos) ? args.find(',', q2 + 1) : std::string::npos; + if (q1 != std::string::npos && q2 != std::string::npos && comma != std::string::npos) { + std::string defName = args.substr(comma + 1); + Trim(defName); + bool exists = std::filesystem::exists(enbseriesPath / args.substr(q1 + 1, q2 - q1 - 1)); + result += "#define " + defName + (exists ? " 1" : " 0") + "\n"; + continue; + } + } + } + } + + if (HandlePragmaUIDefine(line, stream, iniPath, iniSection, result, uiDefines)) + continue; + result += line + "\n"; + } + + content = std::move(result); + } + + // D3DPreprocess doesn't support the # stringification operator, so macros like + // #define TO_STRING(x) #x produce unexpanded tokens instead of string literals. + // We detect these definitions, strip them, and replace all invocations with quoted arguments. + + static std::optional ParseStringifyDefine(const std::string& line) + { + std::string trimmed = line; + if (!trimmed.empty() && trimmed.back() == '\r') + trimmed.pop_back(); + + auto hash = trimmed.find_first_not_of(" \t"); + if (hash == std::string::npos || trimmed[hash] != '#') + return std::nullopt; + + auto keyword = trimmed.find_first_not_of(" \t", hash + 1); + if (keyword == std::string::npos || trimmed.compare(keyword, 6, "define") != 0) + return std::nullopt; + + auto nameStart = trimmed.find_first_not_of(" \t", keyword + 6); + if (nameStart == std::string::npos) + return std::nullopt; + + auto nameEnd = trimmed.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_", nameStart); + if (nameEnd == std::string::npos || nameEnd == nameStart) + return std::nullopt; + + auto openParen = trimmed.find('(', nameEnd); + if (openParen == std::string::npos || trimmed.find_first_not_of(" \t", nameEnd) != openParen) + return std::nullopt; + + auto closeParen = trimmed.find(')', openParen); + if (closeParen == std::string::npos) + return std::nullopt; + + std::string param = trimmed.substr(openParen + 1, closeParen - openParen - 1); + Trim(param); + + auto bodyStart = trimmed.find_first_not_of(" \t", closeParen + 1); + if (bodyStart == std::string::npos || trimmed[bodyStart] != '#') + return std::nullopt; + + std::string body = trimmed.substr(bodyStart + 1); + Trim(body, " \t\r"); + + if (body != param) + return std::nullopt; + + return trimmed.substr(nameStart, nameEnd - nameStart); + } + + static std::string ReplaceStringifyInvocations(const std::string& source, const std::string& macroName) + { + std::string result; + result.reserve(source.size()); + size_t pos = 0; + + while (pos < source.size()) { + size_t found = source.find(macroName, pos); + if (found == std::string::npos) { + result.append(source, pos); + break; + } + + bool partOfLargerIdent = + (found > 0 && IsIdentChar(source[found - 1])) || + (found + macroName.size() < source.size() && IsIdentChar(source[found + macroName.size()])); + + if (partOfLargerIdent) { + result.append(source, pos, found + macroName.size() - pos); + pos = found + macroName.size(); + continue; + } + + size_t afterName = found + macroName.size(); + while (afterName < source.size() && (source[afterName] == ' ' || source[afterName] == '\t')) + ++afterName; + + if (afterName >= source.size() || source[afterName] != '(') { + result.append(source, pos, found + macroName.size() - pos); + pos = found + macroName.size(); + continue; + } + + int depth = 1; + size_t argEnd = afterName + 1; + while (argEnd < source.size() && depth > 0) { + if (source[argEnd] == '(') + ++depth; + else if (source[argEnd] == ')') + --depth; + ++argEnd; + } + + if (depth != 0) { + result.append(source, pos, found + macroName.size() - pos); + pos = found + macroName.size(); + continue; + } + + std::string arg = source.substr(afterName + 1, argEnd - afterName - 2); + Trim(arg); + + result.append(source, pos, found - pos); + result += "\"" + arg + "\""; + pos = argEnd; + } + + return result; + } + + void ExpandStringificationMacros(std::string& source) + { + std::vector macroNames; + std::string stripped; + stripped.reserve(source.size()); + + std::istringstream stream(source); + std::string line; + while (std::getline(stream, line)) { + if (auto name = ParseStringifyDefine(line)) { + macroNames.push_back(*name); + stripped += "\n"; + } else { + stripped += line + "\n"; + } + } + + if (macroNames.empty()) + return; + + for (auto& name : macroNames) { + logger::debug("[ENBEXTENDER] Expanding stringification macro: {}", name); + stripped = ReplaceStringifyInvocations(stripped, name); + } + + source = std::move(stripped); + } + + // Source-based group scoping (compiled effect reorders variable types, so source text is the ground truth for declaration order) + + static bool IsHLSLType(const std::string& s) + { + static const std::unordered_set types = { "float", "float2", "float3", "float4", "int", "bool", "string" }; + return types.count(s) > 0; + } + + void ParseSourceGroupScopes(const std::string& preprocessedSource, Effect& effect) + { + effect.sourceGroupMap.clear(); + effect.sourceOrderMap.clear(); + std::vector groupStack; + int declarationIndex = 0; + + std::istringstream stream(preprocessedSource); + std::string line; + + while (std::getline(stream, line)) { + size_t firstNonSpace = line.find_first_not_of(" \t"); + if (firstNonSpace == std::string::npos || line[firstNonSpace] == '#') + continue; + + if (line.find("UIGroupBegin") != std::string::npos) { + std::string groupName; + size_t gt = line.rfind('>'); + size_t q1 = (gt != std::string::npos) ? line.find('"', gt) : std::string::npos; + size_t q2 = (q1 != std::string::npos) ? line.find('"', q1 + 1) : std::string::npos; + if (q2 != std::string::npos) + groupName = line.substr(q1 + 1, q2 - q1 - 1); + if (groupName.empty()) + groupName = ExtractAnnotation(line, "UIGroup"); + if (!groupName.empty()) { + groupStack.push_back(groupName); + auto groupPath = BuildGroupPath(groupStack); + auto& gm = effect.groupMeta[groupPath]; + auto orderStr = ExtractAnnotation(line, "UIOrdering"); + if (!orderStr.empty()) { + gm.ordering = SafeStoi(orderStr); + gm.hasOrdering = true; + } + } + continue; + } + + if (line.find("UIGroupEnd") != std::string::npos) { + if (!groupStack.empty()) + groupStack.pop_back(); + continue; + } + + std::string trimmed = line.substr(firstNonSpace); + size_t spacePos = trimmed.find_first_of(" \t"); + if (spacePos == std::string::npos || !IsHLSLType(trimmed.substr(0, spacePos))) + continue; + + size_t nameStart = trimmed.find_first_not_of(" \t", spacePos); + if (nameStart == std::string::npos) + continue; + size_t nameEnd = nameStart; + while (nameEnd < trimmed.size() && IsIdentChar(trimmed[nameEnd])) + nameEnd++; + + if (nameEnd > nameStart) { + std::string varName = trimmed.substr(nameStart, nameEnd - nameStart); + if (varName.find("UIGroupBegin") == std::string::npos && varName.find("UIGroupEnd") == std::string::npos) { + auto [it, inserted] = effect.sourceOrderMap.try_emplace(varName, declarationIndex); + if (inserted) { + declarationIndex++; + if (!groupStack.empty()) + effect.sourceGroupMap[varName] = BuildGroupPath(groupStack); + } + } + } + } + } + + // Group metadata tracking + + static void CollectGroupMeta(const std::string& groupPath, ID3DX11EffectVariable* variable, Effect& effect) + { + if (groupPath.empty()) + return; + auto& gm = effect.groupMeta[groupPath]; + auto get = [&](const char* name) { return effect.GetUIAnnotation(variable, name); }; + if (gm.displayName.empty()) { + auto s = get("UIGroupName"); + if (!s.empty()) + gm.displayName = s; + } + if (!gm.defaultOpen) { + auto s = get("UIGroupOpen"); + if (!s.empty()) + gm.defaultOpen = IsTruthy(s); + } + if (!gm.hasOrdering) { + auto s = get("UIOrdering"); + if (!s.empty()) { + gm.ordering = SafeStoi(s); + gm.hasOrdering = true; + } + } + if (!gm.isTopLevel) { + auto s = get("UITopLevel"); + if (!s.empty()) + gm.isTopLevel = IsTruthy(s); + } + } + + // Shared annotation application (file-local, used by CreateUIVariable and ProcessExtenderStringVariable) + + static void ApplyAnnotations(Effect::UIVariable& uiVar, ID3DX11EffectVariable* variable, + const std::vector& groupStack, Effect& effect) + { + auto get = [&](const char* name) { return effect.GetUIAnnotation(variable, name); }; + + auto explicitGroup = get("UIGroup"); + uiVar.group = ResolveGroup(uiVar.name, explicitGroup, groupStack, effect); + uiVar.sourceOrder = GetSourceOrder(uiVar.name, effect); + + auto orderStr = get("UIOrdering"); + if (!orderStr.empty()) + uiVar.ordering = SafeStoi(orderStr); + + uiVar.isReadOnly = IsTruthy(get("UIReadOnly")); + auto hiddenStr = get("UIHidden"), visibleStr = get("UIVisible"); + uiVar.isHidden = IsTruthy(hiddenStr) || (!visibleStr.empty() && !IsTruthy(visibleStr)); + + CollectGroupMeta(uiVar.group, variable, effect); + if (IsTruthy(get("UITopLevel")) && explicitGroup.empty()) + uiVar.group.clear(); + + uiVar.uniqueName = get("UniqueName"); + uiVar.uiBinding = get("UIBinding"); + uiVar.uiBindingFile = get("UIBindingFile"); + uiVar.uiBindingProperty = get("UIBindingProperty"); + uiVar.uiBindingCondition = get("UIBindingCondition"); + uiVar.ignorePerfMode = IsTruthy(get("UIIgnorePerfMode")); + if (IsTruthy(get("UIWeatherString"))) + logger::info("[ENBExtender] UIWeatherString on '{}' (not yet implemented)", uiVar.name); + uiVar.isWeatherOnlyString = IsTruthy(get("UIWeatherOnlyString")); + if (uiVar.isWeatherOnlyString) + logger::info("[ENBExtender] UIWeatherOnlyString on '{}' — hidden from main UI", uiVar.name); + if (uiVar.type == Effect::UIVariableType::Float || uiVar.type == Effect::UIVariableType::Float2 || + uiVar.type == Effect::UIVariableType::Float3 || uiVar.type == Effect::UIVariableType::Float4) + uiVar.separation = get("Separation"); + } + + static void ParseTimePeriod(Effect::UIVariable& uiVar) + { + if (uiVar.separation.empty() || uiVar.separation == "None") + return; + static const std::string_view periods[] = { "Dawn", "Sunrise", "Day", "Sunset", "Dusk", "Night", "Interior" }; + for (auto period : periods) { + auto suffix = std::string(period) + " - "; + if (uiVar.displayName.compare(0, 3 + suffix.size(), "|- " + suffix) == 0 || + uiVar.displayName.compare(0, suffix.size(), suffix) == 0) { + uiVar.timePeriod = period; + return; + } + } + } + + // CreateUIVariable — consolidated annotation reading for compiled effect variables + + bool CreateUIVariable(Effect::UIVariable& out, ID3DX11EffectVariable* variable, + const D3DX11_EFFECT_VARIABLE_DESC& varDesc, const D3DX11_EFFECT_TYPE_DESC& typeDesc, + const std::vector& groupStack, Effect& effect) + { + auto get = [&](const char* name) { return effect.GetUIAnnotation(variable, name); }; + + std::string uiName = get("UIName"); + if (uiName.empty()) + return false; + if (uiName.find_first_not_of(" \t") != std::string::npos) + Trim(uiName); + + out = {}; + out.name = varDesc.Name; + out.displayName = uiName; + out.effectVariable.copy_from(variable); + + if (typeDesc.Class == D3D_SVC_SCALAR) { + switch (typeDesc.Type) { + case D3D_SVT_FLOAT: out.type = Effect::UIVariableType::Float; break; + case D3D_SVT_INT: out.type = Effect::UIVariableType::Int; break; + case D3D_SVT_BOOL: out.type = Effect::UIVariableType::Bool; break; + default: return false; + } + } else if (typeDesc.Class == D3D_SVC_VECTOR && typeDesc.Type == D3D_SVT_FLOAT && typeDesc.Elements == 0) { + if (typeDesc.Columns == 2) out.type = Effect::UIVariableType::Float2; + else if (typeDesc.Columns == 3) out.type = Effect::UIVariableType::Float3; + else if (typeDesc.Columns == 4) out.type = Effect::UIVariableType::Float4; + else return false; + } else { + return false; + } + + out.widgetType = ParseWidgetType(get("UIWidget")); + + if (out.type == Effect::UIVariableType::Float || out.type == Effect::UIVariableType::Float2 || + out.type == Effect::UIVariableType::Float3 || out.type == Effect::UIVariableType::Float4) { + auto s = get("UIMin"); if (!s.empty()) out.floatMin = SafeStof(s, out.floatMin); + s = get("UIMax"); if (!s.empty()) out.floatMax = SafeStof(s, out.floatMax); + } else if (out.type == Effect::UIVariableType::Int) { + auto s = get("UIMin"); if (!s.empty()) out.intMin = SafeStoi(s, out.intMin); + s = get("UIMax"); if (!s.empty()) out.intMax = SafeStoi(s, out.intMax); + if (out.widgetType == Effect::UIWidgetType::Dropdown) { + s = get("UIList"); if (!s.empty()) out.dropdownItems = ParseDropdownList(s); + } else if (out.widgetType == Effect::UIWidgetType::Quality) { + out.dropdownItems = { "Very High", "High", "Medium", "Low", "Very Low" }; + out.intMin = -1; + out.intMax = 3; + } + } + + ApplyAnnotations(out, variable, groupStack, effect); + if (out.isHidden) + return false; + + if (!out.isReadOnly) { + if ((out.type == Effect::UIVariableType::Float || out.type == Effect::UIVariableType::Float2 || + out.type == Effect::UIVariableType::Float3 || out.type == Effect::UIVariableType::Float4) && + out.floatMin == out.floatMax) + out.isReadOnly = true; + else if (out.type == Effect::UIVariableType::Int && out.intMin == out.intMax) + out.isReadOnly = true; + } + + ParseTimePeriod(out); + return true; + } + + // ProcessExtenderStringVariable + + bool ProcessExtenderStringVariable(ID3DX11EffectVariable* variable, const D3DX11_EFFECT_VARIABLE_DESC& varDesc, + std::vector& groupStack, Effect& effect) + { + auto get = [&](const char* name) { return effect.GetUIAnnotation(variable, name); }; + + if (IsTruthy(get("UIGroupBegin"))) { + std::string groupName = GetStringVariableValue(variable); + if (groupName.empty()) + groupName = get("UIGroup"); + if (!groupName.empty()) { + groupStack.push_back(groupName); + CollectGroupMeta(BuildGroupPath(groupStack), variable, effect); + } + return true; + } + + if (IsTruthy(get("UIGroupEnd"))) { + if (!groupStack.empty()) + groupStack.pop_back(); + return true; + } + + if (IsTruthy(get("UISeparator"))) { + auto sep = MakeSeparator(varDesc.Name, groupStack, effect); + auto explicitGroup = get("UIGroup"); + if (!explicitGroup.empty()) + sep.group = explicitGroup; + effect.uiVariables.push_back(sep); + return true; + } + + std::string labelText = get("UIName"); + if (labelText.empty()) + labelText = GetStringVariableValue(variable); + Trim(labelText); + if (labelText.empty()) + return true; + + Effect::UIVariable labelVar = {}; + labelVar.name = varDesc.Name; + labelVar.displayName = labelText; + labelVar.isLabel = true; + ApplyAnnotations(labelVar, variable, groupStack, effect); + if (!labelVar.isHidden) + effect.uiVariables.push_back(labelVar); + return true; + } + + // InsertUIDefines + + void InsertUIDefines(Effect& effect) + { + int fallbackOrder = -static_cast(effect.uiDefines.size()); + for (const auto& def : effect.uiDefines) { + Effect::UIVariable uiVar = {}; + uiVar.name = def.defineName; + uiVar.displayName = def.displayName; + uiVar.group = def.group; + uiVar.ordering = def.ordering; + uiVar.isDefine = true; + + auto it = effect.sourceOrderMap.find("__uidef_" + def.defineName); + uiVar.sourceOrder = (it != effect.sourceOrderMap.end()) ? it->second : fallbackOrder++; + + if (def.type == "bool") { + uiVar.type = Effect::UIVariableType::Bool; + uiVar.boolValue = (def.value != "0"); + } else if (def.type == "int") { + uiVar.type = Effect::UIVariableType::Int; + uiVar.intValue = SafeStoi(def.value); + uiVar.intMin = def.intMin; + uiVar.intMax = def.intMax; + uiVar.widgetType = ParseWidgetType(def.widget); + if (uiVar.widgetType == Effect::UIWidgetType::Dropdown && !def.list.empty()) + uiVar.dropdownItems = ParseDropdownList(def.list); + else if (uiVar.widgetType == Effect::UIWidgetType::Quality) { + uiVar.dropdownItems = { "Very High", "High", "Medium", "Low", "Very Low" }; + uiVar.intMin = -1; + uiVar.intMax = 3; + } + } else { + uiVar.type = Effect::UIVariableType::Float; + uiVar.floatValue = SafeStof(def.value); + uiVar.floatMin = def.floatMin; + uiVar.floatMax = def.floatMax; + } + + if (!def.group.empty() && def.hasExplicitOrdering) { + auto& gm = effect.groupMeta[def.group]; + if (!gm.hasOrdering) { + gm.ordering = def.ordering; + gm.hasOrdering = true; + } + } + + effect.uiVariables.push_back(uiVar); + } + } + + // UI tree and rendering + + struct VarRef + { + Effect* effect; + int index; + }; + + struct GroupNode + { + std::string name; + std::string fullPath; + std::vector vars; + std::vector> children; + }; + + static bool EvaluateCondition(const std::string& condStr, float boundValue) + { + if (condStr.empty()) + return boundValue != 0.0f; + size_t valueStart = 0; + if (condStr.size() >= 2 && !std::isdigit(static_cast(condStr[1])) && condStr[1] != '-') + valueStart = 2; + else if (condStr[0] == '<' || condStr[0] == '>') + valueStart = 1; + else + return boundValue != 0.0f; + float cmp = SafeStof(condStr.substr(valueStart)); + char c0 = condStr[0], c1 = (condStr.size() >= 2) ? condStr[1] : '\0'; + if (c0 == '=' && c1 == '=') return boundValue == cmp; + if (c0 == '!' && c1 == '=') return boundValue != cmp; + if (c0 == '<' && c1 == '=') return boundValue <= cmp; + if (c0 == '>' && c1 == '=') return boundValue >= cmp; + if (c0 == '=' && c1 == '<') return boundValue <= cmp; + if (c0 == '=' && c1 == '>') return boundValue >= cmp; + if (c0 == '<') return boundValue < cmp; + if (c0 == '>') return boundValue > cmp; + return false; + } + + using FileUniqueNameMap = std::unordered_map>; + + static std::pair EvaluateBinding(const Effect::UIVariable& var, + const std::unordered_map& uniqueNameMap, + const FileUniqueNameMap& fileUniqueNameMap) + { + bool visible = true, readOnly = var.isReadOnly; + if (var.uiBinding.empty()) + return { visible, readOnly }; + + const VarRef* boundRef = nullptr; + if (!var.uiBindingFile.empty()) { + auto fileIt = fileUniqueNameMap.find(var.uiBindingFile); + if (fileIt != fileUniqueNameMap.end()) { + auto varIt = fileIt->second.find(var.uiBinding); + if (varIt != fileIt->second.end()) + boundRef = &varIt->second; + } + } else { + auto it = uniqueNameMap.find(var.uiBinding); + if (it != uniqueNameMap.end()) + boundRef = &it->second; + } + + if (!boundRef) + return { visible, readOnly }; + + const auto& bv = boundRef->effect->uiVariables[boundRef->index]; + float val = 0.0f; + switch (bv.type) { + case Effect::UIVariableType::Float: val = bv.floatValue; break; + case Effect::UIVariableType::Int: val = static_cast(bv.intValue); break; + case Effect::UIVariableType::Bool: val = bv.boolValue ? 1.0f : 0.0f; break; + default: break; + } + bool cond = EvaluateCondition(var.uiBindingCondition, val); + + std::string prop = var.uiBindingProperty; + std::transform(prop.begin(), prop.end(), prop.begin(), ::tolower); + if (prop == "hidden") visible = !cond; + else if (prop == "visible") visible = cond; + else if (prop == "readonly") readOnly = cond; + else if (prop == "readwrite") readOnly = !cond; + + return { visible, readOnly }; + } + + // Tree construction + + static GroupNode* FindOrCreateChild(GroupNode& parent, const std::string& segment, const std::string& fullPath) + { + for (auto& c : parent.children) + if (c->name == segment) + return c.get(); + auto nc = std::make_unique(); + nc->name = segment; + nc->fullPath = fullPath; + auto* ptr = nc.get(); + parent.children.push_back(std::move(nc)); + return ptr; + } + + using GroupMetaMap = std::unordered_map; + + static GroupNode* TraverseGroupPath(GroupNode& root, const std::string& groupPath, const GroupMetaMap& meta = {}) + { + GroupNode* node = &root; + size_t start = 0; + while (start < groupPath.size()) { + size_t dot = groupPath.find('.', start); + if (dot == std::string::npos) + dot = groupPath.size(); + std::string fullPath = groupPath.substr(0, dot); + auto metaIt = meta.find(fullPath); + if (metaIt != meta.end() && metaIt->second.isTopLevel) + node = &root; + node = FindOrCreateChild(*node, groupPath.substr(start, dot - start), fullPath); + start = dot + 1; + } + return node; + } + + static void BuildUITree(std::span effects, GroupNode& root, GroupMetaMap& meta, + std::unordered_map& uniqueNameMap, FileUniqueNameMap& fileUniqueNameMap) + { + std::unordered_map> seenDisplayNames; + std::unordered_map> seenSepOrders; + + for (auto* effect : effects) { + if (!effect->IsCompiled()) + continue; + + for (auto& [path, gm] : effect->groupMeta) { + auto [it, inserted] = meta.try_emplace(path, gm); + if (!inserted) { + if (it->second.displayName.empty() && !gm.displayName.empty()) + it->second.displayName = gm.displayName; + if (!it->second.defaultOpen && gm.defaultOpen) + it->second.defaultOpen = gm.defaultOpen; + if (!it->second.hasOrdering && gm.hasOrdering) { + it->second.ordering = gm.ordering; + it->second.hasOrdering = true; + } + if (!it->second.isTopLevel && gm.isTopLevel) + it->second.isTopLevel = true; + } + } + + auto& fileMap = fileUniqueNameMap[effect->GetName()]; + + for (int i = 0; i < static_cast(effect->uiVariables.size()); ++i) { + auto& var = effect->uiVariables[i]; + + if (!var.isSeparator) { + std::string uname = !var.uniqueName.empty() ? var.uniqueName + : !var.group.empty() ? var.group + "." + var.displayName + : var.displayName; + uniqueNameMap[uname] = { effect, i }; + fileMap[uname] = { effect, i }; + } + + GroupNode* node = !var.group.empty() ? TraverseGroupPath(root, var.group, meta) : &root; + + if (var.isSeparator) { + if (seenSepOrders[node].insert(var.sourceOrder).second) + node->vars.push_back({ effect, i }); + continue; + } + if (!var.displayName.empty() && !seenDisplayNames[node].insert(var.displayName).second) + continue; + node->vars.push_back({ effect, i }); + } + } + } + + static void InheritChildOrdering(GroupNode& node, GroupMetaMap& meta) + { + for (auto& child : node.children) { + InheritChildOrdering(*child, meta); + auto it = meta.find(child->fullPath); + if (it != meta.end() && !it->second.hasOrdering && !child->children.empty()) { + int maxChildOrder = 0; + for (auto& gc : child->children) { + auto gcIt = meta.find(gc->fullPath); + if (gcIt != meta.end() && gcIt->second.ordering > maxChildOrder) + maxChildOrder = gcIt->second.ordering; + } + if (maxChildOrder > 0) { + it->second.ordering = maxChildOrder; + it->second.hasOrdering = true; + } + } + } + } + + static void ApplyUIOrdering(GroupNode& node, const GroupMetaMap& meta) + { + std::stable_sort(node.vars.begin(), node.vars.end(), [](const VarRef& a, const VarRef& b) { + auto& va = a.effect->uiVariables[a.index]; + auto& vb = b.effect->uiVariables[b.index]; + if (va.ordering != vb.ordering) return va.ordering > vb.ordering; + if (va.sourceOrder != vb.sourceOrder) return va.sourceOrder < vb.sourceOrder; + return false; + }); + std::stable_sort(node.children.begin(), node.children.end(), [&](const auto& a, const auto& b) { + auto itA = meta.find(a->fullPath), itB = meta.find(b->fullPath); + int oA = (itA != meta.end()) ? itA->second.ordering : 0; + int oB = (itB != meta.end()) ? itB->second.ordering : 0; + return oA > oB; + }); + for (auto& child : node.children) + ApplyUIOrdering(*child, meta); + } + + // UI rendering + + static int ComputeMinSourceOrder(const GroupNode& node) + { + int minSO = INT_MAX; + for (auto& ref : node.vars) { + int so = ref.effect->uiVariables[ref.index].sourceOrder; + if (so < minSO) + minSO = so; + } + for (auto& c : node.children) { + int cso = ComputeMinSourceOrder(*c); + if (cso < minSO) + minSO = cso; + } + return minSO; + } + + struct RenderContext + { + std::unordered_map& uniqueNameMap; + FileUniqueNameMap& fileUniqueNameMap; + std::unordered_set& changedEffects; + GroupMetaMap& meta; + bool performanceMode = false; + int tableCounter = 0; + + bool BeginVarTable() + { + std::string tableId = "##ut_" + std::to_string(tableCounter++); + if (ImGui::BeginTable(tableId.c_str(), 2, ImGuiTableFlags_SizingFixedFit)) { + float w = ImGui::GetContentRegionAvail().x; + ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed, w * 0.45f); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, w * 0.55f); + return true; + } + return false; + } + }; + + static void RenderWidget(const std::string& label, const std::string& id, + Effect::UIVariable& uiVar, bool readOnly, Effect* effect, + std::unordered_set& changedEffects) + { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + if (readOnly) + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); + ImGui::Text("%s", label.c_str()); + + ImGui::TableSetColumnIndex(1); + if (readOnly) + ImGui::BeginDisabled(); + bool changed = false; + float floatStep = (uiVar.floatMax - uiVar.floatMin) / 100.0f; + switch (uiVar.type) { + case Effect::UIVariableType::Float: + changed = ImGui::InputFloat(id.c_str(), &uiVar.floatValue, floatStep, floatStep * 10.0f, "%.3f"); + if (changed) + uiVar.floatValue = std::clamp(uiVar.floatValue, uiVar.floatMin, uiVar.floatMax); + break; + case Effect::UIVariableType::Int: + if ((uiVar.widgetType == Effect::UIWidgetType::Dropdown || uiVar.widgetType == Effect::UIWidgetType::Quality) && !uiVar.dropdownItems.empty()) { + int di = (uiVar.widgetType == Effect::UIWidgetType::Quality) ? uiVar.intValue + 1 : uiVar.intValue; + const char* cur = (di >= 0 && di < static_cast(uiVar.dropdownItems.size())) ? uiVar.dropdownItems[di].c_str() : ""; + if (ImGui::BeginCombo(id.c_str(), cur)) { + for (int j = 0; j < static_cast(uiVar.dropdownItems.size()); ++j) { + int iv = (uiVar.widgetType == Effect::UIWidgetType::Quality) ? (j - 1) : j; + if (ImGui::Selectable(uiVar.dropdownItems[j].c_str(), uiVar.intValue == iv)) { + uiVar.intValue = iv; + changed = true; + } + } + ImGui::EndCombo(); + } + } else { + changed = ImGui::InputInt(id.c_str(), &uiVar.intValue, 1, 10); + if (changed) + uiVar.intValue = std::clamp(uiVar.intValue, uiVar.intMin, uiVar.intMax); + } + break; + case Effect::UIVariableType::Bool: + changed = ImGui::Checkbox(id.c_str(), &uiVar.boolValue); + break; + case Effect::UIVariableType::Float2: + changed = ImGui::InputScalarN(id.c_str(), ImGuiDataType_Float, uiVar.vectorValue, 2, &floatStep, nullptr, "%.3f"); + if (changed) + for (int i = 0; i < 2; ++i) + uiVar.vectorValue[i] = std::clamp(uiVar.vectorValue[i], uiVar.floatMin, uiVar.floatMax); + break; + case Effect::UIVariableType::Float3: + if (uiVar.widgetType == Effect::UIWidgetType::Color) { + changed = ImGui::ColorEdit3(id.c_str(), uiVar.vectorValue); + } else { + float min3 = (uiVar.widgetType == Effect::UIWidgetType::Vector) ? -1.0f : uiVar.floatMin; + float max3 = (uiVar.widgetType == Effect::UIWidgetType::Vector) ? 1.0f : uiVar.floatMax; + float step3 = (max3 - min3) / 100.0f; + changed = ImGui::InputScalarN(id.c_str(), ImGuiDataType_Float, uiVar.vectorValue, 3, &step3, nullptr, "%.3f"); + if (changed) + for (int i = 0; i < 3; ++i) + uiVar.vectorValue[i] = std::clamp(uiVar.vectorValue[i], min3, max3); + } + break; + case Effect::UIVariableType::Float4: + if (uiVar.widgetType == Effect::UIWidgetType::Color) { + changed = ImGui::ColorEdit4(id.c_str(), uiVar.vectorValue); + } else { + changed = ImGui::InputScalarN(id.c_str(), ImGuiDataType_Float, uiVar.vectorValue, 4, &floatStep, nullptr, "%.3f"); + if (changed) + for (int i = 0; i < 4; ++i) + uiVar.vectorValue[i] = std::clamp(uiVar.vectorValue[i], uiVar.floatMin, uiVar.floatMax); + } + break; + } + if (changed) + changedEffects.insert(effect); + if (readOnly) + ImGui::EndDisabled(); + + if (readOnly) + ImGui::PopStyleColor(); + } + + static void RenderVar(VarRef& ref, bool& inTable, bool& lastWasSeparator, RenderContext& ctx) + { + auto& uiVar = ref.effect->uiVariables[ref.index]; + + if (uiVar.isSeparator) { + if (!lastWasSeparator) { + if (inTable) { ImGui::EndTable(); inTable = false; } + ImGui::Separator(); + lastWasSeparator = true; + } + return; + } + + if (uiVar.displayName.empty() || uiVar.isHidden || uiVar.isWeatherOnlyString) + return; + if (ctx.performanceMode && !uiVar.ignorePerfMode) + return; + + auto [bindVisible, bindReadOnly] = EvaluateBinding(uiVar, ctx.uniqueNameMap, ctx.fileUniqueNameMap); + if (!bindVisible) + return; + + lastWasSeparator = false; + + if (uiVar.isLabel) { + if (inTable) { ImGui::EndTable(); inTable = false; } + if (uiVar.isReadOnly) + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); + ImGui::TextWrapped("%s", uiVar.displayName.c_str()); + if (uiVar.isReadOnly) + ImGui::PopStyleColor(); + } else { + if (!inTable) { + if (!ctx.BeginVarTable()) + return; + inTable = true; + } + RenderWidget(uiVar.displayName, "##uv_" + std::to_string(ref.index) + "_" + ref.effect->GetName(), + uiVar, bindReadOnly, ref.effect, ctx.changedEffects); + } + } + + static void RenderTechniqueDropdown(Effect* effect, std::unordered_set& changedEffects) + { + ImGui::Text("%s", effect->techniqueDropdown.name.c_str()); + ImGui::SameLine(); + ImGui::SetNextItemWidth(-1); + const char* current = effect->uiTechniques[effect->selectedTechniqueIndex].displayName.c_str(); + if (ImGui::BeginCombo(("##TECHNIQUE_" + effect->GetName()).c_str(), current)) { + for (uint32_t i = 0; i < effect->uiTechniques.size(); ++i) { + if (ImGui::Selectable(effect->uiTechniques[i].displayName.c_str(), effect->selectedTechniqueIndex == i)) { + effect->selectedTechniqueIndex = i; + changedEffects.insert(effect); + } + if (effect->selectedTechniqueIndex == i) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + } + + static void RenderGroupNode(GroupNode& node, RenderContext& ctx, + const std::vector>& techDropdowns) + { + for (auto& [effect, group] : techDropdowns) + if (!group.empty() && group == node.fullPath && !effect->techniqueDropdown.topLevel) + RenderTechniqueDropdown(effect, ctx.changedEffects); + + struct RenderItem + { + int ordering; + int sourceOrder; + VarRef* var = nullptr; + GroupNode* child = nullptr; + }; + + std::vector items; + items.reserve(node.vars.size() + node.children.size()); + + for (auto& ref : node.vars) { + auto& uiVar = ref.effect->uiVariables[ref.index]; + items.push_back({ uiVar.ordering, uiVar.sourceOrder, &ref, nullptr }); + } + + for (auto& child : node.children) { + auto metaIt = ctx.meta.find(child->fullPath); + int ordering = (metaIt != ctx.meta.end()) ? metaIt->second.ordering : 0; + items.push_back({ ordering, ComputeMinSourceOrder(*child), nullptr, child.get() }); + } + + std::stable_sort(items.begin(), items.end(), [](const RenderItem& a, const RenderItem& b) { + if (a.ordering != b.ordering) + return a.ordering > b.ordering; + return a.sourceOrder < b.sourceOrder; + }); + + bool inTable = false; + bool lastWasSeparator = false; + + for (auto& item : items) { + if (item.var) { + RenderVar(*item.var, inTable, lastWasSeparator, ctx); + } else { + if (inTable) { + ImGui::EndTable(); + inTable = false; + } + lastWasSeparator = false; + + std::string displayName = item.child->name; + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_None; + auto metaIt = ctx.meta.find(item.child->fullPath); + if (metaIt != ctx.meta.end()) { + if (!metaIt->second.displayName.empty()) + displayName = metaIt->second.displayName; + if (metaIt->second.defaultOpen) + flags = ImGuiTreeNodeFlags_DefaultOpen; + } + if (ImGui::TreeNodeEx((displayName + "###ugrp_" + item.child->fullPath).c_str(), flags)) { + RenderGroupNode(*item.child, ctx, techDropdowns); + ImGui::TreePop(); + } + } + } + + if (inTable) + ImGui::EndTable(); + } + + void RenderUI(std::span effects) + { + std::unordered_map uniqueNameMap; + FileUniqueNameMap fileUniqueNameMap; + + GroupNode root; + GroupMetaMap meta; + BuildUITree(effects, root, meta, uniqueNameMap, fileUniqueNameMap); + + std::vector> techDropdowns; + for (auto* effect : effects) { + if (!effect->IsCompiled() || effect->uiTechniques.size() <= 1 || !effect->techniqueDropdown.visible) + continue; + techDropdowns.push_back({ effect, effect->techniqueDropdown.group }); + if (!effect->techniqueDropdown.group.empty()) { + auto [it, inserted] = meta.try_emplace(effect->techniqueDropdown.group); + if (inserted) { + it->second.displayName = effect->techniqueDropdown.groupName; + it->second.defaultOpen = effect->techniqueDropdown.groupOpen; + it->second.ordering = effect->techniqueDropdown.ordering; + it->second.hasOrdering = true; + } + TraverseGroupPath(root, effect->techniqueDropdown.group, meta); + } + } + + InheritChildOrdering(root, meta); + ApplyUIOrdering(root, meta); + + std::unordered_set changedEffects; + RenderContext ctx{ uniqueNameMap, fileUniqueNameMap, changedEffects, meta, + EffectManager::GetSingleton().performanceMode }; + + for (auto& [effect, group] : techDropdowns) + if (effect->techniqueDropdown.topLevel || group.empty()) + RenderTechniqueDropdown(effect, changedEffects); + + RenderGroupNode(root, ctx, techDropdowns); + + for (auto* effect : changedEffects) + effect->UpdateUIVariables(); + + for (auto* effect : effects) { + if (!effect->GetErrors().empty()) { + ImGui::TextColored(ImVec4(1, 0.3f, 0.3f, 1), "%s:", effect->GetName().c_str()); + for (const auto& err : effect->GetErrors()) + ImGui::TextWrapped("%s", err.c_str()); + } + } + } + + void RenderUI(Effect& effect) + { + Effect* ptr = &effect; + RenderUI({ &ptr, 1 }); + } + + // Technique evaluation + + bool IsTechniqueEnabled(const Effect::TechniqueInfo& info, const Effect& effect) + { + for (auto& binding : info.bindings) { + bool found = false; + bool val = false; + for (auto& uiVar : effect.uiVariables) { + std::string uname = !uiVar.uniqueName.empty() ? uiVar.uniqueName + : !uiVar.group.empty() ? uiVar.group + "." + uiVar.displayName + : uiVar.displayName; + if (uname == binding.variableName) { + found = true; + switch (uiVar.type) { + case Effect::UIVariableType::Bool: val = uiVar.boolValue; break; + case Effect::UIVariableType::Int: val = uiVar.intValue != 0; break; + case Effect::UIVariableType::Float: val = uiVar.floatValue != 0.0f; break; + default: val = true; break; + } + break; + } + } + if (!found) + continue; + bool enabled = binding.inverted ? !val : val; + if (!enabled) + return false; + } + return true; + } + + // Post-load processing + + void LoadTechniqueDropdownMetadata(Effect& effect) + { + if (!effect.IsCompiled()) + return; + auto* fx = effect.GetEffect(); + if (!fx) + return; + D3DX11_EFFECT_DESC effectDesc; + if (FAILED(fx->GetDesc(&effectDesc))) + return; + + ID3DX11EffectGroup* annotationGroup = nullptr; + ID3DX11EffectTechnique* annotationTechnique = nullptr; + + for (UINT g = 0; g < effectDesc.Groups; ++g) { + auto* group = fx->GetGroupByIndex(g); + if (!group || !group->IsValid()) + continue; + D3DX11_GROUP_DESC groupDesc; + if (FAILED(group->GetDesc(&groupDesc))) + continue; + + if (groupDesc.Name && groupDesc.Name[0]) { + annotationGroup = group; + break; + } else if (!annotationTechnique && groupDesc.Techniques > 0) { + auto* tech = group->GetTechniqueByIndex(0); + if (tech && tech->IsValid()) + annotationTechnique = tech; + } + } + + if (!annotationGroup && !annotationTechnique) + return; + + auto get = [&](const char* name) -> std::string { + if (annotationGroup) + return Effect::GetGroupAnnotation(annotationGroup, name); + return Effect::GetTechniqueAnnotation(annotationTechnique, name); + }; + + auto str = [&](const char* name, std::string& out) { auto s = get(name); if (!s.empty()) out = s; }; + auto flag = [&](const char* name, bool& out) { auto s = get(name); if (!s.empty()) out = IsTruthy(s); }; + + auto& td = effect.techniqueDropdown; + str("UIDropdownName", td.name); + str("UIDropdownGroup", td.group); + str("UIDropdownGroupName", td.groupName); + flag("UIDropdownGroupOpen", td.groupOpen); + flag("UIDropdownVisible", td.visible); + flag("UIDropdownTopLevel", td.topLevel); + auto s = get("UIDropdownOrdering"); + if (!s.empty()) + td.ordering = SafeStoi(s, td.ordering); + } + + // Time-of-day interpolation + + static float GetPeriodWeight(const std::string& period, const EffectManager::CommonVariableData& cd) + { + if (period == "Dawn") return cd.timeOfDay1[static_cast(TimeOfDay1Index::Dawn)]; + if (period == "Sunrise") return cd.timeOfDay1[static_cast(TimeOfDay1Index::Sunrise)]; + if (period == "Day") return cd.timeOfDay1[static_cast(TimeOfDay1Index::Day)]; + if (period == "Sunset") return cd.timeOfDay1[static_cast(TimeOfDay1Index::Sunset)]; + if (period == "Dusk") return cd.timeOfDay2[static_cast(TimeOfDay2Index::Dusk)]; + if (period == "Night") return cd.timeOfDay2[static_cast(TimeOfDay2Index::Night)]; + if (period == "Interior") return cd.eInteriorFactor; + return 0.0f; + } + + void ApplyTimeOfDayInterpolation(Effect& effect) + { + struct PeriodVar { size_t index; float weight; }; + std::unordered_map> baseGroups; + auto& cd = EffectManager::GetSingleton().commonData; + + for (size_t i = 0; i < effect.uiVariables.size(); ++i) { + auto& uiVar = effect.uiVariables[i]; + if (uiVar.timePeriod.empty() || uiVar.isSeparator || !uiVar.effectVariable) + continue; + auto& name = uiVar.name; + auto& period = uiVar.timePeriod; + if (name.size() <= period.size() || name.compare(name.size() - period.size(), period.size(), period) != 0) + continue; + baseGroups[name.substr(0, name.size() - period.size())].push_back({ i, GetPeriodWeight(period, cd) }); + } + + for (auto& [baseName, entries] : baseGroups) { + auto baseVarIt = effect.variables.find(baseName); + if (baseVarIt == effect.variables.end()) + continue; + auto* baseVar = baseVarIt->second.get(); + if (!baseVar || !baseVar->IsValid()) + continue; + + auto& sep = effect.uiVariables[entries[0].index].separation; + if (sep == "ExteriorWeather" && cd.eInteriorFactor > 0.0f) + continue; + + float totalWeight = 0.0f; + for (auto& e : entries) + totalWeight += e.weight; + if (totalWeight <= 0.0f) + continue; + + auto& firstVar = effect.uiVariables[entries[0].index]; + + if (firstVar.type == Effect::UIVariableType::Float) { + float result = 0.0f; + for (auto& e : entries) + result += effect.uiVariables[e.index].floatValue * (e.weight / totalWeight); + baseVar->AsScalar()->SetFloat(result); + } else { + int comps = (firstVar.type == Effect::UIVariableType::Float2) ? 2 : (firstVar.type == Effect::UIVariableType::Float3) ? 3 : 4; + float result[4] = {}; + for (auto& e : entries) { + float w = e.weight / totalWeight; + for (int c = 0; c < comps; ++c) + result[c] += effect.uiVariables[e.index].vectorValue[c] * w; + } + baseVar->AsVector()->SetFloatVector(result); + } + } + } + + // File preprocessing + + void StripLineDirectives(std::string& source) + { + std::string result; + result.reserve(source.size()); + std::istringstream stream(source); + std::string line; + while (std::getline(stream, line)) { + size_t firstNonSpace = line.find_first_not_of(" \t"); + if (firstNonSpace != std::string::npos && line[firstNonSpace] == '#') { + auto directive = line.substr(firstNonSpace + 1); + size_t dirStart = directive.find_first_not_of(" \t"); + if (dirStart != std::string::npos && directive.compare(dirStart, 4, "line") == 0) { + result += "\n"; + continue; + } + } + result += line; + result += "\n"; + } + source = std::move(result); + } + + static std::string ReadAndProcessInclude(const std::filesystem::path& fullPath, + const std::filesystem::path& basePath, + const std::string& iniPath, + const std::string& iniSection, + std::vector& includeDirs, + std::unordered_set& visited, + std::vector& uiDefines, + int depth); + + std::string InlineIncludes(const std::string& source, + const std::filesystem::path& basePath, + const std::string& iniPath, + const std::string& iniSection, + std::vector& includeDirs, + std::unordered_set& visited, + std::vector& uiDefines, + int depth) + { + if (depth > 20) + return source; + + std::string result; + result.reserve(source.size()); + std::istringstream stream(source); + std::string line; + while (std::getline(stream, line)) { + size_t firstNonSpace = line.find_first_not_of(" \t"); + if (firstNonSpace != std::string::npos && line[firstNonSpace] == '#') { + auto rest = line.substr(firstNonSpace + 1); + size_t dirStart = rest.find_first_not_of(" \t"); + if (dirStart != std::string::npos && rest.compare(dirStart, 7, "include") == 0) { + size_t q1 = rest.find('"', dirStart + 7); + size_t q2 = (q1 != std::string::npos) ? rest.find('"', q1 + 1) : std::string::npos; + if (q1 != std::string::npos && q2 != std::string::npos) { + std::string includeName = rest.substr(q1 + 1, q2 - q1 - 1); + + std::string_view nameView(includeName); + while (!nameView.empty() && (nameView.front() == '/' || nameView.front() == '\\')) + nameView.remove_prefix(1); + includeName = std::string(nameView); + + std::filesystem::path inclPath; + bool found = false; + for (auto& dir : includeDirs) { + auto candidate = dir / includeName; + if (std::filesystem::exists(candidate)) { + inclPath = candidate; + found = true; + break; + } + } + if (!found) + inclPath = basePath / includeName; + + std::string canonical = inclPath.string(); + if (visited.count(canonical)) { + result += "\n"; + continue; + } + + std::string expanded = ReadAndProcessInclude(inclPath, basePath, iniPath, iniSection, includeDirs, visited, uiDefines, depth + 1); + result += expanded; + result += "\n"; + continue; + } + } + } + result += line; + result += "\n"; + } + return result; + } + + static std::string ReadAndProcessInclude(const std::filesystem::path& fullPath, + const std::filesystem::path& basePath, + const std::string& iniPath, + const std::string& iniSection, + std::vector& includeDirs, + std::unordered_set& visited, + std::vector& uiDefines, + int depth) + { + std::string canonical = fullPath.string(); + visited.insert(canonical); + + std::ifstream file(fullPath, std::ios::binary | std::ios::ate); + if (!file.is_open()) + return ""; + + auto size = file.tellg(); + if (size < 0) + return ""; + file.seekg(0, std::ios::beg); + + std::string content(static_cast(size), '\0'); + if (!file.read(content.data(), size)) + return ""; + + content = DecodeKIEFX(content); + ConvertExtenderSyntax(content, basePath, uiDefines, iniPath, iniSection); + Util::ShaderPatches::Apply(fullPath.filename().string().c_str(), content); + + auto parentDir = fullPath.parent_path(); + if (std::find(includeDirs.begin(), includeDirs.end(), parentDir) == includeDirs.end()) + includeDirs.push_back(parentDir); + + auto expanded = InlineIncludes(content, basePath, iniPath, iniSection, includeDirs, visited, uiDefines, depth); + visited.erase(canonical); + return expanded; + } + + PresetInclude::PresetInclude(const std::filesystem::path& a_basePath, std::vector& a_uiDefines, const std::string& a_iniPath, const std::string& a_iniSection) : + basePath(a_basePath), uiDefines(a_uiDefines), iniPath(a_iniPath), iniSection(a_iniSection) {} + + HRESULT PresetInclude::Open(D3D_INCLUDE_TYPE, LPCSTR pFileName, LPCVOID, LPCVOID* ppData, UINT* pBytes) + { + std::string_view name(pFileName); + while (!name.empty() && (name.front() == '/' || name.front() == '\\')) + name.remove_prefix(1); + + std::filesystem::path fullPath; + bool found = false; + + for (auto& dir : includeDirs) { + auto candidate = dir / name; + if (std::filesystem::exists(candidate)) { + fullPath = candidate; + found = true; + break; + } + } + + if (!found) { + fullPath = basePath / name; + } + + std::ifstream file(fullPath, std::ios::binary | std::ios::ate); + if (!file.is_open()) { + logger::debug("[ENBEXTENDER] Include file not found: '{}' (resolved: '{}')", std::string(name), fullPath.string()); + auto* buf = new char[1]; + buf[0] = '\n'; + *ppData = buf; + *pBytes = 1; + return S_OK; + } + + auto size = file.tellg(); + if (size < 0) + return E_FAIL; + file.seekg(0, std::ios::beg); + + std::string content(static_cast(size), '\0'); + if (!file.read(content.data(), size)) + return E_FAIL; + + content = DecodeKIEFX(content); + ConvertExtenderSyntax(content, basePath, uiDefines, iniPath, iniSection); + Util::ShaderPatches::Apply(pFileName, content); + + auto parentDir = fullPath.parent_path(); + if (std::find(includeDirs.begin(), includeDirs.end(), parentDir) == includeDirs.end()) + includeDirs.push_back(parentDir); + + auto* buf = new char[content.size()]; + memcpy(buf, content.data(), content.size()); + *ppData = buf; + *pBytes = static_cast(content.size()); + return S_OK; + } + + HRESULT PresetInclude::Close(LPCVOID pData) + { + delete[] static_cast(pData); + return S_OK; + } + +} diff --git a/src/Features/Effect11/ENBExtender.h b/src/Features/Effect11/ENBExtender.h new file mode 100644 index 0000000000..abeca35d9f --- /dev/null +++ b/src/Features/Effect11/ENBExtender.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "Effects/Effect.h" + +namespace ENBExtender +{ + // Shared helpers + int SafeStoi(const std::string& s, int fallback = 0); + float SafeStof(const std::string& s, float fallback = 0.0f); + + // KIEFX encoding + bool IsKIEFX(const std::string& content); + std::string DecodeKIEFX(const std::string& content); + + // Source preprocessing + void ConvertExtenderSyntax(std::string& content, const std::filesystem::path& enbseriesPath, std::vector& uiDefines, const std::string& iniPath = "", const std::string& iniSection = ""); + void ExpandStringificationMacros(std::string& source); + + // File preprocessing + void StripLineDirectives(std::string& source); + std::string InlineIncludes(const std::string& source, + const std::filesystem::path& basePath, + const std::string& iniPath, + const std::string& iniSection, + std::vector& includeDirs, + std::unordered_set& visited, + std::vector& uiDefines, + int depth = 0); + + class PresetInclude : public ID3DInclude + { + public: + PresetInclude(const std::filesystem::path& a_basePath, std::vector& a_uiDefines, const std::string& a_iniPath = "", const std::string& a_iniSection = ""); + HRESULT __stdcall Open(D3D_INCLUDE_TYPE, LPCSTR pFileName, LPCVOID, LPCVOID* ppData, UINT* pBytes) override; + HRESULT __stdcall Close(LPCVOID pData) override; + + private: + std::filesystem::path basePath; + std::vector& uiDefines; + std::string iniPath; + std::string iniSection; + std::vector includeDirs; + }; + + // UI variable processing + void ParseSourceGroupScopes(const std::string& preprocessedSource, Effect& effect); + bool ProcessExtenderStringVariable(ID3DX11EffectVariable* variable, const D3DX11_EFFECT_VARIABLE_DESC& varDesc, std::vector& groupStack, Effect& effect); + bool CreateUIVariable(Effect::UIVariable& out, ID3DX11EffectVariable* variable, const D3DX11_EFFECT_VARIABLE_DESC& varDesc, const D3DX11_EFFECT_TYPE_DESC& typeDesc, const std::vector& groupStack, Effect& effect); + void InsertUIDefines(Effect& effect); + + // Technique evaluation + bool IsTechniqueEnabled(const Effect::TechniqueInfo& info, const Effect& effect); + + // Post-load processing + void LoadTechniqueDropdownMetadata(Effect& effect); + void ApplyTimeOfDayInterpolation(Effect& effect); + + // UI rendering + void RenderUI(std::span effects); + void RenderUI(Effect& effect); +} diff --git a/src/Features/Effect11/ENBHelper.cpp b/src/Features/Effect11/ENBHelper.cpp new file mode 100644 index 0000000000..7dd4384c28 --- /dev/null +++ b/src/Features/Effect11/ENBHelper.cpp @@ -0,0 +1,225 @@ +#include "ENBHelper.h" + +// ENB Helper functionality +// Based on ENBHelperSE by aers (https://github.com/xSyphel/ENBHelperSE) +// Original code licensed under MIT License - Copyright (c) 2021 aers + +namespace ENBHelper +{ + namespace + { + WeatherInfo cachedWeather; + LocationInfo cachedLocation; + TimeInfo cachedTime; + CameraInfo cachedCamera; + + int32_t GetClassification(RE::TESWeather* weather) + { + if (!weather) + return -1; + + using Flags = RE::TESWeather::WeatherDataFlag; + const auto flags = weather->data.flags; + + if (flags.any(Flags::kPleasant)) + return 0; + if (flags.any(Flags::kCloudy)) + return 1; + if (flags.any(Flags::kRainy)) + return 2; + if (flags.any(Flags::kSnow)) + return 3; + + return -1; + } + } + + void Update() + { + // Update weather info + if (const auto* sky = RE::Sky::GetSingleton()) { + if (sky->currentWeather) { + cachedWeather.currentWeatherFormID = sky->currentWeather->formID; + cachedWeather.currentClassification = GetClassification(sky->currentWeather); + } else { + cachedWeather.currentWeatherFormID = 0; + cachedWeather.currentClassification = -1; + } + + if (sky->lastWeather) { + cachedWeather.outgoingWeatherFormID = sky->lastWeather->formID; + cachedWeather.outgoingClassification = GetClassification(sky->lastWeather); + } else { + cachedWeather.outgoingWeatherFormID = 0; + cachedWeather.outgoingClassification = -1; + } + + cachedWeather.weatherTransition = sky->currentWeatherPct; + cachedTime.skyMode = sky->mode.underlying(); + } else { + cachedWeather.currentWeatherFormID = 0; + cachedWeather.currentClassification = -1; + cachedWeather.outgoingWeatherFormID = 0; + cachedWeather.outgoingClassification = -1; + cachedWeather.weatherTransition = 0.0f; + cachedTime.skyMode = 0; + } + + // Update time info + if (const auto* calendar = RE::Calendar::GetSingleton()) { + cachedTime.gameHour = calendar->GetHour(); + cachedTime.dayOfYear = calendar->GetDay() + (calendar->GetMonth() - 1) * 30.0f; // Approximate (GetDayOfYear() is not available) + } else { + cachedTime.gameHour = 12.0f; + cachedTime.dayOfYear = 0.0f; + } + + // Update location info + if (const auto* player = RE::PlayerCharacter::GetSingleton()) { + if (const auto* location = player->GetCurrentLocation()) { + cachedLocation.locationFormID = location->formID; + } else { + cachedLocation.locationFormID = 0; + } + + if (const auto* parentCell = player->GetParentCell()) { + cachedLocation.isInterior = parentCell->IsInteriorCell(); + if (!cachedLocation.isInterior) { + if (const auto* worldSpace = player->GetWorldspace()) { + cachedLocation.worldSpaceFormID = worldSpace->formID; + } else { + cachedLocation.worldSpaceFormID = 0; + } + } else { + cachedLocation.worldSpaceFormID = 0; + } + } else { + cachedLocation.isInterior = false; + cachedLocation.worldSpaceFormID = 0; + } + } else { + cachedLocation.locationFormID = 0; + cachedLocation.isInterior = false; + cachedLocation.worldSpaceFormID = 0; + } + + // Update camera info + if (const auto* playerCamera = RE::PlayerCamera::GetSingleton()) { + if (const auto* cameraNode = playerCamera->cameraRoot.get()) { + if (cameraNode->world.scale != 0.0f) { + cachedCamera.position = cameraNode->world.translate; + cachedCamera.rotation = cameraNode->world.rotate; + } + } + } + } + + const WeatherInfo& GetWeatherInfo() + { + return cachedWeather; + } + + const LocationInfo& GetLocationInfo() + { + return cachedLocation; + } + + const TimeInfo& GetTimeInfo() + { + return cachedTime; + } + + const CameraInfo& GetCameraInfo() + { + return cachedCamera; + } + + bool GetCurrentWeather(uint32_t& formID) + { + if (const auto* sky = RE::Sky::GetSingleton(); sky && sky->currentWeather) { + formID = sky->currentWeather->formID; + return true; + } + return false; + } + + bool GetOutgoingWeather(uint32_t& formID) + { + if (const auto* sky = RE::Sky::GetSingleton(); sky && sky->lastWeather) { + formID = sky->lastWeather->formID; + return true; + } + return false; + } + + bool GetWeatherTransition(float& transition) + { + if (const auto* sky = RE::Sky::GetSingleton()) { + transition = sky->currentWeatherPct; + return true; + } + return false; + } + + bool GetCurrentWeatherClassification(int32_t& classification) + { + if (const auto* sky = RE::Sky::GetSingleton(); sky && sky->currentWeather) { + classification = GetClassification(sky->currentWeather); + return true; + } + return false; + } + + bool GetOutgoingWeatherClassification(int32_t& classification) + { + if (const auto* sky = RE::Sky::GetSingleton(); sky && sky->lastWeather) { + classification = GetClassification(sky->lastWeather); + return true; + } + return false; + } + + bool GetCurrentLocationID(uint32_t& formID) + { + if (const auto* player = RE::PlayerCharacter::GetSingleton()) { + if (const auto* location = player->GetCurrentLocation()) { + formID = location->formID; + return true; + } + } + return false; + } + + bool GetWorldSpaceID(uint32_t& formID) + { + if (const auto* player = RE::PlayerCharacter::GetSingleton()) { + if (const auto* parentCell = player->GetParentCell(); parentCell && parentCell->IsInteriorCell()) { + formID = 0; + return true; + } + if (const auto* worldSpace = player->GetWorldspace()) { + formID = worldSpace->formID; + return true; + } + } + return false; + } + + bool GetTime(float& hour) + { + if (const auto* calendar = RE::Calendar::GetSingleton()) { + hour = calendar->GetHour(); + return true; + } + return false; + } + + bool GetSkyMode(uint32_t& mode) + { + if (const auto* sky = RE::Sky::GetSingleton()) { + mode = sky->mode.underlying(); + return true; + } + return false; + } +} diff --git a/src/Features/Effect11/ENBHelper.h b/src/Features/Effect11/ENBHelper.h new file mode 100644 index 0000000000..7f914bf36d --- /dev/null +++ b/src/Features/Effect11/ENBHelper.h @@ -0,0 +1,59 @@ +#pragma once + +// ENB Helper functionality +// Based on ENBHelperSE by aers (https://github.com/xSyphel/ENBHelperSE) +// Original code licensed under MIT License - Copyright (c) 2021 aers + +#include + +namespace ENBHelper +{ + struct WeatherInfo + { + uint32_t currentWeatherFormID = 0; + uint32_t outgoingWeatherFormID = 0; + float weatherTransition = 0.0f; + int32_t currentClassification = -1; // 0=Pleasant, 1=Cloudy, 2=Rainy, 3=Snow, -1=Unknown + int32_t outgoingClassification = -1; + }; + + struct LocationInfo + { + uint32_t locationFormID = 0; + uint32_t worldSpaceFormID = 0; + bool isInterior = false; + }; + + struct TimeInfo + { + float gameHour = 12.0f; + float dayOfYear = 0.0f; + uint32_t skyMode = 0; + }; + + struct CameraInfo + { + RE::NiPoint3 position = { 0.0f, 0.0f, 0.0f }; + RE::NiMatrix3 rotation; + }; + + // Update all cached values - call once per frame + void Update(); + + // Accessors for cached data + const WeatherInfo& GetWeatherInfo(); + const LocationInfo& GetLocationInfo(); + const TimeInfo& GetTimeInfo(); + const CameraInfo& GetCameraInfo(); + + // Individual getters (for compatibility with ENBHelperSE API) + bool GetCurrentWeather(uint32_t& formID); + bool GetOutgoingWeather(uint32_t& formID); + bool GetWeatherTransition(float& transition); + bool GetCurrentWeatherClassification(int32_t& classification); + bool GetOutgoingWeatherClassification(int32_t& classification); + bool GetCurrentLocationID(uint32_t& formID); + bool GetWorldSpaceID(uint32_t& formID); + bool GetTime(float& hour); + bool GetSkyMode(uint32_t& mode); +} diff --git a/src/Features/Effect11/EffectManager.cpp b/src/Features/Effect11/EffectManager.cpp new file mode 100644 index 0000000000..1b8a1e6e65 --- /dev/null +++ b/src/Features/Effect11/EffectManager.cpp @@ -0,0 +1,1001 @@ +#include "EffectManager.h" + +#include "D3D11StateBackup.h" +#include "ENBExtender.h" +#include "Features/Effect11.h" +#include "State.h" + +#include "SettingManager.h" +#include "TextureManager.h" +#include "WeatherManager.h" + +#include +#include +#include +#include + +namespace +{ + using namespace Effect11Util; +} + +EffectManager& EffectManager::GetSingleton() +{ + static EffectManager instance; + return instance; +} + +uint32_t EffectManager::GetFailedEffectCount() const +{ + uint32_t count = 0; + const Effect* allEffects[] = { &enbBloom, &enbLens, &enbAdaptation, &enbEffect, &enbEffectPostPass }; + for (const auto* effect : allEffects) + if (effect->IsFilePresent() && !effect->GetErrors().empty()) + count++; + return count; +} + +std::vector EffectManager::GetAllErrors() const +{ + std::vector result; + const Effect* allEffects[] = { &enbBloom, &enbLens, &enbAdaptation, &enbEffect, &enbEffectPostPass }; + for (const auto* effect : allEffects) + if (effect->IsFilePresent() && !effect->GetErrors().empty()) + for (const auto& err : effect->GetErrors()) + result.push_back(fmt::format("{}: {}", effect->GetName(), err)); + return result; +} + +void EffectManager::Initialize() +{ + TextureManager::GetSingleton().Initialize(); + RegisterSettings(); + SettingManager::GetSingleton().Load(); + CreateCommonResources(); + Apply(); + + // Verify all critical common resources are initialized correctly + struct ResourceCheck + { + const void* resource; + const char* name; + }; + + const ResourceCheck checks[] = { + { quadVertexBuffer.get(), "quadVertexBuffer" }, + { inputLayout.get(), "inputLayout" }, + { rasterizerState.get(), "rasterizerState" }, + { blendState.get(), "blendState" }, + { copyVertexShader.get(), "copyVertexShader" }, + { copyPixelShader.get(), "copyPixelShader" }, + { colorCorrectionComputeShader.get(), "colorCorrectionComputeShader" }, + { colorCorrectionConstantBuffer.get(), "colorCorrectionConstantBuffer" }, + { ditherConstantBuffer.get(), "ditherConstantBuffer" }, + }; + + bool resourcesValid = true; + for (const auto& [resource, name] : checks) { + if (!resource) { + logger::error("[EffectManager] {} failed to initialize", name); + resourcesValid = false; + } + } + + if (!resourcesValid) { + logger::error("[EffectManager] Initialization failed due to missing resources"); + initialized = false; + } else { + initialized = true; + } + +} + +void EffectManager::Apply() +{ + globals::features::effect11.LoadRaindropTexture(); + + enbBloom.Apply(); + enbLens.Apply(); + enbAdaptation.Apply(); + enbEffect.Apply(); + enbEffectPostPass.Apply(); + + Effect* allEffects[] = { &enbBloom, &enbLens, &enbAdaptation, &enbEffect, &enbEffectPostPass }; + std::vector kiefxEffects; + std::string concatenatedSource; + for (auto* effect : allEffects) { + if (effect->isKIEFX && effect->IsCompiled()) { + kiefxEffects.push_back(effect); + concatenatedSource += effect->preprocessedSource; + concatenatedSource += "\n"; + } + } + + if (!kiefxEffects.empty() && !concatenatedSource.empty()) { + auto* primary = kiefxEffects[0]; + ENBExtender::ParseSourceGroupScopes(concatenatedSource, *primary); + + for (size_t i = 1; i < kiefxEffects.size(); ++i) { + kiefxEffects[i]->sourceGroupMap = primary->sourceGroupMap; + kiefxEffects[i]->sourceOrderMap = primary->sourceOrderMap; + kiefxEffects[i]->groupMeta = primary->groupMeta; + } + + for (auto* effect : kiefxEffects) { + effect->LoadUIVariables(); + effect->Load(); + effect->preprocessedSource.clear(); + } + } +} + +void EffectManager::Load() +{ + Effect* allEffects[] = { &enbBloom, &enbLens, &enbAdaptation, &enbEffect, &enbEffectPostPass }; + for (auto* effect : allEffects) { + effect->lastIniWriteTime = {}; + effect->Load(); + effect->UpdateUIVariables(); + } +} + +void EffectManager::Save() +{ + enbBloom.Save(); + enbLens.Save(); + enbAdaptation.Save(); + enbEffect.Save(); + enbEffectPostPass.Save(); +} + +void EffectManager::RegisterSettings() +{ + auto& settingManager = SettingManager::GetSingleton(); + + settingManager.RegisterBoolSetting("UseEffect", "GLOBAL", false, false); + + settingManager.RegisterBoolSetting("UseOriginalPostProcessing", "EFFECT", false, false); + settingManager.RegisterBoolSetting("EnableAdaptation", "EFFECT", false, false); + settingManager.RegisterBoolSetting("EnableBloom", "EFFECT", false, false); + settingManager.RegisterBoolSetting("EnableLens", "EFFECT", false, false); + settingManager.RegisterBoolSetting("EnablePostPassShader", "EFFECT", false, false); + settingManager.RegisterBoolSetting("EnableProceduralSun", "EFFECT", false, false); + settingManager.RegisterBoolSetting("EnableCloudShadows", "EFFECT", false, false); + settingManager.RegisterBoolSetting("EnableCloudsScattering", "EFFECT", false, false); + settingManager.RegisterBoolSetting("EnableImageBasedLighting", "EFFECT", false, false); + settingManager.RegisterBoolSetting("EnableVolumetricRays", "EFFECT", false, false); + settingManager.RegisterBoolSetting("EnableSkylighting", "EFFECT", false, false); + + settingManager.RegisterFloatSetting("Brightness", "COLORCORRECTION", 1.0f, 0.0f, 10000.0f, 0.01f, false); + settingManager.RegisterFloatSetting("GammaCurve", "COLORCORRECTION", 1.0f, 1.0f, 2.2f, 0.01f, false); + + settingManager.RegisterBoolSetting("EnableMultipleWeathers", "WEATHER", false, false); + settingManager.RegisterBoolSetting("EnableLocationWeather", "WEATHER", false, false); + + settingManager.RegisterFloatSetting("DawnDuration", "TIMEOFDAY", 2.0f, 0.1f, 6.0f, 0.01f, false); + settingManager.RegisterFloatSetting("SunriseTime", "TIMEOFDAY", 7.0f, 2.0f, 12.0f, 0.01f, false); + settingManager.RegisterFloatSetting("DayTime", "TIMEOFDAY", 13.0f, 0.0f, 24.0f, 0.01f, false); + settingManager.RegisterFloatSetting("SunsetTime", "TIMEOFDAY", 19.0f, 0.0f, 23.0f, 0.01f, false); + settingManager.RegisterFloatSetting("DuskDuration", "TIMEOFDAY", 2.0f, 0.1f, 6.0f, 0.01f, false); + settingManager.RegisterFloatSetting("NightTime", "TIMEOFDAY", 1.0f, 0.0f, 24.0f, 0.01f, false); + + settingManager.RegisterFloatSetting("AdaptationSensitivity", "ADAPTATION", 0.5f, 0.0f, 1.0f, 0.01f, false); + settingManager.RegisterFloatSetting("AdaptationTime", "ADAPTATION", 1.0f, 0.05f, 100.0f, 0.01f, false); + settingManager.RegisterBoolSetting("ForceMinMaxValues", "ADAPTATION", false, false); + settingManager.RegisterFloatSetting("AdaptationMin", "ADAPTATION", 0.1f, 0.0f, 65536.0f, 0.01f, false); + settingManager.RegisterFloatSetting("AdaptationMax", "ADAPTATION", 10.0f, 0.0f, 65536.0f, 0.01f, false); + + settingManager.RegisterTimeOfDaySetting("Amount", "BLOOM", 0.1f, 0.0f, 10.0f, 0.01f, true); + + settingManager.RegisterTimeOfDaySetting("Amount", "LENS", 1.0f, 0.0f, 10.0f, 0.01f, true); + + settingManager.RegisterTimeOfDaySetting("DirectLightingIntensity", "ENVIRONMENT", 1.0f, 0.0f, 30000.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("DirectLightingCurve", "ENVIRONMENT", 1.0f, 0.1f, 8.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("DirectLightingDesaturation", "ENVIRONMENT", 0.0f, -1.0f, 1.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("AmbientLightingIntensity", "ENVIRONMENT", 1.0f, 0.0f, 30000.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("AmbientLightingDesaturation", "ENVIRONMENT", 0.0f, -1.0f, 1.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("PointLightingIntensity", "ENVIRONMENT", 1.0f, 0.0f, 30000.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("PointLightingCurve", "ENVIRONMENT", 1.0f, 0.1f, 4.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("PointLightingDesaturation", "ENVIRONMENT", 0.0f, -1.0f, 1.0f, 0.01f, true); + settingManager.RegisterColorTimeOfDaySetting("DirectLightingColorFilter", "ENVIRONMENT", { 1.0f, 1.0f, 1.0f }, true); + settingManager.RegisterTimeOfDaySetting("DirectLightingColorFilterAmount", "ENVIRONMENT", 0.0f, 0.0f, 1.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("FogColorMultiplier", "ENVIRONMENT", 1.0f, 0.0f, 30000.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("FogColorCurve", "ENVIRONMENT", 1.0f, 0.0f, 8.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("FogAmountMultiplier", "ENVIRONMENT", 1.0f, 0.0f, 10.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("FogCurveMultiplier", "ENVIRONMENT", 1.0f, 0.0f, 10.0f, 0.01f, true); + settingManager.RegisterColorTimeOfDaySetting("FogColorFilter", "ENVIRONMENT", { 1.0f, 1.0f, 1.0f }, true); + settingManager.RegisterTimeOfDaySetting("FogColorFilterAmount", "ENVIRONMENT", 0.0f, 0.0f, 1.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("ColorPow", "ENVIRONMENT", 1.0f, 1.0f, 2.2f, 0.01f, true); + + settingManager.RegisterBoolSetting("Enable", "SKY", true, false); + settingManager.RegisterBoolSetting("DisableWrongSkyMath", "SKY", false, false); + settingManager.RegisterTimeOfDaySetting("GradientIntensity", "SKY", 1.0f, 0.0f, 30000.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("GradientDesaturation", "SKY", 0.0f, -1.0f, 1.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("GradientTopIntensity", "SKY", 1.0f, 0.0f, 30000.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("GradientTopCurve", "SKY", 1.0f, 0.1f, 8.0f, 0.01f, true); + settingManager.RegisterColorTimeOfDaySetting("GradientTopColorFilter", "SKY", { 1.0f, 1.0f, 1.0f }, true); + settingManager.RegisterTimeOfDaySetting("GradientMiddleIntensity", "SKY", 1.0f, 0.0f, 30000.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("GradientMiddleCurve", "SKY", 1.0f, 0.1f, 8.0f, 0.01f, true); + settingManager.RegisterColorTimeOfDaySetting("GradientMiddleColorFilter", "SKY", { 1.0f, 1.0f, 1.0f }, true); + settingManager.RegisterTimeOfDaySetting("GradientHorizonIntensity", "SKY", 1.0f, 0.0f, 30000.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("GradientHorizonCurve", "SKY", 1.0f, 0.1f, 8.0f, 0.01f, true); + settingManager.RegisterColorTimeOfDaySetting("GradientHorizonColorFilter", "SKY", { 1.0f, 1.0f, 1.0f }, true); + settingManager.RegisterTimeOfDaySetting("CloudsIntensity", "SKY", 1.0f, 0.0f, 30000.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("CloudsCurve", "SKY", 1.0f, 0.1f, 8.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("CloudsDesaturation", "SKY", 0.0f, -1.0f, 1.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("CloudsOpacity", "SKY", 1.0f, 0.0f, 5.0f, 0.01f, true); + settingManager.RegisterColorTimeOfDaySetting("CloudsColorFilter", "SKY", { 1.0f, 1.0f, 1.0f }, true); + settingManager.RegisterTimeOfDaySetting("SunIntensity", "SKY", 1.0f, 0.0f, 30000.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("SunDesaturation", "SKY", 0.0f, -1.0f, 1.0f, 0.01f, true); + settingManager.RegisterColorTimeOfDaySetting("SunColorFilter", "SKY", { 1.0f, 1.0f, 1.0f }, true); + settingManager.RegisterTimeOfDaySetting("MoonIntensity", "SKY", 1.0f, 0.0f, 30000.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("MoonDesaturation", "SKY", 0.0f, -1.0f, 1.0f, 0.01f, true); + settingManager.RegisterColorTimeOfDaySetting("MoonColorFilter", "SKY", { 1.0f, 1.0f, 1.0f }, true); + settingManager.RegisterTimeOfDaySetting("StarsIntensity", "SKY", 1.0f, 0.0f, 30000.0f, 0.01f, true); + settingManager.RegisterFloatSetting("CloudsEdgeIntensity", "SKY", 2.0f, 0.0f, 10.0f, 0.01f, false); + settingManager.RegisterFloatSetting("CloudsEdgeMoonMultiplier", "SKY", 0.0f, 0.0f, 10.0f, 0.01f, false); + settingManager.RegisterBoolSetting("UseProceduralGradientWeights", "SKY", false, false); + settingManager.RegisterTimeOfDaySetting("ProceduralGradientWeightCurve", "SKY", 4.0f, 1.0f, 32.0f, 0.01f, true); + + settingManager.RegisterBoolSetting("EnableCloudsLightingFromMoon", "SKYSCATTERING", true, false); + settingManager.RegisterBoolSetting("ScatteringColorHDRWeighting", "SKYSCATTERING", false, false); + settingManager.RegisterTimeOfDaySetting("AtmosphereThickness", "SKYSCATTERING", 1.0f, 0.0f, 10.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("HorizonRange", "SKYSCATTERING", 0.5f, 0.0f, 1.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("Intensity", "SKYSCATTERING", 1.0f, 0.0f, 30000.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("Amount", "SKYSCATTERING", 0.0f, 0.0f, 1.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("DustVolume", "SKYSCATTERING", 0.2f, 0.0f, 1.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("DustDensity", "SKYSCATTERING", 0.2f, 0.0f, 1.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("DustDarkening", "SKYSCATTERING", 0.0f, 0.0f, 1.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("ShadowAmount", "SKYSCATTERING", 0.3f, 0.0f, 1.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("ColorFromSun", "SKYSCATTERING", 0.0f, 0.0f, 1.0f, 0.01f, true); + settingManager.RegisterColorTimeOfDaySetting("ScatteringColor", "SKYSCATTERING", { 1.0f, 1.0f, 1.0f }, true); + settingManager.RegisterTimeOfDaySetting("AirGlowIntensity", "SKYSCATTERING", 0.0f, 0.0f, 10.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("AirGlowRange", "SKYSCATTERING", 0.2f, 0.0f, 1.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("SunGlowIntensity", "SKYSCATTERING", 0.0f, 0.0f, 100.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("SunGlowRange", "SKYSCATTERING", 0.2f, 0.0f, 1.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("MoonGlowAmount", "SKYSCATTERING", 0.0f, 0.0f, 1.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("MoonGlowRange", "SKYSCATTERING", 0.2f, 0.0f, 1.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("CloudsLightingSunMinIntensity", "SKYSCATTERING", 0.1f, 0.0f, 10.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("CloudsLightingSunMultiplier", "SKYSCATTERING", 0.0f, 0.0f, 100.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("CloudsLightingMoonIntensity", "SKYSCATTERING", 0.0f, 0.0f, 10.0f, 0.01f, true); + + settingManager.RegisterFloatSetting("Size", "PROCEDURALSUN", 1.0f, 0.0f, 12.0f, 0.01f, false); + settingManager.RegisterFloatSetting("EdgeSoftness", "PROCEDURALSUN", 0.4f, 0.0f, 1.0f, 0.01f, false); + settingManager.RegisterTimeOfDaySetting("GlowIntensity", "PROCEDURALSUN", 0.4f, 0.0f, 30000.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("GlowCurve", "PROCEDURALSUN", 10.0f, 0.0f, 100.0f, 0.01f, true); + + settingManager.RegisterTimeOfDaySetting("Intensity", "VOLUMETRICFOG", 1.0f, 0.0f, 30000.0f, 0.01f, true); + settingManager.RegisterColorTimeOfDaySetting("ColorFilter", "VOLUMETRICFOG", { 1.0f, 1.0f, 1.0f }, true); + + settingManager.RegisterTimeOfDaySetting("MultiplicativeAmount", "IMAGEBASEDLIGHTING", 0.0f, 0.0f, 10.0f, 0.01f, true); + + settingManager.RegisterTimeOfDaySetting("GlowIntensity", "SUNGLARE", 1.0f, 0.0f, 1000.0f, 0.01f, true); + + settingManager.RegisterTimeOfDaySetting("Intensity", "PARTICLE", 1.0f, 0.0f, 30000.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("LightingInfluence", "PARTICLE", 0.5f, 0.0f, 10.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("AmbientInfluence", "PARTICLE", 0.5f, 0.0f, 10.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("PointLightingInfluence", "PARTICLE", 1.0f, 0.0f, 10.0f, 0.01f, true); + + settingManager.RegisterTimeOfDaySetting("Intensity", "LIGHTSPRITE", 1.0f, 0.0f, 30000.0f, 0.01f, true); + + settingManager.RegisterTimeOfDaySetting("Intensity", "FIRE", 6.0f, 0.0f, 30000.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("Curve", "FIRE", 1.0f, 0.1f, 8.0f, 0.01f, true); + + settingManager.RegisterTimeOfDaySetting("AuroraBorealisIntensity", "SKY", 1.0f, 0.0f, 30000.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("AuroraBorealisCurve", "SKY", 1.0f, 0.1f, 8.0f, 0.01f, true); + + settingManager.RegisterBoolSetting("Enable", "RAIN", true); + settingManager.RegisterTimeOfDaySetting("MotionStretch", "RAIN", 0.28f, 0.0f, 1.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("MotionTransparency", "RAIN", 0.1f, 0.0f, 1.0f, 0.01f, true); + + settingManager.RegisterTimeOfDaySetting("Amount", "CLOUDSHADOWS", 0.8f, 0.0f, 4.0f, 0.01f, true); + + settingManager.RegisterTimeOfDaySetting("AmbientMinLevel", "SKYLIGHTING", 0.0f, 0.0f, 1.0f, 0.01f, true); + + settingManager.RegisterTimeOfDaySetting("Intensity", "GAMEVOLUMETRICRAYS", 1.0f, 0.0f, 1000.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("RangeFactor", "GAMEVOLUMETRICRAYS", 1.0f, 0.0f, 100.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("Desaturation", "GAMEVOLUMETRICRAYS", 0.0f, -1.0f, 1.0f, 0.01f, true); + settingManager.RegisterColorTimeOfDaySetting("ColorFilter", "GAMEVOLUMETRICRAYS", { 1.0f, 1.0f, 1.0f }, true); + + settingManager.RegisterTimeOfDaySetting("Intensity", "VOLUMETRICRAYS", 0.2f, 0.0f, 1000.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("Density", "VOLUMETRICRAYS", 1.0f, 0.1f, 100.0f, 0.01f, true); + settingManager.RegisterTimeOfDaySetting("SkyColorAmount", "VOLUMETRICRAYS", 0.5f, 0.0f, 10.0f, 0.01f, true); + + settingManager.SetCategoryExteriorOnly("RAIN", true); + settingManager.SetCategoryExteriorOnly("SKYLIGHTING", true); + settingManager.SetCategoryExteriorOnly("CLOUDSHADOWS", true); + settingManager.SetCategoryExteriorOnly("IMAGEBASEDLIGHTING", true); + settingManager.SetCategoryExteriorOnly("VOLUMETRICRAYS", true); + settingManager.SetCategoryExteriorOnly("SKYSCATTERING", true); + settingManager.SetCategoryExteriorOnly("VOLUMETRICFOG", true); + settingManager.SetCategoryExteriorOnly("GAMEVOLUMETRICRAYS", true); + + // Cache IDs for performance + ids.useBloom = settingManager.GetSettingID("EnableBloom", "EFFECT"); + ids.useLens = settingManager.GetSettingID("EnableLens", "EFFECT"); + ids.useAdaptation = settingManager.GetSettingID("EnableAdaptation", "EFFECT"); + ids.usePostPass = settingManager.GetSettingID("EnablePostPassShader", "EFFECT"); + + ids.enableMultipleWeathers = settingManager.GetSettingID("EnableMultipleWeathers", "WEATHER"); + ids.enableLocationWeather = settingManager.GetSettingID("EnableLocationWeather", "WEATHER"); + + ids.nightTime = settingManager.GetSettingID("NightTime", "TIMEOFDAY"); + ids.sunriseTime = settingManager.GetSettingID("SunriseTime", "TIMEOFDAY"); + ids.dawnDuration = settingManager.GetSettingID("DawnDuration", "TIMEOFDAY"); + ids.dayTime = settingManager.GetSettingID("DayTime", "TIMEOFDAY"); + ids.sunsetTime = settingManager.GetSettingID("SunsetTime", "TIMEOFDAY"); + ids.duskDuration = settingManager.GetSettingID("DuskDuration", "TIMEOFDAY"); + + ids.brightness = settingManager.GetSettingID("Brightness", "COLORCORRECTION"); + ids.gammaCurve = settingManager.GetSettingID("GammaCurve", "COLORCORRECTION"); +} + +void EffectManager::ExecuteEffect(Effect& a_effect, uint32_t enableSettingID) +{ + if (!a_effect.IsCompiled()) + return; + + if (enableSettingID != 0xFFFFFFFF && !SettingManager::GetSingleton().GetValue(enableSettingID)) + return; + + a_effect.profiler = globals::profiler; + UpdateCommonVariablesForEffect(a_effect.GetEffect()); + a_effect.UpdateEffectVariables(); + a_effect.Execute(); + a_effect.profiler = nullptr; +} + +void EffectManager::ExecuteEffects() +{ + if (!initialized) + return; + + auto context = globals::d3d::context; + auto renderer = globals::game::renderer; + + if (!rasterizerState || !blendState || !quadVertexBuffer || !inputLayout || !renderer) + return; + + D3D11FullStateBackup stateBackup; + stateBackup.Save(context); + + auto textureOriginal = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + + // Set our render state + context->RSSetState(rasterizerState.get()); + context->OMSetBlendState(blendState.get(), nullptr, 0xFFFFFFFF); + context->OMSetDepthStencilState(nullptr, 0); + + UINT stride = sizeof(float) * 5; + UINT offset = 0; + ID3D11Buffer* vertexBuffers[] = { quadVertexBuffer.get() }; + context->IASetVertexBuffers(0, 1, vertexBuffers, &stride, &offset); + context->IASetInputLayout(inputLayout.get()); + context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + + globals::profiler->BeginPass("Effect11::ColorCorrection"); + ApplyColorCorrection(textureOriginal.UAV); + globals::profiler->EndPass(); + + auto& textureManager = TextureManager::GetSingleton(); + + textureManager.UpdateDownsampledTexture(textureOriginal.SRV); + + ExecuteEffect(enbBloom, ids.useBloom); + ExecuteEffect(enbLens, ids.useLens); + ExecuteEffect(enbAdaptation, ids.useAdaptation); + ExecuteEffect(enbEffect); + ExecuteEffect(enbEffectPostPass, ids.usePostPass); + + textureManager.IncrementTextureSwap(); + + auto* textureSDRTemp = textureManager.GetCommonTexture("TextureSDRTemp"); + if (textureSDRTemp) { + auto textureFramebuffer = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kIMAGESPACE_TEMP_COPY]; + globals::profiler->BeginPass("Effect11::CopyToFramebuffer"); + CopyTexture(textureSDRTemp->srv.get(), textureFramebuffer.RTV); + globals::profiler->EndPass(); + + auto textureFramebuffer2 = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kIMAGESPACE_TEMP_COPY2]; + globals::profiler->BeginPass("Effect11::CopyToFramebuffer2"); + CopyTexture(textureSDRTemp->srv.get(), textureFramebuffer2.RTV); + globals::profiler->EndPass(); + } + + stateBackup.Restore(context); + stateBackup.Release(); +} + +std::string EffectManager::LoadShaderFile(const char* path) +{ + std::ifstream ifs(path, std::ios::binary); + if (!ifs.is_open()) { + logger::error("[EFFECT11] Failed to open shader file: {}", path); + return {}; + } + return { std::istreambuf_iterator(ifs), std::istreambuf_iterator() }; +} + +void EffectManager::CreateCommonResources() +{ + CreateQuadGeometry(); + CreateRenderStates(); + CreateCopyShaders(); + CreateColorCorrectionShader(); +} + +void EffectManager::CreateQuadGeometry() +{ + // Create a fullscreen quad vertex buffer that all effects can share + struct QuadVertex + { + float position[3]; + float texCoord[2]; + }; + + QuadVertex vertices[] = { + { { -1.0f, -1.0f, 0.0f }, { 0.0f, 1.0f } }, // Bottom left + { { -1.0f, 1.0f, 0.0f }, { 0.0f, 0.0f } }, // Top left + { { 1.0f, -1.0f, 0.0f }, { 1.0f, 1.0f } }, // Bottom right + { { 1.0f, 1.0f, 0.0f }, { 1.0f, 0.0f } } // Top right + }; + + D3D11_BUFFER_DESC bufferDesc = {}; + bufferDesc.Usage = D3D11_USAGE_DEFAULT; + bufferDesc.ByteWidth = sizeof(vertices); + bufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; + bufferDesc.CPUAccessFlags = 0; + + D3D11_SUBRESOURCE_DATA initData = {}; + initData.pSysMem = vertices; + + DX::ThrowIfFailed(globals::d3d::device->CreateBuffer(&bufferDesc, &initData, quadVertexBuffer.put())); + + // Create input layout for ENB post-processing + D3D11_INPUT_ELEMENT_DESC inputElementDescs[] = { + { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, + { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 } + }; + + auto vertexShaderSource = LoadShaderFile("Data\\Shaders\\Effect11\\QuadVS.hlsl"); + if (vertexShaderSource.empty()) + return; + + winrt::com_ptr vertexShaderBlob; + winrt::com_ptr errorBlob; + HRESULT hr = D3DCompile(vertexShaderSource.data(), vertexShaderSource.size(), "QuadVS.hlsl", nullptr, nullptr, + "main", "vs_4_0", 0, 0, vertexShaderBlob.put(), errorBlob.put()); + + if (FAILED(hr)) { + if (errorBlob) { + logger::error("[EFFECT11] Failed to compile input layout vertex shader: {}", static_cast(errorBlob->GetBufferPointer())); + } + return; + } + + hr = globals::d3d::device->CreateInputLayout(inputElementDescs, ARRAYSIZE(inputElementDescs), + vertexShaderBlob->GetBufferPointer(), + vertexShaderBlob->GetBufferSize(), + inputLayout.put()); + if (FAILED(hr)) { + logger::error("[EFFECT11] Failed to create shared input layout for ENB effects"); + } +} + +void EffectManager::CreateRenderStates() +{ + // Rasterizer state for fullscreen quads + D3D11_RASTERIZER_DESC rastDesc = {}; + rastDesc.FillMode = D3D11_FILL_SOLID; + rastDesc.CullMode = D3D11_CULL_NONE; + rastDesc.FrontCounterClockwise = FALSE; + rastDesc.DepthBias = 0; + rastDesc.DepthBiasClamp = 0.0f; + rastDesc.SlopeScaledDepthBias = 0.0f; + rastDesc.DepthClipEnable = TRUE; + rastDesc.ScissorEnable = FALSE; + rastDesc.MultisampleEnable = FALSE; + rastDesc.AntialiasedLineEnable = FALSE; + + DX::ThrowIfFailed(globals::d3d::device->CreateRasterizerState(&rastDesc, rasterizerState.put())); + + // Blend state for standard rendering (no blending) + D3D11_BLEND_DESC blendDesc = {}; + blendDesc.AlphaToCoverageEnable = FALSE; + blendDesc.IndependentBlendEnable = FALSE; + blendDesc.RenderTarget[0].BlendEnable = FALSE; + blendDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; + + DX::ThrowIfFailed(globals::d3d::device->CreateBlendState(&blendDesc, blendState.put())); +} + +void EffectManager::CreateCopyShaders() +{ + auto vertexShaderSource = LoadShaderFile("Data\\Shaders\\Effect11\\QuadVS.hlsl"); + if (vertexShaderSource.empty()) + return; + + winrt::com_ptr vsBlob, errorBlob; + HRESULT hr = D3DCompile(vertexShaderSource.data(), vertexShaderSource.size(), "QuadVS.hlsl", nullptr, nullptr, + "main", "vs_4_0", 0, 0, vsBlob.put(), errorBlob.put()); + + if (FAILED(hr)) { + if (errorBlob) { + logger::error("[EFFECT11] Failed to compile copy vertex shader: {}", static_cast(errorBlob->GetBufferPointer())); + } + return; + } + + hr = globals::d3d::device->CreateVertexShader(vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), nullptr, copyVertexShader.put()); + if (FAILED(hr)) { + logger::error("[EFFECT11] Failed to create copy vertex shader"); + return; + } + + auto pixelShaderSource = LoadShaderFile("Data\\Shaders\\Effect11\\CopyPS.hlsl"); + if (pixelShaderSource.empty()) + return; + + winrt::com_ptr psBlob; + hr = D3DCompile(pixelShaderSource.data(), pixelShaderSource.size(), "CopyPS.hlsl", nullptr, nullptr, + "main", "ps_5_0", 0, 0, psBlob.put(), errorBlob.put()); + + if (FAILED(hr)) { + if (errorBlob) { + logger::error("[EFFECT11] Failed to compile copy pixel shader: {}", static_cast(errorBlob->GetBufferPointer())); + } + return; + } + + hr = globals::d3d::device->CreatePixelShader(psBlob->GetBufferPointer(), psBlob->GetBufferSize(), nullptr, copyPixelShader.put()); + if (FAILED(hr)) { + logger::error("[EFFECT11] Failed to create copy pixel shader"); + return; + } + + D3D11_BUFFER_DESC cbDesc{}; + cbDesc.ByteWidth = 16; + cbDesc.Usage = D3D11_USAGE_DYNAMIC; + cbDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + cbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + globals::d3d::device->CreateBuffer(&cbDesc, nullptr, ditherConstantBuffer.put()); + + logger::info("[EFFECT11] Created texture copy shaders successfully"); +} + +void EffectManager::CreateColorCorrectionShader() +{ + auto computeShaderSource = LoadShaderFile("Data\\Shaders\\Effect11\\ColorCorrectionCS.hlsl"); + if (computeShaderSource.empty()) + return; + + winrt::com_ptr csBlob, errorBlob; + HRESULT hr = D3DCompile(computeShaderSource.data(), computeShaderSource.size(), "ColorCorrectionCS.hlsl", nullptr, nullptr, + "main", "cs_5_0", 0, 0, csBlob.put(), errorBlob.put()); + + if (FAILED(hr)) { + if (errorBlob) { + logger::error("[EFFECT11] Failed to compile color correction compute shader: {}", static_cast(errorBlob->GetBufferPointer())); + } + return; + } + + hr = globals::d3d::device->CreateComputeShader(csBlob->GetBufferPointer(), csBlob->GetBufferSize(), nullptr, colorCorrectionComputeShader.put()); + if (FAILED(hr)) { + logger::error("[EFFECT11] Failed to create color correction compute shader"); + return; + } + + // Create constant buffer + D3D11_BUFFER_DESC cbDesc = {}; + cbDesc.Usage = D3D11_USAGE_DYNAMIC; + cbDesc.ByteWidth = sizeof(float) * 4; // Brightness, GammaCurve, padding[2] + cbDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + cbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + + hr = globals::d3d::device->CreateBuffer(&cbDesc, nullptr, colorCorrectionConstantBuffer.put()); + if (FAILED(hr)) { + logger::error("[EFFECT11] Failed to create color correction constant buffer"); + return; + } + + logger::info("[EFFECT11] Created color correction compute shader successfully"); +} + +void EffectManager::UpdateCommonData() +{ + commonData = {}; + + auto sky = globals::game::sky; + + // Update timer + { + auto delta = (*globals::game::deltaTime); + + static double timer = 0.0f; + timer += delta; + + auto modifiedTimer = std::fmodf(static_cast(timer) * 1000.0f, 16777216); + modifiedTimer /= 16777216.0f; + + commonData.timer[0] = modifiedTimer; + commonData.timer[1] = 60.0f; + commonData.timer[2] = static_cast(frameCount % 9999); + commonData.timer[3] = delta; + + frameCount++; + } + + // Update weather + { + // Strip plugin index (2 leftmost digits) from form IDs + auto stripPluginIndex = [](uint32_t formID) -> uint32_t { + return formID & 0x00FFFFFF; // Keep only the lower 6 hex digits + }; + + if (sky) { + auto& weatherManager = WeatherManager::GetSingleton(); + uint32_t currentID = sky->currentWeather ? stripPluginIndex(sky->currentWeather->formID) : 0; + uint32_t lastID = sky->lastWeather ? stripPluginIndex(sky->lastWeather->formID) : 0; + + commonData.weather[0] = static_cast(weatherManager.GetEffectiveWeatherID(currentID)); + commonData.weather[1] = static_cast(weatherManager.GetEffectiveWeatherID(lastID)); + commonData.weather[2] = sky->currentWeatherPct; + commonData.weather[3] = sky->currentGameHour; + } + } + + // Update time of day + { + auto& settingManager = SettingManager::GetSingleton(); + + // Clamp current time to valid range + float currentTime = sky ? std::clamp(sky->currentGameHour, 0.0f, 24.0f) : 12.0f; + + // Load time of day settings using cached IDs + const float nightTime = settingManager.GetValue(ids.nightTime); + const float sunriseTime = settingManager.GetValue(ids.sunriseTime); + const float dawnDuration = settingManager.GetValue(ids.dawnDuration); + const float dayTime = settingManager.GetValue(ids.dayTime); + const float sunsetTime = settingManager.GetValue(ids.sunsetTime); + const float duskDuration = settingManager.GetValue(ids.duskDuration); + + commonData.eInteriorFactor = Util::IsInterior(); + + // Initialize and set factors + float factors[static_cast(TimeOfDayFactorIndex::Count)] = { 0.0f }; + + if (!commonData.eInteriorFactor) { + // Calculate transition points + const float dawnStart = sunriseTime - dawnDuration; + const float dawnMid = sunriseTime - (dawnDuration * 0.5f); + const float duskMid = sunsetTime + (duskDuration * 0.5f); + const float duskEnd = sunsetTime + duskDuration; + + // Time points array with 24h wraparound + const float timePoints[] = { + nightTime, dawnStart, dawnMid, sunriseTime, dayTime, sunsetTime, duskMid, duskEnd, + nightTime + 24.0f, dawnStart + 24.0f, dawnMid + 24.0f, sunriseTime + 24.0f, + dayTime + 24.0f, sunsetTime + 24.0f, duskMid + 24.0f, duskEnd + 24.0f + }; + + // Find current and next time periods + int currentIdx = 0, nextIdx = 0; + float currentPeriodTime = 0.0f, nextPeriodTime = 24.0f; + + for (int i = 0; i < 16; i++) { + const float t = timePoints[i]; + if (currentTime >= t && t >= currentPeriodTime) { + currentIdx = i; + currentPeriodTime = t; + } + if (t > currentTime && nextPeriodTime >= t) { + nextIdx = i; + nextPeriodTime = t; + } + } + + // Map time point indices to time of day factors + constexpr int factorMapping[] = { + static_cast(TimeOfDayFactorIndex::Night), + static_cast(TimeOfDayFactorIndex::Night), + static_cast(TimeOfDayFactorIndex::Dawn), + static_cast(TimeOfDayFactorIndex::Sunrise), + static_cast(TimeOfDayFactorIndex::Day), + static_cast(TimeOfDayFactorIndex::Sunset), + static_cast(TimeOfDayFactorIndex::Dusk), + static_cast(TimeOfDayFactorIndex::Night) + }; + const int currentFactor = factorMapping[currentIdx % 8]; + const int nextFactor = factorMapping[nextIdx % 8]; + + // Calculate blend weight + float timeDiff = std::abs(nextPeriodTime - currentPeriodTime); + if (timeDiff == 0.0f) + timeDiff = 1.0f; + + const float blend = std::abs(currentTime - currentPeriodTime) / timeDiff; + + if (currentFactor == nextFactor) { + factors[currentFactor] = 1.0f; + } else { + factors[currentFactor] = std::clamp(1.0f - blend, 0.0f, 1.0f); + factors[nextFactor] = std::clamp(blend, 0.0f, 1.0f); + } + + constexpr float dayPowerCurve = 0.6f; + float powDay = std::pow(factors[static_cast(TimeOfDayFactorIndex::Day)], dayPowerCurve); + powDay = std::clamp(powDay, 0.0f, 1.0f); + + if (powDay > FLT_MIN) { + const float complement = 1.0f - powDay; + + if (factors[static_cast(TimeOfDayFactorIndex::Sunrise)] > FLT_MIN) { + factors[static_cast(TimeOfDayFactorIndex::Sunrise)] = std::clamp(complement, 0.0f, 1.0f); + } + + if (factors[static_cast(TimeOfDayFactorIndex::Sunset)] > FLT_MIN) { + factors[static_cast(TimeOfDayFactorIndex::Sunset)] = std::clamp(complement, 0.0f, 1.0f); + } + } + + factors[static_cast(TimeOfDayFactorIndex::Day)] = powDay; + + // Assign to output arrays + commonData.timeOfDay1[static_cast(TimeOfDay1Index::Dawn)] = factors[static_cast(TimeOfDayFactorIndex::Dawn)]; + commonData.timeOfDay1[static_cast(TimeOfDay1Index::Sunrise)] = factors[static_cast(TimeOfDayFactorIndex::Sunrise)]; + commonData.timeOfDay1[static_cast(TimeOfDay1Index::Day)] = factors[static_cast(TimeOfDayFactorIndex::Day)]; + commonData.timeOfDay1[static_cast(TimeOfDay1Index::Sunset)] = factors[static_cast(TimeOfDayFactorIndex::Sunset)]; + commonData.timeOfDay2[static_cast(TimeOfDay2Index::Dusk)] = factors[static_cast(TimeOfDayFactorIndex::Dusk)]; + commonData.timeOfDay2[static_cast(TimeOfDay2Index::Night)] = factors[static_cast(TimeOfDayFactorIndex::Night)]; + } + + // Calculate distance to night time (handling 24h wraparound) + float distToNight = std::abs(currentTime - nightTime); + if (distToNight > 12.0f) { + distToNight = 24.0f - distToNight; + } + + // Calculate distance to day time (handling 24h wraparound) + float distToDay = std::abs(currentTime - dayTime); + if (distToDay > 12.0f) { + distToDay = 24.0f - distToDay; + } + + // Night/day factor: 0.0 = pure night, 1.0 = pure day + // Based on relative proximity to day vs night times + if (distToNight + distToDay > 0.0f) { + commonData.eNightDayFactor = distToNight / (distToNight + distToDay); + } else { + commonData.eNightDayFactor = 0.5f; // Fallback if both distances are 0 + } + + commonData.timeOfDay2[static_cast(TimeOfDay2Index::InteriorDay)] = commonData.eInteriorFactor * commonData.eNightDayFactor; + commonData.timeOfDay2[static_cast(TimeOfDay2Index::InteriorNight)] = commonData.eInteriorFactor * (1.0f - commonData.eNightDayFactor); + } +} + +void EffectManager::UpdateCommonVariablesForEffect(ID3DX11Effect* effect) +{ + if (!effect) + return; + + auto renderer = globals::game::renderer; + + // Set common textures + Effect::SetShaderResourceVariable(effect, "TextureDepth", + renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN].depthSRV); + + // Set format-specific render targets + static const char* const formatTargets[] = { + "RenderTargetRGBA32", "RenderTargetRGBA64", "RenderTargetRGBA64F", + "RenderTargetR16F", "RenderTargetR32F", "RenderTargetRGB32F" + }; + + auto& textureManager = TextureManager::GetSingleton(); + for (const auto& targetName : formatTargets) { + auto* texture = textureManager.GetCommonTexture(targetName); + if (texture) { + Effect::SetShaderResourceVariable(effect, targetName, texture->srv.get()); + } + } + + // Set fixed-size render targets + static const char* const fixedSizeTargets[] = { + "RenderTarget1024", "RenderTarget512", "RenderTarget256", "RenderTarget128", + "RenderTarget64", "RenderTarget32", "RenderTarget16" + }; + + for (const auto& targetName : fixedSizeTargets) { + auto* texture = textureManager.GetCommonTexture(targetName); + if (texture) { + Effect::SetShaderResourceVariable(effect, targetName, texture->srv.get()); + } + } + + // Set vector variables + Effect::SetVectorVariable(effect, "Timer", commonData.timer, sizeof(commonData.timer)); + Effect::SetVectorVariable(effect, "Weather", commonData.weather, sizeof(commonData.weather)); + Effect::SetVectorVariable(effect, "TimeOfDay1", commonData.timeOfDay1, sizeof(commonData.timeOfDay1)); + Effect::SetVectorVariable(effect, "TimeOfDay2", commonData.timeOfDay2, sizeof(commonData.timeOfDay2)); + Effect::SetVectorVariable(effect, "ENightDayFactor", &commonData.eNightDayFactor, sizeof(commonData.eNightDayFactor)); + Effect::SetVectorVariable(effect, "EInteriorFactor", &commonData.eInteriorFactor, sizeof(commonData.eInteriorFactor)); +} + +void EffectManager::CopyTexture(ID3D11ShaderResourceView* a_source, ID3D11RenderTargetView* a_dest) +{ + if (!a_source || !a_dest || !copyPixelShader || !copyVertexShader) { + logger::critical("[EFFECT11] Invalid parameters or shaders not initialized for texture copy"); + return; + } + + auto context = globals::d3d::context; + + // Set viewport based on destination render target + winrt::com_ptr resource; + a_dest->GetResource(resource.put()); + winrt::com_ptr texture; + if (!resource || !resource.try_as(texture) || !texture) { + logger::error("[EFFECT11] Failed to get Texture2D from destination render target"); + return; + } + D3D11_TEXTURE2D_DESC texDesc; + texture->GetDesc(&texDesc); + + D3D11_VIEWPORT viewport = {}; + viewport.TopLeftX = 0.0f; + viewport.TopLeftY = 0.0f; + viewport.Width = static_cast(texDesc.Width); + viewport.Height = static_cast(texDesc.Height); + viewport.MinDepth = 0.0f; + viewport.MaxDepth = 1.0f; + context->RSSetViewports(1, &viewport); + + // Set up for copy operation + context->OMSetRenderTargets(1, &a_dest, nullptr); + context->OMSetDepthStencilState(nullptr, 0); + context->RSSetState(rasterizerState.get()); + context->OMSetBlendState(blendState.get(), nullptr, 0xFFFFFFFF); + + // Set IA state + UINT stride = 20; // 3 floats position + 2 floats texcoord + UINT offset = 0; + ID3D11Buffer* vbs[] = { quadVertexBuffer.get() }; + context->IASetVertexBuffers(0, 1, vbs, &stride, &offset); + context->IASetInputLayout(inputLayout.get()); + context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + + // Set shaders + context->VSSetShader(copyVertexShader.get(), nullptr, 0); + context->PSSetShader(copyPixelShader.get(), nullptr, 0); + + // Update dither frame count + if (ditherConstantBuffer) { + D3D11_MAPPED_SUBRESOURCE mapped; + if (SUCCEEDED(context->Map(ditherConstantBuffer.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped))) { + *static_cast(mapped.pData) = frameCount; + context->Unmap(ditherConstantBuffer.get(), 0); + } + ID3D11Buffer* cbs[] = { ditherConstantBuffer.get() }; + context->PSSetConstantBuffers(0, 1, cbs); + } + + // Set source texture + context->PSSetShaderResources(0, 1, &a_source); + + // Draw fullscreen quad + context->Draw(4, 0); + + // Clean up SRV binding + ID3D11ShaderResourceView* nullSRV = nullptr; + context->PSSetShaderResources(0, 1, &nullSRV); +} + +void EffectManager::ApplyColorCorrection(ID3D11UnorderedAccessView* textureUAV) +{ + if (!textureUAV || !colorCorrectionComputeShader || !colorCorrectionConstantBuffer) { + logger::warn("[EFFECT11] Invalid parameters or shaders not initialized for color correction"); + return; + } + + auto& settingManager = SettingManager::GetSingleton(); + + auto brightness = settingManager.GetValue(ids.brightness); + auto gammaCurve = settingManager.GetValue(ids.gammaCurve); + + auto context = globals::d3d::context; + + // Update constant buffer with current settings + D3D11_MAPPED_SUBRESOURCE mapped; + HRESULT hr = context->Map(colorCorrectionConstantBuffer.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped); + if (FAILED(hr)) { + logger::warn("[EFFECT11] Failed to map color correction constant buffer"); + return; + } + { + struct ColorCorrectionCB + { + float brightness; + float gammaCurve; + uint32_t frameCount; + uint32_t pad; + }; + auto* cbData = static_cast(mapped.pData); + cbData->brightness = brightness; + cbData->gammaCurve = gammaCurve; + cbData->frameCount = frameCount; + context->Unmap(colorCorrectionConstantBuffer.get(), 0); + } + + // Set compute shader and resources + context->CSSetShader(colorCorrectionComputeShader.get(), nullptr, 0); + ID3D11Buffer* bufferArray[] = { colorCorrectionConstantBuffer.get() }; + context->CSSetConstantBuffers(0, 1, bufferArray); + context->CSSetUnorderedAccessViews(0, 1, &textureUAV, nullptr); + + // Get texture dimensions for dispatch + winrt::com_ptr resource; + textureUAV->GetResource(resource.put()); + winrt::com_ptr texture; + if (!resource || !resource.try_as(texture) || !texture) { + logger::error("[EFFECT11] Failed to get Texture2D from UAV in ApplyColorCorrection"); + } else { + D3D11_TEXTURE2D_DESC texDesc; + texture->GetDesc(&texDesc); + + // Dispatch compute shader (8x8 thread groups) + UINT dispatchX = (texDesc.Width + 7) / 8; + UINT dispatchY = (texDesc.Height + 7) / 8; + context->Dispatch(dispatchX, dispatchY, 1); + } + + // Clear bindings + ID3D11UnorderedAccessView* nullUAV = nullptr; + ID3D11Buffer* nullCB = nullptr; + context->CSSetShader(nullptr, nullptr, 0); + context->CSSetConstantBuffers(0, 1, &nullCB); + context->CSSetUnorderedAccessViews(0, 1, &nullUAV, nullptr); +} + +void EffectManager::ReloadShaders() +{ + copyVertexShader = nullptr; + copyPixelShader = nullptr; + colorCorrectionComputeShader = nullptr; + CreateCopyShaders(); + CreateColorCorrectionShader(); +} + +void EffectManager::RenderEffectsList() +{ + Effect* allEffects[] = { &enbBloom, &enbLens, &enbAdaptation, &enbEffect, &enbEffectPostPass }; + + std::vector mergedEffects; + std::vector standaloneEffects; + for (auto* effect : allEffects) { + if (effect->isKIEFX) + mergedEffects.push_back(effect); + else + standaloneEffects.push_back(effect); + } + + if (!mergedEffects.empty()) + ENBExtender::RenderUI(mergedEffects); + + for (auto* effect : standaloneEffects) { + if (!effect->IsFilePresent()) + continue; + + if (effect->IsCompiled()) { + ImGui::Separator(); + if (ImGui::TreeNodeEx(effect->GetName().c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { + effect->RenderImGui(); + ImGui::TreePop(); + } + } else if (!effect->GetErrors().empty()) { + ImGui::TextColored(ImVec4(1, 0.3f, 0.3f, 1), "%s:", effect->GetName().c_str()); + for (const auto& err : effect->GetErrors()) + ImGui::TextWrapped("%s", err.c_str()); + } + } +} \ No newline at end of file diff --git a/src/Features/Effect11/EffectManager.h b/src/Features/Effect11/EffectManager.h new file mode 100644 index 0000000000..8d10e60426 --- /dev/null +++ b/src/Features/Effect11/EffectManager.h @@ -0,0 +1,148 @@ +#pragma once + +#include "Effects/ENBAdaptation.h" +#include "Effects/ENBBloom.h" +#include "Effects/ENBEffect.h" +#include "Effects/ENBEffectPostPass.h" +#include "Effects/ENBLens.h" +#include "Profiler.h" + +enum class TimeOfDay1Index : int +{ + Dawn, + Sunrise, + Day, + Sunset +}; + +enum class TimeOfDay2Index : int +{ + Dusk, + Night, + InteriorDay, + InteriorNight +}; + +enum class TimeOfDayFactorIndex : int +{ + Dawn, + Sunrise, + Day, + Sunset, + Dusk, + Night, + Count +}; + +class EffectManager +{ +public: + static EffectManager& GetSingleton(); + + // Effect execution + void ExecuteEffects(); + + // Lifecycle + void Initialize(); + + void Apply(); + void Load(); + void Save(); + + void RegisterSettings(); + + // Common variable management + void UpdateCommonVariablesForEffect(ID3DX11Effect* effect); + +public: + ENBBloom enbBloom; + ENBLens enbLens; + ENBAdaptation enbAdaptation; + ENBEffect enbEffect; + ENBEffectPostPass enbEffectPostPass; + + // Common resources shared across effects + void CreateCommonResources(); + + // Shared D3D resources + winrt::com_ptr quadVertexBuffer; + winrt::com_ptr inputLayout; + winrt::com_ptr rasterizerState; + winrt::com_ptr blendState; + + // Copy shader resources + winrt::com_ptr copyVertexShader; + winrt::com_ptr copyPixelShader; + winrt::com_ptr ditherConstantBuffer; + + // Color correction compute shader resources + winrt::com_ptr colorCorrectionComputeShader; + winrt::com_ptr colorCorrectionConstantBuffer; + + static std::string LoadShaderFile(const char* path); + void CreateQuadGeometry(); + void CreateRenderStates(); + void CreateCopyShaders(); + void CreateColorCorrectionShader(); + + void RenderEffectsList(); + + // Common variable data (updated once, applied to all effects) + struct CommonVariableData + { + float timer[4]; + float weather[4]; + float timeOfDay1[4]; + float timeOfDay2[4]; + float eNightDayFactor; + float eInteriorFactor; + } commonData; + uint32_t frameCount = 0; + + void UpdateCommonData(); + + struct SettingIDs + { + uint32_t useBloom = 0xFFFFFFFF; + uint32_t useLens = 0xFFFFFFFF; + uint32_t useAdaptation = 0xFFFFFFFF; + uint32_t usePostPass = 0xFFFFFFFF; + + uint32_t enableMultipleWeathers = 0xFFFFFFFF; + uint32_t enableLocationWeather = 0xFFFFFFFF; + + uint32_t nightTime = 0xFFFFFFFF; + uint32_t sunriseTime = 0xFFFFFFFF; + uint32_t dawnDuration = 0xFFFFFFFF; + uint32_t dayTime = 0xFFFFFFFF; + uint32_t sunsetTime = 0xFFFFFFFF; + uint32_t duskDuration = 0xFFFFFFFF; + + uint32_t brightness = 0xFFFFFFFF; + uint32_t gammaCurve = 0xFFFFFFFF; + } ids; + + const CommonVariableData& GetCommonData() const { return commonData; } + + bool IsInitialized() const { return initialized; } + + bool performanceMode = false; + + // Execute a single effect with perf events and common variable setup + void ExecuteEffect(Effect& effect, uint32_t enableSettingID = 0xFFFFFFFF); + + // Texture copy using pixel shader + void CopyTexture(ID3D11ShaderResourceView* source, ID3D11RenderTargetView* destination); + + // Color correction using compute shader + void ApplyColorCorrection(ID3D11UnorderedAccessView* textureUAV); + + void ReloadShaders(); + + // Error reporting for overlay display + uint32_t GetFailedEffectCount() const; + std::vector GetAllErrors() const; + +private: + bool initialized = false; +}; \ No newline at end of file diff --git a/src/Features/Effect11/Effects/ENBAdaptation.cpp b/src/Features/Effect11/Effects/ENBAdaptation.cpp new file mode 100644 index 0000000000..0252a52cb5 --- /dev/null +++ b/src/Features/Effect11/Effects/ENBAdaptation.cpp @@ -0,0 +1,64 @@ +#include "ENBAdaptation.h" + +#include "../SettingManager.h" +#include "../TextureManager.h" + +void ENBAdaptation::Execute() +{ + auto& textureManager = TextureManager::GetSingleton(); + + auto* currentSRV = textureManager.GetDownsampleTextureBlurry(); + if (!currentSRV) { + return; + } + + SetShaderResourceVariable("TextureCurrent", currentSRV); + + if (!textureCurrent.texture) + return; + + ExecuteTechnique("Downsample", textureCurrent); + + SetShaderResourceVariable("TextureCurrent", textureCurrent.srv.get()); + + // Use swap mechanism to determine input/output + const char* texturePreviousName = (textureManager.GetTextureSwap() & 1) ? "TextureAdaptationSwap" : "TextureAdaptation"; + const char* textureAdaptationName = (textureManager.GetTextureSwap() & 1) ? "TextureAdaptation" : "TextureAdaptationSwap"; + + // Set input texture (previous frame's adaptation value) + auto* texturePrevious = textureManager.GetCommonTexture(texturePreviousName); + if (!texturePrevious) { + return; + } + SetShaderResourceVariable("TexturePrevious", texturePrevious->srv.get()); + + // Execute adaptation technique, writing to output texture + auto* textureAdaptation = textureManager.GetCommonTexture(textureAdaptationName); + if (!textureAdaptation) { + return; + } + ExecuteTechnique("Draw", *textureAdaptation); +} + +void ENBAdaptation::UpdateEffectVariables() +{ + auto& settingManager = SettingManager::GetSingleton(); + + auto forceMinMaxValues = settingManager.GetValue("ForceMinMaxValues", "ADAPTATION"); + + float adaptationTime = settingManager.GetValue("AdaptationTime", "ADAPTATION"); + float deltaTime = (globals::game::deltaTime) ? (*globals::game::deltaTime) : 0.0f; + + float4 adaptationParameters{}; + adaptationParameters.x = !forceMinMaxValues ? 0.0f : settingManager.GetValue("AdaptationMin", "ADAPTATION"); + adaptationParameters.y = !forceMinMaxValues ? 65535.0f : settingManager.GetValue("AdaptationMax", "ADAPTATION"); + adaptationParameters.z = settingManager.GetValue("AdaptationSensitivity", "ADAPTATION"); + adaptationParameters.w = std::clamp((adaptationTime > 0.0f) ? (deltaTime / adaptationTime) : 1.0f, 0.0f, 1.0f); + + SetVectorVariable("AdaptationParameters", &adaptationParameters, sizeof(adaptationParameters)); +} + +void ENBAdaptation::CreateEffectTextures() +{ + textureCurrent = CreateTexture(16, 16, DXGI_FORMAT_R32_FLOAT, "ENBAdaptation::TextureCurrent"); +} \ No newline at end of file diff --git a/src/Features/Effect11/Effects/ENBAdaptation.h b/src/Features/Effect11/Effects/ENBAdaptation.h new file mode 100644 index 0000000000..a2fccb7298 --- /dev/null +++ b/src/Features/Effect11/Effects/ENBAdaptation.h @@ -0,0 +1,17 @@ +#pragma once + +#include "Effect.h" + +class ENBAdaptation : public Effect +{ +public: + virtual std::string GetName() const override { return "enbadaptation.fx"; } + + virtual void Execute() override; + virtual void UpdateEffectVariables() override; + + TextureManager::Texture textureCurrent; + +protected: + void CreateEffectTextures() override; +}; \ No newline at end of file diff --git a/src/Features/Effect11/Effects/ENBBloom.cpp b/src/Features/Effect11/Effects/ENBBloom.cpp new file mode 100644 index 0000000000..db9f7166af --- /dev/null +++ b/src/Features/Effect11/Effects/ENBBloom.cpp @@ -0,0 +1,38 @@ +#include "ENBBloom.h" + +#include "../TextureManager.h" + +void ENBBloom::Execute() +{ + // Get common textures for input/output + auto& textureManager = TextureManager::GetSingleton(); + + auto textureHDRTemp = textureManager.GetCommonTexture("TextureBloomTemp"); + auto textureBloom = textureManager.GetCommonTexture("TextureBloom"); + + if (!textureHDRTemp || !textureBloom) { + return; + } + + // Set dowsampled texture, typically the one used (use 1024x1024 mip) + auto downsampledInputSRV = TextureManager::GetSingleton().GetDownsampleTexture(); + + if (!downsampledInputSRV) { + return; + } + + auto [executed, inOutput] = ExecuteTechniqueSequence(GetSelectedTechnique(), downsampledInputSRV, *textureBloom, *textureHDRTemp); + + if (executed && !inOutput) { + textureManager.SwapTextures("TextureBloom", "TextureBloomTemp"); + } +} + +void ENBBloom::UpdateEffectVariables() +{ + // Set dowsampled texture, typically the one used (use 1024x1024 mip) + SetShaderResourceVariable("TextureDownsampled", TextureManager::GetSingleton().GetDownsampleTexture()); + + // Set original texture, not typically used due to aliasing + SetShaderResourceVariable("TextureOriginal", globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN].SRV); +} \ No newline at end of file diff --git a/src/Features/Effect11/Effects/ENBBloom.h b/src/Features/Effect11/Effects/ENBBloom.h new file mode 100644 index 0000000000..1a67272993 --- /dev/null +++ b/src/Features/Effect11/Effects/ENBBloom.h @@ -0,0 +1,12 @@ +#pragma once + +#include "Effect.h" + +class ENBBloom : public Effect +{ +public: + virtual std::string GetName() const override { return "enbbloom.fx"; } + + virtual void Execute() override; + virtual void UpdateEffectVariables() override; +}; \ No newline at end of file diff --git a/src/Features/Effect11/Effects/ENBEffect.cpp b/src/Features/Effect11/Effects/ENBEffect.cpp new file mode 100644 index 0000000000..12e959a47d --- /dev/null +++ b/src/Features/Effect11/Effects/ENBEffect.cpp @@ -0,0 +1,93 @@ +#include "ENBEffect.h" + +#include "../SettingManager.h" +#include "../TextureManager.h" +#include "Utils/Game.h" + +void ENBEffect::Execute() +{ + auto renderer = globals::game::renderer; + + auto& textureManager = TextureManager::GetSingleton(); + + auto textureSDRTemp = textureManager.GetCommonTexture("TextureSDRTemp"); + auto textureSDRTemp2 = textureManager.GetCommonTexture("TextureSDRTemp2"); + + if (!textureSDRTemp || !textureSDRTemp2) { + return; + } + + auto textureOriginal = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + + if (!textureOriginal.SRV) { + return; + } + + // Execute with: input (16bit HDR), output (10bit SDR), temp (10bit SDR) + auto [executed, inOutput] = ExecuteTechniqueSequence(GetSelectedTechnique(), textureOriginal.SRV, *textureSDRTemp, *textureSDRTemp2); + + if (executed && !inOutput) { + textureManager.SwapTextures("TextureSDRTemp", "TextureSDRTemp2"); + } +} + +void ENBEffect::UpdateEffectVariables() +{ + float4 params01[7]{}; + + auto imageSpaceManager = RE::ImageSpaceManager::GetSingleton(); + if (!imageSpaceManager) { + return; + } + + GET_INSTANCE_MEMBER(data, imageSpaceManager); + auto& baseData = data.baseData; + + auto& modAmount = data.modAmount; + auto& modData = data.modData; + + params01[2].x = baseData.hdr.receiveBloomThreshold; + params01[2].y = baseData.hdr.white * RE::GetINISetting("fReinhardWhiteScale:Display")->GetFloat(); + + params01[3].x = baseData.cinematic.saturation; + params01[3].z = baseData.cinematic.contrast; + params01[3].w = baseData.cinematic.brightness; + + params01[4] = { baseData.tint.color.red, + baseData.tint.color.green, + baseData.tint.color.blue, + baseData.tint.amount }; + + params01[5] = { modData.data[RE::ImageSpaceModData::kFadeR] * modAmount, + modData.data[RE::ImageSpaceModData::kFadeG] * modAmount, + modData.data[RE::ImageSpaceModData::kFadeB] * modAmount, + modData.data[RE::ImageSpaceModData::kFadeAmount] * modAmount }; + + params01[6] = { 1, 1, 1, 1 }; + + SetVectorVariable("Params01", ¶ms01, sizeof(params01)); + + auto& textureManager = TextureManager::GetSingleton(); + auto& settingManager = SettingManager::GetSingleton(); + + float4 enbParams01{}; + enbParams01.x = settingManager.GetInterpolatedTimeOfDayValue("Amount", "BLOOM"); + enbParams01.y = settingManager.GetInterpolatedTimeOfDayValue("Amount", "LENS"); + + SetVectorVariable("ENBParams01", &enbParams01, sizeof(enbParams01)); + + auto bindTextureIfEnabled = [&](const char* settingKey, const char* shaderVar, const char* textureName) { + ID3D11ShaderResourceView* srv = nullptr; + if (settingManager.GetValue(settingKey, "EFFECT")) { + auto* texture = textureManager.GetCommonTexture(textureName); + srv = texture ? texture->srv.get() : nullptr; + } + SetShaderResourceVariable(shaderVar, srv); + }; + + bindTextureIfEnabled("EnableBloom", "TextureBloom", "TextureBloom"); + bindTextureIfEnabled("EnableLens", "TextureLens", "TextureLens"); + + const char* adaptationTexName = (textureManager.GetTextureSwap() & 1) ? "TextureAdaptation" : "TextureAdaptationSwap"; + bindTextureIfEnabled("EnableAdaptation", "TextureAdaptation", adaptationTexName); +} \ No newline at end of file diff --git a/src/Features/Effect11/Effects/ENBEffect.h b/src/Features/Effect11/Effects/ENBEffect.h new file mode 100644 index 0000000000..cc1d6de1d5 --- /dev/null +++ b/src/Features/Effect11/Effects/ENBEffect.h @@ -0,0 +1,14 @@ +#pragma once + +#include "Effect.h" + +class ENBEffect : public Effect +{ +public: + virtual std::string GetName() const override { return "enbeffect.fx"; } + virtual bool IsRequired() const override { return true; } + + virtual void Execute() override; + + virtual void UpdateEffectVariables() override; +}; \ No newline at end of file diff --git a/src/Features/Effect11/Effects/ENBEffectPostPass.cpp b/src/Features/Effect11/Effects/ENBEffectPostPass.cpp new file mode 100644 index 0000000000..51967ba6bd --- /dev/null +++ b/src/Features/Effect11/Effects/ENBEffectPostPass.cpp @@ -0,0 +1,28 @@ +#include "ENBEffectPostPass.h" + +#include "../TextureManager.h" + +void ENBEffectPostPass::Execute() +{ + auto& textureManager = TextureManager::GetSingleton(); + + auto textureSDRTemp = textureManager.GetCommonTexture("TextureSDRTemp"); + auto textureSDRTemp2 = textureManager.GetCommonTexture("TextureSDRTemp2"); + + if (!textureSDRTemp || !textureSDRTemp2) { + return; + } + + auto [executed, inOutput] = ExecuteTechniqueSequence(GetSelectedTechnique(), textureSDRTemp->srv.get(), *textureSDRTemp2, *textureSDRTemp); + + if (executed && inOutput) { + textureManager.SwapTextures("TextureSDRTemp", "TextureSDRTemp2"); + } +} + +void ENBEffectPostPass::UpdateEffectVariables() +{ + auto& textureManager = TextureManager::GetSingleton(); + auto textureSDRTemp = textureManager.GetCommonTexture("TextureSDRTemp"); + SetShaderResourceVariable("TextureOriginal", textureSDRTemp ? textureSDRTemp->srv.get() : nullptr); +} \ No newline at end of file diff --git a/src/Features/Effect11/Effects/ENBEffectPostPass.h b/src/Features/Effect11/Effects/ENBEffectPostPass.h new file mode 100644 index 0000000000..fd56468727 --- /dev/null +++ b/src/Features/Effect11/Effects/ENBEffectPostPass.h @@ -0,0 +1,12 @@ +#pragma once + +#include "Effect.h" + +class ENBEffectPostPass : public Effect +{ +public: + virtual std::string GetName() const override { return "enbeffectpostpass.fx"; } + + virtual void Execute() override; + virtual void UpdateEffectVariables() override; +}; \ No newline at end of file diff --git a/src/Features/Effect11/Effects/ENBLens.cpp b/src/Features/Effect11/Effects/ENBLens.cpp new file mode 100644 index 0000000000..2689cd5e5d --- /dev/null +++ b/src/Features/Effect11/Effects/ENBLens.cpp @@ -0,0 +1,41 @@ +#include "ENBLens.h" + +#include "../TextureManager.h" + +void ENBLens::Execute() +{ + // Get common textures for input/output + auto& textureManager = TextureManager::GetSingleton(); + + auto textureHDRTemp = textureManager.GetCommonTexture("TextureHDRTemp"); + auto textureLens = textureManager.GetCommonTexture("TextureLens"); + + if (!textureHDRTemp || !textureLens) { + return; + } + + // Set dowsampled texture, typically the one used (use 1024x1024 mip) + auto downsampledInputSRV = TextureManager::GetSingleton().GetDownsampleTexture(); + + if (!downsampledInputSRV) { + return; + } + + auto [executed, inOutput] = ExecuteTechniqueSequence(GetSelectedTechnique(), downsampledInputSRV, *textureLens, *textureHDRTemp); + + if (executed && !inOutput) { + textureManager.SwapTextures("TextureLens", "TextureHDRTemp"); + } +} + +void ENBLens::UpdateEffectVariables() +{ + if (!effect) + return; + + // Set dowsampled texture, typically the one used (use 1024x1024 mip) + SetShaderResourceVariable("TextureDownsampled", TextureManager::GetSingleton().GetDownsampleTexture()); + + // Set original texture, not typically used due to aliasing + SetShaderResourceVariable("TextureOriginal", globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN].SRV); +} \ No newline at end of file diff --git a/src/Features/Effect11/Effects/ENBLens.h b/src/Features/Effect11/Effects/ENBLens.h new file mode 100644 index 0000000000..3d762801cb --- /dev/null +++ b/src/Features/Effect11/Effects/ENBLens.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Effect.h" + +class ENBLens : public Effect +{ +public: + virtual std::string GetName() const override { return "enblens.fx"; } + + virtual void Execute() override; + + virtual void UpdateEffectVariables() override; +}; \ No newline at end of file diff --git a/src/Features/Effect11/Effects/Effect.cpp b/src/Features/Effect11/Effects/Effect.cpp new file mode 100644 index 0000000000..9504711e6e --- /dev/null +++ b/src/Features/Effect11/Effects/Effect.cpp @@ -0,0 +1,1054 @@ +#include "Effect.h" +#include +#include +#include + +#include + +#include "../ENBExtender.h" +#include "../PresetManager.h" +#include "../TextureManager.h" +#include "State.h" +#include "Utils/SettingsPatches.h" +#include "Utils/ShaderPatches.h" + +bool Effect::Load() +{ + std::filesystem::path iniPath = PresetManager::GetSingleton().GetENBSeriesPath(); + iniPath /= GetName() + ".ini"; + + if (!std::filesystem::exists(iniPath)) { + logger::info("[EFFECT11] Could not find ini file '{}' for effect '{}', using defaults", iniPath.string(), GetName()); + return true; + } + + auto writeTime = std::filesystem::last_write_time(iniPath); + if (writeTime == lastIniWriteTime) { + logger::info("[EFFECT11] Skipping unchanged ini file '{}' for effect '{}'", iniPath.string(), GetName()); + return true; + } + lastIniWriteTime = writeTime; + + std::string section = GetName(); + std::transform(section.begin(), section.end(), section.begin(), ::toupper); + + for (auto& uiVar : uiVariables) { + if (uiVar.isSeparator || uiVar.isLabel) + continue; + if (!uiVar.effectVariable && !uiVar.isDefine) + continue; + std::string iniKey = GetVariableIniKey(uiVar); + if (iniKey.empty()) + continue; + + bool isPerComponent = IsPerComponentVector(uiVar); + if (isPerComponent) { + static const char* suffixes[] = { "X", "Y", "Z", "W" }; + int numComponents = (uiVar.type == UIVariableType::Float2) ? 2 : (uiVar.type == UIVariableType::Float3) ? 3 : 4; + for (int i = 0; i < numComponents; ++i) { + std::string compKey = iniKey + suffixes[i]; + std::vector valueBuffer(1024); + DWORD result = GetPrivateProfileStringA(section.c_str(), compKey.c_str(), "", valueBuffer.data(), 1024, iniPath.string().c_str()); + if (result > 0) { + try { + uiVar.vectorValue[i] = std::stof(std::string(valueBuffer.data())); + } catch (...) {} + } + } + if (uiVar.effectVariable) + uiVar.effectVariable->AsVector()->SetFloatVector(uiVar.vectorValue); + } else { + std::vector valueBuffer(1024); + DWORD result = GetPrivateProfileStringA(section.c_str(), iniKey.c_str(), "", valueBuffer.data(), 1024, iniPath.string().c_str()); + if (result > 0) { + std::string value(valueBuffer.data()); + LoadVariableFromString(uiVar, value); + } + } + } + + if (!uiTechniques.empty()) { + uint32_t techniqueFromIni = static_cast(GetPrivateProfileIntA(section.c_str(), "TECHNIQUE", selectedTechniqueIndex + 1, iniPath.string().c_str())); + if (techniqueFromIni > 0) { + uint32_t maxIndex = static_cast(uiTechniques.size() - 1); + selectedTechniqueIndex = (techniqueFromIni - 1 < maxIndex) ? (techniqueFromIni - 1) : maxIndex; + } else { + selectedTechniqueIndex = 0; + } + } + + Util::SettingsPatches::Apply(*this); + + logger::debug("[EFFECT11] Loaded settings from '{}' for effect '{}'", iniPath.string(), GetName()); + return true; +} + +void Effect::Save() +{ + std::filesystem::path iniPath = PresetManager::GetSingleton().GetENBSeriesPath(); + iniPath /= GetName() + ".ini"; + + std::string section = GetName(); + std::transform(section.begin(), section.end(), section.begin(), ::toupper); + + for (const auto& uiVar : uiVariables) { + if (uiVar.isSeparator || uiVar.isLabel) + continue; + if (!uiVar.effectVariable && !uiVar.isDefine) + continue; + std::string iniKey = GetVariableIniKey(uiVar); + if (iniKey.empty()) + continue; + + std::string value; + + switch (uiVar.type) { + case UIVariableType::Float: + value = std::to_string(uiVar.floatValue); + break; + case UIVariableType::Int: + value = std::to_string(uiVar.intValue); + break; + case UIVariableType::Bool: + value = uiVar.boolValue ? "true" : "false"; + break; + case UIVariableType::Float2: + case UIVariableType::Float3: + case UIVariableType::Float4: + if (IsPerComponentVector(uiVar)) { + static const char* suffixes[] = { "X", "Y", "Z", "W" }; + int numComponents = (uiVar.type == UIVariableType::Float2) ? 2 : (uiVar.type == UIVariableType::Float3) ? 3 : 4; + for (int i = 0; i < numComponents; ++i) { + std::string compKey = iniKey + suffixes[i]; + std::string compValue = std::to_string(uiVar.vectorValue[i]); + BOOL compResult = WritePrivateProfileStringA(section.c_str(), compKey.c_str(), compValue.c_str(), iniPath.string().c_str()); + if (!compResult) + logger::warn("[EFFECT11] Failed to write key '{}' to ini file '{}'", compKey, iniPath.string()); + } + continue; + } else { + std::ostringstream oss; + int numComponents = (uiVar.type == UIVariableType::Float2) ? 2 : (uiVar.type == UIVariableType::Float3) ? 3 : 4; + + std::copy(uiVar.vectorValue, uiVar.vectorValue + numComponents - 1, + std::ostream_iterator(oss, ", ")); + oss << uiVar.vectorValue[numComponents - 1]; + + value = oss.str(); + } + break; + } + + BOOL result = WritePrivateProfileStringA(section.c_str(), iniKey.c_str(), value.c_str(), iniPath.string().c_str()); + if (!result) { + logger::warn("[EFFECT11] Failed to write key '{}' to ini file '{}'", iniKey, iniPath.string()); + } + } + + std::string techniqueValue = std::to_string(selectedTechniqueIndex + 1u); + BOOL techniqueResult = WritePrivateProfileStringA(section.c_str(), "TECHNIQUE", techniqueValue.c_str(), iniPath.string().c_str()); + if (!techniqueResult) { + logger::warn("[EFFECT11] Failed to write TECHNIQUE key to ini file '{}'", iniPath.string()); + } + + WritePrivateProfileStringA(NULL, NULL, NULL, iniPath.string().c_str()); + + logger::info("[EFFECT11] Saved settings to '{}' for effect '{}'", iniPath.string(), GetName()); +} + +bool Effect::Apply() +{ + logger::info("[EFFECT11] Applying effect '{}'", GetName()); + + Unload(); + + if (!LoadFXFile()) { + if (!filePresent) { + if (IsRequired()) { + errors.push_back("Required effect file not found"); + logger::error("[EFFECT11] Required effect file not found for '{}'", GetName()); + return false; + } + logger::info("[EFFECT11] Effect file not found for '{}', skipping", GetName()); + return true; + } + logger::error("[EFFECT11] Failed to compile FX file for effect '{}'", GetName()); + return false; + } + + if (!isKIEFX) { + if (!Load()) { + errors.push_back("Failed to load settings"); + logger::error("[EFFECT11] Failed to load settings for effect '{}'", GetName()); + return false; + } + } + + CreateEffectTextures(); + + logger::info("[EFFECT11] Successfully applied effect '{}'", GetName()); + return true; +} + +void Effect::Unload() +{ + effect = nullptr; + + techniques.clear(); + variables.clear(); + customTextureCache.clear(); + uiVariables.clear(); + effectTextureCache.clear(); + uiTechniques.clear(); + selectedTechniqueIndex = 0; + groupMeta.clear(); + techniqueDropdown = {}; + sourceGroupMap.clear(); + sourceOrderMap.clear(); + preprocessedSource.clear(); + + ClearVariableCache(); + + filePresent = false; + errors.clear(); + + lastIniWriteTime = {}; + + logger::info("[EFFECT11] Unloaded effect '{}'", GetName()); +} + +bool Effect::LoadFXFile() +{ + auto filePath = PresetManager::GetSingleton().GetENBSeriesPath(); + filePath /= GetName(); + + if (!std::filesystem::exists(filePath)) { + filePresent = false; + return false; + } + filePresent = true; + + std::ifstream mainFile(filePath, std::ios::binary | std::ios::ate); + if (!mainFile.is_open()) { + errors.push_back("Failed to open effect file: " + filePath.string()); + return false; + } + + std::streamsize size = mainFile.tellg(); + if (size < 0) { + errors.push_back("Failed to determine size of effect file: " + filePath.string()); + return false; + } + mainFile.seekg(0, std::ios::beg); + std::string sourceCode(size, '\0'); + if (!mainFile.read(sourceCode.data(), size)) { + errors.push_back("Failed to read effect file: " + filePath.string()); + return false; + } + mainFile.close(); + + isKIEFX = ENBExtender::IsKIEFX(sourceCode); + sourceCode = ENBExtender::DecodeKIEFX(sourceCode); + + auto enbseriesPath = filePath.parent_path(); + auto iniFilePath = enbseriesPath / (GetName() + ".ini"); + std::string iniPathStr = iniFilePath.string(); + std::string iniSection = GetName(); + std::transform(iniSection.begin(), iniSection.end(), iniSection.begin(), ::toupper); + + uiDefines.clear(); + ENBExtender::ConvertExtenderSyntax(sourceCode, enbseriesPath, uiDefines, iniPathStr, iniSection); + Util::ShaderPatches::Apply(GetName().c_str(), sourceCode); + + auto filePathStr = filePath.string(); + + auto compile = [&](const std::string& source, ID3DInclude* include) -> bool { + winrt::com_ptr compiled, err; + HRESULT hr = D3DCompile(source.c_str(), source.size(), filePathStr.c_str(), + nullptr, include, nullptr, "fx_5_0", 0, 0, compiled.put(), err.put()); + if (FAILED(hr)) { + if (err) { + std::string raw(static_cast(err->GetBufferPointer()), err->GetBufferSize()); + std::string filtered; + std::istringstream stream(raw); + std::string line; + while (std::getline(stream, line)) + if (!line.empty() && line.find("warning X4717") == std::string::npos) + filtered += line + "\n"; + if (!filtered.empty()) + logger::warn("[EFFECT11] D3DCompile failed for '{}': {}", filePathStr, filtered); + } + return false; + } + return SUCCEEDED(D3DX11CreateEffectFromMemory(compiled->GetBufferPointer(), + compiled->GetBufferSize(), 0, globals::d3d::device, effect.put())); + }; + + auto preprocess = [&](const std::string& source, ID3DInclude* include) -> std::string { + winrt::com_ptr blob, err; + if (FAILED(D3DPreprocess(source.c_str(), source.size(), filePathStr.c_str(), + nullptr, include, blob.put(), err.put())) || !blob) + return {}; + return { static_cast(blob->GetBufferPointer()), blob->GetBufferSize() }; + }; + + auto tryPreprocessAndCompile = [&](const std::string& source, ID3DInclude* include) -> bool { + auto pp = preprocess(source, include); + if (pp.empty()) + return false; + if (isKIEFX) + preprocessedSource = pp; + else + ENBExtender::ParseSourceGroupScopes(pp, *this); + ENBExtender::StripLineDirectives(pp); + return compile(pp, nullptr); + }; + + bool compiled = false; + + { + ENBExtender::PresetInclude ppInclude(enbseriesPath, uiDefines, iniPathStr, iniSection); + compiled = tryPreprocessAndCompile(sourceCode, &ppInclude); + } + + if (!compiled) { + std::vector dirs = { enbseriesPath }; + std::unordered_set visited; + auto inlined = ENBExtender::InlineIncludes(sourceCode, enbseriesPath, iniPathStr, iniSection, dirs, visited, uiDefines); + ENBExtender::ExpandStringificationMacros(inlined); + compiled = tryPreprocessAndCompile(inlined, nullptr); + } + + if (!compiled) { + ENBExtender::PresetInclude includeHandler(enbseriesPath, uiDefines, iniPathStr, iniSection); + winrt::com_ptr compiledShader, errorBlob; + HRESULT hr = D3DCompile(sourceCode.c_str(), sourceCode.size(), filePathStr.c_str(), + nullptr, &includeHandler, nullptr, "fx_5_0", 0, 0, compiledShader.put(), errorBlob.put()); + if (FAILED(hr)) { + std::string errorMsg = "Compilation failed"; + if (errorBlob) { + std::string raw(static_cast(errorBlob->GetBufferPointer()), errorBlob->GetBufferSize()); + errorMsg.clear(); + logger::error("[EFFECT11] Effect compilation failed for '{}'", filePathStr); + std::istringstream errorStream(raw); + std::string errorLine; + while (std::getline(errorStream, errorLine)) + if (!errorLine.empty() && errorLine.find("warning X4717") == std::string::npos) { + logger::error("[EFFECT11] {}", errorLine); + errorMsg += errorLine + "\n"; + } + if (errorMsg.empty()) + errorMsg = "Compilation failed"; + } + errors.push_back(errorMsg); + return false; + } + if (FAILED(D3DX11CreateEffectFromMemory(compiledShader->GetBufferPointer(), + compiledShader->GetBufferSize(), 0, globals::d3d::device, effect.put()))) { + errors.push_back("Failed to create effect from compiled shader"); + return false; + } + } + + EnumerateAllVariables(); + SetupCustomTextures(); + LoadTechniques(); + LoadUITechniques(); + + if (!isKIEFX) + LoadUIVariables(); + + logger::info("[EFFECT11] Successfully loaded FX file: {}", filePathStr); + return true; +} + +Effect::TechniqueSequenceResult Effect::ExecuteTechniqueSequence(const std::string& a_baseTechniqueName, ID3D11ShaderResourceView* a_input, TextureManager::Texture& a_output, TextureManager::Texture& a_temp) +{ + if (!IsCompiled() || !effect) + return {}; + + if (a_baseTechniqueName.empty()) + return {}; + + auto sequenceIt = techniques.find(a_baseTechniqueName); + if (sequenceIt == techniques.end()) + return {}; + + const auto& sequence = sequenceIt->second; + if (sequence.empty()) + return {}; + + auto sourceTexture = effect->GetVariableByName("TextureColor")->AsShaderResource(); + + uint32_t swapCounter = 0; + uint32_t passOffset = 0; + bool targetInOutput = false; + + ID3D11ShaderResourceView* inputSRV = nullptr; + ID3D11RenderTargetView* outputRTV = nullptr; + + for (size_t i = 0; i < sequence.size(); ++i) { + auto& techniqueInfo = sequence[i]; + + if (!techniqueInfo.technique) + continue; + + if (!ENBExtender::IsTechniqueEnabled(techniqueInfo, *this)) + continue; + + if (sequence.size() == 1 || swapCounter == 0) { + inputSRV = a_input; + outputRTV = a_output.rtv.get(); + } else { + bool useTemp = (swapCounter & 1) == 0; + if (useTemp) { + inputSRV = a_temp.srv.get(); + outputRTV = a_output.rtv.get(); + } else { + inputSRV = a_output.srv.get(); + outputRTV = a_temp.rtv.get(); + } + } + + if (!techniqueInfo.renderTargetName.empty()) { + outputRTV = GetRenderTargetView(techniqueInfo.renderTargetName, outputRTV); + } else { + swapCounter++; + } + + targetInOutput = (outputRTV == a_output.rtv.get()); + + if (sourceTexture && sourceTexture->IsValid()) + sourceTexture->AsShaderResource()->SetResource(inputSRV); + + RenderPasses(techniqueInfo.technique.get(), outputRTV, passOffset); + + D3DX11_TECHNIQUE_DESC td; + if (SUCCEEDED(techniqueInfo.technique->GetDesc(&td))) + passOffset += td.Passes; + } + + return { true, targetInOutput }; +} + +void Effect::ExecuteTechnique(const std::string& techniqueName, TextureManager::Texture& output) +{ + if (!IsCompiled() || !effect) + return; + + auto technique = effect->GetTechniqueByName(techniqueName.c_str()); + if (!technique || !technique->IsValid()) + return; + + RenderPasses(technique, output.rtv.get()); +} + +void Effect::SetupCustomTextures() +{ + for (auto& [varName, effectVar] : variables) { + std::string resourceName = GetUIAnnotation(effectVar.get(), "ResourceName"); + if (resourceName.empty()) + continue; + + auto srv = LoadTextureFromFile(resourceName); + if (srv) { + auto shaderResourceVar = effectVar->AsShaderResource(); + if (shaderResourceVar && shaderResourceVar->IsValid()) + shaderResourceVar->SetResource(srv); + } + } +} + +ID3D11ShaderResourceView* Effect::LoadTextureFromFile(const std::string& filename) +{ + auto device = globals::d3d::device; + + auto cacheIt = customTextureCache.find(filename); + if (cacheIt != customTextureCache.end()) + return cacheIt->second.get(); + + std::filesystem::path filepath = PresetManager::GetSingleton().GetENBSeriesPath() / filename; + + winrt::com_ptr srv; + + DirectX::ScratchImage image; + HRESULT hr = DirectX::LoadFromDDSFile(filepath.c_str(), DirectX::DDS_FLAGS_NONE, nullptr, image); + if (FAILED(hr)) + hr = DirectX::LoadFromWICFile(filepath.c_str(), DirectX::WIC_FLAGS_IGNORE_SRGB, nullptr, image); + if (SUCCEEDED(hr)) + hr = DirectX::CreateShaderResourceView(device, image.GetImages(), image.GetImageCount(), image.GetMetadata(), srv.put()); + + if (FAILED(hr)) { + logger::error("[EFFECT11] Failed to load texture file: {} (HRESULT: 0x{:08X})", filepath.string(), static_cast(hr)); + return nullptr; + } + + customTextureCache[filename] = srv; + return srv.get(); +} + +void Effect::LoadTechniques() +{ + D3DX11_EFFECT_DESC effectDesc; + if (FAILED(effect->GetDesc(&effectDesc))) + return; + + for (UINT g = 0; g < effectDesc.Groups; ++g) { + auto group = effect->GetGroupByIndex(g); + if (!group || !group->IsValid()) + continue; + + D3DX11_GROUP_DESC groupDesc; + if (FAILED(group->GetDesc(&groupDesc))) + continue; + + bool isNamedGroup = groupDesc.Name && groupDesc.Name[0]; + + for (UINT t = 0; t < groupDesc.Techniques; ++t) { + auto technique = group->GetTechniqueByIndex(t); + if (!technique || !technique->IsValid()) + continue; + + D3DX11_TECHNIQUE_DESC techDesc; + if (FAILED(technique->GetDesc(&techDesc))) + continue; + + std::string key; + if (isNamedGroup) { + key = std::string(groupDesc.Name); + } else { + std::string techName = techDesc.Name ? std::string(techDesc.Name) : ("technique" + std::to_string(t)); + + // ENB convention: numbered follow-up techniques (Name1, Name2, ...) belong to the base technique (Name) + std::string baseName = techName; + while (!baseName.empty() && std::isdigit(static_cast(baseName.back()))) + baseName.pop_back(); + + if (!baseName.empty() && baseName != techName && techniques.contains(baseName)) + key = baseName; + else + key = techName; + } + + TechniqueInfo info; + info.technique.copy_from(technique); + info.renderTargetName = GetTechniqueAnnotation(technique, "RenderTarget"); + + for (int bi = 0; bi < 16; ++bi) { + std::string bindVal = GetTechniqueAnnotation(technique, "UIBinding" + std::to_string(bi)); + if (!bindVal.empty()) + info.bindings.push_back({ bindVal, false }); + std::string invVal = GetTechniqueAnnotation(technique, "UIInvBinding" + std::to_string(bi)); + if (!invVal.empty()) + info.bindings.push_back({ invVal, true }); + } + + techniques[key].push_back(std::move(info)); + } + } +} + +void Effect::LoadUITechniques() +{ + uiTechniques.clear(); + selectedTechniqueIndex = 0; + + D3DX11_EFFECT_DESC effectDesc; + if (FAILED(effect->GetDesc(&effectDesc))) + return; + + ENBExtender::LoadTechniqueDropdownMetadata(*this); + + uint32_t defaultIndex = 0; + + for (UINT g = 0; g < effectDesc.Groups; ++g) { + auto group = effect->GetGroupByIndex(g); + if (!group || !group->IsValid()) + continue; + + D3DX11_GROUP_DESC groupDesc; + if (FAILED(group->GetDesc(&groupDesc))) + continue; + + bool isNamedGroup = groupDesc.Name && groupDesc.Name[0]; + + if (isNamedGroup) { + std::string uiName = GetGroupAnnotation(group, "UIName"); + if (uiName.empty()) + continue; + + std::string isDefault = GetGroupAnnotation(group, "UIDefault"); + if (!isDefault.empty() && isDefault != "0" && isDefault != "false") + defaultIndex = static_cast(uiTechniques.size()); + + uiTechniques.push_back({ std::string(groupDesc.Name), uiName }); + } else { + for (UINT t = 0; t < groupDesc.Techniques; ++t) { + auto technique = group->GetTechniqueByIndex(t); + if (!technique || !technique->IsValid()) + continue; + + D3DX11_TECHNIQUE_DESC techDesc; + if (FAILED(technique->GetDesc(&techDesc))) + continue; + + std::string uiName = GetTechniqueAnnotation(technique, "UIName"); + if (uiName.empty()) + continue; + + std::string techName = techDesc.Name ? std::string(techDesc.Name) : ""; + + std::string isDefault = GetTechniqueAnnotation(technique, "UIDefault"); + if (!isDefault.empty() && isDefault != "0" && isDefault != "false") + defaultIndex = static_cast(uiTechniques.size()); + + uiTechniques.push_back({ techName, uiName }); + } + } + } + + if (defaultIndex < uiTechniques.size()) + selectedTechniqueIndex = defaultIndex; +} + + +ID3D11RenderTargetView* Effect::GetRenderTargetView(const std::string& renderTargetName, ID3D11RenderTargetView* fallback) +{ + if (renderTargetName.empty()) + return fallback; + + auto it = effectTextureCache.find(renderTargetName); + if (it != effectTextureCache.end() && it->second.rtv) + return it->second.rtv.get(); + + auto* texture = GetCachedCommonTexture(renderTargetName); + if (texture && texture->rtv) + return texture->rtv.get(); + + return fallback; +} + +void Effect::LoadUIVariables() +{ + D3DX11_EFFECT_DESC effectDesc; + if (FAILED(effect->GetDesc(&effectDesc))) + return; + + uiVariables.clear(); + + std::vector groupStack; + + for (UINT i = 0; i < effectDesc.GlobalVariables; ++i) { + auto variable = effect->GetVariableByIndex(i); + if (!variable || !variable->IsValid()) + continue; + + D3DX11_EFFECT_VARIABLE_DESC varDesc; + if (FAILED(variable->GetDesc(&varDesc))) + continue; + + D3DX11_EFFECT_TYPE_DESC typeDesc; + auto effectType = variable->GetType(); + if (FAILED(effectType->GetDesc(&typeDesc))) + continue; + + if (typeDesc.Class == D3D_SVC_OBJECT && typeDesc.Type == D3D_SVT_STRING) { + ENBExtender::ProcessExtenderStringVariable(variable, varDesc, groupStack, *this); + continue; + } + + auto externBinding = GetUIAnnotation(variable, "ExternBinding"); + if (!externBinding.empty()) { + logger::info("[EFFECT11] ExternBinding '{}' requested by variable '{}' in '{}' (not yet implemented)", + externBinding, varDesc.Name, GetName()); + continue; + } + + UIVariable uiVar = {}; + if (ENBExtender::CreateUIVariable(uiVar, variable, varDesc, typeDesc, groupStack, *this)) { + LoadUIVariableValue(uiVar); + uiVariables.push_back(std::move(uiVar)); + } + } + + ENBExtender::InsertUIDefines(*this); + + logger::info("[EFFECT11] Loaded {} UI variables for effect '{}'", uiVariables.size(), GetName()); +} + +static std::string ReadAnnotationValue(ID3DX11EffectVariable* annotation) +{ + if (!annotation || !annotation->IsValid()) + return ""; + + auto stringVar = annotation->AsString(); + if (stringVar && stringVar->IsValid()) { + LPCSTR value = nullptr; + if (SUCCEEDED(stringVar->GetString(&value)) && value) + return std::string(value); + } + + auto scalarVar = annotation->AsScalar(); + if (scalarVar && scalarVar->IsValid()) { + auto annType = annotation->GetType(); + D3DX11_EFFECT_TYPE_DESC typeDesc; + if (annType && SUCCEEDED(annType->GetDesc(&typeDesc))) { + switch (typeDesc.Type) { + case D3D_SVT_INT: { int v; if (SUCCEEDED(scalarVar->GetInt(&v))) return std::to_string(v); break; } + case D3D_SVT_FLOAT: { float v; if (SUCCEEDED(scalarVar->GetFloat(&v))) return std::to_string(v); break; } + case D3D_SVT_BOOL: { bool v; if (SUCCEEDED(scalarVar->GetBool(&v))) return std::to_string(v ? 1 : 0); break; } + default: break; + } + } + int intValue; + if (SUCCEEDED(scalarVar->GetInt(&intValue))) + return std::to_string(intValue); + } + return ""; +} + +std::string Effect::GetUIAnnotation(ID3DX11EffectVariable* variable, const std::string& annotationName) +{ + if (!variable) + return ""; + + auto annotation = variable->GetAnnotationByName(annotationName.c_str()); + if (annotation && annotation->IsValid()) { + auto result = ReadAnnotationValue(annotation); + if (!result.empty()) + return result; + } + + D3DX11_EFFECT_VARIABLE_DESC varDesc; + if (FAILED(variable->GetDesc(&varDesc))) + return ""; + for (UINT i = 0; i < varDesc.Annotations; ++i) { + auto ann = variable->GetAnnotationByIndex(i); + if (!ann || !ann->IsValid()) + continue; + D3DX11_EFFECT_VARIABLE_DESC annDesc; + if (FAILED(ann->GetDesc(&annDesc))) + continue; + if (_stricmp(annDesc.Name, annotationName.c_str()) == 0) + return ReadAnnotationValue(ann); + } + return ""; +} + +std::string Effect::GetTechniqueAnnotation(ID3DX11EffectTechnique* technique, const std::string& annotationName) +{ + if (!technique) + return ""; + auto annotation = technique->GetAnnotationByName(annotationName.c_str()); + return ReadAnnotationValue(annotation); +} + +std::string Effect::GetGroupAnnotation(ID3DX11EffectGroup* group, const std::string& annotationName) +{ + if (!group) + return ""; + auto annotation = group->GetAnnotationByName(annotationName.c_str()); + return ReadAnnotationValue(annotation); +} + +bool Effect::IsPerComponentVector(const UIVariable& uiVar) +{ + return (uiVar.type == UIVariableType::Float2 || uiVar.type == UIVariableType::Float3 || uiVar.type == UIVariableType::Float4) && + uiVar.widgetType != UIWidgetType::Color; +} + +std::string Effect::GetVariableIniKey(const UIVariable& uiVar) +{ + if (!uiVar.uniqueName.empty()) + return uiVar.uniqueName; + return uiVar.group.empty() ? uiVar.displayName : uiVar.group + "." + uiVar.displayName; +} + +void Effect::LoadUIVariableValue(UIVariable& uiVar) +{ + switch (uiVar.type) { + case UIVariableType::Float: + uiVar.effectVariable->AsScalar()->GetFloat(&uiVar.floatValue); + break; + case UIVariableType::Int: + uiVar.effectVariable->AsScalar()->GetInt(&uiVar.intValue); + break; + case UIVariableType::Bool: + uiVar.effectVariable->AsScalar()->GetBool(&uiVar.boolValue); + break; + case UIVariableType::Float2: + case UIVariableType::Float3: + case UIVariableType::Float4: + uiVar.effectVariable->AsVector()->GetFloatVector(uiVar.vectorValue); + break; + } +} + +void Effect::LoadVariableFromString(UIVariable& uiVar, const std::string& value) +{ + try { + switch (uiVar.type) { + case UIVariableType::Float: + uiVar.floatValue = std::stof(value); + if (uiVar.effectVariable) + uiVar.effectVariable->AsScalar()->SetFloat(uiVar.floatValue); + break; + case UIVariableType::Int: + uiVar.intValue = std::stoi(value); + if (uiVar.effectVariable) + uiVar.effectVariable->AsScalar()->SetInt(uiVar.intValue); + break; + case UIVariableType::Bool: + { + std::string lowerValue = value; + std::transform(lowerValue.begin(), lowerValue.end(), lowerValue.begin(), ::tolower); + if (lowerValue == "true" || lowerValue == "1" || lowerValue == "yes" || lowerValue == "on") + uiVar.boolValue = true; + else if (lowerValue == "false" || lowerValue == "0" || lowerValue == "no" || lowerValue == "off") + uiVar.boolValue = false; + else + uiVar.boolValue = std::stoi(value) != 0; + if (uiVar.effectVariable) + uiVar.effectVariable->AsScalar()->SetBool(uiVar.boolValue); + } + break; + case UIVariableType::Float2: + case UIVariableType::Float3: + case UIVariableType::Float4: + { + std::istringstream ss(value); + int numComponents = (uiVar.type == UIVariableType::Float2) ? 2 : (uiVar.type == UIVariableType::Float3) ? 3 : 4; + for (int i = 0; i < numComponents; ++i) { + char sep; + ss >> uiVar.vectorValue[i]; + if (ss.peek() == ',') + ss >> sep; + } + if (uiVar.effectVariable) + uiVar.effectVariable->AsVector()->SetFloatVector(uiVar.vectorValue); + } + break; + } + } catch (const std::exception& e) { + logger::warn("[EFFECT11] Failed to parse value '{}' for variable '{}': {}", value, uiVar.name, e.what()); + } +} + +void Effect::UpdateUIVariables() +{ + for (auto& uiVar : uiVariables) { + if (!uiVar.effectVariable || uiVar.isSeparator) + continue; + + switch (uiVar.type) { + case UIVariableType::Float: + uiVar.effectVariable->AsScalar()->SetFloat(uiVar.floatValue); + break; + case UIVariableType::Int: + uiVar.effectVariable->AsScalar()->SetInt(uiVar.intValue); + break; + case UIVariableType::Bool: + uiVar.effectVariable->AsScalar()->SetBool(uiVar.boolValue); + break; + case UIVariableType::Float2: + case UIVariableType::Float3: + case UIVariableType::Float4: + uiVar.effectVariable->AsVector()->SetFloatVector(uiVar.vectorValue); + break; + } + } + + ENBExtender::ApplyTimeOfDayInterpolation(*this); +} + +void Effect::RenderImGui() +{ + ENBExtender::RenderUI(*this); +} + +void Effect::EnumerateAllVariables() +{ + D3DX11_EFFECT_DESC effectDesc; + if (FAILED(effect->GetDesc(&effectDesc))) + return; + + variables.clear(); + + for (UINT i = 0; i < effectDesc.GlobalVariables; ++i) { + auto variable = effect->GetVariableByIndex(i); + if (!variable || !variable->IsValid()) + continue; + + D3DX11_EFFECT_VARIABLE_DESC varDesc; + if (FAILED(variable->GetDesc(&varDesc))) + continue; + + variables[varDesc.Name].copy_from(variable); + } +} + +ID3DX11EffectVariable* Effect::GetCachedVariable(const std::string& name) +{ + if (!effect) + return nullptr; + + auto it = variableCache.find(name); + if (it != variableCache.end()) + return it->second; + + auto variable = effect->GetVariableByName(name.c_str()); + variableCache[name] = variable; + return variable; +} + +TextureManager::Texture* Effect::GetCachedCommonTexture(const std::string& name) +{ + auto it = commonTexturePointerCache.find(name); + if (it != commonTexturePointerCache.end()) + return it->second; + + auto* texture = TextureManager::GetSingleton().GetCommonTexture(name); + commonTexturePointerCache[name] = texture; + return texture; +} + +void Effect::ClearVariableCache() +{ + variableCache.clear(); + commonTexturePointerCache.clear(); +} + +bool Effect::SetShaderResourceVariable(const std::string& variableName, ID3D11ShaderResourceView* resource) +{ + auto variable = GetCachedVariable(variableName); + if (variable) { + auto srVar = variable->AsShaderResource(); + if (srVar && srVar->IsValid()) { + srVar->SetResource(resource); + return true; + } + } + return false; +} + +bool Effect::SetShaderResourceVariable(ID3DX11Effect* effect, const std::string& variableName, ID3D11ShaderResourceView* resource) +{ + if (!effect) + return false; + + auto variable = effect->GetVariableByName(variableName.c_str())->AsShaderResource(); + if (variable && variable->IsValid()) { + variable->SetResource(resource); + return true; + } + return false; +} + +bool Effect::SetVectorVariable(ID3DX11Effect* effect, const std::string& variableName, const void* data, uint32_t size) +{ + if (!effect) + return false; + + auto variable = effect->GetVariableByName(variableName.c_str()); + if (variable && variable->IsValid()) { + variable->SetRawValue(data, 0, size); + return true; + } + return false; +} + +bool Effect::SetVectorVariable(const std::string& variableName, const void* data, uint32_t size) +{ + auto variable = GetCachedVariable(variableName); + if (variable && variable->IsValid()) { + variable->SetRawValue(data, 0, size); + return true; + } + return false; +} + +TextureManager::Texture Effect::CreateTexture(uint32_t width, uint32_t height, DXGI_FORMAT format, const std::string& debugName) +{ + auto device = globals::d3d::device; + + D3D11_TEXTURE2D_DESC texDesc = {}; + texDesc.Width = width; + texDesc.Height = height; + texDesc.MipLevels = 1; + texDesc.ArraySize = 1; + texDesc.Format = format; + texDesc.SampleDesc.Count = 1; + texDesc.Usage = D3D11_USAGE_DEFAULT; + texDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; + + TextureManager::Texture texture{}; + DX::ThrowIfFailed(device->CreateTexture2D(&texDesc, nullptr, texture.texture.put())); + DX::ThrowIfFailed(device->CreateRenderTargetView(texture.texture.get(), nullptr, texture.rtv.put())); + DX::ThrowIfFailed(device->CreateShaderResourceView(texture.texture.get(), nullptr, texture.srv.put())); + + if (!debugName.empty()) { + Util::SetResourceName(texture.texture.get(), (debugName).c_str()); + Util::SetResourceName(texture.rtv.get(), (debugName + " RTV").c_str()); + Util::SetResourceName(texture.srv.get(), (debugName + " SRV").c_str()); + } + + return texture; +} + +std::string Effect::GetSelectedTechnique() const +{ + if (selectedTechniqueIndex < uiTechniques.size()) + return uiTechniques[selectedTechniqueIndex].techniqueName; + if (!techniques.empty()) + return techniques.begin()->first; + return ""; +} + +void Effect::RenderPasses(ID3DX11EffectTechnique* technique, ID3D11RenderTargetView* outputRTV, uint32_t passOffset) +{ + if (!technique || !outputRTV || !effect) + return; + + auto context = globals::d3d::context; + + context->OMSetRenderTargets(1, &outputRTV, nullptr); + + uint32_t outputWidth = 0, outputHeight = 0; + winrt::com_ptr outputResource; + outputRTV->GetResource(outputResource.put()); + winrt::com_ptr outputTexture; + if (outputResource) { + outputResource.try_as(outputTexture); + if (outputTexture) { + D3D11_TEXTURE2D_DESC outputDesc; + outputTexture->GetDesc(&outputDesc); + outputWidth = outputDesc.Width; + outputHeight = outputDesc.Height; + } + } + + if (outputWidth == 0 || outputHeight == 0) + return; + + float aspect = static_cast(outputWidth) / static_cast(outputHeight); + float screenSize[4] = { static_cast(outputWidth), 1.0f / outputWidth, aspect, 1.0f / aspect }; + SetVectorVariable(effect.get(), "ScreenSize", screenSize, sizeof(screenSize)); + + D3D11_VIEWPORT viewport = {}; + viewport.Width = static_cast(outputWidth); + viewport.Height = static_cast(outputHeight); + viewport.MaxDepth = 1.0f; + context->RSSetViewports(1, &viewport); + + D3DX11_TECHNIQUE_DESC techDesc; + technique->GetDesc(&techDesc); + + for (UINT p = 0; p < techDesc.Passes; p++) { + if (profiler) + profiler->BeginPass(std::format("Effect11::{} Pass {}", GetName(), passOffset + p)); + technique->GetPassByIndex(p)->Apply(0, context); + context->Draw(4, 0); + if (profiler) + profiler->EndPass(); + } +} diff --git a/src/Features/Effect11/Effects/Effect.h b/src/Features/Effect11/Effects/Effect.h new file mode 100644 index 0000000000..fee1f08e10 --- /dev/null +++ b/src/Features/Effect11/Effects/Effect.h @@ -0,0 +1,266 @@ +#pragma once + +#include +#include +#include + +#include "Profiler.h" +#include "../TextureManager.h" + +class Effect +{ +public: + Effect() = default; + virtual ~Effect() = default; + + // UI technique structure (defined early for use in method declarations) + struct UITechnique + { + std::string techniqueName; // Actual technique name + std::string displayName; // UIName annotation + }; + + // Settings methods + bool Load(); + void Save(); + + // Effect lifecycle + virtual bool Apply(); // Clear resources, load settings, recompile, create resources + virtual void Unload(); // Clear all resources + + bool IsCompiled() const { return filePresent && errors.empty(); } + bool IsFilePresent() const { return filePresent; } + const std::vector& GetErrors() const { return errors; } + + virtual void Execute() = 0; + virtual void UpdateEffectVariables() {} + + // Virtual texture creation function for derived classes to override + virtual void CreateEffectTextures() {} + + // UI System + void RenderImGui(); + void LoadUIVariables(); + void UpdateUIVariables(); + + // Technique selection + std::string GetSelectedTechnique() const; + + // Pure virtual methods for derived classes to implement + virtual std::string GetName() const = 0; + virtual bool IsRequired() const { return false; } + + struct TechniqueBinding + { + std::string variableName; + bool inverted = false; + }; + + struct TechniqueInfo + { + winrt::com_ptr technique; + std::string renderTargetName; + std::vector bindings; + }; + + Profiler* profiler = nullptr; + + winrt::com_ptr effect; + std::unordered_map> techniques; + std::unordered_map> variables; + + std::unordered_map effectTextureCache; + std::unordered_map> customTextureCache; + + // UI Variable System + enum class UIVariableType + { + Float, + Float2, + Int, + Bool, + Float3, + Float4 + }; + + enum class UIWidgetType + { + Default, + Dropdown, + Vector, + Quality, + Color + }; + + struct UIVariable + { + UIVariableType type; + UIWidgetType widgetType; + std::string name; + std::string displayName; + std::string group; + winrt::com_ptr effectVariable; + + // Value storage + union + { + float floatValue; + int intValue; + bool boolValue; + }; + + float vectorValue[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + + // UI properties + float floatMin = 0.0f; + float floatMax = 1.0f; + int intMin = 0; + int intMax = 100; + int ordering = 0; + int sourceOrder = INT_MAX; + bool isSeparator = false; + bool isLabel = false; + bool isReadOnly = false; + bool isDefine = false; + bool isHidden = false; + std::string uniqueName; + std::string uiBinding; + std::string uiBindingFile; + std::string uiBindingProperty; + std::string uiBindingCondition; + bool ignorePerfMode = false; + bool isWeatherOnlyString = false; + + // Weather separation ("ExteriorWeather" or "Weather") + std::string separation; + // Parsed time period from UIName (e.g. "Dawn", "Day", "Night", "Interior") + std::string timePeriod; + + std::vector dropdownItems; + }; + + std::vector uiVariables; + + struct GroupMeta + { + std::string displayName; + int ordering = 0; + bool defaultOpen = false; + bool hasOrdering = false; + bool isTopLevel = false; + }; + std::unordered_map groupMeta; + + struct TechniqueDropdownMeta + { + std::string name = "Technique"; + std::string group; + std::string groupName; + bool groupOpen = false; + bool visible = true; + bool topLevel = false; + int ordering = 1; + }; + TechniqueDropdownMeta techniqueDropdown; + + // UI technique selection (indexed by uint, only includes annotated techniques) + std::vector uiTechniques; + uint32_t selectedTechniqueIndex = 0; + + // Error tracking + bool filePresent = false; + std::vector errors; + + // Whether the source file was KIEFX-encoded (determines merged vs standalone UI) + bool isKIEFX = false; + + // Source-parsed group map and declaration order (compiled effect reorders variable types) + std::unordered_map sourceGroupMap; + std::unordered_map sourceOrderMap; + + // Stored preprocessed source for KIEFX cross-file parsing (cleared after use) + std::string preprocessedSource; + + struct UIDefineInfo + { + std::string defineName; + std::string displayName; + std::string group; + std::string type; + std::string value; + std::string widget; + std::string list; + int intMin = 0; + int intMax = 100; + float floatMin = 0.0f; + float floatMax = 1.0f; + int ordering = 0; + bool hasExplicitOrdering = false; + }; + + std::vector uiDefines; + + // INI file modification time tracking to skip redundant reloads + std::filesystem::file_time_type lastIniWriteTime{}; + + struct TechniqueSequenceResult + { + bool executed = false; + bool inOutput = false; + }; + + // Execute a technique sequence with ping-pong rendering + TechniqueSequenceResult ExecuteTechniqueSequence(const std::string& a_baseTechniqueName, ID3D11ShaderResourceView* a_input, TextureManager::Texture& a_output, TextureManager::Texture& a_temp); + + // Execute a single technique + void ExecuteTechnique(const std::string& techniqueName, TextureManager::Texture& output); + + // Allow EffectManager to setup common variables + ID3DX11Effect* GetEffect() const { return effect.get(); } + + // Helper function to set shader resource variables (non-static version for this effect) + bool SetShaderResourceVariable(const std::string& variableName, ID3D11ShaderResourceView* resource); + + // Texture creation helper + static TextureManager::Texture CreateTexture(uint32_t width, uint32_t height, DXGI_FORMAT format, const std::string& debugName); + + // Static helper functions for any effect + static bool SetShaderResourceVariable(ID3DX11Effect* effect, const std::string& variableName, ID3D11ShaderResourceView* resource); + static bool SetVectorVariable(ID3DX11Effect* effect, const std::string& variableName, const void* data, uint32_t size); + + // Helper function for safe vector variable access + bool SetVectorVariable(const std::string& variableName, const void* data, uint32_t size); + + void RenderPasses(ID3DX11EffectTechnique* technique, ID3D11RenderTargetView* outputRTV, uint32_t passOffset = 0); + + // UI annotation helpers (public for ENBExtender access) + std::string GetUIAnnotation(ID3DX11EffectVariable* variable, const std::string& annotationName); + static std::string GetTechniqueAnnotation(ID3DX11EffectTechnique* technique, const std::string& annotationName); + static std::string GetGroupAnnotation(ID3DX11EffectGroup* group, const std::string& annotationName); + +protected: + ID3DX11EffectVariable* GetCachedVariable(const std::string& name); + TextureManager::Texture* GetCachedCommonTexture(const std::string& name); + void ClearVariableCache(); + +private: + bool LoadFXFile(); + + std::unordered_map variableCache; + std::unordered_map commonTexturePointerCache; + + void EnumerateAllVariables(); + + void SetupCustomTextures(); + ID3D11ShaderResourceView* LoadTextureFromFile(const std::string& filename); + + void LoadTechniques(); + void LoadUITechniques(); + ID3D11RenderTargetView* GetRenderTargetView(const std::string& renderTargetName, ID3D11RenderTargetView* fallback); + + // UI Variable helpers (private) + static bool IsPerComponentVector(const UIVariable& uiVar); + std::string GetVariableIniKey(const UIVariable& uiVar); + void LoadUIVariableValue(UIVariable& uiVar); + void LoadVariableFromString(UIVariable& uiVar, const std::string& value); +}; \ No newline at end of file diff --git a/src/Features/Effect11/MenuManager.cpp b/src/Features/Effect11/MenuManager.cpp new file mode 100644 index 0000000000..e11c09bf13 --- /dev/null +++ b/src/Features/Effect11/MenuManager.cpp @@ -0,0 +1,559 @@ +#include "MenuManager.h" + +#include "EffectManager.h" +#include "SettingManager.h" +#include "TextureManager.h" +#include "Utils/ShaderPatches.h" + +static const char* const timeOfDayNames[] = { "Dawn", "Sunrise", "Day", "Sunset", "Dusk", "Night", "InteriorDay", "InteriorNight" }; + +MenuManager& MenuManager::GetSingleton() +{ + static MenuManager instance; + return instance; +} + +void MenuManager::RenderImGui() +{ + if (!ImGui::BeginTable("Effect11", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV, ImVec2(0, 0))) { + return; + } + + ImGui::TableSetupColumn("Settings", ImGuiTableColumnFlags_WidthFixed, 400.0f); + ImGui::TableSetupColumn("Effects", ImGuiTableColumnFlags_WidthStretch); + + ImGui::TableNextRow(); + + // Left side - Settings + ImGui::TableSetColumnIndex(0); + if (ImGui::BeginChild("Settings", ImVec2(0, 0), false)) { + RenderSettingsPanel(); + } + ImGui::EndChild(); + + // Right side - Effects + ImGui::TableSetColumnIndex(1); + if (ImGui::BeginChild("Effects", ImVec2(0, 0), false)) { + EffectManager::GetSingleton().RenderEffectsList(); + } + ImGui::EndChild(); + + ImGui::EndTable(); +} + +void MenuManager::RenderSettingsPanel() +{ + auto& settingManager = SettingManager::GetSingleton(); + auto& effectManager = EffectManager::GetSingleton(); + + if (ImGui::Button("Save & Apply")) { + settingManager.Save(); + effectManager.Save(); + Util::ShaderPatches::Load(); + settingManager.Load(); + effectManager.Apply(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Save all settings, then reload and recompile shaders"); + } + + ImGui::SameLine(); + + if (ImGui::Button("Load & Apply")) { + Util::ShaderPatches::Load(); + settingManager.Load(); + effectManager.Apply(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Load all settings from enbseries.ini, weather files, and effect configurations, reload shaders"); + } + + ImGui::SameLine(); + + if (ImGui::Button("Load")) { + settingManager.Load(); + effectManager.Load(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Load all settings from enbseries.ini, weather files, and effect configurations"); + } + + ImGui::SameLine(); + + if (ImGui::Button("Save")) { + settingManager.Save(); + effectManager.Save(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Save all settings to enbseries.ini, weather files, and effect configurations"); + } + + ImGui::Separator(); + + if (ImGui::BeginChild("SettingsScroll", ImVec2(0, 0), false)) { + RenderAllSettings(); + } + ImGui::EndChild(); +} + +void MenuManager::RenderWeatherControl() +{ + auto& effectManager = EffectManager::GetSingleton(); + + // Current weather status + uint32_t currentWeatherID = static_cast(effectManager.commonData.weather[0]); + uint32_t lastWeatherID = static_cast(effectManager.commonData.weather[1]); + float blendFactor = effectManager.commonData.weather[2]; + + ImGui::Text("Current Weather: 0x%X, Outgoing Weather: 0x%X", currentWeatherID, lastWeatherID); + ImGui::Text("Weather Blend Factor: %.2f", blendFactor); +} + +void MenuManager::RenderDebugControl() +{ + auto& effectManager = EffectManager::GetSingleton(); + auto& weatherManager = WeatherManager::GetSingleton(); + + // Current time of day values + if (ImGui::BeginTable("TimeOfDayValues", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) { + ImGui::TableSetupColumn("Period", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Array", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableHeadersRow(); + + // Display timeOfDay1 values + const char* tod1Names[] = { "Dawn", "Sunrise", "Day", "Sunset" }; + for (int i = 0; i < 4; ++i) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", tod1Names[i]); + ImGui::TableSetColumnIndex(1); + ImGui::Text("%.3f", effectManager.commonData.timeOfDay1[i]); + ImGui::TableSetColumnIndex(2); + ImGui::Text("timeOfDay1[%d]", i); + } + + // Display timeOfDay2 values + const char* tod2Names[] = { "Dusk", "Night", "InteriorDay", "InteriorNight" }; + for (int i = 0; i < 4; ++i) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", tod2Names[i]); + ImGui::TableSetColumnIndex(1); + ImGui::Text("%.3f", effectManager.commonData.timeOfDay2[i]); + ImGui::TableSetColumnIndex(2); + ImGui::Text("timeOfDay2[%d]", i); + } + + ImGui::EndTable(); + } + + ImGui::Separator(); + + // Weather file list + if (ImGui::TreeNodeEx("Loaded Weather Files", ImGuiTreeNodeFlags_DefaultOpen)) { + const auto& weatherEntries = weatherManager.GetWeatherEntries(); + + if (!weatherEntries.empty()) { + if (ImGui::BeginChild("WeatherList", ImVec2(0, 300), true)) { + // Sort weather entries by name for consistent display + std::vector> sortedWeathers; + for (const auto& [key, entry] : weatherEntries) { + sortedWeathers.emplace_back(key, &entry); + } + std::sort(sortedWeathers.begin(), sortedWeathers.end()); + + for (const auto& [key, entry] : sortedWeathers) { + ImGui::PushID(key.c_str()); + + // Show weather file name and IDs + ImGui::Text("%s", entry->fileName.c_str()); + ImGui::SameLine(); + ImGui::Text("(%s)", key.c_str()); + + // Show weather IDs on same line + ImGui::SameLine(); + std::string idsText = "IDs: "; + for (size_t i = 0; i < entry->weatherIDs.size() && i < 3; ++i) { + if (i > 0) + idsText += ", "; + idsText += std::format("0x{:X}", entry->weatherIDs[i]); + } + if (entry->weatherIDs.size() > 3) { + idsText += "..."; + } + ImGui::Text("%s", idsText.c_str()); + + ImGui::PopID(); + } + } + ImGui::EndChild(); + } else { + ImGui::Text("No weather files loaded"); + ImGui::Text("Make sure _weatherlist.ini exists in enbseries folder"); + } + + ImGui::TreePop(); + } +} + +std::map> MenuManager::GetCategorizedSettings() const +{ + std::map> categorizedSettings; + + // Global Settings - Master controls and basic adjustments + categorizedSettings["Main"] = { + "GLOBAL", + "EFFECT", + "COLORCORRECTION", + "WEATHER", + "TIMEOFDAY", + "ADAPTATION" + }; + + // Weather-Based Settings - Categories that change with weather/time + categorizedSettings["Weather"] = { "BLOOM", "LENS", "ENVIRONMENT", "SKY", "SKYSCATTERING", "PROCEDURALSUN", "VOLUMETRICFOG", "VOLUMETRICRAYS", "IMAGEBASEDLIGHTING", "SKYLIGHTING", "PARTICLE", "FIRE", "RAIN", "LIGHTSPRITE", "GAMEVOLUMETRICRAYS", "SUNGLARE", "CLOUDSHADOWS" }; + + // Debug Information + categorizedSettings["Debug"] = {}; + + return categorizedSettings; +} + +std::vector MenuManager::GetActiveTimeOfDayIndices() const +{ + auto& effectManager = EffectManager::GetSingleton(); + std::vector activeIndices; + + // Access time of day data from EffectManager (this data is updated every frame) + const auto& commonData = effectManager.commonData; + + // Check if we're in interior (> 0.5) or exterior + bool isInterior = commonData.eInteriorFactor > 0.5f; + + if (isInterior) { + // For interiors, show both interior time periods + activeIndices.push_back(6); // InteriorDay + activeIndices.push_back(7); // InteriorNight + } else { + // For exteriors, show all exterior time periods + activeIndices.push_back(0); // Dawn + activeIndices.push_back(1); // Sunrise + activeIndices.push_back(2); // Day + activeIndices.push_back(3); // Sunset + activeIndices.push_back(4); // Dusk + activeIndices.push_back(5); // Night + } + + return activeIndices; +} + +float MenuManager::GetTimeOfDayBlendFactor(int timeIndex) const +{ + auto& effectManager = EffectManager::GetSingleton(); + const auto& commonData = effectManager.commonData; + + // Return the actual blend factor for each time period + switch (timeIndex) { + case 0: + return commonData.timeOfDay1[0]; // Dawn + case 1: + return commonData.timeOfDay1[1]; // Sunrise + case 2: + return commonData.timeOfDay1[2]; // Day + case 3: + return commonData.timeOfDay1[3]; // Sunset + case 4: + return commonData.timeOfDay2[0]; // Dusk + case 5: + return commonData.timeOfDay2[1]; // Night + case 6: + return commonData.timeOfDay2[2]; // InteriorDay + case 7: + return commonData.timeOfDay2[3]; // InteriorNight + default: + return 0.0f; + } +} + +void MenuManager::RenderAllSettings() +{ + auto& settingManager = SettingManager::GetSingleton(); + auto categorizedSettings = GetCategorizedSettings(); + + // Define explicit order for tabs + const std::vector tabOrder = { "Main", "Weather", "Debug" }; + + if (ImGui::BeginTabBar("SettingsTabBar", ImGuiTabBarFlags_None)) { + for (const auto& tabName : tabOrder) { + if (categorizedSettings.find(tabName) == categorizedSettings.end()) + continue; + + const auto& categories = categorizedSettings[tabName]; + + if (ImGui::BeginTabItem(tabName.c_str())) { + // Add weather control to the Weather tab + if (tabName == "Weather") { + RenderWeatherControl(); + ImGui::Separator(); + + // Show TimeOfDay header for Weather tab only + + auto activeIndices = GetActiveTimeOfDayIndices(); + + if (!activeIndices.empty()) { + if (ImGui::BeginTable("WeatherTimeOfDayHeader", 2, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp)) { + ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Time Periods"); + ImGui::TableSetColumnIndex(1); + + float totalWidth = ImGui::GetContentRegionAvail().x; + float sliderWidth = (totalWidth - (activeIndices.size() - 1) * 8.0f) / activeIndices.size(); + + for (size_t idx = 0; idx < activeIndices.size(); ++idx) { + int i = activeIndices[idx]; + + if (idx > 0) { + ImGui::SameLine(); + } + + // Use a child region to control the exact width and center the text + ImGui::BeginChild(("##weatherheader_" + std::to_string(i)).c_str(), ImVec2(sliderWidth, ImGui::GetTextLineHeight()), false, ImGuiWindowFlags_NoScrollbar); + + float labelWidth = ImGui::CalcTextSize(timeOfDayNames[i]).x; + float centerOffset = (sliderWidth - labelWidth) * 0.5f; + if (centerOffset > 0) { + ImGui::SetCursorPosX(centerOffset); + } + + // Style the label based on activity + float blendFactor = GetTimeOfDayBlendFactor(i); + bool isActive = blendFactor > 0.01f; + + if (!isActive) { + // Inactive periods: dim the text + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 0.7f)); + } + // Active periods: use default theme color (no style override) + + ImGui::Text("%s", timeOfDayNames[i]); + + if (!isActive) { + ImGui::PopStyleColor(); + } + + ImGui::EndChild(); + } + + ImGui::EndTable(); + } + + ImGui::Separator(); + } + } + + if (tabName == "Debug") { + RenderDebugControl(); + } + + for (const auto& category : categories) { + ImGuiTreeNodeFlags flags = (tabName == "Weather") ? ImGuiTreeNodeFlags_None : ImGuiTreeNodeFlags_DefaultOpen; + + if (ImGui::CollapsingHeader(category.c_str(), flags)) { + auto settings = settingManager.GetSettingsByCategory(category); + + if (ImGui::BeginTable((category + "_table").c_str(), 2, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp)) { + ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + + // Add weather ignore controls for categories with weather support + if (settingManager.CategoryHasWeatherSupport(category)) { + auto& effectManager = EffectManager::GetSingleton(); + bool isInterior = effectManager.commonData.eInteriorFactor > 0.5f; + + if (!isInterior) { + // Show exterior ignore setting when outside + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("IgnoreWeatherSystem"); + ImGui::TableSetColumnIndex(1); + bool ignoreWeather = settingManager.GetIgnoreWeatherSystem(category); + if (ImGui::Checkbox(("##IgnoreWeatherSystem_" + category).c_str(), &ignoreWeather)) { + settingManager.SetIgnoreWeatherSystem(category, ignoreWeather); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("When enabled, uses enbseries.ini values instead of weather-specific values for exterior areas"); + } + } else if (!settingManager.IsCategoryExteriorOnly(category)) { + // Show interior ignore setting when inside (skip for exterior-only categories) + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("IgnoreWeatherSystemInterior"); + ImGui::TableSetColumnIndex(1); + bool ignoreWeatherInterior = settingManager.GetIgnoreWeatherSystemInterior(category); + if (ImGui::Checkbox(("##IgnoreWeatherSystemInterior_" + category).c_str(), &ignoreWeatherInterior)) { + settingManager.SetIgnoreWeatherSystemInterior(category, ignoreWeatherInterior); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("When enabled, uses enbseries.ini values instead of weather-specific values for interior areas"); + } + } + + // Add separator + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); + } + + for (const auto& settingKey : settings) { + auto settingInfo = settingManager.GetSettingInfo(settingKey, category); + if (!settingInfo) + continue; + + uint32_t settingID = settingInfo->id; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", settingKey.c_str()); + ImGui::TableSetColumnIndex(1); + + switch (settingInfo->type) { + case SettingType::Bool: + { + bool v = settingManager.GetValue(settingID, true); + if (ImGui::Checkbox(("##" + settingKey).c_str(), &v)) { + settingManager.SetValue(settingID, v); + } + break; + } + case SettingType::Float: + { + float v = settingManager.GetValue(settingID, true); + if (ImGui::InputFloat(("##" + settingKey).c_str(), &v, settingInfo->step, settingInfo->step * 10.0f, "%.2f")) { + // Clamp value between min and max after input + v = std::clamp(v, settingInfo->minValue, settingInfo->maxValue); + settingManager.SetValue(settingID, v); + } + break; + } + case SettingType::TimeOfDay: + { + auto v = settingManager.GetValue(settingID, true); + bool exteriorOnly = settingManager.IsCategoryExteriorOnly(category); + + bool changed = false; + bool firstRow = true; + + for (int i = 0; i < 8; ++i) { + if (exteriorOnly && i >= 6) + continue; + + if (!firstRow) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TableSetColumnIndex(1); + } + firstRow = false; + + // Style the input based on activity + float blendFactor = GetTimeOfDayBlendFactor(i); + bool isActive = blendFactor > 0.0f; + + if (!isActive) { + // Inactive inputs: dim the appearance + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f); + } + + std::string label = std::string(timeOfDayNames[i]) + "##" + settingKey + std::to_string(i); + if (ImGui::InputFloat(label.c_str(), &v.values[i], settingInfo->step, settingInfo->step * 10.0f, "%.2f")) { + // Clamp value between min and max after input + v.values[i] = std::clamp(v.values[i], settingInfo->minValue, settingInfo->maxValue); + changed = true; + } + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%.0f%%", blendFactor * 100.0f); + } + + if (!isActive) { + ImGui::PopStyleVar(); + } + } + + if (changed) { + settingManager.SetValue(settingID, v); + } + break; + } + case SettingType::ColorTimeOfDay: + { + auto v = settingManager.GetValue(settingID, true); + bool exteriorOnly = settingManager.IsCategoryExteriorOnly(category); + + bool changed = false; + bool firstRow = true; + + for (int i = 0; i < 8; ++i) { + if (exteriorOnly && i >= 6) + continue; + + if (!firstRow) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TableSetColumnIndex(1); + } + firstRow = false; + + // Style the color picker based on activity + float blendFactor = GetTimeOfDayBlendFactor(i); + bool isActive = blendFactor > 0.0f; + + if (!isActive) { + // Inactive sliders: dim the appearance + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f); + } + + std::string label = std::string(timeOfDayNames[i]) + "##" + settingKey + std::to_string(i); + float color[3] = { v.values[i].x, v.values[i].y, v.values[i].z }; + + if (ImGui::ColorEdit3(label.c_str(), color, ImGuiColorEditFlags_NoInputs)) { + v.values[i].x = color[0]; + v.values[i].y = color[1]; + v.values[i].z = color[2]; + changed = true; + } + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%.0f%%", blendFactor * 100.0f); + } + + if (!isActive) { + ImGui::PopStyleVar(); + } + } + + if (changed) { + settingManager.SetValue(settingID, v); + } + break; + } + } + } + + ImGui::EndTable(); + } + } + } + ImGui::EndTabItem(); + } + } + ImGui::EndTabBar(); + } +} + diff --git a/src/Features/Effect11/MenuManager.h b/src/Features/Effect11/MenuManager.h new file mode 100644 index 0000000000..6f1c71dc58 --- /dev/null +++ b/src/Features/Effect11/MenuManager.h @@ -0,0 +1,26 @@ +#pragma once + +#include "EffectManager.h" +#include "WeatherManager.h" + +class MenuManager +{ +public: + static MenuManager& GetSingleton(); + + // Main UI rendering method + void RenderImGui(); + +private: + // UI section rendering methods + void RenderEffectsList(); + void RenderSettingsPanel(); + void RenderWeatherControl(); + void RenderDebugControl(); + + // Helper UI methods + void RenderAllSettings(); + std::map> GetCategorizedSettings() const; + std::vector GetActiveTimeOfDayIndices() const; + float GetTimeOfDayBlendFactor(int timeIndex) const; +}; \ No newline at end of file diff --git a/src/Features/Effect11/PresetManager.cpp b/src/Features/Effect11/PresetManager.cpp new file mode 100644 index 0000000000..469d1182bf --- /dev/null +++ b/src/Features/Effect11/PresetManager.cpp @@ -0,0 +1,26 @@ +#include "PresetManager.h" + +PresetManager& PresetManager::GetSingleton() +{ + static PresetManager instance; + return instance; +} + +bool PresetManager::UseDataFolder() const +{ + return std::filesystem::exists("Data\\enbseries.ini") && std::filesystem::exists("Data\\enbseries\\enbeffect.fx"); +} + +std::filesystem::path PresetManager::GetENBSeriesPath() const +{ + if (UseDataFolder()) + return "Data\\enbseries"; + return "enbseries"; +} + +std::filesystem::path PresetManager::GetENBSeriesIniPath() const +{ + if (UseDataFolder()) + return "Data\\enbseries.ini"; + return "enbseries.ini"; +} diff --git a/src/Features/Effect11/PresetManager.h b/src/Features/Effect11/PresetManager.h new file mode 100644 index 0000000000..383015a5a4 --- /dev/null +++ b/src/Features/Effect11/PresetManager.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +class PresetManager +{ +public: + static PresetManager& GetSingleton(); + + std::filesystem::path GetENBSeriesPath() const; + std::filesystem::path GetENBSeriesIniPath() const; + +private: + bool UseDataFolder() const; +}; diff --git a/src/Features/Effect11/SettingManager.cpp b/src/Features/Effect11/SettingManager.cpp new file mode 100644 index 0000000000..fce4458a43 --- /dev/null +++ b/src/Features/Effect11/SettingManager.cpp @@ -0,0 +1,1021 @@ +#include "SettingManager.h" + +#include "PresetManager.h" +#include "WeatherManager.h" +#include +#include +#include +#include +#include + +static const char* const timeOfDayNames[] = { "Dawn", "Sunrise", "Day", "Sunset", "Dusk", "Night", "InteriorDay", "InteriorNight" }; + +static bool TryParseBool(const std::string& a_value, bool& a_out) +{ + std::string s = a_value; + s.erase(0, s.find_first_not_of(" \t\r\n")); + s.erase(s.find_last_not_of(" \t\r\n") + 1); + std::transform(s.begin(), s.end(), s.begin(), ::tolower); + if (s == "true" || s == "1") { + a_out = true; + return true; + } + if (s == "false" || s == "0") { + a_out = false; + return true; + } + return false; +} + +static bool TryParseFloat(const std::string& a_value, float& a_out) +{ + if (a_value.empty()) + return false; + try { + size_t pos; + a_out = std::stof(a_value, &pos); + for (size_t i = pos; i < a_value.size(); ++i) { + if (!std::isspace(static_cast(a_value[i]))) { + return false; + } + } + return true; + } catch (...) { + return false; + } +} + +static bool TryParseWeatherID(const std::string& a_key, uint32_t& a_out) +{ + const std::string prefix = "weather_"; + if (a_key.size() <= prefix.size() || a_key.compare(0, prefix.size(), prefix) != 0) { + return false; + } + + try { + std::string idStr = a_key.substr(prefix.size()); + size_t pos; + a_out = std::stoul(idStr, &pos); + return pos == idStr.size(); + } catch (...) { + return false; + } +} + +SettingManager& SettingManager::GetSingleton() +{ + static SettingManager instance; + return instance; +} + +void SettingManager::RegisterSettingInternal(Setting& setting) +{ + // Already holding unique_lock from public Register... methods + if (categories.find(setting.category) == categories.end()) { + categoryOrder.push_back(setting.category); + } + + auto& cat = categories[setting.category]; + auto it = cat.settings.find(setting.key); + + if (it == cat.settings.end()) { + setting.id = static_cast(allSettings.size()); + setting.lastSavedValue = setting.currentValue; + cat.settings[setting.key] = setting.id; + cat.settingOrder.push_back(setting.key); + allSettings.push_back(setting); + } else { + // Update existing setting info but keep the same ID + uint32_t existingID = it->second; + setting.id = existingID; + setting.lastSavedValue = allSettings[existingID].lastSavedValue; + allSettings[existingID] = setting; + } +} + +void SettingManager::RegisterBoolSetting(const std::string& key, const std::string& category, + bool defaultValue, bool hasWeatherSupport) +{ + std::unique_lock lock(mutex); + Setting setting; + setting.key = key; + setting.category = category; + setting.type = SettingType::Bool; + setting.hasWeatherSupport = hasWeatherSupport; + setting.defaultValue = defaultValue; + setting.currentValue = defaultValue; + + RegisterSettingInternal(setting); +} + +void SettingManager::RegisterFloatSetting(const std::string& key, const std::string& category, + float defaultValue, float minValue, float maxValue, float step, bool hasWeatherSupport) +{ + std::unique_lock lock(mutex); + Setting setting; + setting.key = key; + setting.category = category; + setting.type = SettingType::Float; + setting.hasWeatherSupport = hasWeatherSupport; + setting.defaultValue = defaultValue; + setting.currentValue = defaultValue; + setting.minValue = minValue; + setting.maxValue = maxValue; + setting.step = step; + + RegisterSettingInternal(setting); +} + +void SettingManager::RegisterTimeOfDaySetting(const std::string& key, const std::string& category, + float defaultValue, float minValue, float maxValue, float step, bool hasWeatherSupport) +{ + std::unique_lock lock(mutex); + TimeOfDayValue timeOfDayDefault; + for (int i = 0; i < TimeOfDayValue::Total; ++i) { + timeOfDayDefault.values[i] = defaultValue; + } + + Setting setting; + setting.key = key; + setting.category = category; + setting.type = SettingType::TimeOfDay; + setting.hasWeatherSupport = hasWeatherSupport; + setting.defaultValue = timeOfDayDefault; + setting.currentValue = timeOfDayDefault; + setting.minValue = minValue; + setting.maxValue = maxValue; + setting.step = step; + + RegisterSettingInternal(setting); +} + +void SettingManager::RegisterColorTimeOfDaySetting(const std::string& key, const std::string& category, + float3 defaultValue, bool hasWeatherSupport) +{ + std::unique_lock lock(mutex); + ColorTimeOfDayValue colorTimeOfDayDefault; + for (int i = 0; i < ColorTimeOfDayValue::Total; ++i) { + colorTimeOfDayDefault.values[i] = defaultValue; + } + + Setting setting; + setting.key = key; + setting.category = category; + setting.type = SettingType::ColorTimeOfDay; + setting.hasWeatherSupport = hasWeatherSupport; + setting.defaultValue = colorTimeOfDayDefault; + setting.currentValue = colorTimeOfDayDefault; + + RegisterSettingInternal(setting); +} + +template +T SettingManager::GetValue(const std::string& key, const std::string& category, bool rawValue) +{ + std::shared_lock lock(mutex); + uint32_t id = GetSettingIDInternal(key, category); + if (id == 0xFFFFFFFF) { + return T{}; + } + return GetValueInternal(id, rawValue); +} + +template +T SettingManager::GetValue(uint32_t id, bool rawValue) +{ + std::shared_lock lock(mutex); + return GetValueInternal(id, rawValue); +} + +template +T SettingManager::GetValueInternal(uint32_t id, bool rawValue) const +{ + if (id >= allSettings.size()) { + return T{}; + } + + const auto& setting = allSettings[id]; + const auto& categorySettings = categories.at(setting.category); + + if (setting.hasWeatherSupport) { + bool isInterior = (timeOfDay2[2] + timeOfDay2[3]) > 0.5f; + bool shouldIgnoreWeather = isInterior ? + categorySettings.ignoreWeatherSystemInterior : + categorySettings.ignoreWeatherSystem; + + if (shouldIgnoreWeather) { + return std::get(setting.currentValue); + } + + auto currentIt = weatherData.find(currentWeatherID); + auto lastIt = weatherData.find(lastWeatherID); + + if (currentIt != weatherData.end() || lastIt != weatherData.end()) { + SettingValue currentValue = setting.currentValue; + SettingValue lastValue = setting.currentValue; + + if (currentIt != weatherData.end() && id < currentIt->second.size()) { + currentValue = currentIt->second[id]; + } + + if (lastIt != weatherData.end() && id < lastIt->second.size()) { + lastValue = lastIt->second[id]; + } + + if (rawValue) { + return std::get(weatherBlendFactor > 0.5f ? currentValue : lastValue); + } + + SettingValue blendedValue = InterpolateValues(lastValue, currentValue, weatherBlendFactor); + return std::get(blendedValue); + } + } + + return std::get(setting.currentValue); +} + +template +void SettingManager::SetValue(const std::string& key, const std::string& category, const T& value) +{ + std::unique_lock lock(mutex); + uint32_t id = GetSettingIDInternal(key, category); + if (id != 0xFFFFFFFF) { + SetValueInternal(id, value); + } +} + +template +void SettingManager::SetValue(uint32_t id, const T& value) +{ + std::unique_lock lock(mutex); + SetValueInternal(id, value); +} + +template +void SettingManager::SetValueInternal(uint32_t id, const T& value) +{ + if (id >= allSettings.size()) { + return; + } + + auto& setting = allSettings[id]; + const auto& categorySettings = categories.at(setting.category); + + if (setting.hasWeatherSupport) { + bool isInterior = (timeOfDay2[2] + timeOfDay2[3]) > 0.5f; + bool shouldIgnoreWeather = isInterior ? + categorySettings.ignoreWeatherSystemInterior : + categorySettings.ignoreWeatherSystem; + + if (shouldIgnoreWeather) { + setting.currentValue = value; + return; + } + + uint32_t targetWeatherID = (weatherBlendFactor > 0.5f) ? currentWeatherID : lastWeatherID; + + // Update all weather IDs sharing the same file + auto& weatherManager = WeatherManager::GetSingleton(); + auto* entry = weatherManager.FindWeatherEntry(targetWeatherID); + + if (entry) { + for (uint32_t linkedID : entry->weatherIDs) { + auto& data = weatherData[linkedID]; + if (data.size() < allSettings.size()) { + data.resize(allSettings.size()); + // Initialize with current values from allSettings + for (size_t i = 0; i < allSettings.size(); ++i) { + data[i] = allSettings[i].currentValue; + } + } + data[id] = value; + } + } else { + // Fallback: update target ID only + auto& data = weatherData[targetWeatherID]; + if (data.size() < allSettings.size()) { + data.resize(allSettings.size()); + for (size_t i = 0; i < allSettings.size(); ++i) { + data[i] = allSettings[i].currentValue; + } + } + data[id] = value; + } + return; + } + + setting.currentValue = value; +} + +uint32_t SettingManager::GetSettingID(const std::string& key, const std::string& category) const +{ + std::shared_lock lock(mutex); + return GetSettingIDInternal(key, category); +} + +uint32_t SettingManager::GetSettingIDInternal(const std::string& key, const std::string& category) const +{ + auto catIt = categories.find(category); + if (catIt != categories.end()) { + auto setIt = catIt->second.settings.find(key); + if (setIt != catIt->second.settings.end()) { + return setIt->second; + } + } + return 0xFFFFFFFF; +} + +float SettingManager::GetInterpolatedTimeOfDayValue(const std::string& key, const std::string& category) +{ + std::shared_lock lock(mutex); + uint32_t id = GetSettingIDInternal(key, category); + if (id == 0xFFFFFFFF) + return 0.0f; + TimeOfDayValue timeOfDayValue = GetValueInternal(id); + return ComputeTimeOfDayInterpolation(timeOfDayValue); +} + +float3 SettingManager::GetInterpolatedColorTimeOfDayValue(const std::string& key, const std::string& category) +{ + std::shared_lock lock(mutex); + uint32_t id = GetSettingIDInternal(key, category); + if (id == 0xFFFFFFFF) + return {}; + ColorTimeOfDayValue colorTimeOfDayValue = GetValueInternal(id); + return ComputeColorTimeOfDayInterpolation(colorTimeOfDayValue); +} + +bool SettingManager::HasSetting(const std::string& key, const std::string& category) const +{ + std::shared_lock lock(mutex); + auto categoryIt = categories.find(category); + return categoryIt != categories.end() && categoryIt->second.settings.find(key) != categoryIt->second.settings.end(); +} + +const Setting* SettingManager::GetSettingInfo(const std::string& key, const std::string& category) const +{ + std::shared_lock lock(mutex); + uint32_t id = GetSettingIDInternal(key, category); + if (id < allSettings.size()) + return &allSettings[id]; + return nullptr; +} + +const Setting* SettingManager::GetSettingInfo(uint32_t id) const +{ + std::shared_lock lock(mutex); + if (id < allSettings.size()) { + return &allSettings[id]; + } + return nullptr; +} + +std::vector SettingManager::GetSettingsByCategory(const std::string& category) const +{ + std::shared_lock lock(mutex); + auto categoryIt = categories.find(category); + if (categoryIt != categories.end()) { + return categoryIt->second.settingOrder; + } + return {}; +} + +std::vector SettingManager::GetAllCategories() const +{ + std::shared_lock lock(mutex); + return categoryOrder; +} + +bool SettingManager::CategoryHasWeatherSupport(const std::string& category) const +{ + std::shared_lock lock(mutex); + auto categoryIt = categories.find(category); + if (categoryIt == categories.end()) + return false; + + for (const auto& [key, id] : categoryIt->second.settings) { + if (allSettings[id].hasWeatherSupport) { + return true; + } + } + return false; +} + +void SettingManager::SetCategoryExteriorOnly(const std::string& category, bool exteriorOnly) +{ + std::unique_lock lock(mutex); + auto it = categories.find(category); + if (it != categories.end()) + it->second.exteriorOnly = exteriorOnly; +} + +bool SettingManager::IsCategoryExteriorOnly(const std::string& category) const +{ + std::shared_lock lock(mutex); + auto it = categories.find(category); + if (it == categories.end()) + return false; + return it->second.exteriorOnly; +} + +void SettingManager::SetWeatherBlendFactors(uint32_t newCurrentWeatherID, uint32_t newLastWeatherID, float blendFactor) +{ + std::unique_lock lock(mutex); + currentWeatherID = newCurrentWeatherID; + lastWeatherID = newLastWeatherID; + weatherBlendFactor = blendFactor; +} + +void SettingManager::LoadWeatherSettings(const std::vector& weatherIDs, const std::string& filePath) +{ + if (!std::filesystem::exists(filePath)) { + logger::warn("[SettingManager] Weather file not found: {}", filePath); + return; + } + + if (weatherIDs.empty()) { + return; + } + + auto writeTime = std::filesystem::last_write_time(filePath); + + // Snapshot allSettings and check for changes under a short shared lock + std::vector settingsCopy; + { + std::shared_lock lock(mutex); + auto it = weatherFileWriteTimes.find(filePath); + if (it != weatherFileWriteTimes.end() && it->second == writeTime) { + return; // File unchanged since last load + } + settingsCopy = allSettings; + } + + // Do all file I/O without holding any lock + std::vector loadedValues(settingsCopy.size()); + for (size_t i = 0; i < settingsCopy.size(); ++i) { + loadedValues[i] = settingsCopy[i].currentValue; + } + for (auto& setting : settingsCopy) { + if (setting.hasWeatherSupport) { + LoadSettingFromFile(filePath, setting.category, setting.key, setting); + loadedValues[setting.id] = setting.currentValue; + } + } + + // Store loaded values for all provided weather IDs under write lock + { + std::unique_lock lock(mutex); + weatherFileWriteTimes[filePath] = writeTime; + for (uint32_t weatherID : weatherIDs) { + weatherData[weatherID] = loadedValues; + lastSavedWeatherData[weatherID] = loadedValues; + } + } +} + +void SettingManager::SaveWeatherSettings(const std::string& weatherKey, const std::string& filePath) +{ + uint32_t weatherID; + if (!TryParseWeatherID(weatherKey, weatherID)) { + logger::error("[SettingManager] Invalid weather key: {}", weatherKey); + return; + } + + std::vector> settingsToWrite; + + { + std::unique_lock lock(mutex); + auto weatherIt = weatherData.find(weatherID); + if (weatherIt == weatherData.end()) { + return; + } + + auto lastIt = lastSavedWeatherData.find(weatherID); + const auto& weatherValues = weatherIt->second; + + for (const auto& setting : allSettings) { + if (setting.hasWeatherSupport && setting.id < weatherValues.size()) { + bool changed = true; + if (lastIt != lastSavedWeatherData.end() && setting.id < lastIt->second.size()) { + if (weatherValues[setting.id] == lastIt->second[setting.id]) { + changed = false; + } + } + + if (changed) { + Setting tempSetting = setting; + tempSetting.currentValue = weatherValues[setting.id]; + settingsToWrite.emplace_back(setting.category, setting.key, tempSetting); + } + } + } + + // Update last saved state + lastSavedWeatherData[weatherID] = weatherValues; + } + + if (settingsToWrite.empty()) { + return; + } + + // Perform IO outside of lock to prevent deadlocks + for (const auto& [category, key, setting] : settingsToWrite) { + SaveSettingToFile(filePath, category, key, setting); + } + + // Flush Windows .ini cache to disk + WritePrivateProfileStringA(NULL, NULL, NULL, filePath.c_str()); +} + +void SettingManager::SaveAllWeatherSettings() +{ + auto& weatherManager = WeatherManager::GetSingleton(); + const auto& weatherFiles = weatherManager.GetWeatherFiles(); + + // Deduplicate by file path to avoid redundant IO and overwrite bugs + std::unordered_map uniqueFiles; + for (const auto& [weatherKey, filePath] : weatherFiles) { + if (uniqueFiles.find(filePath) == uniqueFiles.end()) { + uniqueFiles[filePath] = weatherKey; + } + } + + for (const auto& [filePath, weatherKey] : uniqueFiles) { + SaveWeatherSettings(weatherKey, filePath); + } +} + +void SettingManager::ReloadAllWeatherSettings() +{ + { + std::unique_lock lock(mutex); + weatherData.clear(); + weatherFileWriteTimes.clear(); // Force re-read of all weather files + } + auto& weatherManager = WeatherManager::GetSingleton(); + weatherManager.Initialize(); +} + +void SettingManager::SetTimeOfDayData(const float newTimeOfDay1[4], const float newTimeOfDay2[4]) +{ + std::unique_lock lock(mutex); + memcpy(timeOfDay1, newTimeOfDay1, sizeof(timeOfDay1)); + memcpy(timeOfDay2, newTimeOfDay2, sizeof(timeOfDay2)); +} + +void SettingManager::LoadFromFile(const std::string& filePath) +{ + std::filesystem::path absPath = std::filesystem::absolute(filePath); + + if (!std::filesystem::exists(absPath)) { + logger::warn("[SettingManager] Settings file not found: {}, using defaults", absPath.string()); + return; + } + + auto writeTime = std::filesystem::last_write_time(absPath); + + // Snapshot settings and identify weather categories under brief shared lock + std::vector settingsCopy; + std::vector weatherCategories; + { + std::shared_lock lock(mutex); + if (writeTime == lastMainIniWriteTime) { + return; // File unchanged since last load + } + settingsCopy = allSettings; + for (const auto& [catName, catData] : categories) { + for (const auto& [key, settingID] : catData.settings) { + if (allSettings[settingID].hasWeatherSupport) { + weatherCategories.push_back(catName); + break; + } + } + } + } + + // Do all file I/O without holding any lock + std::string absPathStr = absPath.string(); + for (auto& setting : settingsCopy) { + LoadSettingFromFile(absPathStr, setting.category, setting.key, setting); + setting.lastSavedValue = setting.currentValue; + } + + // Load weather ignore settings (I/O without lock) + struct WeatherIgnoreData + { + std::string category; + bool ignoreWeatherSystem = false; + bool ignoreWeatherSystemInterior = true; + }; + std::vector weatherIgnoreResults; + for (const auto& category : weatherCategories) { + WeatherIgnoreData data; + data.category = category; + + char buffer[256]; + GetPrivateProfileStringA(category.c_str(), "IgnoreWeatherSystem", "false", buffer, sizeof(buffer), absPathStr.c_str()); + bool parsed; + if (TryParseBool(buffer, parsed)) { + data.ignoreWeatherSystem = parsed; + } + + GetPrivateProfileStringA(category.c_str(), "IgnoreWeatherSystemInterior", "true", buffer, sizeof(buffer), absPathStr.c_str()); + if (TryParseBool(buffer, parsed)) { + data.ignoreWeatherSystemInterior = parsed; + } + + weatherIgnoreResults.push_back(std::move(data)); + } + + // Store results under unique lock + { + std::unique_lock lock(mutex); + lastMainIniWriteTime = writeTime; + allSettings = std::move(settingsCopy); + + // Apply weather ignore settings + for (const auto& data : weatherIgnoreResults) { + auto catIt = categories.find(data.category); + if (catIt != categories.end()) { + catIt->second.ignoreWeatherSystem = data.ignoreWeatherSystem; + catIt->second.ignoreWeatherSystemInterior = data.ignoreWeatherSystemInterior; + } + } + + // Sync lastSaved state for all categories + for (auto& [catName, catData] : categories) { + catData.lastSavedIgnoreWeatherSystem = catData.ignoreWeatherSystem; + catData.lastSavedIgnoreWeatherSystemInterior = catData.ignoreWeatherSystemInterior; + } + + lastSavedWeatherData = weatherData; + } +} + +void SettingManager::SaveToFile(const std::string& filePath) +{ + std::vector> settingsToWrite; + std::vector> weatherSupportFlags; + + { + std::unique_lock lock(mutex); + for (auto& setting : allSettings) { + if (!(setting.currentValue == setting.lastSavedValue)) { + settingsToWrite.emplace_back(setting.category, setting.key, setting); + setting.lastSavedValue = setting.currentValue; + } + } + + for (const auto& categoryName : categoryOrder) { + auto& categoryData = categories.at(categoryName); + bool hasWeatherSupport = false; + for (const auto& [key, settingID] : categoryData.settings) { + if (allSettings[settingID].hasWeatherSupport) { + hasWeatherSupport = true; + break; + } + } + + if (hasWeatherSupport) { + if (categoryData.ignoreWeatherSystem != categoryData.lastSavedIgnoreWeatherSystem || + categoryData.ignoreWeatherSystemInterior != categoryData.lastSavedIgnoreWeatherSystemInterior) { + weatherSupportFlags.emplace_back(categoryName, categoryData.ignoreWeatherSystem, categoryData.ignoreWeatherSystemInterior); + categoryData.lastSavedIgnoreWeatherSystem = categoryData.ignoreWeatherSystem; + categoryData.lastSavedIgnoreWeatherSystemInterior = categoryData.ignoreWeatherSystemInterior; + } + } + } + } + + if (settingsToWrite.empty() && weatherSupportFlags.empty()) { + return; + } + + // Perform IO outside of lock + for (const auto& [category, key, setting] : settingsToWrite) { + SaveSettingToFile(filePath, category, key, setting); + } + + for (const auto& [category, ignoreOut, ignoreIn] : weatherSupportFlags) { + WritePrivateProfileStringA(category.c_str(), "IgnoreWeatherSystem", + ignoreOut ? "true" : "false", filePath.c_str()); + WritePrivateProfileStringA(category.c_str(), "IgnoreWeatherSystemInterior", + ignoreIn ? "true" : "false", filePath.c_str()); + } + + // Flush cache + WritePrivateProfileStringA(NULL, NULL, NULL, filePath.c_str()); +} + +SettingValue SettingManager::InterpolateValues(const SettingValue& a, const SettingValue& b, float t) const +{ + if (a.index() != b.index()) { + return t > 0.5f ? b : a; + } + + return std::visit([&](auto&& valA) -> SettingValue { + using T = std::decay_t; + const T& valB = std::get(b); + + if constexpr (std::is_same_v) { + return t > 0.5f ? valB : valA; + } else if constexpr (std::is_same_v) { + return valA + t * (valB - valA); + } else if constexpr (std::is_same_v) { + TimeOfDayValue result; + for (int i = 0; i < 8; ++i) { + result.values[i] = valA.values[i] + t * (valB.values[i] - valA.values[i]); + } + return result; + } else if constexpr (std::is_same_v) { + ColorTimeOfDayValue result; + for (int i = 0; i < 8; ++i) { + result.values[i] = valA.values[i] + t * (valB.values[i] - valA.values[i]); + } + return result; + } + return valB; + }, + a); +} + +float SettingManager::ComputeTimeOfDayInterpolation(const TimeOfDayValue& value) const +{ + return timeOfDay1[0] * value.values[TimeOfDayValue::Dawn] + + timeOfDay1[1] * value.values[TimeOfDayValue::Sunrise] + + timeOfDay1[2] * value.values[TimeOfDayValue::Day] + + timeOfDay1[3] * value.values[TimeOfDayValue::Sunset] + + timeOfDay2[0] * value.values[TimeOfDayValue::Dusk] + + timeOfDay2[1] * value.values[TimeOfDayValue::Night] + + timeOfDay2[2] * value.values[TimeOfDayValue::InteriorDay] + + timeOfDay2[3] * value.values[TimeOfDayValue::InteriorNight]; +} + +float3 SettingManager::ComputeColorTimeOfDayInterpolation(const ColorTimeOfDayValue& value) const +{ + return timeOfDay1[0] * value.values[ColorTimeOfDayValue::Dawn] + + timeOfDay1[1] * value.values[ColorTimeOfDayValue::Sunrise] + + timeOfDay1[2] * value.values[ColorTimeOfDayValue::Day] + + timeOfDay1[3] * value.values[ColorTimeOfDayValue::Sunset] + + timeOfDay2[0] * value.values[ColorTimeOfDayValue::Dusk] + + timeOfDay2[1] * value.values[ColorTimeOfDayValue::Night] + + timeOfDay2[2] * value.values[ColorTimeOfDayValue::InteriorDay] + + timeOfDay2[3] * value.values[ColorTimeOfDayValue::InteriorNight]; +} + +void SettingManager::LoadSettingFromFile(const std::string& filePath, const std::string& section, const std::string& key, Setting& setting) +{ + switch (setting.type) { + case SettingType::Bool: + { + bool defaultVal = std::get(setting.defaultValue); + char buffer[256]; + GetPrivateProfileStringA(section.c_str(), key.c_str(), defaultVal ? "true" : "false", buffer, sizeof(buffer), filePath.c_str()); + bool parsed; + if (TryParseBool(buffer, parsed)) { + setting.currentValue = parsed; + } else { + setting.currentValue = defaultVal; + } + break; + } + case SettingType::Float: + { + float defaultVal = std::get(setting.defaultValue); + char buffer[256]; + GetPrivateProfileStringA(section.c_str(), key.c_str(), std::to_string(defaultVal).c_str(), buffer, sizeof(buffer), filePath.c_str()); + float parsed; + if (TryParseFloat(buffer, parsed)) { + setting.currentValue = std::clamp(parsed, setting.minValue, setting.maxValue); + } else { + setting.currentValue = defaultVal; + } + break; + } + case SettingType::TimeOfDay: + { + TimeOfDayValue timeOfDayValue = std::get(setting.defaultValue); + + for (int i = 0; i < 8; ++i) { + std::string fullKey = key + timeOfDayNames[i]; + char buffer[256]; + std::string defaultStr = std::to_string(timeOfDayValue.values[i]); + GetPrivateProfileStringA(section.c_str(), fullKey.c_str(), defaultStr.c_str(), buffer, sizeof(buffer), filePath.c_str()); + float parsed; + if (TryParseFloat(buffer, parsed)) { + timeOfDayValue.values[i] = std::clamp(parsed, setting.minValue, setting.maxValue); + } + } + + setting.currentValue = timeOfDayValue; + break; + } + case SettingType::ColorTimeOfDay: + { + ColorTimeOfDayValue colorTimeOfDayValue = std::get(setting.defaultValue); + + for (int i = 0; i < 8; ++i) { + std::string fullKey = key + timeOfDayNames[i]; + char buffer[256]; + float3 defaultColor = colorTimeOfDayValue.values[i]; + std::string defaultStr = std::to_string(defaultColor.x) + ", " + + std::to_string(defaultColor.y) + ", " + + std::to_string(defaultColor.z); + + GetPrivateProfileStringA(section.c_str(), fullKey.c_str(), defaultStr.c_str(), buffer, sizeof(buffer), filePath.c_str()); + std::string valueStr = buffer; + + // Parse comma-separated float3 values + std::stringstream ss(valueStr); + std::string item; + std::vector components; + bool success = true; + + while (std::getline(ss, item, ',')) { + float parsed; + if (TryParseFloat(item, parsed)) { + components.push_back(std::clamp(parsed, setting.minValue, setting.maxValue)); + } else { + success = false; + break; + } + } + + // Ensure we have exactly 3 components and parsing was successful + if (success && components.size() == 3) { + colorTimeOfDayValue.values[i].x = components[0]; + colorTimeOfDayValue.values[i].y = components[1]; + colorTimeOfDayValue.values[i].z = components[2]; + } + } + + setting.currentValue = colorTimeOfDayValue; + break; + } + } +} + +void SettingManager::SaveSettingToFile(const std::string& filePath, const std::string& section, const std::string& key, const Setting& setting) +{ + auto formatFloat = [](float value) -> std::string { + char temp[32]; + sprintf_s(temp, "%.3f", value); + std::string result = temp; + + // Remove trailing zeros + while (result.length() > 1 && result.back() == '0') { + result.pop_back(); + } + + // Ensure at least one decimal place (add .0 if needed) + if (result.back() == '.') { + result += '0'; + } + + return result; + }; + + switch (setting.type) { + case SettingType::Bool: + { + bool value = std::get(setting.currentValue); + WritePrivateProfileStringA(section.c_str(), key.c_str(), value ? "true" : "false", filePath.c_str()); + break; + } + case SettingType::Float: + { + float value = std::get(setting.currentValue); + std::string formatted = formatFloat(value); + WritePrivateProfileStringA(section.c_str(), key.c_str(), formatted.c_str(), filePath.c_str()); + break; + } + case SettingType::TimeOfDay: + { + const TimeOfDayValue& timeOfDayValue = std::get(setting.currentValue); + + for (int i = 0; i < 8; ++i) { + std::string fullKey = key + timeOfDayNames[i]; + std::string formatted = formatFloat(timeOfDayValue.values[i]); + WritePrivateProfileStringA(section.c_str(), fullKey.c_str(), formatted.c_str(), filePath.c_str()); + } + break; + } + case SettingType::ColorTimeOfDay: + { + const ColorTimeOfDayValue& colorTimeOfDayValue = std::get(setting.currentValue); + + for (int i = 0; i < 8; ++i) { + std::string fullKey = key + timeOfDayNames[i]; + const auto& color = colorTimeOfDayValue.values[i]; + std::string formatted = formatFloat(color.x) + ", " + formatFloat(color.y) + ", " + formatFloat(color.z); + WritePrivateProfileStringA(section.c_str(), fullKey.c_str(), formatted.c_str(), filePath.c_str()); + } + break; + } + } +} + +void SettingManager::LoadWeatherIgnoreSettings(const std::string& filePath) +{ + // Internal helper, called from methods already holding a unique lock + for (auto& [category, categoryData] : categories) { + bool hasWeatherSupport = false; + for (const auto& [key, settingID] : categoryData.settings) { + if (allSettings[settingID].hasWeatherSupport) { + hasWeatherSupport = true; + break; + } + } + + if (hasWeatherSupport) { + char buffer1[256]; + GetPrivateProfileStringA(category.c_str(), "IgnoreWeatherSystem", "false", buffer1, sizeof(buffer1), filePath.c_str()); + bool parsed; + if (TryParseBool(buffer1, parsed)) { + categoryData.ignoreWeatherSystem = parsed; + } + + char buffer2[256]; + GetPrivateProfileStringA(category.c_str(), "IgnoreWeatherSystemInterior", "true", buffer2, sizeof(buffer2), filePath.c_str()); + if (TryParseBool(buffer2, parsed)) { + categoryData.ignoreWeatherSystemInterior = parsed; + } + } + + categoryData.lastSavedIgnoreWeatherSystem = categoryData.ignoreWeatherSystem; + categoryData.lastSavedIgnoreWeatherSystemInterior = categoryData.ignoreWeatherSystemInterior; + } +} + +bool SettingManager::GetIgnoreWeatherSystem(const std::string& category) const +{ + std::shared_lock lock(mutex); + auto categoryIt = categories.find(category); + return categoryIt != categories.end() ? categoryIt->second.ignoreWeatherSystem : false; +} + +bool SettingManager::GetIgnoreWeatherSystemInterior(const std::string& category) const +{ + std::shared_lock lock(mutex); + auto categoryIt = categories.find(category); + return categoryIt != categories.end() ? categoryIt->second.ignoreWeatherSystemInterior : true; +} + +void SettingManager::SetIgnoreWeatherSystem(const std::string& category, bool ignore) +{ + std::unique_lock lock(mutex); + categories[category].ignoreWeatherSystem = ignore; +} + +void SettingManager::SetIgnoreWeatherSystemInterior(const std::string& category, bool ignore) +{ + std::unique_lock lock(mutex); + categories[category].ignoreWeatherSystemInterior = ignore; +} + +void SettingManager::Load() +{ + { + std::unique_lock lock(mutex); + lastMainIniWriteTime = {}; + } + LoadFromFile(PresetManager::GetSingleton().GetENBSeriesIniPath().string()); + ReloadAllWeatherSettings(); +} + +void SettingManager::Save() +{ + SaveToFile(PresetManager::GetSingleton().GetENBSeriesIniPath().string()); + SaveAllWeatherSettings(); +} + +// Explicit template instantiations +template bool SettingManager::GetValue(const std::string& key, const std::string& category, bool rawValue); +template float SettingManager::GetValue(const std::string& key, const std::string& category, bool rawValue); +template TimeOfDayValue SettingManager::GetValue(const std::string& key, const std::string& category, bool rawValue); +template ColorTimeOfDayValue SettingManager::GetValue(const std::string& key, const std::string& category, bool rawValue); + +template void SettingManager::SetValue(const std::string& key, const std::string& category, const bool& value); +template void SettingManager::SetValue(const std::string& key, const std::string& category, const float& value); +template void SettingManager::SetValue(const std::string& key, const std::string& category, const TimeOfDayValue& value); +template void SettingManager::SetValue(const std::string& key, const std::string& category, const ColorTimeOfDayValue& value); + +template bool SettingManager::GetValue(uint32_t id, bool rawValue); +template float SettingManager::GetValue(uint32_t id, bool rawValue); +template TimeOfDayValue SettingManager::GetValue(uint32_t id, bool rawValue); +template ColorTimeOfDayValue SettingManager::GetValue(uint32_t id, bool rawValue); + +template void SettingManager::SetValue(uint32_t id, const bool& value); +template void SettingManager::SetValue(uint32_t id, const float& value); +template void SettingManager::SetValue(uint32_t id, const TimeOfDayValue& value); +template void SettingManager::SetValue(uint32_t id, const ColorTimeOfDayValue& value); + +template bool SettingManager::GetValueInternal(uint32_t id, bool rawValue) const; +template float SettingManager::GetValueInternal(uint32_t id, bool rawValue) const; +template TimeOfDayValue SettingManager::GetValueInternal(uint32_t id, bool rawValue) const; +template ColorTimeOfDayValue SettingManager::GetValueInternal(uint32_t id, bool rawValue) const; + +template void SettingManager::SetValueInternal(uint32_t id, const bool& value); +template void SettingManager::SetValueInternal(uint32_t id, const float& value); +template void SettingManager::SetValueInternal(uint32_t id, const TimeOfDayValue& value); +template void SettingManager::SetValueInternal(uint32_t id, const ColorTimeOfDayValue& value); \ No newline at end of file diff --git a/src/Features/Effect11/SettingManager.h b/src/Features/Effect11/SettingManager.h new file mode 100644 index 0000000000..5903433dd0 --- /dev/null +++ b/src/Features/Effect11/SettingManager.h @@ -0,0 +1,231 @@ +#pragma once + +#include +#include +#include + +enum class SettingType +{ + Bool, + Float, + TimeOfDay, + ColorTimeOfDay +}; + +// Shared time-of-day index lookup used by both TimeOfDayValue and ColorTimeOfDayValue +inline std::optional TimeOfDayIndexFromName(const std::string& name) +{ + static const std::pair lookup[] = { + { "Dawn", 0 }, { "Sunrise", 1 }, { "Day", 2 }, { "Sunset", 3 }, + { "Dusk", 4 }, { "Night", 5 }, { "InteriorDay", 6 }, { "InteriorNight", 7 } + }; + for (const auto& [n, idx] : lookup) { + if (name == n) + return idx; + } + logger::warn("[SettingManager] Unknown time-of-day name '{}', no default used", name); + return std::nullopt; +} + +struct TimeOfDayValue +{ + float values[8] = { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; + + enum Index + { + Dawn = 0, + Sunrise = 1, + Day = 2, + Sunset = 3, + Dusk = 4, + Night = 5, + InteriorDay = 6, + InteriorNight = 7, + Total = 8 + }; + + float& operator[](Index idx) { return values[idx]; } + const float& operator[](Index idx) const { return values[idx]; } + + bool operator==(const TimeOfDayValue& other) const + { + return std::equal(std::begin(values), std::end(values), std::begin(other.values)); + } + + float& GetByName(const std::string& name) + { + auto idx = TimeOfDayIndexFromName(name); + return values[idx.value_or(0)]; + } +}; + +struct ColorTimeOfDayValue +{ + float3 values[8] = { + { 1.0f, 1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f }, + { 1.0f, 1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f } + }; + + enum Index + { + Dawn = 0, + Sunrise = 1, + Day = 2, + Sunset = 3, + Dusk = 4, + Night = 5, + InteriorDay = 6, + InteriorNight = 7, + Total = 8 + }; + + float3& operator[](Index idx) { return values[idx]; } + const float3& operator[](Index idx) const { return values[idx]; } + + bool operator==(const ColorTimeOfDayValue& other) const + { + for (int i = 0; i < 8; ++i) { + if (values[i].x != other.values[i].x || values[i].y != other.values[i].y || values[i].z != other.values[i].z) { + return false; + } + } + return true; + } + + float3& GetByName(const std::string& name) + { + auto idx = TimeOfDayIndexFromName(name); + return values[idx.value_or(0)]; + } +}; + +using SettingValue = std::variant; + +struct Setting +{ + uint32_t id = 0; + std::string key; + std::string category; + SettingType type; + bool hasWeatherSupport; + SettingValue defaultValue; + SettingValue currentValue; + SettingValue lastSavedValue; + float minValue = 0.0f; + float maxValue = 10.0f; + float step = 0.01f; +}; + +class SettingManager +{ + friend class WeatherManager; + +public: + static SettingManager& GetSingleton(); + + // Setting registration + void RegisterBoolSetting(const std::string& key, const std::string& category, + bool defaultValue, bool hasWeatherSupport = false); + void RegisterFloatSetting(const std::string& key, const std::string& category, + float defaultValue, float minValue = 0.0f, float maxValue = 10.0f, float step = 0.01f, bool hasWeatherSupport = false); + void RegisterTimeOfDaySetting(const std::string& key, const std::string& category, + float defaultValue, float minValue = 0.0f, float maxValue = 10.0f, float step = 0.01f, bool hasWeatherSupport = false); + void RegisterColorTimeOfDaySetting(const std::string& key, const std::string& category, + float3 defaultValue, bool hasWeatherSupport = false); + + template + T GetValue(const std::string& key, const std::string& category, bool rawValue = false); + + template + T GetValue(uint32_t id, bool rawValue = false); + + template + void SetValue(const std::string& key, const std::string& category, const T& value); + + template + void SetValue(uint32_t id, const T& value); + + uint32_t GetSettingID(const std::string& key, const std::string& category) const; + + float GetInterpolatedTimeOfDayValue(const std::string& key, const std::string& category); + float3 GetInterpolatedColorTimeOfDayValue(const std::string& key, const std::string& category); + + bool HasSetting(const std::string& key, const std::string& category) const; + const Setting* GetSettingInfo(const std::string& key, const std::string& category) const; + const Setting* GetSettingInfo(uint32_t id) const; + std::vector GetSettingsByCategory(const std::string& category) const; + std::vector GetAllCategories() const; + bool CategoryHasWeatherSupport(const std::string& category) const; + void SetCategoryExteriorOnly(const std::string& category, bool exteriorOnly); + bool IsCategoryExteriorOnly(const std::string& category) const; + + // Weather integration + void SetWeatherBlendFactors(uint32_t currentWeatherID, uint32_t lastWeatherID, float blendFactor); + void LoadWeatherSettings(const std::vector& weatherIDs, const std::string& filePath); + void SaveWeatherSettings(const std::string& weatherKey, const std::string& filePath); + void SaveAllWeatherSettings(); + void ReloadAllWeatherSettings(); + + // File I/O + void LoadFromFile(const std::string& filePath); + void SaveToFile(const std::string& filePath); + + // Effect save/load coordination + void Load(); + void Save(); + + // Weather ignore settings management + void LoadWeatherIgnoreSettings(const std::string& filePath); + bool GetIgnoreWeatherSystem(const std::string& category) const; + bool GetIgnoreWeatherSystemInterior(const std::string& category) const; + void SetIgnoreWeatherSystem(const std::string& category, bool ignore); + void SetIgnoreWeatherSystemInterior(const std::string& category, bool ignore); + + // Time of day interpolation data + void SetTimeOfDayData(const float timeOfDay1[4], const float timeOfDay2[4]); + +private: + struct CategorySettings + { + std::unordered_map settings; // key -> ID + std::vector settingOrder; + bool ignoreWeatherSystem = false; + bool ignoreWeatherSystemInterior = true; + bool lastSavedIgnoreWeatherSystem = false; + bool lastSavedIgnoreWeatherSystemInterior = true; + bool exteriorOnly = false; + }; + + std::vector allSettings; + std::unordered_map categories; + std::vector categoryOrder; + std::unordered_map> weatherData; + std::unordered_map> lastSavedWeatherData; + + uint32_t currentWeatherID = 0; + uint32_t lastWeatherID = 0; + float weatherBlendFactor = 0.0f; + + float timeOfDay1[4] = { 0, 0, 0, 0 }; + float timeOfDay2[4] = { 0, 0, 0, 0 }; + + // INI file modification time tracking to skip redundant reloads + std::filesystem::file_time_type lastMainIniWriteTime{}; + std::unordered_map weatherFileWriteTimes; + + mutable std::shared_mutex mutex; + + void RegisterSettingInternal(Setting& setting); + + template + T GetValueInternal(uint32_t id, bool rawValue = false) const; + template + void SetValueInternal(uint32_t id, const T& value); + uint32_t GetSettingIDInternal(const std::string& key, const std::string& category) const; + + SettingValue InterpolateValues(const SettingValue& a, const SettingValue& b, float t) const; + float ComputeTimeOfDayInterpolation(const TimeOfDayValue& value) const; + float3 ComputeColorTimeOfDayInterpolation(const ColorTimeOfDayValue& value) const; + void LoadSettingFromFile(const std::string& filePath, const std::string& section, const std::string& key, Setting& setting); + void SaveSettingToFile(const std::string& filePath, const std::string& section, const std::string& key, const Setting& setting); +}; \ No newline at end of file diff --git a/src/Features/Effect11/TextureManager.cpp b/src/Features/Effect11/TextureManager.cpp new file mode 100644 index 0000000000..1ccf4cffaa --- /dev/null +++ b/src/Features/Effect11/TextureManager.cpp @@ -0,0 +1,363 @@ +#include "TextureManager.h" + +#include + +#include "EffectManager.h" +#include "Globals.h" +#include "State.h" + +TextureManager& TextureManager::GetSingleton() +{ + static TextureManager instance; + return instance; +} + +void TextureManager::Initialize() +{ + CreateCommonTextures(); + CreateDownsampleResources(); +} + +TextureManager::Texture* TextureManager::GetCommonTexture(const std::string& name) +{ + auto it = commonTextureCache.find(name); + if (it != commonTextureCache.end()) { + return &it->second; + } + return nullptr; +} + +void TextureManager::SwapTextures(const std::string& name1, const std::string& name2) +{ + auto it1 = commonTextureCache.find(name1); + auto it2 = commonTextureCache.find(name2); + if (it1 != commonTextureCache.end() && it2 != commonTextureCache.end()) { + std::swap(it1->second, it2->second); + } +} + +void TextureManager::CreateCommonTextures() +{ + auto state = globals::state; + UINT screenWidth = static_cast(state->screenSize.x); + UINT screenHeight = static_cast(state->screenSize.y); + + commonTextureCache.insert({ "TextureHDRTemp", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R16G16B16A16_FLOAT, "TextureManager::TextureHDRTemp") }); + commonTextureCache.insert({ "TextureHDRTemp2", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R16G16B16A16_FLOAT, "TextureManager::TextureHDRTemp2") }); + + commonTextureCache.insert({ "RenderTargetRGBA32", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R8G8B8A8_UNORM, "TextureManager::RenderTargetRGBA32") }); + commonTextureCache.insert({ "RenderTargetRGBA64", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R16G16B16A16_UNORM, "TextureManager::RenderTargetRGBA64") }); + commonTextureCache.insert({ "RenderTargetRGBA64F", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R16G16B16A16_FLOAT, "TextureManager::RenderTargetRGBA64F") }); + commonTextureCache.insert({ "RenderTargetR16F", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R16_FLOAT, "TextureManager::RenderTargetR16F") }); + commonTextureCache.insert({ "RenderTargetR32F", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R32_FLOAT, "TextureManager::RenderTargetR32F") }); + commonTextureCache.insert({ "RenderTargetRGB32F", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R11G11B10_FLOAT, "TextureManager::RenderTargetRGB32F") }); + + commonTextureCache.insert({ "TextureSDRTemp", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R10G10B10A2_UNORM, "TextureManager::TextureSDRTemp") }); + commonTextureCache.insert({ "TextureSDRTemp2", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R10G10B10A2_UNORM, "TextureManager::TextureSDRTemp2") }); + + commonTextureCache.insert({ "TextureBloom", CreateTexture(1024, 1024, DXGI_FORMAT_R16G16B16A16_FLOAT, "TextureManager::TextureBloom") }); + commonTextureCache.insert({ "TextureLens", CreateTexture(screenWidth, screenHeight, DXGI_FORMAT_R16G16B16A16_FLOAT, "TextureManager::TextureLens") }); + + commonTextureCache.insert({ "TextureBloomTemp", CreateTexture(1024, 1024, DXGI_FORMAT_R16G16B16A16_FLOAT, "TextureManager::TextureBloomLensTemp") }); + + commonTextureCache.insert({ "TextureAdaptation", CreateTexture(1, 1, DXGI_FORMAT_R32_FLOAT, "TextureManager::TextureAdaptation") }); + commonTextureCache.insert({ "TextureAdaptationSwap", CreateTexture(1, 1, DXGI_FORMAT_R32_FLOAT, "TextureManager::TextureAdaptationSwap") }); + + // Create fixed-size render targets for bloom/lens + std::vector> fixedSizes = { + { "RenderTarget1024", 1024 }, + { "RenderTarget512", 512 }, + { "RenderTarget256", 256 }, + { "RenderTarget128", 128 }, + { "RenderTarget64", 64 }, + { "RenderTarget32", 32 }, + { "RenderTarget16", 16 } + }; + + for (auto& [name, size] : fixedSizes) { + commonTextureCache[name] = CreateTexture(size, size, DXGI_FORMAT_R16G16B16A16_FLOAT, "TextureManager::" + name); + } +} + +TextureManager::Texture TextureManager::CreateTexture(uint32_t width, uint32_t height, DXGI_FORMAT format, const std::string& debugName) +{ + TextureManager::Texture result; + + D3D11_TEXTURE2D_DESC texDesc = {}; + texDesc.Width = width; + texDesc.Height = height; + texDesc.MipLevels = 1; + texDesc.ArraySize = 1; + texDesc.Format = format; + texDesc.SampleDesc.Count = 1; + texDesc.SampleDesc.Quality = 0; + texDesc.Usage = D3D11_USAGE_DEFAULT; + texDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; + texDesc.CPUAccessFlags = 0; + texDesc.MiscFlags = 0; + + DX::ThrowIfFailed(globals::d3d::device->CreateTexture2D(&texDesc, nullptr, result.texture.put())); + + if (!debugName.empty()) { + result.texture->SetPrivateData(WKPDID_D3DDebugObjectName, static_cast(debugName.length()), debugName.c_str()); + } + + D3D11_RENDER_TARGET_VIEW_DESC rtvDesc = {}; + rtvDesc.Format = format; + rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D; + rtvDesc.Texture2D.MipSlice = 0; + + DX::ThrowIfFailed(globals::d3d::device->CreateRenderTargetView(result.texture.get(), &rtvDesc, result.rtv.put())); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.Format = format; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MostDetailedMip = 0; + srvDesc.Texture2D.MipLevels = 1; + + DX::ThrowIfFailed(globals::d3d::device->CreateShaderResourceView(result.texture.get(), &srvDesc, result.srv.put())); + + return result; +} + +void TextureManager::CreateDownsampleResources() +{ + auto device = globals::d3d::device; + + // Create linear sampler + D3D11_SAMPLER_DESC samplerDesc = {}; + samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.ComparisonFunc = D3D11_COMPARISON_NEVER; + samplerDesc.MinLOD = 0; + samplerDesc.MaxLOD = D3D11_FLOAT32_MAX; + + DX::ThrowIfFailed(device->CreateSamplerState(&samplerDesc, linearSampler.put())); + + // Create downsample vertex shader + auto vertexShaderSource = EffectManager::LoadShaderFile("Data\\Shaders\\Effect11\\QuadVS.hlsl"); + if (vertexShaderSource.empty()) + return; + + winrt::com_ptr vertexShaderBlob; + winrt::com_ptr vertexErrorBlob; + + HRESULT vsResult = D3DCompile( + vertexShaderSource.data(), + vertexShaderSource.size(), + "QuadVS.hlsl", + nullptr, + nullptr, + "main", + "vs_5_0", + 0, + 0, + vertexShaderBlob.put(), + vertexErrorBlob.put()); + + if (FAILED(vsResult)) { + if (vertexErrorBlob) { + logger::error("[TextureManager] Downsample vertex shader compilation failed: {}", + static_cast(vertexErrorBlob->GetBufferPointer())); + } + return; + } + + DX::ThrowIfFailed(device->CreateVertexShader( + vertexShaderBlob->GetBufferPointer(), + vertexShaderBlob->GetBufferSize(), + nullptr, + downsampleVS.put())); + + // Create downsample pixel shader + auto pixelShaderSource = EffectManager::LoadShaderFile("Data\\Shaders\\Effect11\\DownsamplePS.hlsl"); + if (pixelShaderSource.empty()) + return; + + winrt::com_ptr pixelShaderBlob; + winrt::com_ptr errorBlob; + + HRESULT result = D3DCompile( + pixelShaderSource.data(), + pixelShaderSource.size(), + "DownsamplePS.hlsl", + nullptr, + nullptr, + "main", + "ps_5_0", + 0, + 0, + pixelShaderBlob.put(), + errorBlob.put()); + + if (FAILED(result)) { + if (errorBlob) { + logger::error("[TextureManager] Downsample shader compilation failed: {}", + static_cast(errorBlob->GetBufferPointer())); + } + return; + } + + DX::ThrowIfFailed(device->CreatePixelShader( + pixelShaderBlob->GetBufferPointer(), + pixelShaderBlob->GetBufferSize(), + nullptr, + downsamplePS.put())); + + // Create Kawase blur pixel shader + auto blurPixelShaderSource = EffectManager::LoadShaderFile("Data\\Shaders\\Effect11\\KawaseBlurPS.hlsl"); + if (blurPixelShaderSource.empty()) + return; + + winrt::com_ptr blurShaderBlob; + winrt::com_ptr blurErrorBlob; + + HRESULT blurResult = D3DCompile( + blurPixelShaderSource.data(), + blurPixelShaderSource.size(), + "KawaseBlurPS.hlsl", + nullptr, + nullptr, + "main", + "ps_5_0", + 0, + 0, + blurShaderBlob.put(), + blurErrorBlob.put()); + + if (FAILED(blurResult)) { + if (blurErrorBlob) { + logger::error("[TextureManager] Blur shader compilation failed: {}", + static_cast(blurErrorBlob->GetBufferPointer())); + } + return; + } + + DX::ThrowIfFailed(device->CreatePixelShader( + blurShaderBlob->GetBufferPointer(), + blurShaderBlob->GetBufferSize(), + nullptr, + blurPS.put())); + + // Create shared downsample texture + sharedDownsampleTexture = CreateDownsampleTexture(DXGI_FORMAT_R11G11B10_FLOAT); + + // Create temp texture for pre-blur downsample + downsampleTempTexture = CreateTexture(1024, 1024, DXGI_FORMAT_R11G11B10_FLOAT, "TextureManager::DownsampleTemp"); +} + +TextureManager::DownsampleTexture TextureManager::CreateDownsampleTexture(DXGI_FORMAT format) +{ + auto device = globals::d3d::device; + + DownsampleTexture fixedTexture; + + D3D11_TEXTURE2D_DESC texDesc = {}; + texDesc.Width = 1024; + texDesc.Height = 1024; + texDesc.MipLevels = 3; // 1024, 512, 256 + texDesc.ArraySize = 1; + texDesc.Format = format; + texDesc.SampleDesc.Count = 1; + texDesc.SampleDesc.Quality = 0; + texDesc.Usage = D3D11_USAGE_DEFAULT; + texDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; + texDesc.CPUAccessFlags = 0; + texDesc.MiscFlags = D3D11_RESOURCE_MISC_GENERATE_MIPS; + + DX::ThrowIfFailed(device->CreateTexture2D(&texDesc, nullptr, fixedTexture.texture.put())); + + D3D11_RENDER_TARGET_VIEW_DESC rtvDesc = {}; + rtvDesc.Format = format; + rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D; + rtvDesc.Texture2D.MipSlice = 0; + + DX::ThrowIfFailed(device->CreateRenderTargetView(fixedTexture.texture.get(), &rtvDesc, fixedTexture.rtv.put())); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.Format = format; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MipLevels = 3; + srvDesc.Texture2D.MostDetailedMip = 0; + DX::ThrowIfFailed(device->CreateShaderResourceView(fixedTexture.texture.get(), &srvDesc, fixedTexture.srvChain.put())); + + srvDesc.Texture2D.MipLevels = 1; + DX::ThrowIfFailed(device->CreateShaderResourceView(fixedTexture.texture.get(), &srvDesc, fixedTexture.srv.put())); + + srvDesc.Texture2D.MostDetailedMip = 2; + DX::ThrowIfFailed(device->CreateShaderResourceView(fixedTexture.texture.get(), &srvDesc, fixedTexture.srvBlurry.put())); + + // Set debug names + Util::SetResourceName(fixedTexture.texture.get(), "TextureManager::DownsampleTexture (1024x1024, 3 mips)"); + Util::SetResourceName(fixedTexture.rtv.get(), "TextureManager::DownsampleTexture RTV"); + Util::SetResourceName(fixedTexture.srvChain.get(), "TextureManager::DownsampleTexture SRV Chain"); + Util::SetResourceName(fixedTexture.srv.get(), "TextureManager::DownsampleTexture SRV 1024x1024"); + Util::SetResourceName(fixedTexture.srvBlurry.get(), "TextureManager::DownsampleTexture SRV 256x256"); + + logger::info("[TextureManager] Created downsample texture: 1024x1024 with 3 mips (1024, 512, 256)"); + + return fixedTexture; +} + +void TextureManager::DownsampleToFixed(ID3D11ShaderResourceView* source, DownsampleTexture& texture) +{ + if (!source || !texture.rtv || !downsampleVS || !downsamplePS || !blurPS || !linearSampler || !texture.srvChain || !downsampleTempTexture.rtv) { + return; + } + + auto context = globals::d3d::context; + + D3D11_VIEWPORT viewport = {}; + viewport.Width = 1024.0f; + viewport.Height = 1024.0f; + viewport.MinDepth = 0.0f; + viewport.MaxDepth = 1.0f; + context->RSSetViewports(1, &viewport); + + context->VSSetShader(downsampleVS.get(), nullptr, 0); + + ID3D11SamplerState* samplerArray[] = { linearSampler.get() }; + context->PSSetSamplers(0, 1, samplerArray); + + // Pass 1: Downsample source into temp texture + ID3D11RenderTargetView* tempRTV[] = { downsampleTempTexture.rtv.get() }; + context->OMSetRenderTargets(1, tempRTV, nullptr); + context->PSSetShaderResources(0, 1, &source); + context->PSSetShader(downsamplePS.get(), nullptr, 0); + globals::profiler->BeginPass("Effect11::Downsample"); + context->Draw(4, 0); + globals::profiler->EndPass(); + + // Pass 2: Kawase blur from temp into final texture + ID3D11ShaderResourceView* nullSRV[] = { nullptr }; + context->PSSetShaderResources(0, 1, nullSRV); + + ID3D11RenderTargetView* finalRTV[] = { texture.rtv.get() }; + context->OMSetRenderTargets(1, finalRTV, nullptr); + + ID3D11ShaderResourceView* tempSRV[] = { downsampleTempTexture.srv.get() }; + context->PSSetShaderResources(0, 1, tempSRV); + context->PSSetShader(blurPS.get(), nullptr, 0); + globals::profiler->BeginPass("Effect11::DownsampleBlur"); + context->Draw(4, 0); + globals::profiler->EndPass(); + + context->GenerateMips(texture.srvChain.get()); +} + +void TextureManager::UpdateDownsampledTexture(ID3D11ShaderResourceView* source) +{ + DownsampleToFixed(source, sharedDownsampleTexture); +} + +ID3D11ShaderResourceView* TextureManager::GetDownsampleTexture() const +{ + return sharedDownsampleTexture.srv.get(); +} + +ID3D11ShaderResourceView* TextureManager::GetDownsampleTextureBlurry() const +{ + return sharedDownsampleTexture.srvBlurry.get(); +} \ No newline at end of file diff --git a/src/Features/Effect11/TextureManager.h b/src/Features/Effect11/TextureManager.h new file mode 100644 index 0000000000..43a4cc7e98 --- /dev/null +++ b/src/Features/Effect11/TextureManager.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include + +class TextureManager +{ +public: + struct Texture + { + winrt::com_ptr texture; + winrt::com_ptr rtv; + winrt::com_ptr srv; + }; + + struct DownsampleTexture + { + winrt::com_ptr texture; + winrt::com_ptr srvChain; // Mip 0 -> Mip 1 -> Mip2 + winrt::com_ptr srv; // Mip 0: 1024x1024 + winrt::com_ptr srvBlurry; // Mip 2: 256x256 + winrt::com_ptr rtv; + }; + + static TextureManager& GetSingleton(); + + void Initialize(); + Texture* GetCommonTexture(const std::string& name); + const std::unordered_map& GetAllCommonTextures() const { return commonTextureCache; } + + void SwapTextures(const std::string& name1, const std::string& name2); + + // Downsampled texture methods + void UpdateDownsampledTexture(ID3D11ShaderResourceView* source); + ID3D11ShaderResourceView* GetDownsampleTexture() const; + ID3D11ShaderResourceView* GetDownsampleTextureBlurry() const; + + // Frame-based state access + uint32_t GetTextureSwap() const { return textureSwap; } + void IncrementTextureSwap() { textureSwap++; } + +private: + void CreateCommonTextures(); + void CreateDownsampleResources(); + static Texture CreateTexture(uint32_t width, uint32_t height, DXGI_FORMAT format, const std::string& debugName); + static DownsampleTexture CreateDownsampleTexture(DXGI_FORMAT format); + void DownsampleToFixed(ID3D11ShaderResourceView* source, DownsampleTexture& texture); + + std::unordered_map commonTextureCache; + + // Downsampling resources + winrt::com_ptr downsampleVS; + winrt::com_ptr downsamplePS; + winrt::com_ptr blurPS; + + winrt::com_ptr linearSampler; + DownsampleTexture sharedDownsampleTexture; + Texture downsampleTempTexture; + + // Frame-based state + uint32_t textureSwap = 0; +}; \ No newline at end of file diff --git a/src/Features/Effect11/WeatherManager.cpp b/src/Features/Effect11/WeatherManager.cpp new file mode 100644 index 0000000000..a45eac48cd --- /dev/null +++ b/src/Features/Effect11/WeatherManager.cpp @@ -0,0 +1,284 @@ +#include "WeatherManager.h" + +#include "EffectManager.h" +#include "PresetManager.h" +#include "SettingManager.h" +#include +#include +#include + +WeatherManager& WeatherManager::GetSingleton() +{ + static WeatherManager instance; + return instance; +} + +void WeatherManager::Initialize() +{ + LoadWeatherList(); + LoadLocationWeather(); +} + +void WeatherManager::LoadWeatherList() +{ + std::filesystem::path weatherListPath = PresetManager::GetSingleton().GetENBSeriesPath() / "_weatherlist.ini"; + weatherEntries.clear(); + weatherIDMap.clear(); + + if (!std::filesystem::exists(weatherListPath)) { + logger::warn("[WeatherManager] _weatherlist.ini not found at {}", weatherListPath.string()); + return; + } + + // Use GetPrivateProfileString to enumerate sections + std::string weatherListPathStr = weatherListPath.string(); + + // Get all section names + constexpr DWORD bufferSize = 32768; + std::vector buffer(bufferSize); + DWORD result = GetPrivateProfileSectionNamesA(buffer.data(), bufferSize, weatherListPathStr.c_str()); + + if (result == 0 || result == bufferSize - 2) { + logger::error("[WeatherManager] Failed to read sections from _weatherlist.ini"); + return; + } + + // Parse section names (null-separated strings) + const char* ptr = buffer.data(); + while (*ptr != '\0') { + std::string sectionName = ptr; + ptr += sectionName.length() + 1; + + // Skip non-weather sections + if (sectionName.find("WEATHER") != 0) { + continue; + } + + // Get filename + char fileName[MAX_PATH] = {}; + GetPrivateProfileStringA(sectionName.c_str(), "FileName", "", fileName, MAX_PATH, weatherListPathStr.c_str()); + if (strlen(fileName) == 0) { + continue; // Skip empty weather entries + } + + // Get weather IDs + char weatherIDsStr[1024] = {}; + GetPrivateProfileStringA(sectionName.c_str(), "WeatherIDs", "", weatherIDsStr, 1024, weatherListPathStr.c_str()); + if (strlen(weatherIDsStr) == 0) { + continue; // Skip entries without weather IDs + } + + WeatherEntry entry; + entry.fileName = fileName; + ParseWeatherIDs(weatherIDsStr, entry.weatherIDs); + + // Load the weather file through SettingManager once for all associated IDs + std::filesystem::path weatherFilePath = PresetManager::GetSingleton().GetENBSeriesPath() / entry.fileName; + if (std::filesystem::exists(weatherFilePath)) { + SettingManager::GetSingleton().LoadWeatherSettings(entry.weatherIDs, weatherFilePath.string()); + } else { + logger::warn("[WeatherManager] Weather file not found: {}", weatherFilePath.string()); + } + + weatherEntries[sectionName] = std::move(entry); + for (uint32_t weatherID : weatherEntries[sectionName].weatherIDs) { + weatherIDMap[weatherID] = sectionName; + } + } +} + +WeatherManager::WeatherEntry* WeatherManager::FindWeatherEntry(uint32_t weatherID) +{ + auto& effectManager = EffectManager::GetSingleton(); + if (!SettingManager::GetSingleton().GetValueInternal(effectManager.ids.enableMultipleWeathers)) { + return nullptr; + } + + auto it = weatherIDMap.find(weatherID); + if (it != weatherIDMap.end()) { + auto entryIt = weatherEntries.find(it->second); + if (entryIt != weatherEntries.end()) { + return &entryIt->second; + } + } + return nullptr; +} + +void WeatherManager::ParseWeatherIDs(const std::string& weatherIDsStr, std::vector& weatherIDs) +{ + weatherIDs.clear(); + + std::stringstream ss(weatherIDsStr); + std::string token; + + while (std::getline(ss, token, ',')) { + // Trim whitespace + token.erase(0, token.find_first_not_of(" \t")); + token.erase(token.find_last_not_of(" \t") + 1); + + if (!token.empty()) { + try { + uint32_t weatherID = ParseHexID(token); + if (weatherID != 0) { + weatherIDs.push_back(weatherID); + } + } catch (const std::exception& e) { + logger::warn("[WeatherManager] Failed to parse weather ID '{}': {}", token, e.what()); + } + } + } +} + +uint32_t WeatherManager::ParseHexID(const std::string& hexStr) +{ + if (hexStr.empty()) { + return 0; + } + + return static_cast(std::stoul(hexStr, nullptr, 16)); +} + +void WeatherManager::LoadLocationWeather() +{ + std::filesystem::path locationWeatherPath = PresetManager::GetSingleton().GetENBSeriesPath() / "_locationweather.ini"; + locationWeatherMap.clear(); + + if (!std::filesystem::exists(locationWeatherPath)) { + logger::info("[WeatherManager] _locationweather.ini not found, location weather disabled"); + return; + } + + std::string pathStr = locationWeatherPath.string(); + + constexpr DWORD bufferSize = 32768; + std::vector buffer(bufferSize); + DWORD result = GetPrivateProfileSectionNamesA(buffer.data(), bufferSize, pathStr.c_str()); + + if (result == 0 || result == bufferSize - 2) { + logger::error("[WeatherManager] Failed to read sections from _locationweather.ini"); + return; + } + + const char* ptr = buffer.data(); + while (*ptr != '\0') { + std::string sectionName = ptr; + ptr += sectionName.length() + 1; + + uint32_t worldSpaceID = 0; + try { + worldSpaceID = ParseHexID(sectionName); + } catch (...) { + continue; + } + + std::vector sectionBuffer(bufferSize); + DWORD sectionResult = GetPrivateProfileSectionA(sectionName.c_str(), sectionBuffer.data(), bufferSize, pathStr.c_str()); + + if (sectionResult == 0 || sectionResult == bufferSize - 2) { + continue; + } + + const char* entryPtr = sectionBuffer.data(); + while (*entryPtr != '\0') { + std::string entry = entryPtr; + entryPtr += entry.length() + 1; + + if (entry.empty() || entry[0] == '/' || entry[0] == ';') { + continue; + } + + size_t eqPos = entry.find('='); + if (eqPos == std::string::npos) { + continue; + } + + std::string locationStr = entry.substr(0, eqPos); + std::string weatherStr = entry.substr(eqPos + 1); + + try { + uint32_t locationID = ParseHexID(locationStr); + uint32_t fakeWeatherID = ParseHexID(weatherStr); + if (locationID != 0 && fakeWeatherID != 0) { + locationWeatherMap[worldSpaceID][locationID] = fakeWeatherID; + } + } catch (...) { + continue; + } + } + } + + logger::info("[WeatherManager] Loaded location weather for {} worldspaces", locationWeatherMap.size()); +} + +uint32_t WeatherManager::GetEffectiveWeatherID(uint32_t actualWeatherID) +{ + auto& effectManager = EffectManager::GetSingleton(); + if (!SettingManager::GetSingleton().GetValue(effectManager.ids.enableLocationWeather)) { + return actualWeatherID; + } + + if (locationWeatherMap.empty()) { + return actualWeatherID; + } + + auto player = RE::PlayerCharacter::GetSingleton(); + if (!player) { + return actualWeatherID; + } + + RE::TESObjectCELL* parentCell = nullptr; + try { + parentCell = player->GetParentCell(); + } catch (...) { + return actualWeatherID; + } + + if (!parentCell) { + return actualWeatherID; + } + + uint32_t worldSpaceID = 0; + uint32_t locationID = 0; + + try { + if (auto worldSpace = parentCell->GetRuntimeData().worldSpace) { + worldSpaceID = worldSpace->GetFormID() & 0x00FFFFFF; + } + + if (auto location = parentCell->GetLocation()) { + locationID = location->GetFormID() & 0x00FFFFFF; + } + } catch (...) { + return actualWeatherID; + } + + if (locationID == 0) { + return actualWeatherID; + } + + auto worldIt = locationWeatherMap.find(worldSpaceID); + if (worldIt == locationWeatherMap.end()) { + return actualWeatherID; + } + + auto locIt = worldIt->second.find(locationID); + if (locIt != worldIt->second.end()) { + return locIt->second; + } + + return actualWeatherID; +} + +std::unordered_map WeatherManager::GetWeatherFiles() const +{ + std::unordered_map result; + + for (const auto& [sectionName, entry] : weatherEntries) { + std::string weatherFilePath = (PresetManager::GetSingleton().GetENBSeriesPath() / entry.fileName).string(); + for (uint32_t weatherID : entry.weatherIDs) { + result["weather_" + std::to_string(weatherID)] = weatherFilePath; + } + } + + return result; +} \ No newline at end of file diff --git a/src/Features/Effect11/WeatherManager.h b/src/Features/Effect11/WeatherManager.h new file mode 100644 index 0000000000..8edc67b485 --- /dev/null +++ b/src/Features/Effect11/WeatherManager.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include + +class WeatherManager +{ +public: + static WeatherManager& GetSingleton(); + + struct WeatherEntry + { + std::string fileName; + std::vector weatherIDs; + }; + + void Initialize(); + void LoadWeatherList(); + void LoadLocationWeather(); + + WeatherEntry* FindWeatherEntry(uint32_t weatherID); + + /// @brief Gets the effective weather ID, checking for location-based overrides first. + /// @param actualWeatherID The real weather form ID from the game + /// @return Location-mapped weather ID if applicable, otherwise the actual weather ID + uint32_t GetEffectiveWeatherID(uint32_t actualWeatherID); + + const std::unordered_map& GetWeatherEntries() const { return weatherEntries; } + + std::unordered_map GetWeatherFiles() const; + +private: + std::unordered_map weatherEntries; + std::unordered_map weatherIDMap; + + // Location weather: worldSpaceID -> (locationID -> fakeWeatherID) + std::unordered_map> locationWeatherMap; + + void ParseWeatherIDs(const std::string& weatherIDsStr, std::vector& weatherIDs); + uint32_t ParseHexID(const std::string& hexStr); +}; \ No newline at end of file diff --git a/src/Features/ExponentialHeightFog.cpp b/src/Features/ExponentialHeightFog.cpp index ddf40c2e3f..9cc58c8d87 100644 --- a/src/Features/ExponentialHeightFog.cpp +++ b/src/Features/ExponentialHeightFog.cpp @@ -1,5 +1,7 @@ #include "ExponentialHeightFog.h" +#include "Effect11.h" +#include "Effect11/SettingManager.h" #include "WeatherVariableRegistry.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( @@ -35,8 +37,30 @@ void ExponentialHeightFog::SaveSettings(json& o_json) o_json = settings; } +ExponentialHeightFog::Settings ExponentialHeightFog::GetCommonBufferData() const +{ + Settings data = settings; + + if (globals::features::effect11.loaded) { + auto& enb = globals::features::effect11; + if (enb.enableEffect) { + data.enabled = 0; + } + } + + return data; +} + void ExponentialHeightFog::DrawSettings() { + if (globals::features::effect11.loaded) { + auto& enb = globals::features::effect11; + if (enb.enableEffect) { + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Settings are currently managed by ENB."); + return; + } + } + ImGui::Checkbox("Enable Exponential Height Fog", (bool*)&settings.enabled); Util::WeatherUI::SliderFloat("Start Distance", this, "startDistance", &settings.startDistance, 0.0f, 100000.0f, "%.1f"); Util::WeatherUI::SliderFloat("Fog Height", this, "fogHeight", &settings.fogHeight, -22000.0f, 22000.0f, "%.1f"); @@ -68,6 +92,13 @@ void ExponentialHeightFog::DrawSettings() void ExponentialHeightFog::RegisterWeatherVariables() { + if (globals::features::effect11.loaded) { + auto& enb = globals::features::effect11; + if (enb.enableEffect) { + return; + } + } + auto* registry = WeatherVariables::GlobalWeatherRegistry::GetSingleton()->GetOrCreateFeatureRegistry(GetShortName()); registry->RegisterVariable(std::make_shared( "Start Distance", diff --git a/src/Features/ExponentialHeightFog.h b/src/Features/ExponentialHeightFog.h index 2d2df0314d..4668a4b8e2 100644 --- a/src/Features/ExponentialHeightFog.h +++ b/src/Features/ExponentialHeightFog.h @@ -27,14 +27,6 @@ struct ExponentialHeightFog : Feature virtual inline std::string_view GetShaderDefineName() override { return "EXP_HEIGHT_FOG"; } bool HasShaderDefine(RE::BSShader::Type) override { return true; }; - virtual void DrawSettings() override; - - virtual void RestoreDefaultSettings() override; - virtual void LoadSettings(json& o_json) override; - virtual void SaveSettings(json& o_json) override; - - void RegisterWeatherVariables() override; - struct alignas(16) Settings { uint enabled = 0; @@ -55,4 +47,14 @@ struct ExponentialHeightFog : Feature float3 pad; } settings; static_assert(sizeof(Settings) == sizeof(float4) * 6, "Settings must match HLSL ExponentialHeightFogSettings."); + + virtual void DrawSettings() override; + + Settings GetCommonBufferData() const; + + virtual void RestoreDefaultSettings() override; + virtual void LoadSettings(json& o_json) override; + virtual void SaveSettings(json& o_json) override; + + void RegisterWeatherVariables() override; }; diff --git a/src/Features/GrassCollision.cpp b/src/Features/GrassCollision.cpp index 3dacd62da4..6f0feae96c 100644 --- a/src/Features/GrassCollision.cpp +++ b/src/Features/GrassCollision.cpp @@ -1,5 +1,6 @@ #include "GrassCollision.h" +#include "Globals.h" #include "State.h" #include "Utils/ActorUtils.h" #include "Utils/D3D.h" @@ -396,7 +397,9 @@ void GrassCollision::UpdateCollisionTexture() context->CSSetUnorderedAccessViews(0, ARRAYSIZE(uavs), uavs, nullptr); context->CSSetShader(GetCollisionUpdateCS(), nullptr, 0); + globals::profiler->BeginPass("GrassCollision::CollisionUpdate"); context->Dispatch(512 / 8, 512 / 8, 1); + globals::profiler->EndPass(); } context->CSSetShader(nullptr, nullptr, 0); diff --git a/src/Features/HDRDisplay.cpp b/src/Features/HDRDisplay.cpp index b691bf1481..48bf45e52d 100644 --- a/src/Features/HDRDisplay.cpp +++ b/src/Features/HDRDisplay.cpp @@ -1031,7 +1031,9 @@ void HDRDisplay::ApplyHDR() } context->CSSetShader(computeShader, nullptr, 0); + globals::profiler->BeginPass("HDRDisplay::HDROutput"); context->Dispatch(dispatchCount.x, dispatchCount.y, 1); + globals::profiler->EndPass(); views[0] = nullptr; views[1] = nullptr; @@ -1285,7 +1287,9 @@ void HDRDisplay::ScaleUIBrightnessForFG() auto computeShader = GetUIBrightnessCS(); if (computeShader) { context->CSSetShader(computeShader, nullptr, 0); + globals::profiler->BeginPass("HDRDisplay::UIBrightness"); context->Dispatch(dispatchCount.x, dispatchCount.y, 1); + globals::profiler->EndPass(); } // Cleanup @@ -1367,7 +1371,7 @@ void HDRDisplay::UpdateHDRData() const data.skipUIComposite = skipUIComposite ? 1.f : 0.f; data.uiBrightness = settings.hdrUIBrightness; data.isSceneLinear = isSceneLinear ? 1.f : 0.f; - data.pad0 = isMainOrLoadingMenu ? 1.f : 0.f; + data.isMainOrLoadingMenu = isMainOrLoadingMenu ? 1.f : 0.f; // TweenMenu = pause UI. ScaleUIBrightnessForFG skips while GameIsPaused(), so HDROutputCS applies the same mid-alpha boost when compositing gamma UI. data.fgTweenMenuMidAlphaBoost = (ui && ui->IsMenuOpen(RE::TweenMenu::MENU_NAME)) ? 1.f : 0.f; hdrDataCB->Update(data); diff --git a/src/Features/HDRDisplay.h b/src/Features/HDRDisplay.h index 589ca1aca7..a8a2396b8c 100644 --- a/src/Features/HDRDisplay.h +++ b/src/Features/HDRDisplay.h @@ -100,7 +100,7 @@ struct HDRDisplay : public Feature float skipUIComposite; ///< 1.0 = FG handles UI, skip our compositing float uiBrightness; ///< UI brightness multiplier (Frame Gen compositing) float isSceneLinear; ///< 1.0 = Linear Lighting active, scene already linear - float pad0; ///< 1.0 = main menu/loading screen active + float isMainOrLoadingMenu; ///< 1.0 = main menu/loading screen active float fgTweenMenuMidAlphaBoost; ///< 1.0 = TweenMenu (pause) open — FG UIBrightnessCS mid-alpha boost only }; diff --git a/src/Features/IBL.cpp b/src/Features/IBL.cpp index dfeafd7ded..7fd3646b14 100644 --- a/src/Features/IBL.cpp +++ b/src/Features/IBL.cpp @@ -6,6 +6,9 @@ #include "State.h" #include "WeatherVariableRegistry.h" +#include "Effect11.h" +#include "Effect11/SettingManager.h" + #include #include @@ -25,10 +28,19 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void IBL::DrawSettings() { - Util::WeatherUI::Checkbox("Enable IBL", this, "EnableIBL", (bool*)&settings.EnableIBL); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Toggle IBL. When enabled, ambient lighting is derived from cubemap spherical harmonics instead of the vanilla system."); + if (globals::features::effect11.loaded) { + auto& enb = globals::features::effect11; + if (enb.enableEffect) { + auto& settingManager = SettingManager::GetSingleton(); + if (settingManager.GetValue("EnableImageBasedLighting", "EFFECT")) { + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Settings are currently managed by ENB."); + return; + } + } } + + Util::WeatherUI::Checkbox("Enable IBL", this, "EnableIBL", (bool*)&settings.EnableIBL); + Util::WeatherUI::SliderFloat("Env IBL Scale", this, "EnvIBLScale", &settings.EnvIBLScale, 0.0f, 10.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("Intensity multiplier for the environment IBL (from Dynamic Cubemaps).\nControls how strongly the surrounding environment contributes to ambient lighting."); @@ -82,6 +94,7 @@ void IBL::DrawSettings() if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("Disables IBL in interior cells."); } + } void IBL::LoadSettings(json& o_json) @@ -101,6 +114,16 @@ void IBL::RestoreDefaultSettings() void IBL::RegisterWeatherVariables() { + if (globals::features::effect11.loaded) { + auto& enb = globals::features::effect11; + if (enb.enableEffect) { + auto& settingManager = SettingManager::GetSingleton(); + if (settingManager.GetValue("EnableImageBasedLighting", "EFFECT")) { + return; + } + } + } + auto* registry = WeatherVariables::GlobalWeatherRegistry::GetSingleton() ->GetOrCreateFeatureRegistry(GetShortName()); // Toggle IBL for this weather (SH-based ambient replaces vanilla) @@ -172,6 +195,24 @@ void IBL::RegisterWeatherVariables() IBL::Settings IBL::GetCommonBufferData() const { Settings data = settings; + + if (globals::features::effect11.loaded) { + auto& enb = globals::features::effect11; + if (enb.enableEffect) { + auto& settingManager = SettingManager::GetSingleton(); + if (settingManager.GetValue("EnableImageBasedLighting", "EFFECT")) { + data.EnableIBL = !Util::IsInterior(); + data.EnvIBLScale = 0.0f; + data.SkyIBLScale = settingManager.GetInterpolatedTimeOfDayValue("MultiplicativeAmount", "IMAGEBASEDLIGHTING"); + data.DALCAmount = 1.0f; + data.EnvIBLSaturation = 1.0f; + data.SkyIBLSaturation = 1.0f; + data.DALCMode = 3; + data.FogAmount = 0.0f; + } + } + } + if (settings.DisableInInteriors && Util::IsInterior()) data.EnableIBL = 0; return data; @@ -226,9 +267,10 @@ void IBL::Prepass() context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); context->CSSetShader(GetDiffuseIBLCS(), nullptr, 0); + globals::profiler->BeginPass("IBL::EnvDiffuseIBL"); context->Dispatch(1, 1, 1); + globals::profiler->EndPass(); } else { - // Still need to set sampler and shader for sky IBL dispatch below context->CSSetSamplers(0, (uint)samplers.size(), samplers.data()); context->CSSetShader(GetDiffuseIBLCS(), nullptr, 0); } @@ -242,7 +284,9 @@ void IBL::Prepass() context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + globals::profiler->BeginPass("IBL::SkyDiffuseIBL"); context->Dispatch(1, 1, 1); + globals::profiler->EndPass(); } // Reset diff --git a/src/Features/LightLimitFix.cpp b/src/Features/LightLimitFix.cpp index c7528fe44c..73f1ebd14c 100644 --- a/src/Features/LightLimitFix.cpp +++ b/src/Features/LightLimitFix.cpp @@ -1,4 +1,5 @@ #include "LightLimitFix.h" +#include "Effect11.h" #include "InverseSquareLighting.h" #include "LinearLighting.h" @@ -50,6 +51,7 @@ void LightLimitFix::DrawSettings() ImGui::TreePop(); } + } void LightLimitFix::DrawOverlay() @@ -248,6 +250,10 @@ void LightLimitFix::BSLightingShader_SetupGeometry_GeometrySetupConstantPointLig light.fade *= bsLight->lodDimmer; + auto& enbpp = globals::features::effect11; + if (inWorld && enbpp.enableEffect) + enbpp.OverridePointLightColor(light.color); + SetLightPosition(light, niLight->world.translate, inWorld); if (i < a_pass->numShadowLights) { @@ -432,6 +438,10 @@ void LightLimitFix::UpdateLights() light.fade *= bsLight->lodDimmer; + auto& enbpp = globals::features::effect11; + if (enbpp.enableEffect) + enbpp.OverridePointLightColor(light.color); + if (!IsGlobalLight(bsLight)) { // List of BSMultiBoundRooms affected by a light for (const auto& roomPtr : bsLight->rooms) { @@ -513,7 +523,9 @@ void LightLimitFix::UpdateStructure() context->CSSetUnorderedAccessViews(0, 1, &clusters_uav, nullptr); context->CSSetShader(clusterBuildingCS, nullptr, 0); + globals::profiler->BeginPass("LightLimitFix::ClusterBuild"); context->Dispatch(clusterSize[0], clusterSize[1], clusterSize[2]); + globals::profiler->EndPass(); ID3D11UnorderedAccessView* null_uav = nullptr; context->CSSetUnorderedAccessViews(0, 1, &null_uav, nullptr); @@ -539,7 +551,9 @@ void LightLimitFix::UpdateStructure() context->CSSetUnorderedAccessViews(0, ARRAYSIZE(uavs), uavs, nullptr); context->CSSetShader(clusterCullingCS, nullptr, 0); + globals::profiler->BeginPass("LightLimitFix::ClusterCull"); context->Dispatch((clusterSize[0] + 15) / 16, (clusterSize[1] + 15) / 16, (clusterSize[2] + 3) / 4); + globals::profiler->EndPass(); } context->CSSetShader(nullptr, nullptr, 0); diff --git a/src/Features/LinearLighting.cpp b/src/Features/LinearLighting.cpp index 6b15280c43..9089115c5b 100644 --- a/src/Features/LinearLighting.cpp +++ b/src/Features/LinearLighting.cpp @@ -2,6 +2,10 @@ #include "State.h" +#include "Effect11.h" +#include "Effect11/SettingManager.h" +#include "Utils/Game.h" + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( LinearLighting::Settings, enableLinearLighting, @@ -32,6 +36,14 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void LinearLighting::DrawSettings() { + if (globals::features::effect11.loaded) { + auto& enb = globals::features::effect11; + if (enb.enableEffect) { + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Settings are currently managed by ENB."); + return; + } + } + ImGui::Checkbox("Enable Linear Lighting", (bool*)&settings.enableLinearLighting); if (ImGui::BeginTabBar("##LinearLightingTabs", ImGuiTabBarFlags_None)) { @@ -114,7 +126,8 @@ void LinearLighting::Prepass() if (!imageSpaceManager) return; - dirLightMult = !globals::game::isVR ? imageSpaceManager->GetRuntimeData().data.baseData.hdr.sunlightScale : imageSpaceManager->GetVRRuntimeData().data.baseData.hdr.sunlightScale; + GET_INSTANCE_MEMBER(data, imageSpaceManager); + dirLightMult = data.baseData.hdr.sunlightScale; } struct LinearLighting::Hooks @@ -165,6 +178,26 @@ LinearLighting::PerFrameData LinearLighting::GetCommonBufferData() data.skyGamma = settings.skyGamma; data.waterGamma = settings.waterGamma; data.vlGamma = settings.vlGamma; + + if (globals::features::effect11.loaded) { + auto& enb = globals::features::effect11; + if (enb.enableEffect) { + data.enableLinearLighting = false; + data.lightGamma = 1.0f; + data.colorGamma = 1.0f; + data.emitColorGamma = 1.0f; + data.glowmapGamma = 1.0f; + data.ambientGamma = 1.0f; + data.fogGamma = 1.0f; + data.fogAlphaGamma = 1.0f; + data.effectGamma = 1.0f; + data.effectAlphaGamma = 1.0f; + data.skyGamma = 1.0f; + data.waterGamma = 1.0f; + data.vlGamma = 1.0f; + } + } + data.vanillaDiffuseColorMult = settings.vanillaDiffuseColorMult; data.directionalLightMult = settings.directionalLightMult; data.pointLightMult = settings.pointLightMult; @@ -177,6 +210,25 @@ LinearLighting::PerFrameData LinearLighting::GetCommonBufferData() data.projectedEffectMult = settings.projectedEffectMult; data.deferredEffectMult = settings.deferredEffectMult; data.otherEffectMult = settings.otherEffectMult; + + // Override multipliers to neutral values when ENB PP is active + if (globals::features::effect11.loaded) { + auto& enb = globals::features::effect11; + if (enb.enableEffect) { + data.vanillaDiffuseColorMult = 1.0f; + data.directionalLightMult = 1.0f; + data.pointLightMult = 1.0f; + data.ambientMult = 1.0f; + data.emitColorMult = 1.0f; + data.glowmapMult = 1.0f; + data.effectLightingMult = 1.0f; + data.membraneEffectMult = 1.0f; + data.bloodEffectMult = 1.0f; + data.projectedEffectMult = 1.0f; + data.deferredEffectMult = 1.0f; + data.otherEffectMult = 1.0f; + } + } return data; } diff --git a/src/Features/PerformanceOverlay.cpp b/src/Features/PerformanceOverlay.cpp index 957e591d02..e2c9a3d226 100644 --- a/src/Features/PerformanceOverlay.cpp +++ b/src/Features/PerformanceOverlay.cpp @@ -24,6 +24,7 @@ #include "Features/Upscaling.h" #include "Globals.h" #include "Menu.h" +#include "Menu/StatisticsRenderer.h" #include "State.h" #include "Utils/FileSystem.h" #include "Utils/Format.h" @@ -103,6 +104,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( ShowInOverlay, ShowDrawCalls, ShowVRAM, + ShowCSPasses, ShowFPS, ShowPreFGFrameTimeGraph, ShowPostFGFrameTimeGraph, @@ -171,6 +173,7 @@ void PerformanceOverlay::DrawSettings() ImGui::Checkbox("Show FPS Counter", &this->settings.ShowFPS); ImGui::Checkbox("Show Draw Calls", &this->settings.ShowDrawCalls); ImGui::Checkbox("Show VRAM Usage", &this->settings.ShowVRAM); + ImGui::Checkbox("Show CS Render Passes", &this->settings.ShowCSPasses); bool isFrameGenerationActive = globals::features::upscaling.IsFrameGenerationActive(); if (this->settings.ShowFPS && isFrameGenerationActive) { @@ -383,6 +386,12 @@ void PerformanceOverlay::DrawOverlay() DrawDrawCallsTable(mainRows, summaryRows); } + // CS Render Passes (GPU-timed) + if (this->settings.ShowCSPasses) { + ImGui::Separator(); + StatisticsRenderer::RenderStatistics(); + } + // VRAM & GPU Usage if (this->settings.ShowVRAM && menu->GetDXGIAdapter3()) { DrawVRAM(); @@ -492,6 +501,7 @@ void PerformanceOverlay::DrawFPS() } } + void PerformanceOverlay::DrawVRAM() { auto menu = Menu::GetSingleton(); diff --git a/src/Features/PerformanceOverlay.h b/src/Features/PerformanceOverlay.h index d29fc6bebd..c773ae460b 100644 --- a/src/Features/PerformanceOverlay.h +++ b/src/Features/PerformanceOverlay.h @@ -267,6 +267,7 @@ struct PerformanceOverlay : OverlayFeature bool ShowInOverlay = true; // was: Enabled bool ShowDrawCalls = true; + bool ShowCSPasses = true; bool ShowVRAM = true; bool ShowFPS = true; bool ShowPreFGFrameTimeGraph = true; diff --git a/src/Features/ScreenSpaceGI.cpp b/src/Features/ScreenSpaceGI.cpp index 5a021c3cd8..f40020466f 100644 --- a/src/Features/ScreenSpaceGI.cpp +++ b/src/Features/ScreenSpaceGI.cpp @@ -342,6 +342,7 @@ void ScreenSpaceGI::DrawSettings() ImGui::TreePop(); } + } void ScreenSpaceGI::LoadSettings(json& o_json) @@ -779,7 +780,9 @@ void ScreenSpaceGI::DrawSSGI() context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); context->CSSetShader(prefilterDepthsCompute.get(), nullptr, 0); + globals::profiler->BeginPass("ScreenSpaceGI::PrefilterDepths"); context->Dispatch((resolution[0] + 15) >> 4, (resolution[1] + 15) >> 4, 1); + globals::profiler->EndPass(); } // fetch radiance and disocclusion @@ -809,7 +812,9 @@ void ScreenSpaceGI::DrawSSGI() context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); context->CSSetShader(radianceDisoccCompute.get(), nullptr, 0); + globals::profiler->BeginPass("ScreenSpaceGI::RadianceDisocc"); context->Dispatch((internalRes[0] + 7u) >> 3, (internalRes[1] + 7u) >> 3, 1); + globals::profiler->EndPass(); // Prefilter radiance texture instead of using GenerateMips for proper dynamic resolution handling. // radianceDisocc wrote mip 0 directly to texRadianceTemp above, so we can bind it @@ -828,7 +833,9 @@ void ScreenSpaceGI::DrawSSGI() context->CSSetShaderResources(0, 1, srvs.data()); context->CSSetUnorderedAccessViews(0, 5, uavs.data(), nullptr); context->CSSetShader(prefilterRadianceCompute.get(), nullptr, 0); + globals::profiler->BeginPass("ScreenSpaceGI::PrefilterRadiance"); context->Dispatch((internalRes[0] + 15u) >> 4, (internalRes[1] + 15u) >> 4, 1); + globals::profiler->EndPass(); } inputAoTexIdx = !inputAoTexIdx; @@ -851,7 +858,9 @@ void ScreenSpaceGI::DrawSSGI() context->CSSetShaderResources(0, 1, srvs.data()); context->CSSetUnorderedAccessViews(0, 5, uavs.data(), nullptr); context->CSSetShader(prefilterNormalCompute.get(), nullptr, 0); + globals::profiler->BeginPass("ScreenSpaceGI::PrefilterNormals"); context->Dispatch((internalRes[0] + 15u) >> 4, (internalRes[1] + 15u) >> 4, 1); + globals::profiler->EndPass(); } // GI @@ -878,7 +887,9 @@ void ScreenSpaceGI::DrawSSGI() context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); context->CSSetShader(giCompute.get(), nullptr, 0); + globals::profiler->BeginPass("ScreenSpaceGI::GI"); context->Dispatch((internalRes[0] + 7u) >> 3, (internalRes[1] + 7u) >> 3, 1); + globals::profiler->EndPass(); inputAoTexIdx = !inputAoTexIdx; inputGITexIdx = !inputGITexIdx; @@ -904,7 +915,9 @@ void ScreenSpaceGI::DrawSSGI() context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); context->CSSetShader(blurCompute.get(), nullptr, 0); + globals::profiler->BeginPass("ScreenSpaceGI::Blur"); context->Dispatch((internalRes[0] + 7u) >> 3, (internalRes[1] + 7u) >> 3, 1); + globals::profiler->EndPass(); inputGITexIdx = !inputGITexIdx; lastFrameGITexIdx = inputGITexIdx; @@ -932,7 +945,9 @@ void ScreenSpaceGI::DrawSSGI() context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); context->CSSetShader(stereoSyncCompute.get(), nullptr, 0); + globals::profiler->BeginPass("ScreenSpaceGI::StereoSync"); context->Dispatch((internalRes[0] + 7u) >> 3, (internalRes[1] + 7u) >> 3, 1); + globals::profiler->EndPass(); inputAoTexIdx = !inputAoTexIdx; inputGITexIdx = !inputGITexIdx; @@ -958,7 +973,9 @@ void ScreenSpaceGI::DrawSSGI() context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); context->CSSetShader(upsampleCompute.get(), nullptr, 0); + globals::profiler->BeginPass("ScreenSpaceGI::Upsample"); context->Dispatch((resolution[0] + 7u) >> 3, (resolution[1] + 7u) >> 3, 1); + globals::profiler->EndPass(); inputAoTexIdx = !inputAoTexIdx; inputGITexIdx = !inputGITexIdx; diff --git a/src/Features/ScreenSpaceShadows.cpp b/src/Features/ScreenSpaceShadows.cpp index e4418db016..71065a36a0 100644 --- a/src/Features/ScreenSpaceShadows.cpp +++ b/src/Features/ScreenSpaceShadows.cpp @@ -55,6 +55,7 @@ void ScreenSpaceShadows::DrawSettings() ImGui::Spacing(); ImGui::TreePop(); } + } void ScreenSpaceShadows::InvalidateRaymarchShaders() @@ -195,12 +196,8 @@ void ScreenSpaceShadows::DrawShadows() // Shared dispatch logic for both VR and non-VR auto DispatchEye = [&](const char* eyeName, ID3D11ComputeShader* shader, const float* lightProj, float invTexSizeX, float invTexSizeY) { - if (globals::state->frameAnnotations && eyeName) { - std::string eventName = std::format("SSS - Ray March ({})", eyeName); - globals::state->BeginPerfEvent(eventName); - } else if (globals::state->frameAnnotations) { - globals::state->BeginPerfEvent("SSS - Ray March"); - } + std::string timerName = eyeName ? std::format("ScreenSpaceShadows::RayMarch({})", eyeName) : "ScreenSpaceShadows::RayMarch"; + globals::profiler->BeginPass(timerName); context->CSSetShader(shader, nullptr, 0); @@ -240,9 +237,7 @@ void ScreenSpaceShadows::DrawShadows() } } - if (globals::state->frameAnnotations) { - globals::state->EndPerfEvent(); - } + globals::profiler->EndPass(); }; float InvTexSizeX = 1.0f / (float)viewportSize[0]; @@ -296,10 +291,8 @@ void ScreenSpaceShadows::DrawStereoSync() ZoneScoped; TracyD3D11Zone(globals::state->tracyCtx, "SSS - Stereo Sync"); - if (globals::state->frameAnnotations) - globals::state->BeginPerfEvent("SSS - Stereo Sync"); - auto context = globals::d3d::context; + globals::profiler->BeginPass("ScreenSpaceShadows::StereoSync"); context->CopyResource(stereoSyncCopyTex->resource.get(), screenSpaceShadowsTexture->resource.get()); @@ -339,8 +332,7 @@ void ScreenSpaceShadows::DrawStereoSync() context->CSSetConstantBuffers(1, 1, &cbPtr); context->CSSetShader(nullptr, nullptr, 0); - if (globals::state->frameAnnotations) - globals::state->EndPerfEvent(); + globals::profiler->EndPass(); } void ScreenSpaceShadows::Prepass() diff --git a/src/Features/SkySync.cpp b/src/Features/SkySync.cpp index 76d7b149d8..67b63ab53c 100644 --- a/src/Features/SkySync.cpp +++ b/src/Features/SkySync.cpp @@ -7,11 +7,12 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( MoonLightSource, SunPath, CustomAngle, - SunriseBeginOffset, - SunriseEndOffset, - SunsetBeginOffset, - SunsetEndOffset, - MinShadowElevation) + MinShadowElevation, + ShadowTransitionDuration, + DimSunlightUnderHorizon, + NewMoonIntensity, + CrescentMoonIntensity, + FullMoonIntensity) void SkySync::DrawSettings() { @@ -50,26 +51,65 @@ void SkySync::DrawSettings() if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("The minimum angle sunlight will set to. Caps shadow length. Higher = shorter shadows at sunset/sunrise."); } - ImGui::Spacing(); - ImGui::Spacing(); - if (ImGui::TreeNodeEx("Sun Position Offsets", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::TextWrapped("Moves sun height during sunrise/sunset. Reset weather to see changes."); - ImGui::SliderFloat("Sunrise Begin (Hours)", &settings.SunriseBeginOffset, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_AlwaysClamp); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Offset for when the sun starts rising."); - } - ImGui::SliderFloat("Sunrise End (Hours)", &settings.SunriseEndOffset, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_AlwaysClamp); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Offset for when the sun finishes rising."); - } - ImGui::SliderFloat("Sunset Begin (Hours)", &settings.SunsetBeginOffset, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_AlwaysClamp); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Offset for when the sun starts setting."); - } - ImGui::SliderFloat("Sunset End (Hours)", &settings.SunsetEndOffset, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_AlwaysClamp); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Offset for when the sun finishes setting."); + + ImGui::SliderFloat("Shadow Transition Duration", &settings.ShadowTransitionDuration, 0.0f, 500.0f, "%.0f", ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("How long (in game-time units) the shadow direction takes to fade between sources. 100 = ~5 seconds at timescale 20."); + } + + ImGui::Checkbox("Dim Sunlight Under Horizon", &settings.DimSunlightUnderHorizon); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::TextUnformatted("Fade directional light to zero as the sun goes below the horizon."); + } + + ImGui::SliderFloat("New Moon Intensity", &settings.NewMoonIntensity, 0.0f, 1.0f, "%.3f", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderFloat("Crescent Intensity", &settings.CrescentMoonIntensity, 0.0f, 1.0f, "%.3f", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderFloat("Full Moon Intensity", &settings.FullMoonIntensity, 0.0f, 1.0f, "%.3f", ImGuiSliderFlags_AlwaysClamp); + + + if (ImGui::TreeNodeEx("Debug", ImGuiTreeNodeFlags_None)) { + static constexpr const char* CasterNames[] = { "Sun", "Masser", "Secunda", "None" }; + static constexpr const char* PhaseNames[] = { "Full", "Waning Gibbous", "Waning Quarter", "Waning Crescent", "New", "Waxing Crescent", "Waxing Quarter", "Waxing Gibbous" }; + + auto getPhase = [](const RE::Moon* moon) -> const char* { + if (!moon || !moon->moonMesh) + return "Unknown"; + if (const auto prop = skyrim_cast(moon->moonMesh->GetGeometryRuntimeData().shaderProperty.get())) { + if (auto tex = prop->GetBaseTexture()) + return PhaseNames[static_cast(Util::Moon::GetPhaseFromTexture(tex->name.c_str()))]; + } + return "Unknown"; + }; + + auto drawMoonEntry = [&](const char* label, Caster caster, const char* phase) { + auto& color = colors[static_cast(caster)]; + ImVec4 swatch = { color.x, color.y, color.z, 1.0f }; + ImGui::ColorButton(label, swatch, ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoPicker, { ImGui::GetTextLineHeight(), ImGui::GetTextLineHeight() }); + ImGui::SameLine(); + ImGui::Text("%s [%s] color (%.3f, %.3f, %.3f, %.3f)", label, phase, color.x, color.y, color.z, color.w); + }; + + const auto sky = globals::game::sky; + drawMoonEntry("Masser", Caster::Masser, sky ? getPhase(sky->masser) : "Unknown"); + drawMoonEntry("Secunda", Caster::Secunda, sky ? getPhase(sky->secunda) : "Unknown"); + + ImGui::Text("Dim: %.3f", currentDim); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + ImGui::Text("Shadow target: %s", CasterNames[static_cast(shadowFader.target)]); + ImGui::Text("Shadow dir: (%.2f, %.2f, %.2f)", shadowFader.currentDir.x, shadowFader.currentDir.y, shadowFader.currentDir.z); + if (shadowFader.transitioning) { + const float t = settings.ShadowTransitionDuration > 0.0f ? shadowFader.fadeTimer / settings.ShadowTransitionDuration : 1.0f; + ImGui::ProgressBar(t, { -1.0f, 0.0f }, ""); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::Text("Transitioning %.0f%%", t * 100.0f); + } else { + ImGui::TextDisabled("No transition"); } + ImGui::TreePop(); } } @@ -80,10 +120,6 @@ void SkySync::LoadSettings(json& o_json) settings.MoonLightSource = std::clamp(settings.MoonLightSource, static_cast(MoonLightSource::Brightest), static_cast(MoonLightSource::Secunda)); settings.SunPath = std::clamp(settings.SunPath, static_cast(SunPath::Southern), static_cast(SunPath::Custom)); settings.CustomAngle = std::clamp(settings.CustomAngle, -90.0f, 90.0f); - settings.SunriseBeginOffset = std::clamp(settings.SunriseBeginOffset, -5.0f, 5.0f); - settings.SunriseEndOffset = std::clamp(settings.SunriseEndOffset, -5.0f, 5.0f); - settings.SunsetBeginOffset = std::clamp(settings.SunsetBeginOffset, -5.0f, 5.0f); - settings.SunsetEndOffset = std::clamp(settings.SunsetEndOffset, -5.0f, 5.0f); settings.MinShadowElevation = std::clamp(settings.MinShadowElevation, 0.0f, 45.0f); SetSunAngle(); } @@ -110,14 +146,9 @@ void SkySync::PostPostLoad() return; } - stl::detour_thunk(REL::RelocationID(25626, 26169)); stl::detour_thunk(REL::RelocationID(25682, 26229)); - stl::detour_thunk(REL::RelocationID(25695, 26242)); gSunPosition = reinterpret_cast(REL::RelocationID(527924, 414871).address()); - gSunGlareSize = reinterpret_cast(REL::RelocationID(502611, 370235).address()); - gMasserSize = reinterpret_cast(REL::RelocationID(502558, 370155).address()); - gSecundaSize = reinterpret_cast(REL::RelocationID(502570, 370173).address()); logger::info("[Sky Sync] Installed hooks"); } @@ -137,6 +168,19 @@ void SkySync::DisableOnConflict(std::string_view conflictName) logger::warn("[Sky Sync] {}", failedLoadedMessage); } +void SkySync::OnSkyUpdateColors(RE::Sky* sky) +{ + if (!settings.Enabled || !sky || !settings.DimSunlightUnderHorizon) + return; + + if (currentDim > 0.0f && currentDim < 1.0f) { + auto& dirLight = sky->skyColor[static_cast(RE::TESWeather::ColorTypes::kSunlight)]; + dirLight.red *= currentDim; + dirLight.green *= currentDim; + dirLight.blue *= currentDim; + } +} + void SkySync::Sky_Update::thunk(RE::Sky* sky) { func(sky); @@ -169,17 +213,40 @@ void SkySync::Update(const RE::Sky* sky) return; } - const float time = sky->currentGameHour; - const bool isDayTime = time > timings.sunriseFadeOutMoonEnd && time < timings.sunsetFadeInMoonStart; + // Compute dim once per frame — used by OnSkyUpdateColors (if option on) and ShadowFader (always) + if (sky->currentClimate) { + const auto& timing = sky->currentClimate->timing; + const float hour = sky->currentGameHour; + const float sunriseBegin = timing.sunrise.begin / 6.0f; + const float sunriseMiddle = (timing.sunrise.begin + timing.sunrise.end) / 12.0f; + const float sunsetMiddle = (timing.sunset.begin + timing.sunset.end) / 12.0f; + const float sunsetEnd = timing.sunset.end / 6.0f; + + if (hour >= sunsetMiddle && hour < sunsetEnd) { + float range = sunsetEnd - sunsetMiddle; + float t = range > 0.0f ? (hour - sunsetMiddle) / range : 1.0f; + currentDim = std::sqrt(1.0f - t); + } else if (hour >= sunsetEnd || hour < sunriseBegin) { + currentDim = 0.0f; + } else if (hour >= sunriseBegin && hour < sunriseMiddle) { + float range = sunriseMiddle - sunriseBegin; + float t = range > 0.0f ? (hour - sunriseBegin) / range : 1.0f; + currentDim = std::sqrt(t); + } else { + currentDim = 1.0f; + } + } else { + currentDim = 1.0f; + } - const auto worldSpace = player->GetWorldspace(); - const float altitude = worldSpace ? player->GetPositionZ() - worldSpace->GetDefaultWaterHeight() : 0.0f; + RE::NiPoint3 directions[3] = {}; + float intensities[3] = {}; - ProcessSun(sun, time, altitude, isDayTime); - ProcessMoon(sky->masser, time, Caster::Masser, altitude, isDayTime); - ProcessMoon(sky->secunda, time, Caster::Secunda, altitude, isDayTime); + ProcessSun(sky, directions, intensities); + ProcessMoon(sky, Caster::Masser, directions, intensities); + ProcessMoon(sky, Caster::Secunda, directions, intensities); - shadowFader.Update(sun, directions, intensities, isDayTime); + shadowFader.Update(sky, directions, intensities, settings.ShadowTransitionDuration); } void SkySync::SetSunAngle() { @@ -217,52 +284,49 @@ void SkySync::SetSkyRotation(const RE::Sky* sky, RE::TESObjectCELL* cell) sky->root->Update(updateData); } -void SkySync::ProcessSun(const RE::Sun* sun, const float time, const float altitude, const bool isDayTime) +void SkySync::ProcessSun(const RE::Sky* sky, RE::NiPoint3 dirs[], float intensities[]) { + const auto sun = sky->sun; RE::NiPoint3 dir; float dist; if (settings.UseAlternateSunPath) { - CalculateAlternateSunDirectionAndDistance(dir, dist, time, timings.sunrise, timings.sunset, sunAngle); + const auto climate = sky->currentClimate; + const float sunrise = (climate->timing.sunrise.begin / 6.0f + climate->timing.sunrise.end / 6.0f) * 0.5f - 0.25f; + const float sunset = (climate->timing.sunset.begin / 6.0f + climate->timing.sunset.end / 6.0f) * 0.5f + 0.25f; + CalculateAlternateSunDirectionAndDistance(dir, dist, sky->currentGameHour, sunrise, sunset, sunAngle); } else CalculateSunDirectionAndDistance(sun, dir, dist); - rawDirections[static_cast(Caster::Sun)] = dir; - - const RE::NiPoint3 apparentDir = GetApparentDirection(dir, altitude); - SetSunPosition(sun, apparentDir, dist); + SetSunPosition(sun, dir, dist); - directions[static_cast(Caster::Sun)] = apparentDir; + dirs[static_cast(Caster::Sun)] = dir; - SetSunBaseVisibility(sun, isDayTime ? 1.0f : 0.0f); - - intensities[static_cast(Caster::Sun)] = isDayTime ? CalculateVisibility(dir, dist, *gSunGlareSize * SunScaleFactor) : 0.0f; + if (const auto prop = skyrim_cast(sun->sunBase->GetGeometryRuntimeData().shaderProperty.get())) + intensities[static_cast(Caster::Sun)] = prop->kBlendColor.alpha; } -void SkySync::ProcessMoon(const RE::Moon* moon, const float time, const Caster type, const float altitude, const bool isDayTime) +void SkySync::ProcessMoon(const RE::Sky* sky, const Caster type, RE::NiPoint3 dirs[], float intensities[]) { - intensities[static_cast(type)] = 0.0f; - directions[static_cast(type)] = { 0.0f, 0.0f, 1.0f }; - rawDirections[static_cast(type)] = { 0.0f, 0.0f, -1.0f }; + const int idx = static_cast(type); + colors[idx] = {}; - if (!moon) + const auto moon = type == Caster::Masser ? sky->masser : sky->secunda; + if (!moon || moon->root->GetFlags().any(RE::NiAVObject::Flag::kHidden)) return; - const auto dir = moon->root->local.rotate.GetVectorY(); - - rawDirections[static_cast(type)] = dir; - - auto apparentDir = GetApparentDirection(dir, altitude); - SetMoonDirection(moon, apparentDir); + auto dir = moon->root->local.rotate.GetVectorY(); - // Moon and Stars adjusts some intermediary rotation matrices for the moon - // Directly changing the directions here avoids 3 matrix multiplications and a vector rotation if (moonAndStarsLoaded) - apparentDir = { apparentDir.y, -apparentDir.x, apparentDir.z }; + dir = { dir.y, -dir.x, dir.z }; - directions[static_cast(type)] = apparentDir; + dirs[idx] = dir; - if (isDayTime) + const float4& baseColor = type == Caster::Masser ? Util::Moon::MasserBaseColor : Util::Moon::SecundaBaseColor; + float4 color = Util::Moon::GetBlendColor(moon, baseColor, settings.NewMoonIntensity, settings.CrescentMoonIntensity, settings.FullMoonIntensity); + colors[idx] = color; + + if (currentDim > 0.0f) return; const auto src = static_cast(settings.MoonLightSource); @@ -270,20 +334,25 @@ void SkySync::ProcessMoon(const RE::Moon* moon, const float time, const Caster t if (!isValidSource) return; - const float moonRadius = type == Caster::Masser ? static_cast(*gMasserSize) : static_cast(*gSecundaSize); - float intensity = CalculateVisibility(dir, moon->moonMesh->local.translate.y, moonRadius); - - if (type == Caster::Masser) - intensity *= masserPhaseIntensityFactor; - else if (type == Caster::Secunda) - intensity *= secundaPhaseIntensityFactor * SecundaIntensityFactor; + intensities[idx] = color.w; +} - if (time >= timings.sunriseFadeOutMoonStart && time <= timings.sunriseFadeOutMoonEnd) - intensity *= SmoothStep(timings.sunriseFadeOutMoonEnd, timings.sunriseFadeOutMoonStart, time); - else if (time >= timings.sunsetFadeInMoonStart && time <= timings.sunsetFadeInMoonEnd) - intensity *= SmoothStep(timings.sunsetFadeInMoonStart, timings.sunsetFadeInMoonEnd, time); +bool SkySync::IsNight(const RE::Sky* sky) +{ + if (!sky || !sky->currentClimate) + return false; + const auto& timing = sky->currentClimate->timing; + const float hour = sky->currentGameHour; + return hour >= timing.sunset.end / 6.0f || hour < timing.sunrise.begin / 6.0f; +} - intensities[static_cast(type)] = intensity; +bool SkySync::IsDaytime(const RE::Sky* sky) +{ + if (!sky || !sky->currentClimate) + return false; + const auto& timing = sky->currentClimate->timing; + const float hour = sky->currentGameHour; + return hour >= timing.sunrise.end / 6.0f && hour < timing.sunset.begin / 6.0f; } inline void SkySync::CalculateSunDirectionAndDistance(const RE::Sun* sun, RE::NiPoint3& outDir, float& outDistance) @@ -314,26 +383,6 @@ inline void SkySync::CalculateAlternateSunDirectionAndDistance(RE::NiPoint3& out outDist = std::lerp(SunHorizonDistance, SunPeakDistance, elevationRatio); } -RE::NiPoint3 SkySync::GetApparentDirection(const RE::NiPoint3& dir, const float altitude) -{ - const float dipAngle = -std::atan(altitude / RenderDistance); - float sinPhi, cosPhi; - DirectX::XMScalarSinCosEst(&sinPhi, &cosPhi, dipAngle); - - const auto rotationAxis = dir.UnitCross({ 0.0f, 0.0f, 1.0f }); - const float axisDotDir = rotationAxis.Dot(dir); - const auto axisCrossDir = rotationAxis.Cross(dir); - const float oneMinusCosPhi = 1.0f - cosPhi; - - const float x = dir.x * cosPhi + axisCrossDir.x * sinPhi + rotationAxis.x * (axisDotDir * oneMinusCosPhi); - const float y = dir.y * cosPhi + axisCrossDir.y * sinPhi + rotationAxis.y * (axisDotDir * oneMinusCosPhi); - const float z = dir.z * cosPhi + axisCrossDir.z * sinPhi + rotationAxis.z * (axisDotDir * oneMinusCosPhi); - - RE::NiPoint3 rotated = { x, y, z }; - rotated.Unitize(); - return rotated; -} - inline void SkySync::SetSunPosition(const RE::Sun* sun, const RE::NiPoint3& dir, const float distance) { const auto position = dir * distance; @@ -342,115 +391,84 @@ inline void SkySync::SetSunPosition(const RE::Sun* sun, const RE::NiPoint3& dir, *gSunPosition = position; } -inline void SkySync::SetMoonDirection(const RE::Moon* moon, const RE::NiPoint3& dir) -{ - auto& m = moon->root->local.rotate; - m.entry[0][1] = dir.x; - m.entry[1][1] = dir.y; - m.entry[2][1] = dir.z; -} - -inline float SkySync::CalculateVisibility(const RE::NiPoint3& dir, const float dist, const float radius) -{ - const float height = dir.Dot({ 0.0f, 0.0f, 1.0f }) * dist; - return SmoothStep(-radius, radius, height); -} - -inline void SkySync::SetSunBaseVisibility(const RE::Sun* sun, const float visibility) -{ - if (const auto property = skyrim_cast(sun->sunBase->GetGeometryRuntimeData().shaderProperty.get())) - property->kBlendColor.alpha = visibility; -} - void SkySync::ShadowFader::Reset() { - fadePhase = Phase::None; - current = Caster::None; - target = Caster::None; + target = Caster::Sun; + previousTarget = Caster::Sun; fadeTimer = 0.0f; + transitioning = false; } -void SkySync::ShadowFader::Update(const RE::Sun* sun, RE::NiPoint3 dirs[3], float intensities[3], const bool isDayTime) +void SkySync::ShadowFader::Update(const RE::Sky* sky, RE::NiPoint3 dirs[], float intensities[], float fadeDuration) { - const float masserIntensity = intensities[static_cast(Caster::Masser)]; - const float secundaIntensity = intensities[static_cast(Caster::Secunda)]; - - auto desired = Caster::None; - if (isDayTime) - desired = Caster::Sun; - else if (masserIntensity > 0.0f && masserIntensity >= secundaIntensity) - desired = Caster::Masser; - else if (secundaIntensity > 0.0f) - desired = Caster::Secunda; - - if (desired != target) { - target = desired; - fadeTimer = 0.0f; - - if (current == Caster::None) { - fadePhase = Phase::FadeIn; - current = target; - } else - fadePhase = Phase::FadeOut; + Caster best; + + if (globals::features::skySync.currentDim <= 0.0f) { + best = Caster::Masser; + if (intensities[static_cast(Caster::Secunda)] > intensities[static_cast(Caster::Masser)]) + best = Caster::Secunda; + } else { + best = Caster::Sun; } - float timeScale = 20.0f; - if (const auto calendar = globals::game::calendar) { - const float currentHoursPassed = calendar->GetHoursPassed(); - timeScale = calendar->GetTimescale(); - const float hoursPassedDiff = std::abs(currentHoursPassed - previousHoursPassed); - previousHoursPassed = currentHoursPassed; - if (timeScale <= 0.0f || hoursPassedDiff >= 0.01f) { - fadePhase = Phase::None; - current = target; + // If best source changed, begin a new transition + if (best != target) { + previousTarget = target; + target = best; + startDir = currentDir; + fadeTimer = 0.0f; + transitioning = true; + + // Snap instantly if transitioning to sun during daytime or to moon during full night + bool snap = (best == Caster::Sun && IsDaytime(sky)) || + ((best == Caster::Masser || best == Caster::Secunda) && IsNight(sky)); + if (snap) { + transitioning = false; + currentDir = dirs[static_cast(best)]; + SetLighting(sky, currentDir); + return; } } - if (current == Caster::None) { - fadePhase = Phase::None; - SetLighting(sun, { 0.0f, 0.0f, 1.0f }, 0.0f); + if (!transitioning) { + currentDir = dirs[static_cast(target)]; + SetLighting(sky, currentDir); return; } - const auto& dir = dirs[static_cast(current)]; - const auto intensity = intensities[static_cast(current)]; - - if (fadePhase == Phase::None) { - SetLighting(sun, dir, intensity); - return; + float timeScale = 20.0f; + if (const auto calendar = globals::game::calendar) + timeScale = calendar->GetTimescale(); + fadeTimer = std::min(fadeTimer + *globals::game::deltaTime * 20.0f / timeScale, fadeDuration); + const float t = fadeDuration > 0.0f ? fadeTimer / fadeDuration : 1.0f; + + RE::NiPoint3 targetDir = dirs[static_cast(target)]; + currentDir = { + std::lerp(startDir.x, targetDir.x, t), + std::lerp(startDir.y, targetDir.y, t), + std::lerp(startDir.z, targetDir.z, t) + }; + currentDir.Unitize(); + + if (t >= 1.0f) { + currentDir = targetDir; + transitioning = false; } - fadeTimer = std::min(fadeTimer + *globals::game::deltaTime * timeScale, FadeTime); - - const float t = fadeTimer / FadeTime; - const float fade = fadePhase == Phase::FadeIn ? t : 1.0f - t; - SetLighting(sun, dir, intensity * fade); - - if (fadePhase == Phase::FadeOut) { - if (t >= 1.0f || intensity <= 0.0f) { - current = target; - fadePhase = Phase::FadeIn; - fadeTimer = 0.0f; - } - } else if (fadePhase == Phase::FadeIn) { - if (t >= 1.0f) - fadePhase = Phase::None; - } + SetLighting(sky, currentDir); } -void SkySync::ShadowFader::SetLighting(const RE::Sun* sun, RE::NiPoint3 dir, float intensity) +void SkySync::ShadowFader::SetLighting(const RE::Sky* sky, RE::NiPoint3 dir) { ClampDirection(dir); - RE::NiMatrix3& m = sun->light->local.rotate; + RE::NiMatrix3& m = sky->sun->light->local.rotate; m.entry[0][0] = -dir.x; m.entry[1][0] = -dir.y; m.entry[2][0] = -dir.z; RE::NiUpdateData updateData; - sun->light->Update(updateData); - - intensity = std::clamp(intensity, 0.0f, 1.0f); + sky->sun->light->Update(updateData); } inline void SkySync::ShadowFader::ClampDirection(RE::NiPoint3& dir) @@ -471,92 +489,5 @@ inline void SkySync::ShadowFader::ClampDirection(RE::NiPoint3& dir) dir.z = sinElev; } -void SkySync::ClimateTimings::Update(const RE::TESClimate* climate) -{ - const float SunriseBeginOffset = globals::features::skySync.settings.SunriseBeginOffset; - const float SunriseEndOffset = globals::features::skySync.settings.SunriseEndOffset; - const float SunsetBeginOffset = globals::features::skySync.settings.SunsetBeginOffset; - const float SunsetEndOffset = globals::features::skySync.settings.SunsetEndOffset; - - sunriseBegin = (climate->timing.sunrise.begin / 6.0f) + SunriseBeginOffset; - sunriseEnd = (climate->timing.sunrise.end / 6.0f) + SunriseEndOffset; - sunsetBegin = (climate->timing.sunset.begin / 6.0f) + SunsetBeginOffset; - sunsetEnd = (climate->timing.sunset.end / 6.0f) + SunsetEndOffset; - // Basic ordering guarantees (prevents divide-by-zero / negative duration paths). - constexpr float kMinGapHours = 0.1f; - if (sunriseEnd <= sunriseBegin) - sunriseEnd = sunriseBegin + kMinGapHours; - if (sunsetEnd <= sunsetBegin) - sunsetEnd = sunsetBegin + kMinGapHours; - if (sunsetBegin <= sunriseEnd) - sunsetBegin = sunriseEnd + kMinGapHours; - if (sunsetEnd <= sunsetBegin) - sunsetEnd = sunsetBegin + kMinGapHours; - sunrise = (sunriseBegin + sunriseEnd) * 0.5f - 0.25f; - sunset = (sunsetBegin + sunsetEnd) * 0.5f + 0.25f; - sunriseFadeOutMoonStart = sunriseBegin - 0.5f; - sunriseFadeOutMoonEnd = sunriseBegin + 1.0f; - sunsetFadeInMoonStart = sunsetEnd - 1.0f; - sunsetFadeInMoonEnd = sunsetEnd + 0.5f; -} -void SkySync::Sky_OnNewClimate::thunk(RE::Sky* sky) -{ - if (auto& singleton = globals::features::skySync; singleton.settings.Enabled && sky && sky->currentClimate) - singleton.timings.Update(sky->currentClimate); - func(sky); -} - -void SkySync::Moon_Update::thunk(RE::Moon* moon, RE::Sky* sky) -{ - const auto updateMoonTexture = moon->updateMoonTexture; - - func(moon, sky); - - if (auto& singleton = globals::features::skySync; singleton.settings.Enabled && updateMoonTexture != moon->updateMoonTexture) { - // Gets the texture name of the current moon phase when it changes rather than reading direct global variables - // Allows for compatability with other mods that don't directly update the in-game phase values - const auto moonShaderProperty = skyrim_cast(moon->moonMesh->GetGeometryRuntimeData().shaderProperty.get()); - - const auto name = moonShaderProperty->GetBaseTexture()->name.c_str(); - const size_t len = std::strlen(name); - std::string lower; - lower.reserve(len); - for (size_t i = 0; i < len; ++i) { - lower.push_back(static_cast(std::tolower(name[i]))); - } - static constexpr std::array, 8> Lookup{ - { { "full", RE::Moon::Phases::Phase::kFull }, - { "three_wan", RE::Moon::Phases::Phase::kWaningGibbous }, - { "half_wan", RE::Moon::Phases::Phase::kWaningQuarter }, - { "one_wan", RE::Moon::Phases::Phase::kWaningCrescent }, - { "new", RE::Moon::Phases::Phase::kNewMoon }, - { "one_wax", RE::Moon::Phases::Phase::kWaxingCrescent }, - { "half_wax", RE::Moon::Phases::Phase::kWaxingQuarter }, - { "three_wax", RE::Moon::Phases::Phase::kWaxingGibbous } } - }; - - RE::Moon::Phases::Phase phase = RE::Moon::Phases::Phase::kFull; - for (auto& [suffix, id] : Lookup) { - if (lower.find(suffix) != std::string::npos) { - phase = id; - break; - } - } - - float* intensityFactor = moon == sky->masser ? &singleton.masserPhaseIntensityFactor : &singleton.secundaPhaseIntensityFactor; - if (phase == RE::Moon::Phases::Phase::kNewMoon) { - *intensityFactor = NewMoonIntensityFactor; - } else { - const float t = (abs(static_cast(phase) - static_cast(RE::Moon::Phases::Phase::kNewMoon)) - 1.0f) / 3.0f; - *intensityFactor = std::lerp(CrescentMoonIntensityFactor, FullMoonIntensityFactor, t); - } - } -} - -inline float SkySync::SmoothStep(const float start, const float end, const float x) -{ - const float t = std::clamp((x - start) / (end - start), 0.0f, 1.0f); - return t * t * (3.0f - 2.0f * t); -} diff --git a/src/Features/SkySync.h b/src/Features/SkySync.h index c1c1eed86c..541891110f 100644 --- a/src/Features/SkySync.h +++ b/src/Features/SkySync.h @@ -1,6 +1,8 @@ #pragma once #include "RE/M/Moon.h" +#include "Utils/Moon.h" + struct SkySync : Feature { private: @@ -21,22 +23,23 @@ struct SkySync : Feature "Smoothly switches the light source between the sun and moons based on visibility", "Moon light source can be switched between Masser, Secunda, or the brightest", "Automatic calculation of moon lighting intensity based on moon phase", - "Fixes the sun appearing higher on the horizon when the player gains altitude" } + "Automatic calculation of shadow lighting direction based on the brightest light source" } }; } struct Settings { bool Enabled = true; - bool UseAlternateSunPath = true; + bool UseAlternateSunPath = false; int32_t MoonLightSource = 0; int32_t SunPath = 0; float CustomAngle = -35.0f; - float SunriseBeginOffset = 0.0f; - float SunriseEndOffset = 0.0f; - float SunsetBeginOffset = 0.0f; - float SunsetEndOffset = 0.0f; - float MinShadowElevation = 0.25f; + float MinShadowElevation = 18.0f; + float ShadowTransitionDuration = 100.0f; + bool DimSunlightUnderHorizon = true; + float NewMoonIntensity = 0.05f; + float CrescentMoonIntensity = 0.25f; + float FullMoonIntensity = 1.0f; }; Settings settings; @@ -49,6 +52,8 @@ struct SkySync : Feature virtual bool SupportsVR() override { return true; } + void OnSkyUpdateColors(RE::Sky* sky); + virtual void PostPostLoad() override; virtual void DataLoaded() override; @@ -58,17 +63,6 @@ struct SkySync : Feature static inline REL::Relocation func; }; - struct Sky_OnNewClimate - { - static void thunk(RE::Sky* sky); - static inline REL::Relocation func; - }; - - struct Moon_Update - { - static void thunk(RE::Moon* moon, RE::Sky* sky); - static inline REL::Relocation func; - }; private: enum class CellFlagExt : uint16_t @@ -104,76 +98,36 @@ struct SkySync : Feature const char* MoonLightSourceNames[static_cast(MoonLightSource::Count)] = { "Brightest", "Masser", "Secunda" }; const char* SunPathNames[static_cast(SunPath::Count)] = { "Southern Sky", "Northern Sky", "Vanilla", "Custom" }; - struct ClimateTimings - { - float sunriseFadeOutMoonStart; - float sunriseBegin; - float sunriseFadeOutMoonEnd; - float sunrise; - float sunriseEnd; - float sunsetBegin; - float sunset; - float sunsetFadeInMoonStart; - float sunsetEnd; - float sunsetFadeInMoonEnd; - - void Update(const RE::TESClimate* climate); - }; - struct ShadowFader { - enum class Phase : uint8_t - { - None, - FadeOut, - FadeIn - }; - - static constexpr float FadeTime = 100.0f; // 5 seconds at timescale 20 - - Phase fadePhase = Phase::None; - Caster current = Caster::None; - Caster target = Caster::None; + RE::NiPoint3 currentDir = { 0.0f, 0.0f, 1.0f }; + RE::NiPoint3 startDir = { 0.0f, 0.0f, 1.0f }; + Caster target = Caster::Sun; + Caster previousTarget = Caster::Sun; float fadeTimer = 0.0f; - float previousHoursPassed = 0.0f; + bool transitioning = false; - void Update(const RE::Sun* sun, RE::NiPoint3 dirs[], float intensities[], bool isDayTime); - static void SetLighting(const RE::Sun* sun, RE::NiPoint3 dir, float intensity); + void Update(const RE::Sky* sky, RE::NiPoint3 dirs[], float intensities[], float fadeDuration); + static void SetLighting(const RE::Sky* sky, RE::NiPoint3 dir); static void ClampDirection(RE::NiPoint3& dir); void Reset(); }; - static constexpr float RenderDistance = 325000.0f; static constexpr float SunHorizonDistance = 280.0f; static constexpr float SunPeakDistance = 400.0f; - static constexpr float SunScaleFactor = 48.0f / 2048.0f; - static constexpr float SouthernSunAngle = 90.0f - 35.0f; static constexpr float NorthernSunAngle = 90.0f + 35.0f; static constexpr float VanillaSunAngle = 90.0f + 5.0f; - static constexpr float SecundaIntensityFactor = 0.67f; - static constexpr float NewMoonIntensityFactor = 0.05f; - static constexpr float CrescentMoonIntensityFactor = 0.25f; - static constexpr float FullMoonIntensityFactor = 1.0f; - inline static RE::NiPoint3* gSunPosition = nullptr; - inline static float* gSunGlareSize = nullptr; - inline static uint32_t* gMasserSize = nullptr; - inline static uint32_t* gSecundaSize = nullptr; bool moonAndStarsLoaded = false; RE::TESObjectCELL* currentCell = nullptr; float sunAngle = 90.0f; float currentSkyRotation = D3D11_FLOAT32_MAX; - float masserPhaseIntensityFactor = 0.0f; - float secundaPhaseIntensityFactor = 0.0f; - - ClimateTimings timings = {}; - RE::NiPoint3 rawDirections[3]; - RE::NiPoint3 directions[3]; - float intensities[3] = {}; + float4 colors[3] = {}; + float currentDim = 1.0f; ShadowFader shadowFader; void DisableOnConflict(std::string_view conflictName); @@ -184,23 +138,16 @@ struct SkySync : Feature void SetSkyRotation(const RE::Sky* sky, RE::TESObjectCELL* cell); - void ProcessSun(const RE::Sun* sun, float time, float altitude, bool isDayTime); + void ProcessSun(const RE::Sky* sky, RE::NiPoint3 dirs[], float intensities[]); - void ProcessMoon(const RE::Moon* moon, float time, Caster type, float altitude, bool isDayTime); + void ProcessMoon(const RE::Sky* sky, Caster type, RE::NiPoint3 dirs[], float intensities[]); + + static bool IsNight(const RE::Sky* sky); + static bool IsDaytime(const RE::Sky* sky); static void CalculateSunDirectionAndDistance(const RE::Sun* sun, RE::NiPoint3& outDir, float& outDistance); static void CalculateAlternateSunDirectionAndDistance(RE::NiPoint3& outDir, float& outDist, float time, float sunrise, float sunset, float sunAngle); - static RE::NiPoint3 GetApparentDirection(const RE::NiPoint3& dir, float altitude); - static void SetSunPosition(const RE::Sun* sun, const RE::NiPoint3& dir, float distance); - - static void SetMoonDirection(const RE::Moon* moon, const RE::NiPoint3& dir); - - static float CalculateVisibility(const RE::NiPoint3& dir, float dist, float radius); - - static void SetSunBaseVisibility(const RE::Sun* sun, float visibility); - - static float SmoothStep(float start, float end, float x); }; diff --git a/src/Features/Skylighting.cpp b/src/Features/Skylighting.cpp index 771c5240a4..d3da3ec646 100644 --- a/src/Features/Skylighting.cpp +++ b/src/Features/Skylighting.cpp @@ -1,5 +1,6 @@ #include "Skylighting.h" +#include "Deferred.h" #include "ShaderCache.h" #include "State.h" #include "Utils/D3D.h" @@ -30,6 +31,11 @@ void Skylighting::ResetSkylighting() auto context = globals::d3d::context; UINT clr[1] = { 0 }; context->ClearUnorderedAccessViewUint(texAccumFramesArray->uav.get(), clr); + context->ClearUnorderedAccessViewUint(texShadowBitmask->uav.get(), clr); + + float clrf[4] = { 1.0f, 1.0f, 1.0f, 1.0f }; + context->ClearUnorderedAccessViewFloat(texShadowVisibility->uav.get(), clrf); + queuedResetSkylighting = false; } @@ -50,6 +56,7 @@ void Skylighting::DrawSettings() ImGui::SliderAngle("Max Zenith Angle", &settings.MaxZenith, 0, 90); if (auto _tt = Util::HoverTooltipWrapper()) ImGui::Text("Smaller angles creates more focused top-down shadow."); + } void Skylighting::SetupResources() @@ -110,6 +117,18 @@ void Skylighting::SetupResources() texAccumFramesArray = new Texture3D(texDesc, "Skylighting::AccumFramesArray"); texAccumFramesArray->CreateSRV(srvDesc); texAccumFramesArray->CreateUAV(uavDesc); + + texDesc.Format = srvDesc.Format = uavDesc.Format = DXGI_FORMAT_R32_UINT; + + texShadowBitmask = new Texture3D(texDesc, "Skylighting::ShadowBitmask"); + texShadowBitmask->CreateSRV(srvDesc); + texShadowBitmask->CreateUAV(uavDesc); + + texDesc.Format = srvDesc.Format = uavDesc.Format = DXGI_FORMAT_R16_FLOAT; + + texShadowVisibility = new Texture3D(texDesc, "Skylighting::ShadowVisibility"); + texShadowVisibility->CreateSRV(srvDesc); + texShadowVisibility->CreateUAV(uavDesc); } { @@ -217,9 +236,20 @@ void Skylighting::Prepass() auto context = globals::d3d::context; { - std::array srvs = { texOcclusion->srv.get() }; - std::array uavs = { texProbeArray->uav.get(), texAccumFramesArray->uav.get() }; - std::array samplers = { comparisonSampler.get() }; + std::array srvs = { + texOcclusion->srv.get(), + shadowCascadeSRV, + globals::deferred->directionalShadowLights->srv.get() + }; + std::array uavs = { + texProbeArray->uav.get(), + texAccumFramesArray->uav.get(), + texShadowBitmask->uav.get(), + texShadowVisibility->uav.get() + }; + std::array samplers = { + comparisonSampler.get() + }; // Update probe array { @@ -227,7 +257,9 @@ void Skylighting::Prepass() context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); context->CSSetShader(probeUpdateCompute.get(), nullptr, 0); + globals::profiler->BeginPass("Skylighting::ProbeUpdate"); context->Dispatch((probeArrayDims[0] + 7u) >> 3, (probeArrayDims[1] + 7u) >> 3, probeArrayDims[2]); + globals::profiler->EndPass(); } // Reset @@ -247,6 +279,9 @@ void Skylighting::Prepass() { ID3D11ShaderResourceView* srv = texProbeArray->srv.get(); context->PSSetShaderResources(50, 1, &srv); + + srv = texShadowVisibility->srv.get(); + context->PSSetShaderResources(53, 1, &srv); } } @@ -513,7 +548,9 @@ void Skylighting::RenderOcclusion() auto particleShaderProperty = netimmerse_cast(shaderProp); auto rain = (RE::BSParticleShaderRainEmitter*)(particleShaderProperty->particleEmitter); + globals::profiler->BeginPass("Skylighting::PrecipMask"); precip->RenderMask(rain); + globals::profiler->EndPass(); } state->EndPerfEvent(); @@ -586,7 +623,9 @@ void Skylighting::RenderOcclusion() BSParticleShaderRainEmitter* rain = new BSParticleShaderRainEmitter; { TracyD3D11Zone(state->tracyCtx, "Skylighting - Render Height Map"); + globals::profiler->BeginPass("Skylighting::OcclusionMask"); precip->RenderMask((RE::BSParticleShaderRainEmitter*)rain); + globals::profiler->EndPass(); } inOcclusion = false; @@ -613,6 +652,16 @@ void Skylighting::RenderOcclusion() } } +void Skylighting::CaptureShadowCascadeSRV() +{ + auto context = globals::d3d::context; + ID3D11ShaderResourceView* srv = nullptr; + context->PSGetShaderResources(4, 1, &srv); + if (shadowCascadeSRV) + shadowCascadeSRV->Release(); + shadowCascadeSRV = srv; +} + void Skylighting::Main_Precipitation_RenderOcclusion::thunk() { globals::features::skylighting.RenderOcclusion(); diff --git a/src/Features/Skylighting.h b/src/Features/Skylighting.h index 71463341da..b906b843b5 100644 --- a/src/Features/Skylighting.h +++ b/src/Features/Skylighting.h @@ -73,6 +73,10 @@ struct Skylighting : Feature Texture2D* texOcclusion = nullptr; Texture3D* texProbeArray = nullptr; Texture3D* texAccumFramesArray = nullptr; + Texture3D* texShadowBitmask = nullptr; + Texture3D* texShadowVisibility = nullptr; + + ID3D11ShaderResourceView* shadowCascadeSRV = nullptr; winrt::com_ptr probeUpdateCompute = nullptr; @@ -88,6 +92,7 @@ struct Skylighting : Feature uint frameCount = 0; void ResetSkylighting(); + void CaptureShadowCascadeSRV(); std::chrono::time_point lastUpdateTimer = std::chrono::system_clock::now(); diff --git a/src/Features/SubsurfaceScattering.cpp b/src/Features/SubsurfaceScattering.cpp index cfa1179070..bf4412af29 100644 --- a/src/Features/SubsurfaceScattering.cpp +++ b/src/Features/SubsurfaceScattering.cpp @@ -12,6 +12,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( EnableCharacterLighting, CharacterLightingStrength, SSMode, + ScatterMode, BaseProfile, HumanProfile, BurleySamples, @@ -33,6 +34,25 @@ void SubsurfaceScattering::DrawSettings() ImGui::SameLine(); ImGui::RadioButton("Burley", &settings.SSMode, 1); + if (settings.SSMode == 0) { + ImGui::Spacing(); + ImGui::Text("Albedo Handling"); + ImGui::RadioButton("Pre-scatter", &settings.ScatterMode, kPreScatter); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Blur the lit color directly. Fastest, but blurs albedo texture detail along with lighting."); + } + ImGui::SameLine(); + ImGui::RadioButton("Post-scatter", &settings.ScatterMode, kPostScatter); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Divide out albedo, blur the irradiance, multiply albedo back. Preserves texture detail."); + } + ImGui::SameLine(); + ImGui::RadioButton("Pre and Post", &settings.ScatterMode, kPreAndPostScatter); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Split albedo across the blur using sqrt(albedo) on each side. A physically motivated middle ground."); + } + } + if (settings.SSMode == 0) { if (ImGui::TreeNodeEx("Base Profile", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::SliderFloat("Blur Radius", &settings.BaseProfile.BlurRadius, 0, 3, "%.2f"); @@ -99,6 +119,7 @@ void SubsurfaceScattering::DrawSettings() ImGui::TreePop(); } + } float3 SubsurfaceScattering::Gaussian(DiffusionProfile& a_profile, float variance, float r) @@ -215,6 +236,10 @@ void SubsurfaceScattering::DrawSSS() blurCBData.HumanProfile = { settings.HumanProfile.BlurRadius, settings.HumanProfile.Thickness, 0, 0 }; blurCBData.BurleySamples = settings.BurleySamples; + // Burley always does full albedo removal/reapply; scatter mode only applies to Separable SSS. + blurCBData.ScatterMode = (settings.SSMode == 0) + ? (uint)std::clamp(settings.ScatterMode, (int)kPreScatter, (int)kPreAndPostScatter) + : (uint)kPostScatter; blurCBData.MeanFreePathBase = settings.MeanFreePathBase; blurCBData.MeanFreePathHuman = settings.MeanFreePathHuman; @@ -228,6 +253,7 @@ void SubsurfaceScattering::DrawSSS() { ID3D11Buffer* buffer[1] = { blurCB->CB() }; context->CSSetConstantBuffers(1, 1, buffer); + context->CSSetSamplers(0, 1, &globals::deferred->pointSampler); auto main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; @@ -235,9 +261,6 @@ void SubsurfaceScattering::DrawSSS() auto albedo = renderer->GetRuntimeData().renderTargets[ALBEDO]; auto normal = renderer->GetRuntimeData().renderTargets[NORMALROUGHNESS]; - ID3D11UnorderedAccessView* uav = blurHorizontalTemp->uav.get(); - context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); - ID3D11ShaderResourceView* views[5]; views[0] = main.SRV; views[1] = Util::GetCurrentSceneDepthSRV(true); @@ -247,21 +270,48 @@ void SubsurfaceScattering::DrawSSS() context->CSSetShaderResources(0, ARRAYSIZE(views), views); + // Pre-pass: remove albedo from diffuse, write to diffuseNoAlbedoTex + { + TracyD3D11Zone(globals::state->tracyCtx, "Subsurface Scattering - Prepass"); + + ID3D11UnorderedAccessView* uav = diffuseNoAlbedoTex->uav.get(); + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + + auto shader = GetComputeShaderPrepass(); + context->CSSetShader(shader, nullptr, 0); + + globals::profiler->BeginPass("SubsurfaceScattering::DiffuseExtract"); + context->Dispatch(dispatchCount.x, dispatchCount.y, 1); + globals::profiler->EndPass(); + + uav = nullptr; + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + } + + // Swap color input to pre-processed texture + views[0] = diffuseNoAlbedoTex->srv.get(); + context->CSSetShaderResources(0, 1, views); + if (settings.SSMode == 0) { - // Horizontal pass to temporary texture + // Horizontal pass: diffuseNoAlbedoTex -> blurHorizontalTemp { TracyD3D11Zone(globals::state->tracyCtx, "Subsurface Scattering - Horizontal"); + ID3D11UnorderedAccessView* uav = blurHorizontalTemp->uav.get(); + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + auto shader = GetComputeShaderHorizontalBlur(); context->CSSetShader(shader, nullptr, 0); + globals::profiler->BeginPass("SubsurfaceScattering::HorizontalBlur"); context->Dispatch(dispatchCount.x, dispatchCount.y, 1); - } + globals::profiler->EndPass(); - uav = nullptr; - context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + uav = nullptr; + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + } - // Vertical pass to main texture + // Vertical pass: blurHorizontalTemp -> main { TracyD3D11Zone(globals::state->tracyCtx, "Subsurface Scattering - Vertical"); @@ -274,19 +324,24 @@ void SubsurfaceScattering::DrawSSS() auto shader = GetComputeShaderVerticalBlur(); context->CSSetShader(shader, nullptr, 0); + globals::profiler->BeginPass("SubsurfaceScattering::VerticalBlur"); context->Dispatch(dispatchCount.x, dispatchCount.y, 1); + globals::profiler->EndPass(); } } else if (settings.SSMode == 1) { - // Burley pass to main texture + // Burley pass: diffuseNoAlbedoTex -> main (SSS pixels only) { TracyD3D11Zone(globals::state->tracyCtx, "Subsurface Scattering - Burley"); + ID3D11UnorderedAccessView* uavs[1] = { main.UAV }; + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + auto shader = GetComputeShaderBurley(); context->CSSetShader(shader, nullptr, 0); + globals::profiler->BeginPass("SubsurfaceScattering::Burley"); context->Dispatch(dispatchCount.x, dispatchCount.y, 1); - - context->CopyResource(main.texture, blurHorizontalTemp->resource.get()); + globals::profiler->EndPass(); } } } @@ -294,6 +349,9 @@ void SubsurfaceScattering::DrawSSS() ID3D11Buffer* buffer = nullptr; context->CSSetConstantBuffers(1, 1, &buffer); + ID3D11SamplerState* nullSampler = nullptr; + context->CSSetSamplers(0, 1, &nullSampler); + ID3D11ShaderResourceView* views[5]{ nullptr, nullptr, nullptr, nullptr, nullptr }; context->CSSetShaderResources(0, ARRAYSIZE(views), views); @@ -329,6 +387,10 @@ void SubsurfaceScattering::SetupResources() blurHorizontalTemp = new Texture2D(texDesc); blurHorizontalTemp->CreateSRV(srvDesc); blurHorizontalTemp->CreateUAV(uavDesc); + + diffuseNoAlbedoTex = new Texture2D(texDesc); + diffuseNoAlbedoTex->CreateSRV(srvDesc); + diffuseNoAlbedoTex->CreateUAV(uavDesc); } } @@ -368,6 +430,10 @@ void SubsurfaceScattering::SaveSettings(json& o_json) void SubsurfaceScattering::ClearShaderCache() { + if (prepassSS) { + prepassSS->Release(); + prepassSS = nullptr; + } if (horizontalSSBlur) { horizontalSSBlur->Release(); horizontalSSBlur = nullptr; @@ -382,6 +448,15 @@ void SubsurfaceScattering::ClearShaderCache() } } +ID3D11ComputeShader* SubsurfaceScattering::GetComputeShaderPrepass() +{ + if (!prepassSS) { + logger::debug("Compiling prepassSS"); + prepassSS = (ID3D11ComputeShader*)Util::CompileShader(L"Data\\Shaders\\SubsurfaceScattering\\DiffuseExtractionCS.hlsl", {}, "cs_5_0"); + } + return prepassSS; +} + ID3D11ComputeShader* SubsurfaceScattering::GetComputeShaderHorizontalBlur() { if (!horizontalSSBlur) { diff --git a/src/Features/SubsurfaceScattering.h b/src/Features/SubsurfaceScattering.h index 9ea4fd58ca..8050c8a89d 100644 --- a/src/Features/SubsurfaceScattering.h +++ b/src/Features/SubsurfaceScattering.h @@ -15,13 +15,21 @@ struct SubsurfaceScattering : Feature float3 Falloff; }; + enum ScatterMode : int + { + kPreScatter = 0, + kPostScatter = 1, + kPreAndPostScatter = 2, + }; + struct Settings { uint EnableCharacterLighting = false; float CharacterLightingStrength = 1.0f; - int SSMode = 1; - DiffusionProfile BaseProfile{ 0.5f, 1.0f, { 0.48f, 0.41f, 0.28f }, { 0.56f, 0.56f, 0.56f } }; - DiffusionProfile HumanProfile{ 0.5f, 1.0f, { 0.48f, 0.41f, 0.28f }, { 1.0f, 0.37f, 0.3f } }; + int SSMode = 0; + int ScatterMode = kPreAndPostScatter; + DiffusionProfile BaseProfile{ 1.0f, 1.0f, { 0.48f, 0.41f, 0.28f }, { 0.56f, 0.56f, 0.56f } }; + DiffusionProfile HumanProfile{ 1.0f, 1.0f, { 0.48f, 0.41f, 0.28f }, { 1.0f, 0.37f, 0.3f } }; uint BurleySamples = 16; float4 MeanFreePathBase = { 0.56f, 0.56f, 0.56f, 2.67f }; float4 MeanFreePathHuman = { 1.0f, 0.37f, 0.3f, 2.67f }; @@ -45,7 +53,8 @@ struct SubsurfaceScattering : Feature float4 HumanProfile; float SSSS_FOVY; uint BurleySamples; - uint pad[2]; + uint ScatterMode; + uint pad; float4 MeanFreePathBase; float4 MeanFreePathHuman; }; @@ -59,7 +68,9 @@ struct SubsurfaceScattering : Feature bool validMaterials = false; Texture2D* blurHorizontalTemp = nullptr; + Texture2D* diffuseNoAlbedoTex = nullptr; + ID3D11ComputeShader* prepassSS = nullptr; ID3D11ComputeShader* horizontalSSBlur = nullptr; ID3D11ComputeShader* verticalSSBlur = nullptr; ID3D11ComputeShader* burleySS = nullptr; @@ -101,6 +112,7 @@ struct SubsurfaceScattering : Feature virtual void SaveSettings(json& o_json) override; virtual void ClearShaderCache() override; + ID3D11ComputeShader* GetComputeShaderPrepass(); ID3D11ComputeShader* GetComputeShaderHorizontalBlur(); ID3D11ComputeShader* GetComputeShaderVerticalBlur(); ID3D11ComputeShader* GetComputeShaderBurley(); diff --git a/src/Features/TerrainBlending.cpp b/src/Features/TerrainBlending.cpp index 5adb6a5905..06793a872c 100644 --- a/src/Features/TerrainBlending.cpp +++ b/src/Features/TerrainBlending.cpp @@ -786,8 +786,6 @@ void TerrainBlending::BlendPrepassDepths() { ZoneScoped; TracyD3D11Zone(globals::state->tracyCtx, "Terrain Blending - Blend Prepass Depths"); - if (globals::state->frameAnnotations) - globals::state->BeginPerfEvent("Terrain Blending - Blend Prepass Depths"); auto context = globals::d3d::context; context->OMSetRenderTargets(0, nullptr, nullptr); @@ -806,7 +804,9 @@ void TerrainBlending::BlendPrepassDepths() context->CSSetShader(GetDepthBlendShader(), nullptr, 0); + globals::profiler->BeginPass("TerrainBlending::DepthBlend"); context->Dispatch(dispatchCount.x, dispatchCount.y, 1); + globals::profiler->EndPass(); } ID3D11ShaderResourceView* views[2] = { nullptr, nullptr }; @@ -820,11 +820,6 @@ void TerrainBlending::BlendPrepassDepths() auto stateUpdateFlags = globals::game::stateUpdateFlags; stateUpdateFlags->set(RE::BSGraphics::ShaderFlags::DIRTY_RENDERTARGET); - // CopyResource(terrainDepth <- mainDepth) eliminated: main depth is now written - // directly into mainDepthCopy (u2) by the CS above, saving a full-stereo D24S8 copy. - - if (globals::state->frameAnnotations) - globals::state->EndPerfEvent(); } void TerrainBlending::ClearShaderCache() diff --git a/src/Features/TerrainShadows.cpp b/src/Features/TerrainShadows.cpp index b695d85e5c..b881a7836b 100644 --- a/src/Features/TerrainShadows.cpp +++ b/src/Features/TerrainShadows.cpp @@ -413,7 +413,9 @@ void TerrainShadows::UpdateShadow() context->CSSetUnorderedAccessViews(0, ARRAYSIZE(newer.uavs), newer.uavs, nullptr); context->CSSetConstantBuffers(0, 1, &newer.buffer); context->CSSetShader(shadowUpdateProgram.get(), nullptr, 0); + globals::profiler->BeginPass("TerrainShadows::ShadowUpdate"); context->Dispatch(abs(shadowUpdateCBData.LightPxDir.x) >= abs(shadowUpdateCBData.LightPxDir.y) ? height : width, 1, 1); + globals::profiler->EndPass(); /* ---- RESTORE ---- */ context->CSSetShaderResources(0, ARRAYSIZE(old.srvs), old.srvs); diff --git a/src/Features/Upscaling.cpp b/src/Features/Upscaling.cpp index b3a0190b6c..805159b91e 100644 --- a/src/Features/Upscaling.cpp +++ b/src/Features/Upscaling.cpp @@ -1041,20 +1041,13 @@ void Upscaling::PreparePerEyeInputs(ID3D11Resource* colorSrc) if (!globals::game::isVR) return; - auto state = globals::state; - if (state->frameAnnotations) - state->BeginPerfEvent("VR Upscaling Prepare"); - auto context = globals::d3d::context; + auto renderSize = Util::ConvertToDynamic(globals::state->screenSize); uint32_t eyeWidthIn = (uint32_t)(renderSize.x / 2); uint32_t eyeHeightIn = (uint32_t)renderSize.y; - // Textures guaranteed to exist: EnsureVRIntermediateTextures() was called in Upscale() - // Read the original game depth SRV for ClearHMDMask — the combined stereo buffer is - // definitively valid here, whereas the per-eye copy may silently produce zeros on some - // depth-stencil format / driver combinations. auto& depthTexture = globals::game::renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; auto& motionVectorRT = globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; @@ -1069,9 +1062,6 @@ void Upscaling::PreparePerEyeInputs(ID3D11Resource* colorSrc) ClearHMDMask(vrIntermediateColorIn[i]->uav.get(), depthTexture.depthSRV, eyeWidthIn, eyeHeightIn, depthOffset, 0); } - - if (state->frameAnnotations) - state->EndPerfEvent(); } void Upscaling::FinalizePerEyeOutputs(ID3D11Resource* colorDst) @@ -1082,12 +1072,9 @@ void Upscaling::FinalizePerEyeOutputs(ID3D11Resource* colorDst) if (!globals::game::isVR) return; - auto state = globals::state; - if (state->frameAnnotations) - state->BeginPerfEvent("VR Upscaling Finalize"); - auto context = globals::d3d::context; - auto screenSize = state->screenSize; + + auto screenSize = globals::state->screenSize; uint32_t eyeWidthOut = (uint32_t)(screenSize.x / 2); uint32_t eyeHeightOut = (uint32_t)screenSize.y; @@ -1098,9 +1085,6 @@ void Upscaling::FinalizePerEyeOutputs(ID3D11Resource* colorDst) D3D11_BOX outBox = { 0, 0, 0, eyeWidthOut, eyeHeightOut, 1 }; context->CopySubresourceRegion(colorDst, 0, offsetXOut, 0, 0, vrIntermediateColorOut[i]->resource.get(), 0, &outBox); } - - if (state->frameAnnotations) - state->EndPerfEvent(); } void Upscaling::ClearHMDMask(ID3D11UnorderedAccessView* colorUAV, ID3D11ShaderResourceView* depthSRV, @@ -1145,7 +1129,9 @@ void Upscaling::ClearHMDMask(ID3D11UnorderedAccessView* colorUAV, ID3D11ShaderRe ID3D11Buffer* cbs[1] = { vrClearHMDMaskCB.get() }; context->CSSetConstantBuffers(0, 1, cbs); + globals::profiler->BeginPass("Upscaling::ClearHMDMask"); context->Dispatch(dispatchX, dispatchY, 1); + globals::profiler->EndPass(); // Unbind ID3D11ShaderResourceView* nullSRV[1] = { nullptr }; @@ -1369,8 +1355,6 @@ void Upscaling::CopySharedD3D12Resources() { ZoneScoped; TracyD3D11Zone(globals::state->tracyCtx, "Upscaling - Copy Shared D3D12 Resources"); - globals::state->BeginPerfEvent("Copy Shared D3D12 Resources"); - auto renderer = globals::game::renderer; auto context = globals::d3d::context; @@ -1415,7 +1399,9 @@ void Upscaling::CopySharedD3D12Resources() context->PSSetShader(copyDepthToSharedBufferPS.get(), nullptr, 0); + globals::profiler->BeginPass("Upscaling::CopyDepthD3D12"); context->Draw(3, 0); + globals::profiler->EndPass(); } // Clean up @@ -1425,8 +1411,6 @@ void Upscaling::CopySharedD3D12Resources() context->OMSetRenderTargets(0, nullptr, nullptr); context->PSSetShader(nullptr, nullptr, 0); context->VSSetShader(nullptr, nullptr, 0); - - globals::state->EndPerfEvent(); } void UpdateCameraData() @@ -1700,7 +1684,6 @@ void Upscaling::Upscale() ZoneScoped; auto upscaleMethod = GetUpscaleMethod(); - auto state = globals::state; auto context = globals::d3d::context; auto renderer = globals::game::renderer; @@ -1710,7 +1693,7 @@ void Upscaling::Upscale() auto& motionVector = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; { - state->BeginPerfEvent("Encode Upscaling Textures"); + globals::profiler->BeginPass("Upscaling::EncodeTextures"); TracyD3D11Zone(globals::state->tracyCtx, "Encode Upscaling Textures"); auto& temporalAAMask = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kTEMPORAL_AA_MASK]; @@ -1768,18 +1751,14 @@ void Upscaling::Upscale() ID3D11ComputeShader* shader = nullptr; context->CSSetShader(shader, nullptr, 0); - state->EndPerfEvent(); + globals::profiler->EndPass(); } { - state->BeginPerfEvent("Upscaling"); + globals::profiler->BeginPass("Upscaling::Upscale"); TracyD3D11Zone(globals::state->tracyCtx, "Upscaling Dispatch"); if (upscaleMethod == UpscaleMethod::kDLSS) { - // VR-only workaround: a worldspace/cell transition causes ~2-3ms persistent GPU-time - // regression in the DLSS feature that only clears on a manual mode/preset toggle. - // Mirror that toggle by tearing down the DLSS feature on LoadingMenu close — the next - // SetDLSSOptions/slEvaluateFeature call below recreates it with current per-eye extents. if (globals::game::isVR && pendingDLSSReset.exchange(false, std::memory_order_relaxed)) { logger::debug("[Upscaling] LoadingMenu close detected — rebuilding DLSS feature"); streamline.DestroyDLSSResources(); @@ -1789,7 +1768,7 @@ void Upscaling::Upscale() fidelityFX.Upscale(main.texture, reactiveMaskTexture->resource.get(), transparencyCompositionMaskTexture->resource.get(), motionVector.texture, settings.sharpnessFSR); } - state->EndPerfEvent(); + globals::profiler->EndPass(); } } @@ -1858,8 +1837,6 @@ void Upscaling::UpscaleDepth() return; } - state->BeginPerfEvent("Render Target Upscaling"); - // Set up Input Assembler for fullscreen triangle (no vertex/index buffers needed) context->IASetInputLayout(nullptr); context->IASetVertexBuffers(0, 0, nullptr, nullptr, nullptr); @@ -1948,7 +1925,9 @@ void Upscaling::UpscaleDepth() context->OMSetRenderTargets(2, rtvs, depth.views[0]); context->PSSetShader(depthUpscalePS, nullptr, 0); + globals::profiler->BeginPass("Upscaling::DepthUpscale"); context->Draw(3, 0); + globals::profiler->EndPass(); } { @@ -1971,7 +1950,9 @@ void Upscaling::UpscaleDepth() context->OMSetRenderTargets(ARRAYSIZE(rtvs), rtvs, nullptr); context->PSSetShader(underwaterMaskPS, nullptr, 0); + globals::profiler->BeginPass("Upscaling::UnderwaterMaskUpscale"); context->Draw(3, 0); + globals::profiler->EndPass(); } // Now propagate the upscaled depth to kMAIN_COPY so downstream VR passes see it. @@ -1982,8 +1963,6 @@ void Upscaling::UpscaleDepth() ID3D11ShaderResourceView* nullPSResources[3] = { nullptr, nullptr, nullptr }; context->PSSetShaderResources(0, ARRAYSIZE(nullPSResources), nullPSResources); - - state->EndPerfEvent(); } void Upscaling::ApplySharpening() diff --git a/src/Features/Upscaling/RCAS/RCAS.cpp b/src/Features/Upscaling/RCAS/RCAS.cpp index d806e9d2c8..f380abe4e0 100644 --- a/src/Features/Upscaling/RCAS/RCAS.cpp +++ b/src/Features/Upscaling/RCAS/RCAS.cpp @@ -37,7 +37,6 @@ void RCAS::ApplySharpen(ID3D11ShaderResourceView* inputSRV, ID3D11UnorderedAcces ZoneScoped; TracyD3D11Zone(globals::state->tracyCtx, "RCAS Sharpening"); - auto state = globals::state; auto context = globals::d3d::context; if (!rcasComputeShader) { @@ -45,10 +44,10 @@ void RCAS::ApplySharpen(ID3D11ShaderResourceView* inputSRV, ID3D11UnorderedAcces return; } - state->BeginPerfEvent("RCAS Sharpening"); + globals::profiler->BeginPass("Upscaling::RCAS"); - uint32_t screenWidth = (uint32_t)state->screenSize.x; - uint32_t screenHeight = (uint32_t)state->screenSize.y; + uint32_t screenWidth = (uint32_t)globals::state->screenSize.x; + uint32_t screenHeight = (uint32_t)globals::state->screenSize.y; RCASConfig config{}; config.sharpness = sharpness; @@ -77,5 +76,5 @@ void RCAS::ApplySharpen(ID3D11ShaderResourceView* inputSRV, ID3D11UnorderedAcces context->CSSetShader(nullptr, nullptr, 0); - state->EndPerfEvent(); + globals::profiler->EndPass(); } diff --git a/src/Features/VRStereoOptimizations.cpp b/src/Features/VRStereoOptimizations.cpp index 63d5da8942..fd287fed39 100644 --- a/src/Features/VRStereoOptimizations.cpp +++ b/src/Features/VRStereoOptimizations.cpp @@ -379,7 +379,9 @@ void VRStereoOptimizations::DispatchStencil() uint32_t fullWidth = texPerPixelMode->desc.Width; uint32_t fullHeight = texPerPixelMode->desc.Height; + globals::profiler->BeginPass("VR::StencilClassify"); context->Dispatch((fullWidth + 7) / 8, (fullHeight + 7) / 8, 1); + globals::profiler->EndPass(); } // Cleanup CS bindings @@ -395,7 +397,9 @@ void VRStereoOptimizations::DispatchStencil() // Transfer classification to hardware stencil buffer { TracyD3D11Zone(globals::state->tracyCtx, "StereoOpt - Stencil Write"); + globals::profiler->BeginPass("VR::StencilWrite"); ExecuteStencilWritePass(); + globals::profiler->EndPass(); } stencilActive = true; diff --git a/src/Features/VolumetricShadows.cpp b/src/Features/VolumetricShadows.cpp index 5bc31c8a04..7f94a06266 100644 --- a/src/Features/VolumetricShadows.cpp +++ b/src/Features/VolumetricShadows.cpp @@ -1,8 +1,15 @@ #include "VolumetricShadows.h" +#include "Globals.h" #include "State.h" #include "Utils/D3D.h" +#include "RE/B/BSShadowDirectionalLight.h" + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + VolumetricShadows::Settings, + BlurRadius) + void VolumetricShadows::SetupResources() { auto device = globals::d3d::device; @@ -21,6 +28,28 @@ void VolumetricShadows::SetupResources() Util::SetResourceName(linearSampler, "VolumetricShadows::LinearSampler"); } + // Create linearization cbuffer + { + D3D11_BUFFER_DESC cbDesc{}; + cbDesc.ByteWidth = sizeof(VSMLinearizeCB); + cbDesc.Usage = D3D11_USAGE_DYNAMIC; + cbDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + cbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + DX::ThrowIfFailed(device->CreateBuffer(&cbDesc, nullptr, &linearizeCB)); + Util::SetResourceName(linearizeCB, "VolumetricShadows::LinearizeCB"); + } + + // Create blur cbuffer + { + D3D11_BUFFER_DESC cbDesc{}; + cbDesc.ByteWidth = sizeof(BlurCB); + cbDesc.Usage = D3D11_USAGE_DYNAMIC; + cbDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + cbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + DX::ThrowIfFailed(device->CreateBuffer(&cbDesc, nullptr, &blurCB)); + Util::SetResourceName(blurCB, "VolumetricShadows::BlurCB"); + } + // Compile compute shaders std::vector> defines; defines.push_back({ "DOWNSAMPLE_SHADOW_MIP0", nullptr }); @@ -71,6 +100,50 @@ void VolumetricShadows::ClearShaderCache() blurShadowVerticalCS = static_cast(Util::CompileShader(L"Data\\Shaders\\VolumetricShadows\\BlurShadowCS.hlsl", defines, "cs_5_0")); } +void VolumetricShadows::ExtractCascadeNearFar() +{ + auto* shadowSceneNode = globals::game::smState->shadowSceneNode[0]; + if (!shadowSceneNode) + return; + + auto* sunShadowLight = shadowSceneNode->GetRuntimeData().sunShadowDirLight; + if (!sunShadowLight) + return; + + auto extractCascade = [&](RE::NiCamera* camera, const REX::W32::XMFLOAT4X4& transform, uint32_t cascadeIdx) { + if (camera) { + auto& frustum = camera->GetRuntimeData2().viewFrustum; + cascadeNear[cascadeIdx] = frustum.fNear; + cascadeFar[cascadeIdx] = frustum.fFar; + } + // Extract world-to-UV scale from shadow projection matrix + // Column 0 of the effective HLSL matrix = row 0 cross rows of C++ row-major storage + // The UV-per-world-unit scale is the length of the first output component's gradient + float sx = transform.m[0][0]; + float sy = transform.m[1][0]; + float sz = transform.m[2][0]; + cascadeScale[cascadeIdx] = std::sqrt(sx * sx + sy * sy + sz * sz); + }; + + if (globals::game::isVR) { + auto& lightData = sunShadowLight->GetVRRuntimeData(); + const auto count = std::min(lightData.shadowmapDescriptors.size(), 2u); + for (uint32_t i = 0; i < count; i++) + extractCascade(lightData.shadowmapDescriptors[i].camera[0].get(), lightData.shadowmapDescriptors[i].lightTransform, i); + } else { + auto& lightData = sunShadowLight->GetRuntimeData(); + const auto count = std::min(lightData.shadowmapDescriptors.size(), 2u); + for (uint32_t i = 0; i < count; i++) + extractCascade(lightData.shadowmapDescriptors[i].camera.get(), lightData.shadowmapDescriptors[i].lightTransform, i); + } +} + +float4 VolumetricShadows::GetCascadeDepthParams() +{ + ExtractCascadeNearFar(); + return { cascadeNear[0], cascadeFar[0], cascadeNear[1], cascadeFar[1] }; +} + void VolumetricShadows::CopyShadowLightData() { ZoneScoped; @@ -86,7 +159,7 @@ void VolumetricShadows::CopyShadowLightData() context->PSGetShaderResources(4, 1, &shadowView); - // Downsample shadow texture array to fixed 512x512 (mip1: 256x256) + // Downsample shadow texture array to fixed size if (shadowView) { constexpr uint32_t SHADOW_COPY_SIZE = 512; @@ -100,7 +173,7 @@ void VolumetricShadows::CopyShadowLightData() copyDesc.Height = SHADOW_COPY_SIZE; copyDesc.MipLevels = 2; copyDesc.ArraySize = 1; - copyDesc.Format = DXGI_FORMAT_R16G16_UNORM; + copyDesc.Format = DXGI_FORMAT_R16G16B16A16_UNORM; copyDesc.SampleDesc.Count = 1; copyDesc.SampleDesc.Quality = 0; copyDesc.Usage = D3D11_USAGE_DEFAULT; @@ -165,6 +238,17 @@ void VolumetricShadows::CopyShadowLightData() Util::SetResourceName(shadowBlurTempMip1UAV, "VolumetricShadows::ShadowBlurTemp UAV mip1"); } + // Extract cascade near/far and projection scale + ExtractCascadeNearFar(); + + // Compute per-cascade blur radii for consistent world-space softness + // Mip 0 (512x512) = cascade 1, Mip 1 (256x256) = cascade 0 + // pixelRadius = worldRadius * cascadeScale * textureSize + uint32_t blurRadiusMip0 = std::max(1u, std::min(32u, + static_cast(std::round(settings.BlurRadius * cascadeScale[1] * float(SHADOW_COPY_SIZE))))); + uint32_t blurRadiusMip1 = std::max(1u, std::min(32u, + static_cast(std::round(settings.BlurRadius * cascadeScale[0] * float(SHADOW_COPY_SIZE / 2))))); + // Get input dimensions for dispatch sizing ID3D11Resource* shadowResource = nullptr; shadowView->GetResource(&shadowResource); @@ -189,17 +273,40 @@ void VolumetricShadows::CopyShadowLightData() // Dispatch covers full input: each thread gathers 2x2, 8 threads per group auto dispatchSize = srcDesc.Width / 16; - // Mip 0 (cascade 1) + // Mip 0 (cascade 1) - update cbuffer with cascade 1 near/far + { + D3D11_MAPPED_SUBRESOURCE mapped{}; + DX::ThrowIfFailed(context->Map(linearizeCB, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped)); + auto* cb = static_cast(mapped.pData); + cb->CascadeNear = cascadeNear[1]; + cb->CascadeFar = cascadeFar[1]; + context->Unmap(linearizeCB, 0); + context->CSSetConstantBuffers(0, 1, &linearizeCB); + } + ID3D11UnorderedAccessView* csUavs[1]{ shadowCopyMip0UAV }; context->CSSetUnorderedAccessViews(0, 1, csUavs, nullptr); context->CSSetShader(downsampleShadowMip0CS, nullptr, 0); + globals::profiler->BeginPass("VolumetricShadows::DownsampleMip0"); context->Dispatch(dispatchSize, dispatchSize, 1); + globals::profiler->EndPass(); + + // Mip 1 (cascade 0) - update cbuffer with cascade 0 near/far + { + D3D11_MAPPED_SUBRESOURCE mapped{}; + DX::ThrowIfFailed(context->Map(linearizeCB, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped)); + auto* cb = static_cast(mapped.pData); + cb->CascadeNear = cascadeNear[0]; + cb->CascadeFar = cascadeFar[0]; + context->Unmap(linearizeCB, 0); + } - // Mip 1 (cascade 0) csUavs[0] = shadowCopyMip1UAV; context->CSSetUnorderedAccessViews(0, 1, csUavs, nullptr); context->CSSetShader(downsampleShadowMip1CS, nullptr, 0); + globals::profiler->BeginPass("VolumetricShadows::DownsampleMip1"); context->Dispatch(dispatchSize, dispatchSize, 1); + globals::profiler->EndPass(); // Unbind SRVs before blur passes csSrvs[0] = nullptr; @@ -207,21 +314,35 @@ void VolumetricShadows::CopyShadowLightData() context->CSSetShaderResources(0, 2, csSrvs); csUavs[0] = nullptr; context->CSSetUnorderedAccessViews(0, 1, csUavs, nullptr); + ID3D11Buffer* nullCB = nullptr; + context->CSSetConstantBuffers(0, 1, &nullCB); constexpr uint32_t mip0Size = SHADOW_COPY_SIZE; constexpr uint32_t mip1Size = SHADOW_COPY_SIZE / 2; - // 11x11 separable blur for Mip 0 + // Separable blur for Mip 0 { const uint32_t GROUP_SIZE = 128; + // Update blur cbuffer for mip 0 + { + D3D11_MAPPED_SUBRESOURCE mapped{}; + DX::ThrowIfFailed(context->Map(blurCB, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped)); + auto* cb = static_cast(mapped.pData); + cb->BlurRadius = blurRadiusMip0; + context->Unmap(blurCB, 0); + context->CSSetConstantBuffers(0, 1, &blurCB); + } + // Horizontal pass: shadowCopy mip0 -> shadowBlurTemp mip0 ID3D11ShaderResourceView* blurSrvs[1]{ shadowCopyMip0SRV }; context->CSSetShaderResources(0, 1, blurSrvs); csUavs[0] = shadowBlurTempMip0UAV; context->CSSetUnorderedAccessViews(0, 1, csUavs, nullptr); context->CSSetShader(blurShadowHorizontalCS, nullptr, 0); + globals::profiler->BeginPass("VolumetricShadows::BlurHMip0"); context->Dispatch((mip0Size + GROUP_SIZE - 1) / GROUP_SIZE, mip0Size, 1); + globals::profiler->EndPass(); // Unbind for next pass blurSrvs[0] = nullptr; @@ -235,7 +356,9 @@ void VolumetricShadows::CopyShadowLightData() csUavs[0] = shadowCopyMip0UAV; context->CSSetUnorderedAccessViews(0, 1, csUavs, nullptr); context->CSSetShader(blurShadowVerticalCS, nullptr, 0); + globals::profiler->BeginPass("VolumetricShadows::BlurVMip0"); context->Dispatch(mip0Size, (mip0Size + GROUP_SIZE - 1) / GROUP_SIZE, 1); + globals::profiler->EndPass(); // Unbind blurSrvs[0] = nullptr; @@ -244,17 +367,28 @@ void VolumetricShadows::CopyShadowLightData() context->CSSetUnorderedAccessViews(0, 1, csUavs, nullptr); } - // 11x11 separable blur for Mip 1 + // Separable blur for Mip 1 { const uint32_t GROUP_SIZE = 128; + // Update blur cbuffer for mip 1 + { + D3D11_MAPPED_SUBRESOURCE mapped{}; + DX::ThrowIfFailed(context->Map(blurCB, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped)); + auto* cb = static_cast(mapped.pData); + cb->BlurRadius = blurRadiusMip1; + context->Unmap(blurCB, 0); + } + // Horizontal pass: shadowCopy mip1 -> shadowBlurTemp mip1 ID3D11ShaderResourceView* blurSrvs[1]{ shadowCopyMip1SRV }; context->CSSetShaderResources(0, 1, blurSrvs); csUavs[0] = shadowBlurTempMip1UAV; context->CSSetUnorderedAccessViews(0, 1, csUavs, nullptr); context->CSSetShader(blurShadowHorizontalCS, nullptr, 0); + globals::profiler->BeginPass("VolumetricShadows::BlurHMip1"); context->Dispatch((mip1Size + GROUP_SIZE - 1) / GROUP_SIZE, mip1Size, 1); + globals::profiler->EndPass(); // Unbind for next pass blurSrvs[0] = nullptr; @@ -268,7 +402,9 @@ void VolumetricShadows::CopyShadowLightData() csUavs[0] = shadowCopyMip1UAV; context->CSSetUnorderedAccessViews(0, 1, csUavs, nullptr); context->CSSetShader(blurShadowVerticalCS, nullptr, 0); + globals::profiler->BeginPass("VolumetricShadows::BlurVMip1"); context->Dispatch(mip1Size, (mip1Size + GROUP_SIZE - 1) / GROUP_SIZE, 1); + globals::profiler->EndPass(); // Unbind blurSrvs[0] = nullptr; @@ -280,6 +416,8 @@ void VolumetricShadows::CopyShadowLightData() // Cleanup CS state ID3D11SamplerState* nullSampler = nullptr; context->CSSetSamplers(0, 1, &nullSampler); + ID3D11Buffer* nullCB2 = nullptr; + context->CSSetConstantBuffers(0, 1, &nullCB2); context->CSSetShader(nullptr, nullptr, 0); shadowTexture->Release(); @@ -304,8 +442,24 @@ void VolumetricShadows::SetSharedShadowMapSRV(ID3D11DeviceContext* a_context, ID void VolumetricShadows::DrawSettings() { + ImGui::SliderFloat("Blur Radius", &settings.BlurRadius, 0.0f, 500.0f, "%.0f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Blur radius in world units. Both cascades are scaled to match this world-space softness."); + ImGui::SeparatorText("Debug"); + if (ImGui::TreeNode("Info")) { + ImGui::Text("Cascade 0: scale=%.6f near=%.1f far=%.1f", cascadeScale[0], cascadeNear[0], cascadeFar[0]); + ImGui::Text("Cascade 1: scale=%.6f near=%.1f far=%.1f", cascadeScale[1], cascadeNear[1], cascadeFar[1]); + + uint32_t blurMip0 = std::max(1u, std::min(32u, + static_cast(std::round(settings.BlurRadius * cascadeScale[1] * float(shadowCopyWidth))))); + uint32_t blurMip1 = std::max(1u, std::min(32u, + static_cast(std::round(settings.BlurRadius * cascadeScale[0] * float(shadowCopyWidth / 2))))); + ImGui::Text("Blur pixels: mip0=%u mip1=%u", blurMip0, blurMip1); + ImGui::TreePop(); + } + if (ImGui::TreeNode("Buffer Viewer")) { static float debugRescale = .3f; ImGui::SliderFloat("View Resize", &debugRescale, 0.f, 1.f); @@ -323,26 +477,26 @@ void VolumetricShadows::DrawSettings() } }; - DisplayRT("VSM Cascade 0", shadowCopyTexture, shadowCopyMip0SRV); - DisplayRT("VSM Cascade 1", shadowCopyTexture, shadowCopyMip1SRV); + DisplayRT("MSM Cascade 0", shadowCopyTexture, shadowCopyMip0SRV); + DisplayRT("MSM Cascade 1", shadowCopyTexture, shadowCopyMip1SRV); ImGui::TreePop(); } } -void VolumetricShadows::LoadSettings(json&) +void VolumetricShadows::LoadSettings(json& o_json) { - // No settings currently + settings = o_json; } -void VolumetricShadows::SaveSettings(json&) +void VolumetricShadows::SaveSettings(json& o_json) { - // No settings currently + o_json = settings; } void VolumetricShadows::RestoreDefaultSettings() { - // No settings currently + settings = {}; } struct CreateDepthStencil_VolumetricLighting @@ -350,8 +504,8 @@ struct CreateDepthStencil_VolumetricLighting static void thunk(RE::BSGraphics::Renderer* This, uint32_t a_target, RE::BSGraphics::DepthStencilTargetProperties* a_properties) { RE::BSGraphics::DepthStencilTargetProperties properties = *a_properties; - a_properties->height = 1024; - a_properties->width = 1024; + properties.height = 1024; + properties.width = 1024; func(This, a_target, &properties); } static inline REL::Relocation func; diff --git a/src/Features/VolumetricShadows.h b/src/Features/VolumetricShadows.h index fb596dad6d..03d95febd5 100644 --- a/src/Features/VolumetricShadows.h +++ b/src/Features/VolumetricShadows.h @@ -17,9 +17,9 @@ struct VolumetricShadows : Feature virtual std::pair> GetFeatureSummary() override { return { - "Volumetric Shadows provides downsampled VSM shadow maps for use by effects like particles and decals.\n" + "Volumetric Shadows provides downsampled MSM shadow maps for use by effects like particles and decals.\n" "This improves shadow quality on transparent objects with minimal performance impact.", - { "Downsampled VSM shadows", + { "Downsampled MSM shadows", "Gaussian blur filtering", "Multi-cascade support", "Optimized for effects rendering" } @@ -28,6 +28,27 @@ struct VolumetricShadows : Feature bool HasShaderDefine(RE::BSShader::Type shaderType) override; + struct Settings + { + float BlurRadius = 100.0f; + }; + Settings settings; + + struct alignas(16) VSMLinearizeCB + { + float CascadeNear; + float CascadeFar; + uint32_t _pad[2]; + }; + + struct alignas(16) BlurCB + { + uint32_t BlurRadius; + uint32_t _pad[3]; + }; + + float4 GetCascadeDepthParams(); + // Compute shaders ID3D11ComputeShader* downsampleShadowMip0CS = nullptr; ID3D11ComputeShader* downsampleShadowMip1CS = nullptr; @@ -53,6 +74,15 @@ struct VolumetricShadows : Feature ID3D11UnorderedAccessView* shadowBlurTempMip0UAV = nullptr; ID3D11UnorderedAccessView* shadowBlurTempMip1UAV = nullptr; + // Cbuffers + ID3D11Buffer* linearizeCB = nullptr; + ID3D11Buffer* blurCB = nullptr; + + // Cached cascade near/far values and projection scale + float cascadeNear[2] = { 0.f, 0.f }; + float cascadeFar[2] = { 1.f, 1.f }; + float cascadeScale[2] = { 1.f, 1.f }; + // Samplers ID3D11SamplerState* linearSampler = nullptr; @@ -72,4 +102,5 @@ struct VolumetricShadows : Feature private: static void SetSharedShadowMapSRV(ID3D11DeviceContext* a_context, ID3D11ShaderResourceView* a_srv); + void ExtractCascadeNearFar(); }; diff --git a/src/Globals.cpp b/src/Globals.cpp index 850d0f9dce..4a346d9c62 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -3,6 +3,7 @@ #include "Deferred.h" #include "Features/CloudShadows.h" #include "Features/DynamicCubemaps.h" +#include "Features/Effect11.h" #include "Features/ExponentialHeightFog.h" #include "Features/ExtendedMaterials.h" #include "Features/ExtendedTranslucency.h" @@ -36,6 +37,7 @@ #include "Features/WaterEffects.h" #include "Features/WeatherEditor.h" #include "Features/WetnessEffects.h" +#include "Profiler.h" #include "Menu.h" #include "ShaderCache.h" #include "State.h" @@ -84,6 +86,7 @@ namespace globals ExtendedTranslucency extendedTranslucency{}; Upscaling upscaling{}; HDRDisplay hdrDisplay{}; + Effect11 effect11{}; RenderDoc renderDoc{}; ScreenshotFeature screenshotFeature{}; WeatherEditor weatherEditor{}; @@ -156,6 +159,9 @@ namespace globals Menu* menu = nullptr; SIE::ShaderCache* shaderCache = nullptr; + static Profiler profilerInstance; + Profiler* profiler = &profilerInstance; + void OnInit() { shaderCache = &SIE::ShaderCache::Instance(); diff --git a/src/Globals.h b/src/Globals.h index a5e262a768..76eb25eb39 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -33,6 +33,7 @@ struct WetnessEffects; struct ExtendedTranslucency; struct Upscaling; struct WeatherEditor; +struct Effect11; struct ExponentialHeightFog; struct HDRDisplay; struct ScreenshotFeature; @@ -42,6 +43,7 @@ class Deferred; struct TruePBR; class RenderDoc; class Menu; +class Profiler; namespace SIE { @@ -91,6 +93,7 @@ namespace globals extern ExtendedTranslucency extendedTranslucency; extern Upscaling upscaling; extern HDRDisplay hdrDisplay; + extern Effect11 effect11; extern RenderDoc renderDoc; extern ScreenshotFeature screenshotFeature; extern WeatherEditor weatherEditor; @@ -262,6 +265,7 @@ namespace globals extern Deferred* deferred; extern Menu* menu; extern SIE::ShaderCache* shaderCache; + extern Profiler* profiler; void OnInit(); void ReInit(); diff --git a/src/Hooks.cpp b/src/Hooks.cpp index cd7dbb0538..99fc0ebcab 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -10,9 +10,11 @@ #include "State.h" #include "Util.h" +#include "Features/Effect11.h" #include "Features/HDRDisplay.h" #include "Features/InteriorSun.h" #include "Features/LightLimitFix.h" +#include "Features/SkySync.h" #include "Features/Upscaling.h" #include "Features/VR.h" #include "Features/VolumetricLighting.h" @@ -588,6 +590,28 @@ namespace Hooks static inline REL::Relocation func; }; + // Fix this texture ruining precision + struct CreateRenderTarget_Snow + { + static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) + { + a_properties->format.set(RE::BSGraphics::Format::kR16G16B16A16_FLOAT); + func(This, a_target, a_properties); + } + static inline REL::Relocation func; + }; + + // Fix this texture ruining precision + struct CreateRenderTarget_SnowSwap + { + static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) + { + a_properties->format.set(RE::BSGraphics::Format::kR16G16B16A16_FLOAT); + func(This, a_target, a_properties); + } + static inline REL::Relocation func; + }; + // kNORMAL_TAAMASK_SSRMASK and its swap need UAV bind because DeferredCompositeCS // writes vanilla-encoded normals through UAV1 (`normals.UAV` in Deferred::DeferredPasses), // which feeds the post-pass vanilla SSAO chain (ISSAORawAO -> ISSAOComposite). Without @@ -893,6 +917,13 @@ namespace Hooks static inline REL::Relocation func; }; + void Sky_UpdateColors::thunk(RE::Sky* sky, float a_delta) + { + func(sky, a_delta); + globals::features::effect11.OnSkyUpdateColors(sky); + globals::features::skySync.OnSkyUpdateColors(sky); + } + /** * @brief Installs hooks, detours, and memory patches for graphics, input, and rendering subsystems. * @@ -932,6 +963,8 @@ namespace Hooks logger::info("Hooking BSShaderRenderTargets::Create::CreateRenderTarget(s)"); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x3F0, 0x3F3, 0x548)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x406, 0x409, 0x55E)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x41C, 0x41F, 0x574)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x458, 0x45B, 0x5B0)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x46B, 0x46E, 0x5C3)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x4F0, 0x4EF, 0x64E)); @@ -959,6 +992,9 @@ namespace Hooks logger::info("Hooking TESWaterReflections::Update_Actor::GetLOSPosition for Sky Reflection Fix"); stl::write_thunk_call(REL::RelocationID(31373, 32160).address() + REL::Relocate(0x1AD, 0x1CA, 0x1ed)); + logger::info("Hooking Sky::UpdateColors"); + stl::detour_thunk(REL::RelocationID(25686, 26233)); + logger::info("Installing SetupGeometry hooks"); stl::write_vfunc<0x6, EffectExtensions::BSEffectShader_SetupGeometry>(RE::VTABLE_BSEffectShader[0]); stl::write_vfunc<0x6, SkyExtensions::BSSkyShader_SetupGeometry>(RE::VTABLE_BSSkyShader[0]); diff --git a/src/Hooks.h b/src/Hooks.h index 335a7df7d8..b87fcc6c57 100644 --- a/src/Hooks.h +++ b/src/Hooks.h @@ -19,6 +19,12 @@ namespace Hooks static inline REL::Relocation func; }; + struct Sky_UpdateColors + { + static void thunk(RE::Sky* sky, float a_delta); + static inline REL::Relocation func; + }; + void Install(); void InstallEarlyHooks(); } diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index 978984eb7d..81b2382726 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -16,6 +16,7 @@ #include "Globals.h" #include "Menu.h" #include "Menu/HomePageRenderer.h" +#include "Menu/StatisticsRenderer.h" #include "Menu/ThemeManager.h" #include "SceneSettingsManager.h" #include "SettingsOverrideManager.h" @@ -26,7 +27,7 @@ namespace { // Core built-in menu names that always appear first in the menu list - constexpr std::array CORE_MENU_NAMES = { "Home", "General", "Advanced", "Display" }; + constexpr std::array CORE_MENU_NAMES = { "Home", "General", "Advanced", "Profiling", "Display" }; bool IsCoreMenu(const std::string& menuName) { @@ -280,7 +281,8 @@ std::vector FeatureListRenderer::BuildMenuLis auto menuList = std::vector{ BuiltInMenu{ "Home", []() { HomePageRenderer::RenderHomePage(); } }, BuiltInMenu{ "General", drawGeneralSettings }, - BuiltInMenu{ "Advanced", drawAdvancedSettings } + BuiltInMenu{ "Advanced", drawAdvancedSettings }, + BuiltInMenu{ "Profiling", []() { StatisticsRenderer::RenderStatistics(); } } }; // NOTE: The menu list is rebuilt every frame, so category expansion states // persist correctly. This is acceptable since the list is small and built // infrequently, but could be optimized if performance becomes an issue. @@ -751,6 +753,10 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureSettings(Feature* feat, ImVec2 cursorPosBefore = ImGui::GetCursorPos(); feat->DrawSettings(); + + ImGui::SeparatorText("Performance"); + StatisticsRenderer::RenderFeatureTimers(feat->GetShortName()); + ImVec2 cursorPosAfter = ImGui::GetCursorPos(); if (sceneControlled) @@ -834,7 +840,7 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureSettings(Feature* feat, void FeatureListRenderer::DrawMenuVisitor::RenderRestoreDefaultsButton(Feature* feat, bool isDisabled, bool isLoaded) { - if (isDisabled || !isLoaded) { + if (isDisabled || !isLoaded || feat->GetShortName() == "Effect11") { return; } diff --git a/src/Menu/OverlayRenderer.cpp b/src/Menu/OverlayRenderer.cpp index 01d5f619e9..1a68e051ae 100644 --- a/src/Menu/OverlayRenderer.cpp +++ b/src/Menu/OverlayRenderer.cpp @@ -12,6 +12,7 @@ #include "Feature.h" #include "FeatureIssues.h" +#include "Features/Effect11/EffectManager.h" #include "Features/RenderDoc.h" #include "Globals.h" #include "Menu.h" @@ -35,12 +36,30 @@ namespace void DrawShaderCompilationFailures(uint64_t failed, const Menu::ThemeSettings& themeSettings) { - ImGui::TextColored(themeSettings.StatusPalette.Error, - "ERROR: %llu shaders failed to compile. Check installation and CommunityShaders.log", - static_cast(failed)); + if (failed) { + ImGui::TextColored(themeSettings.StatusPalette.Error, + "ERROR: %llu shaders failed to compile. Check installation and CommunityShaders.log", + static_cast(failed)); - if (FeatureIssues::HasPotentialShaderModifyingFeatures()) { - ImGui::TextColored(themeSettings.StatusPalette.Error, "Features that may have modified shaders detected. Check Feature Issues in the Menu."); + if (FeatureIssues::HasPotentialShaderModifyingFeatures()) { + ImGui::TextColored(themeSettings.StatusPalette.Error, "Features that may have modified shaders detected. Check Feature Issues in the Menu."); + } + } + } + + void DrawEffect11Errors(const Menu::ThemeSettings& themeSettings) + { + auto& effectManager = EffectManager::GetSingleton(); + if (!effectManager.IsInitialized()) + return; + + uint32_t effectFailed = effectManager.GetFailedEffectCount(); + if (effectFailed) { + ImGui::TextColored(themeSettings.StatusPalette.Error, + "ERROR: %u effect(s) failed to compile", + effectFailed); + for (const auto& err : effectManager.GetAllErrors()) + ImGui::TextColored(themeSettings.StatusPalette.Error, " %s", err.c_str()); } } @@ -197,11 +216,14 @@ bool OverlayRenderer::ShouldSkipRendering() auto* abTestingManager = ABTestingManager::GetSingleton(); auto* renderDoc = RenderDoc::GetSingleton(); + uint32_t effectFailed = EffectManager::GetSingleton().IsInitialized() ? EffectManager::GetSingleton().GetFailedEffectCount() : 0; + return !(shaderCache->IsCompiling() || Menu::GetSingleton()->IsEnabled || EditorWindow::GetSingleton()->open || abTestingManager->IsEnabled() || (failed && !hide) || + effectFailed || globals::features::performanceOverlay.settings.ShowInOverlay || renderDoc->IsAvailable()); } @@ -271,6 +293,8 @@ void OverlayRenderer::RenderShaderCompilationStatus(const std::functionIsCompiling()) { ImGui::SetNextWindowPos(ImVec2(pos, pos)); if (!ImGui::Begin("ShaderCompilationInfo", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings)) { @@ -303,9 +327,9 @@ void OverlayRenderer::RenderShaderCompilationStatus(const std::function +#include +#include +#include + +#include "Globals.h" +#include "State.h" + +static ImU32 HslToImU32(float h, float s, float l) +{ + auto hue2rgb = [](float p, float q, float t) -> float { + if (t < 0.0f) + t += 1.0f; + if (t > 1.0f) + t -= 1.0f; + if (t < 1.0f / 6.0f) + return p + (q - p) * 6.0f * t; + if (t < 0.5f) + return q; + if (t < 2.0f / 3.0f) + return p + (q - p) * (2.0f / 3.0f - t) * 6.0f; + return p; + }; + + float q = l < 0.5f ? l * (1.0f + s) : l + s - l * s; + float p = 2.0f * l - q; + float r = hue2rgb(p, q, h + 1.0f / 3.0f); + float g = hue2rgb(p, q, h); + float b = hue2rgb(p, q, h - 1.0f / 3.0f); + + return IM_COL32( + static_cast(r * 255.0f), + static_cast(g * 255.0f), + static_cast(b * 255.0f), + 255); +} + +static constexpr float kGoldenRatio = 0.618033988749895f; + +ImU32 StatisticsRenderer::GetGroupColor(const std::string& groupName) +{ + auto it = groupColorMap.find(groupName); + if (it != groupColorMap.end()) + return it->second; + + float hue = std::fmod(nextColorIndex * kGoldenRatio, 1.0f); + ImU32 color = HslToImU32(hue, 0.7f, 0.55f); + groupColorMap[groupName] = color; + nextColorIndex++; + return color; +} + +uint32_t StatisticsRenderer::ToLegitColor(ImU32 imColor) +{ + uint8_t r = (imColor >> 0) & 0xFF; + uint8_t g = (imColor >> 8) & 0xFF; + uint8_t b = (imColor >> 16) & 0xFF; + return (0xFF << 24) | (b << 16) | (g << 8) | r; +} + +ImVec4 StatisticsRenderer::HeatColor(float value, float maxValue) +{ + if (maxValue <= 0.0f) + return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + + float x = std::clamp(value / maxValue, 0.0f, 1.0f); + + float x2 = x * x; + float x3 = x2 * x; + float x4 = x2 * x2; + float x5 = x3 * x2; + + float r = 0.13572138f + 4.61539260f * x - 42.66032258f * x2 + 132.13108234f * x3 - 152.94239396f * x4 + 59.28637943f * x5; + float g = 0.09140261f + 2.19418839f * x + 4.84296658f * x2 - 14.18503333f * x3 + 4.27729857f * x4 + 2.82956604f * x5; + float b = 0.10667330f + 12.64194608f * x - 60.58204836f * x2 + 110.36276771f * x3 - 89.90310912f * x4 + 27.34824973f * x5; + + float alpha = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg).w; + + return ImVec4(std::clamp(r, 0.0f, 1.0f), std::clamp(g, 0.0f, 1.0f), std::clamp(b, 0.0f, 1.0f), alpha); +} + +void StatisticsRenderer::TextHeat(const char* fmt, float value, float maxValue) +{ + ImVec4 bg = HeatColor(value, maxValue); + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ImGui::GetColorU32(bg)); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), fmt, value); +} + +void StatisticsRenderer::RenderGraph() +{ + auto& profiler = (*globals::profiler); + const auto& results = profiler.GetResults(); + bool cpuMode = (timingMode == TimingMode::CPU); + + if (results.empty()) + return; + + std::vector tasks; + + double accumulated = 0.0; + for (const auto& result : results) { + if (!result.valid) + continue; + + float timeMs = cpuMode ? result.cpuTimeMs : result.gpuTimeMs; + + std::string groupName; + auto pos = result.name.find("::"); + if (pos != std::string::npos) + groupName = result.name.substr(0, pos); + else + groupName = result.name; + + legit::ProfilerTask task; + task.startTime = accumulated / 1000.0; + task.endTime = (accumulated + timeMs) / 1000.0; + task.name = result.name; + task.color = ToLegitColor(GetGroupColor(groupName)); + tasks.push_back(task); + accumulated += timeMs; + } + + if (tasks.empty()) + return; + + gpuGraph.LoadFrameData(tasks.data(), tasks.size()); + + float maxFrameTimeSec = gpuGraph.GetPeakFrameTime() * 1.2f; + if (maxFrameTimeSec < 0.0001f) + maxFrameTimeSec = 0.0001f; + + float availWidth = ImGui::GetContentRegionAvail().x; + int legendWidth = 260; + int graphWidth = std::max(100, static_cast(availWidth) - legendWidth); + int graphHeight = 180; + + gpuGraph.RenderTimings(graphWidth, legendWidth, graphHeight, 0, maxFrameTimeSec); + + ImGui::Spacing(); +} + +void StatisticsRenderer::RenderStatistics() +{ + auto& profiler = (*globals::profiler); + + int mode = static_cast(timingMode); + ImGui::RadioButton("GPU", &mode, 0); + ImGui::SameLine(); + ImGui::RadioButton("CPU", &mode, 1); + if (static_cast(mode) != timingMode) { + timingMode = static_cast(mode); + timeSinceLastUpdate = 1.0f; + } + + bool cpuMode = (timingMode == TimingMode::CPU); + + ImGui::Separator(); + + float currentTime = static_cast(ImGui::GetTime()); + float deltaTime = currentTime - lastFrameTime; + lastFrameTime = currentTime; + timeSinceLastUpdate += deltaTime; + + if (timeSinceLastUpdate >= 1.0f) { + timeSinceLastUpdate = 0.0f; + + cachedGroups.clear(); + cachedTotalAvgMs = 0.0f; + cachedTotalP95Ms = 0.0f; + cachedTotalP99Ms = 0.0f; + cachedMaxAvgMs = 0.0f; + cachedMaxP95Ms = 0.0f; + cachedMaxP99Ms = 0.0f; + std::unordered_map groupIndex; + + for (const auto& result : profiler.GetResults()) { + if (!result.valid) + continue; + + float avg = cpuMode ? result.cpuAvgMs : result.avgMs; + float p95 = cpuMode ? result.cpuP95Ms : result.p95Ms; + float p99 = cpuMode ? result.cpuP99Ms : result.p99Ms; + + cachedTotalAvgMs += avg; + cachedTotalP95Ms += p95; + cachedTotalP99Ms += p99; + + auto pos = result.name.find("::"); + if (pos != std::string::npos) { + std::string groupName = result.name.substr(0, pos); + std::string passLabel = result.name.substr(pos + 2); + + auto it = groupIndex.find(groupName); + if (it == groupIndex.end()) { + groupIndex[groupName] = cachedGroups.size(); + cachedGroups.push_back({ groupName, 0, 0, 0 }); + } + + auto& group = cachedGroups[groupIndex[groupName]]; + group.totalAvgMs += avg; + group.totalP95Ms += p95; + group.totalP99Ms += p99; + group.passes.push_back({ passLabel, avg, p95, p99 }); + } else { + groupIndex[result.name] = cachedGroups.size(); + cachedGroups.push_back({ result.name, avg, p95, p99 }); + } + } + + for (const auto& group : cachedGroups) { + cachedMaxAvgMs = std::max(cachedMaxAvgMs, group.totalAvgMs); + cachedMaxP95Ms = std::max(cachedMaxP95Ms, group.totalP95Ms); + cachedMaxP99Ms = std::max(cachedMaxP99Ms, group.totalP99Ms); + } + } + + if (cachedGroups.empty()) { + ImGui::TextDisabled("No timing data available (enter game world)"); + return; + } + + RenderGraph(); + + float availHeight = ImGui::GetContentRegionAvail().y - ImGui::GetFrameHeightWithSpacing(); + + if (ImGui::BeginTable("##Profiler", 5, + ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_PadOuterX | ImGuiTableFlags_ScrollY, + ImVec2(0.0f, availHeight))) { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn("Pass", ImGuiTableColumnFlags_WidthStretch, 3.0f); + ImGui::TableSetupColumn("Avg", ImGuiTableColumnFlags_WidthFixed, 55.0f); + ImGui::TableSetupColumn("P95", ImGuiTableColumnFlags_WidthFixed, 55.0f); + ImGui::TableSetupColumn("P99", ImGuiTableColumnFlags_WidthFixed, 55.0f); + ImGui::TableSetupColumn("%%", ImGuiTableColumnFlags_WidthFixed, 45.0f); + ImGui::TableHeadersRow(); + + for (const auto& group : cachedGroups) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + if (group.passes.empty()) { + ImGui::TreeNodeEx(group.name.c_str(), ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen); + ImGui::TableNextColumn(); + TextHeat("%.3f", group.totalAvgMs, cachedMaxAvgMs); + ImGui::TableNextColumn(); + TextHeat("%.3f", group.totalP95Ms, cachedMaxP95Ms); + ImGui::TableNextColumn(); + TextHeat("%.3f", group.totalP99Ms, cachedMaxP99Ms); + ImGui::TableNextColumn(); + if (cachedTotalAvgMs > 0.0f) + TextHeat("%5.1f", (group.totalAvgMs / cachedTotalAvgMs) * 100.0f, 100.0f); + } else { + bool open = ImGui::TreeNodeEx(group.name.c_str(), 0); + ImGui::TableNextColumn(); + TextHeat("%.3f", group.totalAvgMs, cachedMaxAvgMs); + ImGui::TableNextColumn(); + TextHeat("%.3f", group.totalP95Ms, cachedMaxP95Ms); + ImGui::TableNextColumn(); + TextHeat("%.3f", group.totalP99Ms, cachedMaxP99Ms); + ImGui::TableNextColumn(); + if (cachedTotalAvgMs > 0.0f) + TextHeat("%5.1f", (group.totalAvgMs / cachedTotalAvgMs) * 100.0f, 100.0f); + if (open) { + for (const auto& pass : group.passes) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TreeNodeEx(pass.label.c_str(), ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen); + ImGui::TableNextColumn(); + TextHeat("%.3f", pass.avgMs, cachedMaxAvgMs); + ImGui::TableNextColumn(); + TextHeat("%.3f", pass.p95Ms, cachedMaxP95Ms); + ImGui::TableNextColumn(); + TextHeat("%.3f", pass.p99Ms, cachedMaxP99Ms); + ImGui::TableNextColumn(); + if (cachedTotalAvgMs > 0.0f) + TextHeat("%5.1f", (pass.avgMs / cachedTotalAvgMs) * 100.0f, 100.0f); + } + ImGui::TreePop(); + } + } + } + ImGui::EndTable(); + } + + const char* modeLabel = cpuMode ? "CPU" : "GPU"; + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.6f, 1.0f), "Total: %.3f ms (%s avg)", cachedTotalAvgMs, modeLabel); +} + +void StatisticsRenderer::RenderFeatureTimers(const std::string& featurePrefix) +{ + auto& profiler = (*globals::profiler); + const auto& results = profiler.GetResults(); + + int mode = static_cast(timingMode); + ImGui::RadioButton("GPU", &mode, 0); + ImGui::SameLine(); + ImGui::RadioButton("CPU", &mode, 1); + timingMode = static_cast(mode); + + bool cpuMode = (timingMode == TimingMode::CPU); + + struct Entry + { + std::string label; + float timeMs; + float avgMs; + float p95Ms; + float p99Ms; + }; + + std::vector entries; + float totalTimeMs = 0.0f; + float totalAvg = 0.0f; + float totalP95 = 0.0f; + float totalP99 = 0.0f; + float maxAvg = 0.0f; + float maxP95 = 0.0f; + float maxP99 = 0.0f; + + std::string prefix = featurePrefix + "::"; + for (const auto& r : results) { + if (!r.valid || !r.name.starts_with(prefix)) + continue; + std::string label = r.name.substr(prefix.size()); + float timeMs = cpuMode ? r.cpuTimeMs : r.gpuTimeMs; + float avg = cpuMode ? r.cpuAvgMs : r.avgMs; + float p95 = cpuMode ? r.cpuP95Ms : r.p95Ms; + float p99 = cpuMode ? r.cpuP99Ms : r.p99Ms; + entries.push_back({ label, timeMs, avg, p95, p99 }); + totalTimeMs += timeMs; + totalAvg += avg; + totalP95 += p95; + totalP99 += p99; + maxAvg = std::max(maxAvg, avg); + maxP95 = std::max(maxP95, p95); + maxP99 = std::max(maxP99, p99); + } + + if (entries.empty()) { + ImGui::TextDisabled("No timing data"); + return; + } + + auto& state = featureGraphs[featurePrefix]; + + std::vector tasks; + double accumulated = 0.0; + for (const auto& e : entries) { + legit::ProfilerTask task; + task.startTime = accumulated / 1000.0; + task.endTime = (accumulated + e.timeMs) / 1000.0; + task.name = e.label; + task.color = ToLegitColor(GetGroupColor(featurePrefix + "::" + e.label)); + tasks.push_back(task); + accumulated += e.timeMs; + } + + if (!tasks.empty()) { + state.graph.LoadFrameData(tasks.data(), tasks.size()); + + float maxFrameTimeSec = state.graph.GetPeakFrameTime() * 1.2f; + if (maxFrameTimeSec < 0.00001f) + maxFrameTimeSec = 0.00001f; + + float availWidth = ImGui::GetContentRegionAvail().x; + int legendWidth = 200; + int graphWidth = std::max(100, static_cast(availWidth) - legendWidth); + int graphHeight = 100; + + state.graph.RenderTimings(graphWidth, legendWidth, graphHeight, 0, maxFrameTimeSec); + ImGui::Spacing(); + } + + if (ImGui::BeginTable("##FeatureTimers", 4, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_PadOuterX)) { + ImGui::TableSetupColumn("Pass", ImGuiTableColumnFlags_WidthStretch, 3.0f); + ImGui::TableSetupColumn("Avg", ImGuiTableColumnFlags_WidthFixed, 55.0f); + ImGui::TableSetupColumn("P95", ImGuiTableColumnFlags_WidthFixed, 55.0f); + ImGui::TableSetupColumn("P99", ImGuiTableColumnFlags_WidthFixed, 55.0f); + ImGui::TableHeadersRow(); + + for (const auto& e : entries) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%s", e.label.c_str()); + ImGui::TableNextColumn(); + TextHeat("%.3f", e.avgMs, maxAvg); + ImGui::TableNextColumn(); + TextHeat("%.3f", e.p95Ms, maxP95); + ImGui::TableNextColumn(); + TextHeat("%.3f", e.p99Ms, maxP99); + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.6f, 1.0f), "Total"); + ImGui::TableNextColumn(); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.6f, 1.0f), "%.3f", totalAvg); + ImGui::TableNextColumn(); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.6f, 1.0f), "%.3f", totalP95); + ImGui::TableNextColumn(); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.6f, 1.0f), "%.3f", totalP99); + + ImGui::EndTable(); + } +} diff --git a/src/Menu/StatisticsRenderer.h b/src/Menu/StatisticsRenderer.h new file mode 100644 index 0000000000..56403309e0 --- /dev/null +++ b/src/Menu/StatisticsRenderer.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include + +#include + +#include "Profiler.h" +#include "Utils/LegitProfiler.h" + +class StatisticsRenderer +{ +public: + enum class TimingMode + { + GPU, + CPU + }; + + static void RenderStatistics(); + static void RenderFeatureTimers(const std::string& featurePrefix); + +private: + static inline TimingMode timingMode = TimingMode::GPU; + static inline float timeSinceLastUpdate = 0.0f; + static inline float lastFrameTime = 0.0f; + + struct PassEntry + { + std::string label; + float avgMs; + float p95Ms; + float p99Ms; + }; + struct GroupEntry + { + std::string name; + float totalAvgMs = 0.0f; + float totalP95Ms = 0.0f; + float totalP99Ms = 0.0f; + std::vector passes; + }; + static inline float cachedTotalAvgMs = 0.0f; + static inline float cachedTotalP95Ms = 0.0f; + static inline float cachedTotalP99Ms = 0.0f; + static inline float cachedMaxAvgMs = 0.0f; + static inline float cachedMaxP95Ms = 0.0f; + static inline float cachedMaxP99Ms = 0.0f; + static inline std::vector cachedGroups; + + static inline ImGuiUtils::ProfilerGraph gpuGraph{ Profiler::kHistorySize }; + + struct FeatureGraphState + { + ImGuiUtils::ProfilerGraph graph{ Profiler::kHistorySize }; + }; + static inline std::unordered_map featureGraphs; + + static inline std::unordered_map groupColorMap; + static inline size_t nextColorIndex = 0; + + static ImU32 GetGroupColor(const std::string& groupName); + static uint32_t ToLegitColor(ImU32 imColor); + static ImVec4 HeatColor(float value, float maxValue); + static void TextHeat(const char* fmt, float value, float maxValue); + static void RenderGraph(); +}; diff --git a/src/Profiler.cpp b/src/Profiler.cpp new file mode 100644 index 0000000000..b3e96bd2be --- /dev/null +++ b/src/Profiler.cpp @@ -0,0 +1,240 @@ +#include "Profiler.h" + +#include +#include + +float Profiler::RollingHistory::GetAverage() const +{ + if (count == 0) + return lastMs; + float sum = 0.0f; + for (uint32_t i = 0; i < count; i++) + sum += history[i]; + return sum / static_cast(count); +} + +float Profiler::RollingHistory::GetPercentile(float p) const +{ + if (count == 0) + return lastMs; + + thread_local std::vector sorted; + sorted.resize(count); + for (uint32_t i = 0; i < count; i++) + sorted[i] = history[i]; + std::sort(sorted.begin(), sorted.end()); + + float idx = (p / 100.0f) * static_cast(count - 1); + uint32_t lo = static_cast(idx); + uint32_t hi = std::min(lo + 1, count - 1); + float frac = idx - static_cast(lo); + return sorted[lo] * (1.0f - frac) + sorted[hi] * frac; +} + +void Profiler::Initialize(ID3D11Device* device, ID3D11DeviceContext* a_context) +{ + Release(); + + context = a_context; + + LARGE_INTEGER freq; + QueryPerformanceFrequency(&freq); + cpuTicksToMs = 1000.0 / static_cast(freq.QuadPart); + + for (auto& frame : frames) { + D3D11_QUERY_DESC disjointDesc{}; + disjointDesc.Query = D3D11_QUERY_TIMESTAMP_DISJOINT; + device->CreateQuery(&disjointDesc, frame.disjoint.put()); + + frame.timers.resize(kMaxTimers); + for (auto& timer : frame.timers) { + D3D11_QUERY_DESC tsDesc{}; + tsDesc.Query = D3D11_QUERY_TIMESTAMP; + device->CreateQuery(&tsDesc, timer.begin.put()); + device->CreateQuery(&tsDesc, timer.end.put()); + } + frame.activeCount = 0; + frame.inFlight = false; + } + + writeFrame = 0; + readFrame = 0; + framesSinceInit = 0; + initialized = true; +} + +void Profiler::Release() +{ + for (auto& frame : frames) { + frame.disjoint = nullptr; + frame.timers.clear(); + frame.activeCount = 0; + frame.inFlight = false; + } + results.clear(); + knownTimers.clear(); + totalTimeMs = 0.0f; + cpuTotalTimeMs = 0.0f; + initialized = false; + context = nullptr; +} + +void Profiler::BeginFrame() +{ + if (!initialized || !context || frameActive) + return; + + CollectResults(); + + auto& frame = frames[writeFrame]; + frame.activeCount = 0; + frame.inFlight = true; + frameActive = true; + context->Begin(frame.disjoint.get()); +} + +void Profiler::BeginPass(const std::string& name) +{ + if (!initialized || !context) + return; + + if (!frameActive) + BeginFrame(); + + auto& frame = frames[writeFrame]; + if (frame.activeCount >= kMaxTimers) + return; + + auto& timer = frame.timers[frame.activeCount]; + timer.name = name; + context->End(timer.begin.get()); + QueryPerformanceCounter(&timer.cpuBegin); + + if (beginPerfEvent) + beginPerfEvent(name); +} + +void Profiler::EndPass() +{ + if (!initialized || !context || !frameActive) + return; + + auto& frame = frames[writeFrame]; + if (frame.activeCount >= kMaxTimers) + return; + + auto& timer = frame.timers[frame.activeCount]; + + LARGE_INTEGER cpuEnd; + QueryPerformanceCounter(&cpuEnd); + timer.cpuMs = static_cast(static_cast(cpuEnd.QuadPart - timer.cpuBegin.QuadPart) * cpuTicksToMs); + + context->End(timer.end.get()); + frame.activeCount++; + + if (endPerfEvent) + endPerfEvent({}); +} + +void Profiler::EndFrame() +{ + if (!initialized || !context || !frameActive) + return; + + frameActive = false; + context->End(frames[writeFrame].disjoint.get()); + writeFrame = (writeFrame + 1) % kFrameLatency; + framesSinceInit++; +} + +void Profiler::CollectResults() +{ + if (framesSinceInit < kFrameLatency) + return; + + readFrame = writeFrame; + auto& frame = frames[readFrame]; + if (!frame.inFlight) + return; + + D3D11_QUERY_DATA_TIMESTAMP_DISJOINT disjointData{}; + HRESULT hr = context->GetData(frame.disjoint.get(), &disjointData, sizeof(disjointData), D3D11_ASYNC_GETDATA_DONOTFLUSH); + if (hr != S_OK) + return; + + frame.inFlight = false; + + struct ActiveTimerData + { + float gpuMs; + float cpuMs; + }; + std::unordered_map activeTimers; + float activeTotalMs = 0.0f; + float activeCpuTotalMs = 0.0f; + + if (!disjointData.Disjoint) { + double ticksToMs = 1000.0 / static_cast(disjointData.Frequency); + + for (uint32_t i = 0; i < frame.activeCount; i++) { + auto& timer = frame.timers[i]; + UINT64 tsBegin = 0, tsEnd = 0; + + if (context->GetData(timer.begin.get(), &tsBegin, sizeof(tsBegin), D3D11_ASYNC_GETDATA_DONOTFLUSH) != S_OK) + continue; + if (context->GetData(timer.end.get(), &tsEnd, sizeof(tsEnd), D3D11_ASYNC_GETDATA_DONOTFLUSH) != S_OK) + continue; + + float ms = static_cast(static_cast(tsEnd - tsBegin) * ticksToMs); + activeTimers[timer.name] = { ms, timer.cpuMs }; + activeTotalMs += ms; + activeCpuTotalMs += timer.cpuMs; + + bool isNew = true; + for (auto& known : knownTimers) { + if (known.name == timer.name) { + isNew = false; + known.gpu.PushSample(ms); + known.cpu.PushSample(timer.cpuMs); + break; + } + } + if (isNew) { + KnownTimer kt; + kt.name = timer.name; + kt.gpu.PushSample(ms); + kt.cpu.PushSample(timer.cpuMs); + knownTimers.push_back(std::move(kt)); + } + } + } + + totalTimeMs = activeTotalMs; + cpuTotalTimeMs = activeCpuTotalMs; + + results.clear(); + results.reserve(knownTimers.size()); + for (const auto& known : knownTimers) { + TimerResult result; + result.name = known.name; + auto it = activeTimers.find(known.name); + if (it != activeTimers.end()) { + result.gpuTimeMs = it->second.gpuMs; + result.cpuTimeMs = it->second.cpuMs; + } else { + result.gpuTimeMs = known.gpu.lastMs; + result.cpuTimeMs = known.cpu.lastMs; + } + result.avgMs = known.gpu.GetAverage(); + result.p95Ms = known.gpu.GetPercentile(95.0f); + result.p99Ms = known.gpu.GetPercentile(99.0f); + result.cpuAvgMs = known.cpu.GetAverage(); + result.cpuP95Ms = known.cpu.GetPercentile(95.0f); + result.cpuP99Ms = known.cpu.GetPercentile(99.0f); + result.valid = true; + result.historyBuffer = known.gpu.history; + result.historyHead = known.gpu.head; + result.historyCount = known.gpu.count; + results.push_back(std::move(result)); + } +} diff --git a/src/Profiler.h b/src/Profiler.h new file mode 100644 index 0000000000..df93f4bcf6 --- /dev/null +++ b/src/Profiler.h @@ -0,0 +1,139 @@ +#pragma once + +#include +#include +#include +#include +#include + +class Profiler +{ +public: + static constexpr uint32_t kMaxTimers = 128; + static constexpr uint32_t kFrameLatency = 3; + static constexpr uint32_t kHistorySize = 300; + + using PerfEventCallback = std::function; + + struct RollingHistory + { + float history[kHistorySize]{}; + uint32_t head = 0; + uint32_t count = 0; + float lastMs = 0.0f; + + void PushSample(float ms) + { + history[head] = ms; + head = (head + 1) % kHistorySize; + if (count < kHistorySize) + count++; + lastMs = ms; + } + + float GetAverage() const; + float GetPercentile(float p) const; + }; + + struct TimerResult + { + std::string name; + float gpuTimeMs = 0.0f; + float avgMs = 0.0f; + float p95Ms = 0.0f; + float p99Ms = 0.0f; + float cpuTimeMs = 0.0f; + float cpuAvgMs = 0.0f; + float cpuP95Ms = 0.0f; + float cpuP99Ms = 0.0f; + bool valid = false; + + const float* historyBuffer = nullptr; + uint32_t historyHead = 0; + uint32_t historyCount = 0; + + float GetHistorySample(uint32_t index) const + { + if (!historyBuffer || index >= historyCount) + return 0.0f; + return historyBuffer[(historyHead - historyCount + index + kHistorySize) % kHistorySize]; + } + }; + + void Initialize(ID3D11Device* device, ID3D11DeviceContext* context); + void Release(); + + void SetPerfEventCallbacks(PerfEventCallback beginCb, PerfEventCallback endCb) + { + beginPerfEvent = std::move(beginCb); + endPerfEvent = std::move(endCb); + } + + void BeginFrame(); + void BeginPass(const std::string& name); + void EndPass(); + void EndFrame(); + + const std::vector& GetResults() const { return results; } + float GetTotalTimeMs() const { return totalTimeMs; } + float GetCpuTotalTimeMs() const { return cpuTotalTimeMs; } + + void ClearTimers() + { + results.clear(); + knownTimers.clear(); + totalTimeMs = 0.0f; + cpuTotalTimeMs = 0.0f; + } + + void ClearTimersForFeature(const std::string& featureName) + { + std::erase_if(knownTimers, [&featureName](const KnownTimer& kt) { + return kt.name.starts_with(featureName + "::"); + }); + } + +private: + struct FrameQueries + { + winrt::com_ptr disjoint; + struct TimerPair + { + winrt::com_ptr begin; + winrt::com_ptr end; + std::string name; + LARGE_INTEGER cpuBegin{}; + float cpuMs = 0.0f; + }; + std::vector timers; + uint32_t activeCount = 0; + bool inFlight = false; + }; + + ID3D11DeviceContext* context = nullptr; + + FrameQueries frames[kFrameLatency]; + uint32_t writeFrame = 0; + uint32_t readFrame = 0; + uint32_t framesSinceInit = 0; + bool initialized = false; + bool frameActive = false; + double cpuTicksToMs = 0.0; + + PerfEventCallback beginPerfEvent; + PerfEventCallback endPerfEvent; + + std::vector results; + + struct KnownTimer + { + std::string name; + RollingHistory gpu; + RollingHistory cpu; + }; + std::vector knownTimers; + float totalTimeMs = 0.0f; + float cpuTotalTimeMs = 0.0f; + + void CollectResults(); +}; diff --git a/src/ShaderCache.cpp b/src/ShaderCache.cpp index 38bd325651..1a0c965486 100644 --- a/src/ShaderCache.cpp +++ b/src/ShaderCache.cpp @@ -2,6 +2,7 @@ #include "Globals.h" #include "ShaderFileWatcher.h" #include "Util.h" +#include "Utils/ShaderPatches.h" #include @@ -56,6 +57,7 @@ namespace SIE if (!ifs.read(buf.data(), size)) return E_FAIL; } + Util::ShaderPatches::Apply(pFileName, buf); buffers.push_back(std::move(buf)); const auto& storage = buffers.back(); *ppData = storage.empty() ? nullptr : storage.data(); diff --git a/src/State.cpp b/src/State.cpp index 753f2ce18c..a8685d5fc6 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -7,6 +7,8 @@ #include "Deferred.h" #include "FeatureIssues.h" #include "Features/CloudShadows.h" +#include "Features/Effect11.h" +#include "Features/SkySync.h" #include "Features/HDRDisplay.h" #include "Features/InteriorSun.h" #include "Features/PerformanceOverlay.h" @@ -14,6 +16,7 @@ #include "Features/TerrainHelper.h" #include "Features/Upscaling.h" #include "Features/VRStereoOptimizations.h" +#include "Features/Skylighting.h" #include "Features/VolumetricShadows.h" #include "Features/WeatherEditor.h" #include "Menu.h" @@ -56,6 +59,7 @@ void State::Draw() auto& truePBR = globals::features::truePBR; auto context = globals::d3d::context; auto& volumetricShadows = globals::features::volumetricShadows; + auto& skylighting = globals::features::skylighting; if (shaderCache->IsEnabled()) { // Process deferred cell transitions (interior detection) @@ -76,6 +80,11 @@ void State::Draw() cloudShadows.SkyShaderHacks(); } + { + ZoneScopedN("Effect11::ParticleShaderHacks"); + globals::features::effect11.ParticleShaderHacks(); + } + if (terrainHelper.loaded) { ZoneScopedN("TerrainHelper::SetShaderResouces"); terrainHelper.SetShaderResouces(context); @@ -96,6 +105,8 @@ void State::Draw() if (currentPixelDescriptor & static_cast(SIE::ShaderCache::UtilityShaderFlags::RenderShadowmask)) { if (volumetricShadows.loaded) volumetricShadows.CopyShadowLightData(); + if (skylighting.loaded) + skylighting.CaptureShadowCascadeSRV(); } } } @@ -164,6 +175,8 @@ void State::Debug() void State::Reset() { + globals::profiler->EndFrame(); + Feature::ForEachLoadedFeature("Reset", [](Feature* feature) { feature->Reset(); }); if (!globals::game::ui->GameIsPaused()) timer += RE::GetSecondsSinceLastFrame(); @@ -206,6 +219,14 @@ void State::Reset() void State::Setup() { + // Detect Moon and Stars mod for compatibility adjustments + moonAndStarsLoaded = GetModuleHandle(L"po3_MoonMod.dll") != nullptr; + if (moonAndStarsLoaded) + logger::info("Moon and Stars detected, compatibility enabled"); + + + + globals::features::truePBR.SetupResources(); SetupResources(); // Probe typed UAV load support before features set up their resources, so any @@ -755,6 +776,14 @@ void State::SetupResources() #ifdef TRACY_ENABLE Feature::SetTracyCtx(tracyCtx); #endif + + globals::profiler->Initialize(globals::d3d::device, globals::d3d::context); + + if (frameAnnotations) { + globals::profiler->SetPerfEventCallbacks( + [this](std::string_view name) { BeginPerfEvent(name); }, + [this](std::string_view) { EndPerfEvent(); }); + } } void State::ModifyShaderLookup(const RE::BSShader& a_shader, uint& a_vertexDescriptor, uint& a_pixelDescriptor, bool a_forceDeferred) @@ -988,6 +1017,32 @@ void State::UpdateSharedData([[maybe_unused]] bool a_inWorld, [[maybe_unused]] b data.MipBias = 0; } + if (auto sky = globals::game::sky) { + // Process sun + if (auto sun = sky->sun; sun && sun->root && sky->root) { + const auto& sunPos = sun->root->world.translate; + const auto& skyPos = sky->root->world.translate; + float3 sunDirection = { sunPos.x - skyPos.x, sunPos.y - skyPos.y, sunPos.z - skyPos.z }; + sunDirection.Normalize(); + data.SunDirection = { sunDirection.x, sunDirection.y, sunDirection.z, 0.0f }; + + if (const auto prop = skyrim_cast(sun->sunBase->GetGeometryRuntimeData().shaderProperty.get())) + data.SunColor = { prop->kBlendColor.red * prop->kBlendColor.alpha, prop->kBlendColor.green * prop->kBlendColor.alpha, prop->kBlendColor.blue * prop->kBlendColor.alpha, prop->kBlendColor.alpha }; + } + + if (auto masser = sky->masser) { + auto dir = Util::Moon::GetDirection(masser, moonAndStarsLoaded); + data.MasserDirection = { dir.x, dir.y, dir.z, 0.0f }; + data.MasserColor = Util::Moon::GetBlendColor(masser, Util::Moon::MasserBaseColor, globals::features::skySync.settings.NewMoonIntensity, globals::features::skySync.settings.CrescentMoonIntensity, globals::features::skySync.settings.FullMoonIntensity); + } + + if (auto secunda = sky->secunda) { + auto dir = Util::Moon::GetDirection(secunda, moonAndStarsLoaded); + data.SecundaDirection = { dir.x, dir.y, dir.z, 0.0f }; + data.SecundaColor = Util::Moon::GetBlendColor(secunda, Util::Moon::SecundaBaseColor, globals::features::skySync.settings.NewMoonIntensity, globals::features::skySync.settings.CrescentMoonIntensity, globals::features::skySync.settings.FullMoonIntensity); + } + } + // DALC to SH const auto& m = dalcTransform.rotate; const auto& t = dalcTransform.translate; diff --git a/src/State.h b/src/State.h index 2dd811d445..7057fee387 100644 --- a/src/State.h +++ b/src/State.h @@ -241,6 +241,12 @@ class State DirectX::XMFLOAT3X4 DirectionalAmbient; float4 DirLightDirection; float4 DirLightColor; + float4 SunDirection; + float4 SunColor; + float4 MasserDirection; + float4 MasserColor; + float4 SecundaDirection; + float4 SecundaColor; float4 CameraData; float4 BufferDim; float Timer; @@ -275,6 +281,9 @@ class State TracyD3D11Ctx tracyCtx = nullptr; // Tracy context + // Moon and Stars mod detection + inline static bool moonAndStarsLoaded = false; + void ClearDisabledFeatures(); bool SetFeatureDisabled(const std::string& featureName, bool isDisabled); bool IsFeatureDisabled(const std::string& featureName); diff --git a/src/Util.h b/src/Util.h index 2363c06700..02cbb1aa4a 100644 --- a/src/Util.h +++ b/src/Util.h @@ -6,6 +6,7 @@ #include "Utils/Format.h" #include "Utils/Game.h" #include "Utils/GameSetting.h" +#include "Utils/Moon.h" #include "Utils/Serialize.h" #include "Utils/UI.h" #include "Utils/WinApi.h" diff --git a/src/Utils/D3D.cpp b/src/Utils/D3D.cpp index 5f6c4b5737..5b7c7171b3 100644 --- a/src/Utils/D3D.cpp +++ b/src/Utils/D3D.cpp @@ -4,6 +4,7 @@ #include "ShaderCache.h" #include "State.h" #include "Utils/Format.h" +#include "Utils/ShaderPatches.h" #include #include #include @@ -126,10 +127,13 @@ namespace Util file.seekg(0, std::ios::beg); // Create buffer and read file - char* data = new char[size]; - file.read(data, size); + std::string content(size, '\0'); + file.read(content.data(), size); + ShaderPatches::Apply(pFileName, content); + char* data = new char[content.size()]; + memcpy(data, content.data(), content.size()); *ppData = data; - *pBytes = size; + *pBytes = static_cast(content.size()); return S_OK; } diff --git a/src/Utils/Game.cpp b/src/Utils/Game.cpp index 31900f417c..f97a749989 100644 --- a/src/Utils/Game.cpp +++ b/src/Utils/Game.cpp @@ -301,14 +301,16 @@ namespace Util bool IsInterior() { - auto tes = RE::TES::GetSingleton(); - if (tes && !tes->interiorCell) { - if (auto worldSpace = tes->GetRuntimeData2().worldSpace) { - if (!worldSpace->flags.any(RE::TESWorldSpace::Flag::kNoSky, RE::TESWorldSpace::Flag::kFixedDimensions)) { - return false; + if (auto tes = RE::TES::GetSingleton()) { + if (!tes->interiorCell) { + if (auto worldSpace = tes->GetRuntimeData2().worldSpace) { + if (!worldSpace->flags.any(RE::TESWorldSpace::Flag::kNoSky, RE::TESWorldSpace::Flag::kFixedDimensions)) { + return false; + } } } } + return true; } diff --git a/src/Utils/LegitProfiler.h b/src/Utils/LegitProfiler.h new file mode 100644 index 0000000000..9e8e636328 --- /dev/null +++ b/src/Utils/LegitProfiler.h @@ -0,0 +1,289 @@ +// Based on LegitProfiler by Raikiri (https://github.com/Raikiri/LegitProfiler) +// MIT License - modified to remove glm dependency, using ImVec2 directly. +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace legit +{ + namespace Colors + { +#define RGBA_LE(col) (((col & 0xff000000) >> (3 * 8)) + ((col & 0x00ff0000) >> (1 * 8)) + ((col & 0x0000ff00) << (1 * 8)) + ((col & 0x000000ff) << (3 * 8))) + const static uint32_t turqoise = RGBA_LE(0x1abc9cffu); + const static uint32_t greenSea = RGBA_LE(0x16a085ffu); + const static uint32_t emerald = RGBA_LE(0x2ecc71ffu); + const static uint32_t nephritis = RGBA_LE(0x27ae60ffu); + const static uint32_t peterRiver = RGBA_LE(0x3498dbffu); + const static uint32_t belizeHole = RGBA_LE(0x2980b9ffu); + const static uint32_t amethyst = RGBA_LE(0x9b59b6ffu); + const static uint32_t wisteria = RGBA_LE(0x8e44adffu); + const static uint32_t sunFlower = RGBA_LE(0xf1c40fffu); + const static uint32_t orange = RGBA_LE(0xf39c12ffu); + const static uint32_t carrot = RGBA_LE(0xe67e22ffu); + const static uint32_t pumpkin = RGBA_LE(0xd35400ffu); + const static uint32_t alizarin = RGBA_LE(0xe74c3cffu); + const static uint32_t pomegranate = RGBA_LE(0xc0392bffu); + const static uint32_t clouds = RGBA_LE(0xecf0f1ffu); + const static uint32_t silver = RGBA_LE(0xbdc3c7ffu); + const static uint32_t imguiText = RGBA_LE(0xF2F5FAFFu); +#undef RGBA_LE + } + + struct ProfilerTask + { + double startTime; + double endTime; + std::string name; + uint32_t color; + double GetLength() { return endTime - startTime; } + }; +} + +namespace ImGuiUtils +{ + class ProfilerGraph + { + public: + int frameWidth; + int frameSpacing; + bool useColoredLegendText; + + ProfilerGraph(size_t framesCount) + { + frames.resize(framesCount); + for (auto& frame : frames) + frame.tasks.reserve(100); + frameWidth = 3; + frameSpacing = 1; + useColoredLegendText = false; + } + + void LoadFrameData(const legit::ProfilerTask* tasks, size_t count) + { + auto& currFrame = frames[currFrameIndex]; + currFrame.tasks.resize(0); + currFrame.totalTime = 0.0f; + for (size_t taskIndex = 0; taskIndex < count; taskIndex++) { + if (taskIndex == 0) + currFrame.tasks.push_back(tasks[taskIndex]); + else { + if (tasks[taskIndex - 1].color != tasks[taskIndex].color || tasks[taskIndex - 1].name != tasks[taskIndex].name) + currFrame.tasks.push_back(tasks[taskIndex]); + else + currFrame.tasks.back().endTime = tasks[taskIndex].endTime; + } + currFrame.totalTime += float(tasks[taskIndex].endTime - tasks[taskIndex].startTime); + } + currFrame.taskStatsIndex.resize(currFrame.tasks.size()); + + for (size_t taskIndex = 0; taskIndex < currFrame.tasks.size(); taskIndex++) { + auto& task = currFrame.tasks[taskIndex]; + auto it = taskNameToStatsIndex.find(task.name); + if (it == taskNameToStatsIndex.end()) { + taskNameToStatsIndex[task.name] = taskStats.size(); + TaskStats taskStat; + taskStat.maxTime = -1.0; + taskStats.push_back(taskStat); + } + currFrame.taskStatsIndex[taskIndex] = taskNameToStatsIndex[task.name]; + } + { + float recentMax = 0.0f; + size_t lookback = std::min(frames.size(), size_t(120)); + for (size_t i = 0; i < lookback; i++) { + size_t idx = (currFrameIndex + frames.size() - 1 - i) % frames.size(); + recentMax = std::max(recentMax, frames[idx].totalTime); + } + if (peakFrameTime <= 0.0f) + peakFrameTime = recentMax; + else { + float rate = (recentMax < peakFrameTime) ? 0.02f : 0.01f; + peakFrameTime += (recentMax - peakFrameTime) * rate; + } + } + currFrameIndex = (currFrameIndex + 1) % frames.size(); + RebuildTaskStats(currFrameIndex, 300); + } + + float GetTotalTaskTime(int frameIndexOffset) + { + return frames[GetCurrFrameIndex(frameIndexOffset)].totalTime; + } + + float GetPeakFrameTime() const { return peakFrameTime; } + + void RenderTimings(int graphWidth, int legendWidth, int height, int frameIndexOffset, float maxFrameTime) + { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + const ImVec2 widgetPos = ImGui::GetCursorScreenPos(); + RenderGraph(drawList, widgetPos, ImVec2(float(graphWidth), float(height)), frameIndexOffset, maxFrameTime); + RenderLegend(drawList, ImVec2(widgetPos.x + graphWidth, widgetPos.y), ImVec2(float(legendWidth), float(height)), frameIndexOffset, maxFrameTime); + ImGui::Dummy(ImVec2(float(graphWidth + legendWidth), float(height))); + } + + private: + size_t GetCurrFrameIndex(size_t frameIndexOffset) + { + return (currFrameIndex - frameIndexOffset - 1 + 2 * frames.size()) % frames.size(); + } + + void RebuildTaskStats(size_t endFrame, size_t framesCount) + { + for (auto& taskStat : taskStats) { + taskStat.maxTime = -1.0f; + taskStat.priorityOrder = size_t(-1); + taskStat.onScreenIndex = size_t(-1); + } + + for (size_t frameNumber = 0; frameNumber < framesCount; frameNumber++) { + size_t frameIndex = (endFrame - 1 - frameNumber + frames.size()) % frames.size(); + auto& frame = frames[frameIndex]; + for (size_t taskIndex = 0; taskIndex < frame.tasks.size(); taskIndex++) { + auto& task = frame.tasks[taskIndex]; + auto& stats = taskStats[frame.taskStatsIndex[taskIndex]]; + stats.maxTime = std::max(stats.maxTime, task.endTime - task.startTime); + } + } + std::vector statPriorities; + statPriorities.resize(taskStats.size()); + for (size_t statIndex = 0; statIndex < taskStats.size(); statIndex++) + statPriorities[statIndex] = statIndex; + + std::sort(statPriorities.begin(), statPriorities.end(), [this](size_t left, size_t right) { return taskStats[left].maxTime > taskStats[right].maxTime; }); + for (size_t statNumber = 0; statNumber < taskStats.size(); statNumber++) { + size_t statIndex = statPriorities[statNumber]; + taskStats[statIndex].priorityOrder = statNumber; + } + } + + void RenderGraph(ImDrawList* drawList, ImVec2 graphPos, ImVec2 graphSize, size_t frameIndexOffset, float maxFrameTime) + { + Rect(drawList, graphPos, ImVec2(graphPos.x + graphSize.x, graphPos.y + graphSize.y), 0xffffffff, false); + float heightThreshold = 1.0f; + + for (size_t frameNumber = 0; frameNumber < frames.size(); frameNumber++) { + size_t frameIndex = GetCurrFrameIndex(frameIndexOffset + frameNumber); + + ImVec2 framePos = ImVec2(graphPos.x + graphSize.x - 1 - frameWidth - (frameWidth + frameSpacing) * float(frameNumber), graphPos.y + graphSize.y - 1); + if (framePos.x < graphPos.x + 1) + break; + ImVec2 taskPos = framePos; + auto& frame = frames[frameIndex]; + for (const auto& task : frame.tasks) { + float taskStartHeight = (float(task.startTime) / maxFrameTime) * graphSize.y; + float taskEndHeight = (float(task.endTime) / maxFrameTime) * graphSize.y; + if (abs(taskEndHeight - taskStartHeight) > heightThreshold) + Rect(drawList, ImVec2(taskPos.x, taskPos.y - taskStartHeight), ImVec2(taskPos.x + frameWidth, taskPos.y - taskEndHeight), task.color, true); + } + } + } + + void RenderLegend(ImDrawList* drawList, ImVec2 legendPos, ImVec2 legendSize, size_t frameIndexOffset, float maxFrameTime) + { + float markerLeftRectMargin = 3.0f; + float markerLeftRectWidth = 5.0f; + float markerMidWidth = 30.0f; + float markerRightRectWidth = 10.0f; + float markerRigthRectMargin = 3.0f; + float markerRightRectHeight = 10.0f; + float markerRightRectSpacing = 4.0f; + float nameOffset = 30.0f; + ImVec2 textMargin = ImVec2(5.0f, -3.0f); + + auto& currFrame = frames[GetCurrFrameIndex(frameIndexOffset)]; + size_t maxTasksCount = size_t(legendSize.y / (markerRightRectHeight + markerRightRectSpacing)); + + for (auto& taskStat : taskStats) + taskStat.onScreenIndex = size_t(-1); + + size_t tasksToShow = std::min(taskStats.size(), maxTasksCount); + size_t tasksShownCount = 0; + for (size_t taskIndex = 0; taskIndex < currFrame.tasks.size(); taskIndex++) { + auto& task = currFrame.tasks[taskIndex]; + auto& stat = taskStats[currFrame.taskStatsIndex[taskIndex]]; + + if (stat.priorityOrder >= tasksToShow) + continue; + + if (stat.onScreenIndex == size_t(-1)) + stat.onScreenIndex = tasksShownCount++; + else + continue; + + float taskStartHeight = (float(task.startTime) / maxFrameTime) * legendSize.y; + float taskEndHeight = (float(task.endTime) / maxFrameTime) * legendSize.y; + + ImVec2 markerLeftRectMin = ImVec2(legendPos.x + markerLeftRectMargin, legendPos.y + legendSize.y); + ImVec2 markerLeftRectMax = ImVec2(markerLeftRectMin.x + markerLeftRectWidth, markerLeftRectMin.y); + markerLeftRectMin.y -= taskStartHeight; + markerLeftRectMax.y -= taskEndHeight; + + ImVec2 markerRightRectMin = ImVec2(legendPos.x + markerLeftRectMargin + markerLeftRectWidth + markerMidWidth, legendPos.y + legendSize.y - markerRigthRectMargin - (markerRightRectHeight + markerRightRectSpacing) * float(stat.onScreenIndex)); + ImVec2 markerRightRectMax = ImVec2(markerRightRectMin.x + markerRightRectWidth, markerRightRectMin.y - markerRightRectHeight); + RenderTaskMarker(drawList, markerLeftRectMin, markerLeftRectMax, markerRightRectMin, markerRightRectMax, task.color); + + uint32_t textColor = useColoredLegendText ? task.color : legit::Colors::imguiText; + + float taskTimeMs = float(task.endTime - task.startTime); + std::ostringstream timeText; + timeText.precision(2); + timeText << std::fixed << std::string("[") << (taskTimeMs * 1000.0f); + + Text(drawList, ImVec2(markerRightRectMax.x + textMargin.x, markerRightRectMax.y + textMargin.y), textColor, timeText.str().c_str()); + Text(drawList, ImVec2(markerRightRectMax.x + textMargin.x + nameOffset, markerRightRectMax.y + textMargin.y), textColor, (std::string("ms] ") + task.name).c_str()); + } + } + + static void Rect(ImDrawList* drawList, ImVec2 minPoint, ImVec2 maxPoint, uint32_t col, bool filled = true) + { + if (filled) + drawList->AddRectFilled(minPoint, maxPoint, col); + else + drawList->AddRect(minPoint, maxPoint, col); + } + + static void Text(ImDrawList* drawList, ImVec2 point, uint32_t col, const char* text) + { + drawList->AddText(point, col, text); + } + + static void RenderTaskMarker(ImDrawList* drawList, ImVec2 leftMinPoint, ImVec2 leftMaxPoint, ImVec2 rightMinPoint, ImVec2 rightMaxPoint, uint32_t col) + { + Rect(drawList, leftMinPoint, leftMaxPoint, col, true); + Rect(drawList, rightMinPoint, rightMaxPoint, col, true); + std::array points = { + ImVec2(leftMaxPoint.x, leftMinPoint.y), + ImVec2(leftMaxPoint.x, leftMaxPoint.y), + ImVec2(rightMinPoint.x, rightMaxPoint.y), + ImVec2(rightMinPoint.x, rightMinPoint.y) + }; + drawList->AddConvexPolyFilled(points.data(), int(points.size()), col); + } + + struct FrameData + { + std::vector tasks; + std::vector taskStatsIndex; + float totalTime; + }; + + struct TaskStats + { + double maxTime; + size_t priorityOrder; + size_t onScreenIndex; + }; + + std::vector taskStats; + std::map taskNameToStatsIndex; + std::vector frames; + size_t currFrameIndex = 0; + float peakFrameTime = 0.0f; + }; +} diff --git a/src/Utils/Moon.h b/src/Utils/Moon.h new file mode 100644 index 0000000000..fab3f638df --- /dev/null +++ b/src/Utils/Moon.h @@ -0,0 +1,91 @@ +// Shared moon processing utilities +#pragma once + +namespace Util::Moon +{ + // Moon phase intensity constants + static constexpr float NewMoonIntensityFactor = 0.05f; + static constexpr float CrescentMoonIntensityFactor = 0.25f; + static constexpr float FullMoonIntensityFactor = 1.0f; + + // Moon base colors (RGB/255) + static constexpr float4 MasserBaseColor = { 142.0f / 255.0f, 96.0f / 255.0f, 90.0f / 255.0f, 1.0f }; + static constexpr float4 SecundaBaseColor = { 117.0f / 255.0f, 115.0f / 255.0f, 109.0f / 255.0f, 1.0f }; + + // Phase lookup table for determining moon phase from texture name + static constexpr std::array, 8> PhaseLookup{ + { { "full", RE::Moon::Phases::Phase::kFull }, + { "three_wan", RE::Moon::Phases::Phase::kWaningGibbous }, + { "half_wan", RE::Moon::Phases::Phase::kWaningQuarter }, + { "one_wan", RE::Moon::Phases::Phase::kWaningCrescent }, + { "new", RE::Moon::Phases::Phase::kNewMoon }, + { "one_wax", RE::Moon::Phases::Phase::kWaxingCrescent }, + { "half_wax", RE::Moon::Phases::Phase::kWaxingQuarter }, + { "three_wax", RE::Moon::Phases::Phase::kWaxingGibbous } } + }; + + inline float GetPhaseIntensityFactor(RE::Moon::Phases::Phase phase, float newMoon = NewMoonIntensityFactor, float crescent = CrescentMoonIntensityFactor, float full = FullMoonIntensityFactor) + { + if (phase == RE::Moon::Phases::Phase::kNewMoon) { + return newMoon; + } else { + const float t = (abs(static_cast(phase) - static_cast(RE::Moon::Phases::Phase::kNewMoon)) - 1.0f) / 3.0f; + return std::lerp(crescent, full, t); + } + } + + inline RE::Moon::Phases::Phase GetPhaseFromTexture(const char* textureName) + { + if (!textureName) + return RE::Moon::Phases::Phase::kFull; + + const size_t len = std::strlen(textureName); + std::string lower; + lower.reserve(len); + for (size_t i = 0; i < len; ++i) { + lower.push_back(static_cast(std::tolower(static_cast(textureName[i])))); + } + + for (auto& [suffix, id] : PhaseLookup) { + if (lower.find(suffix) != std::string::npos) { + return id; + } + } + + return RE::Moon::Phases::Phase::kFull; + } + + inline RE::NiPoint3 GetDirection(const RE::Moon* moon, bool applyMoonAndStarsCompat = false) + { + if (!moon || !moon->root) + return { 0.0f, 0.0f, 1.0f }; + + auto dir = moon->root->world.rotate.GetVectorY(); + dir.Unitize(); + + if (applyMoonAndStarsCompat) { + std::swap(dir.x, dir.y); + dir.x = -dir.x; + } + + return dir; + } + + inline float4 GetBlendColor(const RE::Moon* moon, const float4& baseColor, float newMoon = NewMoonIntensityFactor, float crescent = CrescentMoonIntensityFactor, float full = FullMoonIntensityFactor) + { + if (!moon || !moon->moonMesh) + return {}; + + const auto prop = skyrim_cast(moon->moonMesh->GetGeometryRuntimeData().shaderProperty.get()); + if (!prop) + return {}; + + float phase = 1.0f; + if (auto tex = prop->GetBaseTexture()) + phase = GetPhaseIntensityFactor(GetPhaseFromTexture(tex->name.c_str()), newMoon, crescent, full); + + float alpha = prop->kBlendColor.alpha; + return { prop->kBlendColor.red * baseColor.x * phase * alpha, prop->kBlendColor.green * baseColor.y * phase * alpha, prop->kBlendColor.blue * baseColor.z * phase * alpha, alpha }; + } + +} diff --git a/src/Utils/SettingsPatches.cpp b/src/Utils/SettingsPatches.cpp new file mode 100644 index 0000000000..a2bc63479c --- /dev/null +++ b/src/Utils/SettingsPatches.cpp @@ -0,0 +1,130 @@ +#include "SettingsPatches.h" + +#include +#include + +#include + +#include "Features/Effect11/Effects/Effect.h" + +namespace Util::SettingsPatches +{ + static std::vector entries; + static bool loaded = false; + + void Load() + { + entries.clear(); + loaded = true; + + std::filesystem::path path = "Data\\Shaders\\Effect11\\SettingsPatches.json"; + std::ifstream ifs(path); + if (!ifs.is_open()) + return; + + try { + nlohmann::json root = nlohmann::json::parse(ifs); + for (auto& item : root) { + Entry entry; + entry.file = item.at("file").get(); + for (auto& p : item.at("patches")) { + Patch patch; + patch.variable = p.at("variable").get(); + patch.value = p.at("value").get(); + entry.patches.push_back(std::move(patch)); + } + entries.push_back(std::move(entry)); + } + } catch (const std::exception& e) { + logger::error("[SettingsPatches] Failed to parse {}: {}", path.string(), e.what()); + } + + if (!entries.empty()) + logger::info("[SettingsPatches] Loaded {} entries", entries.size()); + } + + static bool FilenameMatches(const std::string& effectName, const std::string& pattern) + { + return std::equal(effectName.begin(), effectName.end(), pattern.begin(), pattern.end(), [](char a, char b) { + return std::tolower(static_cast(a)) == std::tolower(static_cast(b)); + }); + } + + static std::string GetUniqueKey(const Effect::UIVariable& uiVar) + { + if (!uiVar.uniqueName.empty()) + return uiVar.uniqueName; + return uiVar.group.empty() ? uiVar.displayName : uiVar.group + "." + uiVar.displayName; + } + + void Apply(Effect& effect) + { + if (!loaded) + Load(); + + for (auto& entry : entries) { + if (!FilenameMatches(effect.GetName(), entry.file)) + continue; + + for (auto& patch : entry.patches) { + bool found = false; + for (auto& uiVar : effect.uiVariables) { + if (uiVar.isSeparator || uiVar.isLabel) + continue; + + if (GetUniqueKey(uiVar) != patch.variable) + continue; + + found = true; + bool patched = false; + switch (uiVar.type) { + case Effect::UIVariableType::Float: + try { + float val = std::stof(patch.value); + if (uiVar.effectVariable && SUCCEEDED(uiVar.effectVariable->AsScalar()->SetFloat(val))) { + uiVar.floatValue = val; + patched = true; + } + } catch (...) {} + break; + case Effect::UIVariableType::Int: + try { + int val = std::stoi(patch.value); + if (uiVar.effectVariable && SUCCEEDED(uiVar.effectVariable->AsScalar()->SetInt(val))) { + uiVar.intValue = val; + patched = true; + } + } catch (...) {} + break; + case Effect::UIVariableType::Bool: + { + std::string lv = patch.value; + std::transform(lv.begin(), lv.end(), lv.begin(), ::tolower); + if (lv == "true" || lv == "1" || lv == "false" || lv == "0") { + bool val = (lv == "true" || lv == "1"); + if (uiVar.effectVariable && SUCCEEDED(uiVar.effectVariable->AsScalar()->SetBool(val))) { + uiVar.boolValue = val; + patched = true; + } + } + } + break; + default: + break; + } + + if (patched) { + uiVar.isReadOnly = true; + logger::debug("[SettingsPatches] Patched '{}' in '{}' to '{}'", + patch.variable, effect.GetName(), patch.value); + } + break; + } + if (!found) { + logger::debug("[SettingsPatches] No match for '{}' in '{}'", + patch.variable, effect.GetName()); + } + } + } + } +} diff --git a/src/Utils/SettingsPatches.h b/src/Utils/SettingsPatches.h new file mode 100644 index 0000000000..714bda9435 --- /dev/null +++ b/src/Utils/SettingsPatches.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +class Effect; + +namespace Util +{ + namespace SettingsPatches + { + struct Patch + { + std::string variable; + std::string value; + }; + + struct Entry + { + std::string file; + std::vector patches; + }; + + void Load(); + void Apply(Effect& effect); + } +} diff --git a/src/Utils/ShaderPatches.cpp b/src/Utils/ShaderPatches.cpp new file mode 100644 index 0000000000..a36a2a4697 --- /dev/null +++ b/src/Utils/ShaderPatches.cpp @@ -0,0 +1,103 @@ +#include "ShaderPatches.h" + +#include +#include + +#include + +namespace Util::ShaderPatches +{ + static std::vector entries; + static bool loaded = false; + + void Load() + { + entries.clear(); + loaded = true; + + std::filesystem::path path = "Data\\Shaders\\Effect11\\ShaderPatches.json"; + std::ifstream ifs(path); + if (!ifs.is_open()) + return; + + try { + nlohmann::json root = nlohmann::json::parse(ifs); + for (auto& item : root) { + Entry entry; + entry.file = item.at("file").get(); + for (auto& r : item.at("patches")) { + Replacement rep; + rep.find = r.at("find").get(); + rep.replace = r.at("replace").get(); + entry.replacements.push_back(std::move(rep)); + } + entries.push_back(std::move(entry)); + } + } catch (const std::exception& e) { + logger::error("[ShaderPatches] Failed to parse {}: {}", path.string(), e.what()); + } + + if (!entries.empty()) + logger::info("[ShaderPatches] Loaded {} entries", entries.size()); + } + + static bool FilenameMatches(const char* includePath, const std::string& pattern) + { + std::string path(includePath); + for (auto& c : path) + if (c == '/') + c = '\\'; + + std::string pat = pattern; + for (auto& c : pat) + if (c == '/') + c = '\\'; + + if (pat.size() > path.size()) + return false; + + auto pathEnd = path.end(); + auto pathStart = pathEnd - static_cast(pat.size()); + if (pathStart != path.begin() && *(pathStart - 1) != '\\') + return false; + + return std::equal(pathStart, pathEnd, pat.begin(), pat.end(), [](char a, char b) { + return std::tolower(static_cast(a)) == std::tolower(static_cast(b)); + }); + } + + bool Apply(const char* filename, std::string& content) + { + if (!loaded) + Load(); + + bool modified = false; + for (auto& entry : entries) { + if (!FilenameMatches(filename, entry.file)) + continue; + for (auto& rep : entry.replacements) { + size_t pos = 0; + while ((pos = content.find(rep.find, pos)) != std::string::npos) { + content.replace(pos, rep.find.size(), rep.replace); + pos += rep.replace.size(); + modified = true; + } + } + } + + if (modified) + logger::debug("[ShaderPatches] Patched {}", filename); + + return modified; + } + + bool Apply(const char* filename, std::vector& buffer) + { + std::string content(buffer.begin(), buffer.end()); + if (Apply(filename, content)) { + buffer.assign(content.begin(), content.end()); + return true; + } + return false; + } +} diff --git a/src/Utils/ShaderPatches.h b/src/Utils/ShaderPatches.h new file mode 100644 index 0000000000..173d3f4610 --- /dev/null +++ b/src/Utils/ShaderPatches.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +namespace Util +{ + namespace ShaderPatches + { + struct Replacement + { + std::string find; + std::string replace; + }; + + struct Entry + { + std::string file; + std::vector replacements; + }; + + void Load(); + bool Apply(const char* filename, std::vector& buffer); + bool Apply(const char* filename, std::string& content); + } +} diff --git a/vcpkg.json b/vcpkg.json index 1d6bd1ab78..726c2f406d 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -15,6 +15,7 @@ "cppwinrt", "directx-headers", "directxtex", + "effects11", "eastl", "efsw", {