Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ed8d1de
test: add deterministic VST3 process fixture
masarray Aug 27, 2026
158d379
test: characterize current VST3 engine process seam
masarray Aug 27, 2026
5e4da18
test: add standalone R0-1 characterization build
masarray Aug 27, 2026
0194ed9
ci: prove R0-1 real-VST3 process seam
masarray Aug 27, 2026
2b52042
docs: explain R0-1 characterization gate
masarray Aug 27, 2026
19dea6b
test: mark R0-1 harness scope
masarray Aug 27, 2026
3fb5049
test: document non-shipping fixture
masarray Aug 27, 2026
1fc9d94
docs: pin R0-1 pre-mutation scope
masarray Aug 27, 2026
f48501f
test: define fixture project version
masarray Aug 27, 2026
81b7728
test: include full VST3 process context
masarray Aug 27, 2026
6c1fc9e
test: align R0-1 harness MSVC runtime
masarray Aug 27, 2026
5ea7c01
test: remove redundant R0-1 placeholder
masarray Aug 27, 2026
b513f9a
test: remove redundant R0-1 notice
masarray Aug 27, 2026
7b3fe31
test: remove redundant R0-1 scope note
masarray Aug 27, 2026
819e394
test: disable Steinberg post-build tools for R0-1 fixture
masarray Aug 28, 2026
ba73297
feat: add protocol-neutral process block view
masarray Aug 28, 2026
c2501f7
test: specify protocol-neutral process view behavior
masarray Aug 28, 2026
da3de86
feat: add protocol-neutral VST3 process entry
masarray Aug 28, 2026
d71575f
feat: migrate VST3 processing behind ProcessBlockView
masarray Aug 28, 2026
29af83c
test: follow neutral process block sample advance
masarray Aug 28, 2026
f60e937
ci: qualify ProcessBlockView seam explicitly
masarray Aug 28, 2026
0d3f203
docs: record two-stage R0-1 characterization
masarray Aug 28, 2026
e09a6b6
ci: include R0-1 host seam in compatibility gate
masarray Aug 28, 2026
dc3a52b
fix: preserve OBS source registration ABI floor
masarray Aug 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/compat-test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ on:
- 'src/obs-plugin/obs_compat_floor.hpp'
- 'src/obs-plugin/plugin.cpp'
- 'src/host/native_editor.cpp'
- 'src/host/process_block_view.hpp'
- 'src/host/vst3_engine.cpp'
- 'src/host/vst3_engine.hpp'
- 'src/scanner/main.cpp'
- 'installer/windows/obs-safe-vst3.iss'
- 'data/locale/en-US.ini'
Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/r0-1-process-seam.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: R0-1 Process Seam Characterization

on:
pull_request:
paths:
- '.github/workflows/r0-1-process-seam.yml'
- 'tests/r0_1/**'
- 'src/host/process_block_view.hpp'
- 'src/host/vst3_engine.cpp'
- 'src/host/vst3_engine.hpp'
- 'src/common/**'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: r0-1-process-seam-${{ github.ref }}
cancel-in-progress: true

jobs:
real-vst3-engine-process:
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Configure deterministic real-VST3 characterization
run: cmake -S tests/r0_1 -B build/r0-1 -A x64
- name: Build fixture and engine characterization
run: cmake --build build/r0-1 --config Release --target r0-1-vst3-engine-process-test --parallel
- name: Run AudioSlot and ProcessBlockView characterization
run: ctest --test-dir build/r0-1 -C Release --output-on-failure
18 changes: 18 additions & 0 deletions src/host/process_block_view.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#pragma once

#include <cstdint>

namespace safevst3 {

// Non-owning protocol-neutral audio view. Buffer ownership and lifetime remain
// with the caller for the duration of process(); no transport layout is
// embedded here.
struct ProcessBlockView {
float* const* input = nullptr;
float* const* output = nullptr;
std::uint32_t channels = 0;
std::uint32_t frames = 0;
std::uint64_t sequence = 0;
};

} // namespace safevst3
42 changes: 31 additions & 11 deletions src/host/vst3_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1130,25 +1130,45 @@ bool Vst3Engine::flush_parameter_changes() noexcept

