Skip to content

CI/docs: gate macOS float32 on Ooura, and say which backend certified… #217

CI/docs: gate macOS float32 on Ooura, and say which backend certified…

CI/docs: gate macOS float32 on Ooura, and say which backend certified… #217

Workflow file for this run

name: CI
on:
push:
pull_request:
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-test:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
# The vDSP float32 leg is a known-flaky observation post, not a gate — see
# the matrix comment below and tap/MuTap#31.
continue-on-error: ${{ matrix.nonblocking }}
strategy:
fail-fast: false
matrix:
include:
- name: Linux GCC
os: ubuntu-latest
cc: gcc
cxx: g++
werror: ON
capi: ON
nonblocking: false
- name: Linux Clang
os: ubuntu-latest
cc: clang
cxx: clang++
werror: ON
capi: ON
nonblocking: false
# macOS gates on the OOURA float32 backend. Apple's vDSP is the
# platform default and stays exercised by the leg below, but
# vDSP_fft_zrip at N=2048 returns one of two bit-exact outputs for
# identical input, drawn once per process (tap/MuTap#31), and the
# residual suppressor amplifies that into a ~97 dB swing on the
# G.168 §7 tone row. A single run of the float32 battery on vDSP is
# therefore a ~72/28 coin flip rather than a gate, and it has been
# reddening unrelated PRs since 2026-07-27.
#
# This job deliberately keeps the name "macOS AppleClang" so branch
# protection referencing that check keeps working. Restore vDSP as
# the gating backend — and delete the leg below — when #31 resolves.
- name: macOS AppleClang
os: macos-latest
werror: ON
capi: ON
nonblocking: false
extra_cmake: -DTAP_DSP_FFT_ACCELERATE=OFF
# Non-gating: keeps the vDSP float32 draw visible (and measures how
# often it bites) without blocking work that has nothing to do with it.
- name: macOS AppleClang (vDSP float32, non-gating)
os: macos-latest
werror: ON
capi: ON
nonblocking: true
# Warnings stay non-fatal on MSVC until /W4 output has been triaged
# on a Windows runner (same policy as the sibling *Tap repos).
# capi stays OFF on Windows: tools/capi carries no __declspec(dllexport)
# (unlike DspTap's), so an MSVC build would link a DLL exporting nothing
# — it would pass without gating anything. Turn this ON in the same
# change that gives the C ABI an export decoration.
- name: Windows MSVC
os: windows-latest
werror: OFF
capi: OFF
nonblocking: false
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
submodules: recursive
# Chip identity matters on Apple: #31's draw was observed on an M1 VM
# and does NOT reproduce on Intel, so a bare "macOS" label is not enough
# to attribute a past result.
- name: Record host identity (macOS)
if: runner.os == 'macOS'
run: |
sysctl -n machdep.cpu.brand_string
sysctl -n hw.model
sw_vers
cc --version | head -2
- name: Configure
env:
CC: ${{ matrix.cc }}
CXX: ${{ matrix.cxx }}
run: >
cmake -B build
-DCMAKE_BUILD_TYPE=Release
-DMUTAP_WERROR=${{ matrix.werror }}
-DMUTAP_BUILD_CAPI=${{ matrix.capi }}
${{ matrix.extra_cmake }}
# Record the backend actually built, so a log can never be misread about
# which float32 configuration produced its numbers.
- name: Record float32 FFT backend
if: runner.os == 'macOS'
run: grep '^TAP_DSP_FFT_ACCELERATE' build/CMakeCache.txt || echo 'TAP_DSP_FFT_ACCELERATE not in cache (default)'
- name: Build
run: cmake --build build --config Release -j 4
- name: Test
run: ctest --test-dir build -C Release --output-on-failure
# A single pass cannot certify a per-process bimodal outcome: on vDSP the
# float32 rows sample a ~72/28 draw. Repeat the implicated rows on this
# host so the log records how often the draw bites, rather than one
# sample of it. Non-gating by virtue of the job's continue-on-error.
- name: Repeat the float32 rows (vDSP draw rate)
if: matrix.nonblocking
run: >
ctest --test-dir build -C Release --output-on-failure
--repeat until-fail:20
-R 'itu_echo\.EchoStability<float>|g168_adapted\.ToneStability<float>|Float32Parity\.ToneRowWithNarrowbandGuard'
sanitizers:
name: ASan + UBSan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
submodules: recursive
- name: Configure
env:
CC: clang
CXX: clang++
run: >
cmake -B build
-DCMAKE_BUILD_TYPE=RelWithDebInfo
-DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-sanitize-recover=all"
- name: Build
run: cmake --build build -j 4
- name: Test
env:
UBSAN_OPTIONS: print_stacktrace=1
run: ctest --test-dir build --output-on-failure
# Cross-compile for Arm Cortex-M55 (bare metal, newlib + semihosting) and
# run the emulation-sized test subset on QEMU's MPS3 AN547 board model:
# the float32 embedded profile the one-core/three-targets plan exists for,
# on a 32-bit MCU-class target with no OS, no threads and no
# double-precision FPU, including the float-tracks-double oracle check
# and the PEM canceller's float closed-loop headline scenario. Platform
# rig (startup, linker script, one-shot gtest harness) ported from
# SampleRateTap.
cortex-m55-qemu:
name: Cortex-M55 cross (QEMU)
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
submodules: recursive
- name: Install toolchain and QEMU
run: >
sudo apt-get update -q &&
sudo apt-get install -y -q gcc-arm-none-eabi qemu-system-arm
- name: Configure
run: >
cmake -B build
-DCMAKE_BUILD_TYPE=MinSizeRel
-DCMAKE_TOOLCHAIN_FILE=cmake/arm-cortex-m55-mps3.cmake
- name: Build
run: cmake --build build -j 4
# The default leg above builds the CMSIS-DSP Helium FFT backend, which is
# ON by default on the bare-metal M55 profile (docs/optimization.md) — so
# ctest exercises the vendored third_party/cmsis-dsp subset and its
# Ooura-contract reconciliation on the full emulated battery.
- name: Test under emulation (CMSIS Helium FFT — default)
run: ctest --test-dir build --output-on-failure
# Second leg keeps the Ooura float32 fallback alive: same emulated battery
# with the backend forced OFF, so -DMUTAP_FFT_CMSIS=OFF cannot bitrot.
- name: Configure (Ooura FFT fallback)
run: >
cmake -B build-ooura
-DCMAKE_BUILD_TYPE=MinSizeRel
-DCMAKE_TOOLCHAIN_FILE=cmake/arm-cortex-m55-mps3.cmake
-DMUTAP_FFT_CMSIS=OFF
- name: Build (Ooura FFT fallback)
run: cmake --build build-ooura -j 4
- name: Test under emulation (Ooura FFT fallback)
run: ctest --test-dir build-ooura --output-on-failure
# Cross-compile for Qualcomm Hexagon (hexagon-unknown-linux-musl, HVX
# auto-vectorization on) and run the FULL test suite under qemu-hexagon
# user-mode emulation: the third target of the one-core/three-targets
# plan. Unlike the M55 leg this is a hosted Linux target — stock gtest
# main, ctest discovery and exit codes all work unchanged; binaries link
# statically so the emulator needs no sysroot. The VTCM/FastRPC
# data-layout work needs the proprietary Hexagon SDK and hardware; this
# job pins down what is checkable without them: the LLVM Hexagon backend
# compiles the core (including HVX vectorized loops) and every numeric
# regression test passes on the target ISA.
hexagon-qemu:
name: Hexagon cross (QEMU)
runs-on: ubuntu-latest
timeout-minutes: 60
env:
TOOLCHAIN: clang+llvm-19.1.5-cross-hexagon-unknown-linux-musl
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
submodules: recursive
- name: Install QEMU
run: >
sudo apt-get update -q &&
sudo apt-get install -y -q qemu-user-static
# The quic/toolchain_for_hexagon GitHub releases carry no assets of
# their own — the release notes link to Codelinaro's artifactory,
# which is where the archives actually live.
- name: Download Hexagon toolchain
run: >
mkdir -p "$RUNNER_TEMP/hexagon-toolchain" &&
curl -fsSL
"https://artifacts.codelinaro.org/artifactory/codelinaro-toolchain-for-hexagon/19.1.5/${TOOLCHAIN}.tar.zst"
| tar --zstd -x -C "$RUNNER_TEMP/hexagon-toolchain"
# The tarball's inner directory naming has varied across releases, so
# find the driver and derive the root from it instead of hardcoding.
- name: Locate toolchain root
run: |
ls "$RUNNER_TEMP/hexagon-toolchain"
cxx=$(find "$RUNNER_TEMP/hexagon-toolchain" -maxdepth 5 \( -type f -o -type l \) \
\( -name 'hexagon-unknown-linux-musl-clang++' -o -name 'clang++' \) | sort | head -1)
test -n "$cxx" || { echo "no clang++ found in the toolchain archive"; exit 1; }
root=$(dirname "$(dirname "$cxx")")
echo "found driver: $cxx"
ls "$root/bin" | head -40
echo "HEXAGON_TOOLCHAIN_ROOT=$root" >> "$GITHUB_ENV"
- name: Configure
run: >
cmake -B build
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_TOOLCHAIN_FILE=cmake/hexagon-linux-musl.cmake
-DHEXAGON_TOOLCHAIN_ROOT="$HEXAGON_TOOLCHAIN_ROOT"
- name: Build
run: cmake --build build -j 4
- name: Test under emulation
run: ctest --test-dir build --output-on-failure
# Keeps the benchmarks compiling and runnable; never a performance gate
# (shared runners are noise — see bench/README.md).
bench-smoke:
name: Benchmark smoke
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
submodules: recursive
- name: Configure
run: >
cmake -B build
-DCMAKE_BUILD_TYPE=Release
-DMUTAP_BUILD_BENCHMARKS=ON
-DMUTAP_BUILD_TESTS=OFF
- name: Build
run: cmake --build build -j 4
- name: Run (smoke)
run: ./build/bench/mutap_bench --benchmark_min_time=0.01s
# The suppressor's pass-1 estimator has two bit-identical forms selected by
# MUTAP_SUPPRESSOR_BRANCHLESS (branch-free on Arm Helium, branchy elsewhere;
# see include/mutap/postfilter.h). No single build compiles both, so this job
# compiles tests/branchless_parity_check.cpp once per macro value and diffs
# its output fingerprint — the guard that the two forms cannot drift apart.
branchless-parity:
name: Suppressor branch-free/branchy parity
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
submodules: recursive
- name: Compile both forms and diff the fingerprint
run: |
set -e
# Ooura is C: compile with gcc so g++ does not name-mangle rdft/rdft_f.
gcc -O2 -c submodules/dsptap/third_party/ooura/fftsg.c -o /tmp/fftsg.o
gcc -O2 -c submodules/dsptap/third_party/ooura/fftsg_float.c -o /tmp/fftsg_float.o
for bl in 0 1; do
g++ -std=c++20 -O2 -DMUTAP_SUPPRESSOR_BRANCHLESS=$bl -Iinclude -Isubmodules/dsptap/include \
tests/branchless_parity_check.cpp /tmp/fftsg.o /tmp/fftsg_float.o -o /tmp/parity_$bl
/tmp/parity_$bl | tee /tmp/out_$bl
done
a=$(awk '{print $2}' /tmp/out_0); b=$(awk '{print $2}' /tmp/out_1)
if [ "$a" != "$b" ]; then
echo "::error::branchy ($a) and branch-free ($b) suppressor forms diverged"; exit 1
fi
echo "both forms fingerprint $a — bit-identical"
# Deterministic instruction-count ratchet (bench/README.md). Unlike the
# wall-clock bench these counts are noise-free under QEMU's TCG plugin, so
# a hard two-sided gate (+-3%) is safe on shared runners. Baselines live
# in bench/baselines.json and are seeded by running scripts/icount.py with
# --update once per target (see bench/README.md); until a target is seeded
# this job reports each scenario's count and fails with "NO BASELINE".
icount-ratchet:
name: Instruction-count ratchet
runs-on: ubuntu-latest
timeout-minutes: 60
env:
# Commit the tag pointed at when pinned (tags are movable; commit SHAs
# are not); the header's digest is verified on download. This SHA is
# QEMU v8.2.2, matching ubuntu-latest's qemu-system-arm (plugin API v2).
QEMU_PLUGIN_HEADER_URL: https://raw.githubusercontent.com/qemu/qemu/11aa0b1ff115b86160c4d37e7c37e6a6b13b77ea/include/qemu/qemu-plugin.h
QEMU_PLUGIN_HEADER_SHA256: "c53a2af163e80e3f4bc6c60dbdfc84003db329d757e37cd8a16a77e1d82606ff"
# Neither Debian's nor CodeLinaro's qemu-hexagon enables TCG plugins, so
# the Hexagon leg builds its own qemu-hexagon from this pinned release
# (matching the plugin header version). Digest verified on download.
QEMU_SRC_URL: https://download.qemu.org/qemu-8.2.2.tar.xz
QEMU_SRC_SHA256: "847346c1b82c1a54b2c38f6edbd85549edeb17430b7d4d3da12620e2962bc4f3"
TOOLCHAIN: clang+llvm-19.1.5-cross-hexagon-unknown-linux-musl
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
submodules: recursive
- name: Install toolchains and QEMU
run: >
sudo apt-get update -q &&
sudo apt-get install -y -q gcc-arm-none-eabi qemu-system-arm
libglib2.0-dev pkg-config ninja-build meson flex bison
- name: Build counting plugin
run: |
curl -sfLo /tmp/qemu-plugin.h "$QEMU_PLUGIN_HEADER_URL"
actual=$(sha256sum /tmp/qemu-plugin.h | cut -d' ' -f1)
if [ "$actual" != "$QEMU_PLUGIN_HEADER_SHA256" ]; then
echo "::error::qemu-plugin.h checksum mismatch"; exit 1
fi
gcc -shared -fPIC $(pkg-config --cflags glib-2.0) -I/tmp \
-o /tmp/libinsncount.so tools/qemu_insn_plugin/insn_count.c
# Release (-O2) M55 workloads, matching how baselines are recorded. No
# -DMUTAP_FFT_CMSIS here: the M55 profile defaults it ON, so the ratchet
# gates the deployed CMSIS-DSP Helium FFT (bench/baselines.json m55).
- name: Build M55 workloads
run: >
cmake -B build-m55
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_TOOLCHAIN_FILE=cmake/arm-cortex-m55-mps3.cmake
-DMUTAP_BUILD_TESTS=OFF
-DMUTAP_BUILD_ICOUNT_BENCH=ON
&& cmake --build build-m55 -j 4
# Each target's ratchet runs even if an earlier one failed (each
# target's numbers are independent evidence, and a legitimate baseline
# move should surface all at once, not one CI round-trip at a time).
# The job still fails overall if any target regressed or is unseeded.
- name: Ratchet M55
if: ${{ !cancelled() }}
run: >
python3 scripts/icount.py --target m55
--build-dir build-m55 --plugin /tmp/libinsncount.so
# qemu-system-arm's plugins cover M55; qemu-hexagon must be built with
# --enable-plugins (linux-user only, ~4 min, cached thereafter).
- name: Cache plugin-enabled qemu-hexagon
if: ${{ !cancelled() }}
id: qemu-hex
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ~/qemu-hexagon-plugins
key: qemu-hexagon-plugins-${{ env.QEMU_SRC_SHA256 }}-1
- name: Build plugin-enabled qemu-hexagon
if: ${{ !cancelled() && steps.qemu-hex.outputs.cache-hit != 'true' }}
run: |
curl -sfLo /tmp/qemu-src.tar.xz "$QEMU_SRC_URL"
actual=$(sha256sum /tmp/qemu-src.tar.xz | cut -d' ' -f1)
if [ "$actual" != "$QEMU_SRC_SHA256" ]; then
echo "::error::qemu source checksum mismatch"; exit 1
fi
tar -xJf /tmp/qemu-src.tar.xz -C /tmp
cd /tmp/qemu-*/
./configure --target-list=hexagon-linux-user --enable-plugins \
--disable-docs --disable-tools --disable-system
ninja -C build qemu-hexagon
mkdir -p ~/qemu-hexagon-plugins
cp build/qemu-hexagon ~/qemu-hexagon-plugins/
- name: Download Hexagon toolchain
if: ${{ !cancelled() }}
run: >
mkdir -p "$RUNNER_TEMP/hexagon-toolchain" &&
curl -fsSL
"https://artifacts.codelinaro.org/artifactory/codelinaro-toolchain-for-hexagon/19.1.5/${TOOLCHAIN}.tar.zst"
| tar --zstd -x -C "$RUNNER_TEMP/hexagon-toolchain"
- name: Locate toolchain root
if: ${{ !cancelled() }}
run: |
cxx=$(find "$RUNNER_TEMP/hexagon-toolchain" -maxdepth 5 \( -type f -o -type l \) \
\( -name 'hexagon-unknown-linux-musl-clang++' -o -name 'clang++' \) | sort | head -1)
test -n "$cxx" || { echo "no clang++ found in the toolchain archive"; exit 1; }
echo "HEXAGON_TOOLCHAIN_ROOT=$(dirname "$(dirname "$cxx")")" >> "$GITHUB_ENV"
- name: Build Hexagon workloads
if: ${{ !cancelled() }}
run: >
cmake -B build-hex
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_TOOLCHAIN_FILE=cmake/hexagon-linux-musl.cmake
-DHEXAGON_TOOLCHAIN_ROOT="$HEXAGON_TOOLCHAIN_ROOT"
-DMUTAP_BUILD_TESTS=OFF
-DMUTAP_BUILD_ICOUNT_BENCH=ON
&& cmake --build build-hex -j 4
- name: Ratchet Hexagon
if: ${{ !cancelled() }}
run: |
export PATH="$HOME/qemu-hexagon-plugins:$PATH"
# qemu-hexagon exits 1 here with or without plugin support (no guest
# binary given), so probe by the error text, not the exit code.
if qemu-hexagon -plugin help 2>&1 | grep -q "unknown option"; then
echo "::error::built qemu-hexagon lacks plugin support"; exit 1
fi
python3 scripts/icount.py --target hexagon \
--build-dir build-hex --plugin /tmp/libinsncount.so
clang-format:
name: clang-format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
submodules: recursive
# Run the TapHouse pre-commit hook — the exact same pinned clang-format
# developers get from `pre-commit install` (.pre-commit-config.yaml). CI
# and local therefore share one clang-format version by construction, and
# cover the same files (all C/C++ minus third_party and generated tables),
# so a green local commit can't fail here. pipx is preinstalled on the
# runner; the pinned hook is fetched and cached on first use.
- name: Check formatting (pre-commit)
run: |
pipx install pre-commit
pre-commit run --all-files --show-diff-on-failure