Skip to content

macos dev compat#1

Draft
jonathan-reichardt wants to merge 13 commits into
masterfrom
feature/macos-dev-compat
Draft

macos dev compat#1
jonathan-reichardt wants to merge 13 commits into
masterfrom
feature/macos-dev-compat

Conversation

@jonathan-reichardt

Copy link
Copy Markdown

No description provided.

Replace inline sched_setscheduler/sched_setaffinity/clock_nanosleep
calls with calls into the new oals::rt namespace from
OpenAudioNetwork/netutils/platform/rt.h. Delete the local
set_thread_realtime/set_running_cpu helpers and the #include
<linux/sched.h> in both files. No behavior change on Linux: same
SCHED_FIFO priorities (engine 25/20/80, io_sim 50), same SCHED_RR (99,
io_sim main), same CPU affinities (engine 1/1/2/3), same CLOCK_MONOTONIC
relative sleeps (100 ns + 10000 ns in engine, ~667 µs in io_sim).

Plus bumped OpenAudioNetwork submodule pointer to the matching M1
commit that introduces the shim and the NOT EMBEDDED_BUILD gate.

After M1+M2 the engine and io_sim will compile on macOS; linking
still depends on M3's transport seam (the four extern "C" hooks that
LowLatSocket's #else arm currently references but doesn't define on
non-Linux hosts).

See Docs/dev-tooling-plan.md M1.
CMake plumbing + source hygiene so the OALS tree configures and
compiles on macOS up to the M3-scope link gap. Linking still depends
on M3's transport seam (__send_data / __recv_data / __fetch_iface_meta
and NetworkMapper::get_mac_by_uid remain undefined on non-Linux hosts).

Top-level CMakeLists.txt:
  - Gate the aarch64 / -ftree-vectorize block with AND NOT APPLE so it
    no longer fires on Apple Silicon (which reports as aarch64 on some
    toolchains, arm64 on others; either way Apple Clang auto-vectorizes
    at -O2+, so the flag is redundant). Linux ARM (Pi, embedded) still
    picks it up.
  - Add option(OAN_HOST_BACKENDS "Build host dev transports + RT shim"
    OFF), force ON via CACHE BOOL ... FORCE when APPLE so subdirectory
    scopes see the override (a plain set() shadows but does not update
    the cache), propagate as add_compile_definitions. No code consumes
    the macro yet — it is plumbing for M3 transport sources.

io_sim ALSA dead-code purge:
  The dev-tooling plan claimed io_sim's ALSA use was "already entirely
  commented out." That was wrong — only the call and the playback_thread
  consuming it were commented; the snd_pcm_t* alsa_setup() function
  body and #include <alsa/asoundlib.h> were live. Decision was to
  delete outright rather than wrap in #ifdef __linux__: io_sim is
  marked deprecated in CLAUDE.md, the VSC track supersedes its playback
  path, and ALSA cannot exist on macOS. Removes -107 lines from
  io_sim/main.cpp plus the ALSA pkg_check_modules and link in
  io_sim/CMakeLists.txt. SNDFILE stays REQUIRED (used live by
  gen_packet_strm_from_file for stem playback; available on macOS via
  brew install libsndfile).

SNDFILE include-dir propagation:
  io_sim and debugger now pass ${SNDFILE_INCLUDE_DIRS} into
  target_include_directories. Linux happened to work because
  /usr/include is searched by default; Homebrew installs to
  /opt/homebrew/include which is not.

Qt include de-Debianisation in plugins/loader/PipeDesc.h:
  #include <qt6/QtGui/QPainter> etc. was Debian/Ubuntu-specific
  (their Qt6 headers live under /usr/include/qt6/). Standard Qt6
  CMake config — used by Homebrew, Arch, Fedora, Alpine, and upstream
  Qt — adds the include dir directly and expects the canonical
  <QtGui/QPainter> form. Linux Debian/Ubuntu users keep working because
  Qt6::Widgets is already linked everywhere these headers are used.

Two missing #include <iostream>:
  engine/NetMan.cpp and debugger/DebuggerWindow.cpp use std::cerr /
  std::cout but did not include <iostream>. Previously transitive via
  other headers; the Mac build path no longer pulls it in. Direct
  include is correct hygiene.

Plus .gitignore for build/ dirs.

Verified on macOS: cmake configure + cmake --build produces 72 .cpp.o
files with 0 compile errors. Only failures are the documented M3-scope
undefined symbols at the oannetutils.dylib link step.

