From 9e0d1978a4ca309d9290c6bb91a87ee7200cfe85 Mon Sep 17 00:00:00 2001 From: Hlib Omelchenko Date: Wed, 7 Oct 2026 13:06:58 +0200 Subject: [PATCH 1/4] Shaders: Share the SMAA passes, XeGTAO, MacLeod-Boynton, RCAS and dgVoodoo helpers between the games that copied them --- .../Luma_BL2TPS_Sharpen.hlsl | 20 - .../Luma_SMAA_impl.hlsl | 76 +- .../Luma_BL_Sharpen.hlsl | 20 - .../Luma_BL_Tonemap.hlsl | 2 +- .../Luma_BL_XeGTAO.hlsl | 648 +-------------- .../Luma_SMAA_impl.hlsl | 70 +- .../Luma_RCAS_PS.hlsl} | 5 +- Shaders/Includes/DgVoodoo.hlsl | 28 + .../Includes/MacLeodBoynton.hlsl | 0 Shaders/Includes/SMAA_Passes.hlsl | 84 ++ Shaders/Includes/XeGTAO.hlsl | 740 ++++++++++++++++++ .../Mass Effect Andromeda/Luma_SMAA_impl.hlsl | 68 +- .../Luma_MELE_Sharpen.hlsl | 20 - .../Luma_MELE_XeGTAO.hlsl | 645 +-------------- .../Luma_SMAA_impl.hlsl | 68 +- .../Mass Effect/Includes/GameBindings.hlsl | 7 +- .../Mass Effect/Includes/MacLeodBoynton.hlsl | 557 ------------- Shaders/Mass Effect/Luma_ME1_Sharpen.hlsl | 19 - Shaders/Mass Effect/Luma_ME1_Tonemap.hlsl | 2 +- Shaders/Mass Effect/Luma_SMAA_impl.hlsl | 71 +- .../Includes/GameBindings.hlsl | 7 +- .../Includes/MacLeodBoynton.hlsl | 557 ------------- .../Luma_MOHA_Sharpen.hlsl | 19 - .../Luma_MOHA_Tonemap.hlsl | 2 +- .../Luma_SMAA_impl.hlsl | 70 +- .../FinalGrade_0xDE5CF9CD.ps_5_0.hlsl | 2 +- .../The Witcher 2/Includes/GameBindings.hlsl | 26 +- .../Includes/MacLeodBoynton.hlsl | 557 ------------- Shaders/The Witcher 2/Luma_SMAA_impl.hlsl | 70 +- Shaders/The Witcher 2/Luma_TW2_Sharpen.hlsl | 22 - Shaders/The Witcher 2/Luma_TW2_XeGTAO.hlsl | 681 +--------------- .../Borderlands 2 and The Pre-Sequel/main.cpp | 2 +- .../Games/Borderlands GOTY Enhanced/main.cpp | 2 +- Source/Games/Mass Effect Andromeda/main.cpp | 2 +- .../Mass Effect Legendary Edition/main.cpp | 2 +- Source/Games/Mass Effect/main.cpp | 2 +- Source/Games/Medal of Honor Airborne/main.cpp | 2 +- Source/Games/The Witcher 2/main.cpp | 2 +- 38 files changed, 949 insertions(+), 4228 deletions(-) delete mode 100644 Shaders/Borderlands 2 and The Pre-Sequel/Luma_BL2TPS_Sharpen.hlsl delete mode 100644 Shaders/Borderlands GOTY Enhanced/Luma_BL_Sharpen.hlsl rename Shaders/{Mass Effect Andromeda/Luma_MEA_Sharpen.hlsl => Global/Luma_RCAS_PS.hlsl} (65%) create mode 100644 Shaders/Includes/DgVoodoo.hlsl rename Shaders/{Borderlands GOTY Enhanced => }/Includes/MacLeodBoynton.hlsl (100%) create mode 100644 Shaders/Includes/SMAA_Passes.hlsl create mode 100644 Shaders/Includes/XeGTAO.hlsl delete mode 100644 Shaders/Mass Effect Legendary Edition/Luma_MELE_Sharpen.hlsl delete mode 100644 Shaders/Mass Effect/Includes/MacLeodBoynton.hlsl delete mode 100644 Shaders/Mass Effect/Luma_ME1_Sharpen.hlsl delete mode 100644 Shaders/Medal of Honor Airborne/Includes/MacLeodBoynton.hlsl delete mode 100644 Shaders/Medal of Honor Airborne/Luma_MOHA_Sharpen.hlsl delete mode 100644 Shaders/The Witcher 2/Includes/MacLeodBoynton.hlsl delete mode 100644 Shaders/The Witcher 2/Luma_TW2_Sharpen.hlsl diff --git a/Shaders/Borderlands 2 and The Pre-Sequel/Luma_BL2TPS_Sharpen.hlsl b/Shaders/Borderlands 2 and The Pre-Sequel/Luma_BL2TPS_Sharpen.hlsl deleted file mode 100644 index ef2f676fb..000000000 --- a/Shaders/Borderlands 2 and The Pre-Sequel/Luma_BL2TPS_Sharpen.hlsl +++ /dev/null @@ -1,20 +0,0 @@ -// RCAS sharpening for the SMAA output: runs after the neighborhood blend on the gamma LDR and writes the LDR RTV -// (Display Composition does paper-white + scRGB downstream). paperWhite = 1.0; the sharpness slider is the knob, -// RCAS_LIMIT bounds the lobe so bright pixels don't over-sharpen. - -#include "../Includes/RCAS.hlsl" - -cbuffer SharpenCB : register(b0) -{ - float4 SharpenParams; // (width, height, sharpness[0..1], unused) -} - -Texture2D tex0 : register(t0); // SMAA output (gamma LDR) -Texture2D dummyMV : register(t1); // unused (dynamicSharpening = false) - -float4 sharpen_ps(float4 pos : SV_Position) : SV_Target -{ - int2 p = int2(pos.xy); - int2 maxPixel = int2((int)SharpenParams.x - 1, (int)SharpenParams.y - 1); - return RCAS(p, int2(0, 0), maxPixel, SharpenParams.z, tex0, dummyMV, 1.0, false, (float4)0, false); -} diff --git a/Shaders/Borderlands 2 and The Pre-Sequel/Luma_SMAA_impl.hlsl b/Shaders/Borderlands 2 and The Pre-Sequel/Luma_SMAA_impl.hlsl index b700843ac..90f7765d0 100644 --- a/Shaders/Borderlands 2 and The Pre-Sequel/Luma_SMAA_impl.hlsl +++ b/Shaders/Borderlands 2 and The Pre-Sequel/Luma_SMAA_impl.hlsl @@ -28,78 +28,20 @@ cbuffer SmaaMetricsCB : register(b1) // half-way point simply means "the extract called this a silhouette". Calibrate its tolerance, not this number. #define SMAA_PREDICATION_STRENGTH 0.5 #define SMAA_PREDICATION_THRESHOLD 0.5 -#define SMAA_CUSTOM_SL -SamplerState LinearSampler : register(s0); -SamplerState PointSampler : register(s1); -#define SMAATexture2D(tex) Texture2D tex -#define SMAATexturePass2D(tex) tex -#define SMAASampleLevelZero(tex, coord) tex.SampleLevel(LinearSampler, coord, 0) -#define SMAASampleLevelZeroPoint(tex, coord) tex.SampleLevel(PointSampler, coord, 0) -#define SMAASampleLevelZeroOffset(tex, coord, offset) tex.SampleLevel(LinearSampler, coord, 0, offset) -#define SMAASample(tex, coord) tex.Sample(LinearSampler, coord) -#define SMAASamplePoint(tex, coord) tex.Sample(PointSampler, coord) -#define SMAASampleOffset(tex, coord, offset) tex.Sample(LinearSampler, coord, offset) -#define SMAA_FLATTEN [flatten] -#define SMAA_BRANCH [branch] -#define SMAATexture2DMS2(tex) Texture2DMS tex -#define SMAALoad(tex, pos, sample) tex.Load(pos, sample) -#define SMAAGather(tex, coord) tex.Gather(LinearSampler, coord, 0) +#define SMAAGather(tex, coord) tex.Gather(LinearSampler, coord, 0) // Color.hlsl only for the re-encode helper. Neither it (it includes only Math.hlsl) nor the self-contained SMAA.hlsl // has a DEVELOPMENT/TEST conditional, so every entry point stays byte-identical across the two define sets. #include "../Includes/Color.hlsl" -#include "../Includes/SMAA.hlsl" - -Texture2D tex0 : register(t0); -Texture2D tex1 : register(t1); -Texture2D tex2 : register(t2); - -void fullscreen_triangle(uint id, out float4 position, out float2 texcoord) -{ - texcoord = float2((id << 1) & 2, id & 2); - position = float4(texcoord * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); -} // SMAAEdgeDetection -void smaa_edge_detection_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset[3] : TEXCOORD1) -{ - fullscreen_triangle(id, position, texcoord); - SMAAEdgeDetectionVS(texcoord, offset); -} - -float2 smaa_edge_detection_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset[3] : TEXCOORD1) : SV_Target -{ - // tex0 = colorTexGamma (the gamma-encoded LDR snapshot) - // tex1 = predicationTex (plane-deviation edge-ness; null fallback -> reads 0, scale 1.0 = plain ULTRA threshold) - return SMAAColorEdgeDetectionPS(texcoord, offset, tex0, tex1); -} - -// SMAABlendingWeightCalculation -void smaa_blending_weight_calculation_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float2 pixcoord : TEXCOORD1, out float4 offset[3] : TEXCOORD2) -{ - fullscreen_triangle(id, position, texcoord); - SMAABlendingWeightCalculationVS(texcoord, pixcoord, offset); -} - -float4 smaa_blending_weight_calculation_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float2 pixcoord : TEXCOORD1, float4 offset[3] : TEXCOORD2) : SV_Target -{ - // tex0 = edgesTex, tex1 = areaTex, tex2 = searchTex - return SMAABlendingWeightCalculationPS(texcoord, pixcoord, offset, tex0, tex1, tex2, 0); -} +// tex0 = colorTexGamma (the gamma-encoded LDR snapshot) +// tex1 = predicationTex (plane-deviation edge-ness; null fallback -> reads 0, scale 1.0 = plain ULTRA threshold) // SMAANeighborhoodBlending -void smaa_neighborhood_blending_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset : TEXCOORD1) -{ - fullscreen_triangle(id, position, texcoord); - SMAANeighborhoodBlendingVS(texcoord, offset); -} +// tex0 = colorTex, the LINEAR decode of the LDR (Luma_BL2TPS_SMAALinearize says why); tex1 = blendTex. Re-encode +// with the tonemap's own linear_to_gamma, so both sides move together if DefaultGamma ever does; GCT_MIRROR brings +// the dither's negative half at black back out unclamped. One encode only: the RTV is never an SRGB view. Alpha +// is left as the blend produced it (the tonemap writes o0.w = 0). No HDR tail. +#define SMAA_NEIGHBORHOOD_OUTPUT(color, position) color.rgb = linear_to_gamma(color.rgb, GCT_MIRROR); -float4 smaa_neighborhood_blending_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset : TEXCOORD1) : SV_Target -{ - // tex0 = colorTex, the LINEAR decode of the LDR (Luma_BL2TPS_SMAALinearize says why); tex1 = blendTex. Re-encode - // with the tonemap's own linear_to_gamma, so both sides move together if DefaultGamma ever does; GCT_MIRROR brings - // the dither's negative half at black back out unclamped. One encode only: the RTV is never an SRGB view. Alpha - // is left as the blend produced it (the tonemap writes o0.w = 0). No HDR tail. - float4 color = SMAANeighborhoodBlendingPS(texcoord, offset, tex0, tex1); - color.rgb = linear_to_gamma(color.rgb, GCT_MIRROR); - return color; -} +#include "../Includes/SMAA_Passes.hlsl" diff --git a/Shaders/Borderlands GOTY Enhanced/Luma_BL_Sharpen.hlsl b/Shaders/Borderlands GOTY Enhanced/Luma_BL_Sharpen.hlsl deleted file mode 100644 index 42ed797f7..000000000 --- a/Shaders/Borderlands GOTY Enhanced/Luma_BL_Sharpen.hlsl +++ /dev/null @@ -1,20 +0,0 @@ -// RCAS sharpening for the SMAA output, for Borderlands GOTY Enhanced. -// Runs after the SMAA neighborhood-blend pass, on the gamma canvas, before the result is copied back into the -// swapchain. paperWhite=1.0; RCAS_LIMIT bounds the lobe so HDR highlights don't over-sharpen. - -#include "../Includes/RCAS.hlsl" - -cbuffer SharpenCB : register(b0) -{ - float4 SharpenParams; // (width, height, sharpness[0..1], unused) -} - -Texture2D tex0 : register(t0); // SMAA output (gamma canvas) -Texture2D dummyMV : register(t1); // unused (dynamicSharpening = false) - -float4 sharpen_ps(float4 pos : SV_Position) : SV_Target -{ - int2 p = int2(pos.xy); - int2 maxPixel = int2((int)SharpenParams.x - 1, (int)SharpenParams.y - 1); - return RCAS(p, int2(0, 0), maxPixel, SharpenParams.z, tex0, dummyMV, 1.0, false, (float4)0, false); -} diff --git a/Shaders/Borderlands GOTY Enhanced/Luma_BL_Tonemap.hlsl b/Shaders/Borderlands GOTY Enhanced/Luma_BL_Tonemap.hlsl index af0163c01..b652269fc 100644 --- a/Shaders/Borderlands GOTY Enhanced/Luma_BL_Tonemap.hlsl +++ b/Shaders/Borderlands GOTY Enhanced/Luma_BL_Tonemap.hlsl @@ -34,7 +34,7 @@ #include "../Includes/ColorGradingLUT.hlsl" // SimpleGamutClip #include "../Includes/DICE.hlsl" // DICETonemap / DefaultDICESettings #include "../Includes/Reinhard.hlsl" // Reinhard::ReinhardPiecewise: soft hue reference -#include "Includes/MacLeodBoynton.hlsl" // MacLeodBoynton::HueOnlyBT2020 (RenoDX-derived hue/purity model) +#include "../Includes/MacLeodBoynton.hlsl" // MacLeodBoynton::HueOnlyBT2020 (RenoDX-derived hue/purity model) // clang-format on // HDR / vanilla. 1 = extended UE3 grade + DICE display map (default). 0 = vanilla clamped SDR reference. diff --git a/Shaders/Borderlands GOTY Enhanced/Luma_BL_XeGTAO.hlsl b/Shaders/Borderlands GOTY Enhanced/Luma_BL_XeGTAO.hlsl index dff150f39..5b7b691ae 100644 --- a/Shaders/Borderlands GOTY Enhanced/Luma_BL_XeGTAO.hlsl +++ b/Shaders/Borderlands GOTY Enhanced/Luma_BL_XeGTAO.hlsl @@ -62,18 +62,6 @@ cbuffer LumaGTAO : register(b11) #include "Includes/Common.hlsl" // game-local: order load-bearing (pulls GameCBuffers before shared includes) — do not let SortIncludes move it // clang-format on -#if XE_GTAO_QUALITY == 0 // Low -#define SLICE_COUNT 4.0 -#elif XE_GTAO_QUALITY == 1 // Medium -#define SLICE_COUNT 7.0 -#elif XE_GTAO_QUALITY == 2 // High -#define SLICE_COUNT 10.0 -#elif XE_GTAO_QUALITY == 3 // Very High -#define SLICE_COUNT 13.0 -#elif XE_GTAO_QUALITY == 4 // Ultra -#define SLICE_COUNT 16.0 -#endif - // User configurable // @@ -81,10 +69,6 @@ cbuffer LumaGTAO : register(b11) #define EFFECT_RADIUS 0.64 // anchored to the game's own HBAO+ radius: probed NegInvR2=0.000463 -> R=sqrt(1/NegInvR2)=46.5 UE3-units; chosen so the effective search (EFFECT_RADIUS * RADIUS_MULTIPLIER * DepthScale) = 0.64*1.457*50 = 46.6uu matches native R. RadiusOverrideRT > 0 wins. #endif -#ifndef RADIUS_MULTIPLIER -#define RADIUS_MULTIPLIER 1.457 // Default 1.457 -#endif - #ifndef EFFECT_FALLOFF_RANGE #define EFFECT_FALLOFF_RANGE 0.005 // punchy hard-edge falloff: full sample weight up to the radius edge = crisp contact AO matching BL's punchy native HBAO+ (vs the soft 0.615/0.95 gradual fade). Pairs with power ~1.0 #endif @@ -93,26 +77,6 @@ cbuffer LumaGTAO : register(b11) #define SAMPLE_DISTRIBUTION_POWER 1.5 // Default 2.0 #endif -#ifndef THIN_OCCLUDER_COMPENSATION -#define THIN_OCCLUDER_COMPENSATION 0.0 // Default 0.0; > 0 causes more mistakes than it fixes on big geometry -#endif - -#ifndef DEPTH_MIP_SAMPLING_OFFSET -#define DEPTH_MIP_SAMPLING_OFFSET 3.3 // Default 3.3 -#endif - -#ifndef SLICE_COUNT -#define SLICE_COUNT 3.0 // Default 3.0 -#endif - -#ifndef STEPS_PER_SLICE -#define STEPS_PER_SLICE 3.0 // Default 3.0 -#endif - -#ifndef DENOISE_BLUR_BETA -#define DENOISE_BLUR_BETA 1.2 // Default 1.2 -#endif - // BL packs the full xyz view-space normal (v*0.5+0.5) — decoded directly, no reconstruction. NORMAL_Z_SIGN // flips only the decoded z if the view-space handedness needs it (verify via DebugViewRT=2: smooth // per-surface shading = correct; flip to -1.0 if the shading looks inverted). @@ -120,20 +84,11 @@ cbuffer LumaGTAO : register(b11) #define NORMAL_Z_SIGN (1.0) #endif -// - #define VIEWPORT_PIXEL_SIZE InvFullResolution // GFSDK ProjInfo is exactly the NDC->view mul/add pair (live FOV, dialogue zoom included). -#define NDC_TO_VIEW_MUL ProjInfo.xy -#define NDC_TO_VIEW_ADD ProjInfo.zw -#define NDC_TO_VIEW_MUL_X_PIXEL_SIZE (NDC_TO_VIEW_MUL * VIEWPORT_PIXEL_SIZE) - -#define XE_GTAO_DEPTH_MIP_LEVELS 5.0 -#define XE_GTAO_OCCLUSION_TERM_SCALE 1.5 - -#define XE_GTAO_PI 3.1415926535897932384626433832795 -#define XE_GTAO_PI_HALF 1.5707963267948966192313216916398 +#define NDC_TO_VIEW_MUL ProjInfo.xy +#define NDC_TO_VIEW_ADD ProjInfo.zw // Hardware d24 (non-reverse-Z) -> view Z via the game's own MinZ_MaxZRatioCS, then rescaled by // DepthScaleRT so the tuned XeGTAO constants (radius/falloff, ~meter scale) apply. UE3 near plane ~10 @@ -145,603 +100,22 @@ float XeGTAO_ScreenSpaceToViewSpaceDepth(const float screenDepth) return max(0.0, viewZ) / max(1e-3, DepthScaleRT); } -// This is also a good place to do non-linear depth conversion for cases where one wants the 'radius' (effectively the threshold between near-field and far-field GI), -// is required to be non-linear (i.e. very large outdoors environments). -float XeGTAO_ClampDepth(float depth) -{ - return clamp(depth, 0.0, 3.402823466e+38); -} - -float XeGTAO_EffectRadius() -{ - return (RadiusOverrideRT > 0.0 ? RadiusOverrideRT : EFFECT_RADIUS) * RADIUS_MULTIPLIER; -} - -// weighted average depth filter -float XeGTAO_DepthMIPFilter(float depth0, float depth1, float depth2, float depth3) -{ - float maxDepth = max(max(depth0, depth1), max(depth2, depth3)); - - const float depthRangeScaleFactor = 0.75; // found empirically :) - const float effectRadius = depthRangeScaleFactor * XeGTAO_EffectRadius(); - const float falloffRange = EFFECT_FALLOFF_RANGE * effectRadius; - const float falloffFrom = effectRadius * (1.0 - EFFECT_FALLOFF_RANGE); - - // fadeout precompute optimisation - const float falloffMul = -1.0 / falloffRange; - const float falloffAdd = falloffFrom / falloffRange + 1.0; - - float weight0 = saturate((maxDepth - depth0) * falloffMul + falloffAdd); - float weight1 = saturate((maxDepth - depth1) * falloffMul + falloffAdd); - float weight2 = saturate((maxDepth - depth2) * falloffMul + falloffAdd); - float weight3 = saturate((maxDepth - depth3) * falloffMul + falloffAdd); - - float weightSum = weight0 + weight1 + weight2 + weight3; - return (weight0 * depth0 + weight1 * depth1 + weight2 * depth2 + weight3 * depth3) * rcp(weightSum); -} - -groupshared float g_scratchDepths[8][8]; -void XeGTAO_PrefilterDepths16x16(uint2 dispatchThreadID, uint2 groupThreadID, Texture2D sourceNDCDepth, RWTexture2D outDepth0, RWTexture2D outDepth1, RWTexture2D outDepth2, RWTexture2D outDepth3, RWTexture2D outDepth4) -{ - // MIP 0 - const uint2 baseCoord = dispatchThreadID; - const uint2 pixCoord = baseCoord * 2; - // Explicit integer Loads of the 2x2, not GatherRed: GatherRed on the r24_unorm_x8 depth view returns 0 on - // some drivers (flat depth -> visibility 1 -> AO silently gone), so do not switch this back to Gather. - // Out-of-bounds Loads return 0 (D3D11-defined), clamped depth. - float d00 = sourceNDCDepth.Load(int3(pixCoord + uint2(0, 0), 0)).x; - float d10 = sourceNDCDepth.Load(int3(pixCoord + uint2(1, 0), 0)).x; - float d01 = sourceNDCDepth.Load(int3(pixCoord + uint2(0, 1), 0)).x; - float d11 = sourceNDCDepth.Load(int3(pixCoord + uint2(1, 1), 0)).x; - float depth0 = XeGTAO_ClampDepth(XeGTAO_ScreenSpaceToViewSpaceDepth(d00)); - float depth1 = XeGTAO_ClampDepth(XeGTAO_ScreenSpaceToViewSpaceDepth(d10)); - float depth2 = XeGTAO_ClampDepth(XeGTAO_ScreenSpaceToViewSpaceDepth(d01)); - float depth3 = XeGTAO_ClampDepth(XeGTAO_ScreenSpaceToViewSpaceDepth(d11)); - outDepth0[pixCoord + uint2(0, 0)] = depth0; - outDepth0[pixCoord + uint2(1, 0)] = depth1; - outDepth0[pixCoord + uint2(0, 1)] = depth2; - outDepth0[pixCoord + uint2(1, 1)] = depth3; - - // MIP 1 - float dm1 = XeGTAO_DepthMIPFilter(depth0, depth1, depth2, depth3); - outDepth1[baseCoord] = dm1; - g_scratchDepths[groupThreadID.x][groupThreadID.y] = dm1; - - GroupMemoryBarrierWithGroupSync(); - - // MIP 2 - [branch] if (all((groupThreadID.xy % 2) == 0)) - { - float inTL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 0]; - float inTR = g_scratchDepths[groupThreadID.x + 1][groupThreadID.y + 0]; - float inBL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 1]; - float inBR = g_scratchDepths[groupThreadID.x + 1][groupThreadID.y + 1]; - - float dm2 = XeGTAO_DepthMIPFilter(inTL, inTR, inBL, inBR); - outDepth2[baseCoord / 2] = dm2; - g_scratchDepths[groupThreadID.x][groupThreadID.y] = dm2; - } - - GroupMemoryBarrierWithGroupSync(); - - // MIP 3 - [branch] if (all((groupThreadID.xy % 4) == 0)) - { - float inTL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 0]; - float inTR = g_scratchDepths[groupThreadID.x + 2][groupThreadID.y + 0]; - float inBL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 2]; - float inBR = g_scratchDepths[groupThreadID.x + 2][groupThreadID.y + 2]; - - float dm3 = XeGTAO_DepthMIPFilter(inTL, inTR, inBL, inBR); - outDepth3[baseCoord / 4] = dm3; - g_scratchDepths[groupThreadID.x][groupThreadID.y] = dm3; - } - - GroupMemoryBarrierWithGroupSync(); - - // MIP 4 - [branch] if (all((groupThreadID.xy % 8) == 0)) - { - float inTL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 0]; - float inTR = g_scratchDepths[groupThreadID.x + 4][groupThreadID.y + 0]; - float inBL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 4]; - float inBR = g_scratchDepths[groupThreadID.x + 4][groupThreadID.y + 4]; - - float dm4 = XeGTAO_DepthMIPFilter(inTL, inTR, inBL, inBR); - outDepth4[baseCoord / 8] = dm4; - // g_scratchDepths[ groupThreadID.x ][ groupThreadID.y ] = dm4; - } -} - -float4 XeGTAO_CalculateEdges(float centerZ, float leftZ, float rightZ, float topZ, float bottomZ) -{ - float4 edgesLRTB = float4(leftZ, rightZ, topZ, bottomZ) - centerZ; - - float slopeLR = (edgesLRTB.y - edgesLRTB.x) * 0.5; - float slopeTB = (edgesLRTB.w - edgesLRTB.z) * 0.5; - float4 edgesLRTBSlopeAdjusted = edgesLRTB + float4(slopeLR, -slopeLR, slopeTB, -slopeTB); - edgesLRTB = min(abs(edgesLRTB), abs(edgesLRTBSlopeAdjusted)); - return saturate(1.25 - edgesLRTB * rcp(centerZ * 0.011)); -} - -// packing/unpacking for edges; 2 bits per edge mean 4 gradient values (0, 0.33, 0.66, 1) for smoother transitions! -float XeGTAO_PackEdges(float4 edgesLRTB) -{ - edgesLRTB = round(saturate(edgesLRTB) * 2.9); - return dot(edgesLRTB, float4(64.0 / 255.0, 16.0 / 255.0, 4.0 / 255.0, 1.0 / 255.0)); -} - -// Inputs are screen XY and viewspace depth, output is viewspace position -float3 XeGTAO_ComputeViewspacePosition(float2 screenPos, float viewspaceDepth) -{ - float3 ret; - ret.xy = (NDC_TO_VIEW_MUL * screenPos.xy + NDC_TO_VIEW_ADD) * viewspaceDepth; - ret.z = viewspaceDepth; - return ret; -} - -// http://h14s.p5r.org/2012/09/0x5f3759df.html, [Drobot2014a] Low Level Optimizations for GCN, https://blog.selfshadow.com/publications/s2016-shading-course/activision/s2016_pbs_activision_occlusion.pdf slide 63 -float XeGTAO_FastSqrt(float x) -{ - return asfloat(0x1fbd1df5 + (asint(x) >> 1)); -} - -// input [-1, 1] and output [0, PI], from https://seblagarde.wordpress.com/2014/12/01/inverse-trigonometric-functions-gpu-optimization-for-amd-gcn-architecture/ -float XeGTAO_FastACos(float inX) -{ - // NOTE: no local "PI" here — the game-local Common.hlsl include chain defines PI as a macro. - float x = abs(inX); - float res = -0.156583 * x + 1.570796; - res *= XeGTAO_FastSqrt(1.0 - x); - return inX >= 0 ? res : 3.141593 - res; -} - -void XeGTAO_MainPass(uint2 pixCoord, float2 localNoise, float3 viewspaceNormal, Texture2D sourceViewspaceDepth, SamplerState depthSampler, RWTexture2D outWorkingAOTermAndEdges) -{ - float2 normalizedScreenPos = (pixCoord + 0.5) * VIEWPORT_PIXEL_SIZE; - - // Center + cross depths from the prefiltered (already linearized + scaled) mip0. This texture is our - // own R32F, Gather is safe here (the r24 quirk is only on the game's depth view). - float4 valuesUL = sourceViewspaceDepth.GatherRed(depthSampler, float2(pixCoord * VIEWPORT_PIXEL_SIZE)); - float4 valuesBR = sourceViewspaceDepth.GatherRed(depthSampler, float2(pixCoord * VIEWPORT_PIXEL_SIZE), int2(1, 1)); - - // viewspace Z at the center - float viewspaceZ = valuesUL.y; - - // viewspace Zs left top right bottom - const float pixLZ = valuesUL.x; - const float pixTZ = valuesUL.z; - const float pixRZ = valuesBR.z; - const float pixBZ = valuesBR.x; - - float4 edgesLRTB = XeGTAO_CalculateEdges(viewspaceZ, pixLZ, pixRZ, pixTZ, pixBZ); - const float edges = XeGTAO_PackEdges(edgesLRTB); - -#if DEVELOPMENT - // Debug views (visible on screen through the game's own AO apply blit; the final denoise passes raw - // values through when DebugViewRT > 0). - if (DebugViewRT > 0.5 && DebugViewRT < 1.5) // 1 = depth gradient (proves live depth + linearization/scale) - { - outWorkingAOTermAndEdges[pixCoord] = float2(saturate(frac(log2(max(viewspaceZ, 1e-6)))), 1.0); - return; - } - if (DebugViewRT >= 3.5 && DebugViewRT < 4.5) // 4 = edges - { - outWorkingAOTermAndEdges[pixCoord] = float2(dot(edgesLRTB, 0.25), 1.0); - return; - } -#endif - - // Move center pixel slightly towards camera to avoid imprecision artifacts due to depth buffer imprecision; offset depends on depth texture format used - viewspaceZ *= 0.99999; // this is good for FP32 depth buffer - - const float3 pixCenterPos = XeGTAO_ComputeViewspacePosition(normalizedScreenPos, viewspaceZ); - const float3 viewVec = normalize(-pixCenterPos); - - // prevents normals that are facing away from the view vector - xeGTAO struggles with extreme cases. Upstream - // ships this line commented out as rare in vanilla; this port runs it. - viewspaceNormal = normalize(viewspaceNormal + max(0, -dot(viewspaceNormal, viewVec)) * viewVec); - -#if DEVELOPMENT - if (DebugViewRT >= 1.5 && DebugViewRT < 2.5) // 2 = normals view-facing term (smooth per-surface shading = correct decode) - { - outWorkingAOTermAndEdges[pixCoord] = float2(saturate(abs(dot(viewspaceNormal, viewVec))), 1.0); - return; - } -#endif - - const float effectRadius = XeGTAO_EffectRadius(); - const float sampleDistributionPower = SAMPLE_DISTRIBUTION_POWER; - const float thinOccluderCompensation = THIN_OCCLUDER_COMPENSATION; - const float falloffRange = EFFECT_FALLOFF_RANGE * effectRadius; - const float falloffFrom = effectRadius * (1.0 - EFFECT_FALLOFF_RANGE); - - // fadeout precompute optimisation - const float falloffMul = -1.0 / falloffRange; - const float falloffAdd = falloffFrom / falloffRange + 1.0; - - float visibility = 0.0; - - // see "Algorithm 1" in https://www.activision.com/cdn/research/Practical_Real_Time_Strategies_for_Accurate_Indirect_Occlusion_NEW%20VERSION_COLOR.pdf - { - const float noiseSlice = localNoise.x; - const float noiseSample = localNoise.y; - - // quality settings / tweaks / hacks - const float pixelTooCloseThreshold = 1.3; // if the offset is under approx pixel size (pixelTooCloseThreshold), push it out to the minimum distance - - // approx viewspace pixel size at pixCoord; approximation of NDCToViewspace( normalizedScreenPos.xy + consts.ViewportPixelSize.xy, pixCenterPos.z ).xy - pixCenterPos.xy; - const float2 pixelDirRBViewspaceSizeAtCenterZ = viewspaceZ.xx * NDC_TO_VIEW_MUL_X_PIXEL_SIZE; - - float screenspaceRadius = effectRadius * rcp(abs(pixelDirRBViewspaceSizeAtCenterZ.x)); - - // fade out for small screen radii - visibility += saturate((10.0 - screenspaceRadius) / 100.0) * 0.5; - - // this is the min distance to start sampling from to avoid sampling from the center pixel (no useful data obtained from sampling center pixel) - const float minS = pixelTooCloseThreshold * rcp(screenspaceRadius); - - //[unroll] - for (float slice = 0.0; slice < SLICE_COUNT; slice++) - { - float sliceK = (slice + noiseSlice) / SLICE_COUNT; - // lines 5, 6 from the paper - float phi = sliceK * XE_GTAO_PI; - float cosPhi = cos(phi); - float sinPhi = sin(phi); - float2 omega = float2(cosPhi, -sinPhi); // lpfloat2 on omega causes issues with big radii - - // convert to screen units (pixels) for later use - omega *= screenspaceRadius; +#define NoiseIndexRT 0 +#define XE_GTAO_ADJUST_VISIBILITY(visibility, viewspaceZ) visibility = max(0.03, visibility); // disallow total occlusion (which wouldn't make any sense anyhow since pixel is visible but also helps with packing bent normals) +#define XE_GTAO_FINAL_OUTPUT_TYPE float2 // the game's r16g16_float final AO (apply blit reads .x) +#define XE_GTAO_ENCODE_FINAL(v) float2(v, 0.0) - // line 8 from the paper - const float3 directionVec = float3(cosPhi, sinPhi, 0.0); - - // line 9 from the paper - const float3 orthoDirectionVec = directionVec - (dot(directionVec, viewVec) * viewVec); - - // line 10 from the paper - // axisVec is orthogonal to directionVec and viewVec, used to define projectedNormal - const float3 axisVec = normalize(cross(orthoDirectionVec, viewVec)); - - // line 11 from the paper - float3 projectedNormalVec = viewspaceNormal - axisVec * dot(viewspaceNormal, axisVec); - - // line 13 from the paper - float signNorm = sign(dot(orthoDirectionVec, projectedNormalVec)); - - // line 14 from the paper - float projectedNormalVecLength = length(projectedNormalVec); - float cosNorm = saturate(dot(projectedNormalVec, viewVec) * rcp(projectedNormalVecLength)); - - // line 15 from the paper - float n = signNorm * XeGTAO_FastACos(cosNorm); - - // this is a lower weight target; not using -1 as in the original paper because it is under horizon, so a 'weight' has different meaning based on the normal - const float lowHorizonCos0 = cos(n + XE_GTAO_PI_HALF); - const float lowHorizonCos1 = cos(n - XE_GTAO_PI_HALF); - - // lines 17, 18 from the paper, manually unrolled the 'side' loop - float horizonCos0 = lowHorizonCos0; //-1; - float horizonCos1 = lowHorizonCos1; //-1; - - [unroll] for (float step = 0.0; step < STEPS_PER_SLICE; step++) - { - // R1 sequence (http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/) - const float stepBaseNoise = (slice + step * STEPS_PER_SLICE) * 0.6180339887498948482; // <- this should unroll - float stepNoise = frac(noiseSample + stepBaseNoise); - - // approx line 20 from the paper, with added noise - float s = (step + stepNoise) / STEPS_PER_SLICE; // + (lpfloat2)1e-6f); - - // additional distribution modifier - s = pow(s, sampleDistributionPower); - - // avoid sampling center pixel - s += minS; - - // approx lines 21-22 from the paper, unrolled - float2 sampleOffset = s * omega; - - float sampleOffsetLength = length(sampleOffset); - - // note: when sampling, using point_point_point or point_point_linear sampler works, but linear_linear_linear will cause unwanted interpolation between neighbouring depth values on the same MIP level! - const float mipLevel = clamp(log2(sampleOffsetLength) - DEPTH_MIP_SAMPLING_OFFSET, 0.0, XE_GTAO_DEPTH_MIP_LEVELS); - - // Snap to pixel center (more correct direction math, avoids artifacts due to sampling pos not matching depth texel center - messes up slope - but adds other - // artifacts due to them being pushed off the slice). Also use full precision for high res cases. - sampleOffset = round(sampleOffset) * VIEWPORT_PIXEL_SIZE; - - float2 sampleScreenPos0 = normalizedScreenPos + sampleOffset; - float SZ0 = sourceViewspaceDepth.SampleLevel(depthSampler, sampleScreenPos0, mipLevel).x; - float3 samplePos0 = XeGTAO_ComputeViewspacePosition(sampleScreenPos0, SZ0); - - float2 sampleScreenPos1 = normalizedScreenPos - sampleOffset; - float SZ1 = sourceViewspaceDepth.SampleLevel(depthSampler, sampleScreenPos1, mipLevel).x; - float3 samplePos1 = XeGTAO_ComputeViewspacePosition(sampleScreenPos1, SZ1); - - float3 sampleDelta0 = samplePos0 - pixCenterPos; // using lpfloat for sampleDelta causes precision issues - float3 sampleDelta1 = samplePos1 - pixCenterPos; // using lpfloat for sampleDelta causes precision issues - float sampleDist0 = length(sampleDelta0); - float sampleDist1 = length(sampleDelta1); - - // approx lines 23, 24 from the paper, unrolled - float3 sampleHorizonVec0 = sampleDelta0 * rcp(sampleDist0); - float3 sampleHorizonVec1 = sampleDelta1 * rcp(sampleDist1); - - // any sample out of radius should be discarded - also use fallof range for smooth transitions; this is a modified idea from "4.3 Implementation details, Bounding the sampling area" - // this is our own thickness heuristic that relies on sooner discarding samples behind the center - float falloffBase0 = length(float3(sampleDelta0.x, sampleDelta0.y, sampleDelta0.z * (1.0 + thinOccluderCompensation))); - float falloffBase1 = length(float3(sampleDelta1.x, sampleDelta1.y, sampleDelta1.z * (1.0 + thinOccluderCompensation))); - float weight0 = saturate(falloffBase0 * falloffMul + falloffAdd); - float weight1 = saturate(falloffBase1 * falloffMul + falloffAdd); - - // sample horizon cos - float shc0 = dot(sampleHorizonVec0, viewVec); - float shc1 = dot(sampleHorizonVec1, viewVec); - - // discard unwanted samples - shc0 = lerp(lowHorizonCos0, shc0, weight0); // this would be more correct but too expensive: cos(lerp( acos(lowHorizonCos0), acos(shc0), weight0 )); - shc1 = lerp(lowHorizonCos1, shc1, weight1); // this would be more correct but too expensive: cos(lerp( acos(lowHorizonCos1), acos(shc1), weight1 )); - - // thickness heuristic disabled (THIN_OCCLUDER_COMPENSATION == 0) - horizonCos0 = max(horizonCos0, shc0); - horizonCos1 = max(horizonCos1, shc1); - } - -#if 1 // I can't figure out the slight overdarkening on high slopes, so I'm adding this fudge - in the training set, 0.05 is close (PSNR 21.34) to disabled (PSNR 21.45) - projectedNormalVecLength = lerp(projectedNormalVecLength, 1.0, 0.05); -#endif - - // line ~27, unrolled - float h0 = -XeGTAO_FastACos(horizonCos1); - float h1 = XeGTAO_FastACos(horizonCos0); - float iarc0 = (cosNorm + 2.0 * h0 * sin(n) - cos(2.0 * h0 - n)) / 4.0; - float iarc1 = (cosNorm + 2.0 * h1 * sin(n) - cos(2.0 * h1 - n)) / 4.0; - float localVisibility = projectedNormalVecLength * (iarc0 + iarc1); - visibility += localVisibility; - } - visibility /= SLICE_COUNT; - visibility = pow(visibility, max(0.05, FinalValuePowerRT)); // runtime dial (calibrated to the vanilla AO histogram) - visibility = max(0.03, visibility); // disallow total occlusion (which wouldn't make any sense anyhow since pixel is visible but also helps with packing bent normals) - } - - visibility = saturate(visibility / XE_GTAO_OCCLUSION_TERM_SCALE); - outWorkingAOTermAndEdges[pixCoord] = float2(visibility, edges); -} - -void XeGTAO_DecodeGatherPartial(float4 packedValue, out float outDecoded[4]) -{ - for (int i = 0; i < 4; i++) - { - outDecoded[i] = packedValue[i]; - } -} - -float4 XeGTAO_UnpackEdges(float _packedVal) -{ - uint packedVal = uint(_packedVal * 255.5); - float4 edgesLRTB; - edgesLRTB.x = float((packedVal >> 6) & 0x03) / 3.0; // there's really no need for mask (as it's an 8 bit input) but I'll leave it in so it doesn't cause any trouble in the future - edgesLRTB.y = float((packedVal >> 4) & 0x03) / 3.0; - edgesLRTB.z = float((packedVal >> 2) & 0x03) / 3.0; - edgesLRTB.w = float((packedVal >> 0) & 0x03) / 3.0; - - return saturate(edgesLRTB); -} - -void XeGTAO_AddSample(float ssaoValue, float edgeValue, inout float sum, inout float sumWeight) -{ - float weight = edgeValue; - - sum += weight * ssaoValue; - sumWeight += weight; -} - -void XeGTAO_Denoise(uint2 pixCoordBase, Texture2D sourceAOTermAndEdges, SamplerState texSampler, -#if XE_GTAO_FINAL_APPLY - RWTexture2D outputTexture // the game's final AO target is r16g16_float (apply reads .x) -#else - RWTexture2D outputTexture -#endif -) -{ -#if DEVELOPMENT - // Debug views: pass the raw working value through unblurred so the on-screen viz is exact. - if (DebugViewRT > 0.5) - { - for (int dside = 0; dside < 2; dside++) - { - const uint2 dpix = uint2(pixCoordBase.x + dside, pixCoordBase.y); - float v = sourceAOTermAndEdges.Load(int3(dpix, 0)).x; -#if XE_GTAO_FINAL_APPLY - if (DebugViewRT >= 2.5 && DebugViewRT < 3.5) // 3 = AO x8 amplification (spot broad over-occlusion) - v = saturate(1.0 - (1.0 - v * XE_GTAO_OCCLUSION_TERM_SCALE) * 8.0); - outputTexture[dpix] = float2(v, 0.0); -#else - outputTexture[dpix] = float2(v, sourceAOTermAndEdges.Load(int3(dpix, 0)).y); -#endif - } - return; - } -#endif - -#if XE_GTAO_FINAL_APPLY - const float blurAmount = DENOISE_BLUR_BETA; -#else - const float blurAmount = DENOISE_BLUR_BETA / 5.0; -#endif - - const float diagWeight = 0.85 * 0.5; - - float aoTerm[2]; // pixel pixCoordBase and pixel pixCoordBase + int2( 1, 0 ) - float4 edgesC_LRTB[2]; - float weightTL[2]; - float weightTR[2]; - float weightBL[2]; - float weightBR[2]; - - // gather edge and visibility quads, used later - const float2 gatherCenter = float2(pixCoordBase.x, pixCoordBase.y) * VIEWPORT_PIXEL_SIZE; - float4 edgesQ0 = sourceAOTermAndEdges.GatherGreen(texSampler, gatherCenter, int2(0, 0)); - float4 edgesQ1 = sourceAOTermAndEdges.GatherGreen(texSampler, gatherCenter, int2(2, 0)); - float4 edgesQ2 = sourceAOTermAndEdges.GatherGreen(texSampler, gatherCenter, int2(1, 2)); - - float visQ0[4]; - XeGTAO_DecodeGatherPartial(sourceAOTermAndEdges.GatherRed(texSampler, gatherCenter, int2(0, 0)), visQ0); - float visQ1[4]; - XeGTAO_DecodeGatherPartial(sourceAOTermAndEdges.GatherRed(texSampler, gatherCenter, int2(2, 0)), visQ1); - float visQ2[4]; - XeGTAO_DecodeGatherPartial(sourceAOTermAndEdges.GatherRed(texSampler, gatherCenter, int2(0, 2)), visQ2); - float visQ3[4]; - XeGTAO_DecodeGatherPartial(sourceAOTermAndEdges.GatherRed(texSampler, gatherCenter, int2(2, 2)), visQ3); - - for (int side = 0; side < 2; side++) - { - const int2 pixCoord = int2(pixCoordBase.x + side, pixCoordBase.y); - - float4 edgesL_LRTB = XeGTAO_UnpackEdges(side == 0 ? edgesQ0.x : edgesQ0.y); - float4 edgesT_LRTB = XeGTAO_UnpackEdges(side == 0 ? edgesQ0.z : edgesQ1.w); - float4 edgesR_LRTB = XeGTAO_UnpackEdges(side == 0 ? edgesQ1.x : edgesQ1.y); - float4 edgesB_LRTB = XeGTAO_UnpackEdges(side == 0 ? edgesQ2.w : edgesQ2.z); - - edgesC_LRTB[side] = XeGTAO_UnpackEdges(side == 0 ? edgesQ0.y : edgesQ1.x); - - // Edges aren't perfectly symmetrical: edge detection algorithm does not guarantee that a left edge on the right pixel will match the right edge on the left pixel (although - // they will match in majority of cases). This line further enforces the symmetricity, creating a slightly sharper blur. Works real nice with TAA. - edgesC_LRTB[side] *= float4(edgesL_LRTB.y, edgesR_LRTB.x, edgesT_LRTB.w, edgesB_LRTB.z); - -#if 1 // this allows some small amount of AO leaking from neighbours if there are 3 or 4 edges; this reduces both spatial and temporal aliasing - const float leak_threshold = 2.5; - const float leak_strength = 0.5; - float edginess = (saturate(4.0 - leak_threshold - dot(edgesC_LRTB[side], 1.0)) * rcp(4.0 - leak_threshold)) * leak_strength; - edgesC_LRTB[side] = saturate(edgesC_LRTB[side] + edginess); -#endif - - // for diagonals; used by first and second pass - weightTL[side] = diagWeight * (edgesC_LRTB[side].x * edgesL_LRTB.z + edgesC_LRTB[side].z * edgesT_LRTB.x); - weightTR[side] = diagWeight * (edgesC_LRTB[side].z * edgesT_LRTB.y + edgesC_LRTB[side].y * edgesR_LRTB.z); - weightBL[side] = diagWeight * (edgesC_LRTB[side].w * edgesB_LRTB.x + edgesC_LRTB[side].x * edgesL_LRTB.w); - weightBR[side] = diagWeight * (edgesC_LRTB[side].y * edgesR_LRTB.w + edgesC_LRTB[side].w * edgesB_LRTB.y); - - // first pass - float ssaoValue = side == 0 ? visQ0[1] : visQ1[0]; - float ssaoValueL = side == 0 ? visQ0[0] : visQ0[1]; - float ssaoValueT = side == 0 ? visQ0[2] : visQ1[3]; - float ssaoValueR = side == 0 ? visQ1[0] : visQ1[1]; - float ssaoValueB = side == 0 ? visQ2[2] : visQ3[3]; - float ssaoValueTL = side == 0 ? visQ0[3] : visQ0[2]; - float ssaoValueBR = side == 0 ? visQ3[3] : visQ3[2]; - float ssaoValueTR = side == 0 ? visQ1[3] : visQ1[2]; - float ssaoValueBL = side == 0 ? visQ2[3] : visQ2[2]; - - float sumWeight = blurAmount; - float sum = ssaoValue * sumWeight; - - XeGTAO_AddSample(ssaoValueL, edgesC_LRTB[side].x, sum, sumWeight); - XeGTAO_AddSample(ssaoValueR, edgesC_LRTB[side].y, sum, sumWeight); - XeGTAO_AddSample(ssaoValueT, edgesC_LRTB[side].z, sum, sumWeight); - XeGTAO_AddSample(ssaoValueB, edgesC_LRTB[side].w, sum, sumWeight); - - XeGTAO_AddSample(ssaoValueTL, weightTL[side], sum, sumWeight); - XeGTAO_AddSample(ssaoValueTR, weightTR[side], sum, sumWeight); - XeGTAO_AddSample(ssaoValueBL, weightBL[side], sum, sumWeight); - XeGTAO_AddSample(ssaoValueBR, weightBR[side], sum, sumWeight); - - aoTerm[side] = sum / sumWeight; - -#if XE_GTAO_FINAL_APPLY - outputTexture[pixCoord] = float2(saturate(aoTerm[side] * XE_GTAO_OCCLUSION_TERM_SCALE), 0.0); -#else - outputTexture[pixCoord] = float2(aoTerm[side], side == 0 ? edgesQ0.y : edgesQ1.x); -#endif - } -} - -// Implementation -///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -SamplerState smp : register(s0); // point-clamp, bound by Luma (the game's AO CS uses raw Loads, s0 is not reliably set) - -Texture2D tex0 : register(t0); Texture2D tex1 : register(t1); -RWTexture2D out_working_depth_mip0 : register(u0); -RWTexture2D out_working_depth_mip1 : register(u1); -RWTexture2D out_working_depth_mip2 : register(u2); -RWTexture2D out_working_depth_mip3 : register(u3); -RWTexture2D out_working_depth_mip4 : register(u4); -RWTexture2D ao_term_and_edges : register(u0); - -#if XE_GTAO_FINAL_APPLY -RWTexture2D final_output : register(u0); // the game's r16g16_float final AO (apply blit reads .x) -#else -RWTexture2D final_output : register(u0); -#endif - -#define XE_GTAO_NUMTHREADS_X 8 -#define XE_GTAO_NUMTHREADS_Y 8 - -// From https://www.shadertoy.com/view/3tB3z3 - except we're using R2 here -#define XE_HILBERT_LEVEL 6U -#define XE_HILBERT_WIDTH (1U << XE_HILBERT_LEVEL) -#define XE_HILBERT_AREA (XE_HILBERT_WIDTH * XE_HILBERT_WIDTH) -uint HilbertIndex(uint posX, uint posY) -{ - uint index = 0U; - [unroll] for (uint curLevel = XE_HILBERT_WIDTH / 2U; curLevel > 0U; curLevel /= 2U) - { - uint regionX = (posX & curLevel) > 0U; - uint regionY = (posY & curLevel) > 0U; - index += curLevel * curLevel * ((3U * regionX) ^ regionY); - if (regionY == 0U) - { - if (regionX == 1U) - { - posX = XE_HILBERT_WIDTH - 1U - posX; - posY = XE_HILBERT_WIDTH - 1U - posY; - } - uint temp = posX; - posX = posY; - posY = temp; - } - } - return index; -} - -// BL GOTY has no TAA: temporalIndex is ALWAYS 0 (frozen pattern — static noise instead of boiling). -float2 SpatioTemporalNoise(uint2 pixCoord, uint temporalIndex) -{ - float2 noise; - uint index = HilbertIndex(pixCoord.x, pixCoord.y); - index += 288 * (temporalIndex % 64); // why 288? tried out a few and that's the best so far (with XE_HILBERT_LEVEL 6U) - but there's probably better :) - // R2 sequence - see http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/ - return float2(frac(0.5 + index * float2(0.75487766624669276005, 0.5698402909980532659114))); -} - -[numthreads(8, 8, 1)] // <- hard coded to 8x8; each thread computes 2x2 blocks so processing 16x16 block: Dispatch needs to be called with (width + 16-1) / 16, (height + 16-1) / 16 - void prefilter_depths16x16_cs(uint2 dtid : SV_DispatchThreadID, uint2 gtid : SV_GroupThreadID) { - // tex0 = the game's full-res scene depth (r24_g8, viewed r24_unorm_x8), captured at the deinterleave dispatch - XeGTAO_PrefilterDepths16x16(dtid, gtid, tex0, out_working_depth_mip0, out_working_depth_mip1, out_working_depth_mip2, out_working_depth_mip3, out_working_depth_mip4); - } - - [numthreads(XE_GTAO_NUMTHREADS_X, XE_GTAO_NUMTHREADS_Y, 1)] void main_pass_cs(uint2 dtid : SV_DispatchThreadID) +float3 XeGTAO_LoadViewspaceNormal(uint2 pixCoord) { - // tex0 = prefiltered viewspace depth MIP pyramid (our R32F) // tex1 = the game's ViewNormalTex (r11g11b10_float, captured at the coarse-AO dispatch) - // smp = point-clamp // Decode the game's packed view-space normals: xyz in [0,1] -> [-1,1], all three channels (see NORMAL_Z_SIGN). - float3 n = tex1.Load(int3(dtid, 0)).xyz * 2.0 - 1.0; + float3 n = tex1.Load(int3(pixCoord, 0)).xyz * 2.0 - 1.0; n.z *= NORMAL_Z_SIGN; - float3 viewspaceNormal = normalize(n); - - XeGTAO_MainPass(dtid, SpatioTemporalNoise(dtid, 0), viewspaceNormal, tex0, smp, ao_term_and_edges); + return normalize(n); } -[numthreads(XE_GTAO_NUMTHREADS_X, XE_GTAO_NUMTHREADS_Y, 1)] void denoise_pass_cs(uint2 dtid : SV_DispatchThreadID) { - // tex0 = working AO term (.x) + packed edges (.y) - // smp = point-clamp - const uint2 pix_coord_base = dtid * uint2(2, 1); // we're computing 2 horizontal pixels at a time (performance optimization) - XeGTAO_Denoise(pix_coord_base, tex0, smp, final_output); -} +// tex0 = the game's full-res scene depth (r24_g8, viewed r24_unorm_x8), captured at the deinterleave dispatch +#include "../Includes/XeGTAO.hlsl" diff --git a/Shaders/Borderlands GOTY Enhanced/Luma_SMAA_impl.hlsl b/Shaders/Borderlands GOTY Enhanced/Luma_SMAA_impl.hlsl index 1c5bffcf9..df80af625 100644 --- a/Shaders/Borderlands GOTY Enhanced/Luma_SMAA_impl.hlsl +++ b/Shaders/Borderlands GOTY Enhanced/Luma_SMAA_impl.hlsl @@ -30,72 +30,14 @@ cbuffer SmaaMetricsCB : register(b1) // half-way point simply means "the extract called this a silhouette". Calibrate its tolerance, not this number. #define SMAA_PREDICATION_STRENGTH 0.5 #define SMAA_PREDICATION_THRESHOLD 0.5 -#define SMAA_CUSTOM_SL -SamplerState LinearSampler : register(s0); -SamplerState PointSampler : register(s1); -#define SMAATexture2D(tex) Texture2D tex -#define SMAATexturePass2D(tex) tex -#define SMAASampleLevelZero(tex, coord) tex.SampleLevel(LinearSampler, coord, 0) -#define SMAASampleLevelZeroPoint(tex, coord) tex.SampleLevel(PointSampler, coord, 0) -#define SMAASampleLevelZeroOffset(tex, coord, offset) tex.SampleLevel(LinearSampler, coord, 0, offset) -#define SMAASample(tex, coord) tex.Sample(LinearSampler, coord) -#define SMAASamplePoint(tex, coord) tex.Sample(PointSampler, coord) -#define SMAASampleOffset(tex, coord, offset) tex.Sample(LinearSampler, coord, offset) -#define SMAA_FLATTEN [flatten] -#define SMAA_BRANCH [branch] -#define SMAATexture2DMS2(tex) Texture2DMS tex -#define SMAALoad(tex, pos, sample) tex.Load(pos, sample) -#define SMAAGather(tex, coord) tex.Gather(LinearSampler, coord, 0) -#include "../Includes/SMAA.hlsl" - -Texture2D tex0 : register(t0); -Texture2D tex1 : register(t1); -Texture2D tex2 : register(t2); - -void fullscreen_triangle(uint id, out float4 position, out float2 texcoord) -{ - texcoord = float2((id << 1) & 2, id & 2); - position = float4(texcoord * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); -} +#define SMAAGather(tex, coord) tex.Gather(LinearSampler, coord, 0) // SMAAEdgeDetection -void smaa_edge_detection_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset[3] : TEXCOORD1) -{ - fullscreen_triangle(id, position, texcoord); - SMAAEdgeDetectionVS(texcoord, offset); -} - -float2 smaa_edge_detection_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset[3] : TEXCOORD1) : SV_Target -{ - // tex0 = colorTexGamma (stored scene snapshot) - // tex1 = predicationTex (plane-deviation edge mask) - return SMAAColorEdgeDetectionPS(texcoord, offset, tex0, tex1); -} - -// SMAABlendingWeightCalculation -void smaa_blending_weight_calculation_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float2 pixcoord : TEXCOORD1, out float4 offset[3] : TEXCOORD2) -{ - fullscreen_triangle(id, position, texcoord); - SMAABlendingWeightCalculationVS(texcoord, pixcoord, offset); -} - -float4 smaa_blending_weight_calculation_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float2 pixcoord : TEXCOORD1, float4 offset[3] : TEXCOORD2) : SV_Target -{ - // tex0 = edgesTex, tex1 = areaTex, tex2 = searchTex - return SMAABlendingWeightCalculationPS(texcoord, pixcoord, offset, tex0, tex1, tex2, 0); -} +// tex0 = colorTexGamma (stored scene snapshot) +// tex1 = predicationTex (plane-deviation edge mask) // SMAANeighborhoodBlending -void smaa_neighborhood_blending_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset : TEXCOORD1) -{ - fullscreen_triangle(id, position, texcoord); - SMAANeighborhoodBlendingVS(texcoord, offset); -} +// tex0 = colorTex (linear copy), tex1 = blendTex. Re-encode to the canvas' gamma. +#define SMAA_NEIGHBORHOOD_OUTPUT(color, position) color.rgb = linear_to_gamma(color.rgb, GCT_MIRROR); -float4 smaa_neighborhood_blending_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset : TEXCOORD1) : SV_Target -{ - // tex0 = colorTex (linear copy), tex1 = blendTex. Re-encode to the canvas' gamma. - float4 color = SMAANeighborhoodBlendingPS(texcoord, offset, tex0, tex1); - color.rgb = linear_to_gamma(color.rgb, GCT_MIRROR); - return color; -} +#include "../Includes/SMAA_Passes.hlsl" diff --git a/Shaders/Mass Effect Andromeda/Luma_MEA_Sharpen.hlsl b/Shaders/Global/Luma_RCAS_PS.hlsl similarity index 65% rename from Shaders/Mass Effect Andromeda/Luma_MEA_Sharpen.hlsl rename to Shaders/Global/Luma_RCAS_PS.hlsl index 64d85d698..db66a27d9 100644 --- a/Shaders/Mass Effect Andromeda/Luma_MEA_Sharpen.hlsl +++ b/Shaders/Global/Luma_RCAS_PS.hlsl @@ -1,4 +1,5 @@ -// RCAS sharpening for the SMAA output, for Mass Effect: Andromeda (in-game FXAA mode). +// RCAS sharpening for the SMAA output, run after the neighborhood blend. paperWhite = 1.0, the sharpness slider is +// the knob, and RCAS_LIMIT bounds the lobe so bright pixels don't over-sharpen. #include "../Includes/RCAS.hlsl" @@ -7,7 +8,7 @@ cbuffer SharpenCB : register(b0) float4 SharpenParams; // (width, height, sharpness[0..1], unused) } -Texture2D tex0 : register(t0); // SMAA output (display-encoded color) +Texture2D tex0 : register(t0); // SMAA output Texture2D dummyMV : register(t1); // unused (dynamicSharpening = false) float4 sharpen_ps(float4 pos : SV_Position) : SV_Target diff --git a/Shaders/Includes/DgVoodoo.hlsl b/Shaders/Includes/DgVoodoo.hlsl new file mode 100644 index 000000000..db03c0c75 --- /dev/null +++ b/Shaders/Includes/DgVoodoo.hlsl @@ -0,0 +1,28 @@ +#ifndef SRC_DGVOODOO_HLSL +#define SRC_DGVOODOO_HLSL + +float4 ApplyDgvMask(float4 value, float4 mask, float4 fill) +{ + return asfloat((asuint(value) & asuint(mask)) | asuint(fill)); +} + +// 1e37 is the exact sentinel every dgVoodoo dump uses (l(9999999933815812510711506376257961984.0)); it must not be +// rounded up to 1e38: it is multiplied downstream, and 1e38 overflows to inf ten times sooner, turning a zero-weight +// lerp around it into 0 * inf = NaN. +#define DGVOODOO_BIG 1e37 + +// dgVoodoo's guarded reciprocal: rcp with the huge-constant fallback at exactly 0 (movc in the translated CSO). +float DgVoodooRcp(float x) +{ + return (abs(x) > 0.0) ? (1.0 / x) : DGVOODOO_BIG; +} + +// dgVoodoo's guarded log: log(0) = -inf -> -BIG (so exp2 later yields 0). It tests the -inf BIT PATTERN, not isinf(), +// so a +inf input keeps propagating as vanilla does instead of being flipped to ~0 (a white pixel gone black). +float DgVoodooLog2(float x) +{ + float l = log2(abs(x)); + return (asuint(l) == 0xff800000u) ? -DGVOODOO_BIG : l; +} + +#endif // SRC_DGVOODOO_HLSL diff --git a/Shaders/Borderlands GOTY Enhanced/Includes/MacLeodBoynton.hlsl b/Shaders/Includes/MacLeodBoynton.hlsl similarity index 100% rename from Shaders/Borderlands GOTY Enhanced/Includes/MacLeodBoynton.hlsl rename to Shaders/Includes/MacLeodBoynton.hlsl diff --git a/Shaders/Includes/SMAA_Passes.hlsl b/Shaders/Includes/SMAA_Passes.hlsl new file mode 100644 index 000000000..4eacb82bc --- /dev/null +++ b/Shaders/Includes/SMAA_Passes.hlsl @@ -0,0 +1,84 @@ +// The SMAA passes Core draws for the games with a "Luma_SMAA_impl.hlsl" (see "SMAA Edge Detection VS" etc in core.hpp). +// The game file sets up SMAA (metrics, preset, predication, "SMAAGather" if needed), then includes this instead of +// "SMAA.hlsl". Define "SMAA_NEIGHBORHOOD_OUTPUT(color, position)" to change the blended color before it's written, +// e.g. to go back to gamma space. + +#ifndef SMAA_CUSTOM_SL +#define SMAA_CUSTOM_SL +SamplerState LinearSampler : register(s0); +SamplerState PointSampler : register(s1); +#define SMAATexture2D(tex) Texture2D tex +#define SMAATexturePass2D(tex) tex +#define SMAASampleLevelZero(tex, coord) tex.SampleLevel(LinearSampler, coord, 0) +#define SMAASampleLevelZeroPoint(tex, coord) tex.SampleLevel(PointSampler, coord, 0) +#define SMAASampleLevelZeroOffset(tex, coord, offset) tex.SampleLevel(LinearSampler, coord, 0, offset) +#define SMAASample(tex, coord) tex.Sample(LinearSampler, coord) +#define SMAASamplePoint(tex, coord) tex.Sample(PointSampler, coord) +#define SMAASampleOffset(tex, coord, offset) tex.Sample(LinearSampler, coord, offset) +#define SMAA_FLATTEN [flatten] +#define SMAA_BRANCH [branch] +#define SMAATexture2DMS2(tex) Texture2DMS tex +#define SMAALoad(tex, pos, sample) tex.Load(pos, sample) +#endif + +#include "SMAA.hlsl" + +#ifndef SMAA_NEIGHBORHOOD_OUTPUT +#define SMAA_NEIGHBORHOOD_OUTPUT(color, position) +#endif + +Texture2D tex0 : register(t0); +Texture2D tex1 : register(t1); +Texture2D tex2 : register(t2); + +void fullscreen_triangle(uint id, out float4 position, out float2 texcoord) +{ + texcoord = float2((id << 1) & 2, id & 2); + position = float4(texcoord * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); +} + +// SMAAEdgeDetection +void smaa_edge_detection_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset[3] : TEXCOORD1) +{ + fullscreen_triangle(id, position, texcoord); + SMAAEdgeDetectionVS(texcoord, offset); +} + +float2 smaa_edge_detection_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset[3] : TEXCOORD1) : SV_Target +{ + // tex0 = colorTexGamma, tex1 = predicationTex + return SMAAColorEdgeDetectionPS(texcoord, offset, tex0 +#if SMAA_PREDICATION + , + tex1 +#endif + ); +} + +// SMAABlendingWeightCalculation +void smaa_blending_weight_calculation_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float2 pixcoord : TEXCOORD1, out float4 offset[3] : TEXCOORD2) +{ + fullscreen_triangle(id, position, texcoord); + SMAABlendingWeightCalculationVS(texcoord, pixcoord, offset); +} + +float4 smaa_blending_weight_calculation_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float2 pixcoord : TEXCOORD1, float4 offset[3] : TEXCOORD2) : SV_Target +{ + // tex0 = edgesTex, tex1 = areaTex, tex2 = searchTex + return SMAABlendingWeightCalculationPS(texcoord, pixcoord, offset, tex0, tex1, tex2, 0); +} + +// SMAANeighborhoodBlending +void smaa_neighborhood_blending_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset : TEXCOORD1) +{ + fullscreen_triangle(id, position, texcoord); + SMAANeighborhoodBlendingVS(texcoord, offset); +} + +float4 smaa_neighborhood_blending_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset : TEXCOORD1) : SV_Target +{ + // tex0 = colorTex, tex1 = blendTex + float4 color = SMAANeighborhoodBlendingPS(texcoord, offset, tex0, tex1); + SMAA_NEIGHBORHOOD_OUTPUT(color, position) + return color; +} diff --git a/Shaders/Includes/XeGTAO.hlsl b/Shaders/Includes/XeGTAO.hlsl new file mode 100644 index 000000000..94be5f458 --- /dev/null +++ b/Shaders/Includes/XeGTAO.hlsl @@ -0,0 +1,740 @@ +// XeGTAO (https://github.com/GameTechDev/XeGTAO), shared by the games that replace their native AO with it. +// Each game has its own "Luma__XeGTAO.hlsl", which declares the game's cbuffers and textures, +// defines the stuff below and then includes this. +// +// Every game needs to define: +// "XeGTAO_ScreenSpaceToViewSpaceDepth()", which turns the game's depth buffer values into view space depth +// "VIEWPORT_PIXEL_SIZE", "NDC_TO_VIEW_MUL" and "NDC_TO_VIEW_ADD" (Intel's NDC to view space pair, on the raw uv) +// "FinalValuePowerRT", "RadiusOverrideRT", "DebugViewRT" and "NoiseIndexRT" (define them as 0 if the game has none) +// "XE_GTAO_FINAL_OUTPUT_TYPE" and "XE_GTAO_ENCODE_FINAL()", to match the format of the game's AO texture +// "XeGTAO_LoadViewspaceNormal()", unless "XE_GTAO_GENERATE_NORMALS" is on, in which case we generate normals from depth +// +// The other "XE_GTAO_*" defines below are optional, and do nothing if left undefined. + +#ifndef LUMA_XEGTAO_HLSL +#define LUMA_XEGTAO_HLSL + +#if XE_GTAO_QUALITY == 0 // Low +#define SLICE_COUNT 4.0 +#elif XE_GTAO_QUALITY == 1 // Medium +#define SLICE_COUNT 7.0 +#elif XE_GTAO_QUALITY == 2 // High +#define SLICE_COUNT 10.0 +#elif XE_GTAO_QUALITY == 3 // Very High +#define SLICE_COUNT 13.0 +#elif XE_GTAO_QUALITY == 4 // Ultra +#define SLICE_COUNT 16.0 +#endif + +// User configurable +// + +#ifndef EFFECT_RADIUS +#define EFFECT_RADIUS 0.5 // Default 0.5 +#endif + +#ifndef RADIUS_MULTIPLIER +#define RADIUS_MULTIPLIER 1.457 // Default 1.457 +#endif + +#ifndef EFFECT_FALLOFF_RANGE +#define EFFECT_FALLOFF_RANGE 0.615 // Default 0.615 +#endif + +#ifndef SAMPLE_DISTRIBUTION_POWER +#define SAMPLE_DISTRIBUTION_POWER 2.0 // Default 2.0 +#endif + +#ifndef THIN_OCCLUDER_COMPENSATION +#define THIN_OCCLUDER_COMPENSATION 0.0 // Default 0.0; > 0 causes more mistakes than it fixes on big geometry +#endif + +#ifndef DEPTH_MIP_SAMPLING_OFFSET +#define DEPTH_MIP_SAMPLING_OFFSET 3.3 // Default 3.3 +#endif + +#ifndef SLICE_COUNT +#define SLICE_COUNT 3.0 // Default 3.0 +#endif + +#ifndef STEPS_PER_SLICE +#define STEPS_PER_SLICE 3.0 // Default 3.0 +#endif + +#ifndef DENOISE_BLUR_BETA +#define DENOISE_BLUR_BETA 1.2 // Default 1.2 +#endif + +// + +#define NDC_TO_VIEW_MUL_X_PIXEL_SIZE (NDC_TO_VIEW_MUL * VIEWPORT_PIXEL_SIZE) + +#define XE_GTAO_DEPTH_MIP_LEVELS 5.0 +#define XE_GTAO_OCCLUSION_TERM_SCALE 1.5 + +#define XE_GTAO_PI 3.1415926535897932384626433832795 +#define XE_GTAO_PI_HALF 1.5707963267948966192313216916398 + +#ifndef XE_GTAO_DEPTH_LOAD_COORD +#ifdef XE_GTAO_DEPTH_LOAD_SCALE +#define XE_GTAO_DEPTH_LOAD_COORD(pixCoord) ((pixCoord) * (XE_GTAO_DEPTH_LOAD_SCALE)) +#else +#define XE_GTAO_DEPTH_LOAD_COORD(pixCoord) (pixCoord) +#endif +#endif + +#if XE_GTAO_PIXEL_SIZE_FROM_SOURCE +static float2 XeGTAO_SourcePixelSize; // 1 / tex0's size, set first by each entry point that reads it +#define VIEWPORT_PIXEL_SIZE XeGTAO_SourcePixelSize +#define XE_GTAO_CS_PROLOGUE \ + float2 size; \ + tex0.GetDimensions(size.x, size.y); \ + XeGTAO_SourcePixelSize = rcp(size); +#else +#define XE_GTAO_CS_PROLOGUE +#endif + +#ifndef XE_GTAO_ADJUST_SCREENSPACE_RADIUS +#define XE_GTAO_ADJUST_SCREENSPACE_RADIUS(screenspaceRadius) +#endif + +#ifndef XE_GTAO_ADJUST_VISIBILITY +#define XE_GTAO_ADJUST_VISIBILITY(visibility, viewspaceZ) +#endif + +#ifndef XE_GTAO_FINAL_VALUE +#define XE_GTAO_FINAL_VALUE(v) (v) +#endif + +#if XE_GTAO_FINAL_APPLY +#define XE_GTAO_DENOISE_OUTPUT_TYPE XE_GTAO_FINAL_OUTPUT_TYPE +#else +#define XE_GTAO_DENOISE_OUTPUT_TYPE unorm float2 +#endif + +// This is also a good place to do non-linear depth conversion for cases where one wants the 'radius' (effectively the threshold between near-field and far-field GI), +// is required to be non-linear (i.e. very large outdoors environments). +float XeGTAO_ClampDepth(float depth) +{ + return clamp(depth, 0.0, 3.402823466e+38); +} + +float XeGTAO_EffectRadius() +{ + return (RadiusOverrideRT > 0.0 ? RadiusOverrideRT : EFFECT_RADIUS) * RADIUS_MULTIPLIER; +} + +#ifndef XE_GTAO_EFFECT_RADIUS +#define XE_GTAO_EFFECT_RADIUS(viewspaceZ) XeGTAO_EffectRadius() +#endif + +#ifndef XE_GTAO_MAIN_PASS_EFFECT_RADIUS +#define XE_GTAO_MAIN_PASS_EFFECT_RADIUS(viewspaceZ) XE_GTAO_EFFECT_RADIUS(viewspaceZ) +#endif + +// weighted average depth filter +float XeGTAO_DepthMIPFilter(float depth0, float depth1, float depth2, float depth3) +{ + float maxDepth = max(max(depth0, depth1), max(depth2, depth3)); + + const float depthRangeScaleFactor = 0.75; // found empirically :) + const float effectRadius = depthRangeScaleFactor * XE_GTAO_EFFECT_RADIUS(maxDepth); + const float falloffRange = EFFECT_FALLOFF_RANGE * effectRadius; + const float falloffFrom = effectRadius * (1.0 - EFFECT_FALLOFF_RANGE); + + // fadeout precompute optimisation + const float falloffMul = -1.0 / falloffRange; + const float falloffAdd = falloffFrom / falloffRange + 1.0; + + float weight0 = saturate((maxDepth - depth0) * falloffMul + falloffAdd); + float weight1 = saturate((maxDepth - depth1) * falloffMul + falloffAdd); + float weight2 = saturate((maxDepth - depth2) * falloffMul + falloffAdd); + float weight3 = saturate((maxDepth - depth3) * falloffMul + falloffAdd); + + float weightSum = weight0 + weight1 + weight2 + weight3; + return (weight0 * depth0 + weight1 * depth1 + weight2 * depth2 + weight3 * depth3) * rcp(weightSum); +} + +groupshared float g_scratchDepths[8][8]; +void XeGTAO_PrefilterDepths16x16(uint2 dispatchThreadID, uint2 groupThreadID, Texture2D sourceNDCDepth, RWTexture2D outDepth0, RWTexture2D outDepth1, RWTexture2D outDepth2, RWTexture2D outDepth3, RWTexture2D outDepth4) +{ + // MIP 0 + const uint2 baseCoord = dispatchThreadID; + const uint2 pixCoord = baseCoord * 2; + // Explicit integer Loads of the 2x2, not GatherRed: GatherRed on an r24_unorm_x8 depth view returns 0 on + // some drivers (flat depth -> visibility 1 -> AO silently gone), so do not switch this back to Gather. + // Out-of-bounds Loads return 0 (D3D11-defined), clamped depth. + float d00 = sourceNDCDepth.Load(int3(XE_GTAO_DEPTH_LOAD_COORD(pixCoord + uint2(0, 0)), 0)).x; + float d10 = sourceNDCDepth.Load(int3(XE_GTAO_DEPTH_LOAD_COORD(pixCoord + uint2(1, 0)), 0)).x; + float d01 = sourceNDCDepth.Load(int3(XE_GTAO_DEPTH_LOAD_COORD(pixCoord + uint2(0, 1)), 0)).x; + float d11 = sourceNDCDepth.Load(int3(XE_GTAO_DEPTH_LOAD_COORD(pixCoord + uint2(1, 1)), 0)).x; + float depth0 = XeGTAO_ClampDepth(XeGTAO_ScreenSpaceToViewSpaceDepth(d00)); + float depth1 = XeGTAO_ClampDepth(XeGTAO_ScreenSpaceToViewSpaceDepth(d10)); + float depth2 = XeGTAO_ClampDepth(XeGTAO_ScreenSpaceToViewSpaceDepth(d01)); + float depth3 = XeGTAO_ClampDepth(XeGTAO_ScreenSpaceToViewSpaceDepth(d11)); + outDepth0[pixCoord + uint2(0, 0)] = depth0; + outDepth0[pixCoord + uint2(1, 0)] = depth1; + outDepth0[pixCoord + uint2(0, 1)] = depth2; + outDepth0[pixCoord + uint2(1, 1)] = depth3; + + // MIP 1 + float dm1 = XeGTAO_DepthMIPFilter(depth0, depth1, depth2, depth3); + outDepth1[baseCoord] = dm1; + g_scratchDepths[groupThreadID.x][groupThreadID.y] = dm1; + + GroupMemoryBarrierWithGroupSync(); + + // MIP 2 + [branch] if (all((groupThreadID.xy % 2) == 0)) + { + float inTL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 0]; + float inTR = g_scratchDepths[groupThreadID.x + 1][groupThreadID.y + 0]; + float inBL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 1]; + float inBR = g_scratchDepths[groupThreadID.x + 1][groupThreadID.y + 1]; + + float dm2 = XeGTAO_DepthMIPFilter(inTL, inTR, inBL, inBR); + outDepth2[baseCoord / 2] = dm2; + g_scratchDepths[groupThreadID.x][groupThreadID.y] = dm2; + } + + GroupMemoryBarrierWithGroupSync(); + + // MIP 3 + [branch] if (all((groupThreadID.xy % 4) == 0)) + { + float inTL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 0]; + float inTR = g_scratchDepths[groupThreadID.x + 2][groupThreadID.y + 0]; + float inBL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 2]; + float inBR = g_scratchDepths[groupThreadID.x + 2][groupThreadID.y + 2]; + + float dm3 = XeGTAO_DepthMIPFilter(inTL, inTR, inBL, inBR); + outDepth3[baseCoord / 4] = dm3; + g_scratchDepths[groupThreadID.x][groupThreadID.y] = dm3; + } + + GroupMemoryBarrierWithGroupSync(); + + // MIP 4 + [branch] if (all((groupThreadID.xy % 8) == 0)) + { + float inTL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 0]; + float inTR = g_scratchDepths[groupThreadID.x + 4][groupThreadID.y + 0]; + float inBL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 4]; + float inBR = g_scratchDepths[groupThreadID.x + 4][groupThreadID.y + 4]; + + float dm4 = XeGTAO_DepthMIPFilter(inTL, inTR, inBL, inBR); + outDepth4[baseCoord / 8] = dm4; + // g_scratchDepths[ groupThreadID.x ][ groupThreadID.y ] = dm4; + } +} + +float4 XeGTAO_CalculateEdges(float centerZ, float leftZ, float rightZ, float topZ, float bottomZ) +{ + float4 edgesLRTB = float4(leftZ, rightZ, topZ, bottomZ) - centerZ; + + float slopeLR = (edgesLRTB.y - edgesLRTB.x) * 0.5; + float slopeTB = (edgesLRTB.w - edgesLRTB.z) * 0.5; + float4 edgesLRTBSlopeAdjusted = edgesLRTB + float4(slopeLR, -slopeLR, slopeTB, -slopeTB); + edgesLRTB = min(abs(edgesLRTB), abs(edgesLRTBSlopeAdjusted)); + return saturate(1.25 - edgesLRTB * rcp(centerZ * 0.011)); +} + +// packing/unpacking for edges; 2 bits per edge mean 4 gradient values (0, 0.33, 0.66, 1) for smoother transitions! +float XeGTAO_PackEdges(float4 edgesLRTB) +{ + edgesLRTB = round(saturate(edgesLRTB) * 2.9); + return dot(edgesLRTB, float4(64.0 / 255.0, 16.0 / 255.0, 4.0 / 255.0, 1.0 / 255.0)); +} + +// Inputs are screen XY and viewspace depth, output is viewspace position +float3 XeGTAO_ComputeViewspacePosition(float2 screenPos, float viewspaceDepth) +{ + float3 ret; + ret.xy = (NDC_TO_VIEW_MUL * screenPos.xy + NDC_TO_VIEW_ADD) * viewspaceDepth; + ret.z = viewspaceDepth; + return ret; +} + +#if XE_GTAO_GENERATE_NORMALS +// Depth-derived view-space normal (from the Intel reference; same convention as ComputeViewspacePosition, +// so the space is self-consistent with the positions the main loop uses). +float3 XeGTAO_CalculateNormal(const float4 edgesLRTB, const float3 pixCenterPos, float3 pixLPos, float3 pixRPos, float3 pixTPos, float3 pixBPos) +{ + float4 acceptedNormals = saturate(float4(edgesLRTB.x * edgesLRTB.z, edgesLRTB.z * edgesLRTB.y, edgesLRTB.y * edgesLRTB.w, edgesLRTB.w * edgesLRTB.x) + 0.01); + + pixLPos = normalize(pixLPos - pixCenterPos); + pixRPos = normalize(pixRPos - pixCenterPos); + pixTPos = normalize(pixTPos - pixCenterPos); + pixBPos = normalize(pixBPos - pixCenterPos); + + float3 pixelNormal = acceptedNormals.x * cross(pixLPos, pixTPos) + acceptedNormals.y * cross(pixTPos, pixRPos) + acceptedNormals.z * cross(pixRPos, pixBPos) + acceptedNormals.w * cross(pixBPos, pixLPos); + return normalize(pixelNormal); +} +#endif + +// http://h14s.p5r.org/2012/09/0x5f3759df.html, [Drobot2014a] Low Level Optimizations for GCN, https://blog.selfshadow.com/publications/s2016-shading-course/activision/s2016_pbs_activision_occlusion.pdf slide 63 +float XeGTAO_FastSqrt(float x) +{ + return asfloat(0x1fbd1df5 + (asint(x) >> 1)); +} + +// input [-1, 1] and output [0, PI], from https://seblagarde.wordpress.com/2014/12/01/inverse-trigonometric-functions-gpu-optimization-for-amd-gcn-architecture/ +float XeGTAO_FastACos(float inX) +{ + float x = abs(inX); + float res = -0.156583 * x + 1.570796; + res *= XeGTAO_FastSqrt(1.0 - x); + return inX >= 0 ? res : 3.141593 - res; +} + +void XeGTAO_MainPass(uint2 pixCoord, float2 localNoise, float3 viewspaceNormal, Texture2D sourceViewspaceDepth, SamplerState depthSampler, RWTexture2D outWorkingAOTermAndEdges) +{ + float2 normalizedScreenPos = (pixCoord + 0.5) * VIEWPORT_PIXEL_SIZE; + + // Center + cross depths from the prefiltered (already linearized + scaled) mip0. This texture is our + // own R32F, Gather is safe here (the r24 quirk is only on the game's depth view). + float4 valuesUL = sourceViewspaceDepth.GatherRed(depthSampler, float2(pixCoord * VIEWPORT_PIXEL_SIZE)); + float4 valuesBR = sourceViewspaceDepth.GatherRed(depthSampler, float2(pixCoord * VIEWPORT_PIXEL_SIZE), int2(1, 1)); + + // viewspace Z at the center + float viewspaceZ = valuesUL.y; + + // viewspace Zs left top right bottom + const float pixLZ = valuesUL.x; + const float pixTZ = valuesUL.z; + const float pixRZ = valuesBR.z; + const float pixBZ = valuesBR.x; + + float4 edgesLRTB = XeGTAO_CalculateEdges(viewspaceZ, pixLZ, pixRZ, pixTZ, pixBZ); + const float edges = XeGTAO_PackEdges(edgesLRTB); + +#if DEVELOPMENT + // Debug views (visible on screen through the game's own AO apply blit; the final denoise passes raw + // values through when DebugViewRT > 0). + if (DebugViewRT > 0.5 && DebugViewRT < 1.5) // 1 = depth gradient (proves live depth + linearization/scale) + { + outWorkingAOTermAndEdges[pixCoord] = float2(saturate(frac(log2(max(viewspaceZ, 1e-6)))), 1.0); + return; + } + if (DebugViewRT >= 3.5 && DebugViewRT < 4.5) // 4 = edges + { + outWorkingAOTermAndEdges[pixCoord] = float2(dot(edgesLRTB, 0.25), 1.0); + return; + } +#endif + +#if XE_GTAO_GENERATE_NORMALS + { + float3 CENTER = XeGTAO_ComputeViewspacePosition(normalizedScreenPos, viewspaceZ); + float3 LEFT = XeGTAO_ComputeViewspacePosition(normalizedScreenPos + float2(-1.0, 0.0) * VIEWPORT_PIXEL_SIZE, pixLZ); + float3 RIGHT = XeGTAO_ComputeViewspacePosition(normalizedScreenPos + float2(1.0, 0.0) * VIEWPORT_PIXEL_SIZE, pixRZ); + float3 TOP = XeGTAO_ComputeViewspacePosition(normalizedScreenPos + float2(0.0, -1.0) * VIEWPORT_PIXEL_SIZE, pixTZ); + float3 BOTTOM = XeGTAO_ComputeViewspacePosition(normalizedScreenPos + float2(0.0, 1.0) * VIEWPORT_PIXEL_SIZE, pixBZ); + viewspaceNormal = XeGTAO_CalculateNormal(edgesLRTB, CENTER, LEFT, RIGHT, TOP, BOTTOM); + } +#endif + + // Move center pixel slightly towards camera to avoid imprecision artifacts due to depth buffer imprecision; offset depends on depth texture format used + viewspaceZ *= 0.99999; // this is good for FP32 depth buffer + + const float3 pixCenterPos = XeGTAO_ComputeViewspacePosition(normalizedScreenPos, viewspaceZ); + const float3 viewVec = normalize(-pixCenterPos); + + // prevents normals that are facing away from the view vector - xeGTAO struggles with extreme cases. Upstream + // ships this line commented out as rare in vanilla; this port runs it. + viewspaceNormal = normalize(viewspaceNormal + max(0, -dot(viewspaceNormal, viewVec)) * viewVec); + +#if DEVELOPMENT + if (DebugViewRT >= 1.5 && DebugViewRT < 2.5) // 2 = normals view-facing term (smooth per-surface shading = correct decode) + { + outWorkingAOTermAndEdges[pixCoord] = float2(saturate(abs(dot(viewspaceNormal, viewVec))), 1.0); + return; + } +#endif + + const float effectRadius = XE_GTAO_MAIN_PASS_EFFECT_RADIUS(viewspaceZ); + const float sampleDistributionPower = SAMPLE_DISTRIBUTION_POWER; + const float thinOccluderCompensation = THIN_OCCLUDER_COMPENSATION; + const float falloffRange = EFFECT_FALLOFF_RANGE * effectRadius; + const float falloffFrom = effectRadius * (1.0 - EFFECT_FALLOFF_RANGE); + + // fadeout precompute optimisation + const float falloffMul = -1.0 / falloffRange; + const float falloffAdd = falloffFrom / falloffRange + 1.0; + + float visibility = 0.0; + + // see "Algorithm 1" in https://www.activision.com/cdn/research/Practical_Real_Time_Strategies_for_Accurate_Indirect_Occlusion_NEW%20VERSION_COLOR.pdf + { + const float noiseSlice = localNoise.x; + const float noiseSample = localNoise.y; + + // quality settings / tweaks / hacks + const float pixelTooCloseThreshold = 1.3; // if the offset is under approx pixel size (pixelTooCloseThreshold), push it out to the minimum distance + + // approx viewspace pixel size at pixCoord; approximation of NDCToViewspace( normalizedScreenPos.xy + consts.ViewportPixelSize.xy, pixCenterPos.z ).xy - pixCenterPos.xy; + const float2 pixelDirRBViewspaceSizeAtCenterZ = viewspaceZ.xx * NDC_TO_VIEW_MUL_X_PIXEL_SIZE; + + float screenspaceRadius = effectRadius * rcp(abs(pixelDirRBViewspaceSizeAtCenterZ.x)); + XE_GTAO_ADJUST_SCREENSPACE_RADIUS(screenspaceRadius) + + // fade out for small screen radii + visibility += saturate((10.0 - screenspaceRadius) / 100.0) * 0.5; + + // this is the min distance to start sampling from to avoid sampling from the center pixel (no useful data obtained from sampling center pixel) + const float minS = pixelTooCloseThreshold * rcp(screenspaceRadius); + + //[unroll] + for (float slice = 0.0; slice < SLICE_COUNT; slice++) + { + float sliceK = (slice + noiseSlice) / SLICE_COUNT; + // lines 5, 6 from the paper + float phi = sliceK * XE_GTAO_PI; + float cosPhi = cos(phi); + float sinPhi = sin(phi); + float2 omega = float2(cosPhi, -sinPhi); // lpfloat2 on omega causes issues with big radii + + // convert to screen units (pixels) for later use + omega *= screenspaceRadius; + + // line 8 from the paper + const float3 directionVec = float3(cosPhi, sinPhi, 0.0); + + // line 9 from the paper + const float3 orthoDirectionVec = directionVec - (dot(directionVec, viewVec) * viewVec); + + // line 10 from the paper + // axisVec is orthogonal to directionVec and viewVec, used to define projectedNormal + const float3 axisVec = normalize(cross(orthoDirectionVec, viewVec)); + + // line 11 from the paper + float3 projectedNormalVec = viewspaceNormal - axisVec * dot(viewspaceNormal, axisVec); + + // line 13 from the paper + float signNorm = sign(dot(orthoDirectionVec, projectedNormalVec)); + + // line 14 from the paper + float projectedNormalVecLength = length(projectedNormalVec); + float cosNorm = saturate(dot(projectedNormalVec, viewVec) * rcp(projectedNormalVecLength)); + + // line 15 from the paper + float n = signNorm * XeGTAO_FastACos(cosNorm); + + // this is a lower weight target; not using -1 as in the original paper because it is under horizon, so a 'weight' has different meaning based on the normal + const float lowHorizonCos0 = cos(n + XE_GTAO_PI_HALF); + const float lowHorizonCos1 = cos(n - XE_GTAO_PI_HALF); + + // lines 17, 18 from the paper, manually unrolled the 'side' loop + float horizonCos0 = lowHorizonCos0; //-1; + float horizonCos1 = lowHorizonCos1; //-1; + + [unroll] for (float step = 0.0; step < STEPS_PER_SLICE; step++) + { + // R1 sequence (http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/) + const float stepBaseNoise = (slice + step * STEPS_PER_SLICE) * 0.6180339887498948482; // <- this should unroll + float stepNoise = frac(noiseSample + stepBaseNoise); + + // approx line 20 from the paper, with added noise + float s = (step + stepNoise) / STEPS_PER_SLICE; // + (lpfloat2)1e-6f); + + // additional distribution modifier + s = pow(s, sampleDistributionPower); + + // avoid sampling center pixel + s += minS; + + // approx lines 21-22 from the paper, unrolled + float2 sampleOffset = s * omega; + + float sampleOffsetLength = length(sampleOffset); + + // note: when sampling, using point_point_point or point_point_linear sampler works, but linear_linear_linear will cause unwanted interpolation between neighbouring depth values on the same MIP level! + const float mipLevel = clamp(log2(sampleOffsetLength) - DEPTH_MIP_SAMPLING_OFFSET, 0.0, XE_GTAO_DEPTH_MIP_LEVELS); + + // Snap to pixel center (more correct direction math, avoids artifacts due to sampling pos not matching depth texel center - messes up slope - but adds other + // artifacts due to them being pushed off the slice). Also use full precision for high res cases. + sampleOffset = round(sampleOffset) * VIEWPORT_PIXEL_SIZE; + + float2 sampleScreenPos0 = normalizedScreenPos + sampleOffset; + float SZ0 = sourceViewspaceDepth.SampleLevel(depthSampler, sampleScreenPos0, mipLevel).x; + float3 samplePos0 = XeGTAO_ComputeViewspacePosition(sampleScreenPos0, SZ0); + + float2 sampleScreenPos1 = normalizedScreenPos - sampleOffset; + float SZ1 = sourceViewspaceDepth.SampleLevel(depthSampler, sampleScreenPos1, mipLevel).x; + float3 samplePos1 = XeGTAO_ComputeViewspacePosition(sampleScreenPos1, SZ1); + + float3 sampleDelta0 = samplePos0 - pixCenterPos; // using lpfloat for sampleDelta causes precision issues + float3 sampleDelta1 = samplePos1 - pixCenterPos; // using lpfloat for sampleDelta causes precision issues + float sampleDist0 = length(sampleDelta0); + float sampleDist1 = length(sampleDelta1); + + // approx lines 23, 24 from the paper, unrolled + float3 sampleHorizonVec0 = sampleDelta0 * rcp(sampleDist0); + float3 sampleHorizonVec1 = sampleDelta1 * rcp(sampleDist1); + + // any sample out of radius should be discarded - also use fallof range for smooth transitions; this is a modified idea from "4.3 Implementation details, Bounding the sampling area" + // this is our own thickness heuristic that relies on sooner discarding samples behind the center + float falloffBase0 = length(float3(sampleDelta0.x, sampleDelta0.y, sampleDelta0.z * (1.0 + thinOccluderCompensation))); + float falloffBase1 = length(float3(sampleDelta1.x, sampleDelta1.y, sampleDelta1.z * (1.0 + thinOccluderCompensation))); + float weight0 = saturate(falloffBase0 * falloffMul + falloffAdd); + float weight1 = saturate(falloffBase1 * falloffMul + falloffAdd); + + // sample horizon cos + float shc0 = dot(sampleHorizonVec0, viewVec); + float shc1 = dot(sampleHorizonVec1, viewVec); + + // discard unwanted samples + shc0 = lerp(lowHorizonCos0, shc0, weight0); // this would be more correct but too expensive: cos(lerp( acos(lowHorizonCos0), acos(shc0), weight0 )); + shc1 = lerp(lowHorizonCos1, shc1, weight1); // this would be more correct but too expensive: cos(lerp( acos(lowHorizonCos1), acos(shc1), weight1 )); + + // thickness heuristic disabled (THIN_OCCLUDER_COMPENSATION == 0) + horizonCos0 = max(horizonCos0, shc0); + horizonCos1 = max(horizonCos1, shc1); + } + +#if 1 // I can't figure out the slight overdarkening on high slopes, so I'm adding this fudge - in the training set, 0.05 is close (PSNR 21.34) to disabled (PSNR 21.45) + projectedNormalVecLength = lerp(projectedNormalVecLength, 1.0, 0.05); +#endif + + // line ~27, unrolled + float h0 = -XeGTAO_FastACos(horizonCos1); + float h1 = XeGTAO_FastACos(horizonCos0); + float iarc0 = (cosNorm + 2.0 * h0 * sin(n) - cos(2.0 * h0 - n)) / 4.0; + float iarc1 = (cosNorm + 2.0 * h1 * sin(n) - cos(2.0 * h1 - n)) / 4.0; + float localVisibility = projectedNormalVecLength * (iarc0 + iarc1); + visibility += localVisibility; + } + visibility /= SLICE_COUNT; + visibility = pow(max(0.0, visibility), max(0.05, FinalValuePowerRT)); // runtime dial (calibrated to the vanilla AO histogram); max(0,) also satisfies fxc strict X3571 + XE_GTAO_ADJUST_VISIBILITY(visibility, viewspaceZ) + } + + visibility = saturate(visibility / XE_GTAO_OCCLUSION_TERM_SCALE); + outWorkingAOTermAndEdges[pixCoord] = float2(visibility, edges); +} + +void XeGTAO_DecodeGatherPartial(float4 packedValue, out float outDecoded[4]) +{ + for (int i = 0; i < 4; i++) + { + outDecoded[i] = packedValue[i]; + } +} + +float4 XeGTAO_UnpackEdges(float _packedVal) +{ + uint packedVal = uint(_packedVal * 255.5); + float4 edgesLRTB; + edgesLRTB.x = float((packedVal >> 6) & 0x03) / 3.0; // there's really no need for mask (as it's an 8 bit input) but I'll leave it in so it doesn't cause any trouble in the future + edgesLRTB.y = float((packedVal >> 4) & 0x03) / 3.0; + edgesLRTB.z = float((packedVal >> 2) & 0x03) / 3.0; + edgesLRTB.w = float((packedVal >> 0) & 0x03) / 3.0; + + return saturate(edgesLRTB); +} + +void XeGTAO_AddSample(float ssaoValue, float edgeValue, inout float sum, inout float sumWeight) +{ + float weight = edgeValue; + + sum += weight * ssaoValue; + sumWeight += weight; +} + +void XeGTAO_Denoise(uint2 pixCoordBase, Texture2D sourceAOTermAndEdges, SamplerState texSampler, RWTexture2D outputTexture) +{ +#if DEVELOPMENT + // Debug views: pass the raw working value through unblurred so the on-screen viz is exact. + if (DebugViewRT > 0.5) + { + for (int dside = 0; dside < 2; dside++) + { + const uint2 dpix = uint2(pixCoordBase.x + dside, pixCoordBase.y); + float v = sourceAOTermAndEdges.Load(int3(dpix, 0)).x; +#if XE_GTAO_FINAL_APPLY + if (DebugViewRT >= 2.5 && DebugViewRT < 3.5) // 3 = AO x8 amplification (spot broad over-occlusion) + v = saturate(1.0 - (1.0 - v * XE_GTAO_OCCLUSION_TERM_SCALE) * 8.0); + outputTexture[dpix] = XE_GTAO_ENCODE_FINAL(v); +#else + outputTexture[dpix] = float2(v, sourceAOTermAndEdges.Load(int3(dpix, 0)).y); +#endif + } + return; + } +#endif + +#if XE_GTAO_FINAL_APPLY + const float blurAmount = DENOISE_BLUR_BETA; +#else + const float blurAmount = DENOISE_BLUR_BETA / 5.0; +#endif + + const float diagWeight = 0.85 * 0.5; + + float aoTerm[2]; // pixel pixCoordBase and pixel pixCoordBase + int2( 1, 0 ) + float4 edgesC_LRTB[2]; + float weightTL[2]; + float weightTR[2]; + float weightBL[2]; + float weightBR[2]; + + // gather edge and visibility quads, used later + const float2 gatherCenter = float2(pixCoordBase.x, pixCoordBase.y) * VIEWPORT_PIXEL_SIZE; + float4 edgesQ0 = sourceAOTermAndEdges.GatherGreen(texSampler, gatherCenter, int2(0, 0)); + float4 edgesQ1 = sourceAOTermAndEdges.GatherGreen(texSampler, gatherCenter, int2(2, 0)); + float4 edgesQ2 = sourceAOTermAndEdges.GatherGreen(texSampler, gatherCenter, int2(1, 2)); + + float visQ0[4]; + XeGTAO_DecodeGatherPartial(sourceAOTermAndEdges.GatherRed(texSampler, gatherCenter, int2(0, 0)), visQ0); + float visQ1[4]; + XeGTAO_DecodeGatherPartial(sourceAOTermAndEdges.GatherRed(texSampler, gatherCenter, int2(2, 0)), visQ1); + float visQ2[4]; + XeGTAO_DecodeGatherPartial(sourceAOTermAndEdges.GatherRed(texSampler, gatherCenter, int2(0, 2)), visQ2); + float visQ3[4]; + XeGTAO_DecodeGatherPartial(sourceAOTermAndEdges.GatherRed(texSampler, gatherCenter, int2(2, 2)), visQ3); + + for (int side = 0; side < 2; side++) + { + const int2 pixCoord = int2(pixCoordBase.x + side, pixCoordBase.y); + + float4 edgesL_LRTB = XeGTAO_UnpackEdges(side == 0 ? edgesQ0.x : edgesQ0.y); + float4 edgesT_LRTB = XeGTAO_UnpackEdges(side == 0 ? edgesQ0.z : edgesQ1.w); + float4 edgesR_LRTB = XeGTAO_UnpackEdges(side == 0 ? edgesQ1.x : edgesQ1.y); + float4 edgesB_LRTB = XeGTAO_UnpackEdges(side == 0 ? edgesQ2.w : edgesQ2.z); + + edgesC_LRTB[side] = XeGTAO_UnpackEdges(side == 0 ? edgesQ0.y : edgesQ1.x); + + // Edges aren't perfectly symmetrical: edge detection algorithm does not guarantee that a left edge on the right pixel will match the right edge on the left pixel (although + // they will match in majority of cases). This line further enforces the symmetricity, creating a slightly sharper blur. Works real nice with TAA. + edgesC_LRTB[side] *= float4(edgesL_LRTB.y, edgesR_LRTB.x, edgesT_LRTB.w, edgesB_LRTB.z); + +#if 1 // this allows some small amount of AO leaking from neighbours if there are 3 or 4 edges; this reduces both spatial and temporal aliasing + const float leak_threshold = 2.5; + const float leak_strength = 0.5; + float edginess = (saturate(4.0 - leak_threshold - dot(edgesC_LRTB[side], 1.0)) * rcp(4.0 - leak_threshold)) * leak_strength; + edgesC_LRTB[side] = saturate(edgesC_LRTB[side] + edginess); +#endif + + // for diagonals; used by first and second pass + weightTL[side] = diagWeight * (edgesC_LRTB[side].x * edgesL_LRTB.z + edgesC_LRTB[side].z * edgesT_LRTB.x); + weightTR[side] = diagWeight * (edgesC_LRTB[side].z * edgesT_LRTB.y + edgesC_LRTB[side].y * edgesR_LRTB.z); + weightBL[side] = diagWeight * (edgesC_LRTB[side].w * edgesB_LRTB.x + edgesC_LRTB[side].x * edgesL_LRTB.w); + weightBR[side] = diagWeight * (edgesC_LRTB[side].y * edgesR_LRTB.w + edgesC_LRTB[side].w * edgesB_LRTB.y); + + // first pass + float ssaoValue = side == 0 ? visQ0[1] : visQ1[0]; + float ssaoValueL = side == 0 ? visQ0[0] : visQ0[1]; + float ssaoValueT = side == 0 ? visQ0[2] : visQ1[3]; + float ssaoValueR = side == 0 ? visQ1[0] : visQ1[1]; + float ssaoValueB = side == 0 ? visQ2[2] : visQ3[3]; + float ssaoValueTL = side == 0 ? visQ0[3] : visQ0[2]; + float ssaoValueBR = side == 0 ? visQ3[3] : visQ3[2]; + float ssaoValueTR = side == 0 ? visQ1[3] : visQ1[2]; + float ssaoValueBL = side == 0 ? visQ2[3] : visQ2[2]; + + float sumWeight = blurAmount; + float sum = ssaoValue * sumWeight; + + XeGTAO_AddSample(ssaoValueL, edgesC_LRTB[side].x, sum, sumWeight); + XeGTAO_AddSample(ssaoValueR, edgesC_LRTB[side].y, sum, sumWeight); + XeGTAO_AddSample(ssaoValueT, edgesC_LRTB[side].z, sum, sumWeight); + XeGTAO_AddSample(ssaoValueB, edgesC_LRTB[side].w, sum, sumWeight); + + XeGTAO_AddSample(ssaoValueTL, weightTL[side], sum, sumWeight); + XeGTAO_AddSample(ssaoValueTR, weightTR[side], sum, sumWeight); + XeGTAO_AddSample(ssaoValueBL, weightBL[side], sum, sumWeight); + XeGTAO_AddSample(ssaoValueBR, weightBR[side], sum, sumWeight); + + aoTerm[side] = sum / sumWeight; + +#if XE_GTAO_FINAL_APPLY + outputTexture[pixCoord] = XE_GTAO_ENCODE_FINAL(XE_GTAO_FINAL_VALUE(saturate(aoTerm[side] * XE_GTAO_OCCLUSION_TERM_SCALE))); +#else + outputTexture[pixCoord] = float2(aoTerm[side], side == 0 ? edgesQ0.y : edgesQ1.x); +#endif + } +} + +// Implementation +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +SamplerState smp : register(s0); // point-clamp, bound by Luma + +Texture2D tex0 : register(t0); + +RWTexture2D out_working_depth_mip0 : register(u0); +RWTexture2D out_working_depth_mip1 : register(u1); +RWTexture2D out_working_depth_mip2 : register(u2); +RWTexture2D out_working_depth_mip3 : register(u3); +RWTexture2D out_working_depth_mip4 : register(u4); +RWTexture2D ao_term_and_edges : register(u0); + +RWTexture2D final_output : register(u0); + +#define XE_GTAO_NUMTHREADS_X 8 +#define XE_GTAO_NUMTHREADS_Y 8 + +// From https://www.shadertoy.com/view/3tB3z3 - except we're using R2 here +#define XE_HILBERT_LEVEL 6U +#define XE_HILBERT_WIDTH (1U << XE_HILBERT_LEVEL) +#define XE_HILBERT_AREA (XE_HILBERT_WIDTH * XE_HILBERT_WIDTH) +uint HilbertIndex(uint posX, uint posY) +{ + uint index = 0U; + [unroll] for (uint curLevel = XE_HILBERT_WIDTH / 2U; curLevel > 0U; curLevel /= 2U) + { + uint regionX = (posX & curLevel) > 0U; + uint regionY = (posY & curLevel) > 0U; + index += curLevel * curLevel * ((3U * regionX) ^ regionY); + if (regionY == 0U) + { + if (regionX == 1U) + { + posX = XE_HILBERT_WIDTH - 1U - posX; + posY = XE_HILBERT_WIDTH - 1U - posY; + } + uint temp = posX; + posX = posY; + posY = temp; + } + } + return index; +} + +float2 SpatioTemporalNoise(uint2 pixCoord, uint temporalIndex) +{ + uint index = HilbertIndex(pixCoord.x, pixCoord.y); + index += 288 * (temporalIndex % 64); // why 288? tried out a few and that's the best so far (with XE_HILBERT_LEVEL 6U) - but there's probably better :) + // R2 sequence - see http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/ + return float2(frac(0.5 + index * float2(0.75487766624669276005, 0.5698402909980532659114))); +} + +[numthreads(8, 8, 1)] // <- hard coded to 8x8; each thread computes 2x2 blocks so processing 16x16 block: Dispatch needs to be called with (width + 16-1) / 16, (height + 16-1) / 16 + void prefilter_depths16x16_cs(uint2 dtid : SV_DispatchThreadID, uint2 gtid : SV_GroupThreadID) { + // tex0 = the game's depth + XeGTAO_PrefilterDepths16x16(dtid, gtid, tex0, out_working_depth_mip0, out_working_depth_mip1, out_working_depth_mip2, out_working_depth_mip3, out_working_depth_mip4); + } + + [numthreads(XE_GTAO_NUMTHREADS_X, XE_GTAO_NUMTHREADS_Y, 1)] void main_pass_cs(uint2 dtid : SV_DispatchThreadID) +{ + // tex0 = prefiltered viewspace depth MIP pyramid (our R32F) + // smp = point-clamp + XE_GTAO_CS_PROLOGUE +#if XE_GTAO_GENERATE_NORMALS + // Normals are generated from depth inside XeGTAO_MainPass (XE_GTAO_GENERATE_NORMALS) — no t1 input. + const float3 viewspaceNormal = float3(0.0, 0.0, -1.0); +#else + const float3 viewspaceNormal = XeGTAO_LoadViewspaceNormal(dtid); +#endif + XeGTAO_MainPass(dtid, SpatioTemporalNoise(dtid, uint(NoiseIndexRT)), viewspaceNormal, tex0, smp, ao_term_and_edges); +} + +[numthreads(XE_GTAO_NUMTHREADS_X, XE_GTAO_NUMTHREADS_Y, 1)] void denoise_pass_cs(uint2 dtid : SV_DispatchThreadID) { + // tex0 = working AO term (.x) + packed edges (.y) + // smp = point-clamp + XE_GTAO_CS_PROLOGUE + const uint2 pix_coord_base = dtid * uint2(2, 1); // we're computing 2 horizontal pixels at a time (performance optimization) + XeGTAO_Denoise(pix_coord_base, tex0, smp, final_output); +} + +#endif // LUMA_XEGTAO_HLSL diff --git a/Shaders/Mass Effect Andromeda/Luma_SMAA_impl.hlsl b/Shaders/Mass Effect Andromeda/Luma_SMAA_impl.hlsl index 785e86a17..a9d03b21d 100644 --- a/Shaders/Mass Effect Andromeda/Luma_SMAA_impl.hlsl +++ b/Shaders/Mass Effect Andromeda/Luma_SMAA_impl.hlsl @@ -11,70 +11,10 @@ cbuffer SmaaMetricsCB : register(b1) #define SMAA_RT_METRICS SmaaRtMetrics #define SMAA_PRESET_ULTRA -#define SMAA_PREDICATION 0 -#define SMAA_CUSTOM_SL -SamplerState LinearSampler : register(s0); -SamplerState PointSampler : register(s1); -#define SMAATexture2D(tex) Texture2D tex -#define SMAATexturePass2D(tex) tex -#define SMAASampleLevelZero(tex, coord) tex.SampleLevel(LinearSampler, coord, 0) -#define SMAASampleLevelZeroPoint(tex, coord) tex.SampleLevel(PointSampler, coord, 0) -#define SMAASampleLevelZeroOffset(tex, coord, offset) tex.SampleLevel(LinearSampler, coord, 0, offset) -#define SMAASample(tex, coord) tex.Sample(LinearSampler, coord) -#define SMAASamplePoint(tex, coord) tex.Sample(PointSampler, coord) -#define SMAASampleOffset(tex, coord, offset) tex.Sample(LinearSampler, coord, offset) -#define SMAA_FLATTEN [flatten] -#define SMAA_BRANCH [branch] -#define SMAATexture2DMS2(tex) Texture2DMS tex -#define SMAALoad(tex, pos, sample) tex.Load(pos, sample) -#define SMAAGather(tex, coord) tex.Gather(LinearSampler, coord, 0) -#include "../Includes/SMAA.hlsl" - -Texture2D tex0 : register(t0); -Texture2D tex1 : register(t1); -Texture2D tex2 : register(t2); - -void fullscreen_triangle(uint id, out float4 position, out float2 texcoord) -{ - texcoord = float2((id << 1) & 2, id & 2); - position = float4(texcoord * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); -} +#define SMAA_PREDICATION 0 +#define SMAAGather(tex, coord) tex.Gather(LinearSampler, coord, 0) // SMAAEdgeDetection -void smaa_edge_detection_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset[3] : TEXCOORD1) -{ - fullscreen_triangle(id, position, texcoord); - SMAAEdgeDetectionVS(texcoord, offset); -} - -float2 smaa_edge_detection_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset[3] : TEXCOORD1) : SV_Target -{ - // tex0 = colorTexGamma (predication disabled, so tex1 is unused) - return SMAAColorEdgeDetectionPS(texcoord, offset, tex0); -} - -// SMAABlendingWeightCalculation -void smaa_blending_weight_calculation_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float2 pixcoord : TEXCOORD1, out float4 offset[3] : TEXCOORD2) -{ - fullscreen_triangle(id, position, texcoord); - SMAABlendingWeightCalculationVS(texcoord, pixcoord, offset); -} - -float4 smaa_blending_weight_calculation_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float2 pixcoord : TEXCOORD1, float4 offset[3] : TEXCOORD2) : SV_Target -{ - // tex0 = edgesTex, tex1 = areaTex, tex2 = searchTex - return SMAABlendingWeightCalculationPS(texcoord, pixcoord, offset, tex0, tex1, tex2, 0); -} +// tex0 = colorTexGamma (predication disabled, so tex1 is unused) -// SMAANeighborhoodBlending -void smaa_neighborhood_blending_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset : TEXCOORD1) -{ - fullscreen_triangle(id, position, texcoord); - SMAANeighborhoodBlendingVS(texcoord, offset); -} - -float4 smaa_neighborhood_blending_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset : TEXCOORD1) : SV_Target -{ - // tex0 = colorTex, tex1 = blendTex - return SMAANeighborhoodBlendingPS(texcoord, offset, tex0, tex1); -} +#include "../Includes/SMAA_Passes.hlsl" diff --git a/Shaders/Mass Effect Legendary Edition/Luma_MELE_Sharpen.hlsl b/Shaders/Mass Effect Legendary Edition/Luma_MELE_Sharpen.hlsl deleted file mode 100644 index 0e41d3568..000000000 --- a/Shaders/Mass Effect Legendary Edition/Luma_MELE_Sharpen.hlsl +++ /dev/null @@ -1,20 +0,0 @@ -// RCAS sharpens the gamma SMAA output before it is copied to the fp16 post buffer and decoded by stage 2. It uses -// paperWhite=1 and an SDR-tuned lobe limiter bounded by RCAS_LIMIT, so gamma highlights above 1 may sharpen less -// uniformly. Sharpness defaults to zero. - -#include "../Includes/RCAS.hlsl" - -cbuffer SharpenCB : register(b0) -{ - float4 SharpenParams; // (width, height, sharpness [0,1], unused). -} - -Texture2D tex0 : register(t0); // Gamma SMAA output. -Texture2D dummyMV : register(t1); // Unused because dynamicSharpening is false. - -float4 sharpen_ps(float4 pos : SV_Position) : SV_Target -{ - int2 p = int2(pos.xy); - int2 maxPixel = int2((int)SharpenParams.x - 1, (int)SharpenParams.y - 1); - return RCAS(p, int2(0, 0), maxPixel, SharpenParams.z, tex0, dummyMV, 1.0, false, (float4)0, false); -} diff --git a/Shaders/Mass Effect Legendary Edition/Luma_MELE_XeGTAO.hlsl b/Shaders/Mass Effect Legendary Edition/Luma_MELE_XeGTAO.hlsl index c251be06a..f9f188d63 100644 --- a/Shaders/Mass Effect Legendary Edition/Luma_MELE_XeGTAO.hlsl +++ b/Shaders/Mass Effect Legendary Edition/Luma_MELE_XeGTAO.hlsl @@ -53,28 +53,12 @@ cbuffer LumaGTAO : register(b11) #include "Includes/Common.hlsl" -#if XE_GTAO_QUALITY == 0 // Low -#define SLICE_COUNT 4.0 -#elif XE_GTAO_QUALITY == 1 // Medium -#define SLICE_COUNT 7.0 -#elif XE_GTAO_QUALITY == 2 // High -#define SLICE_COUNT 10.0 -#elif XE_GTAO_QUALITY == 3 // Very High -#define SLICE_COUNT 13.0 -#elif XE_GTAO_QUALITY == 4 // Ultra -#define SLICE_COUNT 16.0 -#endif - // Compile-time defaults; runtime b11 overrides the exposed controls. #ifndef EFFECT_RADIUS #define EFFECT_RADIUS 0.6 // Native ME1LE radius: 30 UE3 units / DepthScale 50; runtime override wins. #endif -#ifndef RADIUS_MULTIPLIER -#define RADIUS_MULTIPLIER 1.457 // Default 1.457 -#endif - #ifndef EFFECT_FALLOFF_RANGE #define EFFECT_FALLOFF_RANGE 0.005 // Hard falloff matches native contact AO; Intel default 0.615 is softer. #endif @@ -83,49 +67,16 @@ cbuffer LumaGTAO : register(b11) #define SAMPLE_DISTRIBUTION_POWER 1.5 // Default 2.0 #endif -#ifndef THIN_OCCLUDER_COMPENSATION -#define THIN_OCCLUDER_COMPENSATION 0.0 // Default 0.0; > 0 causes more mistakes than it fixes on big geometry -#endif - -#ifndef FINAL_VALUE_POWER -#define FINAL_VALUE_POWER 1.0 // Unused; the main pass reads runtime FinalValuePowerRT. -#endif - -#ifndef DEPTH_MIP_SAMPLING_OFFSET -#define DEPTH_MIP_SAMPLING_OFFSET 3.3 // Default 3.3 -#endif - -#ifndef SLICE_COUNT -#define SLICE_COUNT 3.0 // Default 3.0 -#endif - -#ifndef STEPS_PER_SLICE -#define STEPS_PER_SLICE 3.0 // Default 3.0 -#endif - -#ifndef DENOISE_BLUR_BETA -#define DENOISE_BLUR_BETA 1.2 // Default 1.2 -#endif - // ViewNormalTex z sign; view-space normals face the camera. #ifndef NORMAL_Z_SIGN #define NORMAL_Z_SIGN (-1.0) #endif -// - #define VIEWPORT_PIXEL_SIZE InvFullResolution // GFSDK ProjInfo contains the live NDC-to-view multiply/add pair, including dialogue zoom. -#define NDC_TO_VIEW_MUL ProjInfo.xy -#define NDC_TO_VIEW_ADD ProjInfo.zw -#define NDC_TO_VIEW_MUL_X_PIXEL_SIZE (NDC_TO_VIEW_MUL * VIEWPORT_PIXEL_SIZE) - -#define XE_GTAO_DEPTH_MIP_LEVELS 5.0 -#define XE_GTAO_OCCLUSION_TERM_SCALE 1.5 - -#define XE_GTAO_PI 3.1415926535897932384626433832795 -#define XE_GTAO_PI_HALF 1.5707963267948966192313216916398 +#define NDC_TO_VIEW_MUL ProjInfo.xy +#define NDC_TO_VIEW_ADD ProjInfo.zw // Convert non-reverse D24 through native MinZ_MaxZRatioCS, then scale UE3 units into XeGTAO's expected range. // Without scaling, its mip and falloff assumptions cause broad over-occlusion. @@ -135,596 +86,22 @@ float XeGTAO_ScreenSpaceToViewSpaceDepth(const float screenDepth) return max(0.0, viewZ) / max(1e-3, DepthScaleRT); } -// Clamp the converted depth before mip generation. -float XeGTAO_ClampDepth(float depth) -{ - return clamp(depth, 0.0, 3.402823466e+38); -} - -float XeGTAO_EffectRadius() -{ - return (RadiusOverrideRT > 0.0 ? RadiusOverrideRT : EFFECT_RADIUS) * RADIUS_MULTIPLIER; -} - -// Edge-aware depth mip filter. -float XeGTAO_DepthMIPFilter(float depth0, float depth1, float depth2, float depth3) -{ - float maxDepth = max(max(depth0, depth1), max(depth2, depth3)); - - const float depthRangeScaleFactor = 0.75; // Empirical XeGTAO constant. - const float effectRadius = depthRangeScaleFactor * XeGTAO_EffectRadius(); - const float falloffRange = EFFECT_FALLOFF_RANGE * effectRadius; - const float falloffFrom = effectRadius * (1.0 - EFFECT_FALLOFF_RANGE); - - // Precompute falloff coefficients. - const float falloffMul = -1.0 / falloffRange; - const float falloffAdd = falloffFrom / falloffRange + 1.0; - - float weight0 = saturate((maxDepth - depth0) * falloffMul + falloffAdd); - float weight1 = saturate((maxDepth - depth1) * falloffMul + falloffAdd); - float weight2 = saturate((maxDepth - depth2) * falloffMul + falloffAdd); - float weight3 = saturate((maxDepth - depth3) * falloffMul + falloffAdd); - - float weightSum = weight0 + weight1 + weight2 + weight3; - return (weight0 * depth0 + weight1 * depth1 + weight2 * depth2 + weight3 * depth3) * rcp(weightSum); -} - -groupshared float g_scratchDepths[8][8]; -void XeGTAO_PrefilterDepths16x16(uint2 dispatchThreadID, uint2 groupThreadID, Texture2D sourceNDCDepth, RWTexture2D outDepth0, RWTexture2D outDepth1, RWTexture2D outDepth2, RWTexture2D outDepth3, RWTexture2D outDepth4) -{ - // MIP 0 - const uint2 baseCoord = dispatchThreadID; - const uint2 pixCoord = baseCoord * 2; - // GatherRed returns zero on some R24_UNORM_X8 views. Integer Loads preserve depth; D3D11 defines - // out-of-bounds Loads as zero before conversion and clamping. - float d00 = sourceNDCDepth.Load(int3(pixCoord + uint2(0, 0), 0)).x; - float d10 = sourceNDCDepth.Load(int3(pixCoord + uint2(1, 0), 0)).x; - float d01 = sourceNDCDepth.Load(int3(pixCoord + uint2(0, 1), 0)).x; - float d11 = sourceNDCDepth.Load(int3(pixCoord + uint2(1, 1), 0)).x; - float depth0 = XeGTAO_ClampDepth(XeGTAO_ScreenSpaceToViewSpaceDepth(d00)); - float depth1 = XeGTAO_ClampDepth(XeGTAO_ScreenSpaceToViewSpaceDepth(d10)); - float depth2 = XeGTAO_ClampDepth(XeGTAO_ScreenSpaceToViewSpaceDepth(d01)); - float depth3 = XeGTAO_ClampDepth(XeGTAO_ScreenSpaceToViewSpaceDepth(d11)); - outDepth0[pixCoord + uint2(0, 0)] = depth0; - outDepth0[pixCoord + uint2(1, 0)] = depth1; - outDepth0[pixCoord + uint2(0, 1)] = depth2; - outDepth0[pixCoord + uint2(1, 1)] = depth3; - - // MIP 1 - float dm1 = XeGTAO_DepthMIPFilter(depth0, depth1, depth2, depth3); - outDepth1[baseCoord] = dm1; - g_scratchDepths[groupThreadID.x][groupThreadID.y] = dm1; - - GroupMemoryBarrierWithGroupSync(); - - // MIP 2 - [branch] if (all((groupThreadID.xy % 2) == 0)) - { - float inTL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 0]; - float inTR = g_scratchDepths[groupThreadID.x + 1][groupThreadID.y + 0]; - float inBL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 1]; - float inBR = g_scratchDepths[groupThreadID.x + 1][groupThreadID.y + 1]; - - float dm2 = XeGTAO_DepthMIPFilter(inTL, inTR, inBL, inBR); - outDepth2[baseCoord / 2] = dm2; - g_scratchDepths[groupThreadID.x][groupThreadID.y] = dm2; - } - - GroupMemoryBarrierWithGroupSync(); - - // MIP 3 - [branch] if (all((groupThreadID.xy % 4) == 0)) - { - float inTL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 0]; - float inTR = g_scratchDepths[groupThreadID.x + 2][groupThreadID.y + 0]; - float inBL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 2]; - float inBR = g_scratchDepths[groupThreadID.x + 2][groupThreadID.y + 2]; - - float dm3 = XeGTAO_DepthMIPFilter(inTL, inTR, inBL, inBR); - outDepth3[baseCoord / 4] = dm3; - g_scratchDepths[groupThreadID.x][groupThreadID.y] = dm3; - } - - GroupMemoryBarrierWithGroupSync(); - - // MIP 4 - [branch] if (all((groupThreadID.xy % 8) == 0)) - { - float inTL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 0]; - float inTR = g_scratchDepths[groupThreadID.x + 4][groupThreadID.y + 0]; - float inBL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 4]; - float inBR = g_scratchDepths[groupThreadID.x + 4][groupThreadID.y + 4]; - - float dm4 = XeGTAO_DepthMIPFilter(inTL, inTR, inBL, inBR); - outDepth4[baseCoord / 8] = dm4; - // g_scratchDepths[ groupThreadID.x ][ groupThreadID.y ] = dm4; - } -} - -float4 XeGTAO_CalculateEdges(float centerZ, float leftZ, float rightZ, float topZ, float bottomZ) -{ - float4 edgesLRTB = float4(leftZ, rightZ, topZ, bottomZ) - centerZ; - - float slopeLR = (edgesLRTB.y - edgesLRTB.x) * 0.5; - float slopeTB = (edgesLRTB.w - edgesLRTB.z) * 0.5; - float4 edgesLRTBSlopeAdjusted = edgesLRTB + float4(slopeLR, -slopeLR, slopeTB, -slopeTB); - edgesLRTB = min(abs(edgesLRTB), abs(edgesLRTBSlopeAdjusted)); - return saturate(1.25 - edgesLRTB * rcp(centerZ * 0.011)); -} - -// Pack four edge gradients into two bits each: 0, 0.33, 0.66, or 1. -float XeGTAO_PackEdges(float4 edgesLRTB) -{ - edgesLRTB = round(saturate(edgesLRTB) * 2.9); - return dot(edgesLRTB, float4(64.0 / 255.0, 16.0 / 255.0, 4.0 / 255.0, 1.0 / 255.0)); -} - -// Reconstruct view-space position from screen UV and view-space depth. -float3 XeGTAO_ComputeViewspacePosition(float2 screenPos, float viewspaceDepth) -{ - float3 ret; - ret.xy = (NDC_TO_VIEW_MUL * screenPos.xy + NDC_TO_VIEW_ADD) * viewspaceDepth; - ret.z = viewspaceDepth; - return ret; -} - -// http://h14s.p5r.org/2012/09/0x5f3759df.html, [Drobot2014a] Low Level Optimizations for GCN, https://blog.selfshadow.com/publications/s2016-shading-course/activision/s2016_pbs_activision_occlusion.pdf slide 63 -float XeGTAO_FastSqrt(float x) -{ - return asfloat(0x1fbd1df5 + (asint(x) >> 1)); -} - -// Maps [-1,1] to [0,PI]. Source: https://seblagarde.wordpress.com/2014/12/01/inverse-trigonometric-functions-gpu-optimization-for-amd-gcn-architecture/ -float XeGTAO_FastACos(float inX) -{ - // Shared Common defines PI as a macro, so use a literal locally. - float x = abs(inX); - float res = -0.156583 * x + 1.570796; - res *= XeGTAO_FastSqrt(1.0 - x); - return inX >= 0 ? res : 3.141593 - res; -} - -void XeGTAO_MainPass(uint2 pixCoord, float2 localNoise, float3 viewspaceNormal, Texture2D sourceViewspaceDepth, SamplerState depthSampler, RWTexture2D outWorkingAOTermAndEdges) -{ - float2 normalizedScreenPos = (pixCoord + 0.5) * VIEWPORT_PIXEL_SIZE; - - // Gather is safe on the prefiltered R32F mip; the zero-read quirk applies only to native R24 depth. - float4 valuesUL = sourceViewspaceDepth.GatherRed(depthSampler, float2(pixCoord * VIEWPORT_PIXEL_SIZE)); - float4 valuesBR = sourceViewspaceDepth.GatherRed(depthSampler, float2(pixCoord * VIEWPORT_PIXEL_SIZE), int2(1, 1)); - - // Center view-space Z. - float viewspaceZ = valuesUL.y; - - // Neighbor view-space Z in left, top, right, bottom order. - const float pixLZ = valuesUL.x; - const float pixTZ = valuesUL.z; - const float pixRZ = valuesBR.z; - const float pixBZ = valuesBR.x; - - float4 edgesLRTB = XeGTAO_CalculateEdges(viewspaceZ, pixLZ, pixRZ, pixTZ, pixBZ); - const float edges = XeGTAO_PackEdges(edgesLRTB); - -#if DEVELOPMENT - // Debug values pass unfiltered through denoise and native AO apply. - if (DebugViewRT > 0.5 && DebugViewRT < 1.5) // Depth gradient validates live conversion and scale. - { - outWorkingAOTermAndEdges[pixCoord] = float2(saturate(frac(log2(max(viewspaceZ, 1e-6)))), 1.0); - return; - } - if (DebugViewRT >= 3.5 && DebugViewRT < 4.5) // Edge confidence. - { - outWorkingAOTermAndEdges[pixCoord] = float2(dot(edgesLRTB, 0.25), 1.0); - return; - } -#endif - - // Move the center slightly toward the camera to suppress depth precision artifacts. - viewspaceZ *= 0.99999; // Tuned for FP32 view-space depth. - - const float3 pixCenterPos = XeGTAO_ComputeViewspacePosition(normalizedScreenPos, viewspaceZ); - const float3 viewVec = normalize(-pixCenterPos); - - // Fold back-facing normals toward the view vector for extreme cases. - viewspaceNormal = normalize(viewspaceNormal + max(0, -dot(viewspaceNormal, viewVec)) * viewVec); - -#if DEVELOPMENT - if (DebugViewRT >= 1.5 && DebugViewRT < 2.5) // Smooth view-facing shading indicates correct normal decode. - { - outWorkingAOTermAndEdges[pixCoord] = float2(saturate(abs(dot(viewspaceNormal, viewVec))), 1.0); - return; - } -#endif - - const float effectRadius = XeGTAO_EffectRadius(); - const float sampleDistributionPower = SAMPLE_DISTRIBUTION_POWER; - const float thinOccluderCompensation = THIN_OCCLUDER_COMPENSATION; - const float falloffRange = EFFECT_FALLOFF_RANGE * effectRadius; - const float falloffFrom = effectRadius * (1.0 - EFFECT_FALLOFF_RANGE); - - // Precompute falloff coefficients. - const float falloffMul = -1.0 / falloffRange; - const float falloffAdd = falloffFrom / falloffRange + 1.0; - - float visibility = 0.0; - - // Algorithm 1: https://www.activision.com/cdn/research/Practical_Real_Time_Strategies_for_Accurate_Indirect_Occlusion_NEW%20VERSION_COLOR.pdf - { - const float noiseSlice = localNoise.x; - const float noiseSample = localNoise.y; - - // Sampling heuristics. - const float pixelTooCloseThreshold = 1.3; // Push sub-pixel offsets to the minimum useful distance. - - // Approximate view-space pixel size at the center depth. - const float2 pixelDirRBViewspaceSizeAtCenterZ = viewspaceZ.xx * NDC_TO_VIEW_MUL_X_PIXEL_SIZE; - - float screenspaceRadius = effectRadius * rcp(abs(pixelDirRBViewspaceSizeAtCenterZ.x)); - - // Fade for small screen-space radii. - visibility += saturate((10.0 - screenspaceRadius) / 100.0) * 0.5; - - // Minimum useful distance from the center pixel. - const float minS = pixelTooCloseThreshold * rcp(screenspaceRadius); - - //[unroll] - for (float slice = 0.0; slice < SLICE_COUNT; slice++) - { - float sliceK = (slice + noiseSlice) / SLICE_COUNT; - // Algorithm 1, lines 5-6. - float phi = sliceK * XE_GTAO_PI; - float cosPhi = cos(phi); - float sinPhi = sin(phi); - float2 omega = float2(cosPhi, -sinPhi); // Keep full precision for large radii. - - // Convert to screen-space pixels. - omega *= screenspaceRadius; - - // Algorithm 1, line 8. - const float3 directionVec = float3(cosPhi, sinPhi, 0.0); - - // Algorithm 1, line 9. - const float3 orthoDirectionVec = directionVec - (dot(directionVec, viewVec) * viewVec); - - // Algorithm 1, line 10. - // axisVec is orthogonal to directionVec and viewVec, used to define projectedNormal - const float3 axisVec = normalize(cross(orthoDirectionVec, viewVec)); - - // Algorithm 1, line 11. - float3 projectedNormalVec = viewspaceNormal - axisVec * dot(viewspaceNormal, axisVec); - - // Algorithm 1, line 13. - float signNorm = sign(dot(orthoDirectionVec, projectedNormalVec)); - - // Algorithm 1, line 14. - float projectedNormalVecLength = length(projectedNormalVec); - float cosNorm = saturate(dot(projectedNormalVec, viewVec) * rcp(projectedNormalVecLength)); - - // Algorithm 1, line 15. - float n = signNorm * XeGTAO_FastACos(cosNorm); - - // Use the normal-dependent lower horizon rather than the paper's fixed -1 target. - const float lowHorizonCos0 = cos(n + XE_GTAO_PI_HALF); - const float lowHorizonCos1 = cos(n - XE_GTAO_PI_HALF); - - // Algorithm 1, lines 17-18, with the side loop unrolled. - float horizonCos0 = lowHorizonCos0; //-1; - float horizonCos1 = lowHorizonCos1; //-1; - - [unroll] for (float step = 0.0; step < STEPS_PER_SLICE; step++) - { - // R1 sequence (http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/) - const float stepBaseNoise = (slice + step * STEPS_PER_SLICE) * 0.6180339887498948482; // Compile-time unrolled. - float stepNoise = frac(noiseSample + stepBaseNoise); - - // Approximate Algorithm 1 line 20 with noise. - float s = (step + stepNoise) / STEPS_PER_SLICE; - - // Apply sample distribution power. - s = pow(s, sampleDistributionPower); - - // Avoid the center pixel. - s += minS; - - // Approximate Algorithm 1 lines 21-22. - float2 sampleOffset = s * omega; - - float sampleOffsetLength = length(sampleOffset); - - // Point XY sampling avoids interpolation between neighboring depths within a mip. - const float mipLevel = clamp(log2(sampleOffsetLength) - DEPTH_MIP_SAMPLING_OFFSET, 0.0, XE_GTAO_DEPTH_MIP_LEVELS); - - // Snap to pixel centers for correct direction and slope math; retain full precision at high resolution. - sampleOffset = round(sampleOffset) * VIEWPORT_PIXEL_SIZE; - - float2 sampleScreenPos0 = normalizedScreenPos + sampleOffset; - float SZ0 = sourceViewspaceDepth.SampleLevel(depthSampler, sampleScreenPos0, mipLevel).x; - float3 samplePos0 = XeGTAO_ComputeViewspacePosition(sampleScreenPos0, SZ0); - - float2 sampleScreenPos1 = normalizedScreenPos - sampleOffset; - float SZ1 = sourceViewspaceDepth.SampleLevel(depthSampler, sampleScreenPos1, mipLevel).x; - float3 samplePos1 = XeGTAO_ComputeViewspacePosition(sampleScreenPos1, SZ1); - - float3 sampleDelta0 = samplePos0 - pixCenterPos; // Reduced precision causes visible errors. - float3 sampleDelta1 = samplePos1 - pixCenterPos; - float sampleDist0 = length(sampleDelta0); - float sampleDist1 = length(sampleDelta1); - - // Approximate Algorithm 1 lines 23-24. - float3 sampleHorizonVec0 = sampleDelta0 * rcp(sampleDist0); - float3 sampleHorizonVec1 = sampleDelta1 * rcp(sampleDist1); - - // Bound sampling radius with a smooth falloff, following section 4.3. The thickness heuristic rejects - // samples behind the center earlier. - float falloffBase0 = length(float3(sampleDelta0.x, sampleDelta0.y, sampleDelta0.z * (1.0 + thinOccluderCompensation))); - float falloffBase1 = length(float3(sampleDelta1.x, sampleDelta1.y, sampleDelta1.z * (1.0 + thinOccluderCompensation))); - float weight0 = saturate(falloffBase0 * falloffMul + falloffAdd); - float weight1 = saturate(falloffBase1 * falloffMul + falloffAdd); - - // Sample horizon cosines. - float shc0 = dot(sampleHorizonVec0, viewVec); - float shc1 = dot(sampleHorizonVec1, viewVec); - - // Blend rejected samples toward the lower horizon. - shc0 = lerp(lowHorizonCos0, shc0, weight0); // Angular interpolation is more accurate but too expensive. - shc1 = lerp(lowHorizonCos1, shc1, weight1); - - // THIN_OCCLUDER_COMPENSATION=0 disables the thickness adjustment. - horizonCos0 = max(horizonCos0, shc0); - horizonCos1 = max(horizonCos1, shc1); - } - -#if 1 // XeGTAO training-set slope adjustment: 0.05 gives PSNR 21.34 versus 21.45 when disabled. - projectedNormalVecLength = lerp(projectedNormalVecLength, 1.0, 0.05); -#endif - - // Approximate Algorithm 1 line 27. - float h0 = -XeGTAO_FastACos(horizonCos1); - float h1 = XeGTAO_FastACos(horizonCos0); - float iarc0 = (cosNorm + 2.0 * h0 * sin(n) - cos(2.0 * h0 - n)) / 4.0; - float iarc1 = (cosNorm + 2.0 * h1 * sin(n) - cos(2.0 * h1 - n)) / 4.0; - float localVisibility = projectedNormalVecLength * (iarc0 + iarc1); - visibility += localVisibility; - } - visibility /= SLICE_COUNT; - visibility = pow(visibility, max(0.05, FinalValuePowerRT)); // Runtime match to the native AO histogram. - visibility = max(0.03, visibility); // A visible surface cannot be fully occluded. - } - - visibility = saturate(visibility / XE_GTAO_OCCLUSION_TERM_SCALE); - outWorkingAOTermAndEdges[pixCoord] = float2(visibility, edges); -} - -void XeGTAO_DecodeGatherPartial(float4 packedValue, out float outDecoded[4]) -{ - for (int i = 0; i < 4; i++) - { - outDecoded[i] = packedValue[i]; - } -} - -float4 XeGTAO_UnpackEdges(float _packedVal) -{ - uint packedVal = uint(_packedVal * 255.5); - float4 edgesLRTB; - edgesLRTB.x = float((packedVal >> 6) & 0x03) / 3.0; // Retain the mask for explicit packed-input safety. - edgesLRTB.y = float((packedVal >> 4) & 0x03) / 3.0; - edgesLRTB.z = float((packedVal >> 2) & 0x03) / 3.0; - edgesLRTB.w = float((packedVal >> 0) & 0x03) / 3.0; - - return saturate(edgesLRTB); -} - -void XeGTAO_AddSample(float ssaoValue, float edgeValue, inout float sum, inout float sumWeight) -{ - float weight = edgeValue; - - sum += weight * ssaoValue; - sumWeight += weight; -} - -void XeGTAO_Denoise(uint2 pixCoordBase, Texture2D sourceAOTermAndEdges, SamplerState texSampler, -#if XE_GTAO_FINAL_APPLY - RWTexture2D outputTexture // Native single-channel R8_UNORM AO target. -#else - RWTexture2D outputTexture -#endif -) -{ -#if DEVELOPMENT - // Pass debug values through without denoising. - if (DebugViewRT > 0.5) - { - for (int dside = 0; dside < 2; dside++) - { - const uint2 dpix = uint2(pixCoordBase.x + dside, pixCoordBase.y); - float v = sourceAOTermAndEdges.Load(int3(dpix, 0)).x; -#if XE_GTAO_FINAL_APPLY - if (DebugViewRT >= 2.5 && DebugViewRT < 3.5) // AO x8 exposes broad over-occlusion. - v = saturate(1.0 - (1.0 - v * XE_GTAO_OCCLUSION_TERM_SCALE) * 8.0); - outputTexture[dpix] = v; -#else - outputTexture[dpix] = float2(v, sourceAOTermAndEdges.Load(int3(dpix, 0)).y); -#endif - } - return; - } -#endif - -#if XE_GTAO_FINAL_APPLY - const float blurAmount = DENOISE_BLUR_BETA; -#else - const float blurAmount = DENOISE_BLUR_BETA / 5.0; -#endif - - const float diagWeight = 0.85 * 0.5; - - float aoTerm[2]; // pixCoordBase and its right neighbor. - float4 edgesC_LRTB[2]; - float weightTL[2]; - float weightTR[2]; - float weightBL[2]; - float weightBR[2]; - - // Gather edge and visibility quads. - const float2 gatherCenter = float2(pixCoordBase.x, pixCoordBase.y) * VIEWPORT_PIXEL_SIZE; - float4 edgesQ0 = sourceAOTermAndEdges.GatherGreen(texSampler, gatherCenter, int2(0, 0)); - float4 edgesQ1 = sourceAOTermAndEdges.GatherGreen(texSampler, gatherCenter, int2(2, 0)); - float4 edgesQ2 = sourceAOTermAndEdges.GatherGreen(texSampler, gatherCenter, int2(1, 2)); - - float visQ0[4]; - XeGTAO_DecodeGatherPartial(sourceAOTermAndEdges.GatherRed(texSampler, gatherCenter, int2(0, 0)), visQ0); - float visQ1[4]; - XeGTAO_DecodeGatherPartial(sourceAOTermAndEdges.GatherRed(texSampler, gatherCenter, int2(2, 0)), visQ1); - float visQ2[4]; - XeGTAO_DecodeGatherPartial(sourceAOTermAndEdges.GatherRed(texSampler, gatherCenter, int2(0, 2)), visQ2); - float visQ3[4]; - XeGTAO_DecodeGatherPartial(sourceAOTermAndEdges.GatherRed(texSampler, gatherCenter, int2(2, 2)), visQ3); - - for (int side = 0; side < 2; side++) - { - const int2 pixCoord = int2(pixCoordBase.x + side, pixCoordBase.y); - - float4 edgesL_LRTB = XeGTAO_UnpackEdges(side == 0 ? edgesQ0.x : edgesQ0.y); - float4 edgesT_LRTB = XeGTAO_UnpackEdges(side == 0 ? edgesQ0.z : edgesQ1.w); - float4 edgesR_LRTB = XeGTAO_UnpackEdges(side == 0 ? edgesQ1.x : edgesQ1.y); - float4 edgesB_LRTB = XeGTAO_UnpackEdges(side == 0 ? edgesQ2.w : edgesQ2.z); - - edgesC_LRTB[side] = XeGTAO_UnpackEdges(side == 0 ? edgesQ0.y : edgesQ1.x); - - // Edge detection is not guaranteed symmetric across neighboring pixels. Enforce symmetry for a sharper blur. - edgesC_LRTB[side] *= float4(edgesL_LRTB.y, edgesR_LRTB.x, edgesT_LRTB.w, edgesB_LRTB.z); - -#if 1 // Permit slight neighbor leakage across three or four edges to reduce aliasing. - const float leak_threshold = 2.5; - const float leak_strength = 0.5; - float edginess = (saturate(4.0 - leak_threshold - dot(edgesC_LRTB[side], 1.0)) * rcp(4.0 - leak_threshold)) * leak_strength; - edgesC_LRTB[side] = saturate(edgesC_LRTB[side] + edginess); -#endif - - // Diagonal weights shared by both denoise passes. - weightTL[side] = diagWeight * (edgesC_LRTB[side].x * edgesL_LRTB.z + edgesC_LRTB[side].z * edgesT_LRTB.x); - weightTR[side] = diagWeight * (edgesC_LRTB[side].z * edgesT_LRTB.y + edgesC_LRTB[side].y * edgesR_LRTB.z); - weightBL[side] = diagWeight * (edgesC_LRTB[side].w * edgesB_LRTB.x + edgesC_LRTB[side].x * edgesL_LRTB.w); - weightBR[side] = diagWeight * (edgesC_LRTB[side].y * edgesR_LRTB.w + edgesC_LRTB[side].w * edgesB_LRTB.y); - - // Accumulate the 3x3 neighborhood. - float ssaoValue = side == 0 ? visQ0[1] : visQ1[0]; - float ssaoValueL = side == 0 ? visQ0[0] : visQ0[1]; - float ssaoValueT = side == 0 ? visQ0[2] : visQ1[3]; - float ssaoValueR = side == 0 ? visQ1[0] : visQ1[1]; - float ssaoValueB = side == 0 ? visQ2[2] : visQ3[3]; - float ssaoValueTL = side == 0 ? visQ0[3] : visQ0[2]; - float ssaoValueBR = side == 0 ? visQ3[3] : visQ3[2]; - float ssaoValueTR = side == 0 ? visQ1[3] : visQ1[2]; - float ssaoValueBL = side == 0 ? visQ2[3] : visQ2[2]; - - float sumWeight = blurAmount; - float sum = ssaoValue * sumWeight; - - XeGTAO_AddSample(ssaoValueL, edgesC_LRTB[side].x, sum, sumWeight); - XeGTAO_AddSample(ssaoValueR, edgesC_LRTB[side].y, sum, sumWeight); - XeGTAO_AddSample(ssaoValueT, edgesC_LRTB[side].z, sum, sumWeight); - XeGTAO_AddSample(ssaoValueB, edgesC_LRTB[side].w, sum, sumWeight); - - XeGTAO_AddSample(ssaoValueTL, weightTL[side], sum, sumWeight); - XeGTAO_AddSample(ssaoValueTR, weightTR[side], sum, sumWeight); - XeGTAO_AddSample(ssaoValueBL, weightBL[side], sum, sumWeight); - XeGTAO_AddSample(ssaoValueBR, weightBR[side], sum, sumWeight); - - aoTerm[side] = sum / sumWeight; - -#if XE_GTAO_FINAL_APPLY - // Native R8_UNORM stores visibility only; the game applies it to the scene. - outputTexture[pixCoord] = saturate(aoTerm[side] * XE_GTAO_OCCLUSION_TERM_SCALE); -#else - outputTexture[pixCoord] = float2(aoTerm[side], side == 0 ? edgesQ0.y : edgesQ1.x); -#endif - } -} - -// Shader entry points and bindings. -///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#define NoiseIndexRT 0 +#define XE_GTAO_ADJUST_VISIBILITY(visibility, viewspaceZ) visibility = max(0.03, visibility); // A visible surface cannot be fully occluded. +#define XE_GTAO_FINAL_OUTPUT_TYPE unorm float // Native final R8_UNORM AO. +#define XE_GTAO_ENCODE_FINAL(v) (v) -SamplerState smp : register(s0); // Luma binds point-clamp because native AO does not reliably set s0. - -Texture2D tex0 : register(t0); Texture2D tex1 : register(t1); -RWTexture2D out_working_depth_mip0 : register(u0); -RWTexture2D out_working_depth_mip1 : register(u1); -RWTexture2D out_working_depth_mip2 : register(u2); -RWTexture2D out_working_depth_mip3 : register(u3); -RWTexture2D out_working_depth_mip4 : register(u4); -RWTexture2D ao_term_and_edges : register(u0); - -#if XE_GTAO_FINAL_APPLY -RWTexture2D final_output : register(u0); // Native final R8_UNORM AO. -#else -RWTexture2D final_output : register(u0); -#endif - -#define XE_GTAO_NUMTHREADS_X 8 -#define XE_GTAO_NUMTHREADS_Y 8 - -// Hilbert mapping from https://www.shadertoy.com/view/3tB3z3, paired with an R2 sequence. -#define XE_HILBERT_LEVEL 6U -#define XE_HILBERT_WIDTH (1U << XE_HILBERT_LEVEL) -#define XE_HILBERT_AREA (XE_HILBERT_WIDTH * XE_HILBERT_WIDTH) -uint HilbertIndex(uint posX, uint posY) +float3 XeGTAO_LoadViewspaceNormal(uint2 pixCoord) { - uint index = 0U; - [unroll] for (uint curLevel = XE_HILBERT_WIDTH / 2U; curLevel > 0U; curLevel /= 2U) - { - uint regionX = (posX & curLevel) > 0U; - uint regionY = (posY & curLevel) > 0U; - index += curLevel * curLevel * ((3U * regionX) ^ regionY); - if (regionY == 0U) - { - if (regionX == 1U) - { - posX = XE_HILBERT_WIDTH - 1U - posX; - posY = XE_HILBERT_WIDTH - 1U - posY; - } - uint temp = posX; - posX = posY; - posY = temp; - } - } - return index; -} - -// MELE has no TAA; callers keep temporalIndex at zero to prevent boiling. -float2 SpatioTemporalNoise(uint2 pixCoord, uint temporalIndex) -{ - float2 noise; - uint index = HilbertIndex(pixCoord.x, pixCoord.y); - index += 288 * (temporalIndex % 64); // Empirical stride for XE_HILBERT_LEVEL=6. - // R2 sequence: http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/ - return float2(frac(0.5 + index * float2(0.75487766624669276005, 0.5698402909980532659114))); -} - -[numthreads(8, 8, 1)] // Each thread handles 2x2 pixels; dispatch in 16x16 blocks. - void prefilter_depths16x16_cs(uint2 dtid : SV_DispatchThreadID, uint2 gtid : SV_GroupThreadID) { - // tex0 = native half-resolution R24_UNORM_X8 depth captured at deinterleave. - XeGTAO_PrefilterDepths16x16(dtid, gtid, tex0, out_working_depth_mip0, out_working_depth_mip1, out_working_depth_mip2, out_working_depth_mip3, out_working_depth_mip4); - } - - [numthreads(XE_GTAO_NUMTHREADS_X, XE_GTAO_NUMTHREADS_Y, 1)] void main_pass_cs(uint2 dtid : SV_DispatchThreadID) -{ - // tex0 = R32F view-space-depth pyramid; tex1 = native packed R8G8 normals; smp = point-clamp. - // Decode packed view-space xy and reconstruct unit z. Camera-facing normals use negative z. - float2 nxy = tex1.Load(int3(dtid, 0)).xy * 2.0 - 1.0; + float2 nxy = tex1.Load(int3(pixCoord, 0)).xy * 2.0 - 1.0; float3 viewspaceNormal; viewspaceNormal.xy = nxy; viewspaceNormal.z = NORMAL_Z_SIGN * sqrt(saturate(1.0 - dot(nxy, nxy))); - viewspaceNormal = normalize(viewspaceNormal); - - XeGTAO_MainPass(dtid, SpatioTemporalNoise(dtid, 0), viewspaceNormal, tex0, smp, ao_term_and_edges); + return normalize(viewspaceNormal); } -[numthreads(XE_GTAO_NUMTHREADS_X, XE_GTAO_NUMTHREADS_Y, 1)] void denoise_pass_cs(uint2 dtid : SV_DispatchThreadID) { - // tex0 = packed AO and edges; smp = point-clamp. - const uint2 pix_coord_base = dtid * uint2(2, 1); // Two horizontal pixels per thread. - XeGTAO_Denoise(pix_coord_base, tex0, smp, final_output); -} +// tex0 = native half-resolution R24_UNORM_X8 depth captured at deinterleave. +#include "../Includes/XeGTAO.hlsl" diff --git a/Shaders/Mass Effect Legendary Edition/Luma_SMAA_impl.hlsl b/Shaders/Mass Effect Legendary Edition/Luma_SMAA_impl.hlsl index 3aaf23943..cea234530 100644 --- a/Shaders/Mass Effect Legendary Edition/Luma_SMAA_impl.hlsl +++ b/Shaders/Mass Effect Legendary Edition/Luma_SMAA_impl.hlsl @@ -20,71 +20,13 @@ cbuffer SmaaMetricsCB : register(b1) #define SMAA_PREDICATION_SCALE SmaaPredication.x #define SMAA_PREDICATION_THRESHOLD SmaaPredication.y #define SMAA_PREDICATION_STRENGTH SmaaPredication.z -#define SMAA_CUSTOM_SL -SamplerState LinearSampler : register(s0); -SamplerState PointSampler : register(s1); -#define SMAATexture2D(tex) Texture2D tex -#define SMAATexturePass2D(tex) tex -#define SMAASampleLevelZero(tex, coord) tex.SampleLevel(LinearSampler, coord, 0) -#define SMAASampleLevelZeroPoint(tex, coord) tex.SampleLevel(PointSampler, coord, 0) -#define SMAASampleLevelZeroOffset(tex, coord, offset) tex.SampleLevel(LinearSampler, coord, 0, offset) -#define SMAASample(tex, coord) tex.Sample(LinearSampler, coord) -#define SMAASamplePoint(tex, coord) tex.Sample(PointSampler, coord) -#define SMAASampleOffset(tex, coord, offset) tex.Sample(LinearSampler, coord, offset) -#define SMAA_FLATTEN [flatten] -#define SMAA_BRANCH [branch] -#define SMAATexture2DMS2(tex) Texture2DMS tex -#define SMAALoad(tex, pos, sample) tex.Load(pos, sample) -#define SMAAGather(tex, coord) tex.Gather(LinearSampler, coord, 0) -#include "../Includes/SMAA.hlsl" - -Texture2D tex0 : register(t0); -Texture2D tex1 : register(t1); -Texture2D tex2 : register(t2); - -void fullscreen_triangle(uint id, out float4 position, out float2 texcoord) -{ - texcoord = float2((id << 1) & 2, id & 2); - position = float4(texcoord * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); -} +#define SMAAGather(tex, coord) tex.Gather(LinearSampler, coord, 0) // SMAAEdgeDetection -void smaa_edge_detection_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset[3] : TEXCOORD1) -{ - fullscreen_triangle(id, position, texcoord); - SMAAEdgeDetectionVS(texcoord, offset); -} - -float2 smaa_edge_detection_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset[3] : TEXCOORD1) : SV_Target -{ - // tex0 = gamma post color; tex1 = scene depth for predication. - return SMAAColorEdgeDetectionPS(texcoord, offset, tex0, tex1); -} - -// SMAABlendingWeightCalculation -void smaa_blending_weight_calculation_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float2 pixcoord : TEXCOORD1, out float4 offset[3] : TEXCOORD2) -{ - fullscreen_triangle(id, position, texcoord); - SMAABlendingWeightCalculationVS(texcoord, pixcoord, offset); -} - -float4 smaa_blending_weight_calculation_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float2 pixcoord : TEXCOORD1, float4 offset[3] : TEXCOORD2) : SV_Target -{ - // tex0 = edges, tex1 = area, tex2 = search. - return SMAABlendingWeightCalculationPS(texcoord, pixcoord, offset, tex0, tex1, tex2, 0); -} +// tex0 = gamma post color; tex1 = scene depth for predication. // SMAANeighborhoodBlending -void smaa_neighborhood_blending_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset : TEXCOORD1) -{ - fullscreen_triangle(id, position, texcoord); - SMAANeighborhoodBlendingVS(texcoord, offset); -} +// tex0 = linear post color; tex1 = blend weights. Re-encode to the buffer's gamma. +#define SMAA_NEIGHBORHOOD_OUTPUT(color, position) color.rgb = linear_to_gamma(color.rgb, GCT_MIRROR); -float4 smaa_neighborhood_blending_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset : TEXCOORD1) : SV_Target -{ - // tex0 = linear post color; tex1 = blend weights. Re-encode to the buffer's gamma. - float4 color = SMAANeighborhoodBlendingPS(texcoord, offset, tex0, tex1); - color.rgb = linear_to_gamma(color.rgb, GCT_MIRROR); - return color; -} +#include "../Includes/SMAA_Passes.hlsl" diff --git a/Shaders/Mass Effect/Includes/GameBindings.hlsl b/Shaders/Mass Effect/Includes/GameBindings.hlsl index d00660b3c..ce265b45e 100644 --- a/Shaders/Mass Effect/Includes/GameBindings.hlsl +++ b/Shaders/Mass Effect/Includes/GameBindings.hlsl @@ -1,6 +1,8 @@ #ifndef LUMA_ME1_GAME_BINDINGS #define LUMA_ME1_GAME_BINDINGS +#include "../../Includes/DgVoodoo.hlsl" + // Bindings shared by the replaced passes (tonemap, gather, Bink video, GFx HUD), and nothing else. NO textures, // samplers or b4 row aliases: meaning is per pass (t0 = scene here, Y plane in Video; cb4[8] = DoFParams vs matrix // row). @@ -25,11 +27,6 @@ cbuffer PixelShaderConstants : register(b4) #define DgvMaskT2 DgvConstants[48] #define DgvFillT2 DgvConstants[49] -float4 ApplyDgvMask(float4 value, float4 mask, float4 fill) -{ - return asfloat((asuint(value) & asuint(mask)) | asuint(fill)); -} - // The grade's pow() as the original computes it. The tiny floor replaces the compiler's own log2(0) guard. float3 PowUE3(float3 base, float3 exponent) { diff --git a/Shaders/Mass Effect/Includes/MacLeodBoynton.hlsl b/Shaders/Mass Effect/Includes/MacLeodBoynton.hlsl deleted file mode 100644 index b73af6750..000000000 --- a/Shaders/Mass Effect/Includes/MacLeodBoynton.hlsl +++ /dev/null @@ -1,557 +0,0 @@ -// MacLeod–Boynton hue/purity model for linear RGB HDR colors. -// -// Derived from the RenoDX MacLeod–Boynton implementation (clshortfuse/renodx, MIT License, Copyright (c) 2025 -// Carlos Lopez Jr.), commit cd32113a98608e63027d40910cfe296a14dfe228: -// src/games/borderlandsgotyenhanced/macleod_boynton.hlsli constants, MB transforms, the gamut-ray purity solver -// src/games/borderlandsgotyenhanced/common.hlsli ApplyHueAndPurityGrading, its hue/purity emulation -// src/shaders/math.hlsl, deprecated.hlsl, color/rgb.hlsl Invert3x3, DivideSafe, SafeDivision, matrices, xyY -// The constants, the expressions and their order are RenoDX's; the organisation and the API are Luma's, and the -// unused purity modes are kept until the specialised entry points have proven what they need. -// _tools/bl1_bridge/mb_equiv compiles this file, the RenoDX source and the previous BL1 port side by side under -// fxc and holds their listings byte-identical. -// -// The model: RGB -> XYZ -> LMS (CIE 2006), hue as MacLeod–Boynton ratios r = L/(L+M), b = S/(L+M) around an -// adapted white, intensity anchored on T = L + M, purity as the fraction of the distance from white to the RGB -// gamut boundary along that ray. The hue/purity emulation takes a reference's ray direction and re-applies the -// target's own purity on it; with chrominance strength 0 only the hue direction moves. -// -// This file knows nothing about its caller. It takes a target, a reference, two strengths, a colour space and a -// white; which reference, how much of it, and where in a pipeline it runs are the caller's policy. -// -// Sections: A constants and transforms B MacLeod–Boynton coordinates C gamut-ray purity solver -// D hue/purity emulation E colour-space convenience wrappers - -#ifndef LUMA_MACLEOD_BOYNTON_HLSL -#define LUMA_MACLEOD_BOYNTON_HLSL - -namespace MacLeodBoynton -{ -// ------------------------------------------------------------------------------------------------------------ -// A. Constants and transforms -// ------------------------------------------------------------------------------------------------------------ - -// src/shaders/math.hlsl -float DivideSafe(float dividend, float divisor, float fallback) -{ - return (divisor == 0.f) ? fallback : (dividend / divisor); -} - -float3x3 Invert3x3(float3x3 m) -{ - float a = m[0][0], b = m[0][1], c = m[0][2]; - float d = m[1][0], e = m[1][1], f = m[1][2]; - float g = m[2][0], h = m[2][1], i = m[2][2]; - - float A = (e * i - f * h); - float B = -(d * i - f * g); - float C = (d * h - e * g); - float D = -(b * i - c * h); - float E = (a * i - c * g); - float F = -(a * h - b * g); - float G = (b * f - c * e); - float H = -(a * f - c * d); - float I = (a * e - b * d); - - float det = a * A + b * B + c * C; - float invDet = DivideSafe(1.0, det, 0.0); - - return float3x3( - A, D, G, - B, E, H, - C, F, I) * - invDet; -} - -// src/shaders/deprecated.hlsl -float SafeDivision(float quotient, float dividend, float fallback) -{ - return (dividend == 0.f) - ? fallback - : (quotient / dividend); -} - -// src/shaders/color/rgb.hlsl -static const float3x3 BT2020_TO_XYZ_MAT = float3x3( - 0.6369580483f, 0.1446169036f, 0.1688809752f, - 0.2627002120f, 0.6779980715f, 0.0593017165f, - 0.0000000000f, 0.0280726930f, 1.0609850577f); - -static const float3x3 XYZ_TO_BT2020_MAT = float3x3( - 1.7166511880f, -0.3556707838f, -0.2533662814f, - -0.6666843518f, 1.6164812366f, 0.0157685458f, - 0.0176398574f, -0.0427706133f, 0.9421031212f); - -static const float2 WHITE_POINT_D65 = float2(0.31272, 0.32903); - -// renodx::color::xyz::from::xyY -float3 XYZ_From_xyY(float3 xyY) -{ - float3 XYZ; - - XYZ.xz = float2(xyY.x, (1.f - xyY.xy.x - xyY.xy.y)) / xyY.y * xyY[2]; - - XYZ.y = xyY[2]; - - return XYZ; -} - -// The RGB working space the solver runs in: its matrices to and from XYZ. Passed by value; every current -// caller passes a compile-time constant and fxc folds it, so the abstraction costs nothing at runtime. -struct RGBColorSpace -{ - float3x3 rgbToXYZ; - float3x3 xyzToRGB; -}; - -static const RGBColorSpace RGB_BT2020 = {BT2020_TO_XYZ_MAT, XYZ_TO_BT2020_MAT}; - -// src/games/borderlandsgotyenhanced/macleod_boynton.hlsli -static const float3x3 XYZ_TO_LMS_2006 = float3x3( - 0.185082982238733f, 0.584081279463687f, -0.0240722415044404f, - -0.134433056469973f, 0.405752392775348f, 0.0358252602217631f, - 0.000789456671966863f, -0.000912281325916184f, 0.0198490812339463f); - -static const float3x3 LMS_TO_XYZ_2006 = Invert3x3(XYZ_TO_LMS_2006); - -static const float EPSILON = 1e-20f; -static const float INTERVAL_MAX = 1e30f; -static const float MB_NEAR_WHITE_EPSILON = 1e-14f; - -static const int MB_PURITY_MODE_DISTANCE = 0; -static const int MB_PURITY_MODE_NORMALIZED = 1; -static const int MB_PURITY_MODE_SCALE_NEUTWO = 2; -static const int MB_PURITY_ADJUST_NONE = 0; -static const int MB_PURITY_ADJUST_CLIP_MAX = 1; -static const float ONE_MINUS_EPSILON = 1.f - 1e-6f; - -// ------------------------------------------------------------------------------------------------------------ -// B. MacLeod–Boynton coordinates -// ------------------------------------------------------------------------------------------------------------ - -// Neutwo (x / sqrt(x^2 + 1)) and its inverse. -// Used to shape purity scaling with a smooth shoulder near gamut edge. -float Neutwo(float x) -{ - float denominator_squared = mad(x, x, 1.f); - return x * rsqrt(denominator_squared); -} - -float InverseNeutwo(float x) -{ - float clamped = min(max(x, 0.f), ONE_MINUS_EPSILON); - float denominator_squared = mad(-clamped, clamped, 1.f); - return clamped * rsqrt(denominator_squared); -} - -// MB coordinate from LMS. -// r = L / (L + M), b = S / (L + M) -// T = (L + M) is intensity anchor and is preserved while changing purity. -float2 MB_From_LMS(float3 lms) -{ - float t = lms.x + lms.y; - - if (t <= 0.f) - { - return float2(0.f, 0.f); - } - - return float2( - DivideSafe(lms.x, t, 0.f), - DivideSafe(lms.z, t, 0.f)); -} - -// Reconstruct LMS from MB coordinates while holding T = L + M fixed. -float3 LMS_From_MB_T(float2 mb, float t) -{ - float r = mb.x; - float b = mb.y; - return float3(r * t, (1.f - r) * t, b * t); -} - -// Default adaptation white in MB space. -float2 MB_White_D65() -{ - float3 d65_xyz = XYZ_From_xyY(float3(WHITE_POINT_D65, 1.f)); - float3 d65_lms = mul(XYZ_TO_LMS_2006, d65_xyz); - return MB_From_LMS(d65_lms); -} - -// ------------------------------------------------------------------------------------------------------------ -// C. Gamut-ray purity solver -// ------------------------------------------------------------------------------------------------------------ - -// Half-space constraint for one channel in affine form: -// rgb(t) = a * t + b, and we require rgb(t) >= 0. -// Returns interval [lo, hi] where that channel is valid. -void IntervalLower0(float a, float b, out float lo, out float hi) -{ - if (abs(a) < EPSILON) - { - if (b >= 0.f) - { - lo = -INTERVAL_MAX; - hi = INTERVAL_MAX; - } - else - { - lo = 1.f; - hi = 0.f; - } - return; - } - - float t0 = DivideSafe(-b, a, 0.f); - - if (a > 0.f) - { - lo = t0; - hi = INTERVAL_MAX; - } - else - { - lo = -INTERVAL_MAX; - hi = t0; - } -} - -struct PurityResult -{ - float3 rgbOut; - float3 rgbT0; - float3 rgbEdgeGamut; - - // Max reachable t before first channel becomes negative. - float tMaxGamut; - // Applied t after user control mapping. - float tFinal; - - // Purity for input color in normalized 0..1 form (1 / tMax for input at t=1). - float purityCur01; - // Requested normalized purity (user input). - float purity01_in; - // Effective normalized purity after shaping curve. - float purity01_used; -}; - -// Main solver. -// -// There are three control modes: -// - purity_input_mode == MB_PURITY_MODE_NORMALIZED: -// purity_value is 0..1 "fraction of available purity headroom". -// - purity_input_mode == MB_PURITY_MODE_DISTANCE: -// purity_value is direct MB ray distance t. -// - purity_input_mode == MB_PURITY_MODE_SCALE_NEUTWO: -// purity_value is a scale multiplier in Neutwo inverse space (1 = unchanged). -// -// Optional adjust mode: -// - MB_PURITY_ADJUST_NONE: -// use selected input mode as-is. -// - MB_PURITY_ADJUST_CLIP_MAX: -// force max purity for this hue/gamut (tFinal = tMaxGamut). -// -// Important behavior: -// - Constraint is gamut-only (RGB >= 0), no arbitrary <= 1 clamp. -// - This allows scene-linear HDR values above 1.0 to pass through naturally. -PurityResult SolvePurity(float3 rgb_linear, float purity_value, int purity_input_mode, - float curve_gamma, float2 mb_white_override, float t_min, - int purity_adjust_mode, float3x3 rgb_to_xyz_mat, float3x3 xyz_to_rgb_mat) -{ - PurityResult output; - - output.purity01_in = 0.f; - - // 1) RGB -> XYZ -> LMS - float3 xyz = mul(rgb_to_xyz_mat, rgb_linear); - float3 lms = mul(XYZ_TO_LMS_2006, xyz); - - // T = L + M is the intensity anchor for MB purity moves. - float t = lms.x + lms.y; - - if (t <= t_min) - { - output.rgbOut = rgb_linear; - output.rgbT0 = rgb_linear; - output.rgbEdgeGamut = rgb_linear; - output.tMaxGamut = 0.f; - output.purityCur01 = 0.f; - output.purity01_used = 0.f; - return output; - } - - float2 white = - (mb_white_override.x >= 0.f && mb_white_override.y >= 0.f) ? mb_white_override : MB_White_D65(); - - // 2) MB ray from adapted white to input hue. - float2 mb0 = MB_From_LMS(lms); - float2 direction = mb0 - white; - - if (dot(direction, direction) < MB_NEAR_WHITE_EPSILON) - { - output.rgbOut = rgb_linear; - output.rgbT0 = rgb_linear; - output.rgbEdgeGamut = rgb_linear; - output.tMaxGamut = 0.f; - output.tFinal = 0.f; - output.purityCur01 = 0.f; - output.purity01_used = 0.f; - return output; - } - - float3 lms_t0 = LMS_From_MB_T(white, t); - float3 xyz_t0 = mul(LMS_TO_XYZ_2006, lms_t0); - float3 rgb_t0 = mul(xyz_to_rgb_mat, xyz_t0); - output.rgbT0 = rgb_t0; - - // 4) Affine form of RGB along the MB ray at fixed T. - // t = 0 -> white in MB space, t = 1 -> original input. - float3 a = rgb_linear - rgb_t0; - - // 5) Intersect channel constraints RGB(t) >= 0 across R,G,B. - float t_lo = 0.f; - float t_hi = INTERVAL_MAX; - - float lo; - float hi; - - IntervalLower0(a.x, rgb_t0.x, lo, hi); - t_lo = max(t_lo, lo); - t_hi = min(t_hi, hi); - - IntervalLower0(a.y, rgb_t0.y, lo, hi); - t_lo = max(t_lo, lo); - t_hi = min(t_hi, hi); - - IntervalLower0(a.z, rgb_t0.z, lo, hi); - t_lo = max(t_lo, lo); - t_hi = min(t_hi, hi); - - if (t_hi < t_lo) - { - output.rgbOut = max(rgb_linear, 0.f); - output.rgbEdgeGamut = rgb_t0; - output.tMaxGamut = 0.f; - output.tFinal = 0.f; - output.purityCur01 = 0.f; - output.purity01_used = 0.f; - return output; - } - - float t_max = max(0.f, t_hi); - output.tMaxGamut = t_max; - - // Current normalized purity for the input (which is at t = 1). - float p_cur = (t_max > EPSILON) ? DivideSafe(1.f, t_max, 0.f) : 0.f; - output.purityCur01 = saturate(p_cur); - - // Useful validation output: point on the gamut edge for this MB ray. - float3 rgb_edge = a * t_max + rgb_t0; - output.rgbEdgeGamut = rgb_edge; - - float t_final = 0.f; - - if (purity_adjust_mode == MB_PURITY_ADJUST_CLIP_MAX) - { - output.purity01_used = 1.f; - output.purity01_in = 1.f; - t_final = t_max; - } - else - { - if (purity_input_mode == MB_PURITY_MODE_NORMALIZED) - { - // Normalized mode: user operates in [0,1] of available gamut headroom. - output.purity01_in = saturate(purity_value); - float gamma = max(curve_gamma, 1e-6f); - float purity01 = pow(output.purity01_in, gamma); - output.purity01_used = purity01; - t_final = purity01 * t_max; - } - else if (purity_input_mode == MB_PURITY_MODE_SCALE_NEUTWO) - { - // Scale mode: user controls a saturation multiplier around current purity. - // 1 = no change, >1 increases, <1 decreases, with a smooth shoulder near edge. - float saturation_scale = max(purity_value, 0.f); - float purity01_target; - if (output.purityCur01 >= ONE_MINUS_EPSILON && saturation_scale >= 1.f) - { - purity01_target = 1.f; - } - else - { - float z = InverseNeutwo(output.purityCur01); - float z_scaled = z * saturation_scale; - purity01_target = saturate(Neutwo(z_scaled)); - } - output.purity01_in = output.purityCur01; - output.purity01_used = purity01_target; - t_final = purity01_target * t_max; - } - else - { - // Distance mode: user value is interpreted as MB ray distance t. - float t_user = max(purity_value, 0.f); - t_final = min(t_user, t_max); - output.purity01_used = - (t_max > EPSILON) ? saturate(DivideSafe(t_final, t_max, 0.f)) : 0.f; - output.purity01_in = output.purity01_used; - } - } - - output.tFinal = t_final; - - float2 mb_final = white + t_final * direction; - - float3 lms_final = LMS_From_MB_T(mb_final, t); - float3 xyz_final = mul(LMS_TO_XYZ_2006, lms_final); - float3 rgb_final = mul(xyz_to_rgb_mat, xyz_final); - - // Clean up float math - output.rgbOut = max(rgb_final, 0.f); - - return output; -} - -// Normalized-purity solve in a given RGB space: purity01 = 0 is the adapted white, 1 the gamut boundary. -PurityResult ApplyPurity(float3 rgb_linear, float purity01, RGBColorSpace colorSpace, float curve_gamma = 1.f, - float2 mb_white_override = float2(-1.f, -1.f), float t_min = 1e-6f, - int purity_adjust_mode = MB_PURITY_ADJUST_NONE) -{ - return SolvePurity(rgb_linear, purity01, MB_PURITY_MODE_NORMALIZED, curve_gamma, - mb_white_override, t_min, purity_adjust_mode, colorSpace.rgbToXYZ, - colorSpace.xyzToRGB); -} - -// ------------------------------------------------------------------------------------------------------------ -// D. Hue/purity emulation -// ------------------------------------------------------------------------------------------------------------ - -// Moves the target's MB hue direction toward the reference's by `hue_strength` while re-applying the target's -// own purity on the new direction, then optionally moves that purity toward the reference's by -// `chrominance_strength`. T = L + M of the target is preserved throughout. A white override of (-1, -1) -// selects D65. This is the hue block of RenoDX's ApplyHueAndPurityGrading; its saturation / dechroma / -// highlight-saturation tail is not part of the model and is not carried. -float3 HueAndPurityEmulation(float3 ungraded, float3 reference, float hue_strength, float chrominance_strength, - RGBColorSpace colorSpace, float2 mb_white_override, float t_min) -{ - float3 color = ungraded; - if (hue_strength == 0.f && chrominance_strength == 0.f) - { - return color; - } - - const float kNearWhiteEpsilon = MB_NEAR_WHITE_EPSILON; - const float2 white = (mb_white_override.x >= 0.f && mb_white_override.y >= 0.f) - ? mb_white_override - : MB_White_D65(); - - float color_purity01 = ApplyPurity( - color, 1.f, colorSpace, 1.f, mb_white_override, t_min) - .purityCur01; - - // MB hue + purity emulation (analog of OkLab hue/chrominance section). - if (hue_strength != 0.f || chrominance_strength != 0.f) - { - float reference_purity01 = ApplyPurity( - reference, 1.f, colorSpace, 1.f, mb_white_override, t_min) - .purityCur01; - - float purity_current = color_purity01; - float purity_ratio = 1.f; - float3 hue_seed = color; - - if (hue_strength != 0.f) - { - float3 target_lms = mul(XYZ_TO_LMS_2006, - mul(colorSpace.rgbToXYZ, color)); - float3 reference_lms = mul(XYZ_TO_LMS_2006, - mul(colorSpace.rgbToXYZ, reference)); - - float target_t = target_lms.x + target_lms.y; - if (target_t > t_min) - { - float2 target_direction = MB_From_LMS(target_lms) - white; - float2 reference_direction = MB_From_LMS(reference_lms) - white; - - float target_len_sq = dot(target_direction, target_direction); - float reference_len_sq = dot(reference_direction, reference_direction); - - if (target_len_sq > kNearWhiteEpsilon || reference_len_sq > kNearWhiteEpsilon) - { - float2 target_unit = (target_len_sq > kNearWhiteEpsilon) - ? target_direction * rsqrt(target_len_sq) - : float2(0.f, 0.f); - float2 reference_unit = (reference_len_sq > kNearWhiteEpsilon) - ? reference_direction * rsqrt(reference_len_sq) - : target_unit; - - if (target_len_sq <= kNearWhiteEpsilon) - { - target_unit = reference_unit; - } - - float2 blended_unit = lerp(target_unit, reference_unit, hue_strength); - float blended_len_sq = dot(blended_unit, blended_unit); - if (blended_len_sq <= kNearWhiteEpsilon) - { - blended_unit = (hue_strength >= 0.5f) ? reference_unit : target_unit; - blended_len_sq = dot(blended_unit, blended_unit); - } - blended_unit *= rsqrt(max(blended_len_sq, 1e-20f)); - - float seed_len = sqrt(max(target_len_sq, 0.f)); - if (seed_len <= 1e-6f) - { - seed_len = sqrt(max(reference_len_sq, 0.f)); - } - seed_len = max(seed_len, 1e-6f); - - hue_seed = mul( - colorSpace.xyzToRGB, - mul(LMS_TO_XYZ_2006, - LMS_From_MB_T(white + blended_unit * seed_len, target_t))); - - float purity_post = ApplyPurity( - hue_seed, 1.f, colorSpace, 1.f, mb_white_override, t_min) - .purityCur01; - purity_ratio = SafeDivision(purity_current, purity_post, 1.f); - purity_current = purity_post; - } - } - } - - if (chrominance_strength != 0.f) - { - float target_purity_ratio = SafeDivision(reference_purity01, purity_current, 1.f); - purity_ratio = lerp(purity_ratio, target_purity_ratio, chrominance_strength); - } - - float applied_purity01 = saturate(purity_current * max(purity_ratio, 0.f)); - color = ApplyPurity( - hue_seed, applied_purity01, colorSpace, 1.f, mb_white_override, t_min) - .rgbOut; - color_purity01 = applied_purity01; - } - - return color; -} - -// ------------------------------------------------------------------------------------------------------------ -// E. Colour-space convenience wrappers (compile-time fixed: BT.2020, D65, the solver's 1e-7 T floor) -// ------------------------------------------------------------------------------------------------------------ - -float3 HueAndPurityEmulationBT2020(float3 ungraded_bt2020, float3 reference_bt2020, float hue_strength, float chrominance_strength) -{ - return HueAndPurityEmulation(ungraded_bt2020, reference_bt2020, hue_strength, chrominance_strength, RGB_BT2020, float2(-1.f, -1.f), 1e-7f); -} - -// Reference hue direction on the target's own purity and T; nothing of the reference's purity is taken. -// Deliberately a plain call: a hand-specialised copy (reference purity solve and chrominance branch removed) -// compiles to byte-identical DXBC, since fxc already eliminates that dead work, so it would only duplicate -// the hue block. Measured on both BL1 wrappers and held bit-exact over random pairs before being dropped. -float3 HueOnlyBT2020(float3 ungraded_bt2020, float3 reference_bt2020) -{ - return HueAndPurityEmulationBT2020(ungraded_bt2020, reference_bt2020, 1.f, 0.f); -} -} // namespace MacLeodBoynton - -#endif // LUMA_MACLEOD_BOYNTON_HLSL diff --git a/Shaders/Mass Effect/Luma_ME1_Sharpen.hlsl b/Shaders/Mass Effect/Luma_ME1_Sharpen.hlsl deleted file mode 100644 index 11773b93f..000000000 --- a/Shaders/Mass Effect/Luma_ME1_Sharpen.hlsl +++ /dev/null @@ -1,19 +0,0 @@ -// RCAS on the SMAA output (BL2/TPS/TW2 shape): after SMAA's neighborhood blend, on the gamma canvas, written back -// before the HUD and the composition. paperWhite 1.0; RCAS_LIMIT bounds the lobe (bloom no longer clamps at 1.0). - -#include "../Includes/RCAS.hlsl" - -cbuffer SharpenCB : register(b0) -{ - float4 SharpenParams; // (width, height, sharpness[0..1], unused) -} - -Texture2D tex0 : register(t0); // SMAA output (gamma canvas) -Texture2D dummyMV : register(t1); // unused (dynamicSharpening = false) - -float4 sharpen_ps(float4 pos : SV_Position) : SV_Target -{ - int2 p = int2(pos.xy); - int2 maxPixel = int2((int)SharpenParams.x - 1, (int)SharpenParams.y - 1); - return RCAS(p, int2(0, 0), maxPixel, SharpenParams.z, tex0, dummyMV, 1.0, false, (float4)0, false); -} diff --git a/Shaders/Mass Effect/Luma_ME1_Tonemap.hlsl b/Shaders/Mass Effect/Luma_ME1_Tonemap.hlsl index 56f4a291f..8e907b691 100644 --- a/Shaders/Mass Effect/Luma_ME1_Tonemap.hlsl +++ b/Shaders/Mass Effect/Luma_ME1_Tonemap.hlsl @@ -28,7 +28,7 @@ #include "../Includes/ColorGradingLUT.hlsl" // SimpleGamutClip #include "../Includes/DICE.hlsl" // DICETonemap / DefaultDICESettings #include "../Includes/Reinhard.hlsl" // Reinhard::ReinhardPiecewise, the soft hue reference -#include "Includes/MacLeodBoynton.hlsl" // MacLeodBoynton::HueOnlyBT2020. Byte-identical copy of the BL GOTY production model: do not edit here, sync it from "Borderlands GOTY Enhanced/Includes" +#include "../Includes/MacLeodBoynton.hlsl" // MacLeodBoynton::HueOnlyBT2020 // clang-format on #include "Includes/GameBindings.hlsl" // b3/b4, the dgVoodoo masks, ApplyDgvMask, PowUE3, DoFBlurAmount diff --git a/Shaders/Mass Effect/Luma_SMAA_impl.hlsl b/Shaders/Mass Effect/Luma_SMAA_impl.hlsl index c2494bf4c..1c15ad986 100644 --- a/Shaders/Mass Effect/Luma_SMAA_impl.hlsl +++ b/Shaders/Mass Effect/Luma_SMAA_impl.hlsl @@ -24,69 +24,14 @@ cbuffer SmaaMetricsCB : register(b1) // 0.10, silhouette = x (1-STRENGTH) = 0.05 (ULTRA base). SMAA_PREDICATION_THRESHOLD 0.5 is scale-free; tune the CS. #define SMAA_PREDICATION_STRENGTH 0.5 #define SMAA_PREDICATION_THRESHOLD 0.5 -#define SMAA_CUSTOM_SL -SamplerState LinearSampler : register(s0); -SamplerState PointSampler : register(s1); -#define SMAATexture2D(tex) Texture2D tex -#define SMAATexturePass2D(tex) tex -#define SMAASampleLevelZero(tex, coord) tex.SampleLevel(LinearSampler, coord, 0) -#define SMAASampleLevelZeroPoint(tex, coord) tex.SampleLevel(PointSampler, coord, 0) -#define SMAASampleLevelZeroOffset(tex, coord, offset) tex.SampleLevel(LinearSampler, coord, 0, offset) -#define SMAASample(tex, coord) tex.Sample(LinearSampler, coord) -#define SMAASamplePoint(tex, coord) tex.Sample(PointSampler, coord) -#define SMAASampleOffset(tex, coord, offset) tex.Sample(LinearSampler, coord, offset) -#define SMAA_FLATTEN [flatten] -#define SMAA_BRANCH [branch] -#define SMAATexture2DMS2(tex) Texture2DMS tex -#define SMAALoad(tex, pos, sample) tex.Load(pos, sample) -#define SMAAGather(tex, coord) tex.Gather(LinearSampler, coord, 0) -#include "../Includes/SMAA.hlsl" +#define SMAAGather(tex, coord) tex.Gather(LinearSampler, coord, 0) -Texture2D tex0 : register(t0); -Texture2D tex1 : register(t1); -Texture2D tex2 : register(t2); +// SMAAEdgeDetection +// tex0 = colorTexGamma (gamma-encoded graded canvas) +// tex1 = predicationTex (edge-ness in [0,1]; null fallback -> reads 0, scale 1.0 = plain ULTRA threshold) -void fullscreen_triangle(uint id, out float4 position, out float2 texcoord) -{ - texcoord = float2((id << 1) & 2, id & 2); - position = float4(texcoord * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); -} - -void smaa_edge_detection_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset[3] : TEXCOORD1) -{ - fullscreen_triangle(id, position, texcoord); - SMAAEdgeDetectionVS(texcoord, offset); -} +// SMAANeighborhoodBlending +// tex0 = colorTex (linear copy), tex1 = blendTex. Re-encode to the canvas' gamma. +#define SMAA_NEIGHBORHOOD_OUTPUT(color, position) color.rgb = linear_to_gamma(color.rgb, GCT_MIRROR); -float2 smaa_edge_detection_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset[3] : TEXCOORD1) : SV_Target -{ - // tex0 = colorTexGamma (gamma-encoded graded canvas) - // tex1 = predicationTex (edge-ness in [0,1]; null fallback -> reads 0, scale 1.0 = plain ULTRA threshold) - return SMAAColorEdgeDetectionPS(texcoord, offset, tex0, tex1); -} - -void smaa_blending_weight_calculation_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float2 pixcoord : TEXCOORD1, out float4 offset[3] : TEXCOORD2) -{ - fullscreen_triangle(id, position, texcoord); - SMAABlendingWeightCalculationVS(texcoord, pixcoord, offset); -} - -float4 smaa_blending_weight_calculation_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float2 pixcoord : TEXCOORD1, float4 offset[3] : TEXCOORD2) : SV_Target -{ - // tex0 = edgesTex, tex1 = areaTex, tex2 = searchTex - return SMAABlendingWeightCalculationPS(texcoord, pixcoord, offset, tex0, tex1, tex2, 0); -} - -void smaa_neighborhood_blending_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset : TEXCOORD1) -{ - fullscreen_triangle(id, position, texcoord); - SMAANeighborhoodBlendingVS(texcoord, offset); -} - -float4 smaa_neighborhood_blending_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset : TEXCOORD1) : SV_Target -{ - // tex0 = colorTex (linear copy), tex1 = blendTex. Re-encode to the canvas' gamma. - float4 color = SMAANeighborhoodBlendingPS(texcoord, offset, tex0, tex1); - color.rgb = linear_to_gamma(color.rgb, GCT_MIRROR); - return color; -} +#include "../Includes/SMAA_Passes.hlsl" diff --git a/Shaders/Medal of Honor Airborne/Includes/GameBindings.hlsl b/Shaders/Medal of Honor Airborne/Includes/GameBindings.hlsl index 701dbc5ea..5e9cf1c58 100644 --- a/Shaders/Medal of Honor Airborne/Includes/GameBindings.hlsl +++ b/Shaders/Medal of Honor Airborne/Includes/GameBindings.hlsl @@ -1,6 +1,8 @@ #ifndef LUMA_MOHA_GAME_BINDINGS #define LUMA_MOHA_GAME_BINDINGS +#include "../../Includes/DgVoodoo.hlsl" + // Medal of Honor: Airborne - bindings the replaced passes share, and nothing else. Included by the tonemap, the // DoF/bloom gather and the video pass, which need them but none of the tonemap itself. // Deliberately holds no textures or samplers, and no alias for a game-content cbuffer row: slot and row meaning is @@ -29,11 +31,6 @@ cbuffer PixelShaderConstants : register(b4) #define DgvMaskT2 DgvConstants[48] #define DgvFillT2 DgvConstants[49] -float4 ApplyDgvMask(float4 value, float4 mask, float4 fill) -{ - return asfloat((asuint(value) & asuint(mask)) | asuint(fill)); -} - // The grade's pow() as the original computes it. The tiny floor replaces the compiler's own log2(0) guard. float3 PowUE3(float3 base, float3 exponent) { diff --git a/Shaders/Medal of Honor Airborne/Includes/MacLeodBoynton.hlsl b/Shaders/Medal of Honor Airborne/Includes/MacLeodBoynton.hlsl deleted file mode 100644 index b73af6750..000000000 --- a/Shaders/Medal of Honor Airborne/Includes/MacLeodBoynton.hlsl +++ /dev/null @@ -1,557 +0,0 @@ -// MacLeod–Boynton hue/purity model for linear RGB HDR colors. -// -// Derived from the RenoDX MacLeod–Boynton implementation (clshortfuse/renodx, MIT License, Copyright (c) 2025 -// Carlos Lopez Jr.), commit cd32113a98608e63027d40910cfe296a14dfe228: -// src/games/borderlandsgotyenhanced/macleod_boynton.hlsli constants, MB transforms, the gamut-ray purity solver -// src/games/borderlandsgotyenhanced/common.hlsli ApplyHueAndPurityGrading, its hue/purity emulation -// src/shaders/math.hlsl, deprecated.hlsl, color/rgb.hlsl Invert3x3, DivideSafe, SafeDivision, matrices, xyY -// The constants, the expressions and their order are RenoDX's; the organisation and the API are Luma's, and the -// unused purity modes are kept until the specialised entry points have proven what they need. -// _tools/bl1_bridge/mb_equiv compiles this file, the RenoDX source and the previous BL1 port side by side under -// fxc and holds their listings byte-identical. -// -// The model: RGB -> XYZ -> LMS (CIE 2006), hue as MacLeod–Boynton ratios r = L/(L+M), b = S/(L+M) around an -// adapted white, intensity anchored on T = L + M, purity as the fraction of the distance from white to the RGB -// gamut boundary along that ray. The hue/purity emulation takes a reference's ray direction and re-applies the -// target's own purity on it; with chrominance strength 0 only the hue direction moves. -// -// This file knows nothing about its caller. It takes a target, a reference, two strengths, a colour space and a -// white; which reference, how much of it, and where in a pipeline it runs are the caller's policy. -// -// Sections: A constants and transforms B MacLeod–Boynton coordinates C gamut-ray purity solver -// D hue/purity emulation E colour-space convenience wrappers - -#ifndef LUMA_MACLEOD_BOYNTON_HLSL -#define LUMA_MACLEOD_BOYNTON_HLSL - -namespace MacLeodBoynton -{ -// ------------------------------------------------------------------------------------------------------------ -// A. Constants and transforms -// ------------------------------------------------------------------------------------------------------------ - -// src/shaders/math.hlsl -float DivideSafe(float dividend, float divisor, float fallback) -{ - return (divisor == 0.f) ? fallback : (dividend / divisor); -} - -float3x3 Invert3x3(float3x3 m) -{ - float a = m[0][0], b = m[0][1], c = m[0][2]; - float d = m[1][0], e = m[1][1], f = m[1][2]; - float g = m[2][0], h = m[2][1], i = m[2][2]; - - float A = (e * i - f * h); - float B = -(d * i - f * g); - float C = (d * h - e * g); - float D = -(b * i - c * h); - float E = (a * i - c * g); - float F = -(a * h - b * g); - float G = (b * f - c * e); - float H = -(a * f - c * d); - float I = (a * e - b * d); - - float det = a * A + b * B + c * C; - float invDet = DivideSafe(1.0, det, 0.0); - - return float3x3( - A, D, G, - B, E, H, - C, F, I) * - invDet; -} - -// src/shaders/deprecated.hlsl -float SafeDivision(float quotient, float dividend, float fallback) -{ - return (dividend == 0.f) - ? fallback - : (quotient / dividend); -} - -// src/shaders/color/rgb.hlsl -static const float3x3 BT2020_TO_XYZ_MAT = float3x3( - 0.6369580483f, 0.1446169036f, 0.1688809752f, - 0.2627002120f, 0.6779980715f, 0.0593017165f, - 0.0000000000f, 0.0280726930f, 1.0609850577f); - -static const float3x3 XYZ_TO_BT2020_MAT = float3x3( - 1.7166511880f, -0.3556707838f, -0.2533662814f, - -0.6666843518f, 1.6164812366f, 0.0157685458f, - 0.0176398574f, -0.0427706133f, 0.9421031212f); - -static const float2 WHITE_POINT_D65 = float2(0.31272, 0.32903); - -// renodx::color::xyz::from::xyY -float3 XYZ_From_xyY(float3 xyY) -{ - float3 XYZ; - - XYZ.xz = float2(xyY.x, (1.f - xyY.xy.x - xyY.xy.y)) / xyY.y * xyY[2]; - - XYZ.y = xyY[2]; - - return XYZ; -} - -// The RGB working space the solver runs in: its matrices to and from XYZ. Passed by value; every current -// caller passes a compile-time constant and fxc folds it, so the abstraction costs nothing at runtime. -struct RGBColorSpace -{ - float3x3 rgbToXYZ; - float3x3 xyzToRGB; -}; - -static const RGBColorSpace RGB_BT2020 = {BT2020_TO_XYZ_MAT, XYZ_TO_BT2020_MAT}; - -// src/games/borderlandsgotyenhanced/macleod_boynton.hlsli -static const float3x3 XYZ_TO_LMS_2006 = float3x3( - 0.185082982238733f, 0.584081279463687f, -0.0240722415044404f, - -0.134433056469973f, 0.405752392775348f, 0.0358252602217631f, - 0.000789456671966863f, -0.000912281325916184f, 0.0198490812339463f); - -static const float3x3 LMS_TO_XYZ_2006 = Invert3x3(XYZ_TO_LMS_2006); - -static const float EPSILON = 1e-20f; -static const float INTERVAL_MAX = 1e30f; -static const float MB_NEAR_WHITE_EPSILON = 1e-14f; - -static const int MB_PURITY_MODE_DISTANCE = 0; -static const int MB_PURITY_MODE_NORMALIZED = 1; -static const int MB_PURITY_MODE_SCALE_NEUTWO = 2; -static const int MB_PURITY_ADJUST_NONE = 0; -static const int MB_PURITY_ADJUST_CLIP_MAX = 1; -static const float ONE_MINUS_EPSILON = 1.f - 1e-6f; - -// ------------------------------------------------------------------------------------------------------------ -// B. MacLeod–Boynton coordinates -// ------------------------------------------------------------------------------------------------------------ - -// Neutwo (x / sqrt(x^2 + 1)) and its inverse. -// Used to shape purity scaling with a smooth shoulder near gamut edge. -float Neutwo(float x) -{ - float denominator_squared = mad(x, x, 1.f); - return x * rsqrt(denominator_squared); -} - -float InverseNeutwo(float x) -{ - float clamped = min(max(x, 0.f), ONE_MINUS_EPSILON); - float denominator_squared = mad(-clamped, clamped, 1.f); - return clamped * rsqrt(denominator_squared); -} - -// MB coordinate from LMS. -// r = L / (L + M), b = S / (L + M) -// T = (L + M) is intensity anchor and is preserved while changing purity. -float2 MB_From_LMS(float3 lms) -{ - float t = lms.x + lms.y; - - if (t <= 0.f) - { - return float2(0.f, 0.f); - } - - return float2( - DivideSafe(lms.x, t, 0.f), - DivideSafe(lms.z, t, 0.f)); -} - -// Reconstruct LMS from MB coordinates while holding T = L + M fixed. -float3 LMS_From_MB_T(float2 mb, float t) -{ - float r = mb.x; - float b = mb.y; - return float3(r * t, (1.f - r) * t, b * t); -} - -// Default adaptation white in MB space. -float2 MB_White_D65() -{ - float3 d65_xyz = XYZ_From_xyY(float3(WHITE_POINT_D65, 1.f)); - float3 d65_lms = mul(XYZ_TO_LMS_2006, d65_xyz); - return MB_From_LMS(d65_lms); -} - -// ------------------------------------------------------------------------------------------------------------ -// C. Gamut-ray purity solver -// ------------------------------------------------------------------------------------------------------------ - -// Half-space constraint for one channel in affine form: -// rgb(t) = a * t + b, and we require rgb(t) >= 0. -// Returns interval [lo, hi] where that channel is valid. -void IntervalLower0(float a, float b, out float lo, out float hi) -{ - if (abs(a) < EPSILON) - { - if (b >= 0.f) - { - lo = -INTERVAL_MAX; - hi = INTERVAL_MAX; - } - else - { - lo = 1.f; - hi = 0.f; - } - return; - } - - float t0 = DivideSafe(-b, a, 0.f); - - if (a > 0.f) - { - lo = t0; - hi = INTERVAL_MAX; - } - else - { - lo = -INTERVAL_MAX; - hi = t0; - } -} - -struct PurityResult -{ - float3 rgbOut; - float3 rgbT0; - float3 rgbEdgeGamut; - - // Max reachable t before first channel becomes negative. - float tMaxGamut; - // Applied t after user control mapping. - float tFinal; - - // Purity for input color in normalized 0..1 form (1 / tMax for input at t=1). - float purityCur01; - // Requested normalized purity (user input). - float purity01_in; - // Effective normalized purity after shaping curve. - float purity01_used; -}; - -// Main solver. -// -// There are three control modes: -// - purity_input_mode == MB_PURITY_MODE_NORMALIZED: -// purity_value is 0..1 "fraction of available purity headroom". -// - purity_input_mode == MB_PURITY_MODE_DISTANCE: -// purity_value is direct MB ray distance t. -// - purity_input_mode == MB_PURITY_MODE_SCALE_NEUTWO: -// purity_value is a scale multiplier in Neutwo inverse space (1 = unchanged). -// -// Optional adjust mode: -// - MB_PURITY_ADJUST_NONE: -// use selected input mode as-is. -// - MB_PURITY_ADJUST_CLIP_MAX: -// force max purity for this hue/gamut (tFinal = tMaxGamut). -// -// Important behavior: -// - Constraint is gamut-only (RGB >= 0), no arbitrary <= 1 clamp. -// - This allows scene-linear HDR values above 1.0 to pass through naturally. -PurityResult SolvePurity(float3 rgb_linear, float purity_value, int purity_input_mode, - float curve_gamma, float2 mb_white_override, float t_min, - int purity_adjust_mode, float3x3 rgb_to_xyz_mat, float3x3 xyz_to_rgb_mat) -{ - PurityResult output; - - output.purity01_in = 0.f; - - // 1) RGB -> XYZ -> LMS - float3 xyz = mul(rgb_to_xyz_mat, rgb_linear); - float3 lms = mul(XYZ_TO_LMS_2006, xyz); - - // T = L + M is the intensity anchor for MB purity moves. - float t = lms.x + lms.y; - - if (t <= t_min) - { - output.rgbOut = rgb_linear; - output.rgbT0 = rgb_linear; - output.rgbEdgeGamut = rgb_linear; - output.tMaxGamut = 0.f; - output.purityCur01 = 0.f; - output.purity01_used = 0.f; - return output; - } - - float2 white = - (mb_white_override.x >= 0.f && mb_white_override.y >= 0.f) ? mb_white_override : MB_White_D65(); - - // 2) MB ray from adapted white to input hue. - float2 mb0 = MB_From_LMS(lms); - float2 direction = mb0 - white; - - if (dot(direction, direction) < MB_NEAR_WHITE_EPSILON) - { - output.rgbOut = rgb_linear; - output.rgbT0 = rgb_linear; - output.rgbEdgeGamut = rgb_linear; - output.tMaxGamut = 0.f; - output.tFinal = 0.f; - output.purityCur01 = 0.f; - output.purity01_used = 0.f; - return output; - } - - float3 lms_t0 = LMS_From_MB_T(white, t); - float3 xyz_t0 = mul(LMS_TO_XYZ_2006, lms_t0); - float3 rgb_t0 = mul(xyz_to_rgb_mat, xyz_t0); - output.rgbT0 = rgb_t0; - - // 4) Affine form of RGB along the MB ray at fixed T. - // t = 0 -> white in MB space, t = 1 -> original input. - float3 a = rgb_linear - rgb_t0; - - // 5) Intersect channel constraints RGB(t) >= 0 across R,G,B. - float t_lo = 0.f; - float t_hi = INTERVAL_MAX; - - float lo; - float hi; - - IntervalLower0(a.x, rgb_t0.x, lo, hi); - t_lo = max(t_lo, lo); - t_hi = min(t_hi, hi); - - IntervalLower0(a.y, rgb_t0.y, lo, hi); - t_lo = max(t_lo, lo); - t_hi = min(t_hi, hi); - - IntervalLower0(a.z, rgb_t0.z, lo, hi); - t_lo = max(t_lo, lo); - t_hi = min(t_hi, hi); - - if (t_hi < t_lo) - { - output.rgbOut = max(rgb_linear, 0.f); - output.rgbEdgeGamut = rgb_t0; - output.tMaxGamut = 0.f; - output.tFinal = 0.f; - output.purityCur01 = 0.f; - output.purity01_used = 0.f; - return output; - } - - float t_max = max(0.f, t_hi); - output.tMaxGamut = t_max; - - // Current normalized purity for the input (which is at t = 1). - float p_cur = (t_max > EPSILON) ? DivideSafe(1.f, t_max, 0.f) : 0.f; - output.purityCur01 = saturate(p_cur); - - // Useful validation output: point on the gamut edge for this MB ray. - float3 rgb_edge = a * t_max + rgb_t0; - output.rgbEdgeGamut = rgb_edge; - - float t_final = 0.f; - - if (purity_adjust_mode == MB_PURITY_ADJUST_CLIP_MAX) - { - output.purity01_used = 1.f; - output.purity01_in = 1.f; - t_final = t_max; - } - else - { - if (purity_input_mode == MB_PURITY_MODE_NORMALIZED) - { - // Normalized mode: user operates in [0,1] of available gamut headroom. - output.purity01_in = saturate(purity_value); - float gamma = max(curve_gamma, 1e-6f); - float purity01 = pow(output.purity01_in, gamma); - output.purity01_used = purity01; - t_final = purity01 * t_max; - } - else if (purity_input_mode == MB_PURITY_MODE_SCALE_NEUTWO) - { - // Scale mode: user controls a saturation multiplier around current purity. - // 1 = no change, >1 increases, <1 decreases, with a smooth shoulder near edge. - float saturation_scale = max(purity_value, 0.f); - float purity01_target; - if (output.purityCur01 >= ONE_MINUS_EPSILON && saturation_scale >= 1.f) - { - purity01_target = 1.f; - } - else - { - float z = InverseNeutwo(output.purityCur01); - float z_scaled = z * saturation_scale; - purity01_target = saturate(Neutwo(z_scaled)); - } - output.purity01_in = output.purityCur01; - output.purity01_used = purity01_target; - t_final = purity01_target * t_max; - } - else - { - // Distance mode: user value is interpreted as MB ray distance t. - float t_user = max(purity_value, 0.f); - t_final = min(t_user, t_max); - output.purity01_used = - (t_max > EPSILON) ? saturate(DivideSafe(t_final, t_max, 0.f)) : 0.f; - output.purity01_in = output.purity01_used; - } - } - - output.tFinal = t_final; - - float2 mb_final = white + t_final * direction; - - float3 lms_final = LMS_From_MB_T(mb_final, t); - float3 xyz_final = mul(LMS_TO_XYZ_2006, lms_final); - float3 rgb_final = mul(xyz_to_rgb_mat, xyz_final); - - // Clean up float math - output.rgbOut = max(rgb_final, 0.f); - - return output; -} - -// Normalized-purity solve in a given RGB space: purity01 = 0 is the adapted white, 1 the gamut boundary. -PurityResult ApplyPurity(float3 rgb_linear, float purity01, RGBColorSpace colorSpace, float curve_gamma = 1.f, - float2 mb_white_override = float2(-1.f, -1.f), float t_min = 1e-6f, - int purity_adjust_mode = MB_PURITY_ADJUST_NONE) -{ - return SolvePurity(rgb_linear, purity01, MB_PURITY_MODE_NORMALIZED, curve_gamma, - mb_white_override, t_min, purity_adjust_mode, colorSpace.rgbToXYZ, - colorSpace.xyzToRGB); -} - -// ------------------------------------------------------------------------------------------------------------ -// D. Hue/purity emulation -// ------------------------------------------------------------------------------------------------------------ - -// Moves the target's MB hue direction toward the reference's by `hue_strength` while re-applying the target's -// own purity on the new direction, then optionally moves that purity toward the reference's by -// `chrominance_strength`. T = L + M of the target is preserved throughout. A white override of (-1, -1) -// selects D65. This is the hue block of RenoDX's ApplyHueAndPurityGrading; its saturation / dechroma / -// highlight-saturation tail is not part of the model and is not carried. -float3 HueAndPurityEmulation(float3 ungraded, float3 reference, float hue_strength, float chrominance_strength, - RGBColorSpace colorSpace, float2 mb_white_override, float t_min) -{ - float3 color = ungraded; - if (hue_strength == 0.f && chrominance_strength == 0.f) - { - return color; - } - - const float kNearWhiteEpsilon = MB_NEAR_WHITE_EPSILON; - const float2 white = (mb_white_override.x >= 0.f && mb_white_override.y >= 0.f) - ? mb_white_override - : MB_White_D65(); - - float color_purity01 = ApplyPurity( - color, 1.f, colorSpace, 1.f, mb_white_override, t_min) - .purityCur01; - - // MB hue + purity emulation (analog of OkLab hue/chrominance section). - if (hue_strength != 0.f || chrominance_strength != 0.f) - { - float reference_purity01 = ApplyPurity( - reference, 1.f, colorSpace, 1.f, mb_white_override, t_min) - .purityCur01; - - float purity_current = color_purity01; - float purity_ratio = 1.f; - float3 hue_seed = color; - - if (hue_strength != 0.f) - { - float3 target_lms = mul(XYZ_TO_LMS_2006, - mul(colorSpace.rgbToXYZ, color)); - float3 reference_lms = mul(XYZ_TO_LMS_2006, - mul(colorSpace.rgbToXYZ, reference)); - - float target_t = target_lms.x + target_lms.y; - if (target_t > t_min) - { - float2 target_direction = MB_From_LMS(target_lms) - white; - float2 reference_direction = MB_From_LMS(reference_lms) - white; - - float target_len_sq = dot(target_direction, target_direction); - float reference_len_sq = dot(reference_direction, reference_direction); - - if (target_len_sq > kNearWhiteEpsilon || reference_len_sq > kNearWhiteEpsilon) - { - float2 target_unit = (target_len_sq > kNearWhiteEpsilon) - ? target_direction * rsqrt(target_len_sq) - : float2(0.f, 0.f); - float2 reference_unit = (reference_len_sq > kNearWhiteEpsilon) - ? reference_direction * rsqrt(reference_len_sq) - : target_unit; - - if (target_len_sq <= kNearWhiteEpsilon) - { - target_unit = reference_unit; - } - - float2 blended_unit = lerp(target_unit, reference_unit, hue_strength); - float blended_len_sq = dot(blended_unit, blended_unit); - if (blended_len_sq <= kNearWhiteEpsilon) - { - blended_unit = (hue_strength >= 0.5f) ? reference_unit : target_unit; - blended_len_sq = dot(blended_unit, blended_unit); - } - blended_unit *= rsqrt(max(blended_len_sq, 1e-20f)); - - float seed_len = sqrt(max(target_len_sq, 0.f)); - if (seed_len <= 1e-6f) - { - seed_len = sqrt(max(reference_len_sq, 0.f)); - } - seed_len = max(seed_len, 1e-6f); - - hue_seed = mul( - colorSpace.xyzToRGB, - mul(LMS_TO_XYZ_2006, - LMS_From_MB_T(white + blended_unit * seed_len, target_t))); - - float purity_post = ApplyPurity( - hue_seed, 1.f, colorSpace, 1.f, mb_white_override, t_min) - .purityCur01; - purity_ratio = SafeDivision(purity_current, purity_post, 1.f); - purity_current = purity_post; - } - } - } - - if (chrominance_strength != 0.f) - { - float target_purity_ratio = SafeDivision(reference_purity01, purity_current, 1.f); - purity_ratio = lerp(purity_ratio, target_purity_ratio, chrominance_strength); - } - - float applied_purity01 = saturate(purity_current * max(purity_ratio, 0.f)); - color = ApplyPurity( - hue_seed, applied_purity01, colorSpace, 1.f, mb_white_override, t_min) - .rgbOut; - color_purity01 = applied_purity01; - } - - return color; -} - -// ------------------------------------------------------------------------------------------------------------ -// E. Colour-space convenience wrappers (compile-time fixed: BT.2020, D65, the solver's 1e-7 T floor) -// ------------------------------------------------------------------------------------------------------------ - -float3 HueAndPurityEmulationBT2020(float3 ungraded_bt2020, float3 reference_bt2020, float hue_strength, float chrominance_strength) -{ - return HueAndPurityEmulation(ungraded_bt2020, reference_bt2020, hue_strength, chrominance_strength, RGB_BT2020, float2(-1.f, -1.f), 1e-7f); -} - -// Reference hue direction on the target's own purity and T; nothing of the reference's purity is taken. -// Deliberately a plain call: a hand-specialised copy (reference purity solve and chrominance branch removed) -// compiles to byte-identical DXBC, since fxc already eliminates that dead work, so it would only duplicate -// the hue block. Measured on both BL1 wrappers and held bit-exact over random pairs before being dropped. -float3 HueOnlyBT2020(float3 ungraded_bt2020, float3 reference_bt2020) -{ - return HueAndPurityEmulationBT2020(ungraded_bt2020, reference_bt2020, 1.f, 0.f); -} -} // namespace MacLeodBoynton - -#endif // LUMA_MACLEOD_BOYNTON_HLSL diff --git a/Shaders/Medal of Honor Airborne/Luma_MOHA_Sharpen.hlsl b/Shaders/Medal of Honor Airborne/Luma_MOHA_Sharpen.hlsl deleted file mode 100644 index 11773b93f..000000000 --- a/Shaders/Medal of Honor Airborne/Luma_MOHA_Sharpen.hlsl +++ /dev/null @@ -1,19 +0,0 @@ -// RCAS on the SMAA output (BL2/TPS/TW2 shape): after SMAA's neighborhood blend, on the gamma canvas, written back -// before the HUD and the composition. paperWhite 1.0; RCAS_LIMIT bounds the lobe (bloom no longer clamps at 1.0). - -#include "../Includes/RCAS.hlsl" - -cbuffer SharpenCB : register(b0) -{ - float4 SharpenParams; // (width, height, sharpness[0..1], unused) -} - -Texture2D tex0 : register(t0); // SMAA output (gamma canvas) -Texture2D dummyMV : register(t1); // unused (dynamicSharpening = false) - -float4 sharpen_ps(float4 pos : SV_Position) : SV_Target -{ - int2 p = int2(pos.xy); - int2 maxPixel = int2((int)SharpenParams.x - 1, (int)SharpenParams.y - 1); - return RCAS(p, int2(0, 0), maxPixel, SharpenParams.z, tex0, dummyMV, 1.0, false, (float4)0, false); -} diff --git a/Shaders/Medal of Honor Airborne/Luma_MOHA_Tonemap.hlsl b/Shaders/Medal of Honor Airborne/Luma_MOHA_Tonemap.hlsl index dd6025e58..0d69e759f 100644 --- a/Shaders/Medal of Honor Airborne/Luma_MOHA_Tonemap.hlsl +++ b/Shaders/Medal of Honor Airborne/Luma_MOHA_Tonemap.hlsl @@ -23,7 +23,7 @@ #include "../Includes/ColorGradingLUT.hlsl" // SimpleGamutClip #include "../Includes/DICE.hlsl" // DICETonemap / DefaultDICESettings #include "../Includes/Reinhard.hlsl" // Reinhard::ReinhardPiecewise, the soft hue reference -#include "Includes/MacLeodBoynton.hlsl" // MacLeodBoynton::HueOnlyBT2020. Byte-identical copy of the BL GOTY production model: do not edit here, sync it from "Borderlands GOTY Enhanced/Includes" +#include "../Includes/MacLeodBoynton.hlsl" // MacLeodBoynton::HueOnlyBT2020 // clang-format on #include "Includes/GameBindings.hlsl" // b3/b4, the dgVoodoo masks, ApplyDgvMask, PowUE3 diff --git a/Shaders/Medal of Honor Airborne/Luma_SMAA_impl.hlsl b/Shaders/Medal of Honor Airborne/Luma_SMAA_impl.hlsl index 6c17ea29c..d607f771f 100644 --- a/Shaders/Medal of Honor Airborne/Luma_SMAA_impl.hlsl +++ b/Shaders/Medal of Honor Airborne/Luma_SMAA_impl.hlsl @@ -38,72 +38,14 @@ cbuffer SmaaMetricsCB : register(b1) // (deviation from the local tangent plane), not a depth. The CS tolerance is the lever, not this value. #define SMAA_PREDICATION_STRENGTH 0.5 #define SMAA_PREDICATION_THRESHOLD 0.5 -#define SMAA_CUSTOM_SL -SamplerState LinearSampler : register(s0); -SamplerState PointSampler : register(s1); -#define SMAATexture2D(tex) Texture2D tex -#define SMAATexturePass2D(tex) tex -#define SMAASampleLevelZero(tex, coord) tex.SampleLevel(LinearSampler, coord, 0) -#define SMAASampleLevelZeroPoint(tex, coord) tex.SampleLevel(PointSampler, coord, 0) -#define SMAASampleLevelZeroOffset(tex, coord, offset) tex.SampleLevel(LinearSampler, coord, 0, offset) -#define SMAASample(tex, coord) tex.Sample(LinearSampler, coord) -#define SMAASamplePoint(tex, coord) tex.Sample(PointSampler, coord) -#define SMAASampleOffset(tex, coord, offset) tex.Sample(LinearSampler, coord, offset) -#define SMAA_FLATTEN [flatten] -#define SMAA_BRANCH [branch] -#define SMAATexture2DMS2(tex) Texture2DMS tex -#define SMAALoad(tex, pos, sample) tex.Load(pos, sample) -#define SMAAGather(tex, coord) tex.Gather(LinearSampler, coord, 0) -#include "../Includes/SMAA.hlsl" - -Texture2D tex0 : register(t0); -Texture2D tex1 : register(t1); -Texture2D tex2 : register(t2); - -void fullscreen_triangle(uint id, out float4 position, out float2 texcoord) -{ - texcoord = float2((id << 1) & 2, id & 2); - position = float4(texcoord * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); -} +#define SMAAGather(tex, coord) tex.Gather(LinearSampler, coord, 0) // SMAAEdgeDetection -void smaa_edge_detection_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset[3] : TEXCOORD1) -{ - fullscreen_triangle(id, position, texcoord); - SMAAEdgeDetectionVS(texcoord, offset); -} - -float2 smaa_edge_detection_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset[3] : TEXCOORD1) : SV_Target -{ - // tex0 = colorTexGamma (gamma-encoded graded canvas) - // tex1 = predicationTex (edge-ness in [0,1]; null fallback -> reads 0, scale 1.0 = plain ULTRA threshold) - return SMAAColorEdgeDetectionPS(texcoord, offset, tex0, tex1); -} - -// SMAABlendingWeightCalculation -void smaa_blending_weight_calculation_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float2 pixcoord : TEXCOORD1, out float4 offset[3] : TEXCOORD2) -{ - fullscreen_triangle(id, position, texcoord); - SMAABlendingWeightCalculationVS(texcoord, pixcoord, offset); -} - -float4 smaa_blending_weight_calculation_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float2 pixcoord : TEXCOORD1, float4 offset[3] : TEXCOORD2) : SV_Target -{ - // tex0 = edgesTex, tex1 = areaTex, tex2 = searchTex - return SMAABlendingWeightCalculationPS(texcoord, pixcoord, offset, tex0, tex1, tex2, 0); -} +// tex0 = colorTexGamma (gamma-encoded graded canvas) +// tex1 = predicationTex (edge-ness in [0,1]; null fallback -> reads 0, scale 1.0 = plain ULTRA threshold) // SMAANeighborhoodBlending -void smaa_neighborhood_blending_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset : TEXCOORD1) -{ - fullscreen_triangle(id, position, texcoord); - SMAANeighborhoodBlendingVS(texcoord, offset); -} +// tex0 = colorTex (linear copy), tex1 = blendTex. Re-encode to the canvas' gamma. +#define SMAA_NEIGHBORHOOD_OUTPUT(color, position) color.rgb = linear_to_gamma(color.rgb, GCT_MIRROR); -float4 smaa_neighborhood_blending_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset : TEXCOORD1) : SV_Target -{ - // tex0 = colorTex (linear copy), tex1 = blendTex. Re-encode to the canvas' gamma. - float4 color = SMAANeighborhoodBlendingPS(texcoord, offset, tex0, tex1); - color.rgb = linear_to_gamma(color.rgb, GCT_MIRROR); - return color; -} +#include "../Includes/SMAA_Passes.hlsl" diff --git a/Shaders/The Witcher 2/FinalGrade_0xDE5CF9CD.ps_5_0.hlsl b/Shaders/The Witcher 2/FinalGrade_0xDE5CF9CD.ps_5_0.hlsl index c9da4ec61..ad7ccd051 100644 --- a/Shaders/The Witcher 2/FinalGrade_0xDE5CF9CD.ps_5_0.hlsl +++ b/Shaders/The Witcher 2/FinalGrade_0xDE5CF9CD.ps_5_0.hlsl @@ -4,7 +4,7 @@ #include "../Includes/Color.hlsl" #include "../Includes/DICE.hlsl" #include "../Includes/Reinhard.hlsl" // Reinhard::ReinhardPiecewise, the soft hue reference -#include "Includes/MacLeodBoynton.hlsl" // MacLeodBoynton::HueOnlyBT2020. Byte-identical copy of the BL GOTY production model: do not edit here, sync it from "Borderlands GOTY Enhanced/Includes" +#include "../Includes/MacLeodBoynton.hlsl" // MacLeodBoynton::HueOnlyBT2020 #include "Includes/GameBindings.hlsl" // b3/b4, the dgVoodoo masks, ApplyDgvMask, DgVoodooRcp, DgVoodooLog2 // clang-format on diff --git a/Shaders/The Witcher 2/Includes/GameBindings.hlsl b/Shaders/The Witcher 2/Includes/GameBindings.hlsl index b3533bc55..c76a306d7 100644 --- a/Shaders/The Witcher 2/Includes/GameBindings.hlsl +++ b/Shaders/The Witcher 2/Includes/GameBindings.hlsl @@ -1,6 +1,8 @@ #ifndef LUMA_TW2_GAME_BINDINGS #define LUMA_TW2_GAME_BINDINGS +#include "../../Includes/DgVoodoo.hlsl" + // The Witcher 2 - dgVoodoo bindings and ops the replaced passes share, and nothing else. Included by the tonemap, the // final grade, the glow and light-shaft blends and the video pass. // Deliberately holds no textures or samplers, and no alias for a game-content cb4 row: row meaning is per pass (cb4[60] @@ -28,28 +30,4 @@ cbuffer cb4 : register(b4) #define DgvMaskT2 cb3[48] #define DgvFillT2 cb3[49] -float4 ApplyDgvMask(float4 value, float4 mask, float4 fill) -{ - return asfloat((asuint(value) & asuint(mask)) | asuint(fill)); -} - -// 1e37 is the exact sentinel every dgVoodoo dump uses (l(9999999933815812510711506376257961984.0)); it must not be -// rounded up to 1e38: it is multiplied downstream, and 1e38 overflows to inf ten times sooner, turning a zero-weight -// lerp around it into 0 * inf = NaN. -#define DGVOODOO_BIG 1e37 - -// dgVoodoo's guarded reciprocal: rcp with the huge-constant fallback at exactly 0 (movc in the translated CSO). -float DgVoodooRcp(float x) -{ - return (abs(x) > 0.0) ? (1.0 / x) : DGVOODOO_BIG; -} - -// dgVoodoo's guarded log: log(0) = -inf -> -BIG (so exp2 later yields 0). It tests the -inf BIT PATTERN, not isinf(), -// so a +inf input keeps propagating as vanilla does instead of being flipped to ~0 (a white pixel gone black). -float DgVoodooLog2(float x) -{ - float l = log2(abs(x)); - return (asuint(l) == 0xff800000u) ? -DGVOODOO_BIG : l; -} - #endif // LUMA_TW2_GAME_BINDINGS diff --git a/Shaders/The Witcher 2/Includes/MacLeodBoynton.hlsl b/Shaders/The Witcher 2/Includes/MacLeodBoynton.hlsl deleted file mode 100644 index b73af6750..000000000 --- a/Shaders/The Witcher 2/Includes/MacLeodBoynton.hlsl +++ /dev/null @@ -1,557 +0,0 @@ -// MacLeod–Boynton hue/purity model for linear RGB HDR colors. -// -// Derived from the RenoDX MacLeod–Boynton implementation (clshortfuse/renodx, MIT License, Copyright (c) 2025 -// Carlos Lopez Jr.), commit cd32113a98608e63027d40910cfe296a14dfe228: -// src/games/borderlandsgotyenhanced/macleod_boynton.hlsli constants, MB transforms, the gamut-ray purity solver -// src/games/borderlandsgotyenhanced/common.hlsli ApplyHueAndPurityGrading, its hue/purity emulation -// src/shaders/math.hlsl, deprecated.hlsl, color/rgb.hlsl Invert3x3, DivideSafe, SafeDivision, matrices, xyY -// The constants, the expressions and their order are RenoDX's; the organisation and the API are Luma's, and the -// unused purity modes are kept until the specialised entry points have proven what they need. -// _tools/bl1_bridge/mb_equiv compiles this file, the RenoDX source and the previous BL1 port side by side under -// fxc and holds their listings byte-identical. -// -// The model: RGB -> XYZ -> LMS (CIE 2006), hue as MacLeod–Boynton ratios r = L/(L+M), b = S/(L+M) around an -// adapted white, intensity anchored on T = L + M, purity as the fraction of the distance from white to the RGB -// gamut boundary along that ray. The hue/purity emulation takes a reference's ray direction and re-applies the -// target's own purity on it; with chrominance strength 0 only the hue direction moves. -// -// This file knows nothing about its caller. It takes a target, a reference, two strengths, a colour space and a -// white; which reference, how much of it, and where in a pipeline it runs are the caller's policy. -// -// Sections: A constants and transforms B MacLeod–Boynton coordinates C gamut-ray purity solver -// D hue/purity emulation E colour-space convenience wrappers - -#ifndef LUMA_MACLEOD_BOYNTON_HLSL -#define LUMA_MACLEOD_BOYNTON_HLSL - -namespace MacLeodBoynton -{ -// ------------------------------------------------------------------------------------------------------------ -// A. Constants and transforms -// ------------------------------------------------------------------------------------------------------------ - -// src/shaders/math.hlsl -float DivideSafe(float dividend, float divisor, float fallback) -{ - return (divisor == 0.f) ? fallback : (dividend / divisor); -} - -float3x3 Invert3x3(float3x3 m) -{ - float a = m[0][0], b = m[0][1], c = m[0][2]; - float d = m[1][0], e = m[1][1], f = m[1][2]; - float g = m[2][0], h = m[2][1], i = m[2][2]; - - float A = (e * i - f * h); - float B = -(d * i - f * g); - float C = (d * h - e * g); - float D = -(b * i - c * h); - float E = (a * i - c * g); - float F = -(a * h - b * g); - float G = (b * f - c * e); - float H = -(a * f - c * d); - float I = (a * e - b * d); - - float det = a * A + b * B + c * C; - float invDet = DivideSafe(1.0, det, 0.0); - - return float3x3( - A, D, G, - B, E, H, - C, F, I) * - invDet; -} - -// src/shaders/deprecated.hlsl -float SafeDivision(float quotient, float dividend, float fallback) -{ - return (dividend == 0.f) - ? fallback - : (quotient / dividend); -} - -// src/shaders/color/rgb.hlsl -static const float3x3 BT2020_TO_XYZ_MAT = float3x3( - 0.6369580483f, 0.1446169036f, 0.1688809752f, - 0.2627002120f, 0.6779980715f, 0.0593017165f, - 0.0000000000f, 0.0280726930f, 1.0609850577f); - -static const float3x3 XYZ_TO_BT2020_MAT = float3x3( - 1.7166511880f, -0.3556707838f, -0.2533662814f, - -0.6666843518f, 1.6164812366f, 0.0157685458f, - 0.0176398574f, -0.0427706133f, 0.9421031212f); - -static const float2 WHITE_POINT_D65 = float2(0.31272, 0.32903); - -// renodx::color::xyz::from::xyY -float3 XYZ_From_xyY(float3 xyY) -{ - float3 XYZ; - - XYZ.xz = float2(xyY.x, (1.f - xyY.xy.x - xyY.xy.y)) / xyY.y * xyY[2]; - - XYZ.y = xyY[2]; - - return XYZ; -} - -// The RGB working space the solver runs in: its matrices to and from XYZ. Passed by value; every current -// caller passes a compile-time constant and fxc folds it, so the abstraction costs nothing at runtime. -struct RGBColorSpace -{ - float3x3 rgbToXYZ; - float3x3 xyzToRGB; -}; - -static const RGBColorSpace RGB_BT2020 = {BT2020_TO_XYZ_MAT, XYZ_TO_BT2020_MAT}; - -// src/games/borderlandsgotyenhanced/macleod_boynton.hlsli -static const float3x3 XYZ_TO_LMS_2006 = float3x3( - 0.185082982238733f, 0.584081279463687f, -0.0240722415044404f, - -0.134433056469973f, 0.405752392775348f, 0.0358252602217631f, - 0.000789456671966863f, -0.000912281325916184f, 0.0198490812339463f); - -static const float3x3 LMS_TO_XYZ_2006 = Invert3x3(XYZ_TO_LMS_2006); - -static const float EPSILON = 1e-20f; -static const float INTERVAL_MAX = 1e30f; -static const float MB_NEAR_WHITE_EPSILON = 1e-14f; - -static const int MB_PURITY_MODE_DISTANCE = 0; -static const int MB_PURITY_MODE_NORMALIZED = 1; -static const int MB_PURITY_MODE_SCALE_NEUTWO = 2; -static const int MB_PURITY_ADJUST_NONE = 0; -static const int MB_PURITY_ADJUST_CLIP_MAX = 1; -static const float ONE_MINUS_EPSILON = 1.f - 1e-6f; - -// ------------------------------------------------------------------------------------------------------------ -// B. MacLeod–Boynton coordinates -// ------------------------------------------------------------------------------------------------------------ - -// Neutwo (x / sqrt(x^2 + 1)) and its inverse. -// Used to shape purity scaling with a smooth shoulder near gamut edge. -float Neutwo(float x) -{ - float denominator_squared = mad(x, x, 1.f); - return x * rsqrt(denominator_squared); -} - -float InverseNeutwo(float x) -{ - float clamped = min(max(x, 0.f), ONE_MINUS_EPSILON); - float denominator_squared = mad(-clamped, clamped, 1.f); - return clamped * rsqrt(denominator_squared); -} - -// MB coordinate from LMS. -// r = L / (L + M), b = S / (L + M) -// T = (L + M) is intensity anchor and is preserved while changing purity. -float2 MB_From_LMS(float3 lms) -{ - float t = lms.x + lms.y; - - if (t <= 0.f) - { - return float2(0.f, 0.f); - } - - return float2( - DivideSafe(lms.x, t, 0.f), - DivideSafe(lms.z, t, 0.f)); -} - -// Reconstruct LMS from MB coordinates while holding T = L + M fixed. -float3 LMS_From_MB_T(float2 mb, float t) -{ - float r = mb.x; - float b = mb.y; - return float3(r * t, (1.f - r) * t, b * t); -} - -// Default adaptation white in MB space. -float2 MB_White_D65() -{ - float3 d65_xyz = XYZ_From_xyY(float3(WHITE_POINT_D65, 1.f)); - float3 d65_lms = mul(XYZ_TO_LMS_2006, d65_xyz); - return MB_From_LMS(d65_lms); -} - -// ------------------------------------------------------------------------------------------------------------ -// C. Gamut-ray purity solver -// ------------------------------------------------------------------------------------------------------------ - -// Half-space constraint for one channel in affine form: -// rgb(t) = a * t + b, and we require rgb(t) >= 0. -// Returns interval [lo, hi] where that channel is valid. -void IntervalLower0(float a, float b, out float lo, out float hi) -{ - if (abs(a) < EPSILON) - { - if (b >= 0.f) - { - lo = -INTERVAL_MAX; - hi = INTERVAL_MAX; - } - else - { - lo = 1.f; - hi = 0.f; - } - return; - } - - float t0 = DivideSafe(-b, a, 0.f); - - if (a > 0.f) - { - lo = t0; - hi = INTERVAL_MAX; - } - else - { - lo = -INTERVAL_MAX; - hi = t0; - } -} - -struct PurityResult -{ - float3 rgbOut; - float3 rgbT0; - float3 rgbEdgeGamut; - - // Max reachable t before first channel becomes negative. - float tMaxGamut; - // Applied t after user control mapping. - float tFinal; - - // Purity for input color in normalized 0..1 form (1 / tMax for input at t=1). - float purityCur01; - // Requested normalized purity (user input). - float purity01_in; - // Effective normalized purity after shaping curve. - float purity01_used; -}; - -// Main solver. -// -// There are three control modes: -// - purity_input_mode == MB_PURITY_MODE_NORMALIZED: -// purity_value is 0..1 "fraction of available purity headroom". -// - purity_input_mode == MB_PURITY_MODE_DISTANCE: -// purity_value is direct MB ray distance t. -// - purity_input_mode == MB_PURITY_MODE_SCALE_NEUTWO: -// purity_value is a scale multiplier in Neutwo inverse space (1 = unchanged). -// -// Optional adjust mode: -// - MB_PURITY_ADJUST_NONE: -// use selected input mode as-is. -// - MB_PURITY_ADJUST_CLIP_MAX: -// force max purity for this hue/gamut (tFinal = tMaxGamut). -// -// Important behavior: -// - Constraint is gamut-only (RGB >= 0), no arbitrary <= 1 clamp. -// - This allows scene-linear HDR values above 1.0 to pass through naturally. -PurityResult SolvePurity(float3 rgb_linear, float purity_value, int purity_input_mode, - float curve_gamma, float2 mb_white_override, float t_min, - int purity_adjust_mode, float3x3 rgb_to_xyz_mat, float3x3 xyz_to_rgb_mat) -{ - PurityResult output; - - output.purity01_in = 0.f; - - // 1) RGB -> XYZ -> LMS - float3 xyz = mul(rgb_to_xyz_mat, rgb_linear); - float3 lms = mul(XYZ_TO_LMS_2006, xyz); - - // T = L + M is the intensity anchor for MB purity moves. - float t = lms.x + lms.y; - - if (t <= t_min) - { - output.rgbOut = rgb_linear; - output.rgbT0 = rgb_linear; - output.rgbEdgeGamut = rgb_linear; - output.tMaxGamut = 0.f; - output.purityCur01 = 0.f; - output.purity01_used = 0.f; - return output; - } - - float2 white = - (mb_white_override.x >= 0.f && mb_white_override.y >= 0.f) ? mb_white_override : MB_White_D65(); - - // 2) MB ray from adapted white to input hue. - float2 mb0 = MB_From_LMS(lms); - float2 direction = mb0 - white; - - if (dot(direction, direction) < MB_NEAR_WHITE_EPSILON) - { - output.rgbOut = rgb_linear; - output.rgbT0 = rgb_linear; - output.rgbEdgeGamut = rgb_linear; - output.tMaxGamut = 0.f; - output.tFinal = 0.f; - output.purityCur01 = 0.f; - output.purity01_used = 0.f; - return output; - } - - float3 lms_t0 = LMS_From_MB_T(white, t); - float3 xyz_t0 = mul(LMS_TO_XYZ_2006, lms_t0); - float3 rgb_t0 = mul(xyz_to_rgb_mat, xyz_t0); - output.rgbT0 = rgb_t0; - - // 4) Affine form of RGB along the MB ray at fixed T. - // t = 0 -> white in MB space, t = 1 -> original input. - float3 a = rgb_linear - rgb_t0; - - // 5) Intersect channel constraints RGB(t) >= 0 across R,G,B. - float t_lo = 0.f; - float t_hi = INTERVAL_MAX; - - float lo; - float hi; - - IntervalLower0(a.x, rgb_t0.x, lo, hi); - t_lo = max(t_lo, lo); - t_hi = min(t_hi, hi); - - IntervalLower0(a.y, rgb_t0.y, lo, hi); - t_lo = max(t_lo, lo); - t_hi = min(t_hi, hi); - - IntervalLower0(a.z, rgb_t0.z, lo, hi); - t_lo = max(t_lo, lo); - t_hi = min(t_hi, hi); - - if (t_hi < t_lo) - { - output.rgbOut = max(rgb_linear, 0.f); - output.rgbEdgeGamut = rgb_t0; - output.tMaxGamut = 0.f; - output.tFinal = 0.f; - output.purityCur01 = 0.f; - output.purity01_used = 0.f; - return output; - } - - float t_max = max(0.f, t_hi); - output.tMaxGamut = t_max; - - // Current normalized purity for the input (which is at t = 1). - float p_cur = (t_max > EPSILON) ? DivideSafe(1.f, t_max, 0.f) : 0.f; - output.purityCur01 = saturate(p_cur); - - // Useful validation output: point on the gamut edge for this MB ray. - float3 rgb_edge = a * t_max + rgb_t0; - output.rgbEdgeGamut = rgb_edge; - - float t_final = 0.f; - - if (purity_adjust_mode == MB_PURITY_ADJUST_CLIP_MAX) - { - output.purity01_used = 1.f; - output.purity01_in = 1.f; - t_final = t_max; - } - else - { - if (purity_input_mode == MB_PURITY_MODE_NORMALIZED) - { - // Normalized mode: user operates in [0,1] of available gamut headroom. - output.purity01_in = saturate(purity_value); - float gamma = max(curve_gamma, 1e-6f); - float purity01 = pow(output.purity01_in, gamma); - output.purity01_used = purity01; - t_final = purity01 * t_max; - } - else if (purity_input_mode == MB_PURITY_MODE_SCALE_NEUTWO) - { - // Scale mode: user controls a saturation multiplier around current purity. - // 1 = no change, >1 increases, <1 decreases, with a smooth shoulder near edge. - float saturation_scale = max(purity_value, 0.f); - float purity01_target; - if (output.purityCur01 >= ONE_MINUS_EPSILON && saturation_scale >= 1.f) - { - purity01_target = 1.f; - } - else - { - float z = InverseNeutwo(output.purityCur01); - float z_scaled = z * saturation_scale; - purity01_target = saturate(Neutwo(z_scaled)); - } - output.purity01_in = output.purityCur01; - output.purity01_used = purity01_target; - t_final = purity01_target * t_max; - } - else - { - // Distance mode: user value is interpreted as MB ray distance t. - float t_user = max(purity_value, 0.f); - t_final = min(t_user, t_max); - output.purity01_used = - (t_max > EPSILON) ? saturate(DivideSafe(t_final, t_max, 0.f)) : 0.f; - output.purity01_in = output.purity01_used; - } - } - - output.tFinal = t_final; - - float2 mb_final = white + t_final * direction; - - float3 lms_final = LMS_From_MB_T(mb_final, t); - float3 xyz_final = mul(LMS_TO_XYZ_2006, lms_final); - float3 rgb_final = mul(xyz_to_rgb_mat, xyz_final); - - // Clean up float math - output.rgbOut = max(rgb_final, 0.f); - - return output; -} - -// Normalized-purity solve in a given RGB space: purity01 = 0 is the adapted white, 1 the gamut boundary. -PurityResult ApplyPurity(float3 rgb_linear, float purity01, RGBColorSpace colorSpace, float curve_gamma = 1.f, - float2 mb_white_override = float2(-1.f, -1.f), float t_min = 1e-6f, - int purity_adjust_mode = MB_PURITY_ADJUST_NONE) -{ - return SolvePurity(rgb_linear, purity01, MB_PURITY_MODE_NORMALIZED, curve_gamma, - mb_white_override, t_min, purity_adjust_mode, colorSpace.rgbToXYZ, - colorSpace.xyzToRGB); -} - -// ------------------------------------------------------------------------------------------------------------ -// D. Hue/purity emulation -// ------------------------------------------------------------------------------------------------------------ - -// Moves the target's MB hue direction toward the reference's by `hue_strength` while re-applying the target's -// own purity on the new direction, then optionally moves that purity toward the reference's by -// `chrominance_strength`. T = L + M of the target is preserved throughout. A white override of (-1, -1) -// selects D65. This is the hue block of RenoDX's ApplyHueAndPurityGrading; its saturation / dechroma / -// highlight-saturation tail is not part of the model and is not carried. -float3 HueAndPurityEmulation(float3 ungraded, float3 reference, float hue_strength, float chrominance_strength, - RGBColorSpace colorSpace, float2 mb_white_override, float t_min) -{ - float3 color = ungraded; - if (hue_strength == 0.f && chrominance_strength == 0.f) - { - return color; - } - - const float kNearWhiteEpsilon = MB_NEAR_WHITE_EPSILON; - const float2 white = (mb_white_override.x >= 0.f && mb_white_override.y >= 0.f) - ? mb_white_override - : MB_White_D65(); - - float color_purity01 = ApplyPurity( - color, 1.f, colorSpace, 1.f, mb_white_override, t_min) - .purityCur01; - - // MB hue + purity emulation (analog of OkLab hue/chrominance section). - if (hue_strength != 0.f || chrominance_strength != 0.f) - { - float reference_purity01 = ApplyPurity( - reference, 1.f, colorSpace, 1.f, mb_white_override, t_min) - .purityCur01; - - float purity_current = color_purity01; - float purity_ratio = 1.f; - float3 hue_seed = color; - - if (hue_strength != 0.f) - { - float3 target_lms = mul(XYZ_TO_LMS_2006, - mul(colorSpace.rgbToXYZ, color)); - float3 reference_lms = mul(XYZ_TO_LMS_2006, - mul(colorSpace.rgbToXYZ, reference)); - - float target_t = target_lms.x + target_lms.y; - if (target_t > t_min) - { - float2 target_direction = MB_From_LMS(target_lms) - white; - float2 reference_direction = MB_From_LMS(reference_lms) - white; - - float target_len_sq = dot(target_direction, target_direction); - float reference_len_sq = dot(reference_direction, reference_direction); - - if (target_len_sq > kNearWhiteEpsilon || reference_len_sq > kNearWhiteEpsilon) - { - float2 target_unit = (target_len_sq > kNearWhiteEpsilon) - ? target_direction * rsqrt(target_len_sq) - : float2(0.f, 0.f); - float2 reference_unit = (reference_len_sq > kNearWhiteEpsilon) - ? reference_direction * rsqrt(reference_len_sq) - : target_unit; - - if (target_len_sq <= kNearWhiteEpsilon) - { - target_unit = reference_unit; - } - - float2 blended_unit = lerp(target_unit, reference_unit, hue_strength); - float blended_len_sq = dot(blended_unit, blended_unit); - if (blended_len_sq <= kNearWhiteEpsilon) - { - blended_unit = (hue_strength >= 0.5f) ? reference_unit : target_unit; - blended_len_sq = dot(blended_unit, blended_unit); - } - blended_unit *= rsqrt(max(blended_len_sq, 1e-20f)); - - float seed_len = sqrt(max(target_len_sq, 0.f)); - if (seed_len <= 1e-6f) - { - seed_len = sqrt(max(reference_len_sq, 0.f)); - } - seed_len = max(seed_len, 1e-6f); - - hue_seed = mul( - colorSpace.xyzToRGB, - mul(LMS_TO_XYZ_2006, - LMS_From_MB_T(white + blended_unit * seed_len, target_t))); - - float purity_post = ApplyPurity( - hue_seed, 1.f, colorSpace, 1.f, mb_white_override, t_min) - .purityCur01; - purity_ratio = SafeDivision(purity_current, purity_post, 1.f); - purity_current = purity_post; - } - } - } - - if (chrominance_strength != 0.f) - { - float target_purity_ratio = SafeDivision(reference_purity01, purity_current, 1.f); - purity_ratio = lerp(purity_ratio, target_purity_ratio, chrominance_strength); - } - - float applied_purity01 = saturate(purity_current * max(purity_ratio, 0.f)); - color = ApplyPurity( - hue_seed, applied_purity01, colorSpace, 1.f, mb_white_override, t_min) - .rgbOut; - color_purity01 = applied_purity01; - } - - return color; -} - -// ------------------------------------------------------------------------------------------------------------ -// E. Colour-space convenience wrappers (compile-time fixed: BT.2020, D65, the solver's 1e-7 T floor) -// ------------------------------------------------------------------------------------------------------------ - -float3 HueAndPurityEmulationBT2020(float3 ungraded_bt2020, float3 reference_bt2020, float hue_strength, float chrominance_strength) -{ - return HueAndPurityEmulation(ungraded_bt2020, reference_bt2020, hue_strength, chrominance_strength, RGB_BT2020, float2(-1.f, -1.f), 1e-7f); -} - -// Reference hue direction on the target's own purity and T; nothing of the reference's purity is taken. -// Deliberately a plain call: a hand-specialised copy (reference purity solve and chrominance branch removed) -// compiles to byte-identical DXBC, since fxc already eliminates that dead work, so it would only duplicate -// the hue block. Measured on both BL1 wrappers and held bit-exact over random pairs before being dropped. -float3 HueOnlyBT2020(float3 ungraded_bt2020, float3 reference_bt2020) -{ - return HueAndPurityEmulationBT2020(ungraded_bt2020, reference_bt2020, 1.f, 0.f); -} -} // namespace MacLeodBoynton - -#endif // LUMA_MACLEOD_BOYNTON_HLSL diff --git a/Shaders/The Witcher 2/Luma_SMAA_impl.hlsl b/Shaders/The Witcher 2/Luma_SMAA_impl.hlsl index 03475dc43..71bba57e9 100644 --- a/Shaders/The Witcher 2/Luma_SMAA_impl.hlsl +++ b/Shaders/The Witcher 2/Luma_SMAA_impl.hlsl @@ -36,72 +36,14 @@ cbuffer SmaaMetricsCB : register(b1) // it only restores the base 0.05 threshold, never lowers it. #define SMAA_PREDICATION_STRENGTH 0.5 #define SMAA_PREDICATION_THRESHOLD 0.5 -#define SMAA_CUSTOM_SL -SamplerState LinearSampler : register(s0); -SamplerState PointSampler : register(s1); -#define SMAATexture2D(tex) Texture2D tex -#define SMAATexturePass2D(tex) tex -#define SMAASampleLevelZero(tex, coord) tex.SampleLevel(LinearSampler, coord, 0) -#define SMAASampleLevelZeroPoint(tex, coord) tex.SampleLevel(PointSampler, coord, 0) -#define SMAASampleLevelZeroOffset(tex, coord, offset) tex.SampleLevel(LinearSampler, coord, 0, offset) -#define SMAASample(tex, coord) tex.Sample(LinearSampler, coord) -#define SMAASamplePoint(tex, coord) tex.Sample(PointSampler, coord) -#define SMAASampleOffset(tex, coord, offset) tex.Sample(LinearSampler, coord, offset) -#define SMAA_FLATTEN [flatten] -#define SMAA_BRANCH [branch] -#define SMAATexture2DMS2(tex) Texture2DMS tex -#define SMAALoad(tex, pos, sample) tex.Load(pos, sample) -#define SMAAGather(tex, coord) tex.Gather(LinearSampler, coord, 0) -#include "../Includes/SMAA.hlsl" - -Texture2D tex0 : register(t0); -Texture2D tex1 : register(t1); -Texture2D tex2 : register(t2); - -void fullscreen_triangle(uint id, out float4 position, out float2 texcoord) -{ - texcoord = float2((id << 1) & 2, id & 2); - position = float4(texcoord * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); -} +#define SMAAGather(tex, coord) tex.Gather(LinearSampler, coord, 0) // SMAAEdgeDetection -void smaa_edge_detection_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset[3] : TEXCOORD1) -{ - fullscreen_triangle(id, position, texcoord); - SMAAEdgeDetectionVS(texcoord, offset); -} - -float2 smaa_edge_detection_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset[3] : TEXCOORD1) : SV_Target -{ - // tex0 = colorTexGamma (gamma-encoded graded canvas) - // tex1 = predicationTex (edge-ness in [0,1]; null fallback -> reads 0, scale 1.0 = plain ULTRA threshold) - return SMAAColorEdgeDetectionPS(texcoord, offset, tex0, tex1); -} - -// SMAABlendingWeightCalculation -void smaa_blending_weight_calculation_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float2 pixcoord : TEXCOORD1, out float4 offset[3] : TEXCOORD2) -{ - fullscreen_triangle(id, position, texcoord); - SMAABlendingWeightCalculationVS(texcoord, pixcoord, offset); -} - -float4 smaa_blending_weight_calculation_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float2 pixcoord : TEXCOORD1, float4 offset[3] : TEXCOORD2) : SV_Target -{ - // tex0 = edgesTex, tex1 = areaTex, tex2 = searchTex - return SMAABlendingWeightCalculationPS(texcoord, pixcoord, offset, tex0, tex1, tex2, 0); -} +// tex0 = colorTexGamma (gamma-encoded graded canvas) +// tex1 = predicationTex (edge-ness in [0,1]; null fallback -> reads 0, scale 1.0 = plain ULTRA threshold) // SMAANeighborhoodBlending -void smaa_neighborhood_blending_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset : TEXCOORD1) -{ - fullscreen_triangle(id, position, texcoord); - SMAANeighborhoodBlendingVS(texcoord, offset); -} +// tex0 = colorTex (linear copy), tex1 = blendTex. Re-encode to the canvas' gamma. +#define SMAA_NEIGHBORHOOD_OUTPUT(color, position) color.rgb = linear_to_gamma(color.rgb, GCT_MIRROR); -float4 smaa_neighborhood_blending_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset : TEXCOORD1) : SV_Target -{ - // tex0 = colorTex (linear copy), tex1 = blendTex. Re-encode to the canvas' gamma. - float4 color = SMAANeighborhoodBlendingPS(texcoord, offset, tex0, tex1); - color.rgb = linear_to_gamma(color.rgb, GCT_MIRROR); - return color; -} +#include "../Includes/SMAA_Passes.hlsl" diff --git a/Shaders/The Witcher 2/Luma_TW2_Sharpen.hlsl b/Shaders/The Witcher 2/Luma_TW2_Sharpen.hlsl deleted file mode 100644 index a67adab69..000000000 --- a/Shaders/The Witcher 2/Luma_TW2_Sharpen.hlsl +++ /dev/null @@ -1,22 +0,0 @@ -// RCAS sharpening for the SMAA output (The Witcher 2; same shape as the shipped BL2/TPS pass). -// Runs after the SMAA neighborhood-blend pass, on the gamma canvas color (the same gamma buffer SMAA -// consumed), and writes straight into the canvas RTV (the UI then draws on it and the core Display -// Composition does paper-white + scRGB downstream). paperWhite=1.0; the sharpness slider is the tuning knob. -// RCAS_LIMIT bounds the lobe so bright pixels don't over-sharpen. - -#include "../Includes/RCAS.hlsl" - -cbuffer SharpenCB : register(b0) -{ - float4 SharpenParams; // (width, height, sharpness[0..1], unused) -} - -Texture2D tex0 : register(t0); // SMAA output (gamma canvas) -Texture2D dummyMV : register(t1); // unused (dynamicSharpening = false) - -float4 sharpen_ps(float4 pos : SV_Position) : SV_Target -{ - int2 p = int2(pos.xy); - int2 maxPixel = int2((int)SharpenParams.x - 1, (int)SharpenParams.y - 1); - return RCAS(p, int2(0, 0), maxPixel, SharpenParams.z, tex0, dummyMV, 1.0, false, (float4)0, false); -} diff --git a/Shaders/The Witcher 2/Luma_TW2_XeGTAO.hlsl b/Shaders/The Witcher 2/Luma_TW2_XeGTAO.hlsl index dba9f1409..3370a5fd5 100644 --- a/Shaders/The Witcher 2/Luma_TW2_XeGTAO.hlsl +++ b/Shaders/The Witcher 2/Luma_TW2_XeGTAO.hlsl @@ -36,18 +36,6 @@ cbuffer LumaGTAO : register(b9) float2 PaddingRT; } -#if XE_GTAO_QUALITY == 0 // Low -#define SLICE_COUNT 4.0 -#elif XE_GTAO_QUALITY == 1 // Medium -#define SLICE_COUNT 7.0 -#elif XE_GTAO_QUALITY == 2 // High -#define SLICE_COUNT 10.0 -#elif XE_GTAO_QUALITY == 3 // Very High -#define SLICE_COUNT 13.0 -#elif XE_GTAO_QUALITY == 4 // Ultra -#define SLICE_COUNT 16.0 -#endif - // User configurable // @@ -59,10 +47,6 @@ cbuffer LumaGTAO : register(b9) #define EFFECT_RADIUS 0.81 // anchored to the game's own HBAO radius: probed cb4[11] = (R, R^2) = (1.1835, 1.4008) view units (meters — depth p50 ~7.3); 0.81 * RADIUS_MULTIPLIER 1.457 = 1.18 matches native R. RadiusOverrideRT > 0 wins. #endif -#ifndef RADIUS_MULTIPLIER -#define RADIUS_MULTIPLIER 1.457 // Default 1.457 -#endif - #ifndef EFFECT_FALLOFF_RANGE #define EFFECT_FALLOFF_RANGE 0.005 // hard-edge falloff (full sample weight up to the radius edge) = crisp contact AO, repo-standard for HBAO-native games; Intel default 0.615 is softer #endif @@ -71,45 +55,14 @@ cbuffer LumaGTAO : register(b9) #define SAMPLE_DISTRIBUTION_POWER 1.5 // Default 2.0 #endif -#ifndef THIN_OCCLUDER_COMPENSATION -#define THIN_OCCLUDER_COMPENSATION 0.0 // Default 0.0; > 0 causes more mistakes than it fixes on big geometry -#endif - -#ifndef FINAL_VALUE_POWER -#define FINAL_VALUE_POWER 1.0 // Default 2.2; shadow default for FinalValuePowerRT (the CB value is what actually applies) -#endif - -#ifndef DEPTH_MIP_SAMPLING_OFFSET -#define DEPTH_MIP_SAMPLING_OFFSET 3.3 // Default 3.3 -#endif - -#ifndef SLICE_COUNT -#define SLICE_COUNT 3.0 // Default 3.0 -#endif - -#ifndef STEPS_PER_SLICE -#define STEPS_PER_SLICE 3.0 // Default 3.0 -#endif - -#ifndef DENOISE_BLUR_BETA -#define DENOISE_BLUR_BETA 1.2 // Default 1.2 -#endif - // #define VIEWPORT_PIXEL_SIZE ViewportPixelSizeRT // Transcribed from the native HBAO PS (0x3FEEC0F7): ndc = (uv.x*2-1, 1-2*uv.y), viewRay = ndc * cb4[9].zw. // Expressed as the XeGTAO mul/add pair over raw uv: viewPos.xy = (uv * MUL + ADD) * viewZ. -#define NDC_TO_VIEW_MUL (float2(2.0, -2.0) * cb4[9].zw) -#define NDC_TO_VIEW_ADD (float2(-1.0, 1.0) * cb4[9].zw) -#define NDC_TO_VIEW_MUL_X_PIXEL_SIZE (NDC_TO_VIEW_MUL * VIEWPORT_PIXEL_SIZE) - -#define XE_GTAO_DEPTH_MIP_LEVELS 5.0 -#define XE_GTAO_OCCLUSION_TERM_SCALE 1.5 - -#define XE_GTAO_PI 3.1415926535897932384626433832795 -#define XE_GTAO_PI_HALF 1.5707963267948966192313216916398 +#define NDC_TO_VIEW_MUL (float2(2.0, -2.0) * cb4[9].zw) +#define NDC_TO_VIEW_ADD (float2(-1.0, 1.0) * cb4[9].zw) // TW2's depth texture is ALREADY linear view-space depth (the native HBAO scales its NDC ray by the raw // sample) — no projection unpack. DepthScaleRT only rescales the game units into the ~meter range XeGTAO's @@ -119,630 +72,18 @@ float XeGTAO_ScreenSpaceToViewSpaceDepth(const float screenDepth) return max(0.0, screenDepth) / max(1e-3, DepthScaleRT); } -// This is also a good place to do non-linear depth conversion for cases where one wants the 'radius' (effectively the threshold between near-field and far-field GI), -// is required to be non-linear (i.e. very large outdoors environments). -float XeGTAO_ClampDepth(float depth) -{ - return clamp(depth, 0.0, 3.402823466e+38); -} - -float XeGTAO_EffectRadius() -{ - return (RadiusOverrideRT > 0.0 ? RadiusOverrideRT : EFFECT_RADIUS) * RADIUS_MULTIPLIER; -} - -// weighted average depth filter -float XeGTAO_DepthMIPFilter(float depth0, float depth1, float depth2, float depth3) -{ - float maxDepth = max(max(depth0, depth1), max(depth2, depth3)); - - const float depthRangeScaleFactor = 0.75; // found empirically :) - const float effectRadius = depthRangeScaleFactor * XeGTAO_EffectRadius(); - const float falloffRange = EFFECT_FALLOFF_RANGE * effectRadius; - const float falloffFrom = effectRadius * (1.0 - EFFECT_FALLOFF_RANGE); - - // fadeout precompute optimisation - const float falloffMul = -1.0 / falloffRange; - const float falloffAdd = falloffFrom / falloffRange + 1.0; - - float weight0 = saturate((maxDepth - depth0) * falloffMul + falloffAdd); - float weight1 = saturate((maxDepth - depth1) * falloffMul + falloffAdd); - float weight2 = saturate((maxDepth - depth2) * falloffMul + falloffAdd); - float weight3 = saturate((maxDepth - depth3) * falloffMul + falloffAdd); - - float weightSum = weight0 + weight1 + weight2 + weight3; - return (weight0 * depth0 + weight1 * depth1 + weight2 * depth2 + weight3 * depth3) * rcp(weightSum); -} - -groupshared float g_scratchDepths[8][8]; -void XeGTAO_PrefilterDepths16x16(uint2 dispatchThreadID, uint2 groupThreadID, Texture2D sourceNDCDepth, RWTexture2D outDepth0, RWTexture2D outDepth1, RWTexture2D outDepth2, RWTexture2D outDepth3, RWTexture2D outDepth4) -{ - // MIP 0 - const uint2 baseCoord = dispatchThreadID; - const uint2 pixCoord = baseCoord * 2; - // Explicit integer Loads of the 2x2 instead of GatherRed — GatherRed on some dgVoodoo depth views - // returns 0 on some drivers (flat depth -> visibility 1 -> AO silently gone). Loads read real - // values. Out-of-bounds Loads return 0 (D3D11-defined), clamped depth. - float d00 = sourceNDCDepth.Load(int3(pixCoord + uint2(0, 0), 0)).x; - float d10 = sourceNDCDepth.Load(int3(pixCoord + uint2(1, 0), 0)).x; - float d01 = sourceNDCDepth.Load(int3(pixCoord + uint2(0, 1), 0)).x; - float d11 = sourceNDCDepth.Load(int3(pixCoord + uint2(1, 1), 0)).x; - float depth0 = XeGTAO_ClampDepth(XeGTAO_ScreenSpaceToViewSpaceDepth(d00)); - float depth1 = XeGTAO_ClampDepth(XeGTAO_ScreenSpaceToViewSpaceDepth(d10)); - float depth2 = XeGTAO_ClampDepth(XeGTAO_ScreenSpaceToViewSpaceDepth(d01)); - float depth3 = XeGTAO_ClampDepth(XeGTAO_ScreenSpaceToViewSpaceDepth(d11)); - outDepth0[pixCoord + uint2(0, 0)] = depth0; - outDepth0[pixCoord + uint2(1, 0)] = depth1; - outDepth0[pixCoord + uint2(0, 1)] = depth2; - outDepth0[pixCoord + uint2(1, 1)] = depth3; - - // MIP 1 - float dm1 = XeGTAO_DepthMIPFilter(depth0, depth1, depth2, depth3); - outDepth1[baseCoord] = dm1; - g_scratchDepths[groupThreadID.x][groupThreadID.y] = dm1; - - GroupMemoryBarrierWithGroupSync(); - - // MIP 2 - [branch] if (all((groupThreadID.xy % 2) == 0)) - { - float inTL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 0]; - float inTR = g_scratchDepths[groupThreadID.x + 1][groupThreadID.y + 0]; - float inBL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 1]; - float inBR = g_scratchDepths[groupThreadID.x + 1][groupThreadID.y + 1]; - - float dm2 = XeGTAO_DepthMIPFilter(inTL, inTR, inBL, inBR); - outDepth2[baseCoord / 2] = dm2; - g_scratchDepths[groupThreadID.x][groupThreadID.y] = dm2; - } - - GroupMemoryBarrierWithGroupSync(); - - // MIP 3 - [branch] if (all((groupThreadID.xy % 4) == 0)) - { - float inTL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 0]; - float inTR = g_scratchDepths[groupThreadID.x + 2][groupThreadID.y + 0]; - float inBL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 2]; - float inBR = g_scratchDepths[groupThreadID.x + 2][groupThreadID.y + 2]; - - float dm3 = XeGTAO_DepthMIPFilter(inTL, inTR, inBL, inBR); - outDepth3[baseCoord / 4] = dm3; - g_scratchDepths[groupThreadID.x][groupThreadID.y] = dm3; - } - - GroupMemoryBarrierWithGroupSync(); - - // MIP 4 - [branch] if (all((groupThreadID.xy % 8) == 0)) - { - float inTL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 0]; - float inTR = g_scratchDepths[groupThreadID.x + 4][groupThreadID.y + 0]; - float inBL = g_scratchDepths[groupThreadID.x + 0][groupThreadID.y + 4]; - float inBR = g_scratchDepths[groupThreadID.x + 4][groupThreadID.y + 4]; - - float dm4 = XeGTAO_DepthMIPFilter(inTL, inTR, inBL, inBR); - outDepth4[baseCoord / 8] = dm4; - // g_scratchDepths[ groupThreadID.x ][ groupThreadID.y ] = dm4; - } -} - -float4 XeGTAO_CalculateEdges(float centerZ, float leftZ, float rightZ, float topZ, float bottomZ) -{ - float4 edgesLRTB = float4(leftZ, rightZ, topZ, bottomZ) - centerZ; - - float slopeLR = (edgesLRTB.y - edgesLRTB.x) * 0.5; - float slopeTB = (edgesLRTB.w - edgesLRTB.z) * 0.5; - float4 edgesLRTBSlopeAdjusted = edgesLRTB + float4(slopeLR, -slopeLR, slopeTB, -slopeTB); - edgesLRTB = min(abs(edgesLRTB), abs(edgesLRTBSlopeAdjusted)); - return saturate(1.25 - edgesLRTB * rcp(centerZ * 0.011)); -} - -// packing/unpacking for edges; 2 bits per edge mean 4 gradient values (0, 0.33, 0.66, 1) for smoother transitions! -float XeGTAO_PackEdges(float4 edgesLRTB) -{ - edgesLRTB = round(saturate(edgesLRTB) * 2.9); - return dot(edgesLRTB, float4(64.0 / 255.0, 16.0 / 255.0, 4.0 / 255.0, 1.0 / 255.0)); -} - -// Inputs are screen XY and viewspace depth, output is viewspace position -float3 XeGTAO_ComputeViewspacePosition(float2 screenPos, float viewspaceDepth) -{ - float3 ret; - ret.xy = (NDC_TO_VIEW_MUL * screenPos.xy + NDC_TO_VIEW_ADD) * viewspaceDepth; - ret.z = viewspaceDepth; - return ret; -} - -#if XE_GTAO_GENERATE_NORMALS -// Depth-derived view-space normal (from the Intel reference; same convention as ComputeViewspacePosition, -// so the space is self-consistent with the positions the main loop uses). -float3 XeGTAO_CalculateNormal(const float4 edgesLRTB, const float3 pixCenterPos, float3 pixLPos, float3 pixRPos, float3 pixTPos, float3 pixBPos) -{ - float4 acceptedNormals = saturate(float4(edgesLRTB.x * edgesLRTB.z, edgesLRTB.z * edgesLRTB.y, edgesLRTB.y * edgesLRTB.w, edgesLRTB.w * edgesLRTB.x) + 0.01); - - pixLPos = normalize(pixLPos - pixCenterPos); - pixRPos = normalize(pixRPos - pixCenterPos); - pixTPos = normalize(pixTPos - pixCenterPos); - pixBPos = normalize(pixBPos - pixCenterPos); - - float3 pixelNormal = acceptedNormals.x * cross(pixLPos, pixTPos) + acceptedNormals.y * cross(pixTPos, pixRPos) + acceptedNormals.z * cross(pixRPos, pixBPos) + acceptedNormals.w * cross(pixBPos, pixLPos); - return normalize(pixelNormal); -} -#endif - -// http://h14s.p5r.org/2012/09/0x5f3759df.html, [Drobot2014a] Low Level Optimizations for GCN, https://blog.selfshadow.com/publications/s2016-shading-course/activision/s2016_pbs_activision_occlusion.pdf slide 63 -float XeGTAO_FastSqrt(float x) -{ - return asfloat(0x1fbd1df5 + (asint(x) >> 1)); -} - -// input [-1, 1] and output [0, PI], from https://seblagarde.wordpress.com/2014/12/01/inverse-trigonometric-functions-gpu-optimization-for-amd-gcn-architecture/ -float XeGTAO_FastACos(float inX) -{ - float x = abs(inX); - float res = -0.156583 * x + 1.570796; - res *= XeGTAO_FastSqrt(1.0 - x); - return inX >= 0 ? res : 3.141593 - res; -} - -void XeGTAO_MainPass(uint2 pixCoord, float2 localNoise, float3 viewspaceNormal, Texture2D sourceViewspaceDepth, SamplerState depthSampler, RWTexture2D outWorkingAOTermAndEdges) -{ - float2 normalizedScreenPos = (pixCoord + 0.5) * VIEWPORT_PIXEL_SIZE; - - // Center + cross depths from the prefiltered (already linearized + scaled) mip0. This texture is our - // own R32F, Gather is safe here (the dgVoodoo view quirk is only on the game's depth view). - float4 valuesUL = sourceViewspaceDepth.GatherRed(depthSampler, float2(pixCoord * VIEWPORT_PIXEL_SIZE)); - float4 valuesBR = sourceViewspaceDepth.GatherRed(depthSampler, float2(pixCoord * VIEWPORT_PIXEL_SIZE), int2(1, 1)); - - // viewspace Z at the center - float viewspaceZ = valuesUL.y; - - // viewspace Zs left top right bottom - const float pixLZ = valuesUL.x; - const float pixTZ = valuesUL.z; - const float pixRZ = valuesBR.z; - const float pixBZ = valuesBR.x; - - float4 edgesLRTB = XeGTAO_CalculateEdges(viewspaceZ, pixLZ, pixRZ, pixTZ, pixBZ); - const float edges = XeGTAO_PackEdges(edgesLRTB); - -#if DEVELOPMENT - // Debug views (visible on screen through the game's own AO blur/apply chain; the final denoise passes - // raw values through when DebugViewRT > 0). - if (DebugViewRT > 0.5 && DebugViewRT < 1.5) // 1 = depth gradient (proves live depth + linearization/scale) - { - outWorkingAOTermAndEdges[pixCoord] = float2(saturate(frac(log2(max(viewspaceZ, 1e-6)))), 1.0); - return; - } - if (DebugViewRT >= 3.5 && DebugViewRT < 4.5) // 4 = edges - { - outWorkingAOTermAndEdges[pixCoord] = float2(dot(edgesLRTB, 0.25), 1.0); - return; - } -#endif - -#if XE_GTAO_GENERATE_NORMALS - // TW2 has no normal buffer at this point of the frame: derive the view-space normal from the depth - // cross, edge-weighted (Intel reference path). Requires the R32F pyramid we already build. - { - float3 CENTER = XeGTAO_ComputeViewspacePosition(normalizedScreenPos, viewspaceZ); - float3 LEFT = XeGTAO_ComputeViewspacePosition(normalizedScreenPos + float2(-1.0, 0.0) * VIEWPORT_PIXEL_SIZE, pixLZ); - float3 RIGHT = XeGTAO_ComputeViewspacePosition(normalizedScreenPos + float2(1.0, 0.0) * VIEWPORT_PIXEL_SIZE, pixRZ); - float3 TOP = XeGTAO_ComputeViewspacePosition(normalizedScreenPos + float2(0.0, -1.0) * VIEWPORT_PIXEL_SIZE, pixTZ); - float3 BOTTOM = XeGTAO_ComputeViewspacePosition(normalizedScreenPos + float2(0.0, 1.0) * VIEWPORT_PIXEL_SIZE, pixBZ); - viewspaceNormal = XeGTAO_CalculateNormal(edgesLRTB, CENTER, LEFT, RIGHT, TOP, BOTTOM); - } -#endif - - // Move center pixel slightly towards camera to avoid imprecision artifacts due to depth buffer imprecision; offset depends on depth texture format used - viewspaceZ *= 0.99999; // this is good for FP32 depth buffer - - const float3 pixCenterPos = XeGTAO_ComputeViewspacePosition(normalizedScreenPos, viewspaceZ); - const float3 viewVec = normalize(-pixCenterPos); - - // prevents normals that are facing away from the view vector - xeGTAO struggles with extreme cases, but in Vanilla it seems rare so it's disabled by default - viewspaceNormal = normalize(viewspaceNormal + max(0, -dot(viewspaceNormal, viewVec)) * viewVec); - -#if DEVELOPMENT - if (DebugViewRT >= 1.5 && DebugViewRT < 2.5) // 2 = normals view-facing term (smooth per-surface shading = correct space/convention) - { - outWorkingAOTermAndEdges[pixCoord] = float2(saturate(abs(dot(viewspaceNormal, viewVec))), 1.0); - return; - } -#endif - - // Capped at the native HBAO kernel, cb4[16].y pixels. - const float effectRadius = min(XeGTAO_EffectRadius(), cb4[16].y * viewspaceZ * NDC_TO_VIEW_MUL_X_PIXEL_SIZE.x); - const float sampleDistributionPower = SAMPLE_DISTRIBUTION_POWER; - const float thinOccluderCompensation = THIN_OCCLUDER_COMPENSATION; - const float falloffRange = EFFECT_FALLOFF_RANGE * effectRadius; - const float falloffFrom = effectRadius * (1.0 - EFFECT_FALLOFF_RANGE); - - // fadeout precompute optimisation - const float falloffMul = -1.0 / falloffRange; - const float falloffAdd = falloffFrom / falloffRange + 1.0; - - float visibility = 0.0; - - // see "Algorithm 1" in https://www.activision.com/cdn/research/Practical_Real_Time_Strategies_for_Accurate_Indirect_Occlusion_NEW%20VERSION_COLOR.pdf - { - const float noiseSlice = localNoise.x; - const float noiseSample = localNoise.y; - - // quality settings / tweaks / hacks - const float pixelTooCloseThreshold = 1.3; // if the offset is under approx pixel size (pixelTooCloseThreshold), push it out to the minimum distance - - // approx viewspace pixel size at pixCoord; approximation of NDCToViewspace( normalizedScreenPos.xy + consts.ViewportPixelSize.xy, pixCenterPos.z ).xy - pixCenterPos.xy; - const float2 pixelDirRBViewspaceSizeAtCenterZ = viewspaceZ.xx * NDC_TO_VIEW_MUL_X_PIXEL_SIZE; - - float screenspaceRadius = effectRadius * rcp(abs(pixelDirRBViewspaceSizeAtCenterZ.x)); - - // fade out for small screen radii - visibility += saturate((10.0 - screenspaceRadius) / 100.0) * 0.5; - - // this is the min distance to start sampling from to avoid sampling from the center pixel (no useful data obtained from sampling center pixel) - const float minS = pixelTooCloseThreshold * rcp(screenspaceRadius); - - //[unroll] - for (float slice = 0.0; slice < SLICE_COUNT; slice++) - { - float sliceK = (slice + noiseSlice) / SLICE_COUNT; - // lines 5, 6 from the paper - float phi = sliceK * XE_GTAO_PI; - float cosPhi = cos(phi); - float sinPhi = sin(phi); - float2 omega = float2(cosPhi, -sinPhi); // lpfloat2 on omega causes issues with big radii - - // convert to screen units (pixels) for later use - omega *= screenspaceRadius; - - // line 8 from the paper - const float3 directionVec = float3(cosPhi, sinPhi, 0.0); - - // line 9 from the paper - const float3 orthoDirectionVec = directionVec - (dot(directionVec, viewVec) * viewVec); - - // line 10 from the paper - // axisVec is orthogonal to directionVec and viewVec, used to define projectedNormal - const float3 axisVec = normalize(cross(orthoDirectionVec, viewVec)); - - // line 11 from the paper - float3 projectedNormalVec = viewspaceNormal - axisVec * dot(viewspaceNormal, axisVec); - - // line 13 from the paper - float signNorm = sign(dot(orthoDirectionVec, projectedNormalVec)); - - // line 14 from the paper - float projectedNormalVecLength = length(projectedNormalVec); - float cosNorm = saturate(dot(projectedNormalVec, viewVec) * rcp(projectedNormalVecLength)); - - // line 15 from the paper - float n = signNorm * XeGTAO_FastACos(cosNorm); - - // this is a lower weight target; not using -1 as in the original paper because it is under horizon, so a 'weight' has different meaning based on the normal - const float lowHorizonCos0 = cos(n + XE_GTAO_PI_HALF); - const float lowHorizonCos1 = cos(n - XE_GTAO_PI_HALF); - - // lines 17, 18 from the paper, manually unrolled the 'side' loop - float horizonCos0 = lowHorizonCos0; //-1; - float horizonCos1 = lowHorizonCos1; //-1; - - [unroll] for (float step = 0.0; step < STEPS_PER_SLICE; step++) - { - // R1 sequence (http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/) - const float stepBaseNoise = (slice + step * STEPS_PER_SLICE) * 0.6180339887498948482; // <- this should unroll - float stepNoise = frac(noiseSample + stepBaseNoise); - - // approx line 20 from the paper, with added noise - float s = (step + stepNoise) / STEPS_PER_SLICE; // + (lpfloat2)1e-6f); - - // additional distribution modifier - s = pow(s, sampleDistributionPower); - - // avoid sampling center pixel - s += minS; - - // approx lines 21-22 from the paper, unrolled - float2 sampleOffset = s * omega; - - float sampleOffsetLength = length(sampleOffset); - - // note: when sampling, using point_point_point or point_point_linear sampler works, but linear_linear_linear will cause unwanted interpolation between neighbouring depth values on the same MIP level! - const float mipLevel = clamp(log2(sampleOffsetLength) - DEPTH_MIP_SAMPLING_OFFSET, 0.0, XE_GTAO_DEPTH_MIP_LEVELS); - - // Snap to pixel center (more correct direction math, avoids artifacts due to sampling pos not matching depth texel center - messes up slope - but adds other - // artifacts due to them being pushed off the slice). Also use full precision for high res cases. - sampleOffset = round(sampleOffset) * VIEWPORT_PIXEL_SIZE; - - float2 sampleScreenPos0 = normalizedScreenPos + sampleOffset; - float SZ0 = sourceViewspaceDepth.SampleLevel(depthSampler, sampleScreenPos0, mipLevel).x; - float3 samplePos0 = XeGTAO_ComputeViewspacePosition(sampleScreenPos0, SZ0); - - float2 sampleScreenPos1 = normalizedScreenPos - sampleOffset; - float SZ1 = sourceViewspaceDepth.SampleLevel(depthSampler, sampleScreenPos1, mipLevel).x; - float3 samplePos1 = XeGTAO_ComputeViewspacePosition(sampleScreenPos1, SZ1); - - float3 sampleDelta0 = samplePos0 - pixCenterPos; // using lpfloat for sampleDelta causes precision issues - float3 sampleDelta1 = samplePos1 - pixCenterPos; // using lpfloat for sampleDelta causes precision issues - float sampleDist0 = length(sampleDelta0); - float sampleDist1 = length(sampleDelta1); - - // approx lines 23, 24 from the paper, unrolled - float3 sampleHorizonVec0 = sampleDelta0 * rcp(sampleDist0); - float3 sampleHorizonVec1 = sampleDelta1 * rcp(sampleDist1); - - // any sample out of radius should be discarded - also use fallof range for smooth transitions; this is a modified idea from "4.3 Implementation details, Bounding the sampling area" - // this is our own thickness heuristic that relies on sooner discarding samples behind the center - float falloffBase0 = length(float3(sampleDelta0.x, sampleDelta0.y, sampleDelta0.z * (1.0 + thinOccluderCompensation))); - float falloffBase1 = length(float3(sampleDelta1.x, sampleDelta1.y, sampleDelta1.z * (1.0 + thinOccluderCompensation))); - float weight0 = saturate(falloffBase0 * falloffMul + falloffAdd); - float weight1 = saturate(falloffBase1 * falloffMul + falloffAdd); - - // sample horizon cos - float shc0 = dot(sampleHorizonVec0, viewVec); - float shc1 = dot(sampleHorizonVec1, viewVec); - - // discard unwanted samples - shc0 = lerp(lowHorizonCos0, shc0, weight0); // this would be more correct but too expensive: cos(lerp( acos(lowHorizonCos0), acos(shc0), weight0 )); - shc1 = lerp(lowHorizonCos1, shc1, weight1); // this would be more correct but too expensive: cos(lerp( acos(lowHorizonCos1), acos(shc1), weight1 )); - - // thickness heuristic disabled (THIN_OCCLUDER_COMPENSATION == 0) - horizonCos0 = max(horizonCos0, shc0); - horizonCos1 = max(horizonCos1, shc1); - } - -#if 1 // I can't figure out the slight overdarkening on high slopes, so I'm adding this fudge - in the training set, 0.05 is close (PSNR 21.34) to disabled (PSNR 21.45) - projectedNormalVecLength = lerp(projectedNormalVecLength, 1.0, 0.05); -#endif - - // line ~27, unrolled - float h0 = -XeGTAO_FastACos(horizonCos1); - float h1 = XeGTAO_FastACos(horizonCos0); - float iarc0 = (cosNorm + 2.0 * h0 * sin(n) - cos(2.0 * h0 - n)) / 4.0; - float iarc1 = (cosNorm + 2.0 * h1 * sin(n) - cos(2.0 * h1 - n)) / 4.0; - float localVisibility = projectedNormalVecLength * (iarc0 + iarc1); - visibility += localVisibility; - } - visibility /= SLICE_COUNT; - visibility = pow(max(0.0, visibility), max(0.05, FinalValuePowerRT)); // runtime dial (calibrated to the vanilla AO histogram); max(0,) also satisfies fxc strict X3571 - visibility = max(0.0, visibility); // no occlusion floor: TW2's native HBAO crushes creases all the way to 0 (Intel's 0.03 floor exists for bent-normal packing, unused here) - } - - visibility = saturate(visibility / XE_GTAO_OCCLUSION_TERM_SCALE); - outWorkingAOTermAndEdges[pixCoord] = float2(visibility, edges); -} - -void XeGTAO_DecodeGatherPartial(float4 packedValue, out float outDecoded[4]) -{ - for (int i = 0; i < 4; i++) - { - outDecoded[i] = packedValue[i]; - } -} - -float4 XeGTAO_UnpackEdges(float _packedVal) -{ - uint packedVal = uint(_packedVal * 255.5); - float4 edgesLRTB; - edgesLRTB.x = float((packedVal >> 6) & 0x03) / 3.0; // there's really no need for mask (as it's an 8 bit input) but I'll leave it in so it doesn't cause any trouble in the future - edgesLRTB.y = float((packedVal >> 4) & 0x03) / 3.0; - edgesLRTB.z = float((packedVal >> 2) & 0x03) / 3.0; - edgesLRTB.w = float((packedVal >> 0) & 0x03) / 3.0; +#define NoiseIndexRT 0 - return saturate(edgesLRTB); -} - -void XeGTAO_AddSample(float ssaoValue, float edgeValue, inout float sum, inout float sumWeight) -{ - float weight = edgeValue; - - sum += weight * ssaoValue; - sumWeight += weight; -} +// Capped at the native HBAO kernel, cb4[16].y pixels. +#define XE_GTAO_MAIN_PASS_EFFECT_RADIUS(viewspaceZ) min(XeGTAO_EffectRadius(), cb4[16].y * viewspaceZ * NDC_TO_VIEW_MUL_X_PIXEL_SIZE.x) -void XeGTAO_Denoise(uint2 pixCoordBase, Texture2D sourceAOTermAndEdges, SamplerState texSampler, -#if XE_GTAO_FINAL_APPLY - RWTexture2D outputTexture // copy-source in the game AO RT's actual format; the pack pass reads only .x (AO) -#else - RWTexture2D outputTexture -#endif -) -{ -#if DEVELOPMENT - // Debug views: pass the raw working value through unblurred so the on-screen viz is exact. - if (DebugViewRT > 0.5) - { - for (int dside = 0; dside < 2; dside++) - { - const uint2 dpix = uint2(pixCoordBase.x + dside, pixCoordBase.y); - float v = sourceAOTermAndEdges.Load(int3(dpix, 0)).x; -#if XE_GTAO_FINAL_APPLY - if (DebugViewRT >= 2.5 && DebugViewRT < 3.5) // 3 = AO x8 amplification (spot broad over-occlusion) - v = saturate(1.0 - (1.0 - v * XE_GTAO_OCCLUSION_TERM_SCALE) * 8.0); - outputTexture[dpix] = float4(v, 0.0, 0.0, 0.0); -#else - outputTexture[dpix] = float2(v, sourceAOTermAndEdges.Load(int3(dpix, 0)).y); -#endif - } - return; - } -#endif - -#if XE_GTAO_FINAL_APPLY - const float blurAmount = DENOISE_BLUR_BETA; -#else - const float blurAmount = DENOISE_BLUR_BETA / 5.0; -#endif - - const float diagWeight = 0.85 * 0.5; - - float aoTerm[2]; // pixel pixCoordBase and pixel pixCoordBase + int2( 1, 0 ) - float4 edgesC_LRTB[2]; - float weightTL[2]; - float weightTR[2]; - float weightBL[2]; - float weightBR[2]; - - // gather edge and visibility quads, used later - const float2 gatherCenter = float2(pixCoordBase.x, pixCoordBase.y) * VIEWPORT_PIXEL_SIZE; - float4 edgesQ0 = sourceAOTermAndEdges.GatherGreen(texSampler, gatherCenter, int2(0, 0)); - float4 edgesQ1 = sourceAOTermAndEdges.GatherGreen(texSampler, gatherCenter, int2(2, 0)); - float4 edgesQ2 = sourceAOTermAndEdges.GatherGreen(texSampler, gatherCenter, int2(1, 2)); - - float visQ0[4]; - XeGTAO_DecodeGatherPartial(sourceAOTermAndEdges.GatherRed(texSampler, gatherCenter, int2(0, 0)), visQ0); - float visQ1[4]; - XeGTAO_DecodeGatherPartial(sourceAOTermAndEdges.GatherRed(texSampler, gatherCenter, int2(2, 0)), visQ1); - float visQ2[4]; - XeGTAO_DecodeGatherPartial(sourceAOTermAndEdges.GatherRed(texSampler, gatherCenter, int2(0, 2)), visQ2); - float visQ3[4]; - XeGTAO_DecodeGatherPartial(sourceAOTermAndEdges.GatherRed(texSampler, gatherCenter, int2(2, 2)), visQ3); - - for (int side = 0; side < 2; side++) - { - const int2 pixCoord = int2(pixCoordBase.x + side, pixCoordBase.y); - - float4 edgesL_LRTB = XeGTAO_UnpackEdges(side == 0 ? edgesQ0.x : edgesQ0.y); - float4 edgesT_LRTB = XeGTAO_UnpackEdges(side == 0 ? edgesQ0.z : edgesQ1.w); - float4 edgesR_LRTB = XeGTAO_UnpackEdges(side == 0 ? edgesQ1.x : edgesQ1.y); - float4 edgesB_LRTB = XeGTAO_UnpackEdges(side == 0 ? edgesQ2.w : edgesQ2.z); - - edgesC_LRTB[side] = XeGTAO_UnpackEdges(side == 0 ? edgesQ0.y : edgesQ1.x); - - // Edges aren't perfectly symmetrical: edge detection algorithm does not guarantee that a left edge on the right pixel will match the right edge on the left pixel (although - // they will match in majority of cases). This line further enforces the symmetricity, creating a slightly sharper blur. Works real nice with TAA. - edgesC_LRTB[side] *= float4(edgesL_LRTB.y, edgesR_LRTB.x, edgesT_LRTB.w, edgesB_LRTB.z); - -#if 1 // this allows some small amount of AO leaking from neighbours if there are 3 or 4 edges; this reduces both spatial and temporal aliasing - const float leak_threshold = 2.5; - const float leak_strength = 0.5; - float edginess = (saturate(4.0 - leak_threshold - dot(edgesC_LRTB[side], 1.0)) * rcp(4.0 - leak_threshold)) * leak_strength; - edgesC_LRTB[side] = saturate(edgesC_LRTB[side] + edginess); -#endif - - // for diagonals; used by first and second pass - weightTL[side] = diagWeight * (edgesC_LRTB[side].x * edgesL_LRTB.z + edgesC_LRTB[side].z * edgesT_LRTB.x); - weightTR[side] = diagWeight * (edgesC_LRTB[side].z * edgesT_LRTB.y + edgesC_LRTB[side].y * edgesR_LRTB.z); - weightBL[side] = diagWeight * (edgesC_LRTB[side].w * edgesB_LRTB.x + edgesC_LRTB[side].x * edgesL_LRTB.w); - weightBR[side] = diagWeight * (edgesC_LRTB[side].y * edgesR_LRTB.w + edgesC_LRTB[side].w * edgesB_LRTB.y); - - // first pass - float ssaoValue = side == 0 ? visQ0[1] : visQ1[0]; - float ssaoValueL = side == 0 ? visQ0[0] : visQ0[1]; - float ssaoValueT = side == 0 ? visQ0[2] : visQ1[3]; - float ssaoValueR = side == 0 ? visQ1[0] : visQ1[1]; - float ssaoValueB = side == 0 ? visQ2[2] : visQ3[3]; - float ssaoValueTL = side == 0 ? visQ0[3] : visQ0[2]; - float ssaoValueBR = side == 0 ? visQ3[3] : visQ3[2]; - float ssaoValueTR = side == 0 ? visQ1[3] : visQ1[2]; - float ssaoValueBL = side == 0 ? visQ2[3] : visQ2[2]; - - float sumWeight = blurAmount; - float sum = ssaoValue * sumWeight; - - XeGTAO_AddSample(ssaoValueL, edgesC_LRTB[side].x, sum, sumWeight); - XeGTAO_AddSample(ssaoValueR, edgesC_LRTB[side].y, sum, sumWeight); - XeGTAO_AddSample(ssaoValueT, edgesC_LRTB[side].z, sum, sumWeight); - XeGTAO_AddSample(ssaoValueB, edgesC_LRTB[side].w, sum, sumWeight); - - XeGTAO_AddSample(ssaoValueTL, weightTL[side], sum, sumWeight); - XeGTAO_AddSample(ssaoValueTR, weightTR[side], sum, sumWeight); - XeGTAO_AddSample(ssaoValueBL, weightBL[side], sum, sumWeight); - XeGTAO_AddSample(ssaoValueBR, weightBR[side], sum, sumWeight); - - aoTerm[side] = sum / sumWeight; - -#if XE_GTAO_FINAL_APPLY - // Vanilla generator writes (AO, viewZ, 0, 0); the downstream pack pass 0x953119B5 reads only .x from - // this target (it re-reads depth from the depth texture), so .yzw can stay 0. - outputTexture[pixCoord] = float4(saturate(aoTerm[side] * XE_GTAO_OCCLUSION_TERM_SCALE), 0.0, 0.0, 0.0); -#else - outputTexture[pixCoord] = float2(aoTerm[side], side == 0 ? edgesQ0.y : edgesQ1.x); -#endif - } -} - -// Implementation -///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -SamplerState smp : register(s0); // point-clamp, bound by Luma (the hooked draw's s0 belongs to the pixel pipeline, not CS) - -Texture2D tex0 : register(t0); - -RWTexture2D out_working_depth_mip0 : register(u0); -RWTexture2D out_working_depth_mip1 : register(u1); -RWTexture2D out_working_depth_mip2 : register(u2); -RWTexture2D out_working_depth_mip3 : register(u3); -RWTexture2D out_working_depth_mip4 : register(u4); -RWTexture2D ao_term_and_edges : register(u0); - -#if XE_GTAO_FINAL_APPLY // Plain float4 (not unorm): the copy-source texture matches the game AO RT's ACTUAL format, which is // rgba16_float whenever Luma's swapchain-aspect r8g8b8a8 upgrade catches it (and unorm8 otherwise) — // float4 UAV writes are valid against both. -RWTexture2D final_output : register(u0); -#else -RWTexture2D final_output : register(u0); -#endif +#define XE_GTAO_FINAL_OUTPUT_TYPE float4 +// Vanilla generator writes (AO, viewZ, 0, 0); the downstream pack pass 0x953119B5 reads only .x from +// this target (it re-reads depth from the depth texture), so .yzw can stay 0. +#define XE_GTAO_ENCODE_FINAL(v) float4(v, 0.0, 0.0, 0.0) -#define XE_GTAO_NUMTHREADS_X 8 -#define XE_GTAO_NUMTHREADS_Y 8 - -// From https://www.shadertoy.com/view/3tB3z3 - except we're using R2 here -#define XE_HILBERT_LEVEL 6U -#define XE_HILBERT_WIDTH (1U << XE_HILBERT_LEVEL) -#define XE_HILBERT_AREA (XE_HILBERT_WIDTH * XE_HILBERT_WIDTH) -uint HilbertIndex(uint posX, uint posY) -{ - uint index = 0U; - [unroll] for (uint curLevel = XE_HILBERT_WIDTH / 2U; curLevel > 0U; curLevel /= 2U) - { - uint regionX = (posX & curLevel) > 0U; - uint regionY = (posY & curLevel) > 0U; - index += curLevel * curLevel * ((3U * regionX) ^ regionY); - if (regionY == 0U) - { - if (regionX == 1U) - { - posX = XE_HILBERT_WIDTH - 1U - posX; - posY = XE_HILBERT_WIDTH - 1U - posY; - } - uint temp = posX; - posX = posY; - posY = temp; - } - } - return index; -} - -// TW2 has no TAA: temporalIndex is ALWAYS 0 (frozen pattern — static noise instead of boiling). -float2 SpatioTemporalNoise(uint2 pixCoord, uint temporalIndex) -{ - float2 noise; - uint index = HilbertIndex(pixCoord.x, pixCoord.y); - index += 288 * (temporalIndex % 64); // why 288? tried out a few and that's the best so far (with XE_HILBERT_LEVEL 6U) - but there's probably better :) - // R2 sequence - see http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/ - return float2(frac(0.5 + index * float2(0.75487766624669276005, 0.5698402909980532659114))); -} - -[numthreads(8, 8, 1)] // <- hard coded to 8x8; each thread computes 2x2 blocks so processing 16x16 block: Dispatch needs to be called with (width + 16-1) / 16, (height + 16-1) / 16 - void prefilter_depths16x16_cs(uint2 dtid : SV_DispatchThreadID, uint2 gtid : SV_GroupThreadID) { - // tex0 = the game's half-res r32_float linear view-space depth (the hooked HBAO draw's t0) - XeGTAO_PrefilterDepths16x16(dtid, gtid, tex0, out_working_depth_mip0, out_working_depth_mip1, out_working_depth_mip2, out_working_depth_mip3, out_working_depth_mip4); - } - - [numthreads(XE_GTAO_NUMTHREADS_X, XE_GTAO_NUMTHREADS_Y, 1)] void main_pass_cs(uint2 dtid : SV_DispatchThreadID) -{ - // tex0 = prefiltered viewspace depth MIP pyramid (our R32F) - // smp = point-clamp - // Normals are generated from depth inside XeGTAO_MainPass (XE_GTAO_GENERATE_NORMALS) — no t1 input. - XeGTAO_MainPass(dtid, SpatioTemporalNoise(dtid, 0), float3(0.0, 0.0, -1.0), tex0, smp, ao_term_and_edges); -} - -[numthreads(XE_GTAO_NUMTHREADS_X, XE_GTAO_NUMTHREADS_Y, 1)] void denoise_pass_cs(uint2 dtid : SV_DispatchThreadID) { - // tex0 = g_srcWorkingAOTerm and g_srcWorkingEdges, packed - // smp = point-clamp - const uint2 pix_coord_base = dtid * uint2(2, 1); // we're computing 2 horizontal pixels at a time (performance optimization) - XeGTAO_Denoise(pix_coord_base, tex0, smp, final_output); -} +// tex0 = the game's half-res r32_float linear view-space depth (the hooked HBAO draw's t0) +#include "../Includes/XeGTAO.hlsl" diff --git a/Source/Games/Borderlands 2 and The Pre-Sequel/main.cpp b/Source/Games/Borderlands 2 and The Pre-Sequel/main.cpp index 931a53e42..930b08b3d 100644 --- a/Source/Games/Borderlands 2 and The Pre-Sequel/main.cpp +++ b/Source/Games/Borderlands 2 and The Pre-Sequel/main.cpp @@ -539,7 +539,7 @@ class Borderlands2 final : public Game // Core auto-registers the 6 SMAA passes; these three are this game's own. RCAS sharpen PS (drawn via core // "Copy VS" + DrawCustomPixelShader after SMAA). native_shaders_definitions.emplace(CompileTimeStringHash("BL2TPS Sharpen PS"), - ShaderDefinition{"Luma_BL2TPS_Sharpen", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); + ShaderDefinition{"Luma_RCAS_PS", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); // Depth-extract CS for SMAA predication: scene-color .a (linear view Z) -> R16F plane-deviation edge-ness. native_shaders_definitions.emplace(CompileTimeStringHash("BL2TPS Depth Extract CS"), ShaderDefinition("Luma_BL2TPS_DepthExtract", reshade::api::pipeline_subobject_type::compute_shader)); diff --git a/Source/Games/Borderlands GOTY Enhanced/main.cpp b/Source/Games/Borderlands GOTY Enhanced/main.cpp index fc8ccf871..c4a7e0777 100644 --- a/Source/Games/Borderlands GOTY Enhanced/main.cpp +++ b/Source/Games/Borderlands GOTY Enhanced/main.cpp @@ -537,7 +537,7 @@ class BorderlandsGoty final : public Game ShaderDefinition("Luma_BL_DepthExtract", reshade::api::pipeline_subobject_type::compute_shader)); // RCAS sharpen PS (drawn via core "Copy VS" + DrawCustomPixelShader after SMAA). native_shaders_definitions.emplace(CompileTimeStringHash("BL Sharpen PS"), - ShaderDefinition{"Luma_BL_Sharpen", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); + ShaderDefinition{"Luma_RCAS_PS", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); // XeGTAO (replaces the game's native HBAO+; see the AO hash block above). 4 compute passes out of one // file; the two denoise variants differ only by XE_GTAO_FINAL_APPLY (the final one writes the game's diff --git a/Source/Games/Mass Effect Andromeda/main.cpp b/Source/Games/Mass Effect Andromeda/main.cpp index a28a3b612..6c4958661 100644 --- a/Source/Games/Mass Effect Andromeda/main.cpp +++ b/Source/Games/Mass Effect Andromeda/main.cpp @@ -457,7 +457,7 @@ class MassEffectAndromeda final : public Game #if ENABLE_SMAA // RCAS sharpening PS for the SMAA output (reuses core's "Copy VS" fullscreen vertex shader). native_shaders_definitions.emplace(CompileTimeStringHash("MEA Sharpen PS"), - ShaderDefinition{"Luma_MEA_Sharpen", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); + ShaderDefinition{"Luma_RCAS_PS", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); #endif #if ENABLE_SR // Format-converting SR output hand-off: the game's "Buffer Format" setting can switch the TAA resolve diff --git a/Source/Games/Mass Effect Legendary Edition/main.cpp b/Source/Games/Mass Effect Legendary Edition/main.cpp index a40c55731..eb0f5da6e 100644 --- a/Source/Games/Mass Effect Legendary Edition/main.cpp +++ b/Source/Games/Mass Effect Legendary Edition/main.cpp @@ -391,7 +391,7 @@ class MassEffectLE final : public Game native_shaders_definitions.emplace(kNameSMAALinearizeCS, ShaderDefinition("Luma_MELE_SMAALinearize", reshade::api::pipeline_subobject_type::compute_shader)); native_shaders_definitions.emplace(kNameSharpenPS, - ShaderDefinition{"Luma_MELE_Sharpen", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); + ShaderDefinition{"Luma_RCAS_PS", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); // Four XeGTAO compute entries share one source; XE_GTAO_FINAL_APPLY selects the game's R8_UNORM target. native_shaders_definitions.emplace(kNameGTAOPrefilterCS, diff --git a/Source/Games/Mass Effect/main.cpp b/Source/Games/Mass Effect/main.cpp index 0d4925b90..217efdce9 100644 --- a/Source/Games/Mass Effect/main.cpp +++ b/Source/Games/Mass Effect/main.cpp @@ -599,7 +599,7 @@ class MassEffect final : public Game ShaderDefinition("Luma_ME1_DepthExtract", reshade::api::pipeline_subobject_type::compute_shader)); // RCAS PS, drawn via core "Copy VS" + DrawCustomPixelShader. native_shaders_definitions.emplace(CompileTimeStringHash("ME1 Sharpen PS"), - ShaderDefinition{"Luma_ME1_Sharpen", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); + ShaderDefinition{"Luma_RCAS_PS", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); #endif // Address-space ceiling, PROBED: no LARGE_ADDRESS_AWARE (0x0102 measured) = 2 GB, Luma's 4K scratch ~365 MB. diff --git a/Source/Games/Medal of Honor Airborne/main.cpp b/Source/Games/Medal of Honor Airborne/main.cpp index 2de39fe49..5562baba9 100644 --- a/Source/Games/Medal of Honor Airborne/main.cpp +++ b/Source/Games/Medal of Honor Airborne/main.cpp @@ -466,7 +466,7 @@ class MedalOfHonorAirborne final : public Game ShaderDefinition("Luma_MOHA_DepthExtract", reshade::api::pipeline_subobject_type::compute_shader)); // RCAS sharpen PS, drawn via core "Copy VS" + DrawCustomPixelShader after SMAA. native_shaders_definitions.emplace(CompileTimeStringHash("MOHA Sharpen PS"), - ShaderDefinition{"Luma_MOHA_Sharpen", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); + ShaderDefinition{"Luma_RCAS_PS", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); #endif // Buffers stay in GAMMA space: the gamma-SDR HUD blends onto this canvas and a linear buffer washes it out. diff --git a/Source/Games/The Witcher 2/main.cpp b/Source/Games/The Witcher 2/main.cpp index 645df4782..d21aa2751 100644 --- a/Source/Games/The Witcher 2/main.cpp +++ b/Source/Games/The Witcher 2/main.cpp @@ -981,7 +981,7 @@ class TheWitcher2Game final : public Game ShaderDefinition("Luma_TW2_SMAALinearize", reshade::api::pipeline_subobject_type::compute_shader)); // RCAS sharpen PS (drawn via core "Copy VS" + DrawCustomPixelShader after SMAA). native_shaders_definitions.emplace(CompileTimeStringHash("TW2 Sharpen PS"), - ShaderDefinition{"Luma_TW2_Sharpen", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); + ShaderDefinition{"Luma_RCAS_PS", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); // Depth-extract CS for SMAA predication: game r32f LINEAR view-space depth -> R16F edge-ness in [0,1]. native_shaders_definitions.emplace(CompileTimeStringHash("TW2 Depth Extract CS"), ShaderDefinition("Luma_TW2_DepthExtract", reshade::api::pipeline_subobject_type::compute_shader)); From bbbaeb332df8881c384194d1a4335adda074d467 Mon Sep 17 00:00:00 2001 From: Hlib Omelchenko Date: Wed, 7 Oct 2026 15:03:26 +0200 Subject: [PATCH 2/4] Shaders: Use the shared SMAA passes in BioShock and Metal Gear Solid 4 too --- Shaders/BioShock Series/Luma_SMAA_impl.hlsl | 84 +------------------ .../Luma_SMAA_impl.hlsl | 83 +----------------- 2 files changed, 6 insertions(+), 161 deletions(-) diff --git a/Shaders/BioShock Series/Luma_SMAA_impl.hlsl b/Shaders/BioShock Series/Luma_SMAA_impl.hlsl index f47fef680..eab99d514 100644 --- a/Shaders/BioShock Series/Luma_SMAA_impl.hlsl +++ b/Shaders/BioShock Series/Luma_SMAA_impl.hlsl @@ -5,86 +5,6 @@ #define SMAA_RT_METRICS float4(LumaSettings.GameSettings.InvOutputRes, LumaSettings.GameSettings.OutputRes) #define SMAA_PRESET_ULTRA -#define SMAA_PREDICATION 1 -#define SMAA_CUSTOM_SL -SamplerState LinearSampler : register(s0); -SamplerState PointSampler : register(s1); -#define SMAATexture2D(tex) Texture2D tex -#define SMAATexturePass2D(tex) tex -#define SMAASampleLevelZero(tex, coord) tex.SampleLevel(LinearSampler, coord, 0) -#define SMAASampleLevelZeroPoint(tex, coord) tex.SampleLevel(PointSampler, coord, 0) -#define SMAASampleLevelZeroOffset(tex, coord, offset) tex.SampleLevel(LinearSampler, coord, 0, offset) -#define SMAASample(tex, coord) tex.Sample(LinearSampler, coord) -#define SMAASamplePoint(tex, coord) tex.Sample(PointSampler, coord) -#define SMAASampleOffset(tex, coord, offset) tex.Sample(LinearSampler, coord, offset) -#define SMAA_FLATTEN [flatten] -#define SMAA_BRANCH [branch] -#define SMAATexture2DMS2(tex) Texture2DMS tex -#define SMAALoad(tex, pos, sample) tex.Load(pos, sample) +#define SMAA_PREDICATION 1 #define SMAAGather(tex, coord) tex.Gather(LinearSampler, coord, 0) -#include "../Includes/SMAA.hlsl" - -Texture2D tex0 : register(t0); -Texture2D tex1 : register(t1); -Texture2D tex2 : register(t2); - -void fullscreen_triangle(uint id, out float4 position, out float2 texcoord) -{ - texcoord = float2((id << 1) & 2, id & 2); - position = float4(texcoord * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); -} - -// SMAAEdgeDetection -// - -void smaa_edge_detection_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset[3] : TEXCOORD1) -{ - fullscreen_triangle(id, position, texcoord); - SMAAEdgeDetectionVS(texcoord, offset); -} - -float2 smaa_edge_detection_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset[3] : TEXCOORD1) : SV_Target -{ - // tex0 = colorTexGamma - // tex1 = predicationTex - return SMAAColorEdgeDetectionPS(texcoord, offset, tex0, tex1); -} - -// - -// SMAABlendingWeightCalculation -// - -void smaa_blending_weight_calculation_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float2 pixcoord : TEXCOORD1, out float4 offset[3] : TEXCOORD2) -{ - fullscreen_triangle(id, position, texcoord); - SMAABlendingWeightCalculationVS(texcoord, pixcoord, offset); -} - -float4 smaa_blending_weight_calculation_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float2 pixcoord : TEXCOORD1, float4 offset[3] : TEXCOORD2) : SV_Target -{ - // tex0 = edgesTex - // tex1 = areaTex - // tex2 = searchTex - return SMAABlendingWeightCalculationPS(texcoord, pixcoord, offset, tex0, tex1, tex2, 0); -} - -// - -// SMAANeighborhoodBlending -// - -void smaa_neighborhood_blending_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset : TEXCOORD1) -{ - fullscreen_triangle(id, position, texcoord); - SMAANeighborhoodBlendingVS(texcoord, offset); -} - -float4 smaa_neighborhood_blending_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset : TEXCOORD1) : SV_Target -{ - // tex0 = colorTex - // tex1 = blendTex - return SMAANeighborhoodBlendingPS(texcoord, offset, tex0, tex1); -} - -// \ No newline at end of file +#include "../Includes/SMAA_Passes.hlsl" diff --git a/Shaders/Metal Gear Solid 4 Guns of the Patriots/Luma_SMAA_impl.hlsl b/Shaders/Metal Gear Solid 4 Guns of the Patriots/Luma_SMAA_impl.hlsl index 51c05de14..34b5a6b87 100644 --- a/Shaders/Metal Gear Solid 4 Guns of the Patriots/Luma_SMAA_impl.hlsl +++ b/Shaders/Metal Gear Solid 4 Guns of the Patriots/Luma_SMAA_impl.hlsl @@ -9,88 +9,13 @@ #define SMAA_RT_METRICS float4(LumaSettings.RenderInvSize, LumaSettings.RenderSize) #define SMAA_PRESET_ULTRA -#define SMAA_PREDICATION 0 // We have no depth buffer hooked up in this game -#define SMAA_CUSTOM_SL -SamplerState LinearSampler : register(s0); -SamplerState PointSampler : register(s1); -#define SMAATexture2D(tex) Texture2D tex -#define SMAATexturePass2D(tex) tex -#define SMAASampleLevelZero(tex, coord) tex.SampleLevel(LinearSampler, coord, 0) -#define SMAASampleLevelZeroPoint(tex, coord) tex.SampleLevel(PointSampler, coord, 0) -#define SMAASampleLevelZeroOffset(tex, coord, offset) tex.SampleLevel(LinearSampler, coord, 0, offset) -#define SMAASample(tex, coord) tex.Sample(LinearSampler, coord) -#define SMAASamplePoint(tex, coord) tex.Sample(PointSampler, coord) -#define SMAASampleOffset(tex, coord, offset) tex.Sample(LinearSampler, coord, offset) -#define SMAA_FLATTEN [flatten] -#define SMAA_BRANCH [branch] -#define SMAATexture2DMS2(tex) Texture2DMS tex -#define SMAALoad(tex, pos, sample) tex.Load(pos, sample) +#define SMAA_PREDICATION 0 // We have no depth buffer hooked up in this game #define SMAAGather(tex, coord) tex.Gather(LinearSampler, coord, 0) -#include "../Includes/SMAA.hlsl" - -Texture2D tex0 : register(t0); -Texture2D tex1 : register(t1); -Texture2D tex2 : register(t2); - -void fullscreen_triangle(uint id, out float4 position, out float2 texcoord) -{ - texcoord = float2((id << 1) & 2, id & 2); - position = float4(texcoord * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); -} // SMAAEdgeDetection -// - -void smaa_edge_detection_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset[3] : TEXCOORD1) -{ - fullscreen_triangle(id, position, texcoord); - SMAAEdgeDetectionVS(texcoord, offset); -} - -float2 smaa_edge_detection_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset[3] : TEXCOORD1) : SV_Target -{ - // tex0 = colorTexGamma - // (predication is disabled, so no "tex1" here) - return SMAAColorEdgeDetectionPS(texcoord, offset, tex0); -} - -// - -// SMAABlendingWeightCalculation -// - -void smaa_blending_weight_calculation_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float2 pixcoord : TEXCOORD1, out float4 offset[3] : TEXCOORD2) -{ - fullscreen_triangle(id, position, texcoord); - SMAABlendingWeightCalculationVS(texcoord, pixcoord, offset); -} - -float4 smaa_blending_weight_calculation_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float2 pixcoord : TEXCOORD1, float4 offset[3] : TEXCOORD2) : SV_Target -{ - // tex0 = edgesTex - // tex1 = areaTex - // tex2 = searchTex - return SMAABlendingWeightCalculationPS(texcoord, pixcoord, offset, tex0, tex1, tex2, 0); -} - -// +// (predication is disabled, so no "tex1" here) // SMAANeighborhoodBlending -// - -void smaa_neighborhood_blending_vs(uint id : SV_VertexID, out float4 position : SV_Position, out float2 texcoord : TEXCOORD0, out float4 offset : TEXCOORD1) -{ - fullscreen_triangle(id, position, texcoord); - SMAANeighborhoodBlendingVS(texcoord, offset); -} - -float4 smaa_neighborhood_blending_ps(float4 position : SV_Position, float2 texcoord : TEXCOORD0, float4 offset : TEXCOORD1) : SV_Target -{ - // tex0 = colorTex - // tex1 = blendTex - float4 color = SMAANeighborhoodBlendingPS(texcoord, offset, tex0, tex1); - color.a = 1.0; // The original FXAA pass also always wrote out an opaque alpha - return color; -} +#define SMAA_NEIGHBORHOOD_OUTPUT(color, position) color.a = 1.0; // The original FXAA pass also always wrote out an opaque alpha -// \ No newline at end of file +#include "../Includes/SMAA_Passes.hlsl" From 6936ad2833ac4478d2ec632c7cb031ec57ccbdd7 Mon Sep 17 00:00:00 2001 From: Hlib Omelchenko Date: Wed, 7 Oct 2026 17:45:53 +0200 Subject: [PATCH 3/4] Core: Load and draw RCAS from Core, and let DrawSMAA follow its target size and bind its metrics cbuffer --- Shaders/Global/Luma_RCAS_PS.hlsl | 4 +- Source/Core/core.hpp | 8 ++ Source/Core/utils/draw.hpp | 111 ++++++++++++++++-- .../Borderlands 2 and The Pre-Sequel/main.cpp | 68 ++--------- .../Games/Borderlands GOTY Enhanced/main.cpp | 69 ++--------- Source/Games/Mass Effect Andromeda/main.cpp | 97 ++------------- .../Mass Effect Legendary Edition/main.cpp | 80 ++----------- Source/Games/Mass Effect/main.cpp | 69 ++--------- Source/Games/Medal of Honor Airborne/main.cpp | 72 ++---------- .../main.cpp | 17 --- Source/Games/The Witcher 2/main.cpp | 66 ++--------- 11 files changed, 180 insertions(+), 481 deletions(-) diff --git a/Shaders/Global/Luma_RCAS_PS.hlsl b/Shaders/Global/Luma_RCAS_PS.hlsl index db66a27d9..0a6eb9f2c 100644 --- a/Shaders/Global/Luma_RCAS_PS.hlsl +++ b/Shaders/Global/Luma_RCAS_PS.hlsl @@ -1,5 +1,5 @@ -// RCAS sharpening for the SMAA output, run after the neighborhood blend. paperWhite = 1.0, the sharpness slider is -// the knob, and RCAS_LIMIT bounds the lobe so bright pixels don't over-sharpen. +// RCAS sharpening pass drawn by Core's "DrawRCAS()" (e.g. on the SMAA output). paperWhite = 1.0, the sharpness slider +// is the knob, and RCAS_LIMIT bounds the lobe so bright pixels don't over-sharpen. #include "../Includes/RCAS.hlsl" diff --git a/Source/Core/core.hpp b/Source/Core/core.hpp index 64d9667c1..80362f03a 100644 --- a/Source/Core/core.hpp +++ b/Source/Core/core.hpp @@ -86,6 +86,10 @@ #ifndef ENABLE_SMAA #define ENABLE_SMAA 0 #endif // ENABLE_SMAA +// RCAS sharpening pass drawn by "DrawRCAS()" (e.g. after SMAA or super resolution) +#ifndef ENABLE_RCAS +#define ENABLE_RCAS 0 +#endif // ENABLE_RCAS // 64x only #ifndef ENABLE_NGX #define ENABLE_NGX 0 @@ -662,6 +666,10 @@ namespace { CompileTimeStringHash("SMAA Neighborhood Blending PS"), { "Luma_SMAA_impl", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "smaa_neighborhood_blending_ps" } }, #endif +#if ENABLE_RCAS + { CompileTimeStringHash("RCAS PS"), { "Luma_RCAS_PS", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps" } }, +#endif + #if ENABLE_BLOOM { CompileTimeStringHash("Bloom VS"), { "Luma_Bloom_impl", reshade::api::pipeline_subobject_type::vertex_shader, nullptr, "bloom_main_vs" } }, { CompileTimeStringHash("Bloom Prefilter PS"), { "Luma_Bloom_impl", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "bloom_prefilter_ps" } }, diff --git a/Source/Core/utils/draw.hpp b/Source/Core/utils/draw.hpp index 55a157d4a..0396abe10 100644 --- a/Source/Core/utils/draw.hpp +++ b/Source/Core/utils/draw.hpp @@ -1360,7 +1360,19 @@ void SanitizeNaNs(ID3D11Device* device, ID3D11DeviceContext* device_context, ID3 } } -void DrawSMAA(ID3D11Device* device, ID3D11DeviceContext* device_context, DeviceData& device_data, ID3D11RenderTargetView* rtv, ID3D11ShaderResourceView* srv_color_tex, ID3D11ShaderResourceView* srv_color_tex_gamma, ID3D11ShaderResourceView* srv_predication_tex = nullptr) +// Drops the target-sized intermediates of "DrawSMAA()" (~83 MB at 4K), e.g. once a game stops drawing SMAA. They are recreated on the next draw. +void ReleaseSMAAIntermediates(DeviceData& device_data) +{ + auto& managed_resources = device_data.managed_resources; + managed_resources.depth_stencil_views["smaa_dsv"_h].reset(); + managed_resources.render_target_views["smaa_edge_detection"_h].reset(); + managed_resources.shader_resource_views["smaa_edge_detection"_h].reset(); + managed_resources.render_target_views["smaa_blending_weight_calculation"_h].reset(); + managed_resources.shader_resource_views["smaa_blending_weight_calculation"_h].reset(); +} + +// "cb_smaa_metrics" (optional) is bound to VS and PS b1, the slot the "Luma_SMAA_impl.hlsl" files declare their metrics cbuffer at. +void DrawSMAA(ID3D11Device* device, ID3D11DeviceContext* device_context, DeviceData& device_data, ID3D11RenderTargetView* rtv, ID3D11ShaderResourceView* srv_color_tex, ID3D11ShaderResourceView* srv_color_tex_gamma, ID3D11ShaderResourceView* srv_predication_tex = nullptr, ID3D11Buffer* cb_smaa_metrics = nullptr) { auto& managed_resources = device_data.managed_resources; @@ -1380,6 +1392,15 @@ void DrawSMAA(ID3D11Device* device, ID3D11DeviceContext* device_context, DeviceD std::array ps_srvs_original = {}; device_context->PSGetShaderResources(0, ps_srvs_original.size(), ps_srvs_original.data()); + // Backup the metrics cbuffer slot. + ComPtr vs_cb_metrics_original; + ComPtr ps_cb_metrics_original; + if (cb_smaa_metrics) + { + device_context->VSGetConstantBuffers(1, 1, vs_cb_metrics_original.put()); + device_context->PSGetConstantBuffers(1, 1, ps_cb_metrics_original.put()); + } + // Backup Viewports. UINT num_viewports; device_context->RSGetViewports(&num_viewports, nullptr); @@ -1413,6 +1434,31 @@ void DrawSMAA(ID3D11Device* device, ID3D11DeviceContext* device_context, DeviceD ensure(resource->QueryInterface(tex.put()), >= 0); D3D11_TEXTURE2D_DESC tex_desc; tex->GetDesc(&tex_desc); + // The intermediates only share the target's size, not its mips, array, MSAA, usage or misc flags. + tex_desc.MipLevels = 1; + tex_desc.ArraySize = 1; + tex_desc.SampleDesc = {1, 0}; + tex_desc.Usage = D3D11_USAGE_DEFAULT; + tex_desc.CPUAccessFlags = 0; + tex_desc.MiscFlags = 0; + + // Rebuild the intermediates whenever the target size changes (e.g. a game's resolution scale), not only with the swapchain. + if (const auto& edge_detection_rtv = managed_resources.render_target_views["smaa_edge_detection"_h]) + { + uint4 edge_detection_size; + DXGI_FORMAT edge_detection_format; + GetResourceInfo(edge_detection_rtv.get(), edge_detection_size, edge_detection_format); + if (edge_detection_size.x != tex_desc.Width || edge_detection_size.y != tex_desc.Height) + { + ReleaseSMAAIntermediates(device_data); + } + } + + if (cb_smaa_metrics) + { + device_context->VSSetConstantBuffers(1, 1, &cb_smaa_metrics); + device_context->PSSetConstantBuffers(1, 1, &cb_smaa_metrics); + } // EdgeDetection pass // @@ -1559,18 +1605,12 @@ void DrawSMAA(ID3D11Device* device, ID3D11DeviceContext* device_context, DeviceD // - // Reset resolution dependent resources on init swapchain. - // Some are intentionaly left out, we will recreate them here (in the DrawSMAA function). - auto on_init_swapchain = [&]() - { - managed_resources.depth_stencil_views["smaa_dsv"_h].reset(); - managed_resources.render_target_views["smaa_edge_detection"_h].reset(); - managed_resources.render_target_views["smaa_blending_weight_calculation"_h].reset(); - }; - - LumaCallbacks::on_init_swapchain.try_emplace("luma_smaa"_h, on_init_swapchain); - // Restore. + if (cb_smaa_metrics) + { + device_context->VSSetConstantBuffers(1, 1, vs_cb_metrics_original.get_addressof()); + device_context->PSSetConstantBuffers(1, 1, ps_cb_metrics_original.get_addressof()); + } device_context->OMSetBlendState(blend_original.get(), blend_factor_original, sample_mask_original); device_context->OMSetDepthStencilState(ds_original.get(), stencil_ref_original); device_context->OMSetRenderTargets(rtvs_original.size(), rtvs_original.data(), dsv_original.get()); @@ -1589,6 +1629,53 @@ void DrawSMAA(ID3D11Device* device, ID3D11DeviceContext* device_context, DeviceD release_com_array(ps_srvs_original); } +#if ENABLE_RCAS +// Whether "DrawRCAS()" can draw: its shaders load asynchronously (and reload in development builds), and its cbuffer is created here. +// Call it before routing a pass into the RCAS source: once it returns true, "DrawRCAS()" always writes its target. +bool PrepareRCAS(ID3D11Device* device, DeviceData& device_data) +{ + const auto vs = device_data.native_vertex_shaders.find("Copy VS"_h); + const auto ps = device_data.native_pixel_shaders.find("RCAS PS"_h); + if (vs == device_data.native_vertex_shaders.end() || !vs->second || ps == device_data.native_pixel_shaders.end() || !ps->second) + return false; + + auto& cb = device_data.managed_resources.buffers["rcas_params"_h]; + if (!cb) + { + CD3D11_BUFFER_DESC cb_desc(sizeof(float) * 4, D3D11_BIND_CONSTANT_BUFFER, D3D11_USAGE_DYNAMIC, D3D11_CPU_ACCESS_WRITE); + device->CreateBuffer(&cb_desc, nullptr, cb.put()); + } + return cb.get() != nullptr; +} + +// RCAS sharpening ("Luma_RCAS_PS") of "srv_source" into the whole "rtv", e.g. after SMAA or super resolution. +// Call "PrepareRCAS()" first. Restores the state it changes. +void DrawRCAS(ID3D11DeviceContext* device_context, DeviceData& device_data, ID3D11ShaderResourceView* srv_source, ID3D11RenderTargetView* rtv, float sharpness) +{ + uint4 size; + DXGI_FORMAT format; + GetResourceInfo(rtv, size, format); + + // Map only fails with a removed device. We draw anyway, so the target is never left unwritten. + const auto& cb = device_data.managed_resources.buffers.at("rcas_params"_h); + if (D3D11_MAPPED_SUBRESOURCE mapped_cb; SUCCEEDED(device_context->Map(cb.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped_cb))) + { + const float params[4] = {(float)size.x, (float)size.y, sharpness, 0.f}; + std::memcpy(mapped_cb.pData, params, sizeof(params)); + device_context->Unmap(cb.get(), 0); + } + + DrawStateStack draw_state_stack; + draw_state_stack.Cache(device_context, device_data.uav_max_count); + + device_context->PSSetConstantBuffers(0, 1, cb.get_addressof()); + DrawCustomPixelShader(device_context, device_data.default_depth_stencil_state.get(), device_data.default_blend_state.get(), nullptr, + device_data.native_vertex_shaders.at("Copy VS"_h).get(), device_data.native_pixel_shaders.at("RCAS PS"_h).get(), srv_source, rtv, size.x, size.y, false); + + draw_state_stack.Restore(device_context); +} +#endif // ENABLE_RCAS + void DrawKarisAverage(ID3D11Device* device, ID3D11DeviceContext* device_context, DeviceData& device_data, ID3D11ShaderResourceView* srv_source, ID3D11ShaderResourceView** srv_out) { auto& managed_resources = device_data.managed_resources; diff --git a/Source/Games/Borderlands 2 and The Pre-Sequel/main.cpp b/Source/Games/Borderlands 2 and The Pre-Sequel/main.cpp index 930b08b3d..dfa67f65b 100644 --- a/Source/Games/Borderlands 2 and The Pre-Sequel/main.cpp +++ b/Source/Games/Borderlands 2 and The Pre-Sequel/main.cpp @@ -19,6 +19,7 @@ // No DLSS/DLAA: UE3 has no usable motion vectors, and NGX is x64-only (game is 32-bit) #define GEOMETRY_SHADER_SUPPORT 0 #define ENABLE_SMAA 1 // SMAA ULTRA (+RCAS) injected post-tonemap; core auto-registers the 6 "SMAA ..." passes from Luma_SMAA_impl +#define ENABLE_RCAS 1 // core registers the "RCAS PS" pass, drawn by DrawRCAS after SMAA #define ENABLE_BLOOM 1 // core auto-registers the Bloom VS/Prefilter/Downsample/Upsample passes -> Luma_Bloom_impl // SMAA runs POST-tonemap through the post-draw callback (see RunPostTonemapSMAA); needs original_draw_dispatch_func. #define ENABLE_POST_DRAW_DISPATCH_CALLBACK 1 @@ -137,8 +138,6 @@ struct Borderlands2GameDeviceData final : public GameDeviceData ComPtr cb_smaa_metrics; uint32_t smaa_metrics_w = 0, smaa_metrics_h = 0; - uint32_t smaa_core_w = 0, smaa_core_h = 0; - // SMAA scratch, one texture per colour domain DrawSMAA takes. tex_input_encoded = snapshot of the LDR as the // tonemap wrote it (gamma 2.2), for edge detection; tex_input_linear = its decode (fp16), for the neighborhood // blend, written by the linearize CS. @@ -156,11 +155,6 @@ struct Borderlands2GameDeviceData final : public GameDeviceData ComPtr tex_smaa_out_srv; uint32_t smaa_out_w = 0, smaa_out_h = 0; - // RCAS sharpen CB (b0) = (w,h,sharpness,0). RCAS writes the LDR RTV, so it needs no output temp. - ComPtr cb_sharpen; - uint32_t sharpen_w = 0, sharpen_h = 0; - float sharpen_amount = -1.f; - // Resource the tonemap renders to; on BL2 the HUD draws onto it afterwards. Used by Hide UI and the FXAA override. uint64_t ldr_buffer_handle = 0; // Set when the tonemap runs, cleared every Present: scopes Hide UI's alpha-blend skip to the post-tonemap @@ -298,17 +292,6 @@ class Borderlands2 final : public Game return; } - // Drop DrawSMAA's core-managed intermediates on resolution change so they recreate at the new size. - if (gd.smaa_core_w != w || gd.smaa_core_h != h) - { - auto& mr = device_data.managed_resources; - mr.depth_stencil_views[CompileTimeStringHash("smaa_dsv")].reset(); - mr.render_target_views[CompileTimeStringHash("smaa_edge_detection")].reset(); - mr.render_target_views[CompileTimeStringHash("smaa_blending_weight_calculation")].reset(); - gd.smaa_core_w = w; - gd.smaa_core_h = h; - } - // SMAA depth predication: plane-deviation edge-ness from the captured scene-color SRV (.a). Plain ULTRA fallback when // any input is missing (never scale 2.0 with a null texture). auto* pred_cs = FindShader(device_data.native_compute_shaders, CompileTimeStringHash("BL2TPS Depth Extract CS")); @@ -363,21 +346,9 @@ class Borderlands2 final : public Game // Resolve RCAS before allocating: it decides whether SMAA renders into the LDR RTV directly or into the // intermediate RCAS reads. - auto* copy_vs = FindShader(device_data.native_vertex_shaders, CompileTimeStringHash("Copy VS")); - auto* sharpen_ps = FindShader(device_data.native_pixel_shaders, CompileTimeStringHash("BL2TPS Sharpen PS")); - bool do_sharpen = g_rcas_sharpness > 0.f && copy_vs != nullptr && sharpen_ps != nullptr; + bool do_sharpen = g_rcas_sharpness > 0.f && PrepareRCAS(native_device, device_data); if (do_sharpen) { - if (!gd.cb_sharpen || gd.sharpen_w != w || gd.sharpen_h != h || gd.sharpen_amount != g_rcas_sharpness) - { - const float sp[4] = {(float)w, (float)h, g_rcas_sharpness, 0.f}; - if (CreateImmutableCB(native_device, sp, sizeof(sp), gd.cb_sharpen)) - { - gd.sharpen_w = w; - gd.sharpen_h = h; - gd.sharpen_amount = g_rcas_sharpness; - } - } // SMAA output temp (LDR format, SRV+RTV): only needed as the RCAS input. if (!gd.tex_smaa_out || gd.smaa_out_w != w || gd.smaa_out_h != h) { @@ -392,8 +363,10 @@ class Borderlands2 final : public Game gd.smaa_out_h = h; } } - if (!gd.cb_sharpen || !gd.tex_smaa_out_rtv || !gd.tex_smaa_out_srv) + if (!gd.tex_smaa_out_rtv || !gd.tex_smaa_out_srv) + { do_sharpen = false; + } } // The two colour inputs: the encoded snapshot (LDR format, so CopyResource matches) and its linear decode. @@ -461,6 +434,7 @@ class Borderlands2 final : public Game if (pred_ok && g_smaa_pred_debug) { + auto* copy_vs = FindShader(device_data.native_vertex_shaders, CompileTimeStringHash("Copy VS")); auto* copy_ps = FindShader(device_data.native_pixel_shaders, CompileTimeStringHash("Copy PS")); if (copy_vs != nullptr && copy_ps != nullptr) { @@ -477,39 +451,18 @@ class Borderlands2 final : public Game } #endif - // SMAA (3 passes). Metrics CB at VS+PS b1 (DrawSMAA restores VS/PS/SRVs/RTs, not cbuffers). - ComPtr vs_cb1_orig, ps_cb1_orig; - native_device_context->VSGetConstantBuffers(1, 1, vs_cb1_orig.put()); - native_device_context->PSGetConstantBuffers(1, 1, ps_cb1_orig.put()); - ID3D11Buffer* mcb = gd.cb_smaa_metrics.get(); - native_device_context->VSSetConstantBuffers(1, 1, &mcb); - native_device_context->PSSetConstantBuffers(1, 1, &mcb); - // The last pass of the chain renders straight into the LDR RTV, which is safe because SMAA and RCAS sample // the snapshot copies, never the LDR itself. DrawSMAA(native_device, native_device_context, device_data, do_sharpen ? gd.tex_smaa_out_rtv.get() : ldr_rtv, gd.srv_input_linear.get() /*neighborhood blend (linear light)*/, gd.srv_input_encoded.get() /*edge detection (gamma 2.2)*/, - pred_ok ? gd.srv_pred.get() : nullptr /*predication signal*/); + pred_ok ? gd.srv_pred.get() : nullptr /*predication signal*/, gd.cb_smaa_metrics.get()); if (do_sharpen) { - DrawStateStack sharpen_state; - sharpen_state.Cache(native_device_context, device_data.uav_max_count); - - ID3D11Buffer* scb = gd.cb_sharpen.get(); - native_device_context->PSSetConstantBuffers(0, 1, &scb); - DrawCustomPixelShader(native_device_context, device_data.default_depth_stencil_state.get(), device_data.default_blend_state.get(), nullptr, - copy_vs, sharpen_ps, gd.tex_smaa_out_srv.get(), ldr_rtv, w, h, false); - - sharpen_state.Restore(native_device_context); + DrawRCAS(native_device_context, device_data, gd.tex_smaa_out_srv.get(), ldr_rtv, g_rcas_sharpness); } - - ID3D11Buffer* vcb = vs_cb1_orig.get(); - ID3D11Buffer* pcb = ps_cb1_orig.get(); - native_device_context->VSSetConstantBuffers(1, 1, &vcb); - native_device_context->PSSetConstantBuffers(1, 1, &pcb); } #endif // ENABLE_SMAA @@ -536,10 +489,7 @@ class Borderlands2 final : public Game // "UI Paper White" slider on UI_DRAW_TYPE >= 1 && !use_os_reference_white_level. UI default 203 nits (BT.2408). use_os_reference_white_level = false; - // Core auto-registers the 6 SMAA passes; these three are this game's own. RCAS sharpen PS (drawn via core - // "Copy VS" + DrawCustomPixelShader after SMAA). - native_shaders_definitions.emplace(CompileTimeStringHash("BL2TPS Sharpen PS"), - ShaderDefinition{"Luma_RCAS_PS", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); + // Core auto-registers the 6 SMAA passes and the RCAS sharpen PS; these two are this game's own. // Depth-extract CS for SMAA predication: scene-color .a (linear view Z) -> R16F plane-deviation edge-ness. native_shaders_definitions.emplace(CompileTimeStringHash("BL2TPS Depth Extract CS"), ShaderDefinition("Luma_BL2TPS_DepthExtract", reshade::api::pipeline_subobject_type::compute_shader)); diff --git a/Source/Games/Borderlands GOTY Enhanced/main.cpp b/Source/Games/Borderlands GOTY Enhanced/main.cpp index c4a7e0777..b79c05074 100644 --- a/Source/Games/Borderlands GOTY Enhanced/main.cpp +++ b/Source/Games/Borderlands GOTY Enhanced/main.cpp @@ -17,6 +17,7 @@ #define GEOMETRY_SHADER_SUPPORT 0 #define ENABLE_SMAA 1 +#define ENABLE_RCAS 1 #include "..\..\Core\core.hpp" #include // ShellExecuteA for About links (system() hangs the render thread in exclusive fullscreen) @@ -289,9 +290,6 @@ struct BorderlandsGotyGameDeviceData final : public GameDeviceData uint32_t smaa_metrics_w = 0, smaa_metrics_h = 0; float smaa_metrics_pred_scale = -1.f; // 2.0 = valid depth (predication active), 1.0 = no/mismatched depth (plain ULTRA threshold) - // Tracks the size DrawSMAA built its core-managed intermediates at (to rebuild on resolution change). - uint32_t smaa_core_w = 0, smaa_core_h = 0; - // SMAA inputs (fp16), recreated on resolution change: scene-color snapshot (CopyResource'd each frame, edge // detection) and its linear-light decode (neighborhood blend). ComPtr tex_input; @@ -307,10 +305,6 @@ struct BorderlandsGotyGameDeviceData final : public GameDeviceData ComPtr tex_smaa_out_srv; uint32_t smaa_out_w = 0, smaa_out_h = 0; - // RCAS sharpen CB (b0) = (w, h, sharpness, 0), recreated on resolution/sharpness change. - ComPtr cb_sharpen; - uint32_t sharpen_w = 0, sharpen_h = 0; - float sharpen_amount = -1.f; // RCAS output temp (fp16, RTV). RCAS reads tex_smaa_out_srv -> writes here -> copied into the swapchain target. ComPtr tex_rcas_out; ComPtr tex_rcas_out_rtv; @@ -535,9 +529,6 @@ class BorderlandsGoty final : public Game // Depth-extract CS for SMAA predication: hardware d24 -> R16F plane-deviation edge-ness. native_shaders_definitions.emplace(CompileTimeStringHash("BL Depth Extract CS"), ShaderDefinition("Luma_BL_DepthExtract", reshade::api::pipeline_subobject_type::compute_shader)); - // RCAS sharpen PS (drawn via core "Copy VS" + DrawCustomPixelShader after SMAA). - native_shaders_definitions.emplace(CompileTimeStringHash("BL Sharpen PS"), - ShaderDefinition{"Luma_RCAS_PS", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); // XeGTAO (replaces the game's native HBAO+; see the AO hash block above). 4 compute passes out of one // file; the two denoise variants differ only by XE_GTAO_FINAL_APPLY (the final one writes the game's @@ -914,18 +905,6 @@ class BorderlandsGoty final : public Game !AllShadersReady(device_data.native_vertex_shaders, {CompileTimeStringHash("SMAA Edge Detection VS"), CompileTimeStringHash("SMAA Blending Weight Calculation VS"), CompileTimeStringHash("SMAA Neighborhood Blending VS")})) return DrawOrDispatchOverrideType::None; - // DrawSMAA sizes its edge/blend/DSV intermediates from the first RTV and rebuilds only on swapchain re-init. - // On a resolution change (in-game Resolution Scale) drop the 3 core-managed views so they recreate at the new size. - if (gd.smaa_core_w != w || gd.smaa_core_h != h) - { - auto& mr = device_data.managed_resources; - mr.depth_stencil_views[CompileTimeStringHash("smaa_dsv")].reset(); - mr.render_target_views[CompileTimeStringHash("smaa_edge_detection")].reset(); - mr.render_target_views[CompileTimeStringHash("smaa_blending_weight_calculation")].reset(); - gd.smaa_core_w = w; - gd.smaa_core_h = h; - } - // (Re)create the SMAA metrics CB on resolution change OR when the predication state flips (depth // present/absent). pred_scale only toggles 1.0<->2.0 on menu/transition boundaries, so recreation is rare. if (!gd.cb_smaa_metrics || gd.smaa_metrics_w != w || gd.smaa_metrics_h != h || gd.smaa_metrics_pred_scale != pred_scale) @@ -1040,39 +1019,18 @@ class BorderlandsGoty final : public Game #endif // --- SMAA (3 passes) into the temp RTV, then copy into the swapchain target. --- - // Bind metrics at VS+PS b1 (DrawSMAA restores VS/PS/SRVs/RTs but NOT cbuffer slots). - ComPtr vs_cb1_orig, ps_cb1_orig; - native_device_context->VSGetConstantBuffers(1, 1, vs_cb1_orig.put()); - native_device_context->PSGetConstantBuffers(1, 1, ps_cb1_orig.put()); - ID3D11Buffer* mcb = gd.cb_smaa_metrics.get(); - native_device_context->VSSetConstantBuffers(1, 1, &mcb); - native_device_context->PSSetConstantBuffers(1, 1, &mcb); - // Pass depth for predication only when valid (same-size, captured this frame); otherwise null + the // pred_scale=1.0 baked into the metrics CB make SMAA run as plain ULTRA instead of degraded predication. DrawSMAA(native_device, native_device_context, device_data, gd.tex_smaa_out_rtv.get(), gd.srv_input_linear.get(), gd.srv_input.get(), - pred_ok ? gd.srv_pred.get() : nullptr /*predication (plane-deviation edge-ness)*/); + pred_ok ? gd.srv_pred.get() : nullptr /*predication (plane-deviation edge-ness)*/, gd.cb_smaa_metrics.get()); // --- Optional RCAS sharpen on the SMAA output, then copy into the swapchain target. --- // SMAA output -> RCAS -> tex_rcas_out -> color_res. If sharpening is off or anything isn't ready, copy the // SMAA output straight through (never leave the swapchain unwritten on a Replaced dispatch). - auto* sharpen_vs = FindShader(device_data.native_vertex_shaders, CompileTimeStringHash("Copy VS")); - auto* sharpen_ps = FindShader(device_data.native_pixel_shaders, CompileTimeStringHash("BL Sharpen PS")); - bool do_sharpen = g_rcas_sharpness > 0.f && sharpen_vs != nullptr && sharpen_ps != nullptr; + bool do_sharpen = g_rcas_sharpness > 0.f && PrepareRCAS(native_device, device_data); if (do_sharpen) { - // (Re)create the RCAS CB on resolution/sharpness change. - if (!gd.cb_sharpen || gd.sharpen_w != w || gd.sharpen_h != h || gd.sharpen_amount != g_rcas_sharpness) - { - const float sp[4] = {(float)w, (float)h, g_rcas_sharpness, 0.f}; - if (CreateImmutableCB(native_device, sp, sizeof(sp), gd.cb_sharpen)) - { - gd.sharpen_w = w; - gd.sharpen_h = h; - gd.sharpen_amount = g_rcas_sharpness; - } - } // (Re)create the RCAS output temp (fp16, RTV+SRV-capable) on resolution change. if (!gd.tex_rcas_out || gd.rcas_out_w != w || gd.rcas_out_h != h) { @@ -1085,23 +1043,15 @@ class BorderlandsGoty final : public Game gd.rcas_out_h = h; } } - if (!gd.cb_sharpen || !gd.tex_rcas_out_rtv) + if (!gd.tex_rcas_out_rtv) + { do_sharpen = false; + } } if (do_sharpen) { - // DrawCustomPixelShader does NOT restore state -> wrap in core's DrawStateStack. - DrawStateStack sharpen_state; - sharpen_state.Cache(native_device_context, device_data.uav_max_count); - - ID3D11Buffer* scb = gd.cb_sharpen.get(); - native_device_context->PSSetConstantBuffers(0, 1, &scb); - DrawCustomPixelShader(native_device_context, device_data.default_depth_stencil_state.get(), device_data.default_blend_state.get(), nullptr, - sharpen_vs, sharpen_ps, gd.tex_smaa_out_srv.get(), gd.tex_rcas_out_rtv.get(), w, h, false); - - sharpen_state.Restore(native_device_context); - + DrawRCAS(native_device_context, device_data, gd.tex_smaa_out_srv.get(), gd.tex_rcas_out_rtv.get(), g_rcas_sharpness); native_device_context->CopyResource(color_res.get(), gd.tex_rcas_out.get()); } else @@ -1109,11 +1059,6 @@ class BorderlandsGoty final : public Game native_device_context->CopyResource(color_res.get(), gd.tex_smaa_out.get()); } - ID3D11Buffer* vcb = vs_cb1_orig.get(); - ID3D11Buffer* pcb = ps_cb1_orig.get(); - native_device_context->VSSetConstantBuffers(1, 1, &vcb); - native_device_context->PSSetConstantBuffers(1, 1, &pcb); - return DrawOrDispatchOverrideType::Replaced; // cancel the FXAA resolve dispatch } #endif // ENABLE_SMAA diff --git a/Source/Games/Mass Effect Andromeda/main.cpp b/Source/Games/Mass Effect Andromeda/main.cpp index 6c4958661..9937d1d52 100644 --- a/Source/Games/Mass Effect Andromeda/main.cpp +++ b/Source/Games/Mass Effect Andromeda/main.cpp @@ -12,6 +12,7 @@ // FSR 3 Native AA as a vendor-neutral alternative to DLAA (selectable in core's "Super Resolution" combo) #define GEOMETRY_SHADER_SUPPORT 0 #define ENABLE_SMAA 1 // replaces the game's FXAA pass (FXAA AA mode) with SMAA +#define ENABLE_RCAS 1 // optional sharpening of the SMAA output // The dialogue/cutscene DOF-variant resolve is run-native-then-override via original_draw_dispatch_func, // which the core only populates with this enabled — otherwise it is null outside DEVELOPMENT builds and // every DOF resolve silently bails to native TAA in Test/Publishing. @@ -214,14 +215,7 @@ struct MassEffectAndromedaGameDeviceData final : public GameDeviceData ComPtr tex_smaa_out; ComPtr tex_smaa_out_rtv; ComPtr tex_smaa_out_srv; - ComPtr cb_sharpen; uint32_t smaa_out_w = 0, smaa_out_h = 0; - uint32_t sharpen_w = 0, sharpen_h = 0; // cache key for cb_sharpen - float sharpen_amount = -1.f; // cache key for cb_sharpen - // Size the core-managed DrawSMAA intermediates (smaa_dsv / edge / blend RTs) were last built at. Core sizes them - // from the FIRST RTV and only rebuilds on swapchain re-init — not on MEA's in-game Resolution Scale change — so we - // drop them ourselves on a size change (see the SMAA branch). 0 = not built yet. - uint32_t smaa_core_w = 0, smaa_core_h = 0; #endif }; @@ -263,7 +257,7 @@ class MassEffectAndromeda final : public Game } // Create an IMMUTABLE constant buffer holding `size` bytes from `data`. Resets `out` first; returns true on - // success (out null on failure). Shared by the SMAA RT_METRICS CB and the RCAS sharpen CB. + // success (out null on failure). Used for the SMAA RT_METRICS CB. static bool CreateImmutableCB(ID3D11Device* device, const void* data, UINT size, ComPtr& out) { out.reset(); @@ -454,11 +448,6 @@ class MassEffectAndromeda final : public Game luma_settings_cbuffer_index = -1; luma_data_cbuffer_index = -1; luma_ui_cbuffer_index = -1; -#if ENABLE_SMAA - // RCAS sharpening PS for the SMAA output (reuses core's "Copy VS" fullscreen vertex shader). - native_shaders_definitions.emplace(CompileTimeStringHash("MEA Sharpen PS"), - ShaderDefinition{"Luma_RCAS_PS", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); -#endif #if ENABLE_SR // Format-converting SR output hand-off: the game's "Buffer Format" setting can switch the TAA resolve // target (u2/u3) to r11g11b10_float, where CopySubresourceRegion from our rgba16f output silently @@ -497,7 +486,6 @@ class MassEffectAndromeda final : public Game gd.tex_smaa_out.reset(); gd.tex_smaa_out_rtv.reset(); gd.tex_smaa_out_srv.reset(); - gd.cb_sharpen.reset(); #endif } delete device_data.game; @@ -546,7 +534,7 @@ class MassEffectAndromeda final : public Game if (srv_color && rtv) { - // Use the real RTV size (not swapchain) so SMAA RT_METRICS / sharpen CB / viewport all match the target + // Use the real RTV size (not swapchain) so SMAA RT_METRICS and the viewport match the target // under in-game Resolution Scale. Falls back to swapchain size if the desc can't be read. uint32_t w = (uint32_t)device_data.output_resolution.x; uint32_t h = (uint32_t)device_data.output_resolution.y; @@ -556,27 +544,6 @@ class MassEffectAndromeda final : public Game TryGetTex2DSize(rtv_res.get(), w, h); } - // DrawSMAA sizes its edge/blend/DSV intermediates from the FIRST RTV and rebuilds them only - // on swapchain re-init — not on a Resolution-Scale change. On a size change, drop the 3 core-managed views - // so DrawSMAA recreates them at the new size. - if (gd.smaa_core_w != w || gd.smaa_core_h != h) - { - auto& mr = device_data.managed_resources; - mr.depth_stencil_views[CompileTimeStringHash("smaa_dsv")].reset(); - mr.render_target_views[CompileTimeStringHash("smaa_edge_detection")].reset(); - mr.render_target_views[CompileTimeStringHash("smaa_blending_weight_calculation")].reset(); -#if DEVELOPMENT || TEST - if (gd.smaa_core_w || gd.smaa_core_h) // skip the first-build transition (0 -> size); only log real resizes - { - char b[160]; - snprintf(b, sizeof(b), "MEA SMAA: resized core intermediates %ux%u -> %ux%u (Resolution-Scale change).", gd.smaa_core_w, gd.smaa_core_h, w, h); - reshade::log::message(reshade::log::level::info, b); - } -#endif - gd.smaa_core_w = w; - gd.smaa_core_h = h; - } - // (Re)create the SMAA metrics CB on resolution change. if (!gd.cb_smaa_metrics || gd.smaa_metrics_w != w || gd.smaa_metrics_h != h) { @@ -602,23 +569,11 @@ class MassEffectAndromeda final : public Game if (gd.cb_smaa_metrics && smaa_shaders_ready) { - // Bind the metrics CB at VS+PS b1 (DrawSMAA restores VS/PS/SRVs/RTs but NOT cbuffer slots). - ComPtr vs_cb1_orig, ps_cb1_orig; - native_device_context->VSGetConstantBuffers(1, 1, vs_cb1_orig.put()); - native_device_context->PSGetConstantBuffers(1, 1, ps_cb1_orig.put()); - ID3D11Buffer* mcb = gd.cb_smaa_metrics.get(); - native_device_context->VSSetConstantBuffers(1, 1, &mcb); - native_device_context->PSSetConstantBuffers(1, 1, &mcb); - // MEA's FXAA input is already display-encoded → use it as both color and gamma; no predication. // With sharpening on, SMAA renders into a temp texture and an RCAS pass writes the final RTV. - ID3D11RenderTargetView* smaa_target = rtv.get(); // Only route SMAA through the temp texture if sharpening is on AND the RCAS shaders are // actually compiled — otherwise SMAA must write straight to the final RTV, or the image is lost. - const bool sharpen_shaders_ready = - device_data.native_vertex_shaders[CompileTimeStringHash("Copy VS")].get() != nullptr && - device_data.native_pixel_shaders[CompileTimeStringHash("MEA Sharpen PS")].get() != nullptr; - bool do_sharpen = g_smaa_sharpness > 0.f && sharpen_shaders_ready; + bool do_sharpen = g_smaa_sharpness > 0.f && PrepareRCAS(native_device, device_data); if (do_sharpen) { if (!gd.tex_smaa_out || gd.smaa_out_w != w || gd.smaa_out_h != h) @@ -633,56 +588,20 @@ class MassEffectAndromeda final : public Game gd.smaa_out_h = h; } } - if (gd.tex_smaa_out_rtv && gd.tex_smaa_out_srv) + if (!gd.tex_smaa_out_rtv || !gd.tex_smaa_out_srv) { - // (Re)create the RCAS CB up-front, BEFORE choosing smaa_target: if it fails we must fall back to - // SMAA-straight-to-RTV, else the final RTV is left unwritten (still Replaced) → stale/black. - if (!gd.cb_sharpen || gd.sharpen_w != w || gd.sharpen_h != h || gd.sharpen_amount != g_smaa_sharpness) - { - const float sp[4] = {(float)w, (float)h, g_smaa_sharpness, 0.f}; - if (CreateImmutableCB(native_device, sp, sizeof(sp), gd.cb_sharpen)) - { - gd.sharpen_w = w; - gd.sharpen_h = h; - gd.sharpen_amount = g_smaa_sharpness; - } - } - if (gd.cb_sharpen) - smaa_target = gd.tex_smaa_out_rtv.get(); - else - do_sharpen = false; - } - else do_sharpen = false; + } } DrawSMAA(native_device, native_device_context, device_data, - smaa_target, srv_color.get(), srv_color.get(), nullptr); + do_sharpen ? gd.tex_smaa_out_rtv.get() : rtv.get(), srv_color.get(), srv_color.get(), nullptr, gd.cb_smaa_metrics.get()); if (do_sharpen) { - // cb_sharpen + both RCAS shaders are guaranteed non-null here: the do_sharpen gate above is set - // false if any is missing (SMAA then renders straight to the final RTV), so no re-guard. - auto* sharpen_vs = device_data.native_vertex_shaders[CompileTimeStringHash("Copy VS")].get(); - auto* sharpen_ps = device_data.native_pixel_shaders[CompileTimeStringHash("MEA Sharpen PS")].get(); - // DrawCustomPixelShader does NOT restore state → wrap in core's DrawStateStack - // (caches/restores all PS SRV/CB/sampler slots, IA, RS, scissors, viewport, blend, DS). - DrawStateStack sharpen_state; - sharpen_state.Cache(native_device_context, device_data.uav_max_count); - - ID3D11Buffer* scb = gd.cb_sharpen.get(); - native_device_context->PSSetConstantBuffers(0, 1, &scb); - DrawCustomPixelShader(native_device_context, device_data.default_depth_stencil_state.get(), device_data.default_blend_state.get(), nullptr, - sharpen_vs, sharpen_ps, gd.tex_smaa_out_srv.get(), rtv.get(), w, h, false); - - sharpen_state.Restore(native_device_context); + DrawRCAS(native_device_context, device_data, gd.tex_smaa_out_srv.get(), rtv.get(), g_smaa_sharpness); } - ID3D11Buffer* vcb = vs_cb1_orig.get(); - ID3D11Buffer* pcb = ps_cb1_orig.get(); - native_device_context->VSSetConstantBuffers(1, 1, &vcb); - native_device_context->PSSetConstantBuffers(1, 1, &pcb); - device_data.has_drawn_main_post_processing = true; return DrawOrDispatchOverrideType::Replaced; // cancel native FXAA } diff --git a/Source/Games/Mass Effect Legendary Edition/main.cpp b/Source/Games/Mass Effect Legendary Edition/main.cpp index eb0f5da6e..d4822f64b 100644 --- a/Source/Games/Mass Effect Legendary Edition/main.cpp +++ b/Source/Games/Mass Effect Legendary Edition/main.cpp @@ -16,6 +16,7 @@ #define DISABLE_AUTO_DEBUGGER 1 // The DEVELOPMENT attach prompt is hidden by fullscreen and blocks the loader. #define ENABLE_SMAA 1 // replaces the game's compute FXAA +#define ENABLE_RCAS 1 // optional sharpening of the SMAA output #define ENABLE_BLOOM 1 // fp16 pyramidal bloom replaces the game's clamped bloom #include "..\..\Core\core.hpp" @@ -193,9 +194,6 @@ struct MassEffectGameDeviceData final : public GameDeviceData uint32_t smaa_metrics_w = 0, smaa_metrics_h = 0; bool smaa_metrics_predicated = false; // Whether the cached metrics carry the depth-edge term or plain ULTRA. - // Size DrawSMAA built its core-managed intermediates at (rebuild on resolution change). - uint32_t smaa_core_w = 0, smaa_core_h = 0; - // SRV-readable snapshot of the in-place gamma post buffer. RGBA16FTarget smaa_input; // Its linear-light decode, for the neighborhood blend. @@ -204,10 +202,7 @@ struct MassEffectGameDeviceData final : public GameDeviceData // fp16 SMAA output, copied back directly or through RCAS. RGBA16FTarget smaa_out; - // RCAS b0 = (width, height, sharpness, 0). - ComPtr cb_sharpen; - uint32_t sharpen_w = 0, sharpen_h = 0; - float sharpen_amount = -1.f; + // RCAS output, copied back. RGBA16FTarget rcas_out; // XeGTAO inputs at half-res AO size: R24 depth from deinterleave t0, packed R8G8 view normals from horizon t0. @@ -270,8 +265,6 @@ class MassEffectLE final : public Game static constexpr uint32_t kNameSMAAWeightPS = CompileTimeStringHash("SMAA Blending Weight Calculation PS"); static constexpr uint32_t kNameSMAABlendVS = CompileTimeStringHash("SMAA Neighborhood Blending VS"); static constexpr uint32_t kNameSMAABlendPS = CompileTimeStringHash("SMAA Neighborhood Blending PS"); - static constexpr uint32_t kNameCopyVS = CompileTimeStringHash("Copy VS"); - static constexpr uint32_t kNameSharpenPS = CompileTimeStringHash("MELE Sharpen PS"); static constexpr uint32_t kNameSMAALinearizeCS = CompileTimeStringHash("MELE SMAA Linearize CS"); // operator[] default-inserts on a miss, which would mutate a map the render thread otherwise only reads. @@ -387,11 +380,9 @@ class MassEffectLE final : public Game luma_data_cbuffer_index = 12; // Core registers SMAA through ENABLE_SMAA; its neighborhood blend reads a linear decode of the gamma post buffer. - // RCAS runs afterwards through Copy VS and DrawCustomPixelShader. + // Core registers RCAS through ENABLE_RCAS, and DrawRCAS draws it afterwards. native_shaders_definitions.emplace(kNameSMAALinearizeCS, ShaderDefinition("Luma_MELE_SMAALinearize", reshade::api::pipeline_subobject_type::compute_shader)); - native_shaders_definitions.emplace(kNameSharpenPS, - ShaderDefinition{"Luma_RCAS_PS", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); // Four XeGTAO compute entries share one source; XE_GTAO_FINAL_APPLY selects the game's R8_UNORM target. native_shaders_definitions.emplace(kNameGTAOPrefilterCS, @@ -846,17 +837,6 @@ class MassEffectLE final : public Game if (!smaa_ready) return DrawOrDispatchOverrideType::None; - // DrawSMAA rebuilds managed views only on swapchain init; drop them explicitly on resolution changes. - if (gd.smaa_core_w != w || gd.smaa_core_h != h) - { - auto& mr = device_data.managed_resources; - mr.depth_stencil_views[CompileTimeStringHash("smaa_dsv")].reset(); - mr.render_target_views[CompileTimeStringHash("smaa_edge_detection")].reset(); - mr.render_target_views[CompileTimeStringHash("smaa_blending_weight_calculation")].reset(); - gd.smaa_core_w = w; - gd.smaa_core_h = h; - } - if (!gd.cb_smaa_metrics || gd.smaa_metrics_w != w || gd.smaa_metrics_h != h || gd.smaa_metrics_predicated != depth_ok) { const float metrics[8] = {1.f / (float)w, 1.f / (float)h, (float)w, (float)h, pred_scale, kPredThreshold, kPredStrength, 0.f}; @@ -892,57 +872,21 @@ class MassEffectLE final : public Game linearize_state.Restore(native_device_context); } - // DrawSMAA restores shaders, resources, and targets, but not cbuffer slots; save VS/PS b1 explicitly. - ComPtr vs_cb1_orig, ps_cb1_orig; - native_device_context->VSGetConstantBuffers(1, 1, vs_cb1_orig.put()); - native_device_context->PSGetConstantBuffers(1, 1, ps_cb1_orig.put()); - ID3D11Buffer* mcb = gd.cb_smaa_metrics.get(); - native_device_context->VSSetConstantBuffers(1, 1, &mcb); - native_device_context->PSSetConstantBuffers(1, 1, &mcb); - DrawSMAA(native_device, native_device_context, device_data, gd.smaa_out.rtv.get(), gd.smaa_input_linear.srv.get() /*blend color (linear)*/, gd.smaa_input.srv.get() /*edge color (gamma)*/, - depth_ok ? gd.srv_depth.get() : nullptr /*predication*/); + depth_ok ? gd.srv_depth.get() : nullptr /*predication*/, gd.cb_smaa_metrics.get()); // Apply optional RCAS, otherwise copy SMAA directly so the cancelled resolve always produces output. - const bool sharpen_shaders_ready = - AllShadersReady(device_data.native_vertex_shaders, {kNameCopyVS}) && - AllShadersReady(device_data.native_pixel_shaders, {kNameSharpenPS}); - bool do_sharpen = g_rcas_sharpness > 0.f && sharpen_shaders_ready; - if (do_sharpen) + bool do_sharpen = g_rcas_sharpness > 0.f && PrepareRCAS(native_device, device_data); + // Written by the sharpen pass and then copied out, so it needs no SRV. + if (do_sharpen && !EnsureRGBA16FTarget(native_device, w, h, D3D11_BIND_RENDER_TARGET, &gd.rcas_out)) { - if (!gd.cb_sharpen || gd.sharpen_w != w || gd.sharpen_h != h || gd.sharpen_amount != g_rcas_sharpness) - { - const float sp[4] = {(float)w, (float)h, g_rcas_sharpness, 0.f}; - if (CreateImmutableCB(native_device, sp, sizeof(sp), gd.cb_sharpen)) - { - gd.sharpen_w = w; - gd.sharpen_h = h; - gd.sharpen_amount = g_rcas_sharpness; - } - } - // Written by the sharpen pass and then copied out, so it needs no SRV. - const bool rcas_target_ready = - EnsureRGBA16FTarget(native_device, w, h, D3D11_BIND_RENDER_TARGET, &gd.rcas_out); - if (!gd.cb_sharpen || !rcas_target_ready) - do_sharpen = false; + do_sharpen = false; } if (do_sharpen) { - auto* sharpen_vs = FindShader(device_data.native_vertex_shaders, kNameCopyVS); - auto* sharpen_ps = FindShader(device_data.native_pixel_shaders, kNameSharpenPS); - // DrawCustomPixelShader does not restore state; FullGraphics also prevents RCAS b0 leaking into HUD draws. - DrawStateStack sharpen_state; - sharpen_state.Cache(native_device_context, device_data.uav_max_count); - - ID3D11Buffer* scb = gd.cb_sharpen.get(); - native_device_context->PSSetConstantBuffers(0, 1, &scb); - DrawCustomPixelShader(native_device_context, device_data.default_depth_stencil_state.get(), device_data.default_blend_state.get(), nullptr, - sharpen_vs, sharpen_ps, gd.smaa_out.srv.get(), gd.rcas_out.rtv.get(), w, h, false); - - sharpen_state.Restore(native_device_context); - + DrawRCAS(native_device_context, device_data, gd.smaa_out.srv.get(), gd.rcas_out.rtv.get(), g_rcas_sharpness); native_device_context->CopyResource(color_res.get(), gd.rcas_out.tex.get()); } else @@ -950,12 +894,6 @@ class MassEffectLE final : public Game native_device_context->CopyResource(color_res.get(), gd.smaa_out.tex.get()); } - // Restore native VS/PS b1; the linearize dispatch already restored its compute state. - ID3D11Buffer* vcb = vs_cb1_orig.get(); - ID3D11Buffer* pcb = ps_cb1_orig.get(); - native_device_context->VSSetConstantBuffers(1, 1, &vcb); - native_device_context->PSSetConstantBuffers(1, 1, &pcb); - gd.smaa_applied_handles.insert(color_handle); device_data.has_drawn_main_post_processing = true; return DrawOrDispatchOverrideType::Replaced; diff --git a/Source/Games/Mass Effect/main.cpp b/Source/Games/Mass Effect/main.cpp index 217efdce9..381af3a52 100644 --- a/Source/Games/Mass Effect/main.cpp +++ b/Source/Games/Mass Effect/main.cpp @@ -11,6 +11,7 @@ #define GEOMETRY_SHADER_SUPPORT 0 // The game ships no AA at all (no option, no post AA pass, sampleCount 1), so SMAA adds rather than replaces. #define ENABLE_SMAA 1 +#define ENABLE_RCAS 1 // Replaces the game's quarter-res bright-pass glow, which the replaced gather then stops writing. #define ENABLE_BLOOM 1 // Outside DEVELOPMENT only this define makes original_draw_dispatch_func non-null; without it the callback never fires. @@ -130,8 +131,8 @@ struct MassEffectGameDeviceData final : public GameDeviceData #if ENABLE_SMAA // ---- SMAA (TW2/BL2 shape, see RunPostFinalGradeSMAA) ---- // Metrics CB (b1) = (1/w, 1/h, w, h) + (predication scale, 0, 0, 0). - // Canvas size every resource below (and core's DrawSMAA intermediates) was created for. A change releases them all - // at once; each is then recreated on first use, so no resource tracks a size of its own. + // Canvas size every resource below was created for. A change releases them all at once; each is then recreated on + // first use, so no resource tracks a size of its own. uint32_t smaa_w = 0, smaa_h = 0; ComPtr cb_smaa_metrics; float smaa_metrics_pred_scale = -1.f; @@ -158,8 +159,6 @@ struct MassEffectGameDeviceData final : public GameDeviceData } // RCAS. The intermediate exists ONLY while sharpening is on: at 0 the last SMAA pass writes the canvas directly. - ComPtr cb_sharpen; - float sharpen_amount = -1.f; ComPtr tex_smaa_out; ComPtr tex_smaa_out_rtv; ComPtr tex_smaa_out_srv; @@ -597,9 +596,6 @@ class MassEffect final : public Game ShaderDefinition("Luma_ME1_SMAALinearize", reshade::api::pipeline_subobject_type::compute_shader)); native_shaders_definitions.emplace(CompileTimeStringHash("ME1 Depth Extract CS"), ShaderDefinition("Luma_ME1_DepthExtract", reshade::api::pipeline_subobject_type::compute_shader)); - // RCAS PS, drawn via core "Copy VS" + DrawCustomPixelShader. - native_shaders_definitions.emplace(CompileTimeStringHash("ME1 Sharpen PS"), - ShaderDefinition{"Luma_RCAS_PS", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); #endif // Address-space ceiling, PROBED: no LARGE_ADDRESS_AWARE (0x0102 measured) = 2 GB, Luma's 4K scratch ~365 MB. @@ -674,17 +670,6 @@ class MassEffect final : public Game } #if ENABLE_SMAA - // Core's DrawSMAA intermediates, ~83 MB at 4K, dropped only on swapchain init. Views hold references: release all. - static void ReleaseCoreSMAAIntermediates(DeviceData& device_data) - { - auto& mr = device_data.managed_resources; - mr.depth_stencil_views[CompileTimeStringHash("smaa_dsv")].reset(); - mr.render_target_views[CompileTimeStringHash("smaa_edge_detection")].reset(); - mr.render_target_views[CompileTimeStringHash("smaa_blending_weight_calculation")].reset(); - mr.shader_resource_views[CompileTimeStringHash("smaa_edge_detection")].reset(); - mr.shader_resource_views[CompileTimeStringHash("smaa_blending_weight_calculation")].reset(); - } - // SMAA after the grade, before the HUD (TW2/BL2 chain): snapshot -> linear decode -> DrawSMAA -> [RCAS] -> canvas. void RunPostFinalGradeSMAA(ID3D11Device* native_device, ID3D11DeviceContext* native_device_context, DeviceData& device_data, MassEffectGameDeviceData& gd, ID3D11Resource* canvas_res, ID3D11RenderTargetView* canvas_rtv) { @@ -702,14 +687,12 @@ class MassEffect final : public Game if (!smaa_ready) return; - // Resolution change: drop every size-bound resource, ours and DrawSMAA's core-managed intermediates, so each is - // recreated at the new size (below, or by core). The predication CB does not depend on the size and stays. + // Resolution change: drop every size-bound resource so each is recreated at the new size below (DrawSMAA follows + // the target size itself). The predication CB does not depend on the size and stays. if (gd.smaa_w != w || gd.smaa_h != h) { gd.ReleaseSMAAScratch(); gd.cb_smaa_metrics.reset(); - gd.cb_sharpen.reset(); - ReleaseCoreSMAAIntermediates(device_data); gd.smaa_w = w; gd.smaa_h = h; } @@ -755,17 +738,9 @@ class MassEffect final : public Game return; // RCAS decides the chain's SHAPE: with sharpening off the last SMAA pass writes the canvas, saving a write-back. - auto* copy_vs = FindShader(device_data.native_vertex_shaders, CompileTimeStringHash("Copy VS")); - auto* sharpen_ps = FindShader(device_data.native_pixel_shaders, CompileTimeStringHash("ME1 Sharpen PS")); - bool do_sharpen = g_rcas_sharpness > 0.f && copy_vs != nullptr && sharpen_ps != nullptr; + bool do_sharpen = g_rcas_sharpness > 0.f && PrepareRCAS(native_device, device_data); if (do_sharpen) { - if (!gd.cb_sharpen || gd.sharpen_amount != g_rcas_sharpness) - { - const float sp[4] = {(float)w, (float)h, g_rcas_sharpness, 0.f}; - if (CreateImmutableCB(native_device, sp, sizeof(sp), gd.cb_sharpen)) - gd.sharpen_amount = g_rcas_sharpness; - } if (!gd.tex_smaa_out_rtv || !gd.tex_smaa_out_srv) { gd.ReleaseSharpenScratch(); @@ -775,8 +750,10 @@ class MassEffect final : public Game native_device->CreateShaderResourceView(gd.tex_smaa_out.get(), nullptr, gd.tex_smaa_out_srv.put()); } } - if (!gd.cb_sharpen || !gd.tex_smaa_out_rtv || !gd.tex_smaa_out_srv) + if (!gd.tex_smaa_out_rtv || !gd.tex_smaa_out_srv) + { do_sharpen = false; // allocation failed: fall back to the un-sharpened chain rather than dropping SMAA + } } if (!gd.srv_input || !gd.uav_input_linear || !gd.srv_input_linear) @@ -841,6 +818,7 @@ class MassEffect final : public Game } if (pred_ok && g_smaa_pred_debug) { + auto* copy_vs = FindShader(device_data.native_vertex_shaders, CompileTimeStringHash("Copy VS")); auto* copy_ps = FindShader(device_data.native_pixel_shaders, CompileTimeStringHash("Copy PS")); if (copy_vs != nullptr && copy_ps != nullptr) { @@ -855,34 +833,13 @@ class MassEffect final : public Game } #endif - // Metrics CB at VS+PS b1 (DrawSMAA restores VS/PS/SRVs/RTs, but not cbuffers). - ComPtr vs_cb1_orig, ps_cb1_orig; - native_device_context->VSGetConstantBuffers(1, 1, vs_cb1_orig.put()); - native_device_context->PSGetConstantBuffers(1, 1, ps_cb1_orig.put()); - ID3D11Buffer* mcb = gd.cb_smaa_metrics.get(); - native_device_context->VSSetConstantBuffers(1, 1, &mcb); - native_device_context->PSSetConstantBuffers(1, 1, &mcb); - // Reading the canvas as the target is safe: the chain samples the snapshot, never the canvas itself. - DrawSMAA(native_device, native_device_context, device_data, do_sharpen ? gd.tex_smaa_out_rtv.get() : canvas_rtv, gd.srv_input_linear.get(), gd.srv_input.get(), pred_ok ? gd.srv_pred.get() : nullptr /*predication signal*/); + DrawSMAA(native_device, native_device_context, device_data, do_sharpen ? gd.tex_smaa_out_rtv.get() : canvas_rtv, gd.srv_input_linear.get(), gd.srv_input.get(), pred_ok ? gd.srv_pred.get() : nullptr /*predication signal*/, gd.cb_smaa_metrics.get()); if (do_sharpen) { - DrawStateStack sharpen_state; - sharpen_state.Cache(native_device_context, device_data.uav_max_count); - - ID3D11Buffer* scb = gd.cb_sharpen.get(); - native_device_context->PSSetConstantBuffers(0, 1, &scb); - DrawCustomPixelShader(native_device_context, device_data.default_depth_stencil_state.get(), device_data.default_blend_state.get(), nullptr, - copy_vs, sharpen_ps, gd.tex_smaa_out_srv.get(), canvas_rtv, w, h, false); - - sharpen_state.Restore(native_device_context); + DrawRCAS(native_device_context, device_data, gd.tex_smaa_out_srv.get(), canvas_rtv, g_rcas_sharpness); } - - ID3D11Buffer* vcb = vs_cb1_orig.get(); - ID3D11Buffer* pcb = ps_cb1_orig.get(); - native_device_context->VSSetConstantBuffers(1, 1, &vcb); - native_device_context->PSSetConstantBuffers(1, 1, &pcb); } #endif // ENABLE_SMAA @@ -1102,7 +1059,7 @@ class MassEffect final : public Game if (!g_smaa_enable && gd.tex_input) { gd.ReleaseSMAAScratch(); - ReleaseCoreSMAAIntermediates(device_data); + ReleaseSMAAIntermediates(device_data); gd.smaa_w = gd.smaa_h = 0; // core recreates lazily; keep the latch from claiming anything is current } else diff --git a/Source/Games/Medal of Honor Airborne/main.cpp b/Source/Games/Medal of Honor Airborne/main.cpp index 5562baba9..b84f1b601 100644 --- a/Source/Games/Medal of Honor Airborne/main.cpp +++ b/Source/Games/Medal of Honor Airborne/main.cpp @@ -18,6 +18,7 @@ // The game ships no AA at all (no option, no post AA pass in the dump, sampleCount 1 everywhere), so SMAA adds // rather than replaces. Core auto-registers the 6 "SMAA ..." passes from Luma_SMAA_impl.hlsl. #define ENABLE_SMAA 1 +#define ENABLE_RCAS 1 // Luma's multi-scale HDR bloom pyramid REPLACES the game's own quarter-res bright-pass glow (which the replaced // gather pass then stops writing). Core auto-registers the 4 "Bloom ..." passes from Luma_Bloom_impl.hlsl. #define ENABLE_BLOOM 1 @@ -111,8 +112,8 @@ struct MedalOfHonorAirborneGameDeviceData final : public GameDeviceData #if ENABLE_SMAA // ---- SMAA (TW2/BL2 shape, see RunPostFinalGradeSMAA) ---- - // Canvas size every resource below (and core's DrawSMAA intermediates) was created for. A change releases them all - // at once (RunPostFinalGradeSMAA); each is then recreated on first use, so no resource tracks a size of its own. + // Canvas size every resource below was created for. A change releases them all at once (RunPostFinalGradeSMAA); + // each is then recreated on first use, so no resource tracks a size of its own. uint32_t smaa_w = 0, smaa_h = 0; // Metrics CB (b1) = (1/w, 1/h, w, h) + (predication scale, 0, 0, 0). ComPtr cb_smaa_metrics; @@ -141,8 +142,6 @@ struct MedalOfHonorAirborneGameDeviceData final : public GameDeviceData // RCAS. The intermediate exists ONLY while sharpening is on: with the slider at 0 the SMAA chain renders // straight into the canvas, which saves both this full-resolution copy and a write-back. - ComPtr cb_sharpen; - float sharpen_amount = -1.f; ComPtr tex_smaa_out; ComPtr tex_smaa_out_rtv; ComPtr tex_smaa_out_srv; @@ -464,9 +463,6 @@ class MedalOfHonorAirborne final : public Game ShaderDefinition("Luma_MOHA_SMAALinearize", reshade::api::pipeline_subobject_type::compute_shader)); native_shaders_definitions.emplace(CompileTimeStringHash("MOHA Depth Extract CS"), ShaderDefinition("Luma_MOHA_DepthExtract", reshade::api::pipeline_subobject_type::compute_shader)); - // RCAS sharpen PS, drawn via core "Copy VS" + DrawCustomPixelShader after SMAA. - native_shaders_definitions.emplace(CompileTimeStringHash("MOHA Sharpen PS"), - ShaderDefinition{"Luma_RCAS_PS", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); #endif // Buffers stay in GAMMA space: the gamma-SDR HUD blends onto this canvas and a linear buffer washes it out. @@ -521,18 +517,6 @@ class MedalOfHonorAirborne final : public Game } #if ENABLE_SMAA - // Core's DrawSMAA intermediates, ~83 MB at 4K, sized from the RTV handed to them and dropped only on swapchain - // init, not this canvas' resize. The SRVs hold their own reference, so release both. - static void ReleaseCoreSMAAIntermediates(DeviceData& device_data) - { - auto& mr = device_data.managed_resources; - mr.depth_stencil_views[CompileTimeStringHash("smaa_dsv")].reset(); - mr.render_target_views[CompileTimeStringHash("smaa_edge_detection")].reset(); - mr.render_target_views[CompileTimeStringHash("smaa_blending_weight_calculation")].reset(); - mr.shader_resource_views[CompileTimeStringHash("smaa_edge_detection")].reset(); - mr.shader_resource_views[CompileTimeStringHash("smaa_blending_weight_calculation")].reset(); - } - // SMAA on the graded gamma canvas from the post-draw callback, so it lands after the grade and before the HUD. // TW2/BL2 chain: snapshot -> SRV -> DrawSMAA, last pass into the canvas RTV. No-op if incomplete. void RunPostFinalGradeSMAA(ID3D11Device* native_device, ID3D11DeviceContext* native_device_context, DeviceData& device_data, MedalOfHonorAirborneGameDeviceData& gd, ID3D11Resource* canvas_res, ID3D11RenderTargetView* canvas_rtv) @@ -551,14 +535,12 @@ class MedalOfHonorAirborne final : public Game if (!smaa_ready) return; - // Resolution change: drop every size-bound resource, ours and DrawSMAA's core-managed intermediates, so each is - // recreated at the new size (below, or by core). The predication CB does not depend on the size and stays. + // Resolution change: drop every size-bound resource so each is recreated at the new size below (DrawSMAA follows + // the target size itself). The predication CB does not depend on the size and stays. if (gd.smaa_w != w || gd.smaa_h != h) { gd.ReleaseSMAAScratch(); gd.cb_smaa_metrics.reset(); - gd.cb_sharpen.reset(); - ReleaseCoreSMAAIntermediates(device_data); gd.smaa_w = w; gd.smaa_h = h; } @@ -606,18 +588,9 @@ class MedalOfHonorAirborne final : public Game // RCAS decides the chain's SHAPE, so resolve it before allocating anything: with sharpening off the last // SMAA pass writes the canvas directly, which removes both a full-frame write-back and the intermediate. - // Core's fullscreen "Copy VS", shared by RCAS below and by the predication debug view. - auto* copy_vs = FindShader(device_data.native_vertex_shaders, CompileTimeStringHash("Copy VS")); - auto* sharpen_ps = FindShader(device_data.native_pixel_shaders, CompileTimeStringHash("MOHA Sharpen PS")); - bool do_sharpen = g_rcas_sharpness > 0.f && copy_vs != nullptr && sharpen_ps != nullptr; + bool do_sharpen = g_rcas_sharpness > 0.f && PrepareRCAS(native_device, device_data); if (do_sharpen) { - if (!gd.cb_sharpen || gd.sharpen_amount != g_rcas_sharpness) - { - const float sp[4] = {(float)w, (float)h, g_rcas_sharpness, 0.f}; - if (CreateImmutableCB(native_device, sp, sizeof(sp), gd.cb_sharpen)) - gd.sharpen_amount = g_rcas_sharpness; - } if (!gd.tex_smaa_out_rtv || !gd.tex_smaa_out_srv) { gd.ReleaseSharpenScratch(); @@ -627,8 +600,10 @@ class MedalOfHonorAirborne final : public Game native_device->CreateShaderResourceView(gd.tex_smaa_out.get(), nullptr, gd.tex_smaa_out_srv.put()); } } - if (!gd.cb_sharpen || !gd.tex_smaa_out_rtv || !gd.tex_smaa_out_srv) + if (!gd.tex_smaa_out_rtv || !gd.tex_smaa_out_srv) + { do_sharpen = false; // allocation failed: fall back to the un-sharpened chain rather than dropping SMAA + } } if (!gd.srv_input || !gd.uav_input_linear || !gd.srv_input_linear) @@ -683,6 +658,7 @@ class MedalOfHonorAirborne final : public Game // Calibration aid, driven from the Anti-Aliasing section. Reads the mask that was just written. if (pred_ok && g_smaa_pred_debug) { + auto* copy_vs = FindShader(device_data.native_vertex_shaders, CompileTimeStringHash("Copy VS")); auto* copy_ps = FindShader(device_data.native_pixel_shaders, CompileTimeStringHash("Copy PS")); if (copy_vs != nullptr && copy_ps != nullptr) { @@ -698,36 +674,14 @@ class MedalOfHonorAirborne final : public Game } #endif - // Metrics CB at VS+PS b1. DrawSMAA restores VS/PS/SRVs/RTs but not cbuffers, and a full state stack every SMAA - // frame would be wasted work for two slots, so only these two are saved. - ComPtr vs_cb1_orig, ps_cb1_orig; - native_device_context->VSGetConstantBuffers(1, 1, vs_cb1_orig.put()); - native_device_context->PSGetConstantBuffers(1, 1, ps_cb1_orig.put()); - ID3D11Buffer* mcb = gd.cb_smaa_metrics.get(); - native_device_context->VSSetConstantBuffers(1, 1, &mcb); - native_device_context->PSSetConstantBuffers(1, 1, &mcb); - // Reading the canvas as the target is safe: the chain samples the snapshot, never the canvas itself. - DrawSMAA(native_device, native_device_context, device_data, do_sharpen ? gd.tex_smaa_out_rtv.get() : canvas_rtv, gd.srv_input_linear.get(), gd.srv_input.get(), pred_ok ? gd.srv_pred.get() : nullptr /*predication signal*/); + DrawSMAA(native_device, native_device_context, device_data, do_sharpen ? gd.tex_smaa_out_rtv.get() : canvas_rtv, gd.srv_input_linear.get(), gd.srv_input.get(), pred_ok ? gd.srv_pred.get() : nullptr /*predication signal*/, gd.cb_smaa_metrics.get()); // RCAS on the SMAA output, written into the canvas. if (do_sharpen) { - DrawStateStack sharpen_state; - sharpen_state.Cache(native_device_context, device_data.uav_max_count); - - ID3D11Buffer* scb = gd.cb_sharpen.get(); - native_device_context->PSSetConstantBuffers(0, 1, &scb); - DrawCustomPixelShader(native_device_context, device_data.default_depth_stencil_state.get(), device_data.default_blend_state.get(), nullptr, - copy_vs, sharpen_ps, gd.tex_smaa_out_srv.get(), canvas_rtv, w, h, false); - - sharpen_state.Restore(native_device_context); + DrawRCAS(native_device_context, device_data, gd.tex_smaa_out_srv.get(), canvas_rtv, g_rcas_sharpness); } - - ID3D11Buffer* vcb = vs_cb1_orig.get(); - ID3D11Buffer* pcb = ps_cb1_orig.get(); - native_device_context->VSSetConstantBuffers(1, 1, &vcb); - native_device_context->PSSetConstantBuffers(1, 1, &pcb); } #endif // ENABLE_SMAA @@ -903,7 +857,7 @@ class MedalOfHonorAirborne final : public Game if (!g_smaa_enable && gd.tex_input) { gd.ReleaseSMAAScratch(); - ReleaseCoreSMAAIntermediates(device_data); + ReleaseSMAAIntermediates(device_data); gd.smaa_w = gd.smaa_h = 0; // core recreates lazily; keep the latch from claiming anything is current } else diff --git a/Source/Games/Metal Gear Solid 4 Guns of the Patriots/main.cpp b/Source/Games/Metal Gear Solid 4 Guns of the Patriots/main.cpp index c8224861e..07b1fcb6e 100644 --- a/Source/Games/Metal Gear Solid 4 Guns of the Patriots/main.cpp +++ b/Source/Games/Metal Gear Solid 4 Guns of the Patriots/main.cpp @@ -38,10 +38,6 @@ struct GameDeviceDataMetalGearSolid4 final : public GameDeviceData { MGS4GTAO::Data gtao_data; - // The resolution "DrawSMAA()" last created its (Luma managed) intermediate render targets at - uint32_t smaa_width = 0; - uint32_t smaa_height = 0; - bool drawn_fxaa = false; CustomPixelShaderPassData correct_subtractive_blends_data; @@ -467,19 +463,6 @@ class GameMetalGearSolid4 final : public Game if (rtv_texture_desc.Width != (UINT)device_data.render_resolution.x || rtv_texture_desc.Height != (UINT)device_data.render_resolution.y) return DrawOrDispatchOverrideType::None; - auto& managed_resources = device_data.managed_resources; - - // "DrawSMAA()" only re-creates its resolution dependent resources when the swapchain is re-initialized, so do it ourselves in case the game ever changed its post processing resolution on its own. - // Note: the game resolution cannot change after boot anyway! - if (game_device_data.smaa_width != rtv_texture_desc.Width || game_device_data.smaa_height != rtv_texture_desc.Height) - { - managed_resources.depth_stencil_views["smaa_dsv"_h].reset(); - managed_resources.render_target_views["smaa_edge_detection"_h].reset(); - managed_resources.render_target_views["smaa_blending_weight_calculation"_h].reset(); - game_device_data.smaa_width = rtv_texture_desc.Width; - game_device_data.smaa_height = rtv_texture_desc.Height; - } - SetLumaConstantBuffers(native_device_context, cmd_list_data, device_data, stages, LumaConstantBufferType::LumaSettings); SetLumaConstantBuffers(native_device_context, cmd_list_data, device_data, stages, LumaConstantBufferType::LumaData); updated_cbuffers = true; diff --git a/Source/Games/The Witcher 2/main.cpp b/Source/Games/The Witcher 2/main.cpp index d21aa2751..7b9c5562f 100644 --- a/Source/Games/The Witcher 2/main.cpp +++ b/Source/Games/The Witcher 2/main.cpp @@ -17,6 +17,7 @@ #define GEOMETRY_SHADER_SUPPORT 0 #define ENABLE_SMAA 1 // replaces the final grade's built-in FXAA with SMAA ULTRA (+RCAS); core registers the 6 "SMAA ..." passes +#define ENABLE_RCAS 1 // SMAA runs POST-final-grade via the post-draw callback, so it needs original_draw_dispatch_func non-null. #define ENABLE_POST_DRAW_DISPATCH_CALLBACK 1 @@ -98,8 +99,8 @@ struct TheWitcher2GameDeviceData final : public GameDeviceData std::map, BlendDescCompare> fixed_blend_states; // ---- SMAA (see RunPostFinalGradeSMAA) ---- - // The one resolution every surface below is sized to, core's own DrawSMAA intermediates included: they all come - // from the same canvas, so a change drops the lot and the per-pointer checks rebuild it. + // The one resolution every surface below is sized to: they all come from the same canvas, so a change drops the + // lot and the per-pointer checks rebuild it. uint32_t scratch_w = 0, scratch_h = 0; // SMAA metrics CB (b1) = (1/w,1/h,w,h) + (predication scale,0,0,0); scale 2.0 when predication on, else 1.0. ComPtr cb_smaa_metrics; @@ -115,9 +116,6 @@ struct TheWitcher2GameDeviceData final : public GameDeviceData ComPtr tex_smaa_out; ComPtr tex_smaa_out_rtv; ComPtr tex_smaa_out_srv; - // RCAS sharpen CB (b0) = (w,h,sharpness,0) + output temp (canvas format, RTV). - ComPtr cb_sharpen; - float sharpen_amount = 0.f; // the value cb_sharpen was built with; meaningful only while cb_sharpen exists // Full-res r32_float depth, captured at whichever comes first: the BRIGHT-PASS tonemap draw (t1) or the AO // pack pass (t0). tex_pred is the R16F edge-ness from the Depth Extract CS, not a depth. ComPtr srv_scene_depth; @@ -207,7 +205,6 @@ struct TheWitcher2GameDeviceData final : public GameDeviceData tex_smaa_out_rtv.reset(); tex_smaa_out_srv.reset(); tex_smaa_out.reset(); - cb_sharpen.reset(); } void ReleasePredicationScratch() @@ -575,18 +572,6 @@ class TheWitcher2Game final : public Game } #if ENABLE_SMAA - // Core's DrawSMAA intermediates, ~83 MB at 4K, sized from the RTV handed to them and dropped only on swapchain - // init, not on this canvas' resize. The SRVs hold their own reference, so release both. - static void ReleaseCoreSMAAIntermediates(DeviceData& device_data) - { - auto& mr = device_data.managed_resources; - mr.depth_stencil_views[CompileTimeStringHash("smaa_dsv")].reset(); - mr.render_target_views[CompileTimeStringHash("smaa_edge_detection")].reset(); - mr.render_target_views[CompileTimeStringHash("smaa_blending_weight_calculation")].reset(); - mr.shader_resource_views[CompileTimeStringHash("smaa_edge_detection")].reset(); - mr.shader_resource_views[CompileTimeStringHash("smaa_blending_weight_calculation")].reset(); - } - // SMAA on the graded gamma canvas, after the original final-grade draw and before the UI draws on it; the // replaced grade skipped its own FXAA via LumaData.CustomData2. void RunPostFinalGradeSMAA(ID3D11Device* native_device, ID3D11DeviceContext* native_device_context, DeviceData& device_data, TheWitcher2GameDeviceData& gd, ID3D11Resource* canvas_res, ID3D11RenderTargetView* canvas_rtv) @@ -610,13 +595,12 @@ class TheWitcher2Game final : public Game return; } - // One resolution latch: every surface here is canvas-sized, so a change drops the lot, core's intermediates - // included, and the per-pointer checks below rebuild it. + // One resolution latch: every surface here is canvas-sized, so a change drops the lot and the per-pointer + // checks below rebuild it. if (gd.scratch_w != w || gd.scratch_h != h) { gd.ReleaseSMAAScratch(); gd.ReleasePredicationScratch(); - ReleaseCoreSMAAIntermediates(device_data); gd.cb_smaa_metrics.reset(); // it holds the resolution itself gd.scratch_w = w; gd.scratch_h = h; @@ -662,24 +646,18 @@ class TheWitcher2Game final : public Game // Resolve RCAS before allocating: it decides whether the last pass writes the canvas RTV directly, // which removes both the copy back and the intermediate. - auto* sharpen_vs = FindShader(device_data.native_vertex_shaders, CompileTimeStringHash("Copy VS")); - auto* sharpen_ps = FindShader(device_data.native_pixel_shaders, CompileTimeStringHash("TW2 Sharpen PS")); - bool do_sharpen = g_rcas_sharpness > 0.f && sharpen_vs != nullptr && sharpen_ps != nullptr; + bool do_sharpen = g_rcas_sharpness > 0.f && PrepareRCAS(native_device, device_data); if (do_sharpen) { - if (!gd.cb_sharpen || gd.sharpen_amount != g_rcas_sharpness) - { - const float sharpen_params[4] = {(float)w, (float)h, g_rcas_sharpness, 0.f}; - if (CreateImmutableCB(native_device, sharpen_params, sizeof(sharpen_params), gd.cb_sharpen)) - gd.sharpen_amount = g_rcas_sharpness; - } if (!gd.tex_smaa_out && CreateDefaultTex(native_device, w, h, D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET, gd.tex_smaa_out, cfmt)) { native_device->CreateRenderTargetView(gd.tex_smaa_out.get(), nullptr, gd.tex_smaa_out_rtv.put()); native_device->CreateShaderResourceView(gd.tex_smaa_out.get(), nullptr, gd.tex_smaa_out_srv.put()); } - if (!gd.cb_sharpen || !gd.tex_smaa_out_rtv || !gd.tex_smaa_out_srv) + if (!gd.tex_smaa_out_rtv || !gd.tex_smaa_out_srv) + { do_sharpen = false; + } } if (!gd.tex_input && CreateDefaultTex(native_device, w, h, D3D11_BIND_SHADER_RESOURCE, gd.tex_input, cfmt)) @@ -722,33 +700,16 @@ class TheWitcher2Game final : public Game pred_cs_state.Restore(native_device_context); } - // SMAA (3 passes). Metrics CB at VS+PS b1 (DrawSMAA restores VS/PS/SRVs/RTs, not cbuffers). - ComPtr vs_cb1_orig, ps_cb1_orig; - native_device_context->VSGetConstantBuffers(1, 1, vs_cb1_orig.put()); - native_device_context->PSGetConstantBuffers(1, 1, ps_cb1_orig.put()); - native_device_context->VSSetConstantBuffers(1, 1, gd.cb_smaa_metrics.get_addressof()); - native_device_context->PSSetConstantBuffers(1, 1, gd.cb_smaa_metrics.get_addressof()); - // The last pass of the chain renders straight into the canvas RTV — no write-back copy. Reading the // canvas is safe because SMAA/RCAS sample the snapshot (tex_input), never the canvas itself. DrawSMAA(native_device, native_device_context, device_data, do_sharpen ? gd.tex_smaa_out_rtv.get() : canvas_rtv, gd.srv_input_linear.get(), gd.srv_input.get(), - pred_ok ? gd.srv_pred.get() : nullptr /*predication signal*/); + pred_ok ? gd.srv_pred.get() : nullptr /*predication signal*/, gd.cb_smaa_metrics.get()); if (do_sharpen) { - DrawStateStack sharpen_state; - sharpen_state.Cache(native_device_context, device_data.uav_max_count); - - native_device_context->PSSetConstantBuffers(0, 1, gd.cb_sharpen.get_addressof()); - DrawCustomPixelShader(native_device_context, device_data.default_depth_stencil_state.get(), device_data.default_blend_state.get(), nullptr, - sharpen_vs, sharpen_ps, gd.tex_smaa_out_srv.get(), canvas_rtv, w, h, false); - - sharpen_state.Restore(native_device_context); + DrawRCAS(native_device_context, device_data, gd.tex_smaa_out_srv.get(), canvas_rtv, g_rcas_sharpness); } - - native_device_context->VSSetConstantBuffers(1, 1, vs_cb1_orig.get_addressof()); - native_device_context->PSSetConstantBuffers(1, 1, ps_cb1_orig.get_addressof()); } #endif // ENABLE_SMAA @@ -979,9 +940,6 @@ class TheWitcher2Game final : public Game // decode. native_shaders_definitions.emplace(CompileTimeStringHash("TW2 SMAA Linearize CS"), ShaderDefinition("Luma_TW2_SMAALinearize", reshade::api::pipeline_subobject_type::compute_shader)); - // RCAS sharpen PS (drawn via core "Copy VS" + DrawCustomPixelShader after SMAA). - native_shaders_definitions.emplace(CompileTimeStringHash("TW2 Sharpen PS"), - ShaderDefinition{"Luma_RCAS_PS", reshade::api::pipeline_subobject_type::pixel_shader, nullptr, "sharpen_ps"}); // Depth-extract CS for SMAA predication: game r32f LINEAR view-space depth -> R16F edge-ness in [0,1]. native_shaders_definitions.emplace(CompileTimeStringHash("TW2 Depth Extract CS"), ShaderDefinition("Luma_TW2_DepthExtract", reshade::api::pipeline_subobject_type::compute_shader)); @@ -1142,7 +1100,7 @@ class TheWitcher2Game final : public Game if (!g_smaa_enable && game_device_data.tex_input) { game_device_data.ReleaseSMAAScratch(); - ReleaseCoreSMAAIntermediates(device_data); + ReleaseSMAAIntermediates(device_data); } if ((!g_smaa_enable || !g_smaa_predication) && game_device_data.tex_pred) game_device_data.ReleasePredicationScratch(); From 1ca28f30bbe0351a40a6599cd3ace8d50e11f072 Mon Sep 17 00:00:00 2001 From: Hlib Omelchenko Date: Sat, 10 Oct 2026 15:08:25 +0200 Subject: [PATCH 4/4] Shaders: Add XeGTAO custom position and no-denoise options --- Shaders/Includes/XeGTAO.hlsl | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/Shaders/Includes/XeGTAO.hlsl b/Shaders/Includes/XeGTAO.hlsl index 94be5f458..c3eb8450f 100644 --- a/Shaders/Includes/XeGTAO.hlsl +++ b/Shaders/Includes/XeGTAO.hlsl @@ -112,6 +112,19 @@ static float2 XeGTAO_SourcePixelSize; // 1 / tex0's size, set first by each entr #define XE_GTAO_DENOISE_OUTPUT_TYPE unorm float2 #endif +// Define "XE_GTAO_NO_DENOISE" if the game filters the AO itself and doesn't dispatch "denoise_pass_cs". +// With "XE_GTAO_FINAL_APPLY", the main pass then writes the final value. Debug view 3 only exists in the denoise pass. +#if XE_GTAO_NO_DENOISE && XE_GTAO_FINAL_APPLY +#define XE_GTAO_MAIN_PASS_OUTPUT_TYPE XE_GTAO_FINAL_OUTPUT_TYPE +#define XE_GTAO_MAIN_PASS_DEBUG_VALUE(v) XE_GTAO_ENCODE_FINAL(v) +// The same value "XeGTAO_Denoise()" writes, without the blur. "saturate(saturate(v / scale) * scale)" is just "saturate(v)". +#define XE_GTAO_MAIN_PASS_VALUE(visibility, edges) XE_GTAO_ENCODE_FINAL(XE_GTAO_FINAL_VALUE(saturate(visibility))) +#else +#define XE_GTAO_MAIN_PASS_OUTPUT_TYPE unorm float2 +#define XE_GTAO_MAIN_PASS_DEBUG_VALUE(v) float2(v, 1.0) +#define XE_GTAO_MAIN_PASS_VALUE(visibility, edges) float2(saturate((visibility) / XE_GTAO_OCCLUSION_TERM_SCALE), edges) +#endif + // This is also a good place to do non-linear depth conversion for cases where one wants the 'radius' (effectively the threshold between near-field and far-field GI), // is required to be non-linear (i.e. very large outdoors environments). float XeGTAO_ClampDepth(float depth) @@ -246,6 +259,9 @@ float XeGTAO_PackEdges(float4 edgesLRTB) return dot(edgesLRTB, float4(64.0 / 255.0, 16.0 / 255.0, 4.0 / 255.0, 1.0 / 255.0)); } +// Define "XE_GTAO_CUSTOM_VIEWSPACE_POSITION" to replace this with the game's own reconstruction, with the same signature. +// "NDC_TO_VIEW_MUL" is still needed for the screen space radius. +#if !XE_GTAO_CUSTOM_VIEWSPACE_POSITION // Inputs are screen XY and viewspace depth, output is viewspace position float3 XeGTAO_ComputeViewspacePosition(float2 screenPos, float viewspaceDepth) { @@ -254,6 +270,7 @@ float3 XeGTAO_ComputeViewspacePosition(float2 screenPos, float viewspaceDepth) ret.z = viewspaceDepth; return ret; } +#endif #if XE_GTAO_GENERATE_NORMALS // Depth-derived view-space normal (from the Intel reference; same convention as ComputeViewspacePosition, @@ -287,7 +304,7 @@ float XeGTAO_FastACos(float inX) return inX >= 0 ? res : 3.141593 - res; } -void XeGTAO_MainPass(uint2 pixCoord, float2 localNoise, float3 viewspaceNormal, Texture2D sourceViewspaceDepth, SamplerState depthSampler, RWTexture2D outWorkingAOTermAndEdges) +void XeGTAO_MainPass(uint2 pixCoord, float2 localNoise, float3 viewspaceNormal, Texture2D sourceViewspaceDepth, SamplerState depthSampler, RWTexture2D outWorkingAOTermAndEdges) { float2 normalizedScreenPos = (pixCoord + 0.5) * VIEWPORT_PIXEL_SIZE; @@ -313,12 +330,12 @@ void XeGTAO_MainPass(uint2 pixCoord, float2 localNoise, float3 viewspaceNormal, // values through when DebugViewRT > 0). if (DebugViewRT > 0.5 && DebugViewRT < 1.5) // 1 = depth gradient (proves live depth + linearization/scale) { - outWorkingAOTermAndEdges[pixCoord] = float2(saturate(frac(log2(max(viewspaceZ, 1e-6)))), 1.0); + outWorkingAOTermAndEdges[pixCoord] = XE_GTAO_MAIN_PASS_DEBUG_VALUE(saturate(frac(log2(max(viewspaceZ, 1e-6))))); return; } if (DebugViewRT >= 3.5 && DebugViewRT < 4.5) // 4 = edges { - outWorkingAOTermAndEdges[pixCoord] = float2(dot(edgesLRTB, 0.25), 1.0); + outWorkingAOTermAndEdges[pixCoord] = XE_GTAO_MAIN_PASS_DEBUG_VALUE(dot(edgesLRTB, 0.25)); return; } #endif @@ -347,7 +364,7 @@ void XeGTAO_MainPass(uint2 pixCoord, float2 localNoise, float3 viewspaceNormal, #if DEVELOPMENT if (DebugViewRT >= 1.5 && DebugViewRT < 2.5) // 2 = normals view-facing term (smooth per-surface shading = correct decode) { - outWorkingAOTermAndEdges[pixCoord] = float2(saturate(abs(dot(viewspaceNormal, viewVec))), 1.0); + outWorkingAOTermAndEdges[pixCoord] = XE_GTAO_MAIN_PASS_DEBUG_VALUE(saturate(abs(dot(viewspaceNormal, viewVec)))); return; } #endif @@ -509,8 +526,7 @@ void XeGTAO_MainPass(uint2 pixCoord, float2 localNoise, float3 viewspaceNormal, XE_GTAO_ADJUST_VISIBILITY(visibility, viewspaceZ) } - visibility = saturate(visibility / XE_GTAO_OCCLUSION_TERM_SCALE); - outWorkingAOTermAndEdges[pixCoord] = float2(visibility, edges); + outWorkingAOTermAndEdges[pixCoord] = XE_GTAO_MAIN_PASS_VALUE(visibility, edges); } void XeGTAO_DecodeGatherPartial(float4 packedValue, out float outDecoded[4]) @@ -667,7 +683,7 @@ RWTexture2D out_working_depth_mip1 : register(u1); RWTexture2D out_working_depth_mip2 : register(u2); RWTexture2D out_working_depth_mip3 : register(u3); RWTexture2D out_working_depth_mip4 : register(u4); -RWTexture2D ao_term_and_edges : register(u0); +RWTexture2D ao_term_and_edges : register(u0); RWTexture2D final_output : register(u0);