Files
Brian Degenhardt b2e22efc45 Android: send Auto to Vulkan where GL cannot read the target in-tile
The Auto renderer resolution picked Vulkan on Adreno and OpenGL everywhere
else, on the reasoning that Mali runs GL_ARM_shader_framebuffer_fetch and so
has the in-tile fast path on GL. That holds for a healthy Mali. It does not
hold for a driver on the fetch blocklist, and the two decisions were made in
different places, so nothing noticed when they disagreed.

On GLES framebuffer fetch and the texture barrier are one capability -- there
is no ARB or NV barrier extension -- so a blocklisted driver loses both. That
is not a mild fallback on a tiler: it is not only accurate blending that starts
reading the render target from a copy, it is every self-referential draw, and
each copy forces the tile to flush and resolve to main memory. Measured on an
Anbernic RG 477V (Mali-G615, r44p1) with Shadow of the Colossus: 7 fps on
OpenGL against ~30 on Vulkan, same device, same settings. Vulkan reaches the
same copy-based concept with an ordinary image copy and no tile flush.

So Auto now also prefers Vulkan when the device's OpenGL driver profile carries
UseRenderTargetCopyForFeedback. Both halves of the question are asked of the
driver database rather than of substrings, which also retires the case-sensitive
search for "Adreno" in GL_RENDERER in favour of the resolved runtime profile.

The decision has to be native, because the database is: rules match a PARSED
driver revision, which is what lets one say "exactly r44p1". The app cannot do
that, so it now hands over the GL strings it already probes -- GL_VERSION is
where the driver revision lives, and the probe was reading GL_RENDERER and
throwing the rest away -- and GSUtil::AndroidAutoPrefersVulkan answers.
setPreferVulkan(boolean) is replaced by setAutoRendererGpuStrings(3 strings)
rather than kept alongside it; there was one call site.

An explicit Vulkan/OpenGL/SW pick still wins, as before. The only devices this
moves are the ones whose GL is degraded: currently r44p1 Mali and nothing else.
2026-08-12 00:09:08 -07:00

449 lines
14 KiB
C++

// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "GS/GS.h"
#include "GS/GSExtra.h"
#include "GS/GSUtil.h"
#include "MultiISA.h"
#include "common/StringUtil.h"
#include <array>
#if defined(__ANDROID__)
#include "GS/Renderers/Common/GSGPUProfile.h"
#include "common/Console.h"
#endif
#ifdef ENABLE_VULKAN
#include "GS/Renderers/Vulkan/GSDeviceVK.h"
#endif
#ifdef _WIN32
#include "common/RedtapeWindows.h"
#include <d3dcommon.h>
#include <dxgi.h>
#include <VersionHelpers.h>
#include "GS/Renderers/DX11/D3D.h"
#include <wil/com.h>
#endif
namespace {
struct GSUtilMaps
{
u32 CompatibleBitsField[64][2] = {};
u32 SharedBitsField[64][2] = {};
u32 SwizzleField[64][2] = {};
constexpr GSUtilMaps()
{
for (int i = 0; i < 64; i++)
{
CompatibleBitsField[i][i >> 5] |= 1U << (i & 0x1f);
}
CompatibleBitsField[PSMCT32][PSMCT24 >> 5] |= 1 << (PSMCT24 & 0x1f);
CompatibleBitsField[PSMCT24][PSMCT32 >> 5] |= 1 << (PSMCT32 & 0x1f);
CompatibleBitsField[PSMCT16][PSMCT16S >> 5] |= 1 << (PSMCT16S & 0x1f);
CompatibleBitsField[PSMCT16S][PSMCT16 >> 5] |= 1 << (PSMCT16 & 0x1f);
CompatibleBitsField[PSMZ32][PSMZ24 >> 5] |= 1 << (PSMZ24 & 0x1f);
CompatibleBitsField[PSMZ24][PSMZ32 >> 5] |= 1 << (PSMZ32 & 0x1f);
CompatibleBitsField[PSMZ16][PSMZ16S >> 5] |= 1 << (PSMZ16S & 0x1f);
CompatibleBitsField[PSMZ16S][PSMZ16 >> 5] |= 1 << (PSMZ16 & 0x1f);
for (int i = 0; i < 64; i++)
{
SwizzleField[i][i >> 5] |= 1U << (i & 0x1f);
}
SwizzleField[PSMCT32][PSMCT24 >> 5] |= 1 << (PSMCT24 & 0x1f);
SwizzleField[PSMCT24][PSMCT32 >> 5] |= 1 << (PSMCT32 & 0x1f);
SwizzleField[PSMT8H][PSMCT32 >> 5] |= 1 << (PSMCT32 & 0x1f);
SwizzleField[PSMCT32][PSMT8H >> 5] |= 1 << (PSMT8H & 0x1f);
SwizzleField[PSMT4HL][PSMCT32 >> 5] |= 1 << (PSMCT32 & 0x1f);
SwizzleField[PSMCT32][PSMT4HL >> 5] |= 1 << (PSMT4HL & 0x1f);
SwizzleField[PSMT4HH][PSMCT32 >> 5] |= 1 << (PSMCT32 & 0x1f);
SwizzleField[PSMCT32][PSMT4HH >> 5] |= 1 << (PSMT4HH & 0x1f);
SwizzleField[PSMZ32][PSMZ24 >> 5] |= 1 << (PSMZ24 & 0x1f);
SwizzleField[PSMZ24][PSMZ32 >> 5] |= 1 << (PSMZ32 & 0x1f);
SharedBitsField[PSMCT24][PSMT8H >> 5] |= 1 << (PSMT8H & 0x1f);
SharedBitsField[PSMCT24][PSMT4HL >> 5] |= 1 << (PSMT4HL & 0x1f);
SharedBitsField[PSMCT24][PSMT4HH >> 5] |= 1 << (PSMT4HH & 0x1f);
SharedBitsField[PSMZ24][PSMT8H >> 5] |= 1 << (PSMT8H & 0x1f);
SharedBitsField[PSMZ24][PSMT4HL >> 5] |= 1 << (PSMT4HL & 0x1f);
SharedBitsField[PSMZ24][PSMT4HH >> 5] |= 1 << (PSMT4HH & 0x1f);
SharedBitsField[PSMT8H][PSMCT24 >> 5] |= 1 << (PSMCT24 & 0x1f);
SharedBitsField[PSMT8H][PSMZ24 >> 5] |= 1 << (PSMZ24 & 0x1f);
SharedBitsField[PSMT4HL][PSMCT24 >> 5] |= 1 << (PSMCT24 & 0x1f);
SharedBitsField[PSMT4HL][PSMZ24 >> 5] |= 1 << (PSMZ24 & 0x1f);
SharedBitsField[PSMT4HL][PSMT4HH >> 5] |= 1 << (PSMT4HH & 0x1f);
SharedBitsField[PSMT4HH][PSMCT24 >> 5] |= 1 << (PSMCT24 & 0x1f);
SharedBitsField[PSMT4HH][PSMZ24 >> 5] |= 1 << (PSMZ24 & 0x1f);
SharedBitsField[PSMT4HH][PSMT4HL >> 5] |= 1 << (PSMT4HL & 0x1f);
}
};
}
static constexpr const GSUtilMaps s_maps;
const char* GSUtil::GetATSTName(u32 atst)
{
static constexpr const char* names[] = {
"NEVER", "ALWAYS", "LESS", "LEQUAL", "EQUAL", "GEQUAL", "GREATER", "NOTEQUAL" };
return (atst < std::size(names)) ? names[atst] : "";
}
const char* GSUtil::GetAFAILName(u32 afail)
{
static constexpr const char* names[] = {"KEEP", "FB_ONLY", "ZB_ONLY", "RGB_ONLY"};
return (afail < std::size(names)) ? names[afail] : "";
}
const char* GSUtil::GetWMName(u32 wm)
{
static constexpr const char* names[] = {"REPEAT", "CLAMP", "REGION_CLAMP", "REGION_REPEAT"};
return (wm < std::size(names)) ? names[wm] : "";
}
const char* GSUtil::GetZTSTName(u32 ztst)
{
static constexpr const char* names[] = {
"NEVER", "ALWAYS", "GEQUAL", "GREATER"};
return (ztst < std::size(names)) ? names[ztst] : "";
}
const char* GSUtil::GetPrimName(u32 prim)
{
static constexpr const char* names[] = {
"POINT", "LINE", "LINESTRIP", "TRIANGLE", "TRIANGLESTRIP", "TRIANGLEFAN", "SPRITE", "INVALID"};
return (prim < std::size(names)) ? names[prim] : "";
}
const char* GSUtil::GetPrimClassName(u32 primclass)
{
static constexpr const char* names[] = {
"POINT", "LINE", "TRIANGLE", "SPRITE", "INVALID"};
return (primclass < std::size(names)) ? names[primclass] : "";
}
const char* GSUtil::GetMMAGName(u32 mmag)
{
static constexpr const char* names[] = {"NEAREST", "LINEAR"};
return (mmag < std::size(names)) ? names[mmag] : "";
}
const char* GSUtil::GetMMINName(u32 mmin)
{
static constexpr const char* names[8] = {"NEAREST", "LINEAR", "NEAREST_MIPMAP_NEAREST", "NEAREST_MIPMAP_LINEAR",
"LINEAR_MIPMAP_NEAREST", "LINEAR_MIPMAP_LINEAR"};
return (mmin < std::size(names)) ? names[mmin] : "";
}
const char* GSUtil::GetMTBAName(u32 mtba)
{
static constexpr const char* names[] = {"MIPTBP1", "AUTO"};
return (mtba < std::size(names)) ? names[mtba] : "";
}
const char* GSUtil::GetLCMName(u32 lcm)
{
static constexpr const char* names[] = {"Formula", "K"};
return (lcm < std::size(names)) ? names[lcm] : "";
}
const char* GSUtil::GetSCANMSKName(u32 scanmsk)
{
static constexpr const char* names[] = {"Normal", "Reserved", "Even prohibited", "Odd prohibited"};
return (scanmsk < std::size(names)) ? names[scanmsk] : "";
}
const char* GSUtil::GetDATMName(u32 datm)
{
static constexpr const char* names[] = {"0 pass", "1 pass"};
return (datm < std::size(names)) ? names[datm] : "";
}
const char* GSUtil::GetTFXName(u32 tfx)
{
static constexpr const char* names[] = {"MODULATE", "DECAL", "HIGHLIGHT", "HIGHLIGHT2"};
return (tfx < std::size(names)) ? names[tfx] : "";
}
const char* GSUtil::GetTCCName(u32 tcc)
{
static constexpr const char* names[] = {"RGB", "RGBA"};
return (tcc < std::size(names)) ? names[tcc] : "";
}
const char* GSUtil::GetACName(u32 ac)
{
static constexpr const char* names[] = {"PRMODE", "PRIM"};
return (ac < std::size(names)) ? names[ac] : "";
}
const char* GSUtil::GetPerfMonCounterName(GSPerfMon::counter_t counter, bool hw)
{
if (hw)
{
static constexpr const char* names_hw[GSPerfMon::CounterLastHW] = {
"Prim",
"Draw",
"DrawCalls",
"Readbacks",
"Swizzle",
"Unswizzle",
"TextureCopies",
"TextureUploads",
"Barriers",
"RenderPasses",
"TextureCopiesROV",
"DrawCallsROV",
"BarriersROV",
"TCTargetHit",
"TCTargetMiss",
"TCSourceHit",
"TCSourceMiss",
"HashCacheHit",
"HashCacheMiss"
};
return counter < std::size(names_hw) ? names_hw[counter] : "";
}
else
{
static constexpr const char* names_sw[GSPerfMon::CounterLastSW] = {
"Prim",
"Draw",
"DrawCalls",
"Readbacks",
"Swizzle",
"Unswizzle",
"Fillrate",
"SyncPoint"
};
return counter < std::size(names_sw) ? names_sw[counter] : "";
}
}
const u32* GSUtil::HasSharedBitsPtr(u32 dpsm)
{
return s_maps.SharedBitsField[dpsm];
}
bool GSUtil::HasSharedBits(u32 spsm, const u32* RESTRICT ptr)
{
return (ptr[spsm >> 5] & (1 << (spsm & 0x1f))) == 0;
}
// Pixels can NOT coexist in the same 32bits of space.
// Example: Using PSMT8H or PSMT4HL/HH with CT24 would fail this check.
bool GSUtil::HasSharedBits(u32 spsm, u32 dpsm)
{
return (s_maps.SharedBitsField[dpsm][spsm >> 5] & (1 << (spsm & 0x1f))) == 0;
}
// Pixels can NOT coexist in the same 32bits of space.
// Example: Using PSMT8H or PSMT4HL/HH with CT24 would fail this check.
// SBP and DBO must match.
bool GSUtil::HasSharedBits(u32 sbp, u32 spsm, u32 dbp, u32 dpsm)
{
return ((sbp ^ dbp) | (s_maps.SharedBitsField[dpsm][spsm >> 5] & (1 << (spsm & 0x1f)))) == 0;
}
// Shares bit depths, only detects 16/24/32 bit formats.
// 24/32bit cross compatible, 16bit compatbile with 16bit.
bool GSUtil::HasCompatibleBits(u32 spsm, u32 dpsm)
{
return (s_maps.CompatibleBitsField[spsm][dpsm >> 5] & (1 << (dpsm & 0x1f))) != 0;
}
bool GSUtil::HasSameSwizzleBits(u32 spsm, u32 dpsm)
{
return (s_maps.SwizzleField[spsm][dpsm >> 5] & (1 << (dpsm & 0x1f))) != 0;
}
u32 GSUtil::GetChannelMask(u32 spsm)
{
switch (spsm)
{
case PSMCT24:
case PSMZ24:
return 0x7;
case PSMT8H:
case PSMT4HH: // This sucks, I'm sorry, but we don't have a way to do half channels
case PSMT4HL: // So uuhh TODO I guess.
return 0x8;
default:
return 0xf;
}
}
u32 GSUtil::GetChannelMask(u32 spsm, u32 fbmsk)
{
u32 mask = GetChannelMask(spsm);
mask &= ((fbmsk & 0xFF) == 0xFF) ? (~0x1 & 0xf) : 0xf;
mask &= ((fbmsk & 0xFF00) == 0xFF00) ? (~0x2 & 0xf) : 0xf;
mask &= ((fbmsk & 0xFF0000) == 0xFF0000) ? (~0x4 & 0xf) : 0xf;
mask &= ((fbmsk & 0xFF000000) == 0xFF000000) ? (~0x8 & 0xf) : 0xf;
return mask;
}
#if defined(__ANDROID__)
// Set by the Android app from the GL strings (NativeApp.setAutoRendererGpuStrings): true steers the
// Auto renderer resolution to Vulkan HW; false keeps OpenGL HW. See AndroidAutoPrefersVulkan.
bool g_gs_android_prefer_vk = false;
// Kept for the log line in GetPreferredRenderer. The steering decision is made at app startup,
// before the log file is open, so logging it at the point of decision prints into nothing -- which
// is how a silent renderer choice ends up in a bug report as "it just picked OpenGL".
static std::string s_android_gl_renderer;
static std::string s_android_gl_version;
bool GSUtil::AndroidAutoPrefersVulkan(
std::string_view gl_vendor, std::string_view gl_renderer, std::string_view gl_version)
{
s_android_gl_renderer = gl_renderer;
s_android_gl_version = gl_version;
// Both questions are asked of the driver database rather than of substrings, so the set of
// affected devices lives in the one table that already models them and a future bad blob is a
// table row rather than another rule here.
//
// "auto" rather than GSConfig.AndroidGpuProfileOverride deliberately: this runs at app startup,
// before the settings are loaded, so reading config here would sample a default-constructed
// value and look like it worked.
MobileDriverContext driver_context;
driver_context.api = MobileGpuApi::OpenGL;
driver_context.driver_name = gl_renderer;
driver_context.api_version_string = gl_version;
const GpuProfileSelection selection =
GpuProfileDetector::Resolve("auto", gl_vendor, gl_renderer, driver_context);
// Adreno: Vulkan is the tile-memory framebuffer-fetch fast path, and the GL blob is the weaker
// of the two. This is the original rule, now keyed on the resolved profile instead of a
// case-sensitive search for "Adreno" in GL_RENDERER.
if (selection.runtime_profile == RuntimeGpuProfile::Adreno)
return true;
// Any driver whose GL cannot do an in-tile attachment self-read. On a tiler that is not a mild
// fallback: framebuffer fetch and the texture barrier are one capability there (GLES has no
// ARB/NV barrier), so losing fetch loses both, and every self-referential draw -- not just
// accurate blending -- round-trips the tile to main memory. Vulkan reaches the same copy-based
// concept with an ordinary image copy and no tile flush. Measured on a Mali-G615 r44p1
// (Anbernic RG 477V) with Shadow of the Colossus: 7 fps on GL against ~30 on Vulkan, same
// device, same settings. Sending those devices to GL by default is what made 2.6.6.5 unplayable
// on them.
return selection.driver.UsesWorkaround(DriverWorkaround::UseRenderTargetCopyForFeedback);
}
#endif
GSRendererType GSUtil::GetPreferredRenderer()
{
// Memorize the value, so we don't keep re-querying it.
static GSRendererType preferred_renderer = GSRendererType::Auto;
if (preferred_renderer == GSRendererType::Auto)
{
#if defined(__APPLE__)
// Mac: Prefer Metal hardware.
preferred_renderer = GSRendererType::Metal;
#elif defined(_WIN32) && defined(ARCH_ARM64)
// Default to DX12 on Windows-on-ARM.
preferred_renderer = GSRendererType::DX12;
#elif defined(_WIN32)
// Use D3D device info to select renderer.
preferred_renderer = D3D::GetPreferredRenderer();
#elif defined(__ANDROID__)
// Android: Auto resolves to Vulkan HW on Adreno (the tile-memory framebuffer-fetch fast
// path) and on any device whose GL driver cannot read the render target in-tile, OpenGL HW
// elsewhere (a healthy Mali runs GL_ARM_shader_framebuffer_fetch, which is the fast path
// there; Xclipse has no working VK fbfetch). The app sets g_gs_android_prefer_vk from
// AndroidAutoPrefersVulkan before the GS starts. This only steers Auto — an explicit
// Vulkan/OpenGL/SW pick still wins.
#if defined(ENABLE_VULKAN) && defined(ENABLE_OPENGL)
preferred_renderer = g_gs_android_prefer_vk ? GSRendererType::VK : GSRendererType::OGL;
// Logged here rather than where it was decided: the decision happens at app startup, before
// the log file exists. A renderer chosen silently is one nobody can diagnose from a report.
Console.WriteLn("Android: Auto renderer -> %s (GL_RENDERER='%s' GL_VERSION='%s').",
g_gs_android_prefer_vk ? "Vulkan" : "OpenGL", s_android_gl_renderer.c_str(),
s_android_gl_version.c_str());
#elif defined(ENABLE_OPENGL)
preferred_renderer = GSRendererType::OGL;
#elif defined(ENABLE_VULKAN)
preferred_renderer = GSRendererType::VK;
#else
preferred_renderer = GSRendererType::SW;
#endif
#else
// Linux: Prefer Vulkan if the driver isn't buggy.
#if defined(ENABLE_VULKAN)
if (GSDeviceVK::IsSuitableDefaultRenderer())
preferred_renderer = GSRendererType::VK;
#endif
// Otherwise, whatever is available.
if (preferred_renderer == GSRendererType::Auto) // If it's still auto, VK wasn't selected.
#if defined(ENABLE_OPENGL)
preferred_renderer = GSRendererType::OGL;
#elif defined(ENABLE_VULKAN)
preferred_renderer = GSRendererType::VK;
#else
preferred_renderer = GSRendererType::SW;
#endif
#endif
}
return preferred_renderer;
}
const char* GSUtil::GetPSMName(int psm)
{
switch (psm)
{
// Normal color
case PSMCT32: return "C_32";
case PSMCT24: return "C_24";
case PSMCT16: return "C_16";
case PSMCT16S: return "C_16S";
// Palette color
case PSMT8: return "P_8";
case PSMT4: return "P_4";
case PSMT8H: return "P_8H";
case PSMT4HL: return "P_4HL";
case PSMT4HH: return "P_4HH";
// Depth
case PSMZ32: return "Z_32";
case PSMZ24: return "Z_24";
case PSMZ16: return "Z_16";
case PSMZ16S: return "Z_16S";
case PSGPU24: return "PS24";
default:break;
}
return "BAD_PSM";
}
bool GSUtil::IsValidPSM(int psm)
{
switch (psm)
{
case PSMCT32:
case PSMCT24:
case PSMCT16:
case PSMCT16S:
case PSMT8:
case PSMT4:
case PSMT8H:
case PSMT4HL:
case PSMT4HH:
case PSMZ32:
case PSMZ24:
case PSMZ16:
case PSMZ16S:
case PSGPU24:
return true;
default:
return false;
}
}