Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .github/workflows/compat-test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ on:
- 'src/obs-plugin/obs_compat_floor.hpp'
- 'src/obs-plugin/plugin.cpp'
- 'src/host/native_editor.cpp'
- 'src/host/hosted_plugin.cpp'
- 'src/host/hosted_plugin.hpp'
- 'src/host/process_block_view.hpp'
- 'src/host/vst3_engine.cpp'
- 'src/host/vst3_engine.hpp'
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/r0-1-process-seam.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:
paths:
- '.github/workflows/r0-1-process-seam.yml'
- 'tests/r0_1/**'
- 'src/host/hosted_plugin.cpp'
- 'src/host/hosted_plugin.hpp'
- 'src/host/process_block_view.hpp'
- 'src/host/vst3_engine.cpp'
- 'src/host/vst3_engine.hpp'
Expand Down
36 changes: 36 additions & 0 deletions .github/workflows/r0-2-hosted-plugin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: R0-2 HostedPlugin Characterization

on:
pull_request:
paths:
- '.github/workflows/r0-2-hosted-plugin.yml'
- 'tests/r0_2/**'
- 'src/host/hosted_plugin.cpp'
- 'src/host/hosted_plugin.hpp'
- 'src/host/process_block_view.hpp'
- 'src/host/vst3_engine.cpp'
- 'src/host/vst3_engine.hpp'
- 'src/common/**'
- 'CMakeLists.txt'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: r0-2-hosted-plugin-${{ github.ref }}
cancel-in-progress: true

jobs:
real-vst3-hosted-plugin:
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Configure deterministic HostedPlugin characterization
run: cmake -S tests/r0_2 -B build/r0-2 -A x64
- name: Build fixture and HostedPlugin characterization
run: cmake --build build/r0-2 --config Release --target r0-2-hosted-plugin-characterization --parallel
- name: Run HostedPlugin characterization
run: ctest --test-dir build/r0-2 -C Release --output-on-failure
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ if(WIN32)
add_executable(obs-safe-vst3-host
src/host/main.cpp
src/host/native_editor.cpp
src/host/hosted_plugin.cpp
src/host/vst3_engine.cpp
)
target_include_directories(obs-safe-vst3-host PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src")
Expand Down
156 changes: 156 additions & 0 deletions src/host/hosted_plugin.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#ifdef _WIN32

#include "host/hosted_plugin.hpp"

#include "host/vst3_engine.hpp"

#include <utility>

namespace safevst3 {

HostedPlugin::HostedPlugin()
: engine_(std::make_unique<Vst3Engine>())
{
}

HostedPlugin::~HostedPlugin() = default;

bool HostedPlugin::open(const std::string& path,
const std::string& class_id,
std::uint32_t sample_rate,
std::uint32_t channels,
Steinberg::Vst::IComponentHandler* component_handler,
StartupPhaseSink* startup_phase_sink,
std::string& error)
{
return engine_->open(path, class_id, sample_rate, channels,
component_handler, startup_phase_sink, error);
}

bool HostedPlugin::open(const std::string& path,
const std::string& class_id,
std::uint32_t sample_rate,
std::uint32_t channels,
Steinberg::Vst::IComponentHandler* component_handler,
std::string& error)
{
return engine_->open(path, class_id, sample_rate, channels,
component_handler, error);
}

void HostedPlugin::close() noexcept
{
engine_->close();
}

bool HostedPlugin::process(const ProcessBlockView& block) noexcept
{
return engine_->process(block);
}

bool HostedPlugin::capture_state(PluginStateSnapshot& snapshot, std::string& error)
{
return engine_->capture_state(snapshot, error);
}

bool HostedPlugin::restore_state(const PluginStateSnapshot& snapshot, std::string& error)
{
return engine_->restore_state(snapshot, error);
}

bool HostedPlugin::refresh_latency_after_restart(std::string& error)
{
return engine_->refresh_latency_after_restart(error);
}

bool HostedPlugin::reconfigure_io_after_restart(IoLayout& layout,
std::uint32_t& latency_samples,
std::string& error)
{
return engine_->reconfigure_io_after_restart(layout, latency_samples, error);
}

bool HostedPlugin::queue_parameter(std::uint32_t id, double normalized) noexcept
{
return engine_->queue_parameter(id, normalized);
}

bool HostedPlugin::queue_parameter_from_controller(std::uint32_t id, double normalized) noexcept
{
return engine_->queue_parameter_from_controller(id, normalized);
}

bool HostedPlugin::set_controller_parameter(std::uint32_t id, double normalized) noexcept
{
return engine_->set_controller_parameter(id, normalized);
}

bool HostedPlugin::queue_processor_parameter(std::uint32_t id, double normalized) noexcept
{
return engine_->queue_processor_parameter(id, normalized);
}

bool HostedPlugin::flush_parameter_changes() noexcept
{
return engine_->flush_parameter_changes();
}

void HostedPlugin::refresh_parameter_values() noexcept
{
engine_->refresh_parameter_values();
}

bool HostedPlugin::refresh_parameter_metadata(std::string& error)
{
return engine_->refresh_parameter_metadata(error);
}

std::size_t HostedPlugin::take_parameter_updates(EngineParameterUpdate* destination,
std::size_t capacity) noexcept
{
return engine_->take_parameter_updates(destination, capacity);
}

void HostedPlugin::set_component_handler(Steinberg::Vst::IComponentHandler* handler) noexcept
{
engine_->set_component_handler(handler);
}

Steinberg::Vst::IEditController* HostedPlugin::edit_controller() const noexcept
{
return engine_->edit_controller();
}

const std::string& HostedPlugin::plugin_name() const noexcept
{
return engine_->plugin_name();
}

const std::string& HostedPlugin::loaded_class_id() const noexcept
{
return engine_->loaded_class_id();
}

std::uint32_t HostedPlugin::latency_samples() const noexcept
{
return engine_->latency_samples();
}

std::uint32_t HostedPlugin::process_context_requirements() const noexcept
{
return engine_->process_context_requirements();
}

std::uint32_t HostedPlugin::unsupported_process_context_requirements() const noexcept
{
return engine_->unsupported_process_context_requirements();
}

const std::vector<EngineParameter>& HostedPlugin::parameters() const noexcept
{
return engine_->parameters();
}

} // namespace safevst3

#endif
89 changes: 89 additions & 0 deletions src/host/hosted_plugin.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#pragma once

#ifdef _WIN32

#include "host/process_block_view.hpp"

#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>

namespace Steinberg::Vst {
class IComponentHandler;
class IEditController;
}

namespace safevst3 {

class StartupPhaseSink;
class Vst3Engine;
struct EngineParameter;
struct EngineParameterUpdate;
struct IoLayout;
struct PluginStateSnapshot;

// Protocol-neutral helper-side facade for exactly one VST3 audio effect.
// Single/Rack transports own their buffer/layout adapters outside this class.
// The implementation intentionally reuses the already-qualified Vst3Engine
// core so R0-2 introduces no second lifecycle/state implementation.
class HostedPlugin final {
public:
HostedPlugin();
~HostedPlugin();

HostedPlugin(const HostedPlugin&) = delete;
HostedPlugin& operator=(const HostedPlugin&) = delete;

bool open(const std::string& path,
const std::string& class_id,
std::uint32_t sample_rate,
std::uint32_t channels,
Steinberg::Vst::IComponentHandler* component_handler,
StartupPhaseSink* startup_phase_sink,
std::string& error);
bool open(const std::string& path,
const std::string& class_id,
std::uint32_t sample_rate,
std::uint32_t channels,
Steinberg::Vst::IComponentHandler* component_handler,
std::string& error);

void close() noexcept;
bool process(const ProcessBlockView& block) noexcept;

bool capture_state(PluginStateSnapshot& snapshot, std::string& error);
bool restore_state(const PluginStateSnapshot& snapshot, std::string& error);
bool refresh_latency_after_restart(std::string& error);
bool reconfigure_io_after_restart(IoLayout& layout,
std::uint32_t& latency_samples,
std::string& error);

bool queue_parameter(std::uint32_t id, double normalized) noexcept;
bool queue_parameter_from_controller(std::uint32_t id, double normalized) noexcept;
bool set_controller_parameter(std::uint32_t id, double normalized) noexcept;
bool queue_processor_parameter(std::uint32_t id, double normalized) noexcept;
bool flush_parameter_changes() noexcept;
void refresh_parameter_values() noexcept;
bool refresh_parameter_metadata(std::string& error);
std::size_t take_parameter_updates(EngineParameterUpdate* destination,
std::size_t capacity) noexcept;

void set_component_handler(Steinberg::Vst::IComponentHandler* handler) noexcept;
Steinberg::Vst::IEditController* edit_controller() const noexcept;

const std::string& plugin_name() const noexcept;
const std::string& loaded_class_id() const noexcept;
std::uint32_t latency_samples() const noexcept;
std::uint32_t process_context_requirements() const noexcept;
std::uint32_t unsupported_process_context_requirements() const noexcept;
const std::vector<EngineParameter>& parameters() const noexcept;

private:
std::unique_ptr<Vst3Engine> engine_;
};

} // namespace safevst3

#endif
59 changes: 59 additions & 0 deletions tests/r0_2/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
cmake_minimum_required(VERSION 3.25)
project(safevst3-r0-2-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-2 HostedPlugin characterization is Windows-only")
endif()

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)
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)

