From 2c6b61e9fb2963460a26e260bdd3ce4b861a2ce2 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 13 Oct 2026 16:54:12 +0200 Subject: [PATCH] runloop: tick RUN_FRAMES only after advancing the core --- .github/workflows/Linux-menu-harness.yml | 9 ++ command.h | 5 +- core.h | 4 +- runahead.c | 31 ++-- runahead.h | 6 +- runloop.c | 20 ++- samples/network/command_frames/Makefile | 14 ++ samples/network/command_frames/build.sh | 57 +++++++ .../command_frames/command_frames_test.c | 148 ++++++++++++++++++ samples/network/command_frames/harness_core.c | 53 +++++++ 10 files changed, 322 insertions(+), 25 deletions(-) create mode 100644 samples/network/command_frames/Makefile create mode 100755 samples/network/command_frames/build.sh create mode 100644 samples/network/command_frames/command_frames_test.c create mode 100644 samples/network/command_frames/harness_core.c diff --git a/.github/workflows/Linux-menu-harness.yml b/.github/workflows/Linux-menu-harness.yml index 01b586e4d0..2e2c8ecc31 100644 --- a/.github/workflows/Linux-menu-harness.yml +++ b/.github/workflows/Linux-menu-harness.yml @@ -22,6 +22,7 @@ on: - 'playlist.h' - 'tasks/task_content*' - 'samples/menu/playlist_nav/**' + - 'samples/network/command_frames/**' - 'samples/menu/nav_repeat_stall/**' - 'samples/menu/mouse_click/**' - 'samples/menu/menu_icon_ownership/**' @@ -44,6 +45,7 @@ on: - 'playlist.h' - 'tasks/task_content*' - 'samples/menu/playlist_nav/**' + - 'samples/network/command_frames/**' - 'samples/menu/nav_repeat_stall/**' - 'samples/menu/mouse_click/**' - 'samples/menu/menu_icon_ownership/**' @@ -438,6 +440,13 @@ jobs: ./samples/menu/explore_build/explore_build_test make -C samples/menu/explore_build clean + - name: Frame command progress through the real runloop + shell: bash + run: | + set -eu + make -C samples/network/command_frames check + make -C samples/network/command_frames clean + - name: Save ccache # Only master's pushes keep a cache: pull requests restore # master's, so one push does not evict the next one's. diff --git a/command.h b/command.h index c07a8cc0c2..485722288b 100644 --- a/command.h +++ b/command.h @@ -546,8 +546,7 @@ bool command_set_option(command_t *cmd, const char *arg); bool command_get_log(command_t *cmd, const char *arg); #endif -/* RUN_FRAMES ; the run loop calls tick after each frame it runs - * and cancel when the content goes */ +/* RUN_FRAMES ; tick after a frontend advance, cancel on close. */ #define COMMAND_RUN_FRAMES_MAX 36000 bool command_run_frames(command_t *cmd, const char *arg); void command_run_frames_tick(void); @@ -745,7 +744,7 @@ static const struct cmd_action_map action_map[] = { #ifdef HAVE_MCP { "GET_LOG", command_get_log, "[since]", "Report the lines RetroArch has logged since the given line, then NEXT and the line to ask from next time.", CMD_INFO_READ_ONLY, cmd_params_log }, #endif - { "RUN_FRAMES", command_run_frames, "", "Pause and run the content the given number of frames, answered once the last has run; it stays paused.", 0, cmd_params_run_frames }, + { "RUN_FRAMES", command_run_frames, "", "Pause and advance the content by the given number of frontend frames, excluding speculative replay calls; finish paused.", 0, cmd_params_run_frames }, { "SET_INPUT", command_set_input, " ", "Hold a RetroPad button or stick axis for the core, over the player's own input, until set again or released. Answers with what the player holds: buttons and axes, tab separated.", 0, cmd_params_input }, { "RELEASE_INPUT", command_release_input, "[player]", "Let go of everything SET_INPUT holds, for one player or all.", 0, cmd_params_input_release }, { "GET_CORE_OPTION", command_get_core_option, "[key]", "List the running core's options: key, value and description, tab separated; with a key, that option with the values it takes.", CMD_INFO_READ_ONLY, cmd_params_core_option }, diff --git a/core.h b/core.h index 51e63b55aa..ad6d7a1860 100644 --- a/core.h +++ b/core.h @@ -35,8 +35,8 @@ bool core_unset_netplay_callbacks(void); bool core_set_poll_type(unsigned type); -/* Runs the core for one frame. */ -void core_run(void); +/* Runs the core for one frame; false when it did not run. */ +bool core_run(void); void core_reset(void); diff --git a/runahead.c b/runahead.c index dbf24dd614..534b7cba78 100644 --- a/runahead.c +++ b/runahead.c @@ -1749,7 +1749,7 @@ static void runahead_core_run_use_last_input(runloop_state_t *runloop_st) runloop_st->current_core.retro_set_input_state(cbs->state_cb); } -void runahead_run(void *data, +bool runahead_run(void *data, int runahead_count, bool runahead_hide_warnings, bool use_secondary) @@ -1758,6 +1758,7 @@ void runahead_run(void *data, int frame_number = 0; bool last_frame = false; bool suspended_frame = false; + bool advanced = false; #if defined(HAVE_DYNAMIC) const bool have_dynamic = true; settings_t *settings = config_get_ptr(); @@ -1865,7 +1866,7 @@ void runahead_run(void *data, } if (frame_number == 0) - core_run(); + advanced = core_run(); else runahead_core_run_use_last_input(runloop_st); @@ -1879,6 +1880,9 @@ void runahead_run(void *data, AUDIO_FLAGS_CLEAR(audio_st, AUDIO_FLAG_SUSPENDED); } + if (!advanced) + return false; + if (frame_number == 0) { if (!runahead_save_state(runloop_st)) @@ -1888,7 +1892,7 @@ void runahead_run(void *data, runloop_msg_queue_push(_msg, strlen(_msg), 0, 3 * 60, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); RARCH_WARN("[Run-Ahead] %s\n", _msg); - return; + return advanced; } } @@ -1900,7 +1904,7 @@ void runahead_run(void *data, runloop_msg_queue_push(_msg, strlen(_msg), 0, 3 * 60, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); RARCH_WARN("[Run-Ahead] %s\n", _msg); - return; + return advanced; } } } @@ -1912,12 +1916,15 @@ void runahead_run(void *data, /* run main core with video suspended */ video_st->main_flags &= ~VIDEO_FLAG_ACTIVE; - core_run(); + advanced = core_run(); if ((uint32_t)retro_atomic_load_relaxed_int(&video_st->flags) & VIDEO_FLAG_RUNAHEAD_IS_ACTIVE) video_st->main_flags |= VIDEO_FLAG_ACTIVE; else video_st->main_flags &= ~VIDEO_FLAG_ACTIVE; + if (!advanced) + return false; + if ( (runloop_st->flags & RUNLOOP_FLAG_INPUT_IS_DIRTY) || (runloop_st->flags & RUNLOOP_FLAG_RUNAHEAD_FORCE_INPUT_DIRTY)) { @@ -1929,7 +1936,7 @@ void runahead_run(void *data, runloop_msg_queue_push(_msg, strlen(_msg), 0, 3 * 60, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); RARCH_WARN("[Run-Ahead] %s\n", _msg); - return; + return advanced; } if (!runahead_load_state_secondary(runloop_st, settings)) @@ -1938,7 +1945,7 @@ void runahead_run(void *data, runloop_msg_queue_push(_msg, strlen(_msg), 0, 3 * 60, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); RARCH_WARN("[Run-Ahead] %s\n", _msg); - return; + return advanced; } for (frame_number = 0; frame_number < runahead_count - 1; frame_number++) @@ -1965,11 +1972,12 @@ void runahead_run(void *data, #endif } runloop_st->flags &= ~RUNLOOP_FLAG_RUNAHEAD_FORCE_INPUT_DIRTY; - return; + return advanced; force_input_dirty: - core_run(); + advanced = core_run(); runloop_st->flags |= RUNLOOP_FLAG_RUNAHEAD_FORCE_INPUT_DIRTY; + return advanced; } /* Preemptive Frames */ @@ -2303,7 +2311,7 @@ static INLINE void preempt_input_poll(preempt_t *preempt, * * Call in place of core_run() for preemptive frames. **/ -void preempt_run(preempt_t *preempt, void *data) +bool preempt_run(preempt_t *preempt, void *data) { runloop_state_t *runloop_st = (runloop_state_t*)data; struct retro_core_t *current_core = &runloop_st->current_core; @@ -2374,7 +2382,7 @@ void preempt_run(preempt_t *preempt, void *data) runloop_st->auto_state_load_core_ran = true; audio_driver_frame_end(); preempt->frame_count++; - return; + return true; error: runloop_st->flags &= ~(RUNLOOP_FLAG_REQUEST_SPECIAL_SAVESTATE @@ -2387,6 +2395,7 @@ error: runloop_msg_queue_push(_msg, strlen(_msg), 0, 2 * 60, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); RARCH_ERR("[Run-Ahead Preemptive] %s\n", _msg); + return false; } void runahead_clear_variables(void *data) diff --git a/runahead.h b/runahead.h index cd40a086b5..cdddfad00a 100644 --- a/runahead.h +++ b/runahead.h @@ -70,7 +70,8 @@ RETRO_BEGIN_DECLS typedef bool(*runahead_load_state_function)(const void*, size_t); -void runahead_run( +/* Whether the primary core advanced. */ +bool runahead_run( void *data, int runahead_count, bool runahead_hide_warnings, @@ -91,7 +92,8 @@ void runahead_secondary_core_destroy(void *data); bool preempt_init(void *data); void preempt_deinit(void *data); -void preempt_run(preempt_t *preempt, void *data); +/* Whether the normal frame ran, excluding replay. */ +bool preempt_run(preempt_t *preempt, void *data); RETRO_END_DECLS diff --git a/runloop.c b/runloop.c index d82020de2f..28990fa5c6 100644 --- a/runloop.c +++ b/runloop.c @@ -9439,6 +9439,7 @@ int runloop_iterate(void) #endif float slowmotion_ratio = settings->floats.slowmotion_ratio; bool audio_sync = settings->bools.audio_sync; + bool frame_advanced; bool savestate_automatic_enable = settings->uints.savestate_automatic_interval > 0; runloop_msg_queue_drain_deferred(); @@ -9803,23 +9804,25 @@ int runloop_iterate(void) #endif if (want_runahead) - runahead_run( + frame_advanced = runahead_run( runloop_st, run_ahead_num_frames, run_ahead_hide_warnings, run_ahead_secondary_instance); else if (runloop_st->preempt_data) - preempt_run(runloop_st->preempt_data, runloop_st); + frame_advanced = preempt_run(runloop_st->preempt_data, runloop_st); else #endif - core_run(); + frame_advanced = core_run(); } runloop_load_deferred_auto_state(); #ifdef HAVE_COMMAND - if (runloop_st->frame_work & RUNLOOP_WORK_RUN_FRAMES) + if (frame_advanced && (runloop_st->frame_work & RUNLOOP_WORK_RUN_FRAMES)) command_run_frames_tick(); +#else + (void)frame_advanced; #endif /* Increment runtime tick counter after each call to @@ -10706,7 +10709,7 @@ static void runloop_load_deferred_auto_state(void) runloop_st->auto_state_load_core_ran = false; } -void core_run(void) +bool core_run(void) { runloop_state_t *runloop_st = &runloop_state; struct retro_core_t * @@ -10718,6 +10721,7 @@ void core_run(void) : (enum poll_type)(current_core->poll_type); bool early_polling = (new_poll_type == POLL_TYPE_EARLY); bool late_polling = (new_poll_type == POLL_TYPE_LATE); + bool ran = false; #ifdef HAVE_NETWORKING /* Declared with the other locals and assigned below, so the * closing guard can return before netplay's pre-frame call @@ -10742,7 +10746,7 @@ void core_run(void) { input_driver_poll_between_frames(input_driver_poll); video_driver_cached_frame(); - return; + return false; } #ifdef HAVE_NETWORKING @@ -10755,7 +10759,7 @@ void core_run(void) * netplay peer pausing doesn't just hang. */ input_driver_poll_between_frames(input_driver_poll); video_driver_cached_frame(); - return; + return false; } #endif @@ -10779,6 +10783,7 @@ void core_run(void) * all, reach the frame's own audio rather than the next frame's. */ audio_driver_publish_runloop(); current_core->retro_run(); + ran = true; if (runloop_st->auto_state_load_pending) runloop_st->auto_state_load_core_ran = true; audio_driver_frame_end(); @@ -10818,6 +10823,7 @@ void core_run(void) #ifdef HAVE_NETWORKING netplay_driver_ctl(RARCH_NETPLAY_CTL_POST_FRAME, NULL); #endif + return ran; } bool core_has_set_input_descriptor(void) diff --git a/samples/network/command_frames/Makefile b/samples/network/command_frames/Makefile new file mode 100644 index 0000000000..12c49d41bb --- /dev/null +++ b/samples/network/command_frames/Makefile @@ -0,0 +1,14 @@ +MAKE_ARGS ?= +EXE ?= + +all: + @MAKE_ARGS="$(MAKE_ARGS)" ./build.sh + +check: all + $(RUNNER) ./command_frames_test$(EXE) ./harness_core.so + +clean: + rm -f command_frames_test command_frames_test.exe harness_core.so + rm -f retroarch_nomain.o retroarch_nomain.d harness_main.o harness_main.d + +.PHONY: all check clean diff --git a/samples/network/command_frames/build.sh b/samples/network/command_frames/build.sh new file mode 100755 index 0000000000..a4f7c30390 --- /dev/null +++ b/samples/network/command_frames/build.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# Links the shipping frontend objects with a test main and netplay gate. +# Requires a built non-Qt tree; MAKE_ARGS selects its build flags. +set -eu + +here=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$here/../../.." && pwd) +out=samples/network/command_frames +MAKE_ARGS=${MAKE_ARGS:-} + +cd "$root" + +cc_line=$(mktemp) +ld_line=$(mktemp) +trap 'rm -f "$cc_line" "$ld_line"' EXIT + +# The project's own compile line for retroarch.o, which also tells us +# which object directory this configuration builds into. +touch retroarch.c +make -n $MAKE_ARGS 2>/dev/null | grep -E '\-o obj-unix/[^/ ]+/retroarch\.o' | head -1 > "$cc_line" +if [ ! -s "$cc_line" ]; then + echo "could not determine the compile command for retroarch.o" >&2 + exit 1 +fi +objdir=$(grep -oE 'obj-unix/[^/ ]+/retroarch\.o' "$cc_line" | head -1 | sed 's#/retroarch\.o##') +if [ ! -f "$objdir/video_driver.o" ] && [ ! -f "$objdir/gfx/video_driver.o" ]; then + echo "build RetroArch first: ./configure --disable-qt && make $MAKE_ARGS" >&2 + exit 1 +fi + +sed "s#-o $objdir/retroarch\.o#-Dmain=rarch_harness_unused_main -o $out/retroarch_nomain.o#" \ + "$cc_line" | sh + +sed -e "s#-o $objdir/retroarch\.o#-o $out/harness_main.o#" \ + -e "s# retroarch\.c# $out/command_frames_test.c#" \ + "$cc_line" | sh + +rm -f retroarch retroarch_debug retroarch.exe retroarch_debug.exe +make -n $MAKE_ARGS 2>/dev/null | grep -E ' -o retroarch(_debug)?(\.exe)? ' | tail -1 > "$ld_line" +if [ ! -s "$ld_line" ]; then + echo "could not determine the link command" >&2 + exit 1 +fi + +sed -e "s#$objdir/retroarch\.o#$out/retroarch_nomain.o $out/harness_main.o#" \ + -e "s#-o retroarch\(_debug\)\?\(\.exe\)\? #-Wl,--wrap=netplay_driver_ctl -o $out/command_frames_test\2 #" \ + "$ld_line" | sh + +# The harness core: a plain shared library, no sanitizer, so that what +# the sanitizer reports is the frontend. Built with the compiler the +# tree's own compile line names, so a cross build (mingw) gets a core +# the frontend can load; the .so name is only a name, every loader +# takes it. +core_cc=$(awk '{print $1}' "$cc_line") +$core_cc -O1 -g -shared -fPIC -Ilibretro-common/include -o $out/harness_core.so $out/harness_core.c + +echo "built $out/command_frames_test and $out/harness_core.so" diff --git a/samples/network/command_frames/command_frames_test.c b/samples/network/command_frames/command_frames_test.c new file mode 100644 index 0000000000..cadccad425 --- /dev/null +++ b/samples/network/command_frames/command_frames_test.c @@ -0,0 +1,148 @@ + +#include +#include +#include +#include +#include +#include