// Copyright 2014 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "VideoCommon/PostProcessing.h" #include #include #include #include "Common/CommonTypes.h" #include "Common/MsgHandler.h" #include "VideoCommon/AbstractFramebuffer.h" #include "VideoCommon/AbstractGfx.h" #include "VideoCommon/AbstractPipeline.h" #include "VideoCommon/AbstractShader.h" #include "VideoCommon/AbstractTexture.h" #include "VideoCommon/Present.h" #include "VideoCommon/RenderState.h" #include "VideoCommon/ShaderGenCommon.h" #include "VideoCommon/VertexManagerBase.h" #include "VideoCommon/VideoCommon.h" #include "VideoCommon/VideoConfig.h" namespace VideoCommon { namespace { constexpr char s_copy_pixel_shader[] = "void main() { SetOutput(Sample()); }\n"; // Resampling, gamut and gamma correction and HDR scaling for the final pass. constexpr char s_correction_pixel_shader[] = R"(/***** COLOR CORRECTION *****/ // Color Space references: // https://www.unravel.com.au/understanding-color-spaces // SMPTE 170M - BT.601 (NTSC-M) -> BT.709 mat3 from_NTSCM = transpose(mat3( 0.939497225737661, 0.0502268452914346, 0.0102759289709032, 0.0177558637510127, 0.965824605885027, 0.0164195303639603, -0.00162163209967010, -0.00437400622653655, 1.00599563832621)); // ARIB TR-B9 (9300K+27MPCD with chromatic adaptation) (NTSC-J) -> BT.709 mat3 from_NTSCJ = transpose(mat3( 0.768497526, -0.210804164, 0.000297427177, 0.0397904068, 1.04825413, 0.00555809540, 0.00147510506, 0.0328789241, 1.36515128)); // EBU - BT.470BG/BT.601 (PAL) -> BT.709 mat3 from_PAL = transpose(mat3( 1.04408168421813, -0.0440816842181253, 0.000000000000000, 0.000000000000000, 1.00000000000000, 0.000000000000000, 0.000000000000000, 0.0118044782106489, 0.988195521789351)); float3 LinearTosRGBGamma(float3 color) { const float a = 0.055; for (int i = 0; i < 3; ++i) { float x = color[i]; if (x <= 0.0031308) x = x * 12.92; else x = (1.0 + a) * pow(x, 1.0 / 2.4) - a; color[i] = x; } return color; } float3 Rec709_Luminance = float3(0.2126, 0.7152, 0.0722); float Luminance(float3 color, bool native_color_space) { if (native_color_space && OptionEnabled(correct_color_space)) { if (game_color_space == 0) color *= from_NTSCM; else if (game_color_space == 1) color *= from_NTSCJ; else if (game_color_space == 2) color *= from_PAL; } return dot(color, Rec709_Luminance); } /***** COLOR SAMPLING *****/ // Non filtered gamma corrected sample (nearest neighbor) float4 QuickSample(float3 uvw, float gamma) { #if 0 // Test sampling range const float threshold = 0.00000001; float2 xy = uvw.xy * GetResolution(); // Sampling outside the valid range, draw in yellow if (xy.x < (0.0 - threshold) || xy.x > (GetResolution().x + threshold) || xy.y < (0.0 - threshold) || xy.y > (GetResolution().y + threshold)) return float4(1.0, 1.0, 0.0, 1); // Sampling at the edges, draw in purple if (xy.x < 1.0 || xy.x > (GetResolution().x - 1.0) || xy.y < 1.0 || xy.y > (GetResolution().y - 1.0)) return float4(0.5, 0, 0.5, 1); #endif float4 color = texture(samp1, uvw); color.rgb = pow(color.rgb, float3(gamma)); return color; } float4 QuickSample(float2 uv, float w, float gamma) { return QuickSample(float3(uv, w), gamma); } float4 QuickSampleByPixel(float2 xy, float w, float gamma) { float3 uvw = float3(xy * GetInvResolution(), w); return QuickSample(uvw, gamma); } /***** Bilinear Interpolation *****/ float4 BilinearSample(float3 uvw, float gamma) { // This emulates the (bi)linear filtering done directly from GPUs HW. // Note that GPUs might natively filter red green and blue differently, but we don't do it. // They might also use different filtering between upscaling and downscaling. float2 source_size = GetResolution(); float2 pixel = (uvw.xy * source_size) - 0.5; // Try to find the matching pixel top left corner // Find the integer and floating point parts float2 int_pixel = floor(pixel); float2 frac_pixel = fract(pixel); // Take 4 samples around the original uvw float4 c11 = QuickSampleByPixel(int_pixel + float2(0.5, 0.5), uvw.z, gamma); float4 c21 = QuickSampleByPixel(int_pixel + float2(1.5, 0.5), uvw.z, gamma); float4 c12 = QuickSampleByPixel(int_pixel + float2(0.5, 1.5), uvw.z, gamma); float4 c22 = QuickSampleByPixel(int_pixel + float2(1.5, 1.5), uvw.z, gamma); // Blend the 4 samples by their weight float4 avg_c = lerp(lerp(c11, c21, frac_pixel.x), lerp(c12, c22, frac_pixel.x), frac_pixel.y); // Calculate the average of the gamma space luminance, as that can be used to // properly determine the perceptual brightness the color should have. float l11 = pow(Luminance(c11.rgb, true), 1.0 / gamma); float l21 = pow(Luminance(c21.rgb, true), 1.0 / gamma); float l12 = pow(Luminance(c12.rgb, true), 1.0 / gamma); float l22 = pow(Luminance(c22.rgb, true), 1.0 / gamma); float target_avg_c_lum = lerp(lerp(l11, l21, frac_pixel.x), lerp(l12, l22, frac_pixel.x), frac_pixel.y); float avg_c_lum = Luminance(avg_c.rgb, true); if (avg_c_lum != 0.0) { avg_c.rgb *= pow(target_avg_c_lum, gamma) / avg_c_lum; } return avg_c; } void main() { // This tries to fall back on GPU HW sampling if it can (it won't be gamma corrected). bool needs_rescaling = GetResolution() != GetWindowResolution(); bool needs_resampling = needs_rescaling && (OptionEnabled(hdr_output) || OptionEnabled(correct_gamma)); float4 color; if (needs_resampling) { // Doing linear sampling in "gamma space" on linear texture formats isn't correct. // If the source and target resolutions don't match, the GPU will return a color // that is the average of 4 gamma space colors, but gamma space colors can't be blended together, // gamma needs to be de-applied first. This makes a big difference if colors change // drastically between two pixels. color = BilinearSample(v_tex0, game_gamma); } else { // Default GPU HW sampling. Bilinear is identical to Nearest Neighbor if the input and output resolutions match. if (needs_rescaling) color = texture(samp0, v_tex0); else color = texture(samp1, v_tex0); // Convert to linear before doing any other of follow up operations. color.rgb = pow(color.rgb, float3(game_gamma)); } if (OptionEnabled(correct_color_space)) { if (game_color_space == 0) color.rgb = color.rgb * from_NTSCM; else if (game_color_space == 1) color.rgb = color.rgb * from_NTSCJ; else if (game_color_space == 2) color.rgb = color.rgb * from_PAL; } if (OptionEnabled(hdr_output)) { float hdr_paper_white = hdr_paper_white_nits / hdr_sdr_white_nits; color.rgb *= hdr_paper_white; } if (OptionEnabled(linear_space_output)) { // Nothing to do here } // Correct the SDR gamma for sRGB (PC/Monitor) or ~2.2 (Common TV gamma) else if (OptionEnabled(correct_gamma)) { if (OptionEnabled(sdr_display_gamma_sRGB)) color.rgb = LinearTosRGBGamma(color.rgb); else color.rgb = pow(color.rgb, float3(1.0 / sdr_display_custom_gamma)); } // Restore the original gamma without changes else { color.rgb = pow(color.rgb, float3(1.0 / game_gamma)); } SetOutput(color); } )"; struct Uniforms { // bools are s32 std::array source_resolution; std::array target_resolution; std::array window_resolution; std::array stereo_views; std::array src_rect; s32 src_layer; u32 time; s32 graphics_api; s32 intermediary_buffer; s32 correct_color_space; s32 game_color_space; s32 correct_gamma; float game_gamma; s32 sdr_display_gamma_sRGB; float sdr_display_custom_gamma; s32 linear_space_output; s32 hdr_output; float hdr_paper_white_nits; float hdr_sdr_white_nits; }; constexpr char s_uniform_block[] = R"(UBO_BINDING(std140, 1) uniform PSBlock { float4 resolution; float4 target_resolution; float4 window_resolution; int2 stereo_views; float4 src_rect; int src_layer; uint time; int graphics_api; int intermediary_buffer; int correct_color_space; int game_color_space; int correct_gamma; float game_gamma; int sdr_display_gamma_sRGB; float sdr_display_custom_gamma; int linear_space_output; int hdr_output; float hdr_paper_white_nits; float hdr_sdr_white_nits; }; )"; constexpr char s_pixel_header[] = R"(SAMPLER_BINDING(0) uniform sampler2DArray samp0; SAMPLER_BINDING(1) uniform sampler2DArray samp1; FRAGMENT_OUTPUT_LOCATION(0) out float4 ocol0; float4 Sample() { return texture(samp0, v_tex0); } float2 GetWindowResolution() { return window_resolution.xy; } float2 GetInvWindowResolution() { return window_resolution.zw; } float2 GetResolution() { return resolution.xy; } float2 GetInvResolution() { return resolution.zw; } void SetOutput(float4 color) { ocol0 = color; } #define OptionEnabled(x) ((x) != 0) )"; ShaderCode PixelShaderSource(const char* body) { ShaderCode source; source.Puts(s_uniform_block); if (g_backend_info.bSupportsGeometryShaders) source.Puts("VARYING_LOCATION(0) in VertexData {\n float3 v_tex0;\n};\n"); else source.Puts("VARYING_LOCATION(0) in float3 v_tex0;\n"); source.Puts(s_pixel_header); source.Puts(body); return source; } ShaderCode VertexShaderSource() { ShaderCode source; source.Puts(s_uniform_block); if (g_backend_info.bSupportsGeometryShaders) source.Puts("VARYING_LOCATION(0) out VertexData {\n float3 v_tex0;\n};\n"); else source.Puts("VARYING_LOCATION(0) out float3 v_tex0;\n"); source.Puts("void main() {\n" " v_tex0 = float3(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2), 0.0f);\n" " gl_Position = float4(v_tex0.xy * float2(2.0f, -2.0f) + float2(-1.0f, 1.0f), " "0.0f, 1.0f);\n" " v_tex0 = float3(src_rect.xy + (src_rect.zw * v_tex0.xy), float(src_layer));\n"); // Vulkan's Y axis points down if (g_backend_info.api_type == APIType::Vulkan) source.Puts(" gl_Position.y = -gl_Position.y;\n"); source.Puts("}\n"); return source; } } // namespace PostProcessing::PostProcessing() = default; PostProcessing::~PostProcessing() { DestroyPipelines(); AbstractShaderDelete(m_correction_pixel_shader); AbstractShaderDelete(m_copy_pixel_shader); AbstractShaderDelete(m_vertex_shader); } void PostProcessing::DestroyPipelines() { AbstractPipelineDelete(m_copy_pipeline); m_copy_pipeline = nullptr; AbstractPipelineDelete(m_correction_pipeline); m_correction_pipeline = nullptr; AbstractPipelineDelete(m_screen_pipeline); m_screen_pipeline = nullptr; } bool PostProcessing::Initialize(AbstractTextureFormat format) { m_framebuffer_format = format; m_uniform_staging_buffer.resize(sizeof(Uniforms)); return CompileShaders() && CompilePipelines(); } void PostProcessing::RecompilePipeline() { DestroyPipelines(); CompilePipelines(); } bool PostProcessing::IsColorCorrectionActive() const { return g_ActiveConfig.color_correction.bCorrectColorSpace || g_ActiveConfig.color_correction.bCorrectGamma || m_framebuffer_format == AbstractTextureFormat::RGBA16F; } void PostProcessing::BlitFromTexture(const MathUtil::Rectangle& dst, const MathUtil::Rectangle& src, const AbstractTexture* src_tex, int src_layer) { if (g_gfx->GetCurrentFramebuffer()->GetColorFormat() != m_framebuffer_format) { m_framebuffer_format = g_gfx->GetCurrentFramebuffer()->GetColorFormat(); RecompilePipeline(); } // The bilinear sampler does the scaling; colour correction or HDR takes the correction shader. const bool needs_correction = IsColorCorrectionActive(); const AbstractPipeline* pipeline = needs_correction && m_correction_pipeline ? m_correction_pipeline : m_copy_pipeline; if (!pipeline) return; g_gfx->SetSamplerState(0, RenderState::GetLinearSamplerState()); g_gfx->SetSamplerState(1, RenderState::GetPointSamplerState()); g_gfx->SetTexture(0, src_tex); g_gfx->SetTexture(1, src_tex); FillUniformBuffer(src, src_tex, std::max(src_layer, 0), g_gfx->GetCurrentFramebuffer()->GetRect(), g_presenter->GetTargetRectangle()); g_vertex_manager->UploadUtilityUniforms(m_uniform_staging_buffer.data(), static_cast(m_uniform_staging_buffer.size())); g_gfx->SetViewportAndScissor( g_gfx->ConvertFramebufferRectangle(dst, g_gfx->GetCurrentFramebuffer())); g_gfx->SetPipeline(pipeline); g_gfx->Draw(0, 3); } void PostProcessing::BlitScreen(const MathUtil::Rectangle& dst, const MathUtil::Rectangle& src, const AbstractTexture* src_tex) { if (g_gfx->GetCurrentFramebuffer()->GetColorFormat() != m_framebuffer_format) { m_framebuffer_format = g_gfx->GetCurrentFramebuffer()->GetColorFormat(); RecompilePipeline(); } if (!m_screen_pipeline) return; g_gfx->SetSamplerState(0, RenderState::GetPointSamplerState()); g_gfx->SetTexture(0, src_tex); FillUniformBuffer(src, src_tex, 0, dst, dst); g_vertex_manager->UploadUtilityUniforms(m_uniform_staging_buffer.data(), static_cast(m_uniform_staging_buffer.size())); g_gfx->SetViewportAndScissor( g_gfx->ConvertFramebufferRectangle(dst, g_gfx->GetCurrentFramebuffer())); g_gfx->SetPipeline(m_screen_pipeline); g_gfx->Draw(0, 3); } bool PostProcessing::CompileShaders() { m_vertex_shader = g_gfx->CreateShaderFromSource(ShaderStage::Vertex, VertexShaderSource(), "Present vertex shader"); m_copy_pixel_shader = g_gfx->CreateShaderFromSource( ShaderStage::Pixel, PixelShaderSource(s_copy_pixel_shader), "Present copy pixel shader"); // Optional: without it the output is copied uncorrected. m_correction_pixel_shader = g_gfx->CreateShaderFromSource( ShaderStage::Pixel, PixelShaderSource(s_correction_pixel_shader), "Present correction pixel shader"); if (!m_vertex_shader || !m_copy_pixel_shader) { PanicAlertFmt("Failed to compile the presentation shaders"); return false; } return true; } bool PostProcessing::CompilePipelines() { // Some backends refuse pipelines with no targets; nothing is drawn then anyway. if (m_framebuffer_format == AbstractTextureFormat::Undefined) return true; AbstractPipelineConfig config = {}; config.vertex_shader = m_vertex_shader; config.rasterization_state = RenderState::GetNoCullRasterizationState(PrimitiveType::Triangles); config.depth_state = RenderState::GetNoDepthTestingDepthState(); config.blending_state = RenderState::GetNoBlendingBlendState(); config.framebuffer_state = RenderState::GetColorFramebufferState(m_framebuffer_format); config.usage = AbstractPipelineUsage::Utility; if (m_correction_pixel_shader) { config.pixel_shader = m_correction_pixel_shader; m_correction_pipeline = g_gfx->CreatePipeline(config); } config.pixel_shader = m_copy_pixel_shader; m_copy_pipeline = g_gfx->CreatePipeline(config); config.blending_state.alpha_update = false; m_screen_pipeline = g_gfx->CreatePipeline(config); return m_copy_pipeline != nullptr; } void PostProcessing::FillUniformBuffer(const MathUtil::Rectangle& src, const AbstractTexture* src_tex, int src_layer, const MathUtil::Rectangle& dst, const MathUtil::Rectangle& wnd) { const float rcp_src_width = 1.0f / src_tex->GetWidth(); const float rcp_src_height = 1.0f / src_tex->GetHeight(); const auto& cc = g_ActiveConfig.color_correction; const bool hdr = m_framebuffer_format == AbstractTextureFormat::RGBA16F; Uniforms u = {}; u.source_resolution = {static_cast(src_tex->GetWidth()), static_cast(src_tex->GetHeight()), rcp_src_width, rcp_src_height}; u.target_resolution = {static_cast(dst.GetWidth()), static_cast(dst.GetHeight()), 1.0f / static_cast(dst.GetWidth()), 1.0f / static_cast(dst.GetHeight())}; u.window_resolution = {static_cast(wnd.GetWidth()), static_cast(wnd.GetHeight()), 1.0f / static_cast(wnd.GetWidth()), 1.0f / static_cast(wnd.GetHeight())}; u.src_rect = {static_cast(src.left) * rcp_src_width, static_cast(src.top) * rcp_src_height, static_cast(src.GetWidth()) * rcp_src_width, static_cast(src.GetHeight()) * rcp_src_height}; u.src_layer = src_layer; u.graphics_api = static_cast(g_backend_info.api_type); u.correct_color_space = cc.bCorrectColorSpace; u.game_color_space = static_cast(cc.game_color_space); u.correct_gamma = cc.bCorrectGamma; u.game_gamma = cc.fGameGamma; u.sdr_display_gamma_sRGB = cc.bSDRDisplayGammaSRGB; u.sdr_display_custom_gamma = cc.fSDRDisplayCustomGamma; // scRGB (RGBA16F) takes linear values, which may leave the 0-1 range u.linear_space_output = hdr; u.hdr_output = hdr; u.hdr_paper_white_nits = cc.fHDRPaperWhiteNits; // 1.0 is about 80 nits in HDR u.hdr_sdr_white_nits = 80.f; std::memcpy(m_uniform_staging_buffer.data(), &u, sizeof(u)); } } // namespace VideoCommon