2022-05-23 01:09:50 +00:00
|
|
|
import os
|
2021-05-05 00:33:43 +00:00
|
|
|
import yaml
|
|
|
|
|
|
|
|
# Assumes this is ran from the root of the repository
|
2022-05-23 01:09:50 +00:00
|
|
|
file_path = os.environ['GAMEDB_PATH']
|
2021-05-05 00:33:43 +00:00
|
|
|
|
|
|
|
# These settings have to be manually kept in sync with the emulator code unfortunately.
|
|
|
|
# up to date validation should ALWAYS be provided via the application!
|
|
|
|
allowed_game_options = [
|
|
|
|
"name",
|
|
|
|
"region",
|
|
|
|
"compat",
|
|
|
|
"roundModes",
|
|
|
|
"clampModes",
|
|
|
|
"gameFixes",
|
2022-03-03 14:32:26 +00:00
|
|
|
"gsHWFixes",
|
2021-05-05 00:33:43 +00:00
|
|
|
"speedHacks",
|
|
|
|
"memcardFilters",
|
|
|
|
"patches",
|
|
|
|
]
|
|
|
|
allowed_round_modes = ["eeRoundMode", "vuRoundMode"]
|
|
|
|
allowed_clamp_modes = ["eeClampMode", "vuClampMode"]
|
|
|
|
allowed_game_fixes = [
|
|
|
|
"FpuMulHack",
|
|
|
|
"FpuNegDivHack",
|
2022-03-14 22:47:23 +00:00
|
|
|
"GoemonTlbHack",
|
|
|
|
"SoftwareRendererFMVHack",
|
2021-05-05 00:33:43 +00:00
|
|
|
"SkipMPEGHack",
|
|
|
|
"OPHFlagHack",
|
2022-03-14 22:47:23 +00:00
|
|
|
"EETimingHack",
|
2021-05-05 00:33:43 +00:00
|
|
|
"DMABusyHack",
|
2022-03-14 22:47:23 +00:00
|
|
|
"GIFFIFOHack",
|
2021-05-05 00:33:43 +00:00
|
|
|
"VIFFIFOHack",
|
|
|
|
"VIF1StallHack",
|
2022-03-14 22:47:23 +00:00
|
|
|
"VuAddSubHack",
|
2021-06-08 18:07:19 +00:00
|
|
|
"IbitHack",
|
2022-03-14 22:47:23 +00:00
|
|
|
"VUSyncHack",
|
2021-09-29 09:50:49 +00:00
|
|
|
"VUOverflowHack",
|
2022-03-14 22:47:23 +00:00
|
|
|
"XGKickHack",
|
2022-04-23 06:37:11 +00:00
|
|
|
"BlitInternalFPSHack",
|
2021-05-05 00:33:43 +00:00
|
|
|
]
|
2022-03-03 14:32:26 +00:00
|
|
|
allowed_gs_hw_fixes = [
|
|
|
|
"autoFlush",
|
|
|
|
"conservativeFramebuffer",
|
|
|
|
"cpuFramebufferConversion",
|
|
|
|
"disableDepthSupport",
|
|
|
|
"wrapGSMem",
|
|
|
|
"preloadFrameData",
|
2022-03-18 13:06:26 +00:00
|
|
|
"disablePartialInvalidation",
|
2022-03-03 14:32:26 +00:00
|
|
|
"textureInsideRT",
|
|
|
|
"alignSprite",
|
|
|
|
"mergeSprite",
|
|
|
|
"wildArmsHack",
|
|
|
|
"pointListPalette",
|
|
|
|
"mipmap",
|
|
|
|
"trilinearFiltering",
|
|
|
|
"skipDrawStart",
|
|
|
|
"skipDrawEnd",
|
|
|
|
"halfBottomOverride",
|
|
|
|
"halfPixelOffset",
|
|
|
|
"roundSprite",
|
|
|
|
"texturePreloading",
|
2022-04-03 17:08:56 +00:00
|
|
|
"deinterlace",
|
2022-06-02 15:00:39 +00:00
|
|
|
"cpuSpriteRenderBW",
|
GameDatabase: Add gpuPaletteConversion = 2 value
Some games (e.g. Metal Gear Solid 2) use large-ish textures, with a
bunch of different CLUTs/palettes, depending on the draw. Kind-of like a
texture atlas.
This causes issues when texture preloading is enabled, as both VRAM and
GS CPU thread usage increase proporiately to the number of texture:clut
pairs (since it has to be hashed).
An alternative to disabling preloading, which is what we currently do,
is enabling GPU palette conversion in these games. Even though we
ever-so-slightly increase the GPU load due to having to do shader
sampling, the CPU load on the GS thread is considerably reduced, and
overall performance is greater. In theory it'll also achieve higher
cache hit rates on the GPU, since we're not duplicating a bunch of
textures.
However, as a general rule of thumb, we don't want to encourage people
to enable paltex, as most games run slower with it on. So, what this PR
does, is add a GameDB option for these types of games, to enable paltex
when texture preloading is set to full/hash cache, but otherwise leave
the setting alone. The best of both worlds.
NOTE: I've also forced paltex=0 for Spider-Man 2, as it uses a massive
number of palettes which can cause descriptor issues in DX12/Vulkan. A
perfect example of where you *don't* want to use paltex.
2022-08-26 10:11:17 +00:00
|
|
|
"gpuPaletteConversion",
|
2022-03-03 14:32:26 +00:00
|
|
|
]
|
|
|
|
gs_hw_fix_ranges = {
|
|
|
|
"mipmap": (0, 2),
|
|
|
|
"trilinearFiltering": (0, 2),
|
|
|
|
"skipDrawStart": (0, 100000),
|
|
|
|
"skipDrawEnd": (0, 100000),
|
|
|
|
"halfPixelOffset": (0, 3),
|
|
|
|
"roundSprite": (0, 2),
|
2022-04-03 17:08:56 +00:00
|
|
|
"deinterlace": (0, 7),
|
2022-06-02 15:00:39 +00:00
|
|
|
"cpuSpriteRenderBW": (1, 10),
|
GameDatabase: Add gpuPaletteConversion = 2 value
Some games (e.g. Metal Gear Solid 2) use large-ish textures, with a
bunch of different CLUTs/palettes, depending on the draw. Kind-of like a
texture atlas.
This causes issues when texture preloading is enabled, as both VRAM and
GS CPU thread usage increase proporiately to the number of texture:clut
pairs (since it has to be hashed).
An alternative to disabling preloading, which is what we currently do,
is enabling GPU palette conversion in these games. Even though we
ever-so-slightly increase the GPU load due to having to do shader
sampling, the CPU load on the GS thread is considerably reduced, and
overall performance is greater. In theory it'll also achieve higher
cache hit rates on the GPU, since we're not duplicating a bunch of
textures.
However, as a general rule of thumb, we don't want to encourage people
to enable paltex, as most games run slower with it on. So, what this PR
does, is add a GameDB option for these types of games, to enable paltex
when texture preloading is set to full/hash cache, but otherwise leave
the setting alone. The best of both worlds.
NOTE: I've also forced paltex=0 for Spider-Man 2, as it uses a massive
number of palettes which can cause descriptor issues in DX12/Vulkan. A
perfect example of where you *don't* want to use paltex.
2022-08-26 10:11:17 +00:00
|
|
|
"gpuPaletteConversion": (0, 2),
|
2022-03-03 14:32:26 +00:00
|
|
|
}
|
2021-10-23 19:52:41 +00:00
|
|
|
allowed_speed_hacks = ["mvuFlagSpeedHack", "InstantVU1SpeedHack", "MTVUSpeedHack"]
|
2021-05-05 00:33:43 +00:00
|
|
|
# Patches are allowed to have a 'default' key or a crc-32 key, followed by
|
|
|
|
allowed_patch_options = ["author", "content"]
|
|
|
|
|
|
|
|
issue_list = []
|
|
|
|
|
|
|
|
|
|
|
|
def is_hex_number(string):
|
|
|
|
try:
|
|
|
|
int(string, 16)
|
|
|
|
return True
|
2021-05-05 01:16:05 +00:00
|
|
|
except Exception:
|
2021-05-05 00:33:43 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def validate_string_option(serial, key, value):
|
2021-05-05 01:16:05 +00:00
|
|
|
if not isinstance(value, str):
|
2021-05-05 00:33:43 +00:00
|
|
|
issue_list.append("[{}]: '{}' must be a string".format(serial, key))
|
|
|
|
|
|
|
|
|
|
|
|
def validate_int_option(serial, key, value, low, high):
|
2021-05-05 01:16:05 +00:00
|
|
|
if not isinstance(value, int) or (value < low or value > high):
|
2021-05-05 00:33:43 +00:00
|
|
|
issue_list.append(
|
|
|
|
"[{}]: '{}' must be an int and between {}-{} (inclusive)".format(
|
|
|
|
serial, key, low, high
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-05-05 01:16:05 +00:00
|
|
|
def validate_list_of_strings(serial, key, value):
|
|
|
|
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
|
|
|
|
issue_list.append("[{}]: '{}' must be a list of strings".format(serial, key))
|
|
|
|
|
|
|
|
|
2021-05-05 00:33:43 +00:00
|
|
|
def validate_valid_options(serial, key, value, allowed_values):
|
|
|
|
if value not in allowed_values:
|
|
|
|
issue_list.append("[{}]: Invalid '{}' option [{}]".format(serial, key, value))
|
|
|
|
|
|
|
|
|
2021-05-05 01:16:05 +00:00
|
|
|
def validate_clamp_round_modes(serial, key, value, allowed_values):
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
issue_list.append("[{}]: '{}' must be a valid object".format(serial, key))
|
|
|
|
return
|
|
|
|
for mode_key, mode_value in value.items():
|
|
|
|
validate_valid_options(serial, key, mode_key, allowed_values)
|
|
|
|
validate_int_option(serial, key, mode_value, 0, 3)
|
|
|
|
|
|
|
|
|
|
|
|
def validate_game_fixes(serial, key, value):
|
|
|
|
if not isinstance(value, list):
|
|
|
|
issue_list.append(
|
|
|
|
"[{}]: 'gameFixes' must be a list of valid gameFixes".format(serial)
|
|
|
|
)
|
|
|
|
return
|
|
|
|
for gamefix in value:
|
|
|
|
validate_valid_options(serial, key, gamefix, allowed_game_fixes)
|
|
|
|
|
|
|
|
|
2022-03-03 14:32:26 +00:00
|
|
|
def validate_gs_hw_fix_value(serial, key, value):
|
|
|
|
low, high = 0, 1
|
|
|
|
if key in gs_hw_fix_ranges:
|
|
|
|
low, high = gs_hw_fix_ranges[key]
|
|
|
|
validate_int_option(serial, key, value, low, high)
|
|
|
|
|
|
|
|
|
|
|
|
def validate_gs_hw_fixes(serial, key, value):
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
issue_list.append("[{}]: 'gsHWFixes' must be a valid object".format(serial))
|
|
|
|
return
|
|
|
|
for fix, fix_value in value.items():
|
|
|
|
validate_valid_options(serial, key, fix, allowed_gs_hw_fixes)
|
|
|
|
validate_gs_hw_fix_value(serial, fix, fix_value)
|
|
|
|
|
|
|
|
# skipdraw range must have end >= start
|
|
|
|
skip_draw_start = value["skipDrawStart"] if "skipDrawStart" in value else 0
|
|
|
|
skip_draw_end = value["skipDrawEnd"] if "skipDrawEnd" in value else 0
|
|
|
|
if isinstance(skip_draw_start, int) and isinstance(skip_draw_end, int) and skip_draw_end < skip_draw_start:
|
|
|
|
issue_list.append("[{}]: skipDrawStart({}) must be greater or equal to skipDrawEnd({})".format(
|
|
|
|
serial, skip_draw_start, skip_draw_end))
|
|
|
|
|
|
|
|
|
2021-05-05 01:16:05 +00:00
|
|
|
def validate_speed_hacks(serial, key, value):
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
issue_list.append("[{}]: 'speedHacks' must be a valid object".format(serial))
|
|
|
|
return
|
|
|
|
for speedhack, speedhack_value in value.items():
|
|
|
|
validate_valid_options(serial, key, speedhack, allowed_speed_hacks)
|
|
|
|
validate_int_option(serial, speedhack, speedhack_value, 0, 1)
|
|
|
|
|
|
|
|
|
|
|
|
def validate_patches(serial, key, value):
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
issue_list.append(
|
|
|
|
"[{}]: 'patches' must be valid mapping of CRC32 -> Patch Objects".format(
|
|
|
|
serial
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return
|
|
|
|
for crc, patch in value.items():
|
|
|
|
if crc != "default" and not is_hex_number(str(crc)):
|
|
|
|
issue_list.append(
|
|
|
|
"[{}]: Patches must either be key'd with 'default' or a valid CRC-32 Hex String".format(
|
|
|
|
serial
|
|
|
|
)
|
|
|
|
)
|
|
|
|
continue
|
|
|
|
for patch_option, option_value in patch.items():
|
|
|
|
validate_valid_options(serial, key, patch_option, allowed_patch_options)
|
|
|
|
if patch_option == "author":
|
|
|
|
validate_string_option(serial, patch_option, option_value)
|
|
|
|
if patch_option == "content":
|
|
|
|
validate_string_option(serial, patch_option, option_value)
|
|
|
|
|
|
|
|
|
|
|
|
# pylint:disable=unnecessary-lambda
|
|
|
|
option_validation_handlers = {
|
|
|
|
"name": (lambda serial, key, value: validate_string_option(serial, key, value)),
|
|
|
|
"region": (lambda serial, key, value: validate_string_option(serial, key, value)),
|
|
|
|
"compat": (
|
|
|
|
lambda serial, key, value: validate_int_option(serial, key, value, 0, 6)
|
|
|
|
),
|
|
|
|
"roundModes": (
|
|
|
|
lambda serial, key, value: validate_clamp_round_modes(
|
|
|
|
serial, key, value, allowed_round_modes
|
|
|
|
)
|
|
|
|
),
|
|
|
|
"clampModes": (
|
|
|
|
lambda serial, key, value: validate_clamp_round_modes(
|
|
|
|
serial, key, value, allowed_clamp_modes
|
|
|
|
)
|
|
|
|
),
|
|
|
|
"gameFixes": (lambda serial, key, value: validate_game_fixes(serial, key, value)),
|
2022-03-03 14:32:26 +00:00
|
|
|
"gsHWFixes": (lambda serial, key, value: validate_gs_hw_fixes(serial, key, value)),
|
2021-05-05 01:16:05 +00:00
|
|
|
"speedHacks": (lambda serial, key, value: validate_speed_hacks(serial, key, value)),
|
|
|
|
"memcardFilters": (
|
|
|
|
lambda serial, key, value: validate_list_of_strings(serial, key, value)
|
|
|
|
),
|
|
|
|
"patches": (lambda serial, key, value: validate_patches(serial, key, value)),
|
|
|
|
}
|
|
|
|
|
2022-05-23 01:09:50 +00:00
|
|
|
class UniqueKeyLoader(yaml.FullLoader):
|
|
|
|
def construct_mapping(self, node, deep=False):
|
|
|
|
mapping = set()
|
2022-05-23 01:10:47 +00:00
|
|
|
for key_node, _ in node.value:
|
2022-05-23 01:09:50 +00:00
|
|
|
key = self.construct_object(key_node, deep=deep)
|
|
|
|
if key in mapping:
|
|
|
|
raise ValueError(f"Duplicate {key!r} key found in YAML.")
|
|
|
|
mapping.add(key)
|
|
|
|
return super().construct_mapping(node, deep)
|
|
|
|
|
2021-05-05 00:33:43 +00:00
|
|
|
print("Opening {}...".format(file_path))
|
2022-05-23 01:09:50 +00:00
|
|
|
with open(file_path, encoding="utf-8") as f:
|
2021-05-05 00:33:43 +00:00
|
|
|
try:
|
|
|
|
print("Attempting to parse GameDB file...")
|
2022-05-23 01:09:50 +00:00
|
|
|
gamedb = yaml.load(f, Loader=UniqueKeyLoader)
|
2021-05-05 00:33:43 +00:00
|
|
|
except Exception as err:
|
2021-05-05 01:16:05 +00:00
|
|
|
print(err)
|
2021-05-05 00:33:43 +00:00
|
|
|
print(
|
|
|
|
"Unable to parse GameDB. Exiting, verify that the file indeed is valid YAML."
|
|
|
|
)
|
|
|
|
exit(1)
|
|
|
|
|
|
|
|
print("File loaded successfully, validating schema...")
|
|
|
|
progress_counter = 0
|
|
|
|
for serial, game_options in gamedb.items():
|
|
|
|
progress_counter = progress_counter + 1
|
|
|
|
if progress_counter % 500 == 0 or progress_counter >= len(gamedb.items()):
|
|
|
|
print(
|
|
|
|
"[{}/{}] Processing GameDB Entries...".format(
|
|
|
|
progress_counter, len(gamedb.items())
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
# Check for required values
|
|
|
|
if not "name" in game_options.keys():
|
|
|
|
issue_list.append("[{}]: 'name' is a required value".format(serial))
|
|
|
|
if not "region" in game_options.keys():
|
|
|
|
issue_list.append("[{}]: 'region' is a required value".format(serial))
|
|
|
|
|
2021-05-05 01:16:05 +00:00
|
|
|
# Check the options
|
2021-05-05 00:33:43 +00:00
|
|
|
for key, value in game_options.items():
|
|
|
|
if key not in allowed_game_options:
|
|
|
|
issue_list.append("[{}]: Invalid option [{}]".format(serial, key))
|
2021-05-05 01:16:05 +00:00
|
|
|
continue
|
2021-05-05 00:33:43 +00:00
|
|
|
|
2021-05-05 01:16:05 +00:00
|
|
|
if key in option_validation_handlers:
|
|
|
|
option_validation_handlers[key](serial, key, value)
|
2021-05-05 00:33:43 +00:00
|
|
|
|
|
|
|
if len(issue_list) > 0:
|
|
|
|
print("Issues found during validation:")
|
|
|
|
print(*issue_list, sep="\n")
|
|
|
|
exit(1)
|
|
|
|
else:
|
|
|
|
print("GameDB Validated Successfully!")
|
|
|
|
exit(0)
|