diff --git a/package/Shaders/Common/SharedData.hlsli b/package/Shaders/Common/SharedData.hlsli index c9458e8cb7..2df2504f1e 100644 --- a/package/Shaders/Common/SharedData.hlsli +++ b/package/Shaders/Common/SharedData.hlsli @@ -23,6 +23,7 @@ namespace SharedData bool InMapMenu; // If the world/local map is open (note that the renderer is still deferred here) bool HideSky; // HideSky flag in WorldSpace, e.g. Blackreach float MipBias; // Offset to mip level for TAA sharpness# + uint IsNotNativeD3D11; // Linux/Proton/DXVK detected - use spatial shadow denoising }; struct GrassLightingSettings diff --git a/package/Shaders/Utility.hlsl b/package/Shaders/Utility.hlsl index 075dce0f38..a71eeec3df 100644 --- a/package/Shaders/Utility.hlsl +++ b/package/Shaders/Utility.hlsl @@ -375,7 +375,15 @@ float GetPoissonDiskFilteredShadowVisibility(uint3 seed, Texture2DArray float alphaTestOffset = -AlphaTestRef.y; float sampleRadius = ShadowSampleParam.z * 2048.0; float seedNormalized = seed.x * (1.0 / 4294967295.0); // Use only x component for efficiency - uint frameOffset = SharedData::FrameCount * sampleCount; + + // On Linux/Proton/DXVK, use position-based offset instead of frame-based to avoid temporal jitter + uint frameOffset; + if (SharedData::IsNotNativeD3D11) { + int3 quantizedPos = int3(floor(positionMS.xyz * 8.0)); + frameOffset = Random::murmur3(uint3(quantizedPos)); + } else { + frameOffset = SharedData::FrameCount * sampleCount; + } // Pre-compute constants const float3 positionOffsetUp = float3(0, 0, 1); @@ -616,7 +624,15 @@ PS_OUTPUT main(PS_INPUT input) float fadeFactor = input.Alpha.x; # endif - float noise = Random::InterleavedGradientNoise(input.PositionCS.xy, SharedData::FrameCount); + // On Linux/Proton/DXVK, use spatial noise based on world position instead of frame-based temporal noise to stop shimmering + float noise; + if (SharedData::IsNotNativeD3D11) { + int3 quantizedPos = int3(floor(positionMS.xyz * 8.0)); + uint posHash = Random::murmur3(uint3(quantizedPos)); + noise = float(posHash) / 4294967295.0; + } else { + noise = Random::InterleavedGradientNoise(input.PositionCS.xy, SharedData::FrameCount); + } float2 rotation; sincos(Math::TAU * noise, rotation.y, rotation.x); diff --git a/src/State.cpp b/src/State.cpp index 3846ab0046..34fdeb0611 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -707,6 +707,16 @@ void State::SetAdapterDescription(const std::wstring& description) { std::wstring_convert> converter; adapterDescription = converter.to_bytes(description); + + // Detect Linux/Proton/DXVK by checking adapter description + std::string lowerDesc = adapterDescription; + std::transform(lowerDesc.begin(), lowerDesc.end(), lowerDesc.begin(), ::tolower); + isRunningOnDXVK = (lowerDesc.find("dxvk") != std::string::npos) || + (lowerDesc.find("wine") != std::string::npos) || + (lowerDesc.find("llvmpipe") != std::string::npos); + if (isRunningOnDXVK) { + logger::info("Detected Linux/Proton/DXVK environment - using spatial shadow denoising"); + } } void State::UpdateSharedData([[maybe_unused]] bool a_inWorld, [[maybe_unused]] bool a_prepass) @@ -778,6 +788,8 @@ void State::UpdateSharedData([[maybe_unused]] bool a_inWorld, [[maybe_unused]] b data.MipBias = 0; } + data.IsNotNativeD3D11 = isRunningOnDXVK ? 1 : 0; + sharedDataCB->Update(data); } diff --git a/src/State.h b/src/State.h index d529e904ff..838ee78343 100644 --- a/src/State.h +++ b/src/State.h @@ -45,6 +45,7 @@ class State bool settingCustomShader = false; RE::BSShader* currentShader = nullptr; std::string adapterDescription = ""; + bool isRunningOnDXVK = false; // Detected via adapter description uint32_t currentVertexDescriptor = 0; uint32_t currentPixelDescriptor = 0; @@ -203,7 +204,7 @@ class State uint InMapMenu; uint HideSky; float MipBias; - float pad0; + uint IsNotNativeD3D11; // True on DXVK/Wine - use spatial denoising instead of temporal }; STATIC_ASSERT_ALIGNAS_16(SharedDataCB);