bool Vst3Engine::process(AudioSlot& slot) noexcept
{
if (!processor_ || slot.frames == 0 || slot.frames > kMaxFrames ||
slot.channels != channels_ ||
float* input[kMaxChannels] = {slot.input[0], slot.input[1]};
float* output[kMaxChannels] = {slot.output[0], slot.output[1]};
const ProcessBlockView block{
input,
output,
slot.channels,
slot.frames,
slot.sequence,
};
return process(block);
}

bool Vst3Engine::process(const ProcessBlockView& block) noexcept
{
if (!processor_ || !block.input || !block.output ||
block.frames == 0 || block.frames > kMaxFrames ||
block.channels != channels_ ||
(plugin_input_channels_ != 1 && plugin_input_channels_ != 2) ||
(plugin_output_channels_ != 1 && plugin_output_channels_ != 2))
return false;

for (std::uint32_t ch = 0; ch < block.channels; ++ch) {
if (!block.input[ch] || !block.output[ch])
return false;
}

Sample32* in[kMaxChannels]{};
Sample32* out[kMaxChannels]{};

if (plugin_input_channels_ == channels_) {
for (std::uint32_t ch = 0; ch < channels_; ++ch)
in[ch] = slot.input[ch];
in[ch] = block.input[ch];
} else if (channels_ == 2 && plugin_input_channels_ == 1) {
average_stereo_to_mono(
slot.input[0], slot.input[1], input_adapter_[0].data(), slot.frames);
block.input[0], block.input[1], input_adapter_[0].data(), block.frames);
in[0] = input_adapter_[0].data();
} else if (channels_ == 1 && plugin_input_channels_ == 2) {
duplicate_mono_to_stereo(
slot.input[0], input_adapter_[0].data(), input_adapter_[1].data(), slot.frames);
block.input[0], input_adapter_[0].data(), input_adapter_[1].data(), block.frames);
in[0] = input_adapter_[0].data();
in[1] = input_adapter_[1].data();
} else {
Expand All @@ -1158,7 +1178,7 @@ bool Vst3Engine::process(AudioSlot& slot) noexcept
const bool output_direct = plugin_output_channels_ == channels_;
if (output_direct) {
for (std::uint32_t ch = 0; ch < channels_; ++ch)
out[ch] = slot.output[ch];
out[ch] = block.output[ch];
} else {
for (std::uint32_t ch = 0; ch < plugin_output_channels_; ++ch)
out[ch] = output_adapter_[ch].data();
Expand All @@ -1170,7 +1190,7 @@ bool Vst3Engine::process(AudioSlot& slot) noexcept
kOutput, main_output_bus_, out, static_cast<int32>(plugin_output_channels_)))
return false;

process_data_.numSamples = static_cast<int32>(slot.frames);
process_data_.numSamples = static_cast<int32>(block.frames);
process_data_.inputEvents = nullptr;
process_data_.outputEvents = nullptr;
process_data_.outputParameterChanges = &output_parameter_changes_;
Expand All @@ -1188,16 +1208,16 @@ bool Vst3Engine::process(AudioSlot& slot) noexcept
capture_output_parameter_changes();
if (result != kResultOk)
return false;
sample_position_ += slot.frames;
sample_position_ += block.frames;

if (!output_direct) {
if (channels_ == 2 && plugin_output_channels_ == 1) {
duplicate_mono_to_stereo(
output_adapter_[0].data(), slot.output[0], slot.output[1], slot.frames);
output_adapter_[0].data(), block.output[0], block.output[1], block.frames);
} else if (channels_ == 1 && plugin_output_channels_ == 2) {
average_stereo_to_mono(
output_adapter_[0].data(), output_adapter_[1].data(),
slot.output[0], slot.frames);
block.output[0], block.frames);
} else {
return false;
}
Expand All @@ -1208,4 +1228,4 @@ bool Vst3Engine::process(AudioSlot& slot) noexcept

} // namespace safevst3

