Vst3 preset selector vs state - #543
Merged
defiantnerd merged 2 commits intoSep 13, 2026
Merged
Conversation
Loading a project in Cubase gave back the preset the rig had come from,
not the rig the project was saved with. The state was restored correctly
and then overwritten, one process block later, by the preset selector.
Cubase sends a program list parameter's value in every process block, and
those blocks run while the project is still being restored. So the audio
thread can queue a preset-load request from a block that ran BEFORE
setState, and onIdle() - which acts on the request, on the main thread,
without consulting the state that has landed in the meantime - then loads
the untouched preset over everything the state just restored.
_presetIndexInEffect already turns a value the host merely repeats into a
no-op, but only once something has established what is in effect: a load
through onIdle, or preset_loaded() resolving what the plugin reports. A
state restore does neither if the rig it carries never came from the
preset list, and it is not consulted at all at the point the queued
request is finally acted on.
Three places, one rule - the state is the newer fact about what the
plugin holds, and the selector is a label on where that content started:
- setState() drops a request queued before it ran,
- setState() takes the selector where it stands as the value in effect
when nothing else has said otherwise,
- onIdle() re-tests the request against it before loading, because the
two are on different threads and the state can arrive between them.
Nothing changes for an actual program change: the value then names
something other than what is in effect, which is what the guard tests.
Nothing a plug-in saved under VST3 was ever restored in Cubase. setState was called, the wrapper handed the plug-in a clap_istream, and the first read of it returned -1 - so every plug-in that reads its state the way clap_istream is meant to be read threw the whole project away before looking at a byte of it. The adapter reported anything but kResultOk as an error. Cubase answers a read that runs past the end of the plug-in's chunk with kResultFalse rather than with kResultOk and a short count, and asking for more than is left is how a reader with no length reads a stream - which is every reader clap_istream has, since the extension gives it no way to ask. CLAP has one code for "there is nothing more", and it is 0. A real I/O failure now reports a short read instead, so the reader gets a truncated chunk and rejects it - where it was headed anyway - while a host that phrases the end of a stream differently no longer costs a whole project.
defiantnerd
added a commit
to defiantnerd/clap-wrapper
that referenced
this pull request
Sep 13, 2026
…be read before the host sends it Two corrections to free-audio#543, both found in review. The stream adapter reported EVERY non-kResultOk result as end of file, not just the kResultFalse a host uses to say "that ran past the end of the chunk". kOutOfMemory - which the SDK's own MemoryStream::read returns when its buffer could not be allocated - then reads as a clean EOF: a plug-in that treats "no bytes" as "no state" reports a successful load, and the next save writes its defaults over the user's project. Worse, a reader that loops until it has the bytes it asked for never terminates, because the call that used to break the loop now returns 0 forever - and that loop is the pattern in this repository's own conformance fixtures. A real error is a real error again. The size argument is also clamped rather than cast: IBStream counts in int32, and a reader with no length asking for more than that is not making a mistake - the extension gives it no way to ask how much there is. setState seeded _presetIndexInEffect from the selector parameter, which cannot work: at that point the parameter still holds createPresetSelector's default of 0. A host replays a program list parameter AFTER the component state, the selector's value is not in the state chunk, and syncParameterValuesFromClap cannot fill it either - the selector's id is a tag this wrapper invented and no CLAP plug-in owns, so get_value fails for it. The seed was therefore always 0, which missed every preset except index 0 and made index 0 itself unreachable for the life of the instance: the first entry in every program list, dead after any setState. What works instead is to adopt the first value the host sends after a state load, whatever it says, and load nothing. It is armed only by a setState that succeeded - a fresh instance has no state to protect, and a load that failed leaves the host's selector the better authority - which is also why it cannot be inferred from _presetIndexInEffect being -1, a condition both cases share. The cost is stated at the guard: on a host that sends this parameter only when it changes, the user's first pick after a project load is spent on the adopt and the second one loads.
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.
VST3 Wrapper: That was a nasty one that breaks restore if the stream is too long....