mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30:42 -07:00
asio: cache result from CoCreateInstance to work around buggy asio drivers (#781)
## Link to GitHub Issue or related Pull Request, if one exists n/a ## Description of change IIDX likes to call `CoCreateInstance` and `init` multiple times on ASIO drivers, but some ASIO drivers like `Neva Uno` really don't like that and ends up crashing. In our wrapper, cache the instances and try to reuse them for better compatibility. ## Testing Worked fine for FlexASIO / Xonar AE / Realtek ASIO though none of these drivers repro the crashing behavior.
This commit is contained in:
@@ -154,36 +154,30 @@ namespace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// live wrappers by CLSID. ASIO drivers are single-instance, so a host that creates a
|
// one wrapper per CLSID, kept alive for the process lifetime. ASIO drivers are
|
||||||
// new instance without releasing the old one has leaked it; we track this to tear the
|
// single-instance, and some hardware drivers (e.g. Neva Uno) crash if their COM object
|
||||||
// stale one down
|
// is destroyed and re-created within a process - which the host triggers by leaking and
|
||||||
|
// re-instantiating the driver during startup probing. so we build the real driver and
|
||||||
|
// its wrapper once, hold a reference so it survives the host's Release calls, and hand
|
||||||
|
// the same wrapper back for every later CoCreate. as a result the real driver is created
|
||||||
|
// and initialized exactly once
|
||||||
std::mutex g_wrappers_mutex;
|
std::mutex g_wrappers_mutex;
|
||||||
std::vector<std::pair<CLSID, WrappedAsio *>> g_wrappers;
|
std::vector<std::pair<CLSID, WrappedAsio *>> g_wrappers;
|
||||||
|
|
||||||
// register a wrapper as the live instance for its CLSID, returning any stale wrapper it
|
WrappedAsio *find_wrapper(REFCLSID clsid) {
|
||||||
// replaces so the caller can tear it down outside the lock
|
|
||||||
WrappedAsio *register_wrapper(REFCLSID clsid, WrappedAsio *wrapper) {
|
|
||||||
std::lock_guard lock(g_wrappers_mutex);
|
std::lock_guard lock(g_wrappers_mutex);
|
||||||
for (auto &entry : g_wrappers) {
|
for (auto &entry : g_wrappers) {
|
||||||
if (IsEqualCLSID(entry.first, clsid)) {
|
if (IsEqualCLSID(entry.first, clsid)) {
|
||||||
WrappedAsio *stale = entry.second;
|
return entry.second;
|
||||||
entry.second = wrapper;
|
|
||||||
return stale;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
g_wrappers.emplace_back(clsid, wrapper);
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void unregister_wrapper(WrappedAsio *wrapper) {
|
void store_wrapper(REFCLSID clsid, WrappedAsio *wrapper) {
|
||||||
std::lock_guard lock(g_wrappers_mutex);
|
std::lock_guard lock(g_wrappers_mutex);
|
||||||
for (auto it = g_wrappers.begin(); it != g_wrappers.end(); ++it) {
|
g_wrappers.emplace_back(clsid, wrapper);
|
||||||
if (it->second == wrapper) {
|
|
||||||
g_wrappers.erase(it);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ASIO drivers registered on this system (CLSID + registry name), scanned once
|
// ASIO drivers registered on this system (CLSID + registry name), scanned once
|
||||||
@@ -256,12 +250,11 @@ WrappedAsio::StereoDownmix WrappedAsio::name_to_stereo_downmix(const char *name)
|
|||||||
}
|
}
|
||||||
|
|
||||||
WrappedAsio::~WrappedAsio() {
|
WrappedAsio::~WrappedAsio() {
|
||||||
unregister_wrapper(this);
|
|
||||||
|
|
||||||
this->detach_post_process();
|
this->detach_post_process();
|
||||||
|
|
||||||
// our refcount is decoupled from the host's (see AddRef/Release), so pReal's count is
|
// never runs mid-run: wrap() pins the wrapper so the refcount stays above zero until
|
||||||
// exactly one here and this releases/unloads it deterministically
|
// release_all_wrappers() drops the pin at shutdown - the only point this can fire, and
|
||||||
|
// only once the host has released its own references. tears down the real driver
|
||||||
this->pReal->Release();
|
this->pReal->Release();
|
||||||
|
|
||||||
log_info("audio::wrappedasio", "destroying wrapped ASIO driver, clsid={}", guid2s(this->clsid));
|
log_info("audio::wrappedasio", "destroying wrapped ASIO driver, clsid={}", guid2s(this->clsid));
|
||||||
@@ -315,8 +308,17 @@ ULONG STDMETHODCALLTYPE WrappedAsio::Release() {
|
|||||||
|
|
||||||
#pragma region IAsio
|
#pragma region IAsio
|
||||||
AsioBool __thiscall WrappedAsio::init(void *sys_handle) {
|
AsioBool __thiscall WrappedAsio::init(void *sys_handle) {
|
||||||
|
// the real driver is single-instance and kept alive; initialize it exactly once. the
|
||||||
|
// host re-calls init() on each CoCreate during probing, but re-initializing a live
|
||||||
|
// driver crashes some hardware drivers, so once we have a live driver we report success
|
||||||
|
if (this->initialized) {
|
||||||
|
log_misc("audio::wrappedasio", "init skipped, '{}' already initialized", this->driver_name);
|
||||||
|
return AsioTrue;
|
||||||
|
}
|
||||||
|
|
||||||
const AsioBool result = this->pReal->init(sys_handle);
|
const AsioBool result = this->pReal->init(sys_handle);
|
||||||
if (result == AsioTrue) {
|
if (result == AsioTrue) {
|
||||||
|
this->initialized = true;
|
||||||
log_info(
|
log_info(
|
||||||
"audio::wrappedasio",
|
"audio::wrappedasio",
|
||||||
"init succeeded for '{}' (driver version {})",
|
"init succeeded for '{}' (driver version {})",
|
||||||
@@ -346,6 +348,7 @@ void __thiscall WrappedAsio::get_error_message(char *string) {
|
|||||||
AsioError __thiscall WrappedAsio::start() {
|
AsioError __thiscall WrappedAsio::start() {
|
||||||
const AsioError result = this->pReal->start();
|
const AsioError result = this->pReal->start();
|
||||||
if (result == ASE_OK) {
|
if (result == ASE_OK) {
|
||||||
|
this->started = true;
|
||||||
log_info(
|
log_info(
|
||||||
"audio::wrappedasio",
|
"audio::wrappedasio",
|
||||||
"start succeeded, ASIO stream is now running on '{}'",
|
"start succeeded, ASIO stream is now running on '{}'",
|
||||||
@@ -360,6 +363,7 @@ AsioError __thiscall WrappedAsio::start() {
|
|||||||
AsioError __thiscall WrappedAsio::stop() {
|
AsioError __thiscall WrappedAsio::stop() {
|
||||||
const AsioError result = this->pReal->stop();
|
const AsioError result = this->pReal->stop();
|
||||||
if (result == ASE_OK) {
|
if (result == ASE_OK) {
|
||||||
|
this->started = false;
|
||||||
log_info("audio::wrappedasio", "stop succeeded, ASIO stream on '{}' halted", this->driver_name);
|
log_info("audio::wrappedasio", "stop succeeded, ASIO stream on '{}' halted", this->driver_name);
|
||||||
} else {
|
} else {
|
||||||
log_warning("audio::wrappedasio", "stop failed, err={}", static_cast<long>(result));
|
log_warning("audio::wrappedasio", "stop failed, err={}", static_cast<long>(result));
|
||||||
@@ -740,6 +744,27 @@ void WrappedAsio::detach_post_process() {
|
|||||||
WrappedAsio::active_instance.compare_exchange_strong(expected, nullptr);
|
WrappedAsio::active_instance.compare_exchange_strong(expected, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WrappedAsio::quiesce_for_reuse() {
|
||||||
|
// a previous abandoned probing cycle may have left a running stream and live buffers on
|
||||||
|
// the real driver without calling stop/dispose; tear that down now (without destroying
|
||||||
|
// the driver) so the host's next create_buffers starts clean. stop() guarantees no
|
||||||
|
// further buffer_switch, and dispose_buffers detaches our trampolines
|
||||||
|
if (this->started) {
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"reuse: stopping leftover stream on '{}' before handing the driver back",
|
||||||
|
this->driver_name);
|
||||||
|
this->stop();
|
||||||
|
}
|
||||||
|
if (this->buffers_created) {
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"reuse: disposing leftover buffers on '{}' before handing the driver back",
|
||||||
|
this->driver_name);
|
||||||
|
this->dispose_buffers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void WrappedAsio::apply_output_volume(long double_buffer_index) {
|
void WrappedAsio::apply_output_volume(long double_buffer_index) {
|
||||||
if (double_buffer_index != 0 && double_buffer_index != 1) {
|
if (double_buffer_index != 0 && double_buffer_index != 1) {
|
||||||
return;
|
return;
|
||||||
@@ -838,6 +863,7 @@ AsioError __thiscall WrappedAsio::create_buffers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this->publish_post_process(buffer_size);
|
this->publish_post_process(buffer_size);
|
||||||
|
this->buffers_created = true;
|
||||||
} else {
|
} else {
|
||||||
log_warning(
|
log_warning(
|
||||||
"audio::wrappedasio",
|
"audio::wrappedasio",
|
||||||
@@ -940,6 +966,7 @@ AsioError WrappedAsio::create_buffers_front_pair(
|
|||||||
// device's 2.0 output. then publish ourselves to the realtime thread
|
// device's 2.0 output. then publish ourselves to the realtime thread
|
||||||
this->record_downmix_channels(buffer_infos, num_channels, buffer_size);
|
this->record_downmix_channels(buffer_infos, num_channels, buffer_size);
|
||||||
this->publish_post_process(buffer_size);
|
this->publish_post_process(buffer_size);
|
||||||
|
this->buffers_created = true;
|
||||||
|
|
||||||
log_info(
|
log_info(
|
||||||
"audio::wrappedasio",
|
"audio::wrappedasio",
|
||||||
@@ -962,6 +989,7 @@ AsioError __thiscall WrappedAsio::dispose_buffers() {
|
|||||||
this->volume_channels.clear();
|
this->volume_channels.clear();
|
||||||
this->volume_active = false;
|
this->volume_active = false;
|
||||||
this->downmix_active = false;
|
this->downmix_active = false;
|
||||||
|
this->buffers_created = false;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -986,29 +1014,54 @@ namespace hooks::audio::asio {
|
|||||||
auto *wrapper = new WrappedAsio(
|
auto *wrapper = new WrappedAsio(
|
||||||
reinterpret_cast<IAsio *>(real), clsid, registered_asio_name(clsid));
|
reinterpret_cast<IAsio *>(real), clsid, registered_asio_name(clsid));
|
||||||
|
|
||||||
// if the host already had a live wrapper for this CLSID it leaked the previous
|
// pin the wrapper (and the real driver it owns) for the process lifetime; later
|
||||||
// instance (ASIO is single-instance); tear it down now so the real driver is
|
// CoCreate calls reuse this same instance via wrap_existing
|
||||||
// released before the reinit. works around games (iidx32+) that ref twice but
|
wrapper->AddRef();
|
||||||
// deref once before re-initializing.
|
store_wrapper(clsid, wrapper);
|
||||||
//
|
|
||||||
// FlexASIO 1.9 and many DAC ASIO drivers can't handle this; FlexASIO 1.10 and
|
|
||||||
// Xonar AE can
|
|
||||||
if (WrappedAsio *stale = register_wrapper(clsid, wrapper)) {
|
|
||||||
log_info(
|
|
||||||
"audio::wrappedasio",
|
|
||||||
"host did not release previous instance for clsid={}, forcing teardown",
|
|
||||||
guid2s(clsid));
|
|
||||||
|
|
||||||
// the stale instance may still have a running stream on the driver's realtime
|
|
||||||
// thread. stop and dispose it before deleting so no in-flight buffer switch
|
|
||||||
// (e.g. our volume trampoline) touches buffers we are about to free. ASIO
|
|
||||||
// guarantees no further buffer_switch once stop() returns, and dispose_buffers
|
|
||||||
// detaches our trampolines, fully quiescing the realtime path before delete
|
|
||||||
stale->stop();
|
|
||||||
stale->dispose_buffers();
|
|
||||||
delete stale;
|
|
||||||
}
|
|
||||||
|
|
||||||
return static_cast<IAsio *>(wrapper);
|
return static_cast<IAsio *>(wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IUnknown *wrap_existing(REFCLSID clsid) {
|
||||||
|
WrappedAsio *wrapper = find_wrapper(clsid);
|
||||||
|
if (wrapper == nullptr) {
|
||||||
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log_misc(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"reusing cached ASIO driver instance, clsid={}",
|
||||||
|
guid2s(clsid));
|
||||||
|
|
||||||
|
wrapper->quiesce_for_reuse();
|
||||||
|
|
||||||
|
// hand the host another reference to the one instance we keep alive
|
||||||
|
wrapper->AddRef();
|
||||||
|
return static_cast<IAsio *>(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
void release_all_wrappers() {
|
||||||
|
// collect the pinned wrappers under the lock, then drop our references outside it so
|
||||||
|
// a final Release (which can run the driver's own teardown) never happens while the
|
||||||
|
// lock is held
|
||||||
|
std::vector<std::pair<CLSID, WrappedAsio *>> wrappers;
|
||||||
|
{
|
||||||
|
std::lock_guard lock(g_wrappers_mutex);
|
||||||
|
wrappers = g_wrappers;
|
||||||
|
g_wrappers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto &entry : wrappers) {
|
||||||
|
// drop only the pin wrap() took. if the host has already released its own
|
||||||
|
// references (the normal case at shutdown) this destroys the wrapper and releases
|
||||||
|
// the real driver; if the host is still using it, the refcount stays above zero
|
||||||
|
// and we leave its live stream/buffers untouched
|
||||||
|
log_misc(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"releasing cached ASIO driver instance, clsid={}",
|
||||||
|
guid2s(entry.first));
|
||||||
|
entry.second->Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,20 @@ namespace hooks::audio::asio {
|
|||||||
bool is_asio_creation(REFCLSID rclsid, REFIID riid);
|
bool is_asio_creation(REFCLSID rclsid, REFIID riid);
|
||||||
|
|
||||||
// wrap a real ASIO driver instance, taking ownership of the supplied reference, and
|
// wrap a real ASIO driver instance, taking ownership of the supplied reference, and
|
||||||
// return a proxy that forwards every call to it
|
// return a proxy that forwards every call to it. also records it as the cached
|
||||||
|
// instance for its CLSID so later CoCreate calls can reuse it (see wrap_existing)
|
||||||
IUnknown *wrap(REFCLSID clsid, void *real);
|
IUnknown *wrap(REFCLSID clsid, void *real);
|
||||||
|
|
||||||
|
// if a cached wrapper already exists for this CLSID, return it (with an added
|
||||||
|
// reference); otherwise nullptr to signal the caller to create the real driver and
|
||||||
|
// wrap it. lets the host reuse one driver instance instead of re-instantiating it
|
||||||
|
IUnknown *wrap_existing(REFCLSID clsid);
|
||||||
|
|
||||||
|
// drop the process-lifetime references taken by wrap() so cached drivers can be released
|
||||||
|
// at shutdown. only relinquishes our pin, so a real driver is torn down once the host
|
||||||
|
// has released its own references too. call from a controlled shutdown point, never from
|
||||||
|
// a static destructor (the driver DLL may already be unloaded)
|
||||||
|
void release_all_wrappers();
|
||||||
}
|
}
|
||||||
|
|
||||||
// transparent proxy around a real ASIO driver; a single place to intercept ASIO traffic
|
// transparent proxy around a real ASIO driver; a single place to intercept ASIO traffic
|
||||||
@@ -103,6 +115,10 @@ struct WrappedAsio final : IAsio {
|
|||||||
AsioError __thiscall output_ready() override;
|
AsioError __thiscall output_ready() override;
|
||||||
#pragma endregion
|
#pragma endregion
|
||||||
|
|
||||||
|
// quiesces any leftover stream/buffer state before the cached wrapper is handed back
|
||||||
|
// for reuse, without destroying the real driver (see wrap_existing)
|
||||||
|
void quiesce_for_reuse();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// create_buffers implementation used when a stereo extraction is active: forwards only
|
// create_buffers implementation used when a stereo extraction is active: forwards only
|
||||||
// the channels the real device has and hands the game throwaway buffers for the rest
|
// the channels the real device has and hands the game throwaway buffers for the rest
|
||||||
@@ -170,6 +186,14 @@ private:
|
|||||||
// unambiguous name; constant for our lifetime
|
// unambiguous name; constant for our lifetime
|
||||||
std::string driver_name;
|
std::string driver_name;
|
||||||
|
|
||||||
|
// the real driver is initialized exactly once; repeat init() calls are a no-op success
|
||||||
|
bool initialized = false;
|
||||||
|
|
||||||
|
// whether the real driver currently has a buffer set / running stream. used to quiesce
|
||||||
|
// leftover state when the cached wrapper is reused (see quiesce_for_reuse)
|
||||||
|
bool buffers_created = false;
|
||||||
|
bool started = false;
|
||||||
|
|
||||||
// our own reference count; we hold one reference on pReal and release it when this
|
// our own reference count; we hold one reference on pReal and release it when this
|
||||||
// drops to zero
|
// drops to zero
|
||||||
std::atomic<ULONG> ref_count {1};
|
std::atomic<ULONG> ref_count {1};
|
||||||
|
|||||||
@@ -68,6 +68,15 @@ static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
|
|||||||
|
|
||||||
HRESULT ret;
|
HRESULT ret;
|
||||||
|
|
||||||
|
// ASIO: if we already hold a cached instance for this CLSID, hand back a fresh
|
||||||
|
// wrapper over it instead of re-creating the real driver (see asio_proxy.cpp)
|
||||||
|
if (ppv != nullptr && hooks::audio::asio::is_asio_creation(rclsid, riid)) {
|
||||||
|
if (IUnknown *reused = hooks::audio::asio::wrap_existing(rclsid)) {
|
||||||
|
*ppv = reused;
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// call original
|
// call original
|
||||||
ret = CoCreateInstance_orig(rclsid, pUnkOuter, dwClsContext, riid, ppv);
|
ret = CoCreateInstance_orig(rclsid, pUnkOuter, dwClsContext, riid, ppv);
|
||||||
if (FAILED(ret)) {
|
if (FAILED(ret)) {
|
||||||
@@ -130,5 +139,9 @@ namespace hooks::audio {
|
|||||||
CLIENT = nullptr;
|
CLIENT = nullptr;
|
||||||
}
|
}
|
||||||
stop_low_latency();
|
stop_low_latency();
|
||||||
|
|
||||||
|
// release the ASIO drivers we kept pinned for the process lifetime now that we are
|
||||||
|
// at a controlled shutdown point (see asio_proxy.cpp)
|
||||||
|
hooks::audio::asio::release_all_wrappers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user