Skip to content

Commit f4a23bb

Browse files
committed
Add offline RetroAchievements rc_libretro E2E test
Download pinned rcheevos, build librcheevos.a, verify rc_libretro_memory_* against Virtual Jaguar with the same Jaguar console regions RetroArch uses. CI runs after the memory map contract test; respects matrix CC for i686. Made-with: Cursor
1 parent 1bdd2fb commit f4a23bb

6 files changed

Lines changed: 357 additions & 0 deletions

File tree

.github/workflows/c-cpp.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,13 @@ jobs:
202202
$CC -O2 -Wall -o test_memory_map test/tools/test_memory_map.c $LDFLAGS
203203
./test_memory_map ./${{ matrix.config.artifact }}
204204
205+
- name: RetroAchievements rc_libretro E2E test
206+
if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.cross && runner.os != 'Windows' }}
207+
env:
208+
RCHEEVOS_REF: v12.3.0
209+
CC: ${{ matrix.config.cc }}
210+
run: bash test/tools/test_rcheevos_e2e.sh ./${{ matrix.config.artifact }}
211+
205212
- name: Upload artifact
206213
uses: actions/upload-artifact@v4
207214
with:

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Local E2E test build (downloads rcheevos tarball)
2+
/build/
3+
14
# Build artifacts
25
*.o
36
*.so

CLAUDE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ Key docs:
9292

9393
### Testing
9494

