Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 $<TARGET_FILE:retro-core-runner>
Expand All @@ -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 $<TARGET_FILE:retro-core-runner>)
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).
Expand Down Expand Up @@ -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=$<TARGET_FILE:retro-core-runner>
-DPKG_CORE=$<TARGET_FILE:rcore_fake_pkg_core>
-DPLAIN_CORE=$<TARGET_FILE:rcore_fake_core>
-DPACKAGE=${CMAKE_CURRENT_SOURCE_DIR}/tests/fake_package.txt
-P ${CMAKE_CURRENT_SOURCE_DIR}/tests/describe_test.cmake)
endforeach()
endif()
5 changes: 3 additions & 2 deletions corelink/link_test_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
3 changes: 3 additions & 0 deletions corelink/runner_probe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) &&
Expand Down
3 changes: 3 additions & 0 deletions corelink/runner_probe.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions docs/CORE_LINK.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 57 additions & 2 deletions docs/CORE_RUNNER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path> (envelope|bare, N bytes)`.

5. **Describes a core** (`--describe`, 2026-09-26) for a host building a
settings page: `--describe --core <library> [--package <library>]`, 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:

Expand All @@ -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 <core_id> <core_version> <platforms>
option <key> <type> <flags> <has_default> <default> <int_min> <int_max> <label> <description>
value <key> <one enum value>
input <button> <axis> <axis_direction> <label>
```

- Every field escapes `\` as `\\`, TAB as `\t`, LF as `\n` and CR as `\r`, so a
record is always one line. A NULL C string is the empty field.
- `<type>` is `enum`, `bool`, `int` or `string`; a type this runner does not
know prints as its number.
- `<flags>` is a comma-separated subset of `restart,netplay,developer`, in
that order, or `-` for none.
- `<has_default>` is `1` when `default_value` is non-NULL, else `0` (unset:
the core applies its own default); `<default>` is the value or empty.
- `<int_min>` and `<int_max>` are decimal int64, printed for every type.
- `value` records follow their enum option, one per entry of its
NULL-terminated `values`, in declared order.
- `<button>` is the `RCORE_PAD_*` bit in decimal (0 for an axis),
`<axis>` is as declared (`RCORE_AXIS_*` + 1, or 0), and `<axis_direction>`
is a signed decimal.
- Options and inputs appear in declared order. `describe 1` is the format's
version.

## How it is checked

Because the command line matches, n64lle's own gate grades this runner with no
Expand Down Expand Up @@ -117,6 +164,14 @@ package's hash logged, and each refusal above exits 2
(`runner_package_*`, `tests/package_test.cmake`). No real generic core has
run through it yet.

`--describe` (2026-09-26) is checked by ctest on both fake cores, whose
options and inputs cover every type and flag, NULL defaults and
descriptions, and a TAB, LF, CR and backslash inside a field. Each case
compares stdout byte for byte (`runner_describe_*`,
`tests/describe_test.cmake`). **Run by hand 2026-09-26** on n64lle's generic
`n64lle_core.so` (0.374.0, sidecar beside it): exit 0, 76 options, 16 enum
values and 16 inputs, every record with its field count.

## Not built yet

- **Netplay** (rev 4: the runner binds recomp-net's `rb_driver`).
Expand Down
6 changes: 6 additions & 0 deletions docs/RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ rcore_abi_major 0
rcore_draft_revision 5
gl 1
game_package 1
describe 1
```

A build outside a release says `version dev`. `game_package 1` (2026-09-26)
Expand All @@ -122,6 +123,11 @@ runner from before it prints no such line, and `probe_runner` reports 0. It
is not yet a field of `runtime-manifest.json`; a host checks it on the
extracted binary (step 7).

`describe 1` (2026-09-26) means the runner answers `--describe` with format
version 1 (`CORE_RUNNER.md`, "--describe"); a runner from before it prints
no such line, and `probe_runner` reports 0. Like `game_package`, it is read
from the binary, not the manifest.

## The update rule, for a host

1. Fetch the manifest. If `schema` is not one the host knows, keep the
Expand Down
Loading
Loading