Merge branch 'spice2x' into spice22x

This commit is contained in:
[ ]
2026-02-19 16:05:17 +09:00
211 changed files with 26320 additions and 19061 deletions
+230
View File
@@ -0,0 +1,230 @@
#include "audio.h"
#include <mutex>
#include <external/robin_hood.h>
#include <windows.h>
#include <mmsystem.h>
#include <msacm.h>
#include "avs/game.h"
#include "util/detour.h"
#include "util/libutils.h"
#include "util/logging.h"
#include "util/memutils.h"
#include "util/time.h"
#include "util/unique_plain_ptr.h"
#define ACM_CACHE_ENABLED 1
#define ACM_DEBUG_PERF 0
#define ACM_DEBUG_VERBOSE 0
#if ACM_DEBUG_VERBOSE
#define log_debug(module, format_str, ...) logger::push( \
LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY)
#else
#define log_debug(module, format_str, ...)
#endif
namespace hooks::audio::acm {
struct WAVE_FORMAT {
uint32_t suggest;
// for WAVEFORMATEX
std::vector<uint8_t> wave_format_ex;
bool operator==(const WAVE_FORMAT& other) const {
return (this->suggest == other.suggest &&
this->wave_format_ex == other.wave_format_ex);
}
};
}
namespace robin_hood {
template <>
struct hash<hooks::audio::acm::WAVE_FORMAT> {
std::size_t operator()(const hooks::audio::acm::WAVE_FORMAT& k) const noexcept {
std::size_t h = robin_hood::hash_bytes(
k.wave_format_ex.data(), k.wave_format_ex.size());
h = h ^ robin_hood::hash<uint32_t>{}(k.suggest) << 1;
return h;
}
};
}
namespace hooks::audio::acm {
static decltype(acmFormatSuggest) *acmFormatSuggest_orig = nullptr;
static std::mutex acm_formats_mutex;
static robin_hood::unordered_map<WAVE_FORMAT, std::vector<uint8_t>> acm_formats;
// hooks calls to acmFormatSuggest and returns cached results from previous calls
//
// iidx calls this many times during song load and reload (function of # of keysounds)
// - with near-identical arguments over and over again
// (when hovering over song in song select, selecting a song, and exiting result screen)
//
// on Linux this results in ACM module load/unload which is very expensive,
// resulting in significant perf improvement (seconds per song load);
// on Windows this saves milliseconds per song, at best
//
// https://codeberg.org/nixac/spicetools/issues/2
// https://codeberg.org/nixac/spicetools/issues/4
MMRESULT
acmFormatSuggest_cached (
HACMDRIVER had,
LPWAVEFORMATEX pwfxSrc,
LPWAVEFORMATEX pwfxDst,
DWORD cbwfxDst,
DWORD fdwSuggest) {
if (had != 0 || cbwfxDst == 0) {
return acmFormatSuggest_orig(had, pwfxSrc, pwfxDst, cbwfxDst, fdwSuggest);
}
WAVE_FORMAT key;
key.suggest = fdwSuggest;
const size_t src_size = sizeof(*pwfxSrc) + pwfxSrc->cbSize;
key.wave_format_ex.insert(
key.wave_format_ex.end(),
reinterpret_cast<uint8_t *>(pwfxSrc),
reinterpret_cast<uint8_t *>(pwfxSrc) + src_size);
std::lock_guard<std::mutex> lock(acm_formats_mutex);
MMRESULT mmresult = 0;
if (acm_formats.contains(key)) {
// found in cache
const auto &result = acm_formats.at(key);
if (cbwfxDst >= result.size()) {
// dest buffer big enough, return cached result
log_debug("audio::acm", "acmFormatSuggest cache hit, copying {} bytes", result.size());
std::memcpy(reinterpret_cast<uint8_t *>(pwfxDst), result.data(), result.size());
mmresult = 0;
} else {
// dest buffer not big enough, call original
log_debug("audio::acm", "acmFormatSuggest cache fail; cbwfxDst too small ({})", cbwfxDst);
mmresult = acmFormatSuggest_orig(had, pwfxSrc, pwfxDst, cbwfxDst, fdwSuggest);
}
} else {
log_debug("audio::acm", "acmFormatSuggest cache miss; calling original");
mmresult = acmFormatSuggest_orig(had, pwfxSrc, pwfxDst, cbwfxDst, fdwSuggest);
// cache the result, but don't allow unconstrained growth
if (mmresult == 0 && acm_formats.size() < 128) {
const size_t dest_size = sizeof(*pwfxDst) + pwfxDst->cbSize;
log_debug(
"audio::acm",
"acmFormatSuggest cache add; current cache size {}, new data {} bytes",
acm_formats.size(),
dest_size);
acm_formats[key] = std::vector<uint8_t>();
acm_formats[key].reserve(dest_size);
acm_formats[key].insert(
acm_formats[key].begin(),
reinterpret_cast<uint8_t *>(pwfxDst),
reinterpret_cast<uint8_t *>(pwfxDst) + dest_size);
}
}
return mmresult;
}
MMRESULT
ACMAPI
acmFormatSuggest_hook (
HACMDRIVER had,
LPWAVEFORMATEX pwfxSrc,
LPWAVEFORMATEX pwfxDst,
DWORD cbwfxDst,
DWORD fdwSuggest) {
log_debug(
"audio::acm",
"acmFormatSuggest called: had={}, "
"formattag={}, ch={}, samplespersec={}, bytespersec={}, blockalign={}, bits={}, extrasize={}",
fmt::ptr(had),
pwfxSrc->wFormatTag,
pwfxSrc->nChannels,
pwfxSrc->nSamplesPerSec,
pwfxSrc->nAvgBytesPerSec,
pwfxSrc->nBlockAlign,
pwfxSrc->wBitsPerSample,
pwfxSrc->cbSize);
#if ACM_DEBUG_PERF
const auto start = get_performance_milliseconds();
#endif
// make a call to acmFormatSuggest
#if ACM_CACHE_ENABLED
const auto result = acmFormatSuggest_cached(had, pwfxSrc, pwfxDst, cbwfxDst, fdwSuggest);
#else
const auto result = acmFormatSuggest_orig(had, pwfxSrc, pwfxDst, cbwfxDst, fdwSuggest);
#endif
// log result
#if ACM_DEBUG_PERF
const auto delta = get_performance_milliseconds() - start;
static double delta_total = 0;
delta_total += delta;
log_info(
"audio::acm",
"acmFormatSuggest_hook returned {}, took {} us (running total {} ms)",
result,
delta * 1000,
delta_total);
#else
log_debug("audio::acm", "acmFormatSuggest_hook returned {}", result);
#endif
if (result == 0 && cbwfxDst > sizeof(WAVEFORMATEX)) {
log_debug(
"audio::acm",
"....formattag={}, ch={}, samplespersec={}, bytespersec={}, blockalign={}, bits={}, extrasize={}",
pwfxDst->wFormatTag,
pwfxDst->nChannels,
pwfxDst->nSamplesPerSec,
pwfxDst->nAvgBytesPerSec,
pwfxDst->nBlockAlign,
pwfxDst->wBitsPerSample,
pwfxDst->cbSize);
}
return result;
}
void init() {
// only enabled on Linux as the performance gains are negligible on Windows
#if SPICE_LINUX
HMODULE msacm = libutils::try_library("msacm32.dll");
if (msacm == nullptr) {
log_info("audio::acm", "msacm32.dll failed to hook");
return;
}
acmFormatSuggest_orig = detour::iat_try(
"acmFormatSuggest", acmFormatSuggest_hook, avs::game::DLL_INSTANCE);
if (acmFormatSuggest_orig != nullptr) {
log_misc("audio::acm", "acmFormatSuggest hooked");
#if !ACM_CACHE_ENABLED
log_warning("audio::acm", "acmFormatSuggest cache DISABLED");
#endif
}
#endif
}
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
namespace hooks::audio::acm {
void init();
}
+2
View File
@@ -13,6 +13,7 @@
#include "util/memutils.h"
#include "audio_private.h"
#include "acm.h"
#ifdef _MSC_VER
DEFINE_GUID(CLSID_MMDeviceEnumerator,
@@ -101,6 +102,7 @@ namespace hooks::audio {
log_info("audio", "initializing");
init_low_latency();
hooks::audio::acm::init();
// general hooks
CoCreateInstance_orig = detour::iat_try("CoCreateInstance", CoCreateInstance_hook);
+12 -12
View File
@@ -8,6 +8,7 @@
#include "hooks/audio/audio_private.h"
#include "hooks/audio/backends/mmdevice/audio_endpoint_volume.h"
#include "hooks/audio/backends/wasapi/audio_client.h"
#include "util/utils.h"
#define PRINT_FAILED_RESULT(name, ret) \
do { \
@@ -56,6 +57,7 @@ ULONG STDMETHODCALLTYPE WrappedIMMDevice::Release() {
ULONG refs = pReal != nullptr ? pReal->Release() : 0;
if (refs == 0) {
log_misc("audio::mmdevice", "WrappedIMMDevice::Release");
delete this;
}
@@ -69,7 +71,7 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDevice::Activate(
PROPVARIANT *pActivationParams,
void **ppInterface)
{
log_misc("audio::mmdevice", "WrappedIMMDevice::Activate");
log_misc("audio::mmdevice", "WrappedIMMDevice::Activate {}", guid2s(iid));
// call original
HRESULT ret = pReal->Activate(iid, dwClsCtx, pActivationParams, ppInterface);
@@ -80,7 +82,10 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDevice::Activate(
return ret;
}
if (iid == IID_IAudioClient) {
// almost all games request IAudioClient
// so far we have not seen any games request IAudioClient2
// SDVX EG Final uses IID_IAudioClient3, but only when shared mode patch is on
if (iid == IID_IAudioClient || iid == IID_IAudioClient3) {
// prevent initialization recursion when using some ASIO backends that proxy to DirectSound, WASAPI, or WDM
// like ASIO4All or FlexASIO
@@ -90,23 +95,18 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDevice::Activate(
}
std::lock_guard initialize_guard(hooks::audio::INITIALIZE_LOCK, std::adopt_lock);
auto client = reinterpret_cast<IAudioClient *>(*ppInterface);
// release old audio client if initialized
if (hooks::audio::CLIENT) {
hooks::audio::CLIENT->Release();
}
/*
ret = wrap_audio_client(pReal, dwClsCtx, pActivationParams, &client);
if (FAILED(ret)) {
return ret;
IAudioClient *client = nullptr;
if (iid == IID_IAudioClient) {
client = wrap_audio_client(reinterpret_cast<IAudioClient *>(*ppInterface));
} else { // IID_IAudioClient3
client = wrap_audio_client3(reinterpret_cast<IAudioClient3 *>(*ppInterface));
}
*/
client = wrap_audio_client(client);
*ppInterface = client;
// persist the audio client
hooks::audio::CLIENT = client;
hooks::audio::CLIENT->AddRef();
@@ -0,0 +1,44 @@
#include "device_collection.h"
#include "device.h"
#include "util/utils.h"
#include "util/logging.h"
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceCollection::QueryInterface(REFIID riid, void **ppvObj) {
if (ppvObj == nullptr) {
return E_POINTER;
}
if (riid == __uuidof(IMMDeviceCollection)) {
this->AddRef();
*ppvObj = this;
return S_OK;
}
return pReal->QueryInterface(riid, ppvObj);
}
ULONG STDMETHODCALLTYPE WrappedIMMDeviceCollection::AddRef() {
return pReal->AddRef();
}
ULONG STDMETHODCALLTYPE WrappedIMMDeviceCollection::Release() {
// get reference count of underlying interface
ULONG refs = pReal != nullptr ? pReal->Release() : 0;
if (refs == 0) {
delete this;
}
return refs;
}
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceCollection::GetCount(UINT *pcDevices) {
return pReal->GetCount(pcDevices);
}
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceCollection::Item(UINT nDevice, IMMDevice **ppDevice) {
log_info("audio", "WrappedIMMDeviceCollection::Item[{}]", nDevice);
// call original
const auto hr = pReal->Item(nDevice, ppDevice);
// wrap interface
*ppDevice = new WrappedIMMDevice(*ppDevice);
return hr;
}
@@ -0,0 +1,28 @@
#pragma once
#include <initguid.h>
#include <mmdeviceapi.h>
struct WrappedIMMDeviceCollection : IMMDeviceCollection {
explicit WrappedIMMDeviceCollection(IMMDeviceCollection *orig) : pReal(orig) {
}
WrappedIMMDeviceCollection(const WrappedIMMDeviceCollection &) = delete;
WrappedIMMDeviceCollection &operator=(const WrappedIMMDeviceCollection &) = delete;
virtual ~WrappedIMMDeviceCollection() = default;
#pragma region IUnknown
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
virtual ULONG STDMETHODCALLTYPE AddRef() override;
virtual ULONG STDMETHODCALLTYPE Release() override;
#pragma endregion
#pragma region IMMDeviceCollection
virtual HRESULT STDMETHODCALLTYPE GetCount(UINT *pcDevices) override;
virtual HRESULT STDMETHODCALLTYPE Item(UINT nDevice, IMMDevice **ppDevice) override;
#pragma endregion
private:
IMMDeviceCollection *const pReal;
};
@@ -1,4 +1,5 @@
#include "device_enumerator.h"
#include "device_collection.h"
#include "device.h"
#include "util/utils.h"
@@ -42,7 +43,11 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDeviceEnumerator::EnumAudioEndpoints(
DWORD dwStateMask,
IMMDeviceCollection **ppDevices)
{
return pReal->EnumAudioEndpoints(dataFlow, dwStateMask, ppDevices);
const auto hr = pReal->EnumAudioEndpoints(dataFlow, dwStateMask, ppDevices);
if (SUCCEEDED(hr) && (ppDevices != nullptr) && (*ppDevices != nullptr)) {
*ppDevices = new WrappedIMMDeviceCollection(*ppDevices);
}
return hr;
}
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceEnumerator::GetDefaultAudioEndpoint(
+112 -126
View File
@@ -12,6 +12,7 @@
#include "hooks/audio/implementations/pipewire.h"
#include "hooks/audio/implementations/none.h"
//#include "util/co_task_mem_ptr.h"
#include "util/utils.h"
#include "defs.h"
#include "dummy_audio_client.h"
@@ -42,114 +43,9 @@ static void fix_rec_format(WAVEFORMATEX *pFormat) {
pFormat->nAvgBytesPerSec = pFormat->nSamplesPerSec * pFormat->nBlockAlign;
}
// TODO(felix): is it appropriate to automatically switch to shared mode? should we do a
// `MessageBox` to notify the user?
/*
static bool check_for_exclusive_access(IAudioClient *client) {
static bool checked_once = false;
static bool previous_check_result = false;
CoTaskMemPtr<WAVEFORMATEX> mix_format;
REFERENCE_TIME requested_duration = 0;
if (checked_once) {
return previous_check_result;
}
if (audio::BACKEND.has_value()) {
return false;
}
// scope function so it has access to the local static variables
auto set_result = [](bool result) {
checked_once = true;
previous_check_result = result;
return result;
};
HRESULT ret = client->GetMixFormat(mix_format.ppv());
if (FAILED(ret)) {
PRINT_FAILED_RESULT("IAudioClient::GetMixFormat", ret);
return set_result(false);
}
log_info("audio::wasapi", "Mix Format:");
print_format(mix_format.data());
ret = client->IsFormatSupported(AUDCLNT_SHAREMODE_EXCLUSIVE, mix_format.data(), nullptr);
if (ret == AUDCLNT_E_UNSUPPORTED_FORMAT) {
auto mix_format_ex = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(mix_format.data());
log_warning("audio::wasapi", "device does not natively support the mix format, converting to PCM");
if (mix_format->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
IsEqualGUID(GUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, mix_format_ex->SubFormat))
{
mix_format_ex->Format.wBitsPerSample = 16;
mix_format_ex->Format.nBlockAlign = mix_format_ex->Format.nChannels * (mix_format_ex->Format.wBitsPerSample / 8);
mix_format_ex->Format.nAvgBytesPerSec = mix_format_ex->Format.nSamplesPerSec * mix_format_ex->Format.nBlockAlign;
mix_format_ex->Samples.wValidBitsPerSample = 16;
mix_format_ex->SubFormat = GUID_KSDATAFORMAT_SUBTYPE_PCM;
} else if (mix_format->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) {
mix_format->wBitsPerSample = 16;
mix_format->nBlockAlign = mix_format->nChannels * (mix_format->wBitsPerSample / 8);
mix_format->nAvgBytesPerSec = mix_format->nSamplesPerSec * mix_format->nBlockAlign;
mix_format->wFormatTag = WAVE_FORMAT_PCM;
} else {
log_warning("audio::wasapi", "mix format is not a floating point format");
return set_result(false);
}
ret = client->IsFormatSupported(AUDCLNT_SHAREMODE_EXCLUSIVE, mix_format.data(), nullptr);
if (FAILED(ret)) {
log_warning("audio::wasapi", "mix format is not supported");
return set_result(false);
}
}
ret = client->GetDevicePeriod(nullptr, &requested_duration);
if (FAILED(ret)) {
PRINT_FAILED_RESULT("IAudioClient::GetDevicePeriod", ret);
return false;
}
ret = client->Initialize(
AUDCLNT_SHAREMODE_EXCLUSIVE,
AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
requested_duration,
requested_duration,
mix_format.data(),
nullptr);
if (ret == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED || SUCCEEDED(ret)) {
log_info("audio::wasapi", "exclusive mode is available, disabling backend");
return set_result(true);
} else {
log_warning("audio::wasapi", "exclusive mode is not available, enabling backend, hr={}", FMT_HRESULT(ret));
}
return set_result(false);
}
HRESULT wrap_audio_client(
IMMDevice *device,
DWORD cls_ctx,
PROPVARIANT *activation_params,
IAudioClient **audio_client)
{
auto exclusive_available = check_for_exclusive_access(*audio_client);
(*audio_client)->Stop();
(*audio_client)->Reset();
(*audio_client)->Release();
*audio_client = nullptr;
SAFE_CALL("IMMDevice", "Activate", device->Activate(
IID_IAudioClient,
cls_ctx,
activation_params,
reinterpret_cast<void **>(audio_client)));
*/
IAudioClient *wrap_audio_client(IAudioClient *audio_client) {
log_misc("audio::wasapi", "wrapping IAudioClient");
AudioBackend *backend = nullptr;
bool requires_dummy = false;
@@ -172,9 +68,6 @@ IAudioClient *wrap_audio_client(IAudioClient *audio_client) {
break;
}
}
//} else if (!exclusive_available) {
// backend = new WaveOutBackend();
//}
IAudioClient *new_client;
@@ -190,6 +83,23 @@ IAudioClient *wrap_audio_client(IAudioClient *audio_client) {
return new_client;
}
IAudioClient3 *wrap_audio_client3(IAudioClient3 *audio_client) {
// TODO: ASIO backend for IAudioClient3, if there is a game that needs it
log_misc("audio::wasapi", "wrapping IAudioClient3");
if (hooks::audio::BACKEND.has_value()) {
log_fatal(
"audio::wasapi",
"IAudioClient3 does not currently support backends! clear -audiobackend and try again");
}
if (hooks::audio::USE_DUMMY) {
log_fatal(
"audio::wasapi",
"IAudioClient3 does not currently support dummy context, clear -audiodummy and try again");
}
return new WrappedIAudioClient(audio_client, nullptr);
}
// IUnknown
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::QueryInterface(REFIID riid, void **ppvObj) {
@@ -198,8 +108,8 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::QueryInterface(REFIID riid, void
}
if (riid == IID_WrappedIAudioClient ||
riid == IID_IAudioClient)
{
riid == IID_IAudioClient ||
riid == IID_IAudioClient3) {
this->AddRef();
*ppvObj = this;
@@ -290,20 +200,6 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
}
log_info("audio::wasapi", "IAudioClient::Initialize success, hr={}", FMT_HRESULT(ret));
/*
if (ShareMode == AUDCLNT_SHAREMODE_SHARED) {
IAudioClockAdjustment *clock = nullptr;
SAFE_CALL("IAudioClient", "GetService", pReal->GetService(
IID_IAudioClockAdjustment,
reinterpret_cast<void **>(&clock)));
SAFE_CALL("IAudioClockAdjustment", "SetSampleRate", clock->SetSampleRate(
static_cast<float>(pFormat->nSamplesPerSec)));
}
*/
copy_wave_format(&hooks::audio::FORMAT, pFormat);
return ret;
@@ -490,3 +386,93 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetService(REFIID riid, void **pp
return ret;
}
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::IsOffloadCapable(
AUDIO_STREAM_CATEGORY Category,
BOOL *pbOffloadCapable) {
WRAP_VERBOSE;
CHECK_RESULT(pReal3->IsOffloadCapable(Category, pbOffloadCapable));
}
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::SetClientProperties(
const AudioClientProperties *pProperties) {
WRAP_VERBOSE;
CHECK_RESULT(pReal3->SetClientProperties(pProperties));
}
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetBufferSizeLimits(
const WAVEFORMATEX *pFormat,
BOOL bEventDriven,
REFERENCE_TIME *phnsMinBufferDuration,
REFERENCE_TIME *phnsMaxBufferDuration) {
WRAP_VERBOSE;
CHECK_RESULT(pReal3->GetBufferSizeLimits(
pFormat,
bEventDriven,
phnsMinBufferDuration,
phnsMaxBufferDuration));
}
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetSharedModeEnginePeriod(
const WAVEFORMATEX *pFormat,
UINT32 *pDefaultPeriodInFrames,
UINT32 *pFundamentalPeriodInFrames,
UINT32 *pMinPeriodInFrames,
UINT32 *pMaxPeriodInFrames) {
WRAP_VERBOSE;
CHECK_RESULT(pReal3->GetSharedModeEnginePeriod(
pFormat,
pDefaultPeriodInFrames,
pFundamentalPeriodInFrames,
pMinPeriodInFrames,
pMaxPeriodInFrames));
}
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetCurrentSharedModeEnginePeriod(
WAVEFORMATEX **ppFormat,
UINT32 *pCurrentPeriodInFrames) {
WRAP_VERBOSE;
CHECK_RESULT(pReal3->GetCurrentSharedModeEnginePeriod(
ppFormat,
pCurrentPeriodInFrames));
}
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::InitializeSharedAudioStream(
DWORD StreamFlags,
UINT32 PeriodInFrames,
const WAVEFORMATEX *pFormat,
LPCGUID AudioSessionGuid) {
if (!pFormat) {
return E_POINTER;
}
// verbose output
log_info("audio::wasapi", "IAudioClient3::InitializeSharedAudioStream hook hit");
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(AUDCLNT_SHAREMODE_SHARED));
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(StreamFlags));
log_info("audio::wasapi", "... PeriodInFrames : {}", PeriodInFrames);
print_format(pFormat);
// call next
HRESULT ret = pReal3->InitializeSharedAudioStream(
StreamFlags,
PeriodInFrames,
pFormat,
AudioSessionGuid);
// check for failure
if (FAILED(ret)) {
PRINT_FAILED_RESULT("IAudioClient3", "InitializeSharedAudioStream", ret);
return ret;
}
log_info("audio::wasapi", "IAudioClient3::InitializeSharedAudioStream success, hr={}", FMT_HRESULT(ret));
copy_wave_format(&hooks::audio::FORMAT, pFormat);
return ret;
}
+45 -4
View File
@@ -16,9 +16,16 @@ static const GUID IID_WrappedIAudioClient = {
};
IAudioClient *wrap_audio_client(IAudioClient *client);
IAudioClient3 *wrap_audio_client3(IAudioClient3 *client);
struct WrappedIAudioClient : IAudioClient {
explicit WrappedIAudioClient(IAudioClient *orig, AudioBackend *backend) : pReal(orig), backend(backend) {
struct WrappedIAudioClient : IAudioClient3 {
explicit WrappedIAudioClient(IAudioClient3 *orig3, AudioBackend *backend) :
pReal(orig3), pReal3(orig3), backend(backend) {
}
explicit WrappedIAudioClient(IAudioClient *orig, AudioBackend *backend) :
pReal(orig), pReal3(nullptr), backend(backend) {
}
WrappedIAudioClient(const WrappedIAudioClient &) = delete;
@@ -47,9 +54,43 @@ struct WrappedIAudioClient : IAudioClient {
HRESULT STDMETHODCALLTYPE GetService(REFIID riid, void **ppv) override;
#pragma endregion
IAudioClient *const pReal;
AudioBackend *const backend;
#pragma region IAudioClient2
HRESULT STDMETHODCALLTYPE IsOffloadCapable(
AUDIO_STREAM_CATEGORY Category,
BOOL *pbOffloadCapable) override;
HRESULT STDMETHODCALLTYPE SetClientProperties(
const AudioClientProperties *pProperties) override;
HRESULT STDMETHODCALLTYPE GetBufferSizeLimits(
const WAVEFORMATEX *pFormat,
BOOL bEventDriven,
REFERENCE_TIME *phnsMinBufferDuration,
REFERENCE_TIME *phnsMaxBufferDuration) override;
#pragma endregion
#pragma region IAudioClient3
HRESULT STDMETHODCALLTYPE GetSharedModeEnginePeriod(
const WAVEFORMATEX *pFormat,
UINT32 *pDefaultPeriodInFrames,
UINT32 *pFundamentalPeriodInFrames,
UINT32 *pMinPeriodInFrames,
UINT32 *pMaxPeriodInFrames) override;
HRESULT STDMETHODCALLTYPE GetCurrentSharedModeEnginePeriod(
WAVEFORMATEX **ppFormat,
UINT32 *pCurrentPeriodInFrames) override;
HRESULT STDMETHODCALLTYPE InitializeSharedAudioStream(
DWORD StreamFlags,
UINT32 PeriodInFrames,
const WAVEFORMATEX *pFormat,
LPCGUID AudioSessionGuid) override;
#pragma endregion
IAudioClient *const pReal;
IAudioClient3 *const pReal3;
AudioBackend *const backend;
bool exclusive_mode = false;
int frame_size = 0;
};
@@ -149,5 +149,12 @@ LowLatencyAudioClient *LowLatencyAudioClient::Create(IMMDevice *device) {
log_info("audio::lowlatency", "... max buffer size : {} samples ({} ms)", maxPeriod, 1000.0f * maxPeriod / pFormat->nSamplesPerSec);
log_info("audio::lowlatency", "... default buffer size : {} samples ({} ms)", defaultPeriod, 1000.0f * defaultPeriod / pFormat->nSamplesPerSec);
log_info("audio::lowlatency", "... Windows will use minimum buffer size (instead of default) for shared mode audio clients from now on");
if (minPeriod < defaultPeriod) {
log_info("audio::lowlatency", "minimum period is less than default period, you will see latency improvement");
} else {
log_warning("audio::lowlatency", "minimum period is not less than default period, you will see NO latency improvement");
log_warning("audio::lowlatency", "this an audio driver / hardware limitation");
}
return new LowLatencyAudioClient(audioClient);
}
+27
View File
@@ -0,0 +1,27 @@
#include "mme.h"
#include <mmeapi.h>
#include "util/detour.h"
#include "util/logging.h"
namespace hooks::audio::mme {
static decltype(mixerSetControlDetails) *mixerSetControlDetails_orig = nullptr;
MMRESULT
WINAPI
mixerSetControlDetails_hook(
HMIXEROBJ hmxobj, LPMIXERCONTROLDETAILS pmxcd, DWORD fdwDetails) {
log_misc("audio::mme", "mixerSetControlDetails_hook called; ignoring volume change");
return MMSYSERR_NOERROR;
}
void init(HINSTANCE module) {
mixerSetControlDetails_orig =
detour::iat_try("mixerSetControlDetails", mixerSetControlDetails_hook, module);
if (mixerSetControlDetails_orig != nullptr) {
log_misc("audio::mme", "mixerSetControlDetails hooked");
}
}
}
+7
View File
@@ -0,0 +1,7 @@
#pragma once
#include <windows.h>
namespace hooks::audio::mme {
void init(HINSTANCE module);
}
+1 -1
View File
@@ -35,7 +35,7 @@ std::string share_mode_str(AUDCLNT_SHAREMODE share_mode) {
ENUM_VARIANT(AUDCLNT_SHAREMODE_SHARED);
ENUM_VARIANT(AUDCLNT_SHAREMODE_EXCLUSIVE);
default:
return fmt::format("ShareMode(0x{:08x})", share_mode);
return fmt::format("ShareMode(0x{:08x})", static_cast<uint32_t>(share_mode));
}
}
+81 -1
View File
@@ -1,7 +1,10 @@
#include "avshook.h"
#include <cassert>
#include <mutex>
#include <map>
#include <optional>
#include <external/robin_hood.h>
#include "avs/core.h"
#include "avs/ea3.h"
@@ -20,8 +23,12 @@ static bool FAKE_FILE_OPEN = false;
static std::map<std::string, std::string> ROM_FILE_MAP;
static std::string *ROM_FILE_CONTENTS = nullptr;
static std::mutex lstat_cache_mutex;
static robin_hood::unordered_map<std::string, std::pair<int, struct avs::core::avs_stat>> lstat_cache;
namespace hooks::avs::config {
bool DISABLE_VFS_DRIVE_REDIRECTION = false;
bool DISABLE_CACHE = false;
bool LOG = false;
};
@@ -79,6 +86,74 @@ static bool is_spam_file(const char *file) {
return false;
}
static bool is_ldj_cacheable_file(const char *file) {
static const char *spam_prefixes[] = {
// iidx33: profile background customization, spams on every frame during card entry
"/data/graphic/entry_card",
// iidx32+: movie thumbnails, spams on every frame during song select
"/data/graphic/thumbnail/",
"/data/graphic/movie_thumbnail/",
};
for (auto &spam : spam_prefixes) {
if (string_begins_with(file, spam)) {
return true;
}
}
return false;
}
static int cached_avs_fs_lstat(const char *name, struct avs::core::avs_stat *st) {
assert(name != nullptr);
assert(st != nullptr);
const std::string filename(name);
int value = 0;
std::lock_guard<std::mutex> lock(lstat_cache_mutex);
// if this was already seen, return cached result
if (lstat_cache.contains(filename)) {
const auto &result = lstat_cache.at(filename);
value = result.first;
memcpy(st, &result.second, sizeof(*st));
WRAP_DEBUG_FMT(
"(cached) file={}, a={}, m={}, c={}, size={}, u={}",
name,
st->st_atime,
st->st_mtime,
st->st_ctime,
st->filesize,
st->unk1);
return value;
}
// if not, call the original avs function
// this returns 1 on success with valid values in st
// return 0 on failure, with filesize set to 0
// maybe this could be used for future optimization
value = avs::core::avs_fs_lstat(name, st);
WRAP_DEBUG_FMT(
"file={} a={}, m={}, c={}, size={}, u={}",
name,
st->st_atime,
st->st_mtime,
st->st_ctime,
st->filesize,
st->unk1);
// and cache it if there is room (prevent unconstrained growth)
if (lstat_cache.size() <= 4096) {
// c++ was a mistake
lstat_cache.emplace(
std::piecewise_construct,
std::forward_as_tuple(filename),
std::forward_as_tuple(value, *st));
}
// done
return value;
}
static int avs_fs_fstat(avs::core::avs_file_t fd, struct avs::core::avs_stat *st) {
if (is_fake_fd(fd) && ROM_FILE_CONTENTS) {
if (st) {
@@ -104,7 +179,12 @@ static int avs_fs_lstat(const char *name, struct avs::core::avs_stat *st) {
return 1;
}
auto value = avs::core::avs_fs_lstat(name, st);
if (!hooks::avs::config::DISABLE_CACHE && (st != nullptr) &&
avs::game::is_model("LDJ") && is_ldj_cacheable_file(name)) {
return cached_avs_fs_lstat(name, st);
}
const auto value = avs::core::avs_fs_lstat(name, st);
if (!is_spam_file(name)) {
WRAP_DEBUG_FMT("name: {}", name);
}
+1
View File
@@ -9,6 +9,7 @@
namespace hooks::avs {
namespace config {
extern bool DISABLE_VFS_DRIVE_REDIRECTION;
extern bool DISABLE_CACHE;
extern bool LOG;
};
+7
View File
@@ -3,6 +3,7 @@
#include <vector>
#include "avs/game.h"
#include "games/gitadora/gitadora.h"
#include "util/detour.h"
#include "util/utils.h"
@@ -399,6 +400,12 @@ static BOOL WINAPI ClearCommError_hook(HANDLE hFile, LPDWORD lpErrors, LPCOMSTAT
lpStat->cbInQue = custom_handle->bytes_available();
}
// gitadora arena model needs this, or else
// the game will keep spamming 0xAA
if (games::gitadora::is_arena_model() && lpErrors) {
*lpErrors = 0;
}
return TRUE;
}
+122 -43
View File
@@ -13,8 +13,10 @@
#include "avs/game.h"
#include "cfg/screen_resize.h"
#include "games/gitadora/gitadora.h"
#include "games/iidx/iidx.h"
#include "games/sdvx/sdvx.h"
#include "games/mfc/mfc.h"
#include "games/io.h"
#include "hooks/graphics/graphics.h"
#include "launcher/launcher.h"
@@ -40,6 +42,15 @@
#undef min
#endif
#define D3D9_BACKEND_DEBUG 0
#if D3D9_BACKEND_DEBUG
#define log_debug(module, format_str, ...) logger::push( \
LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY)
#else
#define log_debug(module, format_str, ...)
#endif
#define CHECK_RESULT(x) \
do { \
HRESULT __ret = (x); \
@@ -188,7 +199,7 @@ static std::string format2s(D3DFORMAT format) {
ENUM_VARIANT(D3DFMT_A2B10G10R10_XR_BIAS);
ENUM_VARIANT(D3DFMT_BINARYBUFFER);
default:
return fmt::to_string(format);
return fmt::to_string(static_cast<uint32_t>(format));
}
}
@@ -238,14 +249,17 @@ static void log_create_device_failure(HRESULT hresult) {
}
static bool is_dx9_on_12_enabled() {
bool result = false;
switch (GRAPHICS_9_ON_12_STATE) {
case DX9ON12_FORCE_OFF:
log_info("graphics::d3d9", "DirectX 9on12: forced OFF by user (-dx9on12)");
return false;
result = false;
break;
case DX9ON12_FORCE_ON:
log_info("graphics::d3d9", "DirectX 9on12: forced ON by user (-9on12 or -dx9on12)");
return true;
result = true;
break;
case DX9ON12_AUTO:
default:
@@ -253,14 +267,33 @@ static bool is_dx9_on_12_enabled() {
log_info(
"graphics::d3d9",
"DirectX 9on12: enabled automatically for current game (tip: use -dx9on12 to force on or off)");
return true;
result = true;
} else {
log_info(
"graphics::d3d9",
"DirectX 9on12: disabled by default, using DX9 (tip: use -dx9on12 to force on or off)");
return false;
result = false;
}
break;
}
if (GRAPHICS_9_ON_12_STATE == DX9ON12_FORCE_ON) {
if (avs::game::is_model("LDJ") && avs::game::is_ext(2023091500, MAXINT)) {
deferredlogs::defer_error_messages({
"dx9on12 was force enabled by user (-dx9on12)",
" IIDX31+ is known to be incompatible with DX 9on12, leading to blank screen or crashes",
" try again with -dx9on12 option set to default value"
});
} else {
deferredlogs::defer_error_messages({
"dx9on12 was force enabled by user (-dx9on12)",
" not very game is compatible with this, and can lead to crashes",
" try without force enabling this if you are seeing issues"
});
}
}
return result;
}
static IDirect3D9 *WINAPI Direct3DCreate9_hook(UINT SDKVersion) {
@@ -491,7 +524,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes(
if (!modified && games::iidx::is_tdj_fhd() && refresh < 110) {
if ((width == 1920 && height == 1080) ||
(width == 1280 && height == 720)){
log_misc(
log_debug(
"graphics::d3d9", "removing mode {}, {}x{} @ {}Hz (for TDJ FHD)",
Mode, width, height, refresh);
memset(pMode, 0, sizeof(*pMode));
@@ -504,7 +537,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes(
// - skip 75 and 90 Hz entries because LDJ game timing is messed up with it
// (TDJ should be fine as it assumes a fixed 60 Hz timing)
if (!modified && (width == 1360 || width == 1366 || refresh == 75 || refresh == 90)) {
log_misc(
log_debug(
"graphics::d3d9", "removing mode {}, {}x{} @ {}Hz (for LDJ/TDJ)",
Mode, width, height, refresh);
memset(pMode, 0, sizeof(*pMode));
@@ -512,7 +545,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes(
}
if (!modified && !games::iidx::TDJ_MODE && games::iidx::FORCE_720P && (height > 720)) {
log_misc(
log_debug(
"graphics::d3d9", "removing mode {}, {}x{} @ {}Hz (-iidxforce720p)",
Mode, width, height, refresh);
memset(pMode, 0, sizeof(*pMode));
@@ -673,7 +706,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
{
log_misc("graphics::d3d9", "IDirect3D9::CreateDevice hook hit ({}, {}, {}, {}, {}, {})",
Adapter,
DeviceType,
static_cast<uint32_t>(DeviceType),
fmt::ptr(hFocusWindow),
behavior2s(BehaviorFlags),
fmt::ptr(pPresentationParameters),
@@ -748,9 +781,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
params->BackBufferHeight,
format2s(params->BackBufferFormat),
params->BackBufferCount,
params->MultiSampleType,
static_cast<uint32_t>(params->MultiSampleType),
params->MultiSampleQuality,
params->SwapEffect,
static_cast<uint32_t>(params->SwapEffect),
params->Windowed,
params->EnableAutoDepthStencil,
format2s(params->AutoDepthStencilFormat),
@@ -780,6 +813,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
"either use -graphics-force-refresh option or change the desktop resolution beforehand.");
}
if (!GRAPHICS_WINDOWED && num_adapters >= 2 && GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
log_info("graphics::d3d9", "force sub refresh rate: {} => {} Hz (-graphics-force-refresh-sub option)",
pPresentationParameters[1].FullScreen_RefreshRateInHz,
GRAPHICS_FORCE_REFRESH_SUB.value());
pPresentationParameters[1].FullScreen_RefreshRateInHz = GRAPHICS_FORCE_REFRESH_SUB.value();
}
// force single adapter
if (GRAPHICS_FORCE_SINGLE_ADAPTER) {
log_info("graphics::d3d9", "disabling adapter group device with force single adapter mode");
@@ -862,7 +903,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
log_misc("graphics::d3d9", "IDirect3D9Ex::CreateDeviceEx hook hit ({}, {}, {}, {}, {}, {})",
Adapter,
DeviceType,
static_cast<uint32_t>(DeviceType),
fmt::ptr(hFocusWindow),
behavior2s(BehaviorFlags),
fmt::ptr(pPresentationParameters),
@@ -904,26 +945,37 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
for (size_t i = 0; i < num_adapters; i++) {
auto *params = &pPresentationParameters[i];
if (!GRAPHICS_WINDOWED && i == 0) {
GRAPHICS_FS_ORIGINAL_WIDTH = params->BackBufferWidth;
GRAPHICS_FS_ORIGINAL_HEIGHT = params->BackBufferHeight;
log_misc("graphics::d3d9", "original resolution: {}x{}", GRAPHICS_FS_ORIGINAL_WIDTH, GRAPHICS_FS_ORIGINAL_HEIGHT);
if (GRAPHICS_FS_CUSTOM_RESOLUTION.has_value()) {
if (!GRAPHICS_WINDOWED){
if (i == 0) {
GRAPHICS_FS_ORIGINAL_WIDTH = params->BackBufferWidth;
GRAPHICS_FS_ORIGINAL_HEIGHT = params->BackBufferHeight;
log_misc("graphics::d3d9", "original resolution: {}x{}", GRAPHICS_FS_ORIGINAL_WIDTH, GRAPHICS_FS_ORIGINAL_HEIGHT);
if (GRAPHICS_FS_CUSTOM_RESOLUTION.has_value()) {
log_misc(
"graphics::d3d9",
"use custom resolution {}x{} => {}x{}",
params->BackBufferWidth, params->BackBufferHeight,
GRAPHICS_FS_CUSTOM_RESOLUTION.value().first,
GRAPHICS_FS_CUSTOM_RESOLUTION.value().second);
params->BackBufferWidth = GRAPHICS_FS_CUSTOM_RESOLUTION.value().first;
params->BackBufferHeight = GRAPHICS_FS_CUSTOM_RESOLUTION.value().second;
} else if (GRAPHICS_FS_ORIENTATION_SWAP) {
log_misc(
"graphics::d3d9",
"swap full orientation {}x{} => {}x{}",
params->BackBufferWidth, params->BackBufferHeight,
params->BackBufferHeight, params->BackBufferWidth);
std::swap(params->BackBufferWidth, params->BackBufferHeight);
}
} else if (i == 1 && GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
log_misc(
"graphics::d3d9",
"use custom resolution {}x{} => {}x{}",
"use custom sub resolution {}x{} => {}x{}",
params->BackBufferWidth, params->BackBufferHeight,
GRAPHICS_FS_CUSTOM_RESOLUTION.value().first,
GRAPHICS_FS_CUSTOM_RESOLUTION.value().second);
params->BackBufferWidth = GRAPHICS_FS_CUSTOM_RESOLUTION.value().first;
params->BackBufferHeight = GRAPHICS_FS_CUSTOM_RESOLUTION.value().second;
} else if (GRAPHICS_FS_ORIENTATION_SWAP) {
log_misc(
"graphics::d3d9",
"swap full orientation {}x{} => {}x{}",
params->BackBufferWidth, params->BackBufferHeight,
params->BackBufferHeight, params->BackBufferWidth);
std::swap(params->BackBufferWidth, params->BackBufferHeight);
GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().first,
GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().second);
params->BackBufferWidth = GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().first;
params->BackBufferHeight = GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().second;
}
}
@@ -937,9 +989,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
params->BackBufferHeight,
format2s(params->BackBufferFormat),
params->BackBufferCount,
params->MultiSampleType,
static_cast<uint32_t>(params->MultiSampleType),
params->MultiSampleQuality,
params->SwapEffect,
static_cast<uint32_t>(params->SwapEffect),
params->Windowed,
params->EnableAutoDepthStencil,
format2s(params->AutoDepthStencilFormat),
@@ -951,12 +1003,17 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
for (size_t i = 0; i < num_adapters; i++) {
auto *fullscreen_display_mode = &pFullscreenDisplayMode[i];
if (!GRAPHICS_WINDOWED && i == 0) {
if (GRAPHICS_FS_CUSTOM_RESOLUTION.has_value()) {
fullscreen_display_mode->Width = GRAPHICS_FS_CUSTOM_RESOLUTION.value().first;
fullscreen_display_mode->Height = GRAPHICS_FS_CUSTOM_RESOLUTION.value().second;
} else if (GRAPHICS_FS_ORIENTATION_SWAP) {
std::swap(fullscreen_display_mode->Width, fullscreen_display_mode->Height);
if (!GRAPHICS_WINDOWED) {
if (i == 0) {
if (GRAPHICS_FS_CUSTOM_RESOLUTION.has_value()) {
fullscreen_display_mode->Width = GRAPHICS_FS_CUSTOM_RESOLUTION.value().first;
fullscreen_display_mode->Height = GRAPHICS_FS_CUSTOM_RESOLUTION.value().second;
} else if (GRAPHICS_FS_ORIENTATION_SWAP) {
std::swap(fullscreen_display_mode->Width, fullscreen_display_mode->Height);
}
} else if (i == 1 && GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
fullscreen_display_mode->Width = GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().first;
fullscreen_display_mode->Height = GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().second;
}
}
@@ -968,7 +1025,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
fullscreen_display_mode->Height,
fullscreen_display_mode->RefreshRate,
format2s(fullscreen_display_mode->Format),
fullscreen_display_mode->ScanLineOrdering);
static_cast<uint32_t>(fullscreen_display_mode->ScanLineOrdering));
}
}
@@ -999,6 +1056,17 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
}
}
if (!GRAPHICS_WINDOWED && num_adapters >= 2 && GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
log_info("graphics::d3d9", "force sub refresh rate: {} => {} Hz (-graphics-force-refresh-sub option)",
pPresentationParameters[1].FullScreen_RefreshRateInHz,
GRAPHICS_FORCE_REFRESH_SUB.value());
pPresentationParameters[1].FullScreen_RefreshRateInHz = GRAPHICS_FORCE_REFRESH_SUB.value();
if (pFullscreenDisplayMode) {
pFullscreenDisplayMode[1].RefreshRate = GRAPHICS_FORCE_REFRESH_SUB.value();
}
}
// force single adapter
if (GRAPHICS_FORCE_SINGLE_ADAPTER) {
log_info("graphics::d3d9", "disabling adapter group device with force single adapter mode");
@@ -1119,9 +1187,15 @@ IDirect3DSurface9 *graphics_d3d9_ldj_get_sub_screen() {
}
static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
// iidx/sdvx
int swapchain = 1;
if (games::gitadora::is_arena_model()) {
swapchain = 3;
}
if (!ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE && SUB_SWAP_CHAIN == nullptr) {
ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE = true;
HRESULT hr = wrapped_device->GetSwapChain(1, &SUB_SWAP_CHAIN);
HRESULT hr = wrapped_device->GetSwapChain(swapchain, &SUB_SWAP_CHAIN);
if (FAILED(hr)) {
log_warning(
"graphics::d3d9",
@@ -1330,14 +1404,19 @@ void graphics_d3d9_on_present(
}
// for IIDX TDJ / SDVX UFC, handle subscreen
const bool is_vm =
avs::game::is_model("KFC") &&
(avs::game::SPEC[0] == 'G' || avs::game::SPEC[0] == 'H');
const bool is_vm = games::sdvx::is_valkyrie_model();
const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE;
if (is_vm || is_tdj) {
const bool is_gfdm_arena =
games::gitadora::is_arena_model() && games::gitadora::ARENA_SINGLE_WINDOW;
if (is_vm || is_tdj || is_gfdm_arena) {
graphics_d3d9_ldj_on_present(wrapped_device);
}
const bool is_mfc = avs::game::is_model("KK9") && games::mfc::HG_MODE;
if (is_mfc) {
wintouchemu::update();
}
// check screenshot key
static bool trigger_last = false;
auto buttons = games::get_buttons_overlay(eamuse_get_game());
+65 -2
View File
@@ -4,6 +4,7 @@
#include <mutex>
#include "avs/game.h"
#include "games/gitadora/gitadora.h"
#include "hooks/graphics/graphics.h"
#include "overlay/overlay.h"
#include "util/flags_helper.h"
@@ -36,6 +37,14 @@ constexpr bool CUSTOM_RESET = false;
constexpr D3DFORMAT D3DFMT_DF24 = static_cast<D3DFORMAT>(MAKEFOURCC('D', 'F', '2', '4'));
auto format_as(D3DPOOL f) {
return fmt::underlying(f);
}
auto format_as(D3DFORMAT f) {
return fmt::underlying(f);
}
std::string usage2s(DWORD dwUsage) {
FLAGS_START(dwUsage);
FLAG(dwUsage, D3DUSAGE_RENDERTARGET);
@@ -241,8 +250,16 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
WRAP_VERBOSE;
HRESULT hr = pReal->CreateAdditionalSwapChain(pPresentationParameters, ppSwapChain);
bool create_swap_chain = false;
if (avs::game::is_model({"LDJ", "KFC"})) {
create_swap_chain = true;
} else if (games::gitadora::is_arena_model() &&
games::gitadora::ARENA_SINGLE_WINDOW &&
pPresentationParameters->BackBufferWidth == 800) {
create_swap_chain = true;
}
if (create_swap_chain) {
if (SUCCEEDED(hr) && !sub_swapchain) {
sub_swapchain = new WrappedIDirect3DSwapChain9(this, *ppSwapChain);
sub_swapchain->should_run_hooks = false;
@@ -287,7 +304,17 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
return D3D_OK;
}
bool swap = false;
if (iSwapChain == 1 && avs::game::is_model({"LDJ", "KFC"})) {
swap = true;
}
if (games::gitadora::is_arena_model() &&
games::gitadora::ARENA_SINGLE_WINDOW &&
iSwapChain == 3) {
swap = true;
}
if (swap) {
if (sub_swapchain) {
sub_swapchain->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(sub_swapchain);
@@ -733,6 +760,14 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
const int rectTop = param.BackBufferHeight;
const int w = param.BackBufferWidth;
const int h = param.BackBufferHeight;
// this code used to clear the surface using ColorFill on every call, but
// this turned out to be very expensive, leading to major frame drops in
// SDVX Live2D scenario
if (cfg::SCREENRESIZE->need_surface_clean) {
pReal->ColorFill(topSurface, nullptr, D3DCOLOR_XRGB(0, 0, 0));
cfg::SCREENRESIZE->need_surface_clean = false;
}
D3DLOCKED_RECT rect;
HRESULT hr;
@@ -750,6 +785,7 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
(LONG)(rectLeft + w),
(LONG)(rectTop + h),
};
if (GRAPHICS_FS_ORIENTATION_SWAP) {
originRect.left = (topSurface_width / 2) - (GRAPHICS_FS_ORIGINAL_WIDTH / 2);
originRect.top = (topSurface_height / 2) - (GRAPHICS_FS_ORIGINAL_HEIGHT / 2);
@@ -757,6 +793,21 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
originRect.bottom = originRect.top + GRAPHICS_FS_ORIGINAL_HEIGHT;
}
bool duplicate = false;
RECT originRect2 = originRect;
if (cfg::SCREENRESIZE->enable_screen_resize) {
auto& scene = cfg::SCREENRESIZE->scene_settings[cfg::SCREENRESIZE->screen_resize_current_scene];
if (scene.duplicate == cfg::ScreenDuplicateMode::CopyLeft) {
duplicate = true;
originRect2.right = originRect.left;
originRect2.left = (LONG)(originRect2.left - (originRect.right - originRect.left));
} else if (scene.duplicate == cfg::ScreenDuplicateMode::CopyRight) {
duplicate = true;
originRect2.left = originRect.right;
originRect2.right = (LONG)(originRect2.left + (originRect.right - originRect.left));
}
}
// log_misc(
// "graphics::d3d9", "originRect: {} {} {} {}",
// originRect.left, originRect.top, originRect.right, originRect.bottom);
@@ -768,9 +819,21 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
if (hr != D3D_OK) {
log_warning(
"graphics::d3d9",
"SurfaceHook - StretchRect backbuffer failed, rect: {} {} {} {}",
"SurfaceHook - StretchRect backbuffer failed for main image, rect: {} {} {} {}",
originRect.left, originRect.top, originRect.right, originRect.bottom);
}
if (duplicate) {
hr = pReal->StretchRect(
backbuffer, nullptr,
topSurface, &originRect2,
D3DTEXF_LINEAR);
if (hr != D3D_OK) {
log_warning(
"graphics::d3d9",
"SurfaceHook - StretchRect backbuffer failed for the duplicate, rect: {} {} {} {}",
originRect2.left, originRect2.top, originRect2.right, originRect2.bottom);
}
}
topSurface->UnlockRect();
// phase 2 - viewport calculation - do the actual zoom / offset math
+94 -7
View File
@@ -11,6 +11,7 @@
#include "cfg/icon.h"
#include "cfg/screen_resize.h"
#include "games/ddr/ddr.h"
#include "games/gitadora/gitadora.h"
#include "games/iidx/iidx.h"
#include "hooks/graphics/backends/d3d9/d3d9_backend.h"
#include "launcher/shutdown.h"
@@ -33,6 +34,7 @@ struct CaptureData {
HWND TDJ_SUBSCREEN_WINDOW = nullptr;
HWND SDVX_SUBSCREEN_WINDOW = nullptr;
HWND GFDM_SUBSCREEN_WINDOW = nullptr;
// icon
static HICON WINDOW_ICON = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(MAINICON));
@@ -60,6 +62,7 @@ graphics_orientation GRAPHICS_ADJUST_ORIENTATION = ORIENTATION_NORMAL;
bool GRAPHICS_WINDOWED = false;
std::vector<HWND> GRAPHICS_WINDOWS;
UINT GRAPHICS_FORCE_REFRESH = 0;
std::optional<uint32_t> GRAPHICS_FORCE_REFRESH_SUB;
std::optional<int> GRAPHICS_FORCE_VSYNC_BUFFER;
bool GRAPHICS_FORCE_SINGLE_ADAPTER = false;
bool GRAPHICS_PREVENT_SECONDARY_WINDOW = false;
@@ -68,6 +71,7 @@ bool GRAPHICS_9_ON_12_REQUESTED_BY_GAME = false;
bool SUBSCREEN_FORCE_REDRAW = false;
bool D3D9_DEVICE_HOOK_DISABLE = false;
std::optional<std::pair<uint32_t, uint32_t>> GRAPHICS_FS_CUSTOM_RESOLUTION;
std::optional<std::pair<uint32_t, uint32_t>> GRAPHICS_FS_CUSTOM_RESOLUTION_SUB;
bool GRAPHICS_FS_ORIENTATION_SWAP = false;
uint32_t GRAPHICS_FS_ORIGINAL_WIDTH = 0;
uint32_t GRAPHICS_FS_ORIGINAL_HEIGHT = 0;
@@ -94,6 +98,7 @@ static decltype(SetCursor) *SetCursor_orig = nullptr;
static decltype(SetWindowLongA) *SetWindowLongA_orig = nullptr;
static decltype(SetWindowLongW) *SetWindowLongW_orig = nullptr;
static decltype(SetWindowPos) *SetWindowPos_orig = nullptr;
static decltype(ShowWindow) *ShowWindow_orig = nullptr;
static void reset_window_hook(HWND hWnd) {
overlay::destroy(hWnd);
@@ -296,6 +301,30 @@ static BOOL WINAPI ClipCursor_hook(const RECT *lpRect) {
return ClipCursor_orig(lpRect);
}
static void fix_up_rb_auto_rotate_dimensions(DWORD &w, DWORD &h) {
// reflec beat
// for whatever reason, rb creates the first window it weird dimensions (774x1389 for reflesia, for example)
// so doing ChangeDisplaySettings as-is ends up with DISP_CHANGE_BADMODE
// while a second window (with title D3DProxyWindow) does get created with correct dimensions, it's
// too late to rotate the window then; as a workaround force the display change now with patched up dimensions
bool changed = false;
if (w == 774) {
// LBR, MBR reflesia
w = 768;
h = 1360;
changed = true;
} else if (w == 1086) {
// MBR (before reflesia)
w = 1080;
h = 1920;
changed = true;
}
if (changed) {
log_misc("graphics", "fix auto-rotate dimensions for reflec beat: w={}, h={}", w, h);
}
}
static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPCSTR lpWindowName,
DWORD dwStyle, int x, int y, int nWidth, int nHeight,
HWND hWndParent, HMENU hMenu, HINSTANCE hInstance,
@@ -320,10 +349,19 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
// set display orientation and/or refresh rate
// only set orientation when the target window is portrait
// (avoid doing this for SDVX subscreen which is in landscape, for example)
const auto adjust_orientation =
auto adjust_orientation =
(GRAPHICS_ADJUST_ORIENTATION == ORIENTATION_CW ||
GRAPHICS_ADJUST_ORIENTATION == ORIENTATION_CCW);
// reflec beat
if (avs::game::is_model("KBR")) {
// KBR (rb1) launches landscape and draws rotated by itself, don't do any rotation
adjust_orientation = false;
} else if (avs::game::is_model({"LBR", "MBR"}) && window_name == "D3DProxyWindow") {
// do not auto-rotate for second window (D3DProxyWindow)
adjust_orientation = false;
}
if ((nHeight > nWidth && adjust_orientation) || GRAPHICS_FORCE_REFRESH > 0) {
DEVMODE mode {};
@@ -333,11 +371,16 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
DWORD orientation = GRAPHICS_ADJUST_ORIENTATION == ORIENTATION_CW ? DMDO_90 : DMDO_270;
log_misc(
"graphics",
"auto-rotate: call ChangeDisplaySettings and rotate display to DMDO_xx mode {}",
orientation);
"auto-rotate: call ChangeDisplaySettings and rotate display to DMDO_xx mode {}, w={}, h={}",
orientation, nWidth, nHeight);
mode.dmPelsWidth = nWidth;
mode.dmPelsHeight = nHeight;
mode.dmDisplayOrientation = orientation;
// reflec beat
if (avs::game::is_model({"LBR", "MBR"})) {
fix_up_rb_auto_rotate_dimensions(mode.dmPelsWidth, mode.dmPelsHeight);
}
}
if (GRAPHICS_FORCE_REFRESH > 0) {
@@ -352,7 +395,7 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
const auto disp_res = ChangeDisplaySettings(&mode, CDS_FULLSCREEN);
if (disp_res != DISP_CHANGE_SUCCESSFUL) {
log_warning("graphics", "failed to change display settings: {}", disp_res);
log_warning("graphics", "ChangeDisplaySettings failed: {}", disp_res);
}
} else {
log_warning("graphics", "failed to get display settings");
@@ -369,11 +412,16 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
bool is_tdj_sub_window = avs::game::is_model("LDJ") && window_name.ends_with(" sub");
bool is_sdvx_sub_window = avs::game::is_model("KFC") && window_name.ends_with(" Sub Screen");
bool is_gfdm_sub_window = games::gitadora::is_arena_model() && window_name.ends_with("SMALL");
// TDJ windowed mode with subscreen: hide maximize button
if ((is_tdj_sub_window && GRAPHICS_IIDX_WSUB) || is_sdvx_sub_window) {
// hide maximize button (prevent misaligned touches)
if ((is_tdj_sub_window && GRAPHICS_IIDX_WSUB) || is_sdvx_sub_window || is_gfdm_sub_window) {
dwStyle &= ~(WS_MAXIMIZEBOX);
}
// mouse clicks become misaligned when resized
if (is_gfdm_sub_window) {
dwStyle &= ~(WS_SIZEBOX);
}
if ((is_tdj_sub_window || is_sdvx_sub_window) && GRAPHICS_WSUB_BORDERLESS) {
dwStyle &= ~(WS_OVERLAPPEDWINDOW);
}
@@ -417,7 +465,19 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
graphics_hook_subscreen_window(SDVX_SUBSCREEN_WINDOW);
}
// only hook touch window if multiple windows are allowed
if (is_gfdm_sub_window && !games::gitadora::ARENA_SINGLE_WINDOW) {
GFDM_SUBSCREEN_WINDOW = result;
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
}
disable_touch_indicators(result);
log_misc(
"graphics",
"CreateWindowExA returned {}, {}",
fmt::ptr(result),
lpWindowName ? lpWindowName : "(null)");
return result;
}
@@ -449,7 +509,8 @@ static HWND WINAPI CreateWindowExW_hook(DWORD dwExStyle, LPCWSTR lpClassName, LP
}
// windowed mode adjustments
if (GRAPHICS_WINDOWED) {
// check the width to filter out invisible system-created windows
if (GRAPHICS_WINDOWED && nWidth > 0) {
// change window style
dwExStyle = 0;
@@ -484,6 +545,12 @@ static HWND WINAPI CreateWindowExW_hook(DWORD dwExStyle, LPCWSTR lpClassName, LP
hWndParent, hMenu, hInstance, lpParam);
GRAPHICS_WINDOWS.push_back(result);
log_misc(
"graphics",
"CreateWindowExW returned {}, {}",
fmt::ptr(result),
lpWindowName ? ws2s(lpWindowName) : "(null)");
disable_touch_indicators(result);
return result;
}
@@ -648,10 +715,29 @@ static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
return TRUE;
}
// prevent gitadora arena model from shifting windows around if the user has preferences
if (GRAPHICS_WINDOWED && games::gitadora::is_arena_model() &&
GRAPHICS_HOOKED_WINDOW.has_value() && hWnd == GRAPHICS_HOOKED_WINDOW.value() &&
cfg::SCREENRESIZE->enable_window_resize) {
return TRUE;
}
// call original
return SetWindowPos_orig(hWnd, hWndInsertAfter, X, Y, cx, cy, uFlags);
}
static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
if (games::gitadora::is_arena_model() &&
games::gitadora::ARENA_SINGLE_WINDOW &&
hWnd != GRAPHICS_HOOKED_WINDOW) {
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
return true;
}
// call original
return ShowWindow_orig(hWnd, nCmdShow);
}
static ATOM WINAPI RegisterClassA_hook(const WNDCLASSA *lpWndClass) {
// check for null
@@ -794,6 +880,7 @@ void graphics_init() {
SetWindowLongA_orig = detour::iat_try("SetWindowLongA", SetWindowLongA_hook);
SetWindowLongW_orig = detour::iat_try("SetWindowLongW", SetWindowLongW_hook);
SetWindowPos_orig = detour::iat_try("SetWindowPos", SetWindowPos_hook);
ShowWindow_orig = detour::iat_try("ShowWindow", ShowWindow_hook);
detour::iat_try("MessageBoxA", MessageBoxA_hook);
detour::iat_try("MessageBoxExA", MessageBoxExA_hook);
+8 -1
View File
@@ -32,10 +32,12 @@ extern bool GRAPHICS_WINDOWED;
extern graphics_orientation GRAPHICS_ADJUST_ORIENTATION;
extern std::vector<HWND> GRAPHICS_WINDOWS;
extern UINT GRAPHICS_FORCE_REFRESH;
extern std::optional<uint32_t> GRAPHICS_FORCE_REFRESH_SUB;
extern std::optional<int> GRAPHICS_FORCE_VSYNC_BUFFER;
extern bool GRAPHICS_FORCE_SINGLE_ADAPTER;
extern bool GRAPHICS_PREVENT_SECONDARY_WINDOW;
extern std::optional<std::pair<uint32_t, uint32_t>> GRAPHICS_FS_CUSTOM_RESOLUTION;
extern std::optional<std::pair<uint32_t, uint32_t>> GRAPHICS_FS_CUSTOM_RESOLUTION_SUB;
extern bool GRAPHICS_FS_ORIENTATION_SWAP;
extern uint32_t GRAPHICS_FS_ORIGINAL_WIDTH;
extern uint32_t GRAPHICS_FS_ORIGINAL_HEIGHT;
@@ -48,6 +50,7 @@ extern std::optional<std::pair<uint32_t, uint32_t>> GRAPHICS_WINDOW_SIZE;
extern std::optional<std::string> GRAPHICS_WINDOW_POS;
extern bool GRAPHICS_WINDOW_ALWAYS_ON_TOP;
extern bool GRAPHICS_WINDOW_BACKBUFFER_SCALE;
extern std::optional<HWND> GRAPHICS_HOOKED_WINDOW;
extern bool GRAPHICS_IIDX_WSUB;
extern std::optional<std::string> GRAPHICS_WSUB_SIZE;
@@ -60,6 +63,7 @@ extern bool GRAPHICS_WSUB_BORDERLESS;
extern bool GRAPHICS_WSUB_ALWAYS_ON_TOP;
extern HWND TDJ_SUBSCREEN_WINDOW;
extern HWND SDVX_SUBSCREEN_WINDOW;
extern HWND GFDM_SUBSCREEN_WINDOW;
extern bool SUBSCREEN_FORCE_REDRAW;
@@ -98,5 +102,8 @@ void graphics_capture_initial_window(HWND hWnd);
void graphics_update_window_style(HWND hWnd);
void graphics_update_z_order(HWND hWnd, bool always_on_top);
void graphics_move_resize_window(HWND hWnd);
bool graphics_window_change_crashes_game();
bool graphics_window_options_breaks_game();
bool graphics_window_decoration_change_crashes_game();
bool graphics_window_resize_breaks_game();
bool graphics_window_move_and_resize_breaks_game();
void graphics_load_windowed_subscreen_parameters();
+108 -16
View File
@@ -1,6 +1,7 @@
#include "graphics.h"
#include "avs/game.h"
#include "games/gitadora/gitadora.h"
#include "cfg/screen_resize.h"
#include "overlay/overlay.h"
#include "util/logging.h"
@@ -23,6 +24,7 @@ std::optional<std::pair<uint32_t, uint32_t>> GRAPHICS_WINDOW_SIZE;
std::optional<std::string> GRAPHICS_WINDOW_POS;
bool GRAPHICS_WINDOW_ALWAYS_ON_TOP = false;
bool GRAPHICS_WINDOW_BACKBUFFER_SCALE = false;
std::optional<HWND> GRAPHICS_HOOKED_WINDOW;
// IIDX Windowed Subscreen - starts out as false, enabled by IIDX module on pre-attach as needed
bool GRAPHICS_IIDX_WSUB = false;
@@ -55,15 +57,22 @@ void graphics_capture_initial_window(HWND hWnd) {
graphics_load_windowed_parameters();
cfg::SCREENRESIZE->init_window_style = GetWindowLong(hWnd, GWL_STYLE);
GRAPHICS_HOOKED_WINDOW = hWnd;
log_debug("graphics-windowed", "graphics_capture_initial_window called");
cfg::SCREENRESIZE->init_window_style = GetWindowLong(hWnd, GWL_STYLE);
cfg::SCREENRESIZE->init_window_style_ex = GetWindowLong(hWnd, GWL_EXSTYLE);
log_debug(
"graphics-windowed",
"graphics_capture_initial_window called, hWnd={}",
fmt::ptr(hWnd));
RECT rect;
if (!GetClientRect(hWnd, &rect)) {
log_warning(
"graphics",
"graphics_capture_initial_window - GetClientRect failed, GLE: {}",
"graphics-windowed",
"[{}] graphics_capture_initial_window - GetClientRect failed, GLE: {}",
fmt::ptr(hWnd),
GetLastError());
return;
}
@@ -75,12 +84,22 @@ void graphics_capture_initial_window(HWND hWnd) {
cfg::SCREENRESIZE->init_client_aspect_ratio = (float)client_w / (float)client_h;
log_debug(
"graphics-windowed",
"graphics_capture_initial_window initial window size {}x{}, ratio {}",
"[{}] graphics_capture_initial_window initial window size {}x{}, ratio {}",
fmt::ptr(hWnd),
client_w, client_h, cfg::SCREENRESIZE->init_client_aspect_ratio);
// ensure frame size is captured
graphics_wm_style_changed(hWnd, false);
// if the game is not compatible, ignore user-supplied values and keep the current size
if (graphics_window_resize_breaks_game()) {
cfg::SCREENRESIZE->client_width = client_w;
cfg::SCREENRESIZE->client_height = client_h;
}
if (graphics_window_move_and_resize_breaks_game()) {
cfg::SCREENRESIZE->enable_window_resize = false;
}
// if there was no user-supplied dimension, seed it with the current size
// so that the next resize operation will work
if (cfg::SCREENRESIZE->client_width == 0) {
@@ -94,7 +113,8 @@ void graphics_capture_initial_window(HWND hWnd) {
// resize must be done before applying the border
if (cfg::SCREENRESIZE->enable_window_resize) {
log_info(
"graphics-windowed", "change window rect - window offset: {}x{}, client size: {}x{}",
"graphics-windowed", "[{}] change window rect - window offset: {}x{}, client size: {}x{}",
fmt::ptr(hWnd),
cfg::SCREENRESIZE->window_offset_x,
cfg::SCREENRESIZE->window_offset_y,
cfg::SCREENRESIZE->client_width,
@@ -104,12 +124,13 @@ void graphics_capture_initial_window(HWND hWnd) {
// ddr hangs when window frame doesn't have overlapped
if (cfg::SCREENRESIZE->window_decoration != cfg::WindowDecorationMode::Default) {
log_info(
"graphics-windowed", "change window style - decoration: {}",
"graphics-windowed", "[{}] change window style - decoration: {}",
fmt::ptr(hWnd),
cfg::SCREENRESIZE->window_decoration);
graphics_update_window_style(hWnd);
}
if (cfg::SCREENRESIZE->window_always_on_top) {
log_info("graphics-windowed", "change window z-order - always on top");
log_info("graphics-windowed", "[{}] change window z-order - always on top", fmt::ptr(hWnd));
graphics_update_z_order(hWnd, true);
}
@@ -152,7 +173,7 @@ void graphics_load_windowed_parameters() {
cfg::SCREENRESIZE->window_offset_x = result.first;
cfg::SCREENRESIZE->window_offset_y = result.second;
} else {
log_warning("graphics-windowed", "failed to parse -windowpos");
log_fatal("graphics-windowed", "failed to parse -windowpos");
}
}
@@ -181,7 +202,7 @@ void graphics_load_windowed_subscreen_parameters() {
GRAPHICS_WSUB_WIDTH = result.first;
GRAPHICS_WSUB_HEIGHT = result.second;
} else {
log_warning("graphics-windowed", "failed to parse -wsubsize");
log_fatal("graphics-windowed", "failed to parse -iidxwsubsize / -sdvxwsubsize");
}
}
@@ -195,7 +216,7 @@ void graphics_load_windowed_subscreen_parameters() {
GRAPHICS_WSUB_X = result.first;
GRAPHICS_WSUB_Y = result.second;
} else {
log_warning("graphics-windowed", "failed to parse -wsubpos");
log_fatal("graphics-windowed", "failed to parse -iidxwsubpos / -sdvxwsubpos");
}
}
}
@@ -368,20 +389,29 @@ void graphics_update_window_style(HWND hWnd) {
if (!GRAPHICS_WINDOWED) {
return;
}
if (graphics_window_change_crashes_game()) {
if (graphics_window_decoration_change_crashes_game()) {
return;
}
log_debug("graphics-windowed", "graphics_update_window_style called");
log_debug(
"graphics-windowed",
"graphics_update_window_style called for hWnd={}",
fmt::ptr(hWnd));
// update frame style
auto style = cfg::SCREENRESIZE->init_window_style;
auto style_ex = cfg::SCREENRESIZE->init_window_style_ex;
switch (cfg::SCREENRESIZE->window_decoration) {
case cfg::WindowDecorationMode::Borderless:
style &= ~WS_OVERLAPPEDWINDOW;
style_ex &= ~WS_EX_CLIENTEDGE;
style_ex &= ~WS_EX_WINDOWEDGE;
break;
case cfg::WindowDecorationMode::ResizableFrame:
style |= WS_OVERLAPPEDWINDOW;
if (graphics_window_resize_breaks_game()) {
style &= ~WS_SIZEBOX;
}
break;
case cfg::WindowDecorationMode::Default:
default:
@@ -393,11 +423,26 @@ void graphics_update_window_style(HWND hWnd) {
"graphics_update_window_style - calling SetWindowLong with Mode {}, style 0x{:x}",
static_cast<int>(cfg::SCREENRESIZE->window_decoration),
style);
SetWindowLong(hWnd, GWL_STYLE, style);
const auto ret = SetWindowLong(hWnd, GWL_STYLE, style);
(void)ret;
log_debug(
"graphics-windowed",
"graphics_update_window_style - SetWindowLong(GWL_STYLE) returned {:x}, GLE={}",
ret,
GetLastError());
const auto ret2 = SetWindowLong(hWnd, GWL_EXSTYLE, style_ex);
(void)ret2;
log_debug(
"graphics-windowed",
"graphics_update_window_style SetWindowLong(GWL_EXSTYLE) returned {:x}, GLE={}",
ret2,
GetLastError());
// SetWindowPos must be called after SetWindowLong if the frame style changed
// this will be done in WM_STYLECHANGED handler
log_debug("graphics-windowed", "graphics_update_window_style returned");
}
void graphics_update_z_order(HWND hWnd, bool always_on_top) {
@@ -454,12 +499,19 @@ void graphics_move_resize_window(HWND hWnd) {
log_debug("graphics-windowed", "graphics_move_resize_window returned");
}
bool graphics_window_change_crashes_game() {
bool graphics_window_decoration_change_crashes_game() {
static std::once_flag flag;
static bool result = false;
std::call_once(flag, []() {
// ddr crashes when frame style changes
result = avs::game::is_model("MDX");
// changing window decoration also changes dimensions and position
// so this must be prevented
if (graphics_window_move_and_resize_breaks_game()) {
result = true;
}
if (result) {
log_warning(
"graphics-windowed",
@@ -468,3 +520,43 @@ bool graphics_window_change_crashes_game() {
});
return result;
}
bool graphics_window_move_and_resize_breaks_game() {
static std::once_flag flag;
static bool result = false;
std::call_once(flag, []() {
// games like reflec beat in use spice touch overlay for windowed mode
// and mouse input breaks if window is moved as the overlay does not
// currently support window moves/resizes
result = avs::game::is_model({"KBR", "LBR", "MBR"});
if (result) {
log_warning(
"graphics-windowed",
"ignoring changes to window properties due to incompatibility with this game");
}
});
return result;
}
bool graphics_window_resize_breaks_game() {
// for whatever reason, gitadora games on dx/sd/sd2 cabs behave poorly when window is resized
// (eihter at launch or while it's running)
// usually results in: soft lock during scene transtions or really long transitions,
// backgrounds of menus not rendering, etc.
bool is_gitadora = avs::game::is_model({ "J32", "J33", "K32", "K33", "L32", "L33", "M32" });
bool is_arena = games::gitadora::is_arena_model();
bool result = graphics_window_move_and_resize_breaks_game() || (is_gitadora && !is_arena);
static std::once_flag flag;
std::call_once(flag, [&result]() {
if (result) {
log_warning(
"graphics-windowed",
"ignoring changes to window size due to incompatibility with this game");
}
});
return result;
}
+137 -21
View File
@@ -9,6 +9,7 @@
#include "util/detour.h"
#include "util/libutils.h"
#include "util/logging.h"
#include "util/utils.h"
#include "nvenc_hook.h"
@@ -16,17 +17,54 @@
typedef NVENCSTATUS(NVENCAPI *NvEncodeAPICreateInstance_Type)(NV_ENCODE_API_FUNCTION_LIST*);
static NvEncodeAPICreateInstance_Type NvEncodeAPICreateInstance_orig = nullptr;
static PNVENCOPENENCODESESSIONEX nvEncOpenEncodeSessionEx_orig = nullptr;
static PNVENCINITIALIZEENCODER nvEncInitializeEncoder_orig = nullptr;
static PNVENCGETENCODEPRESETCONFIG nvEncGetEncodePresetConfig_orig = nullptr;
static PNVENCGETENCODEPRESETCONFIGEX nvEncGetEncodePresetConfigEx_orig = nullptr;
static BOOL initialized = false;
static BOOL new_preset_guids = false;
namespace nvenc_hook {
bool FORCE_DISABLE;
std::optional<std::string> VIDEO_CQP_STRING_OVERRIDE;
std::optional<NV_ENC_QP> VIDEO_CQP_OVERRIDE;
void parse_qcp_params();
void dump_init_params(NV_ENC_INITIALIZE_PARAMS* encode) {
log_misc("nvenc_hook", "NV_ENC_INITIALIZE_PARAMS:");
log_misc("nvenc_hook", " version: {:x}", encode->version);
log_misc("nvenc_hook", " encodeGUID: {}", guid2s(encode->encodeGUID));
log_misc("nvenc_hook", " presetGUID: {}", guid2s(encode->presetGUID));
log_misc("nvenc_hook", " encodeWidth: {}", encode->encodeWidth);
log_misc("nvenc_hook", " encodeHeight: {}", encode->encodeHeight);
log_misc("nvenc_hook", " darWidth: {}", encode->darWidth);
log_misc("nvenc_hook", " darHeight: {}", encode->darHeight);
log_misc("nvenc_hook", " frameRateNum: {}", encode->frameRateNum);
log_misc("nvenc_hook", " frameRateDen: {}", encode->frameRateDen);
log_misc("nvenc_hook", " enableEncodeAsync: {}", encode->enableEncodeAsync);
log_misc("nvenc_hook", " enablePTD: {}", encode->enablePTD);
log_misc("nvenc_hook", " maxEncodeWidth: {}", encode->maxEncodeWidth);
log_misc("nvenc_hook", " maxEncodeHeight: {}", encode->maxEncodeHeight);
log_misc("nvenc_hook", " tuningInfo: {}", static_cast<uint32_t>(encode->tuningInfo));
log_misc("nvenc_hook", " bufferFormat: {}", static_cast<uint32_t>(encode->bufferFormat));
log_misc("nvenc_hook", " encodeConfig.version: {:x}", encode->encodeConfig->version);
log_misc("nvenc_hook", " encodeConfig.profileGUID: {}", guid2s(encode->encodeConfig->profileGUID));
log_misc("nvenc_hook", " encodeConfig.gopLength: {}", encode->encodeConfig->gopLength);
log_misc("nvenc_hook", " encodeConfig.frameIntervalP: {}", encode->encodeConfig->frameIntervalP);
log_misc("nvenc_hook", " encodeConfig.monoChromeEncoding: {}", encode->encodeConfig->monoChromeEncoding);
const auto rc = &encode->encodeConfig->rcParams;
const auto h264 = &encode->encodeConfig->encodeCodecConfig.h264Config;
log_misc("nvenc_hook", " encodeConfig.encodeCodecConfig.h264Config.sliceMode: {}", h264->sliceMode);
log_misc("nvenc_hook", " encodeConfig.encodeCodecConfig.h264Config.sliceModeData: {}", h264->sliceModeData);
log_misc("nvenc_hook", " encodeConfig.rcParams.version: {:x}", rc->version);
log_misc("nvenc_hook", " encodeConfig.rcParams.rateControlMode: {}", static_cast<uint32_t>(rc->rateControlMode));
}
NVENCSTATUS NVENCAPI nvEncOpenEncodeSessionEx_hook(
NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS *openSessionExParams,
void **encoder
@@ -51,41 +89,113 @@ namespace nvenc_hook {
try {
// check input params
if (createEncodeParams == nullptr || createEncodeParams->encodeConfig == nullptr) {
log_warning("nvenc_hook", "nvEncInitializeEncoder called with invalid params");
goto done;
}
const auto rc = &createEncodeParams->encodeConfig->rcParams;
if (rc->rateControlMode != NV_ENC_PARAMS_RC_CONSTQP) {
log_misc("nvenc_hook", "nvEncInitializeEncoder hook hit with expected params");
dump_init_params(createEncodeParams);
if (new_preset_guids) {
log_misc("nvenc_hook", "swapping out encoder preset for newer NVENC SDK");
memcpy(&createEncodeParams->presetGUID, &NV_ENC_PRESET_P4_GUID, sizeof(GUID));
createEncodeParams->tuningInfo = NV_ENC_TUNING_INFO_HIGH_QUALITY;
}
// cqp p/b/i override
if (rc->rateControlMode == NV_ENC_PARAMS_RC_CONSTQP) {
// print out most relevant video quality settings
// note: NvEncoder.cpp sample uses {28, 31, 25} (and that's what some hex edits modify)
// but bm2dx later adds 8 to each value, before calling this routine
log_misc(
"nvenc_hook", "nvEncInitializeEncoder: original constQP p={}, b={}, i={}",
rc->constQP.qpInterP, rc->constQP.qpInterB, rc->constQP.qpIntra);
// video quality override
if (VIDEO_CQP_OVERRIDE.has_value()) {
rc->constQP = VIDEO_CQP_OVERRIDE.value();
log_misc(
"nvenc_hook", "nvEncInitializeEncoder: user overriden constQP p={}, b={}, i={}",
rc->constQP.qpInterP, rc->constQP.qpInterB, rc->constQP.qpIntra);
}
} else {
log_warning(
"nvenc_hook",
"nvEncInitializeEncoder: unexpected rateControlMode, expected: NV_ENC_PARAMS_RC_CONSTQP, actual: {}",
rc->rateControlMode);
goto done;
}
log_misc("nvenc_hook", "nvEncInitializeEncoder hook hit with expected params");
// print out most relevant video quality settings
// note: NvEncoder.cpp sample uses {28, 31, 25} (and that's what some hex edits modify)
// but bm2dx later adds 8 to each value, before calling this routine
log_misc(
"nvenc_hook", "nvEncInitializeEncoder: original constQP p={}, b={}, i={}",
rc->constQP.qpInterP, rc->constQP.qpInterB, rc->constQP.qpIntra);
// video quality override
if (VIDEO_CQP_OVERRIDE.has_value()) {
rc->constQP = VIDEO_CQP_OVERRIDE.value();
log_misc(
"nvenc_hook", "nvEncInitializeEncoder: user overriden constQP p={}, b={}, i={}",
rc->constQP.qpInterP, rc->constQP.qpInterB, rc->constQP.qpIntra);
static_cast<uint32_t>(rc->rateControlMode));
}
} catch (const std::exception &ex) {}
done:
return nvEncInitializeEncoder_orig(encoder, createEncodeParams);
const auto status = nvEncInitializeEncoder_orig(encoder, createEncodeParams);
log_misc(
"nvenc_hook",
"nvEncInitializeEncoder returned 0x{:x}",
static_cast<uint32_t>(status));
return status;
}
NVENCSTATUS NVENCAPI nvEncGetEncodePresetConfig_hook (
void* encoder, GUID encodeGUID, GUID presetGUID, NV_ENC_PRESET_CONFIG* presetConfig) {
// IIDX32 calls this with
// presetGUID = {34DBA71D-A77B-4B8F-9C3E-B6D5DA24C012} (NV_ENC_PRESET_HQ_GUID) (for h264)
// this preset is deprecated according to NVIDIA
const auto status = nvEncGetEncodePresetConfig_orig(
encoder, encodeGUID, presetGUID, presetConfig);
log_misc(
"nvenc_hook",
"NvEncGetEncodePresetConfig called with encodeGUID = {}, presetGUID = {}. and returned 0x{:x}",
guid2s(encodeGUID),
guid2s(presetGUID),
static_cast<uint32_t>(status));
if (status == NV_ENC_SUCCESS) {
return status;
}
// in NVIDIA driver 591.44 released in December 2025,
// NvEncGetEncodePresetConfig started to fail with NV_ENC_ERR_UNSUPPORTED_PARAM and
// eventually cause a crash
//
// IIDX32 calls this with
// presetGUID = {34DBA71D-A77B-4B8F-9C3E-B6D5DA24C012} (NV_ENC_PRESET_HQ_GUID) (for h264)
// this preset is deprecated according to NVIDIA
// https://docs.nvidia.com/video-technologies/video-codec-sdk/13.0/deprecation-notices/index.html
//
// references:
// https://github.com/NVIDIA/video-sdk-samples/tree/aa3544dcea2fe63122e4feb83bf805ea40e58dbe/Samples/NvCodec/NvEncoder
// https://forums.developer.nvidia.com/t/drivers-591-44-broke-nvenc-getencodepresetconfig-no-longer-works/353613
// https://docs.nvidia.com/video-technologies/video-codec-sdk/11.1/nvenc-preset-migration-guide/index.html
new_preset_guids = true;
const auto status_ex =
nvEncGetEncodePresetConfigEx_orig(
encoder,
encodeGUID,
NV_ENC_PRESET_P4_GUID,
NV_ENC_TUNING_INFO_HIGH_QUALITY,
presetConfig);
log_misc(
"nvenc_hook",
"called NvEncGetEncodePresetConfigEx instead; returned 0x{:x}",
static_cast<uint32_t>(status_ex));
return status_ex;
}
NVENCSTATUS NVENCAPI NvEncodeAPICreateInstance_hook(NV_ENCODE_API_FUNCTION_LIST *pFunctionList) {
if (FORCE_DISABLE) {
return NV_ENC_ERR_NO_ENCODE_DEVICE;
}
// log_misc("nvenc_hook", "NvEncodeAPICreateInstance hook hit");
auto status = NvEncodeAPICreateInstance_orig(pFunctionList);
@@ -101,6 +211,11 @@ namespace nvenc_hook {
pFunctionList->nvEncInitializeEncoder,
nvEncInitializeEncoder_hook,
&nvEncInitializeEncoder_orig);
detour::trampoline_try(
pFunctionList->nvEncGetEncodePresetConfig,
nvEncGetEncodePresetConfig_hook,
&nvEncGetEncodePresetConfig_orig);
nvEncGetEncodePresetConfigEx_orig = pFunctionList->nvEncGetEncodePresetConfigEx;
log_misc("nvenc_hook", "NvEncodeAPICreateInstance_hook called and functions hooked");
initialized = true;
}
@@ -123,6 +238,7 @@ namespace nvenc_hook {
} else {
log_warning("nvenc_hook", "failed to hook NvEncodeAPICreateInstance");
}
parse_qcp_params();
}
+5
View File
@@ -1,7 +1,12 @@
#pragma once
#ifdef SPICE64
namespace nvenc_hook {
extern bool FORCE_DISABLE;
extern std::optional<std::string> VIDEO_CQP_STRING_OVERRIDE;
void initialize();
}
#endif
+74 -2
View File
@@ -8,6 +8,9 @@
#include <ntstatus.h>
#include "avs/game.h"
#include "games/iidx/iidx.h"
#include "games/gitadora/gitadora.h"
#include "games/sdvx/sdvx.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/utils.h"
@@ -22,6 +25,8 @@ static decltype(GetLocaleInfoEx) *GetLocaleInfoEx_orig = nullptr;
#ifdef SPICE64
static decltype(GetSystemDefaultLCID) *GetSystemDefaultLCID_orig = nullptr;
static decltype(IsDBCSLeadByte) *IsDBCSLeadByte_orig = nullptr;
static decltype(WideCharToMultiByte) *WideCharToMultiByte_orig = nullptr;
#endif
static NTSTATUS NTAPI RtlMultiByteToUnicodeN_hook(
@@ -162,6 +167,52 @@ static int WINAPI GetLocaleInfoEx_hook (
return GetLocaleInfoEx_orig(lpLocaleName, LCType, lpLCData, cchData);
}
#ifdef SPICE64
static BOOL WINAPI IsDBCSLeadByte_hook (
BYTE TestChar
)
{
return IsDBCSLeadByteEx(CODEPAGE_SHIFT_JIS, TestChar);
}
static
int
WINAPI
WideCharToMultiByte_hook(
UINT CodePage,
DWORD dwFlags,
LPCWCH lpWideCharStr,
int cchWideChar,
LPSTR lpMultiByteStr,
int cbMultiByte,
LPCCH lpDefaultChar,
LPBOOL lpUsedDefaultChar
)
{
switch (CodePage) {
case CP_ACP:
case CP_THREAD_ACP:
SetThreadLocale(MAKELANGID(LANG_JAPANESE, SUBLANG_JAPANESE_JAPAN));
CodePage = CODEPAGE_SHIFT_JIS;
break;
default:
break;
}
return WideCharToMultiByte_orig(
CodePage,
dwFlags,
lpWideCharStr,
cchWideChar,
lpMultiByteStr,
cbMultiByte,
lpDefaultChar,
lpUsedDefaultChar);
}
#endif
void hooks::lang::early_init() {
log_info("hooks::lang", "early initialization");
@@ -170,8 +221,7 @@ void hooks::lang::early_init() {
detour::trampoline_try("kernel32.dll", "GetOEMCP", GetOEMCP_hook, &GetOEMCP_orig);
#ifdef SPICE64 // SDVX5+ specific code
bool isValkyrieCabinetMode = avs::game::SPEC[0] == 'G' || avs::game::SPEC[0] == 'H';
if (avs::game::is_model("KFC") && isValkyrieCabinetMode) {
if (games::sdvx::is_valkyrie_model()) {
log_info("hooks::lang", "hooking GetSystemDefaultLCID");
detour::trampoline_try(
"kernel32.dll",
@@ -189,6 +239,28 @@ void hooks::lang::early_init() {
GetLocaleInfoEx_hook,
&GetLocaleInfoEx_orig);
}
#ifdef SPICE64
// for TDJ subscreen search keyboard
if (avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) {
log_info("hooks::lang", "hooking IsDBCSLeadByte");
detour::trampoline_try(
"kernel32.dll",
"IsDBCSLeadByte",
IsDBCSLeadByte_hook,
&IsDBCSLeadByte_orig);
}
if (games::gitadora::is_arena_model()) {
log_info("hooks::lang", "hooking WideCharToMultiByte");
detour::trampoline_try(
"kernel32.dll",
"WideCharToMultiByte",
WideCharToMultiByte_hook,
&WideCharToMultiByte_orig);
}
#endif
}
void hooks::lang::init() {
+44 -1
View File
@@ -4,6 +4,7 @@
#include <iphlpapi.h>
#include <stdlib.h>
#include <string>
#include <mutex>
#include "avs/core.h"
#include "avs/ea3.h"
@@ -12,6 +13,7 @@
#include "util/detour.h"
#include "util/fileutils.h"
#include "util/libutils.h"
#include "util/deferlog.h"
// hooking related stuff
static decltype(GetAdaptersInfo) *GetAdaptersInfo_orig = nullptr;
@@ -27,6 +29,19 @@ static struct in_addr network;
static struct in_addr prefix;
static struct in_addr subnet;
static void defer_network_adapter_error() {
static std::once_flag printed;
std::call_once(printed, []() {
deferredlogs::defer_error_messages({
"network adapter issue detected!",
" ensure you have at least one network adapter with a valid IPv4 address",
" the IPv4 address can be external or internal, it just needs to be valid",
" the network adapter can be a wired or wireless connection",
" you still need to do this even if you are connecting to a local server!",
});
});
}
static ULONG WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO pAdapterInfo, PULONG pOutBufLen) {
// call orig
@@ -53,6 +68,15 @@ static ULONG WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO pAdapterInfo, PULONG p
free(pAdapterInfo2);
}
if (ret != ERROR_SUCCESS && ret != ERROR_BUFFER_OVERFLOW) {
defer_network_adapter_error();
log_warning(
"network",
"GetAdaptersInfo failed with {}; "
"check if you have at least one network adapter with a valid IPv4 address!",
ret);
}
return ret;
}
@@ -101,8 +125,13 @@ static ULONG WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO pAdapterInfo, PULONG p
free(pIpForwardTable);
pIpForwardTable = (MIB_IPFORWARDTABLE *) malloc(dwSize);
}
if (GetIpForwardTable(pIpForwardTable, &dwSize, 1) != NO_ERROR || pIpForwardTable->dwNumEntries == 0)
if (GetIpForwardTable(pIpForwardTable, &dwSize, 1) != NO_ERROR || pIpForwardTable->dwNumEntries == 0) {
defer_network_adapter_error();
if (GetAdaptersInfo_log) {
log_warning("network", "GetIpForwardTable failed");
}
return ret;
}
// determine best interface
DWORD best = pIpForwardTable->table[0].dwForwardIfIndex;
@@ -134,6 +163,20 @@ static ULONG WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO pAdapterInfo, PULONG p
info = info->Next;
}
if (pAdapterInfo->IpAddressList.IpAddress.String[0] == 0 ||
pAdapterInfo->IpAddressList.IpAddress.String[0] == '0') {
defer_network_adapter_error();
if (GetAdaptersInfo_log) {
log_warning(
"network",
"invalid IPv4 address for adapter {}, {} = {}; "
"ensure you have at least one network adapter with valid IPv4 address!",
pAdapterInfo->AdapterName,
pAdapterInfo->Description,
pAdapterInfo->IpAddressList.IpAddress.String);
}
}
// return original value
GetAdaptersInfo_log = false;
return ret;