From 11b02959a41c376c2830f8b934a513755b5eba55 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:37:54 +0200 Subject: [PATCH 1/2] AUv3: lay the plugin subview out at the size adjust_size agreed to --- src/detail/auv3/auv3_audiounit.mm | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/detail/auv3/auv3_audiounit.mm b/src/detail/auv3/auv3_audiounit.mm index cd7c6183..cc895095 100644 --- a/src/detail/auv3/auv3_audiounit.mm +++ b/src/detail/auv3/auv3_audiounit.mm @@ -2077,7 +2077,13 @@ - (BOOL)setGUISize:(uint32_t)width height:(uint32_t)height // misbehaviour. auto *gui = _impl->_plugin->_ext._gui; if (gui->adjust_size) gui->adjust_size(_impl->_plugin->_plugin, &width, &height); - return gui->set_size(_impl->_plugin->_plugin, width, height) ? YES : NO; + if (!gui->set_size(_impl->_plugin->_plugin, width, height)) return NO; + + // Keep the cached size in step: prepareGUIAndReturnWidth:height: hands it + // back, and that is how the view controller learns the adjusted size. + _impl->_guiWidth = width; + _impl->_guiHeight = height; + return YES; } - (void)setViewController:(ClapAUv3ViewController *)vc @@ -2565,10 +2571,20 @@ - (void)viewDidLayoutSubviews [self.audioUnit setGUISize:(uint32_t)bounds.size.width height:(uint32_t)bounds.size.height]; } - // Ensure the CLAP plugin's subview fills the container + // adjust_size may have snapped the container bounds (fixed aspect ratio, + // step sizes), and the plugin laid out at the snapped size — so the subview + // has to get that size too, not the raw bounds. + CGRect childFrame = bounds; + uint32_t agreedWidth = 0, agreedHeight = 0; + if ([self.audioUnit prepareGUIAndReturnWidth:&agreedWidth height:&agreedHeight] && + agreedWidth > 0 && agreedHeight > 0) + { + childFrame = CGRectMake(0, 0, agreedWidth, agreedHeight); + } + for (CLAPWRAP_ViewClass *subview in self.view.subviews) { - subview.frame = bounds; + subview.frame = childFrame; } } } From 0858e042d2a57a4be5bc8b5ea6781cc79a3024b3 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:40:32 +0200 Subject: [PATCH 2/2] AUv3: serialize audio unit creation against the host's factory call The loadView bootstrap and the host's XPC factory call each built one, and the loser was released with its GUI still created. --- src/detail/auv3/auv3_audiounit.h | 4 +++ src/detail/auv3/auv3_audiounit.mm | 26 +++++++++++++++++-- src/detail/auv3/build-helper/build-helper.cpp | 13 +++++----- .../generated_auv3_entrypoints.hxx.in | 13 +++++----- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/detail/auv3/auv3_audiounit.h b/src/detail/auv3/auv3_audiounit.h index 98212c4b..9a790d24 100644 --- a/src/detail/auv3/auv3_audiounit.h +++ b/src/detail/auv3/auv3_audiounit.h @@ -33,6 +33,10 @@ // Subclasses (generated by build-helper) override this to provide plugin-specific info - (AUAudioUnit *)createAudioUnitWithComponentDescription:(AudioComponentDescription)desc error:(NSError **)error; +// The generated override must build its audio unit through this: the host's +// factory call and -loadView's self-bootstrap race, and only one audio unit may +// ever exist per view controller. `make` runs only if there is none yet. +- (AUAudioUnit *)createAudioUnitOnce:(ClapAUv3AudioUnit * (^)(NSError **))make error:(NSError **)error; @end @interface ClapAUv3AudioUnit : AUAudioUnit diff --git a/src/detail/auv3/auv3_audiounit.mm b/src/detail/auv3/auv3_audiounit.mm index cc895095..715aee23 100644 --- a/src/detail/auv3/auv3_audiounit.mm +++ b/src/detail/auv3/auv3_audiounit.mm @@ -2283,6 +2283,8 @@ @implementation ClapAUv3ViewController CGSize _pluginSize; // We already tried to create the audio unit ourselves (see _bootstrapAudioUnit). BOOL _bootstrapAttempted; + // Serializes the two creation paths (see -createAudioUnitOnce:error:). + std::mutex _audioUnitMutex; } - (void)loadView @@ -2315,6 +2317,10 @@ - (void)loadView - (void)setAudioUnit:(ClapAUv3AudioUnit *)audioUnit { + if (_audioUnit == audioUnit) return; + // Never drop a live audio unit by bare overwrite: releasing the last reference + // reaches plugin->terminate() with the GUI still created, which CLAP forbids. + if (_audioUnit) [self _destroyGUI]; _audioUnit = audioUnit; // Establish the back-reference so the AU can return us from // requestViewControllerWithCompletionHandler: @@ -2368,6 +2374,22 @@ - (BOOL)_ownComponentDescription:(AudioComponentDescription *)outDesc return NO; } +// Both creation paths meet here: the host's factory call, which arrives on an +// XPC worker thread, and -loadView's self-bootstrap on the appex main thread. +// Unserialized they both see a nil audioUnit and build an audio unit, and the +// loser is then thrown away with its GUI still alive. Nothing under this lock +// ever waits on the main queue, so the losing path only stalls. +- (AUAudioUnit *)createAudioUnitOnce:(ClapAUv3AudioUnit * (^)(NSError **))make error:(NSError **)error +{ + std::lock_guard lock(_audioUnitMutex); + if (_audioUnit) return _audioUnit; + + ClapAUv3AudioUnit *au = make(error); + if (!au) return nil; + self.audioUnit = au; + return au; +} + // Out-of-process AUv3: the viewbridge loads our view from inside // -beginRequestWithExtensionContext:, before the host has asked the factory for // an audio unit, and it captures preferredContentSize as the view is vended — @@ -2576,8 +2598,8 @@ - (void)viewDidLayoutSubviews // has to get that size too, not the raw bounds. CGRect childFrame = bounds; uint32_t agreedWidth = 0, agreedHeight = 0; - if ([self.audioUnit prepareGUIAndReturnWidth:&agreedWidth height:&agreedHeight] && - agreedWidth > 0 && agreedHeight > 0) + if ([self.audioUnit prepareGUIAndReturnWidth:&agreedWidth height:&agreedHeight] && agreedWidth > 0 && + agreedHeight > 0) { childFrame = CGRectMake(0, 0, agreedWidth, agreedHeight); } diff --git a/src/detail/auv3/build-helper/build-helper.cpp b/src/detail/auv3/build-helper/build-helper.cpp index 8b878a6f..7997cfd9 100644 --- a/src/detail/auv3/build-helper/build-helper.cpp +++ b/src/detail/auv3/build-helper/build-helper.cpp @@ -481,19 +481,18 @@ int main(int argc, char **argv) cppf << "- (AUAudioUnit *)createAudioUnitWithComponentDescription:(AudioComponentDescription)desc\n" << " error:(NSError **)error {\n" - << " // The view controller may have created it already, to answer a size\n" - << " // query before the host got here (see -_bootstrapAudioUnit).\n" - << " if (self.audioUnit) return self.audioUnit;\n" - << " ClapAUv3AudioUnit *au = [[ClapAUv3AudioUnit alloc] initWithComponentDescription:desc\n" + << " // -loadView may be bootstrapping one right now on the main thread, so the\n" + << " // check and the build have to happen under the base class's lock.\n" + << " return [self createAudioUnitOnce:^ClapAUv3AudioUnit *(NSError **err) {\n" + << " return [[ClapAUv3AudioUnit alloc] initWithComponentDescription:desc\n" << " options:0\n" - << " error:error\n" + << " error:err\n" << " clapName:@\"" << escapeObjCString(u.clapname) << "\"\n" << " clapId:@\"" << escapeObjCString(u.clapid) << "\"\n" << " clapIndex:" << idx << "];\n" - << " self.audioUnit = au;\n" - << " return au;\n" + << " } error:error];\n" << "}\n" << "@end\n\n"; diff --git a/src/detail/auv3/templates/generated_auv3_entrypoints.hxx.in b/src/detail/auv3/templates/generated_auv3_entrypoints.hxx.in index 5592236d..2c730533 100644 --- a/src/detail/auv3/templates/generated_auv3_entrypoints.hxx.in +++ b/src/detail/auv3/templates/generated_auv3_entrypoints.hxx.in @@ -13,16 +13,15 @@ @implementation @AUV3_FACTORY_CLASS_NAME@ - (AUAudioUnit *)createAudioUnitWithComponentDescription:(AudioComponentDescription)desc error:(NSError **)error { - // The view controller may have created it already, to answer a size - // query before the host got here (see -_bootstrapAudioUnit). - if (self.audioUnit) return self.audioUnit; - ClapAUv3AudioUnit *au = [[ClapAUv3AudioUnit alloc] initWithComponentDescription:desc + // -loadView may be bootstrapping one right now on the main thread, so the + // check and the build have to happen under the base class's lock. + return [self createAudioUnitOnce:^ClapAUv3AudioUnit *(NSError **err) { + return [[ClapAUv3AudioUnit alloc] initWithComponentDescription:desc options:0 - error:error + error:err clapName:@"@AUV3_CLAP_NAME@" clapId:@"@AUV3_CLAP_ID@" clapIndex:0]; - self.audioUnit = au; - return au; + } error:error]; } @end