mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
74bb133b50
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
246 lines
11 KiB
C
246 lines
11 KiB
C
/* quality_tier.c -- graphics preset autodetection rules (quality_tier.h).
|
|
*
|
|
* A static heuristic, no benchmark (docs/QUALITY_PRESETS.md): recognised
|
|
* hardware gets Low or Ultra; an unrecognised GPU gets Medium (or the nearest
|
|
* lower preset the title offers, psx_quality_pick_offered); High is never
|
|
* detected, only chosen by the player. Few CPU threads or little memory cap
|
|
* the result at Low. */
|
|
#include "quality_tier.h"
|
|
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
static const char *const kNames[] = {"low", "medium", "high", "ultra", "custom"};
|
|
static const char *const kLabels[] = {"Low", "Medium", "High", "Ultra", "Custom"};
|
|
|
|
const char *psx_quality_name(int tier) {
|
|
return (tier >= 0 && tier <= PSX_QUALITY_CUSTOM) ? kNames[tier] : NULL;
|
|
}
|
|
|
|
int psx_quality_from_name(const char *name) {
|
|
if (!name) return PSX_QUALITY_NONE;
|
|
for (int i = 0; i <= PSX_QUALITY_CUSTOM; i++) {
|
|
const char *a = name, *b = kNames[i];
|
|
while (*a && *b && tolower((unsigned char)*a) == *b) { a++; b++; }
|
|
if (!*a && !*b) return i;
|
|
}
|
|
return PSX_QUALITY_NONE;
|
|
}
|
|
|
|
static void lower_copy(const char *in, char *out, size_t cap) {
|
|
size_t i = 0;
|
|
for (; in && in[i] && i + 1 < cap; i++) out[i] = (char)tolower((unsigned char)in[i]);
|
|
out[i] = 0;
|
|
}
|
|
|
|
static int has(const char *hay, const char *needle) { return strstr(hay, needle) != NULL; }
|
|
|
|
/* "apple m<N>[ pro| max| ultra]" -> generation N and variant 0 base, 1 pro,
|
|
* 2 max/ultra. 0 when not Apple silicon. */
|
|
static int apple_gen(const char *s, int *variant) {
|
|
const char *p = strstr(s, "apple m");
|
|
*variant = 0;
|
|
if (!p) return 0;
|
|
p += 7;
|
|
int gen = 0;
|
|
while (isdigit((unsigned char)*p)) gen = gen * 10 + (*p++ - '0');
|
|
if (has(p, "max") || has(p, "ultra")) *variant = 2;
|
|
else if (has(p, "pro")) *variant = 1;
|
|
return gen;
|
|
}
|
|
|
|
/* Autodetect picks between Low and Ultra only (owner decision 2026-10-08):
|
|
* low-end machines -- software GL, the Steam Deck's APU, mobile GPUs, every
|
|
* integrated GPU up to the 890M class, Apple M1/M2 base chips, GeForce MX/GT
|
|
* -- start on Low; recognised capable GPUs start on Ultra
|
|
* with dynamic resolution, Smooth motion from surplus and the adaptive aspect
|
|
* as the safety net. A GPU the rules do not recognise gets Medium (see
|
|
* psx_quality_classify). High is a manual choice.
|
|
*
|
|
* The input is a GL_RENDERER string as Windows drivers, Mesa and macOS
|
|
* report it ("AMD Radeon(TM) Graphics", "AMD Radeon RX Vega 11 Graphics",
|
|
* "AMD Radeon Graphics (radeonsi, renoir, LLVM 15.0.7, DRM 3.54, ...)",
|
|
* "Mesa Intel(R) UHD Graphics 620 (KBL GT2)", "NVIDIA GeForce RTX 3060/PCIe/SSE2",
|
|
* "Apple M2 Pro"). tests: recompiler/tests/quality_presets_test.cpp.
|
|
* Returns -1 when the name says nothing we know (=> Ultra). */
|
|
|
|
/* "radeon 680m" ... "radeon 890m": the integrated RDNA parts. */
|
|
static int amd_igpu_number(const char *g) {
|
|
for (const char *p = strstr(g, "radeon"); p; p = strstr(p + 1, "radeon")) {
|
|
const char *q = p + 6;
|
|
if (strncmp(q, "(tm)", 4) == 0) q += 4;
|
|
while (*q == ' ') q++;
|
|
if (q[0] >= '1' && q[0] <= '9' && q[1] >= '0' && q[1] <= '9' &&
|
|
q[2] >= '0' && q[2] <= '9' && q[3] == 'm') return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int gpu_tier(const char *g, const char **why) {
|
|
int variant = 0;
|
|
const int gen = apple_gen(g, &variant);
|
|
if (gen) {
|
|
*why = "Apple silicon";
|
|
/* M1 / M2 base GPUs (7-10 cores): low-end class. */
|
|
return (variant == 0 && gen <= 2) ? PSX_QUALITY_LOW : PSX_QUALITY_ULTRA;
|
|
}
|
|
if (has(g, "llvmpipe") || has(g, "softpipe") || has(g, "swiftshader") || has(g, "lavapipe") ||
|
|
has(g, "basic render") || has(g, "gdi generic") || has(g, "software")) {
|
|
*why = "software rendering";
|
|
return PSX_QUALITY_LOW;
|
|
}
|
|
if (has(g, "vangogh") || has(g, "van gogh") || has(g, "custom gpu 0405") ||
|
|
has(g, "custom gpu 0932")) {
|
|
*why = "Steam Deck GPU";
|
|
return PSX_QUALITY_LOW;
|
|
}
|
|
if (has(g, "mali") || has(g, "adreno") || has(g, "powervr") || has(g, "videocore") ||
|
|
has(g, "tegra")) {
|
|
*why = "mobile GPU";
|
|
return PSX_QUALITY_LOW;
|
|
}
|
|
if (has(g, "nvidia") || has(g, "geforce") || has(g, "quadro") || has(g, "rtx")) {
|
|
*why = "NVIDIA GPU";
|
|
if (has(g, " mx") || has(g, "gt ") || has(g, "gt1") || has(g, "gt7") || has(g, "gt6"))
|
|
return PSX_QUALITY_LOW;
|
|
return PSX_QUALITY_ULTRA;
|
|
}
|
|
if (has(g, "intel")) {
|
|
/* Arc discrete cards are "Intel(R) Arc(TM) A770 Graphics"; the
|
|
* integrated Arc of Meteor/Lunar Lake is "Intel(R) Arc(TM) Graphics". */
|
|
if (has(g, "arc") && (has(g, " a3") || has(g, " a5") || has(g, " a7") ||
|
|
has(g, " b5") || has(g, " b7"))) {
|
|
*why = "Intel Arc GPU";
|
|
return PSX_QUALITY_ULTRA;
|
|
}
|
|
*why = "Intel integrated GPU";
|
|
return PSX_QUALITY_LOW; /* HD / UHD / Iris / Xe / Arc iGPU */
|
|
}
|
|
if (has(g, "radeon") || has(g, "amd") || has(g, "ati ")) {
|
|
if (has(g, "8060s") || has(g, "8050s")) {
|
|
*why = "AMD Strix Halo";
|
|
return PSX_QUALITY_ULTRA;
|
|
}
|
|
/* Integrated: "Radeon(TM) Graphics", "Radeon Graphics", "Radeon
|
|
* Vega 8 Graphics", "Radeon RX Vega 11 Graphics" (note the RX),
|
|
* "Radeon 780M". Discrete Vega is "RX Vega 56/64" (no "Graphics"). */
|
|
const int vega_igpu = has(g, "vega") && has(g, "graphics") &&
|
|
!has(g, "vega 56") && !has(g, "vega 64");
|
|
if (vega_igpu || has(g, "radeon(tm) graphics") || has(g, "radeon graphics") ||
|
|
amd_igpu_number(g) || has(g, "r5 graphics") || has(g, "r7 graphics")) {
|
|
*why = "AMD integrated GPU";
|
|
return PSX_QUALITY_LOW;
|
|
}
|
|
*why = "AMD Radeon GPU";
|
|
return PSX_QUALITY_ULTRA; /* RX, Pro, W-series, Vega 56/64 */
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/* Linux without a GL probe: the PCI id from sysfs ("0x8086:0x5917").
|
|
* Vendor-level only, plus the Steam Deck parts. */
|
|
static int pci_tier(const char *id, const char **why) {
|
|
unsigned vendor = 0, device = 0;
|
|
if (!id || sscanf(id, "%x:%x", &vendor, &device) != 2) return -1;
|
|
if (vendor == 0x1002 && (device == 0x163f || device == 0x1435)) {
|
|
*why = "Steam Deck GPU (PCI id)";
|
|
return PSX_QUALITY_LOW;
|
|
}
|
|
if (vendor == 0x8086) {
|
|
/* Arc discrete: 0x56xx (Alchemist), 0xe20x (Battlemage). */
|
|
if ((device & 0xff00) == 0x5600 || (device & 0xfff0) == 0xe200) {
|
|
*why = "Intel Arc GPU (PCI id)";
|
|
return PSX_QUALITY_ULTRA;
|
|
}
|
|
*why = "Intel integrated GPU (PCI id)";
|
|
return PSX_QUALITY_LOW;
|
|
}
|
|
if (vendor == 0x10de) { *why = "NVIDIA GPU (PCI id)"; return PSX_QUALITY_ULTRA; }
|
|
return -1; /* AMD: the id alone does not tell an APU from a card (=> Medium) */
|
|
}
|
|
|
|
int psx_quality_classify(const PsxHostInfo *h, char *reason, size_t reason_cap) {
|
|
char g[160], c[160];
|
|
lower_copy(h ? h->gpu : "", g, sizeof g);
|
|
lower_copy(h ? h->cpu : "", c, sizeof c);
|
|
const char *why = "unknown GPU";
|
|
int tier = -1;
|
|
const int deck = h && (h->steam_deck || has(c, "custom apu 0405") || has(c, "custom apu 0932"));
|
|
if (deck) {
|
|
tier = PSX_QUALITY_LOW;
|
|
why = "Steam Deck";
|
|
}
|
|
if (tier < 0 && g[0]) tier = gpu_tier(g, &why);
|
|
if (tier < 0 && !g[0] && h && h->gpu_id[0]) tier = pci_tier(h->gpu_id, &why);
|
|
if (tier < 0) tier = gpu_tier(c, &why); /* Apple: the CPU names the GPU */
|
|
/* Unrecognised GPU: Medium, not Ultra. Nothing was measured, so the
|
|
* expensive Ultra-only work (1.5x supersampling, mipmaps + 16x aniso,
|
|
* bloom) is not assumed; Medium keeps the display's own resolution,
|
|
* Smooth motion from surplus and the PGXP extras, which Low would drop
|
|
* on what is often new, capable hardware. A title without Medium falls
|
|
* to the nearest lower preset (psx_quality_pick_offered). */
|
|
if (tier < 0) { tier = PSX_QUALITY_MEDIUM; why = "unknown GPU"; }
|
|
|
|
/* Only what the dynamic systems cannot absorb: too few CPU threads for
|
|
* the emulation + render threads, or too little memory. */
|
|
const char *cap_why = NULL;
|
|
if (h && h->logical_cores > 0 && h->logical_cores < 4) {
|
|
if (tier > PSX_QUALITY_LOW) { tier = PSX_QUALITY_LOW; cap_why = "fewer than 4 CPU threads"; }
|
|
} else if (h && h->logical_cores > 0 && h->logical_cores == 4) {
|
|
if (tier > PSX_QUALITY_LOW) { tier = PSX_QUALITY_LOW; cap_why = "4 CPU threads"; }
|
|
}
|
|
if (h && h->ram_mb > 0 && h->ram_mb < 6u * 1024u) {
|
|
if (tier > PSX_QUALITY_LOW) { tier = PSX_QUALITY_LOW; cap_why = "under 6 GB of memory"; }
|
|
}
|
|
if (reason && reason_cap) {
|
|
if (cap_why)
|
|
snprintf(reason, reason_cap, "%s, capped by %s: %s", why, cap_why, kLabels[tier]);
|
|
else
|
|
snprintf(reason, reason_cap, "%s: %s", why, kLabels[tier]);
|
|
}
|
|
return tier;
|
|
}
|
|
|
|
int psx_quality_pick_offered(int want, unsigned offered_mask) {
|
|
if (!(offered_mask & 0xFu)) return PSX_QUALITY_NONE;
|
|
if (want < 0) want = 0;
|
|
if (want >= PSX_QUALITY_COUNT) want = PSX_QUALITY_COUNT - 1;
|
|
for (int t = want; t >= 0; t--)
|
|
if (offered_mask & (1u << t)) return t;
|
|
for (int t = want + 1; t < PSX_QUALITY_COUNT; t++)
|
|
if (offered_mask & (1u << t)) return t;
|
|
return PSX_QUALITY_NONE;
|
|
}
|
|
|
|
static uint64_t fnv1a(uint64_t h, const char *s) {
|
|
for (; s && *s; s++) { h ^= (unsigned char)*s; h *= 1099511628211ull; }
|
|
return h ^ 0xFFu; /* field separator */
|
|
}
|
|
|
|
void psx_quality_fingerprint(const PsxHostInfo *h, char out[17]) {
|
|
char num[64];
|
|
uint64_t x = 14695981039346656037ull;
|
|
x = fnv1a(x, h->cpu);
|
|
x = fnv1a(x, h->gpu[0] ? h->gpu : h->gpu_id);
|
|
snprintf(num, sizeof num, "%d/%llu/%d", h->logical_cores,
|
|
(unsigned long long)((h->ram_mb + 512u) / 1024u), h->steam_deck);
|
|
x = fnv1a(x, num);
|
|
snprintf(out, 17, "%016llx", (unsigned long long)x);
|
|
}
|
|
|
|
void psx_quality_summary(const PsxHostInfo *h, char *out, size_t cap) {
|
|
const char *gpu = h->gpu[0] ? h->gpu : "";
|
|
const char *cpu = h->cpu[0] ? h->cpu : "Unknown CPU";
|
|
char mem[32] = "";
|
|
if (h->ram_mb) snprintf(mem, sizeof mem, " \xC2\xB7 %llu GB",
|
|
(unsigned long long)((h->ram_mb + 512u) / 1024u));
|
|
/* Apple silicon names the GPU after the CPU: say it once. */
|
|
if (gpu[0] && strcmp(gpu, cpu) != 0)
|
|
snprintf(out, cap, "%s%s \xC2\xB7 %s \xC2\xB7 %d threads%s", h->steam_deck ? "Steam Deck \xC2\xB7 " : "",
|
|
gpu, cpu, h->logical_cores, mem);
|
|
else
|
|
snprintf(out, cap, "%s%s \xC2\xB7 %d threads%s", h->steam_deck ? "Steam Deck \xC2\xB7 " : "",
|
|
cpu, h->logical_cores, mem);
|
|
}
|