GPU: Add alpha blending support to overlay

This commit is contained in:
Stenzek 2025-01-19 16:37:51 +10:00
parent 9b8d2a88de
commit 9fdeeb3fb6
No known key found for this signature in database
9 changed files with 172 additions and 74 deletions

View File

@ -4,3 +4,4 @@ displayStartY: 260
displayEndX: 2850
displayEndY: 1655
alphaBlend: false
destinationAlphaBlend: false

View File

@ -5771,10 +5771,15 @@ void FullscreenUI::DrawPostProcessingSettingsPage()
FSUI_CSTR("Determines the area of the overlay image that the display will be drawn within."), "BorderOverlay",
"DisplayStartX", 0, "DisplayStartY", 0, "DisplayEndX", 0, "DisplayEndY", 0, 0, 65535, "%dpx");
reload_pending |=
DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_BLENDER, "Alpha Blending"),
FSUI_CSTR("If enabled, the transparency of the overlay image will be applied."),
"BorderOverlay", "AlphaBlend", false);
reload_pending |= DrawToggleSetting(
bsi, FSUI_ICONSTR(ICON_FA_BLENDER, "Destination Alpha Blending"),
FSUI_CSTR("If enabled, the display will be blended with the transparency of the overlay image."),
"BorderOverlay", "AlphaBlend", false);
"BorderOverlay", "DestinationAlphaBlend", false);
}
}
@ -8660,6 +8665,7 @@ TRANSLATE_NOOP("FullscreenUI", "Advanced Settings");
TRANSLATE_NOOP("FullscreenUI", "All Time: {}");
TRANSLATE_NOOP("FullscreenUI", "Allow Booting Without SBI File");
TRANSLATE_NOOP("FullscreenUI", "Allows loading protected games without subchannel information.");
TRANSLATE_NOOP("FullscreenUI", "Alpha Blending");
TRANSLATE_NOOP("FullscreenUI", "Always Track Uploads");
TRANSLATE_NOOP("FullscreenUI", "An error occurred while deleting empty game settings:\n{}");
TRANSLATE_NOOP("FullscreenUI", "An error occurred while saving game settings:\n{}");
@ -8925,6 +8931,7 @@ TRANSLATE_NOOP("FullscreenUI", "How many saves will be kept for rewinding. Highe
TRANSLATE_NOOP("FullscreenUI", "How often a rewind state will be created. Higher frequencies have greater system requirements.");
TRANSLATE_NOOP("FullscreenUI", "Identifies any new files added to the game directories.");
TRANSLATE_NOOP("FullscreenUI", "If enabled, the display will be blended with the transparency of the overlay image.");
TRANSLATE_NOOP("FullscreenUI", "If enabled, the transparency of the overlay image will be applied.");
TRANSLATE_NOOP("FullscreenUI", "If not enabled, the current post processing chain will be ignored.");
TRANSLATE_NOOP("FullscreenUI", "Image Path");
TRANSLATE_NOOP("FullscreenUI", "Increases the field of view from 4:3 to the chosen display aspect ratio in 3D games.");

View File

@ -153,66 +153,69 @@ bool GPUPresenter::CompileDisplayPipelines(bool display, bool deinterlace, bool
GL_OBJECT_NAME_FMT(m_display_pipeline, "Display Pipeline [{}]",
Settings::GetDisplayScalingName(g_gpu_settings.display_scaling));
std::unique_ptr<GPUShader> rotate_copy_fso = g_gpu_device->CreateShader(
std::unique_ptr<GPUShader> copy_fso = g_gpu_device->CreateShader(
GPUShaderStage::Fragment, shadergen.GetLanguage(), shadergen.GenerateCopyFragmentShader(false), error);
if (!rotate_copy_fso)
if (!copy_fso)
return false;
GL_OBJECT_NAME(rotate_copy_fso, "Display Rotate/Copy Fragment Shader");
GL_OBJECT_NAME(copy_fso, "Display Copy Fragment Shader");
plconfig.fragment_shader = rotate_copy_fso.get();
plconfig.fragment_shader = copy_fso.get();
if (!(m_present_copy_pipeline = g_gpu_device->CreatePipeline(plconfig, error)))
return false;
GL_OBJECT_NAME(m_present_copy_pipeline, "Display Rotate/Copy Pipeline");
GL_OBJECT_NAME(m_present_copy_pipeline, "Display Copy Pipeline");
// blended variants
if (m_border_overlay_texture)
{
std::unique_ptr<GPUShader> clear_fso =
g_gpu_device->CreateShader(GPUShaderStage::Fragment, shadergen.GetLanguage(),
shadergen.GenerateFillFragmentShader(GSVector4i::zero()), error);
std::unique_ptr<GPUShader> clear_fso = g_gpu_device->CreateShader(
GPUShaderStage::Fragment, shadergen.GetLanguage(),
shadergen.GenerateFillFragmentShader(GSVector4::cxpr(0.0f, 0.0f, 0.0f, 1.0f)), error);
if (!clear_fso)
return false;
GL_OBJECT_NAME(clear_fso, "Display Clear Fragment Shader");
plconfig.fragment_shader = clear_fso.get();
if (!(m_present_clear_pipeline = g_gpu_device->CreatePipeline(plconfig, error)))
plconfig.fragment_shader = copy_fso.get();
plconfig.blend = m_border_overlay_alpha_blend ? GPUPipeline::BlendState::GetAlphaBlendingState() :
GPUPipeline::BlendState::GetNoBlendingState();
if (!(m_border_overlay_pipeline = g_gpu_device->CreatePipeline(plconfig, error)))
return false;
GL_OBJECT_NAME(m_present_clear_pipeline, "Display Clear Pipeline");
GL_OBJECT_NAME(m_border_overlay_pipeline, "Border Overlay Pipeline");
if (m_border_overlay_alpha_blend)
plconfig.blend = GPUPipeline::BlendState::GetNoBlendingState();
if (m_border_overlay_destination_alpha_blend)
{
// destination blend the main present, not source
plconfig.blend.enable = true;
plconfig.blend.src_blend = GPUPipeline::BlendFunc::InvDstAlpha;
plconfig.blend.blend_op = GPUPipeline::BlendOp::Add;
plconfig.blend.dst_blend = GPUPipeline::BlendFunc::One;
plconfig.blend.src_alpha_blend = GPUPipeline::BlendFunc::One;
plconfig.blend.src_alpha_blend = GPUPipeline::BlendFunc::Zero;
plconfig.blend.alpha_blend_op = GPUPipeline::BlendOp::Add;
plconfig.blend.dst_alpha_blend = GPUPipeline::BlendFunc::Zero;
plconfig.fragment_shader = fso.get();
if (!(m_display_blend_pipeline = g_gpu_device->CreatePipeline(plconfig, error)))
return false;
GL_OBJECT_NAME_FMT(m_display_blend_pipeline, "Display Pipeline [Blended, {}]",
Settings::GetDisplayScalingName(g_gpu_settings.display_scaling));
plconfig.fragment_shader = rotate_copy_fso.get();
if (!(m_present_copy_blend_pipeline = g_gpu_device->CreatePipeline(plconfig, error)))
return false;
GL_OBJECT_NAME(m_present_copy_blend_pipeline, "Display Rotate/Copy Pipeline [Blended]");
plconfig.fragment_shader = clear_fso.get();
if (!(m_present_clear_blend_pipeline = g_gpu_device->CreatePipeline(plconfig, error)))
return false;
GL_OBJECT_NAME(m_present_clear_blend_pipeline, "Display Clear Pipeline [Blended]");
plconfig.blend.dst_alpha_blend = GPUPipeline::BlendFunc::One;
}
plconfig.fragment_shader = clear_fso.get();
if (!(m_present_clear_pipeline = g_gpu_device->CreatePipeline(plconfig, error)))
return false;
GL_OBJECT_NAME(m_present_clear_pipeline, "Display Clear Pipeline");
plconfig.fragment_shader = fso.get();
if (!(m_display_blend_pipeline = g_gpu_device->CreatePipeline(plconfig, error)))
return false;
GL_OBJECT_NAME_FMT(m_display_blend_pipeline, "Display Pipeline [Blended, {}]",
Settings::GetDisplayScalingName(g_gpu_settings.display_scaling));
plconfig.fragment_shader = copy_fso.get();
if (!(m_present_copy_blend_pipeline = g_gpu_device->CreatePipeline(plconfig, error)))
return false;
GL_OBJECT_NAME(m_present_copy_blend_pipeline, "Display Copy Pipeline [Blended]");
}
else
{
m_border_overlay_pipeline.reset();
m_present_clear_pipeline.reset();
m_display_blend_pipeline.reset();
m_present_copy_blend_pipeline.reset();
m_present_clear_blend_pipeline.reset();
}
}
@ -483,13 +486,12 @@ GPUDevice::PresentResult GPUPresenter::RenderDisplay(GPUTexture* target, const G
if (have_overlay)
{
GL_SCOPE_FMT("Draw overlay and postfx buffer");
g_gpu_device->SetPipeline(m_present_copy_pipeline.get());
g_gpu_device->SetPipeline(m_border_overlay_pipeline.get());
g_gpu_device->SetTextureSampler(0, m_border_overlay_texture.get(), g_gpu_device->GetLinearSampler());
DrawScreenQuad(overlay_rect, GSVector4::cxpr(0.0f, 0.0f, 1.0f, 1.0f), target_size, DisplayRotation::Normal,
prerotation);
g_gpu_device->SetPipeline(m_border_overlay_alpha_blend ? m_present_copy_blend_pipeline.get() :
m_present_copy_pipeline.get());
g_gpu_device->SetPipeline(m_border_overlay_pipeline.get());
g_gpu_device->SetTextureSampler(0, postfx_output, g_gpu_device->GetNearestSampler());
DrawScreenQuad(overlay_display_rect, GSVector4::cxpr(0.0f, 0.0f, 1.0f, 1.0f), target_size,
DisplayRotation::Normal, prerotation);
@ -523,7 +525,7 @@ GPUDevice::PresentResult GPUPresenter::RenderDisplay(GPUTexture* target, const G
if (have_overlay)
{
GL_SCOPE_FMT("Draw overlay to {}", overlay_rect);
g_gpu_device->SetPipeline(m_present_copy_pipeline.get());
g_gpu_device->SetPipeline(m_border_overlay_pipeline.get());
g_gpu_device->SetTextureSampler(0, m_border_overlay_texture.get(), g_gpu_device->GetLinearSampler());
DrawScreenQuad(overlay_rect, GSVector4::cxpr(0.0f, 0.0f, 1.0f, 1.0f), target_size, DisplayRotation::Normal,
@ -533,15 +535,14 @@ GPUDevice::PresentResult GPUPresenter::RenderDisplay(GPUTexture* target, const G
{
// Need to fill in the borders.
GL_SCOPE_FMT("Fill in overlay borders - odisplay={}, draw={}", overlay_display_rect, draw_rect);
g_gpu_device->SetPipeline(m_border_overlay_alpha_blend ? m_present_clear_blend_pipeline.get() :
m_present_clear_pipeline.get());
g_gpu_device->SetPipeline(m_present_clear_pipeline.get());
DrawScreenQuad(overlay_display_rect, GSVector4::zero(), target_size, g_settings.display_rotation, prerotation);
}
}
if (m_display_texture)
{
DrawDisplay(target_size, display_rect, m_border_overlay_alpha_blend, g_gpu_settings.display_rotation,
DrawDisplay(target_size, display_rect, m_border_overlay_destination_alpha_blend, g_gpu_settings.display_rotation,
prerotation);
}
@ -1127,14 +1128,9 @@ bool GPUPresenter::UpdatePostProcessingSettings(bool force_reload, Error* error)
if (LoadOverlaySettings())
{
// something changed, need to recompile pipelines, the needed pipelines are based on alpha blend
if (LoadOverlayTexture() &&
((m_border_overlay_alpha_blend &&
(!m_present_copy_blend_pipeline || !m_display_blend_pipeline || !m_present_clear_blend_pipeline)) ||
!m_present_clear_pipeline) &&
!CompileDisplayPipelines(true, false, false, error))
{
LoadOverlayTexture();
if (!CompileDisplayPipelines(true, false, false, error))
return false;
}
}
// Update postfx settings
@ -1245,6 +1241,7 @@ bool GPUPresenter::LoadOverlaySettings()
std::string image_path;
GSVector4i display_rect = m_border_overlay_display_rect;
bool alpha_blend = m_border_overlay_alpha_blend;
bool destination_alpha_blend = m_border_overlay_destination_alpha_blend;
if (preset_name == "Custom")
{
image_path = Host::GetStringSettingValue("BorderOverlay", "ImagePath");
@ -1253,6 +1250,7 @@ bool GPUPresenter::LoadOverlaySettings()
Host::GetIntSettingValue("BorderOverlay", "DisplayEndX", 0),
Host::GetIntSettingValue("BorderOverlay", "DisplayEndY", 0));
alpha_blend = Host::GetBoolSettingValue("BorderOverlay", "AlphaBlend", false);
destination_alpha_blend = Host::GetBoolSettingValue("BorderOverlay", "DestinationAlphaBlend", false);
}
// check rect validity.. ignore everything if it's bogus
@ -1281,15 +1279,16 @@ bool GPUPresenter::LoadOverlaySettings()
m_border_overlay_display_rect = display_rect;
// but images and alphablend require pipeline/texture changes
if (m_border_overlay_image_path == image_path && (image_path.empty() || alpha_blend == m_border_overlay_alpha_blend))
{
m_border_overlay_alpha_blend = alpha_blend;
return false;
}
const bool image_changed = (m_border_overlay_image_path != image_path);
const bool changed =
(image_changed || (!image_path.empty() && (alpha_blend == m_border_overlay_alpha_blend ||
destination_alpha_blend == m_border_overlay_destination_alpha_blend)));
if (image_changed)
m_border_overlay_image_path = std::move(image_path);
m_border_overlay_image_path = std::move(image_path);
m_border_overlay_alpha_blend = alpha_blend;
return true;
m_border_overlay_destination_alpha_blend = destination_alpha_blend;
return changed;
}
bool GPUPresenter::LoadOverlayTexture()
@ -1371,13 +1370,15 @@ bool GPUPresenter::LoadOverlayPreset(Error* error, Image* image)
std::string_view image_filename;
GSVector4i display_area = GSVector4i::zero();
bool display_alpha_blend = false;
bool alpha_blend = false;
bool destination_alpha_blend = false;
if (!GetStringFromObject(root, "image", &image_filename) ||
!GetUIntFromObject(root, "displayStartX", &display_area.x) ||
!GetUIntFromObject(root, "displayStartY", &display_area.y) ||
!GetUIntFromObject(root, "displayEndX", &display_area.z) ||
!GetUIntFromObject(root, "displayEndY", &display_area.w) ||
!GetUIntFromObject(root, "alphaBlend", &display_alpha_blend))
!GetUIntFromObject(root, "alphaBlend", &alpha_blend) ||
!GetUIntFromObject(root, "destinationAlphaBlend", &destination_alpha_blend))
{
Error::SetStringView(error, "One or more parameters is missing.");
return false;
@ -1389,6 +1390,7 @@ bool GPUPresenter::LoadOverlayPreset(Error* error, Image* image)
return false;
m_border_overlay_display_rect = display_area;
m_border_overlay_alpha_blend = display_alpha_blend;
m_border_overlay_alpha_blend = alpha_blend;
m_border_overlay_destination_alpha_blend = destination_alpha_blend;
return true;
}

View File

@ -147,17 +147,17 @@ private:
u32 m_skipped_present_count = 0;
GPUTexture::Format m_present_format = GPUTexture::Format::Unknown;
bool m_border_overlay_alpha_blend = false;
bool m_border_overlay_destination_alpha_blend = false;
std::unique_ptr<GPUPipeline> m_present_copy_pipeline;
std::unique_ptr<PostProcessing::Chain> m_display_postfx;
std::unique_ptr<GPUTexture> m_border_overlay_texture;
std::unique_ptr<GPUPipeline> m_present_clear_pipeline;
// blended variants of pipelines, used when overlays are enabled
std::unique_ptr<GPUPipeline> m_border_overlay_pipeline;
std::unique_ptr<GPUPipeline> m_present_clear_pipeline;
std::unique_ptr<GPUPipeline> m_display_blend_pipeline;
std::unique_ptr<GPUPipeline> m_present_copy_blend_pipeline;
std::unique_ptr<GPUPipeline> m_present_clear_blend_pipeline;
GSVector4i m_border_overlay_display_rect = GSVector4i::zero();

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>540</width>
<height>355</height>
<height>381</height>
</rect>
</property>
<property name="windowTitle">
@ -115,10 +115,10 @@
</item>
</layout>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="alphaBlend">
<item row="2" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Destination Alpha Blending</string>
<string>Display End:</string>
</property>
</widget>
</item>
@ -160,12 +160,47 @@
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Display End:</string>
</property>
</widget>
<item row="3" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout" columnstretch="1,1">
<item row="0" column="1">
<widget class="QCheckBox" name="destinationAlphaBlend">
<property name="text">
<string>Destination Alpha Blending</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="alphaBlend">
<property name="text">
<string>Alpha Blending</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>1</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="exportCustomConfig">
<property name="text">
<string>Export</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>

View File

@ -511,6 +511,8 @@ PostProcessingOverlayConfigWidget::PostProcessingOverlayConfigWidget(SettingsWin
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.displayEndX, "BorderOverlay", "DisplayEndX", 0);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.displayEndY, "BorderOverlay", "DisplayEndY", 0);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.alphaBlend, "BorderOverlay", "AlphaBlend", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.destinationAlphaBlend, "BorderOverlay",
"DestinationAlphaBlend", false);
connect(m_ui.overlayName, &QComboBox::currentIndexChanged, this,
&PostProcessingOverlayConfigWidget::onOverlayNameCurrentIndexChanged);
@ -525,12 +527,36 @@ PostProcessingOverlayConfigWidget::PostProcessingOverlayConfigWidget(SettingsWin
connect(m_ui.displayEndY, &QSpinBox::textChanged, this, &PostProcessingOverlayConfigWidget::triggerSettingsReload);
connect(m_ui.alphaBlend, &QCheckBox::checkStateChanged, this,
&PostProcessingOverlayConfigWidget::triggerSettingsReload);
connect(m_ui.destinationAlphaBlend, &QCheckBox::checkStateChanged, this,
&PostProcessingOverlayConfigWidget::triggerSettingsReload);
onOverlayNameCurrentIndexChanged(m_ui.overlayName->currentIndex());
dialog->registerWidgetHelp(m_ui.imagePath, tr("Image Path"), tr("Unspecified"),
tr("Defines the path of the custom overlay image that will be loaded."));
const QString display_rect_title = tr("Display Rectangle");
const QString display_rect_rec_value = tr("Unspecified");
const QString display_rect_help = tr("Defines the area in the overlay image that the game image will be drawn into.");
dialog->registerWidgetHelp(m_ui.displayStartX, display_rect_title, display_rect_rec_value, display_rect_help);
dialog->registerWidgetHelp(m_ui.displayStartY, display_rect_title, display_rect_rec_value, display_rect_help);
dialog->registerWidgetHelp(m_ui.displayEndX, display_rect_title, display_rect_rec_value, display_rect_help);
dialog->registerWidgetHelp(m_ui.displayEndY, display_rect_title, display_rect_rec_value, display_rect_help);
dialog->registerWidgetHelp(
m_ui.alphaBlend, tr("Alpha Blending"), tr("Unchecked"),
tr("If checked, the overlay image will be alpha blended with the framebuffer, i.e. transparency will be applied."));
dialog->registerWidgetHelp(
m_ui.destinationAlphaBlend, tr("Destination Alpha Blending"), tr("Unchecked"),
tr("If checked, the game image will be blended with the inverse amount of alpha in the overlay image. For example, "
"an image with alpha of 0.75 will draw the game image at 25% brightness."));
}
PostProcessingOverlayConfigWidget::~PostProcessingOverlayConfigWidget() = default;
void PostProcessingOverlayConfigWidget::triggerSettingsReload()
{
g_emu_thread->updatePostProcessingSettings(true, false, false);
}
void PostProcessingOverlayConfigWidget::onOverlayNameCurrentIndexChanged(int index)
{
const int custom_idx = m_dialog->isPerGameSettings() ? 2 : 1;
@ -549,7 +575,33 @@ void PostProcessingOverlayConfigWidget::onImagePathBrowseClicked()
m_ui.imagePath->setText(QDir::toNativeSeparators(path));
}
void PostProcessingOverlayConfigWidget::triggerSettingsReload()
void PostProcessingOverlayConfigWidget::onExportCustomConfigClicked()
{
g_emu_thread->updatePostProcessingSettings(true, false, false);
const QString path =
QFileDialog::getSaveFileName(QtUtils::GetRootWidget(this), tr("Export to YAML"),
QFileInfo(m_ui.imagePath->text()).dir().path(), tr("YAML Files (*.yml)"));
if (path.isEmpty())
return;
const QString output = QStringLiteral("imagePath: \"%1\"\n"
"displayStartX: %2\n"
"displayStartY: %3\n"
"displayEndX: %4\n"
"displayEndY: %5\n"
"alphaBlend: %6\n"
"destinationAlphaBlend: %7\n")
.arg(QFileInfo(m_ui.imagePath->text()).fileName(), m_ui.displayStartX->value())
.arg(m_ui.displayStartY->value())
.arg(m_ui.displayEndX->value())
.arg(m_ui.displayEndY->value())
.arg(m_ui.alphaBlend->isChecked() ? "true" : "false")
.arg(m_ui.destinationAlphaBlend->isChecked() ? "true" : "false");
Error error;
if (!FileSystem::WriteStringToFile(QDir::toNativeSeparators(path).toStdString().c_str(), output.toStdString(),
&error))
{
QMessageBox::critical(this, tr("Export Error"),
tr("Failed to save file: %1").arg(QString::fromStdString(error.GetDescription())));
}
}

View File

@ -98,9 +98,10 @@ public:
~PostProcessingOverlayConfigWidget();
private Q_SLOTS:
void triggerSettingsReload();
void onOverlayNameCurrentIndexChanged(int index);
void onImagePathBrowseClicked();
void triggerSettingsReload();
void onExportCustomConfigClicked();
private:
Ui::PostProcessingOverlayConfigWidget m_ui;

View File

@ -845,7 +845,7 @@ std::string ShaderGen::GenerateFillFragmentShader() const
return ss.str();
}
std::string ShaderGen::GenerateFillFragmentShader(const GSVector4i fixed_color) const
std::string ShaderGen::GenerateFillFragmentShader(const GSVector4 fixed_color) const
{
std::stringstream ss;
WriteHeader(ss);

View File

@ -30,7 +30,7 @@ public:
std::string GenerateScreenQuadVertexShader(float z = 0.0f) const;
std::string GenerateUVQuadVertexShader() const;
std::string GenerateFillFragmentShader() const;
std::string GenerateFillFragmentShader(const GSVector4i fixed_color) const;
std::string GenerateFillFragmentShader(const GSVector4 fixed_color) const;
std::string GenerateCopyFragmentShader(bool offset = true) const;
std::string GenerateImGuiVertexShader() const;