mirror of
https://github.com/libretro/parallel-gx1.git
synced 2026-11-10 03:11:35 -10:00
511 lines
17 KiB
C++
511 lines
17 KiB
C++
// Copyright 2014 Dolphin Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include "VideoCommon/PostProcessing.h"
|
|
|
|
#include <array>
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
#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<float, 4> source_resolution;
|
|
std::array<float, 4> target_resolution;
|
|
std::array<float, 4> window_resolution;
|
|
std::array<float, 4> stereo_views;
|
|
std::array<float, 4> 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<int>& dst,
|
|
const MathUtil::Rectangle<int>& 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<u32>(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<int>& dst,
|
|
const MathUtil::Rectangle<int>& 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<u32>(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<int>& src,
|
|
const AbstractTexture* src_tex, int src_layer,
|
|
const MathUtil::Rectangle<int>& dst,
|
|
const MathUtil::Rectangle<int>& 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<float>(src_tex->GetWidth()),
|
|
static_cast<float>(src_tex->GetHeight()), rcp_src_width, rcp_src_height};
|
|
u.target_resolution = {static_cast<float>(dst.GetWidth()), static_cast<float>(dst.GetHeight()),
|
|
1.0f / static_cast<float>(dst.GetWidth()),
|
|
1.0f / static_cast<float>(dst.GetHeight())};
|
|
u.window_resolution = {static_cast<float>(wnd.GetWidth()), static_cast<float>(wnd.GetHeight()),
|
|
1.0f / static_cast<float>(wnd.GetWidth()),
|
|
1.0f / static_cast<float>(wnd.GetHeight())};
|
|
u.src_rect = {static_cast<float>(src.left) * rcp_src_width,
|
|
static_cast<float>(src.top) * rcp_src_height,
|
|
static_cast<float>(src.GetWidth()) * rcp_src_width,
|
|
static_cast<float>(src.GetHeight()) * rcp_src_height};
|
|
u.src_layer = src_layer;
|
|
u.graphics_api = static_cast<s32>(g_backend_info.api_type);
|
|
u.correct_color_space = cc.bCorrectColorSpace;
|
|
u.game_color_space = static_cast<s32>(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
|