@@ -25,14 +25,26 @@ jobs:
2525 - name : Install dependencies
2626 run : |
2727 sudo apt-get update -y
28- sudo apt-get install -y build-essential zlib1g-dev
28+ sudo apt-get install -y build-essential zlib1g-dev clang
2929
3030 - name : Checkout
3131 uses : actions/checkout@v3
3232
3333 - name : Build and run samples
3434 shell : bash
3535 working-directory : libretro-common/samples
36+ env :
37+ # Build samples under AddressSanitizer + UndefinedBehaviorSanitizer.
38+ # Most overflow tests under libretro-common/samples (rpng_chunk_
39+ # overflow_test, rbmp_test, vfs_read_overflow_test, cdrom_cuesheet
40+ # _overflow_test, cdfs_dir_record_test, chd_meta_overflow_test, ...)
41+ # use ASan as the regression discriminator for the bound-check
42+ # they verify, so make ASan the default for this workflow. Each
43+ # test's Makefile honours SANITIZER= via the conventional opt-in
44+ # block (matching samples/tasks/http/Makefile). Test sources
45+ # that don't have the opt-in block treat the variable as
46+ # ignored.
47+ MAKE_ARGS : " SANITIZER=address,undefined"
3648 run : |
3749 set -u
3850 set -o pipefail
@@ -48,18 +60,29 @@ jobs:
4860 archive_zstd_test
4961 config_file_test
5062 path_resolve_realpath_test
63+ fill_pathname_test
5164 nbio_test
5265 rpng
5366 rzip_chunk_size_test
5467 net_ifinfo
5568 vfs_read_overflow_test
5669 cdrom_cuesheet_overflow_test
70+ cdfs_dir_record_test
71+ chd_meta_overflow_test
72+ strlcpy_append_test
5773 http_parse_test
5874 rjson_test
5975 rtga_test
6076 rbmp_test
6177 rpng_chunk_overflow_test
6278 rpng_roundtrip_test
79+ word_wrap_overflow_test
80+ task_queue_title_error_test
81+ tpool_wait_test
82+ retro_atomic_test
83+ retro_atomic_extern_c_linkage_test
84+ retro_atomic_extern_c_linkage_test_cxx
85+ retro_spsc_test
6386 )
6487
6588 # Per-binary run command (overrides ./<binary> if present).
@@ -114,7 +137,7 @@ jobs:
114137 fi
115138
116139 # Build
117- if ! ( cd "$d" && make clean all ); then
140+ if ! ( cd "$d" && make clean all $MAKE_ARGS ); then
118141 printf '\n::error title=Build failed::%s failed to build\n' "$rel"
119142 fails=$((fails+1))
120143 continue
@@ -177,3 +200,263 @@ jobs:
177200 if [[ $fails -gt 0 ]]; then
178201 exit 1
179202 fi
203+
204+ - name : Compile-test retro_atomic.h from a C++11 TU
205+ shell : bash
206+ working-directory : libretro-common
207+ run : |
208+ # The C++11 backend in retro_atomic.h is a fresh code path that
209+ # none of the C samples above exercise. Compile a tiny inline
210+ # C++11 TU against the in-tree header to catch regressions like
211+ # accidentally re-introducing an extern "C" wrapper around the
212+ # std::atomic include, or breaking the __cplusplus / _MSVC_LANG
213+ # gate. This step is build-and-run, single-threaded only -- the
214+ # behavioural SPSC stress is already covered by the C test
215+ # binary above on this same host, and the C++11 backend bottoms
216+ # out through the same libstdc++ __atomic_* builtins.
217+ #
218+ # Two fixtures share the g++/clang++ x c++11/14/17 sweep:
219+ #
220+ # cxx_smoke.cpp : header included at C++ linkage,
221+ # the "ordinary" C++ TU shape.
222+ # cxx_smoke_extern_c.cpp : header included from inside an
223+ # extern "C" { ... } wrap, the
224+ # shape ui_qt.cpp uses under
225+ # !CXX_BUILD. This is the
226+ # regression case for the
227+ # RETRO_BEGIN_DECLS_CXX shield in
228+ # retro_atomic.h's C++11 backend.
229+ # The standalone build of the
230+ # sample at samples/atomic/
231+ # retro_atomic_extern_c_linkage/
232+ # covers the same case under the
233+ # default toolchain; this matrix
234+ # extends coverage to clang++ and
235+ # the higher language standards.
236+ set -u
237+ set -o pipefail
238+
239+ tmpdir=$(mktemp -d)
240+ cat > "$tmpdir/cxx_smoke.cpp" <<'EOF'
241+ #include <cstdio>
242+ #include <cstddef>
243+ #include <retro_atomic.h>
244+
245+ #if !defined(HAVE_RETRO_ATOMIC) || !defined(RETRO_ATOMIC_LOCK_FREE)
246+ # error "retro_atomic.h: capability flags not set on a C++11 host"
247+ #endif
248+
249+ int main(void) {
250+ retro_atomic_int_t ai; retro_atomic_int_init(&ai, 0);
251+ retro_atomic_size_t as; retro_atomic_size_init(&as, 0);
252+
253+ retro_atomic_store_release_int(&ai, 42);
254+ retro_atomic_store_release_size(&as, (std::size_t)42);
255+
256+ int li = retro_atomic_load_acquire_int(&ai);
257+ int ls = (int)retro_atomic_load_acquire_size(&as);
258+
259+ int pi = retro_atomic_fetch_add_int(&ai, 1);
260+ int ps = (int)retro_atomic_fetch_add_size(&as, 1);
261+
262+ retro_atomic_inc_int(&ai);
263+ retro_atomic_dec_size(&as);
264+
265+ int qi = retro_atomic_load_acquire_int(&ai);
266+ int qs = (int)retro_atomic_load_acquire_size(&as);
267+
268+ std::printf("backend: %s\n", RETRO_ATOMIC_BACKEND_NAME);
269+
270+ bool ok = (li == 42) && (ls == 42)
271+ && (pi == 42) && (ps == 42)
272+ && (qi == 44) && (qs == 42);
273+ std::puts(ok ? "ALL OK" : "FAIL");
274+ return ok ? 0 : 1;
275+ }
276+ EOF
277+
278+ # Regression fixture: include retro_atomic.h from inside an
279+ # extern "C" wrap. Faithful mirror of ui_qt.cpp's pattern.
280+ # Pre-fix retro_atomic.h would emit ~80 "template with C
281+ # linkage" errors here; the RETRO_BEGIN_DECLS_CXX shield
282+ # around <atomic> makes this compile cleanly.
283+ cat > "$tmpdir/cxx_smoke_extern_c.cpp" <<'EOF'
284+ #include <cstdio>
285+ #include <cstddef>
286+
287+ extern "C" {
288+ #include <retro_atomic.h>
289+ }
290+
291+ int main(void) {
292+ retro_atomic_int_t ai; retro_atomic_int_init(&ai, 0);
293+ retro_atomic_store_release_int(&ai, 7);
294+ int v = retro_atomic_load_acquire_int(&ai);
295+ std::printf("backend: %s\n", RETRO_ATOMIC_BACKEND_NAME);
296+ std::puts(v == 7 ? "ALL OK" : "FAIL");
297+ return v == 7 ? 0 : 1;
298+ }
299+ EOF
300+
301+ for cxx in g++ clang++; do
302+ for std in c++11 c++14 c++17; do
303+ for fixture in cxx_smoke cxx_smoke_extern_c; do
304+ echo "==> compile-test with $cxx -std=$std ($fixture)"
305+ $cxx -std=$std -Wall -Wextra -pedantic -O2 \
306+ -I include \
307+ "$tmpdir/$fixture.cpp" \
308+ -o "$tmpdir/$fixture" \
309+ || { echo "::error title=C++ compile failed::$cxx -std=$std $fixture"; exit 1; }
310+ "$tmpdir/$fixture" \
311+ || { echo "::error title=C++ smoke failed::$cxx -std=$std $fixture"; exit 1; }
312+ done
313+ done
314+ done
315+
316+ rm -rf "$tmpdir"
317+
318+ - name : Run retro_atomic_test under Clang + ThreadSanitizer
319+ shell : bash
320+ working-directory : libretro-common/samples/atomic/retro_atomic_test
321+ run : |
322+ # The native samples job above runs with GCC and ASan/UBSan.
323+ # Clang is the toolchain on every Apple platform, Android NDK
324+ # (since r18), Emscripten, and PS4-ORBIS, so a Clang lane is
325+ # not optional coverage. ThreadSanitizer is the strict
326+ # validator for this test in particular: it instruments every
327+ # atomic load and store and would flag a missing acquire /
328+ # release barrier as a race in the 1M-iteration SPSC stress
329+ # (a class of bug that x86 TSO would otherwise hide on the
330+ # native runner).
331+ set -u
332+ set -o pipefail
333+
334+ make clean
335+ CC=clang make all SANITIZER=thread
336+
337+ TSAN_OPTIONS=halt_on_error=1 ./retro_atomic_test
338+
339+ - name : Run retro_spsc_test under Clang + ThreadSanitizer
340+ shell : bash
341+ working-directory : libretro-common/samples/queues/retro_spsc_test
342+ run : |
343+ # retro_spsc.c is a lock-free SPSC byte queue built on
344+ # retro_atomic.h. Its correctness contract is acquire-load
345+ # / release-store on the head and tail cursors, with the
346+ # buffer reads/writes between them ordered by those barriers.
347+ # Missing or weakened barriers produce torn data on the
348+ # consumer side, observable as content mismatches in the
349+ # stress harness AND as TSan-reported races. The default
350+ # ASan/UBSan pass above catches the content mismatches but
351+ # not the races; this lane catches both.
352+ #
353+ # halt_on_error=1 makes TSan exit non-zero on the first race
354+ # rather than continuing -- which is what we want for CI:
355+ # any race here means the SPSC contract is broken.
356+ set -u
357+ set -o pipefail
358+
359+ make clean
360+ CC=clang make all SANITIZER=thread
361+
362+ TSAN_OPTIONS=halt_on_error=1 ./retro_spsc_test
363+
364+ # Cross-architecture validation lane for retro_atomic_test.
365+ #
366+ # The samples job above runs on x86_64, which is a strongly-ordered
367+ # (TSO) architecture. retro_atomic.h's contract is that acquire-load
368+ # / release-store / acq_rel-RMW emit real barriers on weakly-ordered
369+ # SMP targets (ARM, AArch64, PowerPC, MIPS). An x86_64 host run
370+ # cannot exercise that property, because TSO masks reordering bugs
371+ # at the hardware level even when the macros emit no barriers at all.
372+ #
373+ # This job cross-compiles retro_atomic_test for AArch64 and ARMv7 and
374+ # runs the binary under qemu-user-static. qemu-user emulates the
375+ # weak memory model faithfully enough to expose missing-barrier bugs
376+ # in the SPSC stress test, and is cheap enough to run on every push.
377+ #
378+ # We deliberately do NOT run the full samples sweep here -- the rest
379+ # of the samples don't have architecture-dependent codegen that
380+ # warrants the extra CI time. retro_atomic_test is the one that
381+ # benefits from cross-arch coverage.
382+ #
383+ # Real ARM hardware still beats qemu (see e.g. PostgreSQL's 2025
384+ # Win11/ARM64 atomic ordering bug, found only on real silicon),
385+ # but qemu catches most categorical errors and is much cheaper than
386+ # provisioning ARM runners.
387+ retro-atomic-cross :
388+ name : Cross-arch retro_atomic_test (${{ matrix.arch }})
389+ runs-on : ubuntu-latest
390+ timeout-minutes : 10
391+ strategy :
392+ fail-fast : false
393+ matrix :
394+ include :
395+ - arch : aarch64
396+ cc : aarch64-linux-gnu-gcc
397+ apt_pkgs : gcc-aarch64-linux-gnu
398+ qemu : qemu-aarch64-static
399+ sysroot : /usr/aarch64-linux-gnu
400+ - arch : armv7
401+ cc : arm-linux-gnueabihf-gcc
402+ apt_pkgs : gcc-arm-linux-gnueabihf
403+ qemu : qemu-arm-static
404+ sysroot : /usr/arm-linux-gnueabihf
405+
406+ steps :
407+ - name : Install dependencies
408+ run : |
409+ sudo apt-get update -y
410+ sudo apt-get install -y build-essential ${{ matrix.apt_pkgs }} qemu-user-static
411+
412+ - name : Checkout
413+ uses : actions/checkout@v3
414+
415+ - name : Build retro_atomic_test for ${{ matrix.arch }}
416+ working-directory : libretro-common/samples/atomic/retro_atomic_test
417+ run : |
418+ set -u
419+ set -o pipefail
420+ make clean
421+ CC=${{ matrix.cc }} make all
422+
423+ - name : Run retro_atomic_test under qemu-user
424+ working-directory : libretro-common/samples/atomic/retro_atomic_test
425+ run : |
426+ set -u
427+ set -o pipefail
428+ ${{ matrix.qemu }} -L ${{ matrix.sysroot }} ./retro_atomic_test
429+
430+ - name : Inspect emitted atomic instructions
431+ working-directory : libretro-common/samples/atomic/retro_atomic_test
432+ run : |
433+ set -u
434+ set -o pipefail
435+ # Spot-check the codegen. If retro_atomic.h were silently
436+ # falling through to a no-barrier backend on this arch, the
437+ # asm would be conspicuously missing acquire/release
438+ # instructions. This is a cheap sanity check on top of the
439+ # behavioural SPSC test above.
440+ ${{ matrix.cc }} -O2 -S \
441+ -I../../../include -DHAVE_THREADS \
442+ retro_atomic_test.c -o /tmp/retro_atomic_test.s
443+ echo
444+ echo '== Unique barrier-emitting mnemonics =='
445+ case "${{ matrix.arch }}" in
446+ aarch64)
447+ # Expect: ldar, stlr, and __aarch64_ldadd*_acq_rel libcalls
448+ # (or inline ldaddal LSE on +lse builds).
449+ pattern='\b(ldar|stlr|ldax|stlx|dmb|ldadd[a-z0-9_]*|swp[a-z0-9_]*|__aarch64_(ldadd|swp)[a-z0-9_]*acq_rel)\b'
450+ ;;
451+ armv7)
452+ # Expect: dmb (data memory barrier) and ldrex/strex pairs.
453+ pattern='\b(dmb|ldrex|strex|ldrexb|strexb|ldrexh|strexh)\b'
454+ ;;
455+ esac
456+ mnemonics=$(grep -oE "$pattern" /tmp/retro_atomic_test.s | sort -u)
457+ echo "$mnemonics"
458+ if [[ -z "$mnemonics" ]]; then
459+ echo
460+ 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.'
461+ exit 1
462+ fi
0 commit comments