See Docs/dev-tooling-plan.md M2.
Bump OpenAudioNetwork submodule to the M3 commit that introduces the
ITransport seam, so OALSEngine, io_simulator, OALSCoreUI, debugger,
and the builtin plugins all link cleanly on macOS for the first time
(M2 only got them to compile; oannetutils.dylib was failing to link
with four undefined symbols — __send_data, __recv_data,
__fetch_iface_meta, NetworkMapper::get_mac_by_uid). After M3 the
SimTransport stub is reachable: ./OALSEngine sim:default routes
through it and exits cleanly with a "not yet implemented (M4)"
message. Plain ifname on Mac fails loudly with a clear "this host
requires a transport prefix" message.

Plus a CMake fix the M3 link surfaced: target_link_directories with
${SNDFILE_LIBRARY_DIRS} on both io_simulator and debugger. M2 added
${SNDFILE_INCLUDE_DIRS} for the compile step but missed the parallel
${SNDFILE_LIBRARY_DIRS} for the link step. On Linux this happened to
work because /usr/lib is searched by default; on Mac, Homebrew's
/opt/homebrew/lib is not. Without this, `ld: library 'sndfile' not
found` blocks io_simulator and debugger from linking. M2 didn't
surface this because the M2 link gap stopped earlier in the chain.

See OpenAudioLiveSystem/Docs/dev-tooling-plan.md M3 and the OAN
submodule's M3 commit (61ed291) for the transport seam details.
New tools/sim_switch/ — single-threaded poll() user-space switch daemon
emulating a switched L2 segment over AF_UNIX. Pure POSIX, builds on
Linux + macOS. Live TUI with traffic rates, device discovery decode,
per-conn drop counters; --headless mode for CI. 7-scenario GTest smoke
suite (hello, broadcast, unicast, unknown-unicast, bad magic, slow
client, SimTransport interop). Submodule bump pulls in OAN's real
SimTransport implementation.

Wire framing (sim_proto.h) is owned here, not in OAN — OAN is vendored
into firmware repos and must stay free of host-dev-only contracts.
SimTransport keeps a byte-identical local copy.

Also in this commit (M4 verification follow-ups discovered on macOS):

- ENGINE_PLUGIN_SYSLOCATION is now $HOME/.osst/core_plugins on APPLE;
  the Linux /core_plugins path can't be created on macOS's sealed root
  volume. Linux target unchanged.
- io_simulator now respects argv[1] as the interface string, matching
  OALSEngine. Previously hardcoded to 'virbr0'.
- coreui/surface_config/surface.json uses sim:default as the default
  eth_interface so the UI works out of the box on macOS. Linux dev
  loops can override per-host.
- Top-level enable_testing() so ctest discovers sim_switch_smoke
  reliably across CMake versions.
io_sim's wav paths were hardcoded to a developer's home dir, so it
exited on every other machine. Replace with a JSON config (argv[2],
default ./io_sim.json) that lists tracks as either {channel, path} or
{channel, tone: {freq, gain}}.

- nlohmann/json pulled in via FetchContent (header-only, dev-only tool
  so the dep is acceptable; coreui still uses QJsonDocument).
- Tilde-expand paths so ~/Music/... works directly.
- Reject non-96k wavs with the exact ffmpeg command needed to convert.
- Tone-gen tracks for "is the wire alive?" testing without wavs.
- Loops normalized to shortest stream so the cursor wraps cleanly.
- io_sim.example.json: 4-channel tone demo (440/880/1k/1.5k Hz).

Submodule bump pulls in the NetworkMapper disco/age-loop throttle on
host backends.
sim_switch:
  - SimFrame v2: switch populates src_uid, so observers see who sent.
  - SimHello v2: flags field with SIM_HELLO_PROMISCUOUS for inspectors
    that want every fanout regardless of ethertype / dst_uid.
  - Per-peer tx/rx stat counters per ethertype, attributed during
    consume_frame and try_write. Pruned alongside disco entries.
  - TUI rates smoothed via EWMA (α=0.15, ~1s window) so disco's bursty
    5-sec interval no longer makes the dashboard oscillate between
    20/s and 0/s.
  - New Peers table sorted by uid with A/D/C/S tx/rx columns — actual
    data-flow visibility for installations with more than 2 nodes.
  - "Inspectors: N attached" line.

oaninspect (new tool):
  - Connects as a promiscuous client, ANSI-coloured per-EtherType
    pretty-print of every frame.
  - Filter expression with key=value[,value,...] grammar (ethertype/
    src/dst/peer/uid). Default suppresses audio.
  - Audio fast-path: when filter excludes audio, skip at ingest
    (4000 frames/s) so the 100k ring holds plenty of disco/sync/
    control history — audio still counted for the suppression hint.
  - Pause stops ring ingest (with pending counter), so the frozen
    view stays frozen even at high wire rates. Filter changes
    re-filter the existing ring so they work while paused.
  - Diff-render: only redraws rows whose content changed, no
    full-screen clear flicker.
  - Interactive: Space pause, j/k scroll, Ctrl-D/U half-page,
    g/G top/end, / re-filter, h hex, q quit. Cheat-sheet footer.
  - Non-TTY fallback: prints plain decoded lines to stdout for CI.
  - --pcap record / --replay (own binary format).

