From 1769cc72bc214c9271b22fb60cb0e813ab7a77f6 Mon Sep 17 00:00:00 2001 From: Lilium Date: Thu, 21 Sep 2023 17:18:15 +0200 Subject: [PATCH 001/270] add orignal untouched shaders From da201ab8ce3b717d2a31e4fb8c2bac970f243465 Mon Sep 17 00:00:00 2001 From: Lilium Date: Thu, 21 Sep 2023 18:05:28 +0200 Subject: [PATCH 002/270] small cleanup and rename all colour to color --- shaders/HDRComposite/HDRComposite_ps.hlsl | 358 +++++++++++----------- 1 file changed, 179 insertions(+), 179 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index f7a56683..50946d12 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -141,17 +141,17 @@ struct FrameData { int u1; int u2; float2 u3; float u4; float u5; float u6; floa struct HDRCompositeData { - float4 HighlightsColourFilter; - float4 ColourFilter; - float HableSaturation; - float BrightnessMultiplier; - float ContrastIntensity; + float4 HighlightsColorFilter; + float4 ColorFilter; + float HableSaturation; + float BrightnessMultiplier; + float ContrastIntensity; int i_0; //unused }; cbuffer SharedFrameData : register(b0, space6) { - FrameData SharedFrameData; + FrameData SharedFrameData; } cbuffer PerSceneConstants : register(b0, space7) @@ -165,7 +165,7 @@ cbuffer PushConstantWrapper_HDRComposite : register(b0, space0) }; -Texture2D InputColour : register(t0, space9); +Texture2D InputColor : register(t0, space9); #if defined(APPLY_MERGED_COLOR_GRADING_LUT) @@ -216,13 +216,13 @@ struct PSOutput // exp2(x * 1.44269502162933349609375f) is the same as exp(x) -#define FLT10_MAX 64512.f -#define FLT11_MAX 65024.f -#define FLT16_MAX 65504.f +#define FLT10_MAX 64512.f +#define FLT11_MAX 65024.f +#define FLT16_MAX 65504.f -static float LUTStrenght = 0.f; -static float RestorePreLUTLuminance = 1.f; -static bool ForceACES = false; //TODO: delete once Hable works +static const float LUTStrength = 0.f; +static const float RestorePreLUTLuminance = 1.f; +static const bool ForceACES = false; //TODO: delete once Hable works float gamma_linear_to_sRGB(float channel) { @@ -278,64 +278,64 @@ static const float ACES_c = 2.43f; static const float ACES_d = 0.59f; static const float ACES_e = 0.14f; -float3 ACES(float3 Colour, bool Clamp, float modE, float modA) +float3 ACES(float3 Color, bool Clamp, float modE, float modA) { - Colour = (Colour * (modA * Colour + ACES_b)) / ((Colour * (ACES_c * Colour + ACES_d)) + modE); - return Clamp ? saturate(Colour) : Colour; + Color = (Color * (modA * Color + ACES_b)) / ((Color * (ACES_c * Color + ACES_d)) + modE); + return Clamp ? saturate(Color) : Color; } // ACESFilm by Krzysztof Narkowicz (https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/) // (color * ((a * color) + b)) / (color * ((c * color) + d) + e) -float3 ACESReference(float3 Colour, bool Clamp) +float3 ACESReference(float3 Color, bool Clamp) { - return ACES(Colour, Clamp, ACES_e, ACES_a); + return ACES(Color, Clamp, ACES_e, ACES_a); } // ACESFilm "per scene" -float3 ACESParametric(in float3 Colour, in bool Clamp, inout float modE, inout float modA) +float3 ACESParametric(in float3 Color, in bool Clamp, inout float modE, inout float modA) { const float AcesParam0 = PerSceneConstants[3266u].x; const float AcesParam1 = PerSceneConstants[3266u].y; modE = AcesParam1; modA = ((0.56f / AcesParam0) + ACES_b) + (AcesParam1 / (AcesParam0 * AcesParam0)); - return ACES(Colour, Clamp, modE, modA); + return ACES(Color, Clamp, modE, modA); } -float3 ACES_Inverse(float3 Colour, float modE, float modA) +float3 ACES_Inverse(float3 Color, float modE, float modA) { //TODO: does this apply for `ACESParametric` as well? Probably //TODO: figure out if we could still use the unclamped color. There should be a way to invert it. // ACES is not defined for any values beyond 0-1 - Colour = saturate(Colour); - - float3 fixed0 = (-ACES_d * Colour) + ACES_b; - float3 fixed1 = (ACES_c * Colour) - modA; + Color = saturate(Color); + + float3 fixed0 = (-ACES_d * Color) + ACES_b; + float3 fixed1 = (ACES_c * Color) - modA; float3 variable_numerator_part0 = -fixed0; - float3 variable_numerator = sqrt((variable_numerator_part0 * variable_numerator_part0) - (4.f * modE * Colour * fixed1)); + float3 variable_numerator = sqrt((variable_numerator_part0 * variable_numerator_part0) - (4.f * modE * Color * fixed1)); float3 denominator = 2.f * fixed1; float3 result0 = (fixed0 + variable_numerator) / denominator; float3 result1 = (fixed0 - variable_numerator) / denominator; - + // "result1" is likely what we always want return max(result0, result1); } -float3 ACESReference_Inverse(float3 Colour) +float3 ACESReference_Inverse(float3 Color) { - return ACES_Inverse(Colour, ACES_e, ACES_a); + return ACES_Inverse(Color, ACES_e, ACES_a); } -float3 ACESParametric_Inverse(float3 Colour, float modE, float modA) +float3 ACESParametric_Inverse(float3 Color, float modE, float modA) { - return ACES_Inverse(Colour, modE, modA); + return ACES_Inverse(Color, modE, modA); } // https://github.com/johnhable/fw-public float3 Hable( - in float3 InputColour, + in float3 InputColor, inout float params_y0, inout float params_y1, inout float dstParams_W, @@ -372,7 +372,7 @@ float3 Hable( float extraW = exp2(shoulderStrength) - LFLT1; float initialW = dstParams_x0 + remainingY; dstParams_W = initialW + extraW; - + // "W * " was optimised away by DXIL->SPIRV->HLSL conversion as down the line there is a "/ W" float dstParams_overshootX = 2.f * shoulderAngle * shoulderStrength; float dstParams_overshootY = 0.5f * shoulderAngle * shoulderStrength; @@ -395,7 +395,7 @@ float3 Hable( midSegment_offsetX = (b / m); //no minus midSegment_lnA = log2(m); - + // EvalDerivativeLinearGamma https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L81 // max(EPSILON, pow(params_yX, gamma)) params_y0 = max(EPSILON, dstParams_y0); // is pow(x, 1) because gamma = 1 @@ -407,14 +407,14 @@ float3 Hable( // -1 was optimised away as shoulderSegment_offsetY is params_overshootY + 1 float params_overshootY = 1.f + dstParams_overshootY; // is pow(x, 1) because gamma = 1 //OLD: float params_overshootY = exp2(log2(1.f + dstParams_overshootY)); - + // toe section // SolveAB https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L60 toeSegment_B = (m * params_x0) / (params_y0 + EPSILON); float _410 = log2(params_y0); //doesn't belong to SolveAB toeSegment_lnA = toeSegment_B * (-log(params_x0)); // toe section end - + // shoulder section float shoulderSection_x0 = 1.f + params_overshootX - params_x1; float shoulderSection_y0 = params_overshootY - params_y1; // 1 + x was optimised away @@ -427,7 +427,7 @@ float3 Hable( shoulderSegment_lnA = (log2(shoulderSection_y0) * RCP_LOG2_E) - (shoulderSegment_B * log2(shoulderSection_x0)); float shoulderSegment_B_optimised = shoulderSegment_B * RCP_LOG2_E; // shoulder section end - + // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L6 // Eval float evalY0 = 0.f; @@ -442,7 +442,7 @@ float3 Hable( for (uint channel = 0; channel < 3; channel++) { - float normX = InputColour[channel] * dstCurve_invW; + float normX = InputColor[channel] * dstCurve_invW; bool isToeSegment = normX < params_x0; float returnChannel = 0.f; @@ -497,73 +497,73 @@ float HableEvalInverse( // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L45-L52 float3 Hable_Inverse( - float3 InputColour, - float params_y0, - float params_y1, - float dstParams_W, - float toeSegment_lnA, - float toeSegment_B, - float midSegment_offsetX, - float midSegment_lnA, - float shoulderSegment_offsetX, - float shoulderSegment_offsetY, - float shoulderSegment_scaleX, - float shoulderSegment_lnA, - float shoulderSegment_B, - float invScale) + float3 InputColor, + float params_y0, + float params_y1, + float dstParams_W, + float toeSegment_lnA, + float toeSegment_B, + float midSegment_offsetX, + float midSegment_lnA, + float shoulderSegment_offsetX, + float shoulderSegment_offsetY, + float shoulderSegment_scaleX, + float shoulderSegment_lnA, + float shoulderSegment_B, + float invScale) { - float3 itmColour; + float3 itmColor; for (uint channel = 0; channel < 3; channel++) { - if (InputColour[channel] < params_y0) + if (InputColor[channel] < params_y0) { - itmColour[channel] = HableEvalInverse(InputColour[channel], - 0.f, - 0.f, // * invScale - 1.f, - 1.f * invScale, - toeSegment_lnA, - toeSegment_B); + itmColor[channel] = HableEvalInverse(InputColor[channel], + 0.f, + 0.f, // * invScale + 1.f, + 1.f * invScale, + toeSegment_lnA, + toeSegment_B); } - else if (InputColour[channel] < params_y1) + else if (InputColor[channel] < params_y1) { - itmColour[channel] = HableEvalInverse(InputColour[channel], - -midSegment_offsetX, // minus was optimised away - 0.f, // * invScale - 1.f, - 1.f * invScale, - midSegment_lnA, - 1.f); + itmColor[channel] = HableEvalInverse(InputColor[channel], + -midSegment_offsetX, // minus was optimised away + 0.f, // * invScale + 1.f, + 1.f * invScale, + midSegment_lnA, + 1.f); } else { - itmColour[channel] = HableEvalInverse(InputColour[channel], - shoulderSegment_offsetX, - shoulderSegment_offsetY * invScale, - shoulderSegment_scaleX, - -1.f * invScale, - shoulderSegment_lnA, - shoulderSegment_B); + itmColor[channel] = HableEvalInverse(InputColor[channel], + shoulderSegment_offsetX, + shoulderSegment_offsetY * invScale, + shoulderSegment_scaleX, + -1.f * invScale, + shoulderSegment_lnA, + shoulderSegment_B); } } - return itmColour * dstParams_W; + return itmColor * dstParams_W; } float FindLuminanceToRestore(float tonemapLostLuminance, float preColorCorrectionLuminance, float postColorCorrectionLuminance) { - // Try to restore any luminance above "color", as it would have been lost during tone mapping. - // - // To achieve this, we re-apply the lost luminance, but only by the inverse amount the luminance changed during the color correction. - // e.g. if the color correction fully changed the luminance, then we don't change it any further, - // but if the color correction changed the luminance only by 25%, then we re-apply apply 75% of the lost luminance on top. - // This will be "accurate" as long as the color correction scaled the luminance linearly in the output as it grew in the input, - // but even if it didn't, it should still look fine. - - // This will re-apply an amount of lost luminance based on how much the CC reduced it: - // the more the CC reduced the luminance, the less we will apply. - // If the CC increased the luminance though, we will apply even more than the base lost amount. + // Try to restore any luminance above "color", as it would have been lost during tone mapping. + // + // To achieve this, we re-apply the lost luminance, but only by the inverse amount the luminance changed during the color correction. + // e.g. if the color correction fully changed the luminance, then we don't change it any further, + // but if the color correction changed the luminance only by 25%, then we re-apply apply 75% of the lost luminance on top. + // This will be "accurate" as long as the color correction scaled the luminance linearly in the output as it grew in the input, + // but even if it didn't, it should still look fine. + + // This will re-apply an amount of lost luminance based on how much the CC reduced it: + // the more the CC reduced the luminance, the less we will apply. + // If the CC increased the luminance though, we will apply even more than the base lost amount. tonemapLostLuminance = isnan(tonemapLostLuminance) ? 0.f : tonemapLostLuminance; const float luminanceCCInvChange = max(1.f - (preColorCorrectionLuminance - postColorCorrectionLuminance), 0.f); //const float luminanceCCInvChange = clamp(postColorCorrectionLuminance / preColorCorrectionLuminance, 0.f, 2.f); //TODO: try this @@ -581,26 +581,26 @@ float FindLuminanceToRestore(float tonemapLostLuminance, float preColorCorrectio float3 PostProcess(float3 Color, inout float ColorLuminance) { const uint hdrCmpDatIndex = PcwHdrComposite.HdrCmpDatIndex; - const float4 highlightsColorFilter = HdrCmpDat[hdrCmpDatIndex].HighlightsColourFilter; - const float4 colorFilter = HdrCmpDat[hdrCmpDatIndex].ColourFilter; + const float4 highlightsColorFilter = HdrCmpDat[hdrCmpDatIndex].HighlightsColorFilter; + const float4 colorFilter = HdrCmpDat[hdrCmpDatIndex].ColorFilter; const float hableSaturation = HdrCmpDat[hdrCmpDatIndex].HableSaturation; const float brightnessMultiplier = HdrCmpDat[hdrCmpDatIndex].BrightnessMultiplier; // Neutral at 1 const float contrastIntensity = HdrCmpDat[hdrCmpDatIndex].ContrastIntensity; // Neutral at 1 const float contrastMidPoint = PerSceneConstants[316u].z; - + ColorLuminance = Luminance(Color); - + // saturation adjustment a la Hable // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicColorGrading.cpp#L307-L309 Color = ((Color - ColorLuminance) * hableSaturation) + ColorLuminance; // Blend in another color based on the luminance. Color += lerp(float3(0.f, 0.f, 0.f), ColorLuminance * highlightsColorFilter.rgb, highlightsColorFilter.a); Color *= brightnessMultiplier; - + // Contrast adjustment (shift the colors from 0<->1 to (e.g.) -0.5<->0.5 range, multiply and shift back). // The higher the distance from the contrast middle point, the more contrast will change the color. Color = ((Color - contrastMidPoint) * contrastIntensity) + contrastMidPoint; - + Color = lerp(Color, colorFilter.rgb, colorFilter.a); return Color; } @@ -608,34 +608,34 @@ float3 PostProcess(float3 Color, inout float ColorLuminance) float3 PostProcess_Inverse(float3 Color, float ColorLuminance) { const uint hdrCmpDatIndex = PcwHdrComposite.HdrCmpDatIndex; - const float4 highlightsColorFilter = HdrCmpDat[hdrCmpDatIndex].HighlightsColourFilter; - const float4 colorFilter = HdrCmpDat[hdrCmpDatIndex].ColourFilter; + const float4 highlightsColorFilter = HdrCmpDat[hdrCmpDatIndex].HighlightsColorFilter; + const float4 colorFilter = HdrCmpDat[hdrCmpDatIndex].ColorFilter; const float hableSaturation = HdrCmpDat[hdrCmpDatIndex].HableSaturation; const float brightnessMultiplier = HdrCmpDat[hdrCmpDatIndex].BrightnessMultiplier; // Neutral at 1 const float contrastIntensity = HdrCmpDat[hdrCmpDatIndex].ContrastIntensity; // Neutral at 1 const float contrastMidPoint = PerSceneConstants[316u].z; - + // We can't invert the color filter, so we will only inverse the post process by the unfiltered amount const float colorFilterInverse = 1.f - colorFilter.a; - + Color = ((Color - contrastMidPoint) / contrastIntensity) + contrastMidPoint; - + Color /= brightnessMultiplier; - + Color -= lerp(float3(0.f, 0.f, 0.f), ColorLuminance * highlightsColorFilter.rgb, highlightsColorFilter.a); - + Color = ((Color - ColorLuminance) / hableSaturation) + ColorLuminance; - + return Color; } PSOutput PS(PSInput psInput) -{ - float3 inputColour = InputColour.Load(int3(int2(psInput.SV_Position.xy), 0)); +{ + float3 inputColor = InputColor.Load(int3(int2(psInput.SV_Position.xy), 0)); #if CLAMP_INPUT_OUTPUT // Remove any negative value caused by using R16G16B16A16F buffers (originally this was R11G11B10F, which has no negative values). // Doing gamut mapping, or keeping the colors outside of BT.709 doesn't seem to be right, as they seem to be just be accidentally coming out of some shader math. - inputColour = max(inputColour, 0.f); + inputColor = max(inputColor, 0.f); #endif #if defined(APPLY_BLOOM) @@ -644,7 +644,7 @@ PSOutput PS(PSInput psInput) float bloomMultiplier = PcwHdrComposite.BloomMultiplier; - inputColour = (bloomMultiplier * bloom) + inputColour; + inputColor = (bloomMultiplier * bloom) + inputColor; #endif // APPLY_BLOOM @@ -652,10 +652,10 @@ PSOutput PS(PSInput psInput) float3 tonemappedUnclampedColor; #if DISABLE_TONEMAP - tonemappedColor = inputColour; - tonemappedUnclampedColor = inputColour; + tonemappedColor = inputColor; + tonemappedUnclampedColor = inputColor; #else - + float acesParam_modE; float acesParam_modA; @@ -676,38 +676,38 @@ PSOutput PS(PSInput psInput) const bool clampACES = false; if (PcwHdrComposite.Tmo == 1u || ForceACES) { - tonemappedUnclampedColor = ACESReference(inputColour, clampACES); + tonemappedUnclampedColor = ACESReference(inputColor, clampACES); tonemappedColor = saturate(tonemappedUnclampedColor); } else if (PcwHdrComposite.Tmo == 2u) { - tonemappedUnclampedColor = ACESParametric(inputColour, clampACES, acesParam_modE, acesParam_modA); + tonemappedUnclampedColor = ACESParametric(inputColor, clampACES, acesParam_modE, acesParam_modA); tonemappedColor = saturate(tonemappedUnclampedColor); } else if (PcwHdrComposite.Tmo == 3u) { - tonemappedUnclampedColor = Hable(inputColour, - hable_params_y0, - hable_params_y1, - hable_dstParams_W, - hable_toeSegment_lnA, - hable_toeSegment_B, - hable_midSegment_offsetX, - hable_midSegment_lnA, - hable_shoulderSegment_offsetX, - hable_shoulderSegment_offsetY, - hable_shoulderSegment_scaleX, - hable_shoulderSegment_lnA, - hable_shoulderSegment_B, - hable_invScale); + tonemappedUnclampedColor = Hable(inputColor, + hable_params_y0, + hable_params_y1, + hable_dstParams_W, + hable_toeSegment_lnA, + hable_toeSegment_B, + hable_midSegment_offsetX, + hable_midSegment_lnA, + hable_shoulderSegment_offsetX, + hable_shoulderSegment_offsetY, + hable_shoulderSegment_scaleX, + hable_shoulderSegment_lnA, + hable_shoulderSegment_B, + hable_invScale); tonemappedColor = tonemappedUnclampedColor; // Hable was never clamped } else { - tonemappedUnclampedColor = inputColour; + tonemappedUnclampedColor = inputColor; tonemappedColor = saturate(tonemappedUnclampedColor); } - + #if !DISABLE_POST_PROCESS float prePostProcessColorLuminance; float prePostProcessUnclampedColorLuminance; @@ -716,112 +716,112 @@ PSOutput PS(PSInput psInput) tonemappedUnclampedColor = PostProcess(tonemappedUnclampedColor, prePostProcessUnclampedColorLuminance); #endif #endif // DISABLE_TONEMAP - + tonemappedColor = saturate(tonemappedColor); - + // This is the luminance lost by clamping SDR tonemappers to the 0-1 range, and again by the fact that LUTs only take 0-1 range input. const float tonemapLostLuminance = Luminance(tonemappedUnclampedColor - tonemappedColor); //TODO: clip any negative values here? The unclamped tonemapper might actually have negative "lost" luminance. - + const float preColorCorrectionLuminance = Luminance(tonemappedColor); - - float3 colour = tonemappedColor; - + + float3 color = tonemappedColor; + #if defined(APPLY_MERGED_COLOR_GRADING_LUT) && !DISABLE_TONEMAP && !DISABLE_LUT #if FIX_WRONG_SRGB_GAMMA - float3 LUTEncodedColour = gamma_linear_to_sRGB(colour); + float3 LUTEncodedColor = gamma_linear_to_sRGB(color); #else //read gamma ini value, defaulting at 2.4 (makes little sense) float inverseGamma = 1.f / (max(SharedFrameData.Gamma, 0.001f)); //weird linear -> sRGB conversion that clips values just above 0, and raises blacks. - float3 LUTEncodedColour = (pow(colour, inverseGamma) * 1.055f) - 0.055f; - LUTEncodedColour = max(LUTEncodedColour, 0.f); // Unnecessary as the LUT sampling is already clamped. + float3 LUTEncodedColor = (pow(color, inverseGamma) * 1.055f) - 0.055f; + LUTEncodedColor = max(LUTEncodedColor, 0.f); // Unnecessary as the LUT sampling is already clamped. #endif // FIX_WRONG_SRGB_GAMMA - - float3 LUTColor = Lut.Sample(Sampler0, LUTEncodedColour * (1.f - (1.f / LUT_SIZE)) + ((1.f / LUT_SIZE) / 2.f)); + + float3 LUTColor = Lut.Sample(Sampler0, LUTEncodedColor * (1.f - (1.f / LUT_SIZE)) + ((1.f / LUT_SIZE) / 2.f)); #if !FIX_LUT_GAMMA_MAPPING LUTColor = gamma_sRGB_to_linear(LUTColor); #endif // FIX_LUT_GAMMA_MAPPING - float LUTMaskAlpha = saturate(LutMask.Sample(Sampler0, psInput.TEXCOORD).x + (1.f - LUTStrenght)); - colour = lerp(LUTColor, colour, LUTMaskAlpha); + float LUTMaskAlpha = saturate(LutMask.Sample(Sampler0, psInput.TEXCOORD).x + (1.f - LUTStrength)); + color = lerp(LUTColor, color, LUTMaskAlpha); #else #endif // APPLY_MERGED_COLOR_GRADING_LUT - - const float postColorCorrectionLuminance = Luminance(colour); - + + const float postColorCorrectionLuminance = Luminance(color); + #if ENABLE_HDR // Restore any luminance beyond 1 that ended up clipped by HDR->SDR tonemappers and any subsequent image manipulation - colour *= FindLuminanceToRestore(tonemapLostLuminance, preColorCorrectionLuminance, postColorCorrectionLuminance); - + color *= FindLuminanceToRestore(tonemapLostLuminance, preColorCorrectionLuminance, postColorCorrectionLuminance); + #if !DISABLE_TONEMAP && !DISABLE_INVERSE_TONEMAP - + #if !DISABLE_POST_PROCESS && !DISABLE_INVERSE_POST_PROCESS //TODO: passing in the previous luminance might not be the best, though is there any other way really? //Should we at least shift it by how much the LUT shifted the luminance? To invert it more correctly. - colour = PostProcess_Inverse(colour, prePostProcessColorLuminance); + color = PostProcess_Inverse(color, prePostProcessColorLuminance); #endif // !DISABLE_POST_PROCESS && !DISABLE_INVERSE_POST_PROCESS - + if (PcwHdrComposite.Tmo == 1u || ForceACES) { - colour = ACESReference_Inverse(colour); + color = ACESReference_Inverse(color); } else if (PcwHdrComposite.Tmo == 2u) { - colour = ACESParametric_Inverse(colour, acesParam_modE, acesParam_modA); + color = ACESParametric_Inverse(color, acesParam_modE, acesParam_modA); } else if (PcwHdrComposite.Tmo == 3u) { - colour = Hable_Inverse(colour, - hable_params_y0, - hable_params_y1, - hable_dstParams_W, - hable_toeSegment_lnA, - hable_toeSegment_B, - hable_midSegment_offsetX, - hable_midSegment_lnA, - hable_shoulderSegment_offsetX, - hable_shoulderSegment_offsetY, - hable_shoulderSegment_scaleX, - hable_shoulderSegment_lnA, - hable_shoulderSegment_B, - hable_invScale); + color = Hable_Inverse(color, + hable_params_y0, + hable_params_y1, + hable_dstParams_W, + hable_toeSegment_lnA, + hable_toeSegment_B, + hable_midSegment_offsetX, + hable_midSegment_lnA, + hable_shoulderSegment_offsetX, + hable_shoulderSegment_offsetY, + hable_shoulderSegment_scaleX, + hable_shoulderSegment_lnA, + hable_shoulderSegment_B, + hable_invScale); } else { // Any luminance lost by this tonemap case would have already been restored above with "tonemapLostLuminance". } - + #if !DISABLE_POST_PROCESS && !DISABLE_INVERSE_POST_PROCESS //TODO: ... this has no use now, and also, it won't work in the HDR range - colour = PostProcess(colour, prePostProcessColorLuminance); + color = PostProcess(color, prePostProcessColorLuminance); #endif // !DISABLE_POST_PROCESS && !DISABLE_INVERSE_POST_PROCESS #endif // !DISABLE_TONEMAP && !DISABLE_INVERSE_TONEMAP - - colour *= HDR_GAME_PAPER_WHITE; + + color *= HDR_GAME_PAPER_WHITE; #if CLAMP_INPUT_OUTPUT - colour = clamp(colour, 0.f, FLT16_MAX); // Avoid extremely high numbers turning into NaN in FP16 + color = clamp(color, 0.f, FLT16_MAX); // Avoid extremely high numbers turning into NaN in FP16 #endif - + //TODO: do DICE tonemapping to display nits, or some Hable like HDR tonemapper. - + #else - + #if SDR_USE_GAMMA_2_2 //TODO: This makes sense to have if we fix up the LUTs, though the SDR tonemapper had still been developed with sRGB<->2.2 mismatch - colour = pow(colour, 1.f / 2.2f); + color = pow(color, 1.f / 2.2f); #else // Do sRGB gamma even if we'd be playing on gamma 2.2 screens, as the game was already calibrated for 2.2 gamma - colour = gamma_linear_to_sRGB(colour); + color = gamma_linear_to_sRGB(color); #if CLAMP_INPUT_OUTPUT - colour = clamp(colour, 0.f, 1.f); + color = clamp(color, 0.f, 1.f); #endif #endif // SDR_USE_GAMMA_2_2 - + #endif // ENABLE_HDR - + PSOutput psOutput; - psOutput.SV_Target.rgb = colour; + psOutput.SV_Target.rgb = color; psOutput.SV_Target.a = 1.f; - return psOutput; + return psOutput; } From 8bef7b42e8772f090eed4aad056ddf78f5c51a24 Mon Sep 17 00:00:00 2001 From: Lilium Date: Thu, 21 Sep 2023 20:23:59 +0200 Subject: [PATCH 003/270] fix cs being compiled as ps and target file issue --- shaders/compile_all_shaders.bat | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/shaders/compile_all_shaders.bat b/shaders/compile_all_shaders.bat index fce915f5..6e0aaeb4 100644 --- a/shaders/compile_all_shaders.bat +++ b/shaders/compile_all_shaders.bat @@ -20,42 +20,42 @@ if not exist "%target_folder%" ( "%dxc%" "HDRComposite\HDRComposite_ps.hlsl" ^ -T ps_6_6 -E PS -D APPLY_MERGED_COLOR_GRADING_LUT -Fo ^ - "%target_folder%\HDRComposite\HDRComposite_E00FF1A_ps.bin" + "%target_folder%HDRComposite\HDRComposite_E00FF1A_ps.bin" "%dxc%" "HDRComposite\HDRComposite_ps.hlsl" ^ -T ps_6_6 -E PS -Fo ^ - "%target_folder%\HDRComposite\HDRComposite_600FF1A_ps.bin" + "%target_folder%HDRComposite\HDRComposite_600FF1A_ps.bin" "%dxc%" "HDRComposite\HDRComposite_ps.hlsl" ^ -T ps_6_6 -E PS -D APPLY_BLOOM -Fo ^ - "%target_folder%\HDRComposite\HDRComposite_700FF1A_ps.bin" + "%target_folder%HDRComposite\HDRComposite_700FF1A_ps.bin" "%dxc%" "HDRComposite\HDRComposite_ps.hlsl" ^ -T ps_6_6 -E PS -D APPLY_BLOOM -D APPLY_MERGED_COLOR_GRADING_LUT -Fo ^ - "%target_folder%\HDRComposite\HDRComposite_F00FF1A_ps.bin" + "%target_folder%HDRComposite\HDRComposite_F00FF1A_ps.bin" "%dxc%" "ScaleformComposite\ScaleformComposite_FFAA_ps.hlsl" ^ -T ps_6_6 -E PS -Fo ^ - "%target_folder%\ScaleformComposite\ScaleformComposite_FFAA_ps.bin" + "%target_folder%ScaleformComposite\ScaleformComposite_FFAA_ps.bin" "%dxc%" "ColorGradingMerge\ColorGradingMerge_FF81_cs.hlsl" ^ -T cs_6_6 -E main -Fo ^ - "%target_folder%\ColorGradingMerge\ColorGradingMerge_FF81_cs.bin" + "%target_folder%ColorGradingMerge\ColorGradingMerge_FF81_cs.bin" "%dxc%" "BinkMovie\BinkMovie_FFAB_ps.hlsl" ^ -T ps_6_6 -E PS -Fo ^ - "%target_folder%\BinkMovie\BinkMovie_FFAB_ps.bin" + "%target_folder%BinkMovie\BinkMovie_FFAB_ps.bin" "%dxc%" "FilmGrain\FilmGrain_FF75_ps.hlsl" ^ -T ps_6_6 -E main -Fo ^ - "%target_folder%\FilmGrain\FilmGrain_FF75_ps.bin" + "%target_folder%FilmGrain\FilmGrain_FF75_ps.bin" "%dxc%" "PostSharpen\PostSharpen_FF9A_ps.hlsl" ^ -T ps_6_6 -E main -Fo ^ - "%target_folder%\PostSharpen\PostSharpen_FF9A_ps.bin" + "%target_folder%PostSharpen\PostSharpen_FF9A_ps.bin" "%dxc%" "ContrastAdaptiveSharpening\ContrastAdaptiveSharpening_100FF94_cs.hlsl" ^ - -T ps_6_6 -E main -Fo ^ - "%target_folder%\ContrastAdaptiveSharpening\ContrastAdaptiveSharpening_100FF94_cs.bin" + -T cs_6_6 -E main -Fo ^ + "%target_folder%ContrastAdaptiveSharpening\ContrastAdaptiveSharpening_100FF94_cs.bin" pause \ No newline at end of file From 0d5e959c978562a4728c38f6c2ff00485ca33d1f Mon Sep 17 00:00:00 2001 From: Lilium Date: Thu, 21 Sep 2023 22:31:04 +0200 Subject: [PATCH 004/270] fix Hable tone mapping (again) --- shaders/HDRComposite/HDRComposite_ps.hlsl | 34 ++++++++++------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 50946d12..7cd42878 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -131,7 +131,7 @@ struct SPerSceneConstants struct PushConstantWrapper_HDRComposite { - uint HdrCmpDatIndex; + uint HdrCmpDatIndex; //index for HDRCompositeData uint Tmo; //tone mapping operator float BloomMultiplier; }; @@ -345,7 +345,6 @@ float3 Hable( inout float midSegment_lnA, inout float shoulderSegment_offsetX, inout float shoulderSegment_offsetY, - inout float shoulderSegment_scaleX, inout float shoulderSegment_lnA, inout float shoulderSegment_B, inout float invScale) @@ -396,16 +395,16 @@ float3 Hable( midSegment_offsetX = (b / m); //no minus midSegment_lnA = log2(m); - // EvalDerivativeLinearGamma https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L81 + // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L137-L138 // max(EPSILON, pow(params_yX, gamma)) - params_y0 = max(EPSILON, dstParams_y0); // is pow(x, 1) because gamma = 1 - //OLD: params_y0 = max(EPSILON, exp2(log2(dstParams_y0))); // toeM - params_y1 = max(EPSILON, dstParams_y1); // is pow(x, 1) because gamma = 1 - //OLD: params_y1 = max(EPSILON, exp2(log2(dstParams_y1))); // shoulderM + params_y0 = max(EPSILON, dstParams_y0); // is pow(x, gamma) with gamma = 1 + //OLD: params_y0 = max(EPSILON, exp2(log2(dstParams_y0))); + params_y1 = max(EPSILON, dstParams_y1); // is pow(x, gamma) with gamma = 1 + //OLD: params_y1 = max(EPSILON, exp2(log2(dstParams_y1))); // pow(1.f + dstParams_overshootY, gamma) - 1.f // -1 was optimised away as shoulderSegment_offsetY is params_overshootY + 1 - float params_overshootY = 1.f + dstParams_overshootY; // is pow(x, 1) because gamma = 1 + float params_overshootY = 1.f + dstParams_overshootY; // is pow(x, gamma) with gamma = 1 //OLD: float params_overshootY = exp2(log2(1.f + dstParams_overshootY)); // toe section @@ -418,14 +417,12 @@ float3 Hable( // shoulder section float shoulderSection_x0 = 1.f + params_overshootX - params_x1; float shoulderSection_y0 = params_overshootY - params_y1; // 1 + x was optimised away - shoulderSegment_offsetX = 1.f + params_overshootX * dstParams_W; - float shoulderSegment_offsetX_optimised = 1.f + params_overshootX; + shoulderSegment_offsetX = 1.f + params_overshootX; shoulderSegment_offsetY = params_overshootY; // x + 1 was optimised away - shoulderSegment_scaleX = -1.f; // could be optimised away if we move this outside of the PS // SolveAB shoulderSegment_B = ((m * shoulderSection_x0) / (shoulderSection_y0 + EPSILON)); - shoulderSegment_lnA = (log2(shoulderSection_y0) * RCP_LOG2_E) - (shoulderSegment_B * log2(shoulderSection_x0)); float shoulderSegment_B_optimised = shoulderSegment_B * RCP_LOG2_E; + shoulderSegment_lnA = (log2(shoulderSection_y0) * RCP_LOG2_E) - (shoulderSegment_B_optimised * log2(shoulderSection_x0)); // shoulder section end // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L6 @@ -433,10 +430,10 @@ float3 Hable( float evalY0 = 0.f; if (params_overshootX > 0.f) { - evalY0 = -exp2(((shoulderSegment_B_optimised * log2(params_overshootX)) + shoulderSegment_lnA) * LOG2_E); + evalY0 = -exp2(((shoulderSegment_B_optimised * log2(params_overshootX)) + shoulderSegment_lnA) * LOG2_E) + params_overshootY; } // Eval end - invScale = 1.f / (evalY0 + params_overshootY); + invScale = 1.f / (evalY0); float3 toneMapped; @@ -461,7 +458,8 @@ float3 Hable( else { //float evalShoulderSegment_y0 = ((-1.f) - params_overshootX) + normX; - float evalShoulderSegment_y0 = (normX - shoulderSegment_offsetX_optimised) * shoulderSegment_scaleX; + // small optimisation from the original decompilation + float evalShoulderSegment_y0 = (1.f + params_overshootX) - normX; float evalShoulderReturn = 0.f; if (evalShoulderSegment_y0 > 0.f) { @@ -475,7 +473,7 @@ float3 Hable( } // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L21-L34 -float HableEvalInverse( +float HableEval_Inverse( float Channel, float offsetX, float offsetY, @@ -507,7 +505,6 @@ float3 Hable_Inverse( float midSegment_lnA, float shoulderSegment_offsetX, float shoulderSegment_offsetY, - float shoulderSegment_scaleX, float shoulderSegment_lnA, float shoulderSegment_B, float invScale) @@ -668,7 +665,6 @@ PSOutput PS(PSInput psInput) float hable_midSegment_lnA; float hable_shoulderSegment_offsetX; float hable_shoulderSegment_offsetY; - float hable_shoulderSegment_scaleX; float hable_shoulderSegment_lnA; float hable_shoulderSegment_B; float hable_invScale; @@ -696,7 +692,6 @@ PSOutput PS(PSInput psInput) hable_midSegment_lnA, hable_shoulderSegment_offsetX, hable_shoulderSegment_offsetY, - hable_shoulderSegment_scaleX, hable_shoulderSegment_lnA, hable_shoulderSegment_B, hable_invScale); @@ -782,7 +777,6 @@ PSOutput PS(PSInput psInput) hable_midSegment_lnA, hable_shoulderSegment_offsetX, hable_shoulderSegment_offsetY, - hable_shoulderSegment_scaleX, hable_shoulderSegment_lnA, hable_shoulderSegment_B, hable_invScale); From 6afbe5019c45cc03b37bfd41679d04e972e91faf Mon Sep 17 00:00:00 2001 From: Lilium Date: Thu, 21 Sep 2023 22:41:25 +0200 Subject: [PATCH 005/270] fixup Habel inverse tone mapping --- shaders/FilmGrain/FilmGrain_FF75_ps.hlsl | 4 +- shaders/HDRComposite/HDRComposite_ps.hlsl | 70 +++++++++++++---------- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl b/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl index dac50022..590077b9 100644 --- a/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl +++ b/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl @@ -1,5 +1,5 @@ #define ENABLE_HDR 1 -#define HDR_GAME_PAPER_WHITE 2.5f +#define HDR_GAME_PAPER_WHITE 1.f #define FILM_GRAIN_TEXTURE_SIZE 1024u cbuffer _13_15 : register(b0, space0) @@ -80,7 +80,7 @@ void frag_main() #else float inverseLuminance = saturate(1.f - Luminance(tonemappedColor)); #endif // ENABLE_HDR - + #if 0 // WIP fixes const float filmGrainInvSize = 1.f / FILM_GRAIN_TEXTURE_SIZE; // This was "1.f / (FILM_GRAIN_TEXTURE_SIZE - 1u)", though that seems incorrect const float filmGrainHalfSize = FILM_GRAIN_TEXTURE_SIZE * 0.5f; // This was "521.f", which seems like a mistake on 512 diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 7cd42878..3d93545d 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -6,7 +6,7 @@ // Also in "ps_4105314757" #define ENABLE_HDR 1 -#define HDR_GAME_PAPER_WHITE 2.5f +#define HDR_GAME_PAPER_WHITE 1.f #define SDR_USE_GAMMA_2_2 0 #define FIX_WRONG_SRGB_GAMMA 1 // Also in "cs_580663709" @@ -16,7 +16,7 @@ #define DISABLE_TONEMAP 0 #define DISABLE_POST_PROCESS 1 #define DISABLE_LUT 1 -#define DISABLE_INVERSE_TONEMAP 1 +#define DISABLE_INVERSE_TONEMAP 0 #define DISABLE_INVERSE_POST_PROCESS 1 #define CLAMP_INPUT_OUTPUT 0 @@ -387,13 +387,16 @@ float3 Hable( #define params_overshootX dstParams_overshootX float dx = y1_offset / dstParams_W; + // mid section // AsSlopeIntercept https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L67 float m = (abs(dx) < EPSILON) ? 1.f : (y1_offset / dx); m += EPSILON; float b = dstParams_y0 - (m * params_x0); midSegment_offsetX = (b / m); //no minus - midSegment_lnA = log2(m); + float midSegment_lnA_optimised = log2(m); + midSegment_lnA = RCP_LOG2_E * midSegment_lnA_optimised; + // mid section end // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L137-L138 // max(EPSILON, pow(params_yX, gamma)) @@ -410,8 +413,9 @@ float3 Hable( // toe section // SolveAB https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L60 toeSegment_B = (m * params_x0) / (params_y0 + EPSILON); - float _410 = log2(params_y0); //doesn't belong to SolveAB - toeSegment_lnA = toeSegment_B * (-log(params_x0)); + float _410 = log2(params_y0); //doesn't belong to SolveAB (it might does) + float toeSegment_lnA_optimised = -toeSegment_B * log(params_x0); + toeSegment_lnA = _410 * RCP_LOG2_E + toeSegment_lnA_optimised; // toe section end // shoulder section @@ -445,14 +449,14 @@ float3 Hable( if (isToeSegment && normX > 0.f) { - returnChannel = exp2(((((log2(normX) * toeSegment_B) + _410) * RCP_LOG2_E) + toeSegment_lnA) * LOG2_E); + returnChannel = exp2(((((log2(normX) * toeSegment_B) + _410) * RCP_LOG2_E) + toeSegment_lnA_optimised) * LOG2_E); } else if (normX < params_x1) { float evalMidSegment_y0 = normX + midSegment_offsetX; //is -(-midSegment_offsetX) if (evalMidSegment_y0 > 0.f) { - returnChannel = exp2(log2(evalMidSegment_y0) + midSegment_lnA); + returnChannel = exp2(log2(evalMidSegment_y0) + midSegment_lnA_optimised); } } else @@ -513,35 +517,39 @@ float3 Hable_Inverse( for (uint channel = 0; channel < 3; channel++) { - if (InputColor[channel] < params_y0) + // scaleY and offsetY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L187-L197 + if (InputColor[channel] < params_y0) // toe { - itmColor[channel] = HableEvalInverse(InputColor[channel], - 0.f, - 0.f, // * invScale - 1.f, - 1.f * invScale, - toeSegment_lnA, - toeSegment_B); + // scaleXY and offsetXY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L151-L154 + itmColor[channel] = HableEval_Inverse(InputColor[channel], + 0.f, + 0.f, // * invScale + 1.f, + 1.f * invScale, + toeSegment_lnA, + toeSegment_B); } - else if (InputColor[channel] < params_y1) + else if (InputColor[channel] < params_y1) // mid { - itmColor[channel] = HableEvalInverse(InputColor[channel], - -midSegment_offsetX, // minus was optimised away - 0.f, // * invScale - 1.f, - 1.f * invScale, - midSegment_lnA, - 1.f); + // scaleXY and offsetY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L125-L127 + itmColor[channel] = HableEval_Inverse(InputColor[channel], + -midSegment_offsetX, // minus was optimised away + 0.f, // * invScale + 1.f, + 1.f * invScale, + midSegment_lnA, + 1.f); } - else + else // shoulder { - itmColor[channel] = HableEvalInverse(InputColor[channel], - shoulderSegment_offsetX, - shoulderSegment_offsetY * invScale, - shoulderSegment_scaleX, - -1.f * invScale, - shoulderSegment_lnA, - shoulderSegment_B); + // scaleXY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L175-L176 + itmColor[channel] = HableEval_Inverse(InputColor[channel], + shoulderSegment_offsetX, + shoulderSegment_offsetY * invScale, + -1.f, + -1.f * invScale, + shoulderSegment_lnA, + shoulderSegment_B); } } From 66e8f2eef77163cadb8b010c050a4a7d0355abcd Mon Sep 17 00:00:00 2001 From: Filoppi Date: Fri, 22 Sep 2023 00:21:13 +0300 Subject: [PATCH 006/270] add includes --- shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl | 4 +-- .../ColorGradingMerge_FF81_cs.hlsl | 26 ++++++++++++------- ...ContrastAdaptiveSharpening_100FF94_cs.hlsl | 2 +- shaders/FilmGrain/FilmGrain_FF75_ps.hlsl | 4 +-- shaders/HDRComposite/HDRComposite_ps.hlsl | 14 ++++------ shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl | 2 +- .../ScaleformComposite_FFAA_ps.hlsl | 5 ++-- shaders/shared.h | 13 ++++++++++ 8 files changed, 42 insertions(+), 28 deletions(-) create mode 100644 shaders/shared.h diff --git a/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl b/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl index 0c0c8f0d..cc2db342 100644 --- a/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl +++ b/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl @@ -1,5 +1,5 @@ -#define ENABLE_HDR 1 -#define HDR_GAME_PAPER_WHITE 2.5f +#include "../shared.h" + #define HDR_USE_GAMMA_2_2 1 Texture2D _8 : register(t0, space8); diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index aa707e41..3c378437 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -1,10 +1,10 @@ -// Also in "tonemap" shaders. -// This currently shifts colors too much, even if it's mathematically correct. -#define FIX_LUT_GAMMA_MAPPING 1 -#define LUT_SIZE 16.f +#include "../shared.h" -static float additionalNeutralLUTPercentage = 0.5f; -static float LUTLuminancePreservationPercentage = 0.5f; +// 0 None, 1 ShortFuse technique, 2 luminance preservation (doesn't look so good) +#define LUT_IMPROVEMENT_TYPE 0 + +static float additionalNeutralLUTPercentage = 0.0f; +static float LUTCorrectionPercentage = 0.0f; struct PushConstantWrapper_ColorGradingMerge { @@ -105,14 +105,20 @@ void CS() float LUT3Luminance = Luminance(LUT3Color); float LUT4Luminance = Luminance(LUT4Color); +#if LUT_IMPROVEMENT_TYPE == 0 + +#elif LUT_IMPROVEMENT_TYPE == 1 + //TODO +#elif LUT_IMPROVEMENT_TYPE == 2 if (LUT1Luminance != 0.f) - LUT1Color *= lerp(1.f, neutralLUTLuminance / LUT1Luminance, LUTLuminancePreservationPercentage); + LUT1Color *= lerp(1.f, neutralLUTLuminance / LUT1Luminance, LUTCorrectionPercentage); if (LUT2Luminance != 0.f) - LUT2Color *= lerp(1.f, neutralLUTLuminance / LUT2Luminance, LUTLuminancePreservationPercentage); + LUT2Color *= lerp(1.f, neutralLUTLuminance / LUT2Luminance, LUTCorrectionPercentage); if (LUT3Luminance != 0.f) - LUT3Color *= lerp(1.f, neutralLUTLuminance / LUT3Luminance, LUTLuminancePreservationPercentage); + LUT3Color *= lerp(1.f, neutralLUTLuminance / LUT3Luminance, LUTCorrectionPercentage); if (LUT4Luminance != 0.f) - LUT4Color *= lerp(1.f, neutralLUTLuminance / LUT4Luminance, LUTLuminancePreservationPercentage); + LUT4Color *= lerp(1.f, neutralLUTLuminance / LUT4Luminance, LUTCorrectionPercentage); +#endif #if !FIX_LUT_GAMMA_MAPPING && 0 // Disabled as it's still preferable to do LUT blends in linear space neutralLUTColor = gamma_linear_to_sRGB(neutralLUTColor); diff --git a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_100FF94_cs.hlsl b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_100FF94_cs.hlsl index c6233cdf..259e999e 100644 --- a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_100FF94_cs.hlsl +++ b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_100FF94_cs.hlsl @@ -1,4 +1,4 @@ -#define ENABLE_HDR 1 +#include "../shared.h" cbuffer _16_18 : register(b0, space6) { diff --git a/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl b/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl index 590077b9..887e0d78 100644 --- a/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl +++ b/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl @@ -1,5 +1,5 @@ -#define ENABLE_HDR 1 -#define HDR_GAME_PAPER_WHITE 1.f +#include "../shared.h" + #define FILM_GRAIN_TEXTURE_SIZE 1024u cbuffer _13_15 : register(b0, space0) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 3d93545d..2619819e 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -1,21 +1,17 @@ +#include "../shared.h" + // These are defined at compile time (shaders permutations) //#define APPLY_BLOOM //#define APPLY_TONEMAPPING //#define APPLY_CINEMATICS //#define APPLY_MERGED_COLOR_GRADING_LUT -// Also in "ps_4105314757" -#define ENABLE_HDR 1 -#define HDR_GAME_PAPER_WHITE 1.f #define SDR_USE_GAMMA_2_2 0 #define FIX_WRONG_SRGB_GAMMA 1 -// Also in "cs_580663709" -#define FIX_LUT_GAMMA_MAPPING 1 -#define LUT_SIZE 16.f -// Also sets "DISABLE_LUT" and "DISABLE_POST_PROCESS" and "DISABLE_INVERSE_TONEMAP" +// Also sets "DISABLE_LUT", "DISABLE_POST_PROCESS" and "DISABLE_INVERSE_TONEMAP" #define DISABLE_TONEMAP 0 -#define DISABLE_POST_PROCESS 1 -#define DISABLE_LUT 1 +#define DISABLE_POST_PROCESS 0 +#define DISABLE_LUT 0 #define DISABLE_INVERSE_TONEMAP 0 #define DISABLE_INVERSE_POST_PROCESS 1 #define CLAMP_INPUT_OUTPUT 0 diff --git a/shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl b/shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl index 81105716..70d507f5 100644 --- a/shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl +++ b/shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl @@ -1,4 +1,4 @@ -#define ENABLE_HDR 1 +#include "../shared.h" cbuffer _13_15 : register(b0, space7) { diff --git a/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl b/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl index 12e8d572..6a3a79ec 100644 --- a/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl +++ b/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl @@ -1,6 +1,5 @@ -// Also in "tonemap" shaders. -#define ENABLE_HDR 1 -#define HDR_UI_PAPER_WHITE 2.5f +#include "../shared.h" + // Hack: change the alpha value at which the UI blends in in HDR, to increase readability. Range is 0 to 1, with 1 having no effect. #define HDR_UI_BLEND_POW 0.775f #define HDR_GAMMA_CORRECTION 1 diff --git a/shaders/shared.h b/shaders/shared.h new file mode 100644 index 00000000..3a05646d --- /dev/null +++ b/shaders/shared.h @@ -0,0 +1,13 @@ +// Helpers for HDR rendering. + +#ifndef HDR_H +#define HDR_H + +#define ENABLE_HDR 1 +#define HDR_GAME_PAPER_WHITE 1.f +#define HDR_UI_PAPER_WHITE 2.5f +// This possibly shifts colors a lot, but it's mathematically correct. +#define FIX_LUT_GAMMA_MAPPING 1 +#define LUT_SIZE 16.f + +#endif \ No newline at end of file From 5545b1d41e02ec66bdab0c01d78c3e59cb4bab7f Mon Sep 17 00:00:00 2001 From: Lilium Date: Fri, 22 Sep 2023 01:01:10 +0200 Subject: [PATCH 007/270] HDRComposite: change inout from Hable to just out --- shaders/HDRComposite/HDRComposite_ps.hlsl | 26 +++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 2619819e..5127286b 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -331,19 +331,19 @@ float3 ACESParametric_Inverse(float3 Color, float modE, float modA) // https://github.com/johnhable/fw-public float3 Hable( - in float3 InputColor, - inout float params_y0, - inout float params_y1, - inout float dstParams_W, - inout float toeSegment_lnA, - inout float toeSegment_B, - inout float midSegment_offsetX, - inout float midSegment_lnA, - inout float shoulderSegment_offsetX, - inout float shoulderSegment_offsetY, - inout float shoulderSegment_lnA, - inout float shoulderSegment_B, - inout float invScale) + in float3 InputColor, + out float params_y0, + out float params_y1, + out float dstParams_W, + out float toeSegment_lnA, + out float toeSegment_B, + out float midSegment_offsetX, + out float midSegment_lnA, + out float shoulderSegment_offsetX, + out float shoulderSegment_offsetY, + out float shoulderSegment_lnA, + out float shoulderSegment_B, + out float invScale) { // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L202 From 9f0b9045f01574fe254463a339abfc7bacb2c476 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Fri, 22 Sep 2023 03:21:20 +0300 Subject: [PATCH 008/270] Add new include files, minor polish --- shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl | 22 +------- ...ContrastAdaptiveSharpening_100FF94_cs.hlsl | 1 + shaders/FilmGrain/FilmGrain_FF75_ps.hlsl | 48 +--------------- shaders/HDRComposite/HDRComposite_ps.hlsl | 56 ++----------------- shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl | 1 + .../ScaleformComposite_FFAA_ps.hlsl | 43 +------------- shaders/color.h | 52 +++++++++++++++++ shaders/math.h | 6 ++ shaders/shared.h | 8 +-- 9 files changed, 70 insertions(+), 167 deletions(-) create mode 100644 shaders/color.h create mode 100644 shaders/math.h diff --git a/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl b/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl index cc2db342..c7a17b7f 100644 --- a/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl +++ b/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl @@ -1,4 +1,5 @@ #include "../shared.h" +#include "../color.h" #define HDR_USE_GAMMA_2_2 1 @@ -13,27 +14,6 @@ struct PSInputs float2 TEXCOORD : TEXCOORD0; }; -float gamma_sRGB_to_linear(float channel) -{ - [flatten] - if (channel <= 0.04045f) - { - channel = channel / 12.92f; - } - else - { - channel = pow((channel + 0.055f) / 1.055f, 2.4f); - } - return channel; -} - -float3 gamma_sRGB_to_linear(float3 Color) -{ - return float3(gamma_sRGB_to_linear(Color.r), - gamma_sRGB_to_linear(Color.g), - gamma_sRGB_to_linear(Color.b)); -} - float4 PS(PSInputs inputs) : SV_Target { float Y = _8.Sample(_13, inputs.TEXCOORD.xy).x * 1.16412353515625f; diff --git a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_100FF94_cs.hlsl b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_100FF94_cs.hlsl index 259e999e..83bae5f1 100644 --- a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_100FF94_cs.hlsl +++ b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_100FF94_cs.hlsl @@ -1,4 +1,5 @@ #include "../shared.h" +#include "../color.h" cbuffer _16_18 : register(b0, space6) { diff --git a/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl b/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl index 887e0d78..e7572563 100644 --- a/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl +++ b/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl @@ -1,4 +1,5 @@ #include "../shared.h" +#include "../color.h" #define FILM_GRAIN_TEXTURE_SIZE 1024u @@ -23,53 +24,6 @@ struct SPIRV_Cross_Output float4 SV_Target : SV_Target0; }; -float gamma_linear_to_sRGB(float channel) -{ - [flatten] - if (channel <= 0.0031308f) - { - channel = channel * 12.92f; - } - else - { - channel = 1.055f * pow(channel, 1.0f / 2.4f) - 0.055f; - } - return channel; -} - -float3 gamma_linear_to_sRGB(float3 Color) -{ - return float3(gamma_linear_to_sRGB(Color.r), - gamma_linear_to_sRGB(Color.g), - gamma_linear_to_sRGB(Color.b)); -} - -float gamma_sRGB_to_linear(float channel) -{ - [flatten] - if (channel <= 0.04045f) - { - channel = channel / 12.92f; - } - else - { - channel = pow((channel + 0.055f) / 1.055f, 2.4f); - } - return channel; -} - -float3 gamma_sRGB_to_linear(float3 Color) -{ - return float3(gamma_sRGB_to_linear(Color.r), - gamma_sRGB_to_linear(Color.g), - gamma_sRGB_to_linear(Color.b)); -} -float Luminance(float3 color) -{ - // Fixed from "wrong" values: 0.2125 0.7154 0.0721f - return dot(color, float3(0.2126f, 0.7152f, 0.0722f)); -} - void frag_main() { float3 tonemappedColor = TonemappedColorTexture.Sample(Sampler0, float2(TEXCOORD.x, TEXCOORD.y)); diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 5127286b..87fd5c46 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -1,4 +1,5 @@ #include "../shared.h" +#include "../color.h" // These are defined at compile time (shaders permutations) //#define APPLY_BLOOM @@ -14,7 +15,7 @@ #define DISABLE_LUT 0 #define DISABLE_INVERSE_TONEMAP 0 #define DISABLE_INVERSE_POST_PROCESS 1 -#define CLAMP_INPUT_OUTPUT 0 +#define CLAMP_INPUT_OUTPUT 1 struct ResolutionBlock { @@ -218,55 +219,6 @@ struct PSOutput static const float LUTStrength = 0.f; static const float RestorePreLUTLuminance = 1.f; -static const bool ForceACES = false; //TODO: delete once Hable works - -float gamma_linear_to_sRGB(float channel) -{ - [flatten] - if (channel <= 0.0031308f) - { - channel = channel * 12.92f; - } - else - { - channel = 1.055f * pow(channel, 1.0f / 2.4f) - 0.055f; - } - return channel; -} - -float3 gamma_linear_to_sRGB(float3 Color) -{ - return float3(gamma_linear_to_sRGB(Color.r), - gamma_linear_to_sRGB(Color.g), - gamma_linear_to_sRGB(Color.b)); -} - -float gamma_sRGB_to_linear(float channel) -{ - [flatten] - if (channel <= 0.04045f) - { - channel = channel / 12.92f; - } - else - { - channel = pow((channel + 0.055f) / 1.055f, 2.4f); - } - return channel; -} - -float3 gamma_sRGB_to_linear(float3 Color) -{ - return float3(gamma_sRGB_to_linear(Color.r), - gamma_sRGB_to_linear(Color.g), - gamma_sRGB_to_linear(Color.b)); -} - -float Luminance(float3 Color) -{ - // Fixed from "wrong" values: 0.2125 0.7154 0.0721f - return dot(Color, float3(0.2126f, 0.7152f, 0.0722f)); -} static const float ACES_a = 2.51f; static const float ACES_b = 0.03f; @@ -674,7 +626,7 @@ PSOutput PS(PSInput psInput) float hable_invScale; const bool clampACES = false; - if (PcwHdrComposite.Tmo == 1u || ForceACES) + if (PcwHdrComposite.Tmo == 1u) { tonemappedUnclampedColor = ACESReference(inputColor, clampACES); tonemappedColor = saturate(tonemappedUnclampedColor); @@ -761,7 +713,7 @@ PSOutput PS(PSInput psInput) color = PostProcess_Inverse(color, prePostProcessColorLuminance); #endif // !DISABLE_POST_PROCESS && !DISABLE_INVERSE_POST_PROCESS - if (PcwHdrComposite.Tmo == 1u || ForceACES) + if (PcwHdrComposite.Tmo == 1u) { color = ACESReference_Inverse(color); } diff --git a/shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl b/shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl index 70d507f5..d0b75f8c 100644 --- a/shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl +++ b/shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl @@ -1,4 +1,5 @@ #include "../shared.h" +#include "../color.h" cbuffer _13_15 : register(b0, space7) { diff --git a/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl b/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl index 6a3a79ec..d469f284 100644 --- a/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl +++ b/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl @@ -1,4 +1,5 @@ #include "../shared.h" +#include "../color.h" // Hack: change the alpha value at which the UI blends in in HDR, to increase readability. Range is 0 to 1, with 1 having no effect. #define HDR_UI_BLEND_POW 0.775f @@ -18,48 +19,6 @@ cbuffer stub_PushConstantWrapper_ScaleformCompositeLayout : register(b0) Texture2D inputTexture : register(t0, space8); SamplerState inputSampler : register(s0, space8); -float gamma_linear_to_sRGB(float channel) -{ - [flatten] - if (channel <= 0.0031308f) - { - channel = channel * 12.92f; - } - else - { - channel = 1.055f * pow(channel, 1.0f / 2.4f) - 0.055f; - } - return channel; -} - -float3 gamma_linear_to_sRGB(float3 Color) -{ - return float3(gamma_linear_to_sRGB(Color.r), - gamma_linear_to_sRGB(Color.g), - gamma_linear_to_sRGB(Color.b)); -} - -float gamma_sRGB_to_linear(float channel) -{ - [flatten] - if (channel <= 0.04045f) - { - channel = channel / 12.92f; - } - else - { - channel = pow((channel + 0.055f) / 1.055f, 2.4f); - } - return channel; -} - -float3 gamma_sRGB_to_linear(float3 Color) -{ - return float3(gamma_sRGB_to_linear(Color.r), - gamma_sRGB_to_linear(Color.g), - gamma_sRGB_to_linear(Color.b)); -} - struct PSInputs { float4 uv : TEXCOORD0; diff --git a/shaders/color.h b/shaders/color.h new file mode 100644 index 00000000..d377d8aa --- /dev/null +++ b/shaders/color.h @@ -0,0 +1,52 @@ +#ifndef COLOR_H +#define COLOR_H + +float gamma_linear_to_sRGB(float channel) +{ + [flatten] + if (channel <= 0.0031308f) + { + channel = channel * 12.92f; + } + else + { + channel = 1.055f * pow(channel, 1.0f / 2.4f) - 0.055f; + } + return channel; +} + +float3 gamma_linear_to_sRGB(float3 Color) +{ + return float3(gamma_linear_to_sRGB(Color.r), + gamma_linear_to_sRGB(Color.g), + gamma_linear_to_sRGB(Color.b)); +} + +float gamma_sRGB_to_linear(float channel) +{ + [flatten] + if (channel <= 0.04045f) + { + channel = channel / 12.92f; + } + else + { + channel = pow((channel + 0.055f) / 1.055f, 2.4f); + } + return channel; +} + +float3 gamma_sRGB_to_linear(float3 Color) +{ + return float3(gamma_sRGB_to_linear(Color.r), + gamma_sRGB_to_linear(Color.g), + gamma_sRGB_to_linear(Color.b)); +} + +float Luminance(float3 color) +{ + // Fixed from "wrong" values: 0.2125 0.7154 0.0721f + return dot(color, float3(0.2126f, 0.7152f, 0.0722f)); +} + +#endif \ No newline at end of file diff --git a/shaders/math.h b/shaders/math.h new file mode 100644 index 00000000..bda85717 --- /dev/null +++ b/shaders/math.h @@ -0,0 +1,6 @@ +#ifndef MATH_H +#define MATH_H + +#define FLT_MAX 3.402823466e+38F + +#endif \ No newline at end of file diff --git a/shaders/shared.h b/shaders/shared.h index 3a05646d..6aae532d 100644 --- a/shaders/shared.h +++ b/shaders/shared.h @@ -1,12 +1,10 @@ -// Helpers for HDR rendering. - -#ifndef HDR_H -#define HDR_H +#ifndef SHARED_H +#define SHARED_H #define ENABLE_HDR 1 #define HDR_GAME_PAPER_WHITE 1.f #define HDR_UI_PAPER_WHITE 2.5f -// This possibly shifts colors a lot, but it's mathematically correct. +// This possibly shifts colors a lot, but it's mathematically correct, it's also necessary for HDR LUTs to work. #define FIX_LUT_GAMMA_MAPPING 1 #define LUT_SIZE 16.f From 0dea0aa68106b106eeb853c63e05d7f453ba2ec0 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Fri, 22 Sep 2023 03:21:36 +0300 Subject: [PATCH 009/270] Add normalized LUTs --- .../ColorGradingMerge_FF81_cs.hlsl | 214 +++++++++++++----- shaders/math.h | 13 ++ 2 files changed, 175 insertions(+), 52 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 3c378437..a0caeaa9 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -1,10 +1,12 @@ #include "../shared.h" +#include "../color.h" +#include "../math.h" -// 0 None, 1 ShortFuse technique, 2 luminance preservation (doesn't look so good) -#define LUT_IMPROVEMENT_TYPE 0 +// 0 None, 1 ShortFuse technique (normalization), 2 luminance preservation (doesn't look so good) +#define LUT_IMPROVEMENT_TYPE 1 static float additionalNeutralLUTPercentage = 0.0f; -static float LUTCorrectionPercentage = 0.0f; +static float LUTCorrectionPercentage = 1.0f; struct PushConstantWrapper_ColorGradingMerge { @@ -20,76 +22,179 @@ cbuffer PushConstantWrapper_ColorGradingMerge : register(b0, space0) PushConstantWrapper_ColorGradingMerge PcwColorGradingMerge : packoffset(c0); }; -Texture2D LUT1 : register(t0, space8); -Texture2D LUT2 : register(t1, space8); -Texture2D LUT3 : register(t2, space8); -Texture2D LUT4 : register(t3, space8); -RWTexture3D outMixedLUT : register(u0, space8); - -static uint3 gl_GlobalInvocationID; -struct SPIRV_Cross_Input +struct LUTAnalysis { - uint3 gl_GlobalInvocationID : SV_DispatchThreadID; + float minY; + float maxY; + float averageY; + float range; + float3 black; + float3 white; + float blackY; + float whiteY; + //float medianY; + float minChannel; + float averageChannel; + float maxChannel; + float averageRed; + float averageGreen; + float averageBlue; + float maxRed; + float maxGreen; + float maxBlue; + float minRed; + float minGreen; + float minBlue; }; -float gamma_linear_to_sRGB(float channel) +void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) { - [flatten] - if (channel <= 0.0031308f) - { - channel = channel * 12.92f; - } - else + Analysis.minY = FLT_MAX; + Analysis.maxY = 0.f; + Analysis.minRed = FLT_MAX; + Analysis.minBlue = FLT_MAX; + Analysis.minGreen = FLT_MAX; + Analysis.maxRed = 0.f; + Analysis.maxBlue = 0.f; + Analysis.maxGreen = 0.f; + Analysis.black = gamma_sRGB_to_linear(LUT.Load(int3(0, 0, 0)).rgb); + Analysis.white = gamma_sRGB_to_linear(LUT.Load(int3(1, 1, 1)).rgb); + Analysis.blackY = Luminance(Analysis.black); + Analysis.whiteY = Luminance(Analysis.white); + + float reds = 0.f; + float greens = 0.f; + float blues = 0.f; + float Ys = 0.f; + + const uint texelCount = LUT_SIZE * LUT_SIZE * LUT_SIZE; + + for (uint x = 0; x < (uint)LUT_SIZE; x++) { - channel = 1.055f * pow(channel, 1.0f / 2.4f) - 0.055f; + for (uint y = 0; y < (uint)LUT_SIZE; y++) + { + for (uint z = 0; z < (uint)LUT_SIZE; z++) + { + const uint inU = (z << 4u) + x; + const uint3 inUVW = uint3(inU, y, 0u); // In pixels + float3 LUTColor = gamma_sRGB_to_linear(LUT.Load(inUVW).rgb); + + float Y = Luminance(LUTColor); + Analysis.minY = min(Analysis.minY, Y); + Analysis.maxY = max(Analysis.maxY, Y); + + Analysis.minRed = min(Analysis.minRed, LUTColor.r); + Analysis.minGreen = min(Analysis.minGreen, LUTColor.g); + Analysis.minBlue = min(Analysis.minBlue, LUTColor.b); + + Analysis.maxRed = max(Analysis.maxRed, LUTColor.r); + Analysis.maxGreen = max(Analysis.maxGreen, LUTColor.g); + Analysis.maxBlue = max(Analysis.maxBlue, LUTColor.b); + + reds += LUTColor.r; + greens += LUTColor.g; + blues += LUTColor.b; + Ys += Y; + } + } } - return channel; -} + + //TODO: either store min/max channels merged or separately, but not both + Analysis.minChannel = max(Analysis.minRed, max(Analysis.minGreen, Analysis.minBlue)); + Analysis.maxChannel = max(Analysis.maxRed, max(Analysis.maxGreen, Analysis.maxBlue)); + + Analysis.averageY = Ys / texelCount; + Analysis.averageRed = reds / texelCount; + Analysis.averageGreen = greens / texelCount; + Analysis.averageBlue = blues / texelCount; -float3 gamma_linear_to_sRGB(float3 Color) -{ - return float3(gamma_linear_to_sRGB(Color.r), - gamma_linear_to_sRGB(Color.g), - gamma_linear_to_sRGB(Color.b)); + //TODO: necessary? + Analysis.averageChannel = (Analysis.averageRed + Analysis.averageGreen + Analysis.averageBlue) / 3.f; + //TODO: unncessary? + Analysis.range = Analysis.maxY - Analysis.minY; + + //TODO: ??? Can't do this in shader but it's not needed + //Ys.sort((a, b) => a - b); + //Analysis.medianY = ((Ys[Math.floor(texelCount / 2)]) + (Ys[Math.ceil(texelCount / 2)])) / 2; } -float gamma_sRGB_to_linear(float channel) +// Analyzes each LUT texel and normalize their range. +// Slow but effective. +float3 PatchLUTColor(Texture2D LUT, uint3 UVW, bool SDRRange = false) { - [flatten] - if (channel <= 0.04045f) + LUTAnalysis analysis; + AnalyzeLUT(LUT, analysis); + + float scaleDown = (0.f - analysis.minChannel); + float scaleUp = (1.f - analysis.maxChannel); + // Black will be scaled by min channel + float3 scaledBlack = analysis.black - analysis.minChannel; + // White will be scaled up by max channel + float3 scaledWhite = analysis.white + (1.f - analysis.maxChannel); + float scaledBlackY = Luminance(scaledBlack); + float scaledWhiteY = Luminance(scaledWhite); + +#if 0 //TODO: delete once verified this works + if (saturate(scaleDown) != scaleDown || saturate(scaleUp) != scaleUp + || saturate(scaledBlackY) != scaledBlackY || saturate(scaledWhiteY) != scaledWhiteY) { - channel = channel / 12.92f; + return 0; } - else +#endif + + float3 color = gamma_sRGB_to_linear(LUT.Load(UVW).rgb); + float3 originalColor = color; + + float blackDistance = hypot3(color); + float whiteDistance = hypot3(1.f - color); + float totalRange = (blackDistance + whiteDistance); + + float3 scaledColor = linearNormalization(color, 0.f, 1.f, scaleDown, scaleUp); + color += scaledColor; + + float YChange = linearNormalization(blackDistance, 0.f, totalRange, -scaledBlackY, 1.f - scaledWhiteY); + float Y = Luminance(color); + if (Y > 0.f) // Black will always stay black (and should) { - channel = pow((channel + 0.055f) / 1.055f, 2.4f); + float targetY = Y + YChange; + if (SDRRange && targetY >= 1.f) // Retarget to pure white + color = 1.f; + else + color *= targetY / Y; } - return channel; + + // Optional step to keep colors in the SDR range. + if (SDRRange) + color = saturate(color); + + color = lerp(originalColor, color, LUTCorrectionPercentage); + + return color; } -float3 gamma_sRGB_to_linear(float3 Color) -{ - return float3(gamma_sRGB_to_linear(Color.r), - gamma_sRGB_to_linear(Color.g), - gamma_sRGB_to_linear(Color.b)); -} +Texture2D LUT1 : register(t0, space8); +Texture2D LUT2 : register(t1, space8); +Texture2D LUT3 : register(t2, space8); +Texture2D LUT4 : register(t3, space8); +RWTexture3D outMixedLUT : register(u0, space8); -float Luminance(float3 color) +static uint3 gl_GlobalInvocationID; +struct SPIRV_Cross_Input { - // Fixed from "wrong" values: 0.2125 0.7154 0.0721f - return dot(color, float3(0.2126f, 0.7152f, 0.0722f)); -} + uint3 gl_GlobalInvocationID : SV_DispatchThreadID; +}; void CS() { const uint3 outUVW = uint3(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y, gl_GlobalInvocationID.z); // In pixels - const uint inU = (gl_GlobalInvocationID.z << 4u) + gl_GlobalInvocationID.x; - const int3 inUVW = int3(inU, gl_GlobalInvocationID.y, 0u); // In pixels + const uint inU = (gl_GlobalInvocationID.z << 4u) + gl_GlobalInvocationID.x; // 4u is (LUT_SIZE / 4) + const uint3 inUVW = uint3(inU, gl_GlobalInvocationID.y, 0u); // In pixels const float UVWScale = 1.f / (LUT_SIZE - 1.f); // Was "0.066666670143604278564453125", pixel coordinates 0-15 for a resolution of 16, which is half of LUTs size of 16x16x16 float3 neutralLUTColor = float3(outUVW) * UVWScale; // The neutral LUT is automatically generated by the coordinates, but it's baked with sRGB gamma neutralLUTColor = gamma_sRGB_to_linear(neutralLUTColor); +#if LUT_IMPROVEMENT_TYPE == 0 || LUT_IMPROVEMENT_TYPE == 2 float3 LUT1Color = LUT1.Load(inUVW); float3 LUT2Color = LUT2.Load(inUVW); float3 LUT3Color = LUT3.Load(inUVW); @@ -98,18 +203,23 @@ void CS() LUT2Color = gamma_sRGB_to_linear(LUT2Color); LUT3Color = gamma_sRGB_to_linear(LUT3Color); LUT4Color = gamma_sRGB_to_linear(LUT4Color); +#endif // LUT_IMPROVEMENT_TYPE +#if LUT_IMPROVEMENT_TYPE == 0 + // Nothing to do +#elif LUT_IMPROVEMENT_TYPE == 1 + const bool SDRRange = false; + float3 LUT1Color = PatchLUTColor(LUT1, inUVW, SDRRange); + float3 LUT2Color = PatchLUTColor(LUT3, inUVW, SDRRange); + float3 LUT3Color = PatchLUTColor(LUT3, inUVW, SDRRange); + float3 LUT4Color = PatchLUTColor(LUT4, inUVW, SDRRange); +#elif LUT_IMPROVEMENT_TYPE == 2 float neutralLUTLuminance = Luminance(neutralLUTColor); float LUT1Luminance = Luminance(LUT1Color); float LUT2Luminance = Luminance(LUT2Color); float LUT3Luminance = Luminance(LUT3Color); float LUT4Luminance = Luminance(LUT4Color); -#if LUT_IMPROVEMENT_TYPE == 0 - -#elif LUT_IMPROVEMENT_TYPE == 1 - //TODO -#elif LUT_IMPROVEMENT_TYPE == 2 if (LUT1Luminance != 0.f) LUT1Color *= lerp(1.f, neutralLUTLuminance / LUT1Luminance, LUTCorrectionPercentage); if (LUT2Luminance != 0.f) @@ -118,7 +228,7 @@ void CS() LUT3Color *= lerp(1.f, neutralLUTLuminance / LUT3Luminance, LUTCorrectionPercentage); if (LUT4Luminance != 0.f) LUT4Color *= lerp(1.f, neutralLUTLuminance / LUT4Luminance, LUTCorrectionPercentage); -#endif +#endif // LUT_IMPROVEMENT_TYPE #if !FIX_LUT_GAMMA_MAPPING && 0 // Disabled as it's still preferable to do LUT blends in linear space neutralLUTColor = gamma_linear_to_sRGB(neutralLUTColor); diff --git a/shaders/math.h b/shaders/math.h index bda85717..30db11bc 100644 --- a/shaders/math.h +++ b/shaders/math.h @@ -3,4 +3,17 @@ #define FLT_MAX 3.402823466e+38F +float hypot3(float3 input) +{ + float3 inputSquared = input * input; + return sqrt(inputSquared.x + inputSquared.y + inputSquared.z); +} + +// From old to new range +template +T linearNormalization(T input, float min, float max, float newMin, float newMax) +{ + return ((input - min) * ((newMax - newMin) / (max - min))) + newMin; +} + #endif \ No newline at end of file From 2cecef8e6a83df6cf1bd854c5d5ed5d59ba33c7e Mon Sep 17 00:00:00 2001 From: Filoppi Date: Fri, 22 Sep 2023 03:54:28 +0300 Subject: [PATCH 010/270] Optimize LUTs normalization --- .../ColorGradingMerge_FF81_cs.hlsl | 95 +++++++++++-------- 1 file changed, 53 insertions(+), 42 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index a0caeaa9..49261aca 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -5,6 +5,9 @@ // 0 None, 1 ShortFuse technique (normalization), 2 luminance preservation (doesn't look so good) #define LUT_IMPROVEMENT_TYPE 1 +// For future development +#define UNUSED_PARAMS 0 + static float additionalNeutralLUTPercentage = 0.0f; static float LUTCorrectionPercentage = 1.0f; @@ -24,18 +27,20 @@ cbuffer PushConstantWrapper_ColorGradingMerge : register(b0, space0) struct LUTAnalysis { + float minChannel; + float maxChannel; + float3 black; + float3 white; + +#if UNUSED_PARAMS float minY; float maxY; float averageY; float range; - float3 black; - float3 white; float blackY; float whiteY; - //float medianY; - float minChannel; + float medianY; float averageChannel; - float maxChannel; float averageRed; float averageGreen; float averageBlue; @@ -45,77 +50,84 @@ struct LUTAnalysis float minRed; float minGreen; float minBlue; +#endif // UNUSED_PARAMS }; +// In/Out in pixels +uint3 ThreeToTwoDimensionCoordinates(uint x, uint y, uint z) +{ + const uint U = (z << 4u) + x; // 4u is (LUT_SIZE / 4) + const uint3 UVW = uint3(U, y, 0u); + return UVW; +} + void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) { + Analysis.black = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(0, 0, 0)).rgb); + Analysis.white = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(1, 1, 1)).rgb); +#if UNUSED_PARAMS Analysis.minY = FLT_MAX; - Analysis.maxY = 0.f; - Analysis.minRed = FLT_MAX; - Analysis.minBlue = FLT_MAX; - Analysis.minGreen = FLT_MAX; - Analysis.maxRed = 0.f; - Analysis.maxBlue = 0.f; - Analysis.maxGreen = 0.f; - Analysis.black = gamma_sRGB_to_linear(LUT.Load(int3(0, 0, 0)).rgb); - Analysis.white = gamma_sRGB_to_linear(LUT.Load(int3(1, 1, 1)).rgb); + Analysis.maxY = -FLT_MAX; Analysis.blackY = Luminance(Analysis.black); Analysis.whiteY = Luminance(Analysis.white); +#endif // UNUSED_PARAMS - float reds = 0.f; - float greens = 0.f; - float blues = 0.f; + float3 colors = 0.f; float Ys = 0.f; + float3 minColor = FLT_MAX; + float3 maxColor = -FLT_MAX; + const uint texelCount = LUT_SIZE * LUT_SIZE * LUT_SIZE; + //TODO: optimize, to get the min/max colors we could just iterate on the last (e.g.) 3 pixels of each axis for (uint x = 0; x < (uint)LUT_SIZE; x++) { for (uint y = 0; y < (uint)LUT_SIZE; y++) { for (uint z = 0; z < (uint)LUT_SIZE; z++) { - const uint inU = (z << 4u) + x; - const uint3 inUVW = uint3(inU, y, 0u); // In pixels - float3 LUTColor = gamma_sRGB_to_linear(LUT.Load(inUVW).rgb); + float3 LUTColor = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(x, y, z)).rgb); + + minColor = min(minColor, LUTColor); + maxColor = max(maxColor, LUTColor); +#if UNUSED_PARAMS float Y = Luminance(LUTColor); Analysis.minY = min(Analysis.minY, Y); Analysis.maxY = max(Analysis.maxY, Y); - - Analysis.minRed = min(Analysis.minRed, LUTColor.r); - Analysis.minGreen = min(Analysis.minGreen, LUTColor.g); - Analysis.minBlue = min(Analysis.minBlue, LUTColor.b); - - Analysis.maxRed = max(Analysis.maxRed, LUTColor.r); - Analysis.maxGreen = max(Analysis.maxGreen, LUTColor.g); - Analysis.maxBlue = max(Analysis.maxBlue, LUTColor.b); - - reds += LUTColor.r; - greens += LUTColor.g; - blues += LUTColor.b; Ys += Y; + + colors += LUTColor.r; +#endif // UNUSED_PARAMS } } } //TODO: either store min/max channels merged or separately, but not both - Analysis.minChannel = max(Analysis.minRed, max(Analysis.minGreen, Analysis.minBlue)); - Analysis.maxChannel = max(Analysis.maxRed, max(Analysis.maxGreen, Analysis.maxBlue)); + Analysis.minChannel = max(minColor.r, max(minColor.g, minColor.b)); + Analysis.maxChannel = max(maxColor.r, max(maxColor.g, maxColor.b)); + +#if UNUSED_PARAMS + Analysis.minRed = minRed; + Analysis.minGreen = minGreen; + Analysis.minBlue = minBlue; + Analysis.maxRed = maxRed; + Analysis.maxGreen = maxGreen; + Analysis.maxBlue = maxBlue; Analysis.averageY = Ys / texelCount; - Analysis.averageRed = reds / texelCount; - Analysis.averageGreen = greens / texelCount; - Analysis.averageBlue = blues / texelCount; + Analysis.averageRed = colors.r / texelCount; + Analysis.averageGreen = colors.g / texelCount; + Analysis.averageBlue = colors.b / texelCount; - //TODO: necessary? Analysis.averageChannel = (Analysis.averageRed + Analysis.averageGreen + Analysis.averageBlue) / 3.f; - //TODO: unncessary? Analysis.range = Analysis.maxY - Analysis.minY; //TODO: ??? Can't do this in shader but it's not needed //Ys.sort((a, b) => a - b); //Analysis.medianY = ((Ys[Math.floor(texelCount / 2)]) + (Ys[Math.ceil(texelCount / 2)])) / 2; +#endif // UNUSED_PARAMS } // Analyzes each LUT texel and normalize their range. @@ -152,7 +164,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, bool SDRRange = false) float3 scaledColor = linearNormalization(color, 0.f, 1.f, scaleDown, scaleUp); color += scaledColor; - float YChange = linearNormalization(blackDistance, 0.f, totalRange, -scaledBlackY, 1.f - scaledWhiteY); + float YChange = linearNormalization(blackDistance, 0.f, totalRange, -scaledBlackY, 1.f - scaledWhiteY); float Y = Luminance(color); if (Y > 0.f) // Black will always stay black (and should) { @@ -187,8 +199,7 @@ struct SPIRV_Cross_Input void CS() { const uint3 outUVW = uint3(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y, gl_GlobalInvocationID.z); // In pixels - const uint inU = (gl_GlobalInvocationID.z << 4u) + gl_GlobalInvocationID.x; // 4u is (LUT_SIZE / 4) - const uint3 inUVW = uint3(inU, gl_GlobalInvocationID.y, 0u); // In pixels + const uint3 inUVW = ThreeToTwoDimensionCoordinates(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y, gl_GlobalInvocationID.z); const float UVWScale = 1.f / (LUT_SIZE - 1.f); // Was "0.066666670143604278564453125", pixel coordinates 0-15 for a resolution of 16, which is half of LUTs size of 16x16x16 float3 neutralLUTColor = float3(outUVW) * UVWScale; // The neutral LUT is automatically generated by the coordinates, but it's baked with sRGB gamma From d6d02ba3dbed00ca61ead2048cc1a614ccede8ea Mon Sep 17 00:00:00 2001 From: Filoppi Date: Fri, 22 Sep 2023 20:06:15 +0300 Subject: [PATCH 011/270] Fix luts correction and improve performance --- .../ColorGradingMerge_FF81_cs.hlsl | 31 ++++++++++++++----- shaders/HDRComposite/HDRComposite_ps.hlsl | 5 ++- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 49261aca..1c4a576a 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -2,9 +2,13 @@ #include "../color.h" #include "../math.h" +#define LUT_SIZE_UINT (uint)LUT_SIZE + // 0 None, 1 ShortFuse technique (normalization), 2 luminance preservation (doesn't look so good) #define LUT_IMPROVEMENT_TYPE 1 +// Make some small quality cuts for the purpose of optimization +#define OPTIMIZE_LUT_ANALYSIS true // For future development #define UNUSED_PARAMS 0 @@ -53,10 +57,13 @@ struct LUTAnalysis #endif // UNUSED_PARAMS }; +static const uint LUTSizeLog2 = (uint)log2(LUT_SIZE); + // In/Out in pixels uint3 ThreeToTwoDimensionCoordinates(uint x, uint y, uint z) { - const uint U = (z << 4u) + x; // 4u is (LUT_SIZE / 4) + // 2D LUT extends horizontally. + const uint U = (z << LUTSizeLog2) + x; const uint3 UVW = uint3(U, y, 0u); return UVW; } @@ -64,7 +71,7 @@ uint3 ThreeToTwoDimensionCoordinates(uint x, uint y, uint z) void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) { Analysis.black = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(0, 0, 0)).rgb); - Analysis.white = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(1, 1, 1)).rgb); + Analysis.white = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(LUT_SIZE_UINT - 1u, LUT_SIZE_UINT - 1u, LUT_SIZE_UINT - 1u)).rgb); #if UNUSED_PARAMS Analysis.minY = FLT_MAX; Analysis.maxY = -FLT_MAX; @@ -78,14 +85,21 @@ void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) float3 minColor = FLT_MAX; float3 maxColor = -FLT_MAX; - const uint texelCount = LUT_SIZE * LUT_SIZE * LUT_SIZE; + const uint texelCount = LUT_SIZE_UINT * LUT_SIZE_UINT * LUT_SIZE_UINT; - //TODO: optimize, to get the min/max colors we could just iterate on the last (e.g.) 3 pixels of each axis - for (uint x = 0; x < (uint)LUT_SIZE; x++) + // TODO: this loops on every LUT pixel for every output pixel. + // given we only need the min/max color channels here, to optimize we could: + // -Just iterate on the last (e.g.) 3 pixels of each axis, which should guarantee to find the max for all normal LUTs. + // According to ShortFuse some LUTs don't have their min and max in the edges, but it could be in the middle or close to it. + // It's arguable we'd care about this cases, and it's also arguable to correct them by global min instead of edges min, as the result could actually be worse? + // -Work with atomic (shared variables), group sync and thread/thread group to only calculate this once. + // -Add a new post LUT mixing compute shader pass that does this once. + const uint analyzeTexelFrequency = OPTIMIZE_LUT_ANALYSIS ? (LUT_SIZE_UINT - 1u) : 1u; // Set to 1 for all. Set to "LUT_SIZE_UINT - 1u" for edges only. + for (uint x = 0; x < LUT_SIZE_UINT; x += analyzeTexelFrequency) { - for (uint y = 0; y < (uint)LUT_SIZE; y++) + for (uint y = 0; y < LUT_SIZE_UINT; y += analyzeTexelFrequency) { - for (uint z = 0; z < (uint)LUT_SIZE; z++) + for (uint z = 0; z < LUT_SIZE_UINT; z += analyzeTexelFrequency) { float3 LUTColor = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(x, y, z)).rgb); @@ -267,7 +281,8 @@ void CS() outMixedLUT[outUVW] = float4(mixedLUT, 1.f); } -[numthreads(16, 16, 1)] +// Dispatch size is 1 1 16 (x and y have one thread and one thread group, while z has 16 thread groups with a thread each) +[numthreads(LUT_SIZE_UINT, LUT_SIZE_UINT, 1u)] void main(SPIRV_Cross_Input stage_input) { gl_GlobalInvocationID = stage_input.gl_GlobalInvocationID; diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 87fd5c46..5269e037 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -217,7 +217,7 @@ struct PSOutput #define FLT11_MAX 65024.f #define FLT16_MAX 65504.f -static const float LUTStrength = 0.f; +static const float LUTStrength = 1.f; static const float RestorePreLUTLuminance = 1.f; static const float ACES_a = 2.51f; @@ -695,8 +695,7 @@ PSOutput PS(PSInput psInput) #endif // FIX_LUT_GAMMA_MAPPING float LUTMaskAlpha = saturate(LutMask.Sample(Sampler0, psInput.TEXCOORD).x + (1.f - LUTStrength)); color = lerp(LUTColor, color, LUTMaskAlpha); -#else - + #endif // APPLY_MERGED_COLOR_GRADING_LUT const float postColorCorrectionLuminance = Luminance(color); From a5d7f5495701700dd181a7bb713c54378952e429 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Fri, 22 Sep 2023 21:03:21 +0300 Subject: [PATCH 012/270] First version of DICE like tonemapper --- shaders/HDRComposite/HDRComposite_ps.hlsl | 39 +++++++++++++++++++---- shaders/color.h | 29 +++++++++++++++++ shaders/math.h | 38 ++++++++++++++++++++++ shaders/shared.h | 6 +++- 4 files changed, 104 insertions(+), 8 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 5269e037..02968ed1 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -1,5 +1,6 @@ #include "../shared.h" #include "../color.h" +#include "../math.h" // These are defined at compile time (shaders permutations) //#define APPLY_BLOOM @@ -12,8 +13,8 @@ // Also sets "DISABLE_LUT", "DISABLE_POST_PROCESS" and "DISABLE_INVERSE_TONEMAP" #define DISABLE_TONEMAP 0 #define DISABLE_POST_PROCESS 0 -#define DISABLE_LUT 0 -#define DISABLE_INVERSE_TONEMAP 0 +#define DISABLE_LUT 1 +#define DISABLE_REPLACED_TONEMAP 0 #define DISABLE_INVERSE_POST_PROCESS 1 #define CLAMP_INPUT_OUTPUT 1 @@ -504,6 +505,22 @@ float3 Hable_Inverse( return itmColor * dstParams_W; } +// Tonemapper inspired from DICE. Works on luminance to maintain hue, not per channel. +float3 DICETonemap(float3 Color, float MaxOutputLuminance) +{ + // Between 0 and 1. Determines where the highlights curve (shoulder) starts. + // Leaving at zero for now as it's a simple and good looking default. + const float highlightsShoulderStart = 0.f; + + const float sourceLuminance = Luminance(Color); + if (sourceLuminance > 0.0f) + { + const float compressedLuminance = luminanceCompress(sourceLuminance, MaxOutputLuminance, highlightsShoulderStart); + Color *= compressedLuminance / sourceLuminance; + } + return Color; +} + float FindLuminanceToRestore(float tonemapLostLuminance, float preColorCorrectionLuminance, float postColorCorrectionLuminance) { // Try to restore any luminance above "color", as it would have been lost during tone mapping. @@ -704,7 +721,7 @@ PSOutput PS(PSInput psInput) // Restore any luminance beyond 1 that ended up clipped by HDR->SDR tonemappers and any subsequent image manipulation color *= FindLuminanceToRestore(tonemapLostLuminance, preColorCorrectionLuminance, postColorCorrectionLuminance); -#if !DISABLE_TONEMAP && !DISABLE_INVERSE_TONEMAP +#if !DISABLE_TONEMAP && !DISABLE_REPLACED_TONEMAP #if !DISABLE_POST_PROCESS && !DISABLE_INVERSE_POST_PROCESS //TODO: passing in the previous luminance might not be the best, though is there any other way really? @@ -712,6 +729,7 @@ PSOutput PS(PSInput psInput) color = PostProcess_Inverse(color, prePostProcessColorLuminance); #endif // !DISABLE_POST_PROCESS && !DISABLE_INVERSE_POST_PROCESS + // Restore a color very close to the original linear one, but with all the other post process and LUT transformations baked in if (PcwHdrComposite.Tmo == 1u) { color = ACESReference_Inverse(color); @@ -745,15 +763,22 @@ PSOutput PS(PSInput psInput) //TODO: ... this has no use now, and also, it won't work in the HDR range color = PostProcess(color, prePostProcessColorLuminance); #endif // !DISABLE_POST_PROCESS && !DISABLE_INVERSE_POST_PROCESS -#endif // !DISABLE_TONEMAP && !DISABLE_INVERSE_TONEMAP - + + color *= HDR_GAME_PAPER_WHITE; + + const float maxOutputLuminance = HDR_MAX_OUTPUT_NITS / WhiteNits_BT709; + //TODO: find a tonemapper that looks more like Hable? + color = DICETonemap(color, maxOutputLuminance); +#else + color *= HDR_GAME_PAPER_WHITE; + +#endif // !DISABLE_TONEMAP && !DISABLE_REPLACED_TONEMAP + #if CLAMP_INPUT_OUTPUT color = clamp(color, 0.f, FLT16_MAX); // Avoid extremely high numbers turning into NaN in FP16 #endif - //TODO: do DICE tonemapping to display nits, or some Hable like HDR tonemapper. - #else #if SDR_USE_GAMMA_2_2 //TODO: This makes sense to have if we fix up the LUTs, though the SDR tonemapper had still been developed with sRGB<->2.2 mismatch diff --git a/shaders/color.h b/shaders/color.h index d377d8aa..1a50f5dd 100644 --- a/shaders/color.h +++ b/shaders/color.h @@ -1,6 +1,35 @@ #ifndef COLOR_H #define COLOR_H +// Rec.709 SDR white is meant to be mapped to 80 nits (not 100, even if some game engine (UE) and consoles (PS5) interpret it as such). +static const float WhiteNits_BT709 = 80.f; + +// SMPTE ST 2084 (Perceptual Quantization) is only defined until this amount of nits. +// This is also the max each color channel can have in HDR10. +static const float PQMaxNits = 10000.0f; +// SMPTE ST 2084 is defined as using BT.2020 white point. +static const float PQMaxWhitePoint = PQMaxNits / WhiteNits_BT709; + +// These have been calculated to be as accurate as possible +static const float3x3 BT709_2_BT2020 = float3x3( + 0.627401924722236, 0.329291971755002, 0.0433061035227622, + 0.0690954897392608, 0.919544281267395, 0.0113602289933443, + 0.0163937090881632, 0.0880281623979006, 0.895578128513936); +static const float3x3 BT2020_2_BT709 = float3x3( + 1.66049621914783, -0.587656444131135, -0.0728397750166941, + -0.124547095586012, 1.13289510924730, -0.00834801366128445, + -0.0181536813870718, -0.100597371685743, 1.11875105307281); + +float3 BT709_To_BT2020(float3 color) +{ + return mul(BT709_2_BT2020, color); +} + +float3 BT2020_To_BT709(float3 color) +{ + return mul(BT2020_2_BT709, color); +} + float gamma_linear_to_sRGB(float channel) { [flatten] diff --git a/shaders/math.h b/shaders/math.h index 30db11bc..30b8f5db 100644 --- a/shaders/math.h +++ b/shaders/math.h @@ -2,6 +2,9 @@ #define MATH_H #define FLT_MAX 3.402823466e+38F +#define FLT10_MAX 64512.f +#define FLT11_MAX 65024.f +#define FLT16_MAX 65504.f float hypot3(float3 input) { @@ -16,4 +19,39 @@ T linearNormalization(T input, float min, float max, float newMin, float newMax) return ((input - min) * ((newMax - newMin) / (max - min))) + newMin; } +// Aplies exponential ("Photographic") luma compression. +// The pow can modulate the curve without changing the values around the edges. +float rangeCompressPow(float x, float fMax = FLT_MAX, float fPow = 1.f) +{ + // Branches are for static parameters optimizations + if (fPow == 1.f && fMax == FLT_MAX) + { + // This does e^x. We expect x to be between 0 and 1. + return 1.f - exp(-x); + } + if (fPow == 1.f && fMax != FLT_MAX) + { + const float fLostRange = exp(-fMax); + const float fRestoreRangeScale = 1.f / (1.f - fLostRange); + return (1.f - exp(-x)) * fRestoreRangeScale; + } + if (fPow != 1.f && fMax == FLT_MAX) + { + return (1.f - pow(exp(-x), fPow)); + } + const float fLostRange = pow(exp(-fMax), fPow); + const float fRestoreRangeScale = 1.f / (1.f - fLostRange); + return (1.f - pow(exp(-x), fPow)) * fRestoreRangeScale; +} + +// Refurbished DICE HDR tonemapper +float luminanceCompress(float fInValue, float fOutMaxValue, float fShoulderStart = 0.f, float fInMaxValue = FLT_MAX, float fModulationPow = 1.f) +{ + float fCompressableValue = fInValue - fShoulderStart; + float fCompressableRange = fInMaxValue - fShoulderStart; + float fCompressedRange = fOutMaxValue - fShoulderStart; + float fPossibleOutValue = fShoulderStart + fCompressedRange * rangeCompressPow(fCompressableValue / fCompressedRange, fCompressableRange / fCompressedRange, fModulationPow); + return fInValue <= fShoulderStart ? fInValue : fPossibleOutValue; +} + #endif \ No newline at end of file diff --git a/shaders/shared.h b/shaders/shared.h index 6aae532d..d75b5c2b 100644 --- a/shaders/shared.h +++ b/shaders/shared.h @@ -1,8 +1,12 @@ #ifndef SHARED_H #define SHARED_H +// Enables HDR scRGB output. If false, we output Rec.709 in gamma space (~sRGB). #define ENABLE_HDR 1 -#define HDR_GAME_PAPER_WHITE 1.f +// Set equal to the max nits your display can output +#define HDR_MAX_OUTPUT_NITS 1000.f +// Brings the range roughly from 80 nits to 200 nits (close to the target reference of 203 nits) +#define HDR_GAME_PAPER_WHITE 2.5f #define HDR_UI_PAPER_WHITE 2.5f // This possibly shifts colors a lot, but it's mathematically correct, it's also necessary for HDR LUTs to work. #define FIX_LUT_GAMMA_MAPPING 1 From f47b0c70d46b74f9bef1c35008e67669f5beb565 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Fri, 22 Sep 2023 21:55:26 +0300 Subject: [PATCH 013/270] Update to 1.7 code of LUTs normalization plugin (removed the clamps to SDR) --- .../ColorGradingMerge_FF81_cs.hlsl | 37 ++++++++++--------- shaders/HDRComposite/HDRComposite_ps.hlsl | 2 +- shaders/math.h | 2 +- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 1c4a576a..9064b616 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -35,7 +35,6 @@ struct LUTAnalysis float maxChannel; float3 black; float3 white; - #if UNUSED_PARAMS float minY; float maxY; @@ -151,16 +150,16 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, bool SDRRange = false) LUTAnalysis analysis; AnalyzeLUT(LUT, analysis); - float scaleDown = (0.f - analysis.minChannel); - float scaleUp = (1.f - analysis.maxChannel); // Black will be scaled by min channel - float3 scaledBlack = analysis.black - analysis.minChannel; + float3 scaledBlack = 1.f - ((1.f - analysis.black) * (1.f / (1.f - analysis.minChannel))); // White will be scaled up by max channel - float3 scaledWhite = analysis.white + (1.f - analysis.maxChannel); + float3 scaledWhite = analysis.white / analysis.maxChannel; float scaledBlackY = Luminance(scaledBlack); float scaledWhiteY = Luminance(scaledWhite); #if 0 //TODO: delete once verified this works + float scaleDown = (0.f - analysis.minChannel); + float scaleUp = (1.f - analysis.maxChannel); if (saturate(scaleDown) != scaleDown || saturate(scaleUp) != scaleUp || saturate(scaledBlackY) != scaledBlackY || saturate(scaledWhiteY) != scaledWhiteY) { @@ -169,24 +168,28 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, bool SDRRange = false) #endif float3 color = gamma_sRGB_to_linear(LUT.Load(UVW).rgb); - float3 originalColor = color; + const float3 originalColor = color; - float blackDistance = hypot3(color); - float whiteDistance = hypot3(1.f - color); - float totalRange = (blackDistance + whiteDistance); - - float3 scaledColor = linearNormalization(color, 0.f, 1.f, scaleDown, scaleUp); - color += scaledColor; + //TODO: convert to using lerp function??? + const float3 reducedColor = linearNormalization(color, 0.f, 1.f, 1.f / (1.f - analysis.minChannel), 1.f); + const float3 increasedColor = linearNormalization(color, 0.f, 1.f, 1.f, 1.f / analysis.maxChannel); + // Scale the color + color = (1.f - ((1.f - color) * reducedColor)) * increasedColor; + + const float blackDistance = hypot3(originalColor); + const float whiteDistance = hypot3(1.f - originalColor); + const float totalRange = (blackDistance + whiteDistance); - float YChange = linearNormalization(blackDistance, 0.f, totalRange, -scaledBlackY, 1.f - scaledWhiteY); - float Y = Luminance(color); - if (Y > 0.f) // Black will always stay black (and should) + const float sourceY = Luminance(color); + if (sourceY > 0.f) // Black will always stay black (and should) { - float targetY = Y + YChange; + const float decreasedY = linearNormalization(blackDistance, 0.f, totalRange, 1.f / (1.f - scaledBlackY), 1.f); + const float increasedY = linearNormalization(whiteDistance, 0.f, totalRange, 1.f / scaledWhiteY, 1.f); + const float targetY = (1.f - ((1.f - sourceY) * decreasedY)) * increasedY; if (SDRRange && targetY >= 1.f) // Retarget to pure white color = 1.f; else - color *= targetY / Y; + color *= max(targetY, 0.f) / sourceY; // targetY could be negative according to ShortFuse } // Optional step to keep colors in the SDR range. diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 02968ed1..575ac608 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -13,7 +13,7 @@ // Also sets "DISABLE_LUT", "DISABLE_POST_PROCESS" and "DISABLE_INVERSE_TONEMAP" #define DISABLE_TONEMAP 0 #define DISABLE_POST_PROCESS 0 -#define DISABLE_LUT 1 +#define DISABLE_LUT 0 #define DISABLE_REPLACED_TONEMAP 0 #define DISABLE_INVERSE_POST_PROCESS 1 #define CLAMP_INPUT_OUTPUT 1 diff --git a/shaders/math.h b/shaders/math.h index 30b8f5db..f0403e62 100644 --- a/shaders/math.h +++ b/shaders/math.h @@ -14,7 +14,7 @@ float hypot3(float3 input) // From old to new range template -T linearNormalization(T input, float min, float max, float newMin, float newMax) +T linearNormalization(T input, T min, T max, T newMin, T newMax) { return ((input - min) * ((newMax - newMin) / (max - min))) + newMin; } From aafa7c62d5511aaa07f7c6d960f5b7850da6e20c Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 23 Sep 2023 02:59:09 +0300 Subject: [PATCH 014/270] Fix LUTs --- .../ColorGradingMerge_FF81_cs.hlsl | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 9064b616..c41fd2f4 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -145,7 +145,7 @@ void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) // Analyzes each LUT texel and normalize their range. // Slow but effective. -float3 PatchLUTColor(Texture2D LUT, uint3 UVW, bool SDRRange = false) +float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, bool SDRRange = false) { LUTAnalysis analysis; AnalyzeLUT(LUT, analysis); @@ -173,8 +173,8 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, bool SDRRange = false) //TODO: convert to using lerp function??? const float3 reducedColor = linearNormalization(color, 0.f, 1.f, 1.f / (1.f - analysis.minChannel), 1.f); const float3 increasedColor = linearNormalization(color, 0.f, 1.f, 1.f, 1.f / analysis.maxChannel); - // Scale the color - color = (1.f - ((1.f - color) * reducedColor)) * increasedColor; + // Scale the color ("neutralLUTColor" here represents the coordinates of the point). + color = (1.f - ((1.f - neutralLUTColor) * reducedColor)) * increasedColor; const float blackDistance = hypot3(originalColor); const float whiteDistance = hypot3(1.f - originalColor); @@ -217,9 +217,8 @@ void CS() { const uint3 outUVW = uint3(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y, gl_GlobalInvocationID.z); // In pixels const uint3 inUVW = ThreeToTwoDimensionCoordinates(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y, gl_GlobalInvocationID.z); - const float UVWScale = 1.f / (LUT_SIZE - 1.f); // Was "0.066666670143604278564453125", pixel coordinates 0-15 for a resolution of 16, which is half of LUTs size of 16x16x16 - float3 neutralLUTColor = float3(outUVW) * UVWScale; // The neutral LUT is automatically generated by the coordinates, but it's baked with sRGB gamma + float3 neutralLUTColor = float3(outUVW) / (LUT_SIZE - 1.f); // The neutral LUT is automatically generated by the coordinates, but it's baked with sRGB gamma neutralLUTColor = gamma_sRGB_to_linear(neutralLUTColor); #if LUT_IMPROVEMENT_TYPE == 0 || LUT_IMPROVEMENT_TYPE == 2 @@ -237,10 +236,10 @@ void CS() // Nothing to do #elif LUT_IMPROVEMENT_TYPE == 1 const bool SDRRange = false; - float3 LUT1Color = PatchLUTColor(LUT1, inUVW, SDRRange); - float3 LUT2Color = PatchLUTColor(LUT3, inUVW, SDRRange); - float3 LUT3Color = PatchLUTColor(LUT3, inUVW, SDRRange); - float3 LUT4Color = PatchLUTColor(LUT4, inUVW, SDRRange); + float3 LUT1Color = PatchLUTColor(LUT1, inUVW, neutralLUTColor, SDRRange); + float3 LUT2Color = PatchLUTColor(LUT3, inUVW, neutralLUTColor, SDRRange); + float3 LUT3Color = PatchLUTColor(LUT3, inUVW, neutralLUTColor, SDRRange); + float3 LUT4Color = PatchLUTColor(LUT4, inUVW, neutralLUTColor, SDRRange); #elif LUT_IMPROVEMENT_TYPE == 2 float neutralLUTLuminance = Luminance(neutralLUTColor); float LUT1Luminance = Luminance(LUT1Color); From 308d5238fc17b809933749445306b33d052b2c72 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Fri, 22 Sep 2023 21:46:48 -0400 Subject: [PATCH 015/270] fix(luts): fix tint removal Use original neutral shape to compute color loss from clamping. Use XYZ coordinates to compute % Y change per texel. --- .../ColorGradingMerge_FF81_cs.hlsl | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index c41fd2f4..777dc617 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -171,13 +171,13 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b const float3 originalColor = color; //TODO: convert to using lerp function??? - const float3 reducedColor = linearNormalization(color, 0.f, 1.f, 1.f / (1.f - analysis.minChannel), 1.f); - const float3 increasedColor = linearNormalization(color, 0.f, 1.f, 1.f, 1.f / analysis.maxChannel); + const float3 reducedColor = linearNormalization(neutralLUTColor, 0.f, 1.f, 1.f / (1.f - analysis.minChannel), 1.f); + const float3 increasedColor = linearNormalization(neutralLUTColor, 0.f, 1.f, 1.f, 1.f / analysis.maxChannel); // Scale the color ("neutralLUTColor" here represents the coordinates of the point). - color = (1.f - ((1.f - neutralLUTColor) * reducedColor)) * increasedColor; + color = (1.f - ((1.f - color) * reducedColor)) * increasedColor; - const float blackDistance = hypot3(originalColor); - const float whiteDistance = hypot3(1.f - originalColor); + const float blackDistance = hypot3(neutralLUTColor); + const float whiteDistance = hypot3(1.f - neutralLUTColor); const float totalRange = (blackDistance + whiteDistance); const float sourceY = Luminance(color); @@ -186,10 +186,14 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b const float decreasedY = linearNormalization(blackDistance, 0.f, totalRange, 1.f / (1.f - scaledBlackY), 1.f); const float increasedY = linearNormalization(whiteDistance, 0.f, totalRange, 1.f / scaledWhiteY, 1.f); const float targetY = (1.f - ((1.f - sourceY) * decreasedY)) * increasedY; - if (SDRRange && targetY >= 1.f) // Retarget to pure white - color = 1.f; - else - color *= max(targetY, 0.f) / sourceY; // targetY could be negative according to ShortFuse + if (targetY >= 0.9999f && targetY <= 1.0005f) { + color = 1.f; // Intentionally targeting pure white (1,1,1) + } else if (SDRRange && targetY >= 0.999f) { + color = 1.f; // Clamp + } else { + // targetY could be on LUTs (raised black point and dark blues) + color *= max(targetY, 0.f) / sourceY; + } } // Optional step to keep colors in the SDR range. From 7a4881887eb6caa057c54c4e442c0354ca33e94d Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Sat, 23 Sep 2023 00:17:55 -0400 Subject: [PATCH 016/270] fix(luts): undeepfried blues --- shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 777dc617..8e518443 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -118,7 +118,7 @@ void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) } //TODO: either store min/max channels merged or separately, but not both - Analysis.minChannel = max(minColor.r, max(minColor.g, minColor.b)); + Analysis.minChannel = min(minColor.r, min(minColor.g, minColor.b)); Analysis.maxChannel = max(maxColor.r, max(maxColor.g, maxColor.b)); #if UNUSED_PARAMS @@ -194,6 +194,8 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b // targetY could be on LUTs (raised black point and dark blues) color *= max(targetY, 0.f) / sourceY; } + } else { + color = 0; // Color may have gone below 0 when scaling } // Optional step to keep colors in the SDR range. From 826ebbec778155568f7f860b3d03d6422c7459e3 Mon Sep 17 00:00:00 2001 From: ersh1 <16157893+ersh1@users.noreply.github.com> Date: Sat, 23 Sep 2023 14:22:11 +0200 Subject: [PATCH 017/270] added sfse plugin source --- .gitignore | 7 + .gitmodules | 6 + Plugin/!update.ps1 | 263 ++++++++++++++++++ Plugin/.clang-format | 101 +++++++ Plugin/CMakeLists.txt | 144 ++++++++++ Plugin/CMakePresets.json | 60 ++++ Plugin/cmake/Plugin.h.in | 12 + Plugin/cmake/build_stl_modules.props | 8 + Plugin/cmake/common.cmake | 44 +++ Plugin/cmake/version.rc.in | 32 +++ Plugin/dist/rules/!base.json | 16 ++ Plugin/dist/rules/bin_copy.json | 16 ++ Plugin/dist/rules/config_copy.json | 23 ++ Plugin/dist/rules/publish_package.json | 25 ++ Plugin/include/SFSE/Stub.h | 258 +++++++++++++++++ Plugin/src/Hooks.cpp | 97 +++++++ Plugin/src/Hooks.h | 131 +++++++++ Plugin/src/Offsets.h | 35 +++ Plugin/src/PCH.h | 115 ++++++++ Plugin/src/RE/Buffers.h | 365 +++++++++++++++++++++++++ Plugin/src/Settings.cpp | 20 ++ Plugin/src/Settings.h | 24 ++ Plugin/src/Utils.cpp | 162 +++++++++++ Plugin/src/Utils.h | 11 + Plugin/src/main.cpp | 50 ++++ Plugin/vcpkg.json | 14 + build-release.ps1 | 3 + extern/DKUtil | 1 + extern/SFSE | 1 + generate-sln.ps1 | 2 + 30 files changed, 2046 insertions(+) create mode 100644 .gitmodules create mode 100644 Plugin/!update.ps1 create mode 100644 Plugin/.clang-format create mode 100644 Plugin/CMakeLists.txt create mode 100644 Plugin/CMakePresets.json create mode 100644 Plugin/cmake/Plugin.h.in create mode 100644 Plugin/cmake/build_stl_modules.props create mode 100644 Plugin/cmake/common.cmake create mode 100644 Plugin/cmake/version.rc.in create mode 100644 Plugin/dist/rules/!base.json create mode 100644 Plugin/dist/rules/bin_copy.json create mode 100644 Plugin/dist/rules/config_copy.json create mode 100644 Plugin/dist/rules/publish_package.json create mode 100644 Plugin/include/SFSE/Stub.h create mode 100644 Plugin/src/Hooks.cpp create mode 100644 Plugin/src/Hooks.h create mode 100644 Plugin/src/Offsets.h create mode 100644 Plugin/src/PCH.h create mode 100644 Plugin/src/RE/Buffers.h create mode 100644 Plugin/src/Settings.cpp create mode 100644 Plugin/src/Settings.h create mode 100644 Plugin/src/Utils.cpp create mode 100644 Plugin/src/Utils.h create mode 100644 Plugin/src/main.cpp create mode 100644 Plugin/vcpkg.json create mode 100644 build-release.ps1 create mode 160000 extern/DKUtil create mode 160000 extern/SFSE create mode 100644 generate-sln.ps1 diff --git a/.gitignore b/.gitignore index 663293b0..ecbd4425 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,9 @@ +[Bb]uild/ +.vs/ +.vscode/ +*.zip +*.timestamp +deploy-*.ps1 + shaders/target_folder.txt tools/dxc_analysis_tools/dndxc.settings.xml diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..d25bdedf --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "extern/DKUtil"] + path = extern/DKUtil + url = https://github.com/gottyduke/DKUtil.git +[submodule "extern/SFSE"] + path = extern/SFSE + url = https://github.com/ianpatt/sfse.git diff --git a/Plugin/!update.ps1 b/Plugin/!update.ps1 new file mode 100644 index 00000000..1cbb5dfb --- /dev/null +++ b/Plugin/!update.ps1 @@ -0,0 +1,263 @@ +#Requires -Version 5 + +# args +param ( + [Parameter(Mandatory)][ValidateSet('SOURCEGEN', 'DISTRIBUTE')][string]$Mode = 'SOURCEGEN', + [string]$Version, + [string]$Path, + [string]$Payload +) + + +$ErrorActionPreference = "Stop" + +$Folder = $PSScriptRoot | Split-Path -Leaf +$SourceExt = @('.asm', '.c', '.cc', '.cpp', '.cxx', '.h', '.hpp', '.hxx', 'inc', '.inl', '.ixx') +$ConfigExt = @('.ini', '.json', '.toml', '.xml') +$DocsExt = @('.md') + +function Normalize-Path { + param ( + [string]$in + ) + + $out = $in -replace '\\', '/' + while ($out.Contains('//')) { + $out = $out -replace '//', '/' + } + return $out +} + +function Resolve-Files { + param ( + [Parameter(ValueFromPipeline)][string]$parent = $PSScriptRoot, + [string[]]$range = @('include', 'src', 'test') + ) + + process { + Push-Location $PSScriptRoot + $_generated = [System.Collections.ArrayList]::new() + + try { + foreach ($directory in $range) { + if (!$env:RebuildInvoke) { + Write-Host "`t[$parent/$directory]" + } + + Get-ChildItem "$parent/$directory" -Recurse -File -ErrorAction SilentlyContinue | Where-Object { + ($_.Extension -in ($SourceExt + $DocsExt)) -and + ($_.Name -notmatch 'Plugin.h|Version.h') + } | Resolve-Path -Relative | ForEach-Object { + if (!$env:RebuildInvoke) { + Write-Host "`t`t<$_>" + } + $_generated.Add("`n`t`"$(Normalize-Path $_.Substring(2))`"") | Out-Null + } + } + + Get-ChildItem "$parent/dist" -Exclude "rules" | Get-ChildItem -File -Recurse -ErrorAction SilentlyContinue | Where-Object { + ($_.Extension -in ($ConfigExt + $DocsExt)) -and + ($_.Name -notmatch 'cmake|vcpkg') + } | Resolve-Path -Relative | ForEach-Object { + if (!$env:RebuildInvoke) { + Write-Host "`t`t<$_>" + } + $_generated.Add("`n`t`"$(Normalize-Path $_.Substring(2))`"") | Out-Null + } + } + finally { + Pop-Location + } + + return $_generated + } +} + + +Write-Host "`n`t<$Folder> [$Mode]" + + +# @@SOURCEGEN +if ($Mode.ToUpper() -eq 'SOURCEGEN') { + Write-Host "`tGenerating CMake sourcelist..." + Remove-Item "$Path/sourcelist.cmake" -Force -Confirm:$false -ErrorAction Ignore + + $generated = 'set(SOURCES' + $generated += $PSScriptRoot | Resolve-Files + if ($Path) { + $generated += $Path | Resolve-Files + } + $generated += "`n)" + [IO.File]::WriteAllText("$Path/sourcelist.cmake", $generated) +} + +$RuleVarTbl = @{ + config = 'debug'; + cmake_output = Normalize-Path ($Path + '/'); + dist = Normalize-Path "$PSScriptRoot/dist/"; + project_name = $Payload; + project_version = $Version; +} +$RuleCmds = @() + +function Resolve-RuleVar { + param ( + [string]$Path + ) + + $Path = $Path.Trim(' ') + $any = [regex]::Matches($Path, '\{.*?\}').Value + $Resolved = $Path + + # env: + foreach ($unset in $any) { + $inner = $unset.Trim('{', '}') + if ($inner.StartsWith('env:')) { + $Resolved = $Resolved -replace $unset, [System.Environment]::GetEnvironmentVariable($inner.TrimStart('env:')) + } + else { + $Resolved = if ($script:RuleVarTbl.Contains($inner)) { + $Resolved -replace $unset, $script:RuleVarTbl[$inner] + } + else { + $Resolved -replace $unset, $inner + } + } + } + + return Normalize-Path $Resolved.Trim('{', '}') +} + +function Resolve-Rules { + param ( + [Object]$Deployee + ) + + switch ($Deployee.action) { + "base" { + $script:RuleVarTbl[($Deployee.params[0].Trim('{', '}'))] = Resolve-RuleVar $Deployee.params[1] + #"setting $($Deployee.params[0]) to $(Resolve-RuleVar $Deployee.params[1])" + break + } + "copy" { + $source = $(Resolve-RuleVar $Deployee.params[0]) + $destination = $(Resolve-RuleVar $Deployee.params[1]) + $dest_path = $destination.Substring(0, $destination.LastIndexOf('/') + 1) + $script:RuleCmds += "New-Item `'$dest_path`' -ItemType Directory -Force -ErrorAction:SilentlyContinue" + $script:RuleCmds += "Copy-Item `'$source`' `'$destination`' -Force -Recurse -ErrorAction:SilentlyContinue" + break + } + "copy_if" { + $source = $(Resolve-RuleVar $Deployee.params[0]) + $destination = $(Resolve-RuleVar $Deployee.params[1]) + $dest_path = $destination.Substring(0, $destination.LastIndexOf('/') + 1) + $script:RuleCmds += "if (Test-Path `'$source`') {" + $script:RuleCmds += "New-Item `'$dest_path`' -ItemType Directory -Force -ErrorAction:SilentlyContinue" + $script:RuleCmds += "Copy-Item `'$source`' `'$destination`' -Force -Recurse -ErrorAction:SilentlyContinue }" + break + } + "package" { + $source_tree = @() + foreach ($source in $Deployee.params[0..($Deployee.params.Length - 2)]) { + $source_tree += , "`'$(Resolve-RuleVar $source)`'" + } + $script:RuleCmds += "Compress-Archive -Path $($source_tree -join ',') -DestinationPath `'$(Resolve-RuleVar $Deployee.params[-1])`' -Force -ErrorAction:SilentlyContinue" + break + } + "remove" { + foreach ($to_remove in $Deployee.params) { + $script:RuleCmds += "Remove-Item `'$(Resolve-RuleVar $to_remove)`' -Force -Recurse -ErrorAction:SilentlyContinue" + } + break + } + "script" { + foreach ($raw_script in $Deployee.params) { + $script:RuleCmds += $raw_script + } + break + } + default { + break + } + } +} + + +# @@DISTRIBUTE +if ($Mode.ToUpper() -eq 'DISTRIBUTE') { + if ($Version.ToUpper() -eq '-1') { + # update script to every project + Get-ChildItem "$PSScriptRoot/$Path" -Directory -Recurse | Where-Object { + $_.Name -notin @('vcpkg', 'build', '.git', '.vs') -and + (Test-Path "$_/CMakeLists.txt" -PathType Leaf) -and + (Test-Path "$_/vcpkg.json" -PathType Leaf) + } | ForEach-Object { + Write-Host "`tUpdated <$_>" + Robocopy.exe "$PSScriptRoot" "$_" '!Update.ps1' /MT /NJS /NFL /NDL /NJH | Out-Null + } + exit + } + + $Path = Normalize-Path $Path + $RuleVarTbl.config = $Path.Split('/')[-1].ToLower() + + $rules = Get-ChildItem "$($RuleVarTbl.dist)/rules" -File *.json + $rule_timestamp = @( "!update : $((Get-Item $MyInvocation.MyCommand.Path).LastWriteTime.Ticks)" ) + + # generate timestamp & distribution step + foreach ($rule in $rules) { + $rule_timestamp += , "$($rule.BaseName) : $($rule.LastWriteTime.Ticks)" + } + + $regenerate = $true + $timestamp = "$($RuleVarTbl.dist)/rules/$($RuleVarTbl.project_version).timestamp" + Write-Host "`tChecking deploy rules..." + + if (Test-Path "$($RuleVarTbl.dist)/deploy.ps1") { + $rule_timestamp += , "deployer : $((Get-Item "$($RuleVarTbl.dist)/deploy.ps1").LastWriteTime.Ticks)" + } + + if (Test-Path $timestamp) { + $old_timestamp = [IO.File]::ReadAllText($timestamp) + + if ($old_timestamp -eq ($rule_timestamp | Out-String)) { + $regenerate = $false + Write-Host "`t...No changes" + } + else { + Write-Host "`t...Pending changes" + } + } + + # parse all rules + if ($regenerate) { + Write-Host "`tRegenerating deploy rules..." + + foreach ($rule in $rules) { + $deployer = [IO.File]::ReadAllText($rule.FullName) | ConvertFrom-Json + + foreach ($deployee in $deployer) { + if (("config" -in $deployee.PSObject.Properties.Name) -and ($deployee.config -ne $RuleVarTbl.config)) { + continue + } + else { + Resolve-Rules $deployee + } + } + } + + $RuleCmds | Out-File "$($RuleVarTbl.dist)/deploy-$($RuleVarTbl.config.ToLower()).ps1" utf8 + + $rule_timestamp[-1] = "deployer : $((Get-Item "$($RuleVarTbl.dist)/deploy-$($RuleVarTbl.config.ToLower()).ps1").LastWriteTime.Ticks)" + Remove-Item "$($RuleVarTbl.dist)/rules/*.timestamp" -Force -ErrorAction:SilentlyContinue | Out-Null + $rule_timestamp | Out-File $timestamp utf8 + + Write-Host "`t...Ok" + } + + # deploy + Write-Host "`tExecuting deploy rules..." + & "$($RuleVarTbl.dist)/deploy-$($RuleVarTbl.config.ToLower()).ps1" + + Write-Host "`t...Ok" +} diff --git a/Plugin/.clang-format b/Plugin/.clang-format new file mode 100644 index 00000000..415f78e0 --- /dev/null +++ b/Plugin/.clang-format @@ -0,0 +1,101 @@ +--- +AccessModifierOffset: -4 +AlignAfterOpenBracket: DontAlign +AlignConsecutiveAssignments: 'false' +AlignConsecutiveBitFields: 'false' +AlignConsecutiveDeclarations: 'false' +AlignConsecutiveMacros: 'false' +AlignEscapedNewlines: Left +AlignOperands: Align +AlignTrailingComments: 'true' +AllowAllArgumentsOnNextLine: 'false' +AllowAllConstructorInitializersOnNextLine: 'false' +AllowAllParametersOfDeclarationOnNextLine: 'false' +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: 'false' +AllowShortEnumsOnASingleLine: 'true' +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: 'true' +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: 'true' +AlwaysBreakTemplateDeclarations: 'Yes' +BinPackArguments: 'true' +BinPackParameters: 'true' +BitFieldColonSpacing: After +BraceWrapping: + AfterCaseLabel: 'true' + AfterClass: 'true' + AfterControlStatement: 'false' + AfterEnum: 'true' + AfterFunction: 'true' + AfterNamespace: 'true' + AfterStruct: 'true' + AfterUnion: 'true' + AfterExternBlock: 'true' + BeforeCatch: 'false' + BeforeElse: 'false' + BeforeLambdaBody: 'false' + BeforeWhile: 'false' + IndentBraces: 'false' + SplitEmptyFunction: 'false' + SplitEmptyRecord: 'false' + SplitEmptyNamespace: 'false' +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakBeforeTernaryOperators: 'false' +BreakConstructorInitializers: AfterColon +BreakInheritanceList: AfterColon +BreakStringLiterals: 'true' +ColumnLimit: 0 +CompactNamespaces: 'false' +ConstructorInitializerAllOnOneLineOrOnePerLine: 'false' +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: 'false' +DeriveLineEnding: 'true' +DerivePointerAlignment: 'false' +DisableFormat: 'false' +FixNamespaceComments: 'false' +IncludeBlocks: Preserve +IndentCaseBlocks: 'true' +IndentCaseLabels: 'false' +IndentExternBlock: Indent +IndentGotoLabels: 'false' +IndentPPDirectives: AfterHash +IndentWidth: 4 +IndentWrappedFunctionNames: 'true' +KeepEmptyLinesAtTheStartOfBlocks: 'false' +Language: Cpp +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: All +PointerAlignment: Left +ReflowComments : 'false' +SortIncludes: 'true' +SortUsingDeclarations: 'true' +SpaceAfterCStyleCast: 'false' +SpaceAfterLogicalNot: 'false' +SpaceAfterTemplateKeyword: 'true' +SpaceBeforeAssignmentOperators: 'true' +SpaceBeforeCpp11BracedList: 'false' +SpaceBeforeCtorInitializerColon: 'true' +SpaceBeforeInheritanceColon: 'true' +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: 'true' +SpaceBeforeSquareBrackets: 'false' +SpaceInEmptyBlock: 'false' +SpaceInEmptyParentheses: 'false' +SpacesBeforeTrailingComments: 2 +SpacesInAngles: 'false' +SpacesInCStyleCastParentheses: 'false' +SpacesInConditionalStatement: 'false' +SpacesInContainerLiterals: 'true' +SpacesInParentheses: 'false' +SpacesInSquareBrackets: 'false' +Standard: Latest +TabWidth: 4 +UseCRLF: 'true' +UseTab: AlignWithSpaces + +... diff --git a/Plugin/CMakeLists.txt b/Plugin/CMakeLists.txt new file mode 100644 index 00000000..7dee6202 --- /dev/null +++ b/Plugin/CMakeLists.txt @@ -0,0 +1,144 @@ +cmake_minimum_required(VERSION 3.21) + +# info +project( + NativeHDR + VERSION 5.1.1 + LANGUAGES CXX +) + +# boiler +include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/common.cmake) + +# in-place configuration +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Plugin.h.in + ${CMAKE_CURRENT_BINARY_DIR}/include/Plugin.h + @ONLY +) + +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/cmake/version.rc.in + ${CMAKE_CURRENT_BINARY_DIR}/version.rc + @ONLY +) + +# source files +execute_process(COMMAND powershell -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_SOURCE_DIR}/!update.ps1" "SOURCEGEN" "${PROJECT_VERSION}" "${CMAKE_CURRENT_BINARY_DIR}") +include(${CMAKE_CURRENT_BINARY_DIR}/sourcelist.cmake) +source_group( + TREE ${CMAKE_CURRENT_SOURCE_DIR} + FILES ${SOURCES} +) + +source_group( + TREE ${CMAKE_CURRENT_BINARY_DIR} + FILES ${CMAKE_CURRENT_BINARY_DIR}/include/Plugin.h +) + +# dependencies +find_package(spdlog CONFIG REQUIRED) +find_dependency_path(DKUtil include/DKUtil/Logger.hpp) +find_dependency_path(SFSE sfse/sfse.cpp) + +# +target_compile_options( + sfse + PRIVATE + /WX- +) +target_compile_options( + sfse_common + PRIVATE + /WX- +) + +# cmake target +# runtime +add_library( + ${PROJECT_NAME} + SHARED + ${SOURCES} + ${CMAKE_CURRENT_BINARY_DIR}/include/Plugin.h + ${CMAKE_CURRENT_BINARY_DIR}/version.rc + .clang-format + vcpkg.json +) + +# include dir +target_include_directories( + ${PROJECT_NAME} + PRIVATE + ${CMAKE_CURRENT_BINARY_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +# linkage +target_link_libraries( + ${PROJECT_NAME} + PRIVATE + DKUtil::DKUtil + sfse::sfse_common + spdlog::spdlog +) + +# compiler def +if (MSVC) + add_compile_definitions(_UNICODE) + + target_compile_options( + ${PROJECT_NAME} + PRIVATE + /MP + /await + /W0 + /WX + /permissive- + /utf-8 + /Zc:alignedNew + /Zc:auto + /Zc:__cplusplus + /Zc:externC + /Zc:externConstexpr + /Zc:forScope + /Zc:hiddenFriend + /Zc:implicitNoexcept + /Zc:lambda + /Zc:noexceptTypes + /Zc:preprocessor + /Zc:referenceBinding + /Zc:rvalueCast + /Zc:sizedDealloc + /Zc:strictStrings + /Zc:ternary + /Zc:threadSafeInit + /Zc:trigraphs + /Zc:wchar_t + /wd4200 # nonstandard extension used : zero-sized array in struct/union + /FI${CMAKE_BINARY_DIR}/CMakeFiles/${PROJECT_NAME}.dir/$/cmake_pch.hxx + ) +endif() + +# PCH +target_precompile_headers( + ${PROJECT_NAME} + PRIVATE + src/PCH.h +) + +set_property( + TARGET + ${PROJECT_NAME} + PROPERTY VS_USER_PROPS + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/build_stl_modules.props" +) + +# update deployments +add_custom_command( + TARGET + ${PROJECT_NAME} + POST_BUILD + COMMAND powershell -NoProfile -ExecutionPolicy Bypass -File + "${CMAKE_CURRENT_SOURCE_DIR}/!update.ps1" "DISTRIBUTE" "${PROJECT_VERSION}" "${CMAKE_CURRENT_BINARY_DIR}/$(ConfigurationName)" "${PROJECT_NAME}" +) diff --git a/Plugin/CMakePresets.json b/Plugin/CMakePresets.json new file mode 100644 index 00000000..e071e0a7 --- /dev/null +++ b/Plugin/CMakePresets.json @@ -0,0 +1,60 @@ +{ + "version": 3, + "cmakeMinimumRequired": { + "major": 3, + "minor": 21, + "patch": 0 + }, + "configurePresets": [ + { + "name": "common", + "hidden": true, + "cacheVariables": { + "PluginAuthor": "$env{SKSEPluginAuthor}" + } + }, + { + "name": "vcpkg", + "hidden": true, + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": { + "type": "STRING", + "value": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" + }, + "VCPKG_TARGET_TRIPLET": "x64-windows-static-md" + } + }, + { + "name": "win64", + "hidden": true, + "architecture": "x64", + "cacheVariables": { + "CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreaded$<$:Debug>DLL" + } + }, + { + "name": "msvc", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4 /WX $penv{CXXFLAGS}" + }, + "generator": "Visual Studio 17 2022", + "vendor": { + "microsoft.com/VisualStudioSettings/CMake/1.0": { + "intelliSenseMode": "windows-msvc-x64", + "enableMicrosoftCodeAnalysis": true, + "enableClangTidyCodeAnalysis": true + } + } + }, + { + "name": "REL", + "inherits": [ + "common", + "vcpkg", + "win64", + "msvc" + ] + } + ] +} \ No newline at end of file diff --git a/Plugin/cmake/Plugin.h.in b/Plugin/cmake/Plugin.h.in new file mode 100644 index 00000000..c0de7ad0 --- /dev/null +++ b/Plugin/cmake/Plugin.h.in @@ -0,0 +1,12 @@ +#pragma once + + +namespace Plugin +{ + inline constexpr auto NAME = "@PROJECT_NAME@"sv; + inline constexpr auto AUTHOR = "@PluginAuthor@"sv; + inline constexpr auto Version = + @PROJECT_VERSION_MAJOR@u * 10000 + + @PROJECT_VERSION_MINOR@u * 100 + + @PROJECT_VERSION_PATCH@u; +} diff --git a/Plugin/cmake/build_stl_modules.props b/Plugin/cmake/build_stl_modules.props new file mode 100644 index 00000000..a8184de9 --- /dev/null +++ b/Plugin/cmake/build_stl_modules.props @@ -0,0 +1,8 @@ + + + + + false + + + \ No newline at end of file diff --git a/Plugin/cmake/common.cmake b/Plugin/cmake/common.cmake new file mode 100644 index 00000000..22ece508 --- /dev/null +++ b/Plugin/cmake/common.cmake @@ -0,0 +1,44 @@ +# dependency macros +macro(find_dependency_path DEPENDENCY FILE) + # searches extern for dependencies and if not checks the environment variable + if(NOT ${DEPENDENCY} STREQUAL "") + # Check extern + message( + STATUS + "Searching for ${DEPENDENCY} using file ${FILE}" + ) + find_path("${DEPENDENCY}Path" + ${FILE} + PATHS "${CMAKE_CURRENT_SOURCE_DIR}/../extern/${DEPENDENCY}") + + if("${${DEPENDENCY}Path}" STREQUAL "${DEPENDENCY}Path-NOTFOUND") + # Check path + message( + STATUS + "Getting environment for ${DEPENDENCY}Path: $ENV{${DEPENDENCY}Path}" + ) + set("${DEPENDENCY}Path" "$ENV{${DEPENDENCY}Path}") + endif() + + message( + STATUS + "Found ${DEPENDENCY} in ${${DEPENDENCY}Path}; adding" + ) + add_subdirectory("${${DEPENDENCY}Path}" ${DEPENDENCY}) + endif() +endmacro() + + +# standards & flags +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON) +set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_DEBUG OFF) +set_property(GLOBAL PROPERTY USE_FOLDERS ON) +set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" CACHE FILEPATH "") + + +# out-of-source builds only +if(${PROJECT_SOURCE_DIR} STREQUAL ${PROJECT_BINARY_DIR}) + message(FATAL_ERROR "In-source builds are not allowed.") +endif() diff --git a/Plugin/cmake/version.rc.in b/Plugin/cmake/version.rc.in new file mode 100644 index 00000000..efbb7561 --- /dev/null +++ b/Plugin/cmake/version.rc.in @@ -0,0 +1,32 @@ +#include + +1 VERSIONINFO + FILEVERSION @PROJECT_VERSION_MAJOR@, @PROJECT_VERSION_MINOR@, @PROJECT_VERSION_PATCH@, 0 + PRODUCTVERSION @PROJECT_VERSION_MAJOR@, @PROJECT_VERSION_MINOR@, @PROJECT_VERSION_PATCH@, 0 + FILEFLAGSMASK 0x17L +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x4L + FILETYPE 0x1L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "FileDescription", "@PROJECT_NAME@" + VALUE "FileVersion", "@PROJECT_VERSION@.0" + VALUE "InternalName", "@PROJECT_NAME@" + VALUE "LegalCopyright", "MIT License" + VALUE "ProductName", "@PROJECT_NAME@" + VALUE "ProductVersion", "@PROJECT_VERSION@.0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END diff --git a/Plugin/dist/rules/!base.json b/Plugin/dist/rules/!base.json new file mode 100644 index 00000000..dafe05d4 --- /dev/null +++ b/Plugin/dist/rules/!base.json @@ -0,0 +1,16 @@ +[ + { + "action": "base", + "params": [ + "{target_base}", + "{env:SFPath}\\Data\\SFSE\\Plugins\\" + ] + }, + { + "action": "copy", + "params": [ + "{cmake_output}\\*", + "{dist}\\" + ] + } +] \ No newline at end of file diff --git a/Plugin/dist/rules/bin_copy.json b/Plugin/dist/rules/bin_copy.json new file mode 100644 index 00000000..a78daf1e --- /dev/null +++ b/Plugin/dist/rules/bin_copy.json @@ -0,0 +1,16 @@ +[ + { + "action": "copy", + "params": [ + "{dist}\\*.dll", + "{target_base}" + ] + }, + { + "action": "copy_if", + "params": [ + "{dist}\\*.pdb", + "{target_base}" + ] + } +] \ No newline at end of file diff --git a/Plugin/dist/rules/config_copy.json b/Plugin/dist/rules/config_copy.json new file mode 100644 index 00000000..16e0de00 --- /dev/null +++ b/Plugin/dist/rules/config_copy.json @@ -0,0 +1,23 @@ +[ + { + "action": "copy_if", + "params": [ + "{dist}\\*.ini", + "{target_base}" + ] + }, + { + "action": "copy_if", + "params": [ + "{dist}\\*.toml", + "{target_base}" + ] + }, + { + "action": "copy_if", + "params": [ + "{dist}\\*.json", + "{target_base}" + ] + } +] \ No newline at end of file diff --git a/Plugin/dist/rules/publish_package.json b/Plugin/dist/rules/publish_package.json new file mode 100644 index 00000000..73c362d2 --- /dev/null +++ b/Plugin/dist/rules/publish_package.json @@ -0,0 +1,25 @@ +[ + { + "action": "package", + "config": "Release", + "params": [ + "{dist}\\*.dll", + "{dist}\\*.ini", + "{dist}\\*.json", + "{dist}\\*.toml", + "{dist}\\{project_name}.v{project_version}.zip" + ] + }, + { + "action": "package", + "config": "RelWithDebInfo", + "params": [ + "{dist}\\*.dll", + "{dist}\\*.pdb", + "{dist}\\*.ini", + "{dist}\\*.json", + "{dist}\\*.toml", + "{dist}\\{project_name}.v{project_version}.zip" + ] + } +] \ No newline at end of file diff --git a/Plugin/include/SFSE/Stub.h b/Plugin/include/SFSE/Stub.h new file mode 100644 index 00000000..48ba3754 --- /dev/null +++ b/Plugin/include/SFSE/Stub.h @@ -0,0 +1,258 @@ +#pragma once + +#include "DKUtil/Hook.hpp" +#include "sfse/PluginAPI.h" +#include "sfse_common/sfse_version.h" + +#define SFSEAPI __cdecl + +// interface +namespace SFSE +{ + namespace detail + { + inline static SFSEInterface* load_storage = nullptr; + inline static SFSEMessagingInterface* messaging_storage = nullptr; + inline static SFSETrampolineInterface* trampoline_storage = nullptr; + } // namespace detail + + class LoadInterface : public dku::model::Singleton + { + public: + enum + { + kVersion = 1, + }; + + enum : std::uint32_t + { + kInvalid = 0, + kMessaging, + kTrampoline, + + kTotal, + }; + + [[nodiscard]] constexpr auto RuntimeVersion() const { return GetProxy()->runtimeVersion; } + [[nodiscard]] constexpr auto SFSEVersion() const { return GetProxy()->sfseVersion; } + [[nodiscard]] constexpr auto GetPluginHandle() const { return handle; }; + [[nodiscard]] constexpr const auto* GetPluginInfo(const char* a_name) const { return GetProxy()->GetPluginInfo(a_name); } + template + [[nodiscard]] constexpr T* QueryInterface(std::uint32_t a_id) const + { + return std::bit_cast(GetProxy()->QueryInterface(a_id)); + }; + + PluginHandle handle = static_cast(-1); + + protected: + constexpr const SFSEInterface* const GetProxy() const + { + return detail::load_storage; + } + }; + + class MessagingInterface : public dku::model::Singleton + { + public: + enum + { + kVersion = 1, + }; + + struct Message + { + const char* sender; + std::uint32_t type; + std::uint32_t dataLen; + void* data; + }; + using EventCallback = std::add_pointer_t; + + enum : std::uint32_t + { + kPostLoad, // sent to registered plugins once all plugins have been loaded (no data) + kPostPostLoad, // sent right after kPostPostLoad to facilitate the correct dispatching/registering of messages/listeners + }; + + constexpr bool Dispatch(std::uint32_t a_messageType, void* a_data, std::uint32_t a_dataLength, std::string_view a_receiver) const + { + auto success = GetProxy()->Dispatch(LoadInterface::GetSingleton()->GetPluginHandle(), a_messageType, a_data, a_dataLength, a_receiver.data()); + if (!success) { + FATAL( + "failed to dispatch message!\nexpected receiver : {}\n" + "message {{\ntype : {}\nlength : {}}}", + a_receiver, a_messageType, a_dataLength); + } + + return success; + } + + constexpr bool RegisterListener(std::string_view a_sender, EventCallback a_callback) const + { + auto success = GetProxy()->RegisterListener(LoadInterface::GetSingleton()->GetPluginHandle(), a_sender.data(), std::bit_cast(a_callback)); + if (!success) { + FATAL("failed to register messaging listener!\nexpected sender : {}\nmessage handler : {}.{:X}", + a_sender, Plugin::NAME, AsAddress(a_callback)); + } + + return success; + } + + constexpr bool RegisterListener(EventCallback a_callback) const + { + return RegisterListener("SFSE"sv, a_callback); + } + + protected: + constexpr const SFSEMessagingInterface* const GetProxy() const + { + return detail::messaging_storage; + } + }; + + class TrampolineInterface : public dku::model::Singleton + { + public: + enum + { + kVersion = 1, + }; + + [[nodiscard]] constexpr void* AllocateFromBranchPool(std::size_t a_size) const + { + return GetProxy()->AllocateFromBranchPool(LoadInterface::GetSingleton()->GetPluginHandle(), a_size); + } + + [[nodiscard]] constexpr void* AllocateFromLocalPool(std::size_t a_size) const + { + return GetProxy()->AllocateFromLocalPool(LoadInterface::GetSingleton()->GetPluginHandle(), a_size); + } + + protected: + constexpr const SFSETrampolineInterface* const GetProxy() const + { + return detail::trampoline_storage; + } + }; +} // namespace SFSE + +// stubs +namespace SFSE +{ + inline void Init(SFSEInterface* a_intfc) noexcept + { + if (!a_intfc) { + FATAL("SFSEInterface is null"sv); + } + + auto* storage = SFSE::LoadInterface::GetSingleton(); + + detail::load_storage = std::launder(a_intfc); + detail::messaging_storage = std::launder(storage->QueryInterface(LoadInterface::kMessaging)); + detail::trampoline_storage = std::launder(storage->QueryInterface(LoadInterface::kTrampoline)); + + storage->handle = detail::load_storage->GetPluginHandle(); + } + + [[nodiscard]] inline constexpr const auto* GetLoadInterface() noexcept { return SFSE::LoadInterface::GetSingleton(); } + [[nodiscard]] inline constexpr const auto* GetMessagingInterface() noexcept + { + return MessagingInterface::GetSingleton(); + } + [[nodiscard]] inline constexpr const auto* GetTrampolineInterface() noexcept + { + return TrampolineInterface::GetSingleton(); + } + + [[nodiscard]] inline auto& GetTrampoline() noexcept { return DKUtil::Hook::Trampoline::GetTrampoline(); } + + // offer SFSE reserve pool first, otherwise use local trampoline + void* AllocTrampoline(std::size_t a_size, bool a_useSFSEReserve = true) + { + auto& trampoline = GetTrampoline(); + + if (auto intfc = GetTrampolineInterface(); intfc && a_useSFSEReserve) { + auto* mem = intfc->AllocateFromBranchPool(a_size); + if (mem) { + trampoline.set_trampoline(mem, a_size); + return mem; + } else { + WARN( + "requesting allocation from SFSE branch pool failed\n" + "falling back to local trampoline"); + } + } + + auto* mem = trampoline.PageAlloc(a_size); + if (mem) { + return mem; + } else { + FATAL( + "failed to allocate any memory from either branch pool or local trampoline\n" + "this is fatal!\nSize : {}", + a_size); + std::unreachable(); + } + } +} // namespace SFSE + +// versiondata +namespace SFSE +{ + struct PluginVersionData + { + public: + enum + { + kVersion = 1, + }; + + constexpr void PluginVersion(std::uint32_t a_version) noexcept { pluginVersion = a_version; } + constexpr void PluginName(std::string_view a_plugin) noexcept { SetCharBuffer(a_plugin, std::span{ pluginName }); } + constexpr void AuthorName(std::string_view a_name) noexcept { SetCharBuffer(a_name, std::span{ author }); } + constexpr void UsesSigScanning(bool a_value) noexcept { addressIndependence = !a_value; } + constexpr void UsesAddressLibrary(bool a_value) noexcept { addressIndependence = a_value; } + constexpr void HasNoStructUse(bool a_value) noexcept { structureCompatibility = !a_value; } + constexpr void IsLayoutDependent(bool a_value) noexcept { structureCompatibility = a_value; } + constexpr void CompatibleVersions(std::initializer_list a_versions) noexcept + { + // must be zero-terminated + assert(a_versions.size() < std::size(compatibleVersions) - 1); + std::ranges::copy(a_versions, compatibleVersions); + } + constexpr void MinimumRequiredXSEVersion(std::uint32_t a_version) noexcept { xseMinimum = a_version; } + + const std::uint32_t dataVersion{ kVersion }; // shipped with xse + std::uint32_t pluginVersion = 0; // version number of your plugin + char pluginName[256] = {}; // null-terminated ASCII plugin name (please make this recognizable to users) + char author[256] = {}; // null-terminated ASCII plugin author name + std::uint32_t addressIndependence; // describe how you find your addressese using the kAddressIndependence_ enums + std::uint32_t structureCompatibility; // describe how you handle structure layout using the kStructureIndependence_ enums + std::uint32_t compatibleVersions[16] = {}; // list of compatible versions + std::uint32_t xseMinimum = 0; // minimum version of the script extender required + const std::uint32_t reservedNonBreaking = 0; // set to 0 + const std::uint32_t reservedBreaking = 0; // set to 0 + + private: + static constexpr void SetCharBuffer( + std::string_view a_src, + std::span a_dst) noexcept + { + assert(a_src.size() < a_dst.size()); + std::ranges::fill(a_dst, '\0'); + std::ranges::copy(a_src, a_dst.begin()); + } + }; + static_assert(offsetof(PluginVersionData, dataVersion) == 0x000); + static_assert(offsetof(PluginVersionData, pluginVersion) == 0x004); + static_assert(offsetof(PluginVersionData, pluginName) == 0x008); + static_assert(offsetof(PluginVersionData, author) == 0x108); + static_assert(offsetof(PluginVersionData, addressIndependence) == 0x208); + static_assert(offsetof(PluginVersionData, structureCompatibility) == 0x20C); + static_assert(offsetof(PluginVersionData, compatibleVersions) == 0x210); + static_assert(offsetof(PluginVersionData, xseMinimum) == 0x250); + static_assert(offsetof(PluginVersionData, reservedNonBreaking) == 0x254); + static_assert(offsetof(PluginVersionData, reservedBreaking) == 0x258); + static_assert(sizeof(PluginVersionData) == 0x25C); +} // namespace SFSE \ No newline at end of file diff --git a/Plugin/src/Hooks.cpp b/Plugin/src/Hooks.cpp new file mode 100644 index 00000000..8f32fa97 --- /dev/null +++ b/Plugin/src/Hooks.cpp @@ -0,0 +1,97 @@ +#include "Hooks.h" + +#include "Offsets.h" +#include "Utils.h" + +namespace Hooks +{ + RE::BufferDefinition* Patches::GetBufferFromString(std::string_view a_bufferName) + { + const auto& bufferArray = *Offsets::bufferArray; + for (const auto& bufferDefinition : bufferArray) { + if (bufferDefinition->bufferName == a_bufferName) { + return bufferDefinition; + } + } + + return nullptr; + } + + void Patches::UpgradeRenderTarget(RE::BufferDefinition* a_buffer, RE::BS_DXGI_FORMAT a_newFormat, bool a_bLimited) + { + if (!a_buffer) { + return; + } + + if (!std::strcmp(a_buffer->bufferName, "FrameBuffer") || !std::strcmp(a_buffer->bufferName, "ImageSpaceBuffer") || !std::strcmp(a_buffer->bufferName, "ScaleformCompositeBuffer")) { + INFO("Warning: {} - skipping because buffer is handled by a separate setting", a_buffer->bufferName) + return; + } + + if (a_bLimited && a_buffer->format != RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R8G8B8A8_UNORM && a_buffer->format != RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R8G8B8A8_UNORM_SRGB) { + auto formatNames = Utils::GetDXGIFormatNameMap(); + INFO("{} - skipping because format is {}", a_buffer->bufferName, formatNames[Offsets::GetDXGIFormat(a_buffer->format)]) + return; + } + + SetBufferFormat(a_buffer, a_newFormat); + } + + void Patches::SetBufferFormat(RE::BufferDefinition* a_buffer, RE::BS_DXGI_FORMAT a_newFormat) + { + if (!a_buffer) { + return; + } + + auto formatNames = Utils::GetDXGIFormatNameMap(); + INFO("{} - changing from format {} to {}", a_buffer->bufferName, formatNames[Offsets::GetDXGIFormat(a_buffer->format)], formatNames[Offsets::GetDXGIFormat(a_newFormat)]) + a_buffer->format = a_newFormat; + } + + void Patches::SetBufferFormat(RE::Buffers a_buffer, RE::BS_DXGI_FORMAT a_format) + { + const auto buffer = (*Offsets::bufferArray)[static_cast(a_buffer)]; + SetBufferFormat(buffer, a_format); + } + + void Hooks::Hook_UnkFunc(uintptr_t a1, UnkObject* a2) + { + const auto settings = Settings::Main::GetSingleton(); + switch (*settings->FrameBufferFormat) { + case 1: + a2->swapChainInterface->SetColorSpace1(DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020); + break; + case 2: + a2->swapChainInterface->SetColorSpace1(DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709); + break; + } + + return _UnkFunc(a1, a2); + } + +#ifndef NDEBUG + void DebugHooks::Hook_CreateRenderTargetView(uintptr_t a1, ID3D12Resource* a_resource, DXGI_FORMAT a_format, uint8_t a4, uint16_t a5, uintptr_t a6) + { + const auto textureDesc = a_resource->GetDesc(); + + _CreateRenderTargetView(a1, a_resource, a_format, a4, a5, a6); + } + + void DebugHooks::Hook_CreateDepthStencilView(uintptr_t a1, ID3D12Resource* a_resource, DXGI_FORMAT a_format, uint8_t a4, uint16_t a5, uintptr_t a6) + { + const auto textureDesc = a_resource->GetDesc(); + + _CreateDepthStencilView(a1, a_resource, a_format, a4, a5, a6); + } +#endif + + void Install() + { +#ifndef NDEBUG + Utils::LogBuffers(); + DebugHooks::Hook(); +#endif + Hooks::Hook(); + Patches::Patch(); + } +} diff --git a/Plugin/src/Hooks.h b/Plugin/src/Hooks.h new file mode 100644 index 00000000..d69f3743 --- /dev/null +++ b/Plugin/src/Hooks.h @@ -0,0 +1,131 @@ +#pragma once +#include "Settings.h" +#include "Utils.h" +#include "RE/Buffers.h" + +#include + +namespace Hooks +{ + class Patches + { + public: + static void Patch() + { + const auto settings = Settings::Main::GetSingleton(); + + switch (*settings->FrameBufferFormat) { + case 1: + SetBufferFormat(RE::Buffers::FrameBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R10G10B10A2_UNORM); + break; + case 2: + SetBufferFormat(RE::Buffers::FrameBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT); + break; + } + + switch (*settings->ImageSpaceBufferFormat) { + case 1: + SetBufferFormat(RE::Buffers::ImageSpaceBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R10G10B10A2_UNORM); + break; + case 2: + SetBufferFormat(RE::Buffers::ImageSpaceBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT); + break; + } + + if (*settings->UpgradeUIRenderTarget) { + SetBufferFormat(RE::Buffers::ScaleformCompositeBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT); + } + + { + if (*settings->UpgradeRenderTargets > 0) { + const bool bLimited = *settings->UpgradeRenderTargets == 1 ? true : false; + const RE::BS_DXGI_FORMAT format = *settings->UpgradeRenderTargets == 1 ? RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R10G10B10A2_UNORM : RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT; + + for (const auto& renderTargetName : settings->RenderTargetsToUpgrade.get_collection()) { + if (auto buffer = GetBufferFromString(renderTargetName)) { + UpgradeRenderTarget(buffer, format, bLimited); + } + } + } + } + + Utils::LogBuffers(); + } + + private: + static RE::BufferDefinition* GetBufferFromString(std::string_view a_bufferName); + + static void UpgradeRenderTarget(RE::BufferDefinition* a_buffer, RE::BS_DXGI_FORMAT a_format, bool a_bLimited); + static void SetBufferFormat(RE::BufferDefinition* a_buffer, RE::BS_DXGI_FORMAT a_format); + static void SetBufferFormat(RE::Buffers a_buffer, RE::BS_DXGI_FORMAT a_format); + }; + + class Hooks + { + public: + struct UnkObject + { + uint64_t unk00; + uint64_t unk08; + uint64_t unk10; + uint64_t unk18; + uint64_t unk20; + uint64_t unk28; + uint64_t unk30; + uint64_t unk38; + uint64_t unk40; + uint64_t unk48; + IDXGISwapChain3* swapChainInterface; + }; + + static void Hook() + { + const auto settings = Settings::Main::GetSingleton(); + if (*settings->FrameBufferFormat != 0) { + const auto scan = static_cast(dku::Hook::Assembly::search_pattern<"E8 ?? ?? ?? ?? 8B 4D A8 8B 45 AC">()); + if (!scan) { + ERROR("Failed to find color space hook") + } + const auto callsiteOffset = *reinterpret_cast(scan + 1); + const auto UnkFuncCallsite = AsAddress(scan + 5 + callsiteOffset + 0x3EA); + + _UnkFunc = dku::Hook::write_call<5>(UnkFuncCallsite, Hook_UnkFunc); // 32E7856 + + INFO("Found color space hook callsite at {:X}", UnkFuncCallsite) + } + } + + private: + static void Hook_UnkFunc(uintptr_t a1, UnkObject* a2); + + static inline std::add_pointer_t _UnkFunc; + }; + +#ifndef NDEBUG + class DebugHooks + { + public: + static void Hook() + { + const auto callsite1 = AsAddress(dku::Hook::Module::get().base() + 0x32ED294); + const auto callsite2 = AsAddress(dku::Hook::Module::get().base() + 0x32ED341); + const auto callsite3 = AsAddress(dku::Hook::Module::get().base() + 0x32ED2C1); + const auto callsite4 = AsAddress(dku::Hook::Module::get().base() + 0x32ED37F); + + _CreateRenderTargetView = dku::Hook::write_call<5>(callsite1, Hook_CreateRenderTargetView); + dku::Hook::write_call<5>(callsite2, Hook_CreateRenderTargetView); + _CreateDepthStencilView = dku::Hook::write_call<5>(callsite3, Hook_CreateDepthStencilView); + dku::Hook::write_call<5>(callsite4, Hook_CreateDepthStencilView); + } + + private: + static void Hook_CreateRenderTargetView(uintptr_t a1, ID3D12Resource* a_resource, DXGI_FORMAT a_format, uint8_t a4, uint16_t a5, uintptr_t a6); + static void Hook_CreateDepthStencilView(uintptr_t a1, ID3D12Resource* a_resource, DXGI_FORMAT a_format, uint8_t a4, uint16_t a5, uintptr_t a6); + + static inline std::add_pointer_t _CreateRenderTargetView; + static inline std::add_pointer_t _CreateDepthStencilView; + }; +#endif + + void Install(); +} \ No newline at end of file diff --git a/Plugin/src/Offsets.h b/Plugin/src/Offsets.h new file mode 100644 index 00000000..5b4183a6 --- /dev/null +++ b/Plugin/src/Offsets.h @@ -0,0 +1,35 @@ +#pragma once +#include "RE/Buffers.h" + +class Offsets +{ +public: + using BufferArray = std::array; + static inline BufferArray* bufferArray = nullptr; + + using tGetDXGIFormat = DXGI_FORMAT (*)(RE::BS_DXGI_FORMAT a_bgsFormat); + static inline tGetDXGIFormat GetDXGIFormat = nullptr; + + static void Initialize() + { + { + const auto scan = static_cast(dku::Hook::Assembly::search_pattern<"4C 8D 15 ?? ?? ?? ?? BE ?? ?? ?? ??">()); + if (!scan) { + ERROR("Failed to find buffer definition array!") + } + const auto offset = *reinterpret_cast(scan + 3); + const auto address = reinterpret_cast(scan) + 7 + offset; + bufferArray = reinterpret_cast(address); // 4718E40 + INFO("Found buffer array at {:X}", address) + } + + { + const auto scan = static_cast(dku::Hook::Assembly::search_pattern<"E8 ?? ?? ?? ?? 89 45 94">()); + if (!scan) { + ERROR("Failed to find GetDXGIFormat!") + } + GetDXGIFormat = dku::Hook::GetDisp(scan); // 32F18A0 + INFO("Found GetDXGIFormat at {:X}", reinterpret_cast(scan)) + } + } +}; diff --git a/Plugin/src/PCH.h b/Plugin/src/PCH.h new file mode 100644 index 00000000..4a6eb4f7 --- /dev/null +++ b/Plugin/src/PCH.h @@ -0,0 +1,115 @@ +#pragma once + +// c +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// cxx +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// winnt +#include + +#undef min +#undef max + +using namespace std::literals; + +#define DLLEXPORT extern "C" [[maybe_unused]] __declspec(dllexport) + +// Plugin +#include "Plugin.h" + +// DKUtil +#include "DKUtil/Hook.hpp" +#include "DKUtil/Logger.hpp" diff --git a/Plugin/src/RE/Buffers.h b/Plugin/src/RE/Buffers.h new file mode 100644 index 00000000..7eea8f13 --- /dev/null +++ b/Plugin/src/RE/Buffers.h @@ -0,0 +1,365 @@ +#pragma once + +namespace RE +{ + enum class BS_DXGI_FORMAT + { + BS_DXGI_FORMAT_UNKNOWN_0 = 0, + BS_DXGI_FORMAT_R8_UNORM = 1, + BS_DXGI_FORMAT_R8_SNORM = 2, + BS_DXGI_FORMAT_R8_UINT, + BS_DXGI_FORMAT_R8_SINT, + BS_DXGI_FORMAT_UNKNOWN_5, + BS_DXGI_FORMAT_UNKNOWN_6, + BS_DXGI_FORMAT_B4G4R4A4_UNORM, + BS_DXGI_FORMAT_UNKNOWN_8, + BS_DXGI_FORMAT_UNKNOWN_9, + BS_DXGI_FORMAT_B5G6R5_UNORM_10, + BS_DXGI_FORMAT_B5G6R5_UNORM_11, + BS_DXGI_FORMAT_UNKNOWN_12, + BS_DXGI_FORMAT_B5G5R5A1_UNORM, + BS_DXGI_FORMAT_R8G8_UNORM, + BS_DXGI_FORMAT_R8G8_SNORM, + BS_DXGI_FORMAT_UNKNOWN_16, + BS_DXGI_FORMAT_UNKNOWN_17, + BS_DXGI_FORMAT_R8G8_UINT, + BS_DXGI_FORMAT_R8G8_SINT, + BS_DXGI_FORMAT_UNKNOWN_20, + BS_DXGI_FORMAT_R16_UNORM, + BS_DXGI_FORMAT_R16_SNORM, + BS_DXGI_FORMAT_R16_UINT, + BS_DXGI_FORMAT_R16_SINT, + BS_DXGI_FORMAT_R16_FLOAT, + BS_DXGI_FORMAT_UNKNOWN_26, + BS_DXGI_FORMAT_UNKNOWN_27, + BS_DXGI_FORMAT_UNKNOWN_28, + BS_DXGI_FORMAT_UNKNOWN_29, + BS_DXGI_FORMAT_UNKNOWN_30, + BS_DXGI_FORMAT_UNKNOWN_31, + BS_DXGI_FORMAT_UNKNOWN_32, + BS_DXGI_FORMAT_UNKNOWN_33, + BS_DXGI_FORMAT_UNKNOWN_34, + BS_DXGI_FORMAT_UNKNOWN_35, + BS_DXGI_FORMAT_R8G8B8A8_UNORM, + BS_DXGI_FORMAT_R8G8B8A8_SNORM, + BS_DXGI_FORMAT_R8G8B8A8_UINT, + BS_DXGI_FORMAT_R8G8B8A8_SINT, + BS_DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, + BS_DXGI_FORMAT_B8G8R8A8_UNORM, + BS_DXGI_FORMAT_UNKNOWN_42, + BS_DXGI_FORMAT_UNKNOWN_43, + BS_DXGI_FORMAT_UNKNOWN_44, + BS_DXGI_FORMAT_B8G8R8A8_UNORM_SRGB, + BS_DXGI_FORMAT_UNKNOWN_46, + BS_DXGI_FORMAT_B8G8R8X8_UNORM, + BS_DXGI_FORMAT_R16G16_UNORM, + BS_DXGI_FORMAT_UNKNOWN_49, + BS_DXGI_FORMAT_R16G16_SNORM, + BS_DXGI_FORMAT_UNKNOWN_51, + BS_DXGI_FORMAT_R16G16_UINT, + BS_DXGI_FORMAT_R16G16_SINT, + BS_DXGI_FORMAT_R16G16_FLOAT, + BS_DXGI_FORMAT_R32_UINT, + BS_DXGI_FORMAT_R32_SINT, + BS_DXGI_FORMAT_R32_FLOAT, + BS_DXGI_FORMAT_UNKNOWN_58, + BS_DXGI_FORMAT_UNKNOWN_59, + BS_DXGI_FORMAT_UNKNOWN_60, + BS_DXGI_FORMAT_UNKNOWN_61, + BS_DXGI_FORMAT_R10G10B10A2_UNORM, + BS_DXGI_FORMAT_R10G10B10A2_UINT, + BS_DXGI_FORMAT_UNKNOWN_64, + BS_DXGI_FORMAT_UNKNOWN_65, + BS_DXGI_FORMAT_R11G11B10_FLOAT, + BS_DXGI_FORMAT_R9G9B9E5_SHAREDEXP, + BS_DXGI_FORMAT_UNKNOWN_68, + BS_DXGI_FORMAT_UNKNOWN_69, + BS_DXGI_FORMAT_UNKNOWN_70, + BS_DXGI_FORMAT_UNKNOWN_71, + BS_DXGI_FORMAT_UNKNOWN_72, + BS_DXGI_FORMAT_R16G16B16A16_UNORM, + BS_DXGI_FORMAT_R16G16B16A16_SNORM, + BS_DXGI_FORMAT_R16G16B16A16_UINT, + BS_DXGI_FORMAT_R16G16B16A16_SINT, + BS_DXGI_FORMAT_R16G16B16A16_FLOAT, + BS_DXGI_FORMAT_R32G32_UINT, + BS_DXGI_FORMAT_R32G32_SINT, + BS_DXGI_FORMAT_R32G32_FLOAT, + BS_DXGI_FORMAT_R32G32B32_UINT, + BS_DXGI_FORMAT_R32G32B32_SINT, + BS_DXGI_FORMAT_R32G32B32_FLOAT, + BS_DXGI_FORMAT_R32G32B32A32_UINT, + BS_DXGI_FORMAT_R32G32B32A32_SINT, + BS_DXGI_FORMAT_R32G32B32A32_FLOAT, + BS_DXGI_FORMAT_UNKNOWN_87, + BS_DXGI_FORMAT_UNKNOWN_88, + BS_DXGI_FORMAT_UNKNOWN_89, + BS_DXGI_FORMAT_UNKNOWN_90, + BS_DXGI_FORMAT_UNKNOWN_91, + BS_DXGI_FORMAT_UNKNOWN_92, + BS_DXGI_FORMAT_UNKNOWN_93, + BS_DXGI_FORMAT_UNKNOWN_94, + BS_DXGI_FORMAT_UNKNOWN_95, + BS_DXGI_FORMAT_UNKNOWN_96, + BS_DXGI_FORMAT_UNKNOWN_97, + BS_DXGI_FORMAT_UNKNOWN_98, + BS_DXGI_FORMAT_D16_UNORM, + BS_DXGI_FORMAT_D24_UNORM_S8_UINT, + BS_DXGI_FORMAT_D32_FLOAT, + BS_DXGI_FORMAT_D24_UNORM_S8_UINT_102, + BS_DXGI_FORMAT_D24_UNORM_S8_UINT_103, + BS_DXGI_FORMAT_D32_FLOAT_S8X24_UINT, + BS_DXGI_FORMAT_BC1_UNORM_105, + BS_DXGI_FORMAT_BC1_UNORM_SRGB_106, + BS_DXGI_FORMAT_BC1_UNORM_107, + BS_DXGI_FORMAT_BC1_UNORM_SRGB_108, + BS_DXGI_FORMAT_BC2_UNORM, + BS_DXGI_FORMAT_BC2_UNORM_SRGB, + BS_DXGI_FORMAT_BC3_UNORM, + BS_DXGI_FORMAT_BC3_UNORM_SRGB, + BS_DXGI_FORMAT_BC4_UNORM, + BS_DXGI_FORMAT_BC4_SNORM, + BS_DXGI_FORMAT_BC5_UNORM, + BS_DXGI_FORMAT_BC5_SNORM, + BS_DXGI_FORMAT_BC6H_UF16, + BS_DXGI_FORMAT_BC6H_SF16, + BS_DXGI_FORMAT_BC7_UNORM, + BS_DXGI_FORMAT_BC7_UNORM_SRGB + }; + + enum class Buffers + { + SF_ColorBuffer = 0, + SF_StencilBuffer = 1, + DepthBuffer, + PostUpscaleDepthBuffer, + TileDepthBoundsVS, + PreviousDepthBufferDesc, + DepthBufferDebug, + TopDownDepthSector, + ShadowMap, + TerrainShadowMap, + TerrainShadowMapBias, + ShadowMaskDesc, + ShadowMaskArrayDesc, + ScaleformStencilBuffer, + ScaleformToTextureStencilBuffer, + Scaleform3DStencilBuffer, + GBuffer_AlbedoMisc, + GBuffer_Normal_EmissiveIntensity, + GBuffer_AO_Rough_Metal, + GBuffer_MV, + GBuffer_Mask, + GBuffer_Optional, + LightingBufferY, + LightingBufferUV, + ContactShadowsBuffer, + ContactShadowsSwapBuffer, + RayTracedLightingDiffuseBuffer, + IndirectLightingDiffuseBuffer, + IndirectLightingSpecularBuffer, + ColorBuffer01, + ColorBufferUAV, + NativeResolutionColorBuffer01, + DistortionValueBuffer, + DistortionBlurStrengthBuffer, + DistortionColorBuffer, + ImageSpaceBuffer, + HDRImagespaceBuffer, + ImageSpaceBufferR10G10B10A2, + ImageSpaceBufferB10G11R11, + ImageSpaceBufferE5B9G9R9, + ImageSpaceBufferR16G16B16A16, + ImageSpaceBufferR32G32B32A32, + ImageSpaceHalfResBuffer, + ScaleformCompositeBuffer, + ColorGradingLUT, + LumaLowResPreUpscale, + LumaLowResPostUpscale, + BloomPyramidPreUpscale, + BloomPyramidPostUpscale, + HDRLum, + HDRSingleValueAverageLum, + HDRBloom, + HDRBloomDS1, + HDRBloomDS2, + HDRBloomDS3, + HDRBloomDS4, + HDRBloomDS5, + HDRBloomDS6, + SAOCameraZ, + SAORawAO, + SAOFinalAO, + FrameBuffer, + NullRenderTarget, + VisualizationBuffer, + HeightClipmap, + BlurHeightClipmap, + ShoreClipmap, + ShoreFlood, + SplatClipmap, + NormalClipmap, + VErrorClipmap, + MaterialIndexMap, + ProcGenDensityMap, + DownsampleOutputPrevFrame, + DownsampleOutput, + SobelOutput, + SpaceGlareBlur, + CGenTestTexture3D, + DownsampleMipChainOutput, + BlurredMipChainOutput, + RefractionPrevFrame, + DoFHTileMask, + DoFCoCTarget, + DoFColorTarget, + DoFTAAHistoryTarget, + TAA_idTech7HistoryColorTarget, + TAA_idTech7HistoryDataTarget, + RT_HighlightMask, + RT_ColorGradeMask, + SeparableSSSBufferY, + SeparableSSSBufferUV, + SHLUTAngle_High, + SHLUTAngle_Mid, + SHLUTAngle_Low, + SHLUTAngleProbe, + EnvBRDF, + BinkYPlane, + BinkcRcBPlane, + DebugMotionVectorColorBufferCopy, + AlphaBuffer, + VRSMask, + VRSMaskHistory, + VRSMaskCompute, + MaxTileVelocityBuffer, + VelocityDepthBuffer, + RefractionMaskCoarse, + ReflectionProbeSHAtlas, + FSR2ReactiveMask, + FSR2TandCMask, + FSR2_PREPARED_INPUT_COLOR, + FSR2_RECONSTRUCTED_PREVIOUS_NEAREST_DEPTH, + FSR2_INTERNAL_DILATED_MOTION_VECTORS, + FSR2_DILATED_DEPTH, + FSR2_RESAMPLED_LUMA_HISTORY, + FSR2_DEPTH_CLIP, + FSR2_AUTO_EXPOSURE, + FSR2_DILATED_REACTIVE_MASKS, + FSR2_INTERNAL_UPSCALED_COLOR_W, + MBOITMoments4, + MBOITOpticalDepth, + MBOITMoments4HalfRes, + MBOITOpticalDepthHalfRes, + MBOITTransparentAccumulation, + MBOITTransparentAccumulationHalfRes, + MBOITForwardHalfResTarget, + ForwardHalfResTarget, + ForwardHalfResDepth, + ForwardHalfResOverdraw, + BlurHeightFieldStaging, + BlurHeightField, + SkyOcclusionSH, + TopDownDepthArray, + FilteredHeightfield, + DownsampledHeightfield, + ProbeRayTrace, + ProbeDistanceAtlas, + ProbeIrradianceAtlas, + ProbeStateMaxDistanceAtlas, + CascadeProbeRayTrace, + CascadeProbeDistanceAtlas, + CascadeProbeIrradianceAtlas, + CascadeProbeStateMaxDistanceAtlas, + CascadeProbeScrollSpace, + ThinDepthBuffer, + ThinGBuffer_Albedo, + ThinGBuffer_Normal, + ThinGBuffer_Optional, + ThinDepthBufferArray, + ThinGBuffer_AlbedoArray, + ThinGBuffer_NormalArray, + ThinGBuffer_OptionalArray, + ReflectionCubemapArray, + ReflectionCubemapCaptureArray, + ReflectionCubemapForSHArray, + ReflectionCubemapDepthArray, + SkyCubemapAtmosScatteringOutput, + SkyCubemap, + SkyCubemapThinDepthBuffer, + SkyCubemapThinGBuffer_Albedo, + SkyCubemapThinGBuffer_Normal, + SkyCubemapThinGBuffer_Optional, + CelestialBodyThinGBuffer_Albedo, + CelestialBodyThinGBuffer_Normal, + CelestialBodyThinGBuffer_Optional, + CelestialBodyDepthBuffer, + NetDensityToAtmTop, + SingleScattering, + ScatterRadiance, + InscatterOrder, + HighOrderScatter, + EpipolarSliceEndpoints, + EpipolarCoordinates, + EpipolarRawDepth, + EpipolarExtinction, + EpipolarInscattering, + EpipolarInterpolationSource, + EpipolarInitialScatteredLight, + EpipolarSliceUVDirAndOrigin, + MinMaxShadowMap, + AtmosphericScatteringOutput, + PrecomputedTransmittance, + FogMap, + MaterialVolume, + ScatteringVolume, + ScatteringVolumeHistoryTarget, + DistantScattering, + DistantScatteringHistoryTarget, + DistantScatteringComposite, + PerlinNoise3D, + PerlinNoise2D, + VolumeDepthDownsample, + FaceCustomizationDiffuse, + FaceCustomizationDiffuseLinear, + FaceCustomizationNormals, + FaceCustomizationAO, + FaceCustomizationRoughness, + WaterDisplacementTarget, + ImageProcessColorTarget, + ImageProcessR8Target, + TextureReadbackInput_All, + ParticleLightingAtlas, + CloudsHalfResTarget, + CloudCardsOpacity, + ImageCaptureHDR_OutputImage + }; + + struct BufferDefinition + { + BS_DXGI_FORMAT format; + uint32_t unk04; + const char* bufferName; + uint16_t unk10; + uint16_t unk12; + uint32_t unk14; + uint16_t unk18; + uint16_t unk1A; + uint32_t unk1C; + uint16_t unk20; + uint16_t unk22; + uint32_t unk24; + uint16_t unk28; + uint16_t unk2A; + uint32_t unk2C; + uint32_t unk30; + float unk34; + float unk38; + float unk3C; + uint32_t unk40; + uint32_t unk44; + uint32_t unk48; + uint32_t unk4C; + }; + static_assert(sizeof(BufferDefinition) == 0x50); +} diff --git a/Plugin/src/Settings.cpp b/Plugin/src/Settings.cpp new file mode 100644 index 00000000..c6a9408b --- /dev/null +++ b/Plugin/src/Settings.cpp @@ -0,0 +1,20 @@ +#include "Settings.h" + +namespace Settings +{ + void Main::Load() noexcept + { + static std::once_flag ConfigInit; + std::call_once(ConfigInit, [&]() { + config.Bind(ImageSpaceBufferFormat, 0); + config.Bind(UpgradeUIRenderTarget, true); + config.Bind(UpgradeRenderTargets, 2); + config.Bind(FrameBufferFormat, 0); + config.Bind(RenderTargetsToUpgrade, "SF_ColorBuffer", "HDRImagespaceBuffer", "ImageSpaceHalfResBuffer", "ImageProcessColorTarget", "ImageSpaceBufferB10G11R11", "ImageSpaceBufferE5B9G9R9", "TAA_idTech7HistoryColorTarget", "EnvBRDF", "GBuffer_Normal_EmissiveIntensity", "ImageSpaceBufferR10G10B10A2"); + }); + + config.Load(); + + INFO("Config loaded"sv) + } +} \ No newline at end of file diff --git a/Plugin/src/Settings.h b/Plugin/src/Settings.h new file mode 100644 index 00000000..53d1a602 --- /dev/null +++ b/Plugin/src/Settings.h @@ -0,0 +1,24 @@ +#pragma once +#include "DKUtil/Config.hpp" + +namespace Settings +{ + using namespace DKUtil::Alias; + + class Main : public DKUtil::model::Singleton
+ { + public: + Integer ImageSpaceBufferFormat{ "ImageSpaceBufferFormat", "Main" }; + Boolean UpgradeUIRenderTarget{ "UpgradeUIRenderTarget", "Main" }; + Integer UpgradeRenderTargets{ "UpgradeRenderTargets", "Main" }; + + Integer FrameBufferFormat{ "FrameBufferFormat", "HDR" }; + + String RenderTargetsToUpgrade{ "RenderTargetsToUpgrade", "RenderTargets" }; + + void Load() noexcept; + + private: + TomlConfig config = COMPILE_PROXY("Data/SFSE/Plugins/NativeHDR.toml"sv); + }; +} \ No newline at end of file diff --git a/Plugin/src/Utils.cpp b/Plugin/src/Utils.cpp new file mode 100644 index 00000000..5914c10d --- /dev/null +++ b/Plugin/src/Utils.cpp @@ -0,0 +1,162 @@ +#include "Utils.h" + +#include "Offsets.h" + +namespace Utils +{ + std::unordered_map GetDXGIFormatNameMap() + { + std::unordered_map formatNames; + + formatNames[DXGI_FORMAT_UNKNOWN] = "DXGI_FORMAT_UNKNOWN"; + formatNames[DXGI_FORMAT_R32G32B32A32_TYPELESS] = "DXGI_FORMAT_R32G32B32A32_TYPELESS"; + formatNames[DXGI_FORMAT_R32G32B32A32_FLOAT] = "DXGI_FORMAT_R32G32B32A32_FLOAT"; + formatNames[DXGI_FORMAT_R32G32B32A32_UINT] = "DXGI_FORMAT_R32G32B32A32_UINT"; + formatNames[DXGI_FORMAT_R32G32B32A32_SINT] = "DXGI_FORMAT_R32G32B32A32_SINT"; + formatNames[DXGI_FORMAT_R32G32B32_TYPELESS] = "DXGI_FORMAT_R32G32B32_TYPELESS"; + formatNames[DXGI_FORMAT_R32G32B32_FLOAT] = "DXGI_FORMAT_R32G32B32_FLOAT"; + formatNames[DXGI_FORMAT_R32G32B32_UINT] = "DXGI_FORMAT_R32G32B32_UINT"; + formatNames[DXGI_FORMAT_R32G32B32_SINT] = "DXGI_FORMAT_R32G32B32_SINT"; + formatNames[DXGI_FORMAT_R16G16B16A16_TYPELESS] = "DXGI_FORMAT_R16G16B16A16_TYPELESS"; + formatNames[DXGI_FORMAT_R16G16B16A16_FLOAT] = "DXGI_FORMAT_R16G16B16A16_FLOAT"; + formatNames[DXGI_FORMAT_R16G16B16A16_UNORM] = "DXGI_FORMAT_R16G16B16A16_UNORM"; + formatNames[DXGI_FORMAT_R16G16B16A16_UINT] = "DXGI_FORMAT_R16G16B16A16_UINT"; + formatNames[DXGI_FORMAT_R16G16B16A16_SNORM] = "DXGI_FORMAT_R16G16B16A16_SNORM"; + formatNames[DXGI_FORMAT_R16G16B16A16_SINT] = "DXGI_FORMAT_R16G16B16A16_SINT"; + formatNames[DXGI_FORMAT_R32G32_TYPELESS] = "DXGI_FORMAT_R32G32_TYPELESS"; + formatNames[DXGI_FORMAT_R32G32_FLOAT] = "DXGI_FORMAT_R32G32_FLOAT"; + formatNames[DXGI_FORMAT_R32G32_UINT] = "DXGI_FORMAT_R32G32_UINT"; + formatNames[DXGI_FORMAT_R32G32_SINT] = "DXGI_FORMAT_R32G32_SINT"; + formatNames[DXGI_FORMAT_R32G8X24_TYPELESS] = "DXGI_FORMAT_R32G8X24_TYPELESS"; + formatNames[DXGI_FORMAT_D32_FLOAT_S8X24_UINT] = "DXGI_FORMAT_D32_FLOAT_S8X24_UINT"; + formatNames[DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS] = "DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS"; + formatNames[DXGI_FORMAT_X32_TYPELESS_G8X24_UINT] = "DXGI_FORMAT_X32_TYPELESS_G8X24_UINT"; + formatNames[DXGI_FORMAT_R10G10B10A2_TYPELESS] = "DXGI_FORMAT_R10G10B10A2_TYPELESS"; + formatNames[DXGI_FORMAT_R10G10B10A2_UNORM] = "DXGI_FORMAT_R10G10B10A2_UNORM"; + formatNames[DXGI_FORMAT_R10G10B10A2_UINT] = "DXGI_FORMAT_R10G10B10A2_UINT"; + formatNames[DXGI_FORMAT_R11G11B10_FLOAT] = "DXGI_FORMAT_R11G11B10_FLOAT"; + formatNames[DXGI_FORMAT_R8G8B8A8_TYPELESS] = "DXGI_FORMAT_R8G8B8A8_TYPELESS"; + formatNames[DXGI_FORMAT_R8G8B8A8_UNORM] = "DXGI_FORMAT_R8G8B8A8_UNORM"; + formatNames[DXGI_FORMAT_R8G8B8A8_UNORM_SRGB] = "DXGI_FORMAT_R8G8B8A8_UNORM_SRGB"; + formatNames[DXGI_FORMAT_R8G8B8A8_UINT] = "DXGI_FORMAT_R8G8B8A8_UINT"; + formatNames[DXGI_FORMAT_R8G8B8A8_SNORM] = "DXGI_FORMAT_R8G8B8A8_SNORM"; + formatNames[DXGI_FORMAT_R8G8B8A8_SINT] = "DXGI_FORMAT_R8G8B8A8_SINT"; + formatNames[DXGI_FORMAT_R16G16_TYPELESS] = "DXGI_FORMAT_R16G16_TYPELESS"; + formatNames[DXGI_FORMAT_R16G16_FLOAT] = "DXGI_FORMAT_R16G16_FLOAT"; + formatNames[DXGI_FORMAT_R16G16_UNORM] = "DXGI_FORMAT_R16G16_UNORM"; + formatNames[DXGI_FORMAT_R16G16_UINT] = "DXGI_FORMAT_R16G16_UINT"; + formatNames[DXGI_FORMAT_R16G16_SNORM] = "DXGI_FORMAT_R16G16_SNORM"; + formatNames[DXGI_FORMAT_R16G16_SINT] = "DXGI_FORMAT_R16G16_SINT"; + formatNames[DXGI_FORMAT_R32_TYPELESS] = "DXGI_FORMAT_R32_TYPELESS"; + formatNames[DXGI_FORMAT_D32_FLOAT] = "DXGI_FORMAT_D32_FLOAT"; + formatNames[DXGI_FORMAT_R32_FLOAT] = "DXGI_FORMAT_R32_FLOAT"; + formatNames[DXGI_FORMAT_R32_UINT] = "DXGI_FORMAT_R32_UINT"; + formatNames[DXGI_FORMAT_R32_SINT] = "DXGI_FORMAT_R32_SINT"; + formatNames[DXGI_FORMAT_R24G8_TYPELESS] = "DXGI_FORMAT_R24G8_TYPELESS"; + formatNames[DXGI_FORMAT_D24_UNORM_S8_UINT] = "DXGI_FORMAT_D24_UNORM_S8_UINT"; + formatNames[DXGI_FORMAT_R24_UNORM_X8_TYPELESS] = "DXGI_FORMAT_R24_UNORM_X8_TYPELESS"; + formatNames[DXGI_FORMAT_X24_TYPELESS_G8_UINT] = "DXGI_FORMAT_X24_TYPELESS_G8_UINT"; + formatNames[DXGI_FORMAT_R8G8_TYPELESS] = "DXGI_FORMAT_R8G8_TYPELESS"; + formatNames[DXGI_FORMAT_R8G8_UNORM] = "DXGI_FORMAT_R8G8_UNORM"; + formatNames[DXGI_FORMAT_R8G8_UINT] = "DXGI_FORMAT_R8G8_UINT"; + formatNames[DXGI_FORMAT_R8G8_SNORM] = "DXGI_FORMAT_R8G8_SNORM"; + formatNames[DXGI_FORMAT_R8G8_SINT] = "DXGI_FORMAT_R8G8_SINT"; + formatNames[DXGI_FORMAT_R16_TYPELESS] = "DXGI_FORMAT_R16_TYPELESS"; + formatNames[DXGI_FORMAT_R16_FLOAT] = "DXGI_FORMAT_R16_FLOAT"; + formatNames[DXGI_FORMAT_D16_UNORM] = "DXGI_FORMAT_D16_UNORM"; + formatNames[DXGI_FORMAT_R16_UNORM] = "DXGI_FORMAT_R16_UNORM"; + formatNames[DXGI_FORMAT_R16_UINT] = "DXGI_FORMAT_R16_UINT"; + formatNames[DXGI_FORMAT_R16_SNORM] = "DXGI_FORMAT_R16_SNORM"; + formatNames[DXGI_FORMAT_R16_SINT] = "DXGI_FORMAT_R16_SINT"; + formatNames[DXGI_FORMAT_R8_TYPELESS] = "DXGI_FORMAT_R8_TYPELESS"; + formatNames[DXGI_FORMAT_R8_UNORM] = "DXGI_FORMAT_R8_UNORM"; + formatNames[DXGI_FORMAT_R8_UINT] = "DXGI_FORMAT_R8_UINT"; + formatNames[DXGI_FORMAT_R8_SNORM] = "DXGI_FORMAT_R8_SNORM"; + formatNames[DXGI_FORMAT_R8_SINT] = "DXGI_FORMAT_R8_SINT"; + formatNames[DXGI_FORMAT_A8_UNORM] = "DXGI_FORMAT_A8_UNORM"; + formatNames[DXGI_FORMAT_R1_UNORM] = "DXGI_FORMAT_R1_UNORM"; + formatNames[DXGI_FORMAT_R9G9B9E5_SHAREDEXP] = "DXGI_FORMAT_R9G9B9E5_SHAREDEXP"; + formatNames[DXGI_FORMAT_R8G8_B8G8_UNORM] = "DXGI_FORMAT_R8G8_B8G8_UNORM"; + formatNames[DXGI_FORMAT_G8R8_G8B8_UNORM] = "DXGI_FORMAT_G8R8_G8B8_UNORM"; + formatNames[DXGI_FORMAT_BC1_TYPELESS] = "DXGI_FORMAT_BC1_TYPELESS"; + formatNames[DXGI_FORMAT_BC1_UNORM] = "DXGI_FORMAT_BC1_UNORM"; + formatNames[DXGI_FORMAT_BC1_UNORM_SRGB] = "DXGI_FORMAT_BC1_UNORM_SRGB"; + formatNames[DXGI_FORMAT_BC2_TYPELESS] = "DXGI_FORMAT_BC2_TYPELESS"; + formatNames[DXGI_FORMAT_BC2_UNORM] = "DXGI_FORMAT_BC2_UNORM"; + formatNames[DXGI_FORMAT_BC2_UNORM_SRGB] = "DXGI_FORMAT_BC2_UNORM_SRGB"; + formatNames[DXGI_FORMAT_BC3_TYPELESS] = "DXGI_FORMAT_BC3_TYPELESS"; + formatNames[DXGI_FORMAT_BC3_UNORM] = "DXGI_FORMAT_BC3_UNORM"; + formatNames[DXGI_FORMAT_BC3_UNORM_SRGB] = "DXGI_FORMAT_BC3_UNORM_SRGB"; + formatNames[DXGI_FORMAT_BC4_TYPELESS] = "DXGI_FORMAT_BC4_TYPELESS"; + formatNames[DXGI_FORMAT_BC4_UNORM] = "DXGI_FORMAT_BC4_UNORM"; + formatNames[DXGI_FORMAT_BC4_SNORM] = "DXGI_FORMAT_BC4_SNORM"; + formatNames[DXGI_FORMAT_BC5_TYPELESS] = "DXGI_FORMAT_BC5_TYPELESS"; + formatNames[DXGI_FORMAT_BC5_UNORM] = "DXGI_FORMAT_BC5_UNORM"; + formatNames[DXGI_FORMAT_BC5_SNORM] = "DXGI_FORMAT_BC5_SNORM"; + formatNames[DXGI_FORMAT_B5G6R5_UNORM] = "DXGI_FORMAT_B5G6R5_UNORM"; + formatNames[DXGI_FORMAT_B5G5R5A1_UNORM] = "DXGI_FORMAT_B5G5R5A1_UNORM"; + formatNames[DXGI_FORMAT_B8G8R8A8_UNORM] = "DXGI_FORMAT_B8G8R8A8_UNORM"; + formatNames[DXGI_FORMAT_B8G8R8X8_UNORM] = "DXGI_FORMAT_B8G8R8X8_UNORM"; + formatNames[DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM] = "DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM"; + formatNames[DXGI_FORMAT_B8G8R8A8_TYPELESS] = "DXGI_FORMAT_B8G8R8A8_TYPELESS"; + formatNames[DXGI_FORMAT_B8G8R8A8_UNORM_SRGB] = "DXGI_FORMAT_B8G8R8A8_UNORM_SRGB"; + formatNames[DXGI_FORMAT_B8G8R8X8_TYPELESS] = "DXGI_FORMAT_B8G8R8X8_TYPELESS"; + formatNames[DXGI_FORMAT_B8G8R8X8_UNORM_SRGB] = "DXGI_FORMAT_B8G8R8X8_UNORM_SRGB"; + formatNames[DXGI_FORMAT_BC6H_TYPELESS] = "DXGI_FORMAT_BC6H_TYPELESS"; + formatNames[DXGI_FORMAT_BC6H_UF16] = "DXGI_FORMAT_BC6H_UF16"; + formatNames[DXGI_FORMAT_BC6H_SF16] = "DXGI_FORMAT_BC6H_SF16"; + formatNames[DXGI_FORMAT_BC7_TYPELESS] = "DXGI_FORMAT_BC7_TYPELESS"; + formatNames[DXGI_FORMAT_BC7_UNORM] = "DXGI_FORMAT_BC7_UNORM"; + formatNames[DXGI_FORMAT_BC7_UNORM_SRGB] = "DXGI_FORMAT_BC7_UNORM_SRGB"; + formatNames[DXGI_FORMAT_AYUV] = "DXGI_FORMAT_AYUV"; + formatNames[DXGI_FORMAT_Y410] = "DXGI_FORMAT_Y410"; + formatNames[DXGI_FORMAT_Y416] = "DXGI_FORMAT_Y416"; + formatNames[DXGI_FORMAT_NV12] = "DXGI_FORMAT_NV12"; + formatNames[DXGI_FORMAT_P010] = "DXGI_FORMAT_P010"; + formatNames[DXGI_FORMAT_P016] = "DXGI_FORMAT_P016"; + formatNames[DXGI_FORMAT_420_OPAQUE] = "DXGI_FORMAT_420_OPAQUE"; + formatNames[DXGI_FORMAT_YUY2] = "DXGI_FORMAT_YUY2"; + formatNames[DXGI_FORMAT_Y210] = "DXGI_FORMAT_Y210"; + formatNames[DXGI_FORMAT_Y216] = "DXGI_FORMAT_Y216"; + formatNames[DXGI_FORMAT_NV11] = "DXGI_FORMAT_NV11"; + formatNames[DXGI_FORMAT_AI44] = "DXGI_FORMAT_AI44"; + formatNames[DXGI_FORMAT_IA44] = "DXGI_FORMAT_IA44"; + formatNames[DXGI_FORMAT_P8] = "DXGI_FORMAT_P8"; + formatNames[DXGI_FORMAT_A8P8] = "DXGI_FORMAT_A8P8"; + formatNames[DXGI_FORMAT_B4G4R4A4_UNORM] = "DXGI_FORMAT_B4G4R4A4_UNORM"; + formatNames[DXGI_FORMAT_P208] = "DXGI_FORMAT_P208"; + formatNames[DXGI_FORMAT_V208] = "DXGI_FORMAT_V208"; + formatNames[DXGI_FORMAT_V408] = "DXGI_FORMAT_V408"; + formatNames[DXGI_FORMAT_SAMPLER_FEEDBACK_MIN_MIP_OPAQUE] = "DXGI_FORMAT_SAMPLER_FEEDBACK_MIN_MIP_OPAQUE"; + formatNames[DXGI_FORMAT_SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE] = "DXGI_FORMAT_SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE"; + + return formatNames; + } + + void LogFormats() + { + auto formatNames = GetDXGIFormatNameMap(); + + for (int i = 0; i < 128; ++i) { + const auto format = Offsets::GetDXGIFormat(static_cast(i)); + INFO("{} - {}", i, formatNames[format]) + } + } + + void LogBuffers() + { + INFO("===LOGGING BUFFERS===") + + std::string logString = "Index, Buffer name, DXGI_FORMAT\n"; + + auto formatNames = GetDXGIFormatNameMap(); + for (int i = 0; i < Offsets::bufferArray->size(); ++i) { + const auto& bufferDefinition = (*Offsets::bufferArray)[i]; + const auto dxgiFormat = Offsets::GetDXGIFormat(bufferDefinition->format); + logString.append(fmt::format("{:3}, {}, {}\n", i, bufferDefinition->bufferName, formatNames[dxgiFormat])); + } + + INFO(logString) + INFO("===END LOGGING BUFFERS===") + } +} diff --git a/Plugin/src/Utils.h b/Plugin/src/Utils.h new file mode 100644 index 00000000..0c57bf09 --- /dev/null +++ b/Plugin/src/Utils.h @@ -0,0 +1,11 @@ +#pragma once +#include + +namespace Utils +{ + std::unordered_map GetDXGIFormatNameMap(); + + void LogFormats(); + + void LogBuffers(); +} diff --git a/Plugin/src/main.cpp b/Plugin/src/main.cpp new file mode 100644 index 00000000..8bf84710 --- /dev/null +++ b/Plugin/src/main.cpp @@ -0,0 +1,50 @@ +#include "SFSE/Stub.h" + +#include "Hooks.h" +#include "Offsets.h" +#include "Settings.h" + +DLLEXPORT constinit auto SFSEPlugin_Version = []() noexcept { + SFSE::PluginVersionData data{}; + + data.PluginVersion(Plugin::Version); + data.PluginName(Plugin::NAME); + data.AuthorName(Plugin::AUTHOR); + data.UsesSigScanning(true); + //data.UsesAddressLibrary(true); + data.HasNoStructUse(true); + //data.IsLayoutDependent(true); + data.CompatibleVersions({ RUNTIME_VERSION_1_7_23, RUNTIME_VERSION_1_7_29 }); + + return data; +}(); + +/** +// for preload plugins +void SFSEPlugin_Preload(SFSE::LoadInterface* a_sfse); +/**/ + +DLLEXPORT bool SFSEAPI SFSEPlugin_Load(SFSEInterface* a_sfse) +{ +#ifndef NDEBUG + while (!IsDebuggerPresent()) { + Sleep(100); + } +#endif + + SFSE::Init(a_sfse); + + DKUtil::Logger::Init(Plugin::NAME, std::to_string(Plugin::Version)); + + INFO("{} v{} loaded", Plugin::NAME, Plugin::Version); + INFO("base address : {:X}", dku::Hook::Module::get().base()) + + // do stuff + Settings::Main::GetSingleton()->Load(); + + SFSE::AllocTrampoline(1 << 7); + Offsets::Initialize(); + Hooks::Install(); + + return true; +} diff --git a/Plugin/vcpkg.json b/Plugin/vcpkg.json new file mode 100644 index 00000000..cd849a65 --- /dev/null +++ b/Plugin/vcpkg.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json", + "name": "nativehdr", + "version-string": "1.0.0", + "description": "SFSE plugin that adds native HDR support to Starfield", + "dependencies": [ + "fmt", + "spdlog", + "nlohmann-json", + "simpleini", + "tomlplusplus", + "xbyak" + ] +} \ No newline at end of file diff --git a/build-release.ps1 b/build-release.ps1 new file mode 100644 index 00000000..7800e3de --- /dev/null +++ b/build-release.ps1 @@ -0,0 +1,3 @@ +Remove-Item $PSScriptRoot/build -Recurse -Force -ErrorAction:SilentlyContinue -Confirm:$False | Out-Null +& cmake -B $PSScriptRoot/build -S $PSScriptRoot/Plugin --preset=REL +& cmake --build $PSScriptRoot/build --config Release \ No newline at end of file diff --git a/extern/DKUtil b/extern/DKUtil new file mode 160000 index 00000000..3d383c03 --- /dev/null +++ b/extern/DKUtil @@ -0,0 +1 @@ +Subproject commit 3d383c03bbcb437e35651edb625ba4ec6f1af377 diff --git a/extern/SFSE b/extern/SFSE new file mode 160000 index 00000000..8c4af406 --- /dev/null +++ b/extern/SFSE @@ -0,0 +1 @@ +Subproject commit 8c4af406ed86baf69af676132fea337621084ae3 diff --git a/generate-sln.ps1 b/generate-sln.ps1 new file mode 100644 index 00000000..b2cf1b38 --- /dev/null +++ b/generate-sln.ps1 @@ -0,0 +1,2 @@ +Remove-Item $PSScriptRoot/build -Recurse -Force -ErrorAction:SilentlyContinue -Confirm:$False | Out-Null +& cmake -B $PSScriptRoot/build -S $PSScriptRoot/Plugin --preset=REL \ No newline at end of file From 07122086a53f46fe42d209180b80f20d424d1fd6 Mon Sep 17 00:00:00 2001 From: Nukem Date: Sat, 23 Sep 2023 17:02:07 -0400 Subject: [PATCH 018/270] Preliminary support and structures for SettingsDataModel UI integration --- Plugin/src/Hooks.cpp | 138 ++++++++++++++++++++- Plugin/src/Hooks.h | 11 +- Plugin/src/RE/SettingsDataModel.h | 124 +++++++++++++++++++ Plugin/src/RE/UI.h | 194 ++++++++++++++++++++++++++++++ Plugin/src/RE/UIDataShuttle.h | 91 ++++++++++++++ 5 files changed, 550 insertions(+), 8 deletions(-) create mode 100644 Plugin/src/RE/SettingsDataModel.h create mode 100644 Plugin/src/RE/UI.h create mode 100644 Plugin/src/RE/UIDataShuttle.h diff --git a/Plugin/src/Hooks.cpp b/Plugin/src/Hooks.cpp index 8f32fa97..85a43877 100644 --- a/Plugin/src/Hooks.cpp +++ b/Plugin/src/Hooks.cpp @@ -1,5 +1,4 @@ #include "Hooks.h" - #include "Offsets.h" #include "Utils.h" @@ -69,7 +68,137 @@ namespace Hooks return _UnkFunc(a1, a2); } -#ifndef NDEBUG + void DebugHooks::Hook_CreateDataModelOptions(void* a_arg1, RE::ArrayNestedUIValue& a_SettingList) + { + int id = 600; + + { + auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); + auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); + + s.m_Text.SetStringValue("Hello Checkbox"); + s.m_Description.SetStringValue("Hello World Checkbox Description"); + s.m_ID.SetValue(id++); + s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Checkbox); + s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); + s.m_Enabled.SetValue(true); + s.m_CheckBoxData.m_ShuttleMap.GetData().m_Value.SetValue(false); + a_SettingList.AddItem(s); + } + + { + auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); + auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); + + s.m_Text.SetStringValue("Hello Checkbox Disabled"); + s.m_Description.SetStringValue("Hello World Checkbox Disabled Description"); + s.m_ID.SetValue(id++); + s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Checkbox); + s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); + s.m_Enabled.SetValue(false); + s.m_CheckBoxData.m_ShuttleMap.GetData().m_Value.SetValue(false); + a_SettingList.AddItem(s); + } + + { + auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); + auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); + + s.m_Text.SetStringValue("Hello Checkbox On"); + s.m_Description.SetStringValue("Hello World Checkbox On Description"); + s.m_ID.SetValue(id++); + s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Checkbox); + s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); + s.m_Enabled.SetValue(false); + s.m_CheckBoxData.m_ShuttleMap.GetData().m_Value.SetValue(true); + a_SettingList.AddItem(s); + } + + { + auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); + auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); + + s.m_Text.SetStringValue("Hello Stepper"); + s.m_Description.SetStringValue("Hello World Stepper Description"); + s.m_ID.SetValue(id++); + s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Stepper); + s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); + s.m_Enabled.SetValue(true); + s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("Option 1"); + s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("Option 2"); + s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("Option 3"); + s.m_StepperData.m_ShuttleMap.GetData().m_Value.SetValue(0); + a_SettingList.AddItem(s); + } + + { + auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); + auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); + + s.m_Text.SetStringValue("Hello Stepper Disabled"); + s.m_Description.SetStringValue("Hello World Stepper Disabled Description"); + s.m_ID.SetValue(id++); + s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Stepper); + s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); + s.m_Enabled.SetValue(false); + s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("Option 1"); + s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("Option 2"); + s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("Option 3"); + s.m_StepperData.m_ShuttleMap.GetData().m_Value.SetValue(0); + a_SettingList.AddItem(s); + } + + { + auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); + auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); + + s.m_Text.SetStringValue("Hello Stepper On"); + s.m_Description.SetStringValue("Hello World Stepper On Description"); + s.m_ID.SetValue(id++); + s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Stepper); + s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); + s.m_Enabled.SetValue(false); + s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("Option 1"); + s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("Option 2"); + s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("Option 3"); + s.m_StepperData.m_ShuttleMap.GetData().m_Value.SetValue(2); + a_SettingList.AddItem(s); + } + + { + auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); + auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); + + s.m_Text.SetStringValue("Hello Slider"); + s.m_Description.SetStringValue("Hello World Slider Description"); + s.m_ID.SetValue(id++); + s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Slider); + s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); + s.m_Enabled.SetValue(true); + s.m_SliderData.m_ShuttleMap.GetData().m_Value.SetValue(0.5f); + s.m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue("0.5"); + a_SettingList.AddItem(s); + } + + { + auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); + auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); + + s.m_Text.SetStringValue("Hello Slider Disabled"); + s.m_Description.SetStringValue("Hello World Slider Disabled Description"); + s.m_ID.SetValue(id++); + s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Slider); + s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); + s.m_Enabled.SetValue(false); + s.m_SliderData.m_ShuttleMap.GetData().m_Value.SetValue(0.5f); + s.m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue("0.5"); + a_SettingList.AddItem(s); + } + + // Initialize the rest of the settings after ours + _CreateDataModelOptions(a_arg1, a_SettingList); + } + void DebugHooks::Hook_CreateRenderTargetView(uintptr_t a1, ID3D12Resource* a_resource, DXGI_FORMAT a_format, uint8_t a4, uint16_t a5, uintptr_t a6) { const auto textureDesc = a_resource->GetDesc(); @@ -83,14 +212,13 @@ namespace Hooks _CreateDepthStencilView(a1, a_resource, a_format, a4, a5, a6); } -#endif void Install() { -#ifndef NDEBUG +//#ifndef NDEBUG Utils::LogBuffers(); DebugHooks::Hook(); -#endif +//#endif Hooks::Hook(); Patches::Patch(); } diff --git a/Plugin/src/Hooks.h b/Plugin/src/Hooks.h index d69f3743..f42bacf9 100644 --- a/Plugin/src/Hooks.h +++ b/Plugin/src/Hooks.h @@ -2,6 +2,7 @@ #include "Settings.h" #include "Utils.h" #include "RE/Buffers.h" +#include "RE/SettingsDataModel.h" #include @@ -101,12 +102,14 @@ namespace Hooks static inline std::add_pointer_t _UnkFunc; }; -#ifndef NDEBUG class DebugHooks { public: static void Hook() { + _CreateDataModelOptions = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20BBAA9, Hook_CreateDataModelOptions); + +#if 0 const auto callsite1 = AsAddress(dku::Hook::Module::get().base() + 0x32ED294); const auto callsite2 = AsAddress(dku::Hook::Module::get().base() + 0x32ED341); const auto callsite3 = AsAddress(dku::Hook::Module::get().base() + 0x32ED2C1); @@ -116,16 +119,18 @@ namespace Hooks dku::Hook::write_call<5>(callsite2, Hook_CreateRenderTargetView); _CreateDepthStencilView = dku::Hook::write_call<5>(callsite3, Hook_CreateDepthStencilView); dku::Hook::write_call<5>(callsite4, Hook_CreateDepthStencilView); +#endif } private: + static void Hook_CreateDataModelOptions(void* a_arg1, RE::ArrayNestedUIValue& a_SettingList); + static inline std::add_pointer_t _CreateDataModelOptions; + static void Hook_CreateRenderTargetView(uintptr_t a1, ID3D12Resource* a_resource, DXGI_FORMAT a_format, uint8_t a4, uint16_t a5, uintptr_t a6); static void Hook_CreateDepthStencilView(uintptr_t a1, ID3D12Resource* a_resource, DXGI_FORMAT a_format, uint8_t a4, uint16_t a5, uintptr_t a6); - static inline std::add_pointer_t _CreateRenderTargetView; static inline std::add_pointer_t _CreateDepthStencilView; }; -#endif void Install(); } \ No newline at end of file diff --git a/Plugin/src/RE/SettingsDataModel.h b/Plugin/src/RE/SettingsDataModel.h new file mode 100644 index 00000000..f6fb05f5 --- /dev/null +++ b/Plugin/src/RE/SettingsDataModel.h @@ -0,0 +1,124 @@ +#pragma once + +#include "UIDataShuttle.h" +#include "UI.h" + +namespace RE +{ + class SubSettingsList + { + public: + class GeneralSetting + { + public: + struct Type + { + enum + { + Slider = 0, + Stepper = 1, + LargeStepper = 2, + Checkbox = 3, + }; + }; + + struct Category + { + enum + { + Gameplay = 0, + Display = 1, + Interface = 2, + Controls = 3, + ControlMappings = 4, + Audio = 5, + Accessibility = 6, + }; + }; + + struct SliderData + { + TUIValue m_Value; // 0 + StringUIValue m_DisplayValue; // 20 + char _pad40[0x8]; // 40 + }; + + struct StepperData + { + TUIValue m_Value; // 0 + ArrayUIValue, 0> m_DisplayValues; // 20 + char _pad98[0x8]; // 98 + }; + + struct CheckBoxData + { + TUIValue m_Value; // 0 + char _pad20[0x8]; // 20 + }; + + StringUIValue m_Text; // 00 + StringUIValue m_Description; // 20 + TUIValue m_ID; // 40 + TUIValue m_Type; // 60 + TUIValue m_Category; // 80 + TUIValue m_Enabled; // A0 + NestedUIValue m_SliderData; // C0 + NestedUIValue m_StepperData; // 158 + NestedUIValue m_CheckBoxData; // 248 + char _pad2C0[0x58]; // 2D0 + + GeneralSetting() + { + auto addr = dku::Hook::Module::get().base() + 0x20B801C; + auto func = reinterpret_cast(addr); + func(*this); + } + + ~GeneralSetting() + { + auto addr = dku::Hook::Module::get().base() + 0x20B9220; + auto func = reinterpret_cast(addr); + func(*this); + } + }; + + ArrayNestedUIValue m_Settings; // 0 + char _pad78[0x8]; // 78 + }; + + class SettingsDataModel + { + public: + class UpdateEventData + { + public: + SettingsDataModel* m_Model; + union + { + bool Bool; + int Int; + float Float; + } m_Value; + int m_ID; + }; + + char _pad0[0x190 + 0x20]; + TUIDataShuttleContainerMap m_SubSettingsMap; + }; + + // Sanity checks + static_assert(sizeof(NestedUIValue) == 0x98); + static_assert(sizeof(NestedUIValue) == 0xF0); + static_assert(sizeof(NestedUIValue) == 0x78); + + static_assert(sizeof(TUIDataShuttleContainerMap) == 0x70); + static_assert(sizeof(TUIDataShuttleContainerMap) == 0xC8); + static_assert(sizeof(TUIDataShuttleContainerMap) == 0x50); + + static_assert(sizeof(ArrayUIValue, 0>) == 0x78); + + static_assert(sizeof(SubSettingsList::GeneralSetting) == 0x318); + static_assert(sizeof(ArrayNestedUIValue) == 0x78); + + static_assert(sizeof(TUIDataShuttleContainerMap) == 0xA8); +} \ No newline at end of file diff --git a/Plugin/src/RE/UI.h b/Plugin/src/RE/UI.h new file mode 100644 index 00000000..c0d3886f --- /dev/null +++ b/Plugin/src/RE/UI.h @@ -0,0 +1,194 @@ +#pragma once + +#include "UIDataShuttle.h" + +namespace RE +{ + class BSFixedStringCS + { + private: + void* m_EntryPoolData = nullptr; + + public: + BSFixedStringCS() = default; + BSFixedStringCS(const char *String) + { + auto addr = dku::Hook::Module::get().base() + 0x3149530; + auto func = reinterpret_cast(addr); + func(this, String); + } + BSFixedStringCS(const BSFixedStringCS& Other) = delete; + ~BSFixedStringCS() + { + auto addr = dku::Hook::Module::get().base() + 0x0546D20; + auto func = reinterpret_cast(addr); + func(this); + } + BSFixedStringCS& operator=(const BSFixedStringCS& Other) = delete; + }; + + class __declspec(novtable) IUIValue + { + public: + IUIValue() = default; + virtual ~IUIValue() = default; // 00 + virtual void IUIValue_01() = 0; // 01 + virtual void IUIValue_02() = 0; // 02 + virtual void IUIValue_03() = 0; // 03 + virtual void IUIValue_04() = 0; // 04 + virtual void IUIValue_05() = 0; // 05 + virtual void IUIValue_06() = 0; // 06 + virtual void IUIValue_07() = 0; // 07 + virtual bool GetNeedsUpdate(); // 08 + virtual void SetNeedsUpdateAndNotifyParent(bool NeedsUpdate); // 09 + virtual void SetNeedsUpdate(); // 0A + virtual void IUIValue_0B() = 0; // 0B + + void* m_Parent = nullptr; // 08 + bool m_NeedsUpdate = true; // 10 + + void TriggerParentUpdate() + { + if (m_Parent) { + if ((*(bool (**)(void*))(*(__int64*)m_Parent + 48))(m_Parent)) + (*(void (**)(void*, IUIValue&))(*(__int64*)m_Parent + 16))(m_Parent, *this); + } + } + }; + static_assert(sizeof(IUIValue) == 0x18); + + class __declspec(novtable) IArrayUIValue + { + public: + }; + static_assert(sizeof(IArrayUIValue) == 0x1); + + template + class __declspec(novtable) TUIValue : public IUIValue + { + public: + TUIValue() = default; + virtual ~TUIValue() override = default; // 00 + virtual void IUIValue_01() override; // 01 + virtual void IUIValue_02() override; // 02 + virtual void IUIValue_03() override; // 03 + virtual void IUIValue_04() override; // 04 + virtual void IUIValue_05() override; // 05 + virtual void IUIValue_06() override; // 06 + virtual void IUIValue_07() override; // 07 + virtual void IUIValue_0B() override; // 0B + + T m_Value = {}; // 18 + + TUIValue& SetValue(const T& Value) + { + TriggerParentUpdate(); + + if (m_Value != Value) { + m_Value = Value; + SetNeedsUpdateAndNotifyParent(true); + } + + return *this; + } + }; + static_assert(sizeof(TUIValue) == 0x20); + static_assert(sizeof(TUIValue) == 0x20); + + class __declspec(novtable) StringUIValue : public TUIValue + { + public: + StringUIValue() = default; + virtual ~StringUIValue() override = default; // 00 + virtual void StringUIValue_0C(); // 0C + virtual void SetStringValue(const char* Value); // 0D + }; + static_assert(sizeof(StringUIValue) == 0x20); + + template + class __declspec(novtable) NestedUIValue : public IUIValue, public UIDataShuttle + { + public: + NestedUIValue() = default; + virtual ~NestedUIValue() override = default; // 00 + + // IUIValue + virtual void IUIValue_01() override; // 01 + virtual void IUIValue_02() override; // 02 + virtual void IUIValue_03() override; // 03 + virtual void IUIValue_04() override; // 04 + virtual void IUIValue_05() override; // 05 + virtual void IUIValue_06() override; // 06 + virtual void IUIValue_07() override; // 07 + virtual void SetNeedsUpdateAndNotifyParent(bool NeedsUpdate) override; // 09 + virtual void SetNeedsUpdate() override; // 0A + virtual void IUIValue_0B() override; // 0B + + // UIDataShuttle + virtual void UIDataShuttle_01() override; // 01 + virtual void UIDataShuttle_02() override; // 02 + virtual void UIDataShuttle_03() override; // 03 + virtual void UIDataShuttle_04() override; // 04 + virtual void UIDataShuttle_05() override; // 05 + virtual bool UIDataShuttle_06() override; // 06 + virtual bool UIDataShuttle_07() override; // 07 + + TUIDataShuttleContainerMap m_ShuttleMap; // 28 + }; + + template + class __declspec(novtable) ArrayUIValue : public IUIValue, public UIDataShuttle, public IArrayUIValue + { + public: + ArrayUIValue() = default; + virtual ~ArrayUIValue() override = default; // 00 + + // IUIValue + virtual void IUIValue_01() override; // 01 + virtual void IUIValue_02() override; // 02 + virtual void IUIValue_03() override; // 03 + virtual void IUIValue_04() override; // 04 + virtual void IUIValue_05() override; // 05 + virtual void IUIValue_06() override; // 06 + virtual void IUIValue_07() override; // 07 + virtual void SetNeedsUpdateAndNotifyParent(bool NeedsUpdate) override; // 09 + virtual void SetNeedsUpdate() override; // 0A + virtual void IUIValue_0B() override; // 0B + + // UIDataShuttle + virtual void UIDataShuttle_01() override; // 01 + virtual void UIDataShuttle_02() override; // 02 + virtual void UIDataShuttle_03() override; // 03 + virtual void UIDataShuttle_04() override; // 04 + virtual void UIDataShuttle_05() override; // 05 + virtual bool UIDataShuttle_06() override; // 06 + virtual bool UIDataShuttle_07() override; // 07 + + TUIDataShuttleContainerArray m_ShuttleArray; // 28 + + void AddItem(const BSFixedStringCS& Item) + { + auto addr = dku::Hook::Module::get().base() + 0x207D080; + auto func = reinterpret_cast*, const BSFixedStringCS&)>(addr); + func(this, Item); + } + + [[nodiscard]] auto Items() const noexcept + { + return m_ShuttleArray.Items(); + } + }; + + template + class __declspec(novtable) ArrayNestedUIValue : public ArrayUIValue, Count> + { + public: + void AddItem(T& Item) + { + // Seems like they have a helper function to apply NestedUIValue<> to the type + auto addr = dku::Hook::Module::get().base() + 0x20B50C8; + auto func = reinterpret_cast> *, T&)>(addr); + func(&__super::m_ShuttleArray, Item); + } + }; +} \ No newline at end of file diff --git a/Plugin/src/RE/UIDataShuttle.h b/Plugin/src/RE/UIDataShuttle.h new file mode 100644 index 00000000..18573a42 --- /dev/null +++ b/Plugin/src/RE/UIDataShuttle.h @@ -0,0 +1,91 @@ +#pragma once + +#include + +namespace RE +{ + class __declspec(novtable) UIDataShuttle + { + public: + UIDataShuttle() = default; + virtual ~UIDataShuttle() = default; // 00 + virtual void UIDataShuttle_01() = 0; // 01 + virtual void UIDataShuttle_02() = 0; // 02 + virtual void UIDataShuttle_03() = 0; // 03 + virtual void UIDataShuttle_04() = 0; // 04 + virtual void UIDataShuttle_05() = 0; // 05 + virtual bool UIDataShuttle_06() = 0; // 06 + virtual bool UIDataShuttle_07() = 0; // 07 + + char _pad0[0x8]; // 08 + }; + static_assert(sizeof(UIDataShuttle) == 0x10); + + class __declspec(novtable) UIDataShuttleContainer + { + public: + UIDataShuttleContainer() = default; + virtual void UIDataShuttleContainer_00(); // 00 + virtual void UIDataShuttleContainer_01(); // 01 + virtual void UIDataShuttleContainer_02() = 0; // 02 + virtual void UIDataShuttleContainer_03() = 0; // 03 + virtual void UIDataShuttleContainer_04() = 0; // 04 + virtual void UIDataShuttleContainer_05(); // 05 + virtual void UIDataShuttleContainer_06(); // 06 + virtual void UIDataShuttleContainer_07(); // 07 + virtual void UIDataShuttleContainer_08() = 0; // 08 + virtual ~UIDataShuttleContainer() = default; // 09 + + char _pad0[0x8]; // 08 + }; + static_assert(sizeof(UIDataShuttleContainer) == 0x10); + + template + class __declspec(novtable) TUIDataShuttleContainerArray : public UIDataShuttleContainer + { + public: + TUIDataShuttleContainerArray() = default; + virtual void UIDataShuttleContainer_00() override; // 00 + virtual void UIDataShuttleContainer_01() override; // 01 + virtual void UIDataShuttleContainer_02() override; // 02 + virtual void UIDataShuttleContainer_03() override; // 03 + virtual void UIDataShuttleContainer_04() override; // 04 + virtual void UIDataShuttleContainer_05() override; // 05 + virtual void UIDataShuttleContainer_06() override; // 06 + virtual void UIDataShuttleContainer_07() override; // 07 + virtual void UIDataShuttleContainer_08() override; // 08 + virtual ~TUIDataShuttleContainerArray() override = default; // 09 + + T* m_ArrayBegin; // 10 + T* m_ArrayEnd; // 18 + char _pad20[0x30]; // 20 + + [[nodiscard]] auto Items() const noexcept + { + return { m_ArrayBegin, m_ArrayEnd }; + } + }; + //static_assert(sizeof(TUIDataShuttleContainerArray>) == 0x50); + + template + class __declspec(novtable) TUIDataShuttleContainerMap : public UIDataShuttleContainer + { + public: + TUIDataShuttleContainerMap() = default; + virtual void UIDataShuttleContainer_00() override; // 00 + virtual void UIDataShuttleContainer_02() override; // 02 + virtual void UIDataShuttleContainer_03() override; // 03 + virtual void UIDataShuttleContainer_04() override; // 04 + virtual void UIDataShuttleContainer_05() override; // 05 + virtual void UIDataShuttleContainer_06() override; // 06 + virtual void UIDataShuttleContainer_07() override; // 07 + virtual void UIDataShuttleContainer_08() override; // 08 + virtual ~TUIDataShuttleContainerMap() override = default; // 09 + virtual void TUIDataShuttleContainerMap_0A(); // 0A Identical to GetDataHolder(). Const version? + virtual T& GetData(); // 0B + + char _pad10[0x18]; // 10 + T m_Data; // 28 + }; + //static_assert((sizeof(TUIDataShuttleContainerMap>) - sizeof(TUIValue)) == 0x28); +} \ No newline at end of file From c835e8003279e5c1e6f460a4b6d12a964f1e97bc Mon Sep 17 00:00:00 2001 From: Nukem Date: Sun, 24 Sep 2023 09:31:56 -0400 Subject: [PATCH 019/270] BSFixedStringCS belongs in its own file --- Plugin/src/RE/BSFixedString.h | 41 +++++++++++++++++++++++++++++++++++ Plugin/src/RE/UI.h | 24 +------------------- 2 files changed, 42 insertions(+), 23 deletions(-) create mode 100644 Plugin/src/RE/BSFixedString.h diff --git a/Plugin/src/RE/BSFixedString.h b/Plugin/src/RE/BSFixedString.h new file mode 100644 index 00000000..8217b242 --- /dev/null +++ b/Plugin/src/RE/BSFixedString.h @@ -0,0 +1,41 @@ +#pragma once + +namespace RE +{ + class BSFixedStringCS + { + private: + void* m_EntryPoolData = nullptr; + + public: + BSFixedStringCS() = default; + + BSFixedStringCS(const char* String) + { + auto addr = dku::Hook::Module::get().base() + 0x3149530; + auto func = reinterpret_cast(addr); + func(this, String); + } + + BSFixedStringCS(const BSFixedStringCS& Other) = delete; + + ~BSFixedStringCS() + { + auto addr = dku::Hook::Module::get().base() + 0x0546D20; + auto func = reinterpret_cast(addr); + func(this); + } + + BSFixedStringCS& operator=(const BSFixedStringCS& Other) = delete; + + bool operator==(const BSFixedStringCS& Other) const + { + return m_EntryPoolData == Other.m_EntryPoolData; + } + + bool operator!=(const BSFixedStringCS& Other) const + { + return m_EntryPoolData != Other.m_EntryPoolData; + } + }; +} \ No newline at end of file diff --git a/Plugin/src/RE/UI.h b/Plugin/src/RE/UI.h index c0d3886f..a0ae70ae 100644 --- a/Plugin/src/RE/UI.h +++ b/Plugin/src/RE/UI.h @@ -1,32 +1,10 @@ #pragma once +#include "BSFixedString.h" #include "UIDataShuttle.h" namespace RE { - class BSFixedStringCS - { - private: - void* m_EntryPoolData = nullptr; - - public: - BSFixedStringCS() = default; - BSFixedStringCS(const char *String) - { - auto addr = dku::Hook::Module::get().base() + 0x3149530; - auto func = reinterpret_cast(addr); - func(this, String); - } - BSFixedStringCS(const BSFixedStringCS& Other) = delete; - ~BSFixedStringCS() - { - auto addr = dku::Hook::Module::get().base() + 0x0546D20; - auto func = reinterpret_cast(addr); - func(this); - } - BSFixedStringCS& operator=(const BSFixedStringCS& Other) = delete; - }; - class __declspec(novtable) IUIValue { public: From cd1a8c2fdd8b084afd4e21ce3eb184e47eff2276 Mon Sep 17 00:00:00 2001 From: Nukem Date: Sun, 24 Sep 2023 09:33:46 -0400 Subject: [PATCH 020/270] Add callback hooks for SettingsDataModel --- Plugin/src/Hooks.cpp | 42 +++++++++++ Plugin/src/Hooks.h | 9 +++ Plugin/src/RE/SettingsDataModel.h | 22 ++++-- Plugin/src/RE/UI.h | 114 +++++++++++++++--------------- Plugin/src/RE/UIDataShuttle.h | 24 ++++--- 5 files changed, 137 insertions(+), 74 deletions(-) diff --git a/Plugin/src/Hooks.cpp b/Plugin/src/Hooks.cpp index 85a43877..ce05adec 100644 --- a/Plugin/src/Hooks.cpp +++ b/Plugin/src/Hooks.cpp @@ -199,6 +199,48 @@ namespace Hooks _CreateDataModelOptions(a_arg1, a_SettingList); } + void DebugHooks::Hook_SettingsDataModelBoolEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData) + { + char buffer[128]; + sprintf_s(buffer, "Got a bool event: %d %s\n", EventData.m_SettingID, EventData.m_Value.Bool ? "true" : "false"); + OutputDebugStringA(buffer); + + _SettingsDataModelBoolEvent(a_arg1, EventData); + } + + void DebugHooks::Hook_SettingsDataModelIntEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData) + { + char buffer[128]; + sprintf_s(buffer, "Got an integer event: %d %d\n", EventData.m_SettingID, EventData.m_Value.Int); + OutputDebugStringA(buffer); + + if (auto settingTest = EventData.m_Model->FindSettingById(602)) + { + OutputDebugStringA("Manually updating checkbox state\n"); + settingTest->m_CheckBoxData.m_ShuttleMap.GetData().m_Value.SetValue(false); + } + + _SettingsDataModelIntEvent(a_arg1, EventData); + } + + void DebugHooks::Hook_SettingsDataModelFloatEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData) + { + char buffer[128]; + sprintf_s(buffer, "Got a float event: %d %f\n", EventData.m_SettingID, EventData.m_Value.Float); + OutputDebugStringA(buffer); + + if (EventData.m_SettingID >= 600) { + if (auto settingTest = EventData.m_Model->FindSettingById(EventData.m_SettingID)) { + char buffer[128]; + sprintf_s(buffer, "%.0f%%", EventData.m_Value.Float * 100.0f); + + settingTest->m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue(buffer); + } + } + + _SettingsDataModelFloatEvent(a_arg1, EventData); + } + void DebugHooks::Hook_CreateRenderTargetView(uintptr_t a1, ID3D12Resource* a_resource, DXGI_FORMAT a_format, uint8_t a4, uint16_t a5, uintptr_t a6) { const auto textureDesc = a_resource->GetDesc(); diff --git a/Plugin/src/Hooks.h b/Plugin/src/Hooks.h index f42bacf9..f2e760b5 100644 --- a/Plugin/src/Hooks.h +++ b/Plugin/src/Hooks.h @@ -108,6 +108,9 @@ namespace Hooks static void Hook() { _CreateDataModelOptions = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20BBAA9, Hook_CreateDataModelOptions); + _SettingsDataModelBoolEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20C9DEE, Hook_SettingsDataModelBoolEvent); + _SettingsDataModelIntEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CA0E7, Hook_SettingsDataModelIntEvent); + _SettingsDataModelFloatEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CA09B, Hook_SettingsDataModelFloatEvent); #if 0 const auto callsite1 = AsAddress(dku::Hook::Module::get().base() + 0x32ED294); @@ -125,6 +128,12 @@ namespace Hooks private: static void Hook_CreateDataModelOptions(void* a_arg1, RE::ArrayNestedUIValue& a_SettingList); static inline std::add_pointer_t _CreateDataModelOptions; + static void Hook_SettingsDataModelBoolEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData); + static inline std::add_pointer_t _SettingsDataModelBoolEvent; + static void Hook_SettingsDataModelIntEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData); + static inline std::add_pointer_t _SettingsDataModelIntEvent; + static void Hook_SettingsDataModelFloatEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData); + static inline std::add_pointer_t _SettingsDataModelFloatEvent; static void Hook_CreateRenderTargetView(uintptr_t a1, ID3D12Resource* a_resource, DXGI_FORMAT a_format, uint8_t a4, uint16_t a5, uintptr_t a6); static void Hook_CreateDepthStencilView(uintptr_t a1, ID3D12Resource* a_resource, DXGI_FORMAT a_format, uint8_t a4, uint16_t a5, uintptr_t a6); diff --git a/Plugin/src/RE/SettingsDataModel.h b/Plugin/src/RE/SettingsDataModel.h index f6fb05f5..fe7bc9f3 100644 --- a/Plugin/src/RE/SettingsDataModel.h +++ b/Plugin/src/RE/SettingsDataModel.h @@ -65,7 +65,7 @@ namespace RE NestedUIValue m_SliderData; // C0 NestedUIValue m_StepperData; // 158 NestedUIValue m_CheckBoxData; // 248 - char _pad2C0[0x58]; // 2D0 + char _pad2C0[0x8]; // 2D0 GeneralSetting() { @@ -99,11 +99,23 @@ namespace RE int Int; float Float; } m_Value; - int m_ID; + int m_SettingID; }; - char _pad0[0x190 + 0x20]; - TUIDataShuttleContainerMap m_SubSettingsMap; + char _pad0[0x190 + 0x20]; // 0 + TUIDataShuttleContainerMap m_SubSettingsMap; // 1B0 + + SubSettingsList::GeneralSetting* FindSettingById(int Id) + { + // This is supposed to be held under a lock but I don't care enough right now + for (auto& item : m_SubSettingsMap.GetData().m_Settings.Items()) + { + if (item.m_ShuttleMap.GetData().m_ID.m_Value == Id) + return &item.m_ShuttleMap.GetData(); + } + + return nullptr; + } }; // Sanity checks @@ -117,7 +129,7 @@ namespace RE static_assert(sizeof(ArrayUIValue, 0>) == 0x78); - static_assert(sizeof(SubSettingsList::GeneralSetting) == 0x318); + static_assert(sizeof(NestedUIValue) == 0x318); static_assert(sizeof(ArrayNestedUIValue) == 0x78); static_assert(sizeof(TUIDataShuttleContainerMap) == 0xA8); diff --git a/Plugin/src/RE/UI.h b/Plugin/src/RE/UI.h index a0ae70ae..aa29949b 100644 --- a/Plugin/src/RE/UI.h +++ b/Plugin/src/RE/UI.h @@ -9,28 +9,26 @@ namespace RE { public: IUIValue() = default; - virtual ~IUIValue() = default; // 00 - virtual void IUIValue_01() = 0; // 01 - virtual void IUIValue_02() = 0; // 02 - virtual void IUIValue_03() = 0; // 03 - virtual void IUIValue_04() = 0; // 04 - virtual void IUIValue_05() = 0; // 05 - virtual void IUIValue_06() = 0; // 06 - virtual void IUIValue_07() = 0; // 07 - virtual bool GetNeedsUpdate(); // 08 - virtual void SetNeedsUpdateAndNotifyParent(bool NeedsUpdate); // 09 - virtual void SetNeedsUpdate(); // 0A - virtual void IUIValue_0B() = 0; // 0B - - void* m_Parent = nullptr; // 08 - bool m_NeedsUpdate = true; // 10 + virtual ~IUIValue() = default; // 00 + virtual void IUIValue_01() = 0; // 01 + virtual void IUIValue_02() = 0; // 02 + virtual void IUIValue_03() = 0; // 03 + virtual void IUIValue_04() = 0; // 04 + virtual void IUIValue_05() = 0; // 05 + virtual void IUIValue_06() = 0; // 06 + virtual void IUIValue_07() = 0; // 07 + virtual bool GetNeedsUpdate(); // 08 + virtual void SetNeedsUpdateAndNotifyShuttle(bool NeedsUpdate); // 09 + virtual void SetNeedsUpdate(); // 0A + virtual void IUIValue_0B() = 0; // 0B + + UIDataShuttle* m_DataShuttle = nullptr; // 08 + bool m_NeedsUpdate = true; // 10 void TriggerParentUpdate() { - if (m_Parent) { - if ((*(bool (**)(void*))(*(__int64*)m_Parent + 48))(m_Parent)) - (*(void (**)(void*, IUIValue&))(*(__int64*)m_Parent + 16))(m_Parent, *this); - } + if (m_DataShuttle && m_DataShuttle->UIDataShuttle_06()) + m_DataShuttle->UIDataShuttle_02(*this); } }; static_assert(sizeof(IUIValue) == 0x18); @@ -64,7 +62,7 @@ namespace RE if (m_Value != Value) { m_Value = Value; - SetNeedsUpdateAndNotifyParent(true); + SetNeedsUpdateAndNotifyShuttle(true); } return *this; @@ -77,9 +75,9 @@ namespace RE { public: StringUIValue() = default; - virtual ~StringUIValue() override = default; // 00 - virtual void StringUIValue_0C(); // 0C - virtual void SetStringValue(const char* Value); // 0D + virtual ~StringUIValue() override = default; // 00 + virtual void StringUIValue_0C(); // 0C + virtual void SetStringValue(const char* Value); // 0D }; static_assert(sizeof(StringUIValue) == 0x20); @@ -91,25 +89,25 @@ namespace RE virtual ~NestedUIValue() override = default; // 00 // IUIValue - virtual void IUIValue_01() override; // 01 - virtual void IUIValue_02() override; // 02 - virtual void IUIValue_03() override; // 03 - virtual void IUIValue_04() override; // 04 - virtual void IUIValue_05() override; // 05 - virtual void IUIValue_06() override; // 06 - virtual void IUIValue_07() override; // 07 - virtual void SetNeedsUpdateAndNotifyParent(bool NeedsUpdate) override; // 09 - virtual void SetNeedsUpdate() override; // 0A - virtual void IUIValue_0B() override; // 0B + virtual void IUIValue_01() override; // 01 + virtual void IUIValue_02() override; // 02 + virtual void IUIValue_03() override; // 03 + virtual void IUIValue_04() override; // 04 + virtual void IUIValue_05() override; // 05 + virtual void IUIValue_06() override; // 06 + virtual void IUIValue_07() override; // 07 + virtual void SetNeedsUpdateAndNotifyShuttle(bool NeedsUpdate) override; // 09 + virtual void SetNeedsUpdate() override; // 0A + virtual void IUIValue_0B() override; // 0B // UIDataShuttle - virtual void UIDataShuttle_01() override; // 01 - virtual void UIDataShuttle_02() override; // 02 - virtual void UIDataShuttle_03() override; // 03 - virtual void UIDataShuttle_04() override; // 04 - virtual void UIDataShuttle_05() override; // 05 - virtual bool UIDataShuttle_06() override; // 06 - virtual bool UIDataShuttle_07() override; // 07 + virtual void UIDataShuttle_01() override; // 01 + virtual void UIDataShuttle_02(IUIValue& UIValue) override; // 02 + virtual void UIDataShuttle_03() override; // 03 + virtual void UIDataShuttle_04() override; // 04 + virtual void UIDataShuttle_05() override; // 05 + virtual bool UIDataShuttle_06() override; // 06 + virtual bool UIDataShuttle_07() override; // 07 TUIDataShuttleContainerMap m_ShuttleMap; // 28 }; @@ -122,25 +120,25 @@ namespace RE virtual ~ArrayUIValue() override = default; // 00 // IUIValue - virtual void IUIValue_01() override; // 01 - virtual void IUIValue_02() override; // 02 - virtual void IUIValue_03() override; // 03 - virtual void IUIValue_04() override; // 04 - virtual void IUIValue_05() override; // 05 - virtual void IUIValue_06() override; // 06 - virtual void IUIValue_07() override; // 07 - virtual void SetNeedsUpdateAndNotifyParent(bool NeedsUpdate) override; // 09 - virtual void SetNeedsUpdate() override; // 0A - virtual void IUIValue_0B() override; // 0B + virtual void IUIValue_01() override; // 01 + virtual void IUIValue_02() override; // 02 + virtual void IUIValue_03() override; // 03 + virtual void IUIValue_04() override; // 04 + virtual void IUIValue_05() override; // 05 + virtual void IUIValue_06() override; // 06 + virtual void IUIValue_07() override; // 07 + virtual void SetNeedsUpdateAndNotifyShuttle(bool NeedsUpdate) override; // 09 + virtual void SetNeedsUpdate() override; // 0A + virtual void IUIValue_0B() override; // 0B // UIDataShuttle - virtual void UIDataShuttle_01() override; // 01 - virtual void UIDataShuttle_02() override; // 02 - virtual void UIDataShuttle_03() override; // 03 - virtual void UIDataShuttle_04() override; // 04 - virtual void UIDataShuttle_05() override; // 05 - virtual bool UIDataShuttle_06() override; // 06 - virtual bool UIDataShuttle_07() override; // 07 + virtual void UIDataShuttle_01() override; // 01 + virtual void UIDataShuttle_02(IUIValue& UIValue) override; // 02 + virtual void UIDataShuttle_03() override; // 03 + virtual void UIDataShuttle_04() override; // 04 + virtual void UIDataShuttle_05() override; // 05 + virtual bool UIDataShuttle_06() override; // 06 + virtual bool UIDataShuttle_07() override; // 07 TUIDataShuttleContainerArray m_ShuttleArray; // 28 @@ -165,7 +163,7 @@ namespace RE { // Seems like they have a helper function to apply NestedUIValue<> to the type auto addr = dku::Hook::Module::get().base() + 0x20B50C8; - auto func = reinterpret_cast> *, T&)>(addr); + auto func = reinterpret_cast>*, T&)>(addr); func(&__super::m_ShuttleArray, Item); } }; diff --git a/Plugin/src/RE/UIDataShuttle.h b/Plugin/src/RE/UIDataShuttle.h index 18573a42..a790653c 100644 --- a/Plugin/src/RE/UIDataShuttle.h +++ b/Plugin/src/RE/UIDataShuttle.h @@ -4,18 +4,20 @@ namespace RE { + class IUIValue; + class __declspec(novtable) UIDataShuttle { public: UIDataShuttle() = default; - virtual ~UIDataShuttle() = default; // 00 - virtual void UIDataShuttle_01() = 0; // 01 - virtual void UIDataShuttle_02() = 0; // 02 - virtual void UIDataShuttle_03() = 0; // 03 - virtual void UIDataShuttle_04() = 0; // 04 - virtual void UIDataShuttle_05() = 0; // 05 - virtual bool UIDataShuttle_06() = 0; // 06 - virtual bool UIDataShuttle_07() = 0; // 07 + virtual ~UIDataShuttle() = default; // 00 + virtual void UIDataShuttle_01() = 0; // 01 + virtual void UIDataShuttle_02(IUIValue& UIValue) = 0; // 02 + virtual void UIDataShuttle_03() = 0; // 03 + virtual void UIDataShuttle_04() = 0; // 04 + virtual void UIDataShuttle_05() = 0; // 05 + virtual bool UIDataShuttle_06() = 0; // 06 + virtual bool UIDataShuttle_07() = 0; // 07 char _pad0[0x8]; // 08 }; @@ -62,7 +64,7 @@ namespace RE [[nodiscard]] auto Items() const noexcept { - return { m_ArrayBegin, m_ArrayEnd }; + return std::span{ m_ArrayBegin, m_ArrayEnd }; } }; //static_assert(sizeof(TUIDataShuttleContainerArray>) == 0x50); @@ -81,8 +83,8 @@ namespace RE virtual void UIDataShuttleContainer_07() override; // 07 virtual void UIDataShuttleContainer_08() override; // 08 virtual ~TUIDataShuttleContainerMap() override = default; // 09 - virtual void TUIDataShuttleContainerMap_0A(); // 0A Identical to GetDataHolder(). Const version? - virtual T& GetData(); // 0B + virtual void TUIDataShuttleContainerMap_0A(); // 0A Identical to GetData(). Const version? + virtual T& GetData(); // 0A char _pad10[0x18]; // 10 T m_Data; // 28 From 85b083d3405610182230b7070d02c0d92b0d505b Mon Sep 17 00:00:00 2001 From: Lilium Date: Mon, 25 Sep 2023 17:27:16 +0200 Subject: [PATCH 021/270] another cleanup --- shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl | 4 +- .../ColorGradingMerge_FF81_cs.hlsl | 197 ++++----- ...ContrastAdaptiveSharpening_100FF94_cs.hlsl | 4 +- shaders/FilmGrain/FilmGrain_FF75_ps.hlsl | 4 +- shaders/HDRComposite/HDRComposite_ps.hlsl | 412 ++++++++---------- shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl | 4 +- .../ScaleformComposite_FFAA_ps.hlsl | 4 +- shaders/{color.h => color.hlsl} | 5 +- shaders/compile_all_shaders.bat | 35 +- shaders/{math.h => math.hlsl} | 5 +- shaders/{shared.h => shared.hlsl} | 16 +- shaders/structs.hlsl | 143 ++++++ 12 files changed, 474 insertions(+), 359 deletions(-) rename shaders/{color.h => color.hlsl} (98%) rename shaders/{math.h => math.hlsl} (98%) rename shaders/{shared.h => shared.hlsl} (68%) create mode 100644 shaders/structs.hlsl diff --git a/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl b/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl index c7a17b7f..767eb902 100644 --- a/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl +++ b/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl @@ -1,5 +1,5 @@ -#include "../shared.h" -#include "../color.h" +#include "../shared.hlsl" +#include "../color.hlsl" #define HDR_USE_GAMMA_2_2 1 diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 8e518443..2c51b6c3 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -1,6 +1,6 @@ -#include "../shared.h" -#include "../color.h" -#include "../math.h" +#include "../shared.hlsl" +#include "../color.hlsl" +#include "../math.hlsl" #define LUT_SIZE_UINT (uint)LUT_SIZE @@ -15,20 +15,17 @@ static float additionalNeutralLUTPercentage = 0.0f; static float LUTCorrectionPercentage = 1.0f; -struct PushConstantWrapper_ColorGradingMerge -{ - float LUT1Percentage; - float LUT2Percentage; - float LUT3Percentage; - float LUT4Percentage; - float neutralLUTPercentage; -}; - -cbuffer PushConstantWrapper_ColorGradingMerge : register(b0, space0) +cbuffer CPushConstantWrapper_ColorGradingMerge : register(b0, space0) { PushConstantWrapper_ColorGradingMerge PcwColorGradingMerge : packoffset(c0); }; +Texture2D LUT1 : register(t0, space8); +Texture2D LUT2 : register(t1, space8); +Texture2D LUT3 : register(t2, space8); +Texture2D LUT4 : register(t3, space8); +RWTexture3D OutMixetLUT : register(u0, space8); + struct LUTAnalysis { float minChannel; @@ -59,33 +56,32 @@ struct LUTAnalysis static const uint LUTSizeLog2 = (uint)log2(LUT_SIZE); // In/Out in pixels -uint3 ThreeToTwoDimensionCoordinates(uint x, uint y, uint z) +uint3 ThreeToTwoDimensionCoordinates(uint3 UVW) { // 2D LUT extends horizontally. - const uint U = (z << LUTSizeLog2) + x; - const uint3 UVW = uint3(U, y, 0u); - return UVW; + const uint U = (UVW.z << LUTSizeLog2) + UVW.x; + return uint3(U, UVW.y, 0); } void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) { - Analysis.black = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(0, 0, 0)).rgb); - Analysis.white = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(LUT_SIZE_UINT - 1u, LUT_SIZE_UINT - 1u, LUT_SIZE_UINT - 1u)).rgb); + Analysis.black = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(0)).rgb); + Analysis.white = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(LUT_SIZE_UINT - 1u)).rgb); #if UNUSED_PARAMS Analysis.minY = FLT_MAX; Analysis.maxY = -FLT_MAX; Analysis.blackY = Luminance(Analysis.black); Analysis.whiteY = Luminance(Analysis.white); #endif // UNUSED_PARAMS - + float3 colors = 0.f; float Ys = 0.f; - + float3 minColor = FLT_MAX; float3 maxColor = -FLT_MAX; - + const uint texelCount = LUT_SIZE_UINT * LUT_SIZE_UINT * LUT_SIZE_UINT; - + // TODO: this loops on every LUT pixel for every output pixel. // given we only need the min/max color channels here, to optimize we could: // -Just iterate on the last (e.g.) 3 pixels of each axis, which should guarantee to find the max for all normal LUTs. @@ -100,27 +96,27 @@ void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) { for (uint z = 0; z < LUT_SIZE_UINT; z += analyzeTexelFrequency) { - float3 LUTColor = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(x, y, z)).rgb); + float3 LUTColor = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(uint3(x, y, z))).rgb); minColor = min(minColor, LUTColor); maxColor = max(maxColor, LUTColor); - + #if UNUSED_PARAMS float Y = Luminance(LUTColor); Analysis.minY = min(Analysis.minY, Y); Analysis.maxY = max(Analysis.maxY, Y); Ys += Y; - + colors += LUTColor.r; #endif // UNUSED_PARAMS } } } - + //TODO: either store min/max channels merged or separately, but not both Analysis.minChannel = min(minColor.r, min(minColor.g, minColor.b)); Analysis.maxChannel = max(maxColor.r, max(maxColor.g, maxColor.b)); - + #if UNUSED_PARAMS Analysis.minRed = minRed; Analysis.minGreen = minGreen; @@ -128,7 +124,7 @@ void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) Analysis.maxRed = maxRed; Analysis.maxGreen = maxGreen; Analysis.maxBlue = maxBlue; - + Analysis.averageY = Ys / texelCount; Analysis.averageRed = colors.r / texelCount; Analysis.averageGreen = colors.g / texelCount; @@ -136,7 +132,7 @@ void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) Analysis.averageChannel = (Analysis.averageRed + Analysis.averageGreen + Analysis.averageBlue) / 3.f; Analysis.range = Analysis.maxY - Analysis.minY; - + //TODO: ??? Can't do this in shader but it's not needed //Ys.sort((a, b) => a - b); //Analysis.medianY = ((Ys[Math.floor(texelCount / 2)]) + (Ys[Math.ceil(texelCount / 2)])) / 2; @@ -149,33 +145,35 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b { LUTAnalysis analysis; AnalyzeLUT(LUT, analysis); - + // Black will be scaled by min channel float3 scaledBlack = 1.f - ((1.f - analysis.black) * (1.f / (1.f - analysis.minChannel))); // White will be scaled up by max channel float3 scaledWhite = analysis.white / analysis.maxChannel; float scaledBlackY = Luminance(scaledBlack); float scaledWhiteY = Luminance(scaledWhite); - + #if 0 //TODO: delete once verified this works float scaleDown = (0.f - analysis.minChannel); float scaleUp = (1.f - analysis.maxChannel); - if (saturate(scaleDown) != scaleDown || saturate(scaleUp) != scaleUp - || saturate(scaledBlackY) != scaledBlackY || saturate(scaledWhiteY) != scaledWhiteY) + if (saturate(scaleDown) != scaleDown + || saturate(scaleUp) != scaleUp + || saturate(scaledBlackY) != scaledBlackY + || saturate(scaledWhiteY) != scaledWhiteY) { return 0; } #endif - + float3 color = gamma_sRGB_to_linear(LUT.Load(UVW).rgb); const float3 originalColor = color; - + //TODO: convert to using lerp function??? const float3 reducedColor = linearNormalization(neutralLUTColor, 0.f, 1.f, 1.f / (1.f - analysis.minChannel), 1.f); const float3 increasedColor = linearNormalization(neutralLUTColor, 0.f, 1.f, 1.f, 1.f / analysis.maxChannel); // Scale the color ("neutralLUTColor" here represents the coordinates of the point). color = (1.f - ((1.f - color) * reducedColor)) * increasedColor; - + const float blackDistance = hypot3(neutralLUTColor); const float whiteDistance = hypot3(1.f - neutralLUTColor); const float totalRange = (blackDistance + whiteDistance); @@ -186,61 +184,65 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b const float decreasedY = linearNormalization(blackDistance, 0.f, totalRange, 1.f / (1.f - scaledBlackY), 1.f); const float increasedY = linearNormalization(whiteDistance, 0.f, totalRange, 1.f / scaledWhiteY, 1.f); const float targetY = (1.f - ((1.f - sourceY) * decreasedY)) * increasedY; - if (targetY >= 0.9999f && targetY <= 1.0005f) { + + if (targetY >= 0.9999f && targetY <= 1.0005f) + { color = 1.f; // Intentionally targeting pure white (1,1,1) - } else if (SDRRange && targetY >= 0.999f) { + } + else if (SDRRange && targetY >= 0.999f) + { color = 1.f; // Clamp - } else { + } + else + { // targetY could be on LUTs (raised black point and dark blues) color *= max(targetY, 0.f) / sourceY; } - } else { + } + else + { color = 0; // Color may have gone below 0 when scaling } - + // Optional step to keep colors in the SDR range. if (SDRRange) + { color = saturate(color); - + } + color = lerp(originalColor, color, LUTCorrectionPercentage); - + return color; } -Texture2D LUT1 : register(t0, space8); -Texture2D LUT2 : register(t1, space8); -Texture2D LUT3 : register(t2, space8); -Texture2D LUT4 : register(t3, space8); -RWTexture3D outMixedLUT : register(u0, space8); -static uint3 gl_GlobalInvocationID; -struct SPIRV_Cross_Input +// Dispatch size is 1 1 16 (x and y have one thread and one thread group, while z has 16 thread groups with a thread each) +[numthreads(LUT_SIZE_UINT, LUT_SIZE_UINT, 1)] +void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) { - uint3 gl_GlobalInvocationID : SV_DispatchThreadID; -}; + const int3 inUVW = ThreeToTwoDimensionCoordinates(SV_DispatchThreadID); + #define outUVW SV_DispatchThreadID -void CS() -{ - const uint3 outUVW = uint3(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y, gl_GlobalInvocationID.z); // In pixels - const uint3 inUVW = ThreeToTwoDimensionCoordinates(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y, gl_GlobalInvocationID.z); - float3 neutralLUTColor = float3(outUVW) / (LUT_SIZE - 1.f); // The neutral LUT is automatically generated by the coordinates, but it's baked with sRGB gamma + +#if LUT_FIX_GAMMA_MAPPING || LUT_BLEND_LUTS_IN_LINEAR neutralLUTColor = gamma_sRGB_to_linear(neutralLUTColor); - +#endif + #if LUT_IMPROVEMENT_TYPE == 0 || LUT_IMPROVEMENT_TYPE == 2 float3 LUT1Color = LUT1.Load(inUVW); float3 LUT2Color = LUT2.Load(inUVW); float3 LUT3Color = LUT3.Load(inUVW); float3 LUT4Color = LUT4.Load(inUVW); +#if LUT_FIX_GAMMA_MAPPING || LUT_BLEND_LUTS_IN_LINEAR LUT1Color = gamma_sRGB_to_linear(LUT1Color); LUT2Color = gamma_sRGB_to_linear(LUT2Color); LUT3Color = gamma_sRGB_to_linear(LUT3Color); LUT4Color = gamma_sRGB_to_linear(LUT4Color); +#endif // LUT_FIX_GAMMA_MAPPING #endif // LUT_IMPROVEMENT_TYPE - -#if LUT_IMPROVEMENT_TYPE == 0 - // Nothing to do -#elif LUT_IMPROVEMENT_TYPE == 1 + +#if LUT_IMPROVEMENT_TYPE == 1 const bool SDRRange = false; float3 LUT1Color = PatchLUTColor(LUT1, inUVW, neutralLUTColor, SDRRange); float3 LUT2Color = PatchLUTColor(LUT3, inUVW, neutralLUTColor, SDRRange); @@ -252,47 +254,48 @@ void CS() float LUT2Luminance = Luminance(LUT2Color); float LUT3Luminance = Luminance(LUT3Color); float LUT4Luminance = Luminance(LUT4Color); - + if (LUT1Luminance != 0.f) + { LUT1Color *= lerp(1.f, neutralLUTLuminance / LUT1Luminance, LUTCorrectionPercentage); + } if (LUT2Luminance != 0.f) + { LUT2Color *= lerp(1.f, neutralLUTLuminance / LUT2Luminance, LUTCorrectionPercentage); + } if (LUT3Luminance != 0.f) + { LUT3Color *= lerp(1.f, neutralLUTLuminance / LUT3Luminance, LUTCorrectionPercentage); + } if (LUT4Luminance != 0.f) + { LUT4Color *= lerp(1.f, neutralLUTLuminance / LUT4Luminance, LUTCorrectionPercentage); + } #endif // LUT_IMPROVEMENT_TYPE - -#if !FIX_LUT_GAMMA_MAPPING && 0 // Disabled as it's still preferable to do LUT blends in linear space - neutralLUTColor = gamma_linear_to_sRGB(neutralLUTColor); - LUT1Color = gamma_linear_to_sRGB(LUT1Color); - LUT2Color = gamma_linear_to_sRGB(LUT2Color); - LUT3Color = gamma_linear_to_sRGB(LUT3Color); - LUT4Color = gamma_linear_to_sRGB(LUT4Color); -#endif // FIX_LUT_GAMMA_MAPPING - - PushConstantWrapper_ColorGradingMerge adjustedPcwColorGradingMerge = PcwColorGradingMerge; - adjustedPcwColorGradingMerge.neutralLUTPercentage = lerp(adjustedPcwColorGradingMerge.neutralLUTPercentage, 1.f, additionalNeutralLUTPercentage); - adjustedPcwColorGradingMerge.LUT1Percentage = lerp(adjustedPcwColorGradingMerge.LUT1Percentage, 0.f, additionalNeutralLUTPercentage); - adjustedPcwColorGradingMerge.LUT2Percentage = lerp(adjustedPcwColorGradingMerge.LUT2Percentage, 0.f, additionalNeutralLUTPercentage); - adjustedPcwColorGradingMerge.LUT3Percentage = lerp(adjustedPcwColorGradingMerge.LUT3Percentage, 0.f, additionalNeutralLUTPercentage); - adjustedPcwColorGradingMerge.LUT4Percentage = lerp(adjustedPcwColorGradingMerge.LUT4Percentage, 0.f, additionalNeutralLUTPercentage); - - float3 mixedLUT = (adjustedPcwColorGradingMerge.neutralLUTPercentage * neutralLUTColor) - + (adjustedPcwColorGradingMerge.LUT1Percentage * LUT1Color) - + (adjustedPcwColorGradingMerge.LUT2Percentage * LUT2Color) - + (adjustedPcwColorGradingMerge.LUT3Percentage * LUT3Color) - + (adjustedPcwColorGradingMerge.LUT4Percentage * LUT4Color); -#if !FIX_LUT_GAMMA_MAPPING // Convert to linear after blending between LUTs, so the blends are done in linear space - mixedLUT = gamma_linear_to_sRGB(mixedLUT); -#endif // FIX_LUT_GAMMA_MAPPING - outMixedLUT[outUVW] = float4(mixedLUT, 1.f); -} -// Dispatch size is 1 1 16 (x and y have one thread and one thread group, while z has 16 thread groups with a thread each) -[numthreads(LUT_SIZE_UINT, LUT_SIZE_UINT, 1u)] -void main(SPIRV_Cross_Input stage_input) -{ - gl_GlobalInvocationID = stage_input.gl_GlobalInvocationID; - CS(); -} \ No newline at end of file +#if LUT_ADDITIONAL_NEUTRAL_PERCENTAGE + + float adjustedNeutralLUTPercentage = lerp(PcwColorGradingMerge.neutralLUTPercentage, 1.f, additionalNeutralLUTPercentage); + float adjustedLUT1Percentage = lerp(PcwColorGradingMerge.LUT1Percentage, 0.f, additionalNeutralLUTPercentage); + float adjustedLUT2Percentage = lerp(PcwColorGradingMerge.LUT2Percentage, 0.f, additionalNeutralLUTPercentage); + float adjustedLUT3Percentage = lerp(PcwColorGradingMerge.LUT3Percentage, 0.f, additionalNeutralLUTPercentage); + float adjustedLUT4Percentage = lerp(PcwColorGradingMerge.LUT4Percentage, 0.f, additionalNeutralLUTPercentage); + + float3 mixedLUT = (adjustedNeutralLUTPercentage * neutralLUTColor) + + (adjustedLUT1Percentage * LUT1Color) + + (adjustedLUT2Percentage * LUT2Color) + + (adjustedLUT3Percentage * LUT3Color) + + (adjustedLUT4Percentage * LUT4Color); + +#else + + float3 mixedLUT = (PcwColorGradingMerge.neutralLUTPercentage * neutralLUTColor) + + (PcwColorGradingMerge.LUT1Percentage * LUT1Color) + + (PcwColorGradingMerge.LUT2Percentage * LUT2Color) + + (PcwColorGradingMerge.LUT3Percentage * LUT3Color) + + (PcwColorGradingMerge.LUT4Percentage * LUT4Color); + +#endif // LUT_ADDITIONAL_NEUTRAL_PERCENTAGE + + OutMixetLUT[outUVW] = float4(mixedLUT, 1.f); +} diff --git a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_100FF94_cs.hlsl b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_100FF94_cs.hlsl index 83bae5f1..2c78ee2b 100644 --- a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_100FF94_cs.hlsl +++ b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_100FF94_cs.hlsl @@ -1,5 +1,5 @@ -#include "../shared.h" -#include "../color.h" +#include "../shared.hlsl" +#include "../color.hlsl" cbuffer _16_18 : register(b0, space6) { diff --git a/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl b/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl index e7572563..f3380037 100644 --- a/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl +++ b/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl @@ -1,5 +1,5 @@ -#include "../shared.h" -#include "../color.h" +#include "../shared.hlsl" +#include "../color.hlsl" #define FILM_GRAIN_TEXTURE_SIZE 1024u diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 575ac608..9f383f84 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -1,6 +1,6 @@ -#include "../shared.h" -#include "../color.h" -#include "../math.h" +#include "../shared.hlsl" +#include "../color.hlsl" +#include "../math.hlsl" // These are defined at compile time (shaders permutations) //#define APPLY_BLOOM @@ -10,154 +10,28 @@ #define SDR_USE_GAMMA_2_2 0 #define FIX_WRONG_SRGB_GAMMA 1 -// Also sets "DISABLE_LUT", "DISABLE_POST_PROCESS" and "DISABLE_INVERSE_TONEMAP" -#define DISABLE_TONEMAP 0 -#define DISABLE_POST_PROCESS 0 -#define DISABLE_LUT 0 -#define DISABLE_REPLACED_TONEMAP 0 -#define DISABLE_INVERSE_POST_PROCESS 1 -#define CLAMP_INPUT_OUTPUT 1 - -struct ResolutionBlock -{ - float2 f2_0; - float2 f2_1; - float2 f2_2; - float2 f2_3; - int4 i4_0; - float2 f2_4; - float2 f2_5; - float2 f2_6; - float2 f2_7; - float2 f2_8; - int2 i2_0; - int4 i4_1; - float f_0; - int i_0; - int i_1; - int i_2; -}; - -struct CameraBlock -{ - float3 f3_0; - int i_1; - float4x4 f4x4_0; - float4x4 f4x4_1; - float4x4 f4x4_2; - float4x4 f4x4_3; - float4x4 f4x4_4; - float4x4 f4x4_5; - float3 f3_1; - int i_0; - float4x4 f4x4_6; - float4x4 f4x4_7; - float4x4 f4x4_8; - float4 f4_0; - float4 f4_1; - float4 f4_2; - float4 f4_3; - float2 f2_0; - float f_0; - float f_1; -}; -struct CameraBlockArray -{ - CameraBlock cb0; - CameraBlock cb1; - CameraBlock cb2; - ResolutionBlock rb0; - ResolutionBlock rb1; - ResolutionBlock rb2; - ResolutionBlock rb3; - ResolutionBlock rb4; -}; - -struct TonemappingParams -{ - float AcesParam0; - float AcesParam1; - float HableParam0; - float HableParam1; - float HableParam2; - float HableParam3; - float HableParam4; - int param7; //unused -}; - -struct SPerSceneConstants -{ - CameraBlockArray cba; -// WindData; -// CameraExposureData; -// GlobalLightData; -// GlobalShadowData; -// ReflectionProbeDescData; -// ReflectionProbeExposureData; -// SIndirectLightingData; -// ProbeRenderData; -// PlanetConstantsData; -// float pcs0; -// int pcs1; -// int pcs2; -// float pcs3; -// TiledBinning_idTech7FrameData; -// float pcs4; -// float pcs5; -// float pcs6; -// float pcs7; -// float pcs8; -// float pcs9; -// float pcs10; -// float pcs11; -// float pcs12; -// int pcs13; -// int pcs14; -// int pcs15; -// HairConstantData; -// FogParams; -// VolumetricLightingApplyParameters; -// PrecomputeTransmittanceParameters; -// HeightfieldData; -// MomentBasedOITSettings; -// TiledLightingDebug; -// GPUDebugGeometrySettings; -// TonemappingParams; -// EffectsAlphaThresholdParams; -}; - -struct PushConstantWrapper_HDRComposite -{ - uint HdrCmpDatIndex; //index for HDRCompositeData - uint Tmo; //tone mapping operator - float BloomMultiplier; -}; +// Also sets "ENABLE_LUT", "ENABLE_POST_PROCESS" and "ENABLE_INVERSE_TONEMAP" +#define ENABLE_TONEMAP 1 +#define ENABLE_POST_PROCESS 1 +#define ENABLE_PP_CONTRAST_ADJUSTMENT 1 +#define ENABLE_LUT 1 +#define ENABLE_REPLACED_TONEMAP 1 +#define ENABLE_INVERSE_POST_PROCESS 0 +#define CLAMP_INPUT_OUTPUT 1 -struct FrameDebug { int2 u1; int2 u2; int2 u3; int u4; int u5; float u6; int u7; int u8; int u9; int u10; int u11; int u12; int u13; }; -struct FrameData { int u1; int u2; float2 u3; float u4; float u5; float u6; float Gamma; FrameDebug u8; float4 u9; float u10; float u11; int u12; int u13; }; -struct HDRCompositeData +cbuffer CSharedFrameData : register(b0, space6) { - float4 HighlightsColorFilter; - float4 ColorFilter; - float HableSaturation; - float BrightnessMultiplier; - float ContrastIntensity; - int i_0; //unused + FrameData SharedFrameData : packoffset(c0); }; -cbuffer SharedFrameData : register(b0, space6) -{ - FrameData SharedFrameData; -} - -cbuffer PerSceneConstants : register(b0, space7) +cbuffer CPerSceneConstants : register(b0, space7) { float4 PerSceneConstants[3269] : packoffset(c0); }; -cbuffer PushConstantWrapper_HDRComposite : register(b0, space0) +cbuffer CPushConstantWrapper_HDRComposite : register(b0, space0) { PushConstantWrapper_HDRComposite PcwHdrComposite : packoffset(c0); }; @@ -214,10 +88,6 @@ struct PSOutput // exp2(x * 1.44269502162933349609375f) is the same as exp(x) -#define FLT10_MAX 64512.f -#define FLT11_MAX 65024.f -#define FLT16_MAX 65504.f - static const float LUTStrength = 1.f; static const float RestorePreLUTLuminance = 1.f; @@ -227,6 +97,18 @@ static const float ACES_c = 2.43f; static const float ACES_d = 0.59f; static const float ACES_e = 0.14f; + +static const float3x3 Bt709ToXYZ = float3x3( + 0.412135323426798, 0.357675002654190, 0.180356796374193, + 0.212507276141942, 0.715350005308380, 0.0721427185496773, + 0.0193188432856311, 0.119225000884730, 0.949879127570751); + +static const float3x3 XYZToBt709 = float3x3( + 3.24297896532120, -1.53833617585749, -0.498919840818647, + -0.968997952917093, 1.87549198225861, 0.0415445240532242, + 0.0556683243682128, -0.204117189350113, 1.05769816299604); + + float3 ACES(float3 Color, bool Clamp, float modE, float modA) { Color = (Color * (modA * Color + ACES_b)) / ((Color * (ACES_c * Color + ACES_d)) + modE); @@ -467,7 +349,8 @@ float3 Hable_Inverse( for (uint channel = 0; channel < 3; channel++) { // scaleY and offsetY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L187-L197 - if (InputColor[channel] < params_y0) // toe + // toe + if (InputColor[channel] < params_y0) { // scaleXY and offsetXY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L151-L154 itmColor[channel] = HableEval_Inverse(InputColor[channel], @@ -478,7 +361,8 @@ float3 Hable_Inverse( toeSegment_lnA, toeSegment_B); } - else if (InputColor[channel] < params_y1) // mid + // mid + else if (InputColor[channel] < params_y1) { // scaleXY and offsetY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L125-L127 itmColor[channel] = HableEval_Inverse(InputColor[channel], @@ -489,7 +373,8 @@ float3 Hable_Inverse( midSegment_lnA, 1.f); } - else // shoulder + // shoulder + else { // scaleXY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L175-L176 itmColor[channel] = HableEval_Inverse(InputColor[channel], @@ -511,7 +396,7 @@ float3 DICETonemap(float3 Color, float MaxOutputLuminance) // Between 0 and 1. Determines where the highlights curve (shoulder) starts. // Leaving at zero for now as it's a simple and good looking default. const float highlightsShoulderStart = 0.f; - + const float sourceLuminance = Luminance(Color); if (sourceLuminance > 0.0f) { @@ -540,6 +425,7 @@ float FindLuminanceToRestore(float tonemapLostLuminance, float preColorCorrectio const float currentLum = postColorCorrectionLuminance; // Should be equal to "Luminance(color)" const float targetLum = max(currentLum + tonemapLostLuminance, 0.f); float scale = luminanceCCInvChange; + if (currentLum > 0.f) { scale *= targetLum / currentLum; @@ -567,10 +453,15 @@ float3 PostProcess(float3 Color, inout float ColorLuminance) Color += lerp(float3(0.f, 0.f, 0.f), ColorLuminance * highlightsColorFilter.rgb, highlightsColorFilter.a); Color *= brightnessMultiplier; + +#if ENABLE_PP_CONTRAST_ADJUSTMENT + // Contrast adjustment (shift the colors from 0<->1 to (e.g.) -0.5<->0.5 range, multiply and shift back). // The higher the distance from the contrast middle point, the more contrast will change the color. Color = ((Color - contrastMidPoint) * contrastIntensity) + contrastMidPoint; +#endif + Color = lerp(Color, colorFilter.rgb, colorFilter.a); return Color; } @@ -602,18 +493,19 @@ float3 PostProcess_Inverse(float3 Color, float ColorLuminance) PSOutput PS(PSInput psInput) { float3 inputColor = InputColor.Load(int3(int2(psInput.SV_Position.xy), 0)); + #if CLAMP_INPUT_OUTPUT + // Remove any negative value caused by using R16G16B16A16F buffers (originally this was R11G11B10F, which has no negative values). // Doing gamut mapping, or keeping the colors outside of BT.709 doesn't seem to be right, as they seem to be just be accidentally coming out of some shader math. inputColor = max(inputColor, 0.f); + #endif #if defined(APPLY_BLOOM) float3 bloom = Bloom.Sample(Sampler0, psInput.TEXCOORD); - float bloomMultiplier = PcwHdrComposite.BloomMultiplier; - inputColor = (bloomMultiplier * bloom) + inputColor; #endif // APPLY_BLOOM @@ -621,9 +513,11 @@ PSOutput PS(PSInput psInput) float3 tonemappedColor; float3 tonemappedUnclampedColor; -#if DISABLE_TONEMAP +#if !ENABLE_TONEMAP + tonemappedColor = inputColor; tonemappedUnclampedColor = inputColor; + #else float acesParam_modE; @@ -643,47 +537,57 @@ PSOutput PS(PSInput psInput) float hable_invScale; const bool clampACES = false; - if (PcwHdrComposite.Tmo == 1u) - { - tonemappedUnclampedColor = ACESReference(inputColor, clampACES); - tonemappedColor = saturate(tonemappedUnclampedColor); - } - else if (PcwHdrComposite.Tmo == 2u) - { - tonemappedUnclampedColor = ACESParametric(inputColor, clampACES, acesParam_modE, acesParam_modA); - tonemappedColor = saturate(tonemappedUnclampedColor); - } - else if (PcwHdrComposite.Tmo == 3u) - { - tonemappedUnclampedColor = Hable(inputColor, - hable_params_y0, - hable_params_y1, - hable_dstParams_W, - hable_toeSegment_lnA, - hable_toeSegment_B, - hable_midSegment_offsetX, - hable_midSegment_lnA, - hable_shoulderSegment_offsetX, - hable_shoulderSegment_offsetY, - hable_shoulderSegment_lnA, - hable_shoulderSegment_B, - hable_invScale); - tonemappedColor = tonemappedUnclampedColor; // Hable was never clamped - } - else + + switch (PcwHdrComposite.Tmo) { - tonemappedUnclampedColor = inputColor; - tonemappedColor = saturate(tonemappedUnclampedColor); + case 1: + { + tonemappedUnclampedColor = ACESReference(inputColor, clampACES); + tonemappedColor = saturate(tonemappedUnclampedColor); + } break; + + case 2: + { + tonemappedUnclampedColor = ACESParametric(inputColor, clampACES, acesParam_modE, acesParam_modA); + tonemappedColor = saturate(tonemappedUnclampedColor); + } break; + + case 3: + { + tonemappedUnclampedColor = Hable(inputColor, + hable_params_y0, + hable_params_y1, + hable_dstParams_W, + hable_toeSegment_lnA, + hable_toeSegment_B, + hable_midSegment_offsetX, + hable_midSegment_lnA, + hable_shoulderSegment_offsetX, + hable_shoulderSegment_offsetY, + hable_shoulderSegment_lnA, + hable_shoulderSegment_B, + hable_invScale); + tonemappedColor = tonemappedUnclampedColor; // Hable was never clamped + } break; + + default: + { + tonemappedUnclampedColor = inputColor; + tonemappedColor = saturate(tonemappedUnclampedColor); + } break; } -#if !DISABLE_POST_PROCESS +#if ENABLE_POST_PROCESS + float prePostProcessColorLuminance; float prePostProcessUnclampedColorLuminance; tonemappedColor = PostProcess(tonemappedColor, prePostProcessColorLuminance); // Repeat on unclamped tonemapped color to later retrieve the lost luminance tonemappedUnclampedColor = PostProcess(tonemappedUnclampedColor, prePostProcessUnclampedColorLuminance); -#endif -#endif // DISABLE_TONEMAP + +#endif //ENABLE_POST_PROCESS + +#endif // ENABLE_TONEMAP tonemappedColor = saturate(tonemappedColor); @@ -694,101 +598,137 @@ PSOutput PS(PSInput psInput) float3 color = tonemappedColor; -#if defined(APPLY_MERGED_COLOR_GRADING_LUT) && !DISABLE_TONEMAP && !DISABLE_LUT +#if defined(APPLY_MERGED_COLOR_GRADING_LUT) && ENABLE_TONEMAP && ENABLE_LUT + +#if FIX_WRONG_SRGB_GAMMA || !LUT_FIX_GAMMA_MAPPING || !LUT_BLEND_LUTS_IN_LINEAR + + color = gamma_linear_to_sRGB(color); + +#elif !LUT_FIX_GAMMA_MAPPING || !LUT_BLEND_LUTS_IN_LINEAR -#if FIX_WRONG_SRGB_GAMMA - float3 LUTEncodedColor = gamma_linear_to_sRGB(color); -#else //read gamma ini value, defaulting at 2.4 (makes little sense) float inverseGamma = 1.f / (max(SharedFrameData.Gamma, 0.001f)); //weird linear -> sRGB conversion that clips values just above 0, and raises blacks. - float3 LUTEncodedColor = (pow(color, inverseGamma) * 1.055f) - 0.055f; - LUTEncodedColor = max(LUTEncodedColor, 0.f); // Unnecessary as the LUT sampling is already clamped. -#endif // FIX_WRONG_SRGB_GAMMA - - float3 LUTColor = Lut.Sample(Sampler0, LUTEncodedColor * (1.f - (1.f / LUT_SIZE)) + ((1.f / LUT_SIZE) / 2.f)); -#if !FIX_LUT_GAMMA_MAPPING - LUTColor = gamma_sRGB_to_linear(LUTColor); -#endif // FIX_LUT_GAMMA_MAPPING + color = (pow(color, inverseGamma) * 1.055f) - 0.055f; + color = max(color, 0.f); // Unnecessary as the LUT sampling is already clamped. + +#endif // FIX_WRONG_SRGB_GAMMA || !LUT_FIX_GAMMA_MAPPING + + float3 LUTColor = Lut.Sample(Sampler0, color * (1.f - (1.f / LUT_SIZE)) + ((1.f / LUT_SIZE) / 2.f)); + +#if LUT_CUSTOM_STRENGTH + float LUTMaskAlpha = saturate(LutMask.Sample(Sampler0, psInput.TEXCOORD).x + (1.f - LUTStrength)); +#else + + float LUTMaskAlpha = LutMask.Sample(Sampler0, psInput.TEXCOORD).x; + +#endif // LUT_CUSTOM_STRENGTH + color = lerp(LUTColor, color, LUTMaskAlpha); - + +#if !LUT_FIX_GAMMA_MAPPING || !LUT_BLEND_LUTS_IN_LINEAR + + color = pow(color, 2.2f); + +#endif // !LUT_FIX_GAMMA_MAPPING + #endif // APPLY_MERGED_COLOR_GRADING_LUT const float postColorCorrectionLuminance = Luminance(color); #if ENABLE_HDR + // Restore any luminance beyond 1 that ended up clipped by HDR->SDR tonemappers and any subsequent image manipulation color *= FindLuminanceToRestore(tonemapLostLuminance, preColorCorrectionLuminance, postColorCorrectionLuminance); -#if !DISABLE_TONEMAP && !DISABLE_REPLACED_TONEMAP +#if ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP + +#if ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS -#if !DISABLE_POST_PROCESS && !DISABLE_INVERSE_POST_PROCESS //TODO: passing in the previous luminance might not be the best, though is there any other way really? //Should we at least shift it by how much the LUT shifted the luminance? To invert it more correctly. color = PostProcess_Inverse(color, prePostProcessColorLuminance); -#endif // !DISABLE_POST_PROCESS && !DISABLE_INVERSE_POST_PROCESS + +#endif // ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS // Restore a color very close to the original linear one, but with all the other post process and LUT transformations baked in - if (PcwHdrComposite.Tmo == 1u) - { - color = ACESReference_Inverse(color); - } - else if (PcwHdrComposite.Tmo == 2u) + switch(PcwHdrComposite.Tmo) { - color = ACESParametric_Inverse(color, acesParam_modE, acesParam_modA); - } - else if (PcwHdrComposite.Tmo == 3u) - { - color = Hable_Inverse(color, - hable_params_y0, - hable_params_y1, - hable_dstParams_W, - hable_toeSegment_lnA, - hable_toeSegment_B, - hable_midSegment_offsetX, - hable_midSegment_lnA, - hable_shoulderSegment_offsetX, - hable_shoulderSegment_offsetY, - hable_shoulderSegment_lnA, - hable_shoulderSegment_B, - hable_invScale); - } - else - { - // Any luminance lost by this tonemap case would have already been restored above with "tonemapLostLuminance". + case 1: + { + color = ACESReference_Inverse(color); + } break; + + case 2: + { + color = ACESParametric_Inverse(color, acesParam_modE, acesParam_modA); + } break; + + case 3: + { + color = Hable_Inverse(color, + hable_params_y0, + hable_params_y1, + hable_dstParams_W, + hable_toeSegment_lnA, + hable_toeSegment_B, + hable_midSegment_offsetX, + hable_midSegment_lnA, + hable_shoulderSegment_offsetX, + hable_shoulderSegment_offsetY, + hable_shoulderSegment_lnA, + hable_shoulderSegment_B, + hable_invScale); + } break; + + default: + break; + // Any luminance lost by this tonemap case would have already been restored above with "tonemapLostLuminance". } -#if !DISABLE_POST_PROCESS && !DISABLE_INVERSE_POST_PROCESS +#if ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS + //TODO: ... this has no use now, and also, it won't work in the HDR range color = PostProcess(color, prePostProcessColorLuminance); -#endif // !DISABLE_POST_PROCESS && !DISABLE_INVERSE_POST_PROCESS - + +#endif // ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS + color *= HDR_GAME_PAPER_WHITE; - + const float maxOutputLuminance = HDR_MAX_OUTPUT_NITS / WhiteNits_BT709; //TODO: find a tonemapper that looks more like Hable? color = DICETonemap(color, maxOutputLuminance); + #else - + color *= HDR_GAME_PAPER_WHITE; - -#endif // !DISABLE_TONEMAP && !DISABLE_REPLACED_TONEMAP + +#endif // ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP #if CLAMP_INPUT_OUTPUT + color = clamp(color, 0.f, FLT16_MAX); // Avoid extremely high numbers turning into NaN in FP16 + #endif #else #if SDR_USE_GAMMA_2_2 //TODO: This makes sense to have if we fix up the LUTs, though the SDR tonemapper had still been developed with sRGB<->2.2 mismatch + color = pow(color, 1.f / 2.2f); + #else + // Do sRGB gamma even if we'd be playing on gamma 2.2 screens, as the game was already calibrated for 2.2 gamma color = gamma_linear_to_sRGB(color); + #if CLAMP_INPUT_OUTPUT - color = clamp(color, 0.f, 1.f); -#endif + + color = saturate(color); + +#endif // CLAMP_INPUT_OUTPUT + #endif // SDR_USE_GAMMA_2_2 #endif // ENABLE_HDR diff --git a/shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl b/shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl index d0b75f8c..975a3eac 100644 --- a/shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl +++ b/shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl @@ -1,5 +1,5 @@ -#include "../shared.h" -#include "../color.h" +#include "../shared.hlsl" +#include "../color.hlsl" cbuffer _13_15 : register(b0, space7) { diff --git a/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl b/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl index d469f284..2c9a1c95 100644 --- a/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl +++ b/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl @@ -1,5 +1,5 @@ -#include "../shared.h" -#include "../color.h" +#include "../shared.hlsl" +#include "../color.hlsl" // Hack: change the alpha value at which the UI blends in in HDR, to increase readability. Range is 0 to 1, with 1 having no effect. #define HDR_UI_BLEND_POW 0.775f diff --git a/shaders/color.h b/shaders/color.hlsl similarity index 98% rename from shaders/color.h rename to shaders/color.hlsl index 1a50f5dd..5e7a4f00 100644 --- a/shaders/color.h +++ b/shaders/color.hlsl @@ -1,5 +1,4 @@ -#ifndef COLOR_H -#define COLOR_H +#pragma once // Rec.709 SDR white is meant to be mapped to 80 nits (not 100, even if some game engine (UE) and consoles (PS5) interpret it as such). static const float WhiteNits_BT709 = 80.f; @@ -77,5 +76,3 @@ float Luminance(float3 color) // Fixed from "wrong" values: 0.2125 0.7154 0.0721f return dot(color, float3(0.2126f, 0.7152f, 0.0722f)); } - -#endif \ No newline at end of file diff --git a/shaders/compile_all_shaders.bat b/shaders/compile_all_shaders.bat index 6e0aaeb4..064bbbbb 100644 --- a/shaders/compile_all_shaders.bat +++ b/shaders/compile_all_shaders.bat @@ -1,5 +1,7 @@ @echo off +cd /d "%~dp0" + set dxc=..\tools\dxc_2023_08_14\bin\x64\dxc.exe for /f "delims=" %%x in (target_folder.txt) do set target_folder=%%x @@ -22,40 +24,69 @@ if not exist "%target_folder%" ( -T ps_6_6 -E PS -D APPLY_MERGED_COLOR_GRADING_LUT -Fo ^ "%target_folder%HDRComposite\HDRComposite_E00FF1A_ps.bin" +call :errorlevel_check + "%dxc%" "HDRComposite\HDRComposite_ps.hlsl" ^ -T ps_6_6 -E PS -Fo ^ "%target_folder%HDRComposite\HDRComposite_600FF1A_ps.bin" +call :errorlevel_check + "%dxc%" "HDRComposite\HDRComposite_ps.hlsl" ^ -T ps_6_6 -E PS -D APPLY_BLOOM -Fo ^ "%target_folder%HDRComposite\HDRComposite_700FF1A_ps.bin" +call :errorlevel_check + "%dxc%" "HDRComposite\HDRComposite_ps.hlsl" ^ -T ps_6_6 -E PS -D APPLY_BLOOM -D APPLY_MERGED_COLOR_GRADING_LUT -Fo ^ "%target_folder%HDRComposite\HDRComposite_F00FF1A_ps.bin" +call :errorlevel_check + "%dxc%" "ScaleformComposite\ScaleformComposite_FFAA_ps.hlsl" ^ -T ps_6_6 -E PS -Fo ^ "%target_folder%ScaleformComposite\ScaleformComposite_FFAA_ps.bin" +call :errorlevel_check + "%dxc%" "ColorGradingMerge\ColorGradingMerge_FF81_cs.hlsl" ^ - -T cs_6_6 -E main -Fo ^ + -T cs_6_6 -E CS -Fo ^ "%target_folder%ColorGradingMerge\ColorGradingMerge_FF81_cs.bin" +call :errorlevel_check + "%dxc%" "BinkMovie\BinkMovie_FFAB_ps.hlsl" ^ -T ps_6_6 -E PS -Fo ^ "%target_folder%BinkMovie\BinkMovie_FFAB_ps.bin" +call :errorlevel_check + "%dxc%" "FilmGrain\FilmGrain_FF75_ps.hlsl" ^ -T ps_6_6 -E main -Fo ^ "%target_folder%FilmGrain\FilmGrain_FF75_ps.bin" +call :errorlevel_check + "%dxc%" "PostSharpen\PostSharpen_FF9A_ps.hlsl" ^ -T ps_6_6 -E main -Fo ^ "%target_folder%PostSharpen\PostSharpen_FF9A_ps.bin" +call :errorlevel_check + "%dxc%" "ContrastAdaptiveSharpening\ContrastAdaptiveSharpening_100FF94_cs.hlsl" ^ -T cs_6_6 -E main -Fo ^ "%target_folder%ContrastAdaptiveSharpening\ContrastAdaptiveSharpening_100FF94_cs.bin" -pause \ No newline at end of file +call :errorlevel_check + +pause + +:errorlevel_check + if %ERRORLEVEL% NEQ 0 ( + echo. + echo compilation failed. Press any key to continue... + pause > NUL + echo. + ) +goto :eof diff --git a/shaders/math.h b/shaders/math.hlsl similarity index 98% rename from shaders/math.h rename to shaders/math.hlsl index f0403e62..778f17ce 100644 --- a/shaders/math.h +++ b/shaders/math.hlsl @@ -1,5 +1,4 @@ -#ifndef MATH_H -#define MATH_H +#pragma once #define FLT_MAX 3.402823466e+38F #define FLT10_MAX 64512.f @@ -53,5 +52,3 @@ float luminanceCompress(float fInValue, float fOutMaxValue, float fShoulderStart float fPossibleOutValue = fShoulderStart + fCompressedRange * rangeCompressPow(fCompressableValue / fCompressedRange, fCompressableRange / fCompressedRange, fModulationPow); return fInValue <= fShoulderStart ? fInValue : fPossibleOutValue; } - -#endif \ No newline at end of file diff --git a/shaders/shared.h b/shaders/shared.hlsl similarity index 68% rename from shaders/shared.h rename to shaders/shared.hlsl index d75b5c2b..30ba0b7d 100644 --- a/shaders/shared.h +++ b/shaders/shared.hlsl @@ -1,15 +1,19 @@ -#ifndef SHARED_H -#define SHARED_H +#pragma once + +#include "structs.hlsl" // Enables HDR scRGB output. If false, we output Rec.709 in gamma space (~sRGB). #define ENABLE_HDR 1 // Set equal to the max nits your display can output #define HDR_MAX_OUTPUT_NITS 1000.f // Brings the range roughly from 80 nits to 200 nits (close to the target reference of 203 nits) -#define HDR_GAME_PAPER_WHITE 2.5f +#define HDR_GAME_PAPER_WHITE 1.f #define HDR_UI_PAPER_WHITE 2.5f + + // This possibly shifts colors a lot, but it's mathematically correct, it's also necessary for HDR LUTs to work. -#define FIX_LUT_GAMMA_MAPPING 1 +#define LUT_FIX_GAMMA_MAPPING 1 +#define LUT_BLEND_LUTS_IN_LINEAR 0 +#define LUT_CUSTOM_STRENGTH 1 +#define LUT_ADDITIONAL_NEUTRAL_PERCENTAGE 1 #define LUT_SIZE 16.f - -#endif \ No newline at end of file diff --git a/shaders/structs.hlsl b/shaders/structs.hlsl new file mode 100644 index 00000000..5dd5dd15 --- /dev/null +++ b/shaders/structs.hlsl @@ -0,0 +1,143 @@ +#pragma once + +//HDRComposite start +struct ResolutionBlock +{ + float2 f2_0; + float2 f2_1; + float2 f2_2; + float2 f2_3; + int4 i4_0; + float2 f2_4; + float2 f2_5; + float2 f2_6; + float2 f2_7; + float2 f2_8; + int2 i2_0; + int4 i4_1; + float f_0; + int i_0; + int i_1; + int i_2; +}; + +struct CameraBlock +{ + float3 f3_0; + int i_1; + float4x4 f4x4_0; + float4x4 f4x4_1; + float4x4 f4x4_2; + float4x4 f4x4_3; + float4x4 f4x4_4; + float4x4 f4x4_5; + float3 f3_1; + int i_0; + float4x4 f4x4_6; + float4x4 f4x4_7; + float4x4 f4x4_8; + float4 f4_0; + float4 f4_1; + float4 f4_2; + float4 f4_3; + float2 f2_0; + float f_0; + float f_1; +}; + +struct CameraBlockArray +{ + CameraBlock cb0; + CameraBlock cb1; + CameraBlock cb2; + ResolutionBlock rb0; + ResolutionBlock rb1; + ResolutionBlock rb2; + ResolutionBlock rb3; + ResolutionBlock rb4; +}; + +struct SPerSceneConstants +{ + CameraBlockArray cba; +// WindData; +// CameraExposureData; +// GlobalLightData; +// GlobalShadowData; +// ReflectionProbeDescData; +// ReflectionProbeExposureData; +// SIndirectLightingData; +// ProbeRenderData; +// PlanetConstantsData; +// float pcs0; +// int pcs1; +// int pcs2; +// float pcs3; +// TiledBinning_idTech7FrameData; +// float pcs4; +// float pcs5; +// float pcs6; +// float pcs7; +// float pcs8; +// float pcs9; +// float pcs10; +// float pcs11; +// float pcs12; +// int pcs13; +// int pcs14; +// int pcs15; +// HairConstantData; +// FogParams; +// VolumetricLightingApplyParameters; +// PrecomputeTransmittanceParameters; +// HeightfieldData; +// MomentBasedOITSettings; +// TiledLightingDebug; +// GPUDebugGeometrySettings; +// TonemappingParams; +// EffectsAlphaThresholdParams; +}; + +struct TonemappingParams +{ + float AcesParam0; + float AcesParam1; + float HableParam0; + float HableParam1; + float HableParam2; + float HableParam3; + float HableParam4; + int param7; //unused +}; + +struct PushConstantWrapper_HDRComposite +{ + uint HdrCmpDatIndex; //index for HDRCompositeData + uint Tmo; //tone mapping operator + float BloomMultiplier; +}; + +struct FrameDebug { int2 u1; int2 u2; int2 u3; int u4; int u5; float u6; int u7; int u8; int u9; int u10; int u11; int u12; int u13; }; +struct FrameData { int u1; int u2; float2 u3; float u4; float u5; float u6; float Gamma; FrameDebug u8; float4 u9; float u10; float u11; int u12; int u13; }; + +struct HDRCompositeData +{ + float4 HighlightsColorFilter; + float4 ColorFilter; + float HableSaturation; + float BrightnessMultiplier; + float ContrastIntensity; + int i_0; //unused +}; +//HDRComposite end + +//ColorGradingMerge start +struct PushConstantWrapper_ColorGradingMerge +{ + float LUT1Percentage; + float LUT2Percentage; + float LUT3Percentage; + float LUT4Percentage; + float neutralLUTPercentage; +}; +//ColorGradingMerge end From 8bf602c5ad597be994b9e1cce0b78956ce01c9fa Mon Sep 17 00:00:00 2001 From: Lilium Date: Mon, 25 Sep 2023 21:57:41 +0200 Subject: [PATCH 022/270] fixup the cleanup --- .../ColorGradingMerge_FF81_cs.hlsl | 16 ++++++------ shaders/HDRComposite/HDRComposite_ps.hlsl | 25 ++++++++++--------- shaders/shared.hlsl | 5 +++- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 2c51b6c3..d84cbbec 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -2,11 +2,6 @@ #include "../color.hlsl" #include "../math.hlsl" -#define LUT_SIZE_UINT (uint)LUT_SIZE - -// 0 None, 1 ShortFuse technique (normalization), 2 luminance preservation (doesn't look so good) -#define LUT_IMPROVEMENT_TYPE 1 - // Make some small quality cuts for the purpose of optimization #define OPTIMIZE_LUT_ANALYSIS true // For future development @@ -225,21 +220,17 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) float3 neutralLUTColor = float3(outUVW) / (LUT_SIZE - 1.f); // The neutral LUT is automatically generated by the coordinates, but it's baked with sRGB gamma -#if LUT_FIX_GAMMA_MAPPING || LUT_BLEND_LUTS_IN_LINEAR neutralLUTColor = gamma_sRGB_to_linear(neutralLUTColor); -#endif #if LUT_IMPROVEMENT_TYPE == 0 || LUT_IMPROVEMENT_TYPE == 2 float3 LUT1Color = LUT1.Load(inUVW); float3 LUT2Color = LUT2.Load(inUVW); float3 LUT3Color = LUT3.Load(inUVW); float3 LUT4Color = LUT4.Load(inUVW); -#if LUT_FIX_GAMMA_MAPPING || LUT_BLEND_LUTS_IN_LINEAR LUT1Color = gamma_sRGB_to_linear(LUT1Color); LUT2Color = gamma_sRGB_to_linear(LUT2Color); LUT3Color = gamma_sRGB_to_linear(LUT3Color); LUT4Color = gamma_sRGB_to_linear(LUT4Color); -#endif // LUT_FIX_GAMMA_MAPPING #endif // LUT_IMPROVEMENT_TYPE #if LUT_IMPROVEMENT_TYPE == 1 @@ -297,5 +288,12 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) #endif // LUT_ADDITIONAL_NEUTRAL_PERCENTAGE +// Convert to sRGB after blending between LUTs, so the blends are done in sRGB space +#if !LUT_FIX_GAMMA_MAPPING + + mixedLUT = gamma_linear_to_sRGB(mixedLUT); + +#endif // !LUT_FIX_GAMMA_MAPPING + OutMixetLUT[outUVW] = float4(mixedLUT, 1.f); } diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 9f383f84..8bd0c581 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -409,7 +409,7 @@ float3 DICETonemap(float3 Color, float MaxOutputLuminance) float FindLuminanceToRestore(float tonemapLostLuminance, float preColorCorrectionLuminance, float postColorCorrectionLuminance) { // Try to restore any luminance above "color", as it would have been lost during tone mapping. - // + // // To achieve this, we re-apply the lost luminance, but only by the inverse amount the luminance changed during the color correction. // e.g. if the color correction fully changed the luminance, then we don't change it any further, // but if the color correction changed the luminance only by 25%, then we re-apply apply 75% of the lost luminance on top. @@ -600,34 +600,35 @@ PSOutput PS(PSInput psInput) #if defined(APPLY_MERGED_COLOR_GRADING_LUT) && ENABLE_TONEMAP && ENABLE_LUT -#if FIX_WRONG_SRGB_GAMMA || !LUT_FIX_GAMMA_MAPPING || !LUT_BLEND_LUTS_IN_LINEAR +#if FIX_WRONG_SRGB_GAMMA - color = gamma_linear_to_sRGB(color); + float3 colorForLutSampling = gamma_linear_to_sRGB(color); -#elif !LUT_FIX_GAMMA_MAPPING || !LUT_BLEND_LUTS_IN_LINEAR +#else //read gamma ini value, defaulting at 2.4 (makes little sense) float inverseGamma = 1.f / (max(SharedFrameData.Gamma, 0.001f)); //weird linear -> sRGB conversion that clips values just above 0, and raises blacks. - color = (pow(color, inverseGamma) * 1.055f) - 0.055f; - color = max(color, 0.f); // Unnecessary as the LUT sampling is already clamped. + float3 colorForLutSampling = (pow(color, inverseGamma) * 1.055f) - 0.055f; + colorForLutSampling = max(colorForLutSampling, 0.f); // Unnecessary as the LUT sampling is already clamped. -#endif // FIX_WRONG_SRGB_GAMMA || !LUT_FIX_GAMMA_MAPPING +#endif // FIX_WRONG_SRGB_GAMMA - float3 LUTColor = Lut.Sample(Sampler0, color * (1.f - (1.f / LUT_SIZE)) + ((1.f / LUT_SIZE) / 2.f)); + float3 LutColor = Lut.Sample(Sampler0, colorForLutSampling * (1.f - (1.f / LUT_SIZE)) + ((1.f / LUT_SIZE) / 2.f)); #if LUT_CUSTOM_STRENGTH - float LUTMaskAlpha = saturate(LutMask.Sample(Sampler0, psInput.TEXCOORD).x + (1.f - LUTStrength)); + float LutMaskAlpha = saturate(LutMask.Sample(Sampler0, psInput.TEXCOORD).x + (1.f - LUTStrength)); + #else - float LUTMaskAlpha = LutMask.Sample(Sampler0, psInput.TEXCOORD).x; + float LutMaskAlpha = LutMask.Sample(Sampler0, psInput.TEXCOORD).x; #endif // LUT_CUSTOM_STRENGTH - color = lerp(LUTColor, color, LUTMaskAlpha); + color = lerp(LutColor, color, LutMaskAlpha); -#if !LUT_FIX_GAMMA_MAPPING || !LUT_BLEND_LUTS_IN_LINEAR +#if !LUT_FIX_GAMMA_MAPPING color = pow(color, 2.2f); diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 30ba0b7d..04192193 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -13,7 +13,10 @@ // This possibly shifts colors a lot, but it's mathematically correct, it's also necessary for HDR LUTs to work. #define LUT_FIX_GAMMA_MAPPING 1 -#define LUT_BLEND_LUTS_IN_LINEAR 0 #define LUT_CUSTOM_STRENGTH 1 #define LUT_ADDITIONAL_NEUTRAL_PERCENTAGE 1 #define LUT_SIZE 16.f +#define LUT_SIZE_UINT (uint)LUT_SIZE + +// 0 None, 1 ShortFuse technique (normalization), 2 luminance preservation (doesn't look so good) +#define LUT_IMPROVEMENT_TYPE 1 From aaf892ee3102d904f1c0a38b6b907ab29c210fdd Mon Sep 17 00:00:00 2001 From: Lilium Date: Mon, 25 Sep 2023 22:43:17 +0200 Subject: [PATCH 023/270] add tetrahedral interpolation as new default --- shaders/HDRComposite/HDRComposite_ps.hlsl | 133 +++++++++++++++++++++- 1 file changed, 131 insertions(+), 2 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 8bd0c581..8a37e124 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -20,6 +20,7 @@ #define ENABLE_INVERSE_POST_PROCESS 0 #define CLAMP_INPUT_OUTPUT 1 +#define LUT_USE_TETRAHEDRAL_INTERPOLATION 1 cbuffer CSharedFrameData : register(b0, space6) { @@ -490,6 +491,126 @@ float3 PostProcess_Inverse(float3 Color, float ColorLuminance) return Color; } + +float3 TetrahedralInterpolation( + Texture3D Lut, + float3 Color) +{ + float3 coords = Color.rgb * (LUT_SIZE - 1); + + // baseInd is on [0,LUT_SIZE-1] + int3 baseInd = coords; + int3 nextInd = baseInd + 1; + + // fract is on [0,1] + float3 fract = frac(coords); + + float3 f1, f4; + + float3 v1 = Lut.Load(int4(baseInd, 0)).rgb; + float3 v4 = Lut.Load(int4(nextInd, 0)).rgb; + + if (fract.r >= fract.g) + { + if (fract.g >= fract.b) // R > G > B + { + nextInd = baseInd + int3(1, 0, 0); + float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; + + nextInd = baseInd + int3(1, 1, 0); + float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; + + f1 = 1.f - fract.r; + f4 = fract.b; + float3 f2 = fract.r - fract.g; + float3 f3 = fract.g - fract.b; + + Color = (f2 * v2) + (f3 * v3); + } + else if (fract.r >= fract.b) // R > B > G + { + nextInd = baseInd + int3(1, 0, 0); + float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; + + nextInd = baseInd + int3(1, 0, 1); + float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; + + f1 = 1.f - fract.r; + f4 = fract.g; + float3 f2 = fract.r - fract.b; + float3 f3 = fract.b - fract.g; + + Color.rgb = (f2 * v2) + (f3 * v3); + } + else // B > R > G + { + nextInd = baseInd + int3(0, 0, 1); + float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; + + nextInd = baseInd + int3(1, 0, 1); + float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; + + f1 = 1.f - fract.b; + f4 = fract.g; + float3 f2 = fract.b - fract.r; + float3 f3 = fract.r - fract.g; + + Color = (f2 * v2) + (f3 * v3); + } + } + else + { + if (fract.g <= fract.b) // B > G > R + { + nextInd = baseInd + int3(0, 0, 1); + float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; + + nextInd = baseInd + int3(0, 1, 1); + float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; + + f1 = 1.f - fract.b; + f4 = fract.r; + float3 f2 = fract.b - fract.g; + float3 f3 = fract.g - fract.r; + + Color = (f2 * v2) + (f3 * v3); + } + else if (fract.r >= fract.b) // G > R > B + { + nextInd = baseInd + int3(0, 1, 0); + float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; + + nextInd = baseInd + int3(1, 1, 0); + float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; + + f1 = 1.f - fract.g; + f4 = fract.b; + float3 f2 = fract.g - fract.r; + float3 f3 = fract.r - fract.b; + + Color = (f2 * v2) + (f3 * v3); + } + else // G > B > R + { + nextInd = baseInd + int3(0, 1, 0); + float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; + + nextInd = baseInd + int3(0, 1, 1); + float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; + + f1 = 1.f - fract.g; + f4 = fract.r; + float3 f2 = fract.g - fract.b; + float3 f3 = fract.b - fract.r; + + Color = (f2 * v2) + (f3 * v3); + } + } + + return Color + (f1 * v1) + (f4 * v4); +} + + PSOutput PS(PSInput psInput) { float3 inputColor = InputColor.Load(int3(int2(psInput.SV_Position.xy), 0)); @@ -614,8 +735,16 @@ PSOutput PS(PSInput psInput) #endif // FIX_WRONG_SRGB_GAMMA +#if LUT_USE_TETRAHEDRAL_INTERPOLATION + + float3 LutColor = TetrahedralInterpolation(Lut, colorForLutSampling); + +#else + float3 LutColor = Lut.Sample(Sampler0, colorForLutSampling * (1.f - (1.f / LUT_SIZE)) + ((1.f / LUT_SIZE) / 2.f)); +#endif //LUT_USE_TETRAHEDRAL_INTERPOLATION + #if LUT_CUSTOM_STRENGTH float LutMaskAlpha = saturate(LutMask.Sample(Sampler0, psInput.TEXCOORD).x + (1.f - LUTStrength)); @@ -697,9 +826,9 @@ PSOutput PS(PSInput psInput) color *= HDR_GAME_PAPER_WHITE; - const float maxOutputLuminance = HDR_MAX_OUTPUT_NITS / WhiteNits_BT709; + //const float maxOutputLuminance = HDR_MAX_OUTPUT_NITS / WhiteNits_BT709; //TODO: find a tonemapper that looks more like Hable? - color = DICETonemap(color, maxOutputLuminance); + //color = DICETonemap(color, maxOutputLuminance); #else From e4dfba6c70240ecd2e76dd227021b04695228fa3 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Mon, 25 Sep 2023 18:38:41 -0400 Subject: [PATCH 024/270] fix(luts): unholey blues. better saturation --- .../ColorGradingMerge_FF81_cs.hlsl | 50 ++++++------------- 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index d84cbbec..9db491d7 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -23,17 +23,17 @@ RWTexture3D OutMixetLUT : register(u0, space8); struct LUTAnalysis { - float minChannel; - float maxChannel; float3 black; float3 white; + float whiteY; #if UNUSED_PARAMS + float minChannel; + float maxChannel; float minY; float maxY; float averageY; float range; float blackY; - float whiteY; float medianY; float averageChannel; float averageRed; @@ -62,12 +62,11 @@ void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) { Analysis.black = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(0)).rgb); Analysis.white = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(LUT_SIZE_UINT - 1u)).rgb); + Analysis.whiteY = Luminance(Analysis.white); #if UNUSED_PARAMS Analysis.minY = FLT_MAX; Analysis.maxY = -FLT_MAX; Analysis.blackY = Luminance(Analysis.black); - Analysis.whiteY = Luminance(Analysis.white); -#endif // UNUSED_PARAMS float3 colors = 0.f; float Ys = 0.f; @@ -96,14 +95,12 @@ void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) minColor = min(minColor, LUTColor); maxColor = max(maxColor, LUTColor); -#if UNUSED_PARAMS float Y = Luminance(LUTColor); Analysis.minY = min(Analysis.minY, Y); Analysis.maxY = max(Analysis.maxY, Y); Ys += Y; colors += LUTColor.r; -#endif // UNUSED_PARAMS } } } @@ -111,8 +108,6 @@ void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) //TODO: either store min/max channels merged or separately, but not both Analysis.minChannel = min(minColor.r, min(minColor.g, minColor.b)); Analysis.maxChannel = max(maxColor.r, max(maxColor.g, maxColor.b)); - -#if UNUSED_PARAMS Analysis.minRed = minRed; Analysis.minGreen = minGreen; Analysis.minBlue = minBlue; @@ -141,34 +136,21 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b LUTAnalysis analysis; AnalyzeLUT(LUT, analysis); - // Black will be scaled by min channel - float3 scaledBlack = 1.f - ((1.f - analysis.black) * (1.f / (1.f - analysis.minChannel))); - // White will be scaled up by max channel - float3 scaledWhite = analysis.white / analysis.maxChannel; - float scaledBlackY = Luminance(scaledBlack); - float scaledWhiteY = Luminance(scaledWhite); - -#if 0 //TODO: delete once verified this works - float scaleDown = (0.f - analysis.minChannel); - float scaleUp = (1.f - analysis.maxChannel); - if (saturate(scaleDown) != scaleDown - || saturate(scaleUp) != scaleUp - || saturate(scaledBlackY) != scaledBlackY - || saturate(scaledWhiteY) != scaledWhiteY) - { - return 0; - } -#endif - float3 color = gamma_sRGB_to_linear(LUT.Load(UVW).rgb); const float3 originalColor = color; //TODO: convert to using lerp function??? - const float3 reducedColor = linearNormalization(neutralLUTColor, 0.f, 1.f, 1.f / (1.f - analysis.minChannel), 1.f); - const float3 increasedColor = linearNormalization(neutralLUTColor, 0.f, 1.f, 1.f, 1.f / analysis.maxChannel); - // Scale the color ("neutralLUTColor" here represents the coordinates of the point). + // Reduce all colors by raised level of black texel + // Normalize as relative to their distance from black ("neutralLUTColor" is xyz coordinates) + const float3 reducedColor = linearNormalization(neutralLUTColor, 0.f, 1.f, 1.f / (1.f - analysis.black), 1.f); + + // Reapply black level as a multiple (applies shadow tint) + const float3 increasedColor = linearNormalization(neutralLUTColor, 0.f, 1.f, 1.f + analysis.black, 1.f); + + // Apply to base color color = (1.f - ((1.f - color) * reducedColor)) * increasedColor; + const float blackDistance = hypot3(neutralLUTColor); const float whiteDistance = hypot3(1.f - neutralLUTColor); const float totalRange = (blackDistance + whiteDistance); @@ -176,9 +158,8 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b const float sourceY = Luminance(color); if (sourceY > 0.f) // Black will always stay black (and should) { - const float decreasedY = linearNormalization(blackDistance, 0.f, totalRange, 1.f / (1.f - scaledBlackY), 1.f); - const float increasedY = linearNormalization(whiteDistance, 0.f, totalRange, 1.f / scaledWhiteY, 1.f); - const float targetY = (1.f - ((1.f - sourceY) * decreasedY)) * increasedY; + const float increasedY = linearNormalization(whiteDistance, 0.f, totalRange, 1.f / analysis.whiteY, 1.f); + const float targetY = (1.f - (1.f - sourceY)) * increasedY; if (targetY >= 0.9999f && targetY <= 1.0005f) { @@ -196,6 +177,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b } else { + // TODO: Consider removal since no longer scaling color = 0; // Color may have gone below 0 when scaling } From a3eff1f8650435d31d4dc46a8f4b07fad28629c2 Mon Sep 17 00:00:00 2001 From: ersh1 <16157893+ersh1@users.noreply.github.com> Date: Wed, 27 Sep 2023 02:39:21 +0200 Subject: [PATCH 025/270] updated the UI offsets to 1.7.33 --- Plugin/CMakeLists.txt | 2 +- Plugin/src/Hooks.h | 8 ++++---- Plugin/src/RE/BSFixedString.h | 4 ++-- Plugin/src/RE/SettingsDataModel.h | 4 ++-- Plugin/src/RE/UI.h | 4 ++-- Plugin/src/main.cpp | 2 +- extern/SFSE | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Plugin/CMakeLists.txt b/Plugin/CMakeLists.txt index 7dee6202..c8a4ba6a 100644 --- a/Plugin/CMakeLists.txt +++ b/Plugin/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.21) # info project( NativeHDR - VERSION 5.1.1 + VERSION 1.0.0 LANGUAGES CXX ) diff --git a/Plugin/src/Hooks.h b/Plugin/src/Hooks.h index f2e760b5..49fda4ab 100644 --- a/Plugin/src/Hooks.h +++ b/Plugin/src/Hooks.h @@ -107,10 +107,10 @@ namespace Hooks public: static void Hook() { - _CreateDataModelOptions = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20BBAA9, Hook_CreateDataModelOptions); - _SettingsDataModelBoolEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20C9DEE, Hook_SettingsDataModelBoolEvent); - _SettingsDataModelIntEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CA0E7, Hook_SettingsDataModelIntEvent); - _SettingsDataModelFloatEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CA09B, Hook_SettingsDataModelFloatEvent); + _CreateDataModelOptions = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20BD2F9, Hook_CreateDataModelOptions); + _SettingsDataModelBoolEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB63E, Hook_SettingsDataModelBoolEvent); + _SettingsDataModelIntEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB937, Hook_SettingsDataModelIntEvent); + _SettingsDataModelFloatEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB8EB, Hook_SettingsDataModelFloatEvent); #if 0 const auto callsite1 = AsAddress(dku::Hook::Module::get().base() + 0x32ED294); diff --git a/Plugin/src/RE/BSFixedString.h b/Plugin/src/RE/BSFixedString.h index 8217b242..749d172e 100644 --- a/Plugin/src/RE/BSFixedString.h +++ b/Plugin/src/RE/BSFixedString.h @@ -12,7 +12,7 @@ namespace RE BSFixedStringCS(const char* String) { - auto addr = dku::Hook::Module::get().base() + 0x3149530; + auto addr = dku::Hook::Module::get().base() + 0x314D050; auto func = reinterpret_cast(addr); func(this, String); } @@ -21,7 +21,7 @@ namespace RE ~BSFixedStringCS() { - auto addr = dku::Hook::Module::get().base() + 0x0546D20; + auto addr = dku::Hook::Module::get().base() + 0x05466F0; auto func = reinterpret_cast(addr); func(this); } diff --git a/Plugin/src/RE/SettingsDataModel.h b/Plugin/src/RE/SettingsDataModel.h index fe7bc9f3..4cc9d5e8 100644 --- a/Plugin/src/RE/SettingsDataModel.h +++ b/Plugin/src/RE/SettingsDataModel.h @@ -69,14 +69,14 @@ namespace RE GeneralSetting() { - auto addr = dku::Hook::Module::get().base() + 0x20B801C; + auto addr = dku::Hook::Module::get().base() + 0x20B986C; auto func = reinterpret_cast(addr); func(*this); } ~GeneralSetting() { - auto addr = dku::Hook::Module::get().base() + 0x20B9220; + auto addr = dku::Hook::Module::get().base() + 0x20BAA70; auto func = reinterpret_cast(addr); func(*this); } diff --git a/Plugin/src/RE/UI.h b/Plugin/src/RE/UI.h index aa29949b..1dec37d4 100644 --- a/Plugin/src/RE/UI.h +++ b/Plugin/src/RE/UI.h @@ -144,7 +144,7 @@ namespace RE void AddItem(const BSFixedStringCS& Item) { - auto addr = dku::Hook::Module::get().base() + 0x207D080; + auto addr = dku::Hook::Module::get().base() + 0x207E280; auto func = reinterpret_cast*, const BSFixedStringCS&)>(addr); func(this, Item); } @@ -162,7 +162,7 @@ namespace RE void AddItem(T& Item) { // Seems like they have a helper function to apply NestedUIValue<> to the type - auto addr = dku::Hook::Module::get().base() + 0x20B50C8; + auto addr = dku::Hook::Module::get().base() + 0x20B6918; auto func = reinterpret_cast>*, T&)>(addr); func(&__super::m_ShuttleArray, Item); } diff --git a/Plugin/src/main.cpp b/Plugin/src/main.cpp index 8bf84710..b92664d1 100644 --- a/Plugin/src/main.cpp +++ b/Plugin/src/main.cpp @@ -14,7 +14,7 @@ DLLEXPORT constinit auto SFSEPlugin_Version = []() noexcept { //data.UsesAddressLibrary(true); data.HasNoStructUse(true); //data.IsLayoutDependent(true); - data.CompatibleVersions({ RUNTIME_VERSION_1_7_23, RUNTIME_VERSION_1_7_29 }); + data.CompatibleVersions({ RUNTIME_VERSION_1_7_33 }); return data; }(); diff --git a/extern/SFSE b/extern/SFSE index 8c4af406..9f55120a 160000 --- a/extern/SFSE +++ b/extern/SFSE @@ -1 +1 @@ -Subproject commit 8c4af406ed86baf69af676132fea337621084ae3 +Subproject commit 9f55120ae048f2e598cfb9e6e16903ea8e318ccc From d3c7b8b4f3a3dc9a035b78291e5c9fc1f4da98a8 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Tue, 26 Sep 2023 20:08:09 -0400 Subject: [PATCH 026/270] fix(lut): more shadow detail, working photomode --- .../ColorGradingMerge_FF81_cs.hlsl | 129 +++++++++++------- 1 file changed, 80 insertions(+), 49 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 9db491d7..5358f8e2 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -24,6 +24,7 @@ RWTexture3D OutMixetLUT : register(u0, space8); struct LUTAnalysis { float3 black; + float blackY; float3 white; float whiteY; #if UNUSED_PARAMS @@ -33,7 +34,6 @@ struct LUTAnalysis float maxY; float averageY; float range; - float blackY; float medianY; float averageChannel; float averageRed; @@ -61,12 +61,12 @@ uint3 ThreeToTwoDimensionCoordinates(uint3 UVW) void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) { Analysis.black = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(0)).rgb); + Analysis.blackY = Luminance(Analysis.black); Analysis.white = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(LUT_SIZE_UINT - 1u)).rgb); Analysis.whiteY = Luminance(Analysis.white); #if UNUSED_PARAMS Analysis.minY = FLT_MAX; Analysis.maxY = -FLT_MAX; - Analysis.blackY = Luminance(Analysis.black); float3 colors = 0.f; float Ys = 0.f; @@ -131,60 +131,91 @@ void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) // Analyzes each LUT texel and normalize their range. // Slow but effective. -float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, bool SDRRange = false) +float3 PatchLUTColor(Texture2D LUT, int3 UVW, float3 neutralLUTColor, bool SDRRange = false) { LUTAnalysis analysis; AnalyzeLUT(LUT, analysis); - float3 color = gamma_sRGB_to_linear(LUT.Load(UVW).rgb); + float3 color = gamma_sRGB_to_linear(LUT.Load(UVW)); const float3 originalColor = color; - //TODO: convert to using lerp function??? - // Reduce all colors by raised level of black texel - // Normalize as relative to their distance from black ("neutralLUTColor" is xyz coordinates) - const float3 reducedColor = linearNormalization(neutralLUTColor, 0.f, 1.f, 1.f / (1.f - analysis.black), 1.f); - - // Reapply black level as a multiple (applies shadow tint) - const float3 increasedColor = linearNormalization(neutralLUTColor, 0.f, 1.f, 1.f + analysis.black, 1.f); - - // Apply to base color - color = (1.f - ((1.f - color) * reducedColor)) * increasedColor; - - - const float blackDistance = hypot3(neutralLUTColor); - const float whiteDistance = hypot3(1.f - neutralLUTColor); - const float totalRange = (blackDistance + whiteDistance); - - const float sourceY = Luminance(color); - if (sourceY > 0.f) // Black will always stay black (and should) - { - const float increasedY = linearNormalization(whiteDistance, 0.f, totalRange, 1.f / analysis.whiteY, 1.f); - const float targetY = (1.f - (1.f - sourceY)) * increasedY; - - if (targetY >= 0.9999f && targetY <= 1.0005f) - { - color = 1.f; // Intentionally targeting pure white (1,1,1) - } - else if (SDRRange && targetY >= 0.999f) - { - color = 1.f; // Clamp - } - else - { - // targetY could be on LUTs (raised black point and dark blues) - color *= max(targetY, 0.f) / sourceY; + if ((analysis.whiteY > analysis.blackY) + && ((analysis.whiteY - analysis.blackY) < 1.f)) { + // LUT is clamped and not inversed + + // Lower black floor (shadow pass): + // In 3D space, drags all points towards 0 relative to how much black + // point has moved (eg: -4, -10, -5) + // Distance from 0 is factor to decide how much each pixel should move, + // with (1) (white) not moving at all. + // *Applying to each channel individually also removes tint + const float3 reduceFactor = linearNormalization( + neutralLUTColor, + 0.f, + 1.f, + 1.f / (1.f - analysis.black), + 1.f); + + // Reapply tint without raising black floor: + // In 3D space, shift points away from black **per channel** to + // reintroduce tint back to the pixels. Use multiplication to avoid + // raised black floor. + const float3 increaseFactor = linearNormalization( + neutralLUTColor, + 0.f, + 1.f, + 1.f + analysis.black, + 1.f); + + // color * reduce = [0,1] relative to XYZ: + // result *= black tint + // (0,0,0) must always compute to 0 + // (1,1,1) must remain unchanged + // Scaling is strongest at 0, and weakest at 1 (none) + color = (1.f - ((1.f - color) * reduceFactor)) * increaseFactor; + + const float blackDistance = hypot3(neutralLUTColor); + const float whiteDistance = hypot3(1.f - neutralLUTColor); + const float totalRange = (blackDistance + whiteDistance); + + const float currentY = Luminance(color); + if (currentY > 0.f) { // Skip if black + float shadowRaise = 1.f; + if (currentY < analysis.blackY) { + shadowRaise = linearNormalization( + currentY, + 0.f, + analysis.blackY, + 1.f + (1.f - analysis.blackY), + 1.f); + } + const float highlightsRaise = linearNormalization( + whiteDistance, + 0.f, + totalRange, + 1.f / analysis.whiteY, + 1.f); + const float targetY = (1.f - (1.f - currentY)) + * highlightsRaise + * shadowRaise; + + if (targetY >= 0.9999f && targetY <= 1.0005f) { + color = 1.f; // Intentionally targeting pure white (1,1,1) + } else if (SDRRange && targetY >= 0.999f) { + color = 1.f; // Clamp + } else { + // targetY could be on LUTs (raised black point and dark blues) + color *= max(targetY, 0.f) / currentY; + } + } else { + // TODO: Consider removal since no longer scaling + color = 0; // Color may have gone below 0 when scaling } - } - else - { - // TODO: Consider removal since no longer scaling - color = 0; // Color may have gone below 0 when scaling - } - // Optional step to keep colors in the SDR range. - if (SDRRange) - { - color = saturate(color); + // Optional step to keep colors in the SDR range. + if (SDRRange) { + color = saturate(color); + } } color = lerp(originalColor, color, LUTCorrectionPercentage); @@ -218,7 +249,7 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) #if LUT_IMPROVEMENT_TYPE == 1 const bool SDRRange = false; float3 LUT1Color = PatchLUTColor(LUT1, inUVW, neutralLUTColor, SDRRange); - float3 LUT2Color = PatchLUTColor(LUT3, inUVW, neutralLUTColor, SDRRange); + float3 LUT2Color = PatchLUTColor(LUT2, inUVW, neutralLUTColor, SDRRange); float3 LUT3Color = PatchLUTColor(LUT3, inUVW, neutralLUTColor, SDRRange); float3 LUT4Color = PatchLUTColor(LUT4, inUVW, neutralLUTColor, SDRRange); #elif LUT_IMPROVEMENT_TYPE == 2 From 0c00c0bf60063d7f15716d46730b269a87190afa Mon Sep 17 00:00:00 2001 From: ersh1 <16157893+ersh1@users.noreply.github.com> Date: Wed, 27 Sep 2023 06:25:31 +0200 Subject: [PATCH 027/270] some work on the ingame settings (swapchain format is fully working) --- Plugin/src/Hooks.cpp | 182 +++++++++++++++++++++++++++++++++++----- Plugin/src/Hooks.h | 70 ++++++++-------- Plugin/src/Offsets.h | 11 +++ Plugin/src/RE/Buffers.h | 24 ++++++ Plugin/src/Settings.cpp | 49 ++++++++++- Plugin/src/Settings.h | 26 ++++++ Plugin/src/Utils.cpp | 17 ++++ Plugin/src/Utils.h | 5 ++ 8 files changed, 327 insertions(+), 57 deletions(-) diff --git a/Plugin/src/Hooks.cpp b/Plugin/src/Hooks.cpp index ce05adec..b4d9439c 100644 --- a/Plugin/src/Hooks.cpp +++ b/Plugin/src/Hooks.cpp @@ -33,42 +33,186 @@ namespace Hooks return; } - SetBufferFormat(a_buffer, a_newFormat); + Utils::SetBufferFormat(a_buffer, a_newFormat); } - void Patches::SetBufferFormat(RE::BufferDefinition* a_buffer, RE::BS_DXGI_FORMAT a_newFormat) + void Hooks::RecreateSwapChain(RE::BGSSwapChainObject* a_bgsSwapchainObject, RE::BS_DXGI_FORMAT a_newFormat) { - if (!a_buffer) { - return; + if (a_bgsSwapchainObject->format != a_newFormat) { + a_bgsSwapchainObject->format = a_newFormat; + Offsets::RecreateSwapChain(*reinterpret_cast(*Offsets::unkRecreateSwapChainArg1Ptr + 0x28), a_bgsSwapchainObject, a_bgsSwapchainObject->width, a_bgsSwapchainObject->height, *Offsets::unkRecreateSwapChainArg5); + + // set correct color space + DXGI_COLOR_SPACE_TYPE newColorSpace; + switch (a_newFormat) { + case RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_B8G8R8A8_UNORM: + default: + newColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709; + break; + case RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R10G10B10A2_UNORM: + newColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020; + break; + case RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT: + newColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709; + } + a_bgsSwapchainObject->swapChainInterface->SetColorSpace1(newColorSpace); } - - auto formatNames = Utils::GetDXGIFormatNameMap(); - INFO("{} - changing from format {} to {}", a_buffer->bufferName, formatNames[Offsets::GetDXGIFormat(a_buffer->format)], formatNames[Offsets::GetDXGIFormat(a_newFormat)]) - a_buffer->format = a_newFormat; } - void Patches::SetBufferFormat(RE::Buffers a_buffer, RE::BS_DXGI_FORMAT a_format) - { - const auto buffer = (*Offsets::bufferArray)[static_cast(a_buffer)]; - SetBufferFormat(buffer, a_format); - } + void Hooks::ToggleEnableHDRSubSettings(RE::SettingsDataModel* a_model, bool a_bEnable) + { + if (const auto maxLuminanceSetting = a_model->FindSettingById(static_cast(Settings::SettingID::kMaxLuminance))) { + maxLuminanceSetting->m_Enabled.SetValue(a_bEnable); + } - void Hooks::Hook_UnkFunc(uintptr_t a1, UnkObject* a2) + if (const auto paperwhiteSetting = a_model->FindSettingById(static_cast(Settings::SettingID::kPaperwhite))) { + paperwhiteSetting->m_Enabled.SetValue(a_bEnable); + } + } + + void Hooks::Hook_UnkFunc(uintptr_t a1, RE::BGSSwapChainObject* a_bgsSwapchainObject) { + // save the pointer for later + swapChainObject = a_bgsSwapchainObject; + const auto settings = Settings::Main::GetSingleton(); switch (*settings->FrameBufferFormat) { case 1: - a2->swapChainInterface->SetColorSpace1(DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020); + a_bgsSwapchainObject->swapChainInterface->SetColorSpace1(DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020); break; case 2: - a2->swapChainInterface->SetColorSpace1(DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709); + a_bgsSwapchainObject->swapChainInterface->SetColorSpace1(DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709); break; } - return _UnkFunc(a1, a2); + return _UnkFunc(a1, a_bgsSwapchainObject); + } + + void Hooks::Hook_CreateDataModelOptions(void* a_arg1, RE::ArrayNestedUIValue& a_SettingList) + { + const auto settings = Settings::Main::GetSingleton(); + + { + auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); + auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); + constexpr auto id = static_cast(Settings::SettingID::kHDR); + + s.m_Text.SetStringValue("HDR"); + s.m_Description.SetStringValue("Sets the game's output mode between SDR, HDR10 PQ, or HDR10 scRGB."); + s.m_ID.SetValue(id); + s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Stepper); + s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); + s.m_Enabled.SetValue(true); + s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("OFF"); + s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("HDR10 PQ"); + s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("HDR10 scRGB"); + s.m_StepperData.m_ShuttleMap.GetData().m_Value.SetValue(*settings->FrameBufferFormat); + a_SettingList.AddItem(s); + } + + { + auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); + auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); + constexpr auto id = static_cast(Settings::SettingID::kMaxLuminance); + + s.m_Text.SetStringValue("Max Luminance"); + s.m_Description.SetStringValue("Sets the maximum luminance in HDR modes."); + s.m_ID.SetValue(id); + s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Slider); + s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); + s.m_Enabled.SetValue(settings->IsHDREnabled()); + s.m_SliderData.m_ShuttleMap.GetData().m_Value.SetValue(settings->GetMaxLuminanceSliderPercentage()); + s.m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue(settings->GetMaxLuminanceText().data()); + a_SettingList.AddItem(s); + } + + { + auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); + auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); + constexpr auto id = static_cast(Settings::SettingID::kPaperwhite); + + s.m_Text.SetStringValue("Paperwhite Brightness"); + s.m_Description.SetStringValue("Sets the paperwhite brightness used in HDR modes."); + s.m_ID.SetValue(id); + s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Slider); + s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); + s.m_Enabled.SetValue(settings->IsHDREnabled()); + s.m_SliderData.m_ShuttleMap.GetData().m_Value.SetValue(settings->GetPaperwhiteSliderPercentage()); + s.m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue(settings->GetPaperwhiteText().data()); + a_SettingList.AddItem(s); + } + + // Initialize the rest of the settings after ours + _CreateDataModelOptions(a_arg1, a_SettingList); + } + + void Hooks::Hook_SettingsDataModelBoolEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData) + { + _SettingsDataModelBoolEvent(a_arg1, EventData); + } + + void Hooks::Hook_SettingsDataModelIntEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData) + { + if (EventData.m_SettingID == static_cast(Settings::SettingID::kHDR)) { + const auto settings = Settings::Main::GetSingleton(); + + const auto prevValue = *settings->FrameBufferFormat; + const auto newValue = EventData.m_Value.Int; + if (prevValue != newValue) { + *settings->FrameBufferFormat = newValue; + + RE::BS_DXGI_FORMAT newFormat; + switch (*settings->FrameBufferFormat) { + case 0: + default: + newFormat = RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_B8G8R8A8_UNORM; + break; + case 1: + newFormat = RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R10G10B10A2_UNORM; + break; + case 2: + newFormat = RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT; + break; + } + + // the value in the buffer definition is not going to be read by the game anymore by this point, but changing it anyway in case something else tries to read it + Utils::SetBufferFormat(RE::Buffers::FrameBuffer, newFormat); + + if (prevValue == 0) { + ToggleEnableHDRSubSettings(EventData.m_Model, true); + } else if (newValue == 0) { + ToggleEnableHDRSubSettings(EventData.m_Model, false); + } + + RecreateSwapChain(swapChainObject, newFormat); + } + } + + _SettingsDataModelIntEvent(a_arg1, EventData); + } + + void Hooks::Hook_SettingsDataModelFloatEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData) + { + if (EventData.m_SettingID == static_cast(Settings::SettingID::kMaxLuminance)) { + const auto settings = Settings::Main::GetSingleton(); + + settings->SetMaxLuminanceFromSlider(EventData.m_Value.Float); + if (auto setting = EventData.m_Model->FindSettingById(EventData.m_SettingID)) { + setting->m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue(settings->GetMaxLuminanceText().data()); + } + + //settingTest->m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue(buffer); + } else if (EventData.m_SettingID == static_cast(Settings::SettingID::kPaperwhite)) { + const auto settings = Settings::Main::GetSingleton(); + + settings->SetPaperwhiteFromSlider(EventData.m_Value.Float); + if (auto setting = EventData.m_Model->FindSettingById(EventData.m_SettingID)) { + setting->m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue(settings->GetPaperwhiteText().data()); + } + } } - void DebugHooks::Hook_CreateDataModelOptions(void* a_arg1, RE::ArrayNestedUIValue& a_SettingList) + void DebugHooks::Hook_CreateDataModelOptions(void* a_arg1, RE::ArrayNestedUIValue& a_SettingList) { int id = 600; @@ -259,7 +403,7 @@ namespace Hooks { //#ifndef NDEBUG Utils::LogBuffers(); - DebugHooks::Hook(); + //DebugHooks::Hook(); //#endif Hooks::Hook(); Patches::Patch(); diff --git a/Plugin/src/Hooks.h b/Plugin/src/Hooks.h index 49fda4ab..8e6a95b2 100644 --- a/Plugin/src/Hooks.h +++ b/Plugin/src/Hooks.h @@ -4,8 +4,6 @@ #include "RE/Buffers.h" #include "RE/SettingsDataModel.h" -#include - namespace Hooks { class Patches @@ -17,24 +15,24 @@ namespace Hooks switch (*settings->FrameBufferFormat) { case 1: - SetBufferFormat(RE::Buffers::FrameBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R10G10B10A2_UNORM); + Utils::SetBufferFormat(RE::Buffers::FrameBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R10G10B10A2_UNORM); break; case 2: - SetBufferFormat(RE::Buffers::FrameBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT); + Utils::SetBufferFormat(RE::Buffers::FrameBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT); break; } switch (*settings->ImageSpaceBufferFormat) { case 1: - SetBufferFormat(RE::Buffers::ImageSpaceBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R10G10B10A2_UNORM); + Utils::SetBufferFormat(RE::Buffers::ImageSpaceBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R10G10B10A2_UNORM); break; case 2: - SetBufferFormat(RE::Buffers::ImageSpaceBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT); + Utils::SetBufferFormat(RE::Buffers::ImageSpaceBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT); break; } if (*settings->UpgradeUIRenderTarget) { - SetBufferFormat(RE::Buffers::ScaleformCompositeBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT); + Utils::SetBufferFormat(RE::Buffers::ScaleformCompositeBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT); } { @@ -57,49 +55,47 @@ namespace Hooks static RE::BufferDefinition* GetBufferFromString(std::string_view a_bufferName); static void UpgradeRenderTarget(RE::BufferDefinition* a_buffer, RE::BS_DXGI_FORMAT a_format, bool a_bLimited); - static void SetBufferFormat(RE::BufferDefinition* a_buffer, RE::BS_DXGI_FORMAT a_format); - static void SetBufferFormat(RE::Buffers a_buffer, RE::BS_DXGI_FORMAT a_format); }; class Hooks { public: - struct UnkObject - { - uint64_t unk00; - uint64_t unk08; - uint64_t unk10; - uint64_t unk18; - uint64_t unk20; - uint64_t unk28; - uint64_t unk30; - uint64_t unk38; - uint64_t unk40; - uint64_t unk48; - IDXGISwapChain3* swapChainInterface; - }; - static void Hook() { - const auto settings = Settings::Main::GetSingleton(); - if (*settings->FrameBufferFormat != 0) { - const auto scan = static_cast(dku::Hook::Assembly::search_pattern<"E8 ?? ?? ?? ?? 8B 4D A8 8B 45 AC">()); - if (!scan) { - ERROR("Failed to find color space hook") - } - const auto callsiteOffset = *reinterpret_cast(scan + 1); - const auto UnkFuncCallsite = AsAddress(scan + 5 + callsiteOffset + 0x3EA); + const auto scan = static_cast(dku::Hook::Assembly::search_pattern<"E8 ?? ?? ?? ?? 8B 4D A8 8B 45 AC">()); + if (!scan) { + ERROR("Failed to find color space hook") + } + const auto callsiteOffset = *reinterpret_cast(scan + 1); + const auto UnkFuncCallsite = AsAddress(scan + 5 + callsiteOffset + 0x3EA); - _UnkFunc = dku::Hook::write_call<5>(UnkFuncCallsite, Hook_UnkFunc); // 32E7856 + _UnkFunc = dku::Hook::write_call<5>(UnkFuncCallsite, Hook_UnkFunc); // 32E7856 - INFO("Found color space hook callsite at {:X}", UnkFuncCallsite) - } + INFO("Found color space hook callsite at {:X}", UnkFuncCallsite) + + _CreateDataModelOptions = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20BD2F9, Hook_CreateDataModelOptions); + _SettingsDataModelBoolEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB63E, Hook_SettingsDataModelBoolEvent); + _SettingsDataModelIntEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB937, Hook_SettingsDataModelIntEvent); + _SettingsDataModelFloatEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB8EB, Hook_SettingsDataModelFloatEvent); } private: - static void Hook_UnkFunc(uintptr_t a1, UnkObject* a2); + static inline RE::BGSSwapChainObject* swapChainObject = nullptr; + static void RecreateSwapChain(RE::BGSSwapChainObject* a_bgsSwapchainObject, RE::BS_DXGI_FORMAT a_newFormat); + static void ToggleEnableHDRSubSettings(RE::SettingsDataModel* a_model, bool a_bEnable); + + static void Hook_UnkFunc(uintptr_t a1, RE::BGSSwapChainObject* a_bgsSwapchainObject); static inline std::add_pointer_t _UnkFunc; + + static void Hook_CreateDataModelOptions(void* a_arg1, RE::ArrayNestedUIValue& a_SettingList); + static inline std::add_pointer_t _CreateDataModelOptions; + static void Hook_SettingsDataModelBoolEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData); + static inline std::add_pointer_t _SettingsDataModelBoolEvent; + static void Hook_SettingsDataModelIntEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData); + static inline std::add_pointer_t _SettingsDataModelIntEvent; + static void Hook_SettingsDataModelFloatEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData); + static inline std::add_pointer_t _SettingsDataModelFloatEvent; }; class DebugHooks @@ -107,7 +103,7 @@ namespace Hooks public: static void Hook() { - _CreateDataModelOptions = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20BD2F9, Hook_CreateDataModelOptions); + //_CreateDataModelOptions = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20BD2F9, Hook_CreateDataModelOptions); _SettingsDataModelBoolEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB63E, Hook_SettingsDataModelBoolEvent); _SettingsDataModelIntEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB937, Hook_SettingsDataModelIntEvent); _SettingsDataModelFloatEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB8EB, Hook_SettingsDataModelFloatEvent); diff --git a/Plugin/src/Offsets.h b/Plugin/src/Offsets.h index 5b4183a6..0b5f3ac7 100644 --- a/Plugin/src/Offsets.h +++ b/Plugin/src/Offsets.h @@ -10,6 +10,12 @@ class Offsets using tGetDXGIFormat = DXGI_FORMAT (*)(RE::BS_DXGI_FORMAT a_bgsFormat); static inline tGetDXGIFormat GetDXGIFormat = nullptr; + using tRecreateSwapChain = void (*)(void* a1, RE::BGSSwapChainObject* a_bgsSwapChainObject, uint32_t a_width, uint32_t a_height, uint8_t a5); + static inline tRecreateSwapChain RecreateSwapChain = nullptr; + + static inline uintptr_t* unkRecreateSwapChainArg1Ptr = nullptr; + static inline uint8_t* unkRecreateSwapChainArg5 = nullptr; + static void Initialize() { { @@ -31,5 +37,10 @@ class Offsets GetDXGIFormat = dku::Hook::GetDisp(scan); // 32F18A0 INFO("Found GetDXGIFormat at {:X}", reinterpret_cast(scan)) } + + RecreateSwapChain = reinterpret_cast(dku::Hook::Module::get().base() + 0x32F4BEC); // 0x32F14BC in 1.7.23 + + unkRecreateSwapChainArg1Ptr = reinterpret_cast(dku::Hook::Module::get().base() + 0x5916FF0); // 0x5912F80 in 1.7.23 + unkRecreateSwapChainArg5 = reinterpret_cast(dku::Hook::Module::get().base() + 0x55F8DB0); // 0x55F4D70 in 1.7.23 } }; diff --git a/Plugin/src/RE/Buffers.h b/Plugin/src/RE/Buffers.h index 7eea8f13..3fe7d42a 100644 --- a/Plugin/src/RE/Buffers.h +++ b/Plugin/src/RE/Buffers.h @@ -1,4 +1,6 @@ #pragma once +#include +#include namespace RE { @@ -362,4 +364,26 @@ namespace RE uint32_t unk4C; }; static_assert(sizeof(BufferDefinition) == 0x50); + + struct BGSSwapChainObject + { + HWND hwnd; + uint64_t unk08; + uint32_t unk10; + uint32_t bufferCount; + uint32_t width; + uint32_t height; + uint32_t unk20; + uint32_t sampleQuality; + BS_DXGI_FORMAT format; + uint64_t unk30; + uint64_t unk38; + uint64_t unk40; + uint64_t unk48; + IDXGISwapChain3* swapChainInterface; + uint64_t unk58; + ID3D12Resource** renderTargets; + uint64_t unk68; + HANDLE handle; + }; } diff --git a/Plugin/src/Settings.cpp b/Plugin/src/Settings.cpp index c6a9408b..39314303 100644 --- a/Plugin/src/Settings.cpp +++ b/Plugin/src/Settings.cpp @@ -2,7 +2,52 @@ namespace Settings { - void Main::Load() noexcept + bool Main::IsHDREnabled() const + { + return FrameBufferFormat.get_data() != 0; + } + + float Main::GetMaxLuminanceSliderPercentage() const + { + return (MaxLuminance.get_data() - luminanceSliderMin) / (luminanceSliderMax - luminanceSliderMin); + } + + std::string Main::GetMaxLuminanceText() const + { + return std::format("{}", MaxLuminance.get_data()); + } + + float Main::GetMaxLuminanceFromSlider(float a_percentage) const + { + return std::roundf(a_percentage * (luminanceSliderMax - luminanceSliderMin) + luminanceSliderMin); + } + + void Main::SetMaxLuminanceFromSlider(float a_percentage) + { + *MaxLuminance = GetMaxLuminanceFromSlider(a_percentage); + } + + float Main::GetPaperwhiteSliderPercentage() const + { + return (Paperwhite.get_data() - paperwhiteSliderMin) / (paperwhiteSliderMax - paperwhiteSliderMin); + } + + std::string Main::GetPaperwhiteText() const + { + return std::format("{}", Paperwhite.get_data()); + } + + float Main::GetPaperwhiteFromSlider(float a_percentage) const + { + return std::roundf(a_percentage * (paperwhiteSliderMax - paperwhiteSliderMin) + paperwhiteSliderMin); + } + + void Main::SetPaperwhiteFromSlider(float a_percentage) + { + *Paperwhite = GetPaperwhiteFromSlider(a_percentage); + } + + void Main::Load() noexcept { static std::once_flag ConfigInit; std::call_once(ConfigInit, [&]() { @@ -10,6 +55,8 @@ namespace Settings config.Bind(UpgradeUIRenderTarget, true); config.Bind(UpgradeRenderTargets, 2); config.Bind(FrameBufferFormat, 0); + config.Bind(MaxLuminance, 1000.f); + config.Bind(Paperwhite, 200.f); config.Bind(RenderTargetsToUpgrade, "SF_ColorBuffer", "HDRImagespaceBuffer", "ImageSpaceHalfResBuffer", "ImageProcessColorTarget", "ImageSpaceBufferB10G11R11", "ImageSpaceBufferE5B9G9R9", "TAA_idTech7HistoryColorTarget", "EnvBRDF", "GBuffer_Normal_EmissiveIntensity", "ImageSpaceBufferR10G10B10A2"); }); diff --git a/Plugin/src/Settings.h b/Plugin/src/Settings.h index 53d1a602..40f32d81 100644 --- a/Plugin/src/Settings.h +++ b/Plugin/src/Settings.h @@ -5,6 +5,13 @@ namespace Settings { using namespace DKUtil::Alias; + enum class SettingID : unsigned int + { + kHDR = 600, + kMaxLuminance, + kPaperwhite + }; + class Main : public DKUtil::model::Singleton
{ public: @@ -13,12 +20,31 @@ namespace Settings Integer UpgradeRenderTargets{ "UpgradeRenderTargets", "Main" }; Integer FrameBufferFormat{ "FrameBufferFormat", "HDR" }; + Double MaxLuminance{ "MaxLuminance", "HDR" }; + Double Paperwhite{ "Paperwhite", "HDR" }; String RenderTargetsToUpgrade{ "RenderTargetsToUpgrade", "RenderTargets" }; + bool IsHDREnabled() const; + + float GetMaxLuminanceSliderPercentage() const; + std::string GetMaxLuminanceText() const; + float GetMaxLuminanceFromSlider(float a_percentage) const; + void SetMaxLuminanceFromSlider(float a_percentage); + + float GetPaperwhiteSliderPercentage() const; + std::string GetPaperwhiteText() const; + float GetPaperwhiteFromSlider(float a_percentage) const; + void SetPaperwhiteFromSlider(float a_percentage); + void Load() noexcept; private: + static inline constexpr float luminanceSliderMin = 400.f; + static inline constexpr float luminanceSliderMax = 2000.f; + static inline constexpr float paperwhiteSliderMin = 80.f; + static inline constexpr float paperwhiteSliderMax = 500.f; + TomlConfig config = COMPILE_PROXY("Data/SFSE/Plugins/NativeHDR.toml"sv); }; } \ No newline at end of file diff --git a/Plugin/src/Utils.cpp b/Plugin/src/Utils.cpp index 5914c10d..c2584f4b 100644 --- a/Plugin/src/Utils.cpp +++ b/Plugin/src/Utils.cpp @@ -159,4 +159,21 @@ namespace Utils INFO(logString) INFO("===END LOGGING BUFFERS===") } + + void SetBufferFormat(RE::BufferDefinition* a_buffer, RE::BS_DXGI_FORMAT a_format) + { + if (!a_buffer) { + return; + } + + auto formatNames = Utils::GetDXGIFormatNameMap(); + INFO("{} - changing from format {} to {}", a_buffer->bufferName, formatNames[Offsets::GetDXGIFormat(a_buffer->format)], formatNames[Offsets::GetDXGIFormat(a_format)]) + a_buffer->format = a_format; + } + + void SetBufferFormat(RE::Buffers a_buffer, RE::BS_DXGI_FORMAT a_format) + { + const auto buffer = (*Offsets::bufferArray)[static_cast(a_buffer)]; + SetBufferFormat(buffer, a_format); + } } diff --git a/Plugin/src/Utils.h b/Plugin/src/Utils.h index 0c57bf09..deafe62f 100644 --- a/Plugin/src/Utils.h +++ b/Plugin/src/Utils.h @@ -1,4 +1,6 @@ #pragma once +#include "RE/Buffers.h" + #include namespace Utils @@ -8,4 +10,7 @@ namespace Utils void LogFormats(); void LogBuffers(); + + void SetBufferFormat(RE::BufferDefinition* a_buffer, RE::BS_DXGI_FORMAT a_format); + void SetBufferFormat(RE::Buffers a_buffer, RE::BS_DXGI_FORMAT a_format); } From eef7587c2b547cdc94067993e708236481e1f654 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Thu, 28 Sep 2023 08:00:47 -0400 Subject: [PATCH 028/270] docs(flmgrain): add reverse engineer notes --- shaders/FilmGrain/FilmGrain_FF75_ps.hlsl | 64 ++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl b/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl index f3380037..6211e40a 100644 --- a/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl +++ b/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl @@ -24,8 +24,28 @@ struct SPIRV_Cross_Output float4 SV_Target : SV_Target0; }; +// Better? random generator +float rand(float2 uv) { + return frac(sin(dot(uv,float2(12.9898,78.233)))*43758.5453123); +} + + +// Returns a random number between 0 and 1 based on seed +float bethesdaRandom(float seed) { + return frac(sin(seed) * 493013.f); +} + + void frag_main() { +// TEXCOORD = float2 [0,1] relative to screen +// TonemappedColorTexture is the framebuffer? +// Generate random noise and overlay on top of screen buffer +// filmGrainColorAndIntensity +// .x - randomInt32? changed every frame? +// .y = randomInt32? changed every frame? +// .z = strength? maybe 0.03 + float3 tonemappedColor = TonemappedColorTexture.Sample(Sampler0, float2(TEXCOORD.x, TEXCOORD.y)); #if ENABLE_HDR tonemappedColor /= HDR_GAME_PAPER_WHITE; @@ -42,13 +62,51 @@ void frag_main() const float filmGrainInvSize = 1.f / (FILM_GRAIN_TEXTURE_SIZE - 1u); const float filmGrainHalfSize = 521.f; //TODO: rename in case we keep this as 521 #endif - float additiveFilmGrain = (((frac(sin(((float(int(uint(filmGrainColorAndIntensity.x) & (FILM_GRAIN_TEXTURE_SIZE - 1u))) * filmGrainInvSize) + TEXCOORD.x) + (((float(int(uint(filmGrainColorAndIntensity.y) & (FILM_GRAIN_TEXTURE_SIZE - 1u))) * filmGrainInvSize) + TEXCOORD.y) * filmGrainHalfSize)) * 493013.f) * 2.f) - 1.f) * filmGrainColorAndIntensity.z) * inverseLuminance; - float3 tonemappedColorWithFilmGrain = tonemappedColor + float3(additiveFilmGrain, additiveFilmGrain, additiveFilmGrain); // Note: we let this possibly generate colors below 1 for scRGB + // Applying modulus against 1023 will give value between 0 and 1023 + // Same two values for all texels this frame + float randomFromRange1 = float(int(uint(filmGrainColorAndIntensity.x) & (FILM_GRAIN_TEXTURE_SIZE - 1u))); + float randomFromRange2 = float(int(uint(filmGrainColorAndIntensity.y) & (FILM_GRAIN_TEXTURE_SIZE - 1u))); + + // Divide by 1023 to get a number between 0 and 1 + float randomNormalized1 = randomFromRange1 * filmGrainInvSize; + float randomNormalized2 = randomFromRange1 * filmGrainInvSize; + + // Offset by x/y position + float uniqueX = randomNormalized1 + TEXCOORD.x; + float uniqueY = (randomNormalized2 + TEXCOORD.y) * filmGrainHalfSize; + + float seed = (uniqueX + uniqueY); + // Random number between -1 and -1; + // luminanceShift at 0 is black, 0.5 unchanged, 1 negative of current texel + float luminanceShift = (bethesdaRandom(seed) * 2.f) - 1.f; + +#if 0 // WIP fixes + float additiveFilmGrain = (luminanceShift * 1.f * inverseLuminance); + float3 tonemappedColorWithFilmGrain = float3(additiveFilmGrain, additiveFilmGrain,additiveFilmGrain); +#else + float additiveFilmGrain = (luminanceShift * filmGrainColorAndIntensity.z * inverseLuminance); + float3 newColor = float3(additiveFilmGrain, additiveFilmGrain,additiveFilmGrain); + + // Use addition to overlay color on top + // Note: we let this possibly generate colors below 1 for scRGB + float3 tonemappedColorWithFilmGrain = tonemappedColor + newColor; +#endif + #if ENABLE_HDR tonemappedColorWithFilmGrain = gamma_sRGB_to_linear(tonemappedColorWithFilmGrain) * HDR_GAME_PAPER_WHITE; #endif // ENABLE_HDR - SV_Target.xyz = tonemappedColorWithFilmGrain; +#if 0 // WIP fixes + // Right side only + if (TEXCOORD.x >= 0.5f) { + SV_Target.rgb = tonemappedColorWithFilmGrain; + } else { + SV_Target.rgb = tonemappedColor; + } +#else + SV_Target.rgb = tonemappedColorWithFilmGrain; +#endif + SV_Target.w = 1.0f; } From 000c1f26b0a2c41a6f2cda1c69efb3f33752ea30 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Thu, 28 Sep 2023 08:08:23 -0400 Subject: [PATCH 029/270] docs(filmgrain): more notes --- shaders/FilmGrain/FilmGrain_FF75_ps.hlsl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl b/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl index 6211e40a..df3f5b14 100644 --- a/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl +++ b/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl @@ -75,9 +75,11 @@ void frag_main() float uniqueX = randomNormalized1 + TEXCOORD.x; float uniqueY = (randomNormalized2 + TEXCOORD.y) * filmGrainHalfSize; + // Unique each frame and texel. (Is it though?) float seed = (uniqueX + uniqueY); - // Random number between -1 and -1; - // luminanceShift at 0 is black, 0.5 unchanged, 1 negative of current texel + + // Generate a random number between -1 and -1; + // luminanceShift at -1 is black, 0 unchanged, 1 negative of current texel float luminanceShift = (bethesdaRandom(seed) * 2.f) - 1.f; #if 0 // WIP fixes From 028a2d46b3952423644a37cb8e2ae4cba17bd1e0 Mon Sep 17 00:00:00 2001 From: Lilium Date: Thu, 28 Sep 2023 17:45:17 +0200 Subject: [PATCH 030/270] another fixup of the cleanup... --- shaders/HDRComposite/HDRComposite_ps.hlsl | 28 ++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 8a37e124..c6315082 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -719,32 +719,40 @@ PSOutput PS(PSInput psInput) float3 color = tonemappedColor; + float3 lutColor = color; + #if defined(APPLY_MERGED_COLOR_GRADING_LUT) && ENABLE_TONEMAP && ENABLE_LUT #if FIX_WRONG_SRGB_GAMMA - float3 colorForLutSampling = gamma_linear_to_sRGB(color); + lutColor = gamma_linear_to_sRGB(lutColor); -#else +#elif LUT_FIX_GAMMA_MAPPING //read gamma ini value, defaulting at 2.4 (makes little sense) float inverseGamma = 1.f / (max(SharedFrameData.Gamma, 0.001f)); //weird linear -> sRGB conversion that clips values just above 0, and raises blacks. - float3 colorForLutSampling = (pow(color, inverseGamma) * 1.055f) - 0.055f; - colorForLutSampling = max(colorForLutSampling, 0.f); // Unnecessary as the LUT sampling is already clamped. + lutColor = (pow(lutColor, inverseGamma) * 1.055f) - 0.055f; + lutColor = max(lutColor, 0.f); // Unnecessary as the LUT sampling is already clamped. #endif // FIX_WRONG_SRGB_GAMMA #if LUT_USE_TETRAHEDRAL_INTERPOLATION - float3 LutColor = TetrahedralInterpolation(Lut, colorForLutSampling); + lutColor = TetrahedralInterpolation(Lut, lutColor); #else - float3 LutColor = Lut.Sample(Sampler0, colorForLutSampling * (1.f - (1.f / LUT_SIZE)) + ((1.f / LUT_SIZE) / 2.f)); + lutColor = Lut.Sample(Sampler0, lutColor * (1.f - (1.f / LUT_SIZE)) + ((1.f / LUT_SIZE) / 2.f)); #endif //LUT_USE_TETRAHEDRAL_INTERPOLATION +#if !LUT_FIX_GAMMA_MAPPING + + lutColor = gamma_sRGB_to_linear(lutColor); + +#endif // !LUT_FIX_GAMMA_MAPPING + #if LUT_CUSTOM_STRENGTH float LutMaskAlpha = saturate(LutMask.Sample(Sampler0, psInput.TEXCOORD).x + (1.f - LUTStrength)); @@ -755,13 +763,7 @@ PSOutput PS(PSInput psInput) #endif // LUT_CUSTOM_STRENGTH - color = lerp(LutColor, color, LutMaskAlpha); - -#if !LUT_FIX_GAMMA_MAPPING - - color = pow(color, 2.2f); - -#endif // !LUT_FIX_GAMMA_MAPPING + color = lerp(lutColor, color, LutMaskAlpha); #endif // APPLY_MERGED_COLOR_GRADING_LUT From 4db826269720371de065205b3b5d94aa07863e39 Mon Sep 17 00:00:00 2001 From: Lilium Date: Thu, 28 Sep 2023 17:54:57 +0200 Subject: [PATCH 031/270] add sigmoidal contrast adjustment and enable it by default sigmoidal contrast adjustment doesn't clip colors but is less intensive --- shaders/HDRComposite/HDRComposite_ps.hlsl | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index c6315082..192b4431 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -14,13 +14,13 @@ // Also sets "ENABLE_LUT", "ENABLE_POST_PROCESS" and "ENABLE_INVERSE_TONEMAP" #define ENABLE_TONEMAP 1 #define ENABLE_POST_PROCESS 1 -#define ENABLE_PP_CONTRAST_ADJUSTMENT 1 +#define ENABLE_SIGMOIDAL_CONTRAST_ADJUSTMENT 1 #define ENABLE_LUT 1 +#define LUT_USE_TETRAHEDRAL_INTERPOLATION 1 #define ENABLE_REPLACED_TONEMAP 1 #define ENABLE_INVERSE_POST_PROCESS 0 #define CLAMP_INPUT_OUTPUT 1 -#define LUT_USE_TETRAHEDRAL_INTERPOLATION 1 cbuffer CSharedFrameData : register(b0, space6) { @@ -455,7 +455,22 @@ float3 PostProcess(float3 Color, inout float ColorLuminance) Color *= brightnessMultiplier; -#if ENABLE_PP_CONTRAST_ADJUSTMENT +#if ENABLE_SIGMOIDAL_CONTRAST_ADJUSTMENT + + // sigmoidal contrast adjustment doesn't clip colors + // https://www.imagemagick.org/Usage/color_mods/#sigmoidal + + // * 2.5 to somewhat match default contrast adjustment + float c = max(contrastIntensity * 2.5f, EPSILON); // protect against division by zero + float s = contrastMidPoint; // can be set to 0.5 for better darkening of shadows + + float minus = -1 / (1 + exp(c * s)); + + // there should be some optimisation left here + Color = (1 / (1 + exp(c * (s - Color))) + minus) + / (1 / (1 + exp(c * (s - 1))) + minus); + +#else // Contrast adjustment (shift the colors from 0<->1 to (e.g.) -0.5<->0.5 range, multiply and shift back). // The higher the distance from the contrast middle point, the more contrast will change the color. From 383eeba146d0fe4c3b7a5fc2449b47ab8ac7bb7f Mon Sep 17 00:00:00 2001 From: Lilium Date: Thu, 28 Sep 2023 18:06:27 +0200 Subject: [PATCH 032/270] update original shaders to 1.7.33.0 From 47e999a606cd0506ee0c2da8ac5d0c361a096625 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Thu, 28 Sep 2023 23:36:58 +0300 Subject: [PATCH 033/270] tonemap polish --- .../ColorGradingMerge_FF81_cs.hlsl | 27 ++---- shaders/HDRComposite/HDRComposite_ps.hlsl | 85 +++++++++---------- .../ScaleformComposite_FFAA_ps.hlsl | 5 +- shaders/Unused/ps_2532921200.hlsl | 33 ------- shaders/shared.hlsl | 6 +- 5 files changed, 50 insertions(+), 106 deletions(-) delete mode 100644 shaders/Unused/ps_2532921200.hlsl diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 5358f8e2..2621d6dc 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -131,7 +131,7 @@ void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) // Analyzes each LUT texel and normalize their range. // Slow but effective. -float3 PatchLUTColor(Texture2D LUT, int3 UVW, float3 neutralLUTColor, bool SDRRange = false) +float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, bool SDRRange = false) { LUTAnalysis analysis; AnalyzeLUT(LUT, analysis); @@ -199,9 +199,7 @@ float3 PatchLUTColor(Texture2D LUT, int3 UVW, float3 neutralLUTColor, bo * highlightsRaise * shadowRaise; - if (targetY >= 0.9999f && targetY <= 1.0005f) { - color = 1.f; // Intentionally targeting pure white (1,1,1) - } else if (SDRRange && targetY >= 0.999f) { + if (SDRRange && targetY >= 1.f) { color = 1.f; // Clamp } else { // targetY could be on LUTs (raised black point and dark blues) @@ -228,11 +226,10 @@ float3 PatchLUTColor(Texture2D LUT, int3 UVW, float3 neutralLUTColor, bo [numthreads(LUT_SIZE_UINT, LUT_SIZE_UINT, 1)] void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) { - const int3 inUVW = ThreeToTwoDimensionCoordinates(SV_DispatchThreadID); - #define outUVW SV_DispatchThreadID + const uint3 inUVW = ThreeToTwoDimensionCoordinates(SV_DispatchThreadID); + const uint3 outUVW = SV_DispatchThreadID; float3 neutralLUTColor = float3(outUVW) / (LUT_SIZE - 1.f); // The neutral LUT is automatically generated by the coordinates, but it's baked with sRGB gamma - neutralLUTColor = gamma_sRGB_to_linear(neutralLUTColor); #if LUT_IMPROVEMENT_TYPE == 0 || LUT_IMPROVEMENT_TYPE == 2 @@ -247,7 +244,7 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) #endif // LUT_IMPROVEMENT_TYPE #if LUT_IMPROVEMENT_TYPE == 1 - const bool SDRRange = false; + const bool SDRRange = !((bool)ENABLE_HDR); float3 LUT1Color = PatchLUTColor(LUT1, inUVW, neutralLUTColor, SDRRange); float3 LUT2Color = PatchLUTColor(LUT2, inUVW, neutralLUTColor, SDRRange); float3 LUT3Color = PatchLUTColor(LUT3, inUVW, neutralLUTColor, SDRRange); @@ -277,8 +274,6 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) } #endif // LUT_IMPROVEMENT_TYPE -#if LUT_ADDITIONAL_NEUTRAL_PERCENTAGE - float adjustedNeutralLUTPercentage = lerp(PcwColorGradingMerge.neutralLUTPercentage, 1.f, additionalNeutralLUTPercentage); float adjustedLUT1Percentage = lerp(PcwColorGradingMerge.LUT1Percentage, 0.f, additionalNeutralLUTPercentage); float adjustedLUT2Percentage = lerp(PcwColorGradingMerge.LUT2Percentage, 0.f, additionalNeutralLUTPercentage); @@ -291,17 +286,7 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) + (adjustedLUT3Percentage * LUT3Color) + (adjustedLUT4Percentage * LUT4Color); -#else - - float3 mixedLUT = (PcwColorGradingMerge.neutralLUTPercentage * neutralLUTColor) - + (PcwColorGradingMerge.LUT1Percentage * LUT1Color) - + (PcwColorGradingMerge.LUT2Percentage * LUT2Color) - + (PcwColorGradingMerge.LUT3Percentage * LUT3Color) - + (PcwColorGradingMerge.LUT4Percentage * LUT4Color); - -#endif // LUT_ADDITIONAL_NEUTRAL_PERCENTAGE - -// Convert to sRGB after blending between LUTs, so the blends are done in sRGB space +// Convert to sRGB after blending between LUTs, so the blends are done in linear space, which gives more consistent and correct results #if !LUT_FIX_GAMMA_MAPPING mixedLUT = gamma_linear_to_sRGB(mixedLUT); diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 192b4431..1f77ab94 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -8,10 +8,12 @@ //#define APPLY_CINEMATICS //#define APPLY_MERGED_COLOR_GRADING_LUT -#define SDR_USE_GAMMA_2_2 0 +// This makes sense to use given we fix up (normalize) the LUTs colors and their gamma mapping, though the SDR tonemapper had still been developed with sRGB<->2.2 mismatch. +#define SDR_USE_GAMMA_2_2 1 +// Suggested if "LUT_FIX_GAMMA_MAPPING" is true #define FIX_WRONG_SRGB_GAMMA 1 -// Also sets "ENABLE_LUT", "ENABLE_POST_PROCESS" and "ENABLE_INVERSE_TONEMAP" +// Also disables "ENABLE_LUT", "ENABLE_POST_PROCESS" and "ENABLE_INVERSE_TONEMAP" #define ENABLE_TONEMAP 1 #define ENABLE_POST_PROCESS 1 #define ENABLE_SIGMOIDAL_CONTRAST_ADJUSTMENT 1 @@ -409,7 +411,7 @@ float3 DICETonemap(float3 Color, float MaxOutputLuminance) float FindLuminanceToRestore(float tonemapLostLuminance, float preColorCorrectionLuminance, float postColorCorrectionLuminance) { - // Try to restore any luminance above "color", as it would have been lost during tone mapping. + // Try to restore any luminance that would have been lost during tone mapping (e.g. tonemapping, post process, LUTs, ... can all clip values). // // To achieve this, we re-apply the lost luminance, but only by the inverse amount the luminance changed during the color correction. // e.g. if the color correction fully changed the luminance, then we don't change it any further, @@ -506,12 +508,13 @@ float3 PostProcess_Inverse(float3 Color, float ColorLuminance) return Color; } - +// Takes input coordinates. Returns output color in linear space (also works in sRGB but it's not ideal). float3 TetrahedralInterpolation( Texture3D Lut, - float3 Color) + float3 EncodedColor) { - float3 coords = Color.rgb * (LUT_SIZE - 1); + float3 coords = EncodedColor.rgb * (LUT_SIZE - 1); + float3 color = 0; // baseInd is on [0,LUT_SIZE-1] int3 baseInd = coords; @@ -540,7 +543,7 @@ float3 TetrahedralInterpolation( float3 f2 = fract.r - fract.g; float3 f3 = fract.g - fract.b; - Color = (f2 * v2) + (f3 * v3); + color = (f2 * v2) + (f3 * v3); } else if (fract.r >= fract.b) // R > B > G { @@ -555,7 +558,7 @@ float3 TetrahedralInterpolation( float3 f2 = fract.r - fract.b; float3 f3 = fract.b - fract.g; - Color.rgb = (f2 * v2) + (f3 * v3); + color = (f2 * v2) + (f3 * v3); } else // B > R > G { @@ -570,12 +573,12 @@ float3 TetrahedralInterpolation( float3 f2 = fract.b - fract.r; float3 f3 = fract.r - fract.g; - Color = (f2 * v2) + (f3 * v3); + color = (f2 * v2) + (f3 * v3); } } else { - if (fract.g <= fract.b) // B > G > R + if (fract.g <= fract.b) // B > G > R { nextInd = baseInd + int3(0, 0, 1); float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; @@ -588,7 +591,7 @@ float3 TetrahedralInterpolation( float3 f2 = fract.b - fract.g; float3 f3 = fract.g - fract.r; - Color = (f2 * v2) + (f3 * v3); + color = (f2 * v2) + (f3 * v3); } else if (fract.r >= fract.b) // G > R > B { @@ -603,7 +606,7 @@ float3 TetrahedralInterpolation( float3 f2 = fract.g - fract.r; float3 f3 = fract.r - fract.b; - Color = (f2 * v2) + (f3 * v3); + color = (f2 * v2) + (f3 * v3); } else // G > B > R { @@ -618,11 +621,11 @@ float3 TetrahedralInterpolation( float3 f2 = fract.g - fract.b; float3 f3 = fract.b - fract.r; - Color = (f2 * v2) + (f3 * v3); + color = (f2 * v2) + (f3 * v3); } } - return Color + (f1 * v1) + (f4 * v4); + return color + (f1 * v1) + (f4 * v4); } @@ -636,7 +639,7 @@ PSOutput PS(PSInput psInput) // Doing gamut mapping, or keeping the colors outside of BT.709 doesn't seem to be right, as they seem to be just be accidentally coming out of some shader math. inputColor = max(inputColor, 0.f); -#endif +#endif // CLAMP_INPUT_OUTPUT #if defined(APPLY_BLOOM) @@ -728,64 +731,53 @@ PSOutput PS(PSInput psInput) tonemappedColor = saturate(tonemappedColor); // This is the luminance lost by clamping SDR tonemappers to the 0-1 range, and again by the fact that LUTs only take 0-1 range input. - const float tonemapLostLuminance = Luminance(tonemappedUnclampedColor - tonemappedColor); //TODO: clip any negative values here? The unclamped tonemapper might actually have negative "lost" luminance. + const float tonemapLostLuminance = Luminance(tonemappedUnclampedColor - tonemappedColor); const float preColorCorrectionLuminance = Luminance(tonemappedColor); float3 color = tonemappedColor; - float3 lutColor = color; - #if defined(APPLY_MERGED_COLOR_GRADING_LUT) && ENABLE_TONEMAP && ENABLE_LUT #if FIX_WRONG_SRGB_GAMMA - lutColor = gamma_linear_to_sRGB(lutColor); + const float3 LUTCoordinates = gamma_linear_to_sRGB(color); -#elif LUT_FIX_GAMMA_MAPPING +#else - //read gamma ini value, defaulting at 2.4 (makes little sense) + // Read gamma ini value, defaulting at 2.4 (makes little sense) float inverseGamma = 1.f / (max(SharedFrameData.Gamma, 0.001f)); - //weird linear -> sRGB conversion that clips values just above 0, and raises blacks. - lutColor = (pow(lutColor, inverseGamma) * 1.055f) - 0.055f; - lutColor = max(lutColor, 0.f); // Unnecessary as the LUT sampling is already clamped. + // Weird linear -> sRGB conversion that clips values just above 0, and raises blacks. + const float3 LUTCoordinates = max((pow(color, inverseGamma) * 1.055f) - 0.055f, 0.f); // Does "max()" is unnecessary as LUT sampling is already clamped. #endif // FIX_WRONG_SRGB_GAMMA #if LUT_USE_TETRAHEDRAL_INTERPOLATION - lutColor = TetrahedralInterpolation(Lut, lutColor); + const float3 lutColor = TetrahedralInterpolation(Lut, LUTCoordinates); #else - lutColor = Lut.Sample(Sampler0, lutColor * (1.f - (1.f / LUT_SIZE)) + ((1.f / LUT_SIZE) / 2.f)); + const float3 lutColor = Lut.Sample(Sampler0, LUTCoordinates * (1.f - (1.f / LUT_SIZE)) + ((1.f / LUT_SIZE) / 2.f)); #endif //LUT_USE_TETRAHEDRAL_INTERPOLATION #if !LUT_FIX_GAMMA_MAPPING - lutColor = gamma_sRGB_to_linear(lutColor); + lutColor = gamma_sRGB_to_linear(lutColor); // We always work in linear space #endif // !LUT_FIX_GAMMA_MAPPING -#if LUT_CUSTOM_STRENGTH - float LutMaskAlpha = saturate(LutMask.Sample(Sampler0, psInput.TEXCOORD).x + (1.f - LUTStrength)); -#else - - float LutMaskAlpha = LutMask.Sample(Sampler0, psInput.TEXCOORD).x; - -#endif // LUT_CUSTOM_STRENGTH - color = lerp(lutColor, color, LutMaskAlpha); #endif // APPLY_MERGED_COLOR_GRADING_LUT - const float postColorCorrectionLuminance = Luminance(color); - #if ENABLE_HDR + const float postColorCorrectionLuminance = Luminance(color); + // Restore any luminance beyond 1 that ended up clipped by HDR->SDR tonemappers and any subsequent image manipulation color *= FindLuminanceToRestore(tonemapLostLuminance, preColorCorrectionLuminance, postColorCorrectionLuminance); @@ -795,6 +787,7 @@ PSOutput PS(PSInput psInput) //TODO: passing in the previous luminance might not be the best, though is there any other way really? //Should we at least shift it by how much the LUT shifted the luminance? To invert it more correctly. + //If we did this, we should also move FindLuminanceToRestore() to be after it? color = PostProcess_Inverse(color, prePostProcessColorLuminance); #endif // ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS @@ -842,10 +835,10 @@ PSOutput PS(PSInput psInput) #endif // ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS color *= HDR_GAME_PAPER_WHITE; - - //const float maxOutputLuminance = HDR_MAX_OUTPUT_NITS / WhiteNits_BT709; + //TODO: find a tonemapper that looks more like Hable? - //color = DICETonemap(color, maxOutputLuminance); + const float maxOutputLuminance = HDR_MAX_OUTPUT_NITS / WhiteNits_BT709; + color = DICETonemap(color, maxOutputLuminance); #else @@ -857,27 +850,27 @@ PSOutput PS(PSInput psInput) color = clamp(color, 0.f, FLT16_MAX); // Avoid extremely high numbers turning into NaN in FP16 -#endif +#endif // CLAMP_INPUT_OUTPUT -#else +#else // ENABLE_HDR -#if SDR_USE_GAMMA_2_2 //TODO: This makes sense to have if we fix up the LUTs, though the SDR tonemapper had still been developed with sRGB<->2.2 mismatch +#if SDR_USE_GAMMA_2_2 color = pow(color, 1.f / 2.2f); #else - // Do sRGB gamma even if we'd be playing on gamma 2.2 screens, as the game was already calibrated for 2.2 gamma + // Do sRGB gamma even if we'd be playing on gamma 2.2 screens, as the game was already calibrated for 2.2 gamma despite using the wrong formula color = gamma_linear_to_sRGB(color); +#endif // SDR_USE_GAMMA_2_2 + #if CLAMP_INPUT_OUTPUT color = saturate(color); #endif // CLAMP_INPUT_OUTPUT -#endif // SDR_USE_GAMMA_2_2 - #endif // ENABLE_HDR PSOutput psOutput; diff --git a/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl b/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl index 2c9a1c95..ab7cca70 100644 --- a/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl +++ b/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl @@ -30,6 +30,7 @@ float4 PS(PSInputs inputs) : SV_Target float4 UIColor = inputTexture.Sample(inputSampler, float2(inputs.uv.x, inputs.uv.y)); float UIIntensity = asfloat(Layout.Unknown1); + // Theoretically all UI is in sRGB (though it might have been designed on gamma 2.2 screens, we can't know that, but it was definately targeting gamma 2.2 as output anyway). UIColor.xyz = gamma_sRGB_to_linear(UIColor.xyz); UIColor.xyz = UIColor.xyz * UIIntensity; #if !ENABLE_HDR @@ -37,12 +38,12 @@ float4 PS(PSInputs inputs) : SV_Target #else #if HDR_GAMMA_CORRECTION UIColor.xyz = pow(gamma_linear_to_sRGB(UIColor.xyz), 2.2f); -#endif +#endif // HDR_GAMMA_CORRECTION UIColor.xyz *= HDR_UI_PAPER_WHITE; // Scale alpha to emulate sRGB gamma blending (we blend in linear space in HDR), // this won't ever be perfect but it's close enough for most cases. // We do a saturate to avoid pow of -0, which might lead to unexpected results. - //UIColor.a = pow(saturate(UIColor.a), HDR_UI_BLEND_POW); //TODO: base the percentage of application of "HDR_UI_BLEND_POW" based on how black/dark the UI color is. + UIColor.a = pow(saturate(UIColor.a), HDR_UI_BLEND_POW); //TODO: base the percentage of application of "HDR_UI_BLEND_POW" based on how black/dark the UI color is. #endif // ENABLE_HDR return UIColor; diff --git a/shaders/Unused/ps_2532921200.hlsl b/shaders/Unused/ps_2532921200.hlsl deleted file mode 100644 index 3b091822..00000000 --- a/shaders/Unused/ps_2532921200.hlsl +++ /dev/null @@ -1,33 +0,0 @@ -Texture2D _8 : register(t0, space8); -SamplerState _11 : register(s0, space8); - -static float4 TEXCOORD; -static float4 SV_Target; - -struct SPIRV_Cross_Input -{ - float4 TEXCOORD : TEXCOORD0; -}; - -struct SPIRV_Cross_Output -{ - float4 SV_Target : SV_Target0; -}; - -void frag_main() -{ - float4 _31 = _8.Sample(_11, float2(TEXCOORD.x, TEXCOORD.y)); - SV_Target.x = _31.x; - SV_Target.y = _31.y; - SV_Target.z = _31.z; - SV_Target.w = _31.w; -} - -SPIRV_Cross_Output main(SPIRV_Cross_Input stage_input) -{ - TEXCOORD = stage_input.TEXCOORD; - frag_main(); - SPIRV_Cross_Output stage_output; - stage_output.SV_Target = SV_Target; - return stage_output; -} diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 04192193..09fc8bbe 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -10,11 +10,9 @@ #define HDR_GAME_PAPER_WHITE 1.f #define HDR_UI_PAPER_WHITE 2.5f - -// This possibly shifts colors a lot, but it's mathematically correct, it's also necessary for HDR LUTs to work. +// Makes LUTs sampling work in linear space, which is mathematically correct. Without this, they are stored as ~sRGB in a float texture and sampled in sRGB without acknowledging it. +// This possibly shifts colors a lot, but it's correct, it's also necessary for HDR LUTs to work. #define LUT_FIX_GAMMA_MAPPING 1 -#define LUT_CUSTOM_STRENGTH 1 -#define LUT_ADDITIONAL_NEUTRAL_PERCENTAGE 1 #define LUT_SIZE 16.f #define LUT_SIZE_UINT (uint)LUT_SIZE From a43238e2ba05f986fe87994fe52065e8c0b0558b Mon Sep 17 00:00:00 2001 From: Filoppi Date: Fri, 29 Sep 2023 14:57:58 +0300 Subject: [PATCH 034/270] -Add Pumbo AutoHDR as inverse tonemap mode, as Hable Inv might be changing colors too much -Changed PaperWhite to 203 nits -Added saturate to tetrahdral LUT interp coords -Added saturate to inv hable input -Added ForceTonemapper index variable for quick test -Added new contrast curve type, closer to the og (?) -Moved luminance restoration to be done after inv tonemap (as inv tonemap expects a 0-1 SDR range as input) -Improved luminance restoration by acknowleding the LUTs HDR colors that would get clipped by inv tonemap -Moved some defines around, added some comments -Fixed compilation errors --- .../ColorGradingMerge_FF81_cs.hlsl | 14 +- shaders/HDRComposite/HDRComposite_ps.hlsl | 202 +++++++++++++----- shaders/color.hlsl | 4 + shaders/shared.hlsl | 12 +- 4 files changed, 164 insertions(+), 68 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 2621d6dc..534b8602 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -2,6 +2,9 @@ #include "../color.hlsl" #include "../math.hlsl" +// 0 None, 1 ShortFuse technique (normalization), 2 luminance preservation (doesn't look so good) +#define LUT_IMPROVEMENT_TYPE 1 +#define FORCE_SDR_RANGE_LUTS 0 // Make some small quality cuts for the purpose of optimization #define OPTIMIZE_LUT_ANALYSIS true // For future development @@ -180,7 +183,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b const float currentY = Luminance(color); if (currentY > 0.f) { // Skip if black - float shadowRaise = 1.f; + float shadowRaise = 1.f; // Brightness multiplier from shadow if (currentY < analysis.blackY) { shadowRaise = linearNormalization( currentY, @@ -194,11 +197,10 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b 0.f, totalRange, 1.f / analysis.whiteY, - 1.f); - const float targetY = (1.f - (1.f - currentY)) - * highlightsRaise - * shadowRaise; + 1.f); // Brightness multiplier from highlight + const float targetY = currentY * highlightsRaise * shadowRaise; + // TODO: why are we clamping to full white here? luminance 1 (or beyond) might not match a white color at all, should we instead try to conserve the hue? Though that's much harder as we'd need hue mapping. if (SDRRange && targetY >= 1.f) { color = 1.f; // Clamp } else { @@ -244,7 +246,7 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) #endif // LUT_IMPROVEMENT_TYPE #if LUT_IMPROVEMENT_TYPE == 1 - const bool SDRRange = !((bool)ENABLE_HDR); + const bool SDRRange = !((bool)ENABLE_HDR) || (bool)FORCE_SDR_RANGE_LUTS; float3 LUT1Color = PatchLUTColor(LUT1, inUVW, neutralLUTColor, SDRRange); float3 LUT2Color = PatchLUTColor(LUT2, inUVW, neutralLUTColor, SDRRange); float3 LUT3Color = PatchLUTColor(LUT3, inUVW, neutralLUTColor, SDRRange); diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 1f77ab94..c8be13b2 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -11,16 +11,20 @@ // This makes sense to use given we fix up (normalize) the LUTs colors and their gamma mapping, though the SDR tonemapper had still been developed with sRGB<->2.2 mismatch. #define SDR_USE_GAMMA_2_2 1 // Suggested if "LUT_FIX_GAMMA_MAPPING" is true -#define FIX_WRONG_SRGB_GAMMA 1 +#define FIX_WRONG_SRGB_GAMMA_FORMULA 1 -// Also disables "ENABLE_LUT", "ENABLE_POST_PROCESS" and "ENABLE_INVERSE_TONEMAP" +// This disables most other features (post process, LUTs, ...) #define ENABLE_TONEMAP 1 #define ENABLE_POST_PROCESS 1 -#define ENABLE_SIGMOIDAL_CONTRAST_ADJUSTMENT 1 +// 0 original (weak, generates values beyond 0-1 which then get clipped), 1 improved (looks more natural, avoids values below 0, but will overshoot beyond 1 more often, and will raise blacks), 2 Sigmoidal (smoothest looking, but harder to match) +#define POST_PROCESS_CONTRAST_TYPE 1 #define ENABLE_LUT 1 -#define LUT_USE_TETRAHEDRAL_INTERPOLATION 1 +#define ENABLE_LUT_TETRAHEDRAL_INTERPOLATION 1 #define ENABLE_REPLACED_TONEMAP 1 +// Use AutoHDR as "inverse tonemapper" (maintains a look closer to the original) +#define ENABLE_AUTOHDR 1 #define ENABLE_INVERSE_POST_PROCESS 0 +#define ENABLE_LUMINANCE_RESTORE 1 #define CLAMP_INPUT_OUTPUT 1 @@ -93,6 +97,8 @@ struct PSOutput static const float LUTStrength = 1.f; static const float RestorePreLUTLuminance = 1.f; +// 0 Ignored, 1 ACES Reference, 2 ACES Custom, 3 Hable, 4+ Disable tonemapper +static const uint ForceTonemapper = 0; static const float ACES_a = 2.51f; static const float ACES_b = 0.03f; @@ -347,6 +353,9 @@ float3 Hable_Inverse( float shoulderSegment_B, float invScale) { + // There's no inverse formula for colors beyond the 0-1 range + InputColor = saturate(InputColor); + float3 itmColor; for (uint channel = 0; channel < 3; channel++) @@ -409,7 +418,24 @@ float3 DICETonemap(float3 Color, float MaxOutputLuminance) return Color; } -float FindLuminanceToRestore(float tonemapLostLuminance, float preColorCorrectionLuminance, float postColorCorrectionLuminance) +// AutoHDR pass to generate some HDR brightess out of an SDR signal (it has no effect if HDR is not engaged). +// This is hue conserving and only really affects highlights. +// https://github.com/Filoppi/PumboAutoHDR +float3 PumboAutoHDR(float3 Color, float MaxOutputNits, float PaperWhite) +{ + static const float AutoHDRShoulderPow = 3.5f; //TODO: exposure to user? + + const float SDRRatio = Luminance(Color); + // Limit AutoHDR brightness, it won't look good beyond a certain level. + // The paper white multiplier is applied later so we account for that. + const float AutoHDRMaxWhite = max(MaxOutputNits / PaperWhite, WhiteNits_BT709) / WhiteNits_BT709; + const float AutoHDRShoulderRatio = 1.f - max(1.f - SDRRatio, 0.f); + const float AutoHDRExtraRatio = pow(AutoHDRShoulderRatio, AutoHDRShoulderPow) * (AutoHDRMaxWhite - 1.f); + const float AutoHDRTotalRatio = SDRRatio + AutoHDRExtraRatio; + return Color * (AutoHDRTotalRatio / SDRRatio); +} + +float FindLuminanceToRestoreScale(float3 color, float tonemapLostLuminance, float preColorCorrectionLuminance, float postColorCorrectionLuminance) { // Try to restore any luminance that would have been lost during tone mapping (e.g. tonemapping, post process, LUTs, ... can all clip values). // @@ -419,25 +445,26 @@ float FindLuminanceToRestore(float tonemapLostLuminance, float preColorCorrectio // This will be "accurate" as long as the color correction scaled the luminance linearly in the output as it grew in the input, // but even if it didn't, it should still look fine. - // This will re-apply an amount of lost luminance based on how much the CC reduced it: + // This will re-apply an amount of lost luminance based on how much the CC (LUT) reduced it: // the more the CC reduced the luminance, the less we will apply. // If the CC increased the luminance though, we will apply even more than the base lost amount. - tonemapLostLuminance = isnan(tonemapLostLuminance) ? 0.f : tonemapLostLuminance; - const float luminanceCCInvChange = max(1.f - (preColorCorrectionLuminance - postColorCorrectionLuminance), 0.f); - //const float luminanceCCInvChange = clamp(postColorCorrectionLuminance / preColorCorrectionLuminance, 0.f, 2.f); //TODO: try this - const float currentLum = postColorCorrectionLuminance; // Should be equal to "Luminance(color)" - const float targetLum = max(currentLum + tonemapLostLuminance, 0.f); - float scale = luminanceCCInvChange; - + const float currentLum = Luminance(color); if (currentLum > 0.f) { + tonemapLostLuminance = isnan(tonemapLostLuminance) ? 0.f : tonemapLostLuminance; + const float luminanceCCInvChange = max(1.f - (preColorCorrectionLuminance - postColorCorrectionLuminance), 0.f); + //const float luminanceCCInvChange = clamp(postColorCorrectionLuminance / preColorCorrectionLuminance, 0.f, 2.f); //TODO: try this + + const float targetLum = max(currentLum + tonemapLostLuminance, 0.f); + float scale = luminanceCCInvChange; scale *= targetLum / currentLum; return scale; } return 1.f; } -float3 PostProcess(float3 Color, inout float ColorLuminance) +// "MidGrayScale" is how much the mid gray shifted from the originally intended tonemapped input (e.g. if we run this function on the untonemapped image, we can remap the mid gray) +float3 PostProcess(float3 Color, inout float ColorLuminance, float MidGrayScale = 1.f) { const uint hdrCmpDatIndex = PcwHdrComposite.HdrCmpDatIndex; const float4 highlightsColorFilter = HdrCmpDat[hdrCmpDatIndex].HighlightsColorFilter; @@ -445,7 +472,7 @@ float3 PostProcess(float3 Color, inout float ColorLuminance) const float hableSaturation = HdrCmpDat[hdrCmpDatIndex].HableSaturation; const float brightnessMultiplier = HdrCmpDat[hdrCmpDatIndex].BrightnessMultiplier; // Neutral at 1 const float contrastIntensity = HdrCmpDat[hdrCmpDatIndex].ContrastIntensity; // Neutral at 1 - const float contrastMidPoint = PerSceneConstants[316u].z; + const float contrastMidPoint = PerSceneConstants[316u].z * MidGrayScale; ColorLuminance = Luminance(Color); @@ -456,14 +483,24 @@ float3 PostProcess(float3 Color, inout float ColorLuminance) Color += lerp(float3(0.f, 0.f, 0.f), ColorLuminance * highlightsColorFilter.rgb, highlightsColorFilter.a); Color *= brightnessMultiplier; - -#if ENABLE_SIGMOIDAL_CONTRAST_ADJUSTMENT +#if POST_PROCESS_CONTRAST_TYPE == 0 + + // Contrast adjustment (shift the colors from 0<->1 to (e.g.) -0.5<->0.5 range, multiply and shift back). + // The higher the distance from the contrast middle point, the more contrast will change the color. + Color = ((Color - contrastMidPoint) * contrastIntensity) + contrastMidPoint; + +#elif POST_PROCESS_CONTRAST_TYPE == 1 + + Color = pow(Color / contrastMidPoint, contrastIntensity) * contrastMidPoint; + +#elif POST_PROCESS_CONTRAST_TYPE == 2 // sigmoidal contrast adjustment doesn't clip colors // https://www.imagemagick.org/Usage/color_mods/#sigmoidal - // * 2.5 to somewhat match default contrast adjustment - float c = max(contrastIntensity * 2.5f, EPSILON); // protect against division by zero + // Multiplier to somewhat match the original contrast adjustment + static const float SigmoidalContrastAdjustment = 2.5f; //TODO: we could probably find an even better default multiplication + float c = max(contrastIntensity * SigmoidalContrastAdjustment, EPSILON); // protect against division by zero float s = contrastMidPoint; // can be set to 0.5 for better darkening of shadows float minus = -1 / (1 + exp(c * s)); @@ -472,19 +509,13 @@ float3 PostProcess(float3 Color, inout float ColorLuminance) Color = (1 / (1 + exp(c * (s - Color))) + minus) / (1 / (1 + exp(c * (s - 1))) + minus); -#else - - // Contrast adjustment (shift the colors from 0<->1 to (e.g.) -0.5<->0.5 range, multiply and shift back). - // The higher the distance from the contrast middle point, the more contrast will change the color. - Color = ((Color - contrastMidPoint) * contrastIntensity) + contrastMidPoint; - #endif - Color = lerp(Color, colorFilter.rgb, colorFilter.a); + Color = lerp(Color, colorFilter.rgb * MidGrayScale, colorFilter.a); return Color; } -float3 PostProcess_Inverse(float3 Color, float ColorLuminance) +float3 PostProcess_Inverse(float3 Color, float ColorLuminance, float MidGrayScale = 1.f) { const uint hdrCmpDatIndex = PcwHdrComposite.HdrCmpDatIndex; const float4 highlightsColorFilter = HdrCmpDat[hdrCmpDatIndex].HighlightsColorFilter; @@ -492,12 +523,18 @@ float3 PostProcess_Inverse(float3 Color, float ColorLuminance) const float hableSaturation = HdrCmpDat[hdrCmpDatIndex].HableSaturation; const float brightnessMultiplier = HdrCmpDat[hdrCmpDatIndex].BrightnessMultiplier; // Neutral at 1 const float contrastIntensity = HdrCmpDat[hdrCmpDatIndex].ContrastIntensity; // Neutral at 1 - const float contrastMidPoint = PerSceneConstants[316u].z; + const float contrastMidPoint = PerSceneConstants[316u].z * MidGrayScale; // We can't invert the color filter, so we will only inverse the post process by the unfiltered amount const float colorFilterInverse = 1.f - colorFilter.a; +#if POST_PROCESS_CONTRAST_TYPE == 0 Color = ((Color - contrastMidPoint) / contrastIntensity) + contrastMidPoint; +#elif POST_PROCESS_CONTRAST_TYPE == 1 + Color = pow(Color / contrastMidPoint, 1.f / contrastIntensity) * contrastMidPoint; +#elif POST_PROCESS_CONTRAST_TYPE == 2 + //TODO: implement +#endif Color /= brightnessMultiplier; @@ -511,9 +548,10 @@ float3 PostProcess_Inverse(float3 Color, float ColorLuminance) // Takes input coordinates. Returns output color in linear space (also works in sRGB but it's not ideal). float3 TetrahedralInterpolation( Texture3D Lut, - float3 EncodedColor) + float3 LUTCoordinates) { - float3 coords = EncodedColor.rgb * (LUT_SIZE - 1); + // We need to clip the input coordinates as LUT texure samples below are not clamped. + float3 coords = saturate(LUTCoordinates.rgb) * (LUT_SIZE - 1); // Pixel coords float3 color = 0; // baseInd is on [0,LUT_SIZE-1] @@ -676,8 +714,10 @@ PSOutput PS(PSInput psInput) float hable_invScale; const bool clampACES = false; + + int tonemapperIndex = ForceTonemapper > 0 ? ForceTonemapper : PcwHdrComposite.Tmo; - switch (PcwHdrComposite.Tmo) + switch (tonemapperIndex) { case 1: { @@ -726,12 +766,12 @@ PSOutput PS(PSInput psInput) #endif //ENABLE_POST_PROCESS -#endif // ENABLE_TONEMAP - tonemappedColor = saturate(tonemappedColor); +#endif // ENABLE_TONEMAP + // This is the luminance lost by clamping SDR tonemappers to the 0-1 range, and again by the fact that LUTs only take 0-1 range input. - const float tonemapLostLuminance = Luminance(tonemappedUnclampedColor - tonemappedColor); + float tonemapLostLuminance = Luminance(tonemappedUnclampedColor - tonemappedColor); const float preColorCorrectionLuminance = Luminance(tonemappedColor); @@ -739,7 +779,7 @@ PSOutput PS(PSInput psInput) #if defined(APPLY_MERGED_COLOR_GRADING_LUT) && ENABLE_TONEMAP && ENABLE_LUT -#if FIX_WRONG_SRGB_GAMMA +#if FIX_WRONG_SRGB_GAMMA_FORMULA const float3 LUTCoordinates = gamma_linear_to_sRGB(color); @@ -750,25 +790,27 @@ PSOutput PS(PSInput psInput) // Weird linear -> sRGB conversion that clips values just above 0, and raises blacks. const float3 LUTCoordinates = max((pow(color, inverseGamma) * 1.055f) - 0.055f, 0.f); // Does "max()" is unnecessary as LUT sampling is already clamped. -#endif // FIX_WRONG_SRGB_GAMMA +#endif // FIX_WRONG_SRGB_GAMMA_FORMULA -#if LUT_USE_TETRAHEDRAL_INTERPOLATION +#if ENABLE_LUT_TETRAHEDRAL_INTERPOLATION - const float3 lutColor = TetrahedralInterpolation(Lut, LUTCoordinates); + float3 lutColor = TetrahedralInterpolation(Lut, LUTCoordinates); #else - const float3 lutColor = Lut.Sample(Sampler0, LUTCoordinates * (1.f - (1.f / LUT_SIZE)) + ((1.f / LUT_SIZE) / 2.f)); + float3 lutColor = Lut.Sample(Sampler0, LUTCoordinates * (1.f - (1.f / LUT_SIZE)) + ((1.f / LUT_SIZE) / 2.f)); -#endif //LUT_USE_TETRAHEDRAL_INTERPOLATION +#endif // ENABLE_LUT_TETRAHEDRAL_INTERPOLATION #if !LUT_FIX_GAMMA_MAPPING - lutColor = gamma_sRGB_to_linear(lutColor); // We always work in linear space + // We always work in linear space so convert to it. + // We never acknowledge the original wrong gamma function here (we don't really care). + lutColor = gamma_sRGB_to_linear(lutColor); #endif // !LUT_FIX_GAMMA_MAPPING - float LutMaskAlpha = saturate(LutMask.Sample(Sampler0, psInput.TEXCOORD).x + (1.f - LUTStrength)); + const float LutMaskAlpha = saturate(LutMask.Sample(Sampler0, psInput.TEXCOORD).x + (1.f - LUTStrength)); color = lerp(lutColor, color, LutMaskAlpha); @@ -776,33 +818,54 @@ PSOutput PS(PSInput psInput) #if ENABLE_HDR - const float postColorCorrectionLuminance = Luminance(color); - - // Restore any luminance beyond 1 that ended up clipped by HDR->SDR tonemappers and any subsequent image manipulation - color *= FindLuminanceToRestore(tonemapLostLuminance, preColorCorrectionLuminance, postColorCorrectionLuminance); - #if ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP + float postColorCorrectionLuminance = Luminance(color); + //TODO: this isn't hue conserving, though we don't really have a way of doing it in a hue conserving way, as then it would break the brightness mapping. + const float postColorCorrectionClampedLuminance = Luminance(saturate(color)); + // Some inverse tonemapper formulas can't take any values beyond 0-1, so we'll need to clip them and restore their luminance. + const float colorCorrectionHDRClippedLuminanceChange = postColorCorrectionClampedLuminance > 0.f ? (postColorCorrectionLuminance / postColorCorrectionClampedLuminance) : 1.f; + #if ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS //TODO: passing in the previous luminance might not be the best, though is there any other way really? //Should we at least shift it by how much the LUT shifted the luminance? To invert it more correctly. - //If we did this, we should also move FindLuminanceToRestore() to be after it? color = PostProcess_Inverse(color, prePostProcessColorLuminance); #endif // ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS + + float paperWhite = HDR_GAME_PAPER_WHITE; + + const float midGrayIn = MidGray; + float midGrayOut = midGrayIn; +#if ENABLE_AUTOHDR + if (tonemapperIndex == 1 || tonemapperIndex == 2 || tonemapperIndex == 3) + { + paperWhite = HDR_REFERENCE_PAPER_WHITE; //TODO: fix this up, this is because HDR_GAME_PAPER_WHITE is 1 atm and it's not good for AutoHDR + tonemapperIndex = 0; + color = PumboAutoHDR(color, HDR_MAX_OUTPUT_NITS, paperWhite); + midGrayOut = PumboAutoHDR(midGrayIn.xxx, HDR_MAX_OUTPUT_NITS, paperWhite).x; + } +#endif // ENABLE_AUTOHDR + // Restore a color very close to the original linear one, but with all the other post process and LUT transformations baked in - switch(PcwHdrComposite.Tmo) + switch (tonemapperIndex) { case 1: { color = ACESReference_Inverse(color); + midGrayOut = ACESReference_Inverse(midGrayIn.xxx).x; + tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; + postColorCorrectionLuminance = postColorCorrectionClampedLuminance; } break; case 2: { color = ACESParametric_Inverse(color, acesParam_modE, acesParam_modA); + midGrayOut = ACESParametric_Inverse(midGrayIn.xxx, acesParam_modE, acesParam_modA).x; + tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; + postColorCorrectionLuminance = postColorCorrectionClampedLuminance; } break; case 3: @@ -820,27 +883,56 @@ PSOutput PS(PSInput psInput) hable_shoulderSegment_lnA, hable_shoulderSegment_B, hable_invScale); + midGrayOut = Hable_Inverse(midGrayIn.xxx, + hable_params_y0, + hable_params_y1, + hable_dstParams_W, + hable_toeSegment_lnA, + hable_toeSegment_B, + hable_midSegment_offsetX, + hable_midSegment_lnA, + hable_shoulderSegment_offsetX, + hable_shoulderSegment_offsetY, + hable_shoulderSegment_lnA, + hable_shoulderSegment_B, + hable_invScale).x; + tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; + postColorCorrectionLuminance = postColorCorrectionClampedLuminance; } break; default: - break; // Any luminance lost by this tonemap case would have already been restored above with "tonemapLostLuminance". + break; } - + #if ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS - //TODO: ... this has no use now, and also, it won't work in the HDR range - color = PostProcess(color, prePostProcessColorLuminance); + color = PostProcess(color, prePostProcessColorLuminance, midGrayScale); #endif // ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS - color *= HDR_GAME_PAPER_WHITE; +#if ENABLE_LUMINANCE_RESTORE //TODO: this will go beyond the max output nits if "ENABLE_AUTOHDR" is true, we need to fix a way to make them work together (maybe re-tonemap always at the end?) + float midGrayScale = midGrayOut / midGrayIn; + // Change the "tonemapLostLuminance" from being in the tonemapped SDR (0-1) range to the untonemapped HDR image range (by scaling it by the mid gray change, which is the best we can do). + // This will also acknowledge any clipped luminance from the range beyond 0-1. + tonemapLostLuminance *= midGrayScale; + + // Restore any luminance beyond 1 that ended up clipped by HDR->SDR tonemappers and any subsequent image manipulation. + // It's important to do these after the inverse tonemappers, as they can't handle values beyond 0-1. + color *= FindLuminanceToRestoreScale(color, tonemapLostLuminance, preColorCorrectionLuminance, postColorCorrectionLuminance); +#endif // ENABLE_LUMINANCE_RESTORE + + color *= paperWhite; + +#if !ENABLE_AUTOHDR //TODO: find a tonemapper that looks more like Hable? const float maxOutputLuminance = HDR_MAX_OUTPUT_NITS / WhiteNits_BT709; color = DICETonemap(color, maxOutputLuminance); + +#endif // !ENABLE_AUTOHDR -#else +#else // ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP color *= HDR_GAME_PAPER_WHITE; diff --git a/shaders/color.hlsl b/shaders/color.hlsl index 5e7a4f00..18650b1e 100644 --- a/shaders/color.hlsl +++ b/shaders/color.hlsl @@ -2,6 +2,10 @@ // Rec.709 SDR white is meant to be mapped to 80 nits (not 100, even if some game engine (UE) and consoles (PS5) interpret it as such). static const float WhiteNits_BT709 = 80.f; +static const float ReferenceWhiteNits_BT2408 = 203.f; + +// SDR mid gray +static const float MidGray = 0.18f; // SMPTE ST 2084 (Perceptual Quantization) is only defined until this amount of nits. // This is also the max each color channel can have in HDR10. diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 09fc8bbe..bc472338 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -6,15 +6,13 @@ #define ENABLE_HDR 1 // Set equal to the max nits your display can output #define HDR_MAX_OUTPUT_NITS 1000.f -// Brings the range roughly from 80 nits to 200 nits (close to the target reference of 203 nits) -#define HDR_GAME_PAPER_WHITE 1.f -#define HDR_UI_PAPER_WHITE 2.5f +// Brings the range roughly from 80 nits to 203 nits +#define HDR_REFERENCE_PAPER_WHITE (ReferenceWhiteNits_BT2408 / WhiteNits_BT709) +#define HDR_GAME_PAPER_WHITE 1.0f +#define HDR_UI_PAPER_WHITE HDR_REFERENCE_PAPER_WHITE // Makes LUTs sampling work in linear space, which is mathematically correct. Without this, they are stored as ~sRGB in a float texture and sampled in sRGB without acknowledging it. // This possibly shifts colors a lot, but it's correct, it's also necessary for HDR LUTs to work. #define LUT_FIX_GAMMA_MAPPING 1 #define LUT_SIZE 16.f -#define LUT_SIZE_UINT (uint)LUT_SIZE - -// 0 None, 1 ShortFuse technique (normalization), 2 luminance preservation (doesn't look so good) -#define LUT_IMPROVEMENT_TYPE 1 +#define LUT_SIZE_UINT (uint)LUT_SIZE \ No newline at end of file From 78c07fd7073d5239db76e6454a81cbb618ac0883 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Fri, 29 Sep 2023 18:05:19 +0300 Subject: [PATCH 035/270] compilation fix --- shaders/HDRComposite/HDRComposite_ps.hlsl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index c8be13b2..25c22a2e 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -905,6 +905,8 @@ PSOutput PS(PSInput psInput) break; } + const float midGrayScale = midGrayOut / midGrayIn; + #if ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS color = PostProcess(color, prePostProcessColorLuminance, midGrayScale); @@ -912,7 +914,6 @@ PSOutput PS(PSInput psInput) #endif // ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS #if ENABLE_LUMINANCE_RESTORE //TODO: this will go beyond the max output nits if "ENABLE_AUTOHDR" is true, we need to fix a way to make them work together (maybe re-tonemap always at the end?) - float midGrayScale = midGrayOut / midGrayIn; // Change the "tonemapLostLuminance" from being in the tonemapped SDR (0-1) range to the untonemapped HDR image range (by scaling it by the mid gray change, which is the best we can do). // This will also acknowledge any clipped luminance from the range beyond 0-1. tonemapLostLuminance *= midGrayScale; From 8c6f11e870b3a3c7664d0b9102fea12d243f0485 Mon Sep 17 00:00:00 2001 From: Lilium Date: Sat, 30 Sep 2023 01:19:55 +0200 Subject: [PATCH 036/270] add readability change to Hable tone mapping --- shaders/HDRComposite/HDRComposite_ps.hlsl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 25c22a2e..e5a45c75 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -238,6 +238,12 @@ float3 Hable( midSegment_lnA = RCP_LOG2_E * midSegment_lnA_optimised; // mid section end + // toeM and shoulderM are missing because EvalDerivativeLinearGamma (https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L81-L85) + // evaluates to gamma * m * pow(m * x + b, gamma - 1) with gamma being 1 + // this just returns m + #define toeM m + #define shoulderM m + // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L137-L138 // max(EPSILON, pow(params_yX, gamma)) params_y0 = max(EPSILON, dstParams_y0); // is pow(x, gamma) with gamma = 1 @@ -252,7 +258,7 @@ float3 Hable( // toe section // SolveAB https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L60 - toeSegment_B = (m * params_x0) / (params_y0 + EPSILON); + toeSegment_B = (toeM * params_x0) / (params_y0 + EPSILON); float _410 = log2(params_y0); //doesn't belong to SolveAB (it might does) float toeSegment_lnA_optimised = -toeSegment_B * log(params_x0); toeSegment_lnA = _410 * RCP_LOG2_E + toeSegment_lnA_optimised; @@ -264,7 +270,7 @@ float3 Hable( shoulderSegment_offsetX = 1.f + params_overshootX; shoulderSegment_offsetY = params_overshootY; // x + 1 was optimised away // SolveAB - shoulderSegment_B = ((m * shoulderSection_x0) / (shoulderSection_y0 + EPSILON)); + shoulderSegment_B = ((shoulderM * shoulderSection_x0) / (shoulderSection_y0 + EPSILON)); float shoulderSegment_B_optimised = shoulderSegment_B * RCP_LOG2_E; shoulderSegment_lnA = (log2(shoulderSection_y0) * RCP_LOG2_E) - (shoulderSegment_B_optimised * log2(shoulderSection_x0)); // shoulder section end From a739a25faaea88d88f0af3a4a1ef080a7f6c85f5 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 30 Sep 2023 14:11:39 +0300 Subject: [PATCH 037/270] Polish LUTs a bit, simplify math and fix some issues with it (mostly to constrain the output within 0-1) --- .../ColorGradingMerge_FF81_cs.hlsl | 116 ++++++++++-------- 1 file changed, 64 insertions(+), 52 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 534b8602..29e3d7bc 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -63,7 +63,7 @@ uint3 ThreeToTwoDimensionCoordinates(uint3 UVW) void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) { - Analysis.black = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(0)).rgb); + Analysis.black = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(0u)).rgb); Analysis.blackY = Luminance(Analysis.black); Analysis.white = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(LUT_SIZE_UINT - 1u)).rgb); Analysis.whiteY = Luminance(Analysis.white); @@ -142,33 +142,24 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b float3 color = gamma_sRGB_to_linear(LUT.Load(UVW)); const float3 originalColor = color; + // TODO: do we need these branches? Shaders are best without branches, especially when they could be replaced by min/max math. + // Is the branch to prevent weird LUTs from being adjusted? if ((analysis.whiteY > analysis.blackY) - && ((analysis.whiteY - analysis.blackY) < 1.f)) { - // LUT is clamped and not inversed - + && ((analysis.whiteY - analysis.blackY) < 1.f)) + { // Lower black floor (shadow pass): // In 3D space, drags all points towards 0 relative to how much black - // point has moved (eg: -4, -10, -5) + // point has moved (eg (on 256): -4, -10, -5) // Distance from 0 is factor to decide how much each pixel should move, // with (1) (white) not moving at all. // *Applying to each channel individually also removes tint - const float3 reduceFactor = linearNormalization( - neutralLUTColor, - 0.f, - 1.f, - 1.f / (1.f - analysis.black), - 1.f); + const float3 reduceFactor = lerp(1.f / (1.f - analysis.black), 1.f, pow(neutralLUTColor, 2.f)); //TODO: test pow // Reapply tint without raising black floor: // In 3D space, shift points away from black **per channel** to // reintroduce tint back to the pixels. Use multiplication to avoid // raised black floor. - const float3 increaseFactor = linearNormalization( - neutralLUTColor, - 0.f, - 1.f, - 1.f + analysis.black, - 1.f); + const float3 increaseFactor = lerp(1.f + analysis.black, 1.f, neutralLUTColor); // color * reduce = [0,1] relative to XYZ: // result *= black tint @@ -176,46 +167,68 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b // (1,1,1) must remain unchanged // Scaling is strongest at 0, and weakest at 1 (none) color = (1.f - ((1.f - color) * reduceFactor)) * increaseFactor; + // TODO: this introduces a lot of colors beyond the 0-1 range, which will then get clipped, causing a hue shift + +#if 0 // HDR range analysis + if (any(color > 1)) + { + return 0; + } + else if (any(color < 0)) + { + return 1; + } +#endif + + static const float cubeNormalization = sqrt(3.f); // Normalize to 0-1 range + // How distant are our LUT coordinates from black or white in 3D cube space? + const float blackDistance = hypot3(neutralLUTColor) / cubeNormalization; + const float whiteDistance = hypot3(1.f - neutralLUTColor) / cubeNormalization; + const float totalRange = blackDistance + whiteDistance; + + const float currentY = Luminance(color); // In case this was negative, the shadows raise pass should bring it back to the >= 0 range + + // Brightness multiplier from shadows + const float shadowsRaise = linearNormalization( + min(currentY, analysis.blackY), + 0.f, + analysis.blackY, + 1.f + analysis.blackY, + 1.f); + + // Brightness multiplier from highlights + float highlightsRaise = linearNormalization( + whiteDistance, + 0.f, + totalRange, + 1.f / analysis.whiteY, + 1.f); + // Scale by the maximum value this could ever have, so it's normalized and we are guaranteed target luminance doesn't go beyond 1. + highlightsRaise *= SDRRange ? analysis.whiteY : 1.f; + const float targetY = currentY * highlightsRaise * shadowsRaise; - const float blackDistance = hypot3(neutralLUTColor); - const float whiteDistance = hypot3(1.f - neutralLUTColor); - const float totalRange = (blackDistance + whiteDistance); - - const float currentY = Luminance(color); - if (currentY > 0.f) { // Skip if black - float shadowRaise = 1.f; // Brightness multiplier from shadow - if (currentY < analysis.blackY) { - shadowRaise = linearNormalization( - currentY, - 0.f, - analysis.blackY, - 1.f + (1.f - analysis.blackY), - 1.f); - } - const float highlightsRaise = linearNormalization( - whiteDistance, - 0.f, - totalRange, - 1.f / analysis.whiteY, - 1.f); // Brightness multiplier from highlight - const float targetY = currentY * highlightsRaise * shadowRaise; - - // TODO: why are we clamping to full white here? luminance 1 (or beyond) might not match a white color at all, should we instead try to conserve the hue? Though that's much harder as we'd need hue mapping. - if (SDRRange && targetY >= 1.f) { - color = 1.f; // Clamp - } else { - // targetY could be on LUTs (raised black point and dark blues) - color *= max(targetY, 0.f) / currentY; - } - } else { - // TODO: Consider removal since no longer scaling - color = 0; // Color may have gone below 0 when scaling + // TODO: why are we clamping to full white here? luminance 1 (or beyond) might not match a white color at all, should we instead try to conserve the hue? Though that's much harder as we'd need to analyze more LUT pixels. + if (SDRRange && targetY >= 1.f) + { + color = 1.f; // Clamp + } + else + { + // targetY could be on LUTs (raised black point and dark blues) + color *= max(targetY, 0.f) / currentY; } // Optional step to keep colors in the SDR range. - if (SDRRange) { + if (SDRRange) + { color = saturate(color); } + // TODO: consider removing this if it's proven unnecessary on all LUTs + // Some protection against invalid colors + else if (Luminance(color) < 0.f) + { + color = 0.f; + } } color = lerp(originalColor, color, LUTCorrectionPercentage); @@ -223,7 +236,6 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b return color; } - // Dispatch size is 1 1 16 (x and y have one thread and one thread group, while z has 16 thread groups with a thread each) [numthreads(LUT_SIZE_UINT, LUT_SIZE_UINT, 1)] void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) From b5ddd423c05e2b126db671d2ba99a1ff8f3fbc80 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 30 Sep 2023 14:12:32 +0300 Subject: [PATCH 038/270] WIP: Hable improvements --- shaders/HDRComposite/HDRComposite_ps.hlsl | 35 +++++++++++++++++------ 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index e5a45c75..2805385b 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -15,14 +15,17 @@ // This disables most other features (post process, LUTs, ...) #define ENABLE_TONEMAP 1 +// Tweak the OG tonemappers to either look better in general, or simply be more compatible with HDR +#define ENABLE_TONEMAP_IMPROVEMENTS 1 #define ENABLE_POST_PROCESS 1 // 0 original (weak, generates values beyond 0-1 which then get clipped), 1 improved (looks more natural, avoids values below 0, but will overshoot beyond 1 more often, and will raise blacks), 2 Sigmoidal (smoothest looking, but harder to match) #define POST_PROCESS_CONTRAST_TYPE 1 #define ENABLE_LUT 1 +// LUTs are too low resolutions to resolve gradients smoothly if the LUT color suddenly changes between samples #define ENABLE_LUT_TETRAHEDRAL_INTERPOLATION 1 #define ENABLE_REPLACED_TONEMAP 1 // Use AutoHDR as "inverse tonemapper" (maintains a look closer to the original) -#define ENABLE_AUTOHDR 1 +#define ENABLE_AUTOHDR 0 #define ENABLE_INVERSE_POST_PROCESS 0 #define ENABLE_LUMINANCE_RESTORE 1 #define CLAMP_INPUT_OUTPUT 1 @@ -174,6 +177,8 @@ float3 ACESParametric_Inverse(float3 Color, float modE, float modA) } // https://github.com/johnhable/fw-public +// http://filmicworlds.com/blog/filmic-tonemapping-with-piecewise-power-curves/ +// This is NOT the Uncharted 2 tonemapper, which was also by Hable. float3 Hable( in float3 InputColor, out float params_y0, @@ -191,11 +196,14 @@ float3 Hable( { // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L202 - float toeLength = pow(saturate(PerSceneConstants[3266u].w), 2.2f); + float toeLength = pow(saturate(PerSceneConstants[3266u].w), 2.2f); // 2.2 is probably based on perceptual gamma, but we don't know why it's applied here float toeStrength = saturate(PerSceneConstants[3266u].z); float shoulderLength = clamp(saturate(PerSceneConstants[3267u].y), EPSILON, BTHCNST); float shoulderStrength = max(PerSceneConstants[3267u].x, 0.f); float shoulderAngle = saturate(PerSceneConstants[3267u].z); +#if ENABLE_TONEMAP_IMPROVEMENTS + +#endif //dstParams float dstParams_x0 = toeLength * 0.5f; @@ -224,7 +232,7 @@ float3 Hable( float params_x0 = dstParams_x0 / dstParams_W; float params_x1 = dstParams_x1 / dstParams_W; //float params_overshootX = dstParams_overshootX / dstParams_W; // this step was optimised away - #define params_overshootX dstParams_overshootX + float params_overshootX = dstParams_overshootX; float dx = y1_offset / dstParams_W; // mid section @@ -252,7 +260,7 @@ float3 Hable( //OLD: params_y1 = max(EPSILON, exp2(log2(dstParams_y1))); // pow(1.f + dstParams_overshootY, gamma) - 1.f - // -1 was optimised away as shoulderSegment_offsetY is params_overshootY + 1 + // -1 was optimised away as shoulderSegment_offsetY is "params_overshootY + 1" float params_overshootY = 1.f + dstParams_overshootY; // is pow(x, gamma) with gamma = 1 //OLD: float params_overshootY = exp2(log2(1.f + dstParams_overshootY)); @@ -283,7 +291,7 @@ float3 Hable( evalY0 = -exp2(((shoulderSegment_B_optimised * log2(params_overshootX)) + shoulderSegment_lnA) * LOG2_E) + params_overshootY; } // Eval end - invScale = 1.f / (evalY0); + invScale = 1.f / evalY0; float3 toneMapped; @@ -299,7 +307,7 @@ float3 Hable( } else if (normX < params_x1) { - float evalMidSegment_y0 = normX + midSegment_offsetX; //is -(-midSegment_offsetX) + float evalMidSegment_y0 = normX + midSegment_offsetX; // was -(-midSegment_offsetX) if (evalMidSegment_y0 > 0.f) { returnChannel = exp2(log2(evalMidSegment_y0) + midSegment_lnA_optimised); @@ -307,8 +315,7 @@ float3 Hable( } else { - //float evalShoulderSegment_y0 = ((-1.f) - params_overshootX) + normX; - // small optimisation from the original decompilation + //float evalShoulderSegment_y0 = ((-1.f) - params_overshootX) + normX; // small optimisation from the original decompilation float evalShoulderSegment_y0 = (1.f + params_overshootX) - normX; float evalShoulderReturn = 0.f; if (evalShoulderSegment_y0 > 0.f) @@ -332,15 +339,22 @@ float HableEval_Inverse( float lnA, float B) { +#if 0 // This version might work better on Nvidia (to be investigated more before deleting the alternative branch) + float y0 = max((Channel - offsetY) / scaleY, EPSILON); + float x0 = exp((log(y0) - lnA) / B); + + return x0 / scaleX + offsetX; +#else float y0 = (Channel - offsetY) / scaleY; float x0 = 0.f; - if (y0 > 0.f) + if (y0 > 0.f) // log(0) is invalid { x0 = exp((log(y0) - lnA) / B); } return x0 / scaleX + offsetX; +#endif } // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L45-L52 @@ -366,6 +380,8 @@ float3 Hable_Inverse( for (uint channel = 0; channel < 3; channel++) { + InputColor[channel] = min(InputColor[channel], 0.995f); //TODO: remove... temp workaround for broken shoulder + // scaleY and offsetY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L187-L197 // toe if (InputColor[channel] < params_y0) @@ -889,6 +905,7 @@ PSOutput PS(PSInput psInput) hable_shoulderSegment_lnA, hable_shoulderSegment_B, hable_invScale); + //TODO: make function template midGrayOut = Hable_Inverse(midGrayIn.xxx, hable_params_y0, hable_params_y1, From 3d12aef0d62c3042ba1476410dc5a6aa221ca9b4 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 30 Sep 2023 14:30:54 +0300 Subject: [PATCH 039/270] Re-implement PR #4 from @clshortfuse (with some minor changes) --- .../ColorGradingMerge_FF81_cs.hlsl | 43 +++++++++++++++---- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 29e3d7bc..63baf1e1 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -167,9 +167,8 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b // (1,1,1) must remain unchanged // Scaling is strongest at 0, and weakest at 1 (none) color = (1.f - ((1.f - color) * reduceFactor)) * increaseFactor; - // TODO: this introduces a lot of colors beyond the 0-1 range, which will then get clipped, causing a hue shift - -#if 0 // HDR range analysis + +#if 0 // "Invalid" range analysis if (any(color > 1)) { return 0; @@ -180,6 +179,13 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b } #endif +#if 0 // TODO: the above code introduces a lot of colors beyond the 0-1 range, which will then get clipped, causing a hue shift + // Color may have gone negative + // For example, if black is (3,3,3) and another value is (0,0,4), that + // may result in (-3,-3,1) + color = clamp(color, 0.f, FLT_MAX); +#endif + static const float cubeNormalization = sqrt(3.f); // Normalize to 0-1 range // How distant are our LUT coordinates from black or white in 3D cube space? const float blackDistance = hypot3(neutralLUTColor) / cubeNormalization; @@ -188,15 +194,32 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b const float currentY = Luminance(color); // In case this was negative, the shadows raise pass should bring it back to the >= 0 range - // Brightness multiplier from shadows + // Brightness multiplier from shadows: + // Because the amount the black level was raised is proportional to + // the harshness of a linear gradient, a compensation must be made + // to avoid black crushing, and maintain visibility at certain + // points in the LUT. The scaling used will be relative to the + // raised black floor level to ensure proportional consistency per + // LUT. This is only done to the newly created shadow section (if any). + // Simplified, if black started raised, a new shadow section was + // created, and that needs extra luminance for visibility. const float shadowsRaise = linearNormalization( min(currentY, analysis.blackY), 0.f, analysis.blackY, - 1.f + analysis.blackY, + 1.f + analysis.blackY, // Shadow compensation percentage ramp factor 1.f); + // TODO: Analyze grayscale for shadow raise. For example, if black shadow was raised + // by 5%, then analyze the ramping from 5%-10% and apply that to + // the new 0% - 5% raise. + + // TODO: apply both shadow and highlight raise to the whole image range, instead of a portion of it, to make its gradient smoother? Or maybe apply it in perceptual (gamma) space. + // The functions could also be simplified a lot. - // Brightness multiplier from highlights + // Brightness multiplier from highlights: + // Boost luminance of all texels relative to their distance to white + // and how much white is to be raised. + // (ie: white drags everything towards it) float highlightsRaise = linearNormalization( whiteDistance, 0.f, @@ -210,12 +233,14 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b // TODO: why are we clamping to full white here? luminance 1 (or beyond) might not match a white color at all, should we instead try to conserve the hue? Though that's much harder as we'd need to analyze more LUT pixels. if (SDRRange && targetY >= 1.f) { - color = 1.f; // Clamp + color = 1.f; // Clamp (clip to full white, it also looks great with external AutoHDR) + // Or, limit to max luminance while maintaining hue, though may result in LUT max Y being below 1 + // Or, still target full white for (1,1,1), but scale others relative to whiteY + // Or, don't adjust unless white point } else { - // targetY could be on LUTs (raised black point and dark blues) - color *= max(targetY, 0.f) / currentY; + color *= max(targetY, 0.f) / max(currentY, 0.f); // Clamp luminances to avoid a double negative creating a positive number } // Optional step to keep colors in the SDR range. From 703c7deb32a09d29e1a16e9aaa3fa9e78746db66 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 30 Sep 2023 14:31:16 +0300 Subject: [PATCH 040/270] avoid using defines in funcs --- shaders/HDRComposite/HDRComposite_ps.hlsl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 2805385b..2c081e88 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -249,9 +249,9 @@ float3 Hable( // toeM and shoulderM are missing because EvalDerivativeLinearGamma (https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L81-L85) // evaluates to gamma * m * pow(m * x + b, gamma - 1) with gamma being 1 // this just returns m - #define toeM m - #define shoulderM m - + const float toeM = m; + const float shoulderM = m; + // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L137-L138 // max(EPSILON, pow(params_yX, gamma)) params_y0 = max(EPSILON, dstParams_y0); // is pow(x, gamma) with gamma = 1 From 8420cebc77db7e314d494b424d86f7fe4b1138d2 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 30 Sep 2023 16:52:37 +0300 Subject: [PATCH 041/270] Add: -Inverse tonemap by luminance (to maintain SDR hue) -Inverse tonemap highlights only (to maintain SDR crushed shadow filmic look) -Automatically find paper white multiplier by checking how much mid gray shifted with the inverse tonemapper -Improve LUTs shadow invalid colors --- .../ColorGradingMerge_FF81_cs.hlsl | 4 +- shaders/HDRComposite/HDRComposite_ps.hlsl | 275 ++++++++++++------ shaders/shared.hlsl | 10 +- 3 files changed, 197 insertions(+), 92 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 63baf1e1..9ada7918 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -179,7 +179,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b } #endif -#if 0 // TODO: the above code introduces a lot of colors beyond the 0-1 range, which will then get clipped, causing a hue shift +#if 1 // TODO: the above code introduces a lot of colors beyond the 0-1 range, which will then get clipped, causing a hue shift // Color may have gone negative // For example, if black is (3,3,3) and another value is (0,0,4), that // may result in (-3,-3,1) @@ -226,8 +226,10 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b totalRange, 1.f / analysis.whiteY, 1.f); +#if 1 // This destroys luminance // Scale by the maximum value this could ever have, so it's normalized and we are guaranteed target luminance doesn't go beyond 1. highlightsRaise *= SDRRange ? analysis.whiteY : 1.f; +#endif const float targetY = currentY * highlightsRaise * shadowsRaise; // TODO: why are we clamping to full white here? luminance 1 (or beyond) might not match a white color at all, should we instead try to conserve the hue? Though that's much harder as we'd need to analyze more LUT pixels. diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 2c081e88..4a9f084e 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -16,7 +16,7 @@ // This disables most other features (post process, LUTs, ...) #define ENABLE_TONEMAP 1 // Tweak the OG tonemappers to either look better in general, or simply be more compatible with HDR -#define ENABLE_TONEMAP_IMPROVEMENTS 1 +#define ENABLE_TONEMAP_IMPROVEMENTS 0 #define ENABLE_POST_PROCESS 1 // 0 original (weak, generates values beyond 0-1 which then get clipped), 1 improved (looks more natural, avoids values below 0, but will overshoot beyond 1 more often, and will raise blacks), 2 Sigmoidal (smoothest looking, but harder to match) #define POST_PROCESS_CONTRAST_TYPE 1 @@ -24,6 +24,10 @@ // LUTs are too low resolutions to resolve gradients smoothly if the LUT color suddenly changes between samples #define ENABLE_LUT_TETRAHEDRAL_INTERPOLATION 1 #define ENABLE_REPLACED_TONEMAP 1 +// Invert tonemap by luminance (conserves SDR hue) +#define INVERT_TONEMAP_BY_LUMINANCE 1 +// Only invert highlights, which helps conserve the SDR filmic look (shadow crush) +#define INVERT_TONEMAP_HIGHLIGHTS_ONLY 1 // Use AutoHDR as "inverse tonemapper" (maintains a look closer to the original) #define ENABLE_AUTOHDR 0 #define ENABLE_INVERSE_POST_PROCESS 0 @@ -137,41 +141,44 @@ float3 ACESReference(float3 Color, bool Clamp) // ACESFilm "per scene" float3 ACESParametric(in float3 Color, in bool Clamp, inout float modE, inout float modA) { - const float AcesParam0 = PerSceneConstants[3266u].x; - const float AcesParam1 = PerSceneConstants[3266u].y; + const float AcesParam0 = PerSceneConstants[3266u].x; // Constant is usually 11.2 + const float AcesParam1 = PerSceneConstants[3266u].y; // Constant is usually 0.022 modE = AcesParam1; modA = ((0.56f / AcesParam0) + ACES_b) + (AcesParam1 / (AcesParam0 * AcesParam0)); return ACES(Color, Clamp, modE, modA); } -float3 ACES_Inverse(float3 Color, float modE, float modA) +template +T ACES_Inverse(T Color, float modE, float modA) { //TODO: does this apply for `ACESParametric` as well? Probably //TODO: figure out if we could still use the unclamped color. There should be a way to invert it. // ACES is not defined for any values beyond 0-1 Color = saturate(Color); - float3 fixed0 = (-ACES_d * Color) + ACES_b; - float3 fixed1 = (ACES_c * Color) - modA; + T fixed0 = (-ACES_d * Color) + ACES_b; + T fixed1 = (ACES_c * Color) - modA; - float3 variable_numerator_part0 = -fixed0; - float3 variable_numerator = sqrt((variable_numerator_part0 * variable_numerator_part0) - (4.f * modE * Color * fixed1)); + T variable_numerator_part0 = -fixed0; + T variable_numerator = sqrt((variable_numerator_part0 * variable_numerator_part0) - (4.f * modE * Color * fixed1)); - float3 denominator = 2.f * fixed1; + T denominator = 2.f * fixed1; - float3 result0 = (fixed0 + variable_numerator) / denominator; - float3 result1 = (fixed0 - variable_numerator) / denominator; + T result0 = (fixed0 + variable_numerator) / denominator; + T result1 = (fixed0 - variable_numerator) / denominator; // "result1" is likely what we always want return max(result0, result1); } -float3 ACESReference_Inverse(float3 Color) +template +T ACESReference_Inverse(T Color) { return ACES_Inverse(Color, ACES_e, ACES_a); } -float3 ACESParametric_Inverse(float3 Color, float modE, float modA) +template +T ACESParametric_Inverse(T Color, float modE, float modA) { return ACES_Inverse(Color, modE, modA); } @@ -196,13 +203,14 @@ float3 Hable( { // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L202 - float toeLength = pow(saturate(PerSceneConstants[3266u].w), 2.2f); // 2.2 is probably based on perceptual gamma, but we don't know why it's applied here - float toeStrength = saturate(PerSceneConstants[3266u].z); - float shoulderLength = clamp(saturate(PerSceneConstants[3267u].y), EPSILON, BTHCNST); - float shoulderStrength = max(PerSceneConstants[3267u].x, 0.f); - float shoulderAngle = saturate(PerSceneConstants[3267u].z); + // 2.2 is probably based on perceptual gamma, but we don't know why it's applied here + const float toeLength = pow(saturate(PerSceneConstants[3266u].w), 2.2f); // Constant is usually 0.3 + const float toeStrength = saturate(PerSceneConstants[3266u].z); // Constant is usually 0.5 + const float shoulderLength = clamp(saturate(PerSceneConstants[3267u].y), EPSILON, BTHCNST); // Constant is usually 0.8 + const float shoulderStrength = max(PerSceneConstants[3267u].x, 0.f); // Constant is usually 9.9 + const float shoulderAngle = saturate(PerSceneConstants[3267u].z); // Constant is usually 0.3 #if ENABLE_TONEMAP_IMPROVEMENTS - + //TODO: fix highlights #endif //dstParams @@ -339,7 +347,7 @@ float HableEval_Inverse( float lnA, float B) { -#if 0 // This version might work better on Nvidia (to be investigated more before deleting the alternative branch) +#if 0 //TODO: delete. This version might work better on Nvidia (to be investigated more before deleting the alternative branch) float y0 = max((Channel - offsetY) / scaleY, EPSILON); float x0 = exp((log(y0) - lnA) / B); @@ -357,9 +365,84 @@ float HableEval_Inverse( #endif } +void Hable_Inverse_Channel( + inout float ColorChannel, + float params_y0, + float params_y1, + float dstParams_W, + float toeSegment_lnA, + float toeSegment_B, + float midSegment_offsetX, + float midSegment_lnA, + float shoulderSegment_offsetX, + float shoulderSegment_offsetY, + float shoulderSegment_lnA, + float shoulderSegment_B, + float invScale, + bool onlyInvertHighlights = false) +{ + // Scale all non highlights by the scale the smallest (first) highlight would have, so we keep the curves connected + const float minHighlightsColor = max(params_y0, params_y1); // Check both toe and mid params for extra safety + if (onlyInvertHighlights && ColorChannel < minHighlightsColor) + { + ColorChannel *= HableEval_Inverse(minHighlightsColor, + shoulderSegment_offsetX, + shoulderSegment_offsetY * invScale, + -1.f, + -1.f * invScale, + shoulderSegment_lnA, + shoulderSegment_B) / minHighlightsColor; + return; + } + + // scaleY and offsetY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L187-L197 + // toe + if (ColorChannel < params_y0) + { + // scaleXY and offsetXY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L151-L154 + ColorChannel = HableEval_Inverse(ColorChannel, + 0.f, + 0.f, // * invScale + 1.f, + 1.f * invScale, + toeSegment_lnA, + toeSegment_B); + } + // mid (linear segment) + else if (ColorChannel < params_y1) + { + // scaleXY and offsetY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L125-L127 + ColorChannel = HableEval_Inverse(ColorChannel, + -midSegment_offsetX, // minus was optimised away + 0.f, // * invScale + 1.f, + 1.f * invScale, + midSegment_lnA, + 1.f); + } + // shoulder + else + { +#if ENABLE_TONEMAP_IMPROVEMENTS && 0 //TODO: remove... temp workaround for broken shoulder + ColorChannel = min(ColorChannel, 0.995f); +#endif + + // scaleXY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L175-L176 + ColorChannel = HableEval_Inverse(ColorChannel, + shoulderSegment_offsetX, + shoulderSegment_offsetY * invScale, + -1.f, + -1.f * invScale, + shoulderSegment_lnA, + shoulderSegment_B); + } +} + // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L45-L52 -float3 Hable_Inverse( - float3 InputColor, +template +T Hable_Inverse( + T InputColor, + uint Channels /*1 to 3*/, float params_y0, float params_y1, float dstParams_W, @@ -371,57 +454,53 @@ float3 Hable_Inverse( float shoulderSegment_offsetY, float shoulderSegment_lnA, float shoulderSegment_B, - float invScale) + float invScale, + bool onlyInvertHighlights = false) { // There's no inverse formula for colors beyond the 0-1 range InputColor = saturate(InputColor); - float3 itmColor; + float3 itmColor = InputColor; // Make this float3 to make it compile for all cases - for (uint channel = 0; channel < 3; channel++) + if (Channels <= 1) // InputColor[channel] won't compile if T is float(1) { - InputColor[channel] = min(InputColor[channel], 0.995f); //TODO: remove... temp workaround for broken shoulder - - // scaleY and offsetY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L187-L197 - // toe - if (InputColor[channel] < params_y0) - { - // scaleXY and offsetXY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L151-L154 - itmColor[channel] = HableEval_Inverse(InputColor[channel], - 0.f, - 0.f, // * invScale - 1.f, - 1.f * invScale, - toeSegment_lnA, - toeSegment_B); - } - // mid - else if (InputColor[channel] < params_y1) - { - // scaleXY and offsetY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L125-L127 - itmColor[channel] = HableEval_Inverse(InputColor[channel], - -midSegment_offsetX, // minus was optimised away - 0.f, // * invScale - 1.f, - 1.f * invScale, - midSegment_lnA, - 1.f); - } - // shoulder - else + Hable_Inverse_Channel(itmColor.x, + params_y0, + params_y1, + dstParams_W, + toeSegment_lnA, + toeSegment_B, + midSegment_offsetX, + midSegment_lnA, + shoulderSegment_offsetX, + shoulderSegment_offsetY, + shoulderSegment_lnA, + shoulderSegment_B, + invScale, + onlyInvertHighlights); + } + else + { + for (uint channel = 0; channel < Channels; channel++) { - // scaleXY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L175-L176 - itmColor[channel] = HableEval_Inverse(InputColor[channel], - shoulderSegment_offsetX, - shoulderSegment_offsetY * invScale, - -1.f, - -1.f * invScale, - shoulderSegment_lnA, - shoulderSegment_B); + Hable_Inverse_Channel(itmColor[channel], + params_y0, + params_y1, + dstParams_W, + toeSegment_lnA, + toeSegment_B, + midSegment_offsetX, + midSegment_lnA, + shoulderSegment_offsetX, + shoulderSegment_offsetY, + shoulderSegment_lnA, + shoulderSegment_B, + invScale, + onlyInvertHighlights); } } - return itmColor * dstParams_W; + return (T)itmColor * dstParams_W; } // Tonemapper inspired from DICE. Works on luminance to maintain hue, not per channel. @@ -494,7 +573,7 @@ float3 PostProcess(float3 Color, inout float ColorLuminance, float MidGrayScale const float hableSaturation = HdrCmpDat[hdrCmpDatIndex].HableSaturation; const float brightnessMultiplier = HdrCmpDat[hdrCmpDatIndex].BrightnessMultiplier; // Neutral at 1 const float contrastIntensity = HdrCmpDat[hdrCmpDatIndex].ContrastIntensity; // Neutral at 1 - const float contrastMidPoint = PerSceneConstants[316u].z * MidGrayScale; + const float contrastMidPoint = PerSceneConstants[316u].z * MidGrayScale; // Game usually has this around 0.18 (mid gray) ColorLuminance = Luminance(Color); @@ -856,43 +935,69 @@ PSOutput PS(PSInput psInput) #endif // ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS - float paperWhite = HDR_GAME_PAPER_WHITE; + const float paperWhite = HDR_GAME_PAPER_WHITE; const float midGrayIn = MidGray; float midGrayOut = midGrayIn; #if ENABLE_AUTOHDR if (tonemapperIndex == 1 || tonemapperIndex == 2 || tonemapperIndex == 3) { - paperWhite = HDR_REFERENCE_PAPER_WHITE; //TODO: fix this up, this is because HDR_GAME_PAPER_WHITE is 1 atm and it's not good for AutoHDR tonemapperIndex = 0; color = PumboAutoHDR(color, HDR_MAX_OUTPUT_NITS, paperWhite); midGrayOut = PumboAutoHDR(midGrayIn.xxx, HDR_MAX_OUTPUT_NITS, paperWhite).x; - } #endif // ENABLE_AUTOHDR +#if INVERT_TONEMAP_BY_LUMINANCE + float preInverseTonemapColorLuminance = Luminance(color); + float inverseTonemapColor = preInverseTonemapColorLuminance; + const uint inverseTonemapColorComponents = 1; +#else + float3 inverseTonemapColor = color; + const uint inverseTonemapColorComponents = 3; +#endif + // Restore a color very close to the original linear one, but with all the other post process and LUT transformations baked in switch (tonemapperIndex) { + //TODO: implement INVERT_TONEMAP_HIGHLIGHTS_ONLY for ACES + case 1: { - color = ACESReference_Inverse(color); - midGrayOut = ACESReference_Inverse(midGrayIn.xxx).x; + inverseTonemapColor = ACESReference_Inverse(inverseTonemapColor); + midGrayOut = ACESReference_Inverse(midGrayIn); tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; postColorCorrectionLuminance = postColorCorrectionClampedLuminance; } break; case 2: { - color = ACESParametric_Inverse(color, acesParam_modE, acesParam_modA); - midGrayOut = ACESParametric_Inverse(midGrayIn.xxx, acesParam_modE, acesParam_modA).x; + inverseTonemapColor = ACESParametric_Inverse(inverseTonemapColor, acesParam_modE, acesParam_modA); + midGrayOut = ACESParametric_Inverse(midGrayIn, acesParam_modE, acesParam_modA); tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; postColorCorrectionLuminance = postColorCorrectionClampedLuminance; } break; case 3: { - color = Hable_Inverse(color, + const bool onlyInvertHighlights = (bool)INVERT_TONEMAP_HIGHLIGHTS_ONLY; + inverseTonemapColor = Hable_Inverse(inverseTonemapColor, + inverseTonemapColorComponents, + hable_params_y0, + hable_params_y1, + hable_dstParams_W, + hable_toeSegment_lnA, + hable_toeSegment_B, + hable_midSegment_offsetX, + hable_midSegment_lnA, + hable_shoulderSegment_offsetX, + hable_shoulderSegment_offsetY, + hable_shoulderSegment_lnA, + hable_shoulderSegment_B, + hable_invScale, + onlyInvertHighlights); + midGrayOut = Hable_Inverse(midGrayIn, + 1, hable_params_y0, hable_params_y1, hable_dstParams_W, @@ -904,21 +1009,8 @@ PSOutput PS(PSInput psInput) hable_shoulderSegment_offsetY, hable_shoulderSegment_lnA, hable_shoulderSegment_B, - hable_invScale); - //TODO: make function template - midGrayOut = Hable_Inverse(midGrayIn.xxx, - hable_params_y0, - hable_params_y1, - hable_dstParams_W, - hable_toeSegment_lnA, - hable_toeSegment_B, - hable_midSegment_offsetX, - hable_midSegment_lnA, - hable_shoulderSegment_offsetX, - hable_shoulderSegment_offsetY, - hable_shoulderSegment_lnA, - hable_shoulderSegment_B, - hable_invScale).x; + hable_invScale, + onlyInvertHighlights); tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; postColorCorrectionLuminance = postColorCorrectionClampedLuminance; } break; @@ -928,6 +1020,12 @@ PSOutput PS(PSInput psInput) break; } +#if INVERT_TONEMAP_BY_LUMINANCE + color *= preInverseTonemapColorLuminance > 0.f ? (inverseTonemapColor / preInverseTonemapColorLuminance) : 1.f; +#else + color = inverseTonemapColor; +#endif + const float midGrayScale = midGrayOut / midGrayIn; #if ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS @@ -946,7 +1044,8 @@ PSOutput PS(PSInput psInput) color *= FindLuminanceToRestoreScale(color, tonemapLostLuminance, preColorCorrectionLuminance, postColorCorrectionLuminance); #endif // ENABLE_LUMINANCE_RESTORE - color *= paperWhite; + // Bring back the color to the same range as SDR by dividing by the mid gray change. + color *= paperWhite / midGrayScale; #if !ENABLE_AUTOHDR diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index bc472338..84e8483b 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -6,10 +6,14 @@ #define ENABLE_HDR 1 // Set equal to the max nits your display can output #define HDR_MAX_OUTPUT_NITS 1000.f -// Brings the range roughly from 80 nits to 203 nits +// User configured variable +#define HDR_GAME_PAPER_WHITE_MULTIPLIER 1.f +// User configured variable +#define HDR_UI_PAPER_WHITE_MULTIPLIER 1.f +// Brings the range roughly from 80 nits to 203 nits (~2.5) #define HDR_REFERENCE_PAPER_WHITE (ReferenceWhiteNits_BT2408 / WhiteNits_BT709) -#define HDR_GAME_PAPER_WHITE 1.0f -#define HDR_UI_PAPER_WHITE HDR_REFERENCE_PAPER_WHITE +#define HDR_GAME_PAPER_WHITE HDR_REFERENCE_PAPER_WHITE * HDR_GAME_PAPER_WHITE_MULTIPLIER +#define HDR_UI_PAPER_WHITE HDR_REFERENCE_PAPER_WHITE * HDR_UI_PAPER_WHITE_MULTIPLIER // Makes LUTs sampling work in linear space, which is mathematically correct. Without this, they are stored as ~sRGB in a float texture and sampled in sRGB without acknowledging it. // This possibly shifts colors a lot, but it's correct, it's also necessary for HDR LUTs to work. From 26f9b6bc76f247f92e494f1fe93967ff42074139 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 30 Sep 2023 18:37:36 +0300 Subject: [PATCH 042/270] Move LUTs application to be after inverse tonemapping, this allows LUTs to have HDR/scRGB colors (beyond 0-1) --- .../ColorGradingMerge_FF81_cs.hlsl | 23 +++---- shaders/HDRComposite/HDRComposite_ps.hlsl | 69 ++++++++++++------- shaders/shared.hlsl | 6 +- 3 files changed, 61 insertions(+), 37 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 9ada7918..07321bf3 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -2,16 +2,13 @@ #include "../color.hlsl" #include "../math.hlsl" -// 0 None, 1 ShortFuse technique (normalization), 2 luminance preservation (doesn't look so good) -#define LUT_IMPROVEMENT_TYPE 1 #define FORCE_SDR_RANGE_LUTS 0 // Make some small quality cuts for the purpose of optimization #define OPTIMIZE_LUT_ANALYSIS true // For future development #define UNUSED_PARAMS 0 -static float additionalNeutralLUTPercentage = 0.0f; -static float LUTCorrectionPercentage = 1.0f; +static float additionalNeutralLUTPercentage = 0.f; // ~0.25 might be a good compromise cbuffer CPushConstantWrapper_ColorGradingMerge : register(b0, space0) { @@ -179,11 +176,11 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b } #endif -#if 1 // TODO: the above code introduces a lot of colors beyond the 0-1 range, which will then get clipped, causing a hue shift +#if 0 // TODO: the above code introduces a lot of colors beyond the 0-1 range, which will then get clipped, causing a hue shift. This fix up might not be necessary as later we raise blacks anyway. // Color may have gone negative // For example, if black is (3,3,3) and another value is (0,0,4), that // may result in (-3,-3,1) - color = clamp(color, 0.f, FLT_MAX); + color = max(color, 0.f); #endif static const float cubeNormalization = sqrt(3.f); // Normalize to 0-1 range @@ -226,10 +223,8 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b totalRange, 1.f / analysis.whiteY, 1.f); -#if 1 // This destroys luminance - // Scale by the maximum value this could ever have, so it's normalized and we are guaranteed target luminance doesn't go beyond 1. + // Scale by the maximum value this could ever have, so it's normalized and we are guaranteed target luminance doesn't go beyond 1. Note that this can literally destroy luminance. highlightsRaise *= SDRRange ? analysis.whiteY : 1.f; -#endif const float targetY = currentY * highlightsRaise * shadowsRaise; // TODO: why are we clamping to full white here? luminance 1 (or beyond) might not match a white color at all, should we instead try to conserve the hue? Though that's much harder as we'd need to analyze more LUT pixels. @@ -273,7 +268,7 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) float3 neutralLUTColor = float3(outUVW) / (LUT_SIZE - 1.f); // The neutral LUT is automatically generated by the coordinates, but it's baked with sRGB gamma neutralLUTColor = gamma_sRGB_to_linear(neutralLUTColor); -#if LUT_IMPROVEMENT_TYPE == 0 || LUT_IMPROVEMENT_TYPE == 2 +#if LUT_IMPROVEMENT_TYPE != 1 float3 LUT1Color = LUT1.Load(inUVW); float3 LUT2Color = LUT2.Load(inUVW); float3 LUT3Color = LUT3.Load(inUVW); @@ -283,9 +278,9 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) LUT3Color = gamma_sRGB_to_linear(LUT3Color); LUT4Color = gamma_sRGB_to_linear(LUT4Color); #endif // LUT_IMPROVEMENT_TYPE - -#if LUT_IMPROVEMENT_TYPE == 1 + const bool SDRRange = !((bool)ENABLE_HDR) || (bool)FORCE_SDR_RANGE_LUTS; +#if LUT_IMPROVEMENT_TYPE == 1 float3 LUT1Color = PatchLUTColor(LUT1, inUVW, neutralLUTColor, SDRRange); float3 LUT2Color = PatchLUTColor(LUT2, inUVW, neutralLUTColor, SDRRange); float3 LUT3Color = PatchLUTColor(LUT3, inUVW, neutralLUTColor, SDRRange); @@ -300,18 +295,22 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) if (LUT1Luminance != 0.f) { LUT1Color *= lerp(1.f, neutralLUTLuminance / LUT1Luminance, LUTCorrectionPercentage); + if (SDRRange) { LUT1Color = saturate(LUT1Color) }; } if (LUT2Luminance != 0.f) { LUT2Color *= lerp(1.f, neutralLUTLuminance / LUT2Luminance, LUTCorrectionPercentage); + if (SDRRange) { LUT2Color = saturate(LUT2Color) }; } if (LUT3Luminance != 0.f) { LUT3Color *= lerp(1.f, neutralLUTLuminance / LUT3Luminance, LUTCorrectionPercentage); + if (SDRRange) { LUT3Color = saturate(LUT3Color) }; } if (LUT4Luminance != 0.f) { LUT4Color *= lerp(1.f, neutralLUTLuminance / LUT4Luminance, LUTCorrectionPercentage); + if (SDRRange) { LUT4Color = saturate(LUT4Color) }; } #endif // LUT_IMPROVEMENT_TYPE diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 4a9f084e..27e47a9a 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -16,16 +16,18 @@ // This disables most other features (post process, LUTs, ...) #define ENABLE_TONEMAP 1 // Tweak the OG tonemappers to either look better in general, or simply be more compatible with HDR -#define ENABLE_TONEMAP_IMPROVEMENTS 0 +#define ENABLE_TONEMAP_IMPROVEMENTS 1 #define ENABLE_POST_PROCESS 1 // 0 original (weak, generates values beyond 0-1 which then get clipped), 1 improved (looks more natural, avoids values below 0, but will overshoot beyond 1 more often, and will raise blacks), 2 Sigmoidal (smoothest looking, but harder to match) #define POST_PROCESS_CONTRAST_TYPE 1 #define ENABLE_LUT 1 // LUTs are too low resolutions to resolve gradients smoothly if the LUT color suddenly changes between samples #define ENABLE_LUT_TETRAHEDRAL_INTERPOLATION 1 +// Applies LUTs after inverse tonemap, so we have HDR values in them (inv tonemappers only take 0-1 range) +#define ENABLE_APPLY_LUT_AFTER_INVERSE_TONEMAP 1 #define ENABLE_REPLACED_TONEMAP 1 -// Invert tonemap by luminance (conserves SDR hue) -#define INVERT_TONEMAP_BY_LUMINANCE 1 +// Invert tonemap by luminance (conserves SDR hue (and damped saturation look)) +#define INVERT_TONEMAP_BY_LUMINANCE 0 // Only invert highlights, which helps conserve the SDR filmic look (shadow crush) #define INVERT_TONEMAP_HIGHLIGHTS_ONLY 1 // Use AutoHDR as "inverse tonemapper" (maintains a look closer to the original) @@ -423,8 +425,8 @@ void Hable_Inverse_Channel( // shoulder else { -#if ENABLE_TONEMAP_IMPROVEMENTS && 0 //TODO: remove... temp workaround for broken shoulder - ColorChannel = min(ColorChannel, 0.995f); +#if ENABLE_TONEMAP_IMPROVEMENTS //TODO: remove... temp workaround for broken shoulder + ColorChannel = min(ColorChannel, 0.99999f); #endif // scaleXY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L175-L176 @@ -536,7 +538,7 @@ float3 PumboAutoHDR(float3 Color, float MaxOutputNits, float PaperWhite) return Color * (AutoHDRTotalRatio / SDRRatio); } -float FindLuminanceToRestoreScale(float3 color, float tonemapLostLuminance, float preColorCorrectionLuminance, float postColorCorrectionLuminance) +float FindLuminanceToRestoreScale(float3 color, float tonemapLostLuminance, float preColorCorrectionLuminance, float postColorCorrectionLuminance, float midGrayScale = 1.f) { // Try to restore any luminance that would have been lost during tone mapping (e.g. tonemapping, post process, LUTs, ... can all clip values). // @@ -552,12 +554,20 @@ float FindLuminanceToRestoreScale(float3 color, float tonemapLostLuminance, floa const float currentLum = Luminance(color); if (currentLum > 0.f) { - tonemapLostLuminance = isnan(tonemapLostLuminance) ? 0.f : tonemapLostLuminance; + // Change the "tonemapLostLuminance" from being in the tonemapped SDR (0-1) range to the untonemapped HDR image range (by scaling it by the mid gray change, which is the best we can do). + // This will also acknowledge any clipped luminance from the range beyond 0-1. + tonemapLostLuminance = isnan(tonemapLostLuminance) ? 0.f : (tonemapLostLuminance * midGrayScale); + +#if defined(APPLY_MERGED_COLOR_GRADING_LUT) && ENABLE_LUT && !ENABLE_APPLY_LUT_AFTER_INVERSE_TONEMAP && LUT_IMPROVEMENT_TYPE == 3 // Restore any luminance lost by the LUT + float scale = 1.f; + scale *= postColorCorrectionLuminance > 0.f ? lerp(1.f, preColorCorrectionLuminance / postColorCorrectionLuminance, LUTCorrectionPercentage) : 1.f; +#else const float luminanceCCInvChange = max(1.f - (preColorCorrectionLuminance - postColorCorrectionLuminance), 0.f); - //const float luminanceCCInvChange = clamp(postColorCorrectionLuminance / preColorCorrectionLuminance, 0.f, 2.f); //TODO: try this - - const float targetLum = max(currentLum + tonemapLostLuminance, 0.f); + //const float luminanceCCInvChange = clamp(postColorCorrectionLuminance / preColorCorrectionLuminance, 0.f, 2.f); //TODO: try this, especially if brightness goes crazy high float scale = luminanceCCInvChange; +#endif + + const float targetLum = max(currentLum + tonemapLostLuminance, 0.f); //TODO: review max() math scale *= targetLum / currentLum; return scale; } @@ -874,10 +884,10 @@ PSOutput PS(PSInput psInput) // This is the luminance lost by clamping SDR tonemappers to the 0-1 range, and again by the fact that LUTs only take 0-1 range input. float tonemapLostLuminance = Luminance(tonemappedUnclampedColor - tonemappedColor); - const float preColorCorrectionLuminance = Luminance(tonemappedColor); - float3 color = tonemappedColor; + const float preColorCorrectionLuminance = Luminance(color); + #if defined(APPLY_MERGED_COLOR_GRADING_LUT) && ENABLE_TONEMAP && ENABLE_LUT #if FIX_WRONG_SRGB_GAMMA_FORMULA @@ -895,11 +905,11 @@ PSOutput PS(PSInput psInput) #if ENABLE_LUT_TETRAHEDRAL_INTERPOLATION - float3 lutColor = TetrahedralInterpolation(Lut, LUTCoordinates); + float3 LUTColor = TetrahedralInterpolation(Lut, LUTCoordinates); #else - float3 lutColor = Lut.Sample(Sampler0, LUTCoordinates * (1.f - (1.f / LUT_SIZE)) + ((1.f / LUT_SIZE) / 2.f)); + float3 LUTColor = Lut.Sample(Sampler0, LUTCoordinates * (1.f - (1.f / LUT_SIZE)) + ((1.f / LUT_SIZE) / 2.f)); #endif // ENABLE_LUT_TETRAHEDRAL_INTERPOLATION @@ -907,13 +917,19 @@ PSOutput PS(PSInput psInput) // We always work in linear space so convert to it. // We never acknowledge the original wrong gamma function here (we don't really care). - lutColor = gamma_sRGB_to_linear(lutColor); + LUTColor = gamma_sRGB_to_linear(LUTColor); #endif // !LUT_FIX_GAMMA_MAPPING - const float LutMaskAlpha = saturate(LutMask.Sample(Sampler0, psInput.TEXCOORD).x + (1.f - LUTStrength)); + const float LUTMaskAlpha = saturate(LutMask.Sample(Sampler0, psInput.TEXCOORD).x + (1.f - LUTStrength)); - color = lerp(lutColor, color, LutMaskAlpha); + LUTColor = lerp(LUTColor, color, LUTMaskAlpha); + +#if ENABLE_APPLY_LUT_AFTER_INVERSE_TONEMAP && ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP + const float3 LUTColorShift = LUTColor / color; +#else + color = LUTColor; +#endif #endif // APPLY_MERGED_COLOR_GRADING_LUT @@ -955,7 +971,7 @@ PSOutput PS(PSInput psInput) #else float3 inverseTonemapColor = color; const uint inverseTonemapColorComponents = 3; -#endif +#endif // INVERT_TONEMAP_BY_LUMINANCE // Restore a color very close to the original linear one, but with all the other post process and LUT transformations baked in switch (tonemapperIndex) @@ -1035,18 +1051,23 @@ PSOutput PS(PSInput psInput) #endif // ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS #if ENABLE_LUMINANCE_RESTORE //TODO: this will go beyond the max output nits if "ENABLE_AUTOHDR" is true, we need to fix a way to make them work together (maybe re-tonemap always at the end?) - // Change the "tonemapLostLuminance" from being in the tonemapped SDR (0-1) range to the untonemapped HDR image range (by scaling it by the mid gray change, which is the best we can do). - // This will also acknowledge any clipped luminance from the range beyond 0-1. - tonemapLostLuminance *= midGrayScale; - // Restore any luminance beyond 1 that ended up clipped by HDR->SDR tonemappers and any subsequent image manipulation. // It's important to do these after the inverse tonemappers, as they can't handle values beyond 0-1. - color *= FindLuminanceToRestoreScale(color, tonemapLostLuminance, preColorCorrectionLuminance, postColorCorrectionLuminance); + color *= FindLuminanceToRestoreScale(color, tonemapLostLuminance, preColorCorrectionLuminance, postColorCorrectionLuminance, midGrayScale); #endif // ENABLE_LUMINANCE_RESTORE +#if defined(APPLY_MERGED_COLOR_GRADING_LUT) && ENABLE_LUT && ENABLE_APPLY_LUT_AFTER_INVERSE_TONEMAP + const float3 preLUTShiftedColor = color; + color *= LUTColorShift; +#if LUT_IMPROVEMENT_TYPE == 3 + const float postLUTShiftedColorLuminance = Luminance(color); + color *= postLUTShiftedColorLuminance > 0.f ? (Luminance(preLUTShiftedColor) / postLUTShiftedColorLuminance) : 1.f; +#endif // LUT_IMPROVEMENT_TYPE == 3 +#endif // ENABLE_APPLY_LUT_AFTER_INVERSE_TONEMAP + // Bring back the color to the same range as SDR by dividing by the mid gray change. color *= paperWhite / midGrayScale; - + #if !ENABLE_AUTOHDR //TODO: find a tonemapper that looks more like Hable? diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 84e8483b..0984bfe8 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -15,8 +15,12 @@ #define HDR_GAME_PAPER_WHITE HDR_REFERENCE_PAPER_WHITE * HDR_GAME_PAPER_WHITE_MULTIPLIER #define HDR_UI_PAPER_WHITE HDR_REFERENCE_PAPER_WHITE * HDR_UI_PAPER_WHITE_MULTIPLIER +// 0 None, 1 ShortFuse technique (normalization), 2 luminance preservation (doesn't look so good and it's kinda broken), 3 tonemapper LUT lost luminance restore (improved iteration of type 2) +#define LUT_IMPROVEMENT_TYPE 1 // Makes LUTs sampling work in linear space, which is mathematically correct. Without this, they are stored as ~sRGB in a float texture and sampled in sRGB without acknowledging it. // This possibly shifts colors a lot, but it's correct, it's also necessary for HDR LUTs to work. #define LUT_FIX_GAMMA_MAPPING 1 #define LUT_SIZE 16.f -#define LUT_SIZE_UINT (uint)LUT_SIZE \ No newline at end of file +#define LUT_SIZE_UINT (uint)LUT_SIZE + +static float LUTCorrectionPercentage = 1.f; \ No newline at end of file From d6cb4841a252d2ad0827987d4573ea4190bdc06a Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 30 Sep 2023 21:37:39 +0300 Subject: [PATCH 043/270] Small tonemapper and LUT fixes (from ShortFuse) --- .../ColorGradingMerge_FF81_cs.hlsl | 13 ++++++++----- shaders/HDRComposite/HDRComposite_ps.hlsl | 12 ++++++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 07321bf3..8e6ee893 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -2,7 +2,7 @@ #include "../color.hlsl" #include "../math.hlsl" -#define FORCE_SDR_RANGE_LUTS 0 +#define FORCE_SDR_LUTS 0 // Make some small quality cuts for the purpose of optimization #define OPTIMIZE_LUT_ANALYSIS true // For future development @@ -176,7 +176,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b } #endif -#if 0 // TODO: the above code introduces a lot of colors beyond the 0-1 range, which will then get clipped, causing a hue shift. This fix up might not be necessary as later we raise blacks anyway. +#if 1 // TODO: the above code introduces a lot of colors beyond the 0-1 range, which will then get clipped, causing a hue shift. This fix up might not be necessary as later we raise blacks anyway. // Color may have gone negative // For example, if black is (3,3,3) and another value is (0,0,4), that // may result in (-3,-3,1) @@ -191,6 +191,9 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b const float currentY = Luminance(color); // In case this was negative, the shadows raise pass should bring it back to the >= 0 range + // Magic number to decide by how much to scale the new shadow area + const float shadowCompensationPercentage = 1.f - analysis.blackY; + // Brightness multiplier from shadows: // Because the amount the black level was raised is proportional to // the harshness of a linear gradient, a compensation must be made @@ -204,7 +207,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b min(currentY, analysis.blackY), 0.f, analysis.blackY, - 1.f + analysis.blackY, // Shadow compensation percentage ramp factor + 1.f + shadowCompensationPercentage, 1.f); // TODO: Analyze grayscale for shadow raise. For example, if black shadow was raised // by 5%, then analyze the ramping from 5%-10% and apply that to @@ -235,7 +238,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b // Or, still target full white for (1,1,1), but scale others relative to whiteY // Or, don't adjust unless white point } - else + else if (currentY != 0.f) { color *= max(targetY, 0.f) / max(currentY, 0.f); // Clamp luminances to avoid a double negative creating a positive number } @@ -279,7 +282,7 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) LUT4Color = gamma_sRGB_to_linear(LUT4Color); #endif // LUT_IMPROVEMENT_TYPE - const bool SDRRange = !((bool)ENABLE_HDR) || (bool)FORCE_SDR_RANGE_LUTS; + const bool SDRRange = !((bool)ENABLE_HDR) || (bool)FORCE_SDR_LUTS; #if LUT_IMPROVEMENT_TYPE == 1 float3 LUT1Color = PatchLUTColor(LUT1, inUVW, neutralLUTColor, SDRRange); float3 LUT2Color = PatchLUTColor(LUT2, inUVW, neutralLUTColor, SDRRange); diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 27e47a9a..7f4be2e4 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -205,8 +205,7 @@ float3 Hable( { // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L202 - // 2.2 is probably based on perceptual gamma, but we don't know why it's applied here - const float toeLength = pow(saturate(PerSceneConstants[3266u].w), 2.2f); // Constant is usually 0.3 + const float toeLength = pow(saturate(PerSceneConstants[3266u].w), 2.2f); // Constant is usually 0.3. Pow 2.2 is just so that the user doesn't have to input tiny numbers (it's not gamma related). const float toeStrength = saturate(PerSceneConstants[3266u].z); // Constant is usually 0.5 const float shoulderLength = clamp(saturate(PerSceneConstants[3267u].y), EPSILON, BTHCNST); // Constant is usually 0.8 const float shoulderStrength = max(PerSceneConstants[3267u].x, 0.f); // Constant is usually 9.9 @@ -926,7 +925,12 @@ PSOutput PS(PSInput psInput) LUTColor = lerp(LUTColor, color, LUTMaskAlpha); #if ENABLE_APPLY_LUT_AFTER_INVERSE_TONEMAP && ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP - const float3 LUTColorShift = LUTColor / color; + float3 LUTColorShift = LUTColor / color; + for (uint channel = 0; channel < 3; channel++) + { + if (color[channel] == 0.f) + LUTColorShift[channel] = 1.f; + } #else color = LUTColor; #endif @@ -1061,7 +1065,7 @@ PSOutput PS(PSInput psInput) color *= LUTColorShift; #if LUT_IMPROVEMENT_TYPE == 3 const float postLUTShiftedColorLuminance = Luminance(color); - color *= postLUTShiftedColorLuminance > 0.f ? (Luminance(preLUTShiftedColor) / postLUTShiftedColorLuminance) : 1.f; + color *= postLUTShiftedColorLuminance > 0.f ? lerp(1.f, Luminance(preLUTShiftedColor) / postLUTShiftedColorLuminance, LUTCorrectionPercentage) : 1.f; #endif // LUT_IMPROVEMENT_TYPE == 3 #endif // ENABLE_APPLY_LUT_AFTER_INVERSE_TONEMAP From 287f21668b872837b12800bc5e3e8e9f923dfc70 Mon Sep 17 00:00:00 2001 From: Lilium Date: Sat, 30 Sep 2023 21:41:29 +0200 Subject: [PATCH 044/270] small cleanup + describe the pow(x, 2.2) for the toeLength parameter --- .../ColorGradingMerge_FF81_cs.hlsl | 16 +- shaders/HDRComposite/HDRComposite_ps.hlsl | 179 +++++++++--------- shaders/shared.hlsl | 2 +- 3 files changed, 99 insertions(+), 98 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 8e6ee893..ea7ebe20 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -164,7 +164,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b // (1,1,1) must remain unchanged // Scaling is strongest at 0, and weakest at 1 (none) color = (1.f - ((1.f - color) * reduceFactor)) * increaseFactor; - + #if 0 // "Invalid" range analysis if (any(color > 1)) { @@ -175,14 +175,14 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b return 1; } #endif - + #if 1 // TODO: the above code introduces a lot of colors beyond the 0-1 range, which will then get clipped, causing a hue shift. This fix up might not be necessary as later we raise blacks anyway. // Color may have gone negative // For example, if black is (3,3,3) and another value is (0,0,4), that // may result in (-3,-3,1) color = max(color, 0.f); #endif - + static const float cubeNormalization = sqrt(3.f); // Normalize to 0-1 range // How distant are our LUT coordinates from black or white in 3D cube space? const float blackDistance = hypot3(neutralLUTColor) / cubeNormalization; @@ -190,10 +190,10 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b const float totalRange = blackDistance + whiteDistance; const float currentY = Luminance(color); // In case this was negative, the shadows raise pass should bring it back to the >= 0 range - + // Magic number to decide by how much to scale the new shadow area const float shadowCompensationPercentage = 1.f - analysis.blackY; - + // Brightness multiplier from shadows: // Because the amount the black level was raised is proportional to // the harshness of a linear gradient, a compensation must be made @@ -212,10 +212,10 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b // TODO: Analyze grayscale for shadow raise. For example, if black shadow was raised // by 5%, then analyze the ramping from 5%-10% and apply that to // the new 0% - 5% raise. - + // TODO: apply both shadow and highlight raise to the whole image range, instead of a portion of it, to make its gradient smoother? Or maybe apply it in perceptual (gamma) space. // The functions could also be simplified a lot. - + // Brightness multiplier from highlights: // Boost luminance of all texels relative to their distance to white // and how much white is to be raised. @@ -281,7 +281,7 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) LUT3Color = gamma_sRGB_to_linear(LUT3Color); LUT4Color = gamma_sRGB_to_linear(LUT4Color); #endif // LUT_IMPROVEMENT_TYPE - + const bool SDRRange = !((bool)ENABLE_HDR) || (bool)FORCE_SDR_LUTS; #if LUT_IMPROVEMENT_TYPE == 1 float3 LUT1Color = PatchLUTColor(LUT1, inUVW, neutralLUTColor, SDRRange); diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 7f4be2e4..b12ad0c0 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -205,7 +205,8 @@ float3 Hable( { // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L202 - const float toeLength = pow(saturate(PerSceneConstants[3266u].w), 2.2f); // Constant is usually 0.3. Pow 2.2 is just so that the user doesn't have to input tiny numbers (it's not gamma related). + // the 2.2 is so you don't have to input very small numbers + const float toeLength = pow(saturate(PerSceneConstants[3266u].w), 2.2f); // Constant is usually 0.3 const float toeStrength = saturate(PerSceneConstants[3266u].z); // Constant is usually 0.5 const float shoulderLength = clamp(saturate(PerSceneConstants[3267u].y), EPSILON, BTHCNST); // Constant is usually 0.8 const float shoulderStrength = max(PerSceneConstants[3267u].x, 0.f); // Constant is usually 9.9 @@ -260,7 +261,7 @@ float3 Hable( // this just returns m const float toeM = m; const float shoulderM = m; - + // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L137-L138 // max(EPSILON, pow(params_yX, gamma)) params_y0 = max(EPSILON, dstParams_y0); // is pow(x, gamma) with gamma = 1 @@ -368,58 +369,58 @@ float HableEval_Inverse( void Hable_Inverse_Channel( inout float ColorChannel, - float params_y0, - float params_y1, - float dstParams_W, - float toeSegment_lnA, - float toeSegment_B, - float midSegment_offsetX, - float midSegment_lnA, - float shoulderSegment_offsetX, - float shoulderSegment_offsetY, - float shoulderSegment_lnA, - float shoulderSegment_B, - float invScale, - bool onlyInvertHighlights = false) + float params_y0, + float params_y1, + float dstParams_W, + float toeSegment_lnA, + float toeSegment_B, + float midSegment_offsetX, + float midSegment_lnA, + float shoulderSegment_offsetX, + float shoulderSegment_offsetY, + float shoulderSegment_lnA, + float shoulderSegment_B, + float invScale, + bool onlyInvertHighlights = false) { // Scale all non highlights by the scale the smallest (first) highlight would have, so we keep the curves connected const float minHighlightsColor = max(params_y0, params_y1); // Check both toe and mid params for extra safety if (onlyInvertHighlights && ColorChannel < minHighlightsColor) { ColorChannel *= HableEval_Inverse(minHighlightsColor, - shoulderSegment_offsetX, - shoulderSegment_offsetY * invScale, - -1.f, - -1.f * invScale, - shoulderSegment_lnA, - shoulderSegment_B) / minHighlightsColor; + shoulderSegment_offsetX, + shoulderSegment_offsetY * invScale, + -1.f, + -1.f * invScale, + shoulderSegment_lnA, + shoulderSegment_B) / minHighlightsColor; return; } - + // scaleY and offsetY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L187-L197 // toe if (ColorChannel < params_y0) { // scaleXY and offsetXY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L151-L154 ColorChannel = HableEval_Inverse(ColorChannel, - 0.f, - 0.f, // * invScale - 1.f, - 1.f * invScale, - toeSegment_lnA, - toeSegment_B); + 0.f, + 0.f, // * invScale + 1.f, + 1.f * invScale, + toeSegment_lnA, + toeSegment_B); } // mid (linear segment) else if (ColorChannel < params_y1) { // scaleXY and offsetY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L125-L127 ColorChannel = HableEval_Inverse(ColorChannel, - -midSegment_offsetX, // minus was optimised away - 0.f, // * invScale - 1.f, - 1.f * invScale, - midSegment_lnA, - 1.f); + -midSegment_offsetX, // minus was optimised away + 0.f, // * invScale + 1.f, + 1.f * invScale, + midSegment_lnA, + 1.f); } // shoulder else @@ -427,23 +428,23 @@ void Hable_Inverse_Channel( #if ENABLE_TONEMAP_IMPROVEMENTS //TODO: remove... temp workaround for broken shoulder ColorChannel = min(ColorChannel, 0.99999f); #endif - + // scaleXY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L175-L176 ColorChannel = HableEval_Inverse(ColorChannel, - shoulderSegment_offsetX, - shoulderSegment_offsetY * invScale, - -1.f, - -1.f * invScale, - shoulderSegment_lnA, - shoulderSegment_B); + shoulderSegment_offsetX, + shoulderSegment_offsetY * invScale, + -1.f, + -1.f * invScale, + shoulderSegment_lnA, + shoulderSegment_B); } } // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L45-L52 template T Hable_Inverse( - T InputColor, - uint Channels /*1 to 3*/, + T InputColor, + uint Channels /*1 to 3*/, //there has to be better solution than this float params_y0, float params_y1, float dstParams_W, @@ -460,7 +461,7 @@ T Hable_Inverse( { // There's no inverse formula for colors beyond the 0-1 range InputColor = saturate(InputColor); - + float3 itmColor = InputColor; // Make this float3 to make it compile for all cases if (Channels <= 1) // InputColor[channel] won't compile if T is float(1) @@ -526,7 +527,7 @@ float3 DICETonemap(float3 Color, float MaxOutputLuminance) float3 PumboAutoHDR(float3 Color, float MaxOutputNits, float PaperWhite) { static const float AutoHDRShoulderPow = 3.5f; //TODO: exposure to user? - + const float SDRRatio = Luminance(Color); // Limit AutoHDR brightness, it won't look good beyond a certain level. // The paper white multiplier is applied later so we account for that. @@ -565,7 +566,7 @@ float FindLuminanceToRestoreScale(float3 color, float tonemapLostLuminance, floa //const float luminanceCCInvChange = clamp(postColorCorrectionLuminance / preColorCorrectionLuminance, 0.f, 2.f); //TODO: try this, especially if brightness goes crazy high float scale = luminanceCCInvChange; #endif - + const float targetLum = max(currentLum + tonemapLostLuminance, 0.f); //TODO: review max() math scale *= targetLum / currentLum; return scale; @@ -594,15 +595,15 @@ float3 PostProcess(float3 Color, inout float ColorLuminance, float MidGrayScale Color *= brightnessMultiplier; #if POST_PROCESS_CONTRAST_TYPE == 0 - + // Contrast adjustment (shift the colors from 0<->1 to (e.g.) -0.5<->0.5 range, multiply and shift back). // The higher the distance from the contrast middle point, the more contrast will change the color. Color = ((Color - contrastMidPoint) * contrastIntensity) + contrastMidPoint; - + #elif POST_PROCESS_CONTRAST_TYPE == 1 - + Color = pow(Color / contrastMidPoint, contrastIntensity) * contrastMidPoint; - + #elif POST_PROCESS_CONTRAST_TYPE == 2 // sigmoidal contrast adjustment doesn't clip colors @@ -824,7 +825,7 @@ PSOutput PS(PSInput psInput) float hable_invScale; const bool clampACES = false; - + int tonemapperIndex = ForceTonemapper > 0 ? ForceTonemapper : PcwHdrComposite.Tmo; switch (tonemapperIndex) @@ -954,9 +955,9 @@ PSOutput PS(PSInput psInput) color = PostProcess_Inverse(color, prePostProcessColorLuminance); #endif // ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS - + const float paperWhite = HDR_GAME_PAPER_WHITE; - + const float midGrayIn = MidGray; float midGrayOut = midGrayIn; #if ENABLE_AUTOHDR @@ -967,7 +968,7 @@ PSOutput PS(PSInput psInput) midGrayOut = PumboAutoHDR(midGrayIn.xxx, HDR_MAX_OUTPUT_NITS, paperWhite).x; } #endif // ENABLE_AUTOHDR - + #if INVERT_TONEMAP_BY_LUMINANCE float preInverseTonemapColorLuminance = Luminance(color); float inverseTonemapColor = preInverseTonemapColorLuminance; @@ -976,12 +977,12 @@ PSOutput PS(PSInput psInput) float3 inverseTonemapColor = color; const uint inverseTonemapColorComponents = 3; #endif // INVERT_TONEMAP_BY_LUMINANCE - + // Restore a color very close to the original linear one, but with all the other post process and LUT transformations baked in switch (tonemapperIndex) { //TODO: implement INVERT_TONEMAP_HIGHLIGHTS_ONLY for ACES - + case 1: { inverseTonemapColor = ACESReference_Inverse(inverseTonemapColor); @@ -1002,35 +1003,35 @@ PSOutput PS(PSInput psInput) { const bool onlyInvertHighlights = (bool)INVERT_TONEMAP_HIGHLIGHTS_ONLY; inverseTonemapColor = Hable_Inverse(inverseTonemapColor, - inverseTonemapColorComponents, - hable_params_y0, - hable_params_y1, - hable_dstParams_W, - hable_toeSegment_lnA, - hable_toeSegment_B, - hable_midSegment_offsetX, - hable_midSegment_lnA, - hable_shoulderSegment_offsetX, - hable_shoulderSegment_offsetY, - hable_shoulderSegment_lnA, - hable_shoulderSegment_B, - hable_invScale, - onlyInvertHighlights); + inverseTonemapColorComponents, + hable_params_y0, + hable_params_y1, + hable_dstParams_W, + hable_toeSegment_lnA, + hable_toeSegment_B, + hable_midSegment_offsetX, + hable_midSegment_lnA, + hable_shoulderSegment_offsetX, + hable_shoulderSegment_offsetY, + hable_shoulderSegment_lnA, + hable_shoulderSegment_B, + hable_invScale, + onlyInvertHighlights); midGrayOut = Hable_Inverse(midGrayIn, - 1, - hable_params_y0, - hable_params_y1, - hable_dstParams_W, - hable_toeSegment_lnA, - hable_toeSegment_B, - hable_midSegment_offsetX, - hable_midSegment_lnA, - hable_shoulderSegment_offsetX, - hable_shoulderSegment_offsetY, - hable_shoulderSegment_lnA, - hable_shoulderSegment_B, - hable_invScale, - onlyInvertHighlights); + 1, + hable_params_y0, + hable_params_y1, + hable_dstParams_W, + hable_toeSegment_lnA, + hable_toeSegment_B, + hable_midSegment_offsetX, + hable_midSegment_lnA, + hable_shoulderSegment_offsetX, + hable_shoulderSegment_offsetY, + hable_shoulderSegment_lnA, + hable_shoulderSegment_B, + hable_invScale, + onlyInvertHighlights); tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; postColorCorrectionLuminance = postColorCorrectionClampedLuminance; } break; @@ -1039,15 +1040,15 @@ PSOutput PS(PSInput psInput) // Any luminance lost by this tonemap case would have already been restored above with "tonemapLostLuminance". break; } - + #if INVERT_TONEMAP_BY_LUMINANCE color *= preInverseTonemapColorLuminance > 0.f ? (inverseTonemapColor / preInverseTonemapColorLuminance) : 1.f; #else color = inverseTonemapColor; #endif - + const float midGrayScale = midGrayOut / midGrayIn; - + #if ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS color = PostProcess(color, prePostProcessColorLuminance, midGrayScale); @@ -1073,11 +1074,11 @@ PSOutput PS(PSInput psInput) color *= paperWhite / midGrayScale; #if !ENABLE_AUTOHDR - + //TODO: find a tonemapper that looks more like Hable? const float maxOutputLuminance = HDR_MAX_OUTPUT_NITS / WhiteNits_BT709; color = DICETonemap(color, maxOutputLuminance); - + #endif // !ENABLE_AUTOHDR #else // ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 0984bfe8..525f216d 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -23,4 +23,4 @@ #define LUT_SIZE 16.f #define LUT_SIZE_UINT (uint)LUT_SIZE -static float LUTCorrectionPercentage = 1.f; \ No newline at end of file +static float LUTCorrectionPercentage = 1.f; From 0d3b403f75ab89807e015c972801f8bd170ca7f8 Mon Sep 17 00:00:00 2001 From: Lilium Date: Sat, 30 Sep 2023 23:42:47 +0200 Subject: [PATCH 045/270] add struct for hable parameters which are shared between tone mapping and inverse tone mapping --- shaders/HDRComposite/HDRComposite_ps.hlsl | 276 +++++++--------------- shaders/structs.hlsl | 43 ++++ 2 files changed, 127 insertions(+), 192 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index b12ad0c0..0a1b2a47 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -116,17 +116,6 @@ static const float ACES_d = 0.59f; static const float ACES_e = 0.14f; -static const float3x3 Bt709ToXYZ = float3x3( - 0.412135323426798, 0.357675002654190, 0.180356796374193, - 0.212507276141942, 0.715350005308380, 0.0721427185496773, - 0.0193188432856311, 0.119225000884730, 0.949879127570751); - -static const float3x3 XYZToBt709 = float3x3( - 3.24297896532120, -1.53833617585749, -0.498919840818647, - -0.968997952917093, 1.87549198225861, 0.0415445240532242, - 0.0556683243682128, -0.204117189350113, 1.05769816299604); - - float3 ACES(float3 Color, bool Clamp, float modE, float modA) { Color = (Color * (modA * Color + ACES_b)) / ((Color * (ACES_c * Color + ACES_d)) + modE); @@ -189,19 +178,8 @@ T ACESParametric_Inverse(T Color, float modE, float modA) // http://filmicworlds.com/blog/filmic-tonemapping-with-piecewise-power-curves/ // This is NOT the Uncharted 2 tonemapper, which was also by Hable. float3 Hable( - in float3 InputColor, - out float params_y0, - out float params_y1, - out float dstParams_W, - out float toeSegment_lnA, - out float toeSegment_B, - out float midSegment_offsetX, - out float midSegment_lnA, - out float shoulderSegment_offsetX, - out float shoulderSegment_offsetY, - out float shoulderSegment_lnA, - out float shoulderSegment_B, - out float invScale) + in float3 InputColor, + out HableParamters hableParams) { // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L202 @@ -211,6 +189,7 @@ float3 Hable( const float shoulderLength = clamp(saturate(PerSceneConstants[3267u].y), EPSILON, BTHCNST); // Constant is usually 0.8 const float shoulderStrength = max(PerSceneConstants[3267u].x, 0.f); // Constant is usually 9.9 const float shoulderAngle = saturate(PerSceneConstants[3267u].z); // Constant is usually 0.3 + #if ENABLE_TONEMAP_IMPROVEMENTS //TODO: fix highlights #endif @@ -228,7 +207,7 @@ float3 Hable( float extraW = exp2(shoulderStrength) - LFLT1; float initialW = dstParams_x0 + remainingY; - dstParams_W = initialW + extraW; + hableParams.dstParams.W = initialW + extraW; // "W * " was optimised away by DXIL->SPIRV->HLSL conversion as down the line there is a "/ W" float dstParams_overshootX = 2.f * shoulderAngle * shoulderStrength; @@ -237,13 +216,14 @@ float3 Hable( // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L87 //dstCurve - float dstCurve_invW = 1.f / dstParams_W; + float dstCurve_invW = 1.f / hableParams.dstParams.W; + //params - float params_x0 = dstParams_x0 / dstParams_W; - float params_x1 = dstParams_x1 / dstParams_W; - //float params_overshootX = dstParams_overshootX / dstParams_W; // this step was optimised away - float params_overshootX = dstParams_overshootX; - float dx = y1_offset / dstParams_W; + float params_x0 = dstParams_x0 / hableParams.dstParams.W; + float params_x1 = dstParams_x1 / hableParams.dstParams.W; + //float params_overshootX = dstParams_overshootX / hableParams.dstParams.W; // this step was optimised away + const float params_overshootX = dstParams_overshootX; + float dx = y1_offset / hableParams.dstParams.W; // mid section // AsSlopeIntercept https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L67 @@ -251,9 +231,9 @@ float3 Hable( m += EPSILON; float b = dstParams_y0 - (m * params_x0); - midSegment_offsetX = (b / m); //no minus + hableParams.midSegment.offsetX = (b / m); //no minus float midSegment_lnA_optimised = log2(m); - midSegment_lnA = RCP_LOG2_E * midSegment_lnA_optimised; + hableParams.midSegment.lnA = RCP_LOG2_E * midSegment_lnA_optimised; // mid section end // toeM and shoulderM are missing because EvalDerivativeLinearGamma (https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L81-L85) @@ -264,33 +244,33 @@ float3 Hable( // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L137-L138 // max(EPSILON, pow(params_yX, gamma)) - params_y0 = max(EPSILON, dstParams_y0); // is pow(x, gamma) with gamma = 1 - //OLD: params_y0 = max(EPSILON, exp2(log2(dstParams_y0))); - params_y1 = max(EPSILON, dstParams_y1); // is pow(x, gamma) with gamma = 1 - //OLD: params_y1 = max(EPSILON, exp2(log2(dstParams_y1))); + hableParams.params.y0 = max(EPSILON, dstParams_y0); // is pow(x, gamma) with gamma = 1 + //OLD: hableParams.params.y0 = max(EPSILON, exp2(log2(dstParams_y0))); + hableParams.params.y1 = max(EPSILON, dstParams_y1); // is pow(x, gamma) with gamma = 1 + //OLD: hableParams.params.y1 = max(EPSILON, exp2(log2(dstParams_y1))); // pow(1.f + dstParams_overshootY, gamma) - 1.f - // -1 was optimised away as shoulderSegment_offsetY is "params_overshootY + 1" + // -1 was optimised away as hableParams.shoulderSegment.offsetY is "params_overshootY + 1" float params_overshootY = 1.f + dstParams_overshootY; // is pow(x, gamma) with gamma = 1 //OLD: float params_overshootY = exp2(log2(1.f + dstParams_overshootY)); // toe section // SolveAB https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L60 - toeSegment_B = (toeM * params_x0) / (params_y0 + EPSILON); - float _410 = log2(params_y0); //doesn't belong to SolveAB (it might does) - float toeSegment_lnA_optimised = -toeSegment_B * log(params_x0); - toeSegment_lnA = _410 * RCP_LOG2_E + toeSegment_lnA_optimised; + hableParams.toeSegment.B = (toeM * params_x0) / (hableParams.params.y0 + EPSILON); + float _410 = log2(hableParams.params.y0); //doesn't belong to SolveAB (it might does) + float toeSegment_lnA_optimised = -hableParams.toeSegment.B * log(params_x0); + hableParams.toeSegment.lnA = _410 * RCP_LOG2_E + toeSegment_lnA_optimised; // toe section end // shoulder section float shoulderSection_x0 = 1.f + params_overshootX - params_x1; - float shoulderSection_y0 = params_overshootY - params_y1; // 1 + x was optimised away - shoulderSegment_offsetX = 1.f + params_overshootX; - shoulderSegment_offsetY = params_overshootY; // x + 1 was optimised away + float shoulderSection_y0 = params_overshootY - hableParams.params.y1; // 1 + x was optimised away + hableParams.shoulderSegment.offsetX = 1.f + params_overshootX; + hableParams.shoulderSegment.offsetY = params_overshootY; // x + 1 was optimised away // SolveAB - shoulderSegment_B = ((shoulderM * shoulderSection_x0) / (shoulderSection_y0 + EPSILON)); - float shoulderSegment_B_optimised = shoulderSegment_B * RCP_LOG2_E; - shoulderSegment_lnA = (log2(shoulderSection_y0) * RCP_LOG2_E) - (shoulderSegment_B_optimised * log2(shoulderSection_x0)); + hableParams.shoulderSegment.B = ((shoulderM * shoulderSection_x0) / (shoulderSection_y0 + EPSILON)); + float shoulderSegment_B_optimised = hableParams.shoulderSegment.B * RCP_LOG2_E; + hableParams.shoulderSegment.lnA = (log2(shoulderSection_y0) * RCP_LOG2_E) - (shoulderSegment_B_optimised * log2(shoulderSection_x0)); // shoulder section end // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L6 @@ -298,10 +278,10 @@ float3 Hable( float evalY0 = 0.f; if (params_overshootX > 0.f) { - evalY0 = -exp2(((shoulderSegment_B_optimised * log2(params_overshootX)) + shoulderSegment_lnA) * LOG2_E) + params_overshootY; + evalY0 = -exp2(((shoulderSegment_B_optimised * log2(params_overshootX)) + hableParams.shoulderSegment.lnA) * LOG2_E) + params_overshootY; } // Eval end - invScale = 1.f / evalY0; + hableParams.invScale = 1.f / evalY0; float3 toneMapped; @@ -313,11 +293,11 @@ float3 Hable( if (isToeSegment && normX > 0.f) { - returnChannel = exp2(((((log2(normX) * toeSegment_B) + _410) * RCP_LOG2_E) + toeSegment_lnA_optimised) * LOG2_E); + returnChannel = exp2(((((log2(normX) * hableParams.toeSegment.B) + _410) * RCP_LOG2_E) + toeSegment_lnA_optimised) * LOG2_E); } else if (normX < params_x1) { - float evalMidSegment_y0 = normX + midSegment_offsetX; // was -(-midSegment_offsetX) + float evalMidSegment_y0 = normX + hableParams.midSegment.offsetX; // was -(-hableParams.midSegment.offsetX) if (evalMidSegment_y0 > 0.f) { returnChannel = exp2(log2(evalMidSegment_y0) + midSegment_lnA_optimised); @@ -330,11 +310,11 @@ float3 Hable( float evalShoulderReturn = 0.f; if (evalShoulderSegment_y0 > 0.f) { - evalShoulderReturn = exp2(((shoulderSegment_B_optimised * log2(evalShoulderSegment_y0)) + shoulderSegment_lnA) * LOG2_E); + evalShoulderReturn = exp2(((shoulderSegment_B_optimised * log2(evalShoulderSegment_y0)) + hableParams.shoulderSegment.lnA) * LOG2_E); } returnChannel = params_overshootY - evalShoulderReturn; } - toneMapped[channel] = returnChannel * invScale; + toneMapped[channel] = returnChannel * hableParams.invScale; } return toneMapped; } @@ -368,58 +348,47 @@ float HableEval_Inverse( } void Hable_Inverse_Channel( - inout float ColorChannel, - float params_y0, - float params_y1, - float dstParams_W, - float toeSegment_lnA, - float toeSegment_B, - float midSegment_offsetX, - float midSegment_lnA, - float shoulderSegment_offsetX, - float shoulderSegment_offsetY, - float shoulderSegment_lnA, - float shoulderSegment_B, - float invScale, - bool onlyInvertHighlights = false) + inout float ColorChannel, + in HableParamters hableParams, + in bool onlyInvertHighlights = false) { // Scale all non highlights by the scale the smallest (first) highlight would have, so we keep the curves connected - const float minHighlightsColor = max(params_y0, params_y1); // Check both toe and mid params for extra safety + const float minHighlightsColor = max(hableParams.params.y0, hableParams.params.y1); // Check both toe and mid params for extra safety if (onlyInvertHighlights && ColorChannel < minHighlightsColor) { ColorChannel *= HableEval_Inverse(minHighlightsColor, - shoulderSegment_offsetX, - shoulderSegment_offsetY * invScale, + hableParams.shoulderSegment.offsetX, + hableParams.shoulderSegment.offsetY * hableParams.invScale, -1.f, - -1.f * invScale, - shoulderSegment_lnA, - shoulderSegment_B) / minHighlightsColor; + -1.f * hableParams.invScale, + hableParams.shoulderSegment.lnA, + hableParams.shoulderSegment.B) / minHighlightsColor; return; } // scaleY and offsetY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L187-L197 // toe - if (ColorChannel < params_y0) + if (ColorChannel < hableParams.params.y0) { // scaleXY and offsetXY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L151-L154 ColorChannel = HableEval_Inverse(ColorChannel, 0.f, - 0.f, // * invScale + 0.f, // * hableParams.invScale 1.f, - 1.f * invScale, - toeSegment_lnA, - toeSegment_B); + 1.f * hableParams.invScale, + hableParams.toeSegment.lnA, + hableParams.toeSegment.B); } // mid (linear segment) - else if (ColorChannel < params_y1) + else if (ColorChannel < hableParams.params.y1) { // scaleXY and offsetY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L125-L127 ColorChannel = HableEval_Inverse(ColorChannel, - -midSegment_offsetX, // minus was optimised away - 0.f, // * invScale + -hableParams.midSegment.offsetX, // minus was optimised away + 0.f, // * hableParams.invScale 1.f, - 1.f * invScale, - midSegment_lnA, + 1.f * hableParams.invScale, + hableParams.midSegment.lnA, 1.f); } // shoulder @@ -431,33 +400,22 @@ void Hable_Inverse_Channel( // scaleXY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L175-L176 ColorChannel = HableEval_Inverse(ColorChannel, - shoulderSegment_offsetX, - shoulderSegment_offsetY * invScale, + hableParams.shoulderSegment.offsetX, + hableParams.shoulderSegment.offsetY * hableParams.invScale, -1.f, - -1.f * invScale, - shoulderSegment_lnA, - shoulderSegment_B); + -1.f * hableParams.invScale, + hableParams.shoulderSegment.lnA, + hableParams.shoulderSegment.B); } } // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L45-L52 template T Hable_Inverse( - T InputColor, - uint Channels /*1 to 3*/, //there has to be better solution than this - float params_y0, - float params_y1, - float dstParams_W, - float toeSegment_lnA, - float toeSegment_B, - float midSegment_offsetX, - float midSegment_lnA, - float shoulderSegment_offsetX, - float shoulderSegment_offsetY, - float shoulderSegment_lnA, - float shoulderSegment_B, - float invScale, - bool onlyInvertHighlights = false) + T InputColor, + uint Channels /*1 to 3*/, //there has to be better solution than this + HableParamters hableParams, + bool onlyInvertHighlights = false) { // There's no inverse formula for colors beyond the 0-1 range InputColor = saturate(InputColor); @@ -467,18 +425,7 @@ T Hable_Inverse( if (Channels <= 1) // InputColor[channel] won't compile if T is float(1) { Hable_Inverse_Channel(itmColor.x, - params_y0, - params_y1, - dstParams_W, - toeSegment_lnA, - toeSegment_B, - midSegment_offsetX, - midSegment_lnA, - shoulderSegment_offsetX, - shoulderSegment_offsetY, - shoulderSegment_lnA, - shoulderSegment_B, - invScale, + hableParams, onlyInvertHighlights); } else @@ -486,23 +433,12 @@ T Hable_Inverse( for (uint channel = 0; channel < Channels; channel++) { Hable_Inverse_Channel(itmColor[channel], - params_y0, - params_y1, - dstParams_W, - toeSegment_lnA, - toeSegment_B, - midSegment_offsetX, - midSegment_lnA, - shoulderSegment_offsetX, - shoulderSegment_offsetY, - shoulderSegment_lnA, - shoulderSegment_B, - invScale, + hableParams, onlyInvertHighlights); } } - return (T)itmColor * dstParams_W; + return (T)itmColor * hableParams.dstParams.W; } // Tonemapper inspired from DICE. Works on luminance to maintain hue, not per channel. @@ -535,6 +471,7 @@ float3 PumboAutoHDR(float3 Color, float MaxOutputNits, float PaperWhite) const float AutoHDRShoulderRatio = 1.f - max(1.f - SDRRatio, 0.f); const float AutoHDRExtraRatio = pow(AutoHDRShoulderRatio, AutoHDRShoulderPow) * (AutoHDRMaxWhite - 1.f); const float AutoHDRTotalRatio = SDRRatio + AutoHDRExtraRatio; + return Color * (AutoHDRTotalRatio / SDRRatio); } @@ -577,13 +514,13 @@ float FindLuminanceToRestoreScale(float3 color, float tonemapLostLuminance, floa // "MidGrayScale" is how much the mid gray shifted from the originally intended tonemapped input (e.g. if we run this function on the untonemapped image, we can remap the mid gray) float3 PostProcess(float3 Color, inout float ColorLuminance, float MidGrayScale = 1.f) { - const uint hdrCmpDatIndex = PcwHdrComposite.HdrCmpDatIndex; + const uint hdrCmpDatIndex = PcwHdrComposite.HdrCmpDatIndex; const float4 highlightsColorFilter = HdrCmpDat[hdrCmpDatIndex].HighlightsColorFilter; - const float4 colorFilter = HdrCmpDat[hdrCmpDatIndex].ColorFilter; - const float hableSaturation = HdrCmpDat[hdrCmpDatIndex].HableSaturation; - const float brightnessMultiplier = HdrCmpDat[hdrCmpDatIndex].BrightnessMultiplier; // Neutral at 1 - const float contrastIntensity = HdrCmpDat[hdrCmpDatIndex].ContrastIntensity; // Neutral at 1 - const float contrastMidPoint = PerSceneConstants[316u].z * MidGrayScale; // Game usually has this around 0.18 (mid gray) + const float4 colorFilter = HdrCmpDat[hdrCmpDatIndex].ColorFilter; + const float hableSaturation = HdrCmpDat[hdrCmpDatIndex].HableSaturation; + const float brightnessMultiplier = HdrCmpDat[hdrCmpDatIndex].BrightnessMultiplier; // Neutral at 1 + const float contrastIntensity = HdrCmpDat[hdrCmpDatIndex].ContrastIntensity; // Neutral at 1 + const float contrastMidPoint = PerSceneConstants[316u].z * MidGrayScale; // Game usually has this around 0.18 (mid gray) ColorLuminance = Luminance(Color); @@ -610,8 +547,8 @@ float3 PostProcess(float3 Color, inout float ColorLuminance, float MidGrayScale // https://www.imagemagick.org/Usage/color_mods/#sigmoidal // Multiplier to somewhat match the original contrast adjustment - static const float SigmoidalContrastAdjustment = 2.5f; //TODO: we could probably find an even better default multiplication - float c = max(contrastIntensity * SigmoidalContrastAdjustment, EPSILON); // protect against division by zero + static const float sigmoidalContrastAdjustment = 2.5f; //TODO: we could probably find an even better default multiplication + float c = max(contrastIntensity * sigmoidalContrastAdjustment, EPSILON); // protect against division by zero float s = contrastMidPoint; // can be set to 0.5 for better darkening of shadows float minus = -1 / (1 + exp(c * s)); @@ -628,13 +565,13 @@ float3 PostProcess(float3 Color, inout float ColorLuminance, float MidGrayScale float3 PostProcess_Inverse(float3 Color, float ColorLuminance, float MidGrayScale = 1.f) { - const uint hdrCmpDatIndex = PcwHdrComposite.HdrCmpDatIndex; + const uint hdrCmpDatIndex = PcwHdrComposite.HdrCmpDatIndex; const float4 highlightsColorFilter = HdrCmpDat[hdrCmpDatIndex].HighlightsColorFilter; - const float4 colorFilter = HdrCmpDat[hdrCmpDatIndex].ColorFilter; - const float hableSaturation = HdrCmpDat[hdrCmpDatIndex].HableSaturation; - const float brightnessMultiplier = HdrCmpDat[hdrCmpDatIndex].BrightnessMultiplier; // Neutral at 1 - const float contrastIntensity = HdrCmpDat[hdrCmpDatIndex].ContrastIntensity; // Neutral at 1 - const float contrastMidPoint = PerSceneConstants[316u].z * MidGrayScale; + const float4 colorFilter = HdrCmpDat[hdrCmpDatIndex].ColorFilter; + const float hableSaturation = HdrCmpDat[hdrCmpDatIndex].HableSaturation; + const float brightnessMultiplier = HdrCmpDat[hdrCmpDatIndex].BrightnessMultiplier; // Neutral at 1 + const float contrastIntensity = HdrCmpDat[hdrCmpDatIndex].ContrastIntensity; // Neutral at 1 + const float contrastMidPoint = PerSceneConstants[316u].z * MidGrayScale; // We can't invert the color filter, so we will only inverse the post process by the unfiltered amount const float colorFilterInverse = 1.f - colorFilter.a; @@ -811,18 +748,7 @@ PSOutput PS(PSInput psInput) float acesParam_modE; float acesParam_modA; - float hable_params_y0; - float hable_params_y1; - float hable_dstParams_W; - float hable_toeSegment_lnA; - float hable_toeSegment_B; - float hable_midSegment_offsetX; - float hable_midSegment_lnA; - float hable_shoulderSegment_offsetX; - float hable_shoulderSegment_offsetY; - float hable_shoulderSegment_lnA; - float hable_shoulderSegment_B; - float hable_invScale; + HableParamters hableParams; const bool clampACES = false; @@ -844,19 +770,7 @@ PSOutput PS(PSInput psInput) case 3: { - tonemappedUnclampedColor = Hable(inputColor, - hable_params_y0, - hable_params_y1, - hable_dstParams_W, - hable_toeSegment_lnA, - hable_toeSegment_B, - hable_midSegment_offsetX, - hable_midSegment_lnA, - hable_shoulderSegment_offsetX, - hable_shoulderSegment_offsetY, - hable_shoulderSegment_lnA, - hable_shoulderSegment_B, - hable_invScale); + tonemappedUnclampedColor = Hable(inputColor, hableParams); tonemappedColor = tonemappedUnclampedColor; // Hable was never clamped } break; @@ -1004,33 +918,11 @@ PSOutput PS(PSInput psInput) const bool onlyInvertHighlights = (bool)INVERT_TONEMAP_HIGHLIGHTS_ONLY; inverseTonemapColor = Hable_Inverse(inverseTonemapColor, inverseTonemapColorComponents, - hable_params_y0, - hable_params_y1, - hable_dstParams_W, - hable_toeSegment_lnA, - hable_toeSegment_B, - hable_midSegment_offsetX, - hable_midSegment_lnA, - hable_shoulderSegment_offsetX, - hable_shoulderSegment_offsetY, - hable_shoulderSegment_lnA, - hable_shoulderSegment_B, - hable_invScale, + hableParams, onlyInvertHighlights); midGrayOut = Hable_Inverse(midGrayIn, 1, - hable_params_y0, - hable_params_y1, - hable_dstParams_W, - hable_toeSegment_lnA, - hable_toeSegment_B, - hable_midSegment_offsetX, - hable_midSegment_lnA, - hable_shoulderSegment_offsetX, - hable_shoulderSegment_offsetY, - hable_shoulderSegment_lnA, - hable_shoulderSegment_B, - hable_invScale, + hableParams, onlyInvertHighlights); tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; postColorCorrectionLuminance = postColorCorrectionClampedLuminance; diff --git a/shaders/structs.hlsl b/shaders/structs.hlsl index 5dd5dd15..e1c6e664 100644 --- a/shaders/structs.hlsl +++ b/shaders/structs.hlsl @@ -129,6 +129,49 @@ struct HDRCompositeData float ContrastIntensity; int i_0; //unused }; + +//HDRComposite shader structs start +struct Hable_params +{ + float y0; + float y1; +}; + +struct Hable_dstParams +{ + float W; +}; + +struct Hable_toeSegment +{ + float lnA; + float B; +}; + +struct Hable_midSigment +{ + float offsetX; + float lnA; +}; + +struct Hable_shoulderSegment +{ + float offsetX; + float offsetY; + float lnA; + float B; +}; + +struct HableParamters +{ + Hable_params params; + Hable_dstParams dstParams; + Hable_toeSegment toeSegment; + Hable_midSigment midSegment; + Hable_shoulderSegment shoulderSegment; + float invScale; +}; +//HDRComposite shader structs end //HDRComposite end //ColorGradingMerge start From 025b1c1e10c112a0d4e4773ff36f0b50a7de9347 Mon Sep 17 00:00:00 2001 From: Lilium Date: Sun, 1 Oct 2023 00:09:47 +0200 Subject: [PATCH 046/270] convert everything connected to shaders to use tab indentation and add ".editorconfig" --- .editorconfig | 7 + shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl | 46 +- .../ColorGradingMerge_FF81_cs.hlsl | 544 +++---- ...ContrastAdaptiveSharpening_100FF94_cs.hlsl | 416 ++--- shaders/FilmGrain/FilmGrain_FF75_ps.hlsl | 112 +- shaders/HDRComposite/HDRComposite_ps.hlsl | 1340 +++++++++-------- shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl | 86 +- .../ScaleformComposite_FFAA_ps.hlsl | 36 +- shaders/color.hlsl | 60 +- shaders/compile_all_shaders.bat | 28 +- shaders/structs.hlsl | 168 +-- 11 files changed, 1469 insertions(+), 1374 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..740324b1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +[*] +root = true +charset = utf-8 +end_of_line = crlf +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl b/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl index 767eb902..51e90854 100644 --- a/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl +++ b/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl @@ -10,36 +10,36 @@ SamplerState _13 : register(s0, space8); struct PSInputs { - float4 pos : SV_Position; - float2 TEXCOORD : TEXCOORD0; + float4 pos : SV_Position; + float2 TEXCOORD : TEXCOORD0; }; float4 PS(PSInputs inputs) : SV_Target { - float Y = _8.Sample(_13, inputs.TEXCOORD.xy).x * 1.16412353515625f; - float Cb = _9.Sample(_13, inputs.TEXCOORD.xy).x; - float Cr = _10.Sample(_13, inputs.TEXCOORD.xy).x; - float3 color; - //TODO: fix matrices??? Could these videos be meant for both SDR and HDR? - color.x = (Y - 0.870655059814453125f) + (Cb * 1.595794677734375f); - color.y = ((Y + 0.529705047607421875f) - (Cb * 0.8134765625f)) - (Cr * 0.391448974609375f); - color.z = (Y - 1.081668853759765625f) + (Cr * 2.017822265625f); - - // Clamp for safety of pow as the above matrix produces negative values, - // this breaks the UI pass if we are using R16G16B16A16F textures, - // as UI blending produces invalid pixels if it's blended with an invalid color. - color = max(color, 0.f); - + float Y = _8.Sample(_13, inputs.TEXCOORD.xy).x * 1.16412353515625f; + float Cb = _9.Sample(_13, inputs.TEXCOORD.xy).x; + float Cr = _10.Sample(_13, inputs.TEXCOORD.xy).x; + float3 color; + //TODO: fix matrices??? Could these videos be meant for both SDR and HDR? + color.x = (Y - 0.870655059814453125f) + (Cb * 1.595794677734375f); + color.y = ((Y + 0.529705047607421875f) - (Cb * 0.8134765625f)) - (Cr * 0.391448974609375f); + color.z = (Y - 1.081668853759765625f) + (Cr * 2.017822265625f); + + // Clamp for safety of pow as the above matrix produces negative values, + // this breaks the UI pass if we are using R16G16B16A16F textures, + // as UI blending produces invalid pixels if it's blended with an invalid color. + color = max(color, 0.f); + #if ENABLE_HDR #if HDR_USE_GAMMA_2_2 - color = pow(color, 2.2f); + color = pow(color, 2.2f); #else - color = gamma_sRGB_to_linear(color); + color = gamma_sRGB_to_linear(color); #endif - //TODO: AutoHDR on movies??? - - color *= HDR_GAME_PAPER_WHITE; + //TODO: AutoHDR on movies??? + + color *= HDR_GAME_PAPER_WHITE; #endif // ENABLE_HDR - return float4(color, 1.0f); -} \ No newline at end of file + return float4(color, 1.0f); +} diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index ea7ebe20..b79af6df 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -12,7 +12,7 @@ static float additionalNeutralLUTPercentage = 0.f; // ~0.25 might be a good comp cbuffer CPushConstantWrapper_ColorGradingMerge : register(b0, space0) { - PushConstantWrapper_ColorGradingMerge PcwColorGradingMerge : packoffset(c0); + PushConstantWrapper_ColorGradingMerge PcwColorGradingMerge : packoffset(c0); }; Texture2D LUT1 : register(t0, space8); @@ -23,28 +23,28 @@ RWTexture3D OutMixetLUT : register(u0, space8); struct LUTAnalysis { - float3 black; - float blackY; - float3 white; - float whiteY; + float3 black; + float blackY; + float3 white; + float whiteY; #if UNUSED_PARAMS - float minChannel; - float maxChannel; - float minY; - float maxY; - float averageY; - float range; - float medianY; - float averageChannel; - float averageRed; - float averageGreen; - float averageBlue; - float maxRed; - float maxGreen; - float maxBlue; - float minRed; - float minGreen; - float minBlue; + float minChannel; + float maxChannel; + float minY; + float maxY; + float averageY; + float range; + float medianY; + float averageChannel; + float averageRed; + float averageGreen; + float averageBlue; + float maxRed; + float maxGreen; + float maxBlue; + float minRed; + float minGreen; + float minBlue; #endif // UNUSED_PARAMS }; @@ -53,79 +53,79 @@ static const uint LUTSizeLog2 = (uint)log2(LUT_SIZE); // In/Out in pixels uint3 ThreeToTwoDimensionCoordinates(uint3 UVW) { - // 2D LUT extends horizontally. - const uint U = (UVW.z << LUTSizeLog2) + UVW.x; - return uint3(U, UVW.y, 0); + // 2D LUT extends horizontally. + const uint U = (UVW.z << LUTSizeLog2) + UVW.x; + return uint3(U, UVW.y, 0); } void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) { - Analysis.black = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(0u)).rgb); - Analysis.blackY = Luminance(Analysis.black); - Analysis.white = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(LUT_SIZE_UINT - 1u)).rgb); - Analysis.whiteY = Luminance(Analysis.white); + Analysis.black = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(0u)).rgb); + Analysis.blackY = Luminance(Analysis.black); + Analysis.white = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(LUT_SIZE_UINT - 1u)).rgb); + Analysis.whiteY = Luminance(Analysis.white); #if UNUSED_PARAMS - Analysis.minY = FLT_MAX; - Analysis.maxY = -FLT_MAX; - - float3 colors = 0.f; - float Ys = 0.f; - - float3 minColor = FLT_MAX; - float3 maxColor = -FLT_MAX; - - const uint texelCount = LUT_SIZE_UINT * LUT_SIZE_UINT * LUT_SIZE_UINT; - - // TODO: this loops on every LUT pixel for every output pixel. - // given we only need the min/max color channels here, to optimize we could: - // -Just iterate on the last (e.g.) 3 pixels of each axis, which should guarantee to find the max for all normal LUTs. - // According to ShortFuse some LUTs don't have their min and max in the edges, but it could be in the middle or close to it. - // It's arguable we'd care about this cases, and it's also arguable to correct them by global min instead of edges min, as the result could actually be worse? - // -Work with atomic (shared variables), group sync and thread/thread group to only calculate this once. - // -Add a new post LUT mixing compute shader pass that does this once. - const uint analyzeTexelFrequency = OPTIMIZE_LUT_ANALYSIS ? (LUT_SIZE_UINT - 1u) : 1u; // Set to 1 for all. Set to "LUT_SIZE_UINT - 1u" for edges only. - for (uint x = 0; x < LUT_SIZE_UINT; x += analyzeTexelFrequency) - { - for (uint y = 0; y < LUT_SIZE_UINT; y += analyzeTexelFrequency) - { - for (uint z = 0; z < LUT_SIZE_UINT; z += analyzeTexelFrequency) - { - float3 LUTColor = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(uint3(x, y, z))).rgb); - - minColor = min(minColor, LUTColor); - maxColor = max(maxColor, LUTColor); - - float Y = Luminance(LUTColor); - Analysis.minY = min(Analysis.minY, Y); - Analysis.maxY = max(Analysis.maxY, Y); - Ys += Y; - - colors += LUTColor.r; - } - } - } - - //TODO: either store min/max channels merged or separately, but not both - Analysis.minChannel = min(minColor.r, min(minColor.g, minColor.b)); - Analysis.maxChannel = max(maxColor.r, max(maxColor.g, maxColor.b)); - Analysis.minRed = minRed; - Analysis.minGreen = minGreen; - Analysis.minBlue = minBlue; - Analysis.maxRed = maxRed; - Analysis.maxGreen = maxGreen; - Analysis.maxBlue = maxBlue; - - Analysis.averageY = Ys / texelCount; - Analysis.averageRed = colors.r / texelCount; - Analysis.averageGreen = colors.g / texelCount; - Analysis.averageBlue = colors.b / texelCount; - - Analysis.averageChannel = (Analysis.averageRed + Analysis.averageGreen + Analysis.averageBlue) / 3.f; - Analysis.range = Analysis.maxY - Analysis.minY; - - //TODO: ??? Can't do this in shader but it's not needed - //Ys.sort((a, b) => a - b); - //Analysis.medianY = ((Ys[Math.floor(texelCount / 2)]) + (Ys[Math.ceil(texelCount / 2)])) / 2; + Analysis.minY = FLT_MAX; + Analysis.maxY = -FLT_MAX; + + float3 colors = 0.f; + float Ys = 0.f; + + float3 minColor = FLT_MAX; + float3 maxColor = -FLT_MAX; + + const uint texelCount = LUT_SIZE_UINT * LUT_SIZE_UINT * LUT_SIZE_UINT; + + // TODO: this loops on every LUT pixel for every output pixel. + // given we only need the min/max color channels here, to optimize we could: + // -Just iterate on the last (e.g.) 3 pixels of each axis, which should guarantee to find the max for all normal LUTs. + // According to ShortFuse some LUTs don't have their min and max in the edges, but it could be in the middle or close to it. + // It's arguable we'd care about this cases, and it's also arguable to correct them by global min instead of edges min, as the result could actually be worse? + // -Work with atomic (shared variables), group sync and thread/thread group to only calculate this once. + // -Add a new post LUT mixing compute shader pass that does this once. + const uint analyzeTexelFrequency = OPTIMIZE_LUT_ANALYSIS ? (LUT_SIZE_UINT - 1u) : 1u; // Set to 1 for all. Set to "LUT_SIZE_UINT - 1u" for edges only. + for (uint x = 0; x < LUT_SIZE_UINT; x += analyzeTexelFrequency) + { + for (uint y = 0; y < LUT_SIZE_UINT; y += analyzeTexelFrequency) + { + for (uint z = 0; z < LUT_SIZE_UINT; z += analyzeTexelFrequency) + { + float3 LUTColor = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(uint3(x, y, z))).rgb); + + minColor = min(minColor, LUTColor); + maxColor = max(maxColor, LUTColor); + + float Y = Luminance(LUTColor); + Analysis.minY = min(Analysis.minY, Y); + Analysis.maxY = max(Analysis.maxY, Y); + Ys += Y; + + colors += LUTColor.r; + } + } + } + + //TODO: either store min/max channels merged or separately, but not both + Analysis.minChannel = min(minColor.r, min(minColor.g, minColor.b)); + Analysis.maxChannel = max(maxColor.r, max(maxColor.g, maxColor.b)); + Analysis.minRed = minRed; + Analysis.minGreen = minGreen; + Analysis.minBlue = minBlue; + Analysis.maxRed = maxRed; + Analysis.maxGreen = maxGreen; + Analysis.maxBlue = maxBlue; + + Analysis.averageY = Ys / texelCount; + Analysis.averageRed = colors.r / texelCount; + Analysis.averageGreen = colors.g / texelCount; + Analysis.averageBlue = colors.b / texelCount; + + Analysis.averageChannel = (Analysis.averageRed + Analysis.averageGreen + Analysis.averageBlue) / 3.f; + Analysis.range = Analysis.maxY - Analysis.minY; + + //TODO: ??? Can't do this in shader but it's not needed + //Ys.sort((a, b) => a - b); + //Analysis.medianY = ((Ys[Math.floor(texelCount / 2)]) + (Ys[Math.ceil(texelCount / 2)])) / 2; #endif // UNUSED_PARAMS } @@ -133,208 +133,224 @@ void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) // Slow but effective. float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, bool SDRRange = false) { - LUTAnalysis analysis; - AnalyzeLUT(LUT, analysis); - - float3 color = gamma_sRGB_to_linear(LUT.Load(UVW)); - const float3 originalColor = color; - - // TODO: do we need these branches? Shaders are best without branches, especially when they could be replaced by min/max math. - // Is the branch to prevent weird LUTs from being adjusted? - if ((analysis.whiteY > analysis.blackY) - && ((analysis.whiteY - analysis.blackY) < 1.f)) - { - // Lower black floor (shadow pass): - // In 3D space, drags all points towards 0 relative to how much black - // point has moved (eg (on 256): -4, -10, -5) - // Distance from 0 is factor to decide how much each pixel should move, - // with (1) (white) not moving at all. - // *Applying to each channel individually also removes tint - const float3 reduceFactor = lerp(1.f / (1.f - analysis.black), 1.f, pow(neutralLUTColor, 2.f)); //TODO: test pow - - // Reapply tint without raising black floor: - // In 3D space, shift points away from black **per channel** to - // reintroduce tint back to the pixels. Use multiplication to avoid - // raised black floor. - const float3 increaseFactor = lerp(1.f + analysis.black, 1.f, neutralLUTColor); - - // color * reduce = [0,1] relative to XYZ: - // result *= black tint - // (0,0,0) must always compute to 0 - // (1,1,1) must remain unchanged - // Scaling is strongest at 0, and weakest at 1 (none) - color = (1.f - ((1.f - color) * reduceFactor)) * increaseFactor; + LUTAnalysis analysis; + AnalyzeLUT(LUT, analysis); + + float3 color = gamma_sRGB_to_linear(LUT.Load(UVW)); + const float3 originalColor = color; + + // TODO: do we need these branches? Shaders are best without branches, especially when they could be replaced by min/max math. + // Is the branch to prevent weird LUTs from being adjusted? + if ((analysis.whiteY > analysis.blackY) + && ((analysis.whiteY - analysis.blackY) < 1.f)) + { + // Lower black floor (shadow pass): + // In 3D space, drags all points towards 0 relative to how much black + // point has moved (eg (on 256): -4, -10, -5) + // Distance from 0 is factor to decide how much each pixel should move, + // with (1) (white) not moving at all. + // *Applying to each channel individually also removes tint + const float3 reduceFactor = lerp(1.f / (1.f - analysis.black), 1.f, pow(neutralLUTColor, 2.f)); //TODO: test pow + + // Reapply tint without raising black floor: + // In 3D space, shift points away from black **per channel** to + // reintroduce tint back to the pixels. Use multiplication to avoid + // raised black floor. + const float3 increaseFactor = lerp(1.f + analysis.black, 1.f, neutralLUTColor); + + // color * reduce = [0,1] relative to XYZ: + // result *= black tint + // (0,0,0) must always compute to 0 + // (1,1,1) must remain unchanged + // Scaling is strongest at 0, and weakest at 1 (none) + color = (1.f - ((1.f - color) * reduceFactor)) * increaseFactor; #if 0 // "Invalid" range analysis - if (any(color > 1)) - { - return 0; - } - else if (any(color < 0)) - { - return 1; - } + if (any(color > 1)) + { + return 0; + } + else if (any(color < 0)) + { + return 1; + } #endif #if 1 // TODO: the above code introduces a lot of colors beyond the 0-1 range, which will then get clipped, causing a hue shift. This fix up might not be necessary as later we raise blacks anyway. - // Color may have gone negative - // For example, if black is (3,3,3) and another value is (0,0,4), that - // may result in (-3,-3,1) - color = max(color, 0.f); + // Color may have gone negative + // For example, if black is (3,3,3) and another value is (0,0,4), that + // may result in (-3,-3,1) + color = max(color, 0.f); #endif - static const float cubeNormalization = sqrt(3.f); // Normalize to 0-1 range - // How distant are our LUT coordinates from black or white in 3D cube space? - const float blackDistance = hypot3(neutralLUTColor) / cubeNormalization; - const float whiteDistance = hypot3(1.f - neutralLUTColor) / cubeNormalization; - const float totalRange = blackDistance + whiteDistance; - - const float currentY = Luminance(color); // In case this was negative, the shadows raise pass should bring it back to the >= 0 range - - // Magic number to decide by how much to scale the new shadow area - const float shadowCompensationPercentage = 1.f - analysis.blackY; - - // Brightness multiplier from shadows: - // Because the amount the black level was raised is proportional to - // the harshness of a linear gradient, a compensation must be made - // to avoid black crushing, and maintain visibility at certain - // points in the LUT. The scaling used will be relative to the - // raised black floor level to ensure proportional consistency per - // LUT. This is only done to the newly created shadow section (if any). - // Simplified, if black started raised, a new shadow section was - // created, and that needs extra luminance for visibility. - const float shadowsRaise = linearNormalization( - min(currentY, analysis.blackY), - 0.f, - analysis.blackY, - 1.f + shadowCompensationPercentage, - 1.f); - // TODO: Analyze grayscale for shadow raise. For example, if black shadow was raised - // by 5%, then analyze the ramping from 5%-10% and apply that to - // the new 0% - 5% raise. - - // TODO: apply both shadow and highlight raise to the whole image range, instead of a portion of it, to make its gradient smoother? Or maybe apply it in perceptual (gamma) space. - // The functions could also be simplified a lot. - - // Brightness multiplier from highlights: - // Boost luminance of all texels relative to their distance to white - // and how much white is to be raised. - // (ie: white drags everything towards it) - float highlightsRaise = linearNormalization( - whiteDistance, - 0.f, - totalRange, - 1.f / analysis.whiteY, - 1.f); - // Scale by the maximum value this could ever have, so it's normalized and we are guaranteed target luminance doesn't go beyond 1. Note that this can literally destroy luminance. - highlightsRaise *= SDRRange ? analysis.whiteY : 1.f; - const float targetY = currentY * highlightsRaise * shadowsRaise; - - // TODO: why are we clamping to full white here? luminance 1 (or beyond) might not match a white color at all, should we instead try to conserve the hue? Though that's much harder as we'd need to analyze more LUT pixels. - if (SDRRange && targetY >= 1.f) - { - color = 1.f; // Clamp (clip to full white, it also looks great with external AutoHDR) - // Or, limit to max luminance while maintaining hue, though may result in LUT max Y being below 1 - // Or, still target full white for (1,1,1), but scale others relative to whiteY - // Or, don't adjust unless white point - } - else if (currentY != 0.f) - { - color *= max(targetY, 0.f) / max(currentY, 0.f); // Clamp luminances to avoid a double negative creating a positive number - } - - // Optional step to keep colors in the SDR range. - if (SDRRange) - { - color = saturate(color); - } - // TODO: consider removing this if it's proven unnecessary on all LUTs - // Some protection against invalid colors - else if (Luminance(color) < 0.f) - { - color = 0.f; - } - } - - color = lerp(originalColor, color, LUTCorrectionPercentage); - - return color; + static const float cubeNormalization = sqrt(3.f); // Normalize to 0-1 range + // How distant are our LUT coordinates from black or white in 3D cube space? + const float blackDistance = hypot3(neutralLUTColor) / cubeNormalization; + const float whiteDistance = hypot3(1.f - neutralLUTColor) / cubeNormalization; + const float totalRange = blackDistance + whiteDistance; + + const float currentY = Luminance(color); // In case this was negative, the shadows raise pass should bring it back to the >= 0 range + + // Magic number to decide by how much to scale the new shadow area + const float shadowCompensationPercentage = 1.f - analysis.blackY; + + // Brightness multiplier from shadows: + // Because the amount the black level was raised is proportional to + // the harshness of a linear gradient, a compensation must be made + // to avoid black crushing, and maintain visibility at certain + // points in the LUT. The scaling used will be relative to the + // raised black floor level to ensure proportional consistency per + // LUT. This is only done to the newly created shadow section (if any). + // Simplified, if black started raised, a new shadow section was + // created, and that needs extra luminance for visibility. + const float shadowsRaise = linearNormalization( + min(currentY, analysis.blackY), + 0.f, + analysis.blackY, + 1.f + shadowCompensationPercentage, + 1.f); + // TODO: Analyze grayscale for shadow raise. For example, if black shadow was raised + // by 5%, then analyze the ramping from 5%-10% and apply that to + // the new 0% - 5% raise. + + // TODO: apply both shadow and highlight raise to the whole image range, instead of a portion of it, to make its gradient smoother? Or maybe apply it in perceptual (gamma) space. + // The functions could also be simplified a lot. + + // Brightness multiplier from highlights: + // Boost luminance of all texels relative to their distance to white + // and how much white is to be raised. + // (ie: white drags everything towards it) + float highlightsRaise = linearNormalization( + whiteDistance, + 0.f, + totalRange, + 1.f / analysis.whiteY, + 1.f); + // Scale by the maximum value this could ever have, so it's normalized and we are guaranteed target luminance doesn't go beyond 1. Note that this can literally destroy luminance. + highlightsRaise *= SDRRange ? analysis.whiteY : 1.f; + const float targetY = currentY * highlightsRaise * shadowsRaise; + + // TODO: why are we clamping to full white here? luminance 1 (or beyond) might not match a white color at all, should we instead try to conserve the hue? Though that's much harder as we'd need to analyze more LUT pixels. + if (SDRRange && targetY >= 1.f) + { + color = 1.f; // Clamp (clip to full white, it also looks great with external AutoHDR) + // Or, limit to max luminance while maintaining hue, though may result in LUT max Y being below 1 + // Or, still target full white for (1,1,1), but scale others relative to whiteY + // Or, don't adjust unless white point + } + else if (currentY != 0.f) + { + color *= max(targetY, 0.f) / max(currentY, 0.f); // Clamp luminances to avoid a double negative creating a positive number + } + + // Optional step to keep colors in the SDR range. + if (SDRRange) + { + color = saturate(color); + } + // TODO: consider removing this if it's proven unnecessary on all LUTs + // Some protection against invalid colors + else if (Luminance(color) < 0.f) + { + color = 0.f; + } + } + + color = lerp(originalColor, color, LUTCorrectionPercentage); + + return color; } // Dispatch size is 1 1 16 (x and y have one thread and one thread group, while z has 16 thread groups with a thread each) [numthreads(LUT_SIZE_UINT, LUT_SIZE_UINT, 1)] void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) { - const uint3 inUVW = ThreeToTwoDimensionCoordinates(SV_DispatchThreadID); - const uint3 outUVW = SV_DispatchThreadID; + const uint3 inUVW = ThreeToTwoDimensionCoordinates(SV_DispatchThreadID); + const uint3 outUVW = SV_DispatchThreadID; - float3 neutralLUTColor = float3(outUVW) / (LUT_SIZE - 1.f); // The neutral LUT is automatically generated by the coordinates, but it's baked with sRGB gamma - neutralLUTColor = gamma_sRGB_to_linear(neutralLUTColor); + float3 neutralLUTColor = float3(outUVW) / (LUT_SIZE - 1.f); // The neutral LUT is automatically generated by the coordinates, but it's baked with sRGB gamma + neutralLUTColor = gamma_sRGB_to_linear(neutralLUTColor); #if LUT_IMPROVEMENT_TYPE != 1 - float3 LUT1Color = LUT1.Load(inUVW); - float3 LUT2Color = LUT2.Load(inUVW); - float3 LUT3Color = LUT3.Load(inUVW); - float3 LUT4Color = LUT4.Load(inUVW); - LUT1Color = gamma_sRGB_to_linear(LUT1Color); - LUT2Color = gamma_sRGB_to_linear(LUT2Color); - LUT3Color = gamma_sRGB_to_linear(LUT3Color); - LUT4Color = gamma_sRGB_to_linear(LUT4Color); + + float3 LUT1Color = LUT1.Load(inUVW); + float3 LUT2Color = LUT2.Load(inUVW); + float3 LUT3Color = LUT3.Load(inUVW); + float3 LUT4Color = LUT4.Load(inUVW); + LUT1Color = gamma_sRGB_to_linear(LUT1Color); + LUT2Color = gamma_sRGB_to_linear(LUT2Color); + LUT3Color = gamma_sRGB_to_linear(LUT3Color); + LUT4Color = gamma_sRGB_to_linear(LUT4Color); + #endif // LUT_IMPROVEMENT_TYPE - const bool SDRRange = !((bool)ENABLE_HDR) || (bool)FORCE_SDR_LUTS; + const bool SDRRange = !((bool)ENABLE_HDR) || (bool)FORCE_SDR_LUTS; #if LUT_IMPROVEMENT_TYPE == 1 - float3 LUT1Color = PatchLUTColor(LUT1, inUVW, neutralLUTColor, SDRRange); - float3 LUT2Color = PatchLUTColor(LUT2, inUVW, neutralLUTColor, SDRRange); - float3 LUT3Color = PatchLUTColor(LUT3, inUVW, neutralLUTColor, SDRRange); - float3 LUT4Color = PatchLUTColor(LUT4, inUVW, neutralLUTColor, SDRRange); + + float3 LUT1Color = PatchLUTColor(LUT1, inUVW, neutralLUTColor, SDRRange); + float3 LUT2Color = PatchLUTColor(LUT2, inUVW, neutralLUTColor, SDRRange); + float3 LUT3Color = PatchLUTColor(LUT3, inUVW, neutralLUTColor, SDRRange); + float3 LUT4Color = PatchLUTColor(LUT4, inUVW, neutralLUTColor, SDRRange); + #elif LUT_IMPROVEMENT_TYPE == 2 - float neutralLUTLuminance = Luminance(neutralLUTColor); - float LUT1Luminance = Luminance(LUT1Color); - float LUT2Luminance = Luminance(LUT2Color); - float LUT3Luminance = Luminance(LUT3Color); - float LUT4Luminance = Luminance(LUT4Color); - - if (LUT1Luminance != 0.f) - { - LUT1Color *= lerp(1.f, neutralLUTLuminance / LUT1Luminance, LUTCorrectionPercentage); - if (SDRRange) { LUT1Color = saturate(LUT1Color) }; - } - if (LUT2Luminance != 0.f) - { - LUT2Color *= lerp(1.f, neutralLUTLuminance / LUT2Luminance, LUTCorrectionPercentage); - if (SDRRange) { LUT2Color = saturate(LUT2Color) }; - } - if (LUT3Luminance != 0.f) - { - LUT3Color *= lerp(1.f, neutralLUTLuminance / LUT3Luminance, LUTCorrectionPercentage); - if (SDRRange) { LUT3Color = saturate(LUT3Color) }; - } - if (LUT4Luminance != 0.f) - { - LUT4Color *= lerp(1.f, neutralLUTLuminance / LUT4Luminance, LUTCorrectionPercentage); - if (SDRRange) { LUT4Color = saturate(LUT4Color) }; - } + float neutralLUTLuminance = Luminance(neutralLUTColor); + float LUT1Luminance = Luminance(LUT1Color); + float LUT2Luminance = Luminance(LUT2Color); + float LUT3Luminance = Luminance(LUT3Color); + float LUT4Luminance = Luminance(LUT4Color); + + if (LUT1Luminance != 0.f) + { + LUT1Color *= lerp(1.f, neutralLUTLuminance / LUT1Luminance, LUTCorrectionPercentage); + if (SDRRange) + { + LUT1Color = saturate(LUT1Color) + }; + } + if (LUT2Luminance != 0.f) + { + LUT2Color *= lerp(1.f, neutralLUTLuminance / LUT2Luminance, LUTCorrectionPercentage); + if (SDRRange) + { + LUT2Color = saturate(LUT2Color) + }; + } + if (LUT3Luminance != 0.f) + { + LUT3Color *= lerp(1.f, neutralLUTLuminance / LUT3Luminance, LUTCorrectionPercentage); + if (SDRRange) + { + LUT3Color = saturate(LUT3Color) + }; + } + if (LUT4Luminance != 0.f) + { + LUT4Color *= lerp(1.f, neutralLUTLuminance / LUT4Luminance, LUTCorrectionPercentage); + if (SDRRange) + { + LUT4Color = saturate(LUT4Color) + }; + } #endif // LUT_IMPROVEMENT_TYPE - float adjustedNeutralLUTPercentage = lerp(PcwColorGradingMerge.neutralLUTPercentage, 1.f, additionalNeutralLUTPercentage); - float adjustedLUT1Percentage = lerp(PcwColorGradingMerge.LUT1Percentage, 0.f, additionalNeutralLUTPercentage); - float adjustedLUT2Percentage = lerp(PcwColorGradingMerge.LUT2Percentage, 0.f, additionalNeutralLUTPercentage); - float adjustedLUT3Percentage = lerp(PcwColorGradingMerge.LUT3Percentage, 0.f, additionalNeutralLUTPercentage); - float adjustedLUT4Percentage = lerp(PcwColorGradingMerge.LUT4Percentage, 0.f, additionalNeutralLUTPercentage); + float adjustedNeutralLUTPercentage = lerp(PcwColorGradingMerge.neutralLUTPercentage, 1.f, additionalNeutralLUTPercentage); + float adjustedLUT1Percentage = lerp(PcwColorGradingMerge.LUT1Percentage, 0.f, additionalNeutralLUTPercentage); + float adjustedLUT2Percentage = lerp(PcwColorGradingMerge.LUT2Percentage, 0.f, additionalNeutralLUTPercentage); + float adjustedLUT3Percentage = lerp(PcwColorGradingMerge.LUT3Percentage, 0.f, additionalNeutralLUTPercentage); + float adjustedLUT4Percentage = lerp(PcwColorGradingMerge.LUT4Percentage, 0.f, additionalNeutralLUTPercentage); - float3 mixedLUT = (adjustedNeutralLUTPercentage * neutralLUTColor) - + (adjustedLUT1Percentage * LUT1Color) - + (adjustedLUT2Percentage * LUT2Color) - + (adjustedLUT3Percentage * LUT3Color) - + (adjustedLUT4Percentage * LUT4Color); + float3 mixedLUT = (adjustedNeutralLUTPercentage * neutralLUTColor) + + (adjustedLUT1Percentage * LUT1Color) + + (adjustedLUT2Percentage * LUT2Color) + + (adjustedLUT3Percentage * LUT3Color) + + (adjustedLUT4Percentage * LUT4Color); // Convert to sRGB after blending between LUTs, so the blends are done in linear space, which gives more consistent and correct results #if !LUT_FIX_GAMMA_MAPPING - mixedLUT = gamma_linear_to_sRGB(mixedLUT); + mixedLUT = gamma_linear_to_sRGB(mixedLUT); #endif // !LUT_FIX_GAMMA_MAPPING - OutMixetLUT[outUVW] = float4(mixedLUT, 1.f); + OutMixetLUT[outUVW] = float4(mixedLUT, 1.f); } diff --git a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_100FF94_cs.hlsl b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_100FF94_cs.hlsl index 2c78ee2b..91b6e299 100644 --- a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_100FF94_cs.hlsl +++ b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_100FF94_cs.hlsl @@ -3,12 +3,12 @@ cbuffer _16_18 : register(b0, space6) { - float4 _18_m0[8] : packoffset(c0); + float4 _18_m0[8] : packoffset(c0); }; cbuffer _21_23 : register(b0, space8) { - float4 _23_m0[4] : packoffset(c0); + float4 _23_m0[4] : packoffset(c0); }; Texture2D _8 : register(t0, space8); @@ -18,231 +18,231 @@ static uint3 gl_WorkGroupID; static uint3 gl_LocalInvocationID; struct SPIRV_Cross_Input { - uint3 gl_WorkGroupID : SV_GroupID; - uint3 gl_LocalInvocationID : SV_GroupThreadID; + uint3 gl_WorkGroupID : SV_GroupID; + uint3 gl_LocalInvocationID : SV_GroupThreadID; }; uint spvPackHalf2x16(float2 value) { - uint2 Packed = f32tof16(value); - return Packed.x | (Packed.y << 16); + uint2 Packed = f32tof16(value); + return Packed.x | (Packed.y << 16); } float2 spvUnpackHalf2x16(uint value) { - return f16tof32(uint2(value & 0xffff, value >> 16)); + return f16tof32(uint2(value & 0xffff, value >> 16)); } void comp_main() { #if ENABLE_HDR - // butchered the CS so it just outputs the input - uint4 _55 = asuint(_23_m0[2u]); - uint _58 = _55.x + (((gl_LocalInvocationID.x >> 1u) & 7u) | (gl_WorkGroupID.x << 4u)); - uint _59 = ((((gl_LocalInvocationID.x >> 3u) & 6u) | (gl_LocalInvocationID.x & 1u)) | (gl_WorkGroupID.y << 4u)) + _55.y; - uint _458 = _58 + 8u; - uint _489 = _59 + 8u; + // butchered the CS so it just outputs the input + uint4 _55 = asuint(_23_m0[2u]); + uint _58 = _55.x + (((gl_LocalInvocationID.x >> 1u) & 7u) | (gl_WorkGroupID.x << 4u)); + uint _59 = ((((gl_LocalInvocationID.x >> 3u) & 6u) | (gl_LocalInvocationID.x & 1u)) | (gl_WorkGroupID.y << 4u)) + _55.y; + uint _458 = _58 + 8u; + uint _489 = _59 + 8u; - _11[uint2(_58, _59)] = _8.Load(int3(uint2(_58, _59), 0u)); - _11[uint2(_458, _59)] = _8.Load(int3(uint2(_458, _59), 0u)); - _11[uint2(_58, _489)] = _8.Load(int3(uint2(_58, _489), 0u)); - _11[uint2(_458, _489)] = _8.Load(int3(uint2(_458, _489), 0u)); + _11[uint2(_58, _59)] = _8.Load(int3(uint2(_58, _59), 0u)); + _11[uint2(_458, _59)] = _8.Load(int3(uint2(_458, _59), 0u)); + _11[uint2(_58, _489)] = _8.Load(int3(uint2(_58, _489), 0u)); + _11[uint2(_458, _489)] = _8.Load(int3(uint2(_458, _489), 0u)); #else - uint4 _55 = asuint(_23_m0[2u]); - uint _58 = _55.x + (((gl_LocalInvocationID.x >> 1u) & 7u) | (gl_WorkGroupID.x << 4u)); - uint _59 = ((((gl_LocalInvocationID.x >> 3u) & 6u) | (gl_LocalInvocationID.x & 1u)) | (gl_WorkGroupID.y << 4u)) + _55.y; - uint4 _69 = asuint(_23_m0[3u]); - uint _70 = _69.x; - uint _71 = _69.y; - uint _72 = _69.z; - uint _73 = _69.w; - uint _74 = _58 << 16u; - uint _76 = uint(int(_74) >> int(16u)); - uint _77 = _59 << 16u; - uint _87 = uint(int(max(min(_76, _72), _70) << 16u) >> int(16u)); - uint _89 = uint(int(max(min(uint(int(_77 + 4294901760u) >> int(16u)), _73), _71) << 16u) >> int(16u)); - float4 _91 = _8.Load(int3(uint2(_87, _89), 0u)); - uint _96 = uint(int(_74 + 4294901760u) >> int(16u)); - uint _105 = uint(int(max(min(uint(int(_77) >> int(16u)), _73), _71) << 16u) >> int(16u)); - float4 _106 = _8.Load(int3(uint2(uint(int(max(min(_96, _72), _70) << 16u) >> int(16u)), _105), 0u)); - float4 _109 = _8.Load(int3(uint2(_87, _105), 0u)); - uint _114 = uint(int(_74 + 65536u) >> int(16u)); - float4 _119 = _8.Load(int3(uint2(uint(int(max(min(_114, _72), _70) << 16u) >> int(16u)), _105), 0u)); - uint _127 = uint(int(max(min(uint(int(_77 + 65536u) >> int(16u)), _73), _71) << 16u) >> int(16u)); - float4 _128 = _8.Load(int3(uint2(_87, _127), 0u)); - uint _134 = uint(int((_58 << 16u) + 524288u) >> int(16u)); - uint _138 = uint(int(max(min(_134, _72), _70) << 16u) >> int(16u)); - float4 _139 = _8.Load(int3(uint2(_138, _89), 0u)); - uint _146 = uint(int(_74 + 458752u) >> int(16u)); - float4 _151 = _8.Load(int3(uint2(uint(int(max(min(_146, _72), _70) << 16u) >> int(16u)), _105), 0u)); - float4 _156 = _8.Load(int3(uint2(_138, _105), 0u)); - uint _163 = uint(int(_74 + 589824u) >> int(16u)); - float4 _168 = _8.Load(int3(uint2(uint(int(max(min(_163, _72), _70) << 16u) >> int(16u)), _105), 0u)); - float4 _173 = _8.Load(int3(uint2(_138, _127), 0u)); - float _193 = exp2(log2((_91.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _194 = exp2(log2((_139.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _213 = exp2(log2((_106.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _214 = exp2(log2((_151.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _233 = exp2(log2((_109.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _234 = exp2(log2((_156.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _253 = exp2(log2((_119.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _254 = exp2(log2((_168.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _273 = exp2(log2((_128.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _274 = exp2(log2((_173.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _280 = isnan(_213) ? _193 : (isnan(_193) ? _213 : min(_193, _213)); - float _281 = isnan(_214) ? _194 : (isnan(_194) ? _214 : min(_194, _214)); - float _282 = isnan(_233) ? _280 : (isnan(_280) ? _233 : min(_280, _233)); - float _283 = isnan(_234) ? _281 : (isnan(_281) ? _234 : min(_281, _234)); - float _284 = isnan(_273) ? _253 : (isnan(_253) ? _273 : min(_253, _273)); - float _285 = isnan(_274) ? _254 : (isnan(_254) ? _274 : min(_254, _274)); - float _286 = isnan(_282) ? _284 : (isnan(_284) ? _282 : min(_284, _282)); - float _287 = isnan(_283) ? _285 : (isnan(_285) ? _283 : min(_285, _283)); - float _288 = isnan(_213) ? _193 : (isnan(_193) ? _213 : max(_193, _213)); - float _289 = isnan(_214) ? _194 : (isnan(_194) ? _214 : max(_194, _214)); - float _290 = isnan(_233) ? _288 : (isnan(_288) ? _233 : max(_288, _233)); - float _291 = isnan(_234) ? _289 : (isnan(_289) ? _234 : max(_289, _234)); - float _292 = isnan(_273) ? _253 : (isnan(_253) ? _273 : max(_253, _273)); - float _293 = isnan(_274) ? _254 : (isnan(_254) ? _274 : max(_254, _274)); - float _294 = isnan(_290) ? _292 : (isnan(_292) ? _290 : max(_292, _290)); - float _295 = isnan(_291) ? _293 : (isnan(_293) ? _291 : max(_293, _291)); - float _299 = 1.0f - _294; - float _300 = 1.0f - _295; - float _303 = (isnan(_299) ? _286 : (isnan(_286) ? _299 : min(_286, _299))) * (1.0f / _294); - float _304 = (isnan(_300) ? _287 : (isnan(_287) ? _300 : min(_287, _300))) * (1.0f / _295); - float _981 = isnan(0.0f) ? _303 : (isnan(_303) ? 0.0f : max(_303, 0.0f)); - float _992 = isnan(0.0f) ? _304 : (isnan(_304) ? 0.0f : max(_304, 0.0f)); - float2 _313 = spvUnpackHalf2x16(asuint(_23_m0[1u]).y & 65535u); - float _314 = _313.x; - float _315 = _314 * sqrt(isnan(1.0f) ? _981 : (isnan(_981) ? 1.0f : min(_981, 1.0f))); - float _316 = _314 * sqrt(isnan(1.0f) ? _992 : (isnan(_992) ? 1.0f : min(_992, 1.0f))); - float _322 = 1.0f / ((_315 * 4.0f) + 1.0f); - float _323 = 1.0f / ((_316 * 4.0f) + 1.0f); - float _329 = ((_316 * (((exp2(_18_m0[1u].w * log2((_151.x + 0.05499267578125f) * 0.9482421875f)) + exp2(_18_m0[1u].w * log2((_139.x + 0.05499267578125f) * 0.9482421875f))) + exp2(_18_m0[1u].w * log2((_168.x + 0.05499267578125f) * 0.9482421875f))) + exp2(_18_m0[1u].w * log2((_173.x + 0.05499267578125f) * 0.9482421875f)))) + exp2(_18_m0[1u].w * log2((_156.x + 0.05499267578125f) * 0.9482421875f))) * _323; - float _1003 = isnan(0.0f) ? _329 : (isnan(_329) ? 0.0f : max(_329, 0.0f)); - float _336 = ((_316 * (((_214 + _194) + _254) + _274)) + _234) * _323; - float _1014 = isnan(0.0f) ? _336 : (isnan(_336) ? 0.0f : max(_336, 0.0f)); - float _343 = ((_316 * (((exp2(log2((_151.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w) + exp2(log2((_139.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_168.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_173.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w))) + exp2(log2((_156.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _323; - float _1025 = isnan(0.0f) ? _343 : (isnan(_343) ? 0.0f : max(_343, 0.0f)); - float _345 = isnan(0.00100040435791015625f) ? _18_m0[1u].w : (isnan(_18_m0[1u].w) ? 0.00100040435791015625f : max(_18_m0[1u].w, 0.00100040435791015625f)); - if ((_58 <= _55.z) && (_59 <= _55.w)) - { - float _388 = (((((exp2(log2((_106.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w) + exp2(log2((_91.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_119.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_128.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _315) + exp2(log2((_109.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _322; - float _1041 = isnan(0.0f) ? _388 : (isnan(_388) ? 0.0f : max(_388, 0.0f)); - float _391 = 1.0f / _345; - float _396 = (exp2(_391 * log2(isnan(1.0f) ? _1041 : (isnan(_1041) ? 1.0f : min(_1041, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - float _404 = ((_315 * (((_213 + _193) + _253) + _273)) + _233) * _322; - float _1057 = isnan(0.0f) ? _404 : (isnan(_404) ? 0.0f : max(_404, 0.0f)); - float _410 = (exp2(_391 * log2(isnan(1.0f) ? _1057 : (isnan(_1057) ? 1.0f : min(_1057, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - float _447 = (((((exp2(log2((_106.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w) + exp2(log2((_91.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_119.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_128.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _315) + exp2(log2((_109.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _322; - float _1073 = isnan(0.0f) ? _447 : (isnan(_447) ? 0.0f : max(_447, 0.0f)); - float _453 = (exp2(_391 * log2(isnan(1.0f) ? _1073 : (isnan(_1073) ? 1.0f : min(_1073, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - _11[uint2(_58, _59)] = float4(isnan(0.0f) ? _453 : (isnan(_453) ? 0.0f : max(_453, 0.0f)), isnan(0.0f) ? _410 : (isnan(_410) ? 0.0f : max(_410, 0.0f)), isnan(0.0f) ? _396 : (isnan(_396) ? 0.0f : max(_396, 0.0f)), 1.0f); - } - uint _458 = _58 + 8u; - uint4 _461 = asuint(_23_m0[2u]); - if ((_458 <= _461.z) && (_59 <= _461.w)) - { - float _468 = 1.0f / _345; - float _472 = (exp2(_468 * log2(isnan(1.0f) ? _1025 : (isnan(_1025) ? 1.0f : min(_1025, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - float _478 = (exp2(_468 * log2(isnan(1.0f) ? _1014 : (isnan(_1014) ? 1.0f : min(_1014, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - float _484 = (exp2(_468 * log2(isnan(1.0f) ? _1003 : (isnan(_1003) ? 1.0f : min(_1003, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - _11[uint2(_458, _59)] = float4(isnan(0.0f) ? _484 : (isnan(_484) ? 0.0f : max(_484, 0.0f)), isnan(0.0f) ? _478 : (isnan(_478) ? 0.0f : max(_478, 0.0f)), isnan(0.0f) ? _472 : (isnan(_472) ? 0.0f : max(_472, 0.0f)), 1.0f); - } - uint _489 = _59 + 8u; - uint4 _492 = asuint(_23_m0[3u]); - uint _493 = _492.x; - uint _494 = _492.y; - uint _495 = _492.z; - uint _496 = _492.w; - uint _497 = _489 << 16u; - uint _505 = uint(int(max(min(_76, _495), _493) << 16u) >> int(16u)); - uint _507 = uint(int(max(min(uint(int(_497 + 4294901760u) >> int(16u)), _496), _494) << 16u) >> int(16u)); - float4 _509 = _8.Load(int3(uint2(_505, _507), 0u)); - uint _520 = uint(int(max(min(uint(int(_497) >> int(16u)), _496), _494) << 16u) >> int(16u)); - float4 _521 = _8.Load(int3(uint2(uint(int(max(min(_96, _495), _493) << 16u) >> int(16u)), _520), 0u)); - float4 _524 = _8.Load(int3(uint2(_505, _520), 0u)); - float4 _531 = _8.Load(int3(uint2(uint(int(max(min(_114, _495), _493) << 16u) >> int(16u)), _520), 0u)); - uint _539 = uint(int(max(min(uint(int(_497 + 65536u) >> int(16u)), _496), _494) << 16u) >> int(16u)); - float4 _540 = _8.Load(int3(uint2(_505, _539), 0u)); - uint _546 = uint(int(max(min(_134, _495), _493) << 16u) >> int(16u)); - float4 _547 = _8.Load(int3(uint2(_546, _507), 0u)); - float4 _556 = _8.Load(int3(uint2(uint(int(max(min(_146, _495), _493) << 16u) >> int(16u)), _520), 0u)); - float4 _561 = _8.Load(int3(uint2(_546, _520), 0u)); - float4 _570 = _8.Load(int3(uint2(uint(int(max(min(_163, _495), _493) << 16u) >> int(16u)), _520), 0u)); - float4 _575 = _8.Load(int3(uint2(_546, _539), 0u)); - float _596 = exp2(log2((_509.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _597 = exp2(log2((_547.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _616 = exp2(log2((_521.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _617 = exp2(log2((_556.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _636 = exp2(log2((_524.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _637 = exp2(log2((_561.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _656 = exp2(log2((_531.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _657 = exp2(log2((_570.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _676 = exp2(log2((_540.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _677 = exp2(log2((_575.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _683 = isnan(_616) ? _596 : (isnan(_596) ? _616 : min(_596, _616)); - float _684 = isnan(_617) ? _597 : (isnan(_597) ? _617 : min(_597, _617)); - float _685 = isnan(_636) ? _683 : (isnan(_683) ? _636 : min(_683, _636)); - float _686 = isnan(_637) ? _684 : (isnan(_684) ? _637 : min(_684, _637)); - float _687 = isnan(_676) ? _656 : (isnan(_656) ? _676 : min(_656, _676)); - float _688 = isnan(_677) ? _657 : (isnan(_657) ? _677 : min(_657, _677)); - float _689 = isnan(_685) ? _687 : (isnan(_687) ? _685 : min(_687, _685)); - float _690 = isnan(_686) ? _688 : (isnan(_688) ? _686 : min(_688, _686)); - float _691 = isnan(_616) ? _596 : (isnan(_596) ? _616 : max(_596, _616)); - float _692 = isnan(_617) ? _597 : (isnan(_597) ? _617 : max(_597, _617)); - float _693 = isnan(_636) ? _691 : (isnan(_691) ? _636 : max(_691, _636)); - float _694 = isnan(_637) ? _692 : (isnan(_692) ? _637 : max(_692, _637)); - float _695 = isnan(_676) ? _656 : (isnan(_656) ? _676 : max(_656, _676)); - float _696 = isnan(_677) ? _657 : (isnan(_657) ? _677 : max(_657, _677)); - float _697 = isnan(_693) ? _695 : (isnan(_695) ? _693 : max(_695, _693)); - float _698 = isnan(_694) ? _696 : (isnan(_696) ? _694 : max(_696, _694)); - float _701 = 1.0f - _697; - float _702 = 1.0f - _698; - float _705 = (isnan(_701) ? _689 : (isnan(_689) ? _701 : min(_689, _701))) * (1.0f / _697); - float _706 = (isnan(_702) ? _690 : (isnan(_690) ? _702 : min(_690, _702))) * (1.0f / _698); - float _1194 = isnan(0.0f) ? _705 : (isnan(_705) ? 0.0f : max(_705, 0.0f)); - float _1205 = isnan(0.0f) ? _706 : (isnan(_706) ? 0.0f : max(_706, 0.0f)); - float _711 = _314 * sqrt(isnan(1.0f) ? _1194 : (isnan(_1194) ? 1.0f : min(_1194, 1.0f))); - float _712 = _314 * sqrt(isnan(1.0f) ? _1205 : (isnan(_1205) ? 1.0f : min(_1205, 1.0f))); - float _717 = 1.0f / ((_711 * 4.0f) + 1.0f); - float _718 = 1.0f / ((_712 * 4.0f) + 1.0f); - float _724 = ((_712 * (((exp2(_18_m0[1u].w * log2((_556.x + 0.05499267578125f) * 0.9482421875f)) + exp2(_18_m0[1u].w * log2((_547.x + 0.05499267578125f) * 0.9482421875f))) + exp2(_18_m0[1u].w * log2((_570.x + 0.05499267578125f) * 0.9482421875f))) + exp2(_18_m0[1u].w * log2((_575.x + 0.05499267578125f) * 0.9482421875f)))) + exp2(_18_m0[1u].w * log2((_561.x + 0.05499267578125f) * 0.9482421875f))) * _718; - float _1216 = isnan(0.0f) ? _724 : (isnan(_724) ? 0.0f : max(_724, 0.0f)); - float _731 = ((_712 * (((_617 + _597) + _657) + _677)) + _637) * _718; - float _1227 = isnan(0.0f) ? _731 : (isnan(_731) ? 0.0f : max(_731, 0.0f)); - float _738 = ((_712 * (((exp2(log2((_556.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w) + exp2(log2((_547.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_570.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_575.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w))) + exp2(log2((_561.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _718; - float _1238 = isnan(0.0f) ? _738 : (isnan(_738) ? 0.0f : max(_738, 0.0f)); - uint4 _742 = asuint(_23_m0[2u]); - if ((_58 <= _742.z) && (_489 <= _742.w)) - { - float _783 = (((((exp2(log2((_521.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w) + exp2(log2((_509.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_531.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_540.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _711) + exp2(log2((_524.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _717; - float _1249 = isnan(0.0f) ? _783 : (isnan(_783) ? 0.0f : max(_783, 0.0f)); - float _786 = 1.0f / _345; - float _790 = (exp2(_786 * log2(isnan(1.0f) ? _1249 : (isnan(_1249) ? 1.0f : min(_1249, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - float _797 = ((_711 * (((_616 + _596) + _656) + _676)) + _636) * _717; - float _1265 = isnan(0.0f) ? _797 : (isnan(_797) ? 0.0f : max(_797, 0.0f)); - float _803 = (exp2(_786 * log2(isnan(1.0f) ? _1265 : (isnan(_1265) ? 1.0f : min(_1265, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - float _840 = (((((exp2(log2((_521.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w) + exp2(log2((_509.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_531.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_540.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _711) + exp2(log2((_524.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _717; - float _1281 = isnan(0.0f) ? _840 : (isnan(_840) ? 0.0f : max(_840, 0.0f)); - float _846 = (exp2(_786 * log2(isnan(1.0f) ? _1281 : (isnan(_1281) ? 1.0f : min(_1281, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - _11[uint2(_58, _489)] = float4(isnan(0.0f) ? _846 : (isnan(_846) ? 0.0f : max(_846, 0.0f)), isnan(0.0f) ? _803 : (isnan(_803) ? 0.0f : max(_803, 0.0f)), isnan(0.0f) ? _790 : (isnan(_790) ? 0.0f : max(_790, 0.0f)), 1.0f); - } - uint4 _853 = asuint(_23_m0[2u]); - if ((_458 <= _853.z) && (_489 <= _853.w)) - { - float _860 = 1.0f / _345; - float _864 = (exp2(_860 * log2(isnan(1.0f) ? _1238 : (isnan(_1238) ? 1.0f : min(_1238, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - float _870 = (exp2(_860 * log2(isnan(1.0f) ? _1227 : (isnan(_1227) ? 1.0f : min(_1227, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - float _876 = (exp2(_860 * log2(isnan(1.0f) ? _1216 : (isnan(_1216) ? 1.0f : min(_1216, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - _11[uint2(_458, _489)] = float4(isnan(0.0f) ? _876 : (isnan(_876) ? 0.0f : max(_876, 0.0f)), isnan(0.0f) ? _870 : (isnan(_870) ? 0.0f : max(_870, 0.0f)), isnan(0.0f) ? _864 : (isnan(_864) ? 0.0f : max(_864, 0.0f)), 1.0f); - } + uint4 _55 = asuint(_23_m0[2u]); + uint _58 = _55.x + (((gl_LocalInvocationID.x >> 1u) & 7u) | (gl_WorkGroupID.x << 4u)); + uint _59 = ((((gl_LocalInvocationID.x >> 3u) & 6u) | (gl_LocalInvocationID.x & 1u)) | (gl_WorkGroupID.y << 4u)) + _55.y; + uint4 _69 = asuint(_23_m0[3u]); + uint _70 = _69.x; + uint _71 = _69.y; + uint _72 = _69.z; + uint _73 = _69.w; + uint _74 = _58 << 16u; + uint _76 = uint(int(_74) >> int(16u)); + uint _77 = _59 << 16u; + uint _87 = uint(int(max(min(_76, _72), _70) << 16u) >> int(16u)); + uint _89 = uint(int(max(min(uint(int(_77 + 4294901760u) >> int(16u)), _73), _71) << 16u) >> int(16u)); + float4 _91 = _8.Load(int3(uint2(_87, _89), 0u)); + uint _96 = uint(int(_74 + 4294901760u) >> int(16u)); + uint _105 = uint(int(max(min(uint(int(_77) >> int(16u)), _73), _71) << 16u) >> int(16u)); + float4 _106 = _8.Load(int3(uint2(uint(int(max(min(_96, _72), _70) << 16u) >> int(16u)), _105), 0u)); + float4 _109 = _8.Load(int3(uint2(_87, _105), 0u)); + uint _114 = uint(int(_74 + 65536u) >> int(16u)); + float4 _119 = _8.Load(int3(uint2(uint(int(max(min(_114, _72), _70) << 16u) >> int(16u)), _105), 0u)); + uint _127 = uint(int(max(min(uint(int(_77 + 65536u) >> int(16u)), _73), _71) << 16u) >> int(16u)); + float4 _128 = _8.Load(int3(uint2(_87, _127), 0u)); + uint _134 = uint(int((_58 << 16u) + 524288u) >> int(16u)); + uint _138 = uint(int(max(min(_134, _72), _70) << 16u) >> int(16u)); + float4 _139 = _8.Load(int3(uint2(_138, _89), 0u)); + uint _146 = uint(int(_74 + 458752u) >> int(16u)); + float4 _151 = _8.Load(int3(uint2(uint(int(max(min(_146, _72), _70) << 16u) >> int(16u)), _105), 0u)); + float4 _156 = _8.Load(int3(uint2(_138, _105), 0u)); + uint _163 = uint(int(_74 + 589824u) >> int(16u)); + float4 _168 = _8.Load(int3(uint2(uint(int(max(min(_163, _72), _70) << 16u) >> int(16u)), _105), 0u)); + float4 _173 = _8.Load(int3(uint2(_138, _127), 0u)); + float _193 = exp2(log2((_91.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _194 = exp2(log2((_139.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _213 = exp2(log2((_106.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _214 = exp2(log2((_151.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _233 = exp2(log2((_109.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _234 = exp2(log2((_156.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _253 = exp2(log2((_119.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _254 = exp2(log2((_168.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _273 = exp2(log2((_128.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _274 = exp2(log2((_173.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _280 = isnan(_213) ? _193 : (isnan(_193) ? _213 : min(_193, _213)); + float _281 = isnan(_214) ? _194 : (isnan(_194) ? _214 : min(_194, _214)); + float _282 = isnan(_233) ? _280 : (isnan(_280) ? _233 : min(_280, _233)); + float _283 = isnan(_234) ? _281 : (isnan(_281) ? _234 : min(_281, _234)); + float _284 = isnan(_273) ? _253 : (isnan(_253) ? _273 : min(_253, _273)); + float _285 = isnan(_274) ? _254 : (isnan(_254) ? _274 : min(_254, _274)); + float _286 = isnan(_282) ? _284 : (isnan(_284) ? _282 : min(_284, _282)); + float _287 = isnan(_283) ? _285 : (isnan(_285) ? _283 : min(_285, _283)); + float _288 = isnan(_213) ? _193 : (isnan(_193) ? _213 : max(_193, _213)); + float _289 = isnan(_214) ? _194 : (isnan(_194) ? _214 : max(_194, _214)); + float _290 = isnan(_233) ? _288 : (isnan(_288) ? _233 : max(_288, _233)); + float _291 = isnan(_234) ? _289 : (isnan(_289) ? _234 : max(_289, _234)); + float _292 = isnan(_273) ? _253 : (isnan(_253) ? _273 : max(_253, _273)); + float _293 = isnan(_274) ? _254 : (isnan(_254) ? _274 : max(_254, _274)); + float _294 = isnan(_290) ? _292 : (isnan(_292) ? _290 : max(_292, _290)); + float _295 = isnan(_291) ? _293 : (isnan(_293) ? _291 : max(_293, _291)); + float _299 = 1.0f - _294; + float _300 = 1.0f - _295; + float _303 = (isnan(_299) ? _286 : (isnan(_286) ? _299 : min(_286, _299))) * (1.0f / _294); + float _304 = (isnan(_300) ? _287 : (isnan(_287) ? _300 : min(_287, _300))) * (1.0f / _295); + float _981 = isnan(0.0f) ? _303 : (isnan(_303) ? 0.0f : max(_303, 0.0f)); + float _992 = isnan(0.0f) ? _304 : (isnan(_304) ? 0.0f : max(_304, 0.0f)); + float2 _313 = spvUnpackHalf2x16(asuint(_23_m0[1u]).y & 65535u); + float _314 = _313.x; + float _315 = _314 * sqrt(isnan(1.0f) ? _981 : (isnan(_981) ? 1.0f : min(_981, 1.0f))); + float _316 = _314 * sqrt(isnan(1.0f) ? _992 : (isnan(_992) ? 1.0f : min(_992, 1.0f))); + float _322 = 1.0f / ((_315 * 4.0f) + 1.0f); + float _323 = 1.0f / ((_316 * 4.0f) + 1.0f); + float _329 = ((_316 * (((exp2(_18_m0[1u].w * log2((_151.x + 0.05499267578125f) * 0.9482421875f)) + exp2(_18_m0[1u].w * log2((_139.x + 0.05499267578125f) * 0.9482421875f))) + exp2(_18_m0[1u].w * log2((_168.x + 0.05499267578125f) * 0.9482421875f))) + exp2(_18_m0[1u].w * log2((_173.x + 0.05499267578125f) * 0.9482421875f)))) + exp2(_18_m0[1u].w * log2((_156.x + 0.05499267578125f) * 0.9482421875f))) * _323; + float _1003 = isnan(0.0f) ? _329 : (isnan(_329) ? 0.0f : max(_329, 0.0f)); + float _336 = ((_316 * (((_214 + _194) + _254) + _274)) + _234) * _323; + float _1014 = isnan(0.0f) ? _336 : (isnan(_336) ? 0.0f : max(_336, 0.0f)); + float _343 = ((_316 * (((exp2(log2((_151.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w) + exp2(log2((_139.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_168.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_173.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w))) + exp2(log2((_156.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _323; + float _1025 = isnan(0.0f) ? _343 : (isnan(_343) ? 0.0f : max(_343, 0.0f)); + float _345 = isnan(0.00100040435791015625f) ? _18_m0[1u].w : (isnan(_18_m0[1u].w) ? 0.00100040435791015625f : max(_18_m0[1u].w, 0.00100040435791015625f)); + if ((_58 <= _55.z) && (_59 <= _55.w)) + { + float _388 = (((((exp2(log2((_106.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w) + exp2(log2((_91.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_119.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_128.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _315) + exp2(log2((_109.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _322; + float _1041 = isnan(0.0f) ? _388 : (isnan(_388) ? 0.0f : max(_388, 0.0f)); + float _391 = 1.0f / _345; + float _396 = (exp2(_391 * log2(isnan(1.0f) ? _1041 : (isnan(_1041) ? 1.0f : min(_1041, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); + float _404 = ((_315 * (((_213 + _193) + _253) + _273)) + _233) * _322; + float _1057 = isnan(0.0f) ? _404 : (isnan(_404) ? 0.0f : max(_404, 0.0f)); + float _410 = (exp2(_391 * log2(isnan(1.0f) ? _1057 : (isnan(_1057) ? 1.0f : min(_1057, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); + float _447 = (((((exp2(log2((_106.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w) + exp2(log2((_91.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_119.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_128.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _315) + exp2(log2((_109.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _322; + float _1073 = isnan(0.0f) ? _447 : (isnan(_447) ? 0.0f : max(_447, 0.0f)); + float _453 = (exp2(_391 * log2(isnan(1.0f) ? _1073 : (isnan(_1073) ? 1.0f : min(_1073, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); + _11[uint2(_58, _59)] = float4(isnan(0.0f) ? _453 : (isnan(_453) ? 0.0f : max(_453, 0.0f)), isnan(0.0f) ? _410 : (isnan(_410) ? 0.0f : max(_410, 0.0f)), isnan(0.0f) ? _396 : (isnan(_396) ? 0.0f : max(_396, 0.0f)), 1.0f); + } + uint _458 = _58 + 8u; + uint4 _461 = asuint(_23_m0[2u]); + if ((_458 <= _461.z) && (_59 <= _461.w)) + { + float _468 = 1.0f / _345; + float _472 = (exp2(_468 * log2(isnan(1.0f) ? _1025 : (isnan(_1025) ? 1.0f : min(_1025, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); + float _478 = (exp2(_468 * log2(isnan(1.0f) ? _1014 : (isnan(_1014) ? 1.0f : min(_1014, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); + float _484 = (exp2(_468 * log2(isnan(1.0f) ? _1003 : (isnan(_1003) ? 1.0f : min(_1003, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); + _11[uint2(_458, _59)] = float4(isnan(0.0f) ? _484 : (isnan(_484) ? 0.0f : max(_484, 0.0f)), isnan(0.0f) ? _478 : (isnan(_478) ? 0.0f : max(_478, 0.0f)), isnan(0.0f) ? _472 : (isnan(_472) ? 0.0f : max(_472, 0.0f)), 1.0f); + } + uint _489 = _59 + 8u; + uint4 _492 = asuint(_23_m0[3u]); + uint _493 = _492.x; + uint _494 = _492.y; + uint _495 = _492.z; + uint _496 = _492.w; + uint _497 = _489 << 16u; + uint _505 = uint(int(max(min(_76, _495), _493) << 16u) >> int(16u)); + uint _507 = uint(int(max(min(uint(int(_497 + 4294901760u) >> int(16u)), _496), _494) << 16u) >> int(16u)); + float4 _509 = _8.Load(int3(uint2(_505, _507), 0u)); + uint _520 = uint(int(max(min(uint(int(_497) >> int(16u)), _496), _494) << 16u) >> int(16u)); + float4 _521 = _8.Load(int3(uint2(uint(int(max(min(_96, _495), _493) << 16u) >> int(16u)), _520), 0u)); + float4 _524 = _8.Load(int3(uint2(_505, _520), 0u)); + float4 _531 = _8.Load(int3(uint2(uint(int(max(min(_114, _495), _493) << 16u) >> int(16u)), _520), 0u)); + uint _539 = uint(int(max(min(uint(int(_497 + 65536u) >> int(16u)), _496), _494) << 16u) >> int(16u)); + float4 _540 = _8.Load(int3(uint2(_505, _539), 0u)); + uint _546 = uint(int(max(min(_134, _495), _493) << 16u) >> int(16u)); + float4 _547 = _8.Load(int3(uint2(_546, _507), 0u)); + float4 _556 = _8.Load(int3(uint2(uint(int(max(min(_146, _495), _493) << 16u) >> int(16u)), _520), 0u)); + float4 _561 = _8.Load(int3(uint2(_546, _520), 0u)); + float4 _570 = _8.Load(int3(uint2(uint(int(max(min(_163, _495), _493) << 16u) >> int(16u)), _520), 0u)); + float4 _575 = _8.Load(int3(uint2(_546, _539), 0u)); + float _596 = exp2(log2((_509.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _597 = exp2(log2((_547.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _616 = exp2(log2((_521.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _617 = exp2(log2((_556.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _636 = exp2(log2((_524.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _637 = exp2(log2((_561.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _656 = exp2(log2((_531.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _657 = exp2(log2((_570.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _676 = exp2(log2((_540.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _677 = exp2(log2((_575.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); + float _683 = isnan(_616) ? _596 : (isnan(_596) ? _616 : min(_596, _616)); + float _684 = isnan(_617) ? _597 : (isnan(_597) ? _617 : min(_597, _617)); + float _685 = isnan(_636) ? _683 : (isnan(_683) ? _636 : min(_683, _636)); + float _686 = isnan(_637) ? _684 : (isnan(_684) ? _637 : min(_684, _637)); + float _687 = isnan(_676) ? _656 : (isnan(_656) ? _676 : min(_656, _676)); + float _688 = isnan(_677) ? _657 : (isnan(_657) ? _677 : min(_657, _677)); + float _689 = isnan(_685) ? _687 : (isnan(_687) ? _685 : min(_687, _685)); + float _690 = isnan(_686) ? _688 : (isnan(_688) ? _686 : min(_688, _686)); + float _691 = isnan(_616) ? _596 : (isnan(_596) ? _616 : max(_596, _616)); + float _692 = isnan(_617) ? _597 : (isnan(_597) ? _617 : max(_597, _617)); + float _693 = isnan(_636) ? _691 : (isnan(_691) ? _636 : max(_691, _636)); + float _694 = isnan(_637) ? _692 : (isnan(_692) ? _637 : max(_692, _637)); + float _695 = isnan(_676) ? _656 : (isnan(_656) ? _676 : max(_656, _676)); + float _696 = isnan(_677) ? _657 : (isnan(_657) ? _677 : max(_657, _677)); + float _697 = isnan(_693) ? _695 : (isnan(_695) ? _693 : max(_695, _693)); + float _698 = isnan(_694) ? _696 : (isnan(_696) ? _694 : max(_696, _694)); + float _701 = 1.0f - _697; + float _702 = 1.0f - _698; + float _705 = (isnan(_701) ? _689 : (isnan(_689) ? _701 : min(_689, _701))) * (1.0f / _697); + float _706 = (isnan(_702) ? _690 : (isnan(_690) ? _702 : min(_690, _702))) * (1.0f / _698); + float _1194 = isnan(0.0f) ? _705 : (isnan(_705) ? 0.0f : max(_705, 0.0f)); + float _1205 = isnan(0.0f) ? _706 : (isnan(_706) ? 0.0f : max(_706, 0.0f)); + float _711 = _314 * sqrt(isnan(1.0f) ? _1194 : (isnan(_1194) ? 1.0f : min(_1194, 1.0f))); + float _712 = _314 * sqrt(isnan(1.0f) ? _1205 : (isnan(_1205) ? 1.0f : min(_1205, 1.0f))); + float _717 = 1.0f / ((_711 * 4.0f) + 1.0f); + float _718 = 1.0f / ((_712 * 4.0f) + 1.0f); + float _724 = ((_712 * (((exp2(_18_m0[1u].w * log2((_556.x + 0.05499267578125f) * 0.9482421875f)) + exp2(_18_m0[1u].w * log2((_547.x + 0.05499267578125f) * 0.9482421875f))) + exp2(_18_m0[1u].w * log2((_570.x + 0.05499267578125f) * 0.9482421875f))) + exp2(_18_m0[1u].w * log2((_575.x + 0.05499267578125f) * 0.9482421875f)))) + exp2(_18_m0[1u].w * log2((_561.x + 0.05499267578125f) * 0.9482421875f))) * _718; + float _1216 = isnan(0.0f) ? _724 : (isnan(_724) ? 0.0f : max(_724, 0.0f)); + float _731 = ((_712 * (((_617 + _597) + _657) + _677)) + _637) * _718; + float _1227 = isnan(0.0f) ? _731 : (isnan(_731) ? 0.0f : max(_731, 0.0f)); + float _738 = ((_712 * (((exp2(log2((_556.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w) + exp2(log2((_547.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_570.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_575.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w))) + exp2(log2((_561.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _718; + float _1238 = isnan(0.0f) ? _738 : (isnan(_738) ? 0.0f : max(_738, 0.0f)); + uint4 _742 = asuint(_23_m0[2u]); + if ((_58 <= _742.z) && (_489 <= _742.w)) + { + float _783 = (((((exp2(log2((_521.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w) + exp2(log2((_509.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_531.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_540.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _711) + exp2(log2((_524.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _717; + float _1249 = isnan(0.0f) ? _783 : (isnan(_783) ? 0.0f : max(_783, 0.0f)); + float _786 = 1.0f / _345; + float _790 = (exp2(_786 * log2(isnan(1.0f) ? _1249 : (isnan(_1249) ? 1.0f : min(_1249, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); + float _797 = ((_711 * (((_616 + _596) + _656) + _676)) + _636) * _717; + float _1265 = isnan(0.0f) ? _797 : (isnan(_797) ? 0.0f : max(_797, 0.0f)); + float _803 = (exp2(_786 * log2(isnan(1.0f) ? _1265 : (isnan(_1265) ? 1.0f : min(_1265, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); + float _840 = (((((exp2(log2((_521.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w) + exp2(log2((_509.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_531.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_540.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _711) + exp2(log2((_524.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _717; + float _1281 = isnan(0.0f) ? _840 : (isnan(_840) ? 0.0f : max(_840, 0.0f)); + float _846 = (exp2(_786 * log2(isnan(1.0f) ? _1281 : (isnan(_1281) ? 1.0f : min(_1281, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); + _11[uint2(_58, _489)] = float4(isnan(0.0f) ? _846 : (isnan(_846) ? 0.0f : max(_846, 0.0f)), isnan(0.0f) ? _803 : (isnan(_803) ? 0.0f : max(_803, 0.0f)), isnan(0.0f) ? _790 : (isnan(_790) ? 0.0f : max(_790, 0.0f)), 1.0f); + } + uint4 _853 = asuint(_23_m0[2u]); + if ((_458 <= _853.z) && (_489 <= _853.w)) + { + float _860 = 1.0f / _345; + float _864 = (exp2(_860 * log2(isnan(1.0f) ? _1238 : (isnan(_1238) ? 1.0f : min(_1238, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); + float _870 = (exp2(_860 * log2(isnan(1.0f) ? _1227 : (isnan(_1227) ? 1.0f : min(_1227, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); + float _876 = (exp2(_860 * log2(isnan(1.0f) ? _1216 : (isnan(_1216) ? 1.0f : min(_1216, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); + _11[uint2(_458, _489)] = float4(isnan(0.0f) ? _876 : (isnan(_876) ? 0.0f : max(_876, 0.0f)), isnan(0.0f) ? _870 : (isnan(_870) ? 0.0f : max(_870, 0.0f)), isnan(0.0f) ? _864 : (isnan(_864) ? 0.0f : max(_864, 0.0f)), 1.0f); + } #endif } [numthreads(64, 1, 1)] void main(SPIRV_Cross_Input stage_input) { - gl_WorkGroupID = stage_input.gl_WorkGroupID; - gl_LocalInvocationID = stage_input.gl_LocalInvocationID; - comp_main(); + gl_WorkGroupID = stage_input.gl_WorkGroupID; + gl_LocalInvocationID = stage_input.gl_LocalInvocationID; + comp_main(); } diff --git a/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl b/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl index df3f5b14..e5d70c60 100644 --- a/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl +++ b/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl @@ -5,7 +5,7 @@ cbuffer _13_15 : register(b0, space0) { - float4 filmGrainColorAndIntensity : packoffset(c0); + float4 filmGrainColorAndIntensity : packoffset(c0); }; Texture2D TonemappedColorTexture : register(t0, space8); // Possibly in gamma space in SDR @@ -16,23 +16,23 @@ static float4 SV_Target; struct SPIRV_Cross_Input { - float4 TEXCOORD : TEXCOORD0; + float4 TEXCOORD : TEXCOORD0; }; struct SPIRV_Cross_Output { - float4 SV_Target : SV_Target0; + float4 SV_Target : SV_Target0; }; // Better? random generator float rand(float2 uv) { - return frac(sin(dot(uv,float2(12.9898,78.233)))*43758.5453123); + return frac(sin(dot(uv,float2(12.9898,78.233)))*43758.5453123); } // Returns a random number between 0 and 1 based on seed float bethesdaRandom(float seed) { - return frac(sin(seed) * 493013.f); + return frac(sin(seed) * 493013.f); } @@ -46,77 +46,77 @@ void frag_main() // .y = randomInt32? changed every frame? // .z = strength? maybe 0.03 - float3 tonemappedColor = TonemappedColorTexture.Sample(Sampler0, float2(TEXCOORD.x, TEXCOORD.y)); + float3 tonemappedColor = TonemappedColorTexture.Sample(Sampler0, float2(TEXCOORD.x, TEXCOORD.y)); #if ENABLE_HDR - tonemappedColor /= HDR_GAME_PAPER_WHITE; - tonemappedColor = gamma_linear_to_sRGB(tonemappedColor); - float inverseLuminance = saturate(1.f - Luminance(tonemappedColor)); + tonemappedColor /= HDR_GAME_PAPER_WHITE; + tonemappedColor = gamma_linear_to_sRGB(tonemappedColor); + float inverseLuminance = saturate(1.f - Luminance(tonemappedColor)); #else - float inverseLuminance = saturate(1.f - Luminance(tonemappedColor)); + float inverseLuminance = saturate(1.f - Luminance(tonemappedColor)); #endif // ENABLE_HDR #if 0 // WIP fixes - const float filmGrainInvSize = 1.f / FILM_GRAIN_TEXTURE_SIZE; // This was "1.f / (FILM_GRAIN_TEXTURE_SIZE - 1u)", though that seems incorrect - const float filmGrainHalfSize = FILM_GRAIN_TEXTURE_SIZE * 0.5f; // This was "521.f", which seems like a mistake on 512 + const float filmGrainInvSize = 1.f / FILM_GRAIN_TEXTURE_SIZE; // This was "1.f / (FILM_GRAIN_TEXTURE_SIZE - 1u)", though that seems incorrect + const float filmGrainHalfSize = FILM_GRAIN_TEXTURE_SIZE * 0.5f; // This was "521.f", which seems like a mistake on 512 #else - const float filmGrainInvSize = 1.f / (FILM_GRAIN_TEXTURE_SIZE - 1u); - const float filmGrainHalfSize = 521.f; //TODO: rename in case we keep this as 521 + const float filmGrainInvSize = 1.f / (FILM_GRAIN_TEXTURE_SIZE - 1u); + const float filmGrainHalfSize = 521.f; //TODO: rename in case we keep this as 521 #endif - // Applying modulus against 1023 will give value between 0 and 1023 - // Same two values for all texels this frame - float randomFromRange1 = float(int(uint(filmGrainColorAndIntensity.x) & (FILM_GRAIN_TEXTURE_SIZE - 1u))); - float randomFromRange2 = float(int(uint(filmGrainColorAndIntensity.y) & (FILM_GRAIN_TEXTURE_SIZE - 1u))); - - // Divide by 1023 to get a number between 0 and 1 - float randomNormalized1 = randomFromRange1 * filmGrainInvSize; - float randomNormalized2 = randomFromRange1 * filmGrainInvSize; - - // Offset by x/y position - float uniqueX = randomNormalized1 + TEXCOORD.x; - float uniqueY = (randomNormalized2 + TEXCOORD.y) * filmGrainHalfSize; - - // Unique each frame and texel. (Is it though?) - float seed = (uniqueX + uniqueY); - - // Generate a random number between -1 and -1; - // luminanceShift at -1 is black, 0 unchanged, 1 negative of current texel - float luminanceShift = (bethesdaRandom(seed) * 2.f) - 1.f; + // Applying modulus against 1023 will give value between 0 and 1023 + // Same two values for all texels this frame + float randomFromRange1 = float(int(uint(filmGrainColorAndIntensity.x) & (FILM_GRAIN_TEXTURE_SIZE - 1u))); + float randomFromRange2 = float(int(uint(filmGrainColorAndIntensity.y) & (FILM_GRAIN_TEXTURE_SIZE - 1u))); + + // Divide by 1023 to get a number between 0 and 1 + float randomNormalized1 = randomFromRange1 * filmGrainInvSize; + float randomNormalized2 = randomFromRange1 * filmGrainInvSize; + + // Offset by x/y position + float uniqueX = randomNormalized1 + TEXCOORD.x; + float uniqueY = (randomNormalized2 + TEXCOORD.y) * filmGrainHalfSize; + + // Unique each frame and texel. (Is it though?) + float seed = (uniqueX + uniqueY); + + // Generate a random number between -1 and -1; + // luminanceShift at -1 is black, 0 unchanged, 1 negative of current texel + float luminanceShift = (bethesdaRandom(seed) * 2.f) - 1.f; #if 0 // WIP fixes - float additiveFilmGrain = (luminanceShift * 1.f * inverseLuminance); - float3 tonemappedColorWithFilmGrain = float3(additiveFilmGrain, additiveFilmGrain,additiveFilmGrain); + float additiveFilmGrain = (luminanceShift * 1.f * inverseLuminance); + float3 tonemappedColorWithFilmGrain = float3(additiveFilmGrain, additiveFilmGrain,additiveFilmGrain); #else - float additiveFilmGrain = (luminanceShift * filmGrainColorAndIntensity.z * inverseLuminance); - float3 newColor = float3(additiveFilmGrain, additiveFilmGrain,additiveFilmGrain); - - // Use addition to overlay color on top - // Note: we let this possibly generate colors below 1 for scRGB - float3 tonemappedColorWithFilmGrain = tonemappedColor + newColor; + float additiveFilmGrain = (luminanceShift * filmGrainColorAndIntensity.z * inverseLuminance); + float3 newColor = float3(additiveFilmGrain, additiveFilmGrain,additiveFilmGrain); + + // Use addition to overlay color on top + // Note: we let this possibly generate colors below 1 for scRGB + float3 tonemappedColorWithFilmGrain = tonemappedColor + newColor; #endif #if ENABLE_HDR - tonemappedColorWithFilmGrain = gamma_sRGB_to_linear(tonemappedColorWithFilmGrain) * HDR_GAME_PAPER_WHITE; + tonemappedColorWithFilmGrain = gamma_sRGB_to_linear(tonemappedColorWithFilmGrain) * HDR_GAME_PAPER_WHITE; #endif // ENABLE_HDR #if 0 // WIP fixes - // Right side only - if (TEXCOORD.x >= 0.5f) { - SV_Target.rgb = tonemappedColorWithFilmGrain; - } else { - SV_Target.rgb = tonemappedColor; - } + // Right side only + if (TEXCOORD.x >= 0.5f) { + SV_Target.rgb = tonemappedColorWithFilmGrain; + } else { + SV_Target.rgb = tonemappedColor; + } #else - SV_Target.rgb = tonemappedColorWithFilmGrain; + SV_Target.rgb = tonemappedColorWithFilmGrain; #endif - SV_Target.w = 1.0f; + SV_Target.w = 1.0f; } SPIRV_Cross_Output main(SPIRV_Cross_Input stage_input) { - TEXCOORD = stage_input.TEXCOORD; - frag_main(); - SPIRV_Cross_Output stage_output; - stage_output.SV_Target = SV_Target; - return stage_output; -} \ No newline at end of file + TEXCOORD = stage_input.TEXCOORD; + frag_main(); + SPIRV_Cross_Output stage_output; + stage_output.SV_Target = SV_Target; + return stage_output; +} diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 0a1b2a47..cf047c76 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -39,17 +39,17 @@ cbuffer CSharedFrameData : register(b0, space6) { - FrameData SharedFrameData : packoffset(c0); + FrameData SharedFrameData : packoffset(c0); }; cbuffer CPerSceneConstants : register(b0, space7) { - float4 PerSceneConstants[3269] : packoffset(c0); + float4 PerSceneConstants[3269] : packoffset(c0); }; cbuffer CPushConstantWrapper_HDRComposite : register(b0, space0) { - PushConstantWrapper_HDRComposite PcwHdrComposite : packoffset(c0); + PushConstantWrapper_HDRComposite PcwHdrComposite : packoffset(c0); }; @@ -78,13 +78,13 @@ SamplerState Sampler0 : register(s0, space9); // Clamped Bilinear struct PSInput { - float4 SV_Position : SV_Position0; - float2 TEXCOORD : TEXCOORD0; + float4 SV_Position : SV_Position0; + float2 TEXCOORD : TEXCOORD0; }; struct PSOutput { - float4 SV_Target : SV_Target0; + float4 SV_Target : SV_Target0; }; // 1.1920928955078125e-07 @@ -116,738 +116,791 @@ static const float ACES_d = 0.59f; static const float ACES_e = 0.14f; -float3 ACES(float3 Color, bool Clamp, float modE, float modA) +float3 ACES( + float3 Color, + bool Clamp, + float modE, + float modA) { - Color = (Color * (modA * Color + ACES_b)) / ((Color * (ACES_c * Color + ACES_d)) + modE); - return Clamp ? saturate(Color) : Color; + Color = (Color * (modA * Color + ACES_b)) + / ((Color * (ACES_c * Color + ACES_d)) + modE); + + return Clamp ? saturate(Color) : Color; } // ACESFilm by Krzysztof Narkowicz (https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/) // (color * ((a * color) + b)) / (color * ((c * color) + d) + e) -float3 ACESReference(float3 Color, bool Clamp) +float3 ACESReference( + float3 Color, + bool Clamp) { - return ACES(Color, Clamp, ACES_e, ACES_a); + return ACES(Color, Clamp, ACES_e, ACES_a); } // ACESFilm "per scene" -float3 ACESParametric(in float3 Color, in bool Clamp, inout float modE, inout float modA) +float3 ACESParametric( + in float3 Color, + in bool Clamp, + inout float modE, + inout float modA) { - const float AcesParam0 = PerSceneConstants[3266u].x; // Constant is usually 11.2 - const float AcesParam1 = PerSceneConstants[3266u].y; // Constant is usually 0.022 - modE = AcesParam1; - modA = ((0.56f / AcesParam0) + ACES_b) + (AcesParam1 / (AcesParam0 * AcesParam0)); - return ACES(Color, Clamp, modE, modA); + const float AcesParam0 = PerSceneConstants[3266u].x; // Constant is usually 11.2 + const float AcesParam1 = PerSceneConstants[3266u].y; // Constant is usually 0.022 + + modE = AcesParam1; + modA = ((0.56f / AcesParam0) + ACES_b) + (AcesParam1 / (AcesParam0 * AcesParam0)); + + return ACES(Color, Clamp, modE, modA); } template -T ACES_Inverse(T Color, float modE, float modA) +T ACES_Inverse( + T Color, + float modE, + float modA) { - //TODO: does this apply for `ACESParametric` as well? Probably - //TODO: figure out if we could still use the unclamped color. There should be a way to invert it. - // ACES is not defined for any values beyond 0-1 - Color = saturate(Color); + //TODO: does this apply for `ACESParametric` as well? Probably + //TODO: figure out if we could still use the unclamped color. There should be a way to invert it. + // ACES is not defined for any values beyond 0-1 + Color = saturate(Color); - T fixed0 = (-ACES_d * Color) + ACES_b; - T fixed1 = (ACES_c * Color) - modA; + T fixed0 = (-ACES_d * Color) + ACES_b; + T fixed1 = (ACES_c * Color) - modA; - T variable_numerator_part0 = -fixed0; - T variable_numerator = sqrt((variable_numerator_part0 * variable_numerator_part0) - (4.f * modE * Color * fixed1)); + T variable_numerator_part0 = -fixed0; + T variable_numerator = sqrt((variable_numerator_part0 * variable_numerator_part0) - (4.f * modE * Color * fixed1)); - T denominator = 2.f * fixed1; + T denominator = 2.f * fixed1; - T result0 = (fixed0 + variable_numerator) / denominator; - T result1 = (fixed0 - variable_numerator) / denominator; + T result0 = (fixed0 + variable_numerator) / denominator; + T result1 = (fixed0 - variable_numerator) / denominator; - // "result1" is likely what we always want - return max(result0, result1); + // "result1" is likely what we always want + return max(result0, result1); } template T ACESReference_Inverse(T Color) { - return ACES_Inverse(Color, ACES_e, ACES_a); + return ACES_Inverse(Color, ACES_e, ACES_a); } template T ACESParametric_Inverse(T Color, float modE, float modA) { - return ACES_Inverse(Color, modE, modA); + return ACES_Inverse(Color, modE, modA); } // https://github.com/johnhable/fw-public // http://filmicworlds.com/blog/filmic-tonemapping-with-piecewise-power-curves/ // This is NOT the Uncharted 2 tonemapper, which was also by Hable. float3 Hable( - in float3 InputColor, - out HableParamters hableParams) + in float3 InputColor, + out HableParamters hableParams) { - // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L202 + // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L202 - // the 2.2 is so you don't have to input very small numbers - const float toeLength = pow(saturate(PerSceneConstants[3266u].w), 2.2f); // Constant is usually 0.3 - const float toeStrength = saturate(PerSceneConstants[3266u].z); // Constant is usually 0.5 - const float shoulderLength = clamp(saturate(PerSceneConstants[3267u].y), EPSILON, BTHCNST); // Constant is usually 0.8 - const float shoulderStrength = max(PerSceneConstants[3267u].x, 0.f); // Constant is usually 9.9 - const float shoulderAngle = saturate(PerSceneConstants[3267u].z); // Constant is usually 0.3 + // the 2.2 is so you don't have to input very small numbers + const float toeLength = pow(saturate(PerSceneConstants[3266u].w), 2.2f); // Constant is usually 0.3 + const float toeStrength = saturate(PerSceneConstants[3266u].z); // Constant is usually 0.5 + const float shoulderLength = clamp(saturate(PerSceneConstants[3267u].y), EPSILON, BTHCNST); // Constant is usually 0.8 + const float shoulderStrength = max(PerSceneConstants[3267u].x, 0.f); // Constant is usually 9.9 + const float shoulderAngle = saturate(PerSceneConstants[3267u].z); // Constant is usually 0.3 #if ENABLE_TONEMAP_IMPROVEMENTS - //TODO: fix highlights + //TODO: fix highlights #endif - //dstParams - float dstParams_x0 = toeLength * 0.5f; - float dstParams_y0 = (1.f - toeStrength) * dstParams_x0; - - float remainingY = 1.f - dstParams_y0; - - float y1_offset = (1.f - shoulderLength) * remainingY; - //dstParams - float dstParams_x1 = dstParams_x0 + y1_offset; - float dstParams_y1 = dstParams_y0 + y1_offset; - - float extraW = exp2(shoulderStrength) - LFLT1; - float initialW = dstParams_x0 + remainingY; - hableParams.dstParams.W = initialW + extraW; - - // "W * " was optimised away by DXIL->SPIRV->HLSL conversion as down the line there is a "/ W" - float dstParams_overshootX = 2.f * shoulderAngle * shoulderStrength; - float dstParams_overshootY = 0.5f * shoulderAngle * shoulderStrength; - - // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L87 - - //dstCurve - float dstCurve_invW = 1.f / hableParams.dstParams.W; - - //params - float params_x0 = dstParams_x0 / hableParams.dstParams.W; - float params_x1 = dstParams_x1 / hableParams.dstParams.W; - //float params_overshootX = dstParams_overshootX / hableParams.dstParams.W; // this step was optimised away - const float params_overshootX = dstParams_overshootX; - float dx = y1_offset / hableParams.dstParams.W; - - // mid section - // AsSlopeIntercept https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L67 - float m = (abs(dx) < EPSILON) ? 1.f : (y1_offset / dx); - m += EPSILON; - float b = dstParams_y0 - (m * params_x0); - - hableParams.midSegment.offsetX = (b / m); //no minus - float midSegment_lnA_optimised = log2(m); - hableParams.midSegment.lnA = RCP_LOG2_E * midSegment_lnA_optimised; - // mid section end - - // toeM and shoulderM are missing because EvalDerivativeLinearGamma (https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L81-L85) - // evaluates to gamma * m * pow(m * x + b, gamma - 1) with gamma being 1 - // this just returns m - const float toeM = m; - const float shoulderM = m; - - // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L137-L138 - // max(EPSILON, pow(params_yX, gamma)) - hableParams.params.y0 = max(EPSILON, dstParams_y0); // is pow(x, gamma) with gamma = 1 - //OLD: hableParams.params.y0 = max(EPSILON, exp2(log2(dstParams_y0))); - hableParams.params.y1 = max(EPSILON, dstParams_y1); // is pow(x, gamma) with gamma = 1 - //OLD: hableParams.params.y1 = max(EPSILON, exp2(log2(dstParams_y1))); - - // pow(1.f + dstParams_overshootY, gamma) - 1.f - // -1 was optimised away as hableParams.shoulderSegment.offsetY is "params_overshootY + 1" - float params_overshootY = 1.f + dstParams_overshootY; // is pow(x, gamma) with gamma = 1 - //OLD: float params_overshootY = exp2(log2(1.f + dstParams_overshootY)); - - // toe section - // SolveAB https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L60 - hableParams.toeSegment.B = (toeM * params_x0) / (hableParams.params.y0 + EPSILON); - float _410 = log2(hableParams.params.y0); //doesn't belong to SolveAB (it might does) - float toeSegment_lnA_optimised = -hableParams.toeSegment.B * log(params_x0); - hableParams.toeSegment.lnA = _410 * RCP_LOG2_E + toeSegment_lnA_optimised; - // toe section end - - // shoulder section - float shoulderSection_x0 = 1.f + params_overshootX - params_x1; - float shoulderSection_y0 = params_overshootY - hableParams.params.y1; // 1 + x was optimised away - hableParams.shoulderSegment.offsetX = 1.f + params_overshootX; - hableParams.shoulderSegment.offsetY = params_overshootY; // x + 1 was optimised away - // SolveAB - hableParams.shoulderSegment.B = ((shoulderM * shoulderSection_x0) / (shoulderSection_y0 + EPSILON)); - float shoulderSegment_B_optimised = hableParams.shoulderSegment.B * RCP_LOG2_E; - hableParams.shoulderSegment.lnA = (log2(shoulderSection_y0) * RCP_LOG2_E) - (shoulderSegment_B_optimised * log2(shoulderSection_x0)); - // shoulder section end - - // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L6 - // Eval - float evalY0 = 0.f; - if (params_overshootX > 0.f) - { - evalY0 = -exp2(((shoulderSegment_B_optimised * log2(params_overshootX)) + hableParams.shoulderSegment.lnA) * LOG2_E) + params_overshootY; - } - // Eval end - hableParams.invScale = 1.f / evalY0; - - float3 toneMapped; - - for (uint channel = 0; channel < 3; channel++) - { - float normX = InputColor[channel] * dstCurve_invW; - bool isToeSegment = normX < params_x0; - float returnChannel = 0.f; - - if (isToeSegment && normX > 0.f) - { - returnChannel = exp2(((((log2(normX) * hableParams.toeSegment.B) + _410) * RCP_LOG2_E) + toeSegment_lnA_optimised) * LOG2_E); - } - else if (normX < params_x1) - { - float evalMidSegment_y0 = normX + hableParams.midSegment.offsetX; // was -(-hableParams.midSegment.offsetX) - if (evalMidSegment_y0 > 0.f) - { - returnChannel = exp2(log2(evalMidSegment_y0) + midSegment_lnA_optimised); - } - } - else - { - //float evalShoulderSegment_y0 = ((-1.f) - params_overshootX) + normX; // small optimisation from the original decompilation - float evalShoulderSegment_y0 = (1.f + params_overshootX) - normX; - float evalShoulderReturn = 0.f; - if (evalShoulderSegment_y0 > 0.f) - { - evalShoulderReturn = exp2(((shoulderSegment_B_optimised * log2(evalShoulderSegment_y0)) + hableParams.shoulderSegment.lnA) * LOG2_E); - } - returnChannel = params_overshootY - evalShoulderReturn; - } - toneMapped[channel] = returnChannel * hableParams.invScale; - } - return toneMapped; + //dstParams + float dstParams_x0 = toeLength * 0.5f; + float dstParams_y0 = (1.f - toeStrength) * dstParams_x0; + + float remainingY = 1.f - dstParams_y0; + + float y1_offset = (1.f - shoulderLength) * remainingY; + //dstParams + float dstParams_x1 = dstParams_x0 + y1_offset; + float dstParams_y1 = dstParams_y0 + y1_offset; + + float extraW = exp2(shoulderStrength) - LFLT1; + float initialW = dstParams_x0 + remainingY; + hableParams.dstParams.W = initialW + extraW; + + // "W * " was optimised away by DXIL->SPIRV->HLSL conversion as down the line there is a "/ W" + float dstParams_overshootX = 2.f * shoulderAngle * shoulderStrength; + float dstParams_overshootY = 0.5f * shoulderAngle * shoulderStrength; + + // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L87 + + //dstCurve + float dstCurve_invW = 1.f / hableParams.dstParams.W; + + //params + float params_x0 = dstParams_x0 / hableParams.dstParams.W; + float params_x1 = dstParams_x1 / hableParams.dstParams.W; + //float params_overshootX = dstParams_overshootX / hableParams.dstParams.W; // this step was optimised away + const float params_overshootX = dstParams_overshootX; + float dx = y1_offset / hableParams.dstParams.W; + + // mid section + // AsSlopeIntercept https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L67 + float m = (abs(dx) < EPSILON) ? 1.f : (y1_offset / dx); + m += EPSILON; + float b = dstParams_y0 - (m * params_x0); + + hableParams.midSegment.offsetX = (b / m); //no minus + float midSegment_lnA_optimised = log2(m); + hableParams.midSegment.lnA = RCP_LOG2_E * midSegment_lnA_optimised; + // mid section end + + // toeM and shoulderM are missing because EvalDerivativeLinearGamma (https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L81-L85) + // evaluates to gamma * m * pow(m * x + b, gamma - 1) with gamma being 1 + // this just returns m + const float toeM = m; + const float shoulderM = m; + + // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L137-L138 + // max(EPSILON, pow(params_yX, gamma)) + hableParams.params.y0 = max(EPSILON, dstParams_y0); // is pow(x, gamma) with gamma = 1 + //OLD: hableParams.params.y0 = max(EPSILON, exp2(log2(dstParams_y0))); + hableParams.params.y1 = max(EPSILON, dstParams_y1); // is pow(x, gamma) with gamma = 1 + //OLD: hableParams.params.y1 = max(EPSILON, exp2(log2(dstParams_y1))); + + // pow(1.f + dstParams_overshootY, gamma) - 1.f + // -1 was optimised away as hableParams.shoulderSegment.offsetY is "params_overshootY + 1" + float params_overshootY = 1.f + dstParams_overshootY; // is pow(x, gamma) with gamma = 1 + //OLD: float params_overshootY = exp2(log2(1.f + dstParams_overshootY)); + + // toe section + // SolveAB https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L60 + hableParams.toeSegment.B = (toeM * params_x0) / (hableParams.params.y0 + EPSILON); + float _410 = log2(hableParams.params.y0); //doesn't belong to SolveAB (it might does) + float toeSegment_lnA_optimised = -hableParams.toeSegment.B * log(params_x0); + hableParams.toeSegment.lnA = _410 * RCP_LOG2_E + toeSegment_lnA_optimised; + // toe section end + + // shoulder section + float shoulderSection_x0 = 1.f + params_overshootX - params_x1; + float shoulderSection_y0 = params_overshootY - hableParams.params.y1; // 1 + x was optimised away + hableParams.shoulderSegment.offsetX = 1.f + params_overshootX; + hableParams.shoulderSegment.offsetY = params_overshootY; // x + 1 was optimised away + // SolveAB + hableParams.shoulderSegment.B = ((shoulderM * shoulderSection_x0) / (shoulderSection_y0 + EPSILON)); + float shoulderSegment_B_optimised = hableParams.shoulderSegment.B * RCP_LOG2_E; + hableParams.shoulderSegment.lnA = (log2(shoulderSection_y0) * RCP_LOG2_E) - (shoulderSegment_B_optimised * log2(shoulderSection_x0)); + // shoulder section end + + // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L6 + // Eval + float evalY0 = 0.f; + if (params_overshootX > 0.f) + { + evalY0 = -exp2(((shoulderSegment_B_optimised * log2(params_overshootX)) + hableParams.shoulderSegment.lnA) * LOG2_E) + params_overshootY; + } + // Eval end + hableParams.invScale = 1.f / evalY0; + + float3 toneMapped; + + for (uint channel = 0; channel < 3; channel++) + { + float normX = InputColor[channel] * dstCurve_invW; + bool isToeSegment = normX < params_x0; + float returnChannel = 0.f; + + if (isToeSegment && normX > 0.f) + { + returnChannel = exp2(((((log2(normX) * hableParams.toeSegment.B) + _410) * RCP_LOG2_E) + toeSegment_lnA_optimised) * LOG2_E); + } + else if (normX < params_x1) + { + float evalMidSegment_y0 = normX + hableParams.midSegment.offsetX; // was -(-hableParams.midSegment.offsetX) + if (evalMidSegment_y0 > 0.f) + { + returnChannel = exp2(log2(evalMidSegment_y0) + midSegment_lnA_optimised); + } + } + else + { + //float evalShoulderSegment_y0 = ((-1.f) - params_overshootX) + normX; // small optimisation from the original decompilation + float evalShoulderSegment_y0 = (1.f + params_overshootX) - normX; + float evalShoulderReturn = 0.f; + if (evalShoulderSegment_y0 > 0.f) + { + evalShoulderReturn = exp2(((shoulderSegment_B_optimised * log2(evalShoulderSegment_y0)) + hableParams.shoulderSegment.lnA) * LOG2_E); + } + returnChannel = params_overshootY - evalShoulderReturn; + } + toneMapped[channel] = returnChannel * hableParams.invScale; + } + return toneMapped; } // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L21-L34 float HableEval_Inverse( - float Channel, - float offsetX, - float offsetY, - float scaleX, - float scaleY, - float lnA, - float B) + float Channel, + float offsetX, + float offsetY, + float scaleX, + float scaleY, + float lnA, + float B) { #if 0 //TODO: delete. This version might work better on Nvidia (to be investigated more before deleting the alternative branch) - float y0 = max((Channel - offsetY) / scaleY, EPSILON); - float x0 = exp((log(y0) - lnA) / B); + float y0 = max((Channel - offsetY) / scaleY, EPSILON); + float x0 = exp((log(y0) - lnA) / B); - return x0 / scaleX + offsetX; + return x0 / scaleX + offsetX; #else - float y0 = (Channel - offsetY) / scaleY; - float x0 = 0.f; + float y0 = (Channel - offsetY) / scaleY; + float x0 = 0.f; - if (y0 > 0.f) // log(0) is invalid - { - x0 = exp((log(y0) - lnA) / B); - } + if (y0 > 0.f) // log(0) is invalid + { + x0 = exp((log(y0) - lnA) / B); + } - return x0 / scaleX + offsetX; + return x0 / scaleX + offsetX; #endif } void Hable_Inverse_Channel( - inout float ColorChannel, - in HableParamters hableParams, - in bool onlyInvertHighlights = false) + inout float ColorChannel, + in HableParamters hableParams, + in bool onlyInvertHighlights = false) { - // Scale all non highlights by the scale the smallest (first) highlight would have, so we keep the curves connected - const float minHighlightsColor = max(hableParams.params.y0, hableParams.params.y1); // Check both toe and mid params for extra safety - if (onlyInvertHighlights && ColorChannel < minHighlightsColor) - { - ColorChannel *= HableEval_Inverse(minHighlightsColor, - hableParams.shoulderSegment.offsetX, - hableParams.shoulderSegment.offsetY * hableParams.invScale, - -1.f, - -1.f * hableParams.invScale, - hableParams.shoulderSegment.lnA, - hableParams.shoulderSegment.B) / minHighlightsColor; - return; - } - - // scaleY and offsetY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L187-L197 - // toe - if (ColorChannel < hableParams.params.y0) - { - // scaleXY and offsetXY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L151-L154 - ColorChannel = HableEval_Inverse(ColorChannel, - 0.f, - 0.f, // * hableParams.invScale - 1.f, - 1.f * hableParams.invScale, - hableParams.toeSegment.lnA, - hableParams.toeSegment.B); - } - // mid (linear segment) - else if (ColorChannel < hableParams.params.y1) - { - // scaleXY and offsetY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L125-L127 - ColorChannel = HableEval_Inverse(ColorChannel, - -hableParams.midSegment.offsetX, // minus was optimised away - 0.f, // * hableParams.invScale - 1.f, - 1.f * hableParams.invScale, - hableParams.midSegment.lnA, - 1.f); - } - // shoulder - else - { + // Scale all non highlights by the scale the smallest (first) highlight would have, so we keep the curves connected + const float minHighlightsColor = max(hableParams.params.y0, hableParams.params.y1); // Check both toe and mid params for extra safety + + if (onlyInvertHighlights && ColorChannel < minHighlightsColor) + { + ColorChannel *= HableEval_Inverse(minHighlightsColor, + hableParams.shoulderSegment.offsetX, + hableParams.shoulderSegment.offsetY * hableParams.invScale, + -1.f, + -1.f * hableParams.invScale, + hableParams.shoulderSegment.lnA, + hableParams.shoulderSegment.B) / minHighlightsColor; + return; + } + + // scaleY and offsetY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L187-L197 + // toe + if (ColorChannel < hableParams.params.y0) + { + // scaleXY and offsetXY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L151-L154 + ColorChannel = HableEval_Inverse(ColorChannel, + 0.f, + 0.f, // * hableParams.invScale + 1.f, + 1.f * hableParams.invScale, + hableParams.toeSegment.lnA, + hableParams.toeSegment.B); + } + // mid (linear segment) + else if (ColorChannel < hableParams.params.y1) + { + // scaleXY and offsetY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L125-L127 + ColorChannel = HableEval_Inverse(ColorChannel, + -hableParams.midSegment.offsetX, // minus was optimised away + 0.f, // * hableParams.invScale + 1.f, + 1.f * hableParams.invScale, + hableParams.midSegment.lnA, + 1.f); + } + // shoulder + else + { #if ENABLE_TONEMAP_IMPROVEMENTS //TODO: remove... temp workaround for broken shoulder - ColorChannel = min(ColorChannel, 0.99999f); + // seems to be Nvidia specific workaround? + // maybe use BTHCNST + ColorChannel = min(ColorChannel, 0.99999f); + #endif - // scaleXY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L175-L176 - ColorChannel = HableEval_Inverse(ColorChannel, - hableParams.shoulderSegment.offsetX, - hableParams.shoulderSegment.offsetY * hableParams.invScale, - -1.f, - -1.f * hableParams.invScale, - hableParams.shoulderSegment.lnA, - hableParams.shoulderSegment.B); - } + // scaleXY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L175-L176 + ColorChannel = HableEval_Inverse(ColorChannel, + hableParams.shoulderSegment.offsetX, + hableParams.shoulderSegment.offsetY * hableParams.invScale, + -1.f, + -1.f * hableParams.invScale, + hableParams.shoulderSegment.lnA, + hableParams.shoulderSegment.B); + } } // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L45-L52 template T Hable_Inverse( - T InputColor, - uint Channels /*1 to 3*/, //there has to be better solution than this - HableParamters hableParams, - bool onlyInvertHighlights = false) + T InputColor, + uint Channels /*1 to 3*/, //there has to be better solution than this + HableParamters hableParams, + bool onlyInvertHighlights = false) { - // There's no inverse formula for colors beyond the 0-1 range - InputColor = saturate(InputColor); - - float3 itmColor = InputColor; // Make this float3 to make it compile for all cases - - if (Channels <= 1) // InputColor[channel] won't compile if T is float(1) - { - Hable_Inverse_Channel(itmColor.x, - hableParams, - onlyInvertHighlights); - } - else - { - for (uint channel = 0; channel < Channels; channel++) - { - Hable_Inverse_Channel(itmColor[channel], - hableParams, - onlyInvertHighlights); - } - } - - return (T)itmColor * hableParams.dstParams.W; + // There's no inverse formula for colors beyond the 0-1 range + InputColor = saturate(InputColor); + + float3 itmColor = InputColor; // Make this float3 to make it compile for all cases + + if (Channels <= 1) // InputColor[channel] won't compile if T is float(1) + { + Hable_Inverse_Channel(itmColor.x, + hableParams, + onlyInvertHighlights); + } + else + { + for (uint channel = 0; channel < Channels; channel++) + { + Hable_Inverse_Channel(itmColor[channel], + hableParams, + onlyInvertHighlights); + } + } + + return (T)itmColor * hableParams.dstParams.W; } // Tonemapper inspired from DICE. Works on luminance to maintain hue, not per channel. -float3 DICETonemap(float3 Color, float MaxOutputLuminance) +float3 DICETonemap( + float3 Color, + float MaxOutputLuminance) { - // Between 0 and 1. Determines where the highlights curve (shoulder) starts. - // Leaving at zero for now as it's a simple and good looking default. - const float highlightsShoulderStart = 0.f; - - const float sourceLuminance = Luminance(Color); - if (sourceLuminance > 0.0f) - { - const float compressedLuminance = luminanceCompress(sourceLuminance, MaxOutputLuminance, highlightsShoulderStart); - Color *= compressedLuminance / sourceLuminance; - } - return Color; + // Between 0 and 1. Determines where the highlights curve (shoulder) starts. + // Leaving at zero for now as it's a simple and good looking default. + const float highlightsShoulderStart = 0.f; + + const float sourceLuminance = Luminance(Color); + if (sourceLuminance > 0.0f) + { + const float compressedLuminance = luminanceCompress(sourceLuminance, MaxOutputLuminance, highlightsShoulderStart); + Color *= compressedLuminance / sourceLuminance; + } + return Color; } // AutoHDR pass to generate some HDR brightess out of an SDR signal (it has no effect if HDR is not engaged). // This is hue conserving and only really affects highlights. // https://github.com/Filoppi/PumboAutoHDR -float3 PumboAutoHDR(float3 Color, float MaxOutputNits, float PaperWhite) +float3 PumboAutoHDR( + float3 Color, + float MaxOutputNits, + float PaperWhite) { - static const float AutoHDRShoulderPow = 3.5f; //TODO: exposure to user? + static const float AutoHDRShoulderPow = 3.5f; //TODO: exposure to user? - const float SDRRatio = Luminance(Color); + const float SDRRatio = Luminance(Color); // Limit AutoHDR brightness, it won't look good beyond a certain level. // The paper white multiplier is applied later so we account for that. - const float AutoHDRMaxWhite = max(MaxOutputNits / PaperWhite, WhiteNits_BT709) / WhiteNits_BT709; - const float AutoHDRShoulderRatio = 1.f - max(1.f - SDRRatio, 0.f); - const float AutoHDRExtraRatio = pow(AutoHDRShoulderRatio, AutoHDRShoulderPow) * (AutoHDRMaxWhite - 1.f); - const float AutoHDRTotalRatio = SDRRatio + AutoHDRExtraRatio; + const float AutoHDRMaxWhite = max(MaxOutputNits / PaperWhite, WhiteNits_BT709) / WhiteNits_BT709; + const float AutoHDRShoulderRatio = 1.f - max(1.f - SDRRatio, 0.f); + const float AutoHDRExtraRatio = pow(AutoHDRShoulderRatio, AutoHDRShoulderPow) * (AutoHDRMaxWhite - 1.f); + const float AutoHDRTotalRatio = SDRRatio + AutoHDRExtraRatio; - return Color * (AutoHDRTotalRatio / SDRRatio); + return Color * (AutoHDRTotalRatio / SDRRatio); } -float FindLuminanceToRestoreScale(float3 color, float tonemapLostLuminance, float preColorCorrectionLuminance, float postColorCorrectionLuminance, float midGrayScale = 1.f) +float FindLuminanceToRestoreScale( + float3 color, + float tonemapLostLuminance, + float preColorCorrectionLuminance, + float postColorCorrectionLuminance, + float midGrayScale = 1.f) { - // Try to restore any luminance that would have been lost during tone mapping (e.g. tonemapping, post process, LUTs, ... can all clip values). - // - // To achieve this, we re-apply the lost luminance, but only by the inverse amount the luminance changed during the color correction. - // e.g. if the color correction fully changed the luminance, then we don't change it any further, - // but if the color correction changed the luminance only by 25%, then we re-apply apply 75% of the lost luminance on top. - // This will be "accurate" as long as the color correction scaled the luminance linearly in the output as it grew in the input, - // but even if it didn't, it should still look fine. - - // This will re-apply an amount of lost luminance based on how much the CC (LUT) reduced it: - // the more the CC reduced the luminance, the less we will apply. - // If the CC increased the luminance though, we will apply even more than the base lost amount. - const float currentLum = Luminance(color); - if (currentLum > 0.f) - { - // Change the "tonemapLostLuminance" from being in the tonemapped SDR (0-1) range to the untonemapped HDR image range (by scaling it by the mid gray change, which is the best we can do). - // This will also acknowledge any clipped luminance from the range beyond 0-1. - tonemapLostLuminance = isnan(tonemapLostLuminance) ? 0.f : (tonemapLostLuminance * midGrayScale); + // Try to restore any luminance that would have been lost during tone mapping (e.g. tonemapping, post process, LUTs, ... can all clip values). + // + // To achieve this, we re-apply the lost luminance, but only by the inverse amount the luminance changed during the color correction. + // e.g. if the color correction fully changed the luminance, then we don't change it any further, + // but if the color correction changed the luminance only by 25%, then we re-apply apply 75% of the lost luminance on top. + // This will be "accurate" as long as the color correction scaled the luminance linearly in the output as it grew in the input, + // but even if it didn't, it should still look fine. + + // This will re-apply an amount of lost luminance based on how much the CC (LUT) reduced it: + // the more the CC reduced the luminance, the less we will apply. + // If the CC increased the luminance though, we will apply even more than the base lost amount. + const float currentLum = Luminance(color); + if (currentLum > 0.f) + { + // Change the "tonemapLostLuminance" from being in the tonemapped SDR (0-1) range to the untonemapped HDR image range (by scaling it by the mid gray change, which is the best we can do). + // This will also acknowledge any clipped luminance from the range beyond 0-1. + tonemapLostLuminance = isnan(tonemapLostLuminance) ? 0.f : (tonemapLostLuminance * midGrayScale); #if defined(APPLY_MERGED_COLOR_GRADING_LUT) && ENABLE_LUT && !ENABLE_APPLY_LUT_AFTER_INVERSE_TONEMAP && LUT_IMPROVEMENT_TYPE == 3 // Restore any luminance lost by the LUT - float scale = 1.f; - scale *= postColorCorrectionLuminance > 0.f ? lerp(1.f, preColorCorrectionLuminance / postColorCorrectionLuminance, LUTCorrectionPercentage) : 1.f; + float scale = 1.f; + scale *= postColorCorrectionLuminance > 0.f ? lerp(1.f, preColorCorrectionLuminance / postColorCorrectionLuminance, LUTCorrectionPercentage) : 1.f; #else - const float luminanceCCInvChange = max(1.f - (preColorCorrectionLuminance - postColorCorrectionLuminance), 0.f); - //const float luminanceCCInvChange = clamp(postColorCorrectionLuminance / preColorCorrectionLuminance, 0.f, 2.f); //TODO: try this, especially if brightness goes crazy high - float scale = luminanceCCInvChange; + const float luminanceCCInvChange = max(1.f - (preColorCorrectionLuminance - postColorCorrectionLuminance), 0.f); + //const float luminanceCCInvChange = clamp(postColorCorrectionLuminance / preColorCorrectionLuminance, 0.f, 2.f); //TODO: try this, especially if brightness goes crazy high + float scale = luminanceCCInvChange; #endif - const float targetLum = max(currentLum + tonemapLostLuminance, 0.f); //TODO: review max() math - scale *= targetLum / currentLum; - return scale; - } - return 1.f; + const float targetLum = max(currentLum + tonemapLostLuminance, 0.f); //TODO: review max() math + scale *= targetLum / currentLum; + return scale; + } + return 1.f; } // "MidGrayScale" is how much the mid gray shifted from the originally intended tonemapped input (e.g. if we run this function on the untonemapped image, we can remap the mid gray) -float3 PostProcess(float3 Color, inout float ColorLuminance, float MidGrayScale = 1.f) +float3 PostProcess( + float3 Color, + inout float ColorLuminance, + float MidGrayScale = 1.f) { - const uint hdrCmpDatIndex = PcwHdrComposite.HdrCmpDatIndex; - const float4 highlightsColorFilter = HdrCmpDat[hdrCmpDatIndex].HighlightsColorFilter; - const float4 colorFilter = HdrCmpDat[hdrCmpDatIndex].ColorFilter; - const float hableSaturation = HdrCmpDat[hdrCmpDatIndex].HableSaturation; - const float brightnessMultiplier = HdrCmpDat[hdrCmpDatIndex].BrightnessMultiplier; // Neutral at 1 - const float contrastIntensity = HdrCmpDat[hdrCmpDatIndex].ContrastIntensity; // Neutral at 1 - const float contrastMidPoint = PerSceneConstants[316u].z * MidGrayScale; // Game usually has this around 0.18 (mid gray) - - ColorLuminance = Luminance(Color); - - // saturation adjustment a la Hable - // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicColorGrading.cpp#L307-L309 - Color = ((Color - ColorLuminance) * hableSaturation) + ColorLuminance; - // Blend in another color based on the luminance. - Color += lerp(float3(0.f, 0.f, 0.f), ColorLuminance * highlightsColorFilter.rgb, highlightsColorFilter.a); - Color *= brightnessMultiplier; + const uint hdrCmpDatIndex = PcwHdrComposite.HdrCmpDatIndex; + const float4 highlightsColorFilter = HdrCmpDat[hdrCmpDatIndex].HighlightsColorFilter; + const float4 colorFilter = HdrCmpDat[hdrCmpDatIndex].ColorFilter; + const float hableSaturation = HdrCmpDat[hdrCmpDatIndex].HableSaturation; + const float brightnessMultiplier = HdrCmpDat[hdrCmpDatIndex].BrightnessMultiplier; // Neutral at 1 + const float contrastIntensity = HdrCmpDat[hdrCmpDatIndex].ContrastIntensity; // Neutral at 1 + const float contrastMidPoint = PerSceneConstants[316u].z * MidGrayScale; // Game usually has this around 0.18 (mid gray) + + ColorLuminance = Luminance(Color); + + // saturation adjustment a la Hable + // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicColorGrading.cpp#L307-L309 + Color = ((Color - ColorLuminance) * hableSaturation) + ColorLuminance; + + // Blend in another color based on the luminance. + Color += lerp(float3(0.f, 0.f, 0.f), ColorLuminance * highlightsColorFilter.rgb, highlightsColorFilter.a); + Color *= brightnessMultiplier; #if POST_PROCESS_CONTRAST_TYPE == 0 - // Contrast adjustment (shift the colors from 0<->1 to (e.g.) -0.5<->0.5 range, multiply and shift back). - // The higher the distance from the contrast middle point, the more contrast will change the color. - Color = ((Color - contrastMidPoint) * contrastIntensity) + contrastMidPoint; + // Contrast adjustment (shift the colors from 0<->1 to (e.g.) -0.5<->0.5 range, multiply and shift back). + // The higher the distance from the contrast middle point, the more contrast will change the color. + Color = ((Color - contrastMidPoint) * contrastIntensity) + contrastMidPoint; #elif POST_PROCESS_CONTRAST_TYPE == 1 - Color = pow(Color / contrastMidPoint, contrastIntensity) * contrastMidPoint; + Color = pow(Color / contrastMidPoint, contrastIntensity) * contrastMidPoint; #elif POST_PROCESS_CONTRAST_TYPE == 2 - // sigmoidal contrast adjustment doesn't clip colors - // https://www.imagemagick.org/Usage/color_mods/#sigmoidal + // sigmoidal contrast adjustment doesn't clip colors + // https://www.imagemagick.org/Usage/color_mods/#sigmoidal + + // Multiplier to somewhat match the original contrast adjustment + static const float sigmoidalContrastAdjustment = 2.5f; //TODO: we could probably find an even better default multiplication - // Multiplier to somewhat match the original contrast adjustment - static const float sigmoidalContrastAdjustment = 2.5f; //TODO: we could probably find an even better default multiplication - float c = max(contrastIntensity * sigmoidalContrastAdjustment, EPSILON); // protect against division by zero - float s = contrastMidPoint; // can be set to 0.5 for better darkening of shadows + float c = max(contrastIntensity * sigmoidalContrastAdjustment, EPSILON); // protect against division by zero + float s = contrastMidPoint; // can be set to 0.5 for better darkening of shadows - float minus = -1 / (1 + exp(c * s)); + float minus = -1 / (1 + exp(c * s)); - // there should be some optimisation left here - Color = (1 / (1 + exp(c * (s - Color))) + minus) - / (1 / (1 + exp(c * (s - 1))) + minus); + // there should be some optimisation left here + Color = (1 / (1 + exp(c * (s - Color))) + minus) + / (1 / (1 + exp(c * (s - 1))) + minus); #endif - Color = lerp(Color, colorFilter.rgb * MidGrayScale, colorFilter.a); - return Color; + Color = lerp(Color, colorFilter.rgb * MidGrayScale, colorFilter.a); + return Color; } -float3 PostProcess_Inverse(float3 Color, float ColorLuminance, float MidGrayScale = 1.f) +float3 PostProcess_Inverse( + float3 Color, + float ColorLuminance, + float MidGrayScale = 1.f) { - const uint hdrCmpDatIndex = PcwHdrComposite.HdrCmpDatIndex; - const float4 highlightsColorFilter = HdrCmpDat[hdrCmpDatIndex].HighlightsColorFilter; - const float4 colorFilter = HdrCmpDat[hdrCmpDatIndex].ColorFilter; - const float hableSaturation = HdrCmpDat[hdrCmpDatIndex].HableSaturation; - const float brightnessMultiplier = HdrCmpDat[hdrCmpDatIndex].BrightnessMultiplier; // Neutral at 1 - const float contrastIntensity = HdrCmpDat[hdrCmpDatIndex].ContrastIntensity; // Neutral at 1 - const float contrastMidPoint = PerSceneConstants[316u].z * MidGrayScale; + const uint hdrCmpDatIndex = PcwHdrComposite.HdrCmpDatIndex; + const float4 highlightsColorFilter = HdrCmpDat[hdrCmpDatIndex].HighlightsColorFilter; + const float4 colorFilter = HdrCmpDat[hdrCmpDatIndex].ColorFilter; + const float hableSaturation = HdrCmpDat[hdrCmpDatIndex].HableSaturation; + const float brightnessMultiplier = HdrCmpDat[hdrCmpDatIndex].BrightnessMultiplier; // Neutral at 1 + const float contrastIntensity = HdrCmpDat[hdrCmpDatIndex].ContrastIntensity; // Neutral at 1 + const float contrastMidPoint = PerSceneConstants[316u].z * MidGrayScale; - // We can't invert the color filter, so we will only inverse the post process by the unfiltered amount - const float colorFilterInverse = 1.f - colorFilter.a; + // We can't invert the color filter, so we will only inverse the post process by the unfiltered amount + const float colorFilterInverse = 1.f - colorFilter.a; #if POST_PROCESS_CONTRAST_TYPE == 0 - Color = ((Color - contrastMidPoint) / contrastIntensity) + contrastMidPoint; + + Color = ((Color - contrastMidPoint) / contrastIntensity) + contrastMidPoint; + #elif POST_PROCESS_CONTRAST_TYPE == 1 - Color = pow(Color / contrastMidPoint, 1.f / contrastIntensity) * contrastMidPoint; + + Color = pow(Color / contrastMidPoint, 1.f / contrastIntensity) * contrastMidPoint; + #elif POST_PROCESS_CONTRAST_TYPE == 2 - //TODO: implement + + //TODO: implement + #endif - Color /= brightnessMultiplier; + Color /= brightnessMultiplier; - Color -= lerp(float3(0.f, 0.f, 0.f), ColorLuminance * highlightsColorFilter.rgb, highlightsColorFilter.a); + Color -= lerp(float3(0.f, 0.f, 0.f), ColorLuminance * highlightsColorFilter.rgb, highlightsColorFilter.a); - Color = ((Color - ColorLuminance) / hableSaturation) + ColorLuminance; + Color = ((Color - ColorLuminance) / hableSaturation) + ColorLuminance; - return Color; + return Color; } // Takes input coordinates. Returns output color in linear space (also works in sRGB but it's not ideal). float3 TetrahedralInterpolation( - Texture3D Lut, - float3 LUTCoordinates) + Texture3D Lut, + float3 LUTCoordinates) { - // We need to clip the input coordinates as LUT texure samples below are not clamped. - float3 coords = saturate(LUTCoordinates.rgb) * (LUT_SIZE - 1); // Pixel coords - float3 color = 0; - - // baseInd is on [0,LUT_SIZE-1] - int3 baseInd = coords; - int3 nextInd = baseInd + 1; - - // fract is on [0,1] - float3 fract = frac(coords); - - float3 f1, f4; - - float3 v1 = Lut.Load(int4(baseInd, 0)).rgb; - float3 v4 = Lut.Load(int4(nextInd, 0)).rgb; - - if (fract.r >= fract.g) - { - if (fract.g >= fract.b) // R > G > B - { - nextInd = baseInd + int3(1, 0, 0); - float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; - - nextInd = baseInd + int3(1, 1, 0); - float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; - - f1 = 1.f - fract.r; - f4 = fract.b; - float3 f2 = fract.r - fract.g; - float3 f3 = fract.g - fract.b; - - color = (f2 * v2) + (f3 * v3); - } - else if (fract.r >= fract.b) // R > B > G - { - nextInd = baseInd + int3(1, 0, 0); - float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; - - nextInd = baseInd + int3(1, 0, 1); - float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; - - f1 = 1.f - fract.r; - f4 = fract.g; - float3 f2 = fract.r - fract.b; - float3 f3 = fract.b - fract.g; - - color = (f2 * v2) + (f3 * v3); - } - else // B > R > G - { - nextInd = baseInd + int3(0, 0, 1); - float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; - - nextInd = baseInd + int3(1, 0, 1); - float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; - - f1 = 1.f - fract.b; - f4 = fract.g; - float3 f2 = fract.b - fract.r; - float3 f3 = fract.r - fract.g; - - color = (f2 * v2) + (f3 * v3); - } - } - else - { - if (fract.g <= fract.b) // B > G > R - { - nextInd = baseInd + int3(0, 0, 1); - float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; - - nextInd = baseInd + int3(0, 1, 1); - float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; - - f1 = 1.f - fract.b; - f4 = fract.r; - float3 f2 = fract.b - fract.g; - float3 f3 = fract.g - fract.r; - - color = (f2 * v2) + (f3 * v3); - } - else if (fract.r >= fract.b) // G > R > B - { - nextInd = baseInd + int3(0, 1, 0); - float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; - - nextInd = baseInd + int3(1, 1, 0); - float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; - - f1 = 1.f - fract.g; - f4 = fract.b; - float3 f2 = fract.g - fract.r; - float3 f3 = fract.r - fract.b; - - color = (f2 * v2) + (f3 * v3); - } - else // G > B > R - { - nextInd = baseInd + int3(0, 1, 0); - float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; - - nextInd = baseInd + int3(0, 1, 1); - float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; - - f1 = 1.f - fract.g; - f4 = fract.r; - float3 f2 = fract.g - fract.b; - float3 f3 = fract.b - fract.r; - - color = (f2 * v2) + (f3 * v3); - } - } - - return color + (f1 * v1) + (f4 * v4); + // We need to clip the input coordinates as LUT texure samples below are not clamped. + float3 coords = saturate(LUTCoordinates.rgb) * (LUT_SIZE - 1); // Pixel coords + float3 color = 0; + + // baseInd is on [0,LUT_SIZE-1] + int3 baseInd = coords; + int3 nextInd = baseInd + 1; + + // fract is on [0,1] + float3 fract = frac(coords); + + float3 f1, f4; + + float3 v1 = Lut.Load(int4(baseInd, 0)).rgb; + float3 v4 = Lut.Load(int4(nextInd, 0)).rgb; + + if (fract.r >= fract.g) + { + if (fract.g >= fract.b) // R > G > B + { + nextInd = baseInd + int3(1, 0, 0); + float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; + + nextInd = baseInd + int3(1, 1, 0); + float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; + + f1 = 1.f - fract.r; + f4 = fract.b; + float3 f2 = fract.r - fract.g; + float3 f3 = fract.g - fract.b; + + color = (f2 * v2) + (f3 * v3); + } + else if (fract.r >= fract.b) // R > B > G + { + nextInd = baseInd + int3(1, 0, 0); + float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; + + nextInd = baseInd + int3(1, 0, 1); + float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; + + f1 = 1.f - fract.r; + f4 = fract.g; + float3 f2 = fract.r - fract.b; + float3 f3 = fract.b - fract.g; + + color = (f2 * v2) + (f3 * v3); + } + else // B > R > G + { + nextInd = baseInd + int3(0, 0, 1); + float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; + + nextInd = baseInd + int3(1, 0, 1); + float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; + + f1 = 1.f - fract.b; + f4 = fract.g; + float3 f2 = fract.b - fract.r; + float3 f3 = fract.r - fract.g; + + color = (f2 * v2) + (f3 * v3); + } + } + else + { + if (fract.g <= fract.b) // B > G > R + { + nextInd = baseInd + int3(0, 0, 1); + float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; + + nextInd = baseInd + int3(0, 1, 1); + float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; + + f1 = 1.f - fract.b; + f4 = fract.r; + float3 f2 = fract.b - fract.g; + float3 f3 = fract.g - fract.r; + + color = (f2 * v2) + (f3 * v3); + } + else if (fract.r >= fract.b) // G > R > B + { + nextInd = baseInd + int3(0, 1, 0); + float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; + + nextInd = baseInd + int3(1, 1, 0); + float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; + + f1 = 1.f - fract.g; + f4 = fract.b; + float3 f2 = fract.g - fract.r; + float3 f3 = fract.r - fract.b; + + color = (f2 * v2) + (f3 * v3); + } + else // G > B > R + { + nextInd = baseInd + int3(0, 1, 0); + float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; + + nextInd = baseInd + int3(0, 1, 1); + float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; + + f1 = 1.f - fract.g; + f4 = fract.r; + float3 f2 = fract.g - fract.b; + float3 f3 = fract.b - fract.r; + + color = (f2 * v2) + (f3 * v3); + } + } + + return color + (f1 * v1) + (f4 * v4); } PSOutput PS(PSInput psInput) { - float3 inputColor = InputColor.Load(int3(int2(psInput.SV_Position.xy), 0)); + float3 inputColor = InputColor.Load(int3(int2(psInput.SV_Position.xy), 0)); #if CLAMP_INPUT_OUTPUT - // Remove any negative value caused by using R16G16B16A16F buffers (originally this was R11G11B10F, which has no negative values). - // Doing gamut mapping, or keeping the colors outside of BT.709 doesn't seem to be right, as they seem to be just be accidentally coming out of some shader math. - inputColor = max(inputColor, 0.f); + // Remove any negative value caused by using R16G16B16A16F buffers (originally this was R11G11B10F, which has no negative values). + // Doing gamut mapping, or keeping the colors outside of BT.709 doesn't seem to be right, as they seem to be just be accidentally coming out of some shader math. + inputColor = max(inputColor, 0.f); #endif // CLAMP_INPUT_OUTPUT #if defined(APPLY_BLOOM) - float3 bloom = Bloom.Sample(Sampler0, psInput.TEXCOORD); - float bloomMultiplier = PcwHdrComposite.BloomMultiplier; - inputColor = (bloomMultiplier * bloom) + inputColor; + float3 bloom = Bloom.Sample(Sampler0, psInput.TEXCOORD); + float bloomMultiplier = PcwHdrComposite.BloomMultiplier; + inputColor = (bloomMultiplier * bloom) + inputColor; #endif // APPLY_BLOOM - float3 tonemappedColor; - float3 tonemappedUnclampedColor; + float3 tonemappedColor; + float3 tonemappedUnclampedColor; #if !ENABLE_TONEMAP - tonemappedColor = inputColor; - tonemappedUnclampedColor = inputColor; + tonemappedColor = inputColor; + tonemappedUnclampedColor = inputColor; #else - float acesParam_modE; - float acesParam_modA; + float acesParam_modE; + float acesParam_modA; - HableParamters hableParams; + HableParamters hableParams; - const bool clampACES = false; + const bool clampACES = false; - int tonemapperIndex = ForceTonemapper > 0 ? ForceTonemapper : PcwHdrComposite.Tmo; + int tonemapperIndex = ForceTonemapper > 0 ? ForceTonemapper : PcwHdrComposite.Tmo; - switch (tonemapperIndex) - { - case 1: - { - tonemappedUnclampedColor = ACESReference(inputColor, clampACES); - tonemappedColor = saturate(tonemappedUnclampedColor); - } break; + switch (tonemapperIndex) + { + case 1: + { + tonemappedUnclampedColor = ACESReference(inputColor, clampACES); + tonemappedColor = saturate(tonemappedUnclampedColor); + } break; - case 2: - { - tonemappedUnclampedColor = ACESParametric(inputColor, clampACES, acesParam_modE, acesParam_modA); - tonemappedColor = saturate(tonemappedUnclampedColor); - } break; + case 2: + { + tonemappedUnclampedColor = ACESParametric(inputColor, clampACES, acesParam_modE, acesParam_modA); + tonemappedColor = saturate(tonemappedUnclampedColor); + } break; - case 3: - { - tonemappedUnclampedColor = Hable(inputColor, hableParams); - tonemappedColor = tonemappedUnclampedColor; // Hable was never clamped - } break; + case 3: + { + tonemappedUnclampedColor = Hable(inputColor, hableParams); + tonemappedColor = tonemappedUnclampedColor; // Hable was never clamped + } break; - default: - { - tonemappedUnclampedColor = inputColor; - tonemappedColor = saturate(tonemappedUnclampedColor); - } break; - } + default: + { + tonemappedUnclampedColor = inputColor; + tonemappedColor = saturate(tonemappedUnclampedColor); + } break; + } #if ENABLE_POST_PROCESS - float prePostProcessColorLuminance; - float prePostProcessUnclampedColorLuminance; - tonemappedColor = PostProcess(tonemappedColor, prePostProcessColorLuminance); - // Repeat on unclamped tonemapped color to later retrieve the lost luminance - tonemappedUnclampedColor = PostProcess(tonemappedUnclampedColor, prePostProcessUnclampedColorLuminance); + float prePostProcessColorLuminance; + float prePostProcessUnclampedColorLuminance; + + tonemappedColor = PostProcess(tonemappedColor, prePostProcessColorLuminance); + // Repeat on unclamped tonemapped color to later retrieve the lost luminance + tonemappedUnclampedColor = PostProcess(tonemappedUnclampedColor, prePostProcessUnclampedColorLuminance); #endif //ENABLE_POST_PROCESS - tonemappedColor = saturate(tonemappedColor); + tonemappedColor = saturate(tonemappedColor); #endif // ENABLE_TONEMAP - // This is the luminance lost by clamping SDR tonemappers to the 0-1 range, and again by the fact that LUTs only take 0-1 range input. - float tonemapLostLuminance = Luminance(tonemappedUnclampedColor - tonemappedColor); + // This is the luminance lost by clamping SDR tonemappers to the 0-1 range, and again by the fact that LUTs only take 0-1 range input. + float tonemapLostLuminance = Luminance(tonemappedUnclampedColor - tonemappedColor); - float3 color = tonemappedColor; + float3 color = tonemappedColor; - const float preColorCorrectionLuminance = Luminance(color); + const float preColorCorrectionLuminance = Luminance(color); #if defined(APPLY_MERGED_COLOR_GRADING_LUT) && ENABLE_TONEMAP && ENABLE_LUT #if FIX_WRONG_SRGB_GAMMA_FORMULA - const float3 LUTCoordinates = gamma_linear_to_sRGB(color); + const float3 LUTCoordinates = gamma_linear_to_sRGB(color); #else - // Read gamma ini value, defaulting at 2.4 (makes little sense) - float inverseGamma = 1.f / (max(SharedFrameData.Gamma, 0.001f)); - // Weird linear -> sRGB conversion that clips values just above 0, and raises blacks. - const float3 LUTCoordinates = max((pow(color, inverseGamma) * 1.055f) - 0.055f, 0.f); // Does "max()" is unnecessary as LUT sampling is already clamped. + // Read gamma ini value, defaulting at 2.4 (makes little sense) + float inverseGamma = 1.f / (max(SharedFrameData.Gamma, 0.001f)); + // Weird linear -> sRGB conversion that clips values just above 0, and raises blacks. + const float3 LUTCoordinates = max((pow(color, inverseGamma) * 1.055f) - 0.055f, 0.f); // Does "max()" is unnecessary as LUT sampling is already clamped. #endif // FIX_WRONG_SRGB_GAMMA_FORMULA #if ENABLE_LUT_TETRAHEDRAL_INTERPOLATION - float3 LUTColor = TetrahedralInterpolation(Lut, LUTCoordinates); + float3 LUTColor = TetrahedralInterpolation(Lut, LUTCoordinates); #else - float3 LUTColor = Lut.Sample(Sampler0, LUTCoordinates * (1.f - (1.f / LUT_SIZE)) + ((1.f / LUT_SIZE) / 2.f)); + float3 LUTColor = Lut.Sample(Sampler0, LUTCoordinates * (1.f - (1.f / LUT_SIZE)) + ((1.f / LUT_SIZE) / 2.f)); #endif // ENABLE_LUT_TETRAHEDRAL_INTERPOLATION #if !LUT_FIX_GAMMA_MAPPING - // We always work in linear space so convert to it. - // We never acknowledge the original wrong gamma function here (we don't really care). - LUTColor = gamma_sRGB_to_linear(LUTColor); + // We always work in linear space so convert to it. + // We never acknowledge the original wrong gamma function here (we don't really care). + LUTColor = gamma_sRGB_to_linear(LUTColor); #endif // !LUT_FIX_GAMMA_MAPPING - const float LUTMaskAlpha = saturate(LutMask.Sample(Sampler0, psInput.TEXCOORD).x + (1.f - LUTStrength)); + const float LUTMaskAlpha = saturate(LutMask.Sample(Sampler0, psInput.TEXCOORD).x + (1.f - LUTStrength)); - LUTColor = lerp(LUTColor, color, LUTMaskAlpha); + LUTColor = lerp(LUTColor, color, LUTMaskAlpha); #if ENABLE_APPLY_LUT_AFTER_INVERSE_TONEMAP && ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP - float3 LUTColorShift = LUTColor / color; - for (uint channel = 0; channel < 3; channel++) - { - if (color[channel] == 0.f) - LUTColorShift[channel] = 1.f; - } + + float3 LUTColorShift = LUTColor / color; + + for (uint channel = 0; channel < 3; channel++) + { + if (color[channel] == 0.f) + { + LUTColorShift[channel] = 1.f; + } + } + #else - color = LUTColor; + + color = LUTColor; + #endif #endif // APPLY_MERGED_COLOR_GRADING_LUT @@ -856,132 +909,151 @@ PSOutput PS(PSInput psInput) #if ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP - float postColorCorrectionLuminance = Luminance(color); - //TODO: this isn't hue conserving, though we don't really have a way of doing it in a hue conserving way, as then it would break the brightness mapping. - const float postColorCorrectionClampedLuminance = Luminance(saturate(color)); - // Some inverse tonemapper formulas can't take any values beyond 0-1, so we'll need to clip them and restore their luminance. - const float colorCorrectionHDRClippedLuminanceChange = postColorCorrectionClampedLuminance > 0.f ? (postColorCorrectionLuminance / postColorCorrectionClampedLuminance) : 1.f; + float postColorCorrectionLuminance = Luminance(color); + //TODO: this isn't hue conserving, though we don't really have a way of doing it in a hue conserving way, as then it would break the brightness mapping. + const float postColorCorrectionClampedLuminance = Luminance(saturate(color)); + // Some inverse tonemapper formulas can't take any values beyond 0-1, so we'll need to clip them and restore their luminance. + const float colorCorrectionHDRClippedLuminanceChange = postColorCorrectionClampedLuminance > 0.f ? (postColorCorrectionLuminance / postColorCorrectionClampedLuminance) : 1.f; #if ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS - //TODO: passing in the previous luminance might not be the best, though is there any other way really? - //Should we at least shift it by how much the LUT shifted the luminance? To invert it more correctly. - color = PostProcess_Inverse(color, prePostProcessColorLuminance); + //TODO: passing in the previous luminance might not be the best, though is there any other way really? + //Should we at least shift it by how much the LUT shifted the luminance? To invert it more correctly. + color = PostProcess_Inverse(color, prePostProcessColorLuminance); #endif // ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS - const float paperWhite = HDR_GAME_PAPER_WHITE; + const float paperWhite = HDR_GAME_PAPER_WHITE; + + const float midGrayIn = MidGray; + float midGrayOut = midGrayIn; - const float midGrayIn = MidGray; - float midGrayOut = midGrayIn; #if ENABLE_AUTOHDR - if (tonemapperIndex == 1 || tonemapperIndex == 2 || tonemapperIndex == 3) - { - tonemapperIndex = 0; - color = PumboAutoHDR(color, HDR_MAX_OUTPUT_NITS, paperWhite); - midGrayOut = PumboAutoHDR(midGrayIn.xxx, HDR_MAX_OUTPUT_NITS, paperWhite).x; - } + + if (tonemapperIndex == 1 || tonemapperIndex == 2 || tonemapperIndex == 3) + { + tonemapperIndex = 0; + color = PumboAutoHDR(color, HDR_MAX_OUTPUT_NITS, paperWhite); + midGrayOut = PumboAutoHDR(midGrayIn.xxx, HDR_MAX_OUTPUT_NITS, paperWhite).x; + } + #endif // ENABLE_AUTOHDR #if INVERT_TONEMAP_BY_LUMINANCE - float preInverseTonemapColorLuminance = Luminance(color); - float inverseTonemapColor = preInverseTonemapColorLuminance; - const uint inverseTonemapColorComponents = 1; + + float preInverseTonemapColorLuminance = Luminance(color); + float inverseTonemapColor = preInverseTonemapColorLuminance; + const uint inverseTonemapColorComponents = 1; + #else - float3 inverseTonemapColor = color; - const uint inverseTonemapColorComponents = 3; + + float3 inverseTonemapColor = color; + const uint inverseTonemapColorComponents = 3; + #endif // INVERT_TONEMAP_BY_LUMINANCE - // Restore a color very close to the original linear one, but with all the other post process and LUT transformations baked in - switch (tonemapperIndex) - { - //TODO: implement INVERT_TONEMAP_HIGHLIGHTS_ONLY for ACES - - case 1: - { - inverseTonemapColor = ACESReference_Inverse(inverseTonemapColor); - midGrayOut = ACESReference_Inverse(midGrayIn); - tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; - postColorCorrectionLuminance = postColorCorrectionClampedLuminance; - } break; - - case 2: - { - inverseTonemapColor = ACESParametric_Inverse(inverseTonemapColor, acesParam_modE, acesParam_modA); - midGrayOut = ACESParametric_Inverse(midGrayIn, acesParam_modE, acesParam_modA); - tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; - postColorCorrectionLuminance = postColorCorrectionClampedLuminance; - } break; - - case 3: - { - const bool onlyInvertHighlights = (bool)INVERT_TONEMAP_HIGHLIGHTS_ONLY; - inverseTonemapColor = Hable_Inverse(inverseTonemapColor, - inverseTonemapColorComponents, - hableParams, - onlyInvertHighlights); - midGrayOut = Hable_Inverse(midGrayIn, - 1, - hableParams, - onlyInvertHighlights); - tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; - postColorCorrectionLuminance = postColorCorrectionClampedLuminance; - } break; - - default: - // Any luminance lost by this tonemap case would have already been restored above with "tonemapLostLuminance". - break; - } + // Restore a color very close to the original linear one, but with all the other post process and LUT transformations baked in + switch (tonemapperIndex) + { + //TODO: implement INVERT_TONEMAP_HIGHLIGHTS_ONLY for ACES + + case 1: + { + inverseTonemapColor = ACESReference_Inverse(inverseTonemapColor); + midGrayOut = ACESReference_Inverse(midGrayIn); + tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; + postColorCorrectionLuminance = postColorCorrectionClampedLuminance; + } break; + + case 2: + { + inverseTonemapColor = ACESParametric_Inverse(inverseTonemapColor, acesParam_modE, acesParam_modA); + midGrayOut = ACESParametric_Inverse(midGrayIn, acesParam_modE, acesParam_modA); + tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; + postColorCorrectionLuminance = postColorCorrectionClampedLuminance; + } break; + + case 3: + { + const bool onlyInvertHighlights = (bool)INVERT_TONEMAP_HIGHLIGHTS_ONLY; + + inverseTonemapColor = Hable_Inverse(inverseTonemapColor, + inverseTonemapColorComponents, + hableParams, + onlyInvertHighlights); + midGrayOut = Hable_Inverse(midGrayIn, + 1, + hableParams, + onlyInvertHighlights); + + tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; + postColorCorrectionLuminance = postColorCorrectionClampedLuminance; + } break; + + default: + // Any luminance lost by this tonemap case would have already been restored above with "tonemapLostLuminance". + break; + } #if INVERT_TONEMAP_BY_LUMINANCE - color *= preInverseTonemapColorLuminance > 0.f ? (inverseTonemapColor / preInverseTonemapColorLuminance) : 1.f; + + color *= preInverseTonemapColorLuminance > 0.f ? (inverseTonemapColor / preInverseTonemapColorLuminance) : 1.f; + #else - color = inverseTonemapColor; + + color = inverseTonemapColor; + #endif - const float midGrayScale = midGrayOut / midGrayIn; + const float midGrayScale = midGrayOut / midGrayIn; #if ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS - color = PostProcess(color, prePostProcessColorLuminance, midGrayScale); + color = PostProcess(color, prePostProcessColorLuminance, midGrayScale); #endif // ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS #if ENABLE_LUMINANCE_RESTORE //TODO: this will go beyond the max output nits if "ENABLE_AUTOHDR" is true, we need to fix a way to make them work together (maybe re-tonemap always at the end?) - // Restore any luminance beyond 1 that ended up clipped by HDR->SDR tonemappers and any subsequent image manipulation. - // It's important to do these after the inverse tonemappers, as they can't handle values beyond 0-1. - color *= FindLuminanceToRestoreScale(color, tonemapLostLuminance, preColorCorrectionLuminance, postColorCorrectionLuminance, midGrayScale); + + // Restore any luminance beyond 1 that ended up clipped by HDR->SDR tonemappers and any subsequent image manipulation. + // It's important to do these after the inverse tonemappers, as they can't handle values beyond 0-1. + color *= FindLuminanceToRestoreScale(color, tonemapLostLuminance, preColorCorrectionLuminance, postColorCorrectionLuminance, midGrayScale); + #endif // ENABLE_LUMINANCE_RESTORE #if defined(APPLY_MERGED_COLOR_GRADING_LUT) && ENABLE_LUT && ENABLE_APPLY_LUT_AFTER_INVERSE_TONEMAP - const float3 preLUTShiftedColor = color; - color *= LUTColorShift; + + const float3 preLUTShiftedColor = color; + color *= LUTColorShift; + #if LUT_IMPROVEMENT_TYPE == 3 - const float postLUTShiftedColorLuminance = Luminance(color); - color *= postLUTShiftedColorLuminance > 0.f ? lerp(1.f, Luminance(preLUTShiftedColor) / postLUTShiftedColorLuminance, LUTCorrectionPercentage) : 1.f; + + const float postLUTShiftedColorLuminance = Luminance(color); + color *= postLUTShiftedColorLuminance > 0.f ? lerp(1.f, Luminance(preLUTShiftedColor) / postLUTShiftedColorLuminance, LUTCorrectionPercentage) : 1.f; + #endif // LUT_IMPROVEMENT_TYPE == 3 #endif // ENABLE_APPLY_LUT_AFTER_INVERSE_TONEMAP - // Bring back the color to the same range as SDR by dividing by the mid gray change. - color *= paperWhite / midGrayScale; + // Bring back the color to the same range as SDR by dividing by the mid gray change. + color *= paperWhite / midGrayScale; #if !ENABLE_AUTOHDR - //TODO: find a tonemapper that looks more like Hable? - const float maxOutputLuminance = HDR_MAX_OUTPUT_NITS / WhiteNits_BT709; - color = DICETonemap(color, maxOutputLuminance); + //TODO: find a tonemapper that looks more like Hable? + const float maxOutputLuminance = HDR_MAX_OUTPUT_NITS / WhiteNits_BT709; + color = DICETonemap(color, maxOutputLuminance); #endif // !ENABLE_AUTOHDR #else // ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP - color *= HDR_GAME_PAPER_WHITE; + color *= HDR_GAME_PAPER_WHITE; #endif // ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP #if CLAMP_INPUT_OUTPUT - color = clamp(color, 0.f, FLT16_MAX); // Avoid extremely high numbers turning into NaN in FP16 + color = clamp(color, 0.f, FLT16_MAX); // Avoid extremely high numbers turning into NaN in FP16 #endif // CLAMP_INPUT_OUTPUT @@ -989,26 +1061,26 @@ PSOutput PS(PSInput psInput) #if SDR_USE_GAMMA_2_2 - color = pow(color, 1.f / 2.2f); + color = pow(color, 1.f / 2.2f); #else - // Do sRGB gamma even if we'd be playing on gamma 2.2 screens, as the game was already calibrated for 2.2 gamma despite using the wrong formula - color = gamma_linear_to_sRGB(color); + // Do sRGB gamma even if we'd be playing on gamma 2.2 screens, as the game was already calibrated for 2.2 gamma despite using the wrong formula + color = gamma_linear_to_sRGB(color); #endif // SDR_USE_GAMMA_2_2 #if CLAMP_INPUT_OUTPUT - color = saturate(color); + color = saturate(color); #endif // CLAMP_INPUT_OUTPUT #endif // ENABLE_HDR - PSOutput psOutput; - psOutput.SV_Target.rgb = color; - psOutput.SV_Target.a = 1.f; + PSOutput psOutput; + psOutput.SV_Target.rgb = color; + psOutput.SV_Target.a = 1.f; - return psOutput; + return psOutput; } diff --git a/shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl b/shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl index 975a3eac..e57281ec 100644 --- a/shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl +++ b/shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl @@ -3,12 +3,12 @@ cbuffer _13_15 : register(b0, space7) { - float4 _15_m0[3269] : packoffset(c0); + float4 _15_m0[3269] : packoffset(c0); }; cbuffer _18_20 : register(b0, space0) { - float4 _20_m0[2] : packoffset(c0); + float4 _20_m0[2] : packoffset(c0); }; Texture2D TonemappedColorTexture : register(t0, space8); // Possibly in gamma space in SDR @@ -20,59 +20,59 @@ static float4 SV_Target; struct SPIRV_Cross_Input { - float4 TEXCOORD : TEXCOORD0; + float4 TEXCOORD : TEXCOORD0; }; struct SPIRV_Cross_Output { - float4 SV_Target : SV_Target0; + float4 SV_Target : SV_Target0; }; void frag_main() { - float3 inColor = TonemappedColorTexture.Sample(Sampler0, float2(TEXCOORD.x, TEXCOORD.y)).xyz; - uint4 sharpenParams = asuint(_20_m0[0u]); - float sharpenIntensity = asfloat(sharpenParams.z); - float3 outColor = inColor; + float3 inColor = TonemappedColorTexture.Sample(Sampler0, float2(TEXCOORD.x, TEXCOORD.y)).xyz; + uint4 sharpenParams = asuint(_20_m0[0u]); + float sharpenIntensity = asfloat(sharpenParams.z); + float3 outColor = inColor; #if !ENABLE_HDR //TODO: implement - if (sharpenIntensity > 0.f) //TODO: should this check for "sharpenIntensity < 1.f"? It seems like its the inverse intensity - { - float _70 = ((_15_m0[161u].z * TEXCOORD.x) * asfloat(sharpenParams.x)) + 0.5f; - float _72 = ((_15_m0[161u].w * TEXCOORD.y) * asfloat(sharpenParams.y)) + 0.5f; - float _76 = frac(_70); - float _77 = frac(_72); - float _87 = ((((((_76 * 2.0f) + (-3.0f)) * _76) + (-3.0f)) * _76) + 5.0f) * 0.16666667163372039794921875f; - float _89 = _76 * 3.0f; - float _98 = ((((((3.0f - _89) + _76) * _76) + 3.0f) * _76) + 1.0f) * 0.16666667163372039794921875f; - float _99 = _76 * _76; - float _113 = _77 * _77; - float _114 = _77 * 3.0f; - float _125 = ((((((_77 * 2.0f) + (-3.0f)) * _77) + (-3.0f)) * _77) + 5.0f) * 0.16666667163372039794921875f; - float _136 = ((((((3.0f - _114) + _77) * _77) + 3.0f) * _77) + 1.0f) * 0.16666667163372039794921875f; - float _139 = floor(_70) + (-0.5f); - float _142 = floor(_72) + (-0.5f); - float _144 = (_139 + (((((_99 * (_89 + (-6.0f))) + 4.0f) * 0.16666667163372039794921875f) / _87) + (-1.0f))) / _15_m0[161u].z; - float _145 = (_142 + (((((_113 * (_114 + (-6.0f))) + 4.0f) * 0.16666667163372039794921875f) / _125) + (-1.0f))) / _15_m0[161u].w; - float _147 = (_139 + ((((_99 * 0.16666667163372039794921875f) * _76) / _98) + 1.0f)) / _15_m0[161u].z; - float _149 = (_142 + ((((_113 * 0.16666667163372039794921875f) * _77) / _136) + 1.0f)) / _15_m0[161u].w; - float3 _152 = TonemappedColorTexture.SampleLevel(Sampler1, float2(_144, _145), 0.0f); - float3 _157 = TonemappedColorTexture.SampleLevel(Sampler1, float2(_147, _145), 0.0f); - float3 _162 = TonemappedColorTexture.SampleLevel(Sampler1, float2(_144, _149), 0.0f); - float3 _167 = TonemappedColorTexture.SampleLevel(Sampler1, float2(_147, _149), 0.0f); - float3 sharpenedColor = (((_167 * _98) + (_162 * _87)) * _136) + (((_157 * _98) + (_152 * _87)) * _125); - outColor = ((inColor - sharpenedColor) * sharpenIntensity) + sharpenedColor; - //outColor = lerp(sharpenedColor, inColor, sharpenIntensity); - } + if (sharpenIntensity > 0.f) //TODO: should this check for "sharpenIntensity < 1.f"? It seems like its the inverse intensity + { + float _70 = ((_15_m0[161u].z * TEXCOORD.x) * asfloat(sharpenParams.x)) + 0.5f; + float _72 = ((_15_m0[161u].w * TEXCOORD.y) * asfloat(sharpenParams.y)) + 0.5f; + float _76 = frac(_70); + float _77 = frac(_72); + float _87 = ((((((_76 * 2.0f) + (-3.0f)) * _76) + (-3.0f)) * _76) + 5.0f) * 0.16666667163372039794921875f; + float _89 = _76 * 3.0f; + float _98 = ((((((3.0f - _89) + _76) * _76) + 3.0f) * _76) + 1.0f) * 0.16666667163372039794921875f; + float _99 = _76 * _76; + float _113 = _77 * _77; + float _114 = _77 * 3.0f; + float _125 = ((((((_77 * 2.0f) + (-3.0f)) * _77) + (-3.0f)) * _77) + 5.0f) * 0.16666667163372039794921875f; + float _136 = ((((((3.0f - _114) + _77) * _77) + 3.0f) * _77) + 1.0f) * 0.16666667163372039794921875f; + float _139 = floor(_70) + (-0.5f); + float _142 = floor(_72) + (-0.5f); + float _144 = (_139 + (((((_99 * (_89 + (-6.0f))) + 4.0f) * 0.16666667163372039794921875f) / _87) + (-1.0f))) / _15_m0[161u].z; + float _145 = (_142 + (((((_113 * (_114 + (-6.0f))) + 4.0f) * 0.16666667163372039794921875f) / _125) + (-1.0f))) / _15_m0[161u].w; + float _147 = (_139 + ((((_99 * 0.16666667163372039794921875f) * _76) / _98) + 1.0f)) / _15_m0[161u].z; + float _149 = (_142 + ((((_113 * 0.16666667163372039794921875f) * _77) / _136) + 1.0f)) / _15_m0[161u].w; + float3 _152 = TonemappedColorTexture.SampleLevel(Sampler1, float2(_144, _145), 0.0f); + float3 _157 = TonemappedColorTexture.SampleLevel(Sampler1, float2(_147, _145), 0.0f); + float3 _162 = TonemappedColorTexture.SampleLevel(Sampler1, float2(_144, _149), 0.0f); + float3 _167 = TonemappedColorTexture.SampleLevel(Sampler1, float2(_147, _149), 0.0f); + float3 sharpenedColor = (((_167 * _98) + (_162 * _87)) * _136) + (((_157 * _98) + (_152 * _87)) * _125); + outColor = ((inColor - sharpenedColor) * sharpenIntensity) + sharpenedColor; + //outColor = lerp(sharpenedColor, inColor, sharpenIntensity); + } #endif - SV_Target.xyz = outColor; - SV_Target.w = 1.0f; + SV_Target.xyz = outColor; + SV_Target.w = 1.0f; } SPIRV_Cross_Output main(SPIRV_Cross_Input stage_input) { - TEXCOORD = stage_input.TEXCOORD; - frag_main(); - SPIRV_Cross_Output stage_output; - stage_output.SV_Target = SV_Target; - return stage_output; + TEXCOORD = stage_input.TEXCOORD; + frag_main(); + SPIRV_Cross_Output stage_output; + stage_output.SV_Target = SV_Target; + return stage_output; } diff --git a/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl b/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl index ab7cca70..86110806 100644 --- a/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl +++ b/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl @@ -7,13 +7,13 @@ struct PushConstantWrapper_ScaleformCompositeLayout { - int Unknown1; - int Unknown2; + int Unknown1; + int Unknown2; }; cbuffer stub_PushConstantWrapper_ScaleformCompositeLayout : register(b0) { - PushConstantWrapper_ScaleformCompositeLayout Layout : packoffset(c0); + PushConstantWrapper_ScaleformCompositeLayout Layout : packoffset(c0); }; Texture2D inputTexture : register(t0, space8); @@ -21,30 +21,30 @@ SamplerState inputSampler : register(s0, space8); struct PSInputs { - float4 uv : TEXCOORD0; - float4 pos : SV_Position; + float4 uv : TEXCOORD0; + float4 pos : SV_Position; }; float4 PS(PSInputs inputs) : SV_Target { - float4 UIColor = inputTexture.Sample(inputSampler, float2(inputs.uv.x, inputs.uv.y)); - float UIIntensity = asfloat(Layout.Unknown1); + float4 UIColor = inputTexture.Sample(inputSampler, float2(inputs.uv.x, inputs.uv.y)); + float UIIntensity = asfloat(Layout.Unknown1); - // Theoretically all UI is in sRGB (though it might have been designed on gamma 2.2 screens, we can't know that, but it was definately targeting gamma 2.2 as output anyway). - UIColor.xyz = gamma_sRGB_to_linear(UIColor.xyz); - UIColor.xyz = UIColor.xyz * UIIntensity; + // Theoretically all UI is in sRGB (though it might have been designed on gamma 2.2 screens, we can't know that, but it was definately targeting gamma 2.2 as output anyway). + UIColor.xyz = gamma_sRGB_to_linear(UIColor.xyz); + UIColor.xyz = UIColor.xyz * UIIntensity; #if !ENABLE_HDR - UIColor.xyz = gamma_linear_to_sRGB(UIColor.xyz); + UIColor.xyz = gamma_linear_to_sRGB(UIColor.xyz); #else #if HDR_GAMMA_CORRECTION - UIColor.xyz = pow(gamma_linear_to_sRGB(UIColor.xyz), 2.2f); + UIColor.xyz = pow(gamma_linear_to_sRGB(UIColor.xyz), 2.2f); #endif // HDR_GAMMA_CORRECTION - UIColor.xyz *= HDR_UI_PAPER_WHITE; - // Scale alpha to emulate sRGB gamma blending (we blend in linear space in HDR), - // this won't ever be perfect but it's close enough for most cases. - // We do a saturate to avoid pow of -0, which might lead to unexpected results. - UIColor.a = pow(saturate(UIColor.a), HDR_UI_BLEND_POW); //TODO: base the percentage of application of "HDR_UI_BLEND_POW" based on how black/dark the UI color is. + UIColor.xyz *= HDR_UI_PAPER_WHITE; + // Scale alpha to emulate sRGB gamma blending (we blend in linear space in HDR), + // this won't ever be perfect but it's close enough for most cases. + // We do a saturate to avoid pow of -0, which might lead to unexpected results. + UIColor.a = pow(saturate(UIColor.a), HDR_UI_BLEND_POW); //TODO: base the percentage of application of "HDR_UI_BLEND_POW" based on how black/dark the UI color is. #endif // ENABLE_HDR - return UIColor; + return UIColor; } diff --git a/shaders/color.hlsl b/shaders/color.hlsl index 18650b1e..4bcf4ed0 100644 --- a/shaders/color.hlsl +++ b/shaders/color.hlsl @@ -25,58 +25,58 @@ static const float3x3 BT2020_2_BT709 = float3x3( float3 BT709_To_BT2020(float3 color) { - return mul(BT709_2_BT2020, color); + return mul(BT709_2_BT2020, color); } float3 BT2020_To_BT709(float3 color) { - return mul(BT2020_2_BT709, color); + return mul(BT2020_2_BT709, color); } float gamma_linear_to_sRGB(float channel) { - [flatten] - if (channel <= 0.0031308f) - { - channel = channel * 12.92f; - } - else - { - channel = 1.055f * pow(channel, 1.0f / 2.4f) - 0.055f; - } - return channel; + [flatten] + if (channel <= 0.0031308f) + { + channel = channel * 12.92f; + } + else + { + channel = 1.055f * pow(channel, 1.0f / 2.4f) - 0.055f; + } + return channel; } float3 gamma_linear_to_sRGB(float3 Color) { - return float3(gamma_linear_to_sRGB(Color.r), - gamma_linear_to_sRGB(Color.g), - gamma_linear_to_sRGB(Color.b)); + return float3(gamma_linear_to_sRGB(Color.r), + gamma_linear_to_sRGB(Color.g), + gamma_linear_to_sRGB(Color.b)); } float gamma_sRGB_to_linear(float channel) { - [flatten] - if (channel <= 0.04045f) - { - channel = channel / 12.92f; - } - else - { - channel = pow((channel + 0.055f) / 1.055f, 2.4f); - } - return channel; + [flatten] + if (channel <= 0.04045f) + { + channel = channel / 12.92f; + } + else + { + channel = pow((channel + 0.055f) / 1.055f, 2.4f); + } + return channel; } float3 gamma_sRGB_to_linear(float3 Color) { - return float3(gamma_sRGB_to_linear(Color.r), - gamma_sRGB_to_linear(Color.g), - gamma_sRGB_to_linear(Color.b)); + return float3(gamma_sRGB_to_linear(Color.r), + gamma_sRGB_to_linear(Color.g), + gamma_sRGB_to_linear(Color.b)); } float Luminance(float3 color) { - // Fixed from "wrong" values: 0.2125 0.7154 0.0721f - return dot(color, float3(0.2126f, 0.7152f, 0.0722f)); + // Fixed from "wrong" values: 0.2125 0.7154 0.0721f + return dot(color, float3(0.2126f, 0.7152f, 0.0722f)); } diff --git a/shaders/compile_all_shaders.bat b/shaders/compile_all_shaders.bat index 064bbbbb..7c6f1e0e 100644 --- a/shaders/compile_all_shaders.bat +++ b/shaders/compile_all_shaders.bat @@ -7,17 +7,17 @@ set dxc=..\tools\dxc_2023_08_14\bin\x64\dxc.exe for /f "delims=" %%x in (target_folder.txt) do set target_folder=%%x if "%target_folder%" == "" ( - echo target folder isn't set in "target_folder.txt"! - echo single line with the folder with no quotation marks and a trailing \ - echo example: C:\Games\Starfield\Data\shadersfx\ - pause - goto :eof + echo target folder isn't set in "target_folder.txt"! + echo single line with the folder with no quotation marks and a trailing \ + echo example: C:\Games\Starfield\Data\shadersfx\ + pause + goto :eof ) if not exist "%target_folder%" ( - echo target folder doesn't exist! - echo creating it... - mkdir "%target_folder%" + echo target folder doesn't exist! + echo creating it... + mkdir "%target_folder%" ) "%dxc%" "HDRComposite\HDRComposite_ps.hlsl" ^ @@ -83,10 +83,10 @@ call :errorlevel_check pause :errorlevel_check - if %ERRORLEVEL% NEQ 0 ( - echo. - echo compilation failed. Press any key to continue... - pause > NUL - echo. - ) + if %ERRORLEVEL% NEQ 0 ( + echo. + echo compilation failed. Press any key to continue... + pause > NUL + echo. + ) goto :eof diff --git a/shaders/structs.hlsl b/shaders/structs.hlsl index e1c6e664..cdd8fb17 100644 --- a/shaders/structs.hlsl +++ b/shaders/structs.hlsl @@ -3,63 +3,63 @@ //HDRComposite start struct ResolutionBlock { - float2 f2_0; - float2 f2_1; - float2 f2_2; - float2 f2_3; - int4 i4_0; - float2 f2_4; - float2 f2_5; - float2 f2_6; - float2 f2_7; - float2 f2_8; - int2 i2_0; - int4 i4_1; - float f_0; - int i_0; - int i_1; - int i_2; + float2 f2_0; + float2 f2_1; + float2 f2_2; + float2 f2_3; + int4 i4_0; + float2 f2_4; + float2 f2_5; + float2 f2_6; + float2 f2_7; + float2 f2_8; + int2 i2_0; + int4 i4_1; + float f_0; + int i_0; + int i_1; + int i_2; }; struct CameraBlock { - float3 f3_0; - int i_1; - float4x4 f4x4_0; - float4x4 f4x4_1; - float4x4 f4x4_2; - float4x4 f4x4_3; - float4x4 f4x4_4; - float4x4 f4x4_5; - float3 f3_1; - int i_0; - float4x4 f4x4_6; - float4x4 f4x4_7; - float4x4 f4x4_8; - float4 f4_0; - float4 f4_1; - float4 f4_2; - float4 f4_3; - float2 f2_0; - float f_0; - float f_1; + float3 f3_0; + int i_1; + float4x4 f4x4_0; + float4x4 f4x4_1; + float4x4 f4x4_2; + float4x4 f4x4_3; + float4x4 f4x4_4; + float4x4 f4x4_5; + float3 f3_1; + int i_0; + float4x4 f4x4_6; + float4x4 f4x4_7; + float4x4 f4x4_8; + float4 f4_0; + float4 f4_1; + float4 f4_2; + float4 f4_3; + float2 f2_0; + float f_0; + float f_1; }; struct CameraBlockArray { - CameraBlock cb0; - CameraBlock cb1; - CameraBlock cb2; - ResolutionBlock rb0; - ResolutionBlock rb1; - ResolutionBlock rb2; - ResolutionBlock rb3; - ResolutionBlock rb4; + CameraBlock cb0; + CameraBlock cb1; + CameraBlock cb2; + ResolutionBlock rb0; + ResolutionBlock rb1; + ResolutionBlock rb2; + ResolutionBlock rb3; + ResolutionBlock rb4; }; struct SPerSceneConstants { - CameraBlockArray cba; + CameraBlockArray cba; // WindData; // CameraExposureData; // GlobalLightData; @@ -100,21 +100,21 @@ struct SPerSceneConstants struct TonemappingParams { - float AcesParam0; - float AcesParam1; - float HableParam0; - float HableParam1; - float HableParam2; - float HableParam3; - float HableParam4; - int param7; //unused + float AcesParam0; + float AcesParam1; + float HableParam0; + float HableParam1; + float HableParam2; + float HableParam3; + float HableParam4; + int param7; //unused }; struct PushConstantWrapper_HDRComposite { - uint HdrCmpDatIndex; //index for HDRCompositeData - uint Tmo; //tone mapping operator - float BloomMultiplier; + uint HdrCmpDatIndex; //index for HDRCompositeData + uint Tmo; //tone mapping operator + float BloomMultiplier; }; struct FrameDebug { int2 u1; int2 u2; int2 u3; int u4; int u5; float u6; int u7; int u8; int u9; int u10; int u11; int u12; int u13; }; @@ -122,54 +122,54 @@ struct FrameData { int u1; int u2; float2 u3; float u4; float u5; float u6; floa struct HDRCompositeData { - float4 HighlightsColorFilter; - float4 ColorFilter; - float HableSaturation; - float BrightnessMultiplier; - float ContrastIntensity; - int i_0; //unused + float4 HighlightsColorFilter; + float4 ColorFilter; + float HableSaturation; + float BrightnessMultiplier; + float ContrastIntensity; + int i_0; //unused }; //HDRComposite shader structs start struct Hable_params { - float y0; - float y1; + float y0; + float y1; }; struct Hable_dstParams { - float W; + float W; }; struct Hable_toeSegment { - float lnA; - float B; + float lnA; + float B; }; struct Hable_midSigment { - float offsetX; - float lnA; + float offsetX; + float lnA; }; struct Hable_shoulderSegment { - float offsetX; - float offsetY; - float lnA; - float B; + float offsetX; + float offsetY; + float lnA; + float B; }; struct HableParamters { - Hable_params params; - Hable_dstParams dstParams; - Hable_toeSegment toeSegment; - Hable_midSigment midSegment; - Hable_shoulderSegment shoulderSegment; - float invScale; + Hable_params params; + Hable_dstParams dstParams; + Hable_toeSegment toeSegment; + Hable_midSigment midSegment; + Hable_shoulderSegment shoulderSegment; + float invScale; }; //HDRComposite shader structs end //HDRComposite end @@ -177,10 +177,10 @@ struct HableParamters //ColorGradingMerge start struct PushConstantWrapper_ColorGradingMerge { - float LUT1Percentage; - float LUT2Percentage; - float LUT3Percentage; - float LUT4Percentage; - float neutralLUTPercentage; + float LUT1Percentage; + float LUT2Percentage; + float LUT3Percentage; + float LUT4Percentage; + float neutralLUTPercentage; }; //ColorGradingMerge end From b156b7d2748bc19cf75ca12b4a366586782ba153 Mon Sep 17 00:00:00 2001 From: Lilium Date: Sun, 1 Oct 2023 00:37:40 +0200 Subject: [PATCH 047/270] forgot to change something --- .../ColorGradingMerge_FF81_cs.hlsl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index b79af6df..1631bfeb 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -304,32 +304,32 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) LUT1Color *= lerp(1.f, neutralLUTLuminance / LUT1Luminance, LUTCorrectionPercentage); if (SDRRange) { - LUT1Color = saturate(LUT1Color) - }; + LUT1Color = saturate(LUT1Color); + } } if (LUT2Luminance != 0.f) { LUT2Color *= lerp(1.f, neutralLUTLuminance / LUT2Luminance, LUTCorrectionPercentage); if (SDRRange) { - LUT2Color = saturate(LUT2Color) - }; + LUT2Color = saturate(LUT2Color); + } } if (LUT3Luminance != 0.f) { LUT3Color *= lerp(1.f, neutralLUTLuminance / LUT3Luminance, LUTCorrectionPercentage); if (SDRRange) { - LUT3Color = saturate(LUT3Color) - }; + LUT3Color = saturate(LUT3Color); + } } if (LUT4Luminance != 0.f) { LUT4Color *= lerp(1.f, neutralLUTLuminance / LUT4Luminance, LUTCorrectionPercentage); if (SDRRange) { - LUT4Color = saturate(LUT4Color) - }; + LUT4Color = saturate(LUT4Color); + } } #endif // LUT_IMPROVEMENT_TYPE From ecd44e705e58a7a526d40a52136046f200ed0544 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sun, 1 Oct 2023 12:10:09 +0300 Subject: [PATCH 048/270] -Add support for "INVERT_TONEMAP_HIGHLIGHTS_ONLY" to ACES, so that most of the image will have the same look as SDR -Improve LUTs shadow gradient -Remove AutoHDR (it was just a fun test) -Clean up code --- .../ColorGradingMerge_FF81_cs.hlsl | 3 +- shaders/HDRComposite/HDRComposite_ps.hlsl | 131 ++++++------------ shaders/color.hlsl | 3 +- shaders/math.hlsl | 2 +- 4 files changed, 47 insertions(+), 92 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 1631bfeb..7734728a 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -150,7 +150,8 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b // Distance from 0 is factor to decide how much each pixel should move, // with (1) (white) not moving at all. // *Applying to each channel individually also removes tint - const float3 reduceFactor = lerp(1.f / (1.f - analysis.black), 1.f, pow(neutralLUTColor, 2.f)); //TODO: test pow + static const float shadowCrushingModulation = 2.2f; // Defines the gradient with which shadow are shifted to zero //TODO: find best value + const float3 reduceFactor = lerp(1.f / (1.f - analysis.black), 1.f, pow(neutralLUTColor, shadowCrushingModulation)); // Reapply tint without raising black floor: // In 3D space, shift points away from black **per channel** to diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index cf047c76..94378202 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -15,8 +15,6 @@ // This disables most other features (post process, LUTs, ...) #define ENABLE_TONEMAP 1 -// Tweak the OG tonemappers to either look better in general, or simply be more compatible with HDR -#define ENABLE_TONEMAP_IMPROVEMENTS 1 #define ENABLE_POST_PROCESS 1 // 0 original (weak, generates values beyond 0-1 which then get clipped), 1 improved (looks more natural, avoids values below 0, but will overshoot beyond 1 more often, and will raise blacks), 2 Sigmoidal (smoothest looking, but harder to match) #define POST_PROCESS_CONTRAST_TYPE 1 @@ -30,8 +28,6 @@ #define INVERT_TONEMAP_BY_LUMINANCE 0 // Only invert highlights, which helps conserve the SDR filmic look (shadow crush) #define INVERT_TONEMAP_HIGHLIGHTS_ONLY 1 -// Use AutoHDR as "inverse tonemapper" (maintains a look closer to the original) -#define ENABLE_AUTOHDR 0 #define ENABLE_INVERSE_POST_PROCESS 0 #define ENABLE_LUMINANCE_RESTORE 1 #define CLAMP_INPUT_OUTPUT 1 @@ -159,9 +155,7 @@ T ACES_Inverse( float modE, float modA) { - //TODO: does this apply for `ACESParametric` as well? Probably - //TODO: figure out if we could still use the unclamped color. There should be a way to invert it. - // ACES is not defined for any values beyond 0-1 + // ACES is not defined for any values beyond 0-1, as they already represent the 0-INF range. Color = saturate(Color); T fixed0 = (-ACES_d * Color) + ACES_b; @@ -207,10 +201,6 @@ float3 Hable( const float shoulderStrength = max(PerSceneConstants[3267u].x, 0.f); // Constant is usually 9.9 const float shoulderAngle = saturate(PerSceneConstants[3267u].z); // Constant is usually 0.3 -#if ENABLE_TONEMAP_IMPROVEMENTS - //TODO: fix highlights -#endif - //dstParams float dstParams_x0 = toeLength * 0.5f; float dstParams_y0 = (1.f - toeStrength) * dstParams_x0; @@ -226,7 +216,7 @@ float3 Hable( float initialW = dstParams_x0 + remainingY; hableParams.dstParams.W = initialW + extraW; - // "W * " was optimised away by DXIL->SPIRV->HLSL conversion as down the line there is a "/ W" + // "W * " was optimised away by DXIL->SPIRV->HLSL conversion as down the line there was a "/ W" float dstParams_overshootX = 2.f * shoulderAngle * shoulderStrength; float dstParams_overshootY = 0.5f * shoulderAngle * shoulderStrength; @@ -308,10 +298,12 @@ float3 Hable( bool isToeSegment = normX < params_x0; float returnChannel = 0.f; + // Toe if (isToeSegment && normX > 0.f) { returnChannel = exp2(((((log2(normX) * hableParams.toeSegment.B) + _410) * RCP_LOG2_E) + toeSegment_lnA_optimised) * LOG2_E); } + // Mid else if (normX < params_x1) { float evalMidSegment_y0 = normX + hableParams.midSegment.offsetX; // was -(-hableParams.midSegment.offsetX) @@ -320,8 +312,11 @@ float3 Hable( returnChannel = exp2(log2(evalMidSegment_y0) + midSegment_lnA_optimised); } } + // Highlight else { + // Note that this will "clip" to 1 way before +INF + //float evalShoulderSegment_y0 = ((-1.f) - params_overshootX) + normX; // small optimisation from the original decompilation float evalShoulderSegment_y0 = (1.f + params_overshootX) - normX; float evalShoulderReturn = 0.f; @@ -346,12 +341,6 @@ float HableEval_Inverse( float lnA, float B) { -#if 0 //TODO: delete. This version might work better on Nvidia (to be investigated more before deleting the alternative branch) - float y0 = max((Channel - offsetY) / scaleY, EPSILON); - float x0 = exp((log(y0) - lnA) / B); - - return x0 / scaleX + offsetX; -#else float y0 = (Channel - offsetY) / scaleY; float x0 = 0.f; @@ -361,26 +350,27 @@ float HableEval_Inverse( } return x0 / scaleX + offsetX; -#endif } void Hable_Inverse_Channel( inout float ColorChannel, in HableParamters hableParams, + inout float minHighlightsColor, in bool onlyInvertHighlights = false) { // Scale all non highlights by the scale the smallest (first) highlight would have, so we keep the curves connected - const float minHighlightsColor = max(hableParams.params.y0, hableParams.params.y1); // Check both toe and mid params for extra safety - - if (onlyInvertHighlights && ColorChannel < minHighlightsColor) + const float targetMinHighlightsColor = max(hableParams.params.y0, hableParams.params.y1); // Check both toe and mid params for extra safety + if (onlyInvertHighlights && ColorChannel < targetMinHighlightsColor) { - ColorChannel *= HableEval_Inverse(minHighlightsColor, - hableParams.shoulderSegment.offsetX, - hableParams.shoulderSegment.offsetY * hableParams.invScale, - -1.f, - -1.f * hableParams.invScale, - hableParams.shoulderSegment.lnA, - hableParams.shoulderSegment.B) / minHighlightsColor; + const float remappedMinHighlightsColor = HableEval_Inverse(targetMinHighlightsColor, + hableParams.shoulderSegment.offsetX, + hableParams.shoulderSegment.offsetY * hableParams.invScale, + -1.f, + -1.f * hableParams.invScale, + hableParams.shoulderSegment.lnA, + hableParams.shoulderSegment.B); + ColorChannel *= remappedMinHighlightsColor / targetMinHighlightsColor; + minHighlightsColor = remappedMinHighlightsColor; return; } @@ -412,13 +402,6 @@ void Hable_Inverse_Channel( // shoulder else { -#if ENABLE_TONEMAP_IMPROVEMENTS //TODO: remove... temp workaround for broken shoulder - // seems to be Nvidia specific workaround? - // maybe use BTHCNST - ColorChannel = min(ColorChannel, 0.99999f); - -#endif - // scaleXY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L175-L176 ColorChannel = HableEval_Inverse(ColorChannel, hableParams.shoulderSegment.offsetX, @@ -436,6 +419,7 @@ T Hable_Inverse( T InputColor, uint Channels /*1 to 3*/, //there has to be better solution than this HableParamters hableParams, + inout float minHighlightsColor, bool onlyInvertHighlights = false) { // There's no inverse formula for colors beyond the 0-1 range @@ -447,6 +431,7 @@ T Hable_Inverse( { Hable_Inverse_Channel(itmColor.x, hableParams, + minHighlightsColor, onlyInvertHighlights); } else @@ -455,6 +440,7 @@ T Hable_Inverse( { Hable_Inverse_Channel(itmColor[channel], hableParams, + minHighlightsColor, onlyInvertHighlights); } } @@ -463,44 +449,22 @@ T Hable_Inverse( } // Tonemapper inspired from DICE. Works on luminance to maintain hue, not per channel. +// "HighlightsShoulderStart" should be between 0 and 1. Determines where the highlights curve (shoulder) starts. Leaving at zero for now as it's a simple and good looking default. float3 DICETonemap( float3 Color, - float MaxOutputLuminance) + float MaxOutputLuminance, + float HighlightsShoulderStart = 0.f) { - // Between 0 and 1. Determines where the highlights curve (shoulder) starts. - // Leaving at zero for now as it's a simple and good looking default. - const float highlightsShoulderStart = 0.f; - + //TODO: try "per channel" option given all other tm is per channel const float sourceLuminance = Luminance(Color); if (sourceLuminance > 0.0f) { - const float compressedLuminance = luminanceCompress(sourceLuminance, MaxOutputLuminance, highlightsShoulderStart); + const float compressedLuminance = luminanceCompress(sourceLuminance, MaxOutputLuminance, HighlightsShoulderStart); Color *= compressedLuminance / sourceLuminance; } return Color; } -// AutoHDR pass to generate some HDR brightess out of an SDR signal (it has no effect if HDR is not engaged). -// This is hue conserving and only really affects highlights. -// https://github.com/Filoppi/PumboAutoHDR -float3 PumboAutoHDR( - float3 Color, - float MaxOutputNits, - float PaperWhite) -{ - static const float AutoHDRShoulderPow = 3.5f; //TODO: exposure to user? - - const float SDRRatio = Luminance(Color); - // Limit AutoHDR brightness, it won't look good beyond a certain level. - // The paper white multiplier is applied later so we account for that. - const float AutoHDRMaxWhite = max(MaxOutputNits / PaperWhite, WhiteNits_BT709) / WhiteNits_BT709; - const float AutoHDRShoulderRatio = 1.f - max(1.f - SDRRatio, 0.f); - const float AutoHDRExtraRatio = pow(AutoHDRShoulderRatio, AutoHDRShoulderPow) * (AutoHDRMaxWhite - 1.f); - const float AutoHDRTotalRatio = SDRRatio + AutoHDRExtraRatio; - - return Color * (AutoHDRTotalRatio / SDRRatio); -} - float FindLuminanceToRestoreScale( float3 color, float tonemapLostLuminance, @@ -535,7 +499,7 @@ float FindLuminanceToRestoreScale( float scale = luminanceCCInvChange; #endif - const float targetLum = max(currentLum + tonemapLostLuminance, 0.f); //TODO: review max() math + const float targetLum = max(currentLum + tonemapLostLuminance, 0.f); scale *= targetLum / currentLum; return scale; } @@ -885,7 +849,7 @@ PSOutput PS(PSInput psInput) LUTColor = lerp(LUTColor, color, LUTMaskAlpha); -#if ENABLE_APPLY_LUT_AFTER_INVERSE_TONEMAP && ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP +#if ENABLE_APPLY_LUT_AFTER_INVERSE_TONEMAP && ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP && ENABLE_HDR //TODO: try this with the post process pass as well float3 LUTColorShift = LUTColor / color; @@ -925,20 +889,9 @@ PSOutput PS(PSInput psInput) const float paperWhite = HDR_GAME_PAPER_WHITE; - const float midGrayIn = MidGray; + const float midGrayIn = MidGray; //TODO: try to increase this to ~0.21 float midGrayOut = midGrayIn; -#if ENABLE_AUTOHDR - - if (tonemapperIndex == 1 || tonemapperIndex == 2 || tonemapperIndex == 3) - { - tonemapperIndex = 0; - color = PumboAutoHDR(color, HDR_MAX_OUTPUT_NITS, paperWhite); - midGrayOut = PumboAutoHDR(midGrayIn.xxx, HDR_MAX_OUTPUT_NITS, paperWhite).x; - } - -#endif // ENABLE_AUTOHDR - #if INVERT_TONEMAP_BY_LUMINANCE float preInverseTonemapColorLuminance = Luminance(color); @@ -952,15 +905,17 @@ PSOutput PS(PSInput psInput) #endif // INVERT_TONEMAP_BY_LUMINANCE + float minHighlightsColor = pow(2.f / 3.f, 2.2f); // We consider highlight the last ~33% of perception space + const bool onlyInvertHighlights = (bool)INVERT_TONEMAP_HIGHLIGHTS_ONLY; + // Restore a color very close to the original linear one, but with all the other post process and LUT transformations baked in switch (tonemapperIndex) { - //TODO: implement INVERT_TONEMAP_HIGHLIGHTS_ONLY for ACES - case 1: { inverseTonemapColor = ACESReference_Inverse(inverseTonemapColor); midGrayOut = ACESReference_Inverse(midGrayIn); + minHighlightsColor = ACESReference_Inverse(minHighlightsColor); tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; postColorCorrectionLuminance = postColorCorrectionClampedLuminance; } break; @@ -969,21 +924,22 @@ PSOutput PS(PSInput psInput) { inverseTonemapColor = ACESParametric_Inverse(inverseTonemapColor, acesParam_modE, acesParam_modA); midGrayOut = ACESParametric_Inverse(midGrayIn, acesParam_modE, acesParam_modA); + minHighlightsColor = ACESParametric_Inverse(minHighlightsColor, acesParam_modE, acesParam_modA); tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; postColorCorrectionLuminance = postColorCorrectionClampedLuminance; } break; case 3: { - const bool onlyInvertHighlights = (bool)INVERT_TONEMAP_HIGHLIGHTS_ONLY; - inverseTonemapColor = Hable_Inverse(inverseTonemapColor, inverseTonemapColorComponents, hableParams, + minHighlightsColor, onlyInvertHighlights); midGrayOut = Hable_Inverse(midGrayIn, 1, hableParams, + minHighlightsColor, onlyInvertHighlights); tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; @@ -1013,7 +969,7 @@ PSOutput PS(PSInput psInput) #endif // ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS -#if ENABLE_LUMINANCE_RESTORE //TODO: this will go beyond the max output nits if "ENABLE_AUTOHDR" is true, we need to fix a way to make them work together (maybe re-tonemap always at the end?) +#if ENABLE_LUMINANCE_RESTORE // Restore any luminance beyond 1 that ended up clipped by HDR->SDR tonemappers and any subsequent image manipulation. // It's important to do these after the inverse tonemappers, as they can't handle values beyond 0-1. @@ -1036,18 +992,15 @@ PSOutput PS(PSInput psInput) // Bring back the color to the same range as SDR by dividing by the mid gray change. color *= paperWhite / midGrayScale; + minHighlightsColor *= paperWhite / midGrayScale; -#if !ENABLE_AUTOHDR - - //TODO: find a tonemapper that looks more like Hable? const float maxOutputLuminance = HDR_MAX_OUTPUT_NITS / WhiteNits_BT709; - color = DICETonemap(color, maxOutputLuminance); - -#endif // !ENABLE_AUTOHDR + const float highlightsShoulderStart = (bool)INVERT_TONEMAP_HIGHLIGHTS_ONLY ? minHighlightsColor : 0.f; + color = DICETonemap(color, maxOutputLuminance, highlightsShoulderStart); #else // ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP - color *= HDR_GAME_PAPER_WHITE; + color *= HDR_GAME_PAPER_WHITE_MULTIPLIER; #endif // ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP diff --git a/shaders/color.hlsl b/shaders/color.hlsl index 4bcf4ed0..1a681b27 100644 --- a/shaders/color.hlsl +++ b/shaders/color.hlsl @@ -4,7 +4,8 @@ static const float WhiteNits_BT709 = 80.f; static const float ReferenceWhiteNits_BT2408 = 203.f; -// SDR mid gray +// SDR mid gray. +// This is based on the commonly used value, though perception space mid gray in sRGB or Gamma 2.2 would theoretically be ~0.2155 static const float MidGray = 0.18f; // SMPTE ST 2084 (Perceptual Quantization) is only defined until this amount of nits. diff --git a/shaders/math.hlsl b/shaders/math.hlsl index 778f17ce..f8ffc6ba 100644 --- a/shaders/math.hlsl +++ b/shaders/math.hlsl @@ -43,7 +43,7 @@ float rangeCompressPow(float x, float fMax = FLT_MAX, float fPow = 1.f) return (1.f - pow(exp(-x), fPow)) * fRestoreRangeScale; } -// Refurbished DICE HDR tonemapper +// Refurbished DICE HDR tonemapper (per channel or luminance) float luminanceCompress(float fInValue, float fOutMaxValue, float fShoulderStart = 0.f, float fInMaxValue = FLT_MAX, float fModulationPow = 1.f) { float fCompressableValue = fInValue - fShoulderStart; From 2fc07667ec7354ae3991e3685650b2665b3f9cfd Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sun, 1 Oct 2023 18:10:59 +0300 Subject: [PATCH 049/270] -Moved PostProcess changes to apply after inverse tonemap, just like the LUT already was (HDR only) (they are cached on the side when computing the SDR image) -Fixed Inverse Hable being unable to restore highlights, by simply using the original color instead, given that there's no post process anymore happening between tm and inv tm -Improved "INVERT_TONEMAP_HIGHLIGHTS_ONLY" (it was not fully implemented with ACES, and was lacking mid gray adjustments) -Polish code a bit and remove unnecesary old stuff --- .../ColorGradingMerge_FF81_cs.hlsl | 19 +- shaders/HDRComposite/HDRComposite_ps.hlsl | 420 ++++++------------ shaders/color.hlsl | 4 + shaders/math.hlsl | 20 + shaders/shared.hlsl | 4 - 5 files changed, 171 insertions(+), 296 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 7734728a..a3910f5a 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -2,13 +2,16 @@ #include "../color.hlsl" #include "../math.hlsl" +// 0 None, 1 ShortFuse technique (normalization), 2 luminance preservation (doesn't look so good and it's kinda broken in SDR) +#define LUT_IMPROVEMENT_TYPE 1 #define FORCE_SDR_LUTS 0 // Make some small quality cuts for the purpose of optimization #define OPTIMIZE_LUT_ANALYSIS true // For future development #define UNUSED_PARAMS 0 -static float additionalNeutralLUTPercentage = 0.f; // ~0.25 might be a good compromise +static float AdditionalNeutralLUTPercentage = 0.f; // ~0.25 might be a good compromise +static float LUTCorrectionPercentage = 1.f; cbuffer CPushConstantWrapper_ColorGradingMerge : register(b0, space0) { @@ -19,7 +22,7 @@ Texture2D LUT1 : register(t0, space8); Texture2D LUT2 : register(t1, space8); Texture2D LUT3 : register(t2, space8); Texture2D LUT4 : register(t3, space8); -RWTexture3D OutMixetLUT : register(u0, space8); +RWTexture3D OutMixedLUT : register(u0, space8); struct LUTAnalysis { @@ -334,11 +337,11 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) } #endif // LUT_IMPROVEMENT_TYPE - float adjustedNeutralLUTPercentage = lerp(PcwColorGradingMerge.neutralLUTPercentage, 1.f, additionalNeutralLUTPercentage); - float adjustedLUT1Percentage = lerp(PcwColorGradingMerge.LUT1Percentage, 0.f, additionalNeutralLUTPercentage); - float adjustedLUT2Percentage = lerp(PcwColorGradingMerge.LUT2Percentage, 0.f, additionalNeutralLUTPercentage); - float adjustedLUT3Percentage = lerp(PcwColorGradingMerge.LUT3Percentage, 0.f, additionalNeutralLUTPercentage); - float adjustedLUT4Percentage = lerp(PcwColorGradingMerge.LUT4Percentage, 0.f, additionalNeutralLUTPercentage); + float adjustedNeutralLUTPercentage = lerp(PcwColorGradingMerge.neutralLUTPercentage, 1.f, AdditionalNeutralLUTPercentage); + float adjustedLUT1Percentage = lerp(PcwColorGradingMerge.LUT1Percentage, 0.f, AdditionalNeutralLUTPercentage); + float adjustedLUT2Percentage = lerp(PcwColorGradingMerge.LUT2Percentage, 0.f, AdditionalNeutralLUTPercentage); + float adjustedLUT3Percentage = lerp(PcwColorGradingMerge.LUT3Percentage, 0.f, AdditionalNeutralLUTPercentage); + float adjustedLUT4Percentage = lerp(PcwColorGradingMerge.LUT4Percentage, 0.f, AdditionalNeutralLUTPercentage); float3 mixedLUT = (adjustedNeutralLUTPercentage * neutralLUTColor) + (adjustedLUT1Percentage * LUT1Color) @@ -353,5 +356,5 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) #endif // !LUT_FIX_GAMMA_MAPPING - OutMixetLUT[outUVW] = float4(mixedLUT, 1.f); + OutMixedLUT[outUVW] = float4(mixedLUT, 1.f); } diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 94378202..0eb02563 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -21,15 +21,9 @@ #define ENABLE_LUT 1 // LUTs are too low resolutions to resolve gradients smoothly if the LUT color suddenly changes between samples #define ENABLE_LUT_TETRAHEDRAL_INTERPOLATION 1 -// Applies LUTs after inverse tonemap, so we have HDR values in them (inv tonemappers only take 0-1 range) -#define ENABLE_APPLY_LUT_AFTER_INVERSE_TONEMAP 1 #define ENABLE_REPLACED_TONEMAP 1 -// Invert tonemap by luminance (conserves SDR hue (and damped saturation look)) -#define INVERT_TONEMAP_BY_LUMINANCE 0 // Only invert highlights, which helps conserve the SDR filmic look (shadow crush) #define INVERT_TONEMAP_HIGHLIGHTS_ONLY 1 -#define ENABLE_INVERSE_POST_PROCESS 0 -#define ENABLE_LUMINANCE_RESTORE 1 #define CLAMP_INPUT_OUTPUT 1 @@ -53,8 +47,8 @@ Texture2D InputColor : register(t0, space9); #if defined(APPLY_MERGED_COLOR_GRADING_LUT) -Texture2D LutMask : register(t1, space9); -Texture3D Lut : register(t3, space9); +Texture2D LUTMaskTexture : register(t1, space9); +Texture3D LUTTexture : register(t3, space9); #endif @@ -100,8 +94,10 @@ struct PSOutput // exp2(x * 1.44269502162933349609375f) is the same as exp(x) -static const float LUTStrength = 1.f; -static const float RestorePreLUTLuminance = 1.f; +static const float TonemapStrength = 1.f; //TODO: implement +static const float PostProcessStrength = 1.f; //TODO: implement +// Similar to "AdditionalNeutralLUTPercentage" in the LUT shader, though this is more precise as it skips the precision loss induced by a neutral LUT +static const float GradingLUTStrength = 1.f; // 0 Ignored, 1 ACES Reference, 2 ACES Custom, 3 Hable, 4+ Disable tonemapper static const uint ForceTonemapper = 0; @@ -328,7 +324,7 @@ float3 Hable( } toneMapped[channel] = returnChannel * hableParams.invScale; } - return toneMapped; + return toneMapped; // Note: this color needs no clamping, it's already implied to be between 0-1 } // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L21-L34 @@ -354,26 +350,8 @@ float HableEval_Inverse( void Hable_Inverse_Channel( inout float ColorChannel, - in HableParamters hableParams, - inout float minHighlightsColor, - in bool onlyInvertHighlights = false) + in HableParamters hableParams) { - // Scale all non highlights by the scale the smallest (first) highlight would have, so we keep the curves connected - const float targetMinHighlightsColor = max(hableParams.params.y0, hableParams.params.y1); // Check both toe and mid params for extra safety - if (onlyInvertHighlights && ColorChannel < targetMinHighlightsColor) - { - const float remappedMinHighlightsColor = HableEval_Inverse(targetMinHighlightsColor, - hableParams.shoulderSegment.offsetX, - hableParams.shoulderSegment.offsetY * hableParams.invScale, - -1.f, - -1.f * hableParams.invScale, - hableParams.shoulderSegment.lnA, - hableParams.shoulderSegment.B); - ColorChannel *= remappedMinHighlightsColor / targetMinHighlightsColor; - minHighlightsColor = remappedMinHighlightsColor; - return; - } - // scaleY and offsetY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L187-L197 // toe if (ColorChannel < hableParams.params.y0) @@ -418,9 +396,7 @@ template T Hable_Inverse( T InputColor, uint Channels /*1 to 3*/, //there has to be better solution than this - HableParamters hableParams, - inout float minHighlightsColor, - bool onlyInvertHighlights = false) + HableParamters hableParams) { // There's no inverse formula for colors beyond the 0-1 range InputColor = saturate(InputColor); @@ -429,19 +405,13 @@ T Hable_Inverse( if (Channels <= 1) // InputColor[channel] won't compile if T is float(1) { - Hable_Inverse_Channel(itmColor.x, - hableParams, - minHighlightsColor, - onlyInvertHighlights); + Hable_Inverse_Channel(itmColor.x, hableParams); } else { for (uint channel = 0; channel < Channels; channel++) { - Hable_Inverse_Channel(itmColor[channel], - hableParams, - minHighlightsColor, - onlyInvertHighlights); + Hable_Inverse_Channel(itmColor[channel], hableParams); } } @@ -465,47 +435,6 @@ float3 DICETonemap( return Color; } -float FindLuminanceToRestoreScale( - float3 color, - float tonemapLostLuminance, - float preColorCorrectionLuminance, - float postColorCorrectionLuminance, - float midGrayScale = 1.f) -{ - // Try to restore any luminance that would have been lost during tone mapping (e.g. tonemapping, post process, LUTs, ... can all clip values). - // - // To achieve this, we re-apply the lost luminance, but only by the inverse amount the luminance changed during the color correction. - // e.g. if the color correction fully changed the luminance, then we don't change it any further, - // but if the color correction changed the luminance only by 25%, then we re-apply apply 75% of the lost luminance on top. - // This will be "accurate" as long as the color correction scaled the luminance linearly in the output as it grew in the input, - // but even if it didn't, it should still look fine. - - // This will re-apply an amount of lost luminance based on how much the CC (LUT) reduced it: - // the more the CC reduced the luminance, the less we will apply. - // If the CC increased the luminance though, we will apply even more than the base lost amount. - const float currentLum = Luminance(color); - if (currentLum > 0.f) - { - // Change the "tonemapLostLuminance" from being in the tonemapped SDR (0-1) range to the untonemapped HDR image range (by scaling it by the mid gray change, which is the best we can do). - // This will also acknowledge any clipped luminance from the range beyond 0-1. - tonemapLostLuminance = isnan(tonemapLostLuminance) ? 0.f : (tonemapLostLuminance * midGrayScale); - -#if defined(APPLY_MERGED_COLOR_GRADING_LUT) && ENABLE_LUT && !ENABLE_APPLY_LUT_AFTER_INVERSE_TONEMAP && LUT_IMPROVEMENT_TYPE == 3 // Restore any luminance lost by the LUT - float scale = 1.f; - scale *= postColorCorrectionLuminance > 0.f ? lerp(1.f, preColorCorrectionLuminance / postColorCorrectionLuminance, LUTCorrectionPercentage) : 1.f; -#else - const float luminanceCCInvChange = max(1.f - (preColorCorrectionLuminance - postColorCorrectionLuminance), 0.f); - //const float luminanceCCInvChange = clamp(postColorCorrectionLuminance / preColorCorrectionLuminance, 0.f, 2.f); //TODO: try this, especially if brightness goes crazy high - float scale = luminanceCCInvChange; -#endif - - const float targetLum = max(currentLum + tonemapLostLuminance, 0.f); - scale *= targetLum / currentLum; - return scale; - } - return 1.f; -} - // "MidGrayScale" is how much the mid gray shifted from the originally intended tonemapped input (e.g. if we run this function on the untonemapped image, we can remap the mid gray) float3 PostProcess( float3 Color, @@ -565,7 +494,7 @@ float3 PostProcess( float3 PostProcess_Inverse( float3 Color, - float ColorLuminance, + float OriginalColorLuminance, float MidGrayScale = 1.f) { const uint hdrCmpDatIndex = PcwHdrComposite.HdrCmpDatIndex; @@ -595,16 +524,16 @@ float3 PostProcess_Inverse( Color /= brightnessMultiplier; - Color -= lerp(float3(0.f, 0.f, 0.f), ColorLuminance * highlightsColorFilter.rgb, highlightsColorFilter.a); + Color -= lerp(float3(0.f, 0.f, 0.f), OriginalColorLuminance * highlightsColorFilter.rgb, highlightsColorFilter.a); - Color = ((Color - ColorLuminance) / hableSaturation) + ColorLuminance; + Color = ((Color - OriginalColorLuminance) / hableSaturation) + OriginalColorLuminance; return Color; } // Takes input coordinates. Returns output color in linear space (also works in sRGB but it's not ideal). float3 TetrahedralInterpolation( - Texture3D Lut, + Texture3D LUTTextureIn, float3 LUTCoordinates) { // We need to clip the input coordinates as LUT texure samples below are not clamped. @@ -620,18 +549,18 @@ float3 TetrahedralInterpolation( float3 f1, f4; - float3 v1 = Lut.Load(int4(baseInd, 0)).rgb; - float3 v4 = Lut.Load(int4(nextInd, 0)).rgb; + float3 v1 = LUTTextureIn.Load(int4(baseInd, 0)).rgb; + float3 v4 = LUTTextureIn.Load(int4(nextInd, 0)).rgb; if (fract.r >= fract.g) { if (fract.g >= fract.b) // R > G > B { nextInd = baseInd + int3(1, 0, 0); - float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; + float3 v2 = LUTTextureIn.Load(int4(nextInd, 0)).rgb; nextInd = baseInd + int3(1, 1, 0); - float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; + float3 v3 = LUTTextureIn.Load(int4(nextInd, 0)).rgb; f1 = 1.f - fract.r; f4 = fract.b; @@ -643,10 +572,10 @@ float3 TetrahedralInterpolation( else if (fract.r >= fract.b) // R > B > G { nextInd = baseInd + int3(1, 0, 0); - float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; + float3 v2 = LUTTextureIn.Load(int4(nextInd, 0)).rgb; nextInd = baseInd + int3(1, 0, 1); - float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; + float3 v3 = LUTTextureIn.Load(int4(nextInd, 0)).rgb; f1 = 1.f - fract.r; f4 = fract.g; @@ -658,10 +587,10 @@ float3 TetrahedralInterpolation( else // B > R > G { nextInd = baseInd + int3(0, 0, 1); - float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; + float3 v2 = LUTTextureIn.Load(int4(nextInd, 0)).rgb; nextInd = baseInd + int3(1, 0, 1); - float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; + float3 v3 = LUTTextureIn.Load(int4(nextInd, 0)).rgb; f1 = 1.f - fract.b; f4 = fract.g; @@ -676,10 +605,10 @@ float3 TetrahedralInterpolation( if (fract.g <= fract.b) // B > G > R { nextInd = baseInd + int3(0, 0, 1); - float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; + float3 v2 = LUTTextureIn.Load(int4(nextInd, 0)).rgb; nextInd = baseInd + int3(0, 1, 1); - float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; + float3 v3 = LUTTextureIn.Load(int4(nextInd, 0)).rgb; f1 = 1.f - fract.b; f4 = fract.r; @@ -691,10 +620,10 @@ float3 TetrahedralInterpolation( else if (fract.r >= fract.b) // G > R > B { nextInd = baseInd + int3(0, 1, 0); - float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; + float3 v2 = LUTTextureIn.Load(int4(nextInd, 0)).rgb; nextInd = baseInd + int3(1, 1, 0); - float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; + float3 v3 = LUTTextureIn.Load(int4(nextInd, 0)).rgb; f1 = 1.f - fract.g; f4 = fract.b; @@ -706,10 +635,10 @@ float3 TetrahedralInterpolation( else // G > B > R { nextInd = baseInd + int3(0, 1, 0); - float3 v2 = Lut.Load(int4(nextInd, 0)).rgb; + float3 v2 = LUTTextureIn.Load(int4(nextInd, 0)).rgb; nextInd = baseInd + int3(0, 1, 1); - float3 v3 = Lut.Load(int4(nextInd, 0)).rgb; + float3 v3 = LUTTextureIn.Load(int4(nextInd, 0)).rgb; f1 = 1.f - fract.g; f4 = fract.r; @@ -723,9 +652,54 @@ float3 TetrahedralInterpolation( return color + (f1 * v1) + (f4 * v4); } +#if defined(APPLY_MERGED_COLOR_GRADING_LUT) +// In/Out linear space +float3 GradingLUT(float3 color, float2 uv) +{ +#if FIX_WRONG_SRGB_GAMMA_FORMULA + const float3 LUTCoordinates = gamma_linear_to_sRGB(color); +#else + // Read gamma ini value, defaulting at 2.4 (makes little sense) + float inverseGamma = 1.f / (max(SharedFrameData.Gamma, 0.001f)); + // Weird linear -> sRGB conversion that clips values just above 0, and raises blacks. + const float3 LUTCoordinates = max((pow(color, inverseGamma) * 1.055f) - 0.055f, 0.f); // Does "max()" is probably unnecessary as LUT sampling is already clamped. +#endif // FIX_WRONG_SRGB_GAMMA_FORMULA + +#if ENABLE_LUT_TETRAHEDRAL_INTERPOLATION + float3 LUTColor = TetrahedralInterpolation(LUTTexture, LUTCoordinates); +#else + float3 LUTColor = LUTTexture.Sample(Sampler0, LUTCoordinates * (1.f - (1.f / LUT_SIZE)) + ((1.f / LUT_SIZE) / 2.f)); +#endif // ENABLE_LUT_TETRAHEDRAL_INTERPOLATION + +#if !LUT_FIX_GAMMA_MAPPING + // We always work in linear space so convert to it. + // We never acknowledge the original wrong gamma function here (we don't really care). + LUTColor = gamma_sRGB_to_linear(LUTColor); +#endif // !LUT_FIX_GAMMA_MAPPING + + const float LUTMaskAlpha = saturate(LUTMaskTexture.Sample(Sampler0, uv).x + (1.f - GradingLUTStrength)); + LUTColor = lerp(LUTColor, color, LUTMaskAlpha); + + return LUTColor; +} +#endif // APPLY_MERGED_COLOR_GRADING_LUT + +// Takes a linear space untonemapped HDR color and a linear space tonemapped SDR color. +// "midGrayScale" is how much mid gray shifted when doing tm->inv_tm. +float3 RestorePostProcess(float3 inverseTonemappedColor, float3 postProcessColorRatio, float3 postProcessColorOffset, float3 tonemappedColor, float midGrayScale) +{ + float3 postProcessedRatioColor = inverseTonemappedColor; + float3 postProcessedOffsetColor = inverseTonemappedColor; + postProcessedRatioColor *= postProcessColorRatio; + postProcessedOffsetColor += postProcessColorOffset * midGrayScale; + // Near black, we prefer using the "offset" (sum) pp restoration method, as otherwise any raised black would not work, + // for example if any zero was shifted to a more raised color, "postProcessColorRatio" would not be able to replicate that shift. + return lerp(postProcessedOffsetColor, postProcessedRatioColor, saturate(tonemappedColor / MaxShadowsColor)); +} PSOutput PS(PSInput psInput) { + // Linear HDR color straight from the renderer (possibly with exposure pre-applied to it, assuming the game has some auto exposure mechanism) float3 inputColor = InputColor.Load(int3(int2(psInput.SV_Position.xy), 0)); #if CLAMP_INPUT_OUTPUT @@ -740,18 +714,15 @@ PSOutput PS(PSInput psInput) float3 bloom = Bloom.Sample(Sampler0, psInput.TEXCOORD); float bloomMultiplier = PcwHdrComposite.BloomMultiplier; - inputColor = (bloomMultiplier * bloom) + inputColor; + inputColor += bloomMultiplier * bloom; #endif // APPLY_BLOOM float3 tonemappedColor; - float3 tonemappedUnclampedColor; #if !ENABLE_TONEMAP - tonemappedColor = inputColor; - tonemappedUnclampedColor = inputColor; - + float3 tonemappedPostProcessedColor = tonemappedColor; #else float acesParam_modE; @@ -767,272 +738,153 @@ PSOutput PS(PSInput psInput) { case 1: { - tonemappedUnclampedColor = ACESReference(inputColor, clampACES); - tonemappedColor = saturate(tonemappedUnclampedColor); + tonemappedColor = ACESReference(inputColor, clampACES); } break; case 2: { - tonemappedUnclampedColor = ACESParametric(inputColor, clampACES, acesParam_modE, acesParam_modA); - tonemappedColor = saturate(tonemappedUnclampedColor); + tonemappedColor = ACESParametric(inputColor, clampACES, acesParam_modE, acesParam_modA); } break; case 3: { - tonemappedUnclampedColor = Hable(inputColor, hableParams); - tonemappedColor = tonemappedUnclampedColor; // Hable was never clamped + tonemappedColor = Hable(inputColor, hableParams); } break; default: { - tonemappedUnclampedColor = inputColor; - tonemappedColor = saturate(tonemappedUnclampedColor); + tonemappedColor = inputColor; } break; } #if ENABLE_POST_PROCESS - float prePostProcessColorLuminance; - float prePostProcessUnclampedColorLuminance; - - tonemappedColor = PostProcess(tonemappedColor, prePostProcessColorLuminance); - // Repeat on unclamped tonemapped color to later retrieve the lost luminance - tonemappedUnclampedColor = PostProcess(tonemappedUnclampedColor, prePostProcessUnclampedColorLuminance); - + float3 tonemappedPostProcessedColor = PostProcess(tonemappedColor, prePostProcessColorLuminance); +#else + float3 tonemappedPostProcessedColor = tonemappedColor; // No need to do anything (not even a saturate here) #endif //ENABLE_POST_PROCESS - tonemappedColor = saturate(tonemappedColor); - #endif // ENABLE_TONEMAP - // This is the luminance lost by clamping SDR tonemappers to the 0-1 range, and again by the fact that LUTs only take 0-1 range input. - float tonemapLostLuminance = Luminance(tonemappedUnclampedColor - tonemappedColor); - - float3 color = tonemappedColor; - - const float preColorCorrectionLuminance = Luminance(color); + float3 tonemappedPostProcessedGradedColor = tonemappedPostProcessedColor; #if defined(APPLY_MERGED_COLOR_GRADING_LUT) && ENABLE_TONEMAP && ENABLE_LUT + tonemappedPostProcessedGradedColor = GradingLUT(tonemappedPostProcessedColor, psInput.TEXCOORD); +#endif // APPLY_MERGED_COLOR_GRADING_LUT -#if FIX_WRONG_SRGB_GAMMA_FORMULA - - const float3 LUTCoordinates = gamma_linear_to_sRGB(color); - -#else - - // Read gamma ini value, defaulting at 2.4 (makes little sense) - float inverseGamma = 1.f / (max(SharedFrameData.Gamma, 0.001f)); - // Weird linear -> sRGB conversion that clips values just above 0, and raises blacks. - const float3 LUTCoordinates = max((pow(color, inverseGamma) * 1.055f) - 0.055f, 0.f); // Does "max()" is unnecessary as LUT sampling is already clamped. - -#endif // FIX_WRONG_SRGB_GAMMA_FORMULA - -#if ENABLE_LUT_TETRAHEDRAL_INTERPOLATION - - float3 LUTColor = TetrahedralInterpolation(Lut, LUTCoordinates); - -#else - - float3 LUTColor = Lut.Sample(Sampler0, LUTCoordinates * (1.f - (1.f / LUT_SIZE)) + ((1.f / LUT_SIZE) / 2.f)); - -#endif // ENABLE_LUT_TETRAHEDRAL_INTERPOLATION - -#if !LUT_FIX_GAMMA_MAPPING - - // We always work in linear space so convert to it. - // We never acknowledge the original wrong gamma function here (we don't really care). - LUTColor = gamma_sRGB_to_linear(LUTColor); - -#endif // !LUT_FIX_GAMMA_MAPPING - - const float LUTMaskAlpha = saturate(LutMask.Sample(Sampler0, psInput.TEXCOORD).x + (1.f - LUTStrength)); - - LUTColor = lerp(LUTColor, color, LUTMaskAlpha); - -#if ENABLE_APPLY_LUT_AFTER_INVERSE_TONEMAP && ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP && ENABLE_HDR //TODO: try this with the post process pass as well - - float3 LUTColorShift = LUTColor / color; - - for (uint channel = 0; channel < 3; channel++) - { - if (color[channel] == 0.f) - { - LUTColorShift[channel] = 1.f; - } - } - -#else - - color = LUTColor; + const float3 finalOriginalColor = tonemappedPostProcessedGradedColor; // Final "original" (vanilla, ~unmodded) linear SDR color before output transform +#if ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP && ENABLE_HDR //TODO: try this with the post process pass as well + const float3 postProcessColorRatio = safeDivision(finalOriginalColor, tonemappedColor); + const float3 postProcessColorOffset = finalOriginalColor - tonemappedColor; #endif -#endif // APPLY_MERGED_COLOR_GRADING_LUT - #if ENABLE_HDR #if ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP - float postColorCorrectionLuminance = Luminance(color); - //TODO: this isn't hue conserving, though we don't really have a way of doing it in a hue conserving way, as then it would break the brightness mapping. - const float postColorCorrectionClampedLuminance = Luminance(saturate(color)); - // Some inverse tonemapper formulas can't take any values beyond 0-1, so we'll need to clip them and restore their luminance. - const float colorCorrectionHDRClippedLuminanceChange = postColorCorrectionClampedLuminance > 0.f ? (postColorCorrectionLuminance / postColorCorrectionClampedLuminance) : 1.f; - -#if ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS - - //TODO: passing in the previous luminance might not be the best, though is there any other way really? - //Should we at least shift it by how much the LUT shifted the luminance? To invert it more correctly. - color = PostProcess_Inverse(color, prePostProcessColorLuminance); - -#endif // ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS - const float paperWhite = HDR_GAME_PAPER_WHITE; - const float midGrayIn = MidGray; //TODO: try to increase this to ~0.21 + const float midGrayIn = MidGray; //TODO: try to increase this to ~0.21? float midGrayOut = midGrayIn; -#if INVERT_TONEMAP_BY_LUMINANCE - - float preInverseTonemapColorLuminance = Luminance(color); - float inverseTonemapColor = preInverseTonemapColorLuminance; - const uint inverseTonemapColorComponents = 1; - -#else - - float3 inverseTonemapColor = color; - const uint inverseTonemapColorComponents = 3; + float3 inverseTonemappedColor = tonemappedColor; -#endif // INVERT_TONEMAP_BY_LUMINANCE - - float minHighlightsColor = pow(2.f / 3.f, 2.2f); // We consider highlight the last ~33% of perception space + float minHighlightsColorIn = MinHighlightsColor; // We consider highlight the last ~33% of perception space + float minHighlightsColorOut = minHighlightsColorIn; const bool onlyInvertHighlights = (bool)INVERT_TONEMAP_HIGHLIGHTS_ONLY; - // Restore a color very close to the original linear one, but with all the other post process and LUT transformations baked in + // Restore a color very close to the original linear one (some information might get close in the direct tonemapper) switch (tonemapperIndex) { case 1: { - inverseTonemapColor = ACESReference_Inverse(inverseTonemapColor); + inverseTonemappedColor = ACESReference_Inverse(inverseTonemappedColor); midGrayOut = ACESReference_Inverse(midGrayIn); - minHighlightsColor = ACESReference_Inverse(minHighlightsColor); - tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; - postColorCorrectionLuminance = postColorCorrectionClampedLuminance; + minHighlightsColorOut = ACESReference_Inverse(minHighlightsColorIn); } break; case 2: { - inverseTonemapColor = ACESParametric_Inverse(inverseTonemapColor, acesParam_modE, acesParam_modA); + inverseTonemappedColor = ACESParametric_Inverse(inverseTonemappedColor, acesParam_modE, acesParam_modA); midGrayOut = ACESParametric_Inverse(midGrayIn, acesParam_modE, acesParam_modA); - minHighlightsColor = ACESParametric_Inverse(minHighlightsColor, acesParam_modE, acesParam_modA); - tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; - postColorCorrectionLuminance = postColorCorrectionClampedLuminance; + minHighlightsColorOut = ACESParametric_Inverse(minHighlightsColorIn, acesParam_modE, acesParam_modA); } break; case 3: { - inverseTonemapColor = Hable_Inverse(inverseTonemapColor, - inverseTonemapColorComponents, - hableParams, - minHighlightsColor, - onlyInvertHighlights); - midGrayOut = Hable_Inverse(midGrayIn, - 1, - hableParams, - minHighlightsColor, - onlyInvertHighlights); - - tonemapLostLuminance *= colorCorrectionHDRClippedLuminanceChange; - postColorCorrectionLuminance = postColorCorrectionClampedLuminance; + // Setup highlights for Hable, we use the official param based highlights shoulder start for it + minHighlightsColorIn = max(hableParams.params.y0, hableParams.params.y1); // Check both toe and mid params for extra safety + + inverseTonemappedColor = Hable_Inverse(inverseTonemappedColor, 3, hableParams); + midGrayOut = Hable_Inverse(midGrayIn, 1, hableParams); + minHighlightsColorOut = Hable_Inverse(minHighlightsColorIn, 1, hableParams); } break; default: - // Any luminance lost by this tonemap case would have already been restored above with "tonemapLostLuminance". break; } -#if INVERT_TONEMAP_BY_LUMINANCE - - color *= preInverseTonemapColorLuminance > 0.f ? (inverseTonemapColor / preInverseTonemapColorLuminance) : 1.f; - -#else - - color = inverseTonemapColor; - -#endif + for (uint channel = 0; channel < 3; channel++) + { + // Scale all non highlights by the scale the smallest (first) highlight would have, so we keep the curves connected + if (onlyInvertHighlights && inverseTonemappedColor[channel] < minHighlightsColorOut) + { + inverseTonemappedColor[channel] = tonemappedColor[channel] * (minHighlightsColorOut / minHighlightsColorIn); + } + // Restore any highlight clipped or just crushed by the direct tonemappers (Hable does that) + if (inverseTonemappedColor[channel] >= minHighlightsColorOut) + { + inverseTonemappedColor[channel] = inputColor[channel]; + } + } + if (onlyInvertHighlights && midGrayOut < minHighlightsColorOut) // This is probably always true + { + midGrayOut = midGrayIn * (minHighlightsColorOut / minHighlightsColorIn); + } const float midGrayScale = midGrayOut / midGrayIn; -#if ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS - - color = PostProcess(color, prePostProcessColorLuminance, midGrayScale); - -#endif // ENABLE_POST_PROCESS && ENABLE_INVERSE_POST_PROCESS - -#if ENABLE_LUMINANCE_RESTORE - - // Restore any luminance beyond 1 that ended up clipped by HDR->SDR tonemappers and any subsequent image manipulation. - // It's important to do these after the inverse tonemappers, as they can't handle values beyond 0-1. - color *= FindLuminanceToRestoreScale(color, tonemapLostLuminance, preColorCorrectionLuminance, postColorCorrectionLuminance, midGrayScale); - -#endif // ENABLE_LUMINANCE_RESTORE - -#if defined(APPLY_MERGED_COLOR_GRADING_LUT) && ENABLE_LUT && ENABLE_APPLY_LUT_AFTER_INVERSE_TONEMAP - - const float3 preLUTShiftedColor = color; - color *= LUTColorShift; - -#if LUT_IMPROVEMENT_TYPE == 3 - - const float postLUTShiftedColorLuminance = Luminance(color); - color *= postLUTShiftedColorLuminance > 0.f ? lerp(1.f, Luminance(preLUTShiftedColor) / postLUTShiftedColorLuminance, LUTCorrectionPercentage) : 1.f; - -#endif // LUT_IMPROVEMENT_TYPE == 3 -#endif // ENABLE_APPLY_LUT_AFTER_INVERSE_TONEMAP + float3 inverseTonemappedPostProcessedColor = RestorePostProcess(inverseTonemappedColor, postProcessColorRatio, postProcessColorOffset, tonemappedColor, midGrayScale); + //minHighlightsColorOut = RestorePostProcess(minHighlightsColorOut, postProcessColorRatio, postProcessColorOffset, tonemappedColor, midGrayScale); //TODO: should we even do this? // Bring back the color to the same range as SDR by dividing by the mid gray change. - color *= paperWhite / midGrayScale; - minHighlightsColor *= paperWhite / midGrayScale; + inverseTonemappedPostProcessedColor *= paperWhite / midGrayScale; + minHighlightsColorOut *= paperWhite / midGrayScale; const float maxOutputLuminance = HDR_MAX_OUTPUT_NITS / WhiteNits_BT709; - const float highlightsShoulderStart = (bool)INVERT_TONEMAP_HIGHLIGHTS_ONLY ? minHighlightsColor : 0.f; - color = DICETonemap(color, maxOutputLuminance, highlightsShoulderStart); + const float highlightsShoulderStart = onlyInvertHighlights ? minHighlightsColorOut : 0.f; + float3 outputColor = DICETonemap(inverseTonemappedPostProcessedColor, maxOutputLuminance, highlightsShoulderStart); #else // ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP - color *= HDR_GAME_PAPER_WHITE_MULTIPLIER; + float3 outputColor = finalOriginalColor * HDR_GAME_PAPER_WHITE_MULTIPLIER; // Don't use "HDR_GAME_PAPER_WHITE" as it'd be too bright on an untonemapped image #endif // ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP #if CLAMP_INPUT_OUTPUT - - color = clamp(color, 0.f, FLT16_MAX); // Avoid extremely high numbers turning into NaN in FP16 - + outputColor = clamp(outputColor, 0.f, FLT16_MAX); // Avoid extremely high numbers turning into NaN in FP16 #endif // CLAMP_INPUT_OUTPUT #else // ENABLE_HDR -#if SDR_USE_GAMMA_2_2 - - color = pow(color, 1.f / 2.2f); + float3 outputColor = finalOriginalColor; +#if SDR_USE_GAMMA_2_2 + outputColor = pow(outputColor, 1.f / 2.2f); #else - // Do sRGB gamma even if we'd be playing on gamma 2.2 screens, as the game was already calibrated for 2.2 gamma despite using the wrong formula - color = gamma_linear_to_sRGB(color); - + outputColor = gamma_linear_to_sRGB(outputColor); #endif // SDR_USE_GAMMA_2_2 #if CLAMP_INPUT_OUTPUT - - color = saturate(color); - + outputColor = saturate(outputColor); #endif // CLAMP_INPUT_OUTPUT #endif // ENABLE_HDR PSOutput psOutput; - psOutput.SV_Target.rgb = color; + psOutput.SV_Target.rgb = outputColor; psOutput.SV_Target.a = 1.f; return psOutput; diff --git a/shaders/color.hlsl b/shaders/color.hlsl index 1a681b27..b0ac156b 100644 --- a/shaders/color.hlsl +++ b/shaders/color.hlsl @@ -8,6 +8,10 @@ static const float ReferenceWhiteNits_BT2408 = 203.f; // This is based on the commonly used value, though perception space mid gray in sRGB or Gamma 2.2 would theoretically be ~0.2155 static const float MidGray = 0.18f; +// Start of the highlights shoulder (empirical) +static const float MinHighlightsColor = pow(2.f / 3.f, 2.2f); +static const float MaxShadowsColor = pow(1.f / 3.f, 2.2f); + // SMPTE ST 2084 (Perceptual Quantization) is only defined until this amount of nits. // This is also the max each color channel can have in HDR10. static const float PQMaxNits = 10000.0f; diff --git a/shaders/math.hlsl b/shaders/math.hlsl index f8ffc6ba..701fd255 100644 --- a/shaders/math.hlsl +++ b/shaders/math.hlsl @@ -18,6 +18,26 @@ T linearNormalization(T input, T min, T max, T newMin, T newMax) return ((input - min) * ((newMax - newMin) / (max - min))) + newMin; } +// Returns 1 if "dividend" is 0 +float safeDivision(float quotient, float dividend) +{ + return dividend == 0.f ? 1.f : (quotient / dividend); +} + +// Returns 1 if "dividend" is 0 +float3 safeDivision(float3 quotient, float3 dividend) +{ + float3 result = quotient / dividend; + for (uint channel = 0; channel < 3; channel++) + { + if (dividend[channel] == 0.f) + { + result = 1.f; + } + } + return result; +} + // Aplies exponential ("Photographic") luma compression. // The pow can modulate the curve without changing the values around the edges. float rangeCompressPow(float x, float fMax = FLT_MAX, float fPow = 1.f) diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 525f216d..10ee040d 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -15,12 +15,8 @@ #define HDR_GAME_PAPER_WHITE HDR_REFERENCE_PAPER_WHITE * HDR_GAME_PAPER_WHITE_MULTIPLIER #define HDR_UI_PAPER_WHITE HDR_REFERENCE_PAPER_WHITE * HDR_UI_PAPER_WHITE_MULTIPLIER -// 0 None, 1 ShortFuse technique (normalization), 2 luminance preservation (doesn't look so good and it's kinda broken), 3 tonemapper LUT lost luminance restore (improved iteration of type 2) -#define LUT_IMPROVEMENT_TYPE 1 // Makes LUTs sampling work in linear space, which is mathematically correct. Without this, they are stored as ~sRGB in a float texture and sampled in sRGB without acknowledging it. // This possibly shifts colors a lot, but it's correct, it's also necessary for HDR LUTs to work. #define LUT_FIX_GAMMA_MAPPING 1 #define LUT_SIZE 16.f #define LUT_SIZE_UINT (uint)LUT_SIZE - -static float LUTCorrectionPercentage = 1.f; From 18a3dd78ff256b4a39fca16e7a62cb2c533620f5 Mon Sep 17 00:00:00 2001 From: ersh1 <16157893+ersh1@users.noreply.github.com> Date: Mon, 2 Oct 2023 06:54:14 +0200 Subject: [PATCH 050/270] added photo mode blocking --- Plugin/src/Hooks.cpp | 27 ++++++++++++++++++++--- Plugin/src/Offsets.h | 12 ++++++++++ Plugin/src/RE/MessageBoxData.h | 40 ++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 Plugin/src/RE/MessageBoxData.h diff --git a/Plugin/src/Hooks.cpp b/Plugin/src/Hooks.cpp index b4d9439c..0628772a 100644 --- a/Plugin/src/Hooks.cpp +++ b/Plugin/src/Hooks.cpp @@ -88,6 +88,25 @@ namespace Hooks return _UnkFunc(a1, a_bgsSwapchainObject); } + bool Hooks::Hook_TakeSnapshot(uintptr_t a1) + { + const auto settings = Settings::Main::GetSingleton(); + //if (settings->IsHDREnabled()) { + if (true) { // actually it's always going to be the case because we're always going to update the image space buffer format + auto hack = alloca(sizeof(RE::MessageBoxData)); + auto& message = *(new (hack) RE::MessageBoxData("Photo Mode", "Taking screenshots with Photo Mode is not supported with Native HDR. Use an external tool (e.g. Xbox Game Bar) to take a screenshot.", nullptr, 0)); + Offsets::ShowMessageBox(*Offsets::MessageMenuManagerPtr, message, false); + + // hack to refresh the UI visibility after the snapshot + Offsets::PhotoMode_ToggleUI(a1 + 0x8); + Offsets::PhotoMode_ToggleUI(a1 + 0x8); + + return true; + } + + return _TakeSnapshot(a1); + } + void Hooks::Hook_CreateDataModelOptions(void* a_arg1, RE::ArrayNestedUIValue& a_SettingList) { const auto settings = Settings::Main::GetSingleton(); @@ -98,14 +117,14 @@ namespace Hooks constexpr auto id = static_cast(Settings::SettingID::kHDR); s.m_Text.SetStringValue("HDR"); - s.m_Description.SetStringValue("Sets the game's output mode between SDR, HDR10 PQ, or HDR10 scRGB."); + s.m_Description.SetStringValue("Sets the game's output mode between SDR, HDR10 PQ, or HDR scRGB."); s.m_ID.SetValue(id); s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Stepper); s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); s.m_Enabled.SetValue(true); s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("OFF"); s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("HDR10 PQ"); - s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("HDR10 scRGB"); + s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("HDR scRGB"); s.m_StepperData.m_ShuttleMap.GetData().m_Value.SetValue(*settings->FrameBufferFormat); a_SettingList.AddItem(s); } @@ -183,7 +202,7 @@ namespace Hooks } else if (newValue == 0) { ToggleEnableHDRSubSettings(EventData.m_Model, false); } - + RecreateSwapChain(swapChainObject, newFormat); } } @@ -210,6 +229,8 @@ namespace Hooks setting->m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue(settings->GetPaperwhiteText().data()); } } + + _SettingsDataModelFloatEvent(a_arg1, EventData); } void DebugHooks::Hook_CreateDataModelOptions(void* a_arg1, RE::ArrayNestedUIValue& a_SettingList) diff --git a/Plugin/src/Offsets.h b/Plugin/src/Offsets.h index 0b5f3ac7..de4e61fe 100644 --- a/Plugin/src/Offsets.h +++ b/Plugin/src/Offsets.h @@ -1,5 +1,6 @@ #pragma once #include "RE/Buffers.h" +#include "RE/MessageBoxData.h" class Offsets { @@ -16,6 +17,13 @@ class Offsets static inline uintptr_t* unkRecreateSwapChainArg1Ptr = nullptr; static inline uint8_t* unkRecreateSwapChainArg5 = nullptr; + static inline void** MessageMenuManagerPtr = nullptr; + using tShowMessageBox = void (*)(void* a_messageMenuManager, const RE::MessageBoxData& a_messageBoxData, bool a3); + static inline tShowMessageBox ShowMessageBox = nullptr; + + using tPhotoMode_ToggleUI = bool (*)(uintptr_t a1); + static inline tPhotoMode_ToggleUI PhotoMode_ToggleUI = nullptr; + static void Initialize() { { @@ -42,5 +50,9 @@ class Offsets unkRecreateSwapChainArg1Ptr = reinterpret_cast(dku::Hook::Module::get().base() + 0x5916FF0); // 0x5912F80 in 1.7.23 unkRecreateSwapChainArg5 = reinterpret_cast(dku::Hook::Module::get().base() + 0x55F8DB0); // 0x55F4D70 in 1.7.23 + + MessageMenuManagerPtr = reinterpret_cast(dku::Hook::Module::get().base() + 0x58F5E58); + ShowMessageBox = reinterpret_cast(dku::Hook::Module::get().base() + 0x28B04E4); + PhotoMode_ToggleUI = reinterpret_cast(dku::Hook::Module::get().base() + 0x2192538); } }; diff --git a/Plugin/src/RE/MessageBoxData.h b/Plugin/src/RE/MessageBoxData.h new file mode 100644 index 00000000..f0b411fd --- /dev/null +++ b/Plugin/src/RE/MessageBoxData.h @@ -0,0 +1,40 @@ +#pragma once + +namespace RE +{ + class MessageBoxData + { + public: + MessageBoxData() = default; + + MessageBoxData(const char* a_messageTitle, const char* a_messageText, void* a4, int32_t a5) + { + auto addr = dku::Hook::Module::get().base() + 0x13F5BC8; + auto func = reinterpret_cast(addr); + func(this, a_messageTitle, a_messageText, a4, a5); + } + + MessageBoxData(const MessageBoxData& Other) = delete; + + ~MessageBoxData() + { + auto addr = dku::Hook::Module::get().base() + 0x13F701C; + auto func = reinterpret_cast(addr); + func(this); + } + + MessageBoxData& operator=(const MessageBoxData& Other) = delete; + + // members + uint64_t unk00; + uint64_t unk08; + uint64_t unk10; + uint64_t unk18; + uint64_t unk20; + uint64_t unk28; + uint64_t unk30; + int32_t unk38; + }; + static_assert(sizeof(MessageBoxData) == 0x40); + +} From e871228a2c24798c0f5f76567a905b63137625c9 Mon Sep 17 00:00:00 2001 From: ersh1 <16157893+ersh1@users.noreply.github.com> Date: Mon, 2 Oct 2023 23:01:32 +0200 Subject: [PATCH 051/270] moved to address library; updated the plugin to the newest template version; fixed swapchain setting --- Plugin/.clang-format | 134 ++++++++-------- Plugin/CMakePresets.json | 208 ++++++++++++++++++++++--- Plugin/cmake/version.rc.in | 1 - Plugin/dist/rules/publish_package.json | 45 +++++- Plugin/include/SFSE/Stub.h | 69 +++++--- Plugin/src/Hooks.cpp | 50 +++--- Plugin/src/Hooks.h | 46 +++--- Plugin/src/Offsets.h | 50 ++---- Plugin/src/PCH.h | 4 +- Plugin/src/RE/BSFixedString.h | 6 +- Plugin/src/RE/MessageBoxData.h | 4 +- Plugin/src/RE/SettingsDataModel.h | 6 +- Plugin/src/RE/UI.h | 6 +- Plugin/src/Settings.h | 4 +- Plugin/src/Utils.cpp | 6 + Plugin/src/Utils.h | 1 + Plugin/src/main.cpp | 55 ++++++- Plugin/vcpkg.json | 3 +- build-msvc.bat | 4 + build-release.ps1 | 3 - extern/DKUtil | 2 +- extern/SFSE | 2 +- generate-sln.ps1 | 2 - make-sln-msvc.bat | 3 + update-submodule.bat | 2 + 25 files changed, 484 insertions(+), 232 deletions(-) create mode 100644 build-msvc.bat delete mode 100644 build-release.ps1 delete mode 100644 generate-sln.ps1 create mode 100644 make-sln-msvc.bat create mode 100644 update-submodule.bat diff --git a/Plugin/.clang-format b/Plugin/.clang-format index 415f78e0..e7031d45 100644 --- a/Plugin/.clang-format +++ b/Plugin/.clang-format @@ -1,101 +1,97 @@ --- AccessModifierOffset: -4 AlignAfterOpenBracket: DontAlign -AlignConsecutiveAssignments: 'false' -AlignConsecutiveBitFields: 'false' -AlignConsecutiveDeclarations: 'false' -AlignConsecutiveMacros: 'false' +AlignConsecutiveAssignments: false +AlignConsecutiveBitFields: false +AlignConsecutiveDeclarations: true +AlignConsecutiveMacros: false AlignEscapedNewlines: Left AlignOperands: Align -AlignTrailingComments: 'true' -AllowAllArgumentsOnNextLine: 'false' -AllowAllConstructorInitializersOnNextLine: 'false' -AllowAllParametersOfDeclarationOnNextLine: 'false' +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false AllowShortBlocksOnASingleLine: Empty -AllowShortCaseLabelsOnASingleLine: 'false' -AllowShortEnumsOnASingleLine: 'true' +AllowShortCaseLabelsOnASingleLine: false +AllowShortEnumsOnASingleLine: true AllowShortFunctionsOnASingleLine: All AllowShortIfStatementsOnASingleLine: Never AllowShortLambdasOnASingleLine: All -AllowShortLoopsOnASingleLine: 'true' +AllowShortLoopsOnASingleLine: true AlwaysBreakAfterReturnType: None -AlwaysBreakBeforeMultilineStrings: 'true' -AlwaysBreakTemplateDeclarations: 'Yes' -BinPackArguments: 'true' -BinPackParameters: 'true' +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: true +BinPackParameters: true BitFieldColonSpacing: After BraceWrapping: - AfterCaseLabel: 'true' - AfterClass: 'true' - AfterControlStatement: 'false' - AfterEnum: 'true' - AfterFunction: 'true' - AfterNamespace: 'true' - AfterStruct: 'true' - AfterUnion: 'true' - AfterExternBlock: 'true' - BeforeCatch: 'false' - BeforeElse: 'false' - BeforeLambdaBody: 'false' - BeforeWhile: 'false' - IndentBraces: 'false' - SplitEmptyFunction: 'false' - SplitEmptyRecord: 'false' - SplitEmptyNamespace: 'false' + AfterCaseLabel: true + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false BreakBeforeBinaryOperators: None BreakBeforeBraces: Custom -BreakBeforeTernaryOperators: 'false' +BreakBeforeTernaryOperators: false BreakConstructorInitializers: AfterColon BreakInheritanceList: AfterColon -BreakStringLiterals: 'true' +BreakStringLiterals: true ColumnLimit: 0 -CompactNamespaces: 'false' -ConstructorInitializerAllOnOneLineOrOnePerLine: 'false' +CompactNamespaces: false ConstructorInitializerIndentWidth: 4 ContinuationIndentWidth: 4 -Cpp11BracedListStyle: 'false' -DeriveLineEnding: 'true' -DerivePointerAlignment: 'false' -DisableFormat: 'false' -FixNamespaceComments: 'false' +Cpp11BracedListStyle: false +DerivePointerAlignment: false +DisableFormat: false +FixNamespaceComments: false IncludeBlocks: Preserve -IndentCaseBlocks: 'true' -IndentCaseLabels: 'false' +IndentCaseBlocks: true +IndentCaseLabels: false IndentExternBlock: Indent -IndentGotoLabels: 'false' +IndentGotoLabels: false IndentPPDirectives: AfterHash IndentWidth: 4 -IndentWrappedFunctionNames: 'true' -KeepEmptyLinesAtTheStartOfBlocks: 'false' +IndentWrappedFunctionNames: true +KeepEmptyLinesAtTheStartOfBlocks: false Language: Cpp MaxEmptyLinesToKeep: 1 NamespaceIndentation: All PointerAlignment: Left -ReflowComments : 'false' -SortIncludes: 'true' -SortUsingDeclarations: 'true' -SpaceAfterCStyleCast: 'false' -SpaceAfterLogicalNot: 'false' -SpaceAfterTemplateKeyword: 'true' -SpaceBeforeAssignmentOperators: 'true' -SpaceBeforeCpp11BracedList: 'false' -SpaceBeforeCtorInitializerColon: 'true' -SpaceBeforeInheritanceColon: 'true' +ReflowComments: false +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true SpaceBeforeParens: ControlStatements -SpaceBeforeRangeBasedForLoopColon: 'true' -SpaceBeforeSquareBrackets: 'false' -SpaceInEmptyBlock: 'false' -SpaceInEmptyParentheses: 'false' +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 2 -SpacesInAngles: 'false' -SpacesInCStyleCastParentheses: 'false' -SpacesInConditionalStatement: 'false' -SpacesInContainerLiterals: 'true' -SpacesInParentheses: 'false' -SpacesInSquareBrackets: 'false' +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInParentheses: false +SpacesInSquareBrackets: false Standard: Latest TabWidth: 4 -UseCRLF: 'true' UseTab: AlignWithSpaces - -... +LineEnding: CRLF +PackConstructorInitializers: BinPack diff --git a/Plugin/CMakePresets.json b/Plugin/CMakePresets.json index e071e0a7..0ff1178c 100644 --- a/Plugin/CMakePresets.json +++ b/Plugin/CMakePresets.json @@ -1,8 +1,8 @@ { - "version": 3, + "version": 6, "cmakeMinimumRequired": { "major": 3, - "minor": 21, + "minor": 26, "patch": 0 }, "configurePresets": [ @@ -10,22 +10,42 @@ "name": "common", "hidden": true, "cacheVariables": { - "PluginAuthor": "$env{SKSEPluginAuthor}" + "CMAKE_CXX_FLAGS": "$env{PROJECT_PLATFORM_FLAGS} $env{PROJECT_TEXT_FLAGS} $env{PROJECT_COMPILER_FLAGS} $penv{CXXFLAGS}", + "SFSE_SUPPORT_XBYAK": "ON" + }, + "vendor": { + "microsoft.com/VisualStudioSettings/CMake/1.0": { + "intelliSenseMode": "windows-msvc-x64", + "enableMicrosoftCodeAnalysis": true, + "enableClangTidyCodeAnalysis": true + } } }, { - "name": "vcpkg", + "name": "packaging-vcpkg", "hidden": true, "cacheVariables": { - "CMAKE_TOOLCHAIN_FILE": { - "type": "STRING", - "value": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" - }, + "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "VCPKG_HOST_TRIPLET": "x64-windows-static-md", "VCPKG_TARGET_TRIPLET": "x64-windows-static-md" } }, { - "name": "win64", + "name": "buildtype-debug", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "buildtype-release", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "x64", "hidden": true, "architecture": "x64", "cacheVariables": { @@ -33,28 +53,178 @@ } }, { - "name": "msvc", + "name": "generator-msvc", + "hidden": true, + "inherits": "x64", + "generator": "Visual Studio 17 2022" + }, + { + "name": "generator-ninja", + "hidden": true, + "generator": "Ninja" + }, + { + "name": "compiler-msvc", + "hidden": true, + "environment": { + "PROJECT_COMPILER_FLAGS": "/cgthreads8 /diagnostics:caret /fp:contract /fp:except- /guard:cf- /permissive- /Zc:__cplusplus /Zc:enumTypes /Zc:lambda /Zc:preprocessor /Zc:referenceBinding /Zc:rvalueCast /Zc:templateScope /Zc:ternary /Zc:preprocessor /EHsc /MP /W4 /external:anglebrackets /external:W0", + "PROJECT_COMPILER": "msvc" + } + }, + { + "name": "compiler-clang", "hidden": true, "cacheVariables": { - "CMAKE_CXX_FLAGS": "/EHsc /MP /W4 /WX $penv{CXXFLAGS}" + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + }, + "environment": { + "PROJECT_COMPILER": "clang", + "PROJECT_COMPILER_FLAGS": "-Wno-overloaded-virtual -Wno-delete-non-abstract-non-virtual-dtor -Wno-inconsistent-missing-override -Wno-reinterpret-base-class" }, - "generator": "Visual Studio 17 2022", "vendor": { "microsoft.com/VisualStudioSettings/CMake/1.0": { - "intelliSenseMode": "windows-msvc-x64", - "enableMicrosoftCodeAnalysis": true, - "enableClangTidyCodeAnalysis": true + "intelliSenseMode": "windows-clang-x64" } } }, { - "name": "REL", + "name": "compiler-clang-cl", + "hidden": true, + "inherits": "compiler-clang", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang-cl", + "CMAKE_CXX_COMPILER": "clang-cl" + }, + "environment": { + "CC": "clang-cl", + "CXX": "clang-cl", + "PROJECT_COMPILER_FLAGS": "/permissive- /EHsc /W4 -Wno-overloaded-virtual -Wno-delete-non-abstract-non-virtual-dtor -Wno-inconsistent-missing-override -Wno-reinterpret-base-class -D__cpp_consteval" + } + }, + { + "name": "build-debug-msvc-msvc", + "inherits": [ + "common", + "packaging-vcpkg", + "buildtype-debug", + "generator-msvc", + "compiler-msvc" + ] + }, + { + "name": "build-debug-msvc-ninja", + "inherits": [ + "common", + "packaging-vcpkg", + "buildtype-debug", + "generator-ninja", + "compiler-msvc" + ] + }, + { + "name": "build-debug-clang-cl-msvc", + "toolset": "ClangCL", + "inherits": [ + "common", + "packaging-vcpkg", + "buildtype-debug", + "generator-msvc", + "compiler-clang-cl" + ] + }, + { + "name": "build-debug-clang-cl-ninja", + "inherits": [ + "common", + "packaging-vcpkg", + "buildtype-debug", + "generator-ninja", + "compiler-clang-cl" + ] + }, + { + "name": "build-release-msvc-msvc", + "inherits": [ + "common", + "packaging-vcpkg", + "buildtype-release", + "generator-msvc", + "compiler-msvc" + ] + }, + { + "name": "build-release-msvc-ninja", + "inherits": [ + "common", + "packaging-vcpkg", + "buildtype-release", + "generator-ninja", + "compiler-msvc" + ] + }, + { + "name": "build-release-clang-cl-msvc", + "toolset": "ClangCL", + "inherits": [ + "common", + "packaging-vcpkg", + "buildtype-release", + "generator-msvc", + "compiler-clang-cl" + ] + }, + { + "name": "build-release-clang-cl-ninja", "inherits": [ "common", - "vcpkg", - "win64", - "msvc" + "packaging-vcpkg", + "buildtype-release", + "generator-ninja", + "compiler-clang-cl" ] } + ], + "buildPresets": [ + { + "name": "debug-msvc-ninja", + "configurePreset": "build-debug-msvc-ninja", + "displayName": "1. (Debug) MSVC - Ninja" + }, + { + "name": "release-msvc-ninja", + "configurePreset": "build-release-msvc-ninja", + "displayName": "2. (Release) MSVC - Ninja" + }, + { + "name": "debug-msvc-msvc", + "configurePreset": "build-debug-msvc-msvc", + "displayName": "3. (Debug) MSVC - MSVC" + }, + { + "name": "release-msvc-msvc", + "configurePreset": "build-release-msvc-msvc", + "displayName": "4. (Release) MSVC - MSVC" + }, + { + "name": "debug-clang-cl-ninja", + "configurePreset": "build-debug-clang-cl-ninja", + "displayName": "5. (Debug) Clang - Ninja" + }, + { + "name": "release-clang-cl-ninja", + "configurePreset": "build-release-clang-cl-ninja", + "displayName": "6. (Release) Clang - Ninja" + }, + { + "name": "debug-clang-cl-msvc", + "configurePreset": "build-debug-clang-cl-msvc", + "displayName": "7. (Debug) Clang - MSVC" + }, + { + "name": "release-clang-cl-msvc", + "configurePreset": "build-release-clang-cl-msvc", + "displayName": "8. (Release) Clang - MSVC" + } ] } \ No newline at end of file diff --git a/Plugin/cmake/version.rc.in b/Plugin/cmake/version.rc.in index efbb7561..82de531b 100644 --- a/Plugin/cmake/version.rc.in +++ b/Plugin/cmake/version.rc.in @@ -20,7 +20,6 @@ BEGIN VALUE "FileDescription", "@PROJECT_NAME@" VALUE "FileVersion", "@PROJECT_VERSION@.0" VALUE "InternalName", "@PROJECT_NAME@" - VALUE "LegalCopyright", "MIT License" VALUE "ProductName", "@PROJECT_NAME@" VALUE "ProductVersion", "@PROJECT_VERSION@.0" END diff --git a/Plugin/dist/rules/publish_package.json b/Plugin/dist/rules/publish_package.json index 73c362d2..586d170d 100644 --- a/Plugin/dist/rules/publish_package.json +++ b/Plugin/dist/rules/publish_package.json @@ -1,12 +1,45 @@ [ { - "action": "package", - "config": "Release", + "action": "copy", "params": [ "{dist}\\*.dll", + "{dist}\\Data\\SFSE\\Plugins\\" + ] + }, + { + "action": "copy_if", + "config": "RelWithDebInfo", + "params": [ + "{dist}\\*.pdb", + "{dist}\\Data\\SFSE\\Plugins\\" + ] + }, + { + "action": "copy_if", + "params": [ "{dist}\\*.ini", - "{dist}\\*.json", + "{dist}\\Data\\SFSE\\Plugins\\" + ] + }, + { + "action": "copy_if", + "params": [ "{dist}\\*.toml", + "{dist}\\Data\\SFSE\\Plugins\\" + ] + }, + { + "action": "copy_if", + "params": [ + "{dist}\\*.json", + "{dist}\\Data\\SFSE\\Plugins\\" + ] + }, + { + "action": "package", + "config": "Release", + "params": [ + "{dist}\\Data\\", "{dist}\\{project_name}.v{project_version}.zip" ] }, @@ -14,11 +47,7 @@ "action": "package", "config": "RelWithDebInfo", "params": [ - "{dist}\\*.dll", - "{dist}\\*.pdb", - "{dist}\\*.ini", - "{dist}\\*.json", - "{dist}\\*.toml", + "{dist}\\Data\\", "{dist}\\{project_name}.v{project_version}.zip" ] } diff --git a/Plugin/include/SFSE/Stub.h b/Plugin/include/SFSE/Stub.h index 48ba3754..d80e54b7 100644 --- a/Plugin/include/SFSE/Stub.h +++ b/Plugin/include/SFSE/Stub.h @@ -4,15 +4,13 @@ #include "sfse/PluginAPI.h" #include "sfse_common/sfse_version.h" -#define SFSEAPI __cdecl - // interface namespace SFSE { namespace detail { - inline static SFSEInterface* load_storage = nullptr; - inline static SFSEMessagingInterface* messaging_storage = nullptr; + inline static SFSEInterface* load_storage = nullptr; + inline static SFSEMessagingInterface* messaging_storage = nullptr; inline static SFSETrampolineInterface* trampoline_storage = nullptr; } // namespace detail @@ -33,9 +31,9 @@ namespace SFSE kTotal, }; - [[nodiscard]] constexpr auto RuntimeVersion() const { return GetProxy()->runtimeVersion; } - [[nodiscard]] constexpr auto SFSEVersion() const { return GetProxy()->sfseVersion; } - [[nodiscard]] constexpr auto GetPluginHandle() const { return handle; }; + [[nodiscard]] constexpr auto RuntimeVersion() const { return GetProxy()->runtimeVersion; } + [[nodiscard]] constexpr auto SFSEVersion() const { return GetProxy()->sfseVersion; } + [[nodiscard]] constexpr auto GetPluginHandle() const { return handle; }; [[nodiscard]] constexpr const auto* GetPluginInfo(const char* a_name) const { return GetProxy()->GetPluginInfo(a_name); } template [[nodiscard]] constexpr T* QueryInterface(std::uint32_t a_id) const @@ -62,10 +60,10 @@ namespace SFSE struct Message { - const char* sender; + const char* sender; std::uint32_t type; std::uint32_t dataLen; - void* data; + void* data; }; using EventCallback = std::add_pointer_t; @@ -208,13 +206,34 @@ namespace SFSE kVersion = 1, }; + // describe how you find your addresses + enum class AddressIndependence : std::uint32_t + { + kNone = 0, + // signature matching only + kSignatureScanning = 1 << 0, + // Address Library v1 (https://www.nexusmods.com/starfield/mods/3256) + kAddressLibrary = 1 << 1, + }; + + // describe your structure compatibility + enum class StructureIndependence : std::uint32_t + { + kNone = 0, + // doesn't use any game structures + kNoStructs = 1 << 0, + // works with the structure layout the game shipped with + kInitialLayout = 1 << 1, + // additional bits will be added here when breaking changes happen + }; + constexpr void PluginVersion(std::uint32_t a_version) noexcept { pluginVersion = a_version; } constexpr void PluginName(std::string_view a_plugin) noexcept { SetCharBuffer(a_plugin, std::span{ pluginName }); } constexpr void AuthorName(std::string_view a_name) noexcept { SetCharBuffer(a_name, std::span{ author }); } - constexpr void UsesSigScanning(bool a_value) noexcept { addressIndependence = !a_value; } - constexpr void UsesAddressLibrary(bool a_value) noexcept { addressIndependence = a_value; } - constexpr void HasNoStructUse(bool a_value) noexcept { structureCompatibility = !a_value; } - constexpr void IsLayoutDependent(bool a_value) noexcept { structureCompatibility = a_value; } + constexpr void UsesSigScanning(bool a_value) noexcept { addressIndependence.set(AddressIndependence::kSignatureScanning); } + constexpr void UsesAddressLibrary(bool a_value) noexcept { addressIndependence.set(AddressIndependence::kAddressLibrary); } + constexpr void HasNoStructUse(bool a_value) noexcept { structureCompatibility.set(StructureIndependence::kNoStructs); } + constexpr void IsLayoutDependent(bool a_value) noexcept { structureCompatibility.set(StructureIndependence::kInitialLayout); } constexpr void CompatibleVersions(std::initializer_list a_versions) noexcept { // must be zero-terminated @@ -223,21 +242,21 @@ namespace SFSE } constexpr void MinimumRequiredXSEVersion(std::uint32_t a_version) noexcept { xseMinimum = a_version; } - const std::uint32_t dataVersion{ kVersion }; // shipped with xse - std::uint32_t pluginVersion = 0; // version number of your plugin - char pluginName[256] = {}; // null-terminated ASCII plugin name (please make this recognizable to users) - char author[256] = {}; // null-terminated ASCII plugin author name - std::uint32_t addressIndependence; // describe how you find your addressese using the kAddressIndependence_ enums - std::uint32_t structureCompatibility; // describe how you handle structure layout using the kStructureIndependence_ enums - std::uint32_t compatibleVersions[16] = {}; // list of compatible versions - std::uint32_t xseMinimum = 0; // minimum version of the script extender required - const std::uint32_t reservedNonBreaking = 0; // set to 0 - const std::uint32_t reservedBreaking = 0; // set to 0 + const std::uint32_t dataVersion{ kVersion }; // shipped with xse + std::uint32_t pluginVersion = 0; // version number of your plugin + char pluginName[256] = {}; // null-terminated ASCII plugin name (please make this recognizable to users) + char author[256] = {}; // null-terminated ASCII plugin author name + dku::enumeration addressIndependence{ AddressIndependence::kNone }; // describe how you find your addressese using the kAddressIndependence_ enums + dku::enumeration structureCompatibility{ StructureIndependence::kNone }; // describe how you handle structure layout using the kStructureIndependence_ enums + std::uint32_t compatibleVersions[16] = {}; // list of compatible versions + std::uint32_t xseMinimum = 0; // minimum version of the script extender required + const std::uint32_t reservedNonBreaking = 0; // set to 0 + const std::uint32_t reservedBreaking = 0; // set to 0 private: static constexpr void SetCharBuffer( std::string_view a_src, - std::span a_dst) noexcept + std::span a_dst) noexcept { assert(a_src.size() < a_dst.size()); std::ranges::fill(a_dst, '\0'); @@ -255,4 +274,4 @@ namespace SFSE static_assert(offsetof(PluginVersionData, reservedNonBreaking) == 0x254); static_assert(offsetof(PluginVersionData, reservedBreaking) == 0x258); static_assert(sizeof(PluginVersionData) == 0x25C); -} // namespace SFSE \ No newline at end of file +} // namespace SFSE diff --git a/Plugin/src/Hooks.cpp b/Plugin/src/Hooks.cpp index 0628772a..511c9a8a 100644 --- a/Plugin/src/Hooks.cpp +++ b/Plugin/src/Hooks.cpp @@ -36,29 +36,6 @@ namespace Hooks Utils::SetBufferFormat(a_buffer, a_newFormat); } - void Hooks::RecreateSwapChain(RE::BGSSwapChainObject* a_bgsSwapchainObject, RE::BS_DXGI_FORMAT a_newFormat) - { - if (a_bgsSwapchainObject->format != a_newFormat) { - a_bgsSwapchainObject->format = a_newFormat; - Offsets::RecreateSwapChain(*reinterpret_cast(*Offsets::unkRecreateSwapChainArg1Ptr + 0x28), a_bgsSwapchainObject, a_bgsSwapchainObject->width, a_bgsSwapchainObject->height, *Offsets::unkRecreateSwapChainArg5); - - // set correct color space - DXGI_COLOR_SPACE_TYPE newColorSpace; - switch (a_newFormat) { - case RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_B8G8R8A8_UNORM: - default: - newColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709; - break; - case RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R10G10B10A2_UNORM: - newColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020; - break; - case RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT: - newColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709; - } - a_bgsSwapchainObject->swapChainInterface->SetColorSpace1(newColorSpace); - } - } - void Hooks::ToggleEnableHDRSubSettings(RE::SettingsDataModel* a_model, bool a_bEnable) { if (const auto maxLuminanceSetting = a_model->FindSettingById(static_cast(Settings::SettingID::kMaxLuminance))) { @@ -107,6 +84,25 @@ namespace Hooks return _TakeSnapshot(a1); } + void Hooks::Hook_RecreateSwapchain(void* a1, RE::BGSSwapChainObject* a_bgsSwapChainObject, uint32_t a_width, uint32_t a_height, uint8_t a5) + { + _RecreateSwapchain(a1, a_bgsSwapChainObject, a_width, a_height, a5); + + DXGI_COLOR_SPACE_TYPE newColorSpace; + switch (Utils::GetBufferFormat(RE::Buffers::FrameBuffer)) { + case RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_B8G8R8A8_UNORM: + default: + newColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709; + break; + case RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R10G10B10A2_UNORM: + newColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020; + break; + case RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT: + newColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709; + } + a_bgsSwapChainObject->swapChainInterface->SetColorSpace1(newColorSpace); + } + void Hooks::Hook_CreateDataModelOptions(void* a_arg1, RE::ArrayNestedUIValue& a_SettingList) { const auto settings = Settings::Main::GetSingleton(); @@ -194,7 +190,6 @@ namespace Hooks break; } - // the value in the buffer definition is not going to be read by the game anymore by this point, but changing it anyway in case something else tries to read it Utils::SetBufferFormat(RE::Buffers::FrameBuffer, newFormat); if (prevValue == 0) { @@ -202,8 +197,11 @@ namespace Hooks } else if (newValue == 0) { ToggleEnableHDRSubSettings(EventData.m_Model, false); } - - RecreateSwapChain(swapChainObject, newFormat); + + if (swapChainObject->format != newFormat) { + swapChainObject->format = newFormat; + Offsets::ToggleVsync(reinterpret_cast(*Offsets::unkToggleVsyncArg1Ptr + 0x8), *Offsets::bEnableVsync); + } } } diff --git a/Plugin/src/Hooks.h b/Plugin/src/Hooks.h index 8e6a95b2..c455a84b 100644 --- a/Plugin/src/Hooks.h +++ b/Plugin/src/Hooks.h @@ -62,32 +62,38 @@ namespace Hooks public: static void Hook() { - const auto scan = static_cast(dku::Hook::Assembly::search_pattern<"E8 ?? ?? ?? ?? 8B 4D A8 8B 45 AC">()); - if (!scan) { - ERROR("Failed to find color space hook") - } - const auto callsiteOffset = *reinterpret_cast(scan + 1); - const auto UnkFuncCallsite = AsAddress(scan + 5 + callsiteOffset + 0x3EA); - - _UnkFunc = dku::Hook::write_call<5>(UnkFuncCallsite, Hook_UnkFunc); // 32E7856 - - INFO("Found color space hook callsite at {:X}", UnkFuncCallsite) - - _CreateDataModelOptions = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20BD2F9, Hook_CreateDataModelOptions); - _SettingsDataModelBoolEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB63E, Hook_SettingsDataModelBoolEvent); - _SettingsDataModelIntEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB937, Hook_SettingsDataModelIntEvent); - _SettingsDataModelFloatEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB8EB, Hook_SettingsDataModelFloatEvent); + // set color space and save swapchain object pointer + _UnkFunc = dku::Hook::write_call<5>(dku::Hook::IDToAbs(204384, 0x3EA), Hook_UnkFunc); + + // disable photo mode screenshots with HDR + const auto takeSnapshotVtbl = dku::Hook::IDToAbs(415473); + auto _Hook_TakeSnapshot = dku::Hook::AddVMTHook(&takeSnapshotVtbl, 1, FUNC_INFO(Hook_TakeSnapshot)); + _TakeSnapshot = reinterpret_cast>(_Hook_TakeSnapshot->OldAddress); + _Hook_TakeSnapshot->Enable(); + + // Settings UI + _CreateDataModelOptions = dku::Hook::write_call<5>(dku::Hook::IDToAbs(135915, 0x89), Hook_CreateDataModelOptions); + _SettingsDataModelBoolEvent = dku::Hook::write_call<5>(dku::Hook::IDToAbs(136121, 0x3A), Hook_SettingsDataModelBoolEvent); + _SettingsDataModelIntEvent = dku::Hook::write_call<5>(dku::Hook::IDToAbs(136131, 0x37), Hook_SettingsDataModelIntEvent); + _SettingsDataModelFloatEvent = dku::Hook::write_call<5>(dku::Hook::IDToAbs(136130, 0x37), Hook_SettingsDataModelFloatEvent); + + _RecreateSwapchain = dku::Hook::write_call<5>(dku::Hook::IDToAbs(203027, 0x89), Hook_RecreateSwapchain); } private: static inline RE::BGSSwapChainObject* swapChainObject = nullptr; - static void RecreateSwapChain(RE::BGSSwapChainObject* a_bgsSwapchainObject, RE::BS_DXGI_FORMAT a_newFormat); static void ToggleEnableHDRSubSettings(RE::SettingsDataModel* a_model, bool a_bEnable); static void Hook_UnkFunc(uintptr_t a1, RE::BGSSwapChainObject* a_bgsSwapchainObject); static inline std::add_pointer_t _UnkFunc; + static bool Hook_TakeSnapshot(uintptr_t a1); + static inline std::add_pointer_t _TakeSnapshot; + + static void Hook_RecreateSwapchain(void* a1, RE::BGSSwapChainObject* a_bgsSwapChainObject, uint32_t a_width, uint32_t a_height, uint8_t a5); + static inline std::add_pointer_t _RecreateSwapchain; + static void Hook_CreateDataModelOptions(void* a_arg1, RE::ArrayNestedUIValue& a_SettingList); static inline std::add_pointer_t _CreateDataModelOptions; static void Hook_SettingsDataModelBoolEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData); @@ -104,9 +110,9 @@ namespace Hooks static void Hook() { //_CreateDataModelOptions = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20BD2F9, Hook_CreateDataModelOptions); - _SettingsDataModelBoolEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB63E, Hook_SettingsDataModelBoolEvent); - _SettingsDataModelIntEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB937, Hook_SettingsDataModelIntEvent); - _SettingsDataModelFloatEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB8EB, Hook_SettingsDataModelFloatEvent); + //_SettingsDataModelBoolEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB63E, Hook_SettingsDataModelBoolEvent); + //_SettingsDataModelIntEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB937, Hook_SettingsDataModelIntEvent); + //_SettingsDataModelFloatEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB8EB, Hook_SettingsDataModelFloatEvent); #if 0 const auto callsite1 = AsAddress(dku::Hook::Module::get().base() + 0x32ED294); @@ -138,4 +144,4 @@ namespace Hooks }; void Install(); -} \ No newline at end of file +} diff --git a/Plugin/src/Offsets.h b/Plugin/src/Offsets.h index de4e61fe..5f3cdd0a 100644 --- a/Plugin/src/Offsets.h +++ b/Plugin/src/Offsets.h @@ -11,12 +11,6 @@ class Offsets using tGetDXGIFormat = DXGI_FORMAT (*)(RE::BS_DXGI_FORMAT a_bgsFormat); static inline tGetDXGIFormat GetDXGIFormat = nullptr; - using tRecreateSwapChain = void (*)(void* a1, RE::BGSSwapChainObject* a_bgsSwapChainObject, uint32_t a_width, uint32_t a_height, uint8_t a5); - static inline tRecreateSwapChain RecreateSwapChain = nullptr; - - static inline uintptr_t* unkRecreateSwapChainArg1Ptr = nullptr; - static inline uint8_t* unkRecreateSwapChainArg5 = nullptr; - static inline void** MessageMenuManagerPtr = nullptr; using tShowMessageBox = void (*)(void* a_messageMenuManager, const RE::MessageBoxData& a_messageBoxData, bool a3); static inline tShowMessageBox ShowMessageBox = nullptr; @@ -24,35 +18,23 @@ class Offsets using tPhotoMode_ToggleUI = bool (*)(uintptr_t a1); static inline tPhotoMode_ToggleUI PhotoMode_ToggleUI = nullptr; + //test + static inline uintptr_t* unkToggleVsyncArg1Ptr = nullptr; + using tToggleVsync = void (*)(void* a1, bool a_bEnable); + static inline tToggleVsync ToggleVsync = nullptr; + static inline bool* bEnableVsync = nullptr; + static void Initialize() { - { - const auto scan = static_cast(dku::Hook::Assembly::search_pattern<"4C 8D 15 ?? ?? ?? ?? BE ?? ?? ?? ??">()); - if (!scan) { - ERROR("Failed to find buffer definition array!") - } - const auto offset = *reinterpret_cast(scan + 3); - const auto address = reinterpret_cast(scan) + 7 + offset; - bufferArray = reinterpret_cast(address); // 4718E40 - INFO("Found buffer array at {:X}", address) - } - - { - const auto scan = static_cast(dku::Hook::Assembly::search_pattern<"E8 ?? ?? ?? ?? 89 45 94">()); - if (!scan) { - ERROR("Failed to find GetDXGIFormat!") - } - GetDXGIFormat = dku::Hook::GetDisp(scan); // 32F18A0 - INFO("Found GetDXGIFormat at {:X}", reinterpret_cast(scan)) - } - - RecreateSwapChain = reinterpret_cast(dku::Hook::Module::get().base() + 0x32F4BEC); // 0x32F14BC in 1.7.23 - - unkRecreateSwapChainArg1Ptr = reinterpret_cast(dku::Hook::Module::get().base() + 0x5916FF0); // 0x5912F80 in 1.7.23 - unkRecreateSwapChainArg5 = reinterpret_cast(dku::Hook::Module::get().base() + 0x55F8DB0); // 0x55F4D70 in 1.7.23 - - MessageMenuManagerPtr = reinterpret_cast(dku::Hook::Module::get().base() + 0x58F5E58); - ShowMessageBox = reinterpret_cast(dku::Hook::Module::get().base() + 0x28B04E4); - PhotoMode_ToggleUI = reinterpret_cast(dku::Hook::Module::get().base() + 0x2192538); + bufferArray = reinterpret_cast(dku::Hook::IDToAbs(477165)); + GetDXGIFormat = reinterpret_cast(dku::Hook::IDToAbs(204483)); + + ToggleVsync = reinterpret_cast(dku::Hook::IDToAbs(184653)); + unkToggleVsyncArg1Ptr = reinterpret_cast(dku::Hook::IDToAbs(878340)); + bEnableVsync = reinterpret_cast(dku::Hook::IDToAbs(875798)); + + MessageMenuManagerPtr = reinterpret_cast(dku::Hook::IDToAbs(878772)); + ShowMessageBox = reinterpret_cast(dku::Hook::IDToAbs(167094)); + PhotoMode_ToggleUI = reinterpret_cast(dku::Hook::IDToAbs(139734)); } }; diff --git a/Plugin/src/PCH.h b/Plugin/src/PCH.h index 4a6eb4f7..fc791a50 100644 --- a/Plugin/src/PCH.h +++ b/Plugin/src/PCH.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once // c #include @@ -106,6 +106,8 @@ using namespace std::literals; #define DLLEXPORT extern "C" [[maybe_unused]] __declspec(dllexport) +#define SFSEAPI __cdecl +#define PLUGIN_MODE // Plugin #include "Plugin.h" diff --git a/Plugin/src/RE/BSFixedString.h b/Plugin/src/RE/BSFixedString.h index 749d172e..1887bbbb 100644 --- a/Plugin/src/RE/BSFixedString.h +++ b/Plugin/src/RE/BSFixedString.h @@ -12,7 +12,7 @@ namespace RE BSFixedStringCS(const char* String) { - auto addr = dku::Hook::Module::get().base() + 0x314D050; + auto addr = dku::Hook::IDToAbs(198219); auto func = reinterpret_cast(addr); func(this, String); } @@ -21,7 +21,7 @@ namespace RE ~BSFixedStringCS() { - auto addr = dku::Hook::Module::get().base() + 0x05466F0; + auto addr = dku::Hook::IDToAbs(33964); auto func = reinterpret_cast(addr); func(this); } @@ -38,4 +38,4 @@ namespace RE return m_EntryPoolData != Other.m_EntryPoolData; } }; -} \ No newline at end of file +} diff --git a/Plugin/src/RE/MessageBoxData.h b/Plugin/src/RE/MessageBoxData.h index f0b411fd..11beca13 100644 --- a/Plugin/src/RE/MessageBoxData.h +++ b/Plugin/src/RE/MessageBoxData.h @@ -9,7 +9,7 @@ namespace RE MessageBoxData(const char* a_messageTitle, const char* a_messageText, void* a4, int32_t a5) { - auto addr = dku::Hook::Module::get().base() + 0x13F5BC8; + auto addr = dku::Hook::IDToAbs(81979); auto func = reinterpret_cast(addr); func(this, a_messageTitle, a_messageText, a4, a5); } @@ -18,7 +18,7 @@ namespace RE ~MessageBoxData() { - auto addr = dku::Hook::Module::get().base() + 0x13F701C; + auto addr = dku::Hook::IDToAbs(82034); auto func = reinterpret_cast(addr); func(this); } diff --git a/Plugin/src/RE/SettingsDataModel.h b/Plugin/src/RE/SettingsDataModel.h index 4cc9d5e8..ecf6893f 100644 --- a/Plugin/src/RE/SettingsDataModel.h +++ b/Plugin/src/RE/SettingsDataModel.h @@ -69,14 +69,14 @@ namespace RE GeneralSetting() { - auto addr = dku::Hook::Module::get().base() + 0x20B986C; + auto addr = dku::Hook::IDToAbs(135831); auto func = reinterpret_cast(addr); func(*this); } ~GeneralSetting() { - auto addr = dku::Hook::Module::get().base() + 0x20BAA70; + auto addr = dku::Hook::IDToAbs(135898); auto func = reinterpret_cast(addr); func(*this); } @@ -133,4 +133,4 @@ namespace RE static_assert(sizeof(ArrayNestedUIValue) == 0x78); static_assert(sizeof(TUIDataShuttleContainerMap) == 0xA8); -} \ No newline at end of file +} diff --git a/Plugin/src/RE/UI.h b/Plugin/src/RE/UI.h index 1dec37d4..bcc8d661 100644 --- a/Plugin/src/RE/UI.h +++ b/Plugin/src/RE/UI.h @@ -144,7 +144,7 @@ namespace RE void AddItem(const BSFixedStringCS& Item) { - auto addr = dku::Hook::Module::get().base() + 0x207E280; + auto addr = dku::Hook::IDToAbs(134612); auto func = reinterpret_cast*, const BSFixedStringCS&)>(addr); func(this, Item); } @@ -162,9 +162,9 @@ namespace RE void AddItem(T& Item) { // Seems like they have a helper function to apply NestedUIValue<> to the type - auto addr = dku::Hook::Module::get().base() + 0x20B6918; + auto addr = dku::Hook::IDToAbs(135760); auto func = reinterpret_cast>*, T&)>(addr); func(&__super::m_ShuttleArray, Item); } }; -} \ No newline at end of file +} diff --git a/Plugin/src/Settings.h b/Plugin/src/Settings.h index 40f32d81..05cb6740 100644 --- a/Plugin/src/Settings.h +++ b/Plugin/src/Settings.h @@ -45,6 +45,6 @@ namespace Settings static inline constexpr float paperwhiteSliderMin = 80.f; static inline constexpr float paperwhiteSliderMax = 500.f; - TomlConfig config = COMPILE_PROXY("Data/SFSE/Plugins/NativeHDR.toml"sv); + TomlConfig config = COMPILE_PROXY("NativeHDR.toml"sv); }; -} \ No newline at end of file +} diff --git a/Plugin/src/Utils.cpp b/Plugin/src/Utils.cpp index c2584f4b..159cff14 100644 --- a/Plugin/src/Utils.cpp +++ b/Plugin/src/Utils.cpp @@ -176,4 +176,10 @@ namespace Utils const auto buffer = (*Offsets::bufferArray)[static_cast(a_buffer)]; SetBufferFormat(buffer, a_format); } + + RE::BS_DXGI_FORMAT GetBufferFormat(RE::Buffers a_buffer) + { + const auto buffer = (*Offsets::bufferArray)[static_cast(a_buffer)]; + return buffer->format; + } } diff --git a/Plugin/src/Utils.h b/Plugin/src/Utils.h index deafe62f..2824ca38 100644 --- a/Plugin/src/Utils.h +++ b/Plugin/src/Utils.h @@ -13,4 +13,5 @@ namespace Utils void SetBufferFormat(RE::BufferDefinition* a_buffer, RE::BS_DXGI_FORMAT a_format); void SetBufferFormat(RE::Buffers a_buffer, RE::BS_DXGI_FORMAT a_format); + RE::BS_DXGI_FORMAT GetBufferFormat(RE::Buffers a_buffer); } diff --git a/Plugin/src/main.cpp b/Plugin/src/main.cpp index b92664d1..63e8fe7c 100644 --- a/Plugin/src/main.cpp +++ b/Plugin/src/main.cpp @@ -10,10 +10,16 @@ DLLEXPORT constinit auto SFSEPlugin_Version = []() noexcept { data.PluginVersion(Plugin::Version); data.PluginName(Plugin::NAME); data.AuthorName(Plugin::AUTHOR); - data.UsesSigScanning(true); - //data.UsesAddressLibrary(true); - data.HasNoStructUse(true); + + // Address Library v1 (https://www.nexusmods.com/starfield/mods/3256) + data.UsesAddressLibrary(true); + // Version independent signature scanning + //data.UsesSigScanning(true); + + // Uses version specific structure definitions //data.IsLayoutDependent(true); + //data.HasNoStructUse(true); + data.CompatibleVersions({ RUNTIME_VERSION_1_7_33 }); return data; @@ -24,8 +30,14 @@ DLLEXPORT constinit auto SFSEPlugin_Version = []() noexcept { void SFSEPlugin_Preload(SFSE::LoadInterface* a_sfse); /**/ +static inline bool bIsLoaded = false; + DLLEXPORT bool SFSEAPI SFSEPlugin_Load(SFSEInterface* a_sfse) { + if (bIsLoaded) { + return true; + } + #ifndef NDEBUG while (!IsDebuggerPresent()) { Sleep(100); @@ -34,10 +46,8 @@ DLLEXPORT bool SFSEAPI SFSEPlugin_Load(SFSEInterface* a_sfse) SFSE::Init(a_sfse); - DKUtil::Logger::Init(Plugin::NAME, std::to_string(Plugin::Version)); - - INFO("{} v{} loaded", Plugin::NAME, Plugin::Version); - INFO("base address : {:X}", dku::Hook::Module::get().base()) + dku::Logger::Init(Plugin::NAME, std::to_string(Plugin::Version)); + INFO("{} v{} loaded", Plugin::NAME, Plugin::Version) // do stuff Settings::Main::GetSingleton()->Load(); @@ -46,5 +56,36 @@ DLLEXPORT bool SFSEAPI SFSEPlugin_Load(SFSEInterface* a_sfse) Offsets::Initialize(); Hooks::Install(); + bIsLoaded = true; + return true; } + +// for non sfse plugin loaders +BOOL APIENTRY DllMain(HMODULE a_hModule, DWORD a_ul_reason_for_call, LPVOID a_lpReserved) +{ + if (bIsLoaded) { + return TRUE; + } + + if (a_ul_reason_for_call == DLL_PROCESS_ATTACH) { +#ifndef NDEBUG + while (!IsDebuggerPresent()) { + Sleep(100); + } +#endif + + dku::Logger::Init(Plugin::NAME, std::to_string(Plugin::Version)); + INFO("{} v{} loaded", Plugin::NAME, Plugin::Version) + + Settings::Main::GetSingleton()->Load(); + + dku::Hook::Trampoline::AllocTrampoline(1 << 7); + Offsets::Initialize(); + Hooks::Install(); + + bIsLoaded = true; + } + + return TRUE; +} diff --git a/Plugin/vcpkg.json b/Plugin/vcpkg.json index cd849a65..87f395ac 100644 --- a/Plugin/vcpkg.json +++ b/Plugin/vcpkg.json @@ -1,10 +1,9 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json", + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", "name": "nativehdr", "version-string": "1.0.0", "description": "SFSE plugin that adds native HDR support to Starfield", "dependencies": [ - "fmt", "spdlog", "nlohmann-json", "simpleini", diff --git a/build-msvc.bat b/build-msvc.bat new file mode 100644 index 00000000..038f8805 --- /dev/null +++ b/build-msvc.bat @@ -0,0 +1,4 @@ +echo off +rd /s /q "%~dp0/build" +cmake -B "%~dp0/build" -S "%~dp0/Plugin" --preset=build-release-msvc-msvc +cmake --build "%~dp0/build" diff --git a/build-release.ps1 b/build-release.ps1 deleted file mode 100644 index 7800e3de..00000000 --- a/build-release.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -Remove-Item $PSScriptRoot/build -Recurse -Force -ErrorAction:SilentlyContinue -Confirm:$False | Out-Null -& cmake -B $PSScriptRoot/build -S $PSScriptRoot/Plugin --preset=REL -& cmake --build $PSScriptRoot/build --config Release \ No newline at end of file diff --git a/extern/DKUtil b/extern/DKUtil index 3d383c03..ca04ebd2 160000 --- a/extern/DKUtil +++ b/extern/DKUtil @@ -1 +1 @@ -Subproject commit 3d383c03bbcb437e35651edb625ba4ec6f1af377 +Subproject commit ca04ebd21839c3a5f223046623903e4537dbb203 diff --git a/extern/SFSE b/extern/SFSE index 9f55120a..cc3b6034 160000 --- a/extern/SFSE +++ b/extern/SFSE @@ -1 +1 @@ -Subproject commit 9f55120ae048f2e598cfb9e6e16903ea8e318ccc +Subproject commit cc3b60344bfa49139db1a98d7047222713ad3ad4 diff --git a/generate-sln.ps1 b/generate-sln.ps1 deleted file mode 100644 index b2cf1b38..00000000 --- a/generate-sln.ps1 +++ /dev/null @@ -1,2 +0,0 @@ -Remove-Item $PSScriptRoot/build -Recurse -Force -ErrorAction:SilentlyContinue -Confirm:$False | Out-Null -& cmake -B $PSScriptRoot/build -S $PSScriptRoot/Plugin --preset=REL \ No newline at end of file diff --git a/make-sln-msvc.bat b/make-sln-msvc.bat new file mode 100644 index 00000000..4fad248b --- /dev/null +++ b/make-sln-msvc.bat @@ -0,0 +1,3 @@ +echo off +rd /s /q "%~dp0/build" +cmake -B "%~dp0/build" -S "%~dp0/Plugin" --preset=build-debug-msvc-msvc diff --git a/update-submodule.bat b/update-submodule.bat new file mode 100644 index 00000000..f82cf7a5 --- /dev/null +++ b/update-submodule.bat @@ -0,0 +1,2 @@ +git submodule update --init --recursive -f +git submodule update --remote -f \ No newline at end of file From a3a9e478cc1e2e4fb123df5ceaa17cc872fdf31d Mon Sep 17 00:00:00 2001 From: Lilium Date: Mon, 2 Oct 2023 00:26:11 +0200 Subject: [PATCH 052/270] add sigmoidal inspired contrast adjustment --- shaders/HDRComposite/HDRComposite_ps.hlsl | 64 +++++++++++++++++++++-- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 0eb02563..b3dd5731 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -16,8 +16,12 @@ // This disables most other features (post process, LUTs, ...) #define ENABLE_TONEMAP 1 #define ENABLE_POST_PROCESS 1 -// 0 original (weak, generates values beyond 0-1 which then get clipped), 1 improved (looks more natural, avoids values below 0, but will overshoot beyond 1 more often, and will raise blacks), 2 Sigmoidal (smoothest looking, but harder to match) -#define POST_PROCESS_CONTRAST_TYPE 1 +// 0 disable contrast adjustment +// 1 original (weak, generates values beyond 0-1 which then get clipped) +// 2 improved (looks more natural, avoids values below 0, but will overshoot beyond 1 more often, and will raise blacks) +// 3 Sigmoidal (smoothest looking, but harder to match) +// 4 Sigmoidal inspired +#define POST_PROCESS_CONTRAST_TYPE 4 #define ENABLE_LUT 1 // LUTs are too low resolutions to resolve gradients smoothly if the LUT color suddenly changes between samples #define ENABLE_LUT_TETRAHEDRAL_INTERPOLATION 1 @@ -435,6 +439,34 @@ float3 DICETonemap( return Color; } +// sigmoidal inspired contrast adjustment using 2 power curves +// normalizes both lower and upper part which is divided by contrastMidPoint +// and then applies a power curve based off of contrastIntensity +// power curve for lower is just pow(x, contrastIntensity) +// while for upper it's pow(x, 1 / contrastIntensity) +float SigmoidalContrastAdjustment( + float Channel, + float contrastMidPoint, + float contrastIntensityLower, + float contrastIntensityUpper, + float normalizationFactorLower, + float normalizationFactorUpper) +{ + [flatten] + if (Channel <= contrastMidPoint) + { + Channel = pow(Channel * normalizationFactorLower, contrastIntensityLower) + / normalizationFactorLower; + } + else + { + Channel = pow((Channel - contrastMidPoint) * normalizationFactorUpper, contrastIntensityUpper) + / normalizationFactorUpper + contrastMidPoint; + } + + return Channel; +} + // "MidGrayScale" is how much the mid gray shifted from the originally intended tonemapped input (e.g. if we run this function on the untonemapped image, we can remap the mid gray) float3 PostProcess( float3 Color, @@ -459,17 +491,17 @@ float3 PostProcess( Color += lerp(float3(0.f, 0.f, 0.f), ColorLuminance * highlightsColorFilter.rgb, highlightsColorFilter.a); Color *= brightnessMultiplier; -#if POST_PROCESS_CONTRAST_TYPE == 0 +#if POST_PROCESS_CONTRAST_TYPE == 1 // Contrast adjustment (shift the colors from 0<->1 to (e.g.) -0.5<->0.5 range, multiply and shift back). // The higher the distance from the contrast middle point, the more contrast will change the color. Color = ((Color - contrastMidPoint) * contrastIntensity) + contrastMidPoint; -#elif POST_PROCESS_CONTRAST_TYPE == 1 +#elif POST_PROCESS_CONTRAST_TYPE == 2 Color = pow(Color / contrastMidPoint, contrastIntensity) * contrastMidPoint; -#elif POST_PROCESS_CONTRAST_TYPE == 2 +#elif POST_PROCESS_CONTRAST_TYPE == 3 // sigmoidal contrast adjustment doesn't clip colors // https://www.imagemagick.org/Usage/color_mods/#sigmoidal @@ -486,6 +518,28 @@ float3 PostProcess( Color = (1 / (1 + exp(c * (s - Color))) + minus) / (1 / (1 + exp(c * (s - 1))) + minus); +#elif POST_PROCESS_CONTRAST_TYPE == 4 + + float cInLower = contrastIntensity; + float cInUpper = 1.f / contrastIntensity; + // adjustments to match native better + // only do them when contrastIntensity is above 1 because 1 is neutral (no adjustment) + // below 1 it matches native nicely (though idk if contrast is ever lowered) + if (contrastIntensity > 1.f) + { + cInLower = cInLower * (4.f / 3.f); + cInUpper = cInUpper * (1.f / 1.02f); // this an optimisation. actual factor is 1.02 + } + float cMid = contrastMidPoint * (2.f / 3.f); + + // normalization factors for lower and upper power curve + float normalizationFactorLower = 1.f / cMid; + float normalizationFactorUpper = 1.f / (1.f - cMid); + + Color.r = SigmoidalContrastAdjustment(Color.r, cMid, cInLower, cInUpper, normalizationFactorLower, normalizationFactorUpper); + Color.g = SigmoidalContrastAdjustment(Color.g, cMid, cInLower, cInUpper, normalizationFactorLower, normalizationFactorUpper); + Color.b = SigmoidalContrastAdjustment(Color.b, cMid, cInLower, cInUpper, normalizationFactorLower, normalizationFactorUpper); + #endif Color = lerp(Color, colorFilter.rgb * MidGrayScale, colorFilter.a); From 24dda4d42163aefe635451e3d66e5464b0da15ec Mon Sep 17 00:00:00 2001 From: Lilium Date: Tue, 3 Oct 2023 01:22:05 +0200 Subject: [PATCH 053/270] add macro for comparing linearization of the LUTs with either sRGB or gamma 2.2 --- .../ColorGradingMerge_FF81_cs.hlsl | 24 ++++++++++++------- shaders/shared.hlsl | 1 + 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index a3910f5a..584e0efa 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -10,6 +10,14 @@ // For future development #define UNUSED_PARAMS 0 +#if LUT_USE_GAMMA_2_2_FOR_LINEARIZATION + #define LINEARIZE(x) \ + pow(x, 2.2f) +#else + #define LINEARIZE(x) \ + gamma_sRGB_to_linear(x) +#endif + static float AdditionalNeutralLUTPercentage = 0.f; // ~0.25 might be a good compromise static float LUTCorrectionPercentage = 1.f; @@ -63,9 +71,9 @@ uint3 ThreeToTwoDimensionCoordinates(uint3 UVW) void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) { - Analysis.black = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(0u)).rgb); + Analysis.black = LINEARIZE(LUT.Load(ThreeToTwoDimensionCoordinates(0u)).rgb); Analysis.blackY = Luminance(Analysis.black); - Analysis.white = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(LUT_SIZE_UINT - 1u)).rgb); + Analysis.white = LINEARIZE(LUT.Load(ThreeToTwoDimensionCoordinates(LUT_SIZE_UINT - 1u)).rgb); Analysis.whiteY = Luminance(Analysis.white); #if UNUSED_PARAMS Analysis.minY = FLT_MAX; @@ -93,7 +101,7 @@ void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) { for (uint z = 0; z < LUT_SIZE_UINT; z += analyzeTexelFrequency) { - float3 LUTColor = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(uint3(x, y, z))).rgb); + float3 LUTColor = LINEARIZE(LUT.Load(ThreeToTwoDimensionCoordinates(uint3(x, y, z))).rgb); minColor = min(minColor, LUTColor); maxColor = max(maxColor, LUTColor); @@ -139,7 +147,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b LUTAnalysis analysis; AnalyzeLUT(LUT, analysis); - float3 color = gamma_sRGB_to_linear(LUT.Load(UVW)); + float3 color = LINEARIZE(LUT.Load(UVW)); const float3 originalColor = color; // TODO: do we need these branches? Shaders are best without branches, especially when they could be replaced by min/max math. @@ -281,10 +289,10 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) float3 LUT2Color = LUT2.Load(inUVW); float3 LUT3Color = LUT3.Load(inUVW); float3 LUT4Color = LUT4.Load(inUVW); - LUT1Color = gamma_sRGB_to_linear(LUT1Color); - LUT2Color = gamma_sRGB_to_linear(LUT2Color); - LUT3Color = gamma_sRGB_to_linear(LUT3Color); - LUT4Color = gamma_sRGB_to_linear(LUT4Color); + LUT1Color = LINEARIZE(LUT1Color); + LUT2Color = LINEARIZE(LUT2Color); + LUT3Color = LINEARIZE(LUT3Color); + LUT4Color = LINEARIZE(LUT4Color); #endif // LUT_IMPROVEMENT_TYPE diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 10ee040d..95c3054d 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -18,5 +18,6 @@ // Makes LUTs sampling work in linear space, which is mathematically correct. Without this, they are stored as ~sRGB in a float texture and sampled in sRGB without acknowledging it. // This possibly shifts colors a lot, but it's correct, it's also necessary for HDR LUTs to work. #define LUT_FIX_GAMMA_MAPPING 1 +#define LUT_USE_GAMMA_2_2_FOR_LINEARIZATION 0 #define LUT_SIZE 16.f #define LUT_SIZE_UINT (uint)LUT_SIZE From 785f83d8f8cbbd8eb8fd5e18afda1c3714e4bdde Mon Sep 17 00:00:00 2001 From: ersh1 <16157893+ersh1@users.noreply.github.com> Date: Tue, 3 Oct 2023 01:50:23 +0200 Subject: [PATCH 054/270] refactored settings; sdr's swapchain is now RGB10A2 --- Plugin/dist/NativeHDR.toml | 22 ++ Plugin/src/Hooks.cpp | 426 +++++++++---------------------------- Plugin/src/Hooks.h | 88 ++------ Plugin/src/Settings.cpp | 84 +++++--- Plugin/src/Settings.h | 68 ++++-- Plugin/src/Utils.h | 2 - 6 files changed, 235 insertions(+), 455 deletions(-) create mode 100644 Plugin/dist/NativeHDR.toml diff --git a/Plugin/dist/NativeHDR.toml b/Plugin/dist/NativeHDR.toml new file mode 100644 index 00000000..76594557 --- /dev/null +++ b/Plugin/dist/NativeHDR.toml @@ -0,0 +1,22 @@ +[HDR] +GamePaperWhite = 200.0 +PeakBrightness = 1000.0 +UIPaperWhite = 200.0 + +[Main] +DisplayMode = 2 + +[RenderTargets] +RenderTargetsToUpgrade = [ + 'ImageSpaceBuffer', + 'ScaleformCompositeBuffer', + 'SF_ColorBuffer', + 'HDRImagespaceBuffer', + 'ImageSpaceHalfResBuffer', + 'ImageProcessColorTarget', + 'ImageSpaceBufferB10G11R11', + 'ImageSpaceBufferE5B9G9R9', + 'TAA_idTech7HistoryColorTarget', + 'EnvBRDF', + 'ImageSpaceBufferR10G10B10A2' +] \ No newline at end of file diff --git a/Plugin/src/Hooks.cpp b/Plugin/src/Hooks.cpp index 511c9a8a..13461bc8 100644 --- a/Plugin/src/Hooks.cpp +++ b/Plugin/src/Hooks.cpp @@ -15,36 +15,69 @@ namespace Hooks return nullptr; } - - void Patches::UpgradeRenderTarget(RE::BufferDefinition* a_buffer, RE::BS_DXGI_FORMAT a_newFormat, bool a_bLimited) + + void Hooks::ToggleEnableHDRSubSettings(RE::SettingsDataModel* a_model, bool a_bEnable) { - if (!a_buffer) { - return; + if (const auto peakBrightnessSetting = a_model->FindSettingById(static_cast(Settings::SettingID::kPeakBrightness))) { + peakBrightnessSetting->m_Enabled.SetValue(a_bEnable); } - if (!std::strcmp(a_buffer->bufferName, "FrameBuffer") || !std::strcmp(a_buffer->bufferName, "ImageSpaceBuffer") || !std::strcmp(a_buffer->bufferName, "ScaleformCompositeBuffer")) { - INFO("Warning: {} - skipping because buffer is handled by a separate setting", a_buffer->bufferName) - return; + if (const auto gamePaperWhiteSetting = a_model->FindSettingById(static_cast(Settings::SettingID::kGamePaperWhite))) { + gamePaperWhiteSetting->m_Enabled.SetValue(a_bEnable); } - if (a_bLimited && a_buffer->format != RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R8G8B8A8_UNORM && a_buffer->format != RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R8G8B8A8_UNORM_SRGB) { - auto formatNames = Utils::GetDXGIFormatNameMap(); - INFO("{} - skipping because format is {}", a_buffer->bufferName, formatNames[Offsets::GetDXGIFormat(a_buffer->format)]) - return; + if (const auto uiPaperWhiteSetting = a_model->FindSettingById(static_cast(Settings::SettingID::kUIPaperWhite))) { + uiPaperWhiteSetting->m_Enabled.SetValue(a_bEnable); } + } - Utils::SetBufferFormat(a_buffer, a_newFormat); + void Hooks::CreateCheckboxSetting(RE::ArrayNestedUIValue& a_settingList, Settings::Checkbox& a_setting, bool a_bEnabled) + { + auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); + auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); + + s.m_Text.SetStringValue(a_setting.name.c_str()); + s.m_Description.SetStringValue(a_setting.description.c_str()); + s.m_ID.SetValue(static_cast(a_setting.id)); + s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Checkbox); + s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); + s.m_Enabled.SetValue(a_bEnabled); + s.m_CheckBoxData.m_ShuttleMap.GetData().m_Value.SetValue(a_setting.value.get_data()); + a_settingList.AddItem(s); } - void Hooks::ToggleEnableHDRSubSettings(RE::SettingsDataModel* a_model, bool a_bEnable) + void Hooks::CreateStepperSetting(RE::ArrayNestedUIValue& a_settingList, Settings::Stepper& a_setting, bool a_bEnabled) { - if (const auto maxLuminanceSetting = a_model->FindSettingById(static_cast(Settings::SettingID::kMaxLuminance))) { - maxLuminanceSetting->m_Enabled.SetValue(a_bEnable); - } + auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); + auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); + + s.m_Text.SetStringValue(a_setting.name.c_str()); + s.m_Description.SetStringValue(a_setting.description.c_str()); + s.m_ID.SetValue(static_cast(a_setting.id)); + s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Stepper); + s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); + s.m_Enabled.SetValue(a_bEnabled); + for (auto& optionName : a_setting.optionNames) { + s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem(optionName.c_str()); + } + s.m_StepperData.m_ShuttleMap.GetData().m_Value.SetValue(a_setting.value.get_data()); + a_settingList.AddItem(s); + } - if (const auto paperwhiteSetting = a_model->FindSettingById(static_cast(Settings::SettingID::kPaperwhite))) { - paperwhiteSetting->m_Enabled.SetValue(a_bEnable); - } + void Hooks::CreateSliderSetting(RE::ArrayNestedUIValue& a_settingList, Settings::Slider& a_setting, bool a_bEnabled) + { + auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); + auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); + + s.m_Text.SetStringValue(a_setting.name.c_str()); + s.m_Description.SetStringValue(a_setting.description.c_str()); + s.m_ID.SetValue(static_cast(a_setting.id)); + s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Slider); + s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); + s.m_Enabled.SetValue(a_bEnabled); + s.m_SliderData.m_ShuttleMap.GetData().m_Value.SetValue(a_setting.GetSliderPercentage()); + s.m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue(a_setting.GetSliderText().c_str()); + a_settingList.AddItem(s); } void Hooks::Hook_UnkFunc(uintptr_t a1, RE::BGSSwapChainObject* a_bgsSwapchainObject) @@ -53,14 +86,8 @@ namespace Hooks swapChainObject = a_bgsSwapchainObject; const auto settings = Settings::Main::GetSingleton(); - switch (*settings->FrameBufferFormat) { - case 1: - a_bgsSwapchainObject->swapChainInterface->SetColorSpace1(DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020); - break; - case 2: - a_bgsSwapchainObject->swapChainInterface->SetColorSpace1(DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709); - break; - } + + a_bgsSwapchainObject->swapChainInterface->SetColorSpace1(settings->GetDisplayModeColorSpaceType()); return _UnkFunc(a1, a_bgsSwapchainObject); } @@ -88,342 +115,97 @@ namespace Hooks { _RecreateSwapchain(a1, a_bgsSwapChainObject, a_width, a_height, a5); - DXGI_COLOR_SPACE_TYPE newColorSpace; - switch (Utils::GetBufferFormat(RE::Buffers::FrameBuffer)) { - case RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_B8G8R8A8_UNORM: - default: - newColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709; - break; - case RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R10G10B10A2_UNORM: - newColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020; - break; - case RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT: - newColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709; - } - a_bgsSwapChainObject->swapChainInterface->SetColorSpace1(newColorSpace); + const auto settings = Settings::Main::GetSingleton(); + a_bgsSwapChainObject->swapChainInterface->SetColorSpace1(settings->GetDisplayModeColorSpaceType()); } - void Hooks::Hook_CreateDataModelOptions(void* a_arg1, RE::ArrayNestedUIValue& a_SettingList) + void Hooks::Hook_CreateDataModelOptions(void* a_arg1, RE::ArrayNestedUIValue& a_settingList) { const auto settings = Settings::Main::GetSingleton(); - { - auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); - auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); - constexpr auto id = static_cast(Settings::SettingID::kHDR); - - s.m_Text.SetStringValue("HDR"); - s.m_Description.SetStringValue("Sets the game's output mode between SDR, HDR10 PQ, or HDR scRGB."); - s.m_ID.SetValue(id); - s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Stepper); - s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); - s.m_Enabled.SetValue(true); - s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("OFF"); - s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("HDR10 PQ"); - s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("HDR scRGB"); - s.m_StepperData.m_ShuttleMap.GetData().m_Value.SetValue(*settings->FrameBufferFormat); - a_SettingList.AddItem(s); - } - - { - auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); - auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); - constexpr auto id = static_cast(Settings::SettingID::kMaxLuminance); - - s.m_Text.SetStringValue("Max Luminance"); - s.m_Description.SetStringValue("Sets the maximum luminance in HDR modes."); - s.m_ID.SetValue(id); - s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Slider); - s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); - s.m_Enabled.SetValue(settings->IsHDREnabled()); - s.m_SliderData.m_ShuttleMap.GetData().m_Value.SetValue(settings->GetMaxLuminanceSliderPercentage()); - s.m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue(settings->GetMaxLuminanceText().data()); - a_SettingList.AddItem(s); - } + CreateStepperSetting(a_settingList, settings->DisplayMode, true); - { - auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); - auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); - constexpr auto id = static_cast(Settings::SettingID::kPaperwhite); - - s.m_Text.SetStringValue("Paperwhite Brightness"); - s.m_Description.SetStringValue("Sets the paperwhite brightness used in HDR modes."); - s.m_ID.SetValue(id); - s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Slider); - s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); - s.m_Enabled.SetValue(settings->IsHDREnabled()); - s.m_SliderData.m_ShuttleMap.GetData().m_Value.SetValue(settings->GetPaperwhiteSliderPercentage()); - s.m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue(settings->GetPaperwhiteText().data()); - a_SettingList.AddItem(s); - } + CreateSliderSetting(a_settingList, settings->PeakBrightness, settings->IsHDREnabled()); + CreateSliderSetting(a_settingList, settings->GamePaperWhite, settings->IsHDREnabled()); + CreateSliderSetting(a_settingList, settings->UIPaperWhite, settings->IsHDREnabled()); // Initialize the rest of the settings after ours - _CreateDataModelOptions(a_arg1, a_SettingList); + _CreateDataModelOptions(a_arg1, a_settingList); } - void Hooks::Hook_SettingsDataModelBoolEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData) + void Hooks::Hook_SettingsDataModelBoolEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& a_eventData) { - _SettingsDataModelBoolEvent(a_arg1, EventData); + _SettingsDataModelBoolEvent(a_arg1, a_eventData); } - void Hooks::Hook_SettingsDataModelIntEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData) + void Hooks::Hook_SettingsDataModelIntEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& a_eventData) { - if (EventData.m_SettingID == static_cast(Settings::SettingID::kHDR)) { + if (a_eventData.m_SettingID == static_cast(Settings::SettingID::kDisplayMode)) { const auto settings = Settings::Main::GetSingleton(); - const auto prevValue = *settings->FrameBufferFormat; - const auto newValue = EventData.m_Value.Int; + const auto prevValue = *settings->DisplayMode.value; + const auto newValue = a_eventData.m_Value.Int; if (prevValue != newValue) { - *settings->FrameBufferFormat = newValue; + *settings->DisplayMode.value = newValue; - RE::BS_DXGI_FORMAT newFormat; - switch (*settings->FrameBufferFormat) { - case 0: - default: - newFormat = RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_B8G8R8A8_UNORM; - break; - case 1: - newFormat = RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R10G10B10A2_UNORM; - break; - case 2: - newFormat = RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT; - break; - } + const RE::BS_DXGI_FORMAT newFormat = settings->GetDisplayModeFormat(); Utils::SetBufferFormat(RE::Buffers::FrameBuffer, newFormat); if (prevValue == 0) { - ToggleEnableHDRSubSettings(EventData.m_Model, true); + ToggleEnableHDRSubSettings(a_eventData.m_Model, true); } else if (newValue == 0) { - ToggleEnableHDRSubSettings(EventData.m_Model, false); + ToggleEnableHDRSubSettings(a_eventData.m_Model, false); } - if (swapChainObject->format != newFormat) { - swapChainObject->format = newFormat; - Offsets::ToggleVsync(reinterpret_cast(*Offsets::unkToggleVsyncArg1Ptr + 0x8), *Offsets::bEnableVsync); - } - } - } - - _SettingsDataModelIntEvent(a_arg1, EventData); - } - - void Hooks::Hook_SettingsDataModelFloatEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData) - { - if (EventData.m_SettingID == static_cast(Settings::SettingID::kMaxLuminance)) { - const auto settings = Settings::Main::GetSingleton(); - - settings->SetMaxLuminanceFromSlider(EventData.m_Value.Float); - if (auto setting = EventData.m_Model->FindSettingById(EventData.m_SettingID)) { - setting->m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue(settings->GetMaxLuminanceText().data()); - } + swapChainObject->format = newFormat; - //settingTest->m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue(buffer); - } else if (EventData.m_SettingID == static_cast(Settings::SettingID::kPaperwhite)) { - const auto settings = Settings::Main::GetSingleton(); + // toggle vsync to force a swapchain recreation + Offsets::ToggleVsync(reinterpret_cast(*Offsets::unkToggleVsyncArg1Ptr + 0x8), *Offsets::bEnableVsync); - settings->SetPaperwhiteFromSlider(EventData.m_Value.Float); - if (auto setting = EventData.m_Model->FindSettingById(EventData.m_SettingID)) { - setting->m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue(settings->GetPaperwhiteText().data()); + settings->Save(); } } - _SettingsDataModelFloatEvent(a_arg1, EventData); + _SettingsDataModelIntEvent(a_arg1, a_eventData); } - void DebugHooks::Hook_CreateDataModelOptions(void* a_arg1, RE::ArrayNestedUIValue& a_SettingList) - { - int id = 600; - - { - auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); - auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); - - s.m_Text.SetStringValue("Hello Checkbox"); - s.m_Description.SetStringValue("Hello World Checkbox Description"); - s.m_ID.SetValue(id++); - s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Checkbox); - s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); - s.m_Enabled.SetValue(true); - s.m_CheckBoxData.m_ShuttleMap.GetData().m_Value.SetValue(false); - a_SettingList.AddItem(s); - } - - { - auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); - auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); - - s.m_Text.SetStringValue("Hello Checkbox Disabled"); - s.m_Description.SetStringValue("Hello World Checkbox Disabled Description"); - s.m_ID.SetValue(id++); - s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Checkbox); - s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); - s.m_Enabled.SetValue(false); - s.m_CheckBoxData.m_ShuttleMap.GetData().m_Value.SetValue(false); - a_SettingList.AddItem(s); - } - - { - auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); - auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); - - s.m_Text.SetStringValue("Hello Checkbox On"); - s.m_Description.SetStringValue("Hello World Checkbox On Description"); - s.m_ID.SetValue(id++); - s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Checkbox); - s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); - s.m_Enabled.SetValue(false); - s.m_CheckBoxData.m_ShuttleMap.GetData().m_Value.SetValue(true); - a_SettingList.AddItem(s); - } - - { - auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); - auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); - - s.m_Text.SetStringValue("Hello Stepper"); - s.m_Description.SetStringValue("Hello World Stepper Description"); - s.m_ID.SetValue(id++); - s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Stepper); - s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); - s.m_Enabled.SetValue(true); - s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("Option 1"); - s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("Option 2"); - s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("Option 3"); - s.m_StepperData.m_ShuttleMap.GetData().m_Value.SetValue(0); - a_SettingList.AddItem(s); - } - - { - auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); - auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); - - s.m_Text.SetStringValue("Hello Stepper Disabled"); - s.m_Description.SetStringValue("Hello World Stepper Disabled Description"); - s.m_ID.SetValue(id++); - s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Stepper); - s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); - s.m_Enabled.SetValue(false); - s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("Option 1"); - s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("Option 2"); - s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("Option 3"); - s.m_StepperData.m_ShuttleMap.GetData().m_Value.SetValue(0); - a_SettingList.AddItem(s); - } - - { - auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); - auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); - - s.m_Text.SetStringValue("Hello Stepper On"); - s.m_Description.SetStringValue("Hello World Stepper On Description"); - s.m_ID.SetValue(id++); - s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Stepper); - s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); - s.m_Enabled.SetValue(false); - s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("Option 1"); - s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("Option 2"); - s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem("Option 3"); - s.m_StepperData.m_ShuttleMap.GetData().m_Value.SetValue(2); - a_SettingList.AddItem(s); - } - - { - auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); - auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); - - s.m_Text.SetStringValue("Hello Slider"); - s.m_Description.SetStringValue("Hello World Slider Description"); - s.m_ID.SetValue(id++); - s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Slider); - s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); - s.m_Enabled.SetValue(true); - s.m_SliderData.m_ShuttleMap.GetData().m_Value.SetValue(0.5f); - s.m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue("0.5"); - a_SettingList.AddItem(s); - } - - { - auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); - auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); - - s.m_Text.SetStringValue("Hello Slider Disabled"); - s.m_Description.SetStringValue("Hello World Slider Disabled Description"); - s.m_ID.SetValue(id++); - s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::Slider); - s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); - s.m_Enabled.SetValue(false); - s.m_SliderData.m_ShuttleMap.GetData().m_Value.SetValue(0.5f); - s.m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue("0.5"); - a_SettingList.AddItem(s); - } - - // Initialize the rest of the settings after ours - _CreateDataModelOptions(a_arg1, a_SettingList); - } - - void DebugHooks::Hook_SettingsDataModelBoolEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData) - { - char buffer[128]; - sprintf_s(buffer, "Got a bool event: %d %s\n", EventData.m_SettingID, EventData.m_Value.Bool ? "true" : "false"); - OutputDebugStringA(buffer); - - _SettingsDataModelBoolEvent(a_arg1, EventData); - } - - void DebugHooks::Hook_SettingsDataModelIntEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData) - { - char buffer[128]; - sprintf_s(buffer, "Got an integer event: %d %d\n", EventData.m_SettingID, EventData.m_Value.Int); - OutputDebugStringA(buffer); - - if (auto settingTest = EventData.m_Model->FindSettingById(602)) - { - OutputDebugStringA("Manually updating checkbox state\n"); - settingTest->m_CheckBoxData.m_ShuttleMap.GetData().m_Value.SetValue(false); - } - - _SettingsDataModelIntEvent(a_arg1, EventData); - } - - void DebugHooks::Hook_SettingsDataModelFloatEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData) - { - char buffer[128]; - sprintf_s(buffer, "Got a float event: %d %f\n", EventData.m_SettingID, EventData.m_Value.Float); - OutputDebugStringA(buffer); - - if (EventData.m_SettingID >= 600) { - if (auto settingTest = EventData.m_Model->FindSettingById(EventData.m_SettingID)) { - char buffer[128]; - sprintf_s(buffer, "%.0f%%", EventData.m_Value.Float * 100.0f); - - settingTest->m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue(buffer); - } - } - - _SettingsDataModelFloatEvent(a_arg1, EventData); - } - - void DebugHooks::Hook_CreateRenderTargetView(uintptr_t a1, ID3D12Resource* a_resource, DXGI_FORMAT a_format, uint8_t a4, uint16_t a5, uintptr_t a6) + void Hooks::Hook_SettingsDataModelFloatEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& a_eventData) { - const auto textureDesc = a_resource->GetDesc(); + const auto settings = Settings::Main::GetSingleton(); - _CreateRenderTargetView(a1, a_resource, a_format, a4, a5, a6); - } + auto HandleSetting = [&](Settings::Slider& a_setting) { + const auto prevValue = a_setting.value.get_data(); + const auto newValue = a_setting.GetValueFromSlider(a_eventData.m_Value.Float); + if (prevValue != newValue) { + *a_setting.value = newValue; + if (auto setting = a_eventData.m_Model->FindSettingById(a_eventData.m_SettingID)) { + setting->m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue(a_setting.GetSliderText().data()); + } + settings->Save(); + } + }; - void DebugHooks::Hook_CreateDepthStencilView(uintptr_t a1, ID3D12Resource* a_resource, DXGI_FORMAT a_format, uint8_t a4, uint16_t a5, uintptr_t a6) - { - const auto textureDesc = a_resource->GetDesc(); + switch (a_eventData.m_SettingID) { + case static_cast(Settings::SettingID::kPeakBrightness): + HandleSetting(settings->PeakBrightness); + break; + case static_cast(Settings::SettingID::kGamePaperWhite): + HandleSetting(settings->GamePaperWhite); + break; + case static_cast(Settings::SettingID::kUIPaperWhite): + HandleSetting(settings->UIPaperWhite); + break; + } - _CreateDepthStencilView(a1, a_resource, a_format, a4, a5, a6); + _SettingsDataModelFloatEvent(a_arg1, a_eventData); } void Install() { -//#ifndef NDEBUG +#ifndef NDEBUG Utils::LogBuffers(); - //DebugHooks::Hook(); -//#endif +#endif Hooks::Hook(); Patches::Patch(); } diff --git a/Plugin/src/Hooks.h b/Plugin/src/Hooks.h index c455a84b..95741a6b 100644 --- a/Plugin/src/Hooks.h +++ b/Plugin/src/Hooks.h @@ -13,38 +13,12 @@ namespace Hooks { const auto settings = Settings::Main::GetSingleton(); - switch (*settings->FrameBufferFormat) { - case 1: - Utils::SetBufferFormat(RE::Buffers::FrameBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R10G10B10A2_UNORM); - break; - case 2: - Utils::SetBufferFormat(RE::Buffers::FrameBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT); - break; - } - - switch (*settings->ImageSpaceBufferFormat) { - case 1: - Utils::SetBufferFormat(RE::Buffers::ImageSpaceBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R10G10B10A2_UNORM); - break; - case 2: - Utils::SetBufferFormat(RE::Buffers::ImageSpaceBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT); - break; - } - - if (*settings->UpgradeUIRenderTarget) { - Utils::SetBufferFormat(RE::Buffers::ScaleformCompositeBuffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT); - } - - { - if (*settings->UpgradeRenderTargets > 0) { - const bool bLimited = *settings->UpgradeRenderTargets == 1 ? true : false; - const RE::BS_DXGI_FORMAT format = *settings->UpgradeRenderTargets == 1 ? RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R10G10B10A2_UNORM : RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT; + auto newFormat = settings->GetDisplayModeFormat(); + Utils::SetBufferFormat(RE::Buffers::FrameBuffer, newFormat); - for (const auto& renderTargetName : settings->RenderTargetsToUpgrade.get_collection()) { - if (auto buffer = GetBufferFromString(renderTargetName)) { - UpgradeRenderTarget(buffer, format, bLimited); - } - } + for (const auto& renderTargetName : settings->RenderTargetsToUpgrade.get_collection()) { + if (const auto buffer = GetBufferFromString(renderTargetName)) { + Utils::SetBufferFormat(buffer, RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT); } } @@ -53,8 +27,6 @@ namespace Hooks private: static RE::BufferDefinition* GetBufferFromString(std::string_view a_bufferName); - - static void UpgradeRenderTarget(RE::BufferDefinition* a_buffer, RE::BS_DXGI_FORMAT a_format, bool a_bLimited); }; class Hooks @@ -84,6 +56,9 @@ namespace Hooks static inline RE::BGSSwapChainObject* swapChainObject = nullptr; static void ToggleEnableHDRSubSettings(RE::SettingsDataModel* a_model, bool a_bEnable); + static void CreateCheckboxSetting(RE::ArrayNestedUIValue& a_settingList, Settings::Checkbox& a_setting, bool a_bEnabled); + static void CreateStepperSetting(RE::ArrayNestedUIValue& a_settingList, Settings::Stepper& a_setting, bool a_bEnabled); + static void CreateSliderSetting(RE::ArrayNestedUIValue& a_settingList, Settings::Slider& a_setting, bool a_bEnabled); static void Hook_UnkFunc(uintptr_t a1, RE::BGSSwapChainObject* a_bgsSwapchainObject); static inline std::add_pointer_t _UnkFunc; @@ -94,54 +69,15 @@ namespace Hooks static void Hook_RecreateSwapchain(void* a1, RE::BGSSwapChainObject* a_bgsSwapChainObject, uint32_t a_width, uint32_t a_height, uint8_t a5); static inline std::add_pointer_t _RecreateSwapchain; - static void Hook_CreateDataModelOptions(void* a_arg1, RE::ArrayNestedUIValue& a_SettingList); + static void Hook_CreateDataModelOptions(void* a_arg1, RE::ArrayNestedUIValue& a_settingList); static inline std::add_pointer_t _CreateDataModelOptions; - static void Hook_SettingsDataModelBoolEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData); + static void Hook_SettingsDataModelBoolEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& a_eventData); static inline std::add_pointer_t _SettingsDataModelBoolEvent; - static void Hook_SettingsDataModelIntEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData); + static void Hook_SettingsDataModelIntEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& a_eventData); static inline std::add_pointer_t _SettingsDataModelIntEvent; - static void Hook_SettingsDataModelFloatEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData); + static void Hook_SettingsDataModelFloatEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& a_eventData); static inline std::add_pointer_t _SettingsDataModelFloatEvent; }; - class DebugHooks - { - public: - static void Hook() - { - //_CreateDataModelOptions = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20BD2F9, Hook_CreateDataModelOptions); - //_SettingsDataModelBoolEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB63E, Hook_SettingsDataModelBoolEvent); - //_SettingsDataModelIntEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB937, Hook_SettingsDataModelIntEvent); - //_SettingsDataModelFloatEvent = dku::Hook::write_call<5>(dku::Hook::Module::get().base() + 0x20CB8EB, Hook_SettingsDataModelFloatEvent); - -#if 0 - const auto callsite1 = AsAddress(dku::Hook::Module::get().base() + 0x32ED294); - const auto callsite2 = AsAddress(dku::Hook::Module::get().base() + 0x32ED341); - const auto callsite3 = AsAddress(dku::Hook::Module::get().base() + 0x32ED2C1); - const auto callsite4 = AsAddress(dku::Hook::Module::get().base() + 0x32ED37F); - - _CreateRenderTargetView = dku::Hook::write_call<5>(callsite1, Hook_CreateRenderTargetView); - dku::Hook::write_call<5>(callsite2, Hook_CreateRenderTargetView); - _CreateDepthStencilView = dku::Hook::write_call<5>(callsite3, Hook_CreateDepthStencilView); - dku::Hook::write_call<5>(callsite4, Hook_CreateDepthStencilView); -#endif - } - - private: - static void Hook_CreateDataModelOptions(void* a_arg1, RE::ArrayNestedUIValue& a_SettingList); - static inline std::add_pointer_t _CreateDataModelOptions; - static void Hook_SettingsDataModelBoolEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData); - static inline std::add_pointer_t _SettingsDataModelBoolEvent; - static void Hook_SettingsDataModelIntEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData); - static inline std::add_pointer_t _SettingsDataModelIntEvent; - static void Hook_SettingsDataModelFloatEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& EventData); - static inline std::add_pointer_t _SettingsDataModelFloatEvent; - - static void Hook_CreateRenderTargetView(uintptr_t a1, ID3D12Resource* a_resource, DXGI_FORMAT a_format, uint8_t a4, uint16_t a5, uintptr_t a6); - static void Hook_CreateDepthStencilView(uintptr_t a1, ID3D12Resource* a_resource, DXGI_FORMAT a_format, uint8_t a4, uint16_t a5, uintptr_t a6); - static inline std::add_pointer_t _CreateRenderTargetView; - static inline std::add_pointer_t _CreateDepthStencilView; - }; - void Install(); } diff --git a/Plugin/src/Settings.cpp b/Plugin/src/Settings.cpp index 39314303..00382723 100644 --- a/Plugin/src/Settings.cpp +++ b/Plugin/src/Settings.cpp @@ -2,66 +2,86 @@ namespace Settings { - bool Main::IsHDREnabled() const - { - return FrameBufferFormat.get_data() != 0; - } - - float Main::GetMaxLuminanceSliderPercentage() const + float Slider::GetSliderPercentage() const { - return (MaxLuminance.get_data() - luminanceSliderMin) / (luminanceSliderMax - luminanceSliderMin); + return (value.get_data() - sliderMin) / (sliderMax - sliderMin); } - std::string Main::GetMaxLuminanceText() const + std::string Slider::GetSliderText() const { - return std::format("{}", MaxLuminance.get_data()); + return std::format("{}", value.get_data()); } - float Main::GetMaxLuminanceFromSlider(float a_percentage) const + float Slider::GetValueFromSlider(float a_percentage) const { - return std::roundf(a_percentage * (luminanceSliderMax - luminanceSliderMin) + luminanceSliderMin); + return std::roundf(a_percentage * (sliderMax - sliderMin) + sliderMin); } - void Main::SetMaxLuminanceFromSlider(float a_percentage) + void Slider::SetValueFromSlider(float a_percentage) { - *MaxLuminance = GetMaxLuminanceFromSlider(a_percentage); + *value = GetValueFromSlider(a_percentage); } - float Main::GetPaperwhiteSliderPercentage() const - { - return (Paperwhite.get_data() - paperwhiteSliderMin) / (paperwhiteSliderMax - paperwhiteSliderMin); - } - - std::string Main::GetPaperwhiteText() const + bool Main::IsHDREnabled() const { - return std::format("{}", Paperwhite.get_data()); + return DisplayMode.value.get_data() != 0; } - float Main::GetPaperwhiteFromSlider(float a_percentage) const + RE::BS_DXGI_FORMAT Main::GetDisplayModeFormat() const { - return std::roundf(a_percentage * (paperwhiteSliderMax - paperwhiteSliderMin) + paperwhiteSliderMin); + switch (DisplayMode.value.get_data()) { + case 0: + case 1: + default: + return RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R10G10B10A2_UNORM; + case 2: + return RE::BS_DXGI_FORMAT::BS_DXGI_FORMAT_R16G16B16A16_FLOAT; + } } - void Main::SetPaperwhiteFromSlider(float a_percentage) + DXGI_COLOR_SPACE_TYPE Main::GetDisplayModeColorSpaceType() const { - *Paperwhite = GetPaperwhiteFromSlider(a_percentage); + switch (DisplayMode.value.get_data()) { + case 0: + default: + return DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709; + case 1: + return DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020; + case 2: + return DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709; + } } void Main::Load() noexcept { static std::once_flag ConfigInit; std::call_once(ConfigInit, [&]() { - config.Bind(ImageSpaceBufferFormat, 0); - config.Bind(UpgradeUIRenderTarget, true); - config.Bind(UpgradeRenderTargets, 2); - config.Bind(FrameBufferFormat, 0); - config.Bind(MaxLuminance, 1000.f); - config.Bind(Paperwhite, 200.f); - config.Bind(RenderTargetsToUpgrade, "SF_ColorBuffer", "HDRImagespaceBuffer", "ImageSpaceHalfResBuffer", "ImageProcessColorTarget", "ImageSpaceBufferB10G11R11", "ImageSpaceBufferE5B9G9R9", "TAA_idTech7HistoryColorTarget", "EnvBRDF", "GBuffer_Normal_EmissiveIntensity", "ImageSpaceBufferR10G10B10A2"); + config.Bind(DisplayMode.value, 0); + config.Bind(PeakBrightness.value, PeakBrightness.defaultValue); + config.Bind(GamePaperWhite.value, GamePaperWhite.defaultValue); + config.Bind(UIPaperWhite.value, UIPaperWhite.defaultValue); + config.Bind(RenderTargetsToUpgrade, + "ImageSpaceBuffer", + "ScaleformCompositeBuffer", + "SF_ColorBuffer", + "HDRImagespaceBuffer", + "ImageSpaceHalfResBuffer", + "ImageProcessColorTarget", + "ImageSpaceBufferB10G11R11", + "ImageSpaceBufferE5B9G9R9", + "TAA_idTech7HistoryColorTarget", + "EnvBRDF", + "ImageSpaceBufferR10G10B10A2"); }); config.Load(); INFO("Config loaded"sv) } -} \ No newline at end of file + + void Main::Save() noexcept + { + config.Generate(); + config.Write(); + } +} diff --git a/Plugin/src/Settings.h b/Plugin/src/Settings.h index 05cb6740..4ef68142 100644 --- a/Plugin/src/Settings.h +++ b/Plugin/src/Settings.h @@ -1,5 +1,8 @@ #pragma once #include "DKUtil/Config.hpp" +#include "RE/Buffers.h" + +#include namespace Settings { @@ -7,44 +10,63 @@ namespace Settings enum class SettingID : unsigned int { - kHDR = 600, - kMaxLuminance, - kPaperwhite + kDisplayMode = 600, + kPeakBrightness, + kGamePaperWhite, + kUIPaperWhite }; + struct Setting + { + SettingID id; + std::string name; + std::string description; + }; + + struct Checkbox : Setting + { + Boolean value; + }; + + struct Stepper : Setting + { + Integer value; + std::vector optionNames; + }; + + struct Slider : Setting + { + Double value; + float sliderMin; + float sliderMax; + float defaultValue; + + float GetSliderPercentage() const; + std::string GetSliderText() const; + float GetValueFromSlider(float a_percentage) const; + void SetValueFromSlider(float a_percentage); + }; + class Main : public DKUtil::model::Singleton
{ public: - Integer ImageSpaceBufferFormat{ "ImageSpaceBufferFormat", "Main" }; - Boolean UpgradeUIRenderTarget{ "UpgradeUIRenderTarget", "Main" }; - Integer UpgradeRenderTargets{ "UpgradeRenderTargets", "Main" }; + Stepper DisplayMode{ SettingID::kDisplayMode, "Display Mode", "Sets the game's display mode between SDR, HDR10 PQ, or HDR scRGB", { "DisplayMode", "Main" }, { "SDR", "HDR10 PQ", "HDR scRGB" } }; - Integer FrameBufferFormat{ "FrameBufferFormat", "HDR" }; - Double MaxLuminance{ "MaxLuminance", "HDR" }; - Double Paperwhite{ "Paperwhite", "HDR" }; + Slider PeakBrightness{ SettingID::kPeakBrightness, "Peak Brightness", "Sets the peak brightness in HDR modes", { "PeakBrightness", "HDR" }, 80.f, 10000.f, 1000.f }; + Slider GamePaperWhite{ SettingID::kGamePaperWhite, "Game Paper White", "Sets the game paper white brightness in HDR modes", { "GamePaperWhite", "HDR" }, 80.f, 500.f, 200.f }; + Slider UIPaperWhite{ SettingID::kUIPaperWhite, "UI Paper White", "Sets the UI paper white brightness in HDR modes", { "UIPaperWhite", "HDR" }, 80.f, 500.f, 200.f }; String RenderTargetsToUpgrade{ "RenderTargetsToUpgrade", "RenderTargets" }; bool IsHDREnabled() const; - float GetMaxLuminanceSliderPercentage() const; - std::string GetMaxLuminanceText() const; - float GetMaxLuminanceFromSlider(float a_percentage) const; - void SetMaxLuminanceFromSlider(float a_percentage); - - float GetPaperwhiteSliderPercentage() const; - std::string GetPaperwhiteText() const; - float GetPaperwhiteFromSlider(float a_percentage) const; - void SetPaperwhiteFromSlider(float a_percentage); + RE::BS_DXGI_FORMAT GetDisplayModeFormat() const; + DXGI_COLOR_SPACE_TYPE GetDisplayModeColorSpaceType() const; void Load() noexcept; + void Save() noexcept; private: - static inline constexpr float luminanceSliderMin = 400.f; - static inline constexpr float luminanceSliderMax = 2000.f; - static inline constexpr float paperwhiteSliderMin = 80.f; - static inline constexpr float paperwhiteSliderMax = 500.f; - TomlConfig config = COMPILE_PROXY("NativeHDR.toml"sv); }; } diff --git a/Plugin/src/Utils.h b/Plugin/src/Utils.h index 2824ca38..326cfc64 100644 --- a/Plugin/src/Utils.h +++ b/Plugin/src/Utils.h @@ -1,8 +1,6 @@ #pragma once #include "RE/Buffers.h" -#include - namespace Utils { std::unordered_map GetDXGIFormatNameMap(); From a79c9d927f39f892aed1c0aa9b63e94d2ecdee99 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Tue, 3 Oct 2023 02:52:20 +0300 Subject: [PATCH 055/270] Add saturation formula --- shaders/color.hlsl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shaders/color.hlsl b/shaders/color.hlsl index b0ac156b..b2a20984 100644 --- a/shaders/color.hlsl +++ b/shaders/color.hlsl @@ -85,3 +85,9 @@ float Luminance(float3 color) // Fixed from "wrong" values: 0.2125 0.7154 0.0721f return dot(color, float3(0.2126f, 0.7152f, 0.0722f)); } + +float3 saturation(float3 color, float saturation) +{ + float3 luminance = Luminance(color); + return lerp(luminance, color, saturation); +} \ No newline at end of file From 5f69098dfa148ff7b01f95c9cb89f7858ba9c342 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Tue, 3 Oct 2023 02:52:56 +0300 Subject: [PATCH 056/270] Remove useless "#if" --- shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 584e0efa..2991803c 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -188,12 +188,11 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b } #endif -#if 1 // TODO: the above code introduces a lot of colors beyond the 0-1 range, which will then get clipped, causing a hue shift. This fix up might not be necessary as later we raise blacks anyway. + // TODO: the above code introduces a lot of colors beyond the 0-1 range, which will then get clipped, causing a hue shift. This fix up might not be necessary as later we raise blacks anyway. // Color may have gone negative // For example, if black is (3,3,3) and another value is (0,0,4), that // may result in (-3,-3,1) color = max(color, 0.f); -#endif static const float cubeNormalization = sqrt(3.f); // Normalize to 0-1 range // How distant are our LUT coordinates from black or white in 3D cube space? From adb50324d5ddad88955dde91ad004426bf5e86bf Mon Sep 17 00:00:00 2001 From: Filoppi Date: Tue, 3 Oct 2023 02:53:56 +0300 Subject: [PATCH 057/270] Add new gamma correction setting and unify all the defines around the code base --- shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl | 4 +-- .../ColorGradingMerge_FF81_cs.hlsl | 2 +- shaders/HDRComposite/HDRComposite_ps.hlsl | 34 ++++++++++++++++--- .../ScaleformComposite_FFAA_ps.hlsl | 3 +- shaders/shared.hlsl | 9 ++++- 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl b/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl index 51e90854..0ffa1e5c 100644 --- a/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl +++ b/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl @@ -1,8 +1,6 @@ #include "../shared.hlsl" #include "../color.hlsl" -#define HDR_USE_GAMMA_2_2 1 - Texture2D _8 : register(t0, space8); Texture2D _9 : register(t1, space8); Texture2D _10 : register(t2, space8); @@ -31,7 +29,7 @@ float4 PS(PSInputs inputs) : SV_Target color = max(color, 0.f); #if ENABLE_HDR -#if HDR_USE_GAMMA_2_2 +#if SDR_USE_GAMMA_2_2 color = pow(color, 2.2f); #else color = gamma_sRGB_to_linear(color); diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 2991803c..149e4dfe 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -10,7 +10,7 @@ // For future development #define UNUSED_PARAMS 0 -#if LUT_USE_GAMMA_2_2_FOR_LINEARIZATION +#if CORRECT_LUT_GAMMA_CHANGES_TYPE == 2 #define LINEARIZE(x) \ pow(x, 2.2f) #else diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index b3dd5731..739cf4ec 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -8,8 +8,6 @@ //#define APPLY_CINEMATICS //#define APPLY_MERGED_COLOR_GRADING_LUT -// This makes sense to use given we fix up (normalize) the LUTs colors and their gamma mapping, though the SDR tonemapper had still been developed with sRGB<->2.2 mismatch. -#define SDR_USE_GAMMA_2_2 1 // Suggested if "LUT_FIX_GAMMA_MAPPING" is true #define FIX_WRONG_SRGB_GAMMA_FORMULA 1 @@ -707,6 +705,17 @@ float3 TetrahedralInterpolation( } #if defined(APPLY_MERGED_COLOR_GRADING_LUT) + +float3 gamma_linear_to_sRGB_Bethesda_Optimized(float3 Color, float InverseGamma = 1.f / 2.4f) +{ + return (pow(Color, InverseGamma) * 1.055f) - 0.055f; +} + +float3 gamma_sRGB_to_linear_Bethesda_Optimized(float3 Color, float Gamma = 2.4f) +{ + return (pow(Color, Gamma) / 1.055f) + 0.055f; +} + // In/Out linear space float3 GradingLUT(float3 color, float2 uv) { @@ -716,7 +725,7 @@ float3 GradingLUT(float3 color, float2 uv) // Read gamma ini value, defaulting at 2.4 (makes little sense) float inverseGamma = 1.f / (max(SharedFrameData.Gamma, 0.001f)); // Weird linear -> sRGB conversion that clips values just above 0, and raises blacks. - const float3 LUTCoordinates = max((pow(color, inverseGamma) * 1.055f) - 0.055f, 0.f); // Does "max()" is probably unnecessary as LUT sampling is already clamped. + const float3 LUTCoordinates = max(gamma_linear_to_sRGB_Bethesda_Optimized(color, inverseGamma), 0.f); // Does "max()" is probably unnecessary as LUT sampling is already clamped. #endif // FIX_WRONG_SRGB_GAMMA_FORMULA #if ENABLE_LUT_TETRAHEDRAL_INTERPOLATION @@ -736,6 +745,7 @@ float3 GradingLUT(float3 color, float2 uv) return LUTColor; } + #endif // APPLY_MERGED_COLOR_GRADING_LUT // Takes a linear space untonemapped HDR color and a linear space tonemapped SDR color. @@ -822,10 +832,25 @@ PSOutput PS(PSInput psInput) float3 tonemappedPostProcessedGradedColor = tonemappedPostProcessedColor; -#if defined(APPLY_MERGED_COLOR_GRADING_LUT) && ENABLE_TONEMAP && ENABLE_LUT +#if defined(APPLY_MERGED_COLOR_GRADING_LUT) && ENABLE_TONEMAP + +#if ENABLE_LUT tonemappedPostProcessedGradedColor = GradingLUT(tonemappedPostProcessedColor, psInput.TEXCOORD); +#endif // ENABLE_LUT + +#if CORRECT_LUT_GAMMA_CHANGES_TYPE == 1 // Do this even if "ENABLE_LUT" is false, for consistency +#if FIX_WRONG_SRGB_GAMMA_FORMULA && 0 // Disabled as this looks awful, probably because the Bethesda optimize sRGB function doesn't even stay in the 0-1 range + // We fixed the LUT input gamma mapping formula so that LUTs apply correctly, technically speaking. Now we compensate for the adjustment. + tonemappedPostProcessedGradedColor = gamma_sRGB_to_linear_Bethesda_Optimized(gamma_linear_to_sRGB(tonemappedPostProcessedGradedColor)); +#endif +#if SDR_USE_GAMMA_2_2 + // This error was always built in the image if we assume Bethesda calibrated the game on gamma 2.2 displays + tonemappedPostProcessedGradedColor = pow(gamma_linear_to_sRGB_Bethesda_Optimized(tonemappedPostProcessedGradedColor), 2.2f); +#endif #endif // APPLY_MERGED_COLOR_GRADING_LUT +#endif // CORRECT_LUT_GAMMA_CHANGES_TYPE + const float3 finalOriginalColor = tonemappedPostProcessedGradedColor; // Final "original" (vanilla, ~unmodded) linear SDR color before output transform #if ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP && ENABLE_HDR //TODO: try this with the post process pass as well @@ -924,6 +949,7 @@ PSOutput PS(PSInput psInput) float3 outputColor = finalOriginalColor; +// Note that gamma was never applied if LUTs were disabled, but we don't care about that as the affected shaders permutations were never used #if SDR_USE_GAMMA_2_2 outputColor = pow(outputColor, 1.f / 2.2f); #else diff --git a/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl b/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl index 86110806..062d27d4 100644 --- a/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl +++ b/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl @@ -3,7 +3,6 @@ // Hack: change the alpha value at which the UI blends in in HDR, to increase readability. Range is 0 to 1, with 1 having no effect. #define HDR_UI_BLEND_POW 0.775f -#define HDR_GAMMA_CORRECTION 1 struct PushConstantWrapper_ScaleformCompositeLayout { @@ -36,7 +35,7 @@ float4 PS(PSInputs inputs) : SV_Target #if !ENABLE_HDR UIColor.xyz = gamma_linear_to_sRGB(UIColor.xyz); #else -#if HDR_GAMMA_CORRECTION +#if SDR_USE_GAMMA_2_2 UIColor.xyz = pow(gamma_linear_to_sRGB(UIColor.xyz), 2.2f); #endif // HDR_GAMMA_CORRECTION UIColor.xyz *= HDR_UI_PAPER_WHITE; diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 95c3054d..d3a19f1e 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -15,9 +15,16 @@ #define HDR_GAME_PAPER_WHITE HDR_REFERENCE_PAPER_WHITE * HDR_GAME_PAPER_WHITE_MULTIPLIER #define HDR_UI_PAPER_WHITE HDR_REFERENCE_PAPER_WHITE * HDR_UI_PAPER_WHITE_MULTIPLIER +// If this is true, the code makes the assumption that Bethesda developed and calibrated the game on gamma 2.2 screens, as opposed to sRGB gamma. +// This implies there's a mismatch baked in the output colors, as they were using a ~sRGB similar formula, which would then be interpreted by screens as 2.2 gamma. +// This makes sense to use given we fix up (normalize) the LUTs colors and their gamma mapping. +#define SDR_USE_GAMMA_2_2 1 + // Makes LUTs sampling work in linear space, which is mathematically correct. Without this, they are stored as ~sRGB in a float texture and sampled in sRGB without acknowledging it. // This possibly shifts colors a lot, but it's correct, it's also necessary for HDR LUTs to work. #define LUT_FIX_GAMMA_MAPPING 1 -#define LUT_USE_GAMMA_2_2_FOR_LINEARIZATION 0 +// Try to compensate for any technical gamma fixes we made. +// 0 none, 1 correct it in the tonemapper, 2 correct it in the LUT mixer +#define CORRECT_LUT_GAMMA_CHANGES_TYPE 1 #define LUT_SIZE 16.f #define LUT_SIZE_UINT (uint)LUT_SIZE From 9bee85eeebfe0ffdbf854855189bce97c63fd476 Mon Sep 17 00:00:00 2001 From: Nukem Date: Mon, 2 Oct 2023 20:41:49 -0400 Subject: [PATCH 058/270] Add shader/pipeline root signatures for later use and modification --- shaders/BinkMovie/RootSignature.hlsl | 9 ++ shaders/ColorGradingMerge/RootSignature.hlsl | 63 ++++++++++++++ .../RootSignature.hlsl | 78 +++++++++++++++++ shaders/FilmGrain/RootSignature.hlsl | 9 ++ shaders/HDRComposite/RootSignature.hlsl | 84 +++++++++++++++++++ shaders/PostSharpen/RootSignature.hlsl | 79 +++++++++++++++++ shaders/ScaleformComposite/RootSignature.hlsl | 9 ++ 7 files changed, 331 insertions(+) create mode 100644 shaders/BinkMovie/RootSignature.hlsl create mode 100644 shaders/ColorGradingMerge/RootSignature.hlsl create mode 100644 shaders/ContrastAdaptiveSharpening/RootSignature.hlsl create mode 100644 shaders/FilmGrain/RootSignature.hlsl create mode 100644 shaders/HDRComposite/RootSignature.hlsl create mode 100644 shaders/PostSharpen/RootSignature.hlsl create mode 100644 shaders/ScaleformComposite/RootSignature.hlsl diff --git a/shaders/BinkMovie/RootSignature.hlsl b/shaders/BinkMovie/RootSignature.hlsl new file mode 100644 index 00000000..1ecbb15e --- /dev/null +++ b/shaders/BinkMovie/RootSignature.hlsl @@ -0,0 +1,9 @@ +#define ShaderRootSignature \ + "RootFlags(ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT), " \ + "DescriptorTable(" \ + "SRV(t0, space=8), " \ + "SRV(t1, space=8), " \ + "SRV(t2, space=8) " \ + "), " \ + \ + "StaticSampler(s0, space=8) " \ No newline at end of file diff --git a/shaders/ColorGradingMerge/RootSignature.hlsl b/shaders/ColorGradingMerge/RootSignature.hlsl new file mode 100644 index 00000000..d6e7a12b --- /dev/null +++ b/shaders/ColorGradingMerge/RootSignature.hlsl @@ -0,0 +1,63 @@ +#define ShaderRootSignature \ + "RootConstants(num32BitConstants=5, b0), " \ + \ + "DescriptorTable(" \ + "SRV(t0, space=6), " \ + "SRV(t1, space=6), " \ + "SRV(t2, space=6), " \ + "SRV(t3, space=6), " \ + "SRV(t4, space=6), " \ + "SRV(t5, space=6), " \ + "SRV(t7, space=6), " \ + "SRV(t8, space=6), " \ + "SRV(t9, space=6), " \ + "SRV(t10, space=6), " \ + "SRV(t11, space=6), " \ + "SRV(t12, space=6), " \ + "SRV(t13, space=6), " \ + "SRV(t14, space=6), " \ + "SRV(t15, space=6), " \ + "SRV(t16, space=6), " \ + "SRV(t17, space=6), " \ + "SRV(t18, space=6), " \ + "SRV(t19, space=6), " \ + "SRV(t20, space=6), " \ + "SRV(t21, space=6), " \ + "SRV(t22, space=6), " \ + "SRV(t23, space=6), " \ + "SRV(t24, space=6), " \ + "SRV(t25, space=6), " \ + "SRV(t26, space=6), " \ + "UAV(u0, space=6), " \ + "UAV(u1, space=6), " \ + "SRV(t27, space=6), " \ + "SRV(t28, space=6) " \ + "), " \ + \ + "DescriptorTable(" \ + "Sampler(s0, space=6, numDescriptors=10), " \ + "Sampler(s10, space=6, numDescriptors=2) " \ + "), " \ + \ + "SRV(t6, space=6), " \ + "CBV(b0, space=6), " \ + "CBV(b1, space=6), " \ + \ + "DescriptorTable(" \ + "SRV(t0, space=8), " \ + "SRV(t1, space=8), " \ + "SRV(t2, space=8), " \ + "SRV(t3, space=8), " \ + "UAV(u0, space=8) " \ + "), " \ + \ + "StaticSampler(s12, space=6), " \ + "StaticSampler(s13, space=6), " \ + "StaticSampler(s14, space=6), " \ + "StaticSampler(s15, space=6), " \ + "StaticSampler(s16, space=6), " \ + "StaticSampler(s17, space=6), " \ + "StaticSampler(s18, space=6), " \ + "StaticSampler(s19, space=6), " \ + "StaticSampler(s20, space=6), " \ + "StaticSampler(s21, space=6) " \ No newline at end of file diff --git a/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl b/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl new file mode 100644 index 00000000..53d39147 --- /dev/null +++ b/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl @@ -0,0 +1,78 @@ +#define ShaderRootSignature \ + "DescriptorTable(" \ + "SRV(t0, space=6), " \ + "SRV(t1, space=6), " \ + "SRV(t2, space=6), " \ + "SRV(t3, space=6), " \ + "SRV(t4, space=6), " \ + "SRV(t5, space=6), " \ + "SRV(t7, space=6), " \ + "SRV(t8, space=6), " \ + "SRV(t9, space=6), " \ + "SRV(t10, space=6), " \ + "SRV(t11, space=6), " \ + "SRV(t12, space=6), " \ + "SRV(t13, space=6), " \ + "SRV(t14, space=6), " \ + "SRV(t15, space=6), " \ + "SRV(t16, space=6), " \ + "SRV(t17, space=6), " \ + "SRV(t18, space=6), " \ + "SRV(t19, space=6), " \ + "SRV(t20, space=6), " \ + "SRV(t21, space=6), " \ + "SRV(t22, space=6), " \ + "SRV(t23, space=6), " \ + "SRV(t24, space=6), " \ + "SRV(t25, space=6), " \ + "SRV(t26, space=6), " \ + "UAV(u0, space=6), " \ + "UAV(u1, space=6), " \ + "SRV(t27, space=6), " \ + "SRV(t28, space=6) " \ + "), " \ + \ + "DescriptorTable(" \ + "Sampler(s0, space=6, numDescriptors=10), " \ + "Sampler(s10, space=6, numDescriptors=2) " \ + "), " \ + \ + "SRV(t6, space=6), " \ + "CBV(b0, space=6), " \ + "CBV(b1, space=6), " \ + \ + "DescriptorTable(" \ + "SRV(t2, space=7), " \ + "SRV(t3, space=7), " \ + "SRV(t5, space=7), " \ + "SRV(t6, space=7), " \ + "UAV(u0, space=7), " \ + "UAV(u1, space=7), " \ + "UAV(u2, space=7), " \ + "UAV(u3, space=7), " \ + "SRV(t7, space=7, numDescriptors=12) " \ + "), " \ + \ + "SRV(t0, space=7), " \ + "SRV(t1, space=7), " \ + "SRV(t4, space=7), " \ + "CBV(b0, space=7), " \ + "CBV(b1, space=7), " \ + "CBV(b2, space=7), " \ + \ + "DescriptorTable(" \ + "SRV(t0, space=8), " \ + "UAV(u0, space=8) " \ + "), " \ + \ + "CBV(b0, space=8), " \ + "StaticSampler(s12, space=6), " \ + "StaticSampler(s13, space=6), " \ + "StaticSampler(s14, space=6), " \ + "StaticSampler(s15, space=6), " \ + "StaticSampler(s16, space=6), " \ + "StaticSampler(s17, space=6), " \ + "StaticSampler(s18, space=6), " \ + "StaticSampler(s19, space=6), " \ + "StaticSampler(s20, space=6), " \ + "StaticSampler(s21, space=6) " \ No newline at end of file diff --git a/shaders/FilmGrain/RootSignature.hlsl b/shaders/FilmGrain/RootSignature.hlsl new file mode 100644 index 00000000..77a1a61e --- /dev/null +++ b/shaders/FilmGrain/RootSignature.hlsl @@ -0,0 +1,9 @@ +#define ShaderRootSignature \ + "RootFlags(ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT), " \ + "RootConstants(num32BitConstants=3, b0), " \ + \ + "DescriptorTable(" \ + "SRV(t0, space=8) " \ + "), " \ + \ + "StaticSampler(s0, space=8) " \ No newline at end of file diff --git a/shaders/HDRComposite/RootSignature.hlsl b/shaders/HDRComposite/RootSignature.hlsl new file mode 100644 index 00000000..b31c97a9 --- /dev/null +++ b/shaders/HDRComposite/RootSignature.hlsl @@ -0,0 +1,84 @@ +#define ShaderRootSignature \ + "RootFlags(ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT), " \ + "RootConstants(num32BitConstants=3, b0), " \ + \ + "DescriptorTable(" \ + "SRV(t0, space=6), " \ + "SRV(t1, space=6), " \ + "SRV(t2, space=6), " \ + "SRV(t3, space=6), " \ + "SRV(t4, space=6), " \ + "SRV(t5, space=6), " \ + "SRV(t7, space=6), " \ + "SRV(t8, space=6), " \ + "SRV(t9, space=6), " \ + "SRV(t10, space=6), " \ + "SRV(t11, space=6), " \ + "SRV(t12, space=6), " \ + "SRV(t13, space=6), " \ + "SRV(t14, space=6), " \ + "SRV(t15, space=6), " \ + "SRV(t16, space=6), " \ + "SRV(t17, space=6), " \ + "SRV(t18, space=6), " \ + "SRV(t19, space=6), " \ + "SRV(t20, space=6), " \ + "SRV(t21, space=6), " \ + "SRV(t22, space=6), " \ + "SRV(t23, space=6), " \ + "SRV(t24, space=6), " \ + "SRV(t25, space=6), " \ + "SRV(t26, space=6), " \ + "UAV(u0, space=6), " \ + "UAV(u1, space=6), " \ + "SRV(t27, space=6), " \ + "SRV(t28, space=6) " \ + "), " \ + \ + "DescriptorTable(" \ + "Sampler(s0, space=6, numDescriptors=10), " \ + "Sampler(s10, space=6, numDescriptors=2) " \ + "), " \ + \ + "SRV(t6, space=6), " \ + "CBV(b0, space=6), " \ + "CBV(b1, space=6), " \ + \ + "DescriptorTable(" \ + "SRV(t2, space=7), " \ + "SRV(t3, space=7), " \ + "SRV(t5, space=7), " \ + "SRV(t6, space=7), " \ + "UAV(u0, space=7), " \ + "UAV(u1, space=7), " \ + "UAV(u2, space=7), " \ + "UAV(u3, space=7), " \ + "SRV(t7, space=7, numDescriptors=12) " \ + "), " \ + \ + "SRV(t0, space=7), " \ + "SRV(t1, space=7), " \ + "SRV(t4, space=7), " \ + "CBV(b0, space=7), " \ + "CBV(b1, space=7), " \ + "CBV(b2, space=7), " \ + \ + "DescriptorTable(" \ + "SRV(t0, space=9), " \ + "SRV(t1, space=9), " \ + "SRV(t2, space=9), " \ + "SRV(t3, space=9), " \ + "SRV(t4, space=9) " \ + "), " \ + \ + "StaticSampler(s12, space=6), " \ + "StaticSampler(s13, space=6), " \ + "StaticSampler(s14, space=6), " \ + "StaticSampler(s15, space=6), " \ + "StaticSampler(s16, space=6), " \ + "StaticSampler(s17, space=6), " \ + "StaticSampler(s18, space=6), " \ + "StaticSampler(s19, space=6), " \ + "StaticSampler(s20, space=6), " \ + "StaticSampler(s21, space=6), " \ + "StaticSampler(s0, space=9) " \ No newline at end of file diff --git a/shaders/PostSharpen/RootSignature.hlsl b/shaders/PostSharpen/RootSignature.hlsl new file mode 100644 index 00000000..9262126c --- /dev/null +++ b/shaders/PostSharpen/RootSignature.hlsl @@ -0,0 +1,79 @@ +#define ShaderRootSignature \ + "RootFlags(ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT), " \ + "RootConstants(num32BitConstants=7, b0), " \ + \ + "DescriptorTable( " \ + "SRV(t0, space=6), " \ + "SRV(t1, space=6), " \ + "SRV(t2, space=6), " \ + "SRV(t3, space=6), " \ + "SRV(t4, space=6), " \ + "SRV(t5, space=6), " \ + "SRV(t7, space=6), " \ + "SRV(t8, space=6), " \ + "SRV(t9, space=6), " \ + "SRV(t10, space=6), " \ + "SRV(t11, space=6), " \ + "SRV(t12, space=6), " \ + "SRV(t13, space=6), " \ + "SRV(t14, space=6), " \ + "SRV(t15, space=6), " \ + "SRV(t16, space=6), " \ + "SRV(t17, space=6), " \ + "SRV(t18, space=6), " \ + "SRV(t19, space=6), " \ + "SRV(t20, space=6), " \ + "SRV(t21, space=6), " \ + "SRV(t22, space=6), " \ + "SRV(t23, space=6), " \ + "SRV(t24, space=6), " \ + "SRV(t25, space=6), " \ + "SRV(t26, space=6), " \ + "UAV(u0, space=6), " \ + "UAV(u1, space=6), " \ + "SRV(t27, space=6), " \ + "SRV(t28, space=6) " \ + "), " \ + \ + "DescriptorTable(" \ + "Sampler(s0, space=6, numDescriptors=10), " \ + "Sampler(s10, space=6, numDescriptors=2) " \ + "), " \ + \ + "SRV(t6, space=6), " \ + "CBV(b0, space=6), " \ + "CBV(b1, space=6), " \ + \ + "DescriptorTable(" \ + "SRV(t2, space=7), " \ + "SRV(t3, space=7), " \ + "SRV(t5, space=7), " \ + "SRV(t6, space=7), " \ + "UAV(u0, space=7), " \ + "UAV(u1, space=7), " \ + "UAV(u2, space=7), " \ + "UAV(u3, space=7), " \ + "SRV(t7, space=7, numDescriptors=12) " \ + "), " \ + \ + "SRV(t0, space=7), " \ + "SRV(t1, space=7), " \ + "SRV(t4, space=7), " \ + "CBV(b0, space=7), " \ + "CBV(b1, space=7), " \ + "CBV(b2, space=7), " \ + \ + "DescriptorTable(" \ + "SRV(t0, space=8) " \ + "), " \ + \ + "StaticSampler(s12, space=6), " \ + "StaticSampler(s13, space=6), " \ + "StaticSampler(s14, space=6), " \ + "StaticSampler(s15, space=6), " \ + "StaticSampler(s16, space=6), " \ + "StaticSampler(s17, space=6), " \ + "StaticSampler(s18, space=6), " \ + "StaticSampler(s19, space=6), " \ + "StaticSampler(s20, space=6), " \ + "StaticSampler(s21, space=6) " \ No newline at end of file diff --git a/shaders/ScaleformComposite/RootSignature.hlsl b/shaders/ScaleformComposite/RootSignature.hlsl new file mode 100644 index 00000000..8a4e8065 --- /dev/null +++ b/shaders/ScaleformComposite/RootSignature.hlsl @@ -0,0 +1,9 @@ +#define ShaderRootSignature \ + "RootFlags(ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT), " \ + "RootConstants(num32BitConstants=3, b0), " \ + \ + "DescriptorTable(" \ + "SRV(t0, space=8)" \ + "), " \ + \ + "StaticSampler(s0, space=8) " \ No newline at end of file From c7f80c28701a8208009f255f4ddb348da0b9a7ff Mon Sep 17 00:00:00 2001 From: Filoppi Date: Tue, 3 Oct 2023 04:08:37 +0300 Subject: [PATCH 059/270] Gamma correction fixes. Make DICE tonemap per channel (WIP) --- .../ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl | 2 +- shaders/HDRComposite/HDRComposite_ps.hlsl | 12 +++++++++--- shaders/shared.hlsl | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 149e4dfe..44a348a7 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -10,7 +10,7 @@ // For future development #define UNUSED_PARAMS 0 -#if CORRECT_LUT_GAMMA_CHANGES_TYPE == 2 +#if CORRECT_LUT_GAMMA_CHANGES_TYPE == 2 && SDR_USE_GAMMA_2_2 #define LINEARIZE(x) \ pow(x, 2.2f) #else diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 739cf4ec..5f2ea1b5 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -427,7 +427,12 @@ float3 DICETonemap( float MaxOutputLuminance, float HighlightsShoulderStart = 0.f) { - //TODO: try "per channel" option given all other tm is per channel +#if 1 // Do by channel for now, to match all other tonemappers, especially when "INVERT_TONEMAP_HIGHLIGHTS_ONLY" is on + Color.r = luminanceCompress(Color.r, MaxOutputLuminance, HighlightsShoulderStart); + Color.g = luminanceCompress(Color.g, MaxOutputLuminance, HighlightsShoulderStart); + Color.b = luminanceCompress(Color.b, MaxOutputLuminance, HighlightsShoulderStart); + return Color; +#else //TODO: try "per channel" option given all other tm is per channel const float sourceLuminance = Luminance(Color); if (sourceLuminance > 0.0f) { @@ -435,6 +440,7 @@ float3 DICETonemap( Color *= compressedLuminance / sourceLuminance; } return Color; +#endif } // sigmoidal inspired contrast adjustment using 2 power curves @@ -568,7 +574,7 @@ float3 PostProcess_Inverse( Color = pow(Color / contrastMidPoint, 1.f / contrastIntensity) * contrastMidPoint; -#elif POST_PROCESS_CONTRAST_TYPE == 2 +#elif POST_PROCESS_CONTRAST_TYPE == 2 || POST_PROCESS_CONTRAST_TYPE == 3 //TODO: implement @@ -845,7 +851,7 @@ PSOutput PS(PSInput psInput) #endif #if SDR_USE_GAMMA_2_2 // This error was always built in the image if we assume Bethesda calibrated the game on gamma 2.2 displays - tonemappedPostProcessedGradedColor = pow(gamma_linear_to_sRGB_Bethesda_Optimized(tonemappedPostProcessedGradedColor), 2.2f); + tonemappedPostProcessedGradedColor = pow(gamma_linear_to_sRGB(tonemappedPostProcessedGradedColor), 2.2f); #endif #endif // APPLY_MERGED_COLOR_GRADING_LUT diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index d3a19f1e..ef25efaa 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -25,6 +25,6 @@ #define LUT_FIX_GAMMA_MAPPING 1 // Try to compensate for any technical gamma fixes we made. // 0 none, 1 correct it in the tonemapper, 2 correct it in the LUT mixer -#define CORRECT_LUT_GAMMA_CHANGES_TYPE 1 +#define CORRECT_LUT_GAMMA_CHANGES_TYPE 2 #define LUT_SIZE 16.f #define LUT_SIZE_UINT (uint)LUT_SIZE From 1b45d9a1714e05450b9ac3fde105bded88e1e5fb Mon Sep 17 00:00:00 2001 From: Filoppi Date: Tue, 3 Oct 2023 04:35:44 +0300 Subject: [PATCH 060/270] Fix RestorePostProcess() sometimes going crazy due to "postProcessColorRatio" having highly unstable values (likely due to tiny values dividing tiny values, producing unstable results). --- shaders/HDRComposite/HDRComposite_ps.hlsl | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 5f2ea1b5..f8efae0b 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -432,7 +432,7 @@ float3 DICETonemap( Color.g = luminanceCompress(Color.g, MaxOutputLuminance, HighlightsShoulderStart); Color.b = luminanceCompress(Color.b, MaxOutputLuminance, HighlightsShoulderStart); return Color; -#else //TODO: try "per channel" option given all other tm is per channel +#else //TODO: try "per channel" option given all other tm is per channel. If not, restore the "luminance based" inverse tonemapping, to keep the SDR colors more. const float sourceLuminance = Luminance(Color); if (sourceLuminance > 0.0f) { @@ -764,7 +764,8 @@ float3 RestorePostProcess(float3 inverseTonemappedColor, float3 postProcessColor postProcessedOffsetColor += postProcessColorOffset * midGrayScale; // Near black, we prefer using the "offset" (sum) pp restoration method, as otherwise any raised black would not work, // for example if any zero was shifted to a more raised color, "postProcessColorRatio" would not be able to replicate that shift. - return lerp(postProcessedOffsetColor, postProcessedRatioColor, saturate(tonemappedColor / MaxShadowsColor)); + return lerp(postProcessedOffsetColor, postProcessedRatioColor, saturate(tonemappedColor / MaxShadowsColor)); //TODO: test and improve. + //return select(tonemappedColor == 0, postProcessedOffsetColor, postProcessedRatioColor); //TODO: improve. This causes flickering sometimes. } PSOutput PS(PSInput psInput) @@ -845,7 +846,7 @@ PSOutput PS(PSInput psInput) #endif // ENABLE_LUT #if CORRECT_LUT_GAMMA_CHANGES_TYPE == 1 // Do this even if "ENABLE_LUT" is false, for consistency -#if FIX_WRONG_SRGB_GAMMA_FORMULA && 0 // Disabled as this looks awful, probably because the Bethesda optimize sRGB function doesn't even stay in the 0-1 range +#if FIX_WRONG_SRGB_GAMMA_FORMULA && 0 //TODO: Disabled as this looks awful, probably because the Bethesda optimize sRGB function doesn't even stay in the 0-1 range // We fixed the LUT input gamma mapping formula so that LUTs apply correctly, technically speaking. Now we compensate for the adjustment. tonemappedPostProcessedGradedColor = gamma_sRGB_to_linear_Bethesda_Optimized(gamma_linear_to_sRGB(tonemappedPostProcessedGradedColor)); #endif @@ -859,8 +860,8 @@ PSOutput PS(PSInput psInput) const float3 finalOriginalColor = tonemappedPostProcessedGradedColor; // Final "original" (vanilla, ~unmodded) linear SDR color before output transform -#if ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP && ENABLE_HDR //TODO: try this with the post process pass as well - const float3 postProcessColorRatio = safeDivision(finalOriginalColor, tonemappedColor); +#if ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP && ENABLE_HDR + const float3 postProcessColorRatio = finalOriginalColor / tonemappedColor; const float3 postProcessColorOffset = finalOriginalColor - tonemappedColor; #endif @@ -910,6 +911,8 @@ PSOutput PS(PSInput psInput) break; } + //TODO: improve inverse tonemapped look. Are mid tones too "raised"? + for (uint channel = 0; channel < 3; channel++) { // Scale all non highlights by the scale the smallest (first) highlight would have, so we keep the curves connected From 4682b3832ce121ae365e4e0be22346e7921349b0 Mon Sep 17 00:00:00 2001 From: Lilium Date: Tue, 3 Oct 2023 06:21:58 +0200 Subject: [PATCH 061/270] rename CORRECT_LUT_GAMMA_CHANGES_TYPE to EMULATE_SDR_GAMMA_APPEARANCE which only enables gamma 2.2 emulation after LUT application as the other mode is incorrect --- .../ColorGradingMerge_FF81_cs.hlsl | 24 +++++++------------ shaders/HDRComposite/HDRComposite_ps.hlsl | 6 ++--- shaders/shared.hlsl | 6 ++--- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 44a348a7..4e1d9184 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -10,14 +10,6 @@ // For future development #define UNUSED_PARAMS 0 -#if CORRECT_LUT_GAMMA_CHANGES_TYPE == 2 && SDR_USE_GAMMA_2_2 - #define LINEARIZE(x) \ - pow(x, 2.2f) -#else - #define LINEARIZE(x) \ - gamma_sRGB_to_linear(x) -#endif - static float AdditionalNeutralLUTPercentage = 0.f; // ~0.25 might be a good compromise static float LUTCorrectionPercentage = 1.f; @@ -71,9 +63,9 @@ uint3 ThreeToTwoDimensionCoordinates(uint3 UVW) void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) { - Analysis.black = LINEARIZE(LUT.Load(ThreeToTwoDimensionCoordinates(0u)).rgb); + Analysis.black = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(0u)).rgb); Analysis.blackY = Luminance(Analysis.black); - Analysis.white = LINEARIZE(LUT.Load(ThreeToTwoDimensionCoordinates(LUT_SIZE_UINT - 1u)).rgb); + Analysis.white = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(LUT_SIZE_UINT - 1u)).rgb); Analysis.whiteY = Luminance(Analysis.white); #if UNUSED_PARAMS Analysis.minY = FLT_MAX; @@ -101,7 +93,7 @@ void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) { for (uint z = 0; z < LUT_SIZE_UINT; z += analyzeTexelFrequency) { - float3 LUTColor = LINEARIZE(LUT.Load(ThreeToTwoDimensionCoordinates(uint3(x, y, z))).rgb); + float3 LUTColor = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(uint3(x, y, z))).rgb); minColor = min(minColor, LUTColor); maxColor = max(maxColor, LUTColor); @@ -147,7 +139,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b LUTAnalysis analysis; AnalyzeLUT(LUT, analysis); - float3 color = LINEARIZE(LUT.Load(UVW)); + float3 color = gamma_sRGB_to_linear(LUT.Load(UVW)); const float3 originalColor = color; // TODO: do we need these branches? Shaders are best without branches, especially when they could be replaced by min/max math. @@ -288,10 +280,10 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) float3 LUT2Color = LUT2.Load(inUVW); float3 LUT3Color = LUT3.Load(inUVW); float3 LUT4Color = LUT4.Load(inUVW); - LUT1Color = LINEARIZE(LUT1Color); - LUT2Color = LINEARIZE(LUT2Color); - LUT3Color = LINEARIZE(LUT3Color); - LUT4Color = LINEARIZE(LUT4Color); + LUT1Color = gamma_sRGB_to_linear(LUT1Color); + LUT2Color = gamma_sRGB_to_linear(LUT2Color); + LUT3Color = gamma_sRGB_to_linear(LUT3Color); + LUT4Color = gamma_sRGB_to_linear(LUT4Color); #endif // LUT_IMPROVEMENT_TYPE diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index f8efae0b..f0d2e315 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -845,7 +845,7 @@ PSOutput PS(PSInput psInput) tonemappedPostProcessedGradedColor = GradingLUT(tonemappedPostProcessedColor, psInput.TEXCOORD); #endif // ENABLE_LUT -#if CORRECT_LUT_GAMMA_CHANGES_TYPE == 1 // Do this even if "ENABLE_LUT" is false, for consistency +#if EMULATE_SDR_GAMMA_APPEARANCE // Do this even if "ENABLE_LUT" is false, for consistency #if FIX_WRONG_SRGB_GAMMA_FORMULA && 0 //TODO: Disabled as this looks awful, probably because the Bethesda optimize sRGB function doesn't even stay in the 0-1 range // We fixed the LUT input gamma mapping formula so that LUTs apply correctly, technically speaking. Now we compensate for the adjustment. tonemappedPostProcessedGradedColor = gamma_sRGB_to_linear_Bethesda_Optimized(gamma_linear_to_sRGB(tonemappedPostProcessedGradedColor)); @@ -854,9 +854,9 @@ PSOutput PS(PSInput psInput) // This error was always built in the image if we assume Bethesda calibrated the game on gamma 2.2 displays tonemappedPostProcessedGradedColor = pow(gamma_linear_to_sRGB(tonemappedPostProcessedGradedColor), 2.2f); #endif -#endif // APPLY_MERGED_COLOR_GRADING_LUT +#endif // EMULATE_SDR_GAMMA_APPEARANCE -#endif // CORRECT_LUT_GAMMA_CHANGES_TYPE +#endif // APPLY_MERGED_COLOR_GRADING_LUT const float3 finalOriginalColor = tonemappedPostProcessedGradedColor; // Final "original" (vanilla, ~unmodded) linear SDR color before output transform diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index ef25efaa..886c74c0 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -23,8 +23,8 @@ // Makes LUTs sampling work in linear space, which is mathematically correct. Without this, they are stored as ~sRGB in a float texture and sampled in sRGB without acknowledging it. // This possibly shifts colors a lot, but it's correct, it's also necessary for HDR LUTs to work. #define LUT_FIX_GAMMA_MAPPING 1 -// Try to compensate for any technical gamma fixes we made. -// 0 none, 1 correct it in the tonemapper, 2 correct it in the LUT mixer -#define CORRECT_LUT_GAMMA_CHANGES_TYPE 2 +// Emulates the SDR look of the game on an SDR display using gamma 2.2. +// Which by our best guesses should be the creative intent. +#define EMULATE_SDR_GAMMA_APPEARANCE 1 #define LUT_SIZE 16.f #define LUT_SIZE_UINT (uint)LUT_SIZE From 1d7a7df330e9b84fa9a74cdf25dc75ecacb61696 Mon Sep 17 00:00:00 2001 From: Lilium Date: Wed, 4 Oct 2023 00:25:25 +0200 Subject: [PATCH 062/270] adjust sigmoidal inspired contrast adjustment to bias the contrastIntensity towards the lower and top end remove old actual sigmoidal contrast adjustment as it is no longer needed --- shaders/HDRComposite/HDRComposite_ps.hlsl | 84 +++++++++++++---------- 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index f0d2e315..b581bb2e 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -17,9 +17,9 @@ // 0 disable contrast adjustment // 1 original (weak, generates values beyond 0-1 which then get clipped) // 2 improved (looks more natural, avoids values below 0, but will overshoot beyond 1 more often, and will raise blacks) -// 3 Sigmoidal (smoothest looking, but harder to match) -// 4 Sigmoidal inspired -#define POST_PROCESS_CONTRAST_TYPE 4 +// 3 Sigmoidal inspired and biases contrast increases towards the lower and top end +// (optimisation left if contrastIntensity doesn't go below 1) +#define POST_PROCESS_CONTRAST_TYPE 3 #define ENABLE_LUT 1 // LUTs are too low resolutions to resolve gradients smoothly if the LUT color suddenly changes between samples #define ENABLE_LUT_TETRAHEDRAL_INTERPOLATION 1 @@ -443,28 +443,61 @@ float3 DICETonemap( #endif } +// don't set this above 1 as it will break the curve (turns into a double S curve instead of a single one) +// below 1 it increases the amount of additional contrast being applied +// only change this for testing +#define C 1 + +static const float den = log2(C + 1.f); + +// bias towards smaller numbers +void Log2Adjust(inout float Channel) +{ +#if (C != 1) + Channel = log2(Channel * c + 1.f) + / den; + return; +#else + Channel = log2(Channel + 1.f); + return; +#endif +} + // sigmoidal inspired contrast adjustment using 2 power curves // normalizes both lower and upper part which is divided by contrastMidPoint // and then applies a power curve based off of contrastIntensity -// power curve for lower is just pow(x, contrastIntensity) -// while for upper it's pow(x, 1 / contrastIntensity) float SigmoidalContrastAdjustment( float Channel, float contrastMidPoint, - float contrastIntensityLower, - float contrastIntensityUpper, + float contrastIntensity, float normalizationFactorLower, float normalizationFactorUpper) { [flatten] if (Channel <= contrastMidPoint) { - Channel = pow(Channel * normalizationFactorLower, contrastIntensityLower) + Channel *= normalizationFactorLower; + // doing this for contrastIntensity below 1 greatly desaturates compared to not doing this + // look into if contrastIntensity ever goes below 1 + // and remove this check if it does not + if (contrastIntensity > 1.f) + { + Log2Adjust(Channel); + } + Channel = pow(Channel, contrastIntensity) / normalizationFactorLower; } else { - Channel = pow((Channel - contrastMidPoint) * normalizationFactorUpper, contrastIntensityUpper) + Channel = 1.f - ((Channel - contrastMidPoint) * normalizationFactorUpper); + // doing this for contrastIntensity below 1 greatly desaturates compared to not doing this + // look into if contrastIntensity ever goes below 1 + // and remove this check if it does not + if (contrastIntensity > 1.f) + { + Log2Adjust(Channel); + } + Channel = (1.f - pow(Channel, contrastIntensity)) / normalizationFactorUpper + contrastMidPoint; } @@ -507,42 +540,23 @@ float3 PostProcess( #elif POST_PROCESS_CONTRAST_TYPE == 3 - // sigmoidal contrast adjustment doesn't clip colors - // https://www.imagemagick.org/Usage/color_mods/#sigmoidal - - // Multiplier to somewhat match the original contrast adjustment - static const float sigmoidalContrastAdjustment = 2.5f; //TODO: we could probably find an even better default multiplication - - float c = max(contrastIntensity * sigmoidalContrastAdjustment, EPSILON); // protect against division by zero - float s = contrastMidPoint; // can be set to 0.5 for better darkening of shadows - - float minus = -1 / (1 + exp(c * s)); - - // there should be some optimisation left here - Color = (1 / (1 + exp(c * (s - Color))) + minus) - / (1 / (1 + exp(c * (s - 1))) + minus); - -#elif POST_PROCESS_CONTRAST_TYPE == 4 - - float cInLower = contrastIntensity; - float cInUpper = 1.f / contrastIntensity; - // adjustments to match native better + float cIn = contrastIntensity; + // adjustment to match native better // only do them when contrastIntensity is above 1 because 1 is neutral (no adjustment) // below 1 it matches native nicely (though idk if contrast is ever lowered) if (contrastIntensity > 1.f) { - cInLower = cInLower * (4.f / 3.f); - cInUpper = cInUpper * (1.f / 1.02f); // this an optimisation. actual factor is 1.02 + cIn = contrastIntensity * (4.f / 3.f); } - float cMid = contrastMidPoint * (2.f / 3.f); + float cMid = contrastMidPoint; // normalization factors for lower and upper power curve float normalizationFactorLower = 1.f / cMid; float normalizationFactorUpper = 1.f / (1.f - cMid); - Color.r = SigmoidalContrastAdjustment(Color.r, cMid, cInLower, cInUpper, normalizationFactorLower, normalizationFactorUpper); - Color.g = SigmoidalContrastAdjustment(Color.g, cMid, cInLower, cInUpper, normalizationFactorLower, normalizationFactorUpper); - Color.b = SigmoidalContrastAdjustment(Color.b, cMid, cInLower, cInUpper, normalizationFactorLower, normalizationFactorUpper); + Color.r = SigmoidalContrastAdjustment(Color.r, cMid, cIn, normalizationFactorLower, normalizationFactorUpper); + Color.g = SigmoidalContrastAdjustment(Color.g, cMid, cIn, normalizationFactorLower, normalizationFactorUpper); + Color.b = SigmoidalContrastAdjustment(Color.b, cMid, cIn, normalizationFactorLower, normalizationFactorUpper); #endif From dead6b009b4954dfa64842ebbbae4e95d6a3bffa Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Sat, 30 Sep 2023 20:05:15 -0400 Subject: [PATCH 063/270] luts: use OKLCH for color boost --- .../ColorGradingMerge_FF81_cs.hlsl | 248 ++++++++++-------- shaders/color.hlsl | 73 +++++- 2 files changed, 213 insertions(+), 108 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 4e1d9184..54fc90cd 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -9,6 +9,12 @@ #define OPTIMIZE_LUT_ANALYSIS true // For future development #define UNUSED_PARAMS 0 +#define LUT__DEBUG_VALUES true + +// Enum: {0: AS_IS, 1: ORIGINAL, 2:WHITE, 3: MAX_HUE, 4: CLAMP } +#define LUT__SDR__ON_CLIP 1 +#define LUT__HDR__ON_CLIP 0 + static float AdditionalNeutralLUTPercentage = 0.f; // ~0.25 might be a good compromise static float LUTCorrectionPercentage = 1.f; @@ -30,6 +36,7 @@ struct LUTAnalysis float blackY; float3 white; float whiteY; + float whiteL; #if UNUSED_PARAMS float minChannel; float maxChannel; @@ -67,6 +74,7 @@ void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) Analysis.blackY = Luminance(Analysis.black); Analysis.white = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(LUT_SIZE_UINT - 1u)).rgb); Analysis.whiteY = Luminance(Analysis.white); + Analysis.whiteL = linear_srgb_to_oklab(Analysis.white)[0]; #if UNUSED_PARAMS Analysis.minY = FLT_MAX; Analysis.maxY = -FLT_MAX; @@ -142,121 +150,147 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b float3 color = gamma_sRGB_to_linear(LUT.Load(UVW)); const float3 originalColor = color; - // TODO: do we need these branches? Shaders are best without branches, especially when they could be replaced by min/max math. - // Is the branch to prevent weird LUTs from being adjusted? - if ((analysis.whiteY > analysis.blackY) - && ((analysis.whiteY - analysis.blackY) < 1.f)) - { - // Lower black floor (shadow pass): - // In 3D space, drags all points towards 0 relative to how much black - // point has moved (eg (on 256): -4, -10, -5) - // Distance from 0 is factor to decide how much each pixel should move, - // with (1) (white) not moving at all. - // *Applying to each channel individually also removes tint - static const float shadowCrushingModulation = 2.2f; // Defines the gradient with which shadow are shifted to zero //TODO: find best value - const float3 reduceFactor = lerp(1.f / (1.f - analysis.black), 1.f, pow(neutralLUTColor, shadowCrushingModulation)); - - // Reapply tint without raising black floor: - // In 3D space, shift points away from black **per channel** to - // reintroduce tint back to the pixels. Use multiplication to avoid - // raised black floor. - const float3 increaseFactor = lerp(1.f + analysis.black, 1.f, neutralLUTColor); - - // color * reduce = [0,1] relative to XYZ: - // result *= black tint - // (0,0,0) must always compute to 0 - // (1,1,1) must remain unchanged - // Scaling is strongest at 0, and weakest at 1 (none) - color = (1.f - ((1.f - color) * reduceFactor)) * increaseFactor; - -#if 0 // "Invalid" range analysis - if (any(color > 1)) - { - return 0; - } - else if (any(color < 0)) - { - return 1; - } + // TODO: Remove if branching + + // If LUT is inversed (eg: photo negative) don't do anything + if (analysis.whiteY < analysis.blackY) return originalColor; // Inversed LUT + + // If LUT is full range already nothing to do + if (analysis.whiteY - analysis.blackY >= 1.f) return lerp(originalColor, color, LUTCorrectionPercentage); + + // Return black on (0,0,0) + if (!any(originalColor)) return float3(0.f, 0.f, 0.f); + + // Return white on (1,1,1) + const float3 white = float3(1.f,1.f,1.f); + if (dot(originalColor, white) >= 3.f) return white; + +#if LUT__DEBUG_VALUES + float3 DEBUG_COLOR = float3(1.f,0,1.f); // Magenta #endif - // TODO: the above code introduces a lot of colors beyond the 0-1 range, which will then get clipped, causing a hue shift. This fix up might not be necessary as later we raise blacks anyway. - // Color may have gone negative - // For example, if black is (3,3,3) and another value is (0,0,4), that - // may result in (-3,-3,1) - color = max(color, 0.f); - - static const float cubeNormalization = sqrt(3.f); // Normalize to 0-1 range - // How distant are our LUT coordinates from black or white in 3D cube space? - const float blackDistance = hypot3(neutralLUTColor) / cubeNormalization; - const float whiteDistance = hypot3(1.f - neutralLUTColor) / cubeNormalization; - const float totalRange = blackDistance + whiteDistance; - - const float currentY = Luminance(color); // In case this was negative, the shadows raise pass should bring it back to the >= 0 range - - // Magic number to decide by how much to scale the new shadow area - const float shadowCompensationPercentage = 1.f - analysis.blackY; - - // Brightness multiplier from shadows: - // Because the amount the black level was raised is proportional to - // the harshness of a linear gradient, a compensation must be made - // to avoid black crushing, and maintain visibility at certain - // points in the LUT. The scaling used will be relative to the - // raised black floor level to ensure proportional consistency per - // LUT. This is only done to the newly created shadow section (if any). - // Simplified, if black started raised, a new shadow section was - // created, and that needs extra luminance for visibility. - const float shadowsRaise = linearNormalization( - min(currentY, analysis.blackY), - 0.f, - analysis.blackY, - 1.f + shadowCompensationPercentage, - 1.f); - // TODO: Analyze grayscale for shadow raise. For example, if black shadow was raised - // by 5%, then analyze the ramping from 5%-10% and apply that to - // the new 0% - 5% raise. - - // TODO: apply both shadow and highlight raise to the whole image range, instead of a portion of it, to make its gradient smoother? Or maybe apply it in perceptual (gamma) space. - // The functions could also be simplified a lot. - - // Brightness multiplier from highlights: - // Boost luminance of all texels relative to their distance to white - // and how much white is to be raised. - // (ie: white drags everything towards it) - float highlightsRaise = linearNormalization( - whiteDistance, + // While it unclear how exactly the floor was raised, remove the tint to floor + // the values to 0. This will remove the haze and tint giving a fuller chroma + // Sample and hold targetChroma + const float3 reduceFactor = linearNormalization( + neutralLUTColor, + 0.f, + 1.f, + 1.f / (1.f - analysis.black), + 1.f); + + const float3 detintedColor = max(0.f, 1.f-((1.f - color) * reduceFactor)); + + const float targetChroma = linear_srgb_to_oklch(detintedColor)[1]; + + // Adjust the value back to recreate a smooth Y gradient since 0 is floored + // Sample and hold targetL + + const float3 increaseFactor = linearNormalization( + neutralLUTColor, 0.f, - totalRange, - 1.f / analysis.whiteY, + 1.f, + 1.f + analysis.black, 1.f); - // Scale by the maximum value this could ever have, so it's normalized and we are guaranteed target luminance doesn't go beyond 1. Note that this can literally destroy luminance. - highlightsRaise *= SDRRange ? analysis.whiteY : 1.f; - const float targetY = currentY * highlightsRaise * shadowsRaise; - // TODO: why are we clamping to full white here? luminance 1 (or beyond) might not match a white color at all, should we instead try to conserve the hue? Though that's much harder as we'd need to analyze more LUT pixels. - if (SDRRange && targetY >= 1.f) - { - color = 1.f; // Clamp (clip to full white, it also looks great with external AutoHDR) - // Or, limit to max luminance while maintaining hue, though may result in LUT max Y being below 1 - // Or, still target full white for (1,1,1), but scale others relative to whiteY - // Or, don't adjust unless white point - } - else if (currentY != 0.f) - { - color *= max(targetY, 0.f) / max(currentY, 0.f); // Clamp luminances to avoid a double negative creating a positive number - } + const float3 retintedColor = detintedColor * increaseFactor; - // Optional step to keep colors in the SDR range. - if (SDRRange) - { - color = saturate(color); + float targetL = linear_srgb_to_oklch(retintedColor)[0]; + +#if LUT__DEBUG_VALUES + if (targetL < 0) return DEBUG_COLOR; +#endif + + // Texels exista as points in 3D cube. We are moving two heavily weighted + // points and need to compute the net force to be applied. + // Black texel is 0 from itself and sqrt(3) from white + // White texel is 0 from itself and sqrt(3) from black + // Values in between can be closer to one than the other. + // This can also be represented as [-1 to 1] or [-B to +W]. + // For interpolation between 0 and a maximum, it become [0, ..., W, ..., W+B] + // Distances do not always add up to sqrt(3) (eg: 1,0,0) + + const float blackDistance = hypot3(neutralLUTColor); + const float whiteDistance = hypot3(1.f - neutralLUTColor); + const float totalRange = blackDistance + whiteDistance; + +#if LUT__DEBUG_VALUES + // whiteY most always be > 0 + if (analysis.whiteL <= 0) return DEBUG_COLOR; +#endif + + + // Boost lightness by how much white was reduced + const float raiseL = linearNormalization( + whiteDistance, + 0.f, + totalRange, + 1.f / analysis.whiteL, + 1.f); + +#if LUT__DEBUG_VALUES + // increaseY must always be >= 1 + if (raiseL < 1) return DEBUG_COLOR; +#endif + + targetL *= raiseL; + + // Use hue from LUT cube color + const float targetHue = linear_srgb_to_oklch(color)[2]; + +#if LUT__SDR__ON_CLIP > 0 || LUT__HDR__ON_CLIP > 0 + if (targetL >= 1.f) { + #if LUT__SDR__ON_CLIP > 0 && LUT__SDR__ON_CLIP != LUT__HDR__ON_CLIP + if (SDRRange) { + #endif + + #if LUT__SDR__ON_CLIP == 1 + // Original (do nothing) + #elif LUT__SDR__ON_CLIP == 2 + // White + color = white; + #elif LUT__SDR__ON_CLIP == 3 + // TODO + #elif LUT__SDR__ON_CLIP == 4 + color = oklch_to_linear_srgb(float3(1.f, targetChroma, targetHue)); + #endif + + #if LUT__SDR__ON_CLIP > 0 && LUT__SDR__ON_CLIP != LUT__HDR__ON_CLIP } - // TODO: consider removing this if it's proven unnecessary on all LUTs - // Some protection against invalid colors - else if (Luminance(color) < 0.f) - { - color = 0.f; + #if LUT__HDR__ON_CLIP > 0 + else { + #endif + #endif + + #if LUT__SDR__ON_CLIP == 0 && LUT__HDR__ON_CLIP > 0 + if (!SDRRange) { + #endif + + #if LUT__HDR__ON_CLIP != LUT__SDR_ON_CLIP + #if LUT__HDR__ON_CLIP == 1 + // Original (do nothing) + #elif LUT__HDR__ON_CLIP == 2 + // White + color = white; + #elif LUT__HDR__ON_CLIP == 3 + // TODO + #elif LUT__HDR__ON_CLIP == 4 + color = oklch_to_linear_srgb(float3(1.f, targetChroma, targetHue)); + #endif + #endif + #if LUT__HDR__ON_CLIP > 0 && LUT__SDR__ON_CLIP != LUT__HDR__ON_CLIP } + #endif + } else { + color = oklch_to_linear_srgb(float3(targetL, targetChroma, targetHue)); + } +#elif + color = oklch_to_linear_srgb(float3(targetL, targetChroma, targetHue)); +#endif + + // Optional step to keep colors in the SDR range. + if (SDRRange) { + color = saturate(color); } color = lerp(originalColor, color, LUTCorrectionPercentage); diff --git a/shaders/color.hlsl b/shaders/color.hlsl index b2a20984..40e988f2 100644 --- a/shaders/color.hlsl +++ b/shaders/color.hlsl @@ -90,4 +90,75 @@ float3 saturation(float3 color, float saturation) { float3 luminance = Luminance(color); return lerp(luminance, color, saturation); -} \ No newline at end of file +} + + +float3 linear_srgb_to_oklab(float3 rgb) { + float l = (0.4122214708f * rgb.r) + (0.5363325363f * rgb.g) + (0.0514459929f * rgb.b); + float m = (0.2119034982f * rgb.r) + (0.6806995451f * rgb.g) + (0.1073969566f * rgb.b); + float s = (0.0883024619f * rgb.r) + (0.2817188376f * rgb.g) + (0.6299787005f * rgb.b); + + float l_ = pow(l, 1.f/3.f); + float m_ = pow(m, 1.f/3.f); + float s_ = pow(s, 1.f/3.f); + + return float3( + (0.2104542553f * l_) + (0.7936177850f * m_) - (0.0040720468f * s_), + (1.9779984951f * l_) - (2.4285922050f * m_) + (0.4505937099f * s_), + (0.0259040371f * l_) + (0.7827717662f * m_) - (0.8086757660f * s_) + ); +} + + +float3 oklab_to_linear_srgb(float3 lab) { + float L = lab[0]; + float a = lab[1]; + float b = lab[2]; + float l_ = L + (0.3963377774f * a) + (0.2158037573f * b); + float m_ = L - (0.1055613458f * a) - (0.0638541728f * b); + float s_ = L - (0.0894841775f * a) - (1.2914855480f * b); + + float l = l_ * l_ * l_; + float m = m_ * m_ * m_; + float s = s_ * s_ * s_; + + return float3( + (+4.0767416621f * l) - (3.3077115913f * m) + (0.2309699292f * s), + (-1.2684380046f * l) + (2.6097574011f * m) - (0.3413193965f * s), + (-0.0041960863f * l) - (0.7034186147f * m) + (1.7076147010f * s) + ); +} + +float3 oklab_to_oklch(float3 lab) { + float L = lab[0]; + float a = lab[1]; + float b = lab[2]; + return float3 ( + L, + sqrt((a*a) + (b*b)), + atan2(b, a) + ); +} + +float3 oklch_to_oklab(float3 lch) { + float L = lch[0]; + float C = lch[1]; + float h = lch[2]; + return float3 ( + L, + C * cos(h), + C * sin(h) + ); +} + +float3 oklch_to_linear_srgb(float3 lch) { + return oklab_to_linear_srgb( + oklch_to_oklab(lch) + ); +} + +float3 linear_srgb_to_oklch(float3 rgb) { + return oklab_to_oklch( + linear_srgb_to_oklab(rgb) + ); +} From 9d9f2784289dccdd1091a2389fd6e2a18324b3b0 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Wed, 4 Oct 2023 02:50:30 +0300 Subject: [PATCH 064/270] Polish --- .../ColorGradingMerge_FF81_cs.hlsl | 95 +++++++++---------- shaders/HDRComposite/HDRComposite_ps.hlsl | 6 +- shaders/shared.hlsl | 6 +- 3 files changed, 49 insertions(+), 58 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 54fc90cd..3b1a5bbd 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -9,12 +9,11 @@ #define OPTIMIZE_LUT_ANALYSIS true // For future development #define UNUSED_PARAMS 0 -#define LUT__DEBUG_VALUES true +#define LUT_DEBUG_VALUES true // Enum: {0: AS_IS, 1: ORIGINAL, 2:WHITE, 3: MAX_HUE, 4: CLAMP } -#define LUT__SDR__ON_CLIP 1 -#define LUT__HDR__ON_CLIP 0 - +#define LUT_SDR_ON_CLIP 1 +#define LUT_HDR_ON_CLIP 0 static float AdditionalNeutralLUTPercentage = 0.f; // ~0.25 might be a good compromise static float LUTCorrectionPercentage = 1.f; @@ -68,6 +67,7 @@ uint3 ThreeToTwoDimensionCoordinates(uint3 UVW) return uint3(U, UVW.y, 0); } +//TODO: try to do the LUT analysys in linear from gamma 2.2 instead of sRGB, but then convert back to linear from sRGB at the end void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) { Analysis.black = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(0u)).rgb); @@ -140,7 +140,7 @@ void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) #endif // UNUSED_PARAMS } -// Analyzes each LUT texel and normalize their range. +// Analyzes each LUT texel and normalizes their range. // Slow but effective. float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, bool SDRRange = false) { @@ -150,24 +150,15 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b float3 color = gamma_sRGB_to_linear(LUT.Load(UVW)); const float3 originalColor = color; - // TODO: Remove if branching - - // If LUT is inversed (eg: photo negative) don't do anything - if (analysis.whiteY < analysis.blackY) return originalColor; // Inversed LUT + if (analysis.whiteY < analysis.blackY // If LUT is inversed (eg: photo negative) don't do anything + || analysis.whiteY - analysis.blackY >= 1.f) // If LUT is full range already nothing to do + { + return color; + } - // If LUT is full range already nothing to do - if (analysis.whiteY - analysis.blackY >= 1.f) return lerp(originalColor, color, LUTCorrectionPercentage); - - // Return black on (0,0,0) - if (!any(originalColor)) return float3(0.f, 0.f, 0.f); - - // Return white on (1,1,1) - const float3 white = float3(1.f,1.f,1.f); - if (dot(originalColor, white) >= 3.f) return white; - -#if LUT__DEBUG_VALUES - float3 DEBUG_COLOR = float3(1.f,0,1.f); // Magenta -#endif +#if LUT_DEBUG_VALUES + float3 DEBUG_COLOR = float3(1.f, 0.f, 1.f); // Magenta +#endif // LUT_DEBUG_VALUES // While it unclear how exactly the floor was raised, remove the tint to floor // the values to 0. This will remove the haze and tint giving a fuller chroma @@ -197,9 +188,9 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b float targetL = linear_srgb_to_oklch(retintedColor)[0]; -#if LUT__DEBUG_VALUES +#if LUT_DEBUG_VALUES if (targetL < 0) return DEBUG_COLOR; -#endif +#endif // LUT_DEBUG_VALUES // Texels exista as points in 3D cube. We are moving two heavily weighted // points and need to compute the net force to be applied. @@ -214,11 +205,10 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b const float whiteDistance = hypot3(1.f - neutralLUTColor); const float totalRange = blackDistance + whiteDistance; -#if LUT__DEBUG_VALUES +#if LUT_DEBUG_VALUES // whiteY most always be > 0 if (analysis.whiteL <= 0) return DEBUG_COLOR; -#endif - +#endif // LUT_DEBUG_VALUES // Boost lightness by how much white was reduced const float raiseL = linearNormalization( @@ -228,59 +218,60 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b 1.f / analysis.whiteL, 1.f); -#if LUT__DEBUG_VALUES +#if LUT_DEBUG_VALUES // increaseY must always be >= 1 if (raiseL < 1) return DEBUG_COLOR; -#endif +#endif // LUT_DEBUG_VALUES targetL *= raiseL; // Use hue from LUT cube color const float targetHue = linear_srgb_to_oklch(color)[2]; -#if LUT__SDR__ON_CLIP > 0 || LUT__HDR__ON_CLIP > 0 +//TODO: copy these define values into runtime bools +#if LUT_SDR_ON_CLIP > 0 || LUT_HDR_ON_CLIP > 0 if (targetL >= 1.f) { - #if LUT__SDR__ON_CLIP > 0 && LUT__SDR__ON_CLIP != LUT__HDR__ON_CLIP +#if LUT_SDR_ON_CLIP > 0 && LUT_SDR_ON_CLIP != LUT_HDR_ON_CLIP if (SDRRange) { - #endif +#endif - #if LUT__SDR__ON_CLIP == 1 +#if LUT_SDR_ON_CLIP == 1 // Original (do nothing) - #elif LUT__SDR__ON_CLIP == 2 +#elif LUT_SDR_ON_CLIP == 2 // White color = white; - #elif LUT__SDR__ON_CLIP == 3 +#elif LUT_SDR_ON_CLIP == 3 // TODO - #elif LUT__SDR__ON_CLIP == 4 +#elif LUT_SDR_ON_CLIP == 4 color = oklch_to_linear_srgb(float3(1.f, targetChroma, targetHue)); - #endif +#endif - #if LUT__SDR__ON_CLIP > 0 && LUT__SDR__ON_CLIP != LUT__HDR__ON_CLIP +#if LUT_SDR_ON_CLIP > 0 && LUT_SDR_ON_CLIP != LUT_HDR_ON_CLIP } - #if LUT__HDR__ON_CLIP > 0 +#if LUT_HDR_ON_CLIP > 0 else { - #endif - #endif +#endif +#endif - #if LUT__SDR__ON_CLIP == 0 && LUT__HDR__ON_CLIP > 0 +#if LUT_SDR_ON_CLIP == 0 && LUT_HDR_ON_CLIP > 0 if (!SDRRange) { - #endif +#endif - #if LUT__HDR__ON_CLIP != LUT__SDR_ON_CLIP - #if LUT__HDR__ON_CLIP == 1 +#if LUT_HDR_ON_CLIP != LUT__SDR_ON_CLIP +#if LUT_HDR_ON_CLIP == 1 // Original (do nothing) - #elif LUT__HDR__ON_CLIP == 2 +#elif LUT_HDR_ON_CLIP == 2 // White color = white; - #elif LUT__HDR__ON_CLIP == 3 +#elif LUT_HDR_ON_CLIP == 3 // TODO - #elif LUT__HDR__ON_CLIP == 4 +#elif LUT_HDR_ON_CLIP == 4 color = oklch_to_linear_srgb(float3(1.f, targetChroma, targetHue)); - #endif - #endif - #if LUT__HDR__ON_CLIP > 0 && LUT__SDR__ON_CLIP != LUT__HDR__ON_CLIP +#endif +#endif +#if LUT_HDR_ON_CLIP > 0 && LUT_SDR_ON_CLIP != LUT_HDR_ON_CLIP } - #endif +#endif } else { color = oklch_to_linear_srgb(float3(targetL, targetChroma, targetHue)); } diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index b581bb2e..4e079bc7 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -580,15 +580,15 @@ float3 PostProcess_Inverse( // We can't invert the color filter, so we will only inverse the post process by the unfiltered amount const float colorFilterInverse = 1.f - colorFilter.a; -#if POST_PROCESS_CONTRAST_TYPE == 0 +#if POST_PROCESS_CONTRAST_TYPE == 1 Color = ((Color - contrastMidPoint) / contrastIntensity) + contrastMidPoint; -#elif POST_PROCESS_CONTRAST_TYPE == 1 +#elif POST_PROCESS_CONTRAST_TYPE == 2 Color = pow(Color / contrastMidPoint, 1.f / contrastIntensity) * contrastMidPoint; -#elif POST_PROCESS_CONTRAST_TYPE == 2 || POST_PROCESS_CONTRAST_TYPE == 3 +#elif POST_PROCESS_CONTRAST_TYPE == 3 //TODO: implement diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 886c74c0..c5547108 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -19,12 +19,12 @@ // This implies there's a mismatch baked in the output colors, as they were using a ~sRGB similar formula, which would then be interpreted by screens as 2.2 gamma. // This makes sense to use given we fix up (normalize) the LUTs colors and their gamma mapping. #define SDR_USE_GAMMA_2_2 1 +// Emulates the SDR look of the game on an SDR display using gamma 2.2. +// Which by our best guesses should be the creative intent. +#define EMULATE_SDR_GAMMA_APPEARANCE 1 // Makes LUTs sampling work in linear space, which is mathematically correct. Without this, they are stored as ~sRGB in a float texture and sampled in sRGB without acknowledging it. // This possibly shifts colors a lot, but it's correct, it's also necessary for HDR LUTs to work. #define LUT_FIX_GAMMA_MAPPING 1 -// Emulates the SDR look of the game on an SDR display using gamma 2.2. -// Which by our best guesses should be the creative intent. -#define EMULATE_SDR_GAMMA_APPEARANCE 1 #define LUT_SIZE 16.f #define LUT_SIZE_UINT (uint)LUT_SIZE From ddbc0e2e0bfae9c495da991e7eb1c4700f188ca9 Mon Sep 17 00:00:00 2001 From: Lilium Date: Wed, 4 Oct 2023 02:32:26 +0200 Subject: [PATCH 065/270] fix float accuracy issues for sigmoidal inspired tone mapping --- shaders/HDRComposite/HDRComposite_ps.hlsl | 34 +++++++++++++---------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 4e079bc7..c32a383e 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -489,7 +489,8 @@ float SigmoidalContrastAdjustment( } else { - Channel = 1.f - ((Channel - contrastMidPoint) * normalizationFactorUpper); + // protect against float issues + Channel = max(1.f - ((Channel - contrastMidPoint) * normalizationFactorUpper), asfloat(0x00000001)); // doing this for contrastIntensity below 1 greatly desaturates compared to not doing this // look into if contrastIntensity ever goes below 1 // and remove this check if it does not @@ -540,23 +541,26 @@ float3 PostProcess( #elif POST_PROCESS_CONTRAST_TYPE == 3 - float cIn = contrastIntensity; - // adjustment to match native better - // only do them when contrastIntensity is above 1 because 1 is neutral (no adjustment) - // below 1 it matches native nicely (though idk if contrast is ever lowered) - if (contrastIntensity > 1.f) + if (contrastIntensity != 1.f) // worth to do performance wise { - cIn = contrastIntensity * (4.f / 3.f); - } - float cMid = contrastMidPoint; + float cIn = contrastIntensity; + // adjustment to match native better + // only do them when contrastIntensity is above 1 because 1 is neutral (no adjustment) + // below 1 it matches native nicely (though idk if contrast is ever lowered) + if (contrastIntensity > 1.f) + { + cIn = contrastIntensity * (4.f / 3.f); + } + float cMid = contrastMidPoint; - // normalization factors for lower and upper power curve - float normalizationFactorLower = 1.f / cMid; - float normalizationFactorUpper = 1.f / (1.f - cMid); + // normalization factors for lower and upper power curve + float normalizationFactorLower = 1.f / cMid; + float normalizationFactorUpper = 1.f / (1.f - cMid); - Color.r = SigmoidalContrastAdjustment(Color.r, cMid, cIn, normalizationFactorLower, normalizationFactorUpper); - Color.g = SigmoidalContrastAdjustment(Color.g, cMid, cIn, normalizationFactorLower, normalizationFactorUpper); - Color.b = SigmoidalContrastAdjustment(Color.b, cMid, cIn, normalizationFactorLower, normalizationFactorUpper); + Color.r = SigmoidalContrastAdjustment(Color.r, cMid, cIn, normalizationFactorLower, normalizationFactorUpper); + Color.g = SigmoidalContrastAdjustment(Color.g, cMid, cIn, normalizationFactorLower, normalizationFactorUpper); + Color.b = SigmoidalContrastAdjustment(Color.b, cMid, cIn, normalizationFactorLower, normalizationFactorUpper); + } #endif From 0de6cd987c7c857dc024a12cee697d89c782cb8d Mon Sep 17 00:00:00 2001 From: Lilium Date: Wed, 4 Oct 2023 02:34:05 +0200 Subject: [PATCH 066/270] fix indentation errors --- .../ColorGradingMerge_FF81_cs.hlsl | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 3b1a5bbd..f87334bf 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -67,7 +67,7 @@ uint3 ThreeToTwoDimensionCoordinates(uint3 UVW) return uint3(U, UVW.y, 0); } -//TODO: try to do the LUT analysys in linear from gamma 2.2 instead of sRGB, but then convert back to linear from sRGB at the end +//TODO: try to do the LUT analysys in linear from gamma 2.2 instead of sRGB, but then convert back to linear from sRGB at the end void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) { Analysis.black = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(0u)).rgb); @@ -155,7 +155,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b { return color; } - + #if LUT_DEBUG_VALUES float3 DEBUG_COLOR = float3(1.f, 0.f, 1.f); // Magenta #endif // LUT_DEBUG_VALUES @@ -164,7 +164,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b // the values to 0. This will remove the haze and tint giving a fuller chroma // Sample and hold targetChroma const float3 reduceFactor = linearNormalization( - neutralLUTColor, + neutralLUTColor, 0.f, 1.f, 1.f / (1.f - analysis.black), @@ -175,8 +175,8 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b const float targetChroma = linear_srgb_to_oklch(detintedColor)[1]; // Adjust the value back to recreate a smooth Y gradient since 0 is floored - // Sample and hold targetL - + // Sample and hold targetL + const float3 increaseFactor = linearNormalization( neutralLUTColor, 0.f, @@ -200,7 +200,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b // This can also be represented as [-1 to 1] or [-B to +W]. // For interpolation between 0 and a maximum, it become [0, ..., W, ..., W+B] // Distances do not always add up to sqrt(3) (eg: 1,0,0) - + const float blackDistance = hypot3(neutralLUTColor); const float whiteDistance = hypot3(1.f - neutralLUTColor); const float totalRange = blackDistance + whiteDistance; @@ -212,10 +212,10 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b // Boost lightness by how much white was reduced const float raiseL = linearNormalization( - whiteDistance, - 0.f, - totalRange, - 1.f / analysis.whiteL, + whiteDistance, + 0.f, + totalRange, + 1.f / analysis.whiteL, 1.f); #if LUT_DEBUG_VALUES @@ -231,7 +231,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b //TODO: copy these define values into runtime bools #if LUT_SDR_ON_CLIP > 0 || LUT_HDR_ON_CLIP > 0 if (targetL >= 1.f) { -#if LUT_SDR_ON_CLIP > 0 && LUT_SDR_ON_CLIP != LUT_HDR_ON_CLIP +#if LUT_SDR_ON_CLIP > 0 && LUT_SDR_ON_CLIP != LUT_HDR_ON_CLIP if (SDRRange) { #endif @@ -246,7 +246,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b color = oklch_to_linear_srgb(float3(1.f, targetChroma, targetHue)); #endif -#if LUT_SDR_ON_CLIP > 0 && LUT_SDR_ON_CLIP != LUT_HDR_ON_CLIP +#if LUT_SDR_ON_CLIP > 0 && LUT_SDR_ON_CLIP != LUT_HDR_ON_CLIP } #if LUT_HDR_ON_CLIP > 0 else { From e2df6cfd72399159eb3fb1d3e801b18e79c662f6 Mon Sep 17 00:00:00 2001 From: Lilium Date: Wed, 4 Oct 2023 09:13:36 +0200 Subject: [PATCH 067/270] add better comments for the sigmoidal inspired contrast adjustment --- shaders/HDRComposite/HDRComposite_ps.hlsl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index c32a383e..c6a2b267 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -464,8 +464,8 @@ void Log2Adjust(inout float Channel) } // sigmoidal inspired contrast adjustment using 2 power curves -// normalizes both lower and upper part which is divided by contrastMidPoint -// and then applies a power curve based off of contrastIntensity +// normalizes both lower and upper part which are divided by contrastMidPoint +// and then applies a power curve based off of contrastIntensity on each part float SigmoidalContrastAdjustment( float Channel, float contrastMidPoint, @@ -544,7 +544,7 @@ float3 PostProcess( if (contrastIntensity != 1.f) // worth to do performance wise { float cIn = contrastIntensity; - // adjustment to match native better + // adjustment to match native better and make the curve be an S curve (too low intensity makes it a double S curve) // only do them when contrastIntensity is above 1 because 1 is neutral (no adjustment) // below 1 it matches native nicely (though idk if contrast is ever lowered) if (contrastIntensity > 1.f) From d2d6caead151699a9590cfcfabec3cebf1da4eb1 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Thu, 5 Oct 2023 01:50:54 +0300 Subject: [PATCH 068/270] Clean up LUTs code and add a setting to make it linearize from gamma 2.2 instead of sRGB, given it's likely what they designed the game for (it's converted back to sRGB at the end). --- .../ColorGradingMerge_FF81_cs.hlsl | 105 ++++++++---------- shaders/color.hlsl | 4 +- 2 files changed, 46 insertions(+), 63 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index f87334bf..5e64755b 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -9,14 +9,28 @@ #define OPTIMIZE_LUT_ANALYSIS true // For future development #define UNUSED_PARAMS 0 -#define LUT_DEBUG_VALUES true +#define LUT_DEBUG_VALUES false + +#if SDR_USE_GAMMA_2_2 // Make sure LUTs are normalized by linearizing them with gamma 2.2, so they work with values closer to the expected ones. Their mixed output is still kept in sRGB. + #define LINEARIZE(x) pow(x, 2.2f) + #define CORRECT_GAMMA(x) gamma_linear_to_sRGB(pow(x, 1.f / 2.2f)) +#else + #define LINEARIZE(x) gamma_sRGB_to_linear(x) + #define CORRECT_GAMMA(x) x +#endif -// Enum: {0: AS_IS, 1: ORIGINAL, 2:WHITE, 3: MAX_HUE, 4: CLAMP } -#define LUT_SDR_ON_CLIP 1 -#define LUT_HDR_ON_CLIP 0 +// Behaviour when the color luminance goes beyond 1 +// -0: As is, keep the normalized value that could be beyond the 0-1 +// -1: Keep orignal (non normalized) color +// -2: "Clip" to full white +// -3: Max Hue +// -4: Clamp +#define LUT_SDR_ON_CLIP 1u +#define LUT_HDR_ON_CLIP 0u static float AdditionalNeutralLUTPercentage = 0.f; // ~0.25 might be a good compromise static float LUTCorrectionPercentage = 1.f; +static float LUTSaturation = 1.f; cbuffer CPushConstantWrapper_ColorGradingMerge : register(b0, space0) { @@ -70,9 +84,9 @@ uint3 ThreeToTwoDimensionCoordinates(uint3 UVW) //TODO: try to do the LUT analysys in linear from gamma 2.2 instead of sRGB, but then convert back to linear from sRGB at the end void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) { - Analysis.black = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(0u)).rgb); + Analysis.black = LINEARIZE(LUT.Load(ThreeToTwoDimensionCoordinates(0u)).rgb); Analysis.blackY = Luminance(Analysis.black); - Analysis.white = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(LUT_SIZE_UINT - 1u)).rgb); + Analysis.white = LINEARIZE(LUT.Load(ThreeToTwoDimensionCoordinates(LUT_SIZE_UINT - 1u)).rgb); Analysis.whiteY = Luminance(Analysis.white); Analysis.whiteL = linear_srgb_to_oklab(Analysis.white)[0]; #if UNUSED_PARAMS @@ -101,7 +115,7 @@ void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) { for (uint z = 0; z < LUT_SIZE_UINT; z += analyzeTexelFrequency) { - float3 LUTColor = gamma_sRGB_to_linear(LUT.Load(ThreeToTwoDimensionCoordinates(uint3(x, y, z))).rgb); + float3 LUTColor = LINEARIZE(LUT.Load(ThreeToTwoDimensionCoordinates(uint3(x, y, z))).rgb); minColor = min(minColor, LUTColor); maxColor = max(maxColor, LUTColor); @@ -147,7 +161,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b LUTAnalysis analysis; AnalyzeLUT(LUT, analysis); - float3 color = gamma_sRGB_to_linear(LUT.Load(UVW)); + float3 color = LINEARIZE(LUT.Load(UVW)); const float3 originalColor = color; if (analysis.whiteY < analysis.blackY // If LUT is inversed (eg: photo negative) don't do anything @@ -172,7 +186,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b const float3 detintedColor = max(0.f, 1.f-((1.f - color) * reduceFactor)); - const float targetChroma = linear_srgb_to_oklch(detintedColor)[1]; + const float targetChroma = linear_srgb_to_oklch(detintedColor)[1] * LUTSaturation; // Adjust the value back to recreate a smooth Y gradient since 0 is floored // Sample and hold targetL @@ -206,7 +220,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b const float totalRange = blackDistance + whiteDistance; #if LUT_DEBUG_VALUES - // whiteY most always be > 0 + // whiteY most always be > 0 if (analysis.whiteL <= 0) return DEBUG_COLOR; #endif // LUT_DEBUG_VALUES @@ -228,56 +242,23 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b // Use hue from LUT cube color const float targetHue = linear_srgb_to_oklch(color)[2]; -//TODO: copy these define values into runtime bools -#if LUT_SDR_ON_CLIP > 0 || LUT_HDR_ON_CLIP > 0 if (targetL >= 1.f) { -#if LUT_SDR_ON_CLIP > 0 && LUT_SDR_ON_CLIP != LUT_HDR_ON_CLIP - if (SDRRange) { -#endif - -#if LUT_SDR_ON_CLIP == 1 - // Original (do nothing) -#elif LUT_SDR_ON_CLIP == 2 - // White - color = white; -#elif LUT_SDR_ON_CLIP == 3 - // TODO -#elif LUT_SDR_ON_CLIP == 4 - color = oklch_to_linear_srgb(float3(1.f, targetChroma, targetHue)); -#endif - -#if LUT_SDR_ON_CLIP > 0 && LUT_SDR_ON_CLIP != LUT_HDR_ON_CLIP - } -#if LUT_HDR_ON_CLIP > 0 - else { -#endif -#endif - -#if LUT_SDR_ON_CLIP == 0 && LUT_HDR_ON_CLIP > 0 - if (!SDRRange) { -#endif - -#if LUT_HDR_ON_CLIP != LUT__SDR_ON_CLIP -#if LUT_HDR_ON_CLIP == 1 - // Original (do nothing) -#elif LUT_HDR_ON_CLIP == 2 - // White - color = white; -#elif LUT_HDR_ON_CLIP == 3 - // TODO -#elif LUT_HDR_ON_CLIP == 4 - color = oklch_to_linear_srgb(float3(1.f, targetChroma, targetHue)); -#endif -#endif -#if LUT_HDR_ON_CLIP > 0 && LUT_SDR_ON_CLIP != LUT_HDR_ON_CLIP + const uint clipBehavior = SDRRange ? LUT_SDR_ON_CLIP : LUT_HDR_ON_CLIP; + if (clipBehavior == 0) // As is (treat it the same as any other color) + color = oklch_to_linear_srgb(float3(targetL, targetChroma, targetHue)); + else if (clipBehavior == 1) { } // Keep original color (do nothing) + else if (clipBehavior == 2) // White + color = 1.f; + else if (clipBehavior == 3) // Max Hue + { + // TODO: can we even do anything about this? if the lightness is >= 1, then the color is white? } -#endif - } else { - color = oklch_to_linear_srgb(float3(targetL, targetChroma, targetHue)); + else if (clipBehavior == 4) // Clamp + color = oklch_to_linear_srgb(float3(1.f, targetChroma, targetHue)); // TODO: isn't this the same as the "White" setting? } -#elif + else { color = oklch_to_linear_srgb(float3(targetL, targetChroma, targetHue)); -#endif + } // Optional step to keep colors in the SDR range. if (SDRRange) { @@ -297,7 +278,7 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) const uint3 outUVW = SV_DispatchThreadID; float3 neutralLUTColor = float3(outUVW) / (LUT_SIZE - 1.f); // The neutral LUT is automatically generated by the coordinates, but it's baked with sRGB gamma - neutralLUTColor = gamma_sRGB_to_linear(neutralLUTColor); + neutralLUTColor = LINEARIZE(neutralLUTColor); #if LUT_IMPROVEMENT_TYPE != 1 @@ -305,10 +286,10 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) float3 LUT2Color = LUT2.Load(inUVW); float3 LUT3Color = LUT3.Load(inUVW); float3 LUT4Color = LUT4.Load(inUVW); - LUT1Color = gamma_sRGB_to_linear(LUT1Color); - LUT2Color = gamma_sRGB_to_linear(LUT2Color); - LUT3Color = gamma_sRGB_to_linear(LUT3Color); - LUT4Color = gamma_sRGB_to_linear(LUT4Color); + LUT1Color = LINEARIZE(LUT1Color); + LUT2Color = LINEARIZE(LUT2Color); + LUT3Color = LINEARIZE(LUT3Color); + LUT4Color = LINEARIZE(LUT4Color); #endif // LUT_IMPROVEMENT_TYPE @@ -372,6 +353,8 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) + (adjustedLUT2Percentage * LUT2Color) + (adjustedLUT3Percentage * LUT3Color) + (adjustedLUT4Percentage * LUT4Color); + + mixedLUT = CORRECT_GAMMA(mixedLUT); // Convert to sRGB after blending between LUTs, so the blends are done in linear space, which gives more consistent and correct results #if !LUT_FIX_GAMMA_MAPPING diff --git a/shaders/color.hlsl b/shaders/color.hlsl index 40e988f2..e2d94e16 100644 --- a/shaders/color.hlsl +++ b/shaders/color.hlsl @@ -92,7 +92,7 @@ float3 saturation(float3 color, float saturation) return lerp(luminance, color, saturation); } - +// sRGB/Rec.709 float3 linear_srgb_to_oklab(float3 rgb) { float l = (0.4122214708f * rgb.r) + (0.5363325363f * rgb.g) + (0.0514459929f * rgb.b); float m = (0.2119034982f * rgb.r) + (0.6806995451f * rgb.g) + (0.1073969566f * rgb.b); @@ -109,7 +109,7 @@ float3 linear_srgb_to_oklab(float3 rgb) { ); } - +// sRGB/Rec.709 float3 oklab_to_linear_srgb(float3 lab) { float L = lab[0]; float a = lab[1]; From d4914dda7443661a57784413a5099f1f24cdb49d Mon Sep 17 00:00:00 2001 From: Filoppi Date: Thu, 5 Oct 2023 01:51:28 +0300 Subject: [PATCH 069/270] Tonemapper stuff --- shaders/HDRComposite/HDRComposite_ps.hlsl | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index c6a2b267..2a8e08b5 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -19,7 +19,7 @@ // 2 improved (looks more natural, avoids values below 0, but will overshoot beyond 1 more often, and will raise blacks) // 3 Sigmoidal inspired and biases contrast increases towards the lower and top end // (optimisation left if contrastIntensity doesn't go below 1) -#define POST_PROCESS_CONTRAST_TYPE 3 +#define POST_PROCESS_CONTRAST_TYPE 2 #define ENABLE_LUT 1 // LUTs are too low resolutions to resolve gradients smoothly if the LUT color suddenly changes between samples #define ENABLE_LUT_TETRAHEDRAL_INTERPOLATION 1 @@ -96,8 +96,8 @@ struct PSOutput // exp2(x * 1.44269502162933349609375f) is the same as exp(x) -static const float TonemapStrength = 1.f; //TODO: implement -static const float PostProcessStrength = 1.f; //TODO: implement +static const float SDRTonemapStrength = 1.f; +static const float PostProcessStrength = 1.f; // Similar to "AdditionalNeutralLUTPercentage" in the LUT shader, though this is more precise as it skips the precision loss induced by a neutral LUT static const float GradingLUTStrength = 1.f; // 0 Ignored, 1 ACES Reference, 2 ACES Custom, 3 Hable, 4+ Disable tonemapper @@ -784,6 +784,7 @@ float3 RestorePostProcess(float3 inverseTonemappedColor, float3 postProcessColor // for example if any zero was shifted to a more raised color, "postProcessColorRatio" would not be able to replicate that shift. return lerp(postProcessedOffsetColor, postProcessedRatioColor, saturate(tonemappedColor / MaxShadowsColor)); //TODO: test and improve. //return select(tonemappedColor == 0, postProcessedOffsetColor, postProcessedRatioColor); //TODO: improve. This causes flickering sometimes. + //return inverseTonemappedColor; //TODO: improve. This causes flickering sometimes. } PSOutput PS(PSInput psInput) @@ -848,7 +849,7 @@ PSOutput PS(PSInput psInput) #if ENABLE_POST_PROCESS float prePostProcessColorLuminance; - float3 tonemappedPostProcessedColor = PostProcess(tonemappedColor, prePostProcessColorLuminance); + float3 tonemappedPostProcessedColor = lerp(tonemappedColor, PostProcess(tonemappedColor, prePostProcessColorLuminance), PostProcessStrength); #else float3 tonemappedPostProcessedColor = tonemappedColor; // No need to do anything (not even a saturate here) #endif //ENABLE_POST_PROCESS @@ -936,7 +937,7 @@ PSOutput PS(PSInput psInput) // Scale all non highlights by the scale the smallest (first) highlight would have, so we keep the curves connected if (onlyInvertHighlights && inverseTonemappedColor[channel] < minHighlightsColorOut) { - inverseTonemappedColor[channel] = tonemappedColor[channel] * (minHighlightsColorOut / minHighlightsColorIn); + inverseTonemappedColor[channel] = lerp(inverseTonemappedColor[channel], tonemappedColor[channel] * (minHighlightsColorOut / minHighlightsColorIn), SDRTonemapStrength); } // Restore any highlight clipped or just crushed by the direct tonemappers (Hable does that) if (inverseTonemappedColor[channel] >= minHighlightsColorOut) @@ -948,6 +949,10 @@ PSOutput PS(PSInput psInput) { midGrayOut = midGrayIn * (minHighlightsColorOut / minHighlightsColorIn); } + if (SDRTonemapStrength != 1.f) + { + + } const float midGrayScale = midGrayOut / midGrayIn; From 30d6775887ccd4ae42c999a47495d4e7713ce74d Mon Sep 17 00:00:00 2001 From: Filoppi Date: Thu, 5 Oct 2023 03:21:00 +0300 Subject: [PATCH 070/270] Tonemappers and LUTs polish (done or removed all TODOs) --- .../ColorGradingMerge_FF81_cs.hlsl | 15 ++++--- shaders/HDRComposite/HDRComposite_ps.hlsl | 45 ++++++++++--------- shaders/shared.hlsl | 6 +-- 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 5e64755b..7eaf1fd4 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -11,9 +11,11 @@ #define UNUSED_PARAMS 0 #define LUT_DEBUG_VALUES false -#if SDR_USE_GAMMA_2_2 // Make sure LUTs are normalized by linearizing them with gamma 2.2, so they work with values closer to the expected ones. Their mixed output is still kept in sRGB. +// Make sure LUTs are normalized by linearizing them with gamma 2.2, so they work with values closer to the expected ones. Their mixed output is still kept in sRGB. +// There's a chance that LUTs had correctly been made on sRGB gamma monitors, so in that case this would be wrong (LUTs were likely made with external tools). +#if SDR_USE_GAMMA_2_2 && 0 // Disabled as this does indeed look too bright. #define LINEARIZE(x) pow(x, 2.2f) - #define CORRECT_GAMMA(x) gamma_linear_to_sRGB(pow(x, 1.f / 2.2f)) + #define CORRECT_GAMMA(x) gamma_sRGB_to_linear(pow(x, 1.f / 2.2f)) #else #define LINEARIZE(x) gamma_sRGB_to_linear(x) #define CORRECT_GAMMA(x) x @@ -30,7 +32,7 @@ static float AdditionalNeutralLUTPercentage = 0.f; // ~0.25 might be a good compromise static float LUTCorrectionPercentage = 1.f; -static float LUTSaturation = 1.f; +static float LUTAdditionalSaturation = 0.f; // Between 0 and 1 cbuffer CPushConstantWrapper_ColorGradingMerge : register(b0, space0) { @@ -81,7 +83,6 @@ uint3 ThreeToTwoDimensionCoordinates(uint3 UVW) return uint3(U, UVW.y, 0); } -//TODO: try to do the LUT analysys in linear from gamma 2.2 instead of sRGB, but then convert back to linear from sRGB at the end void AnalyzeLUT(Texture2D LUT, inout LUTAnalysis Analysis) { Analysis.black = LINEARIZE(LUT.Load(ThreeToTwoDimensionCoordinates(0u)).rgb); @@ -186,7 +187,8 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b const float3 detintedColor = max(0.f, 1.f-((1.f - color) * reduceFactor)); - const float targetChroma = linear_srgb_to_oklch(detintedColor)[1] * LUTSaturation; + //TODO: test if "LUTAdditionalSaturation" works without a saturate() + const float targetChroma = linear_srgb_to_oklch(detintedColor)[1] + LUTAdditionalSaturation; // Adjust the value back to recreate a smooth Y gradient since 0 is floored // Sample and hold targetL @@ -353,7 +355,8 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) + (adjustedLUT2Percentage * LUT2Color) + (adjustedLUT3Percentage * LUT3Color) + (adjustedLUT4Percentage * LUT4Color); - + + // Note: this might not work great if the LUT has colors beyond the SDR 0-1 range mixedLUT = CORRECT_GAMMA(mixedLUT); // Convert to sRGB after blending between LUTs, so the blends are done in linear space, which gives more consistent and correct results diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 2a8e08b5..d94bc690 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -15,7 +15,7 @@ #define ENABLE_TONEMAP 1 #define ENABLE_POST_PROCESS 1 // 0 disable contrast adjustment -// 1 original (weak, generates values beyond 0-1 which then get clipped) +// 1 original (weak, generates values beyond 0-1 which then might get clipped) // 2 improved (looks more natural, avoids values below 0, but will overshoot beyond 1 more often, and will raise blacks) // 3 Sigmoidal inspired and biases contrast increases towards the lower and top end // (optimisation left if contrastIntensity doesn't go below 1) @@ -24,7 +24,9 @@ // LUTs are too low resolutions to resolve gradients smoothly if the LUT color suddenly changes between samples #define ENABLE_LUT_TETRAHEDRAL_INTERPOLATION 1 #define ENABLE_REPLACED_TONEMAP 1 -// Only invert highlights, which helps conserve the SDR filmic look (shadow crush) +// Only invert highlights, which helps conserve the SDR filmic look (shadow crush) and altered colors. +// The alternative is to keep the linear space image tonemapped by the lightweight DICE tonemapper, +// or to replicate the SDR tonemapper by luminance, though both would alter the look too much and break some scenes. #define INVERT_TONEMAP_HIGHLIGHTS_ONLY 1 #define CLAMP_INPUT_OUTPUT 1 @@ -96,7 +98,7 @@ struct PSOutput // exp2(x * 1.44269502162933349609375f) is the same as exp(x) -static const float SDRTonemapStrength = 1.f; +static const float SDRTonemapHDRStrength = 1.f; static const float PostProcessStrength = 1.f; // Similar to "AdditionalNeutralLUTPercentage" in the LUT shader, though this is more precise as it skips the precision loss induced by a neutral LUT static const float GradingLUTStrength = 1.f; @@ -420,19 +422,19 @@ T Hable_Inverse( return (T)itmColor * hableParams.dstParams.W; } -// Tonemapper inspired from DICE. Works on luminance to maintain hue, not per channel. +// Tonemapper inspired from DICE. Can work by luminance to maintain hue. // "HighlightsShoulderStart" should be between 0 and 1. Determines where the highlights curve (shoulder) starts. Leaving at zero for now as it's a simple and good looking default. float3 DICETonemap( float3 Color, float MaxOutputLuminance, float HighlightsShoulderStart = 0.f) { -#if 1 // Do by channel for now, to match all other tonemappers, especially when "INVERT_TONEMAP_HIGHLIGHTS_ONLY" is on +#if 0 // Even if all SDR tonemappers work by channel, in HDR we prefer to maintain the hue and to match the screen peak brightness in absolute (luminance) nits, not as channel values. Color.r = luminanceCompress(Color.r, MaxOutputLuminance, HighlightsShoulderStart); Color.g = luminanceCompress(Color.g, MaxOutputLuminance, HighlightsShoulderStart); Color.b = luminanceCompress(Color.b, MaxOutputLuminance, HighlightsShoulderStart); return Color; -#else //TODO: try "per channel" option given all other tm is per channel. If not, restore the "luminance based" inverse tonemapping, to keep the SDR colors more. +#else const float sourceLuminance = Luminance(Color); if (sourceLuminance > 0.0f) { @@ -781,10 +783,12 @@ float3 RestorePostProcess(float3 inverseTonemappedColor, float3 postProcessColor postProcessedRatioColor *= postProcessColorRatio; postProcessedOffsetColor += postProcessColorOffset * midGrayScale; // Near black, we prefer using the "offset" (sum) pp restoration method, as otherwise any raised black would not work, - // for example if any zero was shifted to a more raised color, "postProcessColorRatio" would not be able to replicate that shift. - return lerp(postProcessedOffsetColor, postProcessedRatioColor, saturate(tonemappedColor / MaxShadowsColor)); //TODO: test and improve. - //return select(tonemappedColor == 0, postProcessedOffsetColor, postProcessedRatioColor); //TODO: improve. This causes flickering sometimes. - //return inverseTonemappedColor; //TODO: improve. This causes flickering sometimes. + // for example if any zero was shifted to a more raised color, "postProcessColorRatio" would not be able to replicate that shift due to a division by zero. +#if 1 // Note: in case "INVERT_TONEMAP_HIGHLIGHTS_ONLY" was false, we might want to test the "postProcessedOffsetColor" blend in range more carefully + return lerp(postProcessedOffsetColor, postProcessedRatioColor, saturate(tonemappedColor / MaxShadowsColor)); +#else // Doing the branching this way might not be so good, as near black colors could still end up with crazy value due to divisions between tiny values + return select(tonemappedColor == 0, postProcessedOffsetColor, postProcessedRatioColor); +#endif } PSOutput PS(PSInput psInput) @@ -864,8 +868,8 @@ PSOutput PS(PSInput psInput) tonemappedPostProcessedGradedColor = GradingLUT(tonemappedPostProcessedColor, psInput.TEXCOORD); #endif // ENABLE_LUT -#if EMULATE_SDR_GAMMA_APPEARANCE // Do this even if "ENABLE_LUT" is false, for consistency -#if FIX_WRONG_SRGB_GAMMA_FORMULA && 0 //TODO: Disabled as this looks awful, probably because the Bethesda optimize sRGB function doesn't even stay in the 0-1 range +// Do these even if "ENABLE_LUT" is false, for consistency +#if FIX_WRONG_SRGB_GAMMA_FORMULA && 0 // Disabled as this looks awful, probably because the Bethesda optimized sRGB function doesn't even stay in the 0-1 range so there's no way to recover from it // We fixed the LUT input gamma mapping formula so that LUTs apply correctly, technically speaking. Now we compensate for the adjustment. tonemappedPostProcessedGradedColor = gamma_sRGB_to_linear_Bethesda_Optimized(gamma_linear_to_sRGB(tonemappedPostProcessedGradedColor)); #endif @@ -873,7 +877,6 @@ PSOutput PS(PSInput psInput) // This error was always built in the image if we assume Bethesda calibrated the game on gamma 2.2 displays tonemappedPostProcessedGradedColor = pow(gamma_linear_to_sRGB(tonemappedPostProcessedGradedColor), 2.2f); #endif -#endif // EMULATE_SDR_GAMMA_APPEARANCE #endif // APPLY_MERGED_COLOR_GRADING_LUT @@ -890,7 +893,7 @@ PSOutput PS(PSInput psInput) const float paperWhite = HDR_GAME_PAPER_WHITE; - const float midGrayIn = MidGray; //TODO: try to increase this to ~0.21? + const float midGrayIn = MidGray; float midGrayOut = midGrayIn; float3 inverseTonemappedColor = tonemappedColor; @@ -930,14 +933,12 @@ PSOutput PS(PSInput psInput) break; } - //TODO: improve inverse tonemapped look. Are mid tones too "raised"? - for (uint channel = 0; channel < 3; channel++) { // Scale all non highlights by the scale the smallest (first) highlight would have, so we keep the curves connected if (onlyInvertHighlights && inverseTonemappedColor[channel] < minHighlightsColorOut) { - inverseTonemappedColor[channel] = lerp(inverseTonemappedColor[channel], tonemappedColor[channel] * (minHighlightsColorOut / minHighlightsColorIn), SDRTonemapStrength); + inverseTonemappedColor[channel] = tonemappedColor[channel] * (minHighlightsColorOut / minHighlightsColorIn); } // Restore any highlight clipped or just crushed by the direct tonemappers (Hable does that) if (inverseTonemappedColor[channel] >= minHighlightsColorOut) @@ -949,15 +950,19 @@ PSOutput PS(PSInput psInput) { midGrayOut = midGrayIn * (minHighlightsColorOut / minHighlightsColorIn); } - if (SDRTonemapStrength != 1.f) + if (onlyInvertHighlights && SDRTonemapHDRStrength != 1.f) { - + inverseTonemappedColor = lerp(inputColor, inverseTonemappedColor, SDRTonemapHDRStrength); + midGrayOut = lerp(midGrayIn, midGrayOut, SDRTonemapHDRStrength); + minHighlightsColorOut = lerp(minHighlightsColorIn, minHighlightsColorOut, SDRTonemapHDRStrength); } const float midGrayScale = midGrayOut / midGrayIn; float3 inverseTonemappedPostProcessedColor = RestorePostProcess(inverseTonemappedColor, postProcessColorRatio, postProcessColorOffset, tonemappedColor, midGrayScale); - //minHighlightsColorOut = RestorePostProcess(minHighlightsColorOut, postProcessColorRatio, postProcessColorOffset, tonemappedColor, midGrayScale); //TODO: should we even do this? +#if 0 // Enable this if you want the highlights should start to be affected by post processing. It doesn't seem like the right thing to do and having it off works just fine. + minHighlightsColorOut = RestorePostProcess(minHighlightsColorOut, postProcessColorRatio, postProcessColorOffset, tonemappedColor, midGrayScale); +#endif // Bring back the color to the same range as SDR by dividing by the mid gray change. inverseTonemappedPostProcessedColor *= paperWhite / midGrayScale; diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index c5547108..755f2a72 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -16,12 +16,10 @@ #define HDR_UI_PAPER_WHITE HDR_REFERENCE_PAPER_WHITE * HDR_UI_PAPER_WHITE_MULTIPLIER // If this is true, the code makes the assumption that Bethesda developed and calibrated the game on gamma 2.2 screens, as opposed to sRGB gamma. -// This implies there's a mismatch baked in the output colors, as they were using a ~sRGB similar formula, which would then be interpreted by screens as 2.2 gamma. +// This implies there was a mismatch baked in the output colors, as they were using a ~sRGB similar formula, which would then be interpreted by screens as 2.2 gamma. +// By turning this on, we emulate the SDR look in HDR by baking that assumption into our calculations. // This makes sense to use given we fix up (normalize) the LUTs colors and their gamma mapping. #define SDR_USE_GAMMA_2_2 1 -// Emulates the SDR look of the game on an SDR display using gamma 2.2. -// Which by our best guesses should be the creative intent. -#define EMULATE_SDR_GAMMA_APPEARANCE 1 // Makes LUTs sampling work in linear space, which is mathematically correct. Without this, they are stored as ~sRGB in a float texture and sampled in sRGB without acknowledging it. // This possibly shifts colors a lot, but it's correct, it's also necessary for HDR LUTs to work. From 1dfd6888ee2232a1873b08d4b632fddf98f3cd06 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Thu, 5 Oct 2023 03:21:13 +0300 Subject: [PATCH 071/270] Documentation on bink videos shader --- shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl b/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl index 0ffa1e5c..4427a4e4 100644 --- a/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl +++ b/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl @@ -14,11 +14,23 @@ struct PSInputs float4 PS(PSInputs inputs) : SV_Target { - float Y = _8.Sample(_13, inputs.TEXCOORD.xy).x * 1.16412353515625f; + float Y = _8.Sample(_13, inputs.TEXCOORD.xy).x; float Cb = _9.Sample(_13, inputs.TEXCOORD.xy).x; float Cr = _10.Sample(_13, inputs.TEXCOORD.xy).x; float3 color; - //TODO: fix matrices??? Could these videos be meant for both SDR and HDR? +#if 0 // Doesn't work, the videos were probably not in YCbCr + // Reference: https://gist.github.com/yohhoy/dafa5a47dade85d8b40625261af3776a + static const float a = 0.2126f; + static const float b = 0.7152f; + static const float c = 0.0722f; + static const float d = 1.8556f; + static const float e = 1.5748f; + color.r = Y + (Cr * e); + color.g = Y - (Cb * (c * d / b)) - (Cr * (a * e / b)); + color.b = Y + (Cb * d); +#else // Original formula, it doesn't seem to make much sense but it works + //TODO: could it be that by treating Y in a different way, we could extract HDR luminance from this video? Probably not. + Y *= 1.16412353515625f; color.x = (Y - 0.870655059814453125f) + (Cb * 1.595794677734375f); color.y = ((Y + 0.529705047607421875f) - (Cb * 0.8134765625f)) - (Cr * 0.391448974609375f); color.z = (Y - 1.081668853759765625f) + (Cr * 2.017822265625f); @@ -27,6 +39,7 @@ float4 PS(PSInputs inputs) : SV_Target // this breaks the UI pass if we are using R16G16B16A16F textures, // as UI blending produces invalid pixels if it's blended with an invalid color. color = max(color, 0.f); +#endif #if ENABLE_HDR #if SDR_USE_GAMMA_2_2 From 5cca2a744491c98e2be15dd0a0ef24291946314a Mon Sep 17 00:00:00 2001 From: Filoppi Date: Fri, 6 Oct 2023 02:13:32 +0300 Subject: [PATCH 072/270] Polish and add new user parameters --- shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl | 5 ++--- shaders/HDRComposite/HDRComposite_ps.hlsl | 9 ++++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl index 7eaf1fd4..1ff61179 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl @@ -32,7 +32,7 @@ static float AdditionalNeutralLUTPercentage = 0.f; // ~0.25 might be a good compromise static float LUTCorrectionPercentage = 1.f; -static float LUTAdditionalSaturation = 0.f; // Between 0 and 1 +static float LUTSaturationMultiplier = 1.f; // 1 is neutral cbuffer CPushConstantWrapper_ColorGradingMerge : register(b0, space0) { @@ -187,8 +187,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b const float3 detintedColor = max(0.f, 1.f-((1.f - color) * reduceFactor)); - //TODO: test if "LUTAdditionalSaturation" works without a saturate() - const float targetChroma = linear_srgb_to_oklch(detintedColor)[1] + LUTAdditionalSaturation; + const float targetChroma = linear_srgb_to_oklch(detintedColor)[1] * LUTSaturationMultiplier; // Adjust the value back to recreate a smooth Y gradient since 0 is floored // Sample and hold targetL diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index d94bc690..83b23b3e 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -102,6 +102,7 @@ static const float SDRTonemapHDRStrength = 1.f; static const float PostProcessStrength = 1.f; // Similar to "AdditionalNeutralLUTPercentage" in the LUT shader, though this is more precise as it skips the precision loss induced by a neutral LUT static const float GradingLUTStrength = 1.f; +static const float SecondaryContrast = 1.f; // 0 Ignored, 1 ACES Reference, 2 ACES Custom, 3 Hable, 4+ Disable tonemapper static const uint ForceTonemapper = 0; @@ -946,14 +947,17 @@ PSOutput PS(PSInput psInput) inverseTonemappedColor[channel] = inputColor[channel]; } } + const float previousMidGrayOut = midGrayOut; + // If we only inverted highlights, the mid gray in and out should have the same value, so we need to base it on the highlights in/out change if (onlyInvertHighlights && midGrayOut < minHighlightsColorOut) // This is probably always true { midGrayOut = midGrayIn * (minHighlightsColorOut / minHighlightsColorIn); } + // Shift back to the original linear color if we want to ignore the SDR tonemapper if (onlyInvertHighlights && SDRTonemapHDRStrength != 1.f) { inverseTonemappedColor = lerp(inputColor, inverseTonemappedColor, SDRTonemapHDRStrength); - midGrayOut = lerp(midGrayIn, midGrayOut, SDRTonemapHDRStrength); + midGrayOut = lerp(previousMidGrayOut, midGrayOut, SDRTonemapHDRStrength); minHighlightsColorOut = lerp(minHighlightsColorIn, minHighlightsColorOut, SDRTonemapHDRStrength); } @@ -963,6 +967,9 @@ PSOutput PS(PSInput psInput) #if 0 // Enable this if you want the highlights should start to be affected by post processing. It doesn't seem like the right thing to do and having it off works just fine. minHighlightsColorOut = RestorePostProcess(minHighlightsColorOut, postProcessColorRatio, postProcessColorOffset, tonemappedColor, midGrayScale); #endif + + // Secondary user driven contrast + inverseTonemappedPostProcessedColor = pow(inverseTonemappedPostProcessedColor / (MidGray * midGrayScale), SecondaryContrast) * (MidGray * midGrayScale); // Bring back the color to the same range as SDR by dividing by the mid gray change. inverseTonemappedPostProcessedColor *= paperWhite / midGrayScale; From f2f9cbb0b5434fd3a578388d1a71b916b6f965da Mon Sep 17 00:00:00 2001 From: Nukem Date: Wed, 4 Oct 2023 20:48:27 -0400 Subject: [PATCH 073/270] Use root signatures dumped with a custom tool since dxa.exe ignores critical attributes --- shaders/BinkMovie/RootSignature.hlsl | 5 ++- shaders/ColorGradingMerge/RootSignature.hlsl | 34 ++++++++------- .../RootSignature.hlsl | 42 +++++++++--------- shaders/FilmGrain/RootSignature.hlsl | 7 +-- shaders/HDRComposite/RootSignature.hlsl | 43 ++++++++++--------- shaders/PostSharpen/RootSignature.hlsl | 39 +++++++++-------- shaders/ScaleformComposite/RootSignature.hlsl | 9 ++-- 7 files changed, 94 insertions(+), 85 deletions(-) diff --git a/shaders/BinkMovie/RootSignature.hlsl b/shaders/BinkMovie/RootSignature.hlsl index 1ecbb15e..62c31c75 100644 --- a/shaders/BinkMovie/RootSignature.hlsl +++ b/shaders/BinkMovie/RootSignature.hlsl @@ -1,9 +1,10 @@ #define ShaderRootSignature \ "RootFlags(ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT), " \ - "DescriptorTable(" \ + \ + "DescriptorTable( " \ "SRV(t0, space=8), " \ "SRV(t1, space=8), " \ "SRV(t2, space=8) " \ "), " \ \ - "StaticSampler(s0, space=8) " \ No newline at end of file + "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " \ No newline at end of file diff --git a/shaders/ColorGradingMerge/RootSignature.hlsl b/shaders/ColorGradingMerge/RootSignature.hlsl index d6e7a12b..67e60570 100644 --- a/shaders/ColorGradingMerge/RootSignature.hlsl +++ b/shaders/ColorGradingMerge/RootSignature.hlsl @@ -1,10 +1,12 @@ #define ShaderRootSignature \ + "RootFlags(0), " \ + \ "RootConstants(num32BitConstants=5, b0), " \ \ - "DescriptorTable(" \ + "DescriptorTable( " \ "SRV(t0, space=6), " \ "SRV(t1, space=6), " \ - "SRV(t2, space=6), " \ + "SRV(t2, space=6, flags=DATA_VOLATILE), " \ "SRV(t3, space=6), " \ "SRV(t4, space=6), " \ "SRV(t5, space=6), " \ @@ -34,16 +36,16 @@ "SRV(t28, space=6) " \ "), " \ \ - "DescriptorTable(" \ - "Sampler(s0, space=6, numDescriptors=10), " \ - "Sampler(s10, space=6, numDescriptors=2) " \ + "DescriptorTable( " \ + "Sampler(s0, numDescriptors=10, space=6), " \ + "Sampler(s10, numDescriptors=2, space=6) " \ "), " \ \ "SRV(t6, space=6), " \ "CBV(b0, space=6), " \ "CBV(b1, space=6), " \ \ - "DescriptorTable(" \ + "DescriptorTable( " \ "SRV(t0, space=8), " \ "SRV(t1, space=8), " \ "SRV(t2, space=8), " \ @@ -51,13 +53,13 @@ "UAV(u0, space=8) " \ "), " \ \ - "StaticSampler(s12, space=6), " \ - "StaticSampler(s13, space=6), " \ - "StaticSampler(s14, space=6), " \ - "StaticSampler(s15, space=6), " \ - "StaticSampler(s16, space=6), " \ - "StaticSampler(s17, space=6), " \ - "StaticSampler(s18, space=6), " \ - "StaticSampler(s19, space=6), " \ - "StaticSampler(s20, space=6), " \ - "StaticSampler(s21, space=6) " \ No newline at end of file + "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s15, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s16, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_MIRROR, addressV=TEXTURE_ADDRESS_MIRROR, addressW=TEXTURE_ADDRESS_MIRROR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s17, maxAnisotropy=4, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s18, addressU=TEXTURE_ADDRESS_MIRROR, addressV=TEXTURE_ADDRESS_MIRROR, addressW=TEXTURE_ADDRESS_MIRROR, maxAnisotropy=4, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s19, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=4, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s20, filter=FILTER_COMPARISON_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_LESS, space=6), " \ + "StaticSampler(s21, filter=FILTER_COMPARISON_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_GREATER, space=6) " \ No newline at end of file diff --git a/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl b/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl index 53d39147..88498cd4 100644 --- a/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl +++ b/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl @@ -1,8 +1,10 @@ #define ShaderRootSignature \ - "DescriptorTable(" \ + "RootFlags(0), " \ + \ + "DescriptorTable( " \ "SRV(t0, space=6), " \ "SRV(t1, space=6), " \ - "SRV(t2, space=6), " \ + "SRV(t2, space=6, flags=DATA_VOLATILE), " \ "SRV(t3, space=6), " \ "SRV(t4, space=6), " \ "SRV(t5, space=6), " \ @@ -32,25 +34,25 @@ "SRV(t28, space=6) " \ "), " \ \ - "DescriptorTable(" \ - "Sampler(s0, space=6, numDescriptors=10), " \ - "Sampler(s10, space=6, numDescriptors=2) " \ + "DescriptorTable( " \ + "Sampler(s0, numDescriptors=10, space=6), " \ + "Sampler(s10, numDescriptors=2, space=6) " \ "), " \ \ "SRV(t6, space=6), " \ "CBV(b0, space=6), " \ "CBV(b1, space=6), " \ \ - "DescriptorTable(" \ + "DescriptorTable( " \ "SRV(t2, space=7), " \ "SRV(t3, space=7), " \ - "SRV(t5, space=7), " \ - "SRV(t6, space=7), " \ + "SRV(t5, space=7, flags=DATA_VOLATILE), " \ + "SRV(t6, space=7, flags=DATA_VOLATILE), " \ "UAV(u0, space=7), " \ "UAV(u1, space=7), " \ "UAV(u2, space=7), " \ "UAV(u3, space=7), " \ - "SRV(t7, space=7, numDescriptors=12) " \ + "SRV(t7, numDescriptors=12, space=7, flags=DATA_VOLATILE) " \ "), " \ \ "SRV(t0, space=7), " \ @@ -60,19 +62,19 @@ "CBV(b1, space=7), " \ "CBV(b2, space=7), " \ \ - "DescriptorTable(" \ + "DescriptorTable( " \ "SRV(t0, space=8), " \ "UAV(u0, space=8) " \ "), " \ \ "CBV(b0, space=8), " \ - "StaticSampler(s12, space=6), " \ - "StaticSampler(s13, space=6), " \ - "StaticSampler(s14, space=6), " \ - "StaticSampler(s15, space=6), " \ - "StaticSampler(s16, space=6), " \ - "StaticSampler(s17, space=6), " \ - "StaticSampler(s18, space=6), " \ - "StaticSampler(s19, space=6), " \ - "StaticSampler(s20, space=6), " \ - "StaticSampler(s21, space=6) " \ No newline at end of file + "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s15, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s16, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_MIRROR, addressV=TEXTURE_ADDRESS_MIRROR, addressW=TEXTURE_ADDRESS_MIRROR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s17, maxAnisotropy=4, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s18, addressU=TEXTURE_ADDRESS_MIRROR, addressV=TEXTURE_ADDRESS_MIRROR, addressW=TEXTURE_ADDRESS_MIRROR, maxAnisotropy=4, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s19, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=4, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s20, filter=FILTER_COMPARISON_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_LESS, space=6), " \ + "StaticSampler(s21, filter=FILTER_COMPARISON_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_GREATER, space=6) " \ No newline at end of file diff --git a/shaders/FilmGrain/RootSignature.hlsl b/shaders/FilmGrain/RootSignature.hlsl index 77a1a61e..0318ae7d 100644 --- a/shaders/FilmGrain/RootSignature.hlsl +++ b/shaders/FilmGrain/RootSignature.hlsl @@ -1,9 +1,10 @@ #define ShaderRootSignature \ "RootFlags(ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT), " \ - "RootConstants(num32BitConstants=3, b0), " \ \ - "DescriptorTable(" \ + "RootConstants(num32BitConstants=4, b0), " \ + \ + "DescriptorTable( " \ "SRV(t0, space=8) " \ "), " \ \ - "StaticSampler(s0, space=8) " \ No newline at end of file + "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " \ No newline at end of file diff --git a/shaders/HDRComposite/RootSignature.hlsl b/shaders/HDRComposite/RootSignature.hlsl index b31c97a9..8cabf6ca 100644 --- a/shaders/HDRComposite/RootSignature.hlsl +++ b/shaders/HDRComposite/RootSignature.hlsl @@ -1,11 +1,12 @@ #define ShaderRootSignature \ "RootFlags(ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT), " \ + \ "RootConstants(num32BitConstants=3, b0), " \ \ - "DescriptorTable(" \ + "DescriptorTable( " \ "SRV(t0, space=6), " \ "SRV(t1, space=6), " \ - "SRV(t2, space=6), " \ + "SRV(t2, space=6, flags=DATA_VOLATILE), " \ "SRV(t3, space=6), " \ "SRV(t4, space=6), " \ "SRV(t5, space=6), " \ @@ -35,25 +36,25 @@ "SRV(t28, space=6) " \ "), " \ \ - "DescriptorTable(" \ - "Sampler(s0, space=6, numDescriptors=10), " \ - "Sampler(s10, space=6, numDescriptors=2) " \ + "DescriptorTable( " \ + "Sampler(s0, numDescriptors=10, space=6), " \ + "Sampler(s10, numDescriptors=2, space=6) " \ "), " \ \ "SRV(t6, space=6), " \ "CBV(b0, space=6), " \ "CBV(b1, space=6), " \ \ - "DescriptorTable(" \ + "DescriptorTable( " \ "SRV(t2, space=7), " \ "SRV(t3, space=7), " \ - "SRV(t5, space=7), " \ - "SRV(t6, space=7), " \ + "SRV(t5, space=7, flags=DATA_VOLATILE), " \ + "SRV(t6, space=7, flags=DATA_VOLATILE), " \ "UAV(u0, space=7), " \ "UAV(u1, space=7), " \ "UAV(u2, space=7), " \ "UAV(u3, space=7), " \ - "SRV(t7, space=7, numDescriptors=12) " \ + "SRV(t7, numDescriptors=12, space=7, flags=DATA_VOLATILE) " \ "), " \ \ "SRV(t0, space=7), " \ @@ -63,7 +64,7 @@ "CBV(b1, space=7), " \ "CBV(b2, space=7), " \ \ - "DescriptorTable(" \ + "DescriptorTable( " \ "SRV(t0, space=9), " \ "SRV(t1, space=9), " \ "SRV(t2, space=9), " \ @@ -71,14 +72,14 @@ "SRV(t4, space=9) " \ "), " \ \ - "StaticSampler(s12, space=6), " \ - "StaticSampler(s13, space=6), " \ - "StaticSampler(s14, space=6), " \ - "StaticSampler(s15, space=6), " \ - "StaticSampler(s16, space=6), " \ - "StaticSampler(s17, space=6), " \ - "StaticSampler(s18, space=6), " \ - "StaticSampler(s19, space=6), " \ - "StaticSampler(s20, space=6), " \ - "StaticSampler(s21, space=6), " \ - "StaticSampler(s0, space=9) " \ No newline at end of file + "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s15, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s16, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_MIRROR, addressV=TEXTURE_ADDRESS_MIRROR, addressW=TEXTURE_ADDRESS_MIRROR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s17, maxAnisotropy=4, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s18, addressU=TEXTURE_ADDRESS_MIRROR, addressV=TEXTURE_ADDRESS_MIRROR, addressW=TEXTURE_ADDRESS_MIRROR, maxAnisotropy=4, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s19, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=4, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s20, filter=FILTER_COMPARISON_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_LESS, space=6), " \ + "StaticSampler(s21, filter=FILTER_COMPARISON_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_GREATER, space=6), " \ + "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=9) " \ No newline at end of file diff --git a/shaders/PostSharpen/RootSignature.hlsl b/shaders/PostSharpen/RootSignature.hlsl index 9262126c..fdafaa4d 100644 --- a/shaders/PostSharpen/RootSignature.hlsl +++ b/shaders/PostSharpen/RootSignature.hlsl @@ -1,11 +1,12 @@ #define ShaderRootSignature \ "RootFlags(ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT), " \ + \ "RootConstants(num32BitConstants=7, b0), " \ \ "DescriptorTable( " \ "SRV(t0, space=6), " \ "SRV(t1, space=6), " \ - "SRV(t2, space=6), " \ + "SRV(t2, space=6, flags=DATA_VOLATILE), " \ "SRV(t3, space=6), " \ "SRV(t4, space=6), " \ "SRV(t5, space=6), " \ @@ -35,25 +36,25 @@ "SRV(t28, space=6) " \ "), " \ \ - "DescriptorTable(" \ - "Sampler(s0, space=6, numDescriptors=10), " \ - "Sampler(s10, space=6, numDescriptors=2) " \ + "DescriptorTable( " \ + "Sampler(s0, numDescriptors=10, space=6), " \ + "Sampler(s10, numDescriptors=2, space=6) " \ "), " \ \ "SRV(t6, space=6), " \ "CBV(b0, space=6), " \ "CBV(b1, space=6), " \ \ - "DescriptorTable(" \ + "DescriptorTable( " \ "SRV(t2, space=7), " \ "SRV(t3, space=7), " \ - "SRV(t5, space=7), " \ - "SRV(t6, space=7), " \ + "SRV(t5, space=7, flags=DATA_VOLATILE), " \ + "SRV(t6, space=7, flags=DATA_VOLATILE), " \ "UAV(u0, space=7), " \ "UAV(u1, space=7), " \ "UAV(u2, space=7), " \ "UAV(u3, space=7), " \ - "SRV(t7, space=7, numDescriptors=12) " \ + "SRV(t7, numDescriptors=12, space=7, flags=DATA_VOLATILE) " \ "), " \ \ "SRV(t0, space=7), " \ @@ -63,17 +64,17 @@ "CBV(b1, space=7), " \ "CBV(b2, space=7), " \ \ - "DescriptorTable(" \ + "DescriptorTable( " \ "SRV(t0, space=8) " \ "), " \ \ - "StaticSampler(s12, space=6), " \ - "StaticSampler(s13, space=6), " \ - "StaticSampler(s14, space=6), " \ - "StaticSampler(s15, space=6), " \ - "StaticSampler(s16, space=6), " \ - "StaticSampler(s17, space=6), " \ - "StaticSampler(s18, space=6), " \ - "StaticSampler(s19, space=6), " \ - "StaticSampler(s20, space=6), " \ - "StaticSampler(s21, space=6) " \ No newline at end of file + "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s15, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s16, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_MIRROR, addressV=TEXTURE_ADDRESS_MIRROR, addressW=TEXTURE_ADDRESS_MIRROR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s17, maxAnisotropy=4, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s18, addressU=TEXTURE_ADDRESS_MIRROR, addressV=TEXTURE_ADDRESS_MIRROR, addressW=TEXTURE_ADDRESS_MIRROR, maxAnisotropy=4, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s19, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=4, comparisonFunc=COMPARISON_NEVER, space=6), " \ + "StaticSampler(s20, filter=FILTER_COMPARISON_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_LESS, space=6), " \ + "StaticSampler(s21, filter=FILTER_COMPARISON_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_GREATER, space=6) " \ No newline at end of file diff --git a/shaders/ScaleformComposite/RootSignature.hlsl b/shaders/ScaleformComposite/RootSignature.hlsl index 8a4e8065..fc4e5391 100644 --- a/shaders/ScaleformComposite/RootSignature.hlsl +++ b/shaders/ScaleformComposite/RootSignature.hlsl @@ -1,9 +1,10 @@ #define ShaderRootSignature \ "RootFlags(ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT), " \ - "RootConstants(num32BitConstants=3, b0), " \ \ - "DescriptorTable(" \ - "SRV(t0, space=8)" \ + "RootConstants(num32BitConstants=2, b0), " \ + \ + "DescriptorTable( " \ + "SRV(t0, space=8) " \ "), " \ \ - "StaticSampler(s0, space=8) " \ No newline at end of file + "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " \ No newline at end of file From d36185040e7c8365a9f8f4d4c87abb7d3f68f159 Mon Sep 17 00:00:00 2001 From: Nukem Date: Wed, 4 Oct 2023 21:01:35 -0400 Subject: [PATCH 074/270] - Rename main shader passes to use consistent file names - Add powershell build script in preparation for root signature bin output --- ...nkMovie_FFAB_ps.hlsl => BinkMovie_ps.hlsl} | 0 ...FF81_cs.hlsl => ColorGradingMerge_cs.hlsl} | 0 ...lsl => ContrastAdaptiveSharpening_cs.hlsl} | 0 ...lmGrain_FF75_ps.hlsl => FilmGrain_ps.hlsl} | 0 ...arpen_FF9A_ps.hlsl => PostSharpen_ps.hlsl} | 0 ...FAA_ps.hlsl => ScaleformComposite_ps.hlsl} | 0 shaders/compile_all_shaders.ps1 | 125 ++++++++++++++++++ 7 files changed, 125 insertions(+) rename shaders/BinkMovie/{BinkMovie_FFAB_ps.hlsl => BinkMovie_ps.hlsl} (100%) rename shaders/ColorGradingMerge/{ColorGradingMerge_FF81_cs.hlsl => ColorGradingMerge_cs.hlsl} (100%) rename shaders/ContrastAdaptiveSharpening/{ContrastAdaptiveSharpening_100FF94_cs.hlsl => ContrastAdaptiveSharpening_cs.hlsl} (100%) rename shaders/FilmGrain/{FilmGrain_FF75_ps.hlsl => FilmGrain_ps.hlsl} (100%) rename shaders/PostSharpen/{PostSharpen_FF9A_ps.hlsl => PostSharpen_ps.hlsl} (100%) rename shaders/ScaleformComposite/{ScaleformComposite_FFAA_ps.hlsl => ScaleformComposite_ps.hlsl} (100%) create mode 100644 shaders/compile_all_shaders.ps1 diff --git a/shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl b/shaders/BinkMovie/BinkMovie_ps.hlsl similarity index 100% rename from shaders/BinkMovie/BinkMovie_FFAB_ps.hlsl rename to shaders/BinkMovie/BinkMovie_ps.hlsl diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl similarity index 100% rename from shaders/ColorGradingMerge/ColorGradingMerge_FF81_cs.hlsl rename to shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl diff --git a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_100FF94_cs.hlsl b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl similarity index 100% rename from shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_100FF94_cs.hlsl rename to shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl diff --git a/shaders/FilmGrain/FilmGrain_FF75_ps.hlsl b/shaders/FilmGrain/FilmGrain_ps.hlsl similarity index 100% rename from shaders/FilmGrain/FilmGrain_FF75_ps.hlsl rename to shaders/FilmGrain/FilmGrain_ps.hlsl diff --git a/shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl b/shaders/PostSharpen/PostSharpen_ps.hlsl similarity index 100% rename from shaders/PostSharpen/PostSharpen_FF9A_ps.hlsl rename to shaders/PostSharpen/PostSharpen_ps.hlsl diff --git a/shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl b/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl similarity index 100% rename from shaders/ScaleformComposite/ScaleformComposite_FFAA_ps.hlsl rename to shaders/ScaleformComposite/ScaleformComposite_ps.hlsl diff --git a/shaders/compile_all_shaders.ps1 b/shaders/compile_all_shaders.ps1 new file mode 100644 index 00000000..5e379512 --- /dev/null +++ b/shaders/compile_all_shaders.ps1 @@ -0,0 +1,125 @@ +$ErrorActionPreference = "Stop" + + +$ShaderOutputDirectory = Get-Content "${PSScriptRoot}\target_folder.txt" +$ShaderOutputEmbedPDB = $false + + +$main = +{ + Compile-Shader -Type "ps" -TechniqueName "HDRComposite" -TechniqueId "FF1A" + Compile-Shader -Type "ps" -TechniqueName "HDRComposite" -TechniqueId "600FF1A" -Defines "APPLY_TONEMAPPING", "APPLY_CINEMATICS" + Compile-Shader -Type "ps" -TechniqueName "HDRComposite" -TechniqueId "700FF1A" -Defines "APPLY_BLOOM", "APPLY_TONEMAPPING", "APPLY_CINEMATICS" + #Compile-Shader -Type "ps" -TechniqueName "HDRComposite" -TechniqueId "800FF1A" -Defines "APPLY_MERGED_COLOR_GRADING_LUT" + Compile-Shader -Type "ps" -TechniqueName "HDRComposite" -TechniqueId "E00FF1A" -Defines "APPLY_TONEMAPPING", "APPLY_CINEMATICS", "APPLY_MERGED_COLOR_GRADING_LUT" + Compile-Shader -Type "ps" -TechniqueName "HDRComposite" -TechniqueId "F00FF1A" -Defines "APPLY_BLOOM", "APPLY_TONEMAPPING", "APPLY_CINEMATICS", "APPLY_MERGED_COLOR_GRADING_LUT" + + Compile-Shader -Type "ps" -TechniqueName "FilmGrain" -TechniqueId "FF75" -Entry "main" + + Compile-Shader -Type "cs" -TechniqueName "ColorGradingMerge" -TechniqueId "FF81" + + #Compile-Shader -Type "cs" -TechniqueName "ContrastAdaptiveSharpening" -TechniqueId "FF94" -Entry "main" + Compile-Shader -Type "cs" -TechniqueName "ContrastAdaptiveSharpening" -TechniqueId "100FF94" -Entry "main" -Defines "USE_PACKED_MATH" + + Compile-Shader -Type "ps" -TechniqueName "PostSharpen" -TechniqueId "FF9A" -Entry "main" + + Compile-Shader -Type "ps" -TechniqueName "ScaleformComposite" -TechniqueId "FFAA" + + Compile-Shader -Type "ps" -TechniqueName "BinkMovie" -TechniqueId "FFAB" +} + + +function Run-DXC { + param ( + [Parameter(Mandatory=$true)] + [string]$Arguments + ) + + $processInfo = New-Object System.Diagnostics.ProcessStartInfo + $processInfo.FileName = "${PSScriptRoot}\..\tools\dxc_2023_08_14\bin\x64\dxc.exe" + $processInfo.Arguments = $Arguments + $processInfo.RedirectStandardError = $true + $processInfo.RedirectStandardOutput = $true + $processInfo.UseShellExecute = $false + $processInfo.CreateNoWindow = $true + + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $processInfo + $process.Start() | Out-Null + $process.WaitForExit() + + # Output the result + $stdout = $process.StandardOutput.ReadToEnd() + $stderr = $process.StandardError.ReadToEnd() + + if ($process.ExitCode -ne 0) { + Write-Error "An error occurred during shader compilation:`n$stderr" + } + + if ($stdout.Length -gt 0) { + Write-Host $stdout + } +} + +function Compile-Shader { + param ( + [Parameter(Mandatory=$true)] + [string]$Type, + + [Parameter(Mandatory=$true)] + [string]$TechniqueName, + + [Parameter(Mandatory=$true)] + [string]$TechniqueId, + + [Parameter(Mandatory=$false)] + [string]$Entry = $Type.ToUpper(), + + [parameter(Mandatory=$false)] + [array]$Defines + ) + + $inputHlslName = "${TechniqueName}_${Type}.hlsl" + $outputBinName = "${TechniqueName}_${TechniqueId}_${Type}.bin" + $outputSigName = "${TechniqueName}_${TechniqueId}_rsg.bin" + + $inputHlslPath = "${PSScriptRoot}\${TechniqueName}\${inputHlslName}" + $stagedBinPath = "${PSScriptRoot}\${TechniqueName}\${outputBinName}" + $stagedSigPath = "${PSScriptRoot}\${TechniqueName}\${outputSigName}" + + Write-Host "Compiling ${outputBinName}..." + + # Build the shader in its staging directory + $args = "`"${inputHlslPath}`" -Fo `"${stagedBinPath}`" -T ${Type}_6_6 -E ${Entry} " + + if ($ShaderOutputEmbedPDB -eq $true) { + $args = $args + "-Qembed_debug -Zi " + } + + foreach ($define in $Defines) { + $args = $args + "-D ${define} " + } + + Run-DXC -Arguments $args + + # Extract and strip away the DXIL root signature + # + # TODO: Can extractrootsignature and Qstrip_rootsignature be used in the same operation? + # TODO: Enable when ready + #Run-DXC -Arguments "-dumpbin `"${stagedBinPath}`" -extractrootsignature -Fo `"${stagedSigPath}`"" + #Run-DXC -Arguments "-dumpbin `"${stagedBinPath}`" -Qstrip_rootsignature -Fo `"${stagedBinPath}`"" + + # Move the resulting bins to the game directory. Move-Item is to avoid partial reads when live shader editing + # is enabled. + New-Item -Force -ItemType Directory -Path "${ShaderOutputDirectory}\${TechniqueName}" | Out-Null + Move-Item -Force -Path $stagedBinPath -Destination "${ShaderOutputDirectory}\${TechniqueName}\${outputBinName}" + #Move-Item -Force -Path $stagedSigPath -Destination "${ShaderOutputDirectory}\${TechniqueName}\${outputSigName}" +} + +try { + & $main +} +catch { + Write-Host -ForegroundColor Red $_ + Pause +} \ No newline at end of file From 597a09f72467a6f15cbfe13f5b9a5b0f30a9f53e Mon Sep 17 00:00:00 2001 From: Nukem Date: Wed, 4 Oct 2023 21:06:18 -0400 Subject: [PATCH 075/270] - Connect root signatures to their respective shader entry points - Enable rsg.bin output in the build script --- shaders/BinkMovie/BinkMovie_ps.hlsl | 2 ++ shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl | 2 ++ .../ContrastAdaptiveSharpening_cs.hlsl | 2 ++ shaders/FilmGrain/FilmGrain_ps.hlsl | 2 ++ shaders/HDRComposite/HDRComposite_ps.hlsl | 2 ++ shaders/PostSharpen/PostSharpen_ps.hlsl | 2 ++ shaders/ScaleformComposite/ScaleformComposite_ps.hlsl | 2 ++ shaders/compile_all_shaders.ps1 | 8 +++----- 8 files changed, 17 insertions(+), 5 deletions(-) diff --git a/shaders/BinkMovie/BinkMovie_ps.hlsl b/shaders/BinkMovie/BinkMovie_ps.hlsl index 4427a4e4..1065c6d1 100644 --- a/shaders/BinkMovie/BinkMovie_ps.hlsl +++ b/shaders/BinkMovie/BinkMovie_ps.hlsl @@ -1,5 +1,6 @@ #include "../shared.hlsl" #include "../color.hlsl" +#include "RootSignature.hlsl" Texture2D _8 : register(t0, space8); Texture2D _9 : register(t1, space8); @@ -12,6 +13,7 @@ struct PSInputs float2 TEXCOORD : TEXCOORD0; }; +[RootSignature(ShaderRootSignature)] float4 PS(PSInputs inputs) : SV_Target { float Y = _8.Sample(_13, inputs.TEXCOORD.xy).x; diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl index 1ff61179..1fd0b111 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl @@ -1,6 +1,7 @@ #include "../shared.hlsl" #include "../color.hlsl" #include "../math.hlsl" +#include "RootSignature.hlsl" // 0 None, 1 ShortFuse technique (normalization), 2 luminance preservation (doesn't look so good and it's kinda broken in SDR) #define LUT_IMPROVEMENT_TYPE 1 @@ -272,6 +273,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b } // Dispatch size is 1 1 16 (x and y have one thread and one thread group, while z has 16 thread groups with a thread each) +[RootSignature(ShaderRootSignature)] [numthreads(LUT_SIZE_UINT, LUT_SIZE_UINT, 1)] void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) { diff --git a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl index 91b6e299..151f73f2 100644 --- a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl +++ b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl @@ -1,5 +1,6 @@ #include "../shared.hlsl" #include "../color.hlsl" +#include "RootSignature.hlsl" cbuffer _16_18 : register(b0, space6) { @@ -239,6 +240,7 @@ void comp_main() #endif } +[RootSignature(ShaderRootSignature)] [numthreads(64, 1, 1)] void main(SPIRV_Cross_Input stage_input) { diff --git a/shaders/FilmGrain/FilmGrain_ps.hlsl b/shaders/FilmGrain/FilmGrain_ps.hlsl index e5d70c60..e8478fd2 100644 --- a/shaders/FilmGrain/FilmGrain_ps.hlsl +++ b/shaders/FilmGrain/FilmGrain_ps.hlsl @@ -1,5 +1,6 @@ #include "../shared.hlsl" #include "../color.hlsl" +#include "RootSignature.hlsl" #define FILM_GRAIN_TEXTURE_SIZE 1024u @@ -112,6 +113,7 @@ void frag_main() SV_Target.w = 1.0f; } +[RootSignature(ShaderRootSignature)] SPIRV_Cross_Output main(SPIRV_Cross_Input stage_input) { TEXCOORD = stage_input.TEXCOORD; diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 83b23b3e..7e8c98bf 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -1,6 +1,7 @@ #include "../shared.hlsl" #include "../color.hlsl" #include "../math.hlsl" +#include "RootSignature.hlsl" // These are defined at compile time (shaders permutations) //#define APPLY_BLOOM @@ -792,6 +793,7 @@ float3 RestorePostProcess(float3 inverseTonemappedColor, float3 postProcessColor #endif } +[RootSignature(ShaderRootSignature)] PSOutput PS(PSInput psInput) { // Linear HDR color straight from the renderer (possibly with exposure pre-applied to it, assuming the game has some auto exposure mechanism) diff --git a/shaders/PostSharpen/PostSharpen_ps.hlsl b/shaders/PostSharpen/PostSharpen_ps.hlsl index e57281ec..33977680 100644 --- a/shaders/PostSharpen/PostSharpen_ps.hlsl +++ b/shaders/PostSharpen/PostSharpen_ps.hlsl @@ -1,5 +1,6 @@ #include "../shared.hlsl" #include "../color.hlsl" +#include "RootSignature.hlsl" cbuffer _13_15 : register(b0, space7) { @@ -68,6 +69,7 @@ void frag_main() SV_Target.w = 1.0f; } +[RootSignature(ShaderRootSignature)] SPIRV_Cross_Output main(SPIRV_Cross_Input stage_input) { TEXCOORD = stage_input.TEXCOORD; diff --git a/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl b/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl index 062d27d4..224cda9f 100644 --- a/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl +++ b/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl @@ -1,5 +1,6 @@ #include "../shared.hlsl" #include "../color.hlsl" +#include "RootSignature.hlsl" // Hack: change the alpha value at which the UI blends in in HDR, to increase readability. Range is 0 to 1, with 1 having no effect. #define HDR_UI_BLEND_POW 0.775f @@ -24,6 +25,7 @@ struct PSInputs float4 pos : SV_Position; }; +[RootSignature(ShaderRootSignature)] float4 PS(PSInputs inputs) : SV_Target { float4 UIColor = inputTexture.Sample(inputSampler, float2(inputs.uv.x, inputs.uv.y)); diff --git a/shaders/compile_all_shaders.ps1 b/shaders/compile_all_shaders.ps1 index 5e379512..fa6a67a6 100644 --- a/shaders/compile_all_shaders.ps1 +++ b/shaders/compile_all_shaders.ps1 @@ -103,17 +103,15 @@ function Compile-Shader { Run-DXC -Arguments $args # Extract and strip away the DXIL root signature - # # TODO: Can extractrootsignature and Qstrip_rootsignature be used in the same operation? - # TODO: Enable when ready - #Run-DXC -Arguments "-dumpbin `"${stagedBinPath}`" -extractrootsignature -Fo `"${stagedSigPath}`"" - #Run-DXC -Arguments "-dumpbin `"${stagedBinPath}`" -Qstrip_rootsignature -Fo `"${stagedBinPath}`"" + Run-DXC -Arguments "-dumpbin `"${stagedBinPath}`" -extractrootsignature -Fo `"${stagedSigPath}`"" + Run-DXC -Arguments "-dumpbin `"${stagedBinPath}`" -Qstrip_rootsignature -Fo `"${stagedBinPath}`"" # Move the resulting bins to the game directory. Move-Item is to avoid partial reads when live shader editing # is enabled. New-Item -Force -ItemType Directory -Path "${ShaderOutputDirectory}\${TechniqueName}" | Out-Null Move-Item -Force -Path $stagedBinPath -Destination "${ShaderOutputDirectory}\${TechniqueName}\${outputBinName}" - #Move-Item -Force -Path $stagedSigPath -Destination "${ShaderOutputDirectory}\${TechniqueName}\${outputSigName}" + Move-Item -Force -Path $stagedSigPath -Destination "${ShaderOutputDirectory}\${TechniqueName}\${outputSigName}" } try { From d656949a379c9a05d054ea24f202365be0d4ec1f Mon Sep 17 00:00:00 2001 From: Nukem Date: Wed, 4 Oct 2023 21:39:15 -0400 Subject: [PATCH 076/270] Add placeholder for new constant buffer members --- shaders/BinkMovie/RootSignature.hlsl | 1 + shaders/ColorGradingMerge/RootSignature.hlsl | 1 + shaders/ContrastAdaptiveSharpening/RootSignature.hlsl | 1 + shaders/FilmGrain/RootSignature.hlsl | 1 + shaders/HDRComposite/RootSignature.hlsl | 1 + shaders/PostSharpen/RootSignature.hlsl | 1 + shaders/ScaleformComposite/RootSignature.hlsl | 1 + shaders/shared.hlsl | 6 ++++++ 8 files changed, 13 insertions(+) diff --git a/shaders/BinkMovie/RootSignature.hlsl b/shaders/BinkMovie/RootSignature.hlsl index 62c31c75..8565e720 100644 --- a/shaders/BinkMovie/RootSignature.hlsl +++ b/shaders/BinkMovie/RootSignature.hlsl @@ -7,4 +7,5 @@ "SRV(t2, space=8) " \ "), " \ \ + "RootConstants(num32BitConstants=4, b3), " \ "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " \ No newline at end of file diff --git a/shaders/ColorGradingMerge/RootSignature.hlsl b/shaders/ColorGradingMerge/RootSignature.hlsl index 67e60570..1a2c2857 100644 --- a/shaders/ColorGradingMerge/RootSignature.hlsl +++ b/shaders/ColorGradingMerge/RootSignature.hlsl @@ -53,6 +53,7 @@ "UAV(u0, space=8) " \ "), " \ \ + "RootConstants(num32BitConstants=4, b3), " \ "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ diff --git a/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl b/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl index 88498cd4..606f8ad6 100644 --- a/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl +++ b/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl @@ -68,6 +68,7 @@ "), " \ \ "CBV(b0, space=8), " \ + "RootConstants(num32BitConstants=4, b3), " \ "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ diff --git a/shaders/FilmGrain/RootSignature.hlsl b/shaders/FilmGrain/RootSignature.hlsl index 0318ae7d..70b314f2 100644 --- a/shaders/FilmGrain/RootSignature.hlsl +++ b/shaders/FilmGrain/RootSignature.hlsl @@ -7,4 +7,5 @@ "SRV(t0, space=8) " \ "), " \ \ + "RootConstants(num32BitConstants=4, b3), " \ "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " \ No newline at end of file diff --git a/shaders/HDRComposite/RootSignature.hlsl b/shaders/HDRComposite/RootSignature.hlsl index 8cabf6ca..e7e01534 100644 --- a/shaders/HDRComposite/RootSignature.hlsl +++ b/shaders/HDRComposite/RootSignature.hlsl @@ -72,6 +72,7 @@ "SRV(t4, space=9) " \ "), " \ \ + "RootConstants(num32BitConstants=4, b3), " \ "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ diff --git a/shaders/PostSharpen/RootSignature.hlsl b/shaders/PostSharpen/RootSignature.hlsl index fdafaa4d..5db8933e 100644 --- a/shaders/PostSharpen/RootSignature.hlsl +++ b/shaders/PostSharpen/RootSignature.hlsl @@ -68,6 +68,7 @@ "SRV(t0, space=8) " \ "), " \ \ + "RootConstants(num32BitConstants=4, b3), " \ "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ diff --git a/shaders/ScaleformComposite/RootSignature.hlsl b/shaders/ScaleformComposite/RootSignature.hlsl index fc4e5391..10cc0da3 100644 --- a/shaders/ScaleformComposite/RootSignature.hlsl +++ b/shaders/ScaleformComposite/RootSignature.hlsl @@ -7,4 +7,5 @@ "SRV(t0, space=8) " \ "), " \ \ + "RootConstants(num32BitConstants=4, b3), " \ "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " \ No newline at end of file diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 755f2a72..2710b4d2 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -26,3 +26,9 @@ #define LUT_FIX_GAMMA_MAPPING 1 #define LUT_SIZE 16.f #define LUT_SIZE_UINT (uint)LUT_SIZE + +// Custom push constants uploaded by the HDR DLL plugin code. Do note that register space comes at a premium when adding members. Bit/byte packing is advised. +cbuffer PushConstants_HdrDllPlugin : register(b3, space0) +{ + float4 CustomValuesInjectedByHdrPlugin : packoffset(c0); +}; \ No newline at end of file From 0e45573d4e5ef32bb816c646864f4e8c47b01635 Mon Sep 17 00:00:00 2001 From: Nukem Date: Wed, 4 Oct 2023 22:00:17 -0400 Subject: [PATCH 077/270] Fix whitespace --- shaders/compile_all_shaders.ps1 | 80 ++++++++++++++++----------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/shaders/compile_all_shaders.ps1 b/shaders/compile_all_shaders.ps1 index fa6a67a6..f5ce4bde 100644 --- a/shaders/compile_all_shaders.ps1 +++ b/shaders/compile_all_shaders.ps1 @@ -30,31 +30,31 @@ $main = function Run-DXC { - param ( - [Parameter(Mandatory=$true)] - [string]$Arguments - ) - - $processInfo = New-Object System.Diagnostics.ProcessStartInfo - $processInfo.FileName = "${PSScriptRoot}\..\tools\dxc_2023_08_14\bin\x64\dxc.exe" - $processInfo.Arguments = $Arguments - $processInfo.RedirectStandardError = $true - $processInfo.RedirectStandardOutput = $true - $processInfo.UseShellExecute = $false - $processInfo.CreateNoWindow = $true - - $process = New-Object System.Diagnostics.Process - $process.StartInfo = $processInfo - $process.Start() | Out-Null - $process.WaitForExit() - - # Output the result - $stdout = $process.StandardOutput.ReadToEnd() - $stderr = $process.StandardError.ReadToEnd() - - if ($process.ExitCode -ne 0) { - Write-Error "An error occurred during shader compilation:`n$stderr" - } + param ( + [Parameter(Mandatory = $true)] + [string]$Arguments + ) + + $processInfo = New-Object System.Diagnostics.ProcessStartInfo + $processInfo.FileName = "${PSScriptRoot}\..\tools\dxc_2023_08_14\bin\x64\dxc.exe" + $processInfo.Arguments = $Arguments + $processInfo.RedirectStandardError = $true + $processInfo.RedirectStandardOutput = $true + $processInfo.UseShellExecute = $false + $processInfo.CreateNoWindow = $true + + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $processInfo + $process.Start() | Out-Null + $process.WaitForExit() + + # Output the result + $stdout = $process.StandardOutput.ReadToEnd() + $stderr = $process.StandardError.ReadToEnd() + + if ($process.ExitCode -ne 0) { + Write-Error "An error occurred during shader compilation:`n$stderr" + } if ($stdout.Length -gt 0) { Write-Host $stdout @@ -62,22 +62,22 @@ function Run-DXC { } function Compile-Shader { - param ( - [Parameter(Mandatory=$true)] - [string]$Type, + param ( + [Parameter(Mandatory = $true)] + [string]$Type, - [Parameter(Mandatory=$true)] - [string]$TechniqueName, + [Parameter(Mandatory = $true)] + [string]$TechniqueName, - [Parameter(Mandatory=$true)] - [string]$TechniqueId, + [Parameter(Mandatory = $true)] + [string]$TechniqueId, - [Parameter(Mandatory=$false)] - [string]$Entry = $Type.ToUpper(), + [Parameter(Mandatory = $false)] + [string]$Entry = $Type.ToUpper(), - [parameter(Mandatory=$false)] - [array]$Defines - ) + [parameter(Mandatory = $false)] + [array]$Defines + ) $inputHlslName = "${TechniqueName}_${Type}.hlsl" $outputBinName = "${TechniqueName}_${TechniqueId}_${Type}.bin" @@ -97,10 +97,10 @@ function Compile-Shader { } foreach ($define in $Defines) { - $args = $args + "-D ${define} " - } + $args = $args + "-D ${define} " + } - Run-DXC -Arguments $args + Run-DXC -Arguments $args # Extract and strip away the DXIL root signature # TODO: Can extractrootsignature and Qstrip_rootsignature be used in the same operation? From d3478fa5bee0c6adbc5ec41029b7a4c19db81dfb Mon Sep 17 00:00:00 2001 From: Nukem Date: Fri, 6 Oct 2023 07:40:50 -0400 Subject: [PATCH 078/270] Add plugin dll hooks to upload (root) constant buffer values to shaders --- Plugin/src/Hooks.cpp | 71 ++++++++++++++++++++++++++++++++++++++++++++ Plugin/src/Hooks.h | 8 ++++- shaders/shared.hlsl | 11 +++++-- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/Plugin/src/Hooks.cpp b/Plugin/src/Hooks.cpp index 13461bc8..139c694b 100644 --- a/Plugin/src/Hooks.cpp +++ b/Plugin/src/Hooks.cpp @@ -201,6 +201,77 @@ namespace Hooks _SettingsDataModelFloatEvent(a_arg1, a_eventData); } + bool Hooks::Hook_ApplyRenderPassRenderState(void* a_arg1, void* a_arg2) + { + const bool result = _ApplyRenderPassRenderState(a_arg1, a_arg2); + + if (result) { + const auto technique = *reinterpret_cast(reinterpret_cast(a_arg2) + 0x8); + const auto techniqueId = *reinterpret_cast(technique + 0x78); + + auto uploadRootConstants = [&](uint32_t RootParameterIndex, bool Compute) { + auto commandList = *reinterpret_cast(reinterpret_cast(a_arg1) + 0x10); + const auto settings = Settings::Main::GetSingleton(); + + // This can be any data type, even a struct. It just has to match StructHdrDllPluginConstants in HLSL. + std::array data { + *settings->PeakBrightness.value, + *settings->GamePaperWhite.value, + *settings->UIPaperWhite.value, + 4.0f + }; + + if (!Compute) + commandList->SetGraphicsRoot32BitConstants(RootParameterIndex, data.size(), reinterpret_cast(data.data()), 0); + else + commandList->SetComputeRoot32BitConstants(RootParameterIndex, data.size(), reinterpret_cast(data.data()), 0); + }; + + // Note: The following switch statement may be called several thousand times per frame. Additionally, it'll be called from multiple + // threads concurrently. The individual cases are called at most once or twice per frame. Keep the amount of code here fairly light. + // + // RootParameterIndex is the index of our custom RootConstants() entry in the root signature. It's taken from the corresponding + // RootSignature.hlsl file stored next to each technique hlsl file. + switch (techniqueId) { + case 0xFF1A: + case 0x600FF1A: + case 0x700FF1A: + //case 0x800FF1A: + case 0xE00FF1A: + case 0xF00FF1A: + uploadRootConstants(14, false); // HDRComposite + break; + + case 0xFF75: + uploadRootConstants(2, false); // FilmGrain + break; + + case 0xFF81: + uploadRootConstants(7, true); // ColorGradingMerge + break; + + //case 0xFF94: + case 0x100FF94: + uploadRootConstants(14, true); // ContrastAdaptiveSharpening + break; + + case 0xFF9A: + uploadRootConstants(14, false); // PostSharpen + break; + + case 0xFFAA: + uploadRootConstants(2, false); // ScaleformComposite + break; + + case 0xFFAB: + uploadRootConstants(1, false); // BinkMovie + break; + } + } + + return result; + } + void Install() { #ifndef NDEBUG diff --git a/Plugin/src/Hooks.h b/Plugin/src/Hooks.h index 95741a6b..d802777d 100644 --- a/Plugin/src/Hooks.h +++ b/Plugin/src/Hooks.h @@ -39,7 +39,7 @@ namespace Hooks // disable photo mode screenshots with HDR const auto takeSnapshotVtbl = dku::Hook::IDToAbs(415473); - auto _Hook_TakeSnapshot = dku::Hook::AddVMTHook(&takeSnapshotVtbl, 1, FUNC_INFO(Hook_TakeSnapshot)); + auto _Hook_TakeSnapshot = dku::Hook::AddVMTHook(&takeSnapshotVtbl, 1, FUNC_INFO(Hook_TakeSnapshot)); _TakeSnapshot = reinterpret_cast>(_Hook_TakeSnapshot->OldAddress); _Hook_TakeSnapshot->Enable(); @@ -50,6 +50,9 @@ namespace Hooks _SettingsDataModelFloatEvent = dku::Hook::write_call<5>(dku::Hook::IDToAbs(136130, 0x37), Hook_SettingsDataModelFloatEvent); _RecreateSwapchain = dku::Hook::write_call<5>(dku::Hook::IDToAbs(203027, 0x89), Hook_RecreateSwapchain); + + _ApplyRenderPassRenderState = dku::Hook::write_call<5>(dku::Hook::IDToAbs(204409, 0x18), Hook_ApplyRenderPassRenderState); // CmdDraw + dku::Hook::write_call<5>(dku::Hook::IDToAbs(204408, 0x20), Hook_ApplyRenderPassRenderState); // CmdDispatch } private: @@ -77,6 +80,9 @@ namespace Hooks static inline std::add_pointer_t _SettingsDataModelIntEvent; static void Hook_SettingsDataModelFloatEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& a_eventData); static inline std::add_pointer_t _SettingsDataModelFloatEvent; + + static bool Hook_ApplyRenderPassRenderState(void* a_arg1, void* a_arg2); + static inline std::add_pointer_t _ApplyRenderPassRenderState; }; void Install(); diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 2710b4d2..7e5c26b5 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -28,7 +28,12 @@ #define LUT_SIZE_UINT (uint)LUT_SIZE // Custom push constants uploaded by the HDR DLL plugin code. Do note that register space comes at a premium when adding members. Bit/byte packing is advised. -cbuffer PushConstants_HdrDllPlugin : register(b3, space0) +struct StructHdrDllPluginConstants { - float4 CustomValuesInjectedByHdrPlugin : packoffset(c0); -}; \ No newline at end of file + float PeakBrightness; + float GamePaperWhite; + float UIPaperWhite; + uint Unused; +}; + +ConstantBuffer HdrDllPluginConstants : register(b3, space0); \ No newline at end of file From b85822aad82a37d6a318e77f86c1e5f93c3b2d1b Mon Sep 17 00:00:00 2001 From: Lilium Date: Fri, 6 Oct 2023 04:07:34 +0200 Subject: [PATCH 079/270] fixup CAS for HDR + a bunch of optimisation (performance is the same or faster than before) --- .../ContrastAdaptiveSharpening_cs.hlsl | 495 ++++++++++-------- shaders/compile_all_shaders.bat | 2 +- shaders/structs.hlsl | 10 + 3 files changed, 287 insertions(+), 220 deletions(-) diff --git a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl index 151f73f2..b4d00c59 100644 --- a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl +++ b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl @@ -2,249 +2,306 @@ #include "../color.hlsl" #include "RootSignature.hlsl" -cbuffer _16_18 : register(b0, space6) -{ - float4 _18_m0[8] : packoffset(c0); -}; +// this is CAS upscaling in FP16 with FFX_CAS_USE_PRECISE_MATH [100FF94] +// other variants are also available, which are: +// - CAS upscaling in FP32 [200FF94] +// - CAS upscaling in FP16 with FFX_CAS_USE_PRECISE_MATH and FFX_CAS_BETTER_DIAGONALS [300FF94] +// - just CAS sharpening in FP32 [FF94] + +// TODO make full process in half not just after csp conversion? +// needs matrices in half -cbuffer _21_23 : register(b0, space8) +// don't need it +//cbuffer _16_18 : register(b0, space6) +//{ +// float4 _18_m0[8] : packoffset(c0); // _18_m0[1u].w = Gamma +//}; + +cbuffer CCASData : register(b0, space8) { - float4 _23_m0[4] : packoffset(c0); + ContrastAdaptiveSharpeningData CASData : packoffset(c0); }; -Texture2D _8 : register(t0, space8); -RWTexture2D _11 : register(u0, space8); +Texture2D ColorIn : register(t0, space8); +RWTexture2D ColorOut : register(u0, space8); -static uint3 gl_WorkGroupID; -static uint3 gl_LocalInvocationID; -struct SPIRV_Cross_Input +struct CSInput { - uint3 gl_WorkGroupID : SV_GroupID; - uint3 gl_LocalInvocationID : SV_GroupThreadID; + uint3 SV_GroupID : SV_GroupID; + uint3 SV_GroupThreadID : SV_GroupThreadID; }; -uint spvPackHalf2x16(float2 value) -{ - uint2 Packed = f32tof16(value); - return Packed.x | (Packed.y << 16); -} +//uint spvPackHalf2x16(float2 value) +//{ +// uint2 Packed = f32tof16(value); +// return Packed.x | (Packed.y << 16); +//} +// +//half2 spvUnpackHalf2x16(uint value) +//{ +// return f16tof32(uint2(value & 0xffff, value >> 16)); +//} -float2 spvUnpackHalf2x16(uint value) -{ - return f16tof32(uint2(value & 0xffff, value >> 16)); -} -void comp_main() +[RootSignature(ShaderRootSignature)] +[numthreads(64, 1, 1)] +void CS(CSInput csInput) { -#if ENABLE_HDR +#if 0 // butchered the CS so it just outputs the input - uint4 _55 = asuint(_23_m0[2u]); - uint _58 = _55.x + (((gl_LocalInvocationID.x >> 1u) & 7u) | (gl_WorkGroupID.x << 4u)); - uint _59 = ((((gl_LocalInvocationID.x >> 3u) & 6u) | (gl_LocalInvocationID.x & 1u)) | (gl_WorkGroupID.y << 4u)) + _55.y; - uint _458 = _58 + 8u; - uint _489 = _59 + 8u; - - _11[uint2(_58, _59)] = _8.Load(int3(uint2(_58, _59), 0u)); - _11[uint2(_458, _59)] = _8.Load(int3(uint2(_458, _59), 0u)); - _11[uint2(_58, _489)] = _8.Load(int3(uint2(_58, _489), 0u)); - _11[uint2(_458, _489)] = _8.Load(int3(uint2(_458, _489), 0u)); + uint4 _55 = CASData.cas2; + uint ppx = _55.x + (((csInput.SV_GroupThreadID.x >> 1u) & 7u) | (csInput.SV_GroupID.x << 4u)); + uint ppy = ((((csInput.SV_GroupThreadID.x >> 3u) & 6u) | (csInput.SV_GroupThreadID.x & 1u)) | (csInput.SV_GroupID.y << 4u)) + _55.y; + ppx += 8; + ppy += 8; + + ColorOut[uint2(ppx, ppy)] = ColorIn.Load(int3(uint2(ppx, ppy), 0u)); + ColorOut[uint2(ppx, ppy)] = ColorIn.Load(int3(uint2(ppx, ppy), 0u)); + ColorOut[uint2(ppx, ppy)] = ColorIn.Load(int3(uint2(ppx, ppy), 0u)); + ColorOut[uint2(ppx, ppy)] = ColorIn.Load(int3(uint2(ppx, ppy), 0u)); #else - uint4 _55 = asuint(_23_m0[2u]); - uint _58 = _55.x + (((gl_LocalInvocationID.x >> 1u) & 7u) | (gl_WorkGroupID.x << 4u)); - uint _59 = ((((gl_LocalInvocationID.x >> 3u) & 6u) | (gl_LocalInvocationID.x & 1u)) | (gl_WorkGroupID.y << 4u)) + _55.y; - uint4 _69 = asuint(_23_m0[3u]); + + uint4 _55 = CASData.cas2; + + uint _58 = _55.x + (((csInput.SV_GroupThreadID.x >> 1u) & 7u) | (csInput.SV_GroupID.x << 4u)); + + uint2 pp = + uint2(_58, + ((((csInput.SV_GroupThreadID.x >> 3u) & 6u) | (csInput.SV_GroupThreadID.x & 1u)) | (csInput.SV_GroupID.y << 4u)) + _55.y); + + uint4 _69 = CASData.cas3; + uint _70 = _69.x; uint _71 = _69.y; uint _72 = _69.z; uint _73 = _69.w; - uint _74 = _58 << 16u; - uint _76 = uint(int(_74) >> int(16u)); - uint _77 = _59 << 16u; - uint _87 = uint(int(max(min(_76, _72), _70) << 16u) >> int(16u)); - uint _89 = uint(int(max(min(uint(int(_77 + 4294901760u) >> int(16u)), _73), _71) << 16u) >> int(16u)); - float4 _91 = _8.Load(int3(uint2(_87, _89), 0u)); - uint _96 = uint(int(_74 + 4294901760u) >> int(16u)); - uint _105 = uint(int(max(min(uint(int(_77) >> int(16u)), _73), _71) << 16u) >> int(16u)); - float4 _106 = _8.Load(int3(uint2(uint(int(max(min(_96, _72), _70) << 16u) >> int(16u)), _105), 0u)); - float4 _109 = _8.Load(int3(uint2(_87, _105), 0u)); - uint _114 = uint(int(_74 + 65536u) >> int(16u)); - float4 _119 = _8.Load(int3(uint2(uint(int(max(min(_114, _72), _70) << 16u) >> int(16u)), _105), 0u)); - uint _127 = uint(int(max(min(uint(int(_77 + 65536u) >> int(16u)), _73), _71) << 16u) >> int(16u)); - float4 _128 = _8.Load(int3(uint2(_87, _127), 0u)); - uint _134 = uint(int((_58 << 16u) + 524288u) >> int(16u)); - uint _138 = uint(int(max(min(_134, _72), _70) << 16u) >> int(16u)); - float4 _139 = _8.Load(int3(uint2(_138, _89), 0u)); - uint _146 = uint(int(_74 + 458752u) >> int(16u)); - float4 _151 = _8.Load(int3(uint2(uint(int(max(min(_146, _72), _70) << 16u) >> int(16u)), _105), 0u)); - float4 _156 = _8.Load(int3(uint2(_138, _105), 0u)); - uint _163 = uint(int(_74 + 589824u) >> int(16u)); - float4 _168 = _8.Load(int3(uint2(uint(int(max(min(_163, _72), _70) << 16u) >> int(16u)), _105), 0u)); - float4 _173 = _8.Load(int3(uint2(_138, _127), 0u)); - float _193 = exp2(log2((_91.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _194 = exp2(log2((_139.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _213 = exp2(log2((_106.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _214 = exp2(log2((_151.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _233 = exp2(log2((_109.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _234 = exp2(log2((_156.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _253 = exp2(log2((_119.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _254 = exp2(log2((_168.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _273 = exp2(log2((_128.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _274 = exp2(log2((_173.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _280 = isnan(_213) ? _193 : (isnan(_193) ? _213 : min(_193, _213)); - float _281 = isnan(_214) ? _194 : (isnan(_194) ? _214 : min(_194, _214)); - float _282 = isnan(_233) ? _280 : (isnan(_280) ? _233 : min(_280, _233)); - float _283 = isnan(_234) ? _281 : (isnan(_281) ? _234 : min(_281, _234)); - float _284 = isnan(_273) ? _253 : (isnan(_253) ? _273 : min(_253, _273)); - float _285 = isnan(_274) ? _254 : (isnan(_254) ? _274 : min(_254, _274)); - float _286 = isnan(_282) ? _284 : (isnan(_284) ? _282 : min(_284, _282)); - float _287 = isnan(_283) ? _285 : (isnan(_285) ? _283 : min(_285, _283)); - float _288 = isnan(_213) ? _193 : (isnan(_193) ? _213 : max(_193, _213)); - float _289 = isnan(_214) ? _194 : (isnan(_194) ? _214 : max(_194, _214)); - float _290 = isnan(_233) ? _288 : (isnan(_288) ? _233 : max(_288, _233)); - float _291 = isnan(_234) ? _289 : (isnan(_289) ? _234 : max(_289, _234)); - float _292 = isnan(_273) ? _253 : (isnan(_253) ? _273 : max(_253, _273)); - float _293 = isnan(_274) ? _254 : (isnan(_254) ? _274 : max(_254, _274)); - float _294 = isnan(_290) ? _292 : (isnan(_292) ? _290 : max(_292, _290)); - float _295 = isnan(_291) ? _293 : (isnan(_293) ? _291 : max(_293, _291)); - float _299 = 1.0f - _294; - float _300 = 1.0f - _295; - float _303 = (isnan(_299) ? _286 : (isnan(_286) ? _299 : min(_286, _299))) * (1.0f / _294); - float _304 = (isnan(_300) ? _287 : (isnan(_287) ? _300 : min(_287, _300))) * (1.0f / _295); - float _981 = isnan(0.0f) ? _303 : (isnan(_303) ? 0.0f : max(_303, 0.0f)); - float _992 = isnan(0.0f) ? _304 : (isnan(_304) ? 0.0f : max(_304, 0.0f)); - float2 _313 = spvUnpackHalf2x16(asuint(_23_m0[1u]).y & 65535u); - float _314 = _313.x; - float _315 = _314 * sqrt(isnan(1.0f) ? _981 : (isnan(_981) ? 1.0f : min(_981, 1.0f))); - float _316 = _314 * sqrt(isnan(1.0f) ? _992 : (isnan(_992) ? 1.0f : min(_992, 1.0f))); - float _322 = 1.0f / ((_315 * 4.0f) + 1.0f); - float _323 = 1.0f / ((_316 * 4.0f) + 1.0f); - float _329 = ((_316 * (((exp2(_18_m0[1u].w * log2((_151.x + 0.05499267578125f) * 0.9482421875f)) + exp2(_18_m0[1u].w * log2((_139.x + 0.05499267578125f) * 0.9482421875f))) + exp2(_18_m0[1u].w * log2((_168.x + 0.05499267578125f) * 0.9482421875f))) + exp2(_18_m0[1u].w * log2((_173.x + 0.05499267578125f) * 0.9482421875f)))) + exp2(_18_m0[1u].w * log2((_156.x + 0.05499267578125f) * 0.9482421875f))) * _323; - float _1003 = isnan(0.0f) ? _329 : (isnan(_329) ? 0.0f : max(_329, 0.0f)); - float _336 = ((_316 * (((_214 + _194) + _254) + _274)) + _234) * _323; - float _1014 = isnan(0.0f) ? _336 : (isnan(_336) ? 0.0f : max(_336, 0.0f)); - float _343 = ((_316 * (((exp2(log2((_151.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w) + exp2(log2((_139.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_168.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_173.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w))) + exp2(log2((_156.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _323; - float _1025 = isnan(0.0f) ? _343 : (isnan(_343) ? 0.0f : max(_343, 0.0f)); - float _345 = isnan(0.00100040435791015625f) ? _18_m0[1u].w : (isnan(_18_m0[1u].w) ? 0.00100040435791015625f : max(_18_m0[1u].w, 0.00100040435791015625f)); - if ((_58 <= _55.z) && (_59 <= _55.w)) + + uint _87 = max(min(pp.x, _72), _70); + uint _89 = max(min(pp.y - 1, _73), _71); + float3 _91f = ColorIn.Load(int3( _87, _89, 0)).rgb; + + uint _105 = max(min(pp.y, _73), _71); + uint _107 = max(min(pp.x - 1, _72), _70); + float3 _106f = ColorIn.Load(int3(_107, _105, 0)).rgb; + float3 _109f = ColorIn.Load(int3( _87, _105, 0)).rgb; + + uint _118 = max(min(pp.x + 1, _72), _70); + float3 _119f = ColorIn.Load(int3(_118, _105, 0)).rgb; + + uint _127 = max(min(pp.y + 1, _73), _71); + float3 _128f = ColorIn.Load(int3( _87, _127, 0)).rgb; + + uint _138 = max(min(pp.x + 8, _72), _70); + float3 _139f = ColorIn.Load(int3(_138, _89, 0)).rgb; + + uint _150 = max(min(pp.x + 7, _72), _70); + float3 _151f = ColorIn.Load(int3(_150, _105, 0)).rgb; + float3 _156f = ColorIn.Load(int3(_138, _105, 0)).rgb; + + uint _167 = max(min(pp.x + 9, _72), _70); + float3 _168f = ColorIn.Load(int3(_167, _105, 0)).rgb; + float3 _173f = ColorIn.Load(int3(_138, _127, 0)).rgb; + + _91f = BT709_To_BT2020( _91f); + _139f = BT709_To_BT2020(_139f); + _106f = BT709_To_BT2020(_106f); + _151f = BT709_To_BT2020(_151f); + _109f = BT709_To_BT2020(_109f); + _156f = BT709_To_BT2020(_156f); + _119f = BT709_To_BT2020(_119f); + _168f = BT709_To_BT2020(_168f); + _128f = BT709_To_BT2020(_128f); + _173f = BT709_To_BT2020(_173f); + + // worst case is red and blue at 0 and green high enough so that the luminance is HDR_MAX_OUTPUT_NITS + static const float normalizationFactor = (HDR_MAX_OUTPUT_NITS / 80.f) / 0.678f; + + half3 _91 = saturate( _91f / normalizationFactor); + half3 _139 = saturate(_139f / normalizationFactor); + half3 _106 = saturate(_106f / normalizationFactor); + half3 _151 = saturate(_151f / normalizationFactor); + half3 _109 = saturate(_109f / normalizationFactor); + half3 _156 = saturate(_156f / normalizationFactor); + half3 _119 = saturate(_119f / normalizationFactor); + half3 _168 = saturate(_168f / normalizationFactor); + half3 _128 = saturate(_128f / normalizationFactor); + half3 _173 = saturate(_173f / normalizationFactor); + + half _193 = _91.y; + half _194 = _139.y; + half _213 = _106.y; + half _214 = _151.y; + half _233 = _109.y; + half _234 = _156.y; + half _253 = _119.y; + half _254 = _168.y; + half _273 = _128.y; + half _274 = _173.y; + + half _280 = min(_193, _213); + half _281 = min(_194, _214); + half _282 = min(_280, _233); + half _283 = min(_281, _234); + half _284 = min(_253, _273); + half _285 = min(_254, _274); + half _286 = min(_284, _282); + half _287 = min(_285, _283); + + half _288 = max(_193, _213); + half _289 = max(_194, _214); + half _290 = max(_288, _233); + half _291 = max(_289, _234); + half _292 = max(_253, _273); + half _293 = max(_254, _274); + half _294 = max(_292, _290); + half _295 = max(_293, _291); + + half _299 = 1.h - _294; + half _300 = 1.h - _295; + + half _303 = min(_286, _299) * (1.h / _294); + half _304 = min(_287, _300) * (1.h / _295); + + half _981 = saturate(_303); + half _992 = saturate(_304); + + half hSharp = f16tof32(CASData.cas1.y & 0xFFFF); + + half _315 = hSharp * sqrt(_981); + half _316 = hSharp * sqrt(_992); + + half _322 = 1.h / ((_315 * 4.h) + 1.h); + half _323 = 1.h / ((_316 * 4.h) + 1.h); + + half3 _329 = (((_151 + _139 + _168 + _173) * _316) + _156) * _323; + _329 = saturate(_329); + float3 _1003 = _329 * normalizationFactor; + + float3 o2 = BT2020_To_BT709(_1003); + + if ((pp.x <= _55.z) && (pp.y <= _55.w)) { - float _388 = (((((exp2(log2((_106.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w) + exp2(log2((_91.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_119.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_128.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _315) + exp2(log2((_109.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _322; - float _1041 = isnan(0.0f) ? _388 : (isnan(_388) ? 0.0f : max(_388, 0.0f)); - float _391 = 1.0f / _345; - float _396 = (exp2(_391 * log2(isnan(1.0f) ? _1041 : (isnan(_1041) ? 1.0f : min(_1041, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - float _404 = ((_315 * (((_213 + _193) + _253) + _273)) + _233) * _322; - float _1057 = isnan(0.0f) ? _404 : (isnan(_404) ? 0.0f : max(_404, 0.0f)); - float _410 = (exp2(_391 * log2(isnan(1.0f) ? _1057 : (isnan(_1057) ? 1.0f : min(_1057, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - float _447 = (((((exp2(log2((_106.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w) + exp2(log2((_91.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_119.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_128.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _315) + exp2(log2((_109.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _322; - float _1073 = isnan(0.0f) ? _447 : (isnan(_447) ? 0.0f : max(_447, 0.0f)); - float _453 = (exp2(_391 * log2(isnan(1.0f) ? _1073 : (isnan(_1073) ? 1.0f : min(_1073, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - _11[uint2(_58, _59)] = float4(isnan(0.0f) ? _453 : (isnan(_453) ? 0.0f : max(_453, 0.0f)), isnan(0.0f) ? _410 : (isnan(_410) ? 0.0f : max(_410, 0.0f)), isnan(0.0f) ? _396 : (isnan(_396) ? 0.0f : max(_396, 0.0f)), 1.0f); + half3 _388 = (((_106 + _91 + _119 + _128) * _315) + _109) * _322; + _388 = saturate(_388); + float3 _396 = _388 * normalizationFactor; + + float3 o1 = BT2020_To_BT709(_396); + + ColorOut[pp] = float4(o1, 1.f); } - uint _458 = _58 + 8u; - uint4 _461 = asuint(_23_m0[2u]); - if ((_458 <= _461.z) && (_59 <= _461.w)) + + pp.x += 8; + + if ((pp.x <= _55.z) && (pp.y <= _55.w)) { - float _468 = 1.0f / _345; - float _472 = (exp2(_468 * log2(isnan(1.0f) ? _1025 : (isnan(_1025) ? 1.0f : min(_1025, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - float _478 = (exp2(_468 * log2(isnan(1.0f) ? _1014 : (isnan(_1014) ? 1.0f : min(_1014, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - float _484 = (exp2(_468 * log2(isnan(1.0f) ? _1003 : (isnan(_1003) ? 1.0f : min(_1003, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - _11[uint2(_458, _59)] = float4(isnan(0.0f) ? _484 : (isnan(_484) ? 0.0f : max(_484, 0.0f)), isnan(0.0f) ? _478 : (isnan(_478) ? 0.0f : max(_478, 0.0f)), isnan(0.0f) ? _472 : (isnan(_472) ? 0.0f : max(_472, 0.0f)), 1.0f); + ColorOut[pp] = float4(o2, 1.f); } - uint _489 = _59 + 8u; - uint4 _492 = asuint(_23_m0[3u]); - uint _493 = _492.x; - uint _494 = _492.y; - uint _495 = _492.z; - uint _496 = _492.w; - uint _497 = _489 << 16u; - uint _505 = uint(int(max(min(_76, _495), _493) << 16u) >> int(16u)); - uint _507 = uint(int(max(min(uint(int(_497 + 4294901760u) >> int(16u)), _496), _494) << 16u) >> int(16u)); - float4 _509 = _8.Load(int3(uint2(_505, _507), 0u)); - uint _520 = uint(int(max(min(uint(int(_497) >> int(16u)), _496), _494) << 16u) >> int(16u)); - float4 _521 = _8.Load(int3(uint2(uint(int(max(min(_96, _495), _493) << 16u) >> int(16u)), _520), 0u)); - float4 _524 = _8.Load(int3(uint2(_505, _520), 0u)); - float4 _531 = _8.Load(int3(uint2(uint(int(max(min(_114, _495), _493) << 16u) >> int(16u)), _520), 0u)); - uint _539 = uint(int(max(min(uint(int(_497 + 65536u) >> int(16u)), _496), _494) << 16u) >> int(16u)); - float4 _540 = _8.Load(int3(uint2(_505, _539), 0u)); - uint _546 = uint(int(max(min(_134, _495), _493) << 16u) >> int(16u)); - float4 _547 = _8.Load(int3(uint2(_546, _507), 0u)); - float4 _556 = _8.Load(int3(uint2(uint(int(max(min(_146, _495), _493) << 16u) >> int(16u)), _520), 0u)); - float4 _561 = _8.Load(int3(uint2(_546, _520), 0u)); - float4 _570 = _8.Load(int3(uint2(uint(int(max(min(_163, _495), _493) << 16u) >> int(16u)), _520), 0u)); - float4 _575 = _8.Load(int3(uint2(_546, _539), 0u)); - float _596 = exp2(log2((_509.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _597 = exp2(log2((_547.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _616 = exp2(log2((_521.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _617 = exp2(log2((_556.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _636 = exp2(log2((_524.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _637 = exp2(log2((_561.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _656 = exp2(log2((_531.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _657 = exp2(log2((_570.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _676 = exp2(log2((_540.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _677 = exp2(log2((_575.y + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w); - float _683 = isnan(_616) ? _596 : (isnan(_596) ? _616 : min(_596, _616)); - float _684 = isnan(_617) ? _597 : (isnan(_597) ? _617 : min(_597, _617)); - float _685 = isnan(_636) ? _683 : (isnan(_683) ? _636 : min(_683, _636)); - float _686 = isnan(_637) ? _684 : (isnan(_684) ? _637 : min(_684, _637)); - float _687 = isnan(_676) ? _656 : (isnan(_656) ? _676 : min(_656, _676)); - float _688 = isnan(_677) ? _657 : (isnan(_657) ? _677 : min(_657, _677)); - float _689 = isnan(_685) ? _687 : (isnan(_687) ? _685 : min(_687, _685)); - float _690 = isnan(_686) ? _688 : (isnan(_688) ? _686 : min(_688, _686)); - float _691 = isnan(_616) ? _596 : (isnan(_596) ? _616 : max(_596, _616)); - float _692 = isnan(_617) ? _597 : (isnan(_597) ? _617 : max(_597, _617)); - float _693 = isnan(_636) ? _691 : (isnan(_691) ? _636 : max(_691, _636)); - float _694 = isnan(_637) ? _692 : (isnan(_692) ? _637 : max(_692, _637)); - float _695 = isnan(_676) ? _656 : (isnan(_656) ? _676 : max(_656, _676)); - float _696 = isnan(_677) ? _657 : (isnan(_657) ? _677 : max(_657, _677)); - float _697 = isnan(_693) ? _695 : (isnan(_695) ? _693 : max(_695, _693)); - float _698 = isnan(_694) ? _696 : (isnan(_696) ? _694 : max(_696, _694)); - float _701 = 1.0f - _697; - float _702 = 1.0f - _698; - float _705 = (isnan(_701) ? _689 : (isnan(_689) ? _701 : min(_689, _701))) * (1.0f / _697); - float _706 = (isnan(_702) ? _690 : (isnan(_690) ? _702 : min(_690, _702))) * (1.0f / _698); - float _1194 = isnan(0.0f) ? _705 : (isnan(_705) ? 0.0f : max(_705, 0.0f)); - float _1205 = isnan(0.0f) ? _706 : (isnan(_706) ? 0.0f : max(_706, 0.0f)); - float _711 = _314 * sqrt(isnan(1.0f) ? _1194 : (isnan(_1194) ? 1.0f : min(_1194, 1.0f))); - float _712 = _314 * sqrt(isnan(1.0f) ? _1205 : (isnan(_1205) ? 1.0f : min(_1205, 1.0f))); - float _717 = 1.0f / ((_711 * 4.0f) + 1.0f); - float _718 = 1.0f / ((_712 * 4.0f) + 1.0f); - float _724 = ((_712 * (((exp2(_18_m0[1u].w * log2((_556.x + 0.05499267578125f) * 0.9482421875f)) + exp2(_18_m0[1u].w * log2((_547.x + 0.05499267578125f) * 0.9482421875f))) + exp2(_18_m0[1u].w * log2((_570.x + 0.05499267578125f) * 0.9482421875f))) + exp2(_18_m0[1u].w * log2((_575.x + 0.05499267578125f) * 0.9482421875f)))) + exp2(_18_m0[1u].w * log2((_561.x + 0.05499267578125f) * 0.9482421875f))) * _718; - float _1216 = isnan(0.0f) ? _724 : (isnan(_724) ? 0.0f : max(_724, 0.0f)); - float _731 = ((_712 * (((_617 + _597) + _657) + _677)) + _637) * _718; - float _1227 = isnan(0.0f) ? _731 : (isnan(_731) ? 0.0f : max(_731, 0.0f)); - float _738 = ((_712 * (((exp2(log2((_556.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w) + exp2(log2((_547.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_570.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_575.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w))) + exp2(log2((_561.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _718; - float _1238 = isnan(0.0f) ? _738 : (isnan(_738) ? 0.0f : max(_738, 0.0f)); - uint4 _742 = asuint(_23_m0[2u]); - if ((_58 <= _742.z) && (_489 <= _742.w)) + + pp.y += 8; + + uint _507 = max(min(pp.y - 1, _73), _71); + float3 _509f = ColorIn.Load(int3( _87, _507, 0)).rgb; + + uint _520 = max(min(pp.y, _73), _71); + float3 _521f = ColorIn.Load(int3(_107, _520, 0)).rgb; + float3 _524f = ColorIn.Load(int3( _87, _520, 0)).rgb; + float3 _531f = ColorIn.Load(int3(_118, _520, 0)).rgb; + + uint _539 = max(min(pp.y + 1, _73), _71); + float3 _540f = ColorIn.Load(int3( _87, _539, 0)).rgb; + float3 _547f = ColorIn.Load(int3(_138, _507, 0)).rgb; + float3 _556f = ColorIn.Load(int3(_150, _520, 0)).rgb; + float3 _561f = ColorIn.Load(int3(_138, _520, 0)).rgb; + float3 _570f = ColorIn.Load(int3(_167, _520, 0)).rgb; + float3 _575f = ColorIn.Load(int3(_138, _539, 0)).rgb; + + _509f = BT709_To_BT2020(_509f); + _547f = BT709_To_BT2020(_547f); + _521f = BT709_To_BT2020(_521f); + _556f = BT709_To_BT2020(_556f); + _524f = BT709_To_BT2020(_524f); + _561f = BT709_To_BT2020(_561f); + _531f = BT709_To_BT2020(_531f); + _570f = BT709_To_BT2020(_570f); + _540f = BT709_To_BT2020(_540f); + _575f = BT709_To_BT2020(_575f); + + half3 _509 = saturate(_509f / normalizationFactor); + half3 _547 = saturate(_547f / normalizationFactor); + half3 _521 = saturate(_521f / normalizationFactor); + half3 _556 = saturate(_556f / normalizationFactor); + half3 _524 = saturate(_524f / normalizationFactor); + half3 _561 = saturate(_561f / normalizationFactor); + half3 _531 = saturate(_531f / normalizationFactor); + half3 _570 = saturate(_570f / normalizationFactor); + half3 _540 = saturate(_540f / normalizationFactor); + half3 _575 = saturate(_575f / normalizationFactor); + + half _596 = _509.y; + half _597 = _547.y; + half _616 = _521.y; + half _617 = _556.y; + half _636 = _524.y; + half _637 = _561.y; + half _656 = _531.y; + half _657 = _570.y; + half _676 = _540.y; + half _677 = _575.y; + + half _683 = min(_596, _616); + half _684 = min(_597, _617); + half _685 = min(_683, _636); + half _686 = min(_684, _637); + half _687 = min(_656, _676); + half _688 = min(_657, _677); + half _689 = min(_687, _685); + half _690 = min(_688, _686); + + half _691 = max(_596, _616); + half _692 = max(_597, _617); + half _693 = max(_691, _636); + half _694 = max(_692, _637); + half _695 = max(_656, _676); + half _696 = max(_657, _677); + half _697 = max(_695, _693); + half _698 = max(_696, _694); + + half _701 = 1.h - _697; + half _702 = 1.h - _698; + + half _705 = min(_689, _701) * (1.h / _697); + half _706 = min(_690, _702) * (1.h / _698); + + half _1194 = saturate(_705); + half _1205 = saturate(_706); + + half _711 = hSharp * sqrt(_1194); + half _712 = hSharp * sqrt(_1205); + + half _717 = 1.h / ((_711 * 4.h) + 1.h); + half _718 = 1.h / ((_712 * 4.h) + 1.h); + + half3 _724 = (((_556 + _547 + _570 + _575) * _712) + _561) * _718; + _724 = saturate(_724); + float3 _1216 = _724 * normalizationFactor; + + float3 o4 = BT2020_To_BT709(_1216); + + if ((pp.x <= _55.z) && (pp.y <= _55.w)) { - float _783 = (((((exp2(log2((_521.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w) + exp2(log2((_509.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_531.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_540.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _711) + exp2(log2((_524.z + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _717; - float _1249 = isnan(0.0f) ? _783 : (isnan(_783) ? 0.0f : max(_783, 0.0f)); - float _786 = 1.0f / _345; - float _790 = (exp2(_786 * log2(isnan(1.0f) ? _1249 : (isnan(_1249) ? 1.0f : min(_1249, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - float _797 = ((_711 * (((_616 + _596) + _656) + _676)) + _636) * _717; - float _1265 = isnan(0.0f) ? _797 : (isnan(_797) ? 0.0f : max(_797, 0.0f)); - float _803 = (exp2(_786 * log2(isnan(1.0f) ? _1265 : (isnan(_1265) ? 1.0f : min(_1265, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - float _840 = (((((exp2(log2((_521.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w) + exp2(log2((_509.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_531.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) + exp2(log2((_540.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _711) + exp2(log2((_524.x + 0.05499267578125f) * 0.9482421875f) * _18_m0[1u].w)) * _717; - float _1281 = isnan(0.0f) ? _840 : (isnan(_840) ? 0.0f : max(_840, 0.0f)); - float _846 = (exp2(_786 * log2(isnan(1.0f) ? _1281 : (isnan(_1281) ? 1.0f : min(_1281, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - _11[uint2(_58, _489)] = float4(isnan(0.0f) ? _846 : (isnan(_846) ? 0.0f : max(_846, 0.0f)), isnan(0.0f) ? _803 : (isnan(_803) ? 0.0f : max(_803, 0.0f)), isnan(0.0f) ? _790 : (isnan(_790) ? 0.0f : max(_790, 0.0f)), 1.0f); + half3 _783 = (((_521 + _509 + _531 + _540) * _711) + _524) * _717; + _783 = saturate(_783); + float3 _790 = _783 * normalizationFactor; + + float3 o3 = BT2020_To_BT709(_790); + + ColorOut[uint2(_58, pp.y)] = float4(o3, 1.f); } - uint4 _853 = asuint(_23_m0[2u]); - if ((_458 <= _853.z) && (_489 <= _853.w)) + + if ((pp.x <= _55.z) && (pp.y <= _55.w)) { - float _860 = 1.0f / _345; - float _864 = (exp2(_860 * log2(isnan(1.0f) ? _1238 : (isnan(_1238) ? 1.0f : min(_1238, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - float _870 = (exp2(_860 * log2(isnan(1.0f) ? _1227 : (isnan(_1227) ? 1.0f : min(_1227, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - float _876 = (exp2(_860 * log2(isnan(1.0f) ? _1216 : (isnan(_1216) ? 1.0f : min(_1216, 1.0f)))) * 1.0546875f) + (-0.05499267578125f); - _11[uint2(_458, _489)] = float4(isnan(0.0f) ? _876 : (isnan(_876) ? 0.0f : max(_876, 0.0f)), isnan(0.0f) ? _870 : (isnan(_870) ? 0.0f : max(_870, 0.0f)), isnan(0.0f) ? _864 : (isnan(_864) ? 0.0f : max(_864, 0.0f)), 1.0f); + ColorOut[pp] = float4(o4, 1.f); } -#endif -} -[RootSignature(ShaderRootSignature)] -[numthreads(64, 1, 1)] -void main(SPIRV_Cross_Input stage_input) -{ - gl_WorkGroupID = stage_input.gl_WorkGroupID; - gl_LocalInvocationID = stage_input.gl_LocalInvocationID; - comp_main(); +#endif } diff --git a/shaders/compile_all_shaders.bat b/shaders/compile_all_shaders.bat index 7c6f1e0e..2189203a 100644 --- a/shaders/compile_all_shaders.bat +++ b/shaders/compile_all_shaders.bat @@ -75,7 +75,7 @@ call :errorlevel_check call :errorlevel_check "%dxc%" "ContrastAdaptiveSharpening\ContrastAdaptiveSharpening_100FF94_cs.hlsl" ^ - -T cs_6_6 -E main -Fo ^ + -T cs_6_6 -E CS -enable-16bit-types -Wno-conversion -Fo ^ "%target_folder%ContrastAdaptiveSharpening\ContrastAdaptiveSharpening_100FF94_cs.bin" call :errorlevel_check diff --git a/shaders/structs.hlsl b/shaders/structs.hlsl index cdd8fb17..968e881c 100644 --- a/shaders/structs.hlsl +++ b/shaders/structs.hlsl @@ -184,3 +184,13 @@ struct PushConstantWrapper_ColorGradingMerge float neutralLUTPercentage; }; //ColorGradingMerge end + +//ContrastAdaptiveSharpening start +struct ContrastAdaptiveSharpeningData +{ + uint4 cas0; + uint4 cas1; + uint4 cas2; + uint4 cas3; +}; +//ContrastAdaptiveSharpening end From 5e8691f84498119c8dc25fe588f9760ad2a1d442 Mon Sep 17 00:00:00 2001 From: Lilium Date: Fri, 6 Oct 2023 20:10:58 +0200 Subject: [PATCH 080/270] add handling SDR with gamma 2.2 plus fix the normalizationFactor being wrong --- .../ContrastAdaptiveSharpening_cs.hlsl | 99 ++++++++++++++++--- 1 file changed, 86 insertions(+), 13 deletions(-) diff --git a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl index b4d00c59..184cff2d 100644 --- a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl +++ b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl @@ -3,13 +3,12 @@ #include "RootSignature.hlsl" // this is CAS upscaling in FP16 with FFX_CAS_USE_PRECISE_MATH [100FF94] -// other variants are also available, which are: +// other variants are also available but seemingly unused, which are: // - CAS upscaling in FP32 [200FF94] // - CAS upscaling in FP16 with FFX_CAS_USE_PRECISE_MATH and FFX_CAS_BETTER_DIAGONALS [300FF94] // - just CAS sharpening in FP32 [FF94] -// TODO make full process in half not just after csp conversion? -// needs matrices in half +// TODO: make full process in half not just after csp conversion? (needs matrices in half) // don't need it //cbuffer _16_18 : register(b0, space6) @@ -102,6 +101,8 @@ void CS(CSInput csInput) float3 _168f = ColorIn.Load(int3(_167, _105, 0)).rgb; float3 _173f = ColorIn.Load(int3(_138, _127, 0)).rgb; +#if ENABLE_HDR + _91f = BT709_To_BT2020( _91f); _139f = BT709_To_BT2020(_139f); _106f = BT709_To_BT2020(_106f); @@ -113,8 +114,10 @@ void CS(CSInput csInput) _128f = BT709_To_BT2020(_128f); _173f = BT709_To_BT2020(_173f); - // worst case is red and blue at 0 and green high enough so that the luminance is HDR_MAX_OUTPUT_NITS - static const float normalizationFactor = (HDR_MAX_OUTPUT_NITS / 80.f) / 0.678f; + // since blue is the least contributing in terms of luminance + // the worst case is red and green at 0 and blue high enough so that the luminance is HDR_MAX_OUTPUT_NITS + // TODO: use more accurate value than just the K factor from the YCbCr<->RGB transform + static const float normalizationFactor = (HDR_MAX_OUTPUT_NITS / 80.f) / 0.0593f; half3 _91 = saturate( _91f / normalizationFactor); half3 _139 = saturate(_139f / normalizationFactor); @@ -127,6 +130,32 @@ void CS(CSInput csInput) half3 _128 = saturate(_128f / normalizationFactor); half3 _173 = saturate(_173f / normalizationFactor); +#else // ENABLE_HDR + + half3 _91 = _91f; + half3 _139 = _139f; + half3 _106 = _106f; + half3 _151 = _151f; + half3 _109 = _109f; + half3 _156 = _156f; + half3 _119 = _119f; + half3 _168 = _168f; + half3 _128 = _128f; + half3 _173 = _173f; + + _91 = saturate(pow( _91, 2.2f)); + _139 = saturate(pow(_139, 2.2f)); + _106 = saturate(pow(_106, 2.2f)); + _151 = saturate(pow(_151, 2.2f)); + _109 = saturate(pow(_109, 2.2f)); + _156 = saturate(pow(_156, 2.2f)); + _119 = saturate(pow(_119, 2.2f)); + _168 = saturate(pow(_168, 2.2f)); + _128 = saturate(pow(_128, 2.2f)); + _173 = saturate(pow(_173, 2.2f)); + +#endif // ENABLE_HDR + half _193 = _91.y; half _194 = _139.y; half _213 = _106.y; @@ -175,26 +204,34 @@ void CS(CSInput csInput) half3 _329 = (((_151 + _139 + _168 + _173) * _316) + _156) * _323; _329 = saturate(_329); +#if ENABLE_HDR float3 _1003 = _329 * normalizationFactor; - float3 o2 = BT2020_To_BT709(_1003); + float3 colorOut2 = BT2020_To_BT709(_1003); +#else // ENABLE_HDR + float3 colorOut2 = pow(_329, 1.f / 2.2f); +#endif // ENABLE_HDR if ((pp.x <= _55.z) && (pp.y <= _55.w)) { half3 _388 = (((_106 + _91 + _119 + _128) * _315) + _109) * _322; _388 = saturate(_388); +#if ENABLE_HDR float3 _396 = _388 * normalizationFactor; - float3 o1 = BT2020_To_BT709(_396); + float3 colorOut1 = BT2020_To_BT709(_396); +#else // ENABLE_HDR + float3 colorOut1 = pow(_388, 1.f / 2.2f); +#endif // ENABLE_HDR - ColorOut[pp] = float4(o1, 1.f); + ColorOut[pp] = float4(colorOut1, 1.f); } pp.x += 8; if ((pp.x <= _55.z) && (pp.y <= _55.w)) { - ColorOut[pp] = float4(o2, 1.f); + ColorOut[pp] = float4(colorOut2, 1.f); } pp.y += 8; @@ -215,6 +252,8 @@ void CS(CSInput csInput) float3 _570f = ColorIn.Load(int3(_167, _520, 0)).rgb; float3 _575f = ColorIn.Load(int3(_138, _539, 0)).rgb; +#if ENABLE_HDR + _509f = BT709_To_BT2020(_509f); _547f = BT709_To_BT2020(_547f); _521f = BT709_To_BT2020(_521f); @@ -237,6 +276,32 @@ void CS(CSInput csInput) half3 _540 = saturate(_540f / normalizationFactor); half3 _575 = saturate(_575f / normalizationFactor); +#else // ENABLE_HDR + + half3 _509 = _509f; + half3 _547 = _547f; + half3 _521 = _521f; + half3 _556 = _556f; + half3 _524 = _524f; + half3 _561 = _561f; + half3 _531 = _531f; + half3 _570 = _570f; + half3 _540 = _540f; + half3 _575 = _575f; + + _509 = saturate(pow(_509f, 2.2f)); + _547 = saturate(pow(_547f, 2.2f)); + _521 = saturate(pow(_521f, 2.2f)); + _556 = saturate(pow(_556f, 2.2f)); + _524 = saturate(pow(_524f, 2.2f)); + _561 = saturate(pow(_561f, 2.2f)); + _531 = saturate(pow(_531f, 2.2f)); + _570 = saturate(pow(_570f, 2.2f)); + _540 = saturate(pow(_540f, 2.2f)); + _575 = saturate(pow(_575f, 2.2f)); + +#endif // ENABLE_HDR + half _596 = _509.y; half _597 = _547.y; half _616 = _521.y; @@ -283,24 +348,32 @@ void CS(CSInput csInput) half3 _724 = (((_556 + _547 + _570 + _575) * _712) + _561) * _718; _724 = saturate(_724); +#if ENABLE_HDR float3 _1216 = _724 * normalizationFactor; - float3 o4 = BT2020_To_BT709(_1216); + float3 colorOut4 = BT2020_To_BT709(_1216); +#else // ENABLE_HDR + float3 colorOut4 = pow(_724, 1.f / 2.2f); +#endif // ENABLE_HDR if ((pp.x <= _55.z) && (pp.y <= _55.w)) { half3 _783 = (((_521 + _509 + _531 + _540) * _711) + _524) * _717; _783 = saturate(_783); +#if ENABLE_HDR float3 _790 = _783 * normalizationFactor; - float3 o3 = BT2020_To_BT709(_790); + float3 colorOut3 = BT2020_To_BT709(_790); +#else // ENABLE_HDR + float3 colorOut3 = pow(_783, 1.f / 2.2f); +#endif // ENABLE_HDR - ColorOut[uint2(_58, pp.y)] = float4(o3, 1.f); + ColorOut[uint2(_58, pp.y)] = float4(colorOut3, 1.f); } if ((pp.x <= _55.z) && (pp.y <= _55.w)) { - ColorOut[pp] = float4(o4, 1.f); + ColorOut[pp] = float4(colorOut4, 1.f); } #endif From c99667a7ce9248b1fa32520acf0bc32463ecf315 Mon Sep 17 00:00:00 2001 From: Lilium Date: Fri, 6 Oct 2023 20:48:57 +0200 Subject: [PATCH 081/270] add ability to add custom compilation paramters to "compile_all_shaders.ps1" and implement it for the CAS shader --- shaders/compile_all_shaders.ps1 | 35 ++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/shaders/compile_all_shaders.ps1 b/shaders/compile_all_shaders.ps1 index f5ce4bde..d59ceb16 100644 --- a/shaders/compile_all_shaders.ps1 +++ b/shaders/compile_all_shaders.ps1 @@ -19,13 +19,13 @@ $main = Compile-Shader -Type "cs" -TechniqueName "ColorGradingMerge" -TechniqueId "FF81" #Compile-Shader -Type "cs" -TechniqueName "ContrastAdaptiveSharpening" -TechniqueId "FF94" -Entry "main" - Compile-Shader -Type "cs" -TechniqueName "ContrastAdaptiveSharpening" -TechniqueId "100FF94" -Entry "main" -Defines "USE_PACKED_MATH" - + Compile-Shader -Type "cs" -TechniqueName "ContrastAdaptiveSharpening" -TechniqueId "100FF94" -Defines "USE_PACKED_MATH" -AdditionalParams "-enable-16bit-types", "-Wno-conversion" + Compile-Shader -Type "ps" -TechniqueName "PostSharpen" -TechniqueId "FF9A" -Entry "main" Compile-Shader -Type "ps" -TechniqueName "ScaleformComposite" -TechniqueId "FFAA" - Compile-Shader -Type "ps" -TechniqueName "BinkMovie" -TechniqueId "FFAB" + Compile-Shader -Type "ps" -TechniqueName "BinkMovie" -TechniqueId "FFAB" } @@ -55,7 +55,7 @@ function Run-DXC { if ($process.ExitCode -ne 0) { Write-Error "An error occurred during shader compilation:`n$stderr" } - + if ($stdout.Length -gt 0) { Write-Host $stdout } @@ -65,18 +65,21 @@ function Compile-Shader { param ( [Parameter(Mandatory = $true)] [string]$Type, - + [Parameter(Mandatory = $true)] [string]$TechniqueName, - + [Parameter(Mandatory = $true)] [string]$TechniqueId, [Parameter(Mandatory = $false)] [string]$Entry = $Type.ToUpper(), - + + [parameter(Mandatory = $false)] + [array]$Defines, + [parameter(Mandatory = $false)] - [array]$Defines + [array]$AdditionalParams ) $inputHlslName = "${TechniqueName}_${Type}.hlsl" @@ -91,22 +94,26 @@ function Compile-Shader { # Build the shader in its staging directory $args = "`"${inputHlslPath}`" -Fo `"${stagedBinPath}`" -T ${Type}_6_6 -E ${Entry} " - + if ($ShaderOutputEmbedPDB -eq $true) { $args = $args + "-Qembed_debug -Zi " } - + foreach ($define in $Defines) { $args = $args + "-D ${define} " } - + + foreach ($param in $AdditionalParams) { + $args = $args + "${param} " + } + Run-DXC -Arguments $args - + # Extract and strip away the DXIL root signature # TODO: Can extractrootsignature and Qstrip_rootsignature be used in the same operation? Run-DXC -Arguments "-dumpbin `"${stagedBinPath}`" -extractrootsignature -Fo `"${stagedSigPath}`"" Run-DXC -Arguments "-dumpbin `"${stagedBinPath}`" -Qstrip_rootsignature -Fo `"${stagedBinPath}`"" - + # Move the resulting bins to the game directory. Move-Item is to avoid partial reads when live shader editing # is enabled. New-Item -Force -ItemType Directory -Path "${ShaderOutputDirectory}\${TechniqueName}" | Out-Null @@ -120,4 +127,4 @@ try { catch { Write-Host -ForegroundColor Red $_ Pause -} \ No newline at end of file +} From 390f42cc81d211b2603fa474c18581fa993cd6d9 Mon Sep 17 00:00:00 2001 From: ersh1 <16157893+ersh1@users.noreply.github.com> Date: Sat, 7 Oct 2023 00:24:47 +0200 Subject: [PATCH 082/270] added more settings; registered a settings overlay in reshade as a reshade plugin for dev convenience --- Plugin/dist/NativeHDR.toml | 8 + Plugin/include/imconfig.h | 122 + Plugin/include/imgui.h | 3459 +++++++++++++++++ Plugin/include/reshade/reshade.hpp | 346 ++ Plugin/include/reshade/reshade_api.hpp | 726 ++++ Plugin/include/reshade/reshade_api_device.hpp | 1017 +++++ Plugin/include/reshade/reshade_api_format.hpp | 460 +++ .../include/reshade/reshade_api_pipeline.hpp | 1001 +++++ .../include/reshade/reshade_api_resource.hpp | 528 +++ Plugin/include/reshade/reshade_events.hpp | 1696 ++++++++ Plugin/include/reshade/reshade_overlay.hpp | 870 +++++ Plugin/src/Hooks.cpp | 56 +- Plugin/src/Hooks.h | 2 - Plugin/src/PCH.h | 4 + Plugin/src/Settings.cpp | 6 + Plugin/src/Settings.h | 89 +- Plugin/src/main.cpp | 64 +- shaders/shared.hlsl | 9 +- 18 files changed, 10412 insertions(+), 51 deletions(-) create mode 100644 Plugin/include/imconfig.h create mode 100644 Plugin/include/imgui.h create mode 100644 Plugin/include/reshade/reshade.hpp create mode 100644 Plugin/include/reshade/reshade_api.hpp create mode 100644 Plugin/include/reshade/reshade_api_device.hpp create mode 100644 Plugin/include/reshade/reshade_api_format.hpp create mode 100644 Plugin/include/reshade/reshade_api_pipeline.hpp create mode 100644 Plugin/include/reshade/reshade_api_resource.hpp create mode 100644 Plugin/include/reshade/reshade_events.hpp create mode 100644 Plugin/include/reshade/reshade_overlay.hpp diff --git a/Plugin/dist/NativeHDR.toml b/Plugin/dist/NativeHDR.toml index 76594557..f934b65f 100644 --- a/Plugin/dist/NativeHDR.toml +++ b/Plugin/dist/NativeHDR.toml @@ -1,10 +1,18 @@ +[Dev] +DevSetting01 = 0.0 +DevSetting02 = 0.0 + [HDR] GamePaperWhite = 200.0 PeakBrightness = 1000.0 +Saturation = 50.0 UIPaperWhite = 200.0 [Main] +ColorGradingStrength = 100.0 +Contrast = 50.0 DisplayMode = 2 +LUTCorrectionStrength = 100.0 [RenderTargets] RenderTargetsToUpgrade = [ diff --git a/Plugin/include/imconfig.h b/Plugin/include/imconfig.h new file mode 100644 index 00000000..876cf32f --- /dev/null +++ b/Plugin/include/imconfig.h @@ -0,0 +1,122 @@ +//----------------------------------------------------------------------------- +// COMPILE-TIME OPTIONS FOR DEAR IMGUI +// Runtime options (clipboard callbacks, enabling various features, etc.) can generally be set via the ImGuiIO structure. +// You can use ImGui::SetAllocatorFunctions() before calling ImGui::CreateContext() to rewire memory allocation functions. +//----------------------------------------------------------------------------- +// A) You may edit imconfig.h (and not overwrite it when updating Dear ImGui, or maintain a patch/rebased branch with your modifications to it) +// B) or '#define IMGUI_USER_CONFIG "my_imgui_config.h"' in your project and then add directives in your own file without touching this template. +//----------------------------------------------------------------------------- +// You need to make sure that configuration settings are defined consistently _everywhere_ Dear ImGui is used, which include the imgui*.cpp +// files but also _any_ of your code that uses Dear ImGui. This is because some compile-time options have an affect on data structures. +// Defining those options in imconfig.h will ensure every compilation unit gets to see the same data structure layouts. +// Call IMGUI_CHECKVERSION() from your .cpp files to verify that the data structures your files are using are matching the ones imgui.cpp is using. +//----------------------------------------------------------------------------- + +#pragma once + +//---- Define assertion handler. Defaults to calling assert(). +// If your macro uses multiple statements, make sure is enclosed in a 'do { .. } while (0)' block so it can be used as a single statement. +//#define IM_ASSERT(_EXPR) MyAssert(_EXPR) +//#define IM_ASSERT(_EXPR) ((void)(_EXPR)) // Disable asserts + +//---- Define attributes of all API symbols declarations, e.g. for DLL under Windows +// Using Dear ImGui via a shared library is not recommended, because of function call overhead and because we don't guarantee backward nor forward ABI compatibility. +// DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions() +// for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for more details. +//#define IMGUI_API __declspec( dllexport ) +//#define IMGUI_API __declspec( dllimport ) + +//---- Don't define obsolete functions/enums/behaviors. Consider enabling from time to time after updating to avoid using soon-to-be obsolete function/names. +//#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS +//#define IMGUI_DISABLE_OBSOLETE_KEYIO // 1.87: disable legacy io.KeyMap[]+io.KeysDown[] in favor io.AddKeyEvent(). This will be folded into IMGUI_DISABLE_OBSOLETE_FUNCTIONS in a few versions. + +//---- Disable all of Dear ImGui or don't implement standard windows/tools. +// It is very strongly recommended to NOT disable the demo windows and debug tool during development. They are extremely useful in day to day work. Please read comments in imgui_demo.cpp. +//#define IMGUI_DISABLE // Disable everything: all headers and source files will be empty. +//#define IMGUI_DISABLE_DEMO_WINDOWS // Disable demo windows: ShowDemoWindow()/ShowStyleEditor() will be empty. +//#define IMGUI_DISABLE_DEBUG_TOOLS // Disable metrics/debugger and other debug tools: ShowMetricsWindow(), ShowDebugLogWindow() and ShowStackToolWindow() will be empty (this was called IMGUI_DISABLE_METRICS_WINDOW before 1.88). + +//---- Don't implement some functions to reduce linkage requirements. +//#define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS // [Win32] Don't implement default clipboard handler. Won't use and link with OpenClipboard/GetClipboardData/CloseClipboard etc. (user32.lib/.a, kernel32.lib/.a) +//#define IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with Visual Studio] Implement default IME handler (require imm32.lib/.a, auto-link for Visual Studio, -limm32 on command-line for MinGW) +//#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with non-Visual Studio compilers] Don't implement default IME handler (won't require imm32.lib/.a) +//#define IMGUI_DISABLE_WIN32_FUNCTIONS // [Win32] Won't use and link with any Win32 function (clipboard, ime). +//#define IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS // [OSX] Implement default OSX clipboard handler (need to link with '-framework ApplicationServices', this is why this is not the default). +//#define IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS // Don't implement ImFormatString/ImFormatStringV so you can implement them yourself (e.g. if you don't want to link with vsnprintf) +//#define IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS // Don't implement ImFabs/ImSqrt/ImPow/ImFmod/ImCos/ImSin/ImAcos/ImAtan2 so you can implement them yourself. +//#define IMGUI_DISABLE_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle at all (replace them with dummies) +//#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle so you can implement them yourself if you don't want to link with fopen/fclose/fread/fwrite. This will also disable the LogToTTY() function. +//#define IMGUI_DISABLE_DEFAULT_ALLOCATORS // Don't implement default allocators calling malloc()/free() to avoid linking with them. You will need to call ImGui::SetAllocatorFunctions(). +//#define IMGUI_DISABLE_SSE // Disable use of SSE intrinsics even if available + +//---- Include imgui_user.h at the end of imgui.h as a convenience +//#define IMGUI_INCLUDE_IMGUI_USER_H + +//---- Pack colors to BGRA8 instead of RGBA8 (to avoid converting from one to another) +//#define IMGUI_USE_BGRA_PACKED_COLOR + +//---- Use 32-bit for ImWchar (default is 16-bit) to support unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...) +//#define IMGUI_USE_WCHAR32 + +//---- Avoid multiple STB libraries implementations, or redefine path/filenames to prioritize another version +// By default the embedded implementations are declared static and not available outside of Dear ImGui sources files. +//#define IMGUI_STB_TRUETYPE_FILENAME "my_folder/stb_truetype.h" +//#define IMGUI_STB_RECT_PACK_FILENAME "my_folder/stb_rect_pack.h" +//#define IMGUI_STB_SPRINTF_FILENAME "my_folder/stb_sprintf.h" // only used if enabled +//#define IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION +//#define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION + +//---- Use stb_sprintf.h for a faster implementation of vsnprintf instead of the one from libc (unless IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS is defined) +// Compatibility checks of arguments and formats done by clang and GCC will be disabled in order to support the extra formats provided by stb_sprintf.h. +//#define IMGUI_USE_STB_SPRINTF + +//---- Use FreeType to build and rasterize the font atlas (instead of stb_truetype which is embedded by default in Dear ImGui) +// Requires FreeType headers to be available in the include path. Requires program to be compiled with 'misc/freetype/imgui_freetype.cpp' (in this repository) + the FreeType library (not provided). +// On Windows you may use vcpkg with 'vcpkg install freetype --triplet=x64-windows' + 'vcpkg integrate install'. +//#define IMGUI_ENABLE_FREETYPE + +//---- Use stb_truetype to build and rasterize the font atlas (default) +// The only purpose of this define is if you want force compilation of the stb_truetype backend ALONG with the FreeType backend. +//#define IMGUI_ENABLE_STB_TRUETYPE + +//---- Define constructor and implicit cast operators to convert back<>forth between your math types and ImVec2/ImVec4. +// This will be inlined as part of ImVec2 and ImVec4 class declarations. +/* +#define IM_VEC2_CLASS_EXTRA \ + constexpr ImVec2(const MyVec2& f) : x(f.x), y(f.y) {} \ + operator MyVec2() const { return MyVec2(x,y); } + +#define IM_VEC4_CLASS_EXTRA \ + constexpr ImVec4(const MyVec4& f) : x(f.x), y(f.y), z(f.z), w(f.w) {} \ + operator MyVec4() const { return MyVec4(x,y,z,w); } +*/ +//---- ...Or use Dear ImGui's own very basic math operators. +//#define IMGUI_DEFINE_MATH_OPERATORS + +//---- Use 32-bit vertex indices (default is 16-bit) is one way to allow large meshes with more than 64K vertices. +// Your renderer backend will need to support it (most example renderer backends support both 16/32-bit indices). +// Another way to allow large meshes while keeping 16-bit indices is to handle ImDrawCmd::VtxOffset in your renderer. +// Read about ImGuiBackendFlags_RendererHasVtxOffset for details. +//#define ImDrawIdx unsigned int + +//---- Override ImDrawCallback signature (will need to modify renderer backends accordingly) +//struct ImDrawList; +//struct ImDrawCmd; +//typedef void (*MyImDrawCallback)(const ImDrawList* draw_list, const ImDrawCmd* cmd, void* my_renderer_user_data); +//#define ImDrawCallback MyImDrawCallback + +//---- Debug Tools: Macro to break in Debugger +// (use 'Metrics->Tools->Item Picker' to pick widgets with the mouse and break into them for easy debugging.) +//#define IM_DEBUG_BREAK IM_ASSERT(0) +//#define IM_DEBUG_BREAK __debugbreak() + +//---- Debug Tools: Enable slower asserts +//#define IMGUI_DEBUG_PARANOID + +//---- Tip: You can add extra functions within the ImGui:: namespace, here or in your own headers files. +/* +namespace ImGui +{ + void MyFunction(const char* name, const MyMatrix44& v); +} +*/ diff --git a/Plugin/include/imgui.h b/Plugin/include/imgui.h new file mode 100644 index 00000000..07cd9560 --- /dev/null +++ b/Plugin/include/imgui.h @@ -0,0 +1,3459 @@ +// dear imgui, v1.89.7 +// (headers) + +// Help: +// - Read FAQ at http://dearimgui.com/faq +// - Newcomers, read 'Programmer guide' in imgui.cpp for notes on how to setup Dear ImGui in your codebase. +// - Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp. All applications in examples/ are doing that. +// Read imgui.cpp for details, links and comments. + +// Resources: +// - FAQ http://dearimgui.com/faq +// - Homepage https://github.com/ocornut/imgui +// - Releases & changelog https://github.com/ocornut/imgui/releases +// - Gallery https://github.com/ocornut/imgui/issues/6478 (please post your screenshots/video there!) +// - Wiki https://github.com/ocornut/imgui/wiki (lots of good stuff there) +// - Getting Started https://github.com/ocornut/imgui/wiki/Getting-Started +// - Glossary https://github.com/ocornut/imgui/wiki/Glossary +// - Issues & support https://github.com/ocornut/imgui/issues + +// Getting Started? +// - Read https://github.com/ocornut/imgui/wiki/Getting-Started +// - For first-time users having issues compiling/linking/running/loading fonts: +// please post in https://github.com/ocornut/imgui/discussions if you cannot find a solution in resources above. + +// Library Version +// (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM >= 12345') +#define IMGUI_VERSION "1.89.7" +#define IMGUI_VERSION_NUM 18971 +#define IMGUI_HAS_TABLE +#define IMGUI_HAS_VIEWPORT // Viewport WIP branch +#define IMGUI_HAS_DOCK // Docking WIP branch + +/* + +Index of this file: +// [SECTION] Header mess +// [SECTION] Forward declarations and basic types +// [SECTION] Dear ImGui end-user API functions +// [SECTION] Flags & Enumerations +// [SECTION] Helpers: Memory allocations macros, ImVector<> +// [SECTION] ImGuiStyle +// [SECTION] ImGuiIO +// [SECTION] Misc data structures (ImGuiInputTextCallbackData, ImGuiSizeCallbackData, ImGuiWindowClass, ImGuiPayload, ImGuiTableSortSpecs, ImGuiTableColumnSortSpecs) +// [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, Math Operators, ImColor) +// [SECTION] Drawing API (ImDrawCallback, ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawFlags, ImDrawListFlags, ImDrawList, ImDrawData) +// [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontGlyphRangesBuilder, ImFontAtlasFlags, ImFontAtlas, ImFont) +// [SECTION] Viewports (ImGuiViewportFlags, ImGuiViewport) +// [SECTION] Platform Dependent Interfaces (ImGuiPlatformIO, ImGuiPlatformMonitor, ImGuiPlatformImeData) +// [SECTION] Obsolete functions and types + +*/ + +#pragma once + +// Configuration file with compile-time options +// (edit imconfig.h or '#define IMGUI_USER_CONFIG "myfilename.h" from your build system') +#ifdef IMGUI_USER_CONFIG +#include IMGUI_USER_CONFIG +#endif +#include "imconfig.h" + +#ifndef IMGUI_DISABLE + +//----------------------------------------------------------------------------- +// [SECTION] Header mess +//----------------------------------------------------------------------------- + +// Includes +#include // FLT_MIN, FLT_MAX +#include // va_list, va_start, va_end +#include // ptrdiff_t, NULL +#include // memset, memmove, memcpy, strlen, strchr, strcpy, strcmp + +// Define attributes of all API symbols declarations (e.g. for DLL under Windows) +// IMGUI_API is used for core imgui functions, IMGUI_IMPL_API is used for the default backends files (imgui_impl_xxx.h) +// Using dear imgui via a shared library is not recommended, because we don't guarantee backward nor forward ABI compatibility (also function call overhead, as dear imgui is a call-heavy API) +#ifndef IMGUI_API +#define IMGUI_API +#endif +#ifndef IMGUI_IMPL_API +#define IMGUI_IMPL_API IMGUI_API +#endif + +// Helper Macros +#ifndef IM_ASSERT +#include +#define IM_ASSERT(_EXPR) assert(_EXPR) // You can override the default assert handler by editing imconfig.h +#endif +#define IM_ARRAYSIZE(_ARR) ((int)(sizeof(_ARR) / sizeof(*(_ARR)))) // Size of a static C-style array. Don't use on pointers! +#define IM_UNUSED(_VAR) ((void)(_VAR)) // Used to silence "unused variable warnings". Often useful as asserts may be stripped out from final builds. +#define IM_OFFSETOF(_TYPE,_MEMBER) offsetof(_TYPE, _MEMBER) // Offset of _MEMBER within _TYPE. Standardized as offsetof() in C++11 +#define IMGUI_CHECKVERSION() ImGui::DebugCheckVersionAndDataLayout(IMGUI_VERSION, sizeof(ImGuiIO), sizeof(ImGuiStyle), sizeof(ImVec2), sizeof(ImVec4), sizeof(ImDrawVert), sizeof(ImDrawIdx)) + +// Helper Macros - IM_FMTARGS, IM_FMTLIST: Apply printf-style warnings to our formatting functions. +#if !defined(IMGUI_USE_STB_SPRINTF) && defined(__MINGW32__) && !defined(__clang__) +#define IM_FMTARGS(FMT) __attribute__((format(gnu_printf, FMT, FMT+1))) +#define IM_FMTLIST(FMT) __attribute__((format(gnu_printf, FMT, 0))) +#elif !defined(IMGUI_USE_STB_SPRINTF) && (defined(__clang__) || defined(__GNUC__)) +#define IM_FMTARGS(FMT) __attribute__((format(printf, FMT, FMT+1))) +#define IM_FMTLIST(FMT) __attribute__((format(printf, FMT, 0))) +#else +#define IM_FMTARGS(FMT) +#define IM_FMTLIST(FMT) +#endif + +// Disable some of MSVC most aggressive Debug runtime checks in function header/footer (used in some simple/low-level functions) +#if defined(_MSC_VER) && !defined(__clang__) && !defined(__INTEL_COMPILER) && !defined(IMGUI_DEBUG_PARANOID) +#define IM_MSVC_RUNTIME_CHECKS_OFF __pragma(runtime_checks("",off)) __pragma(check_stack(off)) __pragma(strict_gs_check(push,off)) +#define IM_MSVC_RUNTIME_CHECKS_RESTORE __pragma(runtime_checks("",restore)) __pragma(check_stack()) __pragma(strict_gs_check(pop)) +#else +#define IM_MSVC_RUNTIME_CHECKS_OFF +#define IM_MSVC_RUNTIME_CHECKS_RESTORE +#endif + +// Warnings +#ifdef _MSC_VER +#pragma warning (push) +#pragma warning (disable: 26495) // [Static Analyzer] Variable 'XXX' is uninitialized. Always initialize a member variable (type.6). +#endif +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wold-style-cast" +#if __has_warning("-Wzero-as-null-pointer-constant") +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" +#endif +#elif defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#endif + +//----------------------------------------------------------------------------- +// [SECTION] Forward declarations and basic types +//----------------------------------------------------------------------------- + +// Forward declarations +struct ImDrawChannel; // Temporary storage to output draw commands out of order, used by ImDrawListSplitter and ImDrawList::ChannelsSplit() +struct ImDrawCmd; // A single draw command within a parent ImDrawList (generally maps to 1 GPU draw call, unless it is a callback) +struct ImDrawData; // All draw command lists required to render the frame + pos/size coordinates to use for the projection matrix. +struct ImDrawList; // A single draw command list (generally one per window, conceptually you may see this as a dynamic "mesh" builder) +struct ImDrawListSharedData; // Data shared among multiple draw lists (typically owned by parent ImGui context, but you may create one yourself) +struct ImDrawListSplitter; // Helper to split a draw list into different layers which can be drawn into out of order, then flattened back. +struct ImDrawVert; // A single vertex (pos + uv + col = 20 bytes by default. Override layout with IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT) +struct ImFont; // Runtime data for a single font within a parent ImFontAtlas +struct ImFontAtlas; // Runtime data for multiple fonts, bake multiple fonts into a single texture, TTF/OTF font loader +struct ImFontBuilderIO; // Opaque interface to a font builder (stb_truetype or FreeType). +struct ImFontConfig; // Configuration data when adding a font or merging fonts +struct ImFontGlyph; // A single font glyph (code point + coordinates within in ImFontAtlas + offset) +struct ImFontGlyphRangesBuilder; // Helper to build glyph ranges from text/string data +struct ImColor; // Helper functions to create a color that can be converted to either u32 or float4 (*OBSOLETE* please avoid using) +struct ImGuiContext; // Dear ImGui context (opaque structure, unless including imgui_internal.h) +struct ImGuiIO; // Main configuration and I/O between your application and ImGui +struct ImGuiInputTextCallbackData; // Shared state of InputText() when using custom ImGuiInputTextCallback (rare/advanced use) +struct ImGuiKeyData; // Storage for ImGuiIO and IsKeyDown(), IsKeyPressed() etc functions. +struct ImGuiListClipper; // Helper to manually clip large list of items +struct ImGuiOnceUponAFrame; // Helper for running a block of code not more than once a frame +struct ImGuiPayload; // User data payload for drag and drop operations +struct ImGuiPlatformIO; // Multi-viewport support: interface for Platform/Renderer backends + viewports to render +struct ImGuiPlatformMonitor; // Multi-viewport support: user-provided bounds for each connected monitor/display. Used when positioning popups and tooltips to avoid them straddling monitors +struct ImGuiPlatformImeData; // Platform IME data for io.SetPlatformImeDataFn() function. +struct ImGuiSizeCallbackData; // Callback data when using SetNextWindowSizeConstraints() (rare/advanced use) +struct ImGuiStorage; // Helper for key->value storage +struct ImGuiStyle; // Runtime data for styling/colors +struct ImGuiTableSortSpecs; // Sorting specifications for a table (often handling sort specs for a single column, occasionally more) +struct ImGuiTableColumnSortSpecs; // Sorting specification for one column of a table +struct ImGuiTextBuffer; // Helper to hold and append into a text buffer (~string builder) +struct ImGuiTextFilter; // Helper to parse and apply text filters (e.g. "aaaaa[,bbbbb][,ccccc]") +struct ImGuiViewport; // A Platform Window (always 1 unless multi-viewport are enabled. One per platform window to output to). In the future may represent Platform Monitor +struct ImGuiWindowClass; // Window class (rare/advanced uses: provide hints to the platform backend via altered viewport flags and parent/child info) + +// Enumerations +// - We don't use strongly typed enums much because they add constraints (can't extend in private code, can't store typed in bit fields, extra casting on iteration) +// - Tip: Use your programming IDE navigation facilities on the names in the _central column_ below to find the actual flags/enum lists! +// In Visual Studio IDE: CTRL+comma ("Edit.GoToAll") can follow symbols in comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// With Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols in comments. +enum ImGuiKey : int; // -> enum ImGuiKey // Enum: A key identifier (ImGuiKey_XXX or ImGuiMod_XXX value) +enum ImGuiMouseSource : int; // -> enum ImGuiMouseSource // Enum; A mouse input source identifier (Mouse, TouchScreen, Pen) +typedef int ImGuiCol; // -> enum ImGuiCol_ // Enum: A color identifier for styling +typedef int ImGuiCond; // -> enum ImGuiCond_ // Enum: A condition for many Set*() functions +typedef int ImGuiDataType; // -> enum ImGuiDataType_ // Enum: A primary data type +typedef int ImGuiDir; // -> enum ImGuiDir_ // Enum: A cardinal direction +typedef int ImGuiMouseButton; // -> enum ImGuiMouseButton_ // Enum: A mouse button identifier (0=left, 1=right, 2=middle) +typedef int ImGuiMouseCursor; // -> enum ImGuiMouseCursor_ // Enum: A mouse cursor shape +typedef int ImGuiSortDirection; // -> enum ImGuiSortDirection_ // Enum: A sorting direction (ascending or descending) +typedef int ImGuiStyleVar; // -> enum ImGuiStyleVar_ // Enum: A variable identifier for styling +typedef int ImGuiTableBgTarget; // -> enum ImGuiTableBgTarget_ // Enum: A color target for TableSetBgColor() + +// Flags (declared as int for compatibility with old C++, to allow using as flags without overhead, and to not pollute the top of this file) +// - Tip: Use your programming IDE navigation facilities on the names in the _central column_ below to find the actual flags/enum lists! +// In Visual Studio IDE: CTRL+comma ("Edit.GoToAll") can follow symbols in comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// With Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols in comments. +typedef int ImDrawFlags; // -> enum ImDrawFlags_ // Flags: for ImDrawList functions +typedef int ImDrawListFlags; // -> enum ImDrawListFlags_ // Flags: for ImDrawList instance +typedef int ImFontAtlasFlags; // -> enum ImFontAtlasFlags_ // Flags: for ImFontAtlas build +typedef int ImGuiBackendFlags; // -> enum ImGuiBackendFlags_ // Flags: for io.BackendFlags +typedef int ImGuiButtonFlags; // -> enum ImGuiButtonFlags_ // Flags: for InvisibleButton() +typedef int ImGuiColorEditFlags; // -> enum ImGuiColorEditFlags_ // Flags: for ColorEdit4(), ColorPicker4() etc. +typedef int ImGuiConfigFlags; // -> enum ImGuiConfigFlags_ // Flags: for io.ConfigFlags +typedef int ImGuiComboFlags; // -> enum ImGuiComboFlags_ // Flags: for BeginCombo() +typedef int ImGuiDockNodeFlags; // -> enum ImGuiDockNodeFlags_ // Flags: for DockSpace() +typedef int ImGuiDragDropFlags; // -> enum ImGuiDragDropFlags_ // Flags: for BeginDragDropSource(), AcceptDragDropPayload() +typedef int ImGuiFocusedFlags; // -> enum ImGuiFocusedFlags_ // Flags: for IsWindowFocused() +typedef int ImGuiHoveredFlags; // -> enum ImGuiHoveredFlags_ // Flags: for IsItemHovered(), IsWindowHovered() etc. +typedef int ImGuiInputTextFlags; // -> enum ImGuiInputTextFlags_ // Flags: for InputText(), InputTextMultiline() +typedef int ImGuiKeyChord; // -> ImGuiKey | ImGuiMod_XXX // Flags: for storage only for now: an ImGuiKey optionally OR-ed with one or more ImGuiMod_XXX values. +typedef int ImGuiPopupFlags; // -> enum ImGuiPopupFlags_ // Flags: for OpenPopup*(), BeginPopupContext*(), IsPopupOpen() +typedef int ImGuiSelectableFlags; // -> enum ImGuiSelectableFlags_ // Flags: for Selectable() +typedef int ImGuiSliderFlags; // -> enum ImGuiSliderFlags_ // Flags: for DragFloat(), DragInt(), SliderFloat(), SliderInt() etc. +typedef int ImGuiTabBarFlags; // -> enum ImGuiTabBarFlags_ // Flags: for BeginTabBar() +typedef int ImGuiTabItemFlags; // -> enum ImGuiTabItemFlags_ // Flags: for BeginTabItem() +typedef int ImGuiTableFlags; // -> enum ImGuiTableFlags_ // Flags: For BeginTable() +typedef int ImGuiTableColumnFlags; // -> enum ImGuiTableColumnFlags_// Flags: For TableSetupColumn() +typedef int ImGuiTableRowFlags; // -> enum ImGuiTableRowFlags_ // Flags: For TableNextRow() +typedef int ImGuiTreeNodeFlags; // -> enum ImGuiTreeNodeFlags_ // Flags: for TreeNode(), TreeNodeEx(), CollapsingHeader() +typedef int ImGuiViewportFlags; // -> enum ImGuiViewportFlags_ // Flags: for ImGuiViewport +typedef int ImGuiWindowFlags; // -> enum ImGuiWindowFlags_ // Flags: for Begin(), BeginChild() + +// ImTexture: user data for renderer backend to identify a texture [Compile-time configurable type] +// - To use something else than an opaque void* pointer: override with e.g. '#define ImTextureID MyTextureType*' in your imconfig.h file. +// - This can be whatever to you want it to be! read the FAQ about ImTextureID for details. +#ifndef ImTextureID +typedef void* ImTextureID; // Default: store a pointer or an integer fitting in a pointer (most renderer backends are ok with that) +#endif + +// ImDrawIdx: vertex index. [Compile-time configurable type] +// - To use 16-bit indices + allow large meshes: backend need to set 'io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset' and handle ImDrawCmd::VtxOffset (recommended). +// - To use 32-bit indices: override with '#define ImDrawIdx unsigned int' in your imconfig.h file. +#ifndef ImDrawIdx +typedef unsigned short ImDrawIdx; // Default: 16-bit (for maximum compatibility with renderer backends) +#endif + +// Scalar data types +typedef unsigned int ImGuiID;// A unique ID used by widgets (typically the result of hashing a stack of string) +typedef signed char ImS8; // 8-bit signed integer +typedef unsigned char ImU8; // 8-bit unsigned integer +typedef signed short ImS16; // 16-bit signed integer +typedef unsigned short ImU16; // 16-bit unsigned integer +typedef signed int ImS32; // 32-bit signed integer == int +typedef unsigned int ImU32; // 32-bit unsigned integer (often used to store packed colors) +typedef signed long long ImS64; // 64-bit signed integer +typedef unsigned long long ImU64; // 64-bit unsigned integer + +// Character types +// (we generally use UTF-8 encoded string in the API. This is storage specifically for a decoded character used for keyboard input and display) +typedef unsigned short ImWchar16; // A single decoded U16 character/code point. We encode them as multi bytes UTF-8 when used in strings. +typedef unsigned int ImWchar32; // A single decoded U32 character/code point. We encode them as multi bytes UTF-8 when used in strings. +#ifdef IMGUI_USE_WCHAR32 // ImWchar [configurable type: override in imconfig.h with '#define IMGUI_USE_WCHAR32' to support Unicode planes 1-16] +typedef ImWchar32 ImWchar; +#else +typedef ImWchar16 ImWchar; +#endif + +// Callback and functions types +typedef int (*ImGuiInputTextCallback)(ImGuiInputTextCallbackData* data); // Callback function for ImGui::InputText() +typedef void (*ImGuiSizeCallback)(ImGuiSizeCallbackData* data); // Callback function for ImGui::SetNextWindowSizeConstraints() +typedef void* (*ImGuiMemAllocFunc)(size_t sz, void* user_data); // Function signature for ImGui::SetAllocatorFunctions() +typedef void (*ImGuiMemFreeFunc)(void* ptr, void* user_data); // Function signature for ImGui::SetAllocatorFunctions() + +// ImVec2: 2D vector used to store positions, sizes etc. [Compile-time configurable type] +// This is a frequently used type in the API. Consider using IM_VEC2_CLASS_EXTRA to create implicit cast from/to our preferred type. +IM_MSVC_RUNTIME_CHECKS_OFF +struct ImVec2 +{ + float x, y; + constexpr ImVec2() : x(0.0f), y(0.0f) { } + constexpr ImVec2(float _x, float _y) : x(_x), y(_y) { } + float& operator[] (size_t idx) { IM_ASSERT(idx == 0 || idx == 1); return ((float*)(void*)(char*)this)[idx]; } // We very rarely use this [] operator, so the assert overhead is fine. + float operator[] (size_t idx) const { IM_ASSERT(idx == 0 || idx == 1); return ((const float*)(const void*)(const char*)this)[idx]; } +#ifdef IM_VEC2_CLASS_EXTRA + IM_VEC2_CLASS_EXTRA // Define additional constructors and implicit cast operators in imconfig.h to convert back and forth between your math types and ImVec2. +#endif +}; + +// ImVec4: 4D vector used to store clipping rectangles, colors etc. [Compile-time configurable type] +struct ImVec4 +{ + float x, y, z, w; + constexpr ImVec4() : x(0.0f), y(0.0f), z(0.0f), w(0.0f) { } + constexpr ImVec4(float _x, float _y, float _z, float _w) : x(_x), y(_y), z(_z), w(_w) { } +#ifdef IM_VEC4_CLASS_EXTRA + IM_VEC4_CLASS_EXTRA // Define additional constructors and implicit cast operators in imconfig.h to convert back and forth between your math types and ImVec4. +#endif +}; +IM_MSVC_RUNTIME_CHECKS_RESTORE + +//----------------------------------------------------------------------------- +// [SECTION] Dear ImGui end-user API functions +// (Note that ImGui:: being a namespace, you can add extra ImGui:: functions in your own separate file. Please don't modify imgui source files!) +//----------------------------------------------------------------------------- + +namespace ImGui +{ + // Context creation and access + // - Each context create its own ImFontAtlas by default. You may instance one yourself and pass it to CreateContext() to share a font atlas between contexts. + // - DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions() + // for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for details. + IMGUI_API ImGuiContext* CreateContext(ImFontAtlas* shared_font_atlas = NULL); + IMGUI_API void DestroyContext(ImGuiContext* ctx = NULL); // NULL = destroy current context + IMGUI_API ImGuiContext* GetCurrentContext(); + IMGUI_API void SetCurrentContext(ImGuiContext* ctx); + + // Main + IMGUI_API ImGuiIO& GetIO(); // access the IO structure (mouse/keyboard/gamepad inputs, time, various configuration options/flags) + IMGUI_API ImGuiStyle& GetStyle(); // access the Style structure (colors, sizes). Always use PushStyleCol(), PushStyleVar() to modify style mid-frame! + IMGUI_API void NewFrame(); // start a new Dear ImGui frame, you can submit any command from this point until Render()/EndFrame(). + IMGUI_API void EndFrame(); // ends the Dear ImGui frame. automatically called by Render(). If you don't need to render data (skipping rendering) you may call EndFrame() without Render()... but you'll have wasted CPU already! If you don't need to render, better to not create any windows and not call NewFrame() at all! + IMGUI_API void Render(); // ends the Dear ImGui frame, finalize the draw data. You can then get call GetDrawData(). + IMGUI_API ImDrawData* GetDrawData(); // valid after Render() and until the next call to NewFrame(). this is what you have to render. + + // Demo, Debug, Information + IMGUI_API void ShowDemoWindow(bool* p_open = NULL); // create Demo window. demonstrate most ImGui features. call this to learn about the library! try to make it always available in your application! + IMGUI_API void ShowMetricsWindow(bool* p_open = NULL); // create Metrics/Debugger window. display Dear ImGui internals: windows, draw commands, various internal state, etc. + IMGUI_API void ShowDebugLogWindow(bool* p_open = NULL); // create Debug Log window. display a simplified log of important dear imgui events. + IMGUI_API void ShowStackToolWindow(bool* p_open = NULL); // create Stack Tool window. hover items with mouse to query information about the source of their unique ID. + IMGUI_API void ShowAboutWindow(bool* p_open = NULL); // create About window. display Dear ImGui version, credits and build/system information. + IMGUI_API void ShowStyleEditor(ImGuiStyle* ref = NULL); // add style editor block (not a window). you can pass in a reference ImGuiStyle structure to compare to, revert to and save to (else it uses the default style) + IMGUI_API bool ShowStyleSelector(const char* label); // add style selector block (not a window), essentially a combo listing the default styles. + IMGUI_API void ShowFontSelector(const char* label); // add font selector block (not a window), essentially a combo listing the loaded fonts. + IMGUI_API void ShowUserGuide(); // add basic help/info block (not a window): how to manipulate ImGui as an end-user (mouse/keyboard controls). + IMGUI_API const char* GetVersion(); // get the compiled version string e.g. "1.80 WIP" (essentially the value for IMGUI_VERSION from the compiled version of imgui.cpp) + + // Styles + IMGUI_API void StyleColorsDark(ImGuiStyle* dst = NULL); // new, recommended style (default) + IMGUI_API void StyleColorsLight(ImGuiStyle* dst = NULL); // best used with borders and a custom, thicker font + IMGUI_API void StyleColorsClassic(ImGuiStyle* dst = NULL); // classic imgui style + + // Windows + // - Begin() = push window to the stack and start appending to it. End() = pop window from the stack. + // - Passing 'bool* p_open != NULL' shows a window-closing widget in the upper-right corner of the window, + // which clicking will set the boolean to false when clicked. + // - You may append multiple times to the same window during the same frame by calling Begin()/End() pairs multiple times. + // Some information such as 'flags' or 'p_open' will only be considered by the first call to Begin(). + // - Begin() return false to indicate the window is collapsed or fully clipped, so you may early out and omit submitting + // anything to the window. Always call a matching End() for each Begin() call, regardless of its return value! + // [Important: due to legacy reason, this is inconsistent with most other functions such as BeginMenu/EndMenu, + // BeginPopup/EndPopup, etc. where the EndXXX call should only be called if the corresponding BeginXXX function + // returned true. Begin and BeginChild are the only odd ones out. Will be fixed in a future update.] + // - Note that the bottom of window stack always contains a window called "Debug". + IMGUI_API bool Begin(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0); + IMGUI_API void End(); + + // Child Windows + // - Use child windows to begin into a self-contained independent scrolling/clipping regions within a host window. Child windows can embed their own child. + // - For each independent axis of 'size': ==0.0f: use remaining host window size / >0.0f: fixed size / <0.0f: use remaining window size minus abs(size) / Each axis can use a different mode, e.g. ImVec2(0,400). + // - BeginChild() returns false to indicate the window is collapsed or fully clipped, so you may early out and omit submitting anything to the window. + // Always call a matching EndChild() for each BeginChild() call, regardless of its return value. + // [Important: due to legacy reason, this is inconsistent with most other functions such as BeginMenu/EndMenu, + // BeginPopup/EndPopup, etc. where the EndXXX call should only be called if the corresponding BeginXXX function + // returned true. Begin and BeginChild are the only odd ones out. Will be fixed in a future update.] + IMGUI_API bool BeginChild(const char* str_id, const ImVec2& size = ImVec2(0, 0), bool border = false, ImGuiWindowFlags flags = 0); + IMGUI_API bool BeginChild(ImGuiID id, const ImVec2& size = ImVec2(0, 0), bool border = false, ImGuiWindowFlags flags = 0); + IMGUI_API void EndChild(); + + // Windows Utilities + // - 'current window' = the window we are appending into while inside a Begin()/End() block. 'next window' = next window we will Begin() into. + IMGUI_API bool IsWindowAppearing(); + IMGUI_API bool IsWindowCollapsed(); + IMGUI_API bool IsWindowFocused(ImGuiFocusedFlags flags=0); // is current window focused? or its root/child, depending on flags. see flags for options. + IMGUI_API bool IsWindowHovered(ImGuiHoveredFlags flags=0); // is current window hovered (and typically: not blocked by a popup/modal)? see flags for options. NB: If you are trying to check whether your mouse should be dispatched to imgui or to your app, you should use the 'io.WantCaptureMouse' boolean for that! Please read the FAQ! + IMGUI_API ImDrawList* GetWindowDrawList(); // get draw list associated to the current window, to append your own drawing primitives + IMGUI_API float GetWindowDpiScale(); // get DPI scale currently associated to the current window's viewport. + IMGUI_API ImVec2 GetWindowPos(); // get current window position in screen space (useful if you want to do your own drawing via the DrawList API) + IMGUI_API ImVec2 GetWindowSize(); // get current window size + IMGUI_API float GetWindowWidth(); // get current window width (shortcut for GetWindowSize().x) + IMGUI_API float GetWindowHeight(); // get current window height (shortcut for GetWindowSize().y) + IMGUI_API ImGuiViewport*GetWindowViewport(); // get viewport currently associated to the current window. + + // Window manipulation + // - Prefer using SetNextXXX functions (before Begin) rather that SetXXX functions (after Begin). + IMGUI_API void SetNextWindowPos(const ImVec2& pos, ImGuiCond cond = 0, const ImVec2& pivot = ImVec2(0, 0)); // set next window position. call before Begin(). use pivot=(0.5f,0.5f) to center on given point, etc. + IMGUI_API void SetNextWindowSize(const ImVec2& size, ImGuiCond cond = 0); // set next window size. set axis to 0.0f to force an auto-fit on this axis. call before Begin() + IMGUI_API void SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& size_max, ImGuiSizeCallback custom_callback = NULL, void* custom_callback_data = NULL); // set next window size limits. use -1,-1 on either X/Y axis to preserve the current size. Sizes will be rounded down. Use callback to apply non-trivial programmatic constraints. + IMGUI_API void SetNextWindowContentSize(const ImVec2& size); // set next window content size (~ scrollable client area, which enforce the range of scrollbars). Not including window decorations (title bar, menu bar, etc.) nor WindowPadding. set an axis to 0.0f to leave it automatic. call before Begin() + IMGUI_API void SetNextWindowCollapsed(bool collapsed, ImGuiCond cond = 0); // set next window collapsed state. call before Begin() + IMGUI_API void SetNextWindowFocus(); // set next window to be focused / top-most. call before Begin() + IMGUI_API void SetNextWindowScroll(const ImVec2& scroll); // set next window scrolling value (use < 0.0f to not affect a given axis). + IMGUI_API void SetNextWindowBgAlpha(float alpha); // set next window background color alpha. helper to easily override the Alpha component of ImGuiCol_WindowBg/ChildBg/PopupBg. you may also use ImGuiWindowFlags_NoBackground. + IMGUI_API void SetNextWindowViewport(ImGuiID viewport_id); // set next window viewport + IMGUI_API void SetWindowPos(const ImVec2& pos, ImGuiCond cond = 0); // (not recommended) set current window position - call within Begin()/End(). prefer using SetNextWindowPos(), as this may incur tearing and side-effects. + IMGUI_API void SetWindowSize(const ImVec2& size, ImGuiCond cond = 0); // (not recommended) set current window size - call within Begin()/End(). set to ImVec2(0, 0) to force an auto-fit. prefer using SetNextWindowSize(), as this may incur tearing and minor side-effects. + IMGUI_API void SetWindowCollapsed(bool collapsed, ImGuiCond cond = 0); // (not recommended) set current window collapsed state. prefer using SetNextWindowCollapsed(). + IMGUI_API void SetWindowFocus(); // (not recommended) set current window to be focused / top-most. prefer using SetNextWindowFocus(). + IMGUI_API void SetWindowFontScale(float scale); // [OBSOLETE] set font scale. Adjust IO.FontGlobalScale if you want to scale all windows. This is an old API! For correct scaling, prefer to reload font + rebuild ImFontAtlas + call style.ScaleAllSizes(). + IMGUI_API void SetWindowPos(const char* name, const ImVec2& pos, ImGuiCond cond = 0); // set named window position. + IMGUI_API void SetWindowSize(const char* name, const ImVec2& size, ImGuiCond cond = 0); // set named window size. set axis to 0.0f to force an auto-fit on this axis. + IMGUI_API void SetWindowCollapsed(const char* name, bool collapsed, ImGuiCond cond = 0); // set named window collapsed state + IMGUI_API void SetWindowFocus(const char* name); // set named window to be focused / top-most. use NULL to remove focus. + + // Content region + // - Retrieve available space from a given point. GetContentRegionAvail() is frequently useful. + // - Those functions are bound to be redesigned (they are confusing, incomplete and the Min/Max return values are in local window coordinates which increases confusion) + IMGUI_API ImVec2 GetContentRegionAvail(); // == GetContentRegionMax() - GetCursorPos() + IMGUI_API ImVec2 GetContentRegionMax(); // current content boundaries (typically window boundaries including scrolling, or current column boundaries), in windows coordinates + IMGUI_API ImVec2 GetWindowContentRegionMin(); // content boundaries min for the full window (roughly (0,0)-Scroll), in window coordinates + IMGUI_API ImVec2 GetWindowContentRegionMax(); // content boundaries max for the full window (roughly (0,0)+Size-Scroll) where Size can be overridden with SetNextWindowContentSize(), in window coordinates + + // Windows Scrolling + // - Any change of Scroll will be applied at the beginning of next frame in the first call to Begin(). + // - You may instead use SetNextWindowScroll() prior to calling Begin() to avoid this delay, as an alternative to using SetScrollX()/SetScrollY(). + IMGUI_API float GetScrollX(); // get scrolling amount [0 .. GetScrollMaxX()] + IMGUI_API float GetScrollY(); // get scrolling amount [0 .. GetScrollMaxY()] + IMGUI_API void SetScrollX(float scroll_x); // set scrolling amount [0 .. GetScrollMaxX()] + IMGUI_API void SetScrollY(float scroll_y); // set scrolling amount [0 .. GetScrollMaxY()] + IMGUI_API float GetScrollMaxX(); // get maximum scrolling amount ~~ ContentSize.x - WindowSize.x - DecorationsSize.x + IMGUI_API float GetScrollMaxY(); // get maximum scrolling amount ~~ ContentSize.y - WindowSize.y - DecorationsSize.y + IMGUI_API void SetScrollHereX(float center_x_ratio = 0.5f); // adjust scrolling amount to make current cursor position visible. center_x_ratio=0.0: left, 0.5: center, 1.0: right. When using to make a "default/current item" visible, consider using SetItemDefaultFocus() instead. + IMGUI_API void SetScrollHereY(float center_y_ratio = 0.5f); // adjust scrolling amount to make current cursor position visible. center_y_ratio=0.0: top, 0.5: center, 1.0: bottom. When using to make a "default/current item" visible, consider using SetItemDefaultFocus() instead. + IMGUI_API void SetScrollFromPosX(float local_x, float center_x_ratio = 0.5f); // adjust scrolling amount to make given position visible. Generally GetCursorStartPos() + offset to compute a valid position. + IMGUI_API void SetScrollFromPosY(float local_y, float center_y_ratio = 0.5f); // adjust scrolling amount to make given position visible. Generally GetCursorStartPos() + offset to compute a valid position. + + // Parameters stacks (shared) + IMGUI_API void PushFont(ImFont* font); // use NULL as a shortcut to push default font + IMGUI_API void PopFont(); + IMGUI_API void PushStyleColor(ImGuiCol idx, ImU32 col); // modify a style color. always use this if you modify the style after NewFrame(). + IMGUI_API void PushStyleColor(ImGuiCol idx, const ImVec4& col); + IMGUI_API void PopStyleColor(int count = 1); + IMGUI_API void PushStyleVar(ImGuiStyleVar idx, float val); // modify a style float variable. always use this if you modify the style after NewFrame(). + IMGUI_API void PushStyleVar(ImGuiStyleVar idx, const ImVec2& val); // modify a style ImVec2 variable. always use this if you modify the style after NewFrame(). + IMGUI_API void PopStyleVar(int count = 1); + IMGUI_API void PushTabStop(bool tab_stop); // == tab stop enable. Allow focusing using TAB/Shift-TAB, enabled by default but you can disable it for certain widgets + IMGUI_API void PopTabStop(); + IMGUI_API void PushButtonRepeat(bool repeat); // in 'repeat' mode, Button*() functions return repeated true in a typematic manner (using io.KeyRepeatDelay/io.KeyRepeatRate setting). Note that you can call IsItemActive() after any Button() to tell if the button is held in the current frame. + IMGUI_API void PopButtonRepeat(); + + // Parameters stacks (current window) + IMGUI_API void PushItemWidth(float item_width); // push width of items for common large "item+label" widgets. >0.0f: width in pixels, <0.0f align xx pixels to the right of window (so -FLT_MIN always align width to the right side). + IMGUI_API void PopItemWidth(); + IMGUI_API void SetNextItemWidth(float item_width); // set width of the _next_ common large "item+label" widget. >0.0f: width in pixels, <0.0f align xx pixels to the right of window (so -FLT_MIN always align width to the right side) + IMGUI_API float CalcItemWidth(); // width of item given pushed settings and current cursor position. NOT necessarily the width of last item unlike most 'Item' functions. + IMGUI_API void PushTextWrapPos(float wrap_local_pos_x = 0.0f); // push word-wrapping position for Text*() commands. < 0.0f: no wrapping; 0.0f: wrap to end of window (or column); > 0.0f: wrap at 'wrap_pos_x' position in window local space + IMGUI_API void PopTextWrapPos(); + + // Style read access + // - Use the ShowStyleEditor() function to interactively see/edit the colors. + IMGUI_API ImFont* GetFont(); // get current font + IMGUI_API float GetFontSize(); // get current font size (= height in pixels) of current font with current scale applied + IMGUI_API ImVec2 GetFontTexUvWhitePixel(); // get UV coordinate for a while pixel, useful to draw custom shapes via the ImDrawList API + IMGUI_API ImU32 GetColorU32(ImGuiCol idx, float alpha_mul = 1.0f); // retrieve given style color with style alpha applied and optional extra alpha multiplier, packed as a 32-bit value suitable for ImDrawList + IMGUI_API ImU32 GetColorU32(const ImVec4& col); // retrieve given color with style alpha applied, packed as a 32-bit value suitable for ImDrawList + IMGUI_API ImU32 GetColorU32(ImU32 col); // retrieve given color with style alpha applied, packed as a 32-bit value suitable for ImDrawList + IMGUI_API const ImVec4& GetStyleColorVec4(ImGuiCol idx); // retrieve style color as stored in ImGuiStyle structure. use to feed back into PushStyleColor(), otherwise use GetColorU32() to get style color with style alpha baked in. + + // Cursor / Layout + // - By "cursor" we mean the current output position. + // - The typical widget behavior is to output themselves at the current cursor position, then move the cursor one line down. + // - You can call SameLine() between widgets to undo the last carriage return and output at the right of the preceding widget. + // - Attention! We currently have inconsistencies between window-local and absolute positions we will aim to fix with future API: + // Window-local coordinates: SameLine(), GetCursorPos(), SetCursorPos(), GetCursorStartPos(), GetContentRegionMax(), GetWindowContentRegion*(), PushTextWrapPos() + // Absolute coordinate: GetCursorScreenPos(), SetCursorScreenPos(), all ImDrawList:: functions. + IMGUI_API void Separator(); // separator, generally horizontal. inside a menu bar or in horizontal layout mode, this becomes a vertical separator. + IMGUI_API void SameLine(float offset_from_start_x=0.0f, float spacing=-1.0f); // call between widgets or groups to layout them horizontally. X position given in window coordinates. + IMGUI_API void NewLine(); // undo a SameLine() or force a new line when in a horizontal-layout context. + IMGUI_API void Spacing(); // add vertical spacing. + IMGUI_API void Dummy(const ImVec2& size); // add a dummy item of given size. unlike InvisibleButton(), Dummy() won't take the mouse click or be navigable into. + IMGUI_API void Indent(float indent_w = 0.0f); // move content position toward the right, by indent_w, or style.IndentSpacing if indent_w <= 0 + IMGUI_API void Unindent(float indent_w = 0.0f); // move content position back to the left, by indent_w, or style.IndentSpacing if indent_w <= 0 + IMGUI_API void BeginGroup(); // lock horizontal starting position + IMGUI_API void EndGroup(); // unlock horizontal starting position + capture the whole group bounding box into one "item" (so you can use IsItemHovered() or layout primitives such as SameLine() on whole group, etc.) + IMGUI_API ImVec2 GetCursorPos(); // cursor position in window coordinates (relative to window position) + IMGUI_API float GetCursorPosX(); // (some functions are using window-relative coordinates, such as: GetCursorPos, GetCursorStartPos, GetContentRegionMax, GetWindowContentRegion* etc. + IMGUI_API float GetCursorPosY(); // other functions such as GetCursorScreenPos or everything in ImDrawList:: + IMGUI_API void SetCursorPos(const ImVec2& local_pos); // are using the main, absolute coordinate system. + IMGUI_API void SetCursorPosX(float local_x); // GetWindowPos() + GetCursorPos() == GetCursorScreenPos() etc.) + IMGUI_API void SetCursorPosY(float local_y); // + IMGUI_API ImVec2 GetCursorStartPos(); // initial cursor position in window coordinates + IMGUI_API ImVec2 GetCursorScreenPos(); // cursor position in absolute coordinates (useful to work with ImDrawList API). generally top-left == GetMainViewport()->Pos == (0,0) in single viewport mode, and bottom-right == GetMainViewport()->Pos+Size == io.DisplaySize in single-viewport mode. + IMGUI_API void SetCursorScreenPos(const ImVec2& pos); // cursor position in absolute coordinates + IMGUI_API void AlignTextToFramePadding(); // vertically align upcoming text baseline to FramePadding.y so that it will align properly to regularly framed items (call if you have text on a line before a framed item) + IMGUI_API float GetTextLineHeight(); // ~ FontSize + IMGUI_API float GetTextLineHeightWithSpacing(); // ~ FontSize + style.ItemSpacing.y (distance in pixels between 2 consecutive lines of text) + IMGUI_API float GetFrameHeight(); // ~ FontSize + style.FramePadding.y * 2 + IMGUI_API float GetFrameHeightWithSpacing(); // ~ FontSize + style.FramePadding.y * 2 + style.ItemSpacing.y (distance in pixels between 2 consecutive lines of framed widgets) + + // ID stack/scopes + // Read the FAQ (docs/FAQ.md or http://dearimgui.com/faq) for more details about how ID are handled in dear imgui. + // - Those questions are answered and impacted by understanding of the ID stack system: + // - "Q: Why is my widget not reacting when I click on it?" + // - "Q: How can I have widgets with an empty label?" + // - "Q: How can I have multiple widgets with the same label?" + // - Short version: ID are hashes of the entire ID stack. If you are creating widgets in a loop you most likely + // want to push a unique identifier (e.g. object pointer, loop index) to uniquely differentiate them. + // - You can also use the "Label##foobar" syntax within widget label to distinguish them from each others. + // - In this header file we use the "label"/"name" terminology to denote a string that will be displayed + used as an ID, + // whereas "str_id" denote a string that is only used as an ID and not normally displayed. + IMGUI_API void PushID(const char* str_id); // push string into the ID stack (will hash string). + IMGUI_API void PushID(const char* str_id_begin, const char* str_id_end); // push string into the ID stack (will hash string). + IMGUI_API void PushID(const void* ptr_id); // push pointer into the ID stack (will hash pointer). + IMGUI_API void PushID(int int_id); // push integer into the ID stack (will hash integer). + IMGUI_API void PopID(); // pop from the ID stack. + IMGUI_API ImGuiID GetID(const char* str_id); // calculate unique ID (hash of whole ID stack + given parameter). e.g. if you want to query into ImGuiStorage yourself + IMGUI_API ImGuiID GetID(const char* str_id_begin, const char* str_id_end); + IMGUI_API ImGuiID GetID(const void* ptr_id); + + // Widgets: Text + IMGUI_API void TextUnformatted(const char* text, const char* text_end = NULL); // raw text without formatting. Roughly equivalent to Text("%s", text) but: A) doesn't require null terminated string if 'text_end' is specified, B) it's faster, no memory copy is done, no buffer size limits, recommended for long chunks of text. + IMGUI_API void Text(const char* fmt, ...) IM_FMTARGS(1); // formatted text + IMGUI_API void TextV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void TextColored(const ImVec4& col, const char* fmt, ...) IM_FMTARGS(2); // shortcut for PushStyleColor(ImGuiCol_Text, col); Text(fmt, ...); PopStyleColor(); + IMGUI_API void TextColoredV(const ImVec4& col, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API void TextDisabled(const char* fmt, ...) IM_FMTARGS(1); // shortcut for PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]); Text(fmt, ...); PopStyleColor(); + IMGUI_API void TextDisabledV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void TextWrapped(const char* fmt, ...) IM_FMTARGS(1); // shortcut for PushTextWrapPos(0.0f); Text(fmt, ...); PopTextWrapPos();. Note that this won't work on an auto-resizing window if there's no other widgets to extend the window width, yoy may need to set a size using SetNextWindowSize(). + IMGUI_API void TextWrappedV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void LabelText(const char* label, const char* fmt, ...) IM_FMTARGS(2); // display text+label aligned the same way as value+label widgets + IMGUI_API void LabelTextV(const char* label, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API void BulletText(const char* fmt, ...) IM_FMTARGS(1); // shortcut for Bullet()+Text() + IMGUI_API void BulletTextV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void SeparatorText(const char* label); // currently: formatted text with an horizontal line + + // Widgets: Main + // - Most widgets return true when the value has been changed or when pressed/selected + // - You may also use one of the many IsItemXXX functions (e.g. IsItemActive, IsItemHovered, etc.) to query widget state. + IMGUI_API bool Button(const char* label, const ImVec2& size = ImVec2(0, 0)); // button + IMGUI_API bool SmallButton(const char* label); // button with FramePadding=(0,0) to easily embed within text + IMGUI_API bool InvisibleButton(const char* str_id, const ImVec2& size, ImGuiButtonFlags flags = 0); // flexible button behavior without the visuals, frequently useful to build custom behaviors using the public api (along with IsItemActive, IsItemHovered, etc.) + IMGUI_API bool ArrowButton(const char* str_id, ImGuiDir dir); // square button with an arrow shape + IMGUI_API bool Checkbox(const char* label, bool* v); + IMGUI_API bool CheckboxFlags(const char* label, int* flags, int flags_value); + IMGUI_API bool CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value); + IMGUI_API bool RadioButton(const char* label, bool active); // use with e.g. if (RadioButton("one", my_value==1)) { my_value = 1; } + IMGUI_API bool RadioButton(const char* label, int* v, int v_button); // shortcut to handle the above pattern when value is an integer + IMGUI_API void ProgressBar(float fraction, const ImVec2& size_arg = ImVec2(-FLT_MIN, 0), const char* overlay = NULL); + IMGUI_API void Bullet(); // draw a small circle + keep the cursor on the same line. advance cursor x position by GetTreeNodeToLabelSpacing(), same distance that TreeNode() uses + + // Widgets: Images + // - Read about ImTextureID here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples + IMGUI_API void Image(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& tint_col = ImVec4(1, 1, 1, 1), const ImVec4& border_col = ImVec4(0, 0, 0, 0)); + IMGUI_API bool ImageButton(const char* str_id, ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); + + // Widgets: Combo Box (Dropdown) + // - The BeginCombo()/EndCombo() api allows you to manage your contents and selection state however you want it, by creating e.g. Selectable() items. + // - The old Combo() api are helpers over BeginCombo()/EndCombo() which are kept available for convenience purpose. This is analogous to how ListBox are created. + IMGUI_API bool BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags = 0); + IMGUI_API void EndCombo(); // only call EndCombo() if BeginCombo() returns true! + IMGUI_API bool Combo(const char* label, int* current_item, const char* const items[], int items_count, int popup_max_height_in_items = -1); + IMGUI_API bool Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int popup_max_height_in_items = -1); // Separate items with \0 within a string, end item-list with \0\0. e.g. "One\0Two\0Three\0" + IMGUI_API bool Combo(const char* label, int* current_item, bool(*items_getter)(void* data, int idx, const char** out_text), void* data, int items_count, int popup_max_height_in_items = -1); + + // Widgets: Drag Sliders + // - CTRL+Click on any drag box to turn them into an input box. Manually input values aren't clamped by default and can go off-bounds. Use ImGuiSliderFlags_AlwaysClamp to always clamp. + // - For all the Float2/Float3/Float4/Int2/Int3/Int4 versions of every function, note that a 'float v[X]' function argument is the same as 'float* v', + // the array syntax is just a way to document the number of elements that are expected to be accessible. You can pass address of your first element out of a contiguous set, e.g. &myvector.x + // - Adjust format string to decorate the value with a prefix, a suffix, or adapt the editing and display precision e.g. "%.3f" -> 1.234; "%5.2f secs" -> 01.23 secs; "Biscuit: %.0f" -> Biscuit: 1; etc. + // - Format string may also be set to NULL or use the default format ("%f" or "%d"). + // - Speed are per-pixel of mouse movement (v_speed=0.2f: mouse needs to move by 5 pixels to increase value by 1). For gamepad/keyboard navigation, minimum speed is Max(v_speed, minimum_step_at_given_precision). + // - Use v_min < v_max to clamp edits to given limits. Note that CTRL+Click manual input can override those limits if ImGuiSliderFlags_AlwaysClamp is not used. + // - Use v_max = FLT_MAX / INT_MAX etc to avoid clamping to a maximum, same with v_min = -FLT_MAX / INT_MIN to avoid clamping to a minimum. + // - We use the same sets of flags for DragXXX() and SliderXXX() functions as the features are the same and it makes it easier to swap them. + // - Legacy: Pre-1.78 there are DragXXX() function signatures that take a final `float power=1.0f' argument instead of the `ImGuiSliderFlags flags=0' argument. + // If you get a warning converting a float to ImGuiSliderFlags, read https://github.com/ocornut/imgui/issues/3361 + IMGUI_API bool DragFloat(const char* label, float* v, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); // If v_min >= v_max we have no bound + IMGUI_API bool DragFloat2(const char* label, float v[2], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragFloat3(const char* label, float v[3], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragFloat4(const char* label, float v[4], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", const char* format_max = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool DragInt(const char* label, int* v, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); // If v_min >= v_max we have no bound + IMGUI_API bool DragInt2(const char* label, int v[2], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragInt3(const char* label, int v[3], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragInt4(const char* label, int v[4], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragIntRange2(const char* label, int* v_current_min, int* v_current_max, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", const char* format_max = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed = 1.0f, const void* p_min = NULL, const void* p_max = NULL, const char* format = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed = 1.0f, const void* p_min = NULL, const void* p_max = NULL, const char* format = NULL, ImGuiSliderFlags flags = 0); + + // Widgets: Regular Sliders + // - CTRL+Click on any slider to turn them into an input box. Manually input values aren't clamped by default and can go off-bounds. Use ImGuiSliderFlags_AlwaysClamp to always clamp. + // - Adjust format string to decorate the value with a prefix, a suffix, or adapt the editing and display precision e.g. "%.3f" -> 1.234; "%5.2f secs" -> 01.23 secs; "Biscuit: %.0f" -> Biscuit: 1; etc. + // - Format string may also be set to NULL or use the default format ("%f" or "%d"). + // - Legacy: Pre-1.78 there are SliderXXX() function signatures that take a final `float power=1.0f' argument instead of the `ImGuiSliderFlags flags=0' argument. + // If you get a warning converting a float to ImGuiSliderFlags, read https://github.com/ocornut/imgui/issues/3361 + IMGUI_API bool SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); // adjust format to decorate the value with a prefix or a suffix for in-slider labels or unit display. + IMGUI_API bool SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderAngle(const char* label, float* v_rad, float v_degrees_min = -360.0f, float v_degrees_max = +360.0f, const char* format = "%.0f deg", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt(const char* label, int* v, int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_min, const void* p_max, const char* format = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format = NULL, ImGuiSliderFlags flags = 0); + + // Widgets: Input with Keyboard + // - If you want to use InputText() with std::string or any custom dynamic string type, see misc/cpp/imgui_stdlib.h and comments in imgui_demo.cpp. + // - Most of the ImGuiInputTextFlags flags are only useful for InputText() and not for InputFloatX, InputIntX, InputDouble etc. + IMGUI_API bool InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size = ImVec2(0, 0), ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputFloat(const char* label, float* v, float step = 0.0f, float step_fast = 0.0f, const char* format = "%.3f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputFloat2(const char* label, float v[2], const char* format = "%.3f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputFloat3(const char* label, float v[3], const char* format = "%.3f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputFloat4(const char* label, float v[4], const char* format = "%.3f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputInt(const char* label, int* v, int step = 1, int step_fast = 100, ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputDouble(const char* label, double* v, double step = 0.0, double step_fast = 0.0, const char* format = "%.6f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_step = NULL, const void* p_step_fast = NULL, const char* format = NULL, ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_step = NULL, const void* p_step_fast = NULL, const char* format = NULL, ImGuiInputTextFlags flags = 0); + + // Widgets: Color Editor/Picker (tip: the ColorEdit* functions have a little color square that can be left-clicked to open a picker, and right-clicked to open an option menu.) + // - Note that in C++ a 'float v[X]' function argument is the _same_ as 'float* v', the array syntax is just a way to document the number of elements that are expected to be accessible. + // - You can pass the address of a first float element out of a contiguous structure, e.g. &myvector.x + IMGUI_API bool ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags = 0); + IMGUI_API bool ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags = 0); + IMGUI_API bool ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags = 0); + IMGUI_API bool ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags = 0, const float* ref_col = NULL); + IMGUI_API bool ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // display a color square/button, hover for details, return true when pressed. + IMGUI_API void SetColorEditOptions(ImGuiColorEditFlags flags); // initialize current options (generally on application startup) if you want to select a default format, picker type, etc. User will be able to change many settings, unless you pass the _NoOptions flag to your calls. + + // Widgets: Trees + // - TreeNode functions return true when the node is open, in which case you need to also call TreePop() when you are finished displaying the tree node contents. + IMGUI_API bool TreeNode(const char* label); + IMGUI_API bool TreeNode(const char* str_id, const char* fmt, ...) IM_FMTARGS(2); // helper variation to easily decorelate the id from the displayed string. Read the FAQ about why and how to use ID. to align arbitrary text at the same level as a TreeNode() you can use Bullet(). + IMGUI_API bool TreeNode(const void* ptr_id, const char* fmt, ...) IM_FMTARGS(2); // " + IMGUI_API bool TreeNodeV(const char* str_id, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API bool TreeNodeV(const void* ptr_id, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API bool TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags = 0); + IMGUI_API bool TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(3); + IMGUI_API bool TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(3); + IMGUI_API bool TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(3); + IMGUI_API bool TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(3); + IMGUI_API void TreePush(const char* str_id); // ~ Indent()+PushId(). Already called by TreeNode() when returning true, but you can call TreePush/TreePop yourself if desired. + IMGUI_API void TreePush(const void* ptr_id); // " + IMGUI_API void TreePop(); // ~ Unindent()+PopId() + IMGUI_API float GetTreeNodeToLabelSpacing(); // horizontal distance preceding label when using TreeNode*() or Bullet() == (g.FontSize + style.FramePadding.x*2) for a regular unframed TreeNode + IMGUI_API bool CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags = 0); // if returning 'true' the header is open. doesn't indent nor push on ID stack. user doesn't have to call TreePop(). + IMGUI_API bool CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags = 0); // when 'p_visible != NULL': if '*p_visible==true' display an additional small close button on upper right of the header which will set the bool to false when clicked, if '*p_visible==false' don't display the header. + IMGUI_API void SetNextItemOpen(bool is_open, ImGuiCond cond = 0); // set next TreeNode/CollapsingHeader open state. + + // Widgets: Selectables + // - A selectable highlights when hovered, and can display another color when selected. + // - Neighbors selectable extend their highlight bounds in order to leave no gap between them. This is so a series of selected Selectable appear contiguous. + IMGUI_API bool Selectable(const char* label, bool selected = false, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // "bool selected" carry the selection state (read-only). Selectable() is clicked is returns true so you can modify your selection state. size.x==0.0: use remaining width, size.x>0.0: specify width. size.y==0.0: use label height, size.y>0.0: specify height + IMGUI_API bool Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // "bool* p_selected" point to the selection state (read-write), as a convenient helper. + + // Widgets: List Boxes + // - This is essentially a thin wrapper to using BeginChild/EndChild with some stylistic changes. + // - The BeginListBox()/EndListBox() api allows you to manage your contents and selection state however you want it, by creating e.g. Selectable() or any items. + // - The simplified/old ListBox() api are helpers over BeginListBox()/EndListBox() which are kept available for convenience purpose. This is analoguous to how Combos are created. + // - Choose frame width: size.x > 0.0f: custom / size.x < 0.0f or -FLT_MIN: right-align / size.x = 0.0f (default): use current ItemWidth + // - Choose frame height: size.y > 0.0f: custom / size.y < 0.0f or -FLT_MIN: bottom-align / size.y = 0.0f (default): arbitrary default height which can fit ~7 items + IMGUI_API bool BeginListBox(const char* label, const ImVec2& size = ImVec2(0, 0)); // open a framed scrolling region + IMGUI_API void EndListBox(); // only call EndListBox() if BeginListBox() returned true! + IMGUI_API bool ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items = -1); + IMGUI_API bool ListBox(const char* label, int* current_item, bool (*items_getter)(void* data, int idx, const char** out_text), void* data, int items_count, int height_in_items = -1); + + // Widgets: Data Plotting + // - Consider using ImPlot (https://github.com/epezent/implot) which is much better! + IMGUI_API void PlotLines(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0), int stride = sizeof(float)); + IMGUI_API void PlotLines(const char* label, float(*values_getter)(void* data, int idx), void* data, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0)); + IMGUI_API void PlotHistogram(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0), int stride = sizeof(float)); + IMGUI_API void PlotHistogram(const char* label, float(*values_getter)(void* data, int idx), void* data, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0)); + + // Widgets: Value() Helpers. + // - Those are merely shortcut to calling Text() with a format string. Output single value in "name: value" format (tip: freely declare more in your code to handle your types. you can add functions to the ImGui namespace) + IMGUI_API void Value(const char* prefix, bool b); + IMGUI_API void Value(const char* prefix, int v); + IMGUI_API void Value(const char* prefix, unsigned int v); + IMGUI_API void Value(const char* prefix, float v, const char* float_format = NULL); + + // Widgets: Menus + // - Use BeginMenuBar() on a window ImGuiWindowFlags_MenuBar to append to its menu bar. + // - Use BeginMainMenuBar() to create a menu bar at the top of the screen and append to it. + // - Use BeginMenu() to create a menu. You can call BeginMenu() multiple time with the same identifier to append more items to it. + // - Not that MenuItem() keyboardshortcuts are displayed as a convenience but _not processed_ by Dear ImGui at the moment. + IMGUI_API bool BeginMenuBar(); // append to menu-bar of current window (requires ImGuiWindowFlags_MenuBar flag set on parent window). + IMGUI_API void EndMenuBar(); // only call EndMenuBar() if BeginMenuBar() returns true! + IMGUI_API bool BeginMainMenuBar(); // create and append to a full screen menu-bar. + IMGUI_API void EndMainMenuBar(); // only call EndMainMenuBar() if BeginMainMenuBar() returns true! + IMGUI_API bool BeginMenu(const char* label, bool enabled = true); // create a sub-menu entry. only call EndMenu() if this returns true! + IMGUI_API void EndMenu(); // only call EndMenu() if BeginMenu() returns true! + IMGUI_API bool MenuItem(const char* label, const char* shortcut = NULL, bool selected = false, bool enabled = true); // return true when activated. + IMGUI_API bool MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled = true); // return true when activated + toggle (*p_selected) if p_selected != NULL + + // Tooltips + // - Tooltips are windows following the mouse. They do not take focus away. + // - A tooltip window can contain items of any types. SetTooltip() is a shortcut for the 'if (BeginTooltip()) { Text(...); EndTooltip(); }' idiom. + IMGUI_API bool BeginTooltip(); // begin/append a tooltip window. + IMGUI_API void EndTooltip(); // only call EndTooltip() if BeginTooltip()/BeginItemTooltip() returns true! + IMGUI_API void SetTooltip(const char* fmt, ...) IM_FMTARGS(1); // set a text-only tooltip. Often used after a ImGui::IsItemHovered() check. Override any previous call to SetTooltip(). + IMGUI_API void SetTooltipV(const char* fmt, va_list args) IM_FMTLIST(1); + + // Tooltips: helpers for showing a tooltip when hovering an item + // - BeginItemTooltip() is a shortcut for the 'if (IsItemHovered(ImGuiHoveredFlags_Tooltip) && BeginTooltip())' idiom. + // - SetItemTooltip() is a shortcut for the 'if (IsItemHovered(ImGuiHoveredFlags_Tooltip)) { SetTooltip(...); }' idiom. + // - Where 'ImGuiHoveredFlags_Tooltip' itself is a shortcut to use 'style.HoverFlagsForTooltipMouse' or 'style.HoverFlagsForTooltipNav' depending on active input type. For mouse it defaults to 'ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort'. + IMGUI_API bool BeginItemTooltip(); // begin/append a tooltip window if preceding item was hovered. + IMGUI_API void SetItemTooltip(const char* fmt, ...) IM_FMTARGS(1); // set a text-only tooltip if preceeding item was hovered. override any previous call to SetTooltip(). + IMGUI_API void SetItemTooltipV(const char* fmt, va_list args) IM_FMTLIST(1); + + // Popups, Modals + // - They block normal mouse hovering detection (and therefore most mouse interactions) behind them. + // - If not modal: they can be closed by clicking anywhere outside them, or by pressing ESCAPE. + // - Their visibility state (~bool) is held internally instead of being held by the programmer as we are used to with regular Begin*() calls. + // - The 3 properties above are related: we need to retain popup visibility state in the library because popups may be closed as any time. + // - You can bypass the hovering restriction by using ImGuiHoveredFlags_AllowWhenBlockedByPopup when calling IsItemHovered() or IsWindowHovered(). + // - IMPORTANT: Popup identifiers are relative to the current ID stack, so OpenPopup and BeginPopup generally needs to be at the same level of the stack. + // This is sometimes leading to confusing mistakes. May rework this in the future. + + // Popups: begin/end functions + // - BeginPopup(): query popup state, if open start appending into the window. Call EndPopup() afterwards. ImGuiWindowFlags are forwarded to the window. + // - BeginPopupModal(): block every interaction behind the window, cannot be closed by user, add a dimming background, has a title bar. + IMGUI_API bool BeginPopup(const char* str_id, ImGuiWindowFlags flags = 0); // return true if the popup is open, and you can start outputting to it. + IMGUI_API bool BeginPopupModal(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0); // return true if the modal is open, and you can start outputting to it. + IMGUI_API void EndPopup(); // only call EndPopup() if BeginPopupXXX() returns true! + + // Popups: open/close functions + // - OpenPopup(): set popup state to open. ImGuiPopupFlags are available for opening options. + // - If not modal: they can be closed by clicking anywhere outside them, or by pressing ESCAPE. + // - CloseCurrentPopup(): use inside the BeginPopup()/EndPopup() scope to close manually. + // - CloseCurrentPopup() is called by default by Selectable()/MenuItem() when activated (FIXME: need some options). + // - Use ImGuiPopupFlags_NoOpenOverExistingPopup to avoid opening a popup if there's already one at the same level. This is equivalent to e.g. testing for !IsAnyPopupOpen() prior to OpenPopup(). + // - Use IsWindowAppearing() after BeginPopup() to tell if a window just opened. + // - IMPORTANT: Notice that for OpenPopupOnItemClick() we exceptionally default flags to 1 (== ImGuiPopupFlags_MouseButtonRight) for backward compatibility with older API taking 'int mouse_button = 1' parameter + IMGUI_API void OpenPopup(const char* str_id, ImGuiPopupFlags popup_flags = 0); // call to mark popup as open (don't call every frame!). + IMGUI_API void OpenPopup(ImGuiID id, ImGuiPopupFlags popup_flags = 0); // id overload to facilitate calling from nested stacks + IMGUI_API void OpenPopupOnItemClick(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1); // helper to open popup when clicked on last item. Default to ImGuiPopupFlags_MouseButtonRight == 1. (note: actually triggers on the mouse _released_ event to be consistent with popup behaviors) + IMGUI_API void CloseCurrentPopup(); // manually close the popup we have begin-ed into. + + // Popups: open+begin combined functions helpers + // - Helpers to do OpenPopup+BeginPopup where the Open action is triggered by e.g. hovering an item and right-clicking. + // - They are convenient to easily create context menus, hence the name. + // - IMPORTANT: Notice that BeginPopupContextXXX takes ImGuiPopupFlags just like OpenPopup() and unlike BeginPopup(). For full consistency, we may add ImGuiWindowFlags to the BeginPopupContextXXX functions in the future. + // - IMPORTANT: Notice that we exceptionally default their flags to 1 (== ImGuiPopupFlags_MouseButtonRight) for backward compatibility with older API taking 'int mouse_button = 1' parameter, so if you add other flags remember to re-add the ImGuiPopupFlags_MouseButtonRight. + IMGUI_API bool BeginPopupContextItem(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1); // open+begin popup when clicked on last item. Use str_id==NULL to associate the popup to previous item. If you want to use that on a non-interactive item such as Text() you need to pass in an explicit ID here. read comments in .cpp! + IMGUI_API bool BeginPopupContextWindow(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1);// open+begin popup when clicked on current window. + IMGUI_API bool BeginPopupContextVoid(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1); // open+begin popup when clicked in void (where there are no windows). + + // Popups: query functions + // - IsPopupOpen(): return true if the popup is open at the current BeginPopup() level of the popup stack. + // - IsPopupOpen() with ImGuiPopupFlags_AnyPopupId: return true if any popup is open at the current BeginPopup() level of the popup stack. + // - IsPopupOpen() with ImGuiPopupFlags_AnyPopupId + ImGuiPopupFlags_AnyPopupLevel: return true if any popup is open. + IMGUI_API bool IsPopupOpen(const char* str_id, ImGuiPopupFlags flags = 0); // return true if the popup is open. + + // Tables + // - Full-featured replacement for old Columns API. + // - See Demo->Tables for demo code. See top of imgui_tables.cpp for general commentary. + // - See ImGuiTableFlags_ and ImGuiTableColumnFlags_ enums for a description of available flags. + // The typical call flow is: + // - 1. Call BeginTable(), early out if returning false. + // - 2. Optionally call TableSetupColumn() to submit column name/flags/defaults. + // - 3. Optionally call TableSetupScrollFreeze() to request scroll freezing of columns/rows. + // - 4. Optionally call TableHeadersRow() to submit a header row. Names are pulled from TableSetupColumn() data. + // - 5. Populate contents: + // - In most situations you can use TableNextRow() + TableSetColumnIndex(N) to start appending into a column. + // - If you are using tables as a sort of grid, where every column is holding the same type of contents, + // you may prefer using TableNextColumn() instead of TableNextRow() + TableSetColumnIndex(). + // TableNextColumn() will automatically wrap-around into the next row if needed. + // - IMPORTANT: Comparatively to the old Columns() API, we need to call TableNextColumn() for the first column! + // - Summary of possible call flow: + // -------------------------------------------------------------------------------------------------------- + // TableNextRow() -> TableSetColumnIndex(0) -> Text("Hello 0") -> TableSetColumnIndex(1) -> Text("Hello 1") // OK + // TableNextRow() -> TableNextColumn() -> Text("Hello 0") -> TableNextColumn() -> Text("Hello 1") // OK + // TableNextColumn() -> Text("Hello 0") -> TableNextColumn() -> Text("Hello 1") // OK: TableNextColumn() automatically gets to next row! + // TableNextRow() -> Text("Hello 0") // Not OK! Missing TableSetColumnIndex() or TableNextColumn()! Text will not appear! + // -------------------------------------------------------------------------------------------------------- + // - 5. Call EndTable() + IMGUI_API bool BeginTable(const char* str_id, int column, ImGuiTableFlags flags = 0, const ImVec2& outer_size = ImVec2(0.0f, 0.0f), float inner_width = 0.0f); + IMGUI_API void EndTable(); // only call EndTable() if BeginTable() returns true! + IMGUI_API void TableNextRow(ImGuiTableRowFlags row_flags = 0, float min_row_height = 0.0f); // append into the first cell of a new row. + IMGUI_API bool TableNextColumn(); // append into the next column (or first column of next row if currently in last column). Return true when column is visible. + IMGUI_API bool TableSetColumnIndex(int column_n); // append into the specified column. Return true when column is visible. + + // Tables: Headers & Columns declaration + // - Use TableSetupColumn() to specify label, resizing policy, default width/weight, id, various other flags etc. + // - Use TableHeadersRow() to create a header row and automatically submit a TableHeader() for each column. + // Headers are required to perform: reordering, sorting, and opening the context menu. + // The context menu can also be made available in columns body using ImGuiTableFlags_ContextMenuInBody. + // - You may manually submit headers using TableNextRow() + TableHeader() calls, but this is only useful in + // some advanced use cases (e.g. adding custom widgets in header row). + // - Use TableSetupScrollFreeze() to lock columns/rows so they stay visible when scrolled. + IMGUI_API void TableSetupColumn(const char* label, ImGuiTableColumnFlags flags = 0, float init_width_or_weight = 0.0f, ImGuiID user_id = 0); + IMGUI_API void TableSetupScrollFreeze(int cols, int rows); // lock columns/rows so they stay visible when scrolled. + IMGUI_API void TableHeadersRow(); // submit all headers cells based on data provided to TableSetupColumn() + submit context menu + IMGUI_API void TableHeader(const char* label); // submit one header cell manually (rarely used) + + // Tables: Sorting & Miscellaneous functions + // - Sorting: call TableGetSortSpecs() to retrieve latest sort specs for the table. NULL when not sorting. + // When 'sort_specs->SpecsDirty == true' you should sort your data. It will be true when sorting specs have + // changed since last call, or the first time. Make sure to set 'SpecsDirty = false' after sorting, + // else you may wastefully sort your data every frame! + // - Functions args 'int column_n' treat the default value of -1 as the same as passing the current column index. + IMGUI_API ImGuiTableSortSpecs* TableGetSortSpecs(); // get latest sort specs for the table (NULL if not sorting). Lifetime: don't hold on this pointer over multiple frames or past any subsequent call to BeginTable(). + IMGUI_API int TableGetColumnCount(); // return number of columns (value passed to BeginTable) + IMGUI_API int TableGetColumnIndex(); // return current column index. + IMGUI_API int TableGetRowIndex(); // return current row index. + IMGUI_API const char* TableGetColumnName(int column_n = -1); // return "" if column didn't have a name declared by TableSetupColumn(). Pass -1 to use current column. + IMGUI_API ImGuiTableColumnFlags TableGetColumnFlags(int column_n = -1); // return column flags so you can query their Enabled/Visible/Sorted/Hovered status flags. Pass -1 to use current column. + IMGUI_API void TableSetColumnEnabled(int column_n, bool v);// change user accessible enabled/disabled state of a column. Set to false to hide the column. User can use the context menu to change this themselves (right-click in headers, or right-click in columns body with ImGuiTableFlags_ContextMenuInBody) + IMGUI_API void TableSetBgColor(ImGuiTableBgTarget target, ImU32 color, int column_n = -1); // change the color of a cell, row, or column. See ImGuiTableBgTarget_ flags for details. + + // Legacy Columns API (prefer using Tables!) + // - You can also use SameLine(pos_x) to mimic simplified columns. + IMGUI_API void Columns(int count = 1, const char* id = NULL, bool border = true); + IMGUI_API void NextColumn(); // next column, defaults to current row or next row if the current row is finished + IMGUI_API int GetColumnIndex(); // get current column index + IMGUI_API float GetColumnWidth(int column_index = -1); // get column width (in pixels). pass -1 to use current column + IMGUI_API void SetColumnWidth(int column_index, float width); // set column width (in pixels). pass -1 to use current column + IMGUI_API float GetColumnOffset(int column_index = -1); // get position of column line (in pixels, from the left side of the contents region). pass -1 to use current column, otherwise 0..GetColumnsCount() inclusive. column 0 is typically 0.0f + IMGUI_API void SetColumnOffset(int column_index, float offset_x); // set position of column line (in pixels, from the left side of the contents region). pass -1 to use current column + IMGUI_API int GetColumnsCount(); + + // Tab Bars, Tabs + // - Note: Tabs are automatically created by the docking system (when in 'docking' branch). Use this to create tab bars/tabs yourself. + IMGUI_API bool BeginTabBar(const char* str_id, ImGuiTabBarFlags flags = 0); // create and append into a TabBar + IMGUI_API void EndTabBar(); // only call EndTabBar() if BeginTabBar() returns true! + IMGUI_API bool BeginTabItem(const char* label, bool* p_open = NULL, ImGuiTabItemFlags flags = 0); // create a Tab. Returns true if the Tab is selected. + IMGUI_API void EndTabItem(); // only call EndTabItem() if BeginTabItem() returns true! + IMGUI_API bool TabItemButton(const char* label, ImGuiTabItemFlags flags = 0); // create a Tab behaving like a button. return true when clicked. cannot be selected in the tab bar. + IMGUI_API void SetTabItemClosed(const char* tab_or_docked_window_label); // notify TabBar or Docking system of a closed tab/window ahead (useful to reduce visual flicker on reorderable tab bars). For tab-bar: call after BeginTabBar() and before Tab submissions. Otherwise call with a window name. + + // Docking + // [BETA API] Enable with io.ConfigFlags |= ImGuiConfigFlags_DockingEnable. + // Note: You can use most Docking facilities without calling any API. You DO NOT need to call DockSpace() to use Docking! + // - Drag from window title bar or their tab to dock/undock. Hold SHIFT to disable docking/undocking. + // - Drag from window menu button (upper-left button) to undock an entire node (all windows). + // - When io.ConfigDockingWithShift == true, you instead need to hold SHIFT to _enable_ docking/undocking. + // About dockspaces: + // - Use DockSpace() to create an explicit dock node _within_ an existing window. See Docking demo for details. + // - Use DockSpaceOverViewport() to create an explicit dock node covering the screen or a specific viewport. + // This is often used with ImGuiDockNodeFlags_PassthruCentralNode. + // - Important: Dockspaces need to be submitted _before_ any window they can host. Submit it early in your frame! + // - Important: Dockspaces need to be kept alive if hidden, otherwise windows docked into it will be undocked. + // e.g. if you have multiple tabs with a dockspace inside each tab: submit the non-visible dockspaces with ImGuiDockNodeFlags_KeepAliveOnly. + IMGUI_API ImGuiID DockSpace(ImGuiID id, const ImVec2& size = ImVec2(0, 0), ImGuiDockNodeFlags flags = 0, const ImGuiWindowClass* window_class = NULL); + IMGUI_API ImGuiID DockSpaceOverViewport(const ImGuiViewport* viewport = NULL, ImGuiDockNodeFlags flags = 0, const ImGuiWindowClass* window_class = NULL); + IMGUI_API void SetNextWindowDockID(ImGuiID dock_id, ImGuiCond cond = 0); // set next window dock id + IMGUI_API void SetNextWindowClass(const ImGuiWindowClass* window_class); // set next window class (control docking compatibility + provide hints to platform backend via custom viewport flags and platform parent/child relationship) + IMGUI_API ImGuiID GetWindowDockID(); + IMGUI_API bool IsWindowDocked(); // is current window docked into another window? + + // Logging/Capture + // - All text output from the interface can be captured into tty/file/clipboard. By default, tree nodes are automatically opened during logging. + IMGUI_API void LogToTTY(int auto_open_depth = -1); // start logging to tty (stdout) + IMGUI_API void LogToFile(int auto_open_depth = -1, const char* filename = NULL); // start logging to file + IMGUI_API void LogToClipboard(int auto_open_depth = -1); // start logging to OS clipboard + IMGUI_API void LogFinish(); // stop logging (close file, etc.) + IMGUI_API void LogButtons(); // helper to display buttons for logging to tty/file/clipboard + IMGUI_API void LogText(const char* fmt, ...) IM_FMTARGS(1); // pass text data straight to log (without being displayed) + IMGUI_API void LogTextV(const char* fmt, va_list args) IM_FMTLIST(1); + + // Drag and Drop + // - On source items, call BeginDragDropSource(), if it returns true also call SetDragDropPayload() + EndDragDropSource(). + // - On target candidates, call BeginDragDropTarget(), if it returns true also call AcceptDragDropPayload() + EndDragDropTarget(). + // - If you stop calling BeginDragDropSource() the payload is preserved however it won't have a preview tooltip (we currently display a fallback "..." tooltip, see #1725) + // - An item can be both drag source and drop target. + IMGUI_API bool BeginDragDropSource(ImGuiDragDropFlags flags = 0); // call after submitting an item which may be dragged. when this return true, you can call SetDragDropPayload() + EndDragDropSource() + IMGUI_API bool SetDragDropPayload(const char* type, const void* data, size_t sz, ImGuiCond cond = 0); // type is a user defined string of maximum 32 characters. Strings starting with '_' are reserved for dear imgui internal types. Data is copied and held by imgui. Return true when payload has been accepted. + IMGUI_API void EndDragDropSource(); // only call EndDragDropSource() if BeginDragDropSource() returns true! + IMGUI_API bool BeginDragDropTarget(); // call after submitting an item that may receive a payload. If this returns true, you can call AcceptDragDropPayload() + EndDragDropTarget() + IMGUI_API const ImGuiPayload* AcceptDragDropPayload(const char* type, ImGuiDragDropFlags flags = 0); // accept contents of a given type. If ImGuiDragDropFlags_AcceptBeforeDelivery is set you can peek into the payload before the mouse button is released. + IMGUI_API void EndDragDropTarget(); // only call EndDragDropTarget() if BeginDragDropTarget() returns true! + IMGUI_API const ImGuiPayload* GetDragDropPayload(); // peek directly into the current payload from anywhere. may return NULL. use ImGuiPayload::IsDataType() to test for the payload type. + + // Disabling [BETA API] + // - Disable all user interactions and dim items visuals (applying style.DisabledAlpha over current colors) + // - Those can be nested but it cannot be used to enable an already disabled section (a single BeginDisabled(true) in the stack is enough to keep everything disabled) + // - BeginDisabled(false) essentially does nothing useful but is provided to facilitate use of boolean expressions. If you can avoid calling BeginDisabled(False)/EndDisabled() best to avoid it. + IMGUI_API void BeginDisabled(bool disabled = true); + IMGUI_API void EndDisabled(); + + // Clipping + // - Mouse hovering is affected by ImGui::PushClipRect() calls, unlike direct calls to ImDrawList::PushClipRect() which are render only. + IMGUI_API void PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect); + IMGUI_API void PopClipRect(); + + // Focus, Activation + // - Prefer using "SetItemDefaultFocus()" over "if (IsWindowAppearing()) SetScrollHereY()" when applicable to signify "this is the default item" + IMGUI_API void SetItemDefaultFocus(); // make last item the default focused item of a window. + IMGUI_API void SetKeyboardFocusHere(int offset = 0); // focus keyboard on the next widget. Use positive 'offset' to access sub components of a multiple component widget. Use -1 to access previous widget. + + // Overlapping mode + IMGUI_API void SetNextItemAllowOverlap(); // allow next item to be overlapped by a subsequent item. Useful with invisible buttons, selectable, treenode covering an area where subsequent items may need to be added. Note that both Selectable() and TreeNode() have dedicated flags doing this. + + // Item/Widgets Utilities and Query Functions + // - Most of the functions are referring to the previous Item that has been submitted. + // - See Demo Window under "Widgets->Querying Status" for an interactive visualization of most of those functions. + IMGUI_API bool IsItemHovered(ImGuiHoveredFlags flags = 0); // is the last item hovered? (and usable, aka not blocked by a popup, etc.). See ImGuiHoveredFlags for more options. + IMGUI_API bool IsItemActive(); // is the last item active? (e.g. button being held, text field being edited. This will continuously return true while holding mouse button on an item. Items that don't interact will always return false) + IMGUI_API bool IsItemFocused(); // is the last item focused for keyboard/gamepad navigation? + IMGUI_API bool IsItemClicked(ImGuiMouseButton mouse_button = 0); // is the last item hovered and mouse clicked on? (**) == IsMouseClicked(mouse_button) && IsItemHovered()Important. (**) this is NOT equivalent to the behavior of e.g. Button(). Read comments in function definition. + IMGUI_API bool IsItemVisible(); // is the last item visible? (items may be out of sight because of clipping/scrolling) + IMGUI_API bool IsItemEdited(); // did the last item modify its underlying value this frame? or was pressed? This is generally the same as the "bool" return value of many widgets. + IMGUI_API bool IsItemActivated(); // was the last item just made active (item was previously inactive). + IMGUI_API bool IsItemDeactivated(); // was the last item just made inactive (item was previously active). Useful for Undo/Redo patterns with widgets that require continuous editing. + IMGUI_API bool IsItemDeactivatedAfterEdit(); // was the last item just made inactive and made a value change when it was active? (e.g. Slider/Drag moved). Useful for Undo/Redo patterns with widgets that require continuous editing. Note that you may get false positives (some widgets such as Combo()/ListBox()/Selectable() will return true even when clicking an already selected item). + IMGUI_API bool IsItemToggledOpen(); // was the last item open state toggled? set by TreeNode(). + IMGUI_API bool IsAnyItemHovered(); // is any item hovered? + IMGUI_API bool IsAnyItemActive(); // is any item active? + IMGUI_API bool IsAnyItemFocused(); // is any item focused? + IMGUI_API ImGuiID GetItemID(); // get ID of last item (~~ often same ImGui::GetID(label) beforehand) + IMGUI_API ImVec2 GetItemRectMin(); // get upper-left bounding rectangle of the last item (screen space) + IMGUI_API ImVec2 GetItemRectMax(); // get lower-right bounding rectangle of the last item (screen space) + IMGUI_API ImVec2 GetItemRectSize(); // get size of last item + + // Viewports + // - Currently represents the Platform Window created by the application which is hosting our Dear ImGui windows. + // - In 'docking' branch with multi-viewport enabled, we extend this concept to have multiple active viewports. + // - In the future we will extend this concept further to also represent Platform Monitor and support a "no main platform window" operation mode. + IMGUI_API ImGuiViewport* GetMainViewport(); // return primary/default viewport. This can never be NULL. + + // Background/Foreground Draw Lists + IMGUI_API ImDrawList* GetBackgroundDrawList(); // get background draw list for the viewport associated to the current window. this draw list will be the first rendering one. Useful to quickly draw shapes/text behind dear imgui contents. + IMGUI_API ImDrawList* GetForegroundDrawList(); // get foreground draw list for the viewport associated to the current window. this draw list will be the last rendered one. Useful to quickly draw shapes/text over dear imgui contents. + IMGUI_API ImDrawList* GetBackgroundDrawList(ImGuiViewport* viewport); // get background draw list for the given viewport. this draw list will be the first rendering one. Useful to quickly draw shapes/text behind dear imgui contents. + IMGUI_API ImDrawList* GetForegroundDrawList(ImGuiViewport* viewport); // get foreground draw list for the given viewport. this draw list will be the last rendered one. Useful to quickly draw shapes/text over dear imgui contents. + + // Miscellaneous Utilities + IMGUI_API bool IsRectVisible(const ImVec2& size); // test if rectangle (of given size, starting from cursor position) is visible / not clipped. + IMGUI_API bool IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max); // test if rectangle (in screen space) is visible / not clipped. to perform coarse clipping on user's side. + IMGUI_API double GetTime(); // get global imgui time. incremented by io.DeltaTime every frame. + IMGUI_API int GetFrameCount(); // get global imgui frame count. incremented by 1 every frame. + IMGUI_API ImDrawListSharedData* GetDrawListSharedData(); // you may use this when creating your own ImDrawList instances. + IMGUI_API const char* GetStyleColorName(ImGuiCol idx); // get a string corresponding to the enum value (for display, saving, etc.). + IMGUI_API void SetStateStorage(ImGuiStorage* storage); // replace current window storage with our own (if you want to manipulate it yourself, typically clear subsection of it) + IMGUI_API ImGuiStorage* GetStateStorage(); + IMGUI_API bool BeginChildFrame(ImGuiID id, const ImVec2& size, ImGuiWindowFlags flags = 0); // helper to create a child window / scrolling region that looks like a normal widget frame + IMGUI_API void EndChildFrame(); // always call EndChildFrame() regardless of BeginChildFrame() return values (which indicates a collapsed/clipped window) + + // Text Utilities + IMGUI_API ImVec2 CalcTextSize(const char* text, const char* text_end = NULL, bool hide_text_after_double_hash = false, float wrap_width = -1.0f); + + // Color Utilities + IMGUI_API ImVec4 ColorConvertU32ToFloat4(ImU32 in); + IMGUI_API ImU32 ColorConvertFloat4ToU32(const ImVec4& in); + IMGUI_API void ColorConvertRGBtoHSV(float r, float g, float b, float& out_h, float& out_s, float& out_v); + IMGUI_API void ColorConvertHSVtoRGB(float h, float s, float v, float& out_r, float& out_g, float& out_b); + + // Inputs Utilities: Keyboard/Mouse/Gamepad + // - the ImGuiKey enum contains all possible keyboard, mouse and gamepad inputs (e.g. ImGuiKey_A, ImGuiKey_MouseLeft, ImGuiKey_GamepadDpadUp...). + // - before v1.87, we used ImGuiKey to carry native/user indices as defined by each backends. About use of those legacy ImGuiKey values: + // - without IMGUI_DISABLE_OBSOLETE_KEYIO (legacy support): you can still use your legacy native/user indices (< 512) according to how your backend/engine stored them in io.KeysDown[], but need to cast them to ImGuiKey. + // - with IMGUI_DISABLE_OBSOLETE_KEYIO (this is the way forward): any use of ImGuiKey will assert with key < 512. GetKeyIndex() is pass-through and therefore deprecated (gone if IMGUI_DISABLE_OBSOLETE_KEYIO is defined). + IMGUI_API bool IsKeyDown(ImGuiKey key); // is key being held. + IMGUI_API bool IsKeyPressed(ImGuiKey key, bool repeat = true); // was key pressed (went from !Down to Down)? if repeat=true, uses io.KeyRepeatDelay / KeyRepeatRate + IMGUI_API bool IsKeyReleased(ImGuiKey key); // was key released (went from Down to !Down)? + IMGUI_API int GetKeyPressedAmount(ImGuiKey key, float repeat_delay, float rate); // uses provided repeat rate/delay. return a count, most often 0 or 1 but might be >1 if RepeatRate is small enough that DeltaTime > RepeatRate + IMGUI_API const char* GetKeyName(ImGuiKey key); // [DEBUG] returns English name of the key. Those names a provided for debugging purpose and are not meant to be saved persistently not compared. + IMGUI_API void SetNextFrameWantCaptureKeyboard(bool want_capture_keyboard); // Override io.WantCaptureKeyboard flag next frame (said flag is left for your application to handle, typically when true it instructs your app to ignore inputs). e.g. force capture keyboard when your widget is being hovered. This is equivalent to setting "io.WantCaptureKeyboard = want_capture_keyboard"; after the next NewFrame() call. + + // Inputs Utilities: Mouse specific + // - To refer to a mouse button, you may use named enums in your code e.g. ImGuiMouseButton_Left, ImGuiMouseButton_Right. + // - You can also use regular integer: it is forever guaranteed that 0=Left, 1=Right, 2=Middle. + // - Dragging operations are only reported after mouse has moved a certain distance away from the initial clicking position (see 'lock_threshold' and 'io.MouseDraggingThreshold') + IMGUI_API bool IsMouseDown(ImGuiMouseButton button); // is mouse button held? + IMGUI_API bool IsMouseClicked(ImGuiMouseButton button, bool repeat = false); // did mouse button clicked? (went from !Down to Down). Same as GetMouseClickedCount() == 1. + IMGUI_API bool IsMouseReleased(ImGuiMouseButton button); // did mouse button released? (went from Down to !Down) + IMGUI_API bool IsMouseDoubleClicked(ImGuiMouseButton button); // did mouse button double-clicked? Same as GetMouseClickedCount() == 2. (note that a double-click will also report IsMouseClicked() == true) + IMGUI_API int GetMouseClickedCount(ImGuiMouseButton button); // return the number of successive mouse-clicks at the time where a click happen (otherwise 0). + IMGUI_API bool IsMouseHoveringRect(const ImVec2& r_min, const ImVec2& r_max, bool clip = true);// is mouse hovering given bounding rect (in screen space). clipped by current clipping settings, but disregarding of other consideration of focus/window ordering/popup-block. + IMGUI_API bool IsMousePosValid(const ImVec2* mouse_pos = NULL); // by convention we use (-FLT_MAX,-FLT_MAX) to denote that there is no mouse available + IMGUI_API bool IsAnyMouseDown(); // [WILL OBSOLETE] is any mouse button held? This was designed for backends, but prefer having backend maintain a mask of held mouse buttons, because upcoming input queue system will make this invalid. + IMGUI_API ImVec2 GetMousePos(); // shortcut to ImGui::GetIO().MousePos provided by user, to be consistent with other calls + IMGUI_API ImVec2 GetMousePosOnOpeningCurrentPopup(); // retrieve mouse position at the time of opening popup we have BeginPopup() into (helper to avoid user backing that value themselves) + IMGUI_API bool IsMouseDragging(ImGuiMouseButton button, float lock_threshold = -1.0f); // is mouse dragging? (if lock_threshold < -1.0f, uses io.MouseDraggingThreshold) + IMGUI_API ImVec2 GetMouseDragDelta(ImGuiMouseButton button = 0, float lock_threshold = -1.0f); // return the delta from the initial clicking position while the mouse button is pressed or was just released. This is locked and return 0.0f until the mouse moves past a distance threshold at least once (if lock_threshold < -1.0f, uses io.MouseDraggingThreshold) + IMGUI_API void ResetMouseDragDelta(ImGuiMouseButton button = 0); // + IMGUI_API ImGuiMouseCursor GetMouseCursor(); // get desired mouse cursor shape. Important: reset in ImGui::NewFrame(), this is updated during the frame. valid before Render(). If you use software rendering by setting io.MouseDrawCursor ImGui will render those for you + IMGUI_API void SetMouseCursor(ImGuiMouseCursor cursor_type); // set desired mouse cursor shape + IMGUI_API void SetNextFrameWantCaptureMouse(bool want_capture_mouse); // Override io.WantCaptureMouse flag next frame (said flag is left for your application to handle, typical when true it instucts your app to ignore inputs). This is equivalent to setting "io.WantCaptureMouse = want_capture_mouse;" after the next NewFrame() call. + + // Clipboard Utilities + // - Also see the LogToClipboard() function to capture GUI into clipboard, or easily output text data to the clipboard. + IMGUI_API const char* GetClipboardText(); + IMGUI_API void SetClipboardText(const char* text); + + // Settings/.Ini Utilities + // - The disk functions are automatically called if io.IniFilename != NULL (default is "imgui.ini"). + // - Set io.IniFilename to NULL to load/save manually. Read io.WantSaveIniSettings description about handling .ini saving manually. + // - Important: default value "imgui.ini" is relative to current working dir! Most apps will want to lock this to an absolute path (e.g. same path as executables). + IMGUI_API void LoadIniSettingsFromDisk(const char* ini_filename); // call after CreateContext() and before the first call to NewFrame(). NewFrame() automatically calls LoadIniSettingsFromDisk(io.IniFilename). + IMGUI_API void LoadIniSettingsFromMemory(const char* ini_data, size_t ini_size=0); // call after CreateContext() and before the first call to NewFrame() to provide .ini data from your own data source. + IMGUI_API void SaveIniSettingsToDisk(const char* ini_filename); // this is automatically called (if io.IniFilename is not empty) a few seconds after any modification that should be reflected in the .ini file (and also by DestroyContext). + IMGUI_API const char* SaveIniSettingsToMemory(size_t* out_ini_size = NULL); // return a zero-terminated string with the .ini data which you can save by your own mean. call when io.WantSaveIniSettings is set, then save data by your own mean and clear io.WantSaveIniSettings. + + // Debug Utilities + IMGUI_API void DebugTextEncoding(const char* text); + IMGUI_API bool DebugCheckVersionAndDataLayout(const char* version_str, size_t sz_io, size_t sz_style, size_t sz_vec2, size_t sz_vec4, size_t sz_drawvert, size_t sz_drawidx); // This is called by IMGUI_CHECKVERSION() macro. + + // Memory Allocators + // - Those functions are not reliant on the current context. + // - DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions() + // for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for more details. + IMGUI_API void SetAllocatorFunctions(ImGuiMemAllocFunc alloc_func, ImGuiMemFreeFunc free_func, void* user_data = NULL); + IMGUI_API void GetAllocatorFunctions(ImGuiMemAllocFunc* p_alloc_func, ImGuiMemFreeFunc* p_free_func, void** p_user_data); + IMGUI_API void* MemAlloc(size_t size); + IMGUI_API void MemFree(void* ptr); + + // (Optional) Platform/OS interface for multi-viewport support + // Read comments around the ImGuiPlatformIO structure for more details. + // Note: You may use GetWindowViewport() to get the current viewport of the current window. + IMGUI_API ImGuiPlatformIO& GetPlatformIO(); // platform/renderer functions, for backend to setup + viewports list. + IMGUI_API void UpdatePlatformWindows(); // call in main loop. will call CreateWindow/ResizeWindow/etc. platform functions for each secondary viewport, and DestroyWindow for each inactive viewport. + IMGUI_API void RenderPlatformWindowsDefault(void* platform_render_arg = NULL, void* renderer_render_arg = NULL); // call in main loop. will call RenderWindow/SwapBuffers platform functions for each secondary viewport which doesn't have the ImGuiViewportFlags_Minimized flag set. May be reimplemented by user for custom rendering needs. + IMGUI_API void DestroyPlatformWindows(); // call DestroyWindow platform functions for all viewports. call from backend Shutdown() if you need to close platform windows before imgui shutdown. otherwise will be called by DestroyContext(). + IMGUI_API ImGuiViewport* FindViewportByID(ImGuiID id); // this is a helper for backends. + IMGUI_API ImGuiViewport* FindViewportByPlatformHandle(void* platform_handle); // this is a helper for backends. the type platform_handle is decided by the backend (e.g. HWND, MyWindow*, GLFWwindow* etc.) + +} // namespace ImGui + +//----------------------------------------------------------------------------- +// [SECTION] Flags & Enumerations +//----------------------------------------------------------------------------- + +// Flags for ImGui::Begin() +// (Those are per-window flags. There are shared flags in ImGuiIO: io.ConfigWindowsResizeFromEdges and io.ConfigWindowsMoveFromTitleBarOnly) +enum ImGuiWindowFlags_ +{ + ImGuiWindowFlags_None = 0, + ImGuiWindowFlags_NoTitleBar = 1 << 0, // Disable title-bar + ImGuiWindowFlags_NoResize = 1 << 1, // Disable user resizing with the lower-right grip + ImGuiWindowFlags_NoMove = 1 << 2, // Disable user moving the window + ImGuiWindowFlags_NoScrollbar = 1 << 3, // Disable scrollbars (window can still scroll with mouse or programmatically) + ImGuiWindowFlags_NoScrollWithMouse = 1 << 4, // Disable user vertically scrolling with mouse wheel. On child window, mouse wheel will be forwarded to the parent unless NoScrollbar is also set. + ImGuiWindowFlags_NoCollapse = 1 << 5, // Disable user collapsing window by double-clicking on it. Also referred to as Window Menu Button (e.g. within a docking node). + ImGuiWindowFlags_AlwaysAutoResize = 1 << 6, // Resize every window to its content every frame + ImGuiWindowFlags_NoBackground = 1 << 7, // Disable drawing background color (WindowBg, etc.) and outside border. Similar as using SetNextWindowBgAlpha(0.0f). + ImGuiWindowFlags_NoSavedSettings = 1 << 8, // Never load/save settings in .ini file + ImGuiWindowFlags_NoMouseInputs = 1 << 9, // Disable catching mouse, hovering test with pass through. + ImGuiWindowFlags_MenuBar = 1 << 10, // Has a menu-bar + ImGuiWindowFlags_HorizontalScrollbar = 1 << 11, // Allow horizontal scrollbar to appear (off by default). You may use SetNextWindowContentSize(ImVec2(width,0.0f)); prior to calling Begin() to specify width. Read code in imgui_demo in the "Horizontal Scrolling" section. + ImGuiWindowFlags_NoFocusOnAppearing = 1 << 12, // Disable taking focus when transitioning from hidden to visible state + ImGuiWindowFlags_NoBringToFrontOnFocus = 1 << 13, // Disable bringing window to front when taking focus (e.g. clicking on it or programmatically giving it focus) + ImGuiWindowFlags_AlwaysVerticalScrollbar= 1 << 14, // Always show vertical scrollbar (even if ContentSize.y < Size.y) + ImGuiWindowFlags_AlwaysHorizontalScrollbar=1<< 15, // Always show horizontal scrollbar (even if ContentSize.x < Size.x) + ImGuiWindowFlags_AlwaysUseWindowPadding = 1 << 16, // Ensure child windows without border uses style.WindowPadding (ignored by default for non-bordered child windows, because more convenient) + ImGuiWindowFlags_NoNavInputs = 1 << 18, // No gamepad/keyboard navigation within the window + ImGuiWindowFlags_NoNavFocus = 1 << 19, // No focusing toward this window with gamepad/keyboard navigation (e.g. skipped by CTRL+TAB) + ImGuiWindowFlags_UnsavedDocument = 1 << 20, // Display a dot next to the title. When used in a tab/docking context, tab is selected when clicking the X + closure is not assumed (will wait for user to stop submitting the tab). Otherwise closure is assumed when pressing the X, so if you keep submitting the tab may reappear at end of tab bar. + ImGuiWindowFlags_NoDocking = 1 << 21, // Disable docking of this window + + ImGuiWindowFlags_NoNav = ImGuiWindowFlags_NoNavInputs | ImGuiWindowFlags_NoNavFocus, + ImGuiWindowFlags_NoDecoration = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse, + ImGuiWindowFlags_NoInputs = ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoNavInputs | ImGuiWindowFlags_NoNavFocus, + + // [Internal] + ImGuiWindowFlags_NavFlattened = 1 << 23, // [BETA] On child window: allow gamepad/keyboard navigation to cross over parent border to this child or between sibling child windows. + ImGuiWindowFlags_ChildWindow = 1 << 24, // Don't use! For internal use by BeginChild() + ImGuiWindowFlags_Tooltip = 1 << 25, // Don't use! For internal use by BeginTooltip() + ImGuiWindowFlags_Popup = 1 << 26, // Don't use! For internal use by BeginPopup() + ImGuiWindowFlags_Modal = 1 << 27, // Don't use! For internal use by BeginPopupModal() + ImGuiWindowFlags_ChildMenu = 1 << 28, // Don't use! For internal use by BeginMenu() + ImGuiWindowFlags_DockNodeHost = 1 << 29, // Don't use! For internal use by Begin()/NewFrame() +}; + +// Flags for ImGui::InputText() +// (Those are per-item flags. There are shared flags in ImGuiIO: io.ConfigInputTextCursorBlink and io.ConfigInputTextEnterKeepActive) +enum ImGuiInputTextFlags_ +{ + ImGuiInputTextFlags_None = 0, + ImGuiInputTextFlags_CharsDecimal = 1 << 0, // Allow 0123456789.+-*/ + ImGuiInputTextFlags_CharsHexadecimal = 1 << 1, // Allow 0123456789ABCDEFabcdef + ImGuiInputTextFlags_CharsUppercase = 1 << 2, // Turn a..z into A..Z + ImGuiInputTextFlags_CharsNoBlank = 1 << 3, // Filter out spaces, tabs + ImGuiInputTextFlags_AutoSelectAll = 1 << 4, // Select entire text when first taking mouse focus + ImGuiInputTextFlags_EnterReturnsTrue = 1 << 5, // Return 'true' when Enter is pressed (as opposed to every time the value was modified). Consider looking at the IsItemDeactivatedAfterEdit() function. + ImGuiInputTextFlags_CallbackCompletion = 1 << 6, // Callback on pressing TAB (for completion handling) + ImGuiInputTextFlags_CallbackHistory = 1 << 7, // Callback on pressing Up/Down arrows (for history handling) + ImGuiInputTextFlags_CallbackAlways = 1 << 8, // Callback on each iteration. User code may query cursor position, modify text buffer. + ImGuiInputTextFlags_CallbackCharFilter = 1 << 9, // Callback on character inputs to replace or discard them. Modify 'EventChar' to replace or discard, or return 1 in callback to discard. + ImGuiInputTextFlags_AllowTabInput = 1 << 10, // Pressing TAB input a '\t' character into the text field + ImGuiInputTextFlags_CtrlEnterForNewLine = 1 << 11, // In multi-line mode, unfocus with Enter, add new line with Ctrl+Enter (default is opposite: unfocus with Ctrl+Enter, add line with Enter). + ImGuiInputTextFlags_NoHorizontalScroll = 1 << 12, // Disable following the cursor horizontally + ImGuiInputTextFlags_AlwaysOverwrite = 1 << 13, // Overwrite mode + ImGuiInputTextFlags_ReadOnly = 1 << 14, // Read-only mode + ImGuiInputTextFlags_Password = 1 << 15, // Password mode, display all characters as '*' + ImGuiInputTextFlags_NoUndoRedo = 1 << 16, // Disable undo/redo. Note that input text owns the text data while active, if you want to provide your own undo/redo stack you need e.g. to call ClearActiveID(). + ImGuiInputTextFlags_CharsScientific = 1 << 17, // Allow 0123456789.+-*/eE (Scientific notation input) + ImGuiInputTextFlags_CallbackResize = 1 << 18, // Callback on buffer capacity changes request (beyond 'buf_size' parameter value), allowing the string to grow. Notify when the string wants to be resized (for string types which hold a cache of their Size). You will be provided a new BufSize in the callback and NEED to honor it. (see misc/cpp/imgui_stdlib.h for an example of using this) + ImGuiInputTextFlags_CallbackEdit = 1 << 19, // Callback on any edit (note that InputText() already returns true on edit, the callback is useful mainly to manipulate the underlying buffer while focus is active) + ImGuiInputTextFlags_EscapeClearsAll = 1 << 20, // Escape key clears content if not empty, and deactivate otherwise (contrast to default behavior of Escape to revert) + + // Obsolete names + //ImGuiInputTextFlags_AlwaysInsertMode = ImGuiInputTextFlags_AlwaysOverwrite // [renamed in 1.82] name was not matching behavior +}; + +// Flags for ImGui::TreeNodeEx(), ImGui::CollapsingHeader*() +enum ImGuiTreeNodeFlags_ +{ + ImGuiTreeNodeFlags_None = 0, + ImGuiTreeNodeFlags_Selected = 1 << 0, // Draw as selected + ImGuiTreeNodeFlags_Framed = 1 << 1, // Draw frame with background (e.g. for CollapsingHeader) + ImGuiTreeNodeFlags_AllowOverlap = 1 << 2, // Hit testing to allow subsequent widgets to overlap this one + ImGuiTreeNodeFlags_NoTreePushOnOpen = 1 << 3, // Don't do a TreePush() when open (e.g. for CollapsingHeader) = no extra indent nor pushing on ID stack + ImGuiTreeNodeFlags_NoAutoOpenOnLog = 1 << 4, // Don't automatically and temporarily open node when Logging is active (by default logging will automatically open tree nodes) + ImGuiTreeNodeFlags_DefaultOpen = 1 << 5, // Default node to be open + ImGuiTreeNodeFlags_OpenOnDoubleClick = 1 << 6, // Need double-click to open node + ImGuiTreeNodeFlags_OpenOnArrow = 1 << 7, // Only open when clicking on the arrow part. If ImGuiTreeNodeFlags_OpenOnDoubleClick is also set, single-click arrow or double-click all box to open. + ImGuiTreeNodeFlags_Leaf = 1 << 8, // No collapsing, no arrow (use as a convenience for leaf nodes). + ImGuiTreeNodeFlags_Bullet = 1 << 9, // Display a bullet instead of arrow + ImGuiTreeNodeFlags_FramePadding = 1 << 10, // Use FramePadding (even for an unframed text node) to vertically align text baseline to regular widget height. Equivalent to calling AlignTextToFramePadding(). + ImGuiTreeNodeFlags_SpanAvailWidth = 1 << 11, // Extend hit box to the right-most edge, even if not framed. This is not the default in order to allow adding other items on the same line. In the future we may refactor the hit system to be front-to-back, allowing natural overlaps and then this can become the default. + ImGuiTreeNodeFlags_SpanFullWidth = 1 << 12, // Extend hit box to the left-most and right-most edges (bypass the indented area). + ImGuiTreeNodeFlags_NavLeftJumpsBackHere = 1 << 13, // (WIP) Nav: left direction may move to this TreeNode() from any of its child (items submitted between TreeNode and TreePop) + //ImGuiTreeNodeFlags_NoScrollOnOpen = 1 << 14, // FIXME: TODO: Disable automatic scroll on TreePop() if node got just open and contents is not visible + ImGuiTreeNodeFlags_CollapsingHeader = ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_NoAutoOpenOnLog, + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiTreeNodeFlags_AllowItemOverlap = ImGuiTreeNodeFlags_AllowOverlap, // Renamed in 1.89.7 +#endif +}; + +// Flags for OpenPopup*(), BeginPopupContext*(), IsPopupOpen() functions. +// - To be backward compatible with older API which took an 'int mouse_button = 1' argument, we need to treat +// small flags values as a mouse button index, so we encode the mouse button in the first few bits of the flags. +// It is therefore guaranteed to be legal to pass a mouse button index in ImGuiPopupFlags. +// - For the same reason, we exceptionally default the ImGuiPopupFlags argument of BeginPopupContextXXX functions to 1 instead of 0. +// IMPORTANT: because the default parameter is 1 (==ImGuiPopupFlags_MouseButtonRight), if you rely on the default parameter +// and want to use another flag, you need to pass in the ImGuiPopupFlags_MouseButtonRight flag explicitly. +// - Multiple buttons currently cannot be combined/or-ed in those functions (we could allow it later). +enum ImGuiPopupFlags_ +{ + ImGuiPopupFlags_None = 0, + ImGuiPopupFlags_MouseButtonLeft = 0, // For BeginPopupContext*(): open on Left Mouse release. Guaranteed to always be == 0 (same as ImGuiMouseButton_Left) + ImGuiPopupFlags_MouseButtonRight = 1, // For BeginPopupContext*(): open on Right Mouse release. Guaranteed to always be == 1 (same as ImGuiMouseButton_Right) + ImGuiPopupFlags_MouseButtonMiddle = 2, // For BeginPopupContext*(): open on Middle Mouse release. Guaranteed to always be == 2 (same as ImGuiMouseButton_Middle) + ImGuiPopupFlags_MouseButtonMask_ = 0x1F, + ImGuiPopupFlags_MouseButtonDefault_ = 1, + ImGuiPopupFlags_NoOpenOverExistingPopup = 1 << 5, // For OpenPopup*(), BeginPopupContext*(): don't open if there's already a popup at the same level of the popup stack + ImGuiPopupFlags_NoOpenOverItems = 1 << 6, // For BeginPopupContextWindow(): don't return true when hovering items, only when hovering empty space + ImGuiPopupFlags_AnyPopupId = 1 << 7, // For IsPopupOpen(): ignore the ImGuiID parameter and test for any popup. + ImGuiPopupFlags_AnyPopupLevel = 1 << 8, // For IsPopupOpen(): search/test at any level of the popup stack (default test in the current level) + ImGuiPopupFlags_AnyPopup = ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel, +}; + +// Flags for ImGui::Selectable() +enum ImGuiSelectableFlags_ +{ + ImGuiSelectableFlags_None = 0, + ImGuiSelectableFlags_DontClosePopups = 1 << 0, // Clicking this doesn't close parent popup window + ImGuiSelectableFlags_SpanAllColumns = 1 << 1, // Selectable frame can span all columns (text will still fit in current column) + ImGuiSelectableFlags_AllowDoubleClick = 1 << 2, // Generate press events on double clicks too + ImGuiSelectableFlags_Disabled = 1 << 3, // Cannot be selected, display grayed out text + ImGuiSelectableFlags_AllowOverlap = 1 << 4, // (WIP) Hit testing to allow subsequent widgets to overlap this one + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiSelectableFlags_AllowItemOverlap = ImGuiSelectableFlags_AllowOverlap, // Renamed in 1.89.7 +#endif +}; + +// Flags for ImGui::BeginCombo() +enum ImGuiComboFlags_ +{ + ImGuiComboFlags_None = 0, + ImGuiComboFlags_PopupAlignLeft = 1 << 0, // Align the popup toward the left by default + ImGuiComboFlags_HeightSmall = 1 << 1, // Max ~4 items visible. Tip: If you want your combo popup to be a specific size you can use SetNextWindowSizeConstraints() prior to calling BeginCombo() + ImGuiComboFlags_HeightRegular = 1 << 2, // Max ~8 items visible (default) + ImGuiComboFlags_HeightLarge = 1 << 3, // Max ~20 items visible + ImGuiComboFlags_HeightLargest = 1 << 4, // As many fitting items as possible + ImGuiComboFlags_NoArrowButton = 1 << 5, // Display on the preview box without the square arrow button + ImGuiComboFlags_NoPreview = 1 << 6, // Display only a square arrow button + ImGuiComboFlags_HeightMask_ = ImGuiComboFlags_HeightSmall | ImGuiComboFlags_HeightRegular | ImGuiComboFlags_HeightLarge | ImGuiComboFlags_HeightLargest, +}; + +// Flags for ImGui::BeginTabBar() +enum ImGuiTabBarFlags_ +{ + ImGuiTabBarFlags_None = 0, + ImGuiTabBarFlags_Reorderable = 1 << 0, // Allow manually dragging tabs to re-order them + New tabs are appended at the end of list + ImGuiTabBarFlags_AutoSelectNewTabs = 1 << 1, // Automatically select new tabs when they appear + ImGuiTabBarFlags_TabListPopupButton = 1 << 2, // Disable buttons to open the tab list popup + ImGuiTabBarFlags_NoCloseWithMiddleMouseButton = 1 << 3, // Disable behavior of closing tabs (that are submitted with p_open != NULL) with middle mouse button. You can still repro this behavior on user's side with if (IsItemHovered() && IsMouseClicked(2)) *p_open = false. + ImGuiTabBarFlags_NoTabListScrollingButtons = 1 << 4, // Disable scrolling buttons (apply when fitting policy is ImGuiTabBarFlags_FittingPolicyScroll) + ImGuiTabBarFlags_NoTooltip = 1 << 5, // Disable tooltips when hovering a tab + ImGuiTabBarFlags_FittingPolicyResizeDown = 1 << 6, // Resize tabs when they don't fit + ImGuiTabBarFlags_FittingPolicyScroll = 1 << 7, // Add scroll buttons when tabs don't fit + ImGuiTabBarFlags_FittingPolicyMask_ = ImGuiTabBarFlags_FittingPolicyResizeDown | ImGuiTabBarFlags_FittingPolicyScroll, + ImGuiTabBarFlags_FittingPolicyDefault_ = ImGuiTabBarFlags_FittingPolicyResizeDown, +}; + +// Flags for ImGui::BeginTabItem() +enum ImGuiTabItemFlags_ +{ + ImGuiTabItemFlags_None = 0, + ImGuiTabItemFlags_UnsavedDocument = 1 << 0, // Display a dot next to the title + tab is selected when clicking the X + closure is not assumed (will wait for user to stop submitting the tab). Otherwise closure is assumed when pressing the X, so if you keep submitting the tab may reappear at end of tab bar. + ImGuiTabItemFlags_SetSelected = 1 << 1, // Trigger flag to programmatically make the tab selected when calling BeginTabItem() + ImGuiTabItemFlags_NoCloseWithMiddleMouseButton = 1 << 2, // Disable behavior of closing tabs (that are submitted with p_open != NULL) with middle mouse button. You can still repro this behavior on user's side with if (IsItemHovered() && IsMouseClicked(2)) *p_open = false. + ImGuiTabItemFlags_NoPushId = 1 << 3, // Don't call PushID(tab->ID)/PopID() on BeginTabItem()/EndTabItem() + ImGuiTabItemFlags_NoTooltip = 1 << 4, // Disable tooltip for the given tab + ImGuiTabItemFlags_NoReorder = 1 << 5, // Disable reordering this tab or having another tab cross over this tab + ImGuiTabItemFlags_Leading = 1 << 6, // Enforce the tab position to the left of the tab bar (after the tab list popup button) + ImGuiTabItemFlags_Trailing = 1 << 7, // Enforce the tab position to the right of the tab bar (before the scrolling buttons) +}; + +// Flags for ImGui::BeginTable() +// - Important! Sizing policies have complex and subtle side effects, much more so than you would expect. +// Read comments/demos carefully + experiment with live demos to get acquainted with them. +// - The DEFAULT sizing policies are: +// - Default to ImGuiTableFlags_SizingFixedFit if ScrollX is on, or if host window has ImGuiWindowFlags_AlwaysAutoResize. +// - Default to ImGuiTableFlags_SizingStretchSame if ScrollX is off. +// - When ScrollX is off: +// - Table defaults to ImGuiTableFlags_SizingStretchSame -> all Columns defaults to ImGuiTableColumnFlags_WidthStretch with same weight. +// - Columns sizing policy allowed: Stretch (default), Fixed/Auto. +// - Fixed Columns (if any) will generally obtain their requested width (unless the table cannot fit them all). +// - Stretch Columns will share the remaining width according to their respective weight. +// - Mixed Fixed/Stretch columns is possible but has various side-effects on resizing behaviors. +// The typical use of mixing sizing policies is: any number of LEADING Fixed columns, followed by one or two TRAILING Stretch columns. +// (this is because the visible order of columns have subtle but necessary effects on how they react to manual resizing). +// - When ScrollX is on: +// - Table defaults to ImGuiTableFlags_SizingFixedFit -> all Columns defaults to ImGuiTableColumnFlags_WidthFixed +// - Columns sizing policy allowed: Fixed/Auto mostly. +// - Fixed Columns can be enlarged as needed. Table will show a horizontal scrollbar if needed. +// - When using auto-resizing (non-resizable) fixed columns, querying the content width to use item right-alignment e.g. SetNextItemWidth(-FLT_MIN) doesn't make sense, would create a feedback loop. +// - Using Stretch columns OFTEN DOES NOT MAKE SENSE if ScrollX is on, UNLESS you have specified a value for 'inner_width' in BeginTable(). +// If you specify a value for 'inner_width' then effectively the scrolling space is known and Stretch or mixed Fixed/Stretch columns become meaningful again. +// - Read on documentation at the top of imgui_tables.cpp for details. +enum ImGuiTableFlags_ +{ + // Features + ImGuiTableFlags_None = 0, + ImGuiTableFlags_Resizable = 1 << 0, // Enable resizing columns. + ImGuiTableFlags_Reorderable = 1 << 1, // Enable reordering columns in header row (need calling TableSetupColumn() + TableHeadersRow() to display headers) + ImGuiTableFlags_Hideable = 1 << 2, // Enable hiding/disabling columns in context menu. + ImGuiTableFlags_Sortable = 1 << 3, // Enable sorting. Call TableGetSortSpecs() to obtain sort specs. Also see ImGuiTableFlags_SortMulti and ImGuiTableFlags_SortTristate. + ImGuiTableFlags_NoSavedSettings = 1 << 4, // Disable persisting columns order, width and sort settings in the .ini file. + ImGuiTableFlags_ContextMenuInBody = 1 << 5, // Right-click on columns body/contents will display table context menu. By default it is available in TableHeadersRow(). + // Decorations + ImGuiTableFlags_RowBg = 1 << 6, // Set each RowBg color with ImGuiCol_TableRowBg or ImGuiCol_TableRowBgAlt (equivalent of calling TableSetBgColor with ImGuiTableBgFlags_RowBg0 on each row manually) + ImGuiTableFlags_BordersInnerH = 1 << 7, // Draw horizontal borders between rows. + ImGuiTableFlags_BordersOuterH = 1 << 8, // Draw horizontal borders at the top and bottom. + ImGuiTableFlags_BordersInnerV = 1 << 9, // Draw vertical borders between columns. + ImGuiTableFlags_BordersOuterV = 1 << 10, // Draw vertical borders on the left and right sides. + ImGuiTableFlags_BordersH = ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_BordersOuterH, // Draw horizontal borders. + ImGuiTableFlags_BordersV = ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersOuterV, // Draw vertical borders. + ImGuiTableFlags_BordersInner = ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersInnerH, // Draw inner borders. + ImGuiTableFlags_BordersOuter = ImGuiTableFlags_BordersOuterV | ImGuiTableFlags_BordersOuterH, // Draw outer borders. + ImGuiTableFlags_Borders = ImGuiTableFlags_BordersInner | ImGuiTableFlags_BordersOuter, // Draw all borders. + ImGuiTableFlags_NoBordersInBody = 1 << 11, // [ALPHA] Disable vertical borders in columns Body (borders will always appear in Headers). -> May move to style + ImGuiTableFlags_NoBordersInBodyUntilResize = 1 << 12, // [ALPHA] Disable vertical borders in columns Body until hovered for resize (borders will always appear in Headers). -> May move to style + // Sizing Policy (read above for defaults) + ImGuiTableFlags_SizingFixedFit = 1 << 13, // Columns default to _WidthFixed or _WidthAuto (if resizable or not resizable), matching contents width. + ImGuiTableFlags_SizingFixedSame = 2 << 13, // Columns default to _WidthFixed or _WidthAuto (if resizable or not resizable), matching the maximum contents width of all columns. Implicitly enable ImGuiTableFlags_NoKeepColumnsVisible. + ImGuiTableFlags_SizingStretchProp = 3 << 13, // Columns default to _WidthStretch with default weights proportional to each columns contents widths. + ImGuiTableFlags_SizingStretchSame = 4 << 13, // Columns default to _WidthStretch with default weights all equal, unless overridden by TableSetupColumn(). + // Sizing Extra Options + ImGuiTableFlags_NoHostExtendX = 1 << 16, // Make outer width auto-fit to columns, overriding outer_size.x value. Only available when ScrollX/ScrollY are disabled and Stretch columns are not used. + ImGuiTableFlags_NoHostExtendY = 1 << 17, // Make outer height stop exactly at outer_size.y (prevent auto-extending table past the limit). Only available when ScrollX/ScrollY are disabled. Data below the limit will be clipped and not visible. + ImGuiTableFlags_NoKeepColumnsVisible = 1 << 18, // Disable keeping column always minimally visible when ScrollX is off and table gets too small. Not recommended if columns are resizable. + ImGuiTableFlags_PreciseWidths = 1 << 19, // Disable distributing remainder width to stretched columns (width allocation on a 100-wide table with 3 columns: Without this flag: 33,33,34. With this flag: 33,33,33). With larger number of columns, resizing will appear to be less smooth. + // Clipping + ImGuiTableFlags_NoClip = 1 << 20, // Disable clipping rectangle for every individual columns (reduce draw command count, items will be able to overflow into other columns). Generally incompatible with TableSetupScrollFreeze(). + // Padding + ImGuiTableFlags_PadOuterX = 1 << 21, // Default if BordersOuterV is on. Enable outermost padding. Generally desirable if you have headers. + ImGuiTableFlags_NoPadOuterX = 1 << 22, // Default if BordersOuterV is off. Disable outermost padding. + ImGuiTableFlags_NoPadInnerX = 1 << 23, // Disable inner padding between columns (double inner padding if BordersOuterV is on, single inner padding if BordersOuterV is off). + // Scrolling + ImGuiTableFlags_ScrollX = 1 << 24, // Enable horizontal scrolling. Require 'outer_size' parameter of BeginTable() to specify the container size. Changes default sizing policy. Because this creates a child window, ScrollY is currently generally recommended when using ScrollX. + ImGuiTableFlags_ScrollY = 1 << 25, // Enable vertical scrolling. Require 'outer_size' parameter of BeginTable() to specify the container size. + // Sorting + ImGuiTableFlags_SortMulti = 1 << 26, // Hold shift when clicking headers to sort on multiple column. TableGetSortSpecs() may return specs where (SpecsCount > 1). + ImGuiTableFlags_SortTristate = 1 << 27, // Allow no sorting, disable default sorting. TableGetSortSpecs() may return specs where (SpecsCount == 0). + + // [Internal] Combinations and masks + ImGuiTableFlags_SizingMask_ = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_SizingFixedSame | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_SizingStretchSame, +}; + +// Flags for ImGui::TableSetupColumn() +enum ImGuiTableColumnFlags_ +{ + // Input configuration flags + ImGuiTableColumnFlags_None = 0, + ImGuiTableColumnFlags_Disabled = 1 << 0, // Overriding/master disable flag: hide column, won't show in context menu (unlike calling TableSetColumnEnabled() which manipulates the user accessible state) + ImGuiTableColumnFlags_DefaultHide = 1 << 1, // Default as a hidden/disabled column. + ImGuiTableColumnFlags_DefaultSort = 1 << 2, // Default as a sorting column. + ImGuiTableColumnFlags_WidthStretch = 1 << 3, // Column will stretch. Preferable with horizontal scrolling disabled (default if table sizing policy is _SizingStretchSame or _SizingStretchProp). + ImGuiTableColumnFlags_WidthFixed = 1 << 4, // Column will not stretch. Preferable with horizontal scrolling enabled (default if table sizing policy is _SizingFixedFit and table is resizable). + ImGuiTableColumnFlags_NoResize = 1 << 5, // Disable manual resizing. + ImGuiTableColumnFlags_NoReorder = 1 << 6, // Disable manual reordering this column, this will also prevent other columns from crossing over this column. + ImGuiTableColumnFlags_NoHide = 1 << 7, // Disable ability to hide/disable this column. + ImGuiTableColumnFlags_NoClip = 1 << 8, // Disable clipping for this column (all NoClip columns will render in a same draw command). + ImGuiTableColumnFlags_NoSort = 1 << 9, // Disable ability to sort on this field (even if ImGuiTableFlags_Sortable is set on the table). + ImGuiTableColumnFlags_NoSortAscending = 1 << 10, // Disable ability to sort in the ascending direction. + ImGuiTableColumnFlags_NoSortDescending = 1 << 11, // Disable ability to sort in the descending direction. + ImGuiTableColumnFlags_NoHeaderLabel = 1 << 12, // TableHeadersRow() will not submit label for this column. Convenient for some small columns. Name will still appear in context menu. + ImGuiTableColumnFlags_NoHeaderWidth = 1 << 13, // Disable header text width contribution to automatic column width. + ImGuiTableColumnFlags_PreferSortAscending = 1 << 14, // Make the initial sort direction Ascending when first sorting on this column (default). + ImGuiTableColumnFlags_PreferSortDescending = 1 << 15, // Make the initial sort direction Descending when first sorting on this column. + ImGuiTableColumnFlags_IndentEnable = 1 << 16, // Use current Indent value when entering cell (default for column 0). + ImGuiTableColumnFlags_IndentDisable = 1 << 17, // Ignore current Indent value when entering cell (default for columns > 0). Indentation changes _within_ the cell will still be honored. + + // Output status flags, read-only via TableGetColumnFlags() + ImGuiTableColumnFlags_IsEnabled = 1 << 24, // Status: is enabled == not hidden by user/api (referred to as "Hide" in _DefaultHide and _NoHide) flags. + ImGuiTableColumnFlags_IsVisible = 1 << 25, // Status: is visible == is enabled AND not clipped by scrolling. + ImGuiTableColumnFlags_IsSorted = 1 << 26, // Status: is currently part of the sort specs + ImGuiTableColumnFlags_IsHovered = 1 << 27, // Status: is hovered by mouse + + // [Internal] Combinations and masks + ImGuiTableColumnFlags_WidthMask_ = ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_WidthFixed, + ImGuiTableColumnFlags_IndentMask_ = ImGuiTableColumnFlags_IndentEnable | ImGuiTableColumnFlags_IndentDisable, + ImGuiTableColumnFlags_StatusMask_ = ImGuiTableColumnFlags_IsEnabled | ImGuiTableColumnFlags_IsVisible | ImGuiTableColumnFlags_IsSorted | ImGuiTableColumnFlags_IsHovered, + ImGuiTableColumnFlags_NoDirectResize_ = 1 << 30, // [Internal] Disable user resizing this column directly (it may however we resized indirectly from its left edge) +}; + +// Flags for ImGui::TableNextRow() +enum ImGuiTableRowFlags_ +{ + ImGuiTableRowFlags_None = 0, + ImGuiTableRowFlags_Headers = 1 << 0, // Identify header row (set default background color + width of its contents accounted differently for auto column width) +}; + +// Enum for ImGui::TableSetBgColor() +// Background colors are rendering in 3 layers: +// - Layer 0: draw with RowBg0 color if set, otherwise draw with ColumnBg0 if set. +// - Layer 1: draw with RowBg1 color if set, otherwise draw with ColumnBg1 if set. +// - Layer 2: draw with CellBg color if set. +// The purpose of the two row/columns layers is to let you decide if a background color change should override or blend with the existing color. +// When using ImGuiTableFlags_RowBg on the table, each row has the RowBg0 color automatically set for odd/even rows. +// If you set the color of RowBg0 target, your color will override the existing RowBg0 color. +// If you set the color of RowBg1 or ColumnBg1 target, your color will blend over the RowBg0 color. +enum ImGuiTableBgTarget_ +{ + ImGuiTableBgTarget_None = 0, + ImGuiTableBgTarget_RowBg0 = 1, // Set row background color 0 (generally used for background, automatically set when ImGuiTableFlags_RowBg is used) + ImGuiTableBgTarget_RowBg1 = 2, // Set row background color 1 (generally used for selection marking) + ImGuiTableBgTarget_CellBg = 3, // Set cell background color (top-most color) +}; + +// Flags for ImGui::IsWindowFocused() +enum ImGuiFocusedFlags_ +{ + ImGuiFocusedFlags_None = 0, + ImGuiFocusedFlags_ChildWindows = 1 << 0, // Return true if any children of the window is focused + ImGuiFocusedFlags_RootWindow = 1 << 1, // Test from root window (top most parent of the current hierarchy) + ImGuiFocusedFlags_AnyWindow = 1 << 2, // Return true if any window is focused. Important: If you are trying to tell how to dispatch your low-level inputs, do NOT use this. Use 'io.WantCaptureMouse' instead! Please read the FAQ! + ImGuiFocusedFlags_NoPopupHierarchy = 1 << 3, // Do not consider popup hierarchy (do not treat popup emitter as parent of popup) (when used with _ChildWindows or _RootWindow) + ImGuiFocusedFlags_DockHierarchy = 1 << 4, // Consider docking hierarchy (treat dockspace host as parent of docked window) (when used with _ChildWindows or _RootWindow) + ImGuiFocusedFlags_RootAndChildWindows = ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_ChildWindows, +}; + +// Flags for ImGui::IsItemHovered(), ImGui::IsWindowHovered() +// Note: if you are trying to check whether your mouse should be dispatched to Dear ImGui or to your app, you should use 'io.WantCaptureMouse' instead! Please read the FAQ! +// Note: windows with the ImGuiWindowFlags_NoInputs flag are ignored by IsWindowHovered() calls. +enum ImGuiHoveredFlags_ +{ + ImGuiHoveredFlags_None = 0, // Return true if directly over the item/window, not obstructed by another window, not obstructed by an active popup or modal blocking inputs under them. + ImGuiHoveredFlags_ChildWindows = 1 << 0, // IsWindowHovered() only: Return true if any children of the window is hovered + ImGuiHoveredFlags_RootWindow = 1 << 1, // IsWindowHovered() only: Test from root window (top most parent of the current hierarchy) + ImGuiHoveredFlags_AnyWindow = 1 << 2, // IsWindowHovered() only: Return true if any window is hovered + ImGuiHoveredFlags_NoPopupHierarchy = 1 << 3, // IsWindowHovered() only: Do not consider popup hierarchy (do not treat popup emitter as parent of popup) (when used with _ChildWindows or _RootWindow) + ImGuiHoveredFlags_DockHierarchy = 1 << 4, // IsWindowHovered() only: Consider docking hierarchy (treat dockspace host as parent of docked window) (when used with _ChildWindows or _RootWindow) + ImGuiHoveredFlags_AllowWhenBlockedByPopup = 1 << 5, // Return true even if a popup window is normally blocking access to this item/window + //ImGuiHoveredFlags_AllowWhenBlockedByModal = 1 << 6, // Return true even if a modal popup window is normally blocking access to this item/window. FIXME-TODO: Unavailable yet. + ImGuiHoveredFlags_AllowWhenBlockedByActiveItem = 1 << 7, // Return true even if an active item is blocking access to this item/window. Useful for Drag and Drop patterns. + ImGuiHoveredFlags_AllowWhenOverlappedByItem = 1 << 8, // IsItemHovered() only: Return true even if the item uses AllowOverlap mode and is overlapped by another hoverable item. + ImGuiHoveredFlags_AllowWhenOverlappedByWindow = 1 << 9, // IsItemHovered() only: Return true even if the position is obstructed or overlapped by another window. + ImGuiHoveredFlags_AllowWhenDisabled = 1 << 10, // IsItemHovered() only: Return true even if the item is disabled + ImGuiHoveredFlags_NoNavOverride = 1 << 11, // IsItemHovered() only: Disable using gamepad/keyboard navigation state when active, always query mouse + ImGuiHoveredFlags_AllowWhenOverlapped = ImGuiHoveredFlags_AllowWhenOverlappedByItem | ImGuiHoveredFlags_AllowWhenOverlappedByWindow, + ImGuiHoveredFlags_RectOnly = ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_AllowWhenOverlapped, + ImGuiHoveredFlags_RootAndChildWindows = ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows, + + // Tooltips mode + // - typically used in IsItemHovered() + SetTooltip() sequence. + // - this is a shortcut to pull flags from 'style.HoverFlagsForTooltipMouse' or 'style.HoverFlagsForTooltipNav' where you can reconfigure desired behavior. + // e.g. 'TooltipHoveredFlagsForMouse' defaults to 'ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort'. + // - for frequently actioned or hovered items providing a tooltip, you want may to use ImGuiHoveredFlags_ForTooltip (stationary + delay) so the tooltip doesn't show too often. + // - for items which main purpose is to be hovered, or items with low affordance, or in less consistent apps, prefer no delay or shorter delay. + ImGuiHoveredFlags_ForTooltip = 1 << 11, // Shortcut for standard flags when using IsItemHovered() + SetTooltip() sequence. + + // (Advanced) Mouse Hovering delays. + // - generally you can use ImGuiHoveredFlags_ForTooltip to use application-standardized flags. + // - use those if you need specific overrides. + ImGuiHoveredFlags_Stationary = 1 << 12, // Require mouse to be stationary for style.HoverStationaryDelay (~0.15 sec) _at least one time_. After this, can move on same item/window. Using the stationary test tends to reduces the need for a long delay. + ImGuiHoveredFlags_DelayNone = 1 << 13, // IsItemHovered() only: Return true immediately (default). As this is the default you generally ignore this. + ImGuiHoveredFlags_DelayShort = 1 << 14, // IsItemHovered() only: Return true after style.HoverDelayShort elapsed (~0.15 sec) (shared between items) + requires mouse to be stationary for style.HoverStationaryDelay (once per item). + ImGuiHoveredFlags_DelayNormal = 1 << 15, // IsItemHovered() only: Return true after style.HoverDelayNormal elapsed (~0.40 sec) (shared between items) + requires mouse to be stationary for style.HoverStationaryDelay (once per item). + ImGuiHoveredFlags_NoSharedDelay = 1 << 16, // IsItemHovered() only: Disable shared delay system where moving from one item to the next keeps the previous timer for a short time (standard for tooltips with long delays) +}; + +// Flags for ImGui::DockSpace(), shared/inherited by child nodes. +// (Some flags can be applied to individual nodes directly) +// FIXME-DOCK: Also see ImGuiDockNodeFlagsPrivate_ which may involve using the WIP and internal DockBuilder api. +enum ImGuiDockNodeFlags_ +{ + ImGuiDockNodeFlags_None = 0, + ImGuiDockNodeFlags_KeepAliveOnly = 1 << 0, // Shared // Don't display the dockspace node but keep it alive. Windows docked into this dockspace node won't be undocked. + //ImGuiDockNodeFlags_NoCentralNode = 1 << 1, // Shared // Disable Central Node (the node which can stay empty) + ImGuiDockNodeFlags_NoDockingInCentralNode = 1 << 2, // Shared // Disable docking inside the Central Node, which will be always kept empty. + ImGuiDockNodeFlags_PassthruCentralNode = 1 << 3, // Shared // Enable passthru dockspace: 1) DockSpace() will render a ImGuiCol_WindowBg background covering everything excepted the Central Node when empty. Meaning the host window should probably use SetNextWindowBgAlpha(0.0f) prior to Begin() when using this. 2) When Central Node is empty: let inputs pass-through + won't display a DockingEmptyBg background. See demo for details. + ImGuiDockNodeFlags_NoSplit = 1 << 4, // Shared/Local // Disable splitting the node into smaller nodes. Useful e.g. when embedding dockspaces into a main root one (the root one may have splitting disabled to reduce confusion). Note: when turned off, existing splits will be preserved. + ImGuiDockNodeFlags_NoResize = 1 << 5, // Shared/Local // Disable resizing node using the splitter/separators. Useful with programmatically setup dockspaces. + ImGuiDockNodeFlags_AutoHideTabBar = 1 << 6, // Shared/Local // Tab bar will automatically hide when there is a single window in the dock node. +}; + +// Flags for ImGui::BeginDragDropSource(), ImGui::AcceptDragDropPayload() +enum ImGuiDragDropFlags_ +{ + ImGuiDragDropFlags_None = 0, + // BeginDragDropSource() flags + ImGuiDragDropFlags_SourceNoPreviewTooltip = 1 << 0, // Disable preview tooltip. By default, a successful call to BeginDragDropSource opens a tooltip so you can display a preview or description of the source contents. This flag disables this behavior. + ImGuiDragDropFlags_SourceNoDisableHover = 1 << 1, // By default, when dragging we clear data so that IsItemHovered() will return false, to avoid subsequent user code submitting tooltips. This flag disables this behavior so you can still call IsItemHovered() on the source item. + ImGuiDragDropFlags_SourceNoHoldToOpenOthers = 1 << 2, // Disable the behavior that allows to open tree nodes and collapsing header by holding over them while dragging a source item. + ImGuiDragDropFlags_SourceAllowNullID = 1 << 3, // Allow items such as Text(), Image() that have no unique identifier to be used as drag source, by manufacturing a temporary identifier based on their window-relative position. This is extremely unusual within the dear imgui ecosystem and so we made it explicit. + ImGuiDragDropFlags_SourceExtern = 1 << 4, // External source (from outside of dear imgui), won't attempt to read current item/window info. Will always return true. Only one Extern source can be active simultaneously. + ImGuiDragDropFlags_SourceAutoExpirePayload = 1 << 5, // Automatically expire the payload if the source cease to be submitted (otherwise payloads are persisting while being dragged) + // AcceptDragDropPayload() flags + ImGuiDragDropFlags_AcceptBeforeDelivery = 1 << 10, // AcceptDragDropPayload() will returns true even before the mouse button is released. You can then call IsDelivery() to test if the payload needs to be delivered. + ImGuiDragDropFlags_AcceptNoDrawDefaultRect = 1 << 11, // Do not draw the default highlight rectangle when hovering over target. + ImGuiDragDropFlags_AcceptNoPreviewTooltip = 1 << 12, // Request hiding the BeginDragDropSource tooltip from the BeginDragDropTarget site. + ImGuiDragDropFlags_AcceptPeekOnly = ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect, // For peeking ahead and inspecting the payload before delivery. +}; + +// Standard Drag and Drop payload types. You can define you own payload types using short strings. Types starting with '_' are defined by Dear ImGui. +#define IMGUI_PAYLOAD_TYPE_COLOR_3F "_COL3F" // float[3]: Standard type for colors, without alpha. User code may use this type. +#define IMGUI_PAYLOAD_TYPE_COLOR_4F "_COL4F" // float[4]: Standard type for colors. User code may use this type. + +// A primary data type +enum ImGuiDataType_ +{ + ImGuiDataType_S8, // signed char / char (with sensible compilers) + ImGuiDataType_U8, // unsigned char + ImGuiDataType_S16, // short + ImGuiDataType_U16, // unsigned short + ImGuiDataType_S32, // int + ImGuiDataType_U32, // unsigned int + ImGuiDataType_S64, // long long / __int64 + ImGuiDataType_U64, // unsigned long long / unsigned __int64 + ImGuiDataType_Float, // float + ImGuiDataType_Double, // double + ImGuiDataType_COUNT +}; + +// A cardinal direction +enum ImGuiDir_ +{ + ImGuiDir_None = -1, + ImGuiDir_Left = 0, + ImGuiDir_Right = 1, + ImGuiDir_Up = 2, + ImGuiDir_Down = 3, + ImGuiDir_COUNT +}; + +// A sorting direction +enum ImGuiSortDirection_ +{ + ImGuiSortDirection_None = 0, + ImGuiSortDirection_Ascending = 1, // Ascending = 0->9, A->Z etc. + ImGuiSortDirection_Descending = 2 // Descending = 9->0, Z->A etc. +}; + +// A key identifier (ImGuiKey_XXX or ImGuiMod_XXX value): can represent Keyboard, Mouse and Gamepad values. +// All our named keys are >= 512. Keys value 0 to 511 are left unused as legacy native/opaque key values (< 1.87). +// Since >= 1.89 we increased typing (went from int to enum), some legacy code may need a cast to ImGuiKey. +// Read details about the 1.87 and 1.89 transition : https://github.com/ocornut/imgui/issues/4921 +// Note that "Keys" related to physical keys and are not the same concept as input "Characters", the later are submitted via io.AddInputCharacter(). +enum ImGuiKey : int +{ + // Keyboard + ImGuiKey_None = 0, + ImGuiKey_Tab = 512, // == ImGuiKey_NamedKey_BEGIN + ImGuiKey_LeftArrow, + ImGuiKey_RightArrow, + ImGuiKey_UpArrow, + ImGuiKey_DownArrow, + ImGuiKey_PageUp, + ImGuiKey_PageDown, + ImGuiKey_Home, + ImGuiKey_End, + ImGuiKey_Insert, + ImGuiKey_Delete, + ImGuiKey_Backspace, + ImGuiKey_Space, + ImGuiKey_Enter, + ImGuiKey_Escape, + ImGuiKey_LeftCtrl, ImGuiKey_LeftShift, ImGuiKey_LeftAlt, ImGuiKey_LeftSuper, + ImGuiKey_RightCtrl, ImGuiKey_RightShift, ImGuiKey_RightAlt, ImGuiKey_RightSuper, + ImGuiKey_Menu, + ImGuiKey_0, ImGuiKey_1, ImGuiKey_2, ImGuiKey_3, ImGuiKey_4, ImGuiKey_5, ImGuiKey_6, ImGuiKey_7, ImGuiKey_8, ImGuiKey_9, + ImGuiKey_A, ImGuiKey_B, ImGuiKey_C, ImGuiKey_D, ImGuiKey_E, ImGuiKey_F, ImGuiKey_G, ImGuiKey_H, ImGuiKey_I, ImGuiKey_J, + ImGuiKey_K, ImGuiKey_L, ImGuiKey_M, ImGuiKey_N, ImGuiKey_O, ImGuiKey_P, ImGuiKey_Q, ImGuiKey_R, ImGuiKey_S, ImGuiKey_T, + ImGuiKey_U, ImGuiKey_V, ImGuiKey_W, ImGuiKey_X, ImGuiKey_Y, ImGuiKey_Z, + ImGuiKey_F1, ImGuiKey_F2, ImGuiKey_F3, ImGuiKey_F4, ImGuiKey_F5, ImGuiKey_F6, + ImGuiKey_F7, ImGuiKey_F8, ImGuiKey_F9, ImGuiKey_F10, ImGuiKey_F11, ImGuiKey_F12, + ImGuiKey_Apostrophe, // ' + ImGuiKey_Comma, // , + ImGuiKey_Minus, // - + ImGuiKey_Period, // . + ImGuiKey_Slash, // / + ImGuiKey_Semicolon, // ; + ImGuiKey_Equal, // = + ImGuiKey_LeftBracket, // [ + ImGuiKey_Backslash, // \ (this text inhibit multiline comment caused by backslash) + ImGuiKey_RightBracket, // ] + ImGuiKey_GraveAccent, // ` + ImGuiKey_CapsLock, + ImGuiKey_ScrollLock, + ImGuiKey_NumLock, + ImGuiKey_PrintScreen, + ImGuiKey_Pause, + ImGuiKey_Keypad0, ImGuiKey_Keypad1, ImGuiKey_Keypad2, ImGuiKey_Keypad3, ImGuiKey_Keypad4, + ImGuiKey_Keypad5, ImGuiKey_Keypad6, ImGuiKey_Keypad7, ImGuiKey_Keypad8, ImGuiKey_Keypad9, + ImGuiKey_KeypadDecimal, + ImGuiKey_KeypadDivide, + ImGuiKey_KeypadMultiply, + ImGuiKey_KeypadSubtract, + ImGuiKey_KeypadAdd, + ImGuiKey_KeypadEnter, + ImGuiKey_KeypadEqual, + + // Gamepad (some of those are analog values, 0.0f to 1.0f) // NAVIGATION ACTION + // (download controller mapping PNG/PSD at http://dearimgui.com/controls_sheets) + ImGuiKey_GamepadStart, // Menu (Xbox) + (Switch) Start/Options (PS) + ImGuiKey_GamepadBack, // View (Xbox) - (Switch) Share (PS) + ImGuiKey_GamepadFaceLeft, // X (Xbox) Y (Switch) Square (PS) // Tap: Toggle Menu. Hold: Windowing mode (Focus/Move/Resize windows) + ImGuiKey_GamepadFaceRight, // B (Xbox) A (Switch) Circle (PS) // Cancel / Close / Exit + ImGuiKey_GamepadFaceUp, // Y (Xbox) X (Switch) Triangle (PS) // Text Input / On-screen Keyboard + ImGuiKey_GamepadFaceDown, // A (Xbox) B (Switch) Cross (PS) // Activate / Open / Toggle / Tweak + ImGuiKey_GamepadDpadLeft, // D-pad Left // Move / Tweak / Resize Window (in Windowing mode) + ImGuiKey_GamepadDpadRight, // D-pad Right // Move / Tweak / Resize Window (in Windowing mode) + ImGuiKey_GamepadDpadUp, // D-pad Up // Move / Tweak / Resize Window (in Windowing mode) + ImGuiKey_GamepadDpadDown, // D-pad Down // Move / Tweak / Resize Window (in Windowing mode) + ImGuiKey_GamepadL1, // L Bumper (Xbox) L (Switch) L1 (PS) // Tweak Slower / Focus Previous (in Windowing mode) + ImGuiKey_GamepadR1, // R Bumper (Xbox) R (Switch) R1 (PS) // Tweak Faster / Focus Next (in Windowing mode) + ImGuiKey_GamepadL2, // L Trig. (Xbox) ZL (Switch) L2 (PS) [Analog] + ImGuiKey_GamepadR2, // R Trig. (Xbox) ZR (Switch) R2 (PS) [Analog] + ImGuiKey_GamepadL3, // L Stick (Xbox) L3 (Switch) L3 (PS) + ImGuiKey_GamepadR3, // R Stick (Xbox) R3 (Switch) R3 (PS) + ImGuiKey_GamepadLStickLeft, // [Analog] // Move Window (in Windowing mode) + ImGuiKey_GamepadLStickRight, // [Analog] // Move Window (in Windowing mode) + ImGuiKey_GamepadLStickUp, // [Analog] // Move Window (in Windowing mode) + ImGuiKey_GamepadLStickDown, // [Analog] // Move Window (in Windowing mode) + ImGuiKey_GamepadRStickLeft, // [Analog] + ImGuiKey_GamepadRStickRight, // [Analog] + ImGuiKey_GamepadRStickUp, // [Analog] + ImGuiKey_GamepadRStickDown, // [Analog] + + // Aliases: Mouse Buttons (auto-submitted from AddMouseButtonEvent() calls) + // - This is mirroring the data also written to io.MouseDown[], io.MouseWheel, in a format allowing them to be accessed via standard key API. + ImGuiKey_MouseLeft, ImGuiKey_MouseRight, ImGuiKey_MouseMiddle, ImGuiKey_MouseX1, ImGuiKey_MouseX2, ImGuiKey_MouseWheelX, ImGuiKey_MouseWheelY, + + // [Internal] Reserved for mod storage + ImGuiKey_ReservedForModCtrl, ImGuiKey_ReservedForModShift, ImGuiKey_ReservedForModAlt, ImGuiKey_ReservedForModSuper, + ImGuiKey_COUNT, + + // Keyboard Modifiers (explicitly submitted by backend via AddKeyEvent() calls) + // - This is mirroring the data also written to io.KeyCtrl, io.KeyShift, io.KeyAlt, io.KeySuper, in a format allowing + // them to be accessed via standard key API, allowing calls such as IsKeyPressed(), IsKeyReleased(), querying duration etc. + // - Code polling every key (e.g. an interface to detect a key press for input mapping) might want to ignore those + // and prefer using the real keys (e.g. ImGuiKey_LeftCtrl, ImGuiKey_RightCtrl instead of ImGuiMod_Ctrl). + // - In theory the value of keyboard modifiers should be roughly equivalent to a logical or of the equivalent left/right keys. + // In practice: it's complicated; mods are often provided from different sources. Keyboard layout, IME, sticky keys and + // backends tend to interfere and break that equivalence. The safer decision is to relay that ambiguity down to the end-user... + ImGuiMod_None = 0, + ImGuiMod_Ctrl = 1 << 12, // Ctrl + ImGuiMod_Shift = 1 << 13, // Shift + ImGuiMod_Alt = 1 << 14, // Option/Menu + ImGuiMod_Super = 1 << 15, // Cmd/Super/Windows + ImGuiMod_Shortcut = 1 << 11, // Alias for Ctrl (non-macOS) _or_ Super (macOS). + ImGuiMod_Mask_ = 0xF800, // 5-bits + + // [Internal] Prior to 1.87 we required user to fill io.KeysDown[512] using their own native index + the io.KeyMap[] array. + // We are ditching this method but keeping a legacy path for user code doing e.g. IsKeyPressed(MY_NATIVE_KEY_CODE) + // If you need to iterate all keys (for e.g. an input mapper) you may use ImGuiKey_NamedKey_BEGIN..ImGuiKey_NamedKey_END. + ImGuiKey_NamedKey_BEGIN = 512, + ImGuiKey_NamedKey_END = ImGuiKey_COUNT, + ImGuiKey_NamedKey_COUNT = ImGuiKey_NamedKey_END - ImGuiKey_NamedKey_BEGIN, +#ifdef IMGUI_DISABLE_OBSOLETE_KEYIO + ImGuiKey_KeysData_SIZE = ImGuiKey_NamedKey_COUNT, // Size of KeysData[]: only hold named keys + ImGuiKey_KeysData_OFFSET = ImGuiKey_NamedKey_BEGIN, // Accesses to io.KeysData[] must use (key - ImGuiKey_KeysData_OFFSET) index. +#else + ImGuiKey_KeysData_SIZE = ImGuiKey_COUNT, // Size of KeysData[]: hold legacy 0..512 keycodes + named keys + ImGuiKey_KeysData_OFFSET = 0, // Accesses to io.KeysData[] must use (key - ImGuiKey_KeysData_OFFSET) index. +#endif + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiKey_ModCtrl = ImGuiMod_Ctrl, ImGuiKey_ModShift = ImGuiMod_Shift, ImGuiKey_ModAlt = ImGuiMod_Alt, ImGuiKey_ModSuper = ImGuiMod_Super, // Renamed in 1.89 + ImGuiKey_KeyPadEnter = ImGuiKey_KeypadEnter, // Renamed in 1.87 +#endif +}; + +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO +// OBSOLETED in 1.88 (from July 2022): ImGuiNavInput and io.NavInputs[]. +// Official backends between 1.60 and 1.86: will keep working and feed gamepad inputs as long as IMGUI_DISABLE_OBSOLETE_KEYIO is not set. +// Custom backends: feed gamepad inputs via io.AddKeyEvent() and ImGuiKey_GamepadXXX enums. +enum ImGuiNavInput +{ + ImGuiNavInput_Activate, ImGuiNavInput_Cancel, ImGuiNavInput_Input, ImGuiNavInput_Menu, ImGuiNavInput_DpadLeft, ImGuiNavInput_DpadRight, ImGuiNavInput_DpadUp, ImGuiNavInput_DpadDown, + ImGuiNavInput_LStickLeft, ImGuiNavInput_LStickRight, ImGuiNavInput_LStickUp, ImGuiNavInput_LStickDown, ImGuiNavInput_FocusPrev, ImGuiNavInput_FocusNext, ImGuiNavInput_TweakSlow, ImGuiNavInput_TweakFast, + ImGuiNavInput_COUNT, +}; +#endif + +// Configuration flags stored in io.ConfigFlags. Set by user/application. +enum ImGuiConfigFlags_ +{ + ImGuiConfigFlags_None = 0, + ImGuiConfigFlags_NavEnableKeyboard = 1 << 0, // Master keyboard navigation enable flag. Enable full Tabbing + directional arrows + space/enter to activate. + ImGuiConfigFlags_NavEnableGamepad = 1 << 1, // Master gamepad navigation enable flag. Backend also needs to set ImGuiBackendFlags_HasGamepad. + ImGuiConfigFlags_NavEnableSetMousePos = 1 << 2, // Instruct navigation to move the mouse cursor. May be useful on TV/console systems where moving a virtual mouse is awkward. Will update io.MousePos and set io.WantSetMousePos=true. If enabled you MUST honor io.WantSetMousePos requests in your backend, otherwise ImGui will react as if the mouse is jumping around back and forth. + ImGuiConfigFlags_NavNoCaptureKeyboard = 1 << 3, // Instruct navigation to not set the io.WantCaptureKeyboard flag when io.NavActive is set. + ImGuiConfigFlags_NoMouse = 1 << 4, // Instruct imgui to clear mouse position/buttons in NewFrame(). This allows ignoring the mouse information set by the backend. + ImGuiConfigFlags_NoMouseCursorChange = 1 << 5, // Instruct backend to not alter mouse cursor shape and visibility. Use if the backend cursor changes are interfering with yours and you don't want to use SetMouseCursor() to change mouse cursor. You may want to honor requests from imgui by reading GetMouseCursor() yourself instead. + + // [BETA] Docking + ImGuiConfigFlags_DockingEnable = 1 << 6, // Docking enable flags. + + // [BETA] Viewports + // When using viewports it is recommended that your default value for ImGuiCol_WindowBg is opaque (Alpha=1.0) so transition to a viewport won't be noticeable. + ImGuiConfigFlags_ViewportsEnable = 1 << 10, // Viewport enable flags (require both ImGuiBackendFlags_PlatformHasViewports + ImGuiBackendFlags_RendererHasViewports set by the respective backends) + ImGuiConfigFlags_DpiEnableScaleViewports= 1 << 14, // [BETA: Don't use] FIXME-DPI: Reposition and resize imgui windows when the DpiScale of a viewport changed (mostly useful for the main viewport hosting other window). Note that resizing the main window itself is up to your application. + ImGuiConfigFlags_DpiEnableScaleFonts = 1 << 15, // [BETA: Don't use] FIXME-DPI: Request bitmap-scaled fonts to match DpiScale. This is a very low-quality workaround. The correct way to handle DPI is _currently_ to replace the atlas and/or fonts in the Platform_OnChangedViewport callback, but this is all early work in progress. + + // User storage (to allow your backend/engine to communicate to code that may be shared between multiple projects. Those flags are NOT used by core Dear ImGui) + ImGuiConfigFlags_IsSRGB = 1 << 20, // Application is SRGB-aware. + ImGuiConfigFlags_IsTouchScreen = 1 << 21, // Application is using a touch screen instead of a mouse. +}; + +// Backend capabilities flags stored in io.BackendFlags. Set by imgui_impl_xxx or custom backend. +enum ImGuiBackendFlags_ +{ + ImGuiBackendFlags_None = 0, + ImGuiBackendFlags_HasGamepad = 1 << 0, // Backend Platform supports gamepad and currently has one connected. + ImGuiBackendFlags_HasMouseCursors = 1 << 1, // Backend Platform supports honoring GetMouseCursor() value to change the OS cursor shape. + ImGuiBackendFlags_HasSetMousePos = 1 << 2, // Backend Platform supports io.WantSetMousePos requests to reposition the OS mouse position (only used if ImGuiConfigFlags_NavEnableSetMousePos is set). + ImGuiBackendFlags_RendererHasVtxOffset = 1 << 3, // Backend Renderer supports ImDrawCmd::VtxOffset. This enables output of large meshes (64K+ vertices) while still using 16-bit indices. + + // [BETA] Viewports + ImGuiBackendFlags_PlatformHasViewports = 1 << 10, // Backend Platform supports multiple viewports. + ImGuiBackendFlags_HasMouseHoveredViewport=1 << 11, // Backend Platform supports calling io.AddMouseViewportEvent() with the viewport under the mouse. IF POSSIBLE, ignore viewports with the ImGuiViewportFlags_NoInputs flag (Win32 backend, GLFW 3.30+ backend can do this, SDL backend cannot). If this cannot be done, Dear ImGui needs to use a flawed heuristic to find the viewport under. + ImGuiBackendFlags_RendererHasViewports = 1 << 12, // Backend Renderer supports multiple viewports. +}; + +// Enumeration for PushStyleColor() / PopStyleColor() +enum ImGuiCol_ +{ + ImGuiCol_Text, + ImGuiCol_TextDisabled, + ImGuiCol_WindowBg, // Background of normal windows + ImGuiCol_ChildBg, // Background of child windows + ImGuiCol_PopupBg, // Background of popups, menus, tooltips windows + ImGuiCol_Border, + ImGuiCol_BorderShadow, + ImGuiCol_FrameBg, // Background of checkbox, radio button, plot, slider, text input + ImGuiCol_FrameBgHovered, + ImGuiCol_FrameBgActive, + ImGuiCol_TitleBg, + ImGuiCol_TitleBgActive, + ImGuiCol_TitleBgCollapsed, + ImGuiCol_MenuBarBg, + ImGuiCol_ScrollbarBg, + ImGuiCol_ScrollbarGrab, + ImGuiCol_ScrollbarGrabHovered, + ImGuiCol_ScrollbarGrabActive, + ImGuiCol_CheckMark, + ImGuiCol_SliderGrab, + ImGuiCol_SliderGrabActive, + ImGuiCol_Button, + ImGuiCol_ButtonHovered, + ImGuiCol_ButtonActive, + ImGuiCol_Header, // Header* colors are used for CollapsingHeader, TreeNode, Selectable, MenuItem + ImGuiCol_HeaderHovered, + ImGuiCol_HeaderActive, + ImGuiCol_Separator, + ImGuiCol_SeparatorHovered, + ImGuiCol_SeparatorActive, + ImGuiCol_ResizeGrip, // Resize grip in lower-right and lower-left corners of windows. + ImGuiCol_ResizeGripHovered, + ImGuiCol_ResizeGripActive, + ImGuiCol_Tab, // TabItem in a TabBar + ImGuiCol_TabHovered, + ImGuiCol_TabActive, + ImGuiCol_TabUnfocused, + ImGuiCol_TabUnfocusedActive, + ImGuiCol_DockingPreview, // Preview overlay color when about to docking something + ImGuiCol_DockingEmptyBg, // Background color for empty node (e.g. CentralNode with no window docked into it) + ImGuiCol_PlotLines, + ImGuiCol_PlotLinesHovered, + ImGuiCol_PlotHistogram, + ImGuiCol_PlotHistogramHovered, + ImGuiCol_TableHeaderBg, // Table header background + ImGuiCol_TableBorderStrong, // Table outer and header borders (prefer using Alpha=1.0 here) + ImGuiCol_TableBorderLight, // Table inner borders (prefer using Alpha=1.0 here) + ImGuiCol_TableRowBg, // Table row background (even rows) + ImGuiCol_TableRowBgAlt, // Table row background (odd rows) + ImGuiCol_TextSelectedBg, + ImGuiCol_DragDropTarget, // Rectangle highlighting a drop target + ImGuiCol_NavHighlight, // Gamepad/keyboard: current highlighted item + ImGuiCol_NavWindowingHighlight, // Highlight window when using CTRL+TAB + ImGuiCol_NavWindowingDimBg, // Darken/colorize entire screen behind the CTRL+TAB window list, when active + ImGuiCol_ModalWindowDimBg, // Darken/colorize entire screen behind a modal window, when one is active + ImGuiCol_COUNT +}; + +// Enumeration for PushStyleVar() / PopStyleVar() to temporarily modify the ImGuiStyle structure. +// - The enum only refers to fields of ImGuiStyle which makes sense to be pushed/popped inside UI code. +// During initialization or between frames, feel free to just poke into ImGuiStyle directly. +// - Tip: Use your programming IDE navigation facilities on the names in the _second column_ below to find the actual members and their description. +// In Visual Studio IDE: CTRL+comma ("Edit.GoToAll") can follow symbols in comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// With Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols in comments. +// - When changing this enum, you need to update the associated internal table GStyleVarInfo[] accordingly. This is where we link enum values to members offset/type. +enum ImGuiStyleVar_ +{ + // Enum name --------------------- // Member in ImGuiStyle structure (see ImGuiStyle for descriptions) + ImGuiStyleVar_Alpha, // float Alpha + ImGuiStyleVar_DisabledAlpha, // float DisabledAlpha + ImGuiStyleVar_WindowPadding, // ImVec2 WindowPadding + ImGuiStyleVar_WindowRounding, // float WindowRounding + ImGuiStyleVar_WindowBorderSize, // float WindowBorderSize + ImGuiStyleVar_WindowMinSize, // ImVec2 WindowMinSize + ImGuiStyleVar_WindowTitleAlign, // ImVec2 WindowTitleAlign + ImGuiStyleVar_ChildRounding, // float ChildRounding + ImGuiStyleVar_ChildBorderSize, // float ChildBorderSize + ImGuiStyleVar_PopupRounding, // float PopupRounding + ImGuiStyleVar_PopupBorderSize, // float PopupBorderSize + ImGuiStyleVar_FramePadding, // ImVec2 FramePadding + ImGuiStyleVar_FrameRounding, // float FrameRounding + ImGuiStyleVar_FrameBorderSize, // float FrameBorderSize + ImGuiStyleVar_ItemSpacing, // ImVec2 ItemSpacing + ImGuiStyleVar_ItemInnerSpacing, // ImVec2 ItemInnerSpacing + ImGuiStyleVar_IndentSpacing, // float IndentSpacing + ImGuiStyleVar_CellPadding, // ImVec2 CellPadding + ImGuiStyleVar_ScrollbarSize, // float ScrollbarSize + ImGuiStyleVar_ScrollbarRounding, // float ScrollbarRounding + ImGuiStyleVar_GrabMinSize, // float GrabMinSize + ImGuiStyleVar_GrabRounding, // float GrabRounding + ImGuiStyleVar_TabRounding, // float TabRounding + ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign + ImGuiStyleVar_SelectableTextAlign, // ImVec2 SelectableTextAlign + ImGuiStyleVar_SeparatorTextBorderSize,// float SeparatorTextBorderSize + ImGuiStyleVar_SeparatorTextAlign, // ImVec2 SeparatorTextAlign + ImGuiStyleVar_SeparatorTextPadding,// ImVec2 SeparatorTextPadding + ImGuiStyleVar_COUNT +}; + +// Flags for InvisibleButton() [extended in imgui_internal.h] +enum ImGuiButtonFlags_ +{ + ImGuiButtonFlags_None = 0, + ImGuiButtonFlags_MouseButtonLeft = 1 << 0, // React on left mouse button (default) + ImGuiButtonFlags_MouseButtonRight = 1 << 1, // React on right mouse button + ImGuiButtonFlags_MouseButtonMiddle = 1 << 2, // React on center mouse button + + // [Internal] + ImGuiButtonFlags_MouseButtonMask_ = ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight | ImGuiButtonFlags_MouseButtonMiddle, + ImGuiButtonFlags_MouseButtonDefault_ = ImGuiButtonFlags_MouseButtonLeft, +}; + +// Flags for ColorEdit3() / ColorEdit4() / ColorPicker3() / ColorPicker4() / ColorButton() +enum ImGuiColorEditFlags_ +{ + ImGuiColorEditFlags_None = 0, + ImGuiColorEditFlags_NoAlpha = 1 << 1, // // ColorEdit, ColorPicker, ColorButton: ignore Alpha component (will only read 3 components from the input pointer). + ImGuiColorEditFlags_NoPicker = 1 << 2, // // ColorEdit: disable picker when clicking on color square. + ImGuiColorEditFlags_NoOptions = 1 << 3, // // ColorEdit: disable toggling options menu when right-clicking on inputs/small preview. + ImGuiColorEditFlags_NoSmallPreview = 1 << 4, // // ColorEdit, ColorPicker: disable color square preview next to the inputs. (e.g. to show only the inputs) + ImGuiColorEditFlags_NoInputs = 1 << 5, // // ColorEdit, ColorPicker: disable inputs sliders/text widgets (e.g. to show only the small preview color square). + ImGuiColorEditFlags_NoTooltip = 1 << 6, // // ColorEdit, ColorPicker, ColorButton: disable tooltip when hovering the preview. + ImGuiColorEditFlags_NoLabel = 1 << 7, // // ColorEdit, ColorPicker: disable display of inline text label (the label is still forwarded to the tooltip and picker). + ImGuiColorEditFlags_NoSidePreview = 1 << 8, // // ColorPicker: disable bigger color preview on right side of the picker, use small color square preview instead. + ImGuiColorEditFlags_NoDragDrop = 1 << 9, // // ColorEdit: disable drag and drop target. ColorButton: disable drag and drop source. + ImGuiColorEditFlags_NoBorder = 1 << 10, // // ColorButton: disable border (which is enforced by default) + + // User Options (right-click on widget to change some of them). + ImGuiColorEditFlags_AlphaBar = 1 << 16, // // ColorEdit, ColorPicker: show vertical alpha bar/gradient in picker. + ImGuiColorEditFlags_AlphaPreview = 1 << 17, // // ColorEdit, ColorPicker, ColorButton: display preview as a transparent color over a checkerboard, instead of opaque. + ImGuiColorEditFlags_AlphaPreviewHalf= 1 << 18, // // ColorEdit, ColorPicker, ColorButton: display half opaque / half checkerboard, instead of opaque. + ImGuiColorEditFlags_HDR = 1 << 19, // // (WIP) ColorEdit: Currently only disable 0.0f..1.0f limits in RGBA edition (note: you probably want to use ImGuiColorEditFlags_Float flag as well). + ImGuiColorEditFlags_DisplayRGB = 1 << 20, // [Display] // ColorEdit: override _display_ type among RGB/HSV/Hex. ColorPicker: select any combination using one or more of RGB/HSV/Hex. + ImGuiColorEditFlags_DisplayHSV = 1 << 21, // [Display] // " + ImGuiColorEditFlags_DisplayHex = 1 << 22, // [Display] // " + ImGuiColorEditFlags_Uint8 = 1 << 23, // [DataType] // ColorEdit, ColorPicker, ColorButton: _display_ values formatted as 0..255. + ImGuiColorEditFlags_Float = 1 << 24, // [DataType] // ColorEdit, ColorPicker, ColorButton: _display_ values formatted as 0.0f..1.0f floats instead of 0..255 integers. No round-trip of value via integers. + ImGuiColorEditFlags_PickerHueBar = 1 << 25, // [Picker] // ColorPicker: bar for Hue, rectangle for Sat/Value. + ImGuiColorEditFlags_PickerHueWheel = 1 << 26, // [Picker] // ColorPicker: wheel for Hue, triangle for Sat/Value. + ImGuiColorEditFlags_InputRGB = 1 << 27, // [Input] // ColorEdit, ColorPicker: input and output data in RGB format. + ImGuiColorEditFlags_InputHSV = 1 << 28, // [Input] // ColorEdit, ColorPicker: input and output data in HSV format. + + // Defaults Options. You can set application defaults using SetColorEditOptions(). The intent is that you probably don't want to + // override them in most of your calls. Let the user choose via the option menu and/or call SetColorEditOptions() once during startup. + ImGuiColorEditFlags_DefaultOptions_ = ImGuiColorEditFlags_Uint8 | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_InputRGB | ImGuiColorEditFlags_PickerHueBar, + + // [Internal] Masks + ImGuiColorEditFlags_DisplayMask_ = ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV | ImGuiColorEditFlags_DisplayHex, + ImGuiColorEditFlags_DataTypeMask_ = ImGuiColorEditFlags_Uint8 | ImGuiColorEditFlags_Float, + ImGuiColorEditFlags_PickerMask_ = ImGuiColorEditFlags_PickerHueWheel | ImGuiColorEditFlags_PickerHueBar, + ImGuiColorEditFlags_InputMask_ = ImGuiColorEditFlags_InputRGB | ImGuiColorEditFlags_InputHSV, + + // Obsolete names + //ImGuiColorEditFlags_RGB = ImGuiColorEditFlags_DisplayRGB, ImGuiColorEditFlags_HSV = ImGuiColorEditFlags_DisplayHSV, ImGuiColorEditFlags_HEX = ImGuiColorEditFlags_DisplayHex // [renamed in 1.69] +}; + +// Flags for DragFloat(), DragInt(), SliderFloat(), SliderInt() etc. +// We use the same sets of flags for DragXXX() and SliderXXX() functions as the features are the same and it makes it easier to swap them. +// (Those are per-item flags. There are shared flags in ImGuiIO: io.ConfigDragClickToInputText) +enum ImGuiSliderFlags_ +{ + ImGuiSliderFlags_None = 0, + ImGuiSliderFlags_AlwaysClamp = 1 << 4, // Clamp value to min/max bounds when input manually with CTRL+Click. By default CTRL+Click allows going out of bounds. + ImGuiSliderFlags_Logarithmic = 1 << 5, // Make the widget logarithmic (linear otherwise). Consider using ImGuiSliderFlags_NoRoundToFormat with this if using a format-string with small amount of digits. + ImGuiSliderFlags_NoRoundToFormat = 1 << 6, // Disable rounding underlying value to match precision of the display format string (e.g. %.3f values are rounded to those 3 digits) + ImGuiSliderFlags_NoInput = 1 << 7, // Disable CTRL+Click or Enter key allowing to input text directly into the widget + ImGuiSliderFlags_InvalidMask_ = 0x7000000F, // [Internal] We treat using those bits as being potentially a 'float power' argument from the previous API that has got miscast to this enum, and will trigger an assert if needed. + + // Obsolete names + //ImGuiSliderFlags_ClampOnInput = ImGuiSliderFlags_AlwaysClamp, // [renamed in 1.79] +}; + +// Identify a mouse button. +// Those values are guaranteed to be stable and we frequently use 0/1 directly. Named enums provided for convenience. +enum ImGuiMouseButton_ +{ + ImGuiMouseButton_Left = 0, + ImGuiMouseButton_Right = 1, + ImGuiMouseButton_Middle = 2, + ImGuiMouseButton_COUNT = 5 +}; + +// Enumeration for GetMouseCursor() +// User code may request backend to display given cursor by calling SetMouseCursor(), which is why we have some cursors that are marked unused here +enum ImGuiMouseCursor_ +{ + ImGuiMouseCursor_None = -1, + ImGuiMouseCursor_Arrow = 0, + ImGuiMouseCursor_TextInput, // When hovering over InputText, etc. + ImGuiMouseCursor_ResizeAll, // (Unused by Dear ImGui functions) + ImGuiMouseCursor_ResizeNS, // When hovering over a horizontal border + ImGuiMouseCursor_ResizeEW, // When hovering over a vertical border or a column + ImGuiMouseCursor_ResizeNESW, // When hovering over the bottom-left corner of a window + ImGuiMouseCursor_ResizeNWSE, // When hovering over the bottom-right corner of a window + ImGuiMouseCursor_Hand, // (Unused by Dear ImGui functions. Use for e.g. hyperlinks) + ImGuiMouseCursor_NotAllowed, // When hovering something with disallowed interaction. Usually a crossed circle. + ImGuiMouseCursor_COUNT +}; + +// Enumeration for AddMouseSourceEvent() actual source of Mouse Input data. +// Historically we use "Mouse" terminology everywhere to indicate pointer data, e.g. MousePos, IsMousePressed(), io.AddMousePosEvent() +// But that "Mouse" data can come from different source which occasionally may be useful for application to know about. +// You can submit a change of pointer type using io.AddMouseSourceEvent(). +enum ImGuiMouseSource : int +{ + ImGuiMouseSource_Mouse = 0, // Input is coming from an actual mouse. + ImGuiMouseSource_TouchScreen, // Input is coming from a touch screen (no hovering prior to initial press, less precise initial press aiming, dual-axis wheeling possible). + ImGuiMouseSource_Pen, // Input is coming from a pressure/magnetic pen (often used in conjunction with high-sampling rates). + ImGuiMouseSource_COUNT +}; + +// Enumeration for ImGui::SetWindow***(), SetNextWindow***(), SetNextItem***() functions +// Represent a condition. +// Important: Treat as a regular enum! Do NOT combine multiple values using binary operators! All the functions above treat 0 as a shortcut to ImGuiCond_Always. +enum ImGuiCond_ +{ + ImGuiCond_None = 0, // No condition (always set the variable), same as _Always + ImGuiCond_Always = 1 << 0, // No condition (always set the variable), same as _None + ImGuiCond_Once = 1 << 1, // Set the variable once per runtime session (only the first call will succeed) + ImGuiCond_FirstUseEver = 1 << 2, // Set the variable if the object/window has no persistently saved data (no entry in .ini file) + ImGuiCond_Appearing = 1 << 3, // Set the variable if the object/window is appearing after being hidden/inactive (or the first time) +}; + +//----------------------------------------------------------------------------- +// [SECTION] Helpers: Memory allocations macros, ImVector<> +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// IM_MALLOC(), IM_FREE(), IM_NEW(), IM_PLACEMENT_NEW(), IM_DELETE() +// We call C++ constructor on own allocated memory via the placement "new(ptr) Type()" syntax. +// Defining a custom placement new() with a custom parameter allows us to bypass including which on some platforms complains when user has disabled exceptions. +//----------------------------------------------------------------------------- + +struct ImNewWrapper {}; +inline void* operator new(size_t, ImNewWrapper, void* ptr) { return ptr; } +inline void operator delete(void*, ImNewWrapper, void*) {} // This is only required so we can use the symmetrical new() +#define IM_ALLOC(_SIZE) ImGui::MemAlloc(_SIZE) +#define IM_FREE(_PTR) ImGui::MemFree(_PTR) +#define IM_PLACEMENT_NEW(_PTR) new(ImNewWrapper(), _PTR) +#define IM_NEW(_TYPE) new(ImNewWrapper(), ImGui::MemAlloc(sizeof(_TYPE))) _TYPE +template void IM_DELETE(T* p) { if (p) { p->~T(); ImGui::MemFree(p); } } + +//----------------------------------------------------------------------------- +// ImVector<> +// Lightweight std::vector<>-like class to avoid dragging dependencies (also, some implementations of STL with debug enabled are absurdly slow, we bypass it so our code runs fast in debug). +//----------------------------------------------------------------------------- +// - You generally do NOT need to care or use this ever. But we need to make it available in imgui.h because some of our public structures are relying on it. +// - We use std-like naming convention here, which is a little unusual for this codebase. +// - Important: clear() frees memory, resize(0) keep the allocated buffer. We use resize(0) a lot to intentionally recycle allocated buffers across frames and amortize our costs. +// - Important: our implementation does NOT call C++ constructors/destructors, we treat everything as raw data! This is intentional but be extra mindful of that, +// Do NOT use this class as a std::vector replacement in your own code! Many of the structures used by dear imgui can be safely initialized by a zero-memset. +//----------------------------------------------------------------------------- + +IM_MSVC_RUNTIME_CHECKS_OFF +template +struct ImVector +{ + int Size; + int Capacity; + T* Data; + + // Provide standard typedefs but we don't use them ourselves. + typedef T value_type; + typedef value_type* iterator; + typedef const value_type* const_iterator; + + // Constructors, destructor + inline ImVector() { Size = Capacity = 0; Data = NULL; } + inline ImVector(const ImVector& src) { Size = Capacity = 0; Data = NULL; operator=(src); } + inline ImVector& operator=(const ImVector& src) { clear(); resize(src.Size); if (src.Data) memcpy(Data, src.Data, (size_t)Size * sizeof(T)); return *this; } + inline ~ImVector() { if (Data) IM_FREE(Data); } // Important: does not destruct anything + + inline void clear() { if (Data) { Size = Capacity = 0; IM_FREE(Data); Data = NULL; } } // Important: does not destruct anything + inline void clear_delete() { for (int n = 0; n < Size; n++) IM_DELETE(Data[n]); clear(); } // Important: never called automatically! always explicit. + inline void clear_destruct() { for (int n = 0; n < Size; n++) Data[n].~T(); clear(); } // Important: never called automatically! always explicit. + + inline bool empty() const { return Size == 0; } + inline int size() const { return Size; } + inline int size_in_bytes() const { return Size * (int)sizeof(T); } + inline int max_size() const { return 0x7FFFFFFF / (int)sizeof(T); } + inline int capacity() const { return Capacity; } + inline T& operator[](int i) { IM_ASSERT(i >= 0 && i < Size); return Data[i]; } + inline const T& operator[](int i) const { IM_ASSERT(i >= 0 && i < Size); return Data[i]; } + + inline T* begin() { return Data; } + inline const T* begin() const { return Data; } + inline T* end() { return Data + Size; } + inline const T* end() const { return Data + Size; } + inline T& front() { IM_ASSERT(Size > 0); return Data[0]; } + inline const T& front() const { IM_ASSERT(Size > 0); return Data[0]; } + inline T& back() { IM_ASSERT(Size > 0); return Data[Size - 1]; } + inline const T& back() const { IM_ASSERT(Size > 0); return Data[Size - 1]; } + inline void swap(ImVector& rhs) { int rhs_size = rhs.Size; rhs.Size = Size; Size = rhs_size; int rhs_cap = rhs.Capacity; rhs.Capacity = Capacity; Capacity = rhs_cap; T* rhs_data = rhs.Data; rhs.Data = Data; Data = rhs_data; } + + inline int _grow_capacity(int sz) const { int new_capacity = Capacity ? (Capacity + Capacity / 2) : 8; return new_capacity > sz ? new_capacity : sz; } + inline void resize(int new_size) { if (new_size > Capacity) reserve(_grow_capacity(new_size)); Size = new_size; } + inline void resize(int new_size, const T& v) { if (new_size > Capacity) reserve(_grow_capacity(new_size)); if (new_size > Size) for (int n = Size; n < new_size; n++) memcpy(&Data[n], &v, sizeof(v)); Size = new_size; } + inline void shrink(int new_size) { IM_ASSERT(new_size <= Size); Size = new_size; } // Resize a vector to a smaller size, guaranteed not to cause a reallocation + inline void reserve(int new_capacity) { if (new_capacity <= Capacity) return; T* new_data = (T*)IM_ALLOC((size_t)new_capacity * sizeof(T)); if (Data) { memcpy(new_data, Data, (size_t)Size * sizeof(T)); IM_FREE(Data); } Data = new_data; Capacity = new_capacity; } + inline void reserve_discard(int new_capacity) { if (new_capacity <= Capacity) return; if (Data) IM_FREE(Data); Data = (T*)IM_ALLOC((size_t)new_capacity * sizeof(T)); Capacity = new_capacity; } + + // NB: It is illegal to call push_back/push_front/insert with a reference pointing inside the ImVector data itself! e.g. v.push_back(v[10]) is forbidden. + inline void push_back(const T& v) { if (Size == Capacity) reserve(_grow_capacity(Size + 1)); memcpy(&Data[Size], &v, sizeof(v)); Size++; } + inline void pop_back() { IM_ASSERT(Size > 0); Size--; } + inline void push_front(const T& v) { if (Size == 0) push_back(v); else insert(Data, v); } + inline T* erase(const T* it) { IM_ASSERT(it >= Data && it < Data + Size); const ptrdiff_t off = it - Data; memmove(Data + off, Data + off + 1, ((size_t)Size - (size_t)off - 1) * sizeof(T)); Size--; return Data + off; } + inline T* erase(const T* it, const T* it_last){ IM_ASSERT(it >= Data && it < Data + Size && it_last >= it && it_last <= Data + Size); const ptrdiff_t count = it_last - it; const ptrdiff_t off = it - Data; memmove(Data + off, Data + off + count, ((size_t)Size - (size_t)off - (size_t)count) * sizeof(T)); Size -= (int)count; return Data + off; } + inline T* erase_unsorted(const T* it) { IM_ASSERT(it >= Data && it < Data + Size); const ptrdiff_t off = it - Data; if (it < Data + Size - 1) memcpy(Data + off, Data + Size - 1, sizeof(T)); Size--; return Data + off; } + inline T* insert(const T* it, const T& v) { IM_ASSERT(it >= Data && it <= Data + Size); const ptrdiff_t off = it - Data; if (Size == Capacity) reserve(_grow_capacity(Size + 1)); if (off < (int)Size) memmove(Data + off + 1, Data + off, ((size_t)Size - (size_t)off) * sizeof(T)); memcpy(&Data[off], &v, sizeof(v)); Size++; return Data + off; } + inline bool contains(const T& v) const { const T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data++ == v) return true; return false; } + inline T* find(const T& v) { T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data == v) break; else ++data; return data; } + inline const T* find(const T& v) const { const T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data == v) break; else ++data; return data; } + inline bool find_erase(const T& v) { const T* it = find(v); if (it < Data + Size) { erase(it); return true; } return false; } + inline bool find_erase_unsorted(const T& v) { const T* it = find(v); if (it < Data + Size) { erase_unsorted(it); return true; } return false; } + inline int index_from_ptr(const T* it) const { IM_ASSERT(it >= Data && it < Data + Size); const ptrdiff_t off = it - Data; return (int)off; } +}; +IM_MSVC_RUNTIME_CHECKS_RESTORE + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiStyle +//----------------------------------------------------------------------------- +// You may modify the ImGui::GetStyle() main instance during initialization and before NewFrame(). +// During the frame, use ImGui::PushStyleVar(ImGuiStyleVar_XXXX)/PopStyleVar() to alter the main style values, +// and ImGui::PushStyleColor(ImGuiCol_XXX)/PopStyleColor() for colors. +//----------------------------------------------------------------------------- + +struct ImGuiStyle +{ + float Alpha; // Global alpha applies to everything in Dear ImGui. + float DisabledAlpha; // Additional alpha multiplier applied by BeginDisabled(). Multiply over current value of Alpha. + ImVec2 WindowPadding; // Padding within a window. + float WindowRounding; // Radius of window corners rounding. Set to 0.0f to have rectangular windows. Large values tend to lead to variety of artifacts and are not recommended. + float WindowBorderSize; // Thickness of border around windows. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + ImVec2 WindowMinSize; // Minimum window size. This is a global setting. If you want to constrain individual windows, use SetNextWindowSizeConstraints(). + ImVec2 WindowTitleAlign; // Alignment for title bar text. Defaults to (0.0f,0.5f) for left-aligned,vertically centered. + ImGuiDir WindowMenuButtonPosition; // Side of the collapsing/docking button in the title bar (None/Left/Right). Defaults to ImGuiDir_Left. + float ChildRounding; // Radius of child window corners rounding. Set to 0.0f to have rectangular windows. + float ChildBorderSize; // Thickness of border around child windows. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + float PopupRounding; // Radius of popup window corners rounding. (Note that tooltip windows use WindowRounding) + float PopupBorderSize; // Thickness of border around popup/tooltip windows. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + ImVec2 FramePadding; // Padding within a framed rectangle (used by most widgets). + float FrameRounding; // Radius of frame corners rounding. Set to 0.0f to have rectangular frame (used by most widgets). + float FrameBorderSize; // Thickness of border around frames. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + ImVec2 ItemSpacing; // Horizontal and vertical spacing between widgets/lines. + ImVec2 ItemInnerSpacing; // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label). + ImVec2 CellPadding; // Padding within a table cell + ImVec2 TouchExtraPadding; // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! + float IndentSpacing; // Horizontal indentation when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). + float ColumnsMinSpacing; // Minimum horizontal spacing between two columns. Preferably > (FramePadding.x + 1). + float ScrollbarSize; // Width of the vertical scrollbar, Height of the horizontal scrollbar. + float ScrollbarRounding; // Radius of grab corners for scrollbar. + float GrabMinSize; // Minimum width/height of a grab box for slider/scrollbar. + float GrabRounding; // Radius of grabs corners rounding. Set to 0.0f to have rectangular slider grabs. + float LogSliderDeadzone; // The size in pixels of the dead-zone around zero on logarithmic sliders that cross zero. + float TabRounding; // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs. + float TabBorderSize; // Thickness of border around tabs. + float TabMinWidthForCloseButton; // Minimum width for close button to appear on an unselected tab when hovered. Set to 0.0f to always show when hovering, set to FLT_MAX to never show close button unless selected. + ImGuiDir ColorButtonPosition; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. + ImVec2 ButtonTextAlign; // Alignment of button text when button is larger than text. Defaults to (0.5f, 0.5f) (centered). + ImVec2 SelectableTextAlign; // Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. + float SeparatorTextBorderSize; // Thickkness of border in SeparatorText() + ImVec2 SeparatorTextAlign; // Alignment of text within the separator. Defaults to (0.0f, 0.5f) (left aligned, center). + ImVec2 SeparatorTextPadding; // Horizontal offset of text from each edge of the separator + spacing on other axis. Generally small values. .y is recommended to be == FramePadding.y. + ImVec2 DisplayWindowPadding; // Window position are clamped to be visible within the display area or monitors by at least this amount. Only applies to regular windows. + ImVec2 DisplaySafeAreaPadding; // If you cannot see the edges of your screen (e.g. on a TV) increase the safe area padding. Apply to popups/tooltips as well regular windows. NB: Prefer configuring your TV sets correctly! + float MouseCursorScale; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). We apply per-monitor DPI scaling over this scale. May be removed later. + bool AntiAliasedLines; // Enable anti-aliased lines/borders. Disable if you are really tight on CPU/GPU. Latched at the beginning of the frame (copied to ImDrawList). + bool AntiAliasedLinesUseTex; // Enable anti-aliased lines/borders using textures where possible. Require backend to render with bilinear filtering (NOT point/nearest filtering). Latched at the beginning of the frame (copied to ImDrawList). + bool AntiAliasedFill; // Enable anti-aliased edges around filled shapes (rounded rectangles, circles, etc.). Disable if you are really tight on CPU/GPU. Latched at the beginning of the frame (copied to ImDrawList). + float CurveTessellationTol; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. + float CircleTessellationMaxError; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry. + ImVec4 Colors[ImGuiCol_COUNT]; + + // Behaviors + // (It is possible to modify those fields mid-frame if specific behavior need it, unlike e.g. configuration fields in ImGuiIO) + float HoverStationaryDelay; // Delay for IsItemHovered(ImGuiHoveredFlags_Stationary). Time required to consider mouse stationary. + float HoverDelayShort; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayShort). Usually used along with HoverStationaryDelay. + float HoverDelayNormal; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayNormal). " + ImGuiHoveredFlags HoverFlagsForTooltipMouse;// Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using mouse. + ImGuiHoveredFlags HoverFlagsForTooltipNav; // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using keyboard/gamepad. + + IMGUI_API ImGuiStyle(); + IMGUI_API void ScaleAllSizes(float scale_factor); +}; + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiIO +//----------------------------------------------------------------------------- +// Communicate most settings and inputs/outputs to Dear ImGui using this structure. +// Access via ImGui::GetIO(). Read 'Programmer guide' section in .cpp file for general usage. +//----------------------------------------------------------------------------- + +// [Internal] Storage used by IsKeyDown(), IsKeyPressed() etc functions. +// If prior to 1.87 you used io.KeysDownDuration[] (which was marked as internal), you should use GetKeyData(key)->DownDuration and *NOT* io.KeysData[key]->DownDuration. +struct ImGuiKeyData +{ + bool Down; // True for if key is down + float DownDuration; // Duration the key has been down (<0.0f: not pressed, 0.0f: just pressed, >0.0f: time held) + float DownDurationPrev; // Last frame duration the key has been down + float AnalogValue; // 0.0f..1.0f for gamepad values +}; + +struct ImGuiIO +{ + //------------------------------------------------------------------ + // Configuration // Default value + //------------------------------------------------------------------ + + ImGuiConfigFlags ConfigFlags; // = 0 // See ImGuiConfigFlags_ enum. Set by user/application. Gamepad/keyboard navigation options, etc. + ImGuiBackendFlags BackendFlags; // = 0 // See ImGuiBackendFlags_ enum. Set by backend (imgui_impl_xxx files or custom backend) to communicate features supported by the backend. + ImVec2 DisplaySize; // // Main display size, in pixels (generally == GetMainViewport()->Size). May change every frame. + float DeltaTime; // = 1.0f/60.0f // Time elapsed since last frame, in seconds. May change every frame. + float IniSavingRate; // = 5.0f // Minimum time between saving positions/sizes to .ini file, in seconds. + const char* IniFilename; // = "imgui.ini" // Path to .ini file (important: default "imgui.ini" is relative to current working dir!). Set NULL to disable automatic .ini loading/saving or if you want to manually call LoadIniSettingsXXX() / SaveIniSettingsXXX() functions. + const char* LogFilename; // = "imgui_log.txt"// Path to .log file (default parameter to ImGui::LogToFile when no file is specified). + void* UserData; // = NULL // Store your own data. + + ImFontAtlas*Fonts; // // Font atlas: load, rasterize and pack one or more fonts into a single texture. + float FontGlobalScale; // = 1.0f // Global scale all fonts + bool FontAllowUserScaling; // = false // Allow user scaling text of individual window with CTRL+Wheel. + ImFont* FontDefault; // = NULL // Font to use on NewFrame(). Use NULL to uses Fonts->Fonts[0]. + ImVec2 DisplayFramebufferScale; // = (1, 1) // For retina display or other situations where window coordinates are different from framebuffer coordinates. This generally ends up in ImDrawData::FramebufferScale. + + // Docking options (when ImGuiConfigFlags_DockingEnable is set) + bool ConfigDockingNoSplit; // = false // Simplified docking mode: disable window splitting, so docking is limited to merging multiple windows together into tab-bars. + bool ConfigDockingWithShift; // = false // Enable docking with holding Shift key (reduce visual noise, allows dropping in wider space) + bool ConfigDockingAlwaysTabBar; // = false // [BETA] [FIXME: This currently creates regression with auto-sizing and general overhead] Make every single floating window display within a docking node. + bool ConfigDockingTransparentPayload;// = false // [BETA] Make window or viewport transparent when docking and only display docking boxes on the target viewport. Useful if rendering of multiple viewport cannot be synced. Best used with ConfigViewportsNoAutoMerge. + + // Viewport options (when ImGuiConfigFlags_ViewportsEnable is set) + bool ConfigViewportsNoAutoMerge; // = false; // Set to make all floating imgui windows always create their own viewport. Otherwise, they are merged into the main host viewports when overlapping it. May also set ImGuiViewportFlags_NoAutoMerge on individual viewport. + bool ConfigViewportsNoTaskBarIcon; // = false // Disable default OS task bar icon flag for secondary viewports. When a viewport doesn't want a task bar icon, ImGuiViewportFlags_NoTaskBarIcon will be set on it. + bool ConfigViewportsNoDecoration; // = true // Disable default OS window decoration flag for secondary viewports. When a viewport doesn't want window decorations, ImGuiViewportFlags_NoDecoration will be set on it. Enabling decoration can create subsequent issues at OS levels (e.g. minimum window size). + bool ConfigViewportsNoDefaultParent; // = false // Disable default OS parenting to main viewport for secondary viewports. By default, viewports are marked with ParentViewportId = , expecting the platform backend to setup a parent/child relationship between the OS windows (some backend may ignore this). Set to true if you want the default to be 0, then all viewports will be top-level OS windows. + + // Miscellaneous options + bool MouseDrawCursor; // = false // Request ImGui to draw a mouse cursor for you (if you are on a platform without a mouse cursor). Cannot be easily renamed to 'io.ConfigXXX' because this is frequently used by backend implementations. + bool ConfigMacOSXBehaviors; // = defined(__APPLE__) // OS X style: Text editing cursor movement using Alt instead of Ctrl, Shortcuts using Cmd/Super instead of Ctrl, Line/Text Start and End using Cmd+Arrows instead of Home/End, Double click selects by word instead of selecting whole text, Multi-selection in lists uses Cmd/Super instead of Ctrl. + bool ConfigInputTrickleEventQueue; // = true // Enable input queue trickling: some types of events submitted during the same frame (e.g. button down + up) will be spread over multiple frames, improving interactions with low framerates. + bool ConfigInputTextCursorBlink; // = true // Enable blinking cursor (optional as some users consider it to be distracting). + bool ConfigInputTextEnterKeepActive; // = false // [BETA] Pressing Enter will keep item active and select contents (single-line only). + bool ConfigDragClickToInputText; // = false // [BETA] Enable turning DragXXX widgets into text input with a simple mouse click-release (without moving). Not desirable on devices without a keyboard. + bool ConfigWindowsResizeFromEdges; // = true // Enable resizing of windows from their edges and from the lower-left corner. This requires (io.BackendFlags & ImGuiBackendFlags_HasMouseCursors) because it needs mouse cursor feedback. (This used to be a per-window ImGuiWindowFlags_ResizeFromAnySide flag) + bool ConfigWindowsMoveFromTitleBarOnly; // = false // Enable allowing to move windows only when clicking on their title bar. Does not apply to windows without a title bar. + float ConfigMemoryCompactTimer; // = 60.0f // Timer (in seconds) to free transient windows/tables memory buffers when unused. Set to -1.0f to disable. + + // Inputs Behaviors + // (other variables, ones which are expected to be tweaked within UI code, are exposed in ImGuiStyle) + float MouseDoubleClickTime; // = 0.30f // Time for a double-click, in seconds. + float MouseDoubleClickMaxDist; // = 6.0f // Distance threshold to stay in to validate a double-click, in pixels. + float MouseDragThreshold; // = 6.0f // Distance threshold before considering we are dragging. + float KeyRepeatDelay; // = 0.275f // When holding a key/button, time before it starts repeating, in seconds (for buttons in Repeat mode, etc.). + float KeyRepeatRate; // = 0.050f // When holding a key/button, rate at which it repeats, in seconds. + + //------------------------------------------------------------------ + // Debug options + //------------------------------------------------------------------ + + // Tools to test correct Begin/End and BeginChild/EndChild behaviors. + // Presently Begin()/End() and BeginChild()/EndChild() needs to ALWAYS be called in tandem, regardless of return value of BeginXXX() + // This is inconsistent with other BeginXXX functions and create confusion for many users. + // We expect to update the API eventually. In the meanwhile we provide tools to facilitate checking user-code behavior. + bool ConfigDebugBeginReturnValueOnce;// = false // First-time calls to Begin()/BeginChild() will return false. NEEDS TO BE SET AT APPLICATION BOOT TIME if you don't want to miss windows. + bool ConfigDebugBeginReturnValueLoop;// = false // Some calls to Begin()/BeginChild() will return false. Will cycle through window depths then repeat. Suggested use: add "io.ConfigDebugBeginReturnValue = io.KeyShift" in your main loop then occasionally press SHIFT. Windows should be flickering while running. + + // Option to deactivate io.AddFocusEvent(false) handling. May facilitate interactions with a debugger when focus loss leads to clearing inputs data. + // Backends may have other side-effects on focus loss, so this will reduce side-effects but not necessary remove all of them. + // Consider using e.g. Win32's IsDebuggerPresent() as an additional filter (or see ImOsIsDebuggerPresent() in imgui_test_engine/imgui_te_utils.cpp for a Unix compatible version). + bool ConfigDebugIgnoreFocusLoss; // = false // Ignore io.AddFocusEvent(false), consequently not calling io.ClearInputKeys() in input processing. + + // Option to audit .ini data + bool ConfigDebugIniSettings; // = false // Save .ini data with extra comments (particularly helpful for Docking, but makes saving slower) + + //------------------------------------------------------------------ + // Platform Functions + // (the imgui_impl_xxxx backend files are setting those up for you) + //------------------------------------------------------------------ + + // Optional: Platform/Renderer backend name (informational only! will be displayed in About Window) + User data for backend/wrappers to store their own stuff. + const char* BackendPlatformName; // = NULL + const char* BackendRendererName; // = NULL + void* BackendPlatformUserData; // = NULL // User data for platform backend + void* BackendRendererUserData; // = NULL // User data for renderer backend + void* BackendLanguageUserData; // = NULL // User data for non C++ programming language backend + + // Optional: Access OS clipboard + // (default to use native Win32 clipboard on Windows, otherwise uses a private clipboard. Override to access OS clipboard on other architectures) + const char* (*GetClipboardTextFn)(void* user_data); + void (*SetClipboardTextFn)(void* user_data, const char* text); + void* ClipboardUserData; + + // Optional: Notify OS Input Method Editor of the screen position of your cursor for text input position (e.g. when using Japanese/Chinese IME on Windows) + // (default to use native imm32 api on Windows) + void (*SetPlatformImeDataFn)(ImGuiViewport* viewport, ImGuiPlatformImeData* data); +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + void* ImeWindowHandle; // = NULL // [Obsolete] Set ImGuiViewport::PlatformHandleRaw instead. Set this to your HWND to get automatic IME cursor positioning. +#else + void* _UnusedPadding; // Unused field to keep data structure the same size. +#endif + + //------------------------------------------------------------------ + // Input - Call before calling NewFrame() + //------------------------------------------------------------------ + + // Input Functions + IMGUI_API void AddKeyEvent(ImGuiKey key, bool down); // Queue a new key down/up event. Key should be "translated" (as in, generally ImGuiKey_A matches the key end-user would use to emit an 'A' character) + IMGUI_API void AddKeyAnalogEvent(ImGuiKey key, bool down, float v); // Queue a new key down/up event for analog values (e.g. ImGuiKey_Gamepad_ values). Dead-zones should be handled by the backend. + IMGUI_API void AddMousePosEvent(float x, float y); // Queue a mouse position update. Use -FLT_MAX,-FLT_MAX to signify no mouse (e.g. app not focused and not hovered) + IMGUI_API void AddMouseButtonEvent(int button, bool down); // Queue a mouse button change + IMGUI_API void AddMouseWheelEvent(float wheel_x, float wheel_y); // Queue a mouse wheel update. wheel_y<0: scroll down, wheel_y>0: scroll up, wheel_x<0: scroll right, wheel_x>0: scroll left. + IMGUI_API void AddMouseSourceEvent(ImGuiMouseSource source); // Queue a mouse source change (Mouse/TouchScreen/Pen) + IMGUI_API void AddMouseViewportEvent(ImGuiID id); // Queue a mouse hovered viewport. Requires backend to set ImGuiBackendFlags_HasMouseHoveredViewport to call this (for multi-viewport support). + IMGUI_API void AddFocusEvent(bool focused); // Queue a gain/loss of focus for the application (generally based on OS/platform focus of your window) + IMGUI_API void AddInputCharacter(unsigned int c); // Queue a new character input + IMGUI_API void AddInputCharacterUTF16(ImWchar16 c); // Queue a new character input from a UTF-16 character, it can be a surrogate + IMGUI_API void AddInputCharactersUTF8(const char* str); // Queue a new characters input from a UTF-8 string + + IMGUI_API void SetKeyEventNativeData(ImGuiKey key, int native_keycode, int native_scancode, int native_legacy_index = -1); // [Optional] Specify index for legacy <1.87 IsKeyXXX() functions with native indices + specify native keycode, scancode. + IMGUI_API void SetAppAcceptingEvents(bool accepting_events); // Set master flag for accepting key/mouse/text events (default to true). Useful if you have native dialog boxes that are interrupting your application loop/refresh, and you want to disable events being queued while your app is frozen. + IMGUI_API void ClearInputCharacters(); // [Internal] Clear the text input buffer manually + IMGUI_API void ClearInputKeys(); // [Internal] Release all keys + + //------------------------------------------------------------------ + // Output - Updated by NewFrame() or EndFrame()/Render() + // (when reading from the io.WantCaptureMouse, io.WantCaptureKeyboard flags to dispatch your inputs, it is + // generally easier and more correct to use their state BEFORE calling NewFrame(). See FAQ for details!) + //------------------------------------------------------------------ + + bool WantCaptureMouse; // Set when Dear ImGui will use mouse inputs, in this case do not dispatch them to your main game/application (either way, always pass on mouse inputs to imgui). (e.g. unclicked mouse is hovering over an imgui window, widget is active, mouse was clicked over an imgui window, etc.). + bool WantCaptureKeyboard; // Set when Dear ImGui will use keyboard inputs, in this case do not dispatch them to your main game/application (either way, always pass keyboard inputs to imgui). (e.g. InputText active, or an imgui window is focused and navigation is enabled, etc.). + bool WantTextInput; // Mobile/console: when set, you may display an on-screen keyboard. This is set by Dear ImGui when it wants textual keyboard input to happen (e.g. when a InputText widget is active). + bool WantSetMousePos; // MousePos has been altered, backend should reposition mouse on next frame. Rarely used! Set only when ImGuiConfigFlags_NavEnableSetMousePos flag is enabled. + bool WantSaveIniSettings; // When manual .ini load/save is active (io.IniFilename == NULL), this will be set to notify your application that you can call SaveIniSettingsToMemory() and save yourself. Important: clear io.WantSaveIniSettings yourself after saving! + bool NavActive; // Keyboard/Gamepad navigation is currently allowed (will handle ImGuiKey_NavXXX events) = a window is focused and it doesn't use the ImGuiWindowFlags_NoNavInputs flag. + bool NavVisible; // Keyboard/Gamepad navigation is visible and allowed (will handle ImGuiKey_NavXXX events). + float Framerate; // Estimate of application framerate (rolling average over 60 frames, based on io.DeltaTime), in frame per second. Solely for convenience. Slow applications may not want to use a moving average or may want to reset underlying buffers occasionally. + int MetricsRenderVertices; // Vertices output during last call to Render() + int MetricsRenderIndices; // Indices output during last call to Render() = number of triangles * 3 + int MetricsRenderWindows; // Number of visible windows + int MetricsActiveWindows; // Number of active windows + int MetricsActiveAllocations; // Number of active allocations, updated by MemAlloc/MemFree based on current context. May be off if you have multiple imgui contexts. + ImVec2 MouseDelta; // Mouse delta. Note that this is zero if either current or previous position are invalid (-FLT_MAX,-FLT_MAX), so a disappearing/reappearing mouse won't have a huge delta. + + // Legacy: before 1.87, we required backend to fill io.KeyMap[] (imgui->native map) during initialization and io.KeysDown[] (native indices) every frame. + // This is still temporarily supported as a legacy feature. However the new preferred scheme is for backend to call io.AddKeyEvent(). + // Old (<1.87): ImGui::IsKeyPressed(ImGui::GetIO().KeyMap[ImGuiKey_Space]) --> New (1.87+) ImGui::IsKeyPressed(ImGuiKey_Space) +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + int KeyMap[ImGuiKey_COUNT]; // [LEGACY] Input: map of indices into the KeysDown[512] entries array which represent your "native" keyboard state. The first 512 are now unused and should be kept zero. Legacy backend will write into KeyMap[] using ImGuiKey_ indices which are always >512. + bool KeysDown[ImGuiKey_COUNT]; // [LEGACY] Input: Keyboard keys that are pressed (ideally left in the "native" order your engine has access to keyboard keys, so you can use your own defines/enums for keys). This used to be [512] sized. It is now ImGuiKey_COUNT to allow legacy io.KeysDown[GetKeyIndex(...)] to work without an overflow. + float NavInputs[ImGuiNavInput_COUNT]; // [LEGACY] Since 1.88, NavInputs[] was removed. Backends from 1.60 to 1.86 won't build. Feed gamepad inputs via io.AddKeyEvent() and ImGuiKey_GamepadXXX enums. +#endif + + //------------------------------------------------------------------ + // [Internal] Dear ImGui will maintain those fields. Forward compatibility not guaranteed! + //------------------------------------------------------------------ + + ImGuiContext* Ctx; // Parent UI context (needs to be set explicitly by parent). + + // Main Input State + // (this block used to be written by backend, since 1.87 it is best to NOT write to those directly, call the AddXXX functions above instead) + // (reading from those variables is fair game, as they are extremely unlikely to be moving anywhere) + ImVec2 MousePos; // Mouse position, in pixels. Set to ImVec2(-FLT_MAX, -FLT_MAX) if mouse is unavailable (on another screen, etc.) + bool MouseDown[5]; // Mouse buttons: 0=left, 1=right, 2=middle + extras (ImGuiMouseButton_COUNT == 5). Dear ImGui mostly uses left and right buttons. Other buttons allow us to track if the mouse is being used by your application + available to user as a convenience via IsMouse** API. + float MouseWheel; // Mouse wheel Vertical: 1 unit scrolls about 5 lines text. >0 scrolls Up, <0 scrolls Down. Hold SHIFT to turn vertical scroll into horizontal scroll. + float MouseWheelH; // Mouse wheel Horizontal. >0 scrolls Left, <0 scrolls Right. Most users don't have a mouse with a horizontal wheel, may not be filled by all backends. + ImGuiMouseSource MouseSource; // Mouse actual input peripheral (Mouse/TouchScreen/Pen). + ImGuiID MouseHoveredViewport; // (Optional) Modify using io.AddMouseViewportEvent(). With multi-viewports: viewport the OS mouse is hovering. If possible _IGNORING_ viewports with the ImGuiViewportFlags_NoInputs flag is much better (few backends can handle that). Set io.BackendFlags |= ImGuiBackendFlags_HasMouseHoveredViewport if you can provide this info. If you don't imgui will infer the value using the rectangles and last focused time of the viewports it knows about (ignoring other OS windows). + bool KeyCtrl; // Keyboard modifier down: Control + bool KeyShift; // Keyboard modifier down: Shift + bool KeyAlt; // Keyboard modifier down: Alt + bool KeySuper; // Keyboard modifier down: Cmd/Super/Windows + + // Other state maintained from data above + IO function calls + ImGuiKeyChord KeyMods; // Key mods flags (any of ImGuiMod_Ctrl/ImGuiMod_Shift/ImGuiMod_Alt/ImGuiMod_Super flags, same as io.KeyCtrl/KeyShift/KeyAlt/KeySuper but merged into flags. DOES NOT CONTAINS ImGuiMod_Shortcut which is pretranslated). Read-only, updated by NewFrame() + ImGuiKeyData KeysData[ImGuiKey_KeysData_SIZE]; // Key state for all known keys. Use IsKeyXXX() functions to access this. + bool WantCaptureMouseUnlessPopupClose; // Alternative to WantCaptureMouse: (WantCaptureMouse == true && WantCaptureMouseUnlessPopupClose == false) when a click over void is expected to close a popup. + ImVec2 MousePosPrev; // Previous mouse position (note that MouseDelta is not necessary == MousePos-MousePosPrev, in case either position is invalid) + ImVec2 MouseClickedPos[5]; // Position at time of clicking + double MouseClickedTime[5]; // Time of last click (used to figure out double-click) + bool MouseClicked[5]; // Mouse button went from !Down to Down (same as MouseClickedCount[x] != 0) + bool MouseDoubleClicked[5]; // Has mouse button been double-clicked? (same as MouseClickedCount[x] == 2) + ImU16 MouseClickedCount[5]; // == 0 (not clicked), == 1 (same as MouseClicked[]), == 2 (double-clicked), == 3 (triple-clicked) etc. when going from !Down to Down + ImU16 MouseClickedLastCount[5]; // Count successive number of clicks. Stays valid after mouse release. Reset after another click is done. + bool MouseReleased[5]; // Mouse button went from Down to !Down + bool MouseDownOwned[5]; // Track if button was clicked inside a dear imgui window or over void blocked by a popup. We don't request mouse capture from the application if click started outside ImGui bounds. + bool MouseDownOwnedUnlessPopupClose[5]; // Track if button was clicked inside a dear imgui window. + bool MouseWheelRequestAxisSwap; // On a non-Mac system, holding SHIFT requests WheelY to perform the equivalent of a WheelX event. On a Mac system this is already enforced by the system. + float MouseDownDuration[5]; // Duration the mouse button has been down (0.0f == just clicked) + float MouseDownDurationPrev[5]; // Previous time the mouse button has been down + ImVec2 MouseDragMaxDistanceAbs[5]; // Maximum distance, absolute, on each axis, of how much mouse has traveled from the clicking point + float MouseDragMaxDistanceSqr[5]; // Squared maximum distance of how much mouse has traveled from the clicking point (used for moving thresholds) + float PenPressure; // Touch/Pen pressure (0.0f to 1.0f, should be >0.0f only when MouseDown[0] == true). Helper storage currently unused by Dear ImGui. + bool AppFocusLost; // Only modify via AddFocusEvent() + bool AppAcceptingEvents; // Only modify via SetAppAcceptingEvents() + ImS8 BackendUsingLegacyKeyArrays; // -1: unknown, 0: using AddKeyEvent(), 1: using legacy io.KeysDown[] + bool BackendUsingLegacyNavInputArray; // 0: using AddKeyAnalogEvent(), 1: writing to legacy io.NavInputs[] directly + ImWchar16 InputQueueSurrogate; // For AddInputCharacterUTF16() + ImVector InputQueueCharacters; // Queue of _characters_ input (obtained by platform backend). Fill using AddInputCharacter() helper. + + IMGUI_API ImGuiIO(); +}; + +//----------------------------------------------------------------------------- +// [SECTION] Misc data structures +//----------------------------------------------------------------------------- + +// Shared state of InputText(), passed as an argument to your callback when a ImGuiInputTextFlags_Callback* flag is used. +// The callback function should return 0 by default. +// Callbacks (follow a flag name and see comments in ImGuiInputTextFlags_ declarations for more details) +// - ImGuiInputTextFlags_CallbackEdit: Callback on buffer edit (note that InputText() already returns true on edit, the callback is useful mainly to manipulate the underlying buffer while focus is active) +// - ImGuiInputTextFlags_CallbackAlways: Callback on each iteration +// - ImGuiInputTextFlags_CallbackCompletion: Callback on pressing TAB +// - ImGuiInputTextFlags_CallbackHistory: Callback on pressing Up/Down arrows +// - ImGuiInputTextFlags_CallbackCharFilter: Callback on character inputs to replace or discard them. Modify 'EventChar' to replace or discard, or return 1 in callback to discard. +// - ImGuiInputTextFlags_CallbackResize: Callback on buffer capacity changes request (beyond 'buf_size' parameter value), allowing the string to grow. +struct ImGuiInputTextCallbackData +{ + ImGuiContext* Ctx; // Parent UI context + ImGuiInputTextFlags EventFlag; // One ImGuiInputTextFlags_Callback* // Read-only + ImGuiInputTextFlags Flags; // What user passed to InputText() // Read-only + void* UserData; // What user passed to InputText() // Read-only + + // Arguments for the different callback events + // - To modify the text buffer in a callback, prefer using the InsertChars() / DeleteChars() function. InsertChars() will take care of calling the resize callback if necessary. + // - If you know your edits are not going to resize the underlying buffer allocation, you may modify the contents of 'Buf[]' directly. You need to update 'BufTextLen' accordingly (0 <= BufTextLen < BufSize) and set 'BufDirty'' to true so InputText can update its internal state. + ImWchar EventChar; // Character input // Read-write // [CharFilter] Replace character with another one, or set to zero to drop. return 1 is equivalent to setting EventChar=0; + ImGuiKey EventKey; // Key pressed (Up/Down/TAB) // Read-only // [Completion,History] + char* Buf; // Text buffer // Read-write // [Resize] Can replace pointer / [Completion,History,Always] Only write to pointed data, don't replace the actual pointer! + int BufTextLen; // Text length (in bytes) // Read-write // [Resize,Completion,History,Always] Exclude zero-terminator storage. In C land: == strlen(some_text), in C++ land: string.length() + int BufSize; // Buffer size (in bytes) = capacity+1 // Read-only // [Resize,Completion,History,Always] Include zero-terminator storage. In C land == ARRAYSIZE(my_char_array), in C++ land: string.capacity()+1 + bool BufDirty; // Set if you modify Buf/BufTextLen! // Write // [Completion,History,Always] + int CursorPos; // // Read-write // [Completion,History,Always] + int SelectionStart; // // Read-write // [Completion,History,Always] == to SelectionEnd when no selection) + int SelectionEnd; // // Read-write // [Completion,History,Always] + + // Helper functions for text manipulation. + // Use those function to benefit from the CallbackResize behaviors. Calling those function reset the selection. + IMGUI_API ImGuiInputTextCallbackData(); + IMGUI_API void DeleteChars(int pos, int bytes_count); + IMGUI_API void InsertChars(int pos, const char* text, const char* text_end = NULL); + void SelectAll() { SelectionStart = 0; SelectionEnd = BufTextLen; } + void ClearSelection() { SelectionStart = SelectionEnd = BufTextLen; } + bool HasSelection() const { return SelectionStart != SelectionEnd; } +}; + +// Resizing callback data to apply custom constraint. As enabled by SetNextWindowSizeConstraints(). Callback is called during the next Begin(). +// NB: For basic min/max size constraint on each axis you don't need to use the callback! The SetNextWindowSizeConstraints() parameters are enough. +struct ImGuiSizeCallbackData +{ + void* UserData; // Read-only. What user passed to SetNextWindowSizeConstraints(). Generally store an integer or float in here (need reinterpret_cast<>). + ImVec2 Pos; // Read-only. Window position, for reference. + ImVec2 CurrentSize; // Read-only. Current window size. + ImVec2 DesiredSize; // Read-write. Desired size, based on user's mouse position. Write to this field to restrain resizing. +}; + +// [ALPHA] Rarely used / very advanced uses only. Use with SetNextWindowClass() and DockSpace() functions. +// Important: the content of this class is still highly WIP and likely to change and be refactored +// before we stabilize Docking features. Please be mindful if using this. +// Provide hints: +// - To the platform backend via altered viewport flags (enable/disable OS decoration, OS task bar icons, etc.) +// - To the platform backend for OS level parent/child relationships of viewport. +// - To the docking system for various options and filtering. +struct ImGuiWindowClass +{ + ImGuiID ClassId; // User data. 0 = Default class (unclassed). Windows of different classes cannot be docked with each others. + ImGuiID ParentViewportId; // Hint for the platform backend. -1: use default. 0: request platform backend to not parent the platform. != 0: request platform backend to create a parent<>child relationship between the platform windows. Not conforming backends are free to e.g. parent every viewport to the main viewport or not. + ImGuiViewportFlags ViewportFlagsOverrideSet; // Viewport flags to set when a window of this class owns a viewport. This allows you to enforce OS decoration or task bar icon, override the defaults on a per-window basis. + ImGuiViewportFlags ViewportFlagsOverrideClear; // Viewport flags to clear when a window of this class owns a viewport. This allows you to enforce OS decoration or task bar icon, override the defaults on a per-window basis. + ImGuiTabItemFlags TabItemFlagsOverrideSet; // [EXPERIMENTAL] TabItem flags to set when a window of this class gets submitted into a dock node tab bar. May use with ImGuiTabItemFlags_Leading or ImGuiTabItemFlags_Trailing. + ImGuiDockNodeFlags DockNodeFlagsOverrideSet; // [EXPERIMENTAL] Dock node flags to set when a window of this class is hosted by a dock node (it doesn't have to be selected!) + bool DockingAlwaysTabBar; // Set to true to enforce single floating windows of this class always having their own docking node (equivalent of setting the global io.ConfigDockingAlwaysTabBar) + bool DockingAllowUnclassed; // Set to true to allow windows of this class to be docked/merged with an unclassed window. // FIXME-DOCK: Move to DockNodeFlags override? + + ImGuiWindowClass() { memset(this, 0, sizeof(*this)); ParentViewportId = (ImGuiID)-1; DockingAllowUnclassed = true; } +}; + +// Data payload for Drag and Drop operations: AcceptDragDropPayload(), GetDragDropPayload() +struct ImGuiPayload +{ + // Members + void* Data; // Data (copied and owned by dear imgui) + int DataSize; // Data size + + // [Internal] + ImGuiID SourceId; // Source item id + ImGuiID SourceParentId; // Source parent id (if available) + int DataFrameCount; // Data timestamp + char DataType[32 + 1]; // Data type tag (short user-supplied string, 32 characters max) + bool Preview; // Set when AcceptDragDropPayload() was called and mouse has been hovering the target item (nb: handle overlapping drag targets) + bool Delivery; // Set when AcceptDragDropPayload() was called and mouse button is released over the target item. + + ImGuiPayload() { Clear(); } + void Clear() { SourceId = SourceParentId = 0; Data = NULL; DataSize = 0; memset(DataType, 0, sizeof(DataType)); DataFrameCount = -1; Preview = Delivery = false; } + bool IsDataType(const char* type) const { return DataFrameCount != -1 && strcmp(type, DataType) == 0; } + bool IsPreview() const { return Preview; } + bool IsDelivery() const { return Delivery; } +}; + +// Sorting specification for one column of a table (sizeof == 12 bytes) +struct ImGuiTableColumnSortSpecs +{ + ImGuiID ColumnUserID; // User id of the column (if specified by a TableSetupColumn() call) + ImS16 ColumnIndex; // Index of the column + ImS16 SortOrder; // Index within parent ImGuiTableSortSpecs (always stored in order starting from 0, tables sorted on a single criteria will always have a 0 here) + ImGuiSortDirection SortDirection : 8; // ImGuiSortDirection_Ascending or ImGuiSortDirection_Descending (you can use this or SortSign, whichever is more convenient for your sort function) + + ImGuiTableColumnSortSpecs() { memset(this, 0, sizeof(*this)); } +}; + +// Sorting specifications for a table (often handling sort specs for a single column, occasionally more) +// Obtained by calling TableGetSortSpecs(). +// When 'SpecsDirty == true' you can sort your data. It will be true with sorting specs have changed since last call, or the first time. +// Make sure to set 'SpecsDirty = false' after sorting, else you may wastefully sort your data every frame! +struct ImGuiTableSortSpecs +{ + const ImGuiTableColumnSortSpecs* Specs; // Pointer to sort spec array. + int SpecsCount; // Sort spec count. Most often 1. May be > 1 when ImGuiTableFlags_SortMulti is enabled. May be == 0 when ImGuiTableFlags_SortTristate is enabled. + bool SpecsDirty; // Set to true when specs have changed since last time! Use this to sort again, then clear the flag. + + ImGuiTableSortSpecs() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, Math Operators, ImColor) +//----------------------------------------------------------------------------- + +// Helper: Unicode defines +#define IM_UNICODE_CODEPOINT_INVALID 0xFFFD // Invalid Unicode code point (standard value). +#ifdef IMGUI_USE_WCHAR32 +#define IM_UNICODE_CODEPOINT_MAX 0x10FFFF // Maximum Unicode code point supported by this build. +#else +#define IM_UNICODE_CODEPOINT_MAX 0xFFFF // Maximum Unicode code point supported by this build. +#endif + +// Helper: Execute a block of code at maximum once a frame. Convenient if you want to quickly create a UI within deep-nested code that runs multiple times every frame. +// Usage: static ImGuiOnceUponAFrame oaf; if (oaf) ImGui::Text("This will be called only once per frame"); +struct ImGuiOnceUponAFrame +{ + ImGuiOnceUponAFrame() { RefFrame = -1; } + mutable int RefFrame; + operator bool() const { int current_frame = ImGui::GetFrameCount(); if (RefFrame == current_frame) return false; RefFrame = current_frame; return true; } +}; + +// Helper: Parse and apply text filters. In format "aaaaa[,bbbb][,ccccc]" +struct ImGuiTextFilter +{ + IMGUI_API ImGuiTextFilter(const char* default_filter = ""); + IMGUI_API bool Draw(const char* label = "Filter (inc,-exc)", float width = 0.0f); // Helper calling InputText+Build + IMGUI_API bool PassFilter(const char* text, const char* text_end = NULL) const; + IMGUI_API void Build(); + void Clear() { InputBuf[0] = 0; Build(); } + bool IsActive() const { return !Filters.empty(); } + + // [Internal] + struct ImGuiTextRange + { + const char* b; + const char* e; + + ImGuiTextRange() { b = e = NULL; } + ImGuiTextRange(const char* _b, const char* _e) { b = _b; e = _e; } + bool empty() const { return b == e; } + IMGUI_API void split(char separator, ImVector* out) const; + }; + char InputBuf[256]; + ImVectorFilters; + int CountGrep; +}; + +// Helper: Growable text buffer for logging/accumulating text +// (this could be called 'ImGuiTextBuilder' / 'ImGuiStringBuilder') +struct ImGuiTextBuffer +{ + ImVector Buf; + IMGUI_API static char EmptyString[1]; + + ImGuiTextBuffer() { } + inline char operator[](int i) const { IM_ASSERT(Buf.Data != NULL); return Buf.Data[i]; } + const char* begin() const { return Buf.Data ? &Buf.front() : EmptyString; } + const char* end() const { return Buf.Data ? &Buf.back() : EmptyString; } // Buf is zero-terminated, so end() will point on the zero-terminator + int size() const { return Buf.Size ? Buf.Size - 1 : 0; } + bool empty() const { return Buf.Size <= 1; } + void clear() { Buf.clear(); } + void reserve(int capacity) { Buf.reserve(capacity); } + const char* c_str() const { return Buf.Data ? Buf.Data : EmptyString; } + IMGUI_API void append(const char* str, const char* str_end = NULL); + IMGUI_API void appendf(const char* fmt, ...) IM_FMTARGS(2); + IMGUI_API void appendfv(const char* fmt, va_list args) IM_FMTLIST(2); +}; + +// Helper: Key->Value storage +// Typically you don't have to worry about this since a storage is held within each Window. +// We use it to e.g. store collapse state for a tree (Int 0/1) +// This is optimized for efficient lookup (dichotomy into a contiguous buffer) and rare insertion (typically tied to user interactions aka max once a frame) +// You can use it as custom user storage for temporary values. Declare your own storage if, for example: +// - You want to manipulate the open/close state of a particular sub-tree in your interface (tree node uses Int 0/1 to store their state). +// - You want to store custom debug data easily without adding or editing structures in your code (probably not efficient, but convenient) +// Types are NOT stored, so it is up to you to make sure your Key don't collide with different types. +struct ImGuiStorage +{ + // [Internal] + struct ImGuiStoragePair + { + ImGuiID key; + union { int val_i; float val_f; void* val_p; }; + ImGuiStoragePair(ImGuiID _key, int _val_i) { key = _key; val_i = _val_i; } + ImGuiStoragePair(ImGuiID _key, float _val_f) { key = _key; val_f = _val_f; } + ImGuiStoragePair(ImGuiID _key, void* _val_p) { key = _key; val_p = _val_p; } + }; + + ImVector Data; + + // - Get***() functions find pair, never add/allocate. Pairs are sorted so a query is O(log N) + // - Set***() functions find pair, insertion on demand if missing. + // - Sorted insertion is costly, paid once. A typical frame shouldn't need to insert any new pair. + void Clear() { Data.clear(); } + IMGUI_API int GetInt(ImGuiID key, int default_val = 0) const; + IMGUI_API void SetInt(ImGuiID key, int val); + IMGUI_API bool GetBool(ImGuiID key, bool default_val = false) const; + IMGUI_API void SetBool(ImGuiID key, bool val); + IMGUI_API float GetFloat(ImGuiID key, float default_val = 0.0f) const; + IMGUI_API void SetFloat(ImGuiID key, float val); + IMGUI_API void* GetVoidPtr(ImGuiID key) const; // default_val is NULL + IMGUI_API void SetVoidPtr(ImGuiID key, void* val); + + // - Get***Ref() functions finds pair, insert on demand if missing, return pointer. Useful if you intend to do Get+Set. + // - References are only valid until a new value is added to the storage. Calling a Set***() function or a Get***Ref() function invalidates the pointer. + // - A typical use case where this is convenient for quick hacking (e.g. add storage during a live Edit&Continue session if you can't modify existing struct) + // float* pvar = ImGui::GetFloatRef(key); ImGui::SliderFloat("var", pvar, 0, 100.0f); some_var += *pvar; + IMGUI_API int* GetIntRef(ImGuiID key, int default_val = 0); + IMGUI_API bool* GetBoolRef(ImGuiID key, bool default_val = false); + IMGUI_API float* GetFloatRef(ImGuiID key, float default_val = 0.0f); + IMGUI_API void** GetVoidPtrRef(ImGuiID key, void* default_val = NULL); + + // Use on your own storage if you know only integer are being stored (open/close all tree nodes) + IMGUI_API void SetAllInt(int val); + + // For quicker full rebuild of a storage (instead of an incremental one), you may add all your contents and then sort once. + IMGUI_API void BuildSortByKey(); +}; + +// Helper: Manually clip large list of items. +// If you have lots evenly spaced items and you have random access to the list, you can perform coarse +// clipping based on visibility to only submit items that are in view. +// The clipper calculates the range of visible items and advance the cursor to compensate for the non-visible items we have skipped. +// (Dear ImGui already clip items based on their bounds but: it needs to first layout the item to do so, and generally +// fetching/submitting your own data incurs additional cost. Coarse clipping using ImGuiListClipper allows you to easily +// scale using lists with tens of thousands of items without a problem) +// Usage: +// ImGuiListClipper clipper; +// clipper.Begin(1000); // We have 1000 elements, evenly spaced. +// while (clipper.Step()) +// for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) +// ImGui::Text("line number %d", i); +// Generally what happens is: +// - Clipper lets you process the first element (DisplayStart = 0, DisplayEnd = 1) regardless of it being visible or not. +// - User code submit that one element. +// - Clipper can measure the height of the first element +// - Clipper calculate the actual range of elements to display based on the current clipping rectangle, position the cursor before the first visible element. +// - User code submit visible elements. +// - The clipper also handles various subtleties related to keyboard/gamepad navigation, wrapping etc. +struct ImGuiListClipper +{ + ImGuiContext* Ctx; // Parent UI context + int DisplayStart; // First item to display, updated by each call to Step() + int DisplayEnd; // End of items to display (exclusive) + int ItemsCount; // [Internal] Number of items + float ItemsHeight; // [Internal] Height of item after a first step and item submission can calculate it + float StartPosY; // [Internal] Cursor position at the time of Begin() or after table frozen rows are all processed + void* TempData; // [Internal] Internal data + + // items_count: Use INT_MAX if you don't know how many items you have (in which case the cursor won't be advanced in the final step) + // items_height: Use -1.0f to be calculated automatically on first step. Otherwise pass in the distance between your items, typically GetTextLineHeightWithSpacing() or GetFrameHeightWithSpacing(). + IMGUI_API ImGuiListClipper(); + IMGUI_API ~ImGuiListClipper(); + IMGUI_API void Begin(int items_count, float items_height = -1.0f); + IMGUI_API void End(); // Automatically called on the last call of Step() that returns false. + IMGUI_API bool Step(); // Call until it returns false. The DisplayStart/DisplayEnd fields will be set and you can process/draw those items. + + // Call IncludeRangeByIndices() *BEFORE* first call to Step() if you need a range of items to not be clipped, regardless of their visibility. + // (Due to alignment / padding of certain items it is possible that an extra item may be included on either end of the display range). + IMGUI_API void IncludeRangeByIndices(int item_begin, int item_end); // item_end is exclusive e.g. use (42, 42+1) to make item 42 never clipped. + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + inline void ForceDisplayRangeByIndices(int item_begin, int item_end) { IncludeRangeByIndices(item_begin, item_end); } // [renamed in 1.89.6] + //inline ImGuiListClipper(int items_count, float items_height = -1.0f) { memset(this, 0, sizeof(*this)); ItemsCount = -1; Begin(items_count, items_height); } // [removed in 1.79] +#endif +}; + +// Helpers: ImVec2/ImVec4 operators +// - It is important that we are keeping those disabled by default so they don't leak in user space. +// - This is in order to allow user enabling implicit cast operators between ImVec2/ImVec4 and their own types (using IM_VEC2_CLASS_EXTRA in imconfig.h) +// - You can use '#define IMGUI_DEFINE_MATH_OPERATORS' to import our operators, provided as a courtesy. +#ifdef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS_IMPLEMENTED +IM_MSVC_RUNTIME_CHECKS_OFF +static inline ImVec2 operator*(const ImVec2& lhs, const float rhs) { return ImVec2(lhs.x * rhs, lhs.y * rhs); } +static inline ImVec2 operator/(const ImVec2& lhs, const float rhs) { return ImVec2(lhs.x / rhs, lhs.y / rhs); } +static inline ImVec2 operator+(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x + rhs.x, lhs.y + rhs.y); } +static inline ImVec2 operator-(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x - rhs.x, lhs.y - rhs.y); } +static inline ImVec2 operator*(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); } +static inline ImVec2 operator/(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x / rhs.x, lhs.y / rhs.y); } +static inline ImVec2 operator-(const ImVec2& lhs) { return ImVec2(-lhs.x, -lhs.y); } +static inline ImVec2& operator*=(ImVec2& lhs, const float rhs) { lhs.x *= rhs; lhs.y *= rhs; return lhs; } +static inline ImVec2& operator/=(ImVec2& lhs, const float rhs) { lhs.x /= rhs; lhs.y /= rhs; return lhs; } +static inline ImVec2& operator+=(ImVec2& lhs, const ImVec2& rhs) { lhs.x += rhs.x; lhs.y += rhs.y; return lhs; } +static inline ImVec2& operator-=(ImVec2& lhs, const ImVec2& rhs) { lhs.x -= rhs.x; lhs.y -= rhs.y; return lhs; } +static inline ImVec2& operator*=(ImVec2& lhs, const ImVec2& rhs) { lhs.x *= rhs.x; lhs.y *= rhs.y; return lhs; } +static inline ImVec2& operator/=(ImVec2& lhs, const ImVec2& rhs) { lhs.x /= rhs.x; lhs.y /= rhs.y; return lhs; } +static inline ImVec4 operator+(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z, lhs.w + rhs.w); } +static inline ImVec4 operator-(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z, lhs.w - rhs.w); } +static inline ImVec4 operator*(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x * rhs.x, lhs.y * rhs.y, lhs.z * rhs.z, lhs.w * rhs.w); } +IM_MSVC_RUNTIME_CHECKS_RESTORE +#endif + +// Helpers macros to generate 32-bit encoded colors +// User can declare their own format by #defining the 5 _SHIFT/_MASK macros in their imconfig file. +#ifndef IM_COL32_R_SHIFT +#ifdef IMGUI_USE_BGRA_PACKED_COLOR +#define IM_COL32_R_SHIFT 16 +#define IM_COL32_G_SHIFT 8 +#define IM_COL32_B_SHIFT 0 +#define IM_COL32_A_SHIFT 24 +#define IM_COL32_A_MASK 0xFF000000 +#else +#define IM_COL32_R_SHIFT 0 +#define IM_COL32_G_SHIFT 8 +#define IM_COL32_B_SHIFT 16 +#define IM_COL32_A_SHIFT 24 +#define IM_COL32_A_MASK 0xFF000000 +#endif +#endif +#define IM_COL32(R,G,B,A) (((ImU32)(A)<> IM_COL32_R_SHIFT) & 0xFF) * sc; Value.y = (float)((rgba >> IM_COL32_G_SHIFT) & 0xFF) * sc; Value.z = (float)((rgba >> IM_COL32_B_SHIFT) & 0xFF) * sc; Value.w = (float)((rgba >> IM_COL32_A_SHIFT) & 0xFF) * sc; } + inline operator ImU32() const { return ImGui::ColorConvertFloat4ToU32(Value); } + inline operator ImVec4() const { return Value; } + + // FIXME-OBSOLETE: May need to obsolete/cleanup those helpers. + inline void SetHSV(float h, float s, float v, float a = 1.0f){ ImGui::ColorConvertHSVtoRGB(h, s, v, Value.x, Value.y, Value.z); Value.w = a; } + static ImColor HSV(float h, float s, float v, float a = 1.0f) { float r, g, b; ImGui::ColorConvertHSVtoRGB(h, s, v, r, g, b); return ImColor(r, g, b, a); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Drawing API (ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawListFlags, ImDrawList, ImDrawData) +// Hold a series of drawing commands. The user provides a renderer for ImDrawData which essentially contains an array of ImDrawList. +//----------------------------------------------------------------------------- + +// The maximum line width to bake anti-aliased textures for. Build atlas with ImFontAtlasFlags_NoBakedLines to disable baking. +#ifndef IM_DRAWLIST_TEX_LINES_WIDTH_MAX +#define IM_DRAWLIST_TEX_LINES_WIDTH_MAX (63) +#endif + +// ImDrawCallback: Draw callbacks for advanced uses [configurable type: override in imconfig.h] +// NB: You most likely do NOT need to use draw callbacks just to create your own widget or customized UI rendering, +// you can poke into the draw list for that! Draw callback may be useful for example to: +// A) Change your GPU render state, +// B) render a complex 3D scene inside a UI element without an intermediate texture/render target, etc. +// The expected behavior from your rendering function is 'if (cmd.UserCallback != NULL) { cmd.UserCallback(parent_list, cmd); } else { RenderTriangles() }' +// If you want to override the signature of ImDrawCallback, you can simply use e.g. '#define ImDrawCallback MyDrawCallback' (in imconfig.h) + update rendering backend accordingly. +#ifndef ImDrawCallback +typedef void (*ImDrawCallback)(const ImDrawList* parent_list, const ImDrawCmd* cmd); +#endif + +// Special Draw callback value to request renderer backend to reset the graphics/render state. +// The renderer backend needs to handle this special value, otherwise it will crash trying to call a function at this address. +// This is useful for example if you submitted callbacks which you know have altered the render state and you want it to be restored. +// It is not done by default because they are many perfectly useful way of altering render state for imgui contents (e.g. changing shader/blending settings before an Image call). +#define ImDrawCallback_ResetRenderState (ImDrawCallback)(-1) + +// Typically, 1 command = 1 GPU draw call (unless command is a callback) +// - VtxOffset: When 'io.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset' is enabled, +// this fields allow us to render meshes larger than 64K vertices while keeping 16-bit indices. +// Backends made for <1.71. will typically ignore the VtxOffset fields. +// - The ClipRect/TextureId/VtxOffset fields must be contiguous as we memcmp() them together (this is asserted for). +struct ImDrawCmd +{ + ImVec4 ClipRect; // 4*4 // Clipping rectangle (x1, y1, x2, y2). Subtract ImDrawData->DisplayPos to get clipping rectangle in "viewport" coordinates + ImTextureID TextureId; // 4-8 // User-provided texture ID. Set by user in ImfontAtlas::SetTexID() for fonts or passed to Image*() functions. Ignore if never using images or multiple fonts atlas. + unsigned int VtxOffset; // 4 // Start offset in vertex buffer. ImGuiBackendFlags_RendererHasVtxOffset: always 0, otherwise may be >0 to support meshes larger than 64K vertices with 16-bit indices. + unsigned int IdxOffset; // 4 // Start offset in index buffer. + unsigned int ElemCount; // 4 // Number of indices (multiple of 3) to be rendered as triangles. Vertices are stored in the callee ImDrawList's vtx_buffer[] array, indices in idx_buffer[]. + ImDrawCallback UserCallback; // 4-8 // If != NULL, call the function instead of rendering the vertices. clip_rect and texture_id will be set normally. + void* UserCallbackData; // 4-8 // The draw callback code can access this. + + ImDrawCmd() { memset(this, 0, sizeof(*this)); } // Also ensure our padding fields are zeroed + + // Since 1.83: returns ImTextureID associated with this draw call. Warning: DO NOT assume this is always same as 'TextureId' (we will change this function for an upcoming feature) + inline ImTextureID GetTexID() const { return TextureId; } +}; + +// Vertex layout +#ifndef IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT +struct ImDrawVert +{ + ImVec2 pos; + ImVec2 uv; + ImU32 col; +}; +#else +// You can override the vertex format layout by defining IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT in imconfig.h +// The code expect ImVec2 pos (8 bytes), ImVec2 uv (8 bytes), ImU32 col (4 bytes), but you can re-order them or add other fields as needed to simplify integration in your engine. +// The type has to be described within the macro (you can either declare the struct or use a typedef). This is because ImVec2/ImU32 are likely not declared at the time you'd want to set your type up. +// NOTE: IMGUI DOESN'T CLEAR THE STRUCTURE AND DOESN'T CALL A CONSTRUCTOR SO ANY CUSTOM FIELD WILL BE UNINITIALIZED. IF YOU ADD EXTRA FIELDS (SUCH AS A 'Z' COORDINATES) YOU WILL NEED TO CLEAR THEM DURING RENDER OR TO IGNORE THEM. +IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT; +#endif + +// [Internal] For use by ImDrawList +struct ImDrawCmdHeader +{ + ImVec4 ClipRect; + ImTextureID TextureId; + unsigned int VtxOffset; +}; + +// [Internal] For use by ImDrawListSplitter +struct ImDrawChannel +{ + ImVector _CmdBuffer; + ImVector _IdxBuffer; +}; + + +// Split/Merge functions are used to split the draw list into different layers which can be drawn into out of order. +// This is used by the Columns/Tables API, so items of each column can be batched together in a same draw call. +struct ImDrawListSplitter +{ + int _Current; // Current channel number (0) + int _Count; // Number of active channels (1+) + ImVector _Channels; // Draw channels (not resized down so _Count might be < Channels.Size) + + inline ImDrawListSplitter() { memset(this, 0, sizeof(*this)); } + inline ~ImDrawListSplitter() { ClearFreeMemory(); } + inline void Clear() { _Current = 0; _Count = 1; } // Do not clear Channels[] so our allocations are reused next frame + IMGUI_API void ClearFreeMemory(); + IMGUI_API void Split(ImDrawList* draw_list, int count); + IMGUI_API void Merge(ImDrawList* draw_list); + IMGUI_API void SetCurrentChannel(ImDrawList* draw_list, int channel_idx); +}; + +// Flags for ImDrawList functions +// (Legacy: bit 0 must always correspond to ImDrawFlags_Closed to be backward compatible with old API using a bool. Bits 1..3 must be unused) +enum ImDrawFlags_ +{ + ImDrawFlags_None = 0, + ImDrawFlags_Closed = 1 << 0, // PathStroke(), AddPolyline(): specify that shape should be closed (Important: this is always == 1 for legacy reason) + ImDrawFlags_RoundCornersTopLeft = 1 << 4, // AddRect(), AddRectFilled(), PathRect(): enable rounding top-left corner only (when rounding > 0.0f, we default to all corners). Was 0x01. + ImDrawFlags_RoundCornersTopRight = 1 << 5, // AddRect(), AddRectFilled(), PathRect(): enable rounding top-right corner only (when rounding > 0.0f, we default to all corners). Was 0x02. + ImDrawFlags_RoundCornersBottomLeft = 1 << 6, // AddRect(), AddRectFilled(), PathRect(): enable rounding bottom-left corner only (when rounding > 0.0f, we default to all corners). Was 0x04. + ImDrawFlags_RoundCornersBottomRight = 1 << 7, // AddRect(), AddRectFilled(), PathRect(): enable rounding bottom-right corner only (when rounding > 0.0f, we default to all corners). Wax 0x08. + ImDrawFlags_RoundCornersNone = 1 << 8, // AddRect(), AddRectFilled(), PathRect(): disable rounding on all corners (when rounding > 0.0f). This is NOT zero, NOT an implicit flag! + ImDrawFlags_RoundCornersTop = ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight, + ImDrawFlags_RoundCornersBottom = ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight, + ImDrawFlags_RoundCornersLeft = ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersTopLeft, + ImDrawFlags_RoundCornersRight = ImDrawFlags_RoundCornersBottomRight | ImDrawFlags_RoundCornersTopRight, + ImDrawFlags_RoundCornersAll = ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight | ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight, + ImDrawFlags_RoundCornersDefault_ = ImDrawFlags_RoundCornersAll, // Default to ALL corners if none of the _RoundCornersXX flags are specified. + ImDrawFlags_RoundCornersMask_ = ImDrawFlags_RoundCornersAll | ImDrawFlags_RoundCornersNone, +}; + +// Flags for ImDrawList instance. Those are set automatically by ImGui:: functions from ImGuiIO settings, and generally not manipulated directly. +// It is however possible to temporarily alter flags between calls to ImDrawList:: functions. +enum ImDrawListFlags_ +{ + ImDrawListFlags_None = 0, + ImDrawListFlags_AntiAliasedLines = 1 << 0, // Enable anti-aliased lines/borders (*2 the number of triangles for 1.0f wide line or lines thin enough to be drawn using textures, otherwise *3 the number of triangles) + ImDrawListFlags_AntiAliasedLinesUseTex = 1 << 1, // Enable anti-aliased lines/borders using textures when possible. Require backend to render with bilinear filtering (NOT point/nearest filtering). + ImDrawListFlags_AntiAliasedFill = 1 << 2, // Enable anti-aliased edge around filled shapes (rounded rectangles, circles). + ImDrawListFlags_AllowVtxOffset = 1 << 3, // Can emit 'VtxOffset > 0' to allow large meshes. Set when 'ImGuiBackendFlags_RendererHasVtxOffset' is enabled. +}; + +// Draw command list +// This is the low-level list of polygons that ImGui:: functions are filling. At the end of the frame, +// all command lists are passed to your ImGuiIO::RenderDrawListFn function for rendering. +// Each dear imgui window contains its own ImDrawList. You can use ImGui::GetWindowDrawList() to +// access the current window draw list and draw custom primitives. +// You can interleave normal ImGui:: calls and adding primitives to the current draw list. +// In single viewport mode, top-left is == GetMainViewport()->Pos (generally 0,0), bottom-right is == GetMainViewport()->Pos+Size (generally io.DisplaySize). +// You are totally free to apply whatever transformation matrix to want to the data (depending on the use of the transformation you may want to apply it to ClipRect as well!) +// Important: Primitives are always added to the list and not culled (culling is done at higher-level by ImGui:: functions), if you use this API a lot consider coarse culling your drawn objects. +struct ImDrawList +{ + // This is what you have to render + ImVector CmdBuffer; // Draw commands. Typically 1 command = 1 GPU draw call, unless the command is a callback. + ImVector IdxBuffer; // Index buffer. Each command consume ImDrawCmd::ElemCount of those + ImVector VtxBuffer; // Vertex buffer. + ImDrawListFlags Flags; // Flags, you may poke into these to adjust anti-aliasing settings per-primitive. + + // [Internal, used while building lists] + unsigned int _VtxCurrentIdx; // [Internal] generally == VtxBuffer.Size unless we are past 64K vertices, in which case this gets reset to 0. + ImDrawListSharedData* _Data; // Pointer to shared draw data (you can use ImGui::GetDrawListSharedData() to get the one from current ImGui context) + const char* _OwnerName; // Pointer to owner window's name for debugging + ImDrawVert* _VtxWritePtr; // [Internal] point within VtxBuffer.Data after each add command (to avoid using the ImVector<> operators too much) + ImDrawIdx* _IdxWritePtr; // [Internal] point within IdxBuffer.Data after each add command (to avoid using the ImVector<> operators too much) + ImVector _ClipRectStack; // [Internal] + ImVector _TextureIdStack; // [Internal] + ImVector _Path; // [Internal] current path building + ImDrawCmdHeader _CmdHeader; // [Internal] template of active commands. Fields should match those of CmdBuffer.back(). + ImDrawListSplitter _Splitter; // [Internal] for channels api (note: prefer using your own persistent instance of ImDrawListSplitter!) + float _FringeScale; // [Internal] anti-alias fringe is scaled by this value, this helps to keep things sharp while zooming at vertex buffer content + + // If you want to create ImDrawList instances, pass them ImGui::GetDrawListSharedData() or create and use your own ImDrawListSharedData (so you can use ImDrawList without ImGui) + ImDrawList(ImDrawListSharedData* shared_data) { memset(this, 0, sizeof(*this)); _Data = shared_data; } + + ~ImDrawList() { _ClearFreeMemory(); } + IMGUI_API void PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect = false); // Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling) + IMGUI_API void PushClipRectFullScreen(); + IMGUI_API void PopClipRect(); + IMGUI_API void PushTextureID(ImTextureID texture_id); + IMGUI_API void PopTextureID(); + inline ImVec2 GetClipRectMin() const { const ImVec4& cr = _ClipRectStack.back(); return ImVec2(cr.x, cr.y); } + inline ImVec2 GetClipRectMax() const { const ImVec4& cr = _ClipRectStack.back(); return ImVec2(cr.z, cr.w); } + + // Primitives + // - Filled shapes must always use clockwise winding order. The anti-aliasing fringe depends on it. Counter-clockwise shapes will have "inward" anti-aliasing. + // - For rectangular primitives, "p_min" and "p_max" represent the upper-left and lower-right corners. + // - For circle primitives, use "num_segments == 0" to automatically calculate tessellation (preferred). + // In older versions (until Dear ImGui 1.77) the AddCircle functions defaulted to num_segments == 12. + // In future versions we will use textures to provide cheaper and higher-quality circles. + // Use AddNgon() and AddNgonFilled() functions if you need to guarantee a specific number of sides. + IMGUI_API void AddLine(const ImVec2& p1, const ImVec2& p2, ImU32 col, float thickness = 1.0f); + IMGUI_API void AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, ImDrawFlags flags = 0, float thickness = 1.0f); // a: upper-left, b: lower-right (== upper-left + size) + IMGUI_API void AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, ImDrawFlags flags = 0); // a: upper-left, b: lower-right (== upper-left + size) + IMGUI_API void AddRectFilledMultiColor(const ImVec2& p_min, const ImVec2& p_max, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left); + IMGUI_API void AddQuad(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness = 1.0f); + IMGUI_API void AddQuadFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col); + IMGUI_API void AddTriangle(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness = 1.0f); + IMGUI_API void AddTriangleFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col); + IMGUI_API void AddCircle(const ImVec2& center, float radius, ImU32 col, int num_segments = 0, float thickness = 1.0f); + IMGUI_API void AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments = 0); + IMGUI_API void AddNgon(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness = 1.0f); + IMGUI_API void AddNgonFilled(const ImVec2& center, float radius, ImU32 col, int num_segments); + IMGUI_API void AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL); + IMGUI_API void AddText(const ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL, float wrap_width = 0.0f, const ImVec4* cpu_fine_clip_rect = NULL); + IMGUI_API void AddPolyline(const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags, float thickness); + IMGUI_API void AddConvexPolyFilled(const ImVec2* points, int num_points, ImU32 col); + IMGUI_API void AddBezierCubic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness, int num_segments = 0); // Cubic Bezier (4 control points) + IMGUI_API void AddBezierQuadratic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness, int num_segments = 0); // Quadratic Bezier (3 control points) + + // Image primitives + // - Read FAQ to understand what ImTextureID is. + // - "p_min" and "p_max" represent the upper-left and lower-right corners of the rectangle. + // - "uv_min" and "uv_max" represent the normalized texture coordinates to use for those corners. Using (0,0)->(1,1) texture coordinates will generally display the entire texture. + IMGUI_API void AddImage(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min = ImVec2(0, 0), const ImVec2& uv_max = ImVec2(1, 1), ImU32 col = IM_COL32_WHITE); + IMGUI_API void AddImageQuad(ImTextureID user_texture_id, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& uv1 = ImVec2(0, 0), const ImVec2& uv2 = ImVec2(1, 0), const ImVec2& uv3 = ImVec2(1, 1), const ImVec2& uv4 = ImVec2(0, 1), ImU32 col = IM_COL32_WHITE); + IMGUI_API void AddImageRounded(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float rounding, ImDrawFlags flags = 0); + + // Stateful path API, add points then finish with PathFillConvex() or PathStroke() + // - Filled shapes must always use clockwise winding order. The anti-aliasing fringe depends on it. Counter-clockwise shapes will have "inward" anti-aliasing. + inline void PathClear() { _Path.Size = 0; } + inline void PathLineTo(const ImVec2& pos) { _Path.push_back(pos); } + inline void PathLineToMergeDuplicate(const ImVec2& pos) { if (_Path.Size == 0 || memcmp(&_Path.Data[_Path.Size - 1], &pos, 8) != 0) _Path.push_back(pos); } + inline void PathFillConvex(ImU32 col) { AddConvexPolyFilled(_Path.Data, _Path.Size, col); _Path.Size = 0; } + inline void PathStroke(ImU32 col, ImDrawFlags flags = 0, float thickness = 1.0f) { AddPolyline(_Path.Data, _Path.Size, col, flags, thickness); _Path.Size = 0; } + IMGUI_API void PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments = 0); + IMGUI_API void PathArcToFast(const ImVec2& center, float radius, int a_min_of_12, int a_max_of_12); // Use precomputed angles for a 12 steps circle + IMGUI_API void PathBezierCubicCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments = 0); // Cubic Bezier (4 control points) + IMGUI_API void PathBezierQuadraticCurveTo(const ImVec2& p2, const ImVec2& p3, int num_segments = 0); // Quadratic Bezier (3 control points) + IMGUI_API void PathRect(const ImVec2& rect_min, const ImVec2& rect_max, float rounding = 0.0f, ImDrawFlags flags = 0); + + // Advanced + IMGUI_API void AddCallback(ImDrawCallback callback, void* callback_data); // Your rendering function must check for 'UserCallback' in ImDrawCmd and call the function instead of rendering triangles. + IMGUI_API void AddDrawCmd(); // This is useful if you need to forcefully create a new draw call (to allow for dependent rendering / blending). Otherwise primitives are merged into the same draw-call as much as possible + IMGUI_API ImDrawList* CloneOutput() const; // Create a clone of the CmdBuffer/IdxBuffer/VtxBuffer. + + // Advanced: Channels + // - Use to split render into layers. By switching channels to can render out-of-order (e.g. submit FG primitives before BG primitives) + // - Use to minimize draw calls (e.g. if going back-and-forth between multiple clipping rectangles, prefer to append into separate channels then merge at the end) + // - FIXME-OBSOLETE: This API shouldn't have been in ImDrawList in the first place! + // Prefer using your own persistent instance of ImDrawListSplitter as you can stack them. + // Using the ImDrawList::ChannelsXXXX you cannot stack a split over another. + inline void ChannelsSplit(int count) { _Splitter.Split(this, count); } + inline void ChannelsMerge() { _Splitter.Merge(this); } + inline void ChannelsSetCurrent(int n) { _Splitter.SetCurrentChannel(this, n); } + + // Advanced: Primitives allocations + // - We render triangles (three vertices) + // - All primitives needs to be reserved via PrimReserve() beforehand. + IMGUI_API void PrimReserve(int idx_count, int vtx_count); + IMGUI_API void PrimUnreserve(int idx_count, int vtx_count); + IMGUI_API void PrimRect(const ImVec2& a, const ImVec2& b, ImU32 col); // Axis aligned rectangle (composed of two triangles) + IMGUI_API void PrimRectUV(const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, ImU32 col); + IMGUI_API void PrimQuadUV(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, const ImVec2& uv_a, const ImVec2& uv_b, const ImVec2& uv_c, const ImVec2& uv_d, ImU32 col); + inline void PrimWriteVtx(const ImVec2& pos, const ImVec2& uv, ImU32 col) { _VtxWritePtr->pos = pos; _VtxWritePtr->uv = uv; _VtxWritePtr->col = col; _VtxWritePtr++; _VtxCurrentIdx++; } + inline void PrimWriteIdx(ImDrawIdx idx) { *_IdxWritePtr = idx; _IdxWritePtr++; } + inline void PrimVtx(const ImVec2& pos, const ImVec2& uv, ImU32 col) { PrimWriteIdx((ImDrawIdx)_VtxCurrentIdx); PrimWriteVtx(pos, uv, col); } // Write vertex with unique index + + // Obsolete names + //inline void AddBezierCurve(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness, int num_segments = 0) { AddBezierCubic(p1, p2, p3, p4, col, thickness, num_segments); } // OBSOLETED in 1.80 (Jan 2021) + //inline void PathBezierCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments = 0) { PathBezierCubicCurveTo(p2, p3, p4, num_segments); } // OBSOLETED in 1.80 (Jan 2021) + + // [Internal helpers] + IMGUI_API void _ResetForNewFrame(); + IMGUI_API void _ClearFreeMemory(); + IMGUI_API void _PopUnusedDrawCmd(); + IMGUI_API void _TryMergeDrawCmds(); + IMGUI_API void _OnChangedClipRect(); + IMGUI_API void _OnChangedTextureID(); + IMGUI_API void _OnChangedVtxOffset(); + IMGUI_API int _CalcCircleAutoSegmentCount(float radius) const; + IMGUI_API void _PathArcToFastEx(const ImVec2& center, float radius, int a_min_sample, int a_max_sample, int a_step); + IMGUI_API void _PathArcToN(const ImVec2& center, float radius, float a_min, float a_max, int num_segments); +}; + +// All draw data to render a Dear ImGui frame +// (NB: the style and the naming convention here is a little inconsistent, we currently preserve them for backward compatibility purpose, +// as this is one of the oldest structure exposed by the library! Basically, ImDrawList == CmdList) +struct ImDrawData +{ + bool Valid; // Only valid after Render() is called and before the next NewFrame() is called. + int CmdListsCount; // Number of ImDrawList* to render + int TotalIdxCount; // For convenience, sum of all ImDrawList's IdxBuffer.Size + int TotalVtxCount; // For convenience, sum of all ImDrawList's VtxBuffer.Size + ImDrawList** CmdLists; // Array of ImDrawList* to render. The ImDrawList are owned by ImGuiContext and only pointed to from here. + ImVec2 DisplayPos; // Top-left position of the viewport to render (== top-left of the orthogonal projection matrix to use) (== GetMainViewport()->Pos for the main viewport, == (0.0) in most single-viewport applications) + ImVec2 DisplaySize; // Size of the viewport to render (== GetMainViewport()->Size for the main viewport, == io.DisplaySize in most single-viewport applications) + ImVec2 FramebufferScale; // Amount of pixels for each unit of DisplaySize. Based on io.DisplayFramebufferScale. Generally (1,1) on normal display, (2,2) on OSX with Retina display. + ImGuiViewport* OwnerViewport; // Viewport carrying the ImDrawData instance, might be of use to the renderer (generally not). + + // Functions + ImDrawData() { Clear(); } + void Clear() { memset(this, 0, sizeof(*this)); } // The ImDrawList are owned by ImGuiContext! + IMGUI_API void DeIndexAllBuffers(); // Helper to convert all buffers from indexed to non-indexed, in case you cannot render indexed. Note: this is slow and most likely a waste of resources. Always prefer indexed rendering! + IMGUI_API void ScaleClipRects(const ImVec2& fb_scale); // Helper to scale the ClipRect field of each ImDrawCmd. Use if your final output buffer is at a different scale than Dear ImGui expects, or if there is a difference between your window resolution and framebuffer resolution. +}; + +//----------------------------------------------------------------------------- +// [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontAtlasFlags, ImFontAtlas, ImFontGlyphRangesBuilder, ImFont) +//----------------------------------------------------------------------------- + +struct ImFontConfig +{ + void* FontData; // // TTF/OTF data + int FontDataSize; // // TTF/OTF data size + bool FontDataOwnedByAtlas; // true // TTF/OTF data ownership taken by the container ImFontAtlas (will delete memory itself). + int FontNo; // 0 // Index of font within TTF/OTF file + float SizePixels; // // Size in pixels for rasterizer (more or less maps to the resulting font height). + int OversampleH; // 3 // Rasterize at higher quality for sub-pixel positioning. Note the difference between 2 and 3 is minimal so you can reduce this to 2 to save memory. Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details. + int OversampleV; // 1 // Rasterize at higher quality for sub-pixel positioning. This is not really useful as we don't use sub-pixel positions on the Y axis. + bool PixelSnapH; // false // Align every glyph to pixel boundary. Useful e.g. if you are merging a non-pixel aligned font with the default font. If enabled, you can set OversampleH/V to 1. + ImVec2 GlyphExtraSpacing; // 0, 0 // Extra spacing (in pixels) between glyphs. Only X axis is supported for now. + ImVec2 GlyphOffset; // 0, 0 // Offset all glyphs from this font input. + const ImWchar* GlyphRanges; // NULL // THE ARRAY DATA NEEDS TO PERSIST AS LONG AS THE FONT IS ALIVE. Pointer to a user-provided list of Unicode range (2 value per range, values are inclusive, zero-terminated list). + float GlyphMinAdvanceX; // 0 // Minimum AdvanceX for glyphs, set Min to align font icons, set both Min/Max to enforce mono-space font + float GlyphMaxAdvanceX; // FLT_MAX // Maximum AdvanceX for glyphs + bool MergeMode; // false // Merge into previous ImFont, so you can combine multiple inputs font into one ImFont (e.g. ASCII font + icons + Japanese glyphs). You may want to use GlyphOffset.y when merge font of different heights. + unsigned int FontBuilderFlags; // 0 // Settings for custom font builder. THIS IS BUILDER IMPLEMENTATION DEPENDENT. Leave as zero if unsure. + float RasterizerMultiply; // 1.0f // Brighten (>1.0f) or darken (<1.0f) font output. Brightening small fonts may be a good workaround to make them more readable. + ImWchar EllipsisChar; // -1 // Explicitly specify unicode codepoint of ellipsis character. When fonts are being merged first specified ellipsis will be used. + + // [Internal] + char Name[40]; // Name (strictly to ease debugging) + ImFont* DstFont; + + IMGUI_API ImFontConfig(); +}; + +// Hold rendering data for one glyph. +// (Note: some language parsers may fail to convert the 31+1 bitfield members, in this case maybe drop store a single u32 or we can rework this) +struct ImFontGlyph +{ + unsigned int Colored : 1; // Flag to indicate glyph is colored and should generally ignore tinting (make it usable with no shift on little-endian as this is used in loops) + unsigned int Visible : 1; // Flag to indicate glyph has no visible pixels (e.g. space). Allow early out when rendering. + unsigned int Codepoint : 30; // 0x0000..0x10FFFF + float AdvanceX; // Distance to next character (= data from font + ImFontConfig::GlyphExtraSpacing.x baked in) + float X0, Y0, X1, Y1; // Glyph corners + float U0, V0, U1, V1; // Texture coordinates +}; + +// Helper to build glyph ranges from text/string data. Feed your application strings/characters to it then call BuildRanges(). +// This is essentially a tightly packed of vector of 64k booleans = 8KB storage. +struct ImFontGlyphRangesBuilder +{ + ImVector UsedChars; // Store 1-bit per Unicode code point (0=unused, 1=used) + + ImFontGlyphRangesBuilder() { Clear(); } + inline void Clear() { int size_in_bytes = (IM_UNICODE_CODEPOINT_MAX + 1) / 8; UsedChars.resize(size_in_bytes / (int)sizeof(ImU32)); memset(UsedChars.Data, 0, (size_t)size_in_bytes); } + inline bool GetBit(size_t n) const { int off = (int)(n >> 5); ImU32 mask = 1u << (n & 31); return (UsedChars[off] & mask) != 0; } // Get bit n in the array + inline void SetBit(size_t n) { int off = (int)(n >> 5); ImU32 mask = 1u << (n & 31); UsedChars[off] |= mask; } // Set bit n in the array + inline void AddChar(ImWchar c) { SetBit(c); } // Add character + IMGUI_API void AddText(const char* text, const char* text_end = NULL); // Add string (each character of the UTF-8 string are added) + IMGUI_API void AddRanges(const ImWchar* ranges); // Add ranges, e.g. builder.AddRanges(ImFontAtlas::GetGlyphRangesDefault()) to force add all of ASCII/Latin+Ext + IMGUI_API void BuildRanges(ImVector* out_ranges); // Output new ranges +}; + +// See ImFontAtlas::AddCustomRectXXX functions. +struct ImFontAtlasCustomRect +{ + unsigned short Width, Height; // Input // Desired rectangle dimension + unsigned short X, Y; // Output // Packed position in Atlas + unsigned int GlyphID; // Input // For custom font glyphs only (ID < 0x110000) + float GlyphAdvanceX; // Input // For custom font glyphs only: glyph xadvance + ImVec2 GlyphOffset; // Input // For custom font glyphs only: glyph display offset + ImFont* Font; // Input // For custom font glyphs only: target font + ImFontAtlasCustomRect() { Width = Height = 0; X = Y = 0xFFFF; GlyphID = 0; GlyphAdvanceX = 0.0f; GlyphOffset = ImVec2(0, 0); Font = NULL; } + bool IsPacked() const { return X != 0xFFFF; } +}; + +// Flags for ImFontAtlas build +enum ImFontAtlasFlags_ +{ + ImFontAtlasFlags_None = 0, + ImFontAtlasFlags_NoPowerOfTwoHeight = 1 << 0, // Don't round the height to next power of two + ImFontAtlasFlags_NoMouseCursors = 1 << 1, // Don't build software mouse cursors into the atlas (save a little texture memory) + ImFontAtlasFlags_NoBakedLines = 1 << 2, // Don't build thick line textures into the atlas (save a little texture memory, allow support for point/nearest filtering). The AntiAliasedLinesUseTex features uses them, otherwise they will be rendered using polygons (more expensive for CPU/GPU). +}; + +// Load and rasterize multiple TTF/OTF fonts into a same texture. The font atlas will build a single texture holding: +// - One or more fonts. +// - Custom graphics data needed to render the shapes needed by Dear ImGui. +// - Mouse cursor shapes for software cursor rendering (unless setting 'Flags |= ImFontAtlasFlags_NoMouseCursors' in the font atlas). +// It is the user-code responsibility to setup/build the atlas, then upload the pixel data into a texture accessible by your graphics api. +// - Optionally, call any of the AddFont*** functions. If you don't call any, the default font embedded in the code will be loaded for you. +// - Call GetTexDataAsAlpha8() or GetTexDataAsRGBA32() to build and retrieve pixels data. +// - Upload the pixels data into a texture within your graphics system (see imgui_impl_xxxx.cpp examples) +// - Call SetTexID(my_tex_id); and pass the pointer/identifier to your texture in a format natural to your graphics API. +// This value will be passed back to you during rendering to identify the texture. Read FAQ entry about ImTextureID for more details. +// Common pitfalls: +// - If you pass a 'glyph_ranges' array to AddFont*** functions, you need to make sure that your array persist up until the +// atlas is build (when calling GetTexData*** or Build()). We only copy the pointer, not the data. +// - Important: By default, AddFontFromMemoryTTF() takes ownership of the data. Even though we are not writing to it, we will free the pointer on destruction. +// You can set font_cfg->FontDataOwnedByAtlas=false to keep ownership of your data and it won't be freed, +// - Even though many functions are suffixed with "TTF", OTF data is supported just as well. +// - This is an old API and it is currently awkward for those and various other reasons! We will address them in the future! +struct ImFontAtlas +{ + IMGUI_API ImFontAtlas(); + IMGUI_API ~ImFontAtlas(); + IMGUI_API ImFont* AddFont(const ImFontConfig* font_cfg); + IMGUI_API ImFont* AddFontDefault(const ImFontConfig* font_cfg = NULL); + IMGUI_API ImFont* AddFontFromFileTTF(const char* filename, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); + IMGUI_API ImFont* AddFontFromMemoryTTF(void* font_data, int font_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // Note: Transfer ownership of 'ttf_data' to ImFontAtlas! Will be deleted after destruction of the atlas. Set font_cfg->FontDataOwnedByAtlas=false to keep ownership of your data and it won't be freed. + IMGUI_API ImFont* AddFontFromMemoryCompressedTTF(const void* compressed_font_data, int compressed_font_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data' still owned by caller. Compress with binary_to_compressed_c.cpp. + IMGUI_API ImFont* AddFontFromMemoryCompressedBase85TTF(const char* compressed_font_data_base85, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data_base85' still owned by caller. Compress with binary_to_compressed_c.cpp with -base85 parameter. + IMGUI_API void ClearInputData(); // Clear input data (all ImFontConfig structures including sizes, TTF data, glyph ranges, etc.) = all the data used to build the texture and fonts. + IMGUI_API void ClearTexData(); // Clear output texture data (CPU side). Saves RAM once the texture has been copied to graphics memory. + IMGUI_API void ClearFonts(); // Clear output font data (glyphs storage, UV coordinates). + IMGUI_API void Clear(); // Clear all input and output. + + // Build atlas, retrieve pixel data. + // User is in charge of copying the pixels into graphics memory (e.g. create a texture with your engine). Then store your texture handle with SetTexID(). + // The pitch is always = Width * BytesPerPixels (1 or 4) + // Building in RGBA32 format is provided for convenience and compatibility, but note that unless you manually manipulate or copy color data into + // the texture (e.g. when using the AddCustomRect*** api), then the RGB pixels emitted will always be white (~75% of memory/bandwidth waste. + IMGUI_API bool Build(); // Build pixels data. This is called automatically for you by the GetTexData*** functions. + IMGUI_API void GetTexDataAsAlpha8(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel = NULL); // 1 byte per-pixel + IMGUI_API void GetTexDataAsRGBA32(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel = NULL); // 4 bytes-per-pixel + bool IsBuilt() const { return Fonts.Size > 0 && TexReady; } // Bit ambiguous: used to detect when user didn't build texture but effectively we should check TexID != 0 except that would be backend dependent... + void SetTexID(ImTextureID id) { TexID = id; } + + //------------------------------------------- + // Glyph Ranges + //------------------------------------------- + + // Helpers to retrieve list of common Unicode ranges (2 value per range, values are inclusive, zero-terminated list) + // NB: Make sure that your string are UTF-8 and NOT in your local code page. + // Read https://github.com/ocornut/imgui/blob/master/docs/FONTS.md/#about-utf-8-encoding for details. + // NB: Consider using ImFontGlyphRangesBuilder to build glyph ranges from textual data. + IMGUI_API const ImWchar* GetGlyphRangesDefault(); // Basic Latin, Extended Latin + IMGUI_API const ImWchar* GetGlyphRangesGreek(); // Default + Greek and Coptic + IMGUI_API const ImWchar* GetGlyphRangesKorean(); // Default + Korean characters + IMGUI_API const ImWchar* GetGlyphRangesJapanese(); // Default + Hiragana, Katakana, Half-Width, Selection of 2999 Ideographs + IMGUI_API const ImWchar* GetGlyphRangesChineseFull(); // Default + Half-Width + Japanese Hiragana/Katakana + full set of about 21000 CJK Unified Ideographs + IMGUI_API const ImWchar* GetGlyphRangesChineseSimplifiedCommon();// Default + Half-Width + Japanese Hiragana/Katakana + set of 2500 CJK Unified Ideographs for common simplified Chinese + IMGUI_API const ImWchar* GetGlyphRangesCyrillic(); // Default + about 400 Cyrillic characters + IMGUI_API const ImWchar* GetGlyphRangesThai(); // Default + Thai characters + IMGUI_API const ImWchar* GetGlyphRangesVietnamese(); // Default + Vietnamese characters + + //------------------------------------------- + // [BETA] Custom Rectangles/Glyphs API + //------------------------------------------- + + // You can request arbitrary rectangles to be packed into the atlas, for your own purposes. + // - After calling Build(), you can query the rectangle position and render your pixels. + // - If you render colored output, set 'atlas->TexPixelsUseColors = true' as this may help some backends decide of prefered texture format. + // - You can also request your rectangles to be mapped as font glyph (given a font + Unicode point), + // so you can render e.g. custom colorful icons and use them as regular glyphs. + // - Read docs/FONTS.md for more details about using colorful icons. + // - Note: this API may be redesigned later in order to support multi-monitor varying DPI settings. + IMGUI_API int AddCustomRectRegular(int width, int height); + IMGUI_API int AddCustomRectFontGlyph(ImFont* font, ImWchar id, int width, int height, float advance_x, const ImVec2& offset = ImVec2(0, 0)); + ImFontAtlasCustomRect* GetCustomRectByIndex(int index) { IM_ASSERT(index >= 0); return &CustomRects[index]; } + + // [Internal] + IMGUI_API void CalcCustomRectUV(const ImFontAtlasCustomRect* rect, ImVec2* out_uv_min, ImVec2* out_uv_max) const; + IMGUI_API bool GetMouseCursorTexData(ImGuiMouseCursor cursor, ImVec2* out_offset, ImVec2* out_size, ImVec2 out_uv_border[2], ImVec2 out_uv_fill[2]); + + //------------------------------------------- + // Members + //------------------------------------------- + + ImFontAtlasFlags Flags; // Build flags (see ImFontAtlasFlags_) + ImTextureID TexID; // User data to refer to the texture once it has been uploaded to user's graphic systems. It is passed back to you during rendering via the ImDrawCmd structure. + int TexDesiredWidth; // Texture width desired by user before Build(). Must be a power-of-two. If have many glyphs your graphics API have texture size restrictions you may want to increase texture width to decrease height. + int TexGlyphPadding; // Padding between glyphs within texture in pixels. Defaults to 1. If your rendering method doesn't rely on bilinear filtering you may set this to 0 (will also need to set AntiAliasedLinesUseTex = false). + bool Locked; // Marked as Locked by ImGui::NewFrame() so attempt to modify the atlas will assert. + void* UserData; // Store your own atlas related user-data (if e.g. you have multiple font atlas). + + // [Internal] + // NB: Access texture data via GetTexData*() calls! Which will setup a default font for you. + bool TexReady; // Set when texture was built matching current font input + bool TexPixelsUseColors; // Tell whether our texture data is known to use colors (rather than just alpha channel), in order to help backend select a format. + unsigned char* TexPixelsAlpha8; // 1 component per pixel, each component is unsigned 8-bit. Total size = TexWidth * TexHeight + unsigned int* TexPixelsRGBA32; // 4 component per pixel, each component is unsigned 8-bit. Total size = TexWidth * TexHeight * 4 + int TexWidth; // Texture width calculated during Build(). + int TexHeight; // Texture height calculated during Build(). + ImVec2 TexUvScale; // = (1.0f/TexWidth, 1.0f/TexHeight) + ImVec2 TexUvWhitePixel; // Texture coordinates to a white pixel + ImVector Fonts; // Hold all the fonts returned by AddFont*. Fonts[0] is the default font upon calling ImGui::NewFrame(), use ImGui::PushFont()/PopFont() to change the current font. + ImVector CustomRects; // Rectangles for packing custom texture data into the atlas. + ImVector ConfigData; // Configuration data + ImVec4 TexUvLines[IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1]; // UVs for baked anti-aliased lines + + // [Internal] Font builder + const ImFontBuilderIO* FontBuilderIO; // Opaque interface to a font builder (default to stb_truetype, can be changed to use FreeType by defining IMGUI_ENABLE_FREETYPE). + unsigned int FontBuilderFlags; // Shared flags (for all fonts) for custom font builder. THIS IS BUILD IMPLEMENTATION DEPENDENT. Per-font override is also available in ImFontConfig. + + // [Internal] Packing data + int PackIdMouseCursors; // Custom texture rectangle ID for white pixel and mouse cursors + int PackIdLines; // Custom texture rectangle ID for baked anti-aliased lines + + // [Obsolete] + //typedef ImFontAtlasCustomRect CustomRect; // OBSOLETED in 1.72+ + //typedef ImFontGlyphRangesBuilder GlyphRangesBuilder; // OBSOLETED in 1.67+ +}; + +// Font runtime data and rendering +// ImFontAtlas automatically loads a default embedded font for you when you call GetTexDataAsAlpha8() or GetTexDataAsRGBA32(). +struct ImFont +{ + // Members: Hot ~20/24 bytes (for CalcTextSize) + ImVector IndexAdvanceX; // 12-16 // out // // Sparse. Glyphs->AdvanceX in a directly indexable way (cache-friendly for CalcTextSize functions which only this this info, and are often bottleneck in large UI). + float FallbackAdvanceX; // 4 // out // = FallbackGlyph->AdvanceX + float FontSize; // 4 // in // // Height of characters/line, set during loading (don't change after loading) + + // Members: Hot ~28/40 bytes (for CalcTextSize + render loop) + ImVector IndexLookup; // 12-16 // out // // Sparse. Index glyphs by Unicode code-point. + ImVector Glyphs; // 12-16 // out // // All glyphs. + const ImFontGlyph* FallbackGlyph; // 4-8 // out // = FindGlyph(FontFallbackChar) + + // Members: Cold ~32/40 bytes + ImFontAtlas* ContainerAtlas; // 4-8 // out // // What we has been loaded into + const ImFontConfig* ConfigData; // 4-8 // in // // Pointer within ContainerAtlas->ConfigData + short ConfigDataCount; // 2 // in // ~ 1 // Number of ImFontConfig involved in creating this font. Bigger than 1 when merging multiple font sources into one ImFont. + ImWchar FallbackChar; // 2 // out // = FFFD/'?' // Character used if a glyph isn't found. + ImWchar EllipsisChar; // 2 // out // = '...'/'.'// Character used for ellipsis rendering. + short EllipsisCharCount; // 1 // out // 1 or 3 + float EllipsisWidth; // 4 // out // Width + float EllipsisCharStep; // 4 // out // Step between characters when EllipsisCount > 0 + bool DirtyLookupTables; // 1 // out // + float Scale; // 4 // in // = 1.f // Base font scale, multiplied by the per-window font scale which you can adjust with SetWindowFontScale() + float Ascent, Descent; // 4+4 // out // // Ascent: distance from top to bottom of e.g. 'A' [0..FontSize] + int MetricsTotalSurface;// 4 // out // // Total surface in pixels to get an idea of the font rasterization/texture cost (not exact, we approximate the cost of padding between glyphs) + ImU8 Used4kPagesMap[(IM_UNICODE_CODEPOINT_MAX+1)/4096/8]; // 2 bytes if ImWchar=ImWchar16, 34 bytes if ImWchar==ImWchar32. Store 1-bit for each block of 4K codepoints that has one active glyph. This is mainly used to facilitate iterations across all used codepoints. + + // Methods + IMGUI_API ImFont(); + IMGUI_API ~ImFont(); + IMGUI_API const ImFontGlyph*FindGlyph(ImWchar c) const; + IMGUI_API const ImFontGlyph*FindGlyphNoFallback(ImWchar c) const; + float GetCharAdvance(ImWchar c) const { return ((int)c < IndexAdvanceX.Size) ? IndexAdvanceX[(int)c] : FallbackAdvanceX; } + bool IsLoaded() const { return ContainerAtlas != NULL; } + const char* GetDebugName() const { return ConfigData ? ConfigData->Name : ""; } + + // 'max_width' stops rendering after a certain width (could be turned into a 2d size). FLT_MAX to disable. + // 'wrap_width' enable automatic word-wrapping across multiple lines to fit into given width. 0.0f to disable. + IMGUI_API ImVec2 CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end = NULL, const char** remaining = NULL) const; // utf8 + IMGUI_API const char* CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width) const; + IMGUI_API void RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, ImWchar c) const; + IMGUI_API void RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width = 0.0f, bool cpu_fine_clip = false) const; + + // [Internal] Don't use! + IMGUI_API void BuildLookupTable(); + IMGUI_API void ClearOutputData(); + IMGUI_API void GrowIndex(int new_size); + IMGUI_API void AddGlyph(const ImFontConfig* src_cfg, ImWchar c, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float advance_x); + IMGUI_API void AddRemapChar(ImWchar dst, ImWchar src, bool overwrite_dst = true); // Makes 'dst' character/glyph points to 'src' character/glyph. Currently needs to be called AFTER fonts have been built. + IMGUI_API void SetGlyphVisible(ImWchar c, bool visible); + IMGUI_API bool IsGlyphRangeUnused(unsigned int c_begin, unsigned int c_last); +}; + +//----------------------------------------------------------------------------- +// [SECTION] Viewports +//----------------------------------------------------------------------------- + +// Flags stored in ImGuiViewport::Flags, giving indications to the platform backends. +enum ImGuiViewportFlags_ +{ + ImGuiViewportFlags_None = 0, + ImGuiViewportFlags_IsPlatformWindow = 1 << 0, // Represent a Platform Window + ImGuiViewportFlags_IsPlatformMonitor = 1 << 1, // Represent a Platform Monitor (unused yet) + ImGuiViewportFlags_OwnedByApp = 1 << 2, // Platform Window: Was created/managed by the user application? (rather than our backend) + ImGuiViewportFlags_NoDecoration = 1 << 3, // Platform Window: Disable platform decorations: title bar, borders, etc. (generally set all windows, but if ImGuiConfigFlags_ViewportsDecoration is set we only set this on popups/tooltips) + ImGuiViewportFlags_NoTaskBarIcon = 1 << 4, // Platform Window: Disable platform task bar icon (generally set on popups/tooltips, or all windows if ImGuiConfigFlags_ViewportsNoTaskBarIcon is set) + ImGuiViewportFlags_NoFocusOnAppearing = 1 << 5, // Platform Window: Don't take focus when created. + ImGuiViewportFlags_NoFocusOnClick = 1 << 6, // Platform Window: Don't take focus when clicked on. + ImGuiViewportFlags_NoInputs = 1 << 7, // Platform Window: Make mouse pass through so we can drag this window while peaking behind it. + ImGuiViewportFlags_NoRendererClear = 1 << 8, // Platform Window: Renderer doesn't need to clear the framebuffer ahead (because we will fill it entirely). + ImGuiViewportFlags_NoAutoMerge = 1 << 9, // Platform Window: Avoid merging this window into another host window. This can only be set via ImGuiWindowClass viewport flags override (because we need to now ahead if we are going to create a viewport in the first place!). + ImGuiViewportFlags_TopMost = 1 << 10, // Platform Window: Display on top (for tooltips only). + ImGuiViewportFlags_CanHostOtherWindows = 1 << 11, // Viewport can host multiple imgui windows (secondary viewports are associated to a single window). // FIXME: In practice there's still probably code making the assumption that this is always and only on the MainViewport. Will fix once we add support for "no main viewport". + + // Output status flags (from Platform) + ImGuiViewportFlags_IsMinimized = 1 << 12, // Platform Window: Window is minimized, can skip render. When minimized we tend to avoid using the viewport pos/size for clipping window or testing if they are contained in the viewport. + ImGuiViewportFlags_IsFocused = 1 << 13, // Platform Window: Window is focused (last call to Platform_GetWindowFocus() returned true) +}; + +// - Currently represents the Platform Window created by the application which is hosting our Dear ImGui windows. +// - With multi-viewport enabled, we extend this concept to have multiple active viewports. +// - In the future we will extend this concept further to also represent Platform Monitor and support a "no main platform window" operation mode. +// - About Main Area vs Work Area: +// - Main Area = entire viewport. +// - Work Area = entire viewport minus sections used by main menu bars (for platform windows), or by task bar (for platform monitor). +// - Windows are generally trying to stay within the Work Area of their host viewport. +struct ImGuiViewport +{ + ImGuiID ID; // Unique identifier for the viewport + ImGuiViewportFlags Flags; // See ImGuiViewportFlags_ + ImVec2 Pos; // Main Area: Position of the viewport (Dear ImGui coordinates are the same as OS desktop/native coordinates) + ImVec2 Size; // Main Area: Size of the viewport. + ImVec2 WorkPos; // Work Area: Position of the viewport minus task bars, menus bars, status bars (>= Pos) + ImVec2 WorkSize; // Work Area: Size of the viewport minus task bars, menu bars, status bars (<= Size) + float DpiScale; // 1.0f = 96 DPI = No extra scale. + ImGuiID ParentViewportId; // (Advanced) 0: no parent. Instruct the platform backend to setup a parent/child relationship between platform windows. + ImDrawData* DrawData; // The ImDrawData corresponding to this viewport. Valid after Render() and until the next call to NewFrame(). + + // Platform/Backend Dependent Data + // Our design separate the Renderer and Platform backends to facilitate combining default backends with each others. + // When our create your own backend for a custom engine, it is possible that both Renderer and Platform will be handled + // by the same system and you may not need to use all the UserData/Handle fields. + // The library never uses those fields, they are merely storage to facilitate backend implementation. + void* RendererUserData; // void* to hold custom data structure for the renderer (e.g. swap chain, framebuffers etc.). generally set by your Renderer_CreateWindow function. + void* PlatformUserData; // void* to hold custom data structure for the OS / platform (e.g. windowing info, render context). generally set by your Platform_CreateWindow function. + void* PlatformHandle; // void* for FindViewportByPlatformHandle(). (e.g. suggested to use natural platform handle such as HWND, GLFWWindow*, SDL_Window*) + void* PlatformHandleRaw; // void* to hold lower-level, platform-native window handle (under Win32 this is expected to be a HWND, unused for other platforms), when using an abstraction layer like GLFW or SDL (where PlatformHandle would be a SDL_Window*) + bool PlatformWindowCreated; // Platform window has been created (Platform_CreateWindow() has been called). This is false during the first frame where a viewport is being created. + bool PlatformRequestMove; // Platform window requested move (e.g. window was moved by the OS / host window manager, authoritative position will be OS window position) + bool PlatformRequestResize; // Platform window requested resize (e.g. window was resized by the OS / host window manager, authoritative size will be OS window size) + bool PlatformRequestClose; // Platform window requested closure (e.g. window was moved by the OS / host window manager, e.g. pressing ALT-F4) + + ImGuiViewport() { memset(this, 0, sizeof(*this)); } + ~ImGuiViewport() { IM_ASSERT(PlatformUserData == NULL && RendererUserData == NULL); } + + // Helpers + ImVec2 GetCenter() const { return ImVec2(Pos.x + Size.x * 0.5f, Pos.y + Size.y * 0.5f); } + ImVec2 GetWorkCenter() const { return ImVec2(WorkPos.x + WorkSize.x * 0.5f, WorkPos.y + WorkSize.y * 0.5f); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Platform Dependent Interfaces (for e.g. multi-viewport support) +//----------------------------------------------------------------------------- +// [BETA] (Optional) This is completely optional, for advanced users! +// If you are new to Dear ImGui and trying to integrate it into your engine, you can probably ignore this for now. +// +// This feature allows you to seamlessly drag Dear ImGui windows outside of your application viewport. +// This is achieved by creating new Platform/OS windows on the fly, and rendering into them. +// Dear ImGui manages the viewport structures, and the backend create and maintain one Platform/OS window for each of those viewports. +// +// See Glossary https://github.com/ocornut/imgui/wiki/Glossary for details about some of the terminology. +// See Thread https://github.com/ocornut/imgui/issues/1542 for gifs, news and questions about this evolving feature. +// +// About the coordinates system: +// - When multi-viewports are enabled, all Dear ImGui coordinates become absolute coordinates (same as OS coordinates!) +// - So e.g. ImGui::SetNextWindowPos(ImVec2(0,0)) will position a window relative to your primary monitor! +// - If you want to position windows relative to your main application viewport, use ImGui::GetMainViewport()->Pos as a base position. +// +// Steps to use multi-viewports in your application, when using a default backend from the examples/ folder: +// - Application: Enable feature with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'. +// - Backend: The backend initialization will setup all necessary ImGuiPlatformIO's functions and update monitors info every frame. +// - Application: In your main loop, call ImGui::UpdatePlatformWindows(), ImGui::RenderPlatformWindowsDefault() after EndFrame() or Render(). +// - Application: Fix absolute coordinates used in ImGui::SetWindowPos() or ImGui::SetNextWindowPos() calls. +// +// Steps to use multi-viewports in your application, when using a custom backend: +// - Important: THIS IS NOT EASY TO DO and comes with many subtleties not described here! +// It's also an experimental feature, so some of the requirements may evolve. +// Consider using default backends if you can. Either way, carefully follow and refer to examples/ backends for details. +// - Application: Enable feature with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'. +// - Backend: Hook ImGuiPlatformIO's Platform_* and Renderer_* callbacks (see below). +// Set 'io.BackendFlags |= ImGuiBackendFlags_PlatformHasViewports' and 'io.BackendFlags |= ImGuiBackendFlags_PlatformHasViewports'. +// Update ImGuiPlatformIO's Monitors list every frame. +// Update MousePos every frame, in absolute coordinates. +// - Application: In your main loop, call ImGui::UpdatePlatformWindows(), ImGui::RenderPlatformWindowsDefault() after EndFrame() or Render(). +// You may skip calling RenderPlatformWindowsDefault() if its API is not convenient for your needs. Read comments below. +// - Application: Fix absolute coordinates used in ImGui::SetWindowPos() or ImGui::SetNextWindowPos() calls. +// +// About ImGui::RenderPlatformWindowsDefault(): +// - This function is a mostly a _helper_ for the common-most cases, and to facilitate using default backends. +// - You can check its simple source code to understand what it does. +// It basically iterates secondary viewports and call 4 functions that are setup in ImGuiPlatformIO, if available: +// Platform_RenderWindow(), Renderer_RenderWindow(), Platform_SwapBuffers(), Renderer_SwapBuffers() +// Those functions pointers exists only for the benefit of RenderPlatformWindowsDefault(). +// - If you have very specific rendering needs (e.g. flipping multiple swap-chain simultaneously, unusual sync/threading issues, etc.), +// you may be tempted to ignore RenderPlatformWindowsDefault() and write customized code to perform your renderingg. +// You may decide to setup the platform_io's *RenderWindow and *SwapBuffers pointers and call your functions through those pointers, +// or you may decide to never setup those pointers and call your code directly. They are a convenience, not an obligatory interface. +//----------------------------------------------------------------------------- + +// (Optional) Access via ImGui::GetPlatformIO() +struct ImGuiPlatformIO +{ + //------------------------------------------------------------------ + // Input - Backend interface/functions + Monitor List + //------------------------------------------------------------------ + + // (Optional) Platform functions (e.g. Win32, GLFW, SDL2) + // For reference, the second column shows which function are generally calling the Platform Functions: + // N = ImGui::NewFrame() ~ beginning of the dear imgui frame: read info from platform/OS windows (latest size/position) + // F = ImGui::Begin(), ImGui::EndFrame() ~ during the dear imgui frame + // U = ImGui::UpdatePlatformWindows() ~ after the dear imgui frame: create and update all platform/OS windows + // R = ImGui::RenderPlatformWindowsDefault() ~ render + // D = ImGui::DestroyPlatformWindows() ~ shutdown + // The general idea is that NewFrame() we will read the current Platform/OS state, and UpdatePlatformWindows() will write to it. + // + // The functions are designed so we can mix and match 2 imgui_impl_xxxx files, one for the Platform (~window/input handling), one for Renderer. + // Custom engine backends will often provide both Platform and Renderer interfaces and so may not need to use all functions. + // Platform functions are typically called before their Renderer counterpart, apart from Destroy which are called the other way. + + // Platform function --------------------------------------------------- Called by ----- + void (*Platform_CreateWindow)(ImGuiViewport* vp); // . . U . . // Create a new platform window for the given viewport + void (*Platform_DestroyWindow)(ImGuiViewport* vp); // N . U . D // + void (*Platform_ShowWindow)(ImGuiViewport* vp); // . . U . . // Newly created windows are initially hidden so SetWindowPos/Size/Title can be called on them before showing the window + void (*Platform_SetWindowPos)(ImGuiViewport* vp, ImVec2 pos); // . . U . . // Set platform window position (given the upper-left corner of client area) + ImVec2 (*Platform_GetWindowPos)(ImGuiViewport* vp); // N . . . . // + void (*Platform_SetWindowSize)(ImGuiViewport* vp, ImVec2 size); // . . U . . // Set platform window client area size (ignoring OS decorations such as OS title bar etc.) + ImVec2 (*Platform_GetWindowSize)(ImGuiViewport* vp); // N . . . . // Get platform window client area size + void (*Platform_SetWindowFocus)(ImGuiViewport* vp); // N . . . . // Move window to front and set input focus + bool (*Platform_GetWindowFocus)(ImGuiViewport* vp); // . . U . . // + bool (*Platform_GetWindowMinimized)(ImGuiViewport* vp); // N . . . . // Get platform window minimized state. When minimized, we generally won't attempt to get/set size and contents will be culled more easily + void (*Platform_SetWindowTitle)(ImGuiViewport* vp, const char* str); // . . U . . // Set platform window title (given an UTF-8 string) + void (*Platform_SetWindowAlpha)(ImGuiViewport* vp, float alpha); // . . U . . // (Optional) Setup global transparency (not per-pixel transparency) + void (*Platform_UpdateWindow)(ImGuiViewport* vp); // . . U . . // (Optional) Called by UpdatePlatformWindows(). Optional hook to allow the platform backend from doing general book-keeping every frame. + void (*Platform_RenderWindow)(ImGuiViewport* vp, void* render_arg); // . . . R . // (Optional) Main rendering (platform side! This is often unused, or just setting a "current" context for OpenGL bindings). 'render_arg' is the value passed to RenderPlatformWindowsDefault(). + void (*Platform_SwapBuffers)(ImGuiViewport* vp, void* render_arg); // . . . R . // (Optional) Call Present/SwapBuffers (platform side! This is often unused!). 'render_arg' is the value passed to RenderPlatformWindowsDefault(). + float (*Platform_GetWindowDpiScale)(ImGuiViewport* vp); // N . . . . // (Optional) [BETA] FIXME-DPI: DPI handling: Return DPI scale for this viewport. 1.0f = 96 DPI. + void (*Platform_OnChangedViewport)(ImGuiViewport* vp); // . F . . . // (Optional) [BETA] FIXME-DPI: DPI handling: Called during Begin() every time the viewport we are outputting into changes, so backend has a chance to swap fonts to adjust style. + int (*Platform_CreateVkSurface)(ImGuiViewport* vp, ImU64 vk_inst, const void* vk_allocators, ImU64* out_vk_surface); // (Optional) For a Vulkan Renderer to call into Platform code (since the surface creation needs to tie them both). + + // (Optional) Renderer functions (e.g. DirectX, OpenGL, Vulkan) + void (*Renderer_CreateWindow)(ImGuiViewport* vp); // . . U . . // Create swap chain, frame buffers etc. (called after Platform_CreateWindow) + void (*Renderer_DestroyWindow)(ImGuiViewport* vp); // N . U . D // Destroy swap chain, frame buffers etc. (called before Platform_DestroyWindow) + void (*Renderer_SetWindowSize)(ImGuiViewport* vp, ImVec2 size); // . . U . . // Resize swap chain, frame buffers etc. (called after Platform_SetWindowSize) + void (*Renderer_RenderWindow)(ImGuiViewport* vp, void* render_arg); // . . . R . // (Optional) Clear framebuffer, setup render target, then render the viewport->DrawData. 'render_arg' is the value passed to RenderPlatformWindowsDefault(). + void (*Renderer_SwapBuffers)(ImGuiViewport* vp, void* render_arg); // . . . R . // (Optional) Call Present/SwapBuffers. 'render_arg' is the value passed to RenderPlatformWindowsDefault(). + + // (Optional) Monitor list + // - Updated by: app/backend. Update every frame to dynamically support changing monitor or DPI configuration. + // - Used by: dear imgui to query DPI info, clamp popups/tooltips within same monitor and not have them straddle monitors. + ImVector Monitors; + + //------------------------------------------------------------------ + // Output - List of viewports to render into platform windows + //------------------------------------------------------------------ + + // Viewports list (the list is updated by calling ImGui::EndFrame or ImGui::Render) + // (in the future we will attempt to organize this feature to remove the need for a "main viewport") + ImVector Viewports; // Main viewports, followed by all secondary viewports. + ImGuiPlatformIO() { memset(this, 0, sizeof(*this)); } // Zero clear +}; + +// (Optional) This is required when enabling multi-viewport. Represent the bounds of each connected monitor/display and their DPI. +// We use this information for multiple DPI support + clamping the position of popups and tooltips so they don't straddle multiple monitors. +struct ImGuiPlatformMonitor +{ + ImVec2 MainPos, MainSize; // Coordinates of the area displayed on this monitor (Min = upper left, Max = bottom right) + ImVec2 WorkPos, WorkSize; // Coordinates without task bars / side bars / menu bars. Used to avoid positioning popups/tooltips inside this region. If you don't have this info, please copy the value for MainPos/MainSize. + float DpiScale; // 1.0f = 96 DPI + void* PlatformHandle; // Backend dependant data (e.g. HMONITOR, GLFWmonitor*, SDL Display Index, NSScreen*) + ImGuiPlatformMonitor() { MainPos = MainSize = WorkPos = WorkSize = ImVec2(0, 0); DpiScale = 1.0f; PlatformHandle = NULL; } +}; + +// (Optional) Support for IME (Input Method Editor) via the io.SetPlatformImeDataFn() function. +struct ImGuiPlatformImeData +{ + bool WantVisible; // A widget wants the IME to be visible + ImVec2 InputPos; // Position of the input cursor + float InputLineHeight; // Line height + + ImGuiPlatformImeData() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Obsolete functions and types +// (Will be removed! Read 'API BREAKING CHANGES' section in imgui.cpp for details) +// Please keep your copy of dear imgui up to date! Occasionally set '#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS' in imconfig.h to stay ahead. +//----------------------------------------------------------------------------- + +namespace ImGui +{ +#ifndef IMGUI_DISABLE_OBSOLETE_KEYIO + IMGUI_API ImGuiKey GetKeyIndex(ImGuiKey key); // map ImGuiKey_* values into legacy native key index. == io.KeyMap[key] +#else + static inline ImGuiKey GetKeyIndex(ImGuiKey key) { IM_ASSERT(key >= ImGuiKey_NamedKey_BEGIN && key < ImGuiKey_NamedKey_END && "ImGuiKey and native_index was merged together and native_index is disabled by IMGUI_DISABLE_OBSOLETE_KEYIO. Please switch to ImGuiKey."); return key; } +#endif +} + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +namespace ImGui +{ + // OBSOLETED in 1.89.7 (from June 2023) + IMGUI_API void SetItemAllowOverlap(); // Use SetNextItemAllowOverlap() before item. + // OBSOLETED in 1.89.4 (from March 2023) + static inline void PushAllowKeyboardFocus(bool tab_stop) { PushTabStop(tab_stop); } + static inline void PopAllowKeyboardFocus() { PopTabStop(); } + // OBSOLETED in 1.89 (from August 2022) + IMGUI_API bool ImageButton(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), int frame_padding = -1, const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); // Use new ImageButton() signature (explicit item id, regular FramePadding) + // OBSOLETED in 1.88 (from May 2022) + static inline void CaptureKeyboardFromApp(bool want_capture_keyboard = true) { SetNextFrameWantCaptureKeyboard(want_capture_keyboard); } // Renamed as name was misleading + removed default value. + static inline void CaptureMouseFromApp(bool want_capture_mouse = true) { SetNextFrameWantCaptureMouse(want_capture_mouse); } // Renamed as name was misleading + removed default value. + // OBSOLETED in 1.86 (from November 2021) + IMGUI_API void CalcListClipping(int items_count, float items_height, int* out_items_display_start, int* out_items_display_end); // Calculate coarse clipping for large list of evenly sized items. Prefer using ImGuiListClipper. + // OBSOLETED in 1.85 (from August 2021) + static inline float GetWindowContentRegionWidth() { return GetWindowContentRegionMax().x - GetWindowContentRegionMin().x; } + + // Some of the older obsolete names along with their replacement (commented out so they are not reported in IDE) + //-- OBSOLETED in 1.81 (from February 2021) + //static inline bool ListBoxHeader(const char* label, const ImVec2& size = ImVec2(0, 0)) { return BeginListBox(label, size); } + //static inline bool ListBoxHeader(const char* label, int items_count, int height_in_items = -1) { float height = GetTextLineHeightWithSpacing() * ((height_in_items < 0 ? ImMin(items_count, 7) : height_in_items) + 0.25f) + GetStyle().FramePadding.y * 2.0f; return BeginListBox(label, ImVec2(0.0f, height)); } // Helper to calculate size from items_count and height_in_items + //static inline void ListBoxFooter() { EndListBox(); } + //-- OBSOLETED in 1.79 (from August 2020) + //static inline void OpenPopupContextItem(const char* str_id = NULL, ImGuiMouseButton mb = 1) { OpenPopupOnItemClick(str_id, mb); } // Bool return value removed. Use IsWindowAppearing() in BeginPopup() instead. Renamed in 1.77, renamed back in 1.79. Sorry! + //-- OBSOLETED in 1.78 (from June 2020): Old drag/sliders functions that took a 'float power > 1.0f' argument instead of ImGuiSliderFlags_Logarithmic. See github.com/ocornut/imgui/issues/3361 for details. + //IMGUI_API bool DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed, const void* p_min, const void* p_max, const char* format, float power = 1.0f) // OBSOLETED in 1.78 (from June 2020) + //IMGUI_API bool DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed, const void* p_min, const void* p_max, const char* format, float power = 1.0f); // OBSOLETED in 1.78 (from June 2020) + //IMGUI_API bool SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, float power = 1.0f); // OBSOLETED in 1.78 (from June 2020) + //IMGUI_API bool SliderScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_min, const void* p_max, const char* format, float power = 1.0f); // OBSOLETED in 1.78 (from June 2020) + //static inline bool DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, float power = 1.0f) { return DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, float power = 1.0f) { return DragScalarN(label, ImGuiDataType_Float, v, 2, v_speed, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, float power = 1.0f) { return DragScalarN(label, ImGuiDataType_Float, v, 3, v_speed, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, float power = 1.0f) { return DragScalarN(label, ImGuiDataType_Float, v, 4, v_speed, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, float power = 1.0f) { return SliderScalar(label, ImGuiDataType_Float, v, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, float power = 1.0f) { return SliderScalarN(label, ImGuiDataType_Float, v, 2, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, float power = 1.0f) { return SliderScalarN(label, ImGuiDataType_Float, v, 3, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, float power = 1.0f) { return SliderScalarN(label, ImGuiDataType_Float, v, 4, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //-- OBSOLETED in 1.77 and before + //static inline bool BeginPopupContextWindow(const char* str_id, ImGuiMouseButton mb, bool over_items) { return BeginPopupContextWindow(str_id, mb | (over_items ? 0 : ImGuiPopupFlags_NoOpenOverItems)); } // OBSOLETED in 1.77 (from June 2020) + //static inline void TreeAdvanceToLabelPos() { SetCursorPosX(GetCursorPosX() + GetTreeNodeToLabelSpacing()); } // OBSOLETED in 1.72 (from July 2019) + //static inline void SetNextTreeNodeOpen(bool open, ImGuiCond cond = 0) { SetNextItemOpen(open, cond); } // OBSOLETED in 1.71 (from June 2019) + //static inline float GetContentRegionAvailWidth() { return GetContentRegionAvail().x; } // OBSOLETED in 1.70 (from May 2019) + //static inline ImDrawList* GetOverlayDrawList() { return GetForegroundDrawList(); } // OBSOLETED in 1.69 (from Mar 2019) + //static inline void SetScrollHere(float ratio = 0.5f) { SetScrollHereY(ratio); } // OBSOLETED in 1.66 (from Nov 2018) + //static inline bool IsItemDeactivatedAfterChange() { return IsItemDeactivatedAfterEdit(); } // OBSOLETED in 1.63 (from Aug 2018) + //-- OBSOLETED in 1.60 and before + //static inline bool IsAnyWindowFocused() { return IsWindowFocused(ImGuiFocusedFlags_AnyWindow); } // OBSOLETED in 1.60 (from Apr 2018) + //static inline bool IsAnyWindowHovered() { return IsWindowHovered(ImGuiHoveredFlags_AnyWindow); } // OBSOLETED in 1.60 (between Dec 2017 and Apr 2018) + //static inline void ShowTestWindow() { return ShowDemoWindow(); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline bool IsRootWindowFocused() { return IsWindowFocused(ImGuiFocusedFlags_RootWindow); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline bool IsRootWindowOrAnyChildFocused() { return IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline void SetNextWindowContentWidth(float w) { SetNextWindowContentSize(ImVec2(w, 0.0f)); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline float GetItemsLineHeightWithSpacing() { return GetFrameHeightWithSpacing(); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //IMGUI_API bool Begin(char* name, bool* p_open, ImVec2 size_first_use, float bg_alpha = -1.0f, ImGuiWindowFlags flags=0); // OBSOLETED in 1.52 (between Aug 2017 and Oct 2017): Equivalent of using SetNextWindowSize(size, ImGuiCond_FirstUseEver) and SetNextWindowBgAlpha(). + //static inline bool IsRootWindowOrAnyChildHovered() { return IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); } // OBSOLETED in 1.52 (between Aug 2017 and Oct 2017) + //static inline void AlignFirstTextHeightToWidgets() { AlignTextToFramePadding(); } // OBSOLETED in 1.52 (between Aug 2017 and Oct 2017) + //static inline void SetNextWindowPosCenter(ImGuiCond c=0) { SetNextWindowPos(GetMainViewport()->GetCenter(), c, ImVec2(0.5f,0.5f)); } // OBSOLETED in 1.52 (between Aug 2017 and Oct 2017) + //static inline bool IsItemHoveredRect() { return IsItemHovered(ImGuiHoveredFlags_RectOnly); } // OBSOLETED in 1.51 (between Jun 2017 and Aug 2017) + //static inline bool IsPosHoveringAnyWindow(const ImVec2&) { IM_ASSERT(0); return false; } // OBSOLETED in 1.51 (between Jun 2017 and Aug 2017): This was misleading and partly broken. You probably want to use the io.WantCaptureMouse flag instead. + //static inline bool IsMouseHoveringAnyWindow() { return IsWindowHovered(ImGuiHoveredFlags_AnyWindow); } // OBSOLETED in 1.51 (between Jun 2017 and Aug 2017) + //static inline bool IsMouseHoveringWindow() { return IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); } // OBSOLETED in 1.51 (between Jun 2017 and Aug 2017) + //-- OBSOLETED in 1.50 and before + //static inline bool CollapsingHeader(char* label, const char* str_id, bool framed = true, bool default_open = false) { return CollapsingHeader(label, (default_open ? (1 << 5) : 0)); } // OBSOLETED in 1.49 + //static inline ImFont*GetWindowFont() { return GetFont(); } // OBSOLETED in 1.48 + //static inline float GetWindowFontSize() { return GetFontSize(); } // OBSOLETED in 1.48 + //static inline void SetScrollPosHere() { SetScrollHere(); } // OBSOLETED in 1.42 +} + +// OBSOLETED in 1.82 (from Mars 2021): flags for AddRect(), AddRectFilled(), AddImageRounded(), PathRect() +typedef ImDrawFlags ImDrawCornerFlags; +enum ImDrawCornerFlags_ +{ + ImDrawCornerFlags_None = ImDrawFlags_RoundCornersNone, // Was == 0 prior to 1.82, this is now == ImDrawFlags_RoundCornersNone which is != 0 and not implicit + ImDrawCornerFlags_TopLeft = ImDrawFlags_RoundCornersTopLeft, // Was == 0x01 (1 << 0) prior to 1.82. Order matches ImDrawFlags_NoRoundCorner* flag (we exploit this internally). + ImDrawCornerFlags_TopRight = ImDrawFlags_RoundCornersTopRight, // Was == 0x02 (1 << 1) prior to 1.82. + ImDrawCornerFlags_BotLeft = ImDrawFlags_RoundCornersBottomLeft, // Was == 0x04 (1 << 2) prior to 1.82. + ImDrawCornerFlags_BotRight = ImDrawFlags_RoundCornersBottomRight, // Was == 0x08 (1 << 3) prior to 1.82. + ImDrawCornerFlags_All = ImDrawFlags_RoundCornersAll, // Was == 0x0F prior to 1.82 + ImDrawCornerFlags_Top = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_TopRight, + ImDrawCornerFlags_Bot = ImDrawCornerFlags_BotLeft | ImDrawCornerFlags_BotRight, + ImDrawCornerFlags_Left = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_BotLeft, + ImDrawCornerFlags_Right = ImDrawCornerFlags_TopRight | ImDrawCornerFlags_BotRight, +}; + +// RENAMED and MERGED both ImGuiKey_ModXXX and ImGuiModFlags_XXX into ImGuiMod_XXX (from September 2022) +// RENAMED ImGuiKeyModFlags -> ImGuiModFlags in 1.88 (from April 2022). Exceptionally commented out ahead of obscolescence schedule to reduce confusion and because they were not meant to be used in the first place. +typedef ImGuiKeyChord ImGuiModFlags; // == int. We generally use ImGuiKeyChord to mean "a ImGuiKey or-ed with any number of ImGuiMod_XXX value", but you may store only mods in there. +enum ImGuiModFlags_ { ImGuiModFlags_None = 0, ImGuiModFlags_Ctrl = ImGuiMod_Ctrl, ImGuiModFlags_Shift = ImGuiMod_Shift, ImGuiModFlags_Alt = ImGuiMod_Alt, ImGuiModFlags_Super = ImGuiMod_Super }; +//typedef ImGuiKeyChord ImGuiKeyModFlags; // == int +//enum ImGuiKeyModFlags_ { ImGuiKeyModFlags_None = 0, ImGuiKeyModFlags_Ctrl = ImGuiMod_Ctrl, ImGuiKeyModFlags_Shift = ImGuiMod_Shift, ImGuiKeyModFlags_Alt = ImGuiMod_Alt, ImGuiKeyModFlags_Super = ImGuiMod_Super }; + +#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +// RENAMED IMGUI_DISABLE_METRICS_WINDOW > IMGUI_DISABLE_DEBUG_TOOLS in 1.88 (from June 2022) +#if defined(IMGUI_DISABLE_METRICS_WINDOW) && !defined(IMGUI_DISABLE_OBSOLETE_FUNCTIONS) && !defined(IMGUI_DISABLE_DEBUG_TOOLS) +#define IMGUI_DISABLE_DEBUG_TOOLS +#endif +#if defined(IMGUI_DISABLE_METRICS_WINDOW) && defined(IMGUI_DISABLE_OBSOLETE_FUNCTIONS) +#error IMGUI_DISABLE_METRICS_WINDOW was renamed to IMGUI_DISABLE_DEBUG_TOOLS, please use new name. +#endif + +//----------------------------------------------------------------------------- + +#if defined(__clang__) +#pragma clang diagnostic pop +#elif defined(__GNUC__) +#pragma GCC diagnostic pop +#endif + +#ifdef _MSC_VER +#pragma warning (pop) +#endif + +// Include imgui_user.h at the end of imgui.h (convenient for user to only explicitly include vanilla imgui.h) +#ifdef IMGUI_INCLUDE_IMGUI_USER_H +#include "imgui_user.h" +#endif + +#endif // #ifndef IMGUI_DISABLE diff --git a/Plugin/include/reshade/reshade.hpp b/Plugin/include/reshade/reshade.hpp new file mode 100644 index 00000000..0b1b85c9 --- /dev/null +++ b/Plugin/include/reshade/reshade.hpp @@ -0,0 +1,346 @@ +/* + * Copyright (C) 2021 Patrick Mours + * SPDX-License-Identifier: BSD-3-Clause OR MIT + */ + +#pragma once + +#include "reshade_events.hpp" +#include "reshade_overlay.hpp" +#include +#include + +// Current version of the ReShade API +#define RESHADE_API_VERSION 8 + +// Optionally import ReShade API functions when 'RESHADE_API_LIBRARY' is defined instead of using header-only mode +#if defined(RESHADE_API_LIBRARY) || defined(RESHADE_API_LIBRARY_EXPORT) + +#ifdef RESHADE_API_LIBRARY_EXPORT + #define RESHADE_API_LIBRARY_DECLSPEC extern "C" __declspec(dllexport) +#else + #define RESHADE_API_LIBRARY_DECLSPEC extern "C" __declspec(dllimport) +#endif + +RESHADE_API_LIBRARY_DECLSPEC void ReShadeLogMessage(HMODULE module, int level, const char *message); + +RESHADE_API_LIBRARY_DECLSPEC void ReShadeGetBasePath(char *path, size_t *path_size); + +RESHADE_API_LIBRARY_DECLSPEC bool ReShadeGetConfigValue(HMODULE module, reshade::api::effect_runtime *runtime, const char *section, const char *key, char *value, size_t *value_size); +RESHADE_API_LIBRARY_DECLSPEC void ReShadeSetConfigValue(HMODULE module, reshade::api::effect_runtime *runtime, const char *section, const char *key, const char *value); + +RESHADE_API_LIBRARY_DECLSPEC bool ReShadeRegisterAddon(HMODULE module, uint32_t api_version); +RESHADE_API_LIBRARY_DECLSPEC void ReShadeUnregisterAddon(HMODULE module); + +RESHADE_API_LIBRARY_DECLSPEC void ReShadeRegisterEvent(reshade::addon_event ev, void *callback); +RESHADE_API_LIBRARY_DECLSPEC void ReShadeUnregisterEvent(reshade::addon_event ev, void *callback); + +RESHADE_API_LIBRARY_DECLSPEC void ReShadeRegisterOverlay(const char *title, void(*callback)(reshade::api::effect_runtime *runtime)); +RESHADE_API_LIBRARY_DECLSPEC void ReShadeUnregisterOverlay(const char *title, void(*callback)(reshade::api::effect_runtime *runtime)); + +#else + +// Use the kernel32 variant of module enumeration functions so it can be safely called from 'DllMain' +extern "C" BOOL WINAPI K32EnumProcessModules(HANDLE hProcess, HMODULE *lphModule, DWORD cb, LPDWORD lpcbNeeded); + +namespace reshade { namespace internal +{ + /// + /// Gets the handle to the ReShade module. + /// + inline HMODULE get_reshade_module_handle(HMODULE reshade_module = nullptr) + { + static HMODULE handle = reshade_module; + if (handle == nullptr) + { + HMODULE modules[1024]; DWORD num = 0; + if (K32EnumProcessModules(GetCurrentProcess(), modules, sizeof(modules), &num)) + { + if (num > sizeof(modules)) + num = sizeof(modules); + + for (DWORD i = 0; i < num / sizeof(HMODULE); ++i) + { + if (GetProcAddress(modules[i], "ReShadeRegisterAddon") && + GetProcAddress(modules[i], "ReShadeUnregisterAddon")) + { + handle = modules[i]; + break; + } + } + } + } + return handle; + } + + /// + /// Gets the handle to the current add-on module. + /// + inline HMODULE get_current_module_handle(HMODULE addon_module = nullptr) + { + static HMODULE handle = addon_module; + return handle; + } +} } + +#endif + +namespace reshade +{ + /// + /// Available log severity levels. + /// + enum class log_level + { + error = 1, + warning = 2, + info = 3, + debug = 4 + }; + + /// + /// Writes a message to ReShade's log. + /// + /// Severity level. + /// A null-terminated message string. + inline void log_message(log_level level, const char *message) + { +#if defined(RESHADE_API_LIBRARY) || defined(RESHADE_API_LIBRARY_EXPORT) + ReShadeLogMessage(nullptr, static_cast(level), message); +#else + static const auto func = reinterpret_cast( + GetProcAddress(internal::get_reshade_module_handle(), "ReShadeLogMessage")); + func(internal::get_current_module_handle(), static_cast(level), message); +#endif + } + + /// + /// Gets the file path ReShade uses to resolve relative paths. + /// + /// Pointer to a string buffer that is filled with the file path to the preset, or to query the necessary size. + /// Pointer to an integer that contains the size of the string buffer and is set to the actual length of the string, including the null-terminator. + inline void get_reshade_base_path(char *path, size_t *path_size) + { +#if defined(RESHADE_API_LIBRARY) || defined(RESHADE_API_LIBRARY_EXPORT) + ReShadeGetBasePath(path, path_size); +#else + static const auto func = reinterpret_cast( + GetProcAddress(internal::get_reshade_module_handle(), "ReShadeGetBasePath")); + func(path, path_size); +#endif + } + + /// + /// Gets a value from one of ReShade's config files. + /// + /// Optional effect runtime to use the config file from, or to use the global config file. + /// Name of the config section. + /// Name of the config value. + /// Pointer to a string buffer that is filled with the config value, or to query the necessary size. + /// Pointer to an integer that contains the size of the string buffer and is set to the actual length of the string, including the null-terminator. + /// if the specified config value exists, otherwise. + inline bool get_config_value(api::effect_runtime *runtime, const char *section, const char *key, char *value, size_t *value_size) + { +#if defined(RESHADE_API_LIBRARY) || defined(RESHADE_API_LIBRARY_EXPORT) + return ReShadeGetConfigValue(nullptr, runtime, section, key, value, value_size); +#else + static const auto func = reinterpret_cast( + GetProcAddress(internal::get_reshade_module_handle(), "ReShadeGetConfigValue")); + return func(internal::get_current_module_handle(), runtime, section, key, value, value_size); +#endif + } +#if _HAS_CXX17 + template + inline bool get_config_value(api::effect_runtime *runtime, const char *section, const char *key, T &value) + { + char value_string[32]; size_t value_length = sizeof(value_string) - 1; + if (!get_config_value(runtime, section, key, value_string, &value_length)) + return false; + return std::from_chars(value_string, value_string + value_length, value).ec == std::errc {}; + } + template <> + inline bool get_config_value(api::effect_runtime *runtime, const char *section, const char *key, bool &value) + { + int value_int = 0; + if (!get_config_value(runtime, section, key, value_int)) + return false; + value = (value_int != 0); + return true; + } +#endif + + /// + /// Sets and saves a value in one of ReShade's config files. + /// + /// Optional effect runtime to use the config file from, or to use the global config file. + /// Name of the config section. + /// Name of the config value. + /// Config value to set. + inline void set_config_value(api::effect_runtime *runtime, const char *section, const char *key, const char *value) + { +#if defined(RESHADE_API_LIBRARY) || defined(RESHADE_API_LIBRARY_EXPORT) + ReShadeSetConfigValue(nullptr, runtime, section, key, value); +#else + static const auto func = reinterpret_cast( + GetProcAddress(internal::get_reshade_module_handle(), "ReShadeSetConfigValue")); + func(internal::get_current_module_handle(), runtime, section, key, value); +#endif + } +#if _HAS_CXX17 + template + inline void set_config_value(api::effect_runtime *runtime, const char *section, const char *key, const T &value) + { + char value_string[32] = ""; + std::to_chars(value_string, value_string + sizeof(value_string) - 1, value); + set_config_value(runtime, section, key, static_cast(value_string)); + } + template <> + inline void set_config_value(api::effect_runtime *runtime, const char *section, const char *key, const bool &value) + { + set_config_value(runtime, section, key, value ? 1 : 0); + } +#endif + + /// + /// Registers this module as an add-on with ReShade. + /// Call this in 'AddonInit' or 'DllMain' during process attach, before any of the other API functions! + /// + /// Handle of the current module. + /// Handle of the ReShade module in the process, or to find it automatically. + inline bool register_addon(HMODULE addon_module, [[maybe_unused]] HMODULE reshade_module = nullptr) + { +#if defined(RESHADE_API_LIBRARY) || (defined(RESHADE_API_LIBRARY_EXPORT) && RESHADE_ADDON) + return ReShadeRegisterAddon(addon_module, RESHADE_API_VERSION); +#elif !defined(RESHADE_API_LIBRARY_EXPORT) + addon_module = internal::get_current_module_handle(addon_module); + reshade_module = internal::get_reshade_module_handle(reshade_module); + + if (reshade_module == nullptr) + return false; + + const auto func = reinterpret_cast( + GetProcAddress(reshade_module, "ReShadeRegisterAddon")); + // Check that the ReShade module supports the used API + if (func == nullptr || !func(addon_module, RESHADE_API_VERSION)) + return false; + +#if defined(IMGUI_VERSION_NUM) + const auto imgui_func = reinterpret_cast( + GetProcAddress(reshade_module, "ReShadeGetImGuiFunctionTable")); + // Check that the ReShade module was built with Dear ImGui support and supports the used version + if (imgui_func == nullptr || !(imgui_function_table_instance() = imgui_func(IMGUI_VERSION_NUM))) + return false; +#endif + + return true; +#else + UNREFERENCED_PARAMETER(addon_module); + UNREFERENCED_PARAMETER(reshade_module); + return false; +#endif + } + /// + /// Unregisters this module. + /// Call this in 'AddonUninit' or 'DllMain' during process detach, after any of the other API functions. + /// + /// Handle of the current module. + /// Handle of the ReShade module in the process, or to find it automatically. + inline void unregister_addon(HMODULE addon_module, [[maybe_unused]] HMODULE reshade_module = nullptr) + { +#if defined(RESHADE_API_LIBRARY) || (defined(RESHADE_API_LIBRARY_EXPORT) && RESHADE_ADDON) + ReShadeUnregisterAddon(addon_module); +#elif !defined(RESHADE_API_LIBRARY_EXPORT) + addon_module = internal::get_current_module_handle(addon_module); + reshade_module = internal::get_reshade_module_handle(reshade_module); + + if (reshade_module == nullptr) + return; + + const auto func = reinterpret_cast( + GetProcAddress(reshade_module, "ReShadeUnregisterAddon")); + if (func != nullptr) + func(addon_module); +#else + UNREFERENCED_PARAMETER(addon_module); + UNREFERENCED_PARAMETER(reshade_module); +#endif + } + + /// + /// Registers a callback for the specified event (via template) with ReShade. + /// The callback function is then called whenever the application performs a task associated with this event (see also the enumeration). + /// + /// Pointer to the callback function. + template + inline void register_event(typename reshade::addon_event_traits::decl callback) + { +#if defined(RESHADE_API_LIBRARY) || (defined(RESHADE_API_LIBRARY_EXPORT) && RESHADE_ADDON) + ReShadeRegisterEvent(ev, static_cast(callback)); +#elif !defined(RESHADE_API_LIBRARY_EXPORT) + static const auto func = reinterpret_cast( + GetProcAddress(internal::get_reshade_module_handle(), "ReShadeRegisterEvent")); + if (func != nullptr) + func(ev, static_cast(callback)); +#else + UNREFERENCED_PARAMETER(callback); +#endif + } + /// + /// Unregisters a callback for the specified event (via template) that was previously registered via . + /// + /// Pointer to the callback function. + template + inline void unregister_event(typename reshade::addon_event_traits::decl callback) + { +#if defined(RESHADE_API_LIBRARY) || (defined(RESHADE_API_LIBRARY_EXPORT) && RESHADE_ADDON) + ReShadeUnregisterEvent(ev, static_cast(callback)); +#elif !defined(RESHADE_API_LIBRARY_EXPORT) + static const auto func = reinterpret_cast( + GetProcAddress(internal::get_reshade_module_handle(), "ReShadeUnregisterEvent")); + if (func != nullptr) + func(ev, static_cast(callback)); +#else + UNREFERENCED_PARAMETER(callback); +#endif + } + + /// + /// Registers an overlay with ReShade. + /// The callback function is then called when the overlay is visible and allows adding Dear ImGui widgets for user interaction. + /// + /// Null-terminated title string, or to register a settings overlay for this add-on. + /// Pointer to the callback function. + inline void register_overlay(const char *title, void(*callback)(reshade::api::effect_runtime *runtime)) + { +#if defined(RESHADE_API_LIBRARY) || (defined(RESHADE_API_LIBRARY_EXPORT) && RESHADE_ADDON && RESHADE_GUI) + ReShadeRegisterOverlay(title, callback); +#elif !defined(RESHADE_API_LIBRARY_EXPORT) + static const auto func = reinterpret_cast( + GetProcAddress(internal::get_reshade_module_handle(), "ReShadeRegisterOverlay")); + if (func != nullptr) + func(title, callback); +#else + UNREFERENCED_PARAMETER(title); + UNREFERENCED_PARAMETER(callback); +#endif + } + /// + /// Unregisters an overlay that was previously registered via . + /// + /// Null-terminated title string. + /// Pointer to the callback function. + inline void unregister_overlay(const char *title, void(*callback)(reshade::api::effect_runtime *runtime)) + { +#if defined(RESHADE_API_LIBRARY) || (defined(RESHADE_API_LIBRARY_EXPORT) && RESHADE_ADDON && RESHADE_GUI) + ReShadeUnregisterOverlay(title, callback); +#elif !defined(RESHADE_API_LIBRARY_EXPORT) + static const auto func = reinterpret_cast( + GetProcAddress(internal::get_reshade_module_handle(), "ReShadeUnregisterOverlay")); + if (func != nullptr) + func(title, callback); +#else + UNREFERENCED_PARAMETER(title); + UNREFERENCED_PARAMETER(callback); +#endif + } +} diff --git a/Plugin/include/reshade/reshade_api.hpp b/Plugin/include/reshade/reshade_api.hpp new file mode 100644 index 00000000..4691bd4d --- /dev/null +++ b/Plugin/include/reshade/reshade_api.hpp @@ -0,0 +1,726 @@ +/* + * Copyright (C) 2021 Patrick Mours + * SPDX-License-Identifier: BSD-3-Clause OR MIT + */ + +#pragma once + +#include "reshade_api_device.hpp" + +namespace reshade { namespace api +{ + /// + /// An opaque handle to a technique in an effect. + /// + /// + /// This handle is only valid until effects are next reloaded again (). + /// + RESHADE_DEFINE_HANDLE(effect_technique); + /// + /// An opaque handle to a texture variable in an effect. + /// + /// + /// This handle is only valid until effects are next reloaded again (). + /// + RESHADE_DEFINE_HANDLE(effect_texture_variable); + /// + /// An opaque handle to a uniform variable in an effect. + /// + /// + /// This handle is only valid until effects are next reloaded again (). + /// + RESHADE_DEFINE_HANDLE(effect_uniform_variable); + + /// + /// A ReShade effect runtime, used to control effects. + /// A separate runtime is instantiated for every swap chain. + /// + struct __declspec(novtable) effect_runtime : public swapchain + { + /// + /// Gets the main graphics command queue associated with this effect runtime. + /// This may potentially be different from the presentation queue and should be used to execute graphics commands on. + /// + virtual command_queue *get_command_queue() = 0; + + /// + /// Applies post-processing effects to the specified render targets and prevents the usual rendering of effects before swap chain presentation of the current frame. + /// This can be used to force ReShade to render effects at a certain point during the frame to e.g. avoid effects being applied to user interface elements of the application. + /// + /// + /// The resource the render target views point to has to be in the state. + /// This call may modify current state on the command list (pipeline, render targets, descriptor tables, ...), so it may be necessary for an add-on to backup and restore state around it if the application does not bind all state again afterwards already. + /// Calling this with set to zero will cause nothing to be rendered, but uniform variables to still be updated. + /// + /// Command list to add effect rendering commands to. + /// Render target view to use for passes that write to the back buffer with SRGBWriteEnabled state set to . + /// Render target view to use for passes that write to the back buffer with SRGBWriteEnabled state set to , or zero in which case the view from is used. + virtual void render_effects(command_list *cmd_list, resource_view rtv, resource_view rtv_srgb = { 0 }) = 0; + + /// + /// Captures a screenshot of the current back buffer resource and returns its image data in 32 bits-per-pixel RGBA format. + /// + /// Pointer to an array of width * height * 4 bytes the image data is written to. + virtual bool capture_screenshot(uint8_t *pixels) = 0; + + /// + /// Gets the current buffer dimensions of the swap chain. + /// + virtual void get_screenshot_width_and_height(uint32_t *out_width, uint32_t *out_height) const = 0; + + /// + /// Gets the current status of the specified key. + /// + /// The virtual key code to check. + /// if the key is currently pressed down, otherwise. + virtual bool is_key_down(uint32_t keycode) const = 0; + /// + /// Gets whether the specified key was pressed this frame. + /// + /// The virtual key code to check. + /// if the key was pressed this frame, otherwise. + virtual bool is_key_pressed(uint32_t keycode) const = 0; + /// + /// Gets whether the specified key was released this frame. + /// + /// The virtual key code to check. + /// if the key was released this frame, otherwise. + virtual bool is_key_released(uint32_t keycode) const = 0; + /// + /// Gets the current status of the specified mouse button. + /// + /// The mouse button index to check (0 = left, 1 = middle, 2 = right). + /// if the mouse button is currently pressed down, otherwise. + virtual bool is_mouse_button_down(uint32_t button) const = 0; + /// + /// Gets whether the specified mouse button was pressed this frame. + /// + /// The mouse button index to check (0 = left, 1 = middle, 2 = right). + /// if the mouse button was pressed this frame, otherwise. + virtual bool is_mouse_button_pressed(uint32_t button) const = 0; + /// + /// Gets whether the specified mouse button was released this frame. + /// + /// The mouse button index to check (0 = left, 1 = middle, 2 = right). + /// if the mouse button was released this frame, otherwise. + virtual bool is_mouse_button_released(uint32_t button) const = 0; + + /// + /// Gets the current absolute position of the mouse cursor in screen coordinates. + /// + /// Pointer to a variable that is set to the X coordinate of the current cursor position. + /// Pointer to a variable that is set to the Y coordinate of the current cursor position. + /// Optional pointer to a variable that is set to the mouse wheel delta since the last frame. + virtual void get_mouse_cursor_position(uint32_t *out_x, uint32_t *out_y, int16_t *out_wheel_delta = nullptr) const = 0; + + /// + /// Enumerates all uniform variables of loaded effects and calls the specified function with a handle for each one. + /// + /// File name of the effect file to enumerate uniform variables from, or to enumerate those of all loaded effects. + /// Function to call for every uniform variable. + /// Optional pointer passed to the callback function. + virtual void enumerate_uniform_variables(const char *effect_name, void(*callback)(effect_runtime *runtime, effect_uniform_variable variable, void *user_data), void *user_data) = 0; + /// + /// Enumerates all uniform variables of loaded effects and calls the specified callback function with a handle for each one. + /// + /// File name of the effect file to enumerate uniform variables from, or to enumerate those of all loaded effects. + /// Function to call for every uniform variable. + template + inline void enumerate_uniform_variables(const char *effect_name, F lambda) + { + enumerate_uniform_variables(effect_name, [](effect_runtime *runtime, effect_uniform_variable variable, void *user_data) { static_cast(user_data)->operator()(runtime, variable); }, &lambda); + } + + /// + /// Finds a specific uniform variable in the loaded effects and returns a handle to it. + /// + /// File name of the effect file the variable is declared in, or to search in all loaded effects. + /// Name of the uniform variable declaration to find. + /// Opaque handle to the uniform variable, or zero in case it was not found. + virtual effect_uniform_variable find_uniform_variable(const char *effect_name, const char *variable_name) const = 0; + + /// + /// Gets information about the data type of a uniform . + /// + /// Opaque handle to the uniform variable. + /// Optional pointer to a variable that is set to the base type of the uniform variable (, , or ). + /// Optional pointer to a variable that is set to the number of vector rows of the uniform variable type. + /// Optional pointer to a variable that is set to the number of matrix column of the uniform variable type. + /// Optional pointer to a variable that is set to the number of array elements of the uniform variable type. + virtual void get_uniform_variable_type(effect_uniform_variable variable, format *out_base_type, uint32_t *out_rows = nullptr, uint32_t *out_columns = nullptr, uint32_t *out_array_length = nullptr) const = 0; + + /// + /// Gets the name of a uniform . + /// + /// Opaque handle to the uniform variable. + /// Pointer to a string buffer that is filled with the name of the uniform variable, or to query the necessary size. + /// Pointer to an integer that contains the size of the string buffer and is set to the actual length of the string, including the null-terminator. + virtual void get_uniform_variable_name(effect_uniform_variable variable, char *name, size_t *name_size) const = 0; + template + inline void get_uniform_variable_name(effect_uniform_variable variable, char(&name)[SIZE]) const + { + size_t name_size = SIZE; + get_uniform_variable_name(variable, name, &name_size); + } + + /// + /// Gets the value from an annotation attached to the specified uniform as boolean values. + /// + /// Opaque handle to the uniform variable. + /// Name of the annotation. + /// Pointer to an array of booleans that is filled with the values of the annotation. + /// Number of values to read. + /// Array offset to start reading values from when the annotation is an array. + virtual bool get_annotation_bool_from_uniform_variable(effect_uniform_variable variable, const char *name, bool *values, size_t count, size_t array_index = 0) const = 0; + /// + /// Gets the value from an annotation attached to the specified uniform as floating-point values. + /// + /// Opaque handle to the uniform variable. + /// Name of the annotation. + /// Pointer to an array of floating-points that is filled with the values of the annotation. + /// Number of values to read. + /// Array offset to start reading values from when the annotation is an array. + virtual bool get_annotation_float_from_uniform_variable(effect_uniform_variable variable, const char *name, float *values, size_t count, size_t array_index = 0) const = 0; + /// + /// Gets the value from an annotation attached to the specified uniform as signed integer values. + /// + /// Opaque handle to the uniform variable. + /// Name of the annotation. + /// Pointer to an array of signed integers that is filled with the values of the annotation. + /// Number of values to read. + /// Array offset to start reading values from when the annotation is an array. + virtual bool get_annotation_int_from_uniform_variable(effect_uniform_variable variable, const char *name, int32_t *values, size_t count, size_t array_index = 0) const = 0; + /// + /// Gets the value from an annotation attached to the specified uniform as unsigned integer values. + /// + /// Opaque handle to the uniform variable. + /// Name of the annotation. + /// Pointer to an array of unsigned integers that is filled with the values of the annotation. + /// Number of values to read. + /// Array offset to start reading values from when the annotation is an array. + virtual bool get_annotation_uint_from_uniform_variable(effect_uniform_variable variable, const char *name, uint32_t *values, size_t count, size_t array_index = 0) const = 0; + /// + /// Gets the value from a string annotation attached to the specified uniform . + /// + /// Opaque handle to the uniform variable. + /// Name of the annotation. + /// Pointer to a string buffer that is filled with the value of the annotation, or to query the necessary size. + /// Pointer to an integer that contains the size of the string buffer and is set to the actual length of the string, including the null-terminator. + virtual bool get_annotation_string_from_uniform_variable(effect_uniform_variable variable, const char *name, char *value, size_t *value_size) const = 0; + template + inline bool get_annotation_string_from_uniform_variable(effect_uniform_variable variable, const char *name, char(&value)[SIZE]) const + { + size_t value_size = SIZE; + return get_annotation_string_from_uniform_variable(variable, name, value, &value_size); + } + + /// + /// Gets the value of the specified uniform as boolean values. + /// + /// Opaque handle to the uniform variable. + /// Pointer to an array of booleans that is filled with the values of this uniform variable. + /// Number of values to read. + /// Array offset to start reading values from when this uniform variable is an array variable. + virtual void get_uniform_value_bool(effect_uniform_variable variable, bool *values, size_t count, size_t array_index = 0) const = 0; + /// + /// Gets the value of the specified uniform as floating-point values. + /// + /// Opaque handle to the uniform variable. + /// Pointer to an array of floating-points that is filled with the values of this uniform variable. + /// Number of values to read. + /// Array offset to start reading values from when this uniform variable is an array variable. + virtual void get_uniform_value_float(effect_uniform_variable variable, float *values, size_t count, size_t array_index = 0) const = 0; + /// + /// Gets the value of the specified uniform as signed integer values. + /// + /// Opaque handle to the uniform variable. + /// Pointer to an array of signed integers that is filled with the values of this uniform variable. + /// Number of values to read. + /// Array offset to start reading values from when this uniform variable is an array variable. + virtual void get_uniform_value_int(effect_uniform_variable variable, int32_t *values, size_t count, size_t array_index = 0) const = 0; + /// + /// Gets the value of the specified uniform as unsigned integer values. + /// + /// Opaque handle to the uniform variable. + /// Pointer to an array of unsigned integers that is filled with the values of this uniform variable. + /// Number of values to read. + /// Array offset to start reading values from when this uniform variable is an array variable. + virtual void get_uniform_value_uint(effect_uniform_variable variable, uint32_t *values, size_t count, size_t array_index = 0) const = 0; + + /// + /// Sets the value of the specified uniform as boolean values. + /// + /// Opaque handle to the uniform variable. + /// Pointer to an array of booleans that are used to update this uniform variable. + /// Number of values to write. + /// Array offset to start writing values to when this uniform variable is an array variable. + /// Setting the uniform value won't result in a save of the current preset. To make sure the current preset with the changed value + /// is saved to disk, call + virtual void set_uniform_value_bool(effect_uniform_variable variable, const bool *values, size_t count, size_t array_index = 0) = 0; + /// + /// Sets the value of the specified uniform as a vector of boolean values. + /// + /// Opaque handle to the uniform variable. + /// Value of the first component in the vector that is used to update this uniform variable. + /// Optional value of the second component in the vector that is used to update this uniform variable. + /// Optional value of the third component in the vector that is used to update this uniform variable. + /// Optional value of the fourth component in the vector that is used to update this uniform variable. + /// Setting the uniform value won't result in a save of the current preset. To make sure the current preset with the changed value + /// is saved to disk, call + inline void set_uniform_value_bool(effect_uniform_variable variable, bool x, bool y = bool(0), bool z = bool(0), bool w = bool(0)) + { + const bool values[4] = { x, y, z, w }; + set_uniform_value_bool(variable, values, 4); + } + /// + /// Sets the value of the specified uniform as floating-point values. + /// + /// Opaque handle to the uniform variable. + /// Pointer to an array of floating-points that are used to update this uniform variable. + /// Number of values to write. + /// Array offset to start writing values to when this uniform variable is an array variable. + /// Setting the uniform value won't result in a save of the current preset. To make sure the current preset with the changed value + /// is saved to disk, call + virtual void set_uniform_value_float(effect_uniform_variable variable, const float *values, size_t count, size_t array_index = 0) = 0; + /// + /// Sets the value of the specified uniform as a vector of floating-point values. + /// + /// Opaque handle to the uniform variable. + /// Value of the first component in the vector that is used to update this uniform variable. + /// Optional value of the second component in the vector that is used to update this uniform variable. + /// Optional value of the third component in the vector that is used to update this uniform variable. + /// Optional value of the fourth component in the vector that is used to update this uniform variable. + /// Setting the uniform value won't result in a save of the current preset. To make sure the current preset with the changed value + /// is saved to disk, call + inline void set_uniform_value_float(effect_uniform_variable variable, float x, float y = float(0), float z = float(0), float w = float(0)) + { + const float values[4] = { x, y, z, w }; + set_uniform_value_float(variable, values, 4); + } + /// + /// Sets the value of the specified uniform as signed integer values. + /// + /// Opaque handle to the uniform variable. + /// Pointer to an array of signed integers that are used to update this uniform variable. + /// Number of values to write. + /// Array offset to start writing values to when this uniform variable is an array variable. + /// Setting the uniform value won't result in a save of the current preset. To make sure the current preset with the changed value + /// is saved to disk, call + virtual void set_uniform_value_int(effect_uniform_variable variable, const int32_t *values, size_t count, size_t array_index = 0) = 0; + /// + /// Sets the value of the specified uniform as a vector of signed integer values. + /// + /// Opaque handle to the uniform variable. + /// Value of the first component in the vector that is used to update this uniform variable. + /// Optional value of the second component in the vector that is used to update this uniform variable. + /// Optional value of the third component in the vector that is used to update this uniform variable. + /// Optional value of the fourth component in the vector that is used to update this uniform variable. + /// Setting the uniform value won't result in a save of the current preset. To make sure the current preset with the changed value + /// is saved to disk, call + inline void set_uniform_value_int(effect_uniform_variable variable, int32_t x, int32_t y = int32_t(0), int32_t z = int32_t(0), int32_t w = int32_t(0)) + { + const int32_t values[4] = { x, y, z, w }; + set_uniform_value_int(variable, values, 4); + } + /// + /// Sets the value of the specified uniform as unsigned integer values. + /// + /// Opaque handle to the uniform variable. + /// Pointer to an array of unsigned integers that are used to update this uniform variable. + /// Number of values to write. + /// Array offset to start writing values to when this uniform variable is an array variable. + /// Setting the uniform value won't result in a save of the current preset. To make sure the current preset with the changed value + /// is saved to disk, call + virtual void set_uniform_value_uint(effect_uniform_variable variable, const uint32_t *values, size_t count, size_t array_index = 0) = 0; + /// + /// Sets the value of the specified uniform as a vector of unsigned integer values. + /// + /// Opaque handle to the uniform variable. + /// Value of the first component in the vector that is used to update this uniform variable. + /// Optional value of the second component in the vector that is used to update this uniform variable. + /// Optional value of the third component in the vector that is used to update this uniform variable. + /// Optional value of the fourth component in the vector that is used to update this uniform variable. + /// Setting the uniform value won't result in a save of the current preset. To make sure the current preset with the changed value + /// is saved to disk, call + inline void set_uniform_value_uint(effect_uniform_variable variable, uint32_t x, uint32_t y = uint32_t(0), uint32_t z = uint32_t(0), uint32_t w = uint32_t(0)) + { + const uint32_t values[4] = { x, y, z, w }; + set_uniform_value_uint(variable, values, 4); + } + + /// + /// Enumerates all texture variables of loaded effects and calls the specified function with a handle for each one. + /// + /// File name of the effect file to enumerate texture variables from, or to enumerate those of all loaded effects. + /// Function to call for every texture variable. + /// Optional pointer passed to the callback function. + virtual void enumerate_texture_variables(const char *effect_name, void(*callback)(effect_runtime *runtime, effect_texture_variable variable, void *user_data), void *user_data) = 0; + /// + /// Enumerates all texture variables of loaded effects and calls the specified callback function with a handle for each one. + /// + /// File name of the effect file to enumerate texture variables from, or to enumerate those of all loaded effects. + /// Function to call for every texture variable. + template + inline void enumerate_texture_variables(const char *effect_name, F lambda) + { + enumerate_texture_variables(effect_name, [](effect_runtime *runtime, effect_texture_variable variable, void *user_data) { static_cast(user_data)->operator()(runtime, variable); }, &lambda); + } + + /// + /// Finds a specific texture variable in the loaded effects and returns a handle to it. + /// + /// File name of the effect file the variable is declared in, or to search in all loaded effects. + /// Name of the texture variable declaration to find. + /// Opaque handle to the texture variable, or zero in case it was not found. + virtual effect_texture_variable find_texture_variable(const char *effect_name, const char *variable_name) const = 0; + + /// + /// Gets the name of a texture . + /// + /// Opaque handle to the texture variable. + /// Pointer to a string buffer that is filled with the name of the texture variable, or to query the necessary size. + /// Pointer to an integer that contains the size of the string buffer and is set to the actual length of the string, including the null-terminator. + virtual void get_texture_variable_name(effect_texture_variable variable, char *name, size_t *name_size) const = 0; + template + inline void get_texture_variable_name(effect_texture_variable variable, char(&name)[SIZE]) const + { + size_t name_size = SIZE; + get_texture_variable_name(variable, name, &name_size); + } + + /// + /// Gets the value from an annotation attached to the specified texture as boolean values. + /// + /// Opaque handle to the texture variable. + /// Name of the annotation. + /// Pointer to an array of booleans that is filled with the values of the annotation. + /// Number of values to read. + /// Array offset to start reading values from when the annotation is an array. + virtual bool get_annotation_bool_from_texture_variable(effect_texture_variable variable, const char *name, bool *values, size_t count, size_t array_index = 0) const = 0; + /// + /// Gets the value from an annotation attached to the specified texture as floating-point values. + /// + /// Opaque handle to the texture variable. + /// Name of the annotation. + /// Pointer to an array of floating-points that is filled with the values of the annotation. + /// Number of values to read. + /// Array offset to start reading values from when the annotation is an array. + virtual bool get_annotation_float_from_texture_variable(effect_texture_variable variable, const char *name, float *values, size_t count, size_t array_index = 0) const = 0; + /// + /// Gets the value from an annotation attached to the specified texture as signed integer values. + /// + /// Opaque handle to the texture variable. + /// Name of the annotation. + /// Pointer to an array of signed integers that is filled with the values of the annotation. + /// Number of values to read. + /// Array offset to start reading values from when the annotation is an array. + virtual bool get_annotation_int_from_texture_variable(effect_texture_variable variable, const char *name, int32_t *values, size_t count, size_t array_index = 0) const = 0; + /// + /// Gets the value from an annotation attached to the specified texture as unsigned integer values. + /// + /// Opaque handle to the texture variable. + /// Name of the annotation. + /// Pointer to an array of unsigned integers that is filled with the values of the annotation. + /// Number of values to read. + /// Array offset to start reading values from when the annotation is an array. + virtual bool get_annotation_uint_from_texture_variable(effect_texture_variable variable, const char *name, uint32_t *values, size_t count, size_t array_index = 0) const = 0; + /// + /// Gets the value from a string annotation attached to the specified texture . + /// + /// Opaque handle to the texture variable. + /// Name of the annotation. + /// Pointer to a string buffer that is filled with the value of the annotation, or to query the necessary size. + /// Pointer to an integer that contains the size of the string buffer and is set to the actual length of the string, including the null-terminator. + virtual bool get_annotation_string_from_texture_variable(effect_texture_variable variable, const char *name, char *value, size_t *value_size) const = 0; + template + inline bool get_annotation_string_from_texture_variable(effect_texture_variable variable, const char *name, char(&value)[SIZE]) const + { + size_t value_size = SIZE; + return get_annotation_string_from_texture_variable(variable, name, value, &value_size); + } + + /// + /// Uploads 32 bits-per-pixel RGBA image data to the specified texture . + /// + /// Opaque handle to the texture variable. + /// Width of the image data. + /// Height of the image data. + /// Pointer to an array of width * height * 4 bytes the image data is read from. + virtual void update_texture(effect_texture_variable variable, const uint32_t width, const uint32_t height, const uint8_t *pixels) = 0; + + /// + /// Gets the shader resource view that is bound to the specified texture . + /// + /// Opaque handle to the texture variable. + /// Pointer to a variable that is set to the shader resource view. + /// Pointer to a variable that is set to the sRGB shader resource view. + virtual void get_texture_binding(effect_texture_variable variable, resource_view *out_srv, resource_view *out_srv_srgb = nullptr) const = 0; + + /// + /// Binds a new shader resource view to all texture variables that use the specified . + /// + /// + /// The resource the shader resource views point to has to be in the state at the time is executed. + /// + /// ReShade FX semantic to filter textures to update by (texture name : SEMANTIC). + /// Shader resource view to use for samplers with SRGBTexture state set to . + /// Shader resource view to use for samplers with SRGBTexture state set to , or zero in which case the view from is used. + virtual void update_texture_bindings(const char *semantic, resource_view srv, resource_view srv_srgb = { 0 }) = 0; + + /// + /// Enumerates all techniques of loaded effects and calls the specified function with a handle for each one. + /// + /// File name of the effect file to enumerate techniques from, or to enumerate those of all loaded effects. + /// Function to call for every technique. + /// Optional pointer passed to the callback function. + virtual void enumerate_techniques(const char *effect_name, void(*callback)(effect_runtime *runtime, effect_technique technique, void *user_data), void *user_data) = 0; + /// + /// Enumerates all techniques of loaded effects and calls the specified callback function with a handle for each one. + /// + /// File name of the effect file to enumerate techniques from, or to enumerate those of all loaded effects. + /// Function to call for every technique. + template + inline void enumerate_techniques(const char *effect_name, F lambda) + { + enumerate_techniques(effect_name, [](effect_runtime *runtime, effect_technique technique, void *user_data) { static_cast(user_data)->operator()(runtime, technique); }, &lambda); + } + + /// + /// Finds a specific technique in the loaded effects and returns a handle to it. + /// + /// File name of the effect file the technique is declared in, or to search in all loaded effects. + /// Name of the technique to find. + /// Opaque handle to the technique, or zero in case it was not found. + virtual effect_technique find_technique(const char *effect_name, const char *technique_name) = 0; + + /// + /// Gets the name of a . + /// + /// Opaque handle to the technique. + /// Pointer to a string buffer that is filled with the name of the technique, or to query the necessary size. + /// Pointer to an integer that contains the size of the string buffer and is set to the actual length of the string, including the null-terminator. + virtual void get_technique_name(effect_technique technique, char *name, size_t *name_size) const = 0; + template + inline void get_technique_name(effect_technique technique, char(&name)[SIZE]) const + { + size_t name_size = SIZE; + get_technique_name(technique, name, &name_size); + } + + /// + /// Gets the value from an annotation attached to the specified as boolean values. + /// + /// Opaque handle to the technique. + /// Name of the annotation. + /// Pointer to an array of booleans that is filled with the values of the annotation. + /// Number of values to read. + /// Array offset to start reading values from when the annotation is an array. + virtual bool get_annotation_bool_from_technique(effect_technique technique, const char *name, bool *values, size_t count, size_t array_index = 0) const = 0; + /// + /// Gets the value from an annotation attached to the specified as floating-point values. + /// + /// Opaque handle to the technique. + /// Name of the annotation. + /// Pointer to an array of floating-points that is filled with the values of the annotation. + /// Number of values to read. + /// Array offset to start reading values from when the annotation is an array. + virtual bool get_annotation_float_from_technique(effect_technique technique, const char *name, float *values, size_t count, size_t array_index = 0) const = 0; + /// + /// Gets the value from an annotation attached to the specified as signed integer values. + /// + /// Opaque handle to the technique. + /// Name of the annotation. + /// Pointer to an array of signed integers that is filled with the values of the annotation. + /// Number of values to read. + /// Array offset to start reading values from when the annotation is an array. + virtual bool get_annotation_int_from_technique(effect_technique technique, const char *name, int32_t *values, size_t count, size_t array_index = 0) const = 0; + /// + /// Gets the value from an annotation attached to the specified as unsigned integer values. + /// + /// Opaque handle to the technique. + /// Name of the annotation. + /// Pointer to an array of unsigned integers that is filled with the values of the annotation. + /// Number of values to read. + /// Array offset to start reading values from when the annotation is an array. + virtual bool get_annotation_uint_from_technique(effect_technique technique, const char *name, uint32_t *values, size_t count, size_t array_index = 0) const = 0; + /// + /// Gets the value from a string annotation attached to the specified . + /// + /// Opaque handle to the technique. + /// Name of the annotation. + /// Pointer to a string buffer that is filled with the value of the annotation, or to query the necessary size. + /// Pointer to an integer that contains the size of the string buffer and is set to the actual length of the string, including the null-terminator. + virtual bool get_annotation_string_from_technique(effect_technique technique, const char *name, char *value, size_t *value_size) const = 0; + template + inline bool get_annotation_string_from_technique(effect_technique technique, const char *name, char(&value)[SIZE]) const + { + size_t value_size = SIZE; + return get_annotation_string_from_technique(technique, name, value, &value_size); + } + + /// + /// Gets the state of a . + /// + /// Opaque handle to the technique. + virtual bool get_technique_state(effect_technique technique) const = 0; + /// + /// Enables or disables the specified . + /// + /// Opaque handle to the technique. + /// Set to to enable the technique, or to disable it. + virtual void set_technique_state(effect_technique technique, bool enabled) = 0; + + /// + /// Gets the value of a preprocessor definition. + /// + /// Name of the definition. + /// Pointer to a string buffer that is filled with the value of the definition, or to query the necessary size. + /// Pointer to an integer that contains the size of the string buffer and is set to the actual length of the string, including the null-terminator. + virtual bool get_preprocessor_definition(const char *name, char *value, size_t *value_size) const = 0; + template + inline bool get_preprocessor_definition(const char *name, char(&value)[SIZE]) const + { + size_t value_size = SIZE; + return get_preprocessor_definition(name, value, &value_size); + } + /// + /// Defines a preprocessor definition to the specified . + /// + /// Name of the definition. + /// Value of the definition. + virtual void set_preprocessor_definition(const char *name, const char *value) = 0; + + /// + /// Applies a to the specified render targets (regardless of the state of this technique). + /// + /// + /// The width and height of the specified render target should match those used to render all other effects! + /// The resource the render target views point to has to be in the state. + /// This call may modify current state on the command list (pipeline, render targets, descriptor tables, ...), so it may be necessary for an add-on to backup and restore state around it if the application does not bind all state again afterwards already. + /// + /// Opaque handle to the technique. + /// Command list to add effect rendering commands to. + /// Render target view to use for passes that write to the back buffer with SRGBWriteEnabled state set to . + /// Render target view to use for passes that write to the back buffer with SRGBWriteEnabled state set to , or zero in which case the view from is used. + virtual void render_technique(effect_technique technique, command_list *cmd_list, resource_view rtv, resource_view rtv_srgb = { 0 }) = 0; + + /// + /// Gets whether effects are enabled or disabled. + /// + virtual bool get_effects_state() const = 0; + /// + /// Enables or disables all effects. + /// + /// Set to to enable effects, or to disable them. + virtual void set_effects_state(bool enabled) = 0; + + /// + /// Gets the file path to the currently active preset. + /// + /// Pointer to a string buffer that is filled with the file path to the preset, or to query the necessary size. + /// Pointer to an integer that contains the size of the string buffer and is set to the actual length of the string, including the null-terminator. + virtual void get_current_preset_path(char *path, size_t *path_size) const = 0; + template + inline void get_current_preset_path(char(&path)[SIZE]) const + { + size_t path_size = SIZE; + get_current_preset_path(path, &path_size); + } + /// + /// Saves the currently active preset and then switches to the specified new preset. + /// + /// File path to the preset to switch to. + virtual void set_current_preset_path(const char *path) = 0; + + /// + /// Changes the rendering order of loaded techniques to that of the specified technique list. + /// + /// Number of handles in the technique list. + /// Array of techniques in the order they should be rendered in. + virtual void reorder_techniques(size_t count, const effect_technique *techniques) = 0; + + /// + /// Makes ReShade block any keyboard and mouse input from reaching the game for the duration of the next frame. + /// Call this every frame for as long as input should be blocked. This can be used to ensure input is only applied to overlays created in a callback. + /// + virtual void block_input_next_frame() = 0; + + /// + /// Gets the virtual key code of the last key that was pressed. + /// + virtual uint32_t last_key_pressed() const = 0; + /// + /// Gets the virtual key code of the last key that was released. + /// + virtual uint32_t last_key_released() const = 0; + + /// + /// Gets the effect file name of a uniform . + /// + /// Opaque handle to the uniform variable. + /// Pointer to a string buffer that is filled with the effect file name of the uniform variable, or to query the necessary size. + /// Pointer to an integer that contains the size of the string buffer and is set to the actual length of the string, including the null-terminator. + virtual void get_uniform_variable_effect_name(effect_uniform_variable variable, char *effect_name, size_t *effect_name_size) const = 0; + template + inline void get_uniform_variable_effect_name(effect_uniform_variable variable, char(&effect_name)[SIZE]) const + { + size_t effect_name_size = SIZE; + get_uniform_variable_effect_name(variable, effect_name, &effect_name_size); + } + + /// + /// Gets the effect file name of a texture . + /// + /// Opaque handle to the texture variable. + /// Pointer to a string buffer that is filled with the effect file name of the texture variable, or to query the necessary size. + /// Pointer to an integer that contains the size of the string buffer and is set to the actual length of the string, including the null-terminator. + virtual void get_texture_variable_effect_name(effect_texture_variable variable, char *effect_name, size_t *effect_name_size) const = 0; + template + inline void get_texture_variable_effect_name(effect_texture_variable variable, char(&effect_name)[SIZE]) const + { + size_t effect_name_size = SIZE; + get_texture_variable_effect_name(variable, effect_name, &effect_name_size); + } + + /// + /// Gets the effect file name of a . + /// + /// Opaque handle to the technique. + /// Pointer to a string buffer that is filled with the effect file name of the technique, or to query the necessary size. + /// Pointer to an integer that contains the size of the string buffer and is set to the actual length of the string, including the null-terminator. + virtual void get_technique_effect_name(effect_technique technique, char *effect_name, size_t *effect_name_size) const = 0; + template + inline void get_technique_effect_name(effect_technique technique, char(&effect_name)[SIZE]) const + { + size_t effect_name_size = SIZE; + get_technique_effect_name(technique, effect_name, &effect_name_size); + } + + /// + /// Saves the current preset with the current state of the loaded techniques and uniform variables. + /// + virtual void save_current_preset() const = 0; + + /// + /// Gets the value of a preprocessor definition for the specified effect. + /// + /// File name of the effect file the preprocessor definition is defined for. + /// Name of the definition. + /// Pointer to a string buffer that is filled with the value of the definition, or to query the necessary size. + /// Pointer to an integer that contains the size of the string buffer and upon completion is set to the actual length of the string, including the null-terminator. + virtual bool get_preprocessor_definition_for_effect(const char *effect_name, const char *name, char *value, size_t *value_size) const = 0; + template + inline bool get_preprocessor_definition_for_effect(const char *effect_name, const char *name, char(&value)[SIZE]) const + { + size_t value_size = SIZE; + return get_preprocessor_definition_for_effect(effect_name, name, value, &value_size); + } + /// + /// Defines a preprocessor definition for the specified effect to the specified . + /// + /// File name of the effect file the preprocessor definition should be defined for. + /// Name of the definition. + /// Value of the definition. + virtual void set_preprocessor_definition_for_effect(const char *effect_name, const char *name, const char *value) = 0; + }; +} } diff --git a/Plugin/include/reshade/reshade_api_device.hpp b/Plugin/include/reshade/reshade_api_device.hpp new file mode 100644 index 00000000..92f8c3b3 --- /dev/null +++ b/Plugin/include/reshade/reshade_api_device.hpp @@ -0,0 +1,1017 @@ +/* + * Copyright (C) 2021 Patrick Mours + * SPDX-License-Identifier: BSD-3-Clause OR MIT + */ + +#pragma once + +#include "reshade_api_pipeline.hpp" + +namespace reshade { namespace api +{ + /// + /// The underlying render API a device is using, as returned by . + /// + enum class device_api + { + /// Direct3D 9 + /// https://docs.microsoft.com/windows/win32/direct3d9 + d3d9 = 0x9000, + /// Direct3D 10 + /// https://docs.microsoft.com/windows/win32/direct3d10 + d3d10 = 0xa000, + /// Direct3D 11 + /// https://docs.microsoft.com/windows/win32/direct3d11 + d3d11 = 0xb000, + /// Direct3D 12 + /// https://docs.microsoft.com/windows/win32/direct3d12 + d3d12 = 0xc000, + /// OpenGL + /// https://www.khronos.org/opengl/ + opengl = 0x10000, + /// Vulkan + /// https://www.khronos.org/vulkan/ + vulkan = 0x20000 + }; + + /// + /// The available features a device may support. + /// + enum class device_caps + { + /// + /// Specifies whether compute shaders are supported. + /// If this feature is not present, the stage and must not be used. + /// + compute_shader = 1, + /// + /// Specifies whether geometry shaders are supported. + /// If this feature is not present, the stage must not be used. + /// + geometry_shader, + /// + /// Specifies whether hull and domain (tessellation) shaders are supported. + /// If this feature is not present, the and stages must not be used. + /// + hull_and_domain_shader, + /// + /// Specifies whether logic operations are available in the blend state. + /// If this feature is not present, the and fields are ignored. + /// + logic_op, + /// + /// Specifies whether blend operations which take two sources are supported. + /// If this feature is not present, , , and must not be used. + /// + dual_source_blend, + /// + /// Specifies whether blend state is controlled independently per render target. + /// If this feature is not present, the blend state settings for all render targets must be identical. + /// + independent_blend, + /// + /// Specifies whether point and wireframe fill modes are supported. + /// If this feature is not present, and must not be used. + /// + fill_mode_non_solid, + /// + /// Specifies whether conservative rasterization is supported. + /// If this feature is not present, the field must be 0. + /// + conservative_rasterization, + /// + /// Specifies whether binding individual render target and depth-stencil resource views is supported. + /// If this feature is not present, must not be used (only render passes). + /// + bind_render_targets_and_depth_stencil, + /// + /// Specifies whther more than one viewport is supported. + /// If this feature is not present, the "first" and "count" parameters to and must be 0 and 1. + /// + multi_viewport, + /// + /// Specifies whether partial push constant updates are supported. + /// If this feature is not present, the "first" parameter to must be 0 and "count" must cover the entire constant range. + /// + partial_push_constant_updates, + /// + /// Specifies whether partial push descriptor updates are supported. + /// If this feature is not present, the "first" parameter to must be 0 and "count" must cover the entire descriptor range. + /// + partial_push_descriptor_updates, + /// + /// Specifies whether instancing is supported. + /// If this feature is not present, the "instance_count" and "first_instance" parameters to and must be 1 and 0. + /// + draw_instanced, + /// + /// Specifies whether indirect draw or dispatch calls are supported. + /// If this feature is not present, must not be used. + /// + draw_or_dispatch_indirect, + /// + /// Specifies whether copying between buffers is supported. + /// If this feature is not present, must not be used. + /// + copy_buffer_region, + /// + /// Specifies whether copying between buffers and textures is supported. + /// If this feature is not present, and must not be used. + /// + copy_buffer_to_texture, + /// + /// Specifies whether blitting between resources is supported. + /// If this feature is not present, the "source_box" and "dest_box" parameters to must have the same dimensions. + /// + blit, + /// + /// Specifies whether resolving a region of a resource rather than its entirety is supported. + /// If this feature is not present, the "source_box", "dest_x", "dest_y" and "dest_z" parameters to must be and zero. + /// + resolve_region, + /// + /// Specifies whether copying query results to a buffer is supported. + /// If this feature is not present, must not be used. + /// + copy_query_heap_results, + /// + /// Specifies whether comparison sampling is supported. + /// If this feature is not present, the field is ignored and the compare filter types have no effect. + /// + sampler_compare, + /// + /// Specifies whether anisotropic filtering is supported. + /// If this feature is not present, must not be used. + /// + sampler_anisotropic, + /// + /// Specifies whether combined sampler and resource view descriptors are supported. + /// If this feature is not present, must not be used. + /// + sampler_with_resource_view, + /// + /// Specifies whether resource sharing is supported. + /// If this feature is not present, must not be used. + /// + shared_resource, + /// + /// Specifies whether resource sharing with NT handles is supported. + /// If this feature is not present, must not be used. + /// + shared_resource_nt_handle + }; + + /// + /// The base class for objects provided by the ReShade API. + /// This lets you store and retrieve custom data with objects, e.g. to be able to communicate persistent information between event callbacks. + /// + struct __declspec(novtable) api_object + { + /// + /// Gets the underlying native object for this API object. + /// + /// + /// For this will be be a pointer to a 'IDirect3DDevice9', 'ID3D10Device', 'ID3D11Device' or 'ID3D12Device' object or a 'HGLRC' or 'VkDevice' handle.
+ /// For this will be a pointer to a 'ID3D11DeviceContext' (when recording), 'ID3D11CommandList' (when executing) or 'ID3D12GraphicsCommandList' object or a 'VkCommandBuffer' handle.
+ /// For this will be a pointer to a 'ID3D11DeviceContext' or 'ID3D12CommandQueue' object or a 'VkQueue' handle.
+ /// For this will be a pointer to a 'IDirect3DSwapChain9' or 'IDXGISwapChain' object or a 'HDC' or 'VkSwapchainKHR' handle. + ///
+ virtual uint64_t get_native() const = 0; + + /// + /// Gets a user-defined 64-bit value from the object that was previously set via , or zero if none associated with the specified exists. + /// + virtual void get_private_data(const uint8_t guid[16], uint64_t *data) const = 0; + /// + /// Stores a user-defined 64-bit value in the object and associates it with the specified . + /// + /// + /// This function may NOT be called concurrently from multiple threads! + /// + virtual void set_private_data(const uint8_t guid[16], const uint64_t data) = 0; + + /// + /// Gets a reference to user-defined data from the object that was previously allocated via . + /// + template inline T &get_private_data() const + { + uint64_t res; + get_private_data(reinterpret_cast(&__uuidof(T)), &res); + return *reinterpret_cast(static_cast(res)); + } + /// + /// Allocates user-defined data and stores it in the object. + /// + template inline T &create_private_data() + { + uint64_t res = reinterpret_cast(new T()); + set_private_data(reinterpret_cast(&__uuidof(T)), res); + return *reinterpret_cast(static_cast(res)); + } + /// + /// Frees user-defined data that was previously allocated via . + /// + template inline void destroy_private_data() + { + uint64_t res; + get_private_data(reinterpret_cast(&__uuidof(T)), &res); + delete reinterpret_cast(static_cast(res)); + set_private_data(reinterpret_cast(&__uuidof(T)), 0); + } + }; + + /// + /// A logical render device, used for resource creation and global operations. + /// Functionally equivalent to a 'IDirect3DDevice9', 'ID3D10Device', 'ID3D11Device', 'ID3D12Device', 'HGLRC' or 'VkDevice'. + /// + /// + /// This class is safe to use concurrently from multiple threads in D3D10+ and Vulkan. + /// + struct __declspec(novtable) device : public api_object + { + /// + /// Gets the underlying graphics API used by this device. + /// + virtual device_api get_api() const = 0; + + /// + /// Checks whether the device supports the specified . + /// + virtual bool check_capability(device_caps capability) const = 0; + /// + /// Checks whether the specified texture supports the specified . + /// + virtual bool check_format_support(format format, resource_usage usage) const = 0; + + /// + /// Creates a new sampler state object. + /// + /// Description of the sampler to create. + /// Pointer to a variable that is set to the handle of the created sampler. + /// if the sampler was successfully created, otherwise (in this case is set to zero). + virtual bool create_sampler(const sampler_desc &desc, sampler *out_handle) = 0; + /// + /// Instantly destroys a sampler that was previously created via . + /// + virtual void destroy_sampler(sampler handle) = 0; + + /// + /// Allocates and creates a new resource. + /// + /// Description of the resource to create. + /// Optional data to upload to the resource after creation. This should point to an array of , one for each subresource (mipmap levels and array layers). Can be to indicate no initial data to upload. + /// Initial state of the resource after creation. This can later be changed via . + /// Pointer to a variable that is set to the handle of the created resource. + /// Optional pointer to a variable of type HANDLE used when contains . When that variable is a , it is set to the exported shared handle of the created resource. When that variable is a valid handle, the resource is imported from that shared handle. + /// if the resource was successfully created, otherwise (in this case is set to zero). + virtual bool create_resource(const resource_desc &desc, const subresource_data *initial_data, resource_usage initial_state, resource *out_handle, void **shared_handle = nullptr) = 0; + /// + /// Instantly destroys a resource that was previously created via and frees its memory. + /// Make sure the resource is no longer in use on the GPU (via any command list that may reference it and is still being executed) before doing this and never try to destroy resources created by the application! + /// + virtual void destroy_resource(resource handle) = 0; + + /// + /// Gets the description of the specified resource. + /// + virtual resource_desc get_resource_desc(resource resource) const = 0; + + /// + /// Creates a new resource view for the specified . + /// + /// Resource to create the view to. + /// Usage type of the resource view to create. Set to to create a shader resource view, for a depth-stencil view, for a render target etc. + /// Description of the resource view to create. + /// Pointer to a variable that is set to the handle of the created resource view. + /// if the resource view was successfully created, otherwise (in this case is set to zero). + virtual bool create_resource_view(resource resource, resource_usage usage_type, const resource_view_desc &desc, resource_view *out_handle) = 0; + /// + /// Instantly destroys a resource view that was previously created via . + /// + virtual void destroy_resource_view(resource_view handle) = 0; + + /// + /// Gets the handle to the underlying resource the specified resource was created for. + /// + /// + /// Resource views may be created without a resource in D3D12, which is used to initialize a null descriptor (reading zeroes, writes are discarded). This may therefore return zero for such views. + /// + virtual resource get_resource_from_view(resource_view view) const = 0; + /// + /// Gets the description of the specified resource view. + /// + virtual resource_view_desc get_resource_view_desc(resource_view view) const = 0; + + /// + /// Maps the memory of a buffer resource into application address space. + /// + /// Buffer resource to map to host memory. + /// Offset (in bytes) into the buffer resource to start mapping. + /// Number of bytes to map. Set to -1 (UINT64_MAX) to indicate that the entire buffer should be mapped. + /// Hint on how the returned data pointer will be accessed. + /// Pointer to a variable that is set to a pointer to the memory of the buffer resource. + /// if the memory of the buffer resource was successfully mapped, otherwise (in this case is set to ). + virtual bool map_buffer_region(resource resource, uint64_t offset, uint64_t size, map_access access, void **out_data) = 0; + /// + /// Unmaps a previously mapped buffer resource. + /// + /// Buffer resource to unmap from host memory. + virtual void unmap_buffer_region(resource resource) = 0; + /// + /// Maps the memory of a texture resource into application address space. + /// + /// Texture resource to map to host memory. + /// Index of the subresource to map (level + (layer * levels)). + /// Optional 3D box (or to reference the entire subresource) that defines the region in the to map. + /// Hint on how the returned data pointer will be accessed. + /// Pointer to a variable that is set to a pointer to the memory of the texture resource and optionally the row and slice pitch of that data (depending on the resource type). + /// if the memory of the texture resource was successfully mapped, otherwise (in this case is set to ). + virtual bool map_texture_region(resource resource, uint32_t subresource, const subresource_box *box, map_access access, subresource_data *out_data) = 0; + /// + /// Unmaps a previously mapped texture resource. + /// + /// Texture resource to unmap from host memory. + /// Index of the subresource to unmap (level + (layer * levels)). + virtual void unmap_texture_region(resource resource, uint32_t subresource) = 0; + + /// + /// Uploads data to a buffer resource. + /// + /// Pointer to the data to upload. + /// Buffer resource to upload to. + /// Offset (in bytes) into the buffer resource to start uploading to. + /// Number of bytes to upload. + virtual void update_buffer_region(const void *data, resource resource, uint64_t offset, uint64_t size) = 0; + /// + /// Uploads data to a texture resource. + /// + /// Pointer to the data to upload. + /// Texture resource to upload to. + /// Index of the subresource to upload to (level + (layer * levels)). + /// Optional 3D box (or to reference the entire subresource) that defines the region in the to upload to. + virtual void update_texture_region(const subresource_data &data, resource resource, uint32_t subresource, const subresource_box *box = nullptr) = 0; + + /// + /// Creates a new pipeline state object. + /// + /// Pipeline layout to use. + /// Number of sub-objects. + /// Pointer to an array of sub-objects that describe this pipeline. + /// Pointer to a variable that is set to the handle of the created pipeline state object. + /// if the pipeline state object was successfully created, otherwise (in this case is set to zero). + virtual bool create_pipeline(pipeline_layout layout, uint32_t subobject_count, const pipeline_subobject *subobjects, pipeline *out_handle) = 0; + /// + /// Instantly destroys a pipeline state object that was previously created via . + /// + virtual void destroy_pipeline(pipeline handle) = 0; + + /// + /// Creates a new pipeline layout. + /// + /// Number of layout parameters. + /// Pointer to an array of layout parameters that describe this pipeline layout. + /// Pointer to a variable that is set to the handle of the created pipeline layout. + /// if the pipeline layout was successfully created, otherwise (in this case is set to zero). + virtual bool create_pipeline_layout(uint32_t param_count, const pipeline_layout_param *params, pipeline_layout *out_handle) = 0; + /// + /// Instantly destroys a pipeline layout that was previously created via . + /// + virtual void destroy_pipeline_layout(pipeline_layout handle) = 0; + + /// + /// Allocates a descriptor table from an internal heap. + /// + /// Pipeline layout that contains a parameter that describes the descriptor table. + /// Index of the pipeline layout parameter that describes the descriptor table. + /// Pointer to a a variable that is set to the handles of the created descriptor table. + /// if the descriptor table was successfully allocated, otherwise (in this case is set to zeroe). + inline bool allocate_descriptor_table(pipeline_layout layout, uint32_t param, descriptor_table *out_handle) { return allocate_descriptor_tables(1, layout, param, out_handle); } + /// + /// Allocates one or more descriptor tables from an internal heap. + /// + /// Number of descriptor tables to allocate. + /// Pipeline layout that contains a parameter that describes the descriptor table. + /// Index of the pipeline layout parameter that describes the descriptor table. + /// Pointer to an array of handles with at least elements that is filled with the handles of the created descriptor tables. + /// if the descriptor tables were successfully allocated, otherwise (in this case is filled with zeroes). + virtual bool allocate_descriptor_tables(uint32_t count, pipeline_layout layout, uint32_t param, descriptor_table *out_handles) = 0; + /// + /// Frees a descriptor table that was previously allocated via . + /// + inline void free_descriptor_table(descriptor_table handle) { free_descriptor_tables(1, &handle); } + /// + /// Frees one or more descriptor tables that were previously allocated via . + /// + virtual void free_descriptor_tables(uint32_t count, const descriptor_table *handles) = 0; + + /// + /// Gets the offset (in descriptors) of the specified binding in the underlying descriptor heap of a descriptor table. + /// + /// Descriptor table to get the offset from. + /// Binding in the descriptor table to get the offset from. + /// Array index in the specified . + /// Pointer to a variable that is set to the handle of the underlying descriptor heap the was allocated from. + /// Pointer to a variable that is set to the offset of the binding in the underlying descriptor heap. + virtual void get_descriptor_heap_offset(descriptor_table table, uint32_t binding, uint32_t array_offset, descriptor_heap *out_heap, uint32_t *out_offset) const = 0; + + /// + /// Copies the contents of a descriptor table to another descriptor table. + /// + /// Descriptor table copy to process. + inline void copy_descriptors(const descriptor_table_copy ©) { copy_descriptor_tables(1, ©); } + /// + /// Copies the contents between multiple descriptor tables. + /// + /// Number of to process. + /// Pointer to an array of descriptor table copies to process. + virtual void copy_descriptor_tables(uint32_t count, const descriptor_table_copy *copies) = 0; + /// + /// Updates the contents of a descriptor table with the specified descriptors. + /// + /// Descriptor table update to process. + inline void update_descriptors(const descriptor_table_update &update) { update_descriptor_tables(1, &update); } + /// + /// Updates the contents of multiple descriptor tables with the specified descriptors. + /// + /// Number of to process. + /// Pointer to an array of descriptor table updates to process. + virtual void update_descriptor_tables(uint32_t count, const descriptor_table_update *updates) = 0; + + /// + /// Creates a new query heap. + /// + /// Type of queries that will be used with this query heap. + /// Number of queries to allocate in the query heap. + /// Pointer to a variable that is set to the handle of the created query heap. + /// if the query heap was successfully created, otherwise (in this case is set to zero). + virtual bool create_query_heap(query_type type, uint32_t size, query_heap *out_handle) = 0; + /// + /// Instantly destroys a query heap that was previously created via . + /// + virtual void destroy_query_heap(query_heap handle) = 0; + + /// + /// Gets the results of queries in a query heap. + /// + /// Query heap that contains the queries. + /// Index of the first query in the query heap to copy the results from. + /// Number of query results to copy. + /// Pointer to an array that is filled with the results. + /// Size (in bytes) of each element in the array. + /// if the query results were successfully downloaded from the GPU, otherwise. + virtual bool get_query_heap_results(query_heap heap, uint32_t first, uint32_t count, void *results, uint32_t stride) = 0; + + /// + /// Associates a name with a resource, for easier debugging in external tools. + /// + /// Resource to associate a name with. + /// Null-terminated name string. + virtual void set_resource_name(resource handle, const char *name) = 0; + /// + /// Associates a name with a resource view, for easier debugging in external tools. + /// + /// Resource view to associate a name with. + /// Null-terminated name string. + virtual void set_resource_view_name(resource_view handle, const char *name) = 0; + }; + + /// + /// The base class for objects that are children to a logical render . + /// + struct __declspec(novtable) device_object : public api_object + { + /// + /// Gets the parent device for this object. + /// + virtual device *get_device() = 0; + }; + + /// + /// The available indirect command types. + /// + enum class indirect_command + { + unknown, + draw, + draw_indexed, + dispatch + }; + + /// + /// A command list, used to enqueue render commands on the CPU, before later executing them in a command queue. + /// Functionally equivalent to a 'ID3D11CommandList', 'ID3D12CommandList' or 'VkCommandBuffer'. + /// + /// + /// This class may NOT be used concurrently from multiple threads! + /// + struct __declspec(novtable) command_list : public device_object + { + /// + /// Adds a barrier for the specified to the command stream. + /// When both and are a UAV barrier is added, otherwise a state transition is performed. + /// + /// Resource to transition. + /// Usage flags describing how the was used before this barrier. + /// Usage flags describing how the will be used after this barrier. + inline void barrier(resource resource, resource_usage old_state, resource_usage new_state) { barrier(1, &resource, &old_state, &new_state); } + /// + /// Adds a barrier for the specified to the command stream. + /// + /// Number of resources to transition. + /// Pointer to an array of resources to transition. + /// Pointer to an array of usage flags describing how the were used before this barrier. + /// Pointer to an array of usage flags describing how the will be used after this barrier. + virtual void barrier(uint32_t count, const resource *resources, const resource_usage *old_states, const resource_usage *new_states) = 0; + + /// + /// Begins a render pass and binds render target and depth-stencil resource views. + /// + /// Number of render target views to bind. + /// Pointer to an array of render target descriptions. + /// Optional pointer to a depth-stencil description, or to bind none. + virtual void begin_render_pass(uint32_t count, const render_pass_render_target_desc *rts, const render_pass_depth_stencil_desc *ds = nullptr) = 0; + /// + /// Ends a render pass. + /// This must be preceeded by a call to . + /// Render passes cannot be nested. + /// + virtual void end_render_pass() = 0; + /// + /// Binds individual render target and depth-stencil resource views. + /// This must not be called between and . + /// + /// + /// Number of render target views to bind. + /// Pointer to an array of render target views to bind. + /// Depth-stencil view to bind, or zero to bind none. + virtual void bind_render_targets_and_depth_stencil(uint32_t count, const resource_view *rtvs, resource_view dsv = { 0 }) = 0; + + /// + /// Binds a pipeline state object. + /// + /// Pipeline stages to update with state from the pipeline state object. + /// Pipeline state object to bind. + virtual void bind_pipeline(pipeline_stage stages, pipeline pipeline) = 0; + /// + /// Updates the specfified pipeline to the specified . + /// This is only valid for states that have been listed in the dynamic states provided at creation of the currently bound pipeline state object (). + /// + /// Pipeline state to update. + /// Value to update the pipeline state to. + inline void bind_pipeline_state(dynamic_state state, uint32_t value) { bind_pipeline_states(1, &state, &value); } + /// + /// Updates the specfified pipeline to the specified . + /// This is only valid for states that have been listed in the dynamic states provided at creation of the currently bound pipeline state object (). + /// + /// Number of pipeline states to update. + /// Pointer to an array of pipeline states to update. + /// Pointer to an array of values to update the pipeline states to, with one for each state in . + virtual void bind_pipeline_states(uint32_t count, const dynamic_state *states, const uint32_t *values) = 0; + /// + /// Binds an array of viewports to the rasterizer stage. + /// + /// + /// Index of the first viewport to bind. In D3D9, D3D10, D3D11 and D3D12 this has to be 0. + /// Number of viewports to bind. + /// Pointer to an array of viewports. + virtual void bind_viewports(uint32_t first, uint32_t count, const viewport *viewports) = 0; + /// + /// Binds an array of scissor rectangles to the rasterizer stage. + /// + /// + /// Index of the first scissor rectangle to bind. In D3D9, D3D10, D3D11 and D3D12 this has to be 0. + /// Number of scissor rectangles to bind. + /// Pointer to an array of scissor rectangles. + virtual void bind_scissor_rects(uint32_t first, uint32_t count, const rect *rects) = 0; + + /// + /// Directly updates constant values in the specified shader pipeline stages. + /// In D3D9 this updates the values of uniform registers, in D3D10/11 and OpenGL the constant buffer specified in the pipeline layout, in D3D12 it sets root constants and in Vulkan push constants. + /// + /// + /// Shader stages that will use the updated constants. + /// Pipeline layout that describes where the constants are located. + /// Layout parameter index of the constant range in the pipeline (root parameter index in D3D12). + /// Start offset (in 32-bit values) to the first constant in the constant range to begin updating. + /// Number of 32-bit values to update. + /// Pointer to an array of 32-bit values to set the constants to. These can be floating-point, integer or boolean depending on what the shader is expecting. + virtual void push_constants(shader_stage stages, pipeline_layout layout, uint32_t param, uint32_t first, uint32_t count, const void *values) = 0; + /// + /// Directly binds a temporary descriptor table for the specfified shader pipeline stage and updates with an array of descriptors. + /// + /// + /// Shader stages that will use the updated descriptors. + /// Pipeline layout that describes the descriptors. + /// Layout parameter index of the descriptor table in the pipeline (root parameter index in D3D12, descriptor set index in Vulkan). + /// Range of descriptors to update in the temporary descriptor table ( is ignored). + virtual void push_descriptors(shader_stage stages, pipeline_layout layout, uint32_t param, const descriptor_table_update &update) = 0; + /// + /// Binds a single descriptor table. + /// + /// Shader stages that will use the descriptors. + /// Pipeline layout that describes the descriptors. + /// Index of the pipeline parameter that describes the descriptor table (root parameter index in D3D12, descriptor set index in Vulkan). + /// Descriptor table to bind. + inline void bind_descriptor_table(shader_stage stages, pipeline_layout layout, uint32_t param, descriptor_table table) { bind_descriptor_tables(stages, layout, param, 1, &table); } + /// + /// Binds an array of descriptor tables. + /// + /// Shader stages that will use the descriptors. + /// Pipeline layout that describes the descriptors. + /// Index of the first pipeline parameter that describes the first descriptor table to bind (root parameter index in D3D12, descriptor set index in Vulkan). + /// Number of descriptor tables to bind. + /// Pointer to an array of descriptor tables to bind. + virtual void bind_descriptor_tables(shader_stage stages, pipeline_layout layout, uint32_t first, uint32_t count, const descriptor_table *tables) = 0; + + /// + /// Binds an index buffer to the input-assembler stage. + /// + /// Index buffer resource. This resource must have been created with the usage. + /// Offset (in bytes) from the start of the index buffer to the first index to use. In D3D9 this has to be 0. + /// Size (in bytes) of each index. Can typically be 2 (16-bit indices) or 4 (32-bit indices). + virtual void bind_index_buffer(resource buffer, uint64_t offset, uint32_t index_size) = 0; + /// + /// Binds a single vertex buffer to the input-assembler stage. + /// + /// Input slot for binding. + /// Vertex buffer resource. This resources must have been created with the usage. + /// Offset (in bytes) from the start of the vertex buffer to the first vertex element to use. + /// Size (in bytes) of the vertex element that will be used from the vertex buffer (is added to an element offset to advance to the next). + inline void bind_vertex_buffer(uint32_t index, resource buffer, uint64_t offset, uint32_t stride) { bind_vertex_buffers(index, 1, &buffer, &offset, &stride); } + /// + /// Binds an array of vertex buffers to the input-assembler stage. + /// + /// First input slot for binding. + /// Number of vertex buffers to bind. + /// Pointer to an array of vertex buffer resources. These resources must have been created with the usage. + /// Pointer to an array of offset values, one for each buffer. Each offset is the number of bytes from the start of the vertex buffer to the first vertex element to use. + /// Pointer to an array of stride values, one for each buffer. Each stride is the size (in bytes) of the vertex element that will be used from that vertex buffer (is added to an element offset to advance to the next). + virtual void bind_vertex_buffers(uint32_t first, uint32_t count, const resource *buffers, const uint64_t *offsets, const uint32_t *strides) = 0; + + /// + /// Binds an array of buffers to the stream-output stage. + /// + /// First stream-output slot for binding. + /// Number of stream-output targets to bind. + /// Pointer to an array of buffer resources. These resources must have been created with the usage. + /// Pointer to an array of offset values, one for each buffer. Each offset is the number of bytes from the start of the buffer to the first element to write to. + /// Optional pointer to an array of size values, one for each buffer. Can be or have elements set to UINT64_MAX to use the entire buffer. + /// Pointer to an array of counter buffer resources. These resources must have been created with the usage. + /// Pointer to an array of counter offset values, one for each counter buffer. Each offset is the number of bytes from the start of the counter buffer to the first element to write to. + virtual void bind_stream_output_buffers(uint32_t first, uint32_t count, const api::resource *buffers, const uint64_t *offsets, const uint64_t *max_sizes, const api::resource *counter_buffers, const uint64_t *counter_offsets) = 0; + + /// + /// Draws non-indexed primitives. + /// + /// + /// Number of vertices to draw. + /// Number of instances to draw. In D3D9 this has to be 1. + /// Index of the first vertex. + /// Value added to each index before reading per-instance data from a vertex buffer. In D3D9 this has to be 0. + virtual void draw(uint32_t vertex_count, uint32_t instance_count, uint32_t first_vertex, uint32_t first_instance) = 0; + /// + /// Draws indexed primitives. + /// + /// + /// Number of indices read from the index buffer for each instance. + /// Number of instances to draw. In D3D9 this has to be 1. + /// Location of the first index read from the index buffer. + /// Value added to each index before reading per-vertex data from a vertex buffer. + /// Value added to each index before reading per-instance data from a vertex buffer. In D3D9 this has to be 0. + virtual void draw_indexed(uint32_t index_count, uint32_t instance_count, uint32_t first_index, int32_t vertex_offset, uint32_t first_instance) = 0; + /// + /// Performs a compute shader dispatch. + /// + /// + /// Number of thread groups dispatched in the x direction. + /// Number of thread groups dispatched in the y direction. + /// Number of thread groups dispatched in the z direction. + virtual void dispatch(uint32_t group_count_x, uint32_t group_count_y, uint32_t group_count_z) = 0; + /// + /// Executes indirect draw or dispatch commands. + /// + /// + /// Specifies whether this is an indirect draw, indexed draw or dispatch command. + /// Buffer resource that contains command arguments. + /// Offset (in bytes) from the start of the argument buffer to the first argument to use. + /// Number of commands to execute. + /// Stride (in bytes) between commands in the argument buffer. + virtual void draw_or_dispatch_indirect(indirect_command type, resource buffer, uint64_t offset, uint32_t draw_count, uint32_t stride) = 0; + + /// + /// Copies the entire contents of the resource to the ination resource. Dimensions of the two resources have to match. + /// + /// + /// The resource has to be in the state. + /// The ination resource has to be in the state. + /// + /// Resource to copy from. + /// Resource to copy to. + virtual void copy_resource(resource source, resource dest) = 0; + /// + /// Copies a linear memory region from the buffer to the ination buffer. + /// + /// + /// The resource has to be in the state. + /// The ination resource has to be in the state. + /// + /// + /// Buffer resource to copy from. + /// Offset (in bytes) into the buffer to start copying at. + /// Buffer resource to copy to. + /// Offset (in bytes) into the ination buffer to start copying to. + /// Number of bytes to copy. + virtual void copy_buffer_region(resource source, uint64_t source_offset, resource dest, uint64_t dest_offset, uint64_t size) = 0; + /// + /// Copies a texture region from the buffer to the ination texture. + /// + /// + /// The resource has to be in the state. + /// The ination resource has to be in the state. + /// + /// + /// Buffer resource to copy from. + /// Offset (in bytes) into the buffer to start copying at. + /// Number of pixels from one row to the next (in the buffer), or zero if data is tightly packed. + /// Number of rows from one slice to the next (in the buffer) or zero if data is tightly packed. + /// Texture resource to copy to. + /// Index of the subresource of the ination texture to copy to. + /// Optional 3D box (or to reference the entire subresource) that defines the region in the ination texture to copy to. + virtual void copy_buffer_to_texture(resource source, uint64_t source_offset, uint32_t row_length, uint32_t slice_height, resource dest, uint32_t dest_subresource, const subresource_box *dest_box = nullptr) = 0; + /// + /// Copies or blits a texture region from the texture to the ination texture. + /// + /// + /// The resource has to be in the state. + /// The ination resource has to be in the state. + /// + /// + /// Texture resource to copy from. + /// Index of the subresource of the texture to copy from. + /// Optional 3D box (or to reference the entire subresource) that defines the region in the texture to blit from. + /// Texture resource to copy to. + /// Index of the subresource of the ination texture to copy to. + /// Optional 3D box (or to reference the entire subresource) that defines the region in the ination texture to blit to. + /// Filter to apply when copy requires scaling. + virtual void copy_texture_region(resource source, uint32_t source_subresource, const subresource_box *source_box, resource dest, uint32_t dest_subresource, const subresource_box *dest_box, filter_mode filter = filter_mode::min_mag_mip_point) = 0; + /// + /// Copies a texture region from the texture to the ination buffer. + /// + /// + /// The resource has to be in the state. + /// The ination resource has to be in the state. + /// + /// + /// Texture resource to copy from. + /// Index of the subresource of the texture to copy from. + /// Optional 3D box (or to reference the entire subresource) that defines the region in the texture to copy from. + /// Buffer resource to copy to. + /// Offset (in bytes) into the ination buffer to start copying to. + /// Number of pixels from one row to the next (in the buffer), or zero if data is tightly packed. + /// Number of rows from one slice to the next (in the buffer), or zero if data is tightly packed. + virtual void copy_texture_to_buffer(resource source, uint32_t source_subresource, const subresource_box *source_box, resource dest, uint64_t dest_offset, uint32_t row_length = 0, uint32_t slice_height = 0) = 0; + /// + /// Copies a region from the multisampled texture to the non-multisampled ination texture. + /// + /// + /// The resource has to be in the state. + /// The ination resource has to be in the state. + /// + /// + /// Texture resource to resolve from. + /// Index of the subresource of the texture to resolve from. + /// Optional 3D box (or to reference the entire subresource) that defines the region in the texture to resolve. + /// Texture resource to resolve to. + /// Index of the subresource of the ination texture to resolve to. + /// Optional X offset (in texels) that defines the region in the ination texture to resolve to. + /// Optional Y offset (in texels) that defines the region in the ination texture to resolve to. + /// Optional Z offset (in texels) that defines the region in the ination texture to resolve to. + /// Format of the resource data. + virtual void resolve_texture_region(resource source, uint32_t source_subresource, const subresource_box *source_box, resource dest, uint32_t dest_subresource, int32_t dest_x, int32_t dest_y, int32_t dest_z, format format) = 0; + + /// + /// Clears the resource referenced by the depth-stencil view. + /// + /// + /// The resource the depth-stencil view points to has to be in the state. + /// + /// Resource view handle of the depth-stencil. + /// Optional value to clear the depth buffer with. + /// Optional value to clear the stencil buffer with. + /// Number of rectangles to clear in the depth-stencil resource, or zero to clear the whole resource. + /// Pointer to an array of rectangles. + virtual void clear_depth_stencil_view(resource_view dsv, const float *depth, const uint8_t *stencil, uint32_t rect_count = 0, const rect *rects = nullptr) = 0; + /// + /// Clears the resource referenced by the render target view. + /// + /// + /// The resource the render target view points to has to be in the state. + /// + /// Resource view handle of the render target. + /// Value to clear the resource with. + /// Number of rectangles to clear in the render target resource, or zero to clear the whole resource. + /// Pointer to an array of rectangles. + virtual void clear_render_target_view(resource_view rtv, const float color[4], uint32_t rect_count = 0, const rect *rects = nullptr) = 0; + /// + /// Clears the resource referenced by the unordered access view. + /// + /// + /// The resource the unordered access view points to has to be in the state. + /// + /// Resource view handle of the unordered access view. + /// Value to clear the resource with. + /// Number of rectangles to clear in the unordered access resource, or zero to clear the whole resource. + /// Pointer to an array of rectangles. + virtual void clear_unordered_access_view_uint(resource_view uav, const uint32_t values[4], uint32_t rect_count = 0, const rect *rects = nullptr) = 0; + /// + /// Clears the resource referenced by the unordered access view. + /// + /// + /// The resource the unordered access view points to has to be in the state. + /// + /// Resource view handle of the unordered access view. + /// Value to clear the resource with. + /// Number of rectangles to clear in the unordered access resource, or zero to clear the whole resource. + /// Pointer to an array of rectangles. + virtual void clear_unordered_access_view_float(resource_view uav, const float values[4], uint32_t rect_count = 0, const rect *rects = nullptr) = 0; + + /// + /// Generates the lower mipmap levels for the specified shader resource view. + /// Uses the largest mipmap level of the view to recursively generate the lower levels of the mipmap chain and stops with the smallest level that is specified by the view. + /// + /// + /// This will invalidate all previous descriptor and pipeline bindings, which will need to be reset by calls to or and . + /// The resource the shader resource view points to has to be in the state and has to have been created with the flag. + /// + /// Shader resource view to update. + virtual void generate_mipmaps(resource_view srv) = 0; + + /// + /// Begins a query. + /// + /// Query heap that will manage the results of the query. + /// Type of the query to begin. + /// Index of the query in the query heap. + virtual void begin_query(query_heap heap, query_type type, uint32_t index) = 0; + /// + /// Ends a query. + /// + /// Query heap that will manage the results of the query. + /// Type of the query end. + /// Index of the query in the query heap. + virtual void end_query(query_heap heap, query_type type, uint32_t index) = 0; + /// + /// Copies the results of queries in a query heap to a buffer resource. + /// + /// + /// The ination resource has to be in the state. + /// + /// + /// Query heap that manages the results of the queries. + /// Type of the queries to copy. + /// Index of the first query in the query heap to copy the result from. + /// Number of query results to copy. + /// Buffer resource to copy to. + /// Offset (in bytes) into the ination buffer to start copying to. + /// Size (in bytes) of each result element. + virtual void copy_query_heap_results(query_heap heap, query_type type, uint32_t first, uint32_t count, resource dest, uint64_t dest_offset, uint32_t stride) = 0; + + /// + /// Opens a debug event region in the command list. + /// + /// Null-terminated string containing the label of the event. + /// Optional RGBA color value associated with the event. + virtual void begin_debug_event(const char *label, const float color[4] = nullptr) = 0; + /// + /// Closes the current debug event region (the last one opened with ). + /// + virtual void end_debug_event() = 0; + /// + /// Inserts a debug marker into the command list. + /// + /// Null-terminated string containing the label of the debug marker. + /// Optional RGBA color value associated with the debug marker. + virtual void insert_debug_marker(const char *label, const float color[4] = nullptr) = 0; + }; + + /// + /// A list of flags that represent the available command queue types, as returned by . + /// + enum class command_queue_type + { + graphics = 0x1, + compute = 0x2, + copy = 0x4 + }; + RESHADE_DEFINE_ENUM_FLAG_OPERATORS(command_queue_type); + + /// + /// A command queue, used to execute command lists on the GPU. + /// Functionally equivalent to the immediate 'ID3D11DeviceContext' or a 'ID3D12CommandQueue' or 'VkQueue'. + /// + /// + /// This class may NOT be used concurrently from multiple threads! + /// + struct __declspec(novtable) command_queue : public device_object + { + /// + /// Gets the type of the command queue, which specifies what commands can be executed on it. + /// + virtual command_queue_type get_type() const = 0; + + /// + /// Waits for all issued GPU operations on this queue to finish before returning. + /// This can be used to ensure that e.g. resources are no longer in use on the GPU before destroying them. + /// + virtual void wait_idle() const = 0; + + /// + /// Flushes and executes the special immediate command list returned by immediately. + /// This can be used to force commands to execute right away instead of waiting for the runtime to flush it automatically at some point. + /// + virtual void flush_immediate_command_list() const = 0; + + /// + /// Gets a special command list, on which all issued commands are executed as soon as possible (or right before the application executes its next command list on this queue). + /// This only exists on command queues that contain the flag, on other queues is returned. + /// + virtual command_list *get_immediate_command_list() = 0; + + /// + /// Opens a debug event region in the command queue. + /// + /// Null-terminated string containing the label of the event. + /// Optional RGBA color value associated with the event. + virtual void begin_debug_event(const char *label, const float color[4] = nullptr) = 0; + /// + /// Closes the current debug event region (the last one opened with ). + /// + virtual void end_debug_event() = 0; + /// + /// Inserts a debug marker into the command queue. + /// + /// Null-terminated string containing the label of the debug marker. + /// Optional RGBA color value associated with the debug marker. + virtual void insert_debug_marker(const char *label, const float color[4] = nullptr) = 0; + }; + + /// + /// Describes a swap chain and its back buffer resources. + /// + struct swapchain_desc + { + /// + /// Description of the back buffer resources. + /// + resource_desc back_buffer; + + /// + /// Number of back buffer resources in the swap chain. + /// + uint32_t back_buffer_count = 0; + + /// + /// Defines how the back buffers should be swapped when a present occurs. + /// Depending on the render API this can be a 'D3DSWAPEFFECT', 'DXGI_SWAP_EFFECT', 'WGL_SWAP_METHOD_ARB' or 'VkPresentModeKHR' value. + /// + uint32_t present_mode = 0; + + /// + /// Swap chain creation flags. + /// Depending on the render API this can be a 'D3DPRESENT', 'DXGI_PRESENT', 'PFD_*' or 'VkSwapchainCreateFlagsKHR' value. + /// + uint32_t present_flags = 0; + }; + + /// + /// A swap chain, used to present images to the screen. + /// Functionally equivalent to a 'IDirect3DSwapChain9', 'IDXGISwapChain', 'HDC' or 'VkSwapchainKHR'. + /// + struct __declspec(novtable) swapchain : public device_object + { + /// + /// Gets the handle of the window this swap chain was created with, or if this is an offscreen swap chain. + /// + virtual void *get_hwnd() const = 0; + + /// + /// Gets the back buffer resource at the specified in this swap chain. + /// + /// Index of the back buffer. This has to be between zero and the value returned by . + virtual resource get_back_buffer(uint32_t index) = 0; + + /// + /// Gets the number of back buffer resources in this swap chain. + /// + virtual uint32_t get_back_buffer_count() const = 0; + + /// + /// Gets the current back buffer resource. + /// + inline resource get_current_back_buffer() { return get_back_buffer(get_current_back_buffer_index()); } + /// + /// Gets the index of the back buffer resource that can currently be rendered into. + /// + virtual uint32_t get_current_back_buffer_index() const = 0; + }; +} } diff --git a/Plugin/include/reshade/reshade_api_format.hpp b/Plugin/include/reshade/reshade_api_format.hpp new file mode 100644 index 00000000..023b87e0 --- /dev/null +++ b/Plugin/include/reshade/reshade_api_format.hpp @@ -0,0 +1,460 @@ +/* + * Copyright (C) 2021 Patrick Mours + * SPDX-License-Identifier: BSD-3-Clause OR MIT + */ + +#pragma once + +#include +#include + +namespace reshade { namespace api +{ + /// + /// The available data and texture formats. + /// This is mostly compatible with 'DXGI_FORMAT'. + /// + enum class format : uint32_t + { + unknown = 0, + + // Color formats + + r1_unorm = 66, + l8_unorm = 0x3030384C, + a8_unorm = 65, + r8_typeless = 60, + r8_uint = 62, + r8_sint = 64, + r8_unorm = 61, + r8_snorm = 63, + l8a8_unorm = 0x3038414C, + r8g8_typeless = 48, + r8g8_uint = 50, + r8g8_sint = 52, + r8g8_unorm = 49, + r8g8_snorm = 51, + r8g8b8a8_typeless = 27, + r8g8b8a8_uint = 30, + r8g8b8a8_sint = 32, + r8g8b8a8_unorm = 28, + r8g8b8a8_unorm_srgb = 29, + r8g8b8a8_snorm = 31, + r8g8b8x8_unorm = 0x424757B9, + r8g8b8x8_unorm_srgb = 0x424757BA, + b8g8r8a8_typeless = 90, + b8g8r8a8_unorm = 87, + b8g8r8a8_unorm_srgb = 91, + b8g8r8x8_typeless = 92, + b8g8r8x8_unorm = 88, + b8g8r8x8_unorm_srgb = 93, + r10g10b10a2_typeless = 23, + r10g10b10a2_uint = 25, + r10g10b10a2_unorm = 24, + r10g10b10a2_xr_bias = 89, + b10g10r10a2_typeless = 0x42475330, + b10g10r10a2_uint = 0x42475332, + b10g10r10a2_unorm = 0x42475331, + l16_unorm = 0x3036314C, + r16_typeless = 53, + r16_uint = 57, + r16_sint = 59, + r16_unorm = 56, + r16_snorm = 58, + r16_float = 54, + l16a16_unorm = 0x3631414C, + r16g16_typeless = 33, + r16g16_uint = 36, + r16g16_sint = 38, + r16g16_unorm = 35, + r16g16_snorm = 37, + r16g16_float = 34, + r16g16b16a16_typeless = 9, + r16g16b16a16_uint = 12, + r16g16b16a16_sint = 14, + r16g16b16a16_unorm = 11, + r16g16b16a16_snorm = 13, + r16g16b16a16_float = 10, + r32_typeless = 39, + r32_uint = 42, + r32_sint = 43, + r32_float = 41, + r32g32_typeless = 15, + r32g32_uint = 17, + r32g32_sint = 18, + r32g32_float = 16, + r32g32b32_typeless = 5, + r32g32b32_uint = 7, + r32g32b32_sint = 8, + r32g32b32_float = 6, + r32g32b32a32_typeless = 1, + r32g32b32a32_uint = 3, + r32g32b32a32_sint = 4, + r32g32b32a32_float = 2, + r9g9b9e5 = 67, + r11g11b10_float = 26, + b5g6r5_unorm = 85, + b5g5r5a1_unorm = 86, + b5g5r5x1_unorm = 0x424757B5, + b4g4r4a4_unorm = 115, + a4b4g4r4_unorm = 191, + + // Depth-stencil formats + + s8_uint = 0x30303853, + d16_unorm = 55, + d16_unorm_s8_uint = 0x38363144, + d24_unorm_x8_uint = 0x38343244, + d24_unorm_s8_uint = 45, + d32_float = 40, + d32_float_s8_uint = 20, + + r24_g8_typeless = 44, + r24_unorm_x8_uint = 46, + x24_unorm_g8_uint = 47, + r32_g8_typeless = 19, + r32_float_x8_uint = 21, + x32_float_g8_uint = 22, + + // Compressed data formats + + bc1_typeless = 70, + bc1_unorm = 71, + bc1_unorm_srgb = 72, + bc2_typeless = 73, + bc2_unorm = 74, + bc2_unorm_srgb = 75, + bc3_typeless = 76, + bc3_unorm = 77, + bc3_unorm_srgb = 78, + bc4_typeless = 79, + bc4_unorm = 80, + bc4_snorm = 81, + bc5_typeless = 82, + bc5_unorm = 83, + bc5_snorm = 84, + bc6h_typeless = 94, + bc6h_ufloat = 95, + bc6h_sfloat = 96, + bc7_typeless = 97, + bc7_unorm = 98, + bc7_unorm_srgb = 99, + + // Video formats + + r8g8_b8g8_unorm = 68, + g8r8_g8b8_unorm = 69, + + // Special purpose formats + + intz = 0x5A544E49, + }; + + /// + /// The available color space types for presentation. + /// + enum class color_space : uint32_t + { + unknown = 0, + + srgb_nonlinear, + extended_srgb_linear, + hdr10_st2084, + hdr10_hlg, + }; + + /// + /// Converts the specified format to its equivalent typeless variant. + /// + /// The format to convert. + inline format format_to_typeless(format value) + { + switch (value) + { + case format::l8_unorm: + case format::r8_typeless: + case format::r8_uint: + case format::r8_sint: + case format::r8_unorm: + case format::r8_snorm: + return format::r8_typeless; + case format::l8a8_unorm: + case format::r8g8_typeless: + case format::r8g8_uint: + case format::r8g8_sint: + case format::r8g8_unorm: + case format::r8g8_snorm: + return format::r8g8_typeless; + case format::r8g8b8a8_typeless: + case format::r8g8b8a8_uint: + case format::r8g8b8a8_sint: + case format::r8g8b8a8_unorm: + case format::r8g8b8a8_unorm_srgb: + case format::r8g8b8a8_snorm: + case format::r8g8b8x8_unorm: + case format::r8g8b8x8_unorm_srgb: + return format::r8g8b8a8_typeless; + case format::b8g8r8a8_typeless: + case format::b8g8r8a8_unorm: + case format::b8g8r8a8_unorm_srgb: + return format::b8g8r8a8_typeless; + case format::b8g8r8x8_typeless: + case format::b8g8r8x8_unorm: + case format::b8g8r8x8_unorm_srgb: + return format::b8g8r8x8_typeless; + case format::r10g10b10a2_typeless: + case format::r10g10b10a2_uint: + case format::r10g10b10a2_unorm: + case format::r10g10b10a2_xr_bias: + return format::r10g10b10a2_typeless; + case format::b10g10r10a2_typeless: + case format::b10g10r10a2_uint: + case format::b10g10r10a2_unorm: + return format::b10g10r10a2_typeless; + case format::l16_unorm: + case format::d16_unorm: + case format::r16_typeless: + case format::r16_uint: + case format::r16_sint: + case format::r16_float: + case format::r16_unorm: + case format::r16_snorm: + return format::r16_typeless; + case format::l16a16_unorm: + case format::r16g16_typeless: + case format::r16g16_uint: + case format::r16g16_sint: + case format::r16g16_float: + case format::r16g16_unorm: + case format::r16g16_snorm: + return format::r16g16_typeless; + case format::r16g16b16a16_typeless: + case format::r16g16b16a16_uint: + case format::r16g16b16a16_sint: + case format::r16g16b16a16_float: + case format::r16g16b16a16_unorm: + case format::r16g16b16a16_snorm: + return format::r16g16b16a16_typeless; + case format::d32_float: + case format::r32_typeless: + case format::r32_uint: + case format::r32_sint: + case format::r32_float: + return format::r32_typeless; + case format::r32g32_typeless: + case format::r32g32_uint: + case format::r32g32_sint: + case format::r32g32_float: + return format::r32g32_typeless; + case format::r32g32b32_typeless: + case format::r32g32b32_uint: + case format::r32g32b32_sint: + case format::r32g32b32_float: + return format::r32g32b32_typeless; + case format::r32g32b32a32_typeless: + case format::r32g32b32a32_uint: + case format::r32g32b32a32_sint: + case format::r32g32b32a32_float: + return format::r32g32b32a32_typeless; + case format::d32_float_s8_uint: + case format::r32_g8_typeless: + case format::r32_float_x8_uint: + case format::x32_float_g8_uint: + return format::r32_g8_typeless; + case format::d24_unorm_s8_uint: // Do not also convert 'd24_unorm_x8_uint' here, to keep it distinguishable from 'd24_unorm_s8_uint' + case format::r24_g8_typeless: + case format::r24_unorm_x8_uint: + case format::x24_unorm_g8_uint: + return format::r24_g8_typeless; + case format::bc1_typeless: + case format::bc1_unorm: + case format::bc1_unorm_srgb: + return format::bc1_typeless; + case format::bc2_typeless: + case format::bc2_unorm: + case format::bc2_unorm_srgb: + return format::bc2_typeless; + case format::bc3_typeless: + case format::bc3_unorm: + case format::bc3_unorm_srgb: + return format::bc2_typeless; + case format::bc4_typeless: + case format::bc4_unorm: + case format::bc4_snorm: + return format::bc4_typeless; + case format::bc5_typeless: + case format::bc5_unorm: + case format::bc5_snorm: + return format::bc5_typeless; + case format::bc6h_typeless: + case format::bc6h_ufloat: + case format::bc6h_sfloat: + return format::bc6h_typeless; + case format::bc7_typeless: + case format::bc7_unorm: + case format::bc7_unorm_srgb: + return format::bc7_typeless; + default: + return value; + } + } + + /// + /// Converts the specified format to its equivalent typed variant ("unorm" or "float"). + /// + /// The format to convert. + /// Set to 1 to get sRGB variant, 0 to get linear variant and -1 to preserve existing one. + inline format format_to_default_typed(format value, int srgb_variant = -1) + { + switch (value) + { + case format::r8_typeless: + return format::r8_unorm; + case format::r8g8_typeless: + return format::r8g8_unorm; + case format::r8g8b8a8_typeless: + case format::r8g8b8a8_unorm: + return srgb_variant == 1 ? format::r8g8b8a8_unorm_srgb : format::r8g8b8a8_unorm; + case format::r8g8b8a8_unorm_srgb: + return srgb_variant != 0 ? format::r8g8b8a8_unorm_srgb : format::r8g8b8a8_unorm; + case format::r8g8b8x8_unorm: + return srgb_variant == 1 ? format::r8g8b8x8_unorm_srgb : format::r8g8b8x8_unorm; + case format::r8g8b8x8_unorm_srgb: + return srgb_variant != 0 ? format::r8g8b8x8_unorm_srgb : format::r8g8b8x8_unorm; + case format::b8g8r8a8_typeless: + case format::b8g8r8a8_unorm: + return srgb_variant == 1 ? format::b8g8r8a8_unorm_srgb : format::b8g8r8a8_unorm; + case format::b8g8r8a8_unorm_srgb: + return srgb_variant != 0 ? format::b8g8r8a8_unorm_srgb : format::b8g8r8a8_unorm; + case format::b8g8r8x8_typeless: + case format::b8g8r8x8_unorm: + return srgb_variant == 1 ? format::b8g8r8x8_unorm_srgb : format::b8g8r8x8_unorm; + case format::b8g8r8x8_unorm_srgb: + return srgb_variant != 0 ? format::b8g8r8x8_unorm_srgb : format::b8g8r8x8_unorm; + case format::r10g10b10a2_typeless: + return format::r10g10b10a2_unorm; + case format::b10g10r10a2_typeless: + return format::b10g10r10a2_unorm; + case format::d16_unorm: + case format::r16_typeless: + return format::r16_float; + case format::r16g16_typeless: + return format::r16g16_float; + case format::r16g16b16a16_typeless: + return format::r16g16b16a16_float; + case format::d32_float: + case format::r32_typeless: + return format::r32_float; + case format::r32g32_typeless: + return format::r32g32_float; + case format::r32g32b32_typeless: + return format::r32g32b32_float; + case format::r32g32b32a32_typeless: + return format::r32g32b32a32_float; + case format::d32_float_s8_uint: + case format::r32_g8_typeless: + return format::r32_float_x8_uint; + case format::d24_unorm_s8_uint: // Do not also convert 'd24_unorm_x8_uint' here, to keep it distinguishable from 'd24_unorm_s8_uint' + case format::r24_g8_typeless: + return format::r24_unorm_x8_uint; + case format::bc1_typeless: + case format::bc1_unorm: + return srgb_variant == 1 ? format::bc1_unorm_srgb : format::bc1_unorm; + case format::bc1_unorm_srgb: + return srgb_variant != 0 ? format::bc1_unorm_srgb : format::bc1_unorm; + case format::bc2_typeless: + case format::bc2_unorm: + return srgb_variant == 1 ? format::bc2_unorm_srgb : format::bc2_unorm; + case format::bc2_unorm_srgb: + return srgb_variant != 0 ? format::bc2_unorm_srgb : format::bc2_unorm; + case format::bc3_typeless: + case format::bc3_unorm: + return srgb_variant == 1 ? format::bc3_unorm_srgb : format::bc3_unorm; + case format::bc3_unorm_srgb: + return srgb_variant != 0 ? format::bc3_unorm_srgb : format::bc3_unorm; + case format::bc4_typeless: + return format::bc4_unorm; + case format::bc5_typeless: + return format::bc5_unorm; + case format::bc6h_typeless: + return format::bc6h_ufloat; + case format::bc7_typeless: + case format::bc7_unorm: + return srgb_variant == 1 ? format::bc7_unorm_srgb : format::bc7_unorm; + case format::bc7_unorm_srgb: + return srgb_variant != 0 ? format::bc7_unorm_srgb : format::bc7_unorm; + default: + return value; + } + } + + /// + /// Converts the specified format to its equivalent depth-stencil variant. + /// + /// The format to convert. + inline format format_to_depth_stencil_typed(format value) + { + switch (value) + { + case format::r16_typeless: + case format::r16_unorm: + return format::d16_unorm; + case format::r32_typeless: + case format::r32_float: + return format::d32_float; + case format::r32_g8_typeless: + case format::r32_float_x8_uint: + case format::x32_float_g8_uint: + return format::d32_float_s8_uint; + case format::r24_g8_typeless: + case format::r24_unorm_x8_uint: + case format::x24_unorm_g8_uint: + return format::d24_unorm_s8_uint; + default: + return value; + } + } + + /// + /// Gets the number of bytes a texture row of the specified format occupies. + /// + inline const uint32_t format_row_pitch(format value, uint32_t width) + { + if (value == format::unknown) + return 0; + + if (value <= format::r32g32b32a32_sint) + return 16 * width; + if (value <= format::r32g32b32_sint) + return 12 * width; + if (value <= format::x32_float_g8_uint) + return 8 * width; + if (value <= format::x24_unorm_g8_uint || value == format::l16a16_unorm) + return 4 * width; + if (value <= format::r16_sint || value == format::b5g6r5_unorm || value == format::b5g5r5a1_unorm || value == format::b5g5r5x1_unorm || value == format::l8a8_unorm || value == format::l16_unorm) + return 2 * width; + if (value <= format::a8_unorm || value == format::l8_unorm) + return 1 * width; + if (value <= format::g8r8_g8b8_unorm || (value >= format::b8g8r8a8_unorm && value <= format::b8g8r8x8_unorm_srgb) || (value == format::r8g8b8x8_unorm || value == format::r8g8b8x8_unorm_srgb)) + return 4 * width; + + // Block compressed formats are bytes per block, rather than per pixel + if ((value >= format::bc1_typeless && value <= format::bc1_unorm_srgb) || (value >= format::bc4_typeless && value <= format::bc4_snorm)) + return 8 * ((width + 3) / 4); + if ((value >= format::bc2_typeless && value <= format::bc2_unorm_srgb) || (value >= format::bc3_typeless && value <= format::bc3_unorm_srgb) || (value >= format::bc5_typeless && value <= format::bc7_unorm_srgb)) + return 16 * ((width + 3) / 4); + + return 0; + } + + /// + /// Gets the number of bytes a texture slice of the specified format occupies. + /// + inline const uint32_t format_slice_pitch(format value, uint32_t row_pitch, uint32_t height) + { + if ((value >= format::bc1_typeless && value <= format::bc5_snorm) || (value >= format::bc6h_typeless && value <= format::bc7_unorm_srgb)) + return row_pitch * ((height + 3) / 4); + + return row_pitch * height; + } +} } diff --git a/Plugin/include/reshade/reshade_api_pipeline.hpp b/Plugin/include/reshade/reshade_api_pipeline.hpp new file mode 100644 index 00000000..51601617 --- /dev/null +++ b/Plugin/include/reshade/reshade_api_pipeline.hpp @@ -0,0 +1,1001 @@ +/* + * Copyright (C) 2021 Patrick Mours + * SPDX-License-Identifier: BSD-3-Clause OR MIT + */ + +#pragma once + +#include "reshade_api_resource.hpp" + +namespace reshade { namespace api +{ + /// + /// A list of flags that represent the available shader stages in the render pipeline. + /// + enum class shader_stage : uint32_t + { + vertex = 0x1, + hull = 0x2, + domain = 0x4, + geometry = 0x8, + pixel = 0x10, + compute = 0x20, + + all = 0x7FFFFFFF, + all_compute = compute, + all_graphics = vertex | hull | domain | geometry | pixel + }; + RESHADE_DEFINE_ENUM_FLAG_OPERATORS(shader_stage); + + /// + /// A list of flags that represent the available pipeline stages in the render pipeline. + /// + enum class pipeline_stage : uint32_t + { + vertex_shader = 0x8, + hull_shader = 0x10, + domain_shader = 0x20, + geometry_shader = 0x40, + pixel_shader = 0x80, + compute_shader = 0x800, + + input_assembler = 0x2, + stream_output = 0x4, + rasterizer = 0x100, + depth_stencil = 0x200, + output_merger = 0x400, + + all = 0x7FFFFFFF, + all_compute = compute_shader, + all_graphics = vertex_shader | hull_shader | domain_shader | geometry_shader | pixel_shader | input_assembler | stream_output | rasterizer | depth_stencil | output_merger, + all_shader_stages = vertex_shader | hull_shader | domain_shader | geometry_shader | pixel_shader | compute_shader + }; + RESHADE_DEFINE_ENUM_FLAG_OPERATORS(pipeline_stage); + + /// + /// The available descriptor types. + /// + enum class descriptor_type : uint32_t + { + sampler = 0, + sampler_with_resource_view = 1, + shader_resource_view = 2, + unordered_access_view = 3, + constant_buffer = 6, + shader_storage_buffer = 7 + }; + + /// + /// The available pipeline layout parameter types. + /// + enum class pipeline_layout_param_type : uint32_t + { + push_constants = 1, + descriptor_table = 0, + push_descriptors = 2, + push_descriptors_with_ranges = 3 + }; + + /// + /// Describes a range of constants in a pipeline layout. + /// + struct constant_range + { + /// + /// OpenGL uniform buffer binding index. + /// + uint32_t binding = 0; + /// + /// D3D10/D3D11/D3D12 constant buffer register index. + /// + uint32_t dx_register_index = 0; + /// + /// D3D12 constant buffer register space. + /// + uint32_t dx_register_space = 0; + /// + /// Number of constants in this range (in 32-bit values). + /// + uint32_t count = 0; + /// + /// Shader pipeline stages that can make use of the constants in this range. + /// + shader_stage visibility = shader_stage::all; + }; + + /// + /// Describes a range of descriptors of a descriptor table in a pipeline layout. + /// + struct descriptor_range + { + /// + /// OpenGL/Vulkan binding index (layout(binding=X) in GLSL). + /// In D3D this is equivalent to the offset (in descriptors) of this range in the descriptor table (since each binding can only have an array size of 1). + /// + uint32_t binding = 0; + /// + /// D3D9/D3D10/D3D11/D3D12 shader register index (register(xX) in HLSL). + /// + uint32_t dx_register_index = 0; + /// + /// D3D12 register space (register(..., spaceX) in HLSL). + /// + uint32_t dx_register_space = 0; + /// + /// Number of descriptors in this range. + /// Set to -1 (UINT32_MAX) to indicate an unbounded range. + /// + uint32_t count = 0; + /// + /// Shader pipeline stages that can make use of the descriptors in this range. + /// + shader_stage visibility = shader_stage::all; + /// + /// Size of the array in case this is an array binding. + /// Only meaningful in Vulkan, in OpenGL and other APIs this has to be 1 (since each array element is a separate binding there). + /// If this is less than the total number of descriptors specified in , then the remaining descriptors are assigned a separate binding (with an array size of 1), with the binding index incrementing with each additional descriptor. + /// + uint32_t array_size = 1; + /// + /// Type of the descriptors in this range. + /// + descriptor_type type = descriptor_type::sampler; + }; + + /// + /// Describes a single parameter in a pipeline layout. + /// + struct pipeline_layout_param + { + constexpr pipeline_layout_param() : push_descriptors() {} + constexpr pipeline_layout_param(const constant_range &push_constants) : type(pipeline_layout_param_type::push_constants), push_constants(push_constants) {} + constexpr pipeline_layout_param(const descriptor_range &push_descriptors) : type(pipeline_layout_param_type::push_descriptors), push_descriptors(push_descriptors) {} + constexpr pipeline_layout_param(uint32_t count, const descriptor_range *ranges) : type(pipeline_layout_param_type::descriptor_table), descriptor_table({ count, ranges }) {} + + /// + /// Type of the parameter. + /// + pipeline_layout_param_type type = pipeline_layout_param_type::push_descriptors; + + union + { + /// + /// Used when parameter type is . + /// + constant_range push_constants; + + /// + /// Used when parameter type is . + /// + descriptor_range push_descriptors; + + /// + /// Used when parameter type is or . + /// + struct + { + uint32_t count; + const descriptor_range *ranges; + } descriptor_table; + }; + }; + + /// + /// An opaque handle to a pipeline layout object. + /// In D3D12 this is a pointer to a 'ID3D12RootSignature' object, in Vulkan a 'VkPipelineLayout' handle. + /// + RESHADE_DEFINE_HANDLE(pipeline_layout); + + /// + /// The fill mode to use when rendering triangles. + /// + enum class fill_mode : uint32_t + { + solid = 0, + wireframe = 1, + point = 2 + }; + + /// + /// Indicates triangles facing a particular direction are not drawn. + /// + enum class cull_mode : uint32_t + { + none = 0, + front = 1, + back = 2, + front_and_back = front | back + }; + RESHADE_DEFINE_ENUM_FLAG_OPERATORS(cull_mode); + + /// + /// The available logic operations. + /// + enum class logic_op : uint32_t + { + clear = 0, + bitwise_and = 1, + bitwise_and_reverse = 2, + copy = 3, + bitwise_and_inverted = 4, + noop = 5, + bitwise_xor = 6, + bitwise_or = 7, + bitwise_nor = 8, + equivalent = 9, + invert = 10, + bitwise_or_reverse = 11, + copy_inverted = 12, + bitwise_or_inverted = 13, + bitwise_nand = 14, + set = 15 + }; + + /// + /// The available color or alpha blending operations. + /// + enum class blend_op : uint32_t + { + add = 0, + subtract = 1, + reverse_subtract = 2, + min = 3, + max = 4 + }; + + /// + /// The available blend factors in color or alpha blending operations. + /// + enum class blend_factor : uint32_t + { + zero = 0, + one = 1, + source_color = 2, + one_minus_source_color = 3, + dest_color = 4, + one_minus_dest_color = 5, + source_alpha = 6, + one_minus_source_alpha = 7, + dest_alpha = 8, + one_minus_dest_alpha = 9, + constant_color = 10, + one_minus_constant_color = 11, + constant_alpha = 12, + one_minus_constant_alpha = 13, + source_alpha_saturate = 14, + source1_color = 15, + one_minus_source1_color = 16, + source1_alpha = 17, + one_minus_source1_alpha = 18 + }; + + /// + /// The available stencil operations that can be performed during depth-stencil testing. + /// + enum class stencil_op : uint32_t + { + keep = 0, + zero = 1, + replace = 2, + increment_saturate = 3, + decrement_saturate = 4, + invert = 5, + increment = 6, + decrement = 7 + }; + + /// + /// Specifies how the pipeline interprets vertex data that is bound to the vertex input stage and subsequently renders it. + /// + enum class primitive_topology : uint32_t + { + undefined = 0, + + point_list = 1, + line_list = 2, + line_strip = 3, + triangle_list = 4, + triangle_strip = 5, + triangle_fan = 6, + quad_list = 8, + quad_strip = 9, + line_list_adj = 10, + line_strip_adj = 11, + triangle_list_adj = 12, + triangle_strip_adj = 13, + + patch_list_01_cp = 33, + patch_list_02_cp, + patch_list_03_cp, + patch_list_04_cp, + patch_list_05_cp, + patch_list_06_cp, + patch_list_07_cp, + patch_list_08_cp, + patch_list_09_cp, + patch_list_10_cp, + patch_list_11_cp, + patch_list_12_cp, + patch_list_13_cp, + patch_list_14_cp, + patch_list_15_cp, + patch_list_16_cp, + patch_list_17_cp, + patch_list_18_cp, + patch_list_19_cp, + patch_list_20_cp, + patch_list_21_cp, + patch_list_22_cp, + patch_list_23_cp, + patch_list_24_cp, + patch_list_25_cp, + patch_list_26_cp, + patch_list_27_cp, + patch_list_28_cp, + patch_list_29_cp, + patch_list_30_cp, + patch_list_31_cp, + patch_list_32_cp + }; + + /// + /// Describes a shader object. + /// + struct shader_desc + { + /// + /// Shader source code or binary. + /// + const void *code = nullptr; + /// + /// Size (in bytes) of the shader source or binary. + /// + size_t code_size = 0; + /// + /// Optional entry point name if the shader source or binary contains multiple entry points. + /// Can be if it does not, or to use the default "main" entry point. + /// + const char *entry_point = nullptr; + + /// + /// Number of entries in the and arrays. + /// This is meaningful only when the shader binary is a SPIR-V module and is ignored otherwise. + /// + uint32_t spec_constants = 0; + /// + /// Pointer to an array of specialization constant indices. + /// + const uint32_t *spec_constant_ids = nullptr; + /// + /// Pointer to an array of constant values, one for each specialization constant index in . + /// + const uint32_t *spec_constant_values = nullptr; + }; + + /// + /// Describes a single element in the vertex layout for the input-assembler stage. + /// + struct input_element + { + /// + /// GLSL attribute location associated with this element (layout(location = X)). + /// + uint32_t location = 0; + /// + /// HLSL semantic associated with this element. + /// + const char *semantic = nullptr; + /// + /// Optional index for the HLSL semantic (e.g. for "TEXCOORD1" set to "TEXCOORD" and to 1). + /// + uint32_t semantic_index = 0; + /// + /// Format of the element data. + /// + format format = format::unknown; + /// + /// Index of the vertex buffer binding. + /// + uint32_t buffer_binding = 0; + /// + /// Offset (in bytes) from the start of the vertex to this element. + /// + uint32_t offset = 0; + /// + /// Stride of the entire vertex (this has to be consistent for all elements per vertex buffer binding). + /// Set to zero in case this is unknown. + /// + uint32_t stride = 0; + /// + /// Number of instances to draw using the same per-instance data before advancing by one element. + /// This has to be consistent for all elements per vertex buffer binding. + /// Set to zero to indicate that this element is per-vertex rather than per-instance. + /// + uint32_t instance_step_rate = 0; + }; + + /// + /// Describes the state of the stream-output stage. + /// + struct stream_output_desc + { + /// + /// Index of the stream output stream to be sent to the rasterizer stage. + /// + uint32_t rasterized_stream = 0; + }; + + /// + /// Describes the state of the output-merger stage. + /// + struct blend_desc + { + /// + /// Use alpha-to-coverage as a multisampling technique when setting a pixel to a render target. + /// + bool alpha_to_coverage_enable = false; + /// + /// Enable or disable blending for each render target. + /// + /// + bool blend_enable[8] = { false, false, false, false, false, false, false, false }; + /// + /// Enable or disable a logical operation for each render target. + /// + /// + bool logic_op_enable[8] = { false, false, false, false, false, false, false, false }; + /// + /// Source to use for the RGB value that the pixel shader outputs. + /// + /// + blend_factor source_color_blend_factor[8] = { blend_factor::one, blend_factor::one, blend_factor::one, blend_factor::one, blend_factor::one, blend_factor::one, blend_factor::one, blend_factor::one }; + /// + /// Destination to use for the current RGB value in the render target. + /// + /// + blend_factor dest_color_blend_factor[8] = { blend_factor::zero, blend_factor::zero, blend_factor::zero, blend_factor::zero, blend_factor::zero, blend_factor::zero, blend_factor::zero, blend_factor::zero }; + /// + /// Operation to use to combine and . + /// + /// + blend_op color_blend_op[8] = { blend_op::add, blend_op::add, blend_op::add, blend_op::add, blend_op::add, blend_op::add, blend_op::add, blend_op::add }; + /// + /// Source to use for the alpha value that the pixel shader outputs. + /// + /// + blend_factor source_alpha_blend_factor[8] = { blend_factor::one, blend_factor::one, blend_factor::one, blend_factor::one, blend_factor::one, blend_factor::one, blend_factor::one, blend_factor::one }; + /// + /// Destination to use for the current alpha value in the render target. + /// + /// + blend_factor dest_alpha_blend_factor[8] = { blend_factor::zero, blend_factor::zero, blend_factor::zero, blend_factor::zero, blend_factor::zero, blend_factor::zero, blend_factor::zero, blend_factor::zero }; + /// + /// Operation to use to combine and . + /// + /// + blend_op alpha_blend_op[8] = { blend_op::add, blend_op::add, blend_op::add, blend_op::add, blend_op::add, blend_op::add, blend_op::add, blend_op::add }; + /// + /// Constant RGBA value to use when or is . + /// + float blend_constant[4] = { 1.0f, 1.0f, 1.0f, 1.0f }; + /// + /// Logical operation for each render target. Ignored if is . + /// + /// + logic_op logic_op[8] = { logic_op::noop, logic_op::noop, logic_op::noop, logic_op::noop, logic_op::noop, logic_op::noop, logic_op::noop, logic_op::noop }; + /// + /// A write mask specifying which color components are written to each render target. Bitwise combination of 0x1 for red, 0x2 for green, 0x4 for blue and 0x8 for alpha. + /// + uint8_t render_target_write_mask[8] = { 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF }; + }; + + /// + /// Describes the state of the rasterizer stage. + /// + struct rasterizer_desc + { + /// + /// Fill mode to use when rendering triangles. + /// + /// + fill_mode fill_mode = fill_mode::solid; + /// + /// Triangles facing the specified direction are not drawn. + /// + cull_mode cull_mode = cull_mode::back; + /// + /// Determines if a triangle is front or back-facing. + /// + bool front_counter_clockwise = false; + /// + /// Depth value added to a given pixel. + /// + float depth_bias = 0.0f; + /// + /// Maximum depth bias of a pixel. + /// + float depth_bias_clamp = 0.0f; + /// + /// Scalar on the slope of a given pixel. + /// + float slope_scaled_depth_bias = 0.0f; + /// + /// Enable or disable clipping based on distance. + /// + bool depth_clip_enable = true; + /// + /// Enable or disable scissor testing (scissor rectangle culling). + /// + bool scissor_enable = false; + /// + /// Use the quadrilateral or alpha line anti-aliasing algorithm on multisample antialiasing render targets. + /// + bool multisample_enable = false; + /// + /// Enable or disable line antialiasing. Only applies if doing line drawing and is . + /// + bool antialiased_line_enable = false; + /// + /// Enable or disable conservative rasterization mode. + /// + /// + uint32_t conservative_rasterization = 0; + }; + + /// + /// Describes the state of the depth-stencil stage. + /// + struct depth_stencil_desc + { + /// + /// Enable or disable depth testing. + /// + bool depth_enable = true; + /// + /// Enable or disable writes to the depth-stencil buffer. + /// + bool depth_write_mask = true; + /// + /// Comparison function to use to compare new depth value from a fragment against current depth value in the depth-stencil buffer. + /// + compare_op depth_func = compare_op::less; + /// + /// Enable or disable stencil testing. + /// + bool stencil_enable = false; + /// + /// Mask applied to stencil values read from the depth-stencil buffer for pixels whose surface normal is towards the camera. + /// + uint8_t front_stencil_read_mask = 0xFF; + /// + /// Mask applied to stencil values written to the depth-stencil buffer for pixels whose surface normal is towards the camera. + /// + uint8_t front_stencil_write_mask = 0xFF; + /// + /// Reference value to perform against when stencil testing pixels whose surface normal is towards the camera. + /// + uint8_t front_stencil_reference_value = 0; + /// + /// Comparison function to use to compare new stencil value from a fragment against current stencil value for pixels whose surface normal is facing towards the camera. + /// + compare_op front_stencil_func = compare_op::always; + /// + /// Stencil operation to perform when stencil testing and depth testing both pass for pixels whose surface normal is facing towards the camera. + /// + stencil_op front_stencil_pass_op = stencil_op::keep; + /// + /// Stencil operation to perform when stencil testing fails for pixels whose surface normal is towards the camera. + /// + stencil_op front_stencil_fail_op = stencil_op::keep; + /// + /// Stencil operation to perform when stencil testing passes and depth testing fails for pixels whose surface normal is facing towards the camera. + /// + stencil_op front_stencil_depth_fail_op = stencil_op::keep; + /// + /// Mask applied to stencil values read from the depth-stencil buffer for pixels whose surface normal is facing away from the camera. + /// + uint8_t back_stencil_read_mask = 0xFF; + /// + /// Mask applied to stencil values written to the depth-stencil buffer for pixels whose surface normal is facing away from the camera. + /// + uint8_t back_stencil_write_mask = 0xFF; + /// + /// Reference value to perform against when stencil testing pixels whose surface normal is facing away from the camera. + /// + uint8_t back_stencil_reference_value = 0; + /// + /// Comparison function to use to compare new stencil value from a fragment against current stencil value for pixels whose surface normal is facing away from the camera. + /// + compare_op back_stencil_func = compare_op::always; + /// + /// Stencil operation to perform when stencil testing and depth testing both pass for pixels whose surface normal is facing away from the camera. + /// + stencil_op back_stencil_pass_op = stencil_op::keep; + /// + /// Stencil operation to perform when stencil testing fails for pixels whose surface normal is facing away from the camera. + /// + stencil_op back_stencil_fail_op = stencil_op::keep; + /// + /// Stencil operation to perform when stencil testing passes and depth testing fails for pixels whose surface normal is facing away from the camera. + /// + stencil_op back_stencil_depth_fail_op = stencil_op::keep; + }; + + /// + /// The available pipeline sub-object types. + /// + enum class pipeline_subobject_type : uint32_t + { + unknown, + + /// + /// Vertex shader to use. + /// Sub-object data is a pointer to a . + /// + /// + /// + vertex_shader, + /// + /// Hull shader to use. + /// Sub-object data is a pointer to a . + /// + /// + /// + hull_shader, + /// + /// Domain shader to use. + /// Sub-object data is a pointer to a . + /// + /// + /// + domain_shader, + /// + /// Geometry shader to use. + /// Sub-object data is a pointer to a . + /// + /// + /// + geometry_shader, + /// + /// Pixel shader to use. + /// Sub-object data is a pointer to a . + /// + /// + /// + pixel_shader, + /// + /// Compute shader to use. + /// Sub-object data is a pointer to a . + /// + /// + /// + compute_shader, + /// + /// Vertex layout for the input-assembler stage. + /// Sub-object data is a pointer to an array of . + /// + /// + input_layout, + /// + /// State of the stream-output stage. + /// Sub-object data is a pointer to a . + /// + /// + stream_output_state, + /// + /// State of the output-merger stage. + /// Sub-object data is a pointer to a . + /// + /// + blend_state, + /// + /// State of the rasterizer stage. + /// Sub-object data is a pointer to a . + /// + /// + rasterizer_state, + /// + /// State of the depth-stencil stage. + /// Sub-object data is a pointer to a . + /// + /// + depth_stencil_state, + /// + /// Primitive topology to use when rendering. + /// Sub-object data is a pointer to a value. + /// + primitive_topology, + /// + /// Format of the depth-stencil view that may be used with this pipeline. + /// Sub-object data is a pointer to a value. + /// + depth_stencil_format, + /// + /// Formats of the render target views that may be used with this pipeline. + /// Sub-object data is a pointer to an array of values. + /// + render_target_formats, + /// + /// Mask applied to the coverage mask for a fragment during rasterization. + /// Sub-object data is a pointer to a 32-bit unsigned integer value. + /// + sample_mask, + /// + /// Number of samples used in rasterization. + /// Sub-object data is a pointer to a 32-bit unsigned integer value. + /// + sample_count, + /// + /// Maximum number of viewports that may be bound via with this pipeline. + /// Sub-object data is a pointer to a 32-bit unsigned integer value. + /// + viewport_count, + /// + /// States that may be dynamically updated via after binding this pipeline. + /// Sub-object data is a pointer to an array of values. + /// + dynamic_pipeline_states, + /// + /// Maximum number of vertices a draw call with this pipeline will draw. + /// Sub-object data is a pointer to a 32-bit unsigned integer value. + /// + max_vertex_count + }; + + /// + /// Describes a pipeline sub-object. + /// + struct pipeline_subobject + { + /// + /// Type of the specified sub-object . + /// + pipeline_subobject_type type = pipeline_subobject_type::unknown; + /// + /// Number of sub-object descriptions. + /// This should usually be 1, except for array sub-objects like (where this specifies the number of render targets) or . + /// + uint32_t count = 0; + /// + /// Pointer to an array of sub-object descriptions (which should be as large as the specified ). + /// Depending on the sub-object this should be a pointer to a , , , or ... + /// + void *data = nullptr; + }; + + /// + /// An opaque handle to a pipeline state object. + /// In D3D9, D3D10, D3D11 or D3D12 this is a pointer to a 'IDirect3D(...)Shader', 'ID3D10(...)(Shader/State)', 'ID3D11(...)(Shader/State)' or 'ID3D12PipelineState' object, in Vulkan a 'VkPipeline' handle. + /// + RESHADE_DEFINE_HANDLE(pipeline); + + /// + /// A constant buffer resource descriptor. + /// + struct buffer_range + { + /// + /// Constant buffer resource. + /// + resource buffer = { 0 }; + /// + /// Offset from the start of the buffer resource (in bytes). + /// + uint64_t offset = 0; + /// + /// Number of elements this range covers in the buffer resource (in bytes). + /// Set to -1 (UINT64_MAX) to indicate that the whole buffer should be used. + /// + uint64_t size = UINT64_MAX; + }; + + /// + /// A combined sampler and resource view descriptor. + /// + struct sampler_with_resource_view + { + /// + /// Sampler to sampler the shader resource view with. + /// + sampler sampler = { 0 }; + /// + /// Shader resource view. + /// + resource_view view = { 0 }; + }; + + /// + /// An opaque handle to a descriptor table in a descriptor heap. + /// In Vulkan this is a 'VkDescriptorSet' handle. + /// + RESHADE_DEFINE_HANDLE(descriptor_table); + + /// + /// All information needed to copy descriptors between descriptor tables. + /// + struct descriptor_table_copy + { + /// + /// Descriptor table to copy from. + /// + descriptor_table source_table = { 0 }; + /// + /// Index of the binding in the source descriptor table. + /// + uint32_t source_binding = 0; + /// + /// Array index in the specified source binding to begin copying from. + /// + uint32_t source_array_offset = 0; + /// + /// Descriptor table to copy to. + /// + descriptor_table dest_table = { 0 }; + /// + /// Index of the binding in the destination descriptor table. + /// + uint32_t dest_binding = 0; + /// + /// Array index in the specified destination binding to begin copying to. + /// + uint32_t dest_array_offset = 0; + /// + /// Number of descriptors to copy. + /// + uint32_t count = 0; + }; + + /// + /// All information needed to update descriptors in a descriptor table. + /// + struct descriptor_table_update + { + /// + /// Descriptor table to update. + /// + descriptor_table table = { 0 }; + /// + /// OpenGL/Vulkan binding index in the descriptor set. + /// In D3D this is equivalent to the offset (in descriptors) from the start of the table. + /// + uint32_t binding = 0; + /// + /// Array index in the specified to begin updating at. + /// Only meaningful in Vulkan, in OpenGL and other APIs this has to be 0 (since each array element gets a separate binding). + /// + uint32_t array_offset = 0; + /// + /// Number of descriptors to update, starting at the specified . + /// If the specified has fewer than array elements starting from , then the remainder will be used to update the subsequent binding starting at array element zero, recursively. + /// + uint32_t count = 0; + /// + /// Type of the specified . + /// + descriptor_type type = descriptor_type::sampler; + /// + /// Pointer to an array of descriptors to update in the descriptor table (which should be as large as the specified ). + /// Depending on the descriptor this should be pointer to an array of , , or . + /// + const void *descriptors = nullptr; + }; + + /// + /// An opaque handle to a descriptor heap. + /// In D3D12 this is a pointer to a 'ID3D12DescriptorHeap' object, in Vulkan a 'VkDescriptorPool' handle. + /// + RESHADE_DEFINE_HANDLE(descriptor_heap); + + /// + /// The available query types. + /// + enum class query_type + { + occlusion = 0, + binary_occlusion = 1, + timestamp = 2, + pipeline_statistics = 3, + stream_output_statistics_0 = 4, + stream_output_statistics_1, + stream_output_statistics_2, + stream_output_statistics_3 + }; + + /// + /// An opaque handle to a query heap. + /// In D3D12 this is a pointer to a 'ID3D12QueryHeap' object, in Vulkan a 'VkQueryPool' handle. + /// + RESHADE_DEFINE_HANDLE(query_heap); + + /// + /// A list of all possible render pipeline states that can be set independent of pipeline state objects. + /// Support for these varies between render APIs (e.g. modern APIs like D3D12 and Vulkan support much less dynamic states than D3D9). + /// + enum class dynamic_state + { + unknown = 0, + + alpha_test_enable = 15, + alpha_reference_value = 24, + alpha_func = 25, + srgb_write_enable = 194, + primitive_topology = 1000, + sample_mask = 162, + + // Blend state + + alpha_to_coverage_enable = 1003, + blend_enable = 27, + logic_op_enable = 1004, + source_color_blend_factor = 19, + dest_color_blend_factor = 20, + color_blend_op = 171, + source_alpha_blend_factor = 207, + dest_alpha_blend_factor = 208, + alpha_blend_op = 209, + blend_constant = 193, + logic_op = 1005, + render_target_write_mask = 168, + + // Rasterizer state + + fill_mode = 8, + cull_mode = 22, + front_counter_clockwise = 1001, + depth_bias = 195, + depth_bias_clamp = 1002, + depth_bias_slope_scaled = 175, + depth_clip_enable = 136, + scissor_enable = 174, + multisample_enable = 161, + antialiased_line_enable = 176, + + // Depth-stencil state + + depth_enable = 7, + depth_write_mask = 14, + depth_func = 23, + stencil_enable = 52, + front_stencil_read_mask = 58, + front_stencil_write_mask = 59, + front_stencil_reference_value = 57, + front_stencil_func = 56, + front_stencil_pass_op = 55, + front_stencil_fail_op = 53, + front_stencil_depth_fail_op = 54, + back_stencil_read_mask = 1006, + back_stencil_write_mask = 1007, + back_stencil_reference_value = 1008, + back_stencil_func = 189, + back_stencil_pass_op = 188, + back_stencil_fail_op = 186, + back_stencil_depth_fail_op = 187, + }; + + /// + /// Describes a rectangle. + /// + struct rect + { + int32_t left = 0; + int32_t top = 0; + int32_t right = 0; + int32_t bottom = 0; + + constexpr uint32_t width() const { return right - left; } + constexpr uint32_t height() const { return bottom - top; } + }; + + /// + /// Describes a render viewport. + /// + struct viewport + { + float x = 0.0f; + float y = 0.0f; + float width = 0.0f; + float height = 0.0f; + float min_depth = 0.0f; + float max_depth = 1.0f; + }; +} } diff --git a/Plugin/include/reshade/reshade_api_resource.hpp b/Plugin/include/reshade/reshade_api_resource.hpp new file mode 100644 index 00000000..9da2df04 --- /dev/null +++ b/Plugin/include/reshade/reshade_api_resource.hpp @@ -0,0 +1,528 @@ +/* + * Copyright (C) 2021 Patrick Mours + * SPDX-License-Identifier: BSD-3-Clause OR MIT + */ + +#pragma once + +#define RESHADE_DEFINE_HANDLE(name) \ + typedef struct { uint64_t handle; } name; \ + constexpr bool operator< (name lhs, name rhs) { return lhs.handle < rhs.handle; } \ + constexpr bool operator!=(name lhs, name rhs) { return lhs.handle != rhs.handle; } \ + constexpr bool operator!=(name lhs, uint64_t rhs) { return lhs.handle != rhs; } \ + constexpr bool operator==(name lhs, name rhs) { return lhs.handle == rhs.handle; } \ + constexpr bool operator==(name lhs, uint64_t rhs) { return lhs.handle == rhs; } + +#define RESHADE_DEFINE_ENUM_FLAG_OPERATORS(type) \ + constexpr type operator~(type a) { return static_cast(~static_cast(a)); } \ + inline type &operator&=(type &a, type b) { return reinterpret_cast(reinterpret_cast(a) &= static_cast(b)); } \ + constexpr type operator&(type a, type b) { return static_cast(static_cast(a) & static_cast(b)); } \ + inline type &operator|=(type &a, type b) { return reinterpret_cast(reinterpret_cast(a) |= static_cast(b)); } \ + constexpr type operator|(type a, type b) { return static_cast(static_cast(a) | static_cast(b)); } \ + inline type &operator^=(type &a, type b) { return reinterpret_cast(reinterpret_cast(a) ^= static_cast(b)); } \ + constexpr type operator^(type a, type b) { return static_cast(static_cast(a) ^ static_cast(b)); } \ + constexpr bool operator==(type lhs, uint32_t rhs) { return static_cast(lhs) == rhs; } \ + constexpr bool operator!=(type lhs, uint32_t rhs) { return static_cast(lhs) != rhs; } + +#include "reshade_api_format.hpp" + +namespace reshade { namespace api +{ + /// + /// The available comparison types. + /// + enum class compare_op : uint32_t + { + never = 0, + less = 1, + equal = 2, + less_equal = 3, + greater = 4, + not_equal = 5, + greater_equal = 6, + always = 7 + }; + + /// + /// The available filtering modes used for texture sampling operations. + /// + enum class filter_mode : uint32_t + { + min_mag_mip_point = 0, + min_mag_point_mip_linear = 0x1, + min_point_mag_linear_mip_point = 0x4, + min_point_mag_mip_linear = 0x5, + min_linear_mag_mip_point = 0x10, + min_linear_mag_point_mip_linear = 0x11, + min_mag_linear_mip_point = 0x14, + min_mag_mip_linear = 0x15, + min_mag_anisotropic_mip_point = 0x54, + anisotropic = 0x55, + compare_min_mag_mip_point = 0x80, + compare_min_mag_point_mip_linear = 0x81, + compare_min_point_mag_linear_mip_point = 0x84, + compare_min_point_mag_mip_linear = 0x85, + compare_min_linear_mag_mip_point = 0x90, + compare_min_linear_mag_point_mip_linear = 0x91, + compare_min_mag_linear_mip_point = 0x94, + compare_min_mag_mip_linear = 0x95, + compare_min_mag_anisotropic_mip_point = 0xd4, + compare_anisotropic = 0xd5 + }; + + /// + /// Specifies behavior of sampling with texture coordinates outside a texture resource. + /// + enum class texture_address_mode : uint32_t + { + wrap = 1, + mirror = 2, + clamp = 3, + border = 4, + mirror_once = 5 + }; + + /// + /// Describes a sampler state. + /// + struct sampler_desc + { + /// + /// Filtering mode to use when sampling a texture. + /// + filter_mode filter = filter_mode::min_mag_mip_linear; + /// + /// Method to use for resolving U texture coordinates outside 0 to 1 range. + /// + texture_address_mode address_u = texture_address_mode::clamp; + /// + /// Method to use for resolving V texture coordinates outside 0 to 1 range. + /// + texture_address_mode address_v = texture_address_mode::clamp; + /// + /// Method to use for resolving W texture coordinates outside 0 to 1 range. + /// + texture_address_mode address_w = texture_address_mode::clamp; + /// + /// Offset applied to the calculated mipmap level when sampling a texture. + /// + float mip_lod_bias = 0.0f; + /// + /// Clamping value to use when filtering mode is . + /// + float max_anisotropy = 1.0f; + /// + /// Comparison function to use to compare sampled data against existing sampled data. + /// + compare_op compare_op = compare_op::never; + /// + /// RGBA value to return for texture coordinates outside 0 to 1 range when addressing mode is . + /// + float border_color[4] = { 1.0f, 1.0f, 1.0f, 1.0f }; + /// + /// Lower end of the mipmap range to clamp access to. + /// + float min_lod = -FLT_MAX; + /// + /// Upper end of the mipmap range to clamp access to. + /// + float max_lod = +FLT_MAX; + }; + + /// + /// An opaque handle to a sampler state object. + /// Depending on the render API this can be a pointer to a 'ID3D10SamplerState', 'ID3D11SamplerState' or a 'D3D12_CPU_DESCRIPTOR_HANDLE' (to a sampler descriptor) or 'VkSampler' handle. + /// + RESHADE_DEFINE_HANDLE(sampler); + + /// + /// The available memory mapping access types. + /// + enum class map_access + { + read_only = 1, + write_only, + read_write, + write_discard + }; + + /// + /// The available memory heap types, which give a hint as to where to place the memory allocation for a resource. + /// + enum class memory_heap : uint32_t + { + unknown, // Usually indicates a resource that is reserved, but not yet bound to any memory. + gpu_only, + // Upload heap + cpu_to_gpu, + // Readback heap + gpu_to_cpu, + cpu_only, + custom + }; + + /// + /// The available resource types. The type of a resource is specified during creation and is immutable. + /// Various operations may have special requirements on the type of resources they operate on (e.g. copies can only happen between resources of the same type, ...). + /// + enum class resource_type : uint32_t + { + unknown, + buffer, + texture_1d, + texture_2d, + texture_3d, + surface // Special type for resources that are implicitly both resource and render target view. + }; + + /// + /// A list of flags that describe additional parameters of a resource. + /// + enum class resource_flags : uint32_t + { + none = 0, + dynamic = (1 << 3), + cube_compatible = (1 << 2), + generate_mipmaps = (1 << 0), + shared = (1 << 1), + shared_nt_handle = (1 << 11), + structured = (1 << 6), + sparse_binding = (1 << 18) + }; + RESHADE_DEFINE_ENUM_FLAG_OPERATORS(resource_flags); + + /// + /// A list of flags that specify how a resource is to be used. + /// This needs to be specified during creation and is also used to transition between different resource states within a command list. + /// + enum class resource_usage : uint32_t + { + undefined = 0, + + index_buffer = 0x2, + vertex_buffer = 0x1, + constant_buffer = 0x8000, + stream_output = 0x100, + indirect_argument = 0x200, + + depth_stencil = 0x30, + depth_stencil_read = 0x20, + depth_stencil_write = 0x10, + render_target = 0x4, + shader_resource = 0xC0, + shader_resource_pixel = 0x80, + shader_resource_non_pixel = 0x40, + unordered_access = 0x8, + + copy_dest = 0x400, + copy_source = 0x800, + resolve_dest = 0x1000, + resolve_source = 0x2000, + + // The following are special resource states and may only be used in barriers: + + general = 0x80000000, + present = 0x80000000 | render_target | copy_source, + cpu_access = vertex_buffer | index_buffer | shader_resource | indirect_argument | copy_source + }; + RESHADE_DEFINE_ENUM_FLAG_OPERATORS(resource_usage); + + /// + /// Describes a resource, such as a buffer or texture. + /// + struct [[nodiscard]] resource_desc + { + constexpr resource_desc() : texture() {} + constexpr resource_desc(uint64_t size, memory_heap heap, resource_usage usage) : + type(resource_type::buffer), buffer({ size }), heap(heap), usage(usage) {} + constexpr resource_desc(uint32_t width, uint32_t height, uint16_t layers, uint16_t levels, format format, uint16_t samples, memory_heap heap, resource_usage usage, resource_flags flags = resource_flags::none) : + type(resource_type::texture_2d), texture({ width, height, layers, levels, format, samples }), heap(heap), usage(usage), flags(flags) {} + constexpr resource_desc(resource_type type, uint32_t width, uint32_t height, uint16_t depth_or_layers, uint16_t levels, format format, uint16_t samples, memory_heap heap, resource_usage usage, resource_flags flags = resource_flags::none) : + type(type), texture({ width, height, depth_or_layers, levels, format, samples }), heap(heap), usage(usage), flags(flags) {} + + /// + /// Type of the resource. + /// + resource_type type = resource_type::unknown; + + union + { + /// + /// Used when resource type is a buffer. + /// + struct + { + /// + /// Size of the buffer (in bytes). + /// + uint64_t size = 0; + /// + /// Structure stride for structured buffers (in bytes), otherwise zero. + /// + uint32_t stride = 0; + } buffer; + + /// + /// Used when resource type is a texture or surface. + /// + struct + { + /// + /// Width of the texture (in texels). + /// + uint32_t width = 1; + /// + /// If this is a 2D or 3D texture, height of the texture (in texels), otherwise 1. + /// + uint32_t height = 1; + /// + /// If this is a 3D texture, depth of the texture (in texels), otherwise number of array layers. + /// + uint16_t depth_or_layers = 1; + /// + /// Maximum number of mipmap levels in the texture, including the base level, so at least 1. + /// Can also be zero in case the exact number of mipmap levels is unknown. + /// + uint16_t levels = 1; + /// + /// Data format of each texel in the texture. + /// + format format = format::unknown; + /// + /// The number of samples per texel. Set to a value higher than 1 for multisampling. + /// + uint16_t samples = 1; + } texture; + }; + + /// + /// Memory heap the resource allocation is placed in. + /// + memory_heap heap = memory_heap::unknown; + /// + /// Flags that specify how this resource may be used. + /// + resource_usage usage = resource_usage::undefined; + /// + /// Flags that describe additional parameters. + /// + resource_flags flags = resource_flags::none; + }; + + /// + /// An opaque handle to a resource object (buffer, texture, ...). + /// Resources created by the application are only guaranteed to be valid during event callbacks. + /// Depending on the render API this can be a pointer to a 'IDirect3DResource9', 'ID3D10Resource', 'ID3D11Resource' or 'ID3D12Resource' object or a 'VkImage' handle. + /// + RESHADE_DEFINE_HANDLE(resource); + + /// + /// The available resource view types. These identify how a resource view interprets the data of its resource. + /// + enum class resource_view_type : uint32_t + { + unknown, + buffer, + texture_1d, + texture_1d_array, + texture_2d, + texture_2d_array, + texture_2d_multisample, + texture_2d_multisample_array, + texture_3d, + texture_cube, + texture_cube_array + }; + + /// + /// Describes a resource view, which specifies how to interpret the data of a resource. + /// + struct [[nodiscard]] resource_view_desc + { + constexpr resource_view_desc() : texture() {} + constexpr resource_view_desc(format format, uint64_t offset, uint64_t size) : + type(resource_view_type::buffer), format(format), buffer({ offset, size }) {} + constexpr resource_view_desc(format format, uint32_t first_level, uint32_t levels, uint32_t first_layer, uint32_t layers) : + type(resource_view_type::texture_2d), format(format), texture({ first_level, levels, first_layer, layers }) {} + constexpr resource_view_desc(resource_view_type type, format format, uint32_t first_level, uint32_t levels, uint32_t first_layer, uint32_t layers) : + type(type), format(format), texture({ first_level, levels, first_layer, layers }) {} + constexpr explicit resource_view_desc(format format) : type(resource_view_type::texture_2d), format(format), texture({ 0, 1, 0, 1 }) {} + + /// + /// Resource type the view should interpret the resource data to. + /// + resource_view_type type = resource_view_type::unknown; + /// + /// Format the view should reinterpret the resource data to (can be different than the format of the resource as long as they are compatible). + /// + format format = format::unknown; + + union + { + /// + /// Used when view type is a buffer. + /// + struct + { + /// + /// Offset from the start of the buffer resource (in bytes). + /// + uint64_t offset = 0; + /// + /// Number of elements this view covers in the buffer resource (in bytes). + /// Set to -1 (UINT64_MAX) to indicate that the entire buffer resource should be used. + /// + uint64_t size = UINT64_MAX; + } buffer; + + /// + /// Used when view type is a texture. + /// + struct + { + /// + /// Index of the most detailed mipmap level to use. This number has to be between zero and the maximum number of mipmap levels in the texture minus 1. + /// + uint32_t first_level = 0; + /// + /// Maximum number of mipmap levels for the view of the texture. + /// Set to -1 (UINT32_MAX) to indicate that all mipmap levels down to the least detailed should be used. + /// + uint32_t level_count = UINT32_MAX; + /// + /// Index of the first array layer of the texture array to use. This value is ignored if the texture is not layered. + /// + uint32_t first_layer = 0; + /// + /// Maximum number of array layers for the view of the texture array. This value is ignored if the texture is not layered. + /// Set to -1 (UINT32_MAX) to indicate that all array layers should be used. + /// + uint32_t layer_count = UINT32_MAX; + } texture; + }; + }; + + /// + /// An opaque handle to a resource view object (depth-stencil, render target, shader resource view, ...). + /// Resource views created by the application are only guaranteed to be valid during event callbacks. + /// Depending on the render API this can be a pointer to a 'IDirect3DResource9', 'ID3D10View' or 'ID3D11View' object, or a 'D3D12_CPU_DESCRIPTOR_HANDLE' (to a view descriptor) or 'VkImageView' handle. + /// + RESHADE_DEFINE_HANDLE(resource_view); + + /// + /// Describes a region inside a subresource. + /// + struct subresource_box + { + int32_t left = 0; + int32_t top = 0; + int32_t front = 0; + int32_t right = 0; + int32_t bottom = 0; + int32_t back = 0; + + constexpr uint32_t width() const { return right - left; } + constexpr uint32_t height() const { return bottom - top; } + constexpr uint32_t depth() const { return back - front; } + }; + + /// + /// Describes the data of a subresource. + /// + struct subresource_data + { + /// + /// Pointer to the data. + /// + void *data = nullptr; + /// + /// Row pitch of the data (added to the data pointer to move between texture rows, unused for buffers and 1D textures). + /// + /// + uint32_t row_pitch = 0; + /// + /// Depth pitch of the data (added to the data pointer to move between texture depth/array slices, unused for buffers and 1D/2D textures). + /// + /// + uint32_t slice_pitch = 0; + }; + + /// + /// Specifies how the contents of a render target or depth-stencil view are treated at the start of a render pass. + /// + enum class render_pass_load_op : uint32_t + { + load, + clear, + discard, + no_access + }; + + /// + /// Specifies how the contents of a render target or depth-stencil view are treated at the end of a render pass. + /// + enum class render_pass_store_op : uint32_t + { + store, + discard, + no_access + }; + + /// + /// Describes a depth-stencil view and how it is treated at the start and end of a render pass. + /// + struct render_pass_depth_stencil_desc + { + /// + /// Depth-stencil resource view. + /// + resource_view view = { 0 }; + /// + /// Specifies how the depth contents of the depth-stencil view are treated at the start of the render pass. + /// + render_pass_load_op depth_load_op = render_pass_load_op::load; + /// + /// Specifies how the depth contents of the depth-stencil view are treated at the end of the render pass. + /// + render_pass_store_op depth_store_op = render_pass_store_op::store; + /// + /// Specifies how the stencil contents of the depth-stencil view are treated at the start of the render pass. + /// + render_pass_load_op stencil_load_op = render_pass_load_op::load; + /// + /// Specifies how the stencil contents of the depth-stencil view are treated at the end of the render pass. + /// + render_pass_store_op stencil_store_op = render_pass_store_op::store; + /// + /// Value the depth contents of the depth-stencil resource is cleared to when is . + /// + float clear_depth = 0.0f; + /// + /// Value the stencil contents of the depth-stencil resource is cleared to when is . + /// + uint8_t clear_stencil = 0; + }; + + /// + /// Describes a render target view and how it is treated at the start and end of a render pass. + /// + struct render_pass_render_target_desc + { + /// + /// Render target resource view. + /// + resource_view view = { 0 }; + /// + /// Specifies how the contents of the render target view are treated at the start of the render pass. + /// + render_pass_load_op load_op = render_pass_load_op::load; + /// + /// Specifies how the contents of the render target view are treated at the end of the render pass. + /// + render_pass_store_op store_op = render_pass_store_op::store; + /// + /// Value the render target resource is cleared to when is . + /// + float clear_color[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + }; +} } diff --git a/Plugin/include/reshade/reshade_events.hpp b/Plugin/include/reshade/reshade_events.hpp new file mode 100644 index 00000000..52eeb61b --- /dev/null +++ b/Plugin/include/reshade/reshade_events.hpp @@ -0,0 +1,1696 @@ +/* + * Copyright (C) 2021 Patrick Mours + * SPDX-License-Identifier: BSD-3-Clause OR MIT + */ + +#pragma once + +#include "reshade_api.hpp" + +namespace reshade +{ + enum class addon_event : uint32_t + { + /// + /// Called after successfull device creation, from: + /// + /// IDirect3D9::CreateDevice + /// IDirect3D9Ex::CreateDeviceEx + /// IDirect3DDevice9::Reset + /// IDirect3DDevice9Ex::ResetEx + /// D3D10CreateDevice + /// D3D10CreateDevice1 + /// D3D10CreateDeviceAndSwapChain + /// D3D10CreateDeviceAndSwapChain1 + /// D3D11CreateDevice + /// D3D11CreateDeviceAndSwapChain + /// D3D12CreateDevice + /// glMakeCurrent + /// vkCreateDevice + /// + /// Callback function signature: void (api::device *device) + /// + init_device, + + /// + /// Called on device destruction, before: + /// + /// IDirect3DDevice9::Reset + /// IDirect3DDevice9Ex::ResetEx + /// IDirect3DDevice9::Release + /// ID3D10Device::Release + /// ID3D11Device::Release + /// ID3D12Device::Release + /// wglDeleteContext + /// vkDestroyDevice + /// + /// Callback function signature: void (api::device *device) + /// + destroy_device, + + /// + /// Called after successfull command list creation, from: + /// + /// ID3D11Device::CreateDeferredContext + /// ID3D11Device1::CreateDeferredContext1 + /// ID3D11Device2::CreateDeferredContext2 + /// ID3D11Device3::CreateDeferredContext3 + /// ID3D12Device::CreateCommandList + /// ID3D12Device4::CreateCommandList1 + /// vkAllocateCommandBuffers + /// + /// Callback function signature: void (api::command_list *cmd_list) + /// + /// + /// In case of D3D9, D3D10, D3D11 and OpenGL this is called during device initialization as well and behaves as if an implicit immediate command list was created. + /// + init_command_list, + + /// + /// Called on command list destruction, before: + /// + /// ID3D11CommandList::Release + /// ID3D12CommandList::Release + /// vkFreeCommandBuffers + /// + /// Callback function signature: void (api::command_list *cmd_list) + /// + destroy_command_list, + + /// + /// Called after successfull command queue creation, from: + /// + /// ID3D12Device::CreateCommandQueue + /// vkCreateDevice (for every queue associated with the device) + /// + /// Callback function signature: void (api::command_queue *queue) + /// + /// + /// In case of D3D9, D3D10, D3D11 and OpenGL this is called during device initialization as well and behaves as if an implicit command queue was created. + /// + init_command_queue, + + /// + /// Called on command queue destruction, before: + /// + /// ID3D12CommandQueue::Release + /// vkDestroyDevice (for every queue associated with the device) + /// + /// Callback function signature: void (api::command_queue *queue) + /// + destroy_command_queue, + + /// + /// Called after successfull swap chain creation, from: + /// + /// IDirect3D9::CreateDevice (for the implicit swap chain) + /// IDirect3D9Ex::CreateDeviceEx (for the implicit swap chain) + /// IDirect3D9Device::CreateAdditionalSwapChain + /// IDirect3DDevice9::Reset (for the implicit swap chain) + /// IDirect3DDevice9Ex::ResetEx (for the implicit swap chain) + /// IDXGIFactory::CreateSwapChain + /// IDXGIFactory2::CreateSwapChain(...) + /// IDXGISwapChain::ResizeBuffers + /// IDXGISwapChain3::ResizeBuffers1 + /// wglMakeCurrent + /// wglSwapBuffers (after window was resized) + /// vkCreateSwapchainKHR + /// + /// Callback function signature: void (api::swapchain *swapchain) + /// + init_swapchain, + + /// + /// Called on swap chain creation, before: + /// + /// IDirect3D9::CreateDevice (for the implicit swap chain) + /// IDirect3D9Ex::CreateDeviceEx (for the implicit swap chain) + /// IDirect3D9Device::CreateAdditionalSwapChain + /// IDirect3D9Device::Reset (for the implicit swap chain) + /// IDirect3D9DeviceEx::ResetEx (for the implicit swap chain) + /// IDXGIFactory::CreateSwapChain + /// IDXGIFactory2::CreateSwapChain(...) + /// IDXGISwapChain::ResizeBuffers + /// IDXGISwapChain3::ResizeBuffers1 + /// wglSetPixelFormat + /// vkCreateSwapchainKHR + /// + /// Callback function signature: bool (api::swapchain_desc &desc, void *hwnd) + /// + /// + /// To overwrite the swap chain description, modify desc in the callback and return , otherwise return . + /// + create_swapchain, + + /// + /// Called on swap chain destruction, before: + /// + /// IDirect3DDevice9::Release (for the implicit swap chain) + /// IDirect3DSwapChain9::Release + /// IDXGISwapChain::Release + /// wglDeleteContext + /// wglSwapBuffers (after window was resized) + /// vkDestroySwapchainKHR + /// + /// In addition, called when swap chain is reset, before: + /// + /// IDirect3DDevice9::Reset (for the implicit swap chain) + /// IDirect3DDevice9Ex::ResetEx (for the implicit swap chain) + /// IDXGISwapChain::ResizeBuffers + /// IDXGISwapChain1::ResizeBuffers1 + /// + /// Callback function signature: void (api::swapchain *swapchain) + /// + destroy_swapchain, + + /// + /// Called after effect runtime initialization (which happens after swap chain creation or a swap chain buffer resize). + /// Callback function signature: void (api::effect_runtime *runtime) + /// + init_effect_runtime, + + /// + /// Called when an effect runtime is reset or destroyed. + /// Callback function signature: void (api::effect_runtime *runtime) + /// + destroy_effect_runtime, + + /// + /// Called after successfull sampler creation from: + /// + /// ID3D10Device::CreateSamplerState + /// ID3D11Device::CreateSamplerState + /// ID3D12Device::CreateSampler + /// vkCreateSampler + /// + /// Callback function signature: void (api::device *device, const api::sampler_desc &desc, api::sampler sampler) + /// + /// + /// Is not called in D3D9 (since samplers are loose state there) or OpenGL. + /// + init_sampler, + + /// + /// Called on sampler creation, before: + /// + /// ID3D10Device::CreateSamplerState + /// ID3D11Device::CreateSamplerState + /// ID3D12Device::CreateSampler + /// ID3D12Device::CreateRootSignature + /// vkCreateSampler + /// + /// Callback function signature: bool (api::device *device, api::sampler_desc &desc) + /// + /// + /// To overwrite the sampler description, modify desc in the callback and return , otherwise return . + /// Is not called in D3D9 (since samplers are loose state there) or OpenGL. + /// + create_sampler, + + /// + /// Called on sampler destruction, before: + /// + /// ID3D10SamplerState::Release + /// ID3D11SamplerState::Release + /// glDeleteSamplers + /// vkDestroySampler + /// + /// Callback function signature: void (api::device *device, api::sampler sampler) + /// + /// + /// Is not called in D3D9 (since samplers are loose state there), D3D12 (since samplers are descriptor handles instead of objects there) or OpenGL. + /// + destroy_sampler, + + /// + /// Called after successfull resource creation from: + /// + /// IDirect3DDevice9::CreateVertexBuffer + /// IDirect3DDevice9::CreateIndexBuffer + /// IDirect3DDevice9::CreateTexture + /// IDirect3DDevice9::CreateCubeTexture + /// IDirect3DDevice9::CreateVolumeTexture + /// IDirect3DDevice9::CreateRenderTargetSurface + /// IDirect3DDevice9::CreateDepthStencilSurface + /// IDirect3DDevice9::CreateOffscreenPlainSurface + /// IDirect3DDevice9Ex::CreateRenderTargetSurfaceEx + /// IDirect3DDevice9Ex::CreateDepthStencilSurfaceEx + /// IDirect3DDevice9Ex::CreateOffscreenPlainSurfaceEx + /// ID3D10Device::CreateBuffer + /// ID3D10Device::CreateTexture1D + /// ID3D10Device::CreateTexture2D + /// ID3D10Device::CreateTexture2D + /// ID3D11Device::CreateBuffer + /// ID3D11Device::CreateTexture1D + /// ID3D11Device::CreateTexture2D + /// ID3D11Device::CreateTexture3D + /// ID3D11Device3::CreateTexture2D + /// ID3D11Device3::CreateTexture3D + /// ID3D12Device::CreateCommittedResource + /// ID3D12Device::CreatePlacedResource + /// ID3D12Device::CreateReservedResource + /// ID3D12Device4::CreateCommittedResource1 + /// ID3D12Device4::CreateReservedResource1 + /// glBufferData + /// glBufferStorage + /// glNamedBufferData + /// glNamedBufferStorage + /// glTexImage1D + /// glTexImage2D + /// glTexImage2DMultisample + /// glTexImage3D + /// glTexImage3DMultisample + /// glCompressedTexImage1D + /// glCompressedTexImage2D + /// glCompressedTexImage3D + /// glTexStorage1D + /// glTexStorage2D + /// glTexStorage2DMultisample + /// glTexStorage3D + /// glTexStorage3DMultisample + /// glTextureStorage1D + /// glTextureStorage2D + /// glTextureStorage2DMultisample + /// glTextureStorage3D + /// glTextureStorage3DMultisample + /// glRenderbufferStorage + /// glRenderbufferStorageMultisample + /// glNamedRenderbufferStorage + /// glNamedRenderbufferStorageMultisample + /// vkBindBufferMemory + /// vkBindBufferMemory2 + /// vkBindImageMemory + /// vkBindImageMemory2 + /// + /// Callback function signature: void (api::device *device, const api::resource_desc &desc, const api::subresource_data *initial_data, api::resource_usage initial_state, api::resource resource) + /// + init_resource, + + /// + /// Called on resource creation, before: + /// + /// IDirect3DDevice9::CreateVertexBuffer + /// IDirect3DDevice9::CreateIndexBuffer + /// IDirect3DDevice9::CreateTexture + /// IDirect3DDevice9::CreateCubeTexture + /// IDirect3DDevice9::CreateVolumeTexture + /// IDirect3DDevice9::CreateRenderTargetSurface + /// IDirect3DDevice9::CreateDepthStencilSurface + /// IDirect3DDevice9::CreateOffscreenPlainSurface + /// IDirect3DDevice9Ex::CreateRenderTargetSurfaceEx + /// IDirect3DDevice9Ex::CreateDepthStencilSurfaceEx + /// IDirect3DDevice9Ex::CreateOffscreenPlainSurfaceEx + /// ID3D10Device::CreateBuffer + /// ID3D10Device::CreateTexture1D + /// ID3D10Device::CreateTexture2D + /// ID3D10Device::CreateTexture2D + /// ID3D11Device::CreateBuffer + /// ID3D11Device::CreateTexture1D + /// ID3D11Device::CreateTexture2D + /// ID3D11Device::CreateTexture3D + /// ID3D11Device3::CreateTexture2D + /// ID3D11Device3::CreateTexture3D + /// ID3D12Device::CreateCommittedResource + /// ID3D12Device::CreatePlacedResource + /// ID3D12Device::CreateReservedResource + /// ID3D12Device4::CreateCommittedResource1 + /// ID3D12Device4::CreateReservedResource1 + /// glBufferData + /// glBufferStorage + /// glNamedBufferData + /// glNamedBufferStorage + /// glTexImage1D + /// glTexImage2D + /// glTexImage2DMultisample + /// glTexImage3D + /// glTexImage3DMultisample + /// glCompressedTexImage1D + /// glCompressedTexImage2D + /// glCompressedTexImage3D + /// glTexStorage1D + /// glTexStorage2D + /// glTexStorage2DMultisample + /// glTexStorage3D + /// glTexStorage3DMultisample + /// glTextureStorage1D + /// glTextureStorage2D + /// glTextureStorage2DMultisample + /// glTextureStorage3D + /// glTextureStorage3DMultisample + /// glRenderbufferStorage + /// glRenderbufferStorageMultisample + /// glNamedRenderbufferStorage + /// glNamedRenderbufferStorageMultisample + /// vkCreateBuffer + /// vkCreateImage + /// + /// Callback function signature: bool (api::device *device, api::resource_desc &desc, api::subresource_data *initial_data, api::resource_usage initial_state) + /// + /// + /// To overwrite the resource description, modify desc in the callback and return , otherwise return . + /// + create_resource, + + /// + /// Called on resource destruction, before: + /// + /// IDirect3DResource9::Release + /// ID3D10Resource::Release + /// ID3D11Resource::Release + /// ID3D12Resource::Release + /// glDeleteBuffers + /// glDeleteTextures + /// glDeleteRenderbuffers + /// vkDestroyBuffer + /// vkDestroyImage + /// + /// Callback function signature: void (api::device *device, api::resource resource) + /// + destroy_resource, + + /// + /// Called after successfull resource view creation from: + /// + /// IDirect3DDevice9::CreateTexture + /// IDirect3DDevice9::CreateCubeTexture + /// IDirect3DDevice9::CreateVolumeTexture + /// ID3D10Device::CreateShaderResourceView + /// ID3D10Device::CreateRenderTargetView + /// ID3D10Device::CreateDepthStencilView + /// ID3D10Device1::CreateShaderResourceView1 + /// ID3D11Device::CreateShaderResourceView + /// ID3D11Device::CreateUnorderedAccessView + /// ID3D11Device::CreateRenderTargetView + /// ID3D11Device::CreateDepthStencilView + /// ID3D11Device3::CreateShaderResourceView1 + /// ID3D11Device3::CreateUnorderedAccessView1 + /// ID3D11Device3::CreateRenderTargetView1 + /// ID3D12Device::CreateShaderResourceView + /// ID3D12Device::CreateUnorderedAccessView + /// ID3D12Device::CreateRenderTargetView + /// ID3D12Device::CreateDepthStencilView + /// glTexBuffer + /// glTextureBuffer + /// glTextureView + /// vkCreateBufferView + /// vkCreateImageView + /// + /// Callback function signature: void (api::device *device, api::resource resource, api::resource_usage usage_type, const api::resource_view_desc &desc, api::resource_view view) + /// + init_resource_view, + + /// + /// Called on resource view creation, before: + /// + /// ID3D10Device::CreateShaderResourceView + /// ID3D10Device::CreateRenderTargetView + /// ID3D10Device::CreateDepthStencilView + /// ID3D10Device1::CreateShaderResourceView1 + /// ID3D11Device::CreateShaderResourceView + /// ID3D11Device::CreateUnorderedAccessView + /// ID3D11Device::CreateRenderTargetView + /// ID3D11Device::CreateDepthStencilView + /// ID3D11Device3::CreateShaderResourceView1 + /// ID3D11Device3::CreateUnorderedAccessView1 + /// ID3D11Device3::CreateRenderTargetView1 + /// ID3D12Device::CreateShaderResourceView + /// ID3D12Device::CreateUnorderedAccessView + /// ID3D12Device::CreateRenderTargetView + /// ID3D12Device::CreateDepthStencilView + /// glTexBuffer + /// glTextureBuffer + /// glTextureView + /// vkCreateBufferView + /// vkCreateImageView + /// + /// Callback function signature: bool (api::device *device, api::resource resource, api::resource_usage usage_type, api::resource_view_desc &desc) + /// + /// + /// To overwrite the resource view description, modify desc in the callback and return , otherwise return . + /// Is not called in D3D9 (since resource views are tied to resources there). + /// + create_resource_view, + + /// + /// Called on resource view destruction, before: + /// + /// IDirect3DResource9::Release + /// ID3D10View::Release + /// ID3D11View::Release + /// glDeleteTextures + /// vkDestroyBufferView + /// vkDestroyImageView + /// + /// Callback function signature: void (api::device *device, api::resource_view view) + /// + /// + /// Is not called in D3D12 (since resource views are descriptor handles instead of objects there). + /// + destroy_resource_view, + + /// + /// Called after: + /// + /// IDirect3DVertexBuffer9::Lock + /// IDirect3DIndexBuffer9::Lock + /// ID3D10Resource::Map + /// ID3D11DeviceContext::Map + /// ID3D12Resource::Map + /// glMapBuffer + /// glMapBufferRange + /// glMapNamedBuffer + /// glMapNamedBufferRange + /// + /// Callback function signature: void (api::device *device, api::resource resource, uint64_t offset, uint64_t size, api::map_access access, void **data) + /// + map_buffer_region, + + /// + /// Called before: + /// + /// IDirect3DVertexBuffer9::Unlock + /// IDirect3DIndexBuffer9::Unlock + /// ID3D10Resource::Unmap + /// ID3D11DeviceContext::Unmap + /// ID3D12Resource::Unmap + /// glUnmapBuffer + /// glUnmapNamedBuffer + /// + /// Callback function signature: void (api::device *device, api::resource resource) + /// + unmap_buffer_region, + + /// + /// Called after: + /// + /// IDirect3DSurface9::LockRect + /// IDirect3DVolume9::LockBox + /// IDirect3DTexture9::LockRect + /// IDirect3DVolumeTexture9::LockBox + /// IDirect3DCubeTexture9::LockRect + /// ID3D10Resource::Map + /// ID3D11DeviceContext::Map + /// ID3D12Resource::Map + /// + /// Callback function signature: void (api::device *device, api::resource resource, uint32_t subresource, const api::subresource_box *box, api::map_access access, api::subresource_data *data) + /// + map_texture_region, + + /// + /// Called before: + /// + /// IDirect3DSurface9::UnlockRect + /// IDirect3DVolume9::UnlockBox + /// IDirect3DTexture9::UnlockRect + /// IDirect3DVolumeTexture9::UnlockBox + /// IDirect3DCubeTexture9::UnlockRect + /// ID3D10Resource::Unmap + /// ID3D11DeviceContext::Unmap + /// ID3D12Resource::Unmap + /// + /// Callback function signature: void (api::device *device, api::resource resource, uint32_t subresource) + /// + unmap_texture_region, + + /// + /// Called before: + /// + /// ID3D10Device::UpdateSubresource + /// ID3D11DeviceContext::UpdateSubresource + /// glBufferSubData + /// glNamedBufferSubData + /// + /// Callback function signature: bool (api::device *device, const void *data, api::resource resource, uint64_t offset, uint64_t size) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Destination resource will be in the state. + /// + update_buffer_region, + + /// + /// Called before: + /// + /// ID3D10Device::UpdateSubresource + /// ID3D11DeviceContext::UpdateSubresource + /// glTexSubData1D + /// glTexSubData2D + /// glTexSubData3D + /// glTextureSubData1D + /// glTextureSubData2D + /// glTextureSubData3D + /// glCompressedTexSubData1D + /// glCompressedTexSubData2D + /// glCompressedTexSubData3D + /// glCompressedTextureSubData1D + /// glCompressedTextureSubData2D + /// glCompressedTextureSubData3D + /// + /// Callback function signature: bool (api::device *device, const api::subresource_data &data, api::resource resource, uint32_t subresource, const api::subresource_box *box) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Destination resource will be in the state. + /// + update_texture_region, + + /// + /// Called after successfull pipeline creation from: + /// + /// IDirect3DDevice9::CreateVertexShader + /// IDirect3DDevice9::CreatePixelShader + /// IDirect3DDevice9::CreateVertexDeclaration + /// ID3D10Device::CreateVertexShader + /// ID3D10Device::CreateGeometryShader + /// ID3D10Device::CreateGeometryShaderWithStreamOutput + /// ID3D10Device::CreatePixelShader + /// ID3D10Device::CreateInputLayout + /// ID3D10Device::CreateBlendState + /// ID3D10Device::CreateDepthStencilState + /// ID3D10Device::CreateRasterizerState + /// ID3D10Device1::CreateBlendState1 + /// ID3D11Device::CreateVertexShader + /// ID3D11Device::CreateHullShader + /// ID3D11Device::CreateDomainShader + /// ID3D11Device::CreateGeometryShader + /// ID3D11Device::CreateGeometryShaderWithStreamOutput + /// ID3D11Device::CreatePixelShader + /// ID3D11Device::CreateComputeShader + /// ID3D11Device::CreateInputLayout + /// ID3D11Device::CreateBlendState + /// ID3D11Device::CreateDepthStencilState + /// ID3D11Device::CreateRasterizerState + /// ID3D11Device1::CreateBlendState1 + /// ID3D11Device1::CreateRasterizerState1 + /// ID3D11Device3::CreateRasterizerState2 + /// ID3D12Device::CreateComputePipelineState + /// ID3D12Device::CreateGraphicsPipelineState + /// ID3D12Device2::CreatePipelineState + /// ID3D12PipelineLibrary::LoadComputePipeline + /// ID3D12PipelineLibrary::LoadGraphicsPipeline + /// ID3D12PipelineLibrary1::LoadPipeline + /// glLinkProgram + /// vkCreateComputePipelines + /// vkCreateGraphicsPipelines + /// + /// Callback function signature: void (api::device *device, api::pipeline_layout layout, uint32_t subobject_count, const api::pipeline_subobject *subobjects, api::pipeline pipeline) + /// + /// + /// May be called multiple times with the same pipeline handle (whenever the pipeline is updated or its reference count is incremented). + /// + init_pipeline, + + /// + /// Called on pipeline creation, before: + /// + /// IDirect3DDevice9::CreateVertexShader + /// IDirect3DDevice9::CreatePixelShader + /// IDirect3DDevice9::CreateVertexDeclaration + /// ID3D10Device::CreateVertexShader + /// ID3D10Device::CreateGeometryShader + /// ID3D10Device::CreateGeometryShaderWithStreamOutput + /// ID3D10Device::CreatePixelShader + /// ID3D10Device::CreateInputLayout + /// ID3D10Device::CreateBlendState + /// ID3D10Device::CreateDepthStencilState + /// ID3D10Device::CreateRasterizerState + /// ID3D10Device1::CreateBlendState1 + /// ID3D11Device::CreateVertexShader + /// ID3D11Device::CreateHullShader + /// ID3D11Device::CreateDomainShader + /// ID3D11Device::CreateGeometryShader + /// ID3D11Device::CreateGeometryShaderWithStreamOutput + /// ID3D11Device::CreatePixelShader + /// ID3D11Device::CreateComputeShader + /// ID3D11Device::CreateInputLayout + /// ID3D11Device::CreateBlendState + /// ID3D11Device::CreateDepthStencilState + /// ID3D11Device::CreateRasterizerState + /// ID3D11Device1::CreateBlendState1 + /// ID3D11Device1::CreateRasterizerState1 + /// ID3D11Device3::CreateRasterizerState2 + /// ID3D12Device::CreateComputePipelineState + /// ID3D12Device::CreateGraphicsPipelineState + /// ID3D12Device2::CreatePipelineState + /// glShaderSource + /// vkCreateComputePipelines + /// vkCreateGraphicsPipelines + /// + /// Callback function signature: bool (api::device *device, api::pipeline_layout layout, uint32_t subobject_count, const api::pipeline_subobject *subobjects) + /// + /// + /// To overwrite the pipeline description, modify desc in the callback and return , otherwise return . + /// + create_pipeline, + + /// + /// Called on pipeline destruction, before: + /// + /// ID3D10VertexShader::Release + /// ID3D10GeometryShader::Release + /// ID3D10PixelShader::Release + /// ID3D10InputLayout::Release + /// ID3D10BlendState::Release + /// ID3D10DepthStencilState::Release + /// ID3D10RasterizerState::Release + /// ID3D11VertexShader::Release + /// ID3D11HullShader::Release + /// ID3D11DomainShader::Release + /// ID3D11GeometryShader::Release + /// ID3D11PixelShader::Release + /// ID3D11ComputeShader::Release + /// ID3D11InputLayout::Release + /// ID3D11BlendState::Release + /// ID3D11DepthStencilState::Release + /// ID3D11RasterizerState::Release + /// ID3D12PipelineState::Release + /// glDeleteProgram + /// vkDestroyPipeline + /// + /// Callback function signature: void (api::device *device, api::pipeline pipeline) + /// + /// + /// Is not called in D3D9. + /// + destroy_pipeline, + + /// + /// Called after successfull pipeline layout creation from: + /// + /// ID3D12Device::CreateRootSignature + /// vkCreatePipelineLayout + /// + /// Callback function signature: void (api::device *device, uint32_t param_count, const api::pipeline_layout_param *params, api::pipeline_layout layout) + /// + init_pipeline_layout, + + /// + /// Called on pipeline layout creation. + /// Callback function signature: bool (api::device *device, uint32_t param_count, const api::pipeline_layout_param *params) + /// + create_pipeline_layout, + + /// + /// Called on pipeline layout destruction, before: + /// + /// ID3D12RootSignature::Release + /// VkDestroyPipelineLayout + /// + /// Callback function signature: void (api::device *device, api::pipeline_layout layout) + /// + destroy_pipeline_layout, + + /// + /// Called before: + /// + /// ID3D12Device::CopyDescriptors + /// ID3D12Device::CopyDescriptorsSimple + /// vkUpdateDescriptorSets + /// + /// Callback function signature: bool (api::device *device, uint32_t count, const api::descriptor_table_copy *copies) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + copy_descriptor_tables, + + /// + /// Called before: + /// + /// ID3D12Device::CreateConstantBufferView + /// ID3D12Device::CreateShaderResourceView + /// ID3D12Device::CreateUnorderedAccessView + /// ID3D12Device::CreateSampler + /// vkUpdateDescriptorSets + /// + /// Callback function signature: bool (api::device *device, uint32_t count, const api::descriptor_table_update *updates) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + update_descriptor_tables, + + /// + /// Called after successfull query heap creation from: + /// + /// ID3D12Device::CreateQueryHeap + /// vkCreateQueryPool + /// + /// Callback function signature: void (api::device *device, api::query_type type, uint32_t size, api::query_heap heap) + /// + init_query_heap, + + /// + /// Called on query heap creation, before: + /// + /// ID3D12Device::CreateQueryHeap + /// vkCreateQueryPool + /// + /// Callback function signature: bool (api::device *device, api::query_type type, uint32_t &size) + /// + create_query_heap, + + /// + /// Called on query heap destruction, before: + /// + /// ID3D12QueryHeap::Release + /// vkDestroyQueryPool + /// + /// Callback function signature: void (api::device *device, api::query_heap heap) + /// + destroy_query_heap, + + /// + /// Called before: + /// + /// vkGetQueryPoolResults + /// + /// Callback function signature: bool (api::device *device, api::query_heap heap, uint32_t first, uint32_t count, void *results, uint32_t stride) + /// + get_query_heap_results, + + /// + /// Called after: + /// + /// ID3D12GraphicsCommandList::ResourceBarrier + /// ID3D12GraphicsCommandList7::Barrier + /// vkCmdPipelineBarrier + /// vkCmdPipelineBarrier2 + /// + /// Callback function signature: void (api::command_list *cmd_list, uint32_t count, const api::resource *resources, const api::resource_usage *old_states, const api::resource_usage *new_states) + /// + barrier, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList4::BeginRenderPass + /// vkCmdBeginRenderPass + /// vkCmdBeginRenderPass2 + /// vkCmdNextSubpass + /// vkCmdNextSubpass2 + /// vkCmdBeginRendering + /// + /// Callback function signature: void (api::command_list *cmd_list, uint32_t count, const api::render_pass_render_target_desc *rts, const api::render_pass_depth_stencil_desc *ds) + /// + /// + /// The depth-stencil description argument is optional and may be . + /// + begin_render_pass, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList4::EndRenderPass + /// vkCmdEndRenderPass + /// vkCmdEndRenderPass2 + /// vkCmdNextSubpass + /// vkCmdNextSubpass2 + /// vkCmdEndRendering + /// + /// Callback function signature: void (api::command_list *cmd_list) + /// + end_render_pass, + + /// + /// Called after: + /// + /// IDirect3DDevice9::SetRenderTarget + /// IDirect3DDevice9::SetDepthStencilSurface + /// ID3D10Device::OMSetRenderTargets + /// ID3D11DeviceContext::OMSetRenderTargets + /// ID3D11DeviceContext::OMSetRenderTargetsAndUnorderedAccessViews + /// ID3D12GraphicsCommandList::OMSetRenderTargets + /// glBindFramebuffer + /// + /// Callback function signature: void (api::command_list *cmd_list, uint32_t count, const api::resource_view *rtvs, api::resource_view dsv) + /// + bind_render_targets_and_depth_stencil, + + /// + /// Called after: + /// + /// IDirect3DDevice9::SetVertexShader + /// IDirect3DDevice9::SetPixelShader + /// IDirect3DDevice9::SetVertexDeclaration + /// IDirect3DDevice9::ProcessVertices + /// ID3D10Device::VSSetShader + /// ID3D10Device::GSSetShader + /// ID3D10Device::PSSetShader + /// ID3D10Device::IASetInputLayout + /// ID3D10Device::OMSetBlendState + /// ID3D10Device::OMSetDepthStencilState + /// ID3D10Device::RSSetState + /// ID3D11DeviceContext::VSSetShader + /// ID3D11DeviceContext::HSSetShader + /// ID3D11DeviceContext::DSSetShader + /// ID3D11DeviceContext::GSSetShader + /// ID3D11DeviceContext::PSSetShader + /// ID3D11DeviceContext::CSSetShader + /// ID3D11DeviceContext::IASetInputLayout + /// ID3D11DeviceContext::OMSetBlendState + /// ID3D11DeviceContext::OMSetDepthStencilState + /// ID3D11DeviceContext::RSSetState + /// ID3D12GraphicsCommandList::Reset + /// ID3D12GraphicsCommandList::SetPipelineState + /// glUseProgram + /// vkCmdBindPipeline + /// + /// Callback function signature: void (api::command_list *cmd_list, api::pipeline_stage stages, api::pipeline pipeline) + /// + bind_pipeline, + + /// + /// Called after: + /// + /// IDirect3DDevice9::SetRenderState + /// ID3D10Device::IASetPrimitiveTopology + /// ID3D10Device::OMSetBlendState + /// ID3D10Device::OMSetDepthStencilState + /// ID3D11DeviceContext::IASetPrimitiveTopology + /// ID3D11DeviceContext::OMSetBlendState + /// ID3D11DeviceContext::OMSetDepthStencilState + /// ID3D12GraphicsCommandList::IASetPrimitiveTopology + /// ID3D12GraphicsCommandList::OMSetBlendFactor + /// ID3D12GraphicsCommandList::OMSetStencilRef + /// gl(...) + /// vkCmdSetDepthBias + /// vkCmdSetBlendConstants + /// vkCmdSetStencilCompareMask + /// vkCmdSetStencilWriteMask + /// vkCmdSetStencilReference + /// + /// Callback function signature: void (api::command_list *cmd_list, uint32_t count, const api::dynamic_state *states, const uint32_t *values) + /// + bind_pipeline_states, + + /// + /// Called after: + /// + /// IDirect3DDevice9::SetViewport + /// IDirect3DDevice9::SetRenderTarget (implicitly updates the viewport) + /// ID3D10Device::RSSetViewports + /// ID3D11DeviceContext::RSSetViewports + /// ID3D12GraphicsCommandList::RSSetViewports + /// glViewport + /// glViewportArrayv + /// glViewportIndexedf + /// glViewportIndexedfv + /// vkCmdSetViewport + /// + /// Callback function signature: void (api::command_list *cmd_list, uint32_t first, uint32_t count, const api::viewport *viewports) + /// + bind_viewports, + + /// + /// Called after: + /// + /// IDirect3DDevice9::SetScissorRect + /// ID3D10Device::RSSetScissorRects + /// ID3D11DeviceContext::RSSetScissorRects + /// ID3D12GraphicsCommandList::RSSetScissorRects + /// glScissor + /// glScissorArrayv + /// glScissorIndexed + /// glScissorIndexedv + /// vkCmdSetScissor + /// + /// Callback function signature: void (api::command_list *cmd_list, uint32_t first, uint32_t count, const api::rect *rects) + /// + bind_scissor_rects, + + /// + /// Called after: + /// + /// IDirect3DDevice9::SetVertexShaderConstantF + /// IDirect3DDevice9::SetPixelShaderConstantF + /// ID3D12GraphicsCommandList::SetComputeRoot32BitConstant + /// ID3D12GraphicsCommandList::SetComputeRoot32BitConstants + /// ID3D12GraphicsCommandList::SetGraphicsRoot32BitConstant + /// ID3D12GraphicsCommandList::SetGraphicsRoot32BitConstants + /// glUniform(...) + /// vkCmdPushConstants + /// + /// Callback function signature: void (api::command_list *cmd_list, api::shader_stage stages, api::pipeline_layout layout, uint32_t layout_param, uint32_t first, uint32_t count, const void *values) + /// + push_constants, + + /// + /// Called after: + /// + /// IDirect3DDevice9::SetTexture + /// ID3D10Device::VSSetSamplers + /// ID3D10Device::VSSetShaderResources + /// ID3D10Device::VSSetConstantBuffers + /// ID3D10Device::GSSetSamplers + /// ID3D10Device::GSSetShaderResources + /// ID3D10Device::GSSetConstantBuffers + /// ID3D10Device::PSSetSamplers + /// ID3D10Device::PSSetShaderResources + /// ID3D10Device::PSSetConstantBuffers + /// ID3D11DeviceContext::VSSetSamplers + /// ID3D11DeviceContext::VSSetShaderResources + /// ID3D11DeviceContext::VSSetConstantBuffers + /// ID3D11DeviceContext::HSSetSamplers + /// ID3D11DeviceContext::HSSetShaderResources + /// ID3D11DeviceContext::HSSetConstantBuffers + /// ID3D11DeviceContext::DSSetSamplers + /// ID3D11DeviceContext::DSSetShaderResources + /// ID3D11DeviceContext::DSSetConstantBuffers + /// ID3D11DeviceContext::GSSetSamplers + /// ID3D11DeviceContext::GSSetShaderResources + /// ID3D11DeviceContext::GSSetConstantBuffers + /// ID3D11DeviceContext::PSSetSamplers + /// ID3D11DeviceContext::PSSetShaderResources + /// ID3D11DeviceContext::PSSetConstantBuffers + /// ID3D11DeviceContext::CSSetSamplers + /// ID3D11DeviceContext::CSSetShaderResources + /// ID3D11DeviceContext::CSSetUnorderedAccessViews + /// ID3D11DeviceContext::CSSetConstantBuffers + /// ID3D12GraphicsCommandList::SetComputeRootConstantBufferView + /// ID3D12GraphicsCommandList::SetGraphicsRootConstantBufferView + /// ID3D12GraphicsCommandList::SetComputeRootShaderResourceView + /// ID3D12GraphicsCommandList::SetGraphicsRootShaderResourceView + /// ID3D12GraphicsCommandList::SetComputeRootUnorderedAccessView + /// ID3D12GraphicsCommandList::SetGraphicsRootUnorderedAccessView + /// glBindBufferBase + /// glBindBufferRange + /// glBindBuffersBase + /// glBindBuffersRange + /// glBindTexture + /// glBindImageTexture + /// glBindTextures + /// glBindImageTextures + /// glBindTextureUnit + /// glBindMultiTextureEXT + /// vkCmdPushDescriptorSetKHR + /// + /// Callback function signature: void (api::command_list *cmd_list, api::shader_stage stages, api::pipeline_layout layout, uint32_t layout_param, const api::descriptor_table_update &update) + /// + push_descriptors, + + /// + /// Called after: + /// + /// ID3D12GraphicsCommandList::SetComputeRootSignature + /// ID3D12GraphicsCommandList::SetGraphicsRootSignature + /// ID3D12GraphicsCommandList::SetComputeRootDescriptorTable + /// ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable + /// vkCmdBindDescriptorSets + /// + /// Callback function signature: void (api::command_list *cmd_list, api::shader_stage stages, api::pipeline_layout layout, uint32_t first, uint32_t count, const api::descriptor_table *tables) + /// + bind_descriptor_tables, + + /// + /// Called after: + /// + /// IDirect3DDevice9::SetIndices + /// ID3D10Device::IASetIndexBuffer + /// ID3D11DeviceContext::IASetIndexBuffer + /// ID3D12GraphicsCommandList::IASetIndexBuffer + /// glBindBuffer + /// vkCmdBindIndexBuffer + /// + /// Callback function signature: void (api::command_list *cmd_list, api::resource buffer, uint64_t offset, uint32_t index_size) + /// + bind_index_buffer, + + /// + /// Called after: + /// + /// IDirect3DDevice9::SetStreamSource + /// ID3D10Device::IASetVertexBuffers + /// ID3D11DeviceContext::IASetVertexBuffers + /// ID3D12GraphicsCommandList::IASetVertexBuffers + /// glBindBuffer + /// glBindVertexBuffer + /// glBindVertexBuffers + /// vkCmdBindVertexBuffers + /// vkCmdBindVertexBuffers2 + /// + /// Callback function signature: void (api::command_list *cmd_list, uint32_t first, uint32_t count, const api::resource *buffers, const uint64_t *offsets, const uint32_t *strides) + /// + /// + /// The strides argument is optional and may be . + /// + bind_vertex_buffers, + + /// + /// Called after: + /// + /// IDirect3DDevice9::ProcessVertices + /// ID3D10Device::SOSetTargets + /// ID3D11DeviceContext::SOSetTargets + /// ID3D12GraphicsCommandList::SOSetTargets + /// glBindBufferBase + /// glBindBufferRange + /// glBindBuffersBase + /// glBindBuffersRange + /// vkCmdBindTransformFeedbackBuffersEXT + /// + /// Callback function signature: void (api::command_list *cmd_list, uint32_t first, uint32_t count, const api::resource *buffers, const uint64_t *offsets, const uint64_t *max_sizes, const api::resource *counter_buffers, const uint64_t *counter_offsets) + /// + /// + /// The counter arguments are optional and may be . + /// + bind_stream_output_buffers, + + /// + /// Called before: + /// + /// IDirect3DDevice9::DrawPrimitive + /// IDirect3DDevice9::DrawPrimitiveUP + /// IDirect3DDevice9::ProcessVertices + /// ID3D10Device::Draw + /// ID3D10Device::DrawInstanced + /// ID3D11DeviceContext::Draw + /// ID3D11DeviceContext::DrawInstanced + /// ID3D12GraphicsCommandList::DrawInstanced + /// glDrawArrays + /// glDrawArraysInstanced + /// glDrawArraysInstancedBaseInstance + /// glMultiDrawArrays + /// vkCmdDraw + /// + /// Callback function signature: bool (api::command_list *cmd_list, uint32_t vertex_count, uint32_t instance_count, uint32_t first_vertex, uint32_t first_instance) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + draw, + + /// + /// Called before: + /// + /// IDirect3DDevice9::DrawIndexedPrimitive + /// IDirect3DDevice9::DrawIndexedPrimitiveUP + /// ID3D10Device::DrawIndexed + /// ID3D10Device::DrawIndexedInstanced + /// ID3D11DeviceContext::DrawIndexed + /// ID3D11DeviceContext::DrawIndexedInstanced + /// ID3D12GraphicsCommandList::DrawIndexedInstanced + /// glDrawElements + /// glDrawElementsBaseVertex + /// glDrawElementsInstanced + /// glDrawElementsInstancedBaseVertex + /// glDrawElementsInstancedBaseInstance + /// glDrawElementsInstancedBaseVertexBaseInstance + /// glMultiDrawElements + /// glMultiDrawElementsBaseVertex + /// vkCmdDrawIndexed + /// + /// Callback function signature: bool (api::command_list *cmd_list, uint32_t index_count, uint32_t instance_count, uint32_t first_index, int32_t vertex_offset, uint32_t first_instance) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + draw_indexed, + + /// + /// Called before: + /// + /// ID3D11DeviceContext::Dispatch + /// ID3D12GraphicsCommandList::Dispatch + /// glDispatchCompute + /// vkCmdDispatch + /// + /// Callback function signature: bool (api::command_list *cmd_list, uint32_t group_count_x, uint32_t group_count_y, uint32_t group_count_z) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + dispatch, + + /// + /// Called before: + /// + /// ID3D11DeviceContext::DrawInstancedIndirect + /// ID3D11DeviceContext::DrawIndexedInstancedIndirect + /// ID3D11DeviceContext::DispatchIndirect + /// ID3D12GraphicsCommandList::ExecuteIndirect + /// glDrawArraysIndirect + /// glDrawElementsIndirect + /// glMultiDrawArraysIndirect + /// glMultiDrawElementsIndirect + /// glDispatchComputeIndirect + /// vkCmdDrawIndirect + /// vkCmdDrawIndexedIndirect + /// vkCmdDispatchIndirect + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::indirect_command type, api::resource buffer, uint64_t offset, uint32_t draw_count, uint32_t stride) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + draw_or_dispatch_indirect, + + /// + /// Called before: + /// + /// IDirect3DDevice9::UpdateTexture + /// IDirect3DDevice9::GetRenderTargetData + /// ID3D10Device::CopyResource + /// ID3D11DeviceContext::CopyResource + /// ID3D12GraphicsCommandList::CopyResource + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource source, api::resource dest) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Source resource will be in the state. + /// Destination resource will be in the state. + /// + copy_resource, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList::CopyBufferRegion + /// glCopyBufferSubData + /// glCopyNamedBufferSubData + /// vkCmdCopyBuffer + /// vkCmdCopyBuffer2 + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource source, uint64_t source_offset, api::resource dest, uint64_t dest_offset, uint64_t size) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Source resource will be in the state. + /// Destination resource will be in the state. + /// + copy_buffer_region, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList::CopyTextureRegion + /// vkCmdCopyBufferToImage + /// vkCmdCopyBufferToImage2 + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource source, uint64_t source_offset, uint32_t row_length, uint32_t slice_height, api::resource dest, uint32_t dest_subresource, const api::subresource_box *dest_box) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Source resource will be in the state. + /// Destination resource will be in the state. + /// The subresource box argument is optional and may be . + /// + copy_buffer_to_texture, + + /// + /// Called before: + /// + /// IDirect3DDevice9::UpdateSurface + /// IDirect3DDevice9::StretchRect + /// ID3D10Device::CopySubresourceRegion + /// ID3D11DeviceContext::CopySubresourceRegion + /// ID3D12GraphicsCommandList::CopyTextureRegion + /// glBlitFramebuffer + /// glBlitNamedFramebuffer + /// glCopyImageSubData + /// glCopyTexSubImage1D + /// glCopyTexSubImage2D + /// glCopyTexSubImage3D + /// glCopyTextureSubImage1D + /// glCopyTextureSubImage2D + /// glCopyTextureSubImage3D + /// vkCmdBlitImage + /// vkCmdBlitImage2 + /// vkCmdCopyImage + /// vkCmdCopyImage2 + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource source, uint32_t source_subresource, const api::subresource_box *source_box, api::resource dest, uint32_t dest_subresource, const api::subresource_box *dest_box, api::filter_mode filter) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Source resource will be in the state. + /// Destination resource will be in the state. + /// The subresource box arguments are optional and may be . + /// + copy_texture_region, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList::CopyTextureRegion + /// vkCmdCopyImageToBuffer + /// vkCmdCopyImageToBuffer2 + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource source, uint32_t source_subresource, const api::subresource_box *source_box, api::resource dest, uint64_t dest_offset, uint32_t row_length, uint32_t slice_height) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Source resource will be in the state. + /// Destination resource will be in the state. + /// The subresource box argument is optional and may be . + /// + copy_texture_to_buffer, + + /// + /// Called before: + /// + /// IDirect3DDevice9::StretchRect + /// ID3D10Device::ResolveSubresource + /// ID3D11DeviceContext::ResolveSubresource + /// ID3D12GraphicsCommandList::ResolveSubresource + /// ID3D12GraphicsCommandList1::ResolveSubresourceRegion + /// glBlitFramebuffer + /// glBlitNamedFramebuffer + /// vkCmdResolveImage + /// vkCmdResolveImage2 + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource source, uint32_t source_subresource, const api::subresource_box *source_box, api::resource dest, uint32_t dest_subresource, int32_t dest_x, int32_t dest_y, int32_t dest_z, api::format format) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Source resource will be in the state. + /// Destination resource will be in the state. + /// The subresource box argument is optional and may be . + /// + resolve_texture_region, + + /// + /// Called before: + /// + /// IDirect3DDevice9::Clear + /// ID3D10Device::ClearDepthStencilView + /// ID3D11DeviceContext::ClearDepthStencilView + /// ID3D11DeviceContext1::ClearView (for depth-stencil views) + /// ID3D12GraphicsCommandList::ClearDepthStencilView + /// glClear + /// glClearBufferfi + /// glClearBufferfv + /// glClearNamedFramebufferfi + /// glClearNamedFramebufferfv + /// vkCmdClearDepthStencilImage + /// vkCmdClearAttachments + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource_view dsv, const float *depth, const uint8_t *stencil, uint32_t rect_count, const api::rect *rects) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Resource will be in the state. + /// One of the depth or stencil clear value arguments may be when the respective component is not cleared. + /// + clear_depth_stencil_view, + + /// + /// Called before: + /// + /// IDirect3DDevice9::Clear + /// IDirect3DDevice9::ColorFill + /// ID3D10Device::ClearRenderTargetView + /// ID3D11DeviceContext::ClearRenderTargetView + /// ID3D11DeviceContext1::ClearView (for render target views) + /// ID3D12GraphicsCommandList::ClearRenderTargetView + /// glClear + /// glClearBufferfv + /// glClearNamedFramebufferfv + /// vkCmdClearColorImage + /// vkCmdClearAttachments + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource_view rtv, const float color[4], uint32_t rect_count, const api::rect *rects) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Resources will be in the state. + /// + clear_render_target_view, + + /// + /// Called before: + /// + /// ID3D11DeviceContext::ClearUnorderedAccessViewUint + /// ID3D12GraphicsCommandList::ClearUnorderedAccessViewUint + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource_view uav, const uint32_t values[4], uint32_t rect_count, const api::rect *rects) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Resource will be in the state. + /// + clear_unordered_access_view_uint, + + /// + /// Called before: + /// + /// ID3D11DeviceContext::ClearUnorderedAccessViewFloat + /// ID3D11DeviceContext1::ClearView (for unordered access views) + /// ID3D12GraphicsCommandList::ClearUnorderedAccessViewFloat + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource_view uav, const float values[4], uint32_t rect_count, const api::rect *rects) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Resource will be in the state. + /// + clear_unordered_access_view_float, + + /// + /// Called before: + /// + /// ID3D10Device::GenerateMips + /// ID3D11DeviceContext::GenerateMips + /// glGenerateMipmap + /// glGenerateTextureMipmap + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource_view srv) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + generate_mipmaps, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList::BeginQuery + /// vkCmdBeginQuery + /// vkCmdBeginQueryIndexedEXT + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::query_heap heap, api::query_type type, uint32_t index) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + begin_query, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList::EndQuery + /// vkCmdEndQuery + /// vkCmdEndQueryIndexedEXT + /// vkCmdWriteTimestamp + /// vkCmdWriteTimestamp2 + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::query_heap heap, api::query_type type, uint32_t index) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + end_query, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList::ResolveQueryData + /// vkCmdCopyQueryPoolResults + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::query_heap heap, api::query_type type, uint32_t first, uint32_t count, api::resource dest, uint64_t dest_offset, uint32_t stride) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + copy_query_heap_results, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList::Reset + /// vkBeginCommandBuffer + /// + /// Callback function signature: void (api::command_list *cmd_list) + /// + /// + /// Is not called for immediate command lists (since they cannot be reset). + /// + reset_command_list, + + /// + /// Called before: + /// + /// ID3D11DeviceContext::FinishCommandList + /// ID3D12GraphicsCommandList::Close + /// vkEndCommandBuffer + /// + /// Callback function signature: void (api::command_list *cmd_list) + /// + /// + /// Is not called for immediate command lists (since they cannot be closed). + /// + close_command_list, + + /// + /// Called when a command list is submitted to a command queue (or an immediate command list is flushed), before: + /// + /// IDirect3DDevice9::EndScene + /// ID3D10Device::Flush + /// ID3D11DeviceContext::Flush + /// ID3D11DeviceContext3::Flush1 + /// ID3D12CommandQueue::ExecuteCommandLists + /// glFlush + /// vkQueueSubmit + /// + /// Callback function signature: void (api::command_queue *queue, api::command_list *cmd_list) + /// + execute_command_list, + + /// + /// Called when a secondary command list is executed on a primary command list, before: + /// + /// ID3D11DeviceContext::ExecuteCommandList + /// ID3D12GraphicsCommandList::ExecuteBundle + /// vkCmdExecuteCommands + /// + /// In addition, called after: + /// + /// ID3D11DeviceContext::FinishCommandList + /// + /// Callback function signature: void (api::command_list *cmd_list, api::command_list *secondary_cmd_list) + /// + execute_secondary_command_list, + + /// + /// Called before: + /// + /// IDirect3DDevice9::Present + /// IDirect3DDevice9Ex::PresentEx + /// IDirect3DSwapChain9::Present + /// IDXGISwapChain::Present + /// IDXGISwapChain3::Present1 + /// ID3D12CommandQueueDownlevel::Present + /// wglSwapBuffers + /// vkQueuePresentKHR + /// IVRCompositor::Submit + /// + /// Callback function signature: void (api::command_queue *queue, api::swapchain *swapchain, const api::rect *source_rect, const api::rect *dest_rect, uint32_t dirty_rect_count, const api::rect *dirty_rects) + /// + /// + /// The source and destination rectangle arguments are optional and may be . + /// + present, + + /// + /// Called after ReShade has rendered its overlay. + /// Callback function signature: void (api::effect_runtime *runtime) + /// + reshade_present, + + /// + /// Called right before ReShade effects are rendered. + /// Callback function signature: void (api::effect_runtime *runtime, api::command_list *cmd_list, api::resource_view rtv, api::resource_view rtv_srgb) + /// + reshade_begin_effects, + + /// + /// Called right after ReShade effects were rendered. + /// Callback function signature: void (api::effect_runtime *runtime, api::command_list *cmd_list, api::resource_view rtv, api::resource_view rtv_srgb) + /// + reshade_finish_effects, + + /// + /// Called right after all ReShade effects were reloaded. + /// This occurs during effect runtime initialization or because the user pressed the "Reload" button in the overlay. + /// Callback function signature: void (api::effect_runtime *runtime) + /// + reshade_reloaded_effects, + + /// + /// Called before a uniform variable is changed. + /// Callback function signature: bool (api::effect_runtime *runtime, api::effect_uniform_variable variable, const void *data, size_t size) + /// + /// + /// To prevent the variable from being changed, return , otherwise return . + /// + reshade_set_uniform_value, + /// + /// Called before a technique is enabled or disabled. + /// Callback function signature: bool (api::effect_runtime *runtime, api::effect_technique technique, bool enabled) + /// + /// + /// To prevent the technique from being changed, return , otherwise return . + /// + reshade_set_technique_state, + + /// + /// Called between the ImGui::NewFrame and ImGui::EndFrame calls for the ReShade overlay. + /// Can be used to perform custom Dear ImGui calls, but it is recommended to instead use to register a dedicated overlay. + /// Callback function signature: void (api::effect_runtime *runtime) + /// + /// + /// This is not called for effect runtimes in VR. + /// + reshade_overlay, + + /// + /// Called after a screenshot was taken and saved to disk. + /// Callback function signature: void (api::effect_runtime *runtime, const char *path) + /// + reshade_screenshot, + + /// + /// Called for each technique after it was rendered, usually between and . + /// Callback function signature: void (api::effect_runtime *runtime, api::effect_technique technique, api::command_list *cmd_list, api::resource_view rtv, api::resource_view rtv_srgb) + /// + reshade_render_technique, + + /// + /// Called after a preset was loaded and applied. + /// This occurs during reloading or when the user chooses a new preset in the overlay. + /// Callback function signature: void (api::effect_runtime *runtime, const char *path) + /// + reshade_set_current_preset_path, + + /// + /// Called when the rendering order of loaded techniques is changed. + /// Callback function signature: bool (api::effect_runtime *runtime, size_t count, api::effect_technique *techniques) + /// + /// + /// To prevent the order from being changed, return , otherwise return . + /// + reshade_reorder_techniques, + +#if RESHADE_ADDON + max // Last value used internally by ReShade to determine number of events in this enum +#endif + }; + + template + struct addon_event_traits; + +#define RESHADE_DEFINE_ADDON_EVENT_TRAITS(ev, ret, ...) \ + template <> \ + struct addon_event_traits { \ + using decl = ret(*)(__VA_ARGS__); \ + using type = ret; \ + } + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::init_device, void, api::device *device); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::destroy_device, void, api::device *device); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::init_command_list, void, api::command_list *cmd_list); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::destroy_command_list, void, api::command_list *cmd_list); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::init_command_queue, void, api::command_queue *queue); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::destroy_command_queue, void, api::command_queue *queue); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::init_swapchain, void, api::swapchain *swapchain); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::create_swapchain, bool, api::swapchain_desc &desc, void *hwnd); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::destroy_swapchain, void, api::swapchain *swapchain); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::init_effect_runtime, void, api::effect_runtime *runtime); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::destroy_effect_runtime, void, api::effect_runtime *runtime); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::init_sampler, void, api::device *device, const api::sampler_desc &desc, api::sampler sampler); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::create_sampler, bool, api::device *device, api::sampler_desc &desc); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::destroy_sampler, void, api::device *device, api::sampler sampler); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::init_resource, void, api::device *device, const api::resource_desc &desc, const api::subresource_data *initial_data, api::resource_usage initial_state, api::resource resource); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::create_resource, bool, api::device *device, api::resource_desc &desc, api::subresource_data *initial_data, api::resource_usage initial_state); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::destroy_resource, void, api::device *device, api::resource resource); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::init_resource_view, void, api::device *device, api::resource resource, api::resource_usage usage_type, const api::resource_view_desc &desc, api::resource_view view); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::create_resource_view, bool, api::device *device, api::resource resource, api::resource_usage usage_type, api::resource_view_desc &desc); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::destroy_resource_view, void, api::device *device, api::resource_view view); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::map_buffer_region, void, api::device *device, api::resource resource, uint64_t offset, uint64_t size, api::map_access access, void **data); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::unmap_buffer_region, void, api::device *device, api::resource resource); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::map_texture_region, void, api::device *device, api::resource resource, uint32_t subresource, const api::subresource_box *box, api::map_access access, api::subresource_data *data); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::unmap_texture_region, void, api::device *device, api::resource resource, uint32_t subresource); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::update_buffer_region, bool, api::device *device, const void *data, api::resource resource, uint64_t offset, uint64_t size); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::update_texture_region, bool, api::device *device, const api::subresource_data &data, api::resource resource, uint32_t subresource, const api::subresource_box *box); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::init_pipeline, void, api::device *device, api::pipeline_layout layout, uint32_t subobject_count, const api::pipeline_subobject *subobjects, api::pipeline pipeline); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::create_pipeline, bool, api::device *device, api::pipeline_layout layout, uint32_t subobject_count, const api::pipeline_subobject *subobjects); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::destroy_pipeline, void, api::device *device, api::pipeline pipeline); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::init_pipeline_layout, void, api::device *device, uint32_t param_count, const api::pipeline_layout_param *params, api::pipeline_layout layout); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::create_pipeline_layout, bool, api::device *device, uint32_t param_count, const api::pipeline_layout_param *params); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::destroy_pipeline_layout, void, api::device *device, api::pipeline_layout layout); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::copy_descriptor_tables, bool, api::device *device, uint32_t count, const api::descriptor_table_copy *copies); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::update_descriptor_tables, bool, api::device *device, uint32_t count, const api::descriptor_table_update *updates); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::init_query_heap, void, api::device *device, api::query_type type, uint32_t size, api::query_heap heap); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::create_query_heap, bool, api::device *device, api::query_type type, uint32_t &size); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::destroy_query_heap, void, api::device *device, api::query_heap heap); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::get_query_heap_results, bool, api::device *device, api::query_heap heap, uint32_t first, uint32_t count, void *results, uint32_t stride); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::barrier, void, api::command_list *cmd_list, uint32_t count, const api::resource *resources, const api::resource_usage *old_states, const api::resource_usage *new_states); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::begin_render_pass, void, api::command_list *cmd_list, uint32_t count, const api::render_pass_render_target_desc *rts, const api::render_pass_depth_stencil_desc *ds); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::end_render_pass, void, api::command_list *cmd_list); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::bind_render_targets_and_depth_stencil, void, api::command_list *cmd_list, uint32_t count, const api::resource_view *rtvs, api::resource_view dsv); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::bind_pipeline, void, api::command_list *cmd_list, api::pipeline_stage stages, api::pipeline pipeline); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::bind_pipeline_states, void, api::command_list *cmd_list, uint32_t count, const api::dynamic_state *states, const uint32_t *values); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::bind_viewports, void, api::command_list *cmd_list, uint32_t first, uint32_t count, const api::viewport *viewports); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::bind_scissor_rects, void, api::command_list *cmd_list, uint32_t first, uint32_t count, const api::rect *rects); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::push_constants, void, api::command_list *cmd_list, api::shader_stage stages, api::pipeline_layout layout, uint32_t layout_param, uint32_t first, uint32_t count, const void *values); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::push_descriptors, void, api::command_list *cmd_list, api::shader_stage stages, api::pipeline_layout layout, uint32_t layout_param, const api::descriptor_table_update &update); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::bind_descriptor_tables, void, api::command_list *cmd_list, api::shader_stage stages, api::pipeline_layout layout, uint32_t first, uint32_t count, const api::descriptor_table *tables); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::bind_index_buffer, void, api::command_list *cmd_list, api::resource buffer, uint64_t offset, uint32_t index_size); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::bind_vertex_buffers, void, api::command_list *cmd_list, uint32_t first, uint32_t count, const api::resource *buffers, const uint64_t *offsets, const uint32_t *strides); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::bind_stream_output_buffers, void, api::command_list *cmd_list, uint32_t first, uint32_t count, const api::resource *buffers, const uint64_t *offsets, const uint64_t *max_sizes, const api::resource *counter_buffers, const uint64_t *counter_offsets); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::draw, bool, api::command_list *cmd_list, uint32_t vertex_count, uint32_t instance_count, uint32_t first_vertex, uint32_t first_instance); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::draw_indexed, bool, api::command_list *cmd_list, uint32_t index_count, uint32_t instance_count, uint32_t first_index, int32_t vertex_offset, uint32_t first_instance); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::dispatch, bool, api::command_list *cmd_list, uint32_t group_count_x, uint32_t group_count_y, uint32_t group_count_z); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::draw_or_dispatch_indirect, bool, api::command_list *cmd_list, api::indirect_command type, api::resource buffer, uint64_t offset, uint32_t draw_count, uint32_t stride); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::copy_resource, bool, api::command_list *cmd_list, api::resource source, api::resource dest); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::copy_buffer_region, bool, api::command_list *cmd_list, api::resource source, uint64_t source_offset, api::resource dest, uint64_t dest_offset, uint64_t size); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::copy_buffer_to_texture, bool, api::command_list *cmd_list, api::resource source, uint64_t source_offset, uint32_t row_length, uint32_t slice_height, api::resource dest, uint32_t dest_subresource, const api::subresource_box *dest_box); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::copy_texture_region, bool, api::command_list *cmd_list, api::resource source, uint32_t source_subresource, const api::subresource_box *source_box, api::resource dest, uint32_t dest_subresource, const api::subresource_box *dest_box, api::filter_mode filter); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::copy_texture_to_buffer, bool, api::command_list *cmd_list, api::resource source, uint32_t source_subresource, const api::subresource_box *source_box, api::resource dest, uint64_t dest_offset, uint32_t row_length, uint32_t slice_height); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::resolve_texture_region, bool, api::command_list *cmd_list, api::resource source, uint32_t source_subresource, const api::subresource_box *source_box, api::resource dest, uint32_t dest_subresource, int32_t dest_x, int32_t dest_y, int32_t dest_z, api::format format); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::clear_depth_stencil_view, bool, api::command_list *cmd_list, api::resource_view dsv, const float *depth, const uint8_t *stencil, uint32_t rect_count, const api::rect *rects); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::clear_render_target_view, bool, api::command_list *cmd_list, api::resource_view rtv, const float color[4], uint32_t rect_count, const api::rect *rects); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::clear_unordered_access_view_uint, bool, api::command_list *cmd_list, api::resource_view uav, const uint32_t values[4], uint32_t rect_count, const api::rect *rects); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::clear_unordered_access_view_float, bool, api::command_list *cmd_list, api::resource_view uav, const float values[4], uint32_t rect_count, const api::rect *rects); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::generate_mipmaps, bool, api::command_list *cmd_list, api::resource_view srv); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::begin_query, bool, api::command_list *cmd_list, api::query_heap heap, api::query_type type, uint32_t index); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::end_query, bool, api::command_list *cmd_list, api::query_heap heap, api::query_type type, uint32_t index); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::copy_query_heap_results, bool, api::command_list *cmd_list, api::query_heap heap, api::query_type type, uint32_t first, uint32_t count, api::resource dest, uint64_t dest_offset, uint32_t stride); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::reset_command_list, void, api::command_list *cmd_list); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::close_command_list, void, api::command_list *cmd_list); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::execute_command_list, void, api::command_queue *queue, api::command_list *cmd_list); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::execute_secondary_command_list, void, api::command_list *cmd_list, api::command_list *secondary_cmd_list); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::present, void, api::command_queue *queue, api::swapchain *swapchain, const api::rect *source_rect, const api::rect *dest_rect, uint32_t dirty_rect_count, const api::rect *dirty_rects); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::reshade_present, void, api::effect_runtime *runtime); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::reshade_begin_effects, void, api::effect_runtime *runtime, api::command_list *cmd_list, api::resource_view rtv, api::resource_view rtv_srgb); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::reshade_finish_effects, void, api::effect_runtime *runtime, api::command_list *cmd_list, api::resource_view rtv, api::resource_view rtv_srgb); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::reshade_reloaded_effects, void, api::effect_runtime *runtime); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::reshade_set_uniform_value, bool, api::effect_runtime *runtime, api::effect_uniform_variable variable, const void *data, size_t size); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::reshade_set_technique_state, bool, api::effect_runtime *runtime, api::effect_technique technique, bool enabled); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::reshade_overlay, void, api::effect_runtime *runtime); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::reshade_screenshot, void, api::effect_runtime *runtime, const char *path); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::reshade_render_technique, void, api::effect_runtime *runtime, api::effect_technique technique, api::command_list *cmd_list, api::resource_view rtv, api::resource_view rtv_srgb); + + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::reshade_set_current_preset_path, void, api::effect_runtime *runtime, const char *path); + RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::reshade_reorder_techniques, bool, api::effect_runtime *runtime, size_t count, api::effect_technique *techniques); +} diff --git a/Plugin/include/reshade/reshade_overlay.hpp b/Plugin/include/reshade/reshade_overlay.hpp new file mode 100644 index 00000000..a68ffaef --- /dev/null +++ b/Plugin/include/reshade/reshade_overlay.hpp @@ -0,0 +1,870 @@ +/* + * Copyright (C) 2021 Patrick Mours + * Copyright (C) 2014-2023 Omar Cornut + * SPDX-License-Identifier: BSD-3-Clause OR MIT + */ + +#if defined(IMGUI_VERSION_NUM) + +#if IMGUI_VERSION_NUM != 18971 +#error Unexpected ImGui version, please update the "imgui.h" header to version 18971! +#endif + +// Check that the 'ImTextureID' type has the same size as 'reshade::api::resource_view' +static_assert(sizeof(ImTextureID) == 8, "missing \"#define ImTextureID ImU64\" before \"#include \""); + +struct imgui_function_table_18971 +{ + ImGuiIO&(*GetIO)(); + ImGuiStyle&(*GetStyle)(); + const char*(*GetVersion)(); + bool(*Begin)(const char* name, bool* p_open, ImGuiWindowFlags flags); + void(*End)(); + bool(*BeginChild)(const char* str_id, const ImVec2& size, bool border, ImGuiWindowFlags flags); + bool(*BeginChild2)(ImGuiID id, const ImVec2& size, bool border, ImGuiWindowFlags flags); + void(*EndChild)(); + bool(*IsWindowAppearing)(); + bool(*IsWindowCollapsed)(); + bool(*IsWindowFocused)(ImGuiFocusedFlags flags); + bool(*IsWindowHovered)(ImGuiHoveredFlags flags); + ImDrawList*(*GetWindowDrawList)(); + float(*GetWindowDpiScale)(); + ImVec2(*GetWindowPos)(); + ImVec2(*GetWindowSize)(); + float(*GetWindowWidth)(); + float(*GetWindowHeight)(); + void(*SetNextWindowPos)(const ImVec2& pos, ImGuiCond cond, const ImVec2& pivot); + void(*SetNextWindowSize)(const ImVec2& size, ImGuiCond cond); + void(*SetNextWindowSizeConstraints)(const ImVec2& size_min, const ImVec2& size_max, ImGuiSizeCallback custom_callback, void* custom_callback_data); + void(*SetNextWindowContentSize)(const ImVec2& size); + void(*SetNextWindowCollapsed)(bool collapsed, ImGuiCond cond); + void(*SetNextWindowFocus)(); + void(*SetNextWindowScroll)(const ImVec2& scroll); + void(*SetNextWindowBgAlpha)(float alpha); + void(*SetWindowPos)(const ImVec2& pos, ImGuiCond cond); + void(*SetWindowSize)(const ImVec2& size, ImGuiCond cond); + void(*SetWindowCollapsed)(bool collapsed, ImGuiCond cond); + void(*SetWindowFocus)(); + void(*SetWindowFontScale)(float scale); + void(*SetWindowPos2)(const char* name, const ImVec2& pos, ImGuiCond cond); + void(*SetWindowSize2)(const char* name, const ImVec2& size, ImGuiCond cond); + void(*SetWindowCollapsed2)(const char* name, bool collapsed, ImGuiCond cond); + void(*SetWindowFocus2)(const char* name); + ImVec2(*GetContentRegionAvail)(); + ImVec2(*GetContentRegionMax)(); + ImVec2(*GetWindowContentRegionMin)(); + ImVec2(*GetWindowContentRegionMax)(); + float(*GetScrollX)(); + float(*GetScrollY)(); + void(*SetScrollX)(float scroll_x); + void(*SetScrollY)(float scroll_y); + float(*GetScrollMaxX)(); + float(*GetScrollMaxY)(); + void(*SetScrollHereX)(float center_x_ratio); + void(*SetScrollHereY)(float center_y_ratio); + void(*SetScrollFromPosX)(float local_x, float center_x_ratio); + void(*SetScrollFromPosY)(float local_y, float center_y_ratio); + void(*PushFont)(ImFont* font); + void(*PopFont)(); + void(*PushStyleColor)(ImGuiCol idx, ImU32 col); + void(*PushStyleColor2)(ImGuiCol idx, const ImVec4& col); + void(*PopStyleColor)(int count); + void(*PushStyleVar)(ImGuiStyleVar idx, float val); + void(*PushStyleVar2)(ImGuiStyleVar idx, const ImVec2& val); + void(*PopStyleVar)(int count); + void(*PushTabStop)(bool tab_stop); + void(*PopTabStop)(); + void(*PushButtonRepeat)(bool repeat); + void(*PopButtonRepeat)(); + void(*PushItemWidth)(float item_width); + void(*PopItemWidth)(); + void(*SetNextItemWidth)(float item_width); + float(*CalcItemWidth)(); + void(*PushTextWrapPos)(float wrap_local_pos_x); + void(*PopTextWrapPos)(); + ImFont*(*GetFont)(); + float(*GetFontSize)(); + ImVec2(*GetFontTexUvWhitePixel)(); + ImU32(*GetColorU32)(ImGuiCol idx, float alpha_mul); + ImU32(*GetColorU322)(const ImVec4& col); + ImU32(*GetColorU323)(ImU32 col); + const ImVec4&(*GetStyleColorVec4)(ImGuiCol idx); + void(*Separator)(); + void(*SameLine)(float offset_from_start_x, float spacing); + void(*NewLine)(); + void(*Spacing)(); + void(*Dummy)(const ImVec2& size); + void(*Indent)(float indent_w); + void(*Unindent)(float indent_w); + void(*BeginGroup)(); + void(*EndGroup)(); + ImVec2(*GetCursorPos)(); + float(*GetCursorPosX)(); + float(*GetCursorPosY)(); + void(*SetCursorPos)(const ImVec2& local_pos); + void(*SetCursorPosX)(float local_x); + void(*SetCursorPosY)(float local_y); + ImVec2(*GetCursorStartPos)(); + ImVec2(*GetCursorScreenPos)(); + void(*SetCursorScreenPos)(const ImVec2& pos); + void(*AlignTextToFramePadding)(); + float(*GetTextLineHeight)(); + float(*GetTextLineHeightWithSpacing)(); + float(*GetFrameHeight)(); + float(*GetFrameHeightWithSpacing)(); + void(*PushID)(const char* str_id); + void(*PushID2)(const char* str_id_begin, const char* str_id_end); + void(*PushID3)(const void* ptr_id); + void(*PushID4)(int int_id); + void(*PopID)(); + ImGuiID(*GetID)(const char* str_id); + ImGuiID(*GetID2)(const char* str_id_begin, const char* str_id_end); + ImGuiID(*GetID3)(const void* ptr_id); + void(*TextUnformatted)(const char* text, const char* text_end); + void(*TextV)(const char* fmt, va_list args); + void(*TextColoredV)(const ImVec4& col, const char* fmt, va_list args); + void(*TextDisabledV)(const char* fmt, va_list args); + void(*TextWrappedV)(const char* fmt, va_list args); + void(*LabelTextV)(const char* label, const char* fmt, va_list args); + void(*BulletTextV)(const char* fmt, va_list args); + void(*SeparatorText)(const char* label); + bool(*Button)(const char* label, const ImVec2& size); + bool(*SmallButton)(const char* label); + bool(*InvisibleButton)(const char* str_id, const ImVec2& size, ImGuiButtonFlags flags); + bool(*ArrowButton)(const char* str_id, ImGuiDir dir); + bool(*Checkbox)(const char* label, bool* v); + bool(*CheckboxFlags)(const char* label, int* flags, int flags_value); + bool(*CheckboxFlags2)(const char* label, unsigned int* flags, unsigned int flags_value); + bool(*RadioButton)(const char* label, bool active); + bool(*RadioButton2)(const char* label, int* v, int v_button); + void(*ProgressBar)(float fraction, const ImVec2& size_arg, const char* overlay); + void(*Bullet)(); + void(*Image)(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col); + bool(*ImageButton)(const char* str_id, ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col); + bool(*BeginCombo)(const char* label, const char* preview_value, ImGuiComboFlags flags); + void(*EndCombo)(); + bool(*Combo)(const char* label, int* current_item, const char* const items[], int items_count, int popup_max_height_in_items); + bool(*Combo2)(const char* label, int* current_item, const char* items_separated_by_zeros, int popup_max_height_in_items); + bool(*Combo3)(const char* label, int* current_item, bool(*items_getter)(void* data, int idx, const char** out_text), void* data, int items_count, int popup_max_height_in_items); + bool(*DragFloat)(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags); + bool(*DragFloat2)(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags); + bool(*DragFloat3)(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags); + bool(*DragFloat4)(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags); + bool(*DragFloatRange2)(const char* label, float* v_current_min, float* v_current_max, float v_speed, float v_min, float v_max, const char* format, const char* format_max, ImGuiSliderFlags flags); + bool(*DragInt)(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags); + bool(*DragInt2)(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags); + bool(*DragInt3)(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags); + bool(*DragInt4)(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags); + bool(*DragIntRange2)(const char* label, int* v_current_min, int* v_current_max, float v_speed, int v_min, int v_max, const char* format, const char* format_max, ImGuiSliderFlags flags); + bool(*DragScalar)(const char* label, ImGuiDataType data_type, void* p_data, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags); + bool(*DragScalarN)(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags); + bool(*SliderFloat)(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags); + bool(*SliderFloat2)(const char* label, float v[2], float v_min, float v_max, const char* format, ImGuiSliderFlags flags); + bool(*SliderFloat3)(const char* label, float v[3], float v_min, float v_max, const char* format, ImGuiSliderFlags flags); + bool(*SliderFloat4)(const char* label, float v[4], float v_min, float v_max, const char* format, ImGuiSliderFlags flags); + bool(*SliderAngle)(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format, ImGuiSliderFlags flags); + bool(*SliderInt)(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags); + bool(*SliderInt2)(const char* label, int v[2], int v_min, int v_max, const char* format, ImGuiSliderFlags flags); + bool(*SliderInt3)(const char* label, int v[3], int v_min, int v_max, const char* format, ImGuiSliderFlags flags); + bool(*SliderInt4)(const char* label, int v[4], int v_min, int v_max, const char* format, ImGuiSliderFlags flags); + bool(*SliderScalar)(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags); + bool(*SliderScalarN)(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags); + bool(*VSliderFloat)(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags); + bool(*VSliderInt)(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags); + bool(*VSliderScalar)(const char* label, const ImVec2& size, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags); + bool(*InputText)(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data); + bool(*InputTextMultiline)(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data); + bool(*InputTextWithHint)(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data); + bool(*InputFloat)(const char* label, float* v, float step, float step_fast, const char* format, ImGuiInputTextFlags flags); + bool(*InputFloat2)(const char* label, float v[2], const char* format, ImGuiInputTextFlags flags); + bool(*InputFloat3)(const char* label, float v[3], const char* format, ImGuiInputTextFlags flags); + bool(*InputFloat4)(const char* label, float v[4], const char* format, ImGuiInputTextFlags flags); + bool(*InputInt)(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags flags); + bool(*InputInt2)(const char* label, int v[2], ImGuiInputTextFlags flags); + bool(*InputInt3)(const char* label, int v[3], ImGuiInputTextFlags flags); + bool(*InputInt4)(const char* label, int v[4], ImGuiInputTextFlags flags); + bool(*InputDouble)(const char* label, double* v, double step, double step_fast, const char* format, ImGuiInputTextFlags flags); + bool(*InputScalar)(const char* label, ImGuiDataType data_type, void* p_data, const void* p_step, const void* p_step_fast, const char* format, ImGuiInputTextFlags flags); + bool(*InputScalarN)(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_step, const void* p_step_fast, const char* format, ImGuiInputTextFlags flags); + bool(*ColorEdit3)(const char* label, float col[3], ImGuiColorEditFlags flags); + bool(*ColorEdit4)(const char* label, float col[4], ImGuiColorEditFlags flags); + bool(*ColorPicker3)(const char* label, float col[3], ImGuiColorEditFlags flags); + bool(*ColorPicker4)(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col); + bool(*ColorButton)(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, const ImVec2& size); + void(*SetColorEditOptions)(ImGuiColorEditFlags flags); + bool(*TreeNode)(const char* label); + bool(*TreeNodeV)(const char* str_id, const char* fmt, va_list args); + bool(*TreeNodeV2)(const void* ptr_id, const char* fmt, va_list args); + bool(*TreeNodeEx)(const char* label, ImGuiTreeNodeFlags flags); + bool(*TreeNodeExV)(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args); + bool(*TreeNodeExV2)(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args); + void(*TreePush)(const char* str_id); + void(*TreePush2)(const void* ptr_id); + void(*TreePop)(); + float(*GetTreeNodeToLabelSpacing)(); + bool(*CollapsingHeader)(const char* label, ImGuiTreeNodeFlags flags); + bool(*CollapsingHeader2)(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags); + void(*SetNextItemOpen)(bool is_open, ImGuiCond cond); + bool(*Selectable)(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size); + bool(*Selectable2)(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size); + bool(*BeginListBox)(const char* label, const ImVec2& size); + void(*EndListBox)(); + bool(*ListBox)(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items); + bool(*ListBox2)(const char* label, int* current_item, bool(*items_getter)(void* data, int idx, const char** out_text), void* data, int items_count, int height_in_items); + void(*PlotLines)(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride); + void(*PlotLines2)(const char* label, float(*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size); + void(*PlotHistogram)(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride); + void(*PlotHistogram2)(const char* label, float(*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size); + void(*Value)(const char* prefix, bool b); + void(*Value2)(const char* prefix, int v); + void(*Value3)(const char* prefix, unsigned int v); + void(*Value4)(const char* prefix, float v, const char* float_format); + bool(*BeginMenuBar)(); + void(*EndMenuBar)(); + bool(*BeginMainMenuBar)(); + void(*EndMainMenuBar)(); + bool(*BeginMenu)(const char* label, bool enabled); + void(*EndMenu)(); + bool(*MenuItem)(const char* label, const char* shortcut, bool selected, bool enabled); + bool(*MenuItem2)(const char* label, const char* shortcut, bool* p_selected, bool enabled); + bool(*BeginTooltip)(); + void(*EndTooltip)(); + void(*SetTooltipV)(const char* fmt, va_list args); + bool(*BeginItemTooltip)(); + void(*SetItemTooltipV)(const char* fmt, va_list args); + bool(*BeginPopup)(const char* str_id, ImGuiWindowFlags flags); + bool(*BeginPopupModal)(const char* name, bool* p_open, ImGuiWindowFlags flags); + void(*EndPopup)(); + void(*OpenPopup)(const char* str_id, ImGuiPopupFlags popup_flags); + void(*OpenPopup2)(ImGuiID id, ImGuiPopupFlags popup_flags); + void(*OpenPopupOnItemClick)(const char* str_id, ImGuiPopupFlags popup_flags); + void(*CloseCurrentPopup)(); + bool(*BeginPopupContextItem)(const char* str_id, ImGuiPopupFlags popup_flags); + bool(*BeginPopupContextWindow)(const char* str_id, ImGuiPopupFlags popup_flags); + bool(*BeginPopupContextVoid)(const char* str_id, ImGuiPopupFlags popup_flags); + bool(*IsPopupOpen)(const char* str_id, ImGuiPopupFlags flags); + bool(*BeginTable)(const char* str_id, int column, ImGuiTableFlags flags, const ImVec2& outer_size, float inner_width); + void(*EndTable)(); + void(*TableNextRow)(ImGuiTableRowFlags row_flags, float min_row_height); + bool(*TableNextColumn)(); + bool(*TableSetColumnIndex)(int column_n); + void(*TableSetupColumn)(const char* label, ImGuiTableColumnFlags flags, float init_width_or_weight, ImGuiID user_id); + void(*TableSetupScrollFreeze)(int cols, int rows); + void(*TableHeadersRow)(); + void(*TableHeader)(const char* label); + ImGuiTableSortSpecs*(*TableGetSortSpecs)(); + int(*TableGetColumnCount)(); + int(*TableGetColumnIndex)(); + int(*TableGetRowIndex)(); + const char*(*TableGetColumnName)(int column_n); + ImGuiTableColumnFlags(*TableGetColumnFlags)(int column_n); + void(*TableSetColumnEnabled)(int column_n, bool v); + void(*TableSetBgColor)(ImGuiTableBgTarget target, ImU32 color, int column_n); + void(*Columns)(int count, const char* id, bool border); + void(*NextColumn)(); + int(*GetColumnIndex)(); + float(*GetColumnWidth)(int column_index); + void(*SetColumnWidth)(int column_index, float width); + float(*GetColumnOffset)(int column_index); + void(*SetColumnOffset)(int column_index, float offset_x); + int(*GetColumnsCount)(); + bool(*BeginTabBar)(const char* str_id, ImGuiTabBarFlags flags); + void(*EndTabBar)(); + bool(*BeginTabItem)(const char* label, bool* p_open, ImGuiTabItemFlags flags); + void(*EndTabItem)(); + bool(*TabItemButton)(const char* label, ImGuiTabItemFlags flags); + void(*SetTabItemClosed)(const char* tab_or_docked_window_label); + ImGuiID(*DockSpace)(ImGuiID id, const ImVec2& size, ImGuiDockNodeFlags flags, const ImGuiWindowClass* window_class); + void(*SetNextWindowDockID)(ImGuiID dock_id, ImGuiCond cond); + void(*SetNextWindowClass)(const ImGuiWindowClass* window_class); + ImGuiID(*GetWindowDockID)(); + bool(*IsWindowDocked)(); + bool(*BeginDragDropSource)(ImGuiDragDropFlags flags); + bool(*SetDragDropPayload)(const char* type, const void* data, size_t sz, ImGuiCond cond); + void(*EndDragDropSource)(); + bool(*BeginDragDropTarget)(); + const ImGuiPayload*(*AcceptDragDropPayload)(const char* type, ImGuiDragDropFlags flags); + void(*EndDragDropTarget)(); + const ImGuiPayload*(*GetDragDropPayload)(); + void(*BeginDisabled)(bool disabled); + void(*EndDisabled)(); + void(*PushClipRect)(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect); + void(*PopClipRect)(); + void(*SetItemDefaultFocus)(); + void(*SetKeyboardFocusHere)(int offset); + void(*SetNextItemAllowOverlap)(); + bool(*IsItemHovered)(ImGuiHoveredFlags flags); + bool(*IsItemActive)(); + bool(*IsItemFocused)(); + bool(*IsItemClicked)(ImGuiMouseButton mouse_button); + bool(*IsItemVisible)(); + bool(*IsItemEdited)(); + bool(*IsItemActivated)(); + bool(*IsItemDeactivated)(); + bool(*IsItemDeactivatedAfterEdit)(); + bool(*IsItemToggledOpen)(); + bool(*IsAnyItemHovered)(); + bool(*IsAnyItemActive)(); + bool(*IsAnyItemFocused)(); + ImGuiID(*GetItemID)(); + ImVec2(*GetItemRectMin)(); + ImVec2(*GetItemRectMax)(); + ImVec2(*GetItemRectSize)(); + ImDrawList*(*GetBackgroundDrawList)(); + ImDrawList*(*GetForegroundDrawList)(); + ImDrawList*(*GetBackgroundDrawList2)(ImGuiViewport* viewport); + ImDrawList*(*GetForegroundDrawList2)(ImGuiViewport* viewport); + bool(*IsRectVisible)(const ImVec2& size); + bool(*IsRectVisible2)(const ImVec2& rect_min, const ImVec2& rect_max); + double(*GetTime)(); + int(*GetFrameCount)(); + ImDrawListSharedData*(*GetDrawListSharedData)(); + const char*(*GetStyleColorName)(ImGuiCol idx); + void(*SetStateStorage)(ImGuiStorage* storage); + ImGuiStorage*(*GetStateStorage)(); + bool(*BeginChildFrame)(ImGuiID id, const ImVec2& size, ImGuiWindowFlags flags); + void(*EndChildFrame)(); + ImVec2(*CalcTextSize)(const char* text, const char* text_end, bool hide_text_after_double_hash, float wrap_width); + ImVec4(*ColorConvertU32ToFloat4)(ImU32 in); + ImU32(*ColorConvertFloat4ToU32)(const ImVec4& in); + void(*ColorConvertRGBtoHSV)(float r, float g, float b, float& out_h, float& out_s, float& out_v); + void(*ColorConvertHSVtoRGB)(float h, float s, float v, float& out_r, float& out_g, float& out_b); + bool(*IsKeyDown)(ImGuiKey key); + bool(*IsKeyPressed)(ImGuiKey key, bool repeat); + bool(*IsKeyReleased)(ImGuiKey key); + int(*GetKeyPressedAmount)(ImGuiKey key, float repeat_delay, float rate); + const char*(*GetKeyName)(ImGuiKey key); + void(*SetNextFrameWantCaptureKeyboard)(bool want_capture_keyboard); + bool(*IsMouseDown)(ImGuiMouseButton button); + bool(*IsMouseClicked)(ImGuiMouseButton button, bool repeat); + bool(*IsMouseReleased)(ImGuiMouseButton button); + bool(*IsMouseDoubleClicked)(ImGuiMouseButton button); + int(*GetMouseClickedCount)(ImGuiMouseButton button); + bool(*IsMouseHoveringRect)(const ImVec2& r_min, const ImVec2& r_max, bool clip); + bool(*IsMousePosValid)(const ImVec2* mouse_pos); + bool(*IsAnyMouseDown)(); + ImVec2(*GetMousePos)(); + ImVec2(*GetMousePosOnOpeningCurrentPopup)(); + bool(*IsMouseDragging)(ImGuiMouseButton button, float lock_threshold); + ImVec2(*GetMouseDragDelta)(ImGuiMouseButton button, float lock_threshold); + void(*ResetMouseDragDelta)(ImGuiMouseButton button); + ImGuiMouseCursor(*GetMouseCursor)(); + void(*SetMouseCursor)(ImGuiMouseCursor cursor_type); + void(*SetNextFrameWantCaptureMouse)(bool want_capture_mouse); + const char*(*GetClipboardText)(); + void(*SetClipboardText)(const char* text); + void(*SetAllocatorFunctions)(ImGuiMemAllocFunc alloc_func, ImGuiMemFreeFunc free_func, void* user_data); + void(*GetAllocatorFunctions)(ImGuiMemAllocFunc* p_alloc_func, ImGuiMemFreeFunc* p_free_func, void** p_user_data); + void*(*MemAlloc)(size_t size); + void(*MemFree)(void* ptr); + int(*ImGuiStorage_GetInt)(const ImGuiStorage *_this, ImGuiID key, int default_val); + void(*ImGuiStorage_SetInt)(ImGuiStorage *_this, ImGuiID key, int val); + bool(*ImGuiStorage_GetBool)(const ImGuiStorage *_this, ImGuiID key, bool default_val); + void(*ImGuiStorage_SetBool)(ImGuiStorage *_this, ImGuiID key, bool val); + float(*ImGuiStorage_GetFloat)(const ImGuiStorage *_this, ImGuiID key, float default_val); + void(*ImGuiStorage_SetFloat)(ImGuiStorage *_this, ImGuiID key, float val); + void*(*ImGuiStorage_GetVoidPtr)(const ImGuiStorage *_this, ImGuiID key); + void(*ImGuiStorage_SetVoidPtr)(ImGuiStorage *_this, ImGuiID key, void* val); + int*(*ImGuiStorage_GetIntRef)(ImGuiStorage *_this, ImGuiID key, int default_val); + bool*(*ImGuiStorage_GetBoolRef)(ImGuiStorage *_this, ImGuiID key, bool default_val); + float*(*ImGuiStorage_GetFloatRef)(ImGuiStorage *_this, ImGuiID key, float default_val); + void**(*ImGuiStorage_GetVoidPtrRef)(ImGuiStorage *_this, ImGuiID key, void* default_val); + void(*ImGuiStorage_SetAllInt)(ImGuiStorage *_this, int val); + void(*ImGuiStorage_BuildSortByKey)(ImGuiStorage *_this); + void(*ConstructImGuiListClipper)(ImGuiListClipper *_this); + void(*DestructImGuiListClipper)(ImGuiListClipper *_this); + void(*ImGuiListClipper_Begin)(ImGuiListClipper *_this, int items_count, float items_height); + void(*ImGuiListClipper_End)(ImGuiListClipper *_this); + bool(*ImGuiListClipper_Step)(ImGuiListClipper *_this); + void(*ImGuiListClipper_IncludeRangeByIndices)(ImGuiListClipper *_this, int item_begin, int item_end); + void(*ImDrawList_PushClipRect)(ImDrawList *_this, const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect); + void(*ImDrawList_PushClipRectFullScreen)(ImDrawList *_this); + void(*ImDrawList_PopClipRect)(ImDrawList *_this); + void(*ImDrawList_PushTextureID)(ImDrawList *_this, ImTextureID texture_id); + void(*ImDrawList_PopTextureID)(ImDrawList *_this); + void(*ImDrawList_AddLine)(ImDrawList *_this, const ImVec2& p1, const ImVec2& p2, ImU32 col, float thickness); + void(*ImDrawList_AddRect)(ImDrawList *_this, const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawFlags flags, float thickness); + void(*ImDrawList_AddRectFilled)(ImDrawList *_this, const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawFlags flags); + void(*ImDrawList_AddRectFilledMultiColor)(ImDrawList *_this, const ImVec2& p_min, const ImVec2& p_max, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left); + void(*ImDrawList_AddQuad)(ImDrawList *_this, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness); + void(*ImDrawList_AddQuadFilled)(ImDrawList *_this, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col); + void(*ImDrawList_AddTriangle)(ImDrawList *_this, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness); + void(*ImDrawList_AddTriangleFilled)(ImDrawList *_this, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col); + void(*ImDrawList_AddCircle)(ImDrawList *_this, const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness); + void(*ImDrawList_AddCircleFilled)(ImDrawList *_this, const ImVec2& center, float radius, ImU32 col, int num_segments); + void(*ImDrawList_AddNgon)(ImDrawList *_this, const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness); + void(*ImDrawList_AddNgonFilled)(ImDrawList *_this, const ImVec2& center, float radius, ImU32 col, int num_segments); + void(*ImDrawList_AddText)(ImDrawList *_this, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end); + void(*ImDrawList_AddText2)(ImDrawList *_this, const ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end, float wrap_width, const ImVec4* cpu_fine_clip_rect); + void(*ImDrawList_AddPolyline)(ImDrawList *_this, const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags, float thickness); + void(*ImDrawList_AddConvexPolyFilled)(ImDrawList *_this, const ImVec2* points, int num_points, ImU32 col); + void(*ImDrawList_AddBezierCubic)(ImDrawList *_this, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness, int num_segments); + void(*ImDrawList_AddBezierQuadratic)(ImDrawList *_this, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness, int num_segments); + void(*ImDrawList_AddImage)(ImDrawList *_this, ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col); + void(*ImDrawList_AddImageQuad)(ImDrawList *_this, ImTextureID user_texture_id, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& uv1, const ImVec2& uv2, const ImVec2& uv3, const ImVec2& uv4, ImU32 col); + void(*ImDrawList_AddImageRounded)(ImDrawList *_this, ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float rounding, ImDrawFlags flags); + void(*ImDrawList_PathArcTo)(ImDrawList *_this, const ImVec2& center, float radius, float a_min, float a_max, int num_segments); + void(*ImDrawList_PathArcToFast)(ImDrawList *_this, const ImVec2& center, float radius, int a_min_of_12, int a_max_of_12); + void(*ImDrawList_PathBezierCubicCurveTo)(ImDrawList *_this, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments); + void(*ImDrawList_PathBezierQuadraticCurveTo)(ImDrawList *_this, const ImVec2& p2, const ImVec2& p3, int num_segments); + void(*ImDrawList_PathRect)(ImDrawList *_this, const ImVec2& rect_min, const ImVec2& rect_max, float rounding, ImDrawFlags flags); + void(*ImDrawList_AddCallback)(ImDrawList *_this, ImDrawCallback callback, void* callback_data); + void(*ImDrawList_AddDrawCmd)(ImDrawList *_this); + ImDrawList*(*ImDrawList_CloneOutput)(const ImDrawList *_this); + void(*ImDrawList_PrimReserve)(ImDrawList *_this, int idx_count, int vtx_count); + void(*ImDrawList_PrimUnreserve)(ImDrawList *_this, int idx_count, int vtx_count); + void(*ImDrawList_PrimRect)(ImDrawList *_this, const ImVec2& a, const ImVec2& b, ImU32 col); + void(*ImDrawList_PrimRectUV)(ImDrawList *_this, const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, ImU32 col); + void(*ImDrawList_PrimQuadUV)(ImDrawList *_this, const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, const ImVec2& uv_a, const ImVec2& uv_b, const ImVec2& uv_c, const ImVec2& uv_d, ImU32 col); + void(*ConstructImFont)(ImFont *_this); + void(*DestructImFont)(ImFont *_this); + const ImFontGlyph*(*ImFont_FindGlyph)(const ImFont *_this, ImWchar c); + const ImFontGlyph*(*ImFont_FindGlyphNoFallback)(const ImFont *_this, ImWchar c); + ImVec2(*ImFont_CalcTextSizeA)(const ImFont *_this, float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** remaining); + const char*(*ImFont_CalcWordWrapPositionA)(const ImFont *_this, float scale, const char* text, const char* text_end, float wrap_width); + void(*ImFont_RenderChar)(const ImFont *_this, ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, ImWchar c); + void(*ImFont_RenderText)(const ImFont *_this, ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width, bool cpu_fine_clip); + +}; + +using imgui_function_table = imgui_function_table_18971; + +inline const imgui_function_table *&imgui_function_table_instance() +{ + static const imgui_function_table *instance = nullptr; + return instance; +} + +#ifndef RESHADE_API_LIBRARY_EXPORT + +namespace ImGui +{ + inline ImGuiIO& GetIO() { return imgui_function_table_instance()->GetIO(); } + inline ImGuiStyle& GetStyle() { return imgui_function_table_instance()->GetStyle(); } + inline const char* GetVersion() { return imgui_function_table_instance()->GetVersion(); } + inline bool Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) { return imgui_function_table_instance()->Begin(name, p_open, flags); } + inline void End() { imgui_function_table_instance()->End(); } + inline bool BeginChild(const char* str_id, const ImVec2& size, bool border, ImGuiWindowFlags flags) { return imgui_function_table_instance()->BeginChild(str_id, size, border, flags); } + inline bool BeginChild(ImGuiID id, const ImVec2& size, bool border, ImGuiWindowFlags flags) { return imgui_function_table_instance()->BeginChild2(id, size, border, flags); } + inline void EndChild() { imgui_function_table_instance()->EndChild(); } + inline bool IsWindowAppearing() { return imgui_function_table_instance()->IsWindowAppearing(); } + inline bool IsWindowCollapsed() { return imgui_function_table_instance()->IsWindowCollapsed(); } + inline bool IsWindowFocused(ImGuiFocusedFlags flags) { return imgui_function_table_instance()->IsWindowFocused(flags); } + inline bool IsWindowHovered(ImGuiHoveredFlags flags) { return imgui_function_table_instance()->IsWindowHovered(flags); } + inline ImDrawList* GetWindowDrawList() { return imgui_function_table_instance()->GetWindowDrawList(); } + inline float GetWindowDpiScale() { return imgui_function_table_instance()->GetWindowDpiScale(); } + inline ImVec2 GetWindowPos() { return imgui_function_table_instance()->GetWindowPos(); } + inline ImVec2 GetWindowSize() { return imgui_function_table_instance()->GetWindowSize(); } + inline float GetWindowWidth() { return imgui_function_table_instance()->GetWindowWidth(); } + inline float GetWindowHeight() { return imgui_function_table_instance()->GetWindowHeight(); } + inline void SetNextWindowPos(const ImVec2& pos, ImGuiCond cond, const ImVec2& pivot) { imgui_function_table_instance()->SetNextWindowPos(pos, cond, pivot); } + inline void SetNextWindowSize(const ImVec2& size, ImGuiCond cond) { imgui_function_table_instance()->SetNextWindowSize(size, cond); } + inline void SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& size_max, ImGuiSizeCallback custom_callback, void* custom_callback_data) { imgui_function_table_instance()->SetNextWindowSizeConstraints(size_min, size_max, custom_callback, custom_callback_data); } + inline void SetNextWindowContentSize(const ImVec2& size) { imgui_function_table_instance()->SetNextWindowContentSize(size); } + inline void SetNextWindowCollapsed(bool collapsed, ImGuiCond cond) { imgui_function_table_instance()->SetNextWindowCollapsed(collapsed, cond); } + inline void SetNextWindowFocus() { imgui_function_table_instance()->SetNextWindowFocus(); } + inline void SetNextWindowScroll(const ImVec2& scroll) { imgui_function_table_instance()->SetNextWindowScroll(scroll); } + inline void SetNextWindowBgAlpha(float alpha) { imgui_function_table_instance()->SetNextWindowBgAlpha(alpha); } + inline void SetWindowPos(const ImVec2& pos, ImGuiCond cond) { imgui_function_table_instance()->SetWindowPos(pos, cond); } + inline void SetWindowSize(const ImVec2& size, ImGuiCond cond) { imgui_function_table_instance()->SetWindowSize(size, cond); } + inline void SetWindowCollapsed(bool collapsed, ImGuiCond cond) { imgui_function_table_instance()->SetWindowCollapsed(collapsed, cond); } + inline void SetWindowFocus() { imgui_function_table_instance()->SetWindowFocus(); } + inline void SetWindowFontScale(float scale) { imgui_function_table_instance()->SetWindowFontScale(scale); } + inline void SetWindowPos(const char* name, const ImVec2& pos, ImGuiCond cond) { imgui_function_table_instance()->SetWindowPos2(name, pos, cond); } + inline void SetWindowSize(const char* name, const ImVec2& size, ImGuiCond cond) { imgui_function_table_instance()->SetWindowSize2(name, size, cond); } + inline void SetWindowCollapsed(const char* name, bool collapsed, ImGuiCond cond) { imgui_function_table_instance()->SetWindowCollapsed2(name, collapsed, cond); } + inline void SetWindowFocus(const char* name) { imgui_function_table_instance()->SetWindowFocus2(name); } + inline ImVec2 GetContentRegionAvail() { return imgui_function_table_instance()->GetContentRegionAvail(); } + inline ImVec2 GetContentRegionMax() { return imgui_function_table_instance()->GetContentRegionMax(); } + inline ImVec2 GetWindowContentRegionMin() { return imgui_function_table_instance()->GetWindowContentRegionMin(); } + inline ImVec2 GetWindowContentRegionMax() { return imgui_function_table_instance()->GetWindowContentRegionMax(); } + inline float GetScrollX() { return imgui_function_table_instance()->GetScrollX(); } + inline float GetScrollY() { return imgui_function_table_instance()->GetScrollY(); } + inline void SetScrollX(float scroll_x) { imgui_function_table_instance()->SetScrollX(scroll_x); } + inline void SetScrollY(float scroll_y) { imgui_function_table_instance()->SetScrollY(scroll_y); } + inline float GetScrollMaxX() { return imgui_function_table_instance()->GetScrollMaxX(); } + inline float GetScrollMaxY() { return imgui_function_table_instance()->GetScrollMaxY(); } + inline void SetScrollHereX(float center_x_ratio) { imgui_function_table_instance()->SetScrollHereX(center_x_ratio); } + inline void SetScrollHereY(float center_y_ratio) { imgui_function_table_instance()->SetScrollHereY(center_y_ratio); } + inline void SetScrollFromPosX(float local_x, float center_x_ratio) { imgui_function_table_instance()->SetScrollFromPosX(local_x, center_x_ratio); } + inline void SetScrollFromPosY(float local_y, float center_y_ratio) { imgui_function_table_instance()->SetScrollFromPosY(local_y, center_y_ratio); } + inline void PushFont(ImFont* font) { imgui_function_table_instance()->PushFont(font); } + inline void PopFont() { imgui_function_table_instance()->PopFont(); } + inline void PushStyleColor(ImGuiCol idx, ImU32 col) { imgui_function_table_instance()->PushStyleColor(idx, col); } + inline void PushStyleColor(ImGuiCol idx, const ImVec4& col) { imgui_function_table_instance()->PushStyleColor2(idx, col); } + inline void PopStyleColor(int count) { imgui_function_table_instance()->PopStyleColor(count); } + inline void PushStyleVar(ImGuiStyleVar idx, float val) { imgui_function_table_instance()->PushStyleVar(idx, val); } + inline void PushStyleVar(ImGuiStyleVar idx, const ImVec2& val) { imgui_function_table_instance()->PushStyleVar2(idx, val); } + inline void PopStyleVar(int count) { imgui_function_table_instance()->PopStyleVar(count); } + inline void PushTabStop(bool tab_stop) { imgui_function_table_instance()->PushTabStop(tab_stop); } + inline void PopTabStop() { imgui_function_table_instance()->PopTabStop(); } + inline void PushButtonRepeat(bool repeat) { imgui_function_table_instance()->PushButtonRepeat(repeat); } + inline void PopButtonRepeat() { imgui_function_table_instance()->PopButtonRepeat(); } + inline void PushItemWidth(float item_width) { imgui_function_table_instance()->PushItemWidth(item_width); } + inline void PopItemWidth() { imgui_function_table_instance()->PopItemWidth(); } + inline void SetNextItemWidth(float item_width) { imgui_function_table_instance()->SetNextItemWidth(item_width); } + inline float CalcItemWidth() { return imgui_function_table_instance()->CalcItemWidth(); } + inline void PushTextWrapPos(float wrap_local_pos_x) { imgui_function_table_instance()->PushTextWrapPos(wrap_local_pos_x); } + inline void PopTextWrapPos() { imgui_function_table_instance()->PopTextWrapPos(); } + inline ImFont* GetFont() { return imgui_function_table_instance()->GetFont(); } + inline float GetFontSize() { return imgui_function_table_instance()->GetFontSize(); } + inline ImVec2 GetFontTexUvWhitePixel() { return imgui_function_table_instance()->GetFontTexUvWhitePixel(); } + inline ImU32 GetColorU32(ImGuiCol idx, float alpha_mul) { return imgui_function_table_instance()->GetColorU32(idx, alpha_mul); } + inline ImU32 GetColorU32(const ImVec4& col) { return imgui_function_table_instance()->GetColorU322(col); } + inline ImU32 GetColorU32(ImU32 col) { return imgui_function_table_instance()->GetColorU323(col); } + inline const ImVec4& GetStyleColorVec4(ImGuiCol idx) { return imgui_function_table_instance()->GetStyleColorVec4(idx); } + inline void Separator() { imgui_function_table_instance()->Separator(); } + inline void SameLine(float offset_from_start_x, float spacing) { imgui_function_table_instance()->SameLine(offset_from_start_x, spacing); } + inline void NewLine() { imgui_function_table_instance()->NewLine(); } + inline void Spacing() { imgui_function_table_instance()->Spacing(); } + inline void Dummy(const ImVec2& size) { imgui_function_table_instance()->Dummy(size); } + inline void Indent(float indent_w) { imgui_function_table_instance()->Indent(indent_w); } + inline void Unindent(float indent_w) { imgui_function_table_instance()->Unindent(indent_w); } + inline void BeginGroup() { imgui_function_table_instance()->BeginGroup(); } + inline void EndGroup() { imgui_function_table_instance()->EndGroup(); } + inline ImVec2 GetCursorPos() { return imgui_function_table_instance()->GetCursorPos(); } + inline float GetCursorPosX() { return imgui_function_table_instance()->GetCursorPosX(); } + inline float GetCursorPosY() { return imgui_function_table_instance()->GetCursorPosY(); } + inline void SetCursorPos(const ImVec2& local_pos) { imgui_function_table_instance()->SetCursorPos(local_pos); } + inline void SetCursorPosX(float local_x) { imgui_function_table_instance()->SetCursorPosX(local_x); } + inline void SetCursorPosY(float local_y) { imgui_function_table_instance()->SetCursorPosY(local_y); } + inline ImVec2 GetCursorStartPos() { return imgui_function_table_instance()->GetCursorStartPos(); } + inline ImVec2 GetCursorScreenPos() { return imgui_function_table_instance()->GetCursorScreenPos(); } + inline void SetCursorScreenPos(const ImVec2& pos) { imgui_function_table_instance()->SetCursorScreenPos(pos); } + inline void AlignTextToFramePadding() { imgui_function_table_instance()->AlignTextToFramePadding(); } + inline float GetTextLineHeight() { return imgui_function_table_instance()->GetTextLineHeight(); } + inline float GetTextLineHeightWithSpacing() { return imgui_function_table_instance()->GetTextLineHeightWithSpacing(); } + inline float GetFrameHeight() { return imgui_function_table_instance()->GetFrameHeight(); } + inline float GetFrameHeightWithSpacing() { return imgui_function_table_instance()->GetFrameHeightWithSpacing(); } + inline void PushID(const char* str_id) { imgui_function_table_instance()->PushID(str_id); } + inline void PushID(const char* str_id_begin, const char* str_id_end) { imgui_function_table_instance()->PushID2(str_id_begin, str_id_end); } + inline void PushID(const void* ptr_id) { imgui_function_table_instance()->PushID3(ptr_id); } + inline void PushID(int int_id) { imgui_function_table_instance()->PushID4(int_id); } + inline void PopID() { imgui_function_table_instance()->PopID(); } + inline ImGuiID GetID(const char* str_id) { return imgui_function_table_instance()->GetID(str_id); } + inline ImGuiID GetID(const char* str_id_begin, const char* str_id_end) { return imgui_function_table_instance()->GetID2(str_id_begin, str_id_end); } + inline ImGuiID GetID(const void* ptr_id) { return imgui_function_table_instance()->GetID3(ptr_id); } + inline void TextUnformatted(const char* text, const char* text_end) { imgui_function_table_instance()->TextUnformatted(text, text_end); } + inline void Text(const char* fmt, ...) { va_list args; va_start(args, fmt); imgui_function_table_instance()->TextV(fmt, args); va_end(args); } + inline void TextV(const char* fmt, va_list args) { imgui_function_table_instance()->TextV(fmt, args); } + inline void TextColored(const ImVec4& col, const char* fmt, ...) { va_list args; va_start(args, fmt); imgui_function_table_instance()->TextColoredV(col, fmt, args); va_end(args); } + inline void TextColoredV(const ImVec4& col, const char* fmt, va_list args) { imgui_function_table_instance()->TextColoredV(col, fmt, args); } + inline void TextDisabled(const char* fmt, ...) { va_list args; va_start(args, fmt); imgui_function_table_instance()->TextDisabledV(fmt, args); va_end(args); } + inline void TextDisabledV(const char* fmt, va_list args) { imgui_function_table_instance()->TextDisabledV(fmt, args); } + inline void TextWrapped(const char* fmt, ...) { va_list args; va_start(args, fmt); imgui_function_table_instance()->TextWrappedV(fmt, args); va_end(args); } + inline void TextWrappedV(const char* fmt, va_list args) { imgui_function_table_instance()->TextWrappedV(fmt, args); } + inline void LabelText(const char* label, const char* fmt, ...) { va_list args; va_start(args, fmt); imgui_function_table_instance()->LabelTextV(label, fmt, args); va_end(args); } + inline void LabelTextV(const char* label, const char* fmt, va_list args) { imgui_function_table_instance()->LabelTextV(label, fmt, args); } + inline void BulletText(const char* fmt, ...) { va_list args; va_start(args, fmt); imgui_function_table_instance()->BulletTextV(fmt, args); va_end(args); } + inline void BulletTextV(const char* fmt, va_list args) { imgui_function_table_instance()->BulletTextV(fmt, args); } + inline void SeparatorText(const char* label) { imgui_function_table_instance()->SeparatorText(label); } + inline bool Button(const char* label, const ImVec2& size) { return imgui_function_table_instance()->Button(label, size); } + inline bool SmallButton(const char* label) { return imgui_function_table_instance()->SmallButton(label); } + inline bool InvisibleButton(const char* str_id, const ImVec2& size, ImGuiButtonFlags flags) { return imgui_function_table_instance()->InvisibleButton(str_id, size, flags); } + inline bool ArrowButton(const char* str_id, ImGuiDir dir) { return imgui_function_table_instance()->ArrowButton(str_id, dir); } + inline bool Checkbox(const char* label, bool* v) { return imgui_function_table_instance()->Checkbox(label, v); } + inline bool CheckboxFlags(const char* label, int* flags, int flags_value) { return imgui_function_table_instance()->CheckboxFlags(label, flags, flags_value); } + inline bool CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value) { return imgui_function_table_instance()->CheckboxFlags2(label, flags, flags_value); } + inline bool RadioButton(const char* label, bool active) { return imgui_function_table_instance()->RadioButton(label, active); } + inline bool RadioButton(const char* label, int* v, int v_button) { return imgui_function_table_instance()->RadioButton2(label, v, v_button); } + inline void ProgressBar(float fraction, const ImVec2& size_arg, const char* overlay) { imgui_function_table_instance()->ProgressBar(fraction, size_arg, overlay); } + inline void Bullet() { imgui_function_table_instance()->Bullet(); } + inline void Image(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col) { imgui_function_table_instance()->Image(user_texture_id, size, uv0, uv1, tint_col, border_col); } + inline bool ImageButton(const char* str_id, ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col) { return imgui_function_table_instance()->ImageButton(str_id, user_texture_id, size, uv0, uv1, bg_col, tint_col); } + inline bool BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags) { return imgui_function_table_instance()->BeginCombo(label, preview_value, flags); } + inline void EndCombo() { imgui_function_table_instance()->EndCombo(); } + inline bool Combo(const char* label, int* current_item, const char* const items[], int items_count, int popup_max_height_in_items) { return imgui_function_table_instance()->Combo(label, current_item, items, items_count, popup_max_height_in_items); } + inline bool Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int popup_max_height_in_items) { return imgui_function_table_instance()->Combo2(label, current_item, items_separated_by_zeros, popup_max_height_in_items); } + inline bool Combo(const char* label, int* current_item, bool(*items_getter)(void* data, int idx, const char** out_text), void* data, int items_count, int popup_max_height_in_items) { return imgui_function_table_instance()->Combo3(label, current_item, items_getter, data, items_count, popup_max_height_in_items); } + inline bool DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->DragFloat(label, v, v_speed, v_min, v_max, format, flags); } + inline bool DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->DragFloat2(label, v, v_speed, v_min, v_max, format, flags); } + inline bool DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->DragFloat3(label, v, v_speed, v_min, v_max, format, flags); } + inline bool DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->DragFloat4(label, v, v_speed, v_min, v_max, format, flags); } + inline bool DragFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_speed, float v_min, float v_max, const char* format, const char* format_max, ImGuiSliderFlags flags) { return imgui_function_table_instance()->DragFloatRange2(label, v_current_min, v_current_max, v_speed, v_min, v_max, format, format_max, flags); } + inline bool DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->DragInt(label, v, v_speed, v_min, v_max, format, flags); } + inline bool DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->DragInt2(label, v, v_speed, v_min, v_max, format, flags); } + inline bool DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->DragInt3(label, v, v_speed, v_min, v_max, format, flags); } + inline bool DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->DragInt4(label, v, v_speed, v_min, v_max, format, flags); } + inline bool DragIntRange2(const char* label, int* v_current_min, int* v_current_max, float v_speed, int v_min, int v_max, const char* format, const char* format_max, ImGuiSliderFlags flags) { return imgui_function_table_instance()->DragIntRange2(label, v_current_min, v_current_max, v_speed, v_min, v_max, format, format_max, flags); } + inline bool DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->DragScalar(label, data_type, p_data, v_speed, p_min, p_max, format, flags); } + inline bool DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->DragScalarN(label, data_type, p_data, components, v_speed, p_min, p_max, format, flags); } + inline bool SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->SliderFloat(label, v, v_min, v_max, format, flags); } + inline bool SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->SliderFloat2(label, v, v_min, v_max, format, flags); } + inline bool SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->SliderFloat3(label, v, v_min, v_max, format, flags); } + inline bool SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->SliderFloat4(label, v, v_min, v_max, format, flags); } + inline bool SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->SliderAngle(label, v_rad, v_degrees_min, v_degrees_max, format, flags); } + inline bool SliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->SliderInt(label, v, v_min, v_max, format, flags); } + inline bool SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->SliderInt2(label, v, v_min, v_max, format, flags); } + inline bool SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->SliderInt3(label, v, v_min, v_max, format, flags); } + inline bool SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->SliderInt4(label, v, v_min, v_max, format, flags); } + inline bool SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->SliderScalar(label, data_type, p_data, p_min, p_max, format, flags); } + inline bool SliderScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->SliderScalarN(label, data_type, p_data, components, p_min, p_max, format, flags); } + inline bool VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->VSliderFloat(label, size, v, v_min, v_max, format, flags); } + inline bool VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->VSliderInt(label, size, v, v_min, v_max, format, flags); } + inline bool VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) { return imgui_function_table_instance()->VSliderScalar(label, size, data_type, p_data, p_min, p_max, format, flags); } + inline bool InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) { return imgui_function_table_instance()->InputText(label, buf, buf_size, flags, callback, user_data); } + inline bool InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) { return imgui_function_table_instance()->InputTextMultiline(label, buf, buf_size, size, flags, callback, user_data); } + inline bool InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) { return imgui_function_table_instance()->InputTextWithHint(label, hint, buf, buf_size, flags, callback, user_data); } + inline bool InputFloat(const char* label, float* v, float step, float step_fast, const char* format, ImGuiInputTextFlags flags) { return imgui_function_table_instance()->InputFloat(label, v, step, step_fast, format, flags); } + inline bool InputFloat2(const char* label, float v[2], const char* format, ImGuiInputTextFlags flags) { return imgui_function_table_instance()->InputFloat2(label, v, format, flags); } + inline bool InputFloat3(const char* label, float v[3], const char* format, ImGuiInputTextFlags flags) { return imgui_function_table_instance()->InputFloat3(label, v, format, flags); } + inline bool InputFloat4(const char* label, float v[4], const char* format, ImGuiInputTextFlags flags) { return imgui_function_table_instance()->InputFloat4(label, v, format, flags); } + inline bool InputInt(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags flags) { return imgui_function_table_instance()->InputInt(label, v, step, step_fast, flags); } + inline bool InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags) { return imgui_function_table_instance()->InputInt2(label, v, flags); } + inline bool InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags) { return imgui_function_table_instance()->InputInt3(label, v, flags); } + inline bool InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags) { return imgui_function_table_instance()->InputInt4(label, v, flags); } + inline bool InputDouble(const char* label, double* v, double step, double step_fast, const char* format, ImGuiInputTextFlags flags) { return imgui_function_table_instance()->InputDouble(label, v, step, step_fast, format, flags); } + inline bool InputScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_step, const void* p_step_fast, const char* format, ImGuiInputTextFlags flags) { return imgui_function_table_instance()->InputScalar(label, data_type, p_data, p_step, p_step_fast, format, flags); } + inline bool InputScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_step, const void* p_step_fast, const char* format, ImGuiInputTextFlags flags) { return imgui_function_table_instance()->InputScalarN(label, data_type, p_data, components, p_step, p_step_fast, format, flags); } + inline bool ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags) { return imgui_function_table_instance()->ColorEdit3(label, col, flags); } + inline bool ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags) { return imgui_function_table_instance()->ColorEdit4(label, col, flags); } + inline bool ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags) { return imgui_function_table_instance()->ColorPicker3(label, col, flags); } + inline bool ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col) { return imgui_function_table_instance()->ColorPicker4(label, col, flags, ref_col); } + inline bool ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, const ImVec2& size) { return imgui_function_table_instance()->ColorButton(desc_id, col, flags, size); } + inline void SetColorEditOptions(ImGuiColorEditFlags flags) { imgui_function_table_instance()->SetColorEditOptions(flags); } + inline bool TreeNode(const char* label) { return imgui_function_table_instance()->TreeNode(label); } + inline bool TreeNode(const char* str_id, const char* fmt, ...) { va_list args; va_start(args, fmt); return imgui_function_table_instance()->TreeNodeV(str_id, fmt, args); va_end(args); } + inline bool TreeNode(const void* ptr_id, const char* fmt, ...) { va_list args; va_start(args, fmt); return imgui_function_table_instance()->TreeNodeV2(ptr_id, fmt, args); va_end(args); } + inline bool TreeNodeV(const char* str_id, const char* fmt, va_list args) { return imgui_function_table_instance()->TreeNodeV(str_id, fmt, args); } + inline bool TreeNodeV(const void* ptr_id, const char* fmt, va_list args) { return imgui_function_table_instance()->TreeNodeV2(ptr_id, fmt, args); } + inline bool TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags) { return imgui_function_table_instance()->TreeNodeEx(label, flags); } + inline bool TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) { va_list args; va_start(args, fmt); return imgui_function_table_instance()->TreeNodeExV(str_id, flags, fmt, args); va_end(args); } + inline bool TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) { va_list args; va_start(args, fmt); return imgui_function_table_instance()->TreeNodeExV2(ptr_id, flags, fmt, args); va_end(args); } + inline bool TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) { return imgui_function_table_instance()->TreeNodeExV(str_id, flags, fmt, args); } + inline bool TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) { return imgui_function_table_instance()->TreeNodeExV2(ptr_id, flags, fmt, args); } + inline void TreePush(const char* str_id) { imgui_function_table_instance()->TreePush(str_id); } + inline void TreePush(const void* ptr_id) { imgui_function_table_instance()->TreePush2(ptr_id); } + inline void TreePop() { imgui_function_table_instance()->TreePop(); } + inline float GetTreeNodeToLabelSpacing() { return imgui_function_table_instance()->GetTreeNodeToLabelSpacing(); } + inline bool CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) { return imgui_function_table_instance()->CollapsingHeader(label, flags); } + inline bool CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags) { return imgui_function_table_instance()->CollapsingHeader2(label, p_visible, flags); } + inline void SetNextItemOpen(bool is_open, ImGuiCond cond) { imgui_function_table_instance()->SetNextItemOpen(is_open, cond); } + inline bool Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size) { return imgui_function_table_instance()->Selectable(label, selected, flags, size); } + inline bool Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size) { return imgui_function_table_instance()->Selectable2(label, p_selected, flags, size); } + inline bool BeginListBox(const char* label, const ImVec2& size) { return imgui_function_table_instance()->BeginListBox(label, size); } + inline void EndListBox() { imgui_function_table_instance()->EndListBox(); } + inline bool ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items) { return imgui_function_table_instance()->ListBox(label, current_item, items, items_count, height_in_items); } + inline bool ListBox(const char* label, int* current_item, bool(*items_getter)(void* data, int idx, const char** out_text), void* data, int items_count, int height_in_items) { return imgui_function_table_instance()->ListBox2(label, current_item, items_getter, data, items_count, height_in_items); } + inline void PlotLines(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride) { imgui_function_table_instance()->PlotLines(label, values, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size, stride); } + inline void PlotLines(const char* label, float(*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size) { imgui_function_table_instance()->PlotLines2(label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); } + inline void PlotHistogram(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride) { imgui_function_table_instance()->PlotHistogram(label, values, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size, stride); } + inline void PlotHistogram(const char* label, float(*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size) { imgui_function_table_instance()->PlotHistogram2(label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); } + inline void Value(const char* prefix, bool b) { imgui_function_table_instance()->Value(prefix, b); } + inline void Value(const char* prefix, int v) { imgui_function_table_instance()->Value2(prefix, v); } + inline void Value(const char* prefix, unsigned int v) { imgui_function_table_instance()->Value3(prefix, v); } + inline void Value(const char* prefix, float v, const char* float_format) { imgui_function_table_instance()->Value4(prefix, v, float_format); } + inline bool BeginMenuBar() { return imgui_function_table_instance()->BeginMenuBar(); } + inline void EndMenuBar() { imgui_function_table_instance()->EndMenuBar(); } + inline bool BeginMainMenuBar() { return imgui_function_table_instance()->BeginMainMenuBar(); } + inline void EndMainMenuBar() { imgui_function_table_instance()->EndMainMenuBar(); } + inline bool BeginMenu(const char* label, bool enabled) { return imgui_function_table_instance()->BeginMenu(label, enabled); } + inline void EndMenu() { imgui_function_table_instance()->EndMenu(); } + inline bool MenuItem(const char* label, const char* shortcut, bool selected, bool enabled) { return imgui_function_table_instance()->MenuItem(label, shortcut, selected, enabled); } + inline bool MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled) { return imgui_function_table_instance()->MenuItem2(label, shortcut, p_selected, enabled); } + inline bool BeginTooltip() { return imgui_function_table_instance()->BeginTooltip(); } + inline void EndTooltip() { imgui_function_table_instance()->EndTooltip(); } + inline void SetTooltip(const char* fmt, ...) { va_list args; va_start(args, fmt); imgui_function_table_instance()->SetTooltipV(fmt, args); va_end(args); } + inline void SetTooltipV(const char* fmt, va_list args) { imgui_function_table_instance()->SetTooltipV(fmt, args); } + inline bool BeginItemTooltip() { return imgui_function_table_instance()->BeginItemTooltip(); } + inline void SetItemTooltip(const char* fmt, ...) { va_list args; va_start(args, fmt); imgui_function_table_instance()->SetItemTooltipV(fmt, args); va_end(args); } + inline void SetItemTooltipV(const char* fmt, va_list args) { imgui_function_table_instance()->SetItemTooltipV(fmt, args); } + inline bool BeginPopup(const char* str_id, ImGuiWindowFlags flags) { return imgui_function_table_instance()->BeginPopup(str_id, flags); } + inline bool BeginPopupModal(const char* name, bool* p_open, ImGuiWindowFlags flags) { return imgui_function_table_instance()->BeginPopupModal(name, p_open, flags); } + inline void EndPopup() { imgui_function_table_instance()->EndPopup(); } + inline void OpenPopup(const char* str_id, ImGuiPopupFlags popup_flags) { imgui_function_table_instance()->OpenPopup(str_id, popup_flags); } + inline void OpenPopup(ImGuiID id, ImGuiPopupFlags popup_flags) { imgui_function_table_instance()->OpenPopup2(id, popup_flags); } + inline void OpenPopupOnItemClick(const char* str_id, ImGuiPopupFlags popup_flags) { imgui_function_table_instance()->OpenPopupOnItemClick(str_id, popup_flags); } + inline void CloseCurrentPopup() { imgui_function_table_instance()->CloseCurrentPopup(); } + inline bool BeginPopupContextItem(const char* str_id, ImGuiPopupFlags popup_flags) { return imgui_function_table_instance()->BeginPopupContextItem(str_id, popup_flags); } + inline bool BeginPopupContextWindow(const char* str_id, ImGuiPopupFlags popup_flags) { return imgui_function_table_instance()->BeginPopupContextWindow(str_id, popup_flags); } + inline bool BeginPopupContextVoid(const char* str_id, ImGuiPopupFlags popup_flags) { return imgui_function_table_instance()->BeginPopupContextVoid(str_id, popup_flags); } + inline bool IsPopupOpen(const char* str_id, ImGuiPopupFlags flags) { return imgui_function_table_instance()->IsPopupOpen(str_id, flags); } + inline bool BeginTable(const char* str_id, int column, ImGuiTableFlags flags, const ImVec2& outer_size, float inner_width) { return imgui_function_table_instance()->BeginTable(str_id, column, flags, outer_size, inner_width); } + inline void EndTable() { imgui_function_table_instance()->EndTable(); } + inline void TableNextRow(ImGuiTableRowFlags row_flags, float min_row_height) { imgui_function_table_instance()->TableNextRow(row_flags, min_row_height); } + inline bool TableNextColumn() { return imgui_function_table_instance()->TableNextColumn(); } + inline bool TableSetColumnIndex(int column_n) { return imgui_function_table_instance()->TableSetColumnIndex(column_n); } + inline void TableSetupColumn(const char* label, ImGuiTableColumnFlags flags, float init_width_or_weight, ImGuiID user_id) { imgui_function_table_instance()->TableSetupColumn(label, flags, init_width_or_weight, user_id); } + inline void TableSetupScrollFreeze(int cols, int rows) { imgui_function_table_instance()->TableSetupScrollFreeze(cols, rows); } + inline void TableHeadersRow() { imgui_function_table_instance()->TableHeadersRow(); } + inline void TableHeader(const char* label) { imgui_function_table_instance()->TableHeader(label); } + inline ImGuiTableSortSpecs* TableGetSortSpecs() { return imgui_function_table_instance()->TableGetSortSpecs(); } + inline int TableGetColumnCount() { return imgui_function_table_instance()->TableGetColumnCount(); } + inline int TableGetColumnIndex() { return imgui_function_table_instance()->TableGetColumnIndex(); } + inline int TableGetRowIndex() { return imgui_function_table_instance()->TableGetRowIndex(); } + inline const char* TableGetColumnName(int column_n) { return imgui_function_table_instance()->TableGetColumnName(column_n); } + inline ImGuiTableColumnFlags TableGetColumnFlags(int column_n) { return imgui_function_table_instance()->TableGetColumnFlags(column_n); } + inline void TableSetColumnEnabled(int column_n, bool v) { imgui_function_table_instance()->TableSetColumnEnabled(column_n, v); } + inline void TableSetBgColor(ImGuiTableBgTarget target, ImU32 color, int column_n) { imgui_function_table_instance()->TableSetBgColor(target, color, column_n); } + inline void Columns(int count, const char* id, bool border) { imgui_function_table_instance()->Columns(count, id, border); } + inline void NextColumn() { imgui_function_table_instance()->NextColumn(); } + inline int GetColumnIndex() { return imgui_function_table_instance()->GetColumnIndex(); } + inline float GetColumnWidth(int column_index) { return imgui_function_table_instance()->GetColumnWidth(column_index); } + inline void SetColumnWidth(int column_index, float width) { imgui_function_table_instance()->SetColumnWidth(column_index, width); } + inline float GetColumnOffset(int column_index) { return imgui_function_table_instance()->GetColumnOffset(column_index); } + inline void SetColumnOffset(int column_index, float offset_x) { imgui_function_table_instance()->SetColumnOffset(column_index, offset_x); } + inline int GetColumnsCount() { return imgui_function_table_instance()->GetColumnsCount(); } + inline bool BeginTabBar(const char* str_id, ImGuiTabBarFlags flags) { return imgui_function_table_instance()->BeginTabBar(str_id, flags); } + inline void EndTabBar() { imgui_function_table_instance()->EndTabBar(); } + inline bool BeginTabItem(const char* label, bool* p_open, ImGuiTabItemFlags flags) { return imgui_function_table_instance()->BeginTabItem(label, p_open, flags); } + inline void EndTabItem() { imgui_function_table_instance()->EndTabItem(); } + inline bool TabItemButton(const char* label, ImGuiTabItemFlags flags) { return imgui_function_table_instance()->TabItemButton(label, flags); } + inline void SetTabItemClosed(const char* tab_or_docked_window_label) { imgui_function_table_instance()->SetTabItemClosed(tab_or_docked_window_label); } + inline ImGuiID DockSpace(ImGuiID id, const ImVec2& size, ImGuiDockNodeFlags flags, const ImGuiWindowClass* window_class) { return imgui_function_table_instance()->DockSpace(id, size, flags, window_class); } + inline void SetNextWindowDockID(ImGuiID dock_id, ImGuiCond cond) { imgui_function_table_instance()->SetNextWindowDockID(dock_id, cond); } + inline void SetNextWindowClass(const ImGuiWindowClass* window_class) { imgui_function_table_instance()->SetNextWindowClass(window_class); } + inline ImGuiID GetWindowDockID() { return imgui_function_table_instance()->GetWindowDockID(); } + inline bool IsWindowDocked() { return imgui_function_table_instance()->IsWindowDocked(); } + inline bool BeginDragDropSource(ImGuiDragDropFlags flags) { return imgui_function_table_instance()->BeginDragDropSource(flags); } + inline bool SetDragDropPayload(const char* type, const void* data, size_t sz, ImGuiCond cond) { return imgui_function_table_instance()->SetDragDropPayload(type, data, sz, cond); } + inline void EndDragDropSource() { imgui_function_table_instance()->EndDragDropSource(); } + inline bool BeginDragDropTarget() { return imgui_function_table_instance()->BeginDragDropTarget(); } + inline const ImGuiPayload* AcceptDragDropPayload(const char* type, ImGuiDragDropFlags flags) { return imgui_function_table_instance()->AcceptDragDropPayload(type, flags); } + inline void EndDragDropTarget() { imgui_function_table_instance()->EndDragDropTarget(); } + inline const ImGuiPayload* GetDragDropPayload() { return imgui_function_table_instance()->GetDragDropPayload(); } + inline void BeginDisabled(bool disabled) { imgui_function_table_instance()->BeginDisabled(disabled); } + inline void EndDisabled() { imgui_function_table_instance()->EndDisabled(); } + inline void PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect) { imgui_function_table_instance()->PushClipRect(clip_rect_min, clip_rect_max, intersect_with_current_clip_rect); } + inline void PopClipRect() { imgui_function_table_instance()->PopClipRect(); } + inline void SetItemDefaultFocus() { imgui_function_table_instance()->SetItemDefaultFocus(); } + inline void SetKeyboardFocusHere(int offset) { imgui_function_table_instance()->SetKeyboardFocusHere(offset); } + inline void SetNextItemAllowOverlap() { imgui_function_table_instance()->SetNextItemAllowOverlap(); } + inline bool IsItemHovered(ImGuiHoveredFlags flags) { return imgui_function_table_instance()->IsItemHovered(flags); } + inline bool IsItemActive() { return imgui_function_table_instance()->IsItemActive(); } + inline bool IsItemFocused() { return imgui_function_table_instance()->IsItemFocused(); } + inline bool IsItemClicked(ImGuiMouseButton mouse_button) { return imgui_function_table_instance()->IsItemClicked(mouse_button); } + inline bool IsItemVisible() { return imgui_function_table_instance()->IsItemVisible(); } + inline bool IsItemEdited() { return imgui_function_table_instance()->IsItemEdited(); } + inline bool IsItemActivated() { return imgui_function_table_instance()->IsItemActivated(); } + inline bool IsItemDeactivated() { return imgui_function_table_instance()->IsItemDeactivated(); } + inline bool IsItemDeactivatedAfterEdit() { return imgui_function_table_instance()->IsItemDeactivatedAfterEdit(); } + inline bool IsItemToggledOpen() { return imgui_function_table_instance()->IsItemToggledOpen(); } + inline bool IsAnyItemHovered() { return imgui_function_table_instance()->IsAnyItemHovered(); } + inline bool IsAnyItemActive() { return imgui_function_table_instance()->IsAnyItemActive(); } + inline bool IsAnyItemFocused() { return imgui_function_table_instance()->IsAnyItemFocused(); } + inline ImGuiID GetItemID() { return imgui_function_table_instance()->GetItemID(); } + inline ImVec2 GetItemRectMin() { return imgui_function_table_instance()->GetItemRectMin(); } + inline ImVec2 GetItemRectMax() { return imgui_function_table_instance()->GetItemRectMax(); } + inline ImVec2 GetItemRectSize() { return imgui_function_table_instance()->GetItemRectSize(); } + inline ImDrawList* GetBackgroundDrawList() { return imgui_function_table_instance()->GetBackgroundDrawList(); } + inline ImDrawList* GetForegroundDrawList() { return imgui_function_table_instance()->GetForegroundDrawList(); } + inline ImDrawList* GetBackgroundDrawList(ImGuiViewport* viewport) { return imgui_function_table_instance()->GetBackgroundDrawList2(viewport); } + inline ImDrawList* GetForegroundDrawList(ImGuiViewport* viewport) { return imgui_function_table_instance()->GetForegroundDrawList2(viewport); } + inline bool IsRectVisible(const ImVec2& size) { return imgui_function_table_instance()->IsRectVisible(size); } + inline bool IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max) { return imgui_function_table_instance()->IsRectVisible2(rect_min, rect_max); } + inline double GetTime() { return imgui_function_table_instance()->GetTime(); } + inline int GetFrameCount() { return imgui_function_table_instance()->GetFrameCount(); } + inline ImDrawListSharedData* GetDrawListSharedData() { return imgui_function_table_instance()->GetDrawListSharedData(); } + inline const char* GetStyleColorName(ImGuiCol idx) { return imgui_function_table_instance()->GetStyleColorName(idx); } + inline void SetStateStorage(ImGuiStorage* storage) { imgui_function_table_instance()->SetStateStorage(storage); } + inline ImGuiStorage* GetStateStorage() { return imgui_function_table_instance()->GetStateStorage(); } + inline bool BeginChildFrame(ImGuiID id, const ImVec2& size, ImGuiWindowFlags flags) { return imgui_function_table_instance()->BeginChildFrame(id, size, flags); } + inline void EndChildFrame() { imgui_function_table_instance()->EndChildFrame(); } + inline ImVec2 CalcTextSize(const char* text, const char* text_end, bool hide_text_after_double_hash, float wrap_width) { return imgui_function_table_instance()->CalcTextSize(text, text_end, hide_text_after_double_hash, wrap_width); } + inline ImVec4 ColorConvertU32ToFloat4(ImU32 in) { return imgui_function_table_instance()->ColorConvertU32ToFloat4(in); } + inline ImU32 ColorConvertFloat4ToU32(const ImVec4& in) { return imgui_function_table_instance()->ColorConvertFloat4ToU32(in); } + inline void ColorConvertRGBtoHSV(float r, float g, float b, float& out_h, float& out_s, float& out_v) { imgui_function_table_instance()->ColorConvertRGBtoHSV(r, g, b, out_h, out_s, out_v); } + inline void ColorConvertHSVtoRGB(float h, float s, float v, float& out_r, float& out_g, float& out_b) { imgui_function_table_instance()->ColorConvertHSVtoRGB(h, s, v, out_r, out_g, out_b); } + inline bool IsKeyDown(ImGuiKey key) { return imgui_function_table_instance()->IsKeyDown(key); } + inline bool IsKeyPressed(ImGuiKey key, bool repeat) { return imgui_function_table_instance()->IsKeyPressed(key, repeat); } + inline bool IsKeyReleased(ImGuiKey key) { return imgui_function_table_instance()->IsKeyReleased(key); } + inline int GetKeyPressedAmount(ImGuiKey key, float repeat_delay, float rate) { return imgui_function_table_instance()->GetKeyPressedAmount(key, repeat_delay, rate); } + inline const char* GetKeyName(ImGuiKey key) { return imgui_function_table_instance()->GetKeyName(key); } + inline void SetNextFrameWantCaptureKeyboard(bool want_capture_keyboard) { imgui_function_table_instance()->SetNextFrameWantCaptureKeyboard(want_capture_keyboard); } + inline bool IsMouseDown(ImGuiMouseButton button) { return imgui_function_table_instance()->IsMouseDown(button); } + inline bool IsMouseClicked(ImGuiMouseButton button, bool repeat) { return imgui_function_table_instance()->IsMouseClicked(button, repeat); } + inline bool IsMouseReleased(ImGuiMouseButton button) { return imgui_function_table_instance()->IsMouseReleased(button); } + inline bool IsMouseDoubleClicked(ImGuiMouseButton button) { return imgui_function_table_instance()->IsMouseDoubleClicked(button); } + inline int GetMouseClickedCount(ImGuiMouseButton button) { return imgui_function_table_instance()->GetMouseClickedCount(button); } + inline bool IsMouseHoveringRect(const ImVec2& r_min, const ImVec2& r_max, bool clip) { return imgui_function_table_instance()->IsMouseHoveringRect(r_min, r_max, clip); } + inline bool IsMousePosValid(const ImVec2* mouse_pos) { return imgui_function_table_instance()->IsMousePosValid(mouse_pos); } + inline bool IsAnyMouseDown() { return imgui_function_table_instance()->IsAnyMouseDown(); } + inline ImVec2 GetMousePos() { return imgui_function_table_instance()->GetMousePos(); } + inline ImVec2 GetMousePosOnOpeningCurrentPopup() { return imgui_function_table_instance()->GetMousePosOnOpeningCurrentPopup(); } + inline bool IsMouseDragging(ImGuiMouseButton button, float lock_threshold) { return imgui_function_table_instance()->IsMouseDragging(button, lock_threshold); } + inline ImVec2 GetMouseDragDelta(ImGuiMouseButton button, float lock_threshold) { return imgui_function_table_instance()->GetMouseDragDelta(button, lock_threshold); } + inline void ResetMouseDragDelta(ImGuiMouseButton button) { imgui_function_table_instance()->ResetMouseDragDelta(button); } + inline ImGuiMouseCursor GetMouseCursor() { return imgui_function_table_instance()->GetMouseCursor(); } + inline void SetMouseCursor(ImGuiMouseCursor cursor_type) { imgui_function_table_instance()->SetMouseCursor(cursor_type); } + inline void SetNextFrameWantCaptureMouse(bool want_capture_mouse) { imgui_function_table_instance()->SetNextFrameWantCaptureMouse(want_capture_mouse); } + inline const char* GetClipboardText() { return imgui_function_table_instance()->GetClipboardText(); } + inline void SetClipboardText(const char* text) { imgui_function_table_instance()->SetClipboardText(text); } + inline void SetAllocatorFunctions(ImGuiMemAllocFunc alloc_func, ImGuiMemFreeFunc free_func, void* user_data) { imgui_function_table_instance()->SetAllocatorFunctions(alloc_func, free_func, user_data); } + inline void GetAllocatorFunctions(ImGuiMemAllocFunc* p_alloc_func, ImGuiMemFreeFunc* p_free_func, void** p_user_data) { imgui_function_table_instance()->GetAllocatorFunctions(p_alloc_func, p_free_func, p_user_data); } + inline void* MemAlloc(size_t size) { return imgui_function_table_instance()->MemAlloc(size); } + inline void MemFree(void* ptr) { imgui_function_table_instance()->MemFree(ptr); } + +} + +inline int ImGuiStorage::GetInt(ImGuiID key, int default_val) const { return imgui_function_table_instance()->ImGuiStorage_GetInt(this, key, default_val); } +inline void ImGuiStorage::SetInt(ImGuiID key, int val) { imgui_function_table_instance()->ImGuiStorage_SetInt(this, key, val); } +inline bool ImGuiStorage::GetBool(ImGuiID key, bool default_val) const { return imgui_function_table_instance()->ImGuiStorage_GetBool(this, key, default_val); } +inline void ImGuiStorage::SetBool(ImGuiID key, bool val) { imgui_function_table_instance()->ImGuiStorage_SetBool(this, key, val); } +inline float ImGuiStorage::GetFloat(ImGuiID key, float default_val) const { return imgui_function_table_instance()->ImGuiStorage_GetFloat(this, key, default_val); } +inline void ImGuiStorage::SetFloat(ImGuiID key, float val) { imgui_function_table_instance()->ImGuiStorage_SetFloat(this, key, val); } +inline void* ImGuiStorage::GetVoidPtr(ImGuiID key) const { return imgui_function_table_instance()->ImGuiStorage_GetVoidPtr(this, key); } +inline void ImGuiStorage::SetVoidPtr(ImGuiID key, void* val) { imgui_function_table_instance()->ImGuiStorage_SetVoidPtr(this, key, val); } +inline int* ImGuiStorage::GetIntRef(ImGuiID key, int default_val) { return imgui_function_table_instance()->ImGuiStorage_GetIntRef(this, key, default_val); } +inline bool* ImGuiStorage::GetBoolRef(ImGuiID key, bool default_val) { return imgui_function_table_instance()->ImGuiStorage_GetBoolRef(this, key, default_val); } +inline float* ImGuiStorage::GetFloatRef(ImGuiID key, float default_val) { return imgui_function_table_instance()->ImGuiStorage_GetFloatRef(this, key, default_val); } +inline void** ImGuiStorage::GetVoidPtrRef(ImGuiID key, void* default_val) { return imgui_function_table_instance()->ImGuiStorage_GetVoidPtrRef(this, key, default_val); } +inline void ImGuiStorage::SetAllInt(int val) { imgui_function_table_instance()->ImGuiStorage_SetAllInt(this, val); } +inline void ImGuiStorage::BuildSortByKey() { imgui_function_table_instance()->ImGuiStorage_BuildSortByKey(this); } +inline ImGuiListClipper::ImGuiListClipper() { imgui_function_table_instance()->ConstructImGuiListClipper(this); } +inline ImGuiListClipper::~ImGuiListClipper() { imgui_function_table_instance()->DestructImGuiListClipper(this); } +inline void ImGuiListClipper::Begin(int items_count, float items_height) { imgui_function_table_instance()->ImGuiListClipper_Begin(this, items_count, items_height); } +inline void ImGuiListClipper::End() { imgui_function_table_instance()->ImGuiListClipper_End(this); } +inline bool ImGuiListClipper::Step() { return imgui_function_table_instance()->ImGuiListClipper_Step(this); } +inline void ImGuiListClipper::IncludeRangeByIndices(int item_begin, int item_end) { imgui_function_table_instance()->ImGuiListClipper_IncludeRangeByIndices(this, item_begin, item_end); } +inline void ImDrawList::PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect) { imgui_function_table_instance()->ImDrawList_PushClipRect(this, clip_rect_min, clip_rect_max, intersect_with_current_clip_rect); } +inline void ImDrawList::PushClipRectFullScreen() { imgui_function_table_instance()->ImDrawList_PushClipRectFullScreen(this); } +inline void ImDrawList::PopClipRect() { imgui_function_table_instance()->ImDrawList_PopClipRect(this); } +inline void ImDrawList::PushTextureID(ImTextureID texture_id) { imgui_function_table_instance()->ImDrawList_PushTextureID(this, texture_id); } +inline void ImDrawList::PopTextureID() { imgui_function_table_instance()->ImDrawList_PopTextureID(this); } +inline void ImDrawList::AddLine(const ImVec2& p1, const ImVec2& p2, ImU32 col, float thickness) { imgui_function_table_instance()->ImDrawList_AddLine(this, p1, p2, col, thickness); } +inline void ImDrawList::AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawFlags flags, float thickness) { imgui_function_table_instance()->ImDrawList_AddRect(this, p_min, p_max, col, rounding, flags, thickness); } +inline void ImDrawList::AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawFlags flags) { imgui_function_table_instance()->ImDrawList_AddRectFilled(this, p_min, p_max, col, rounding, flags); } +inline void ImDrawList::AddRectFilledMultiColor(const ImVec2& p_min, const ImVec2& p_max, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left) { imgui_function_table_instance()->ImDrawList_AddRectFilledMultiColor(this, p_min, p_max, col_upr_left, col_upr_right, col_bot_right, col_bot_left); } +inline void ImDrawList::AddQuad(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness) { imgui_function_table_instance()->ImDrawList_AddQuad(this, p1, p2, p3, p4, col, thickness); } +inline void ImDrawList::AddQuadFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col) { imgui_function_table_instance()->ImDrawList_AddQuadFilled(this, p1, p2, p3, p4, col); } +inline void ImDrawList::AddTriangle(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness) { imgui_function_table_instance()->ImDrawList_AddTriangle(this, p1, p2, p3, col, thickness); } +inline void ImDrawList::AddTriangleFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col) { imgui_function_table_instance()->ImDrawList_AddTriangleFilled(this, p1, p2, p3, col); } +inline void ImDrawList::AddCircle(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness) { imgui_function_table_instance()->ImDrawList_AddCircle(this, center, radius, col, num_segments, thickness); } +inline void ImDrawList::AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments) { imgui_function_table_instance()->ImDrawList_AddCircleFilled(this, center, radius, col, num_segments); } +inline void ImDrawList::AddNgon(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness) { imgui_function_table_instance()->ImDrawList_AddNgon(this, center, radius, col, num_segments, thickness); } +inline void ImDrawList::AddNgonFilled(const ImVec2& center, float radius, ImU32 col, int num_segments) { imgui_function_table_instance()->ImDrawList_AddNgonFilled(this, center, radius, col, num_segments); } +inline void ImDrawList::AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end) { imgui_function_table_instance()->ImDrawList_AddText(this, pos, col, text_begin, text_end); } +inline void ImDrawList::AddText(const ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end, float wrap_width, const ImVec4* cpu_fine_clip_rect) { imgui_function_table_instance()->ImDrawList_AddText2(this, font, font_size, pos, col, text_begin, text_end, wrap_width, cpu_fine_clip_rect); } +inline void ImDrawList::AddPolyline(const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags, float thickness) { imgui_function_table_instance()->ImDrawList_AddPolyline(this, points, num_points, col, flags, thickness); } +inline void ImDrawList::AddConvexPolyFilled(const ImVec2* points, int num_points, ImU32 col) { imgui_function_table_instance()->ImDrawList_AddConvexPolyFilled(this, points, num_points, col); } +inline void ImDrawList::AddBezierCubic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness, int num_segments) { imgui_function_table_instance()->ImDrawList_AddBezierCubic(this, p1, p2, p3, p4, col, thickness, num_segments); } +inline void ImDrawList::AddBezierQuadratic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness, int num_segments) { imgui_function_table_instance()->ImDrawList_AddBezierQuadratic(this, p1, p2, p3, col, thickness, num_segments); } +inline void ImDrawList::AddImage(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col) { imgui_function_table_instance()->ImDrawList_AddImage(this, user_texture_id, p_min, p_max, uv_min, uv_max, col); } +inline void ImDrawList::AddImageQuad(ImTextureID user_texture_id, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& uv1, const ImVec2& uv2, const ImVec2& uv3, const ImVec2& uv4, ImU32 col) { imgui_function_table_instance()->ImDrawList_AddImageQuad(this, user_texture_id, p1, p2, p3, p4, uv1, uv2, uv3, uv4, col); } +inline void ImDrawList::AddImageRounded(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float rounding, ImDrawFlags flags) { imgui_function_table_instance()->ImDrawList_AddImageRounded(this, user_texture_id, p_min, p_max, uv_min, uv_max, col, rounding, flags); } +inline void ImDrawList::PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments) { imgui_function_table_instance()->ImDrawList_PathArcTo(this, center, radius, a_min, a_max, num_segments); } +inline void ImDrawList::PathArcToFast(const ImVec2& center, float radius, int a_min_of_12, int a_max_of_12) { imgui_function_table_instance()->ImDrawList_PathArcToFast(this, center, radius, a_min_of_12, a_max_of_12); } +inline void ImDrawList::PathBezierCubicCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments) { imgui_function_table_instance()->ImDrawList_PathBezierCubicCurveTo(this, p2, p3, p4, num_segments); } +inline void ImDrawList::PathBezierQuadraticCurveTo(const ImVec2& p2, const ImVec2& p3, int num_segments) { imgui_function_table_instance()->ImDrawList_PathBezierQuadraticCurveTo(this, p2, p3, num_segments); } +inline void ImDrawList::PathRect(const ImVec2& rect_min, const ImVec2& rect_max, float rounding, ImDrawFlags flags) { imgui_function_table_instance()->ImDrawList_PathRect(this, rect_min, rect_max, rounding, flags); } +inline void ImDrawList::AddCallback(ImDrawCallback callback, void* callback_data) { imgui_function_table_instance()->ImDrawList_AddCallback(this, callback, callback_data); } +inline void ImDrawList::AddDrawCmd() { imgui_function_table_instance()->ImDrawList_AddDrawCmd(this); } +inline ImDrawList* ImDrawList::CloneOutput() const { return imgui_function_table_instance()->ImDrawList_CloneOutput(this); } +inline void ImDrawList::PrimReserve(int idx_count, int vtx_count) { imgui_function_table_instance()->ImDrawList_PrimReserve(this, idx_count, vtx_count); } +inline void ImDrawList::PrimUnreserve(int idx_count, int vtx_count) { imgui_function_table_instance()->ImDrawList_PrimUnreserve(this, idx_count, vtx_count); } +inline void ImDrawList::PrimRect(const ImVec2& a, const ImVec2& b, ImU32 col) { imgui_function_table_instance()->ImDrawList_PrimRect(this, a, b, col); } +inline void ImDrawList::PrimRectUV(const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, ImU32 col) { imgui_function_table_instance()->ImDrawList_PrimRectUV(this, a, b, uv_a, uv_b, col); } +inline void ImDrawList::PrimQuadUV(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, const ImVec2& uv_a, const ImVec2& uv_b, const ImVec2& uv_c, const ImVec2& uv_d, ImU32 col) { imgui_function_table_instance()->ImDrawList_PrimQuadUV(this, a, b, c, d, uv_a, uv_b, uv_c, uv_d, col); } +inline ImFont::ImFont() { imgui_function_table_instance()->ConstructImFont(this); } +inline ImFont::~ImFont() { imgui_function_table_instance()->DestructImFont(this); } +inline const ImFontGlyph* ImFont::FindGlyph(ImWchar c) const { return imgui_function_table_instance()->ImFont_FindGlyph(this, c); } +inline const ImFontGlyph* ImFont::FindGlyphNoFallback(ImWchar c) const { return imgui_function_table_instance()->ImFont_FindGlyphNoFallback(this, c); } +inline ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** remaining) const { return imgui_function_table_instance()->ImFont_CalcTextSizeA(this, size, max_width, wrap_width, text_begin, text_end, remaining); } +inline const char* ImFont::CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width) const { return imgui_function_table_instance()->ImFont_CalcWordWrapPositionA(this, scale, text, text_end, wrap_width); } +inline void ImFont::RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, ImWchar c) const { imgui_function_table_instance()->ImFont_RenderChar(this, draw_list, size, pos, col, c); } +inline void ImFont::RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width, bool cpu_fine_clip) const { imgui_function_table_instance()->ImFont_RenderText(this, draw_list, size, pos, col, clip_rect, text_begin, text_end, wrap_width, cpu_fine_clip); } + + +#endif + +#endif diff --git a/Plugin/src/Hooks.cpp b/Plugin/src/Hooks.cpp index 139c694b..75159998 100644 --- a/Plugin/src/Hooks.cpp +++ b/Plugin/src/Hooks.cpp @@ -1,6 +1,7 @@ #include "Hooks.h" #include "Offsets.h" #include "Utils.h" +#include "reshade/reshade.hpp" namespace Hooks { @@ -18,17 +19,21 @@ namespace Hooks void Hooks::ToggleEnableHDRSubSettings(RE::SettingsDataModel* a_model, bool a_bEnable) { - if (const auto peakBrightnessSetting = a_model->FindSettingById(static_cast(Settings::SettingID::kPeakBrightness))) { + if (const auto peakBrightnessSetting = a_model->FindSettingById(static_cast(Settings::SettingID::kHDR_PeakBrightness))) { peakBrightnessSetting->m_Enabled.SetValue(a_bEnable); } - if (const auto gamePaperWhiteSetting = a_model->FindSettingById(static_cast(Settings::SettingID::kGamePaperWhite))) { + if (const auto gamePaperWhiteSetting = a_model->FindSettingById(static_cast(Settings::SettingID::kHDR_GamePaperWhite))) { gamePaperWhiteSetting->m_Enabled.SetValue(a_bEnable); } - if (const auto uiPaperWhiteSetting = a_model->FindSettingById(static_cast(Settings::SettingID::kUIPaperWhite))) { + if (const auto uiPaperWhiteSetting = a_model->FindSettingById(static_cast(Settings::SettingID::kHDR_UIPaperWhite))) { uiPaperWhiteSetting->m_Enabled.SetValue(a_bEnable); } + + if (const auto saturation = a_model->FindSettingById(static_cast(Settings::SettingID::kHDR_Saturation))) { + saturation->m_Enabled.SetValue(a_bEnable); + } } void Hooks::CreateCheckboxSetting(RE::ArrayNestedUIValue& a_settingList, Settings::Checkbox& a_setting, bool a_bEnabled) @@ -83,12 +88,14 @@ namespace Hooks void Hooks::Hook_UnkFunc(uintptr_t a1, RE::BGSSwapChainObject* a_bgsSwapchainObject) { // save the pointer for later - swapChainObject = a_bgsSwapchainObject; + Settings::swapChainObject = a_bgsSwapchainObject; const auto settings = Settings::Main::GetSingleton(); a_bgsSwapchainObject->swapChainInterface->SetColorSpace1(settings->GetDisplayModeColorSpaceType()); + Settings::RegisterReshadeOverlay(); + return _UnkFunc(a1, a_bgsSwapchainObject); } @@ -128,6 +135,12 @@ namespace Hooks CreateSliderSetting(a_settingList, settings->PeakBrightness, settings->IsHDREnabled()); CreateSliderSetting(a_settingList, settings->GamePaperWhite, settings->IsHDREnabled()); CreateSliderSetting(a_settingList, settings->UIPaperWhite, settings->IsHDREnabled()); + CreateSliderSetting(a_settingList, settings->Saturation, settings->IsHDREnabled()); + CreateSliderSetting(a_settingList, settings->LUTCorrectionStrength, true); + CreateSliderSetting(a_settingList, settings->ColorGradingStrength, true); + CreateSliderSetting(a_settingList, settings->Contrast, true); + CreateSliderSetting(a_settingList, settings->DevSetting01, true); + CreateSliderSetting(a_settingList, settings->DevSetting02, true); // Initialize the rest of the settings after ours _CreateDataModelOptions(a_arg1, a_settingList); @@ -158,7 +171,7 @@ namespace Hooks ToggleEnableHDRSubSettings(a_eventData.m_Model, false); } - swapChainObject->format = newFormat; + Settings::swapChainObject->format = newFormat; // toggle vsync to force a swapchain recreation Offsets::ToggleVsync(reinterpret_cast(*Offsets::unkToggleVsyncArg1Ptr + 0x8), *Offsets::bEnableVsync); @@ -187,15 +200,33 @@ namespace Hooks }; switch (a_eventData.m_SettingID) { - case static_cast(Settings::SettingID::kPeakBrightness): + case static_cast(Settings::SettingID::kHDR_PeakBrightness): HandleSetting(settings->PeakBrightness); break; - case static_cast(Settings::SettingID::kGamePaperWhite): + case static_cast(Settings::SettingID::kHDR_GamePaperWhite): HandleSetting(settings->GamePaperWhite); break; - case static_cast(Settings::SettingID::kUIPaperWhite): + case static_cast(Settings::SettingID::kHDR_UIPaperWhite): HandleSetting(settings->UIPaperWhite); break; + case static_cast(Settings::SettingID::kHDR_Saturation): + HandleSetting(settings->Saturation); + break; + case static_cast(Settings::SettingID::kLUTCorrectionStrength): + HandleSetting(settings->LUTCorrectionStrength); + break; + case static_cast(Settings::SettingID::kColorGradingStrength): + HandleSetting(settings->ColorGradingStrength); + break; + case static_cast(Settings::SettingID::kContrast): + HandleSetting(settings->Contrast); + break; + case static_cast(Settings::SettingID::kDevSetting01): + HandleSetting(settings->DevSetting01); + break; + case static_cast(Settings::SettingID::kDevSetting02): + HandleSetting(settings->DevSetting02); + break; } _SettingsDataModelFloatEvent(a_arg1, a_eventData); @@ -214,11 +245,16 @@ namespace Hooks const auto settings = Settings::Main::GetSingleton(); // This can be any data type, even a struct. It just has to match StructHdrDllPluginConstants in HLSL. - std::array data { + std::array data { *settings->PeakBrightness.value, *settings->GamePaperWhite.value, *settings->UIPaperWhite.value, - 4.0f + *settings->Saturation.value * 0.02f, + *settings->LUTCorrectionStrength.value * 0.01f, + *settings->ColorGradingStrength.value * 0.01f, + *settings->Contrast.value * 0.02f, + *settings->DevSetting01.value * 0.01f, + *settings->DevSetting02.value * 0.01f }; if (!Compute) diff --git a/Plugin/src/Hooks.h b/Plugin/src/Hooks.h index d802777d..92ae9e76 100644 --- a/Plugin/src/Hooks.h +++ b/Plugin/src/Hooks.h @@ -56,8 +56,6 @@ namespace Hooks } private: - static inline RE::BGSSwapChainObject* swapChainObject = nullptr; - static void ToggleEnableHDRSubSettings(RE::SettingsDataModel* a_model, bool a_bEnable); static void CreateCheckboxSetting(RE::ArrayNestedUIValue& a_settingList, Settings::Checkbox& a_setting, bool a_bEnabled); static void CreateStepperSetting(RE::ArrayNestedUIValue& a_settingList, Settings::Stepper& a_setting, bool a_bEnabled); diff --git a/Plugin/src/PCH.h b/Plugin/src/PCH.h index fc791a50..4b6cf642 100644 --- a/Plugin/src/PCH.h +++ b/Plugin/src/PCH.h @@ -115,3 +115,7 @@ using namespace std::literals; // DKUtil #include "DKUtil/Hook.hpp" #include "DKUtil/Logger.hpp" + +// reshade addon api +#include "imgui.h" +#include "reshade/reshade_api.hpp" diff --git a/Plugin/src/Settings.cpp b/Plugin/src/Settings.cpp index 00382723..7979d362 100644 --- a/Plugin/src/Settings.cpp +++ b/Plugin/src/Settings.cpp @@ -60,6 +60,12 @@ namespace Settings config.Bind(PeakBrightness.value, PeakBrightness.defaultValue); config.Bind(GamePaperWhite.value, GamePaperWhite.defaultValue); config.Bind(UIPaperWhite.value, UIPaperWhite.defaultValue); + config.Bind(Saturation.value, Saturation.defaultValue); + config.Bind(LUTCorrectionStrength.value, LUTCorrectionStrength.defaultValue); + config.Bind(ColorGradingStrength.value, ColorGradingStrength.defaultValue); + config.Bind(Contrast.value, Contrast.defaultValue); + config.Bind(DevSetting01.value, DevSetting01.defaultValue); + config.Bind(DevSetting02.value, DevSetting02.defaultValue); config.Bind(RenderTargetsToUpgrade, "ImageSpaceBuffer", "ScaleformCompositeBuffer", diff --git a/Plugin/src/Settings.h b/Plugin/src/Settings.h index 4ef68142..3ec585d9 100644 --- a/Plugin/src/Settings.h +++ b/Plugin/src/Settings.h @@ -1,6 +1,9 @@ #pragma once +#include "Offsets.h" +#include "Utils.h" #include "DKUtil/Config.hpp" #include "RE/Buffers.h" +#include "reshade/reshade.hpp" #include @@ -11,9 +14,15 @@ namespace Settings enum class SettingID : unsigned int { kDisplayMode = 600, - kPeakBrightness, - kGamePaperWhite, - kUIPaperWhite + kHDR_PeakBrightness, + kHDR_GamePaperWhite, + kHDR_UIPaperWhite, + kHDR_Saturation, + kLUTCorrectionStrength, + kColorGradingStrength, + kContrast, + kDevSetting01, + kDevSetting02 }; struct Setting @@ -52,10 +61,15 @@ namespace Settings public: Stepper DisplayMode{ SettingID::kDisplayMode, "Display Mode", "Sets the game's display mode between SDR, HDR10 PQ, or HDR scRGB", { "DisplayMode", "Main" }, { "SDR", "HDR10 PQ", "HDR scRGB" } }; - Slider PeakBrightness{ SettingID::kPeakBrightness, "Peak Brightness", "Sets the peak brightness in HDR modes", { "PeakBrightness", "HDR" }, 80.f, 10000.f, 1000.f }; - Slider GamePaperWhite{ SettingID::kGamePaperWhite, "Game Paper White", "Sets the game paper white brightness in HDR modes", { "GamePaperWhite", "HDR" }, 80.f, 500.f, 200.f }; - Slider UIPaperWhite{ SettingID::kUIPaperWhite, "UI Paper White", "Sets the UI paper white brightness in HDR modes", { "UIPaperWhite", "HDR" }, 80.f, 500.f, 200.f }; - + Slider PeakBrightness{ SettingID::kHDR_PeakBrightness, "Peak Brightness", "Sets the peak brightness in HDR modes", { "PeakBrightness", "HDR" }, 80.f, 10000.f, 1000.f }; + Slider GamePaperWhite{ SettingID::kHDR_GamePaperWhite, "Game Paper White", "Sets the game paper white brightness in HDR modes", { "GamePaperWhite", "HDR" }, 80.f, 500.f, 200.f }; + Slider UIPaperWhite{ SettingID::kHDR_UIPaperWhite, "UI Paper White", "Sets the UI paper white brightness in HDR modes", { "UIPaperWhite", "HDR" }, 80.f, 500.f, 200.f }; + Slider Saturation{ SettingID::kHDR_Saturation, "Saturation", "Sets the saturation strength in HDR modes", { "Saturation", "HDR" }, 0.f, 100.f, 50.f }; + Slider LUTCorrectionStrength{ SettingID::kLUTCorrectionStrength, "LUT Correction Strength", "Sets the LUT correction (normalization) strength", { "LUTCorrectionStrength", "Main" }, 0.f, 100.f, 100.f }; + Slider ColorGradingStrength{ SettingID::kColorGradingStrength, "Color Grading Strength", "Sets the color grading strength", { "ColorGradingStrength", "Main" }, 0.f, 100.f, 100.f }; + Slider Contrast{ SettingID::kContrast, "Contrast", "Sets the contrast", { "Contrast", "Main" }, 0.f, 100.f, 50.f }; + Slider DevSetting01{ SettingID::kDevSetting01, "DevSetting01", "Development setting", { "DevSetting01", "Dev" }, 0.f, 100.f, 0.f }; + Slider DevSetting02{ SettingID::kDevSetting02, "DevSetting02", "Development setting", { "DevSetting02", "Dev" }, 0.f, 100.f, 0.f }; String RenderTargetsToUpgrade{ "RenderTargetsToUpgrade", "RenderTargets" }; bool IsHDREnabled() const; @@ -69,4 +83,65 @@ namespace Settings private: TomlConfig config = COMPILE_PROXY("NativeHDR.toml"sv); }; + + static inline RE::BGSSwapChainObject* swapChainObject = nullptr; + + // settings reshade overlay + inline bool DrawReshadeStepper(Settings::Main* a_settings, Stepper& a_stepper) + { + int tempValue = *a_stepper.value; + if (ImGui::SliderInt(a_stepper.name.c_str(), &tempValue, 0, a_stepper.optionNames.size() - 1, a_stepper.optionNames[tempValue].c_str(), ImGuiSliderFlags_NoInput)) { + *a_stepper.value = tempValue; + a_settings->Save(); + return true; + } + return false; + } + + inline bool DrawReshadeSlider(Settings::Main* a_settings, Slider& a_slider) + { + float tempValue = *a_slider.value; + if (ImGui::SliderFloat(a_slider.name.c_str(), &tempValue, a_slider.sliderMin, a_slider.sliderMax, "%.0f")) { + *a_slider.value = tempValue; + a_settings->Save(); + return true; + } + return false; + } + + static void DrawSettingsReshade(reshade::api::effect_runtime*) + { + const auto settings = Settings::Main::GetSingleton(); + + if (DrawReshadeStepper(settings, settings->DisplayMode)) { + const RE::BS_DXGI_FORMAT newFormat = settings->GetDisplayModeFormat(); + + Utils::SetBufferFormat(RE::Buffers::FrameBuffer, newFormat); + swapChainObject->format = newFormat; + + // toggle vsync to force a swapchain recreation + Offsets::ToggleVsync(reinterpret_cast(*Offsets::unkToggleVsyncArg1Ptr + 0x8), *Offsets::bEnableVsync); + } + DrawReshadeSlider(settings, settings->PeakBrightness); + DrawReshadeSlider(settings, settings->GamePaperWhite); + DrawReshadeSlider(settings, settings->UIPaperWhite); + DrawReshadeSlider(settings, settings->Saturation); + DrawReshadeSlider(settings, settings->LUTCorrectionStrength); + DrawReshadeSlider(settings, settings->ColorGradingStrength); + DrawReshadeSlider(settings, settings->Contrast); + DrawReshadeSlider(settings, settings->DevSetting01); + DrawReshadeSlider(settings, settings->DevSetting02); + } + + static inline bool bReshadeSettingsOverlayRegistered = false; + + static void RegisterReshadeOverlay() + { + if (!bReshadeSettingsOverlayRegistered) { + if (reshade::register_addon(GetModuleHandle(L"NativeHDR"))) { + reshade::register_overlay("NativeHDR Settings", &DrawSettingsReshade); + bReshadeSettingsOverlayRegistered = true; + } + } + } } diff --git a/Plugin/src/main.cpp b/Plugin/src/main.cpp index 63e8fe7c..47e20774 100644 --- a/Plugin/src/main.cpp +++ b/Plugin/src/main.cpp @@ -32,59 +32,63 @@ void SFSEPlugin_Preload(SFSE::LoadInterface* a_sfse); static inline bool bIsLoaded = false; -DLLEXPORT bool SFSEAPI SFSEPlugin_Load(SFSEInterface* a_sfse) +void LoadPlugin(bool a_bIsSFSE) { - if (bIsLoaded) { - return true; + dku::Logger::Init(Plugin::NAME, std::to_string(Plugin::Version)); + INFO("{} v{} loaded", Plugin::NAME, Plugin::Version) + + // do stuff + Settings::Main::GetSingleton()->Load(); + + if (a_bIsSFSE) { + SFSE::AllocTrampoline(1 << 7); + } else { + dku::Hook::Trampoline::AllocTrampoline(1 << 7); } + + Offsets::Initialize(); + Hooks::Install(); + + bIsLoaded = true; +} +DLLEXPORT bool SFSEAPI SFSEPlugin_Load(SFSEInterface* a_sfse) +{ #ifndef NDEBUG while (!IsDebuggerPresent()) { Sleep(100); } #endif - SFSE::Init(a_sfse); - - dku::Logger::Init(Plugin::NAME, std::to_string(Plugin::Version)); - INFO("{} v{} loaded", Plugin::NAME, Plugin::Version) - - // do stuff - Settings::Main::GetSingleton()->Load(); + if (bIsLoaded) { + return true; + } - SFSE::AllocTrampoline(1 << 7); - Offsets::Initialize(); - Hooks::Install(); + SFSE::Init(a_sfse); - bIsLoaded = true; + LoadPlugin(true); return true; } -// for non sfse plugin loaders BOOL APIENTRY DllMain(HMODULE a_hModule, DWORD a_ul_reason_for_call, LPVOID a_lpReserved) { - if (bIsLoaded) { - return TRUE; - } - - if (a_ul_reason_for_call == DLL_PROCESS_ATTACH) { + switch (a_ul_reason_for_call) { + case DLL_PROCESS_ATTACH: #ifndef NDEBUG while (!IsDebuggerPresent()) { Sleep(100); } #endif + if (bIsLoaded) { + return TRUE; + } - dku::Logger::Init(Plugin::NAME, std::to_string(Plugin::Version)); - INFO("{} v{} loaded", Plugin::NAME, Plugin::Version) - - Settings::Main::GetSingleton()->Load(); - - dku::Hook::Trampoline::AllocTrampoline(1 << 7); - Offsets::Initialize(); - Hooks::Install(); - - bIsLoaded = true; + LoadPlugin(false); + break; + case DLL_PROCESS_DETACH: + reshade::unregister_addon(a_hModule); + break; } return TRUE; diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 7e5c26b5..5d1a02e1 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -33,7 +33,12 @@ struct StructHdrDllPluginConstants float PeakBrightness; float GamePaperWhite; float UIPaperWhite; - uint Unused; + float Saturation; + float LUTCorrectionStrength; + float ColorGradingStrength; + float Contrast; + float DevSetting01; + float DevSetting02; }; -ConstantBuffer HdrDllPluginConstants : register(b3, space0); \ No newline at end of file +ConstantBuffer HdrDllPluginConstants : register(b3, space0); From cc331f0ded58afe6bd1ed57ea8f316bc17268cf8 Mon Sep 17 00:00:00 2001 From: Lilium Date: Sat, 7 Oct 2023 02:15:21 +0200 Subject: [PATCH 083/270] fix matrix in BinkMovie which was for BT.601 limited range. correct is BT.709 full range. --- shaders/BinkMovie/BinkMovie_ps.hlsl | 50 ++++++++++++++--------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/shaders/BinkMovie/BinkMovie_ps.hlsl b/shaders/BinkMovie/BinkMovie_ps.hlsl index 1065c6d1..5960e66d 100644 --- a/shaders/BinkMovie/BinkMovie_ps.hlsl +++ b/shaders/BinkMovie/BinkMovie_ps.hlsl @@ -2,10 +2,11 @@ #include "../color.hlsl" #include "RootSignature.hlsl" -Texture2D _8 : register(t0, space8); -Texture2D _9 : register(t1, space8); -Texture2D _10 : register(t2, space8); -SamplerState _13 : register(s0, space8); +Texture2D TexY : register(t0, space8); +Texture2D TexCb : register(t2, space8); +Texture2D TexCr : register(t1, space8); + +SamplerState Sampler0 : register(s0, space8); struct PSInputs { @@ -16,32 +17,29 @@ struct PSInputs [RootSignature(ShaderRootSignature)] float4 PS(PSInputs inputs) : SV_Target { - float Y = _8.Sample(_13, inputs.TEXCOORD.xy).x; - float Cb = _9.Sample(_13, inputs.TEXCOORD.xy).x; - float Cr = _10.Sample(_13, inputs.TEXCOORD.xy).x; + float Y = TexY.Sample(Sampler0, inputs.TEXCOORD.xy).x; + float Cb = TexCb.Sample(Sampler0, inputs.TEXCOORD.xy).x; + float Cr = TexCr.Sample(Sampler0, inputs.TEXCOORD.xy).x; + float3 color; -#if 0 // Doesn't work, the videos were probably not in YCbCr - // Reference: https://gist.github.com/yohhoy/dafa5a47dade85d8b40625261af3776a - static const float a = 0.2126f; - static const float b = 0.7152f; - static const float c = 0.0722f; - static const float d = 1.8556f; - static const float e = 1.5748f; - color.r = Y + (Cr * e); - color.g = Y - (Cb * (c * d / b)) - (Cr * (a * e / b)); - color.b = Y + (Cb * d); -#else // Original formula, it doesn't seem to make much sense but it works - //TODO: could it be that by treating Y in a different way, we could extract HDR luminance from this video? Probably not. - Y *= 1.16412353515625f; - color.x = (Y - 0.870655059814453125f) + (Cb * 1.595794677734375f); - color.y = ((Y + 0.529705047607421875f) - (Cb * 0.8134765625f)) - (Cr * 0.391448974609375f); - color.z = (Y - 1.081668853759765625f) + (Cr * 2.017822265625f); - - // Clamp for safety of pow as the above matrix produces negative values, + // usually in YCbCr the ranges are (in float): + // Y: 0.0-1.0 + // Cb: -0.5-0.5 + // Cr: -0.5-0.5 + // but since this is a digital signal (in unsinged 8bit: 0-255) it's now: + // Y: 0.0-1.0 + // Cb: 0.0-1.0 + // Cr: 0.0-1.0 + // the formula adjusts for that but was for BT.601 limited range while the video is definitely BT.709 full range + // matrix paramters have been adjusted for BT.709 full range + color.r = Y - 0.790487825870513916015625f + (Cr * 1.574800014495849609375f); + color.g = Y + 0.329009473323822021484375f - (Cb * 0.18732427060604095458984375f) - (Cr * 0.46812427043914794921875f); + color.b = Y - 0.931438446044921875f + (Cb * 1.85559999942779541015625f); + + // Clamp for safety as YCbCr<->RGB is not 100% accurate in float and can produce negative/invalid colors, // this breaks the UI pass if we are using R16G16B16A16F textures, // as UI blending produces invalid pixels if it's blended with an invalid color. color = max(color, 0.f); -#endif #if ENABLE_HDR #if SDR_USE_GAMMA_2_2 From 97a52f8e2231f928624816d7dbe51307e4c48818 Mon Sep 17 00:00:00 2001 From: Lilium Date: Sat, 7 Oct 2023 02:27:56 +0200 Subject: [PATCH 084/270] forgot to change some stuff from float to half + small readability tweak --- .../ContrastAdaptiveSharpening_cs.hlsl | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl index 184cff2d..90dc9853 100644 --- a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl +++ b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl @@ -62,11 +62,14 @@ void CS(CSInput csInput) uint4 _55 = CASData.cas2; - uint _58 = _55.x + (((csInput.SV_GroupThreadID.x >> 1u) & 7u) | (csInput.SV_GroupID.x << 4u)); + uint _58 = (csInput.SV_GroupThreadID.x >> 1) & 7 + | (csInput.SV_GroupID.x << 4) + + _55.x; - uint2 pp = - uint2(_58, - ((((csInput.SV_GroupThreadID.x >> 3u) & 6u) | (csInput.SV_GroupThreadID.x & 1u)) | (csInput.SV_GroupID.y << 4u)) + _55.y); + uint2 pp = uint2(_58, (csInput.SV_GroupThreadID.x >> 3) & 6 + | (csInput.SV_GroupThreadID.x & 1) + | (csInput.SV_GroupID.y << 4) + + _55.y); uint4 _69 = CASData.cas3; @@ -143,16 +146,16 @@ void CS(CSInput csInput) half3 _128 = _128f; half3 _173 = _173f; - _91 = saturate(pow( _91, 2.2f)); - _139 = saturate(pow(_139, 2.2f)); - _106 = saturate(pow(_106, 2.2f)); - _151 = saturate(pow(_151, 2.2f)); - _109 = saturate(pow(_109, 2.2f)); - _156 = saturate(pow(_156, 2.2f)); - _119 = saturate(pow(_119, 2.2f)); - _168 = saturate(pow(_168, 2.2f)); - _128 = saturate(pow(_128, 2.2f)); - _173 = saturate(pow(_173, 2.2f)); + _91 = saturate(pow( _91, 2.2h)); + _139 = saturate(pow(_139, 2.2h)); + _106 = saturate(pow(_106, 2.2h)); + _151 = saturate(pow(_151, 2.2h)); + _109 = saturate(pow(_109, 2.2h)); + _156 = saturate(pow(_156, 2.2h)); + _119 = saturate(pow(_119, 2.2h)); + _168 = saturate(pow(_168, 2.2h)); + _128 = saturate(pow(_128, 2.2h)); + _173 = saturate(pow(_173, 2.2h)); #endif // ENABLE_HDR @@ -209,7 +212,7 @@ void CS(CSInput csInput) float3 colorOut2 = BT2020_To_BT709(_1003); #else // ENABLE_HDR - float3 colorOut2 = pow(_329, 1.f / 2.2f); + float3 colorOut2 = pow(_329, 1.f / 2.2h); #endif // ENABLE_HDR if ((pp.x <= _55.z) && (pp.y <= _55.w)) @@ -221,7 +224,7 @@ void CS(CSInput csInput) float3 colorOut1 = BT2020_To_BT709(_396); #else // ENABLE_HDR - float3 colorOut1 = pow(_388, 1.f / 2.2f); + float3 colorOut1 = pow(_388, 1.f / 2.2h); #endif // ENABLE_HDR ColorOut[pp] = float4(colorOut1, 1.f); @@ -289,16 +292,16 @@ void CS(CSInput csInput) half3 _540 = _540f; half3 _575 = _575f; - _509 = saturate(pow(_509f, 2.2f)); - _547 = saturate(pow(_547f, 2.2f)); - _521 = saturate(pow(_521f, 2.2f)); - _556 = saturate(pow(_556f, 2.2f)); - _524 = saturate(pow(_524f, 2.2f)); - _561 = saturate(pow(_561f, 2.2f)); - _531 = saturate(pow(_531f, 2.2f)); - _570 = saturate(pow(_570f, 2.2f)); - _540 = saturate(pow(_540f, 2.2f)); - _575 = saturate(pow(_575f, 2.2f)); + _509 = saturate(pow(_509f, 2.2h)); + _547 = saturate(pow(_547f, 2.2h)); + _521 = saturate(pow(_521f, 2.2h)); + _556 = saturate(pow(_556f, 2.2h)); + _524 = saturate(pow(_524f, 2.2h)); + _561 = saturate(pow(_561f, 2.2h)); + _531 = saturate(pow(_531f, 2.2h)); + _570 = saturate(pow(_570f, 2.2h)); + _540 = saturate(pow(_540f, 2.2h)); + _575 = saturate(pow(_575f, 2.2h)); #endif // ENABLE_HDR @@ -353,7 +356,7 @@ void CS(CSInput csInput) float3 colorOut4 = BT2020_To_BT709(_1216); #else // ENABLE_HDR - float3 colorOut4 = pow(_724, 1.f / 2.2f); + float3 colorOut4 = pow(_724, 1.f / 2.2h); #endif // ENABLE_HDR if ((pp.x <= _55.z) && (pp.y <= _55.w)) @@ -365,7 +368,7 @@ void CS(CSInput csInput) float3 colorOut3 = BT2020_To_BT709(_790); #else // ENABLE_HDR - float3 colorOut3 = pow(_783, 1.f / 2.2f); + float3 colorOut3 = pow(_783, 1.f / 2.2h); #endif // ENABLE_HDR ColorOut[uint2(_58, pp.y)] = float4(colorOut3, 1.f); From bee7e750b6cfa83c4f56e832774c096bc5a641d3 Mon Sep 17 00:00:00 2001 From: ersh1 <16157893+ersh1@users.noreply.github.com> Date: Sat, 7 Oct 2023 02:46:32 +0200 Subject: [PATCH 085/270] added missing displaymode to shader constants, moved settings below monitor selection --- Plugin/src/Hooks.cpp | 90 +++++++++++++++++++++++++++---------------- Plugin/src/Hooks.h | 14 ++++--- Plugin/src/Settings.h | 23 ++++++++++- shaders/shared.hlsl | 1 + 4 files changed, 86 insertions(+), 42 deletions(-) diff --git a/Plugin/src/Hooks.cpp b/Plugin/src/Hooks.cpp index 75159998..9d27c6d0 100644 --- a/Plugin/src/Hooks.cpp +++ b/Plugin/src/Hooks.cpp @@ -36,7 +36,7 @@ namespace Hooks } } - void Hooks::CreateCheckboxSetting(RE::ArrayNestedUIValue& a_settingList, Settings::Checkbox& a_setting, bool a_bEnabled) + void Hooks::CreateCheckboxSetting(RE::ArrayNestedUIValue* a_settingList, Settings::Checkbox& a_setting, bool a_bEnabled) { auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); @@ -48,10 +48,10 @@ namespace Hooks s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); s.m_Enabled.SetValue(a_bEnabled); s.m_CheckBoxData.m_ShuttleMap.GetData().m_Value.SetValue(a_setting.value.get_data()); - a_settingList.AddItem(s); + a_settingList->AddItem(s); } - void Hooks::CreateStepperSetting(RE::ArrayNestedUIValue& a_settingList, Settings::Stepper& a_setting, bool a_bEnabled) + void Hooks::CreateStepperSetting(RE::ArrayNestedUIValue* a_settingList, Settings::Stepper& a_setting, bool a_bEnabled) { auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); @@ -66,10 +66,10 @@ namespace Hooks s.m_StepperData.m_ShuttleMap.GetData().m_DisplayValues.AddItem(optionName.c_str()); } s.m_StepperData.m_ShuttleMap.GetData().m_Value.SetValue(a_setting.value.get_data()); - a_settingList.AddItem(s); + a_settingList->AddItem(s); } - void Hooks::CreateSliderSetting(RE::ArrayNestedUIValue& a_settingList, Settings::Slider& a_setting, bool a_bEnabled) + void Hooks::CreateSliderSetting(RE::ArrayNestedUIValue* a_settingList, Settings::Slider& a_setting, bool a_bEnabled) { auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); @@ -82,7 +82,39 @@ namespace Hooks s.m_Enabled.SetValue(a_bEnabled); s.m_SliderData.m_ShuttleMap.GetData().m_Value.SetValue(a_setting.GetSliderPercentage()); s.m_SliderData.m_ShuttleMap.GetData().m_DisplayValue.SetStringValue(a_setting.GetSliderText().c_str()); - a_settingList.AddItem(s); + a_settingList->AddItem(s); + } + + void Hooks::CreateSeparator(RE::ArrayNestedUIValue* a_settingList, Settings::SettingID a_id) + { + auto hack = alloca(sizeof(RE::SubSettingsList::GeneralSetting)); + auto& s = *(new (hack) RE::SubSettingsList::GeneralSetting()); + + s.m_Text.SetStringValue(""); + s.m_Description.SetStringValue(""); + s.m_ID.SetValue(static_cast(a_id)); + s.m_Type.SetValue(RE::SubSettingsList::GeneralSetting::Type::LargeStepper); + s.m_Category.SetValue(RE::SubSettingsList::GeneralSetting::Category::Display); + s.m_Enabled.SetValue(false); + a_settingList->AddItem(s); + } + + void Hooks::CreateSettings(RE::ArrayNestedUIValue* a_settingList) + { + const auto settings = Settings::Main::GetSingleton(); + + CreateSeparator(a_settingList, Settings::SettingID::kSTART); + CreateStepperSetting(a_settingList, settings->DisplayMode, true); + CreateSliderSetting(a_settingList, settings->PeakBrightness, settings->IsHDREnabled()); + CreateSliderSetting(a_settingList, settings->GamePaperWhite, settings->IsHDREnabled()); + CreateSliderSetting(a_settingList, settings->UIPaperWhite, settings->IsHDREnabled()); + CreateSliderSetting(a_settingList, settings->Saturation, settings->IsHDREnabled()); + CreateSliderSetting(a_settingList, settings->LUTCorrectionStrength, true); + CreateSliderSetting(a_settingList, settings->ColorGradingStrength, true); + CreateSliderSetting(a_settingList, settings->Contrast, true); + CreateSliderSetting(a_settingList, settings->DevSetting01, true); + CreateSliderSetting(a_settingList, settings->DevSetting02, true); + CreateSeparator(a_settingList, Settings::SettingID::kEND); } void Hooks::Hook_UnkFunc(uintptr_t a1, RE::BGSSwapChainObject* a_bgsSwapchainObject) @@ -126,24 +158,13 @@ namespace Hooks a_bgsSwapChainObject->swapChainInterface->SetColorSpace1(settings->GetDisplayModeColorSpaceType()); } - void Hooks::Hook_CreateDataModelOptions(void* a_arg1, RE::ArrayNestedUIValue& a_settingList) + void Hooks::Hook_CreateMonitorSetting(void* a1, void* a2) { - const auto settings = Settings::Main::GetSingleton(); - - CreateStepperSetting(a_settingList, settings->DisplayMode, true); - - CreateSliderSetting(a_settingList, settings->PeakBrightness, settings->IsHDREnabled()); - CreateSliderSetting(a_settingList, settings->GamePaperWhite, settings->IsHDREnabled()); - CreateSliderSetting(a_settingList, settings->UIPaperWhite, settings->IsHDREnabled()); - CreateSliderSetting(a_settingList, settings->Saturation, settings->IsHDREnabled()); - CreateSliderSetting(a_settingList, settings->LUTCorrectionStrength, true); - CreateSliderSetting(a_settingList, settings->ColorGradingStrength, true); - CreateSliderSetting(a_settingList, settings->Contrast, true); - CreateSliderSetting(a_settingList, settings->DevSetting01, true); - CreateSliderSetting(a_settingList, settings->DevSetting02, true); + _CreateMonitorSetting(a1, a2); - // Initialize the rest of the settings after ours - _CreateDataModelOptions(a_arg1, a_settingList); + // insert our settings after + auto* settingList = reinterpret_cast*>(reinterpret_cast(a1) - 0x28); + CreateSettings(settingList); } void Hooks::Hook_SettingsDataModelBoolEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& a_eventData) @@ -245,22 +266,23 @@ namespace Hooks const auto settings = Settings::Main::GetSingleton(); // This can be any data type, even a struct. It just has to match StructHdrDllPluginConstants in HLSL. - std::array data { - *settings->PeakBrightness.value, - *settings->GamePaperWhite.value, - *settings->UIPaperWhite.value, - *settings->Saturation.value * 0.02f, - *settings->LUTCorrectionStrength.value * 0.01f, - *settings->ColorGradingStrength.value * 0.01f, - *settings->Contrast.value * 0.02f, - *settings->DevSetting01.value * 0.01f, - *settings->DevSetting02.value * 0.01f + const Settings::ShaderConstants data { + static_cast(*settings->DisplayMode.value), + static_cast(*settings->PeakBrightness.value), + static_cast(*settings->GamePaperWhite.value), + static_cast(*settings->UIPaperWhite.value), + static_cast(*settings->Saturation.value * 0.02f), + static_cast(*settings->LUTCorrectionStrength.value * 0.01f), + static_cast(*settings->ColorGradingStrength.value * 0.01f), + static_cast(*settings->Contrast.value * 0.02f), + static_cast(*settings->DevSetting01.value * 0.01f), + static_cast(*settings->DevSetting02.value * 0.01f) }; if (!Compute) - commandList->SetGraphicsRoot32BitConstants(RootParameterIndex, data.size(), reinterpret_cast(data.data()), 0); + commandList->SetGraphicsRoot32BitConstants(RootParameterIndex, Settings::shaderConstantsSize, &data, 0); else - commandList->SetComputeRoot32BitConstants(RootParameterIndex, data.size(), reinterpret_cast(data.data()), 0); + commandList->SetComputeRoot32BitConstants(RootParameterIndex, Settings::shaderConstantsSize, &data, 0); }; // Note: The following switch statement may be called several thousand times per frame. Additionally, it'll be called from multiple diff --git a/Plugin/src/Hooks.h b/Plugin/src/Hooks.h index 92ae9e76..0cb0d3e4 100644 --- a/Plugin/src/Hooks.h +++ b/Plugin/src/Hooks.h @@ -44,7 +44,7 @@ namespace Hooks _Hook_TakeSnapshot->Enable(); // Settings UI - _CreateDataModelOptions = dku::Hook::write_call<5>(dku::Hook::IDToAbs(135915, 0x89), Hook_CreateDataModelOptions); + _CreateMonitorSetting = dku::Hook::write_call<5>(dku::Hook::IDToAbs(136113, 0x62F), Hook_CreateMonitorSetting); _SettingsDataModelBoolEvent = dku::Hook::write_call<5>(dku::Hook::IDToAbs(136121, 0x3A), Hook_SettingsDataModelBoolEvent); _SettingsDataModelIntEvent = dku::Hook::write_call<5>(dku::Hook::IDToAbs(136131, 0x37), Hook_SettingsDataModelIntEvent); _SettingsDataModelFloatEvent = dku::Hook::write_call<5>(dku::Hook::IDToAbs(136130, 0x37), Hook_SettingsDataModelFloatEvent); @@ -57,9 +57,11 @@ namespace Hooks private: static void ToggleEnableHDRSubSettings(RE::SettingsDataModel* a_model, bool a_bEnable); - static void CreateCheckboxSetting(RE::ArrayNestedUIValue& a_settingList, Settings::Checkbox& a_setting, bool a_bEnabled); - static void CreateStepperSetting(RE::ArrayNestedUIValue& a_settingList, Settings::Stepper& a_setting, bool a_bEnabled); - static void CreateSliderSetting(RE::ArrayNestedUIValue& a_settingList, Settings::Slider& a_setting, bool a_bEnabled); + static void CreateCheckboxSetting(RE::ArrayNestedUIValue* a_settingList, Settings::Checkbox& a_setting, bool a_bEnabled); + static void CreateStepperSetting(RE::ArrayNestedUIValue* a_settingList, Settings::Stepper& a_setting, bool a_bEnabled); + static void CreateSliderSetting(RE::ArrayNestedUIValue* a_settingList, Settings::Slider& a_setting, bool a_bEnabled); + static void CreateSeparator(RE::ArrayNestedUIValue* a_settingList, Settings::SettingID a_id); + static void CreateSettings(RE::ArrayNestedUIValue* a_settingList); static void Hook_UnkFunc(uintptr_t a1, RE::BGSSwapChainObject* a_bgsSwapchainObject); static inline std::add_pointer_t _UnkFunc; @@ -70,8 +72,8 @@ namespace Hooks static void Hook_RecreateSwapchain(void* a1, RE::BGSSwapChainObject* a_bgsSwapChainObject, uint32_t a_width, uint32_t a_height, uint8_t a5); static inline std::add_pointer_t _RecreateSwapchain; - static void Hook_CreateDataModelOptions(void* a_arg1, RE::ArrayNestedUIValue& a_settingList); - static inline std::add_pointer_t _CreateDataModelOptions; + static void Hook_CreateMonitorSetting(void* a1, void* a2); + static inline std::add_pointer_t _CreateMonitorSetting; static void Hook_SettingsDataModelBoolEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& a_eventData); static inline std::add_pointer_t _SettingsDataModelBoolEvent; static void Hook_SettingsDataModelIntEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& a_eventData); diff --git a/Plugin/src/Settings.h b/Plugin/src/Settings.h index 3ec585d9..a83db495 100644 --- a/Plugin/src/Settings.h +++ b/Plugin/src/Settings.h @@ -13,7 +13,9 @@ namespace Settings enum class SettingID : unsigned int { - kDisplayMode = 600, + kSTART = 600, + + kDisplayMode, kHDR_PeakBrightness, kHDR_GamePaperWhite, kHDR_UIPaperWhite, @@ -22,7 +24,9 @@ namespace Settings kColorGradingStrength, kContrast, kDevSetting01, - kDevSetting02 + kDevSetting02, + + kEND }; struct Setting @@ -56,6 +60,21 @@ namespace Settings void SetValueFromSlider(float a_percentage); }; + struct ShaderConstants + { + uint32_t DisplayMode; + float PeakBrightness; + float GamePaperWhite; + float UIPaperWhite; + float Saturation; + float LUTCorrectionStrength; + float ColorGradingStrength; + float Contrast; + float DevSetting01; + float DevSetting02; + }; + static inline uint32_t shaderConstantsSize = 10; + class Main : public DKUtil::model::Singleton
{ public: diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 5d1a02e1..12f3ed30 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -30,6 +30,7 @@ // Custom push constants uploaded by the HDR DLL plugin code. Do note that register space comes at a premium when adding members. Bit/byte packing is advised. struct StructHdrDllPluginConstants { + uint DisplayMode; float PeakBrightness; float GamePaperWhite; float UIPaperWhite; From 225fe6ccfd58ab340dcbd0a5a370d6bec4c9b002 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Sat, 7 Oct 2023 00:00:16 -0400 Subject: [PATCH 086/270] feat(filmgrain): add 2 new types --- shaders/FilmGrain/FilmGrain_ps.hlsl | 59 ++++++++++++++++------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/shaders/FilmGrain/FilmGrain_ps.hlsl b/shaders/FilmGrain/FilmGrain_ps.hlsl index e8478fd2..c5b3b303 100644 --- a/shaders/FilmGrain/FilmGrain_ps.hlsl +++ b/shaders/FilmGrain/FilmGrain_ps.hlsl @@ -4,6 +4,9 @@ #define FILM_GRAIN_TEXTURE_SIZE 1024u +// 0 Vanilla, 1 New Random (no flicker/repeating), 2 New random, no raised blacks +#define FILM_GRAIN_TYPE 0 + cbuffer _13_15 : register(b0, space0) { float4 filmGrainColorAndIntensity : packoffset(c0); @@ -25,7 +28,7 @@ struct SPIRV_Cross_Output float4 SV_Target : SV_Target0; }; -// Better? random generator +// Better random generator float rand(float2 uv) { return frac(sin(dot(uv,float2(12.9898,78.233)))*43758.5453123); } @@ -45,24 +48,18 @@ void frag_main() // filmGrainColorAndIntensity // .x - randomInt32? changed every frame? // .y = randomInt32? changed every frame? -// .z = strength? maybe 0.03 +// .z = Film Grading Intensity (0 - 0.03) float3 tonemappedColor = TonemappedColorTexture.Sample(Sampler0, float2(TEXCOORD.x, TEXCOORD.y)); #if ENABLE_HDR tonemappedColor /= HDR_GAME_PAPER_WHITE; tonemappedColor = gamma_linear_to_sRGB(tonemappedColor); - float inverseLuminance = saturate(1.f - Luminance(tonemappedColor)); -#else - float inverseLuminance = saturate(1.f - Luminance(tonemappedColor)); #endif // ENABLE_HDR +float colorY = Luminance(tonemappedColor); -#if 0 // WIP fixes - const float filmGrainInvSize = 1.f / FILM_GRAIN_TEXTURE_SIZE; // This was "1.f / (FILM_GRAIN_TEXTURE_SIZE - 1u)", though that seems incorrect - const float filmGrainHalfSize = FILM_GRAIN_TEXTURE_SIZE * 0.5f; // This was "521.f", which seems like a mistake on 512 -#else +#if FILM_GRAIN_TYPE == 0 const float filmGrainInvSize = 1.f / (FILM_GRAIN_TEXTURE_SIZE - 1u); const float filmGrainHalfSize = 521.f; //TODO: rename in case we keep this as 521 -#endif // Applying modulus against 1023 will give value between 0 and 1023 // Same two values for all texels this frame float randomFromRange1 = float(int(uint(filmGrainColorAndIntensity.x) & (FILM_GRAIN_TEXTURE_SIZE - 1u))); @@ -79,36 +76,46 @@ void frag_main() // Unique each frame and texel. (Is it though?) float seed = (uniqueX + uniqueY); - // Generate a random number between -1 and -1; - // luminanceShift at -1 is black, 0 unchanged, 1 negative of current texel - float luminanceShift = (bethesdaRandom(seed) * 2.f) - 1.f; - -#if 0 // WIP fixes - float additiveFilmGrain = (luminanceShift * 1.f * inverseLuminance); - float3 tonemappedColorWithFilmGrain = float3(additiveFilmGrain, additiveFilmGrain,additiveFilmGrain); + // Generate a random number between 0 and 1; + float randomNumber = bethesdaRandom(seed); #else + float randomNumber = rand(TEXCOORD.xy + + frac(filmGrainColorAndIntensity.x / 2147483647.f) + + frac(filmGrainColorAndIntensity.y / 2147483647.f) + ); +#endif // FILM_GRAIN_TYPE == 0 + +#if FILM_GRAIN_TYPE != 2 + float inverseLuminance = saturate(1.f - colorY); + // luminanceShift at -1 is black, 0 unchanged, 1 negative of current texel + float luminanceShift = (randomNumber * 2.f) - 1.f; float additiveFilmGrain = (luminanceShift * filmGrainColorAndIntensity.z * inverseLuminance); +#else + float additiveFilmGrain = ((randomNumber * 2.f) - 1.f) // (-1 to 1) + * (2.f* (0.5f - abs(colorY - 0.5f))) // Bias towards center (nothing if black or white) + * filmGrainColorAndIntensity.z + * 3.333f; +#endif // FILM_GRAIN_TYPE != 2 + float3 newColor = float3(additiveFilmGrain, additiveFilmGrain,additiveFilmGrain); // Use addition to overlay color on top // Note: we let this possibly generate colors below 1 for scRGB float3 tonemappedColorWithFilmGrain = tonemappedColor + newColor; + + +#if 0 // WIP fixes + // Right side only + if (TEXCOORD.x < 0.5f) { + tonemappedColorWithFilmGrain = tonemappedColor; + } #endif #if ENABLE_HDR tonemappedColorWithFilmGrain = gamma_sRGB_to_linear(tonemappedColorWithFilmGrain) * HDR_GAME_PAPER_WHITE; #endif // ENABLE_HDR -#if 0 // WIP fixes - // Right side only - if (TEXCOORD.x >= 0.5f) { - SV_Target.rgb = tonemappedColorWithFilmGrain; - } else { - SV_Target.rgb = tonemappedColor; - } -#else SV_Target.rgb = tonemappedColorWithFilmGrain; -#endif SV_Target.w = 1.0f; } From 0b7b6ce1c7cf95279df83b61c931d2be7c830dda Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 7 Oct 2023 12:27:02 +0300 Subject: [PATCH 087/270] Plug in new custom shader variables into shaders (almost all of them) NOTE: this is not working for the moment --- build-msvc.bat | 1 + make-sln-msvc.bat | 1 + shaders/BinkMovie/BinkMovie_ps.hlsl | 2 +- shaders/BinkMovie/RootSignature.hlsl | 2 +- .../ColorGradingMerge_cs.hlsl | 19 ++-- shaders/ColorGradingMerge/RootSignature.hlsl | 2 +- .../ContrastAdaptiveSharpening_cs.hlsl | 6 +- .../RootSignature.hlsl | 2 +- shaders/FilmGrain/FilmGrain_ps.hlsl | 5 +- shaders/FilmGrain/RootSignature.hlsl | 2 +- shaders/HDRComposite/HDRComposite_ps.hlsl | 14 ++- shaders/HDRComposite/RootSignature.hlsl | 2 +- shaders/PostSharpen/RootSignature.hlsl | 2 +- shaders/ScaleformComposite/RootSignature.hlsl | 2 +- .../ScaleformComposite_ps.hlsl | 8 +- shaders/compile_all_shaders.bat | 92 ------------------- shaders/shared.hlsl | 35 +++---- 17 files changed, 51 insertions(+), 146 deletions(-) delete mode 100644 shaders/compile_all_shaders.bat diff --git a/build-msvc.bat b/build-msvc.bat index 038f8805..1361d48b 100644 --- a/build-msvc.bat +++ b/build-msvc.bat @@ -2,3 +2,4 @@ echo off rd /s /q "%~dp0/build" cmake -B "%~dp0/build" -S "%~dp0/Plugin" --preset=build-release-msvc-msvc cmake --build "%~dp0/build" +pause > NUL \ No newline at end of file diff --git a/make-sln-msvc.bat b/make-sln-msvc.bat index 4fad248b..e0e8341f 100644 --- a/make-sln-msvc.bat +++ b/make-sln-msvc.bat @@ -1,3 +1,4 @@ echo off rd /s /q "%~dp0/build" cmake -B "%~dp0/build" -S "%~dp0/Plugin" --preset=build-debug-msvc-msvc +pause > NUL \ No newline at end of file diff --git a/shaders/BinkMovie/BinkMovie_ps.hlsl b/shaders/BinkMovie/BinkMovie_ps.hlsl index 5960e66d..5919087d 100644 --- a/shaders/BinkMovie/BinkMovie_ps.hlsl +++ b/shaders/BinkMovie/BinkMovie_ps.hlsl @@ -49,7 +49,7 @@ float4 PS(PSInputs inputs) : SV_Target #endif //TODO: AutoHDR on movies??? - color *= HDR_GAME_PAPER_WHITE; + color *= HdrDllPluginConstants.HDRGamePaperWhiteNits / WhiteNits_BT709; // Use the game brightness, not the UI one, as these are usually videos that are seamless with gameplay #endif // ENABLE_HDR return float4(color, 1.0f); diff --git a/shaders/BinkMovie/RootSignature.hlsl b/shaders/BinkMovie/RootSignature.hlsl index 8565e720..dd5dec88 100644 --- a/shaders/BinkMovie/RootSignature.hlsl +++ b/shaders/BinkMovie/RootSignature.hlsl @@ -7,5 +7,5 @@ "SRV(t2, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=4, b3), " \ + "RootConstants(num32BitConstants=10, b3), " \ "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " \ No newline at end of file diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl index 1fd0b111..591610f0 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl @@ -31,9 +31,7 @@ #define LUT_SDR_ON_CLIP 1u #define LUT_HDR_ON_CLIP 0u -static float AdditionalNeutralLUTPercentage = 0.f; // ~0.25 might be a good compromise -static float LUTCorrectionPercentage = 1.f; -static float LUTSaturationMultiplier = 1.f; // 1 is neutral +static float AdditionalNeutralLUTPercentage = 0.f; // ~0.25 might be a good compromise, but this is mostly replaced by HdrDllPluginConstants.LUTCorrectionStrength cbuffer CPushConstantWrapper_ColorGradingMerge : register(b0, space0) { @@ -188,7 +186,8 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b const float3 detintedColor = max(0.f, 1.f-((1.f - color) * reduceFactor)); - const float targetChroma = linear_srgb_to_oklch(detintedColor)[1] * LUTSaturationMultiplier; + // The saturation multiplier is restricted to HDR as it easily goes beyond Rec.709 + const float targetChroma = linear_srgb_to_oklch(detintedColor)[1] * (SDRRange ? 1.f : HdrDllPluginConstants.HDRLUTCorrectionSaturation); // Adjust the value back to recreate a smooth Y gradient since 0 is floored // Sample and hold targetL @@ -267,7 +266,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b color = saturate(color); } - color = lerp(originalColor, color, LUTCorrectionPercentage); + color = lerp(originalColor, color, HdrDllPluginConstants.LUTCorrectionStrength); return color; } @@ -296,7 +295,7 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) #endif // LUT_IMPROVEMENT_TYPE - const bool SDRRange = !((bool)ENABLE_HDR) || (bool)FORCE_SDR_LUTS; + const bool SDRRange = HdrDllPluginConstants.DisplayMode == 0 || (bool)FORCE_SDR_LUTS; #if LUT_IMPROVEMENT_TYPE == 1 float3 LUT1Color = PatchLUTColor(LUT1, inUVW, neutralLUTColor, SDRRange); @@ -313,7 +312,7 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) if (LUT1Luminance != 0.f) { - LUT1Color *= lerp(1.f, neutralLUTLuminance / LUT1Luminance, LUTCorrectionPercentage); + LUT1Color *= lerp(1.f, neutralLUTLuminance / LUT1Luminance, HdrDllPluginConstants.LUTCorrectionStrength); if (SDRRange) { LUT1Color = saturate(LUT1Color); @@ -321,7 +320,7 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) } if (LUT2Luminance != 0.f) { - LUT2Color *= lerp(1.f, neutralLUTLuminance / LUT2Luminance, LUTCorrectionPercentage); + LUT2Color *= lerp(1.f, neutralLUTLuminance / LUT2Luminance, HdrDllPluginConstants.LUTCorrectionStrength); if (SDRRange) { LUT2Color = saturate(LUT2Color); @@ -329,7 +328,7 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) } if (LUT3Luminance != 0.f) { - LUT3Color *= lerp(1.f, neutralLUTLuminance / LUT3Luminance, LUTCorrectionPercentage); + LUT3Color *= lerp(1.f, neutralLUTLuminance / LUT3Luminance, HdrDllPluginConstants.LUTCorrectionStrength); if (SDRRange) { LUT3Color = saturate(LUT3Color); @@ -337,7 +336,7 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) } if (LUT4Luminance != 0.f) { - LUT4Color *= lerp(1.f, neutralLUTLuminance / LUT4Luminance, LUTCorrectionPercentage); + LUT4Color *= lerp(1.f, neutralLUTLuminance / LUT4Luminance, HdrDllPluginConstants.LUTCorrectionStrength); if (SDRRange) { LUT4Color = saturate(LUT4Color); diff --git a/shaders/ColorGradingMerge/RootSignature.hlsl b/shaders/ColorGradingMerge/RootSignature.hlsl index 1a2c2857..40ab0d42 100644 --- a/shaders/ColorGradingMerge/RootSignature.hlsl +++ b/shaders/ColorGradingMerge/RootSignature.hlsl @@ -53,7 +53,7 @@ "UAV(u0, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=4, b3), " \ + "RootConstants(num32BitConstants=10, b3), " \ "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ diff --git a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl index 90dc9853..5a3bbd38 100644 --- a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl +++ b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl @@ -46,7 +46,7 @@ struct CSInput [numthreads(64, 1, 1)] void CS(CSInput csInput) { -#if 0 +#if 1 // butchered the CS so it just outputs the input uint4 _55 = CASData.cas2; uint ppx = _55.x + (((csInput.SV_GroupThreadID.x >> 1u) & 7u) | (csInput.SV_GroupID.x << 4u)); @@ -120,7 +120,8 @@ void CS(CSInput csInput) // since blue is the least contributing in terms of luminance // the worst case is red and green at 0 and blue high enough so that the luminance is HDR_MAX_OUTPUT_NITS // TODO: use more accurate value than just the K factor from the YCbCr<->RGB transform - static const float normalizationFactor = (HDR_MAX_OUTPUT_NITS / 80.f) / 0.0593f; + // TODO: normalizing by peak brightness isn't right, we need to do a quick inverse pass of the tonemapper. + static const float normalizationFactor = (HdrDllPluginConstants.HDRPeakBrightness / WhiteNits_BT709) / 0.0593f; half3 _91 = saturate( _91f / normalizationFactor); half3 _139 = saturate(_139f / normalizationFactor); @@ -146,6 +147,7 @@ void CS(CSInput csInput) half3 _128 = _128f; half3 _173 = _173f; + //TODO: use sRGB gamma if SDR_USE_GAMMA_2_2 is false _91 = saturate(pow( _91, 2.2h)); _139 = saturate(pow(_139, 2.2h)); _106 = saturate(pow(_106, 2.2h)); diff --git a/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl b/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl index 606f8ad6..d0a754a1 100644 --- a/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl +++ b/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl @@ -68,7 +68,7 @@ "), " \ \ "CBV(b0, space=8), " \ - "RootConstants(num32BitConstants=4, b3), " \ + "RootConstants(num32BitConstants=10, b3), " \ "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ diff --git a/shaders/FilmGrain/FilmGrain_ps.hlsl b/shaders/FilmGrain/FilmGrain_ps.hlsl index c5b3b303..96ecfc54 100644 --- a/shaders/FilmGrain/FilmGrain_ps.hlsl +++ b/shaders/FilmGrain/FilmGrain_ps.hlsl @@ -52,7 +52,8 @@ void frag_main() float3 tonemappedColor = TonemappedColorTexture.Sample(Sampler0, float2(TEXCOORD.x, TEXCOORD.y)); #if ENABLE_HDR - tonemappedColor /= HDR_GAME_PAPER_WHITE; + const float paperWhite = HdrDllPluginConstants.HDRGamePaperWhiteNits / WhiteNits_BT709; + tonemappedColor /= paperWhite; tonemappedColor = gamma_linear_to_sRGB(tonemappedColor); #endif // ENABLE_HDR float colorY = Luminance(tonemappedColor); @@ -112,7 +113,7 @@ float colorY = Luminance(tonemappedColor); #endif #if ENABLE_HDR - tonemappedColorWithFilmGrain = gamma_sRGB_to_linear(tonemappedColorWithFilmGrain) * HDR_GAME_PAPER_WHITE; + tonemappedColorWithFilmGrain = gamma_sRGB_to_linear(tonemappedColorWithFilmGrain) * paperWhite; #endif // ENABLE_HDR SV_Target.rgb = tonemappedColorWithFilmGrain; diff --git a/shaders/FilmGrain/RootSignature.hlsl b/shaders/FilmGrain/RootSignature.hlsl index 70b314f2..f88d9ca3 100644 --- a/shaders/FilmGrain/RootSignature.hlsl +++ b/shaders/FilmGrain/RootSignature.hlsl @@ -7,5 +7,5 @@ "SRV(t0, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=4, b3), " \ + "RootConstants(num32BitConstants=10, b3), " \ "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " \ No newline at end of file diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 7e8c98bf..b91e0e44 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -101,9 +101,6 @@ struct PSOutput static const float SDRTonemapHDRStrength = 1.f; static const float PostProcessStrength = 1.f; -// Similar to "AdditionalNeutralLUTPercentage" in the LUT shader, though this is more precise as it skips the precision loss induced by a neutral LUT -static const float GradingLUTStrength = 1.f; -static const float SecondaryContrast = 1.f; // 0 Ignored, 1 ACES Reference, 2 ACES Custom, 3 Hable, 4+ Disable tonemapper static const uint ForceTonemapper = 0; @@ -768,7 +765,8 @@ float3 GradingLUT(float3 color, float2 uv) LUTColor = gamma_sRGB_to_linear(LUTColor); #endif // !LUT_FIX_GAMMA_MAPPING - const float LUTMaskAlpha = saturate(LUTMaskTexture.Sample(Sampler0, uv).x + (1.f - GradingLUTStrength)); + // "ColorGradingStrength" is similar to "AdditionalNeutralLUTPercentage" in the LUT mixing shader, though this is more precise as it skips the precision loss induced by a neutral LUT + const float LUTMaskAlpha = saturate(LUTMaskTexture.Sample(Sampler0, uv).x + (1.f - HdrDllPluginConstants.ColorGradingStrength)); LUTColor = lerp(LUTColor, color, LUTMaskAlpha); return LUTColor; @@ -894,7 +892,7 @@ PSOutput PS(PSInput psInput) #if ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP - const float paperWhite = HDR_GAME_PAPER_WHITE; + const float paperWhite = HdrDllPluginConstants.HDRGamePaperWhiteNits / WhiteNits_BT709; const float midGrayIn = MidGray; float midGrayOut = midGrayIn; @@ -971,19 +969,19 @@ PSOutput PS(PSInput psInput) #endif // Secondary user driven contrast - inverseTonemappedPostProcessedColor = pow(inverseTonemappedPostProcessedColor / (MidGray * midGrayScale), SecondaryContrast) * (MidGray * midGrayScale); + inverseTonemappedPostProcessedColor = pow(inverseTonemappedPostProcessedColor / (MidGray * midGrayScale), HdrDllPluginConstants.HDRSecondaryContrast) * (MidGray * midGrayScale); // Bring back the color to the same range as SDR by dividing by the mid gray change. inverseTonemappedPostProcessedColor *= paperWhite / midGrayScale; minHighlightsColorOut *= paperWhite / midGrayScale; - const float maxOutputLuminance = HDR_MAX_OUTPUT_NITS / WhiteNits_BT709; + const float maxOutputLuminance = HdrDllPluginConstants.HDRPeakBrightnessNits / WhiteNits_BT709; const float highlightsShoulderStart = onlyInvertHighlights ? minHighlightsColorOut : 0.f; float3 outputColor = DICETonemap(inverseTonemappedPostProcessedColor, maxOutputLuminance, highlightsShoulderStart); #else // ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP - float3 outputColor = finalOriginalColor * HDR_GAME_PAPER_WHITE_MULTIPLIER; // Don't use "HDR_GAME_PAPER_WHITE" as it'd be too bright on an untonemapped image + float3 outputColor = finalOriginalColor * (HdrDllPluginConstants.HDRGamePaperWhiteNits / ReferenceWhiteNits_BT2408); // Don't use "HDRGamePaperWhiteNits" directly as it'd be too bright on an untonemapped image #endif // ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP diff --git a/shaders/HDRComposite/RootSignature.hlsl b/shaders/HDRComposite/RootSignature.hlsl index e7e01534..2ce8e4c2 100644 --- a/shaders/HDRComposite/RootSignature.hlsl +++ b/shaders/HDRComposite/RootSignature.hlsl @@ -72,7 +72,7 @@ "SRV(t4, space=9) " \ "), " \ \ - "RootConstants(num32BitConstants=4, b3), " \ + "RootConstants(num32BitConstants=10, b3), " \ "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ diff --git a/shaders/PostSharpen/RootSignature.hlsl b/shaders/PostSharpen/RootSignature.hlsl index 5db8933e..c46fa491 100644 --- a/shaders/PostSharpen/RootSignature.hlsl +++ b/shaders/PostSharpen/RootSignature.hlsl @@ -68,7 +68,7 @@ "SRV(t0, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=4, b3), " \ + "RootConstants(num32BitConstants=10, b3), " \ "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ diff --git a/shaders/ScaleformComposite/RootSignature.hlsl b/shaders/ScaleformComposite/RootSignature.hlsl index 10cc0da3..87dafe37 100644 --- a/shaders/ScaleformComposite/RootSignature.hlsl +++ b/shaders/ScaleformComposite/RootSignature.hlsl @@ -7,5 +7,5 @@ "SRV(t0, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=4, b3), " \ + "RootConstants(num32BitConstants=10, b3), " \ "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " \ No newline at end of file diff --git a/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl b/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl index 224cda9f..42408c61 100644 --- a/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl +++ b/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl @@ -34,17 +34,17 @@ float4 PS(PSInputs inputs) : SV_Target // Theoretically all UI is in sRGB (though it might have been designed on gamma 2.2 screens, we can't know that, but it was definately targeting gamma 2.2 as output anyway). UIColor.xyz = gamma_sRGB_to_linear(UIColor.xyz); UIColor.xyz = UIColor.xyz * UIIntensity; -#if !ENABLE_HDR - UIColor.xyz = gamma_linear_to_sRGB(UIColor.xyz); -#else +#if ENABLE_HDR #if SDR_USE_GAMMA_2_2 UIColor.xyz = pow(gamma_linear_to_sRGB(UIColor.xyz), 2.2f); #endif // HDR_GAMMA_CORRECTION - UIColor.xyz *= HDR_UI_PAPER_WHITE; + UIColor.xyz *= HdrDllPluginConstants.HDRUIPaperWhiteNits / WhiteNits_BT709; // Scale alpha to emulate sRGB gamma blending (we blend in linear space in HDR), // this won't ever be perfect but it's close enough for most cases. // We do a saturate to avoid pow of -0, which might lead to unexpected results. UIColor.a = pow(saturate(UIColor.a), HDR_UI_BLEND_POW); //TODO: base the percentage of application of "HDR_UI_BLEND_POW" based on how black/dark the UI color is. +#else + UIColor.xyz = gamma_linear_to_sRGB(UIColor.xyz); #endif // ENABLE_HDR return UIColor; diff --git a/shaders/compile_all_shaders.bat b/shaders/compile_all_shaders.bat deleted file mode 100644 index 2189203a..00000000 --- a/shaders/compile_all_shaders.bat +++ /dev/null @@ -1,92 +0,0 @@ -@echo off - -cd /d "%~dp0" - -set dxc=..\tools\dxc_2023_08_14\bin\x64\dxc.exe - -for /f "delims=" %%x in (target_folder.txt) do set target_folder=%%x - -if "%target_folder%" == "" ( - echo target folder isn't set in "target_folder.txt"! - echo single line with the folder with no quotation marks and a trailing \ - echo example: C:\Games\Starfield\Data\shadersfx\ - pause - goto :eof -) - -if not exist "%target_folder%" ( - echo target folder doesn't exist! - echo creating it... - mkdir "%target_folder%" -) - -"%dxc%" "HDRComposite\HDRComposite_ps.hlsl" ^ - -T ps_6_6 -E PS -D APPLY_MERGED_COLOR_GRADING_LUT -Fo ^ - "%target_folder%HDRComposite\HDRComposite_E00FF1A_ps.bin" - -call :errorlevel_check - -"%dxc%" "HDRComposite\HDRComposite_ps.hlsl" ^ - -T ps_6_6 -E PS -Fo ^ - "%target_folder%HDRComposite\HDRComposite_600FF1A_ps.bin" - -call :errorlevel_check - -"%dxc%" "HDRComposite\HDRComposite_ps.hlsl" ^ - -T ps_6_6 -E PS -D APPLY_BLOOM -Fo ^ - "%target_folder%HDRComposite\HDRComposite_700FF1A_ps.bin" - -call :errorlevel_check - -"%dxc%" "HDRComposite\HDRComposite_ps.hlsl" ^ - -T ps_6_6 -E PS -D APPLY_BLOOM -D APPLY_MERGED_COLOR_GRADING_LUT -Fo ^ - "%target_folder%HDRComposite\HDRComposite_F00FF1A_ps.bin" - -call :errorlevel_check - -"%dxc%" "ScaleformComposite\ScaleformComposite_FFAA_ps.hlsl" ^ - -T ps_6_6 -E PS -Fo ^ - "%target_folder%ScaleformComposite\ScaleformComposite_FFAA_ps.bin" - -call :errorlevel_check - -"%dxc%" "ColorGradingMerge\ColorGradingMerge_FF81_cs.hlsl" ^ - -T cs_6_6 -E CS -Fo ^ - "%target_folder%ColorGradingMerge\ColorGradingMerge_FF81_cs.bin" - -call :errorlevel_check - -"%dxc%" "BinkMovie\BinkMovie_FFAB_ps.hlsl" ^ - -T ps_6_6 -E PS -Fo ^ - "%target_folder%BinkMovie\BinkMovie_FFAB_ps.bin" - -call :errorlevel_check - -"%dxc%" "FilmGrain\FilmGrain_FF75_ps.hlsl" ^ - -T ps_6_6 -E main -Fo ^ - "%target_folder%FilmGrain\FilmGrain_FF75_ps.bin" - -call :errorlevel_check - -"%dxc%" "PostSharpen\PostSharpen_FF9A_ps.hlsl" ^ - -T ps_6_6 -E main -Fo ^ - "%target_folder%PostSharpen\PostSharpen_FF9A_ps.bin" - -call :errorlevel_check - -"%dxc%" "ContrastAdaptiveSharpening\ContrastAdaptiveSharpening_100FF94_cs.hlsl" ^ - -T cs_6_6 -E CS -enable-16bit-types -Wno-conversion -Fo ^ - "%target_folder%ContrastAdaptiveSharpening\ContrastAdaptiveSharpening_100FF94_cs.bin" - -call :errorlevel_check - -pause - -:errorlevel_check - if %ERRORLEVEL% NEQ 0 ( - echo. - echo compilation failed. Press any key to continue... - pause > NUL - echo. - ) -goto :eof diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 12f3ed30..f98f2b13 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -2,18 +2,9 @@ #include "structs.hlsl" -// Enables HDR scRGB output. If false, we output Rec.709 in gamma space (~sRGB). #define ENABLE_HDR 1 -// Set equal to the max nits your display can output -#define HDR_MAX_OUTPUT_NITS 1000.f -// User configured variable -#define HDR_GAME_PAPER_WHITE_MULTIPLIER 1.f -// User configured variable -#define HDR_UI_PAPER_WHITE_MULTIPLIER 1.f // Brings the range roughly from 80 nits to 203 nits (~2.5) -#define HDR_REFERENCE_PAPER_WHITE (ReferenceWhiteNits_BT2408 / WhiteNits_BT709) -#define HDR_GAME_PAPER_WHITE HDR_REFERENCE_PAPER_WHITE * HDR_GAME_PAPER_WHITE_MULTIPLIER -#define HDR_UI_PAPER_WHITE HDR_REFERENCE_PAPER_WHITE * HDR_UI_PAPER_WHITE_MULTIPLIER +#define HDR_REFERENCE_PAPER_WHITE_MUTLIPLIER (ReferenceWhiteNits_BT2408 / WhiteNits_BT709) // If this is true, the code makes the assumption that Bethesda developed and calibrated the game on gamma 2.2 screens, as opposed to sRGB gamma. // This implies there was a mismatch baked in the output colors, as they were using a ~sRGB similar formula, which would then be interpreted by screens as 2.2 gamma. @@ -30,16 +21,20 @@ // Custom push constants uploaded by the HDR DLL plugin code. Do note that register space comes at a premium when adding members. Bit/byte packing is advised. struct StructHdrDllPluginConstants { + // SDR 0 (Rec.709 with 2.2 gamma, not sRGB), 1 HDR10 PQ BT.2020, 2 scRGB HDR uint DisplayMode; - float PeakBrightness; - float GamePaperWhite; - float UIPaperWhite; - float Saturation; - float LUTCorrectionStrength; - float ColorGradingStrength; - float Contrast; - float DevSetting01; - float DevSetting02; + float HDRPeakBrightnessNits; // Set equal to the max nits your display can output + float HDRGamePaperWhiteNits; // 203 is the reference value (ReferenceWhiteNits_BT2408) + float HDRUIPaperWhiteNits; // 203 is the reference value (ReferenceWhiteNits_BT2408) + float HDRLUTCorrectionSaturation; // 1 is neutral + float LUTCorrectionStrength; // 1 is full strength + float ColorGradingStrength; // 1 is full strength + float HDRSecondaryContrast; // 1 is neutral + float DevSetting01; // 0-1 variable for development + float DevSetting02; // 0-1 variable for development }; -ConstantBuffer HdrDllPluginConstants : register(b3, space0); +//TODO: use this in the root signature files? +#define HDR_PLUGIN_CONSTANTS_SIZE 10 + +ConstantBuffer HdrDllPluginConstants : register(b3, space0); \ No newline at end of file From 66f3ef4f2bba0fc0221f12690d6452016ea2d5c0 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 7 Oct 2023 12:48:04 +0300 Subject: [PATCH 088/270] Polish user settings Make "Contrast" an HDR only setting. Add compiler conditions on dev settings (WIP). Improve tooltips and change some default setting values. --- Plugin/dist/NativeHDR.toml | 16 ++++++++-------- Plugin/src/Hooks.cpp | 22 ++++++++++++---------- Plugin/src/Settings.h | 24 ++++++++++++++---------- shaders/shared.hlsl | 2 +- 4 files changed, 35 insertions(+), 29 deletions(-) diff --git a/Plugin/dist/NativeHDR.toml b/Plugin/dist/NativeHDR.toml index f934b65f..a262a17e 100644 --- a/Plugin/dist/NativeHDR.toml +++ b/Plugin/dist/NativeHDR.toml @@ -1,19 +1,19 @@ -[Dev] -DevSetting01 = 0.0 -DevSetting02 = 0.0 - [HDR] -GamePaperWhite = 200.0 PeakBrightness = 1000.0 +GamePaperWhite = 203.0 +UIPaperWhite = 203.0 Saturation = 50.0 -UIPaperWhite = 200.0 +Contrast = 50.0 [Main] -ColorGradingStrength = 100.0 -Contrast = 50.0 DisplayMode = 2 +ColorGradingStrength = 100.0 LUTCorrectionStrength = 100.0 +[Dev] +DevSetting01 = 0.0 +DevSetting02 = 0.0 + [RenderTargets] RenderTargetsToUpgrade = [ 'ImageSpaceBuffer', diff --git a/Plugin/src/Hooks.cpp b/Plugin/src/Hooks.cpp index 9d27c6d0..20483b5e 100644 --- a/Plugin/src/Hooks.cpp +++ b/Plugin/src/Hooks.cpp @@ -109,11 +109,13 @@ namespace Hooks CreateSliderSetting(a_settingList, settings->GamePaperWhite, settings->IsHDREnabled()); CreateSliderSetting(a_settingList, settings->UIPaperWhite, settings->IsHDREnabled()); CreateSliderSetting(a_settingList, settings->Saturation, settings->IsHDREnabled()); + CreateSliderSetting(a_settingList, settings->Contrast, settings->IsHDREnabled()); CreateSliderSetting(a_settingList, settings->LUTCorrectionStrength, true); CreateSliderSetting(a_settingList, settings->ColorGradingStrength, true); - CreateSliderSetting(a_settingList, settings->Contrast, true); +#if 1 CreateSliderSetting(a_settingList, settings->DevSetting01, true); CreateSliderSetting(a_settingList, settings->DevSetting02, true); +#endif CreateSeparator(a_settingList, Settings::SettingID::kEND); } @@ -233,15 +235,15 @@ namespace Hooks case static_cast(Settings::SettingID::kHDR_Saturation): HandleSetting(settings->Saturation); break; + case static_cast(Settings::SettingID::kHDR_Contrast): + HandleSetting(settings->Contrast); + break; case static_cast(Settings::SettingID::kLUTCorrectionStrength): HandleSetting(settings->LUTCorrectionStrength); break; case static_cast(Settings::SettingID::kColorGradingStrength): HandleSetting(settings->ColorGradingStrength); break; - case static_cast(Settings::SettingID::kContrast): - HandleSetting(settings->Contrast); - break; case static_cast(Settings::SettingID::kDevSetting01): HandleSetting(settings->DevSetting01); break; @@ -271,12 +273,12 @@ namespace Hooks static_cast(*settings->PeakBrightness.value), static_cast(*settings->GamePaperWhite.value), static_cast(*settings->UIPaperWhite.value), - static_cast(*settings->Saturation.value * 0.02f), - static_cast(*settings->LUTCorrectionStrength.value * 0.01f), - static_cast(*settings->ColorGradingStrength.value * 0.01f), - static_cast(*settings->Contrast.value * 0.02f), - static_cast(*settings->DevSetting01.value * 0.01f), - static_cast(*settings->DevSetting02.value * 0.01f) + static_cast(*settings->Saturation.value * 0.02f), // 0-100 to 0-2 + static_cast(*settings->Contrast.value * 0.02f), // 0-100 to 0-2 + static_cast(*settings->LUTCorrectionStrength.value * 0.01f), // 0-100 to 0-1 + static_cast(*settings->ColorGradingStrength.value * 0.01f), // 0-100 to 0-1 + static_cast(*settings->DevSetting01.value * 0.01f), // 0-100 to 0-1 + static_cast(*settings->DevSetting02.value * 0.01f) // 0-100 to 0-1 }; if (!Compute) diff --git a/Plugin/src/Settings.h b/Plugin/src/Settings.h index a83db495..96bf1907 100644 --- a/Plugin/src/Settings.h +++ b/Plugin/src/Settings.h @@ -20,9 +20,9 @@ namespace Settings kHDR_GamePaperWhite, kHDR_UIPaperWhite, kHDR_Saturation, + kHDR_Contrast, kLUTCorrectionStrength, kColorGradingStrength, - kContrast, kDevSetting01, kDevSetting02, @@ -67,9 +67,9 @@ namespace Settings float GamePaperWhite; float UIPaperWhite; float Saturation; + float Contrast; float LUTCorrectionStrength; float ColorGradingStrength; - float Contrast; float DevSetting01; float DevSetting02; }; @@ -78,17 +78,19 @@ namespace Settings class Main : public DKUtil::model::Singleton
{ public: - Stepper DisplayMode{ SettingID::kDisplayMode, "Display Mode", "Sets the game's display mode between SDR, HDR10 PQ, or HDR scRGB", { "DisplayMode", "Main" }, { "SDR", "HDR10 PQ", "HDR scRGB" } }; + Stepper DisplayMode{ SettingID::kDisplayMode, "Display Mode", "Sets the game's display mode between SDR (Gamma 2.2 Rec.709), HDR10 BT.2020 PQ, or HDR scRGB", { "DisplayMode", "Main" }, { "SDR", "HDR10", "HDR scRGB" } }; - Slider PeakBrightness{ SettingID::kHDR_PeakBrightness, "Peak Brightness", "Sets the peak brightness in HDR modes", { "PeakBrightness", "HDR" }, 80.f, 10000.f, 1000.f }; - Slider GamePaperWhite{ SettingID::kHDR_GamePaperWhite, "Game Paper White", "Sets the game paper white brightness in HDR modes", { "GamePaperWhite", "HDR" }, 80.f, 500.f, 200.f }; - Slider UIPaperWhite{ SettingID::kHDR_UIPaperWhite, "UI Paper White", "Sets the UI paper white brightness in HDR modes", { "UIPaperWhite", "HDR" }, 80.f, 500.f, 200.f }; - Slider Saturation{ SettingID::kHDR_Saturation, "Saturation", "Sets the saturation strength in HDR modes", { "Saturation", "HDR" }, 0.f, 100.f, 50.f }; + Slider PeakBrightness{ SettingID::kHDR_PeakBrightness, "Peak Brightness", "Sets the peak nits brightness in HDR modes, this should match your display peak brightness", { "PeakBrightness", "HDR" }, 80.f, 10000.f, 1000.f }; + Slider GamePaperWhite{ SettingID::kHDR_GamePaperWhite, "Game Paper White", "Sets the game paper white nits brightness in HDR modes", { "GamePaperWhite", "HDR" }, 80.f, 500.f, 203.f }; + Slider UIPaperWhite{ SettingID::kHDR_UIPaperWhite, "UI Paper White", "Sets the UI paper white nits brightness in HDR modes", { "UIPaperWhite", "HDR" }, 80.f, 500.f, 203.f }; + Slider Saturation{ SettingID::kHDR_Saturation, "Saturation", "Sets the saturation strength in HDR modes (only applies if LUT correction is on) (neutral at 50)", { "Saturation", "HDR" }, 0.f, 100.f, 50.f }; + Slider Contrast{ SettingID::kHDR_Contrast, "Contrast", "Sets the contrast strength in HDR modes (neutral at 50)", { "Contrast", "HDR" }, 0.f, 100.f, 50.f }; Slider LUTCorrectionStrength{ SettingID::kLUTCorrectionStrength, "LUT Correction Strength", "Sets the LUT correction (normalization) strength", { "LUTCorrectionStrength", "Main" }, 0.f, 100.f, 100.f }; - Slider ColorGradingStrength{ SettingID::kColorGradingStrength, "Color Grading Strength", "Sets the color grading strength", { "ColorGradingStrength", "Main" }, 0.f, 100.f, 100.f }; - Slider Contrast{ SettingID::kContrast, "Contrast", "Sets the contrast", { "Contrast", "Main" }, 0.f, 100.f, 50.f }; + Slider ColorGradingStrength{ SettingID::kColorGradingStrength, "Color Grading Strength", "Sets the color grading strength (e.g. LUTs)", { "ColorGradingStrength", "Main" }, 0.f, 100.f, 100.f }; +#if 1 Slider DevSetting01{ SettingID::kDevSetting01, "DevSetting01", "Development setting", { "DevSetting01", "Dev" }, 0.f, 100.f, 0.f }; Slider DevSetting02{ SettingID::kDevSetting02, "DevSetting02", "Development setting", { "DevSetting02", "Dev" }, 0.f, 100.f, 0.f }; +#endif String RenderTargetsToUpgrade{ "RenderTargetsToUpgrade", "RenderTargets" }; bool IsHDREnabled() const; @@ -145,11 +147,13 @@ namespace Settings DrawReshadeSlider(settings, settings->GamePaperWhite); DrawReshadeSlider(settings, settings->UIPaperWhite); DrawReshadeSlider(settings, settings->Saturation); + DrawReshadeSlider(settings, settings->Contrast); DrawReshadeSlider(settings, settings->LUTCorrectionStrength); DrawReshadeSlider(settings, settings->ColorGradingStrength); - DrawReshadeSlider(settings, settings->Contrast); +#if 1 DrawReshadeSlider(settings, settings->DevSetting01); DrawReshadeSlider(settings, settings->DevSetting02); +#endif } static inline bool bReshadeSettingsOverlayRegistered = false; diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index f98f2b13..c6258078 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -27,9 +27,9 @@ struct StructHdrDllPluginConstants float HDRGamePaperWhiteNits; // 203 is the reference value (ReferenceWhiteNits_BT2408) float HDRUIPaperWhiteNits; // 203 is the reference value (ReferenceWhiteNits_BT2408) float HDRLUTCorrectionSaturation; // 1 is neutral + float HDRSecondaryContrast; // 1 is neutral float LUTCorrectionStrength; // 1 is full strength float ColorGradingStrength; // 1 is full strength - float HDRSecondaryContrast; // 1 is neutral float DevSetting01; // 0-1 variable for development float DevSetting02; // 0-1 variable for development }; From 63189fa8fce28ca416f48f97432eaf607ea01faa Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 7 Oct 2023 12:54:35 +0300 Subject: [PATCH 089/270] Add HDR toggle support to UI shader --- .../ScaleformComposite_ps.hlsl | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl b/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl index 42408c61..f92c3296 100644 --- a/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl +++ b/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl @@ -34,18 +34,21 @@ float4 PS(PSInputs inputs) : SV_Target // Theoretically all UI is in sRGB (though it might have been designed on gamma 2.2 screens, we can't know that, but it was definately targeting gamma 2.2 as output anyway). UIColor.xyz = gamma_sRGB_to_linear(UIColor.xyz); UIColor.xyz = UIColor.xyz * UIIntensity; -#if ENABLE_HDR + if (HdrDllPluginConstants.DisplayMode > 0) + { #if SDR_USE_GAMMA_2_2 - UIColor.xyz = pow(gamma_linear_to_sRGB(UIColor.xyz), 2.2f); + UIColor.xyz = pow(gamma_linear_to_sRGB(UIColor.xyz), 2.2f); #endif // HDR_GAMMA_CORRECTION - UIColor.xyz *= HdrDllPluginConstants.HDRUIPaperWhiteNits / WhiteNits_BT709; - // Scale alpha to emulate sRGB gamma blending (we blend in linear space in HDR), - // this won't ever be perfect but it's close enough for most cases. - // We do a saturate to avoid pow of -0, which might lead to unexpected results. - UIColor.a = pow(saturate(UIColor.a), HDR_UI_BLEND_POW); //TODO: base the percentage of application of "HDR_UI_BLEND_POW" based on how black/dark the UI color is. -#else - UIColor.xyz = gamma_linear_to_sRGB(UIColor.xyz); -#endif // ENABLE_HDR + UIColor.xyz *= HdrDllPluginConstants.HDRUIPaperWhiteNits / WhiteNits_BT709; + // Scale alpha to emulate sRGB gamma blending (we blend in linear space in HDR), + // this won't ever be perfect but it's close enough for most cases. + // We do a saturate to avoid pow of -0, which might lead to unexpected results. + UIColor.a = pow(saturate(UIColor.a), HDR_UI_BLEND_POW); //TODO: base the percentage of application of "HDR_UI_BLEND_POW" based on how black/dark the UI color is. + } + else + { + UIColor.xyz = gamma_linear_to_sRGB(UIColor.xyz); + } return UIColor; } From e754cd96476f2ea9fc408afadad7c65fbe59d8d6 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 7 Oct 2023 17:26:09 +0300 Subject: [PATCH 090/270] Improve sharpening code --- .../ContrastAdaptiveSharpening_cs.hlsl | 56 ++++++++++--------- shaders/HDRComposite/HDRComposite_ps.hlsl | 3 +- shaders/PostSharpen/PostSharpen_ps.hlsl | 32 ++++++++--- 3 files changed, 57 insertions(+), 34 deletions(-) diff --git a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl index 5a3bbd38..d3bff060 100644 --- a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl +++ b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl @@ -41,6 +41,13 @@ struct CSInput // return f16tof32(uint2(value & 0xffff, value >> 16)); //} +#if SDR_USE_GAMMA_2_2 + #define GAMMA_TO_LINEAR(x) pow(x, 2.2h) + #define LINEAR_TO_GAMMA(x) pow(x, 1.h / 2.2h) +#else //TODO: make this use half instead of float + #define GAMMA_TO_LINEAR(x) gamma_sRGB_to_linear(x) + #define LINEAR_TO_GAMMA(x) gamma_linear_to_sRGB(x) +#endif [RootSignature(ShaderRootSignature)] [numthreads(64, 1, 1)] @@ -147,17 +154,16 @@ void CS(CSInput csInput) half3 _128 = _128f; half3 _173 = _173f; - //TODO: use sRGB gamma if SDR_USE_GAMMA_2_2 is false - _91 = saturate(pow( _91, 2.2h)); - _139 = saturate(pow(_139, 2.2h)); - _106 = saturate(pow(_106, 2.2h)); - _151 = saturate(pow(_151, 2.2h)); - _109 = saturate(pow(_109, 2.2h)); - _156 = saturate(pow(_156, 2.2h)); - _119 = saturate(pow(_119, 2.2h)); - _168 = saturate(pow(_168, 2.2h)); - _128 = saturate(pow(_128, 2.2h)); - _173 = saturate(pow(_173, 2.2h)); + _91 = saturate(GAMMA_TO_LINEAR(_91)); + _139 = saturate(GAMMA_TO_LINEAR(_139)); + _106 = saturate(GAMMA_TO_LINEAR(_106)); + _151 = saturate(GAMMA_TO_LINEAR(_151)); + _109 = saturate(GAMMA_TO_LINEAR(_109)); + _156 = saturate(GAMMA_TO_LINEAR(_156)); + _119 = saturate(GAMMA_TO_LINEAR(_119)); + _168 = saturate(GAMMA_TO_LINEAR(_168)); + _128 = saturate(GAMMA_TO_LINEAR(_128)); + _173 = saturate(GAMMA_TO_LINEAR(_173)); #endif // ENABLE_HDR @@ -214,7 +220,7 @@ void CS(CSInput csInput) float3 colorOut2 = BT2020_To_BT709(_1003); #else // ENABLE_HDR - float3 colorOut2 = pow(_329, 1.f / 2.2h); + float3 colorOut2 = LINEAR_TO_GAMMA(_329); #endif // ENABLE_HDR if ((pp.x <= _55.z) && (pp.y <= _55.w)) @@ -226,7 +232,7 @@ void CS(CSInput csInput) float3 colorOut1 = BT2020_To_BT709(_396); #else // ENABLE_HDR - float3 colorOut1 = pow(_388, 1.f / 2.2h); + float3 colorOut1 = LINEAR_TO_GAMMA(_388); #endif // ENABLE_HDR ColorOut[pp] = float4(colorOut1, 1.f); @@ -294,16 +300,16 @@ void CS(CSInput csInput) half3 _540 = _540f; half3 _575 = _575f; - _509 = saturate(pow(_509f, 2.2h)); - _547 = saturate(pow(_547f, 2.2h)); - _521 = saturate(pow(_521f, 2.2h)); - _556 = saturate(pow(_556f, 2.2h)); - _524 = saturate(pow(_524f, 2.2h)); - _561 = saturate(pow(_561f, 2.2h)); - _531 = saturate(pow(_531f, 2.2h)); - _570 = saturate(pow(_570f, 2.2h)); - _540 = saturate(pow(_540f, 2.2h)); - _575 = saturate(pow(_575f, 2.2h)); + _509 = saturate(GAMMA_TO_LINEAR(_509f)); + _547 = saturate(GAMMA_TO_LINEAR(_547f)); + _521 = saturate(GAMMA_TO_LINEAR(_521f)); + _556 = saturate(GAMMA_TO_LINEAR(_556f)); + _524 = saturate(GAMMA_TO_LINEAR(_524f)); + _561 = saturate(GAMMA_TO_LINEAR(_561f)); + _531 = saturate(GAMMA_TO_LINEAR(_531f)); + _570 = saturate(GAMMA_TO_LINEAR(_570f)); + _540 = saturate(GAMMA_TO_LINEAR(_540f)); + _575 = saturate(GAMMA_TO_LINEAR(_575f)); #endif // ENABLE_HDR @@ -358,7 +364,7 @@ void CS(CSInput csInput) float3 colorOut4 = BT2020_To_BT709(_1216); #else // ENABLE_HDR - float3 colorOut4 = pow(_724, 1.f / 2.2h); + float3 colorOut4 = LINEAR_TO_GAMMA(_724); #endif // ENABLE_HDR if ((pp.x <= _55.z) && (pp.y <= _55.w)) @@ -370,7 +376,7 @@ void CS(CSInput csInput) float3 colorOut3 = BT2020_To_BT709(_790); #else // ENABLE_HDR - float3 colorOut3 = pow(_783, 1.f / 2.2h); + float3 colorOut3 = LINEAR_TO_GAMMA(_783); #endif // ENABLE_HDR ColorOut[uint2(_58, pp.y)] = float4(colorOut3, 1.f); diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index b91e0e44..214d495f 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -808,8 +808,7 @@ PSOutput PS(PSInput psInput) #if defined(APPLY_BLOOM) float3 bloom = Bloom.Sample(Sampler0, psInput.TEXCOORD); - float bloomMultiplier = PcwHdrComposite.BloomMultiplier; - inputColor += bloomMultiplier * bloom; + inputColor += PcwHdrComposite.BloomMultiplier * bloom; #endif // APPLY_BLOOM diff --git a/shaders/PostSharpen/PostSharpen_ps.hlsl b/shaders/PostSharpen/PostSharpen_ps.hlsl index 33977680..967be984 100644 --- a/shaders/PostSharpen/PostSharpen_ps.hlsl +++ b/shaders/PostSharpen/PostSharpen_ps.hlsl @@ -13,7 +13,7 @@ cbuffer _18_20 : register(b0, space0) }; Texture2D TonemappedColorTexture : register(t0, space8); // Possibly in gamma space in SDR -SamplerState Sampler0 : register(s13, space6); +SamplerState Sampler0 : register(s13, space6); // Likely bilinear SamplerState Sampler1 : register(s15, space6); // Likely nearest neighbor static float4 TEXCOORD; @@ -29,14 +29,24 @@ struct SPIRV_Cross_Output float4 SV_Target : SV_Target0; }; +#if SDR_USE_GAMMA_2_2 + #define GAMMA_TO_LINEAR(x) pow(x, 2.2f) + #define LINEAR_TO_GAMMA(x) pow(x, 1.f / 2.2f) +#else //TODO: make this use half instead of float + #define GAMMA_TO_LINEAR(x) gamma_sRGB_to_linear(x) + #define LINEAR_TO_GAMMA(x) gamma_linear_to_sRGB(x) +#endif + void frag_main() { - float3 inColor = TonemappedColorTexture.Sample(Sampler0, float2(TEXCOORD.x, TEXCOORD.y)).xyz; + float3 inColor = TonemappedColorTexture.Sample(Sampler0, TEXCOORD.xy).xyz; +#if !ENABLE_HDR + inColor = GAMMA_TO_LINEAR(inColor); +#endif + float3 outColor = inColor; uint4 sharpenParams = asuint(_20_m0[0u]); float sharpenIntensity = asfloat(sharpenParams.z); - float3 outColor = inColor; -#if !ENABLE_HDR //TODO: implement - if (sharpenIntensity > 0.f) //TODO: should this check for "sharpenIntensity < 1.f"? It seems like its the inverse intensity + if (sharpenIntensity > 0.f) { float _70 = ((_15_m0[161u].z * TEXCOORD.x) * asfloat(sharpenParams.x)) + 0.5f; float _72 = ((_15_m0[161u].w * TEXCOORD.y) * asfloat(sharpenParams.y)) + 0.5f; @@ -60,10 +70,18 @@ void frag_main() float3 _157 = TonemappedColorTexture.SampleLevel(Sampler1, float2(_147, _145), 0.0f); float3 _162 = TonemappedColorTexture.SampleLevel(Sampler1, float2(_144, _149), 0.0f); float3 _167 = TonemappedColorTexture.SampleLevel(Sampler1, float2(_147, _149), 0.0f); +#if !ENABLE_HDR + _152 = GAMMA_TO_LINEAR(_152); + _157 = GAMMA_TO_LINEAR(_157); + _162 = GAMMA_TO_LINEAR(_162); + _167 = GAMMA_TO_LINEAR(_167); +#endif float3 sharpenedColor = (((_167 * _98) + (_162 * _87)) * _136) + (((_157 * _98) + (_152 * _87)) * _125); - outColor = ((inColor - sharpenedColor) * sharpenIntensity) + sharpenedColor; - //outColor = lerp(sharpenedColor, inColor, sharpenIntensity); + // It seems controls the amount of (manaually done) bilinear filtering vs nearest neightbor, so full sharpening is just bilinear + outColor = lerp(sharpenedColor, inColor, sharpenIntensity); } +#if !ENABLE_HDR + outColor = LINEAR_TO_GAMMA(outColor); #endif SV_Target.xyz = outColor; SV_Target.w = 1.0f; From 101d25298f8554862a4cd7fed195a8bed1fdd490 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 7 Oct 2023 17:51:37 +0300 Subject: [PATCH 091/270] Add BT.2020 toggle to CAS --- .../ContrastAdaptiveSharpening_cs.hlsl | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl index d3bff060..8b1a9ec1 100644 --- a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl +++ b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl @@ -49,12 +49,13 @@ struct CSInput #define LINEAR_TO_GAMMA(x) gamma_linear_to_sRGB(x) #endif +#define HDR_BT2020 1 + [RootSignature(ShaderRootSignature)] [numthreads(64, 1, 1)] void CS(CSInput csInput) { -#if 1 - // butchered the CS so it just outputs the input +#if 0 // Disable the CS so it just outputs the input uint4 _55 = CASData.cas2; uint ppx = _55.x + (((csInput.SV_GroupThreadID.x >> 1u) & 7u) | (csInput.SV_GroupID.x << 4u)); uint ppy = ((((csInput.SV_GroupThreadID.x >> 3u) & 6u) | (csInput.SV_GroupThreadID.x & 1u)) | (csInput.SV_GroupID.y << 4u)) + _55.y; @@ -113,6 +114,7 @@ void CS(CSInput csInput) #if ENABLE_HDR +#if HDR_BT2020 _91f = BT709_To_BT2020( _91f); _139f = BT709_To_BT2020(_139f); _106f = BT709_To_BT2020(_106f); @@ -123,6 +125,7 @@ void CS(CSInput csInput) _168f = BT709_To_BT2020(_168f); _128f = BT709_To_BT2020(_128f); _173f = BT709_To_BT2020(_173f); +#endif // since blue is the least contributing in terms of luminance // the worst case is red and green at 0 and blue high enough so that the luminance is HDR_MAX_OUTPUT_NITS @@ -218,7 +221,7 @@ void CS(CSInput csInput) #if ENABLE_HDR float3 _1003 = _329 * normalizationFactor; - float3 colorOut2 = BT2020_To_BT709(_1003); + float3 colorOut2 = (bool)HDR_BT2020 ? BT2020_To_BT709(_1003) : _1003; #else // ENABLE_HDR float3 colorOut2 = LINEAR_TO_GAMMA(_329); #endif // ENABLE_HDR @@ -230,7 +233,7 @@ void CS(CSInput csInput) #if ENABLE_HDR float3 _396 = _388 * normalizationFactor; - float3 colorOut1 = BT2020_To_BT709(_396); + float3 colorOut1 = (bool)HDR_BT2020 ? BT2020_To_BT709(_396) : _396; #else // ENABLE_HDR float3 colorOut1 = LINEAR_TO_GAMMA(_388); #endif // ENABLE_HDR @@ -265,6 +268,7 @@ void CS(CSInput csInput) #if ENABLE_HDR +#if HDR_BT2020 _509f = BT709_To_BT2020(_509f); _547f = BT709_To_BT2020(_547f); _521f = BT709_To_BT2020(_521f); @@ -275,6 +279,7 @@ void CS(CSInput csInput) _570f = BT709_To_BT2020(_570f); _540f = BT709_To_BT2020(_540f); _575f = BT709_To_BT2020(_575f); +#endif half3 _509 = saturate(_509f / normalizationFactor); half3 _547 = saturate(_547f / normalizationFactor); @@ -362,7 +367,7 @@ void CS(CSInput csInput) #if ENABLE_HDR float3 _1216 = _724 * normalizationFactor; - float3 colorOut4 = BT2020_To_BT709(_1216); + float3 colorOut4 = (bool)HDR_BT2020 ? BT2020_To_BT709(_1216) : _1216; #else // ENABLE_HDR float3 colorOut4 = LINEAR_TO_GAMMA(_724); #endif // ENABLE_HDR @@ -374,7 +379,7 @@ void CS(CSInput csInput) #if ENABLE_HDR float3 _790 = _783 * normalizationFactor; - float3 colorOut3 = BT2020_To_BT709(_790); + float3 colorOut3 = (bool)HDR_BT2020 ? BT2020_To_BT709(_790) : _790; #else // ENABLE_HDR float3 colorOut3 = LINEAR_TO_GAMMA(_783); #endif // ENABLE_HDR From 6af61114715293a739638a65bdda187e2cf841fb Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 7 Oct 2023 17:51:47 +0300 Subject: [PATCH 092/270] add negative luminance handling --- shaders/HDRComposite/HDRComposite_ps.hlsl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 214d495f..b75fabee 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -798,11 +798,12 @@ PSOutput PS(PSInput psInput) float3 inputColor = InputColor.Load(int3(int2(psInput.SV_Position.xy), 0)); #if CLAMP_INPUT_OUTPUT - // Remove any negative value caused by using R16G16B16A16F buffers (originally this was R11G11B10F, which has no negative values). // Doing gamut mapping, or keeping the colors outside of BT.709 doesn't seem to be right, as they seem to be just be accidentally coming out of some shader math. inputColor = max(inputColor, 0.f); - +#else + if (Luminance(inputColor) < 0.f) + inputColor = 0.f; #endif // CLAMP_INPUT_OUTPUT #if defined(APPLY_BLOOM) @@ -986,6 +987,9 @@ PSOutput PS(PSInput psInput) #if CLAMP_INPUT_OUTPUT outputColor = clamp(outputColor, 0.f, FLT16_MAX); // Avoid extremely high numbers turning into NaN in FP16 +#else + if (Luminance(outputColor) < 0.f) + outputColor = 0.f; #endif // CLAMP_INPUT_OUTPUT #else // ENABLE_HDR From c3bae1cae1b3c3ca51e3e15c5bfe24fc504939f3 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 7 Oct 2023 17:52:54 +0300 Subject: [PATCH 093/270] Fix build --- .../ContrastAdaptiveSharpening_cs.hlsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl index 8b1a9ec1..61c97721 100644 --- a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl +++ b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl @@ -131,7 +131,7 @@ void CS(CSInput csInput) // the worst case is red and green at 0 and blue high enough so that the luminance is HDR_MAX_OUTPUT_NITS // TODO: use more accurate value than just the K factor from the YCbCr<->RGB transform // TODO: normalizing by peak brightness isn't right, we need to do a quick inverse pass of the tonemapper. - static const float normalizationFactor = (HdrDllPluginConstants.HDRPeakBrightness / WhiteNits_BT709) / 0.0593f; + static const float normalizationFactor = (HdrDllPluginConstants.HDRPeakBrightnessNits / WhiteNits_BT709) / 0.0593f; half3 _91 = saturate( _91f / normalizationFactor); half3 _139 = saturate(_139f / normalizationFactor); From ff0beab4dea2ba8e3ae7a193be6c77d81e157bdc Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 7 Oct 2023 18:27:31 +0300 Subject: [PATCH 094/270] Fixed black screen with Hable --- shaders/HDRComposite/HDRComposite_ps.hlsl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index b75fabee..31001e1e 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -282,7 +282,9 @@ float3 Hable( // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L6 // Eval float evalY0 = 0.f; +#if 0 // Fixes full black output in some location (due to "invScale" being 0) if (params_overshootX > 0.f) +#endif { evalY0 = -exp2(((shoulderSegment_B_optimised * log2(params_overshootX)) + hableParams.shoulderSegment.lnA) * LOG2_E) + params_overshootY; } @@ -969,7 +971,12 @@ PSOutput PS(PSInput psInput) #endif // Secondary user driven contrast +#if 0 // By luminance (no hue shift) (looks off) + float inverseTonemappedPostProcessedColorLuminance = Luminance(inverseTonemappedPostProcessedColor); + inverseTonemappedPostProcessedColor *= safeDivision(pow(inverseTonemappedPostProcessedColorLuminance / (MidGray * midGrayScale), HdrDllPluginConstants.HDRSecondaryContrast) * (MidGray * midGrayScale), inverseTonemappedPostProcessedColorLuminance); +#else // By channel (also increases saturation) inverseTonemappedPostProcessedColor = pow(inverseTonemappedPostProcessedColor / (MidGray * midGrayScale), HdrDllPluginConstants.HDRSecondaryContrast) * (MidGray * midGrayScale); +#endif // Bring back the color to the same range as SDR by dividing by the mid gray change. inverseTonemappedPostProcessedColor *= paperWhite / midGrayScale; From 77052ba2c7ed8e47c8ceb3e2c59b4b960677c652 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 7 Oct 2023 19:07:59 +0300 Subject: [PATCH 095/270] Add HDR/SDR runtime branches on all shaders --- shaders/BinkMovie/BinkMovie_ps.hlsl | 13 +- .../ColorGradingMerge_cs.hlsl | 2 +- .../ContrastAdaptiveSharpening_cs.hlsl | 4 +- shaders/FilmGrain/FilmGrain_ps.hlsl | 16 +- shaders/HDRComposite/HDRComposite_ps.hlsl | 171 +++++++++--------- shaders/PostSharpen/PostSharpen_ps.hlsl | 27 +-- shaders/shared.hlsl | 1 - 7 files changed, 120 insertions(+), 114 deletions(-) diff --git a/shaders/BinkMovie/BinkMovie_ps.hlsl b/shaders/BinkMovie/BinkMovie_ps.hlsl index 5919087d..94b91330 100644 --- a/shaders/BinkMovie/BinkMovie_ps.hlsl +++ b/shaders/BinkMovie/BinkMovie_ps.hlsl @@ -41,16 +41,17 @@ float4 PS(PSInputs inputs) : SV_Target // as UI blending produces invalid pixels if it's blended with an invalid color. color = max(color, 0.f); -#if ENABLE_HDR + if (HdrDllPluginConstants.DisplayMode > 0) + { #if SDR_USE_GAMMA_2_2 - color = pow(color, 2.2f); + color = pow(color, 2.2f); #else - color = gamma_sRGB_to_linear(color); + color = gamma_sRGB_to_linear(color); #endif - //TODO: AutoHDR on movies??? + //TODO: AutoHDR on movies??? - color *= HdrDllPluginConstants.HDRGamePaperWhiteNits / WhiteNits_BT709; // Use the game brightness, not the UI one, as these are usually videos that are seamless with gameplay -#endif // ENABLE_HDR + color *= HdrDllPluginConstants.HDRGamePaperWhiteNits / WhiteNits_BT709; // Use the game brightness, not the UI one, as these are usually videos that are seamless with gameplay + } return float4(color, 1.0f); } diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl index 591610f0..1a871629 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl @@ -295,7 +295,7 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) #endif // LUT_IMPROVEMENT_TYPE - const bool SDRRange = HdrDllPluginConstants.DisplayMode == 0 || (bool)FORCE_SDR_LUTS; + const bool SDRRange = HdrDllPluginConstants.DisplayMode <= 0 || (bool)FORCE_SDR_LUTS; #if LUT_IMPROVEMENT_TYPE == 1 float3 LUT1Color = PatchLUTColor(LUT1, inUVW, neutralLUTColor, SDRRange); diff --git a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl index 61c97721..a6deb343 100644 --- a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl +++ b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl @@ -55,7 +55,7 @@ struct CSInput [numthreads(64, 1, 1)] void CS(CSInput csInput) { -#if 0 // Disable the CS so it just outputs the input +#if 1 // Disable the CS so it just outputs the input uint4 _55 = CASData.cas2; uint ppx = _55.x + (((csInput.SV_GroupThreadID.x >> 1u) & 7u) | (csInput.SV_GroupID.x << 4u)); uint ppy = ((((csInput.SV_GroupThreadID.x >> 3u) & 6u) | (csInput.SV_GroupThreadID.x & 1u)) | (csInput.SV_GroupID.y << 4u)) + _55.y; @@ -112,7 +112,7 @@ void CS(CSInput csInput) float3 _168f = ColorIn.Load(int3(_167, _105, 0)).rgb; float3 _173f = ColorIn.Load(int3(_138, _127, 0)).rgb; -#if ENABLE_HDR +#if ENABLE_HDR //TODO: replace with "if (HdrDllPluginConstants.DisplayMode > 0)" #if HDR_BT2020 _91f = BT709_To_BT2020( _91f); diff --git a/shaders/FilmGrain/FilmGrain_ps.hlsl b/shaders/FilmGrain/FilmGrain_ps.hlsl index 96ecfc54..324c5e75 100644 --- a/shaders/FilmGrain/FilmGrain_ps.hlsl +++ b/shaders/FilmGrain/FilmGrain_ps.hlsl @@ -51,11 +51,12 @@ void frag_main() // .z = Film Grading Intensity (0 - 0.03) float3 tonemappedColor = TonemappedColorTexture.Sample(Sampler0, float2(TEXCOORD.x, TEXCOORD.y)); -#if ENABLE_HDR const float paperWhite = HdrDllPluginConstants.HDRGamePaperWhiteNits / WhiteNits_BT709; - tonemappedColor /= paperWhite; - tonemappedColor = gamma_linear_to_sRGB(tonemappedColor); -#endif // ENABLE_HDR + if (HdrDllPluginConstants.DisplayMode > 0) + { + tonemappedColor /= paperWhite; + tonemappedColor = gamma_linear_to_sRGB(tonemappedColor); + } float colorY = Luminance(tonemappedColor); #if FILM_GRAIN_TYPE == 0 @@ -112,9 +113,10 @@ float colorY = Luminance(tonemappedColor); } #endif -#if ENABLE_HDR - tonemappedColorWithFilmGrain = gamma_sRGB_to_linear(tonemappedColorWithFilmGrain) * paperWhite; -#endif // ENABLE_HDR + if (HdrDllPluginConstants.DisplayMode > 0) + { + tonemappedColorWithFilmGrain = gamma_sRGB_to_linear(tonemappedColorWithFilmGrain) * paperWhite; + } SV_Target.rgb = tonemappedColorWithFilmGrain; diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 31001e1e..045d966a 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -815,6 +815,8 @@ PSOutput PS(PSInput psInput) #endif // APPLY_BLOOM + float3 outputColor = inputColor; + float3 tonemappedColor; #if !ENABLE_TONEMAP @@ -885,137 +887,136 @@ PSOutput PS(PSInput psInput) const float3 finalOriginalColor = tonemappedPostProcessedGradedColor; // Final "original" (vanilla, ~unmodded) linear SDR color before output transform -#if ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP && ENABLE_HDR +#if ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP const float3 postProcessColorRatio = finalOriginalColor / tonemappedColor; const float3 postProcessColorOffset = finalOriginalColor - tonemappedColor; #endif -#if ENABLE_HDR - + if (HdrDllPluginConstants.DisplayMode > 0) // HDR + { #if ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP - const float paperWhite = HdrDllPluginConstants.HDRGamePaperWhiteNits / WhiteNits_BT709; + const float paperWhite = HdrDllPluginConstants.HDRGamePaperWhiteNits / WhiteNits_BT709; - const float midGrayIn = MidGray; - float midGrayOut = midGrayIn; + const float midGrayIn = MidGray; + float midGrayOut = midGrayIn; - float3 inverseTonemappedColor = tonemappedColor; + float3 inverseTonemappedColor = tonemappedColor; - float minHighlightsColorIn = MinHighlightsColor; // We consider highlight the last ~33% of perception space - float minHighlightsColorOut = minHighlightsColorIn; - const bool onlyInvertHighlights = (bool)INVERT_TONEMAP_HIGHLIGHTS_ONLY; + float minHighlightsColorIn = MinHighlightsColor; // We consider highlight the last ~33% of perception space + float minHighlightsColorOut = minHighlightsColorIn; + const bool onlyInvertHighlights = (bool)INVERT_TONEMAP_HIGHLIGHTS_ONLY; - // Restore a color very close to the original linear one (some information might get close in the direct tonemapper) - switch (tonemapperIndex) - { - case 1: + // Restore a color very close to the original linear one (some information might get close in the direct tonemapper) + switch (tonemapperIndex) { - inverseTonemappedColor = ACESReference_Inverse(inverseTonemappedColor); - midGrayOut = ACESReference_Inverse(midGrayIn); - minHighlightsColorOut = ACESReference_Inverse(minHighlightsColorIn); - } break; + case 1: + { + inverseTonemappedColor = ACESReference_Inverse(inverseTonemappedColor); + midGrayOut = ACESReference_Inverse(midGrayIn); + minHighlightsColorOut = ACESReference_Inverse(minHighlightsColorIn); + } break; - case 2: - { - inverseTonemappedColor = ACESParametric_Inverse(inverseTonemappedColor, acesParam_modE, acesParam_modA); - midGrayOut = ACESParametric_Inverse(midGrayIn, acesParam_modE, acesParam_modA); - minHighlightsColorOut = ACESParametric_Inverse(minHighlightsColorIn, acesParam_modE, acesParam_modA); - } break; + case 2: + { + inverseTonemappedColor = ACESParametric_Inverse(inverseTonemappedColor, acesParam_modE, acesParam_modA); + midGrayOut = ACESParametric_Inverse(midGrayIn, acesParam_modE, acesParam_modA); + minHighlightsColorOut = ACESParametric_Inverse(minHighlightsColorIn, acesParam_modE, acesParam_modA); + } break; - case 3: - { - // Setup highlights for Hable, we use the official param based highlights shoulder start for it - minHighlightsColorIn = max(hableParams.params.y0, hableParams.params.y1); // Check both toe and mid params for extra safety + case 3: + { + // Setup highlights for Hable, we use the official param based highlights shoulder start for it + minHighlightsColorIn = max(hableParams.params.y0, hableParams.params.y1); // Check both toe and mid params for extra safety - inverseTonemappedColor = Hable_Inverse(inverseTonemappedColor, 3, hableParams); - midGrayOut = Hable_Inverse(midGrayIn, 1, hableParams); - minHighlightsColorOut = Hable_Inverse(minHighlightsColorIn, 1, hableParams); - } break; + inverseTonemappedColor = Hable_Inverse(inverseTonemappedColor, 3, hableParams); + midGrayOut = Hable_Inverse(midGrayIn, 1, hableParams); + minHighlightsColorOut = Hable_Inverse(minHighlightsColorIn, 1, hableParams); + } break; - default: - break; - } + default: + break; + } - for (uint channel = 0; channel < 3; channel++) - { - // Scale all non highlights by the scale the smallest (first) highlight would have, so we keep the curves connected - if (onlyInvertHighlights && inverseTonemappedColor[channel] < minHighlightsColorOut) + for (uint channel = 0; channel < 3; channel++) { - inverseTonemappedColor[channel] = tonemappedColor[channel] * (minHighlightsColorOut / minHighlightsColorIn); + // Scale all non highlights by the scale the smallest (first) highlight would have, so we keep the curves connected + if (onlyInvertHighlights && inverseTonemappedColor[channel] < minHighlightsColorOut) + { + inverseTonemappedColor[channel] = tonemappedColor[channel] * (minHighlightsColorOut / minHighlightsColorIn); + } + // Restore any highlight clipped or just crushed by the direct tonemappers (Hable does that) + if (inverseTonemappedColor[channel] >= minHighlightsColorOut) + { + inverseTonemappedColor[channel] = inputColor[channel]; + } } - // Restore any highlight clipped or just crushed by the direct tonemappers (Hable does that) - if (inverseTonemappedColor[channel] >= minHighlightsColorOut) + const float previousMidGrayOut = midGrayOut; + // If we only inverted highlights, the mid gray in and out should have the same value, so we need to base it on the highlights in/out change + if (onlyInvertHighlights && midGrayOut < minHighlightsColorOut) // This is probably always true { - inverseTonemappedColor[channel] = inputColor[channel]; + midGrayOut = midGrayIn * (minHighlightsColorOut / minHighlightsColorIn); + } + // Shift back to the original linear color if we want to ignore the SDR tonemapper + if (onlyInvertHighlights && SDRTonemapHDRStrength != 1.f) + { + inverseTonemappedColor = lerp(inputColor, inverseTonemappedColor, SDRTonemapHDRStrength); + midGrayOut = lerp(previousMidGrayOut, midGrayOut, SDRTonemapHDRStrength); + minHighlightsColorOut = lerp(minHighlightsColorIn, minHighlightsColorOut, SDRTonemapHDRStrength); } - } - const float previousMidGrayOut = midGrayOut; - // If we only inverted highlights, the mid gray in and out should have the same value, so we need to base it on the highlights in/out change - if (onlyInvertHighlights && midGrayOut < minHighlightsColorOut) // This is probably always true - { - midGrayOut = midGrayIn * (minHighlightsColorOut / minHighlightsColorIn); - } - // Shift back to the original linear color if we want to ignore the SDR tonemapper - if (onlyInvertHighlights && SDRTonemapHDRStrength != 1.f) - { - inverseTonemappedColor = lerp(inputColor, inverseTonemappedColor, SDRTonemapHDRStrength); - midGrayOut = lerp(previousMidGrayOut, midGrayOut, SDRTonemapHDRStrength); - minHighlightsColorOut = lerp(minHighlightsColorIn, minHighlightsColorOut, SDRTonemapHDRStrength); - } - const float midGrayScale = midGrayOut / midGrayIn; + const float midGrayScale = midGrayOut / midGrayIn; - float3 inverseTonemappedPostProcessedColor = RestorePostProcess(inverseTonemappedColor, postProcessColorRatio, postProcessColorOffset, tonemappedColor, midGrayScale); + float3 inverseTonemappedPostProcessedColor = RestorePostProcess(inverseTonemappedColor, postProcessColorRatio, postProcessColorOffset, tonemappedColor, midGrayScale); #if 0 // Enable this if you want the highlights should start to be affected by post processing. It doesn't seem like the right thing to do and having it off works just fine. - minHighlightsColorOut = RestorePostProcess(minHighlightsColorOut, postProcessColorRatio, postProcessColorOffset, tonemappedColor, midGrayScale); + minHighlightsColorOut = RestorePostProcess(minHighlightsColorOut, postProcessColorRatio, postProcessColorOffset, tonemappedColor, midGrayScale); #endif - // Secondary user driven contrast + // Secondary user driven contrast #if 0 // By luminance (no hue shift) (looks off) - float inverseTonemappedPostProcessedColorLuminance = Luminance(inverseTonemappedPostProcessedColor); - inverseTonemappedPostProcessedColor *= safeDivision(pow(inverseTonemappedPostProcessedColorLuminance / (MidGray * midGrayScale), HdrDllPluginConstants.HDRSecondaryContrast) * (MidGray * midGrayScale), inverseTonemappedPostProcessedColorLuminance); + float inverseTonemappedPostProcessedColorLuminance = Luminance(inverseTonemappedPostProcessedColor); + inverseTonemappedPostProcessedColor *= safeDivision(pow(inverseTonemappedPostProcessedColorLuminance / (MidGray * midGrayScale), HdrDllPluginConstants.HDRSecondaryContrast) * (MidGray * midGrayScale), inverseTonemappedPostProcessedColorLuminance); #else // By channel (also increases saturation) - inverseTonemappedPostProcessedColor = pow(inverseTonemappedPostProcessedColor / (MidGray * midGrayScale), HdrDllPluginConstants.HDRSecondaryContrast) * (MidGray * midGrayScale); + inverseTonemappedPostProcessedColor = pow(inverseTonemappedPostProcessedColor / (MidGray * midGrayScale), HdrDllPluginConstants.HDRSecondaryContrast) * (MidGray * midGrayScale); #endif - // Bring back the color to the same range as SDR by dividing by the mid gray change. - inverseTonemappedPostProcessedColor *= paperWhite / midGrayScale; - minHighlightsColorOut *= paperWhite / midGrayScale; + // Bring back the color to the same range as SDR by dividing by the mid gray change. + inverseTonemappedPostProcessedColor *= paperWhite / midGrayScale; + minHighlightsColorOut *= paperWhite / midGrayScale; - const float maxOutputLuminance = HdrDllPluginConstants.HDRPeakBrightnessNits / WhiteNits_BT709; - const float highlightsShoulderStart = onlyInvertHighlights ? minHighlightsColorOut : 0.f; - float3 outputColor = DICETonemap(inverseTonemappedPostProcessedColor, maxOutputLuminance, highlightsShoulderStart); + const float maxOutputLuminance = HdrDllPluginConstants.HDRPeakBrightnessNits / WhiteNits_BT709; + const float highlightsShoulderStart = onlyInvertHighlights ? minHighlightsColorOut : 0.f; + outputColor = DICETonemap(inverseTonemappedPostProcessedColor, maxOutputLuminance, highlightsShoulderStart); #else // ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP - float3 outputColor = finalOriginalColor * (HdrDllPluginConstants.HDRGamePaperWhiteNits / ReferenceWhiteNits_BT2408); // Don't use "HDRGamePaperWhiteNits" directly as it'd be too bright on an untonemapped image + outputColor = finalOriginalColor * (HdrDllPluginConstants.HDRGamePaperWhiteNits / ReferenceWhiteNits_BT2408); // Don't use "HDRGamePaperWhiteNits" directly as it'd be too bright on an untonemapped image #endif // ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP #if CLAMP_INPUT_OUTPUT - outputColor = clamp(outputColor, 0.f, FLT16_MAX); // Avoid extremely high numbers turning into NaN in FP16 + outputColor = clamp(outputColor, 0.f, FLT16_MAX); // Avoid extremely high numbers turning into NaN in FP16 #else - if (Luminance(outputColor) < 0.f) - outputColor = 0.f; + if (Luminance(outputColor) < 0.f) + outputColor = 0.f; #endif // CLAMP_INPUT_OUTPUT + } + else // SDR + { + outputColor = finalOriginalColor; -#else // ENABLE_HDR - - float3 outputColor = finalOriginalColor; - -// Note that gamma was never applied if LUTs were disabled, but we don't care about that as the affected shaders permutations were never used + // Note that gamma was never applied if LUTs were disabled, but we don't care about that as the affected shaders permutations were never used #if SDR_USE_GAMMA_2_2 - outputColor = pow(outputColor, 1.f / 2.2f); + outputColor = pow(outputColor, 1.f / 2.2f); #else - // Do sRGB gamma even if we'd be playing on gamma 2.2 screens, as the game was already calibrated for 2.2 gamma despite using the wrong formula - outputColor = gamma_linear_to_sRGB(outputColor); + // Do sRGB gamma even if we'd be playing on gamma 2.2 screens, as the game was already calibrated for 2.2 gamma despite using the wrong formula + outputColor = gamma_linear_to_sRGB(outputColor); #endif // SDR_USE_GAMMA_2_2 #if CLAMP_INPUT_OUTPUT - outputColor = saturate(outputColor); + outputColor = saturate(outputColor); #endif // CLAMP_INPUT_OUTPUT - -#endif // ENABLE_HDR + } PSOutput psOutput; psOutput.SV_Target.rgb = outputColor; diff --git a/shaders/PostSharpen/PostSharpen_ps.hlsl b/shaders/PostSharpen/PostSharpen_ps.hlsl index 967be984..9751f99c 100644 --- a/shaders/PostSharpen/PostSharpen_ps.hlsl +++ b/shaders/PostSharpen/PostSharpen_ps.hlsl @@ -40,9 +40,10 @@ struct SPIRV_Cross_Output void frag_main() { float3 inColor = TonemappedColorTexture.Sample(Sampler0, TEXCOORD.xy).xyz; -#if !ENABLE_HDR - inColor = GAMMA_TO_LINEAR(inColor); -#endif + if (HdrDllPluginConstants.DisplayMode <= 0) + { + inColor = GAMMA_TO_LINEAR(inColor); + } float3 outColor = inColor; uint4 sharpenParams = asuint(_20_m0[0u]); float sharpenIntensity = asfloat(sharpenParams.z); @@ -70,19 +71,21 @@ void frag_main() float3 _157 = TonemappedColorTexture.SampleLevel(Sampler1, float2(_147, _145), 0.0f); float3 _162 = TonemappedColorTexture.SampleLevel(Sampler1, float2(_144, _149), 0.0f); float3 _167 = TonemappedColorTexture.SampleLevel(Sampler1, float2(_147, _149), 0.0f); -#if !ENABLE_HDR - _152 = GAMMA_TO_LINEAR(_152); - _157 = GAMMA_TO_LINEAR(_157); - _162 = GAMMA_TO_LINEAR(_162); - _167 = GAMMA_TO_LINEAR(_167); -#endif + if (HdrDllPluginConstants.DisplayMode <= 0) + { + _152 = GAMMA_TO_LINEAR(_152); + _157 = GAMMA_TO_LINEAR(_157); + _162 = GAMMA_TO_LINEAR(_162); + _167 = GAMMA_TO_LINEAR(_167); + } float3 sharpenedColor = (((_167 * _98) + (_162 * _87)) * _136) + (((_157 * _98) + (_152 * _87)) * _125); // It seems controls the amount of (manaually done) bilinear filtering vs nearest neightbor, so full sharpening is just bilinear outColor = lerp(sharpenedColor, inColor, sharpenIntensity); } -#if !ENABLE_HDR - outColor = LINEAR_TO_GAMMA(outColor); -#endif + if (HdrDllPluginConstants.DisplayMode <= 0) + { + outColor = LINEAR_TO_GAMMA(outColor); + } SV_Target.xyz = outColor; SV_Target.w = 1.0f; } diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index c6258078..ad43f01d 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -2,7 +2,6 @@ #include "structs.hlsl" -#define ENABLE_HDR 1 // Brings the range roughly from 80 nits to 203 nits (~2.5) #define HDR_REFERENCE_PAPER_WHITE_MUTLIPLIER (ReferenceWhiteNits_BT2408 / WhiteNits_BT709) From 691439c97fe5ccbe6ec6d6db2f597ac9c3d1ff0a Mon Sep 17 00:00:00 2001 From: Lilium Date: Sat, 7 Oct 2023 18:38:03 +0200 Subject: [PATCH 096/270] fix the "just passthrough" mode for CAS and fix a compiler warning for the HDR / gamma mode --- .../ContrastAdaptiveSharpening_cs.hlsl | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl index a6deb343..23003452 100644 --- a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl +++ b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl @@ -56,25 +56,30 @@ struct CSInput void CS(CSInput csInput) { #if 1 // Disable the CS so it just outputs the input - uint4 _55 = CASData.cas2; - uint ppx = _55.x + (((csInput.SV_GroupThreadID.x >> 1u) & 7u) | (csInput.SV_GroupID.x << 4u)); + uint2 _55 = CASData.cas2.xy; + uint _58 = _55.x + (((csInput.SV_GroupThreadID.x >> 1u) & 7u) | (csInput.SV_GroupID.x << 4u)); + uint ppx = _58; uint ppy = ((((csInput.SV_GroupThreadID.x >> 3u) & 6u) | (csInput.SV_GroupThreadID.x & 1u)) | (csInput.SV_GroupID.y << 4u)) + _55.y; + + ColorOut[uint2(ppx, ppy)] = ColorIn.Load(int3(uint2(ppx, ppy), 0)); + ppx += 8; + + ColorOut[uint2(ppx, ppy)] = ColorIn.Load(int3(uint2(ppx, ppy), 0)); + ppy += 8; - ColorOut[uint2(ppx, ppy)] = ColorIn.Load(int3(uint2(ppx, ppy), 0u)); - ColorOut[uint2(ppx, ppy)] = ColorIn.Load(int3(uint2(ppx, ppy), 0u)); - ColorOut[uint2(ppx, ppy)] = ColorIn.Load(int3(uint2(ppx, ppy), 0u)); - ColorOut[uint2(ppx, ppy)] = ColorIn.Load(int3(uint2(ppx, ppy), 0u)); + ColorOut[uint2(_58, ppy)] = ColorIn.Load(int3(uint2(_58, ppy), 0)); + ColorOut[uint2(ppx, ppy)] = ColorIn.Load(int3(uint2(ppx, ppy), 0)); #else uint4 _55 = CASData.cas2; - uint _58 = (csInput.SV_GroupThreadID.x >> 1) & 7 + uint _58 = ((csInput.SV_GroupThreadID.x >> 1) & 7) | (csInput.SV_GroupID.x << 4) + _55.x; - uint2 pp = uint2(_58, (csInput.SV_GroupThreadID.x >> 3) & 6 + uint2 pp = uint2(_58, ((csInput.SV_GroupThreadID.x >> 3) & 6) | (csInput.SV_GroupThreadID.x & 1) | (csInput.SV_GroupID.y << 4) + _55.y); From aa5571d6943691c32f1b30184cfe75c7c348561b Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 7 Oct 2023 20:03:27 +0300 Subject: [PATCH 097/270] Remap user settings --- shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl | 5 ++--- shaders/HDRComposite/HDRComposite_ps.hlsl | 5 +++-- shaders/math.hlsl | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl index 1a871629..f2d9d4b7 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl @@ -187,7 +187,8 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b const float3 detintedColor = max(0.f, 1.f-((1.f - color) * reduceFactor)); // The saturation multiplier is restricted to HDR as it easily goes beyond Rec.709 - const float targetChroma = linear_srgb_to_oklch(detintedColor)[1] * (SDRRange ? 1.f : HdrDllPluginConstants.HDRLUTCorrectionSaturation); + const float saturation = linearNormalization(HdrDllPluginConstants.HDRLUTCorrectionSaturation, 0.f, 2.f, 0.5f, 1.5f); + const float targetChroma = linear_srgb_to_oklch(detintedColor)[1] * (SDRRange ? 1.f : saturation); // Adjust the value back to recreate a smooth Y gradient since 0 is floored // Sample and hold targetL @@ -297,12 +298,10 @@ void CS(uint3 SV_DispatchThreadID : SV_DispatchThreadID) const bool SDRRange = HdrDllPluginConstants.DisplayMode <= 0 || (bool)FORCE_SDR_LUTS; #if LUT_IMPROVEMENT_TYPE == 1 - float3 LUT1Color = PatchLUTColor(LUT1, inUVW, neutralLUTColor, SDRRange); float3 LUT2Color = PatchLUTColor(LUT2, inUVW, neutralLUTColor, SDRRange); float3 LUT3Color = PatchLUTColor(LUT3, inUVW, neutralLUTColor, SDRRange); float3 LUT4Color = PatchLUTColor(LUT4, inUVW, neutralLUTColor, SDRRange); - #elif LUT_IMPROVEMENT_TYPE == 2 float neutralLUTLuminance = Luminance(neutralLUTColor); float LUT1Luminance = Luminance(LUT1Color); diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 045d966a..ec1d160a 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -973,11 +973,12 @@ PSOutput PS(PSInput psInput) #endif // Secondary user driven contrast + const float secondaryContrast = linearNormalization(HdrDllPluginConstants.HDRSecondaryContrast, 0.f, 2.f, 0.5f, 1.5f); #if 0 // By luminance (no hue shift) (looks off) float inverseTonemappedPostProcessedColorLuminance = Luminance(inverseTonemappedPostProcessedColor); - inverseTonemappedPostProcessedColor *= safeDivision(pow(inverseTonemappedPostProcessedColorLuminance / (MidGray * midGrayScale), HdrDllPluginConstants.HDRSecondaryContrast) * (MidGray * midGrayScale), inverseTonemappedPostProcessedColorLuminance); + inverseTonemappedPostProcessedColor *= safeDivision(pow(inverseTonemappedPostProcessedColorLuminance / (MidGray * midGrayScale), secondaryContrast) * (MidGray * midGrayScale), inverseTonemappedPostProcessedColorLuminance); #else // By channel (also increases saturation) - inverseTonemappedPostProcessedColor = pow(inverseTonemappedPostProcessedColor / (MidGray * midGrayScale), HdrDllPluginConstants.HDRSecondaryContrast) * (MidGray * midGrayScale); + inverseTonemappedPostProcessedColor = pow(inverseTonemappedPostProcessedColor / (MidGray * midGrayScale), secondaryContrast) * (MidGray * midGrayScale); #endif // Bring back the color to the same range as SDR by dividing by the mid gray change. diff --git a/shaders/math.hlsl b/shaders/math.hlsl index 701fd255..d68682c7 100644 --- a/shaders/math.hlsl +++ b/shaders/math.hlsl @@ -11,7 +11,7 @@ float hypot3(float3 input) return sqrt(inputSquared.x + inputSquared.y + inputSquared.z); } -// From old to new range +// From old to new range (just a remap function) template T linearNormalization(T input, T min, T max, T newMin, T newMax) { From 9998c57cb37431974060a64d4dddfb7dca4d9285 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 7 Oct 2023 20:03:35 +0300 Subject: [PATCH 098/270] Polish --- Plugin/src/Settings.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Plugin/src/Settings.cpp b/Plugin/src/Settings.cpp index 7979d362..a8a081c6 100644 --- a/Plugin/src/Settings.cpp +++ b/Plugin/src/Settings.cpp @@ -61,9 +61,9 @@ namespace Settings config.Bind(GamePaperWhite.value, GamePaperWhite.defaultValue); config.Bind(UIPaperWhite.value, UIPaperWhite.defaultValue); config.Bind(Saturation.value, Saturation.defaultValue); + config.Bind(Contrast.value, Contrast.defaultValue); config.Bind(LUTCorrectionStrength.value, LUTCorrectionStrength.defaultValue); config.Bind(ColorGradingStrength.value, ColorGradingStrength.defaultValue); - config.Bind(Contrast.value, Contrast.defaultValue); config.Bind(DevSetting01.value, DevSetting01.defaultValue); config.Bind(DevSetting02.value, DevSetting02.defaultValue); config.Bind(RenderTargetsToUpgrade, @@ -77,7 +77,12 @@ namespace Settings "ImageSpaceBufferE5B9G9R9", "TAA_idTech7HistoryColorTarget", "EnvBRDF", - "ImageSpaceBufferR10G10B10A2"); + "ImageSpaceBufferR10G10B10A2", +#if 0 + 'NativeResolutionColorBuffer01', + 'ColorBuffer01' +#endif + ); }); config.Load(); From e899d5acdeca64320ae5c3ebe5419eae91a75ee0 Mon Sep 17 00:00:00 2001 From: Lilium Date: Sat, 7 Oct 2023 19:11:24 +0200 Subject: [PATCH 099/270] small cleanup for PostSharpen --- shaders/PostSharpen/PostSharpen_ps.hlsl | 81 ++++++++++++------------- shaders/compile_all_shaders.ps1 | 2 +- 2 files changed, 41 insertions(+), 42 deletions(-) diff --git a/shaders/PostSharpen/PostSharpen_ps.hlsl b/shaders/PostSharpen/PostSharpen_ps.hlsl index 9751f99c..6832cf32 100644 --- a/shaders/PostSharpen/PostSharpen_ps.hlsl +++ b/shaders/PostSharpen/PostSharpen_ps.hlsl @@ -7,24 +7,23 @@ cbuffer _13_15 : register(b0, space7) float4 _15_m0[3269] : packoffset(c0); }; -cbuffer _18_20 : register(b0, space0) +cbuffer PushConstantWrapper_PostSharpen : register(b0, space0) { - float4 _20_m0[2] : packoffset(c0); + float4 PcwPostSharpen0; + float3 PcwPostSharpen1; }; Texture2D TonemappedColorTexture : register(t0, space8); // Possibly in gamma space in SDR SamplerState Sampler0 : register(s13, space6); // Likely bilinear SamplerState Sampler1 : register(s15, space6); // Likely nearest neighbor -static float4 TEXCOORD; -static float4 SV_Target; - -struct SPIRV_Cross_Input +struct PSInput { - float4 TEXCOORD : TEXCOORD0; + float4 SV_Position : SV_Position0; + float2 TEXCOORD : TEXCOORD0; }; -struct SPIRV_Cross_Output +struct PSOutput { float4 SV_Target : SV_Target0; }; @@ -32,45 +31,50 @@ struct SPIRV_Cross_Output #if SDR_USE_GAMMA_2_2 #define GAMMA_TO_LINEAR(x) pow(x, 2.2f) #define LINEAR_TO_GAMMA(x) pow(x, 1.f / 2.2f) -#else //TODO: make this use half instead of float +#else #define GAMMA_TO_LINEAR(x) gamma_sRGB_to_linear(x) #define LINEAR_TO_GAMMA(x) gamma_linear_to_sRGB(x) #endif -void frag_main() + +[RootSignature(ShaderRootSignature)] +PSOutput PS(PSInput psInput) { - float3 inColor = TonemappedColorTexture.Sample(Sampler0, TEXCOORD.xy).xyz; +#if 0 + float3 outColor = TonemappedColorTexture.Sample(Sampler0, psInput.TEXCOORD.xy).rgb; +#else + float3 inColor = TonemappedColorTexture.Sample(Sampler0, psInput.TEXCOORD.xy).rgb; if (HdrDllPluginConstants.DisplayMode <= 0) { inColor = GAMMA_TO_LINEAR(inColor); } float3 outColor = inColor; - uint4 sharpenParams = asuint(_20_m0[0u]); - float sharpenIntensity = asfloat(sharpenParams.z); + float4 sharpenParams = PcwPostSharpen0; + float sharpenIntensity = sharpenParams.z; if (sharpenIntensity > 0.f) { - float _70 = ((_15_m0[161u].z * TEXCOORD.x) * asfloat(sharpenParams.x)) + 0.5f; - float _72 = ((_15_m0[161u].w * TEXCOORD.y) * asfloat(sharpenParams.y)) + 0.5f; + float _70 = ((_15_m0[161u].z * psInput.TEXCOORD.x) * sharpenParams.x) + 0.5f; + float _72 = ((_15_m0[161u].w * psInput.TEXCOORD.y) * sharpenParams.y) + 0.5f; float _76 = frac(_70); float _77 = frac(_72); - float _87 = ((((((_76 * 2.0f) + (-3.0f)) * _76) + (-3.0f)) * _76) + 5.0f) * 0.16666667163372039794921875f; - float _89 = _76 * 3.0f; - float _98 = ((((((3.0f - _89) + _76) * _76) + 3.0f) * _76) + 1.0f) * 0.16666667163372039794921875f; + float _87 = ((((((_76 * 2.f) + (-3.f)) * _76) + (-3.f)) * _76) + 5.f) * (1.f / 6.f); + float _89 = _76 * 3.f; + float _98 = ((((((3.f - _89) + _76) * _76) + 3.f) * _76) + 1.f) * (1.f / 6.f); float _99 = _76 * _76; float _113 = _77 * _77; - float _114 = _77 * 3.0f; - float _125 = ((((((_77 * 2.0f) + (-3.0f)) * _77) + (-3.0f)) * _77) + 5.0f) * 0.16666667163372039794921875f; - float _136 = ((((((3.0f - _114) + _77) * _77) + 3.0f) * _77) + 1.0f) * 0.16666667163372039794921875f; + float _114 = _77 * 3.f; + float _125 = ((((((_77 * 2.f) + (-3.f)) * _77) + (-3.f)) * _77) + 5.f) * (1.f / 6.f); + float _136 = ((((((3.f - _114) + _77) * _77) + 3.f) * _77) + 1.f) * (1.f / 6.f); float _139 = floor(_70) + (-0.5f); float _142 = floor(_72) + (-0.5f); - float _144 = (_139 + (((((_99 * (_89 + (-6.0f))) + 4.0f) * 0.16666667163372039794921875f) / _87) + (-1.0f))) / _15_m0[161u].z; - float _145 = (_142 + (((((_113 * (_114 + (-6.0f))) + 4.0f) * 0.16666667163372039794921875f) / _125) + (-1.0f))) / _15_m0[161u].w; - float _147 = (_139 + ((((_99 * 0.16666667163372039794921875f) * _76) / _98) + 1.0f)) / _15_m0[161u].z; - float _149 = (_142 + ((((_113 * 0.16666667163372039794921875f) * _77) / _136) + 1.0f)) / _15_m0[161u].w; - float3 _152 = TonemappedColorTexture.SampleLevel(Sampler1, float2(_144, _145), 0.0f); - float3 _157 = TonemappedColorTexture.SampleLevel(Sampler1, float2(_147, _145), 0.0f); - float3 _162 = TonemappedColorTexture.SampleLevel(Sampler1, float2(_144, _149), 0.0f); - float3 _167 = TonemappedColorTexture.SampleLevel(Sampler1, float2(_147, _149), 0.0f); + float _144 = (_139 + ((((( _99 * ( _89 + (-6.f))) + 4.f) * (1.f / 6.f)) / _87) + (-1.f))) / _15_m0[161u].z; + float _145 = (_142 + (((((_113 * (_114 + (-6.f))) + 4.f) * (1.f / 6.f)) / _125) + (-1.f))) / _15_m0[161u].w; + float _147 = (_139 + (((( _99 * (1.f / 6.f)) * _76) / _98) + 1.f)) / _15_m0[161u].z; + float _149 = (_142 + ((((_113 * (1.f / 6.f)) * _77) / _136) + 1.f)) / _15_m0[161u].w; + float3 _152 = TonemappedColorTexture.Sample(Sampler1, float2(_144, _145)); + float3 _157 = TonemappedColorTexture.Sample(Sampler1, float2(_147, _145)); + float3 _162 = TonemappedColorTexture.Sample(Sampler1, float2(_144, _149)); + float3 _167 = TonemappedColorTexture.Sample(Sampler1, float2(_147, _149)); if (HdrDllPluginConstants.DisplayMode <= 0) { _152 = GAMMA_TO_LINEAR(_152); @@ -80,22 +84,17 @@ void frag_main() } float3 sharpenedColor = (((_167 * _98) + (_162 * _87)) * _136) + (((_157 * _98) + (_152 * _87)) * _125); // It seems controls the amount of (manaually done) bilinear filtering vs nearest neightbor, so full sharpening is just bilinear - outColor = lerp(sharpenedColor, inColor, sharpenIntensity); + outColor = lerp(sharpenedColor, inColor, sharpenIntensity); } if (HdrDllPluginConstants.DisplayMode <= 0) { outColor = LINEAR_TO_GAMMA(outColor); } - SV_Target.xyz = outColor; - SV_Target.w = 1.0f; -} +#endif + PSOutput psOutput; -[RootSignature(ShaderRootSignature)] -SPIRV_Cross_Output main(SPIRV_Cross_Input stage_input) -{ - TEXCOORD = stage_input.TEXCOORD; - frag_main(); - SPIRV_Cross_Output stage_output; - stage_output.SV_Target = SV_Target; - return stage_output; + psOutput.SV_Target.rgb = outColor; + psOutput.SV_Target.a = 1.f; + + return psOutput; } diff --git a/shaders/compile_all_shaders.ps1 b/shaders/compile_all_shaders.ps1 index d59ceb16..570cd79b 100644 --- a/shaders/compile_all_shaders.ps1 +++ b/shaders/compile_all_shaders.ps1 @@ -21,7 +21,7 @@ $main = #Compile-Shader -Type "cs" -TechniqueName "ContrastAdaptiveSharpening" -TechniqueId "FF94" -Entry "main" Compile-Shader -Type "cs" -TechniqueName "ContrastAdaptiveSharpening" -TechniqueId "100FF94" -Defines "USE_PACKED_MATH" -AdditionalParams "-enable-16bit-types", "-Wno-conversion" - Compile-Shader -Type "ps" -TechniqueName "PostSharpen" -TechniqueId "FF9A" -Entry "main" + Compile-Shader -Type "ps" -TechniqueName "PostSharpen" -TechniqueId "FF9A" Compile-Shader -Type "ps" -TechniqueName "ScaleformComposite" -TechniqueId "FFAA" From bf69af695c411f98b5d24cf37123b50437930797 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 7 Oct 2023 20:33:39 +0300 Subject: [PATCH 100/270] Add minimum peak brightness and paper white checks to avoid the HDR DICE tonemapper looking awful with high paper white and low peak brightness --- shaders/HDRComposite/HDRComposite_ps.hlsl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index ec1d160a..a4316eb7 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -945,7 +945,8 @@ PSOutput PS(PSInput psInput) { inverseTonemappedColor[channel] = tonemappedColor[channel] * (minHighlightsColorOut / minHighlightsColorIn); } - // Restore any highlight clipped or just crushed by the direct tonemappers (Hable does that) + // Restore any highlight clipped or just crushed by the direct tonemappers (Hable does that). + // If there were problems with this (a disconnect in gradients), directly compare "inputColor" against "minHighlightsColorOut". if (inverseTonemappedColor[channel] >= minHighlightsColorOut) { inverseTonemappedColor[channel] = inputColor[channel]; @@ -986,7 +987,8 @@ PSOutput PS(PSInput psInput) minHighlightsColorOut *= paperWhite / midGrayScale; const float maxOutputLuminance = HdrDllPluginConstants.HDRPeakBrightnessNits / WhiteNits_BT709; - const float highlightsShoulderStart = onlyInvertHighlights ? minHighlightsColorOut : 0.f; + // The highlights shoulder (compression) curve should never start beyond 33.33% of the max output brightness + const float highlightsShoulderStart = onlyInvertHighlights ? min(maxOutputLuminance * (1.f / 3.f), minHighlightsColorOut) : 0.f; outputColor = DICETonemap(inverseTonemappedPostProcessedColor, maxOutputLuminance, highlightsShoulderStart); #else // ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP From d3e4d9fa2f3c0879dc55adb8c63f3a2b821854fb Mon Sep 17 00:00:00 2001 From: Lilium Date: Sat, 7 Oct 2023 19:52:18 +0200 Subject: [PATCH 101/270] fixup the error introduced by the last commit changes to PostSharpen --- shaders/PostSharpen/PostSharpen_ps.hlsl | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/shaders/PostSharpen/PostSharpen_ps.hlsl b/shaders/PostSharpen/PostSharpen_ps.hlsl index 6832cf32..34182a50 100644 --- a/shaders/PostSharpen/PostSharpen_ps.hlsl +++ b/shaders/PostSharpen/PostSharpen_ps.hlsl @@ -7,10 +7,17 @@ cbuffer _13_15 : register(b0, space7) float4 _15_m0[3269] : packoffset(c0); }; -cbuffer PushConstantWrapper_PostSharpen : register(b0, space0) + +struct PushConstantWrapper_PostSharpen +{ + float4 params0; + float3 params1; +}; + + +cbuffer CPushConstantWrapper_PostSharpen : register(b0, space0) { - float4 PcwPostSharpen0; - float3 PcwPostSharpen1; + PushConstantWrapper_PostSharpen PcwPostSharpen : packoffset(c0); }; Texture2D TonemappedColorTexture : register(t0, space8); // Possibly in gamma space in SDR @@ -19,8 +26,8 @@ SamplerState Sampler1 : register(s15, space6); // Likely nearest neighbor struct PSInput { + float4 TEXCOORD : TEXCOORD0; float4 SV_Position : SV_Position0; - float2 TEXCOORD : TEXCOORD0; }; struct PSOutput @@ -49,7 +56,7 @@ PSOutput PS(PSInput psInput) inColor = GAMMA_TO_LINEAR(inColor); } float3 outColor = inColor; - float4 sharpenParams = PcwPostSharpen0; + float4 sharpenParams = PcwPostSharpen.params0; float sharpenIntensity = sharpenParams.z; if (sharpenIntensity > 0.f) { From 47a70a40c53396c582bb6ac561a7fbf33fba002e Mon Sep 17 00:00:00 2001 From: Lilium Date: Sat, 7 Oct 2023 20:53:22 +0200 Subject: [PATCH 102/270] fix CAS issue and set it up for HDR --- .../ContrastAdaptiveSharpening_cs.hlsl | 302 ++++++++++-------- 1 file changed, 171 insertions(+), 131 deletions(-) diff --git a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl index 23003452..8cb78fc1 100644 --- a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl +++ b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl @@ -51,11 +51,17 @@ struct CSInput #define HDR_BT2020 1 +// since blue is the least contributing in terms of luminance +// the worst case is red and green at 0 and blue high enough so that the luminance is HDR_MAX_OUTPUT_NITS +// TODO: use more accurate value than just the K factor from the YCbCr<->RGB transform +// TODO: normalizing by peak brightness isn't right, we need to do a quick inverse pass of the tonemapper. +static const float normalizationFactor = (HdrDllPluginConstants.HDRPeakBrightnessNits / WhiteNits_BT709) / 0.0593f; + [RootSignature(ShaderRootSignature)] [numthreads(64, 1, 1)] void CS(CSInput csInput) { -#if 1 // Disable the CS so it just outputs the input +#if 0 // Disable the CS so it just outputs the input uint2 _55 = CASData.cas2.xy; uint _58 = _55.x + (((csInput.SV_GroupThreadID.x >> 1u) & 7u) | (csInput.SV_GroupID.x << 4u)); uint ppx = _58; @@ -75,13 +81,13 @@ void CS(CSInput csInput) uint4 _55 = CASData.cas2; - uint _58 = ((csInput.SV_GroupThreadID.x >> 1) & 7) - | (csInput.SV_GroupID.x << 4) + uint _58 = (((csInput.SV_GroupThreadID.x >> 1) & 7) + | (csInput.SV_GroupID.x << 4)) + _55.x; - uint2 pp = uint2(_58, ((csInput.SV_GroupThreadID.x >> 3) & 6) - | (csInput.SV_GroupThreadID.x & 1) - | (csInput.SV_GroupID.y << 4) + uint2 pp = uint2(_58, (((csInput.SV_GroupThreadID.x >> 3) & 6) + | (csInput.SV_GroupThreadID.x & 1) + | (csInput.SV_GroupID.y << 4)) + _55.y); uint4 _69 = CASData.cas3; @@ -117,63 +123,67 @@ void CS(CSInput csInput) float3 _168f = ColorIn.Load(int3(_167, _105, 0)).rgb; float3 _173f = ColorIn.Load(int3(_138, _127, 0)).rgb; -#if ENABLE_HDR //TODO: replace with "if (HdrDllPluginConstants.DisplayMode > 0)" - + half3 _91; + half3 _139; + half3 _106; + half3 _151; + half3 _109; + half3 _156; + half3 _119; + half3 _168; + half3 _128; + half3 _173; + + if (HdrDllPluginConstants.DisplayMode > 0) + { #if HDR_BT2020 - _91f = BT709_To_BT2020( _91f); - _139f = BT709_To_BT2020(_139f); - _106f = BT709_To_BT2020(_106f); - _151f = BT709_To_BT2020(_151f); - _109f = BT709_To_BT2020(_109f); - _156f = BT709_To_BT2020(_156f); - _119f = BT709_To_BT2020(_119f); - _168f = BT709_To_BT2020(_168f); - _128f = BT709_To_BT2020(_128f); - _173f = BT709_To_BT2020(_173f); + _91f = BT709_To_BT2020( _91f); + _139f = BT709_To_BT2020(_139f); + _106f = BT709_To_BT2020(_106f); + _151f = BT709_To_BT2020(_151f); + _109f = BT709_To_BT2020(_109f); + _156f = BT709_To_BT2020(_156f); + _119f = BT709_To_BT2020(_119f); + _168f = BT709_To_BT2020(_168f); + _128f = BT709_To_BT2020(_128f); + _173f = BT709_To_BT2020(_173f); #endif - // since blue is the least contributing in terms of luminance - // the worst case is red and green at 0 and blue high enough so that the luminance is HDR_MAX_OUTPUT_NITS - // TODO: use more accurate value than just the K factor from the YCbCr<->RGB transform - // TODO: normalizing by peak brightness isn't right, we need to do a quick inverse pass of the tonemapper. - static const float normalizationFactor = (HdrDllPluginConstants.HDRPeakBrightnessNits / WhiteNits_BT709) / 0.0593f; - - half3 _91 = saturate( _91f / normalizationFactor); - half3 _139 = saturate(_139f / normalizationFactor); - half3 _106 = saturate(_106f / normalizationFactor); - half3 _151 = saturate(_151f / normalizationFactor); - half3 _109 = saturate(_109f / normalizationFactor); - half3 _156 = saturate(_156f / normalizationFactor); - half3 _119 = saturate(_119f / normalizationFactor); - half3 _168 = saturate(_168f / normalizationFactor); - half3 _128 = saturate(_128f / normalizationFactor); - half3 _173 = saturate(_173f / normalizationFactor); - -#else // ENABLE_HDR - - half3 _91 = _91f; - half3 _139 = _139f; - half3 _106 = _106f; - half3 _151 = _151f; - half3 _109 = _109f; - half3 _156 = _156f; - half3 _119 = _119f; - half3 _168 = _168f; - half3 _128 = _128f; - half3 _173 = _173f; - - _91 = saturate(GAMMA_TO_LINEAR(_91)); - _139 = saturate(GAMMA_TO_LINEAR(_139)); - _106 = saturate(GAMMA_TO_LINEAR(_106)); - _151 = saturate(GAMMA_TO_LINEAR(_151)); - _109 = saturate(GAMMA_TO_LINEAR(_109)); - _156 = saturate(GAMMA_TO_LINEAR(_156)); - _119 = saturate(GAMMA_TO_LINEAR(_119)); - _168 = saturate(GAMMA_TO_LINEAR(_168)); - _128 = saturate(GAMMA_TO_LINEAR(_128)); - _173 = saturate(GAMMA_TO_LINEAR(_173)); - -#endif // ENABLE_HDR + _91 = saturate( _91f / normalizationFactor); + _139 = saturate(_139f / normalizationFactor); + _106 = saturate(_106f / normalizationFactor); + _151 = saturate(_151f / normalizationFactor); + _109 = saturate(_109f / normalizationFactor); + _156 = saturate(_156f / normalizationFactor); + _119 = saturate(_119f / normalizationFactor); + _168 = saturate(_168f / normalizationFactor); + _128 = saturate(_128f / normalizationFactor); + _173 = saturate(_173f / normalizationFactor); + } + else + { + _91 = _91f; + _139 = _139f; + _106 = _106f; + _151 = _151f; + _109 = _109f; + _156 = _156f; + _119 = _119f; + _168 = _168f; + _128 = _128f; + _173 = _173f; + + _91 = saturate(GAMMA_TO_LINEAR(_91)); + _139 = saturate(GAMMA_TO_LINEAR(_139)); + _106 = saturate(GAMMA_TO_LINEAR(_106)); + _151 = saturate(GAMMA_TO_LINEAR(_151)); + _109 = saturate(GAMMA_TO_LINEAR(_109)); + _156 = saturate(GAMMA_TO_LINEAR(_156)); + _119 = saturate(GAMMA_TO_LINEAR(_119)); + _168 = saturate(GAMMA_TO_LINEAR(_168)); + _128 = saturate(GAMMA_TO_LINEAR(_128)); + _173 = saturate(GAMMA_TO_LINEAR(_173)); + } half _193 = _91.y; half _194 = _139.y; @@ -223,25 +233,35 @@ void CS(CSInput csInput) half3 _329 = (((_151 + _139 + _168 + _173) * _316) + _156) * _323; _329 = saturate(_329); -#if ENABLE_HDR - float3 _1003 = _329 * normalizationFactor; - float3 colorOut2 = (bool)HDR_BT2020 ? BT2020_To_BT709(_1003) : _1003; -#else // ENABLE_HDR - float3 colorOut2 = LINEAR_TO_GAMMA(_329); -#endif // ENABLE_HDR + float3 colorOut2; + if (HdrDllPluginConstants.DisplayMode > 0) + { + float3 _1003 = _329 * normalizationFactor; + + colorOut2 = (bool)HDR_BT2020 ? BT2020_To_BT709(_1003) : _1003; + } + else + { + colorOut2 = LINEAR_TO_GAMMA(_329); + } if ((pp.x <= _55.z) && (pp.y <= _55.w)) { half3 _388 = (((_106 + _91 + _119 + _128) * _315) + _109) * _322; _388 = saturate(_388); -#if ENABLE_HDR - float3 _396 = _388 * normalizationFactor; - float3 colorOut1 = (bool)HDR_BT2020 ? BT2020_To_BT709(_396) : _396; -#else // ENABLE_HDR - float3 colorOut1 = LINEAR_TO_GAMMA(_388); -#endif // ENABLE_HDR + float3 colorOut1; + if (HdrDllPluginConstants.DisplayMode > 0) + { + float3 _396 = _388 * normalizationFactor; + + colorOut1 = (bool)HDR_BT2020 ? BT2020_To_BT709(_396) : _396; + } + else + { + colorOut1 = LINEAR_TO_GAMMA(_388); + } ColorOut[pp] = float4(colorOut1, 1.f); } @@ -271,57 +291,67 @@ void CS(CSInput csInput) float3 _570f = ColorIn.Load(int3(_167, _520, 0)).rgb; float3 _575f = ColorIn.Load(int3(_138, _539, 0)).rgb; -#if ENABLE_HDR - + half3 _509; + half3 _547; + half3 _521; + half3 _556; + half3 _524; + half3 _561; + half3 _531; + half3 _570; + half3 _540; + half3 _575; + + if (HdrDllPluginConstants.DisplayMode > 0) + { #if HDR_BT2020 - _509f = BT709_To_BT2020(_509f); - _547f = BT709_To_BT2020(_547f); - _521f = BT709_To_BT2020(_521f); - _556f = BT709_To_BT2020(_556f); - _524f = BT709_To_BT2020(_524f); - _561f = BT709_To_BT2020(_561f); - _531f = BT709_To_BT2020(_531f); - _570f = BT709_To_BT2020(_570f); - _540f = BT709_To_BT2020(_540f); - _575f = BT709_To_BT2020(_575f); + _509f = BT709_To_BT2020(_509f); + _547f = BT709_To_BT2020(_547f); + _521f = BT709_To_BT2020(_521f); + _556f = BT709_To_BT2020(_556f); + _524f = BT709_To_BT2020(_524f); + _561f = BT709_To_BT2020(_561f); + _531f = BT709_To_BT2020(_531f); + _570f = BT709_To_BT2020(_570f); + _540f = BT709_To_BT2020(_540f); + _575f = BT709_To_BT2020(_575f); #endif - half3 _509 = saturate(_509f / normalizationFactor); - half3 _547 = saturate(_547f / normalizationFactor); - half3 _521 = saturate(_521f / normalizationFactor); - half3 _556 = saturate(_556f / normalizationFactor); - half3 _524 = saturate(_524f / normalizationFactor); - half3 _561 = saturate(_561f / normalizationFactor); - half3 _531 = saturate(_531f / normalizationFactor); - half3 _570 = saturate(_570f / normalizationFactor); - half3 _540 = saturate(_540f / normalizationFactor); - half3 _575 = saturate(_575f / normalizationFactor); - -#else // ENABLE_HDR - - half3 _509 = _509f; - half3 _547 = _547f; - half3 _521 = _521f; - half3 _556 = _556f; - half3 _524 = _524f; - half3 _561 = _561f; - half3 _531 = _531f; - half3 _570 = _570f; - half3 _540 = _540f; - half3 _575 = _575f; - - _509 = saturate(GAMMA_TO_LINEAR(_509f)); - _547 = saturate(GAMMA_TO_LINEAR(_547f)); - _521 = saturate(GAMMA_TO_LINEAR(_521f)); - _556 = saturate(GAMMA_TO_LINEAR(_556f)); - _524 = saturate(GAMMA_TO_LINEAR(_524f)); - _561 = saturate(GAMMA_TO_LINEAR(_561f)); - _531 = saturate(GAMMA_TO_LINEAR(_531f)); - _570 = saturate(GAMMA_TO_LINEAR(_570f)); - _540 = saturate(GAMMA_TO_LINEAR(_540f)); - _575 = saturate(GAMMA_TO_LINEAR(_575f)); - -#endif // ENABLE_HDR + _509 = saturate(_509f / normalizationFactor); + _547 = saturate(_547f / normalizationFactor); + _521 = saturate(_521f / normalizationFactor); + _556 = saturate(_556f / normalizationFactor); + _524 = saturate(_524f / normalizationFactor); + _561 = saturate(_561f / normalizationFactor); + _531 = saturate(_531f / normalizationFactor); + _570 = saturate(_570f / normalizationFactor); + _540 = saturate(_540f / normalizationFactor); + _575 = saturate(_575f / normalizationFactor); + } + else + { + _509 = _509f; + _547 = _547f; + _521 = _521f; + _556 = _556f; + _524 = _524f; + _561 = _561f; + _531 = _531f; + _570 = _570f; + _540 = _540f; + _575 = _575f; + + _509 = saturate(GAMMA_TO_LINEAR(_509f)); + _547 = saturate(GAMMA_TO_LINEAR(_547f)); + _521 = saturate(GAMMA_TO_LINEAR(_521f)); + _556 = saturate(GAMMA_TO_LINEAR(_556f)); + _524 = saturate(GAMMA_TO_LINEAR(_524f)); + _561 = saturate(GAMMA_TO_LINEAR(_561f)); + _531 = saturate(GAMMA_TO_LINEAR(_531f)); + _570 = saturate(GAMMA_TO_LINEAR(_570f)); + _540 = saturate(GAMMA_TO_LINEAR(_540f)); + _575 = saturate(GAMMA_TO_LINEAR(_575f)); + } half _596 = _509.y; half _597 = _547.y; @@ -369,25 +399,35 @@ void CS(CSInput csInput) half3 _724 = (((_556 + _547 + _570 + _575) * _712) + _561) * _718; _724 = saturate(_724); -#if ENABLE_HDR - float3 _1216 = _724 * normalizationFactor; - float3 colorOut4 = (bool)HDR_BT2020 ? BT2020_To_BT709(_1216) : _1216; -#else // ENABLE_HDR - float3 colorOut4 = LINEAR_TO_GAMMA(_724); -#endif // ENABLE_HDR + float3 colorOut4; + if (HdrDllPluginConstants.DisplayMode > 0) + { + float3 _1216 = _724 * normalizationFactor; + + colorOut4 = (bool)HDR_BT2020 ? BT2020_To_BT709(_1216) : _1216; + } + else + { + colorOut4 = LINEAR_TO_GAMMA(_724); + } if ((pp.x <= _55.z) && (pp.y <= _55.w)) { half3 _783 = (((_521 + _509 + _531 + _540) * _711) + _524) * _717; _783 = saturate(_783); -#if ENABLE_HDR + + float3 colorOut3; + if (HdrDllPluginConstants.DisplayMode > 0) + { float3 _790 = _783 * normalizationFactor; - float3 colorOut3 = (bool)HDR_BT2020 ? BT2020_To_BT709(_790) : _790; -#else // ENABLE_HDR - float3 colorOut3 = LINEAR_TO_GAMMA(_783); -#endif // ENABLE_HDR + colorOut3 = (bool)HDR_BT2020 ? BT2020_To_BT709(_790) : _790; + } + else + { + colorOut3 = LINEAR_TO_GAMMA(_783); + } ColorOut[uint2(_58, pp.y)] = float4(colorOut3, 1.f); } From 72f375d00a9406058087f23731a57d31c15d0fe1 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Sat, 7 Oct 2023 15:18:27 -0400 Subject: [PATCH 103/270] fix(filmgrain): saturate(Y) on type2 --- shaders/FilmGrain/FilmGrain_ps.hlsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shaders/FilmGrain/FilmGrain_ps.hlsl b/shaders/FilmGrain/FilmGrain_ps.hlsl index 324c5e75..f510fa4d 100644 --- a/shaders/FilmGrain/FilmGrain_ps.hlsl +++ b/shaders/FilmGrain/FilmGrain_ps.hlsl @@ -94,7 +94,7 @@ float colorY = Luminance(tonemappedColor); float additiveFilmGrain = (luminanceShift * filmGrainColorAndIntensity.z * inverseLuminance); #else float additiveFilmGrain = ((randomNumber * 2.f) - 1.f) // (-1 to 1) - * (2.f* (0.5f - abs(colorY - 0.5f))) // Bias towards center (nothing if black or white) + * (2.f * (0.5f - abs(saturate(colorY) - 0.5f))) // Bias towards center (nothing if black or white) * filmGrainColorAndIntensity.z * 3.333f; #endif // FILM_GRAIN_TYPE != 2 From 2d845cfff05c4b0a5fa953daed09c679ba7f4502 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Sat, 7 Oct 2023 15:47:47 -0400 Subject: [PATCH 104/270] feat(filmgrain): enable new midtone-baised film-grain Fixes #11 --- shaders/FilmGrain/FilmGrain_ps.hlsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shaders/FilmGrain/FilmGrain_ps.hlsl b/shaders/FilmGrain/FilmGrain_ps.hlsl index f510fa4d..c3b9275d 100644 --- a/shaders/FilmGrain/FilmGrain_ps.hlsl +++ b/shaders/FilmGrain/FilmGrain_ps.hlsl @@ -5,7 +5,7 @@ #define FILM_GRAIN_TEXTURE_SIZE 1024u // 0 Vanilla, 1 New Random (no flicker/repeating), 2 New random, no raised blacks -#define FILM_GRAIN_TYPE 0 +#define FILM_GRAIN_TYPE 2 cbuffer _13_15 : register(b0, space0) { From 05325971cecfca42e0ab9e0323d60d65fe511410 Mon Sep 17 00:00:00 2001 From: Lilium Date: Sun, 8 Oct 2023 01:57:31 +0200 Subject: [PATCH 105/270] add fully described CAS input struct + add helper functions for input and output handling in CAS --- .../ContrastAdaptiveSharpening_cs.hlsl | 322 ++++++------------ shaders/structs.hlsl | 25 +- 2 files changed, 129 insertions(+), 218 deletions(-) diff --git a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl index 8cb78fc1..c5e497e5 100644 --- a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl +++ b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl @@ -54,8 +54,46 @@ struct CSInput // since blue is the least contributing in terms of luminance // the worst case is red and green at 0 and blue high enough so that the luminance is HDR_MAX_OUTPUT_NITS // TODO: use more accurate value than just the K factor from the YCbCr<->RGB transform -// TODO: normalizing by peak brightness isn't right, we need to do a quick inverse pass of the tonemapper. -static const float normalizationFactor = (HdrDllPluginConstants.HDRPeakBrightnessNits / WhiteNits_BT709) / 0.0593f; +#if HDR_BT2020 + static const float blueFactor = 0.0593f; +#else + static const float blueFactor = 0.0722f; +#endif +static const float normalizationFactor = (HdrDllPluginConstants.HDRPeakBrightnessNits / WhiteNits_BT709) / blueFactor; + + +half3 PrepareForProcessing(half3 Color) +{ + if (HdrDllPluginConstants.DisplayMode > 0) + { +#if HDR_BT2020 + Color = BT709_To_BT2020(Color); +#endif + return saturate(Color / normalizationFactor); + } + else + { + return saturate(GAMMA_TO_LINEAR(Color)); + } +} + +half3 PrepareForOutput(half3 Color) +{ + if (HdrDllPluginConstants.DisplayMode > 0) + { + Color *= normalizationFactor; +#if HDR_BT2020 + return BT2020_To_BT709(Color); +#else + return Color; +#endif + } + else + { + return LINEAR_TO_GAMMA(Color); + } +} + [RootSignature(ShaderRootSignature)] [numthreads(64, 1, 1)] @@ -79,111 +117,59 @@ void CS(CSInput csInput) ColorOut[uint2(ppx, ppy)] = ColorIn.Load(int3(uint2(ppx, ppy), 0)); #else - uint4 _55 = CASData.cas2; + uint4 _55 = CASData.rectLimits0; + + uint _58 = ((csInput.SV_GroupThreadID.x >> 1) & 7) + | (csInput.SV_GroupID.x << 4); + _58 += _55.x; - uint _58 = (((csInput.SV_GroupThreadID.x >> 1) & 7) - | (csInput.SV_GroupID.x << 4)) - + _55.x; + uint _59 = ((csInput.SV_GroupThreadID.x >> 3) & 6) + | (csInput.SV_GroupThreadID.x & 1) + | (csInput.SV_GroupID.y << 4); + _59 += _55.y; - uint2 pp = uint2(_58, (((csInput.SV_GroupThreadID.x >> 3) & 6) - | (csInput.SV_GroupThreadID.x & 1) - | (csInput.SV_GroupID.y << 4)) - + _55.y); + uint2 pp = uint2(_58, _59); - uint4 _69 = CASData.cas3; + uint4 _69 = CASData.rectLimits1; uint _70 = _69.x; uint _71 = _69.y; uint _72 = _69.z; uint _73 = _69.w; - uint _87 = max(min(pp.x, _72), _70); - uint _89 = max(min(pp.y - 1, _73), _71); - float3 _91f = ColorIn.Load(int3( _87, _89, 0)).rgb; - - uint _105 = max(min(pp.y, _73), _71); uint _107 = max(min(pp.x - 1, _72), _70); - float3 _106f = ColorIn.Load(int3(_107, _105, 0)).rgb; - float3 _109f = ColorIn.Load(int3( _87, _105, 0)).rgb; - + uint _87 = max(min(pp.x, _72), _70); uint _118 = max(min(pp.x + 1, _72), _70); - float3 _119f = ColorIn.Load(int3(_118, _105, 0)).rgb; + uint _89 = max(min(pp.y - 1, _73), _71); + uint _105 = max(min(pp.y, _73), _71); uint _127 = max(min(pp.y + 1, _73), _71); - float3 _128f = ColorIn.Load(int3( _87, _127, 0)).rgb; - - uint _138 = max(min(pp.x + 8, _72), _70); - float3 _139f = ColorIn.Load(int3(_138, _89, 0)).rgb; uint _150 = max(min(pp.x + 7, _72), _70); - float3 _151f = ColorIn.Load(int3(_150, _105, 0)).rgb; - float3 _156f = ColorIn.Load(int3(_138, _105, 0)).rgb; - + uint _138 = max(min(pp.x + 8, _72), _70); uint _167 = max(min(pp.x + 9, _72), _70); - float3 _168f = ColorIn.Load(int3(_167, _105, 0)).rgb; - float3 _173f = ColorIn.Load(int3(_138, _127, 0)).rgb; - - half3 _91; - half3 _139; - half3 _106; - half3 _151; - half3 _109; - half3 _156; - half3 _119; - half3 _168; - half3 _128; - half3 _173; - if (HdrDllPluginConstants.DisplayMode > 0) - { -#if HDR_BT2020 - _91f = BT709_To_BT2020( _91f); - _139f = BT709_To_BT2020(_139f); - _106f = BT709_To_BT2020(_106f); - _151f = BT709_To_BT2020(_151f); - _109f = BT709_To_BT2020(_109f); - _156f = BT709_To_BT2020(_156f); - _119f = BT709_To_BT2020(_119f); - _168f = BT709_To_BT2020(_168f); - _128f = BT709_To_BT2020(_128f); - _173f = BT709_To_BT2020(_173f); -#endif - - _91 = saturate( _91f / normalizationFactor); - _139 = saturate(_139f / normalizationFactor); - _106 = saturate(_106f / normalizationFactor); - _151 = saturate(_151f / normalizationFactor); - _109 = saturate(_109f / normalizationFactor); - _156 = saturate(_156f / normalizationFactor); - _119 = saturate(_119f / normalizationFactor); - _168 = saturate(_168f / normalizationFactor); - _128 = saturate(_128f / normalizationFactor); - _173 = saturate(_173f / normalizationFactor); - } - else - { - _91 = _91f; - _139 = _139f; - _106 = _106f; - _151 = _151f; - _109 = _109f; - _156 = _156f; - _119 = _119f; - _168 = _168f; - _128 = _128f; - _173 = _173f; - - _91 = saturate(GAMMA_TO_LINEAR(_91)); - _139 = saturate(GAMMA_TO_LINEAR(_139)); - _106 = saturate(GAMMA_TO_LINEAR(_106)); - _151 = saturate(GAMMA_TO_LINEAR(_151)); - _109 = saturate(GAMMA_TO_LINEAR(_109)); - _156 = saturate(GAMMA_TO_LINEAR(_156)); - _119 = saturate(GAMMA_TO_LINEAR(_119)); - _168 = saturate(GAMMA_TO_LINEAR(_168)); - _128 = saturate(GAMMA_TO_LINEAR(_128)); - _173 = saturate(GAMMA_TO_LINEAR(_173)); - } + half3 _91 = ColorIn.Load(int3( _87, _89, 0)).rgb; + half3 _106 = ColorIn.Load(int3(_107, _105, 0)).rgb; + half3 _109 = ColorIn.Load(int3( _87, _105, 0)).rgb; + half3 _119 = ColorIn.Load(int3(_118, _105, 0)).rgb; + half3 _128 = ColorIn.Load(int3( _87, _127, 0)).rgb; + half3 _139 = ColorIn.Load(int3(_138, _89, 0)).rgb; + half3 _151 = ColorIn.Load(int3(_150, _105, 0)).rgb; + half3 _156 = ColorIn.Load(int3(_138, _105, 0)).rgb; + half3 _168 = ColorIn.Load(int3(_167, _105, 0)).rgb; + half3 _173 = ColorIn.Load(int3(_138, _127, 0)).rgb; + + _91 = PrepareForProcessing( _91); + _106 = PrepareForProcessing(_106); + _109 = PrepareForProcessing(_109); + _119 = PrepareForProcessing(_119); + _128 = PrepareForProcessing(_128); + _139 = PrepareForProcessing(_139); + _151 = PrepareForProcessing(_151); + _156 = PrepareForProcessing(_156); + _168 = PrepareForProcessing(_168); + _173 = PrepareForProcessing(_173); half _193 = _91.y; half _194 = _139.y; @@ -223,7 +209,7 @@ void CS(CSInput csInput) half _981 = saturate(_303); half _992 = saturate(_304); - half hSharp = f16tof32(CASData.cas1.y & 0xFFFF); + half hSharp = f16tof32(CASData.upscalingConst1.sharpAsHalf & 0xFFFF); half _315 = hSharp * sqrt(_981); half _316 = hSharp * sqrt(_992); @@ -232,36 +218,16 @@ void CS(CSInput csInput) half _323 = 1.h / ((_316 * 4.h) + 1.h); half3 _329 = (((_151 + _139 + _168 + _173) * _316) + _156) * _323; - _329 = saturate(_329); - - float3 colorOut2; - if (HdrDllPluginConstants.DisplayMode > 0) - { - float3 _1003 = _329 * normalizationFactor; + half3 colorOut2 = saturate(_329); - colorOut2 = (bool)HDR_BT2020 ? BT2020_To_BT709(_1003) : _1003; - } - else - { - colorOut2 = LINEAR_TO_GAMMA(_329); - } + colorOut2 = PrepareForOutput(colorOut2); if ((pp.x <= _55.z) && (pp.y <= _55.w)) { half3 _388 = (((_106 + _91 + _119 + _128) * _315) + _109) * _322; - _388 = saturate(_388); - - float3 colorOut1; - if (HdrDllPluginConstants.DisplayMode > 0) - { - float3 _396 = _388 * normalizationFactor; + half3 colorOut1 = saturate(_388); - colorOut1 = (bool)HDR_BT2020 ? BT2020_To_BT709(_396) : _396; - } - else - { - colorOut1 = LINEAR_TO_GAMMA(_388); - } + colorOut1 = PrepareForOutput(colorOut1); ColorOut[pp] = float4(colorOut1, 1.f); } @@ -276,82 +242,30 @@ void CS(CSInput csInput) pp.y += 8; uint _507 = max(min(pp.y - 1, _73), _71); - float3 _509f = ColorIn.Load(int3( _87, _507, 0)).rgb; - - uint _520 = max(min(pp.y, _73), _71); - float3 _521f = ColorIn.Load(int3(_107, _520, 0)).rgb; - float3 _524f = ColorIn.Load(int3( _87, _520, 0)).rgb; - float3 _531f = ColorIn.Load(int3(_118, _520, 0)).rgb; - + uint _520 = max(min(pp.y, _73), _71); uint _539 = max(min(pp.y + 1, _73), _71); - float3 _540f = ColorIn.Load(int3( _87, _539, 0)).rgb; - float3 _547f = ColorIn.Load(int3(_138, _507, 0)).rgb; - float3 _556f = ColorIn.Load(int3(_150, _520, 0)).rgb; - float3 _561f = ColorIn.Load(int3(_138, _520, 0)).rgb; - float3 _570f = ColorIn.Load(int3(_167, _520, 0)).rgb; - float3 _575f = ColorIn.Load(int3(_138, _539, 0)).rgb; - - half3 _509; - half3 _547; - half3 _521; - half3 _556; - half3 _524; - half3 _561; - half3 _531; - half3 _570; - half3 _540; - half3 _575; - if (HdrDllPluginConstants.DisplayMode > 0) - { -#if HDR_BT2020 - _509f = BT709_To_BT2020(_509f); - _547f = BT709_To_BT2020(_547f); - _521f = BT709_To_BT2020(_521f); - _556f = BT709_To_BT2020(_556f); - _524f = BT709_To_BT2020(_524f); - _561f = BT709_To_BT2020(_561f); - _531f = BT709_To_BT2020(_531f); - _570f = BT709_To_BT2020(_570f); - _540f = BT709_To_BT2020(_540f); - _575f = BT709_To_BT2020(_575f); -#endif - - _509 = saturate(_509f / normalizationFactor); - _547 = saturate(_547f / normalizationFactor); - _521 = saturate(_521f / normalizationFactor); - _556 = saturate(_556f / normalizationFactor); - _524 = saturate(_524f / normalizationFactor); - _561 = saturate(_561f / normalizationFactor); - _531 = saturate(_531f / normalizationFactor); - _570 = saturate(_570f / normalizationFactor); - _540 = saturate(_540f / normalizationFactor); - _575 = saturate(_575f / normalizationFactor); - } - else - { - _509 = _509f; - _547 = _547f; - _521 = _521f; - _556 = _556f; - _524 = _524f; - _561 = _561f; - _531 = _531f; - _570 = _570f; - _540 = _540f; - _575 = _575f; - - _509 = saturate(GAMMA_TO_LINEAR(_509f)); - _547 = saturate(GAMMA_TO_LINEAR(_547f)); - _521 = saturate(GAMMA_TO_LINEAR(_521f)); - _556 = saturate(GAMMA_TO_LINEAR(_556f)); - _524 = saturate(GAMMA_TO_LINEAR(_524f)); - _561 = saturate(GAMMA_TO_LINEAR(_561f)); - _531 = saturate(GAMMA_TO_LINEAR(_531f)); - _570 = saturate(GAMMA_TO_LINEAR(_570f)); - _540 = saturate(GAMMA_TO_LINEAR(_540f)); - _575 = saturate(GAMMA_TO_LINEAR(_575f)); - } + half3 _509 = ColorIn.Load(int3( _87, _507, 0)).rgb; + half3 _521 = ColorIn.Load(int3(_107, _520, 0)).rgb; + half3 _524 = ColorIn.Load(int3( _87, _520, 0)).rgb; + half3 _531 = ColorIn.Load(int3(_118, _520, 0)).rgb; + half3 _540 = ColorIn.Load(int3( _87, _539, 0)).rgb; + half3 _547 = ColorIn.Load(int3(_138, _507, 0)).rgb; + half3 _556 = ColorIn.Load(int3(_150, _520, 0)).rgb; + half3 _561 = ColorIn.Load(int3(_138, _520, 0)).rgb; + half3 _570 = ColorIn.Load(int3(_167, _520, 0)).rgb; + half3 _575 = ColorIn.Load(int3(_138, _539, 0)).rgb; + + _509 = PrepareForProcessing(_509); + _547 = PrepareForProcessing(_547); + _521 = PrepareForProcessing(_521); + _556 = PrepareForProcessing(_556); + _524 = PrepareForProcessing(_524); + _561 = PrepareForProcessing(_561); + _531 = PrepareForProcessing(_531); + _570 = PrepareForProcessing(_570); + _540 = PrepareForProcessing(_540); + _575 = PrepareForProcessing(_575); half _596 = _509.y; half _597 = _547.y; @@ -398,36 +312,16 @@ void CS(CSInput csInput) half _718 = 1.h / ((_712 * 4.h) + 1.h); half3 _724 = (((_556 + _547 + _570 + _575) * _712) + _561) * _718; - _724 = saturate(_724); + half3 colorOut4 = saturate(_724); - float3 colorOut4; - if (HdrDllPluginConstants.DisplayMode > 0) - { - float3 _1216 = _724 * normalizationFactor; - - colorOut4 = (bool)HDR_BT2020 ? BT2020_To_BT709(_1216) : _1216; - } - else - { - colorOut4 = LINEAR_TO_GAMMA(_724); - } + colorOut4 = PrepareForOutput(colorOut4); if ((pp.x <= _55.z) && (pp.y <= _55.w)) { half3 _783 = (((_521 + _509 + _531 + _540) * _711) + _524) * _717; - _783 = saturate(_783); - - float3 colorOut3; - if (HdrDllPluginConstants.DisplayMode > 0) - { - float3 _790 = _783 * normalizationFactor; - - colorOut3 = (bool)HDR_BT2020 ? BT2020_To_BT709(_790) : _790; - } - else - { - colorOut3 = LINEAR_TO_GAMMA(_783); - } + half3 colorOut3 = saturate(_783); + + colorOut3 = PrepareForOutput(colorOut3); ColorOut[uint2(_58, pp.y)] = float4(colorOut3, 1.f); } diff --git a/shaders/structs.hlsl b/shaders/structs.hlsl index 968e881c..9b73f0f3 100644 --- a/shaders/structs.hlsl +++ b/shaders/structs.hlsl @@ -186,11 +186,28 @@ struct PushConstantWrapper_ColorGradingMerge //ColorGradingMerge end //ContrastAdaptiveSharpening start + +struct CASConst0 +{ + float rcpScalingFactorX; + float rcpScalingFactorY; + float otherScalingFactorX; //0.5 * inputSizeInPixelsX * (1.0 / outputSizeInPixelsX) - 0.5 + float otherScalingFactorY; //0.5 * inputSizeInPixelsY * (1.0 / outputSizeInPixelsY) - 0.5 +}; + +struct CASConst1 +{ + float sharp; + uint sharpAsHalf; //needs to be unpacked + float rcpScalingFactorXTimes8; + int unused; +}; + struct ContrastAdaptiveSharpeningData { - uint4 cas0; - uint4 cas1; - uint4 cas2; - uint4 cas3; + CASConst0 upscalingConst0; + CASConst1 upscalingConst1; + uint4 rectLimits0; + uint4 rectLimits1; }; //ContrastAdaptiveSharpening end From abe6929570f515afe561affebfe89aa60095cf0a Mon Sep 17 00:00:00 2001 From: Lilium Date: Sat, 7 Oct 2023 23:36:17 +0200 Subject: [PATCH 106/270] commit unchanged CAS upscaling for git diff comparisons --- .../ContrastAdaptiveSharpening_cs.hlsl | 477 ++++++++++++++++++ 1 file changed, 477 insertions(+) diff --git a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl index c5e497e5..cf69fa88 100644 --- a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl +++ b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl @@ -2,6 +2,9 @@ #include "../color.hlsl" #include "RootSignature.hlsl" +#define USE_PACKED_MATH +#define USE_UPSCALING + // this is CAS upscaling in FP16 with FFX_CAS_USE_PRECISE_MATH [100FF94] // other variants are also available but seemingly unused, which are: // - CAS upscaling in FP32 [200FF94] @@ -99,6 +102,479 @@ half3 PrepareForOutput(half3 Color) [numthreads(64, 1, 1)] void CS(CSInput csInput) { + +#if defined(USE_PACKED_MATH) \ + && defined(USE_UPSCALING) + + uint4 _55 = asuint(_23_m0[2u]); + uint _58 = _55.x + (((gl_LocalInvocationID.x >> 1u) & 7u) | (gl_WorkGroupID.x << 4u)); + uint _59 = ((((gl_LocalInvocationID.x >> 3u) & 6u) | (gl_LocalInvocationID.x & 1u)) | (gl_WorkGroupID.y << 4u)) + _55.y; + uint4 _62 = asuint(_23_m0[0u]); + uint4 _69 = asuint(_23_m0[1u]); + half _76 = half(_18_m0[1u].w); + float _80 = asfloat(_62.y); + float _84 = asfloat(_62.w); + float _85 = (float(_58) * asfloat(_62.x)) + asfloat(_62.z); + float _86 = (float(_59) * _80) + _84; + float _88 = floor(_85); + float _89 = floor(_86); + half _91 = half(_85 - _88); + half _93 = half(_86 - _89); + uint16_t _95 = (int16_t(_88)); + uint16_t _96 = (int16_t(_89)); + uint4 _101 = asuint(_23_m0[3u]); + uint _102 = _101.x; + uint _103 = _101.y; + uint _104 = _101.z; + uint _105 = _101.w; + uint _106 = uint(_95); + uint _114 = uint(int(max(min(_106, _104), _102) << 16u) >> int(16u)); + uint _116 = uint(int(max(min(uint((_96 + 65535u)), _105), _103) << 16u) >> int(16u)); + float4 _118 = _8.Load(int3(uint2(_114, _116), 0u)); + uint _124 = uint((_95 + 65535u)); + uint _131 = uint(int(max(min(_124, _104), _102) << 16u) >> int(16u)); + uint _133 = uint(int(max(min(uint(_96), _105), _103) << 16u) >> int(16u)); + float4 _134 = _8.Load(int3(uint2(_131, _133), 0u)); + float4 _138 = _8.Load(int3(uint2(_114, _133), 0u)); + uint _144 = uint((_95 + 1u)); + uint _148 = uint(int(max(min(_144, _104), _102) << 16u) >> int(16u)); + float4 _149 = _8.Load(int3(uint2(_148, _116), 0u)); + float4 _153 = _8.Load(int3(uint2(_148, _133), 0u)); + uint _159 = uint((_95 + 2u)); + uint _163 = uint(int(max(min(_159, _104), _102) << 16u) >> int(16u)); + float4 _164 = _8.Load(int3(uint2(_163, _133), 0u)); + uint _173 = uint(int(max(min(uint((_96 + 1u)), _105), _103) << 16u) >> int(16u)); + float4 _174 = _8.Load(int3(uint2(_131, _173), 0u)); + float4 _178 = _8.Load(int3(uint2(_114, _173), 0u)); + uint _187 = uint(int(max(min(uint((_96 + 2u)), _105), _103) << 16u) >> int(16u)); + float4 _188 = _8.Load(int3(uint2(_114, _187), 0u)); + float4 _192 = _8.Load(int3(uint2(_148, _173), 0u)); + float4 _196 = _8.Load(int3(uint2(_163, _173), 0u)); + float4 _200 = _8.Load(int3(uint2(_148, _187), 0u)); + float _205 = _85 + asfloat(_69.z); + float _206 = floor(_205); + half _208 = half(_205 - _206); + uint16_t _209 = (int16_t(_206)); + uint _210 = uint(_209); + uint _214 = uint(int(max(min(_210, _104), _102) << 16u) >> int(16u)); + float4 _215 = _8.Load(int3(uint2(_214, _116), 0u)); + uint _224 = uint((_209 + 65535u)); + uint _228 = uint(int(max(min(_224, _104), _102) << 16u) >> int(16u)); + float4 _229 = _8.Load(int3(uint2(_228, _133), 0u)); + float4 _237 = _8.Load(int3(uint2(_214, _133), 0u)); + uint _246 = uint((_209 + 1u)); + uint _250 = uint(int(max(min(_246, _104), _102) << 16u) >> int(16u)); + float4 _251 = _8.Load(int3(uint2(_250, _116), 0u)); + float4 _259 = _8.Load(int3(uint2(_250, _133), 0u)); + uint _268 = uint((_209 + 2u)); + uint _272 = uint(int(max(min(_268, _104), _102) << 16u) >> int(16u)); + float4 _273 = _8.Load(int3(uint2(_272, _133), 0u)); + float4 _281 = _8.Load(int3(uint2(_228, _173), 0u)); + float4 _289 = _8.Load(int3(uint2(_214, _173), 0u)); + float4 _297 = _8.Load(int3(uint2(_214, _187), 0u)); + float4 _305 = _8.Load(int3(uint2(_250, _173), 0u)); + float4 _313 = _8.Load(int3(uint2(_272, _173), 0u)); + float4 _321 = _8.Load(int3(uint2(_250, _187), 0u)); + half _344 = exp2(log2((half(_118.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _345 = exp2(log2((half(_215.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _364 = exp2(log2((half(_149.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _365 = exp2(log2((half(_251.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _384 = exp2(log2((half(_134.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _385 = exp2(log2((half(_229.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _404 = exp2(log2((half(_138.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _405 = exp2(log2((half(_237.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _424 = exp2(log2((half(_153.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _425 = exp2(log2((half(_259.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _444 = exp2(log2((half(_164.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _445 = exp2(log2((half(_273.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _464 = exp2(log2((half(_174.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _465 = exp2(log2((half(_281.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _484 = exp2(log2((half(_178.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _485 = exp2(log2((half(_289.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _504 = exp2(log2((half(_192.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _505 = exp2(log2((half(_305.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _524 = exp2(log2((half(_196.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _525 = exp2(log2((half(_313.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _544 = exp2(log2((half(_188.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _545 = exp2(log2((half(_297.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _564 = exp2(log2((half(_200.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _565 = exp2(log2((half(_321.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); + half _571 = isnan(_404) ? _384 : (isnan(_384) ? _404 : min(_384, _404)); + half _572 = isnan(_405) ? _385 : (isnan(_385) ? _405 : min(_385, _405)); + half _573 = isnan(_571) ? _344 : (isnan(_344) ? _571 : min(_344, _571)); + half _574 = isnan(_572) ? _345 : (isnan(_345) ? _572 : min(_345, _572)); + half _575 = isnan(_484) ? _424 : (isnan(_424) ? _484 : min(_424, _484)); + half _576 = isnan(_485) ? _425 : (isnan(_425) ? _485 : min(_425, _485)); + half _577 = isnan(_575) ? _573 : (isnan(_573) ? _575 : min(_573, _575)); + half _578 = isnan(_576) ? _574 : (isnan(_574) ? _576 : min(_574, _576)); + half _579 = isnan(_404) ? _384 : (isnan(_384) ? _404 : max(_384, _404)); + half _580 = isnan(_405) ? _385 : (isnan(_385) ? _405 : max(_385, _405)); + half _581 = isnan(_579) ? _344 : (isnan(_344) ? _579 : max(_344, _579)); + half _582 = isnan(_580) ? _345 : (isnan(_345) ? _580 : max(_345, _580)); + half _583 = isnan(_484) ? _424 : (isnan(_424) ? _484 : max(_424, _484)); + half _584 = isnan(_485) ? _425 : (isnan(_425) ? _485 : max(_425, _485)); + half _585 = isnan(_583) ? _581 : (isnan(_581) ? _583 : max(_581, _583)); + half _586 = isnan(_584) ? _582 : (isnan(_582) ? _584 : max(_582, _584)); + half _587 = isnan(_424) ? _404 : (isnan(_404) ? _424 : min(_404, _424)); + half _588 = isnan(_425) ? _405 : (isnan(_405) ? _425 : min(_405, _425)); + half _589 = isnan(_587) ? _364 : (isnan(_364) ? _587 : min(_364, _587)); + half _590 = isnan(_588) ? _365 : (isnan(_365) ? _588 : min(_365, _588)); + half _591 = isnan(_504) ? _444 : (isnan(_444) ? _504 : min(_444, _504)); + half _592 = isnan(_505) ? _445 : (isnan(_445) ? _505 : min(_445, _505)); + half _593 = isnan(_591) ? _589 : (isnan(_589) ? _591 : min(_589, _591)); + half _594 = isnan(_592) ? _590 : (isnan(_590) ? _592 : min(_590, _592)); + half _595 = isnan(_424) ? _404 : (isnan(_404) ? _424 : max(_404, _424)); + half _596 = isnan(_425) ? _405 : (isnan(_405) ? _425 : max(_405, _425)); + half _597 = isnan(_595) ? _364 : (isnan(_364) ? _595 : max(_364, _595)); + half _598 = isnan(_596) ? _365 : (isnan(_365) ? _596 : max(_365, _596)); + half _599 = isnan(_504) ? _444 : (isnan(_444) ? _504 : max(_444, _504)); + half _600 = isnan(_505) ? _445 : (isnan(_445) ? _505 : max(_445, _505)); + half _601 = isnan(_599) ? _597 : (isnan(_597) ? _599 : max(_597, _599)); + half _602 = isnan(_600) ? _598 : (isnan(_598) ? _600 : max(_598, _600)); + half _603 = isnan(_484) ? _464 : (isnan(_464) ? _484 : min(_464, _484)); + half _604 = isnan(_485) ? _465 : (isnan(_465) ? _485 : min(_465, _485)); + half _605 = isnan(_603) ? _404 : (isnan(_404) ? _603 : min(_404, _603)); + half _606 = isnan(_604) ? _405 : (isnan(_405) ? _604 : min(_405, _604)); + half _607 = isnan(_544) ? _504 : (isnan(_504) ? _544 : min(_504, _544)); + half _608 = isnan(_545) ? _505 : (isnan(_505) ? _545 : min(_505, _545)); + half _609 = isnan(_607) ? _605 : (isnan(_605) ? _607 : min(_605, _607)); + half _610 = isnan(_608) ? _606 : (isnan(_606) ? _608 : min(_606, _608)); + half _611 = isnan(_484) ? _464 : (isnan(_464) ? _484 : max(_464, _484)); + half _612 = isnan(_485) ? _465 : (isnan(_465) ? _485 : max(_465, _485)); + half _613 = isnan(_611) ? _404 : (isnan(_404) ? _611 : max(_404, _611)); + half _614 = isnan(_612) ? _405 : (isnan(_405) ? _612 : max(_405, _612)); + half _615 = isnan(_544) ? _504 : (isnan(_504) ? _544 : max(_504, _544)); + half _616 = isnan(_545) ? _505 : (isnan(_505) ? _545 : max(_505, _545)); + half _617 = isnan(_615) ? _613 : (isnan(_613) ? _615 : max(_613, _615)); + half _618 = isnan(_616) ? _614 : (isnan(_614) ? _616 : max(_614, _616)); + half _619 = isnan(_504) ? _484 : (isnan(_484) ? _504 : min(_484, _504)); + half _620 = isnan(_505) ? _485 : (isnan(_485) ? _505 : min(_485, _505)); + half _621 = isnan(_619) ? _424 : (isnan(_424) ? _619 : min(_424, _619)); + half _622 = isnan(_620) ? _425 : (isnan(_425) ? _620 : min(_425, _620)); + half _623 = isnan(_564) ? _524 : (isnan(_524) ? _564 : min(_524, _564)); + half _624 = isnan(_565) ? _525 : (isnan(_525) ? _565 : min(_525, _565)); + half _625 = isnan(_623) ? _621 : (isnan(_621) ? _623 : min(_621, _623)); + half _626 = isnan(_624) ? _622 : (isnan(_622) ? _624 : min(_622, _624)); + half _627 = isnan(_504) ? _484 : (isnan(_484) ? _504 : max(_484, _504)); + half _628 = isnan(_505) ? _485 : (isnan(_485) ? _505 : max(_485, _505)); + half _629 = isnan(_627) ? _424 : (isnan(_424) ? _627 : max(_424, _627)); + half _630 = isnan(_628) ? _425 : (isnan(_425) ? _628 : max(_425, _628)); + half _631 = isnan(_564) ? _524 : (isnan(_524) ? _564 : max(_524, _564)); + half _632 = isnan(_565) ? _525 : (isnan(_525) ? _565 : max(_525, _565)); + half _633 = isnan(_631) ? _629 : (isnan(_629) ? _631 : max(_629, _631)); + half _634 = isnan(_632) ? _630 : (isnan(_630) ? _632 : max(_630, _632)); + half _644 = half(1.0) - _585; + half _645 = half(1.0) - _586; + half _648 = (isnan(_644) ? _577 : (isnan(_577) ? _644 : min(_577, _644))) * (half(1.0) / _585); + half _649 = (isnan(_645) ? _578 : (isnan(_578) ? _645 : min(_578, _645))) * (half(1.0) / _586); + half _2515 = isnan(half(0.0)) ? _648 : (isnan(_648) ? half(0.0) : max(_648, half(0.0))); + half _2526 = isnan(half(0.0)) ? _649 : (isnan(_649) ? half(0.0) : max(_649, half(0.0))); + half _653 = half(1.0) - _601; + half _654 = half(1.0) - _602; + half _657 = (isnan(_653) ? _593 : (isnan(_593) ? _653 : min(_593, _653))) * (half(1.0) / _601); + half _658 = (isnan(_654) ? _594 : (isnan(_594) ? _654 : min(_594, _654))) * (half(1.0) / _602); + half _2547 = isnan(half(0.0)) ? _657 : (isnan(_657) ? half(0.0) : max(_657, half(0.0))); + half _2558 = isnan(half(0.0)) ? _658 : (isnan(_658) ? half(0.0) : max(_658, half(0.0))); + half _661 = half(1.0) - _617; + half _662 = half(1.0) - _618; + half _665 = (isnan(_661) ? _609 : (isnan(_609) ? _661 : min(_609, _661))) * (half(1.0) / _617); + half _666 = (isnan(_662) ? _610 : (isnan(_610) ? _662 : min(_610, _662))) * (half(1.0) / _618); + half _2579 = isnan(half(0.0)) ? _665 : (isnan(_665) ? half(0.0) : max(_665, half(0.0))); + half _2590 = isnan(half(0.0)) ? _666 : (isnan(_666) ? half(0.0) : max(_666, half(0.0))); + half _669 = half(1.0) - _633; + half _670 = half(1.0) - _634; + half _673 = (isnan(_669) ? _625 : (isnan(_625) ? _669 : min(_625, _669))) * (half(1.0) / _633); + half _674 = (isnan(_670) ? _626 : (isnan(_626) ? _670 : min(_626, _670))) * (half(1.0) / _634); + half _2611 = isnan(half(0.0)) ? _673 : (isnan(_673) ? half(0.0) : max(_673, half(0.0))); + half _2622 = isnan(half(0.0)) ? _674 : (isnan(_674) ? half(0.0) : max(_674, half(0.0))); + half _690 = half(spvUnpackHalf2x16(_69.y & 65535u).x); + half _699 = half(1.0) - _91; + half _700 = half(1.0) - _208; + half _701 = half(1.0) - _93; + half _717 = (_699 * _701) * (half(1.0) / ((half(0.03125) - _577) + _585)); + half _718 = (_700 * _701) * (half(1.0) / ((half(0.03125) - _578) + _586)); + half _725 = (_701 * _91) * (half(1.0) / ((half(0.03125) - _593) + _601)); + half _726 = (_208 * _701) * (half(1.0) / ((half(0.03125) - _594) + _602)); + half _733 = (_699 * _93) * (half(1.0) / ((half(0.03125) - _609) + _617)); + half _734 = (_700 * _93) * (half(1.0) / ((half(0.03125) - _610) + _618)); + half _741 = (_91 * _93) * (half(1.0) / ((half(0.03125) - _625) + _633)); + half _742 = (_208 * _93) * (half(1.0) / ((half(0.03125) - _626) + _634)); + half _743 = (_690 * sqrt(isnan(half(1.0)) ? _2515 : (isnan(_2515) ? half(1.0) : min(_2515, half(1.0))))) * _717; + half _744 = _718 * (_690 * sqrt(isnan(half(1.0)) ? _2526 : (isnan(_2526) ? half(1.0) : min(_2526, half(1.0))))); + half _745 = _725 * (_690 * sqrt(isnan(half(1.0)) ? _2547 : (isnan(_2547) ? half(1.0) : min(_2547, half(1.0))))); + half _746 = _726 * (_690 * sqrt(isnan(half(1.0)) ? _2558 : (isnan(_2558) ? half(1.0) : min(_2558, half(1.0))))); + half _747 = _733 * (_690 * sqrt(isnan(half(1.0)) ? _2579 : (isnan(_2579) ? half(1.0) : min(_2579, half(1.0))))); + half _748 = _734 * (_690 * sqrt(isnan(half(1.0)) ? _2590 : (isnan(_2590) ? half(1.0) : min(_2590, half(1.0))))); + half _750 = (_745 + _717) + _747; + half _752 = (_746 + _718) + _748; + half _753 = _741 * (_690 * sqrt(isnan(half(1.0)) ? _2611 : (isnan(_2611) ? half(1.0) : min(_2611, half(1.0))))); + half _754 = _742 * (_690 * sqrt(isnan(half(1.0)) ? _2622 : (isnan(_2622) ? half(1.0) : min(_2622, half(1.0))))); + half _756 = (_725 + _743) + _753; + half _758 = (_726 + _744) + _754; + half _760 = (_733 + _743) + _753; + half _762 = (_734 + _744) + _754; + half _765 = (_747 + _745) + _741; + half _766 = (_748 + _746) + _742; + half _784 = half(1.0) / ((((_765 + _750) + ((((_745 + _743) + _747) + _753) * half(2.0))) + _756) + _760); + half _785 = half(1.0) / ((((_766 + _752) + ((((_746 + _744) + _748) + _754) * half(2.0))) + _758) + _762); + half _803 = ((((((_752 * exp2(_76 * log2((half(_237.x) + half(0.05499267578125)) * half(0.9482421875)))) + (_744 * (exp2(_76 * log2((half(_229.x) + half(0.05499267578125)) * half(0.9482421875))) + exp2(_76 * log2((half(_215.x) + half(0.05499267578125)) * half(0.9482421875)))))) + (_766 * exp2(_76 * log2((half(_305.x) + half(0.05499267578125)) * half(0.9482421875))))) + (_758 * exp2(_76 * log2((half(_259.x) + half(0.05499267578125)) * half(0.9482421875))))) + (_762 * exp2(_76 * log2((half(_289.x) + half(0.05499267578125)) * half(0.9482421875))))) + (_754 * (exp2(_76 * log2((half(_321.x) + half(0.05499267578125)) * half(0.9482421875))) + exp2(_76 * log2((half(_313.x) + half(0.05499267578125)) * half(0.9482421875)))))) + (_748 * (exp2(_76 * log2((half(_297.x) + half(0.05499267578125)) * half(0.9482421875))) + exp2(_76 * log2((half(_281.x) + half(0.05499267578125)) * half(0.9482421875))))); + half _805 = (_803 + (_746 * (exp2(_76 * log2((half(_273.x) + half(0.05499267578125)) * half(0.9482421875))) + exp2(_76 * log2((half(_251.x) + half(0.05499267578125)) * half(0.9482421875)))))) * _785; + half _2633 = isnan(half(0.0)) ? _805 : (isnan(_805) ? half(0.0) : max(_805, half(0.0))); + half _826 = ((((((((_752 * _405) + (_744 * (_385 + _345))) + (_766 * _505)) + (_758 * _425)) + (_762 * _485)) + (_754 * (_565 + _525))) + (_748 * (_545 + _465))) + (_746 * (_445 + _365))) * _785; + half _2644 = isnan(half(0.0)) ? _826 : (isnan(_826) ? half(0.0) : max(_826, half(0.0))); + half _845 = ((((((_752 * exp2(log2((half(_237.z) + half(0.05499267578125)) * half(0.9482421875)) * _76)) + (_744 * (exp2(log2((half(_229.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_215.z) + half(0.05499267578125)) * half(0.9482421875)) * _76)))) + (_766 * exp2(log2((half(_305.z) + half(0.05499267578125)) * half(0.9482421875)) * _76))) + (_758 * exp2(log2((half(_259.z) + half(0.05499267578125)) * half(0.9482421875)) * _76))) + (_762 * exp2(log2((half(_289.z) + half(0.05499267578125)) * half(0.9482421875)) * _76))) + (_754 * (exp2(log2((half(_321.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_313.z) + half(0.05499267578125)) * half(0.9482421875)) * _76)))) + (_748 * (exp2(log2((half(_297.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_281.z) + half(0.05499267578125)) * half(0.9482421875)) * _76))); + half _847 = (_845 + (_746 * (exp2(log2((half(_273.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_251.z) + half(0.05499267578125)) * half(0.9482421875)) * _76)))) * _785; + half _2655 = isnan(half(0.0)) ? _847 : (isnan(_847) ? half(0.0) : max(_847, half(0.0))); + half _849 = isnan(half(0.00100040435791015625)) ? _76 : (isnan(_76) ? half(0.00100040435791015625) : max(_76, half(0.00100040435791015625))); + uint4 _853 = asuint(_23_m0[2u]); + if ((_58 <= _853.z) && (_59 <= _853.w)) + { + half _961 = ((((((exp2(log2((half(_138.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) * _750) + ((exp2(log2((half(_134.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_118.z) + half(0.05499267578125)) * half(0.9482421875)) * _76)) * _743)) + (exp2(log2((half(_153.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) * _756)) + (exp2(log2((half(_178.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) * _760)) + (exp2(log2((half(_192.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) * _765)) + ((exp2(log2((half(_200.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_196.z) + half(0.05499267578125)) * half(0.9482421875)) * _76)) * _753)) + ((exp2(log2((half(_188.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_174.z) + half(0.05499267578125)) * half(0.9482421875)) * _76)) * _747); + half _963 = (_961 + ((exp2(log2((half(_164.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_149.z) + half(0.05499267578125)) * half(0.9482421875)) * _76)) * _745)) * _784; + half _2671 = isnan(half(0.0)) ? _963 : (isnan(_963) ? half(0.0) : max(_963, half(0.0))); + half _966 = half(1.0) / _849; + half _971 = (exp2(_966 * log2(isnan(half(1.0)) ? _2671 : (isnan(_2671) ? half(1.0) : min(_2671, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); + half _994 = ((((((((_750 * _404) + (_743 * (_384 + _344))) + (_765 * _504)) + (_756 * _424)) + (_760 * _484)) + (_753 * (_564 + _524))) + (_747 * (_544 + _464))) + (_745 * (_444 + _364))) * _784; + half _2687 = isnan(half(0.0)) ? _994 : (isnan(_994) ? half(0.0) : max(_994, half(0.0))); + half _1000 = (exp2(_966 * log2(isnan(half(1.0)) ? _2687 : (isnan(_2687) ? half(1.0) : min(_2687, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); + half _1104 = ((((((exp2(log2((half(_138.x) + half(0.05499267578125)) * half(0.9482421875)) * _76) * _750) + ((exp2(log2((half(_134.x) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_118.x) + half(0.05499267578125)) * half(0.9482421875)) * _76)) * _743)) + (exp2(log2((half(_153.x) + half(0.05499267578125)) * half(0.9482421875)) * _76) * _756)) + (exp2(log2((half(_178.x) + half(0.05499267578125)) * half(0.9482421875)) * _76) * _760)) + (exp2(log2((half(_192.x) + half(0.05499267578125)) * half(0.9482421875)) * _76) * _765)) + ((exp2(log2((half(_200.x) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_196.x) + half(0.05499267578125)) * half(0.9482421875)) * _76)) * _753)) + ((exp2(log2((half(_188.x) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_174.x) + half(0.05499267578125)) * half(0.9482421875)) * _76)) * _747); + half _1106 = (_1104 + ((exp2(log2((half(_164.x) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_149.x) + half(0.05499267578125)) * half(0.9482421875)) * _76)) * _745)) * _784; + half _2703 = isnan(half(0.0)) ? _1106 : (isnan(_1106) ? half(0.0) : max(_1106, half(0.0))); + half _1112 = (exp2(_966 * log2(isnan(half(1.0)) ? _2703 : (isnan(_2703) ? half(1.0) : min(_2703, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); + _11[uint2(_58, _59)] = float4(float(isnan(half(0.0)) ? _1112 : (isnan(_1112) ? half(0.0) : max(_1112, half(0.0)))), float(isnan(half(0.0)) ? _1000 : (isnan(_1000) ? half(0.0) : max(_1000, half(0.0)))), float(isnan(half(0.0)) ? _971 : (isnan(_971) ? half(0.0) : max(_971, half(0.0)))), 1.0f); + } + uint _1119 = _58 + 8u; + uint4 _1122 = asuint(_23_m0[2u]); + if ((_1119 <= _1122.z) && (_59 <= _1122.w)) + { + half _1129 = half(1.0) / _849; + half _1133 = (exp2(_1129 * log2(isnan(half(1.0)) ? _2655 : (isnan(_2655) ? half(1.0) : min(_2655, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); + half _1140 = (exp2(_1129 * log2(isnan(half(1.0)) ? _2644 : (isnan(_2644) ? half(1.0) : min(_2644, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); + half _1147 = (exp2(_1129 * log2(isnan(half(1.0)) ? _2633 : (isnan(_2633) ? half(1.0) : min(_2633, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); + _11[uint2(_1119, _59)] = float4(float(isnan(half(0.0)) ? _1147 : (isnan(_1147) ? half(0.0) : max(_1147, half(0.0)))), float(isnan(half(0.0)) ? _1140 : (isnan(_1140) ? half(0.0) : max(_1140, half(0.0)))), float(isnan(half(0.0)) ? _1133 : (isnan(_1133) ? half(0.0) : max(_1133, half(0.0)))), 1.0f); + } + uint _1153 = _59 + 8u; + float _1156 = (float(_1153) * _80) + _84; + float _1157 = floor(_1156); + half _1159 = half(_1156 - _1157); + uint16_t _1160 = (int16_t(_1157)); + uint4 _1164 = asuint(_23_m0[3u]); + uint _1165 = _1164.x; + uint _1166 = _1164.y; + uint _1167 = _1164.z; + uint _1168 = _1164.w; + uint _1175 = uint(int(max(min(_106, _1167), _1165) << 16u) >> int(16u)); + uint _1177 = uint(int(max(min(uint((_1160 + 65535u)), _1168), _1166) << 16u) >> int(16u)); + float4 _1179 = _8.Load(int3(uint2(_1175, _1177), 0u)); + uint _1189 = uint(int(max(min(_124, _1167), _1165) << 16u) >> int(16u)); + uint _1191 = uint(int(max(min(uint(_1160), _1168), _1166) << 16u) >> int(16u)); + float4 _1192 = _8.Load(int3(uint2(_1189, _1191), 0u)); + float4 _1196 = _8.Load(int3(uint2(_1175, _1191), 0u)); + uint _1203 = uint(int(max(min(_144, _1167), _1165) << 16u) >> int(16u)); + float4 _1204 = _8.Load(int3(uint2(_1203, _1177), 0u)); + float4 _1208 = _8.Load(int3(uint2(_1203, _1191), 0u)); + uint _1215 = uint(int(max(min(_159, _1167), _1165) << 16u) >> int(16u)); + float4 _1216 = _8.Load(int3(uint2(_1215, _1191), 0u)); + uint _1225 = uint(int(max(min(uint((_1160 + 1u)), _1168), _1166) << 16u) >> int(16u)); + float4 _1226 = _8.Load(int3(uint2(_1189, _1225), 0u)); + float4 _1230 = _8.Load(int3(uint2(_1175, _1225), 0u)); + uint _1239 = uint(int(max(min(uint((_1160 + 2u)), _1168), _1166) << 16u) >> int(16u)); + float4 _1240 = _8.Load(int3(uint2(_1175, _1239), 0u)); + float4 _1244 = _8.Load(int3(uint2(_1203, _1225), 0u)); + float4 _1248 = _8.Load(int3(uint2(_1215, _1225), 0u)); + float4 _1252 = _8.Load(int3(uint2(_1203, _1239), 0u)); + uint _1259 = uint(int(max(min(_210, _1167), _1165) << 16u) >> int(16u)); + float4 _1260 = _8.Load(int3(uint2(_1259, _1177), 0u)); + uint _1271 = uint(int(max(min(_224, _1167), _1165) << 16u) >> int(16u)); + float4 _1272 = _8.Load(int3(uint2(_1271, _1191), 0u)); + float4 _1280 = _8.Load(int3(uint2(_1259, _1191), 0u)); + uint _1291 = uint(int(max(min(_246, _1167), _1165) << 16u) >> int(16u)); + float4 _1292 = _8.Load(int3(uint2(_1291, _1177), 0u)); + float4 _1300 = _8.Load(int3(uint2(_1291, _1191), 0u)); + uint _1311 = uint(int(max(min(_268, _1167), _1165) << 16u) >> int(16u)); + float4 _1312 = _8.Load(int3(uint2(_1311, _1191), 0u)); + float4 _1320 = _8.Load(int3(uint2(_1271, _1225), 0u)); + float4 _1328 = _8.Load(int3(uint2(_1259, _1225), 0u)); + float4 _1336 = _8.Load(int3(uint2(_1259, _1239), 0u)); + float4 _1344 = _8.Load(int3(uint2(_1291, _1225), 0u)); + float4 _1352 = _8.Load(int3(uint2(_1311, _1225), 0u)); + float4 _1360 = _8.Load(int3(uint2(_1291, _1239), 0u)); + half _1371 = half(_18_m0[1u].w); + half _1385 = exp2(log2((half(_1179.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1386 = exp2(log2((half(_1260.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1405 = exp2(log2((half(_1204.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1406 = exp2(log2((half(_1292.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1425 = exp2(log2((half(_1192.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1426 = exp2(log2((half(_1272.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1445 = exp2(log2((half(_1196.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1446 = exp2(log2((half(_1280.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1465 = exp2(log2((half(_1208.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1466 = exp2(log2((half(_1300.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1485 = exp2(log2((half(_1216.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1486 = exp2(log2((half(_1312.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1505 = exp2(log2((half(_1226.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1506 = exp2(log2((half(_1320.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1525 = exp2(log2((half(_1230.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1526 = exp2(log2((half(_1328.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1545 = exp2(log2((half(_1244.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1546 = exp2(log2((half(_1344.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1565 = exp2(log2((half(_1248.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1566 = exp2(log2((half(_1352.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1585 = exp2(log2((half(_1240.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1586 = exp2(log2((half(_1336.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1605 = exp2(log2((half(_1252.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1606 = exp2(log2((half(_1360.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); + half _1612 = isnan(_1445) ? _1425 : (isnan(_1425) ? _1445 : min(_1425, _1445)); + half _1613 = isnan(_1446) ? _1426 : (isnan(_1426) ? _1446 : min(_1426, _1446)); + half _1614 = isnan(_1612) ? _1385 : (isnan(_1385) ? _1612 : min(_1385, _1612)); + half _1615 = isnan(_1613) ? _1386 : (isnan(_1386) ? _1613 : min(_1386, _1613)); + half _1616 = isnan(_1525) ? _1465 : (isnan(_1465) ? _1525 : min(_1465, _1525)); + half _1617 = isnan(_1526) ? _1466 : (isnan(_1466) ? _1526 : min(_1466, _1526)); + half _1618 = isnan(_1616) ? _1614 : (isnan(_1614) ? _1616 : min(_1614, _1616)); + half _1619 = isnan(_1617) ? _1615 : (isnan(_1615) ? _1617 : min(_1615, _1617)); + half _1620 = isnan(_1445) ? _1425 : (isnan(_1425) ? _1445 : max(_1425, _1445)); + half _1621 = isnan(_1446) ? _1426 : (isnan(_1426) ? _1446 : max(_1426, _1446)); + half _1622 = isnan(_1620) ? _1385 : (isnan(_1385) ? _1620 : max(_1385, _1620)); + half _1623 = isnan(_1621) ? _1386 : (isnan(_1386) ? _1621 : max(_1386, _1621)); + half _1624 = isnan(_1525) ? _1465 : (isnan(_1465) ? _1525 : max(_1465, _1525)); + half _1625 = isnan(_1526) ? _1466 : (isnan(_1466) ? _1526 : max(_1466, _1526)); + half _1626 = isnan(_1624) ? _1622 : (isnan(_1622) ? _1624 : max(_1622, _1624)); + half _1627 = isnan(_1625) ? _1623 : (isnan(_1623) ? _1625 : max(_1623, _1625)); + half _1628 = isnan(_1465) ? _1445 : (isnan(_1445) ? _1465 : min(_1445, _1465)); + half _1629 = isnan(_1466) ? _1446 : (isnan(_1446) ? _1466 : min(_1446, _1466)); + half _1630 = isnan(_1628) ? _1405 : (isnan(_1405) ? _1628 : min(_1405, _1628)); + half _1631 = isnan(_1629) ? _1406 : (isnan(_1406) ? _1629 : min(_1406, _1629)); + half _1632 = isnan(_1545) ? _1485 : (isnan(_1485) ? _1545 : min(_1485, _1545)); + half _1633 = isnan(_1546) ? _1486 : (isnan(_1486) ? _1546 : min(_1486, _1546)); + half _1634 = isnan(_1632) ? _1630 : (isnan(_1630) ? _1632 : min(_1630, _1632)); + half _1635 = isnan(_1633) ? _1631 : (isnan(_1631) ? _1633 : min(_1631, _1633)); + half _1636 = isnan(_1465) ? _1445 : (isnan(_1445) ? _1465 : max(_1445, _1465)); + half _1637 = isnan(_1466) ? _1446 : (isnan(_1446) ? _1466 : max(_1446, _1466)); + half _1638 = isnan(_1636) ? _1405 : (isnan(_1405) ? _1636 : max(_1405, _1636)); + half _1639 = isnan(_1637) ? _1406 : (isnan(_1406) ? _1637 : max(_1406, _1637)); + half _1640 = isnan(_1545) ? _1485 : (isnan(_1485) ? _1545 : max(_1485, _1545)); + half _1641 = isnan(_1546) ? _1486 : (isnan(_1486) ? _1546 : max(_1486, _1546)); + half _1642 = isnan(_1640) ? _1638 : (isnan(_1638) ? _1640 : max(_1638, _1640)); + half _1643 = isnan(_1641) ? _1639 : (isnan(_1639) ? _1641 : max(_1639, _1641)); + half _1644 = isnan(_1525) ? _1505 : (isnan(_1505) ? _1525 : min(_1505, _1525)); + half _1645 = isnan(_1526) ? _1506 : (isnan(_1506) ? _1526 : min(_1506, _1526)); + half _1646 = isnan(_1644) ? _1445 : (isnan(_1445) ? _1644 : min(_1445, _1644)); + half _1647 = isnan(_1645) ? _1446 : (isnan(_1446) ? _1645 : min(_1446, _1645)); + half _1648 = isnan(_1585) ? _1545 : (isnan(_1545) ? _1585 : min(_1545, _1585)); + half _1649 = isnan(_1586) ? _1546 : (isnan(_1546) ? _1586 : min(_1546, _1586)); + half _1650 = isnan(_1648) ? _1646 : (isnan(_1646) ? _1648 : min(_1646, _1648)); + half _1651 = isnan(_1649) ? _1647 : (isnan(_1647) ? _1649 : min(_1647, _1649)); + half _1652 = isnan(_1525) ? _1505 : (isnan(_1505) ? _1525 : max(_1505, _1525)); + half _1653 = isnan(_1526) ? _1506 : (isnan(_1506) ? _1526 : max(_1506, _1526)); + half _1654 = isnan(_1652) ? _1445 : (isnan(_1445) ? _1652 : max(_1445, _1652)); + half _1655 = isnan(_1653) ? _1446 : (isnan(_1446) ? _1653 : max(_1446, _1653)); + half _1656 = isnan(_1585) ? _1545 : (isnan(_1545) ? _1585 : max(_1545, _1585)); + half _1657 = isnan(_1586) ? _1546 : (isnan(_1546) ? _1586 : max(_1546, _1586)); + half _1658 = isnan(_1656) ? _1654 : (isnan(_1654) ? _1656 : max(_1654, _1656)); + half _1659 = isnan(_1657) ? _1655 : (isnan(_1655) ? _1657 : max(_1655, _1657)); + half _1660 = isnan(_1545) ? _1525 : (isnan(_1525) ? _1545 : min(_1525, _1545)); + half _1661 = isnan(_1546) ? _1526 : (isnan(_1526) ? _1546 : min(_1526, _1546)); + half _1662 = isnan(_1660) ? _1465 : (isnan(_1465) ? _1660 : min(_1465, _1660)); + half _1663 = isnan(_1661) ? _1466 : (isnan(_1466) ? _1661 : min(_1466, _1661)); + half _1664 = isnan(_1605) ? _1565 : (isnan(_1565) ? _1605 : min(_1565, _1605)); + half _1665 = isnan(_1606) ? _1566 : (isnan(_1566) ? _1606 : min(_1566, _1606)); + half _1666 = isnan(_1664) ? _1662 : (isnan(_1662) ? _1664 : min(_1662, _1664)); + half _1667 = isnan(_1665) ? _1663 : (isnan(_1663) ? _1665 : min(_1663, _1665)); + half _1668 = isnan(_1545) ? _1525 : (isnan(_1525) ? _1545 : max(_1525, _1545)); + half _1669 = isnan(_1546) ? _1526 : (isnan(_1526) ? _1546 : max(_1526, _1546)); + half _1670 = isnan(_1668) ? _1465 : (isnan(_1465) ? _1668 : max(_1465, _1668)); + half _1671 = isnan(_1669) ? _1466 : (isnan(_1466) ? _1669 : max(_1466, _1669)); + half _1672 = isnan(_1605) ? _1565 : (isnan(_1565) ? _1605 : max(_1565, _1605)); + half _1673 = isnan(_1606) ? _1566 : (isnan(_1566) ? _1606 : max(_1566, _1606)); + half _1674 = isnan(_1672) ? _1670 : (isnan(_1670) ? _1672 : max(_1670, _1672)); + half _1675 = isnan(_1673) ? _1671 : (isnan(_1671) ? _1673 : max(_1671, _1673)); + half _1684 = half(1.0) - _1626; + half _1685 = half(1.0) - _1627; + half _1688 = (isnan(_1684) ? _1618 : (isnan(_1618) ? _1684 : min(_1618, _1684))) * (half(1.0) / _1626); + half _1689 = (isnan(_1685) ? _1619 : (isnan(_1619) ? _1685 : min(_1619, _1685))) * (half(1.0) / _1627); + half _3064 = isnan(half(0.0)) ? _1688 : (isnan(_1688) ? half(0.0) : max(_1688, half(0.0))); + half _3075 = isnan(half(0.0)) ? _1689 : (isnan(_1689) ? half(0.0) : max(_1689, half(0.0))); + half _1692 = half(1.0) - _1642; + half _1693 = half(1.0) - _1643; + half _1696 = (isnan(_1692) ? _1634 : (isnan(_1634) ? _1692 : min(_1634, _1692))) * (half(1.0) / _1642); + half _1697 = (isnan(_1693) ? _1635 : (isnan(_1635) ? _1693 : min(_1635, _1693))) * (half(1.0) / _1643); + half _3096 = isnan(half(0.0)) ? _1696 : (isnan(_1696) ? half(0.0) : max(_1696, half(0.0))); + half _3107 = isnan(half(0.0)) ? _1697 : (isnan(_1697) ? half(0.0) : max(_1697, half(0.0))); + half _1700 = half(1.0) - _1658; + half _1701 = half(1.0) - _1659; + half _1704 = (isnan(_1700) ? _1650 : (isnan(_1650) ? _1700 : min(_1650, _1700))) * (half(1.0) / _1658); + half _1705 = (isnan(_1701) ? _1651 : (isnan(_1651) ? _1701 : min(_1651, _1701))) * (half(1.0) / _1659); + half _3128 = isnan(half(0.0)) ? _1704 : (isnan(_1704) ? half(0.0) : max(_1704, half(0.0))); + half _3139 = isnan(half(0.0)) ? _1705 : (isnan(_1705) ? half(0.0) : max(_1705, half(0.0))); + half _1708 = half(1.0) - _1674; + half _1709 = half(1.0) - _1675; + half _1712 = (isnan(_1708) ? _1666 : (isnan(_1666) ? _1708 : min(_1666, _1708))) * (half(1.0) / _1674); + half _1713 = (isnan(_1709) ? _1667 : (isnan(_1667) ? _1709 : min(_1667, _1709))) * (half(1.0) / _1675); + half _3160 = isnan(half(0.0)) ? _1712 : (isnan(_1712) ? half(0.0) : max(_1712, half(0.0))); + half _3171 = isnan(half(0.0)) ? _1713 : (isnan(_1713) ? half(0.0) : max(_1713, half(0.0))); + half _1732 = half(1.0) - _1159; + half _1747 = (_1732 * _699) * (half(1.0) / ((half(0.03125) - _1618) + _1626)); + half _1748 = (_700 * _1732) * (half(1.0) / ((half(0.03125) - _1619) + _1627)); + half _1755 = (_1732 * _91) * (half(1.0) / ((half(0.03125) - _1634) + _1642)); + half _1756 = (_208 * _1732) * (half(1.0) / ((half(0.03125) - _1635) + _1643)); + half _1763 = (_699 * _1159) * (half(1.0) / ((half(0.03125) - _1650) + _1658)); + half _1764 = (_700 * _1159) * (half(1.0) / ((half(0.03125) - _1651) + _1659)); + half _1771 = (_1159 * _91) * (half(1.0) / ((half(0.03125) - _1666) + _1674)); + half _1772 = (_208 * _1159) * (half(1.0) / ((half(0.03125) - _1667) + _1675)); + half _1773 = (_690 * sqrt(isnan(half(1.0)) ? _3064 : (isnan(_3064) ? half(1.0) : min(_3064, half(1.0))))) * _1747; + half _1774 = _1748 * (_690 * sqrt(isnan(half(1.0)) ? _3075 : (isnan(_3075) ? half(1.0) : min(_3075, half(1.0))))); + half _1775 = _1755 * (_690 * sqrt(isnan(half(1.0)) ? _3096 : (isnan(_3096) ? half(1.0) : min(_3096, half(1.0))))); + half _1776 = _1756 * (_690 * sqrt(isnan(half(1.0)) ? _3107 : (isnan(_3107) ? half(1.0) : min(_3107, half(1.0))))); + half _1777 = _1763 * (_690 * sqrt(isnan(half(1.0)) ? _3128 : (isnan(_3128) ? half(1.0) : min(_3128, half(1.0))))); + half _1778 = _1764 * (_690 * sqrt(isnan(half(1.0)) ? _3139 : (isnan(_3139) ? half(1.0) : min(_3139, half(1.0))))); + half _1780 = (_1775 + _1747) + _1777; + half _1782 = (_1776 + _1748) + _1778; + half _1783 = _1771 * (_690 * sqrt(isnan(half(1.0)) ? _3160 : (isnan(_3160) ? half(1.0) : min(_3160, half(1.0))))); + half _1784 = _1772 * (_690 * sqrt(isnan(half(1.0)) ? _3171 : (isnan(_3171) ? half(1.0) : min(_3171, half(1.0))))); + half _1786 = (_1755 + _1773) + _1783; + half _1788 = (_1756 + _1774) + _1784; + half _1790 = (_1763 + _1773) + _1783; + half _1792 = (_1764 + _1774) + _1784; + half _1795 = (_1777 + _1775) + _1771; + half _1796 = (_1778 + _1776) + _1772; + half _1813 = half(1.0) / ((((_1795 + _1780) + ((((_1775 + _1773) + _1777) + _1783) * half(2.0))) + _1786) + _1790); + half _1814 = half(1.0) / ((((_1796 + _1782) + ((((_1776 + _1774) + _1778) + _1784) * half(2.0))) + _1788) + _1792); + half _1832 = ((((((_1782 * exp2(_1371 * log2((half(_1280.x) + half(0.05499267578125)) * half(0.9482421875)))) + (_1774 * (exp2(_1371 * log2((half(_1272.x) + half(0.05499267578125)) * half(0.9482421875))) + exp2(_1371 * log2((half(_1260.x) + half(0.05499267578125)) * half(0.9482421875)))))) + (_1796 * exp2(_1371 * log2((half(_1344.x) + half(0.05499267578125)) * half(0.9482421875))))) + (_1788 * exp2(_1371 * log2((half(_1300.x) + half(0.05499267578125)) * half(0.9482421875))))) + (_1792 * exp2(_1371 * log2((half(_1328.x) + half(0.05499267578125)) * half(0.9482421875))))) + (_1784 * (exp2(_1371 * log2((half(_1360.x) + half(0.05499267578125)) * half(0.9482421875))) + exp2(_1371 * log2((half(_1352.x) + half(0.05499267578125)) * half(0.9482421875)))))) + (_1778 * (exp2(_1371 * log2((half(_1336.x) + half(0.05499267578125)) * half(0.9482421875))) + exp2(_1371 * log2((half(_1320.x) + half(0.05499267578125)) * half(0.9482421875))))); + half _1834 = (_1832 + (_1776 * (exp2(_1371 * log2((half(_1312.x) + half(0.05499267578125)) * half(0.9482421875))) + exp2(_1371 * log2((half(_1292.x) + half(0.05499267578125)) * half(0.9482421875)))))) * _1814; + half _3182 = isnan(half(0.0)) ? _1834 : (isnan(_1834) ? half(0.0) : max(_1834, half(0.0))); + half _1855 = ((((((((_1782 * _1446) + (_1774 * (_1426 + _1386))) + (_1796 * _1546)) + (_1788 * _1466)) + (_1792 * _1526)) + (_1784 * (_1606 + _1566))) + (_1778 * (_1586 + _1506))) + (_1776 * (_1486 + _1406))) * _1814; + half _3193 = isnan(half(0.0)) ? _1855 : (isnan(_1855) ? half(0.0) : max(_1855, half(0.0))); + half _1874 = ((((((_1782 * exp2(log2((half(_1280.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371)) + (_1774 * (exp2(log2((half(_1272.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1260.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371)))) + (_1796 * exp2(log2((half(_1344.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371))) + (_1788 * exp2(log2((half(_1300.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371))) + (_1792 * exp2(log2((half(_1328.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371))) + (_1784 * (exp2(log2((half(_1360.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1352.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371)))) + (_1778 * (exp2(log2((half(_1336.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1320.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371))); + half _1876 = (_1874 + (_1776 * (exp2(log2((half(_1312.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1292.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371)))) * _1814; + half _3204 = isnan(half(0.0)) ? _1876 : (isnan(_1876) ? half(0.0) : max(_1876, half(0.0))); + uint4 _1880 = asuint(_23_m0[2u]); + if ((_58 <= _1880.z) && (_1153 <= _1880.w)) + { + half _1987 = ((((((exp2(log2((half(_1196.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) * _1780) + ((exp2(log2((half(_1192.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1179.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371)) * _1773)) + (exp2(log2((half(_1208.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) * _1786)) + (exp2(log2((half(_1230.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) * _1790)) + (exp2(log2((half(_1244.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) * _1795)) + ((exp2(log2((half(_1252.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1248.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371)) * _1783)) + ((exp2(log2((half(_1240.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1226.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371)) * _1777); + half _1989 = (_1987 + ((exp2(log2((half(_1216.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1204.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371)) * _1775)) * _1813; + half _3215 = isnan(half(0.0)) ? _1989 : (isnan(_1989) ? half(0.0) : max(_1989, half(0.0))); + half _1992 = half(1.0) / _849; + half _1996 = (exp2(_1992 * log2(isnan(half(1.0)) ? _3215 : (isnan(_3215) ? half(1.0) : min(_3215, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); + half _2018 = ((((((((_1780 * _1445) + (_1773 * (_1425 + _1385))) + (_1795 * _1545)) + (_1786 * _1465)) + (_1790 * _1525)) + (_1783 * (_1605 + _1565))) + (_1777 * (_1585 + _1505))) + (_1775 * (_1485 + _1405))) * _1813; + half _3231 = isnan(half(0.0)) ? _2018 : (isnan(_2018) ? half(0.0) : max(_2018, half(0.0))); + half _2024 = (exp2(_1992 * log2(isnan(half(1.0)) ? _3231 : (isnan(_3231) ? half(1.0) : min(_3231, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); + half _2128 = ((((((exp2(log2((half(_1196.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371) * _1780) + ((exp2(log2((half(_1192.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1179.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371)) * _1773)) + (exp2(log2((half(_1208.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371) * _1786)) + (exp2(log2((half(_1230.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371) * _1790)) + (exp2(log2((half(_1244.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371) * _1795)) + ((exp2(log2((half(_1252.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1248.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371)) * _1783)) + ((exp2(log2((half(_1240.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1226.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371)) * _1777); + half _2130 = (_2128 + ((exp2(log2((half(_1216.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1204.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371)) * _1775)) * _1813; + half _3247 = isnan(half(0.0)) ? _2130 : (isnan(_2130) ? half(0.0) : max(_2130, half(0.0))); + half _2136 = (exp2(_1992 * log2(isnan(half(1.0)) ? _3247 : (isnan(_3247) ? half(1.0) : min(_3247, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); + _11[uint2(_58, _1153)] = float4(float(isnan(half(0.0)) ? _2136 : (isnan(_2136) ? half(0.0) : max(_2136, half(0.0)))), float(isnan(half(0.0)) ? _2024 : (isnan(_2024) ? half(0.0) : max(_2024, half(0.0)))), float(isnan(half(0.0)) ? _1996 : (isnan(_1996) ? half(0.0) : max(_1996, half(0.0)))), 1.0f); + } + uint4 _2144 = asuint(_23_m0[2u]); + if ((_1119 <= _2144.z) && (_1153 <= _2144.w)) + { + half _2151 = half(1.0) / _849; + half _2155 = (exp2(_2151 * log2(isnan(half(1.0)) ? _3204 : (isnan(_3204) ? half(1.0) : min(_3204, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); + half _2162 = (exp2(_2151 * log2(isnan(half(1.0)) ? _3193 : (isnan(_3193) ? half(1.0) : min(_3193, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); + half _2169 = (exp2(_2151 * log2(isnan(half(1.0)) ? _3182 : (isnan(_3182) ? half(1.0) : min(_3182, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); + _11[uint2(_1119, _1153)] = float4(float(isnan(half(0.0)) ? _2169 : (isnan(_2169) ? half(0.0) : max(_2169, half(0.0)))), float(isnan(half(0.0)) ? _2162 : (isnan(_2162) ? half(0.0) : max(_2162, half(0.0)))), float(isnan(half(0.0)) ? _2155 : (isnan(_2155) ? half(0.0) : max(_2155, half(0.0)))), 1.0f); + } + +#elif defined(USE_PACKED_MATH) #if 0 // Disable the CS so it just outputs the input uint2 _55 = CASData.cas2.xy; uint _58 = _55.x + (((csInput.SV_GroupThreadID.x >> 1u) & 7u) | (csInput.SV_GroupID.x << 4u)); @@ -332,4 +808,5 @@ void CS(CSInput csInput) } #endif +#endif // USE_PACKED_MATH } From c0d5e12f9ff76ff74276995441a5d1fd758f6704 Mon Sep 17 00:00:00 2001 From: Lilium Date: Sun, 8 Oct 2023 03:16:42 +0200 Subject: [PATCH 107/270] fixup CAS upscaling and optimise it a little (still needs to be hooked up to new cBuffer) --- .../ContrastAdaptiveSharpening_cs.hlsl | 927 +++++++++--------- shaders/compile_all_shaders.ps1 | 1 + 2 files changed, 485 insertions(+), 443 deletions(-) diff --git a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl index cf69fa88..8fd0b3cf 100644 --- a/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl +++ b/shaders/ContrastAdaptiveSharpening/ContrastAdaptiveSharpening_cs.hlsl @@ -2,8 +2,8 @@ #include "../color.hlsl" #include "RootSignature.hlsl" -#define USE_PACKED_MATH -#define USE_UPSCALING +//#define USE_PACKED_MATH +//#define USE_UPSCALING // this is CAS upscaling in FP16 with FFX_CAS_USE_PRECISE_MATH [100FF94] // other variants are also available but seemingly unused, which are: @@ -102,479 +102,520 @@ half3 PrepareForOutput(half3 Color) [numthreads(64, 1, 1)] void CS(CSInput csInput) { - #if defined(USE_PACKED_MATH) \ && defined(USE_UPSCALING) - uint4 _55 = asuint(_23_m0[2u]); - uint _58 = _55.x + (((gl_LocalInvocationID.x >> 1u) & 7u) | (gl_WorkGroupID.x << 4u)); - uint _59 = ((((gl_LocalInvocationID.x >> 3u) & 6u) | (gl_LocalInvocationID.x & 1u)) | (gl_WorkGroupID.y << 4u)) + _55.y; - uint4 _62 = asuint(_23_m0[0u]); - uint4 _69 = asuint(_23_m0[1u]); - half _76 = half(_18_m0[1u].w); - float _80 = asfloat(_62.y); - float _84 = asfloat(_62.w); - float _85 = (float(_58) * asfloat(_62.x)) + asfloat(_62.z); - float _86 = (float(_59) * _80) + _84; - float _88 = floor(_85); - float _89 = floor(_86); - half _91 = half(_85 - _88); - half _93 = half(_86 - _89); - uint16_t _95 = (int16_t(_88)); - uint16_t _96 = (int16_t(_89)); - uint4 _101 = asuint(_23_m0[3u]); + uint4 _55 = CASData.rectLimits0; + + uint _58 = ((csInput.SV_GroupThreadID.x >> 1) & 7) + | (csInput.SV_GroupID.x << 4); + _58 += _55.x; + + uint _59 = ((csInput.SV_GroupThreadID.x >> 3) & 6) + | (csInput.SV_GroupThreadID.x & 1) + | (csInput.SV_GroupID.y << 4); + _59 += _55.y; + + CASConst0 _62 = CASData.upscalingConst0; + CASConst1 _69 = CASData.upscalingConst1; + + float _85 = (float(_58) * _62.rcpScalingFactorX) + _62.otherScalingFactorX; + float _86 = (float(_59) * _62.rcpScalingFactorY) + _62.otherScalingFactorY; + + half _91 = frac(_85); + half _93 = frac(_86); + + uint _95 = uint(_85); + uint _96 = uint(_86); + + uint4 _101 = CASData.rectLimits1; + uint _102 = _101.x; uint _103 = _101.y; uint _104 = _101.z; uint _105 = _101.w; - uint _106 = uint(_95); - uint _114 = uint(int(max(min(_106, _104), _102) << 16u) >> int(16u)); - uint _116 = uint(int(max(min(uint((_96 + 65535u)), _105), _103) << 16u) >> int(16u)); - float4 _118 = _8.Load(int3(uint2(_114, _116), 0u)); - uint _124 = uint((_95 + 65535u)); - uint _131 = uint(int(max(min(_124, _104), _102) << 16u) >> int(16u)); - uint _133 = uint(int(max(min(uint(_96), _105), _103) << 16u) >> int(16u)); - float4 _134 = _8.Load(int3(uint2(_131, _133), 0u)); - float4 _138 = _8.Load(int3(uint2(_114, _133), 0u)); - uint _144 = uint((_95 + 1u)); - uint _148 = uint(int(max(min(_144, _104), _102) << 16u) >> int(16u)); - float4 _149 = _8.Load(int3(uint2(_148, _116), 0u)); - float4 _153 = _8.Load(int3(uint2(_148, _133), 0u)); - uint _159 = uint((_95 + 2u)); - uint _163 = uint(int(max(min(_159, _104), _102) << 16u) >> int(16u)); - float4 _164 = _8.Load(int3(uint2(_163, _133), 0u)); - uint _173 = uint(int(max(min(uint((_96 + 1u)), _105), _103) << 16u) >> int(16u)); - float4 _174 = _8.Load(int3(uint2(_131, _173), 0u)); - float4 _178 = _8.Load(int3(uint2(_114, _173), 0u)); - uint _187 = uint(int(max(min(uint((_96 + 2u)), _105), _103) << 16u) >> int(16u)); - float4 _188 = _8.Load(int3(uint2(_114, _187), 0u)); - float4 _192 = _8.Load(int3(uint2(_148, _173), 0u)); - float4 _196 = _8.Load(int3(uint2(_163, _173), 0u)); - float4 _200 = _8.Load(int3(uint2(_148, _187), 0u)); - float _205 = _85 + asfloat(_69.z); - float _206 = floor(_205); - half _208 = half(_205 - _206); - uint16_t _209 = (int16_t(_206)); - uint _210 = uint(_209); - uint _214 = uint(int(max(min(_210, _104), _102) << 16u) >> int(16u)); - float4 _215 = _8.Load(int3(uint2(_214, _116), 0u)); - uint _224 = uint((_209 + 65535u)); - uint _228 = uint(int(max(min(_224, _104), _102) << 16u) >> int(16u)); - float4 _229 = _8.Load(int3(uint2(_228, _133), 0u)); - float4 _237 = _8.Load(int3(uint2(_214, _133), 0u)); - uint _246 = uint((_209 + 1u)); - uint _250 = uint(int(max(min(_246, _104), _102) << 16u) >> int(16u)); - float4 _251 = _8.Load(int3(uint2(_250, _116), 0u)); - float4 _259 = _8.Load(int3(uint2(_250, _133), 0u)); - uint _268 = uint((_209 + 2u)); - uint _272 = uint(int(max(min(_268, _104), _102) << 16u) >> int(16u)); - float4 _273 = _8.Load(int3(uint2(_272, _133), 0u)); - float4 _281 = _8.Load(int3(uint2(_228, _173), 0u)); - float4 _289 = _8.Load(int3(uint2(_214, _173), 0u)); - float4 _297 = _8.Load(int3(uint2(_214, _187), 0u)); - float4 _305 = _8.Load(int3(uint2(_250, _173), 0u)); - float4 _313 = _8.Load(int3(uint2(_272, _173), 0u)); - float4 _321 = _8.Load(int3(uint2(_250, _187), 0u)); - half _344 = exp2(log2((half(_118.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _345 = exp2(log2((half(_215.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _364 = exp2(log2((half(_149.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _365 = exp2(log2((half(_251.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _384 = exp2(log2((half(_134.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _385 = exp2(log2((half(_229.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _404 = exp2(log2((half(_138.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _405 = exp2(log2((half(_237.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _424 = exp2(log2((half(_153.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _425 = exp2(log2((half(_259.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _444 = exp2(log2((half(_164.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _445 = exp2(log2((half(_273.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _464 = exp2(log2((half(_174.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _465 = exp2(log2((half(_281.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _484 = exp2(log2((half(_178.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _485 = exp2(log2((half(_289.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _504 = exp2(log2((half(_192.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _505 = exp2(log2((half(_305.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _524 = exp2(log2((half(_196.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _525 = exp2(log2((half(_313.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _544 = exp2(log2((half(_188.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _545 = exp2(log2((half(_297.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _564 = exp2(log2((half(_200.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _565 = exp2(log2((half(_321.y) + half(0.05499267578125)) * half(0.9482421875)) * _76); - half _571 = isnan(_404) ? _384 : (isnan(_384) ? _404 : min(_384, _404)); - half _572 = isnan(_405) ? _385 : (isnan(_385) ? _405 : min(_385, _405)); - half _573 = isnan(_571) ? _344 : (isnan(_344) ? _571 : min(_344, _571)); - half _574 = isnan(_572) ? _345 : (isnan(_345) ? _572 : min(_345, _572)); - half _575 = isnan(_484) ? _424 : (isnan(_424) ? _484 : min(_424, _484)); - half _576 = isnan(_485) ? _425 : (isnan(_425) ? _485 : min(_425, _485)); - half _577 = isnan(_575) ? _573 : (isnan(_573) ? _575 : min(_573, _575)); - half _578 = isnan(_576) ? _574 : (isnan(_574) ? _576 : min(_574, _576)); - half _579 = isnan(_404) ? _384 : (isnan(_384) ? _404 : max(_384, _404)); - half _580 = isnan(_405) ? _385 : (isnan(_385) ? _405 : max(_385, _405)); - half _581 = isnan(_579) ? _344 : (isnan(_344) ? _579 : max(_344, _579)); - half _582 = isnan(_580) ? _345 : (isnan(_345) ? _580 : max(_345, _580)); - half _583 = isnan(_484) ? _424 : (isnan(_424) ? _484 : max(_424, _484)); - half _584 = isnan(_485) ? _425 : (isnan(_425) ? _485 : max(_425, _485)); - half _585 = isnan(_583) ? _581 : (isnan(_581) ? _583 : max(_581, _583)); - half _586 = isnan(_584) ? _582 : (isnan(_582) ? _584 : max(_582, _584)); - half _587 = isnan(_424) ? _404 : (isnan(_404) ? _424 : min(_404, _424)); - half _588 = isnan(_425) ? _405 : (isnan(_405) ? _425 : min(_405, _425)); - half _589 = isnan(_587) ? _364 : (isnan(_364) ? _587 : min(_364, _587)); - half _590 = isnan(_588) ? _365 : (isnan(_365) ? _588 : min(_365, _588)); - half _591 = isnan(_504) ? _444 : (isnan(_444) ? _504 : min(_444, _504)); - half _592 = isnan(_505) ? _445 : (isnan(_445) ? _505 : min(_445, _505)); - half _593 = isnan(_591) ? _589 : (isnan(_589) ? _591 : min(_589, _591)); - half _594 = isnan(_592) ? _590 : (isnan(_590) ? _592 : min(_590, _592)); - half _595 = isnan(_424) ? _404 : (isnan(_404) ? _424 : max(_404, _424)); - half _596 = isnan(_425) ? _405 : (isnan(_405) ? _425 : max(_405, _425)); - half _597 = isnan(_595) ? _364 : (isnan(_364) ? _595 : max(_364, _595)); - half _598 = isnan(_596) ? _365 : (isnan(_365) ? _596 : max(_365, _596)); - half _599 = isnan(_504) ? _444 : (isnan(_444) ? _504 : max(_444, _504)); - half _600 = isnan(_505) ? _445 : (isnan(_445) ? _505 : max(_445, _505)); - half _601 = isnan(_599) ? _597 : (isnan(_597) ? _599 : max(_597, _599)); - half _602 = isnan(_600) ? _598 : (isnan(_598) ? _600 : max(_598, _600)); - half _603 = isnan(_484) ? _464 : (isnan(_464) ? _484 : min(_464, _484)); - half _604 = isnan(_485) ? _465 : (isnan(_465) ? _485 : min(_465, _485)); - half _605 = isnan(_603) ? _404 : (isnan(_404) ? _603 : min(_404, _603)); - half _606 = isnan(_604) ? _405 : (isnan(_405) ? _604 : min(_405, _604)); - half _607 = isnan(_544) ? _504 : (isnan(_504) ? _544 : min(_504, _544)); - half _608 = isnan(_545) ? _505 : (isnan(_505) ? _545 : min(_505, _545)); - half _609 = isnan(_607) ? _605 : (isnan(_605) ? _607 : min(_605, _607)); - half _610 = isnan(_608) ? _606 : (isnan(_606) ? _608 : min(_606, _608)); - half _611 = isnan(_484) ? _464 : (isnan(_464) ? _484 : max(_464, _484)); - half _612 = isnan(_485) ? _465 : (isnan(_465) ? _485 : max(_465, _485)); - half _613 = isnan(_611) ? _404 : (isnan(_404) ? _611 : max(_404, _611)); - half _614 = isnan(_612) ? _405 : (isnan(_405) ? _612 : max(_405, _612)); - half _615 = isnan(_544) ? _504 : (isnan(_504) ? _544 : max(_504, _544)); - half _616 = isnan(_545) ? _505 : (isnan(_505) ? _545 : max(_505, _545)); - half _617 = isnan(_615) ? _613 : (isnan(_613) ? _615 : max(_613, _615)); - half _618 = isnan(_616) ? _614 : (isnan(_614) ? _616 : max(_614, _616)); - half _619 = isnan(_504) ? _484 : (isnan(_484) ? _504 : min(_484, _504)); - half _620 = isnan(_505) ? _485 : (isnan(_485) ? _505 : min(_485, _505)); - half _621 = isnan(_619) ? _424 : (isnan(_424) ? _619 : min(_424, _619)); - half _622 = isnan(_620) ? _425 : (isnan(_425) ? _620 : min(_425, _620)); - half _623 = isnan(_564) ? _524 : (isnan(_524) ? _564 : min(_524, _564)); - half _624 = isnan(_565) ? _525 : (isnan(_525) ? _565 : min(_525, _565)); - half _625 = isnan(_623) ? _621 : (isnan(_621) ? _623 : min(_621, _623)); - half _626 = isnan(_624) ? _622 : (isnan(_622) ? _624 : min(_622, _624)); - half _627 = isnan(_504) ? _484 : (isnan(_484) ? _504 : max(_484, _504)); - half _628 = isnan(_505) ? _485 : (isnan(_485) ? _505 : max(_485, _505)); - half _629 = isnan(_627) ? _424 : (isnan(_424) ? _627 : max(_424, _627)); - half _630 = isnan(_628) ? _425 : (isnan(_425) ? _628 : max(_425, _628)); - half _631 = isnan(_564) ? _524 : (isnan(_524) ? _564 : max(_524, _564)); - half _632 = isnan(_565) ? _525 : (isnan(_525) ? _565 : max(_525, _565)); - half _633 = isnan(_631) ? _629 : (isnan(_629) ? _631 : max(_629, _631)); - half _634 = isnan(_632) ? _630 : (isnan(_630) ? _632 : max(_630, _632)); - half _644 = half(1.0) - _585; - half _645 = half(1.0) - _586; - half _648 = (isnan(_644) ? _577 : (isnan(_577) ? _644 : min(_577, _644))) * (half(1.0) / _585); - half _649 = (isnan(_645) ? _578 : (isnan(_578) ? _645 : min(_578, _645))) * (half(1.0) / _586); - half _2515 = isnan(half(0.0)) ? _648 : (isnan(_648) ? half(0.0) : max(_648, half(0.0))); - half _2526 = isnan(half(0.0)) ? _649 : (isnan(_649) ? half(0.0) : max(_649, half(0.0))); - half _653 = half(1.0) - _601; - half _654 = half(1.0) - _602; - half _657 = (isnan(_653) ? _593 : (isnan(_593) ? _653 : min(_593, _653))) * (half(1.0) / _601); - half _658 = (isnan(_654) ? _594 : (isnan(_594) ? _654 : min(_594, _654))) * (half(1.0) / _602); - half _2547 = isnan(half(0.0)) ? _657 : (isnan(_657) ? half(0.0) : max(_657, half(0.0))); - half _2558 = isnan(half(0.0)) ? _658 : (isnan(_658) ? half(0.0) : max(_658, half(0.0))); - half _661 = half(1.0) - _617; - half _662 = half(1.0) - _618; - half _665 = (isnan(_661) ? _609 : (isnan(_609) ? _661 : min(_609, _661))) * (half(1.0) / _617); - half _666 = (isnan(_662) ? _610 : (isnan(_610) ? _662 : min(_610, _662))) * (half(1.0) / _618); - half _2579 = isnan(half(0.0)) ? _665 : (isnan(_665) ? half(0.0) : max(_665, half(0.0))); - half _2590 = isnan(half(0.0)) ? _666 : (isnan(_666) ? half(0.0) : max(_666, half(0.0))); - half _669 = half(1.0) - _633; - half _670 = half(1.0) - _634; - half _673 = (isnan(_669) ? _625 : (isnan(_625) ? _669 : min(_625, _669))) * (half(1.0) / _633); - half _674 = (isnan(_670) ? _626 : (isnan(_626) ? _670 : min(_626, _670))) * (half(1.0) / _634); - half _2611 = isnan(half(0.0)) ? _673 : (isnan(_673) ? half(0.0) : max(_673, half(0.0))); - half _2622 = isnan(half(0.0)) ? _674 : (isnan(_674) ? half(0.0) : max(_674, half(0.0))); - half _690 = half(spvUnpackHalf2x16(_69.y & 65535u).x); - half _699 = half(1.0) - _91; - half _700 = half(1.0) - _208; - half _701 = half(1.0) - _93; - half _717 = (_699 * _701) * (half(1.0) / ((half(0.03125) - _577) + _585)); - half _718 = (_700 * _701) * (half(1.0) / ((half(0.03125) - _578) + _586)); - half _725 = (_701 * _91) * (half(1.0) / ((half(0.03125) - _593) + _601)); - half _726 = (_208 * _701) * (half(1.0) / ((half(0.03125) - _594) + _602)); - half _733 = (_699 * _93) * (half(1.0) / ((half(0.03125) - _609) + _617)); - half _734 = (_700 * _93) * (half(1.0) / ((half(0.03125) - _610) + _618)); - half _741 = (_91 * _93) * (half(1.0) / ((half(0.03125) - _625) + _633)); - half _742 = (_208 * _93) * (half(1.0) / ((half(0.03125) - _626) + _634)); - half _743 = (_690 * sqrt(isnan(half(1.0)) ? _2515 : (isnan(_2515) ? half(1.0) : min(_2515, half(1.0))))) * _717; - half _744 = _718 * (_690 * sqrt(isnan(half(1.0)) ? _2526 : (isnan(_2526) ? half(1.0) : min(_2526, half(1.0))))); - half _745 = _725 * (_690 * sqrt(isnan(half(1.0)) ? _2547 : (isnan(_2547) ? half(1.0) : min(_2547, half(1.0))))); - half _746 = _726 * (_690 * sqrt(isnan(half(1.0)) ? _2558 : (isnan(_2558) ? half(1.0) : min(_2558, half(1.0))))); - half _747 = _733 * (_690 * sqrt(isnan(half(1.0)) ? _2579 : (isnan(_2579) ? half(1.0) : min(_2579, half(1.0))))); - half _748 = _734 * (_690 * sqrt(isnan(half(1.0)) ? _2590 : (isnan(_2590) ? half(1.0) : min(_2590, half(1.0))))); + + uint _131 = max(min(_95 - 1, _104), _102); + uint _114 = max(min(_95, _104), _102); + uint _148 = max(min(_95 + 1, _104), _102); + uint _163 = max(min(_95 + 2, _104), _102); + + uint _116 = max(min(_96 - 1, _105), _103); + uint _133 = max(min(_96, _105), _103); + uint _173 = max(min(_96 + 1, _105), _103); + uint _187 = max(min(_96 + 2, _105), _103); + + half3 _118 = ColorIn.Load(int3(_114, _116, 0)).rgb; + half3 _134 = ColorIn.Load(int3(_131, _133, 0)).rgb; + half3 _138 = ColorIn.Load(int3(_114, _133, 0)).rgb; + half3 _149 = ColorIn.Load(int3(_148, _116, 0)).rgb; + half3 _153 = ColorIn.Load(int3(_148, _133, 0)).rgb; + half3 _164 = ColorIn.Load(int3(_163, _133, 0)).rgb; + half3 _174 = ColorIn.Load(int3(_131, _173, 0)).rgb; + half3 _178 = ColorIn.Load(int3(_114, _173, 0)).rgb; + half3 _188 = ColorIn.Load(int3(_114, _187, 0)).rgb; + half3 _192 = ColorIn.Load(int3(_148, _173, 0)).rgb; + half3 _196 = ColorIn.Load(int3(_163, _173, 0)).rgb; + half3 _200 = ColorIn.Load(int3(_148, _187, 0)).rgb; + + float _205 = _85 + _69.rcpScalingFactorXTimes8; + + half _208 = frac(_205); + + uint _209 = uint(_205); + + uint _228 = max(min(_209 - 1, _104), _102); + uint _214 = max(min(_209, _104), _102); + uint _250 = max(min(_209 + 1, _104), _102); + uint _272 = max(min(_209 + 2, _104), _102); + + half3 _215 = ColorIn.Load(int3(_214, _116, 0)).rgb; + half3 _229 = ColorIn.Load(int3(_228, _133, 0)).rgb; + half3 _237 = ColorIn.Load(int3(_214, _133, 0)).rgb; + half3 _251 = ColorIn.Load(int3(_250, _116, 0)).rgb; + half3 _259 = ColorIn.Load(int3(_250, _133, 0)).rgb; + half3 _273 = ColorIn.Load(int3(_272, _133, 0)).rgb; + half3 _281 = ColorIn.Load(int3(_228, _173, 0)).rgb; + half3 _289 = ColorIn.Load(int3(_214, _173, 0)).rgb; + half3 _297 = ColorIn.Load(int3(_214, _187, 0)).rgb; + half3 _305 = ColorIn.Load(int3(_250, _173, 0)).rgb; + half3 _313 = ColorIn.Load(int3(_272, _173, 0)).rgb; + half3 _321 = ColorIn.Load(int3(_250, _187, 0)).rgb; + + _118 = PrepareForProcessing(_118); + _134 = PrepareForProcessing(_134); + _138 = PrepareForProcessing(_138); + _149 = PrepareForProcessing(_149); + _153 = PrepareForProcessing(_153); + _164 = PrepareForProcessing(_164); + _174 = PrepareForProcessing(_174); + _178 = PrepareForProcessing(_178); + _188 = PrepareForProcessing(_188); + _192 = PrepareForProcessing(_192); + _196 = PrepareForProcessing(_196); + _200 = PrepareForProcessing(_200); + _215 = PrepareForProcessing(_215); + _229 = PrepareForProcessing(_229); + _237 = PrepareForProcessing(_237); + _251 = PrepareForProcessing(_251); + _259 = PrepareForProcessing(_259); + _273 = PrepareForProcessing(_273); + _281 = PrepareForProcessing(_281); + _289 = PrepareForProcessing(_289); + _297 = PrepareForProcessing(_297); + _305 = PrepareForProcessing(_305); + _313 = PrepareForProcessing(_313); + _321 = PrepareForProcessing(_321); + + half _344 = _118.y; + half _345 = _215.y; + half _364 = _149.y; + half _365 = _251.y; + half _384 = _134.y; + half _385 = _229.y; + half _404 = _138.y; + half _405 = _237.y; + half _424 = _153.y; + half _425 = _259.y; + half _444 = _164.y; + half _445 = _273.y; + half _464 = _174.y; + half _465 = _281.y; + half _484 = _178.y; + half _485 = _289.y; + half _504 = _192.y; + half _505 = _305.y; + half _524 = _196.y; + half _525 = _313.y; + half _544 = _188.y; + half _545 = _297.y; + half _564 = _200.y; + half _565 = _321.y; + + half _571 = min(_384, _404); + half _572 = min(_385, _405); + half _573 = min(_344, _571); + half _574 = min(_345, _572); + half _575 = min(_424, _484); + half _576 = min(_425, _485); + half _577 = min(_573, _575); + half _578 = min(_574, _576); + half _579 = max(_384, _404); + half _580 = max(_385, _405); + half _581 = max(_344, _579); + half _582 = max(_345, _580); + half _583 = max(_424, _484); + half _584 = max(_425, _485); + half _585 = max(_581, _583); + half _586 = max(_582, _584); + half _587 = min(_404, _424); + half _588 = min(_405, _425); + half _589 = min(_364, _587); + half _590 = min(_365, _588); + half _591 = min(_444, _504); + half _592 = min(_445, _505); + half _593 = min(_589, _591); + half _594 = min(_590, _592); + half _595 = max(_404, _424); + half _596 = max(_405, _425); + half _597 = max(_364, _595); + half _598 = max(_365, _596); + half _599 = max(_444, _504); + half _600 = max(_445, _505); + half _601 = max(_597, _599); + half _602 = max(_598, _600); + half _603 = min(_464, _484); + half _604 = min(_465, _485); + half _605 = min(_404, _603); + half _606 = min(_405, _604); + half _607 = min(_504, _544); + half _608 = min(_505, _545); + half _609 = min(_605, _607); + half _610 = min(_606, _608); + half _611 = max(_464, _484); + half _612 = max(_465, _485); + half _613 = max(_404, _611); + half _614 = max(_405, _612); + half _615 = max(_504, _544); + half _616 = max(_505, _545); + half _617 = max(_613, _615); + half _618 = max(_614, _616); + half _619 = min(_484, _504); + half _620 = min(_485, _505); + half _621 = min(_424, _619); + half _622 = min(_425, _620); + half _623 = min(_524, _564); + half _624 = min(_525, _565); + half _625 = min(_621, _623); + half _626 = min(_622, _624); + half _627 = max(_484, _504); + half _628 = max(_485, _505); + half _629 = max(_424, _627); + half _630 = max(_425, _628); + half _631 = max(_524, _564); + half _632 = max(_525, _565); + half _633 = max(_629, _631); + half _634 = max(_630, _632); + + half _644 = 1.h - _585; + half _645 = 1.h - _586; + half _653 = 1.h - _601; + half _654 = 1.h - _602; + half _661 = 1.h - _617; + half _662 = 1.h - _618; + half _669 = 1.h - _633; + half _670 = 1.h - _634; + + half _648 = min(_577, _644) * (1.h / _585); + half _649 = min(_578, _645) * (1.h / _586); + half _657 = min(_593, _653) * (1.h / _601); + half _658 = min(_594, _654) * (1.h / _602); + half _665 = min(_609, _661) * (1.h / _617); + half _666 = min(_610, _662) * (1.h / _618); + half _673 = min(_625, _669) * (1.h / _633); + half _674 = min(_626, _670) * (1.h / _634); + + half _2515 = max(_648, 0.h); + half _2526 = max(_649, 0.h); + half _2547 = max(_657, 0.h); + half _2558 = max(_658, 0.h); + half _2579 = max(_665, 0.h); + half _2590 = max(_666, 0.h); + half _2611 = max(_673, 0.h); + half _2622 = max(_674, 0.h); + + half hSharp = f16tof32(_69.sharpAsHalf & 0xFFFF); + + half _699 = 1.h - _91; + half _700 = 1.h - _208; + half _701 = 1.h - _93; + + half _717 = (_699 * _701) * (1.h / ((0.03125h - _577) + _585)); + half _718 = (_700 * _701) * (1.h / ((0.03125h - _578) + _586)); + half _725 = ( _91 * _701) * (1.h / ((0.03125h - _593) + _601)); + half _726 = (_208 * _701) * (1.h / ((0.03125h - _594) + _602)); + half _733 = (_699 * _93) * (1.h / ((0.03125h - _609) + _617)); + half _734 = (_700 * _93) * (1.h / ((0.03125h - _610) + _618)); + half _741 = ( _91 * _93) * (1.h / ((0.03125h - _625) + _633)); + half _742 = (_208 * _93) * (1.h / ((0.03125h - _626) + _634)); + + half _743 = _717 * (hSharp * sqrt(min(_2515, 1.h))); + half _744 = _718 * (hSharp * sqrt(min(_2526, 1.h))); + half _745 = _725 * (hSharp * sqrt(min(_2547, 1.h))); + half _746 = _726 * (hSharp * sqrt(min(_2558, 1.h))); + half _747 = _733 * (hSharp * sqrt(min(_2579, 1.h))); + half _748 = _734 * (hSharp * sqrt(min(_2590, 1.h))); + half _753 = _741 * (hSharp * sqrt(min(_2611, 1.h))); + half _754 = _742 * (hSharp * sqrt(min(_2622, 1.h))); + half _750 = (_745 + _717) + _747; half _752 = (_746 + _718) + _748; - half _753 = _741 * (_690 * sqrt(isnan(half(1.0)) ? _2611 : (isnan(_2611) ? half(1.0) : min(_2611, half(1.0))))); - half _754 = _742 * (_690 * sqrt(isnan(half(1.0)) ? _2622 : (isnan(_2622) ? half(1.0) : min(_2622, half(1.0))))); half _756 = (_725 + _743) + _753; half _758 = (_726 + _744) + _754; half _760 = (_733 + _743) + _753; half _762 = (_734 + _744) + _754; half _765 = (_747 + _745) + _741; half _766 = (_748 + _746) + _742; - half _784 = half(1.0) / ((((_765 + _750) + ((((_745 + _743) + _747) + _753) * half(2.0))) + _756) + _760); - half _785 = half(1.0) / ((((_766 + _752) + ((((_746 + _744) + _748) + _754) * half(2.0))) + _758) + _762); - half _803 = ((((((_752 * exp2(_76 * log2((half(_237.x) + half(0.05499267578125)) * half(0.9482421875)))) + (_744 * (exp2(_76 * log2((half(_229.x) + half(0.05499267578125)) * half(0.9482421875))) + exp2(_76 * log2((half(_215.x) + half(0.05499267578125)) * half(0.9482421875)))))) + (_766 * exp2(_76 * log2((half(_305.x) + half(0.05499267578125)) * half(0.9482421875))))) + (_758 * exp2(_76 * log2((half(_259.x) + half(0.05499267578125)) * half(0.9482421875))))) + (_762 * exp2(_76 * log2((half(_289.x) + half(0.05499267578125)) * half(0.9482421875))))) + (_754 * (exp2(_76 * log2((half(_321.x) + half(0.05499267578125)) * half(0.9482421875))) + exp2(_76 * log2((half(_313.x) + half(0.05499267578125)) * half(0.9482421875)))))) + (_748 * (exp2(_76 * log2((half(_297.x) + half(0.05499267578125)) * half(0.9482421875))) + exp2(_76 * log2((half(_281.x) + half(0.05499267578125)) * half(0.9482421875))))); - half _805 = (_803 + (_746 * (exp2(_76 * log2((half(_273.x) + half(0.05499267578125)) * half(0.9482421875))) + exp2(_76 * log2((half(_251.x) + half(0.05499267578125)) * half(0.9482421875)))))) * _785; - half _2633 = isnan(half(0.0)) ? _805 : (isnan(_805) ? half(0.0) : max(_805, half(0.0))); - half _826 = ((((((((_752 * _405) + (_744 * (_385 + _345))) + (_766 * _505)) + (_758 * _425)) + (_762 * _485)) + (_754 * (_565 + _525))) + (_748 * (_545 + _465))) + (_746 * (_445 + _365))) * _785; - half _2644 = isnan(half(0.0)) ? _826 : (isnan(_826) ? half(0.0) : max(_826, half(0.0))); - half _845 = ((((((_752 * exp2(log2((half(_237.z) + half(0.05499267578125)) * half(0.9482421875)) * _76)) + (_744 * (exp2(log2((half(_229.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_215.z) + half(0.05499267578125)) * half(0.9482421875)) * _76)))) + (_766 * exp2(log2((half(_305.z) + half(0.05499267578125)) * half(0.9482421875)) * _76))) + (_758 * exp2(log2((half(_259.z) + half(0.05499267578125)) * half(0.9482421875)) * _76))) + (_762 * exp2(log2((half(_289.z) + half(0.05499267578125)) * half(0.9482421875)) * _76))) + (_754 * (exp2(log2((half(_321.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_313.z) + half(0.05499267578125)) * half(0.9482421875)) * _76)))) + (_748 * (exp2(log2((half(_297.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_281.z) + half(0.05499267578125)) * half(0.9482421875)) * _76))); - half _847 = (_845 + (_746 * (exp2(log2((half(_273.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_251.z) + half(0.05499267578125)) * half(0.9482421875)) * _76)))) * _785; - half _2655 = isnan(half(0.0)) ? _847 : (isnan(_847) ? half(0.0) : max(_847, half(0.0))); - half _849 = isnan(half(0.00100040435791015625)) ? _76 : (isnan(_76) ? half(0.00100040435791015625) : max(_76, half(0.00100040435791015625))); - uint4 _853 = asuint(_23_m0[2u]); - if ((_58 <= _853.z) && (_59 <= _853.w)) + + half _784 = 1.h / ((((_765 + _750) + ((((_745 + _743) + _747) + _753) * 2.h)) + _756) + _760); + half _785 = 1.h / ((((_766 + _752) + ((((_746 + _744) + _748) + _754) * 2.h)) + _758) + _762); + + half3 _803 = ((((((((_752 * _237) + (_744 * (_229 + _215))) + (_766 * _305)) + (_758 * _259)) + (_762 * _289)) + (_754 * (_321 + _313))) + (_748 * (_297 + _281))) + (_746 * (_273 + _251))) * _785; + half3 colorOut2 = saturate(_803); + + colorOut2 = PrepareForOutput(colorOut2); + + if ((_58 <= _55.z) && (_59 <= _55.w)) { - half _961 = ((((((exp2(log2((half(_138.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) * _750) + ((exp2(log2((half(_134.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_118.z) + half(0.05499267578125)) * half(0.9482421875)) * _76)) * _743)) + (exp2(log2((half(_153.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) * _756)) + (exp2(log2((half(_178.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) * _760)) + (exp2(log2((half(_192.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) * _765)) + ((exp2(log2((half(_200.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_196.z) + half(0.05499267578125)) * half(0.9482421875)) * _76)) * _753)) + ((exp2(log2((half(_188.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_174.z) + half(0.05499267578125)) * half(0.9482421875)) * _76)) * _747); - half _963 = (_961 + ((exp2(log2((half(_164.z) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_149.z) + half(0.05499267578125)) * half(0.9482421875)) * _76)) * _745)) * _784; - half _2671 = isnan(half(0.0)) ? _963 : (isnan(_963) ? half(0.0) : max(_963, half(0.0))); - half _966 = half(1.0) / _849; - half _971 = (exp2(_966 * log2(isnan(half(1.0)) ? _2671 : (isnan(_2671) ? half(1.0) : min(_2671, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); - half _994 = ((((((((_750 * _404) + (_743 * (_384 + _344))) + (_765 * _504)) + (_756 * _424)) + (_760 * _484)) + (_753 * (_564 + _524))) + (_747 * (_544 + _464))) + (_745 * (_444 + _364))) * _784; - half _2687 = isnan(half(0.0)) ? _994 : (isnan(_994) ? half(0.0) : max(_994, half(0.0))); - half _1000 = (exp2(_966 * log2(isnan(half(1.0)) ? _2687 : (isnan(_2687) ? half(1.0) : min(_2687, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); - half _1104 = ((((((exp2(log2((half(_138.x) + half(0.05499267578125)) * half(0.9482421875)) * _76) * _750) + ((exp2(log2((half(_134.x) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_118.x) + half(0.05499267578125)) * half(0.9482421875)) * _76)) * _743)) + (exp2(log2((half(_153.x) + half(0.05499267578125)) * half(0.9482421875)) * _76) * _756)) + (exp2(log2((half(_178.x) + half(0.05499267578125)) * half(0.9482421875)) * _76) * _760)) + (exp2(log2((half(_192.x) + half(0.05499267578125)) * half(0.9482421875)) * _76) * _765)) + ((exp2(log2((half(_200.x) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_196.x) + half(0.05499267578125)) * half(0.9482421875)) * _76)) * _753)) + ((exp2(log2((half(_188.x) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_174.x) + half(0.05499267578125)) * half(0.9482421875)) * _76)) * _747); - half _1106 = (_1104 + ((exp2(log2((half(_164.x) + half(0.05499267578125)) * half(0.9482421875)) * _76) + exp2(log2((half(_149.x) + half(0.05499267578125)) * half(0.9482421875)) * _76)) * _745)) * _784; - half _2703 = isnan(half(0.0)) ? _1106 : (isnan(_1106) ? half(0.0) : max(_1106, half(0.0))); - half _1112 = (exp2(_966 * log2(isnan(half(1.0)) ? _2703 : (isnan(_2703) ? half(1.0) : min(_2703, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); - _11[uint2(_58, _59)] = float4(float(isnan(half(0.0)) ? _1112 : (isnan(_1112) ? half(0.0) : max(_1112, half(0.0)))), float(isnan(half(0.0)) ? _1000 : (isnan(_1000) ? half(0.0) : max(_1000, half(0.0)))), float(isnan(half(0.0)) ? _971 : (isnan(_971) ? half(0.0) : max(_971, half(0.0)))), 1.0f); + half3 _961 = ((((((((_138 * _750) + ((_134 + _118) * _743)) + (_153 * _756)) + (_178 * _760)) + (_192 * _765)) + ((_200 + _196) * _753)) + ((_188 + _174) * _747)) + ((_164 + _149) * _745)) * _784; + half3 colorOut1 = saturate(_961); + + colorOut1 = PrepareForOutput(colorOut1); + + ColorOut[uint2(_58, _59)] = float4(float3(colorOut1), 1.f); } - uint _1119 = _58 + 8u; - uint4 _1122 = asuint(_23_m0[2u]); - if ((_1119 <= _1122.z) && (_59 <= _1122.w)) + + uint _1119 = _58 + 8; + + if ((_1119 <= _55.z) && (_59 <= _55.w)) { - half _1129 = half(1.0) / _849; - half _1133 = (exp2(_1129 * log2(isnan(half(1.0)) ? _2655 : (isnan(_2655) ? half(1.0) : min(_2655, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); - half _1140 = (exp2(_1129 * log2(isnan(half(1.0)) ? _2644 : (isnan(_2644) ? half(1.0) : min(_2644, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); - half _1147 = (exp2(_1129 * log2(isnan(half(1.0)) ? _2633 : (isnan(_2633) ? half(1.0) : min(_2633, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); - _11[uint2(_1119, _59)] = float4(float(isnan(half(0.0)) ? _1147 : (isnan(_1147) ? half(0.0) : max(_1147, half(0.0)))), float(isnan(half(0.0)) ? _1140 : (isnan(_1140) ? half(0.0) : max(_1140, half(0.0)))), float(isnan(half(0.0)) ? _1133 : (isnan(_1133) ? half(0.0) : max(_1133, half(0.0)))), 1.0f); + ColorOut[uint2(_1119, _59)] = float4(float3(colorOut2), 1.f); } - uint _1153 = _59 + 8u; - float _1156 = (float(_1153) * _80) + _84; - float _1157 = floor(_1156); - half _1159 = half(_1156 - _1157); - uint16_t _1160 = (int16_t(_1157)); - uint4 _1164 = asuint(_23_m0[3u]); - uint _1165 = _1164.x; - uint _1166 = _1164.y; - uint _1167 = _1164.z; - uint _1168 = _1164.w; - uint _1175 = uint(int(max(min(_106, _1167), _1165) << 16u) >> int(16u)); - uint _1177 = uint(int(max(min(uint((_1160 + 65535u)), _1168), _1166) << 16u) >> int(16u)); - float4 _1179 = _8.Load(int3(uint2(_1175, _1177), 0u)); - uint _1189 = uint(int(max(min(_124, _1167), _1165) << 16u) >> int(16u)); - uint _1191 = uint(int(max(min(uint(_1160), _1168), _1166) << 16u) >> int(16u)); - float4 _1192 = _8.Load(int3(uint2(_1189, _1191), 0u)); - float4 _1196 = _8.Load(int3(uint2(_1175, _1191), 0u)); - uint _1203 = uint(int(max(min(_144, _1167), _1165) << 16u) >> int(16u)); - float4 _1204 = _8.Load(int3(uint2(_1203, _1177), 0u)); - float4 _1208 = _8.Load(int3(uint2(_1203, _1191), 0u)); - uint _1215 = uint(int(max(min(_159, _1167), _1165) << 16u) >> int(16u)); - float4 _1216 = _8.Load(int3(uint2(_1215, _1191), 0u)); - uint _1225 = uint(int(max(min(uint((_1160 + 1u)), _1168), _1166) << 16u) >> int(16u)); - float4 _1226 = _8.Load(int3(uint2(_1189, _1225), 0u)); - float4 _1230 = _8.Load(int3(uint2(_1175, _1225), 0u)); - uint _1239 = uint(int(max(min(uint((_1160 + 2u)), _1168), _1166) << 16u) >> int(16u)); - float4 _1240 = _8.Load(int3(uint2(_1175, _1239), 0u)); - float4 _1244 = _8.Load(int3(uint2(_1203, _1225), 0u)); - float4 _1248 = _8.Load(int3(uint2(_1215, _1225), 0u)); - float4 _1252 = _8.Load(int3(uint2(_1203, _1239), 0u)); - uint _1259 = uint(int(max(min(_210, _1167), _1165) << 16u) >> int(16u)); - float4 _1260 = _8.Load(int3(uint2(_1259, _1177), 0u)); - uint _1271 = uint(int(max(min(_224, _1167), _1165) << 16u) >> int(16u)); - float4 _1272 = _8.Load(int3(uint2(_1271, _1191), 0u)); - float4 _1280 = _8.Load(int3(uint2(_1259, _1191), 0u)); - uint _1291 = uint(int(max(min(_246, _1167), _1165) << 16u) >> int(16u)); - float4 _1292 = _8.Load(int3(uint2(_1291, _1177), 0u)); - float4 _1300 = _8.Load(int3(uint2(_1291, _1191), 0u)); - uint _1311 = uint(int(max(min(_268, _1167), _1165) << 16u) >> int(16u)); - float4 _1312 = _8.Load(int3(uint2(_1311, _1191), 0u)); - float4 _1320 = _8.Load(int3(uint2(_1271, _1225), 0u)); - float4 _1328 = _8.Load(int3(uint2(_1259, _1225), 0u)); - float4 _1336 = _8.Load(int3(uint2(_1259, _1239), 0u)); - float4 _1344 = _8.Load(int3(uint2(_1291, _1225), 0u)); - float4 _1352 = _8.Load(int3(uint2(_1311, _1225), 0u)); - float4 _1360 = _8.Load(int3(uint2(_1291, _1239), 0u)); - half _1371 = half(_18_m0[1u].w); - half _1385 = exp2(log2((half(_1179.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1386 = exp2(log2((half(_1260.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1405 = exp2(log2((half(_1204.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1406 = exp2(log2((half(_1292.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1425 = exp2(log2((half(_1192.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1426 = exp2(log2((half(_1272.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1445 = exp2(log2((half(_1196.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1446 = exp2(log2((half(_1280.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1465 = exp2(log2((half(_1208.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1466 = exp2(log2((half(_1300.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1485 = exp2(log2((half(_1216.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1486 = exp2(log2((half(_1312.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1505 = exp2(log2((half(_1226.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1506 = exp2(log2((half(_1320.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1525 = exp2(log2((half(_1230.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1526 = exp2(log2((half(_1328.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1545 = exp2(log2((half(_1244.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1546 = exp2(log2((half(_1344.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1565 = exp2(log2((half(_1248.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1566 = exp2(log2((half(_1352.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1585 = exp2(log2((half(_1240.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1586 = exp2(log2((half(_1336.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1605 = exp2(log2((half(_1252.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1606 = exp2(log2((half(_1360.y) + half(0.05499267578125)) * half(0.9482421875)) * _1371); - half _1612 = isnan(_1445) ? _1425 : (isnan(_1425) ? _1445 : min(_1425, _1445)); - half _1613 = isnan(_1446) ? _1426 : (isnan(_1426) ? _1446 : min(_1426, _1446)); - half _1614 = isnan(_1612) ? _1385 : (isnan(_1385) ? _1612 : min(_1385, _1612)); - half _1615 = isnan(_1613) ? _1386 : (isnan(_1386) ? _1613 : min(_1386, _1613)); - half _1616 = isnan(_1525) ? _1465 : (isnan(_1465) ? _1525 : min(_1465, _1525)); - half _1617 = isnan(_1526) ? _1466 : (isnan(_1466) ? _1526 : min(_1466, _1526)); - half _1618 = isnan(_1616) ? _1614 : (isnan(_1614) ? _1616 : min(_1614, _1616)); - half _1619 = isnan(_1617) ? _1615 : (isnan(_1615) ? _1617 : min(_1615, _1617)); - half _1620 = isnan(_1445) ? _1425 : (isnan(_1425) ? _1445 : max(_1425, _1445)); - half _1621 = isnan(_1446) ? _1426 : (isnan(_1426) ? _1446 : max(_1426, _1446)); - half _1622 = isnan(_1620) ? _1385 : (isnan(_1385) ? _1620 : max(_1385, _1620)); - half _1623 = isnan(_1621) ? _1386 : (isnan(_1386) ? _1621 : max(_1386, _1621)); - half _1624 = isnan(_1525) ? _1465 : (isnan(_1465) ? _1525 : max(_1465, _1525)); - half _1625 = isnan(_1526) ? _1466 : (isnan(_1466) ? _1526 : max(_1466, _1526)); - half _1626 = isnan(_1624) ? _1622 : (isnan(_1622) ? _1624 : max(_1622, _1624)); - half _1627 = isnan(_1625) ? _1623 : (isnan(_1623) ? _1625 : max(_1623, _1625)); - half _1628 = isnan(_1465) ? _1445 : (isnan(_1445) ? _1465 : min(_1445, _1465)); - half _1629 = isnan(_1466) ? _1446 : (isnan(_1446) ? _1466 : min(_1446, _1466)); - half _1630 = isnan(_1628) ? _1405 : (isnan(_1405) ? _1628 : min(_1405, _1628)); - half _1631 = isnan(_1629) ? _1406 : (isnan(_1406) ? _1629 : min(_1406, _1629)); - half _1632 = isnan(_1545) ? _1485 : (isnan(_1485) ? _1545 : min(_1485, _1545)); - half _1633 = isnan(_1546) ? _1486 : (isnan(_1486) ? _1546 : min(_1486, _1546)); - half _1634 = isnan(_1632) ? _1630 : (isnan(_1630) ? _1632 : min(_1630, _1632)); - half _1635 = isnan(_1633) ? _1631 : (isnan(_1631) ? _1633 : min(_1631, _1633)); - half _1636 = isnan(_1465) ? _1445 : (isnan(_1445) ? _1465 : max(_1445, _1465)); - half _1637 = isnan(_1466) ? _1446 : (isnan(_1446) ? _1466 : max(_1446, _1466)); - half _1638 = isnan(_1636) ? _1405 : (isnan(_1405) ? _1636 : max(_1405, _1636)); - half _1639 = isnan(_1637) ? _1406 : (isnan(_1406) ? _1637 : max(_1406, _1637)); - half _1640 = isnan(_1545) ? _1485 : (isnan(_1485) ? _1545 : max(_1485, _1545)); - half _1641 = isnan(_1546) ? _1486 : (isnan(_1486) ? _1546 : max(_1486, _1546)); - half _1642 = isnan(_1640) ? _1638 : (isnan(_1638) ? _1640 : max(_1638, _1640)); - half _1643 = isnan(_1641) ? _1639 : (isnan(_1639) ? _1641 : max(_1639, _1641)); - half _1644 = isnan(_1525) ? _1505 : (isnan(_1505) ? _1525 : min(_1505, _1525)); - half _1645 = isnan(_1526) ? _1506 : (isnan(_1506) ? _1526 : min(_1506, _1526)); - half _1646 = isnan(_1644) ? _1445 : (isnan(_1445) ? _1644 : min(_1445, _1644)); - half _1647 = isnan(_1645) ? _1446 : (isnan(_1446) ? _1645 : min(_1446, _1645)); - half _1648 = isnan(_1585) ? _1545 : (isnan(_1545) ? _1585 : min(_1545, _1585)); - half _1649 = isnan(_1586) ? _1546 : (isnan(_1546) ? _1586 : min(_1546, _1586)); - half _1650 = isnan(_1648) ? _1646 : (isnan(_1646) ? _1648 : min(_1646, _1648)); - half _1651 = isnan(_1649) ? _1647 : (isnan(_1647) ? _1649 : min(_1647, _1649)); - half _1652 = isnan(_1525) ? _1505 : (isnan(_1505) ? _1525 : max(_1505, _1525)); - half _1653 = isnan(_1526) ? _1506 : (isnan(_1506) ? _1526 : max(_1506, _1526)); - half _1654 = isnan(_1652) ? _1445 : (isnan(_1445) ? _1652 : max(_1445, _1652)); - half _1655 = isnan(_1653) ? _1446 : (isnan(_1446) ? _1653 : max(_1446, _1653)); - half _1656 = isnan(_1585) ? _1545 : (isnan(_1545) ? _1585 : max(_1545, _1585)); - half _1657 = isnan(_1586) ? _1546 : (isnan(_1546) ? _1586 : max(_1546, _1586)); - half _1658 = isnan(_1656) ? _1654 : (isnan(_1654) ? _1656 : max(_1654, _1656)); - half _1659 = isnan(_1657) ? _1655 : (isnan(_1655) ? _1657 : max(_1655, _1657)); - half _1660 = isnan(_1545) ? _1525 : (isnan(_1525) ? _1545 : min(_1525, _1545)); - half _1661 = isnan(_1546) ? _1526 : (isnan(_1526) ? _1546 : min(_1526, _1546)); - half _1662 = isnan(_1660) ? _1465 : (isnan(_1465) ? _1660 : min(_1465, _1660)); - half _1663 = isnan(_1661) ? _1466 : (isnan(_1466) ? _1661 : min(_1466, _1661)); - half _1664 = isnan(_1605) ? _1565 : (isnan(_1565) ? _1605 : min(_1565, _1605)); - half _1665 = isnan(_1606) ? _1566 : (isnan(_1566) ? _1606 : min(_1566, _1606)); - half _1666 = isnan(_1664) ? _1662 : (isnan(_1662) ? _1664 : min(_1662, _1664)); - half _1667 = isnan(_1665) ? _1663 : (isnan(_1663) ? _1665 : min(_1663, _1665)); - half _1668 = isnan(_1545) ? _1525 : (isnan(_1525) ? _1545 : max(_1525, _1545)); - half _1669 = isnan(_1546) ? _1526 : (isnan(_1526) ? _1546 : max(_1526, _1546)); - half _1670 = isnan(_1668) ? _1465 : (isnan(_1465) ? _1668 : max(_1465, _1668)); - half _1671 = isnan(_1669) ? _1466 : (isnan(_1466) ? _1669 : max(_1466, _1669)); - half _1672 = isnan(_1605) ? _1565 : (isnan(_1565) ? _1605 : max(_1565, _1605)); - half _1673 = isnan(_1606) ? _1566 : (isnan(_1566) ? _1606 : max(_1566, _1606)); - half _1674 = isnan(_1672) ? _1670 : (isnan(_1670) ? _1672 : max(_1670, _1672)); - half _1675 = isnan(_1673) ? _1671 : (isnan(_1671) ? _1673 : max(_1671, _1673)); - half _1684 = half(1.0) - _1626; - half _1685 = half(1.0) - _1627; - half _1688 = (isnan(_1684) ? _1618 : (isnan(_1618) ? _1684 : min(_1618, _1684))) * (half(1.0) / _1626); - half _1689 = (isnan(_1685) ? _1619 : (isnan(_1619) ? _1685 : min(_1619, _1685))) * (half(1.0) / _1627); - half _3064 = isnan(half(0.0)) ? _1688 : (isnan(_1688) ? half(0.0) : max(_1688, half(0.0))); - half _3075 = isnan(half(0.0)) ? _1689 : (isnan(_1689) ? half(0.0) : max(_1689, half(0.0))); - half _1692 = half(1.0) - _1642; - half _1693 = half(1.0) - _1643; - half _1696 = (isnan(_1692) ? _1634 : (isnan(_1634) ? _1692 : min(_1634, _1692))) * (half(1.0) / _1642); - half _1697 = (isnan(_1693) ? _1635 : (isnan(_1635) ? _1693 : min(_1635, _1693))) * (half(1.0) / _1643); - half _3096 = isnan(half(0.0)) ? _1696 : (isnan(_1696) ? half(0.0) : max(_1696, half(0.0))); - half _3107 = isnan(half(0.0)) ? _1697 : (isnan(_1697) ? half(0.0) : max(_1697, half(0.0))); - half _1700 = half(1.0) - _1658; - half _1701 = half(1.0) - _1659; - half _1704 = (isnan(_1700) ? _1650 : (isnan(_1650) ? _1700 : min(_1650, _1700))) * (half(1.0) / _1658); - half _1705 = (isnan(_1701) ? _1651 : (isnan(_1651) ? _1701 : min(_1651, _1701))) * (half(1.0) / _1659); - half _3128 = isnan(half(0.0)) ? _1704 : (isnan(_1704) ? half(0.0) : max(_1704, half(0.0))); - half _3139 = isnan(half(0.0)) ? _1705 : (isnan(_1705) ? half(0.0) : max(_1705, half(0.0))); - half _1708 = half(1.0) - _1674; - half _1709 = half(1.0) - _1675; - half _1712 = (isnan(_1708) ? _1666 : (isnan(_1666) ? _1708 : min(_1666, _1708))) * (half(1.0) / _1674); - half _1713 = (isnan(_1709) ? _1667 : (isnan(_1667) ? _1709 : min(_1667, _1709))) * (half(1.0) / _1675); - half _3160 = isnan(half(0.0)) ? _1712 : (isnan(_1712) ? half(0.0) : max(_1712, half(0.0))); - half _3171 = isnan(half(0.0)) ? _1713 : (isnan(_1713) ? half(0.0) : max(_1713, half(0.0))); - half _1732 = half(1.0) - _1159; - half _1747 = (_1732 * _699) * (half(1.0) / ((half(0.03125) - _1618) + _1626)); - half _1748 = (_700 * _1732) * (half(1.0) / ((half(0.03125) - _1619) + _1627)); - half _1755 = (_1732 * _91) * (half(1.0) / ((half(0.03125) - _1634) + _1642)); - half _1756 = (_208 * _1732) * (half(1.0) / ((half(0.03125) - _1635) + _1643)); - half _1763 = (_699 * _1159) * (half(1.0) / ((half(0.03125) - _1650) + _1658)); - half _1764 = (_700 * _1159) * (half(1.0) / ((half(0.03125) - _1651) + _1659)); - half _1771 = (_1159 * _91) * (half(1.0) / ((half(0.03125) - _1666) + _1674)); - half _1772 = (_208 * _1159) * (half(1.0) / ((half(0.03125) - _1667) + _1675)); - half _1773 = (_690 * sqrt(isnan(half(1.0)) ? _3064 : (isnan(_3064) ? half(1.0) : min(_3064, half(1.0))))) * _1747; - half _1774 = _1748 * (_690 * sqrt(isnan(half(1.0)) ? _3075 : (isnan(_3075) ? half(1.0) : min(_3075, half(1.0))))); - half _1775 = _1755 * (_690 * sqrt(isnan(half(1.0)) ? _3096 : (isnan(_3096) ? half(1.0) : min(_3096, half(1.0))))); - half _1776 = _1756 * (_690 * sqrt(isnan(half(1.0)) ? _3107 : (isnan(_3107) ? half(1.0) : min(_3107, half(1.0))))); - half _1777 = _1763 * (_690 * sqrt(isnan(half(1.0)) ? _3128 : (isnan(_3128) ? half(1.0) : min(_3128, half(1.0))))); - half _1778 = _1764 * (_690 * sqrt(isnan(half(1.0)) ? _3139 : (isnan(_3139) ? half(1.0) : min(_3139, half(1.0))))); + + uint _1153 = _59 + 8; + + float _1156 = (float(_1153) * _62.rcpScalingFactorY) + _62.otherScalingFactorY; + + half _1159 = frac(_1156); + + uint _1160 = uint(_1156); + + uint _1177 = max(min(_1160 - 1, _105), _103); + uint _1191 = max(min(_1160, _105), _103); + uint _1225 = max(min(_1160 + 1, _105), _103); + uint _1239 = max(min(_1160 + 2, _105), _103); + + half3 _1179 = ColorIn.Load(int3(_114, _1177, 0)).rgb; + half3 _1192 = ColorIn.Load(int3(_131, _1191, 0)).rgb; + half3 _1196 = ColorIn.Load(int3(_114, _1191, 0)).rgb; + half3 _1204 = ColorIn.Load(int3(_148, _1177, 0)).rgb; + half3 _1208 = ColorIn.Load(int3(_148, _1191, 0)).rgb; + half3 _1216 = ColorIn.Load(int3(_163, _1191, 0)).rgb; + half3 _1226 = ColorIn.Load(int3(_131, _1225, 0)).rgb; + half3 _1230 = ColorIn.Load(int3(_114, _1225, 0)).rgb; + half3 _1240 = ColorIn.Load(int3(_114, _1239, 0)).rgb; + half3 _1244 = ColorIn.Load(int3(_148, _1225, 0)).rgb; + half3 _1248 = ColorIn.Load(int3(_163, _1225, 0)).rgb; + half3 _1252 = ColorIn.Load(int3(_148, _1239, 0)).rgb; + half3 _1260 = ColorIn.Load(int3(_214, _1177, 0)).rgb; + half3 _1272 = ColorIn.Load(int3(_228, _1191, 0)).rgb; + half3 _1280 = ColorIn.Load(int3(_214, _1191, 0)).rgb; + half3 _1292 = ColorIn.Load(int3(_250, _1177, 0)).rgb; + half3 _1300 = ColorIn.Load(int3(_250, _1191, 0)).rgb; + half3 _1312 = ColorIn.Load(int3(_272, _1191, 0)).rgb; + half3 _1320 = ColorIn.Load(int3(_228, _1225, 0)).rgb; + half3 _1328 = ColorIn.Load(int3(_214, _1225, 0)).rgb; + half3 _1336 = ColorIn.Load(int3(_214, _1239, 0)).rgb; + half3 _1344 = ColorIn.Load(int3(_250, _1225, 0)).rgb; + half3 _1352 = ColorIn.Load(int3(_272, _1225, 0)).rgb; + half3 _1360 = ColorIn.Load(int3(_250, _1239, 0)).rgb; + + _1179 = PrepareForProcessing(_1179); + _1192 = PrepareForProcessing(_1192); + _1196 = PrepareForProcessing(_1196); + _1204 = PrepareForProcessing(_1204); + _1208 = PrepareForProcessing(_1208); + _1216 = PrepareForProcessing(_1216); + _1226 = PrepareForProcessing(_1226); + _1230 = PrepareForProcessing(_1230); + _1240 = PrepareForProcessing(_1240); + _1244 = PrepareForProcessing(_1244); + _1248 = PrepareForProcessing(_1248); + _1252 = PrepareForProcessing(_1252); + _1260 = PrepareForProcessing(_1260); + _1272 = PrepareForProcessing(_1272); + _1280 = PrepareForProcessing(_1280); + _1292 = PrepareForProcessing(_1292); + _1300 = PrepareForProcessing(_1300); + _1312 = PrepareForProcessing(_1312); + _1320 = PrepareForProcessing(_1320); + _1328 = PrepareForProcessing(_1328); + _1336 = PrepareForProcessing(_1336); + _1344 = PrepareForProcessing(_1344); + _1352 = PrepareForProcessing(_1352); + _1360 = PrepareForProcessing(_1360); + + half _1385 = _1179.y; + half _1386 = _1260.y; + half _1405 = _1204.y; + half _1406 = _1292.y; + half _1425 = _1192.y; + half _1426 = _1272.y; + half _1445 = _1196.y; + half _1446 = _1280.y; + half _1465 = _1208.y; + half _1466 = _1300.y; + half _1485 = _1216.y; + half _1486 = _1312.y; + half _1505 = _1226.y; + half _1506 = _1320.y; + half _1525 = _1230.y; + half _1526 = _1328.y; + half _1545 = _1244.y; + half _1546 = _1344.y; + half _1565 = _1248.y; + half _1566 = _1352.y; + half _1585 = _1240.y; + half _1586 = _1336.y; + half _1605 = _1252.y; + half _1606 = _1360.y; + + half _1612 = min(_1425, _1445); + half _1613 = min(_1426, _1446); + half _1614 = min(_1385, _1612); + half _1615 = min(_1386, _1613); + half _1616 = min(_1465, _1525); + half _1617 = min(_1466, _1526); + half _1618 = min(_1614, _1616); + half _1619 = min(_1615, _1617); + half _1620 = max(_1425, _1445); + half _1621 = max(_1426, _1446); + half _1622 = max(_1385, _1620); + half _1623 = max(_1386, _1621); + half _1624 = max(_1465, _1525); + half _1625 = max(_1466, _1526); + half _1626 = max(_1622, _1624); + half _1627 = max(_1623, _1625); + half _1628 = min(_1445, _1465); + half _1629 = min(_1446, _1466); + half _1630 = min(_1405, _1628); + half _1631 = min(_1406, _1629); + half _1632 = min(_1485, _1545); + half _1633 = min(_1486, _1546); + half _1634 = min(_1630, _1632); + half _1635 = min(_1631, _1633); + half _1636 = max(_1445, _1465); + half _1637 = max(_1446, _1466); + half _1638 = max(_1405, _1636); + half _1639 = max(_1406, _1637); + half _1640 = max(_1485, _1545); + half _1641 = max(_1486, _1546); + half _1642 = max(_1638, _1640); + half _1643 = max(_1639, _1641); + half _1644 = min(_1505, _1525); + half _1645 = min(_1506, _1526); + half _1646 = min(_1445, _1644); + half _1647 = min(_1446, _1645); + half _1648 = min(_1545, _1585); + half _1649 = min(_1546, _1586); + half _1650 = min(_1646, _1648); + half _1651 = min(_1647, _1649); + half _1652 = max(_1505, _1525); + half _1653 = max(_1506, _1526); + half _1654 = max(_1445, _1652); + half _1655 = max(_1446, _1653); + half _1656 = max(_1545, _1585); + half _1657 = max(_1546, _1586); + half _1658 = max(_1654, _1656); + half _1659 = max(_1655, _1657); + half _1660 = min(_1525, _1545); + half _1661 = min(_1526, _1546); + half _1662 = min(_1465, _1660); + half _1663 = min(_1466, _1661); + half _1664 = min(_1565, _1605); + half _1665 = min(_1566, _1606); + half _1666 = min(_1662, _1664); + half _1667 = min(_1663, _1665); + half _1668 = max(_1525, _1545); + half _1669 = max(_1526, _1546); + half _1670 = max(_1465, _1668); + half _1671 = max(_1466, _1669); + half _1672 = max(_1565, _1605); + half _1673 = max(_1566, _1606); + half _1674 = max(_1670, _1672); + half _1675 = max(_1671, _1673); + + half _1684 = 1.h - _1626; + half _1685 = 1.h - _1627; + half _1692 = 1.h - _1642; + half _1693 = 1.h - _1643; + half _1700 = 1.h - _1658; + half _1701 = 1.h - _1659; + half _1708 = 1.h - _1674; + half _1709 = 1.h - _1675; + + half _1688 = min(_1618, _1684) * (1.h / _1626); + half _1689 = min(_1619, _1685) * (1.h / _1627); + half _1696 = min(_1634, _1692) * (1.h / _1642); + half _1697 = min(_1635, _1693) * (1.h / _1643); + half _1704 = min(_1650, _1700) * (1.h / _1658); + half _1705 = min(_1651, _1701) * (1.h / _1659); + half _1712 = min(_1666, _1708) * (1.h / _1674); + half _1713 = min(_1667, _1709) * (1.h / _1675); + + half _3064 = max(_1688, 0.h); + half _3075 = max(_1689, 0.h); + half _3096 = max(_1696, 0.h); + half _3107 = max(_1697, 0.h); + half _3128 = max(_1704, 0.h); + half _3139 = max(_1705, 0.h); + half _3160 = max(_1712, 0.h); + half _3171 = max(_1713, 0.h); + + half _1732 = 1.h - _1159; + + half _1747 = ( _699 * _1732) * (1.h / ((0.03125h - _1618) + _1626)); + half _1748 = ( _700 * _1732) * (1.h / ((0.03125h - _1619) + _1627)); + half _1755 = ( _91 * _1732) * (1.h / ((0.03125h - _1634) + _1642)); + half _1756 = ( _208 * _1732) * (1.h / ((0.03125h - _1635) + _1643)); + half _1763 = ( _699 * _1159) * (1.h / ((0.03125h - _1650) + _1658)); + half _1764 = ( _700 * _1159) * (1.h / ((0.03125h - _1651) + _1659)); + half _1771 = ( _91 * _1159) * (1.h / ((0.03125h - _1666) + _1674)); + half _1772 = ( _208 * _1159) * (1.h / ((0.03125h - _1667) + _1675)); + + half _1773 = _1747 * (hSharp * sqrt(min(_3064, 1.h))); + half _1774 = _1748 * (hSharp * sqrt(min(_3075, 1.h))); + half _1775 = _1755 * (hSharp * sqrt(min(_3096, 1.h))); + half _1776 = _1756 * (hSharp * sqrt(min(_3107, 1.h))); + half _1777 = _1763 * (hSharp * sqrt(min(_3128, 1.h))); + half _1778 = _1764 * (hSharp * sqrt(min(_3139, 1.h))); + half _1783 = _1771 * (hSharp * sqrt(min(_3160, 1.h))); + half _1784 = _1772 * (hSharp * sqrt(min(_3171, 1.h))); + half _1780 = (_1775 + _1747) + _1777; half _1782 = (_1776 + _1748) + _1778; - half _1783 = _1771 * (_690 * sqrt(isnan(half(1.0)) ? _3160 : (isnan(_3160) ? half(1.0) : min(_3160, half(1.0))))); - half _1784 = _1772 * (_690 * sqrt(isnan(half(1.0)) ? _3171 : (isnan(_3171) ? half(1.0) : min(_3171, half(1.0))))); half _1786 = (_1755 + _1773) + _1783; half _1788 = (_1756 + _1774) + _1784; half _1790 = (_1763 + _1773) + _1783; half _1792 = (_1764 + _1774) + _1784; half _1795 = (_1777 + _1775) + _1771; half _1796 = (_1778 + _1776) + _1772; - half _1813 = half(1.0) / ((((_1795 + _1780) + ((((_1775 + _1773) + _1777) + _1783) * half(2.0))) + _1786) + _1790); - half _1814 = half(1.0) / ((((_1796 + _1782) + ((((_1776 + _1774) + _1778) + _1784) * half(2.0))) + _1788) + _1792); - half _1832 = ((((((_1782 * exp2(_1371 * log2((half(_1280.x) + half(0.05499267578125)) * half(0.9482421875)))) + (_1774 * (exp2(_1371 * log2((half(_1272.x) + half(0.05499267578125)) * half(0.9482421875))) + exp2(_1371 * log2((half(_1260.x) + half(0.05499267578125)) * half(0.9482421875)))))) + (_1796 * exp2(_1371 * log2((half(_1344.x) + half(0.05499267578125)) * half(0.9482421875))))) + (_1788 * exp2(_1371 * log2((half(_1300.x) + half(0.05499267578125)) * half(0.9482421875))))) + (_1792 * exp2(_1371 * log2((half(_1328.x) + half(0.05499267578125)) * half(0.9482421875))))) + (_1784 * (exp2(_1371 * log2((half(_1360.x) + half(0.05499267578125)) * half(0.9482421875))) + exp2(_1371 * log2((half(_1352.x) + half(0.05499267578125)) * half(0.9482421875)))))) + (_1778 * (exp2(_1371 * log2((half(_1336.x) + half(0.05499267578125)) * half(0.9482421875))) + exp2(_1371 * log2((half(_1320.x) + half(0.05499267578125)) * half(0.9482421875))))); - half _1834 = (_1832 + (_1776 * (exp2(_1371 * log2((half(_1312.x) + half(0.05499267578125)) * half(0.9482421875))) + exp2(_1371 * log2((half(_1292.x) + half(0.05499267578125)) * half(0.9482421875)))))) * _1814; - half _3182 = isnan(half(0.0)) ? _1834 : (isnan(_1834) ? half(0.0) : max(_1834, half(0.0))); - half _1855 = ((((((((_1782 * _1446) + (_1774 * (_1426 + _1386))) + (_1796 * _1546)) + (_1788 * _1466)) + (_1792 * _1526)) + (_1784 * (_1606 + _1566))) + (_1778 * (_1586 + _1506))) + (_1776 * (_1486 + _1406))) * _1814; - half _3193 = isnan(half(0.0)) ? _1855 : (isnan(_1855) ? half(0.0) : max(_1855, half(0.0))); - half _1874 = ((((((_1782 * exp2(log2((half(_1280.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371)) + (_1774 * (exp2(log2((half(_1272.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1260.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371)))) + (_1796 * exp2(log2((half(_1344.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371))) + (_1788 * exp2(log2((half(_1300.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371))) + (_1792 * exp2(log2((half(_1328.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371))) + (_1784 * (exp2(log2((half(_1360.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1352.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371)))) + (_1778 * (exp2(log2((half(_1336.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1320.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371))); - half _1876 = (_1874 + (_1776 * (exp2(log2((half(_1312.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1292.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371)))) * _1814; - half _3204 = isnan(half(0.0)) ? _1876 : (isnan(_1876) ? half(0.0) : max(_1876, half(0.0))); - uint4 _1880 = asuint(_23_m0[2u]); - if ((_58 <= _1880.z) && (_1153 <= _1880.w)) + + half _1813 = 1.h / ((((_1795 + _1780) + ((((_1775 + _1773) + _1777) + _1783) * 2.h)) + _1786) + _1790); + half _1814 = 1.h / ((((_1796 + _1782) + ((((_1776 + _1774) + _1778) + _1784) * 2.h)) + _1788) + _1792); + + half3 _1832 = ((((((((_1782 * _1280) + (_1774 * (_1272 + _1260))) + (_1796 * _1344)) + (_1788 * _1300)) + (_1792 * _1328)) + (_1784 * (_1360 + _1352))) + (_1778 * (_1336 + _1320))) + (_1776 * (_1312 + _1292))) * _1814; + half3 colorOut4 = saturate(_1832); + + colorOut4 = PrepareForOutput(colorOut4); + + if ((_58 <= _55.z) && (_1153 <= _55.w)) { - half _1987 = ((((((exp2(log2((half(_1196.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) * _1780) + ((exp2(log2((half(_1192.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1179.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371)) * _1773)) + (exp2(log2((half(_1208.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) * _1786)) + (exp2(log2((half(_1230.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) * _1790)) + (exp2(log2((half(_1244.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) * _1795)) + ((exp2(log2((half(_1252.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1248.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371)) * _1783)) + ((exp2(log2((half(_1240.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1226.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371)) * _1777); - half _1989 = (_1987 + ((exp2(log2((half(_1216.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1204.z) + half(0.05499267578125)) * half(0.9482421875)) * _1371)) * _1775)) * _1813; - half _3215 = isnan(half(0.0)) ? _1989 : (isnan(_1989) ? half(0.0) : max(_1989, half(0.0))); - half _1992 = half(1.0) / _849; - half _1996 = (exp2(_1992 * log2(isnan(half(1.0)) ? _3215 : (isnan(_3215) ? half(1.0) : min(_3215, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); - half _2018 = ((((((((_1780 * _1445) + (_1773 * (_1425 + _1385))) + (_1795 * _1545)) + (_1786 * _1465)) + (_1790 * _1525)) + (_1783 * (_1605 + _1565))) + (_1777 * (_1585 + _1505))) + (_1775 * (_1485 + _1405))) * _1813; - half _3231 = isnan(half(0.0)) ? _2018 : (isnan(_2018) ? half(0.0) : max(_2018, half(0.0))); - half _2024 = (exp2(_1992 * log2(isnan(half(1.0)) ? _3231 : (isnan(_3231) ? half(1.0) : min(_3231, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); - half _2128 = ((((((exp2(log2((half(_1196.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371) * _1780) + ((exp2(log2((half(_1192.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1179.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371)) * _1773)) + (exp2(log2((half(_1208.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371) * _1786)) + (exp2(log2((half(_1230.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371) * _1790)) + (exp2(log2((half(_1244.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371) * _1795)) + ((exp2(log2((half(_1252.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1248.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371)) * _1783)) + ((exp2(log2((half(_1240.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1226.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371)) * _1777); - half _2130 = (_2128 + ((exp2(log2((half(_1216.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371) + exp2(log2((half(_1204.x) + half(0.05499267578125)) * half(0.9482421875)) * _1371)) * _1775)) * _1813; - half _3247 = isnan(half(0.0)) ? _2130 : (isnan(_2130) ? half(0.0) : max(_2130, half(0.0))); - half _2136 = (exp2(_1992 * log2(isnan(half(1.0)) ? _3247 : (isnan(_3247) ? half(1.0) : min(_3247, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); - _11[uint2(_58, _1153)] = float4(float(isnan(half(0.0)) ? _2136 : (isnan(_2136) ? half(0.0) : max(_2136, half(0.0)))), float(isnan(half(0.0)) ? _2024 : (isnan(_2024) ? half(0.0) : max(_2024, half(0.0)))), float(isnan(half(0.0)) ? _1996 : (isnan(_1996) ? half(0.0) : max(_1996, half(0.0)))), 1.0f); + half3 _1987 = ((((((((_1196 * _1780) + ((_1192 + _1179) * _1773)) + (_1208 * _1786)) + (_1230 * _1790)) + (_1244 * _1795)) + ((_1252 + _1248) * _1783)) + ((_1240 + _1226) * _1777)) + ((_1216 + _1204) * _1775)) * _1813; + half3 colorOut3 = saturate(_1987); + + colorOut3 = PrepareForOutput(colorOut3); + + ColorOut[uint2(_58, _1153)] = float4(float3(colorOut3), 1.f); } - uint4 _2144 = asuint(_23_m0[2u]); - if ((_1119 <= _2144.z) && (_1153 <= _2144.w)) + + if ((_1119 <= _55.z) && (_1153 <= _55.w)) { - half _2151 = half(1.0) / _849; - half _2155 = (exp2(_2151 * log2(isnan(half(1.0)) ? _3204 : (isnan(_3204) ? half(1.0) : min(_3204, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); - half _2162 = (exp2(_2151 * log2(isnan(half(1.0)) ? _3193 : (isnan(_3193) ? half(1.0) : min(_3193, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); - half _2169 = (exp2(_2151 * log2(isnan(half(1.0)) ? _3182 : (isnan(_3182) ? half(1.0) : min(_3182, half(1.0))))) * half(1.0546875)) + half(-0.05499267578125); - _11[uint2(_1119, _1153)] = float4(float(isnan(half(0.0)) ? _2169 : (isnan(_2169) ? half(0.0) : max(_2169, half(0.0)))), float(isnan(half(0.0)) ? _2162 : (isnan(_2162) ? half(0.0) : max(_2162, half(0.0)))), float(isnan(half(0.0)) ? _2155 : (isnan(_2155) ? half(0.0) : max(_2155, half(0.0)))), 1.0f); + ColorOut[uint2(_1119, _1153)] = float4(float3(colorOut4), 1.f); } -#elif defined(USE_PACKED_MATH) +#elif defined(USE_PACKED_MATH) \ + && !defined(USE_UPSCALING) #if 0 // Disable the CS so it just outputs the input uint2 _55 = CASData.cas2.xy; uint _58 = _55.x + (((csInput.SV_GroupThreadID.x >> 1u) & 7u) | (csInput.SV_GroupID.x << 4u)); diff --git a/shaders/compile_all_shaders.ps1 b/shaders/compile_all_shaders.ps1 index 570cd79b..873e9cbd 100644 --- a/shaders/compile_all_shaders.ps1 +++ b/shaders/compile_all_shaders.ps1 @@ -20,6 +20,7 @@ $main = #Compile-Shader -Type "cs" -TechniqueName "ContrastAdaptiveSharpening" -TechniqueId "FF94" -Entry "main" Compile-Shader -Type "cs" -TechniqueName "ContrastAdaptiveSharpening" -TechniqueId "100FF94" -Defines "USE_PACKED_MATH" -AdditionalParams "-enable-16bit-types", "-Wno-conversion" + Compile-Shader -Type "cs" -TechniqueName "ContrastAdaptiveSharpening" -TechniqueId "300FF94" -Defines "USE_PACKED_MATH", "USE_UPSCALING" -AdditionalParams "-enable-16bit-types", "-Wno-conversion" Compile-Shader -Type "ps" -TechniqueName "PostSharpen" -TechniqueId "FF9A" From 5c4cb1cb75683a9ddc984bbdecf3d2551d2d3aff Mon Sep 17 00:00:00 2001 From: ersh1 <16157893+ersh1@users.noreply.github.com> Date: Sun, 8 Oct 2023 03:30:09 +0200 Subject: [PATCH 108/270] added CAS upscaling to cbuffer hook --- Plugin/src/Hooks.cpp | 1 + Plugin/src/Settings.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Plugin/src/Hooks.cpp b/Plugin/src/Hooks.cpp index 20483b5e..1a86ea41 100644 --- a/Plugin/src/Hooks.cpp +++ b/Plugin/src/Hooks.cpp @@ -312,6 +312,7 @@ namespace Hooks //case 0xFF94: case 0x100FF94: + case 0x300FF94: uploadRootConstants(14, true); // ContrastAdaptiveSharpening break; diff --git a/Plugin/src/Settings.cpp b/Plugin/src/Settings.cpp index a8a081c6..13ba0f4a 100644 --- a/Plugin/src/Settings.cpp +++ b/Plugin/src/Settings.cpp @@ -77,9 +77,9 @@ namespace Settings "ImageSpaceBufferE5B9G9R9", "TAA_idTech7HistoryColorTarget", "EnvBRDF", - "ImageSpaceBufferR10G10B10A2", + "ImageSpaceBufferR10G10B10A2" #if 0 - 'NativeResolutionColorBuffer01', + ,'NativeResolutionColorBuffer01', 'ColorBuffer01' #endif ); From 02cae2bc72c269750d84127006fb55e32c684ee7 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Sun, 8 Oct 2023 00:04:04 -0400 Subject: [PATCH 109/270] feat(filmgrain): use kodak-based midtones --- shaders/FilmGrain/FilmGrain_ps.hlsl | 60 +++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/shaders/FilmGrain/FilmGrain_ps.hlsl b/shaders/FilmGrain/FilmGrain_ps.hlsl index c3b9275d..4c7908b5 100644 --- a/shaders/FilmGrain/FilmGrain_ps.hlsl +++ b/shaders/FilmGrain/FilmGrain_ps.hlsl @@ -1,11 +1,12 @@ +#include "../math.hlsl" #include "../shared.hlsl" #include "../color.hlsl" #include "RootSignature.hlsl" #define FILM_GRAIN_TEXTURE_SIZE 1024u -// 0 Vanilla, 1 New Random (no flicker/repeating), 2 New random, no raised blacks -#define FILM_GRAIN_TYPE 2 +// 0 Vanilla, 1 New Random (no flicker/repeating), 2 New random, no raised blacks 3 Kodak Filmic +#define FILM_GRAIN_TYPE 3 cbuffer _13_15 : register(b0, space0) { @@ -50,14 +51,19 @@ void frag_main() // .y = randomInt32? changed every frame? // .z = Film Grading Intensity (0 - 0.03) - float3 tonemappedColor = TonemappedColorTexture.Sample(Sampler0, float2(TEXCOORD.x, TEXCOORD.y)); + const float3 inputColor = TonemappedColorTexture.Sample(Sampler0, float2(TEXCOORD.x, TEXCOORD.y)); const float paperWhite = HdrDllPluginConstants.HDRGamePaperWhiteNits / WhiteNits_BT709; + // sRGBColor gets the luminance shift + float3 srgbColor = inputColor; + // linearColor is used to compute Y + float3 linearColor = inputColor; + if (HdrDllPluginConstants.DisplayMode > 0) { - tonemappedColor /= paperWhite; - tonemappedColor = gamma_linear_to_sRGB(tonemappedColor); + srgbColor = gamma_linear_to_sRGB(inputColor / paperWhite); + } else { + linearColor = gamma_sRGB_to_linear(inputColor); } -float colorY = Luminance(tonemappedColor); #if FILM_GRAIN_TYPE == 0 const float filmGrainInvSize = 1.f / (FILM_GRAIN_TEXTURE_SIZE - 1u); @@ -86,30 +92,50 @@ float colorY = Luminance(tonemappedColor); + frac(filmGrainColorAndIntensity.y / 2147483647.f) ); #endif // FILM_GRAIN_TYPE == 0 - -#if FILM_GRAIN_TYPE != 2 - float inverseLuminance = saturate(1.f - colorY); // luminanceShift at -1 is black, 0 unchanged, 1 negative of current texel float luminanceShift = (randomNumber * 2.f) - 1.f; - float additiveFilmGrain = (luminanceShift * filmGrainColorAndIntensity.z * inverseLuminance); -#else - float additiveFilmGrain = ((randomNumber * 2.f) - 1.f) // (-1 to 1) - * (2.f * (0.5f - abs(saturate(colorY) - 0.5f))) // Bias towards center (nothing if black or white) - * filmGrainColorAndIntensity.z - * 3.333f; + +#if FILM_GRAIN_TYPE == 0 || FILM_GRAIN_TYPE == 1 + float colorLuma = Luminance(srgbColor); + float customMulitplier = saturate(1.f - colorLuma); // inverseLuma +#elif FILM_GRAIN_TYPE == 2 + float colorLuma = Luminance(srgbColor); + float customMulitplier = (-2.f * (0.5f - abs(saturate(colorLuma) - 0.5f))) // Bias towards center (nothing if black or white) + * 3.333f; // Bump to 10% +#elif FILM_GRAIN_TYPE == 3 + float colorY = Luminance(linearColor); + float midToneStart = 0.022f; // Kodak Gray Scale B (Shadow) + float midToneMid = 0.178f; // Kodak Gray Scale M (Gray) + float midToneEnd = 0.891f; // Kodak Gray Scale A (Highlight) + float midPointScaling; + if (colorY > midToneStart && colorY < midToneEnd) { + if (colorY < midToneMid) { + // Lerp? + midPointScaling = linearNormalization(colorY, midToneStart, midToneMid, 0.f, 1.f); + } else { + // Lerp? + midPointScaling = linearNormalization(colorY, midToneMid, midToneEnd, 1.f, 0.f); + } + } else { + midPointScaling = 0.f; + } + float customMulitplier = midPointScaling + * 3.333f; // Bump to 10% + #endif // FILM_GRAIN_TYPE != 2 + float additiveFilmGrain = (luminanceShift * filmGrainColorAndIntensity.z * customMulitplier); float3 newColor = float3(additiveFilmGrain, additiveFilmGrain,additiveFilmGrain); // Use addition to overlay color on top // Note: we let this possibly generate colors below 1 for scRGB - float3 tonemappedColorWithFilmGrain = tonemappedColor + newColor; + float3 tonemappedColorWithFilmGrain = srgbColor + newColor; #if 0 // WIP fixes // Right side only if (TEXCOORD.x < 0.5f) { - tonemappedColorWithFilmGrain = tonemappedColor; + tonemappedColorWithFilmGrain = inputColor; } #endif From 8df9cae16ea4ce7f2147cd6538d9d781147aed56 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sun, 8 Oct 2023 11:12:09 +0300 Subject: [PATCH 110/270] Polish hable black screen fix --- shaders/HDRComposite/HDRComposite_ps.hlsl | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index a4316eb7..3af7c708 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -193,7 +193,8 @@ float3 Hable( { // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L202 - // the 2.2 is so you don't have to input very small numbers + // Note that all these variables can vary depending on the level. + // The 2.2 pow is so you don't have to input very small numbers, it's not related to gamma. const float toeLength = pow(saturate(PerSceneConstants[3266u].w), 2.2f); // Constant is usually 0.3 const float toeStrength = saturate(PerSceneConstants[3266u].z); // Constant is usually 0.5 const float shoulderLength = clamp(saturate(PerSceneConstants[3267u].y), EPSILON, BTHCNST); // Constant is usually 0.8 @@ -281,12 +282,10 @@ float3 Hable( // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L6 // Eval - float evalY0 = 0.f; -#if 0 // Fixes full black output in some location (due to "invScale" being 0) - if (params_overshootX > 0.f) -#endif + float evalY0 = params_overshootY; + if (params_overshootX > 0.f) // log2(0) goes towards -INF, exp2(-INF) is 0. "params_overshootX" cannot be < 0 but it can occasionally be 0. { - evalY0 = -exp2(((shoulderSegment_B_optimised * log2(params_overshootX)) + hableParams.shoulderSegment.lnA) * LOG2_E) + params_overshootY; + evalY0 -= exp2(((shoulderSegment_B_optimised * log2(params_overshootX)) + hableParams.shoulderSegment.lnA) * LOG2_E); } // Eval end hableParams.invScale = 1.f / evalY0; From 4f323600af489484a8a5ecbc39075562ec92248f Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sun, 8 Oct 2023 11:25:27 +0300 Subject: [PATCH 111/270] Add a single define to force the SDR Vanilla look for quick screenshot comparisons --- shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl | 4 ++-- shaders/HDRComposite/HDRComposite_ps.hlsl | 6 +++--- shaders/shared.hlsl | 7 +++++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl index f2d9d4b7..4e5a5ffc 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl @@ -4,7 +4,7 @@ #include "RootSignature.hlsl" // 0 None, 1 ShortFuse technique (normalization), 2 luminance preservation (doesn't look so good and it's kinda broken in SDR) -#define LUT_IMPROVEMENT_TYPE 1 +#define LUT_IMPROVEMENT_TYPE (FORCE_VANILLA_LOOK ? 0 : 1) #define FORCE_SDR_LUTS 0 // Make some small quality cuts for the purpose of optimization #define OPTIMIZE_LUT_ANALYSIS true @@ -31,7 +31,7 @@ #define LUT_SDR_ON_CLIP 1u #define LUT_HDR_ON_CLIP 0u -static float AdditionalNeutralLUTPercentage = 0.f; // ~0.25 might be a good compromise, but this is mostly replaced by HdrDllPluginConstants.LUTCorrectionStrength +static float AdditionalNeutralLUTPercentage = 0.f; // ~0.25 might be a good compromise, but this is mostly replaced by "HdrDllPluginConstants.LUTCorrectionStrength" cbuffer CPushConstantWrapper_ColorGradingMerge : register(b0, space0) { diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 3af7c708..cd295b22 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -10,7 +10,7 @@ //#define APPLY_MERGED_COLOR_GRADING_LUT // Suggested if "LUT_FIX_GAMMA_MAPPING" is true -#define FIX_WRONG_SRGB_GAMMA_FORMULA 1 +#define FIX_WRONG_SRGB_GAMMA_FORMULA (FORCE_VANILLA_LOOK ? 0 : 1) // This disables most other features (post process, LUTs, ...) #define ENABLE_TONEMAP 1 @@ -20,10 +20,10 @@ // 2 improved (looks more natural, avoids values below 0, but will overshoot beyond 1 more often, and will raise blacks) // 3 Sigmoidal inspired and biases contrast increases towards the lower and top end // (optimisation left if contrastIntensity doesn't go below 1) -#define POST_PROCESS_CONTRAST_TYPE 2 +#define POST_PROCESS_CONTRAST_TYPE (FORCE_VANILLA_LOOK ? 0 : 2) #define ENABLE_LUT 1 // LUTs are too low resolutions to resolve gradients smoothly if the LUT color suddenly changes between samples -#define ENABLE_LUT_TETRAHEDRAL_INTERPOLATION 1 +#define ENABLE_LUT_TETRAHEDRAL_INTERPOLATION (FORCE_VANILLA_LOOK ? 0 : 1) #define ENABLE_REPLACED_TONEMAP 1 // Only invert highlights, which helps conserve the SDR filmic look (shadow crush) and altered colors. // The alternative is to keep the linear space image tonemapped by the lightweight DICE tonemapper, diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index ad43f01d..9adfe7ff 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -2,6 +2,9 @@ #include "structs.hlsl" +// Preset all defines to be as close as possible to the og game vanilla look +#define FORCE_VANILLA_LOOK 0 + // Brings the range roughly from 80 nits to 203 nits (~2.5) #define HDR_REFERENCE_PAPER_WHITE_MUTLIPLIER (ReferenceWhiteNits_BT2408 / WhiteNits_BT709) @@ -9,11 +12,11 @@ // This implies there was a mismatch baked in the output colors, as they were using a ~sRGB similar formula, which would then be interpreted by screens as 2.2 gamma. // By turning this on, we emulate the SDR look in HDR by baking that assumption into our calculations. // This makes sense to use given we fix up (normalize) the LUTs colors and their gamma mapping. -#define SDR_USE_GAMMA_2_2 1 +#define SDR_USE_GAMMA_2_2 (FORCE_VANILLA_LOOK ? 0 : 1) // Makes LUTs sampling work in linear space, which is mathematically correct. Without this, they are stored as ~sRGB in a float texture and sampled in sRGB without acknowledging it. // This possibly shifts colors a lot, but it's correct, it's also necessary for HDR LUTs to work. -#define LUT_FIX_GAMMA_MAPPING 1 +#define LUT_FIX_GAMMA_MAPPING (FORCE_VANILLA_LOOK ? 0 : 1) #define LUT_SIZE 16.f #define LUT_SIZE_UINT (uint)LUT_SIZE From 320a8c304fd6127470eb42054452200b6939b49e Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sun, 8 Oct 2023 13:09:57 +0300 Subject: [PATCH 112/270] Add PQ formulas --- shaders/color.hlsl | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/shaders/color.hlsl b/shaders/color.hlsl index e2d94e16..590b1f41 100644 --- a/shaders/color.hlsl +++ b/shaders/color.hlsl @@ -27,6 +27,12 @@ static const float3x3 BT2020_2_BT709 = float3x3( 1.66049621914783, -0.587656444131135, -0.0728397750166941, -0.124547095586012, 1.13289510924730, -0.00834801366128445, -0.0181536813870718, -0.100597371685743, 1.11875105307281); + +static const float PQ_constant_N = (2610.0 / 4096.0 / 4.0); +static const float PQ_constant_M = (2523.0 / 4096.0 * 128.0); +static const float PQ_constant_C1 = (3424.0 / 4096.0); +static const float PQ_constant_C2 = (2413.0 / 4096.0 * 32.0); +static const float PQ_constant_C3 = (2392.0 / 4096.0 * 32.0); float3 BT709_To_BT2020(float3 color) { @@ -79,6 +85,26 @@ float3 gamma_sRGB_to_linear(float3 Color) gamma_sRGB_to_linear(Color.g), gamma_sRGB_to_linear(Color.b)); } +// HDR10 PQ (Perceptual Quantiser - ST.2084) +float3 linear_to_PQ(float3 Color, const float PQMaxValue = PQMaxWhitePoint) +{ + Color /= PQMaxValue; + float3 colorPow = pow(Color, PQ_constant_N); + float3 numerator = PQ_constant_C1 + PQ_constant_C2 * colorPow; + float3 denominator = 1.f + PQ_constant_C3 * colorPow; + float3 pq = pow(numerator / denominator, PQ_constant_M); + return pq; +} + +float3 PQ_to_Linear(float3 Color, const float PQMaxValue = PQMaxWhitePoint) +{ + float3 colorPow = pow(Color, 1.f / PQ_constant_M ); + float3 numerator = max(colorPow - PQ_constant_C1, 0.f); + float3 denominator = PQ_constant_C2 - (PQ_constant_C3 * colorPow); + float3 linearColor = pow(numerator / denominator, 1.f / PQ_constant_N); + linearColor *= PQMaxValue; + return linearColor; +} float Luminance(float3 color) { From 4e04f8f7ef3ab46356d1e5c5fcc8c7777c446bb2 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sun, 8 Oct 2023 13:21:49 +0300 Subject: [PATCH 113/270] Add development on/off define and improve UI HDR blending --- shaders/ScaleformComposite/ScaleformComposite_ps.hlsl | 10 ++++++++-- shaders/shared.hlsl | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl b/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl index f92c3296..b4998b56 100644 --- a/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl +++ b/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl @@ -3,7 +3,8 @@ #include "RootSignature.hlsl" // Hack: change the alpha value at which the UI blends in in HDR, to increase readability. Range is 0 to 1, with 1 having no effect. -#define HDR_UI_BLEND_POW 0.775f +// We found the best value empirically and it seems to match gamma 2.2. +#define HDR_UI_BLEND_POW (1.f / 2.2f) struct PushConstantWrapper_ScaleformCompositeLayout { @@ -43,7 +44,12 @@ float4 PS(PSInputs inputs) : SV_Target // Scale alpha to emulate sRGB gamma blending (we blend in linear space in HDR), // this won't ever be perfect but it's close enough for most cases. // We do a saturate to avoid pow of -0, which might lead to unexpected results. - UIColor.a = pow(saturate(UIColor.a), HDR_UI_BLEND_POW); //TODO: base the percentage of application of "HDR_UI_BLEND_POW" based on how black/dark the UI color is. +#if DEVELOPMENT && 0 + const float HDRUIBlendPow = 1.f - HdrDllPluginConstants.DevSetting02; +#else + const float HDRUIBlendPow = HDR_UI_BLEND_POW; +#endif + UIColor.a = pow(saturate(UIColor.a), HDRUIBlendPow); //TODO: base the percentage of application of "HDR_UI_BLEND_POW" based on how black/dark the UI color is? } else { diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 9adfe7ff..60452c78 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -2,6 +2,9 @@ #include "structs.hlsl" +// Turn on when developing to engage error checks and development setting variables +#define DEVELOPMENT 1 + // Preset all defines to be as close as possible to the og game vanilla look #define FORCE_VANILLA_LOOK 0 From c43df18cb72231a9401d6cd760fbc376a9548074 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sun, 8 Oct 2023 13:24:29 +0300 Subject: [PATCH 114/270] Polish: -Fix typo -Add const -Add luminance validity check that always runs --- .../ColorGradingMerge_cs.hlsl | 2 +- shaders/HDRComposite/HDRComposite_ps.hlsl | 18 ++++++++++-------- shaders/structs.hlsl | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl index 4e5a5ffc..923f0c21 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl @@ -31,7 +31,7 @@ #define LUT_SDR_ON_CLIP 1u #define LUT_HDR_ON_CLIP 0u -static float AdditionalNeutralLUTPercentage = 0.f; // ~0.25 might be a good compromise, but this is mostly replaced by "HdrDllPluginConstants.LUTCorrectionStrength" +static const float AdditionalNeutralLUTPercentage = 0.f; // ~0.25 might be a good compromise, but this is mostly replaced by "HdrDllPluginConstants.LUTCorrectionStrength" cbuffer CPushConstantWrapper_ColorGradingMerge : register(b0, space0) { diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index cd295b22..56191910 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -189,7 +189,7 @@ T ACESParametric_Inverse(T Color, float modE, float modA) // This is NOT the Uncharted 2 tonemapper, which was also by Hable. float3 Hable( in float3 InputColor, - out HableParamters hableParams) + out HableParameters hableParams) { // https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L202 @@ -312,7 +312,7 @@ float3 Hable( returnChannel = exp2(log2(evalMidSegment_y0) + midSegment_lnA_optimised); } } - // Highlight + // Shoulder (highlight) else { // Note that this will "clip" to 1 way before +INF @@ -354,7 +354,7 @@ float HableEval_Inverse( void Hable_Inverse_Channel( inout float ColorChannel, - in HableParamters hableParams) + in HableParameters hableParams) { // scaleY and offsetY setup: https://github.com/johnhable/fw-public/blob/37de36e662336415f5ef654d8edfc46b4ad025ed/FilmicCurve/FilmicToneCurve.cpp#L187-L197 // toe @@ -400,7 +400,7 @@ template T Hable_Inverse( T InputColor, uint Channels /*1 to 3*/, //there has to be better solution than this - HableParamters hableParams) + HableParameters hableParams) { // There's no inverse formula for colors beyond the 0-1 range InputColor = saturate(InputColor); @@ -826,7 +826,7 @@ PSOutput PS(PSInput psInput) float acesParam_modE; float acesParam_modA; - HableParamters hableParams; + HableParameters hableParams; const bool clampACES = false; @@ -998,9 +998,6 @@ PSOutput PS(PSInput psInput) #if CLAMP_INPUT_OUTPUT outputColor = clamp(outputColor, 0.f, FLT16_MAX); // Avoid extremely high numbers turning into NaN in FP16 -#else - if (Luminance(outputColor) < 0.f) - outputColor = 0.f; #endif // CLAMP_INPUT_OUTPUT } else // SDR @@ -1020,6 +1017,11 @@ PSOutput PS(PSInput psInput) #endif // CLAMP_INPUT_OUTPUT } +#if !CLAMP_INPUT_OUTPUT + if (Luminance(outputColor) < 0.f) // Remove invalid colors + outputColor = 0.f; +#endif + PSOutput psOutput; psOutput.SV_Target.rgb = outputColor; psOutput.SV_Target.a = 1.f; diff --git a/shaders/structs.hlsl b/shaders/structs.hlsl index 9b73f0f3..406c26a1 100644 --- a/shaders/structs.hlsl +++ b/shaders/structs.hlsl @@ -162,7 +162,7 @@ struct Hable_shoulderSegment float B; }; -struct HableParamters +struct HableParameters { Hable_params params; Hable_dstParams dstParams; From b7329f79098f915ca65835fe803b6905ac19f91a Mon Sep 17 00:00:00 2001 From: ersh1 <16157893+ersh1@users.noreply.github.com> Date: Sun, 8 Oct 2023 18:10:55 +0200 Subject: [PATCH 115/270] added PQ support (currently seems oversaturated, error in linear_to_PQ?) also added more dev settings and removed them all from the settings menu settings reshade overlay should now show up for the asi version as well --- Plugin/src/Hooks.cpp | 189 ++++++++++-------- Plugin/src/Hooks.h | 23 ++- Plugin/src/Settings.cpp | 3 + Plugin/src/Settings.h | 30 ++- Plugin/src/main.cpp | 4 +- shaders/BinkMovie/RootSignature.hlsl | 4 +- shaders/ColorGradingMerge/RootSignature.hlsl | 4 +- .../RootSignature.hlsl | 4 +- shaders/Copy/Copy_ps.hlsl | 63 ++++++ shaders/Copy/RootSignature.hlsl | 12 ++ shaders/FilmGrain/RootSignature.hlsl | 4 +- shaders/HDRComposite/RootSignature.hlsl | 4 +- shaders/PostSharpen/RootSignature.hlsl | 4 +- shaders/ScaleformComposite/RootSignature.hlsl | 4 +- shaders/compile_all_shaders.ps1 | 3 + shaders/shared.hlsl | 8 +- 16 files changed, 255 insertions(+), 108 deletions(-) create mode 100644 shaders/Copy/Copy_ps.hlsl create mode 100644 shaders/Copy/RootSignature.hlsl diff --git a/Plugin/src/Hooks.cpp b/Plugin/src/Hooks.cpp index 1a86ea41..a2b8cb6a 100644 --- a/Plugin/src/Hooks.cpp +++ b/Plugin/src/Hooks.cpp @@ -104,6 +104,7 @@ namespace Hooks const auto settings = Settings::Main::GetSingleton(); CreateSeparator(a_settingList, Settings::SettingID::kSTART); + CreateStepperSetting(a_settingList, settings->DisplayMode, true); CreateSliderSetting(a_settingList, settings->PeakBrightness, settings->IsHDREnabled()); CreateSliderSetting(a_settingList, settings->GamePaperWhite, settings->IsHDREnabled()); @@ -112,13 +113,91 @@ namespace Hooks CreateSliderSetting(a_settingList, settings->Contrast, settings->IsHDREnabled()); CreateSliderSetting(a_settingList, settings->LUTCorrectionStrength, true); CreateSliderSetting(a_settingList, settings->ColorGradingStrength, true); -#if 1 - CreateSliderSetting(a_settingList, settings->DevSetting01, true); - CreateSliderSetting(a_settingList, settings->DevSetting02, true); -#endif + CreateSeparator(a_settingList, Settings::SettingID::kEND); } + void Hooks::UploadRootConstants(void* a1, void* a2) + { + const auto technique = *reinterpret_cast(reinterpret_cast(a2) + 0x8); + const auto techniqueId = *reinterpret_cast(technique + 0x78); + + auto uploadRootConstants = [&](uint32_t RootParameterIndex, bool Compute) { + auto commandList = *reinterpret_cast(reinterpret_cast(a1) + 0x10); + const auto settings = Settings::Main::GetSingleton(); + + // This can be any data type, even a struct. It just has to match StructHdrDllPluginConstants in HLSL. + const Settings::ShaderConstants data{ + static_cast(*settings->DisplayMode.value), + bIsAtEndOfFrame.load(), + static_cast(*settings->PeakBrightness.value), + static_cast(*settings->GamePaperWhite.value), + static_cast(*settings->UIPaperWhite.value), + static_cast(*settings->Saturation.value * 0.02f), // 0-100 to 0-2 + static_cast(*settings->Contrast.value * 0.02f), // 0-100 to 0-2 + static_cast(*settings->LUTCorrectionStrength.value * 0.01f), // 0-100 to 0-1 + static_cast(*settings->ColorGradingStrength.value * 0.01f), // 0-100 to 0-1 + static_cast(*settings->DevSetting01.value * 0.01f), // 0-100 to 0-1 + static_cast(*settings->DevSetting02.value * 0.01f), // 0-100 to 0-1 + static_cast(*settings->DevSetting03.value * 0.01f), // 0-100 to 0-1 + static_cast(*settings->DevSetting04.value * 0.01f), // 0-100 to 0-1 + static_cast(*settings->DevSetting05.value * 0.01f) // 0-100 to 0-1 + }; + + if (!Compute) + commandList->SetGraphicsRoot32BitConstants(RootParameterIndex, Settings::shaderConstantsSize, &data, 0); + else + commandList->SetComputeRoot32BitConstants(RootParameterIndex, Settings::shaderConstantsSize, &data, 0); + }; + + // Note: The following switch statement may be called several thousand times per frame. Additionally, it'll be called from multiple + // threads concurrently. The individual cases are called at most once or twice per frame. Keep the amount of code here fairly light. + // + // RootParameterIndex is the index of our custom RootConstants() entry in the root signature. It's taken from the corresponding + // RootSignature.hlsl file stored next to each technique hlsl file. + switch (techniqueId) { + case 0xFF1A: + case 0x600FF1A: + case 0x700FF1A: + //case 0x800FF1A: + case 0xE00FF1A: + case 0xF00FF1A: + uploadRootConstants(14, false); // HDRComposite + break; + + case 0x400FF59: + //case 0x2000FF59: + uploadRootConstants(2, false); // Copy + break; + + case 0xFF75: + uploadRootConstants(2, false); // FilmGrain + break; + + case 0xFF81: + uploadRootConstants(7, true); // ColorGradingMerge + break; + + //case 0xFF94: + case 0x100FF94: + case 0x300FF94: + uploadRootConstants(14, true); // ContrastAdaptiveSharpening + break; + + case 0xFF9A: + uploadRootConstants(14, false); // PostSharpen + break; + + case 0xFFAA: + uploadRootConstants(2, false); // ScaleformComposite + break; + + case 0xFFAB: + uploadRootConstants(1, false); // BinkMovie + break; + } + } + void Hooks::Hook_UnkFunc(uintptr_t a1, RE::BGSSwapChainObject* a_bgsSwapchainObject) { // save the pointer for later @@ -244,95 +323,45 @@ namespace Hooks case static_cast(Settings::SettingID::kColorGradingStrength): HandleSetting(settings->ColorGradingStrength); break; - case static_cast(Settings::SettingID::kDevSetting01): - HandleSetting(settings->DevSetting01); - break; - case static_cast(Settings::SettingID::kDevSetting02): - HandleSetting(settings->DevSetting02); - break; } _SettingsDataModelFloatEvent(a_arg1, a_eventData); } - bool Hooks::Hook_ApplyRenderPassRenderState(void* a_arg1, void* a_arg2) + bool Hooks::Hook_ApplyRenderPassRenderState1(void* a_arg1, void* a_arg2) { - const bool result = _ApplyRenderPassRenderState(a_arg1, a_arg2); + const bool result = _ApplyRenderPassRenderState1(a_arg1, a_arg2); if (result) { - const auto technique = *reinterpret_cast(reinterpret_cast(a_arg2) + 0x8); - const auto techniqueId = *reinterpret_cast(technique + 0x78); - - auto uploadRootConstants = [&](uint32_t RootParameterIndex, bool Compute) { - auto commandList = *reinterpret_cast(reinterpret_cast(a_arg1) + 0x10); - const auto settings = Settings::Main::GetSingleton(); - - // This can be any data type, even a struct. It just has to match StructHdrDllPluginConstants in HLSL. - const Settings::ShaderConstants data { - static_cast(*settings->DisplayMode.value), - static_cast(*settings->PeakBrightness.value), - static_cast(*settings->GamePaperWhite.value), - static_cast(*settings->UIPaperWhite.value), - static_cast(*settings->Saturation.value * 0.02f), // 0-100 to 0-2 - static_cast(*settings->Contrast.value * 0.02f), // 0-100 to 0-2 - static_cast(*settings->LUTCorrectionStrength.value * 0.01f), // 0-100 to 0-1 - static_cast(*settings->ColorGradingStrength.value * 0.01f), // 0-100 to 0-1 - static_cast(*settings->DevSetting01.value * 0.01f), // 0-100 to 0-1 - static_cast(*settings->DevSetting02.value * 0.01f) // 0-100 to 0-1 - }; - - if (!Compute) - commandList->SetGraphicsRoot32BitConstants(RootParameterIndex, Settings::shaderConstantsSize, &data, 0); - else - commandList->SetComputeRoot32BitConstants(RootParameterIndex, Settings::shaderConstantsSize, &data, 0); - }; - - // Note: The following switch statement may be called several thousand times per frame. Additionally, it'll be called from multiple - // threads concurrently. The individual cases are called at most once or twice per frame. Keep the amount of code here fairly light. - // - // RootParameterIndex is the index of our custom RootConstants() entry in the root signature. It's taken from the corresponding - // RootSignature.hlsl file stored next to each technique hlsl file. - switch (techniqueId) { - case 0xFF1A: - case 0x600FF1A: - case 0x700FF1A: - //case 0x800FF1A: - case 0xE00FF1A: - case 0xF00FF1A: - uploadRootConstants(14, false); // HDRComposite - break; - - case 0xFF75: - uploadRootConstants(2, false); // FilmGrain - break; - - case 0xFF81: - uploadRootConstants(7, true); // ColorGradingMerge - break; - - //case 0xFF94: - case 0x100FF94: - case 0x300FF94: - uploadRootConstants(14, true); // ContrastAdaptiveSharpening - break; - - case 0xFF9A: - uploadRootConstants(14, false); // PostSharpen - break; - - case 0xFFAA: - uploadRootConstants(2, false); // ScaleformComposite - break; - - case 0xFFAB: - uploadRootConstants(1, false); // BinkMovie - break; - } + UploadRootConstants(a_arg1, a_arg2); } return result; } + bool Hooks::Hook_ApplyRenderPassRenderState2(void* a_arg1, void* a_arg2) + { + const bool result = _ApplyRenderPassRenderState2(a_arg1, a_arg2); + + if (result) { + UploadRootConstants(a_arg1, a_arg2); + } + + return result; + } + + void Hooks::Hook_EndOfFrame(void* a1, void* a2, const char* a3) + { + bIsAtEndOfFrame.store(true); + _EndOfFrame(a1, a2, a3); + } + + void Hooks::Hook_PostEndOfFrame(void* a1) + { + _PostEndOfFrame(a1); + bIsAtEndOfFrame.store(false); + } + void Install() { #ifndef NDEBUG diff --git a/Plugin/src/Hooks.h b/Plugin/src/Hooks.h index 0cb0d3e4..bbdba81c 100644 --- a/Plugin/src/Hooks.h +++ b/Plugin/src/Hooks.h @@ -51,8 +51,11 @@ namespace Hooks _RecreateSwapchain = dku::Hook::write_call<5>(dku::Hook::IDToAbs(203027, 0x89), Hook_RecreateSwapchain); - _ApplyRenderPassRenderState = dku::Hook::write_call<5>(dku::Hook::IDToAbs(204409, 0x18), Hook_ApplyRenderPassRenderState); // CmdDraw - dku::Hook::write_call<5>(dku::Hook::IDToAbs(204408, 0x20), Hook_ApplyRenderPassRenderState); // CmdDispatch + _ApplyRenderPassRenderState1 = dku::Hook::write_call<5>(dku::Hook::IDToAbs(204409, 0x18), Hook_ApplyRenderPassRenderState1); // CmdDraw + _ApplyRenderPassRenderState2 = dku::Hook::write_call<5>(dku::Hook::IDToAbs(204408, 0x20), Hook_ApplyRenderPassRenderState2); // CmdDispatch + + _EndOfFrame = dku::Hook::write_call<5>(dku::Hook::IDToAbs(205436, 0x3F7), Hook_EndOfFrame); + _PostEndOfFrame = dku::Hook::write_call<5>(dku::Hook::IDToAbs(205436, 0x7CB), Hook_PostEndOfFrame); } private: @@ -63,6 +66,8 @@ namespace Hooks static void CreateSeparator(RE::ArrayNestedUIValue* a_settingList, Settings::SettingID a_id); static void CreateSettings(RE::ArrayNestedUIValue* a_settingList); + static void UploadRootConstants(void* a1, void* a2); + static void Hook_UnkFunc(uintptr_t a1, RE::BGSSwapChainObject* a_bgsSwapchainObject); static inline std::add_pointer_t _UnkFunc; @@ -81,8 +86,18 @@ namespace Hooks static void Hook_SettingsDataModelFloatEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& a_eventData); static inline std::add_pointer_t _SettingsDataModelFloatEvent; - static bool Hook_ApplyRenderPassRenderState(void* a_arg1, void* a_arg2); - static inline std::add_pointer_t _ApplyRenderPassRenderState; + static bool Hook_ApplyRenderPassRenderState1(void* a_arg1, void* a_arg2); + static bool Hook_ApplyRenderPassRenderState2(void* a_arg1, void* a_arg2); + static inline std::add_pointer_t _ApplyRenderPassRenderState1; + static inline std::add_pointer_t _ApplyRenderPassRenderState2; + + static void Hook_EndOfFrame(void* a1, void* a2, const char* a3); + static inline std::add_pointer_t _EndOfFrame; + + static void Hook_PostEndOfFrame(void* a1); + static inline std::add_pointer_t _PostEndOfFrame; + + static inline std::atomic_bool bIsAtEndOfFrame = false; }; void Install(); diff --git a/Plugin/src/Settings.cpp b/Plugin/src/Settings.cpp index 13ba0f4a..80cb93f4 100644 --- a/Plugin/src/Settings.cpp +++ b/Plugin/src/Settings.cpp @@ -66,6 +66,9 @@ namespace Settings config.Bind(ColorGradingStrength.value, ColorGradingStrength.defaultValue); config.Bind(DevSetting01.value, DevSetting01.defaultValue); config.Bind(DevSetting02.value, DevSetting02.defaultValue); + config.Bind(DevSetting03.value, DevSetting03.defaultValue); + config.Bind(DevSetting04.value, DevSetting04.defaultValue); + config.Bind(DevSetting05.value, DevSetting05.defaultValue); config.Bind(RenderTargetsToUpgrade, "ImageSpaceBuffer", "ScaleformCompositeBuffer", diff --git a/Plugin/src/Settings.h b/Plugin/src/Settings.h index 96bf1907..4d29906f 100644 --- a/Plugin/src/Settings.h +++ b/Plugin/src/Settings.h @@ -23,10 +23,14 @@ namespace Settings kHDR_Contrast, kLUTCorrectionStrength, kColorGradingStrength, + + kEND, + kDevSetting01, kDevSetting02, - - kEND + kDevSetting03, + kDevSetting04, + kDevSetting05, }; struct Setting @@ -63,6 +67,7 @@ namespace Settings struct ShaderConstants { uint32_t DisplayMode; + bool bIsAtEndOfFrame; float PeakBrightness; float GamePaperWhite; float UIPaperWhite; @@ -72,8 +77,11 @@ namespace Settings float ColorGradingStrength; float DevSetting01; float DevSetting02; + float DevSetting03; + float DevSetting04; + float DevSetting05; }; - static inline uint32_t shaderConstantsSize = 10; + static inline uint32_t shaderConstantsSize = 14; class Main : public DKUtil::model::Singleton
{ @@ -90,6 +98,9 @@ namespace Settings #if 1 Slider DevSetting01{ SettingID::kDevSetting01, "DevSetting01", "Development setting", { "DevSetting01", "Dev" }, 0.f, 100.f, 0.f }; Slider DevSetting02{ SettingID::kDevSetting02, "DevSetting02", "Development setting", { "DevSetting02", "Dev" }, 0.f, 100.f, 0.f }; + Slider DevSetting03{ SettingID::kDevSetting03, "DevSetting03", "Development setting", { "DevSetting03", "Dev" }, 0.f, 100.f, 0.f }; + Slider DevSetting04{ SettingID::kDevSetting04, "DevSetting04", "Development setting", { "DevSetting04", "Dev" }, 0.f, 100.f, 0.f }; + Slider DevSetting05{ SettingID::kDevSetting05, "DevSetting05", "Development setting", { "DevSetting05", "Dev" }, 0.f, 100.f, 0.f }; #endif String RenderTargetsToUpgrade{ "RenderTargetsToUpgrade", "RenderTargets" }; @@ -153,6 +164,9 @@ namespace Settings #if 1 DrawReshadeSlider(settings, settings->DevSetting01); DrawReshadeSlider(settings, settings->DevSetting02); + DrawReshadeSlider(settings, settings->DevSetting03); + DrawReshadeSlider(settings, settings->DevSetting04); + DrawReshadeSlider(settings, settings->DevSetting05); #endif } @@ -161,9 +175,13 @@ namespace Settings static void RegisterReshadeOverlay() { if (!bReshadeSettingsOverlayRegistered) { - if (reshade::register_addon(GetModuleHandle(L"NativeHDR"))) { - reshade::register_overlay("NativeHDR Settings", &DrawSettingsReshade); - bReshadeSettingsOverlayRegistered = true; + HMODULE hModule = nullptr; + GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(Main::GetSingleton()), &hModule); + if (hModule) { + if (reshade::register_addon(hModule)) { + reshade::register_overlay("NativeHDR Settings", &DrawSettingsReshade); + bReshadeSettingsOverlayRegistered = true; + } } } } diff --git a/Plugin/src/main.cpp b/Plugin/src/main.cpp index 47e20774..01307d85 100644 --- a/Plugin/src/main.cpp +++ b/Plugin/src/main.cpp @@ -41,9 +41,9 @@ void LoadPlugin(bool a_bIsSFSE) Settings::Main::GetSingleton()->Load(); if (a_bIsSFSE) { - SFSE::AllocTrampoline(1 << 7); + SFSE::AllocTrampoline(1 << 8); } else { - dku::Hook::Trampoline::AllocTrampoline(1 << 7); + dku::Hook::Trampoline::AllocTrampoline(1 << 8); } Offsets::Initialize(); diff --git a/shaders/BinkMovie/RootSignature.hlsl b/shaders/BinkMovie/RootSignature.hlsl index dd5dec88..3675f245 100644 --- a/shaders/BinkMovie/RootSignature.hlsl +++ b/shaders/BinkMovie/RootSignature.hlsl @@ -7,5 +7,5 @@ "SRV(t2, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=10, b3), " \ - "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " \ No newline at end of file + "RootConstants(num32BitConstants=14, b3), " \ + "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " diff --git a/shaders/ColorGradingMerge/RootSignature.hlsl b/shaders/ColorGradingMerge/RootSignature.hlsl index 40ab0d42..c569a78b 100644 --- a/shaders/ColorGradingMerge/RootSignature.hlsl +++ b/shaders/ColorGradingMerge/RootSignature.hlsl @@ -53,7 +53,7 @@ "UAV(u0, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=10, b3), " \ + "RootConstants(num32BitConstants=14, b3), " \ "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ @@ -63,4 +63,4 @@ "StaticSampler(s18, addressU=TEXTURE_ADDRESS_MIRROR, addressV=TEXTURE_ADDRESS_MIRROR, addressW=TEXTURE_ADDRESS_MIRROR, maxAnisotropy=4, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s19, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=4, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s20, filter=FILTER_COMPARISON_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_LESS, space=6), " \ - "StaticSampler(s21, filter=FILTER_COMPARISON_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_GREATER, space=6) " \ No newline at end of file + "StaticSampler(s21, filter=FILTER_COMPARISON_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_GREATER, space=6) " diff --git a/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl b/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl index d0a754a1..749fb1ec 100644 --- a/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl +++ b/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl @@ -68,7 +68,7 @@ "), " \ \ "CBV(b0, space=8), " \ - "RootConstants(num32BitConstants=10, b3), " \ + "RootConstants(num32BitConstants=14, b3), " \ "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ @@ -78,4 +78,4 @@ "StaticSampler(s18, addressU=TEXTURE_ADDRESS_MIRROR, addressV=TEXTURE_ADDRESS_MIRROR, addressW=TEXTURE_ADDRESS_MIRROR, maxAnisotropy=4, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s19, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=4, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s20, filter=FILTER_COMPARISON_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_LESS, space=6), " \ - "StaticSampler(s21, filter=FILTER_COMPARISON_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_GREATER, space=6) " \ No newline at end of file + "StaticSampler(s21, filter=FILTER_COMPARISON_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_GREATER, space=6) " diff --git a/shaders/Copy/Copy_ps.hlsl b/shaders/Copy/Copy_ps.hlsl new file mode 100644 index 00000000..6e3062b1 --- /dev/null +++ b/shaders/Copy/Copy_ps.hlsl @@ -0,0 +1,63 @@ +#include "../shared.hlsl" +#include "../color.hlsl" +#include "RootSignature.hlsl" + +Texture2D inputTexture : register(t0, space8); +SamplerState inputSampler : register(s0, space8); + +struct PSInputs +{ + float2 uv : TEXCOORD0; + float4 pos : SV_Position; +}; + +[RootSignature(ShaderRootSignature)] +float4 PS(PSInputs inputs) : SV_Target +{ + float4 color = inputTexture.Sample(inputSampler, float2(inputs.uv.x, inputs.uv.y)); + + if (HdrDllPluginConstants.DisplayMode == 1 && HdrDllPluginConstants.bIsAtEndOfFrame) + { + float3 pq = linear_to_PQ(color.rgb); + color.rgb = pq.rgb; + } + + return color; +} + + +//-- + +// Texture2D _8 : register(t0, space8); +// SamplerState _11 : register(s0, space8); + +// static float4 TEXCOORD; +// static float4 SV_Target; + +// struct SPIRV_Cross_Input +// { +// float4 TEXCOORD : TEXCOORD0; +// }; + +// struct SPIRV_Cross_Output +// { +// float4 SV_Target : SV_Target0; +// }; + +// void frag_main() +// { +// float4 _31 = _8.Sample(_11, float2(TEXCOORD.x, TEXCOORD.y)); +// SV_Target.x = _31.x; +// SV_Target.y = _31.y; +// SV_Target.z = _31.z; +// SV_Target.w = _31.w; +// } + +// SPIRV_Cross_Output main(SPIRV_Cross_Input stage_input) +// { +// TEXCOORD = stage_input.TEXCOORD; +// frag_main(); +// SPIRV_Cross_Output stage_output; +// stage_output.SV_Target = SV_Target; +// return stage_output; +// } diff --git a/shaders/Copy/RootSignature.hlsl b/shaders/Copy/RootSignature.hlsl new file mode 100644 index 00000000..03cab4cb --- /dev/null +++ b/shaders/Copy/RootSignature.hlsl @@ -0,0 +1,12 @@ +#define ShaderRootSignature \ + "RootFlags(ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT), " \ + \ + "DescriptorTable( " \ + "SRV(t0, space=8) " \ + "), " \ + \ + "DescriptorTable( " \ + "Sampler(s0, space=8) " \ + "), " \ + \ + "RootConstants(num32BitConstants=14, b3), " diff --git a/shaders/FilmGrain/RootSignature.hlsl b/shaders/FilmGrain/RootSignature.hlsl index f88d9ca3..141cad2f 100644 --- a/shaders/FilmGrain/RootSignature.hlsl +++ b/shaders/FilmGrain/RootSignature.hlsl @@ -7,5 +7,5 @@ "SRV(t0, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=10, b3), " \ - "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " \ No newline at end of file + "RootConstants(num32BitConstants=14, b3), " \ + "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " diff --git a/shaders/HDRComposite/RootSignature.hlsl b/shaders/HDRComposite/RootSignature.hlsl index 2ce8e4c2..6756b4bc 100644 --- a/shaders/HDRComposite/RootSignature.hlsl +++ b/shaders/HDRComposite/RootSignature.hlsl @@ -72,7 +72,7 @@ "SRV(t4, space=9) " \ "), " \ \ - "RootConstants(num32BitConstants=10, b3), " \ + "RootConstants(num32BitConstants=14, b3), " \ "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ @@ -83,4 +83,4 @@ "StaticSampler(s19, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=4, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s20, filter=FILTER_COMPARISON_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_LESS, space=6), " \ "StaticSampler(s21, filter=FILTER_COMPARISON_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_GREATER, space=6), " \ - "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=9) " \ No newline at end of file + "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=9) " diff --git a/shaders/PostSharpen/RootSignature.hlsl b/shaders/PostSharpen/RootSignature.hlsl index c46fa491..c0f4e2fc 100644 --- a/shaders/PostSharpen/RootSignature.hlsl +++ b/shaders/PostSharpen/RootSignature.hlsl @@ -68,7 +68,7 @@ "SRV(t0, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=10, b3), " \ + "RootConstants(num32BitConstants=14, b3), " \ "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ @@ -78,4 +78,4 @@ "StaticSampler(s18, addressU=TEXTURE_ADDRESS_MIRROR, addressV=TEXTURE_ADDRESS_MIRROR, addressW=TEXTURE_ADDRESS_MIRROR, maxAnisotropy=4, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s19, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=4, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s20, filter=FILTER_COMPARISON_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_LESS, space=6), " \ - "StaticSampler(s21, filter=FILTER_COMPARISON_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_GREATER, space=6) " \ No newline at end of file + "StaticSampler(s21, filter=FILTER_COMPARISON_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_GREATER, space=6) " diff --git a/shaders/ScaleformComposite/RootSignature.hlsl b/shaders/ScaleformComposite/RootSignature.hlsl index 87dafe37..12d0dca3 100644 --- a/shaders/ScaleformComposite/RootSignature.hlsl +++ b/shaders/ScaleformComposite/RootSignature.hlsl @@ -7,5 +7,5 @@ "SRV(t0, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=10, b3), " \ - "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " \ No newline at end of file + "RootConstants(num32BitConstants=14, b3), " \ + "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " diff --git a/shaders/compile_all_shaders.ps1 b/shaders/compile_all_shaders.ps1 index 873e9cbd..a1ef790a 100644 --- a/shaders/compile_all_shaders.ps1 +++ b/shaders/compile_all_shaders.ps1 @@ -14,6 +14,9 @@ $main = Compile-Shader -Type "ps" -TechniqueName "HDRComposite" -TechniqueId "E00FF1A" -Defines "APPLY_TONEMAPPING", "APPLY_CINEMATICS", "APPLY_MERGED_COLOR_GRADING_LUT" Compile-Shader -Type "ps" -TechniqueName "HDRComposite" -TechniqueId "F00FF1A" -Defines "APPLY_BLOOM", "APPLY_TONEMAPPING", "APPLY_CINEMATICS", "APPLY_MERGED_COLOR_GRADING_LUT" + Compile-Shader -Type "ps" -TechniqueName "Copy" -TechniqueId "400FF59" + #Compile-Shader -Type "ps" -TechniqueName "Copy" -TechniqueId "2000FF59" + Compile-Shader -Type "ps" -TechniqueName "FilmGrain" -TechniqueId "FF75" -Entry "main" Compile-Shader -Type "cs" -TechniqueName "ColorGradingMerge" -TechniqueId "FF81" diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 60452c78..2cf3da3f 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -28,6 +28,7 @@ struct StructHdrDllPluginConstants { // SDR 0 (Rec.709 with 2.2 gamma, not sRGB), 1 HDR10 PQ BT.2020, 2 scRGB HDR uint DisplayMode; + bool bIsAtEndOfFrame; float HDRPeakBrightnessNits; // Set equal to the max nits your display can output float HDRGamePaperWhiteNits; // 203 is the reference value (ReferenceWhiteNits_BT2408) float HDRUIPaperWhiteNits; // 203 is the reference value (ReferenceWhiteNits_BT2408) @@ -37,9 +38,12 @@ struct StructHdrDllPluginConstants float ColorGradingStrength; // 1 is full strength float DevSetting01; // 0-1 variable for development float DevSetting02; // 0-1 variable for development + float DevSetting03; // 0-1 variable for development + float DevSetting04; // 0-1 variable for development + float DevSetting05; // 0-1 variable for development }; //TODO: use this in the root signature files? -#define HDR_PLUGIN_CONSTANTS_SIZE 10 +#define HDR_PLUGIN_CONSTANTS_SIZE 14 -ConstantBuffer HdrDllPluginConstants : register(b3, space0); \ No newline at end of file +ConstantBuffer HdrDllPluginConstants : register(b3, space0); From 8770d6087f1765e27a345fb321e55f62744161b2 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sun, 8 Oct 2023 19:32:21 +0300 Subject: [PATCH 116/270] Polish PostSharpen etcetc --- shaders/HDRComposite/HDRComposite_ps.hlsl | 1 - shaders/PostSharpen/PostSharpen_ps.hlsl | 35 ++++++++++++------- .../ScaleformComposite_ps.hlsl | 1 + shaders/shared.hlsl | 8 ++--- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 56191910..0386cecb 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -29,7 +29,6 @@ // The alternative is to keep the linear space image tonemapped by the lightweight DICE tonemapper, // or to replicate the SDR tonemapper by luminance, though both would alter the look too much and break some scenes. #define INVERT_TONEMAP_HIGHLIGHTS_ONLY 1 -#define CLAMP_INPUT_OUTPUT 1 cbuffer CSharedFrameData : register(b0, space6) diff --git a/shaders/PostSharpen/PostSharpen_ps.hlsl b/shaders/PostSharpen/PostSharpen_ps.hlsl index 34182a50..ae7c2a6e 100644 --- a/shaders/PostSharpen/PostSharpen_ps.hlsl +++ b/shaders/PostSharpen/PostSharpen_ps.hlsl @@ -47,19 +47,20 @@ struct PSOutput [RootSignature(ShaderRootSignature)] PSOutput PS(PSInput psInput) { -#if 0 +#if 0 // Disable post sharpen float3 outColor = TonemappedColorTexture.Sample(Sampler0, psInput.TEXCOORD.xy).rgb; #else float3 inColor = TonemappedColorTexture.Sample(Sampler0, psInput.TEXCOORD.xy).rgb; - if (HdrDllPluginConstants.DisplayMode <= 0) - { - inColor = GAMMA_TO_LINEAR(inColor); - } float3 outColor = inColor; float4 sharpenParams = PcwPostSharpen.params0; float sharpenIntensity = sharpenParams.z; - if (sharpenIntensity > 0.f) + if (sharpenIntensity > 0.f && sharpenIntensity != 1.f) { + // Sharpening is best done in linear space, even if Bethesda made in gamma space + if (HdrDllPluginConstants.DisplayMode <= 0) + { + inColor = GAMMA_TO_LINEAR(inColor); + } float _70 = ((_15_m0[161u].z * psInput.TEXCOORD.x) * sharpenParams.x) + 0.5f; float _72 = ((_15_m0[161u].w * psInput.TEXCOORD.y) * sharpenParams.y) + 0.5f; float _76 = frac(_70); @@ -89,13 +90,21 @@ PSOutput PS(PSInput psInput) _162 = GAMMA_TO_LINEAR(_162); _167 = GAMMA_TO_LINEAR(_167); } - float3 sharpenedColor = (((_167 * _98) + (_162 * _87)) * _136) + (((_157 * _98) + (_152 * _87)) * _125); - // It seems controls the amount of (manaually done) bilinear filtering vs nearest neightbor, so full sharpening is just bilinear - outColor = lerp(sharpenedColor, inColor, sharpenIntensity); - } - if (HdrDllPluginConstants.DisplayMode <= 0) - { - outColor = LINEAR_TO_GAMMA(outColor); + float3 unsharpenedColor = (((_167 * _98) + (_162 * _87)) * _136) + (((_157 * _98) + (_152 * _87)) * _125); + // Controls the amount of a custom blurred bilinear filtering vs HW bilinear (which is blurry if there's upscaling), + // by lerping beyond 1 in the opposite direction of the blurred image, we apply sharpening. + outColor = lerp(unsharpenedColor, inColor, sharpenIntensity); +#if CLAMP_INPUT_OUTPUT + // Note: the sharpening multiplication above generates arbitrary colors and shifts the hue to be invalid + if (Luminance(outColor) < 0.f) + { + outColor = 0.f; + } +#endif + if (HdrDllPluginConstants.DisplayMode <= 0) + { + outColor = LINEAR_TO_GAMMA(outColor); + } } #endif PSOutput psOutput; diff --git a/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl b/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl index b4998b56..22a9f5bd 100644 --- a/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl +++ b/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl @@ -34,6 +34,7 @@ float4 PS(PSInputs inputs) : SV_Target // Theoretically all UI is in sRGB (though it might have been designed on gamma 2.2 screens, we can't know that, but it was definately targeting gamma 2.2 as output anyway). UIColor.xyz = gamma_sRGB_to_linear(UIColor.xyz); + // This multiplication is probably used by Bethesda to dim the UI when applying an AutoHDR pass at the very end (they don't have real HDR) UIColor.xyz = UIColor.xyz * UIIntensity; if (HdrDllPluginConstants.DisplayMode > 0) { diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 2cf3da3f..da49e285 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -7,10 +7,7 @@ // Preset all defines to be as close as possible to the og game vanilla look #define FORCE_VANILLA_LOOK 0 - -// Brings the range roughly from 80 nits to 203 nits (~2.5) -#define HDR_REFERENCE_PAPER_WHITE_MUTLIPLIER (ReferenceWhiteNits_BT2408 / WhiteNits_BT709) - +#define CLAMP_INPUT_OUTPUT 1 // If this is true, the code makes the assumption that Bethesda developed and calibrated the game on gamma 2.2 screens, as opposed to sRGB gamma. // This implies there was a mismatch baked in the output colors, as they were using a ~sRGB similar formula, which would then be interpreted by screens as 2.2 gamma. // By turning this on, we emulate the SDR look in HDR by baking that assumption into our calculations. @@ -23,6 +20,9 @@ #define LUT_SIZE 16.f #define LUT_SIZE_UINT (uint)LUT_SIZE +// Brings the range roughly from 80 nits to 203 nits (~2.5) +#define HDR_REFERENCE_PAPER_WHITE_MUTLIPLIER (ReferenceWhiteNits_BT2408 / WhiteNits_BT709) + // Custom push constants uploaded by the HDR DLL plugin code. Do note that register space comes at a premium when adding members. Bit/byte packing is advised. struct StructHdrDllPluginConstants { From 6cfef29c67c62b6e11a51298f8d8d5aca39b62cd Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sun, 8 Oct 2023 20:02:07 +0300 Subject: [PATCH 117/270] Add FilmGrainType setting and PostSharpening toggle --- Plugin/dist/NativeHDR.toml | 2 ++ Plugin/src/Hooks.cpp | 12 ++++++++- Plugin/src/Settings.cpp | 2 ++ Plugin/src/Settings.h | 25 +++++++++++++------ shaders/BinkMovie/RootSignature.hlsl | 2 +- shaders/ColorGradingMerge/RootSignature.hlsl | 2 +- .../RootSignature.hlsl | 2 +- shaders/Copy/Copy_ps.hlsl | 2 +- shaders/Copy/RootSignature.hlsl | 2 +- shaders/FilmGrain/RootSignature.hlsl | 2 +- shaders/HDRComposite/RootSignature.hlsl | 2 +- shaders/PostSharpen/RootSignature.hlsl | 2 +- shaders/ScaleformComposite/RootSignature.hlsl | 2 +- shaders/shared.hlsl | 7 +++--- 14 files changed, 45 insertions(+), 21 deletions(-) diff --git a/Plugin/dist/NativeHDR.toml b/Plugin/dist/NativeHDR.toml index a262a17e..0d2e3cbe 100644 --- a/Plugin/dist/NativeHDR.toml +++ b/Plugin/dist/NativeHDR.toml @@ -9,6 +9,8 @@ Contrast = 50.0 DisplayMode = 2 ColorGradingStrength = 100.0 LUTCorrectionStrength = 100.0 +FilmGrainType = 0 +PostSharpening = true [Dev] DevSetting01 = 0.0 diff --git a/Plugin/src/Hooks.cpp b/Plugin/src/Hooks.cpp index a2b8cb6a..8c1e2c3e 100644 --- a/Plugin/src/Hooks.cpp +++ b/Plugin/src/Hooks.cpp @@ -113,6 +113,8 @@ namespace Hooks CreateSliderSetting(a_settingList, settings->Contrast, settings->IsHDREnabled()); CreateSliderSetting(a_settingList, settings->LUTCorrectionStrength, true); CreateSliderSetting(a_settingList, settings->ColorGradingStrength, true); + CreateStepperSetting(a_settingList, settings->FilmGrainType, true); + CreateStepperSetting(a_settingList, settings->PostSharpening, true); CreateSeparator(a_settingList, Settings::SettingID::kEND); } @@ -129,7 +131,6 @@ namespace Hooks // This can be any data type, even a struct. It just has to match StructHdrDllPluginConstants in HLSL. const Settings::ShaderConstants data{ static_cast(*settings->DisplayMode.value), - bIsAtEndOfFrame.load(), static_cast(*settings->PeakBrightness.value), static_cast(*settings->GamePaperWhite.value), static_cast(*settings->UIPaperWhite.value), @@ -137,6 +138,9 @@ namespace Hooks static_cast(*settings->Contrast.value * 0.02f), // 0-100 to 0-2 static_cast(*settings->LUTCorrectionStrength.value * 0.01f), // 0-100 to 0-1 static_cast(*settings->ColorGradingStrength.value * 0.01f), // 0-100 to 0-1 + static_cast(*settings->FilmGrainType.value), + static_cast(*settings->PostSharpening.value), + bIsAtEndOfFrame.load(), static_cast(*settings->DevSetting01.value * 0.01f), // 0-100 to 0-1 static_cast(*settings->DevSetting02.value * 0.01f), // 0-100 to 0-1 static_cast(*settings->DevSetting03.value * 0.01f), // 0-100 to 0-1 @@ -323,6 +327,12 @@ namespace Hooks case static_cast(Settings::SettingID::kColorGradingStrength): HandleSetting(settings->ColorGradingStrength); break; + case static_cast(Settings::SettingID::kFilmGrainType): + HandleSetting(settings->FilmGrainType); + break; + case static_cast(Settings::SettingID::kPostSharpening): + HandleSetting(settings->PostSharpening); + break; } _SettingsDataModelFloatEvent(a_arg1, a_eventData); diff --git a/Plugin/src/Settings.cpp b/Plugin/src/Settings.cpp index 80cb93f4..aa74de81 100644 --- a/Plugin/src/Settings.cpp +++ b/Plugin/src/Settings.cpp @@ -64,6 +64,8 @@ namespace Settings config.Bind(Contrast.value, Contrast.defaultValue); config.Bind(LUTCorrectionStrength.value, LUTCorrectionStrength.defaultValue); config.Bind(ColorGradingStrength.value, ColorGradingStrength.defaultValue); + config.Bind(FilmGrainType.value, FilmGrainType.defaultValue); + config.Bind(PostSharpening.value, PostSharpening.defaultValue); config.Bind(DevSetting01.value, DevSetting01.defaultValue); config.Bind(DevSetting02.value, DevSetting02.defaultValue); config.Bind(DevSetting03.value, DevSetting03.defaultValue); diff --git a/Plugin/src/Settings.h b/Plugin/src/Settings.h index 4d29906f..c66d1eae 100644 --- a/Plugin/src/Settings.h +++ b/Plugin/src/Settings.h @@ -23,6 +23,8 @@ namespace Settings kHDR_Contrast, kLUTCorrectionStrength, kColorGradingStrength, + kFilmGrainType, + kPostSharpening, kEND, @@ -67,7 +69,6 @@ namespace Settings struct ShaderConstants { uint32_t DisplayMode; - bool bIsAtEndOfFrame; float PeakBrightness; float GamePaperWhite; float UIPaperWhite; @@ -75,6 +76,9 @@ namespace Settings float Contrast; float LUTCorrectionStrength; float ColorGradingStrength; + uint32_t FilmGrainType; + bool PostSharpening; + bool bIsAtEndOfFrame; float DevSetting01; float DevSetting02; float DevSetting03; @@ -86,15 +90,18 @@ namespace Settings class Main : public DKUtil::model::Singleton
{ public: - Stepper DisplayMode{ SettingID::kDisplayMode, "Display Mode", "Sets the game's display mode between SDR (Gamma 2.2 Rec.709), HDR10 BT.2020 PQ, or HDR scRGB", { "DisplayMode", "Main" }, { "SDR", "HDR10", "HDR scRGB" } }; + Stepper DisplayMode{ SettingID::kDisplayMode, "Display Mode", "Sets the game's display mode between SDR (Gamma 2.2 Rec.709), HDR10 BT.2020 PQ, or HDR scRGB. HDR scRGB offers the highest quality but might not be compatible with technologies like DLSS Super Resolution", { "DisplayMode", "Main" }, { "SDR", "HDR10", "HDR scRGB" } }; - Slider PeakBrightness{ SettingID::kHDR_PeakBrightness, "Peak Brightness", "Sets the peak nits brightness in HDR modes, this should match your display peak brightness", { "PeakBrightness", "HDR" }, 80.f, 10000.f, 1000.f }; - Slider GamePaperWhite{ SettingID::kHDR_GamePaperWhite, "Game Paper White", "Sets the game paper white nits brightness in HDR modes", { "GamePaperWhite", "HDR" }, 80.f, 500.f, 203.f }; - Slider UIPaperWhite{ SettingID::kHDR_UIPaperWhite, "UI Paper White", "Sets the UI paper white nits brightness in HDR modes", { "UIPaperWhite", "HDR" }, 80.f, 500.f, 203.f }; - Slider Saturation{ SettingID::kHDR_Saturation, "Saturation", "Sets the saturation strength in HDR modes (only applies if LUT correction is on) (neutral at 50)", { "Saturation", "HDR" }, 0.f, 100.f, 50.f }; + Slider PeakBrightness{ SettingID::kHDR_PeakBrightness, "Peak Brightness", "Sets the peak nits brightness in HDR modes, this should match your display peak brightness. This will not influence the game average brightness", { "PeakBrightness", "HDR" }, 80.f, 10000.f, 1000.f }; + Slider GamePaperWhite{ SettingID::kHDR_GamePaperWhite, "Game Paper White", "Sets the game paper white nits brightness in HDR modes. This influences the average brightness of the image without affecting the peak brightness. Change it to what looks best to you, the reference default is 203", { "GamePaperWhite", "HDR" }, 80.f, 500.f, 203.f }; + Slider UIPaperWhite{ SettingID::kHDR_UIPaperWhite, "UI Paper White", "Sets the UI paper white nits brightness in HDR modes. Change it to what looks best to you, the reference default is 203", { "UIPaperWhite", "HDR" }, 80.f, 500.f, 203.f }; + Slider Saturation{ SettingID::kHDR_Saturation, "Saturation", "Sets the saturation strength in HDR modes (only applies if \"LUT Correction\" is on) (neutral at 50)", { "Saturation", "HDR" }, 0.f, 100.f, 50.f }; Slider Contrast{ SettingID::kHDR_Contrast, "Contrast", "Sets the contrast strength in HDR modes (neutral at 50)", { "Contrast", "HDR" }, 0.f, 100.f, 50.f }; - Slider LUTCorrectionStrength{ SettingID::kLUTCorrectionStrength, "LUT Correction Strength", "Sets the LUT correction (normalization) strength", { "LUTCorrectionStrength", "Main" }, 0.f, 100.f, 100.f }; - Slider ColorGradingStrength{ SettingID::kColorGradingStrength, "Color Grading Strength", "Sets the color grading strength (e.g. LUTs)", { "ColorGradingStrength", "Main" }, 0.f, 100.f, 100.f }; + Slider LUTCorrectionStrength{ SettingID::kLUTCorrectionStrength, "LUT Correction Strength", "Sets the LUT correction (normalization) strength, this removes the fogginess from the game vanilla LUTs", { "LUTCorrectionStrength", "Main" }, 0.f, 100.f, 100.f }; + Slider ColorGradingStrength{ SettingID::kColorGradingStrength, "Color Grading Strength", "Sets the color grading strength (e.g. it disables LUTs)", { "ColorGradingStrength", "Main" }, 0.f, 100.f, 100.f }; + Stepper FilmGrainType{ SettingID::kFilmGrainType, "Film Grain Type", "Change the Film Grain type, pick the one that looks best to you", { "FilmGrainType", "Main" }, { "Vanilla", "Improved" } }; + Stepper PostSharpening{ SettingID::kPostSharpening, "Post Sharpening", "Allows you to disable the game default forced post sharpening pass", { "PostSharpening", "Main" }, { "Off", "On" } }; + #if 1 Slider DevSetting01{ SettingID::kDevSetting01, "DevSetting01", "Development setting", { "DevSetting01", "Dev" }, 0.f, 100.f, 0.f }; Slider DevSetting02{ SettingID::kDevSetting02, "DevSetting02", "Development setting", { "DevSetting02", "Dev" }, 0.f, 100.f, 0.f }; @@ -161,6 +168,8 @@ namespace Settings DrawReshadeSlider(settings, settings->Contrast); DrawReshadeSlider(settings, settings->LUTCorrectionStrength); DrawReshadeSlider(settings, settings->ColorGradingStrength); + DrawReshadeSlider(settings, settings->FilmGrainType); + DrawReshadeSlider(settings, settings->DisablePostSharpening); #if 1 DrawReshadeSlider(settings, settings->DevSetting01); DrawReshadeSlider(settings, settings->DevSetting02); diff --git a/shaders/BinkMovie/RootSignature.hlsl b/shaders/BinkMovie/RootSignature.hlsl index 3675f245..53132a95 100644 --- a/shaders/BinkMovie/RootSignature.hlsl +++ b/shaders/BinkMovie/RootSignature.hlsl @@ -7,5 +7,5 @@ "SRV(t2, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=14, b3), " \ + "RootConstants(num32BitConstants=16, b3), " \ "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " diff --git a/shaders/ColorGradingMerge/RootSignature.hlsl b/shaders/ColorGradingMerge/RootSignature.hlsl index c569a78b..35683874 100644 --- a/shaders/ColorGradingMerge/RootSignature.hlsl +++ b/shaders/ColorGradingMerge/RootSignature.hlsl @@ -53,7 +53,7 @@ "UAV(u0, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=14, b3), " \ + "RootConstants(num32BitConstants=16, b3), " \ "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ diff --git a/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl b/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl index 749fb1ec..1bfb9fd7 100644 --- a/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl +++ b/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl @@ -68,7 +68,7 @@ "), " \ \ "CBV(b0, space=8), " \ - "RootConstants(num32BitConstants=14, b3), " \ + "RootConstants(num32BitConstants=16, b3), " \ "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ diff --git a/shaders/Copy/Copy_ps.hlsl b/shaders/Copy/Copy_ps.hlsl index 6e3062b1..95313ee4 100644 --- a/shaders/Copy/Copy_ps.hlsl +++ b/shaders/Copy/Copy_ps.hlsl @@ -16,7 +16,7 @@ float4 PS(PSInputs inputs) : SV_Target { float4 color = inputTexture.Sample(inputSampler, float2(inputs.uv.x, inputs.uv.y)); - if (HdrDllPluginConstants.DisplayMode == 1 && HdrDllPluginConstants.bIsAtEndOfFrame) + if (HdrDllPluginConstants.DisplayMode == 1 && HdrDllPluginConstants.IsAtEndOfFrame) { float3 pq = linear_to_PQ(color.rgb); color.rgb = pq.rgb; diff --git a/shaders/Copy/RootSignature.hlsl b/shaders/Copy/RootSignature.hlsl index 03cab4cb..9c4cb6fe 100644 --- a/shaders/Copy/RootSignature.hlsl +++ b/shaders/Copy/RootSignature.hlsl @@ -9,4 +9,4 @@ "Sampler(s0, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=14, b3), " + "RootConstants(num32BitConstants=16, b3), " diff --git a/shaders/FilmGrain/RootSignature.hlsl b/shaders/FilmGrain/RootSignature.hlsl index 141cad2f..64e735d3 100644 --- a/shaders/FilmGrain/RootSignature.hlsl +++ b/shaders/FilmGrain/RootSignature.hlsl @@ -7,5 +7,5 @@ "SRV(t0, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=14, b3), " \ + "RootConstants(num32BitConstants=16, b3), " \ "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " diff --git a/shaders/HDRComposite/RootSignature.hlsl b/shaders/HDRComposite/RootSignature.hlsl index 6756b4bc..74c0835c 100644 --- a/shaders/HDRComposite/RootSignature.hlsl +++ b/shaders/HDRComposite/RootSignature.hlsl @@ -72,7 +72,7 @@ "SRV(t4, space=9) " \ "), " \ \ - "RootConstants(num32BitConstants=14, b3), " \ + "RootConstants(num32BitConstants=16, b3), " \ "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ diff --git a/shaders/PostSharpen/RootSignature.hlsl b/shaders/PostSharpen/RootSignature.hlsl index c0f4e2fc..3980a1f3 100644 --- a/shaders/PostSharpen/RootSignature.hlsl +++ b/shaders/PostSharpen/RootSignature.hlsl @@ -68,7 +68,7 @@ "SRV(t0, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=14, b3), " \ + "RootConstants(num32BitConstants=16, b3), " \ "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ diff --git a/shaders/ScaleformComposite/RootSignature.hlsl b/shaders/ScaleformComposite/RootSignature.hlsl index 12d0dca3..4cfeeb6c 100644 --- a/shaders/ScaleformComposite/RootSignature.hlsl +++ b/shaders/ScaleformComposite/RootSignature.hlsl @@ -7,5 +7,5 @@ "SRV(t0, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=14, b3), " \ + "RootConstants(num32BitConstants=16, b3), " \ "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index da49e285..b0549312 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -26,9 +26,7 @@ // Custom push constants uploaded by the HDR DLL plugin code. Do note that register space comes at a premium when adding members. Bit/byte packing is advised. struct StructHdrDllPluginConstants { - // SDR 0 (Rec.709 with 2.2 gamma, not sRGB), 1 HDR10 PQ BT.2020, 2 scRGB HDR - uint DisplayMode; - bool bIsAtEndOfFrame; + uint DisplayMode; // SDR 0 (Rec.709 with 2.2 gamma, not sRGB), 1 HDR10 PQ BT.2020, 2 scRGB HDR float HDRPeakBrightnessNits; // Set equal to the max nits your display can output float HDRGamePaperWhiteNits; // 203 is the reference value (ReferenceWhiteNits_BT2408) float HDRUIPaperWhiteNits; // 203 is the reference value (ReferenceWhiteNits_BT2408) @@ -36,6 +34,9 @@ struct StructHdrDllPluginConstants float HDRSecondaryContrast; // 1 is neutral float LUTCorrectionStrength; // 1 is full strength float ColorGradingStrength; // 1 is full strength + uint FilmGrainType; // 0 is default + bool PostSharpening; // true is default + bool IsAtEndOfFrame; float DevSetting01; // 0-1 variable for development float DevSetting02; // 0-1 variable for development float DevSetting03; // 0-1 variable for development From dbd311fd6f075ca3c5272d0d4068e44f0c52fae1 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sun, 8 Oct 2023 20:26:08 +0300 Subject: [PATCH 118/270] Fix BT.2020 --- shaders/Copy/Copy_ps.hlsl | 4 ++-- shaders/color.hlsl | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/shaders/Copy/Copy_ps.hlsl b/shaders/Copy/Copy_ps.hlsl index 95313ee4..d3eed527 100644 --- a/shaders/Copy/Copy_ps.hlsl +++ b/shaders/Copy/Copy_ps.hlsl @@ -18,8 +18,8 @@ float4 PS(PSInputs inputs) : SV_Target if (HdrDllPluginConstants.DisplayMode == 1 && HdrDllPluginConstants.IsAtEndOfFrame) { - float3 pq = linear_to_PQ(color.rgb); - color.rgb = pq.rgb; + // There is no need to clamp if "CLAMP_INPUT_OUTPUT" is true here as the output buffer is int so it will clip anything beyond 0-1. + color.rgb = linear_to_PQ(BT709_To_BT2020(color.rgb)); } return color; diff --git a/shaders/color.hlsl b/shaders/color.hlsl index 590b1f41..c4fb294b 100644 --- a/shaders/color.hlsl +++ b/shaders/color.hlsl @@ -85,20 +85,21 @@ float3 gamma_sRGB_to_linear(float3 Color) gamma_sRGB_to_linear(Color.g), gamma_sRGB_to_linear(Color.b)); } -// HDR10 PQ (Perceptual Quantiser - ST.2084) -float3 linear_to_PQ(float3 Color, const float PQMaxValue = PQMaxWhitePoint) + +// PQ (Perceptual Quantizer - ST.2084) encode/decode used for HDR10 BT.2100 +float3 linear_to_PQ(float3 LinearColor, const float PQMaxValue = PQMaxWhitePoint) { - Color /= PQMaxValue; - float3 colorPow = pow(Color, PQ_constant_N); + LinearColor /= PQMaxValue; + float3 colorPow = pow(LinearColor, PQ_constant_N); float3 numerator = PQ_constant_C1 + PQ_constant_C2 * colorPow; float3 denominator = 1.f + PQ_constant_C3 * colorPow; float3 pq = pow(numerator / denominator, PQ_constant_M); return pq; } -float3 PQ_to_Linear(float3 Color, const float PQMaxValue = PQMaxWhitePoint) +float3 PQ_to_Linear(float3 ST2084Color, const float PQMaxValue = PQMaxWhitePoint) { - float3 colorPow = pow(Color, 1.f / PQ_constant_M ); + float3 colorPow = pow(ST2084Color, 1.f / PQ_constant_M ); float3 numerator = max(colorPow - PQ_constant_C1, 0.f); float3 denominator = PQ_constant_C2 - (PQ_constant_C3 * colorPow); float3 linearColor = pow(numerator / denominator, 1.f / PQ_constant_N); From 41af4eaa91e185aa855d5ca5f520a2fc34630058 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sun, 8 Oct 2023 20:26:47 +0300 Subject: [PATCH 119/270] Add latest release --- .../Data/SFSE/Plugins/NativeHDR.toml | 32 ++++++++++++++++++ Release/Microsoft Store/NativeHDR.asi | Bin 0 -> 903168 bytes Release/Microsoft Store/SFShaderInjector.asi | Bin 0 -> 458752 bytes Release/ReadMe.txt | 31 +++++++++++++++++ .../Steam/Data/SFSE/Plugins/NativeHDR.toml | 32 ++++++++++++++++++ 5 files changed, 95 insertions(+) create mode 100644 Release/Microsoft Store/Data/SFSE/Plugins/NativeHDR.toml create mode 100644 Release/Microsoft Store/NativeHDR.asi create mode 100644 Release/Microsoft Store/SFShaderInjector.asi create mode 100644 Release/ReadMe.txt create mode 100644 Release/Steam/Data/SFSE/Plugins/NativeHDR.toml diff --git a/Release/Microsoft Store/Data/SFSE/Plugins/NativeHDR.toml b/Release/Microsoft Store/Data/SFSE/Plugins/NativeHDR.toml new file mode 100644 index 00000000..b2e6e225 --- /dev/null +++ b/Release/Microsoft Store/Data/SFSE/Plugins/NativeHDR.toml @@ -0,0 +1,32 @@ +[Dev] +DevSetting01 = 0.0 +DevSetting02 = 0.0 + +[HDR] +Contrast = 50.0 +GamePaperWhite = 203.0 +PeakBrightness = 1000.0 +Saturation = 50.0 +UIPaperWhite = 203.0 + +[Main] +ColorGradingStrength = 100.0 +DisplayMode = 0 +LUTCorrectionStrength = 100.0 + +[RenderTargets] +RenderTargetsToUpgrade = [ + 'ImageSpaceBuffer', + 'ScaleformCompositeBuffer', + 'SF_ColorBuffer', + 'HDRImagespaceBuffer', + 'ImageSpaceHalfResBuffer', + 'ImageProcessColorTarget', + 'ImageSpaceBufferB10G11R11', + 'ImageSpaceBufferE5B9G9R9', + 'TAA_idTech7HistoryColorTarget', + 'EnvBRDF', + 'ImageSpaceBufferR10G10B10A2', + 'NativeResolutionColorBuffer01', + 'ColorBuffer01' +] \ No newline at end of file diff --git a/Release/Microsoft Store/NativeHDR.asi b/Release/Microsoft Store/NativeHDR.asi new file mode 100644 index 0000000000000000000000000000000000000000..44ea70c52ab8c1d5e8a0e3d88e1f1819be4eed1d GIT binary patch literal 903168 zcmd?Sd3+Q_8b3TahD;#P6V52C3>qbhVkD@6sLVhHdUOIo4WL}{g1C4f%n%fTgqaDM zZChDaS6z>dt1Gy=D_&f#kN^@81;ky&V;4MDdKg{f1qrLn@B2K}-P04o_4~Z<`_Jpg zFx}NvPd)Y2^E~y`Q%_ZuUBAqhVYAsX@s~*0Y-{jK|GD|!&)u=t%U@mkmGO7o_MTsr;@$gBX#XVs4ey`8zf1Pd=HEJg@4LNr|9HIj-CnnU zGJeZnt^4Y(SC)P?qiD(g3cSxKx?lfY%K2s#&Hsw>m%r-d-x);_PQUuro2sbp_AM>< z+H5!1b+X;}{+YKJ-&$;Ey1JxyJKL6%ZL{qoirH*mu5;LIPJXNbImC0i&DM#NlD{@r zH$h~iXV@x{RTMIQj(!&@%j{ikvk6wGuC^+!pqtG>RX23C)lZ=h`hQO91|&OjY{f>J z^*1}mR)F`)tQ?!0;piV-*|ynP){Lpog9EqB4ev?tY+cC;teT?$EMfucTA60MERmldig6-%d1_l z1LRnNfsfktF?!ESzJEmT4!ol;ugk{s&rc;=08>6DaEV@L1KPn&X@Rcm^1#EgvQwK5+M)XUB)o@OI|WZm z^m-k`gRE>_=l6L=la@e7|Amt9XssReM1P{vKz?4uM9;OJ37(0v`eMGDXj}VrBL*f} zu{UBokc&3U(a1{FQN?<5Qy*16=dK#mVazvyo(}I~< zfYgGdQPkVfQt(4_X@2op$frzmhyG{xNKb8+)%)v_p!LbhvXx4!T9LdcGu#+hAgf~? za%hpuIVV$A@EG;yyJcl`9s!c20zjJ~Eyll60Y2an|4L48jdZTp_PyjRTlT$_TjrEi zsud+%p0##r2;SYYS^-4M+}c?mcd~hsa zuCg3Wnk`z{AUAtgP=g3C(kmJO*ru&h9JXc1^EB2ukXiZs*bZ%L> zO3GoV$l{gcu+8^~2gEn`~wk3L<3mskFEWy2l$k&TJSO8);x-6K)-nsez#WvegLIM71k>h8eLr|bgFNM2*V0^CHeSzMg-8|0E|Q0xV#>2ev)=jKBy zK*^7M3NvY?M@a*(x@9Gt-@*mZ$Z&ouJtAj0gP))0BuY3WCrgyu$-GNkR!NxvZaXo2 z0m%;d?!NpQ6sOECPL|H4WaKUw(iQla@(Ee0U zUAakC?i1Lf^Ts`dq}t7m$ZiZ<+#j>i*pfWGu?A{{sY`NpE^6d@Sg@(jkX+972n?xO z76_MCXw>LO>rmak-=VtD=GjJFJU&Km%ZFMC+SS8F1rsqB@N5wdcPn|bE~S-A?j_xQ}?CFUu9#9>=UcAq~wx0tBj zJjzWBR9Zi`K5EB3D+EvMFnZvf0_48lrle z!Nh7=mbN^Q4vE+V`XuDUrkcz8+k%I*-%zpW!e%)-b*GtkzqWw#h8Fg*1#X9S$wK*N zZT}*qsTKSAW!>#~Q7RH3uw%|JS?yVY(FZAI`%glPoQ*;79c}x=4luE|S<3b9ha&wD zqr)M;8r+Y16Ar&L3Qx+yLz?y*kX>no{eXS6X-JPouu5cP9SA(JHSYJKP;`krP-v+ zkYiA$R|iVHdaSkvqDR1<(19JAQ5wAkWT@zW6rHn9g3fP3%fF-DMDna`LEj7{d7HgV z=;j7Aq(j}@6mw}Wgwj#+^&0#uH^p7bcI`1rG5$04bCsc=FTw~_ZjrHm9VlpNDfv-I z&UTT^H;<~sEB#ink{(qKG_QdmLEV$c7Fxh{sPAtlP!R#jt%H&Us6ql&V1ja<2-KMb zYPbO9(m@pnP)>8~xWi#cDaog>zhQDKZQ9pI5(!~&&qrZ0xc~bg5&c*OH>?8BnIx#X zovPDQwGX0HsLyEH!HsEw&g*Cx)BeP;tn&zrU&Vg4>tCUbN#hg381O&9Yte2^eHvAV zHWyn=Og;bQsrjE1`KKN;|HFM!;Lj8JFX$k@)qb5ItGDI1Kvy|Q$`pH6cSBcE(`xQ6 zw*>~tN-gmMn>yghPgTK_R02F%0DGa3?1gEt70|%KnaGRZp~7IiSe?@a8k*FvdhvUB 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�gvO{s8bY`^QDP8 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≧on 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 Pk##SPC>IaZ|L*k<@>9`! literal 0 HcmV?d00001 diff --git a/Release/Microsoft Store/SFShaderInjector.asi b/Release/Microsoft Store/SFShaderInjector.asi new file mode 100644 index 0000000000000000000000000000000000000000..b913e69412e98eff368d7bd49d56f104fd4c0fd1 GIT binary patch literal 458752 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%0A(Gt%x}Z>rB(>zudq`xA}5vDW#{ozZWKbtZjH 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%0D_ZDOw$t96pL9!l~}D=e+i%giDz-1QB=g|CN60 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 MWOdC`9^T~BdS_65?#bo2J1pC={* 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 Isu%G3CqZ1MZ~y=R literal 0 HcmV?d00001 diff --git a/Release/ReadMe.txt b/Release/ReadMe.txt new file mode 100644 index 00000000..8141e91f --- /dev/null +++ b/Release/ReadMe.txt @@ -0,0 +1,31 @@ +This mod was created by 5 (HDR passionate) developers (Ersh, Pumbo (Filoppi / Filippo T.), Lilium, Nukem and ShortFuse), whom all took on different roles to achieve a large refactor of the game post processing phase. +The main thing the mod does is adding Native HDR support to the game, which is not the same as the one seen on Xbox, this version changes the game tonemapper and color correction phase to give a much more HDR look, while also trying to retaining the look and feel of the vanilla game. This was achieved by replacing the game shaders. +This mod also improves the look of the game in SDR, so benefits are not restricted to HDR users. +This is a follow up to https://www.nexusmods.com/starfield/mods/588, which just increased the quality of the game buffers to 10bit+, leaving more room for AutoHDR to work. + +List of features: +-Native HDR (scRGB and HDR10) +-Increased buffers accuracy in SDR and HDR, which reduced banding all around (SDR is now 10bit instead of 8bit) +-Normalized LUTs, the grey/flat look is mostly gone, but the color tint is still there (this mod includes a runtime version of https://www.nexusmods.com/starfield/mods/2407) +-Fixed bink fullscreen videos playing back with the wrong colors (BT.709 instead of BT.601) +-Improved the sharpening passes +-Improved film grain to be more realistic and nice to look at (e.g. rebalancing the grain size and strength on white/black colors) +-Fixed the game using very wrong gamma formulas +-Customization settings for you to personalize the game visuals + +How to use: +Drop the content of the mod into the game root folder, or install it with a mod manager in the game root folder. +The game needs to be started through "sfse_loader.exe". +You can configure the mod from the game graphics setting menu. It also hooks to ReShade if you have installed, you will see the settings menu as a small ReShade widget. +There's also a config file in the dll folders, delete it to reset settings. +To uninstall, clear all the files (they are unique to the mod). + +Compatibility: +This mod should work with any other mod, including DLSS Super Resolution and DLSS Frame Generation (scRGB is NOT supported by FG). +You can also use customized LUTs mods, but if you do, make sure to disable "LUTs Correction" in the mods settings, or they will be corrected twice. Our suggestion is to use the LUT correction that is built in with the mod. +You do NOT need any other HDR related mod, like AutoHDR, or "HUD for HDR" (which dimmed the UI for AutoHDR). + +Issues: +-The UI blends in slightly differently in HDR, usually it's not noticeable +-scRGB HDR doesn't work with DLSS Frame Generation +-You can not take screenshots directly from Starfield in photo mode \ No newline at end of file diff --git a/Release/Steam/Data/SFSE/Plugins/NativeHDR.toml b/Release/Steam/Data/SFSE/Plugins/NativeHDR.toml new file mode 100644 index 00000000..b2e6e225 --- /dev/null +++ b/Release/Steam/Data/SFSE/Plugins/NativeHDR.toml @@ -0,0 +1,32 @@ +[Dev] +DevSetting01 = 0.0 +DevSetting02 = 0.0 + +[HDR] +Contrast = 50.0 +GamePaperWhite = 203.0 +PeakBrightness = 1000.0 +Saturation = 50.0 +UIPaperWhite = 203.0 + +[Main] +ColorGradingStrength = 100.0 +DisplayMode = 0 +LUTCorrectionStrength = 100.0 + +[RenderTargets] +RenderTargetsToUpgrade = [ + 'ImageSpaceBuffer', + 'ScaleformCompositeBuffer', + 'SF_ColorBuffer', + 'HDRImagespaceBuffer', + 'ImageSpaceHalfResBuffer', + 'ImageProcessColorTarget', + 'ImageSpaceBufferB10G11R11', + 'ImageSpaceBufferE5B9G9R9', + 'TAA_idTech7HistoryColorTarget', + 'EnvBRDF', + 'ImageSpaceBufferR10G10B10A2', + 'NativeResolutionColorBuffer01', + 'ColorBuffer01' +] \ No newline at end of file From 9ad635c6bed7c050abb164fee895eddb6209825d Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sun, 8 Oct 2023 20:29:28 +0300 Subject: [PATCH 120/270] Fix up shaders variables being bool and risking mismatch due to padding --- Plugin/src/Hooks.cpp | 4 ++-- Plugin/src/Settings.h | 5 +++-- shaders/shared.hlsl | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Plugin/src/Hooks.cpp b/Plugin/src/Hooks.cpp index 8c1e2c3e..0c64bd99 100644 --- a/Plugin/src/Hooks.cpp +++ b/Plugin/src/Hooks.cpp @@ -139,8 +139,8 @@ namespace Hooks static_cast(*settings->LUTCorrectionStrength.value * 0.01f), // 0-100 to 0-1 static_cast(*settings->ColorGradingStrength.value * 0.01f), // 0-100 to 0-1 static_cast(*settings->FilmGrainType.value), - static_cast(*settings->PostSharpening.value), - bIsAtEndOfFrame.load(), + static_cast(*settings->PostSharpening.value), + static_cast(bIsAtEndOfFrame.load()), static_cast(*settings->DevSetting01.value * 0.01f), // 0-100 to 0-1 static_cast(*settings->DevSetting02.value * 0.01f), // 0-100 to 0-1 static_cast(*settings->DevSetting03.value * 0.01f), // 0-100 to 0-1 diff --git a/Plugin/src/Settings.h b/Plugin/src/Settings.h index c66d1eae..8a33da2e 100644 --- a/Plugin/src/Settings.h +++ b/Plugin/src/Settings.h @@ -66,6 +66,7 @@ namespace Settings void SetValueFromSlider(float a_percentage); }; + // Bools are set as uint to avoid padding inconsistencies between c++ and hlsl. struct ShaderConstants { uint32_t DisplayMode; @@ -77,8 +78,8 @@ namespace Settings float LUTCorrectionStrength; float ColorGradingStrength; uint32_t FilmGrainType; - bool PostSharpening; - bool bIsAtEndOfFrame; + uint32_t PostSharpening; + uint32_t bIsAtEndOfFrame; float DevSetting01; float DevSetting02; float DevSetting03; diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index b0549312..82a4836e 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -35,8 +35,8 @@ struct StructHdrDllPluginConstants float LUTCorrectionStrength; // 1 is full strength float ColorGradingStrength; // 1 is full strength uint FilmGrainType; // 0 is default - bool PostSharpening; // true is default - bool IsAtEndOfFrame; + uint PostSharpening; // true is default + uint IsAtEndOfFrame; float DevSetting01; // 0-1 variable for development float DevSetting02; // 0-1 variable for development float DevSetting03; // 0-1 variable for development From 123447625b4b40640c6ea75341c117cc1df4f64a Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sun, 8 Oct 2023 20:32:44 +0300 Subject: [PATCH 121/270] improve ReadMe --- Release/ReadMe.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Release/ReadMe.txt b/Release/ReadMe.txt index 8141e91f..a79eae12 100644 --- a/Release/ReadMe.txt +++ b/Release/ReadMe.txt @@ -15,7 +15,7 @@ List of features: How to use: Drop the content of the mod into the game root folder, or install it with a mod manager in the game root folder. -The game needs to be started through "sfse_loader.exe". +The game needs to be started through the SFSE loader on Steam and ASI loader on Microsoft Store (Game Pass). You can configure the mod from the game graphics setting menu. It also hooks to ReShade if you have installed, you will see the settings menu as a small ReShade widget. There's also a config file in the dll folders, delete it to reset settings. To uninstall, clear all the files (they are unique to the mod). From 94d6a79b0c16e9526f45bb3d0dd738b5ac67e191 Mon Sep 17 00:00:00 2001 From: Lilium Date: Sun, 8 Oct 2023 21:50:26 +0200 Subject: [PATCH 122/270] fix MS store setup --- Release/Microsoft Store/README.txt | 1 + Release/Microsoft Store/SFShaderInjector.asi | Bin 458752 -> 458240 bytes Release/Microsoft Store/bink2w64.dll | Bin 0 -> 344576 bytes 3 files changed, 1 insertion(+) create mode 100644 Release/Microsoft Store/README.txt create mode 100644 Release/Microsoft Store/bink2w64.dll diff --git a/Release/Microsoft Store/README.txt b/Release/Microsoft Store/README.txt new file mode 100644 index 00000000..9f884543 --- /dev/null +++ b/Release/Microsoft Store/README.txt @@ -0,0 +1 @@ +rename original "bink2w64.dll" to "bink2w64Hooked.dll" and then place the "bink2w64.dll" from this archive next to the exe diff --git a/Release/Microsoft Store/SFShaderInjector.asi b/Release/Microsoft Store/SFShaderInjector.asi index b913e69412e98eff368d7bd49d56f104fd4c0fd1..3fc5baa78f627f01b7557b940d76e8ac61e76700 100644 GIT binary patch delta 86990 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%0Ar4@?L)Wa@y)=l~o} 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 delta 87435 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≫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

Qqsf(Ptrw*;evD;D9opUk?j*{U_`d! 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�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 literal 0 HcmV?d00001 From 9c2375dfb10b4a2083f084a16010ee67b322c068 Mon Sep 17 00:00:00 2001 From: ersh1 <16157893+ersh1@users.noreply.github.com> Date: Sun, 8 Oct 2023 22:08:44 +0200 Subject: [PATCH 123/270] hooked up new settings, fixed up autopackage rules --- .gitignore | 4 + {Release => Plugin/dist}/ReadMe.txt | 0 .../dist}/SFShaderInjector.asi | Bin Plugin/dist/SFShaderInjector.dll | Bin 0 -> 456704 bytes Plugin/dist/rules/publish_asI_package.json | 75 ++++++++++ ...package.json => publish_sfse_package.json} | 32 +++-- Plugin/src/Hooks.cpp | 76 +++++++---- Plugin/src/Settings.cpp | 4 +- Plugin/src/Settings.h | 113 ++++++++++++--- .../Data/SFSE/Plugins/NativeHDR.toml | 32 ----- Release/Microsoft Store/NativeHDR.asi | Bin 903168 -> 0 bytes .../Steam/Data/SFSE/Plugins/NativeHDR.toml | 32 ----- shaders/FilmGrain/FilmGrain_ps.hlsl | 129 ++++++++++-------- shaders/PostSharpen/PostSharpen_ps.hlsl | 114 ++++++++-------- shaders/compile_all_shaders.ps1 | 6 + shaders/shared.hlsl | 4 +- 16 files changed, 393 insertions(+), 228 deletions(-) rename {Release => Plugin/dist}/ReadMe.txt (100%) rename {Release/Microsoft Store => Plugin/dist}/SFShaderInjector.asi (100%) create mode 100644 Plugin/dist/SFShaderInjector.dll create mode 100644 Plugin/dist/rules/publish_asI_package.json rename Plugin/dist/rules/{publish_package.json => publish_sfse_package.json} (53%) delete mode 100644 Release/Microsoft Store/Data/SFSE/Plugins/NativeHDR.toml delete mode 100644 Release/Microsoft Store/NativeHDR.asi delete mode 100644 Release/Steam/Data/SFSE/Plugins/NativeHDR.toml diff --git a/.gitignore b/.gitignore index ecbd4425..eefae416 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ deploy-*.ps1 shaders/target_folder.txt tools/dxc_analysis_tools/dndxc.settings.xml + +Plugin/dist/Data/* +Plugin/dist/ASI/* +Plugin/dist/SFSE/* diff --git a/Release/ReadMe.txt b/Plugin/dist/ReadMe.txt similarity index 100% rename from Release/ReadMe.txt rename to Plugin/dist/ReadMe.txt diff --git a/Release/Microsoft Store/SFShaderInjector.asi b/Plugin/dist/SFShaderInjector.asi similarity index 100% rename from Release/Microsoft Store/SFShaderInjector.asi rename to Plugin/dist/SFShaderInjector.asi diff --git a/Plugin/dist/SFShaderInjector.dll b/Plugin/dist/SFShaderInjector.dll new file mode 100644 index 0000000000000000000000000000000000000000..1d3a8747564c077084da4833319e872f1ee9dc90 GIT binary patch literal 456704 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

  • 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�_+Xm94NLtyIjGWY|Dj~QGr8371${oesW14Xp-(%7dPAR; zJnzan1$}!br&QEwLLt_;7fV7dgt8^KUfVYB=6F^U` zbw}~^{{-k(d>0H$@%(EzZY(a7FDoy|*nJAHVo$`B$Wbcgb)W7JM(B)qevy1+8r#Af zGdW{1PnKWWbKCv1TbRQ=9D^`knd}(OLgf8IbxsjpS15)_&e)Ip`38Z$oH&xxO)kE~ zf2`z_Uf5v@4kK>t2mCv~mV`gvMK7AGdj;usN*yIg`>|k1-%m?;4p>{hmj8fxOkv+< zr(QOG&DTfvUA~7kQ!{^e*y7Rrpd5X{GtI_9_N@w;sjtdYY`EcjZ4B*Z)l1v2`*C%D z>8s(svoxi8?FDHd8dXuGQ7YB%+oGQ>eydPSZjl0{A>I2Q> zsNoSCz{tl|cQohIR>cZF=Et3cwB_`g^#AR_4@z!G@q>gpaI2Zm)1yS*BdS7*i(333 ze76e+>0N}~HYS&wjW2NE+3Kgwe>0TlGwRYdR^%4D2w#ugw}<<*bv{1j51l$Euguxl zykgQ;=pKFctxmQmqMWyB_;A@cqJZK{bsS9fJPufi!Qc$)FZ=V@xW$;pG}TdX7~uAGu|J~&u)TQMTf*WA1lX0Q}e|tHF|YiTQ!(R@bgQYL7W~r z=Gh$jl#>^d)Z%Z{;nO^wWgoiB@5m1hm9yjVCRh3BfuVgJm~Pkm!s{{UbA(S=ESz!7 za^exFBTY%p=p}}Tr)0MZB;aj$XIa2)xqF`!n7uAF&NwIMJ;u9KRoYQvdz`IHK*}9{ zCmmETOIgQ1RE+`5@A$Lhb^h5@Iom4M^UGeZT+uz>9!<5)h><^yD(=T^_P^e7~ zTwD&yH^uqah2e-o_4HTc@zj)bgKszbWx=_>3;n)GXpi3McQ(}?fqu7|mr|$){cbhS z;&JSBMZfn7{hlE7i);6bu@BH1{LvfBB^O30?kc}EKZrdpw}{Xk7Cl;&mT6O3AWA3& zi_(-1GG+w*?M`V+p|nnqm?pgq@pMXSOHkTY^Ly6_rG3$b(%Mp#_Fjt8wwkv}S;s+D zqt764c60%oT^CxLU7Up8YEGgAlol6COKYT*$07cLEr3)adp1bw&)*+4u*!H9G&a0 zN^!nZlWxwkv6rT(-9RD7zwv@VJSX>~@qgoS>~qooWqVVr|Cmd4awFC%S1s&GlCY1cHxRX@vwwh;9D2Y2MMx4vzSba(~ zEVf0%me^FTL^SAa>qXAhF3!&gj`bPY$c9%qvr}7{N7O9RPjtbqA(d+|I2cV}*9O5Z zJ*sEwj*T7JWqXY+3F%R+jxRE#H=nk+hfDM{-379G4Ur!Eh1Zvh^q7&-)6y9G)mI4G z9RlsjJIX{)JI`(OB|S={ti?Ub6GtCKiX`HlMDIYDoMQ$OHID^Fq3o*)zYxssYk$Hgq5j{;#K4JeK4_7R$%KJHHYokG zn^4Ek%RLn5ME64~b!6FwK*d8ZrEO69h>RrWS2jv1JnjOfmOabl0S>ouX-TJudFCnY zv>Nkdoh931K0!#tvUuY7WO9bWO1xX%3R}!?Qpj;o>XCQ?1}PRFhk{m!H1~@%PgHgz zO>9qj0)f~&y6rld=6MVh!&$RU@ zZdGW*vWdG(SDUzO-hZr*%XsUvoF&Ie?1Ym~$;gm=mU2=`@?7EyXDnkykAZzcK|_4; zh<$GoUUUJRl%<@w;7FDW%X$CYjuZQORU+1w)se==#(f_}FJd45<>o^>unBkK=q9wpM3Hr_EqbC7+FVxSgh_}F)RcNdQLIlTT8We-8Zwijs z_qty3lE7abUI#i7El_!6EBN>R3iwa5;olJ*eO&VwImUVczKLYVVmK$aPpb~GDAn;r z;7=8|t*t zSD*NW6QuM7AEC?y%LQ}y`zodvEIE#Os>xR3KXpn_Mh=F1P9SY@^ zV&=S7k9<1eyZ?acYWhZJ4v`-&OrQ!ny9(DT!C6Q8nqWKJ0@%^uZfx#scx{s_)u zb<=6S#nHz8wFD|!A7z`rJKB$Oi_Arb{ic*Y6bnzOZp|Kip0uP#WcXTJS=l7QDlp)kg!~VL*SKO(d|fa zg5qachl+Yte&n>#G&!gNMsQ6C7*aA2M#`C?-G86pQdB~`b*tDWcCZHy3%&b{;{%*) zmob0G*QcJ9Y^tCA5%aM0sh1uktGbq5>dV5I!|cLD_%0R~+Vov(Iw?Kt)TL*1R>iG8 zJTUs%LtCGWT%G3*FC)5jeVafG%Ih0o3R+Xk&B?z>T91jBXPKMQ?Z>SDRDXNzrzD-m z7^%*SD~5~zpv!!!?+=pj>gT{zFR!Xns60{WC1C;&Uw=dFPm`1)g?cTYRL;+jRl>%xomj1ZxNAOX9eWGo%bFj_deC{F|Ba{9$&XnG{+i;c(_^!zNXc$z_o?Y+7|> z(mdU;5+NHZUX$kU0{R zV-jTdY*DQ|5IesewUY=55@tu1BJKQ2oH!ypx}c0LqSe){dz7d6w%1Hl@N->(T|p!Q z+#;)NFEN2d8i)XD3JupK$vPg6kqg*4D3+{q*RItFsJQ>!V1DiXalrxb^_d-~q4&>} z7-=0&xq|zHc~Ebbio_o0mT%(uZ~uz*Ws_F_FCIvgR3uE5h?{E5LZCf=wjN%;jbM(t z@tH3&)Ng#@H{O*XT60Ah&BYGyH$G%-sLcCG`@Xh{(CeLk<8AU*sRn|ix;cJwH23$W z7*8E9>e}<c|W&qLd)G zi*f{^R8fLmr0g;OtR&{e{wUG4qDHAyKx^`uxmYn?yN-%*Iq!lwhF;n+z%UkZvwW({ zFqX&{#=zyeM>oE^M{WoN#<@d{3>*Rnv*t-`0?%+Do3GCl<&ow~;WUKO@SsCStc1sO zjfb1eQMe{CyiI1wjVitwd3K1VH~Iy!EEsKM;_R7G?Vj9lJTsJ*QLS#BG)(O1E@{16 zzLt!p^psq7`)BRP8AGmoqR&WOPyRkT<6gpx=dAoK;z4b}AuE2&Q}e2uCC=1?T*gCh z5cKg|Qi4&;S&oE=4*BSZM3|a-tBfc*Xmnw!M9mX^B4aJv$!o$8q|BBqMeOzKdx z3ftE;uGAMG5N4lC6BtGKI#JD88WUQoSb!)pygY4{vV$+u;!CaM){ZarE4l3QPugdS z?D%hmM0>_gz=CC$m$d*9h$Og~t9~z29W?~ph!3+<;lgz?lX3t@qNgOsr0TPWf+u-& zav|AX^QaF)1?@~I@KtOGo$8N{FZ4Uu8=L~Sp5hFYGzZzudC7b}M6!xwr%2S9R6TNx^+$3SNSWdiI>&3gy|_G01u%dF`}`zvVXR=JR-w@R_3jK5m4-VPvYm+ z@=W;X6?x77BH(6A^c$sm#TBUZY0FSXF~R9-9G&UXR<&9>J;%6UDf9Ti)%4FSS5|8x z;7wMO>zqzkZCjtUR1$b`LSDy>*f~&=b6yb{RO}&uKNJm2gmx6Iz@f`?q{Bk#S}_qO zy=5EZeX&eDZwrOuRETF@bE_`puLS12e!0w;1STZd*GrH?!s5lGRjWypOcwqMUtaJy zZ`#`RLnydY%*5%@AKxSf!G`zk&9J6H|4U-v)FlXHx7gC! z@}E*v3?`?-b69sVylK_uPd>-O!?$xS&UykmHz+!7rp^!}lTcc?G)SiPviu!l}E_iTx8=~y;8GKDkKjI7W6`V1L-)D4h7HL@Wr9{3ZW~&!iq;y zWX$u059Rr_3G4Nz6ZuP^DwQ^a$h;$cYZ7TpR#^E+R5Fs6MAwmgBt9c5RHvV@QFowd zG&mLNU)N5M_n&k!YrzS+aZ$UyUmAr+NLmuHCvYh}lEw-5{32YfFqF%oKeGw#fS~I` zM7MJ(=)7@O8z5s>~;^v3QV!8B6h?cV&picX!G(MxRkIMY+jU7=+1b z*Z;=j0b@Xb3Bu{F39M2Ww$*&}qBNMlYF@BH7VQIAw-fKy7dhUivbc0jzphfDN9U{L z>qnLPe3O|;-=Q_~ECKqn`ki)`u(2@vQdtb7Q{0bdCMj-(Tqge6@U!!m!x2gHW=kYp zbW7MI)I}~rE{a7+^T_Qqf3;W5hXRec=#WA_65|{_l87~e?j{uq=}($|qDL3&WqakM zUc~|OvKZ4yBEKP+Cq1j!s(r5|UWM>nSPl1KkS*vF<;^y>0YaBV zmja*o-4d^It&ndD_=XFWX7gBj$)*^H8Ai*x9-~{KeU(0$Tdqn@=32Q-`~-$m!RpAmK9+><1A7&SUF-`a0GJQ zq4PYf1B)6IAe33}&ro*WGtUS@geqT$_c1i0r)0s;?+e1lwWo$uvQ(K92|VU$T>2X= z&p|x&HZ5;T9x>5(5D=SC=~mVrPh*85d4DwUm{+oo%xGiXVZORR$h}kYevmC@>BF@6 zu=(Wuk~+q|`K5L9GhIEH@vu0+&HF;}g`>zObcyH|BhU!a`_#($b|Y}UsI-T`c2Q{y zgmu@g6TnG^4J)6}tq%e01pdIrBr^Gj-%wp(d=%vy5W7@Vd=Q&o5_Lm@s^B{fj1Z(u z3;nC+&E}8jzt)u1ec+O|jht)eC6y6bU0YTeD@d=^2+0DV9?{rU%VZMw6S-H6DY{fz z?jv&VIQ;+Uj^T1vv@anPHiwbPNA@bLYegb*FD1pW0W+46%IGp|0Pa;U*BFyLe9xCd z!}|vWSt(oLhYNJ$fg-iy2V$GSKvBq~*XatGKus2LdVJImzm>!lGc>b90;99S=vqEza3z{U2+WSH70x`^y zOI`6(mG9Cos~W6vKnT=1phs1{wO!_G&mAH}A!{^n1l>+{6%EX3WwZc7m5NO+S$k4n zvGrT(LK*6`g7=f?%Fe|?1_)1i(Me>JV5<&o92{vDg}h>?w!EI9ipN#8FPhE5Op@#E z6Qr7}OS(N`xL>SCvyUUQ)Pj?zM6)rN?O8}7#Jvl1Fqw566EF_>Ne@A>gIJP@wJDyz z=D0Z${DrRLPO`;3n>#;=jiSRx21?qdL~}3I9j}_lsi(9f`gC>2se;4Bj9a3`%h1)h zx0-%66O8CO)9*40)`f$i^W0`67Sh7Rb}ea_?z83z)f- zQfL7U`==ef1M;~mPk)BVl7IIidjgn2OIU(`EZIiyTOf4FGQUbUZUrz)(EKBq0Rz#Z z)<>(v%KU^3rUS0#=UFKnasI2XS_N z!PIDxWG58MdGs5kV+j^5ni4(bB3;t$d`&k_1)%0nl{pEmp3{Bgk}|)=J*iPD0>*mg zwW2E-w2rZ@Hou883@rbwarwt#JvOQ(^%n0*g{5UX>CPDeP#yX6-?<_ zu5N9UDwk|7+S#RCqD4@n7bPhv-^?53tLT(c^VVZUIA2k$R(|ZY!o10raJW2Hr;C_f zB|MTu#Z)7|3L>rK@RL@tu;!l>6-J+Ej}-t|gw{dMvxx9rp)dxG>Hw^c6Q!Y4maL@; zYAD|j_Y~ORmDGX-yI6>`jdaNJB_ANHEFGO& zvw)r(!6?LtJYkd7NUHz#`K9qH9pnf6+f^%08{fP=Bz0*(!rVS2>- z6hrk#y0nkOuv`*9jfkKhM3u(526BW;p+%@n+J1vQ} zd^i=F4_;25qxui6Odb|Te*wL6E2PPcdUpsR&5101q1D@rqTzkPYWyklPd_8=(5g=C zOGnx2uc**Gkyc&xBzI^~a1T5c&mU}HGF1|#NBgV4#;E5+yj?pn#c7_`trHe?bQyYn zFGF8gV*X&d>OipAn38SoV6_`>tteZ1W9-MQb>sPmBG5)Wf1mZUM}C;f$EB}KrJcqk zIrG*O!8JyT4tt4TDB_W#-BP-4ogh|vmN*Kl$(#L?Bo;rew3on}<_r7eSu|(4JPJ zcBzHGye=0TEBTFR0(|xq70Ln|##~B%+j-~<)(VVOVsSkGQu-npTy9INN8X4j^g>CY z1P?u;Vd{sCfYsEhWUQnZl#T+vgOe?VR)&H?i)5)O#+F2umnat2=mSdAHAg*?gx-%> z`N;EXJ}{Rmy9Lm?h{`Q!4gM?zElWR3w?D9lusAOxcnN%>z`SviVn25plWrOZ8r&P4 z(K?X2rxlo2@v4g3JY~c8QYnd{Ld(_KzpJT% zwdACmRQ?n5or%D}$`!IpE-n688eZ%KD7%7V%_d)F#@GqY;3X{TtOR3N(wQ9d=e&Sb z<|>kLz$3|OqH?yFD^#68Bg1S+Re!gtK4DK20WXr(ma8teD5<;3_R5BHG-t;T7@=5# zrso;?2fmxOv$h6rKYEtEE&7ctO!Zs7lbvB?XO7Lur;zsGZ+MA_EwGlecL_j{8`Z)& zCHs`5o}pPzUnzXESFZPcQ&z6%-mRXZ(_t}`VW0@GVZ^kM^Izn%noulzg~!XXu3DsD zJpc1L8NT+trj8skK|R#eIH8a%QUfy_8Eq3T%E}nZPkC;}G-|HF2EJD5t4bXOQZ8jY zV@vM_x^G?%Aj9LX;1YIpDbp_gd0WiuyfVu}a_?mea7sX3mrC)cbdt=RKrxE~h5i!x zllL31tpVBKA5-a_M_R@0aGTi(Pf?TvXn*&x0A-+TM~7vfE$#bg&JFSsF&xR1EjwSa zFq<@UjB9UOW^l;UE;R+4%vThnQ9~=WDx*s+z1dF__K0NWyJcTXGKhIXcgp1eD-wGN z?dX(bReVA{PD_U~(W3y~@FFh~)-SDIiYWzbK{1*Wt6x!}qT{}T{F6mXl^7f62@=dy z#%GJgOe?loOch!xT*gzruu!YtD9B?>EcTC`SgPIsEO%lMIzg%=nb`*6Dl=IxG51b* zg+x=-p1OTfso(_MMxJ*vSCB+bbnd{8lPT`1dwUP=569qWD6ik*HJ_0G44W3o!HfMB z%?n0&kcg^k$4jZEr1<}+;`X<{;)YM)h2XP%H=)n(zE07OA}dn8^j)$tm3rj6v>}Y4 z(26t&!8~UQ^DL7xu#+P)&+Ed%Jhww)D?}vxdA#)R0V~5JT=Xx3y$GQunZ~+x!d#Pv zl=jF3JC)cRzOuQ`wQ|Wng^$A3Sz^i2g)}>;0XERoI7$NLISC{P`o%Dj@Hd=_adBdI z#FfvEzy~y2qDSkCYAGQPo$?UGW#J%(PGpO`F@Mf6 zYLCp_IBiyT#{7IP3b|OAtJih#${7uNo|B)VO za{icMUT~c~-cl}Pyn-3d*!@!Eh~xe1J=S<{N{m;!ULLwY%n64Xa@6`PbiUoGj+139 zg}P&x_S7a}^T)85q-L0rW2f}^K2DwVHe#7y+qERRhHlr&fTiuudl%-szymN?oH(LS z$|Jq*Afu|C4}G?BkHVf=84u;vF3ubknySzCWuf*A=*X>%gCD5FsJ2tV&QggDZ zG^4g=kX;oPA*&9u_}MLYZPv&yG>|Gr&M^5=Ql2Am(0fzxG{N>r4ypq0C#x0ZP}zAM zq*rXw?#FsU`JA`EDQ~w)c_emw;`|P`Cn(9p1s}D^N3GiZr^-jh<*0J`zfm7MgaH=FHN3tGbiJk+0)A|kN$ioe&Kphpe47t6`7v20r< zLvt(`x)#{)sp)0&{?z-4_P5ds-)=JRPPP&{$2OYXl6u5g@buYadXhD0_2R(NmUUBb z{^fe~n~x*0l+7*wr3g`yaF}tgc?>oKU{~`14;nPI`>H9$(0{Fpuo2^XR2IZxv)UJq zWrjYG&p5R~ywkS%=Si+sj9)aJ{Ixuo-#LMeDaFWD~$Sg{c=yiQ4ldm<*%KjNFh@(P>VNQ=E&nvSJ~rBoGF zrM7BV?SU0q{ok3-+5>Y!x!~rC(16v#Sl8Ab(6k281b8@7YxupCSxi>B0jnMA_2Mj7 zXlPl})7jjWyB(URWi9VLp_6JCJ8*-97Hq>ynI>-K|HZ&)2R7 zFxcCb{ih0XWQ!D$F7f@)>U1*Hu0OlUgRTZFGQP6?At(U zrc4&)%f;xZwgOT{M>+i?I#6}lhghQn1{R^Wk*LPsMK^W$BE^#VnP2GZ&qYmAPs84z zLyw#nXm}?y+>H{!*Xyd$y_6?}xHjgW7-C76Bbl4$<&EEgMmTA zG>1w9C^;^@Vtr`X*b#I1UTfG4@x|`ok3G2a?LiG#BU*s@!%ATx7!3UEVxkE;qcfec zODQf2&2<>vP?D0*&s5cX7|ixXmV%gg0JEoo2L2xY)2FiYDHWCap3CGPbvOB*cVuU{ zYWXO^|Iy3D?$T!ca0Pc%4s(U%fl~W?YAkP_ zqJbFn)_Klp!%p4qX{;}5qN=9192d5`8~vBL+~k!E$DK7kZPhVR_obC*FC5_0e$hIA zZ0-JkhX&N%leso{UhV$4x*XL}P#Zh5cK>AykEP6uHMzW2Z?Z-O2ZayhX!qgUjTahh z)Q`*fN*dy6O^J9;ivhuTFdj!gt^b@C`VVwYw7pvL3MJ;hjd9%SnDj+k{yGKx6*m=? zF38;MX`FN_R)<11?;5YZR5p)aF>jC+VEEvP3kEARh@lWcfcQo8@( zcz*7D>?=DvChJCh(W8{(EJdEXZK9A8*UJihk#7|6N~&?og22c*hr=0c29!1VBV$hG zx?^#8+-LW}x}Ond6*?v|;#2NKhKv(6%-D)Yo?iEsQVEAmLQ>(5Hw^bdal#XSe&sbV zN!KHb@f`S&Bl~S|3H}A3z#&fyrphvY^l&-f3aLxGm?*|8w)lA_YkNTo)dsLx)EgZuOWx?_iVDl?0pZ4P2@_39A-gzHGh&`1$g5^`+R%5x%yYG1}QN`fPTr z!fVma*y>4tY2^H|4$2?$JN^p)f>&XmTg9q=Y|f)R3l2`Om||sXK(LRH=b|OLFlx)1 zMBI9oBL*v+pp#1jWMQV;VYQwUUb2Oeg8qXY! zXO6}*M{?p;9fo6$rsrV}FVu|+H8G+9ogiGqcvIM$p-0B#(x-U-_i2U>Zmktn5GL|^ zaDsK*AbH+Et){JI#p*(4gR0{BSMwR`3sInDcXn$$e}<|^c`SwCbmM*{HG*2ecN+T) z|5UF>J`MBblda}AE9BFIG>98G@GTTh#m+n(yNxd+V`iuxS*z6K**-xTfp>V^7+%9T zzfX1X{IOJSw3jvQ(ViSKf)`rDGHC|IU4Z}ZdjZyE0`-w-mCT-DYHU+Z8@ck>XKNw zo84DpMlMBFf?{V*qY81it}n{qiQjn3Hb~`6RfEWk=LcwXY|h2p+K3iwq-1zpF;5uS zBSpGYg+q3+`hB*jkYBC-5}18FKTmypV3~lZOx_#ga`;ZRT1f7`ly1#gfnTh`NBG_( z@Nt@rk2l{zy9=G62{;88iY&eaeDoRH+~~D>-P^)6xOC;oADrOPaOICn{(zN)P>Q&8 zr6fld7V5@G8!NY(bz_r=(7@y`pl|W~3>stR#&Tn0<+v{RSlAsO+ivfUkL!7om@UD_ ze5osik5{P{QuuhFJlTym=*D|X1qWZ^Z|oR>t1;s5TnqL7uBZyY`2lc&`hB)2k6&UZ zfJR!qtY?H^q1<2rf?@onY=#sHRhvbagYhaZ=(u`L+xVIBQ5?^i$xiryl5OFZs>t*# zKkq0A_B7A*MO``W){Iz5yE~$1B01o1a0vqtvR)@vc--9EGeplO6eZ{&Zj^;<11t4D}On`j^otvkW$_ zVxG&0?({P>U&Ti4KIR*ZkcCl2yB2n6Ip#86?GpdYZf}!R<349&(ZEZZgG1wzAUk}(IsYxAwPS$WXl^zgx&%V7VIdSJ zqlCV0lWDTXoB$=bjsNOb1dI*%reJQq;pcY%{mc-$I%-lV^OSQYM^EU|f`7nL9c()@%Xh@GZ z;`#5~O6MYD0@8x=LJa+w{lK}>;}NCmp%PCQ34iWM|CH-0kJhU%Q}QTZ^*kw$u9DVZ zLkW3w;GzU*kVnuz>qO|Ezbq?+?dQD6wJ(xgWaPZSl^i}4#wv>t#b~koD#i=u2YhdwdD-tNB)U|#WskMlO@k1 z4kT~n9?Uu;K9SP^NVBd61KU2Gh-a4`S_ctvEMdNVhUqmnx|?HpUgK@A(c*6Y;1sv~ zm`->2Ev?zaL;ro&aPGb9vVsG`wAyi!?6(DnxsA>4@NVs;Sop1MkE6NcnDAIQiML4} zx{XiV#?Iz{4R=2yT44B}TFDz;P;VLGRfb={M$y^0G_W!!G@|zHrr_|}1Fq0u@Q>IK+T*rD= zN?|jTT^i}JnHju0Y-R;L;m+*fS?I=7qQfuMOI{3jP(lu;a1p#U+>sR=6F!(7%#Zz= zXFkJ;Khju=`5dqM99w)5XY5<@e70!U@;EwtfX}gm9L}QZ*SaH4*5PZQW^8KB3+5oc zi#-u%$1I=rLf4w~Xq^z-9QlIH){Kk=W2JCghJd%@T%Y#5le)EJ%ao^hhPzRUb0MJ& zq_|juL`l#idG38)ZuVq^*j__xh&Y+zY}r7YR%~SM+ZoB^K2ykt(yW3M%fUZ=N;Lao zF?Y=r!&kN7V{*TK?PmBk=C%CK@^fgK)7j1HLG$ixPUyhI#Yh;hb|%RwQGPw;8{Gv` zSiWh5LsCyKpSkyDI)ek-+%j!ep=f?%e;cFOfcE@!MDldj67E8uW4pHmOKYCHc5FxJ zRFTLT z++%ZcyxIMPkLT=a;9OsyKRD+W&4S{P0kYm z`9u-vnEem?K-~u`d5gpr0jK3WUB%Vn5V8QDh$9H_{FW24Sj(q!gx2u z#KU1`K=U@ExrnvV#c8mYWV2QGMNql%vyPk?U?XElbjpx;{_S5wlCJF-%}}s147p<% zlmZ)(N~+eX_4jf=K1DoEzEQw8}JQ3$-R4nj{kc~3^%VRj7X>iFpRc;!-Uxx}FK#akWk z8(%bhs5QLDZG3~5%Wu_l889x>XE*z!7d3E0Fsu)g>t!)Vmb)uP4+x#5N3UXN8};zr z^lpG|AgEsw*9<>~d<;aFb578Qiqi+6uW-r=6YVRz(hkYwj<1Wq=&g8n$&dX)MV9d+ zG7_*Yzt6@Q3ue467iWv5s5VjQV4A8}(kioqC+=;Y7aCr_ zVNu>njE3u~?R^k_q4}Dd1r}XtPOG!v(}U(;8^eoe&ecKdeeX2?!}atyntvcSLG!El zDnawoRG#uDk%dMnt9+M&IMg-@zYQi46yux@7YC(maq_|QNU4kt_mi3C)u+=uS}wTG zYc4sk=6qfxhL})J6895%#J+M<5am82dKIBX+TC(~j0#YSK!04O((V+u5g5-9ib zXwIYTR`70HtG66M=@o~9r)jGO+S7Hs@iIpx&6#jZcZ7q#Mq5lw z#!SUH_NvW!C(vJ>-N&;d!C$N2CSPJe7o>PYCctsn2>=61+VsfMl|W=Q%Q~&tyOO99 zUUa)7+f#*Q4VugcDNFZfg!q;9M%s zEfMd^R_|(=10Drdnm=R5oOh?N6T!RPt=>YJ18HfunxomxY7W*p)f_aLb(Dt&$Gwad zuxs_FiMvqu>Rnd#&xs*lRiB(G2;mJxV`g{d zO6|D~`fQHb4RM0CUXCa^hQH)PiSQuPE8Tx7^)%jIbb+?YUleHc7nSjUsj-D9E!y{* zz0r#hhCfl8XM3Bof}=$q{*fIbwBvRU!H>#4$dXF#R1GKtjh-T02kBOa!~NuW9xA&~ zxhi@MSM~(bqy9XUq3-|?|~ zo9V9I&64wFCOgAi@Qj29iq1fJk^ch)J`S__vw@W|UDN{J)xXg8WWPWBLD${lr~mn@y@ViakuyG|!23I~Vz$>+r>IJ_& zMHldd`XoXLQb~D^5;II-ju~Nxk3FT+IkdP!jeh3~?5FzyXv20+aX_{<2uoUwEdoc) z+b&8%!v<~I$I*$IbhHcjR+<;H!LWiEuTqrddj}rVlp2l)hf#XOc$6-#2BbnoDgDq} zn;1Z&)m<;0vIg>l#6TXEi@AM{#Kh4=MzYczDkHfMY}of2$Z@5y(aS=GOeBkPD_WL1 zjcxScN~4{f(>!@`%w7=184|~htd*Id=tbJ9rg)2^rAOa^rrr08i}||q!-;M@cGCD^W#9bptiM?NP*+T8 z?3=yvL&jtC6vY`ANxy}m_s$QKEJYZ5AN(*el16(ZE6qcA+{AF@xA>u7_~Gl__#rf6 z@x#{@Kb-jA^Fu#9RQyn8Oz}eo*cU%c^!cLi&{NqOwfUiiu<*mRDSmjc3qNf6Px+zn zf_XyJut9&(5;&fx@s9c)+|b+LiOgseJge#lGnBpTuFSB?oY&L~GkgY1-eH*Gk@(+v zUHPBnXakL^mHzjZp3?tZVy}h;1`LEhdk0?DFdjJhMdJ7mW8CR$?kM&&vd)&w2;TbF zEjGrvSjEQn!o|)It?qAo6c^hjXRhI5Q~*EWD;cXWuRo&9U0HO2*O-(G-*S4QcX#1i z??6SC9v6piy~9=BhMuH_M;sZ#N+@nC8~j}va$H{UAnop1dCH&igLcRg+inv~7A zzH}|Fvo9@9Xh3l-6o)>vxFpvS(pFqc=tXfY(d+i~s>NA!v)O#)e-d2lSa^19GT`i@ z#lf|j)45i8-(2f1jIg4`5pe)jD*W0JxK>IN65gb^mJ9&O7p1cs;mbe{%e6L}m)~cN zWTmOYK4Y~BuEnyPjm&Obi?xEqwKnwRT4jA{adg7wxMfK#PP(l6F08jJ*OD3gfpF!X zT3iD9&!a=fUhRczHK({17Jan1?TSn!O~?_nxb0oFI3dxwLcwq>qLWE2!m)%lE^PGn z&b5~In`1;9E_=lvbFopW!`MHxM!`%94=^YPD5sTWS|= zYpZoZ+%qgm0D&MXsI7t!d)~8( z6MLM0j2IROUK77Dam|)bsJb_pp046h!Efwm;BXfF#I1SXAPYe+MC3(cUkLr*lFuWN zvORTNnbo6gZ|=$R%RTbTqSB3#$VH=PIUKXo%JyE8hrj3Tp+ydA(_Hk&T<*SwFJ1W( zVKB3bLyLALMIzB<-Ga?I|kf(sf`m(iI+PjWI9JT@b>lKp6pa+*QIf$u|$ zw&-(|wf@{DUue+T_&{qFIzQl`fOw8srv_}#LAaUB`!_yTii^B6fuEyY*BVVHMJeDspcpgy}7I>EJIVgaZ z^idEKg}3;N)o6Epq9S6^c*yakTThAjCej3D6E7=uAel$3)Pdl8h8>RxQjUx1Z7fwP zi2u~7(Ji}~*g0{EgSU%MggZ^JpCi48f^tN)g8WMWoR@0c zyV}ZNpjvQ!JSJt25vcYXTaCaP6p_M*_K6h#hg#IQ`#1!@)Pf=s(*9E;}Apl;YB%|%Uz z9>VE0wFdUBIfcI`mGtNDRVBx`nZfqmY6d+yxTh0o zF(eq++Bia{5*SQV`0|HlCrM0zA2X3Jx7Lrlp|hENX_ubMD6O%Rm}vf)19uDT8>rJ4 zxipk=j)#VnavB9+J(JvGDXg7nd~p^u3Y!HT_I9_>0X6y0^3gql5_jmLe-xA;ilWsP zBK%O|K@|_wbtCoB>=+Ns$Il#URwOFqDnyzGKKR5e)pMyILUqP^chd2*|VY0ss&alD@#Oe)_yY392;)O}`mcWE^`Ja?qA2UW7% za?9kPyG1s%9G{q-O5hF!t4bz`x9@b(5V=HOj@gh%41ex!9BL975AUGN3)$aW(0k#; z86i=#b7Y#F{|QSA9LKPeNXg=C(hI%`JS~3g@P3*{8{i9)bQ@NZFF<^@8+Vqk)ZHqb zwgAfh7lRjSs?^yh$Kd4xUZK9f8=2$)P!g;Upa7S+x54W6_?>k=qd{!MIX;2dNZ*Tn z|2bmY?S1|^!gA1#`WKRU#z$_crmX~`CF{9sBnOjo4SL5e`c2_UehZdE{YDtaABb|# zq?zkz(1xa0pxIrZJ(Qmlz><6uFJ+fP69Ud6>G?as^OR#d!m~)6>7DN&u8kO;X*nnpV`0wjhi_ri zdv2qq;Vvv>Qnd!1XI#CS^~*?-b3S-rxx>S%=q4q4TrG<9*u8>)bDG;A^e-2D!2Pi%ZHIx_ffB`8Iv* zp1Lx1BbdxMUqwE5gq!3={0r3=wJ3@6y8X9&yjBDYdq^~ z`WRVPMRj>M%NOUG!XvlpmE-|lDTA8y=+jHL_H=9aui_AUgTG+g%&wzD*?%f@euRJ9 zv61rHBV`r)3=Q?Ug#M?QkFOQ?yTCMh)0P&_cOymIeHDWO_6w(?vG#{<&6IWMp@$Y^ z_=8;=ijYr~53O(Te8)`G{$0@jkvtR+6%^6PieGyc{bHh5mXl~s(hQ>sT)>Xl6>G{3 zzVwykMw^vZlXPx_lZI-N@*2LSH_Yg1jQ*qau>ucwZi~Kb>yF)5->~qjTYOC)F${5L z5EGmBMELn`+~UvO3PyPfzR*U8nMYI5djtIZ26%K6@0)qY-0hW(sjInqIZc+gbXz!Q zKEopYPpIc`QjaZ5qn%uyd7z+AnO3fJuZ6X0YUSIK+}hGrIIn8n4dEUA)9WhMXqB9& z91XqM1WCE}W$GDxAlPZEl_*=Q+{4%1-r6LsQv3}iLy^iA)UZu%lj$)^ec3|~s(;7$ z%B#CEvrx*t&T8sFyFrY-Ntf7Wpfd*=Z3oE)*E+~R-^LPm`F~Sq z_YB-Dg9)TEkdkX@KB7-#`xu_qlEO~7dcmnt{wLQ;ha?SN&_@xU+~G-z=#kajweSP= zYyRK`{c9ZOD!lPc#0LWbWjdyk;fh{4^An0j8b1)lS1@1sIkrKGf@WPC9z}u%WG6>x z1bI5W5x29JIqwdql#I5?8@jAE>ci&IWglQtc1XIW;J7AQ%ViZPYJ|fV|~vQvAkx zV8v;p-czv4qm7mW`wf!3l$|Qe{C2$%+E0J(4irx!nIe6d||vKK-uLsPoLHK4qMK@#KKpYL1l(oe9oe3FWDM2k?KyL7|&SWSn{+_XGZR@sJ5x}aYg7ga!>xENXEkU6K3 zgFhpXRvB5CQqVI0#7^D77zhQys7N@mR?dOv`klZ8ZJsFpX1>=F;o%E&n~avY^LHmh z5%GAKy`R%w`Oc&OnQ6vkxJ%a;b=tkBYLKc*q1u#qp2+gb&*q+pDYauA42DYbv|$-d z|7kqFvl~=VMq?E|JXwMx<#kCOW0m%Dourf8{8&9hE*~mG%ItseI2(;`sOS-dKT$Ula~I#wOHfEI%#e4VsVnS@Y!xJC9Nt+wI>IG zEI9>a$tmDkIRy}MrUdrya0HkkdZI)}0M0)QT{$9g`cT$E`z-L+eG1YDu6Ted{kQ0h ziY0GDh*B)ZCi8}mSV<#}c*^F7NH_CH(r9~2sd3(pr*MYs5Y?R1 ztL2bh&gOvOc|omrE9(YI9Ss+mJ1DwK;X!rsMvT(O>!=uexbXSM#VDnY?6mc4JN~*1 z*=4q}lhij3FK^c75jw=jf>4i2)!L>VL|+8QZ4Fr0PnO-@xjMcTb$@!Tw^`jE;;_#6 zdU2G^(53B>8492{QrZBuPZa{#2?AJe25|jEqLVobTfQ`i;i(^8DOcM0GmSYyL9|{@ zt;(M%Lwspy!1iZ4n(z&PIp<>mCjLz4alcJZR{h|#Dm#}`u`e`?;753Yv5}H$ndXo@ z9wq)FvT!ygRy=;Jm1k_1sJ;l!)m<8#>Tt3 zN8EVT{w-W1PDzE1%F;=oh*e(nVe{h0l1c)ik{THCg_|$&m2Yh>G)f%J60AN!%SbLX z<~epz<4ooD6H4aLJ&EE!i}B#IlSZRb^e~dhiqav+P?=j!h6VqTgwT{yeZw5Zpd=iN zYbBid5N2wq0njqzRV6dX$`-}NxS%qa!b0aMoyt`!bZ5kEcp_|5ou}h29O%mthZK7MxCvk=^{Arfg zsP-6d`@)~$FJRYrAw*b2sF=P~wz=U(pQ=kkgQ}2%-T2^ZPrTta*6_BL6N}&Y5B~32 zlI#POQ$aX*F~7OBdjUo>a;g7Fem1iTwI}L%V4WOu0FdALj*qi72V6b;L6)9gFAl%7 zLI9_7Vc13(>2jZvR-ptUhwnwaGP8Vg%gJl-NA{5%F1;GvnjYp}L{EN|+S}>7bZPxd zf5DeLgEO$-xJ!Y`cVxBSu%_q>@5~|w$nV6GX#H5|{MOG7DD(Cg+@%1hEyCLw$5UYC zGj___yHs9e_6*CnwDL377De^doeBs%C&2Rr2UQ98XD|1KKWDp8-2Ns~{tjSqkn>=SOO!Df-9+Am~PRnnFem}}H$MBR|Aj=;deVR_v4jF~~Bd)hw^^TO%4-j76-LsRDtGm}%-Ix7y z^^-fTy!i<2&R_8bL8r&v`=I^1%6832CW+4K$hOioN%pPq@Cs>72a!wV9dQ3_1;5)r zB)M@eY}(Rqb?LUW?rZo{`a#oxHC1=1emZ+7xp8%e9-M7&#z}p>ga&gmikCMYx~vlj zp2uku2tMWA9|XT-it!87f-!5d2u@PF7M=Z3)d&|%I0+lSL(u^cepTJFW-H9`D1) zQvQv*kY&B4NS(xfuKf4=o_5#1h)a8=ro4JUlLrN~-@5Lo2Jdb)wE?mt)9EQ7eM7Qi+-NWoO4Y$$Szc`=A zUAme}+ENL6SGTQ@+r)If?2IEg7aQv~HD@Q4f19jvx))1>RtK@wC=9V1G z-?IW2SdIL?M~1sL zpOU_0Hty2V-Abj!r@4=qSFL;xKanB%22L?nkgYr{&kRkqn}19T^vwRc`wHp8T{9wG z_S%M->iJZyd_K3m$jea}8jW@j*`EpklJOaVRs46cOEo~~j%ALEX)g+gE?W6tcu3&z zJKh<T^uLvxM2wr^InC4oV+~{;IWpzQMy$qIOlUl_Wd{7ug zodbVvEfYvD!TP=0hpEG;kIzqlLO??y5b`DIxtjuCU=c{MmUWk_dyOx&fIz4fxwx;$4d zbGV7#O@?Q=`#QNS9yE3%sn+F@ayhs4G6x2^dyrg~ph9$8;QUkQx#aTka+&j3^fD(@ zx!grAf6{uHq>FNS=aXFC(0aMMb$N?i7L}RZcDi*rESLY#dKne7H1apO{C};NRk)IF z{*ud$v+Nd*ux?!_*RHeYM}<&%{3f|JYFTL>-3h`zhWB^{WCHT1u@zbjDN@~-OH)Nm zln_jLY>+%gWQ5~fuJyLAWyv*|J+`cev!{bjD9W;gm~G3Rr~yRR732CUq>bYxg~w5w z_bzVV__)PZ+dix9ziqb0oo7zA+kVVy`$>N=R7JxzmE5SBe^L*r zE9Mzi5073fTcP){4m~_(^|1XOHF`bPLyFx)wber%Js@$=!xFm(lqTwmStsh7(0N|f zL!i1t57kx=l~xa*ycM0pBRnVY*<$sul?}VWC!?$sRcTEnsG+994&xne%Toj|F->LHNTp@%H1hij}Js^5%4X`bDK zRn0_B_^Cg$ptMDGwAJp2A3}1)94s9L18E(4fU$&-5sB{#saag5`min?W%nXxUgqIq z9;Rx3m)*RJhvka-j;M}^|5Hb6Z+?ef@~wIN+UleEjp#g{;W=6DBCCh-^w6w&0MFDi zkEaK@V$QL8_@+Q%Y*B|Eilm2P^E|7EU)enro0nTX9Ee#QV>SOP)%^aL#Tz7qg;|^z z>);Er^D%|E4gkdPQ<N*bMR{a_H41$&QV@CI^xo zdw3trdt_#^W7M2v$CTO0jthDHiq}eBJLR3f4uakK>xvzFsvST5i|H12BUhnb! zLarap>l;2_$NQ%|=jhijsh{=F(a(|G&zTba*RNk{ztk@Mx~BE(*R6ZH+|t9oQQms0 z&#C?T9ie6P>V0G%`=`D~_3L-Ee9r9GFQwlx*10kISDhc*#al(|t@z~_i89=s<`3bV zQQn-Tg&qefbfd*r@O8;dDqj&hhqN-}jzFHzxQ6o6BM_lf{@g*eEfG8(GA;Q^GW|Uz z8;bb-?D7??Yzz;XA2>@%27dtM^1Eu45Av=Wk^M)tv8%>vgC2CFiJKb@xuMX&D^K09 zA$kM*m5>|zm9X5l`_=r}GT49nLuX}T!zA3g^x`{8vh>iXBpMCRBG6RnJqhj_%5dd4XigNBj01PZ>JB72NBg7-8U2EsZ zS_`~jO$$?_npX3w_OK)UDm@F}v z5*S7}W^IjkgY0r#Dy%jCg5?%cI9VNny*q2XH?TL0IeFYpe(!4D7?j`puB{ge;CmJK zQ}pDlcXEJU@RrS1^Y7C{Gh#T~*mE`BSEYF_6+UNx{u@Ed0i1aIDon#_4oAuj+JOdijSjh>Z zC^?2yuT+#Fb$iEX(&IlmRiRM5wd(Bcsh1*SchG=ehEE;8t(Wtx(tu|PQe}Y^^{l#S z_PtXf)vWajsZ4Vq_b&!RD?2p*8&`tnzr^8<=BIVm{L-JR=8ujx{|5I<^QU%bzQmoN z`CnSiXLQ#5l~(i1*4d*EaKDYS!aps1^-SH^?U8PxvzbFX#-3&UStxhej{Yr9hdaEt zFQIMTsqg&j=8thrs4p~nY;=deJ<;r2o!j0=`Kcdc)e85+?-F?ckj znP@llO*`D7@kwYTmaU^_w>ErB(+A-lPUTQhutHn3fTyRfIsK}8mb)Q}fz?Jh+>jjJ zdO~{mqMWPlNuHf;+ta7k$vq@uBqL0_duDw&b-6Td)NM4w+i(KGZ4Mh^*&bXtj&W-v zs>wbi>PqW^He!vQ4!Owm70o<<8bb(;I9ZLp!A(}BjSXnLk8tk}le@@=*4DFHT=7Ctb2= z7imlaaVTG`UsBeU7fgtlJn>yXxjb!otAg2A9+5KbpXjf$zsSFZa?_i7!A|YYH3AH~ zOsy12oOy&EyonItjFfCzZglII9Lno76t%N8K>7Spa(_V>1MnV_=(5;r&B*=xV0fh8 zz6q8pqwr#WLHSZ=p7#}6vZ|R`GJqL_eCqz1XbrvNtehXSnsrnic6GE_6mM!i9u=W* zyPH0i)l(|vQt1^l_SmhWKDCtimZR!TuBel$lsq>FqH|k}ez5#g^6s~B!vVC__y-^F zE~^%g8+;$YTFUOM)EkaKb)mhm}jH8@91Y?V}rMNej zWKY?>Nj5ZB>4!-YdX?`Foid92F7BzOc*TkRCC?K&v$trAII`eG9p2X`ciq%I;tNNX zmgg^FN`*%DO1H5JatufCjBEB8U*~SnH+^nw*tAtVI#-I}LvPw9noDdSm9K-y7>KrL z_j$6fl5KQqy*SN!L$|pI3_Zmgnw#ZTzO!3A1@CF)uh0ddarmPM_y{8yU-}j4(9Eog z6wL&wY< z;K^-rQ>{r1MB38zo?H-rISwm)VeC7ylP+8lNUwa|t=$*)R@Rl^SXBoO!3PA~>0$An z#mokH#NLuGT*f8zHbo3&#lk#_q%tpCI}4!Qc^wPjMIT7c56nwIYklqplvlzzjSaaQ zNA-x%Ya z5_>!z1{#i3J6EwVS1AZL97p$-emY+MF8{PhXs(V#7Q!ZCzM1;TU$7v9l>4Pt!3N|A zJtp2-68BIk!td1o$o8WSd}WQY>4d-SYiw$IXX=-NNa2UqkYm5m)rKRyxG+?dRHzNF*PA|ISAvRAU_cntFmXx@SE`h)#FG~6(OTV7rdAY2qK^-TRt zXlge?2OEJ>SXWgjGm{&la_9cX*ku0SN?6U*50$5BsK@z&N7T`MSCN@m03Tq)00e&G zV+Gye{~qZL-Qn^$Kh}*#ckXNYrq4(s;luf=X)Bz|FZro3xXZU?8=A&^_~+lO#QKSo z>Lxr2!E9gC2gr(;`ntNO)=0d%KX`#B7gxDAwepwfia>m=l3)`Qv0qpz=iM*JZNA8Q zLH4YTpN$NqEcN8R=?P9FLFV!S?y0XY<|u_D)f)2OLUTB8<5`#D##+~(|3pe6vSNSgFxWZexrKBrqh2dyj zMH#q+wnQ&z`vcj<5+FK>98kmbvBT4q-&hM5@@r45_B+?Oaq!K316X1m7SaQ1+uTIl zGvrOCKfR8!6DQfL(;eM9WmF@m#-nKT!8!|#<_Vd^{t1i4p9sK29w|pl4!S{t$O7)U2WNZah}bTY)Dn$$J^% z?-WZuT~gzHOwLR8UUNZ#aliAmlvEYtiA$6Jnb|6Oj&uFrFS44TAe z;1(~*Rz$GEpDKFv*NxP@U)s`@_h<}>2sB?r>Xc=&QH(J7+aQnMwvPmDcU3+7%VY=6 zWbO$2&!riD5g^BcGG8`o~9!G7-DBf2M*T;K^_U(0-& za6GN8_3i|@xo-oS17rq(*~H>_cVgPbw|U*`$_`VBb zBe|l(GB(wEomuMBdf9%nh>ojl5|@F(^p)nD0~t_`8qjrVg$A38Js?lEjL09HA3q|< z(HaqohxQ{Xut&5N5~d!<+&;dh_m`bTzqHX*XuQ_&C=avad9u63#!3~FKojG&$7&B$ z#wQOo&Jie)v>`Sq1tsct$B&7zcM7FPH(F4_+owI0_S5tAxMYKn-dc{JO~NG%q{D^c zR1WmaVfmtB0PL!6k)%=)-!w_;2;RW}%|!sa;Z1H(YpgBJ5_b#>tmY?Asp<(Xcj>}3 zj1A(jE`l~L;+9XWD)+)*VZ61Ts_VDVyLO*p$}1jMO)tA%@4}WH*>R}`dY%-jNb0j+tYfL3?_G1tzKIV z!;G}*r>b#x#~+DIl<8PWy2G~W0#fpjcY`^dE*qX!w^8F~H@t$e zL*)J9{A=6wZ*@N3>KqoFE}j&|riPhJu@g@l+;4F;Ro?yL+MC!rc_dtj{dE}3iUDlm ze3a$jkvR%Oc+BkcrgY$K5_=fa=hRiB8-+*9(PGagW*nz!tfcbxo>uEUM;K$M;vw zkNCb=eB6+*LxtPTl<8<(NYkkZTl!Q~;4u&}kK%^Y%s&oRDj9B_I?LgBGD*>fF`(`6 zIvdeEHKJE#xb}!%HLq(kqTidBw;R!nX=+3&3?V0OL>vmN5%r3VXmdb~Xpo|Wb`azI zKexLAnSNlHwH;yg^mv&4-h7hp@i42uRX`$~Fm8BI4^(4~a2h;!u>$XT)(A-w0v>z^ zpqL3BuulnBro$ujbLkgf##h1k{#=L z72luiNCrFaJ%m@3HssxsSFR9QWi^CRO;_6K&tyNzI|MR(N0dPNBbY-YBDvT^a!O2W zSvJKITXy{>Dz;eSidZSG>IIK-aBJ(Ops+0{kIjibddppJq1~@u}u{!^_ zqyU;I?@spN?Xm-J7Zgxws1#A*5(!Hs!4x}b;S=UhFO;c!pJ*6C=WKJxGW|N`dcyKt zc2tFrP4y_AkzFhgtQz>Pr1+fh1aXIvzJ2glCHSPeM62;WlcZgF-lnqENx&k^k?8&~YM9lMh`Orq34rk&weDYKusrJJJ2a!VAGNm_Zz=l{ zaA+6~C)9Fk%u~p8z*Aj?o#Io80KmkK>X86ncRG3JOcC@B6y7 zgMykmQx<4gtil3`<wdQYj3M7Dn2q^*xk)zuI%+SJOJ>*I%9YS5l-5fJXKEKwy;_qUiI-$YZ^W~XB+33*^^W$Lq#}|Nu z5JMckKny!eum1vK_%e%MYrMNr03rm0WB$cBhT7r>j?8Oq*#o3|uEV--mji}alp21j z2%u}DkA<`?^R?13Mi#bi?{+O?Xd1J|{dOovM3A92cx(8WK|Kp3~%tcYBR@rySF`#HN?` zo}g~(KaNwsVUc^3t=9Xf3`0%g>s#dazT=)1zQCkk&Tpe<0a0hSy~eTCb}gg0J#=g4glfBm7d9UfCyN@fuOFVfJkzGW7iW#C;L3QBk&Q zNiw;I!tTgced2|L#8qML1Lcv9Bx@k)_lln{`05o(f^(M1vvXi>d#lYYks8F{Nw+$o1bbo|6-T+qc2O;e2&%p zpZj()`cEgT(HBOW?`}7LRfpzhBx-*3TM9VCJ8M2lk4)73 zjaKv9C5K@mEI!+6{+?*_e!KbKb!h$>PRJ(EPYW z%|FiV!v8vJe!A8C#>Z{o+-*0%m3&KWfwMa+!RQOE=GPqA32=H@&CiH7e~#UJS%>C- zov8WG-%z8!sk7#vnxuepWVHEu?pOS;L-YBGn!m?tzDH-xkFlD6=I=IeZnK+T;cO3_ zx6!C|SlT?R`G1g4BO&H|JyDH*T(tS#+^_iGAK-ty9^~~Xub1uDH;=&o{s{l$)%#EI zKVJ2`&Vh~H&1(zyMfqPwynWGD@-IP5a~p0{q4<$aM~z4nVd4A410s(T4ajmMJ?|wQ z#j6DI`pG+kyU@MYj*F@uq+fvQK;#&Bu()wrqx&v^zHXcd&ca}4RNRkDrKdH^K7sLxOKF|g~? zUgQ$Cg-@dTOtj$0isQDUDDR56SIdARn7=*2`5I<|Lh6y#AnJ2k4%A8nF`8o(k5_WO zK}o@hfiT1`N=Wh<7FPnYmKy<11r*BLU2=!VKpP#ptf_qOnAyi^OYb{+%45k6>Y4w# zn0E>_i)0)cpUf$PByfysCSDrjQyMRnuY6ZuyWnOoUwc_!yEh)E7pyNiI!?byk>C^+ zaJN42HGYX&Wv&VSV(x&x!d1iak>&-sn(-zUspw7H>iugWx;?yIQoH<%A|ZuJ}U zU$s|)%omuvx$|h;yKGg`YpCS_Ei>NCBmLERPpR--DE`EnX97}j87Nz+uprL6fAZq_ zj}mXiizy}XzvDl3G2t&A?`?Y}hL4;_?0DR)XkLvxblDg&FFO88QD9c%w^Gay9AUFR znFToe)?t3)deatf1@%8Y)l&Y{V{9>+-Q{b^UGwq^3{pPh@eRd7KhB*JPU)Zc4T&(0 zpyrw%`}9mv3j4I7^*%BakR1Z`P{}nkR??+L5-Sivy1qOzX~wCs{9;Ypt@L8K&7=;i zSh(b`PKUAAPB8XPd8GTyp03hb)CMQGs^&%-$BO3j1i5pB+$o918eP)-iN4U~RH4~V zMhAj*m9<)wrHZjj>}x;Y4bm*fmHz9_lG!|zj6Fos411e!ryRI{{Ss3tTUjbYmgE!BVAt03Y)N*8 zs#n7@(nk?DqOjY7OaH&efx%6VmR~hKejq;xue#GaW8_K#5mvwK=LAP1upgTcw(D{Q$ zN*X%lphOl$SxsFQ@|qPgtl*K+hgEhuD}*-wCd!;C{4oFZq+o_TEa<_F5^*aHs_6!T#9xdm~k|A1#>#%?%o%#)A z2&(r%fuw{Ju1pG-`vyy80aEK3JA4H%ZIKPq>u0gKCxBlYHs>SFR)sJ>f2c=yl{a!kYu-ppp7R|jUu2B;VVMO|R?bKi_vDO> zaz9BM8PENSERGd>lE=s;NkyaJkrqRP$=UbBnBPZ_Tg(sk(mC^kg{XLY7fdg0CuJ@H zRmrKiC{zn)nxec6n61OCX_nTI#If$KU^K3ut?+`zp`OQlL7;UgwL5RV+OQbX#U3Hs zTAmG$NO)-JkTlVS-?8%9{lkX)u3vt1+TCWi#G?8z}N3GwPIGB+MCWFo`QYuzAY_>&=9Yo-c8RvDY} zpj5Z`6kd+c?$TZ4nv(ph39BPw2)}Coxw%4p{c=t+hNZbAht;w?`=5FWP@g2}T(iWV z0HMZ8zZBk?!qFh3DycFO*rC!cX)0d|_IW=^m`*ypjH}LCU?Az10PsmF?dq<;<7$&s z8J6aWi)ab4_vCwxZk6s<;Ys#O_#Le7W|glynyWjqE+#`!F=`I2pH<;UuF#fF&rkx? zp3sP<^5!wxoiBn-#Odd(Tq<$;l~3}fJJ*RH)a4l+@_xCN-r9Is!e4~jxt7il>s;dn z3GZ+%EotnV@E4wCP%8PDy=r(p;Hs(CwwvDo#n-A@d!&PrZmOtVg@;4KV1)d#h&ss} z=`l9ApPVDPiC@}>Ddp0bX#Ypx)#XO;!)_AAreDe=)z8|h=oo{}0zKuhC>c~%#&6bjO{b{dotcK&rnG2MV z6Ukr2%UDA2GPytSo)FX&2r52>*(ccFg}|m;NzP!X?rU(eM}Pa_)95XDg?a7cHR>5~ zD=8%ZZTcaK0e>%Wtr^QWU^o)O~5Hkf~1&{M9**mxRj3OxDi@B$OpuU2l z%voqX81-6-;)s!um1OVz#uN8a);+uwtegVxLa>p^{@au&)F}->TG^mxdcihM-fL4#YeZPKo=5Cm~W5=OHl`ix1gK5dS{BebbD8fs$VI0 zgvv%lzk$2)N9f^tPTUbZm|ck`(w0Wtr4cj$o)%ZtyzKMD_MaOjUzKFUcmz_`GP=lV z#HF3HdnRl#b&yyALp>kdk*w_QsX5m7O1_gh@_cbWsB=}o>hG`dmDj*Kui&U9F%|i^ zFZ7dWEOF%=>0Xva<0MNGWdu9+Na?*;d^D7lG2QZ1;P5PS!c{?K8d)zHja-lvg}6*- zKMeI}0}gHJ@NU@}!u6Y&_=cneymVjU!kfJo(Rm2o90`|-C5m(9a1Ds4=^I^uP)ejO z*agkcVzBes7UAT9QnW-%P=!_DMe)Gbs#B`H4RKnL1JYq%yTg?HD({kI+<|bUk|hZI zaTh3+(s_Bt6)EP9zlrQbQ_ReT;*TI~KaoesKrSWrxMVMO+4IK#7v~{+%3IGvBO4oy zQCKBKe$OWJ?*pQcqf|wP!!d(AQ_D}&4IjZigw|9e@izC@?j-r3!>st1AP+vcAweTi z6x>QUva-wzo?)5iO$Gm2E_ud+6r8uxOBbX$N>X3#!klCc%S5VM7!cQ!(ferip+91< z?oD}#BZefGbXQZyj_qkK^=F@-Vk2NH05+R{!&;4Km4`wR7;xYP1iXz090Yb$X30=w z(IqT^lQEkc336rSi)=6)^Xmw1V9`afz@Sx}EV5=BP-u&~%e4gH`1u!*8Szgj@}hh% z`rgK$yEA(1&!(z$M4i*4=h=kxD9$l^wzs3j$FMMVLW>W+Y||n@j?p5y)-7UuRBVUj zxY}+L<8Q3*%~ZeqVTp0pQ4r%9iY{zo)EUbEnizAwJpeJH^Ct|XGh%E-!YfjQ&t3cI z;S%DN0}h)IF~gCw;vek^@#Dw76Cu{1GdFuJNlb{X^WH4eK4Zyuq`wD$nTY<*OhA7} zc1C}D75&Zm_%P}3_u&K5-^zb=N`G@dYDa%LEJ`QzcghRWAf+tOiegqzR)ZuED@-`dmP^?&|O^f!Dr^!JO0 z4~qW&_V9P4zhf5vkLa&?TSxj+`j2+@&TY0T9EFgm(3Ay4YP^$v69LkfR=!8m?tCc) zewzLr2h?d58+Hat*hyi{wM5%x;r&_ZLoFZ@xKKm7x* zF*U_)bdvzK^l;!BEQ+qOPv9SplKlAmX$KJJtc2j<&(nxTGj(sz~BaCDa>{4d}q;R&H}(-idHxE8U@Nb?1c zP=+Sk77`c5LFFPbMtF?(+~LMdcPQmZqAN8wlMft4c#s4$<<|KNngzIiUX3_{*Wrp? zuy;Wk@x^Prp&kWskd?W0WnNlm;2kyy@JRlGj;#ASKcA-*ol8!B+^A_t58 z0M?_u+)Pb`;QVQrzAvrpGyuBLaSx7JfEJ#kNu?v^mV861)qt0_#v*yuUf9iXarKb z{8z4c->>m*7guC1!ASnhF^b<#+NFMbwZ=Ow36m{kqFfDjnp55Kd*3xx2q~e$o#vg~ z|JrRU@G;@FqbAugbEWlqeO0nUUdO;I_xI|-@6q!~Tm5v~dU2KwTdD0aQtv4=@=(5# zSbphcY2|xkEV}0&<_?Sg#c}I}MTZrO7NUkpV;8B`9}tsMnq?NLD=c_sqnJgsd&9<; zxnFpV8{yL3Z7%&2Fy=yPqzIQj0DYh%5@oklk0QL2+7f{`h;AxWhg-n(q&f6Hn?H}_ z1ZZrAKYK#Q4Pf*`JLS({f$(Qf!RM$vjCHx|6?>k)1@^oZ_H1%U+!YKo8{U_K-v7c# zwshpv8_XT2gAVVoCqRwx>6P&5OXK+T^NLUJO@SDgt>V+oWoXSE$-oR6!zZ9M^k5WBly)D;0-42fT?(OY8w(xEmZhO;?HDNDuIQf86#hIDi|GEqXmOo7Pr=)Q zZ<}&Af^R3p0g^pyS7&H;auy()38_TUrcT*!07uw#O1S7Zp!(&qk!8CFUz)P;1mUX3 zD0b|&_%S;j4B0|HqN6*)&ntvEjGZeS5XyAf06Zx9=|I37q%-_~%Kl};sSR&#gKXag zN-rNxH-&UVxGlIPnlJO1k6_V*xdw~AUp(A{^P_n@VZ##$vpU%!uSKsVJC0pry+*H1 zcF60cb^Cuk#_z(TrOIwr%%^BEB%&LsJH_E%e2OZvOVyRfv$yh&e({!b#WHQVl!+%{ zy=caKc`8^a7;40?Z1AZ<%`qN9^KuIH;$@dcmy>Jt-F-%*%&MK+X^d(Aj8S` zVQfWXM_x?Pd~-#YuS%O&WKJy>zJ=Qho-%`9vK~^=@>DLa^*C8UTJShGv@&b+iZca{ zV8dP6r2tM;mrwticU%69^Koipj~pwtgH?@&lI&uV_yF7~0?|lWu%T z-f}(n8LQ=j`>c0u_!rQl-i=Hay74(>bdpG1qUV02SAJG94K05Pdex+WOTHGz!?zRu z-ob!S{#jCBQ{yS}e3m~n2pYBQcWOh#b#$#cAVBo1-1ez%Jg$l;l+NdnS|Oo=Aahbx z6qyq0w_|3s)BMKF3~?b1&dktWd0fq|d|%d#e&zcXmh>&(cT!14`M#@4(v5XyeVI7N z1iH9N_nj0Nz~9qK&Lmvevb(?_#1b08#9=ie`g1XLxT?-1<2HuZ^92P;8J!q%Wbv37 zf8;6+vwoZO&|PXGYGuihCVN?CClJHiFtiW{tzj9M(@$$85m~}{jzNzDgWGFpWTw{^32%!fU|b?!Fy-Q`fwZT zLuDcq>%QP|4AZJH<5JaqEqfZS1o#8tW~fYR;x-UU{WoT>fvW}oKu9o=bcne!=7O>O z*&p)V5S!5;L7wrQOsErtJ6)d17JG_Iux|3=R(xANBZ_YVJ(sB8A(pke0Z3JBqPaxw z0E%t!fA?YH{{i7UK#eB~-y-&K+Y3RQ|FV>Q6n<;V!-?zDU^&=Bwy?A__eBm8*6`W8 zPMjj@{TQ`h+ZN!L9~!_<7J#3iV6FE1R(MOiy+z;3A`G|bf_o_DoG}@U;_<9!WE34; z0W3>vl<0iQzJ{yP@C>3y%@zqyXN7Kwjrn5vdUacSc!E|tqRRo-=U(Y6hX1-UXN%tv z^cU|h!b(Z0>lLy&@(=$?wL6ZrJ#PKMDG>S`a>0G(56;PiSwkGJ5*4kuuSFa1XCaSg zEdNV5bUo|@+4h@6%=xC+)D?YTQcBD>EP`*f=voZ0W4V?3FOq{gd}|B0(sXQ81SdpFP82R)Y;$jcyqWfIi*ZxgyYea| z+D<@BfgZ)dRWcDZxkmicieUKo$hxuwUq{iURGF-%St&gEB=0{I{=R-o6n}+pTmJl| z^sn?-F^4eWFNyef_Gdpf{++DmBl3dHzr~gg;uLcxGW%jWmc15c(1xGwE;)F9rZ@up zY(alkK%6_;%ew_%OZ{VPCkUYArF!sIehfm$qO73bujECp-=YQ|; z9H9C?#QiVoZ!9|>jC279pu>Xd%`q-|k#NKbG5@koXuYHSbU^v`@*CsfJH>6BO;y)} z)%Uj*J;R@+Q9tYrpK)u3jQ0Hac%j7XKjyDQeH+>m=KrQIG>ZKPl{YkFrsT^4lhax7 z{ni*XE|IU-9Ju|%*iVN}ji;a3ej4X`skYL#?n6rcNkmT-_%#_Y z)K>m<3flbdnLmASLp*#Bl|K!_Y2(KQ&)#Z2o$&kfzJGY$JNdi7^P`V{Z17xu{eJK> z@cr}qp{oCb;rEH?{XU!;eq8uDW72-$`R5D1cY6PF;=$r4-kQ<#V|4#SHzoV$#UCbu z>U24!Pv7q;{ZRMMj{C&{_s`yHeo^|Lb7=c#+f(HKX8-gWqmEPO6R$BGA%BzUnk#36 z@uZI3UDA3i8zNt?ik)vd+n*0vACr7t{CbbK&!ffm{;A?&IN z+WpYev){jb|44B>eD}A1CZH!<9{rp^h{NJihe}U}A&)M&HhJ_JT%LYddGx8P z2lTo;qLc;BCpk9QPTGY&qY`52cF-2 z|MFi=-zITpj>j|-jyezqTYe*FE*qfh-bp8og8?-R+RDJ19mal!M0pX>*omEXTS zdY$V3VDPi^{qvt&4illd+~I+gTvkrV=qot<2g+J zYM=KN z2v-05vN!d_57m2BNm;7}9@ehaYKXv)2`1LDp_3yE>w2}Wn{6(<8x1rau z?s2NvQu(K>Tm#-cXQWx)J=3F}R6F!s+e59@nXf^)lPG_YeagpYy7KYaspsw#9|!zp zhFcze_{&5N!e2)8OI=u!R)4Xbw!c_J`SXz#BF>*rRVBU-w^qHJ+py2Sq}-K-#idK|$<#NBdA2wPY~`UtMwf(`Eh=$9+}=ayBx&M!Jv0Omd&Fx5M;Zm+ zWPg-i#Q$K5&%jZyBzw9qIKdYD%dOmhs`y&e9jPOJNg*#1U)cKpwTZaGL1=-0fb&-G zk|NpF)rr>sAa;cxV*P#b>yQ6w7b6GKIHePE z5*9K>oOP3h<5Mt)e*F$bp3AsW zTZ-GfhYW8MxA={>NdXYJ*&j+dhVl%LJv$A?Y?&QV3^2S*I)N7R?teND=-556n zClyu~Zr;5jspM2&WUW=NS{19-mM*W}njGqpk-XY-#zq;Pw~EwLuXC5Xs(>+*e>J}S zMDugD&hKbxL3!0PbZ^y|=p@yoqSu*mbh;y3u$?J?IF&LLJ{fvEdx^wllzm!E%oc$T zT^8Aa4wXd&Cz10|yzPxI{Km7A7o>1v*zc_Nl`qJ11V&2XE4kGS zyQ8_;8|_rQP+auvGMa7qMr9Ae-EvkhIobdp93mydkvLiMIUh$&F3LATcW9pUp^T+Y z{idi$F6JoO61uXg<$U5&A={Gm1ZT=3d7d0YbtMCILML#|jAd`JOEtI2-Xf)%3;#rb zin-z@nFL`Fq$lGOQ4s%_ELSL2D)28`Da)XYoJ8U(cU!eynOA5_OC;QaicSy*^=>Nx zR^h~I(DW`6Pxzf-NJJO>8K%D&jT*7w;Uks|G;aDp2&Y3nNPn(HJAPxM(2mLlxz3k} zc(Sfu0gCsy4DyLl4%gh2#<8sL6YIQ0usBJhP1-$4>>BF1>rsc}L7&l>yS#N&8=H-0 z)~g^Yf5eTiF*!nv@fv+&16gM&>e@xWDLkp|2%VLYh3mvaIn%wnI*I6p$lBKWyi8K+ z@+PD*$sQ?GHjv3pnsHI9KXse_57c`cd~D)FM#1$UN_PAzaP!RCy~5W`$q1(q zGEXm9RWd^lT4Je02NXr&EA;3$zDb-t4N^7k$kcQ9`U+Zn+OQUpoqLPOCQ=z|V7z5M zG7H3%1cS~dz2KX`lOn5t|4{+h^63{U0-3i?Ivpc_*9$u z+Q~2T%%=bTVDo({a8UErWxl(v>o{NGuUV3PnDfoR&xDs`5soZ-GKHrgm8pQ7wDMDt z=eJNR7v`#-;5RMf(hL>3>|+bRF+;B%nyG;VuBtP%UhXU&$>Wtz-g64syGh)Tp=W5l ze0jd$lnh_3OWo_P${|Z(CY&H+24R<)2={hcDLf#Ggpvz)8*C7ir4r6p22FIy*0ODGUp z01$hUW@Pa zkI`&*C=-iy+O=i`nQMXtvi-Q!a-T2D#I^(i9B?zL{;h0A=U-0}7L^P_Hd@g6#A4Zj zQL$6IFo$By$W9+x9yjLX^nH=VYC{ri&5X(qWuGnz$ zC|G>*BxX7k)m86*$M?W6<$&#j6z~Mh~JoYUdqc9Ce#+MS4A#dGbjwXfE;V z#+9+jwW0GwlN%$G8&0n^UDf0OZda|>>b2O&PUZv~h`!Z%+K!h!!t4eM<;AH0h zMkccpCKyGd>4HY@Ob}GC5{$IVcHUOBQTU|t|7J-_XJa|3m;$f};g=!e0)y##NT+aJ zvia3Cnhd2*|0i$Jl*L(AhzEQYlqZ*c>KgScqxYrhp`lrJZaui+{y1PSAOnNSX~?m7 zc@Avq51^?UoBU~Q{{vD<>Ew5gYaj8C_QM|}A6y?Rg`nw-J}!#(5!e1dzH9rFTHF5* zS*_c^_p*F}vEb`j%pfbC91`seHwnh{k@jkb<$-lDki9+zOe+MQuqmY#~Op7^VJnqrMv z=yePzZcTrrQl0G&EoMnmeXXWkc2m05RGu`I6Kg6XZe$7f*jzP#vd6%l0AiNiT8`CP zh6MvXa>~H_9gaxfQyz%JJz2mgeHkgZ($QA4T=boM{Pclrt?A zrvZBEfSLB5R9TuNFd-oZo2KXojU-Ebiwu3<_~&3eO9!2+lH_)lA6F&&R@#@*IU;;r~d zt5B!GxPvo~SrLaKi;LdhQt@ONOv*%dH=3+Sx7hA_>HgejqMnJ$} z{18+_m(q~BMfxh@UO*$M1NafyO@-s+s4c}@a<0_wkv5z%Dt1s(GRro~R8nX~WP_A8 zqi1MSV(08R2eH9^d>{B>{_HAQ=n;8J<9$(~u`r*MyZuWQfa1X1s-K_;Lk46mxpp*n zRS$@o?-CnZFEexL!A#EVf6vA8Ds{QS0t`DC3OICmjL~Tr5` zw7})eoCI(g$>=04k(I%K_%Xn-){SmdH{K-u$bvqCl2ivyGXFGC&^=NzSWPL>Y%I}d zSehVsk>9z>Bgu~7+04nD%+oZR2{-kA>iBf97bSy2Ze8aqWA`6H~C>~ zZSd{rT{gZ6fDRtt+Jfg$@J&FZTOcy7%rd*GOq2=n%{3;bZ8`mx*x@A45Hj>nbf@@@ zy>6qPJxP^qacHu zr2g4KkU|5BOZcvBxunWmmhNm294`VFTlGzepBK44OC|UhP0n?qLnB@s;eW9pOsD!A zL4VQT?1|IgXu8wV-=MttnDb1W^5$tfu$abGRE0fO(dejX3laO_Z^vBDQ%a44h#d#N zhMQvWLzuJyh^i<700(yf;Askgt~mHf-XsM;NuFd11Vv`)41!U8RxAA4KHnCC*S13N zI|1-doS6;@fcXNzEk&&WwCS+~rO=h`u}a0!K|)!4V><~2n2KU3PCmJ48K)c4p%3m% zK7pCER<`#Wv$AwC2F*#~EyH8*>+m#>A*W7DcTXx8_7C%FuMqC|G!lN_O;upWWQA{u zn7wLW>CV;Gh|2uU20|)ZB@Zn5`X5AhKPi#qp@MRy&mwb&GD;zHR7MsIX3Lkl0Q(U< zqWb#jw!VIPtG<4U2pY&&DC0%i$g`ylq_OqppwpGcK025~79<|NO9YADW0|2Sae<#@ zeV?uy3-UVQ;=(RQvK@9++{t3}iiZrhZ100~W0niKv;FQ9l|HnkN5q5=*(by>@j8T| zh%+XB+&)UwG!JKs+<~UXg{B5$snXPt`XFjIh?hUw?6;QonJFZ(%L)@T_S#agC5D_e zirialx2GrAZlhv|wcQF7)RxQ4bBSLt#y+~cbz^qE z_^P=aQTb2taeKq*cz%!HL6yH6#afk#<1V#MBAgYBkPaTsu9lT7TPX_~ua;KwZrP`( z;kYYxcX3SQoo?-woEcak)W%Z2(K<7%)(4NwoG}oQ7h_RSWO8!fIVpXMU4F5)bWLH% z#d#qKSy$h4wM#JJm7dBz*l7vyjPX9!QWKGLFQL?5@La&J7tw<42YmG*%6qaJgN_0RTd4K5m zzf|Ciwg&$QpPaidOWW;8D!D+P8eVn$S7&ezMue``gCw*T-h@Q!&cJ;pE2uuPp}=`y zJ8o6liSmk5!@v1*Go`%DKKpshAc5a=DZ|7o$h{0637sc-3LNtb#kI#-AIm2tqbUqs zp6o8KPBxPDo9ZU%L9|y12GZK%&=#w)dCOz^1vZG(;Ll>rmII9(OhiGGh}WLqf!Aik zYi<2Z9KW^L?We+SUt|ixZWVitvD*?6?cuep`%4_ZRqS?WTXuVcC>BVIi^`7* zGu|-_stvnU>Y``@MtiiJJ&J8EtK3fJx_GL5MtDeDR7P>evc%j z0BaBs*V!O0<(+2E;9+ZHQ%W$DVWFNazqOh+-jrqq?wz#P7V7^ibqqlLD0SrB*6#wVDyE}gB_HBob?5sER_3WuSpA7-EsaUk-ukw&oWd>roy(27@~!>D zds1_kCmG_O=P|ZWTd^W2Ey^7pBXIsc?I-Ti!V_P>o)mYMW4}^?6{q9ej{e zO)v>JV9c-PB+heIYngUyyIlKgEiV4YI>d`HG(!}jDhSA9+)gCRR*!KTI_?JJAq8LO zw;tm``9%32w=q4#>-@@XEK?T>n!MWR_0A?Y^%)9kw7W21I6MXGwR;|s+bLAE!c*|B zvD2;jR}|E0els*YQm{(9OR(Bquwq^xcR|g3iU|~^8TFJL@fdx6(dhIJe4U>%-37Jt z$!>}&(WuWQ*2;&vw1@h6HEZQ}t}dvZcck^WKbz-sH^&~wEt4AK$y9)ueY}rKFwW&9 za8H_@=?iboiqsXXnqTf~+H!1lV-ITsPNmYPEpy8L?^5=)1eP2NA7lI`Qx>(j^7RGj z^8CWw<%5$J_D$+#a~NApk20CF=dmj7n9OyCTuVSW_sfwhl-k?=T|ABEKsEf0Bbl+1 zGd9%s*^{Fra~n^!(SKdR__D;N`SvTEFddbz2QJi+1A_P~-Ih6vU*Otq&%0HO~Mg;)}D0wyU4ES0bEd`&?>*9DQtL>So25aREatiR z7B|9(;@d2lspxO5S$7u0*;i65$F+gG!IMJ#>n@M(-^0=%%u-*Z8u2C6qm+{`PJG4} z!R}HYtFRzEX7t(~h+Dxh85y}YL{v5^Sb2A(Zd)B%$6e_T4 z&~WJZIk(FSL78z|!%7U=30Lj?OZg#`dKHYw+8Q|It5usGw`#w8N3B}q#OSKM$Ws<# z*?%qk5?4L7kr}^KG-#-Wac=YgF|RZ1E~qc*tI*c{f~>7u8;!Fe8{vwOJK}yp77+0w zbXg>KjYsosbQdh2cWj~YGnVLQXdp%M0#e~UM!UohY&d0t%HE8i^{fsVntt}x>=T$Y#n3#tRh z#9zmQGC4wx0=E%vJl3tfk`Wpj88Ux%I?L~Iu92&XJQo%;FGzJqmN))Ijn9p58cUkN z!0=}V&p}l|bn_KFO)9JBc4Ic>EAuC?iV$6qk`av;1(LZh%U`9`ll;$lDoysh>?|?3 zl&fD#vUB_sXJ)uVgDw`@m~{lggW6i-XoVcszH$@q;h7n82D;&D&RRlMa9{L?dW2}D zZN2QYk)a-CLyeS0@=P$L*W59Y2^mxD*!96`pAuOp}L(EAq z!xwJ2#9#1!V4W9gJB9mkZ~|8u3OCY35_b*_@)q84iN{z2zxWUnR-_IS@=@%AcX!>b1)U%VS`9&EC`6Oe`+K6V-a5Pj*Li+c^dVg*rbEmkeAdG$bzVu ze32gplP4#yVp&e{N{}^cMK(SOz9HI`eY!*qHudXKkj_9=`eQLyRQ)_+9B}8Ba&dT@ zf@ey9wrLYCviL13ajeK(X@4z}U#FYTs;?B#Q)1oG*OJtG=@#9%&nl-NeradR$&&qN zdYr1PuQ+qE7g63K9IXXBdxkl0u)s0ue;R$T=s*t+Hiy{{TApxG$5lAh0ew9~x4}0| zC(mn35jXC#&QYk-J{>4yd03@eNA)96Ya>u7c($oWi26v%b(MYUa>}Er_ZHd^uFT_{ zDNrp!N|KEVD36EOm8TWobNm!Owr|O&7`BuU_J{iZjkB?zTc=YQX0hBm=ano*p$7w1 zo?sx8yYl?Oaanjnx%8kH=cGWsC-`g~xA=q4ZdGsh*UJN;%3bmntlXmBo;7*n&N*;A z?p&;2cLa_H}1?~a9*Q@BV=|G2Qbqenvmo*ivK_M-aNdiqKgAg zvve>$g1LAK&xkd2-L4nKLtIX3m^Bb7r}rhJq{V z1r3xWH?euY9n7nGV6`wKeb$aTYxp+OvWu^4hy#q&9?%_(K@~KOCuYl_a!sIbOlRZ_MKk& z9kU@VBcf-ZcS{swFu&Lsfg;&7a``z9!-Lm_jd7(=Knbu4VQ;J|4EevR^TUZ11;&xA zPKYN{Z5t$dyz@DlnLsFVga4k4e`7ls|P-htsP=_s-4B4 zs8;RFhf`t^okY^Vsvn#wTt-=n^+Tl4HU>A@wQ2sgk*8uAh-s8OMaNw1Buf>Wjt$n3m?Gf1n&gDU859JS%e zRAH2vwc%OQbbMtmIpzS<_v^@`a;XJ(QwyGAEm$(`3|dgf#SxQ3tvzx`_52ekIprf3 zZy=nH6-UG=tou=SOG}_m*51Pi;fO{uNMZ%0lnlq^l2@ybIVY6-y?a$!ZDnX9*aS={ zDVMz2F*T4kZvLzUe^$e`Kn%)?cbrSxv(SeBi9zQyq|!-LO@WISjx%Ue61C8ges(TJ z+qeg;6ycFHU&7|;|7XUl-y=v;8!%qo{ipHjFi;qM91pctnrbzeX<%m}=WrUTD3^(w z<4j+qhNSUmfqOg>D@YoSiqn$H{zv;G&)V%Tk>EWX)s`x^!M3$pviPpbr-0fo@23{{ zF6CT5*8$6cDpv{GWky}$^HR*sS}l6_|ANH%BD{8ku7s;@gR-%cOC0oN>iNl5jU-fT z&0a=~)TFA~vPCbPaF0SIkS#_}qD-~`Jw@FQIx&+iTt$KaF^xg8)WJ(-;2j7XCPk#g znZ&>r3YNGiJ}B|(25yQabe=eg53W_!5edR5*`4E-pL}ShY_xsL2KfS4irRorI%CTu zY4*C?rE+!tJd#m10}6Xm6dY53b-5HJG$TmBz_lnD;d+-^FI+&U6h%q7Dc5_x+s>m^|52EPpmepE=}~=mVWG95<;_;s;mkOTsFA5{WYA*dFttSN z_Ue3^C`#+Wya~l2>K`bg{{KI9jp-o8-O8kEyy&5=j;=ujvW7O=n5sf&>q52%vIaiv zNcC~KP}@pVgkE?8;~@lRw4*=j8I>Get;Cb&y4K-rsC$%*210XSgS8RgBqO`U2yDC7 zh^_O=Z)`FhNJ!DnkI^{7sK}U=Ob(t^zeN*WV`i?|)}y&GY3_mNXu0l#lb4y8vO*(O79erdu?|ej+B;Y#D2uGh z#e_DyIyJL|Ce3ljm<(_%#WbkhC>(NsG`_NwXfCF=NQmfX`{`t^V}Ua!izI8jUG*YSLM8zWk(Tz6?g8*)ogaHDSICjoO8o z^XrX~8Kyz_v3x8&ie#8HBV^uhw&yWv9#yxxW`?4=*ai|l$c=|bt?H_z>Vhr9V8*P( z-9AUrs^~V>0L-dko{Ofo1p&#zj^<+^s6`0bjSKuS7PE)Xc2N}3vv^`fn2F4#3 z`{1P=BO1&^75b!ObK0c}D@i2Pr5~o(qW($2mfR*R5)kM9Og5;xES| zcT6$XvxtLW99}?VolH7Dr-44{)ZBW){+I)_E{d|Tki#%4;ai^q8|RTky`+mL z|3=hSrf@3R-7!Pzk(A-xvW4SO2>7gtSn1DzWRY5`_5tK_Rd@zjQ7-;n7`_3z>gemW z=3oCn;lGlHnINr>JbZ>|rMWWKmmztpkT(<9b>tx|OF9Teb$(}9c-?|A?g7qbwSW)0 z=aXy{f&vR01u3vUU!$Hh&o4KG7E}DuIDH-^C%A-*%t~cU8XY+U#~XanMTNt#abYL7 zF{u(tHM%5rBgXtp6`YXlqjCNL6AIj#bj&o%dpt8lL$+Wc z3Z)G;>tPwQg&Se6BkM!5@CR0>(vtR1t3=FR|1if%45pfrRLB^(cr}SCWU@70)FEl) z^_2N0J`M%x@nMO%L`&AUbev#jJ9?9bGh@CV)~2YebfT$$=z^v`Mpyq(XBZ0j@2bVt z-<6tqmo}i`J{a{|!{yYCuN8U3SABdb9=2L^@<+}uya$(MsI-3?TvCF(2A2eQ)RLC} zFu1h$9$d`GLzK@cw*NZ5#KWeB@r5x1sD;&IABJ0TJo$fRy!j^s?PVw|%s^XvyutWn z>5)g{4H;>FvyBkPo3@e^-~)|lhj|!Kr|KR*&`iJ=>1m*$)e(|fI-R)`Jk!r#zn}*` zGG1F-dx=Tg|EdlBSN&`<=yA&@KgVbfd3E)(6xW`#q@UFbH<0PlJtF=FLb1o9E~5wj z#5;Eyb#9~OuYg?aV+8U0lz!dQuxS_O0=UdQ{JQ5hBu}wukCOddDXt?xm9>$dh%jkbj-*FqZ7WRxH_35C^cq3J?=W4U2;Y>~i?m zNd>W_^2DTQSB1?`B#stH45#{_=KcG`zJH#I8;`_LZDe+)ouMt8k@JJ71&}!~Ws`Z@ zy)5fK*GcwlaVfj@esmo;6&7tfYX_CRt7OsMMAemv-4(x!_m&9z?m)7tQ}ziJmNFmp zE<2WSa8c5m8In9^614H_3z;2sQ}IvQJ4@QNplWAv3#9m#vN za+=vOgzbz+6Hyw%sMY8sYjJrP`p#O~EgW(GAy#YQjbzbw49yGWk9$~9p~7jYa0yg6 zl%;GEFAL2is60A%d2kC1FOLRhZXomWV3Fp(mxl_$%R@y&c~bvYo_sX1MCfD6V*s)L zqdcvsJYYA*6JWQmVRof+Jj68re2z!=EbbD@`=yEsB&f#6yH}&AkEW5fiR~FEHWmqV zsc7SLyl9~w#N$04^>r%_F^CV?nsPq-LaTasIj$C27-gqKOH07J5VIxX_<~7oUvm#1 zS?=u=x>aK#i{Q9+qoj)n_KL(6lup0sF(2@&GyjaH>7RY^ouwUc`C=mWFKw&=4x) zH#FpOQJTq4?I)tp}4dK_p{(j!~;T0^p?Boxlw&=!Qz;6++nFwTl<8XuvcF1pORT-RForT z8SoLyz39sJTZJau=0~9$U>SJpo%(064scq3$DtR>i|;Fh-NlX;ii+fycQ-L+!oCo3 ze@?Z!Ko$g4b+_WK{BjsIJ9R5y1n zr7Ccncpn|Anmw^Yj1iqNWP3AQNW_EGKMk2!Yhw zc(gi2alx2$e1-})NmZ*s5++4S6?EZ~x^_w(l$8=^T06LX;=~f5# zrCT_M0|Lt&X0m;gq*%nzYt>H}-MAI#7()nR<-p*K$~i@5AF81D+aiSYJZmnpg>H^J zm^wr4{8r5IzEn|zQ9D?&kHNHYl~^1LI^rnBS`gI@YRSDXs7@`le}-!5!4FhR zp5Np7BUw0&Qes?ITgpKJwrbzRj@zpB4_v|x{r4_-;gf*aE3;p?-$%VA->(L&EL37m zx~#a`UvXNWTNxXM4Mth$~dwp_3N zuGl#R-Bl=y`F&QX-uyeJcXT&h7x+!v!GZ_lRiF2l47Ovn<%FkHfsZSd+g`+qXa3=D z#p|Bi0o|DuuXt`tsd(e5d<$h2z(@J^Q~B&ENyW9wmpX03-^*t&u2sHbI38O*Y&>P< zQ!nTM2KJQkKUBsoRL1Hlw!Bu)J#<;_wwy86mUqBNuYog$+43&;r&pgD18sSuJJV~( z44o~nMfgKk&*qq5&xiwx3XV%EIG(iSef!G;SI^DA2FhkMvgL(7`rXx2{YWmG5zHzu zOr~C9ua?K!iPgZxwC5L`r=eaMjUMUYE@?j+ZqUM^fdc!iV4Km$IsiwH`IfY+=(@y$ zwYQ|bGKKsw#(3{mPK}~YqAGt;45!iR?_1J>HnS0E(qD4m(mv&A#PU$Y+IcHpVOe_z25 zc={lmv@y^NjrKsQt$^ZV1GeK@6VhgDgk^Skz{G9pWx4N@J6#>hR;|?S44a^gZ^CY- zNGS8^4!;K2uS8o{3>By&BUMpaRot-!L!x34UQyi%#$TrPi4i6^y4IH>)VL$aScEu4 zsBwH34ihq?MqG=wIDJfd+_RWFTOD0c1)7i*6b^%3B*A8emm|-eCCF!+*QJx*i{P}?j-6eoCNU=9>>uqiv(Bk@Dt9%A0wvq_rRh&nl& zHmTN31e(=Qw9l}d9RScF^&-c84e`6FQ$S9=v&le2C~moVcrQ|N{e>v?BQ-NI(m?U znO9_r@R|n44n~!r>M-itdj2+u8e^`V&6$vpP4(*p);yc`8MdPOhQcdtlVc62j-XsqArkJ`xI-)`ih-9mA(Q(j!vJgB&5^vFf>nJsC) zlU>GwD)e+7t1iNfpR#u!LjKTBkCyd(@4fdLsryw}%6-_Yusa235!C(5urOuW6Io*1 zPU-xKgj3+7-nctu2X{jsD#PJ1xv}_CFJn+yO45OuF&=$@*0Zp6TD@^UQ9#o^LL+pB zI^&|}zUEZ*#xmnp&_TaODTs)hpNlW`P6;Z9foP^5($T|8g3W-8BC+N;zu~@EkNss* zR5=X~5T_U(`nKW2g8>%Ed#RbPg!Z@f=o)txTHBi#6y}U135d}H-46T-)%q2TkWWYA zc!S1h9l8jc<#M}$W8S67v@z8-Sd(T8{t7w}?)tV3eHR;C1=>>!bR|4`u*HG$WSXPa2}Y#v@vH6gT7Pu;BVT_W9t>>k60^7KzCu)qr2$ zoT-GC`KDgLh08M|XJDN1qy4}zj&r1$wBr>I>r7CN8|rTx3U0qXJ5@MK?Vlzbz%fBj zd-TOZlwP0)Icikk5J!a!r}dO65V*6ikg>gwwds4TK`&`&P1_iB0F8y3kjzJ(Nk7)R zv2@N$8%?SOb~#;`^Hy5s9mr0#%?8Fywe?^cv3>^{LxxMIi$D*s=3)h8m}{^tyy&s? zJfL||(&0Imi6XtVDK6gti~TE#QY@t?%?(UiL76F-T0*3)x&U)~S2;d;f?68xZ|PA; zCTh5`kN7#5sK@ZC#hC1=S~Q!wzu0!dn`==7h26>^iBlaW0PkW`kf{f?m}G6F86v+* zl3o6uCg|>_#DHb(O#i3L-_!e{-{SzDqa=h_G_5V0|Dz+5`G?pRLI~%crt2PajSHI} zXTT`&!T`jrqlux7{-%PJ(_GA^>h~NWW6XH03btS!lS|IuMY(Qte1ce9Fi{q4V#V&O z%Rz_wqb2W9Z1d?<)r#>X+4o~&9WoWK=w#~e0t|6yh2mty)q#B!IC#je%Ejp})!QRv zvhU5PO0wzdF6a=AB< zgJr0$;>*E76>x;o%tvnxQ-3#DZn@N(vK>);$u|hA>c{og0Nh8XuvGO9!Y1?7eC=^Z zpCo0z-s;n&+;T~04blrfZDe;{uAZ_=jL1R>D1a8KPCglQbQ`sWwShh_(dB}+)1M_^ z4BBnQo$|wdim%Hpm$g9^)&|Hxj^mG(s$d?bD8B9+RAKp}{qg&_cp53FmzE5)DI<-F zQquS`sJ%;kslCM!ClrkxjmE}KX>Q*$la?T;-q7&6&KK`15RH3Ul3w)bLtB@tFp`O9 z44{VOP!o@EU+N%j*3zm&1J&)Y2%bzjADDnt1~4yOzO*X_`Vd_+?8oF2

    $B4BE>y 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 v44+ZqF*+D!!CNSNrGck(@DuSP6#PGFindSettingById(static_cast(Settings::SettingID::kHDR_Saturation))) { saturation->m_Enabled.SetValue(a_bEnable); } + + if (const auto contrast = a_model->FindSettingById(static_cast(Settings::SettingID::kHDR_Contrast))) { + contrast->m_Enabled.SetValue(a_bEnable); + } } void Hooks::CreateCheckboxSetting(RE::ArrayNestedUIValue* a_settingList, Settings::Checkbox& a_setting, bool a_bEnabled) @@ -114,7 +118,7 @@ namespace Hooks CreateSliderSetting(a_settingList, settings->LUTCorrectionStrength, true); CreateSliderSetting(a_settingList, settings->ColorGradingStrength, true); CreateStepperSetting(a_settingList, settings->FilmGrainType, true); - CreateStepperSetting(a_settingList, settings->PostSharpening, true); + CreateCheckboxSetting(a_settingList, settings->PostSharpen, true); CreateSeparator(a_settingList, Settings::SettingID::kEND); } @@ -139,7 +143,7 @@ namespace Hooks static_cast(*settings->LUTCorrectionStrength.value * 0.01f), // 0-100 to 0-1 static_cast(*settings->ColorGradingStrength.value * 0.01f), // 0-100 to 0-1 static_cast(*settings->FilmGrainType.value), - static_cast(*settings->PostSharpening.value), + static_cast(*settings->PostSharpen.value), static_cast(bIsAtEndOfFrame.load()), static_cast(*settings->DevSetting01.value * 0.01f), // 0-100 to 0-1 static_cast(*settings->DevSetting02.value * 0.01f), // 0-100 to 0-1 @@ -254,36 +258,66 @@ namespace Hooks void Hooks::Hook_SettingsDataModelBoolEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& a_eventData) { + const auto settings = Settings::Main::GetSingleton(); + + auto HandleSetting = [&](Settings::Checkbox& a_setting) { + const auto prevValue = a_setting.value.get_data(); + const auto newValue = a_eventData.m_Value.Bool; + if (prevValue != newValue) { + *a_setting.value = newValue; + settings->Save(); + } + }; + + switch (a_eventData.m_SettingID) { + case static_cast(Settings::SettingID::kPostSharpen): + HandleSetting(settings->PostSharpen); + break; + } + _SettingsDataModelBoolEvent(a_arg1, a_eventData); } void Hooks::Hook_SettingsDataModelIntEvent(void* a_arg1, RE::SettingsDataModel::UpdateEventData& a_eventData) { - if (a_eventData.m_SettingID == static_cast(Settings::SettingID::kDisplayMode)) { - const auto settings = Settings::Main::GetSingleton(); + const auto settings = Settings::Main::GetSingleton(); - const auto prevValue = *settings->DisplayMode.value; + auto HandleSetting = [&](Settings::Stepper& a_setting) { + const auto prevValue = a_setting.value.get_data(); const auto newValue = a_eventData.m_Value.Int; if (prevValue != newValue) { - *settings->DisplayMode.value = newValue; - - const RE::BS_DXGI_FORMAT newFormat = settings->GetDisplayModeFormat(); + *a_setting.value = newValue; + settings->Save(); + return true; + } + return false; + }; - Utils::SetBufferFormat(RE::Buffers::FrameBuffer, newFormat); + switch (a_eventData.m_SettingID) { + case static_cast(Settings::SettingID::kDisplayMode): + { + const auto prevDisplayMode = settings->DisplayMode.value.get_data(); + if (HandleSetting(settings->DisplayMode)) { + const RE::BS_DXGI_FORMAT newFormat = settings->GetDisplayModeFormat(); - if (prevValue == 0) { - ToggleEnableHDRSubSettings(a_eventData.m_Model, true); - } else if (newValue == 0) { - ToggleEnableHDRSubSettings(a_eventData.m_Model, false); - } + Utils::SetBufferFormat(RE::Buffers::FrameBuffer, newFormat); - Settings::swapChainObject->format = newFormat; + if (prevDisplayMode == 0) { + ToggleEnableHDRSubSettings(a_eventData.m_Model, true); + } else if (settings->DisplayMode.value.get_data() == 0) { + ToggleEnableHDRSubSettings(a_eventData.m_Model, false); + } - // toggle vsync to force a swapchain recreation - Offsets::ToggleVsync(reinterpret_cast(*Offsets::unkToggleVsyncArg1Ptr + 0x8), *Offsets::bEnableVsync); + Settings::swapChainObject->format = newFormat; - settings->Save(); + // toggle vsync to force a swapchain recreation + Offsets::ToggleVsync(reinterpret_cast(*Offsets::unkToggleVsyncArg1Ptr + 0x8), *Offsets::bEnableVsync); + } } + break; + case static_cast(Settings::SettingID::kFilmGrainType): + HandleSetting(settings->FilmGrainType); + break; } _SettingsDataModelIntEvent(a_arg1, a_eventData); @@ -327,12 +361,6 @@ namespace Hooks case static_cast(Settings::SettingID::kColorGradingStrength): HandleSetting(settings->ColorGradingStrength); break; - case static_cast(Settings::SettingID::kFilmGrainType): - HandleSetting(settings->FilmGrainType); - break; - case static_cast(Settings::SettingID::kPostSharpening): - HandleSetting(settings->PostSharpening); - break; } _SettingsDataModelFloatEvent(a_arg1, a_eventData); diff --git a/Plugin/src/Settings.cpp b/Plugin/src/Settings.cpp index aa74de81..9400f148 100644 --- a/Plugin/src/Settings.cpp +++ b/Plugin/src/Settings.cpp @@ -56,7 +56,7 @@ namespace Settings { static std::once_flag ConfigInit; std::call_once(ConfigInit, [&]() { - config.Bind(DisplayMode.value, 0); + config.Bind(DisplayMode.value, DisplayMode.defaultValue); config.Bind(PeakBrightness.value, PeakBrightness.defaultValue); config.Bind(GamePaperWhite.value, GamePaperWhite.defaultValue); config.Bind(UIPaperWhite.value, UIPaperWhite.defaultValue); @@ -65,7 +65,7 @@ namespace Settings config.Bind(LUTCorrectionStrength.value, LUTCorrectionStrength.defaultValue); config.Bind(ColorGradingStrength.value, ColorGradingStrength.defaultValue); config.Bind(FilmGrainType.value, FilmGrainType.defaultValue); - config.Bind(PostSharpening.value, PostSharpening.defaultValue); + config.Bind(PostSharpen.value, PostSharpen.defaultValue); config.Bind(DevSetting01.value, DevSetting01.defaultValue); config.Bind(DevSetting02.value, DevSetting02.defaultValue); config.Bind(DevSetting03.value, DevSetting03.defaultValue); diff --git a/Plugin/src/Settings.h b/Plugin/src/Settings.h index 8a33da2e..0cb70391 100644 --- a/Plugin/src/Settings.h +++ b/Plugin/src/Settings.h @@ -24,7 +24,7 @@ namespace Settings kLUTCorrectionStrength, kColorGradingStrength, kFilmGrainType, - kPostSharpening, + kPostSharpen, kEND, @@ -45,20 +45,22 @@ namespace Settings struct Checkbox : Setting { Boolean value; + bool defaultValue; }; struct Stepper : Setting { Integer value; + int32_t defaultValue; std::vector optionNames; }; struct Slider : Setting { Double value; + float defaultValue; float sliderMin; float sliderMax; - float defaultValue; float GetSliderPercentage() const; std::string GetSliderText() const; @@ -78,7 +80,7 @@ namespace Settings float LUTCorrectionStrength; float ColorGradingStrength; uint32_t FilmGrainType; - uint32_t PostSharpening; + uint32_t PostSharpen; uint32_t bIsAtEndOfFrame; float DevSetting01; float DevSetting02; @@ -91,18 +93,86 @@ namespace Settings class Main : public DKUtil::model::Singleton
    { public: - Stepper DisplayMode{ SettingID::kDisplayMode, "Display Mode", "Sets the game's display mode between SDR (Gamma 2.2 Rec.709), HDR10 BT.2020 PQ, or HDR scRGB. HDR scRGB offers the highest quality but might not be compatible with technologies like DLSS Super Resolution", { "DisplayMode", "Main" }, { "SDR", "HDR10", "HDR scRGB" } }; - - Slider PeakBrightness{ SettingID::kHDR_PeakBrightness, "Peak Brightness", "Sets the peak nits brightness in HDR modes, this should match your display peak brightness. This will not influence the game average brightness", { "PeakBrightness", "HDR" }, 80.f, 10000.f, 1000.f }; - Slider GamePaperWhite{ SettingID::kHDR_GamePaperWhite, "Game Paper White", "Sets the game paper white nits brightness in HDR modes. This influences the average brightness of the image without affecting the peak brightness. Change it to what looks best to you, the reference default is 203", { "GamePaperWhite", "HDR" }, 80.f, 500.f, 203.f }; - Slider UIPaperWhite{ SettingID::kHDR_UIPaperWhite, "UI Paper White", "Sets the UI paper white nits brightness in HDR modes. Change it to what looks best to you, the reference default is 203", { "UIPaperWhite", "HDR" }, 80.f, 500.f, 203.f }; - Slider Saturation{ SettingID::kHDR_Saturation, "Saturation", "Sets the saturation strength in HDR modes (only applies if \"LUT Correction\" is on) (neutral at 50)", { "Saturation", "HDR" }, 0.f, 100.f, 50.f }; - Slider Contrast{ SettingID::kHDR_Contrast, "Contrast", "Sets the contrast strength in HDR modes (neutral at 50)", { "Contrast", "HDR" }, 0.f, 100.f, 50.f }; - Slider LUTCorrectionStrength{ SettingID::kLUTCorrectionStrength, "LUT Correction Strength", "Sets the LUT correction (normalization) strength, this removes the fogginess from the game vanilla LUTs", { "LUTCorrectionStrength", "Main" }, 0.f, 100.f, 100.f }; - Slider ColorGradingStrength{ SettingID::kColorGradingStrength, "Color Grading Strength", "Sets the color grading strength (e.g. it disables LUTs)", { "ColorGradingStrength", "Main" }, 0.f, 100.f, 100.f }; - Stepper FilmGrainType{ SettingID::kFilmGrainType, "Film Grain Type", "Change the Film Grain type, pick the one that looks best to you", { "FilmGrainType", "Main" }, { "Vanilla", "Improved" } }; - Stepper PostSharpening{ SettingID::kPostSharpening, "Post Sharpening", "Allows you to disable the game default forced post sharpening pass", { "PostSharpening", "Main" }, { "Off", "On" } }; + Stepper DisplayMode{ SettingID::kDisplayMode, "Display Mode", "Sets the game's display mode between SDR (Gamma 2.2 Rec.709), HDR10 BT.2020 PQ, or HDR scRGB.\n\nHDR scRGB offers the highest quality but is not compatible with technologies like DLSS Frame Generation", { "DisplayMode", "Main" }, 0, { "SDR", "HDR10", "HDR scRGB" } }; + Slider PeakBrightness{ + SettingID::kHDR_PeakBrightness, + "Peak Brightness", + "Sets the peak brightness in HDR modes, this should match your display peak brightness. This will not influence the game average brightness.", + { "PeakBrightness", "HDR" }, + 1000.f, + 80.f, + 10000.f + }; + Slider GamePaperWhite{ + SettingID::kHDR_GamePaperWhite, + "Game Paper White", + "Sets the game paper white brightness in HDR modes. This influences the average brightness of the image without affecting the peak brightness. The reference default is 203.", + { "GamePaperWhite", "HDR" }, + 203.f, + 80.f, + 500.f + }; + Slider UIPaperWhite{ + SettingID::kHDR_UIPaperWhite, + "UI Paper White", + "Sets the UI paper white brightness in HDR modes. The reference default is 203.", + { "UIPaperWhite", "HDR" }, + 203.f, + 80.f, + 500.f + }; + Slider Saturation{ + SettingID::kHDR_Saturation, + "Saturation", + "Sets the saturation strength in HDR modes with \"LUT Correction\" enabled. Neutral default at 50.", + { "Saturation", "HDR" }, + 50.f, + 0.f, + 100.f + }; + Slider Contrast{ + SettingID::kHDR_Contrast, + "Contrast", + "Sets the contrast strength in HDR modes. Neutral default at 50.", + { "Contrast", "HDR" }, + 50.f, + 0.f, + 100.f + }; + Slider LUTCorrectionStrength{ + SettingID::kLUTCorrectionStrength, + "LUT Correction Strength", + "Sets the LUT correction (normalization) strength. This removes the fogginess from the game vanilla LUTs.", + { "LUTCorrectionStrength", "Main" }, + 100.f, + 0.f, + 100.f + }; + Slider ColorGradingStrength{ + SettingID::kColorGradingStrength, + "Color Grading Strength", + "Sets the color grading strength - how much the LUTs influence the final image.", + { "ColorGradingStrength", "Main" }, + 100.f, + 0.f, + 100.f + }; + Stepper FilmGrainType{ + SettingID::kFilmGrainType, + "Film Grain Type", + "Sets the film grain type.", + { "FilmGrainType", "Main" }, + 1, + { "Vanilla", "Improved" } + }; + Checkbox PostSharpen{ + SettingID::kPostSharpen, + "Post Sharpening", + "Enables the game default forced post sharpen pass.", + { "PostSharpen", "Main" }, + true, + }; #if 1 Slider DevSetting01{ SettingID::kDevSetting01, "DevSetting01", "Development setting", { "DevSetting01", "Dev" }, 0.f, 100.f, 0.f }; Slider DevSetting02{ SettingID::kDevSetting02, "DevSetting02", "Development setting", { "DevSetting02", "Dev" }, 0.f, 100.f, 0.f }; @@ -127,6 +197,17 @@ namespace Settings static inline RE::BGSSwapChainObject* swapChainObject = nullptr; // settings reshade overlay + inline bool DrawReshadeCheckbox(Settings::Main* a_settings, Checkbox& a_checkbox) + { + bool tempValue = *a_checkbox.value; + if (ImGui::Checkbox(a_checkbox.name.c_str(), &tempValue)) { + *a_checkbox.value = tempValue; + a_settings->Save(); + return true; + } + return false; + } + inline bool DrawReshadeStepper(Settings::Main* a_settings, Stepper& a_stepper) { int tempValue = *a_stepper.value; @@ -169,8 +250,8 @@ namespace Settings DrawReshadeSlider(settings, settings->Contrast); DrawReshadeSlider(settings, settings->LUTCorrectionStrength); DrawReshadeSlider(settings, settings->ColorGradingStrength); - DrawReshadeSlider(settings, settings->FilmGrainType); - DrawReshadeSlider(settings, settings->DisablePostSharpening); + DrawReshadeStepper(settings, settings->FilmGrainType); + DrawReshadeCheckbox(settings, settings->PostSharpen); #if 1 DrawReshadeSlider(settings, settings->DevSetting01); DrawReshadeSlider(settings, settings->DevSetting02); diff --git a/Release/Microsoft Store/Data/SFSE/Plugins/NativeHDR.toml b/Release/Microsoft Store/Data/SFSE/Plugins/NativeHDR.toml deleted file mode 100644 index b2e6e225..00000000 --- a/Release/Microsoft Store/Data/SFSE/Plugins/NativeHDR.toml +++ /dev/null @@ -1,32 +0,0 @@ -[Dev] -DevSetting01 = 0.0 -DevSetting02 = 0.0 - -[HDR] -Contrast = 50.0 -GamePaperWhite = 203.0 -PeakBrightness = 1000.0 -Saturation = 50.0 -UIPaperWhite = 203.0 - -[Main] -ColorGradingStrength = 100.0 -DisplayMode = 0 -LUTCorrectionStrength = 100.0 - -[RenderTargets] -RenderTargetsToUpgrade = [ - 'ImageSpaceBuffer', - 'ScaleformCompositeBuffer', - 'SF_ColorBuffer', - 'HDRImagespaceBuffer', - 'ImageSpaceHalfResBuffer', - 'ImageProcessColorTarget', - 'ImageSpaceBufferB10G11R11', - 'ImageSpaceBufferE5B9G9R9', - 'TAA_idTech7HistoryColorTarget', - 'EnvBRDF', - 'ImageSpaceBufferR10G10B10A2', - 'NativeResolutionColorBuffer01', - 'ColorBuffer01' -] \ No newline at end of file diff --git a/Release/Microsoft Store/NativeHDR.asi b/Release/Microsoft Store/NativeHDR.asi deleted file mode 100644 index 44ea70c52ab8c1d5e8a0e3d88e1f1819be4eed1d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 903168 zcmd?Sd3+Q_8b3TahD;#P6V52C3>qbhVkD@6sLVhHdUOIo4WL}{g1C4f%n%fTgqaDM zZChDaS6z>dt1Gy=D_&f#kN^@81;ky&V;4MDdKg{f1qrLn@B2K}-P04o_4~Z<`_Jpg zFx}NvPd)Y2^E~y`Q%_ZuUBAqhVYAsX@s~*0Y-{jK|GD|!&)u=t%U@mkmGO7o_MTsr;@$gBX#XVs4ey`8zf1Pd=HEJg@4LNr|9HIj-CnnU zGJeZnt^4Y(SC)P?qiD(g3cSxKx?lfY%K2s#&Hsw>m%r-d-x);_PQUuro2sbp_AM>< z+H5!1b+X;}{+YKJ-&$;Ey1JxyJKL6%ZL{qoirH*mu5;LIPJXNbImC0i&DM#NlD{@r zH$h~iXV@x{RTMIQj(!&@%j{ikvk6wGuC^+!pqtG>RX23C)lZ=h`hQO91|&OjY{f>J z^*1}mR)F`)tQ?!0;piV-*|ynP){Lpog9EqB4ev?tY+cC;teT?$EMfucTA60MERmldig6-%d1_l z1LRnNfsfktF?!ESzJEmT4!ol;ugk{s&rc;=08>6DaEV@L1KPn&X@Rcm^1#EgvQwK5+M)XUB)o@OI|WZm z^m-k`gRE>_=l6L=la@e7|Amt9XssReM1P{vKz?4uM9;OJ37(0v`eMGDXj}VrBL*f} zu{UBokc&3U(a1{FQN?<5Qy*16=dK#mVazvyo(}I~< zfYgGdQPkVfQt(4_X@2op$frzmhyG{xNKb8+)%)v_p!LbhvXx4!T9LdcGu#+hAgf~? za%hpuIVV$A@EG;yyJcl`9s!c20zjJ~Eyll60Y2an|4L48jdZTp_PyjRTlT$_TjrEi zsud+%p0##r2;SYYS^-4M+}c?mcd~hsa zuCg3Wnk`z{AUAtgP=g3C(kmJO*ru&h9JXc1^EB2ukXiZs*bZ%L> zO3GoV$l{gcu+8^~2gEn`~wk3L<3mskFEWy2l$k&TJSO8);x-6K)-nsez#WvegLIM71k>h8eLr|bgFNM2*V0^CHeSzMg-8|0E|Q0xV#>2ev)=jKBy zK*^7M3NvY?M@a*(x@9Gt-@*mZ$Z&ouJtAj0gP))0BuY3WCrgyu$-GNkR!NxvZaXo2 z0m%;d?!NpQ6sOECPL|H4WaKUw(iQla@(Ee0U zUAakC?i1Lf^Ts`dq}t7m$ZiZ<+#j>i*pfWGu?A{{sY`NpE^6d@Sg@(jkX+972n?xO z76_MCXw>LO>rmak-=VtD=GjJFJU&Km%ZFMC+SS8F1rsqB@N5wdcPn|bE~S-A?j_xQ}?CFUu9#9>=UcAq~wx0tBj zJjzWBR9Zi`K5EB3D+EvMFnZvf0_48lrle z!Nh7=mbN^Q4vE+V`XuDUrkcz8+k%I*-%zpW!e%)-b*GtkzqWw#h8Fg*1#X9S$wK*N zZT}*qsTKSAW!>#~Q7RH3uw%|JS?yVY(FZAI`%glPoQ*;79c}x=4luE|S<3b9ha&wD zqr)M;8r+Y16Ar&L3Qx+yLz?y*kX>no{eXS6X-JPouu5cP9SA(JHSYJKP;`krP-v+ zkYiA$R|iVHdaSkvqDR1<(19JAQ5wAkWT@zW6rHn9g3fP3%fF-DMDna`LEj7{d7HgV z=;j7Aq(j}@6mw}Wgwj#+^&0#uH^p7bcI`1rG5$04bCsc=FTw~_ZjrHm9VlpNDfv-I z&UTT^H;<~sEB#ink{(qKG_QdmLEV$c7Fxh{sPAtlP!R#jt%H&Us6ql&V1ja<2-KMb zYPbO9(m@pnP)>8~xWi#cDaog>zhQDKZQ9pI5(!~&&qrZ0xc~bg5&c*OH>?8BnIx#X zovPDQwGX0HsLyEH!HsEw&g*Cx)BeP;tn&zrU&Vg4>tCUbN#hg381O&9Yte2^eHvAV zHWyn=Og;bQsrjE1`KKN;|HFM!;Lj8JFX$k@)qb5ItGDI1Kvy|Q$`pH6cSBcE(`xQ6 zw*>~tN-gmMn>yghPgTK_R02F%0DGa3?1gEt70|%KnaGRZp~7IiSe?@a8k*FvdhvUB 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�gvO{s8bY`^QDP8 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≧on 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 Pk##SPC>IaZ|L*k<@>9`! diff --git a/Release/Steam/Data/SFSE/Plugins/NativeHDR.toml b/Release/Steam/Data/SFSE/Plugins/NativeHDR.toml deleted file mode 100644 index b2e6e225..00000000 --- a/Release/Steam/Data/SFSE/Plugins/NativeHDR.toml +++ /dev/null @@ -1,32 +0,0 @@ -[Dev] -DevSetting01 = 0.0 -DevSetting02 = 0.0 - -[HDR] -Contrast = 50.0 -GamePaperWhite = 203.0 -PeakBrightness = 1000.0 -Saturation = 50.0 -UIPaperWhite = 203.0 - -[Main] -ColorGradingStrength = 100.0 -DisplayMode = 0 -LUTCorrectionStrength = 100.0 - -[RenderTargets] -RenderTargetsToUpgrade = [ - 'ImageSpaceBuffer', - 'ScaleformCompositeBuffer', - 'SF_ColorBuffer', - 'HDRImagespaceBuffer', - 'ImageSpaceHalfResBuffer', - 'ImageProcessColorTarget', - 'ImageSpaceBufferB10G11R11', - 'ImageSpaceBufferE5B9G9R9', - 'TAA_idTech7HistoryColorTarget', - 'EnvBRDF', - 'ImageSpaceBufferR10G10B10A2', - 'NativeResolutionColorBuffer01', - 'ColorBuffer01' -] \ No newline at end of file diff --git a/shaders/FilmGrain/FilmGrain_ps.hlsl b/shaders/FilmGrain/FilmGrain_ps.hlsl index 4c7908b5..0766f8a4 100644 --- a/shaders/FilmGrain/FilmGrain_ps.hlsl +++ b/shaders/FilmGrain/FilmGrain_ps.hlsl @@ -5,8 +5,8 @@ #define FILM_GRAIN_TEXTURE_SIZE 1024u -// 0 Vanilla, 1 New Random (no flicker/repeating), 2 New random, no raised blacks 3 Kodak Filmic -#define FILM_GRAIN_TYPE 3 +// 1 New Random (no flicker/repeating), 2 New random, no raised blacks 3 Kodak Filmic +#define IMPROVED_FILM_GRAIN_TYPE 3 cbuffer _13_15 : register(b0, space0) { @@ -65,66 +65,81 @@ void frag_main() linearColor = gamma_sRGB_to_linear(inputColor); } -#if FILM_GRAIN_TYPE == 0 - const float filmGrainInvSize = 1.f / (FILM_GRAIN_TEXTURE_SIZE - 1u); - const float filmGrainHalfSize = 521.f; //TODO: rename in case we keep this as 521 - // Applying modulus against 1023 will give value between 0 and 1023 - // Same two values for all texels this frame - float randomFromRange1 = float(int(uint(filmGrainColorAndIntensity.x) & (FILM_GRAIN_TEXTURE_SIZE - 1u))); - float randomFromRange2 = float(int(uint(filmGrainColorAndIntensity.y) & (FILM_GRAIN_TEXTURE_SIZE - 1u))); - - // Divide by 1023 to get a number between 0 and 1 - float randomNormalized1 = randomFromRange1 * filmGrainInvSize; - float randomNormalized2 = randomFromRange1 * filmGrainInvSize; - - // Offset by x/y position - float uniqueX = randomNormalized1 + TEXCOORD.x; - float uniqueY = (randomNormalized2 + TEXCOORD.y) * filmGrainHalfSize; - - // Unique each frame and texel. (Is it though?) - float seed = (uniqueX + uniqueY); - - // Generate a random number between 0 and 1; - float randomNumber = bethesdaRandom(seed); -#else - float randomNumber = rand(TEXCOORD.xy - + frac(filmGrainColorAndIntensity.x / 2147483647.f) - + frac(filmGrainColorAndIntensity.y / 2147483647.f) - ); -#endif // FILM_GRAIN_TYPE == 0 - // luminanceShift at -1 is black, 0 unchanged, 1 negative of current texel - float luminanceShift = (randomNumber * 2.f) - 1.f; + float randomNumber, customMultiplier; -#if FILM_GRAIN_TYPE == 0 || FILM_GRAIN_TYPE == 1 - float colorLuma = Luminance(srgbColor); - float customMulitplier = saturate(1.f - colorLuma); // inverseLuma -#elif FILM_GRAIN_TYPE == 2 - float colorLuma = Luminance(srgbColor); - float customMulitplier = (-2.f * (0.5f - abs(saturate(colorLuma) - 0.5f))) // Bias towards center (nothing if black or white) - * 3.333f; // Bump to 10% -#elif FILM_GRAIN_TYPE == 3 - float colorY = Luminance(linearColor); - float midToneStart = 0.022f; // Kodak Gray Scale B (Shadow) - float midToneMid = 0.178f; // Kodak Gray Scale M (Gray) - float midToneEnd = 0.891f; // Kodak Gray Scale A (Highlight) - float midPointScaling; - if (colorY > midToneStart && colorY < midToneEnd) { - if (colorY < midToneMid) { - // Lerp? - midPointScaling = linearNormalization(colorY, midToneStart, midToneMid, 0.f, 1.f); - } else { - // Lerp? - midPointScaling = linearNormalization(colorY, midToneMid, midToneEnd, 1.f, 0.f); - } - } else { - midPointScaling = 0.f; + if (HdrDllPluginConstants.FilmGrainType == 0) + { + const float filmGrainInvSize = 1.f / (FILM_GRAIN_TEXTURE_SIZE - 1u); + const float filmGrainHalfSize = 521.f; //TODO: rename in case we keep this as 521 + // Applying modulus against 1023 will give value between 0 and 1023 + // Same two values for all texels this frame + float randomFromRange1 = float(int(uint(filmGrainColorAndIntensity.x) & (FILM_GRAIN_TEXTURE_SIZE - 1u))); + float randomFromRange2 = float(int(uint(filmGrainColorAndIntensity.y) & (FILM_GRAIN_TEXTURE_SIZE - 1u))); + + // Divide by 1023 to get a number between 0 and 1 + float randomNormalized1 = randomFromRange1 * filmGrainInvSize; + float randomNormalized2 = randomFromRange1 * filmGrainInvSize; + + // Offset by x/y position + float uniqueX = randomNormalized1 + TEXCOORD.x; + float uniqueY = (randomNormalized2 + TEXCOORD.y) * filmGrainHalfSize; + + // Unique each frame and texel. (Is it though?) + float seed = (uniqueX + uniqueY); + + // Generate a random number between 0 and 1; + randomNumber = bethesdaRandom(seed); + + float colorLuma = Luminance(srgbColor); + customMultiplier = saturate(1.f - colorLuma); // inverseLuma } - float customMulitplier = midPointScaling + else + { + randomNumber = rand(TEXCOORD.xy + + frac(filmGrainColorAndIntensity.x / 2147483647.f) + + frac(filmGrainColorAndIntensity.y / 2147483647.f) + ); + +#if IMPROVED_FILM_GRAIN_TYPE == 1 + float colorLuma = Luminance(srgbColor); + customMultiplier = saturate(1.f - colorLuma); // inverseLuma +#elif IMPROVED_FILM_GRAIN_TYPE == 2 + float colorLuma = Luminance(srgbColor); + customMultiplier = (-2.f * (0.5f - abs(saturate(colorLuma) - 0.5f))) // Bias towards center (nothing if black or white) * 3.333f; // Bump to 10% +#elif IMPROVED_FILM_GRAIN_TYPE == 3 + float colorY = Luminance(linearColor); + float midToneStart = 0.022f; // Kodak Gray Scale B (Shadow) + float midToneMid = 0.178f; // Kodak Gray Scale M (Gray) + float midToneEnd = 0.891f; // Kodak Gray Scale A (Highlight) + float midPointScaling; + if (colorY > midToneStart && colorY < midToneEnd) + { + if (colorY < midToneMid) + { + // Lerp? + midPointScaling = linearNormalization(colorY, midToneStart, midToneMid, 0.f, 1.f); + } + else + { + // Lerp? + midPointScaling = linearNormalization(colorY, midToneMid, midToneEnd, 1.f, 0.f); + } + } + else + { + midPointScaling = 0.f; + } + customMultiplier = midPointScaling + * 3.333f; // Bump to 10% +#endif + } + + // luminanceShift at -1 is black, 0 unchanged, 1 negative of current texel + float luminanceShift = (randomNumber * 2.f) - 1.f; -#endif // FILM_GRAIN_TYPE != 2 - float additiveFilmGrain = (luminanceShift * filmGrainColorAndIntensity.z * customMulitplier); + float additiveFilmGrain = (luminanceShift * filmGrainColorAndIntensity.z * customMultiplier); float3 newColor = float3(additiveFilmGrain, additiveFilmGrain,additiveFilmGrain); // Use addition to overlay color on top diff --git a/shaders/PostSharpen/PostSharpen_ps.hlsl b/shaders/PostSharpen/PostSharpen_ps.hlsl index ae7c2a6e..ee2a0aac 100644 --- a/shaders/PostSharpen/PostSharpen_ps.hlsl +++ b/shaders/PostSharpen/PostSharpen_ps.hlsl @@ -47,66 +47,72 @@ struct PSOutput [RootSignature(ShaderRootSignature)] PSOutput PS(PSInput psInput) { -#if 0 // Disable post sharpen - float3 outColor = TonemappedColorTexture.Sample(Sampler0, psInput.TEXCOORD.xy).rgb; -#else - float3 inColor = TonemappedColorTexture.Sample(Sampler0, psInput.TEXCOORD.xy).rgb; - float3 outColor = inColor; - float4 sharpenParams = PcwPostSharpen.params0; - float sharpenIntensity = sharpenParams.z; - if (sharpenIntensity > 0.f && sharpenIntensity != 1.f) + float3 outColor; + + if (HdrDllPluginConstants.PostSharpen == false) // Disable post sharpen { - // Sharpening is best done in linear space, even if Bethesda made in gamma space - if (HdrDllPluginConstants.DisplayMode <= 0) - { - inColor = GAMMA_TO_LINEAR(inColor); - } - float _70 = ((_15_m0[161u].z * psInput.TEXCOORD.x) * sharpenParams.x) + 0.5f; - float _72 = ((_15_m0[161u].w * psInput.TEXCOORD.y) * sharpenParams.y) + 0.5f; - float _76 = frac(_70); - float _77 = frac(_72); - float _87 = ((((((_76 * 2.f) + (-3.f)) * _76) + (-3.f)) * _76) + 5.f) * (1.f / 6.f); - float _89 = _76 * 3.f; - float _98 = ((((((3.f - _89) + _76) * _76) + 3.f) * _76) + 1.f) * (1.f / 6.f); - float _99 = _76 * _76; - float _113 = _77 * _77; - float _114 = _77 * 3.f; - float _125 = ((((((_77 * 2.f) + (-3.f)) * _77) + (-3.f)) * _77) + 5.f) * (1.f / 6.f); - float _136 = ((((((3.f - _114) + _77) * _77) + 3.f) * _77) + 1.f) * (1.f / 6.f); - float _139 = floor(_70) + (-0.5f); - float _142 = floor(_72) + (-0.5f); - float _144 = (_139 + ((((( _99 * ( _89 + (-6.f))) + 4.f) * (1.f / 6.f)) / _87) + (-1.f))) / _15_m0[161u].z; - float _145 = (_142 + (((((_113 * (_114 + (-6.f))) + 4.f) * (1.f / 6.f)) / _125) + (-1.f))) / _15_m0[161u].w; - float _147 = (_139 + (((( _99 * (1.f / 6.f)) * _76) / _98) + 1.f)) / _15_m0[161u].z; - float _149 = (_142 + ((((_113 * (1.f / 6.f)) * _77) / _136) + 1.f)) / _15_m0[161u].w; - float3 _152 = TonemappedColorTexture.Sample(Sampler1, float2(_144, _145)); - float3 _157 = TonemappedColorTexture.Sample(Sampler1, float2(_147, _145)); - float3 _162 = TonemappedColorTexture.Sample(Sampler1, float2(_144, _149)); - float3 _167 = TonemappedColorTexture.Sample(Sampler1, float2(_147, _149)); - if (HdrDllPluginConstants.DisplayMode <= 0) + outColor = TonemappedColorTexture.Sample(Sampler0, psInput.TEXCOORD.xy).rgb; + } + else + { + float3 inColor = TonemappedColorTexture.Sample(Sampler0, psInput.TEXCOORD.xy).rgb; + outColor = inColor; + float4 sharpenParams = PcwPostSharpen.params0; + float sharpenIntensity = sharpenParams.z; + if (sharpenIntensity > 0.f && sharpenIntensity != 1.f) { - _152 = GAMMA_TO_LINEAR(_152); - _157 = GAMMA_TO_LINEAR(_157); - _162 = GAMMA_TO_LINEAR(_162); - _167 = GAMMA_TO_LINEAR(_167); - } - float3 unsharpenedColor = (((_167 * _98) + (_162 * _87)) * _136) + (((_157 * _98) + (_152 * _87)) * _125); - // Controls the amount of a custom blurred bilinear filtering vs HW bilinear (which is blurry if there's upscaling), - // by lerping beyond 1 in the opposite direction of the blurred image, we apply sharpening. - outColor = lerp(unsharpenedColor, inColor, sharpenIntensity); + // Sharpening is best done in linear space, even if Bethesda made in gamma space + if (HdrDllPluginConstants.DisplayMode <= 0) + { + inColor = GAMMA_TO_LINEAR(inColor); + } + float _70 = ((_15_m0[161u].z * psInput.TEXCOORD.x) * sharpenParams.x) + 0.5f; + float _72 = ((_15_m0[161u].w * psInput.TEXCOORD.y) * sharpenParams.y) + 0.5f; + float _76 = frac(_70); + float _77 = frac(_72); + float _87 = ((((((_76 * 2.f) + (-3.f)) * _76) + (-3.f)) * _76) + 5.f) * (1.f / 6.f); + float _89 = _76 * 3.f; + float _98 = ((((((3.f - _89) + _76) * _76) + 3.f) * _76) + 1.f) * (1.f / 6.f); + float _99 = _76 * _76; + float _113 = _77 * _77; + float _114 = _77 * 3.f; + float _125 = ((((((_77 * 2.f) + (-3.f)) * _77) + (-3.f)) * _77) + 5.f) * (1.f / 6.f); + float _136 = ((((((3.f - _114) + _77) * _77) + 3.f) * _77) + 1.f) * (1.f / 6.f); + float _139 = floor(_70) + (-0.5f); + float _142 = floor(_72) + (-0.5f); + float _144 = (_139 + (((((_99 * (_89 + (-6.f))) + 4.f) * (1.f / 6.f)) / _87) + (-1.f))) / _15_m0[161u].z; + float _145 = (_142 + (((((_113 * (_114 + (-6.f))) + 4.f) * (1.f / 6.f)) / _125) + (-1.f))) / _15_m0[161u].w; + float _147 = (_139 + ((((_99 * (1.f / 6.f)) * _76) / _98) + 1.f)) / _15_m0[161u].z; + float _149 = (_142 + ((((_113 * (1.f / 6.f)) * _77) / _136) + 1.f)) / _15_m0[161u].w; + float3 _152 = TonemappedColorTexture.Sample(Sampler1, float2(_144, _145)); + float3 _157 = TonemappedColorTexture.Sample(Sampler1, float2(_147, _145)); + float3 _162 = TonemappedColorTexture.Sample(Sampler1, float2(_144, _149)); + float3 _167 = TonemappedColorTexture.Sample(Sampler1, float2(_147, _149)); + if (HdrDllPluginConstants.DisplayMode <= 0) + { + _152 = GAMMA_TO_LINEAR(_152); + _157 = GAMMA_TO_LINEAR(_157); + _162 = GAMMA_TO_LINEAR(_162); + _167 = GAMMA_TO_LINEAR(_167); + } + float3 unsharpenedColor = (((_167 * _98) + (_162 * _87)) * _136) + (((_157 * _98) + (_152 * _87)) * _125); + // Controls the amount of a custom blurred bilinear filtering vs HW bilinear (which is blurry if there's upscaling), + // by lerping beyond 1 in the opposite direction of the blurred image, we apply sharpening. + outColor = lerp(unsharpenedColor, inColor, sharpenIntensity); #if CLAMP_INPUT_OUTPUT - // Note: the sharpening multiplication above generates arbitrary colors and shifts the hue to be invalid - if (Luminance(outColor) < 0.f) - { - outColor = 0.f; - } + // Note: the sharpening multiplication above generates arbitrary colors and shifts the hue to be invalid + if (Luminance(outColor) < 0.f) + { + outColor = 0.f; + } #endif - if (HdrDllPluginConstants.DisplayMode <= 0) - { - outColor = LINEAR_TO_GAMMA(outColor); + if (HdrDllPluginConstants.DisplayMode <= 0) + { + outColor = LINEAR_TO_GAMMA(outColor); + } } } -#endif + PSOutput psOutput; psOutput.SV_Target.rgb = outColor; diff --git a/shaders/compile_all_shaders.ps1 b/shaders/compile_all_shaders.ps1 index a1ef790a..9acb76d5 100644 --- a/shaders/compile_all_shaders.ps1 +++ b/shaders/compile_all_shaders.ps1 @@ -2,6 +2,7 @@ $ErrorActionPreference = "Stop" $ShaderOutputDirectory = Get-Content "${PSScriptRoot}\target_folder.txt" +$DistDirectory = "${PSScriptRoot}\..\Plugin\dist\Data\shadersfx\" $ShaderOutputEmbedPDB = $false @@ -118,6 +119,11 @@ function Compile-Shader { Run-DXC -Arguments "-dumpbin `"${stagedBinPath}`" -extractrootsignature -Fo `"${stagedSigPath}`"" Run-DXC -Arguments "-dumpbin `"${stagedBinPath}`" -Qstrip_rootsignature -Fo `"${stagedBinPath}`"" + # Copy the resulting bins to the dist directory. + New-Item -Force -ItemType Directory -Path "${DistDirectory}\${TechniqueName}" | Out-Null + Copy-Item -Force -Path $stagedBinPath -Destination "${DistDirectory}\${TechniqueName}\${outputBinName}" + Copy-Item -Force -Path $stagedSigPath -Destination "${DistDirectory}\${TechniqueName}\${outputSigName}" + # Move the resulting bins to the game directory. Move-Item is to avoid partial reads when live shader editing # is enabled. New-Item -Force -ItemType Directory -Path "${ShaderOutputDirectory}\${TechniqueName}" | Out-Null diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 82a4836e..22b8bcb9 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -34,8 +34,8 @@ struct StructHdrDllPluginConstants float HDRSecondaryContrast; // 1 is neutral float LUTCorrectionStrength; // 1 is full strength float ColorGradingStrength; // 1 is full strength - uint FilmGrainType; // 0 is default - uint PostSharpening; // true is default + uint FilmGrainType; // 1 is default + uint PostSharpen; // true is default uint IsAtEndOfFrame; float DevSetting01; // 0-1 variable for development float DevSetting02; // 0-1 variable for development From 29aad6b0c5dabc47a6df7bfc3e280cba8a98ac50 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Mon, 9 Oct 2023 01:18:44 +0300 Subject: [PATCH 124/270] Comment on UI --- shaders/ScaleformComposite/ScaleformComposite_ps.hlsl | 1 + 1 file changed, 1 insertion(+) diff --git a/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl b/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl index 22a9f5bd..37e06db6 100644 --- a/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl +++ b/shaders/ScaleformComposite/ScaleformComposite_ps.hlsl @@ -4,6 +4,7 @@ // Hack: change the alpha value at which the UI blends in in HDR, to increase readability. Range is 0 to 1, with 1 having no effect. // We found the best value empirically and it seems to match gamma 2.2. +// Note that this make the look more towards SDR sRGB gamma blends when the background is white and the UI is dark, but in the opposite case, it will have the inverse effect (or something like that). #define HDR_UI_BLEND_POW (1.f / 2.2f) struct PushConstantWrapper_ScaleformCompositeLayout From 6d3a3f5c0cebd42f59f3d583feb6130b2ac95c86 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Sun, 8 Oct 2023 18:45:47 -0400 Subject: [PATCH 125/270] perf(luts): compute gain once --- .../ColorGradingMerge_cs.hlsl | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl index 923f0c21..5c07f590 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl @@ -177,14 +177,14 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b // While it unclear how exactly the floor was raised, remove the tint to floor // the values to 0. This will remove the haze and tint giving a fuller chroma // Sample and hold targetChroma - const float3 reduceFactor = linearNormalization( - neutralLUTColor, - 0.f, - 1.f, - 1.f / (1.f - analysis.black), - 1.f); + const float3 colorGain = linearNormalization( + neutralLUTColor, + 0.f, + 1.f, + 1.f + analysis.black, + 1.f); - const float3 detintedColor = max(0.f, 1.f-((1.f - color) * reduceFactor)); + const float3 detintedColor = max(0.f, 1.f-((1.f - color) * colorGain)); // The saturation multiplier is restricted to HDR as it easily goes beyond Rec.709 const float saturation = linearNormalization(HdrDllPluginConstants.HDRLUTCorrectionSaturation, 0.f, 2.f, 0.5f, 1.5f); @@ -193,14 +193,7 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b // Adjust the value back to recreate a smooth Y gradient since 0 is floored // Sample and hold targetL - const float3 increaseFactor = linearNormalization( - neutralLUTColor, - 0.f, - 1.f, - 1.f + analysis.black, - 1.f); - - const float3 retintedColor = detintedColor * increaseFactor; + const float3 retintedColor = detintedColor * colorGain; float targetL = linear_srgb_to_oklch(retintedColor)[0]; From 1920b2012ebae40e1a3ceb51c30f5feaefbe2943 Mon Sep 17 00:00:00 2001 From: ersh1 <16157893+ersh1@users.noreply.github.com> Date: Mon, 9 Oct 2023 01:26:07 +0200 Subject: [PATCH 126/270] fixed dev settings max values --- Plugin/src/Settings.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Plugin/src/Settings.h b/Plugin/src/Settings.h index 0cb70391..0a9d3186 100644 --- a/Plugin/src/Settings.h +++ b/Plugin/src/Settings.h @@ -174,11 +174,11 @@ namespace Settings true, }; #if 1 - Slider DevSetting01{ SettingID::kDevSetting01, "DevSetting01", "Development setting", { "DevSetting01", "Dev" }, 0.f, 100.f, 0.f }; - Slider DevSetting02{ SettingID::kDevSetting02, "DevSetting02", "Development setting", { "DevSetting02", "Dev" }, 0.f, 100.f, 0.f }; - Slider DevSetting03{ SettingID::kDevSetting03, "DevSetting03", "Development setting", { "DevSetting03", "Dev" }, 0.f, 100.f, 0.f }; - Slider DevSetting04{ SettingID::kDevSetting04, "DevSetting04", "Development setting", { "DevSetting04", "Dev" }, 0.f, 100.f, 0.f }; - Slider DevSetting05{ SettingID::kDevSetting05, "DevSetting05", "Development setting", { "DevSetting05", "Dev" }, 0.f, 100.f, 0.f }; + Slider DevSetting01{ SettingID::kDevSetting01, "DevSetting01", "Development setting", { "DevSetting01", "Dev" }, 0.f, 0.f, 100.f }; + Slider DevSetting02{ SettingID::kDevSetting02, "DevSetting02", "Development setting", { "DevSetting02", "Dev" }, 0.f, 0.f, 100.f }; + Slider DevSetting03{ SettingID::kDevSetting03, "DevSetting03", "Development setting", { "DevSetting03", "Dev" }, 0.f, 0.f, 100.f }; + Slider DevSetting04{ SettingID::kDevSetting04, "DevSetting04", "Development setting", { "DevSetting04", "Dev" }, 0.f, 0.f, 100.f }; + Slider DevSetting05{ SettingID::kDevSetting05, "DevSetting05", "Development setting", { "DevSetting05", "Dev" }, 0.f, 0.f, 100.f }; #endif String RenderTargetsToUpgrade{ "RenderTargetsToUpgrade", "RenderTargets" }; From e80b6a8fe8f09f47d5c4ee86ad63752f77aaa679 Mon Sep 17 00:00:00 2001 From: ersh1 <16157893+ersh1@users.noreply.github.com> Date: Mon, 9 Oct 2023 02:39:59 +0200 Subject: [PATCH 127/270] Revert "perf(luts): compute gain once" This reverts commit 6d3a3f5c0cebd42f59f3d583feb6130b2ac95c86. --- .../ColorGradingMerge_cs.hlsl | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl index 5c07f590..923f0c21 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl @@ -177,14 +177,14 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b // While it unclear how exactly the floor was raised, remove the tint to floor // the values to 0. This will remove the haze and tint giving a fuller chroma // Sample and hold targetChroma - const float3 colorGain = linearNormalization( - neutralLUTColor, - 0.f, - 1.f, - 1.f + analysis.black, - 1.f); + const float3 reduceFactor = linearNormalization( + neutralLUTColor, + 0.f, + 1.f, + 1.f / (1.f - analysis.black), + 1.f); - const float3 detintedColor = max(0.f, 1.f-((1.f - color) * colorGain)); + const float3 detintedColor = max(0.f, 1.f-((1.f - color) * reduceFactor)); // The saturation multiplier is restricted to HDR as it easily goes beyond Rec.709 const float saturation = linearNormalization(HdrDllPluginConstants.HDRLUTCorrectionSaturation, 0.f, 2.f, 0.5f, 1.5f); @@ -193,7 +193,14 @@ float3 PatchLUTColor(Texture2D LUT, uint3 UVW, float3 neutralLUTColor, b // Adjust the value back to recreate a smooth Y gradient since 0 is floored // Sample and hold targetL - const float3 retintedColor = detintedColor * colorGain; + const float3 increaseFactor = linearNormalization( + neutralLUTColor, + 0.f, + 1.f, + 1.f + analysis.black, + 1.f); + + const float3 retintedColor = detintedColor * increaseFactor; float targetL = linear_srgb_to_oklch(retintedColor)[0]; From 5ae94a1bbca4e125006dc45aef87fae1738292b0 Mon Sep 17 00:00:00 2001 From: ersh1 <16157893+ersh1@users.noreply.github.com> Date: Mon, 9 Oct 2023 06:09:32 +0200 Subject: [PATCH 128/270] Added a setting that skips lut correction while in certain menus Added delta time to shader constants --- Plugin/dist/NativeHDR.toml | 24 ++-- Plugin/src/Hooks.cpp | 113 +++++++++++------- Plugin/src/Hooks.h | 2 - Plugin/src/Offsets.h | 12 ++ Plugin/src/RE/BSFixedString.h | 37 ++++++ Plugin/src/Settings.cpp | 28 ++++- Plugin/src/Settings.h | 20 +++- Plugin/src/Utils.cpp | 28 +++++ Plugin/src/Utils.h | 2 + shaders/BinkMovie/RootSignature.hlsl | 2 +- shaders/ColorGradingMerge/RootSignature.hlsl | 2 +- .../RootSignature.hlsl | 2 +- shaders/Copy/RootSignature.hlsl | 2 +- shaders/FilmGrain/RootSignature.hlsl | 2 +- shaders/PostSharpen/RootSignature.hlsl | 2 +- shaders/ScaleformComposite/RootSignature.hlsl | 2 +- shaders/shared.hlsl | 3 +- 17 files changed, 212 insertions(+), 71 deletions(-) diff --git a/Plugin/dist/NativeHDR.toml b/Plugin/dist/NativeHDR.toml index 0d2e3cbe..17003e35 100644 --- a/Plugin/dist/NativeHDR.toml +++ b/Plugin/dist/NativeHDR.toml @@ -1,20 +1,24 @@ +[Dev] +DevSetting01 = 0.0 +DevSetting02 = 0.0 +DevSetting03 = 0.0 +DevSetting04 = 0.0 +DevSetting05 = 0.0 + [HDR] -PeakBrightness = 1000.0 +Contrast = 50.0 GamePaperWhite = 203.0 -UIPaperWhite = 203.0 +PeakBrightness = 1000.0 Saturation = 50.0 -Contrast = 50.0 +UIPaperWhite = 203.0 [Main] -DisplayMode = 2 ColorGradingStrength = 100.0 +DisplayMode = 0 +FilmGrainType = 1 LUTCorrectionStrength = 100.0 -FilmGrainType = 0 -PostSharpening = true - -[Dev] -DevSetting01 = 0.0 -DevSetting02 = 0.0 +PostSharpen = true +VanillaMenuLUTs = true [RenderTargets] RenderTargetsToUpgrade = [ diff --git a/Plugin/src/Hooks.cpp b/Plugin/src/Hooks.cpp index d2c0da0f..84d47a3c 100644 --- a/Plugin/src/Hooks.cpp +++ b/Plugin/src/Hooks.cpp @@ -117,6 +117,7 @@ namespace Hooks CreateSliderSetting(a_settingList, settings->Contrast, settings->IsHDREnabled()); CreateSliderSetting(a_settingList, settings->LUTCorrectionStrength, true); CreateSliderSetting(a_settingList, settings->ColorGradingStrength, true); + CreateCheckboxSetting(a_settingList, settings->VanillaMenuLUTs, true); CreateStepperSetting(a_settingList, settings->FilmGrainType, true); CreateCheckboxSetting(a_settingList, settings->PostSharpen, true); @@ -128,34 +129,13 @@ namespace Hooks const auto technique = *reinterpret_cast(reinterpret_cast(a2) + 0x8); const auto techniqueId = *reinterpret_cast(technique + 0x78); - auto uploadRootConstants = [&](uint32_t RootParameterIndex, bool Compute) { + auto uploadRootConstants = [&](const Settings::ShaderConstants& a_shaderConstants, uint32_t a_rootParameterIndex, bool a_bCompute) { auto commandList = *reinterpret_cast(reinterpret_cast(a1) + 0x10); - const auto settings = Settings::Main::GetSingleton(); - - // This can be any data type, even a struct. It just has to match StructHdrDllPluginConstants in HLSL. - const Settings::ShaderConstants data{ - static_cast(*settings->DisplayMode.value), - static_cast(*settings->PeakBrightness.value), - static_cast(*settings->GamePaperWhite.value), - static_cast(*settings->UIPaperWhite.value), - static_cast(*settings->Saturation.value * 0.02f), // 0-100 to 0-2 - static_cast(*settings->Contrast.value * 0.02f), // 0-100 to 0-2 - static_cast(*settings->LUTCorrectionStrength.value * 0.01f), // 0-100 to 0-1 - static_cast(*settings->ColorGradingStrength.value * 0.01f), // 0-100 to 0-1 - static_cast(*settings->FilmGrainType.value), - static_cast(*settings->PostSharpen.value), - static_cast(bIsAtEndOfFrame.load()), - static_cast(*settings->DevSetting01.value * 0.01f), // 0-100 to 0-1 - static_cast(*settings->DevSetting02.value * 0.01f), // 0-100 to 0-1 - static_cast(*settings->DevSetting03.value * 0.01f), // 0-100 to 0-1 - static_cast(*settings->DevSetting04.value * 0.01f), // 0-100 to 0-1 - static_cast(*settings->DevSetting05.value * 0.01f) // 0-100 to 0-1 - }; - - if (!Compute) - commandList->SetGraphicsRoot32BitConstants(RootParameterIndex, Settings::shaderConstantsSize, &data, 0); + + if (!a_bCompute) + commandList->SetGraphicsRoot32BitConstants(a_rootParameterIndex, Settings::shaderConstantsSize, &a_shaderConstants, 0); else - commandList->SetComputeRoot32BitConstants(RootParameterIndex, Settings::shaderConstantsSize, &data, 0); + commandList->SetComputeRoot32BitConstants(a_rootParameterIndex, Settings::shaderConstantsSize, &a_shaderConstants, 0); }; // Note: The following switch statement may be called several thousand times per frame. Additionally, it'll be called from multiple @@ -170,39 +150,76 @@ namespace Hooks //case 0x800FF1A: case 0xE00FF1A: case 0xF00FF1A: - uploadRootConstants(14, false); // HDRComposite - break; + { + Settings::ShaderConstants shaderConstants; + Settings::Main::GetSingleton()->GetShaderConstants(shaderConstants); + uploadRootConstants(shaderConstants, 14, false); // HDRComposite + break; + } case 0x400FF59: - //case 0x2000FF59: - uploadRootConstants(2, false); // Copy - break; + //case 0x2000FF59: + { + Settings::ShaderConstants shaderConstants; + Settings::Main::GetSingleton()->GetShaderConstants(shaderConstants); + uploadRootConstants(shaderConstants, 2, false); // Copy + break; + } case 0xFF75: - uploadRootConstants(2, false); // FilmGrain - break; + { + Settings::ShaderConstants shaderConstants; + Settings::Main::GetSingleton()->GetShaderConstants(shaderConstants); + uploadRootConstants(shaderConstants, 2, false); // FilmGrain + break; + } case 0xFF81: - uploadRootConstants(7, true); // ColorGradingMerge - break; + { + Settings::ShaderConstants shaderConstants; + const auto settings = Settings::Main::GetSingleton(); + settings->GetShaderConstants(shaderConstants); + if (*settings->VanillaMenuLUTs.value && !Utils::ShouldCorrectLUTs()) { + shaderConstants.LUTCorrectionStrength = 0.f; + shaderConstants.ColorGradingStrength = 1.f; + } + uploadRootConstants(shaderConstants, 7, true); // ColorGradingMerge + break; + } //case 0xFF94: case 0x100FF94: case 0x300FF94: - uploadRootConstants(14, true); // ContrastAdaptiveSharpening - break; + { + Settings::ShaderConstants shaderConstants; + Settings::Main::GetSingleton()->GetShaderConstants(shaderConstants); + uploadRootConstants(shaderConstants, 14, true); // ContrastAdaptiveSharpening + break; + } case 0xFF9A: - uploadRootConstants(14, false); // PostSharpen - break; + { + Settings::ShaderConstants shaderConstants; + Settings::Main::GetSingleton()->GetShaderConstants(shaderConstants); + uploadRootConstants(shaderConstants, 14, false); // PostSharpen + break; + } case 0xFFAA: - uploadRootConstants(2, false); // ScaleformComposite - break; + { + Settings::ShaderConstants shaderConstants; + Settings::Main::GetSingleton()->GetShaderConstants(shaderConstants); + uploadRootConstants(shaderConstants, 2, false); // ScaleformComposite + break; + } case 0xFFAB: - uploadRootConstants(1, false); // BinkMovie - break; + { + Settings::ShaderConstants shaderConstants; + Settings::Main::GetSingleton()->GetShaderConstants(shaderConstants); + uploadRootConstants(shaderConstants, 1, false); // BinkMovie + break; + } } } @@ -269,6 +286,12 @@ namespace Hooks } }; + switch (a_eventData.m_SettingID) { + case static_cast(Settings::SettingID::kVanillaMenuLUTs): + HandleSetting(settings->VanillaMenuLUTs); + break; + } + switch (a_eventData.m_SettingID) { case static_cast(Settings::SettingID::kPostSharpen): HandleSetting(settings->PostSharpen); @@ -390,14 +413,14 @@ namespace Hooks void Hooks::Hook_EndOfFrame(void* a1, void* a2, const char* a3) { - bIsAtEndOfFrame.store(true); + Settings::Main::GetSingleton()->SetAtEndOfFrame(true); _EndOfFrame(a1, a2, a3); } void Hooks::Hook_PostEndOfFrame(void* a1) { _PostEndOfFrame(a1); - bIsAtEndOfFrame.store(false); + Settings::Main::GetSingleton()->SetAtEndOfFrame(false); } void Install() diff --git a/Plugin/src/Hooks.h b/Plugin/src/Hooks.h index bbdba81c..9cacf6db 100644 --- a/Plugin/src/Hooks.h +++ b/Plugin/src/Hooks.h @@ -96,8 +96,6 @@ namespace Hooks static void Hook_PostEndOfFrame(void* a1); static inline std::add_pointer_t _PostEndOfFrame; - - static inline std::atomic_bool bIsAtEndOfFrame = false; }; void Install(); diff --git a/Plugin/src/Offsets.h b/Plugin/src/Offsets.h index 5f3cdd0a..8d675f84 100644 --- a/Plugin/src/Offsets.h +++ b/Plugin/src/Offsets.h @@ -1,7 +1,10 @@ #pragma once +#include "RE/BSFixedString.h" #include "RE/Buffers.h" #include "RE/MessageBoxData.h" +#include "sfse/GameUI.h" + class Offsets { public: @@ -18,6 +21,11 @@ class Offsets using tPhotoMode_ToggleUI = bool (*)(uintptr_t a1); static inline tPhotoMode_ToggleUI PhotoMode_ToggleUI = nullptr; + using tUI_IsMenuOpen = bool (*)(UI* a_ui, const RE::BSFixedString& a_menuName); + static inline tUI_IsMenuOpen UI_IsMenuOpen = nullptr; + + static inline float* g_deltaTimeRealTime = nullptr; + //test static inline uintptr_t* unkToggleVsyncArg1Ptr = nullptr; using tToggleVsync = void (*)(void* a1, bool a_bEnable); @@ -36,5 +44,9 @@ class Offsets MessageMenuManagerPtr = reinterpret_cast(dku::Hook::IDToAbs(878772)); ShowMessageBox = reinterpret_cast(dku::Hook::IDToAbs(167094)); PhotoMode_ToggleUI = reinterpret_cast(dku::Hook::IDToAbs(139734)); + + UI_IsMenuOpen = reinterpret_cast(dku::Hook::IDToAbs(187049)); + + g_deltaTimeRealTime = reinterpret_cast(dku::Hook::IDToAbs(871870)); } }; diff --git a/Plugin/src/RE/BSFixedString.h b/Plugin/src/RE/BSFixedString.h index 1887bbbb..14a6ce85 100644 --- a/Plugin/src/RE/BSFixedString.h +++ b/Plugin/src/RE/BSFixedString.h @@ -2,6 +2,43 @@ namespace RE { + class BSFixedString + { + private: + void* m_EntryPoolData = nullptr; + + public: + BSFixedString() = default; + + BSFixedString(const char* String) + { + auto addr = dku::Hook::IDToAbs(198219); + auto func = reinterpret_cast(addr); + func(this, String); + } + + BSFixedString(const BSFixedString& Other) = delete; + + ~BSFixedString() + { + auto addr = dku::Hook::IDToAbs(36754); + auto func = reinterpret_cast(addr); + func(this); + } + + BSFixedString& operator=(const BSFixedString& Other) = delete; + + bool operator==(const BSFixedString& Other) const + { + return m_EntryPoolData == Other.m_EntryPoolData; + } + + bool operator!=(const BSFixedString& Other) const + { + return m_EntryPoolData != Other.m_EntryPoolData; + } + }; + class BSFixedStringCS { private: diff --git a/Plugin/src/Settings.cpp b/Plugin/src/Settings.cpp index 9400f148..a4d191c6 100644 --- a/Plugin/src/Settings.cpp +++ b/Plugin/src/Settings.cpp @@ -52,6 +52,27 @@ namespace Settings } } + void Main::GetShaderConstants(ShaderConstants& a_outShaderConstants) const + { + a_outShaderConstants.DisplayMode = static_cast(DisplayMode.value.get_data()); + a_outShaderConstants.PeakBrightness = static_cast(PeakBrightness.value.get_data()); + a_outShaderConstants.GamePaperWhite = static_cast(GamePaperWhite.value.get_data()); + a_outShaderConstants.UIPaperWhite = static_cast(UIPaperWhite.value.get_data()); + a_outShaderConstants.Saturation = static_cast(Saturation.value.get_data() * 0.02f); // 0-100 to 0-2 + a_outShaderConstants.Contrast = static_cast(Contrast.value.get_data() * 0.02f); // 0-100 to 0-2 + a_outShaderConstants.LUTCorrectionStrength = static_cast(LUTCorrectionStrength.value.get_data() * 0.01f); // 0-100 to 0-1 + a_outShaderConstants.ColorGradingStrength = static_cast(ColorGradingStrength.value.get_data() * 0.01f); // 0-100 to 0-1 + a_outShaderConstants.FilmGrainType = static_cast(FilmGrainType.value.get_data()); + a_outShaderConstants.PostSharpen = static_cast(PostSharpen.value.get_data()); + a_outShaderConstants.bIsAtEndOfFrame = static_cast(bIsAtEndOfFrame.load()); + a_outShaderConstants.DeltaTime = *Offsets::g_deltaTimeRealTime; + a_outShaderConstants.DevSetting01 = static_cast(DevSetting01.value.get_data() * 0.01f); // 0-100 to 0-1 + a_outShaderConstants.DevSetting02 = static_cast(DevSetting02.value.get_data() * 0.01f); // 0-100 to 0-1 + a_outShaderConstants.DevSetting03 = static_cast(DevSetting03.value.get_data() * 0.01f); // 0-100 to 0-1 + a_outShaderConstants.DevSetting04 = static_cast(DevSetting04.value.get_data() * 0.01f); // 0-100 to 0-1 + a_outShaderConstants.DevSetting05 = static_cast(DevSetting05.value.get_data() * 0.01f); // 0-100 to 0-1 + } + void Main::Load() noexcept { static std::once_flag ConfigInit; @@ -64,6 +85,7 @@ namespace Settings config.Bind(Contrast.value, Contrast.defaultValue); config.Bind(LUTCorrectionStrength.value, LUTCorrectionStrength.defaultValue); config.Bind(ColorGradingStrength.value, ColorGradingStrength.defaultValue); + config.Bind(VanillaMenuLUTs.value, VanillaMenuLUTs.defaultValue); config.Bind(FilmGrainType.value, FilmGrainType.defaultValue); config.Bind(PostSharpen.value, PostSharpen.defaultValue); config.Bind(DevSetting01.value, DevSetting01.defaultValue); @@ -83,10 +105,8 @@ namespace Settings "TAA_idTech7HistoryColorTarget", "EnvBRDF", "ImageSpaceBufferR10G10B10A2" -#if 0 - ,'NativeResolutionColorBuffer01', - 'ColorBuffer01' -#endif + //"NativeResolutionColorBuffer01", // issues on AMD + //"ColorBuffer01" ); }); diff --git a/Plugin/src/Settings.h b/Plugin/src/Settings.h index 0a9d3186..ce2f70b9 100644 --- a/Plugin/src/Settings.h +++ b/Plugin/src/Settings.h @@ -23,6 +23,7 @@ namespace Settings kHDR_Contrast, kLUTCorrectionStrength, kColorGradingStrength, + kVanillaMenuLUTs, kFilmGrainType, kPostSharpen, @@ -68,6 +69,7 @@ namespace Settings void SetValueFromSlider(float a_percentage); }; + // Has to match StructHdrDllPluginConstants in HLSL. // Bools are set as uint to avoid padding inconsistencies between c++ and hlsl. struct ShaderConstants { @@ -82,18 +84,19 @@ namespace Settings uint32_t FilmGrainType; uint32_t PostSharpen; uint32_t bIsAtEndOfFrame; + float DeltaTime; float DevSetting01; float DevSetting02; float DevSetting03; float DevSetting04; float DevSetting05; }; - static inline uint32_t shaderConstantsSize = 14; + static inline uint32_t shaderConstantsSize = 17; class Main : public DKUtil::model::Singleton

    { public: - Stepper DisplayMode{ SettingID::kDisplayMode, "Display Mode", "Sets the game's display mode between SDR (Gamma 2.2 Rec.709), HDR10 BT.2020 PQ, or HDR scRGB.\n\nHDR scRGB offers the highest quality but is not compatible with technologies like DLSS Frame Generation", { "DisplayMode", "Main" }, 0, { "SDR", "HDR10", "HDR scRGB" } }; + Stepper DisplayMode{ SettingID::kDisplayMode, "Display Mode", "Sets the game's display mode between SDR (Gamma 2.2 Rec.709), HDR10 BT.2020 PQ, or HDR scRGB.\n\nHDR scRGB offers the highest quality but is not compatible with technologies like DLSS Frame Generation.", { "DisplayMode", "Main" }, 0, { "SDR", "HDR10", "HDR scRGB" } }; Slider PeakBrightness{ SettingID::kHDR_PeakBrightness, @@ -158,6 +161,13 @@ namespace Settings 0.f, 100.f }; + Checkbox VanillaMenuLUTs{ + SettingID::kVanillaMenuLUTs, + "Vanilla Menu LUTs", + "When enabled, menu LUTs will be unaffected by the \"LUT Correction Strength\" and \"Color Grading Strength\" settings.", + { "VanillaMenuLUTs", "Main" }, + true, + }; Stepper FilmGrainType{ SettingID::kFilmGrainType, "Film Grain Type", @@ -184,14 +194,19 @@ namespace Settings bool IsHDREnabled() const; + void SetAtEndOfFrame(bool a_bIsAtEndOfFrame) { bIsAtEndOfFrame.store(a_bIsAtEndOfFrame); } + RE::BS_DXGI_FORMAT GetDisplayModeFormat() const; DXGI_COLOR_SPACE_TYPE GetDisplayModeColorSpaceType() const; + void GetShaderConstants(ShaderConstants& a_outShaderConstants) const; + void Load() noexcept; void Save() noexcept; private: TomlConfig config = COMPILE_PROXY("NativeHDR.toml"sv); + std::atomic_bool bIsAtEndOfFrame = false; }; static inline RE::BGSSwapChainObject* swapChainObject = nullptr; @@ -250,6 +265,7 @@ namespace Settings DrawReshadeSlider(settings, settings->Contrast); DrawReshadeSlider(settings, settings->LUTCorrectionStrength); DrawReshadeSlider(settings, settings->ColorGradingStrength); + DrawReshadeCheckbox(settings, settings->VanillaMenuLUTs); DrawReshadeStepper(settings, settings->FilmGrainType); DrawReshadeCheckbox(settings, settings->PostSharpen); #if 1 diff --git a/Plugin/src/Utils.cpp b/Plugin/src/Utils.cpp index 159cff14..a38905de 100644 --- a/Plugin/src/Utils.cpp +++ b/Plugin/src/Utils.cpp @@ -182,4 +182,32 @@ namespace Utils const auto buffer = (*Offsets::bufferArray)[static_cast(a_buffer)]; return buffer->format; } + + bool ShouldCorrectLUTs() + { + const auto ui = UI::GetSingleton(); + // make sure we do indeed correct luts in these menus, even though DataMenu is in the menu stack + if (Offsets::UI_IsMenuOpen(ui, "GalaxyStarMapMenu")) { + return true; + } + if (Offsets::UI_IsMenuOpen(ui, "SpaceshipEditorMenu")) { + return true; + } + + // don't correct luts while we're in menus that have a LUT + if (Offsets::UI_IsMenuOpen(ui, "DataMenu")) { + return false; + } + if (Offsets::UI_IsMenuOpen(ui, "InventoryMenu")) { + return false; + } + if (Offsets::UI_IsMenuOpen(ui, "ContainerMenu")) { + return false; + } + if (Offsets::UI_IsMenuOpen(ui, "BarterMenu")) { + return false; + } + + return true; + } } diff --git a/Plugin/src/Utils.h b/Plugin/src/Utils.h index 326cfc64..22d1e5b0 100644 --- a/Plugin/src/Utils.h +++ b/Plugin/src/Utils.h @@ -12,4 +12,6 @@ namespace Utils void SetBufferFormat(RE::BufferDefinition* a_buffer, RE::BS_DXGI_FORMAT a_format); void SetBufferFormat(RE::Buffers a_buffer, RE::BS_DXGI_FORMAT a_format); RE::BS_DXGI_FORMAT GetBufferFormat(RE::Buffers a_buffer); + + bool ShouldCorrectLUTs(); } diff --git a/shaders/BinkMovie/RootSignature.hlsl b/shaders/BinkMovie/RootSignature.hlsl index 53132a95..58e8953e 100644 --- a/shaders/BinkMovie/RootSignature.hlsl +++ b/shaders/BinkMovie/RootSignature.hlsl @@ -7,5 +7,5 @@ "SRV(t2, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=16, b3), " \ + "RootConstants(num32BitConstants=17, b3), " \ "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_LINEAR, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " diff --git a/shaders/ColorGradingMerge/RootSignature.hlsl b/shaders/ColorGradingMerge/RootSignature.hlsl index 35683874..c7db5641 100644 --- a/shaders/ColorGradingMerge/RootSignature.hlsl +++ b/shaders/ColorGradingMerge/RootSignature.hlsl @@ -53,7 +53,7 @@ "UAV(u0, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=16, b3), " \ + "RootConstants(num32BitConstants=17, b3), " \ "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ diff --git a/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl b/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl index 1bfb9fd7..f972248f 100644 --- a/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl +++ b/shaders/ContrastAdaptiveSharpening/RootSignature.hlsl @@ -68,7 +68,7 @@ "), " \ \ "CBV(b0, space=8), " \ - "RootConstants(num32BitConstants=16, b3), " \ + "RootConstants(num32BitConstants=17, b3), " \ "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ diff --git a/shaders/Copy/RootSignature.hlsl b/shaders/Copy/RootSignature.hlsl index 9c4cb6fe..a31aa5f9 100644 --- a/shaders/Copy/RootSignature.hlsl +++ b/shaders/Copy/RootSignature.hlsl @@ -9,4 +9,4 @@ "Sampler(s0, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=16, b3), " + "RootConstants(num32BitConstants=17, b3), " diff --git a/shaders/FilmGrain/RootSignature.hlsl b/shaders/FilmGrain/RootSignature.hlsl index 64e735d3..c39aa320 100644 --- a/shaders/FilmGrain/RootSignature.hlsl +++ b/shaders/FilmGrain/RootSignature.hlsl @@ -7,5 +7,5 @@ "SRV(t0, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=16, b3), " \ + "RootConstants(num32BitConstants=17, b3), " \ "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " diff --git a/shaders/PostSharpen/RootSignature.hlsl b/shaders/PostSharpen/RootSignature.hlsl index 3980a1f3..2f88c465 100644 --- a/shaders/PostSharpen/RootSignature.hlsl +++ b/shaders/PostSharpen/RootSignature.hlsl @@ -68,7 +68,7 @@ "SRV(t0, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=16, b3), " \ + "RootConstants(num32BitConstants=17, b3), " \ "StaticSampler(s12, filter=FILTER_MIN_MAG_MIP_POINT, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s13, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ "StaticSampler(s14, filter=FILTER_MIN_MAG_MIP_LINEAR, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=6), " \ diff --git a/shaders/ScaleformComposite/RootSignature.hlsl b/shaders/ScaleformComposite/RootSignature.hlsl index 4cfeeb6c..40048b99 100644 --- a/shaders/ScaleformComposite/RootSignature.hlsl +++ b/shaders/ScaleformComposite/RootSignature.hlsl @@ -7,5 +7,5 @@ "SRV(t0, space=8) " \ "), " \ \ - "RootConstants(num32BitConstants=16, b3), " \ + "RootConstants(num32BitConstants=17, b3), " \ "StaticSampler(s0, filter=FILTER_MIN_MAG_MIP_POINT, addressU=TEXTURE_ADDRESS_CLAMP, addressV=TEXTURE_ADDRESS_CLAMP, addressW=TEXTURE_ADDRESS_CLAMP, maxAnisotropy=1, comparisonFunc=COMPARISON_NEVER, space=8) " diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 22b8bcb9..08cf1c25 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -37,6 +37,7 @@ struct StructHdrDllPluginConstants uint FilmGrainType; // 1 is default uint PostSharpen; // true is default uint IsAtEndOfFrame; + float DeltaTime; float DevSetting01; // 0-1 variable for development float DevSetting02; // 0-1 variable for development float DevSetting03; // 0-1 variable for development @@ -45,6 +46,6 @@ struct StructHdrDllPluginConstants }; //TODO: use this in the root signature files? -#define HDR_PLUGIN_CONSTANTS_SIZE 14 +#define HDR_PLUGIN_CONSTANTS_SIZE 17 ConstantBuffer HdrDllPluginConstants : register(b3, space0); From 5d0aca27435e8bc10cfdd1aa63407a4376aaec5f Mon Sep 17 00:00:00 2001 From: ersh1 <16157893+ersh1@users.noreply.github.com> Date: Mon, 9 Oct 2023 06:26:00 +0200 Subject: [PATCH 129/270] substituted delta time with application runtime --- Plugin/src/Offsets.h | 2 ++ Plugin/src/Settings.cpp | 2 +- Plugin/src/Settings.h | 2 +- shaders/shared.hlsl | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Plugin/src/Offsets.h b/Plugin/src/Offsets.h index 8d675f84..d10127ac 100644 --- a/Plugin/src/Offsets.h +++ b/Plugin/src/Offsets.h @@ -25,6 +25,7 @@ class Offsets static inline tUI_IsMenuOpen UI_IsMenuOpen = nullptr; static inline float* g_deltaTimeRealTime = nullptr; + static inline uint32_t* g_durationOfApplicationRunTimeMS = nullptr; //test static inline uintptr_t* unkToggleVsyncArg1Ptr = nullptr; @@ -48,5 +49,6 @@ class Offsets UI_IsMenuOpen = reinterpret_cast(dku::Hook::IDToAbs(187049)); g_deltaTimeRealTime = reinterpret_cast(dku::Hook::IDToAbs(871870)); + g_durationOfApplicationRunTimeMS = reinterpret_cast(dku::Hook::IDToAbs(871872)); } }; diff --git a/Plugin/src/Settings.cpp b/Plugin/src/Settings.cpp index a4d191c6..9c7d677f 100644 --- a/Plugin/src/Settings.cpp +++ b/Plugin/src/Settings.cpp @@ -65,7 +65,7 @@ namespace Settings a_outShaderConstants.FilmGrainType = static_cast(FilmGrainType.value.get_data()); a_outShaderConstants.PostSharpen = static_cast(PostSharpen.value.get_data()); a_outShaderConstants.bIsAtEndOfFrame = static_cast(bIsAtEndOfFrame.load()); - a_outShaderConstants.DeltaTime = *Offsets::g_deltaTimeRealTime; + a_outShaderConstants.RuntimeMS = *Offsets::g_durationOfApplicationRunTimeMS; a_outShaderConstants.DevSetting01 = static_cast(DevSetting01.value.get_data() * 0.01f); // 0-100 to 0-1 a_outShaderConstants.DevSetting02 = static_cast(DevSetting02.value.get_data() * 0.01f); // 0-100 to 0-1 a_outShaderConstants.DevSetting03 = static_cast(DevSetting03.value.get_data() * 0.01f); // 0-100 to 0-1 diff --git a/Plugin/src/Settings.h b/Plugin/src/Settings.h index ce2f70b9..97acd38f 100644 --- a/Plugin/src/Settings.h +++ b/Plugin/src/Settings.h @@ -84,7 +84,7 @@ namespace Settings uint32_t FilmGrainType; uint32_t PostSharpen; uint32_t bIsAtEndOfFrame; - float DeltaTime; + uint32_t RuntimeMS; float DevSetting01; float DevSetting02; float DevSetting03; diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 08cf1c25..6fc172a0 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -37,7 +37,7 @@ struct StructHdrDllPluginConstants uint FilmGrainType; // 1 is default uint PostSharpen; // true is default uint IsAtEndOfFrame; - float DeltaTime; + uint RuntimeMS; float DevSetting01; // 0-1 variable for development float DevSetting02; // 0-1 variable for development float DevSetting03; // 0-1 variable for development From 30d8f237a42ac553f003d318a565e4163359409e Mon Sep 17 00:00:00 2001 From: Filoppi Date: Mon, 9 Oct 2023 03:29:58 +0300 Subject: [PATCH 130/270] Polish: -Enable gamma 2.2 correction on LUTs (looks closer to Vanilla LUTs) -Add saturation slider without LUTs normalization (HDR only) -Add highlights modulation (HDR) --- Plugin/dist/ReadMe.txt | 3 +- Plugin/src/Settings.h | 6 ++-- .../ColorGradingMerge_cs.hlsl | 6 ++-- shaders/HDRComposite/HDRComposite_ps.hlsl | 33 +++++++++++-------- shaders/color.hlsl | 2 +- shaders/shared.hlsl | 11 ++++--- 6 files changed, 36 insertions(+), 25 deletions(-) diff --git a/Plugin/dist/ReadMe.txt b/Plugin/dist/ReadMe.txt index a79eae12..c4922ebf 100644 --- a/Plugin/dist/ReadMe.txt +++ b/Plugin/dist/ReadMe.txt @@ -28,4 +28,5 @@ You do NOT need any other HDR related mod, like AutoHDR, or "HUD for HDR" (which Issues: -The UI blends in slightly differently in HDR, usually it's not noticeable -scRGB HDR doesn't work with DLSS Frame Generation --You can not take screenshots directly from Starfield in photo mode \ No newline at end of file +-You can not take screenshots directly from Starfield in photo mode +-FSR2 clips all non Rec.709 (SDR) colors \ No newline at end of file diff --git a/Plugin/src/Settings.h b/Plugin/src/Settings.h index 97acd38f..7d4ee9d0 100644 --- a/Plugin/src/Settings.h +++ b/Plugin/src/Settings.h @@ -128,7 +128,7 @@ namespace Settings Slider Saturation{ SettingID::kHDR_Saturation, "Saturation", - "Sets the saturation strength in HDR modes with \"LUT Correction\" enabled. Neutral default at 50.", + "Sets the saturation strength in HDR modes. Neutral default at 50.", { "Saturation", "HDR" }, 50.f, 0.f, @@ -187,8 +187,8 @@ namespace Settings Slider DevSetting01{ SettingID::kDevSetting01, "DevSetting01", "Development setting", { "DevSetting01", "Dev" }, 0.f, 0.f, 100.f }; Slider DevSetting02{ SettingID::kDevSetting02, "DevSetting02", "Development setting", { "DevSetting02", "Dev" }, 0.f, 0.f, 100.f }; Slider DevSetting03{ SettingID::kDevSetting03, "DevSetting03", "Development setting", { "DevSetting03", "Dev" }, 0.f, 0.f, 100.f }; - Slider DevSetting04{ SettingID::kDevSetting04, "DevSetting04", "Development setting", { "DevSetting04", "Dev" }, 0.f, 0.f, 100.f }; - Slider DevSetting05{ SettingID::kDevSetting05, "DevSetting05", "Development setting", { "DevSetting05", "Dev" }, 0.f, 0.f, 100.f }; + Slider DevSetting04{ SettingID::kDevSetting04, "DevSetting04", "Development setting", { "DevSetting04", "Dev" }, 50.f, 0.f, 100.f }; + Slider DevSetting05{ SettingID::kDevSetting05, "DevSetting05", "Development setting", { "DevSetting05", "Dev" }, 50.f, 0.f, 100.f }; #endif String RenderTargetsToUpgrade{ "RenderTargetsToUpgrade", "RenderTargets" }; diff --git a/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl b/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl index 923f0c21..47241d81 100644 --- a/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl +++ b/shaders/ColorGradingMerge/ColorGradingMerge_cs.hlsl @@ -14,9 +14,11 @@ // Make sure LUTs are normalized by linearizing them with gamma 2.2, so they work with values closer to the expected ones. Their mixed output is still kept in sRGB. // There's a chance that LUTs had correctly been made on sRGB gamma monitors, so in that case this would be wrong (LUTs were likely made with external tools). -#if SDR_USE_GAMMA_2_2 && 0 // Disabled as this does indeed look too bright. +#if SDR_USE_GAMMA_2_2 && 1 // If it's disabled is because this makes LUTs look too bright. #define LINEARIZE(x) pow(x, 2.2f) - #define CORRECT_GAMMA(x) gamma_sRGB_to_linear(pow(x, 1.f / 2.2f)) + // NOTE: to somehow conserve some HDR colors and not generate NaNs, we are doing inverse pow as gamma on negative numbers. + // Alternatively we could skip gamma correction beyond the 0-1 range. + #define CORRECT_GAMMA(x) gamma_sRGB_to_linear(pow(abs(x), 1.f / 2.2f) * (sign(x))) #else #define LINEARIZE(x) gamma_sRGB_to_linear(x) #define CORRECT_GAMMA(x) x diff --git a/shaders/HDRComposite/HDRComposite_ps.hlsl b/shaders/HDRComposite/HDRComposite_ps.hlsl index 0386cecb..61e89015 100644 --- a/shaders/HDRComposite/HDRComposite_ps.hlsl +++ b/shaders/HDRComposite/HDRComposite_ps.hlsl @@ -47,7 +47,7 @@ cbuffer CPushConstantWrapper_HDRComposite : register(b0, space0) }; -Texture2D InputColor : register(t0, space9); +Texture2D InputColorTexture : register(t0, space9); #if defined(APPLY_MERGED_COLOR_GRADING_LUT) @@ -102,6 +102,8 @@ static const float SDRTonemapHDRStrength = 1.f; static const float PostProcessStrength = 1.f; // 0 Ignored, 1 ACES Reference, 2 ACES Custom, 3 Hable, 4+ Disable tonemapper static const uint ForceTonemapper = 0; +// 1 is neutral. Suggested range 0.5-1.5 though 1 is heavily suggested. +static const float HDRHighlightsModulation = 1.f; static const float ACES_a = 2.51f; static const float ACES_b = 0.03f; @@ -426,18 +428,19 @@ T Hable_Inverse( float3 DICETonemap( float3 Color, float MaxOutputLuminance, - float HighlightsShoulderStart = 0.f) + float HighlightsShoulderStart = 0.f, + float HighlightsModulationPow = 1.f) { #if 0 // Even if all SDR tonemappers work by channel, in HDR we prefer to maintain the hue and to match the screen peak brightness in absolute (luminance) nits, not as channel values. - Color.r = luminanceCompress(Color.r, MaxOutputLuminance, HighlightsShoulderStart); - Color.g = luminanceCompress(Color.g, MaxOutputLuminance, HighlightsShoulderStart); - Color.b = luminanceCompress(Color.b, MaxOutputLuminance, HighlightsShoulderStart); + Color.r = luminanceCompress(Color.r, MaxOutputLuminance, HighlightsShoulderStart, FLT_MAX, HighlightsModulationPow); + Color.g = luminanceCompress(Color.g, MaxOutputLuminance, HighlightsShoulderStart, FLT_MAX, HighlightsModulationPow); + Color.b = luminanceCompress(Color.b, MaxOutputLuminance, HighlightsShoulderStart, FLT_MAX, HighlightsModulationPow); return Color; #else const float sourceLuminance = Luminance(Color); if (sourceLuminance > 0.0f) { - const float compressedLuminance = luminanceCompress(sourceLuminance, MaxOutputLuminance, HighlightsShoulderStart); + const float compressedLuminance = luminanceCompress(sourceLuminance, MaxOutputLuminance, HighlightsShoulderStart, FLT_MAX, HighlightsModulationPow); Color *= compressedLuminance / sourceLuminance; } return Color; @@ -765,9 +768,9 @@ float3 GradingLUT(float3 color, float2 uv) LUTColor = gamma_sRGB_to_linear(LUTColor); #endif // !LUT_FIX_GAMMA_MAPPING - // "ColorGradingStrength" is similar to "AdditionalNeutralLUTPercentage" in the LUT mixing shader, though this is more precise as it skips the precision loss induced by a neutral LUT - const float LUTMaskAlpha = saturate(LUTMaskTexture.Sample(Sampler0, uv).x + (1.f - HdrDllPluginConstants.ColorGradingStrength)); - LUTColor = lerp(LUTColor, color, LUTMaskAlpha); + // "ColorGradingStrength" is similar to "AdditionalNeutralLUTPercentage" from the LUT mixing shader, though this is more precise as it skips the precision loss induced by a neutral LUT + const float LUTMaskAlpha = (1.f - LUTMaskTexture.Sample(Sampler0, uv).x) * HdrDllPluginConstants.ColorGradingStrength; + LUTColor = lerp(color, LUTColor, LUTMaskAlpha); return LUTColor; } @@ -787,7 +790,7 @@ float3 RestorePostProcess(float3 inverseTonemappedColor, float3 postProcessColor #if 1 // Note: in case "INVERT_TONEMAP_HIGHLIGHTS_ONLY" was false, we might want to test the "postProcessedOffsetColor" blend in range more carefully return lerp(postProcessedOffsetColor, postProcessedRatioColor, saturate(tonemappedColor / MaxShadowsColor)); #else // Doing the branching this way might not be so good, as near black colors could still end up with crazy value due to divisions between tiny values - return select(tonemappedColor == 0, postProcessedOffsetColor, postProcessedRatioColor); + return select(tonemappedColor == 0.f, postProcessedOffsetColor, postProcessedRatioColor); #endif } @@ -795,7 +798,7 @@ float3 RestorePostProcess(float3 inverseTonemappedColor, float3 postProcessColor PSOutput PS(PSInput psInput) { // Linear HDR color straight from the renderer (possibly with exposure pre-applied to it, assuming the game has some auto exposure mechanism) - float3 inputColor = InputColor.Load(int3(int2(psInput.SV_Position.xy), 0)); + float3 inputColor = InputColorTexture.Load(int3(int2(psInput.SV_Position.xy), 0)); #if CLAMP_INPUT_OUTPUT // Remove any negative value caused by using R16G16B16A16F buffers (originally this was R11G11B10F, which has no negative values). @@ -980,14 +983,18 @@ PSOutput PS(PSInput psInput) inverseTonemappedPostProcessedColor = pow(inverseTonemappedPostProcessedColor / (MidGray * midGrayScale), secondaryContrast) * (MidGray * midGrayScale); #endif + // Secondary user driven saturation. This is already placed in LUTs but it's only applied on LUTs normalization. + const float saturation = linearNormalization(HdrDllPluginConstants.HDRLUTCorrectionSaturation, 0.f, 2.f, 0.5f, 1.5f); + inverseTonemappedPostProcessedColor = Saturation(inverseTonemappedPostProcessedColor, lerp(saturation, 1.f, HdrDllPluginConstants.ColorGradingStrength * HdrDllPluginConstants.LUTCorrectionStrength)); + // Bring back the color to the same range as SDR by dividing by the mid gray change. inverseTonemappedPostProcessedColor *= paperWhite / midGrayScale; minHighlightsColorOut *= paperWhite / midGrayScale; const float maxOutputLuminance = HdrDllPluginConstants.HDRPeakBrightnessNits / WhiteNits_BT709; - // The highlights shoulder (compression) curve should never start beyond 33.33% of the max output brightness + // The highlights shoulder (compression) curve should never start beyond 33.33% of the max output brightness (found empircally) const float highlightsShoulderStart = onlyInvertHighlights ? min(maxOutputLuminance * (1.f / 3.f), minHighlightsColorOut) : 0.f; - outputColor = DICETonemap(inverseTonemappedPostProcessedColor, maxOutputLuminance, highlightsShoulderStart); + outputColor = DICETonemap(inverseTonemappedPostProcessedColor, maxOutputLuminance, highlightsShoulderStart, HDRHighlightsModulation); #else // ENABLE_TONEMAP && ENABLE_REPLACED_TONEMAP diff --git a/shaders/color.hlsl b/shaders/color.hlsl index c4fb294b..d45fad65 100644 --- a/shaders/color.hlsl +++ b/shaders/color.hlsl @@ -113,7 +113,7 @@ float Luminance(float3 color) return dot(color, float3(0.2126f, 0.7152f, 0.0722f)); } -float3 saturation(float3 color, float saturation) +float3 Saturation(float3 color, float saturation) { float3 luminance = Luminance(color); return lerp(luminance, color, saturation); diff --git a/shaders/shared.hlsl b/shaders/shared.hlsl index 6fc172a0..8ca9c1bd 100644 --- a/shaders/shared.hlsl +++ b/shaders/shared.hlsl @@ -24,6 +24,7 @@ #define HDR_REFERENCE_PAPER_WHITE_MUTLIPLIER (ReferenceWhiteNits_BT2408 / WhiteNits_BT709) // Custom push constants uploaded by the HDR DLL plugin code. Do note that register space comes at a premium when adding members. Bit/byte packing is advised. +// Bools are set as uint to avoid padding inconsistencies between c++ and hlsl. struct StructHdrDllPluginConstants { uint DisplayMode; // SDR 0 (Rec.709 with 2.2 gamma, not sRGB), 1 HDR10 PQ BT.2020, 2 scRGB HDR @@ -38,11 +39,11 @@ struct StructHdrDllPluginConstants uint PostSharpen; // true is default uint IsAtEndOfFrame; uint RuntimeMS; - float DevSetting01; // 0-1 variable for development - float DevSetting02; // 0-1 variable for development - float DevSetting03; // 0-1 variable for development - float DevSetting04; // 0-1 variable for development - float DevSetting05; // 0-1 variable for development + float DevSetting01; // 0-1 variable for development. Default 0 + float DevSetting02; // 0-1 variable for development. Default 0 + float DevSetting03; // 0-1 variable for development. Default 0 + float DevSetting04; // 0-1 variable for development. Default 0.5 + float DevSetting05; // 0-1 variable for development. Default 0.5 }; //TODO: use this in the root signature files? From 013605f242b4d18e899fb36aa0adebc62388f8f7 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Tue, 10 Oct 2023 02:28:48 +0300 Subject: [PATCH 131/270] Discord link --- Plugin/dist/ReadMe.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Plugin/dist/ReadMe.txt b/Plugin/dist/ReadMe.txt index c4922ebf..cf36074d 100644 --- a/Plugin/dist/ReadMe.txt +++ b/Plugin/dist/ReadMe.txt @@ -2,6 +2,7 @@ This mod was created by 5 (HDR passionate) developers (Ersh, Pumbo (Filoppi / Fi The main thing the mod does is adding Native HDR support to the game, which is not the same as the one seen on Xbox, this version changes the game tonemapper and color correction phase to give a much more HDR look, while also trying to retaining the look and feel of the vanilla game. This was achieved by replacing the game shaders. This mod also improves the look of the game in SDR, so benefits are not restricted to HDR users. This is a follow up to https://www.nexusmods.com/starfield/mods/588, which just increased the quality of the game buffers to 10bit+, leaving more room for AutoHDR to work. +Join our discord here: https://discord.gg/DNGfMZgH3f List of features: -Native HDR (scRGB and HDR10) From fbf07648cf66df8494c855bf99f9c0442a431e88 Mon Sep 17 00:00:00 2001 From: ersh1 <16157893+ersh1@users.noreply.github.com> Date: Tue, 10 Oct 2023 02:10:44 +0200 Subject: [PATCH 132/270] fixed color grading strength not being set to 1 with vanilla menu luts setting --- Plugin/src/Hooks.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Plugin/src/Hooks.cpp b/Plugin/src/Hooks.cpp index 84d47a3c..86f3308b 100644 --- a/Plugin/src/Hooks.cpp +++ b/Plugin/src/Hooks.cpp @@ -152,7 +152,11 @@ namespace Hooks case 0xF00FF1A: { Settings::ShaderConstants shaderConstants; - Settings::Main::GetSingleton()->GetShaderConstants(shaderConstants); + const auto settings = Settings::Main::GetSingleton(); + settings->GetShaderConstants(shaderConstants); + if (*settings->VanillaMenuLUTs.value && !Utils::ShouldCorrectLUTs()) { + shaderConstants.ColorGradingStrength = 1.f; + } uploadRootConstants(shaderConstants, 14, false); // HDRComposite break; } @@ -181,7 +185,6 @@ namespace Hooks settings->GetShaderConstants(shaderConstants); if (*settings->VanillaMenuLUTs.value && !Utils::ShouldCorrectLUTs()) { shaderConstants.LUTCorrectionStrength = 0.f; - shaderConstants.ColorGradingStrength = 1.f; } uploadRootConstants(shaderConstants, 7, true); // ColorGradingMerge break; From 0dbd4215b60a41e20cf084c0e1c2e77b1d134c32 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Mon, 9 Oct 2023 22:48:04 -0400 Subject: [PATCH 133/270] feat(filmgrain): 24fps; film math --- shaders/FilmGrain/FilmGrain_ps.hlsl | 75 ++++++++++++++++++----------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/shaders/FilmGrain/FilmGrain_ps.hlsl b/shaders/FilmGrain/FilmGrain_ps.hlsl index 0766f8a4..02b13099 100644 --- a/shaders/FilmGrain/FilmGrain_ps.hlsl +++ b/shaders/FilmGrain/FilmGrain_ps.hlsl @@ -40,6 +40,28 @@ float bethesdaRandom(float seed) { return frac(sin(seed) * 493013.f); } +// This is an attempt to replicate Kodak Vision 5242 with (0,3) range: +// Should be channel independent (R/G/B), but just using R curve for now +// Reference target is actually just luminance * 2.046f; +// (0, 0) +// (0.5, 0.2) +// (1, 0.65) +// (2, 1.6) +// (3, 2.38) +float computeFilmDensity(float luminance) { + float scaledX = luminance * 3.0f; + float result = 4.118015f + (-0.00282387f - 4.118015f)/pow(1.f + pow(scaledX/3.024177f,1.75339f),1.262519f); + return result; +} + +// Bartleson +// https://www.imaging.org/common/uploaded%20files/pdfs/Papers/2003/PICS-0-287/8583.pdf +float computeFilmGraininess(float density) { + float preComputedMin = 7.5857757502918375f; + float bofDOverC = 0.880f - (0.736f * density) - (0.003f * pow(density, 7.6f)); + return linearNormalization(pow(10.f, bofDOverC), preComputedMin, 0.f, 1.f, 0.f); +} + void frag_main() { @@ -65,6 +87,12 @@ void frag_main() linearColor = gamma_sRGB_to_linear(inputColor); } +#if 0 // Debug Gradient + float pixelNumber = 1.f - (TEXCOORD.y); + linearColor = float3(pixelNumber, pixelNumber, pixelNumber); + srgbColor = gamma_linear_to_sRGB(linearColor); +#endif + float randomNumber, customMultiplier; if (HdrDllPluginConstants.FilmGrainType == 0) @@ -95,10 +123,13 @@ void frag_main() } else { - randomNumber = rand(TEXCOORD.xy - + frac(filmGrainColorAndIntensity.x / 2147483647.f) - + frac(filmGrainColorAndIntensity.y / 2147483647.f) - ); + float fps = 24.f; // TODO: Make external option + // Mod by FPS to ensure consistent range + float frameNumber = floor(HdrDllPluginConstants.RuntimeMS / (1000.f/(fps))); + // TODO: Use iteration? Use only if repeating is noticeable + // float iteration = fmod(frameNumber, (fps * fps)); + float frame = fmod(frameNumber, fps); + randomNumber = rand(TEXCOORD.xy + (frame / fps)); #if IMPROVED_FILM_GRAIN_TYPE == 1 float colorLuma = Luminance(srgbColor); @@ -108,30 +139,20 @@ void frag_main() customMultiplier = (-2.f * (0.5f - abs(saturate(colorLuma) - 0.5f))) // Bias towards center (nothing if black or white) * 3.333f; // Bump to 10% #elif IMPROVED_FILM_GRAIN_TYPE == 3 + // Film grain is based on film density + // Film works in negative, meaning black has no density + // The greater the film density (lighter), more perceived grain + // Simplified, grain scales with Y + + // Scaling is not not linear + // We can estimate based on film stock. float colorY = Luminance(linearColor); - float midToneStart = 0.022f; // Kodak Gray Scale B (Shadow) - float midToneMid = 0.178f; // Kodak Gray Scale M (Gray) - float midToneEnd = 0.891f; // Kodak Gray Scale A (Highlight) - float midPointScaling; - if (colorY > midToneStart && colorY < midToneEnd) - { - if (colorY < midToneMid) - { - // Lerp? - midPointScaling = linearNormalization(colorY, midToneStart, midToneMid, 0.f, 1.f); - } - else - { - // Lerp? - midPointScaling = linearNormalization(colorY, midToneMid, midToneEnd, 1.f, 0.f); - } - } - else - { - midPointScaling = 0.f; - } - customMultiplier = midPointScaling - * 3.333f; // Bump to 10% + float density = max(0.f, computeFilmDensity(colorY)); + float graininess = computeFilmGraininess(density); + customMultiplier = graininess + * Luminance(srgbColor) // Scale by perceived luma + * 5.f; // Don't see a need to bump this more, 10.f for enthusiasts? + #endif } From 12a2f4b6b9682d8d34fe649adb26b3fff757dfe8 Mon Sep 17 00:00:00 2001 From: ersh1 <16157893+ersh1@users.noreply.github.com> Date: Tue, 10 Oct 2023 18:53:20 +0200 Subject: [PATCH 134/270] 1.7.36 support --- Plugin/dist/SFShaderInjector.asi | Bin 458752 -> 461312 bytes Plugin/dist/SFShaderInjector.dll | Bin 456704 -> 461824 bytes Plugin/src/Offsets.h | 2 ++ Plugin/src/Utils.cpp | 2 +- Plugin/src/main.cpp | 2 +- 5 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Plugin/dist/SFShaderInjector.asi b/Plugin/dist/SFShaderInjector.asi index b913e69412e98eff368d7bd49d56f104fd4c0fd1..cb47084e400255162e0d13a7a9f3dce3fd2818ce 100644 GIT binary patch delta 104422 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