Files
RetroArch/.github/workflows/Linux-libretro-common-samples.yml
T
LibretroAdmin 2ddc97ff80 archive_file: bzip2 zip members and .bz2 files
Zip members stored with method 12 are decoded with rbzip2, whole, from
the mapping, from what the stream lends or else read, as Zstandard
members are. A .bz2 file stands for the file named as it is without the
extension: a stream does not say how long it decodes to, so the member
is decoded when it is listed, its size and CRC then known, and kept for
the extraction that follows. "#" after ".bz2" names its member.

HAVE_RBZIP2, on unless configured off, builds both and the decoder;
RVZ's bzip2 groups take the decoder alone where it is off.
file_archive_whole() is the whole-file read the .zst and .bz2 backends
share.

archive_bz2_test lists, reads and extracts bzip2 zip members and a .bz2
file, and refuses members whose CRC is wrong.
2026-10-27 09:08:39 +01:00

1656 lines
66 KiB
YAML

name: CI Linux libretro-common samples
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
permissions:
contents: read
env:
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
jobs:
samples:
name: Build and run libretro-common/samples
runs-on: ubuntu-latest
env:
APT_PKGS: build-essential zlib1g-dev clang ffmpeg ccache
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Apt cache key
id: apt
run: sh .github/workflows/scripts/apt-cache-key.sh "$APT_PKGS"
- name: Restore apt cache
uses: actions/cache/restore@v4
with:
path: ~/.apt-cache
key: ${{ steps.apt.outputs.key }}
restore-keys: ${{ steps.apt.outputs.prefix }}
- name: Install dependencies
timeout-minutes: 20
run: sh .github/workflows/scripts/apt-install.sh $APT_PKGS
- name: Save apt cache
if: env.APT_CACHE_SAVE == '1'
uses: actions/cache/save@v4
with:
path: ~/.apt-cache
key: ${{ steps.apt.outputs.key }}
- name: Link with lld
run: sh .github/workflows/scripts/use-lld.sh
- name: Restore ccache
uses: actions/cache/restore@v4
with:
path: ~/.ccache
key: ccache-${{ github.workflow }}-${{ github.job }}-${{ github.sha }}
restore-keys: |
ccache-${{ github.workflow }}-${{ github.job }}-
- name: Enable ccache
run: |
set -eu
export CCACHE_DIR="$HOME/.ccache"
echo "CCACHE_DIR=$CCACHE_DIR" >> "$GITHUB_ENV"
echo "/usr/lib/ccache" >> "$GITHUB_PATH"
ccache -M 250M
ccache -z
- name: Build and run samples
shell: bash
working-directory: libretro-common/samples
env:
# Build samples under AddressSanitizer + UndefinedBehaviorSanitizer.
# Most overflow tests under libretro-common/samples (rpng_chunk_
# overflow_test, rbmp_test, vfs_read_overflow_test, cdrom_cuesheet
# _overflow_test, cdfs_dir_record_test, chd_meta_overflow_test, ...)
# use ASan as the regression discriminator for the bound-check
# they verify, so make ASan the default for this workflow. Each
# test's Makefile honours SANITIZER= via the conventional opt-in
# block (matching samples/tasks/http/Makefile). Test sources
# that don't have the opt-in block treat the variable as
# ignored.
MAKE_ARGS: "SANITIZER=address,undefined"
run: |
set -u
set -o pipefail
# Samples whose binary, when invoked with no arguments, runs a
# self-contained test and exits 0 on success / non-zero on
# failure. These are built AND executed.
declare -a RUN_TARGETS=(
rslc_test
float_to_s32_test
compat_fnmatch_test
snprintf
unbase64_test
crc32_test
rzstd_raw_block_test
rbzip2_test
rinflate_window_test
rdeflate_hash_test
emscripten_mmap_test
archive_zip_test
archive_zstd_test
archive_zst_test
archive_bz2_test
archive_rar_test
archive_stream_test
config_file_test
path_resolve_realpath_test
fill_pathname_test
path_mkdir_test
rpng
rzip_chunk_size_test
rzip_matches_buf_test
rzip_codec_test
rzip_parallel_test
data_transfer_source_test
data_transfer_prefix_test
data_transfer_window_test
audio_transfer_window_test
# rvorbis' codebook size bound, and a real stream past it.
rvorbis_codebook_test
rjpeg_test
rh264_lossless_test
rh264_lossless_test_nothreads
rh264_lossless_test_volatile
# The hardware path's sequencing against a checking backend.
rh264_hw_test
# rmp3's frame-at-a-time API against its pull API.
rmp3_frame_test
# raac_last_size over units followed by more.
raac_size_test
rh265_lossless_test
rh265_lossless_test_nothreads
rh265_lossless_test_volatile
rh265_malformed_test
rh265_weight_test
r7z_lzma_props_test
r7z_archive_detach_test
r7z_bcj2_test
rzip_archive_test
# The RAR reader on real archives (LZ and PPMd blocks,
# filters, Unicode names) and on mangled copies of them.
rrar_archive_test
vcdiff_test
net_ifinfo
vfs_read_overflow_test
vfs_mapped_ptr_test
vfs_prefetch_test
vfs_readahead_test
vfs_view_test
vfs_prefetch_retarget_test
vfs_prefetch_view_test
vfs_large_file_test
vfs_seek_contract_test
vfs_bulk_read_test
vfs_v5_metadata_test
vfs_hybrid_test
filestream_rbuf_fault_test
vfs_rename_replace_test
cdrom_cuesheet_overflow_test
rdvd_h262_test
rdvd_lpcm_test
rdvd_ac3_test
rdvd_packet_test
rdvd_highlight_test
rdvd_spu_test
rdvd_ifo_test
rdvd_source_test
rdvd_vfs_test
rdvd_vfs_real_test
cdfs_dir_record_test
cdfs_pure_api_test
cdfs_io_error_test
chd_meta_overflow_test
chd_stream_threads_test
chd_stream_ahead_test
rvz_test
filestream_printf_test
strlcpy_append_test
http_parse_test
rjson_test
rm3u_test
rmp4_fragment_test
rmp4_language_test
rav1_test
rav1_picture_test
rav1_reference_test
rav1_motion_test
rav1_grain_test
rav1_reference_state_test
rav1_hw_session_test
rav1_hw_stream_test
rav1_mp4_stream_test
rav1_webm_stream_test
rav1_hw_backend_test
rav1_frame_test
rav1_tile_info_test
rav1_tiles_test
rav1_sequence_test
rav1_container_test
subtitle_srt_test
subtitle_srt_fault_test
rtga_test
rbmp_test
rbmp_encode_test
# Every 16-bit input through each pixconv converter, with
# SSE2, with MMX only, and with no SIMD at all.
pixconv_test
pixconv_test_nosimd
pixconv_test_scalar
rpng_chunk_overflow_test
rpng_roundtrip_test
rpng_decode_corpus_test
rpng_apng_bounds_test
# Benchmark, not a pass/fail test, but it runs its default
# 4K comparison and exits 0, so under this job's sanitizer
# build it doubles as end-to-end ASan/UBSan smoke coverage
# of the old and new screenshot encode/write paths.
# Timings in its output are informational only.
screenshot_io_bench
word_wrap_overflow_test
task_queue_title_error_test
task_queue_snapshot_test
task_queue_init_failure_test
# Stops the worker with a bound when a detachable
# handler is stuck; waits ~1.5 s by design.
task_queue_detach_test
task_queue_main_thread_test
task_queue_budget_test
# A switch to the threaded queue takes over the tasks
# already queued and counts them.
task_queue_switch_test
# A parked handler runs again on a wake, a cancel or its
# horizon, and not before.
task_queue_park_test
# Parallel tasks on the extra workers, beside the one
# worker plain tasks keep to.
task_queue_parallel_test
# The worker below the main thread as Horizon schedules
# them; the lanes need SCHED_FIFO and skip without root.
task_queue_fifo_prio_test
image_blit_bands_test
image_loader_test
tpool_wait_test
# Four CPU-burning jobs on a pool of four must overlap;
# on a one-core runner it notes that and still passes.
tpool_parallel_test
# The pool's lock-free handoff with tpool.c's test yields
# on: no job posted is left unrun or run twice.
tpool_handoff_test
data_transfer_pool_scrub_test
retro_atomic_test
retro_atomic_extern_c_linkage_test
retro_atomic_extern_c_linkage_test_cxx
# The C++11 backend's value macros given enumerators, and
# parsed against a pre-P0558R1 <atomic> that deduces from
# the value too, as libstdc++ 8 does.
retro_atomic_cxx11_old_stl_test
# The PS2 EE backend, built off-target against a stub
# <kernel.h>: the PS2 CI lane compiles it, this runs it.
retro_atomic_ps2_backend_test
retro_spsc_test
mpsc_stack_test
# A frame handoff where neither side waits: no read sees a
# buffer being filled, the reader ends on the last frame.
retro_triple_buffer_test
retro_waitable_spsc_test
retro_eventcount_test
# retro_sleep_until_us must never return early against the
# microsecond clock; the late bound is loose for a loaded
# runner.
retro_sleep_until_test
# localtime() from four threads, before rtime_init() as
# well as after: each must get its own fields back.
rtime_localtime_test
# Runs its natural tier only here; procbarrier-tiers.yml
# forces every tier on Linux, Windows and macOS runners.
retro_procbarrier_test
retro_asym_eventcount_test
sthread_ex_test
fast_cores_test
memshm_test
fastjmp_test
rdds_block_test
faulthandler_test
memshm_area_test
msg_fifo_queue_test
audio_mixer_loop_test
# Voice volume and gain set and read from two threads while
# a third mixes: every read and every block one of the
# values set.
audio_mixer_voice_threads_test
audio_mixer_rate_guard_test
audio_mixer_pause_test
# The AC-3 / E-AC-3 frame layer against ffmpeg's streams:
# ffmpeg is in the package list above for it.
rac3_frame_test
rac3_tables_test
rac3_decode_test
rac3_encode_test
# IEC 61937 bursts against ffmpeg's spdif muxer and demuxer.
iec61937_test
# The AC-3 arm of audio_transfer against ffmpeg's decode.
audio_transfer_ac3_test
# The FLAC arm, sample-exact in every input mode, a grown
# demuxed packet set and a raised WebM wall both resuming.
audio_transfer_flac_test
# The HDR video blits: PQ and HLG to linear scRGB against
# double-precision references, and HLG tone-mapped as HDR.
image_hdr_blit_test
# PSP movies demultiplexed from program streams built for it,
# fed whole, a byte at a time and in odd chunks.
rpsmf_test
# ATRAC3 against a reference decoder's output for crafted
# stereo and joint stereo streams, then bad frames and
# garbage.
ratrac3_test
# ATRAC3plus against a reference decoder's output for a
# stereo stream (tones, gain points, swap and negate masks)
# and a mono one, then bad frames and garbage.
ratrac3p_test
# Animation playback against a scripted stream: order,
# pacing, loops, catch-up, the worker's two jobs.
video_player_test
# VP9 reference-slot sizing, the hardware session's
# metadata, retries and frame leases, and WebM dispatch
# into it.
rmp4_vp9_config_test
rvp9_config_test
rvp9_reference_test
rvp9_hw_session_test
rwebm_language_test
rwebm_hw_stream_test
# The NGX and NVAPI loaders against a stand-in NGX core
# library on LD_LIBRARY_PATH; the Windows discovery runs
# in the mingw job below.
nvidia_loader_test
# The FidelityFX API and XeSS loaders against stand-in
# libraries planted in a directory of the test's, as the
# user's runtime directory is; the Windows file names run
# in the mingw job below.
ffx_loader_test
xess_loader_test
)
# Targets that are built but deliberately not executed, each
# for a reason that is not "we forgot". Anything built and
# listed in neither this nor RUN_TARGETS is an error below,
# because a silent "not in run allowlist" skip is
# indistinguishable from an omission - which is how seven
# self-contained regression tests came to be compiled every
# run and never executed.
declare -a NORUN_TARGETS=(
# CLI tools that print usage and exit non-zero without
# arguments; they have no self-test mode.
rzip
# Performs real HTTP requests against an external host, so
# running it would make this job depend on a third party
# being up.
http_test
)
# Per-binary run command (overrides ./<binary> if present).
# config_file_test's leak exemption is gone: the harness now
# frees everything it allocates and the parser's own include
# leak is fixed, so LeakSanitizer runs at full strictness and
# any surviving allocation is a real config_file regression.
declare -A RUN_ENV=(
# fastjmp_jmp is a longjmp that is not libc's, so ASan's
# fake-stack check cannot follow it; the same holds for
# libco. The jump is what the test is for.
[fastjmp_test]="ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=0"
# The filter must be the one that sees SIGSEGV: ASan installs
# its own, and with it the faults never reach the code under
# test. handle_segv=0 is how a fastmem core runs.
[faulthandler_test]="ASAN_OPTIONS=detect_leaks=1:handle_segv=0 UBSAN_OPTIONS=handle_segv=0"
[nvidia_loader_test]="LD_LIBRARY_PATH=."
)
# Samples that are build-only (either they require command-line
# arguments, open network sockets, need extra fixtures, or are
# interactive demos). They are compiled to catch build-time
# regressions but not executed.
declare -a BUILD_ONLY_DIRS=(
formats/xml
# A benchmark, not a test: its numbers only mean something
# on two dedicated physical cores, which a runner is not.
queues/retro_spsc_bench
# The same, and for the same reason: it reports park and
# commit counts under contention, which a shared runner
# cannot produce meaningfully. It also builds a second
# binary, the scond variant, and being build-only covers
# both without naming either.
rthreads/retro_eventcount_bench
# What a short job costs to hand to the pool: a figure for
# a machine with cores to spare.
rthreads/tpool_bench
)
# Samples that are currently broken at build time on a stock
# Ubuntu host and are therefore neither built nor run.
declare -a SKIP_DIRS=(
# A 3DS homebrew, built with devkitARM and run on the
# console or an emulator.
queues/ctr_task_queue_prio_test
)
is_in() {
local needle=$1; shift
local h
for h in "$@"; do [[ "$h" == "$needle" ]] && return 0; done
return 1
}
fails=0
builds=0
runs=0
# Collect all Makefile directories (one or two levels deep).
mapfile -t MKDIRS < <(find . -name Makefile -printf '%h\n' | sort)
printf '\n==> %d sample directories found\n' "${#MKDIRS[@]}"
for d in "${MKDIRS[@]}"; do printf ' %s\n' "${d#./}"; done
printf '\n'
for d in "${MKDIRS[@]}"; do
rel=${d#./}
printf '========================================\n'
printf '[%s] %s\n' "$(is_in "$rel" "${SKIP_DIRS[@]}" && echo skip || echo build)" "$rel"
printf '========================================\n'
if is_in "$rel" "${SKIP_DIRS[@]}"; then
printf '[skip] %s is on the skip list\n\n' "$rel"
continue
fi
# Build
if ! ( cd "$d" && make clean all $MAKE_ARGS ); then
printf '\n::error title=Build failed::%s failed to build\n' "$rel"
fails=$((fails+1))
continue
fi
builds=$((builds+1))
# Skip run for build-only dirs
if is_in "$rel" "${BUILD_ONLY_DIRS[@]}"; then
printf '[skip-run] %s (build-only list)\n\n' "$rel"
continue
fi
# Extract targets from Makefile. Handles:
# TARGET := foo
# TARGETS = a b c
# TARGET_TEST := foo_test (second target in same Makefile)
# TARGET_TEST2 := foo_test2 (third target, fourth, ...)
# EXE is empty on Linux.
mapfile -t targets < <(
grep -hE '^(TARGET|TARGETS|TARGET_TEST[0-9]*)[[:space:]]*[:?]?=' "$d/Makefile" \
| sed -E 's/^[^=]*=[[:space:]]*//' \
| sed 's/$(EXE)//g' \
| tr -s ' \t' '\n' \
| grep -v '^$' \
| sort -u
)
for t in "${targets[@]}"; do
if ! is_in "$t" "${RUN_TARGETS[@]}"; then
if is_in "$t" "${NORUN_TARGETS[@]}"; then
printf '[skip-run] %s/%s (deliberately not run)\n' "$rel" "$t"
continue
fi
printf '::error title=Target not classified::%s/%s is built but appears in neither RUN_TARGETS nor NORUN_TARGETS. Add it to one of them.\n' "$rel" "$t"
fails=$((fails+1))
continue
fi
bin="$d/$t"
if [[ ! -x "$bin" ]]; then
printf '::error title=Missing binary::%s was in the run allowlist but %s does not exist after build\n' "$t" "$bin"
fails=$((fails+1))
continue
fi
extra_env=${RUN_ENV[$t]:-}
printf '\n[run] %s\n' "$bin"
if ( cd "$d" && env $extra_env timeout 60 "./$t" ); then
printf '[pass] %s\n\n' "$t"
runs=$((runs+1))
else
rc=$?
printf '\n::error title=Test failed::%s exited with status %d\n' "$t" "$rc"
fails=$((fails+1))
fi
done
done
printf '========================================\n'
printf 'Summary\n'
printf '========================================\n'
printf ' Built: %d\n' "$builds"
printf ' Ran: %d\n' "$runs"
printf ' Failed: %d\n' "$fails"
if [[ $fails -gt 0 ]]; then
exit 1
fi
- name: Parse a generated DVD VOB with the native readers
shell: bash
working-directory: libretro-common/samples/formats/dvd
run: |
set -euo pipefail
fixture=$(mktemp --suffix=.vob)
trap 'rm -f "$fixture"' EXIT
ffmpeg -hide_banner -loglevel error -f lavfi -i color=size=720x576:rate=25 \
-f lavfi -i sine=sample_rate=48000 -t 0.4 \
-c:v mpeg2video -b:v 2000k -c:a ac3 -ac 2 -ar 48000 -f dvd -y "$fixture"
./rdvd_packet_test "$fixture"
./rdvd_vfs_real_test "$fixture"
reference=$(mktemp --suffix=.f32)
trap 'rm -f "$fixture" "$reference"' EXIT
for sample_format in s16 s32; do
ffmpeg -hide_banner -loglevel error -f lavfi -i color=size=720x576:rate=25 \
-f lavfi -i sine=sample_rate=48000 -t 0.4 \
-c:v mpeg2video -b:v 2000k -c:a pcm_dvd -sample_fmt "$sample_format" \
-ac 2 -ar 48000 -f dvd -y "$fixture"
ffmpeg -hide_banner -loglevel error -i "$fixture" -map 0:a:0 \
-c:a pcm_f32le -f f32le -y "$reference"
./rdvd_vfs_real_test "$fixture" "$reference"
done
- name: Reconstruct progressive H262 I pictures against a reference
shell: bash
working-directory: libretro-common/samples/formats/dvd
run: |
set -euo pipefail
fixture=$(mktemp --suffix=.m2v)
reference=$(mktemp --suffix=.yuv)
trap 'rm -f "$fixture" "$reference"' EXIT
ffmpeg -hide_banner -loglevel error -f lavfi -i testsrc2=size=160x128:rate=25 \
-frames:v 3 -c:v mpeg2video -g 1 -bf 0 -q:v 3 -f mpeg2video -y "$fixture"
ffmpeg -hide_banner -loglevel error -i "$fixture" -pix_fmt yuv420p -f rawvideo -y "$reference"
./rdvd_h262_test "$fixture" "$reference"
ffmpeg -hide_banner -loglevel error -f lavfi -i testsrc2=size=160x128:rate=25 \
-frames:v 3 -c:v mpeg1video -g 1 -bf 0 -q:v 3 -f mpeg1video -y "$fixture"
ffmpeg -hide_banner -loglevel error -i "$fixture" -pix_fmt yuv420p -f rawvideo -y "$reference"
./rdvd_h262_test "$fixture" "$reference"
- name: Compile-test retro_atomic.h from a C++11 TU
shell: bash
working-directory: libretro-common
run: |
# The C++11 backend in retro_atomic.h is a fresh code path that
# none of the C samples above exercise. Compile a tiny inline
# C++11 TU against the in-tree header to catch regressions like
# accidentally re-introducing an extern "C" wrapper around the
# std::atomic include, or breaking the __cplusplus / _MSVC_LANG
# gate. This step is build-and-run, single-threaded only -- the
# behavioural SPSC stress is already covered by the C test
# binary above on this same host, and the C++11 backend bottoms
# out through the same libstdc++ __atomic_* builtins.
#
# Two fixtures share the g++/clang++ x c++11/14/17 sweep:
#
# cxx_smoke.cpp : header included at C++ linkage,
# the "ordinary" C++ TU shape.
# cxx_smoke_extern_c.cpp : header included from inside an
# extern "C" { ... } wrap, the
# shape ui_qt.cpp uses under
# !CXX_BUILD. This is the
# regression case for the
# RETRO_BEGIN_DECLS_CXX shield in
# retro_atomic.h's C++11 backend.
# The standalone build of the
# sample at samples/atomic/
# retro_atomic_extern_c_linkage/
# covers the same case under the
# default toolchain; this matrix
# extends coverage to clang++ and
# the higher language standards.
set -u
set -o pipefail
tmpdir=$(mktemp -d)
cat > "$tmpdir/cxx_smoke.cpp" <<'EOF'
#include <cstdio>
#include <cstddef>
#include <retro_atomic.h>
#if !defined(HAVE_RETRO_ATOMIC) || !defined(RETRO_ATOMIC_LOCK_FREE)
# error "retro_atomic.h: capability flags not set on a C++11 host"
#endif
int main(void) {
retro_atomic_int_t ai; retro_atomic_int_init(&ai, 0);
retro_atomic_size_t as; retro_atomic_size_init(&as, 0);
retro_atomic_store_release_int(&ai, 42);
retro_atomic_store_release_size(&as, (std::size_t)42);
int li = retro_atomic_load_acquire_int(&ai);
int ls = (int)retro_atomic_load_acquire_size(&as);
int pi = retro_atomic_fetch_add_int(&ai, 1);
int ps = (int)retro_atomic_fetch_add_size(&as, 1);
retro_atomic_inc_int(&ai);
retro_atomic_dec_size(&as);
int qi = retro_atomic_load_acquire_int(&ai);
int qs = (int)retro_atomic_load_acquire_size(&as);
std::printf("backend: %s\n", RETRO_ATOMIC_BACKEND_NAME);
bool ok = (li == 42) && (ls == 42)
&& (pi == 42) && (ps == 42)
&& (qi == 44) && (qs == 42);
std::puts(ok ? "ALL OK" : "FAIL");
return ok ? 0 : 1;
}
EOF
# Regression fixture: include retro_atomic.h from inside an
# extern "C" wrap. Faithful mirror of ui_qt.cpp's pattern.
# Pre-fix retro_atomic.h would emit ~80 "template with C
# linkage" errors here; the RETRO_BEGIN_DECLS_CXX shield
# around <atomic> makes this compile cleanly.
cat > "$tmpdir/cxx_smoke_extern_c.cpp" <<'EOF'
#include <cstdio>
#include <cstddef>
extern "C" {
#include <retro_atomic.h>
}
int main(void) {
retro_atomic_int_t ai; retro_atomic_int_init(&ai, 0);
retro_atomic_store_release_int(&ai, 7);
int v = retro_atomic_load_acquire_int(&ai);
std::printf("backend: %s\n", RETRO_ATOMIC_BACKEND_NAME);
std::puts(v == 7 ? "ALL OK" : "FAIL");
return v == 7 ? 0 : 1;
}
EOF
for cxx in g++ clang++; do
for std in c++11 c++14 c++17; do
for fixture in cxx_smoke cxx_smoke_extern_c; do
echo "==> compile-test with $cxx -std=$std ($fixture)"
$cxx -std=$std -Wall -Wextra -pedantic -O2 \
-I include \
"$tmpdir/$fixture.cpp" \
-o "$tmpdir/$fixture" \
|| { echo "::error title=C++ compile failed::$cxx -std=$std $fixture"; exit 1; }
"$tmpdir/$fixture" \
|| { echo "::error title=C++ smoke failed::$cxx -std=$std $fixture"; exit 1; }
done
done
done
rm -rf "$tmpdir"
- name: Run retro_atomic_test under Clang + ThreadSanitizer
shell: bash
working-directory: libretro-common/samples/atomic/retro_atomic_test
run: |
# The native samples job above runs with GCC and ASan/UBSan.
# Clang is the toolchain on every Apple platform, Android NDK
# (since r18), Emscripten, and PS4-ORBIS, so a Clang lane is
# not optional coverage. ThreadSanitizer is the strict
# validator for this test in particular: it instruments every
# atomic load and store and would flag a missing acquire /
# release barrier as a race in the 1M-iteration SPSC stress
# (a class of bug that x86 TSO would otherwise hide on the
# native runner).
set -u
set -o pipefail
make clean
CC=clang make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 ./retro_atomic_test
- name: Run video_player_test under Clang + ThreadSanitizer, and threadless
shell: bash
working-directory: libretro-common/samples/formats/video_player
run: |
# The decode worker hands two jobs back and forth with the
# polling thread through atomics alone; the threadless shape
# decodes every frame on the polling thread.
set -u
set -o pipefail
make clean
CC=clang make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 ./video_player_test
make clean
make -j$(nproc) all THREADS=0 SANITIZER=address,undefined
./video_player_test
for threads in 1 0; do
make clean
make -j$(nproc) all THREADS=$threads FAULT_ALLOC=1 SANITIZER=address,undefined || exit $?
VP_TEST_RECOVERY_ONLY=1 ./video_player_test || exit $?
done
- name: Run rtime_localtime_test under Clang + ThreadSanitizer
shell: bash
working-directory: libretro-common/samples/time/rtime_localtime_test
run: |
# rtime_localtime() guards the one struct localtime() fills;
# two callers let in together race on it, which TSan reports
# even on runs where neither hands back the other's fields.
set -u
set -o pipefail
make clean
CC=clang make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 ./rtime_localtime_test
- name: Run retro_triple_buffer_test under Clang + ThreadSanitizer
shell: bash
working-directory: libretro-common/samples/queues/retro_triple_buffer_test
run: |
# The HUB75 driver's canvases and the ffmpeg camera's frames:
# a buffer handed over with one exchange and picked up with
# another, so a missing barrier is a race only TSan reports.
set -u
set -o pipefail
make clean
CC=clang make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 ./retro_triple_buffer_test
- name: Run audio_mixer_voice_threads_test under Clang + ThreadSanitizer
shell: bash
working-directory: libretro-common/samples/audio/audio_mixer_voice_threads
run: |
# A voice's volume and gain are atomics the caller sets and
# reads with no lock while the audio thread mixes the voice;
# TSan reports any field the three threads share unordered.
set -u
set -o pipefail
make clean
CC=clang make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 ./audio_mixer_voice_threads_test
- name: Run mpsc_stack_test under Clang + ThreadSanitizer
shell: bash
working-directory: libretro-common/samples/queues/mpsc_stack_test
run: |
# mpsc_stack.h is the lock-free many-producer single-consumer
# stack the runloop's deferred messages ride. Its contract is
# the push's compare-exchange publishing the node against the
# drain's exchange; a weakened push loses nodes under
# contention, observable as a drained-count mismatch AND as a
# TSan-reported race. The default ASan/UBSan pass above
# catches neither reliably - the race needs TSan's scheduling
# to manifest - so this lane is the one that holds the
# contract.
set -u
set -o pipefail
make clean
CC=clang make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 ./mpsc_stack_test
- name: Run retro_spsc_test on the POSIX shm mirror
shell: bash
working-directory: libretro-common/samples/queues/retro_spsc_test
run: |
# Linux mirrors a ring through memfd; macOS and the BSDs take
# the POSIX shm path, which this builds here so it runs on
# every push, under ASan.
set -eu
make clean
make -j$(nproc) all CC="gcc -DRETRO_SPSC_MIRROR_SHM" SANITIZER=address,undefined
./retro_spsc_test
- name: Run retro_spsc_test under Clang + ThreadSanitizer
shell: bash
working-directory: libretro-common/samples/queues/retro_spsc_test
run: |
# retro_spsc.c is a lock-free SPSC byte queue built on
# retro_atomic.h. Its correctness contract is acquire-load
# / release-store on the head and tail cursors, with the
# buffer reads/writes between them ordered by those barriers.
# Missing or weakened barriers produce torn data on the
# consumer side, observable as content mismatches in the
# stress harness AND as TSan-reported races. The default
# ASan/UBSan pass above catches the content mismatches but
# not the races; this lane catches both.
#
# halt_on_error=1 makes TSan exit non-zero on the first race
# rather than continuing -- which is what we want for CI:
# any race here means the SPSC contract is broken.
set -u
set -o pipefail
make clean
CC=clang make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 ./retro_spsc_test
- name: Run retro_eventcount_test under Clang + ThreadSanitizer
shell: bash
working-directory: libretro-common/samples/rthreads/retro_eventcount_test
run: |
# retro_eventcount parks a consumer without putting a lock on
# the producer's fast path. What it can get wrong is a lost
# wakeup: a notify that reads zero waiters while a consumer
# that has not yet seen the new epoch is on its way into a
# park. The guard is a seq_cst fence on both sides, and a
# build with either fence weakened still passes on x86 TSO,
# so the value checks alone prove nothing -- TSan is what
# sees the missing barrier.
#
# 'make check' runs the address-wait backend and then the
# condition-variable one, which is the backend macOS, the
# BSDs and the console ports get. Both matter; only the
# first is native here.
#
# The harness carries its own watchdog, so a genuine lost
# wakeup fails the job in a minute instead of hanging it.
set -u
set -o pipefail
make clean
CC=clang make check SANITIZER=thread
- name: Run rzip_parallel_test under Clang + ThreadSanitizer
shell: bash
working-directory: libretro-common/samples/streams/rzip
run: |
# rzip_stream.c's chunk-writer pool hands each slot between the
# writer and one worker on an acquire/release status word and
# parks both sides on retro_eventcount, with no lock anywhere.
# The value checks prove in-order emission; a weakened store
# or a lost wakeup still passes them on x86 TSO, and only TSan
# sees the missing barrier. The test drives the pool at every
# width and through two hundred short lifetimes so the
# start / park / shutdown edges are exercised often.
set -u
set -o pipefail
make clean
CC=clang make rzip_parallel_test SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 ./rzip_parallel_test
- name: Run crc32_test under Clang + ThreadSanitizer
shell: bash
working-directory: libretro-common/samples/encodings/crc32
run: |
# encoding_crc32() caches x86 feature detection on first use.
# The failure that matters is publishing that result without
# release/acquire ordering, which is invisible to the value
# checks -- every thread still computes the right number on
# x86 TSO -- and only shows up as a race. The test holds its
# threads on a gate so they all make their first dispatching
# call at once; see the comment above crc_first_touch_worker
# for why that section makes exactly one call per thread.
set -u
set -o pipefail
make clean
CC=clang make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 ./crc32_test
- name: Prefix refill boundaries and sliding-window I/O
run: |
make -B -C libretro-common/samples/formats/data_transfer data_transfer_prefix_test data_transfer_window_test COUNT_IO=1 SAMPLE_OBJDIR=obj-inert-strict CFLAGS='-Wall -pedantic -std=gnu99 -O2 -g1 -DDT_STRICT -DDT_COUNT_IO -DHAVE_MMAP -D_GNU_SOURCE -I../../../include'
cd libretro-common/samples/formats/data_transfer
INERT_ONLY=1 ./data_transfer_prefix_test
./data_transfer_window_test
- name: ccache statistics
if: ${{ !cancelled() }}
continue-on-error: true
run: ccache -sv
- 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.
if: success() && github.event_name == 'push' && github.ref == 'refs/heads/master'
uses: actions/cache/save@v4
with:
path: ~/.ccache
key: ccache-${{ github.workflow }}-${{ github.job }}-${{ github.sha }}
# memmap.h picks between libretro-common's mman shim and the
# platform's own <sys/mman.h>, and Emscripten is the target where
# that choice is load-bearing: its libc declares mmap with a 64-bit
# off_t while the shim declares a 32-bit size_t, so a consumer that
# sees both declarations fails to compile, and reserve/commit has no
# meaning behind it at all - an anonymous mmap there allocates and
# zeroes the whole length, and mprotect and madvise are no-ops.
#
# None of that is reachable from the native sweep above, which
# compiles the sample with every Emscripten branch preprocessed
# away. This lane is where the declarations are held to libc's.
emscripten-memmap:
name: Emscripten memmap ABI
runs-on: ubuntu-latest
timeout-minutes: 20
env:
APT_PKGS: ccache
# Pinned rather than floating: the job exists to hold a libc ABI,
# so the toolchain it is held against changes deliberately.
EM_VERSION: 5.0.1
EM_CACHE_FOLDER: emsdk-cache
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Apt cache key
id: apt
run: sh .github/workflows/scripts/apt-cache-key.sh "$APT_PKGS"
- name: Restore apt cache
uses: actions/cache/restore@v4
with:
path: ~/.apt-cache
key: ${{ steps.apt.outputs.key }}
restore-keys: ${{ steps.apt.outputs.prefix }}
- name: Install dependencies
timeout-minutes: 10
run: sh .github/workflows/scripts/apt-install.sh $APT_PKGS
- name: Save apt cache
if: env.APT_CACHE_SAVE == '1'
uses: actions/cache/save@v4
with:
path: ~/.apt-cache
key: ${{ steps.apt.outputs.key }}
- name: Link with lld
run: sh .github/workflows/scripts/use-lld.sh
- name: Restore ccache
uses: actions/cache/restore@v4
with:
path: ~/.ccache
key: ccache-${{ github.workflow }}-${{ github.job }}-${{ github.sha }}
restore-keys: |
ccache-${{ github.workflow }}-${{ github.job }}-
- name: Enable ccache
run: |
set -eu
export CCACHE_DIR="$HOME/.ccache"
echo "CCACHE_DIR=$CCACHE_DIR" >> "$GITHUB_ENV"
echo "/usr/lib/ccache" >> "$GITHUB_PATH"
ccache -M 250M
ccache -z
- name: Cache emsdk
uses: actions/cache@v4
with:
path: ${{ env.EM_CACHE_FOLDER }}
key: ${{ env.EM_VERSION }}-${{ runner.os }}
- name: Set up Emscripten
uses: emscripten-core/setup-emsdk@v15
with:
version: ${{ env.EM_VERSION }}
actions-cache-folder: ${{ env.EM_CACHE_FOLDER }}
- name: Run the memmap sample under node
shell: bash
working-directory: libretro-common/samples/memmap/emscripten_mmap
run: |
set -eu
set -o pipefail
# Fails to compile without the libc declaration; the mmap
# assignment through an off_t-typed function pointer is the
# discriminator. The mprotect / reserve / msync checks that
# follow it cover the calls the same header choice hands to
# libc.
make clean
make check-wasm
- name: Compile vfs_implementation.c with file mappings enabled
shell: bash
working-directory: libretro-common
run: |
set -eu
set -o pipefail
# The in-tree translation unit that includes memmap.h and
# <sys/mman.h> together. Makefile.emscripten does not set
# HAVE_MMAP today, so nothing else compiles this shape for
# wasm - which is exactly why the two declarations can drift
# apart unnoticed until a downstream consumer hits it.
emcc -std=c89 -D_GNU_SOURCE -DHAVE_MMAP \
-Wall -Wextra -Werror \
-I include -c vfs/vfs_implementation.c \
-o "$RUNNER_TEMP/vfs_implementation.o"
- name: ccache statistics
if: ${{ !cancelled() }}
continue-on-error: true
run: ccache -sv
- 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.
if: success() && github.event_name == 'push' && github.ref == 'refs/heads/master'
uses: actions/cache/save@v4
with:
path: ~/.ccache
key: ccache-${{ github.workflow }}-${{ github.job }}-${{ github.sha }}
# Cross-architecture validation lane for retro_atomic_test.
#
# The samples job above runs on x86_64, which is a strongly-ordered
# (TSO) architecture. retro_atomic.h's contract is that acquire-load
# / release-store / acq_rel-RMW emit real barriers on weakly-ordered
# SMP targets (ARM, AArch64, PowerPC, MIPS). An x86_64 host run
# cannot exercise that property, because TSO masks reordering bugs
# at the hardware level even when the macros emit no barriers at all.
#
# This job cross-compiles retro_atomic_test for AArch64 and ARMv7 and
# runs the binary under qemu-user-static. qemu-user emulates the
# weak memory model faithfully enough to expose missing-barrier bugs
# in the SPSC stress test, and is cheap enough to run on every push.
#
# We deliberately do NOT run the full samples sweep here -- the rest
# of the samples don't have architecture-dependent codegen that
# warrants the extra CI time. retro_atomic_test is the one that
# benefits from cross-arch coverage.
#
# Real ARM hardware still beats qemu (see e.g. PostgreSQL's 2025
# Win11/ARM64 atomic ordering bug, found only on real silicon),
# but qemu catches most categorical errors and is much cheaper than
# provisioning ARM runners.
config-file-fuzz-smoke:
name: config_file differential fuzz (smoke)
runs-on: ubuntu-latest
env:
APT_PKGS: clang ccache
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Apt cache key
id: apt
run: sh .github/workflows/scripts/apt-cache-key.sh "$APT_PKGS"
- name: Restore apt cache
uses: actions/cache/restore@v4
with:
path: ~/.apt-cache
key: ${{ steps.apt.outputs.key }}
restore-keys: ${{ steps.apt.outputs.prefix }}
- name: Install dependencies
timeout-minutes: 20
run: sh .github/workflows/scripts/apt-install.sh $APT_PKGS
- name: Save apt cache
if: env.APT_CACHE_SAVE == '1'
uses: actions/cache/save@v4
with:
path: ~/.apt-cache
key: ${{ steps.apt.outputs.key }}
- name: Link with lld
run: sh .github/workflows/scripts/use-lld.sh
- name: Restore ccache
uses: actions/cache/restore@v4
with:
path: ~/.ccache
key: ccache-${{ github.workflow }}-${{ github.job }}-${{ github.sha }}
restore-keys: |
ccache-${{ github.workflow }}-${{ github.job }}-
- name: Enable ccache
run: |
set -eu
export CCACHE_DIR="$HOME/.ccache"
echo "CCACHE_DIR=$CCACHE_DIR" >> "$GITHUB_ENV"
echo "/usr/lib/ccache" >> "$GITHUB_PATH"
ccache -M 250M
ccache -z
- name: Build fuzz target
shell: bash
working-directory: libretro-common/samples/file/config_file
run: |
set -eu
LC=../../..
clang -O1 -g1 -fsanitize=fuzzer,address,undefined \
-fno-sanitize-recover=all \
-I$LC/include -o config_file_fuzz config_file_fuzz.c \
$LC/file/config_file.c \
$LC/string/rstrtod.c \
$LC/file/file_path.c \
$LC/compat/compat_strl.c \
$LC/compat/compat_strcasestr.c \
$LC/compat/compat_posix_string.c \
$LC/encodings/encoding_utf.c \
$LC/lists/string_list.c \
$LC/time/rtime.c
- name: Run fuzz smoke
shell: bash
working-directory: libretro-common/samples/file/config_file
run: |
set -eu
# Seed corpus: representative config shapes plus the byte
# patterns the target's invariants care about (embedded NUL,
# comment-in-literal, directives, duplicate keys, CRLF).
# Generated here rather than committed so no binary files
# live in the tree.
mkdir -p corpus
printf 'video_driver = "vulkan"\nvideo_fullscreen = "true"\naudio_rate = "48000"\n' > corpus/basic
printf '# comment\nkey = "has # inside"\ndup = "a"\ndup = "b"\n spaced = bare\r\nempty = ""\n' > corpus/edge
printf '#include "sub.cfg"\n#reference "other.cfg"\nafter = "1"\n' > corpus/directives
python3 -c "import sys; sys.stdout.buffer.write(b'a = \"1\"\nb\x00c = \"2\"\nd = \"3\"\n')" > corpus/embedded_nul
# 60 seconds of coverage-guided differential fuzzing:
# every input is parsed whole and again through the
# streaming push API in input-derived packet splits, then
# entry-list equality, hash-map coherence and tail
# validity are enforced (see config_file_fuzz.c). Any
# divergence or sanitizer report fails the job.
./config_file_fuzz -max_len=512 -max_total_time=60 \
-print_final_stats=1 corpus/
- name: ccache statistics
if: ${{ !cancelled() }}
continue-on-error: true
run: ccache -sv
- 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.
if: success() && github.event_name == 'push' && github.ref == 'refs/heads/master'
uses: actions/cache/save@v4
with:
path: ~/.ccache
key: ccache-${{ github.workflow }}-${{ github.job }}-${{ github.sha }}
task-queue-mingw:
name: task_queue samples under mingw-w64 + Wine
runs-on: ubuntu-latest
env:
APT_PKGS: build-essential gcc-mingw-w64-x86-64 wine wine64
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Apt cache key
id: apt
run: sh .github/workflows/scripts/apt-cache-key.sh "$APT_PKGS"
- name: Restore apt cache
uses: actions/cache/restore@v4
with:
path: ~/.apt-cache
key: ${{ steps.apt.outputs.key }}
restore-keys: ${{ steps.apt.outputs.prefix }}
- name: Install dependencies
timeout-minutes: 20
run: sh .github/workflows/scripts/apt-install.sh $APT_PKGS
- name: Save apt cache
if: env.APT_CACHE_SAVE == '1'
uses: actions/cache/save@v4
with:
path: ~/.apt-cache
key: ${{ steps.apt.outputs.key }}
- name: Link with lld
run: sh .github/workflows/scripts/use-lld.sh
- name: Build and run the task_queue samples under Wine
shell: bash
working-directory: libretro-common/samples/queues
env:
WINEDEBUG: -all
run: |
set -u
set -o pipefail
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
# The samples job above runs these on Linux only. On Win32
# retro_sleep() is a function in time/rtime.c rather than a
# macro, so a Makefile that links on Linux can still fail to
# link for Windows; the threaded queue's Win32 paths in
# rthreads.c are not run anywhere else either.
fails=0
for d in task_queue_*; do
t=$(sed -nE 's/^TARGET[[:space:]]*:?=[[:space:]]*//p' "$d/Makefile")
printf '\n[build] %s\n' "$d"
if ! ( cd "$d" && make clean && make CC=x86_64-w64-mingw32-gcc all ); then
printf '::error title=mingw build failed::%s does not build for Windows\n' "$d"
fails=$((fails+1))
continue
fi
printf '[run] %s.exe\n' "$t"
if ( cd "$d" && timeout 120 wine "./$t.exe" ); then
printf '[pass] %s\n' "$t"
else
rc=$?
printf '::error title=Test failed under Wine::%s exited with status %d\n' "$t" "$rc"
fails=$((fails+1))
fi
done
[[ $fails -eq 0 ]]
- name: Build and run the NVIDIA loaders under Wine
shell: bash
working-directory: libretro-common/samples/gfx/nvidia
env:
WINEDEBUG: -all
run: |
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
# The Windows half of the loaders: _nvngx.dll beside the
# program and nvapi64.dll by LoadLibrary, the Direct3D 12
# table resolved, the stand-ins built with mingw.
make clean
make CC=x86_64-w64-mingw32-gcc all
timeout 120 wine ./nvidia_loader_test.exe
make clean
- name: Build and run the FidelityFX and XeSS loaders under Wine
shell: bash
working-directory: libretro-common/samples/gfx
env:
WINEDEBUG: -all
run: |
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
# The Windows half of the loaders: the SDK's .dll names by
# LoadLibraryEx from the directory, the stand-ins built with
# mingw.
for d in amd intel; do
t=$(sed -nE 's/^TARGET[[:space:]]*:?=[[:space:]]*//p' "$d/Makefile")
( cd "$d" && make clean && make CC=x86_64-w64-mingw32-gcc all && timeout 120 wine "./$t.exe" && make clean )
done
- name: Build and run the mapped read-ahead test under Wine
shell: bash
working-directory: libretro-common/samples/file/vfs
env:
WINEDEBUG: -all
run: |
set -eu
set -o pipefail
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
make clean MMAP=0
make CC=x86_64-w64-mingw32-gcc MMAP=0 vfs_readahead_test
timeout 120 wine ./vfs_readahead_test.exe
RETRO_VFS_PREFETCH=touch timeout 120 wine ./vfs_readahead_test.exe
RETRO_VFS_PREFETCH=none timeout 120 wine ./vfs_readahead_test.exe
make clean MMAP=0
- name: Build and run native DVD packets under Wine
shell: bash
working-directory: libretro-common/samples/formats/dvd
env:
WINEDEBUG: -all
run: |
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
make CC=x86_64-w64-mingw32-gcc EXE=.exe all
timeout 120 wine ./rdvd_h262_test.exe
timeout 120 wine ./rdvd_lpcm_test.exe
timeout 120 wine ./rdvd_ac3_test.exe
timeout 120 wine ./rdvd_packet_test.exe
timeout 120 wine ./rdvd_highlight_test.exe
timeout 120 wine ./rdvd_spu_test.exe
timeout 120 wine ./rdvd_ifo_test.exe
timeout 120 wine ./rdvd_source_test.exe
timeout 120 wine ./rdvd_vfs_test.exe
timeout 120 wine ./rdvd_vfs_real_test.exe
retro-atomic-cross:
name: Cross-arch retro_atomic_test (${{ matrix.arch }})
runs-on: ubuntu-latest
env:
APT_PKGS: build-essential ${{ matrix.apt_pkgs }} qemu-user-static ccache
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
include:
- arch: aarch64
cc: aarch64-linux-gnu-gcc
apt_pkgs: gcc-aarch64-linux-gnu
qemu: qemu-aarch64-static
sysroot: /usr/aarch64-linux-gnu
- arch: armv7
cc: arm-linux-gnueabihf-gcc
apt_pkgs: gcc-arm-linux-gnueabihf
qemu: qemu-arm-static
sysroot: /usr/arm-linux-gnueabihf
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Apt cache key
id: apt
run: sh .github/workflows/scripts/apt-cache-key.sh "$APT_PKGS"
- name: Restore apt cache
uses: actions/cache/restore@v4
with:
path: ~/.apt-cache
key: ${{ steps.apt.outputs.key }}
restore-keys: ${{ steps.apt.outputs.prefix }}
- name: Install dependencies
timeout-minutes: 20
run: sh .github/workflows/scripts/apt-install.sh $APT_PKGS
- name: Save apt cache
if: env.APT_CACHE_SAVE == '1'
uses: actions/cache/save@v4
with:
path: ~/.apt-cache
key: ${{ steps.apt.outputs.key }}
- name: Link with lld
run: sh .github/workflows/scripts/use-lld.sh
- name: Restore ccache
uses: actions/cache/restore@v4
with:
path: ~/.ccache
key: ccache-${{ github.workflow }}-${{ github.job }}-${{ matrix.arch }}-${{ github.sha }}
restore-keys: |
ccache-${{ github.workflow }}-${{ github.job }}-${{ matrix.arch }}-
- name: Enable ccache
run: |
set -eu
export CCACHE_DIR="$HOME/.ccache"
echo "CCACHE_DIR=$CCACHE_DIR" >> "$GITHUB_ENV"
echo "/usr/lib/ccache" >> "$GITHUB_PATH"
ccache -M 250M
ccache -z
- name: Build retro_atomic_test for ${{ matrix.arch }}
working-directory: libretro-common/samples/atomic/retro_atomic_test
run: |
set -u
set -o pipefail
make clean
CC=${{ matrix.cc }} make all
- name: Run retro_atomic_test under qemu-user
working-directory: libretro-common/samples/atomic/retro_atomic_test
run: |
set -u
set -o pipefail
${{ matrix.qemu }} -L ${{ matrix.sysroot }} ./retro_atomic_test
- name: Inspect emitted atomic instructions
working-directory: libretro-common/samples/atomic/retro_atomic_test
run: |
set -u
set -o pipefail
# Spot-check the codegen. If retro_atomic.h were silently
# falling through to a no-barrier backend on this arch, the
# asm would be conspicuously missing acquire/release
# instructions. This is a cheap sanity check on top of the
# behavioural SPSC test above.
${{ matrix.cc }} -O2 -S \
-I../../../include -DHAVE_THREADS \
retro_atomic_test.c -o /tmp/retro_atomic_test.s
echo
echo '== Unique barrier-emitting mnemonics =='
case "${{ matrix.arch }}" in
aarch64)
# Expect: ldar, stlr, and __aarch64_ldadd*_acq_rel libcalls
# (or inline ldaddal LSE on +lse builds).
pattern='\b(ldar|stlr|ldax|stlx|dmb|ldadd[a-z0-9_]*|swp[a-z0-9_]*|__aarch64_(ldadd|swp)[a-z0-9_]*acq_rel)\b'
;;
armv7)
# Expect: dmb (data memory barrier) and ldrex/strex pairs.
pattern='\b(dmb|ldrex|strex|ldrexb|strexb|ldrexh|strexh)\b'
;;
esac
mnemonics=$(grep -oE "$pattern" /tmp/retro_atomic_test.s | sort -u)
echo "$mnemonics"
if [[ -z "$mnemonics" ]]; then
echo
echo '::error title=No barrier instructions emitted::retro_atomic_test.s contains no acquire/release/barrier mnemonics for ${{ matrix.arch }}; retro_atomic.h may have fallen through to a no-barrier backend.'
exit 1
fi
- name: ccache statistics
if: ${{ !cancelled() }}
continue-on-error: true
run: ccache -sv
- 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.
if: success() && github.event_name == 'push' && github.ref == 'refs/heads/master'
uses: actions/cache/save@v4
with:
path: ~/.ccache
key: ccache-${{ github.workflow }}-${{ github.job }}-${{ matrix.arch }}-${{ github.sha }}
# encoding_crc32() picks a different implementation per target: the
# ARMv8 CRC32 instructions when the compiler advertises +crc, a
# PCLMULQDQ fold on x86, and a slicing-by-8 table everywhere else.
# The table path reads the input as little-endian 32-bit words and
# byte-swaps under MSB_FIRST, so a big-endian target is the only
# place a missing or wrong swap is visible -- on x86 the swap
# compiles away and every test still passes. powerpc is carried
# here for exactly that reason, and is the only big-endian lane in
# the sample suite.
#
# aarch64 appears twice on purpose. Stock aarch64 gcc does not
# define __ARM_FEATURE_CRC32, so a default build exercises the table
# path; only Makefile.libnx passes +crc today, and the second entry
# is what covers the intrinsic path at all.
#
# qemu-user cannot run the sanitizer runtimes, so these lanes rely on
# the test's own bitwise reference and its guard-page section, which
# catches an overread as a SIGSEGV rather than a redzone hit.
crc32-cross:
name: Cross-arch crc32_test (${{ matrix.name }})
runs-on: ubuntu-latest
env:
APT_PKGS: build-essential ${{ matrix.apt_pkgs }} qemu-user-static ccache
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- name: aarch64
cc: aarch64-linux-gnu-gcc
cflags: ""
apt_pkgs: gcc-aarch64-linux-gnu
qemu: qemu-aarch64-static
sysroot: /usr/aarch64-linux-gnu
- name: aarch64+crc
cc: aarch64-linux-gnu-gcc
cflags: "-march=armv8-a+crc"
apt_pkgs: gcc-aarch64-linux-gnu
qemu: qemu-aarch64-static
sysroot: /usr/aarch64-linux-gnu
- name: armv7
cc: arm-linux-gnueabihf-gcc
cflags: ""
apt_pkgs: gcc-arm-linux-gnueabihf
qemu: qemu-arm-static
sysroot: /usr/arm-linux-gnueabihf
- name: powerpc-big-endian
cc: powerpc-linux-gnu-gcc
cflags: ""
apt_pkgs: gcc-powerpc-linux-gnu
qemu: qemu-ppc-static
sysroot: /usr/powerpc-linux-gnu
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Apt cache key
id: apt
run: sh .github/workflows/scripts/apt-cache-key.sh "$APT_PKGS"
- name: Restore apt cache
uses: actions/cache/restore@v4
with:
path: ~/.apt-cache
key: ${{ steps.apt.outputs.key }}
restore-keys: ${{ steps.apt.outputs.prefix }}
- name: Install dependencies
timeout-minutes: 20
run: sh .github/workflows/scripts/apt-install.sh $APT_PKGS
- name: Save apt cache
if: env.APT_CACHE_SAVE == '1'
uses: actions/cache/save@v4
with:
path: ~/.apt-cache
key: ${{ steps.apt.outputs.key }}
- name: Link with lld
run: sh .github/workflows/scripts/use-lld.sh
- name: Restore ccache
uses: actions/cache/restore@v4
with:
path: ~/.ccache
key: ccache-${{ github.workflow }}-${{ github.job }}-${{ matrix.name }}-${{ github.sha }}
restore-keys: |
ccache-${{ github.workflow }}-${{ github.job }}-${{ matrix.name }}-
- name: Enable ccache
run: |
set -eu
export CCACHE_DIR="$HOME/.ccache"
echo "CCACHE_DIR=$CCACHE_DIR" >> "$GITHUB_ENV"
echo "/usr/lib/ccache" >> "$GITHUB_PATH"
ccache -M 250M
ccache -z
- name: Build crc32_test for ${{ matrix.name }}
working-directory: libretro-common/samples/encodings/crc32
run: |
set -u
set -o pipefail
# HAVE_THREADS=0: rthreads needs a target libpthread that the
# cross toolchains here do not reliably provide. The threaded
# sections are covered by the native Clang/TSan lane above;
# what these lanes are for is the per-architecture arithmetic.
make clean
CC="${{ matrix.cc }}" CFLAGS="${{ matrix.cflags }}" make all HAVE_THREADS=0
- name: Run crc32_test under qemu-user
working-directory: libretro-common/samples/encodings/crc32
run: |
set -u
set -o pipefail
${{ matrix.qemu }} -L ${{ matrix.sysroot }} ./crc32_test
- name: Build and run pixconv_test under qemu-user
shell: bash
working-directory: libretro-common/samples/gfx/pixconv
run: |
set -euo pipefail
# The ARM legs are the only place the NEON converters run;
# PowerPC runs the scalar paths big-endian. The armhf
# toolchain defaults to VFP without NEON, so armv7 builds a
# second time with NEON on.
make clean
CC="${{ matrix.cc }}" CFLAGS="${{ matrix.cflags }}" make all
for t in pixconv_test pixconv_test_nosimd pixconv_test_scalar; do
${{ matrix.qemu }} -L ${{ matrix.sysroot }} ./$t
done
if [ "${{ matrix.name }}" = armv7 ]; then
make clean
CC="${{ matrix.cc }}" CFLAGS="-mfpu=neon" make all
${{ matrix.qemu }} -L ${{ matrix.sysroot }} ./pixconv_test
fi
make clean
- name: Build and run the sinc kernels under qemu-user
if: startsWith(matrix.name, 'aarch64') || matrix.name == 'armv7'
shell: bash
working-directory: samples/audio
run: |
set -euo pipefail
# The only place the NEON sinc kernels run: aarch64 the
# intrinsics, armv7 the armeabi assembly for both table
# layouts, against the scalar kernels, through dispatch and
# the fast-forward stages, and shared between siblings.
flags="${{ matrix.cflags }}"
asm=0
if [ "${{ matrix.name }}" = armv7 ]; then
flags="-mfpu=neon"
asm=1
fi
tests="sinc_dispatch sinc_hq sinc_fastforward sinc_sibling"
for d in $tests; do
make -C $d clean
CFLAGS="$flags" make -C $d CC="${{ matrix.cc }}" NEON_ASM=$asm
done
# Each test is one qemu-user process on one CPU: run them all
# at once, and print each one's output once it is done.
declare -A pid
for d in $tests; do
${{ matrix.qemu }} -L ${{ matrix.sysroot }} $d/${d}_test \
> "$RUNNER_TEMP/$d.log" 2>&1 &
pid[$d]=$!
done
fail=0
for d in $tests; do
wait "${pid[$d]}" || { echo "FAIL $d"; fail=1; }
cat "$RUNNER_TEMP/$d.log"
make -C $d clean
done
exit $fail
- name: Run vfs_mapped_ptr_test without mappings (MMAP=0)
shell: bash
working-directory: libretro-common/samples/file/vfs
run: |
set -eu
# The default build has HAVE_MMAP, where filestream_matches_buf()
# compares against the memory map in a single memcmp and its
# windowed fallback never executes - so the window-boundary
# cases in the run above assert on code that did not run.
# This lane is where that path is actually under test, and it
# is also the only coverage the platforms without mappings
# get. Verified to fail here with the window advance off by
# one, which the mapped lane does not notice at all.
make clean MMAP=0 SANITIZER=address,undefined && make -j$(nproc) all MMAP=0 SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 120 \
./vfs_mapped_ptr_test
echo "[pass] vfs_mapped_ptr_test (MMAP=0, ASan)"
- name: Run vfs_v5_metadata_test against a copy_file_range that ignores len
shell: bash
working-directory: libretro-common/samples/file/vfs
run: |
set -eu
# A sandboxed kernel (gVisor on some hosted runners) answers
# copy_file_range by copying to EOF regardless of len. The
# VFS must account the bytes, retire the kernel path for the
# process, and finish the copy in place on the portable path.
# FAKE_CFR=1 is the to-EOF case, FAKE_CFR=2 a partial overshoot
# that leaves the copy part-way and has to be resumed.
for mode in 1 2; do
make clean >/dev/null
make vfs_v5_metadata_test FAKE_CFR=$mode SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 timeout 120 \
./vfs_v5_metadata_test
echo "[pass] vfs_v5_metadata_test (FAKE_CFR=$mode, ASan)"
done
- name: ccache statistics
if: ${{ !cancelled() }}
continue-on-error: true
run: ccache -sv
- 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.
if: success() && github.event_name == 'push' && github.ref == 'refs/heads/master'
uses: actions/cache/save@v4
with:
path: ~/.ccache
key: ccache-${{ github.workflow }}-${{ github.job }}-${{ matrix.name }}-${{ github.sha }}