WebUI: guided Settings for the 18 newer cores

tools/generate-core-metadata.py now extracts the option tables of a5200, the
Beetle NeoPop/PCE/PC-FX/VB/WonderSwan cores, DOSBox-pure, Flycast, Handy,
NeoCD, Opera, PicoDrive, PokeMini, ProSystem, PUAE, ScummVM, Stella and
Virtual Jaguar: table names are detected, v1 tables are dumped
(dump-options.cpp), DOSBox's namespace tables, NeoCD's runtime builder and
Stella's legacy variables are handled. The 15 existing catalogs are
byte-identical. build-title.sh fails when a shipped core (rpcs3 aside) has no
catalog.

Evidence: build/webui-core-metadata/index.cfg lists all 33 cores; the WebUI
in a browser lists 33 profiles and Beetle PCE shows Video, Audio, Input,
Emulation Hacks, PC Engine CD and General.
Gates: bash tools/verify.sh -> verify: PASS (format unit build integration evidence)
This commit is contained in:
Mihawk committed 2026-10-09 23:14:22 -04:00
1 parent 33d61f4ddf
commit 213a4321f7
3 files changed
+133 -10

No files matched your search

+19
View File
@@ -44,3 +44,22 @@ static void dump(const retro_core_option_v2_category *cats,
}
std::puts("]}");
}
// The v1 tables (no categories) of older cores: the same settings, in no category.
static void dump(std::nullptr_t, const retro_core_option_definition *defs)
{
std::printf("{\"categories\":[],\"settings\":[");
for (auto *s = defs; s->key; ++s)
{
std::printf("%s{\"key\":%s,\"label\":%s,\"description\":%s,\"category\":\"\",\"default\":%s,"
"\"choices\":[",
s == defs ? "" : ",", quoted(s->key).c_str(), quoted(s->desc).c_str(),
quoted(s->info).c_str(), quoted(s->default_value).c_str());
for (size_t i = 0; i < RETRO_NUM_CORE_OPTION_VALUES_MAX && s->values[i].value; ++i)
std::printf(
"%s[%s,%s]", i ? "," : "", quoted(s->values[i].value).c_str(),
quoted(s->values[i].label ? s->values[i].label : s->values[i].value).c_str());
std::printf("]}");
}
std::puts("]}");
}
+7
View File
@@ -293,6 +293,13 @@ webui_daemon=$(bash "$root/tools/build-webui-daemon.sh")
# Extract once, before the identity is computed, so catalog changes identify the build.
python3 "$root/tools/generate-core-metadata.py" "$root/build/webui-core-metadata"
# Every shipped core has its guided Settings catalog (rpcs3 aside: never released). A
# core added without one would show no options in the WebUI, as 18 did until 2026-10-09.
for core_name in "${core_names[@]}"; do
[[ $core_name == rpcs3 ]] && continue
grep -q "^${core_name}_libretro.so = " "$root/build/webui-core-metadata/index.cfg" ||
{ echo "error: $core_name has no WebUI Settings catalog (tools/generate-core-metadata.py CORES)" >&2; exit 1; }
done
# Bind the running trace and FTP readback to these exact source/archive inputs.
# The console transforms the SELF container, so its whole-file digest differs.
+107 -10
View File
@@ -31,10 +31,57 @@ CORES = [
('azahar', 'Azahar', '.deps/azahar-src/src/citra_libretro/core_settings.cpp', ['ENABLE_VULKAN']),
('vice_x64sc', 'VICE x64sc', '.deps/vice-src/libretro/libretro-core.c', ['__X64SC__','HAVE_RESID33','ARCHDEP_PRINTER_DEFAULT_DEV1="vice_printer.txt"']),
('fbneo', 'FinalBurn Neo', 'build/cores/fbneo/src/burner/libretro/retro_common.cpp', ['BUILD_NEOGEO']),
# The cores added 2026-10-06; library names as each core reports them (its settings'
# folder), tables found by name in each source (option_tables).
('a5200', 'a5200', '.deps/a5200-src/libretro/libretro_core_options.h', []),
('mednafen_ngp', 'Beetle NeoPop', '.deps/beetle-ngp-src/libretro_core_options.h', []),
('mednafen_pce', 'Beetle PCE', '.deps/beetle-pce-src/libretro_core_options.h', []),
('mednafen_pcfx', 'Beetle PC-FX', '.deps/beetle-pcfx-src/libretro_core_options.h', []),
('mednafen_vb', 'Beetle VB', '.deps/beetle-vb-src/libretro_core_options.h', []),
('mednafen_wswan', 'Beetle WonderSwan', '.deps/beetle-wswan-src/libretro_core_options.h', []),
('dosbox_pure', 'DOSBox-pure', '.deps/dosbox-pure-src/core_options.h', ['DBP_DEFAULT_SAMPLERATE_STRING="48000"']),
('flycast', 'Flycast', '.deps/flycast-src/shell/libretro/libretro_core_options.h', ['CORE_OPTION_NAME="reicast"']),
('handy', 'Handy', '.deps/handy-src/libretro/libretro_core_options.h', []),
('neocd', 'NeoCD', '.deps/neocd-src/src/libretro_variables.cpp', []),
('opera', 'Opera', '.deps/opera-src/libretro_core_options.c', ['THREADED_DSP=1']), # its Makefile's default, kept for ps5
('picodrive', 'PicoDrive', '.deps/picodrive-src/platform/libretro/libretro_core_options.h', []),
('pokemini', 'PokeMini', '.deps/pokemini-src/libretro/libretro_core_options.h', []),
('prosystem', 'ProSystem', '.deps/prosystem-src/core/libretro_core_options.h', []),
('puae', 'PUAE', '.deps/puae-src/libretro/libretro-core.c', []),
('scummvm', 'ScummVM', '.deps/scummvm-src/backends/platform/libretro/include/libretro-core-options.h', ['USE_HIGHRES']), # Makefile.common's default, kept for ps5
('stella', 'Stella', '.deps/stella-src/src/os/libretro/libretro.cxx', []),
('virtualjaguar', 'Virtual Jaguar', '.deps/virtualjaguar-src/libretro_core_options.h', []),
]
# Cores whose tables are found by name (the English ones), not named in extract().
NAMED = {'fbneo', 'dolphin', 'azahar', 'fceumm', 'ppsspp', 'desmume', 'mednafen_psx_hw',
'mednafen_saturn', 'pcsx2', 'mame', 'mupen64plus_next', 'genesis_plus_gx', 'snes9x',
'mgba', 'vice_x64sc'}
def blocks(text, opening):
"""Each block a pattern opens, to its matching brace (strings skipped)."""
out = []
for match in re.finditer(opening, text, re.M):
depth = 1
for token in re.finditer(r'"(?:\\.|[^"\\])*"|[{}]', text[match.end():]):
depth += 1 if token[0] == '{' else -1 if token[0] == '}' else 0
if not depth:
out.append(text[match.start():match.end() + token.end()])
break
return out
def option_tables(text):
"""The English category and definition tables of a source, and whether they are v1."""
v2 = re.findall(r'retro_core_option_v2_definition\s+(\w+)\s*\[', text)
v1 = re.findall(r'retro_core_option_definition\s+(\w+)\s*\[', text)
cats = re.findall(r'retro_core_option_v2_category\s+(\w+)\s*\[', text)
pick = lambda names: next((n for n in names if n in ('option_defs_us_v2', 'option_defs_us', 'option_defs')), names[0] if names else None)
defs = pick(v2)
if defs:
return next((c for c in cats if c in ('option_cats_us', 'option_cats')), cats[0] if cats else None), defs, False
return None, pick(v1), True
def declaration(text, name):
match = re.search(r'(?:static\s+)?(?:constexpr\s+|const\s+)?(?:struct\s+)?retro_core_option_v2_(?:category|definition)\s+'+re.escape(name)+r'\s*(?:\[\])?\s*=\s*\{', text)
match = re.search(r'(?:static\s+)?(?:constexpr\s+|const\s+)?(?:struct\s+)?retro_core_option_(?:v2_category|v2_definition|definition)\s+'+re.escape(name)+r'\s*(?:\[[^\]]*\])?\s*=\s*\{', text)
if not match:
raise ValueError('Option declaration missing: '+name)
# Tokenize strings so braces in help text cannot terminate an initializer.
@@ -50,9 +97,42 @@ def preprocess(text, flags):
text = re.sub(r'^\s*#\s*include[^\n]*', '', text, flags=re.M)
return subprocess.check_output(['c++','-E','-P','-x','c++','-DHAVE_NO_LANGEXTRA','-D__PROSPERO__', *['-D'+v for v in flags], '-'], input=text, text=True, cwd=ROOT, stderr=subprocess.PIPE)
def neocd_tables(source):
"""NeoCD builds its table at run time (its BIOS choices are the files found): its
builder, compiled here with no BIOS, gives every other option; the BIOS one is the
console's, from the runtime snapshot once the core runs."""
text = source.read_text()
names = text.split('// Variable names and descriptions for the settings', 1)[1].split('// All core variables', 1)[0]
cats = re.search(r'static retro_core_option_v2_category coreOptionCategories\[\] = \{.*?\};', text, re.S)[0]
builders = ''.join(re.search(r'static void '+name+r'\(.*?\n\}\n', text, re.S)[0]
for name in ('fillBasicOption', 'fillBiosOption', 'buildCoreOptionsV2'))
stub = ('#include <cstring>\n#include <string>\n#include <vector>\n'
'struct BiosEntry { std::string description; };\n'
'static struct { std::vector<BiosEntry> biosList; std::string biosChoices; } globals;\n'
'static std::vector<retro_core_option_v2_definition> coreOptionDefinitions;\n'
'static struct { retro_core_option_v2_category *categories; '
'retro_core_option_v2_definition *definitions; } coreOptionsV2;\n')
tail = ('static const retro_core_option_v2_definition *neocd_defs() '
'{ buildCoreOptionsV2(); return coreOptionDefinitions.data(); }\n')
return stub+names+cats+'\n'+builders+tail, 'coreOptionCategories', 'neocd_defs()'
def stella_data(source):
"""Stella declares legacy variables, "Label; first|second": no descriptions, and the
first choice is the default (libretro's rule for them)."""
settings = []
for key, value in re.findall(r'\{\s*"(stella_\w+)",\s*"([^"]+)"\s*\}', source.read_text()):
label, choices = value.split(';', 1)
values = [c.strip() for c in choices.split('|')]
settings.append({'key': key, 'label': label.strip(), 'description': '', 'category': '',
'default': values[0], 'choices': [[v, v] for v in values]})
return {'categories': [], 'settings': settings}
def extract(stem, source, flags):
if stem == 'neocd':
return neocd_tables(source)
text = source.read_text()
if stem == 'vice_x64sc': text = (source.parent/'libretro-core.h').read_text()+'\n'+text
if stem == 'puae': text = (source.parent/'libretro-core.h').read_text()+'\n'+text
if stem == 'fbneo':
text = (source.parent/'retro_common.h').read_text()+'\n'+text
strings = re.findall(r'"(?:\\.|[^"\\])*"', (source.parent/'retro_string.cpp').read_text().split('= {', 1)[1])[:171]
@@ -83,6 +163,18 @@ def extract(stem, source, flags):
# General definitions are already English; game-specific DIP switches
# and cheats are appended by the runtime snapshot when a game loads.
return prefix+'\n'+declaration(text, cats)+'\nretro_core_option_v2_definition option_defs_us[] = {'+','.join(names)+', {}};', cats, defs
if stem not in NAMED:
cats, defs, v1 = option_tables(text)
if not defs:
raise ValueError(stem+': no option table found')
# A table sized or keyed by the source's own enums and namespaces
# (DOSBox Pure's DBP_Option, DBP_OptionCat): those kept whole.
prefix = '\n'.join(re.findall(r'^\s*enum\b[^{;]*\{[^}]*\};', text, re.M | re.S) +
blocks(text, r'^\s*namespace\s+\w+\s*\{'))
if stem == 'dosbox_pure':
prefix = 'class Section;\n' + prefix # declared by DBP_Option's helpers, never used here
body = (declaration(text, cats)+'\n' if cats else '')+declaration(text, defs)
return prefix+'\n'+body, (cats or 'nullptr') if not v1 else 'nullptr', defs
return prefix+'\n'+declaration(text, cats)+'\n'+declaration(text, defs), cats, defs
@@ -124,21 +216,26 @@ def prepare(stem, source, data):
option['default'] = value if value in values else values[0] if values else ''
return data
def compile_tables(stem, source, flags, helper):
"""A core's option tables, compiled with the host helper and dumped as JSON."""
tables, cats, defs = extract(stem, source, flags)
with tempfile.TemporaryDirectory() as folder:
cpp = Path(folder)/'options.cpp'; exe = Path(folder)/'options'
cpp.write_text(helper+'\n'+tables+f'\nint main() {{ dump({cats}, {defs}); }}\n')
result = subprocess.run(['c++', '-std=c++17', '-I'+str(ROOT/'vendor/retroarch/libretro-common/include'),
str(cpp), '-o', str(exe)], capture_output=True, text=True)
if result.returncode:
(ROOT/'build/webui-catalog-error.cpp').write_text(cpp.read_text())
raise RuntimeError(stem+': '+result.stderr[:4000])
return json.loads(subprocess.check_output([str(exe)], text=True))
def generate(output):
output.mkdir(parents=True, exist_ok=True)
helper = (ROOT/'tooling/webui/dump-options.cpp').read_text()
records=[]
for stem,name,source,flags in CORES:
source=ROOT/source
tables,cats,defs=extract(stem,source,flags)
with tempfile.TemporaryDirectory() as folder:
cpp=Path(folder)/'options.cpp'; exe=Path(folder)/'options'
cpp.write_text(helper+'\n'+tables+f'\nint main() {{ dump({cats}, {defs}); }}\n')
result=subprocess.run(['c++','-std=c++17','-I'+str(ROOT/'vendor/retroarch/libretro-common/include'),str(cpp),'-o',str(exe)],capture_output=True,text=True)
if result.returncode:
(ROOT/'build/webui-catalog-error.cpp').write_text(cpp.read_text())
raise RuntimeError(stem+': '+result.stderr[:4000])
data=json.loads(subprocess.check_output([str(exe)],text=True))
data = stella_data(source) if stem == 'stella' else compile_tables(stem, source, flags, helper)
data = prepare(stem, source, data)
data['core']=name
data['binary_sha256'] = hashlib.sha256((ROOT/'build/cores/stage/cores'/(stem+'_libretro.so')).read_bytes()).hexdigest()