Make the combination mode switchable at runtime - #20
Open
zeyuyang42 wants to merge 1 commit into
Open
zeyuyang42 wants to merge 1 commit into
zeyuyang42 wants to merge 1 commit into
Conversation
The mode was bound once in EnvelopeCombinator::initialize(), which resolved a string to a member-function pointer. Nothing could change it afterwards, so "add a message for mode change" needed the core to grow a setter first. Replace the member-function pointer with a CombMode enum held in a std::atomic, dispatched by a switch in perform(). Not std::atomic of the pointer-to-member: that is 16 bytes on the Itanium ABI and would not be lock-free, so set_mode() could block the audio thread on an internal lock. A static_assert pins the lock-free requirement. perform() loads the mode once per block, so a block is never processed partly in one mode and partly in another, and set_mode() is safe to call from a control thread with DSP running. This also removes existing UB. processFunc had no in-class initializer, so the `if (processFunc)` guard in perform() was reading an indeterminate value rather than testing for null. The mode now has a valid default and a `prepared` flag records whether initialize() actually ran. The constructor signature is unchanged and initialize() still returns false on an unknown mode name, which is what the Pd wrapper gates on, so Pd is unaffected -- verified by building it. CombMode, tryParseCombMode/parseCombMode, toString and combModeNames live in types.h next to the existing GenMode parser. That answers the review point on the previous commit: the Max wrapper had its own copy of the mode-name table so that a typo could be caught at object-creation time rather than at DSP start. Both the table and the hardcoded "add, root or max" error text are gone, and adding a mode is now a one-place edit in types.h. The two parse entry points are deliberate -- the throwing one for construction, the noexcept one for the message handler, which runs on a host's C stack where an escaping exception would terminate the process. On the Max side: a "mode" message plus a matching "mode" attribute, both routed through one helper so they cannot diverge, so `mode root`, `@mode root` and the inspector all work. No deferral -- the core publishes through a lock-free atomic, so switching mid-stream is safe and deferring would only add latency. EnvelopeCombinator::perform now takes and returns by reference. Routing Max through the core had reintroduced a per-block copy in each direction; the Max wrapper reads the core's output buffer directly and no longer keeps an output buffer of its own. The remaining input copy is elementwise and allocation-free. Help patch: mode message boxes wired into the left inlet of the `2 max` example. Refs Zerr* board ID 136. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #19 — base is
combinator-core-static-mode, notmain. Review #19 first; this diff only shows the delta.Why the core had to change
Mode was bound once in
EnvelopeCombinator::initialize(), which resolved a string to a member-function pointer. Nothing could change it afterwards, so the "add a message for mode change" half of the task needed a core setter before the Max side could do anything.Core
ProcessFunction processFunc→ aCombModeenum in astd::atomic, dispatched by aswitchinperform().Deliberately not
std::atomic<ProcessFunction>: a pointer-to-member-function is 16 bytes on the Itanium ABI, so that atomic would not be lock-free andset_mode()could block the audio thread on an internal lock. Astatic_assertpins the requirement.perform()loads the mode once per block, so a block is never processed partly in one mode and partly in another.This also removes existing UB:
processFunchad no in-class initializer, soif (processFunc)inperform()was reading an indeterminate value, not testing for null. The mode now has a valid default, and apreparedflag records whetherinitialize()actually ran.Pd is unaffected — the constructor signature is unchanged and
initialize()still returnsfalseon an unknown mode, which is exactly whatpuredata/src/zerr_combinator.cpp:34gates on. Verified by building it.Addressing the review comment on #19
You flagged that the wrapper should be thinner — adding a mode in core shouldn't require editing the Max side. Fixed:
CombMode,tryParseCombMode/parseCombMode,toStringandcombModeNamesnow live intypes.hbeside the existingparseGenMode. The wrapper's private mode table and the hardcoded"add, root or max"error string are both gone. Adding a mode is now a one-place edit.Two parse entry points on purpose: the throwing one for construction, the
noexceptone for the message handler, which runs on a host's C stack where an escaping exception would terminate the process.Max
A
modemessage plus a matchingmodeattribute, both routed through one helper so they can't diverge —mode root,@mode rootand the inspector all work. Nodefer_low: the core publishes through a lock-free atomic, so switching mid-stream is safe and deferring would only add latency.Performance
performnow takes and returns by reference. #19 had reintroduced a per-blockBlockscopy in each direction; the wrapper reads the core's output buffer directly and no longer keeps one of its own. The remaining input copy is elementwise and allocation-free.Verification
Driven through the real wrapper API against this branch's core.
Runtime switching, two sets of constants
0.25/0.81:maxsetMode rootsetMode addsetMode maxsetMode bananaAlso passing: a mode set before DSP ever started is applied by the next
prepare(); a reshape (3→8 channels) preserves a mode set at runtime; all the #19 lifecycle cases still pass.Thread safety: 30,000
set_mode()calls from a control thread whileperform()ran continuously on another — clean under ThreadSanitizer, no race reports. Also clean under ASan + UBSan.All three targets (
core,maxmsp,puredata) build with zero errors.Not yet exercised in Max — the listening pass on the new message boxes is still to do.
Not in this PR
Pd parity for the
modemessage (now ~20 lines, since the core supports it), and three pre-existing Pd bugs found while readingpuredata/src/zerr_combinator~.cpp: the leakedstrdup'd mode at :36, afreebytessize mismatch at :66 against thegetbytesat :45, and silentreturn NULLon every bad-arg path. Those stack next.Refs Zerr* board ID 136.
🤖 Generated with Claude Code