Skip to content

Vst3 preset selector vs state - #543

Merged
defiantnerd merged 2 commits into
free-audio:nextfrom
defiantnerd:vst3-preset-selector-vs-state
Sep 13, 2026
Merged

defiantnerd merged 2 commits into
free-audio:nextfrom
defiantnerd:vst3-preset-selector-vs-state

Conversation

@defiantnerd

Copy link
Copy Markdown
Collaborator

VST3 Wrapper: That was a nasty one that breaks restore if the stream is too long....

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
defiantnerd merged commit fb9ea84 into free-audio:next Sep 13, 2026
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.
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