95+
RetroAchievements-related (no network; offline):
96+
97+
- `test/tools/test_memory_map.c` — asserts `SET_MEMORY_MAPS` / `SET_SUPPORT_ACHIEVEMENTS` and descriptor layout vs `retro_get_memory_data(SYSTEM_RAM)`.
98+
- `test/tools/test_rcheevos_e2e.sh` — downloads pinned **rcheevos** (`RCHEEVOS_REF`, default `v12.3.0`), builds `librcheevos.a`, runs `test_rcheevos_e2e` to verify **rc_libretro** memory resolution (`RC_CONSOLE_ATARI_JAGUAR`) matches host RAM — same mapping stack RetroArch uses before hitting the RA API.
99+
95100
See `docs/test-infrastructure.md` for all test harnesses:
96101
- `test/headless.py` — Python headless runner via libretro.py (screenshots, frame control)
97102
- `test/regression_test.sh` — screenshot regression suite with baseline comparison
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env bash
2+
# Builds a static librcherros.a from the official RetroAchievements/rcheevos repo.
3+
# Used by test_rcheevos_e2e.sh. Pin RCHEEVOS_REF (tag or SHA) for reproducibility.
4+
set -euo pipefail
5+
6+
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
7+
DEST="${DEST:-$ROOT/build/rcheevos-static}"
8+
TAG="${RCHEEVOS_REF:-v12.3.0}"
9+
CC="${CC:-cc}"
10+
11+
mkdir -p "$DEST"
12+
13+
if [[ ! -f "$DEST/.extracted_${TAG}" ]]; then
14+
rm -rf "${DEST:?}/"* "$DEST/.extracted_"*
15+
TMP="$DEST/dl"
16+
mkdir -p "$TMP"
17+
echo "Downloading rcheevos ${TAG}..."
18+
curl -fsSL -o "$TMP/rc.tgz" "https://github.com/RetroAchievements/rcheevos/archive/refs/tags/${TAG}.tar.gz"
19+
tar -xzf "$TMP/rc.tgz" -C "$DEST"
20+
rm -rf "$TMP"
21+
SRC="$(echo "$DEST"/rcheevos-* | head -1)"
22+
mv "$SRC" "$DEST/rcheevos-src"
23+
touch "$DEST/.extracted_${TAG}"
24+
fi
25+
26+
SRCROOT="$DEST/rcheevos-src"
27+
OBJDIR="$DEST/obj"
28+
rm -rf "$OBJDIR"
29+
mkdir -p "$OBJDIR"
30+
31+
CFLAGS="-O2 -I${SRCROOT}/include -I${SRCROOT}/src -I${ROOT}/libretro-common/include -DRC_DISABLE_LUA -DRC_CLIENT_SUPPORTS_HASH"
32+
33+
echo "Compiling rcheevos sources..."
34+
while IFS= read -r -d '' f; do
35+
bn="$(basename "$f" .c)"
36+
$CC -c $CFLAGS -o "$OBJDIR/${bn}.o" "$f"
37+
done < <(find "$SRCROOT/src" -name '*.c' -print0)
38+
39+
ar rcs "$DEST/librcheevos.a" "$OBJDIR"/*.o
40+
echo "Built $DEST/librcheevos.a"

test/tools/test_rcheevos_e2e.c

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/*
2+
* test_rcheevos_e2e.c — Load Virtual Jaguar, feed rcheevos rc_libretro with the same
3+
* SET_MEMORY_MAPS descriptor RetroArch receives, then verify address resolution matches
4+
* host RAM (offline; no RetroAchievements server).
5+
*/
6+
7+
#include <stdio.h>
8+
#include <stdlib.h>
9+
#include <string.h>
10+
#include <stdint.h>
11+
#include <stdbool.h>
12+
13+
#ifdef __APPLE__
14+
#include <dlfcn.h>
15+
#else
16+
#include <dlfcn.h>
17+
#endif
18+
19+
#include "../../libretro-common/include/libretro.h"
20+
#include "rc_libretro.h"
21+
#include "rc_consoles.h"
22+
23+
typedef void (*retro_init_t)(void);
24+
typedef void (*retro_deinit_t)(void);
25+
typedef void (*retro_set_environment_t)(retro_environment_t);
26+
typedef void (*retro_set_video_refresh_t)(retro_video_refresh_t);
27+
typedef void (*retro_set_audio_sample_t)(retro_audio_sample_t);
28+
typedef void (*retro_set_audio_sample_batch_t)(retro_audio_sample_batch_t);
29+
typedef void (*retro_set_input_poll_t)(retro_input_poll_t);
30+
typedef void (*retro_set_input_state_t)(retro_input_state_t);
31+
typedef bool (*retro_load_game_t)(const struct retro_game_info *);
32+
typedef void (*retro_unload_game_t)(void);
33+
typedef void *(*retro_get_memory_data_t)(unsigned);
34+
typedef size_t (*retro_get_memory_size_t)(unsigned);
35+
36+
static const struct retro_memory_map *g_mmap;
37+
static retro_get_memory_data_t g_get_memory_data;
38+
static retro_get_memory_size_t g_get_memory_size;
39+
40+
static bool env_cb(unsigned cmd, void *data)
41+
{
42+
switch (cmd)
43+
{
44+
case RETRO_ENVIRONMENT_SET_MEMORY_MAPS:
45+
g_mmap = (const struct retro_memory_map *)data;
46+
return true;
47+
case RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS:
48+
return true;
49+
case RETRO_ENVIRONMENT_SET_PIXEL_FORMAT:
50+
case RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME:
51+
return true;
52+
case RETRO_ENVIRONMENT_GET_VARIABLE:
53+
return false;
54+
case RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE:
55+
if (data) *(bool *)data = false;
56+
return true;
57+
case RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY:
58+
case RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY:
59+
if (data) *(const char **)data = ".";
60+
return true;
61+
case RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION:
62+
if (data) *(unsigned *)data = 0;
63+
return true;
64+
case RETRO_ENVIRONMENT_GET_INPUT_BITMASKS:
65+
return true;
66+
default:
67+
return false;
68+
}
69+
}
70+
71+
static void libretro_get_core_memory_info(uint32_t id, rc_libretro_core_memory_info_t *info)
72+
{
73+
if (!g_get_memory_data || !g_get_memory_size)
74+
{
75+
info->data = NULL;
76+
info->size = 0;
77+
return;
78+
}
79+
info->data = (uint8_t *)g_get_memory_data(id);
80+
info->size = g_get_memory_size(id);
81+
}
82+
83+
static void video_cb(const void *d, unsigned w, unsigned h, size_t p)
84+
{ (void)d; (void)w; (void)h; (void)p; }
85+
static void audio_cb(int16_t l, int16_t r)
86+
{ (void)l; (void)r; }
87+
static size_t audio_batch(const int16_t *d, size_t f)
88+
{ (void)d; return f; }
89+
static void input_poll(void) {}
90+
static int16_t input_state(unsigned p, unsigned d, unsigned i, unsigned id)
91+
{ (void)p; (void)d; (void)i; (void)id; return 0; }
92+
93+
static void *load_sym(void *handle, const char *name)
94+
{
95+
void *sym = dlsym(handle, name);
96+
if (!sym)
97+
{
98+
fprintf(stderr, "ERROR: Missing symbol: %s\n", name);
99+
exit(1);
100+
}
101+
return sym;
102+
}
103+
104+
static uint8_t *make_dummy_rom(size_t *size_out)
105+
{
106+
size_t sz = 8192;
107+
uint8_t *rom = calloc(1, sz);
108+
if (!rom) { perror("calloc"); exit(1); }
109+
rom[0x404] = 0x00; rom[0x405] = 0x80;
110+
rom[0x406] = 0x20; rom[0x407] = 0x00;
111+
rom[0x2000] = 0x60; rom[0x2001] = 0xFE;
112+
*size_out = sz;
113+
return rom;
114+
}
115+
116+
int main(int argc, char **argv)
117+
{
118+
void *handle;
119+
retro_init_t core_init;
120+
retro_deinit_t core_deinit;
121+
retro_set_environment_t core_set_env;
122+
retro_set_video_refresh_t core_set_video;
123+
retro_set_audio_sample_t core_set_audio;
124+
retro_set_audio_sample_batch_t core_set_audio_batch;
125+
retro_set_input_poll_t core_set_input_poll;
126+
retro_set_input_state_t core_set_input_state;
127+
retro_load_game_t core_load_game;
128+
retro_unload_game_t core_unload_game;
129+
retro_get_memory_data_t core_get_memory_data;
130+
retro_get_memory_size_t core_get_memory_size;
131+
struct retro_game_info info;
132+
uint8_t *rom;
133+
size_t rom_size;
134+
rc_libretro_memory_regions_t regions;
135+
uint8_t *sysram;
136+
uint8_t buf[4];
137+
uint32_t avail;
138+
uint8_t *pfind;
139+
int failures = 0;
140+
141+
if (argc < 2)
142+
{
143+
fprintf(stderr, "Usage: %s <core.so|dylib>\n", argv[0]);
144+
return 1;
145+
}
146+
147+
handle = dlopen(argv[1], RTLD_LAZY);
148+
if (!handle) { fprintf(stderr, "dlopen: %s\n", dlerror()); return 1; }
149+
150+
core_init = (retro_init_t)load_sym(handle, "retro_init");
151+
core_deinit = (retro_deinit_t)load_sym(handle, "retro_deinit");
152+
core_set_env = (retro_set_environment_t)load_sym(handle, "retro_set_environment");
153+
core_set_video = (retro_set_video_refresh_t)load_sym(handle, "retro_set_video_refresh");
154+
core_set_audio = (retro_set_audio_sample_t)load_sym(handle, "retro_set_audio_sample");
155+
core_set_audio_batch = (retro_set_audio_sample_batch_t)load_sym(handle, "retro_set_audio_sample_batch");
156+
core_set_input_poll = (retro_set_input_poll_t)load_sym(handle, "retro_set_input_poll");
157+
core_set_input_state = (retro_set_input_state_t)load_sym(handle, "retro_set_input_state");
158+
core_load_game = (retro_load_game_t)load_sym(handle, "retro_load_game");
159+
core_unload_game = (retro_unload_game_t)load_sym(handle, "retro_unload_game");
160+
core_get_memory_data = (retro_get_memory_data_t)load_sym(handle, "retro_get_memory_data");
161+
core_get_memory_size = (retro_get_memory_size_t)load_sym(handle, "retro_get_memory_size");
162+
163+
g_mmap = NULL;
164+
g_get_memory_data = core_get_memory_data;
165+
g_get_memory_size = core_get_memory_size;
166+
167+
core_set_env(env_cb);
168+
core_set_video(video_cb);
169+
core_set_audio(audio_cb);
170+
core_set_audio_batch(audio_batch);
171+
core_set_input_poll(input_poll);
172+
core_set_input_state(input_state);
173+
core_init();
174+
175+
rom = make_dummy_rom(&rom_size);
176+
memset(&info, 0, sizeof(info));
177+
info.path = "dummy.j64";
178+
info.data = rom;
179+
info.size = rom_size;
180+
181+
if (!core_load_game(&info))
182+
{
183+
fprintf(stderr, "retro_load_game failed\n");
184+
free(rom);
185+
core_deinit();
186+
dlclose(handle);
187+
return 1;
188+
}
189+
190+
printf("Test 1: SET_MEMORY_MAPS captured ... ");
191+
if (!g_mmap || !g_mmap->descriptors || g_mmap->num_descriptors < 1)
192+
{
193+
printf("FAIL\n");
194+
failures++;
195+
}
196+
else
197+
printf("PASS\n");
198+
199+
memset(&regions, 0, sizeof(regions));
200+
printf("Test 2: rc_libretro_memory_init(Jaguar) ... ");
201+
if (!rc_libretro_memory_init(&regions, g_mmap, libretro_get_core_memory_info,
202+
RC_CONSOLE_ATARI_JAGUAR))
203+
{
204+
printf("FAIL\n");
205+
failures++;
206+
}
207+
else
208+
printf("PASS\n");
209+
210+
sysram = (uint8_t *)core_get_memory_data(RETRO_MEMORY_SYSTEM_RAM);
211+
if (sysram)
212+
{
213+
sysram[0xABCD] = 0x42;
214+
sysram[0x1FFFFE] = 0x11;
215+
sysram[0x1FFFFF] = 0x22;
216+
}
217+
218+
printf("Test 3: rc_libretro_memory_read(0xABCD) ... ");
219+
memset(buf, 0, sizeof(buf));
220+
if (rc_libretro_memory_read(&regions, 0xABCDU, buf, 1) == 1 && buf[0] == 0x42)
221+
printf("PASS\n");
222+
else
223+
{
224+
printf("FAIL\n");
225+
failures++;
226+
}
227+
228+
printf("Test 4: rc_libretro_memory_find(0xABCD) matches host ptr ... ");
229+
pfind = rc_libretro_memory_find(&regions, 0xABCDU);
230+
if (sysram && pfind == sysram + 0xABCD)
231+
printf("PASS\n");
232+
else
233+
{
234+
printf("FAIL\n");
235+
failures++;
236+
}
237+
238+
printf("Test 5: cross-boundary read at end of RAM ... ");
239+
memset(buf, 0, sizeof(buf));
240+
if (rc_libretro_memory_read(&regions, 0x1FFFFEU, buf, 2) == 2
241+
&& buf[0] == 0x11 && buf[1] == 0x22)
242+
printf("PASS\n");
243+
else
244+
{
245+
printf("FAIL\n");
246+
failures++;
247+
}
248+
249+
printf("Test 6: rc_libretro_memory_find_avail ... ");
250+
pfind = rc_libretro_memory_find_avail(&regions, 0x1FFFFEU, &avail);
251+
if (pfind && avail >= 2 && pfind == sysram + 0x1FFFFE)
252+
printf("PASS\n");
253+
else
254+
{
255+
printf("FAIL\n");
256+
failures++;
257+
}
258+
259+
rc_libretro_memory_destroy(&regions);
260+
261+
core_unload_game();
262+
core_deinit();
263+
free(rom);
264+
dlclose(handle);
265+
266+
printf("\n%s: %d test(s) failed.\n", failures ? "FAIL" : "OK", failures);
267+
return failures ? 1 : 0;
268+
}

