Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/skills/build-compilation-dependencies/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ Resolution pitfalls learned the hard way (both handled inside `package-root.js`)
| iOS compile error `unknown type 'id'` | C++ file included ObjC-only header | Compile that file as ObjC++ (separate subspec with `-x objective-c++`) |
| `RCT_NEW_ARCH_ENABLED` undefined on Android | Old RN gradle plugin | Ensure `newArchEnabled=true` in app's `gradle.properties` |
| iOS: `'to_chars' is unavailable: introduced in iOS 16.3` from `formatter_floating_point.h`, instantiated by `std::format<...>` | `std::format` in code compiled for iOS. libc++ availability-gates the whole `<format>` library to iOS 16.3; the podspec minimum is `ios_min_version = '14.0'`. The desktop C++ test build and Android NDK have no such gate, so `yarn test:cpp` passes and only the iOS build fails. | Use `std::string` concatenation / `std::to_string` in `common/cpp` and `ios/`. Zero-pad by hand (`insert(0, n, '0')`). Android-only files (`android/src/main/cpp`) may keep `std::format`. |
| Android incremental build only: `Unresolved reference` to a top-level Kotlin function, or `Cannot access '…': it is internal in file`, while `--rerun-tasks` passes | Incremental Kotlin rounds treat the module's own earlier classes as a foreign module (library is built through the `node_modules/react-native-audio-api` symlink) | Use only public classes/objects across files in `android/src/main/java`; no top-level functions, no `internal` |
| clangd only: `'React/RCTBridgeModule.h' file not found` in `.mm` files | `compile_commands.json` has no framework search path | See *clangd compile database* below — regenerate with `yarn setup:clangd` |

## clangd compile database
Expand Down
11 changes: 11 additions & 0 deletions .claude/skills/thread-safety-itc/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,17 @@ Tests stay deterministic by writing at most a pool's worth of buffers (32) per o

---

## Convolver tail prefetch: never `wait()` on the audio thread

`ConvolverNode` does not block on `ConvolverThreadPool`. `Convolver` (`dsp/Convolver.h`) splits the uniformly partitioned IR at `HEAD_SEGMENTS` (8 blocks): the head is multiplied on the audio thread in `process()`, the tail only needs input that is ≥ 8 blocks old, so `ConvolverNode::scheduleTails()` hands `computeTail(nextTailBlock())` to the pool right after `process()` and the worker has 8 quanta (≈21 ms) of slack. Output block k is Σ_i X[k-i]·H[i] over the IR partitions H[i]; only the first `HEAD_SEGMENTS` terms depend on recent input, which is why the tail of block k+HEAD_SEGMENTS is computable as soon as block k is in. A tail that is not published in time is computed inline for that block (the private `_tailMisses` counter, read in tests through `ConvolverInspector`) — a late worker costs CPU, never a stall or a glitch. Rules that keep it lock-free:

- Tasks capture the `Convolver` by `shared_ptr` (never `this` / a node member), so a node released mid-flight cannot leave a dangling read (the second half of issue #1339).
- Each of the `TAIL_SLOT_COUNT` (= HEAD_SEGMENTS + 1) result slots has one atomic `state`: `readyState(block) = (block + 1) << 1`, plus `BUSY_BIT` while a worker writes. A worker claims with one `compare_exchange_strong` that succeeds only if the slot is neither busy nor owned by a newer block, so a straggler preempted anywhere can never overwrite a newer block's spectrum — a separate deadline re-check could not guarantee that. The claim `memset`s the spectrum before accumulating (`accumulateSegments` adds), and publishes `readyState(block)` with release.
- `process(block)` uses a slot only on an exact `== readyState(block)` match (acquire). That match proves the worker's delay-line reads finished before `process(block)`, and the first overwrite of anything it read is `process(block + 9)`, so a consumed result is always built from valid input. The audio thread never writes a slot outside `init`.
- A worker still reading the delay line ≥ 17 quanta after scheduling races `doFFT` on `_segments` (formally UB, result tagged with an already rendered block, so never read). Known and accepted; early abort in `accumulateSegments` would make it rarer, not impossible.
- The node hands tails over with `ThreadPool::trySchedule` (wait-free, `try_send` on the load-balancer queue). `schedule()` sends `WAIT_ON_FULL` and blocks the caller when the load-balancer queue (`CONVOLVER_THREAD_POOL_LOAD_BALANCER_QUEUE_SIZE`) is full, so it must never be called from the audio thread (in a device bench with starved workers the queue never actually filled — the balancer drains it in microseconds — but the contract matters more than the odds). A refused tail is just a miss: `process()` computes it inline.
- `ThreadPool::wait()` waits on `tasksScheduled` itself (atomic wait loop), not on a separate flag: a worker that finished batch N and was preempted before notifying could otherwise release the waiter of batch N+1 (issue #1339). Nothing on the audio thread should call `wait()` at all.

## Driver synchronization (layered model)

Control-plane synchronization uses two layers — both are non-recursive `std::mutex`, never held on the audio thread.
Expand Down
Loading
Loading