Skip to content

Implement the VST3 factory so the plugin actually loads - #7

Merged
godofecht merged 2 commits into
mainfrom
implement-vst3-factory
Jul 22, 2026
Merged

Implement the VST3 factory so the plugin actually loads#7
godofecht merged 2 commits into
mainfrom
implement-vst3-factory

Conversation

@godofecht

Copy link
Copy Markdown
Owner

Includes the docs/examples work it is based on, plus the factory implementation.

The bug

danzig's example plugin could not be loaded by any host.

fn factory_countClasses(_: *anyopaque) callconv(.c) i32 { return 1; }
fn factory_getClassInfo(_: *anyopaque, index: i32, _: *anyopaque) callconv(.c) i32 {
    _ = index;
    return 0;          // 0 is kResultOk: claims success, writes nothing
}

A host is told one class exists, asks for it, and gets an uninitialized struct back with a success code.

Root cause was two things

The stub factory was the visible bug. The bundle also exported no bundleEntry. macOS hosts load a .vst3 via CFBundle and call bundleEntry first; without it the module is discarded before GetPluginFactory is ever looked up. Fixing only the factory would still have produced "Num plugins found: 0".

Before / after

before:  Num plugins found: 0
         !!! Test 1 failed: No types found.
         FAILED!!  1 test failed, out of a total of 1

after:   Num plugins found: 1
         Testing plugin: VST3-Danzig Gain-43e2804f-c026063c
         Superelectric: Danzig Gain v0.1.0
         SUCCESS

Passes at --strictness-level 5 and 10, in Debug and ReleaseFast. No strictness was lowered.

Also corrected in src/vst3.zig

Several constants were wrong: kResultFalse was 0 (it is 1), kBypass was 1 << 5 (kIsBypass is 1 << 16), AudioBusBuffers.channelBuffers32 had = null on a non-optional pointer, and IComponent.getBusCount was missing its MediaType argument. Comptime assertions now pin the struct sizes a host reads.

The test that was lying

The harness asserted getClassInfo(0) == 0, which passes against the stub because the stub returns 0. Every buffer is now zeroed before the call and the content asserted. Reverting the factory to its stub bodies fails 9 checks:

  FAIL  getClassInfo(0) writes the "Audio Module Class" category
  FAIL  getClassInfo(0) writes a non-empty class name
  FAIL  createInstance writes a non-null object
  ...

getClassInfo(0) returns kResultOk still passes against the stub, which is exactly the point: the result code alone proves nothing.

The harness went from 12 to 49 checks and now drives the object as a host does, through initialize, bus info, parameter info, setupProcessing, and real audio: DC at 0 dB comes out 1.0, at +6 dB 1.995, bypassed exactly 1.0.

Not implemented

IConnectionPoint, IUnitInfo, IMidiMapping, IPluginFactory3, 64-bit processing, a custom editor, programs/presets. All refuse cleanly with kNotImplemented rather than a false kResultOk. The plugin does not call restartComponent(kParamValuesChanged) after a state load; pluginval passes without it, but a host caching parameter values aggressively could show stale numbers after loading a session.

Verification

35/35 unit tests and 49/49 integration checks on both 0.14.1 and 0.15.2 from a clean cache. lipo -info reports x86_64 arm64.

🤖 Generated with Claude Code

godofecht and others added 2 commits July 21, 2026 12:16
docs/WIKI.md is a single self-contained page: what danzig is, how the COM
vtable machinery in src/vst3.zig maps to Zig extern structs, plugin
registration, the audio callback path, the parameter system and its
one-cache-line layout, the audio helpers, how the universal VST3 bundle is
built, testing, and troubleshooting. Every command and every block of output
in it was run.

examples/danzig-minimal is new: one parameter, one line of DSP, and the single
symbol a VST3 host looks for. The same source builds as the shared library a
host would load and as an executable, so the DSP can be checked without a DAW.
It is the file to copy when starting a plugin.

examples/danzig-test was a print statement claiming to be an integration test.
It now links the built DanzigGain plugin, calls its exported GetPluginFactory,
and drives the returned object through the raw VST3 C ABI using its own copy
of the vtable layout, so a change to the object layout fails here rather than
in a DAW. It exits non-zero on failure.

Each example directory gets a README with what it demonstrates and the exact
commands, plus an examples/README.md index that also flags the two unbuilt
leftover files.

setup.sh checks the Zig version, warns outside 0.14.1 and 0.15.2, builds,
tests, packages the bundle, and prints the install path. Idempotent, non-zero
on failure.

The README's Build Artifacts section listed sizes and a libdanzig.a that do
not exist. Corrected against a real ReleaseFast build. Both the README and the
wiki now state plainly that the gain plugin's factory is a stub and that a
host currently finds zero classes in the bundle.

Verified on 0.14.1 and 0.15.2: 29/29 build steps, 35/35 tests, and
lipo reporting x86_64 arm64 for the bundle binary.
The example plugin could not be loaded by any host. The factory claimed one
class, wrote nothing into the PClassInfo it was handed, and returned kResultOk
from createInstance without producing an object. The bundle also exported no
bundleEntry, so a macOS host discarded the module before it ever reached the
factory. pluginval reported "Num plugins found: 0".

src/vst3.zig now carries the real C ABI: interface ids, PClassInfo and
PClassInfo2, bus and process structs, and a vtable type per interface, with
comptime size assertions on the layouts a host reads. The old result codes were
wrong (kResultFalse was 0, kBypass was 1 << 5) and are corrected.

examples/danzig-gain is a working single-component effect. One object exposes
IComponent, IAudioProcessor and IEditController, recovering itself from each
interface with @fieldParentPtr, sharing one atomic refcount. Gain comes from
the lock-free ParamStore and is applied per sample; bypass passes the input
through. State is a versioned blob of normalized values. Anything not
implemented returns kNotImplemented rather than a false success.

The integration harness now asserts on content instead of result codes: buffers
are zeroed before every call, the class info must name the Audio Module Class
category, and createInstance must yield an object that queries, counts
references, and renders audio at the gain it was told to use. Nine of those
checks fail against the old stub.

pluginval passes at strictness 10 on both Zig 0.14.1 and 0.15.2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@godofecht
godofecht merged commit 3d42595 into main Jul 22, 2026
2 checks passed
@godofecht
godofecht deleted the implement-vst3-factory branch July 24, 2026 23:25
godofecht added a commit that referenced this pull request Jul 29, 2026
Implement the VST3 factory so the plugin actually 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