diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp
index 4dd0ec3eb..4df84f591 100644
--- a/src/core/fullscreen_ui.cpp
+++ b/src/core/fullscreen_ui.cpp
@@ -3997,6 +3997,10 @@ void FullscreenUI::DrawDisplaySettingsPage()
FSUI_CSTR("Disables dithering and uses the full 8 bits per channel of color information."), "GPU",
"TrueColor", true, is_hardware);
+ DrawToggleSetting(bsi, FSUI_CSTR("True Color Debanding"),
+ FSUI_CSTR("Applies modern dithering techniques to further smooth out gradients when true color is enabled."), "GPU",
+ "Debanding", false, is_hardware);
+
DrawToggleSetting(bsi, FSUI_CSTR("Widescreen Hack"),
FSUI_CSTR("Increases the field of view from 4:3 to the chosen display aspect ratio in 3D games."),
"GPU", "WidescreenHack", false, is_hardware);
diff --git a/src/core/gpu_hw.cpp b/src/core/gpu_hw.cpp
index 2ffdd333f..f76e611b0 100644
--- a/src/core/gpu_hw.cpp
+++ b/src/core/gpu_hw.cpp
@@ -212,6 +212,7 @@ bool GPU_HW::Initialize()
m_supports_framebuffer_fetch = features.framebuffer_fetch;
m_per_sample_shading = g_settings.gpu_per_sample_shading && features.per_sample_shading;
m_true_color = g_settings.gpu_true_color;
+ m_debanding = g_settings.gpu_debanding;
m_scaled_dithering = g_settings.gpu_scaled_dithering;
m_texture_filtering = g_settings.gpu_texture_filter;
m_clamp_uvs = ShouldClampUVs();
@@ -345,7 +346,7 @@ void GPU_HW::UpdateSettings(const Settings& old_settings)
g_settings.gpu_downsample_scale != old_settings.gpu_downsample_scale));
const bool shaders_changed =
(m_resolution_scale != resolution_scale || m_multisamples != multisamples ||
- m_true_color != g_settings.gpu_true_color || m_per_sample_shading != per_sample_shading ||
+ m_true_color != g_settings.gpu_true_color || m_debanding != g_settings.gpu_debanding || m_per_sample_shading != per_sample_shading ||
m_scaled_dithering != g_settings.gpu_scaled_dithering || m_texture_filtering != g_settings.gpu_texture_filter ||
m_clamp_uvs != clamp_uvs || m_chroma_smoothing != g_settings.gpu_24bit_chroma_smoothing ||
m_downsample_mode != downsample_mode ||
@@ -395,6 +396,7 @@ void GPU_HW::UpdateSettings(const Settings& old_settings)
m_multisamples = multisamples;
m_per_sample_shading = per_sample_shading;
m_true_color = g_settings.gpu_true_color;
+ m_debanding = g_settings.gpu_debanding;
m_scaled_dithering = g_settings.gpu_scaled_dithering;
m_texture_filtering = g_settings.gpu_texture_filter;
m_clamp_uvs = clamp_uvs;
@@ -604,7 +606,7 @@ void GPU_HW::PrintSettingsToLog()
VRAM_HEIGHT * m_resolution_scale, GetMaxResolutionScale());
Log_InfoFmt("Multisampling: {}x{}", m_multisamples, m_per_sample_shading ? " (per sample shading)" : "");
Log_InfoFmt("Dithering: {}{}", m_true_color ? "Disabled" : "Enabled",
- (!m_true_color && m_scaled_dithering) ? " (Scaled)" : "");
+ (!m_true_color && m_scaled_dithering) ? " (Scaled)" : ((m_true_color && m_debanding) ? " (Debanding)" : ""));
Log_InfoFmt("Texture Filtering: {}", Settings::GetTextureFilterDisplayName(m_texture_filtering));
Log_InfoFmt("Dual-source blending: {}", m_supports_dual_source_blend ? "Supported" : "Not supported");
Log_InfoFmt("Clamping UVs: {}", m_clamp_uvs ? "YES" : "NO");
@@ -698,7 +700,7 @@ bool GPU_HW::CompilePipelines()
const GPUDevice::Features features = g_gpu_device->GetFeatures();
GPU_HW_ShaderGen shadergen(g_gpu_device->GetRenderAPI(), m_resolution_scale, m_multisamples, m_per_sample_shading,
m_true_color, m_scaled_dithering, m_texture_filtering, m_clamp_uvs, m_pgxp_depth_buffer,
- m_disable_color_perspective, m_supports_dual_source_blend, m_supports_framebuffer_fetch);
+ m_disable_color_perspective, m_supports_dual_source_blend, m_supports_framebuffer_fetch, m_debanding);
ShaderCompileProgressTracker progress("Compiling Pipelines", 2 + (4 * 5 * 9 * 2 * 2) + (3 * 4 * 5 * 9 * 2 * 2) + 1 +
2 + (2 * 2) + 2 + 1 + 1 + (2 * 3) + 1);
@@ -3126,6 +3128,11 @@ void GPU_HW::DrawRendererStats(bool is_idle_frame)
ImGui::TextColored(m_true_color ? active_color : inactive_color, m_true_color ? "Enabled" : "Disabled");
ImGui::NextColumn();
+ ImGui::TextUnformatted("Debanding:");
+ ImGui::NextColumn();
+ ImGui::TextColored(m_debanding ? active_color : inactive_color, m_debanding ? "Enabled" : "Disabled");
+ ImGui::NextColumn();
+
ImGui::TextUnformatted("Scaled Dithering:");
ImGui::NextColumn();
ImGui::TextColored(m_scaled_dithering ? active_color : inactive_color, m_scaled_dithering ? "Enabled" : "Disabled");
diff --git a/src/core/gpu_hw.h b/src/core/gpu_hw.h
index 2ffc31f2e..a3a27d3cd 100644
--- a/src/core/gpu_hw.h
+++ b/src/core/gpu_hw.h
@@ -252,6 +252,7 @@ private:
GPUDownsampleMode m_downsample_mode = GPUDownsampleMode::Disabled;
GPUWireframeMode m_wireframe_mode = GPUWireframeMode::Disabled;
bool m_true_color = true;
+ bool m_debanding = false;
bool m_clamp_uvs = false;
bool m_compute_uv_range = false;
bool m_pgxp_depth_buffer = false;
diff --git a/src/core/gpu_hw_shadergen.cpp b/src/core/gpu_hw_shadergen.cpp
index e9c8e4cf0..398592ed7 100644
--- a/src/core/gpu_hw_shadergen.cpp
+++ b/src/core/gpu_hw_shadergen.cpp
@@ -9,11 +9,11 @@ GPU_HW_ShaderGen::GPU_HW_ShaderGen(RenderAPI render_api, u32 resolution_scale, u
bool per_sample_shading, bool true_color, bool scaled_dithering,
GPUTextureFilter texture_filtering, bool uv_limits, bool pgxp_depth,
bool disable_color_perspective, bool supports_dual_source_blend,
- bool supports_framebuffer_fetch)
+ bool supports_framebuffer_fetch, bool debanding)
: ShaderGen(render_api, supports_dual_source_blend, supports_framebuffer_fetch), m_resolution_scale(resolution_scale),
m_multisamples(multisamples), m_per_sample_shading(per_sample_shading), m_true_color(true_color),
m_scaled_dithering(scaled_dithering), m_texture_filter(texture_filtering), m_uv_limits(uv_limits),
- m_pgxp_depth(pgxp_depth), m_disable_color_perspective(disable_color_perspective)
+ m_pgxp_depth(pgxp_depth), m_disable_color_perspective(disable_color_perspective), m_debanding(debanding)
{
}
@@ -615,7 +615,7 @@ void FilteredSampleFromVRAM(uint4 texpage, float2 coords, float4 uv_limits,
ialpha = res.w;
texcol = float4(res.xyz, resW);
-
+
// Compensate for partially transparent sampling.
if (ialpha > 0.0)
texcol.rgb /= float3(ialpha, ialpha, ialpha);
@@ -662,6 +662,8 @@ std::string GPU_HW_ShaderGen::GenerateBatchFragmentShader(GPU_HW::BatchRenderMod
DefineMacro(ss, "RAW_TEXTURE", raw_texture);
DefineMacro(ss, "DITHERING", dithering);
DefineMacro(ss, "DITHERING_SCALED", m_scaled_dithering);
+ // Debanding requires true color to work correctly.
+ DefineMacro(ss, "DEBANDING", m_true_color && m_debanding);
DefineMacro(ss, "INTERLACING", interlacing);
DefineMacro(ss, "TRUE_COLOR", m_true_color);
DefineMacro(ss, "TEXTURE_FILTERING", m_texture_filter != GPUTextureFilter::Nearest);
@@ -761,13 +763,29 @@ float4 SampleFromVRAM(uint4 texpage, float2 coords)
return SAMPLE_TEXTURE(samp0, float2(palette_icoord) * RCP_VRAM_SIZE);
#else
// Direct texturing. Render-to-texture effects. Use upscaled coordinates.
- uint2 icoord = ApplyUpscaledTextureWindow(FloatToIntegerCoords(coords));
+ uint2 icoord = ApplyUpscaledTextureWindow(FloatToIntegerCoords(coords));
uint2 direct_icoord = texpage.xy + icoord;
return SAMPLE_TEXTURE(samp0, float2(direct_icoord) * RCP_VRAM_SIZE);
#endif
}
#endif
+
+// From https://alex.vlachos.com/graphics/Alex_Vlachos_Advanced_VR_Rendering_GDC2015.pdf
+// and https://www.shadertoy.com/view/MslGR8 (5th one starting from the bottom)
+// NOTE: `frag_coord` is in pixels (i.e. not normalized UV).
+float3 ApplyDebanding(float2 frag_coord) {
+#if DEBANDING
+ // Iestyn's RGB dither (7 asm instructions) from Portal 2 X360, slightly modified for VR.
+ float3 dither = float3(dot(vec2(171.0, 231.0), frag_coord));
+ dither.rgb = fract(dither.rgb / float3(103.0, 71.0, 97.0));
+
+ // Subtract 0.5 to avoid slightly brightening the whole viewport.
+ return (dither.rgb - 0.5) / 255.0;
+#else
+ return float3(0.0);
+#endif
+}
)";
if (textured)
@@ -797,7 +815,7 @@ float4 SampleFromVRAM(uint4 texpage, float2 coords)
ss << R"(
{
- uint3 vertcol = uint3(v_col0.rgb * float3(255.0, 255.0, 255.0));
+ uint3 vertcol = uint3(v_col0.rgb * float3(255.0, 255.0, 255.0) + ApplyDebanding(v_pos.xy));
bool semitransparent;
uint3 icolor;
@@ -821,7 +839,7 @@ float4 SampleFromVRAM(uint4 texpage, float2 coords)
#if UV_LIMITS
float4 uv_limits = v_uv_limits;
#if !PALETTE
- // Extend the UV range to all "upscaled" pixels. This means 1-pixel-high polygon-based
+ // Extend the UV range to all "upscaled" pixels. This means 1-pixel-high polygon-based
// framebuffer effects won't be downsampled. (e.g. Mega Man Legends 2 haze effect)
uv_limits *= float(RESOLUTION_SCALE);
uv_limits.zw += float(RESOLUTION_SCALE - 1u);
@@ -859,7 +877,7 @@ float4 SampleFromVRAM(uint4 texpage, float2 coords)
#endif
#endif
#else
- icolor = uint3(texcol.rgb * float3(255.0, 255.0, 255.0));
+ icolor = uint3(texcol.rgb * float3(255.0, 255.0, 255.0) + ApplyDebanding(v_pos.xy));
#if !RAW_TEXTURE
icolor = (icolor * vertcol) >> 7;
#if DITHERING
@@ -1051,10 +1069,10 @@ float3 SampleVRAM24(uint2 icoords)
uint2 vram_coords = u_vram_offset + uint2((icoords.x * 3u) / 2u, icoords.y);
uint s0 = RGBA8ToRGBA5551(LoadVRAM(int2((vram_coords % clamp_size) * RESOLUTION_SCALE)));
uint s1 = RGBA8ToRGBA5551(LoadVRAM(int2(((vram_coords + uint2(1, 0)) % clamp_size) * RESOLUTION_SCALE)));
-
+
// select which part of the combined 16-bit texels we are currently shading
uint s1s0 = ((s1 << 16) | s0) >> ((icoords.x & 1u) * 8u);
-
+
// extract components and normalize
return float3(float(s1s0 & 0xFFu) / 255.0, float((s1s0 >> 8u) & 0xFFu) / 255.0,
float((s1s0 >> 16u) & 0xFFu) / 255.0);
@@ -1113,7 +1131,7 @@ float3 SampleVRAM24Smoothed(uint2 icoords)
o_col0 = float4(SampleVRAM24Smoothed(icoords), 1.0);
#else
o_col0 = float4(SampleVRAM24(icoords), 1.0);
- #endif
+ #endif
#else
o_col0 = float4(LoadVRAM(int2((icoords + u_vram_offset) % VRAM_SIZE)).rgb, 1.0);
#endif
@@ -1333,7 +1351,7 @@ std::string GPU_HW_ShaderGen::GenerateVRAMWriteFragmentShader(bool use_buffer, b
uint buffer_offset = u_buffer_base_offset + (offset.y * u_size.x) + offset.x;
uint value = GET_VALUE(buffer_offset) | u_mask_or_bits;
#endif
-
+
o_col0 = RGBA5551ToRGBA8(value);
#if !PGXP_DEPTH
o_depth = (o_col0.a == 1.0) ? u_depth_value : 0.0;
diff --git a/src/core/gpu_hw_shadergen.h b/src/core/gpu_hw_shadergen.h
index 576550c72..938e6e142 100644
--- a/src/core/gpu_hw_shadergen.h
+++ b/src/core/gpu_hw_shadergen.h
@@ -11,7 +11,7 @@ public:
GPU_HW_ShaderGen(RenderAPI render_api, u32 resolution_scale, u32 multisamples, bool per_sample_shading,
bool true_color, bool scaled_dithering, GPUTextureFilter texture_filtering, bool uv_limits,
bool pgxp_depth, bool disable_color_perspective, bool supports_dual_source_blend,
- bool supports_framebuffer_fetch);
+ bool supports_framebuffer_fetch, bool debanding);
~GPU_HW_ShaderGen();
std::string GenerateBatchVertexShader(bool textured);
@@ -51,4 +51,5 @@ private:
bool m_uv_limits;
bool m_pgxp_depth;
bool m_disable_color_perspective;
+ bool m_debanding;
};
diff --git a/src/core/imgui_overlays.cpp b/src/core/imgui_overlays.cpp
index 15dd71717..e2f931b37 100644
--- a/src/core/imgui_overlays.cpp
+++ b/src/core/imgui_overlays.cpp
@@ -525,8 +525,13 @@ void ImGuiManager::DrawEnhancementsOverlay()
{
text.append_format(" {}x{}", g_settings.gpu_multisamples, g_settings.gpu_per_sample_shading ? "SSAA" : "MSAA");
}
- if (g_settings.gpu_true_color)
- text.append(" TrueCol");
+ if (g_settings.gpu_true_color) {
+ if (g_settings.gpu_debanding) {
+ text.append(" TrueColDeband");
+ } else {
+ text.append(" TrueCol");
+ }
+ }
if (g_settings.gpu_disable_interlacing)
text.append(" ForceProg");
if (g_settings.gpu_force_ntsc_timings && System::GetRegion() == ConsoleRegion::PAL)
diff --git a/src/core/settings.cpp b/src/core/settings.cpp
index bbcfa7155..18a6c4db5 100644
--- a/src/core/settings.cpp
+++ b/src/core/settings.cpp
@@ -190,6 +190,7 @@ void Settings::Load(SettingsInterface& si)
gpu_use_software_renderer_for_readbacks = si.GetBoolValue("GPU", "UseSoftwareRendererForReadbacks", false);
gpu_threaded_presentation = si.GetBoolValue("GPU", "ThreadedPresentation", true);
gpu_true_color = si.GetBoolValue("GPU", "TrueColor", true);
+ gpu_debanding = si.GetBoolValue("GPU", "Debanding", false);
gpu_scaled_dithering = si.GetBoolValue("GPU", "ScaledDithering", true);
gpu_texture_filter =
ParseTextureFilterName(
@@ -456,6 +457,7 @@ void Settings::Save(SettingsInterface& si) const
si.SetBoolValue("GPU", "ThreadedPresentation", gpu_threaded_presentation);
si.SetBoolValue("GPU", "UseSoftwareRendererForReadbacks", gpu_use_software_renderer_for_readbacks);
si.SetBoolValue("GPU", "TrueColor", gpu_true_color);
+ si.SetBoolValue("GPU", "Debanding", gpu_debanding);
si.SetBoolValue("GPU", "ScaledDithering", gpu_scaled_dithering);
si.SetStringValue("GPU", "TextureFilter", GetTextureFilterName(gpu_texture_filter));
si.SetStringValue("GPU", "DownsampleMode", GetDownsampleModeName(gpu_downsample_mode));
@@ -614,6 +616,7 @@ void Settings::FixIncompatibleSettings(bool display_osd_messages)
g_settings.gpu_multisamples = 1;
g_settings.gpu_per_sample_shading = false;
g_settings.gpu_true_color = false;
+ g_settings.gpu_debanding = false;
g_settings.gpu_scaled_dithering = false;
g_settings.gpu_texture_filter = GPUTextureFilter::Nearest;
g_settings.gpu_disable_interlacing = false;
diff --git a/src/core/settings.h b/src/core/settings.h
index aa4445219..7924a0ce6 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -111,6 +111,7 @@ struct Settings
bool gpu_disable_texture_copy_to_self = false;
bool gpu_per_sample_shading = false;
bool gpu_true_color = true;
+ bool gpu_debanding = false;
bool gpu_scaled_dithering = true;
GPUTextureFilter gpu_texture_filter = DEFAULT_GPU_TEXTURE_FILTER;
GPUDownsampleMode gpu_downsample_mode = DEFAULT_GPU_DOWNSAMPLE_MODE;
diff --git a/src/core/system.cpp b/src/core/system.cpp
index 8e4166aff..044566e09 100644
--- a/src/core/system.cpp
+++ b/src/core/system.cpp
@@ -3626,6 +3626,7 @@ void System::CheckForSettingsChanges(const Settings& old_settings)
g_settings.gpu_fifo_size != old_settings.gpu_fifo_size ||
g_settings.gpu_max_run_ahead != old_settings.gpu_max_run_ahead ||
g_settings.gpu_true_color != old_settings.gpu_true_color ||
+ g_settings.gpu_debanding != old_settings.gpu_debanding ||
g_settings.gpu_scaled_dithering != old_settings.gpu_scaled_dithering ||
g_settings.gpu_texture_filter != old_settings.gpu_texture_filter ||
g_settings.gpu_disable_interlacing != old_settings.gpu_disable_interlacing ||
diff --git a/src/duckstation-qt/enhancementsettingswidget.cpp b/src/duckstation-qt/enhancementsettingswidget.cpp
index ce25bf4e6..0053037bf 100644
--- a/src/duckstation-qt/enhancementsettingswidget.cpp
+++ b/src/duckstation-qt/enhancementsettingswidget.cpp
@@ -80,6 +80,11 @@ EnhancementSettingsWidget::EnhancementSettingsWidget(SettingsWindow* dialog, QWi
"Disabling the option also enables dithering, which makes the transition between colours less sharp by applying "
"a pattern around those pixels. Most games are compatible with this option, but there is a number which aren't "
"and will have broken effects with it enabled. Only applies to the hardware renderers."));
+ dialog->registerWidgetHelp(
+ m_ui.debanding, tr("True Color Debanding"), tr("Unchecked"),
+ tr("Applies modern dithering techniques to further smooth out gradients when true color is enabled. "
+ "This debanding is performed during rendering (as opposed to a post-processing step), which allows it to be fast while preserving detail. "
+ "Debanding increases the file size of screenshots due to the subtle dithering pattern present in screenshots."));
dialog->registerWidgetHelp(
m_ui.scaledDithering, tr("Scaled Dithering (scale dither pattern to resolution)"), tr("Checked"),
tr("Scales the dither pattern to the resolution scale of the emulated GPU. This makes the dither pattern much less "
diff --git a/src/duckstation-qt/enhancementsettingswidget.ui b/src/duckstation-qt/enhancementsettingswidget.ui
index d4e873427..8a4cc52a8 100644
--- a/src/duckstation-qt/enhancementsettingswidget.ui
+++ b/src/duckstation-qt/enhancementsettingswidget.ui
@@ -49,13 +49,20 @@
-
- -
+
-
True Color Rendering (24-bit, disables dithering)
+ -
+
+
+ True Color Debanding
+
+
+
-