mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
A third display upscaler beside FSR1, and the first one written for the hardware this app actually runs on: FSR1's two compute passes are the expensive outlier on mobile, while SGSR is a single pass Qualcomm designed for Adreno. The filter is Qualcomm's, BSD-3-Clause, unchanged in substance. What differs is the shape around it -- theirs is a fragment shader over a fullscreen triangle, this is a compute pass, because that is what GSDevice already knows how to schedule. So the interpolated texcoord becomes a UV computed from the invocation id and the fragment output becomes an imageStore. The crop handling and the widened 0..2 sharpness range come from the Eden Emulator Project's port (GPL-3.0-or-later, compatible with PCSX2's GPL-3.0+); PCSX2 hands the pass a display rectangle inside a larger target, which is the same problem FsrEasuConOffset solves for FSR1. Deliberately a strict subset of what FSR1 already requires -- same descriptor types, same rgba8 storage image, textureGather with a constant component and no offset, which is core Vulkan 1.0 and needs no optional feature. So any driver already running FSR1 can run this, Turnip included, and there is no vendor gate on either. A driver that cannot compile the pipeline clears Features().sgsr and the renderer falls back to plain bilinear with an OSD notice, rather than failing. The Android upscaler control becomes a picker rather than an on/off toggle: three mutually exclusive upscalers expressed as two toggles that silently switch each other off is a worse way to say it than one list. FSR1 and SGSR share the existing sharpness slider -- the number means different things to each, but it is the same intent, and a second slider would only invite the two to disagree. ★ The Settings.kt clamp on the persisted enum was still bounded at UPSCALER_FSR1, and would have silently rewritten any SGSR selection back to Off. That clamp's own comment warns about exactly this failure; it still had to be updated by hand. Worth remembering the next time the enum grows. Suggested by CamilleLaVey, who authored the upstream changes (eden-emu #4293).
2530 lines
89 KiB
C++
2530 lines
89 KiB
C++
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
|
|
// SPDX-License-Identifier: GPL-3.0+
|
|
|
|
#include "GS/Renderers/Common/GSDevice.h"
|
|
#include "GS/Renderers/Common/GSPassScheduler.h"
|
|
#include "GS/GSGL.h"
|
|
#include "GS/GS.h"
|
|
#include "GS/GSUtil.h"
|
|
#include "Host.h"
|
|
|
|
#include "common/Console.h"
|
|
#include "common/BitUtils.h"
|
|
#include "common/FileSystem.h"
|
|
#include "common/HostSys.h"
|
|
#include "common/Path.h"
|
|
#include "common/SmallString.h"
|
|
#include "common/StringUtil.h"
|
|
#include "common/Threading.h"
|
|
|
|
#include "imgui.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <ostream>
|
|
#include <fstream>
|
|
#include <atomic>
|
|
#include <mutex>
|
|
|
|
namespace
|
|
{
|
|
// Shader-chain parameter overrides, handed from the UI thread to the GS thread. See
|
|
// GSDevice::SetShaderChainParams for the contract; the generation is what keeps the
|
|
// per-frame read down to one atomic load on the frame path.
|
|
std::mutex s_shader_param_mutex;
|
|
std::string s_shader_param_preset;
|
|
std::vector<std::pair<std::string, float>> s_shader_params;
|
|
std::atomic<u64> s_shader_param_generation{0};
|
|
} // namespace
|
|
|
|
void GSDevice::SetShaderChainParams(std::string preset, std::vector<std::pair<std::string, float>> params)
|
|
{
|
|
{
|
|
std::unique_lock lock(s_shader_param_mutex);
|
|
s_shader_param_preset = std::move(preset);
|
|
s_shader_params = std::move(params);
|
|
}
|
|
|
|
// Bumped after the store is populated, never before: the GS thread treats a new
|
|
// generation as "the values are ready to read".
|
|
s_shader_param_generation.fetch_add(1, std::memory_order_release);
|
|
}
|
|
|
|
u64 GSDevice::GetShaderChainParamGeneration()
|
|
{
|
|
return s_shader_param_generation.load(std::memory_order_acquire);
|
|
}
|
|
|
|
bool GSDevice::GetShaderChainParams(const std::string& preset, std::vector<std::pair<std::string, float>>* out)
|
|
{
|
|
std::unique_lock lock(s_shader_param_mutex);
|
|
if (s_shader_param_preset != preset || s_shader_params.empty())
|
|
return false;
|
|
|
|
*out = s_shader_params;
|
|
return true;
|
|
}
|
|
|
|
const char* ShaderEntryPoint(ShaderConvert value)
|
|
{
|
|
switch (value)
|
|
{
|
|
// clang-format off
|
|
case ShaderConvert::COPY: return "ps_copy";
|
|
case ShaderConvert::RGB5A1_TO_16_BITS: return "ps_convert_rgb5a1_16bits";
|
|
case ShaderConvert::DATM_1: return "ps_datm1";
|
|
case ShaderConvert::DATM_0: return "ps_datm0";
|
|
case ShaderConvert::DATM_1_RTA_CORRECTION: return "ps_datm1_rta_correction";
|
|
case ShaderConvert::DATM_0_RTA_CORRECTION: return "ps_datm0_rta_correction";
|
|
case ShaderConvert::COLCLIP_INIT: return "ps_colclip_init";
|
|
case ShaderConvert::COLCLIP_RESOLVE: return "ps_colclip_resolve";
|
|
case ShaderConvert::RTA_CORRECTION: return "ps_rta_correction";
|
|
case ShaderConvert::RTA_DECORRECTION: return "ps_rta_decorrection";
|
|
case ShaderConvert::TRANSPARENCY_FILTER: return "ps_filter_transparency";
|
|
case ShaderConvert::DEPTH32_TO_16_BITS: return "ps_convert_depth32_32bits";
|
|
case ShaderConvert::DEPTH32_TO_32_BITS: return "ps_convert_depth32_32bits";
|
|
case ShaderConvert::DEPTH32_TO_RGBA8: return "ps_convert_depth32_rgba8";
|
|
case ShaderConvert::DEPTH32_TO_RGB8: return "ps_convert_depth32_rgba8";
|
|
case ShaderConvert::DEPTH16_TO_RGB5A1: return "ps_convert_depth16_rgb5a1";
|
|
case ShaderConvert::RGBA8_TO_DEPTH32: return "ps_convert_rgba8_depth32";
|
|
case ShaderConvert::RGBA8_TO_DEPTH24: return "ps_convert_rgba8_depth24";
|
|
case ShaderConvert::RGBA8_TO_DEPTH16: return "ps_convert_rgba8_depth16";
|
|
case ShaderConvert::RGB5A1_TO_DEPTH16: return "ps_convert_rgb5a1_depth16";
|
|
case ShaderConvert::DEPTH32_TO_DEPTH24: return "ps_convert_depth32_depth24";
|
|
case ShaderConvert::DEPTH_COPY: return "ps_depth_copy";
|
|
case ShaderConvert::DOWNSAMPLE_COPY: return "ps_downsample_copy";
|
|
case ShaderConvert::RGBA_TO_8I: return "ps_convert_rgba_8i";
|
|
case ShaderConvert::RGB5A1_TO_8I: return "ps_convert_rgb5a1_8i";
|
|
case ShaderConvert::CLUT_4: return "ps_convert_clut_4";
|
|
case ShaderConvert::CLUT_8: return "ps_convert_clut_8";
|
|
case ShaderConvert::YUV: return "ps_yuv";
|
|
// clang-format on
|
|
default:
|
|
pxAssert(0);
|
|
return "ShaderConvertUnknownShader";
|
|
}
|
|
}
|
|
|
|
const char* ShaderEntryPoint(PresentShader value)
|
|
{
|
|
switch (value)
|
|
{
|
|
// clang-format off
|
|
case PresentShader::COPY: return "ps_copy";
|
|
case PresentShader::SCANLINE: return "ps_filter_scanlines";
|
|
case PresentShader::DIAGONAL_FILTER: return "ps_filter_diagonal";
|
|
case PresentShader::TRIANGULAR_FILTER: return "ps_filter_triangular";
|
|
case PresentShader::COMPLEX_FILTER: return "ps_filter_complex";
|
|
case PresentShader::LOTTES_FILTER: return "ps_filter_lottes";
|
|
case PresentShader::SUPERSAMPLE_4xRGSS: return "ps_4x_rgss";
|
|
case PresentShader::SUPERSAMPLE_AUTO: return "ps_automagical_supersampling";
|
|
// clang-format on
|
|
default:
|
|
pxAssert(0);
|
|
return "DisplayShaderUnknownShader";
|
|
}
|
|
}
|
|
|
|
const char* ShaderConvertName(ShaderConvert shader)
|
|
{
|
|
#define ENTRY(x) case ShaderConvert::x: return #x
|
|
switch (shader)
|
|
{
|
|
ENTRY(COPY);
|
|
ENTRY(DEPTH_COPY);
|
|
ENTRY(RGB5A1_TO_16_BITS);
|
|
ENTRY(DATM_1);
|
|
ENTRY(DATM_0);
|
|
ENTRY(DATM_1_RTA_CORRECTION);
|
|
ENTRY(DATM_0_RTA_CORRECTION);
|
|
ENTRY(COLCLIP_INIT);
|
|
ENTRY(COLCLIP_RESOLVE);
|
|
ENTRY(RTA_CORRECTION);
|
|
ENTRY(RTA_DECORRECTION);
|
|
ENTRY(TRANSPARENCY_FILTER);
|
|
ENTRY(DEPTH32_TO_16_BITS);
|
|
ENTRY(DEPTH32_TO_32_BITS);
|
|
ENTRY(DEPTH32_TO_RGBA8);
|
|
ENTRY(DEPTH32_TO_RGB8);
|
|
ENTRY(DEPTH16_TO_RGB5A1);
|
|
ENTRY(RGBA8_TO_DEPTH32);
|
|
ENTRY(RGBA8_TO_DEPTH24);
|
|
ENTRY(RGBA8_TO_DEPTH16);
|
|
ENTRY(RGB5A1_TO_DEPTH16);
|
|
ENTRY(DEPTH32_TO_DEPTH24);
|
|
ENTRY(DOWNSAMPLE_COPY);
|
|
ENTRY(RGBA_TO_8I);
|
|
ENTRY(RGB5A1_TO_8I);
|
|
ENTRY(CLUT_4);
|
|
ENTRY(CLUT_8);
|
|
ENTRY(YUV);
|
|
case ShaderConvert::Count: break;
|
|
}
|
|
#undef ENTRY
|
|
pxAssert(false);
|
|
return nullptr;
|
|
}
|
|
|
|
#ifdef PCSX2_DEVBUILD
|
|
|
|
enum class TextureLabel
|
|
{
|
|
ColorRT,
|
|
ColorHQRT,
|
|
ColorHDRRT,
|
|
ColorClipRT,
|
|
U16RT,
|
|
U32RT,
|
|
DepthStencil,
|
|
PrimIDTexture,
|
|
RWTexture,
|
|
CLUTTexture,
|
|
Texture,
|
|
ReplacementTexture,
|
|
Other,
|
|
Last = Other,
|
|
};
|
|
|
|
static std::array<u32, static_cast<u32>(TextureLabel::Last) + 1> s_texture_counts;
|
|
|
|
static TextureLabel GetTextureLabel(GSTexture::Usage usage, GSTexture::Format format)
|
|
{
|
|
if (GSTexture::IsRenderTarget(usage))
|
|
{
|
|
switch (format)
|
|
{
|
|
case GSTexture::Format::Color:
|
|
return TextureLabel::ColorRT;
|
|
case GSTexture::Format::ColorHQ:
|
|
return TextureLabel::ColorHQRT;
|
|
case GSTexture::Format::ColorHDR:
|
|
return TextureLabel::ColorHDRRT;
|
|
case GSTexture::Format::ColorClip:
|
|
return TextureLabel::ColorClipRT;
|
|
case GSTexture::Format::UInt16:
|
|
return TextureLabel::U16RT;
|
|
case GSTexture::Format::UInt32:
|
|
return TextureLabel::U32RT;
|
|
case GSTexture::Format::PrimID:
|
|
return TextureLabel::PrimIDTexture;
|
|
default:
|
|
return TextureLabel::Other;
|
|
}
|
|
}
|
|
else if (GSTexture::IsTexture(usage))
|
|
{
|
|
switch (format)
|
|
{
|
|
case GSTexture::Format::Color:
|
|
return TextureLabel::Texture;
|
|
case GSTexture::Format::UNorm8:
|
|
return TextureLabel::CLUTTexture;
|
|
case GSTexture::Format::BC1:
|
|
case GSTexture::Format::BC2:
|
|
case GSTexture::Format::BC3:
|
|
case GSTexture::Format::BC7:
|
|
case GSTexture::Format::ColorHDR:
|
|
return TextureLabel::ReplacementTexture;
|
|
default:
|
|
return TextureLabel::Other;
|
|
}
|
|
}
|
|
else if (GSTexture::IsDepthStencil(usage))
|
|
{
|
|
return TextureLabel::DepthStencil;
|
|
}
|
|
else if (GSTexture::IsShaderWrite(usage))
|
|
{
|
|
return TextureLabel::RWTexture;
|
|
}
|
|
else
|
|
{
|
|
return TextureLabel::Other;
|
|
}
|
|
}
|
|
|
|
// Debug names
|
|
static const char* TextureLabelString(TextureLabel label)
|
|
{
|
|
switch (label)
|
|
{
|
|
case TextureLabel::ColorRT:
|
|
return "Color RT";
|
|
case TextureLabel::ColorHQRT:
|
|
return "Color HQ RT";
|
|
case TextureLabel::ColorHDRRT:
|
|
return "Color HDR RT";
|
|
case TextureLabel::ColorClipRT:
|
|
return "Color Clip RT";
|
|
case TextureLabel::U16RT:
|
|
return "U16 RT";
|
|
case TextureLabel::U32RT:
|
|
return "U32 RT";
|
|
case TextureLabel::DepthStencil:
|
|
return "Depth Stencil";
|
|
case TextureLabel::PrimIDTexture:
|
|
return "PrimID";
|
|
case TextureLabel::RWTexture:
|
|
return "RW Texture";
|
|
case TextureLabel::CLUTTexture:
|
|
return "CLUT Texture";
|
|
case TextureLabel::Texture:
|
|
return "Texture";
|
|
case TextureLabel::ReplacementTexture:
|
|
return "Replacement Texture";
|
|
case TextureLabel::Other:
|
|
default:
|
|
return "Unknown Texture";
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
std::unique_ptr<GSDevice> g_gs_device;
|
|
|
|
GSDevice::GSDevice()
|
|
: m_pass_scheduler(std::make_unique<GSPassScheduler>())
|
|
{
|
|
#ifdef PCSX2_DEVBUILD
|
|
s_texture_counts.fill(0);
|
|
#endif
|
|
}
|
|
|
|
GSDevice::~GSDevice()
|
|
{
|
|
// should've been cleaned up in Destroy()
|
|
pxAssert(m_pool[0].empty() && m_pool[1].empty() && !m_merge && !m_weavebob && !m_blend && !m_mad && !m_target_tmp && !m_cas && !m_mfx_output && !m_fsr1_easu && !m_fsr1_output && !m_sgsr_output);
|
|
}
|
|
|
|
GSVector2i GSDevice::GetPresentationSize() const
|
|
{
|
|
const s32 w = GetWindowWidth();
|
|
const s32 h = GetWindowHeight();
|
|
return (GSConfig.Rotation == DisplayRotation::Rot90 || GSConfig.Rotation == DisplayRotation::Rot270)
|
|
? GSVector2i(h, w)
|
|
: GSVector2i(w, h);
|
|
}
|
|
|
|
const char* GSDevice::RenderAPIToString(RenderAPI api)
|
|
{
|
|
switch (api)
|
|
{
|
|
// clang-format off
|
|
#define CASE(x) case RenderAPI::x: return #x
|
|
CASE(None);
|
|
CASE(D3D11);
|
|
CASE(D3D12);
|
|
CASE(Metal);
|
|
CASE(Vulkan);
|
|
CASE(OpenGL);
|
|
#undef CASE
|
|
// clang-format on
|
|
default:
|
|
return "Unknown";
|
|
}
|
|
}
|
|
|
|
bool GSDevice::GetRequestedExclusiveFullscreenMode(u32* width, u32* height, float* refresh_rate)
|
|
{
|
|
const std::string mode = Host::GetStringSettingValue("EmuCore/GS", "FullscreenMode", "");
|
|
if (!mode.empty())
|
|
{
|
|
const std::string_view mode_view = mode;
|
|
std::string_view::size_type sep1 = mode.find('x');
|
|
if (sep1 != std::string_view::npos)
|
|
{
|
|
std::optional<u32> owidth = StringUtil::FromChars<u32>(mode_view.substr(0, sep1));
|
|
sep1++;
|
|
|
|
while (sep1 < mode.length() && std::isspace(mode[sep1]))
|
|
sep1++;
|
|
|
|
if (owidth.has_value() && sep1 < mode.length())
|
|
{
|
|
std::string_view::size_type sep2 = mode.find('@', sep1);
|
|
if (sep2 != std::string_view::npos)
|
|
{
|
|
std::optional<u32> oheight = StringUtil::FromChars<u32>(mode_view.substr(sep1, sep2 - sep1));
|
|
sep2++;
|
|
|
|
while (sep2 < mode.length() && std::isspace(mode[sep2]))
|
|
sep2++;
|
|
|
|
if (oheight.has_value() && sep2 < mode.length())
|
|
{
|
|
std::optional<float> orefresh_rate = StringUtil::FromChars<float>(mode_view.substr(sep2));
|
|
if (orefresh_rate.has_value())
|
|
{
|
|
*width = owidth.value();
|
|
*height = oheight.value();
|
|
*refresh_rate = orefresh_rate.value();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
*width = 0;
|
|
*height = 0;
|
|
*refresh_rate = 0;
|
|
return false;
|
|
}
|
|
|
|
std::string GSDevice::GetFullscreenModeString(u32 width, u32 height, float refresh_rate)
|
|
{
|
|
return StringUtil::StdStringFromFormat("%u x %u @ %f Hz", width, height, refresh_rate);
|
|
}
|
|
|
|
void GSDevice::GenerateExpansionIndexBuffer(void* buffer)
|
|
{
|
|
static constexpr u32 MAX_INDEX = EXPAND_BUFFER_SIZE / 6 / sizeof(u16);
|
|
|
|
u16* idx_buffer = static_cast<u16*>(buffer);
|
|
for (u32 i = 0; i < MAX_INDEX; i++)
|
|
{
|
|
const u32 base = i * 4;
|
|
*(idx_buffer++) = base + 0;
|
|
*(idx_buffer++) = base + 1;
|
|
*(idx_buffer++) = base + 2;
|
|
*(idx_buffer++) = base + 1;
|
|
*(idx_buffer++) = base + 2;
|
|
*(idx_buffer++) = base + 3;
|
|
}
|
|
}
|
|
|
|
GSVector4i GSDevice::ProcessCopyArea(const GSVector4i& rtsize, const GSVector4i& drawarea)
|
|
{
|
|
GSVector4i snapped_drawarea(drawarea);
|
|
// We don't want the snapped box adjustments when the rect is empty as it might make the copy to pass.
|
|
// The empty rect itself needs to be handled in renderer properly.
|
|
if (snapped_drawarea.rempty())
|
|
return snapped_drawarea;
|
|
|
|
// If copy area exceeds 95% coverage then we can do a full copy instead which should be faster.
|
|
const float rt_area = static_cast<float>(rtsize.width() * rtsize.height());
|
|
const float copy_area = static_cast<float>(drawarea.width() * drawarea.height());
|
|
constexpr float coverage = 0.95f;
|
|
if ((copy_area / rt_area) >= coverage)
|
|
{
|
|
snapped_drawarea = rtsize;
|
|
return snapped_drawarea;
|
|
}
|
|
|
|
// Aligning bbox to 4 pixel boundaries so copies will be faster using Direct Memory Access,
|
|
// otherwise it may stall as more commands need to be issued.
|
|
snapped_drawarea = snapped_drawarea.ralign<Align_Outside>(GSVector2i(4, 4)).rintersect(rtsize);
|
|
|
|
return snapped_drawarea;
|
|
}
|
|
|
|
std::optional<std::string> GSDevice::ReadShaderSource(const char* filename)
|
|
{
|
|
return FileSystem::ReadFileToString(Path::Combine(EmuFolders::Resources, filename).c_str());
|
|
}
|
|
|
|
int GSDevice::GetMipmapLevelsForSize(int width, int height)
|
|
{
|
|
return std::min(static_cast<int>(std::log2(std::max(width, height))) + 1, MAXIMUM_TEXTURE_MIPMAP_LEVELS);
|
|
}
|
|
|
|
bool GSDevice::Create(GSVSyncMode vsync_mode, bool allow_present_throttle)
|
|
{
|
|
m_vsync_mode = vsync_mode;
|
|
m_allow_present_throttle = allow_present_throttle;
|
|
return true;
|
|
}
|
|
|
|
void GSDevice::Destroy()
|
|
{
|
|
// Drop rather than emit: nothing is going to present these, and the targets they name
|
|
// are about to be destroyed.
|
|
m_pass_scheduler->Clear();
|
|
m_deferred_draw_count = 0;
|
|
|
|
// Nothing references these any more, and PurgePool() below deletes whatever the pool
|
|
// holds - so putting them back is how they get freed.
|
|
std::vector<GSTexture*> pending;
|
|
pending.swap(m_deferred_recycle);
|
|
for (GSTexture* tex : pending)
|
|
Recycle(tex);
|
|
|
|
ClearCurrent();
|
|
PurgePool();
|
|
}
|
|
|
|
bool GSDevice::AcquireWindow(bool recreate_window)
|
|
{
|
|
std::optional<WindowInfo> wi = Host::AcquireRenderWindow(recreate_window);
|
|
if (!wi.has_value())
|
|
{
|
|
Console.Error("Failed to acquire render window.");
|
|
Host::ReportErrorAsync("Error", "Failed to acquire render window. The log may have more information.");
|
|
return false;
|
|
}
|
|
|
|
m_window_info = std::move(wi.value());
|
|
return true;
|
|
}
|
|
|
|
bool GSDevice::ShouldSkipPresentingFrame()
|
|
{
|
|
// Only needed with FIFO.
|
|
if (!m_allow_present_throttle || m_vsync_mode != GSVSyncMode::FIFO)
|
|
return false;
|
|
|
|
const float throttle_rate = (m_window_info.surface_refresh_rate > 0.0f) ? m_window_info.surface_refresh_rate : 60.0f;
|
|
const u64 throttle_period = static_cast<u64>(static_cast<double>(GetTickFrequency()) / static_cast<double>(throttle_rate));
|
|
|
|
const u64 now = GetCPUTicks();
|
|
const double diff = now - m_last_frame_displayed_time;
|
|
if (diff < throttle_period)
|
|
return true;
|
|
|
|
m_last_frame_displayed_time = now;
|
|
return false;
|
|
}
|
|
|
|
void GSDevice::ThrottlePresentation()
|
|
{
|
|
// Manually throttle presentation when vsync isn't enabled, so we don't try to render the
|
|
// fullscreen UI at thousands of FPS and make the gpu go brrrrrrrr.
|
|
const float throttle_rate = (m_window_info.surface_refresh_rate > 0.0f) ? m_window_info.surface_refresh_rate : 60.0f;
|
|
|
|
const u64 sleep_period = static_cast<u64>(static_cast<double>(GetTickFrequency()) / static_cast<double>(throttle_rate));
|
|
const u64 current_ts = GetCPUTicks();
|
|
|
|
// Allow it to fall behind/run ahead up to 2*period. Sleep isn't that precise, plus we need to
|
|
// allow time for the actual rendering.
|
|
const u64 max_variance = sleep_period * 2;
|
|
if (static_cast<u64>(std::abs(static_cast<s64>(current_ts - m_last_frame_displayed_time))) > max_variance)
|
|
m_last_frame_displayed_time = current_ts + sleep_period;
|
|
else
|
|
m_last_frame_displayed_time += sleep_period;
|
|
|
|
Threading::SleepUntil(m_last_frame_displayed_time);
|
|
}
|
|
|
|
void GSDevice::ClearRenderTarget(GSTexture* t, u32 c)
|
|
{
|
|
FlushDeferredDrawsFor(t);
|
|
t->SetClearColor(c);
|
|
}
|
|
|
|
void GSDevice::ClearDepth(GSTexture* t, float d)
|
|
{
|
|
FlushDeferredDrawsFor(t);
|
|
t->SetClearDepth(d);
|
|
}
|
|
|
|
void GSDevice::DoHintReadbackSource(GSTexture* tex)
|
|
{
|
|
// Default: no scheduling hint. See GSDeviceVK for a backend that uses it.
|
|
}
|
|
|
|
bool GSDevice::ProcessClearsBeforeCopy(GSTexture* sTex, GSTexture* dTex, const bool full_copy)
|
|
{
|
|
FlushDeferredDraws();
|
|
pxAssert(sTex->GetState() == GSTexture::State::Cleared && dTex->IsRenderTargetOrDepthStencil());
|
|
|
|
// Pass it forward if we're clearing the whole thing.
|
|
if (full_copy)
|
|
{
|
|
if (dTex->IsDepthStencil())
|
|
dTex->SetClearDepth(sTex->GetClearDepth());
|
|
else
|
|
dTex->SetClearColor(sTex->GetClearColor());
|
|
|
|
dTex->SetState(GSTexture::State::Cleared);
|
|
|
|
return true;
|
|
}
|
|
|
|
// Destination is cleared, if it's the same colour and rect, we can just avoid this entirely.
|
|
if (dTex->GetState() == GSTexture::State::Cleared)
|
|
{
|
|
if (dTex->IsDepthStencil())
|
|
{
|
|
if (dTex->GetClearDepth() == sTex->GetClearDepth())
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
if (dTex->GetClearColor() == sTex->GetClearColor())
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void GSDevice::InvalidateRenderTarget(GSTexture* t)
|
|
{
|
|
FlushDeferredDrawsFor(t);
|
|
t->SetState(GSTexture::State::Invalidated);
|
|
}
|
|
|
|
void GSDevice::UpdateImGuiTextures()
|
|
{
|
|
// TODO, use ImDrawData https://github.com/ocornut/imgui/issues/8597#issuecomment-2871835598
|
|
for (ImTextureData* im_tex : ImGui::GetPlatformIO().Textures)
|
|
{
|
|
switch (im_tex->Status)
|
|
{
|
|
case ImTextureStatus_OK:
|
|
case ImTextureStatus_Destroyed:
|
|
continue;
|
|
case ImTextureStatus_WantCreate:
|
|
if (GSTexture* gs_tex = g_gs_device->CreateTexture(im_tex->Width, im_tex->Height, 1, GSTexture::Format::Color))
|
|
{
|
|
im_tex->SetTexID(reinterpret_cast<ImTextureID>(gs_tex->GetNativeHandle()));
|
|
im_tex->BackendUserData = gs_tex;
|
|
}
|
|
else
|
|
{
|
|
pxFailRel("Failed to create ImGui texture");
|
|
break;
|
|
}
|
|
[[fallthrough]];
|
|
case ImTextureStatus_WantUpdates:
|
|
if (GSTexture* gs_tex = static_cast<GSTexture*>(im_tex->BackendUserData))
|
|
{
|
|
// If we fell through from WantCreate, then we are uploading the full size
|
|
// Otherwise, we are just updating the specified region
|
|
// clange-format off
|
|
const int upload_x = (im_tex->Status == ImTextureStatus_WantCreate) ? 0 : im_tex->UpdateRect.x;
|
|
const int upload_y = (im_tex->Status == ImTextureStatus_WantCreate) ? 0 : im_tex->UpdateRect.y;
|
|
const int upload_w = (im_tex->Status == ImTextureStatus_WantCreate) ? im_tex->Width : im_tex->UpdateRect.w;
|
|
const int upload_h = (im_tex->Status == ImTextureStatus_WantCreate) ? im_tex->Height : im_tex->UpdateRect.h;
|
|
const int upload_pitch = upload_w * im_tex->BytesPerPixel;
|
|
// clange-format on
|
|
|
|
const GSVector4i rect{
|
|
upload_x,
|
|
upload_y,
|
|
upload_x + upload_w,
|
|
upload_y + upload_h,
|
|
};
|
|
|
|
GSTexture::GSMap map;
|
|
if (gs_tex->Map(map, &rect))
|
|
{
|
|
for (int y = 0; y < upload_h; y++)
|
|
std::memcpy(map.bits + map.pitch * y, im_tex->GetPixelsAt(rect.x, rect.y + y), upload_pitch);
|
|
|
|
gs_tex->Unmap();
|
|
}
|
|
else
|
|
{
|
|
for (int y = 0; y < upload_h; y++)
|
|
gs_tex->Update({rect.left, rect.top + y, rect.right, rect.top + y + 1},
|
|
im_tex->GetPixelsAt(rect.x, rect.y + y), upload_pitch);
|
|
}
|
|
|
|
im_tex->Status = ImTextureStatus_OK;
|
|
}
|
|
break;
|
|
case ImTextureStatus_WantDestroy:
|
|
if (GSTexture* gs_tex = static_cast<GSTexture*>(im_tex->BackendUserData))
|
|
{
|
|
// While it's unlikely we're going to reuse the same size as imgui for rendering,
|
|
// imgui may request a new atlas of the same size if old font sizes are evicted.
|
|
Recycle(gs_tex);
|
|
|
|
im_tex->SetTexID(ImTextureID_Invalid);
|
|
im_tex->BackendUserData = nullptr;
|
|
im_tex->Status = ImTextureStatus_Destroyed;
|
|
}
|
|
break;
|
|
default:
|
|
pxAssert(false);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void GSDevice::DestroyImGuiTextures()
|
|
{
|
|
if (!ImGui::GetCurrentContext())
|
|
return;
|
|
|
|
for (ImTextureData* im_tex : ImGui::GetPlatformIO().Textures)
|
|
{
|
|
if (im_tex->Status != ImTextureStatus_Destroyed)
|
|
{
|
|
GSTexture* gs_tex = static_cast<GSTexture*>(im_tex->BackendUserData);
|
|
if (gs_tex == nullptr)
|
|
continue;
|
|
|
|
Recycle(gs_tex);
|
|
|
|
pxAssert(im_tex->RefCount == 1);
|
|
|
|
im_tex->SetTexID(ImTextureID_Invalid);
|
|
im_tex->BackendUserData = nullptr;
|
|
im_tex->Status = ImTextureStatus_Destroyed;
|
|
}
|
|
}
|
|
}
|
|
|
|
void GSDevice::TextureRecycleDeleter::operator()(GSTexture* const tex)
|
|
{
|
|
g_gs_device->Recycle(tex);
|
|
}
|
|
|
|
GSTexture* GSDevice::FetchSurface(GSTexture::Usage usage, const GSVector2i& size, int levels, GSTexture::Format format, bool clear, bool prefer_reuse)
|
|
{
|
|
return FetchSurface(usage, size.x, size.y, levels, format, clear, prefer_reuse);
|
|
}
|
|
|
|
GSTexture* GSDevice::FetchSurface(GSTexture::Usage usage, int width, int height, int levels, GSTexture::Format format, bool clear, bool prefer_reuse)
|
|
{
|
|
// No blanket flush here: what comes back is either brand new or was recycled into the
|
|
// pool. The deferred-clear calls at the tail guard themselves.
|
|
const GSVector2i size(std::clamp(width, 1, static_cast<int>(g_gs_device->GetMaxTextureSize())),
|
|
std::clamp(height, 1, static_cast<int>(g_gs_device->GetMaxTextureSize())));
|
|
|
|
// Recycle() parks a texture a queued draw still names instead of returning it to the pool.
|
|
// That is invisible to correctness but not to allocation: if the surface being asked for is
|
|
// one of those, skipping the flush hands back a different texture than an undeferred run
|
|
// would, and the working set stays one larger for the rest of the frame. On God of War II
|
|
// that alone cost +7 render passes. Drain the queue when the answer is in that list, and
|
|
// only then - an unconditional flush here is what the scheduler exists to avoid.
|
|
if (!m_deferred_recycle.empty() && !m_flushing)
|
|
{
|
|
for (const GSTexture* held : m_deferred_recycle)
|
|
{
|
|
if (held->GetUsage() == usage && held->GetFormat() == format && held->GetSize() == size &&
|
|
held->GetMipmapLevels() == levels)
|
|
{
|
|
FlushDeferredDraws();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
FastList<GSTexture*>& pool = m_pool[!GSTexture::IsTexture(usage)];
|
|
|
|
GSTexture* t = nullptr;
|
|
auto fallback = pool.end();
|
|
|
|
for (auto i = pool.begin(); i != pool.end(); ++i)
|
|
{
|
|
t = *i;
|
|
|
|
pxAssert(t);
|
|
|
|
if (t->GetUsage() == usage && t->GetFormat() == format && t->GetSize() == size && t->GetMipmapLevels() == levels)
|
|
{
|
|
if (prefer_reuse || t->GetLastFrameUsed() != m_frame)
|
|
{
|
|
m_pool_memory_usage -= t->GetMemUsage();
|
|
pool.erase(i);
|
|
break;
|
|
}
|
|
else if (fallback == pool.end())
|
|
{
|
|
fallback = i;
|
|
}
|
|
}
|
|
|
|
t = nullptr;
|
|
}
|
|
|
|
if (!t)
|
|
{
|
|
if (pool.size() >= GetPoolLimit(GSTexture::IsTexture(usage)) && fallback != pool.end())
|
|
{
|
|
t = *fallback;
|
|
m_pool_memory_usage -= t->GetMemUsage();
|
|
pool.erase(fallback);
|
|
}
|
|
else
|
|
{
|
|
t = CreateSurface(usage, size.x, size.y, levels, format);
|
|
if (!t)
|
|
{
|
|
ERROR_LOG("GS: Memory allocation failure for {}x{} texture. Purging pool and retrying.", size.x, size.y);
|
|
PurgePool();
|
|
t = CreateSurface(usage, size.x, size.y, levels, format);
|
|
if (!t)
|
|
{
|
|
ERROR_LOG("GS: Memory allocation failure for {}x{} texture after purging pool.", size.x, size.y);
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
#ifdef PCSX2_DEVBUILD
|
|
if (GSConfig.UseDebugDevice)
|
|
{
|
|
const TextureLabel label = GetTextureLabel(usage, format);
|
|
const u32 id = ++s_texture_counts[static_cast<u32>(label)];
|
|
t->SetDebugName(TinyString::from_format("{} {}", TextureLabelString(label), id));
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
if (t->IsRenderTarget())
|
|
{
|
|
if (clear)
|
|
ClearRenderTarget(t, 0);
|
|
else
|
|
InvalidateRenderTarget(t);
|
|
}
|
|
else if (t->IsDepthStencil())
|
|
{
|
|
if (clear)
|
|
ClearDepth(t, 0.0f);
|
|
else
|
|
InvalidateRenderTarget(t);
|
|
}
|
|
|
|
return t;
|
|
}
|
|
|
|
void GSDevice::Recycle(GSTexture* t)
|
|
{
|
|
if (!t)
|
|
return;
|
|
|
|
// Holding the texture back is much cheaper than flushing for it. The texture cache has
|
|
// dropped its reference, so nobody but the queue can still name it, and the queue is
|
|
// drained before this list is.
|
|
if (m_deferred_draw_count != 0 && !m_flushing && DeferredDrawsReference(t))
|
|
{
|
|
m_deferred_recycle.push_back(t);
|
|
return;
|
|
}
|
|
|
|
t->SetLastFrameUsed(m_frame);
|
|
|
|
#ifdef PCSX2_DEVBUILD
|
|
t->SetDebugName("");
|
|
#endif
|
|
|
|
FastList<GSTexture*>& pool = m_pool[!t->IsTexture()];
|
|
pool.push_front(t);
|
|
m_pool_memory_usage += t->GetMemUsage();
|
|
|
|
const u32 max_size = GetPoolLimit(t->IsTexture());
|
|
const u32 max_age = GetPoolMaxAge(t->IsTexture());
|
|
while (pool.size() > max_size)
|
|
{
|
|
// Don't toss when the texture was last used in this frame.
|
|
// Because we're going to need to keep it alive anyway.
|
|
GSTexture* back = pool.back();
|
|
if ((m_frame - back->GetLastFrameUsed()) < max_age)
|
|
break;
|
|
|
|
m_pool_memory_usage -= back->GetMemUsage();
|
|
delete back;
|
|
|
|
pool.pop_back();
|
|
}
|
|
}
|
|
|
|
// The pool budget is per-GPU on mobile: the GPU-profile tables resolve a MobileGsTuning
|
|
// whose pool sizes and ages are sized for the part (an Adreno 200 wants ~48 pooled, a
|
|
// laptop-class Adreno X ~160), instead of the one-size 300 that suits a desktop GPU.
|
|
// Ported from sashkinbro/EmuCoreX, including its __ANDROID__ scoping: off Android the
|
|
// profile is never resolved, so the tuning would still hold its conservative default
|
|
// (96/8/96/6) and would silently cut the desktop pool from 300 to 96.
|
|
u32 GSDevice::GetPoolLimit(bool texture) const
|
|
{
|
|
#if defined(__ANDROID__)
|
|
return texture ? m_mobile_gs_tuning.pooled_textures : m_mobile_gs_tuning.pooled_targets;
|
|
#else
|
|
return texture ? MAX_POOLED_TEXTURES : MAX_POOLED_TARGETS;
|
|
#endif
|
|
}
|
|
|
|
u32 GSDevice::GetPoolMaxAge(bool texture) const
|
|
{
|
|
#if defined(__ANDROID__)
|
|
return texture ? m_mobile_gs_tuning.texture_age : m_mobile_gs_tuning.target_age;
|
|
#else
|
|
return texture ? MAX_TEXTURE_AGE : MAX_TARGET_AGE;
|
|
#endif
|
|
}
|
|
|
|
bool GSDevice::UsesLowerLeftOrigin() const
|
|
{
|
|
const RenderAPI api = GetRenderAPI();
|
|
return (api == RenderAPI::OpenGL);
|
|
}
|
|
|
|
void GSDevice::AgePool()
|
|
{
|
|
FlushDeferredDraws();
|
|
m_frame++;
|
|
|
|
// Toss out textures when they're not too-recently used.
|
|
for (u32 pool_idx = 0; pool_idx < m_pool.size(); pool_idx++)
|
|
{
|
|
const u32 max_age = GetPoolMaxAge(pool_idx == 0);
|
|
FastList<GSTexture*>& pool = m_pool[pool_idx];
|
|
while (!pool.empty())
|
|
{
|
|
GSTexture* back = pool.back();
|
|
if ((m_frame - back->GetLastFrameUsed()) < max_age)
|
|
break;
|
|
|
|
m_pool_memory_usage -= back->GetMemUsage();
|
|
delete back;
|
|
|
|
pool.pop_back();
|
|
}
|
|
}
|
|
}
|
|
|
|
void GSDevice::AgePoolAfterPresentCapSkip()
|
|
{
|
|
FlushDeferredDraws();
|
|
m_frame++;
|
|
|
|
// Frame age and deferred draw ordering remain per-frame. Only the deletion
|
|
// scan is amortized, and its four-frame bound prevents retained pool growth.
|
|
static constexpr u32 MAX_CLEANUP_DEFERRAL = 4;
|
|
if (++m_frames_since_pool_cleanup < MAX_CLEANUP_DEFERRAL)
|
|
return;
|
|
m_frames_since_pool_cleanup = 0;
|
|
|
|
// Toss out textures when they're not too-recently used.
|
|
for (u32 pool_idx = 0; pool_idx < m_pool.size(); pool_idx++)
|
|
{
|
|
const u32 max_age = GetPoolMaxAge(pool_idx == 0);
|
|
FastList<GSTexture*>& pool = m_pool[pool_idx];
|
|
while (!pool.empty())
|
|
{
|
|
GSTexture* back = pool.back();
|
|
if ((m_frame - back->GetLastFrameUsed()) < max_age)
|
|
break;
|
|
|
|
m_pool_memory_usage -= back->GetMemUsage();
|
|
delete back;
|
|
|
|
pool.pop_back();
|
|
}
|
|
}
|
|
}
|
|
|
|
void GSDevice::PurgePool()
|
|
{
|
|
FlushDeferredDraws();
|
|
m_frames_since_pool_cleanup = 0;
|
|
for (FastList<GSTexture*>& pool : m_pool)
|
|
{
|
|
for (GSTexture* t : pool)
|
|
delete t;
|
|
pool.clear();
|
|
}
|
|
m_pool_memory_usage = 0;
|
|
}
|
|
|
|
GSTexture* GSDevice::CreateRenderTarget(int w, int h, GSTexture::Format format, bool clear, bool prefer_reuse)
|
|
{
|
|
return FetchSurface(GSTexture::RenderTarget, w, h, 1, format, clear, prefer_reuse);
|
|
}
|
|
|
|
GSTexture* GSDevice::CreateRenderTarget(const GSVector2i& size, GSTexture::Format format, bool clear, bool prefer_reuse)
|
|
{
|
|
return FetchSurface(GSTexture::RenderTarget, size.x, size.y, 1, format, clear, prefer_reuse);
|
|
}
|
|
|
|
GSTexture* GSDevice::CreateFeedbackTarget(int w, int h, GSTexture::Format format, bool clear, bool prefer_reuse)
|
|
{
|
|
return FetchSurface(GSTexture::FeedbackTarget, w, h, 1, format, clear, prefer_reuse);
|
|
}
|
|
|
|
GSTexture* GSDevice::CreateFeedbackTarget(const GSVector2i& size, GSTexture::Format format, bool clear, bool prefer_reuse)
|
|
{
|
|
return FetchSurface(GSTexture::FeedbackTarget, size.x, size.y, 1, format, clear, prefer_reuse);
|
|
}
|
|
|
|
GSTexture* GSDevice::CreateShaderWriteTarget(int w, int h, GSTexture::Format format, bool clear, bool prefer_reuse)
|
|
{
|
|
return FetchSurface(GSTexture::ShaderWriteTarget, w, h, 1, format, clear, prefer_reuse);
|
|
}
|
|
|
|
GSTexture* GSDevice::CreateShaderWriteTarget(const GSVector2i& size, GSTexture::Format format, bool clear, bool prefer_reuse)
|
|
{
|
|
return FetchSurface(GSTexture::ShaderWriteTarget, size.x, size.y, 1, format, clear, prefer_reuse);
|
|
}
|
|
|
|
GSTexture::Usage GSDevice::GetDepthStencilUsage() const
|
|
{
|
|
return m_features.depth_feedback ? GSTexture::FeedbackDepth : GSTexture::DepthStencil;
|
|
}
|
|
|
|
GSTexture* GSDevice::CreateDepthStencil(int w, int h, bool clear, bool prefer_reuse)
|
|
{
|
|
return FetchSurface(GetDepthStencilUsage(), w, h, 1, GSTexture::Format::DepthStencil, clear, prefer_reuse);
|
|
}
|
|
|
|
GSTexture* GSDevice::CreateDepthStencil(const GSVector2i& size, bool clear, bool prefer_reuse)
|
|
{
|
|
return FetchSurface(GetDepthStencilUsage(), size.x, size.y, 1, GSTexture::Format::DepthStencil, clear, prefer_reuse);
|
|
}
|
|
|
|
GSTexture* GSDevice::CreateTexture(int w, int h, int mipmap_levels, GSTexture::Format format, bool prefer_reuse)
|
|
{
|
|
pxAssert(mipmap_levels != 0 && (mipmap_levels < 0 || mipmap_levels <= GetMipmapLevelsForSize(w, h)));
|
|
const int levels = mipmap_levels < 0 ? GetMipmapLevelsForSize(w, h) : mipmap_levels;
|
|
return FetchSurface(GSTexture::Texture, w, h, levels, format, false, m_features.prefer_new_textures && prefer_reuse);
|
|
}
|
|
|
|
GSTexture* GSDevice::CreateTexture(const GSVector2i& size, int mipmap_levels, GSTexture::Format format, bool prefer_reuse)
|
|
{
|
|
return CreateTexture(size.x, size.y, mipmap_levels, format, prefer_reuse);
|
|
}
|
|
|
|
GSTexture* GSDevice::CreateCompatible(GSTexture* tex, bool clear, bool prefer_reuse)
|
|
{
|
|
return CreateCompatible(tex, tex->GetWidth(), tex->GetHeight(), clear, prefer_reuse);
|
|
}
|
|
|
|
GSTexture* GSDevice::CreateCompatible(GSTexture* tex, const GSVector2i& size, bool clear, bool prefer_reuse)
|
|
{
|
|
return CreateCompatible(tex, size.x, size.y, clear, prefer_reuse);
|
|
}
|
|
|
|
GSTexture* GSDevice::CreateCompatible(GSTexture* tex, int w, int h, bool clear, bool prefer_reuse)
|
|
{
|
|
return FetchSurface(tex->GetUsage(), w, h, 1, tex->GetFormat(), clear, prefer_reuse);
|
|
}
|
|
|
|
void GSDevice::DoStretchRectWithAssertions(GSTexture* sTex, const GSVector4& sRect, GSTexture* dTex,
|
|
const GSVector4& dRect, ShaderConvertSelector shader, Filter filter)
|
|
{
|
|
pxAssert((dTex && dTex->IsDepthLike()) == shader.Float32Output());
|
|
pxAssert(!(filter == Biln && shader.SupportsBilinear())); // Don't allow HW bilinear if SW bilinear is required.
|
|
FlushDeferredDraws();
|
|
GL_INS("StretchRect(%s) {%d,%d} %dx%d -> {%d,%d) %dx%d", ShaderConvertName(shader.Shader()),
|
|
int(sRect.left), int(sRect.top),
|
|
int(sRect.right - sRect.left), int(sRect.bottom - sRect.top), int(dRect.left), int(dRect.top),
|
|
int(dRect.right - dRect.left), int(dRect.bottom - dRect.top));
|
|
DoStretchRect(sTex, sRect, dTex, dRect, shader, filter);
|
|
}
|
|
|
|
// Resolves a StretchRect edge onto the texel grid. Both coordinate spaces reach us as integer
|
|
// rects that were divided and re-multiplied by a texture dimension along the way, so the value
|
|
// we see is the intended integer plus a few ULPs of round-trip error. Anything further off the
|
|
// grid than that is a deliberate offset -- a half-texel inset, say -- and has to keep going
|
|
// through the shader, which is why the tolerance is far below the smallest offset anyone means.
|
|
static bool SnapStretchRectEdgeToTexel(float v, s32& out)
|
|
{
|
|
constexpr float tolerance = 1.0f / 512.0f;
|
|
const float rounded = std::round(v);
|
|
out = static_cast<s32>(rounded);
|
|
return std::abs(v - rounded) <= tolerance;
|
|
}
|
|
|
|
bool GSDevice::TryStretchRectAsCopy(GSTexture* sTex, const GSVector4& sRect, GSTexture* dTex,
|
|
const GSVector4& dRect, ShaderConvertSelector shader)
|
|
{
|
|
// Only a plain copy is equivalent. Anything that reformats, rewrites channels or moves
|
|
// colour into depth needs the shader that was asked for.
|
|
const ShaderConvert sh = shader.Shader();
|
|
if ((sh != ShaderConvert::COPY && sh != ShaderConvert::DEPTH_COPY) || shader.Mask() != 0xf)
|
|
return false;
|
|
|
|
if (!sTex || !dTex || sTex == dTex || sTex->GetFormat() != dTex->GetFormat())
|
|
return false;
|
|
|
|
// Copies are per-aspect, so a colour-usage texture and a depth-usage one can't be copied
|
|
// between even when their formats agree.
|
|
if (sTex->IsDepthStencil() != dTex->IsDepthStencil())
|
|
return false;
|
|
|
|
// A source that is a pending clear or is invalidated has no contents to copy, and the two
|
|
// paths resolve that from opposite ends -- the draw path carries the clear into the
|
|
// destination's load op, the copy path commits it to the source first. Neither is what this
|
|
// exists for, so leave them where they already work.
|
|
if (sTex->GetState() != GSTexture::State::Dirty)
|
|
return false;
|
|
|
|
const GSVector2i ssize = sTex->GetSize();
|
|
const GSVector2i dsize = dTex->GetSize();
|
|
|
|
// Source coordinates arrive normalized, destination coordinates in pixels.
|
|
s32 sx, sy, sz, sw, dx, dy, dz, dw;
|
|
if (!SnapStretchRectEdgeToTexel(sRect.x * static_cast<float>(ssize.x), sx) ||
|
|
!SnapStretchRectEdgeToTexel(sRect.y * static_cast<float>(ssize.y), sy) ||
|
|
!SnapStretchRectEdgeToTexel(sRect.z * static_cast<float>(ssize.x), sz) ||
|
|
!SnapStretchRectEdgeToTexel(sRect.w * static_cast<float>(ssize.y), sw) ||
|
|
!SnapStretchRectEdgeToTexel(dRect.x, dx) || !SnapStretchRectEdgeToTexel(dRect.y, dy) ||
|
|
!SnapStretchRectEdgeToTexel(dRect.z, dz) || !SnapStretchRectEdgeToTexel(dRect.w, dw))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// 1:1 only -- a scaled copy is a resample, and then the filter the caller asked for matters.
|
|
// At 1:1 every sample lands dead centre on its texel, so Nearest and Biln agree with each
|
|
// other and with the copy, which is why the filter isn't consulted here.
|
|
if ((sz - sx) != (dz - dx) || (sw - sy) != (dw - dy) || sz <= sx || sw <= sy)
|
|
return false;
|
|
|
|
// The draw path scissors an out-of-bounds destination and clamps out-of-bounds source
|
|
// coordinates to the edge texel. A copy can do neither, so those stay with the shader.
|
|
if (sx < 0 || sy < 0 || sz > ssize.x || sw > ssize.y || dx < 0 || dy < 0 || dz > dsize.x || dw > dsize.y)
|
|
return false;
|
|
|
|
GL_INS("StretchRect(%s) served as copy: {%d,%d} %dx%d -> {%d,%d}", ShaderConvertName(sh), sx, sy, sz - sx,
|
|
sw - sy, dx, dy);
|
|
|
|
CopyRect(sTex, dTex, GSVector4i(sx, sy, sz, sw), static_cast<u32>(dx), static_cast<u32>(dy));
|
|
return true;
|
|
}
|
|
|
|
void GSDevice::StretchRect(GSTexture* sTex, const GSVector4& sRect, GSTexture* dTex, const GSVector4& dRect,
|
|
ShaderConvertSelector shader, Filter filter)
|
|
{
|
|
if (TryStretchRectAsCopy(sTex, sRect, dTex, dRect, shader))
|
|
return;
|
|
|
|
DoStretchRectWithAssertions(sTex, sRect, dTex, dRect, shader, filter);
|
|
}
|
|
|
|
void GSDevice::StretchRect(GSTexture* sTex, GSTexture* dTex, const GSVector4& dRect, ShaderConvertSelector shader, Filter filter)
|
|
{
|
|
StretchRect(sTex, GSVector4(0, 0, 1, 1), dTex, dRect, shader, filter);
|
|
}
|
|
|
|
void GSDevice::StretchRect(GSTexture* sTex, GSTexture* dTex, ShaderConvertSelector shader, Filter filter)
|
|
{
|
|
StretchRect(sTex, dTex, GSVector4(dTex->GetRect()), shader, filter);
|
|
}
|
|
|
|
void GSDevice::StretchRectAuto(GSTexture* sTex, const GSVector4& sRect, GSTexture* dTex, const GSVector4& dRect,
|
|
Filter filter, u32 src_bpp, u32 dst_bpp)
|
|
{
|
|
ShaderConvertSelector shader = GetConvertShader(sTex, dTex, src_bpp, dst_bpp);
|
|
if (shader.SupportsBilinear() && filter == Biln)
|
|
{
|
|
// Bilinear is emulated in the shader.
|
|
shader.SetFilter(Biln);
|
|
filter = Nearest;
|
|
}
|
|
StretchRect(sTex, sRect, dTex, dRect, shader, filter);
|
|
}
|
|
|
|
void GSDevice::StretchRectAuto(GSTexture* sTex, GSTexture* dTex, const GSVector4& dRect, Filter filter, u32 src_bpp, u32 dst_bpp)
|
|
{
|
|
StretchRectAuto(sTex, GSVector4(0, 0, 1, 1), dTex, dRect, filter, src_bpp, dst_bpp);
|
|
}
|
|
|
|
void GSDevice::StretchRectAuto(GSTexture* sTex, GSTexture* dTex, Filter filter, u32 src_bpp, u32 dst_bpp)
|
|
{
|
|
StretchRectAuto(sTex, dTex, GSVector4(dTex->GetRect()), filter, src_bpp, dst_bpp);
|
|
}
|
|
|
|
void GSDevice::StretchRectAutoMask(GSTexture* sTex, const GSVector4& sRect, GSTexture* dTex, const GSVector4& dRect,
|
|
bool red, bool green, bool blue, bool alpha, u32 src_bpp, u32 dst_bpp)
|
|
{
|
|
StretchRect(sTex, sRect, dTex, dRect, GetConvertShaderMask(sTex, dTex, src_bpp, dst_bpp, red, green, blue, alpha), Nearest);
|
|
}
|
|
|
|
void GSDevice::StretchRectAutoMask(GSTexture* sTex, GSTexture* dTex, const GSVector4& dRect, bool red, bool green, bool blue, bool alpha, u32 src_bpp, u32 dst_bpp)
|
|
{
|
|
StretchRectAutoMask(sTex, GSVector4(0, 0, 1, 1), dTex, dRect, red, green, blue, alpha, src_bpp, dst_bpp);
|
|
}
|
|
|
|
void GSDevice::StretchRectAutoMask(GSTexture* sTex, GSTexture* dTex, bool red, bool green, bool blue, bool alpha,
|
|
u32 src_bpp, u32 dst_bpp)
|
|
{
|
|
StretchRectAutoMask(sTex, dTex, GSVector4(dTex->GetRect()), red, green, blue, alpha, src_bpp, dst_bpp);
|
|
}
|
|
|
|
void GSDevice::RenderHW(GSHWDrawConfig& config)
|
|
{
|
|
// m_flushing: we are already inside Emit(), so this is a draw the backend is issuing
|
|
// on its own behalf. IsDSInRTActive: the caller is mid depth-as-colour sequence and
|
|
// will tear the temporary target down as soon as we return.
|
|
if (!GSConfig.CoalesceRenderPasses || m_flushing || IsDSInRTActive() ||
|
|
!GSPassScheduler::IsDeferrable(config))
|
|
{
|
|
FlushDeferredDraws();
|
|
DoRenderHW(config);
|
|
return;
|
|
}
|
|
|
|
if (m_pass_scheduler->TryEnqueue(config) != GSPassScheduler::Disposition::Queued)
|
|
{
|
|
// Either this draw can see something already queued, or a cap was reached. Emit the
|
|
// backlog and retry once against an empty queue; a draw that still will not fit is
|
|
// bigger than the whole arena, so just render it.
|
|
FlushDeferredDraws();
|
|
if (m_pass_scheduler->TryEnqueue(config) != GSPassScheduler::Disposition::Queued)
|
|
{
|
|
DoRenderHW(config);
|
|
return;
|
|
}
|
|
}
|
|
|
|
m_deferred_draw_count = m_pass_scheduler->GetCount();
|
|
}
|
|
|
|
bool GSDevice::DeferredDrawsReference(const GSTexture* tex) const
|
|
{
|
|
return m_pass_scheduler->References(tex);
|
|
}
|
|
|
|
void GSDevice::FlushDeferredDrawsImpl()
|
|
{
|
|
pxAssert(!m_flushing);
|
|
|
|
m_flushing = true;
|
|
m_pass_scheduler->Emit(this);
|
|
m_flushing = false;
|
|
|
|
m_deferred_draw_count = 0;
|
|
|
|
// The draws that were holding these back have run, so the pool can have them. Swap
|
|
// first: Recycle() is re-entrant through the backend overrides.
|
|
if (!m_deferred_recycle.empty())
|
|
{
|
|
std::vector<GSTexture*> pending;
|
|
pending.swap(m_deferred_recycle);
|
|
for (GSTexture* tex : pending)
|
|
Recycle(tex);
|
|
}
|
|
}
|
|
|
|
void GSDevice::DoDrawMultiStretchRects(
|
|
const MultiStretchRect* rects, u32 num_rects, GSTexture* dTex, ShaderConvertSelector shader)
|
|
{
|
|
for (u32 i = 0; i < num_rects; i++)
|
|
{
|
|
const MultiStretchRect& sr = rects[i];
|
|
g_gs_device->StretchRect(sr.src, sr.src_rect, dTex, sr.dst_rect,
|
|
shader.SetMask(rects[0].wmask.wrgba).SetFilter(sr.filter), sr.filter);
|
|
}
|
|
}
|
|
|
|
void GSDevice::SortMultiStretchRects(MultiStretchRect* rects, u32 num_rects)
|
|
{
|
|
// Depending on num_rects, insertion sort may be better here.
|
|
std::sort(rects, rects + num_rects, [](const MultiStretchRect& lhs, const MultiStretchRect& rhs) {
|
|
// Strict weak ordering: only tie-break on filter when src is equal. The old
|
|
// `lhs.src < rhs.src || lhs.filter < rhs.filter` is not a valid comparator
|
|
// (it can report both a<b and b<a), which is undefined behaviour in std::sort.
|
|
if (lhs.src != rhs.src)
|
|
return lhs.src < rhs.src;
|
|
return lhs.filter < rhs.filter;
|
|
});
|
|
}
|
|
|
|
void GSDevice::ClearCurrent()
|
|
{
|
|
FlushDeferredDraws();
|
|
m_current = nullptr;
|
|
|
|
delete m_merge;
|
|
delete m_weavebob;
|
|
delete m_blend;
|
|
delete m_mad;
|
|
delete m_target_tmp;
|
|
delete m_cas;
|
|
delete m_mfx_output;
|
|
delete m_fsr1_easu;
|
|
delete m_fsr1_output;
|
|
delete m_sgsr_output;
|
|
|
|
m_merge = nullptr;
|
|
m_weavebob = nullptr;
|
|
m_blend = nullptr;
|
|
m_mad = nullptr;
|
|
m_target_tmp = nullptr;
|
|
m_cas = nullptr;
|
|
m_mfx_output = nullptr;
|
|
m_fsr1_easu = nullptr;
|
|
m_fsr1_output = nullptr;
|
|
m_sgsr_output = nullptr;
|
|
}
|
|
|
|
void GSDevice::Merge(GSTexture* sTex[3], GSVector4* sRect, GSVector4* dRect, const GSVector2i& fs, const GSRegPMODE& PMODE, const GSRegEXTBUF& EXTBUF, u32 c)
|
|
{
|
|
FlushDeferredDraws();
|
|
if (ResizeRenderTarget(&m_merge, fs.x, fs.y, false, false))
|
|
DoMerge(sTex, sRect, m_merge, dRect, PMODE, EXTBUF, c, BilnIf(GSConfig.PCRTCOffsets));
|
|
|
|
m_current = m_merge;
|
|
}
|
|
|
|
void GSDevice::Interlace(const GSVector2i& ds, int field, int mode, float yoffset)
|
|
{
|
|
FlushDeferredDraws();
|
|
static int bufIdx = 0;
|
|
float offset = yoffset * static_cast<float>(field);
|
|
offset = GSConfig.DisableInterlaceOffset ? 0.0f : offset;
|
|
|
|
auto do_interlace = [this](GSTexture* sTex, GSTexture* dTex, ShaderInterlace shader, Filter filter, float yoffset, int bufIdx) {
|
|
const GSVector2i ds_i = dTex->GetSize();
|
|
const GSVector2 ds = GSVector2(static_cast<float>(ds_i.x), static_cast<float>(ds_i.y));
|
|
|
|
GSVector4 sRect = GSVector4(0.0f, 0.0f, 1.0f, 1.0f);
|
|
GSVector4 dRect = GSVector4(0.0f, yoffset, ds.x, ds.y + yoffset);
|
|
|
|
// Select the top or bottom half for MAD buffering.
|
|
if (shader == ShaderInterlace::MAD_BUFFER)
|
|
{
|
|
const float half_size = ds.y * 0.5f;
|
|
if ((bufIdx >> 1) == 1)
|
|
dRect.y += half_size;
|
|
else
|
|
dRect.w -= half_size;
|
|
}
|
|
|
|
const InterlaceConstantBuffer cb = {
|
|
GSVector4(static_cast<float>(bufIdx), 1.0f / ds.y, ds.y, MAD_SENSITIVITY)
|
|
};
|
|
|
|
GL_PUSH("DoInterlace %dx%d Shader:%d Filter:%d", ds_i.x, ds_i.y, static_cast<int>(shader), filter);
|
|
DoInterlace(sTex, sRect, dTex, dRect, shader, filter, cb);
|
|
};
|
|
|
|
switch (mode)
|
|
{
|
|
case 0: // Weave
|
|
ResizeRenderTarget(&m_weavebob, ds.x, ds.y, true, false);
|
|
do_interlace(m_merge, m_weavebob, ShaderInterlace::WEAVE, Nearest, offset, field);
|
|
m_current = m_weavebob;
|
|
break;
|
|
case 1: // Bob
|
|
// Field is reversed here as we are countering the bounce.
|
|
ResizeRenderTarget(&m_weavebob, ds.x, ds.y, true, false);
|
|
do_interlace(m_merge, m_weavebob, ShaderInterlace::BOB, Biln, yoffset * (1 - field), 0);
|
|
m_current = m_weavebob;
|
|
break;
|
|
case 2: // Blend
|
|
ResizeRenderTarget(&m_weavebob, ds.x, ds.y, true, false);
|
|
do_interlace(m_merge, m_weavebob, ShaderInterlace::WEAVE, Nearest, offset, field);
|
|
ResizeRenderTarget(&m_blend, ds.x, ds.y, true, false);
|
|
do_interlace(m_weavebob, m_blend, ShaderInterlace::BLEND, Biln, 0, 0);
|
|
m_current = m_blend;
|
|
break;
|
|
case 3: // FastMAD Motion Adaptive Deinterlacing
|
|
bufIdx++;
|
|
bufIdx &= ~1;
|
|
bufIdx |= field;
|
|
bufIdx &= 3;
|
|
ResizeRenderTarget(&m_mad, ds.x, ds.y * 2.0f, true, false);
|
|
do_interlace(m_merge, m_mad, ShaderInterlace::MAD_BUFFER, Nearest, offset, bufIdx);
|
|
ResizeRenderTarget(&m_weavebob, ds.x, ds.y, true, false);
|
|
do_interlace(m_mad, m_weavebob, ShaderInterlace::MAD_RECONSTRUCT, Nearest, 0, bufIdx);
|
|
m_current = m_weavebob;
|
|
break;
|
|
default:
|
|
m_current = m_merge;
|
|
break;
|
|
}
|
|
}
|
|
|
|
void GSDevice::FXAA()
|
|
{
|
|
FlushDeferredDraws();
|
|
// Combining FXAA+ShadeBoost can't share the same target.
|
|
GSTexture*& dTex = (m_current == m_target_tmp) ? m_merge : m_target_tmp;
|
|
if (ResizeRenderTarget(&dTex, m_current->GetWidth(), m_current->GetHeight(), false, false))
|
|
{
|
|
DoFXAA(m_current, dTex);
|
|
m_current = dTex;
|
|
}
|
|
}
|
|
|
|
bool GSDevice::ApplyShaderChain(const GSVector2i& output_size)
|
|
{
|
|
FlushDeferredDraws();
|
|
// Guarded here rather than in the backends so a device that never overrides
|
|
// DoApplyShaderChain (software, or a build without librashader) costs nothing.
|
|
const bool wanted = GSConfig.ShaderChainEnabled && !GSConfig.ShaderChainPreset.empty();
|
|
// On the edge, not every frame: turning the chain off used to leave every pass's target
|
|
// and pipeline resident until the preset changed or the device died.
|
|
if (!wanted && m_shader_chain_loaded)
|
|
{
|
|
ReleaseShaderChain();
|
|
m_shader_chain_loaded = false;
|
|
}
|
|
if (!wanted || !m_current)
|
|
return false;
|
|
|
|
// A minimised or mid-resize window yields a degenerate rect; never build a 0-sized target.
|
|
if (output_size.x <= 0 || output_size.y <= 0)
|
|
return false;
|
|
|
|
// Same ping-pong as FXAA: the chain reads m_current, so it can't also write it. Unlike FXAA
|
|
// the target is sized to the caller's on-screen rect rather than to m_current — see the
|
|
// header for why that has to be the aspect-corrected rect and not the window.
|
|
GSTexture*& dTex = (m_current == m_target_tmp) ? m_merge : m_target_tmp;
|
|
if (!ResizeRenderTarget(&dTex, output_size.x, output_size.y, false, false))
|
|
return false;
|
|
|
|
// Only swap on success — a failed chain (bad preset, unsupported backend) must leave
|
|
// m_current pointing at the unshaded frame rather than at a target nothing rendered to.
|
|
if (!DoApplyShaderChain(m_current, dTex))
|
|
return false;
|
|
|
|
m_shader_chain_loaded = true;
|
|
m_current = dTex;
|
|
return true;
|
|
}
|
|
|
|
void GSDevice::ShadeBoost()
|
|
{
|
|
FlushDeferredDraws();
|
|
if (ResizeRenderTarget(&m_target_tmp, m_current->GetWidth(), m_current->GetHeight(), false, false))
|
|
{
|
|
// predivide to avoid the divide (multiply) in the shader
|
|
const GSVector4 params(
|
|
static_cast<float>(GSConfig.ShadeBoost_Brightness) * (1.0f / 50.0f),
|
|
static_cast<float>(GSConfig.ShadeBoost_Contrast) * (1.0f / 50.0f),
|
|
static_cast<float>(GSConfig.ShadeBoost_Saturation) * (1.0f / 50.0f),
|
|
static_cast<float>(GSConfig.ShadeBoost_Gamma) * (1.0f / 50.0f));
|
|
|
|
DoShadeBoost(m_current, m_target_tmp, params.v);
|
|
|
|
m_current = m_target_tmp;
|
|
}
|
|
}
|
|
|
|
void GSDevice::Resize(int width, int height)
|
|
{
|
|
FlushDeferredDraws();
|
|
GSTexture*& dTex = (m_current == m_target_tmp) ? m_merge : m_target_tmp;
|
|
GSVector2i s = m_current->GetSize();
|
|
int multiplier = 1;
|
|
|
|
if ((width > s.x || height > s.y))
|
|
{
|
|
while (width > s.x || height > s.y)
|
|
{
|
|
s = m_current->GetSize() * GSVector2i(++multiplier);
|
|
}
|
|
}
|
|
else
|
|
s = GSVector2i(width, height);
|
|
|
|
if (ResizeRenderTarget(&dTex, s.x, s.y, false, false))
|
|
{
|
|
const GSVector4 sRect(0, 0, 1, 1);
|
|
const GSVector4 dRect(0, 0, s.x, s.y);
|
|
StretchRectAuto(m_current, sRect, dTex, dRect, Nearest);
|
|
m_current = dTex;
|
|
}
|
|
}
|
|
|
|
bool GSDevice::ResizeRenderTarget(GSTexture** t, int w, int h, bool preserve_contents, bool recycle)
|
|
{
|
|
pxAssert(t);
|
|
|
|
GSTexture* orig_tex = *t;
|
|
if (orig_tex && orig_tex->GetWidth() == w && orig_tex->GetHeight() == h)
|
|
{
|
|
if (!preserve_contents)
|
|
InvalidateRenderTarget(orig_tex);
|
|
|
|
return true;
|
|
}
|
|
|
|
const GSTexture::Format fmt = orig_tex ? orig_tex->GetFormat() : GSTexture::Format::Color;
|
|
const GSTexture::Usage usage = orig_tex ? orig_tex->GetUsage() : GSTexture::RenderTarget;
|
|
const bool really_preserve_contents = (preserve_contents && orig_tex);
|
|
GSTexture* new_tex = FetchSurface(usage, w, h, 1, fmt, !really_preserve_contents, false);
|
|
if (!new_tex)
|
|
{
|
|
Console.WriteLn("%dx%d texture allocation failed in ResizeTexture()", w, h);
|
|
return false;
|
|
}
|
|
|
|
if (really_preserve_contents)
|
|
{
|
|
constexpr GSVector4 sRect = GSVector4::cxpr(0, 0, 1, 1);
|
|
const GSVector4 dRect = GSVector4(orig_tex->GetRect());
|
|
StretchRect(orig_tex, sRect, new_tex, dRect, ShaderConvert::COPY, Biln);
|
|
}
|
|
|
|
if (orig_tex)
|
|
{
|
|
if (recycle)
|
|
Recycle(orig_tex);
|
|
else
|
|
delete orig_tex;
|
|
}
|
|
|
|
*t = new_tex;
|
|
return true;
|
|
}
|
|
|
|
void GSDevice::DoBeginDSAsRT(GSTexture* ds, const GSVector4i& drawarea)
|
|
{
|
|
// Create a temporary RT and copy the area needed for the draw.
|
|
const int w = ds->GetWidth();
|
|
const int h = ds->GetHeight();
|
|
if ((m_ds_as_rt = g_gs_device->CreateFeedbackTarget(w, h, GSTexture::Format::DepthColor, false, true)))
|
|
{
|
|
const GSVector4 dRect(drawarea);
|
|
const GSVector4 sRect(dRect.x / w, dRect.y / h, dRect.z / w, dRect.w / h);
|
|
StretchRectAuto(ds, sRect, m_ds_as_rt, dRect, Nearest);
|
|
}
|
|
}
|
|
|
|
void GSDevice::EndDSAsRT()
|
|
{
|
|
Recycle(m_ds_as_rt);
|
|
m_ds_as_rt = nullptr;
|
|
}
|
|
|
|
#if defined(__clang__)
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wunused-function"
|
|
#pragma clang diagnostic ignored "-Wignored-qualifiers"
|
|
#elif defined(__GNUC__)
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wignored-qualifiers"
|
|
#endif
|
|
|
|
// Kinda grotty, but better than copy/pasting the relevant bits in..
|
|
#define A_CPU 1
|
|
#include "bin/resources/shaders/common/ffx_a.h"
|
|
#include "bin/resources/shaders/common/ffx_cas.h"
|
|
// FSR needs the 2021 revision of ffx_a.h on the GPU side, but its CPU-side constant setup
|
|
// (FsrEasuConOffset/FsrRcasCon) is satisfied by the 2019 header above - which is the one
|
|
// PCSX2 patched locally for Metal (A16/A_MSL/A_MAYBE_UNUSED) and that ffx_cas.h depends on.
|
|
// So only the shader gets the 2021 copy; nothing here includes ffx_a_fsr1.h.
|
|
#include "bin/resources/shaders/common/ffx_fsr1.h"
|
|
|
|
#if defined(__clang__)
|
|
#pragma clang diagnostic pop
|
|
#elif defined(__GNUC__)
|
|
#pragma GCC diagnostic pop
|
|
#endif
|
|
|
|
bool GSDevice::GetCASShaderSource(std::string* source)
|
|
{
|
|
std::optional<std::string> ffx_a_source = ReadShaderSource("shaders/common/ffx_a.h");
|
|
std::optional<std::string> ffx_cas_source = ReadShaderSource("shaders/common/ffx_cas.h");
|
|
if (!ffx_a_source.has_value() || !ffx_cas_source.has_value())
|
|
return false;
|
|
|
|
// Since our shader compilers don't support includes, and OpenGL doesn't at all... we'll do a really cheeky string replace.
|
|
StringUtil::ReplaceAll(source, "#include \"ffx_a.h\"", ffx_a_source.value());
|
|
StringUtil::ReplaceAll(source, "#include \"ffx_cas.h\"", ffx_cas_source.value());
|
|
return true;
|
|
}
|
|
|
|
bool GSDevice::GetFSR1ShaderSource(std::string* source, bool easu_pass)
|
|
{
|
|
std::optional<std::string> ffx_a_source = ReadShaderSource("shaders/common/ffx_a_fsr1.h");
|
|
std::optional<std::string> ffx_fsr1_source = ReadShaderSource("shaders/common/ffx_fsr1.h");
|
|
if (!ffx_a_source.has_value() || !ffx_fsr1_source.has_value())
|
|
return false;
|
|
|
|
// FSR_EASU_F/FSR_RCAS_F gate which function bodies ffx_fsr1.h emits at all, so the pass has
|
|
// to be chosen before the preprocessor runs. That is why this takes easu_pass rather than
|
|
// letting the backend pick with a specialization constant the way cas.glsl does.
|
|
source->insert(0, easu_pass ? "#version 460 core\n#define FSR_PASS_EASU 1\n"
|
|
: "#version 460 core\n#define FSR_PASS_EASU 0\n");
|
|
|
|
// Same cheeky string replace as CAS above - our shader compilers don't support includes.
|
|
StringUtil::ReplaceAll(source, "#include \"ffx_a_fsr1.h\"", ffx_a_source.value());
|
|
StringUtil::ReplaceAll(source, "#include \"ffx_fsr1.h\"", ffx_fsr1_source.value());
|
|
return true;
|
|
}
|
|
|
|
void GSDevice::CAS(GSTexture*& tex, GSVector4i& src_rect, GSVector4& src_uv, const GSVector4& draw_rect, bool sharpen_only)
|
|
{
|
|
FlushDeferredDraws();
|
|
const int dst_width = sharpen_only ? src_rect.width() : static_cast<int>(std::ceil(draw_rect.z - draw_rect.x));
|
|
const int dst_height = sharpen_only ? src_rect.height() : static_cast<int>(std::ceil(draw_rect.w - draw_rect.y));
|
|
const int src_offset_x = static_cast<int>(src_rect.x);
|
|
const int src_offset_y = static_cast<int>(src_rect.y);
|
|
|
|
GSTexture* src_tex = tex;
|
|
if (!m_cas || m_cas->GetWidth() != dst_width || m_cas->GetHeight() != dst_height)
|
|
{
|
|
delete m_cas;
|
|
m_cas = CreateSurface(GSTexture::ShaderWriteTexture, dst_width, dst_height, 1, GSTexture::Format::Color);
|
|
if (!m_cas)
|
|
{
|
|
Console.Error("Failed to allocate CAS RW texture.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
std::array<u32, NUM_CAS_CONSTANTS> consts;
|
|
CasSetup(&consts[0], &consts[4], static_cast<float>(GSConfig.CAS_Sharpness) * 0.01f,
|
|
static_cast<AF1>(src_rect.width()), static_cast<AF1>(src_rect.height()),
|
|
static_cast<AF1>(dst_width), static_cast<AF1>(dst_height));
|
|
consts[8] = static_cast<u32>(src_offset_x);
|
|
consts[9] = static_cast<u32>(src_offset_y);
|
|
|
|
if (!DoCAS(src_tex, m_cas, sharpen_only, consts))
|
|
{
|
|
// leave textures intact if we failed
|
|
Console.Warning("Applying CAS failed.");
|
|
return;
|
|
}
|
|
|
|
tex = m_cas;
|
|
src_rect = GSVector4i(0, 0, dst_width, dst_height);
|
|
src_uv = GSVector4(0.0f, 0.0f, 1.0f, 1.0f);
|
|
}
|
|
|
|
void GSDevice::MetalFXUpscale(GSTexture*& tex, GSVector4i& src_rect, GSVector4& src_uv, const GSVector4& draw_rect)
|
|
{
|
|
FlushDeferredDraws();
|
|
const int dst_width = static_cast<int>(std::ceil(draw_rect.z - draw_rect.x));
|
|
const int dst_height = static_cast<int>(std::ceil(draw_rect.w - draw_rect.y));
|
|
if (dst_width <= 0 || dst_height <= 0)
|
|
return;
|
|
|
|
GSTexture* src_tex = tex;
|
|
if (!m_mfx_output || m_mfx_output->GetWidth() != dst_width || m_mfx_output->GetHeight() != dst_height)
|
|
{
|
|
delete m_mfx_output;
|
|
m_mfx_output = CreateSurface(GSTexture::ShaderWriteTexture, dst_width, dst_height, 1, GSTexture::Format::Color);
|
|
if (!m_mfx_output)
|
|
{
|
|
Console.Error("Failed to allocate MetalFX output texture.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!DoMetalFXSpatial(src_tex, m_mfx_output))
|
|
{
|
|
// leave textures intact if we failed
|
|
Console.Warning("Applying MetalFX spatial upscale failed.");
|
|
return;
|
|
}
|
|
|
|
tex = m_mfx_output;
|
|
src_rect = GSVector4i(0, 0, dst_width, dst_height);
|
|
src_uv = GSVector4(0.0f, 0.0f, 1.0f, 1.0f);
|
|
}
|
|
|
|
void GSDevice::FSR1Upscale(GSTexture*& tex, GSVector4i& src_rect, GSVector4& src_uv, const GSVector4& draw_rect)
|
|
{
|
|
FlushDeferredDraws();
|
|
const int dst_width = static_cast<int>(std::ceil(draw_rect.z - draw_rect.x));
|
|
const int dst_height = static_cast<int>(std::ceil(draw_rect.w - draw_rect.y));
|
|
if (dst_width <= 0 || dst_height <= 0)
|
|
return;
|
|
|
|
// Anchored inside FSR1Upscale explicitly: the first attempt at this matched an identical
|
|
// "GSTexture* src_tex = tex;" line in GSDevice::CAS, which FSR REPLACES, so the log could
|
|
// never fire and made a working pass look dead.
|
|
static int s_logged_w = 0, s_logged_h = 0;
|
|
if (s_logged_w != dst_width || s_logged_h != dst_height)
|
|
{
|
|
s_logged_w = dst_width;
|
|
s_logged_h = dst_height;
|
|
Console.WriteLnFmt("@@ANDROID_FSR1@@ upscaling {}x{} -> {}x{} (sharpness {})",
|
|
tex->GetWidth(), tex->GetHeight(), dst_width, dst_height, GSConfig.FSR_Sharpness);
|
|
}
|
|
|
|
GSTexture* src_tex = tex;
|
|
|
|
// Two targets, not one: RCAS is a separate dispatch that reads EASU's whole output, so it
|
|
// cannot write in place.
|
|
if (!m_fsr1_easu || m_fsr1_easu->GetWidth() != dst_width || m_fsr1_easu->GetHeight() != dst_height)
|
|
{
|
|
delete m_fsr1_easu;
|
|
m_fsr1_easu = CreateSurface(GSTexture::ShaderWriteTexture, dst_width, dst_height, 1, GSTexture::Format::Color);
|
|
if (!m_fsr1_easu)
|
|
{
|
|
Console.Error("Failed to allocate FSR1 EASU texture.");
|
|
return;
|
|
}
|
|
}
|
|
if (!m_fsr1_output || m_fsr1_output->GetWidth() != dst_width || m_fsr1_output->GetHeight() != dst_height)
|
|
{
|
|
delete m_fsr1_output;
|
|
m_fsr1_output = CreateSurface(GSTexture::ShaderWriteTexture, dst_width, dst_height, 1, GSTexture::Format::Color);
|
|
if (!m_fsr1_output)
|
|
{
|
|
Console.Error("Failed to allocate FSR1 RCAS texture.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Zero-initialised, and pushed whole by both passes: the shader reads the fifth vector
|
|
// ("Sample") unconditionally to pick AMD's gamma2 output path, which we never want.
|
|
std::array<u32, NUM_FSR1_CONSTANTS> consts = {};
|
|
|
|
// EASU distinguishes the displayed region from the resource holding it: the viewport is the
|
|
// cropped src_rect, the size is the whole texture, and the offset puts the two together.
|
|
FsrEasuConOffset(&consts[0], &consts[4], &consts[8], &consts[12],
|
|
static_cast<AF1>(src_rect.width()), static_cast<AF1>(src_rect.height()),
|
|
static_cast<AF1>(src_tex->GetWidth()), static_cast<AF1>(src_tex->GetHeight()),
|
|
static_cast<AF1>(dst_width), static_cast<AF1>(dst_height),
|
|
static_cast<AF1>(src_rect.x), static_cast<AF1>(src_rect.y));
|
|
|
|
if (!DoFSR1EASU(src_tex, m_fsr1_easu, consts))
|
|
{
|
|
// leave textures intact if we failed
|
|
Console.Warning("Applying FSR1 EASU failed.");
|
|
return;
|
|
}
|
|
|
|
// RCAS takes sharpness in stops - 0 is the maximum and each stop halves it - so the 0..100
|
|
// slider runs backwards across the 2..0 range AMD's own sample exposes.
|
|
std::array<u32, NUM_FSR1_CONSTANTS> rcas_consts = {};
|
|
FsrRcasCon(&rcas_consts[0], 2.0f - (static_cast<float>(GSConfig.FSR_Sharpness) * 0.02f));
|
|
|
|
if (!DoFSR1RCAS(m_fsr1_easu, m_fsr1_output, rcas_consts))
|
|
{
|
|
Console.Warning("Applying FSR1 RCAS failed.");
|
|
return;
|
|
}
|
|
|
|
tex = m_fsr1_output;
|
|
src_rect = GSVector4i(0, 0, dst_width, dst_height);
|
|
src_uv = GSVector4(0.0f, 0.0f, 1.0f, 1.0f);
|
|
}
|
|
|
|
void GSDevice::SGSRUpscale(GSTexture*& tex, GSVector4i& src_rect, GSVector4& src_uv, const GSVector4& draw_rect)
|
|
{
|
|
FlushDeferredDraws();
|
|
const int dst_width = static_cast<int>(std::ceil(draw_rect.z - draw_rect.x));
|
|
const int dst_height = static_cast<int>(std::ceil(draw_rect.w - draw_rect.y));
|
|
if (dst_width <= 0 || dst_height <= 0)
|
|
return;
|
|
|
|
GSTexture* src_tex = tex;
|
|
|
|
static int s_logged_w = 0, s_logged_h = 0;
|
|
if (s_logged_w != dst_width || s_logged_h != dst_height)
|
|
{
|
|
s_logged_w = dst_width;
|
|
s_logged_h = dst_height;
|
|
Console.WriteLnFmt("@@ANDROID_SGSR@@ upscaling {}x{} -> {}x{} (sharpness {})",
|
|
src_rect.width(), src_rect.height(), dst_width, dst_height, GSConfig.FSR_Sharpness);
|
|
}
|
|
|
|
// One target, unlike FSR1: SGSR is a single pass, so there is no intermediate to hand on.
|
|
if (!m_sgsr_output || m_sgsr_output->GetWidth() != dst_width || m_sgsr_output->GetHeight() != dst_height)
|
|
{
|
|
delete m_sgsr_output;
|
|
m_sgsr_output = CreateSurface(GSTexture::ShaderWriteTexture, dst_width, dst_height, 1, GSTexture::Format::Color);
|
|
if (!m_sgsr_output)
|
|
{
|
|
Console.Error("Failed to allocate SGSR output texture.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// The picture occupies src_rect inside a larger merge target, so the shader is told where it
|
|
// is rather than assuming the whole texture — the same problem FsrEasuConOffset solves for
|
|
// FSR1 by taking an offset.
|
|
const float src_tex_w = static_cast<float>(src_tex->GetWidth());
|
|
const float src_tex_h = static_cast<float>(src_tex->GetHeight());
|
|
const float uv_off_x = static_cast<float>(src_rect.x) / src_tex_w;
|
|
const float uv_off_y = static_cast<float>(src_rect.y) / src_tex_h;
|
|
const float uv_scale_x = static_cast<float>(src_rect.width()) / src_tex_w;
|
|
const float uv_scale_y = static_cast<float>(src_rect.height()) / src_tex_h;
|
|
|
|
// Qualcomm's edge sharpness runs 0..2 with 1.0 as its own default. Our slider is the 0..100
|
|
// one FSR1 already uses, so the two features share a control rather than growing a second
|
|
// one that means almost the same thing.
|
|
const float sharpness = std::clamp(static_cast<float>(GSConfig.FSR_Sharpness) * 0.02f, 0.0f, 2.0f);
|
|
|
|
std::array<u32, NUM_SGSR_CONSTANTS> consts = {};
|
|
const auto put_f = [&consts](u32 i, float v) { std::memcpy(&consts[i], &v, sizeof(float)); };
|
|
consts[0] = static_cast<u32>(dst_width);
|
|
consts[1] = static_cast<u32>(dst_height);
|
|
put_f(2, uv_off_x);
|
|
put_f(3, uv_off_y);
|
|
put_f(4, uv_scale_x);
|
|
put_f(5, uv_scale_y);
|
|
put_f(6, src_tex_w);
|
|
put_f(7, src_tex_h);
|
|
put_f(8, 1.0f / src_tex_w);
|
|
put_f(9, 1.0f / src_tex_h);
|
|
put_f(10, sharpness);
|
|
|
|
if (!DoSGSR(src_tex, m_sgsr_output, consts))
|
|
{
|
|
// leave textures intact if we failed
|
|
Console.Warning("Applying SGSR failed.");
|
|
return;
|
|
}
|
|
|
|
tex = m_sgsr_output;
|
|
src_rect = GSVector4i(0, 0, dst_width, dst_height);
|
|
src_uv = GSVector4(0.0f, 0.0f, 1.0f, 1.0f);
|
|
}
|
|
|
|
bool GSHWDrawConfig::BlendState::IsEffective(ColorMaskSelector colormask) const
|
|
{
|
|
return enable && (((colormask.key & 7u) && (src_factor != GSDevice::CONST_ONE || dst_factor != GSDevice::CONST_ZERO)) ||
|
|
((colormask.key & 8u) && (src_factor_alpha != GSDevice::CONST_ONE || dst_factor_alpha != GSDevice::CONST_ZERO)));
|
|
}
|
|
|
|
struct DrawConfigWriter
|
|
{
|
|
fmt::memory_buffer buffer;
|
|
u32 indent = 0;
|
|
bool beginning_of_line = true;
|
|
|
|
/// Uses RAII to add 1 to indent on construction and remove on destruction
|
|
struct RAIIIndent
|
|
{
|
|
DrawConfigWriter& writer;
|
|
RAIIIndent(RAIIIndent&&) = delete;
|
|
explicit RAIIIndent(DrawConfigWriter& writer): writer(writer) { writer.PushIndent(); }
|
|
~RAIIIndent() { writer.PopIndent(); }
|
|
operator DrawConfigWriter&() { return writer; }
|
|
};
|
|
|
|
void PushIndent() { indent++; }
|
|
void PopIndent() { indent--; }
|
|
RAIIIndent WithIndent() { return RAIIIndent(*this); }
|
|
|
|
template <typename... T>
|
|
FMT_INLINE void WriteLn(fmt::format_string<T...> fmt, T&&... args)
|
|
{
|
|
fmt::vargs<T...> va = {{args...}};
|
|
if (beginning_of_line)
|
|
WriteIndent();
|
|
beginning_of_line = true;
|
|
fmt::detail::vformat_to(buffer, fmt.str, va);
|
|
buffer.push_back('\n');
|
|
}
|
|
|
|
template <typename... T>
|
|
FMT_INLINE void Write(fmt::format_string<T...> fmt, T&&... args)
|
|
{
|
|
fmt::vargs<T...> va = {{args...}};
|
|
if (beginning_of_line)
|
|
WriteIndent();
|
|
beginning_of_line = false;
|
|
fmt::detail::vformat_to(buffer, fmt.str, va);
|
|
}
|
|
|
|
private:
|
|
void WriteIndent()
|
|
{
|
|
size_t sz = buffer.size();
|
|
buffer.resize(sz + indent);
|
|
for (u32 i = 0; i < indent; i++)
|
|
buffer[sz + i] = '\t';
|
|
}
|
|
};
|
|
|
|
const char* GSGetTopologyName(GSHWDrawConfig::Topology topology)
|
|
{
|
|
switch (topology)
|
|
{
|
|
case GSHWDrawConfig::Topology::Point: return "Point";
|
|
case GSHWDrawConfig::Topology::Line: return "Line";
|
|
case GSHWDrawConfig::Topology::Triangle: return "Triangle";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
static const char* GetVSExpandName(GSHWDrawConfig::VSExpand vsexpand)
|
|
{
|
|
switch (vsexpand)
|
|
{
|
|
case GSHWDrawConfig::VSExpand::None: return "None";
|
|
case GSHWDrawConfig::VSExpand::Point: return "Point";
|
|
case GSHWDrawConfig::VSExpand::Line: return "Line";
|
|
case GSHWDrawConfig::VSExpand::Sprite: return "Sprite";
|
|
case GSHWDrawConfig::VSExpand::LineAA1: return "LineAA1";
|
|
case GSHWDrawConfig::VSExpand::TriangleAA1: return "TriangleAA1";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
static const char* GetPSDateName(u32 date)
|
|
{
|
|
switch (date)
|
|
{
|
|
case 0: return "Off";
|
|
case 1: return "PrimID Init DATM=0";
|
|
case 2: return "PrimID Init DATM=1";
|
|
case 3: return "PrimID Main";
|
|
case 5: return "Barrier DATM=0";
|
|
case 6: return "Barrier DATM=1";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
static const char* GetPSAlphaTestName(GSShader::PS_ATST atst)
|
|
{
|
|
using GSShader::PS_ATST;
|
|
switch (atst)
|
|
{
|
|
case PS_ATST::NONE: return "NONE";
|
|
case PS_ATST::LEQUAL: return "LEQUAL";
|
|
case PS_ATST::GEQUAL: return "GEQUAL";
|
|
case PS_ATST::EQUAL: return "EQUAL";
|
|
case PS_ATST::NOTEQUAL: return "NOTEQUAL";
|
|
};
|
|
return "UNKNOWN";
|
|
}
|
|
|
|
static const char* GetPSAFAILName(GSShader::PS_AFAIL afail)
|
|
{
|
|
using GSShader::PS_AFAIL;
|
|
switch (afail)
|
|
{
|
|
case PS_AFAIL::KEEP: return "KEEP";
|
|
case PS_AFAIL::FB_ONLY: return "FB_ONLY";
|
|
case PS_AFAIL::ZB_ONLY: return "ZB_ONLY";
|
|
case PS_AFAIL::RGB_ONLY: return "RGB_ONLY";
|
|
case PS_AFAIL::RGB_ONLY_DSB: return "RGB_ONLY_DSB";
|
|
case PS_AFAIL::RGB_ONLY_SW_Z: return "RGB_ONLY_SW_Z";
|
|
};
|
|
return "UNKNOWN";
|
|
}
|
|
|
|
static const char* GetPSDstFmtName(u32 dstfmt)
|
|
{
|
|
switch (dstfmt)
|
|
{
|
|
case 0: return "32bit";
|
|
case 1: return "24bit";
|
|
case 2: return "16bit";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
static const char* GetPSDepthFmtName(u32 depthfmt)
|
|
{
|
|
switch (depthfmt)
|
|
{
|
|
case 0: return "None";
|
|
case 1: return "32bit";
|
|
case 2: return "16bit";
|
|
case 3: return "RGBA";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
static const char* GetPSBlendABDName(u32 abd)
|
|
{
|
|
switch (abd)
|
|
{
|
|
case 0: return "Cs";
|
|
case 1: return "Cd";
|
|
case 2: return "0";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
static const char* GetPSBlendCName(u32 c)
|
|
{
|
|
switch (c)
|
|
{
|
|
case 0: return "As";
|
|
case 1: return "Ad";
|
|
case 2: return "Af";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
static const char* GetPSBlendHWName(HWBlendType blendhw)
|
|
{
|
|
switch (blendhw)
|
|
{
|
|
case HWBlendType::SRC_ONE_DST_FACTOR: return "SRC_ONE_DST_FACTOR";
|
|
case HWBlendType::SRC_ALPHA_DST_FACTOR: return "SRC_ALPHA_DST_FACTOR";
|
|
case HWBlendType::SRC_DOUBLE: return "SRC_DOUBLE";
|
|
case HWBlendType::SRC_HALF_ONE_DST_FACTOR: return "SRC_HALF_ONE_DST_FACTOR";
|
|
case HWBlendType::SRC_INV_DST_BLEND_HALF: return "SRC_INV_DST_BLEND_HALF";
|
|
case HWBlendType::INV_SRC_DST_BLEND_HALF: return "INV_SRC_DST_BLEND_HALF";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
static const char* GetPSBlendMixName(HWBlendType blendmix)
|
|
{
|
|
switch (blendmix)
|
|
{
|
|
case HWBlendType::BMIX1_ALPHA_HIGH_ONE: return "BMIX1_ALPHA_HIGH_ONE";
|
|
case HWBlendType::BMIX1_SRC_HALF: return "BMIX1_SRC_HALF";
|
|
case HWBlendType::BMIX2_OVERFLOW: return "BMIX2_OVERFLOW";
|
|
case HWBlendType::SRC_HALF_ONE_DST_FACTOR:
|
|
case HWBlendType::SRC_INV_DST_BLEND_HALF:
|
|
case HWBlendType::INV_SRC_DST_BLEND_HALF:
|
|
return "INVALID (Blend Mix)";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
static const char* GetHWBlendTypeName(HWBlendType blend, bool mix)
|
|
{
|
|
if (static_cast<u32>(blend) == 0)
|
|
return "Off";
|
|
return mix ? GetPSBlendMixName(blend) : GetPSBlendHWName(blend);
|
|
}
|
|
|
|
static const char* GetPSChannelName(ChannelFetch channel)
|
|
{
|
|
switch (channel)
|
|
{
|
|
case ChannelFetch_NONE: return "None";
|
|
case ChannelFetch_RED: return "FetchRed";
|
|
case ChannelFetch_GREEN: return "FetchGreen";
|
|
case ChannelFetch_BLUE: return "FetchBlue";
|
|
case ChannelFetch_ALPHA: return "FetchAlpha";
|
|
case ChannelFetch_RGB: return "FetchRGB";
|
|
case ChannelFetch_GXBY: return "FetchGXBY";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
static const char* GetPSDitherName(u32 dither)
|
|
{
|
|
switch (dither)
|
|
{
|
|
case 0: return "None";
|
|
case 1: return "Standard";
|
|
case 2: return "ReciprocalScaled";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
static const char* GetSSTrilnName(GS_MIN_FILTER triln)
|
|
{
|
|
switch (triln)
|
|
{
|
|
case GS_MIN_FILTER::Nearest: return "Nearest";
|
|
case GS_MIN_FILTER::Linear: return "Linear";
|
|
case GS_MIN_FILTER::Nearest_Mipmap_Nearest: return "Nearest_Mipmap_Nearest";
|
|
case GS_MIN_FILTER::Nearest_Mipmap_Linear: return "Nearest_Mipmap_Linear";
|
|
case GS_MIN_FILTER::Linear_Mipmap_Nearest: return "Linear_Mipmap_Nearest";
|
|
case GS_MIN_FILTER::Linear_Mipmap_Linear: return "Linear_Mipmap_Linear";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
static const char* GetBlendOpSymbol(GSDevice::BlendOp op)
|
|
{
|
|
switch (op)
|
|
{
|
|
case GSDevice::OP_ADD: return "+";
|
|
case GSDevice::OP_SUBTRACT: return "-";
|
|
case GSDevice::OP_REV_SUBTRACT: return "-";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
static const char* GetBlendFactorFormula(GSDevice::BlendFactor blendfactor)
|
|
{
|
|
switch (blendfactor)
|
|
{
|
|
case GSDevice::SRC_COLOR: return "Cs";
|
|
case GSDevice::INV_SRC_COLOR: return "(1 - Cs)";
|
|
case GSDevice::DST_COLOR: return "Cd";
|
|
case GSDevice::INV_DST_COLOR: return "(1 - Cd)";
|
|
case GSDevice::SRC1_COLOR: return "Cs1";
|
|
case GSDevice::INV_SRC1_COLOR: return "(1 - Cs1)";
|
|
case GSDevice::SRC_ALPHA: return "As";
|
|
case GSDevice::INV_SRC_ALPHA: return "(1 - As)";
|
|
case GSDevice::DST_ALPHA: return "Ad";
|
|
case GSDevice::INV_DST_ALPHA: return "(1 - Ad)";
|
|
case GSDevice::SRC1_ALPHA: return "As1";
|
|
case GSDevice::INV_SRC1_ALPHA: return "(1 - As1)";
|
|
case GSDevice::CONST_COLOR: return "Cf";
|
|
case GSDevice::INV_CONST_COLOR: return "(1 - Cf)";
|
|
case GSDevice::CONST_ONE: return "1";
|
|
case GSDevice::CONST_ZERO: return "0";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
const char* GSGetDestinationAlphaModeName(GSHWDrawConfig::DestinationAlphaMode datm)
|
|
{
|
|
switch (datm)
|
|
{
|
|
case GSHWDrawConfig::DestinationAlphaMode::Off: return "Off";
|
|
case GSHWDrawConfig::DestinationAlphaMode::Stencil: return "Stencil";
|
|
case GSHWDrawConfig::DestinationAlphaMode::StencilOne: return "StencilOne";
|
|
case GSHWDrawConfig::DestinationAlphaMode::PrimIDTracking: return "PrimIDTracking";
|
|
case GSHWDrawConfig::DestinationAlphaMode::Full: return "Full";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
static const char* GetColClipModeName(GSHWDrawConfig::ColClipMode ccmode)
|
|
{
|
|
switch (ccmode)
|
|
{
|
|
case GSHWDrawConfig::ColClipMode::NoModify: return "NoModify";
|
|
case GSHWDrawConfig::ColClipMode::ConvertOnly: return "ConvertOnly";
|
|
case GSHWDrawConfig::ColClipMode::ResolveOnly: return "ResolveOnly";
|
|
case GSHWDrawConfig::ColClipMode::ConvertAndResolve: return "ConvertAndResolve";
|
|
case GSHWDrawConfig::ColClipMode::EarlyResolve: return "EarlyResolve";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
static const char* GetSetDATMName(SetDATM datm)
|
|
{
|
|
switch (datm)
|
|
{
|
|
case SetDATM::DATM0: return "DATM0";
|
|
case SetDATM::DATM1: return "DATM1";
|
|
case SetDATM::DATM0_RTA_CORRECTION: return "DATM0_RTA_CORRECTION";
|
|
case SetDATM::DATM1_RTA_CORRECTION: return "DATM1_RTA_CORRECTION";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
static const char* GetPSAA1Name(GSHWDrawConfig::PS_AA1 aa1)
|
|
{
|
|
switch (aa1)
|
|
{
|
|
case GSHWDrawConfig::PS_AA1::NONE: return "NONE";
|
|
case GSHWDrawConfig::PS_AA1::LINE: return "LINE";
|
|
case GSHWDrawConfig::PS_AA1::TRIANGLE: return "TRIANGLE";
|
|
case GSHWDrawConfig::PS_AA1::TRIANGLE_SW_Z: return "TRIANGLE_SW_Z";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
const char* GSGetTexHazardName(u32 tex_hazard)
|
|
{
|
|
switch (tex_hazard)
|
|
{
|
|
case GSHWDrawConfig::TEX_HAZARD_NONE: return "NONE";
|
|
case GSHWDrawConfig::TEX_HAZARD_RT: return "RT";
|
|
case GSHWDrawConfig::TEX_HAZARD_DEPTH: return "DEPTH";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
static const char* GetPSROVDepthname(GSHWDrawConfig::PS_ROV_DEPTH rov_depth)
|
|
{
|
|
switch (rov_depth)
|
|
{
|
|
case GSHWDrawConfig::PS_ROV_DEPTH::NONE: return "NONE";
|
|
case GSHWDrawConfig::PS_ROV_DEPTH::READ_ONLY: return "READ_ONLY";
|
|
case GSHWDrawConfig::PS_ROV_DEPTH::READ_WRITE: return "READ_WRITE";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
static void DumpPSSelector(DrawConfigWriter& out, const GSHWDrawConfig::PSSelector& ps)
|
|
{
|
|
out.WriteLn("aem_fmt: {}", ps.aem_fmt);
|
|
out.WriteLn("pal_fmt: {}", ps.pal_fmt);
|
|
out.WriteLn("dst_fmt: {} ({})", GetPSDstFmtName(ps.dst_fmt), ps.depth_fmt);
|
|
out.WriteLn("depth_fmt: {} ({})", GetPSDepthFmtName(ps.depth_fmt), ps.depth_fmt);
|
|
out.WriteLn("aem: {}", ps.aem);
|
|
out.WriteLn("fba: {}", ps.fba);
|
|
out.WriteLn("fog: {}", ps.fog);
|
|
out.WriteLn("iip: {}", ps.iip);
|
|
out.WriteLn("date: {} ({})", GetPSDateName(ps.date), ps.date);
|
|
out.WriteLn("atst: {} ({})", GetPSAlphaTestName(ps.atst), static_cast<u32>(ps.atst));
|
|
out.WriteLn("afail: {} ({})", GetPSAFAILName(ps.afail), static_cast<u32>(ps.afail));
|
|
out.WriteLn("fst: {}", ps.fst);
|
|
out.WriteLn("tfx: {}", ps.tfx);
|
|
out.WriteLn("tcc: {} ({})", GSUtil::GetTCCName(ps.tcc), ps.tcc);
|
|
out.WriteLn("wms: {} ({})", GSUtil::GetWMName(ps.wms), ps.wms);
|
|
out.WriteLn("wmt: {} ({})", GSUtil::GetWMName(ps.wmt), ps.wmt);
|
|
out.WriteLn("adjs: {}", ps.adjs);
|
|
out.WriteLn("adjt: {}", ps.adjt);
|
|
out.WriteLn("ltf: {}", ps.ltf);
|
|
out.WriteLn("shuffle: {}", ps.shuffle);
|
|
out.WriteLn("shuffle_same: {}", ps.shuffle_same);
|
|
out.WriteLn("real16src: {}", ps.real16src);
|
|
out.WriteLn("process_ba: {}", ps.process_ba);
|
|
out.WriteLn("process_rg: {}", ps.process_rg);
|
|
out.WriteLn("shuffle_across: {}", ps.shuffle_across);
|
|
out.WriteLn("write_rg: {}", ps.write_rg);
|
|
out.WriteLn("fbmask: {}", ps.fbmask);
|
|
out.WriteLn("blend: ({} - {}) * {} + {}", GetPSBlendABDName(ps.blend_a), GetPSBlendABDName(ps.blend_b), GetPSBlendCName(ps.blend_c), GetPSBlendABDName(ps.blend_d));
|
|
out.WriteLn("fixed_one_a: {}", ps.fixed_one_a);
|
|
out.WriteLn("blend_hw: {} ({})", GetHWBlendTypeName(static_cast<HWBlendType>(ps.blend_hw), ps.blend_mix), ps.blend_hw);
|
|
out.WriteLn("a_masked: {}", ps.a_masked);
|
|
out.WriteLn("colclip_hw: {}", ps.colclip_hw);
|
|
out.WriteLn("rta_correction: {}", ps.rta_correction);
|
|
out.WriteLn("rta_source_correction: {}", ps.rta_source_correction);
|
|
out.WriteLn("colclip: {}", ps.colclip);
|
|
out.WriteLn("blend_mix: {}", ps.blend_mix);
|
|
out.WriteLn("round_inv: {}", ps.round_inv);
|
|
out.WriteLn("pabe: {}", ps.pabe);
|
|
out.WriteLn("no_color: {}", ps.no_color);
|
|
out.WriteLn("no_color1: {}", ps.no_color1);
|
|
out.WriteLn("channel: {} ({})", GetPSChannelName(static_cast<ChannelFetch>(ps.channel)), ps.channel);
|
|
out.WriteLn("dither: {} ({})", GetPSDitherName(ps.dither), ps.dither);
|
|
out.WriteLn("dither_adjust: {}", ps.dither_adjust);
|
|
out.WriteLn("zclamp: {}", ps.zclamp);
|
|
out.WriteLn("tcoffsethack: {}", ps.tcoffsethack);
|
|
out.WriteLn("urban_chaos_hle: {}", ps.urban_chaos_hle);
|
|
out.WriteLn("tales_of_abyss_hle: {}", ps.tales_of_abyss_hle);
|
|
out.WriteLn("tex_is_fb: {}", ps.tex_is_fb);
|
|
out.WriteLn("automatic_lod: {}", ps.automatic_lod);
|
|
out.WriteLn("manual_lod: {}", ps.manual_lod);
|
|
out.WriteLn("point_sampler: {}", ps.point_sampler);
|
|
out.WriteLn("region_rect: {}", ps.region_rect);
|
|
out.WriteLn("scanmsk: {} ({})", GSUtil::GetSCANMSKName(ps.scanmsk), ps.scanmsk);
|
|
out.WriteLn("aa1: {} ({})", GetPSAA1Name(ps.aa1), static_cast<u32>(ps.aa1));
|
|
out.WriteLn("abe: {}", static_cast<u32>(ps.abe));
|
|
out.WriteLn("sw_aniso: {}", ps.sw_aniso);
|
|
out.WriteLn("rov_color: {}", ps.rov_color);
|
|
out.WriteLn("rov_depth: {} ({})", GetPSROVDepthname(ps.rov_depth), static_cast<u32>(ps.rov_depth));
|
|
out.WriteLn("ztst: {} ({})", GSUtil::GetZTSTName(ps.ztst), static_cast<u32>(ps.ztst));
|
|
out.WriteLn("zfloor: {}", static_cast<u32>(ps.zfloor));
|
|
}
|
|
|
|
static void DumpVSSelector(DrawConfigWriter& out, const GSHWDrawConfig::VSSelector& vs)
|
|
{
|
|
out.WriteLn("fst: {}", vs.fst);
|
|
out.WriteLn("tme: {}", vs.tme);
|
|
out.WriteLn("iip: {}", vs.iip);
|
|
out.WriteLn("point_size: {}", vs.point_size);
|
|
out.WriteLn("expand: {} ({})", GetVSExpandName(vs.expand), static_cast<u32>(vs.expand));
|
|
}
|
|
|
|
static void DumpBlendEquation(DrawConfigWriter& out, const char* name, u32 op, u32 src_factor, u32 dst_factor)
|
|
{
|
|
const char* src_formula = GetBlendFactorFormula(static_cast<GSDevice::BlendFactor>(src_factor));
|
|
const char* dst_formula = GetBlendFactorFormula(static_cast<GSDevice::BlendFactor>(dst_factor));
|
|
const char* symbol = GetBlendOpSymbol(static_cast<GSDevice::BlendOp>(op));
|
|
if (op == GSDevice::OP_REV_SUBTRACT)
|
|
out.WriteLn("{}: Cd * {} - Cs * {}", name, dst_formula, src_formula);
|
|
else
|
|
out.WriteLn("{}: Cs * {} {} Cd * {}", name, src_formula, symbol, dst_formula);
|
|
}
|
|
|
|
static void DumpBlendState(DrawConfigWriter& out, const GSHWDrawConfig::BlendState& bs)
|
|
{
|
|
out.WriteLn("enable: {}", bs.enable);
|
|
out.WriteLn("constant_enable: {}", bs.constant_enable);
|
|
out.WriteLn("constant: {}", bs.constant);
|
|
DumpBlendEquation(out, "equation", bs.op, bs.src_factor, bs.dst_factor);
|
|
DumpBlendEquation(out, "equation_alpha", GSDevice::OP_ADD, bs.src_factor_alpha, bs.dst_factor_alpha);
|
|
}
|
|
|
|
static void DumpDepthStencilSelctor(DrawConfigWriter& out, const GSHWDrawConfig::DepthStencilSelector& dss)
|
|
{
|
|
out.WriteLn("ztst: {} ({})", GSUtil::GetZTSTName(dss.ztst), dss.ztst);
|
|
out.WriteLn("zwe: {}", dss.zwe);
|
|
out.WriteLn("date: {}", dss.date);
|
|
out.WriteLn("date_one: {}", dss.date_one);
|
|
}
|
|
|
|
static void DumpSamplerSelector(DrawConfigWriter& out, const GSHWDrawConfig::SamplerSelector& ss)
|
|
{
|
|
out.WriteLn("tau: {}", ss.tau);
|
|
out.WriteLn("tav: {}", ss.tav);
|
|
out.WriteLn("biln: {}", ss.biln);
|
|
out.WriteLn("triln: {} ({})", GetSSTrilnName(static_cast<GS_MIN_FILTER>(ss.triln)), ss.triln);
|
|
out.WriteLn("lodclamp: {}", ss.lodclamp);
|
|
}
|
|
|
|
static void DumpAlphaPass(DrawConfigWriter& out, const GSHWDrawConfig::AlphaPass& ap)
|
|
{
|
|
out.WriteLn("enable: {}", ap.enable);
|
|
out.WriteLn("require_one_barrier: {}", ap.require_one_barrier);
|
|
out.WriteLn("require_full_barrier: {}", ap.require_full_barrier);
|
|
out.WriteLn("colormask: {:x}", ap.colormask.wrgba);
|
|
out.WriteLn("ps_aref: {}", ap.ps_aref);
|
|
|
|
out.WriteLn("ps:");
|
|
DumpPSSelector(out.WithIndent(), ap.ps);
|
|
|
|
out.WriteLn("dss:");
|
|
DumpDepthStencilSelctor(out.WithIndent(), ap.depth);
|
|
}
|
|
|
|
static void DumpBlendMultipass(DrawConfigWriter& out, const GSHWDrawConfig::BlendMultiPass& bmp)
|
|
{
|
|
out.WriteLn("enable: {}", bmp.enable);
|
|
out.WriteLn("no_color1: {}", bmp.no_color1);
|
|
out.WriteLn("blend_hw: {} ({})", GetPSBlendHWName(static_cast<HWBlendType>(bmp.blend_hw)), bmp.blend_hw);
|
|
out.WriteLn("dither: {}", bmp.dither);
|
|
|
|
out.WriteLn("blend:");
|
|
DumpBlendState(out.WithIndent(), bmp.blend);
|
|
}
|
|
|
|
template<typename T, typename U = int>
|
|
static void DumpVector4(DrawConfigWriter& out, const char* name, const T& val)
|
|
{
|
|
out.WriteLn("{}: [{}, {}, {}, {}]", name, val.x, val.y, val.z, val.w);
|
|
};
|
|
|
|
template<typename T>
|
|
static void DumpVector2(DrawConfigWriter& out, const char* name, const T& val)
|
|
{
|
|
out.WriteLn("{}: [{}, {}]", name, val.x, val.y);
|
|
};
|
|
|
|
static void DumpPSConstantBuffer(DrawConfigWriter& out, const GSHWDrawConfig::PSConstantBuffer& cb)
|
|
{
|
|
DumpVector4(out, "FogColor_AREF", cb.FogColor_AREF);
|
|
DumpVector4(out, "WH", cb.WH);
|
|
DumpVector4(out, "TA_MaxDepth_Af", cb.TA_MaxDepth_Af);
|
|
DumpVector4(out, "FbMask", cb.FbMask);
|
|
DumpVector4(out, "HalfTexel", cb.HalfTexel);
|
|
DumpVector4(out, "MinMax", cb.MinMax);
|
|
DumpVector4(out, "LODParams", cb.LODParams);
|
|
DumpVector4(out, "STRange", cb.STRange);
|
|
DumpVector4(out, "ChannelShuffle", cb.ChannelShuffle);
|
|
DumpVector2(out, "ChannelShuffleOffset", cb.ChannelShuffleOffset);
|
|
DumpVector2(out, "TCOffsetHack", cb.TCOffsetHack);
|
|
DumpVector2(out, "STScale", cb.STScale);
|
|
DumpVector4(out, "DitherMatrix_0", cb.DitherMatrix[0]);
|
|
DumpVector4(out, "DitherMatrix_1", cb.DitherMatrix[1]);
|
|
DumpVector4(out, "DitherMatrix_2", cb.DitherMatrix[2]);
|
|
DumpVector4(out, "DitherMatrix_3", cb.DitherMatrix[3]);
|
|
DumpVector4(out, "ScaleFactor", cb.ScaleFactor);
|
|
out.WriteLn("LineCovScale: {}", cb.LineCovScale);
|
|
}
|
|
|
|
static void DumpVSConstantBuffer(DrawConfigWriter& out, const GSHWDrawConfig::VSConstantBuffer& cb)
|
|
{
|
|
DumpVector2(out, "vertex_scale", cb.vertex_scale);
|
|
DumpVector2(out, "vertex_offset", cb.vertex_offset);
|
|
DumpVector2(out, "texture_scale", cb.texture_scale);
|
|
DumpVector2(out, "texture_offset", cb.texture_offset);
|
|
DumpVector2(out, "point_size", cb.point_size);
|
|
out.WriteLn("max_depth: {}", cb.max_depth);
|
|
out.WriteLn("line_aa1_width: {}", cb.line_aa1_width);
|
|
}
|
|
|
|
static void DumpConfig(DrawConfigWriter& out, const GSHWDrawConfig& conf,
|
|
bool ps, bool vs, bool bs, bool dss, bool ss, bool asp, bool bmp, bool cbvs, bool cbps)
|
|
{
|
|
out.WriteLn("topology: {} ({})", GSGetTopologyName(conf.topology), static_cast<u32>(conf.topology));
|
|
out.WriteLn("require_one_barrier: {}", conf.require_one_barrier);
|
|
out.WriteLn("require_full_barrier: {}", conf.require_full_barrier);
|
|
DumpVector4(out, "drawarea", conf.drawarea);
|
|
DumpVector4(out, "samplearea", conf.samplearea);
|
|
out.WriteLn("tex_hazard: {}", GSGetTexHazardName(conf.tex_hazard));
|
|
|
|
out.WriteLn("destination_alpha: {} ({})", GSGetDestinationAlphaModeName(conf.destination_alpha), static_cast<u32>(conf.destination_alpha));
|
|
out.WriteLn("datm: {} ({})", GetSetDATMName(conf.datm), static_cast<u32>(conf.datm));
|
|
out.WriteLn("line_expand: {}", conf.line_expand);
|
|
out.WriteLn("colormask: {:x}", conf.colormask.wrgba);
|
|
|
|
if (ps)
|
|
{
|
|
out.WriteLn("ps:");
|
|
DumpPSSelector(out.WithIndent(), conf.ps);
|
|
}
|
|
|
|
if (vs)
|
|
{
|
|
out.WriteLn("vs:");
|
|
DumpVSSelector(out.WithIndent(), conf.vs);
|
|
}
|
|
|
|
if (bs)
|
|
{
|
|
out.WriteLn("blend:");
|
|
DumpBlendState(out.WithIndent(), conf.blend);
|
|
}
|
|
|
|
if (ss)
|
|
{
|
|
out.WriteLn("sampler:");
|
|
DumpSamplerSelector(out.WithIndent(), conf.sampler);
|
|
}
|
|
|
|
if (dss)
|
|
{
|
|
out.WriteLn("depth:");
|
|
DumpDepthStencilSelctor(out.WithIndent(), conf.depth);
|
|
}
|
|
|
|
if (asp)
|
|
{
|
|
out.WriteLn("alpha_second_pass:");
|
|
DumpAlphaPass(out.WithIndent(), conf.alpha_second_pass);
|
|
}
|
|
|
|
if (bmp)
|
|
{
|
|
out.WriteLn("blend_multi_pass:");
|
|
DumpBlendMultipass(out.WithIndent(), conf.blend_multi_pass);
|
|
}
|
|
|
|
if (cbvs)
|
|
{
|
|
out.WriteLn("cb_vs:");
|
|
DumpVSConstantBuffer(out.WithIndent(), conf.cb_vs);
|
|
}
|
|
|
|
if (cbps)
|
|
{
|
|
out.WriteLn("cb_ps:");
|
|
DumpPSConstantBuffer(out.WithIndent(), conf.cb_ps);
|
|
}
|
|
}
|
|
|
|
void GSHWDrawConfig::DumpConfig(const std::string& path, const GSHWDrawConfig& conf,
|
|
bool ps, bool vs, bool bs, bool dss, bool ss, bool asp, bool bmp, bool cbvs, bool cbps)
|
|
{
|
|
if (FileSystem::ManagedCFilePtr file = FileSystem::OpenManagedCFile(path.c_str(), "w"))
|
|
{
|
|
DrawConfigWriter writer;
|
|
::DumpConfig(writer, conf, ps, vs, bs, dss, ss, asp, bmp, cbvs, cbps);
|
|
fwrite(writer.buffer.data(), 1, writer.buffer.size(), file.get());
|
|
}
|
|
}
|
|
|
|
static constexpr u32 NUM_REMAP_INPUTS = static_cast<u32>(ShaderConvert::Count) * 4;
|
|
|
|
static constexpr ShaderConvertSelector GetRemappedShader(u32 idx)
|
|
{
|
|
ShaderConvert convert = static_cast<ShaderConvert>(idx >> 2);
|
|
bool depth_out = (idx >> 0) & 1;
|
|
Filter filter = static_cast<Filter>((idx >> 1) & 1);
|
|
return ShaderConvertSelector(convert, 0xf, depth_out, filter);
|
|
}
|
|
|
|
static constexpr bool RemapIndexIsValid(u32 idx)
|
|
{
|
|
ShaderConvert convert = static_cast<ShaderConvert>(idx >> 2);
|
|
bool depth_out = (idx >> 0) & 1;
|
|
Filter filter = static_cast<Filter>((idx >> 1) & 1);
|
|
if (HasVariableWriteMask(convert) && !depth_out && filter == Nearest)
|
|
return false; // Handled as variable write mask
|
|
if (depth_out && !HasFloat32Output(convert))
|
|
return false;
|
|
if (filter == Biln && !SupportsBilinear(convert))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
static constexpr u32 CalcNumRemappedShaders()
|
|
{
|
|
u32 num = 0;
|
|
for (u32 i = 0; i < NUM_REMAP_INPUTS; i++)
|
|
num += RemapIndexIsValid(i);
|
|
return num;
|
|
}
|
|
|
|
static constexpr u32 NUM_REMAPPED_SHADERS = CalcNumRemappedShaders();
|
|
static constexpr u32 NUM_TOTAL_SHADERS = NUM_REMAPPED_SHADERS +
|
|
16 * ShaderConvertSelector::NUM_VARIABLE_WRITE_MASK_SHADERS;
|
|
static_assert(NUM_REMAPPED_SHADERS <= 256); // We use u8 for the remap indices.
|
|
|
|
static constexpr std::array<u8, NUM_REMAP_INPUTS> GenRemapArray()
|
|
{
|
|
std::array<u8, NUM_REMAP_INPUTS> out{};
|
|
u8 out_idx = 0;
|
|
const u8 invalid = 0xff;
|
|
for (u32 i = 0; i < NUM_REMAP_INPUTS; i++)
|
|
out[i] = RemapIndexIsValid(i) ? out_idx++ : invalid;
|
|
return out;
|
|
}
|
|
|
|
static constexpr std::array<ShaderConvertSelector, NUM_TOTAL_SHADERS> GetPackedShaders()
|
|
{
|
|
std::array<ShaderConvertSelector, NUM_TOTAL_SHADERS> out{};
|
|
u32 append_idx = 0;
|
|
for (u32 i = 0; i < NUM_REMAP_INPUTS; i++)
|
|
{
|
|
if (RemapIndexIsValid(i))
|
|
out[append_idx++] = GetRemappedShader(i);
|
|
}
|
|
for (u32 i = 0; i < 16; i++)
|
|
out[append_idx++] = ShaderConvertSelector(ShaderConvert::COPY, i);
|
|
for (u32 i = 0; i < 16; i++)
|
|
out[append_idx++] = ShaderConvertSelector(ShaderConvert::RTA_CORRECTION, i);
|
|
return out;
|
|
}
|
|
|
|
constinit const u32 ShaderConvertSelector::NUM_REMAPPED_SHADERS = ::NUM_REMAPPED_SHADERS;
|
|
constinit const u32 ShaderConvertSelector::NUM_TOTAL_SHADERS = ::NUM_TOTAL_SHADERS;
|
|
constinit const std::array<u8, NUM_REMAP_INPUTS> ShaderConvertSelector::INDEX_REMAP = GenRemapArray();
|
|
static constexpr auto PACKED_SHADERS = GetPackedShaders();
|
|
const std::span<const ShaderConvertSelector> ShaderConvertSelector::SHADERS = PACKED_SHADERS;
|
|
|
|
// clang-format off
|
|
|
|
// Maps PS2 blend modes to our best approximation of them with PC hardware
|
|
const std::array<HWBlend, 3*3*3*3> GSDevice::m_blendMap =
|
|
{{
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ONE , CONST_ZERO} , // 0000: (Cs - Cs)*As + Cs ==> Cs
|
|
{ BLEND_CD , OP_ADD , CONST_ZERO , CONST_ONE} , // 0001: (Cs - Cs)*As + Cd ==> Cd
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ZERO , CONST_ZERO} , // 0002: (Cs - Cs)*As + 0 ==> 0
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ONE , CONST_ZERO} , // 0010: (Cs - Cs)*Ad + Cs ==> Cs
|
|
{ BLEND_CD , OP_ADD , CONST_ZERO , CONST_ONE} , // 0011: (Cs - Cs)*Ad + Cd ==> Cd
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ZERO , CONST_ZERO} , // 0012: (Cs - Cs)*Ad + 0 ==> 0
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ONE , CONST_ZERO} , // 0020: (Cs - Cs)*F + Cs ==> Cs
|
|
{ BLEND_CD , OP_ADD , CONST_ZERO , CONST_ONE} , // 0021: (Cs - Cs)*F + Cd ==> Cd
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ZERO , CONST_ZERO} , // 0022: (Cs - Cs)*F + 0 ==> 0
|
|
{ BLEND_A_MAX | BLEND_MIX2 , OP_SUBTRACT , CONST_ONE , SRC1_COLOR} , // 0100: (Cs - Cd)*As + Cs ==> Cs*(As + 1) - Cd*As
|
|
{ BLEND_MIX1 , OP_ADD , SRC1_COLOR , INV_SRC1_COLOR} , // 0101: (Cs - Cd)*As + Cd ==> Cs*As + Cd*(1 - As)
|
|
{ BLEND_MIX1 , OP_SUBTRACT , SRC1_COLOR , SRC1_COLOR} , // 0102: (Cs - Cd)*As + 0 ==> Cs*As - Cd*As
|
|
{ BLEND_A_MAX , OP_SUBTRACT , CONST_ONE , DST_ALPHA} , // 0110: (Cs - Cd)*Ad + Cs ==> Cs*(Ad + 1) - Cd*Ad
|
|
{ 0 , OP_ADD , DST_ALPHA , INV_DST_ALPHA} , // 0111: (Cs - Cd)*Ad + Cd ==> Cs*Ad + Cd*(1 - Ad)
|
|
{ BLEND_HW5 , OP_SUBTRACT , DST_ALPHA , DST_ALPHA} , // 0112: (Cs - Cd)*Ad + 0 ==> Cs*Ad - Cd*Ad
|
|
{ BLEND_A_MAX | BLEND_MIX2 , OP_SUBTRACT , CONST_ONE , CONST_COLOR} , // 0120: (Cs - Cd)*F + Cs ==> Cs*(F + 1) - Cd*F
|
|
{ BLEND_MIX1 , OP_ADD , CONST_COLOR , INV_CONST_COLOR} , // 0121: (Cs - Cd)*F + Cd ==> Cs*F + Cd*(1 - F)
|
|
{ BLEND_MIX1 , OP_SUBTRACT , CONST_COLOR , CONST_COLOR} , // 0122: (Cs - Cd)*F + 0 ==> Cs*F - Cd*F
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ONE , CONST_ZERO} , // 0200: (Cs - 0)*As + Cs ==> Cs*(As + 1)
|
|
{ BLEND_ACCU , OP_ADD , SRC1_COLOR , CONST_ONE} , // 0201: (Cs - 0)*As + Cd ==> Cs*As + Cd
|
|
{ BLEND_NO_REC , OP_ADD , SRC1_COLOR , CONST_ZERO} , // 0202: (Cs - 0)*As + 0 ==> Cs*As
|
|
{ BLEND_A_MAX | BLEND_HW8 , OP_ADD , CONST_ONE , CONST_ZERO} , // 0210: (Cs - 0)*Ad + Cs ==> Cs*(Ad + 1)
|
|
{ BLEND_HW3 , OP_ADD , DST_ALPHA , CONST_ONE} , // 0211: (Cs - 0)*Ad + Cd ==> Cs*Ad + Cd
|
|
{ BLEND_HW3 , OP_ADD , DST_ALPHA , CONST_ZERO} , // 0212: (Cs - 0)*Ad + 0 ==> Cs*Ad
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ONE , CONST_ZERO} , // 0220: (Cs - 0)*F + Cs ==> Cs*(F + 1)
|
|
{ BLEND_ACCU , OP_ADD , CONST_COLOR , CONST_ONE} , // 0221: (Cs - 0)*F + Cd ==> Cs*F + Cd
|
|
{ BLEND_NO_REC , OP_ADD , CONST_COLOR , CONST_ZERO} , // 0222: (Cs - 0)*F + 0 ==> Cs*F
|
|
{ BLEND_MIX3 , OP_ADD , INV_SRC1_COLOR , SRC1_COLOR} , // 1000: (Cd - Cs)*As + Cs ==> Cd*As + Cs*(1 - As)
|
|
{ BLEND_A_MAX | BLEND_MIX1 , OP_REV_SUBTRACT , SRC1_COLOR , CONST_ONE} , // 1001: (Cd - Cs)*As + Cd ==> Cd*(As + 1) - Cs*As
|
|
{ BLEND_MIX1 , OP_REV_SUBTRACT , SRC1_COLOR , SRC1_COLOR} , // 1002: (Cd - Cs)*As + 0 ==> Cd*As - Cs*As
|
|
{ 0 , OP_ADD , INV_DST_ALPHA , DST_ALPHA} , // 1010: (Cd - Cs)*Ad + Cs ==> Cd*Ad + Cs*(1 - Ad)
|
|
{ BLEND_A_MAX , OP_REV_SUBTRACT , DST_ALPHA , CONST_ONE} , // 1011: (Cd - Cs)*Ad + Cd ==> Cd*(Ad + 1) - Cs*Ad
|
|
{ BLEND_HW5 , OP_REV_SUBTRACT , DST_ALPHA , DST_ALPHA} , // 1012: (Cd - Cs)*Ad + 0 ==> Cd*Ad - Cs*Ad
|
|
{ BLEND_MIX3 , OP_ADD , INV_CONST_COLOR , CONST_COLOR} , // 1020: (Cd - Cs)*F + Cs ==> Cd*F + Cs*(1 - F)
|
|
{ BLEND_A_MAX | BLEND_MIX1 , OP_REV_SUBTRACT , CONST_COLOR , CONST_ONE} , // 1021: (Cd - Cs)*F + Cd ==> Cd*(F + 1) - Cs*F
|
|
{ BLEND_MIX1 , OP_REV_SUBTRACT , CONST_COLOR , CONST_COLOR} , // 1022: (Cd - Cs)*F + 0 ==> Cd*F - Cs*F
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ONE , CONST_ZERO} , // 1100: (Cd - Cd)*As + Cs ==> Cs
|
|
{ BLEND_CD , OP_ADD , CONST_ZERO , CONST_ONE} , // 1101: (Cd - Cd)*As + Cd ==> Cd
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ZERO , CONST_ZERO} , // 1102: (Cd - Cd)*As + 0 ==> 0
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ONE , CONST_ZERO} , // 1110: (Cd - Cd)*Ad + Cs ==> Cs
|
|
{ BLEND_CD , OP_ADD , CONST_ZERO , CONST_ONE} , // 1111: (Cd - Cd)*Ad + Cd ==> Cd
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ZERO , CONST_ZERO} , // 1112: (Cd - Cd)*Ad + 0 ==> 0
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ONE , CONST_ZERO} , // 1120: (Cd - Cd)*F + Cs ==> Cs
|
|
{ BLEND_CD , OP_ADD , CONST_ZERO , CONST_ONE} , // 1121: (Cd - Cd)*F + Cd ==> Cd
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ZERO , CONST_ZERO} , // 1122: (Cd - Cd)*F + 0 ==> 0
|
|
{ BLEND_HW4 , OP_ADD , CONST_ONE , SRC1_COLOR} , // 1200: (Cd - 0)*As + Cs ==> Cs + Cd*As
|
|
{ BLEND_HW1 , OP_ADD , DST_COLOR , SRC1_COLOR} , // 1201: (Cd - 0)*As + Cd ==> Cd*(1 + As)
|
|
{ BLEND_HW2 , OP_ADD , DST_COLOR , SRC1_COLOR} , // 1202: (Cd - 0)*As + 0 ==> Cd*As
|
|
{ BLEND_HW6 , OP_ADD , CONST_ONE , DST_ALPHA} , // 1210: (Cd - 0)*Ad + Cs ==> Cs + Cd*Ad
|
|
{ BLEND_HW1 , OP_ADD , DST_COLOR , DST_ALPHA} , // 1211: (Cd - 0)*Ad + Cd ==> Cd*(1 + Ad)
|
|
{ BLEND_HW5 , OP_ADD , CONST_ZERO , DST_ALPHA} , // 1212: (Cd - 0)*Ad + 0 ==> Cd*Ad
|
|
{ BLEND_HW4 , OP_ADD , CONST_ONE , CONST_COLOR} , // 1220: (Cd - 0)*F + Cs ==> Cs + Cd*F
|
|
{ BLEND_HW1 , OP_ADD , DST_COLOR , CONST_COLOR} , // 1221: (Cd - 0)*F + Cd ==> Cd*(1 + F)
|
|
{ BLEND_HW2 , OP_ADD , DST_COLOR , CONST_COLOR} , // 1222: (Cd - 0)*F + 0 ==> Cd*F
|
|
{ BLEND_NO_REC , OP_ADD , INV_SRC1_COLOR , CONST_ZERO} , // 2000: (0 - Cs)*As + Cs ==> Cs*(1 - As)
|
|
{ BLEND_ACCU , OP_REV_SUBTRACT , SRC1_COLOR , CONST_ONE} , // 2001: (0 - Cs)*As + Cd ==> Cd - Cs*As
|
|
{ BLEND_NO_REC , OP_REV_SUBTRACT , SRC1_COLOR , CONST_ZERO} , // 2002: (0 - Cs)*As + 0 ==> 0 - Cs*As
|
|
{ BLEND_HW9 , OP_ADD , INV_DST_ALPHA , CONST_ZERO} , // 2010: (0 - Cs)*Ad + Cs ==> Cs*(1 - Ad)
|
|
{ BLEND_HW3 , OP_REV_SUBTRACT , DST_ALPHA , CONST_ONE} , // 2011: (0 - Cs)*Ad + Cd ==> Cd - Cs*Ad
|
|
{ 0 , OP_REV_SUBTRACT , DST_ALPHA , CONST_ZERO} , // 2012: (0 - Cs)*Ad + 0 ==> 0 - Cs*Ad
|
|
{ BLEND_NO_REC , OP_ADD , INV_CONST_COLOR , CONST_ZERO} , // 2020: (0 - Cs)*F + Cs ==> Cs*(1 - F)
|
|
{ BLEND_ACCU , OP_REV_SUBTRACT , CONST_COLOR , CONST_ONE} , // 2021: (0 - Cs)*F + Cd ==> Cd - Cs*F
|
|
{ BLEND_NO_REC , OP_REV_SUBTRACT , CONST_COLOR , CONST_ZERO} , // 2022: (0 - Cs)*F + 0 ==> 0 - Cs*F
|
|
{ BLEND_HW4 , OP_SUBTRACT , CONST_ONE , SRC1_COLOR} , // 2100: (0 - Cd)*As + Cs ==> Cs - Cd*As
|
|
{ 0 , OP_ADD , CONST_ZERO , INV_SRC1_COLOR} , // 2101: (0 - Cd)*As + Cd ==> Cd*(1 - As)
|
|
{ 0 , OP_SUBTRACT , CONST_ZERO , SRC1_COLOR} , // 2102: (0 - Cd)*As + 0 ==> 0 - Cd*As
|
|
{ BLEND_HW6 , OP_SUBTRACT , CONST_ONE , DST_ALPHA} , // 2110: (0 - Cd)*Ad + Cs ==> Cs - Cd*Ad
|
|
{ BLEND_HW7 , OP_ADD , CONST_ZERO , INV_DST_ALPHA} , // 2111: (0 - Cd)*Ad + Cd ==> Cd*(1 - Ad)
|
|
{ 0 , OP_SUBTRACT , CONST_ZERO , DST_ALPHA} , // 2112: (0 - Cd)*Ad + 0 ==> 0 - Cd*Ad
|
|
{ BLEND_HW4 , OP_SUBTRACT , CONST_ONE , CONST_COLOR} , // 2120: (0 - Cd)*F + Cs ==> Cs - Cd*F
|
|
{ 0 , OP_ADD , CONST_ZERO , INV_CONST_COLOR} , // 2121: (0 - Cd)*F + Cd ==> Cd*(1 - F)
|
|
{ 0 , OP_SUBTRACT , CONST_ZERO , CONST_COLOR} , // 2122: (0 - Cd)*F + 0 ==> 0 - Cd*F
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ONE , CONST_ZERO} , // 2200: (0 - 0)*As + Cs ==> Cs
|
|
{ BLEND_CD , OP_ADD , CONST_ZERO , CONST_ONE} , // 2201: (0 - 0)*As + Cd ==> Cd
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ZERO , CONST_ZERO} , // 2202: (0 - 0)*As + 0 ==> 0
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ONE , CONST_ZERO} , // 2210: (0 - 0)*Ad + Cs ==> Cs
|
|
{ BLEND_CD , OP_ADD , CONST_ZERO , CONST_ONE} , // 2211: (0 - 0)*Ad + Cd ==> Cd
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ZERO , CONST_ZERO} , // 2212: (0 - 0)*Ad + 0 ==> 0
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ONE , CONST_ZERO} , // 2220: (0 - 0)*F + Cs ==> Cs
|
|
{ BLEND_CD , OP_ADD , CONST_ZERO , CONST_ONE} , // 2221: (0 - 0)*F + Cd ==> Cd
|
|
{ BLEND_NO_REC , OP_ADD , CONST_ZERO , CONST_ZERO} , // 2222: (0 - 0)*F + 0 ==> 0
|
|
}};
|