runloop: tick RUN_FRAMES only after advancing the core

This commit is contained in:
Codex authored and libretroadmin committed 2026-10-13 16:57:29 +02:00
1 parent 92d69e6025
commit 2c6b61e9fb
10 files changed
+322 -25

No files matched your search

+9
View File
@@ -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.
+2 -3
View File
@@ -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 <count>; the run loop calls tick after each frame it runs
* and cancel when the content goes */
/* RUN_FRAMES <count>; 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, "<count>", "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, "<count>", "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, "<player> <control> <value>", "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 },
+2 -2
View File
@@ -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);
+20 -11
View File
@@ -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)
+4 -2
View File
@@ -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
+13 -7
View File
@@ -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)
+14
View File
@@ -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
+57
View File
@@ -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"
@@ -0,0 +1,148 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <file/file_path.h>
#include <file/config_file.h>
#include <time/rtime.h>
#include "../../../runloop.h"
#include "../../../retroarch.h"
#include "../../../configuration.h"
#include "../../../command.h"
#include "../../../frontend/frontend_driver.h"
#include "../../../frontend/frontend.h"
#include "../../../runahead.h"
#include "../../../network/netplay/netplay.h"
#include "../../../menu/menu_driver.h"
static unsigned failures;
#define CHECK(c) do { if (!(c)) { fprintf(stderr, "FAIL %u: %s\n", (unsigned)__LINE__, #c); failures++; } } while (0)
static bool blocked;
static unsigned calls, gates;
static void (*loaded_run)(void);
static char reply[256];
static void counted_run(void) { calls++; loaded_run(); }
static bool fail_save(void *data, size_t size) { (void)data; (void)size; return false; }
bool __real_netplay_driver_ctl(enum rarch_netplay_ctl_state, void*);
bool __wrap_netplay_driver_ctl(enum rarch_netplay_ctl_state state, void *data)
{
if (state == RARCH_NETPLAY_CTL_PRE_FRAME && blocked)
{ gates++; return false; }
return __real_netplay_driver_ctl(state, data);
}
static void receive(command_t *cmd, const char *data, size_t n)
{
(void)cmd;
if (n >= sizeof(reply)) n = sizeof(reply)-1;
memcpy(reply,data,n); reply[n]=0;
}
int main(int argc, char **argv)
{
char cfg_path[512];
char *args[7];
FILE *cfg;
unsigned i, before;
command_t cmd;
runloop_state_t *loop;
if (argc < 2) return 2;
snprintf(cfg_path,sizeof(cfg_path),"%s.cfg",argv[0]);
cfg=fopen(cfg_path,"wb"); if (!cfg) return 2;
fputs("video_driver = \"null\"\naudio_driver = \"null\"\ninput_driver = \"null\"\ninput_joypad_driver = \"null\"\nmenu_driver = \"rgui\"\nvideo_threaded = \"false\"\nvideo_vsync = \"false\"\npause_nonactive = \"false\"\nconfig_save_on_exit = \"false\"\nthreaded_data_runloop_enable = \"false\"\n",cfg);
fclose(cfg);
config_file_set_io_default(config_file_io_filestream());
rtime_init(); retroarch_config_init();
retroarch_ctl(RARCH_CTL_STATE_FREE,NULL);
frontend_driver_init_first(NULL);
args[0]=(char*)"retroarch"; args[1]=(char*)"--config"; args[2]=cfg_path;
args[3]=(char*)"-L"; args[4]=argv[1]; args[5]=NULL;
if (!retroarch_main_init(5,args)) return 2;
for (i=0;i<5;i++) runloop_iterate();
loop=runloop_state_get_ptr();
if (loop->current_core_type == CORE_TYPE_DUMMY) return 2;
loaded_run=loop->current_core.retro_run;
loop->current_core.retro_run=counted_run;
memset(&cmd,0,sizeof(cmd)); cmd.replier=receive;
before=calls; blocked=true;
if (!command_run_frames(&cmd,"3")) return 2;
for (i=0;i<3;i++) runloop_iterate();
printf("blocked: core_calls=%u gate_denials=%u pending=%u reply=%s\n",calls-before,gates,
!!(loop->frame_work & RUNLOOP_WORK_RUN_FRAMES),reply);
CHECK(calls == before && gates == 3 && !reply[0]);
CHECK(loop->frame_work & RUNLOOP_WORK_RUN_FRAMES);
blocked=false;
before=calls; reply[0]=0;
for (i=0;i<3;i++) runloop_iterate();
printf("unblocked: core_calls=%u pending=%u reply=%s\n",calls-before,
!!(loop->frame_work & RUNLOOP_WORK_RUN_FRAMES),reply);
CHECK(calls - before == 3 && !strcmp(reply, "RUN_FRAMES 3\n"));
CHECK(!(loop->frame_work & RUNLOOP_WORK_RUN_FRAMES));
before=calls;
for (i=0;i<3;i++) runloop_iterate();
CHECK(calls == before && (loop->flags & RUNLOOP_FLAG_PAUSED));
{
settings_t *settings=config_get_ptr();
uint32_t *state=(uint32_t*)loop->current_core.retro_get_memory_data(RETRO_MEMORY_SYSTEM_RAM);
unsigned base=*state;
before=calls; reply[0]=0;
runahead_clear_variables(loop);
settings->bools.run_ahead_enabled=true;
settings->bools.run_ahead_secondary_instance=false;
settings->uints.run_ahead_frames=2;
if (!command_run_frames(&cmd,"3")) return 2;
for (i=0;i<3;i++) runloop_iterate();
printf("runahead: core_calls=%u logical_frames=%u pending=%u reply=%s\n",calls-before,*state-base,
!!(loop->frame_work & RUNLOOP_WORK_RUN_FRAMES),reply);
CHECK(calls-before == 9 && *state-base == 3 && !strcmp(reply,"RUN_FRAMES 3\n"));
settings->bools.run_ahead_enabled=false;
settings->bools.preemptive_frames_enable=true;
preempt_deinit(loop);
if (!preempt_init(loop)) return 2;
base=*state; before=calls; reply[0]=0;
if (!command_run_frames(&cmd,"3")) return 2;
for (i=0;i<3;i++) runloop_iterate();
printf("preempt: core_calls=%u logical_frames=%u pending=%u reply=%s\n",calls-before,*state-base,
!!(loop->frame_work & RUNLOOP_WORK_RUN_FRAMES),reply);
CHECK(calls-before == 3 && *state-base == 3 && !strcmp(reply,"RUN_FRAMES 3\n"));
CHECK(command_set_input(&cmd,"1 b 1"));
base=*state; before=calls; reply[0]=0;
CHECK(command_run_frames(&cmd,"3"));
for (i=0;i<3;i++) runloop_iterate();
printf("preempt replay: core_calls=%u logical_frames=%u reply=%s\n",calls-before,*state-base,reply);
CHECK(calls-before > 3 && *state-base == 3 && !strcmp(reply,"RUN_FRAMES 3\n"));
{
bool (*save)(void*,size_t)=loop->current_core.retro_serialize;
loop->current_core.retro_serialize=fail_save;
before=calls; reply[0]=0;
CHECK(command_run_frames(&cmd,"1"));
runloop_iterate();
CHECK(calls == before && !reply[0] && (loop->frame_work & RUNLOOP_WORK_RUN_FRAMES));
loop->current_core.retro_serialize=save;
runloop_iterate();
CHECK(calls-before == 1 && !strcmp(reply,"RUN_FRAMES 1\n"));
}
CHECK(command_release_input(&cmd,""));
preempt_deinit(loop);
}
before=calls; reply[0]=0;
CHECK(command_run_frames(&cmd,"3"));
runloop_iterate();
blocked=true;
for(i=0;i<3;i++) runloop_iterate();
CHECK(calls-before == 1 && !reply[0] && (loop->frame_work & RUNLOOP_WORK_RUN_FRAMES));
blocked=false;
runloop_iterate(); runloop_iterate();
CHECK(calls-before == 3 && !strcmp(reply,"RUN_FRAMES 3\n"));
before=calls; reply[0]=0;
CHECK(command_run_frames(&cmd,"1"));
loop->current_core.retro_run=NULL;
runloop_iterate();
CHECK(calls==before && !reply[0] && (loop->frame_work & RUNLOOP_WORK_RUN_FRAMES));
loop->current_core.retro_run=counted_run;
runloop_iterate();
CHECK(calls-before==1 && !strcmp(reply,"RUN_FRAMES 1\n"));
main_exit(NULL); remove(cfg_path);
printf("command_frames: %s (%u failures)\n",failures?"FAIL":"PASS",failures);
return failures ? 1 : 0;
}
@@ -0,0 +1,53 @@
#include <string.h>
#include <stdint.h>
#include <libretro.h>
static retro_video_refresh_t video;
static retro_input_poll_t poll;
static retro_input_state_t input;
static uint32_t frame;
static uint32_t pixels[64];
void retro_set_environment(retro_environment_t cb)
{
bool no_game=true;
enum retro_pixel_format fmt=RETRO_PIXEL_FORMAT_XRGB8888;
cb(RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME,&no_game);
cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT,&fmt);
}
void retro_set_video_refresh(retro_video_refresh_t cb) { video=cb; }
void retro_set_audio_sample(retro_audio_sample_t cb) { (void)cb; }
void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb) { (void)cb; }
void retro_set_input_poll(retro_input_poll_t cb) { poll=cb; }
void retro_set_input_state(retro_input_state_t cb) { input=cb; }
void retro_init(void) { frame=0; }
void retro_deinit(void) { }
unsigned retro_api_version(void) { return RETRO_API_VERSION; }
void retro_get_system_info(struct retro_system_info *i)
{
memset(i,0,sizeof(*i)); i->library_name="command_frames_harness";
i->library_version="1"; i->valid_extensions="";
}
void retro_get_system_av_info(struct retro_system_av_info *i)
{
memset(i,0,sizeof(*i)); i->geometry.base_width=i->geometry.max_width=8;
i->geometry.base_height=i->geometry.max_height=8;
i->timing.fps=60.0; i->timing.sample_rate=48000.0;
}
void retro_set_controller_port_device(unsigned p,unsigned d) { (void)p; (void)d; }
void retro_reset(void) { frame=0; }
void retro_run(void)
{
poll(); input(0,RETRO_DEVICE_JOYPAD,0,RETRO_DEVICE_ID_JOYPAD_B);
frame++; pixels[0]=frame; video(pixels,8,8,8*sizeof(*pixels));
}
size_t retro_serialize_size(void) { return sizeof(frame); }
bool retro_serialize(void *p,size_t n) { if(n<sizeof(frame))return false; memcpy(p,&frame,sizeof(frame));return true; }
bool retro_unserialize(const void *p,size_t n) { if(n<sizeof(frame))return false; memcpy(&frame,p,sizeof(frame));return true; }
void retro_cheat_reset(void) { }
void retro_cheat_set(unsigned i,bool b,const char *c) { (void)i;(void)b;(void)c; }
bool retro_load_game(const struct retro_game_info *g) { (void)g;return true; }
bool retro_load_game_special(unsigned t,const struct retro_game_info *g,size_t n) { (void)t;(void)g;(void)n;return false; }
void retro_unload_game(void) { }
unsigned retro_get_region(void) { return RETRO_REGION_NTSC; }
void *retro_get_memory_data(unsigned id) { return id==RETRO_MEMORY_SYSTEM_RAM?&frame:NULL; }
size_t retro_get_memory_size(unsigned id) { return id==RETRO_MEMORY_SYSTEM_RAM?sizeof(frame):0; }