Commit Graph
10 Commits
Author SHA1 Message Date
Rob LoachandLibretroAdmin be9f97d2cf MCP: review fixes, checkpoints, queued STEPS (#19861)
* MCP: cold paused, held input, stacks, arg types

* MCP: checkpoints, prompt, completions, records

* MCP: completion errors, sized prompt, CI shaders

* MCP: rollback via loader, STOP n, completion fixes

* MCP: ask input_driver whether a replay records

* mcp: Clarify steps, and ensure MCP standards

* MCP: queued STEPS, checkpoint step, 32 steps

* MCP: queued STEPS say how long they sat idle

* MCP: queue ends on load/reset; simplify

* MCP docs: queue stops, idle, list caching

* MCP: shorter STEPS wording

* MCP: notice when port taken, --command, taps

* MCP: drop the play_game prompt and completions

* MCP: queue, tap and reset fixes from a review

* MCP: queue outcomes, hex values, field fixes

* command: --command takes an empty host or port as the default

"CMD;;PORT" and "CMD;HOST;" fill in the default host or port, so an
empty segment after a ";" counts as a host or port field again. A STEPS
list still keeps its own ";" steps, which carry a space, a "|" or are
"checkpoint".

---------

Co-authored-by: LibretroAdmin <[email protected]>
2026-10-19 02:12:59 +02:00
Rob LoachandLibretroAdmin 7f86c7f53c MCP: STEPS, PRESS until, and fixes after WATCH (#19850)
* MCP: answer once when reply_to answered already

* Commands: refuse slots past INT_MAX

* MCP: follow-ups to WATCH and the folded tools

* Commands: take CRLF lines; keep long family names

* MCP: fixes from a review of the server

* stdio_e2e: tool list size after the combined fixes

* MCP: lone CR, over-long stdio lines, slot cap

* MCP: stdio keeps its agreed revision

* docs: MCP server doc matches the code again

* Leave CHANGES.md out of this change

* MCP: STEPS, PRESS until, 16-bit waits, 16 watches

* MCP: 4 kB off tools/list; CORE; LOAD_CONTENT paused

* MCP: refuse unknown params; taps; review fixes

* MCP: GET_LOG, READ, RUN_UNTIL say what they mean

* MCP: LOAD_CONTENT's frame back under budget; STEPS counts its taps

LOAD_CONTENT copies the content path, to the heap, only where a pause
setting follows it, which keeps its stack frame where it was. STEPS
counts the frame let go between two presses of a button against the
frames one request runs, so a list it accepts can finish in them.

Lane: the taps case in samples/network/command_frames.

---------

Co-authored-by: LibretroAdmin <[email protected]>
2026-10-17 03:14:30 +02:00
Rob LoachandLibretroAdmin 75f278e286 mcp: WATCH a step and report what follows (#19834)
* WATCH: a step and the look that follows it, in one request

A field run's steps were a press and then a read for each thing it
needed to know: where the player was, where the enemies were, which
mode the game was in. Four requests to take one step, and the reads
were the bulk of a hundred calls for one level.

WATCH <label> <address> watches a byte, and RUN_FRAMES and PRESS carry
every watch along with their own answer, as a line of label, address
and value. A step needs no reads after it.

The lines land where the rows machinery already looks: both commands
answer with a line that opens with their own name, which the row reader
skips as a header, so the watches are the rows and the two commands
declare the shape they now answer in. A failure stays its reason and
carries no reading.

Each watch is resolved when it is set, as RUN_UNTIL resolves the byte
it waits on, so an address nothing maps is refused there rather than
read later. Content closing drops them all, which is what invalidates
where they point. Eight at once; a label already watched keeps its
place in the list when its address changes, so rows stay where a client
saw them.

RUN_UNTIL is left alone. It answers with the byte it waited on in three
columns of its own, and giving it watch rows would collide with those -
its format landed days ago, and changing it is a separate decision.

Also the test PRESS never had: it lets go only of what it pressed
itself, so a button held with SET_INPUT survives a press of the same
button. That was fixed by eye on the branch before #19815 landed and
has been unguarded since; GET_INPUT makes it three lines.

tools/list is 24715 bytes at 49 tools, from 22944 at 48. A tool's own
rows schema is about 450 bytes and this adds three of them, which is
most of the difference; the budget in the end-to-end test records what
each phase has cost it.

* WATCH: keep RUN_UNTIL rows, range-check addresses

* WATCH takes pairs; RUN_UNTIL carries watches

* Memory commands: check bytes and addresses first

* MCP: MENU, DISK and REINIT, three tools for 12

* SET_OPTION: drop streaming's unused event

* stdio_e2e: cap each tool, lower the total

* MCP: STATE and REPLAY tools; files are commands

* Review fixes: bounds, ERANGE, live watch lookups

* STATE: answer as STATE, not the command it runs

* MCP: family tools answer under their own names

* RUN_UNTIL: look its byte up every frame, as WATCH does

RUN_UNTIL keeps the system address it waits on and reads the byte
through the memory map each frame, so a core that moves its memory is
followed, and one that stops mapping the address ends the wait with an
error. VALUE carries the byte as last read. The step reply is clamped
to its buffer, and the deferred reply's family sits ahead of its
generation counter.

command_frames covers a remapped and an unmapped byte under RUN_UNTIL
and WATCH and the watches dropping when content closes; mcp_server
pins the structured RUN_UNTIL and step replies against the tables
command.h ships.

---------

Co-authored-by: LibretroAdmin <[email protected]>
2026-10-16 00:27:30 +02:00
LibretroAdmin cc4de1186c RUN_UNTIL: wait nine seconds of the content's frames by default
With no frame count given, RUN_UNTIL waits about nine seconds of the
content's own frames, capped at 600, so its answer arrives inside a
structured interface's ten-second reply deadline at any frame rate:
540 frames at 60fps, 450 at 50fps. A request can still ask for up to
600. The command_frames lane checks the default against its core's
60fps.
2026-10-15 04:13:19 +02:00
LibretroAdmin e1c3cdc9b3 Probe a core path without re-entering the loaded core
libretro_get_system_info() opens the library and calls its
retro_set_environment to learn what it declares. When the path names
the core already loaded, that library is the running one, so the
environment is not called again mid-session: what it declared comes
from the running state and nothing is cached for it. A statically
linked build does the same once its core is initialised.

LOAD_CORE on the running core's own path reaches this, as does the
menu's Load Core. The command_frames harness core counts environment
calls after retro_init, and that lane holds it at zero.
2026-10-15 04:13:16 +02:00
LibretroAdmin a4a72b3c33 RUN_UNTIL: range-check the value and frames before narrowing
The value and frame count are compared as the unsigned long strtoul
returns, so an argument past 32 bits is refused instead of wrapping to
an accepted one, and spaces after the value no longer read as a frame
count of zero. The command_frames lane covers both, along with the <
and > conditions.

The LOAD_CORE comment moves back onto command_load_core, and the docs
say hardcore mode refuses SET_INPUT, not GET_INPUT.

Closes #19833
2026-10-15 03:52:22 +02:00
Rob Loach 297986dd43 GET_INPUT, and core paths checked before anything is unloaded
Three things probing master turned up, and a trim that pays for two of
them.

GET_INPUT [player] answers what a player holds without changing it.
There was no way to ask: SET_INPUT answers in that shape but has to set
something first, and RELEASE_INPUT lets go before it answers, so it
always reads none. A client could not ask what it was holding.

LOAD_CORE answered "The command failed." for a missing path and for a
library that is not a core alike - the generic fallback, because it
returned false without ever saying why. It says which now.

Neither LOAD_CORE nor LOAD_CONTENT unloads the running core for a path
that turns out not to be one. The library is probed first, with
libretro_get_system_info(), which is what core scanning already uses
and which keeps what it finds, so asking twice is free. Checking the
paths stopped a typo costing the game; a stale library or something
renamed still did.

tools/list pays for every tool's description on every session, so the
two new ones are paid for where the list repeated itself:
readOnlyHint is false by default and 37 tools said so anyway, which was
770 bytes, and the memory map named hexadecimal in four columns that
its own description already covers. 48 tools in 22861 bytes, from 46 in
21449.

RUN_UNTIL's conditions move into command.c: a table in the header is a
copy in every translation unit that includes it, and nothing outside
command.c compares them.
2026-10-15 03:52:18 +02:00
Rob Loach 84bc936dc0 RUN_UNTIL: wait on a byte, in one request
A field run cleared one level in over a hundred calls and spent most of
them waiting: run a few frames, read a byte, go round again. Until a
Goomba was twenty pixels away, until Mario landed, until a transition
finished.

RUN_UNTIL <address> <condition> <value> [max frames] runs the content
until the byte at a system address meets the condition and answers with
whether it was met, how many frames ran, and what the byte holds. The
loop runs inside the frontend, where a frame costs nothing, rather than
across a connection per iteration.

It is RUN_FRAMES' job with a predicate instead of a counter: the same
frame_work flag, the same deferred reply, the same one-at-a-time rule,
so nothing new blocks and there is no second state machine. Reading
core memory every frame from the runloop is what rcheevos_test() has
always done, on the same flag.

Two bounds keep it honest. An address nothing maps is refused before a
frame runs, rather than after a wait that could not have ended. The
frames are COMMAND_FRAMES_REPLY_MAX at most, and where they run out
first it answers "waiting" with how far it got, so the next request
carries on instead of the client being told it failed.

The harness core now maps a byte that follows its frame counter, so the
waiting itself is tested rather than only the refusals: already met
answers with no frames run, three frames away answers on the third and
not before, and the cap answers waiting.

RUN_UNTIL costs 1391 bytes of tools/list, which took it past the budget
the end-to-end test holds; the ceiling is raised deliberately, with the
cost recorded beside it.
2026-10-15 03:52:15 +02:00
Rob LoachandLibretroAdmin c2e5bce004 MCP: structured results, and LOAD_CONTENT checks before it unloads (#19815)
* LOAD_CONTENT: check paths before unloading

* mcp: results carry structuredContent

* LIST_CORES filters by content path

* Structured rows for core options and memory map

* MCP: do not advertise a cacheable tool list

* SET_INPUT holds several controls at once

* A row opening with MORE is a row, not a cursor

* MCP: fewer tools, and rows a client can rely on

structuredContent and outputSchema never reached a client over stdin
and stdout. The gate read the MCP-Protocol-Version header, or the
version the stateless revision sends with every request; stdio has
neither, so the revision agreed at initialize was parsed, used for the
handshake reply and then dropped. It now holds for the session, which
over stdio is the connection.

A command answers with structuredContent exactly when it declared an
outputSchema, so a client reads the schema it was handed rather than
probing each reply. SET_INPUT, RELEASE_INPUT and GET_LOG declare one
now.

The "MORE [next]" line was read by a literal in the server. The shape
of a reply names its own trailers instead - the lines that carry a
number about the reply rather than a row of it - so GET_LOG's NEXT and
SKIPPED are the same mechanism rather than two more special cases, and
a reply says nothing about being cut short where it cannot be. A reply
whose lines are prose declares no columns: GET_LOG's page runs to 60k,
and a row apiece would answer with it twice.

Thirty-four commands are no longer offered as tools. A hotkey that
steps a selection - the next state slot, the previous shader, the next
cheat - is a hotkey because a person presses it while watching the
screen. Where a command takes the value outright that one is the tool,
and stepping towards it is only a way to get the value wrong; where
nothing reports the selection either, there is nothing worth offering
yet. READ_CORE_RAM and WRITE_CORE_RAM go too: an achievement address
looks like READ_CORE_MEMORY's system address and is not one. The disc
and the menu are kept, because stepping them blind is all there is and
a disc swap is part of playing a game. All of them are still commands
on every line interface.

The tool list is 45 tools and 20k, from 79 and 23k, and the end-to-end
test holds it to a budget: every description is context a client pays
for before it does anything.

* PRESS: hold, run and let go in one request

A tap was four requests: SET_INPUT, RUN_FRAMES, SET_INPUT to release,
RUN_FRAMES again. Each is its own HTTP connection against four slots,
so a field test spent about 25 of them crossing a menu and about 10 on
one jump.

PRESS <player> <control>[+<control>...] <frames> holds the buttons,
runs that many frames and lets them go, answered once the last frame
has run. It is RUN_FRAMES' job with buttons held through it: the same
deferred reply, the same one-at-a-time rule, the same pause the runloop
applies to frame advance, so nothing new blocks and no second state
machine exists. Whatever ends the frames lets the buttons go, including
the error that ends them early.

Axes stay SET_INPUT's. They are held at a value rather than pressed,
and SET_INPUT with RUN_FRAMES is still the way to hold a control across
several steps.

SET_INPUT and PRESS read the same request - a player, controls joined
by "+", and a number - so that parse is one routine now, and both
resolve every control before either acts on any.

* CHANGES: the MCP and command work in this series

* GET_STATUS: say what it actually answers with

The description promised the content's CRC32 and the reply has never
carried one: it is the system and the content's file name, separated by
a comma.

HELP prints that text on the line interfaces and tools/list hands it to
every MCP client, where it is the prose a client reads to decide how to
take the reply apart. One told to expect three comma-separated fields
gets two.

Adding the CRC instead would be worse. Working it out means hashing the
content, which for a core using need_fullpath reads a whole disc image
from disk - task_movie.c already carries that note, having hit it - and
GET_STATUS is the command a client polls. It would also change the
reply for every client that reads the line today.

* command: descriptions that match the replies

tools/list hands a command's description to every MCP client, and HELP
prints it on the line interfaces: it is the prose a client reads to
decide how to take a reply apart. Five of them described a reply that
was not the one sent.

SET_INPUT and RELEASE_INPUT answer with three fields - the player, the
buttons held and the axes held - and the description named two. A
client splitting on what it was told got the player where it expected
the buttons. RELEASE_INPUT did not say it answered at all, and with no
player it lets go for everyone while answering for the first of them,
which is worth saying rather than leaving to be discovered.

GET_LOG can open with SKIPPED and a count where the lines asked for had
already been overwritten, which the description did not mention: a
client reads it as a log line.

READ_CORE_MEMORY and WRITE_CORE_MEMORY both answer - with the bytes,
and with how many were written - and neither said so.

* READ_CORE_MEMORY: format bytes without snprintf

Both memory reads wrote their reply with an snprintf per byte, to turn
one byte into two hexadecimal digits. At COMMAND_READ_NBYTES_MAX that
is 16384 calls, which costs 1.5ms on an aarch64 Pi - a tenth of a frame
spent formatting, on the main thread, in the command an assistant reads
memory with most. A nibble table is 0.02ms for the same bytes, 68 times
faster, and the output is identical for all 256 values of a byte in
both of the formats the two sites used.

The harness core now maps its memory, because nothing in the tree did:
READ_CORE_MEMORY, WRITE_CORE_MEMORY and GET_MEMORY_MAP had no test that
reached past "no memory map defined", so the formatting this changes
was uncovered. The test asserts the replies byte for byte, including
every digit of a byte, a read clamped at the end of its region, and one
outside every region. Breaking the table deliberately fails three of
them.

* PRESS: leave SET_INPUT's holds in place

PRESS lets go only of the buttons it pressed itself, so a button a
client already holds with SET_INPUT is still held once the press ends.

* command, mcp: trim comments and order new struct members

Drop the superseded LIST_CORES and reply-splitting comments, shorten
the new comments to what stays true, and order the new struct members
by alignment.

---------

Co-authored-by: LibretroAdmin <[email protected]>
2026-10-14 18:59:27 +02:00
Codex 2c6b61e9fb runloop: tick RUN_FRAMES only after advancing the core 2026-10-13 16:57:29 +02:00