Files
ARMSX2/pcsx2/PerGameOverrides.cpp
jpolo1224 c3f859270f Credit the PR contributors in the code, not only the release notes
Merging preserves authorship in git history, but nobody reads git log to find
out who wrote a file. The tree already had the convention -- "Feature
contributed by misantronic (PR #391)" on exportRecentGamesPublic -- it just was
not applied to the three PRs merged here.

  MemoryCardBackup.kt           bmdhacks     (PR #608)
  PerGameOverrides.{h,cpp}      bmdhacks     (PR #593)
  RecentGamesContentProvider.kt misantronic  (PR #566)

The provider's line also records that the opt-in gate was added on merge, so the
gate is not mistaken for part of the contribution.
2026-08-21 15:14:13 -04:00

273 lines
10 KiB
C++

// SPDX-FileCopyrightText: 2026 ARMSX2 Contributors
// SPDX-License-Identifier: GPL-3.0+
// Per-game settings precedence over the game database. Feature contributed by
// bmdhacks (PR #593).
#include "PerGameOverrides.h"
#include "GameDatabase.h"
#include "common/Assertions.h"
#include "common/SettingsInterface.h"
#include <array>
#include <cstring>
using GSHWFixId = GameDatabaseSchema::GSHWFixId;
static_assert(static_cast<u32>(GSHWFixId::Count) <= 64, "PerGameOverrides::gs_fixes has a bit per GS hardware fix");
static_assert(static_cast<u32>(GSUserHackOverride::MaxCount) <= 32, "GSOptions::UserHackOverrides has a bit per user hack");
static_assert(static_cast<u32>(SpeedHack::MaxCount) <= 32, "PerGameOverrides::speedhacks has a bit per speedhack");
namespace
{
// One row per `EmuCore/GS` key the player can set that the game database also has
// an opinion about.
//
// `fix` is what applyGSHardwareFixes() iterates and is the reason a row exists.
// `hack` is the older, narrower vocabulary that MaskUserHacks() and the iOS
// frontend already speak; most rows carry both, and the two that carry only a
// hack are the texture-offset pair, which no database entry ever sets but which
// MaskUserHacks() would otherwise strip out from under a player who set it.
struct GSKeyRow
{
const char* key;
GSHWFixId fix; // GSHWFixId::Count when the database never writes this one
GSUserHackOverride hack; // GSUserHackOverride::MaxCount when it is not a user hack
};
constexpr GSKeyRow s_gs_keys[] = {
{"UserHacks_align_sprite_X", GSHWFixId::AlignSprite, GSUserHackOverride::AlignSprite},
{"UserHacks_merge_pp_sprite", GSHWFixId::MergeSprite, GSUserHackOverride::MergeSprite},
{"UserHacks_round_sprite_offset", GSHWFixId::RoundSprite, GSUserHackOverride::RoundSprite},
{"UserHacks_HalfPixelOffset", GSHWFixId::HalfPixelOffset, GSUserHackOverride::HalfPixelOffset},
{"UserHacks_ForceEvenSpritePosition", GSHWFixId::ForceEvenSpritePosition, GSUserHackOverride::ForceEvenSpritePosition},
{"UserHacks_native_scaling", GSHWFixId::NativeScaling, GSUserHackOverride::NativeScaling},
{"UserHacks_NativePaletteDraw", GSHWFixId::NativePaletteDraw, GSUserHackOverride::NativePaletteDraw},
{"UserHacks_BilinearHack", GSHWFixId::BilinearUpscale, GSUserHackOverride::BilinearHack},
{"UserHacks_TCOffsetX", GSHWFixId::Count, GSUserHackOverride::TextureOffsetX},
{"UserHacks_TCOffsetY", GSHWFixId::Count, GSUserHackOverride::TextureOffsetY},
{"UserHacks_AutoFlushLevel", GSHWFixId::AutoFlush, GSUserHackOverride::AutoFlush},
{"UserHacks_TextureInsideRt", GSHWFixId::TextureInsideRT, GSUserHackOverride::TextureInsideRt},
{"preload_frame_with_gs_data", GSHWFixId::PreloadFrameData, GSUserHackOverride::PreloadFrameData},
{"UserHacks_DisablePartialInvalidation", GSHWFixId::DisablePartialInvalidation, GSUserHackOverride::DisablePartialInvalidation},
{"paltex", GSHWFixId::GPUPaletteConversion, GSUserHackOverride::GPUPaletteConversion},
{"UserHacks_DisableDepthSupport", GSHWFixId::DisableDepthSupport, GSUserHackOverride::DisableDepthSupport},
{"UserHacks_CPU_FB_Conversion", GSHWFixId::CPUFramebufferConversion, GSUserHackOverride::CPUFBConversion},
{"UserHacks_ReadTCOnClose", GSHWFixId::FlushTCOnClose, GSUserHackOverride::ReadTCOnClose},
{"UserHacks_Limit24BitDepth", GSHWFixId::Limit24BitDepth, GSUserHackOverride::Limit24BitDepth},
{"UserHacks_EstimateTextureRegion", GSHWFixId::EstimateTextureRegion, GSUserHackOverride::EstimateTextureRegion},
{"UserHacks_DrawBuffering", GSHWFixId::DrawBuffering, GSUserHackOverride::DrawBuffering},
{"UserHacks_CPUSpriteRenderBW", GSHWFixId::CPUSpriteRenderBW, GSUserHackOverride::CPUSpriteRenderBW},
{"UserHacks_CPUSpriteRenderLevel", GSHWFixId::CPUSpriteRenderLevel, GSUserHackOverride::CPUSpriteRenderLevel},
{"UserHacks_CPUCLUTRender", GSHWFixId::CPUCLUTRender, GSUserHackOverride::CPUCLUTRender},
{"UserHacks_GPUTargetCLUTMode", GSHWFixId::GPUTargetCLUT, GSUserHackOverride::GPUTargetCLUT},
// Settings the database contends that were never part of the user-hack
// vocabulary. They have no hack bit because MaskUserHacks() does not strip
// them; a claim on these only has to reach applyGSHardwareFixes().
{"hw_mipmap", GSHWFixId::Mipmap, GSUserHackOverride::MaxCount},
{"HWAccurateAlphaTest", GSHWFixId::AccurateAlphaTest, GSUserHackOverride::MaxCount},
{"pcrtc_offsets", GSHWFixId::PCRTCOffsets, GSUserHackOverride::MaxCount},
{"pcrtc_overscan", GSHWFixId::PCRTCOverscan, GSUserHackOverride::MaxCount},
{"CoalesceRenderPasses", GSHWFixId::CoalesceRenderPasses, GSUserHackOverride::MaxCount},
{"TriFilter", GSHWFixId::TrilinearFiltering, GSUserHackOverride::MaxCount},
{"UserHacks_SkipDraw_Start", GSHWFixId::SkipDrawStart, GSUserHackOverride::MaxCount},
{"UserHacks_SkipDraw_End", GSHWFixId::SkipDrawEnd, GSUserHackOverride::MaxCount},
{"texture_preloading", GSHWFixId::TexturePreloading, GSUserHackOverride::MaxCount},
{"deinterlace_mode", GSHWFixId::Deinterlace, GSUserHackOverride::MaxCount},
{"HWDownloadMode", GSHWFixId::HWDownloadMode, GSUserHackOverride::MaxCount},
// One control, two database fixes: the database clamps the blend level from
// both ends, so claiming the setting has to silence both clamps or the player
// still gets moved.
{"accurate_blending_unit", GSHWFixId::MinimumBlendingLevel, GSUserHackOverride::MaxCount},
{"accurate_blending_unit", GSHWFixId::MaximumBlendingLevel, GSUserHackOverride::MaxCount},
};
// ⚠️ These are the settings keys, NOT GamefixOptions::GetGameFixName(), which
// returns the game database's YAML spelling. The database calls the first one
// "FpuMul" and the INI calls it "FpuMulHack". Order follows GamefixId.
constexpr const char* s_gamefix_keys[GamefixId_COUNT] = {
"FpuMulHack", // Fix_FpuMultiply
"GoemonTlbHack", // Fix_GoemonTlbMiss
"SoftwareRendererFMVHack", // Fix_SoftwareRendererFMV
"SkipMPEGHack", // Fix_SkipMpeg
"OPHFlagHack", // Fix_OPHFlag
"EETimingHack", // Fix_EETiming
"InstantDMAHack", // Fix_InstantDMA
"DMABusyHack", // Fix_DMABusy
"GIFFIFOHack", // Fix_GIFFIFO
"VIFFIFOHack", // Fix_VIFFIFO
"VIF1StallHack", // Fix_VIF1Stall
"VuAddSubHack", // Fix_VuAddSub
"IbitHack", // Fix_Ibit
"VUSyncHack", // Fix_VUSync
"VUOverflowHack", // Fix_VUOverflow
"XgKickHack", // Fix_XGKick
"BlitInternalFPSHack", // Fix_BlitInternalFPS
"FullVU0SyncHack", // Fix_FullVU0Sync
};
constexpr const char* s_speedhack_keys[static_cast<u32>(SpeedHack::MaxCount)] = {
"vuFlagHack", // SpeedHack::MVUFlag
"vu1Instant", // SpeedHack::InstantVU1
"vuThread", // SpeedHack::MTVU
"EECycleRate", // SpeedHack::EECycleRate
};
} // namespace
PerGameOverrideKeys::CoreKnobKeys PerGameOverrideKeys::ForCoreKnob(CoreGameDBKnob knob)
{
// A clamp mode is one picker over several booleans, and the settings screens
// write every one of them together and delete every one together — see
// AdvancedSettingsWidget::setClampingMode and DrawClampingModeSetting. So any one
// of them being present is an exact test for "the player set this mode".
switch (knob)
{
case CoreGameDBKnob::EERoundMode:
return {"EmuCore/CPU", {"FPU.Roundmode"}, 1};
case CoreGameDBKnob::EEDivRoundMode:
return {"EmuCore/CPU", {"FPUDiv.Roundmode"}, 1};
case CoreGameDBKnob::VU0RoundMode:
return {"EmuCore/CPU", {"VU0.Roundmode"}, 1};
case CoreGameDBKnob::VU1RoundMode:
return {"EmuCore/CPU", {"VU1.Roundmode"}, 1};
case CoreGameDBKnob::EEClampMode:
return {"EmuCore/CPU/Recompiler", {"fpuOverflow", "fpuExtraOverflow", "fpuFullMode", "fpuExactMode"}, 4};
case CoreGameDBKnob::VU0ClampMode:
return {"EmuCore/CPU/Recompiler", {"vu0Overflow", "vu0ExtraOverflow", "vu0SignOverflow"}, 3};
case CoreGameDBKnob::VU1ClampMode:
return {"EmuCore/CPU/Recompiler", {"vu1Overflow", "vu1ExtraOverflow", "vu1SignOverflow"}, 3};
default:
return {nullptr, {}, 0};
}
}
const char* PerGameOverrideKeys::ForGamefix(GamefixId id)
{
if (id < GamefixId_FIRST || id >= GamefixId_COUNT)
return nullptr;
return s_gamefix_keys[static_cast<size_t>(id)];
}
const char* PerGameOverrideKeys::ForSpeedHack(SpeedHack hack)
{
if (hack >= SpeedHack::MaxCount)
return nullptr;
return s_speedhack_keys[static_cast<u32>(hack)];
}
const char* PerGameOverrideKeys::ForGSHWFix(GSHWFixId id)
{
for (const GSKeyRow& row : s_gs_keys)
{
if (row.fix == id)
return row.key;
}
return nullptr;
}
bool PerGameOverrideKeys::ClaimsAGameDBSetting(const char* section, const char* key)
{
if (std::strcmp(section, "EmuCore/GS") == 0)
{
for (const GSKeyRow& row : s_gs_keys)
{
if (std::strcmp(row.key, key) == 0)
return true;
}
return false;
}
if (std::strcmp(section, "EmuCore/Gamefixes") == 0)
{
for (const char* gamefix : s_gamefix_keys)
{
if (std::strcmp(gamefix, key) == 0)
return true;
}
return false;
}
if (std::strcmp(section, "EmuCore/Speedhacks") == 0)
{
for (const char* speedhack : s_speedhack_keys)
{
if (std::strcmp(speedhack, key) == 0)
return true;
}
return false;
}
for (u32 i = 0; i < static_cast<u32>(CoreGameDBKnob::MaxCount); i++)
{
// A knob added to the enum but not to the table lands here with no section.
// The drift tests are what catch that; this only keeps it from being a crash.
const CoreKnobKeys keys = ForCoreKnob(static_cast<CoreGameDBKnob>(i));
if (!keys.section || std::strcmp(section, keys.section) != 0)
continue;
for (u32 k = 0; k < keys.count; k++)
{
if (std::strcmp(keys.keys[k], key) == 0)
return true;
}
}
return false;
}
PerGameOverrides ComputePerGameOverrides(const SettingsInterface& game_layer)
{
PerGameOverrides ov;
for (const GSKeyRow& row : s_gs_keys)
{
if (!game_layer.ContainsValue("EmuCore/GS", row.key))
continue;
if (row.fix != GSHWFixId::Count)
ov.gs_fixes |= u64(1) << static_cast<u64>(row.fix);
if (row.hack != GSUserHackOverride::MaxCount)
ov.gs_hacks |= 1u << static_cast<u32>(row.hack);
}
for (u32 i = 0; i < static_cast<u32>(CoreGameDBKnob::MaxCount); i++)
{
const PerGameOverrideKeys::CoreKnobKeys keys = PerGameOverrideKeys::ForCoreKnob(static_cast<CoreGameDBKnob>(i));
if (!keys.section)
continue;
for (u32 k = 0; k < keys.count; k++)
{
if (!game_layer.ContainsValue(keys.section, keys.keys[k]))
continue;
ov.core |= 1u << i;
break;
}
}
for (u32 i = 0; i < static_cast<u32>(SpeedHack::MaxCount); i++)
{
if (game_layer.ContainsValue("EmuCore/Speedhacks", s_speedhack_keys[i]))
ov.speedhacks |= 1u << i;
}
for (u32 i = GamefixId_FIRST; i < GamefixId_COUNT; i++)
{
if (game_layer.ContainsValue("EmuCore/Gamefixes", s_gamefix_keys[i]))
ov.gamefixes.set(i);
}
return ov;
}