#endif
#endif
2 changes: 2 additions & 0 deletions src/host/vst3_engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "common/process_context_policy.hpp"
#include "common/startup_error.hpp"
#include "common/state_snapshot.hpp"
#include "host/process_block_view.hpp"
#include "host/vst3_processing_compat.hpp"

#include "pluginterfaces/vst/ivstaudioprocessor.h"
Expand Down Expand Up @@ -73,6 +74,7 @@ class Vst3Engine final : public LatencyRestartTarget, public IoRestartLifecycleT
return opened;
}
void close() noexcept;
bool process(const ProcessBlockView& block) noexcept;
bool process(AudioSlot& slot) noexcept;

bool capture_state(PluginStateSnapshot& snapshot, std::string& error);
Expand Down
22 changes: 21 additions & 1 deletion src/obs-plugin/obs_compat_floor.hpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
#pragma once
#include <cstddef>
#include <obs-module.h>

// Minimum libobs API used by this plugin: OBS Studio 29.1.
// clean-test3 rebuild marker: also validates stale-copy installer cleanup.
#define SAFEVST3_OBS_MIN_API_VER MAKE_SEMANTIC_VERSION(29, 1, 0)
#undef LIBOBS_API_VER
#define LIBOBS_API_VER SAFEVST3_OBS_MIN_API_VER

// obs_register_source() always forwards sizeof(struct obs_source_info) from
// the SDK used to build the module. Newer OBS SDKs append fields to that
// structure, so forwarding the full current size makes otherwise-compatible
// modules fail registration on older libobs runtimes.
//
// Safe VST3 only populates source-info fields through `save`; all fields after
// it are intentionally unused. Register exactly that ABI prefix so old libobs
// zero-fills its own trailing fields while current libobs receives every
// callback this module actually uses. Keep this boundary in sync if a future
// source implementation starts using a field after `save`.
#define SAFEVST3_OBS_SOURCE_INFO_COMPAT_SIZE \
(offsetof(struct obs_source_info, save) + sizeof(((struct obs_source_info*)0)->save))

static_assert(SAFEVST3_OBS_SOURCE_INFO_COMPAT_SIZE <= sizeof(struct obs_source_info),
"OBS source-info compatibility prefix exceeds the build SDK structure");

#undef obs_register_source
#define obs_register_source(info) \
obs_register_source_s((info), SAFEVST3_OBS_SOURCE_INFO_COMPAT_SIZE)
2 changes: 1 addition & 1 deletion tests/process_context_source_contract.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ endif()

