diff --git a/CMakeLists.txt b/CMakeLists.txt index a5c7ec8..766e8e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -162,7 +162,7 @@ if(RETRO_RUNTIME_TOOLS) # --version: what release packaging and hosts read back from the binary. add_test(NAME runner_version COMMAND retro-core-runner --version) set_tests_properties(runner_version PROPERTIES PASS_REGULAR_EXPRESSION - "version ${RETRO_RUNTIME_VERSION}\ncommit [^\n]+\nlink_protocol [0-9]+\\.[0-9]+\n.*gl [01]\ngame_package 1\n") + "version ${RETRO_RUNTIME_VERSION}\ncommit [^\n]+\nlink_protocol [0-9]+\\.[0-9]+\n.*gl [01]\ngame_package 1\ndescribe 1\n") # Link: the same, driven through retro_corelink the way a host does. add_test(NAME link_fake_core COMMAND retro-core-link-test --runner $ @@ -174,7 +174,7 @@ if(RETRO_RUNTIME_TOOLS) # probe_runner: what a host reads to choose and verify a runner. add_test(NAME runner_probe COMMAND retro-core-link-test --probe $) set_tests_properties(runner_probe PROPERTIES PASS_REGULAR_EXPRESSION - "probe: version ${RETRO_RUNTIME_VERSION}, link [0-9]+\\.[0-9]+, rcore ABI [0-9]+, compatible, game_package 1") + "probe: version ${RETRO_RUNTIME_VERSION}, link [0-9]+\\.[0-9]+, rcore ABI [0-9]+, compatible, game_package 1, describe 1") # A crash mid-session: the runner dies at frame 5 with no unload. The hub # must see it end and still write the save as frames 1-4 left it -- the # save memory is the hub's (docs/CORE_LINK.md). @@ -221,4 +221,17 @@ if(RETRO_RUNTIME_TOOLS) -DOUT=${CMAKE_CURRENT_BINARY_DIR}/test-package-${_case} -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/package_test.cmake) endforeach() + # --describe: the fake cores' declarations as TAB records, without --rom; + # and the refusals (--package for a plain core, a core that is not there). + foreach(_case plain package package_none unwanted no_core) + add_test(NAME runner_describe_${_case} + COMMAND ${CMAKE_COMMAND} + "-DEMULATOR=${CMAKE_CROSSCOMPILING_EMULATOR}" + -DCASE=${_case} + -DRUNNER=$ + -DPKG_CORE=$ + -DPLAIN_CORE=$ + -DPACKAGE=${CMAKE_CURRENT_SOURCE_DIR}/tests/fake_package.txt + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/describe_test.cmake) + endforeach() endif() diff --git a/corelink/link_test_main.cpp b/corelink/link_test_main.cpp index 9186a5a..3c7b41a 100644 --- a/corelink/link_test_main.cpp +++ b/corelink/link_test_main.cpp @@ -94,10 +94,11 @@ int main(int argc, char** argv) { RunnerVersion v; std::string err; if (!probe_runner(utf8_path(val()), v, &err)) die(err); - std::printf("probe: version %s, link %u.%u, rcore ABI %u, %s, game_package %u\n", + std::printf("probe: version %s, link %u.%u, rcore ABI %u, %s, game_package %u, " + "describe %u\n", v.version.c_str(), v.link_major, v.link_minor, v.abi_major, v.compatible() ? "compatible" : "NOT compatible with this host", - v.game_package); + v.game_package, v.describe); return 0; } else if (a == "--runner") spec.runner = utf8_path(val()); else if (a == "--core") spec.core = utf8_path(val()); diff --git a/corelink/runner_probe.cpp b/corelink/runner_probe.cpp index 8a68bb6..ca6df50 100644 --- a/corelink/runner_probe.cpp +++ b/corelink/runner_probe.cpp @@ -67,6 +67,9 @@ bool probe_runner(const fs::path& runner, RunnerVersion& out, std::string* error if (fields.count("game_package") && !num("game_package", v.game_package)) { return fail("--version printed an unreadable game_package line"); } + if (fields.count("describe") && !num("describe", v.describe)) { + return fail("--version printed an unreadable describe line"); + } const std::string link = fields["link_protocol"]; const auto dot = link.find('.'); bool ok = !v.version.empty() && dot != std::string::npos && num("rcore_abi_major", v.abi_major) && diff --git a/corelink/runner_probe.hpp b/corelink/runner_probe.hpp index 87acf33..6e6507a 100644 --- a/corelink/runner_probe.hpp +++ b/corelink/runner_probe.hpp @@ -22,6 +22,9 @@ struct RunnerVersion { // 1 when the runner takes --package (GAME_PACKAGE cores); 0 for a runner // from before it did, which prints no game_package line. std::uint32_t game_package = 0; + // 1 when the runner answers --describe (a core's declared options and + // inputs, no ROM; docs/CORE_RUNNER.md); 0 for a runner from before it. + std::uint32_t describe = 0; // Whether this host can drive it: the link major and the rcore ABI major // are this host's own. bool compatible() const { diff --git a/docs/CORE_LINK.md b/docs/CORE_LINK.md index 4e03963..9c33ec5 100644 --- a/docs/CORE_LINK.md +++ b/docs/CORE_LINK.md @@ -86,6 +86,8 @@ hub runner hub that needs it as data hashes the file it passed. `retro-core-link-test --package` drives it, and `probe_runner` reads `game_package` from `--version` (`RunnerVersion::game_package`, 0 for a runner from before it). + It reads `describe` the same way (`RunnerVersion::describe`): whether the + runner answers `--describe`, which is not a link session (`CORE_RUNNER.md`). - **Input rides inside each Grant**, so the contract's "identical within one frame" holds by construction. - **Savestates (1.1) are taken between frames.** The hub sends `SaveState` or diff --git a/docs/CORE_RUNNER.md b/docs/CORE_RUNNER.md index 441aeee..251b1b2 100644 --- a/docs/CORE_RUNNER.md +++ b/docs/CORE_RUNNER.md @@ -67,9 +67,14 @@ This page covers what exists and how it is checked. write them, and goes to the core unchecked as before. Headless prints `state: loaded (envelope|bare, N bytes)`. +5. **Describes a core** (`--describe`, 2026-09-26) for a host building a + settings page: `--describe --core [--package ]`, no + `--rom`. See "--describe" below. + `--version` prints the release version, commit, link protocol and rcore ABI -compiled in, whether `--gl` is available, and `game_package 1` (this runner -takes `--package`), and exits 0 (`RELEASES.md`). +compiled in, whether `--gl` is available, `game_package 1` (this runner +takes `--package`) and `describe 1` (this runner answers `--describe`), and +exits 0 (`RELEASES.md`). Exit codes: @@ -80,6 +85,48 @@ Exit codes: | 2 | refused before the core ran: arguments, load, ABI, manifest or game package | | 3 | the core broke the contract: an undeclared option key | +## `--describe` + +What a core declares, printed without a ROM and without a session. The runner +loads the library and checks its sidecar exactly as for a run (a refusal is +exit 2, message on stderr, nothing on stdout). It then reads `options()` and +`input_descriptors()`, which `rcore.h` declares "before load" and which the +session already reads before `init`. It calls neither `init` nor `load`, and +it does not refuse an `OWNS_LOOP` core, because nothing is driven. + +`--package` gets the same refusals as for a run (a core without +`game_package` refuses one; it must be a readable file). A `game_package` +core is described without one too: a package reaches a core only through +`load()`, so it cannot change what is declared before it. + +Stdout is UTF-8, one record per line, fields separated by one TAB, and +nothing else: + +``` +describe 1 +core +option