Tests:
  - +7 switch scenarios (promiscuous receives broadcasts +
    unicasts to others, src_uid populated by switch, multi-
    inspector, no loopback, promiscuous not in route table,
    v1 hello rejected).
  - 6 Filter parser unit tests.
  - 3 EWMA unit tests.
  - 23/23 passing.

OAN submodule bump pulls in the matching SimFrame v2 / SimHello v2
on the engine + io_sim + UI side.
OALSEngine idle on macOS was burning ~150 % CPU (and ~350 % with audio
flowing) because three loops weren't actually paced:

  - main thread:    while(true) NetMan::update_netman() — the function
                    body is empty. Pure busy spin, one full core.
  - pipe_updater:   precise_sleep_ns(100) between update_processes()
                    walks of all 64 disabled pipes. nanosleep floor on
                    macOS is ~3 µs, so it ticked at ~300 kHz.
  - clock_syncer:   ClockMaster::sync_process() does an async recv that
                    returns 0 on EAGAIN; the 10 µs precise_sleep_ns
                    between calls couldn't keep up.

io_sim's clock_thread had the same async-recv spin with no sleep at all.
The four real RT recv threads (audiopoll, controlpoll, packet_receiver)
were already cold — they call receive_data(async=false) which blocks in
the kernel correctly via SimTransport::recv's existing poll(-1).

All four fixes are gated by OAN_HOST_BACKENDS so the Linux RT path is
byte-identical. AudioEngine gains a CV pair (notify_block_ready /
wait_for_block); feed_pipe signals it from the audio recv callback so
pipe_updater can block instead of ticking. NetMan gains
clock_wait_or_tick(timeout_ms) that drains the sync poll-spin via
ClockMaster::wait_sync_readable before running the 1 s heartbeat.
Main thread parks on pause(). io_sim's clock_thread does the same
wait_sync_readable wrap.

Measured on macOS, OALSEngine:
  - idle (no io_sim):           151 % → 0.9 %
  - with io_sim audio flowing:  ~350 % → 6.0 %

Audio throughput preserved (~3960 audio frames/s on the switch). All
23 sim_switch tests still green.
OALSEngine and io_simulator now print a real usage block on --help
instead of trying to parse the flag as an interface name. Both
explain the sim:<name> transport spec inline so contributors don't
need to dig in CLAUDE.md.

scripts/dev-up.sh launches the 3-process stack (sim_switch +
OALSEngine + io_simulator, optionally + oaninspect) in a tmux
session. Idempotent: --down kills the session and clears the
socket; re-running cleans up stale processes first. Per-pane logs
tee'd to /tmp/osst-dev-<role>.log.

OAN bump pulls in the M7 Mac RT shim (THREAD_TIME_CONSTRAINT_POLICY
+ mlockall). No engine call site change — set_thread_realtime now
actually applies a budget on Mac instead of being a no-op.
CI: GitHub Actions runs on every push and PR to any branch, on
ubuntu-latest + macos-latest. Installs cmake, ninja, qt6, libsndfile,
gtest from package managers, then configures with OAN_HOST_BACKENDS=ON,
builds, and runs ctest.

clang-format: LLVM-based, K&R braces, 100-col limit, left-aligned
references (auto& is the codebase majority). Editor-on-save only —
no mass reformat.

pre-commit: clang-format + trailing-whitespace + EOF fixer + merge-
conflict + large-file guard. Excludes the OAN submodule (it has its
own config). Install via 'pip install pre-commit && pre-commit install'.
Linux ld is strict about undefined references in shared libs;
oannetutils' LowLatSocket pulls in NetworkMapper::get_mac_by_uid
which lives in oancommon. macOS' -undefined,dynamic_lookup link
option hid this until CI tried a real Linux build.
Real root cause of the Linux link failure: GNU ld defaults to
--as-needed since binutils 2.31. sim_switch_test's own code doesn't
reference any oancommon symbol directly, so the linker would drop
liboancommon.so from the link line — and the transitive reference
from oannetutils' LowLatSocket::get_mac to
NetworkMapper::get_mac_by_uid stayed unresolved.

Wrap both OAN .so's in --no-as-needed/--as-needed on Linux so both
are pinned in. macOS' ld64 has no --as-needed concept; plain list is
fine there.

Previous attempts (--unresolved-symbols=ignore-in-shared-libs, then
-z undefs at the .so level) only addressed building the .so itself,
not the executable link step.

OAN submodule bump: just a comment refresh in netutils/CMakeLists.
@jonathan-reichardt jonathan-reichardt self-assigned this Jun 3, 2026
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