string(FIND "${SOURCE}" "const auto context_frame = make_process_context_frame(" BLOCK_FRAME_POS)
string(FIND "${SOURCE}" "processor_->process(process_data_)" PROCESS_POS)
string(FIND "${SOURCE}" "sample_position_ += slot.frames" ADVANCE_POS)
string(FIND "${SOURCE}" "sample_position_ += block.frames" ADVANCE_POS)
if(BLOCK_FRAME_POS LESS 0 OR PROCESS_POS LESS 0 OR ADVANCE_POS LESS 0)
message(FATAL_ERROR "Could not find deterministic audio-block process-context markers")
endif()
Expand Down
82 changes: 82 additions & 0 deletions tests/r0_1/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
cmake_minimum_required(VERSION 3.25)
project(safevst3-r0-1-characterization VERSION 1.0.0 LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

if(NOT WIN32)
message(FATAL_ERROR "R0-1 real-VST3 characterization is Windows-only")
endif()

# The repository deliberately builds its Steinberg hosting/runtime targets with
# the static MSVC runtime. Keep the standalone fixture/test targets on the same
# runtime so the characterization is about VST3 behavior, not CRT mixing.
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
set(SAFEVST3_BUILD_OBS_PLUGIN OFF CACHE BOOL "" FORCE)
set(SAFEVST3_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(SAFEVST3_STATIC_MSVC_RUNTIME ON CACHE BOOL "" FORCE)

# These are Steinberg-supported build options. The R0-1 fixture exists only to
# exercise the production engine seam inside CI, so do not run the SDK's own
# post-build validator/module-info generation or create a per-user VST3 link.
# Production builds keep the repository defaults; these settings are local to
# this standalone test project before it includes the repository root.
set(SMTG_RUN_VST_VALIDATOR OFF CACHE BOOL "" FORCE)
set(SMTG_CREATE_MODULE_INFO OFF CACHE BOOL "" FORCE)
set(SMTG_CREATE_PLUGIN_LINK OFF CACHE BOOL "" FORCE)

get_filename_component(SAFEVST3_ROOT "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE)
add_subdirectory("${SAFEVST3_ROOT}" safevst3-root)

set(_fixture_source "${CMAKE_CURRENT_LIST_DIR}/vst3_process_fixture.cpp")

smtg_add_vst3plugin(r0-1-fixture-mono "${_fixture_source}")
target_link_libraries(r0-1-fixture-mono PRIVATE sdk)
target_compile_definitions(r0-1-fixture-mono PRIVATE
SAFEVST3_FIXTURE_CHANNELS=1
WIN32_LEAN_AND_MEAN
NOMINMAX
)

smtg_add_vst3plugin(r0-1-fixture-stereo "${_fixture_source}")
target_link_libraries(r0-1-fixture-stereo PRIVATE sdk)
target_compile_definitions(r0-1-fixture-stereo PRIVATE
SAFEVST3_FIXTURE_CHANNELS=2
WIN32_LEAN_AND_MEAN
NOMINMAX
)

add_executable(r0-1-vst3-engine-process-test
"${CMAKE_CURRENT_LIST_DIR}/vst3_engine_process_characterization.cpp"
"${SAFEVST3_ROOT}/src/host/vst3_engine.cpp"
)
target_include_directories(r0-1-vst3-engine-process-test PRIVATE
"${SAFEVST3_ROOT}/src"
)
target_link_libraries(r0-1-vst3-engine-process-test PRIVATE
safevst3_vst3_hosting_runtime
safevst3_lifecycle_policy
safevst3_state_restore_policy
safevst3_latency_restart_transaction
safevst3_parameter_refresh_transaction
safevst3_io_restart_transaction
safevst3_reload_component_transaction
safevst3_process_context_policy
)
target_compile_definitions(r0-1-vst3-engine-process-test PRIVATE
WIN32_LEAN_AND_MEAN
NOMINMAX
)
add_dependencies(r0-1-vst3-engine-process-test
r0-1-fixture-mono
r0-1-fixture-stereo
)

enable_testing()
add_test(
NAME r0-1-vst3-engine-process-characterization
COMMAND $<TARGET_FILE:r0-1-vst3-engine-process-test>
$<TARGET_FILE:r0-1-fixture-mono>
$<TARGET_FILE:r0-1-fixture-stereo>
)
16 changes: 16 additions & 0 deletions tests/r0_1/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# R0-1 deterministic process characterization

This directory is the deterministic real-VST3 gate for Rack R0-1.

It builds two minimal VST3 modules with the pinned Steinberg SDK already used by the repository:

- a fixed-mono effect;
- a fixed-stereo effect.

The gate was deliberately established in two stages. First, before any production process mutation, `vst3_engine_process_characterization.cpp` opened the fixture modules through the production `Vst3Engine` and exercised the unchanged Single `process(AudioSlot&)` seam. That pre-mutation proof passed on source head `819e394949184ed64d86901b0cf3e13a3306c6ea` in R0-1 Process Seam Characterization run `33135381993`.

After that gate passed, R0-1 introduced `ProcessBlockView`. The same harness now keeps all of the original AudioSlot characterization and also drives the protocol-neutral process entry with caller-owned raw buffers, requiring its deterministic output to match an independently opened Single AudioSlot engine. It also verifies invalid neutral buffer tables fail cleanly.

The fixture encodes `projectTimeSamples` into deterministic audio, so the test locks current block-position behavior in addition to direct mono/stereo processing, mono/stereo adaptation, validation failures, and VST3 process-error propagation.

This harness is non-shipping. It does not define or change the Single IPC protocol and it contains no Rack production runtime.
Loading
Loading