test/tools/test_rcheevos_e2e.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env bash
2+
# End-to-end check: load the core, capture SET_MEMORY_MAPS, run rcheevos rc_libretro
3+
# memory initialization (same path RetroArch uses for Jaguar / console 17), then read
4+
# bytes via rc_libretro_memory_read / rc_libretro_memory_find.
5+
#
6+
# Requires: bash, curl, cc, ar, dlopen (libc)
7+
# Usage: ./test/tools/test_rcheevos_e2e.sh path/to/virtualjaguar_libretro.{so,dylib}
8+
set -euo pipefail
9+
10+
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
11+
CORE="${1:?Usage: $0 path/to/core}"
12+
export CC="${CC:-cc}"
13+
14+
"$ROOT/test/tools/build_rcheevos_static.sh"
15+
16+
DEST="${DEST:-$ROOT/build/rcheevos-static}"
17+
LIBRC="$DEST/librcheevos.a"
18+
19+
if [[ "$(uname)" == Linux ]]; then
20+
LDFLAGS="-ldl"
21+
else
22+
LDFLAGS=""
23+
fi
24+
25+
$CC -O2 -Wall \
26+
-I"$ROOT/libretro-common/include" \
27+
-I"$DEST/rcheevos-src/include" \
28+
-I"$DEST/rcheevos-src/src" \
29+
-o "$ROOT/build/test_rcheevos_e2e" \
30+
"$ROOT/test/tools/test_rcheevos_e2e.c" \
31+
"$LIBRC" \
32+
$LDFLAGS
33+
34+
"$ROOT/build/test_rcheevos_e2e" "$CORE"

0 commit comments

Comments
 (0)