diff --git a/Shaders/Borderlands 2 and The Pre-Sequel/Includes/GameCBuffers.hlsl b/Shaders/Borderlands 2 and The Pre-Sequel/Includes/GameCBuffers.hlsl index f825f000..d56201c0 100644 --- a/Shaders/Borderlands 2 and The Pre-Sequel/Includes/GameCBuffers.hlsl +++ b/Shaders/Borderlands 2 and The Pre-Sequel/Includes/GameCBuffers.hlsl @@ -15,16 +15,18 @@ namespace CB // vanilla no-op. SMAA metrics are passed via a dedicated CB at b1, not here. struct LumaGameSettings { - float Exposure; // 1 = vanilla. Scene exposure multiplier, scene-referred / pre-grade. - float Saturation; // 1 = vanilla. Oklab saturation multiplier on the final HDR color. - float HighlightDechroma; // 0 = off (only the mandatory DICE/gamut desat applies); higher = bright sources fade to white sooner. - float BloomIntensity; // 1 = vanilla. Scales the game's bloom contribution in the scene mix. - float Contrast; // 1 = vanilla. Slope contrast around 18% mid-gray on the final HDR color. - float VignetteIntensity; // 1 = vanilla. Scales the game's vignette darkening (0 = no vignette). - float LumaBloomEnable; // 0/1. 1 = composite Luma HDR pyramidal bloom (t5 BL2 / t8 TPS, additive); 0 = vanilla game bloom (t1). - float DOFRadius; // Luma Gaussian DoF strength (half-res blur extent, full-res px @ 4K). - float DOFType; // DoF path: 0 = vanilla game DoF, 2 = Luma separable Gaussian (t7 pre-blurred). - float Dithering; // 0/1 toggle. Animated triangular dither at output (HDR only) to break gradient banding. + float Exposure; // 1 = vanilla. Scene exposure multiplier, scene-referred / pre-grade. + float Saturation; // 1 = vanilla. Oklab saturation multiplier on the final HDR color. + float HighlightDechroma; // 0 = off (only the mandatory DICE/gamut desat applies); higher = bright sources fade to white sooner. + float BloomIntensity; // 1 = vanilla. Scales the game's bloom contribution in the scene mix. + float Contrast; // 1 = vanilla. Slope contrast around 18% mid-gray on the final HDR color. + float VignetteIntensity; // 1 = vanilla. Scales the game's vignette darkening (0 = no vignette). + float LumaBloomEnable; // 0/1. 1 = composite Luma HDR pyramidal bloom (t5 BL2 / t8 TPS, additive); 0 = vanilla game bloom (t1). + float DOFRadius; // Luma Gaussian DoF strength (half-res blur extent, full-res px @ 4K). + float DOFType; // DoF path: 0 = vanilla game DoF, 1 = Luma separable Gaussian (t7 pre-blurred). + float Dithering; // 0/1 toggle. Animated triangular dither at output (HDR only) to break gradient banding. + float VideoAutoHDREnable; // 0/1. 1 = light PumboAutoHDR on Bink videos (HDR only); 0 = flat SDR at paper white. + float VideoAutoHDRBoost; // 0..1. Highlight-expansion strength; peak = lerp(sRGB white, 250 nits, boost). 0 = off. }; // Game specific cbuffer (instance/pass) data. 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 index ccd1bae2..bf1914ff 100644 --- a/Shaders/Borderlands 2 and The Pre-Sequel/Luma_BL2TPS_Sharpen.hlsl +++ b/Shaders/Borderlands 2 and The Pre-Sequel/Luma_BL2TPS_Sharpen.hlsl @@ -1,7 +1,8 @@ // RCAS sharpening for the SMAA output. -// Runs after the SMAA neighborhood-blend pass, on the linear scRGB color (DrawSMAA blends the linear copy), -// before the result is copied back into the fp16 scRGB swapchain. paperWhite=1.0; the sharpness slider is the -// tuning knob. RCAS_LIMIT bounds the lobe so HDR highlights don't over-sharpen. +// Runs after the SMAA neighborhood-blend pass, on the gamma LDR color (the same gamma LDR SMAA consumed), +// before the result is copied back into the LDR buffer (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" @@ -10,7 +11,7 @@ cbuffer SharpenCB : register(b0) float4 SharpenParams; // (width, height, sharpness[0..1], unused) } -Texture2D tex0 : register(t0); // SMAA output (linear scRGB) +Texture2D tex0 : register(t0); // SMAA output (gamma LDR) Texture2D dummyMV : register(t1); // unused (dynamicSharpening = false) float4 sharpen_ps(float4 pos : SV_Position) : SV_Target diff --git a/Shaders/Borderlands 2 and The Pre-Sequel/Luma_BL2TPS_Tonemap.hlsl b/Shaders/Borderlands 2 and The Pre-Sequel/Luma_BL2TPS_Tonemap.hlsl new file mode 100644 index 00000000..08e96250 --- /dev/null +++ b/Shaders/Borderlands 2 and The Pre-Sequel/Luma_BL2TPS_Tonemap.hlsl @@ -0,0 +1,310 @@ +// clang-format off +// ORDER MATTERS — do NOT let clang-format sort these. The game-local Common.hlsl MUST come first: it defines +// LumaGameSettings (via GameCBuffers.hlsl) BEFORE the shared Settings.hlsl (pulled in by Color.hlsl below) +// declares the LumaSettings cbuffer. If sorted after Color.hlsl, GameSettings becomes the empty fallback struct +// and every LumaSettings.GameSettings.* reference fails to compile (invalid subscript). +#include "Includes/Common.hlsl" // game-local: LumaGameSettings (grade sliders) — keep FIRST +#include "../Includes/Color.hlsl" // BT709_To_BT2020 / BT2020_To_BT709 +#include "../Includes/ColorGradingLUT.hlsl" // SimpleGamutClip +#include "../Includes/DICE.hlsl" +#include "../Includes/Reinhard.hlsl" // Reinhard::ReinhardScalable (max-channel compressor) +// clang-format on + +// Borderlands 2 + The Pre-Sequel — uber post-process / tonemap SHARED IMPLEMENTATION (UE3, via dgVoodoo D3D9->11). +// Holds the grade body as RunTonemap(); the per-hash wrapper files (Tonemap_0x.ps_5_0.hlsl, one per game × +// dgVoodoo version) declare the full UE3 interpolator set + main() and forward to it. No hash in this filename -> +// not matched/replaced directly; it is #included by the wrappers. +// +// Vanilla body (DOF + screen-blend bloom + vignette + ImageAdjustments + 16-slice ColorGradingLUT) transcribed +// VERBATIM (register-level) from the readable DX9 BL2 tonemap (tonemap_0x54ED86A0.ps_3_0), +// constants remapped DX9 cN -> cb4[N+8]. +// +// HDR output uses the "apply SDR LUT in HDR" method: max-channel-compress the raw scene into [0,1] BEFORE the +// grade+LUT (so neither the per-channel saturate nor the LUT clip -> highlight hue/chroma preserved), then expand +// back with the exact inverse (sdr / tm) and DICE-map to the display peak. SDR mode keeps tm=1 -> the grade runs on +// the raw scene exactly like vanilla. Compression MUST be max-channel: it scales all 3 channels by one factor (hue +// intact) AND pins the brightest channel <=1 (nothing escapes the LUT). by-luminance would leave a saturated +// channel >1 and the LUT would clip it; per-channel shifts hue outright. +// NOTE: the per-channel ImageAdjustments curve below MUST keep the original's exact swizzles (r0.zzxy / r3.z,w,xy) — a +// "cleaner" rewrite swaps channels and casts the whole image green. + +// ---- Per-game texture/sampler slot map: Borderlands 2 vs The Pre-Sequel ------------------------------ +// dgVoodoo maps each DX9 sampler sN 1:1 onto DX11 tN. BL2 and TPS run the same UE3 uber-post tonemap, but TPS +// inserts a LightShaftTexture at slot 1, shifting bloom/vignette/LUT/DOF DOWN one slot (vs +// the two DX9 tonemaps: BL2 tonemap_0x54ED86A0 has LUT@s3/DOF@s4; TPS tps_tonemap_0xF8997849 has +// lightshaft@s1, LUT@s4, DOF@s5). The grade math is identical, so this body is SHARED — only which register +// each named texture binds to changes, plus TPS's extra light-shaft composite term. The TPS wrapper #defines +// these macros then #includes this file; with them undefined (BL2) every mapping is the identity = the original +// BL2 bindings, so BL2 is byte-for-byte unchanged. +#ifndef TM_T_BLOOM +#define TM_T_BLOOM t1 // FilterColor1Texture (screen-blend bloom) +#define TM_T_VIGNETTE t2 // VignetteTexture +#define TM_T_LUT t3 // ColorGradingLUT (256x16, 16-slice) +#define TM_T_DOF t4 // LowResPostProcessBuffer (half-res DOF) +#define TM_T_LUMABLOOM t5 // injected Luma HDR bloom (free slot on BL2) +#define TM_S_BLOOM s1 +#define TM_S_VIGNETTE s2 +#define TM_S_LUT s3 +#define TM_S_DOF s4 +#endif + +Texture2D t0 : register(t0); // SceneColorTexture (fp16 HDR scene) +#if TM_HAS_LIGHTSHAFT +Texture2D t_lightshaft : register(TM_T_LIGHTSHAFT); // LightShaftTexture (TPS god rays) — slot 1 +#endif +Texture2D t1 : register(TM_T_BLOOM); // FilterColor1Texture (bloom) BL2 t1 / TPS t2 +Texture2D t2 : register(TM_T_VIGNETTE); // VignetteTexture BL2 t2 / TPS t3 +Texture2D t3 : register(TM_T_LUT); // ColorGradingLUT (256x16, 16-slice) BL2 t3 / TPS t4 +Texture2D t4 : register(TM_T_DOF); // LowResPostProcessBuffer (half-res DOF) BL2 t4 / TPS t5 +Texture2D t5 : register(TM_T_LUMABLOOM); // Luma HDR pyramidal bloom, bound by the mod when LumaBloomEnable (BL2 t5 / TPS t8 — TPS t5 is the native DOF) +Texture2D t7 : register(t7); // Half-res pre-blurred scene for the Luma Gaussian DoF, bound when it is active (free on both BL2/TPS) + +SamplerState s0_s : register(s0); +SamplerState s1_s : register(TM_S_BLOOM); // BL2 s1 / TPS s2 +SamplerState s2_s : register(TM_S_VIGNETTE); // BL2 s2 / TPS s3 +SamplerState s3_s : register(TM_S_LUT); // BL2 s3 / TPS s4 +SamplerState s4_s : register(TM_S_DOF); // BL2 s4 / TPS s5 + +cbuffer cb3 : register(b3) +{ + float4 cb3[77]; +} +cbuffer cb4 : register(b4) +{ + float4 cb4[236]; +} + +#define BloomTintAndScreenBlendThreshold cb4[16] // c8 +#define ImageAdjustments2 cb4[17] // c9 +#define ImageAdjustments3 cb4[18] // c10 +#define HalfResMaskRect cb4[19] // c11 +#define DOFKernelSize cb4[20] // c12 +#define VignetteSettings cb4[21] // c13 +#define VignetteColor cb4[22] // c14 + +// The tonemap grade. v5 = TEXCOORD0 (DOF radial/kernel coords in .zw), v6 = TEXCOORD1 (scene UV .xy, half-res DOF +// UV .zw) — the only interpolators the body uses. Returns the final gamma-space color (o0.a is always 0). +float4 RunTonemap(float4 v5, float4 v6) +{ + float4 o; + float4 r0, r1, r2, r3; + + // --- DOF: vanilla half-res Gaussian composite, OR Luma separable HDR Gaussian --- + // The game's depth/object DoF decision is baked into t4.a (the in-focus weight: 1 = sharp focused + // subject, 0.25 = max-blurred background, tracking the focused subject's silhouette). The Luma path KEEPS + // that decision — when the game turns DoF off, t4.a -> ~1 -> the blend + // weight -> 0 -> no blur, so cutscenes/scripted focus stay game-driven — but replaces the half-res + // Gaussian with a denser separable Gaussian of the prefiltered scene (t7), preserving HDR so bright + // defocused areas glow. DOFKernelSize.z/.w + v5.zw drive only the vanilla fallback below. + float2 sceneUV = v6.xy; + float2 dofUV = clamp(v6.zw, HalfResMaskRect.xy, HalfResMaskRect.zw); + float4 sceneTap = t0.SampleLevel(s0_s, sceneUV, 0); + float centerInFocus = t4.SampleLevel(s4_s, dofUV, 0).a; // 1 = sharp .. 0.25 = max blur + + // Replicate the game's EXACT per-pixel DoF blend weight (radial DOF-kernel falloff, VERBATIM from the + // vanilla branch below, + the t4.a in-focus weight), then blend the blurred scene by it. The game blends + // only a fraction of its blurred buffer (sharpness >> t4.a alone), so matching this weight is what keeps + // the effect at vanilla strength instead of fully replacing the background. Without it it over-blurs. + float dofRadial = (v5.z * 2.0 - 1.0) * DOFKernelSize.z; + float radialSharp = 1.0 - saturate(dofRadial * dofRadial); + float blurWeight = 1.0 - saturate(radialSharp + centerInFocus); // 0 = sharp .. 1 = full vanilla blur + + float dofType = LumaSettings.GameSettings.DOFType; // 0 = vanilla game Gaussian, 1 = Luma separable Gaussian + float3 hdr_color; + if (dofType < 0.5) + { + // --- vanilla DOF composite (verbatim) --- + r0.y = DOFKernelSize.w + v5.w; + r0.x = v5.z; + r0.xy = r0.xy * 2 + -1; + r0.xy = r0.xy * DOFKernelSize.z; + r0.x = saturate(dot(r0.x, r0.x) + 0); + r0.x = -r0.x + 1; + r0.yz = max(v6.xzww, HalfResMaskRect.xxyw).yz; + r1.xy = min(HalfResMaskRect.zw, r0.yz); + r1 = t4.Sample(s4_s, r1.xy); + r0.x = saturate(r0.x + r1.w); + r2 = float4(1, 1, 0, 0) * v6.xyxx; + r2 = t0.SampleLevel(s0_s, r2.xy, 0); + hdr_color = lerp(r1.xyz * 4, r2.rgb, r0.x); + } + else if (blurWeight <= 0.02) + { + hdr_color = sceneTap.rgb; // in-focus (both Luma modes) -> no blur; covers most of the screen (cheap) + } + else + { + // Luma separable Gaussian: t7 holds the pre-blurred half-res scene (dense H+V Gaussian on the prefilter), + // so this is a single smooth tap — no sparse gather, no jitter -> grain-free. Blend at the game's DoF + // strength (blurWeight). HDR is preserved, so bright defocused areas glow. + hdr_color = lerp(sceneTap.rgb, t7.SampleLevel(s0_s, sceneUV, 0).rgb, blurWeight); + } + + // --- bloom --- + if (LumaSettings.GameSettings.LumaBloomEnable > 0.5) + { + // Luma HDR pyramidal bloom (t5, generated by the mod from the linear fp16 scene). Additive in linear so + // bright HDR sources glow proportionally (the game's bloom is UNORM-clamped + quarter-res single-level). + // v6.xy = the screen UV used for the scene sample above; s1_s is a linear sampler. + float3 lumaBloom = t5.SampleLevel(s1_s, v6.xy, 0).rgb; + hdr_color += lumaBloom * LumaSettings.GameSettings.BloomIntensity; + } + else + { + // Vanilla bloom (screen-blend gated by luminance, t1). BloomIntensity scales it (1 = vanilla). + r0.w = dot(hdr_color, float3(0.300000012, 0.589999974, 0.109999999)); + r0.w = r0.w * -3; + r0.w = exp2(r0.w); + r0.w = saturate(r0.w * BloomTintAndScreenBlendThreshold.w); + r1 = t1.Sample(s1_s, v5.zw); + r1.xyz = r1.xyz * BloomTintAndScreenBlendThreshold.xyz; + r1.xyz = r1.xyz * 4; + hdr_color += r1.xyz * r0.w * LumaSettings.GameSettings.BloomIntensity; + } + +#if TM_HAS_LIGHTSHAFT + // Light shafts / god rays (TPS only). Vanilla TPS composites a separate low-res light-shaft buffer here, + // between bloom and the tonemap: an inverse-luminance gate (only adds into darker pixels), additive *4 + // color, and a per-pixel attenuation in .a that scales the existing scene down where shafts occlude. + // Transcribed verbatim from the DX9 TPS tonemap (tps_tonemap_0xF8997849). BL2 has no such pass. + { + float lsGate = saturate(exp2(dot(hdr_color, float3(0.300000012, 0.589999974, 0.109999999)) * -3.0)); + float4 ls = t_lightshaft.Sample(s0_s, v5.zw); + hdr_color = hdr_color * ls.w + (ls.xyz * 4.0) * lsGate; + } +#endif + + // User Exposure (scene-referred, pre-grade; 1 = vanilla). Applies to both SDR and HDR — the grade below tracks it. + hdr_color *= LumaSettings.GameSettings.Exposure; + + // --- max-channel compress BEFORE grade+LUT (HDR only) --- + // tm = per-pixel uniform scale (<=1) that maps the brightest channel into [0,1] via a midgray-pinned Reinhard. + // One scale for all 3 channels => hue/chroma intact; brightest channel pinned <=1 => the vanilla-DOF-branch + // saturate and the 16-slice LUT never clip. SDR mode leaves tm=1 so the grade runs on the raw scene like vanilla. + float tm = 1.0; + if (LumaSettings.DisplayMode == 1) + { + float mx = max(hdr_color.r, max(hdr_color.g, hdr_color.b)); + float mx_c = Reinhard::ReinhardScalable(mx, 1.0, 0.0, MidGray, MidGray); + tm = (mx > 1e-6) ? (mx_c / mx) : 1.0; + } + r0.xyz = hdr_color * tm; + + // --- vignette (verbatim) --- + float3 vignette_color = r0.rgb; + r1.xyz = r0.xyz * VignetteColor.xyz; + r2.xyz = r0.xyz * -VignetteColor.xyz + r0.xyz; + r1.xyz = v6.y * r2.xyz + r1.xyz; + r2.xyz = r0.xyz * r1.xyz; + r1.xyz = r0.xyz * -r1.xyz + r0.xyz; + r3.xy = v6.xy + v6.xy; + r3 = t2.Sample(s2_s, r3.xy); + r0.w = saturate(r3.x + VignetteSettings.y); + r1.xyz = r0.w * r1.xyz + r2.xyz; + r2.y = 0.00999999978; + r0.w = r2.y + -VignetteSettings.x; + r0.xyz = (r0.w >= 0) ? r0.xyz : r1.xyz; + // User Vignette Intensity: lerp between the pre-vignette color and the vignetted result (1 = vanilla, 0 = none). + r0.xyz = lerp(vignette_color, r0.xyz, LumaSettings.GameSettings.VignetteIntensity); + + // --- ImageAdjustments per-channel curve (verbatim; keep swizzles exactly) --- + r1 = r0.zzxy + -ImageAdjustments2.z; + r1 = saturate(r1 * 10000); + r2.xyz = r0.xyz + ImageAdjustments2.x; + r3.z = 1 / abs(r2.x); + r3.w = 1 / abs(r2.y); + r3.xy = 1 / abs(r2.z); + r2 = r0.zzxy * r3; + r0.xyz = r0.xyz * ImageAdjustments2.w; + r3.x = log2(r0.x); + r3.y = log2(r0.y); + r3.z = log2(r0.z); + r0.xyz = r3.xyz * 0.454545468; + r3.z = exp2(r0.x); + r3.w = exp2(r0.y); + r3.xy = exp2(r0.z); + r0 = r2.yyzw * ImageAdjustments2.y + -r3.yyzw; + r0 = r1 * r0 + r3; + r1 = r2 * ImageAdjustments2.y + -r0.yyzw; + r0 = saturate(ImageAdjustments3.x * r1 + r0); + + // --- 16-slice ColorGradingLUT (verbatim trilinear; keep swizzles exactly) --- + // (Vanilla trilinear: the LUT is near-linear between grid points, so tetrahedral interpolation buys no visible gain.) + r1.xyw = (r0.xwzz * float4(14.9998999, 0.9375, 0.9375, 0.05859375)).xyw; + r0.x = frac(r1.x); + r0.x = -r0.x + r1.x; + r1.x = r0.x * 0.0625 + r1.w; + r0.x = r0.y * 15 + -r0.x; + r1 = r1.xyxy + float4(0.001953125, 0.03125, 0.064453125, 0.03125); + r2 = t3.Sample(s3_s, r1.zw); + r1 = t3.Sample(s3_s, r1.xy); + r0.yzw = (-r1.xxyz + r2.xxyz).yzw; + o.xyz = r0.x * r0.yzw + r1.xyz; + + // ====================== Luma HDR output (max-channel inverse) ====================== + // o.rgb = the graded look the LUT produced. In HDR it is the grade of the COMPRESSED scene (hdr_color*tm); + // dividing by tm is the exact inverse -> recovers real HDR range with true hue+chroma (two bright pixels that + // both saturate the LUT are re-separated by /tm). In SDR tm was 1, so o.rgb is the vanilla grade of the raw scene. + float3 graded_sdr_gamma = o.rgb; + float3 sdr_lin = gamma_to_linear(graded_sdr_gamma, GCT_MIRROR); + + const float paperWhite = LumaSettings.GamePaperWhiteNits / sRGB_WhiteLevelNits; + const float peakWhite = LumaSettings.PeakWhiteNits / sRGB_WhiteLevelNits; + + float3 postProcessedColor; + + if (LumaSettings.DisplayMode == 1) // HDR + { + float3 recovered = sdr_lin / max(tm, 1e-6); // exact inverse of the pre-grade compression -> HDR range (linear BT.709) + + // Display rolloff to the user's peak/paper-white nits. DICE by-luminance preserves hue; the + // *_CORRECT_CHANNELS_BEYOND_PEAK_WHITE type additionally gamut-maps any single channel that rides past peak + // (e.g. saturated blues) back under it. + // DICE takes/returns linear BT.709 and processes internally in BT.2020 (its defaults), so feed BT.709 + // directly: do NOT round-trip primaries by hand. (With type 1 the manual 709<->2020 cancelled because the + // luminance compress is a pure scalar; the corrected type's per-channel gamut map breaks that cancellation.) + DICESettings settings = DefaultDICESettings(DICE_TYPE_BY_LUMINANCE_PQ_CORRECT_CHANNELS_BEYOND_PEAK_WHITE); + float3 hdr = DICETonemap(recovered * paperWhite, peakWhite, settings) / paperWhite; + + // --- User HDR grade (HDR display path only; defaults are vanilla no-ops) --- + // Highlight desaturation: bright sources fade toward white as luminance approaches peak (eye/sensor + // saturation). exponent in [1,0.05] keeps mid-tones colored; only luminance->peak whitens. + const float highlightDechroma = LumaSettings.GameSettings.HighlightDechroma; + if (highlightDechroma > 0.0) + { + float dcExp = lerp(1.0, 0.05, highlightDechroma); + float dcWeight = saturate(pow(saturate(GetLuminance(hdr) / peakWhite), dcExp)); + hdr = Saturation(hdr, 1.0 - dcWeight); + } + hdr = Saturation(hdr, LumaSettings.GameSettings.Saturation); // user Saturation (Oklab; 1 = vanilla) + // user Contrast: slope around 18% mid-gray (linear, 1.0 = paper white). Excursions caught by the NaN/clamp tail. + const float midGray = 0.18; + hdr = (hdr - midGray) * LumaSettings.GameSettings.Contrast + midGray; + + postProcessedColor = hdr; + } + else // SDR (still presented through the scRGB swapchain) — sdr_lin is the vanilla grade (tm was 1) + { + postProcessedColor = sdr_lin; + } + +#if UI_DRAW_TYPE >= 2 + // Pre-scale so the gamma-SDR HUD drawn on top (not pre-scaled) lands at UIPaperWhite after the composition + // rescales the buffer by it; the scene then lands at GamePaperWhite. Gives the HUD its own paper white. + postProcessedColor *= LumaSettings.GamePaperWhiteNits / max(LumaSettings.UIPaperWhiteNits, 1.0); +#endif + + // Sanitize (inverse divide + DICE + gamma encode can emit NaN/negatives -> garbage on the swapchain). + postProcessedColor = (postProcessedColor == postProcessedColor) ? postProcessedColor : 0.0; // NaN -> 0 + postProcessedColor = max(0.0, postProcessedColor); + + postProcessedColor = linear_to_gamma(postProcessedColor, GCT_MIRROR); + + // Sub-perceptual animated triangular dither (9-bit, gamma space) vs gradient banding from the HDR expansion + + // 10-bit PQ encode. HDR only, runtime toggle (GameSettings.Dithering), FrameIndex animates it. Runs before + // SMAA but ~1/511 noise is below SMAA's 0.05 edge threshold -> no spawned edges / RCAS amplification. + if (LumaSettings.DisplayMode == 1 && LumaSettings.GameSettings.Dithering > 0.5) + ApplyDithering(postProcessedColor, v6.xy, true, 1.0, DITHERING_BIT_DEPTH, LumaSettings.FrameIndex, true); + + return float4(postProcessedColor, 0.0); // vanilla wrote o0.w = 0 +} diff --git a/Shaders/Borderlands 2 and The Pre-Sequel/Luma_Bloom_impl.hlsl b/Shaders/Borderlands 2 and The Pre-Sequel/Luma_Bloom_impl.hlsl index 61663079..a6c626df 100644 --- a/Shaders/Borderlands 2 and The Pre-Sequel/Luma_Bloom_impl.hlsl +++ b/Shaders/Borderlands 2 and The Pre-Sequel/Luma_Bloom_impl.hlsl @@ -5,7 +5,7 @@ // // Threshold = soft-knee quadratic (Bloom.hlsl default LUMA_BLOOM_THRESHOLD_FUNCTION). Source is linear scene // referred to paper white (1.0), so threshold 1.0 blooms above-paper-white highlights; soft knee 0.5 fades the -// transition in (web best-practice — avoids hard popping on cel-shade ink edges). Karis firefly weighting is +// transition in (avoids hard popping on cel-shade ink edges). Karis firefly weighting is // applied separately (DrawKarisAverage) before this, since BL2 has no TAA to hide sparkle. #include "../Includes/Color.hlsl" // GetLuminance, gamma helpers used by Bloom.hlsl 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 93c7eddf..48ac3175 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 @@ -23,7 +23,7 @@ cbuffer SmaaMetricsCB : register(b1) #define SMAA_PRESET_ULTRA #define SMAA_PREDICATION 1 #define SMAA_PREDICATION_SCALE SmaaPredication.x -// Predication tuned to ReShade-community best-practice + BL2's clean view-Z depth signal: +// Predication tuned to BL2's clean view-Z depth signal: // - flat threshold = SCALE * SMAA_THRESHOLD = 2.0 * 0.05 = 0.10 (recommended; rejects busy // cel-shade texture color-noise so SMAA doesn't over-AA flat detail) // - silhouette thr = SCALE * SMAA_THRESHOLD * (1-STR) = 2.0 * 0.05 *0.5 = 0.05 (= plain ULTRA base; predication diff --git a/Shaders/Borderlands 2 and The Pre-Sequel/Tonemap_0x2079F1E8.ps_5_0.hlsl b/Shaders/Borderlands 2 and The Pre-Sequel/Tonemap_0x2079F1E8.ps_5_0.hlsl new file mode 100644 index 00000000..3e92a40d --- /dev/null +++ b/Shaders/Borderlands 2 and The Pre-Sequel/Tonemap_0x2079F1E8.ps_5_0.hlsl @@ -0,0 +1,35 @@ +// Borderlands: The Pre-Sequel — tonemap / HDR injection point (dgVoodoo 2.81.3 -> ps_4_0, hash 0x2079F1E8). +// Older dgVoodoo builds emit ps_4_0 -> a different CSO hash for the same DX9 shader. Identical I/O + slot map +// (LightShaft@t1, +1 shift, Luma bloom -> t8) to the 2.87.3 TPS variant (0xFCFE623E), so it reuses the same +// TPS slot macros over the shared grade impl. +#define TM_HAS_LIGHTSHAFT 1 +#define TM_T_LIGHTSHAFT t1 // LightShaftTexture (god rays) — TPS-only, inserted at slot 1 +#define TM_T_BLOOM t2 // FilterColor1Texture (screen-blend bloom) +#define TM_T_VIGNETTE t3 // VignetteTexture +#define TM_T_LUT t4 // ColorGradingLUT (256x16, 16-slice) +#define TM_T_DOF t5 // LowResPostProcessBuffer (half-res DOF) +#define TM_T_LUMABLOOM t8 // injected Luma HDR bloom (t5 is the native DOF on TPS — bind higher to avoid the clash) +#define TM_S_BLOOM s2 +#define TM_S_VIGNETTE s3 +#define TM_S_LUT s4 +#define TM_S_DOF s5 +#include "Luma_BL2TPS_Tonemap.hlsl" + +void main( + float4 v0 : SV_POSITION0, + float4 v1 : TEXCOORD8, + float4 v2 : COLOR0, + float4 v3 : COLOR1, + float4 v4 : TEXCOORD9, + float4 v5 : TEXCOORD0, + float4 v6 : TEXCOORD1, + float4 v7 : TEXCOORD2, + float4 v8 : TEXCOORD3, + float4 v9 : TEXCOORD4, + float4 v10 : TEXCOORD5, + float4 v11 : TEXCOORD6, + float4 v12 : TEXCOORD7, + out float4 o0 : SV_TARGET0) +{ + o0 = RunTonemap(v5, v6); +} diff --git a/Shaders/Borderlands 2 and The Pre-Sequel/Tonemap_0xD00AA2A7.ps_5_0.hlsl b/Shaders/Borderlands 2 and The Pre-Sequel/Tonemap_0xD00AA2A7.ps_5_0.hlsl index 6df68c96..8c702c21 100644 --- a/Shaders/Borderlands 2 and The Pre-Sequel/Tonemap_0xD00AA2A7.ps_5_0.hlsl +++ b/Shaders/Borderlands 2 and The Pre-Sequel/Tonemap_0xD00AA2A7.ps_5_0.hlsl @@ -1,82 +1,6 @@ -// clang-format off -// ORDER MATTERS — do NOT let clang-format sort these. The game-local Common.hlsl MUST come first: it defines -// LumaGameSettings (via GameCBuffers.hlsl) BEFORE the shared Settings.hlsl (pulled in by Color.hlsl below) -// declares the LumaSettings cbuffer. If sorted after Color.hlsl, GameSettings becomes the empty fallback struct -// and every LumaSettings.GameSettings.* reference fails to compile (invalid subscript). -#include "Includes/Common.hlsl" // game-local: LumaGameSettings (grade sliders) — keep FIRST -#include "../Includes/Color.hlsl" // BT709_To_BT2020 / BT2020_To_BT709 -#include "../Includes/ColorGradingLUT.hlsl" // SimpleGamutClip -#include "../Includes/DICE.hlsl" -#include "../Includes/Reinhard.hlsl" // Reinhard::ReinhardScalable (max-channel compressor) -// clang-format on - -// Borderlands 2 uber post-process / tonemap (UE3, via dgVoodoo D3D9->11 -> ps_5_0). -// Vanilla body (DOF + screen-blend bloom + vignette + ImageAdjustments + 16-slice ColorGradingLUT) transcribed -// VERBATIM (register-level) from the readable DX9 BL2 tonemap (tonemap_0x54ED86A0.ps_3_0), -// constants remapped DX9 cN -> cb4[N+8] (verified vs the live 0xD00AA2A7 disasm). -// -// HDR output uses the "apply SDR LUT in HDR" method: max-channel-compress the raw scene into [0,1] BEFORE the -// grade+LUT (so neither the per-channel saturate nor the LUT clip -> highlight hue/chroma preserved), then expand -// back with the exact inverse (sdr / tm) and DICE-map to the display peak. SDR mode keeps tm=1 -> the grade runs on -// the raw scene exactly like vanilla. Compression MUST be max-channel: it scales all 3 channels by one factor (hue -// intact) AND pins the brightest channel <=1 (nothing escapes the LUT). by-luminance would leave a saturated -// channel >1 and the LUT would clip it; per-channel shifts hue outright. -// NOTE: the per-channel ImageAdjustments curve below MUST keep the original's exact swizzles (r0.zzxy / r3.z,w,xy) — a -// "cleaner" rewrite swaps channels and casts the whole image green. - -// ---- Per-game texture/sampler slot map: Borderlands 2 vs The Pre-Sequel ------------------------------ -// dgVoodoo maps each DX9 sampler sN 1:1 onto DX11 tN. BL2 and TPS run the same UE3 uber-post tonemap, but TPS -// inserts a LightShaftTexture at slot 1, shifting bloom/vignette/LUT/DOF DOWN one slot (verified against -// the two DX9 tonemaps: BL2 tonemap_0x54ED86A0 has LUT@s3/DOF@s4; TPS tps_tonemap_0xF8997849 has -// lightshaft@s1, LUT@s4, DOF@s5). The grade math is identical, so the body below is SHARED verbatim — only -// which register each named texture binds to changes, plus TPS's extra light-shaft composite term. The TPS -// file (Tonemap_0xFCFE623E.ps_5_0.hlsl) #defines these macros then #includes this file; with them undefined -// (BL2) every mapping is the identity = the original BL2 bindings, so BL2 is byte-for-byte unchanged. -#ifndef TM_T_BLOOM -#define TM_T_BLOOM t1 // FilterColor1Texture (screen-blend bloom) -#define TM_T_VIGNETTE t2 // VignetteTexture -#define TM_T_LUT t3 // ColorGradingLUT (256x16, 16-slice) -#define TM_T_DOF t4 // LowResPostProcessBuffer (half-res DOF) -#define TM_T_LUMABLOOM t5 // injected Luma HDR bloom (free slot on BL2) -#define TM_S_BLOOM s1 -#define TM_S_VIGNETTE s2 -#define TM_S_LUT s3 -#define TM_S_DOF s4 -#endif - -Texture2D t0 : register(t0); // SceneColorTexture (fp16 HDR scene) -#if TM_HAS_LIGHTSHAFT -Texture2D t_lightshaft : register(TM_T_LIGHTSHAFT); // LightShaftTexture (TPS god rays) — slot 1 -#endif -Texture2D t1 : register(TM_T_BLOOM); // FilterColor1Texture (bloom) BL2 t1 / TPS t2 -Texture2D t2 : register(TM_T_VIGNETTE); // VignetteTexture BL2 t2 / TPS t3 -Texture2D t3 : register(TM_T_LUT); // ColorGradingLUT (256x16, 16-slice) BL2 t3 / TPS t4 -Texture2D t4 : register(TM_T_DOF); // LowResPostProcessBuffer (half-res DOF) BL2 t4 / TPS t5 -Texture2D t5 : register(TM_T_LUMABLOOM); // Luma HDR pyramidal bloom, bound by the mod when LumaBloomEnable (BL2 t5 / TPS t8 — TPS t5 is the native DOF) -Texture2D t7 : register(t7); // Half-res pre-blurred scene for the Luma Gaussian DoF, bound when it is active (free on both BL2/TPS) - -SamplerState s0_s : register(s0); -SamplerState s1_s : register(TM_S_BLOOM); // BL2 s1 / TPS s2 -SamplerState s2_s : register(TM_S_VIGNETTE); // BL2 s2 / TPS s3 -SamplerState s3_s : register(TM_S_LUT); // BL2 s3 / TPS s4 -SamplerState s4_s : register(TM_S_DOF); // BL2 s4 / TPS s5 - -cbuffer cb3 : register(b3) -{ - float4 cb3[77]; -} -cbuffer cb4 : register(b4) -{ - float4 cb4[236]; -} - -#define BloomTintAndScreenBlendThreshold cb4[16] // c8 -#define ImageAdjustments2 cb4[17] // c9 -#define ImageAdjustments3 cb4[18] // c10 -#define HalfResMaskRect cb4[19] // c11 -#define DOFKernelSize cb4[20] // c12 -#define VignetteSettings cb4[21] // c13 -#define VignetteColor cb4[22] // c14 +// Borderlands 2 tonemap / HDR injection point (dgVoodoo 2.87.3 -> ps_5_0, hash 0xD00AA2A7). +// Thin wrapper: identity BL2 slot map (no macros) + the shared grade impl. See Luma_BL2TPS_Tonemap.hlsl. +#include "Luma_BL2TPS_Tonemap.hlsl" void main( float4 v0 : SV_POSITION0, @@ -84,8 +8,8 @@ void main( float4 v2 : COLOR0, float4 v3 : COLOR1, float4 v4 : TEXCOORD9, - float4 v5 : TEXCOORD0, // DOF radial/kernel coords (.zw) - float4 v6 : TEXCOORD1, // scene UV (.xy), half-res DOF UV (.zw) + float4 v5 : TEXCOORD0, + float4 v6 : TEXCOORD1, float4 v7 : TEXCOORD2, float4 v8 : TEXCOORD3, float4 v9 : TEXCOORD4, @@ -94,227 +18,5 @@ void main( float4 v12 : TEXCOORD7, out float4 o0 : SV_TARGET0) { - float4 o; - float4 r0, r1, r2, r3; - - // --- DOF: vanilla half-res Gaussian composite, OR Luma separable HDR Gaussian --- - // The game's depth/object DoF decision is baked into t4.a (the in-focus weight: 1 = sharp focused - // subject, 0.25 = max-blurred background; devkit clone confirmed it tracks the focused subject's - // silhouette). The Luma path KEEPS that decision — when the game turns DoF off, t4.a -> ~1 -> the blend - // weight -> 0 -> no blur, so cutscenes/scripted focus stay game-driven — but replaces the half-res - // Gaussian with a denser separable Gaussian of the prefiltered scene (t7), preserving HDR so bright - // defocused areas glow. DOFKernelSize.z/.w + v5.zw drive only the vanilla fallback below. - float2 sceneUV = v6.xy; - float2 dofUV = clamp(v6.zw, HalfResMaskRect.xy, HalfResMaskRect.zw); - float4 sceneTap = t0.SampleLevel(s0_s, sceneUV, 0); - float centerInFocus = t4.SampleLevel(s4_s, dofUV, 0).a; // 1 = sharp .. 0.25 = max blur - - // Replicate the game's EXACT per-pixel DoF blend weight (radial DOF-kernel falloff, VERBATIM from the - // vanilla branch below, + the t4.a in-focus weight), then blend the blurred scene by it. The game blends - // only a fraction of its blurred buffer (sharpness >> t4.a alone), so matching this weight is what keeps - // the effect at vanilla strength instead of fully replacing the background. Without it it over-blurs. - float dofRadial = (v5.z * 2.0 - 1.0) * DOFKernelSize.z; - float radialSharp = 1.0 - saturate(dofRadial * dofRadial); - float blurWeight = 1.0 - saturate(radialSharp + centerInFocus); // 0 = sharp .. 1 = full vanilla blur - - float dofType = LumaSettings.GameSettings.DOFType; // 0 = vanilla game Gaussian, 2 = Luma separable Gaussian - float3 hdr_color; - if (dofType < 0.5) - { - // --- vanilla DOF composite (verbatim) --- - r0.y = DOFKernelSize.w + v5.w; - r0.x = v5.z; - r0.xy = r0.xy * 2 + -1; - r0.xy = r0.xy * DOFKernelSize.z; - r0.x = saturate(dot(r0.x, r0.x) + 0); - r0.x = -r0.x + 1; - r0.yz = max(v6.xzww, HalfResMaskRect.xxyw).yz; - r1.xy = min(HalfResMaskRect.zw, r0.yz); - r1 = t4.Sample(s4_s, r1.xy); - r0.x = saturate(r0.x + r1.w); - r2 = float4(1, 1, 0, 0) * v6.xyxx; - r2 = t0.SampleLevel(s0_s, r2.xy, 0); - hdr_color = lerp(r1.xyz * 4, r2.rgb, r0.x); - } - else if (blurWeight <= 0.02) - { - hdr_color = sceneTap.rgb; // in-focus (both Luma modes) -> no blur; covers most of the screen (cheap) - } - else - { - // Luma separable Gaussian: t7 holds the pre-blurred half-res scene (dense H+V Gaussian on the prefilter), - // so this is a single smooth tap — no sparse gather, no jitter -> grain-free. Blend at the game's DoF - // strength (blurWeight). HDR is preserved, so bright defocused areas glow. - hdr_color = lerp(sceneTap.rgb, t7.SampleLevel(s0_s, sceneUV, 0).rgb, blurWeight); - } - - // --- bloom --- - if (LumaSettings.GameSettings.LumaBloomEnable > 0.5) - { - // Luma HDR pyramidal bloom (t5, generated by the mod from the linear fp16 scene). Additive in linear so - // bright HDR sources glow proportionally (the game's bloom is UNORM-clamped + quarter-res single-level). - // v6.xy = the screen UV used for the scene sample above; s1_s is a linear sampler. - float3 lumaBloom = t5.SampleLevel(s1_s, v6.xy, 0).rgb; - hdr_color += lumaBloom * LumaSettings.GameSettings.BloomIntensity; - } - else - { - // Vanilla bloom (screen-blend gated by luminance, t1). BloomIntensity scales it (1 = vanilla). - r0.w = dot(hdr_color, float3(0.300000012, 0.589999974, 0.109999999)); - r0.w = r0.w * -3; - r0.w = exp2(r0.w); - r0.w = saturate(r0.w * BloomTintAndScreenBlendThreshold.w); - r1 = t1.Sample(s1_s, v5.zw); - r1.xyz = r1.xyz * BloomTintAndScreenBlendThreshold.xyz; - r1.xyz = r1.xyz * 4; - hdr_color += r1.xyz * r0.w * LumaSettings.GameSettings.BloomIntensity; - } - -#if TM_HAS_LIGHTSHAFT - // Light shafts / god rays (TPS only). Vanilla TPS composites a separate low-res light-shaft buffer here, - // between bloom and the tonemap: an inverse-luminance gate (only adds into darker pixels), additive *4 - // color, and a per-pixel attenuation in .a that scales the existing scene down where shafts occlude. - // Transcribed verbatim from the DX9 TPS tonemap (tps_tonemap_0xF8997849). BL2 has no such pass. - { - float lsGate = saturate(exp2(dot(hdr_color, float3(0.300000012, 0.589999974, 0.109999999)) * -3.0)); - float4 ls = t_lightshaft.Sample(s0_s, v5.zw); - hdr_color = hdr_color * ls.w + (ls.xyz * 4.0) * lsGate; - } -#endif - - // User Exposure (scene-referred, pre-grade; 1 = vanilla). Applies to both SDR and HDR — the grade below tracks it. - hdr_color *= LumaSettings.GameSettings.Exposure; - - // --- max-channel compress BEFORE grade+LUT (HDR only) --- - // tm = per-pixel uniform scale (<=1) that maps the brightest channel into [0,1] via a midgray-pinned Reinhard. - // One scale for all 3 channels => hue/chroma intact; brightest channel pinned <=1 => the saturate(line ~129) and - // the 16-slice LUT never clip. SDR mode leaves tm=1 so the grade runs on the raw scene exactly like vanilla. - float tm = 1.0; - if (LumaSettings.DisplayMode == 1) - { - float mx = max(hdr_color.r, max(hdr_color.g, hdr_color.b)); - float mx_c = Reinhard::ReinhardScalable(mx, 1.0, 0.0, MidGray, MidGray); - tm = (mx > 1e-6) ? (mx_c / mx) : 1.0; - } - r0.xyz = hdr_color * tm; - - // --- vignette (verbatim) --- - float3 vignette_color = r0.rgb; - r1.xyz = r0.xyz * VignetteColor.xyz; - r2.xyz = r0.xyz * -VignetteColor.xyz + r0.xyz; - r1.xyz = v6.y * r2.xyz + r1.xyz; - r2.xyz = r0.xyz * r1.xyz; - r1.xyz = r0.xyz * -r1.xyz + r0.xyz; - r3.xy = v6.xy + v6.xy; - r3 = t2.Sample(s2_s, r3.xy); - r0.w = saturate(r3.x + VignetteSettings.y); - r1.xyz = r0.w * r1.xyz + r2.xyz; - r2.y = 0.00999999978; - r0.w = r2.y + -VignetteSettings.x; - r0.xyz = (r0.w >= 0) ? r0.xyz : r1.xyz; - // User Vignette Intensity: lerp between the pre-vignette color and the vignetted result (1 = vanilla, 0 = none). - r0.xyz = lerp(vignette_color, r0.xyz, LumaSettings.GameSettings.VignetteIntensity); - - // --- ImageAdjustments per-channel curve (verbatim; keep swizzles exactly) --- - r1 = r0.zzxy + -ImageAdjustments2.z; - r1 = saturate(r1 * 10000); - r2.xyz = r0.xyz + ImageAdjustments2.x; - r3.z = 1 / abs(r2.x); - r3.w = 1 / abs(r2.y); - r3.xy = 1 / abs(r2.z); - r2 = r0.zzxy * r3; - r0.xyz = r0.xyz * ImageAdjustments2.w; - r3.x = log2(r0.x); - r3.y = log2(r0.y); - r3.z = log2(r0.z); - r0.xyz = r3.xyz * 0.454545468; - r3.z = exp2(r0.x); - r3.w = exp2(r0.y); - r3.xy = exp2(r0.z); - r0 = r2.yyzw * ImageAdjustments2.y + -r3.yyzw; - r0 = r1 * r0 + r3; - r1 = r2 * ImageAdjustments2.y + -r0.yyzw; - r0 = saturate(ImageAdjustments3.x * r1 + r0); - - // --- 16-slice ColorGradingLUT (verbatim trilinear; keep swizzles exactly) --- - // (Tetrahedral interpolation was measured against this on the live LUT: maxΔ 0.5/255, 0% visibly different — the - // LUT is near-linear between grid points so it gives no gain at +cost. Reverted to vanilla trilinear.) - r1.xyw = (r0.xwzz * float4(14.9998999, 0.9375, 0.9375, 0.05859375)).xyw; - r0.x = frac(r1.x); - r0.x = -r0.x + r1.x; - r1.x = r0.x * 0.0625 + r1.w; - r0.x = r0.y * 15 + -r0.x; - r1 = r1.xyxy + float4(0.001953125, 0.03125, 0.064453125, 0.03125); - r2 = t3.Sample(s3_s, r1.zw); - r1 = t3.Sample(s3_s, r1.xy); - r0.yzw = (-r1.xxyz + r2.xxyz).yzw; - o.xyz = r0.x * r0.yzw + r1.xyz; - - // ====================== Luma HDR output (max-channel inverse) ====================== - // o.rgb = the graded look the LUT produced. In HDR it is the grade of the COMPRESSED scene (hdr_color*tm); - // dividing by tm is the exact inverse -> recovers real HDR range with true hue+chroma (two bright pixels that - // both saturate the LUT are re-separated by /tm). In SDR tm was 1, so o.rgb is the vanilla grade of the raw scene. - float3 graded_sdr_gamma = o.rgb; - float3 sdr_lin = gamma_to_linear(graded_sdr_gamma, GCT_MIRROR); - - const float paperWhite = LumaSettings.GamePaperWhiteNits / sRGB_WhiteLevelNits; - const float peakWhite = LumaSettings.PeakWhiteNits / sRGB_WhiteLevelNits; - - float3 postProcessedColor; - - if (LumaSettings.DisplayMode == 1) // HDR - { - float3 recovered = sdr_lin / max(tm, 1e-6); // exact inverse of the pre-grade compression -> HDR range (linear BT.709) - - // Display rolloff to the user's peak/paper-white nits. DICE by-luminance preserves hue; the - // *_CORRECT_CHANNELS_BEYOND_PEAK_WHITE type additionally gamut-maps any single channel that rides past peak - // (e.g. saturated blues) back under it (fleet default). - // DICE takes/returns linear BT.709 and processes internally in BT.2020 (its defaults), so feed BT.709 - // directly: do NOT round-trip primaries by hand. (With type 1 the manual 709<->2020 cancelled because the - // luminance compress is a pure scalar; the corrected type's per-channel gamut map breaks that cancellation.) - DICESettings settings = DefaultDICESettings(DICE_TYPE_BY_LUMINANCE_PQ_CORRECT_CHANNELS_BEYOND_PEAK_WHITE); - float3 hdr = DICETonemap(recovered * paperWhite, peakWhite, settings) / paperWhite; - - // --- User HDR grade (HDR display path only; defaults are vanilla no-ops) --- - // Highlight desaturation: bright sources fade toward white as luminance approaches peak (eye/sensor - // saturation). exponent in [1,0.05] keeps mid-tones colored; only luminance->peak whitens. - const float highlightDechroma = LumaSettings.GameSettings.HighlightDechroma; - if (highlightDechroma > 0.0) - { - float dcExp = lerp(1.0, 0.05, highlightDechroma); - float dcWeight = saturate(pow(saturate(GetLuminance(hdr) / peakWhite), dcExp)); - hdr = Saturation(hdr, 1.0 - dcWeight); - } - hdr = Saturation(hdr, LumaSettings.GameSettings.Saturation); // user Saturation (Oklab; 1 = vanilla) - // user Contrast: slope around 18% mid-gray (linear, 1.0 = paper white). Excursions caught by the NaN/clamp tail. - const float midGray = 0.18; - hdr = (hdr - midGray) * LumaSettings.GameSettings.Contrast + midGray; - - postProcessedColor = hdr; - } - else // SDR (still presented through the scRGB swapchain) — sdr_lin is the vanilla grade (tm was 1) - { - postProcessedColor = sdr_lin; - } - -#if UI_DRAW_TYPE >= 2 - // Pre-scale so the gamma-SDR HUD drawn on top (not pre-scaled) lands at UIPaperWhite after the composition - // rescales the buffer by it; the scene then lands at GamePaperWhite. Gives the HUD its own paper white. - postProcessedColor *= LumaSettings.GamePaperWhiteNits / max(LumaSettings.UIPaperWhiteNits, 1.0); -#endif - - // Sanitize (inverse divide + DICE + gamma encode can emit NaN/negatives -> garbage on the swapchain). - postProcessedColor = (postProcessedColor == postProcessedColor) ? postProcessedColor : 0.0; // NaN -> 0 - postProcessedColor = max(0.0, postProcessedColor); - - postProcessedColor = linear_to_gamma(postProcessedColor, GCT_MIRROR); - - // Sub-perceptual animated triangular dither (9-bit, gamma space) vs gradient banding from the HDR expansion + - // 10-bit PQ encode. HDR only, runtime toggle (GameSettings.Dithering), FrameIndex animates it. Runs before - // SMAA but ~1/511 noise is below SMAA's 0.05 edge threshold -> no spawned edges / RCAS amplification. - if (LumaSettings.DisplayMode == 1 && LumaSettings.GameSettings.Dithering > 0.5) - ApplyDithering(postProcessedColor, v6.xy, true, 1.0, DITHERING_BIT_DEPTH, LumaSettings.FrameIndex, true); - - o0.rgb = postProcessedColor; - o0.a = 0; // vanilla wrote o.w = 0 + o0 = RunTonemap(v5, v6); } diff --git a/Shaders/Borderlands 2 and The Pre-Sequel/Tonemap_0xF14F8664.ps_5_0.hlsl b/Shaders/Borderlands 2 and The Pre-Sequel/Tonemap_0xF14F8664.ps_5_0.hlsl new file mode 100644 index 00000000..f34e98d2 --- /dev/null +++ b/Shaders/Borderlands 2 and The Pre-Sequel/Tonemap_0xF14F8664.ps_5_0.hlsl @@ -0,0 +1,23 @@ +// Borderlands 2 tonemap / HDR injection point (dgVoodoo 2.81.3 -> ps_4_0, hash 0xF14F8664). +// Older dgVoodoo builds emit ps_4_0 -> a different CSO hash for the same DX9 shader. Identical I/O + slot map to +// the 2.87.3 variant (0xD00AA2A7), so this is the same identity wrapper over the shared grade impl. +#include "Luma_BL2TPS_Tonemap.hlsl" + +void main( + float4 v0 : SV_POSITION0, + float4 v1 : TEXCOORD8, + float4 v2 : COLOR0, + float4 v3 : COLOR1, + float4 v4 : TEXCOORD9, + float4 v5 : TEXCOORD0, + float4 v6 : TEXCOORD1, + float4 v7 : TEXCOORD2, + float4 v8 : TEXCOORD3, + float4 v9 : TEXCOORD4, + float4 v10 : TEXCOORD5, + float4 v11 : TEXCOORD6, + float4 v12 : TEXCOORD7, + out float4 o0 : SV_TARGET0) +{ + o0 = RunTonemap(v5, v6); +} diff --git a/Shaders/Borderlands 2 and The Pre-Sequel/Tonemap_0xFCFE623E.ps_5_0.hlsl b/Shaders/Borderlands 2 and The Pre-Sequel/Tonemap_0xFCFE623E.ps_5_0.hlsl index 1124d60f..12a2b718 100644 --- a/Shaders/Borderlands 2 and The Pre-Sequel/Tonemap_0xFCFE623E.ps_5_0.hlsl +++ b/Shaders/Borderlands 2 and The Pre-Sequel/Tonemap_0xFCFE623E.ps_5_0.hlsl @@ -3,11 +3,10 @@ // (tps_tonemap_0xF8997849) inserts a LightShaftTexture at sampler slot 1, which shifts bloom/vignette/LUT/DOF // DOWN one slot vs BL2 (the DX9 BL2 tonemap_0x54ED86A0 has LUT@s3/DOF@s4; TPS has lightshaft@s1, LUT@s4, // DOF@s5). dgVoodoo maps DX9 sN 1:1 onto DX11 tN, so the same shift lands in the DX11 register space Luma -// sees. Reusing the BL2 bindings verbatim made the grade sample the Vignette texture as the LUT (grayscale / -// black-and-white) and the LUT as the DOF buffer (broken DoF) — hence the dedicated slot map below. +// sees, so a dedicated slot map (below) rebinds each named texture to its shifted register. // // The grade MATH is identical between the two games, so we set the slot-map + light-shaft macros here and -// pull in the shared BL2 body (one source of truth; matched/replaced by the 0x in this filename). +// pull in the shared grade impl (Luma_BL2TPS_Tonemap.hlsl, one source of truth), then add our own main(). // Injected Luma bloom moves to t8 because TPS's native DOF occupies t5; the Gaussian DoF prefilter (t7) // stays put — free on TPS. The mod binds Luma bloom at t8 when it detects this tonemap hash. #define TM_HAS_LIGHTSHAFT 1 @@ -21,4 +20,23 @@ #define TM_S_VIGNETTE s3 #define TM_S_LUT s4 #define TM_S_DOF s5 -#include "Tonemap_0xD00AA2A7.ps_5_0.hlsl" +#include "Luma_BL2TPS_Tonemap.hlsl" + +void main( + float4 v0 : SV_POSITION0, + float4 v1 : TEXCOORD8, + float4 v2 : COLOR0, + float4 v3 : COLOR1, + float4 v4 : TEXCOORD9, + float4 v5 : TEXCOORD0, + float4 v6 : TEXCOORD1, + float4 v7 : TEXCOORD2, + float4 v8 : TEXCOORD3, + float4 v9 : TEXCOORD4, + float4 v10 : TEXCOORD5, + float4 v11 : TEXCOORD6, + float4 v12 : TEXCOORD7, + out float4 o0 : SV_TARGET0) +{ + o0 = RunTonemap(v5, v6); +} diff --git a/Shaders/Borderlands 2 and The Pre-Sequel/UIEmissive_0xAE61FB46.ps_5_0.hlsl b/Shaders/Borderlands 2 and The Pre-Sequel/UIEmissive_0xAE61FB46.ps_5_0.hlsl new file mode 100644 index 00000000..e203c4d2 --- /dev/null +++ b/Shaders/Borderlands 2 and The Pre-Sequel/UIEmissive_0xAE61FB46.ps_5_0.hlsl @@ -0,0 +1,4 @@ +// Borderlands 2 — emissive Scaleform HUD sprite (autosave/Vault icon), dgVoodoo 2.81.3 -> ps_4_0 (hash 0xAE61FB46). +// Same shader as the 2.87.3 variant (0xC84956AA) with an identical signature; blind-include its body (adds the +// saturate that stops the icon blowing to peak nits in HDR). +#include "UIEmissive_0xC84956AA.ps_5_0.hlsl" diff --git a/Shaders/Borderlands 2 and The Pre-Sequel/UIEmissive_0xC84956AA.ps_5_0.hlsl b/Shaders/Borderlands 2 and The Pre-Sequel/UIEmissive_0xC84956AA.ps_5_0.hlsl index e644a23e..c3bd821c 100644 --- a/Shaders/Borderlands 2 and The Pre-Sequel/UIEmissive_0xC84956AA.ps_5_0.hlsl +++ b/Shaders/Borderlands 2 and The Pre-Sequel/UIEmissive_0xC84956AA.ps_5_0.hlsl @@ -7,7 +7,7 @@ // unclamped value past peak -> the icon blows to ~10000 nits, ignoring the user's Peak setting. // // Fix: re-add the vanilla 8-bit clamp dgVoodoo dropped (saturate the output) so the icon lands at UI paper white -// like the rest of the HUD. Body transcribed VERBATIM from the live dgVoodoo->ps_5_0 disasm of 0xC84956AA; the +// like the rest of the HUD. Body transcribed verbatim from the dgVoodoo->ps_5_0 disasm of 0xC84956AA; the // cb3 and/or pairs are dgVoodoo's texture-format bit emulation (mask+set), kept exactly via asint. Texture2D t0 : register(t0); diff --git a/Shaders/Borderlands 2 and The Pre-Sequel/Video_0x7EBF990A.ps_5_0.hlsl b/Shaders/Borderlands 2 and The Pre-Sequel/Video_0x7EBF990A.ps_5_0.hlsl new file mode 100644 index 00000000..4f23d250 --- /dev/null +++ b/Shaders/Borderlands 2 and The Pre-Sequel/Video_0x7EBF990A.ps_5_0.hlsl @@ -0,0 +1,3 @@ +// Borderlands 2 — Bink video (YUV->RGB) pass, dgVoodoo 2.81.3 -> ps_4_0 (hash 0x7EBF990A). +// Same shader as the 2.87.3 variant (0xE41621CF) with an identical signature; blind-include its body. +#include "Video_0xE41621CF.ps_5_0.hlsl" diff --git a/Shaders/Borderlands 2 and The Pre-Sequel/Video_0xE41621CF.ps_5_0.hlsl b/Shaders/Borderlands 2 and The Pre-Sequel/Video_0xE41621CF.ps_5_0.hlsl index 42120a27..a6f838fa 100644 --- a/Shaders/Borderlands 2 and The Pre-Sequel/Video_0xE41621CF.ps_5_0.hlsl +++ b/Shaders/Borderlands 2 and The Pre-Sequel/Video_0xE41621CF.ps_5_0.hlsl @@ -5,13 +5,13 @@ // (0xD00AA2A7). On Luma's fp16 scRGB swapchain that SDR output would sit flat at paper white. So: restore the // vanilla clamp, then apply a LIGHT PumboAutoHDR so movies gain a little highlight pop in HDR. // -// Body transcribed VERBATIM from the live dgVoodoo->ps_5_0 disasm of 0xE41621CF (maps the DX9 video shader +// Body transcribed verbatim from the dgVoodoo->ps_5_0 disasm of 0xE41621CF (maps the DX9 video shader // 0x33244F80: tor/tog/tob/consts c0..c3 -> cb4[8..11], same cb4[N+8] mapping as the tonemap). The cb3 and/or // pairs are dgVoodoo's texture-format bit emulation (mask+set), kept exactly via asuint/asfloat. // NOTE: dgVoodoo dropped the SM3 `saturate(o)` (the vanilla 8-bit UNORM backbuffer clamped for free); the fp16 // swapchain does not, so we re-add it before the AutoHDR (kills YUV overshoot + negatives). -#include "../Includes/Common.hlsl" +#include "Includes/Common.hlsl" // game-local: pulls GameCBuffers (LumaGameSettings VideoAutoHDR* fields) + shared Common // Light AutoHDR on videos (0 = off -> flat SDR at paper white). Peak kept low on purpose (Bink is low-bitrate; // pushing peak amplifies block/compression artifacts in highlights). PumboAutoHDR self-noops in SDR (peak==paper). @@ -78,7 +78,12 @@ void main( o0.rgb = saturate(o0.rgb); float3 lin = gamma_to_linear(o0.rgb); #if ENABLE_VIDEO_AUTO_HDR - lin = PumboAutoHDR(lin, VIDEO_AUTO_HDR_PEAK_NITS, LumaSettings.GamePaperWhiteNits); + if (LumaSettings.GameSettings.VideoAutoHDREnable > 0.5) + { + // boost 0 = peak at paper white -> PumboAutoHDR no-ops (off); 1 = full VIDEO_AUTO_HDR_PEAK_NITS. + const float peakNits = lerp(sRGB_WhiteLevelNits, VIDEO_AUTO_HDR_PEAK_NITS, saturate(LumaSettings.GameSettings.VideoAutoHDRBoost)); + lin = PumboAutoHDR(lin, peakNits, LumaSettings.GamePaperWhiteNits); + } #endif #if UI_DRAW_TYPE >= 2 // Match the tonemap's linear pre-scale: land movies at the same brightness as in-game after the 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 1069015b..f3581923 100644 --- a/Source/Games/Borderlands 2 and The Pre-Sequel/main.cpp +++ b/Source/Games/Borderlands 2 and The Pre-Sequel/main.cpp @@ -4,7 +4,7 @@ // (re-dumped via devkit). Launch the game exe DIRECTLY: the XNA Launcher.exe also loads d3d9 and would capture // ReShade instead of the game. One shared addon serves both games (tonemap hash = discriminator). // -// Pipeline (devkit-verified, see NOTES.md): +// Pipeline: // - TONEMAP PS 0xD00AA2A7 (BL2) / 0xFCFE623E (TPS): scene fp16 + bloom + vignette + LUT + DOF -> 8-bit LDR. // Replaced to recover HDR (DICE + paper-white); UI composites AFTER, on the LDR. // - FXAA PS 0x0D3001F6 (only when in-game AA on) -> replaced with SMAA ULTRA + optional RCAS. @@ -33,6 +33,13 @@ static constexpr uint32_t kFXAAResolveHash = 0x0D3001F6; static constexpr uint32_t kTonemapHash = 0xD00AA2A7; // BL2: writes the LDR buffer the HUD then draws onto static constexpr uint32_t kTonemapHashTPS = 0xFCFE623E; // The Pre-Sequel: same engine, different tonemap CSO (one addon serves both) +// dgVoodoo 2.81.3 translates DX9 SM3 to ps_4_0 (2.87.3 -> ps_5_0), so the SAME shaders get DIFFERENT CSO hashes. +// The shader bodies are identical (thin wrappers over the shared impl); we just match the alt hashes too via the +// Is* helpers below. FXAA/video/icon are byte-shared between BL2 and TPS -> one 2.81.3 hash each covers both games. +static constexpr uint32_t kFXAAResolveHash_v281 = 0xDF7DB98D; // BL2/TPS FXAA under dgVoodoo 2.81.3 +static constexpr uint32_t kTonemapHash_v281 = 0xF14F8664; // BL2 tonemap under dgVoodoo 2.81.3 +static constexpr uint32_t kTonemapHashTPS_v281 = 0x2079F1E8; // The Pre-Sequel tonemap under dgVoodoo 2.81.3 + // Luma-injected SRV slots on the tonemap. These MUST match the shader register macros in // Tonemap_0xD00AA2A7.ps_5_0.hlsl (BL2) + Tonemap_0xFCFE623E.ps_5_0.hlsl (TPS) — there is no compile-time link, // so keep them in sync if a slot is ever re-RE'd: @@ -42,16 +49,23 @@ static constexpr uint32_t kLumaBloomSlotBL2 = 5; static constexpr uint32_t kLumaBloomSlotTPS = 8; static constexpr uint32_t kLumaDoFSlot = 7; +// Scaleform item-card price shaders: mask-fill + digit-glyph PS hashes (a pair per dgVoodoo build). +static constexpr uint32_t kScaleformMaskFillHash2813 = 0x9F8EA541; +static constexpr uint32_t kScaleformDigitGlyphHash2813 = 0x63898919; +static constexpr uint32_t kScaleformMaskFillHash2873 = 0x616BEBBD; +static constexpr uint32_t kScaleformDigitGlyphHash2873 = 0x79CDF7BA; + // User settings (persisted via ReShade config under the project name; loaded in LoadConfigs). static bool g_smaa_enable = true; static float g_rcas_sharpness = 0.f; // RCAS sharpen on SMAA output (0 = off) static bool g_hide_ui = false; // hide the game's HUD (for clean screenshots) static bool g_smaa_predication = true; // SMAA depth predication (depth from scene-color .a) -static float g_smaa_pred_k = 1000.f; // predication depth compress (world units): D=z/(z+k); k=1000 measured optimum (~100% far-silhouette recall, ~0% flat false-fire @4K; plateau past 1000) +static float g_smaa_pred_k = 1000.f; // predication depth compress (world units): D=z/(z+k); k=1000 (far-silhouette recall plateaus past this) static bool g_luma_bloom_enable = true; // replace the game's clamped bloom with Luma HDR pyramidal bloom (live toggle) +static bool g_video_auto_hdr_enable = true; // light AutoHDR on Bink videos, HDR only (live toggle) static int g_bloom_nmips = 6; // bloom pyramid mip count static float g_bloom_sigmas[6] = {1.5f, 2.0f, 2.0f, 2.0f, 1.0f, 1.0f}; // per-mip Gaussian sigma (tapered, wider middle for a soft natural halo) -static int g_dof_type = 2; // DoF path (live): 0 = vanilla game DoF, 2 = Luma separable Gaussian (default, UI checkbox) +static int g_dof_type = 0; // DoF path (live): 0 = vanilla game DoF (default), 1 = Luma separable Gaussian (UI checkbox) static float g_dof_radius = 9.f; // DoF strength (full-res px @ 4K Gaussian blur extent). Default, users tune struct Borderlands2GameDeviceData final : public GameDeviceData @@ -116,6 +130,21 @@ struct Borderlands2GameDeviceData final : public GameDeviceData ComPtr uav_dof_blur_h, uav_dof_blur_out; ComPtr cb_dof_blur_h, cb_dof_blur_v; // cb_dof_blur (b0): axis-step + sigma, per pass float dof_blur_radius = -1.f; // recreate the CBs when the DoF Radius slider changes + + // Scaleform price-digit stencil repair: armed between a mask-submit and the glyph strips; the mask is + // duplicated into a private scratch D24S8 (cached per RT size) that the strips then test EQUAL/ref=1 against. + bool scaleform_mask_armed = false; + ComPtr dss_scaleform_mask_write; + ComPtr dss_scaleform_mask_test; + ComPtr dsv_scaleform_mask_active; + struct ScaleformMaskDS + { + uint32_t width = 0, height = 0; + ComPtr tex; + ComPtr dsv; + }; + ScaleformMaskDS scaleform_mask_ds_cache[4]; + uint32_t scaleform_mask_ds_next = 0; }; class Borderlands2 final : public Game @@ -125,6 +154,24 @@ class Borderlands2 final : public Game return *static_cast(device_data.game); } + // Pass identity by shader hash, folding every supported dgVoodoo version (2.87.3 ps_5_0 + 2.81.3 ps_4_0). + static bool IsBL2Tonemap(const ShaderHashesList& hashes) + { + return hashes.Contains(kTonemapHash, reshade::api::shader_stage::pixel) || hashes.Contains(kTonemapHash_v281, reshade::api::shader_stage::pixel); + } + static bool IsTPSTonemap(const ShaderHashesList& hashes) + { + return hashes.Contains(kTonemapHashTPS, reshade::api::shader_stage::pixel) || hashes.Contains(kTonemapHashTPS_v281, reshade::api::shader_stage::pixel); + } + static bool IsAnyTonemap(const ShaderHashesList& hashes) + { + return IsBL2Tonemap(hashes) || IsTPSTonemap(hashes); + } + static bool IsFXAA(const ShaderHashesList& hashes) + { + return hashes.Contains(kFXAAResolveHash, reshade::api::shader_stage::pixel) || hashes.Contains(kFXAAResolveHash_v281, reshade::api::shader_stage::pixel); + } + static bool CreateImmutableCB(ID3D11Device* device, const void* data, UINT size, ComPtr& out) { out.reset(); @@ -156,7 +203,7 @@ class Borderlands2 final : public Game #if ENABLE_SMAA // Post-tonemap SMAA on the LDR (gamma space). Runs AFTER the tonemap so it can't perturb the DoF (composited - // inside the tonemap; vanilla == SMAA-off, devkit-verified). Snapshot LDR -> SRV (already gamma; fed to both + // inside the tonemap; vanilla == SMAA-off). Snapshot LDR -> SRV (already gamma; fed to both // DrawSMAA color args, no color-prep) -> DrawSMAA -> optional RCAS -> copy back into the LDR. No-op if not ready. void RunPostTonemapSMAA(ID3D11Device* native_device, ID3D11DeviceContext* native_device_context, DeviceData& device_data, Borderlands2GameDeviceData& gd, ID3D11Resource* ldr_res) { @@ -395,22 +442,24 @@ class Borderlands2 final : public Game native_shaders_definitions.emplace(CompileTimeStringHash("BL2TPS DoF Gaussian Blur CS"), ShaderDefinition{"Luma_BL2TPS_DoFGaussian", reshade::api::pipeline_subobject_type::compute_shader, nullptr, "dof_blur_cs"}); - // The game's post passes use cb0..cb5; b12/b13 are free for Luma (measured via devkit). + // The game's post passes use cb0..cb5; b12/b13 are free for Luma. luma_settings_cbuffer_index = 13; luma_data_cbuffer_index = 12; // User HDR grade controls (read in Tonemap_0xD00AA2A7.ps_5_0.hlsl via LumaSettings.GameSettings). All // default to a vanilla no-op. Exposure/Bloom/Vignette act on both SDR+HDR; Saturation/Dechroma/Contrast HDR-only. - default_luma_global_game_settings.Exposure = 1.f; // scene multiplier (1x) - default_luma_global_game_settings.Saturation = 1.f; // Oklab saturation - default_luma_global_game_settings.HighlightDechroma = 0.f; // off; only mandatory DICE/gamut desat applies - default_luma_global_game_settings.BloomIntensity = 1.f; // Luma HDR bloom strength (additive) - default_luma_global_game_settings.Contrast = 1.f; // slope around 18% mid-gray - default_luma_global_game_settings.VignetteIntensity = 1.f; // game vignette darkening scale - default_luma_global_game_settings.LumaBloomEnable = 1.f; // 1 = Luma HDR pyramidal bloom, 0 = vanilla game bloom - default_luma_global_game_settings.DOFRadius = 9.f; // DoF strength (px @ 4K); = vanilla DoF peak (sigma ~1.98 half-res px) - default_luma_global_game_settings.DOFType = 2.f; // 0 = vanilla game DoF, 2 = Luma separable Gaussian (default) - default_luma_global_game_settings.Dithering = 1.f; // animated triangular dither at output (HDR), anti-banding on + default_luma_global_game_settings.Exposure = 1.f; // scene multiplier (1x) + default_luma_global_game_settings.Saturation = 1.f; // Oklab saturation + default_luma_global_game_settings.HighlightDechroma = 0.f; // off; only mandatory DICE/gamut desat applies + default_luma_global_game_settings.BloomIntensity = 1.f; // Luma HDR bloom strength (additive) + default_luma_global_game_settings.Contrast = 1.f; // slope around 18% mid-gray + default_luma_global_game_settings.VignetteIntensity = 1.f; // game vignette darkening scale + default_luma_global_game_settings.LumaBloomEnable = 1.f; // 1 = Luma HDR pyramidal bloom, 0 = vanilla game bloom + default_luma_global_game_settings.DOFRadius = 9.f; // DoF strength (px @ 4K); = vanilla DoF peak (sigma ~1.98 half-res px) + default_luma_global_game_settings.DOFType = 0.f; // 0 = vanilla game DoF (default), 1 = Luma separable Gaussian + default_luma_global_game_settings.Dithering = 1.f; // animated triangular dither at output (HDR), anti-banding on + default_luma_global_game_settings.VideoAutoHDREnable = 1.f; // light AutoHDR on Bink videos (HDR only) + default_luma_global_game_settings.VideoAutoHDRBoost = 0.5f; // highlight-expansion strength (peak ~165 nits at 0.5) cb_luma_global_settings.GameSettings = default_luma_global_game_settings; } @@ -434,6 +483,14 @@ class Borderlands2 final : public Game gd.tex_rcas_out.reset(); gd.tex_rcas_out_rtv.reset(); gd.srv_luma_bloom.reset(); + gd.dss_scaleform_mask_write.reset(); + gd.dss_scaleform_mask_test.reset(); + gd.dsv_scaleform_mask_active.reset(); + for (auto& slot : gd.scaleform_mask_ds_cache) + { + slot.dsv.reset(); + slot.tex.reset(); + } } delete device_data.game; device_data.game = nullptr; @@ -564,7 +621,7 @@ class Borderlands2 final : public Game // 3. (Re)create the per-axis CBs (axis-step + sigma) on size or radius change. if (size_changed || !gd.cb_dof_blur_h || !gd.cb_dof_blur_v || gd.dof_blur_radius != g_dof_radius) { - const float sigma = g_dof_radius * 0.22f; // half-res sigma; radius 9 = vanilla DoF peak (sigma ~1.98 half-res px @ 4K, R2 0.98) + const float sigma = g_dof_radius * 0.22f; // half-res sigma; radius 9 = vanilla DoF peak const float ph[4] = {1.0f / (float)hw, 0.f, sigma, 0.f}; const float pv[4] = {0.f, 1.0f / (float)hh, sigma, 0.f}; gd.cb_dof_blur_h.reset(); @@ -615,19 +672,212 @@ class Borderlands2 final : public Game blur_state.Restore(ctx); } + // Re-applies the stencil test dgVoodoo drops on the item card's rolling price digits. Returns true iff it ran + // the original draw itself (caller returns Replaced). + bool RepairScaleformStencilMask(ID3D11Device* native_device, ID3D11DeviceContext* native_device_context, Borderlands2GameDeviceData& gd, const ShaderHashesList& original_shader_hashes, bool is_custom_pass, bool is_immediate, std::function* original_draw_dispatch_func) + { + if (!is_immediate || is_custom_pass) + return false; + const bool is_mask_shader = original_shader_hashes.Contains(kScaleformMaskFillHash2813, reshade::api::shader_stage::pixel) || original_shader_hashes.Contains(kScaleformMaskFillHash2873, reshade::api::shader_stage::pixel); + if (!gd.scaleform_mask_armed && !is_mask_shader) + return false; + + // A mask submit is the mask PS drawn with color writes off + a stencil-writing state. + bool mask_submit = false; + if (is_mask_shader) + { + ComPtr blend_state; + FLOAT blend_factor[4]; + UINT sample_mask = 0; + native_device_context->OMGetBlendState(blend_state.put(), blend_factor, &sample_mask); + if (blend_state) + { + D3D11_BLEND_DESC bd; + blend_state->GetDesc(&bd); + if (bd.RenderTarget[0].RenderTargetWriteMask == 0) + { + ComPtr ds_state; + UINT stencil_ref = 0; + native_device_context->OMGetDepthStencilState(ds_state.put(), &stencil_ref); + if (ds_state) + { + D3D11_DEPTH_STENCIL_DESC dsd; + ds_state->GetDesc(&dsd); + mask_submit = dsd.StencilEnable != FALSE; + } + } + } + } + + if (mask_submit) + { + // Duplicate the mask into the scratch stencil (REPLACE/1); arm only when the duplicate actually lands. + gd.scaleform_mask_armed = false; + ComPtr rtv; + ComPtr prev_dsv; + native_device_context->OMGetRenderTargets(1, rtv.put(), prev_dsv.put()); + if (rtv && original_draw_dispatch_func != nullptr) + { + ComPtr rt_res; + rtv->GetResource(rt_res.put()); + ComPtr rt_tex; + if (rt_res && SUCCEEDED(rt_res->QueryInterface(IID_PPV_ARGS(rt_tex.put())))) + { + D3D11_TEXTURE2D_DESC rt_desc; + rt_tex->GetDesc(&rt_desc); + Borderlands2GameDeviceData::ScaleformMaskDS* scratch = nullptr; + for (auto& slot : gd.scaleform_mask_ds_cache) + { + if (slot.dsv && slot.width == rt_desc.Width && slot.height == rt_desc.Height) + { + scratch = &slot; + break; + } + } + if (!scratch) + { + for (auto& slot : gd.scaleform_mask_ds_cache) + { + if (!slot.dsv) + { + scratch = &slot; + break; + } + } + if (!scratch) + scratch = &gd.scaleform_mask_ds_cache[gd.scaleform_mask_ds_next++ % std::size(gd.scaleform_mask_ds_cache)]; + scratch->dsv.reset(); + scratch->tex.reset(); + D3D11_TEXTURE2D_DESC ds_desc = {}; + ds_desc.Width = rt_desc.Width; + ds_desc.Height = rt_desc.Height; + ds_desc.MipLevels = 1; + ds_desc.ArraySize = 1; + ds_desc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; + ds_desc.SampleDesc = rt_desc.SampleDesc; + ds_desc.Usage = D3D11_USAGE_DEFAULT; + ds_desc.BindFlags = D3D11_BIND_DEPTH_STENCIL; + if (SUCCEEDED(native_device->CreateTexture2D(&ds_desc, nullptr, scratch->tex.put()))) + native_device->CreateDepthStencilView(scratch->tex.get(), nullptr, scratch->dsv.put()); + scratch->width = rt_desc.Width; + scratch->height = rt_desc.Height; + } + if (!gd.dss_scaleform_mask_write) + { + D3D11_DEPTH_STENCIL_DESC write_desc = {}; + write_desc.DepthEnable = FALSE; + write_desc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO; + write_desc.DepthFunc = D3D11_COMPARISON_ALWAYS; + write_desc.StencilEnable = TRUE; + write_desc.StencilReadMask = D3D11_DEFAULT_STENCIL_READ_MASK; + write_desc.StencilWriteMask = D3D11_DEFAULT_STENCIL_WRITE_MASK; + write_desc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; + write_desc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; + write_desc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE; + write_desc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS; + write_desc.BackFace = write_desc.FrontFace; + native_device->CreateDepthStencilState(&write_desc, gd.dss_scaleform_mask_write.put()); + } + if (scratch->dsv && gd.dss_scaleform_mask_write) + { + ComPtr prev_ds_state; + UINT prev_stencil_ref = 0; + native_device_context->OMGetDepthStencilState(prev_ds_state.put(), &prev_stencil_ref); + native_device_context->ClearDepthStencilView(scratch->dsv.get(), D3D11_CLEAR_STENCIL, 1.f, 0); + ID3D11RenderTargetView* rtv_raw = rtv.get(); + native_device_context->OMSetRenderTargets(1, &rtv_raw, scratch->dsv.get()); + native_device_context->OMSetDepthStencilState(gd.dss_scaleform_mask_write.get(), 1u); + (*original_draw_dispatch_func)(); + native_device_context->OMSetDepthStencilState(prev_ds_state.get(), prev_stencil_ref); + native_device_context->OMSetRenderTargets(1, &rtv_raw, prev_dsv.get()); + gd.dsv_scaleform_mask_active = scratch->dsv; + gd.scaleform_mask_armed = true; + } + } + } + return false; // the real mask draw still proceeds through the normal path + } + + if (gd.scaleform_mask_armed && (original_shader_hashes.Contains(kScaleformDigitGlyphHash2813, reshade::api::shader_stage::pixel) || original_shader_hashes.Contains(kScaleformDigitGlyphHash2873, reshade::api::shader_stage::pixel))) + { + // Only intervene on glyph draws with stencil ENABLED but the test neutered (ALWAYS func / zero read mask / + // no stencil plane) = the broken masked content; StencilEnable FALSE = genuinely unmasked glyphs (leave), + // an effective test = a correctly translated path (leave). + ComPtr prev_ds_state; + UINT prev_stencil_ref = 0; + native_device_context->OMGetDepthStencilState(prev_ds_state.put(), &prev_stencil_ref); + ComPtr rtv; + ComPtr prev_dsv; + native_device_context->OMGetRenderTargets(1, rtv.put(), prev_dsv.put()); + D3D11_DEPTH_STENCIL_DESC dsd = {}; + if (prev_ds_state) + prev_ds_state->GetDesc(&dsd); + const bool stencil_enabled = prev_ds_state && dsd.StencilEnable != FALSE; + bool effective_stencil_test = false; + if (stencil_enabled && prev_dsv) + { + D3D11_DEPTH_STENCIL_VIEW_DESC dsv_desc; + prev_dsv->GetDesc(&dsv_desc); + const bool has_stencil_plane = dsv_desc.Format == DXGI_FORMAT_D24_UNORM_S8_UINT || dsv_desc.Format == DXGI_FORMAT_D32_FLOAT_S8X24_UINT; + const bool any_func_tests = dsd.FrontFace.StencilFunc != D3D11_COMPARISON_ALWAYS || dsd.BackFace.StencilFunc != D3D11_COMPARISON_ALWAYS; + effective_stencil_test = dsd.StencilReadMask != 0 && has_stencil_plane && any_func_tests; + } + if (stencil_enabled && !effective_stencil_test && original_draw_dispatch_func != nullptr && gd.dsv_scaleform_mask_active && rtv) + { + if (!gd.dss_scaleform_mask_test) + { + // Test-only: pass exactly where the duplicated mask wrote 1, never write, depth off. + D3D11_DEPTH_STENCIL_DESC test_desc = {}; + test_desc.DepthEnable = FALSE; + test_desc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO; + test_desc.DepthFunc = D3D11_COMPARISON_ALWAYS; + test_desc.StencilEnable = TRUE; + test_desc.StencilReadMask = D3D11_DEFAULT_STENCIL_READ_MASK; + test_desc.StencilWriteMask = 0; + test_desc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; + test_desc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; + test_desc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP; + test_desc.FrontFace.StencilFunc = D3D11_COMPARISON_EQUAL; + test_desc.BackFace = test_desc.FrontFace; + native_device->CreateDepthStencilState(&test_desc, gd.dss_scaleform_mask_test.put()); + } + if (gd.dss_scaleform_mask_test) + { + ID3D11RenderTargetView* rtv_raw = rtv.get(); + native_device_context->OMSetRenderTargets(1, &rtv_raw, gd.dsv_scaleform_mask_active.get()); + native_device_context->OMSetDepthStencilState(gd.dss_scaleform_mask_test.get(), 1u); + (*original_draw_dispatch_func)(); + native_device_context->OMSetDepthStencilState(prev_ds_state.get(), prev_stencil_ref); + native_device_context->OMSetRenderTargets(1, &rtv_raw, prev_dsv.get()); + return true; + } + } + return false; + } + + // Any other draw ends the mask span. + gd.scaleform_mask_armed = false; + gd.dsv_scaleform_mask_active.reset(); + return false; + } + DrawOrDispatchOverrideType OnDrawOrDispatch(ID3D11Device* native_device, ID3D11DeviceContext* native_device_context, CommandListData& cmd_list_data, DeviceData& device_data, reshade::api::shader_stage stages, const ShaderHashesList& original_shader_hashes, bool is_custom_pass, bool& updated_cbuffers, std::function* original_draw_dispatch_func) override { auto& gd = GetGameDeviceData(device_data); const bool is_immediate = native_device_context->GetType() == D3D11_DEVICE_CONTEXT_IMMEDIATE; + // Scaleform item-card price-digit stencil repair: returns Replaced when it re-runs the draw itself. + if (RepairScaleformStencilMask(native_device, native_device_context, gd, original_shader_hashes, is_custom_pass, is_immediate, original_draw_dispatch_func)) + return DrawOrDispatchOverrideType::Replaced; + // Track the LDR buffer (the tonemap's render target). The HUD draws onto it afterwards; the Hide UI // toggle uses this to recognize (and drop) those HUD draws. - if (is_immediate && (original_shader_hashes.Contains(kTonemapHash, reshade::api::shader_stage::pixel) || original_shader_hashes.Contains(kTonemapHashTPS, reshade::api::shader_stage::pixel))) + if (is_immediate && IsAnyTonemap(original_shader_hashes)) { // The Pre-Sequel's tonemap inserts a LightShaftTexture at slot 1, shifting its native textures down one // (LUT@t4, DOF@t5) vs BL2. The replaced tonemap shader (Tonemap_0xFCFE623E) compensates in its register // map; the only C++ consequence is the injected Luma bloom must move off t5 (TPS's native DOF) to t8. - const bool is_tps = original_shader_hashes.Contains(kTonemapHashTPS, reshade::api::shader_stage::pixel); + const bool is_tps = IsTPSTonemap(original_shader_hashes); gd.tonemap_fired_this_frame = true; // Hide UI scopes its post-tonemap alpha-blend skip to this span ComPtr rtv; native_device_context->OMGetRenderTargets(1, rtv.put(), nullptr); @@ -679,7 +929,7 @@ class Borderlands2 final : public Game cb_luma_global_settings.GameSettings.DOFType = dof_type_f; device_data.cb_luma_global_settings_dirty = true; } - if (scene_srv && g_dof_type == 2) // Luma separable Gaussian + if (scene_srv && g_dof_type != 0) // Luma separable Gaussian (value-agnostic: any non-zero DOFType) { RunDoFGaussian(native_device, native_device_context, device_data, gd, scene_srv.get()); if (gd.srv_dof_blur_out) @@ -697,7 +947,7 @@ class Borderlands2 final : public Game { // Returning Replaced short-circuits core's per-pass SetLumaConstantBuffers (core.hpp), so upload the // (possibly dirty) grade/DOFType CB ourselves before running the tonemap draw - else it samples - // last-present's values (1-frame lag on grade sliders / DoF toggle). The standard fleet custom-pass idiom. + // last-present's values (1-frame lag on grade sliders / DoF toggle). SetLumaConstantBuffers(native_device_context, cmd_list_data, device_data, reshade::api::shader_stage::pixel, LumaConstantBufferType::LumaSettings); updated_cbuffers = true; (*original_draw_dispatch_func)(); @@ -718,7 +968,7 @@ class Borderlands2 final : public Game // buffer-independent: BL2 draws the HUD on the tonemap's LDR, but TPS routes it to a separate post-FXAA buffer. // `tonemap_fired_this_frame` scopes this to the current frame's post-tonemap span (so next frame's pre-tonemap // transparents survive). RT==LDR kept as a BL2 superset; the mod menu draws post-composition, stays visible. - if (g_hide_ui && is_immediate && !is_custom_pass && gd.tonemap_fired_this_frame && !original_shader_hashes.Contains(kTonemapHash, reshade::api::shader_stage::pixel) && !original_shader_hashes.Contains(kTonemapHashTPS, reshade::api::shader_stage::pixel) && !original_shader_hashes.Contains(kFXAAResolveHash, reshade::api::shader_stage::pixel)) + if (g_hide_ui && is_immediate && !is_custom_pass && gd.tonemap_fired_this_frame && !IsAnyTonemap(original_shader_hashes) && !IsFXAA(original_shader_hashes)) { ComPtr blend_state; FLOAT blend_factor[4]; @@ -756,6 +1006,9 @@ class Borderlands2 final : public Game { auto& gd = GetGameDeviceData(device_data); gd.tonemap_fired_this_frame = false; // new frame: re-arm Hide UI's post-tonemap alpha-blend scope + // Never carry a Scaleform mask span across frames. + gd.scaleform_mask_armed = false; + gd.dsv_scaleform_mask_active.reset(); } void LoadConfigs() override @@ -773,8 +1026,9 @@ class Borderlands2 final : public Game reshade::get_config_value(nullptr, PROJECT_NAME, "Contrast", gs.Contrast); reshade::get_config_value(nullptr, PROJECT_NAME, "VignetteIntensity", gs.VignetteIntensity); - g_dof_type = 2; // default = Luma Gaussian + g_dof_type = 0; // default = vanilla game DoF reshade::get_config_value(nullptr, PROJECT_NAME, "DOFType", g_dof_type); + g_dof_type = (g_dof_type != 0) ? 1 : 0; // normalize to the 0/1 binary (migrates a legacy saved 2 -> 1) reshade::get_config_value(nullptr, PROJECT_NAME, "DOFRadius", g_dof_radius); gs.DOFType = (float)g_dof_type; gs.DOFRadius = g_dof_radius; @@ -783,6 +1037,10 @@ class Borderlands2 final : public Game gs.LumaBloomEnable = g_luma_bloom_enable ? 1.f : 0.f; // mirror to the shader composite switch reshade::get_config_value(nullptr, PROJECT_NAME, "Dithering", gs.Dithering); + + reshade::get_config_value(nullptr, PROJECT_NAME, "VideoAutoHDREnable", g_video_auto_hdr_enable); + gs.VideoAutoHDREnable = g_video_auto_hdr_enable ? 1.f : 0.f; // mirror to the shader runtime gate + reshade::get_config_value(nullptr, PROJECT_NAME, "VideoAutoHDRBoost", gs.VideoAutoHDRBoost); } void DrawImGuiSettings(DeviceData& device_data) override @@ -798,7 +1056,8 @@ class Borderlands2 final : public Game reshade::set_config_value(nullptr, PROJECT_NAME, "RCASSharpness", g_rcas_sharpness); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("RCAS sharpening applied to the SMAA output (0 = off)."); - DrawResetButton(g_rcas_sharpness, 0.f, "RCASSharpness"); + if (DrawResetButton(g_rcas_sharpness, 0.f, "RCASSharpness")) // : core's default write goes to [Luma], which LoadConfigs never reads + reshade::set_config_value(nullptr, PROJECT_NAME, "RCASSharpness", g_rcas_sharpness); ImGui::EndDisabled(); // --- HDR grade (read in Tonemap_0xD00AA2A7.ps_5_0.hlsl via LumaSettings.GameSettings). All vanilla by default. @@ -813,8 +1072,11 @@ class Borderlands2 final : public Game reshade::set_config_value(nullptr, PROJECT_NAME, "Exposure", gs.Exposure); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Scene-referred exposure multiplier (1 = vanilla)."); - if (DrawResetButton(gs.Exposure, gd_def.Exposure, "Exposure")) + if (DrawResetButton(gs.Exposure, gd_def.Exposure, "Exposure")) + { device_data.cb_luma_global_settings_dirty = true; + reshade::set_config_value(nullptr, PROJECT_NAME, "Exposure", gs.Exposure); + } if (ImGui::SliderFloat("Contrast", &gs.Contrast, 0.f, 2.f)) device_data.cb_luma_global_settings_dirty = true; @@ -822,8 +1084,11 @@ class Borderlands2 final : public Game reshade::set_config_value(nullptr, PROJECT_NAME, "Contrast", gs.Contrast); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Slope contrast around 18% mid-gray, HDR only (1 = vanilla)."); - if (DrawResetButton(gs.Contrast, gd_def.Contrast, "Contrast")) + if (DrawResetButton(gs.Contrast, gd_def.Contrast, "Contrast")) + { device_data.cb_luma_global_settings_dirty = true; + reshade::set_config_value(nullptr, PROJECT_NAME, "Contrast", gs.Contrast); + } if (ImGui::SliderFloat("Saturation", &gs.Saturation, 0.f, 2.f)) device_data.cb_luma_global_settings_dirty = true; @@ -831,8 +1096,11 @@ class Borderlands2 final : public Game reshade::set_config_value(nullptr, PROJECT_NAME, "Saturation", gs.Saturation); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Color saturation (Oklab), HDR only (1 = vanilla)."); - if (DrawResetButton(gs.Saturation, gd_def.Saturation, "Saturation")) + if (DrawResetButton(gs.Saturation, gd_def.Saturation, "Saturation")) + { device_data.cb_luma_global_settings_dirty = true; + reshade::set_config_value(nullptr, PROJECT_NAME, "Saturation", gs.Saturation); + } if (ImGui::SliderFloat("Highlights Desaturation", &gs.HighlightDechroma, 0.f, 1.f)) device_data.cb_luma_global_settings_dirty = true; @@ -840,8 +1108,11 @@ class Borderlands2 final : public Game reshade::set_config_value(nullptr, PROJECT_NAME, "HighlightDechroma", gs.HighlightDechroma); if (ImGui::IsItemHovered()) ImGui::SetTooltip("How soon bright sources fade to neutral white, HDR only (0 = keep color at any brightness)."); - if (DrawResetButton(gs.HighlightDechroma, gd_def.HighlightDechroma, "HighlightDechroma")) + if (DrawResetButton(gs.HighlightDechroma, gd_def.HighlightDechroma, "HighlightDechroma")) + { device_data.cb_luma_global_settings_dirty = true; + reshade::set_config_value(nullptr, PROJECT_NAME, "HighlightDechroma", gs.HighlightDechroma); + } if (ImGui::Checkbox("Luma HDR Bloom", &g_luma_bloom_enable)) { @@ -858,8 +1129,11 @@ class Borderlands2 final : public Game reshade::set_config_value(nullptr, PROJECT_NAME, "BloomIntensity", gs.BloomIntensity); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Bloom strength (1 = vanilla). Scales the Luma HDR bloom when enabled, else the vanilla game bloom."); - if (DrawResetButton(gs.BloomIntensity, gd_def.BloomIntensity, "BloomIntensity")) + if (DrawResetButton(gs.BloomIntensity, gd_def.BloomIntensity, "BloomIntensity")) + { device_data.cb_luma_global_settings_dirty = true; + reshade::set_config_value(nullptr, PROJECT_NAME, "BloomIntensity", gs.BloomIntensity); + } if (ImGui::SliderFloat("Vignette Intensity", &gs.VignetteIntensity, 0.f, 1.f)) device_data.cb_luma_global_settings_dirty = true; @@ -867,14 +1141,17 @@ class Borderlands2 final : public Game reshade::set_config_value(nullptr, PROJECT_NAME, "VignetteIntensity", gs.VignetteIntensity); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Scales the game's vignette darkening (1 = vanilla, 0 = none)."); - if (DrawResetButton(gs.VignetteIntensity, gd_def.VignetteIntensity, "VignetteIntensity")) + if (DrawResetButton(gs.VignetteIntensity, gd_def.VignetteIntensity, "VignetteIntensity")) + { device_data.cb_luma_global_settings_dirty = true; + reshade::set_config_value(nullptr, PROJECT_NAME, "VignetteIntensity", gs.VignetteIntensity); + } - // DoF: one checkbox toggles the Luma separable HDR Gaussian (g_dof_type 2) vs the vanilla game DoF (0). + // DoF: one checkbox toggles the Luma separable HDR Gaussian (g_dof_type 1) vs the vanilla game DoF (0). bool dof_on = (g_dof_type != 0); if (ImGui::Checkbox("Luma Gaussian DoF", &dof_on)) { - g_dof_type = dof_on ? 2 : 0; + g_dof_type = dof_on ? 1 : 0; gs.DOFType = (float)g_dof_type; device_data.cb_luma_global_settings_dirty = true; reshade::set_config_value(nullptr, PROJECT_NAME, "DOFType", g_dof_type); @@ -892,10 +1169,11 @@ class Borderlands2 final : public Game reshade::set_config_value(nullptr, PROJECT_NAME, "DOFRadius", g_dof_radius); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Depth of Field blur strength."); - if (DrawResetButton(g_dof_radius, 9.f, "DOFRadius")) + if (DrawResetButton(g_dof_radius, 9.f, "DOFRadius")) { gs.DOFRadius = g_dof_radius; device_data.cb_luma_global_settings_dirty = true; + reshade::set_config_value(nullptr, PROJECT_NAME, "DOFRadius", g_dof_radius); } ImGui::EndDisabled(); @@ -909,6 +1187,29 @@ class Borderlands2 final : public Game if (ImGui::IsItemHovered()) ImGui::SetTooltip("Reduces gradient banding."); + if (ImGui::Checkbox("Video AutoHDR", &g_video_auto_hdr_enable)) + { + gs.VideoAutoHDREnable = g_video_auto_hdr_enable ? 1.f : 0.f; + device_data.cb_luma_global_settings_dirty = true; + reshade::set_config_value(nullptr, PROJECT_NAME, "VideoAutoHDREnable", g_video_auto_hdr_enable); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Adds HDR highlights to pre-rendered videos."); + + ImGui::BeginDisabled(!g_video_auto_hdr_enable); + if (ImGui::SliderFloat("Video HDR Boost", &gs.VideoAutoHDRBoost, 0.f, 1.f)) + device_data.cb_luma_global_settings_dirty = true; + if (ImGui::IsItemDeactivatedAfterEdit()) + reshade::set_config_value(nullptr, PROJECT_NAME, "VideoAutoHDRBoost", gs.VideoAutoHDRBoost); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("Highlight expansion strength."); + if (DrawResetButton(gs.VideoAutoHDRBoost, gd_def.VideoAutoHDRBoost, "VideoAutoHDRBoost")) + { + device_data.cb_luma_global_settings_dirty = true; + reshade::set_config_value(nullptr, PROJECT_NAME, "VideoAutoHDRBoost", gs.VideoAutoHDRBoost); + } + ImGui::EndDisabled(); + ImGui::SeparatorText("UI"); if (ImGui::Checkbox("Hide Gameplay UI", &g_hide_ui)) reshade::set_config_value(nullptr, PROJECT_NAME, "HideUI", g_hide_ui);