smtg_add_vst3plugin(r0-2-hosted-plugin-fixture
"${CMAKE_CURRENT_LIST_DIR}/vst3_hosted_plugin_fixture.cpp"
)
target_link_libraries(r0-2-hosted-plugin-fixture PRIVATE sdk)
target_compile_definitions(r0-2-hosted-plugin-fixture PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX)

add_executable(r0-2-hosted-plugin-characterization
"${CMAKE_CURRENT_LIST_DIR}/hosted_plugin_characterization.cpp"
"${SAFEVST3_ROOT}/src/host/hosted_plugin.cpp"
"${SAFEVST3_ROOT}/src/host/vst3_engine.cpp"
)
target_include_directories(r0-2-hosted-plugin-characterization PRIVATE "${SAFEVST3_ROOT}/src")
target_link_libraries(r0-2-hosted-plugin-characterization 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-2-hosted-plugin-characterization PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX)
add_dependencies(r0-2-hosted-plugin-characterization r0-2-hosted-plugin-fixture)

enable_testing()
add_test(
NAME r0-2-hosted-plugin-characterization
COMMAND $<TARGET_FILE:r0-2-hosted-plugin-characterization>
$<TARGET_FILE:r0-2-hosted-plugin-fixture>
)
add_test(
NAME r0-2-hosted-plugin-protocol-neutral-contract
COMMAND ${CMAKE_COMMAND}
-DHEADER=${SAFEVST3_ROOT}/src/host/hosted_plugin.hpp
-P ${CMAKE_CURRENT_LIST_DIR}/hosted_plugin_protocol_neutral.cmake
)
14 changes: 14 additions & 0 deletions tests/r0_2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# R0-2 HostedPlugin characterization

This focused Windows-only harness is the R0-2 extraction gate for issue #66.

It builds a deterministic real VST3 effect with a separated edit controller and exercises the new protocol-neutral `HostedPlugin` object directly. The characterization locks:

- closed/open/close/reopen lifecycle;
- `ProcessBlockView` processing without Single `AudioSlot` API;
- fixed latency exposure and latency-restart transaction;
- component + controller state capture/restore with observable audio state;
- helper-facing edit-controller accessor lifetime;
- public-header source contract rejecting Single transport types/layout includes.

The fixture is a correctness oracle only. It does not implement Rack transport, Rack helper, multi-plugin processing, graphical Rack UI, scanner changes or any R1 behavior.
Loading
Loading