Files
RetroArch/.github/workflows/Linux-samples-gfx.yml
T
Arnold de Ruiterandlibretroadmin e337babce9 core_info: read an info file added after its core was cached, keep served entries in a rewritten cache (#20007)
* Fix core info cache ignoring late .info files and dropping cached entries on rewrite

* core_info: write the cache straight from the core list

The cache writer takes the core list's entries as they are instead of
a deep copy of every installed core, and a core read from its info
file no longer gets copied into a cache list that is not written.

core_info_cache_test also checks that an unchanged core set leaves the
cache alone, and runs under ASan and UBSan in Linux-samples-tasks.

* core_info: read cached entries in place, take core paths from the listing

The cache reader parses each entry straight into the next slot of the
cache list instead of a separate allocation moved in afterwards, and a
firmware list grows geometrically rather than one entry per realloc.
A core's path is taken over from the directory listing, which is freed
at the end of the same pass, instead of being copied.

core_info_cache_test adds a start served from the cache alone, with
more cores than the list starts with and long firmware lists, and a
cache cut off inside an entry, which must be discarded without a leak.

* string_list: add packed splits, use them for core info lists

string_split_packed() splits into a single allocation holding the
list, its elements and the tokens, marked by cap 0 with elements
present. string_list_free() releases it in one free; growing it and
string_list_deinitialize() are refused, so a packed list stays
read-only.

Every pipe-separated core info field is split this way, from the
cache and from info files alike, which takes a warm start over the
full libretro-core-info set from 16661 allocations to 8851. The
string_split_packed sample checks it against string_split and the
refusals, under ASan and UBSan and as a mingw-w64 build under Wine.

---------

Co-authored-by: libretroadmin <[email protected]>
2026-11-06 14:16:58 +01:00

3539 lines
174 KiB
YAML

name: CI Linux samples/gfx
on:
push:
branches:
- master
# ci_paths: generated by tools/ci_paths.py, do not edit
paths:
- '**'
- '!.github/workflows/*.yml'
- '.github/workflows/Linux-samples-gfx.yml'
# ci_paths: end
pull_request:
branches:
- master
# ci_paths: generated by tools/ci_paths.py, do not edit
paths:
- '**'
- '!.github/workflows/*.yml'
- '.github/workflows/Linux-samples-gfx.yml'
# ci_paths: end
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:
metal-frame-lease:
runs-on: ubuntu-latest
timeout-minutes: 10
env:
APT_PKGS: clang libgnustep-base-dev libblocksruntime-dev ffmpeg
steps:
- uses: actions/checkout@v6
- 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: 15
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: Strict C89 geometry with Clang
run: CC=clang python3 samples/gfx/metal_frame/run.py
- name: VideoToolbox HEVC parameter ownership and session setup
run: |
python3 samples/gfx/videotoolbox_hevc/codec_fixtures.py --verify
python3 samples/gfx/videotoolbox_hevc/hdr_codec_fixtures.py --verify
python3 samples/gfx/videotoolbox_hevc/sequence_fixtures.py --verify
CC=clang python3 samples/gfx/videotoolbox_hevc/run.py --apple-limits
python3 samples/gfx/videotoolbox_hevc/run.py
CFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' python3 samples/gfx/videotoolbox_hevc/run.py
python3 samples/gfx/videotoolbox_hevc/run.py --threadless
CFLAGS='-DRETRO_ATOMIC_FORCE_VOLATILE -DRETRO_ATOMIC_SUPPRESS_WARNING' python3 samples/gfx/videotoolbox_hevc/run.py
TSAN_OPTIONS=halt_on_error=1 CFLAGS='-fsanitize=thread -fno-omit-frame-pointer' RUNNER="setarch $(uname -m) -R" python3 samples/gfx/videotoolbox_hevc/run.py
- name: VideoToolbox VP9 lifecycle and retained renderer snapshots
run: |
for compiler in gcc clang; do
CC="$compiler" make -C samples/gfx/videotoolbox_vp9 clean check SANITIZER=address,undefined
done
TSAN_OPTIONS=halt_on_error=1 CFLAGS='-fsanitize=thread -fno-omit-frame-pointer' RUNNER="setarch $(uname -m) -R" python3 samples/gfx/videotoolbox_vp9/run.py
- name: Native recording reinit ownership
run: |
make -C samples/record/gpu_reinit check CC=gcc SANITIZER=address,undefined
make -C samples/record/gpu_reinit clean
make -C samples/record/gpu_reinit check CC=clang SANITIZER=address,undefined
- name: HEVC software and hardware sequencing regression
working-directory: libretro-common/samples/formats/h265
run: |
make -j2 all SANITIZER=address,undefined
./rh265_malformed_test
./rh265_weight_test
./rh265_lossless_test
./rh265_lossless_test_nothreads
./rh265_lossless_test_volatile
- name: Retained Metal lease ownership and completion
run: python3 samples/gfx/metal_frame/run.py --mock
video-thread-state:
name: The video thread reads its snapshot
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Link with lld
run: sh .github/workflows/scripts/use-lld.sh
- name: Tree check
shell: bash
run: |
set -eu
# The main thread builds a video_frame_info_t and hands it
# over; a driver that reaches back into config_get_ptr() from
# frame() or present_last() reads settings the main thread
# may be writing. TSan found one of those in the null driver
# rather than anyone noticing it by reading, so this looks
# for the rest: it walks what those two entry points call
# and fails on live-state access it can reach. The drivers
# for platforms with no video thread are listed in the
# checker with that reason, and the list has to shrink when
# one of them stops doing it.
tools/check.sh tools/video_thread_state_check.c
drm-vkms-live:
name: The DRM driver on a real kernel (vkms)
runs-on: ubuntu-latest
timeout-minutes: 20
env:
APT_PKGS: libdrm-dev qemu-system-x86 busybox-static zstd cpio
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 }}
# The kernel the VM boots: Ubuntu's current HWE kernel, fetched
# once per version rather than every run
- name: Kernel version
id: kernel
run: echo "kver=$(sh samples/gfx/drm_vkms_live/run_vm.sh --kernel-version)" >> "$GITHUB_OUTPUT"
- name: Restore kernel
id: kernel-cache
uses: actions/cache/restore@v4
with:
path: samples/gfx/drm_vkms_live/.kernel
key: vkms-kernel-${{ steps.kernel.outputs.kver }}
- name: Build and run drm_vkms_live_test in a VM
shell: bash
working-directory: samples/gfx/drm_vkms_live
run: |
set -eu
# gfx/drivers/drm_gfx.c against the kernel itself: Ubuntu's
# current HWE kernel boots in qemu with vkms, its overlay
# planes and its writeback connector, and what the CRTC
# composes is read back and compared with the frame - as it
# is, turned each way, scanned out of the threaded wrapper's
# frame memory, flipped without vsync, under the input overlay
# on a plane of its own, at a CRT modeline, and in RGB565 -
# and with the screenshot readback. The fake kernel in
# drm_surface_lifetime cannot say what a kernel refuses: a
# turn set on its own, outside the commit that places the
# plane, was refused here.
make SANITIZER=address,undefined
make check SANITIZER=address,undefined
- name: Save kernel
if: steps.kernel-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: samples/gfx/drm_vkms_live/.kernel
key: vkms-kernel-${{ steps.kernel.outputs.kver }}
surface-requirements:
name: Producers ask the surface layer
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Link with lld
run: sh .github/workflows/scripts/use-lld.sh
- name: Self-test
shell: bash
run: |
set -eu
# A producer outside the allowlist that reads the video flags
# directly must be caught, and so must an entry whose file has
# stopped reading them - the second is what makes the list
# shrink as producers migrate instead of sitting there.
tools/check.sh tools/surface_requirements_check.c --selftest
- name: Tree check
shell: bash
run: |
set -eu
# What a producer should emit is the surface layer's answer
# (gfx_surface_query_requirements), so a decoder has one place
# to ask rather than three unrelated flags to know about. The
# files still asking directly are listed in the checker and
# are migrated one at a time; a new one may not join them.
tools/check.sh tools/surface_requirements_check.c
- name: texture_image request check
shell: bash
run: |
set -eu
# image_texture_load reads pix10 as the caller's request for a
# 10-bit decode, so a stack image filled field by field asks for
# whatever the stack held and a 16-bit PNG comes back as packed
# 10-bit words now and then. Every loaded image has its request
# set first; the self-test proves an unset one is caught.
tools/check.sh tools/texture_image_init_check.c --selftest
tools/check.sh tools/texture_image_init_check.c
vulkan-swapchain-extent:
name: Android Vulkan orientation extents
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Regression test
run: make -C samples/gfx/vulkan_swapchain_extent check
vulkan-driver-pacing:
name: Vulkan driver pacing backend (fake VK_NV_low_latency2 and NVAPI)
runs-on: ubuntu-latest
timeout-minutes: 15
env:
APT_PKGS: gcc-mingw-w64-x86-64 wine64
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: 15
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: Regression test
run: |
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
# The mode and sleep state machine of the Driver Pacing
# backend, on fake entry points: one sleep and one bounded
# wait a content frame, markers on the released frame only,
# a replaced swapchain starting over, errors falling back.
make -C samples/gfx/vulkan_driver_pacing check
make -C samples/gfx/vulkan_driver_pacing clean
make -C samples/gfx/vulkan_driver_pacing SANITIZER=address,undefined check
make -C samples/gfx/vulkan_driver_pacing clean
# Windows: presenting through DXGI, the mode, sleep and
# markers go through NVAPI (a stand-in nvapi64.dll)
make -C samples/gfx/vulkan_driver_pacing CC=x86_64-w64-mingw32-gcc RUNNER=wine check
make -C samples/gfx/vulkan_driver_pacing clean
vulkan-custom-driver:
name: Android custom Vulkan driver packages and crash state
runs-on: ubuntu-latest
timeout-minutes: 15
env:
APT_PKGS: gcc-mingw-w64-x86-64 wine64
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: 15
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: Regression test (Linux, ASan + UBSan)
run: |
# Package checks, the extraction key, the sticky crash state,
# the session's driver choices and the soname patch, fed from
# buffers; the loader itself is Android-only.
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
make -C samples/gfx/vulkan_custom_driver SANITIZER=address,undefined check
make -C samples/gfx/vulkan_custom_driver clean
- name: Regression test (mingw-w64, Wine)
run: |
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
make -C samples/gfx/vulkan_custom_driver CC=x86_64-w64-mingw32-gcc RUNNER=wine check
make -C samples/gfx/vulkan_custom_driver clean
vulkan-upscaler:
name: Vulkan temporal upscaler glue (lavapipe, stand-in NGX, FSR and XeSS)
runs-on: ubuntu-latest
timeout-minutes: 15
env:
APT_PKGS: libvulkan-dev mesa-vulkan-drivers vulkan-validationlayers gcc-mingw-w64-x86-64 wine64
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: 15
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: Regression test
run: |
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
# gfx/common/vulkan_ngx.c, vulkan_ffx.c, vulkan_xess.c, the
# dispatch of vulkan_upscaler.c and gfx/video_upscaler.c on
# lavapipe under the validation layer, against stand-in NGX,
# FidelityFX API and XeSS libraries: the session, the core's
# request, a frame's feature and dispatch parameters, resizes
# retiring the old output, refused frames, teardown.
make -C samples/gfx/vulkan_upscaler check
make -C samples/gfx/vulkan_upscaler clean
make -C samples/gfx/vulkan_upscaler SANITIZER=address,undefined check
make -C samples/gfx/vulkan_upscaler clean
# Windows: the same glue built with mingw; Wine has no Vulkan
# device here, so the run only proves the build and the skip
make -C samples/gfx/vulkan_upscaler CC=x86_64-w64-mingw32-gcc RUNNER=wine check
make -C samples/gfx/vulkan_upscaler clean
d3d12-upscaler:
name: Direct3D 12 temporal upscaler glue (Wine vkd3d on lavapipe, stand-in NGX, FSR and XeSS)
runs-on: ubuntu-latest
timeout-minutes: 15
env:
APT_PKGS: gcc-mingw-w64-x86-64 wine64 xvfb xauth mesa-vulkan-drivers libvulkan1
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: 15
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: Regression test
run: |
# gfx/common/d3d12_ngx.c, d3d12_ffx.c, d3d12_xess.c, the
# dispatch of d3d12_upscaler.c and gfx/video_upscaler.c,
# cross-built with mingw, on Wine's D3D12 (vkd3d over
# lavapipe, which wants a display) against stand-in NGX,
# FidelityFX API and XeSS libraries: the session, the core's
# request, a frame's feature and dispatch on the frame's list
# with the output read back, resizes retiring the old output,
# refused frames, teardown.
export WINEPREFIX="$RUNNER_TEMP/d3d12-upscaler-wine"
export WINEDEBUG=-all
make -C samples/gfx/d3d12_upscaler
timeout 180 xvfb-run -a bash -c '
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
for pass in 1 2; do
make -C samples/gfx/d3d12_upscaler check
done
'
make -C samples/gfx/d3d12_upscaler clean
slang-rect:
name: Presets read frames where they lie
runs-on: ubuntu-latest
timeout-minutes: 20
env:
APT_PKGS: build-essential libegl-dev libgl-dev libegl-mesa0 libgl1-mesa-dri libspirv-cross-c-shared-dev gcc-mingw-w64-x86-64 wine64
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: 15
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 slang_rect_test (ASan + UBSan, llvmpipe)
shell: bash
working-directory: samples/gfx/slang_rect
run: |
set -eu
# gfx/drivers_shader/slang_rect.c rewrites a preset's stage
# to read its frame inside a larger texture. Each test shader,
# sampled each way, comes back rewritten, left alone or
# refused as it should and cross-compiles; and drawn by Mesa,
# every rewritten stage reads the frame bit for bit as the
# stage it was reads a texture of its own.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \
timeout 600 make check CHECK_ARGS=--require-gl
make clean
- name: Build and run rslc_remap_test (ASan + UBSan, llvmpipe)
shell: bash
working-directory: samples/gfx/rslc_remap
run: |
set -eu
# rslc's frame-rectangle remap, which rewrites a stage in the
# compiler for every destination: each test shader of
# ../slang_rect, sampled each way, comes back as slang_rect.c
# says of rslc's own module, prints as GLSL, GLSL ES, HLSL and
# MSL, places the rectangle past both stages' blocks, fails
# cleanly out of memory; and drawn by Mesa from rslc's GLSL,
# every rewritten stage reads the frame bit for bit as the
# stage it was reads a texture of its own.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \
timeout 600 make check CHECK_ARGS=--require-gl
make clean
- name: Build and run rslc_remap_test (mingw-w64, Wine)
shell: bash
working-directory: samples/gfx/rslc_remap
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
make clean CC=x86_64-w64-mingw32-gcc && make -j$(nproc) all CC=x86_64-w64-mingw32-gcc LDFLAGS=-static
WINEDEBUG=-all timeout 300 wine ./rslc_remap_test.exe
make clean
slang-last-pass-target:
name: Last shader pass renders to the backbuffer
runs-on: ubuntu-latest
timeout-minutes: 20
env:
APT_PKGS: build-essential libgl-dev gcc-mingw-w64-x86-64 wine64
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: 15
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: Build and run slang_hlsl_backend_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/slang_hlsl_backend
run: |
set -eu
# The real slang_process(): rslc hands a pass back as HLSL for
# the Direct3D 10/11/12 drivers, and a preset's simulated
# SourceSize reaches the first pass only, from
# storage that outlives the semantics the D3D drivers copy.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \
timeout 300 make check SANITIZER=address,undefined
make clean
- name: Build and run slang_last_pass_target_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/slang_last_pass_target
run: |
set -eu
# The Vulkan and glcore slang chains give a preset's last pass
# a render target and a hidden copy to the backbuffer only when
# scale/format or compiled feedback requires it: viewport at 1.0
# renders direct; other scales, a preset or shader format, and
# a read of the last pass's feedback keep the target.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \
timeout 60 make check
make clean
- name: Build and run slang_last_pass_target_test (mingw-w64, Wine)
shell: bash
working-directory: samples/gfx/slang_last_pass_target
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
make clean CC=x86_64-w64-mingw32-gcc && make -j$(nproc) all CC=x86_64-w64-mingw32-gcc LDFLAGS="-static -Wl,--gc-sections"
WINEDEBUG=-all timeout 300 wine ./slang_last_pass_target_test.exe
make clean
- name: Build and run shader_chain_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/shader_chain
run: |
set -eu
# The blob table: a .slang two passes name compiles once, and
# a chain built while the one it replaces is alive compiles
# only the passes the live chain does not have.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \
timeout 120 make check
make clean
- name: Build and run shader_chain_test (mingw-w64, Wine)
shell: bash
working-directory: samples/gfx/shader_chain
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
make clean CC=x86_64-w64-mingw32-gcc && make -j$(nproc) all CC=x86_64-w64-mingw32-gcc LDFLAGS="-static"
WINEDEBUG=-all timeout 300 wine ./shader_chain_test.exe
make clean
- name: Build and run gl2/gl3 last_pass_fbo tests (ASan + UBSan)
shell: bash
working-directory: samples/gfx/gl_last_pass_fbo
run: |
set -eu
# The GLSL/Cg render chains of gfx/drivers/gl2.c and gl3.c
# against a scripted GL and shader backend: the last pass gets
# an FBO only for a scale other than viewport at 1.0, an FBO
# format, or the feedback the preset asks of it; a one-pass
# preset that needs none is drawn with none.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \
timeout 60 make check
make clean
- name: Build and run gl2/gl3 last_pass_fbo tests (mingw-w64, Wine)
shell: bash
working-directory: samples/gfx/gl_last_pass_fbo
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
make clean CC=x86_64-w64-mingw32-gcc && make -j$(nproc) all CC=x86_64-w64-mingw32-gcc LDFLAGS=-static
WINEDEBUG=-all timeout 300 wine ./gl2_last_pass_fbo_test.exe
WINEDEBUG=-all timeout 300 wine ./gl3_last_pass_fbo_test.exe
make clean
- name: GLES texture loan capability and ownership (mingw-w64, Wine)
shell: bash
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
CC=x86_64-w64-mingw32-gcc RUNNER="wine" WINEDEBUG=-all \
python3 samples/gfx/gles_texture_lend/run.py
- name: Build and run gl2_pbo_readback_test (mingw-w64, Wine)
shell: bash
working-directory: samples/gfx/gl2_pbo_readback
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
# The Linux runs are in the samples-gfx job; this is the
# Windows build, the GL 1.1 entry points through import
# pointers (gl11_fakes.h).
make clean CC=x86_64-w64-mingw32-gcc && make -j$(nproc) all CC=x86_64-w64-mingw32-gcc LDFLAGS=-static
WINEDEBUG=-all timeout 300 wine ./gl2_pbo_readback_test.exe
CC=x86_64-w64-mingw32-gcc RUNNER=wine WINEDEBUG=-all python3 gles3_init_test.py
make clean
- name: Build and run d3d9 last_pass tests (mingw-w64, Wine)
shell: bash
working-directory: samples/gfx/d3d9_last_pass
run: |
set -eu
# The preset setup of gfx/drivers/d3d9hlsl.c and d3d9cg.c: a
# stock pass is appended after the last pass only for a scale
# other than viewport at 1.0 or a floating-point target.
# Direct3D 9 is Windows-only, so these run under Wine alone.
make clean && make -j$(nproc) all
timeout 300 make check
make clean
- name: Build and run the d3d9 software framebuffer loan test (mingw-w64, Wine)
shell: bash
working-directory: samples/gfx/d3d9_sw_fb
run: |
set -eu
# GET_CURRENT_SOFTWARE_FRAMEBUFFER on the D3D9 drivers: the frame
# texture lent locked, read out of without a copy, a cropped
# window moved to its corner, and the texture holding the cached
# frame kept alive until it is retired.
make clean && make all
timeout 300 make check
make clean
- name: Build and run d3d9 menu vertex ring tests (mingw-w64, Wine)
shell: bash
working-directory: samples/gfx/d3d9_menu_ring
run: |
set -eu
# Each D3D9 driver's display draw against a stand-in device: the
# menu's vertex ring is dynamic, starts over with DISCARD and
# never rewrites a vertex a pending draw reads (d3d9_cg's did,
# and drew whole-screen triangles while a notification was up).
make clean && make -j$(nproc) all
timeout 300 make check
make clean
samples-gfx:
name: Build and run samples/gfx (drivers, shaders, widgets)
# The first of five jobs the gfx samples run in side by side
# (samples-gfx-*): each keeps its steps in order, so a step that
# builds on an earlier one stays in its job. gfx_thumbnail_preview
# is its own job, thumbnail-preview.
runs-on: ubuntu-latest
timeout-minutes: 30
env:
APT_PKGS: build-essential libx11-dev libxrandr-dev libdrm-dev xserver-xorg-core xserver-xorg-video-dummy x11-xserver-utils libsdl2-dev libxinerama-dev libwayland-dev libegl-dev libgles-dev libgbm-dev gcc-mingw-w64-x86-64 wine64 openbox cmake ninja-build ccache libgl-dev xvfb libdbus-1-dev dbus python3-dbus python3-gi weston xwayland mutter libvkd3d1 mesa-vulkan-drivers fonts-dejavu-core fonts-dejavu-mono libfreetype-dev
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: Video modeline engine goldens and C89 lane
shell: bash
run: |
set -eu
# The engine in gfx/modeline/ replaced the SwitchRes 2.2.1
# library; tools/modeline_gold.ref is the reference output of
# the same matrix run through that library. The scoring and
# timing arithmetic are compared bit-for-bit: any drift in
# modeline_create/compare/adjust, the presets or the list
# logic shows up here as a diff rather than as a CRT that
# loses sync in the field.
C89="-std=c89 -ansi -pedantic -Werror=pedantic -Wno-long-long"
C89="$C89 -Werror=declaration-after-statement -Wno-variadic-macros"
C89="$C89 -D_GNU_SOURCE"
INC="-Ilibretro-common/include -I. -Ideps"
# Binaries and outputs go to a scratch dir, never the tree.
T=$(mktemp -d)
gcc $C89 $INC tools/modeline_gold.c \
libretro-common/compat/compat_strl.c -lm -o "$T/modeline_gold"
"$T/modeline_gold" > "$T/modeline_gold.out"
diff -u tools/modeline_gold.ref "$T/modeline_gold.out"
echo "[pass] modeline goldens (C89)"
gcc -g1 -O1 -fsanitize=address,undefined -fno-sanitize-recover=all \
-std=gnu89 -D_GNU_SOURCE $INC tools/modeline_gold.c \
libretro-common/compat/compat_strl.c -lm -o "$T/modeline_gold_san"
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
"$T/modeline_gold_san" > "$T/modeline_gold_san.out"
diff -u tools/modeline_gold.ref "$T/modeline_gold_san.out"
echo "[pass] modeline goldens (ASan/UBSan)"
rm -rf "$T"
# The consumer and the X11 apply path are C89 too; the
# full C89_BUILD covers them only after a complete build.
DEF="-DRARCH_INTERNAL -DHAVE_THREADS -DHAVE_MENU -DHAVE_MODELINE"
DEF="$DEF -DHAVE_X11 -DHAVE_XRANDR -DHAVE_CONFIGFILE -DHAVE_DYNAMIC"
for f in gfx/modeline/modeline_core.c \
gfx/modeline/modeline_monitor.c \
gfx/modeline/modeline_list.c \
gfx/modeline/modeline_ini.c \
gfx/video_crt_switch.c \
gfx/display_servers/dispserv_x11.c; do
gcc $C89 $INC $DEF -c "$f" -o /dev/null
done
echo "[pass] modeline C89 syntax check"
- name: Atomic backend portability of the gfx sources
shell: bash
run: |
set -eu
# The gfx sources that use retro_atomic.h have to build on
# every backend, not just the one this host selects. The
# extended operations (exchange, CAS, the pointer ops) are
# offered only where a backend can express them, so a caller
# that uses one without gating on it builds here and breaks
# on whichever console lands elsewhere; the counters have to
# stay warning-clean as statements, which an expression-form
# fetch_add is not. Both have reached a console lane before
# this one, hence -Werror.
INC="-Ilibretro-common/include -I. -Ideps"
DEF="-DRARCH_INTERNAL -DHAVE_THREADS -DHAVE_MENU -DHAVE_RGUI"
DEF="$DEF -DHAVE_CONFIGFILE -DHAVE_CHEATS -D_GNU_SOURCE"
DEF="$DEF -DRETRO_ATOMIC_SUPPRESS_WARNING"
# The PS2 backend needs ps2sdk's <kernel.h>; the sample under
# libretro-common/samples/atomic carries a stand-in for it.
PS2STUB="libretro-common/samples/atomic/retro_atomic_ps2_backend"
for backend in C11 GCC_NEW SYNC PS2 VOLATILE; do
EXTRA=""
if [ "$backend" = "PS2" ]; then EXTRA="-I$PS2STUB/ps2stub"; fi
for f in gfx/video_thread_wrapper.c \
gfx/font_driver.c \
gfx/gfx_thumbnail.c \
libretro-common/formats/video_player.c \
libretro-common/queues/task_queue.c; do
gcc -std=gnu99 -O2 -Wall -Werror $INC $EXTRA $DEF \
"-DRETRO_ATOMIC_FORCE_$backend" -c "$f" -o /dev/null
done
gcc -std=gnu99 -O2 -Wall -Werror $INC $EXTRA $DEF \
-DHAVE_GFX_WIDGETS "-DRETRO_ATOMIC_FORCE_$backend" \
-c gfx/gfx_widgets.c -o /dev/null
# The Vulkan driver, from the Khronos headers the tree carries
gcc -std=gnu99 -O2 -Wall -Werror $INC -Igfx/include $EXTRA $DEF \
-DHAVE_VULKAN "-DRETRO_ATOMIC_FORCE_$backend" \
-c gfx/drivers/vulkan.c -o /dev/null
echo "[pass] RETRO_ATOMIC_FORCE_$backend"
done
echo "[pass] atomic backend portability"
- name: Build and run vulkan_extension_count_test (ASan)
shell: bash
working-directory: samples/gfx/vulkan_extension_count
run: |
set -eu
# Regression test for the heap-overflow fix in
# gfx/common/vulkan_common.c::vulkan_find_device_extensions.
# Pre-fix the function appended the required-extension list
# twice -- once via memcpy at the top of the body, then
# again in a per-element loop -- consuming
# (count_initial + 2*num_required + num_optional) slots in
# the caller's buffer. vulkan_context_create_device_wrapper
# sized its malloc for (count_initial + num_required +
# num_optional) entries, so a libretro core using the Vulkan
# HW context-negotiation interface against a GPU exposing at
# least one optional extension hit a one-element heap-buffer-
# overflow (8 bytes on 64-bit) at the end of the malloc'd
# block. Build under AddressSanitizer so any reintroduction
# of the duplicate write is caught at the bounds level. If
# vulkan_common.c amends the append-to-enabled[] block, the
# verbatim copy in vulkan_extension_count_test.c must follow.
make clean SANITIZER=address && make -j$(nproc) all SANITIZER=address
test -x vulkan_extension_count_test
timeout 60 ./vulkan_extension_count_test
echo "[pass] vulkan_extension_count_test"
- name: Build and run video_views_test (ASan+UBSan)
shell: bash
working-directory: samples/gfx/video_views
run: |
set -eu
# gfx/video_views.c decides which rectangle of the core's
# frame lands where, and maps touch back through that layout.
# Pure logic, so every mode and edge case runs here.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
test -x video_views_test
timeout 60 ./video_views_test
echo "[pass] video_views_test"
- name: Build and run video_windows_test (ASan+UBSan)
shell: bash
working-directory: samples/gfx/video_windows
run: |
set -eu
make clean all SANITIZER=address,undefined
timeout 60 ./video_windows_test
echo "[pass] video_windows_test"
- name: Build and run video_xr_test (ASan+UBSan)
shell: bash
working-directory: samples/gfx/video_xr
run: |
set -eu
# gfx/video_xr.c places a core's screens in a headset, sizes
# their images and maps a pointer on them back to the frame.
# Pure maths, so every layout and edge runs here.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
test -x video_xr_test
timeout 60 ./video_xr_test
echo "[pass] video_xr_test"
- name: Build and run vulkan_swapchain_clamp_test (ASan)
shell: bash
working-directory: samples/gfx/vulkan_swapchain_clamp
run: |
set -eu
# Regression test for the unclamped-swapchain-image-count
# fix in gfx/common/vulkan_common.c::vulkan_create_swapchain.
# Pre-fix the two vkGetSwapchainImagesKHR calls (count
# query + image fill) had no clamp between them, so a driver
# returning more than VULKAN_MAX_SWAPCHAIN_IMAGES (16) images
# on the second call wrote past context.swapchain_images[16]
# and every loop bounded by num_swapchain_images walked past
# its compile-time-sized companion array (~12 such loops
# across init/deinit/textures/buffers/descriptor pools/
# command buffers/readback and direct vk->swapchain[i]
# uses). Build under AddressSanitizer so any reintroduction
# of either the request-side or the post-create clamp is
# caught at the bounds level. If vulkan_common.c amends
# the cap or the post-create clamp, the verbatim copies in
# vulkan_swapchain_clamp_test.c must follow.
make clean SANITIZER=address && make -j$(nproc) all SANITIZER=address
test -x vulkan_swapchain_clamp_test
timeout 60 ./vulkan_swapchain_clamp_test
echo "[pass] vulkan_swapchain_clamp_test"
- name: Build and run vulkan_output_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/vulkan_output
run: |
set -eu
# The second output's swapchain (vulkan_output_* in
# gfx/common/vulkan_common.c), the real code against a fake
# device: nothing it does in a frame waits, and nothing is
# destroyed before the fence that covers it.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
test -x vulkan_output_test
timeout 60 ./vulkan_output_test
echo "[pass] vulkan_output_test"
- name: Build and run vulkan_chain_alloc_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/vulkan_chain_alloc
run: |
set -eu
# gfx/drivers_shader/shader_vulkan.c's filter chain creation,
# Vulkan through the symbol wrapper pointed at mocks, with
# each allocation it makes failed in turn: a chain comes back
# with every pass and every deferred-disposal queue, or not
# at all, and nothing is left behind. A replay of the last
# frame's final pass reads what that pass read, and a chain
# made to replay keeps one history image more.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./vulkan_chain_alloc_test
make clean
echo "[pass] vulkan_chain_alloc_test"
- name: Build and run gl3_shader_status_test and gl3_chain_alloc_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/gl3_shader_status
run: |
set -eu
# gfx/drivers_shader/shader_gl3.c with GL through the glsym
# table pointed at mocks: shader status checks, the chain's
# allocations failed in turn, and a replay of the last frame's
# final pass reading the textures, at the generations, that
# pass read, the history keeping a spare.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 make check SANITIZER=address,undefined
make clean
echo "[pass] gl3_shader_status_test, gl3_chain_alloc_test"
- name: Build and run vulkan_texture_size_test (ASan)
shell: bash
working-directory: samples/gfx/vulkan_texture_size
run: |
set -eu
# Regression test for the 32-bit-overflow fix in
# gfx/drivers/vulkan.c::vulkan_create_texture's staging-
# buffer sizing. Pre-fix the buffer_info.size calculation
# (buffer_width * height) was unsigned*unsigned in 32-bit
# before being widened to VkDeviceSize on assignment. With
# dimensions large enough to wrap (e.g. 65536x16385x4 ->
# 0x1_0004_0000 truncates to 0x40000), the staging buffer
# was malloc'd at the wrapped (small) size while the per-row
# upload memcpy loop walked the full width x height,
# writing past the mapped region into adjacent heap memory.
# Reachable from libretro cores supplying oversized
# retro_framebuffer dimensions and from vulkan_load_texture
# / vulkan_set_texture_frame. Build under AddressSanitizer
# so any reintroduction of the 32-bit arithmetic is caught
# at the bounds level. If vulkan.c amends the size
# calculation or upload-loop strides, the verbatim copies
# in vulkan_texture_size_test.c must follow.
make clean SANITIZER=address && make -j$(nproc) all SANITIZER=address
test -x vulkan_texture_size_test
timeout 60 ./vulkan_texture_size_test
echo "[pass] vulkan_texture_size_test"
- name: Build and run vulkan_views_target_test
shell: bash
working-directory: samples/gfx/vulkan_views_target
run: |
set -eu
# gfx/drivers/vulkan.c's retained view targets and headset
# draw, copied out of the driver, against a mocked device: a
# failed allocation is not retried at the same size, a resize
# waits while both retired slots are full, and each headset
# screen is drawn straight into the runtime's image, a repeat
# ending no chain frame.
make clean && timeout 60 make check
echo "[pass] vulkan_views_target_test"
- name: Build and run gl3_xr_draw_test
shell: bash
working-directory: samples/gfx/gl3_xr_draw
run: |
set -eu
# gfx/drivers/gl3.c's headset draw, copied out of the driver,
# with GL stubbed: each screen straight into the runtime's
# texture, and a screen moved or resized while paused drawn
# again without ending a chain frame.
make clean && timeout 60 make check
echo "[pass] gl3_xr_draw_test"
- name: GLES persistent loan pixel oracle (Mesa EGL)
shell: bash
run: |
set -eu
T=$(mktemp -d)
cc -std=gnu89 -O1 -g1 -Wall -Werror=implicit-function-declaration \
-DHAVE_OPENGLES -DHAVE_OPENGLES3 -DHAVE_EGL \
-Ilibretro-common/include -Igfx/include \
samples/gfx/gles_texture_lend/gpu_test.c gfx/common/gl_common.c \
libretro-common/glsym/glsym_es3.c libretro-common/glsym/rglgen.c \
-lEGL -lGLESv2 -o "$T/gles_lend"
EGL_PLATFORM=surfaceless LIBGL_ALWAYS_SOFTWARE=1 "$T/gles_lend"
rm -rf "$T"
- name: GLES texture loan capability and ownership (ASan + UBSan)
shell: bash
run: |
set -eu
CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1 \
python3 samples/gfx/gles_texture_lend/run.py
- name: Build and run gl2_pbo_readback_test (plain, ASan + UBSan)
shell: bash
working-directory: samples/gfx/gl2_pbo_readback
run: |
set -eu
# gfx/drivers/gl2.c's PBO readback ring against a scripted
# GL: a slot is read only once the fence placed after its
# readback has signalled, and never waited on in the map. A
# read that maps a pending slot, waits in glClientWaitSync
# or calls glFinish fails the run; teardown must delete
# every fence it issued.
make clean check
echo "[pass] gl2_pbo_readback_test"
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./gl2_pbo_readback_test
echo "[pass] gl2_pbo_readback_test (ASan)"
- name: HDR display metadata contract (ASan/UBSan)
shell: bash
working-directory: samples/gfx/hdr_metadata
run: |
set -eu
make clean
make check SANITIZER=address,undefined
CFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' python3 run.py
CFLAGS='-fsanitize=thread -fno-omit-frame-pointer' python3 run.py
CFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' python3 ownership.py
- name: Menu framebuffer loans (ASan/UBSan, TSan)
shell: bash
run: |
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
CFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' python3 samples/gfx/threaded_video/menu_framebuffer.py
CFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' python3 samples/gfx/threaded_video/vulkan_menu.py
CFLAGS='-fsanitize=thread -fno-omit-frame-pointer' python3 samples/gfx/threaded_video/menu_framebuffer.py
CFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' python3 samples/gfx/threaded_video/lend_refresh.py
CFLAGS='-fsanitize=thread -fno-omit-frame-pointer' python3 samples/gfx/threaded_video/lend_refresh.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/threaded_video/lend_refresh.py
- name: D3D Slang HDR and SwapCount bindings (ASan/UBSan)
shell: bash
working-directory: samples/gfx/slang_hdr_bindings
run: |
set -eu
# Compile both loaders' actual uniform maps from
# gfx/drivers/d3d10.c, gfx/drivers/d3d11.c and gfx/drivers/d3d12.c.
CFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' python3 run.py
CFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' python3 output_state.py
CFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' python3 peak.py
CFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' python3 min_nits.py
CFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' python3 tonemap_operator.py
CFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' python3 source_hdr.py
CFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' python3 passthrough.py
CFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' python3 ../hdr_metadata/framebuffer_format.py
CFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' python3 metal.py
CFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' python3 metal_constants.py
- name: Build and run slang_texture_index_bounds_test (ASan)
shell: bash
working-directory: samples/gfx/slang_texture_index_bounds
run: |
set -eu
# Regression test for the texture-semantic index-bounds
# fix in gfx/drivers_shader/slang_process.cpp::
# validate_texture_semantic_index(). Pre-fix only
# SLANG_TEXTURE_SEMANTIC_PASS_OUTPUT was bounded against
# reflection->pass_number; ORIGINAL_HISTORY, PASS_FEEDBACK
# and USER had no upper bound on their array index. The
# index suffix in arrayed semantic names like
# `OriginalHistory42` is parsed via strtoul in
# slang_name_to_texture_semantic_array() and propagates
# into the downstream resize_minimum() calls in
# set_ubo_texture_offset() and the direct sampler-binding
# loop. A malicious slang shader declaring
# `OriginalHistory4294967294` makes std::vector::resize
# request ~128 GiB, throwing std::bad_alloc which
# propagates unhandled out of the filter-chain create
# path, terminating the process. Reachable from any
# malicious slang preset (downloaded via Online Updater
# or shipped third-party). Build under AddressSanitizer
# so any reintroduction is caught at the bounds level.
# If slang_process.cpp amends the cap table or the
# dispatch structure, the verbatim copy in
# slang_texture_index_bounds_test.c must follow.
make clean SANITIZER=address && make -j$(nproc) all SANITIZER=address
test -x slang_texture_index_bounds_test
timeout 60 ./slang_texture_index_bounds_test
echo "[pass] slang_texture_index_bounds_test"
- name: Build and run shader_dir_cycle_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/shader_dir_cycle
run: |
set -eu
# Which folder the Next/Previous Shader hotkeys cycle, over a
# shader tree built on disk, with gfx/video_shader_parse.c
# compiled for real and only the frontend stubbed.
#
# The folder is chosen from the loaded preset, and the cases
# that decide it are all shapes the menu itself writes:
# retroarch.slangp and the core auto preset are simple
# presets (#reference), so they stand in for what they point
# at, while a preset the user loaded as itself anchors where
# it sits however it is written - "Save Preset As" emits a
# #reference too, and following those would cycle a preset's
# source folder instead of the folder it was saved into.
#
# Two orderings are what this pins. Following a reference
# one hop further than the wrapper puts the cycling in a
# folder the user never loaded from; and re-deriving the
# anchor from a preset the hotkey itself just applied walks
# back to whatever that preset references, so an entry that
# is a simple preset traps the cycling on the pair forever.
# The setting is also toggled mid-run: the list is built
# under one value and must be dropped when it changes, or
# turning it on never leaves the root it was cycling.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
test -x shader_dir_cycle_test
# The tree is built under a scratch dir, never in the repo.
T=$(mktemp -d)
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
timeout 60 ./shader_dir_cycle_test "$T"
rm -rf "${T:?}"
echo "[pass] shader_dir_cycle_test"
- name: Build and run vulkan_mailbox_init_leak_test (ASan + LSan)
shell: bash
working-directory: samples/gfx/vulkan_mailbox_init_leak
run: |
set -eu
# Regression test for the partial-init leak fix in
# gfx/common/vulkan_common.c::vulkan_emulated_mailbox_init.
# The function brings up three things in sequence (two
# eventcounts and the acquire thread; a condition variable,
# a mutex and the thread when this was written) and used to
# return `false` directly when the second or third failed,
# leaking what the earlier ones had allocated. Both
# production call sites in vulkan_create_swapchain ignore
# the return value, so an init failure also left the
# mailbox half built while VK_DATA_FLAG_EMULATING_MAILBOX
# was still set, setting up a crash the next time
# vulkan_acquire_next_image routed into the emulated path.
# Fix routes every early failure through `goto error` to a
# single deinit call, which is safe on a partly built
# mailbox and ends with a memset, leaving the struct in the
# same shape the deinit-on-shutdown path produces and
# tripping the existing `mailbox.swapchain == VK_NULL_
# HANDLE` guard at vulkan_acquire_next_image. Build under
# AddressSanitizer with leak detection so any
# reintroduction is caught at the leak level. If
# vulkan_common.c amends the init or deinit, the verbatim
# copies in vulkan_mailbox_init_leak_test.c must follow.
make clean SANITIZER=address && make -j$(nproc) all SANITIZER=address
test -x vulkan_mailbox_init_leak_test
ASAN_OPTIONS=detect_leaks=1 timeout 60 \
./vulkan_mailbox_init_leak_test
echo "[pass] vulkan_mailbox_init_leak_test"
- name: Build and run vulkan_ctx_double_free_test (ASan)
shell: bash
working-directory: samples/gfx/vulkan_ctx_double_free
run: |
set -eu
# Regression test for the double-free / use-after-free
# fix in the Vulkan context drivers' set_video_mode error
# paths: gfx/drivers_context/wayland_vk_ctx.c,
# w_vk_ctx.c, x_vk_ctx.c. Pre-fix each set_video_mode
# called its own destroy()/destroy_resources()+free() on
# ctx_data before returning false; the caller in
# gfx/drivers/vulkan.c::vulkan_init then ran
# vulkan_free()->ctx_driver->destroy(ctx_data) on the
# already-freed pointer (UAF read of struct fields, then
# a second free of the same allocation). Reachable from
# vulkan_surface_create() failure (missing extension /
# driver issue), Wayland's set_video_mode_common_*
# helpers failing, X11's XGetVisualInfo returning NULL,
# or win32_set_video_mode failing. Cocoa (cocoa_vk_ctx)
# and Android (android_vk_ctx) already handle this
# correctly by returning false without freeing -- the
# fix makes Wayland/Win32/X11 match. Build under
# AddressSanitizer so any reintroduction is caught at
# the bounds level (UAF + double-free both fire). If
# any of the three context drivers amends the
# set_video_mode error path to once again destroy
# ctx_data, the verbatim copy in
# vulkan_ctx_double_free_test.c must follow.
make clean SANITIZER=address && make -j$(nproc) all SANITIZER=address
test -x vulkan_ctx_double_free_test
timeout 60 ./vulkan_ctx_double_free_test
echo "[pass] vulkan_ctx_double_free_test"
- name: Build and run gfx_thumbnail_status_atomic_test (TSan)
shell: bash
working-directory: samples/gfx/gfx_thumbnail_status_atomic
run: |
set -eu
# Regression test for the cross-thread thumbnail status
# synchronisation in gfx/gfx_thumbnail.c. The .status
# field is read by the video thread (gfx_thumbnail_draw)
# and written by the upload-callback thread
# (gfx_thumbnail_handle_upload), with a release-store /
# acquire-load pairing that guards the texture / width /
# height fields published alongside the AVAILABLE
# status. Pre-port the file used a hand-rolled
# __atomic_* / _Interlocked / volatile shim. Post-port
# the .status field is retro_atomic_int_t and the
# accesses route through retro_atomic_*_int macros.
#
# ThreadSanitizer is the right validator for this test:
# it instruments every atomic load and store and would
# flag a missing-barrier regression as a data race, even
# on x86 TSO where the hardware otherwise hides the bug.
# ASan/UBSan would not catch barrier removal at all
# (no out-of-bounds access, no UB). The test also
# carries static-assertion checks that validate the
# struct layout invariant (.status size == sizeof(int))
# so the field's atomic type does not change the
# gfx_thumbnail_t ABI. If gfx_thumbnail.c amends
# GFX_THUMB_STATUS_STORE / GFX_THUMB_STATUS_LOAD or the
# .status field type, the verbatim copies in
# gfx_thumbnail_status_atomic_test.c must follow.
#
# STRESS_ITERS is bounded here because the default of
# 1000000 measures ~145 s on a single core before TSan
# multiplies it, and this step allows 60 s. 20000 runs
# in ~2.4 s under TSan on one core and still routes every
# round trip through the instrumented release/acquire
# pair; the default stays at 1000000 for local soak runs.
make clean SANITIZER=thread EXTRA_CFLAGS="-DSTRESS_ITERS=20000" && make -j$(nproc) all SANITIZER=thread EXTRA_CFLAGS="-DSTRESS_ITERS=20000"
test -x gfx_thumbnail_status_atomic_test
TSAN_OPTIONS=halt_on_error=1 timeout 60 \
./gfx_thumbnail_status_atomic_test
echo "[pass] gfx_thumbnail_status_atomic_test"
- name: Build and run gfx_widgets_msg_queue_race_test (TSan)
shell: bash
working-directory: samples/gfx/gfx_widgets_msg_queue_race
run: |
set -eu
# The widget message ring in gfx/gfx_widgets.c is a lock-free
# single-producer / single-consumer queue: the main thread
# publishes through msg_queue_tail, the widgets' owner pops
# through msg_queue_head, each cursor released by its writer
# and acquired by the other side, and the slot pointer rides
# that pairing. The test builds the production push and pop
# out of gfx_widgets.c itself and drives them from one producer
# thread and one consumer thread, checking order, loss, and the
# full-ring refusal. ThreadSanitizer is the validator that
# matters: a store to the slot array outside the release /
# acquire pairing, or a cursor published before its slot is
# written, is a race it reports and the value checks may not.
# halt_on_error=1 fails the run on the first race.
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
test -x gfx_widgets_msg_queue_race_test
TSAN_OPTIONS=halt_on_error=1 timeout 60 \
./gfx_widgets_msg_queue_race_test
echo "[pass] gfx_widgets_msg_queue_race_test"
- name: Build and run gfx_animation_sanitize_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/gfx_animation_sanitize
run: |
set -eu
# Regression test for the stack overflow in
# gfx/gfx_animation.c::gfx_animation_ticker(). Pre-fix it
# passed PATH_MAX_LENGTH to utf8cpy() as the destination
# size, in all five places it copies, regardless of what the
# caller had provided. utf8cpy()'s second argument is a
# size in BYTES; ticker->len, which the surrounding logic
# uses, is a width in GLYPHS -- so the guard
# `str_len <= ticker->len` compared glyph counts and then
# copied bytes, and one glyph of CJK or emoji is three or
# four of them. PATH_MAX_LENGTH is 512 and not one of the
# 27 call sites in tree provides that much (mostly
# NAME_MAX_LENGTH, 64 bytes in one RGUI case), so the
# function was licensed to write 511 bytes plus a
# terminator into any of them. Reachable from content: RGUI
# sets ticker.len from term_layout.width and ticker.s to a
# NAME_MAX_LENGTH stack buffer, so a playlist entry with a
# long enough Japanese, Cyrillic or emoji title overran it.
# Fixed by giving the struct an s_len field carrying the
# destination size in bytes, mirroring the dst_str_len that
# gfx_animation_ticker_smooth() had all along.
#
# AddressSanitizer is the validator: the test allocates
# every ticker destination at exactly the length it hands
# over, so a single byte past the end is a report rather
# than a stomp on whatever followed. It also sweeps every
# easing type through push/update/expire, frees animation
# subjects while their animation is in flight, and deinits
# with animations still running. If gfx_animation.c amends
# the destination sizing, this goes red.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
test -x gfx_animation_sanitize_test
ASAN_OPTIONS=detect_leaks=1 timeout 120 \
./gfx_animation_sanitize_test
echo "[pass] gfx_animation_sanitize_test"
- name: Build and run gfx_widgets_sanitize_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/gfx_widgets_sanitize
run: |
set -eu
# Sanitizer sweep of gfx/gfx_widgets.c, focused on the
# message queue: it has the same shape as the
# gfx_animation ticker overflow -- a caller-supplied
# string, internal fixed buffers, and arithmetic that has
# to keep glyphs and bytes apart. Multi-byte text,
# over-long text, text with no break opportunity, embedded
# newlines and empty strings across every message/title
# pairing, a 256-message overflow to drive eviction and
# free, and eight init/deinit cycles with messages still
# outstanding at teardown.
#
# Like the thumbnail sweep this pins no historical defect
# and is a tripwire. Two things decide whether it sweeps
# anything: the font metric stubs return a non-zero
# proportional width (a zero-width font makes every "does
# this fit" test succeed and the layout paths are never
# entered), and gfx_widgets_init() returning false makes
# the whole run vacuous -- hence the asserted push and
# frame counters.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
test -x gfx_widgets_sanitize_test
ASAN_OPTIONS=detect_leaks=1 timeout 120 \
./gfx_widgets_sanitize_test
echo "[pass] gfx_widgets_sanitize_test (ASan)"
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 120 \
./gfx_widgets_sanitize_test
echo "[pass] gfx_widgets_sanitize_test (TSan)"
- name: Build and run widget snapshot ownership test
shell: bash
working-directory: samples/gfx/widget_snapshot_ownership
run: |
set -eu
make clean
make
timeout 60 ./widget_snapshot_ownership_test
make clean
- name: Build and run gfx_widgets_task_bar_stack_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/gfx/gfx_widgets_task_bar_stack
run: |
set -eu
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
test -x gfx_widgets_task_bar_stack_test
ASAN_OPTIONS=detect_leaks=1 timeout 120 \
./gfx_widgets_task_bar_stack_test
echo "[pass] gfx_widgets_task_bar_stack_test (ASan)"
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 120 \
./gfx_widgets_task_bar_stack_test
echo "[pass] gfx_widgets_task_bar_stack_test (TSan)"
- name: Build and run gfx_widgets_task_gate_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/gfx/gfx_widgets_task_gate
run: |
set -eu
# A finished task retires through the widgets' push while
# the video thread frees the widget bound to it: the task
# queue must not read the link the widgets clear, and a
# muted task's empty retirement puts nothing up.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 timeout 120 ./gfx_widgets_task_gate_test
echo "[pass] gfx_widgets_task_gate_test (ASan)"
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 120 ./gfx_widgets_task_gate_test
echo "[pass] gfx_widgets_task_gate_test (TSan)"
- name: Build and run load_content_icon_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/gfx/gfx_widget_load_content_animation
run: |
set -eu
# The load content animation started off the widgets' thread
# still finds and loads its icon.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1 timeout 120 ./load_content_icon_test
echo "[pass] load_content_icon_test (ASan)"
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 120 ./load_content_icon_test
echo "[pass] load_content_icon_test (TSan)"
- 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 }}
samples-gfx-display:
name: Build and run samples/gfx (Win32, X11 and display servers)
runs-on: ubuntu-latest
timeout-minutes: 30
env:
APT_PKGS: build-essential libx11-dev libxrandr-dev libdrm-dev xserver-xorg-core xserver-xorg-video-dummy x11-xserver-utils libsdl2-dev libxinerama-dev libwayland-dev libegl-dev libgles-dev libgbm-dev gcc-mingw-w64-x86-64 wine64 openbox cmake ninja-build ccache libgl-dev xvfb libdbus-1-dev dbus python3-dbus python3-gi weston xwayland mutter libvkd3d1 mesa-vulkan-drivers fonts-dejavu-core fonts-dejavu-mono libfreetype-dev
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 win32_msg_route_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/win32_msg_route
run: |
set -eu
# What the Win32 window procedure does with a message.
# gfx/common/win32_common.c had twelve window procedures, one
# for each video family times each kind of input driver,
# built on three copies of one long switch. They are one
# procedure now, which asks win32_msg_route() what to do with
# a message. The three switches are written out in the test
# as they stood, and for every message - with and without the
# hotplug timer's id, on the desktop and on the Xbox, with
# and without threads - the route must be the steps the
# switch for that kind of input driver took. Raw input's
# keys made into keyboard events a second time, which is
# what routing them like another driver's would do, is put
# back and asserted to be caught.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 120 ./win32_msg_route_test
make clean
echo "[pass] win32_msg_route_test (ASan)"
- name: Build and run win32_screensaver_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/win32_screensaver
run: |
set -eu
# win32_suspend_screensaver() from gfx/common/win32_common.c,
# compiled from the shipping text against a fake kernel32
# that counts power requests the way Windows does. Every
# video driver init asks to hold the screensaver off, so the
# one request must be created once and set once however
# often it is asked, and turning the setting off must free
# the display. Before Windows 7 the thread execution state
# must be continuous to hold anything.
make clean
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
make SANITIZER=address,undefined check
make clean
echo "[pass] win32_screensaver_test (ASan)"
- name: Build and run win32_drag_drop_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/win32_drag_drop
run: |
set -eu
# Drag and drop from Windows 95 to current releases, through
# the setup in frontend/drivers/platform_win32.c and the drop
# handler in ui/drivers/ui_win32.c, compiled from the
# shipping text against a fake shell32, user32 and dwmapi.
# Drops must be switched on with or without dwmapi, DWM setup
# must run once, an elevated window must let the drop
# messages through where Windows filters them, and the files
# must arrive through DragQueryFileW or, where that is
# missing or reads nothing, DragQueryFileA.
make clean
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
make SANITIZER=address,undefined check
make clean
echo "[pass] win32_drag_drop_test (ASan)"
- name: Build and run display_servers_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/display_servers
run: |
set -eu
# Regression test for the XRandR query handling in
# gfx/display_servers/dispserv_x11.c. Pre-fix every query
# was treated as infallible: XRRGetOutputInfo() returns NULL
# for an output the server cannot describe and
# info->connection was read straight off it; likewise
# XRRGetCrtcInfo() and crtc->width, XRRGetScreenResources(),
# and XRRGetScreenInfo() feeding a XRRFreeScreenConfigInfo()
# that is not NULL-tolerant. On any X server advertising
# RandR without a usable output -- Xvfb, a headless box, a
# display with everything disconnected -- RetroArch died
# inside video_display_server_init() before drawing
# anything. Both orientation walks also indexed
# screen->crtcs with the output's ncrtc, reading past the
# end and then configuring a crtc other than the one whose
# geometry had just been read.
#
# The test stubs libXrandr and the handful of Xlib entry
# points the driver reaches, so it needs the headers but no
# X server, no display and no RandR extension: what is under
# test is what happens when the queries FAIL, and a working
# display is exactly the environment that cannot produce
# that. The stub numbers screen crtcs from 100 and output
# crtcs from 200 so which array was walked is read off the
# call log, and tracks every pointer it hands out so a
# double free or leak is attributed to the case that caused
# it. If dispserv_x11.c amends the query handling, this
# goes red.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
test -x display_servers_test
timeout 60 ./display_servers_test
echo "[pass] display_servers_test"
- name: Build and run dispserv_gx_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/dispserv_gx
run: |
set -eu
# The GameCube/Wii display server (dispserv_gx.c and the mode
# rules in dispserv_gx_modes.c), linked as they ship against
# stubbed libogc queries, on every TV standard: the list holds
# only modes the standard shows, one of them current, and each
# entry - through the label the menu prints and the parse the
# menu does - selects exactly that mode, stores its config id
# and reports the refresh rate it listed.
#
# The menu's parse used to run strtoul() inside
# VIDEO_SCALE_PUT_H, whose PACK evaluates its argument twice;
# the second call returned 0 and every pick from the list
# went out as Wx0. Guard the shape as well as the behaviour.
if grep -nE 'VIDEO_SCALE_(PUT_[WH]|PACK)\([^;]*strto' \
../../../menu/cbs/menu_cbs_ok.c; then
echo "::error::strtoul inside a VIDEO_SCALE macro runs twice"
exit 1
fi
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./dispserv_gx_test
make clean
- name: Build and run dispserv_ps3_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/dispserv_ps3
run: |
set -eu
# The PS3 display server (dispserv_ps3.c and the mode rules in
# dispserv_ps3_modes.c), linked as they ship against stubbed
# PSL1GHT video output queries, for displays taking different
# sets of modes with the system menu in each: the list holds
# exactly what the display takes with the mode in use marked,
# each entry - through the menu's label and parse - selects
# that mode and reinitialises the driver once, and
# ps3_display_server_resolution(), which the RSX driver
# configures the output from, follows the choice (before, the
# RSX driver ignored the setting and kept the system menu's
# mode) and falls back to the system mode when the display
# does not take it.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./dispserv_ps3_test
make clean
- name: Build and run rsx_alloc_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/rsx_alloc
run: |
set -eu
# The PS3 RSX driver (gfx/drivers/rsx_gfx.c), compiled in
# whole against the tree's PSL1GHT stand-ins, with each RSX
# allocation it makes failed in turn: init, a font and an
# overlay come up whole or not at all, a texture with no
# memory is not handed out, a core frame whose texture got
# none is not drawn, and a frame bigger than its texture's
# buffer gets a new buffer.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./rsx_alloc_test
make clean
- name: X11 window poll sends no requests (Xvfb, ASan + UBSan)
shell: bash
run: |
set -eu
# x11_check_window() and x11_has_focus() run every frame; a
# reply-bearing request there waits behind whatever else is
# queued on the connection (a software Vulkan WSI's whole
# PutImage frame). Focus comes from FocusIn/FocusOut, so the
# X contexts must select FocusChangeMask.
for f in x_ctx.c x_vk_ctx.c xegl_ctx.c; do
grep -q FocusChangeMask gfx/drivers_context/$f \
|| { echo "::error::$f does not select FocusChangeMask"; exit 1; }
done
Xvfb :97 -screen 0 1024x768x24 > /tmp/xvfb97.log 2>&1 &
XVFB_PID=$!
sleep 1
(cd samples/gfx/x11_frame_poll \
&& make clean all SANITIZER=address,undefined \
&& DISPLAY=:97 ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
timeout 60 ./x11_frame_poll_test)
kill $XVFB_PID
echo "[pass] x11_frame_poll_test"
- name: Live mode switching on an Xorg dummy server (X11 + SDL)
shell: bash
run: |
set -eu
# The stubbed display_servers_test proves the driver issues
# the right RandR calls; this proves a real X server accepts
# them. Xorg's 'dummy' driver honours XRRCreateMode /
# XRRAddOutputMode / XRRSetCrtcConfig with arbitrary timings
# (Xvfb's RandR shim returns success and lists nothing, so it
# cannot stand in). The X11 sample runs the modeline engine
# end to end - generate a 15 kHz timing, put it on the wire
# through dispserv_x11, read the crtc back and compare every
# field, switch again, restore - then the Screen Resolution
# callbacks: the listed modes, a smaller, a larger and a
# rate-only switch with the framebuffer following, an
# unlisted size refused, and back. The SDL sample switches
# a real SDL2 window among the listed modes through
# dispserv_sdl2 (and dispserv_sdl3 on an SDL3 build), for the
# modeline path and the menu picker.
# The Win32 build runs after them under Wine.
sudo Xorg :99 -config samples/gfx/display_servers_x11_live/xorg-dummy.conf \
-logfile /tmp/xorg-dummy.log -noreset -nolisten tcp -ac \
> /tmp/xorg-dummy.out 2>&1 &
XORG_PID=$!
for i in $(seq 1 30); do
DISPLAY=:99 xrandr >/dev/null 2>&1 && break
sleep 0.5
done
DISPLAY=:99 xrandr | head -3
export DISPLAY=:99
# SDL3's X11 backend makes a window fullscreen through the
# window manager and reverts the mode switch when none
# answers; a light one runs for the SDL samples.
openbox > /tmp/openbox.out 2>&1 &
sleep 1
(cd samples/gfx/display_servers_x11_live \
&& make clean all SANITIZER=address,undefined \
&& ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
timeout 120 ./display_servers_x11_live_test)
echo "[pass] display_servers_x11_live_test"
# The refresh rate a core polls every frame: answered from
# memory once the X event pump drains the connection, with no
# request to the server, and changed by the next pump after
# another client switches the mode - under a reader on a
# second thread, once under ASan+UBSan and once under TSan.
(cd samples/gfx/x11_refresh_rate \
&& make clean all SANITIZER=address,undefined \
&& ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 120 ./x11_refresh_rate_test \
&& make clean all SANITIZER=thread \
&& TSAN_OPTIONS=halt_on_error=1 timeout 120 ./x11_refresh_rate_test)
echo "[pass] x11_refresh_rate_test"
(cd samples/gfx/display_servers_sdl2_live \
&& make clean all SANITIZER=address,undefined \
&& ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
LSAN_OPTIONS=suppressions=lsan.supp \
timeout 120 ./display_servers_sdl2_live_test)
echo "[pass] display_servers_sdl2_live_test"
# The SDL3 server, audio driver and keyboard against an SDL3
# built from a pinned release (the runner image ships no
# libsdl3-dev): video, events and the dummy audio device, X11
# backend.
SDL3_VER=3.2.20
if [ ! -f /opt/sdl3/lib/pkgconfig/sdl3.pc ]; then
curl -sL -o /tmp/sdl3.tar.gz \
"https://github.com/libsdl-org/SDL/releases/download/release-$SDL3_VER/SDL3-$SDL3_VER.tar.gz"
tar -C /tmp -xzf /tmp/sdl3.tar.gz
cmake -S "/tmp/SDL3-$SDL3_VER" -B /tmp/sdl3-build -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DSDL_SHARED=ON -DSDL_STATIC=OFF \
-DSDL_TESTS=OFF -DSDL_EXAMPLES=OFF -DSDL_AUDIO=ON -DSDL_CAMERA=OFF \
-DSDL_WAYLAND=OFF -DSDL_X11=ON -DSDL_OPENGL=OFF -DSDL_OPENGLES=OFF \
-DSDL_VULKAN=OFF -DSDL_RENDER=OFF -DCMAKE_INSTALL_PREFIX=/opt/sdl3 \
> /tmp/sdl3-cmake.log
ninja -C /tmp/sdl3-build > /tmp/sdl3-build.log
sudo ninja -C /tmp/sdl3-build install > /tmp/sdl3-install.log
fi
# A 59.94 mode beside the desktop's 60, which rounds alike
xrandr --newmode "1280x720_59.94" 74.176 1280 1390 1430 1650 720 725 730 750 +hsync +vsync
xrandr --addmode DUMMY0 1280x720_59.94
(cd samples/gfx/display_servers_sdl3_live \
&& PKG_CONFIG_PATH=/opt/sdl3/lib/pkgconfig make clean all SANITIZER=address,undefined \
&& LD_LIBRARY_PATH=/opt/sdl3/lib \
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
LSAN_OPTIONS=suppressions=lsan.supp \
timeout 120 ./display_servers_sdl3_live_test)
echo "[pass] display_servers_sdl3_live_test"
(cd samples/audio/sdl3_counters \
&& PKG_CONFIG_PATH=/opt/sdl3/lib/pkgconfig make clean all SANITIZER=address,undefined \
&& LD_LIBRARY_PATH=/opt/sdl3/lib SDL_AUDIO_DRIVER=dummy \
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
LSAN_OPTIONS=suppressions=../../gfx/display_servers_sdl3_live/lsan.supp \
timeout 120 ./sdl3_counters_test \
&& make clean)
echo "[pass] sdl3_counters_test"
(cd samples/input/sdl3_keyboard \
&& PKG_CONFIG_PATH=/opt/sdl3/lib/pkgconfig make clean all SANITIZER=address,undefined \
&& LD_LIBRARY_PATH=/opt/sdl3/lib \
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
LSAN_OPTIONS=suppressions=../../gfx/display_servers_sdl3_live/lsan.supp \
timeout 120 ./sdl3_keyboard_test \
&& make clean)
echo "[pass] sdl3_keyboard_test"
# The Win32 server, its vendor timing paths and the engine
# cross-built with mingw-w64 and run under Wine on the same
# display: Wine backs EnumDisplaySettingsExA and
# ChangeDisplaySettingsExA with the server's RandR modes, so
# the CDS listed-mode switch is real; ADL, ATI legacy and
# PowerStrip are linked and must decline.
# Its own Wine prefix: one a Wine program started before the
# display was up keeps that display's single mode, and Wine
# lists only it.
WINE_LIVE_PREFIX=$(mktemp -d)
(cd samples/gfx/display_servers_win32_live \
&& make clean all \
&& WINEPREFIX=$WINE_LIVE_PREFIX WINEDEBUG=-all timeout 300 \
wine ./display_servers_win32_live_test.exe)
WINEPREFIX=$WINE_LIVE_PREFIX wineserver -k || true
rm -rf "$WINE_LIVE_PREFIX"
echo "[pass] display_servers_win32_live_test (Wine)"
# The vendor timing paths against stand-ins: a mock
# atiadlxx.dll that re-plugs the monitor after a forced set,
# a scratch HKLM key for the ATI legacy registry layout, a
# TPShidden window answering PowerStrip's messages.
(cd samples/gfx/display_servers_win32_vendor \
&& make clean all \
&& WINEDEBUG=-all timeout 300 wine ./display_servers_win32_vendor_test.exe)
echo "[pass] display_servers_win32_vendor_test (Wine)"
# The server must be back on its desktop mode with nothing
# generated left behind.
xrandr | head -3
if xrandr | grep -q 'RA-'; then
echo "generated modes left on the server"; exit 1
fi
pkill openbox || true
sudo kill "$XORG_PID" || true
- name: Screen Resolution through Mutter's DisplayConfig (ASan + UBSan, TSan)
shell: bash
run: |
set -eu
# Under GNOME's Wayland session the X11 driver runs on
# XWayland, which lists every mode at the desktop's current
# rate and only scales the window when "switched", and the
# Wayland driver had no resolution list at all; Mutter's
# org.gnome.Mutter.DisplayConfig D-Bus interface is the only
# way to the monitor's real modes. mock_mutter.py carries
# Mutter's signatures and its ApplyMonitorsConfig checks; the
# test drives gfx/common/mutter_displayconfig.c directly and
# through dispserv_x11 (real Xorg dummy server, and Xwayland
# under a headless Weston) and dispserv_wl, with and without
# Mutter on the bus. Without it every path must behave
# exactly as before: no session bus, a bus without Mutter, a
# real X server (Mutter is never asked), XWayland's own list,
# and no Wayland resolution list. Last, a real headless Mutter
# and its XWayland: Mutter names XWayland's outputs after its
# connectors (DP-1, Meta-0), so only the XWAYLAND extension
# says what the server is.
(cd samples/gfx/mutter_displayconfig \
&& make clean all SANITIZER=address,undefined \
&& ASAN_OPTIONS=detect_leaks=1 \
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
XORG="sudo Xorg" timeout 300 ./run.sh)
# The worker publishes its state by swapping a pointer and
# frees the one it replaced once no reader is inside it, with
# no lock. The threads case reads the list from four threads
# while the rate is switched back and forth; freeing the old
# state without waiting for the readers is a TSan data race.
# The bus cases alone: they need no display server.
(cd samples/gfx/mutter_displayconfig \
&& make clean all SANITIZER=thread \
&& TSAN_OPTIONS=halt_on_error=1 BUS_ONLY=1 \
setarch "$(uname -m)" -R timeout 120 ./run.sh)
echo "[pass] mutter_displayconfig_test"
- name: Build and run dbus_screensaver_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/gfx/dbus_screensaver
run: |
set -eu
# gfx/common/dbus_common.c's screensaver inhibit against a
# private session bus and a fake org.freedesktop.ScreenSaver:
# each request answered as itself and never with an older
# one's answer, three threads toggling leave what the last one
# asked, close and reopen release the old inhibit, and no
# service answers FAILED.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 300 ./run.sh
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 300 ./run.sh
echo "[pass] dbus_screensaver_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 }}
samples-gfx-d3d:
name: Build and run samples/gfx (D3D under mingw + Wine)
runs-on: ubuntu-latest
timeout-minutes: 30
env:
APT_PKGS: build-essential libx11-dev libxrandr-dev libdrm-dev xserver-xorg-core xserver-xorg-video-dummy x11-xserver-utils libsdl2-dev libxinerama-dev libwayland-dev libegl-dev libgles-dev libgbm-dev gcc-mingw-w64-x86-64 wine64 openbox cmake ninja-build ccache libgl-dev xvfb libdbus-1-dev dbus python3-dbus python3-gi weston xwayland mutter libvkd3d1 mesa-vulkan-drivers fonts-dejavu-core fonts-dejavu-mono libfreetype-dev
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: Single GL context initialization ownership
shell: bash
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
CC=gcc CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/threaded_video/gl_context_init.py
CC=clang CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/threaded_video/gl_context_init.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/threaded_video/gl_context_init.py
CC=gcc CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/threaded_video/gl_context_drain.py
CC=clang CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/threaded_video/gl_context_drain.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/threaded_video/gl_context_drain.py
CC=gcc CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/threaded_video/gl_context_move.py
CC=clang CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/threaded_video/gl_context_move.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/threaded_video/gl_context_move.py
CC=gcc CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/threaded_video/wgl_context_keep.py
CC=clang CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/threaded_video/wgl_context_keep.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/threaded_video/wgl_context_keep.py
- name: D3D menu source formats (Linux and mingw + Wine)
shell: bash
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/threaded_video/d3d_menu_format.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/threaded_video/d3d_menu_format.py
CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/threaded_video/d3d12_menu.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/threaded_video/d3d12_menu.py
CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/threaded_video/hdr_view_crop.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/threaded_video/hdr_view_crop.py
CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/threaded_video/d3d12_raw_frame.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/threaded_video/d3d12_raw_frame.py
python3 samples/gfx/d3d12_menu_mapping/run.py
- name: D3D8 resource cleanup (Linux and mingw + Wine)
shell: bash
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
make -C samples/gfx/d3d8_cleanup clean check SANITIZER=address,undefined
make -C samples/gfx/d3d8_cleanup clean check CC=x86_64-w64-mingw32-gcc EXE=.exe RUNNER=wine
make -C samples/gfx/d3d8_cleanup clean
- name: D3D11/12 shader feedback lifecycle (Linux and mingw + Wine)
shell: bash
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
make -C samples/gfx/d3d_feedback clean check SANITIZER=address,undefined
make -C samples/gfx/d3d_feedback clean check CC=x86_64-w64-mingw32-gcc EXE=.exe RUNNER=wine
make -C samples/gfx/d3d_feedback clean
- name: D3D12 mipmap generation (Linux and mingw + Wine)
shell: bash
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
make -C samples/gfx/d3d12_mipmaps clean check SANITIZER=address,undefined
make -C samples/gfx/d3d12_mipmaps clean check CC=x86_64-w64-mingw32-gcc EXE=.exe RUNNER=wine
make -C samples/gfx/d3d12_mipmaps clean
- name: D3D11 video decode backend (Linux and mingw + Wine)
shell: bash
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
# H.264, HEVC and VP9 submission buffers, byte by byte
# and against dxva.h, and the backend itself on a D3D11 that
# records what it is told. The GPU oracle builds here and
# skips; it runs on a Windows machine with a video decoder.
make -C samples/gfx/d3d11_video_decode clean check SANITIZER=address,undefined
make -C samples/gfx/d3d11_video_decode clean check CC=x86_64-w64-mingw32-gcc EXE=.exe RUNNER=wine
make -C samples/gfx/d3d11_video_decode clean
- name: Movie GPU handoff (Linux and mingw + Wine)
shell: bash
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
CFLAGS="-O1 -g -std=c89 -Wall -Wextra -fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/movie_gpu_handoff/run.py
CFLAGS="-O1 -g -std=c89 -Wall -Wextra -fsanitize=thread -fno-omit-frame-pointer" python3 samples/gfx/movie_gpu_handoff/run.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/movie_gpu_handoff/run.py
- name: D3D12 descriptor cache resource lifetime (Linux and mingw + Wine)
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
CFLAGS="-O1 -g -std=c89 -Wall -Wextra -fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/d3d12_descriptor_cache/run.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/d3d12_descriptor_cache/run.py
- name: D3D12 pass descriptor tables on their own slots (Linux and mingw + Wine)
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
CFLAGS="-O1 -g -std=c89 -Wall -Wextra -fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/d3d12_pass_tables/run.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/d3d12_pass_tables/run.py
- name: D3D12 repeated screenshots and recording slots (Linux and mingw + Wine)
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
CFLAGS="-O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/d3d12_readback_slot/run.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/d3d12_readback_slot/run.py
- name: D3D12 frame ring, one frame in flight or two (Linux and mingw + Wine)
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
CFLAGS="-O1 -g -std=c89 -Wall -Wextra -fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/d3d12_frame_ring/run.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/d3d12_frame_ring/run.py
- name: Font atlas uploads into a texture the atlas outgrew, and D3D11 failing to make one (Linux and mingw + Wine)
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
CFLAGS="-O1 -g -std=c89 -Wall -Wextra -Werror=declaration-after-statement -fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/font_atlas_upload/run.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/font_atlas_upload/run.py
- name: D3D10/11 failed maps and swapchain recovery (Linux and mingw + Wine)
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
CFLAGS="-O1 -g -std=c89 -Wall -Wextra -fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/d3d_failure_paths/run.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/d3d_failure_paths/run.py
CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/d3d_failure_paths/upload.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/d3d_failure_paths/upload.py
python3 samples/gfx/d3d_failure_paths/gpu.py
CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/d3d_failure_paths/lend.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/d3d_failure_paths/lend.py
python3 samples/gfx/d3d_failure_paths/lend_gpu.py
python3 samples/gfx/d3d_failure_paths/sw_lend_gpu.py
- name: D3D9 HLSL failed locks (Linux and mingw + Wine)
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
CFLAGS="-O1 -g -std=c89 -Wall -Wextra -fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/d3d9_failed_lock/run.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/d3d9_failed_lock/run.py
- name: GPU selection identity (Linux and mingw + Wine)
shell: bash
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
CFLAGS="-O1 -g -std=c89 -Wall -Wextra -Werror=declaration-after-statement -fsanitize=address,undefined -fno-omit-frame-pointer" python3 samples/gfx/gpu_index/run.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 samples/gfx/gpu_index/run.py
- 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 }}
samples-gfx-video:
name: Build and run samples/gfx (video, recording and Win32 under Wine)
runs-on: ubuntu-latest
timeout-minutes: 30
env:
APT_PKGS: build-essential libx11-dev libxrandr-dev libdrm-dev xserver-xorg-core xserver-xorg-video-dummy x11-xserver-utils libsdl2-dev libxinerama-dev libwayland-dev libegl-dev libgles-dev libgbm-dev gcc-mingw-w64-x86-64 wine64 openbox cmake ninja-build ccache libgl-dev xvfb libdbus-1-dev dbus python3-dbus python3-gi weston xwayland mutter libvkd3d1 mesa-vulkan-drivers fonts-dejavu-core fonts-dejavu-mono libfreetype-dev
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: Vulkan video negotiation and HEVC parameters (Linux and mingw + Wine)
shell: bash
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
make -C samples/gfx/vulkan_video_caps clean check SANITIZER=address,undefined
make -C samples/gfx/vulkan_video_caps clean check CC=x86_64-w64-mingw32-gcc EXE=.exe RUNNER=wine
make -C samples/gfx/vulkan_video_caps clean
make -C samples/gfx/vulkan_video_caps av1-session-check
make -C samples/gfx/vulkan_video_caps av1-session-check CC=x86_64-w64-mingw32-gcc EXE=.exe RUNNER=wine
make -C samples/gfx/vulkan_video_caps session-check
make -C samples/gfx/vulkan_video_caps session-check CC=x86_64-w64-mingw32-gcc EXE=.exe RUNNER=wine
make -C samples/gfx/vulkan_video_caps decode-check
make -C samples/gfx/vulkan_video_caps decode-check CC=x86_64-w64-mingw32-gcc EXE=.exe RUNNER=wine
make -C samples/gfx/vulkan_video_caps import-check
make -C samples/gfx/vulkan_video_caps import-check CC=x86_64-w64-mingw32-gcc EXE=.exe RUNNER=wine
make -C samples/gfx/vulkan_video_caps render-check
make -C samples/gfx/vulkan_video_caps render-check CC=x86_64-w64-mingw32-gcc EXE=.exe RUNNER=wine
make -C samples/gfx/vulkan_video_caps direct-check
make -C samples/gfx/vulkan_video_caps direct-check CC=x86_64-w64-mingw32-gcc EXE=.exe RUNNER=wine
make -C samples/gfx/vulkan_video_caps clean
- name: GPU recording slot ownership (Linux, mingw and sanitizers)
shell: bash
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
make -C samples/gfx/record_gpu_queue clean check
make -C samples/gfx/record_gpu_queue clean check THREADS=1 SANITIZE=-fsanitize=address,undefined
make -C samples/gfx/record_gpu_queue clean check THREADS=1 SANITIZE=-fsanitize=thread
make -C samples/gfx/record_gpu_queue clean check THREADS=1 TEST_FRAMES=20000 CC=x86_64-w64-mingw32-gcc EXE=.exe RUNNER=wine
- name: Native NVENC D3D12 encoder (portable fallback and hardware gate)
shell: bash
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
make -C samples/gfx/nvenc_d3d12 check
make -C samples/gfx/nvenc_d3d12 check CC=x86_64-w64-mingw32-gcc EXE=.exe RUNNER=wine
- name: D3D11 hardware ring under threaded video (mingw + Wine, headless)
shell: bash
run: |
set -o pipefail
# The threaded wrapper hands a D3D11 hardware core a deferred
# context and replays its frames on the video thread. Nothing
# on Linux exercises that, and it shipped showing one frame
# and then black. The real proxy and the driver's own
# FinishCommandList argument, run under Wine, where wined3d
# implements deferred contexts over OpenGL. Requires the ring
# and the immediate context to show every frame, and the ring
# as it used to be to fail.
samples/gfx/d3d11_hw_ring/run.sh
- name: libretro_d3d11.h version 2, the contract run (mingw + Wine, headless)
shell: bash
run: |
set -o pipefail
# Version 2 of the D3D11 hardware render interface lets a core
# and a frontend that presents from another thread share the
# device's one immediate context by taking turns, so the core
# keeps everything a deferred context cannot do - reading back
# above all. This runs the two against each other on a real
# D3D11 device (wined3d), and requires every frame and every
# readback to be right, and a core that ignores what
# lock_context tells it to be caught.
REQUIRE_D3D11=1 samples/gfx/d3d11_hw_interface_v2/run.sh
- name: libretro_d3d11.h version 2, turns on the context (mingw + Wine)
shell: bash
run: |
set -o pipefail
# A core that locks and unlocks the context in a tight loop
# gives way to a frontend waiting for it, parked on an event
# rather than spinning; a core that does not must be caught.
samples/gfx/d3d11_turns/run.sh
- name: libretro_d3d12.h version 2, the contract run (mingw + Wine + vkd3d, headless)
shell: bash
run: |
set -o pipefail
# Version 2 of the D3D12 hardware render interface lets a core
# hand a frame to a frontend that presents from another thread
# without the copy version 1 forces. This runs a core and such a
# frontend against each other on a real D3D12 device - vkd3d
# over Mesa's software Vulkan driver - and requires every frame
# to arrive intact, and a core that ignores wait_sync_index to
# be caught.
REQUIRE_D3D12=1 samples/gfx/d3d12_hw_interface_v2/run.sh
- name: D3D texture pokes marshal onto the video thread (mingw, object check)
shell: bash
run: |
set -eu
# Every texture poke a caller reaches with threaded=true that
# touches a device's queue or descriptor heaps goes through
# video_thread_texture_handle(); read off the compiled object's
# relocations, D3D12's compressed load included.
make -C samples/gfx/d3d_texture_marshal check
- name: Win32 window procedure on a real window (mingw + Wine, headless)
shell: bash
run: |
set -o pipefail
# gfx/common/win32_common.c had twelve window procedures and
# has one. This links the real file with stand-ins for what
# it calls, has it create its window for every video family
# and kind of input driver, sends each a script of messages,
# and compares every answer and every call made on the way
# with the trace the twelve old procedures gave on the same
# script - then makes and destroys the window through twenty
# fullscreen toggles. Any difference from the recorded trace
# fails.
samples/gfx/win32_window_proc/run.sh
python3 samples/gfx/win32_window_proc/modal_frame_run.py
- name: Build and run window_driver_test for Windows (Wine)
shell: bash
working-directory: samples/input/window_driver
run: |
set -eu
# Which input driver a Windows window gets
# (input_driver_init_windows() in
# input/input_driver_choice.c): raw input when it is the
# setting and it starts, DirectInput otherwise, including
# when raw input fails to start. Built with mingw-w64 and
# run under Wine, with stand-in drivers. The X11 and
# no-window-system choices run on the host in the input
# samples workflow.
make clean check-windows
make clean
- 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 }}
samples-gfx-kms:
name: Build and run samples/gfx (KMS, Wayland, fonts, media)
runs-on: ubuntu-latest
timeout-minutes: 30
env:
APT_PKGS: build-essential libx11-dev libxrandr-dev libdrm-dev xserver-xorg-core xserver-xorg-video-dummy x11-xserver-utils libsdl2-dev libxinerama-dev libwayland-dev libegl-dev libgles-dev libgbm-dev gcc-mingw-w64-x86-64 wine64 openbox cmake ninja-build ccache libgl-dev xvfb libdbus-1-dev dbus python3-dbus python3-gi weston xwayland mutter libvkd3d1 mesa-vulkan-drivers fonts-dejavu-core fonts-dejavu-mono libfreetype-dev
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 display_servers_kms_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/display_servers_kms
run: |
set -eu
# Regression test for two defects in
# gfx/display_servers/dispserv_kms.c::
# kms_display_server_get_resolution_list(). g_drm_connector
# is owned by gfx/common/drm_common.c and set to NULL by
# drm_free(), which runs on every context teardown -- a
# resolution change, a fullscreen toggle, a driver reinit --
# while the display server object outlives it, so the menu
# could ask for the list with no connector to describe and
# the function read straight through NULL. g_drm_mode, the
# sibling global, was already tested for exactly this a few
# lines earlier. Separately the qsort() at the end was
# passed `count`, which was declared zero and never
# assigned; `j` is the element count. So the sort ran over
# zero elements and the resolution list came back in
# whatever order the connector reported its modes -- no
# crash, no diagnostic, just an unsorted list that looks
# plausible.
#
# The test fabricates the drmModeConnector the driver reads,
# so it needs the libdrm headers but links nothing from
# libdrm: presenting a connector that is absent, empty, or
# reporting modes in an awkward order is the whole exercise
# and a real one will not do it on request. The ordering
# check feeds modes in descending size, which a working sort
# must reverse and a broken one leaves untouched. If
# dispserv_kms.c amends either, this goes red.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
test -x display_servers_kms_test
timeout 60 ./display_servers_kms_test
echo "[pass] display_servers_kms_test"
- name: Build and run drm_hdr_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/drm_hdr
run: |
set -eu
# gfx/common/drm_hdr.c: which EDIDs take HDR10 and what they
# declare, malformed ones refused without an overread, and the
# HDR_OUTPUT_METADATA blob, whose layout must be the kernel's.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./drm_hdr_test
echo "[pass] drm_hdr_test"
- name: Build and run drm_surface_lifetime_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/drm_surface_lifetime
run: |
set -eu
# gfx/drivers/drm_gfx.c's surfaces against a fake kernel: a
# core's resolution changes and the menu's sessions leave only
# the live surface's dumb buffers, framebuffers and mappings,
# the framebuffer the plane scans out is never removed from
# under it, and the driver's free leaves nothing - LeakSanitizer
# holds the plane queries' libdrm objects to the same. Rows the
# kernel pads are written at its stride, each frame read at its
# own pitch, the next page lent to the core and flipped without
# a copy, a dupe survived, and no allocation made per flip. The
# menu scans out RGUI's RGBA4444 as it is where the plane takes
# it, is lent to RGUI, follows the frame's size, and asks the
# plane once rather than every menu frame. Every framebuffer is
# made in its surface's own format, which no legacy depth names
# for RGBX4444. The input overlay goes on a plane of its own,
# raised above the frame's and blending premultiplied, covering
# only its images, composed when something changes and flipped
# in the frame's own commit; where no plane can take it, it is
# blended into the frame. Init passes over a card with no
# display, keeps the console's mode or a fullscreen size's,
# switches mode with the planes, flips without atomic commits
# where the card has none, fails cleanly with no plane for
# frames, and the free turns the plane off and gives the
# console back its framebuffer and mode. A core's rotation is
# made by the plane where its rotation property takes it, and
# the frame still lent; otherwise, or under a blended overlay,
# the copy turns it and nothing is lent. The menu is shown
# unturned, the plane is left unturned, and a screenshot reads
# the frame back as it is shown. Without vsync, commits do not
# wait: one per vblank, none refused, a frame that comes before
# the last is on screen written over, the page written next
# never the one on screen or on its way; a resize, the menu, a
# mode switch and the free wait for the flip first, and vsync
# turned back on waits again. The threaded wrapper's frame
# memory is scanned out where a frame lies, from inside a
# buffer too, never written while shown or on its way, freed
# only once the plane has left it, and copied where the display
# refuses the pitch or the copy turns the frame. The CRT
# switch's modeline is set at init and on a mode switch, HDMI
# timings from the config, malformed ones passed over, and the
# aspect the switch puts taken up with the next frame; content
# running under the menu is never flipped over it, and its last
# frame goes up when the menu closes. On-screen messages are
# drawn into the frame where they are seen, at the size the
# screen shows, upright on a frame the plane turns, and a frame
# in the wrapper's memory is copied to be drawn on. A blended
# overlay is blended into a core's views as into a frame. The
# driver is built with sign-compare as an error and linked
# without rthreads, so it stays clean of both. A turn goes in
# the commit that places the plane, never on its own, and a
# frame placed otherwise in a frame memory buffer still shown
# is copied rather than its framebuffer removed.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./drm_surface_lifetime_test
make clean
echo "[pass] drm_surface_lifetime_test"
- name: Build and run drm_crtc_pick_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/drm_crtc_pick
run: |
set -eu
# gfx/common/drm_common.c against a fake kernel: a connector
# nothing drives yet (a display plugged in after boot) gets an
# encoder and a CRTC that is off, a CRTC that was off is
# switched off again on restore, a size the display lacks falls
# back to mode 0, only a display in use going away (or
# coming back) restarts the video driver on a hotplug, and the
# flip wait notes a hotplug in its own poll without spinning on
# the unread socket.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./drm_crtc_pick_test
make clean
echo "[pass] drm_crtc_pick_test"
- name: Build and run exynos_g2d_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/exynos_g2d
run: |
set -eu
# gfx/drivers/exynos_gfx.c against a fake kernel and a G2D that
# draws into the GEM buffers: the frame at its aspect ratio, a
# core rendering into the buffer the G2D reads, vsync off
# neither waiting nor queueing flips, messages and statistics
# cleared and blended over only what they cover, the menu's
# alpha, screenshots, mode switches and CRT modelines, and a
# free and a failed init that leave nothing behind. Behind an
# IOMMU, frames read from the core's own memory - the kernel
# reuses a mapping unsynced, so a frame must never come
# through one - and the input overlay blended by the G2D,
# mirrored where its quad is; a core's rotation, turned as
# the frame is copied.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./exynos_g2d_test
make clean
echo "[pass] exynos_g2d_test"
- name: Build and run egl_gbm_frames_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/egl_gbm_frames
run: |
set -eu
# gfx/common/egl_gbm_frames.c against a fake GBM, EGL and GPU:
# a format the GPU samples where the CPU writes it is taken, one
# mapped through staging memory refused for the rest of the run;
# a buffer a texture holds, one a draw may still read and the
# last frame's are never lent; the check puts the GL state back,
# and the free leaves no buffer, map, image, fence or fd.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./egl_gbm_frames_test
make clean
echo "[pass] egl_gbm_frames_test"
- name: egl_gbm_frames check on a real GL (Mesa EGL, GLES 2 and desktop GL)
shell: bash
run: |
set -eu
# The check gfx/common/egl_gbm_frames.c runs before a format is
# first used, on llvmpipe: GBM and the dma-buf import are
# stand-ins, the shader, target, draw and readback the GL's own.
# A buffer sampled as written passes in both formats; one whose
# writes are never seen fails, and is not tried again.
T=$(mktemp -d)
for api in es2 gl; do
if [ $api = es2 ]; then
D="-DHAVE_OPENGLES -DHAVE_OPENGLES2"
L="libretro-common/glsym/glsym_es2.c -lEGL -lGLESv2"
else
D="-DHAVE_OPENGL"
L="libretro-common/glsym/glsym_gl.c -lEGL -lGL"
fi
cc -std=gnu89 -O1 -g1 -Wall -Werror=implicit-function-declaration \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-DHAVE_KMS -DHAVE_EGL $D -I. -Ilibretro-common/include \
-Igfx/include $(pkg-config --cflags libdrm) \
samples/gfx/egl_gbm_frames/mesa_check.c gfx/common/egl_gbm_frames.c \
libretro-common/glsym/rglgen.c $L \
-Wl,--wrap=eglGetProcAddress,--wrap=eglQueryString,--wrap=glDrawArrays \
-o "$T/check_$api"
for mode in seen blind; do
EGL_PLATFORM=surfaceless LIBGL_ALWAYS_SOFTWARE=1 \
ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=halt_on_error=1 \
"$T/check_$api" $mode
done
done
rm -rf "$T"
- name: Build and run gl2_kms_frames_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/gl2_kms_frames
run: |
set -eu
# gfx/drivers/gl2.c's frames in scanout memory against a
# scripted egl_gbm_frames: on KMS a plain software frame is lent
# from the buffers, never to a core that reads it back, a
# hardware core, mipmaps or a 10-bit source; a lent frame becomes
# the frame texture's storage with no upload, and any other gets
# the texture's own storage back before it goes up.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./gl2_kms_frames_test
make clean
echo "[pass] gl2_kms_frames_test"
- name: Build and run wayland_wlr_output_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/wayland_wlr_output
run: |
set -eu
# gfx/common/wayland_wlr_output.c, the Wayland display server's
# resolution list and mode switch on wlroots compositors, against
# an in-process compositor that checks every configuration names
# each head exactly once, as the protocol requires.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./wayland_wlr_output_test
echo "[pass] wayland_wlr_output_test"
- name: Build and run wayland_cursor_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/wayland_cursor
run: |
set -eu
# input/common/wayland_cursor.h: the size and scale the Wayland
# context loads its cursor theme at when the compositor has no
# cursor-shape-v1 - XCURSOR_SIZE honoured, fractional scales
# rounded up to a whole buffer scale.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./wayland_cursor_test
echo "[pass] wayland_cursor_test"
- name: Build and run wayland_resize_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/wayland_resize
run: |
set -eu
# gfx/common/wayland_resize.h, what the Wayland GL and Vulkan
# contexts do once their buffers are resized, against an
# in-process compositor: configures count again whatever the
# scaling, and the buffer scale goes only to a surface on the
# whole-scale path that is new enough to take it.
export XDG_RUNTIME_DIR=$(mktemp -d)
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./wayland_resize_test
echo "[pass] wayland_resize_test"
- name: Build and run video_mode_select_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/video_mode_select
run: |
set -eu
# gfx/common/video_mode_select.c: which listed mode a size and
# rate name, and the order a resolution list is shown in - the
# code every display server's mode switch goes through.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./video_mode_select_test
echo "[pass] video_mode_select_test"
- name: Build and run wayland_kwin_output_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/wayland_kwin_output
run: |
set -eu
# gfx/common/wayland_kwin_output.c, the Wayland display server's
# resolution list and mode switch on KWin, against an in-process
# compositor speaking KWin's output protocols in both the
# registry form and the older per-output globals.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./wayland_kwin_output_test
echo "[pass] wayland_kwin_output_test"
- name: Build and run wayland_color_management_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/wayland_color_management
run: |
set -eu
# gfx/common/wayland_color.c, which tags an HDR GL surface as
# Windows-scRGB, against an in-process compositor that records
# every fatal wp_color_manager_v1 rule the client could break.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./wayland_color_management_test
echo "[pass] wayland_color_management_test"
- name: Build and run wayland_present_test (TSan, ASan + UBSan)
shell: bash
working-directory: samples/gfx/wayland_present
run: |
set -eu
# gfx/common/wayland_present.c against an in-process
# compositor: presentation feedback and the GL frame callback
# live on their own event queues, so the main thread
# dispatching input never runs them while the video thread
# requests, waits on and frees them.
# TSan catches a feedback event dispatched off that thread;
# ASan the feedback left behind at teardown.
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 60 ./wayland_present_test
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./wayland_present_test
echo "[pass] wayland_present_test"
- name: Build and run wayland_drm_lease_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/wayland_drm_lease
run: |
set -eu
# The DRM lease path in gfx/common/wayland_drm_lease.c and
# gfx/display_servers/dispserv_wl.c, driven by a test
# compositor on libwayland-server: what is offered, which
# connector an index picks, an index past the offer, a
# refusal, a revoke arriving while the client is idle, and
# no Wayland session at all. The leased descriptor and the
# device's non-master fd are the ones a mistake leaks, so
# the lane runs under ASan. Both protocol halves are
# generated here from deps/wayland-protocols, which needs
# wayland-scanner and the client and server libraries -
# libwayland-dev carries all three.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
test -x wayland_drm_lease_test
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
timeout 60 ./wayland_drm_lease_test
echo "[pass] wayland_drm_lease_test"
- name: Build and run modeline_edid_parse_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/modeline_edid_parse
run: |
set -eu
# The EDID reader in gfx/modeline/modeline_edid.c, fed the
# block the writer produces (round trip), a hand-built EDID
# 1.4 with CTA-861 and DisplayID 2.0 extensions checked
# field by field, the defect cases (short, wrong header, bad
# checksums, a declared but missing extension, data block
# and section lengths running past their block), and a
# deterministic byte-noise sweep. The parser is handed
# whatever an OS reader returns, so every length and offset
# in the data is hostile; under ASan a read past the buffer
# is the failure this lane exists to catch.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
test -x modeline_edid_parse_test
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
timeout 60 ./modeline_edid_parse_test
- name: Build and run integer_scale_axis_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/integer_scale_axis
run: |
set -eu
# video_viewport_get_scaled_integer(), pulled out of
# gfx/video_driver.c by extract_integer.awk, swept over
# viewport sizes, frame sizes, every axis mode that touches
# X and every scaling mode. Y+X has to be integer on both
# axes: the X axis was once multiplied from the
# aspect-corrected width (256x224 at 64:49 is 293), giving
# 1172x896 at 1080p - a 4.58x horizontal scale and uneven
# pixel columns - on every video driver.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
test -x integer_scale_axis_test
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
timeout 60 ./integer_scale_axis_test
make clean
- name: Build and run set_rotation_deferred_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/set_rotation_deferred
run: |
set -eu
# video_driver_set_rotation(), pulled out of gfx/video_driver.c
# by extract_set_rotation.awk. SET_ROTATION from retro_load_game
# arrives before the driver instance exists and must still
# report success (issue #19957), without touching the instance.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
test -x set_rotation_deferred_test
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
timeout 60 ./set_rotation_deferred_test
make clean
- name: Build and run coord_array_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/coord_array
run: |
set -eu
# The coordinate array, pulled out of gfx/video_driver.c by
# extract_coord_array.awk: appends hold what was appended,
# and an append onto an empty array or one that has to grow,
# with each of its allocations failed in turn, reports the
# failure, keeps what the array held, and frees clean.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./coord_array_test
make clean
- name: Build and run slang_cache_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/slang_cache
run: |
set -eu
# The SPIR-V cache: slang_cache.c serialises entries in
# memory and video_shader_parse.c stores them - mapped or read
# in one go, written whole by rename. The test round-trips an
# entry, checks the bytes are the layout caches have always
# had, turns away every truncated prefix and a set of corrupt
# entries, and keeps keys from naming anything outside the
# cache. Built mapped (HAVE_MMAP) and reading.
make clean SAN=address,undefined && make -j$(nproc) all SAN=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \
timeout 300 make check
make clean
- name: Build and run gfx_display_mesh_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/gfx_display_mesh
run: |
set -eu
# The menu mesh facility in gfx_display.c against a display
# driver that records what it is handed: mesh creation and
# its refusals, the CPU path a plain program takes on drivers
# that do not draw meshes (the transform, the perspective
# divide, triangles joined into one strip, triangles behind
# the eye dropped, the tint), each effect program reaching the
# driver's pipeline under its id, and nothing handed to a
# driver that takes no strips.
make clean && make -j$(nproc) all
timeout 120 make check
make clean
- name: Build and run font_lifecycle_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/font_lifecycle
run: |
set -eu
# Where a font is created follows the threading hint; where
# it is REBUILT does not - a rebuild marshals whenever the
# wrapper is up, because a GL backend making its context
# current on the main thread while the video thread holds it
# is an X BadAccess. Nothing ran this sample, and it had the
# older rule written into it.
# The fallback tests read DejaVu in place of the assets'
# fallback fonts: Sans for fallback-font.ttf, and Sans Mono
# and Sans as a general and a second face for the chain test.
# vg and sdl2 draw on-screen messages through the shipping
# OpenVG and SDL2 drivers - OpenVG onto a software OpenVG in
# the test, SDL2 through its own software renderer - and check
# every glyph texel lands where it should; omap blends
# FreeType's glyphs into a page exactly its pixels in size,
# so a glyph clipped off its edge that is blended anyway is
# an overflow the sanitizer reports.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 120 make check SANITIZER=address,undefined
make clean
# A fallback font is read on a thread of its own while two
# fonts on two threads ask for it, and a second face behind
# the first.
make fallback chain SAN=thread
TSAN_OPTIONS=halt_on_error=1 setarch "$(uname -m)" -R \
timeout 120 ./fallback
TSAN_OPTIONS=halt_on_error=1 setarch "$(uname -m)" -R \
timeout 120 ./chain
make clean
- name: Build and run overlay_apng_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/gfx/overlay_apng
run: |
set -eu
# An animated overlay image is a surface with slots whose
# texture is updated in place, so the pages' handles stand
# while its frames advance. The sample builds its own APNG
# fixture and holds that contract: distinct frames, a pass
# that ends and replays, one texture across the submits, and
# a free with a frame still in flight.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 120 ./overlay_apng_test fixtures/anim.png
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 120 ./overlay_apng_test fixtures/anim.png
make clean
- name: Build and run gfx_thumbnail_anim_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/gfx/gfx_thumbnail_anim
run: |
set -eu
# Regression test for the two faults that froze animated
# thumbnails in the file browser, in gfx/gfx_thumbnail.c.
#
# gfx_thumbnail_request_file malloc'd its tag and never set
# tag->path, so gfx_thumbnail_anim_open - which
# gfx_thumbnail_handle_upload feeds that field for every
# load with no video stream to adopt - read uninitialised
# heap and returned without touching the file. Undefined
# behaviour rather than a stable failure: a freed tag is
# the size of the next one, so the allocator commonly
# handed back a block still holding the previous entry's
# path, which opened an animation and looked like the
# feature working. gfx_thumbnail_request fills the field
# for playlist entries, which is why playlists animated and
# the browser did not.
#
# Separately, anim_install marked windowed animations
# 'read pending' via !data_transfer_complete(). A window
# never completes - done stays clear for its whole life -
# and the pump animate() uses (iterate_while) declines
# windows by design, so pending forced the pump branch, the
# pump was a no-op, and animate() returned before ever
# advancing a frame, every vsync, forever. That is every
# path-based open: animated WEBP, APNG, and any WEBM/MP4
# with no still stream to adopt.
#
# The oracle links the real gfx_thumbnail.c and measures
# animation as DISTINCT FRAMES REACHING THE TEXTURE - the
# video-driver stub CRCs every upload - not as internal
# flags, which is what every earlier test in this area got
# wrong and why neither fault was caught. A 188-byte
# 3-frame lossless WEBP is embedded, so there are no
# fixtures. One lane drives anim_open with a garbage path
# and requires no stream; one opens the real animation and
# requires multiple distinct uploads within bounded paced
# vsyncs, naming the windowed+pending deadlock explicitly
# before the timeout would. Both pre-fix trees score zero
# uploads in 240 vsyncs. If gfx_thumbnail.c amends the
# pending decision or the request plumbing, this goes red.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
test -x gfx_thumbnail_anim_test
ASAN_OPTIONS=detect_leaks=1 timeout 120 \
./gfx_thumbnail_anim_test
echo "[pass] gfx_thumbnail_anim_test (ASan)"
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 120 \
./gfx_thumbnail_anim_test
echo "[pass] gfx_thumbnail_anim_test (TSan)"
- name: Build gfx_thumbnail_anim_test with mingw-w64
shell: bash
working-directory: samples/gfx/gfx_thumbnail_anim
run: |
set -eu
# Regression tests build for Windows too.
make clean
make CC=x86_64-w64-mingw32-gcc TARGET=gfx_thumbnail_anim_test.exe
test -f gfx_thumbnail_anim_test.exe
make clean TARGET=gfx_thumbnail_anim_test.exe
- name: Build and run gfx_surface_still_test (ASan + UBSan, mingw-w64)
shell: bash
working-directory: samples/gfx/gfx_surface_still
run: |
set -eu
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
# A still keeps the texture up until the new one has landed,
# updates a same-size image in place, and is freed by the
# completion of an upload or decode still out when it goes.
make clean
make EXTRA_CFLAGS="-fsanitize=address,undefined" LDFLAGS="-fsanitize=address,undefined"
make check
make clean
make CC=x86_64-w64-mingw32-gcc TARGET=gfx_surface_still_test.exe
test -f gfx_surface_still_test.exe
make clean TARGET=gfx_surface_still_test.exe
CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" python3 two_frame.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 two_frame.py
CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" python3 remap.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine python3 remap.py
- name: Build and run badge_cache_test (ASan + UBSan)
shell: bash
working-directory: samples/cheevos/badge_cache
run: |
set -eu
# rcheevos_get_badge_texture() never reads, decodes or
# uploads on the calling thread, so the achievement widgets
# can ask from the video thread. The test links the real
# cheevos/cheevos_badge.c with the task and upload stubbed
# as steppable queues and checks: nothing synchronous on
# the first ask, one decode per badge, the upload posted
# only from the decode's completion, the handle handed out
# exactly once and owned by the taker, locked and unlocked
# as separate keys, a missing file requesting a download
# only when asked and loading when the download lands, a
# failed load not retried per ask, a reset unloading ready
# handles and orphaning in-flight ones (their late delivery
# unloaded, never handed to whatever reused the slot), an
# off-main-thread ask only recorded until the main thread's
# service starts it, the default badge lent rather than
# given, a 0 reported as a short wait only while a file on
# disk is loading, and a refused async upload treated as a
# failure.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
test -x badge_cache_test
ASAN_OPTIONS=detect_leaks=1 timeout 60 ./badge_cache_test
echo "[pass] badge_cache_test (ASan)"
make clean
- name: Build and run progress_bounds_test (ASan + UBSan)
shell: bash
working-directory: samples/cheevos/progress_bounds
run: |
set -eu
# Achievement progress comes back from a save state or a
# netplay host, so its bytes are not the runtime's own. The
# bundled rcheevos reader gets blobs in heap buffers of
# exactly their size: every truncation of a real one, a chunk
# length that wraps the offset back before itself, and a
# digest cut short. A read past a blob fails under ASan; the
# wrap would loop forever, which the timeout ends.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 timeout 60 ./progress_bounds_test
make clean
make CC=x86_64-w64-mingw32-gcc TARGET=progress_bounds_test.exe
test -f progress_bounds_test.exe
make clean TARGET=progress_bounds_test.exe
echo "[pass] progress_bounds_test (ASan + UBSan, and a mingw-w64 build)"
- name: Build and run psp1_gfx_contracts_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/psp1_gfx_contracts
run: |
set -eu
# gfx/drivers/psp1_gfx.c is reached only by the griffin unity
# build for PSP, so nothing else here compiles it, let alone
# runs it. This links the real driver against a stand-in for
# the parts of the machine its correctness turns on: main RAM
# and VRAM each mapped twice so the cached and uncached
# aliases genuinely alias, a no-access page after VRAM so a
# blit off the end faults, display lists that stay the GE's
# until they are waited on, and a vblank sub-interrupt that
# can be fired from inside free().
#
# What it holds: the 24bpp BGR byte order video_driver.h
# specifies for read_viewport, filling the whole buffer the
# screenshot task hands over, releasing the interrupt handler
# before the driver struct it writes through, keeping the GE
# off a display list or framebuffer that is in use, both blits
# staying inside the buffers they target, cache-line ownership
# of everything the GE writes, and init failing cleanly when
# an allocation does.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
rc=0
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
timeout 120 ./psp1_gfx_contracts_test || rc=$?
# 77: the runner would not hand over the PSP address windows.
if [ "$rc" = 77 ]; then
echo "[skip] PSP address windows unavailable"
exit 0
fi
[ "$rc" = 0 ]
make clean
- name: Shared media transport controls (C89, ASan, UBSan, TSan)
shell: bash
working-directory: samples/gfx/media_controls
run: |
set -eu
make -B check THREADS=0
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1 make -B check SANITIZER=address,undefined
TSAN_OPTIONS=halt_on_error=1 make -B check SANITIZER=thread
- name: MinGW strict C89 float math
run: CC=x86_64-w64-mingw32-gcc WINEDEBUG=-all python3 samples/gfx/mingw_c89_math/run.py
- name: Vulkan synchronous readback slot
run: python3 samples/gfx/vulkan_readback_slot/run.py
- name: Capture bounds, conversion failures and list allocation rollback
shell: bash
run: |
set -eu
for sample in samples/gfx/video_frame_failure libretro-common/samples/lists/file_list_oom libretro-common/samples/lists/string_split_packed libretro-common/samples/gfx/scaler_point libretro-common/samples/gfx/image_scale_transfer samples/tasks/translation_reply; do
make -C "$sample" clean
make -C "$sample" check SANITIZER=address,undefined
make -C "$sample" clean
make -C "$sample" check CC=x86_64-w64-mingw32-gcc EXE=.exe RUNNER=wine
make -C "$sample" clean
done
make -C samples/tasks/translation_reply check RPNG=0 SANITIZER=address,undefined
make -C samples/tasks/translation_reply clean
make -C samples/tasks/translation_reply check RPNG=0 CC=x86_64-w64-mingw32-gcc EXE=.exe RUNNER=wine
make -C samples/tasks/translation_reply clean
python3 samples/gfx/vulkan_capture_guard/run.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine WINEDEBUG=-all python3 samples/gfx/vulkan_capture_guard/run.py
python3 samples/gfx/video_frame_failure/translation.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine WINEDEBUG=-all python3 samples/gfx/video_frame_failure/translation.py
SANITIZER=address,undefined python3 samples/gfx/video_frame_failure/screenshot.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine WINEDEBUG=-all python3 samples/gfx/video_frame_failure/screenshot.py
python3 samples/menu/entry_alloc/run.py
CC=x86_64-w64-mingw32-gcc RUNNER=wine WINEDEBUG=-all python3 samples/menu/entry_alloc/run.py
- name: D3D11 resized readback destination guard
run: python3 samples/gfx/d3d11_readback_resize/run.py
- name: Build and run hw_ring_fence_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/gfx/hw_ring_fence
run: |
set -u
set -o pipefail
# The hardware ring's fence handoff between the video thread
# and the core's thread (gfx/video_thread_hw_fence.h), the
# tree's own header, with two threads at one slot: a fence is
# signalled only while idle, waited only once signalled, and
# never inside two calls at once - with no lock. The single
# re-signalled fence it replaced is built too and must fail.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
timeout 120 make check SANITIZER=address,undefined
echo "[pass] hw_ring_fence_test (ASan + UBSan)"
# The two threads share nothing but atomics, and TSan is
# what says so.
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 120 make check SANITIZER=thread
echo "[pass] hw_ring_fence_test (TSan)"
make clean
- name: Build and run hw_ring_queue_test (ASan + UBSan, TSan)
working-directory: samples/gfx/hw_ring_queue
shell: bash
run: |
set -euo pipefail
make clean
make check SANITIZER=address,undefined
make clean
make check SANITIZER=thread
make clean
- 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 }}
thumbnail-preview:
name: The file browser's video preview (gfx_thumbnail_preview, ASan + UBSan)
runs-on: ubuntu-latest
# The ASan + UBSan builds, and the windowed-animation harness over
# ~70 MB of lossless frames; the TSan build runs beside it in
# thumbnail-preview-tsan.
timeout-minutes: 20
env:
APT_PKGS: ffmpeg ccache zlib1g-dev
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: 15
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 gfx_thumbnail_preview_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/gfx_thumbnail_preview
run: |
set -eu
# Regression test for the file-browser video preview in
# gfx/gfx_thumbnail.c, covering the windowed open, the
# anim-first status/alpha handshake, and the preview-audio
# hand-off to libretro-common/audio/audio_mixer.c.
#
# Every fault this pins was invisible to a decode-level
# test, which is why they shipped one after another:
#
# - the windowed open produced a texture and reached
# AVAILABLE but nothing raised alpha, so every video
# decoded perfectly and drew at zero opacity;
# - a LEADING moov left the demuxer's avail below the
# media floor, so the first sample was unreachable, and
# since the feeder anchors at tell==0 nothing ever
# raised the bound - a standstill with no way out;
# - a TRAILING moov cannot be reached by a resident head
# at all, so preview audio was silent on every such
# file until the moov island was committed instead;
# - the mixer hand-off copied the whole container out of
# a partially-committed window (SIGSEGV), took an
# int32_t length that truncated a 7 GB container to
# 3.5 GB, dropped every feeder raise for want of an
# M4A arm in audio_mixer_voice_set_avail (the same
# second of audio on a loop), and applied the resident
# bound only under HAVE_ROPUS.
#
# So this links the REAL gfx_thumbnail.c AND the REAL
# audio_mixer.c. The earlier harness stubbed the mixer and
# was therefore modelling the two functions three of those
# defects lived in. The stubbed audio thread consumes at a
# settable rate and TOUCHES every byte it is told is
# resident, so an over-claimed avail faults under ASan
# rather than passing quietly, and it counts feeder stalls
# (excluding the file end, where hitting the wall is the
# loop point rather than a starved feeder).
#
# The fixtures are container LAYOUTS, not clips - where the
# moov sits, whether media starts past 4 GiB, whether the
# moov outgrows the head - because layout is what every one
# of these faults turned on. The multi-gigabyte ones are
# sparse: two 7.28 GB files cost ~400 KB of disk.
#
# The windowed-animation harness below decodes its two
# lossless fixtures three times each; they are sized just past
# what the feeder may keep resident (~43 MB and ~39 MB), and
# mkfixtures.py reuses an existing set, so the second
# "make fixtures" (TSan pass) costs nothing. ~55 s under
# ASan on one core.
# Frame counts are set from measurement, not taste. At 300
# ASan / 200 TSan frames this step took ~5.6 min on one
# core, against a job budget shared with a dozen others;
# 120 / 60 brings it to ~1.5 min and still crosses every
# window boundary the assertions rest on (the feeder moves
# at AUDIO_BYTES_PER_TICK, not per frame). The job timeout
# goes to 20 min to leave headroom for the slowest runner.
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
make fixtures
test -x gfx_thumbnail_preview_test
ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 \
UBSAN_OPTIONS=print_stacktrace=1 \
AUDIO_BYTES_PER_TICK=65536 FRAMES=120 timeout 300 \
./gfx_thumbnail_preview_test fixtures/*.mp4 fixtures/anim_lossless.webp
echo "[pass] gfx_thumbnail_preview_test (ASan)"
mkdir -p fixtures/packet_span
ffmpeg -v error -y -f lavfi \
-i 'testsrc2=size=1280x720:rate=1,noise=alls=100:allf=t+u:all_seed=10' \
-t 3 -c:v libx264 -preset ultrafast -qp 0 -pix_fmt yuv420p \
-moov_size 16777216 fixtures/packet_span/large.mp4
for bin in gfx_thumbnail_preview_test gfx_thumbnail_preview_test_threaded; do
ASAN_OPTIONS=detect_leaks=1 MP4_PACKET_SPAN=1 timeout 120 \
./$bin fixtures/packet_span/large.mp4
done
# HDR previews: with HDR_OUTPUT the stub driver keeps half
# floats and shows them as linear scRGB, and the PQ-tagged
# fixture must animate as them on the PQ EOTF of its grey; its
# SDR twin, and the HDR one without HDR output, must not upload
# a single half-float frame. Both decode paths: the poll's own
# here, the worker's jobs in the threaded build below.
HDRFX=../../formats/video_hdr_fp16/fixtures
for bin in gfx_thumbnail_preview_test gfx_thumbnail_preview_test_threaded; do
ASAN_OPTIONS=detect_leaks=1 NOAUDIO=1 THREADS=4 FRAMES=60 \
HDR_OUTPUT=1 HDR_EXPECT=1 timeout 120 ./$bin $HDRFX/hdr_hevc.mp4
ASAN_OPTIONS=detect_leaks=1 NOAUDIO=1 THREADS=4 FRAMES=60 \
HDR_OUTPUT=1 timeout 120 ./$bin $HDRFX/sdr_hevc.mp4
ASAN_OPTIONS=detect_leaks=1 NOAUDIO=1 THREADS=4 FRAMES=60 \
timeout 120 ./$bin $HDRFX/hdr_hevc.mp4
done
echo "[pass] gfx_thumbnail_preview HDR lanes"
# The same session with HAVE_THREADS: the thumbnail decodes on
# its worker and the pool, the app's own path, paced at a
# display's rate with a bound on the longest stall.
test -x gfx_thumbnail_preview_test_threaded
ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 \
UBSAN_OPTIONS=print_stacktrace=1 \
THREADS=4 FRAMES=180 PACE_HZ=60 PACE_MAX_STALL_MS=800 NOAUDIO=1 timeout 300 \
./gfx_thumbnail_preview_test_threaded fixtures/bframes_h264.mp4 fixtures/bframes_hevc.mp4
echo "[pass] gfx_thumbnail_preview_test_threaded (ASan, 4 threads)"
# The frame-indexed types (animated WEBP, APNG) through the
# same session, windowed: animated WEBP had no progressive
# open, so the session read the whole file into the
# reservation (for every still WEBP thumbnail too) and
# charged one window for it; APNG had the open but no byte
# cursor, so the feeder never moved past the 12 MB primed at
# open and a longer animation looped early forever. Mapping
# residency is measured with mincore, so the bound holds
# under the sanitizers. ASan+UBSan only: the harness is
# single-threaded (no TSan target), and the lossless decode
# of ~900 720p frames takes it past the job budget under
# TSan (>5 min) for no coverage.
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
timeout 60 ./preview_av1_hw_test
test -x preview_anim_window_test
ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 \
# V1 in this run plays long_video.mp4 (60 s at 4 Mbit/s,
# 29 MB, generated by mkfixtures) end to end through the
# session and requires the resident bytes to stay inside
# the bitrate-sized window: measured 5.25 MiB peak against
# the 20 MiB the fixed 4+8+8 MiB constants held before.
UBSAN_OPTIONS=print_stacktrace=1 timeout 600 \
./preview_anim_window_test fixtures
echo "[pass] preview_anim_window_test (ASan)"
# The decode test is the missing half of the harness above:
# the REAL mixer -> audio_transfer -> rmp4 -> raac chain over
# a budget-fed window, at budgets small enough that the
# decoder reaches the resident frontier. The stubbed audio
# thread above models exactly the layer these faults lived
# in: no wall over the packets on the trailing-moov path
# (SIGSEGV inside raac_decode_frame the moment the decoder
# outran the feeder), demuxer bounds that refused to lower,
# a wall stall taken for end of stream (mid-file rewinds,
# then the voice released), and set_avail compiling to a
# no-op without HAVE_RWEBM - the _norwebm build exists for
# that last one.
test -x preview_audio_decode_test
test -x preview_audio_decode_test_norwebm
ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 600 bash -c '
./preview_audio_decode_test fixtures/trailing_large.mp4 1024 island
./preview_audio_decode_test fixtures/trailing_large.mp4 262144 island
./preview_audio_decode_test_norwebm fixtures/trailing_large.mp4 1024 island
./preview_audio_decode_test_norwebm fixtures/trailing_large.mp4 262144 island'
echo "[pass] preview_audio_decode_test (ASan)"
# The threaded decode against the single-threaded one, frame
# for frame: the colour conversion in row bands on a pool,
# for the VP9 fixture (four tile columns) the columns decoded
# side by side, for the HEVC fixture (WPP) the CTB rows on a
# wavefront. Every frame must be byte-identical.
test -x preview_blit_bands_test
ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 300 bash -c '
./preview_blit_bands_test fixtures/long_video.mp4
./preview_blit_bands_test fixtures/vp9_tiles.webm
./preview_blit_bands_test fixtures/hevc_wpp.mp4
./preview_blit_bands_test fixtures/bframes_hevc.mp4
./preview_blit_bands_test fixtures/hevc_nowpp.mp4 fixtures/mbaff_h264.mp4
RH265_PUBLISH_DELAY=200 ./preview_blit_bands_test fixtures/hevc_wpp.mp4 fixtures/bframes_hevc.mp4 fixtures/hevc_nowpp.mp4'
echo "[pass] preview_blit_bands_test (ASan)"
# Catching up: behind the clock the stream drops only what
# nothing references, and every picture still shown is the
# full decode's; the threaded build does the same with four
# pictures in flight, the drops taken at submit.
test -x preview_catchup_test && test -x preview_catchup_test_threaded
ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 300 bash -c '
./preview_catchup_test fixtures/bframes_h264.mp4 fixtures/bframes_hevc.mp4
./preview_catchup_test_threaded fixtures/bframes_h264.mp4 fixtures/bframes_hevc.mp4'
echo "[pass] preview_catchup_test (ASan, one picture at a time and four in flight)"
# The audio chain against ffmpeg: the track's rate exact, the
# resampled one within a fraction of a percent, from the first
# half second.
test -x preview_audio_oracle_test
ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 300 \
./preview_audio_oracle_test fixtures/trailing_small.mp4 fixtures/trailing_large.mp4
echo "[pass] preview_audio_oracle_test (ASan)"
./preview_audio_feed_test fixtures/audio_large_packet.m4a
./preview_audio_feed_test --prepared-only fixtures/audio_seek_gap.m4a
make audio-timeline-check SANITIZER=address,undefined
./preview_audio_feed_test --prepared-webm fixtures/audio-movie-timeline/timeline-aac-0.mkv fixtures/audio-movie-timeline/timeline-aac-1.mkv
./preview_audio_feed_test --prepared-webm fixtures/audio_seek_opus.weba fixtures/audio_seek_vorbis.weba fixtures/audio_seek_opus_window.webm fixtures/audio_seek_vorbis_window.webm
CPU_GAP=1 ./gfx_thumbnail_preview_test fixtures/video_gap_head.webm
CPU_GAP=1 ./gfx_thumbnail_preview_test_threaded fixtures/video_gap_head.webm
CPU_GAP=1 CPU_GAP_FRAMES=48 ./gfx_thumbnail_preview_test fixtures/video_gap_pin.webm
CPU_GAP=1 CPU_GAP_FRAMES=48 ./gfx_thumbnail_preview_test_threaded fixtures/video_gap_pin.webm
CPU_GAP=1 CPU_GROUP=1 CPU_GAP_FRAMES=48 ./gfx_thumbnail_preview_test fixtures/video_group.webm
CPU_GAP=1 CPU_GROUP=1 CPU_GAP_FRAMES=48 ./gfx_thumbnail_preview_test_threaded fixtures/video_group.webm
GROUP_SEEK=1 ./gfx_thumbnail_preview_test fixtures/video_group.webm
GROUP_SEEK=1 ./gfx_thumbnail_preview_test_threaded fixtures/video_group.webm
for codec in opus vorbis; do
python3 webm_audio_padding.py --audio-group-void fixtures/audio_group_"$codec".webm fixtures/audio_refill_"$codec".webm
done
python3 webm_audio_padding.py --audio-group-void fixtures/audio_group_aac.mkv fixtures/audio_refill_aac.mkv
./preview_audio_feed_test --continuous-webm fixtures/audio_refill_opus.webm
./preview_audio_feed_test --continuous-webm fixtures/audio_refill_vorbis.webm
AUDIO_MOVIE_LOOPS=1 ./preview_audio_feed_test --continuous-webm fixtures/audio_refill_aac.mkv
./preview_audio_feed_test --prepared-webm fixtures/audio_group_aac.mkv
./preview_audio_feed_test --prepared-webm fixtures/audio_group_opus.webm fixtures/audio_group_vorbis.webm
AUDIO_FEED_RACE=1 ./preview_audio_feed_test_threaded --continuous-webm fixtures/audio_refill_opus.webm
AUDIO_FEED_RACE=1 ./preview_audio_feed_test_threaded --continuous-webm fixtures/audio_refill_vorbis.webm
make -B -j2 preview_audio_feed_test preview_audio_feed_test_threaded COUNT_IO=1 SANITIZER=address,undefined
./preview_audio_feed_test --seek-batch
./preview_audio_feed_test_threaded --seek-batch
CPU_MEDIA=1 TRANSPORT_PAUSE=1 TRANSPORT_SEEK=1 ./gfx_thumbnail_preview_test fixtures/trailing_small.mp4
CPU_MEDIA=1 TRANSPORT_PAUSE=1 TRANSPORT_SEEK=1 ./gfx_thumbnail_preview_test_threaded fixtures/trailing_small.mp4
python3 movie_timeline_fixtures.py fixtures/movie-timeline
for fixture in video-timeline-1.mp4 video-timeline-libopus.webm video-timeline-libvorbis.webm; do
FRONTEND_TIMELINE=1 ./gfx_thumbnail_preview_test fixtures/movie-timeline/"$fixture"
FRONTEND_TIMELINE=1 ./gfx_thumbnail_preview_test_threaded fixtures/movie-timeline/"$fixture"
done
# clean keeps fixtures/ by design; regenerate anyway so the
# step cannot silently run against an empty glob.
- name: Save ccache
# Only master's pushes keep a cache, as in samples-gfx
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 }}
thumbnail-preview-tsan:
name: The file browser's video preview (gfx_thumbnail_preview, TSan)
runs-on: ubuntu-latest
timeout-minutes: 20
env:
APT_PKGS: ffmpeg ccache zlib1g-dev
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: 15
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 gfx_thumbnail_preview_test (TSan)
shell: bash
working-directory: samples/gfx/gfx_thumbnail_preview
run: |
set -eu
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
make fixtures
# Fixtures these runs read that make fixtures does not make
for codec in opus vorbis; do
python3 webm_audio_padding.py --audio-group-void fixtures/audio_group_"$codec".webm fixtures/audio_refill_"$codec".webm
done
python3 webm_audio_padding.py --audio-group-void fixtures/audio_group_aac.mkv fixtures/audio_refill_aac.mkv
python3 movie_timeline_fixtures.py fixtures/movie-timeline
TSAN_OPTIONS=halt_on_error=1 timeout 60 ./preview_av1_hw_test
TSAN_OPTIONS=halt_on_error=1:allocator_may_return_null=1 FRAMES=60 timeout 300 \
./gfx_thumbnail_preview_test fixtures/*.mp4 fixtures/anim_lossless.webp
echo "[pass] gfx_thumbnail_preview_test (TSan)"
# and threaded under TSan, which is the run that can see a
# race between the worker, the pool and the poll
THREADS=4 FRAMES=180 PACE_HZ=60 PACE_MAX_STALL_MS=1500 NOAUDIO=1 timeout 300 \
./gfx_thumbnail_preview_test_threaded fixtures/bframes_h264.mp4 fixtures/bframes_hevc.mp4
echo "[pass] gfx_thumbnail_preview_test_threaded (TSan, 4 threads)"
CPU_MEDIA=1 TRANSPORT_PAUSE=1 TRANSPORT_SEEK=1 TSAN_OPTIONS=halt_on_error=1 timeout 120 ./gfx_thumbnail_preview_test_threaded fixtures/trailing_small.mp4
for fixture in video-timeline-1.mp4 video-timeline-libopus.webm video-timeline-libvorbis.webm; do
FRONTEND_TIMELINE=1 TSAN_OPTIONS=halt_on_error=1 timeout 120 ./gfx_thumbnail_preview_test_threaded fixtures/movie-timeline/"$fixture"
done
for codec in opus vorbis; do
AUDIO_MOVIE_LOOPS=1 AUDIO_FEED_RACE=1 TSAN_OPTIONS=halt_on_error=1 timeout 120 \
./preview_audio_feed_test_threaded --continuous-webm fixtures/audio_refill_"$codec".webm
done
AUDIO_MOVIE_LOOPS=1 AUDIO_FEED_RACE=1 TSAN_OPTIONS=halt_on_error=1 timeout 120 \
./preview_audio_feed_test_threaded --continuous-webm fixtures/audio_refill_aac.mkv
# The pool and the tile-column threads under TSan: the
# shared frame buffers and contexts are written in disjoint
# columns, the counts merged after the join.
TSAN_OPTIONS=halt_on_error=1:allocator_may_return_null=1 timeout 300 bash -c '
./preview_blit_bands_test fixtures/long_video.mp4
./preview_blit_bands_test fixtures/vp9_tiles.webm
./preview_blit_bands_test fixtures/hevc_wpp.mp4
./preview_blit_bands_test fixtures/bframes_hevc.mp4
./preview_blit_bands_test fixtures/hevc_nowpp.mp4 fixtures/mbaff_h264.mp4
RH265_PUBLISH_DELAY=200 ./preview_blit_bands_test fixtures/hevc_wpp.mp4 fixtures/bframes_hevc.mp4 fixtures/hevc_nowpp.mp4'
echo "[pass] preview_blit_bands_test (TSan)"
TSAN_OPTIONS=halt_on_error=1:allocator_may_return_null=1 timeout 300 bash -c '
./preview_catchup_test_threaded fixtures/bframes_h264.mp4 fixtures/bframes_hevc.mp4'
echo "[pass] preview_catchup_test_threaded (TSan)"
TSAN_OPTIONS=halt_on_error=1 timeout 600 bash -c '
./preview_audio_decode_test fixtures/trailing_large.mp4 1024 island
./preview_audio_decode_test fixtures/trailing_large.mp4 262144 island'
echo "[pass] preview_audio_decode_test (TSan)"
- name: Save ccache
# Only master's pushes keep a cache, as in samples-gfx
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 }}