Skip to content

Run the test battery bare-metal under QEMU on Cortex-M4, M33 and M55 - #17

Merged
tap merged 3 commits into
mainfrom
claude/wave1-stage1-legs
Sep 17, 2026
Merged

tap merged 3 commits into
mainfrom
claude/wave1-stage1-legs

Conversation

@tap

@tap tap commented Sep 17, 2026

Copy link
Copy Markdown
Owner

What this changes

Stage 1 (DspTap side) of docs/audit-fft-and-code-smells.md, Part 10, amended per Part 13 after the two hostile reviews: the embedded CI legs become real. Four bare-metal jobs — cortex-m4-softfp, cortex-m4f, cortex-m33, cortex-m55 — cross-compile the test battery with gcc-arm-none-eabi, run it under qemu-system-arm with ctest -V, and print arm-none-eabi-size (Berkeley and -A) of the test binary. They replace the compile-only build-embedded-m55 job.

  • platform/: cortexm_startup.c (MuTap's armv8m_startup.c @ 142361b, MSPLIM write guarded by __ARM_ARCH_8M_MAIN__ || __ARM_ARCH_8_1M_MAIN__, and a fault path: HardFault_Handler/Default_Handler print TAP_DSP_TESTS_FAULT <what> ipsr=<n> over semihosting and _exit(2)), mps2_an505.ld and mps3_an547.ld copied from MuTap, a new mps2_an386.ld for QEMU's Cortex-M4 board, SPDX lines on all three scripts, and platform/README.md recording provenance (SampleRateTap → RatioTap → MuTap → here), that this copy is the canonical one from the first consumer pin, and that the duplication is deliberate pending the taphouse consolidation.
  • cmake/: new arm-cortex-m4-mps2.cmake (cache variable TAP_DSP_M4_FPU: OFF → -mcpu=cortex-m4 -mfloat-abi=soft, ON → -mfpu=fpv4-sp-d16 -mfloat-abi=hard; emulator qemu-system-arm -M mps2-an386), new arm-cortex-m33-mps2.cmake (as MuTap's), the existing M55 file repointed at platform/. All set TAP_DSP_BARE_METAL ON (renamed from MUTAP_BARE_METAL), add -Wno-psabi to the C++ flags, and default TAP_DSP_PARITY_MAX_N to 4096 as a cache entry for Stage 2a tests, written first: Ooura parity gate, independent oracle, real-time guard #24's parity target; the M4/M33 files pin TAP_DSP_FFT_CMSIS OFF before the root option() is evaluated.
  • tests/CMakeLists.txt: include(GoogleTest) hoisted, and one registration helper for every test executable, documented in the file header:
    tap_dsp_add_gtest_executable(<target> SOURCES <src>... [MAIN_FILTER <gtest filter>] [LABELS <label>...]) — hosted: links GTest::gtest_main + gtest_discover_tests; TAP_DSP_BARE_METAL: links GTest::gtest + bare_metal_main.cpp, passes MAIN_FILTER as TAP_DSP_BARE_METAL_FILTER, registers one add_test(<target>_emulated) with PASS_REGULAR_EXPRESSION "TAP_DSP_TESTS_COMPLETE rc=0 ", FAIL_REGULAR_EXPRESSION "\[ FAILED \];TAP_DSP_TESTS_FAULT", TIMEOUT 600. tap_dsp_tests uses it.
  • tests/bare_metal_main.cpp: a negative filter (default "*"), so every suite compiled into a test executable runs on target unless excluded by name in MAIN_FILTER; each exclusion is a written budget decision next to the source list (double psola/pvoc/yin/log_mel/nn, six Kaiser design searches, MultitoneAnalysis). The marker is TAP_DSP_TESTS_COMPLETE rc=<n> selected=<n> skipped=<n>; an empty selection or any GTEST_SKIP is rc=1.
  • .github/workflows/ci.yml: runs-on: ubuntu-24.04 (pinned, matching bench.yml); apt-get --no-install-recommends gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib qemu-system-arm; embedded configure with -DTAP_DSP_WERROR=ON; ctest -V on the emulated step; actions/checkout@v5; hosted configure passes -DTAP_DSP_BUILD_BENCH=ON for Stage 1b: FFT benchmark scenarios and the instruction-count ratchet scaffold #21 (inert until it lands); a guarded verbose ctest -L parity -R default_flags step for Stage 2a tests, written first: Ooura parity gate, independent oracle, real-time guard #24's ulp table.
  • README: one paragraph in Build listing the four legs, the negative-filter rule and how to run one locally.

Why

The plan's fixed-point gates (Stage 3) and the .text/instruction-count ratchets (Stage 1b) need legs that actually execute on Cortex-M targets; the previous M55 job linked platform files that only exist in MuTap and built the library with tests off. The reviews then showed the first cut was not integrable: #24 adds test executables of its own, gtest_discover_tests cannot run under the emulator, a positive filter would have silently dropped every suite #24/#22/#19 add, and a passing ctest swallowed the only evidence the legs produce. The helper, the negative filter, -V and the fault path close those; TAP_DSP_PARITY_MAX_N=4096 on the legs is the plan's "parity at N ≤ 4096".

Verification

  • Hosted: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DTAP_DSP_WERROR=ON && cmake --build build -j8 && ctest → 160/160, unchanged.

  • Host one-shot mode (-DTAP_DSP_BARE_METAL=ON, Linux gtest without pthreads and with the three GTEST_HAS_* defines): Running 114 tests from 25 test suites, TAP_DSP_TESTS_COMPLETE rc=0 selected=114 skipped=0 — the same 114 the positive filter selected, so the negative list is an exact inversion today.

  • Scratch merge with Stage 2a tests, written first: Ooura parity gate, independent oracle, real-time guard #24 (4ddff35 onto 09a584e), Stage 2a tests, written first: Ooura parity gate, independent oracle, real-time guard #24's block converted to the helper as posted on Stage 2a tests, written first: Ooura parity gate, independent oracle, real-time guard #24: host one-shot tap_dsp_tests_emulated 161/0, tap_dsp_fft_parity_emulated 4/0, tap_dsp_fft_parity_default_flags_emulated 2/0; hosted 215/215, -L parity = 8 tests, 38 ulp lines under -V.

  • CI, run 35268481640 at 09a584e, all seven jobs green, from the -V logs (GCC 13.2.1 15:13.2.rel1-2, QEMU 8.2.2, newlib 4.4.0, MinSizeRel, -Werror, 0 warnings, 0 -Wpsabi notes):

    leg Running marker ctest wall Berkeley text .text .ARM.extab+exidx .data .bss .heap (NOLOAD)
    cortex-m4-softfp 114 / 25 suites rc=0 selected=114 skipped=0 82.39 s 912 900 852 752 60 084 1 980 7 052 4 119 728
    cortex-m4f 114 / 25 rc=0 selected=114 skipped=0 91.90 s 907 508 846 888 60 556 1 980 7 052 4 119 728
    cortex-m33 114 / 25 rc=0 selected=114 skipped=0 92.64 s 906 340 845 936 60 340 1 980 7 052 4 119 728
    cortex-m55 (CMSIS Helium) 114 / 25 rc=0 selected=114 skipped=0 13.63 s 965 668 905 224 60 380 1 980 7 052 2 087 912

    Wall time across the three runs so far moved 52–95 s on the identical M4 soft-float binary: TCG noise, a budget signal only.

  • clang-format --dry-run --Werror and clang-tidy on tests/bare_metal_main.cpp, and clang-format plus a host gcc -fsyntax-only -Wall -Wextra -Wconversion -Wshadow pass on platform/cortexm_startup.c, were local: neither file is in the hosted compile database the Style workflow tidies (bare_metal_main.cpp compiles only under TAP_DSP_BARE_METAL; the startup file is compiled from the cross link line).

  • Not exercised: the fault path itself. No leg faulted, so TAP_DSP_TESTS_FAULT_exit(2)FAIL_REGULAR_EXPRESSION is proven only by reading (semihosting write on fd 2 and _exit are what initialise_monitor_handles/librdimon already provide for the normal path). A deliberate-fault build would prove it; not done here.

  • No Arm toolchain or QEMU in the development container; everything on-target is CI's evidence.

Notes for the reviewer

🤖 Generated with Claude Code

https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy

tap and others added 2 commits September 17, 2026 18:10
Stage 1 (DspTap side) of docs/audit-fft-and-code-smells.md, Part 10: the
embedded legs become real. Until now cmake/arm-cortex-m55-mps3.cmake linked
platform files that only exist in MuTap, and CI built the static library
with tests off.

- platform/: cortexm_startup.c (MuTap's armv8m_startup.c, MSPLIM write
  guarded by __ARM_ARCH_8M_MAIN__ so it also serves the Armv7E-M M4),
  mps2_an505.ld and mps3_an547.ld copied, and a new mps2_an386.ld for
  QEMU's Cortex-M4 board (SSRAM1 4 MB @ 0x0 code, SSRAM2/3 4 MB @
  0x20000000 data, per hw/arm/mps2.c).
- cmake/: arm-cortex-m4-mps2.cmake (TAP_DSP_M4_FPU OFF = soft-float, ON =
  fpv4-sp-d16 hard), arm-cortex-m33-mps2.cmake as MuTap's, the M55 file
  repointed at platform/. All set TAP_DSP_BARE_METAL (renamed from
  MUTAP_BARE_METAL); M4/M33 pin TAP_DSP_FFT_CMSIS OFF (no MVE).
- tests/: one-shot bare-metal mode (gtest without pthreads/POSIX RE/stream
  redirection/filesystem, bare_metal_main.cpp with a positive filter of the
  emulation-sized selection, judged on "TAP_DSP_TESTS_COMPLETE rc=0").
- ci.yml: the compile-only M55 job becomes four QEMU legs, cortex-m4-softfp,
  cortex-m4f, cortex-m33, cortex-m55 (CMSIS Helium backend ON there, so the
  FFT backend parity suite runs on a Helium core for the first time), each
  ending with arm-none-eabi-size on the test binary.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
Comment-only: the budget note in tests/bare_metal_main.cpp and the job
comment in ci.yml carry the numbers from the first green run
(cortex-m4-softfp 88 s, cortex-m4f 62 s, cortex-m33 97 s, cortex-m55 14 s;
.text 912868 / 907468 / 906316 / 965636 bytes).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy

tap commented Sep 17, 2026

Copy link
Copy Markdown
Owner Author

Embedded legs: first measured run (b18ad52, run 35257259009)

All four legs and the three hosted legs passed at the first attempt; the Tap House Style workflow (drift + clang-tidy) passed too. Toolchain on the runners: arm-none-eabi-gcc 13.2.1 20231009 (Ubuntu 24.04 15:13.2.rel1-2), QEMU 8.2.2, newlib 4.4.0, MinSizeRel, 114 tests from 25 suites per leg.

leg board float ABI FFT backend emulation (ctest wall) .text .data .bss (incl. NOLOAD heap)
cortex-m4-softfp mps2-an386 -mfloat-abi=soft Ooura 87.95 s 912 868 B 2 072 B 4 126 780 B
cortex-m4f mps2-an386 fpv4-sp-d16 hard Ooura 61.68 s 907 468 B 2 072 B 4 126 780 B
cortex-m33 mps2-an505 hard (SP only) Ooura 97.36 s 906 316 B 2 072 B 4 126 780 B
cortex-m55 mps3-an547 hard (SP + DP) CMSIS-DSP Helium 14.15 s 965 636 B 2 276 B 2 094 964 B

Notes from the logs:

  • Every leg is well inside the 5 min budget; the selection was not trimmed. The M55 runs the same double work in hardware FP64, which is the whole gap to the other three. The M33 is slightly slower than the soft-float M4 under TCG (Armv8-M security-state overhead in emulation, not a property of the code).
  • On the M55 the CMSIS-DSP Helium backend was built (tap::dsp: float32 FFT backend = CMSIS-DSP Helium) and all three CertifiedGeometries/* suites (parity, alignment stability, tonal accuracy) passed against Ooura on a Helium core. The larger .text there is the CMSIS twiddle/bit-reversal tables.
  • The .bss figures include the .heap (NOLOAD) region the linker scripts carve out (DATA minus 64 KB on the MPS2 scripts, all of ISRAM on AN547), so they are the map, not the footprint.
  • Wall time per job: ~2.5 min (toolchain install 35-60 s, cross build 15-20 s, emulation as above).
  • Compiler output on all four legs: zero warnings. The only diagnostics are GCC's informational -Wpsabi notes ("parameter passing for argument of type std::span<const double> changed in GCC 7.1") from newlib's libstdc++ and our std::span parameters, which are notes, not warnings, and cannot fail a -Werror build. No -Wconversion noise appeared for the int16x2_t casts in fir_kernels.h on the M4/M33 GCC; the SMLALD Q15 path ran and passed on both.

The numbers are now also in the budget comment of tests/bare_metal_main.cpp and the job comment in ci.yml (1505e41, comment-only).

🤖 Generated with Claude Code

https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy


Generated by Claude Code

@tap tap left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hostile review B (process / downstream)

Scope: ownership, truthfulness of the PR body and the measurements comment against the CI logs, Part 10 compliance, cross-PR hazards against the other seven wave-1 branches, downstream (MuTap) effects, CI hygiene, commit hygiene. Reviewed at 1505e41 against origin/main 5ca3b1c. Hosted suite rebuilt in a private scratch dir with -DTAP_DSP_WERROR=ON: 160/160 pass, 0 warnings. The one-shot harness rebuilt on the host with -DTAP_DSP_BARE_METAL=ON: Running 114 tests from 25 test suites, TAP_DSP_TESTS_COMPLETE rc=0.

0. Ownership — clean

11 files touched: .github/workflows/ci.yml, cmake/arm-cortex-m33-mps2.cmake (new), cmake/arm-cortex-m4-mps2.cmake (new), cmake/arm-cortex-m55-mps3.cmake, platform/cortexm_startup.c, platform/mps2_an386.ld, platform/mps2_an505.ld, platform/mps3_an547.ld (all new), tests/CMakeLists.txt, tests/bare_metal_main.cpp (new), README.md (one Build paragraph + a fenced block replacing the old three-line paragraph). Every one is inside this agent's Part 12 allotment. No header, no root CMakeLists.txt, no docs. Nothing to report.

1. should-fix — the per-leg test count and the "parity suites passed" claims are not in any CI log

The comment says "114 tests from 25 suites per leg" and "all three CertifiedGeometries/* suites ... passed against Ooura on a Helium core". The CI step is ctest --test-dir build-${{ matrix.name }} --output-on-failure, which swallows the child's stdout on a pass. The entire test evidence in each embedded log is one line, e.g. cortex-m55:

2026-09-17T18:12:27.5438253Z 1/1 Test #1: tap_dsp_tests_emulated ...........   Passed   14.15 sec

No [==========] Running N tests, no [ PASSED ], no suite names. 114/25 is the host number (I reproduced it); the target count is the same only by construction (the geometry lists in test_fft_backend.cpp:111/192/255 are unconditional Values(...), no #if TAP_DSP_FFT_CMSIS), and the ≥60 guard would not notice a suite compiled out. The comment presents host-derived facts as per-leg observations.

Fix: run the emulated test with ctest -V (one test, roughly 130 lines of gtest output) so the count, the suite list and the [ PASSED ] 114 tests. line are in the log that future ratchets and reviewers read; then the numbers in the bare_metal_main.cpp budget comment and the job comment are measured, not inferred. Until then, reword the comment.

What does check out against the logs, line for line: emulation 87.95 / 61.68 / 97.36 / 14.15 s; .text 912868 / 907468 / 906316 / 965636; .data 2072/2276; .bss 4126780/2094964; arm-none-eabi-gcc (15:13.2.rel1-2) 13.2.1 20231009; QEMU emulator version 8.2.2 (Debian 1:8.2.2+ds-0ubuntu1.18); libnewlib-arm-none-eabi 4.4.0.20231231-2; -- tap::dsp: float32 FFT backend = CMSIS-DSP Helium on the M55 with the nine CMSIS TUs compiled; install-step durations 45/59/35/33 s.

2. should-fix — "zero warnings" is true, but the legs do not build with TAP_DSP_WERROR, and the PR does not say so

Counted over the four downloaded job logs: warning: lines = 0/0/0/0, note: lines = 28/26/26/26 (all GCC -Wpsabi "parameter passing for argument of type 'std::span' changed in GCC 7.1"), Wconversion = 0. So the claim holds. But the hosted jobs configure with -DTAP_DSP_WERROR=ON (ci.yml:26) and the embedded configure line does not:

cmake -S . -B build-cortex-m4-softfp -DCMAKE_BUILD_TYPE=MinSizeRel -DCMAKE_TOOLCHAIN_FILE=cmake/arm-cortex-m4-mps2.cmake -DTAP_DSP_M4_FPU=OFF

Part 10 item 6 asks for -Wconversion on the int16x2_t casts to be checked "under the M4's GCC": today that check is a human reading a log. Fix: add -DTAP_DSP_WERROR=ON to the embedded configure (the logs say it passes today; notes are not affected by -Werror), or state in the PR body that the embedded legs are warning-tolerant by design and why.

3. should-fix — ubuntu-latest while the comments, the PR body and bench.yml all say 24.04

ci.yml:49 runs-on: ubuntu-latest; the job comment says "Ubuntu 24.04's qemu-system-arm (8.2) has mps2-an386, mps2-an505 and mps3-an547"; apt-get install -y -q gcc-arm-none-eabi qemu-system-arm is unpinned. The bench branch pins runs-on: ubuntu-24.04 with an explicit reason ("the plugin header below is pinned to the QEMU this image ships"). When ubuntu-latest moves to 26.04 the QEMU, GCC and newlib change under the recorded emulation times and .text figures with no diff in this repo. Fix: runs-on: ubuntu-24.04, matching bench.yml, and leave the "Toolchain versions" step as the drift detector it already is.

4. should-fix — "clang-format and clang-tidy clean on the new .cpp/.c" is a local claim CI cannot back

The Style workflow tidies the TUs in the hosted compile database (style.yml:26, -DTAP_DSP_BUILD_TESTS=ON, no toolchain). tests/bare_metal_main.cpp is compiled only under TAP_DSP_BARE_METAL and platform/cortexm_startup.c only appears on the cross link line; neither is in compile_commands.json (grep count 0 on a fresh hosted configure). So "the Tap House Style workflow passed too" is true and covers neither new file. clang-format --dry-run -Werror on both passes locally for me too. Fix: say in Verification that tidy on these two files was local, or give bare_metal_main.cpp a hosted OBJECT/tidy-only target so the house style job sees it.

5. cross-PR (wave-level blocker for whoever merges second; a note for this PR)

git merge-tree --write-tree of this branch against each sibling:

branch result overlap
wave1-stage0-counter clean none
wave1-stage1b-bench (#21) clean README hunk lands correctly after the new paragraph (checked the merged text)
wave1-stage2a-tests CONFLICT tests/CMakeLists.txt 2a appends after line 40; this PR rewrites lines 40-42 into the if(TAP_DSP_BARE_METAL) block
wave1-stage3a-traits clean tests/CMakeLists.txt source list only
wave1-stage5-spectrum clean tests/CMakeLists.txt source list only
wave1-docs-provenance clean README at line 364, disjoint
wave1-capi-fft clean README at line 305, disjoint

Two semantic hazards the textual merge will not flag:

  • stage2a adds tap_dsp_fft_parity and tap_dsp_fft_parity_default_flags, each linked to GTest::gtest_main with gtest_discover_tests(...). Under the cross toolchains gtest_discover_tests runs the binary through CMAKE_CROSSCOMPILING_EMULATOR at build time with --gtest_list_tests; there is no argv on the target and semihosting exit codes are unreliable (this PR's own rationale). All four legs will break the day 2a lands on top of this. 2a needs if(NOT TAP_DSP_BARE_METAL) around both targets, or a one-shot main of its own.
  • Every new suite from 2a (test_fft_oracle.cpp, test_fft_rt.cpp), 3a (test_fft_arith.cpp) and 5 (test_spectrum.cpp) compiles into tap_dsp_tests and is silently not run on target, because the filter is positive and the ≥60 guard cannot see an omission. That is the documented design, but 3a is the fixed-point substrate the legs exist for ("Fixed point's gates (Stage 3) run on those legs"). Suggest this PR add one sentence to tests/CMakeLists.txt next to target_sources(... bare_metal_main.cpp): "adding a test file here does not put it on the target; name its suites in bare_metal_main.cpp", so the 2a/3a/5 owners see it at the merge conflict rather than months later.

Merge position: after stage0 (independent), before every other tests/CMakeLists.txt toucher, i.e. stage0 → #17#21 → 3a → 5 → 2a → docs → capi. #21 rebases cleanly (rc 0) and its bench.yml names match this PR exactly: cmake/arm-cortex-m4-mps2.cmake with -DTAP_DSP_M4_FPU=OFF|ON, cmake/arm-cortex-m33-mps2.cmake, cmake/arm-cortex-m55-mps3.cmake; it builds with -DTAP_DSP_BUILD_TESTS=OFF so TAP_DSP_BARE_METAL is inert there. Note that #21's own CI is red until this PR is on main (the M4 toolchain file does not exist on its branch), so it must merge after, not alongside.

6. should-fix (repo settings, not this diff) — nothing is a gate: branch protection on main is off

GET /repos/tap/DspTap/branches/main"protected": false, required_status_checks.enforcement_level: "off", contexts: []. So: the disappearance of the build-embedded-m55 job name breaks nothing, and none of the seven CI jobs, drift or clang-tidy is required. The plan sentence "until they exist, no sentence in this plan calls an on-target property a DspTap gate" is now stale in both directions: the legs exist, and they still gate nothing in GitHub's sense. Recommend the repo owner require cortex-m4-softfp, cortex-m4f, cortex-m33, cortex-m55, linux-ooura, windows-ooura, macos-vdsp, drift / drift, clang-tidy before wave 2, and that the docs owner (not this PR) rewrite that Stage 1 bullet and Part 10 "What exists to copy" to record the measured numbers and the run id.

For the docs owner as well: CLAUDE.md:69 "The CMSIS-Helium backend is compile-verified under cmake/arm-cortex-m55-mps3.cmake; its runtime parity is the consuming library's job" is false once this merges (CLAUDE.md is outside this PR's ownership; flagging, not asking for an edit here). The README sentence this PR replaced was the same claim and is correctly gone.

7. should-fix — provenance and intent of the third copy are only in the PR body

Verified against /home/user/MuTap at 142361b: mps2_an505.ld and mps3_an547.ld are byte-identical to MuTap's; cortexm_startup.c differs from armv8m_startup.c only in the header, the copyright line, and the #if defined(__ARM_ARCH_8M_MAIN__) guard; the M33 toolchain differs in comments, the _tap_dsp_platform rename and TAP_DSP_BARE_METAL. In-tree provenance is uneven: cortexm_startup.c records the chain (MuTap ← SampleRateTap); mps2_an505.ld carries MuTap's own line "Ported from RatioTap's platform tree ... from SampleRateTap's" (true for MuTap's copy, misleading for one taken from MuTap); mps3_an547.ld has no provenance or license line at all. Nothing in-tree says which of the three family copies is canonical or that the duplication is deliberate pending the taphouse consolidation the plan names (Part 11 "Sharing"). Fix: a four-line platform/README.md (or header lines in the two .lds): copied from MuTap at <sha> on 2026-09-17, MIT, generalized to Armv7E-M here, intended canonical copy for the family / consumers retire theirs when they pin a tree containing it.

Downstream check: MuTap builds DspTap via add_subdirectory(submodules/dsptap) and DspTap's TAP_DSP_BUILD_TESTS defaults OFF when not top-level (CMakeLists.txt:115-117), so the MUTAP_BARE_METALTAP_DSP_BARE_METAL rename does not reach MuTap's M33/M55 legs. MuTap's own toolchains still say MUTAP_BARE_METAL; nothing to do here, noted for its Stage 1 PR.

8. nit — actions/checkout@v4 Node 20 deprecation in every job

Every one of the seven jobs ends with ##[warning]Node.js 20 is deprecated. The following actions target Node.js 20 but are being forced to run on Node.js 24: actions/checkout@v4. Pre-existing and also in style.yml (canonical, do not hand-edit). ci.yml is this PR's file: bump to actions/checkout@v5 here.

9. nit — PR body and footer

  • Verification still says "I am watching the runs and will fix what they turn up ... Per-leg emulation time and .text size will be posted here from the logs once the legs are green." They were; point the body at the comment and the run id.
  • The body ends with both the "🤖 Generated with Claude Code + session URL" footer and a second _Generated by ..._ line. One is enough.
  • Comment attributions presented as findings: "Armv8-M security-state overhead in emulation" and "the larger .text there is the CMSIS twiddle/bit-reversal tables" are plausible and unmeasured; label them as attributions or drop them (house rule: measured, not remembered).

10. Part 10 items 1-7, for the record

1 platform files: done, with the AN386 map sourced from hw/arm/mps2.c as the plan required. 2 toolchains: done, TAP_DSP_M4_FPU forwarded through CMAKE_TRY_COMPILE_PLATFORM_VARIABLES. 3 harness: done; selection matches the plan's list, budget measured and written into the job comment. 4 matrix: done in MinSizeRel with the size step; the .text ceilings and the size-probe object are deferred and the PR says so (consistent with Stage 1b's "the .text ceilings from Part 10 item 4 live in the same job"). 5 icount: Stage 1b, stated. 6 side effects: SMLALD path ran via fir_kernels_test/1, zero -Wconversion output (see 2 for the caveat). 7 cost: ~2.5 min wall per leg against the plan's 10-15; runs on push and pull_request as the rest of ci.yml.

11. Commit hygiene — clean

Two commits: b18ad52 (the change) and 1505e41 (comment-only, the measured numbers). Both carry Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> and Claude-Session: trailers; author Timothy Place. Messages describe the change, not the diff. CI green on both SHAs (runs 35257259009/35257320034 and 35257639886/35257647954), Style green.

Verdict: no blocker inside this diff; fix 1-4 (all in owned files, small) before merge, and merge it first among the tests/CMakeLists.txt touchers so 2a/3a/5 rebase onto the one-shot block rather than the other way round.


Generated by Claude Code

@tap tap left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hostile review A (correctness)

Read against QEMU hw/arm/mps2.c / mps2-tz.c (master, fetched today), GCC 13 config/arm/arm-cpus.in + arm.cc + arm-c.cc, CMake 3.28's GoogleTestAddTests.cmake, both CI runs' logs (35257639886 and 35257647954), and a scratch merge of this branch with claude/wave1-stage2a-tests (#24). Hosted -DTAP_DSP_WERROR=ON: 160/160. Host -DTAP_DSP_BARE_METAL=ON: 114 tests / 25 suites, 347 ms, marker printed — reproduces the PR's claim. No Arm compiler or QEMU here, so everything on-target is by reading.

Blocker

1. Merged with #24, the four legs do not configure; fixed naively, they do not build. git merge of the two branches conflicts textually in tests/CMakeLists.txt (#24 appends after the three hosted lines this PR moved into else()). Resolved the obvious way (#24's block after the if/else), cmake -DTAP_DSP_BARE_METAL=ON fails: tests/CMakeLists.txt:116 Unknown CMake command "gtest_discover_tests" because include(GoogleTest) now lives inside the hosted branch. Hoisting the include does not help: gtest_discover_tests (POST_BUILD) runs ${CROSSCOMPILING_EMULATOR} <exe> --gtest_list_tests (GoogleTestAddTests.cmake:99), i.e. qemu-system-arm -M mps2-an386 ... -kernel tap_dsp_fft_parity --gtest_list_tests, which QEMU rejects as an unknown option → "Error running test executable" → the build step fails. And even if discovery were skipped, #24's two parity executables link GTest::gtest_main (main(0, NULL) runs everything) with TAP_DSP_PARITY_MAX_N defaulting to 2^20: several 8 MB std::vector<double> on a 4 MB heap → bad_alloc → park → 30-minute timeout. Finally, #24's target_sources(tap_dsp_tests ... test_fft_oracle.cpp test_fft_rt.cpp) adds fft_oracle_test (14), fft_rt_test (8), fft_rt_guard, fft_oracle_self_check to this binary, none of which is in the positive filter, so on target they silently do not run; the < 60 guard cannot see an omission, only a typo.
Fix (this PR, since it owns the mode): hoist include(GoogleTest); add a helper, e.g. tap_dsp_add_gtest_executable(<target> FILTER <gtest filter>), that in hosted mode links GTest::gtest_main + gtest_discover_tests, and in bare-metal mode links GTest::gtest + bare_metal_main.cpp with the filter passed as a compile definition (TAP_DSP_BARE_METAL_FILTER="...") and one add_test with the PASS/FAIL regexes; set TAP_DSP_PARITY_MAX_N 4096 in all three toolchain files (the plan's "parity at N ≤ 4096", and #24 already documents "4096 on emulated targets" without anyone setting it). Then #24 calls the helper. Whichever PR merges second rebases; say so in both.

Should-fix

2. Nothing in the CI logs supports the counts or the times this PR writes down. ctest --output-on-failure suppresses a passing test's output, so none of the four logs contains a gtest summary, the TAP_DSP_TESTS_COMPLETE marker, or any SKIPPED line (grepped all four). The only measurable is ctest wall: run 1 = 51.3 / 82.5 / 97.8 / 14.3 s, run 2 = 95.2 / 69.2 / 89.2 / 14.2 s (m4-softfp / m4f / m33 / m55). The "88 s / 62 s / 97 s / 14 s" in ci.yml and bare_metal_main.cpp match neither run. Also note m4-softfp went 51 → 95 s on an identical binary: TCG wall on shared runners is ±50 % noise, which is the argument for icount (Stage 1b), not for wall budgets in comments.
Fix: run ctest -V (or --verbose) on the embedded legs so the gtest summary is in the log; make the marker line carry selected=%d skipped=%d (UnitTest::skipped_test_count()) and fail on skipped > 0 (gtest 1.14 has no fail-on-skip flag, and #24 introduces GTEST_SKIP); replace the hard-coded times with "see ctest wall in the log".

3. A fault costs 30 minutes and leaves no diagnostic. HardFault_Handler executes bkpt #0: under QEMU with no gdb attached, BKPT on M-profile pends DebugMonitor, which is disabled, so it escalates and the core locks up; Default_Handler spins. Either way the leg sits until TIMEOUT 1800 (30 of the job's 45 minutes) and the log shows nothing. MuTap has the same rig, but its selection is 3 minutes; here the slowest leg is 98 s.
Fix: in both handlers write a marker over semihosting (TAP_DSP_TESTS_FAULT <vector>) and _exit(2) (SYS_EXIT), add the marker to FAIL_REGULAR_EXPRESSION, and set TIMEOUT 600 (6× the slowest measured leg). Keep the bkpt under an #ifdef for debugger use.

4. 26–28 -Wpsabi notes per leg. note: parameter passing for argument of type 'std::span<const double>' changed in GCC 7.1 from kaiser.h:111/202, quantize.h:44, multitone_analysis.h:74/96/118 and libstdc++. They are notes, so neither -Werror nor tap_dsp_warnings touches them, and they bury a real warning. The ABI note is irrelevant to a single-toolchain static image.
Fix: -Wno-psabi in the four CMAKE_CXX_FLAGS_INIT.

5. runs-on: ubuntu-latest with comments and a sibling workflow that assume Ubuntu 24.04. The ci.yml comment says "Ubuntu 24.04's qemu-system-arm (8.2)"; bench.yml (Stage 1b) pins ubuntu-24.04 and QEMU 8.2.2's plugin header by SHA. ubuntu-latest is rolling to 26.04 (QEMU 10.x, a newer gcc-arm-none-eabi/newlib); when it does, these four legs silently change toolchain under the icount baselines while bench stays put.
Fix: runs-on: ubuntu-24.04; optionally assert the two versions in the "Toolchain versions" step.

6. The apt line depends on Recommends. libnewlib-arm-none-eabi and libstdc++-arm-none-eabi-newlib are only Recommends of gcc-arm-none-eabi; the install works because the runner's apt default installs Recommends, and for the same reason pulls ~90 packages (gstreamer, spice, pipewire, ceph) behind qemu-system-arm.
Fix: apt-get install -y -q --no-install-recommends gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib qemu-system-arm.

Nits

7. arm-none-eabi-size (Berkeley format) is the wrong instrument for the coming ceilings. The bss column (4 126 780) is the 4 MB .heap NOLOAD region, and text includes .rodata (the M55's +53 kB is CMSIS tables, not code). Use arm-none-eabi-size -A and read .text / .rodata per section when Stage 1b turns this into a gate.

8. MSPLIM guard is correct for GCC, not for clang. GCC 13 arm-cpus.in: armv7e-m → base 7EM, armv8-m.main → base 8M_MAIN, armv8.1-m.main → base 8M_MAIN; arm.cc:3834 builds __ARM_ARCH_<base>__, so __ARM_ARCH_8M_MAIN__ is defined for both -mcpu=cortex-m33 and -mcpu=cortex-m55 and not for cortex-m4. Clang/armclang define __ARM_ARCH_8_1M_MAIN__ for the M55 and would silently skip the write; CMSIS-Core tests both. #if defined(__ARM_ARCH_8M_MAIN__) || defined(__ARM_ARCH_8_1M_MAIN__).

9. Style / banners. cortexm_startup.c: irqLock / irqRestore are camelCase (STYLE §1), and the file is never tidy-checked because the driver compiles it from the link line, so it is not in compile_commands.json (style.yml iterates that). bare_metal_main.cpp puts SPDX/Copyright after 35 lines of prose; every sibling tests/test_*.cpp puts them on lines 1–2. The three .ld files carry no SPDX at all. Rename, move the banners, add /* SPDX-License-Identifier: MIT */ to the scripts.

10. actions/checkout@v4 produces a Node 20 deprecation ##[warning] in every one of the four job logs (pre-existing on the hosted jobs too); the new job could use v5.

Checked and not at issue

  • AN386 script vs mps2.c (mps2_common_init, lines 165–190 / 206–210): SSRAM1 4 MB @ 0, mirror @ 0x400000; SSRAM2/3 4 MB @ 0x20000000; PSRAM 16 MB @ 0x21000000; block RAM 16 KB @ 0x01000000. VTOR resets to 0 on the non-TZ MPS2 boards, .vectors is first in CODE, stack top 0x20400000 is 8-aligned, __stack_limit is defined, LMA == VMA everywhere (no AT>), so the ELF loader places .data and the startup rightly copies nothing.
  • AN505: DATA 4 MB @ 0x38000000 is ssram-1 + ssram-2 (2 + 2 MB, contiguous at NS 0x28000000, secure alias 0x38000000); init_svtor = 0x10000000. AN547 sram 2 is 4 MB, the script's 2 MB is conservative.
  • TAP_DSP_FFT_CMSIS OFF CACHE BOOL in the M4/M33 toolchains: the toolchain runs inside project(), option() never overrides an existing cache entry, and CMP0077 concerns normal variables only, so the pin wins unconditionally; the M55 log alone prints float32 FFT backend = CMSIS-DSP Helium.
  • CMAKE_TRY_COMPILE_PLATFORM_VARIABLES TAP_DSP_M4_FPU: the forwarded normal variable survives the toolchain's set(... CACHE BOOL) under CMP0126 NEW (floor is 3.22), so both reads pick the same float ABI.
  • Every suite and test name in the positive filter exists (25 suites = the count the host run reports); fir_kernels_test/*.* selects all three profiles; no GTEST_SKIP in the current tree. Excluded suites (double psola/pvoc/yin/log_mel/nn, the Kaiser prototype searches, MultitoneAnalysis) are the right cut: on the host the float psola/pvoc tests already top the selection at 86 / 40 / 34 / 32 ms.
  • Startup order (CPACR + dsb; isb, bss clear, initialise_monitor_handles, __libc_init_array, exit(main(0, NULL))) is MuTap's file byte-for-byte apart from the guard and the banner; the CPACR write is RAZ/WI on an FPU-less M4 and harmless in the soft-float build.
  • Toolchain flags: fpv4-sp-d16 is the M4F FPU name; -mfloat-abi=soft selects newlib's thumb/v7e-m/nofp multilib, and rdimon.specs + -nostartfiles is correct for both ABIs (the legs link and run, which is the proof).

Verdict: COMMENT — sound on its own, but do not merge until (1) is settled with #24 in a way both PRs' CI exercise, and (2)/(3) land so a red leg is visible and cheap.


Generated by Claude Code

Blocker (integration with the Stage 2a tests, PR #24): tests/CMakeLists.txt
now hoists include(GoogleTest) and provides
  tap_dsp_add_gtest_executable(<target> SOURCES ... [MAIN_FILTER <f>] [LABELS ...])
which in hosted mode links gtest_main and discovers tests, and under
TAP_DSP_BARE_METAL links gtest + bare_metal_main.cpp and registers one
CTest test judged on the completion marker. tap_dsp_tests uses it; the
four toolchains default TAP_DSP_PARITY_MAX_N to 4096 so #24's parity
target fits the legs.

- bare_metal_main.cpp: NEGATIVE filter (TAP_DSP_BARE_METAL_FILTER, default
  "*"), so every suite a later PR compiles in runs on target unless
  excluded by name; the marker carries selected= and skipped=, and a skip
  or an empty selection is rc=1. Hard-coded emulation times removed (they
  matched neither run; TCG wall is noise). Banner on lines 1-2.
- cortexm_startup.c: HardFault/Default handlers print TAP_DSP_TESTS_FAULT
  with the IPSR value over semihosting and _exit(2); the marker is a
  FAIL_REGULAR_EXPRESSION and the test TIMEOUT drops to 600 s. MSPLIM
  guard also accepts __ARM_ARCH_8_1M_MAIN__ (clang's M55). irq_lock /
  irq_restore in house case.
- Provenance: platform/README.md (copies of MuTap @ 142361b <- SampleRateTap,
  canonical here, duplication deliberate pending taphouse), SPDX lines in
  the .ld files, the inherited "Ported from RatioTap" line corrected.
- Toolchains: -Wno-psabi on the C++ flags (the ~26 GCC notes per leg).
- ci.yml: runs-on ubuntu-24.04; apt --no-install-recommends with newlib and
  the C++ runtime named; -DTAP_DSP_WERROR=ON on the embedded configure;
  ctest -V on the emulated step so counts and the marker are in the log;
  size -A alongside the Berkeley totals; checkout@v5; hosted configure
  passes -DTAP_DSP_BUILD_BENCH=ON for the bench PR; a guarded verbose
  `ctest -L parity -R default_flags` step for #24's ulp table.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
tap added a commit that referenced this pull request Sep 17, 2026
Review findings A2-A12 and B3, B4, B6-B10 on #24; the #17-coupled items
(bare-metal registration through the shared helper) follow in a second
commit once the helper lands.

- Informational target: a second private reference library at default
  flags replaces the link to tap::dsp_fft (which Stage 2c removes and which
  carries CMSIS objects on the M55); the report is one test for both
  precisions and --gtest_output=xml writes its RecordProperty values to
  parity-ulp.xml in the build tree; the test-file comment no longer claims
  the table is in every CI log (ctest hides passing stdout; CI runs the
  label with -V as its own step, Part 13).
- TAP_DSP_PARITY_MAX_N defaults to 4096 when CMAKE_CROSSCOMPILING, is
  static_asserted to be a power of two >= 4, and sizes_up_to_65536() is
  renamed sweep_sizes(). MSVC /fp:precise claim qualified (VS 2022 17.0+).
  objdump FMA-count recipe and the Stage 2c fftsg_float.c hazard recorded
  in the parity file header.
- Oracle: profile<Sample> carries forward_scale(n), inverse_scale(n),
  tolerance(n, norm2) and k_full_scale so Q15/Q31 plug in without touching
  the tests; the Higham comment states mu ~ few u (makewt derives half the
  table arithmetically), that the constant is derived with slack (measured
  0.25/0.17), and why the 2-norm-per-element bound confines the DFT
  comparison to N <= 256; the inverse edge term goes through TwoSum;
  ImpulseHasFlatSpectrum renamed ImpulseIsFlatAtEverySize and the overlap
  with test_fft.cpp stated in the header; self-check constants derived and
  their measured maxima recorded.
- RT guard: the header states it counts C++ operator new only (malloc and
  vendor-internal allocation are invisible); the GCC pragma is push/pop
  scoped to the replacement functions; TransformsAreNoexcept says why it
  exists beside the static_asserts.
- signals.h: db_from_ratio dropped until a caller exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy

tap commented Sep 17, 2026

Copy link
Copy Markdown
Owner Author

Fix pass for both reviews: 09a584e, CI 35268481640 all seven jobs green

Per-leg from the -V logs (this time the numbers are in the logs, not inferred): every leg prints Running 114 tests from 25 test suites, [ PASSED ] 114 tests. and TAP_DSP_TESTS_COMPLETE rc=0 selected=114 skipped=0; ctest wall 82.39 / 91.90 / 92.64 / 13.63 s; .text per size -A 852 752 / 846 888 / 845 936 / 905 224 B (m4-softfp / m4f / m33 / m55); zero warnings and zero -Wpsabi notes under -DTAP_DSP_WERROR=ON. The hosted parity step correctly reports "no parity-labelled tests in this tree" until #24 lands. PR body rewritten accordingly.

Review A (correctness)

# finding status
1 Blocker: merged with #24 the legs do not configure/build; positive filter hides #24's suites Applied. include(GoogleTest) hoisted; tap_dsp_add_gtest_executable(<target> SOURCES ... [MAIN_FILTER ...] [LABELS ...]) does hosted gtest_main+discovery or bare-metal gtest+bare_metal_main.cpp+one add_test; TAP_DSP_PARITY_MAX_N=4096 cache default in all four toolchains; helper documented in the file header. Scratch merge of 4ddff35 onto this: host one-shot 161/4/2 selected, 0 skipped; hosted 215/215. Exact block for #24 posted there.
2 Nothing in the logs supports the counts/times; marker should carry selected/skipped and fail on skip; drop hard-coded times Applied. ctest -V on the emulated step; marker is rc=<n> selected=<n> skipped=<n>, skipped>0 or selected==0rc=1; PASS regex is "TAP_DSP_TESTS_COMPLETE rc=0 ". Hard-coded times removed from bare_metal_main.cpp and ci.yml; both say wall is TCG noise and point at the log.
3 A fault costs 30 min and leaves no diagnostic Applied. Both handlers print TAP_DSP_TESTS_FAULT <what> ipsr=<n> via write(2, ...) and _exit(2); marker added to FAIL_REGULAR_EXPRESSION; TIMEOUT 600; bkpt kept under #ifdef TAP_DSP_FAULT_BKPT. Honest limit: no leg has faulted, so the path is proven by reading, not by a run.
4 26–28 -Wpsabi notes per leg Applied. -Wno-psabi in the four CMAKE_CXX_FLAGS_INIT; the new logs carry none.
5 ubuntu-latest vs the 24.04 assumption Applied. runs-on: ubuntu-24.04; the "Toolchain versions" step stays as the drift detector.
6 apt line depends on Recommends Applied. --no-install-recommends gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib qemu-system-arm; install step 30–57 s instead of 35–60 s and ~70 fewer packages.
7 Berkeley size is the wrong instrument for ceilings Applied. size -A printed next to the Berkeley line; the Stage 1b ceilings should read .text/.rodata per section.
8 MSPLIM guard misses clang's __ARM_ARCH_8_1M_MAIN__ Applied.
9 irqLock/irqRestore case; banner position; SPDX on .ld Applied. irq_lock/irq_restore; bare_metal_main.cpp banner on lines 1–2; /* SPDX-License-Identifier: MIT */ + copyright on all three scripts. The startup file is still not tidy-checked by the Style job (compiled from the link line); a host gcc -fsyntax-only -Wall -Wextra -Wconversion -Wshadow pass and clang-format are the local substitute.
10 actions/checkout@v4 deprecation Applied in ci.yml (v5). style.yml is canonical and left alone.

Review B (process / downstream)

# finding status
1 Per-leg count and "parity suites passed" not in any log Applied (same as A2): -V; the suite list and [ PASSED ] 114 tests. are in each leg's log.
2 Legs did not build with TAP_DSP_WERROR Applied. -DTAP_DSP_WERROR=ON on the embedded configure; green on all four.
3 ubuntu-latest Applied (A5).
4 Tidy claim not backed by CI for the two new files Applied as the first option: the PR body now states tidy/format on bare_metal_main.cpp and cortexm_startup.c were local and why they are outside the hosted compile database. Declined the hosted OBJECT/tidy-only target: it would add a fake TU (a main under bare-metal defines) to every hosted build for one file; if the house wants it, it belongs in the Style workflow, which is canonical.
5 Cross-PR: #24 conflict and semantic hazards; 2a/3a/5 suites silently not run; add a sentence at target_sources Applied. The helper and negative filter make the 2a/3a/5 suites run on target by default; the comment above tap_dsp_add_gtest_executable(tap_dsp_tests ...) states that adding a source puts it on the legs and that MAIN_FILTER is the complete exclusion list. Merge order recorded in the PR body.
6 Branch protection off; CLAUDE.md:69 stale Recorded, not actionable here. Both flagged in the PR body for the repo owner and the docs owner; CLAUDE.md is outside this PR's ownership.
7 Provenance and intent of the third copy only in the PR body Applied. platform/README.md (source files, MuTap 142361b, chain SampleRateTap → RatioTap → MuTap → here, canonical here from the first consumer pin, duplication deliberate pending taphouse), header lines in both .ld files, and the misleading "Ported from RatioTap's" line replaced.
8 checkout@v4 Applied (A10).
9 PR body stale; duplicate footer; unmeasured attributions Applied. Body rewritten with the run id and the table; one footer; the two attributions withdrawn in the body.
10–11 Part 10 record; commit hygiene No action.

Also in this pass (from the coordinator, not in either review)

Not changed: fft.h, root CMakeLists.txt, CLAUDE.md, bench.yml.

🤖 Generated with Claude Code

https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy


Generated by Claude Code

tap added a commit that referenced this pull request Sep 17, 2026
Review findings A2-A12 and B3, B4, B6-B10 on #24; the #17-coupled items
(bare-metal registration through the shared helper) follow in a second
commit once the helper lands.

- Informational target: a second private reference library at default
  flags replaces the link to tap::dsp_fft (which Stage 2c removes and which
  carries CMSIS objects on the M55); the report is one test for both
  precisions and --gtest_output=xml writes its RecordProperty values to
  parity-ulp.xml in the build tree; the test-file comment no longer claims
  the table is in every CI log (ctest hides passing stdout; CI runs the
  label with -V as its own step, Part 13).
- TAP_DSP_PARITY_MAX_N defaults to 4096 when CMAKE_CROSSCOMPILING, is
  static_asserted to be a power of two >= 4, and sizes_up_to_65536() is
  renamed sweep_sizes(). MSVC /fp:precise claim qualified (VS 2022 17.0+).
  objdump FMA-count recipe and the Stage 2c fftsg_float.c hazard recorded
  in the parity file header.
- Oracle: profile<Sample> carries forward_scale(n), inverse_scale(n),
  tolerance(n, norm2) and k_full_scale so Q15/Q31 plug in without touching
  the tests; the Higham comment states mu ~ few u (makewt derives half the
  table arithmetically), that the constant is derived with slack (measured
  0.25/0.17), and why the 2-norm-per-element bound confines the DFT
  comparison to N <= 256; the inverse edge term goes through TwoSum;
  ImpulseHasFlatSpectrum renamed ImpulseIsFlatAtEverySize and the overlap
  with test_fft.cpp stated in the header; self-check constants derived and
  their measured maxima recorded.
- RT guard: the header states it counts C++ operator new only (malloc and
  vendor-internal allocation are invisible); the GCC pragma is push/pop
  scoped to the replacement functions; TransformsAreNoexcept says why it
  exists beside the static_asserts.
- signals.h: db_from_ratio dropped until a caller exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
tap added a commit that referenced this pull request Sep 17, 2026
Rebased onto claude/wave1-stage1-legs. The two parity executables now go
through tap_dsp_add_gtest_executable, so they are discovered hosted and
one-shot under QEMU like tap_dsp_tests; the block no longer calls
include(GoogleTest) or gtest_discover_tests itself. Consequences:

- The 2^20 tests are compiled out (not GTEST_SKIP'ped) when
  TAP_DSP_PARITY_MAX_N is below 2^20, because the one-shot main counts a
  skip as a failed gate; the knob defaults to 4096 when CMAKE_CROSSCOMPILING
  and the toolchains' own cache value is honoured.
- The oracle's closed-form sweep is capped by the same knob (tap_dsp_tests
  gets the definition), so no 65536-point buffers on the MPS2 legs.
- The JUnit XML for the informational report is attached as a GTEST_OUTPUT
  ENVIRONMENT property via a ctest-time TEST_INCLUDE_FILES script, since the
  helper has no EXTRA_ARGS/PROPERTIES pass-through; hosted only.

Verified on the host: GCC 13 and Clang 18 hosted (214/214, -Werror clean),
-DTAP_DSP_BARE_METAL=ON host run (three one-shot markers, rc=0,
selected=161/4/1, skipped=0, oracle and RT suites running under the
negative filter), -DTAP_DSP_PARITY_MAX_N=4096 hosted (2^20 tests absent).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
@tap
tap merged commit 332261b into main Sep 17, 2026
18 checks passed
tap added a commit that referenced this pull request Sep 17, 2026
Review findings A2-A12 and B3, B4, B6-B10 on #24; the #17-coupled items
(bare-metal registration through the shared helper) follow in a second
commit once the helper lands.

- Informational target: a second private reference library at default
  flags replaces the link to tap::dsp_fft (which Stage 2c removes and which
  carries CMSIS objects on the M55); the report is one test for both
  precisions and --gtest_output=xml writes its RecordProperty values to
  parity-ulp.xml in the build tree; the test-file comment no longer claims
  the table is in every CI log (ctest hides passing stdout; CI runs the
  label with -V as its own step, Part 13).
- TAP_DSP_PARITY_MAX_N defaults to 4096 when CMAKE_CROSSCOMPILING, is
  static_asserted to be a power of two >= 4, and sizes_up_to_65536() is
  renamed sweep_sizes(). MSVC /fp:precise claim qualified (VS 2022 17.0+).
  objdump FMA-count recipe and the Stage 2c fftsg_float.c hazard recorded
  in the parity file header.
- Oracle: profile<Sample> carries forward_scale(n), inverse_scale(n),
  tolerance(n, norm2) and k_full_scale so Q15/Q31 plug in without touching
  the tests; the Higham comment states mu ~ few u (makewt derives half the
  table arithmetically), that the constant is derived with slack (measured
  0.25/0.17), and why the 2-norm-per-element bound confines the DFT
  comparison to N <= 256; the inverse edge term goes through TwoSum;
  ImpulseHasFlatSpectrum renamed ImpulseIsFlatAtEverySize and the overlap
  with test_fft.cpp stated in the header; self-check constants derived and
  their measured maxima recorded.
- RT guard: the header states it counts C++ operator new only (malloc and
  vendor-internal allocation are invisible); the GCC pragma is push/pop
  scoped to the replacement functions; TransformsAreNoexcept says why it
  exists beside the static_asserts.
- signals.h: db_from_ratio dropped until a caller exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
tap added a commit that referenced this pull request Sep 17, 2026
Rebased onto claude/wave1-stage1-legs. The two parity executables now go
through tap_dsp_add_gtest_executable, so they are discovered hosted and
one-shot under QEMU like tap_dsp_tests; the block no longer calls
include(GoogleTest) or gtest_discover_tests itself. Consequences:

- The 2^20 tests are compiled out (not GTEST_SKIP'ped) when
  TAP_DSP_PARITY_MAX_N is below 2^20, because the one-shot main counts a
  skip as a failed gate; the knob defaults to 4096 when CMAKE_CROSSCOMPILING
  and the toolchains' own cache value is honoured.
- The oracle's closed-form sweep is capped by the same knob (tap_dsp_tests
  gets the definition), so no 65536-point buffers on the MPS2 legs.
- The JUnit XML for the informational report is attached as a GTEST_OUTPUT
  ENVIRONMENT property via a ctest-time TEST_INCLUDE_FILES script, since the
  helper has no EXTRA_ARGS/PROPERTIES pass-through; hosted only.

Verified on the host: GCC 13 and Clang 18 hosted (214/214, -Werror clean),
-DTAP_DSP_BARE_METAL=ON host run (three one-shot markers, rc=0,
selected=161/4/1, skipped=0, oracle and RT suites running under the
negative filter), -DTAP_DSP_PARITY_MAX_N=4096 hosted (2^20 tests absent).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
tap added a commit that referenced this pull request Sep 17, 2026
#24)

* Add the Stage 2a FFT test suite: Ooura parity gate, independent oracle, RT guard

Written first, against the vendored C, so the wave-2 port works test-first
(docs/audit-fft-and-code-smells.md, Part 3 Stage 2a and Part 9).

- tests/test_fft_parity_ooura.cpp + target tap_dsp_fft_parity: memcmp bit
  identity of basic_real_fft<double|float> against raw rdft/rdft_f from a
  private reference build of fftsg.c/fftsg_float.c, both sides compiled with
  -ffp-contract=off (MSVC: nothing), forward and inverse, N = 4..65536 plus
  2^20, on five materials. ooura_ref and engine_under_test are each
  re-pointable in one place. TAP_DSP_PARITY_MAX_N (default 2^20) caps the
  sizes for emulated targets. A second target,
  tap_dsp_fft_parity_default_flags, prints the measured max-ulp deviation
  per N at default flags and never fails (Part 6, N3).
- tests/test_fft_oracle.cpp: closed-form vectors (impulse, DC, Nyquist,
  on-bin cosine/sine with the +i convention, two-tone) and their
  unnormalized inverses, to a tolerance derived from Higham's FFT bound
  (4 * eps * log2 N * ||y||_2, measured 0.25/0.17 of it for double/float);
  plus a double-double (TwoSum/TwoProd) compensated DFT for N <= 256, with
  a profile<Sample> extension point for the fixed-point stage.
- tests/test_fft_rt.cpp: static_assert(noexcept) on the four transforms and
  size queries; a global operator new/delete counting guard proving the
  four calls allocate nothing at N = 512 and 4096, on the first call after
  construction and in steady state; copy and copy-assignment bit-identical.
- tests/support/signals.h: one xorshift32, random_signal, tone (exact
  modular phase reduction) and db_from_ratio for the new files.
- tests/CMakeLists.txt: additive, delimited block at the end.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy

* Address the wave-1 reviews on the Stage 2a suite (host-side items)

Review findings A2-A12 and B3, B4, B6-B10 on #24; the #17-coupled items
(bare-metal registration through the shared helper) follow in a second
commit once the helper lands.

- Informational target: a second private reference library at default
  flags replaces the link to tap::dsp_fft (which Stage 2c removes and which
  carries CMSIS objects on the M55); the report is one test for both
  precisions and --gtest_output=xml writes its RecordProperty values to
  parity-ulp.xml in the build tree; the test-file comment no longer claims
  the table is in every CI log (ctest hides passing stdout; CI runs the
  label with -V as its own step, Part 13).
- TAP_DSP_PARITY_MAX_N defaults to 4096 when CMAKE_CROSSCOMPILING, is
  static_asserted to be a power of two >= 4, and sizes_up_to_65536() is
  renamed sweep_sizes(). MSVC /fp:precise claim qualified (VS 2022 17.0+).
  objdump FMA-count recipe and the Stage 2c fftsg_float.c hazard recorded
  in the parity file header.
- Oracle: profile<Sample> carries forward_scale(n), inverse_scale(n),
  tolerance(n, norm2) and k_full_scale so Q15/Q31 plug in without touching
  the tests; the Higham comment states mu ~ few u (makewt derives half the
  table arithmetically), that the constant is derived with slack (measured
  0.25/0.17), and why the 2-norm-per-element bound confines the DFT
  comparison to N <= 256; the inverse edge term goes through TwoSum;
  ImpulseHasFlatSpectrum renamed ImpulseIsFlatAtEverySize and the overlap
  with test_fft.cpp stated in the header; self-check constants derived and
  their measured maxima recorded.
- RT guard: the header states it counts C++ operator new only (malloc and
  vendor-internal allocation are invisible); the GCC pragma is push/pop
  scoped to the replacement functions; TransformsAreNoexcept says why it
  exists beside the static_asserts.
- signals.h: db_from_ratio dropped until a caller exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy

* Register the parity targets through the bare-metal helper (#17 coupling)

Rebased onto claude/wave1-stage1-legs. The two parity executables now go
through tap_dsp_add_gtest_executable, so they are discovered hosted and
one-shot under QEMU like tap_dsp_tests; the block no longer calls
include(GoogleTest) or gtest_discover_tests itself. Consequences:

- The 2^20 tests are compiled out (not GTEST_SKIP'ped) when
  TAP_DSP_PARITY_MAX_N is below 2^20, because the one-shot main counts a
  skip as a failed gate; the knob defaults to 4096 when CMAKE_CROSSCOMPILING
  and the toolchains' own cache value is honoured.
- The oracle's closed-form sweep is capped by the same knob (tap_dsp_tests
  gets the definition), so no 65536-point buffers on the MPS2 legs.
- The JUnit XML for the informational report is attached as a GTEST_OUTPUT
  ENVIRONMENT property via a ctest-time TEST_INCLUDE_FILES script, since the
  helper has no EXTRA_ARGS/PROPERTIES pass-through; hosted only.

Verified on the host: GCC 13 and Clang 18 hosted (214/214, -Werror clean),
-DTAP_DSP_BARE_METAL=ON host run (three one-shot markers, rc=0,
selected=161/4/1, skipped=0, oracle and RT suites running under the
negative filter), -DTAP_DSP_PARITY_MAX_N=4096 hosted (2^20 tests absent).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy

* Confine the float oracle sweeps to the CMSIS backend's 32..4096 on the M55

The first QEMU run of the rebased suite HardFaulted in
fft_oracle_test/0.ImpulseIsFlatAtEverySize at N = 4 on the cortex-m55 leg,
the one leg where basic_real_fft<float> routes through CMSIS-DSP:
arm_rfft_fast_init_f32 accepts only 32..4096 and fft.h's wrapper does not
check its return status, so fft.h's ">= 4" contract is undefined behaviour
there below 32 (the existing battery starts at 64, which is why it never
showed). The oracle's profile<Sample> now carries the engine's size range
(k_min_n / k_max_n; 32..4096 for float under TAP_DSP_FFT_CMSIS, 4..2^20
otherwise) and every sweep is built from it. The wrapper defect itself is
recorded for the fft.h owner, not papered over here.

Also: the informational table printed "zu" for N on newlib-nano; %lu now.

Hosted GCC 13 / Clang 18: 214/214, -Werror clean; host one-shot mode:
three markers rc=0, selected=161/4/1, skipped=0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant