VST3: only kResultFalse is end of stream, and the preset selector cannot be read before the host sends it - #544
Merged
defiantnerd merged 1 commit intoSep 13, 2026
Conversation
…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.
defiantnerd
force-pushed
the
vst3-state-restore-followups
branch
from
September 13, 2026 14:48
f375b1e to
b0df674
Compare
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.
Two corrections to #543, both found in review after it merged. They are independent; the first is the one that matters to other plugins.
1. The stream adapter reported every error as end of file
#543 changed
CLAPVST3StreamAdapter::readto return0instead of-1so that Cubase'skResultFalse— its way of saying "that read ran past the end of the chunk" — would not look like a broken stream. That part was needed: asking for more than is left is how a reader with no length reads, andclap_istreamgives it no way to ask, so the first read of a restored project returned an error and plugins threw the whole state away.But it applied to every non-
kResultOkresult, not just that one.kOutOfMemory,kInternalError,kInvalidArgumentall became a clean EOF. Two ways that hurts:MemoryStream::readreturnskOutOfMemorywhen its buffer could not be allocated. A plugin that treats "no bytes" as "no state, use defaults" then reports a successful load, and the next project save writes those defaults over the user's work.0forever. That loop is the pattern in this repository's own conformance fixtures (tests/clap-first-example/distortion_clap.cpp), so it is not a hypothetical shape.Now only
kResultFalseis treated as end of stream, perclap/stream.h: "0 indicates end of file and -1 a read error". Everything else is an error again.The
sizeargument is also clamped rather than cast.IBStreamcounts inint32, and a reader asking for more than that is not making a mistake.2. The preset selector cannot be read before the host sends it
#543 also had
setStateseed_presetIndexInEffectfrom the selector parameter, to stop a host that streams a program list value in every process block from reloading that preset over the state just restored.That cannot work, and it is worth writing down why, because it looks like it should. At
setStatethe parameter still holdscreatePresetSelector's default of0: a host replays a program list parameter after the component state, the selector's value is not in the state chunk, andsyncParameterValuesFromClapcannot fill it either — the selector's id is a tag the wrapper invented and no CLAP plugin owns, soget_valuefails for it.So the seed was always
0, whichsetState.Replaced with what does work: adopt the first selector value the host sends after a successful
setState, whatever it says, and load nothing. Armed only there — 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_presetIndexInEffectbeing-1, a condition both cases share.The cost is written at the guard rather than left for the next reader: on a host that sends this parameter only when it changes rather than in every process block, the user's first pick after a project load is spent on the adopt and the second one loads. That is the lesser of the two — the alternative reloads the saved preset over every restored project, on every host that streams, every time.
Testing
wrapasvst3.cppanddetail/vst3/state.hcompile clean under the wrapper's own-Werror;clap-first-distortion_vst3links and passes the VST3 validator 47/47, as does the plugin this was found with (a CLAP with a 363-entry program list).What has been confirmed in Cubase is the problem #543 set out to fix: with the adapter change, a saved project restores — before it,
setStatewas called and the first read returned-1, so nothing was ever restored under VST3 at all. That was verified with logging at the plugin'sclap_plugin_stateboundary.Both changes here are reasoned and static-checked but not yet re-confirmed in a DAW: the narrowing in 1 is a strict reduction of what is treated as EOF and leaves Cubase's path untouched, and 2 replaces a seed that provably could not fire correctly. Independent testing welcome, particularly of a program list on a host that sends the selector only when it changes.