mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30:42 -07:00
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ece03cabba | |||
| 4b7f68f920 | |||
| 2dae86a6f2 | |||
| 85058c2156 | |||
| ea2f4c5572 | |||
| 48033816a8 | |||
| b517ef3182 | |||
| 6fef16353e | |||
| 84ea3f9e8e | |||
| 6bb6b0a301 | |||
| 465d6c6c18 | |||
| ef384e85dd | |||
| 7ee04879e2 | |||
| 4c73200f58 | |||
| 31aabe9786 | |||
| 4ea55a61b8 | |||
| 0209b80a22 | |||
| 0511dfb6ca | |||
| b558df3340 | |||
| 89602cfcab | |||
| b4557993c9 | |||
| f119d7c988 | |||
| b3d8d0aea9 | |||
| a977cba772 | |||
| 489d40d87d | |||
| 3a468a9a3b | |||
| a2e220d3f4 | |||
| febc02b50b | |||
| 311404f56b | |||
| d929cdf7c8 | |||
| 2b930ad95f | |||
| 3c60f3966b | |||
| c59d399ab8 | |||
| f69e40fa26 | |||
| e9dcc5717e | |||
| 5bec3d5db7 | |||
| 6ec2b47345 | |||
| ebda16beae | |||
| c29a26f183 | |||
| 2014e9cf75 | |||
| 33f2e1d4b7 | |||
| eb037542b4 | |||
| 039b42ad53 | |||
| 33b4718744 | |||
| 71ba9b6b47 | |||
| 1957917270 | |||
| b9ccddf47d | |||
| cd0ba51b5a | |||
| 1f23d88c7a | |||
| 46e43ab09c | |||
| 96950b6b4e | |||
| 5f94ef4478 | |||
| c4193eddc5 | |||
| c8962a0e77 | |||
| b6a749e5db | |||
| 7dbbe9809e | |||
| 79a3e32d5d | |||
| c5a4e954f9 | |||
| 7b862768b0 | |||
| af8d8dae9f | |||
| d9d5823fdb | |||
| 82dda33800 | |||
| 3876e38636 | |||
| 6bc1357d6a | |||
| 37218e7fe0 | |||
| 678e11eade | |||
| f72313fe45 | |||
| 06374ef78a | |||
| bdde1ec6b8 | |||
| d2176fe4f0 | |||
| fb9eef904a | |||
| 48903998a5 | |||
| f2dca0265f | |||
| 1147ed9858 | |||
| 19d93f4ffd |
@@ -235,6 +235,13 @@ add_compile_definitions(
|
||||
_WIN32_IE=0x0400
|
||||
)
|
||||
|
||||
# SPICE_XP: set by build_all.sh for the WinXP-compat toolchains. Gates out
|
||||
# the DX11 overlay backend (WinXP lacks D3DCOMPILER_47.dll).
|
||||
option(SPICE_XP "Build for the WinXP-compat toolchain" OFF)
|
||||
if(SPICE_XP)
|
||||
add_compile_definitions(SPICE_XP=1)
|
||||
endif()
|
||||
|
||||
# acioemu log
|
||||
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DACIOEMU_LOG")
|
||||
|
||||
@@ -409,6 +416,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
games/nost/io.cpp
|
||||
games/nost/poke.cpp
|
||||
games/gitadora/gitadora.cpp
|
||||
games/gitadora/asio.cpp
|
||||
games/gitadora/io.cpp
|
||||
games/gitadora/handle.cpp
|
||||
games/gitadora/j32d.cpp
|
||||
@@ -486,6 +494,8 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
# hooks
|
||||
hooks/audio/acm.cpp
|
||||
hooks/audio/audio.cpp
|
||||
hooks/audio/asio_driver_scan.cpp
|
||||
hooks/audio/asio_proxy.cpp
|
||||
hooks/audio/buffer.cpp
|
||||
hooks/audio/mme.cpp
|
||||
hooks/audio/util.cpp
|
||||
@@ -494,8 +504,12 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
hooks/audio/backends/mmdevice/device.cpp
|
||||
hooks/audio/backends/mmdevice/device_collection.cpp
|
||||
hooks/audio/backends/mmdevice/device_enumerator.cpp
|
||||
hooks/audio/backends/mmdevice/null_device.cpp
|
||||
hooks/audio/backends/mmdevice/null_discard_backend.cpp
|
||||
hooks/audio/backends/wasapi/audio_client.cpp
|
||||
hooks/audio/backends/wasapi/audio_render_client.cpp
|
||||
hooks/audio/backends/wasapi/downmix.cpp
|
||||
hooks/audio/backends/wasapi/resample.cpp
|
||||
hooks/audio/backends/wasapi/dummy_audio_client.cpp
|
||||
hooks/audio/backends/wasapi/dummy_audio_clock.cpp
|
||||
hooks/audio/backends/wasapi/dummy_audio_render_client.cpp
|
||||
@@ -517,12 +531,19 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_swapchain.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_texture.cpp
|
||||
hooks/graphics/backends/d3d11/d3d11_backend.cpp
|
||||
hooks/graphics/backends/d3d11/d3d11_swapchain.cpp
|
||||
hooks/graphics/backends/d3d11/d3d11_factory.cpp
|
||||
hooks/graphics/backends/d3d11/d3d11_vtable_capture.cpp
|
||||
hooks/graphics/backends/d3d11/d3d11_screenshot.cpp
|
||||
hooks/input/dinput8/fake_backend.cpp
|
||||
hooks/input/dinput8/fake_device.cpp
|
||||
hooks/input/dinput8/hook.cpp
|
||||
hooks/lang.cpp
|
||||
hooks/libraryhook.cpp
|
||||
hooks/networkhook.cpp
|
||||
hooks/icmphook_net.cpp
|
||||
hooks/icmphook_iphlpapi.cpp
|
||||
hooks/powrprof.cpp
|
||||
#hooks/rom.cpp
|
||||
hooks/setupapihook.cpp
|
||||
@@ -556,6 +577,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
|
||||
# overlay
|
||||
overlay/overlay.cpp
|
||||
overlay/notifications.cpp
|
||||
overlay/window.cpp
|
||||
overlay/imgui/extensions.cpp
|
||||
overlay/imgui/impl_spice.cpp
|
||||
@@ -607,6 +629,9 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
reader/structuredmessage.cpp
|
||||
reader/crypt.cpp
|
||||
|
||||
# sdk
|
||||
sdk/sdk.cpp
|
||||
|
||||
# stubs
|
||||
stubs/stubs.cpp
|
||||
|
||||
@@ -633,6 +658,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
util/time.cpp
|
||||
util/cpuutils.cpp
|
||||
util/netutils.cpp
|
||||
util/precise_timer.cpp
|
||||
util/sysutils.cpp
|
||||
util/lz77.cpp
|
||||
util/tapeled.cpp
|
||||
@@ -707,6 +733,10 @@ target_link_libraries(spicetools_spice64 PUBLIC winscard)
|
||||
set_target_properties(spicetools_spice64 PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64")
|
||||
target_compile_definitions(spicetools_spice64 PRIVATE SPICE64=1)
|
||||
if(NOT SPICE_XP)
|
||||
target_link_libraries(spicetools_spice64 PUBLIC d3d11 dxgi d3dcompiler)
|
||||
target_compile_definitions(spicetools_spice64 PRIVATE SPICE_D3D11=1)
|
||||
endif()
|
||||
|
||||
IF(NOT MSVC)
|
||||
set_target_properties(spicetools_spice64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
|
||||
@@ -726,6 +756,9 @@ set_target_properties(spicetools_spice64_linux PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_spice64_linux PROPERTIES OUTPUT_NAME "spice64_linux")
|
||||
target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE64=1)
|
||||
target_compile_definitions(spicetools_spice64_linux PRIVATE NO_SCARD=1 PRIVATE SPICE_LINUX=1)
|
||||
# spice64_linux is never built under the WinXP toolchain, so dx11 is always on.
|
||||
target_link_libraries(spicetools_spice64_linux PUBLIC d3d11 dxgi d3dcompiler)
|
||||
target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE_D3D11=1)
|
||||
|
||||
IF(NOT MSVC)
|
||||
set_target_properties(spicetools_spice64_linux PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
|
||||
@@ -738,7 +771,7 @@ set(SOURCE_FILES ${SOURCE_FILES} launcher/options.h launcher/options.cpp)
|
||||
set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc)
|
||||
add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||
target_link_libraries(spicetools_cfg
|
||||
PUBLIC ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
|
||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
||||
target_link_libraries(spicetools_cfg PUBLIC winscard)
|
||||
set_target_properties(spicetools_cfg PROPERTIES PREFIX "")
|
||||
@@ -756,7 +789,7 @@ set(SOURCE_FILES ${SOURCE_FILES} launcher/options.h launcher/options.cpp)
|
||||
set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc)
|
||||
add_executable(spicetools_cfg_linux WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||
target_link_libraries(spicetools_cfg_linux
|
||||
PUBLIC ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
|
||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
||||
set_target_properties(spicetools_cfg_linux PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux")
|
||||
@@ -860,6 +893,36 @@ if(NOT MSVC)
|
||||
set_target_properties(spicetools_stubs_cpusbxpkm PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32")
|
||||
endif()
|
||||
|
||||
# sdk_sample_v0_flat_c.dll (32 bit)
|
||||
set(SOURCE_FILES sdk/sample/v0/flat_c/v0_flat_c.c)
|
||||
add_library(spicetools_sdk_sample_v0_flat_c_32 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/flat_c/v0_flat_c.def)
|
||||
set_target_properties(spicetools_sdk_sample_v0_flat_c_32 PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_sdk_sample_v0_flat_c_32 PROPERTIES OUTPUT_NAME "sdk_sample_v0_flat_c")
|
||||
|
||||
if(NOT MSVC)
|
||||
set_target_properties(spicetools_sdk_sample_v0_flat_c_32 PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32")
|
||||
endif()
|
||||
|
||||
# sdk_sample_v0_flat_c.dll (64 bit)
|
||||
set(SOURCE_FILES sdk/sample/v0/flat_c/v0_flat_c.c)
|
||||
add_library(spicetools_sdk_sample_v0_flat_c_64 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/flat_c/v0_flat_c.def)
|
||||
set_target_properties(spicetools_sdk_sample_v0_flat_c_64 PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_sdk_sample_v0_flat_c_64 PROPERTIES OUTPUT_NAME "sdk_sample_v0_flat_c")
|
||||
|
||||
if(NOT MSVC)
|
||||
set_target_properties(spicetools_sdk_sample_v0_flat_c_64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
|
||||
endif()
|
||||
|
||||
# sdk_sample_v0_cpp.dll (64 bit)
|
||||
set(SOURCE_FILES sdk/sample/v0/cpp/v0_cpp.cpp)
|
||||
add_library(spicetools_sdk_sample_v0_cpp_64 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/cpp/v0_cpp.def)
|
||||
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES OUTPUT_NAME "sdk_sample_v0_cpp")
|
||||
|
||||
if(NOT MSVC)
|
||||
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
|
||||
endif()
|
||||
|
||||
# output directories
|
||||
####################
|
||||
|
||||
@@ -869,14 +932,14 @@ set_target_properties(spicetools_cfg spicetools_cfg_linux
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools")
|
||||
|
||||
# output 32bit
|
||||
set_target_properties(spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_kbt spicetools_stubs_kld spicetools_stubs_cpusbxpkm
|
||||
set_target_properties(spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_kbt spicetools_stubs_kld spicetools_stubs_cpusbxpkm spicetools_sdk_sample_v0_flat_c_32
|
||||
PROPERTIES
|
||||
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive32"
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32")
|
||||
|
||||
# output 64bit
|
||||
set_target_properties(spicetools_spice64 spicetools_spice64_linux spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvcuda spicetools_stubs_nvcuvid spicetools_stubs_nvEncodeAPI64
|
||||
set_target_properties(spicetools_spice64 spicetools_spice64_linux spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvcuda spicetools_stubs_nvcuvid spicetools_stubs_nvEncodeAPI64 spicetools_sdk_sample_v0_flat_c_64 spicetools_sdk_sample_v0_cpp_64
|
||||
PROPERTIES
|
||||
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive64"
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64"
|
||||
|
||||
@@ -283,6 +283,16 @@ Python is the only one that is fully spec compliant.
|
||||
Other libraries may be missing features and contain bugs; please feel free to
|
||||
contribute code to fill the gaps if you work on a project using these libraries.
|
||||
|
||||
## Spice SDK (for DLL hooks / plugins)
|
||||
|
||||
As an alternative to Spice API, Spice SDK is available.
|
||||
|
||||
This is a flat C, header-only library that can be included in your DLL. Once
|
||||
initialized, you can call directly into helper routines provided by Spicetools
|
||||
executable, making low-latency and high-throughput interactions possible.
|
||||
|
||||
Check the documentation on the wiki for more details.
|
||||
|
||||
## License
|
||||
Unless otherwise noted, all files are licensed under the GPLv3.
|
||||
See the LICENSE file for the full license text.
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
#include "mdxf.h"
|
||||
#include "mdxf_poll.h"
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "games/ddr/ddr.h"
|
||||
#include "games/ddr/io.h"
|
||||
#include "launcher/launcher.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "util/utils.h"
|
||||
#include <mutex>
|
||||
|
||||
#define MDFX_DEBUG_VERBOSE 0
|
||||
|
||||
@@ -108,9 +111,10 @@ static void mdxf_thread_start() {
|
||||
MDXF_THREAD = std::thread([] {
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
|
||||
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
while (MDXF_THREAD_RUNNING.load(std::memory_order_acquire)) {
|
||||
mdxf_poll(false);
|
||||
std::this_thread::sleep_for(THREAD_PERIOD);
|
||||
timer.sleep(THREAD_PERIOD);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -357,14 +361,17 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFP
|
||||
|
||||
// decide on button map
|
||||
const size_t *button_map = nullptr;
|
||||
int player = 0;
|
||||
switch (node) {
|
||||
case 17:
|
||||
case 25:
|
||||
button_map = &buttons_p1[0];
|
||||
player = 1;
|
||||
break;
|
||||
case 18:
|
||||
case 26:
|
||||
button_map = &buttons_p2[0];
|
||||
player = 2;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -374,19 +381,27 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFP
|
||||
if (source == EXTERNAL_POLL) {
|
||||
// get buttons
|
||||
auto &buttons = games::ddr::get_buttons();
|
||||
|
||||
// get analogs
|
||||
bool analog_left = false;
|
||||
bool analog_right = false;
|
||||
games::ddr::get_analog_x_axis(player, analog_left, analog_right);
|
||||
bool analog_up = false;
|
||||
bool analog_down = false;
|
||||
games::ddr::get_analog_y_axis(player, analog_up, analog_down);
|
||||
|
||||
uint8_t up_down = 0;
|
||||
uint8_t left_right = 0;
|
||||
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[0]))) {
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[0])) || analog_up) {
|
||||
up_down |= 0xF0;
|
||||
}
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[1]))) {
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[1])) || analog_down) {
|
||||
up_down |= 0x0F;
|
||||
}
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[2]))) {
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[2])) || analog_left) {
|
||||
left_right |= 0xF0;
|
||||
}
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[3]))) {
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[3])) || analog_right) {
|
||||
left_right |= 0x0F;
|
||||
}
|
||||
current_state = (uint16_t(up_down) << 8) | left_right;
|
||||
@@ -500,4 +515,4 @@ acio::MDXFModule::~MDXFModule() {
|
||||
if (IS_THREAD_NEEDED) {
|
||||
mdxf_thread_stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "util/circular_buffer.h"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "games/sdvx/sdvx.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
using namespace acioemu;
|
||||
@@ -46,11 +47,12 @@ ICCADevice::ICCADevice(bool flip_order, bool keypad_thread, uint8_t node_count)
|
||||
// keypad thread for faster polling
|
||||
if (keypad_thread) {
|
||||
this->keypad_thread = new std::thread([this]() {
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
while (this->cards) {
|
||||
for (int unit = 0; unit < this->node_count; unit++) {
|
||||
this->update_keypad(unit, false);
|
||||
}
|
||||
Sleep(7);
|
||||
timer.sleep(7);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
#include "overlay/notifications.h"
|
||||
|
||||
#include "module.h"
|
||||
#include "modules/analogs.h"
|
||||
#include "modules/buttons.h"
|
||||
@@ -197,6 +199,9 @@ void Controller::connection_handler(api::ClientState client_state) {
|
||||
|
||||
// log connection
|
||||
log_info("api", "client connected: {}", client_address_str);
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Success,
|
||||
fmt::format("API client connected ({})", client_address_str));
|
||||
client_states_m.lock();
|
||||
client_states.emplace_back(&client_state);
|
||||
client_states_m.unlock();
|
||||
@@ -277,6 +282,9 @@ void Controller::connection_handler(api::ClientState client_state) {
|
||||
|
||||
// log disconnect
|
||||
log_info("api", "client disconnected: {}", client_address_str);
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Info,
|
||||
fmt::format("API client disconnected ({})", client_address_str));
|
||||
client_states_m.lock();
|
||||
client_states.erase(std::remove(client_states.begin(), client_states.end(), &client_state));
|
||||
client_states_m.unlock();
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "launcher/launcher.h"
|
||||
#include "games/io.h"
|
||||
#include "util/utils.h"
|
||||
#include "acio/mdxf/mdxf_poll.h"
|
||||
|
||||
using namespace std::placeholders;
|
||||
using namespace rapidjson;
|
||||
@@ -104,6 +105,7 @@ namespace api::modules {
|
||||
for (auto &analog : *this->analogs) {
|
||||
analog.override_enabled = false;
|
||||
}
|
||||
mdxf_poll(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -148,6 +150,7 @@ namespace api::modules {
|
||||
if (analog.getName() == name) {
|
||||
analog.override_state = CLAMP(state, 0.f, 1.f);
|
||||
analog.override_enabled = true;
|
||||
mdxf_poll(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -167,6 +170,7 @@ namespace api::modules {
|
||||
for (auto &analog : *this->analogs) {
|
||||
if (analog.getName() == name) {
|
||||
analog.override_enabled = false;
|
||||
mdxf_poll(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "launcher/launcher.h"
|
||||
#include "games/io.h"
|
||||
#include "util/utils.h"
|
||||
#include "acio/mdxf/mdxf_poll.h"
|
||||
|
||||
using namespace std::placeholders;
|
||||
using namespace rapidjson;
|
||||
@@ -107,6 +108,7 @@ namespace api::modules {
|
||||
for (auto &button : *this->buttons) {
|
||||
button.override_enabled = false;
|
||||
}
|
||||
mdxf_poll(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -153,6 +155,7 @@ namespace api::modules {
|
||||
GameAPI::Buttons::BUTTON_PRESSED : GameAPI::Buttons::BUTTON_NOT_PRESSED;
|
||||
button.override_velocity = CLAMP(state, 0.f, 1.f);
|
||||
button.override_enabled = true;
|
||||
mdxf_poll(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -172,6 +175,7 @@ namespace api::modules {
|
||||
for (auto &button : *this->buttons) {
|
||||
if (button.getName() == name) {
|
||||
button.override_enabled = false;
|
||||
mdxf_poll(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "avs/game.h"
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "util/precise_timer.h"
|
||||
|
||||
using namespace std::placeholders;
|
||||
using namespace rapidjson;
|
||||
@@ -58,6 +59,7 @@ namespace api::modules {
|
||||
// get params
|
||||
auto keypad = req.params[0].GetUint();
|
||||
auto input = std::string(req.params[1].GetString());
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
// process all chars
|
||||
for (auto c : input) {
|
||||
@@ -91,11 +93,11 @@ namespace api::modules {
|
||||
|
||||
// set
|
||||
eamuse_set_keypad_overrides(keypad, state);
|
||||
Sleep(sleep_time);
|
||||
timer.sleep(sleep_time);
|
||||
|
||||
// unset
|
||||
eamuse_set_keypad_overrides(keypad, 0);
|
||||
Sleep(sleep_time);
|
||||
timer.sleep(sleep_time);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace api::modules {
|
||||
|
||||
// check if file exists in modules
|
||||
if (!fileutils::file_exists(dll_path)) {
|
||||
return error(res, "Couldn't find " + dll_path.string());
|
||||
return error(res, fmt::format("Couldn't find {}", dll_path));
|
||||
}
|
||||
|
||||
// get module
|
||||
@@ -118,7 +118,7 @@ namespace api::modules {
|
||||
|
||||
// check if file exists in modules
|
||||
if (!fileutils::file_exists(dll_path)) {
|
||||
return error(res, "Couldn't find " + dll_path.string());
|
||||
return error(res, fmt::format("Couldn't find {}", dll_path));
|
||||
}
|
||||
|
||||
// get module
|
||||
@@ -196,7 +196,7 @@ namespace api::modules {
|
||||
|
||||
// check if file exists in modules
|
||||
if (!fileutils::file_exists(dll_path)) {
|
||||
return error(res, "Couldn't find " + dll_path.string());
|
||||
return error(res, fmt::format("Couldn't find {}", dll_path));
|
||||
}
|
||||
|
||||
// get module
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
|
||||
@@ -16,6 +17,7 @@ namespace api {
|
||||
controller->init_state(this->state);
|
||||
this->thread = new std::thread([this] () {
|
||||
log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud);
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
// read buffer
|
||||
uint8_t read_buffer[16*1024];
|
||||
@@ -160,7 +162,7 @@ namespace api {
|
||||
|
||||
// slow down on reconnect
|
||||
if (this->running) {
|
||||
Sleep(retry_time);
|
||||
timer.sleep(retry_time);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "util/utils.h"
|
||||
#include "util/rc4.h"
|
||||
#include "util/logging.h"
|
||||
#include "overlay/notifications.h"
|
||||
#include "controller.h"
|
||||
|
||||
using namespace headsocket;
|
||||
@@ -91,12 +92,18 @@ namespace api {
|
||||
|
||||
// log connection
|
||||
log_info("api::websocket", "client connected");
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Success,
|
||||
"API websocket client connected");
|
||||
}
|
||||
|
||||
void WebSocketClient::on_disconnect() {
|
||||
|
||||
// log disconnection
|
||||
log_info("api::websocket", "client disconnected");
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Info,
|
||||
"API websocket client disconnected");
|
||||
|
||||
// get pointer to server
|
||||
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
|
||||
|
||||
@@ -1168,7 +1168,7 @@ namespace avs {
|
||||
// Ongaku Paradise
|
||||
{"arkjc9.dll", 0xA00000},
|
||||
|
||||
// KAMUNITY
|
||||
// KAMUNITY
|
||||
{"kamunity.dll", 33554432},
|
||||
};
|
||||
|
||||
@@ -1516,7 +1516,7 @@ namespace avs {
|
||||
" * It's also possible that you have incomplete game data\n"
|
||||
" * Do NOT copy over random DLLs from another game installation; DLL must match game version\n"
|
||||
"\n"
|
||||
, DLL_NAME, MODULE_PATH.string()) };
|
||||
, DLL_NAME, MODULE_PATH) };
|
||||
log_warning("avs-ea3", "{}", info_str);
|
||||
log_fatal("avs-ea3", "Failed to find critical avs DLL on disk (avs2-core.dll OR {})", DLL_NAME);
|
||||
}
|
||||
@@ -1757,9 +1757,9 @@ namespace avs {
|
||||
{
|
||||
deferredlogs::defer_error_messages({
|
||||
"AVS filesystem initialization failure was previously detected during boot!",
|
||||
fmt::format(" ERROR: directory could not be created: {}", src_path.string().c_str()),
|
||||
fmt::format(" if you see a crash, it may have been caused by bad <mounttable> contents in {}", avs::core::CFG_PATH.c_str()),
|
||||
" fix the XML file and try again"
|
||||
fmt::format(" ERROR: directory could not be created: {}", src_path),
|
||||
fmt::format(" if you see a crash, it may have been caused by bad <mounttable> contents in {}", avs::core::CFG_PATH),
|
||||
" fix the XML file and try again"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1780,14 +1780,14 @@ namespace avs {
|
||||
auto created = std::filesystem::create_directories(real_path, err);
|
||||
|
||||
if (created) {
|
||||
log_info("avs-core", "created '{}' at '{}'", avs_path, real_path.string());
|
||||
log_info("avs-core", "created '{}' at '{}'", avs_path, real_path);
|
||||
}
|
||||
|
||||
if (err) {
|
||||
avs_dir_err(real_path);
|
||||
log_warning("avs-core", "failed to create '{}' folder at '{}': {}",
|
||||
avs_path,
|
||||
real_path.string(),
|
||||
real_path,
|
||||
err.message());
|
||||
}
|
||||
}
|
||||
@@ -1921,6 +1921,15 @@ namespace avs {
|
||||
// create nvram and raw directories if possible for mounttable configurations
|
||||
create_avs_config_fs_table(config, config_node);
|
||||
|
||||
#if !SPICE64
|
||||
// sdvx4 bad log config fix
|
||||
if (avs::game::DLL_NAME == "soundvoltex.dll" && // it's too early for avs::game::is_model
|
||||
property_search_safe(config, config_node, "/log/enable_console")) {
|
||||
log_info("avs-core", "applying SDVX4 avs-config.xml fix for <log><enable_console>");
|
||||
property_search_remove_safe(config, config_node, "/log/enable_console");
|
||||
}
|
||||
#endif
|
||||
|
||||
// set log level
|
||||
if (!LOG_LEVEL_CUSTOM.empty()) {
|
||||
property_search_remove_safe(config, config_node, "/log/level");
|
||||
|
||||
@@ -151,8 +151,8 @@ namespace avs {
|
||||
" * It's also possible that you have incomplete game data\n"
|
||||
" * Do NOT copy over random DLLs from another game installation; DLL must match game version\n"
|
||||
"\n"
|
||||
, DLL_NAME, MODULE_PATH.string()) };
|
||||
|
||||
, DLL_NAME, MODULE_PATH) };
|
||||
|
||||
log_warning("avs-ea3", "{}", info_str);
|
||||
log_fatal("avs-ea3", "Failed to find critical ea3 DLL on disk (avs2-ea3.dll OR {})", DLL_NAME);
|
||||
}
|
||||
@@ -217,8 +217,10 @@ namespace avs {
|
||||
ea3_config_name = "prop/eamuse-config.xml";
|
||||
}
|
||||
|
||||
if (avs::core::file_exists(ea3_config_name)) {
|
||||
log_info("avs-ea3", "booting (using {})", ea3_config_name);
|
||||
if (fileutils::file_exists(ea3_config_name)) {
|
||||
log_info("avs-ea3", "found {} on disk", ea3_config_name);
|
||||
} else if (CFG_PATH.size()) {
|
||||
log_fatal("avs-ea3", "user-specified ea3 config file is missing: {}", ea3_config_name);
|
||||
} else {
|
||||
log_warning("avs-ea3", "looked for the following files in order:");
|
||||
log_warning("avs-ea3", " * prop/ea3-config.xml");
|
||||
|
||||
@@ -73,11 +73,10 @@ namespace avs {
|
||||
|
||||
// load game instance
|
||||
const auto dll_path = MODULE_PATH / DLL_NAME;
|
||||
const auto dll_path_s = dll_path.string();
|
||||
log_info("avs-game", "DLL path: {}", dll_path_s.c_str());
|
||||
log_info("avs-game", "DLL path: {}", dll_path);
|
||||
|
||||
// MAX_PATH is 260
|
||||
if (130 <= dll_path_s.length()) {
|
||||
if (const auto dll_path_len = dll_path.wstring().length(); 130 <= dll_path_len) {
|
||||
log_warning(
|
||||
"avs-game",
|
||||
"PATH TOO LONG WARNING\n\n"
|
||||
@@ -93,7 +92,7 @@ namespace avs {
|
||||
"long, often resulting in random crashes. Move the game contents to\n"
|
||||
"a directory with shorter path.\n"
|
||||
"-------------------------------------------------------------------\n\n",
|
||||
dll_path_s, dll_path_s.length());
|
||||
dll_path, dll_path_len);
|
||||
}
|
||||
|
||||
// ddr gamemdx.dll user error
|
||||
@@ -112,7 +111,7 @@ namespace avs {
|
||||
|
||||
// file not found on disk
|
||||
if (!fileutils::file_exists(dll_path)) {
|
||||
log_warning("avs-game", "game DLL could not be found on disk: {}", dll_path.string().c_str());
|
||||
log_warning("avs-game", "game DLL could not be found on disk: {}", dll_path);
|
||||
log_warning("avs-game", "double check -exec and -modules parameters; unless you know what you're doing, leave them blank");
|
||||
}
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
<button name="VEFX" vkey="10" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||
</buttons>
|
||||
<analogs>
|
||||
<analog name="Turntable P1" devid="Player 1" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Turntable P2" devid="Player 2" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Turntable P1" devid="Player 1" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Turntable P2" devid="Player 2" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
</analogs>
|
||||
<lights>
|
||||
<light name="P1 1" devid="Player 1" index="0"/>
|
||||
@@ -117,8 +117,8 @@
|
||||
<keypad_buttons/>
|
||||
<!-- Assumes analog mode. Need to invert direction since this FW is designed for INF. -->
|
||||
<analogs>
|
||||
<analog name="Turntable P1" devid="arcin (1p)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="true" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Turntable P2" devid="arcin (2p)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="true" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Turntable P1" devid="arcin (1p)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="true" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Turntable P2" devid="arcin (2p)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="true" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
</analogs>
|
||||
<lights>
|
||||
<light name="P1 1" devid="arcin (1p)" index="0"/>
|
||||
@@ -144,34 +144,34 @@
|
||||
<buttons/>
|
||||
<keypad_buttons/>
|
||||
<analogs>
|
||||
<analog name="Key 1" devid="Analogs key 01-28" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 2" devid="Analogs key 01-28" index="5" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 3" devid="Analogs key 01-28" index="6" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 4" devid="Analogs key 01-28" index="7" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 5" devid="Analogs key 01-28" index="8" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 6" devid="Analogs key 01-28" index="9" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 7" devid="Analogs key 01-28" index="10" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 8" devid="Analogs key 01-28" index="11" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 9" devid="Analogs key 01-28" index="12" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 10" devid="Analogs key 01-28" index="13" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 11" devid="Analogs key 01-28" index="14" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 12" devid="Analogs key 01-28" index="15" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 13" devid="Analogs key 01-28" index="16" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 14" devid="Analogs key 01-28" index="17" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 15" devid="Analogs key 01-28" index="18" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 16" devid="Analogs key 01-28" index="19" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 17" devid="Analogs key 01-28" index="20" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 18" devid="Analogs key 01-28" index="21" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 19" devid="Analogs key 01-28" index="22" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 20" devid="Analogs key 01-28" index="23" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 21" devid="Analogs key 01-28" index="24" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 22" devid="Analogs key 01-28" index="25" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 23" devid="Analogs key 01-28" index="26" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 24" devid="Analogs key 01-28" index="27" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 25" devid="Analogs key 01-28" index="28" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 26" devid="Analogs key 01-28" index="29" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 27" devid="Analogs key 01-28" index="30" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 28" devid="Analogs key 01-28" index="31" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Key 1" devid="Analogs key 01-28" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 2" devid="Analogs key 01-28" index="5" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 3" devid="Analogs key 01-28" index="6" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 4" devid="Analogs key 01-28" index="7" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 5" devid="Analogs key 01-28" index="8" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 6" devid="Analogs key 01-28" index="9" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 7" devid="Analogs key 01-28" index="10" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 8" devid="Analogs key 01-28" index="11" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 9" devid="Analogs key 01-28" index="12" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 10" devid="Analogs key 01-28" index="13" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 11" devid="Analogs key 01-28" index="14" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 12" devid="Analogs key 01-28" index="15" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 13" devid="Analogs key 01-28" index="16" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 14" devid="Analogs key 01-28" index="17" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 15" devid="Analogs key 01-28" index="18" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 16" devid="Analogs key 01-28" index="19" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 17" devid="Analogs key 01-28" index="20" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 18" devid="Analogs key 01-28" index="21" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 19" devid="Analogs key 01-28" index="22" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 20" devid="Analogs key 01-28" index="23" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 21" devid="Analogs key 01-28" index="24" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 22" devid="Analogs key 01-28" index="25" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 23" devid="Analogs key 01-28" index="26" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 24" devid="Analogs key 01-28" index="27" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 25" devid="Analogs key 01-28" index="28" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 26" devid="Analogs key 01-28" index="29" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 27" devid="Analogs key 01-28" index="30" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Key 28" devid="Analogs key 01-28" index="31" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
</analogs>
|
||||
<lights>
|
||||
<light name="Key 1 R" devid="Lights 01-14" index="0"/>
|
||||
@@ -277,9 +277,9 @@
|
||||
</buttons>
|
||||
<keypad_buttons/>
|
||||
<analogs>
|
||||
<analog name="Guitar P1 Wail X" devid="Gitaller P1" index="1" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Guitar P1 Wail Y" devid="Gitaller P1" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Guitar P1 Wail Z" devid="Gitaller P1" index="5" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="Guitar P1 Wail X" devid="Gitaller P1" index="1" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Guitar P1 Wail Y" devid="Gitaller P1" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="Guitar P1 Wail Z" devid="Gitaller P1" index="5" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
</analogs>
|
||||
<lights/>
|
||||
<!-- Gen 1 FAUCETWO in HID mode; should also work for later gens -->
|
||||
@@ -296,8 +296,8 @@
|
||||
</buttons>
|
||||
<keypad_buttons/>
|
||||
<analogs>
|
||||
<analog name="VOL-L" devid="Buttons (MI_01)" index="1" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="VOL-R" devid="Buttons (MI_01)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay="0"/>
|
||||
<analog name="VOL-L" devid="Buttons (MI_01)" index="1" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
<analog name="VOL-R" devid="Buttons (MI_01)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
|
||||
</analogs>
|
||||
<lights>
|
||||
<light name="BT-A" devid="Lights (MI_02)" index="24"/>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
+14
-16
@@ -68,8 +68,8 @@ DIST_FOLDER="./dist"
|
||||
DIST_NAME="spice2x-$(date +%y)-$(date +%m)-$(date +%d).zip"
|
||||
DIST_NAME_EXTRAS="spice2x-$(date +%y)-$(date +%m)-$(date +%d)-full.zip"
|
||||
DIST_COMMENT=${DIST_NAME}$'\n'"$GIT_BRANCH - $GIT_HEAD"$'\nThank you for playing.'
|
||||
TARGETS_32="spicetools_stubs_kbt spicetools_stubs_kld spicetools_cfg spicetools_cfg_linux spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_cpusbxpkm"
|
||||
TARGETS_64="spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvEncodeAPI64 spicetools_stubs_nvcuvid spicetools_stubs_nvcuda spicetools_spice64 spicetools_spice64_linux"
|
||||
TARGETS_32="spicetools_stubs_kbt spicetools_stubs_kld spicetools_cfg spicetools_cfg_linux spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_cpusbxpkm spicetools_sdk_sample_v0_flat_c_32"
|
||||
TARGETS_64="spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvEncodeAPI64 spicetools_stubs_nvcuvid spicetools_stubs_nvcuda spicetools_spice64 spicetools_spice64_linux spicetools_sdk_sample_v0_flat_c_64 spicetools_sdk_sample_v0_cpp_64"
|
||||
TARGETS_XP32="spicetools_cfg spicetools_spice"
|
||||
TARGETS_XP64="spicetools_spice64"
|
||||
|
||||
@@ -163,6 +163,7 @@ time (
|
||||
if ((BUILD_XP > 0))
|
||||
then
|
||||
# 32 bit Windows XP
|
||||
echo ""
|
||||
echo "Building 32bit targets (WinXP toolchain)..."
|
||||
echo "========================="
|
||||
if ((CLEAN_BUILD > 0))
|
||||
@@ -171,7 +172,7 @@ time (
|
||||
fi
|
||||
mkdir -p ${BUILDDIR_WINXP_32}
|
||||
pushd ${BUILDDIR_WINXP_32} > /dev/null
|
||||
CXXFLAGS="$CXXFLAGS -DSPICE_XP=1" cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_32} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} "$OLDPWD" && ninja ${TARGETS_XP32}
|
||||
cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_32} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DSPICE_XP=ON "$OLDPWD" && ninja ${TARGETS_XP32}
|
||||
popd > /dev/null
|
||||
|
||||
# 64 bit Windows XP
|
||||
@@ -184,9 +185,10 @@ time (
|
||||
fi
|
||||
mkdir -p ${BUILDDIR_WINXP_64}
|
||||
pushd ${BUILDDIR_WINXP_64} > /dev/null
|
||||
CXXFLAGS="$CXXFLAGS -DSPICE_XP=1" cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_64} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} "$OLDPWD" && ninja ${TARGETS_XP64}
|
||||
cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_64} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DSPICE_XP=ON "$OLDPWD" && ninja ${TARGETS_XP64}
|
||||
popd > /dev/null
|
||||
else
|
||||
echo ""
|
||||
echo "Skipping WinXP builds, toolchain not specified"
|
||||
fi
|
||||
|
||||
@@ -267,6 +269,8 @@ rm -rf ${OUTDIR_EXTRAS}
|
||||
mkdir -p ${OUTDIR_EXTRAS}
|
||||
mkdir -p ${OUTDIR_EXTRAS}/largeaddressaware
|
||||
mkdir -p ${OUTDIR_EXTRAS}/linux
|
||||
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/32
|
||||
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/64
|
||||
if ((BUILD_XP > 0))
|
||||
then
|
||||
mkdir -p ${OUTDIR_EXTRAS}/winxp
|
||||
@@ -279,16 +283,8 @@ then
|
||||
cp ${BUILDDIR_32}/spicetools/spicecfg-pdb.pdb ${OUTDIR} 2>/dev/null
|
||||
cp ${BUILDDIR_32}/spicetools/32/spice-pdb.exe ${OUTDIR} 2>/dev/null
|
||||
cp ${BUILDDIR_32}/spicetools/32/spice-pdb.pdb ${OUTDIR} 2>/dev/null
|
||||
#cp ${BUILDDIR_32}/spicetools/32/kbt.dll ${OUTDIR}/stubs/32 2>/dev/null
|
||||
#cp ${BUILDDIR_32}/spicetools/32/kld.dll ${OUTDIR}/stubs/32 2>/dev/null
|
||||
cp ${BUILDDIR_64}/spicetools/64/spice64-pdb.exe ${OUTDIR} 2>/dev/null
|
||||
cp ${BUILDDIR_64}/spicetools/64/spice64-pdb.pdb ${OUTDIR} 2>/dev/null
|
||||
#cp ${BUILDDIR_64}/spicetools/64/kbt.dll ${OUTDIR}/stubs/64 2>/dev/null
|
||||
#cp ${BUILDDIR_64}/spicetools/64/kld.dll ${OUTDIR}/stubs/64 2>/dev/null
|
||||
#cp ${BUILDDIR_64}/spicetools/64/nvEncodeAPI64.dll ${OUTDIR}/stubs/64 2>/dev/null
|
||||
#cp ${BUILDDIR_64}/spicetools/64/nvcuda.dll ${OUTDIR}/stubs/64 2>/dev/null
|
||||
#cp ${BUILDDIR_64}/spicetools/64/nvcuvid.dll ${OUTDIR}/stubs/64 2>/dev/null
|
||||
#cp ${BUILDDIR_32}/spicetools/32/cpusbxpkm.dll ${OUTDIR}/stubs/32 2>/dev/null
|
||||
else
|
||||
# release files
|
||||
cp ${BUILDDIR_32}/spicetools/spicecfg.exe ${OUTDIR} 2>/dev/null
|
||||
@@ -296,16 +292,15 @@ else
|
||||
cp ${BUILDDIR_32}/spicetools/32/spice.exe ${OUTDIR} 2>/dev/null
|
||||
cp ${BUILDDIR_32}/spicetools/32/spice_laa.exe ${OUTDIR_EXTRAS}/largeaddressaware/spice.exe 2>/dev/null
|
||||
cp ${BUILDDIR_32}/spicetools/32/spice_linux.exe ${OUTDIR_EXTRAS}/linux/spice.exe 2>/dev/null
|
||||
#cp ${BUILDDIR_32}/spicetools/32/kbt.dll ${OUTDIR}/stubs/32 2>/dev/null
|
||||
#cp ${BUILDDIR_32}/spicetools/32/kld.dll ${OUTDIR}/stubs/32 2>/dev/null
|
||||
cp ${BUILDDIR_64}/spicetools/64/spice64.exe ${OUTDIR} 2>/dev/null
|
||||
cp ${BUILDDIR_64}/spicetools/64/spice64_linux.exe ${OUTDIR_EXTRAS}/linux/spice64.exe 2>/dev/null
|
||||
#cp ${BUILDDIR_64}/spicetools/64/kbt.dll ${OUTDIR}/stubs/64 2>/dev/null
|
||||
#cp ${BUILDDIR_64}/spicetools/64/kld.dll ${OUTDIR}/stubs/64 2>/dev/null
|
||||
cp ${BUILDDIR_64}/spicetools/64/nvEncodeAPI64.dll ${OUTDIR}/stubs/64 2>/dev/null
|
||||
cp ${BUILDDIR_64}/spicetools/64/nvcuda.dll ${OUTDIR}/stubs/64 2>/dev/null
|
||||
cp ${BUILDDIR_64}/spicetools/64/nvcuvid.dll ${OUTDIR}/stubs/64 2>/dev/null
|
||||
cp ${BUILDDIR_32}/spicetools/32/cpusbxpkm.dll ${OUTDIR}/stubs/32 2>/dev/null
|
||||
cp ${BUILDDIR_32}/spicetools/32/sdk_sample_v0_flat_c.dll ${OUTDIR_EXTRAS}/sdk/samples/32/v0_flat_c.dll 2>/dev/null
|
||||
cp ${BUILDDIR_64}/spicetools/64/sdk_sample_v0_flat_c.dll ${OUTDIR_EXTRAS}/sdk/samples/64/v0_flat_c.dll 2>/dev/null
|
||||
cp ${BUILDDIR_64}/spicetools/64/sdk_sample_v0_cpp.dll ${OUTDIR_EXTRAS}/sdk/samples/64/v0_cpp.dll 2>/dev/null
|
||||
if ((BUILD_XP > 0))
|
||||
then
|
||||
cp ${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe ${OUTDIR_EXTRAS}/winxp 2>/dev/null
|
||||
@@ -324,6 +319,9 @@ then
|
||||
echo "WARNING: Couldn't get git to create the archive. Is this a git repository?"
|
||||
fi
|
||||
|
||||
# sdk headers
|
||||
cp -r ./sdk/include ${OUTDIR_EXTRAS}/sdk
|
||||
|
||||
# copy resources
|
||||
rm -rf ${OUTDIR_EXTRAS}/api
|
||||
mkdir ${OUTDIR_EXTRAS}/api
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
docker build --pull external/docker -t spicetools/deps
|
||||
docker build --build-context gitroot=%cd%/../../.git . -t spicetools/spice
|
||||
docker run --rm -it -v %cd%/dist:/src/src/spice2x/dist -v %cd%/bin:/src/src/spice2x/bin -v %cd%/.ccache:/src/src/spice2x/.ccache spicetools/spice %*
|
||||
docker run --rm -i -v %cd%/dist:/src/src/spice2x/dist -v %cd%/bin:/src/src/spice2x/bin -v %cd%/.ccache:/src/src/spice2x/.ccache spicetools/spice %*
|
||||
@REM to generate PDBs, set DEBUG to 1 in build_all.sh, place cv2pdb in external\cv2pdb, and run below
|
||||
@REM external\cv2pdb\cv2pdb.exe bin\spice2x\spicecfg.exe bin\spice2x\spicecfg-pdb.exe bin\spice2x\spicecfg-pdb.pdb
|
||||
@REM external\cv2pdb\cv2pdb.exe bin\spice2x\spice.exe bin\spice2x\spice-pdb.exe bin\spice2x\spice-pdb.pdb
|
||||
@REM external\cv2pdb\cv2pdb.exe bin\spice2x\spice64.exe bin\spice2x\spice64-pdb.exe bin\spice2x\spice64-pdb.pdb
|
||||
@REM external\cv2pdb\cv2pdb.exe bin\spice2x\spice64.exe bin\spice2x\spice64-pdb.exe bin\spice2x\spice64-pdb.pdb
|
||||
|
||||
+102
-15
@@ -207,22 +207,38 @@ float Analog::applyMultiplier(float value) {
|
||||
}
|
||||
|
||||
float Analog::normalizeAnalogValue(float value) {
|
||||
// effectively the same as fmodf(value, 1.f)
|
||||
// for small values, this is MUCH faster than fmodf.
|
||||
float new_value = value;
|
||||
while (new_value > 1.f) {
|
||||
new_value -= 1.f;
|
||||
if (getType() == GameAPI::Analogs::AnalogType::Circular) {
|
||||
// effectively the same as fmodf(value, 1.f)
|
||||
// for small values, this is MUCH faster than fmodf.
|
||||
float new_value = value;
|
||||
while (new_value > 1.f) {
|
||||
new_value -= 1.f;
|
||||
}
|
||||
while (new_value < 0.f) {
|
||||
new_value += 1.f;
|
||||
}
|
||||
return new_value;
|
||||
|
||||
} else {
|
||||
// clamp to [0, 1] range
|
||||
return std::clamp(value, 0.f, 1.f);
|
||||
}
|
||||
while (new_value < 0.f) {
|
||||
new_value += 1.f;
|
||||
}
|
||||
return new_value;
|
||||
}
|
||||
|
||||
float Analog::applyDeadzone(float raw_value) {
|
||||
float value = raw_value;
|
||||
const auto deadzone = this->getDeadzone();
|
||||
if (deadzone > 0) {
|
||||
auto deadzone = this->getDeadzone();
|
||||
|
||||
// in the past, positive deadzone applied in the center, negative deadzone applied to 0
|
||||
// after each analog value received a type (circular/linear) this has been simpliifed to
|
||||
// positive values only since we can figure out where the rest value is
|
||||
// for back compat, treat negative value as positive
|
||||
if (deadzone < 0.f) {
|
||||
deadzone = -deadzone;
|
||||
}
|
||||
|
||||
// relative mode assumes that user is using a stick, so center is neutral regardless of analog type
|
||||
if (getType() != GameAPI::Analogs::AnalogType::LinearPositive || isRelativeMode()) {
|
||||
|
||||
// calculate values
|
||||
const auto delta = value - 0.5f;
|
||||
@@ -255,7 +271,7 @@ float Analog::applyDeadzone(float raw_value) {
|
||||
}
|
||||
}
|
||||
|
||||
} else if (deadzone < 0) {
|
||||
} else {
|
||||
|
||||
// invert for mirror
|
||||
if (this->getDeadzoneMirror()) {
|
||||
@@ -263,8 +279,8 @@ float Analog::applyDeadzone(float raw_value) {
|
||||
}
|
||||
|
||||
// deadzone from minimum value
|
||||
if (deadzone > -1 && value > -deadzone) {
|
||||
value = std::min(1.f, (value + deadzone) / (1.f + deadzone));
|
||||
if (deadzone < 1.f && deadzone < value) {
|
||||
value = std::max(0.f, (value - deadzone) / (1.f - deadzone));
|
||||
} else {
|
||||
value = 0.f;
|
||||
}
|
||||
@@ -275,4 +291,75 @@ float Analog::applyDeadzone(float raw_value) {
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
float Analog::getRelativeModeValue(float raw_value) {
|
||||
const auto now = get_performance_seconds();
|
||||
auto delta_time = now - this->rel_mode_last_read_time_s;
|
||||
|
||||
if (this->rel_mode_last_read_time_s != 0.f) {
|
||||
// some heuristics to prevent huge jumps:
|
||||
// * if we went for more than 250ms without polls, discard it (e.g., during loading screens)
|
||||
// * cap the delta at 100ms to prevent huge jumps in case of very infrequent polling
|
||||
if (delta_time < 0.f || 0.25f < delta_time) {
|
||||
delta_time = 0.f;
|
||||
} else if (delta_time > 0.1f) {
|
||||
delta_time = 0.1f;
|
||||
}
|
||||
|
||||
// scale [0, 1] to [-1, 1] to simplify calculations
|
||||
const auto delta_raw_value = (raw_value - 0.5f) * 2.f;
|
||||
|
||||
// target is one revolution per second at max speed at 1.0 sensitivity
|
||||
auto adjusted_delta_value = delta_raw_value * delta_time;
|
||||
|
||||
// multiplier / divisor
|
||||
if (this->getMultiplier() > 1) {
|
||||
adjusted_delta_value *= this->getMultiplier();
|
||||
} else if (this->getMultiplier() < -1) {
|
||||
adjusted_delta_value /= -this->getMultiplier();
|
||||
}
|
||||
|
||||
// sensitivity
|
||||
if (this->isSensitivitySet()) {
|
||||
adjusted_delta_value *= this->getSensitivity();
|
||||
}
|
||||
|
||||
// calculate the new absolute value
|
||||
this->rel_mode_absolute_value += adjusted_delta_value;
|
||||
}
|
||||
|
||||
// update for next poll
|
||||
this->rel_mode_last_read_time_s = now;
|
||||
this->rel_mode_absolute_value = normalizeAnalogValue(this->rel_mode_absolute_value);
|
||||
return this->rel_mode_absolute_value;
|
||||
}
|
||||
|
||||
float Analog::getDelayedValue(float raw_value) {
|
||||
const double delay_ms = static_cast<double>(this->getDelayMs());
|
||||
if (delay_ms == 0.0) {
|
||||
return raw_value;
|
||||
}
|
||||
|
||||
// always push a new value
|
||||
const auto now = get_performance_milliseconds();
|
||||
this->delayed_inputs.emplace(now, raw_value);
|
||||
|
||||
// drain the queue down to reasonable length to prevent unconstrained growth
|
||||
// this would accommodate 1 second at ~1000Hz which is overkill
|
||||
// (UI only allows for 250ms of delay)
|
||||
while (this->delayed_inputs.size() > 1024) {
|
||||
this->delayed_inputs.pop();
|
||||
}
|
||||
|
||||
// pop until we find the oldest value still inside the delay window
|
||||
while (this->delayed_inputs.size() > 1) {
|
||||
const auto delta_t = now - this->delayed_inputs.front().time_in_ms;
|
||||
if (delta_t <= delay_ms) {
|
||||
break;
|
||||
}
|
||||
this->delayed_inputs.pop();
|
||||
}
|
||||
|
||||
return this->delayed_inputs.front().value;
|
||||
}
|
||||
|
||||
+46
-18
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <cmath>
|
||||
@@ -13,12 +14,32 @@ namespace rawinput {
|
||||
class RawInputManager;
|
||||
}
|
||||
|
||||
namespace GameAPI::Analogs {
|
||||
enum class AnalogType {
|
||||
// default; values wrap around (below 0 turns into 1, over 1 is 0)
|
||||
// knobs, turntables
|
||||
Circular = 0,
|
||||
|
||||
// typical joystick that rests at the center and caps at [0, 1]
|
||||
LinearCentered = 1,
|
||||
|
||||
// one-directional value (sliders and instruments like piano/drum velocity)
|
||||
// starts at 0 and goes up to 1
|
||||
LinearPositive = 2,
|
||||
};
|
||||
}
|
||||
|
||||
struct AnalogMovingAverage {
|
||||
double time_in_ms;
|
||||
float sine;
|
||||
float cosine;
|
||||
};
|
||||
|
||||
struct AnalogDelayEntry {
|
||||
double time_in_ms;
|
||||
float value;
|
||||
};
|
||||
|
||||
class Analog {
|
||||
private:
|
||||
std::string name;
|
||||
@@ -31,10 +52,11 @@ private:
|
||||
float last_state = 0.5f;
|
||||
bool sensitivity_set = false;
|
||||
bool deadzone_set = false;
|
||||
GameAPI::Analogs::AnalogType type = GameAPI::Analogs::AnalogType::Circular;
|
||||
|
||||
// smoothing function
|
||||
bool smoothing = false;
|
||||
std::array<AnalogMovingAverage, ANALOG_HISTORY_CNT> vector_history;
|
||||
std::array<AnalogMovingAverage, ANALOG_HISTORY_CNT> vector_history = {};
|
||||
int vector_history_index = 0;
|
||||
float smoothed_last_state = 0.f;
|
||||
|
||||
@@ -48,12 +70,13 @@ private:
|
||||
unsigned short divisor_region = 0;
|
||||
|
||||
// relative input mode
|
||||
float absolute_value_for_rel_mode = 0.5f;
|
||||
float rel_mode_absolute_value = 0.5f;
|
||||
float rel_mode_last_read_time_s = 0.f;
|
||||
bool relative_mode = false;
|
||||
|
||||
// circular buffer (delayed input)
|
||||
int delay_buffer_depth = 0;
|
||||
std::queue<float> delay_buffer;
|
||||
// delay
|
||||
uint32_t delay_ms = 0;
|
||||
std::queue<AnalogDelayEntry> delayed_inputs;
|
||||
|
||||
float calculateAngularDifference(float old_rads, float new_rads);
|
||||
float normalizeAngle(float rads);
|
||||
@@ -66,7 +89,8 @@ public:
|
||||
float override_state = 0.5f;
|
||||
|
||||
explicit Analog(std::string name) : name(std::move(name)) {
|
||||
vector_history.fill({0.0, 0.f, 0.f});
|
||||
};
|
||||
explicit Analog(std::string name, GameAPI::Analogs::AnalogType type) : name(std::move(name)), type(type) {
|
||||
};
|
||||
|
||||
std::string getDisplayString(rawinput::RawInputManager* manager);
|
||||
@@ -74,6 +98,8 @@ public:
|
||||
float applyAngularSensitivity(float raw_rads);
|
||||
float applyMultiplier(float raw_value);
|
||||
float applyDeadzone(float raw_value);
|
||||
float getRelativeModeValue(float raw_value);
|
||||
float getDelayedValue(float raw_value);
|
||||
|
||||
inline bool isSet() {
|
||||
if (this->override_enabled) {
|
||||
@@ -90,8 +116,8 @@ public:
|
||||
deadzone_mirror = false;
|
||||
setMultiplier(1);
|
||||
setRelativeMode(false);
|
||||
setDelayBufferDepth(0);
|
||||
setLastState(0.5f);
|
||||
setDelayMs(0);
|
||||
}
|
||||
|
||||
inline void clearBindings() {
|
||||
@@ -193,25 +219,27 @@ public:
|
||||
}
|
||||
|
||||
inline void setRelativeMode(bool relative_mode) {
|
||||
if (relative_mode) {
|
||||
this->smoothing = false;
|
||||
}
|
||||
this->relative_mode = relative_mode;
|
||||
this->absolute_value_for_rel_mode = 0.5f;
|
||||
this->rel_mode_absolute_value = 0.5f;
|
||||
this->rel_mode_last_read_time_s = 0.f;
|
||||
}
|
||||
|
||||
inline float getAbsoluteValue(float relative_delta) {
|
||||
this->absolute_value_for_rel_mode =
|
||||
normalizeAnalogValue(this->absolute_value_for_rel_mode + relative_delta);
|
||||
return this->absolute_value_for_rel_mode;
|
||||
inline GameAPI::Analogs::AnalogType getType() const {
|
||||
return this->type;
|
||||
}
|
||||
|
||||
inline int getDelayBufferDepth() const {
|
||||
return this->delay_buffer_depth;
|
||||
inline void setType(GameAPI::Analogs::AnalogType type) {
|
||||
this->type = type;
|
||||
}
|
||||
|
||||
inline void setDelayBufferDepth(int depth) {
|
||||
this->delay_buffer_depth = depth;
|
||||
inline uint32_t getDelayMs() const {
|
||||
return this->delay_ms;
|
||||
}
|
||||
|
||||
inline std::queue<float> &getDelayBuffer() {
|
||||
return this->delay_buffer;
|
||||
inline void setDelayMs(uint32_t delay_ms) {
|
||||
this->delay_ms = delay_ms;
|
||||
}
|
||||
};
|
||||
|
||||
+121
-58
@@ -199,9 +199,19 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
||||
if (vKey < value_states->size()) {
|
||||
auto value = value_states->at(vKey);
|
||||
if (current_button->getAnalogType() == BAT_POSITIVE) {
|
||||
state = value > 0.6f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
float threshold = 0.6f;
|
||||
if (current_button->getBatThreshold() > 0) {
|
||||
threshold = std::clamp(current_button->getBatThreshold() / 100.f, 0.01f, 0.99f);
|
||||
}
|
||||
state = value > threshold ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
|
||||
} else if (current_button->getAnalogType() == BAT_NEGATIVE) {
|
||||
state = value < 0.4f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
float threshold = 0.4f;
|
||||
if (current_button->getBatThreshold() > 0) {
|
||||
threshold = std::clamp((100 - current_button->getBatThreshold()) / 100.f, 0.01f, 0.99f);
|
||||
}
|
||||
state = value < threshold ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
|
||||
} else {
|
||||
state = value > 0.01f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
}
|
||||
@@ -669,81 +679,98 @@ float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, rawinput::D
|
||||
}
|
||||
|
||||
// deadzone
|
||||
if (analog.isDeadzoneSet()) {
|
||||
// do not apply deadzone to circular analogs since it doesn't make sense (except in relative mode)
|
||||
if (analog.isDeadzoneSet() &&
|
||||
(analog.getType() != AnalogType::Circular || analog.isRelativeMode())) {
|
||||
value = analog.applyDeadzone(value);
|
||||
}
|
||||
|
||||
if (analog.isRelativeMode()) {
|
||||
float relative_delta = value - 0.5f;
|
||||
// built-in scaling to make values reasonable
|
||||
relative_delta /= 80.f;
|
||||
if (analog.getType() == AnalogType::Circular) {
|
||||
|
||||
// integer multiplier/divisor
|
||||
const auto mult = analog.getMultiplier();
|
||||
if (mult < -1) {
|
||||
relative_delta /= -mult;
|
||||
} else if (1 < mult) {
|
||||
relative_delta *= mult;
|
||||
}
|
||||
if (analog.isRelativeMode()) {
|
||||
value = analog.getRelativeModeValue(value);
|
||||
|
||||
// sensitivity (ranges from 0.0 to 4.0)
|
||||
if (analog.isSensitivitySet()) {
|
||||
relative_delta *= analog.getSensitivity();
|
||||
}
|
||||
} else {
|
||||
// integer multiplier
|
||||
value = analog.applyMultiplier(value);
|
||||
|
||||
// translate relative movement to absolute value
|
||||
value = analog.getAbsoluteValue(relative_delta);
|
||||
// smoothing/sensitivity
|
||||
if (analog.getSmoothing() || analog.isSensitivitySet()) {
|
||||
float rads = value * (float) M_TAU;
|
||||
|
||||
} else {
|
||||
// integer multiplier
|
||||
value = analog.applyMultiplier(value);
|
||||
// smoothing
|
||||
if (analog.getSmoothing()) {
|
||||
|
||||
// smoothing/sensitivity
|
||||
if (analog.getSmoothing() || analog.isSensitivitySet()) {
|
||||
float rads = value * (float) M_TAU;
|
||||
// preserve direction
|
||||
if (rads >= M_TAU) {
|
||||
rads -= 0.0001f;
|
||||
}
|
||||
|
||||
// smoothing
|
||||
if (analog.getSmoothing()) {
|
||||
|
||||
// preserve direction
|
||||
if (rads >= M_TAU) {
|
||||
rads -= 0.0001f;
|
||||
// calculate angle
|
||||
rads = analog.getSmoothedValue(rads);
|
||||
}
|
||||
|
||||
// calculate angle
|
||||
rads = analog.getSmoothedValue(rads);
|
||||
// sensitivity
|
||||
if (analog.isSensitivitySet()) {
|
||||
rads = analog.applyAngularSensitivity(rads);
|
||||
}
|
||||
|
||||
// apply to value
|
||||
value = rads * (float) M_1_TAU;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// sensitivity
|
||||
if (analog.isSensitivitySet()) {
|
||||
// adjust curve
|
||||
// values < 1.f : less sensitive around neutral
|
||||
// values > 1.f : more sensitive around neutral
|
||||
float curve = analog.getSensitivity();
|
||||
if (curve <= 0.f) {
|
||||
curve = 0.01f;
|
||||
}
|
||||
curve = 1.f / curve;
|
||||
|
||||
if (analog.getType() == AnalogType::LinearCentered) {
|
||||
// convert 0.0..1.0 to -1.0..+1.0
|
||||
float signed_raw = (value - 0.5f) * 2.0f;
|
||||
// apply curve
|
||||
float sign = signed_raw < 0.0f ? -1.0f : 1.0f;
|
||||
float magnitude = fabsf(signed_raw);
|
||||
float curved = sign * powf(magnitude, curve);
|
||||
// convert back to 0.0..1.0
|
||||
value = curved * 0.5f + 0.5f;
|
||||
} else {
|
||||
value = powf(value, curve);
|
||||
}
|
||||
|
||||
// sensitivity
|
||||
if (analog.isSensitivitySet()) {
|
||||
rads = analog.applyAngularSensitivity(rads);
|
||||
value = std::clamp(value, 0.f, 1.f);
|
||||
}
|
||||
|
||||
// multiplier / divisor
|
||||
if (analog.getMultiplier() < -1) {
|
||||
if (analog.getType() == AnalogType::LinearCentered) {
|
||||
value = (value - 0.5f) / (-analog.getMultiplier()) + 0.5f;
|
||||
} else {
|
||||
value /= -analog.getMultiplier();
|
||||
}
|
||||
|
||||
// apply to value
|
||||
value = rads * (float) M_1_TAU;
|
||||
value = std::clamp(value, 0.f, 1.f);
|
||||
|
||||
} else if (analog.getMultiplier() > 1) {
|
||||
if (analog.getType() == AnalogType::LinearCentered) {
|
||||
value = (value - 0.5f) * analog.getMultiplier() + 0.5f;
|
||||
} else {
|
||||
value *= analog.getMultiplier();
|
||||
}
|
||||
|
||||
value = std::clamp(value, 0.f, 1.f);
|
||||
}
|
||||
}
|
||||
|
||||
// delay
|
||||
if (0 < analog.getDelayBufferDepth()) {
|
||||
auto& queue = analog.getDelayBuffer();
|
||||
|
||||
// ensure the queue isn't too long; drop old values
|
||||
while (analog.getDelayBufferDepth() <= (int)queue.size()) {
|
||||
queue.pop();
|
||||
}
|
||||
|
||||
// always push new value
|
||||
queue.push(value);
|
||||
|
||||
// get a new value to return
|
||||
if ((int)queue.size() < analog.getDelayBufferDepth()) {
|
||||
// not enough in the queue, stall for now, shouldn't happen often
|
||||
value = analog.getLastState();
|
||||
} else {
|
||||
value = queue.front();
|
||||
queue.pop();
|
||||
}
|
||||
if (analog.getDelayMs() > 0) {
|
||||
value = analog.getDelayedValue(value);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -829,6 +856,42 @@ std::vector<Analog> GameAPI::Analogs::sortAnalogs(
|
||||
return sorted;
|
||||
}
|
||||
|
||||
static std::vector<Analog> sortAnalogsWithTypeInternal(
|
||||
std::vector<Analog> &analogs,
|
||||
const std::initializer_list<GameAPI::Analogs::AnalogWithType> list) {
|
||||
|
||||
std::vector<Analog> sorted;
|
||||
|
||||
bool analog_found;
|
||||
for (auto &a : list) {
|
||||
analog_found = false;
|
||||
|
||||
for (auto &analog : analogs) {
|
||||
if (a.name == analog.getName()) {
|
||||
analog_found = true;
|
||||
analog.setType(a.type);
|
||||
sorted.push_back(analog);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!analog_found) {
|
||||
sorted.emplace_back(a.name, a.type);
|
||||
}
|
||||
}
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
void GameAPI::Analogs::sortAnalogsWithType(
|
||||
std::vector<Analog> *analogs,
|
||||
const std::initializer_list<AnalogWithType> list) {
|
||||
|
||||
if (analogs) {
|
||||
*analogs = sortAnalogsWithTypeInternal(*analogs, list);
|
||||
}
|
||||
}
|
||||
|
||||
float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, Analog &analog) {
|
||||
|
||||
// check override
|
||||
|
||||
@@ -85,14 +85,15 @@ namespace GameAPI {
|
||||
const std::vector<Analog> &analogs,
|
||||
const std::vector<std::string> &analog_names);
|
||||
|
||||
template<typename T>
|
||||
void sortAnalogs(std::vector<Analog> *analogs, T t) {
|
||||
const std::vector<std::string> analog_names { t };
|
||||
struct AnalogWithType {
|
||||
const std::string name;
|
||||
const AnalogType type;
|
||||
|
||||
if (analogs) {
|
||||
*analogs = GameAPI::Analogs::sortAnalogs(*analogs, analog_names);
|
||||
}
|
||||
}
|
||||
AnalogWithType(std::string name, AnalogType type) :
|
||||
name(std::move(name)), type(type) {}
|
||||
};
|
||||
|
||||
void sortAnalogsWithType(std::vector<Analog> *analogs, const std::initializer_list<AnalogWithType> list);
|
||||
|
||||
template<typename T, typename... Rest>
|
||||
void sortAnalogs(std::vector<Analog> *analogs, T t, Rest... rest) {
|
||||
|
||||
@@ -306,7 +306,10 @@ std::string Button::getDisplayString(rawinput::RawInputManager* manager) {
|
||||
std::string vKeyString = fmt::format("{:#x}", vKey);
|
||||
|
||||
// device must be existing
|
||||
if (this->device_identifier.empty() && vKey == INVALID_VKEY) {
|
||||
if (this->isNaive() && vKey == INVALID_VKEY) {
|
||||
if (this->getInvert()) {
|
||||
return "(always on)";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ private:
|
||||
ButtonAnalogType analog_type = BAT_NONE;
|
||||
double debounce_up = 0.0;
|
||||
double debounce_down = 0.0;
|
||||
int bat_threshold = 0; // for positive/negative only, value in percentage, zero is default
|
||||
bool invert = false;
|
||||
bool is_temporary = false;
|
||||
|
||||
@@ -109,6 +110,7 @@ public:
|
||||
alternatives.clear();
|
||||
device_identifier = "";
|
||||
analog_type = BAT_NONE;
|
||||
bat_threshold = 0;
|
||||
}
|
||||
|
||||
std::string getDisplayString(rawinput::RawInputManager* manager);
|
||||
@@ -169,6 +171,14 @@ public:
|
||||
this->debounce_down = debounce_time_down;
|
||||
}
|
||||
|
||||
inline void setBatThreshold(int bat_threshold) {
|
||||
this->bat_threshold = bat_threshold;
|
||||
}
|
||||
|
||||
inline int getBatThreshold() const {
|
||||
return this->bat_threshold;
|
||||
}
|
||||
|
||||
inline bool getInvert() const {
|
||||
return this->invert;
|
||||
}
|
||||
|
||||
+38
-19
@@ -28,7 +28,7 @@ Config::Config() {
|
||||
this->status = false;
|
||||
if (CONFIG_PATH_OVERRIDE.length() > 0) {
|
||||
this->configLocation = CONFIG_PATH_OVERRIDE;
|
||||
log_info("cfg", "using custom config file: {}", this->configLocation.string());
|
||||
log_info("cfg", "using custom config file: {}", this->configLocation);
|
||||
} else {
|
||||
this->configLocation = std::filesystem::path(_wgetenv(L"APPDATA")) / L"spicetools.xml";
|
||||
// avoids logging the expanded appdata path as it contains user name
|
||||
@@ -57,7 +57,7 @@ Config::Config() {
|
||||
this->firstFillConfigFile();
|
||||
break;
|
||||
case tinyxml2::XMLError::XML_ERROR_FILE_COULD_NOT_BE_OPENED:
|
||||
log_fatal("cfg", "could not open config file: {}", this->configLocation.string());
|
||||
log_fatal("cfg", "could not open config file: {}", this->configLocation);
|
||||
break;
|
||||
case tinyxml2::XMLError::XML_ERROR_FILE_NOT_FOUND:
|
||||
this->createConfigFile();
|
||||
@@ -72,7 +72,7 @@ Config::Config() {
|
||||
case tinyxml2::XMLError::XML_ERROR_PARSING_UNKNOWN:
|
||||
case tinyxml2::XMLError::XML_ERROR_MISMATCHED_ELEMENT:
|
||||
case tinyxml2::XMLError::XML_ERROR_PARSING:
|
||||
log_warning("cfg", "Couldn't read config file: {}", this->configLocation.string());
|
||||
log_warning("cfg", "Couldn't read config file: {}", this->configLocation);
|
||||
this->createConfigFile();
|
||||
this->firstFillConfigFile();
|
||||
break;
|
||||
@@ -98,7 +98,7 @@ bool Config::getStatus() {
|
||||
return this->status;
|
||||
}
|
||||
|
||||
bool Config::addGame(Game &game) {
|
||||
bool Config::addGame(Game &game, bool save) {
|
||||
tinyxml2::XMLNode *rootNode = this->configFile.LastChild();
|
||||
tinyxml2::XMLElement *gameNodes = rootNode->FirstChildElement("game");
|
||||
|
||||
@@ -162,6 +162,7 @@ bool Config::addGame(Game &game) {
|
||||
auto analogType = (int) BAT_NONE;
|
||||
double debounce_up = 0.0;
|
||||
double debounce_down = 0.0;
|
||||
int bat_threshold = 0;
|
||||
int velocity_threshold = 0;
|
||||
bool invert = false;
|
||||
tinyxml2::XMLError attrError = gameButtonNode->QueryIntAttribute("vkey", &vKey);
|
||||
@@ -169,6 +170,7 @@ bool Config::addGame(Game &game) {
|
||||
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
|
||||
gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up);
|
||||
gameButtonNode->QueryDoubleAttribute("debounce_down", &debounce_down);
|
||||
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
|
||||
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
|
||||
gameButtonNode->QueryBoolAttribute("invert", &invert);
|
||||
if (attrError != tinyxml2::XMLError::XML_SUCCESS) {
|
||||
@@ -180,6 +182,7 @@ bool Config::addGame(Game &game) {
|
||||
gameButtonNode->SetAttribute("devid", button->getDeviceIdentifier().c_str());
|
||||
gameButtonNode->SetAttribute("debounce_up", debounce_up);
|
||||
gameButtonNode->SetAttribute("debounce_down", debounce_down);
|
||||
gameButtonNode->SetAttribute("bat_threshold", bat_threshold);
|
||||
gameButtonNode->SetAttribute("velocity_threshold", velocity_threshold);
|
||||
gameButtonNode->SetAttribute("invert", invert);
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
@@ -188,6 +191,7 @@ bool Config::addGame(Game &game) {
|
||||
button->setAnalogType((ButtonAnalogType) analogType);
|
||||
button->setDebounceUp(debounce_up);
|
||||
button->setDebounceDown(debounce_down);
|
||||
button->setBatThreshold(bat_threshold);
|
||||
button->setVelocityThreshold(velocity_threshold);
|
||||
button->setInvert(invert);
|
||||
if (devid) {
|
||||
@@ -207,6 +211,7 @@ bool Config::addGame(Game &game) {
|
||||
gameButtonNode->SetAttribute("analogtype", (int) it.getAnalogType());
|
||||
gameButtonNode->SetAttribute("debounce_up", it.getDebounceUp());
|
||||
gameButtonNode->SetAttribute("debounce_down", it.getDebounceDown());
|
||||
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
|
||||
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
|
||||
gameButtonNode->SetAttribute("invert", it.getInvert());
|
||||
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
|
||||
@@ -248,7 +253,7 @@ bool Config::addGame(Game &game) {
|
||||
bool smoothing = false;
|
||||
int multiplier = 1;
|
||||
bool relative_mode = false;
|
||||
int delay_buffer_depth = 0;
|
||||
uint32_t delay = 0;
|
||||
tinyxml2::XMLError err1 = gameAnalogNode->QueryIntAttribute("index", &index);
|
||||
gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity);
|
||||
gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone);
|
||||
@@ -257,7 +262,7 @@ bool Config::addGame(Game &game) {
|
||||
gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing);
|
||||
gameAnalogNode->QueryIntAttribute("multiplier", &multiplier);
|
||||
gameAnalogNode->QueryBoolAttribute("relative", &relative_mode);
|
||||
gameAnalogNode->QueryIntAttribute("delay", &delay_buffer_depth);
|
||||
gameAnalogNode->QueryUnsignedAttribute("delay_ms", &delay);
|
||||
const char *devid = gameAnalogNode->Attribute("devid");
|
||||
|
||||
if (err1 != tinyxml2::XMLError::XML_SUCCESS || !devid) {
|
||||
@@ -273,7 +278,7 @@ bool Config::addGame(Game &game) {
|
||||
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
|
||||
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
|
||||
gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
|
||||
gameAnalogNode->SetAttribute("delay", it.getDelayBufferDepth());
|
||||
gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
|
||||
gameAnalogsNode->InsertEndChild(gameAnalogNode);
|
||||
} else {
|
||||
it.setIndex(static_cast<unsigned short int>(index));
|
||||
@@ -285,7 +290,7 @@ bool Config::addGame(Game &game) {
|
||||
it.setSmoothing(smoothing);
|
||||
it.setMultiplier(multiplier);
|
||||
it.setRelativeMode(relative_mode);
|
||||
it.setDelayBufferDepth(delay_buffer_depth);
|
||||
it.setDelayMs(delay);
|
||||
}
|
||||
} else {
|
||||
gameAnalogNode = this->configFile.NewElement("analog");
|
||||
@@ -298,7 +303,7 @@ bool Config::addGame(Game &game) {
|
||||
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
|
||||
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
|
||||
gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
|
||||
gameAnalogNode->SetAttribute("delay", it.getDelayBufferDepth());
|
||||
gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
|
||||
gameAnalogNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
|
||||
gameAnalogsNode->InsertEndChild(gameAnalogNode);
|
||||
}
|
||||
@@ -432,6 +437,7 @@ bool Config::addGame(Game &game) {
|
||||
gameButtonNode->SetAttribute("analogtype", it.getAnalogType());
|
||||
gameButtonNode->SetAttribute("debounce_up", it.getDebounceUp());
|
||||
gameButtonNode->SetAttribute("debounce_down", it.getDebounceDown());
|
||||
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
|
||||
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
|
||||
gameButtonNode->SetAttribute("invert", it.getInvert());
|
||||
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
|
||||
@@ -452,7 +458,7 @@ bool Config::addGame(Game &game) {
|
||||
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
|
||||
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
|
||||
gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
|
||||
gameAnalogNode->SetAttribute("delay", it.getDelayBufferDepth());
|
||||
gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
|
||||
gameAnalogsNode->InsertEndChild(gameAnalogNode);
|
||||
}
|
||||
|
||||
@@ -480,13 +486,20 @@ bool Config::addGame(Game &game) {
|
||||
rootNode->InsertEndChild(gameNode);
|
||||
}
|
||||
|
||||
// save config
|
||||
this->saveConfigFile();
|
||||
// save config (skipped when caller batches multiple addGame calls
|
||||
// and flushes once at the end via Config::save())
|
||||
if (save) {
|
||||
this->saveConfigFile();
|
||||
}
|
||||
|
||||
// return success
|
||||
return true;
|
||||
}
|
||||
|
||||
void Config::save() {
|
||||
this->saveConfigFile();
|
||||
}
|
||||
|
||||
bool Config::updateBinding(const Game &game, const Button &button, int alternative) {
|
||||
|
||||
// get root node
|
||||
@@ -535,6 +548,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
|
||||
gameButtonNode->SetAttribute("analogtype", (int) button.getAnalogType());
|
||||
gameButtonNode->SetAttribute("debounce_up", button.getDebounceUp());
|
||||
gameButtonNode->SetAttribute("debounce_down", button.getDebounceDown());
|
||||
gameButtonNode->SetAttribute("bat_threshold", button.getBatThreshold());
|
||||
gameButtonNode->SetAttribute("velocity_threshold", button.getVelocityThreshold());
|
||||
gameButtonNode->SetAttribute("invert", button.getInvert());
|
||||
gameButtonNode->SetAttribute("devid", button.getDeviceIdentifier().c_str());
|
||||
@@ -551,6 +565,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
|
||||
gameButtonNode->SetAttribute("analogtype", 0);
|
||||
gameButtonNode->SetAttribute("debounce_up", 0.0);
|
||||
gameButtonNode->SetAttribute("debounce_down", 0.0);
|
||||
gameButtonNode->SetAttribute("bat_threshold", 0);
|
||||
gameButtonNode->SetAttribute("velocity_threshold", 0);
|
||||
gameButtonNode->SetAttribute("invert", false);
|
||||
gameButtonNode->SetAttribute("devid", "");
|
||||
@@ -684,7 +699,7 @@ bool Config::updateBinding(const Game &game, const Analog &analog) {
|
||||
gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing());
|
||||
gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier());
|
||||
gameAnalogNode->SetAttribute("relative", analog.isRelativeMode());
|
||||
gameAnalogNode->SetAttribute("delay", analog.getDelayBufferDepth());
|
||||
gameAnalogNode->SetAttribute("delay_ms", analog.getDelayMs());
|
||||
gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str());
|
||||
} else {
|
||||
gameAnalogNode = this->configFile.NewElement("analog");
|
||||
@@ -696,7 +711,7 @@ bool Config::updateBinding(const Game &game, const Analog &analog) {
|
||||
gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing());
|
||||
gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier());
|
||||
gameAnalogNode->SetAttribute("relative", analog.isRelativeMode());
|
||||
gameAnalogNode->SetAttribute("delay", analog.getDelayBufferDepth());
|
||||
gameAnalogNode->SetAttribute("delay_ms", analog.getDelayMs());
|
||||
gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str());
|
||||
gameAnalogsNode->InsertEndChild(gameAnalogNode);
|
||||
}
|
||||
@@ -946,12 +961,14 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
auto analogType = (int) BAT_NONE;
|
||||
double debounce_up = 0.0;
|
||||
double debounce_down = 0.0;
|
||||
int bat_threshold = 0;
|
||||
int velocity_threshold = 0;
|
||||
bool invert = false;
|
||||
gameButtonNode->QueryIntAttribute("vkey", &vKey);
|
||||
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
|
||||
gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up);
|
||||
gameButtonNode->QueryDoubleAttribute("debounce_down", &debounce_down);
|
||||
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
|
||||
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
|
||||
gameButtonNode->QueryBoolAttribute("invert", &invert);
|
||||
const char *devid = gameButtonNode->Attribute("devid");
|
||||
@@ -966,6 +983,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
alt.setAnalogType((ButtonAnalogType) analogType);
|
||||
alt.setDebounceUp(debounce_up);
|
||||
alt.setDebounceDown(debounce_down);
|
||||
alt.setBatThreshold(bat_threshold);
|
||||
alt.setVelocityThreshold(velocity_threshold);
|
||||
alt.setInvert(invert);
|
||||
if (devid) {
|
||||
@@ -984,6 +1002,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
button.setAnalogType((ButtonAnalogType) analogType);
|
||||
button.setDebounceUp(debounce_up);
|
||||
button.setDebounceDown(debounce_down);
|
||||
button.setBatThreshold(bat_threshold);
|
||||
button.setVelocityThreshold(velocity_threshold);
|
||||
button.setInvert(invert);
|
||||
if (devid) {
|
||||
@@ -1130,7 +1149,7 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
|
||||
bool smoothing = false;
|
||||
int multiplier = 1;
|
||||
bool relative_mode = false;
|
||||
int delay_buffer_depth = 0;
|
||||
uint32_t delay = 0;
|
||||
gameAnalogNode->QueryIntAttribute("index", &index);
|
||||
gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity);
|
||||
gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone);
|
||||
@@ -1139,7 +1158,7 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
|
||||
gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing);
|
||||
gameAnalogNode->QueryIntAttribute("multiplier", &multiplier);
|
||||
gameAnalogNode->QueryBoolAttribute("relative", &relative_mode);
|
||||
gameAnalogNode->QueryIntAttribute("delay", &delay_buffer_depth);
|
||||
gameAnalogNode->QueryUnsignedAttribute("delay_ms", &delay);
|
||||
const char *devid = gameAnalogNode->Attribute("devid");
|
||||
|
||||
// create analog and add to list
|
||||
@@ -1152,7 +1171,7 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
|
||||
analog.setSmoothing(smoothing);
|
||||
analog.setMultiplier(multiplier);
|
||||
analog.setRelativeMode(relative_mode);
|
||||
analog.setDelayBufferDepth(delay_buffer_depth);
|
||||
analog.setDelayMs(delay);
|
||||
if (devid) {
|
||||
analog.setDeviceIdentifier(devid);
|
||||
}
|
||||
@@ -1315,7 +1334,7 @@ void Config::saveConfigFile() {
|
||||
// create a .tmp file and write to it...
|
||||
const auto xml_result = this->configFile.SaveFile(this->configLocationTemp.c_str(), false);
|
||||
if (xml_result != tinyxml2::XMLError::XML_SUCCESS) {
|
||||
log_info("cfg", "failed to write file: {}", this->configLocationTemp.string());
|
||||
log_info("cfg", "failed to write file: {}", this->configLocationTemp);
|
||||
return;
|
||||
}
|
||||
// copy the .tmp file to the main file...
|
||||
@@ -1325,4 +1344,4 @@ void Config::saveConfigFile() {
|
||||
}
|
||||
// delete the .tmp file (not critical if this fails)
|
||||
DeleteFileW(this->configLocationTemp.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,12 @@ public:
|
||||
bool getStatus();
|
||||
bool createConfigFile();
|
||||
|
||||
bool addGame(Game &game);
|
||||
bool addGame(Game &game, bool save = true);
|
||||
|
||||
// flush pending in-memory changes to disk; intended to be paired with
|
||||
// addGame(game, false) in bulk-init paths so we write the XML once instead
|
||||
// of once per game
|
||||
void save();
|
||||
|
||||
bool updateBinding(const Game &game, const Button &button, int alternative);
|
||||
bool updateBinding(const Game &game, const Analog &analog);
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
#include "configurator.h"
|
||||
#include "configurator.h"
|
||||
|
||||
#include <d3d9.h>
|
||||
|
||||
#include "overlay/overlay.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace cfg {
|
||||
|
||||
// globals
|
||||
bool CONFIGURATOR_STANDALONE = false;
|
||||
bool CONFIGURATOR_FORCE_SOFTWARE_RENDER = false;
|
||||
ConfigType CONFIGURATOR_TYPE = ConfigType::Config;
|
||||
|
||||
Configurator::Configurator() {
|
||||
@@ -16,11 +20,78 @@ namespace cfg {
|
||||
CONFIGURATOR_STANDALONE = false;
|
||||
}
|
||||
|
||||
// Attempt to bring up a D3D9 device backing the configurator's window so the
|
||||
// overlay can use the hardware-accelerated imgui_impl_dx9 path instead of
|
||||
// the CPU rasterizer. Returns true if both Direct3DCreate9 and CreateDevice
|
||||
// succeed; the caller falls back to overlay::create_software() otherwise so
|
||||
// that environments without D3D9 (Wine without dxvk, headless test boxes,
|
||||
// GPUs whose drivers reject HAL) still get a working configurator.
|
||||
static bool try_init_d3d9(ConfiguratorWindow &wnd) {
|
||||
if (cfg::CONFIGURATOR_FORCE_SOFTWARE_RENDER) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (wnd.hWnd == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
IDirect3D9 *d3d = Direct3DCreate9(D3D_SDK_VERSION);
|
||||
if (d3d == nullptr) {
|
||||
log_warning("configurator", "Direct3DCreate9 returned NULL, falling back to software renderer");
|
||||
return false;
|
||||
}
|
||||
|
||||
D3DPRESENT_PARAMETERS pp {};
|
||||
pp.Windowed = TRUE;
|
||||
pp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||
// D3DFMT_UNKNOWN -> driver picks the current desktop format
|
||||
pp.BackBufferFormat = D3DFMT_UNKNOWN;
|
||||
pp.BackBufferWidth = static_cast<UINT>(wnd.client_width);
|
||||
pp.BackBufferHeight = static_cast<UINT>(wnd.client_height);
|
||||
pp.hDeviceWindow = wnd.hWnd;
|
||||
pp.EnableAutoDepthStencil = FALSE;
|
||||
pp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
|
||||
|
||||
IDirect3DDevice9 *device = nullptr;
|
||||
// SOFTWARE_VERTEXPROCESSING is the most compatible behavior flag; the UI
|
||||
// is tiny so we don't need hardware T&L. FPU_PRESERVE keeps our floating
|
||||
// point environment intact in case other spice code relies on it.
|
||||
const DWORD behavior_flags = D3DCREATE_SOFTWARE_VERTEXPROCESSING | D3DCREATE_FPU_PRESERVE;
|
||||
HRESULT hr = d3d->CreateDevice(
|
||||
D3DADAPTER_DEFAULT,
|
||||
D3DDEVTYPE_HAL,
|
||||
wnd.hWnd,
|
||||
behavior_flags,
|
||||
&pp,
|
||||
&device);
|
||||
if (FAILED(hr) || device == nullptr) {
|
||||
log_warning("configurator",
|
||||
"D3D9 CreateDevice failed (hr={:#x}), falling back to software renderer",
|
||||
static_cast<unsigned int>(hr));
|
||||
d3d->Release();
|
||||
return false;
|
||||
}
|
||||
|
||||
wnd.d3d = d3d;
|
||||
wnd.device = device;
|
||||
wnd.pp = pp;
|
||||
wnd.use_d3d9 = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Configurator::run() {
|
||||
|
||||
// create instance
|
||||
// bring up the overlay against either a real D3D9 device or the software
|
||||
// rasterizer. The choice is one-shot - the in-game overlay always uses
|
||||
// the same renderer the configurator picked here.
|
||||
overlay::ENABLED = true;
|
||||
overlay::create_software(this->wnd.hWnd);
|
||||
if (try_init_d3d9(this->wnd)) {
|
||||
log_info("configurator", "using D3D9 hardware-accelerated renderer");
|
||||
overlay::create_d3d9(this->wnd.hWnd, this->wnd.d3d, this->wnd.device);
|
||||
} else {
|
||||
log_info("configurator", "using software renderer");
|
||||
overlay::create_software(this->wnd.hWnd);
|
||||
}
|
||||
overlay::OVERLAY->set_active(true);
|
||||
overlay::OVERLAY->hotkeys_enable = false;
|
||||
ImGui::GetIO().MouseDrawCursor = false;
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace cfg {
|
||||
// globals
|
||||
extern bool CONFIGURATOR_STANDALONE;
|
||||
extern ConfigType CONFIGURATOR_TYPE;
|
||||
extern bool CONFIGURATOR_FORCE_SOFTWARE_RENDER;
|
||||
|
||||
class Configurator {
|
||||
private:
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
#include "configurator_wnd.h"
|
||||
#include "configurator_wnd.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include <windows.h>
|
||||
#include <windowsx.h>
|
||||
|
||||
#include "build/defs.h"
|
||||
#include "external/imgui/imgui.h"
|
||||
#include "launcher/shutdown.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "cfg/configurator.h"
|
||||
|
||||
#include "icon.h"
|
||||
@@ -16,6 +22,156 @@ static int WINDOW_SIZE_X = 800;
|
||||
static int WINDOW_SIZE_Y = 600;
|
||||
static HICON WINDOW_ICON = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(MAINICON));
|
||||
|
||||
static const UINT_PTR RENDER_TIMER_ID = 1;
|
||||
|
||||
// Map a virtual key code to the matching ImGuiKey. Mirrors the table used by
|
||||
// the in-game ImGui spice backend so navigation keys behave identically when
|
||||
// the configurator runs standalone and drives ImGui from Win32 messages.
|
||||
static ImGuiKey vk_to_imgui_key(WPARAM vkey) {
|
||||
switch (vkey) {
|
||||
case VK_TAB: return ImGuiKey_Tab;
|
||||
case VK_LEFT: return ImGuiKey_LeftArrow;
|
||||
case VK_RIGHT: return ImGuiKey_RightArrow;
|
||||
case VK_UP: return ImGuiKey_UpArrow;
|
||||
case VK_DOWN: return ImGuiKey_DownArrow;
|
||||
case VK_PRIOR: return ImGuiKey_PageUp;
|
||||
case VK_NEXT: return ImGuiKey_PageDown;
|
||||
case VK_HOME: return ImGuiKey_Home;
|
||||
case VK_END: return ImGuiKey_End;
|
||||
case VK_INSERT: return ImGuiKey_Insert;
|
||||
case VK_DELETE: return ImGuiKey_Delete;
|
||||
case VK_BACK: return ImGuiKey_Backspace;
|
||||
case VK_SPACE: return ImGuiKey_Space;
|
||||
case VK_RETURN: return ImGuiKey_Enter;
|
||||
case VK_ESCAPE: return ImGuiKey_Escape;
|
||||
case VK_LSHIFT: return ImGuiKey_LeftShift;
|
||||
case VK_RSHIFT: return ImGuiKey_RightShift;
|
||||
case VK_SHIFT: return ImGuiKey_LeftShift;
|
||||
case VK_LCONTROL: return ImGuiKey_LeftCtrl;
|
||||
case VK_RCONTROL: return ImGuiKey_RightCtrl;
|
||||
case VK_CONTROL: return ImGuiKey_LeftCtrl;
|
||||
case 'A': return ImGuiKey_A;
|
||||
case 'C': return ImGuiKey_C;
|
||||
case 'V': return ImGuiKey_V;
|
||||
case 'X': return ImGuiKey_X;
|
||||
case 'Y': return ImGuiKey_Y;
|
||||
case 'Z': return ImGuiKey_Z;
|
||||
default: return ImGuiKey_None;
|
||||
}
|
||||
}
|
||||
|
||||
static ImGuiKey vk_to_imgui_mod_key(WPARAM vkey) {
|
||||
switch (vkey) {
|
||||
case VK_SHIFT:
|
||||
case VK_LSHIFT:
|
||||
case VK_RSHIFT:
|
||||
return ImGuiMod_Shift;
|
||||
case VK_CONTROL:
|
||||
case VK_LCONTROL:
|
||||
case VK_RCONTROL:
|
||||
return ImGuiMod_Ctrl;
|
||||
case VK_MENU:
|
||||
case VK_LMENU:
|
||||
case VK_RMENU:
|
||||
return ImGuiMod_Alt;
|
||||
default:
|
||||
return ImGuiMod_None;
|
||||
}
|
||||
}
|
||||
|
||||
static cfg::ConfiguratorWindow *get_state(HWND hWnd) {
|
||||
return reinterpret_cast<cfg::ConfiguratorWindow *>(
|
||||
GetWindowLongPtrW(hWnd, GWLP_USERDATA));
|
||||
}
|
||||
|
||||
// Returns the refresh rate (Hz) of the monitor the window currently lives on,
|
||||
// clamped to a sane range. Falls back to 60 if detection fails or the value
|
||||
// looks invalid (DEVMODE may report 0 or 1 to mean "use hardware default").
|
||||
static UINT detect_monitor_refresh_hz(HWND hWnd) {
|
||||
UINT hz = 0;
|
||||
HMONITOR mon = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST);
|
||||
if (mon) {
|
||||
MONITORINFOEXW mi {};
|
||||
mi.cbSize = sizeof(mi);
|
||||
if (GetMonitorInfoW(mon, reinterpret_cast<LPMONITORINFO>(&mi))) {
|
||||
DEVMODEW dm {};
|
||||
dm.dmSize = sizeof(dm);
|
||||
if (EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm)) {
|
||||
hz = dm.dmDisplayFrequency;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hz <= 1) {
|
||||
HDC hdc = GetDC(hWnd);
|
||||
if (hdc) {
|
||||
int v = GetDeviceCaps(hdc, VREFRESH);
|
||||
if (v > 1) {
|
||||
hz = static_cast<UINT>(v);
|
||||
}
|
||||
ReleaseDC(hWnd, hdc);
|
||||
}
|
||||
}
|
||||
if (hz < 30 || hz > 240) {
|
||||
hz = 60;
|
||||
}
|
||||
return hz;
|
||||
}
|
||||
|
||||
// Software-path WM_PAINT: blit the overlay's pixel buffer to the window using
|
||||
// SetDIBitsToDevice. Avoids creating/destroying a GDI HBITMAP every frame.
|
||||
static void paint_software(HWND hWnd) {
|
||||
if (!overlay::OVERLAY) {
|
||||
PAINTSTRUCT ps {};
|
||||
BeginPaint(hWnd, &ps);
|
||||
EndPaint(hWnd, &ps);
|
||||
return;
|
||||
}
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
uint32_t *pixel_data = overlay::OVERLAY->sw_get_pixel_data(&width, &height);
|
||||
|
||||
PAINTSTRUCT paint {};
|
||||
HDC hdc = BeginPaint(hWnd, &paint);
|
||||
|
||||
if (pixel_data && width > 0 && height > 0) {
|
||||
BITMAPINFO bmi {};
|
||||
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
bmi.bmiHeader.biWidth = width;
|
||||
// negative height -> top-down DIB, matching how ImGui packs pixels
|
||||
bmi.bmiHeader.biHeight = -height;
|
||||
bmi.bmiHeader.biPlanes = 1;
|
||||
bmi.bmiHeader.biBitCount = 32;
|
||||
bmi.bmiHeader.biCompression = BI_RGB;
|
||||
|
||||
SetDIBitsToDevice(
|
||||
hdc,
|
||||
0, 0,
|
||||
width, height,
|
||||
0, 0,
|
||||
0, height,
|
||||
pixel_data,
|
||||
&bmi,
|
||||
DIB_RGB_COLORS);
|
||||
}
|
||||
|
||||
EndPaint(hWnd, &paint);
|
||||
}
|
||||
|
||||
void cfg::ConfiguratorWindow::start_timer() {
|
||||
if (!this->timer_running && this->hWnd) {
|
||||
SetTimer(this->hWnd, RENDER_TIMER_ID, this->timer_interval_ms, nullptr);
|
||||
this->timer_running = true;
|
||||
}
|
||||
}
|
||||
|
||||
void cfg::ConfiguratorWindow::stop_timer() {
|
||||
if (this->timer_running && this->hWnd) {
|
||||
KillTimer(this->hWnd, RENDER_TIMER_ID);
|
||||
this->timer_running = false;
|
||||
}
|
||||
}
|
||||
|
||||
cfg::ConfiguratorWindow::ConfiguratorWindow() {
|
||||
|
||||
// register the window class
|
||||
@@ -27,12 +183,22 @@ cfg::ConfiguratorWindow::ConfiguratorWindow() {
|
||||
wc.hIcon = WINDOW_ICON;
|
||||
RegisterClass(&wc);
|
||||
|
||||
// raise SetTimer resolution so high refresh rates (120/144Hz) are actually
|
||||
// achievable. Without this, USER timers quantize to ~15.6ms and cap us
|
||||
// near ~64 FPS. Uses the shared helper so it respects -notimerhacks
|
||||
// (Use Legacy Timers) and the Win11 timer-throttling opt-out. The helper
|
||||
// intentionally never calls timeEndPeriod; the kernel releases the request
|
||||
// when spicecfg exits.
|
||||
timeutils::set_timer_resolution();
|
||||
|
||||
// determine window title
|
||||
if (cfg::CONFIGURATOR_TYPE == cfg::ConfigType::Config) {
|
||||
WINDOW_TITLE = "spice2x config (" + to_string(VERSION_STRING_CFG) + ")";
|
||||
WINDOW_SIZE_X = 800;
|
||||
WINDOW_SIZE_Y = 600;
|
||||
}
|
||||
this->client_width = WINDOW_SIZE_X;
|
||||
this->client_height = WINDOW_SIZE_Y;
|
||||
|
||||
// open window
|
||||
this->hWnd = CreateWindowEx(
|
||||
@@ -53,11 +219,23 @@ cfg::ConfiguratorWindow::ConfiguratorWindow() {
|
||||
|
||||
cfg::ConfiguratorWindow::~ConfiguratorWindow() {
|
||||
|
||||
this->stop_timer();
|
||||
|
||||
// close window
|
||||
DestroyWindow(this->hWnd);
|
||||
|
||||
// unregister class
|
||||
UnregisterClass(CLASS_NAME, GetModuleHandle(NULL));
|
||||
|
||||
// release D3D9 resources if any
|
||||
if (this->device) {
|
||||
this->device->Release();
|
||||
this->device = nullptr;
|
||||
}
|
||||
if (this->d3d) {
|
||||
this->d3d->Release();
|
||||
this->d3d = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void cfg::ConfiguratorWindow::run() {
|
||||
@@ -67,8 +245,23 @@ void cfg::ConfiguratorWindow::run() {
|
||||
ShowWindow(this->hWnd, SW_SHOWNORMAL);
|
||||
UpdateWindow(this->hWnd);
|
||||
|
||||
// draw overlay in 60 FPS
|
||||
SetTimer(this->hWnd, 1, 1000 / 60, nullptr);
|
||||
// SW_SHOWNORMAL usually activates a top-level window, but not always (e.g.,
|
||||
// when the launching process doesn't have foreground rights to transfer).
|
||||
// Force foreground+focus explicitly so WM_MOUSEWHEEL is routed to us from
|
||||
// the very first frame; the Win32 default routes wheel events to the
|
||||
// keyboard-focused window.
|
||||
SetForegroundWindow(this->hWnd);
|
||||
SetFocus(this->hWnd);
|
||||
|
||||
// match the render timer to the monitor refresh rate so scrolling stays smooth
|
||||
// on 60/120/144 Hz panels. Idle CPU is bounded by overlay::sw_pixels_dirty
|
||||
// (software path) and overlay::d3d9_frame_dirty (D3D9 path); the timer is also
|
||||
// paused entirely when the window is minimized (see WM_SIZE handler).
|
||||
const UINT hz = detect_monitor_refresh_hz(this->hWnd);
|
||||
this->timer_interval_ms = std::max<UINT>(1, 1000 / hz);
|
||||
log_info("configurator", "render timer {} ms ({} Hz)",
|
||||
this->timer_interval_ms, hz);
|
||||
this->start_timer();
|
||||
|
||||
// window loop
|
||||
BOOL ret;
|
||||
@@ -102,6 +295,72 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
|
||||
|
||||
break;
|
||||
}
|
||||
case WM_SIZE: {
|
||||
|
||||
// pause/resume the render timer based on visibility
|
||||
auto *state = get_state(hWnd);
|
||||
if (state) {
|
||||
if (wParam == SIZE_MINIMIZED) {
|
||||
state->window_minimized = true;
|
||||
state->stop_timer();
|
||||
} else {
|
||||
if (state->window_minimized) {
|
||||
state->window_minimized = false;
|
||||
// force a full repaint on the next frame
|
||||
state->has_valid_draw_hash = false;
|
||||
state->start_timer();
|
||||
}
|
||||
const int new_w = LOWORD(lParam);
|
||||
const int new_h = HIWORD(lParam);
|
||||
if (new_w > 0 && new_h > 0
|
||||
&& (new_w != state->client_width || new_h != state->client_height)) {
|
||||
state->client_width = new_w;
|
||||
state->client_height = new_h;
|
||||
|
||||
// reset the D3D9 device with the new back-buffer size so the
|
||||
// hardware-accelerated path matches the window dimensions.
|
||||
if (state->use_d3d9 && state->device) {
|
||||
if (overlay::OVERLAY) {
|
||||
overlay::OVERLAY->reset_invalidate();
|
||||
}
|
||||
state->pp.BackBufferWidth = static_cast<UINT>(new_w);
|
||||
state->pp.BackBufferHeight = static_cast<UINT>(new_h);
|
||||
HRESULT hr = state->device->Reset(&state->pp);
|
||||
if (FAILED(hr)) {
|
||||
log_warning("configurator", "D3D9 device Reset failed, hr={:#x}",
|
||||
static_cast<unsigned int>(hr));
|
||||
}
|
||||
if (overlay::OVERLAY) {
|
||||
overlay::OVERLAY->reset_recreate();
|
||||
}
|
||||
}
|
||||
|
||||
// force a full repaint after resize
|
||||
state->has_valid_draw_hash = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WM_DISPLAYCHANGE: {
|
||||
|
||||
// monitor refresh rate may have changed (or the window moved to a
|
||||
// different monitor); re-detect and re-arm the render timer.
|
||||
auto *state = get_state(hWnd);
|
||||
if (state && state->timer_running) {
|
||||
const UINT hz = detect_monitor_refresh_hz(hWnd);
|
||||
const UINT new_interval = std::max<UINT>(1, 1000 / hz);
|
||||
if (new_interval != state->timer_interval_ms) {
|
||||
state->stop_timer();
|
||||
state->timer_interval_ms = new_interval;
|
||||
state->start_timer();
|
||||
log_info("configurator",
|
||||
"WM_DISPLAYCHANGE: render timer {} ms ({} Hz)",
|
||||
new_interval, hz);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WM_CLOSE:
|
||||
case WM_DESTROY: {
|
||||
|
||||
@@ -110,62 +369,192 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
|
||||
break;
|
||||
}
|
||||
case WM_TIMER: {
|
||||
if (wParam != RENDER_TIMER_ID) {
|
||||
break;
|
||||
}
|
||||
|
||||
// update overlay
|
||||
auto *state = get_state(hWnd);
|
||||
|
||||
// skip rendering when the window is minimized or hidden - the timer is
|
||||
// already paused on minimize, but defensively skip here as well.
|
||||
if (state && (state->window_minimized || !IsWindowVisible(hWnd))) {
|
||||
break;
|
||||
}
|
||||
|
||||
const bool use_d3d9 = state && state->use_d3d9 && state->device;
|
||||
|
||||
// build the imgui frame (input is already buffered via WM_* messages)
|
||||
if (overlay::OVERLAY) {
|
||||
overlay::OVERLAY->update();
|
||||
overlay::OVERLAY->set_active(true);
|
||||
overlay::OVERLAY->new_frame();
|
||||
overlay::OVERLAY->render();
|
||||
}
|
||||
|
||||
// repaint window
|
||||
InvalidateRect(hWnd, nullptr, TRUE);
|
||||
if (use_d3d9) {
|
||||
const HRESULT cl = state->device->TestCooperativeLevel();
|
||||
if (cl == D3DERR_DEVICELOST) {
|
||||
break;
|
||||
}
|
||||
if (cl == D3DERR_DEVICENOTRESET) {
|
||||
if (overlay::OVERLAY) {
|
||||
overlay::OVERLAY->reset_invalidate();
|
||||
}
|
||||
const HRESULT reset_hr = state->device->Reset(&state->pp);
|
||||
if (SUCCEEDED(reset_hr) && overlay::OVERLAY) {
|
||||
overlay::OVERLAY->reset_recreate();
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (FAILED(cl)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (overlay::OVERLAY) {
|
||||
overlay::OVERLAY->render();
|
||||
}
|
||||
|
||||
// skip Clear/Present when ImGui draw data is unchanged; the last
|
||||
// presented frame stays visible (same fast path as sw_pixels_dirty).
|
||||
const bool dirty = overlay::OVERLAY && overlay::OVERLAY->d3d9_frame_dirty;
|
||||
const bool force_present = state && !state->has_valid_draw_hash;
|
||||
if (dirty || force_present) {
|
||||
if (state) {
|
||||
state->has_valid_draw_hash = true;
|
||||
}
|
||||
state->device->Clear(0, nullptr, D3DCLEAR_TARGET,
|
||||
D3DCOLOR_RGBA(20, 18, 18, 255), 1.0f, 0);
|
||||
if (SUCCEEDED(state->device->BeginScene())) {
|
||||
if (overlay::OVERLAY) {
|
||||
overlay::OVERLAY->d3d9_render_draw(force_present);
|
||||
}
|
||||
state->device->EndScene();
|
||||
}
|
||||
const HRESULT hr = state->device->Present(nullptr, nullptr, nullptr, nullptr);
|
||||
if (hr == D3DERR_DEVICELOST) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (overlay::OVERLAY) {
|
||||
overlay::OVERLAY->render();
|
||||
}
|
||||
// software path: the overlay's renderer already skipped the costly
|
||||
// paint_imgui call when the draw data was unchanged. Only invalidate
|
||||
// the window when those pixels actually changed, or on the first
|
||||
// frame / after a resize when we lost the previously cached state.
|
||||
const bool dirty = overlay::OVERLAY && overlay::OVERLAY->sw_pixels_dirty;
|
||||
const bool force_repaint = state && !state->has_valid_draw_hash;
|
||||
if (dirty || force_repaint) {
|
||||
if (state) {
|
||||
state->has_valid_draw_hash = true;
|
||||
}
|
||||
// pass FALSE for bErase - WM_ERASEBKGND already short-circuits, so
|
||||
// skipping the erase region avoids one extra Win32 round trip.
|
||||
InvalidateRect(hWnd, nullptr, FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case WM_ERASEBKGND: {
|
||||
return 1;
|
||||
}
|
||||
case WM_PAINT: {
|
||||
|
||||
// render overlay
|
||||
if (overlay::OVERLAY) {
|
||||
|
||||
// get pixel data
|
||||
int width, height;
|
||||
uint32_t *pixel_data = overlay::OVERLAY->sw_get_pixel_data(&width, &height);
|
||||
if (width > 0 && height > 0) {
|
||||
|
||||
// create bitmap
|
||||
HBITMAP bitmap = CreateBitmap(width, height, 1, 8 * sizeof(uint32_t), pixel_data);
|
||||
|
||||
// prepare paint
|
||||
PAINTSTRUCT paint{};
|
||||
HDC hdc = BeginPaint(hWnd, &paint);
|
||||
HDC hdcMem = CreateCompatibleDC(hdc);
|
||||
SetBkMode(hdc, TRANSPARENT);
|
||||
|
||||
// draw bitmap
|
||||
SelectObject(hdcMem, bitmap);
|
||||
BitBlt(hdc, paint.rcPaint.left, paint.rcPaint.top,
|
||||
paint.rcPaint.right - paint.rcPaint.left,
|
||||
paint.rcPaint.bottom - paint.rcPaint.top,
|
||||
hdcMem, paint.rcPaint.left, paint.rcPaint.top, SRCCOPY);
|
||||
|
||||
// delete bitmap
|
||||
DeleteObject(bitmap);
|
||||
|
||||
// clean up
|
||||
DeleteDC(hdcMem);
|
||||
EndPaint(hWnd, &paint);
|
||||
} else {
|
||||
return DefWindowProc(hWnd, uMsg, wParam, lParam);
|
||||
}
|
||||
paint_software(hWnd);
|
||||
break;
|
||||
}
|
||||
case WM_ACTIVATE: {
|
||||
const WORD activation = LOWORD(wParam);
|
||||
if (activation == WA_INACTIVE) {
|
||||
// dropping any held mouse buttons so we don't get stuck in a
|
||||
// "button still pressed" state when we come back.
|
||||
auto &io = ImGui::GetIO();
|
||||
io.AddMouseButtonEvent(ImGuiMouseButton_Left, false);
|
||||
io.AddMouseButtonEvent(ImGuiMouseButton_Right, false);
|
||||
io.AddMouseButtonEvent(ImGuiMouseButton_Middle, false);
|
||||
} else {
|
||||
return DefWindowProc(hWnd, uMsg, wParam, lParam);
|
||||
// WA_ACTIVE or WA_CLICKACTIVE: make sure the keyboard focus is
|
||||
// actually on this window so WM_MOUSEWHEEL gets routed to us
|
||||
// again. Without this, scrolling silently stops working after
|
||||
// the user alt-tabs back to spicecfg.
|
||||
SetFocus(hWnd);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WM_KILLFOCUS: {
|
||||
// mirror the WA_INACTIVE mouse-button drop for the keyboard -
|
||||
// without this, a key held during alt-tab stays "down" in ImGui
|
||||
// state until the user presses and releases it again.
|
||||
ImGui::GetIO().ClearInputKeys();
|
||||
break;
|
||||
}
|
||||
// input messages routed straight into ImGui IO. This replaces the previous
|
||||
// per-frame 256-VK rawinput scan in ImGui_ImplSpice_NewFrame for the
|
||||
// standalone configurator (see CONFIGURATOR_STANDALONE branches there).
|
||||
case WM_KEYDOWN:
|
||||
case WM_SYSKEYDOWN:
|
||||
case WM_KEYUP:
|
||||
case WM_SYSKEYUP: {
|
||||
if (wParam == VK_F4) {
|
||||
return DefWindowProc(hWnd, uMsg, wParam, lParam);
|
||||
}
|
||||
const bool down = (uMsg == WM_KEYDOWN) || (uMsg == WM_SYSKEYDOWN);
|
||||
auto &io = ImGui::GetIO();
|
||||
const ImGuiKey key = vk_to_imgui_key(wParam);
|
||||
if (key != ImGuiKey_None) {
|
||||
io.AddKeyEvent(key, down);
|
||||
}
|
||||
const ImGuiKey mod = vk_to_imgui_mod_key(wParam);
|
||||
if (mod != ImGuiMod_None) {
|
||||
io.AddKeyEvent(mod, down);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WM_MOUSEMOVE: {
|
||||
auto &io = ImGui::GetIO();
|
||||
io.AddMousePosEvent(static_cast<float>(GET_X_LPARAM(lParam)),
|
||||
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||
break;
|
||||
}
|
||||
case WM_LBUTTONDOWN:
|
||||
case WM_LBUTTONUP:
|
||||
case WM_RBUTTONDOWN:
|
||||
case WM_RBUTTONUP:
|
||||
case WM_MBUTTONDOWN:
|
||||
case WM_MBUTTONUP: {
|
||||
int button = 0;
|
||||
bool down = false;
|
||||
switch (uMsg) {
|
||||
case WM_LBUTTONDOWN: button = ImGuiMouseButton_Left; down = true; break;
|
||||
case WM_LBUTTONUP: button = ImGuiMouseButton_Left; down = false; break;
|
||||
case WM_RBUTTONDOWN: button = ImGuiMouseButton_Right; down = true; break;
|
||||
case WM_RBUTTONUP: button = ImGuiMouseButton_Right; down = false; break;
|
||||
case WM_MBUTTONDOWN: button = ImGuiMouseButton_Middle; down = true; break;
|
||||
case WM_MBUTTONUP: button = ImGuiMouseButton_Middle; down = false; break;
|
||||
}
|
||||
auto &io = ImGui::GetIO();
|
||||
io.AddMouseButtonEvent(button, down);
|
||||
if (down) {
|
||||
SetCapture(hWnd);
|
||||
} else {
|
||||
ReleaseCapture();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WM_MOUSEWHEEL: {
|
||||
const float delta = static_cast<float>(GET_WHEEL_DELTA_WPARAM(wParam))
|
||||
/ static_cast<float>(WHEEL_DELTA);
|
||||
ImGui::GetIO().AddMouseWheelEvent(0.0f, delta);
|
||||
break;
|
||||
}
|
||||
#if !SPICE_XP
|
||||
case WM_MOUSEHWHEEL: {
|
||||
const float delta = static_cast<float>(GET_WHEEL_DELTA_WPARAM(wParam))
|
||||
/ static_cast<float>(WHEEL_DELTA);
|
||||
ImGui::GetIO().AddMouseWheelEvent(delta, 0.0f);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
default:
|
||||
return DefWindowProc(hWnd, uMsg, wParam, lParam);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <windows.h>
|
||||
#include <d3d9.h>
|
||||
|
||||
namespace cfg {
|
||||
|
||||
@@ -9,11 +12,38 @@ namespace cfg {
|
||||
|
||||
HWND hWnd;
|
||||
|
||||
// optional D3D9 device backing the configurator window; nullptr when running
|
||||
// the software-rendered path. Owned by ConfiguratorWindow when set.
|
||||
IDirect3D9 *d3d = nullptr;
|
||||
IDirect3DDevice9 *device = nullptr;
|
||||
D3DPRESENT_PARAMETERS pp {};
|
||||
bool use_d3d9 = false;
|
||||
|
||||
// throttling / pause state. timer_interval_ms is the default until run()
|
||||
// refines it to the actual monitor refresh rate (see detect_monitor_refresh_hz).
|
||||
UINT timer_interval_ms = 1000 / 60;
|
||||
bool timer_running = false;
|
||||
bool window_minimized = false;
|
||||
|
||||
// tracks whether we've issued at least one InvalidateRect since window
|
||||
// creation/resize. The overlay's per-frame "pixels changed" flag suppresses
|
||||
// idle blits; this flag forces the very first blit on startup or after
|
||||
// a resize so the window doesn't show garbage until the user moves the mouse.
|
||||
bool has_valid_draw_hash = false;
|
||||
|
||||
// dimensions of the configurator client area (kept in sync with WM_SIZE)
|
||||
int client_width = 0;
|
||||
int client_height = 0;
|
||||
|
||||
ConfiguratorWindow();
|
||||
~ConfiguratorWindow();
|
||||
|
||||
void run();
|
||||
|
||||
// start/stop the render timer based on visibility state
|
||||
void start_timer();
|
||||
void stop_timer();
|
||||
|
||||
static LRESULT CALLBACK window_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace overlay::windows {
|
||||
el->SetAttribute("invert", entry.invert);
|
||||
el->SetAttribute("debounce_up", entry.debounce_up);
|
||||
el->SetAttribute("debounce_down", entry.debounce_down);
|
||||
el->SetAttribute("bat_threshold", entry.bat_threshold);
|
||||
el->SetAttribute("velocity_threshold", entry.velocity_threshold);
|
||||
parent->InsertEndChild(el);
|
||||
}
|
||||
@@ -49,6 +50,10 @@ namespace overlay::windows {
|
||||
el->QueryIntAttribute("velocity_threshold", &vel);
|
||||
entry.velocity_threshold = (unsigned short)vel;
|
||||
|
||||
int bat = 0;
|
||||
el->QueryIntAttribute("bat_threshold", &bat);
|
||||
entry.bat_threshold = bat;
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -65,7 +70,7 @@ namespace overlay::windows {
|
||||
el->SetAttribute("smoothing", analog.smoothing);
|
||||
el->SetAttribute("multiplier", analog.multiplier);
|
||||
el->SetAttribute("relative", analog.relative_mode);
|
||||
el->SetAttribute("delay", analog.delay_buffer_depth);
|
||||
el->SetAttribute("delay_ms", analog.delay_ms);
|
||||
parent->InsertEndChild(el);
|
||||
}
|
||||
|
||||
@@ -88,7 +93,7 @@ namespace overlay::windows {
|
||||
el->QueryBoolAttribute("smoothing", &a.smoothing);
|
||||
el->QueryIntAttribute("multiplier", &a.multiplier);
|
||||
el->QueryBoolAttribute("relative", &a.relative_mode);
|
||||
el->QueryIntAttribute("delay", &a.delay_buffer_depth);
|
||||
el->QueryUnsignedAttribute("delay_ms", &a.delay_ms);
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
@@ -21,7 +21,7 @@ namespace cfg {
|
||||
if (SCREEN_RESIZE_CFG_PATH_OVERRIDE.has_value()) {
|
||||
this->config_path = SCREEN_RESIZE_CFG_PATH_OVERRIDE.value();
|
||||
if (fileutils::file_exists(this->config_path)) {
|
||||
log_info("ScreenResize", "loading config from: {}", this->config_path.string());
|
||||
log_info("ScreenResize", "loading config from: {}", this->config_path);
|
||||
file_exists = true;
|
||||
}
|
||||
} else {
|
||||
@@ -35,7 +35,7 @@ namespace cfg {
|
||||
|
||||
ScreenResize::~ScreenResize() {
|
||||
}
|
||||
|
||||
|
||||
void ScreenResize::config_load() {
|
||||
std::string config = fileutils::text_read(this->config_path);
|
||||
if (config.empty()) {
|
||||
@@ -78,7 +78,7 @@ namespace cfg {
|
||||
eamuse_get_game(),
|
||||
use_game_setting,
|
||||
root);
|
||||
|
||||
|
||||
load_bool_value(doc, root + "enable_screen_resize", this->enable_screen_resize);
|
||||
if (this->enable_screen_resize) {
|
||||
log_misc("ScreenResize", "enabled by config file");
|
||||
|
||||
Vendored
+1
@@ -20,6 +20,7 @@ Usage:
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <cstdint>
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
+7
@@ -21,5 +21,12 @@ set(IMGUI_SOURCES
|
||||
misc/cpp/imgui_stdlib.cpp
|
||||
)
|
||||
|
||||
# spice2x: DX11 backend is only built for non-XP toolchains. WinXP lacks
|
||||
# D3DCOMPILER_47.dll, and including this TU would pull in D3DCompile imports.
|
||||
if(NOT SPICE_XP)
|
||||
list(APPEND IMGUI_HEADERS backends/imgui_impl_dx11.h)
|
||||
list(APPEND IMGUI_SOURCES backends/imgui_impl_dx11.cpp)
|
||||
endif()
|
||||
|
||||
add_library(imgui STATIC ${IMGUI_HEADERS} ${IMGUI_SOURCES})
|
||||
target_include_directories(imgui PRIVATE ${PROJECT_SOURCE_DIR})
|
||||
|
||||
@@ -0,0 +1,697 @@
|
||||
// dear imgui: Renderer Backend for DirectX11
|
||||
// This needs to be used along with a Platform Backend (e.g. Win32)
|
||||
|
||||
// Implemented features:
|
||||
// [X] Renderer: User texture binding. Use 'ID3D11ShaderResourceView*' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef!
|
||||
// [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset).
|
||||
// [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures).
|
||||
// [X] Renderer: Expose selected render state for draw callbacks to use. Access in '(ImGui_ImplXXXX_RenderState*)GetPlatformIO().Renderer_RenderState'.
|
||||
|
||||
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
|
||||
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
|
||||
// Learn about Dear ImGui:
|
||||
// - FAQ https://dearimgui.com/faq
|
||||
// - Getting Started https://dearimgui.com/getting-started
|
||||
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
|
||||
// - Introduction, links and more at the top of imgui.cpp
|
||||
|
||||
// CHANGELOG
|
||||
// (minor and older changes stripped away, please see git history for details)
|
||||
// 2026-01-19: DirectX11: Added 'SamplerNearest' in ImGui_ImplDX11_RenderState. Renamed 'SamplerDefault' to 'SamplerLinear'.
|
||||
// 2025-09-18: Call platform_io.ClearRendererHandlers() on shutdown.
|
||||
// 2025-06-11: DirectX11: Added support for ImGuiBackendFlags_RendererHasTextures, for dynamic font atlas.
|
||||
// 2025-05-07: DirectX11: Honor draw_data->FramebufferScale to allow for custom backends and experiment using it (consistently with other renderer backends, even though in normal condition it is not set under Windows).
|
||||
// 2025-01-06: DirectX11: Expose VertexConstantBuffer in ImGui_ImplDX11_RenderState. Reset projection matrix in ImDrawCallback_ResetRenderState handler.
|
||||
// 2024-10-07: DirectX11: Changed default texture sampler to Clamp instead of Repeat/Wrap.
|
||||
// 2024-10-07: DirectX11: Expose selected render state in ImGui_ImplDX11_RenderState, which you can access in 'void* platform_io.Renderer_RenderState' during draw callbacks.
|
||||
// 2022-10-11: Using 'nullptr' instead of 'NULL' as per our switch to C++11.
|
||||
// 2021-06-29: Reorganized backend to pull data from a single structure to facilitate usage with multiple-contexts (all g_XXXX access changed to bd->XXXX).
|
||||
// 2021-05-19: DirectX11: Replaced direct access to ImDrawCmd::TextureId with a call to ImDrawCmd::GetTexID(). (will become a requirement)
|
||||
// 2021-02-18: DirectX11: Change blending equation to preserve alpha in output buffer.
|
||||
// 2019-08-01: DirectX11: Fixed code querying the Geometry Shader state (would generally error with Debug layer enabled).
|
||||
// 2019-07-21: DirectX11: Backup, clear and restore Geometry Shader is any is bound when calling ImGui_ImplDX11_RenderDrawData. Clearing Hull/Domain/Compute shaders without backup/restore.
|
||||
// 2019-05-29: DirectX11: Added support for large mesh (64K+ vertices), enable ImGuiBackendFlags_RendererHasVtxOffset flag.
|
||||
// 2019-04-30: DirectX11: Added support for special ImDrawCallback_ResetRenderState callback to reset render state.
|
||||
// 2018-12-03: Misc: Added #pragma comment statement to automatically link with d3dcompiler.lib when using D3DCompile().
|
||||
// 2018-11-30: Misc: Setting up io.BackendRendererName so it can be displayed in the About Window.
|
||||
// 2018-08-01: DirectX11: Querying for IDXGIFactory instead of IDXGIFactory1 to increase compatibility.
|
||||
// 2018-07-13: DirectX11: Fixed unreleased resources in Init and Shutdown functions.
|
||||
// 2018-06-08: Misc: Extracted imgui_impl_dx11.cpp/.h away from the old combined DX11+Win32 example.
|
||||
// 2018-06-08: DirectX11: Use draw_data->DisplayPos and draw_data->DisplaySize to setup projection matrix and clipping rectangle.
|
||||
// 2018-02-16: Misc: Obsoleted the io.RenderDrawListsFn callback and exposed ImGui_ImplDX11_RenderDrawData() in the .h file so you can call it yourself.
|
||||
// 2018-02-06: Misc: Removed call to ImGui::Shutdown() which is not available from 1.60 WIP, user needs to call CreateContext/DestroyContext themselves.
|
||||
// 2016-05-07: DirectX11: Disabling depth-write.
|
||||
|
||||
#include "imgui.h"
|
||||
#ifndef IMGUI_DISABLE
|
||||
#include "imgui_impl_dx11.h"
|
||||
|
||||
// DirectX
|
||||
#include <stdio.h>
|
||||
#include <d3d11.h>
|
||||
#include <d3dcompiler.h>
|
||||
#ifdef _MSC_VER
|
||||
#pragma comment(lib, "d3dcompiler") // Automatically link with d3dcompiler.lib as we are using D3DCompile() below.
|
||||
#endif
|
||||
|
||||
// Clang/GCC warnings with -Weverything
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse.
|
||||
#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness
|
||||
#endif
|
||||
|
||||
// DirectX11 data
|
||||
struct ImGui_ImplDX11_Texture
|
||||
{
|
||||
ID3D11Texture2D* pTexture;
|
||||
ID3D11ShaderResourceView* pTextureView;
|
||||
};
|
||||
|
||||
struct ImGui_ImplDX11_Data
|
||||
{
|
||||
ID3D11Device* pd3dDevice;
|
||||
ID3D11DeviceContext* pd3dDeviceContext;
|
||||
IDXGIFactory* pFactory;
|
||||
ID3D11Buffer* pVB;
|
||||
ID3D11Buffer* pIB;
|
||||
ID3D11VertexShader* pVertexShader;
|
||||
ID3D11InputLayout* pInputLayout;
|
||||
ID3D11Buffer* pVertexConstantBuffer;
|
||||
ID3D11PixelShader* pPixelShader;
|
||||
ID3D11SamplerState* pTexSamplerLinear;
|
||||
ID3D11SamplerState* pTexSamplerNearest;
|
||||
ID3D11RasterizerState* pRasterizerState;
|
||||
ID3D11BlendState* pBlendState;
|
||||
ID3D11DepthStencilState* pDepthStencilState;
|
||||
int VertexBufferSize;
|
||||
int IndexBufferSize;
|
||||
|
||||
ImGui_ImplDX11_Data() { memset((void*)this, 0, sizeof(*this)); VertexBufferSize = 5000; IndexBufferSize = 10000; }
|
||||
};
|
||||
|
||||
struct VERTEX_CONSTANT_BUFFER_DX11
|
||||
{
|
||||
float mvp[4][4];
|
||||
};
|
||||
|
||||
// Backend data stored in io.BackendRendererUserData to allow support for multiple Dear ImGui contexts
|
||||
// It is STRONGLY preferred that you use docking branch with multi-viewports (== single Dear ImGui context + multiple windows) instead of multiple Dear ImGui contexts.
|
||||
static ImGui_ImplDX11_Data* ImGui_ImplDX11_GetBackendData()
|
||||
{
|
||||
return ImGui::GetCurrentContext() ? (ImGui_ImplDX11_Data*)ImGui::GetIO().BackendRendererUserData : nullptr;
|
||||
}
|
||||
|
||||
// Functions
|
||||
static void ImGui_ImplDX11_SetupRenderState(const ImDrawData* draw_data, ID3D11DeviceContext* device_ctx)
|
||||
{
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
|
||||
// Setup viewport
|
||||
D3D11_VIEWPORT vp = {};
|
||||
vp.Width = draw_data->DisplaySize.x * draw_data->FramebufferScale.x;
|
||||
vp.Height = draw_data->DisplaySize.y * draw_data->FramebufferScale.y;
|
||||
vp.MinDepth = 0.0f;
|
||||
vp.MaxDepth = 1.0f;
|
||||
vp.TopLeftX = vp.TopLeftY = 0;
|
||||
device_ctx->RSSetViewports(1, &vp);
|
||||
|
||||
// Setup orthographic projection matrix into our constant buffer
|
||||
// Our visible imgui space lies from draw_data->DisplayPos (top left) to draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayPos is (0,0) for single viewport apps.
|
||||
D3D11_MAPPED_SUBRESOURCE mapped_resource;
|
||||
if (device_ctx->Map(bd->pVertexConstantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped_resource) == S_OK)
|
||||
{
|
||||
VERTEX_CONSTANT_BUFFER_DX11* constant_buffer = (VERTEX_CONSTANT_BUFFER_DX11*)mapped_resource.pData;
|
||||
float L = draw_data->DisplayPos.x;
|
||||
float R = draw_data->DisplayPos.x + draw_data->DisplaySize.x;
|
||||
float T = draw_data->DisplayPos.y;
|
||||
float B = draw_data->DisplayPos.y + draw_data->DisplaySize.y;
|
||||
float mvp[4][4] =
|
||||
{
|
||||
{ 2.0f/(R-L), 0.0f, 0.0f, 0.0f },
|
||||
{ 0.0f, 2.0f/(T-B), 0.0f, 0.0f },
|
||||
{ 0.0f, 0.0f, 0.5f, 0.0f },
|
||||
{ (R+L)/(L-R), (T+B)/(B-T), 0.5f, 1.0f },
|
||||
};
|
||||
memcpy(&constant_buffer->mvp, mvp, sizeof(mvp));
|
||||
device_ctx->Unmap(bd->pVertexConstantBuffer, 0);
|
||||
}
|
||||
|
||||
// Setup shader and vertex buffers
|
||||
unsigned int stride = sizeof(ImDrawVert);
|
||||
unsigned int offset = 0;
|
||||
device_ctx->IASetInputLayout(bd->pInputLayout);
|
||||
device_ctx->IASetVertexBuffers(0, 1, &bd->pVB, &stride, &offset);
|
||||
device_ctx->IASetIndexBuffer(bd->pIB, sizeof(ImDrawIdx) == 2 ? DXGI_FORMAT_R16_UINT : DXGI_FORMAT_R32_UINT, 0);
|
||||
device_ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
||||
device_ctx->VSSetShader(bd->pVertexShader, nullptr, 0);
|
||||
device_ctx->VSSetConstantBuffers(0, 1, &bd->pVertexConstantBuffer);
|
||||
device_ctx->PSSetShader(bd->pPixelShader, nullptr, 0);
|
||||
device_ctx->PSSetSamplers(0, 1, &bd->pTexSamplerLinear);
|
||||
device_ctx->GSSetShader(nullptr, nullptr, 0);
|
||||
device_ctx->HSSetShader(nullptr, nullptr, 0); // In theory we should backup and restore this as well.. very infrequently used..
|
||||
device_ctx->DSSetShader(nullptr, nullptr, 0); // In theory we should backup and restore this as well.. very infrequently used..
|
||||
device_ctx->CSSetShader(nullptr, nullptr, 0); // In theory we should backup and restore this as well.. very infrequently used..
|
||||
|
||||
// Setup render state
|
||||
const float blend_factor[4] = { 0.f, 0.f, 0.f, 0.f };
|
||||
device_ctx->OMSetBlendState(bd->pBlendState, blend_factor, 0xffffffff);
|
||||
device_ctx->OMSetDepthStencilState(bd->pDepthStencilState, 0);
|
||||
device_ctx->RSSetState(bd->pRasterizerState);
|
||||
}
|
||||
|
||||
// Render function
|
||||
void ImGui_ImplDX11_RenderDrawData(ImDrawData* draw_data)
|
||||
{
|
||||
// Avoid rendering when minimized
|
||||
if (draw_data->DisplaySize.x <= 0.0f || draw_data->DisplaySize.y <= 0.0f)
|
||||
return;
|
||||
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
ID3D11DeviceContext* device = bd->pd3dDeviceContext;
|
||||
|
||||
// Catch up with texture updates. Most of the times, the list will have 1 element with an OK status, aka nothing to do.
|
||||
// (This almost always points to ImGui::GetPlatformIO().Textures[] but is part of ImDrawData to allow overriding or disabling texture updates).
|
||||
if (draw_data->Textures != nullptr)
|
||||
for (ImTextureData* tex : *draw_data->Textures)
|
||||
if (tex->Status != ImTextureStatus_OK)
|
||||
ImGui_ImplDX11_UpdateTexture(tex);
|
||||
|
||||
// Create and grow vertex/index buffers if needed
|
||||
if (!bd->pVB || bd->VertexBufferSize < draw_data->TotalVtxCount)
|
||||
{
|
||||
if (bd->pVB) { bd->pVB->Release(); bd->pVB = nullptr; }
|
||||
bd->VertexBufferSize = draw_data->TotalVtxCount + 5000;
|
||||
D3D11_BUFFER_DESC desc = {};
|
||||
desc.Usage = D3D11_USAGE_DYNAMIC;
|
||||
desc.ByteWidth = bd->VertexBufferSize * sizeof(ImDrawVert);
|
||||
desc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
|
||||
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
|
||||
desc.MiscFlags = 0;
|
||||
if (bd->pd3dDevice->CreateBuffer(&desc, nullptr, &bd->pVB) < 0)
|
||||
return;
|
||||
}
|
||||
if (!bd->pIB || bd->IndexBufferSize < draw_data->TotalIdxCount)
|
||||
{
|
||||
if (bd->pIB) { bd->pIB->Release(); bd->pIB = nullptr; }
|
||||
bd->IndexBufferSize = draw_data->TotalIdxCount + 10000;
|
||||
D3D11_BUFFER_DESC desc = {};
|
||||
desc.Usage = D3D11_USAGE_DYNAMIC;
|
||||
desc.ByteWidth = bd->IndexBufferSize * sizeof(ImDrawIdx);
|
||||
desc.BindFlags = D3D11_BIND_INDEX_BUFFER;
|
||||
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
|
||||
if (bd->pd3dDevice->CreateBuffer(&desc, nullptr, &bd->pIB) < 0)
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload vertex/index data into a single contiguous GPU buffer
|
||||
D3D11_MAPPED_SUBRESOURCE vtx_resource, idx_resource;
|
||||
if (device->Map(bd->pVB, 0, D3D11_MAP_WRITE_DISCARD, 0, &vtx_resource) != S_OK)
|
||||
return;
|
||||
if (device->Map(bd->pIB, 0, D3D11_MAP_WRITE_DISCARD, 0, &idx_resource) != S_OK)
|
||||
return;
|
||||
ImDrawVert* vtx_dst = (ImDrawVert*)vtx_resource.pData;
|
||||
ImDrawIdx* idx_dst = (ImDrawIdx*)idx_resource.pData;
|
||||
for (const ImDrawList* draw_list : draw_data->CmdLists)
|
||||
{
|
||||
memcpy(vtx_dst, draw_list->VtxBuffer.Data, draw_list->VtxBuffer.Size * sizeof(ImDrawVert));
|
||||
memcpy(idx_dst, draw_list->IdxBuffer.Data, draw_list->IdxBuffer.Size * sizeof(ImDrawIdx));
|
||||
vtx_dst += draw_list->VtxBuffer.Size;
|
||||
idx_dst += draw_list->IdxBuffer.Size;
|
||||
}
|
||||
device->Unmap(bd->pVB, 0);
|
||||
device->Unmap(bd->pIB, 0);
|
||||
|
||||
// Backup DX state that will be modified to restore it afterwards (unfortunately this is very ugly looking and verbose. Close your eyes!)
|
||||
struct BACKUP_DX11_STATE
|
||||
{
|
||||
UINT ScissorRectsCount, ViewportsCount;
|
||||
D3D11_RECT ScissorRects[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE];
|
||||
D3D11_VIEWPORT Viewports[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE];
|
||||
ID3D11RasterizerState* RS;
|
||||
ID3D11BlendState* BlendState;
|
||||
FLOAT BlendFactor[4];
|
||||
UINT SampleMask;
|
||||
UINT StencilRef;
|
||||
ID3D11DepthStencilState* DepthStencilState;
|
||||
ID3D11ShaderResourceView* PSShaderResource;
|
||||
ID3D11SamplerState* PSSampler;
|
||||
ID3D11PixelShader* PS;
|
||||
ID3D11VertexShader* VS;
|
||||
ID3D11GeometryShader* GS;
|
||||
UINT PSInstancesCount, VSInstancesCount, GSInstancesCount;
|
||||
ID3D11ClassInstance *PSInstances[256], *VSInstances[256], *GSInstances[256]; // 256 is max according to PSSetShader documentation
|
||||
D3D11_PRIMITIVE_TOPOLOGY PrimitiveTopology;
|
||||
ID3D11Buffer* IndexBuffer, *VertexBuffer, *VSConstantBuffer;
|
||||
UINT IndexBufferOffset, VertexBufferStride, VertexBufferOffset;
|
||||
DXGI_FORMAT IndexBufferFormat;
|
||||
ID3D11InputLayout* InputLayout;
|
||||
};
|
||||
BACKUP_DX11_STATE old = {};
|
||||
old.ScissorRectsCount = old.ViewportsCount = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE;
|
||||
device->RSGetScissorRects(&old.ScissorRectsCount, old.ScissorRects);
|
||||
device->RSGetViewports(&old.ViewportsCount, old.Viewports);
|
||||
device->RSGetState(&old.RS);
|
||||
device->OMGetBlendState(&old.BlendState, old.BlendFactor, &old.SampleMask);
|
||||
device->OMGetDepthStencilState(&old.DepthStencilState, &old.StencilRef);
|
||||
device->PSGetShaderResources(0, 1, &old.PSShaderResource);
|
||||
device->PSGetSamplers(0, 1, &old.PSSampler);
|
||||
old.PSInstancesCount = old.VSInstancesCount = old.GSInstancesCount = 256;
|
||||
device->PSGetShader(&old.PS, old.PSInstances, &old.PSInstancesCount);
|
||||
device->VSGetShader(&old.VS, old.VSInstances, &old.VSInstancesCount);
|
||||
device->VSGetConstantBuffers(0, 1, &old.VSConstantBuffer);
|
||||
device->GSGetShader(&old.GS, old.GSInstances, &old.GSInstancesCount);
|
||||
|
||||
device->IAGetPrimitiveTopology(&old.PrimitiveTopology);
|
||||
device->IAGetIndexBuffer(&old.IndexBuffer, &old.IndexBufferFormat, &old.IndexBufferOffset);
|
||||
device->IAGetVertexBuffers(0, 1, &old.VertexBuffer, &old.VertexBufferStride, &old.VertexBufferOffset);
|
||||
device->IAGetInputLayout(&old.InputLayout);
|
||||
|
||||
// Setup desired DX state
|
||||
ImGui_ImplDX11_SetupRenderState(draw_data, device);
|
||||
|
||||
// Setup render state structure (for callbacks and custom texture bindings)
|
||||
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
|
||||
ImGui_ImplDX11_RenderState render_state;
|
||||
render_state.Device = bd->pd3dDevice;
|
||||
render_state.DeviceContext = bd->pd3dDeviceContext;
|
||||
render_state.SamplerLinear = bd->pTexSamplerLinear;
|
||||
render_state.SamplerNearest = bd->pTexSamplerNearest;
|
||||
render_state.VertexConstantBuffer = bd->pVertexConstantBuffer;
|
||||
platform_io.Renderer_RenderState = &render_state;
|
||||
|
||||
// Render command lists
|
||||
// (Because we merged all buffers into a single one, we maintain our own offset into them)
|
||||
int global_idx_offset = 0;
|
||||
int global_vtx_offset = 0;
|
||||
ImVec2 clip_off = draw_data->DisplayPos;
|
||||
ImVec2 clip_scale = draw_data->FramebufferScale;
|
||||
for (const ImDrawList* draw_list : draw_data->CmdLists)
|
||||
{
|
||||
for (int cmd_i = 0; cmd_i < draw_list->CmdBuffer.Size; cmd_i++)
|
||||
{
|
||||
const ImDrawCmd* pcmd = &draw_list->CmdBuffer[cmd_i];
|
||||
if (pcmd->UserCallback != nullptr)
|
||||
{
|
||||
// User callback, registered via ImDrawList::AddCallback()
|
||||
// (ImDrawCallback_ResetRenderState is a special callback value used by the user to request the renderer to reset render state.)
|
||||
if (pcmd->UserCallback == ImDrawCallback_ResetRenderState)
|
||||
ImGui_ImplDX11_SetupRenderState(draw_data, device);
|
||||
else
|
||||
pcmd->UserCallback(draw_list, pcmd);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Project scissor/clipping rectangles into framebuffer space
|
||||
ImVec2 clip_min((pcmd->ClipRect.x - clip_off.x) * clip_scale.x, (pcmd->ClipRect.y - clip_off.y) * clip_scale.y);
|
||||
ImVec2 clip_max((pcmd->ClipRect.z - clip_off.x) * clip_scale.x, (pcmd->ClipRect.w - clip_off.y) * clip_scale.y);
|
||||
if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y)
|
||||
continue;
|
||||
|
||||
// Apply scissor/clipping rectangle
|
||||
const D3D11_RECT r = { (LONG)clip_min.x, (LONG)clip_min.y, (LONG)clip_max.x, (LONG)clip_max.y };
|
||||
device->RSSetScissorRects(1, &r);
|
||||
|
||||
// Bind texture, Draw
|
||||
ID3D11ShaderResourceView* texture_srv = (ID3D11ShaderResourceView*)pcmd->GetTexID();
|
||||
device->PSSetShaderResources(0, 1, &texture_srv);
|
||||
device->DrawIndexed(pcmd->ElemCount, pcmd->IdxOffset + global_idx_offset, pcmd->VtxOffset + global_vtx_offset);
|
||||
}
|
||||
}
|
||||
global_idx_offset += draw_list->IdxBuffer.Size;
|
||||
global_vtx_offset += draw_list->VtxBuffer.Size;
|
||||
}
|
||||
platform_io.Renderer_RenderState = nullptr;
|
||||
|
||||
// Restore modified DX state
|
||||
device->RSSetScissorRects(old.ScissorRectsCount, old.ScissorRects);
|
||||
device->RSSetViewports(old.ViewportsCount, old.Viewports);
|
||||
device->RSSetState(old.RS); if (old.RS) old.RS->Release();
|
||||
device->OMSetBlendState(old.BlendState, old.BlendFactor, old.SampleMask); if (old.BlendState) old.BlendState->Release();
|
||||
device->OMSetDepthStencilState(old.DepthStencilState, old.StencilRef); if (old.DepthStencilState) old.DepthStencilState->Release();
|
||||
device->PSSetShaderResources(0, 1, &old.PSShaderResource); if (old.PSShaderResource) old.PSShaderResource->Release();
|
||||
device->PSSetSamplers(0, 1, &old.PSSampler); if (old.PSSampler) old.PSSampler->Release();
|
||||
device->PSSetShader(old.PS, old.PSInstances, old.PSInstancesCount); if (old.PS) old.PS->Release();
|
||||
for (UINT i = 0; i < old.PSInstancesCount; i++) if (old.PSInstances[i]) old.PSInstances[i]->Release();
|
||||
device->VSSetShader(old.VS, old.VSInstances, old.VSInstancesCount); if (old.VS) old.VS->Release();
|
||||
device->VSSetConstantBuffers(0, 1, &old.VSConstantBuffer); if (old.VSConstantBuffer) old.VSConstantBuffer->Release();
|
||||
device->GSSetShader(old.GS, old.GSInstances, old.GSInstancesCount); if (old.GS) old.GS->Release();
|
||||
for (UINT i = 0; i < old.VSInstancesCount; i++) if (old.VSInstances[i]) old.VSInstances[i]->Release();
|
||||
device->IASetPrimitiveTopology(old.PrimitiveTopology);
|
||||
device->IASetIndexBuffer(old.IndexBuffer, old.IndexBufferFormat, old.IndexBufferOffset); if (old.IndexBuffer) old.IndexBuffer->Release();
|
||||
device->IASetVertexBuffers(0, 1, &old.VertexBuffer, &old.VertexBufferStride, &old.VertexBufferOffset); if (old.VertexBuffer) old.VertexBuffer->Release();
|
||||
device->IASetInputLayout(old.InputLayout); if (old.InputLayout) old.InputLayout->Release();
|
||||
}
|
||||
|
||||
static void ImGui_ImplDX11_DestroyTexture(ImTextureData* tex)
|
||||
{
|
||||
if (ImGui_ImplDX11_Texture* backend_tex = (ImGui_ImplDX11_Texture*)tex->BackendUserData)
|
||||
{
|
||||
IM_ASSERT(backend_tex->pTextureView == (ID3D11ShaderResourceView*)(intptr_t)tex->TexID);
|
||||
backend_tex->pTextureView->Release();
|
||||
backend_tex->pTexture->Release();
|
||||
IM_DELETE(backend_tex);
|
||||
|
||||
// Clear identifiers and mark as destroyed (in order to allow e.g. calling InvalidateDeviceObjects while running)
|
||||
tex->SetTexID(ImTextureID_Invalid);
|
||||
tex->BackendUserData = nullptr;
|
||||
}
|
||||
tex->SetStatus(ImTextureStatus_Destroyed);
|
||||
}
|
||||
|
||||
void ImGui_ImplDX11_UpdateTexture(ImTextureData* tex)
|
||||
{
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
if (tex->Status == ImTextureStatus_WantCreate)
|
||||
{
|
||||
// Create and upload new texture to graphics system
|
||||
//IMGUI_DEBUG_LOG("UpdateTexture #%03d: WantCreate %dx%d\n", tex->UniqueID, tex->Width, tex->Height);
|
||||
IM_ASSERT(tex->TexID == ImTextureID_Invalid && tex->BackendUserData == nullptr);
|
||||
IM_ASSERT(tex->Format == ImTextureFormat_RGBA32);
|
||||
unsigned int* pixels = (unsigned int*)tex->GetPixels();
|
||||
ImGui_ImplDX11_Texture* backend_tex = IM_NEW(ImGui_ImplDX11_Texture)();
|
||||
|
||||
// Create texture
|
||||
D3D11_TEXTURE2D_DESC desc;
|
||||
ZeroMemory(&desc, sizeof(desc));
|
||||
desc.Width = (UINT)tex->Width;
|
||||
desc.Height = (UINT)tex->Height;
|
||||
desc.MipLevels = 1;
|
||||
desc.ArraySize = 1;
|
||||
#ifdef IMGUI_USE_BGRA_PACKED_COLOR
|
||||
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
|
||||
#else
|
||||
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
#endif
|
||||
desc.SampleDesc.Count = 1;
|
||||
desc.Usage = D3D11_USAGE_DEFAULT;
|
||||
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
||||
desc.CPUAccessFlags = 0;
|
||||
D3D11_SUBRESOURCE_DATA subResource;
|
||||
subResource.pSysMem = pixels;
|
||||
subResource.SysMemPitch = desc.Width * 4;
|
||||
subResource.SysMemSlicePitch = 0;
|
||||
bd->pd3dDevice->CreateTexture2D(&desc, &subResource, &backend_tex->pTexture);
|
||||
IM_ASSERT(backend_tex->pTexture != nullptr && "Backend failed to create texture!");
|
||||
|
||||
// Create texture view
|
||||
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
|
||||
ZeroMemory(&srvDesc, sizeof(srvDesc));
|
||||
#ifdef IMGUI_USE_BGRA_PACKED_COLOR
|
||||
srvDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
|
||||
#else
|
||||
srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
#endif
|
||||
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
|
||||
srvDesc.Texture2D.MipLevels = desc.MipLevels;
|
||||
srvDesc.Texture2D.MostDetailedMip = 0;
|
||||
bd->pd3dDevice->CreateShaderResourceView(backend_tex->pTexture, &srvDesc, &backend_tex->pTextureView);
|
||||
IM_ASSERT(backend_tex->pTextureView != nullptr && "Backend failed to create texture!");
|
||||
|
||||
// Store identifiers
|
||||
tex->SetTexID((ImTextureID)(intptr_t)backend_tex->pTextureView);
|
||||
tex->SetStatus(ImTextureStatus_OK);
|
||||
tex->BackendUserData = backend_tex;
|
||||
}
|
||||
else if (tex->Status == ImTextureStatus_WantUpdates)
|
||||
{
|
||||
// Update selected blocks. We only ever write to textures regions which have never been used before!
|
||||
// This backend choose to use tex->Updates[] but you can use tex->UpdateRect to upload a single region.
|
||||
ImGui_ImplDX11_Texture* backend_tex = (ImGui_ImplDX11_Texture*)tex->BackendUserData;
|
||||
IM_ASSERT(backend_tex->pTextureView == (ID3D11ShaderResourceView*)(intptr_t)tex->TexID);
|
||||
for (ImTextureRect& r : tex->Updates)
|
||||
{
|
||||
D3D11_BOX box = { (UINT)r.x, (UINT)r.y, (UINT)0, (UINT)(r.x + r.w), (UINT)(r.y + r .h), (UINT)1 };
|
||||
bd->pd3dDeviceContext->UpdateSubresource(backend_tex->pTexture, 0, &box, tex->GetPixelsAt(r.x, r.y), (UINT)tex->GetPitch(), 0);
|
||||
}
|
||||
tex->SetStatus(ImTextureStatus_OK);
|
||||
}
|
||||
if (tex->Status == ImTextureStatus_WantDestroy && tex->UnusedFrames > 0)
|
||||
ImGui_ImplDX11_DestroyTexture(tex);
|
||||
}
|
||||
|
||||
bool ImGui_ImplDX11_CreateDeviceObjects()
|
||||
{
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
if (!bd->pd3dDevice)
|
||||
return false;
|
||||
ImGui_ImplDX11_InvalidateDeviceObjects();
|
||||
|
||||
// By using D3DCompile() from <d3dcompiler.h> / d3dcompiler.lib, we introduce a dependency to a given version of d3dcompiler_XX.dll (see D3DCOMPILER_DLL_A)
|
||||
// If you would like to use this DX11 sample code but remove this dependency you can:
|
||||
// 1) compile once, save the compiled shader blobs into a file or source code and pass them to CreateVertexShader()/CreatePixelShader() [preferred solution]
|
||||
// 2) use code to detect any version of the DLL and grab a pointer to D3DCompile from the DLL.
|
||||
// See https://github.com/ocornut/imgui/pull/638 for sources and details.
|
||||
|
||||
// Create the vertex shader
|
||||
{
|
||||
static const char* vertexShader =
|
||||
"cbuffer vertexBuffer : register(b0) \
|
||||
{\
|
||||
float4x4 ProjectionMatrix; \
|
||||
};\
|
||||
struct VS_INPUT\
|
||||
{\
|
||||
float2 pos : POSITION;\
|
||||
float4 col : COLOR0;\
|
||||
float2 uv : TEXCOORD0;\
|
||||
};\
|
||||
\
|
||||
struct PS_INPUT\
|
||||
{\
|
||||
float4 pos : SV_POSITION;\
|
||||
float4 col : COLOR0;\
|
||||
float2 uv : TEXCOORD0;\
|
||||
};\
|
||||
\
|
||||
PS_INPUT main(VS_INPUT input)\
|
||||
{\
|
||||
PS_INPUT output;\
|
||||
output.pos = mul( ProjectionMatrix, float4(input.pos.xy, 0.f, 1.f));\
|
||||
output.col = input.col;\
|
||||
output.uv = input.uv;\
|
||||
return output;\
|
||||
}";
|
||||
|
||||
ID3DBlob* vertexShaderBlob;
|
||||
if (FAILED(D3DCompile(vertexShader, strlen(vertexShader), nullptr, nullptr, nullptr, "main", "vs_4_0", 0, 0, &vertexShaderBlob, nullptr)))
|
||||
return false; // NB: Pass ID3DBlob* pErrorBlob to D3DCompile() to get error showing in (const char*)pErrorBlob->GetBufferPointer(). Make sure to Release() the blob!
|
||||
if (bd->pd3dDevice->CreateVertexShader(vertexShaderBlob->GetBufferPointer(), vertexShaderBlob->GetBufferSize(), nullptr, &bd->pVertexShader) != S_OK)
|
||||
{
|
||||
vertexShaderBlob->Release();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create the input layout
|
||||
D3D11_INPUT_ELEMENT_DESC local_layout[] =
|
||||
{
|
||||
{ "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (UINT)offsetof(ImDrawVert, pos), D3D11_INPUT_PER_VERTEX_DATA, 0 },
|
||||
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (UINT)offsetof(ImDrawVert, uv), D3D11_INPUT_PER_VERTEX_DATA, 0 },
|
||||
#ifdef IMGUI_USE_BGRA_PACKED_COLOR
|
||||
{ "COLOR", 0, DXGI_FORMAT_B8G8R8A8_UNORM, 0, (UINT)offsetof(ImDrawVert, col), D3D11_INPUT_PER_VERTEX_DATA, 0 },
|
||||
#else
|
||||
{ "COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, (UINT)offsetof(ImDrawVert, col), D3D11_INPUT_PER_VERTEX_DATA, 0 },
|
||||
#endif
|
||||
};
|
||||
if (bd->pd3dDevice->CreateInputLayout(local_layout, 3, vertexShaderBlob->GetBufferPointer(), vertexShaderBlob->GetBufferSize(), &bd->pInputLayout) != S_OK)
|
||||
{
|
||||
vertexShaderBlob->Release();
|
||||
return false;
|
||||
}
|
||||
vertexShaderBlob->Release();
|
||||
|
||||
// Create the constant buffer
|
||||
{
|
||||
D3D11_BUFFER_DESC desc = {};
|
||||
desc.ByteWidth = sizeof(VERTEX_CONSTANT_BUFFER_DX11);
|
||||
desc.Usage = D3D11_USAGE_DYNAMIC;
|
||||
desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
|
||||
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
|
||||
desc.MiscFlags = 0;
|
||||
bd->pd3dDevice->CreateBuffer(&desc, nullptr, &bd->pVertexConstantBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the pixel shader
|
||||
{
|
||||
static const char* pixelShader =
|
||||
"struct PS_INPUT\
|
||||
{\
|
||||
float4 pos : SV_POSITION;\
|
||||
float4 col : COLOR0;\
|
||||
float2 uv : TEXCOORD0;\
|
||||
};\
|
||||
sampler sampler0;\
|
||||
Texture2D texture0;\
|
||||
\
|
||||
float4 main(PS_INPUT input) : SV_Target\
|
||||
{\
|
||||
float4 out_col = input.col * texture0.Sample(sampler0, input.uv); \
|
||||
return out_col; \
|
||||
}";
|
||||
|
||||
ID3DBlob* pixelShaderBlob;
|
||||
if (FAILED(D3DCompile(pixelShader, strlen(pixelShader), nullptr, nullptr, nullptr, "main", "ps_4_0", 0, 0, &pixelShaderBlob, nullptr)))
|
||||
return false; // NB: Pass ID3DBlob* pErrorBlob to D3DCompile() to get error showing in (const char*)pErrorBlob->GetBufferPointer(). Make sure to Release() the blob!
|
||||
if (bd->pd3dDevice->CreatePixelShader(pixelShaderBlob->GetBufferPointer(), pixelShaderBlob->GetBufferSize(), nullptr, &bd->pPixelShader) != S_OK)
|
||||
{
|
||||
pixelShaderBlob->Release();
|
||||
return false;
|
||||
}
|
||||
pixelShaderBlob->Release();
|
||||
}
|
||||
|
||||
// Create the blending setup
|
||||
{
|
||||
D3D11_BLEND_DESC desc;
|
||||
ZeroMemory(&desc, sizeof(desc));
|
||||
desc.AlphaToCoverageEnable = false;
|
||||
desc.RenderTarget[0].BlendEnable = true;
|
||||
desc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;
|
||||
desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
|
||||
desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
|
||||
desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
|
||||
desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA;
|
||||
desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
|
||||
desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
|
||||
bd->pd3dDevice->CreateBlendState(&desc, &bd->pBlendState);
|
||||
}
|
||||
|
||||
// Create the rasterizer state
|
||||
{
|
||||
D3D11_RASTERIZER_DESC desc;
|
||||
ZeroMemory(&desc, sizeof(desc));
|
||||
desc.FillMode = D3D11_FILL_SOLID;
|
||||
desc.CullMode = D3D11_CULL_NONE;
|
||||
desc.ScissorEnable = true;
|
||||
desc.DepthClipEnable = true;
|
||||
bd->pd3dDevice->CreateRasterizerState(&desc, &bd->pRasterizerState);
|
||||
}
|
||||
|
||||
// Create depth-stencil State
|
||||
{
|
||||
D3D11_DEPTH_STENCIL_DESC desc;
|
||||
ZeroMemory(&desc, sizeof(desc));
|
||||
desc.DepthEnable = false;
|
||||
desc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
|
||||
desc.DepthFunc = D3D11_COMPARISON_ALWAYS;
|
||||
desc.StencilEnable = false;
|
||||
desc.FrontFace.StencilFailOp = desc.FrontFace.StencilDepthFailOp = desc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
|
||||
desc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
|
||||
desc.BackFace = desc.FrontFace;
|
||||
bd->pd3dDevice->CreateDepthStencilState(&desc, &bd->pDepthStencilState);
|
||||
}
|
||||
|
||||
// Create texture sampler
|
||||
// (Bilinear sampling is required by default. Set 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow point/nearest sampling)
|
||||
{
|
||||
D3D11_SAMPLER_DESC desc;
|
||||
ZeroMemory(&desc, sizeof(desc));
|
||||
desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
|
||||
desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||
desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||
desc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||
desc.MipLODBias = 0.f;
|
||||
desc.ComparisonFunc = D3D11_COMPARISON_ALWAYS;
|
||||
desc.MinLOD = 0.f;
|
||||
desc.MaxLOD = 0.f;
|
||||
bd->pd3dDevice->CreateSamplerState(&desc, &bd->pTexSamplerLinear);
|
||||
desc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT;
|
||||
bd->pd3dDevice->CreateSamplerState(&desc, &bd->pTexSamplerNearest);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImGui_ImplDX11_InvalidateDeviceObjects()
|
||||
{
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
if (!bd->pd3dDevice)
|
||||
return;
|
||||
|
||||
// Destroy all textures
|
||||
for (ImTextureData* tex : ImGui::GetPlatformIO().Textures)
|
||||
if (tex->RefCount == 1)
|
||||
ImGui_ImplDX11_DestroyTexture(tex);
|
||||
|
||||
if (bd->pTexSamplerLinear) { bd->pTexSamplerLinear->Release(); bd->pTexSamplerLinear = nullptr; }
|
||||
if (bd->pTexSamplerNearest) { bd->pTexSamplerNearest->Release(); bd->pTexSamplerNearest = nullptr; }
|
||||
if (bd->pIB) { bd->pIB->Release(); bd->pIB = nullptr; }
|
||||
if (bd->pVB) { bd->pVB->Release(); bd->pVB = nullptr; }
|
||||
if (bd->pBlendState) { bd->pBlendState->Release(); bd->pBlendState = nullptr; }
|
||||
if (bd->pDepthStencilState) { bd->pDepthStencilState->Release(); bd->pDepthStencilState = nullptr; }
|
||||
if (bd->pRasterizerState) { bd->pRasterizerState->Release(); bd->pRasterizerState = nullptr; }
|
||||
if (bd->pPixelShader) { bd->pPixelShader->Release(); bd->pPixelShader = nullptr; }
|
||||
if (bd->pVertexConstantBuffer) { bd->pVertexConstantBuffer->Release(); bd->pVertexConstantBuffer = nullptr; }
|
||||
if (bd->pInputLayout) { bd->pInputLayout->Release(); bd->pInputLayout = nullptr; }
|
||||
if (bd->pVertexShader) { bd->pVertexShader->Release(); bd->pVertexShader = nullptr; }
|
||||
}
|
||||
|
||||
bool ImGui_ImplDX11_Init(ID3D11Device* device, ID3D11DeviceContext* device_context)
|
||||
{
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
IMGUI_CHECKVERSION();
|
||||
IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!");
|
||||
|
||||
// Setup backend capabilities flags
|
||||
ImGui_ImplDX11_Data* bd = IM_NEW(ImGui_ImplDX11_Data)();
|
||||
io.BackendRendererUserData = (void*)bd;
|
||||
io.BackendRendererName = "imgui_impl_dx11";
|
||||
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
|
||||
io.BackendFlags |= ImGuiBackendFlags_RendererHasTextures; // We can honor ImGuiPlatformIO::Textures[] requests during render.
|
||||
|
||||
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
|
||||
platform_io.Renderer_TextureMaxWidth = platform_io.Renderer_TextureMaxHeight = D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION;
|
||||
|
||||
// Get factory from device
|
||||
IDXGIDevice* pDXGIDevice = nullptr;
|
||||
IDXGIAdapter* pDXGIAdapter = nullptr;
|
||||
IDXGIFactory* pFactory = nullptr;
|
||||
|
||||
if (device->QueryInterface(IID_PPV_ARGS(&pDXGIDevice)) == S_OK)
|
||||
if (pDXGIDevice->GetParent(IID_PPV_ARGS(&pDXGIAdapter)) == S_OK)
|
||||
if (pDXGIAdapter->GetParent(IID_PPV_ARGS(&pFactory)) == S_OK)
|
||||
{
|
||||
bd->pd3dDevice = device;
|
||||
bd->pd3dDeviceContext = device_context;
|
||||
bd->pFactory = pFactory;
|
||||
}
|
||||
if (pDXGIDevice) pDXGIDevice->Release();
|
||||
if (pDXGIAdapter) pDXGIAdapter->Release();
|
||||
bd->pd3dDevice->AddRef();
|
||||
bd->pd3dDeviceContext->AddRef();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImGui_ImplDX11_Shutdown()
|
||||
{
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?");
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
|
||||
|
||||
ImGui_ImplDX11_InvalidateDeviceObjects();
|
||||
if (bd->pFactory) { bd->pFactory->Release(); }
|
||||
if (bd->pd3dDevice) { bd->pd3dDevice->Release(); }
|
||||
if (bd->pd3dDeviceContext) { bd->pd3dDeviceContext->Release(); }
|
||||
|
||||
io.BackendRendererName = nullptr;
|
||||
io.BackendRendererUserData = nullptr;
|
||||
io.BackendFlags &= ~(ImGuiBackendFlags_RendererHasVtxOffset | ImGuiBackendFlags_RendererHasTextures);
|
||||
platform_io.ClearRendererHandlers();
|
||||
IM_DELETE(bd);
|
||||
}
|
||||
|
||||
void ImGui_ImplDX11_NewFrame()
|
||||
{
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
IM_ASSERT(bd != nullptr && "Context or backend not initialized! Did you call ImGui_ImplDX11_Init()?");
|
||||
|
||||
if (!bd->pVertexShader)
|
||||
if (!ImGui_ImplDX11_CreateDeviceObjects())
|
||||
IM_ASSERT(0 && "ImGui_ImplDX11_CreateDeviceObjects() failed!");
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
@@ -0,0 +1,52 @@
|
||||
// dear imgui: Renderer Backend for DirectX11
|
||||
// This needs to be used along with a Platform Backend (e.g. Win32)
|
||||
|
||||
// Implemented features:
|
||||
// [X] Renderer: User texture binding. Use 'ID3D11ShaderResourceView*' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef!
|
||||
// [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset).
|
||||
// [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures).
|
||||
// [X] Renderer: Expose selected render state for draw callbacks to use. Access in '(ImGui_ImplXXXX_RenderState*)GetPlatformIO().Renderer_RenderState'.
|
||||
|
||||
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
|
||||
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
|
||||
// Learn about Dear ImGui:
|
||||
// - FAQ https://dearimgui.com/faq
|
||||
// - Getting Started https://dearimgui.com/getting-started
|
||||
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
|
||||
// - Introduction, links and more at the top of imgui.cpp
|
||||
|
||||
#pragma once
|
||||
#include "imgui.h" // IMGUI_IMPL_API
|
||||
#ifndef IMGUI_DISABLE
|
||||
|
||||
struct ID3D11Device;
|
||||
struct ID3D11DeviceContext;
|
||||
struct ID3D11SamplerState;
|
||||
struct ID3D11Buffer;
|
||||
|
||||
// Follow "Getting Started" link and check examples/ folder to learn about using backends!
|
||||
IMGUI_IMPL_API bool ImGui_ImplDX11_Init(ID3D11Device* device, ID3D11DeviceContext* device_context);
|
||||
IMGUI_IMPL_API void ImGui_ImplDX11_Shutdown();
|
||||
IMGUI_IMPL_API void ImGui_ImplDX11_NewFrame();
|
||||
IMGUI_IMPL_API void ImGui_ImplDX11_RenderDrawData(ImDrawData* draw_data);
|
||||
|
||||
// Use if you want to reset your rendering device without losing Dear ImGui state.
|
||||
IMGUI_IMPL_API bool ImGui_ImplDX11_CreateDeviceObjects();
|
||||
IMGUI_IMPL_API void ImGui_ImplDX11_InvalidateDeviceObjects();
|
||||
|
||||
// (Advanced) Use e.g. if you need to precisely control the timing of texture updates (e.g. for staged rendering), by setting ImDrawData::Textures = nullptr to handle this manually.
|
||||
IMGUI_IMPL_API void ImGui_ImplDX11_UpdateTexture(ImTextureData* tex);
|
||||
|
||||
// [BETA] Selected render state data shared with callbacks.
|
||||
// This is temporarily stored in GetPlatformIO().Renderer_RenderState during the ImGui_ImplDX11_RenderDrawData() call.
|
||||
// (Please open an issue if you feel you need access to more data)
|
||||
struct ImGui_ImplDX11_RenderState
|
||||
{
|
||||
ID3D11Device* Device;
|
||||
ID3D11DeviceContext* DeviceContext;
|
||||
ID3D11SamplerState* SamplerLinear;
|
||||
ID3D11SamplerState* SamplerNearest;
|
||||
ID3D11Buffer* VertexConstantBuffer;
|
||||
};
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
Vendored
+1
@@ -39,6 +39,7 @@
|
||||
#define ROBIN_HOOD_VERSION_PATCH 2 // for backwards-compatible bug fixes
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
|
||||
@@ -38,11 +38,12 @@ std::vector<Analog> &games::bc::get_analogs() {
|
||||
if (analogs.empty()) {
|
||||
analogs = GameAPI::Analogs::getAnalogs("Busou Shinki: Armored Princess Battle Conductor");
|
||||
|
||||
GameAPI::Analogs::sortAnalogs(
|
||||
&analogs,
|
||||
"Stick X",
|
||||
"Stick Y"
|
||||
);
|
||||
using GameAPI::Analogs::AnalogType;
|
||||
|
||||
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
|
||||
{ "Stick X", AnalogType::LinearCentered },
|
||||
{ "Stick Y", AnalogType::LinearCentered }
|
||||
});
|
||||
}
|
||||
|
||||
return analogs;
|
||||
|
||||
@@ -45,13 +45,14 @@ std::vector<Analog> &games::ccj::get_analogs() {
|
||||
if (analogs.empty()) {
|
||||
analogs = GameAPI::Analogs::getAnalogs("Chase Chase Jokers");
|
||||
|
||||
GameAPI::Analogs::sortAnalogs(
|
||||
&analogs,
|
||||
"Joystick X",
|
||||
"Joystick Y",
|
||||
"Trackball DX",
|
||||
"Trackball DY"
|
||||
);
|
||||
using GameAPI::Analogs::AnalogType;
|
||||
|
||||
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
|
||||
{ "Joystick X", AnalogType::LinearCentered },
|
||||
{ "Joystick Y", AnalogType::LinearCentered },
|
||||
{ "Trackball DX", AnalogType::LinearCentered },
|
||||
{ "Trackball DY", AnalogType::LinearCentered }
|
||||
});
|
||||
}
|
||||
|
||||
return analogs;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "util/logging.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "games/io.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "util/utils.h"
|
||||
#include "io.h"
|
||||
|
||||
@@ -112,7 +113,7 @@ namespace games::ccj {
|
||||
static std::chrono::time_point<std::chrono::steady_clock> lastModified = std::chrono::steady_clock::now();
|
||||
static std::chrono::milliseconds debounceDuration(100);
|
||||
auto currentTime = std::chrono::steady_clock::now();
|
||||
bool pressed = get_async_secondary_mouse() & 0x8000;
|
||||
bool pressed = get_async_secondary_mouse();
|
||||
bool focused = GetForegroundWindow() == hWnd;
|
||||
|
||||
if (focused && MOUSE_TRACKBALL_USE_TOGGLE && pressed && (currentTime - lastModified > debounceDuration)) {
|
||||
@@ -243,6 +244,7 @@ namespace games::ccj {
|
||||
log_info("trackball", "thread start, use mouse: {}, toggle: {}", MOUSE_TRACKBALL, MOUSE_TRACKBALL_USE_TOGGLE);
|
||||
|
||||
tbThread = new std::thread([&] {
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
while (tbThreadRunning) {
|
||||
if (hWnd && wndProc) {
|
||||
wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle);
|
||||
@@ -251,7 +253,7 @@ namespace games::ccj {
|
||||
if (!tbThreadRunning)
|
||||
break;
|
||||
|
||||
std::this_thread::sleep_for(10ms);
|
||||
timer.sleep(10);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "p3io/usbmem.h"
|
||||
|
||||
#include "p4io/p4io.h"
|
||||
#include <cstring>
|
||||
|
||||
using namespace acioemu;
|
||||
|
||||
@@ -53,9 +54,9 @@ namespace games::ddr {
|
||||
return SendMessage_real(hWnd, Msg, wParam, lParam);
|
||||
}
|
||||
|
||||
bool contains_only_ascii(const std::string& str) {
|
||||
bool contains_only_ascii(const std::wstring& str) {
|
||||
for (auto c: str) {
|
||||
if (static_cast<unsigned char>(c) > 127) {
|
||||
if (c > 127) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -80,24 +81,24 @@ namespace games::ddr {
|
||||
}
|
||||
|
||||
if (fileutils::dir_exists(dir)) {
|
||||
log_info("ddr", "looking for codecs in this directory: {}", dir.string());
|
||||
log_info("ddr", "looking for codecs in this directory: {}", dir);
|
||||
} else {
|
||||
log_info("ddr", "codecs directory not found: {}", dir.string());
|
||||
log_info("ddr", "codecs directory not found: {}", dir);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto &file : std::filesystem::directory_iterator(dir)) {
|
||||
const auto &filename = file.path().filename();
|
||||
const auto extension = strtolower(filename.extension().string());
|
||||
const auto extension = filename.extension().wstring();
|
||||
|
||||
if (extension != ".dll") {
|
||||
if (wcsicmp(extension.c_str(), L".dll") != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log_info("ddr", "found DLL: {}, size: {} bytes", filename.string(), file.file_size());
|
||||
if (filename == "k-clvsd.dll" || filename.string().find("xactengine") == 0) {
|
||||
log_info("ddr", "found DLL: {}, size: {} bytes", filename, file.file_size());
|
||||
if (filename == "k-clvsd.dll" || filename.wstring().find(L"xactengine") == 0) {
|
||||
const std::wstring wcmd = L"regsvr32.exe /s \"" + file.path().wstring() + L"\"";
|
||||
const std::string cmd = "regsvr32.exe /s \"" + file.path().string() + "\"";
|
||||
const std::string cmd = fmt::format("regsvr32.exe /s \"{}\"", file.path());
|
||||
|
||||
int result = 0;
|
||||
std::thread t([wcmd, &result]() {
|
||||
@@ -111,12 +112,12 @@ namespace games::ddr {
|
||||
log_warning("ddr", "`{}` failed, returned {}", cmd, result);
|
||||
deferredlogs::defer_error_messages({
|
||||
"DDR codec registration failure",
|
||||
fmt::format(" {} failed to register with error {}", filename.string(), result)});
|
||||
fmt::format(" {} failed to register with error {}", filename, result)});
|
||||
}
|
||||
|
||||
static std::once_flag printed;
|
||||
std::call_once(printed, [file]() {
|
||||
if (!contains_only_ascii(file.path().string())) {
|
||||
if (!contains_only_ascii(file.path().wstring())) {
|
||||
log_warning(
|
||||
"ddr",
|
||||
"BAD PATH ERROR\n\n"
|
||||
@@ -290,4 +291,50 @@ namespace games::ddr {
|
||||
// dispose device hook
|
||||
devicehook_dispose();
|
||||
}
|
||||
|
||||
static void get_analog_xy_axis(Analog &analog, bool &less, bool &more) {
|
||||
if (!analog.isSet()) {
|
||||
return;
|
||||
}
|
||||
const auto value = GameAPI::Analogs::getState(RI_MGR, analog);
|
||||
|
||||
// stepmania linux source:
|
||||
// https://github.com/stepmania/stepmania/blob/d55acb1ba26f1c5b5e3048d6d6c0bd116625216f/src/arch/InputHandler/InputHandler_Linux_Event.cpp#L410
|
||||
|
||||
// for example, value cap with range [0, 255]:
|
||||
// 127 = 0.498, neutral
|
||||
// 128 = 0.502, both
|
||||
// apparently some adapters report values like above; this obviously makes a lot of
|
||||
// assumptions about bit width of the HID value cap and how the adapter reports
|
||||
// neutral/both values, but this is good enough for parity with stepmania and its many forks
|
||||
if (0.5001f < value && value < 0.75f) {
|
||||
less |= true;
|
||||
more |= true;
|
||||
|
||||
} else if (value <= 0.25f) {
|
||||
less |= true;
|
||||
} else if (value >= 0.75f) {
|
||||
more |= true;
|
||||
}
|
||||
}
|
||||
|
||||
void get_analog_x_axis(int player, bool &left, bool &right) {
|
||||
if (player != 1 && player != 2) {
|
||||
log_fatal("ddr", "invalid player number: {}, expected 1 or 2", player);
|
||||
return;
|
||||
}
|
||||
|
||||
auto &analog = get_analogs().at(player == 1 ? Analogs::P1_LEFT_RIGHT : Analogs::P2_LEFT_RIGHT);
|
||||
get_analog_xy_axis(analog, left, right);
|
||||
}
|
||||
|
||||
void get_analog_y_axis(int player, bool &up, bool &down) {
|
||||
if (player != 1 && player != 2) {
|
||||
log_fatal("ddr", "invalid player number: {}, expected 1 or 2", player);
|
||||
return;
|
||||
}
|
||||
|
||||
auto &analog = get_analogs().at(player == 1 ? Analogs::P1_UP_DOWN : Analogs::P2_UP_DOWN);
|
||||
get_analog_xy_axis(analog, up, down);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,4 +23,7 @@ namespace games::ddr {
|
||||
private:
|
||||
void register_codecs();
|
||||
};
|
||||
|
||||
void get_analog_x_axis(int player, bool &left, bool &right);
|
||||
void get_analog_y_axis(int player, bool &up, bool &down);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,50 @@ std::vector<Button> &games::ddr::get_buttons() {
|
||||
return analogs;
|
||||
}
|
||||
|
||||
|
||||
std::string games::ddr::get_buttons_help() {
|
||||
// keep to max 100 characters wide
|
||||
return
|
||||
"For DDR pad arrows, double check that simultaneous Left+Right and Up+Down can be detected.\n"
|
||||
"You should boot the game, enter test menu, and use Foot Panel Check.\n\n"
|
||||
"When mapping arrows, try the following in order:\n\n"
|
||||
" 1. If your controller uses face buttons (A/B/X/Y), bind them here.\n"
|
||||
" 2. If your controller supports XInput, use that.\n"
|
||||
" 3. If your controller uses analog axis for arrows, try Analogs tab.\n"
|
||||
" 4. Otherwise, you will need to use remapping software or get a better adapter."
|
||||
;
|
||||
}
|
||||
|
||||
std::vector<Analog> &games::ddr::get_analogs() {
|
||||
static std::vector<Analog> analogs;
|
||||
|
||||
if (analogs.empty()) {
|
||||
analogs = GameAPI::Analogs::getAnalogs("Dance Dance Revolution");
|
||||
|
||||
using namespace GameAPI::Analogs;
|
||||
|
||||
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
|
||||
{ "P1 Left-Right (Axis Fix)", AnalogType::LinearCentered },
|
||||
{ "P1 Up-Down (Axis Fix)", AnalogType::LinearCentered },
|
||||
{ "P2 Left-Right (Axis Fix)", AnalogType::LinearCentered },
|
||||
{ "P2 Up-Down (Axis Fix)", AnalogType::LinearCentered }
|
||||
});
|
||||
}
|
||||
return analogs;
|
||||
}
|
||||
|
||||
std::string games::ddr::get_analogs_help() {
|
||||
// keep to max 100 characters wide
|
||||
return
|
||||
"Only use this if your DDR pad outputs analog axis for arrows.\n\n"
|
||||
"If the pad uses face buttons (A/B/X/Y), use Buttons tab instead.\n\n"
|
||||
"Spice will treat values <=25% as Left, >=75% as Right, ~=50% as neutral,\n"
|
||||
"and value between 50% and 75% as both arrows.\n\n"
|
||||
"This is the classic Stepmania \"Axis Fix\" which may or may not work with\n"
|
||||
"your dance pad or your adapter."
|
||||
;
|
||||
}
|
||||
|
||||
std::vector<Light> &games::ddr::get_lights() {
|
||||
static std::vector<Light> lights;
|
||||
|
||||
|
||||
@@ -33,6 +33,16 @@ namespace games::ddr {
|
||||
};
|
||||
}
|
||||
|
||||
// all analogs in correct order
|
||||
namespace Analogs {
|
||||
enum {
|
||||
P1_LEFT_RIGHT,
|
||||
P1_UP_DOWN,
|
||||
P2_LEFT_RIGHT,
|
||||
P2_UP_DOWN
|
||||
};
|
||||
}
|
||||
|
||||
// all lights in correct order
|
||||
namespace Lights {
|
||||
enum {
|
||||
@@ -177,5 +187,8 @@ namespace games::ddr {
|
||||
|
||||
// getters
|
||||
std::vector<Button> &get_buttons();
|
||||
std::string get_buttons_help();
|
||||
std::string get_analogs_help();
|
||||
std::vector<Analog> &get_analogs();
|
||||
std::vector<Light> &get_lights();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "cfg/api.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
#include "../ddr.h"
|
||||
@@ -459,7 +460,8 @@ int games::ddr::DDRP3IOHandle::device_io(
|
||||
if (nOutBufferSize >= 4) {
|
||||
|
||||
// cool down
|
||||
Sleep(1);
|
||||
static thread_local timeutils::PreciseSleepTimer timer;
|
||||
timer.sleep(1);
|
||||
|
||||
// get controls as single variable (4 bytes)
|
||||
auto &controls = *(uint32_t*) lpOutBuffer;
|
||||
@@ -498,32 +500,80 @@ int games::ddr::DDRP3IOHandle::device_io(
|
||||
|
||||
// shift table
|
||||
static size_t shift_table[] = {
|
||||
30, 28, 29, 8, 9, 10, 11, 12, 24, 25, 14, 15, 16, 17, 18, 19, 20, 26, 27, 22, 23
|
||||
30, 28, 29, // service, test, coin
|
||||
8, // p1 start
|
||||
9, 10, 11, 12, // p1 panel
|
||||
24, 25, 14, 15, // p1 menu
|
||||
16, // p2 start
|
||||
17, 18, 19, 20, // p2 panel
|
||||
26, 27, 22, 23 // p2 menu
|
||||
};
|
||||
static size_t button_table[] = {
|
||||
Buttons::SERVICE,
|
||||
Buttons::TEST,
|
||||
Buttons::COIN_MECH,
|
||||
|
||||
Buttons::P1_START,
|
||||
|
||||
Buttons::P1_PANEL_UP,
|
||||
Buttons::P1_PANEL_DOWN,
|
||||
Buttons::P1_PANEL_LEFT,
|
||||
Buttons::P1_PANEL_RIGHT,
|
||||
|
||||
Buttons::P1_MENU_UP,
|
||||
Buttons::P1_MENU_DOWN,
|
||||
Buttons::P1_MENU_LEFT,
|
||||
Buttons::P1_MENU_RIGHT,
|
||||
|
||||
Buttons::P2_START,
|
||||
|
||||
Buttons::P2_PANEL_UP,
|
||||
Buttons::P2_PANEL_DOWN,
|
||||
Buttons::P2_PANEL_LEFT,
|
||||
Buttons::P2_PANEL_RIGHT,
|
||||
|
||||
Buttons::P2_MENU_UP,
|
||||
Buttons::P2_MENU_DOWN,
|
||||
Buttons::P2_MENU_LEFT,
|
||||
Buttons::P2_MENU_RIGHT,
|
||||
};
|
||||
|
||||
// get analogs
|
||||
struct {
|
||||
bool up;
|
||||
bool down;
|
||||
bool left;
|
||||
bool right;
|
||||
} analog_values[2] = {};
|
||||
games::ddr::get_analog_x_axis(1, analog_values[0].left, analog_values[0].right);
|
||||
games::ddr::get_analog_y_axis(1, analog_values[0].up, analog_values[0].down);
|
||||
games::ddr::get_analog_x_axis(2, analog_values[1].left, analog_values[1].right);
|
||||
games::ddr::get_analog_y_axis(2, analog_values[1].up, analog_values[1].down);
|
||||
if (analog_values[0].up) {
|
||||
controls |= 1 << 9;
|
||||
}
|
||||
if (analog_values[0].down) {
|
||||
controls |= 1 << 10;
|
||||
}
|
||||
if (analog_values[0].left) {
|
||||
controls |= 1 << 11;
|
||||
}
|
||||
if (analog_values[0].right) {
|
||||
controls |= 1 << 12;
|
||||
}
|
||||
if (analog_values[1].up) {
|
||||
controls |= 1 << 17;
|
||||
}
|
||||
if (analog_values[1].down) {
|
||||
controls |= 1 << 18;
|
||||
}
|
||||
if (analog_values[1].left) {
|
||||
controls |= 1 << 19;
|
||||
}
|
||||
if (analog_values[1].right) {
|
||||
controls |= 1 << 20;
|
||||
}
|
||||
|
||||
// update states
|
||||
auto &buttons = get_buttons();
|
||||
size_t count = 0;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
#include "../io.h"
|
||||
@@ -255,7 +256,8 @@ namespace games::ddr {
|
||||
case P4IO_IOCTL_GET_INPUTS: {
|
||||
|
||||
// Prevents this function from being called at its normal 2000-2500 kHz cadence and overloading the CPU, instead reduces it to 250 Hz
|
||||
Sleep(4);
|
||||
static thread_local timeutils::PreciseSleepTimer timer;
|
||||
timer.sleep(4);
|
||||
|
||||
memset(lpOutBuffer, 0, 16);
|
||||
auto controls = (uint32_t*) lpOutBuffer;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "games/game.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "util/memutils.h"
|
||||
#include "rgb_cam.h"
|
||||
|
||||
@@ -221,10 +222,11 @@ namespace games::drs {
|
||||
void start_touch() {
|
||||
std::thread t([] {
|
||||
log_info("drs", "starting touch input thread");
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
// main loop
|
||||
while (TRUE) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
timer.sleep(1);
|
||||
|
||||
TOUCH_EVENTS.clear();
|
||||
touch_get_events(TOUCH_EVENTS);
|
||||
|
||||
@@ -35,13 +35,14 @@ std::vector<Analog> &games::ftt::get_analogs() {
|
||||
if (analogs.empty()) {
|
||||
analogs = GameAPI::Analogs::getAnalogs("FutureTomTom");
|
||||
|
||||
GameAPI::Analogs::sortAnalogs(
|
||||
&analogs,
|
||||
"Pad 1",
|
||||
"Pad 2",
|
||||
"Pad 3",
|
||||
"Pad 4"
|
||||
);
|
||||
using GameAPI::Analogs::AnalogType;
|
||||
|
||||
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
|
||||
{ "Pad 1", AnalogType::LinearPositive },
|
||||
{ "Pad 2", AnalogType::LinearPositive },
|
||||
{ "Pad 3", AnalogType::LinearPositive },
|
||||
{ "Pad 4", AnalogType::LinearPositive }
|
||||
});
|
||||
}
|
||||
|
||||
return analogs;
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
#include "asio.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <cstring>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "gitadora.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace games::gitadora {
|
||||
|
||||
// Redirects the game's hard-coded "XONAR" ASIO driver lookup to the
|
||||
// driver name in ASIO_DRIVER by intercepting registry calls to
|
||||
// HKLM\SOFTWARE\ASIO. Sentinel HKEY values mark the redirected handles
|
||||
// so we can recognise them on subsequent reg* calls.
|
||||
|
||||
static const HKEY PARENT_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4001);
|
||||
static const HKEY DEVICE_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4002);
|
||||
static const char *FAKE_ASIO_DEVICE_NAME = "XONAR";
|
||||
|
||||
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
|
||||
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
|
||||
static decltype(RegOpenKeyA) *RegOpenKeyA_orig = nullptr;
|
||||
static decltype(RegOpenKeyExA) *RegOpenKeyExA_orig = nullptr;
|
||||
static decltype(RegQueryValueExA) *RegQueryValueExA_orig = nullptr;
|
||||
|
||||
static HKEY real_asio_reg_handle = nullptr;
|
||||
static HKEY real_asio_device_reg_handle = nullptr;
|
||||
|
||||
static LONG WINAPI RegOpenKeyExA_hook(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired,
|
||||
PHKEY phkResult)
|
||||
{
|
||||
if (ASIO_DRIVER.has_value() &&
|
||||
lpSubKey != nullptr &&
|
||||
phkResult != nullptr &&
|
||||
hKey == PARENT_ASIO_REG_HANDLE &&
|
||||
_stricmp(lpSubKey, FAKE_ASIO_DEVICE_NAME) == 0) {
|
||||
|
||||
*phkResult = DEVICE_ASIO_REG_HANDLE;
|
||||
|
||||
log_info("gitadora::asio", "replacing '{}' with '{}'", lpSubKey, ASIO_DRIVER.value());
|
||||
const auto result = RegOpenKeyExA_orig(
|
||||
real_asio_reg_handle,
|
||||
ASIO_DRIVER.value().c_str(),
|
||||
ulOptions,
|
||||
samDesired,
|
||||
&real_asio_device_reg_handle);
|
||||
|
||||
if (result != ERROR_SUCCESS) {
|
||||
log_warning(
|
||||
"gitadora::asio",
|
||||
"failed to open registry subkey '{}', error=0x{:x}",
|
||||
ASIO_DRIVER.value(), result);
|
||||
log_warning(
|
||||
"gitadora::asio",
|
||||
"due to improper ASIO setting, audio init will fail");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, phkResult);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
|
||||
if (ASIO_DRIVER.has_value() &&
|
||||
lpSubKey != nullptr &&
|
||||
phkResult != nullptr &&
|
||||
hKey == HKEY_LOCAL_MACHINE &&
|
||||
_stricmp(lpSubKey, "software\\asio") == 0)
|
||||
{
|
||||
*phkResult = PARENT_ASIO_REG_HANDLE;
|
||||
return RegOpenKeyA_orig(hKey, lpSubKey, &real_asio_reg_handle);
|
||||
}
|
||||
|
||||
return RegOpenKeyA_orig(hKey, lpSubKey, phkResult);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegEnumKeyA_hook(HKEY hKey, DWORD dwIndex, LPSTR lpName, DWORD cchName) {
|
||||
if (hKey == PARENT_ASIO_REG_HANDLE && ASIO_DRIVER.has_value()) {
|
||||
if (dwIndex == 0) {
|
||||
// forward to real handle just to verify the key exists; we
|
||||
// overwrite the name with our fake driver string regardless
|
||||
auto ret = RegEnumKeyA_orig(real_asio_reg_handle, dwIndex, lpName, cchName);
|
||||
if (ret == ERROR_SUCCESS && lpName != nullptr && cchName > 0) {
|
||||
log_info("gitadora::asio", "stubbing '{}' with '{}'", lpName, FAKE_ASIO_DEVICE_NAME);
|
||||
strncpy(lpName, FAKE_ASIO_DEVICE_NAME, cchName);
|
||||
lpName[cchName - 1] = '\0';
|
||||
}
|
||||
return ret;
|
||||
} else {
|
||||
return ERROR_NO_MORE_ITEMS;
|
||||
}
|
||||
}
|
||||
|
||||
return RegEnumKeyA_orig(hKey, dwIndex, lpName, cchName);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegQueryValueExA_hook(HKEY hKey, LPCSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType,
|
||||
LPBYTE lpData, LPDWORD lpcbData)
|
||||
{
|
||||
HKEY target = hKey;
|
||||
|
||||
if (ASIO_DRIVER.has_value() &&
|
||||
lpValueName != nullptr &&
|
||||
lpData != nullptr &&
|
||||
lpcbData != nullptr &&
|
||||
hKey == DEVICE_ASIO_REG_HANDLE) {
|
||||
if (_stricmp(lpValueName, "Description") == 0) {
|
||||
// engine may verify the driver name after open; ensure it still
|
||||
// sees something containing "XONAR" so the substring check passes
|
||||
const size_t len = strlen(FAKE_ASIO_DEVICE_NAME) + 1;
|
||||
if (*lpcbData < len) {
|
||||
*lpcbData = static_cast<DWORD>(len);
|
||||
return ERROR_MORE_DATA;
|
||||
}
|
||||
memcpy(lpData, FAKE_ASIO_DEVICE_NAME, len);
|
||||
*lpcbData = static_cast<DWORD>(len);
|
||||
if (lpType != nullptr) {
|
||||
*lpType = REG_SZ;
|
||||
}
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
// for everything else (CLSID etc.) defer to the real driver subkey
|
||||
target = real_asio_device_reg_handle;
|
||||
}
|
||||
|
||||
return RegQueryValueExA_orig(target, lpValueName, lpReserved, lpType, lpData, lpcbData);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegCloseKey_hook(HKEY hKey) {
|
||||
if (hKey == PARENT_ASIO_REG_HANDLE) {
|
||||
if (real_asio_reg_handle != nullptr) {
|
||||
RegCloseKey_orig(real_asio_reg_handle);
|
||||
real_asio_reg_handle = nullptr;
|
||||
}
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
if (hKey == DEVICE_ASIO_REG_HANDLE) {
|
||||
if (real_asio_device_reg_handle != nullptr) {
|
||||
RegCloseKey_orig(real_asio_device_reg_handle);
|
||||
real_asio_device_reg_handle = nullptr;
|
||||
}
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
return RegCloseKey_orig(hKey);
|
||||
}
|
||||
|
||||
void asio_hook_init() {
|
||||
if (!ASIO_DRIVER.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
log_info("gitadora::asio", "installing ASIO driver redirect: XONAR -> {}", ASIO_DRIVER.value());
|
||||
|
||||
RegCloseKey_orig = detour::iat_try(
|
||||
"RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE);
|
||||
RegEnumKeyA_orig = detour::iat_try(
|
||||
"RegEnumKeyA", RegEnumKeyA_hook, avs::game::DLL_INSTANCE);
|
||||
RegOpenKeyA_orig = detour::iat_try(
|
||||
"RegOpenKeyA", RegOpenKeyA_hook, avs::game::DLL_INSTANCE);
|
||||
RegOpenKeyExA_orig = detour::iat_try(
|
||||
"RegOpenKeyExA", RegOpenKeyExA_hook, avs::game::DLL_INSTANCE);
|
||||
RegQueryValueExA_orig = detour::iat_try(
|
||||
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
namespace games::gitadora {
|
||||
|
||||
// installs IAT registry hooks in gfdm.dll that redirect the game's
|
||||
// ASIO driver lookup (hard-coded "XONAR" substring) to a user-chosen
|
||||
// driver name read from games::gitadora::ASIO_DRIVER.
|
||||
//
|
||||
// safe to call unconditionally; if ASIO_DRIVER is unset the hooks
|
||||
// forward every call straight through to advapi32.
|
||||
void asio_hook_init();
|
||||
}
|
||||
@@ -1,18 +1,27 @@
|
||||
#include "gitadora.h"
|
||||
#include "asio.h"
|
||||
#include "handle.h"
|
||||
#include "bi2x_hook.h"
|
||||
#include <unordered_map>
|
||||
|
||||
#include <ks.h>
|
||||
#include <ksmedia.h>
|
||||
|
||||
#include "cfg/configurator.h"
|
||||
#include "hooks/audio/audio.h"
|
||||
#include "hooks/audio/mme.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "util/cpuutils.h"
|
||||
#include "util/deferlog.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/libutils.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/sigscan.h"
|
||||
#include "util/socd_cleaner.h"
|
||||
#include "util/sysutils.h"
|
||||
#include "util/utils.h"
|
||||
#include "hooks/setupapihook.h"
|
||||
|
||||
namespace games::gitadora {
|
||||
@@ -20,11 +29,13 @@ namespace games::gitadora {
|
||||
// settings
|
||||
bool TWOCHANNEL = false;
|
||||
std::optional<unsigned int> CAB_TYPE = std::nullopt;
|
||||
bool ARENA_SINGLE_WINDOW = false;
|
||||
bool P1_LEFTY = false;
|
||||
bool P2_LEFTY = false;
|
||||
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
||||
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
|
||||
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
|
||||
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
||||
bool ALLOW_REALTEK_AUDIO = false;
|
||||
|
||||
/*
|
||||
* Prevent GitaDora from creating folders on F drive
|
||||
@@ -221,13 +232,6 @@ namespace games::gitadora {
|
||||
}
|
||||
#endif
|
||||
|
||||
// arena model launches a tiny window yet backbuffer at 4k, resulting in unusable overlay
|
||||
// force scaling to make things usable
|
||||
if (!overlay::UI_SCALE_PERCENT.has_value() && is_arena_model() &&
|
||||
GRAPHICS_WINDOWED && !cfg::CONFIGURATOR_STANDALONE) {
|
||||
overlay::UI_SCALE_PERCENT = 250;
|
||||
}
|
||||
|
||||
// for guitar wail SOCD cleaning
|
||||
socd::ALGORITHM = socd::SocdAlgorithm::PreferRecent;
|
||||
|
||||
@@ -237,12 +241,334 @@ namespace games::gitadora {
|
||||
} else {
|
||||
log_info("gitadora", "guitar pick SOCD algorithm: legacy");
|
||||
}
|
||||
|
||||
#if SPICE64 && !SPICE_XP
|
||||
|
||||
if (is_arena_model()) {
|
||||
// in full screen, if single-adapter option is checked, it's functionally
|
||||
// the same as forcing a single monitor
|
||||
if (!GRAPHICS_WINDOWED && GRAPHICS_FORCE_SINGLE_ADAPTER) {
|
||||
ARENA_WINDOW_COUNT = 1;
|
||||
}
|
||||
|
||||
// figure out default settings if user didn't provide one
|
||||
if (!ARENA_WINDOW_COUNT.has_value()) {
|
||||
if (!GRAPHICS_WINDOWED && sysutils::enumerate_monitors().size() < 4) {
|
||||
log_info("gitadora", "arena model: <4 monitors, defaulting to single window mode");
|
||||
ARENA_WINDOW_COUNT = 1;
|
||||
} else {
|
||||
ARENA_WINDOW_COUNT = 4;
|
||||
}
|
||||
}
|
||||
|
||||
const int count = ARENA_WINDOW_COUNT.value();
|
||||
switch (count) {
|
||||
case 1:
|
||||
log_info("gitadora", "arena model: single-window mode");
|
||||
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
|
||||
GRAPHICS_PREVENT_SECONDARY_WINDOWS = true;
|
||||
break;
|
||||
case 2:
|
||||
if (!GRAPHICS_WINDOWED) {
|
||||
log_fatal(
|
||||
"gitadora",
|
||||
"arena model: 2-window mode is not supported in fullscreen, choose 1 or 4");
|
||||
}
|
||||
log_info("gitadora", "arena model: two-window mode");
|
||||
GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS = true;
|
||||
break;
|
||||
case 4:
|
||||
log_info("gitadora", "arena model: four-window mode");
|
||||
break;
|
||||
default:
|
||||
log_fatal(
|
||||
"gitadora",
|
||||
"arena model: unsupported window count: {}", count);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#if SPICE64 && !SPICE_XP
|
||||
|
||||
static decltype(GetDisplayConfigBufferSizes) *GetDisplayConfigBufferSizes_orig = nullptr;
|
||||
static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr;
|
||||
static decltype(DisplayConfigGetDeviceInfo) *DisplayConfigGetDeviceInfo_orig = nullptr;
|
||||
|
||||
// cached primary real monitor: its path + source/target mode entries.
|
||||
// modeInfoIdx values on the path are renumbered to 0 / 1 so the cache is self-contained.
|
||||
static DISPLAYCONFIG_PATH_INFO real_primary_path = {};
|
||||
static DISPLAYCONFIG_MODE_INFO real_primary_modes[2] = {}; // [0]=source, [1]=target
|
||||
|
||||
// fake monitors appended after the real ones. the game classifies monitors
|
||||
// by outputTechnology + connectorInstance:
|
||||
// HDMI -> main 4k monitor (real primary)
|
||||
// DP connInstance 0 -> left
|
||||
// DP connInstance 1 -> right
|
||||
// DP connInstance 2 -> small (sub/touch)
|
||||
// ids are negated on the fake monitor headers so they can be distinguished
|
||||
// from real ones in DisplayConfigGetDeviceInfo.
|
||||
struct FakeMonitor {
|
||||
LONG id;
|
||||
int width;
|
||||
int height;
|
||||
int offset_x;
|
||||
int offset_y;
|
||||
UINT32 connector_instance;
|
||||
};
|
||||
|
||||
// ORDERING MATTERS: the d3d9 wrapper (FAKE_SUBSCREEN_ADAPTER) hands out
|
||||
// adapters as "\\.\DISPLAY_SPICE_FAKE_{N}" for N=1,2,3. The game maps each
|
||||
// adapter to a swap chain role via its DisplayConfig connector instance,
|
||||
// so the entries here must be listed in the same order the wrapper enumerates
|
||||
// them: id=1 -> adapter 1 -> left, id=2 -> adapter 2 -> right, id=3 -> adapter 3 -> small.
|
||||
static constexpr FakeMonitor FAKE_MONITORS[] = {
|
||||
{ 1, 1080, 1920, -100000, -100000, 0 }, // left (DP connector instance 0)
|
||||
{ 2, 1080, 1920, -200000, -200000, 1 }, // right (DP connector instance 1)
|
||||
{ 3, 800, 1280, -300000, -300000, 2 }, // small (DP connector instance 2, touch)
|
||||
};
|
||||
static constexpr UINT32 FAKE_MONITOR_COUNT = static_cast<UINT32>(std::size(FAKE_MONITORS));
|
||||
|
||||
// call QueryDisplayConfig once, keep only the primary monitor's path and its
|
||||
// two referenced modes (source + target). modeInfoIdx values are rewritten to
|
||||
// 0 and 1 so the cache is self-consistent.
|
||||
static void cache_primary_monitor_info() {
|
||||
UINT32 path_count = 0;
|
||||
UINT32 mode_count = 0;
|
||||
if (GetDisplayConfigBufferSizes_orig(
|
||||
QDC_ONLY_ACTIVE_PATHS, &path_count, &mode_count) != ERROR_SUCCESS) {
|
||||
log_fatal("gitadora", "cache_primary_monitor_info: GetDisplayConfigBufferSizes failed");
|
||||
}
|
||||
|
||||
std::vector<DISPLAYCONFIG_PATH_INFO> all_paths(path_count);
|
||||
std::vector<DISPLAYCONFIG_MODE_INFO> all_modes(mode_count);
|
||||
if (QueryDisplayConfig_orig(
|
||||
QDC_ONLY_ACTIVE_PATHS,
|
||||
&path_count, all_paths.data(),
|
||||
&mode_count, all_modes.data(),
|
||||
nullptr) != ERROR_SUCCESS) {
|
||||
log_fatal("gitadora", "cache_primary_monitor_info: QueryDisplayConfig failed");
|
||||
}
|
||||
all_paths.resize(path_count);
|
||||
all_modes.resize(mode_count);
|
||||
|
||||
// pick the primary monitor: source mode at (0, 0)
|
||||
auto primary = std::find_if(all_paths.begin(), all_paths.end(),
|
||||
[&](const auto &p) {
|
||||
const auto idx = p.sourceInfo.modeInfoIdx;
|
||||
return idx < all_modes.size() &&
|
||||
all_modes[idx].infoType == DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE &&
|
||||
all_modes[idx].sourceMode.position.x == 0 &&
|
||||
all_modes[idx].sourceMode.position.y == 0;
|
||||
});
|
||||
if (primary == all_paths.end()) {
|
||||
log_fatal("gitadora", "cache_primary_monitor_info: no primary monitor found");
|
||||
}
|
||||
|
||||
real_primary_modes[0] = all_modes[primary->sourceInfo.modeInfoIdx];
|
||||
real_primary_modes[1] = all_modes[primary->targetInfo.modeInfoIdx];
|
||||
real_primary_path = *primary;
|
||||
real_primary_path.sourceInfo.modeInfoIdx = 0;
|
||||
real_primary_path.targetInfo.modeInfoIdx = 1;
|
||||
|
||||
log_info("gitadora", "cache_primary_monitor_info: cached primary monitor");
|
||||
}
|
||||
|
||||
static
|
||||
LONG
|
||||
WINAPI
|
||||
GetDisplayConfigBufferSizes_hook(
|
||||
UINT32 Flags,
|
||||
UINT32 *pNumPathArrayElements,
|
||||
UINT32 *pNumModeInfoArrayElements)
|
||||
{
|
||||
// populate cached primary real monitor on the first call
|
||||
static std::once_flag populate_once;
|
||||
std::call_once(populate_once, cache_primary_monitor_info);
|
||||
|
||||
// always report exactly 1 real + N fake monitors
|
||||
*pNumPathArrayElements = 1 + FAKE_MONITOR_COUNT;
|
||||
*pNumModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
|
||||
|
||||
log_info(
|
||||
"gitadora",
|
||||
"GetDisplayConfigBufferSizes: 1 real path + {} fake monitor(s)",
|
||||
FAKE_MONITOR_COUNT);
|
||||
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
// write fake monitor i into the caller's path/mode arrays. layout (single-monitor
|
||||
// assumption): index 0 in both arrays holds the cached primary real monitor, so
|
||||
// fake i occupies path slot (1 + i) and mode slots (2 + i*2) / (2 + i*2 + 1).
|
||||
static void insert_fake_monitor(
|
||||
DISPLAYCONFIG_PATH_INFO *paths,
|
||||
DISPLAYCONFIG_MODE_INFO *modes,
|
||||
UINT32 i)
|
||||
{
|
||||
const FakeMonitor &m = FAKE_MONITORS[i];
|
||||
const UINT32 src_idx = 2 + i * 2;
|
||||
const UINT32 tgt_idx = src_idx + 1;
|
||||
const LUID adapter_id { .LowPart = static_cast<DWORD>(-m.id), .HighPart = -m.id };
|
||||
const UINT32 uid = static_cast<UINT32>(-m.id);
|
||||
|
||||
paths[1 + i] = {
|
||||
.sourceInfo = {
|
||||
.adapterId = adapter_id,
|
||||
.id = uid,
|
||||
.modeInfoIdx = src_idx,
|
||||
.statusFlags = DISPLAYCONFIG_SOURCE_IN_USE,
|
||||
},
|
||||
.targetInfo = {
|
||||
.adapterId = adapter_id,
|
||||
.id = uid,
|
||||
.modeInfoIdx = tgt_idx,
|
||||
.outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL,
|
||||
.rotation = DISPLAYCONFIG_ROTATION_IDENTITY,
|
||||
.scaling = DISPLAYCONFIG_SCALING_IDENTITY,
|
||||
.refreshRate = { .Numerator = 60000, .Denominator = 1000 },
|
||||
.scanLineOrdering = DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE,
|
||||
.targetAvailable = TRUE,
|
||||
.statusFlags = DISPLAYCONFIG_TARGET_IN_USE,
|
||||
},
|
||||
.flags = DISPLAYCONFIG_PATH_ACTIVE,
|
||||
};
|
||||
|
||||
modes[src_idx] = {
|
||||
.infoType = DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE,
|
||||
.id = uid,
|
||||
.adapterId = adapter_id,
|
||||
.sourceMode = {
|
||||
.width = static_cast<UINT32>(m.width),
|
||||
.height = static_cast<UINT32>(m.height),
|
||||
.pixelFormat = DISPLAYCONFIG_PIXELFORMAT_32BPP,
|
||||
.position = { .x = m.offset_x, .y = m.offset_y },
|
||||
},
|
||||
};
|
||||
|
||||
modes[tgt_idx] = {
|
||||
.infoType = DISPLAYCONFIG_MODE_INFO_TYPE_TARGET,
|
||||
.id = uid,
|
||||
.adapterId = adapter_id,
|
||||
.targetMode = {},
|
||||
};
|
||||
|
||||
log_misc(
|
||||
"gitadora",
|
||||
"inserted fake monitor: id={}, width={}, height={}, offset_x={}, offset_y={}",
|
||||
m.id, m.width, m.height, m.offset_x, m.offset_y);
|
||||
}
|
||||
|
||||
static
|
||||
LONG
|
||||
WINAPI
|
||||
QueryDisplayConfig_hook(
|
||||
UINT32 flags,
|
||||
UINT32* numPathArrayElements,
|
||||
DISPLAYCONFIG_PATH_INFO* pathArray,
|
||||
UINT32* numModeInfoArrayElements,
|
||||
DISPLAYCONFIG_MODE_INFO* modeInfoArray,
|
||||
DISPLAYCONFIG_TOPOLOGY_ID* currentTopologyId)
|
||||
{
|
||||
// copy cached primary real monitor into caller buffers at index 0
|
||||
pathArray[0] = real_primary_path;
|
||||
modeInfoArray[0] = real_primary_modes[0];
|
||||
modeInfoArray[1] = real_primary_modes[1];
|
||||
*numPathArrayElements = 1 + FAKE_MONITOR_COUNT;
|
||||
*numModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
|
||||
if (currentTopologyId != nullptr) {
|
||||
*currentTopologyId = DISPLAYCONFIG_TOPOLOGY_EXTEND;
|
||||
}
|
||||
|
||||
log_misc("gitadora", "QueryDisplayConfig returning fake monitor paths and modes");
|
||||
|
||||
// append fake monitors after the real one
|
||||
for (UINT32 i = 0; i < FAKE_MONITOR_COUNT; i++) {
|
||||
insert_fake_monitor(pathArray, modeInfoArray, i);
|
||||
}
|
||||
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
static
|
||||
LONG
|
||||
WINAPI
|
||||
DisplayConfigGetDeviceInfo_hook(DISPLAYCONFIG_DEVICE_INFO_HEADER* requestPacket)
|
||||
{
|
||||
if (requestPacket == nullptr) {
|
||||
return DisplayConfigGetDeviceInfo_orig(requestPacket);
|
||||
}
|
||||
|
||||
// handle fake monitors (negative id) directly without calling orig
|
||||
const auto id = static_cast<int>(requestPacket->id);
|
||||
if (id < 0) {
|
||||
if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME) {
|
||||
const auto sourceName = reinterpret_cast<DISPLAYCONFIG_SOURCE_DEVICE_NAME*>(requestPacket);
|
||||
// name must match WrappedIDirect3D9::GetAdapterIdentifier
|
||||
const std::string adapter_name = fmt::format("\\\\.\\DISPLAY_SPICE_FAKE_{}", -id);
|
||||
wcscpy(sourceName->viewGdiDeviceName, s2ws(adapter_name).c_str());
|
||||
log_misc("gitadora",
|
||||
"DisplayConfigGetDeviceInfo: fake source id={} name={}", id, adapter_name);
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME) {
|
||||
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
|
||||
const LONG fake_id = -id;
|
||||
UINT32 conn_inst = 0xff;
|
||||
for (const auto& f : FAKE_MONITORS) {
|
||||
if (f.id == fake_id) {
|
||||
conn_inst = f.connector_instance;
|
||||
break;
|
||||
}
|
||||
}
|
||||
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL;
|
||||
targetName->connectorInstance = conn_inst;
|
||||
wcscpy(targetName->monitorFriendlyDeviceName, L"Spice Fake Monitor");
|
||||
wcscpy(targetName->monitorDevicePath, L"\\\\?\\SpiceFakeMonitor");
|
||||
log_misc("gitadora",
|
||||
"DisplayConfigGetDeviceInfo: fake target id={} -> DP connInst {}",
|
||||
id, targetName->connectorInstance);
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
const auto ret = DisplayConfigGetDeviceInfo_orig(requestPacket);
|
||||
if (ret != ERROR_SUCCESS ||
|
||||
requestPacket->type != DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// override the cached primary real monitor target info to look like HDMI/0
|
||||
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
|
||||
const auto& target = real_primary_path.targetInfo;
|
||||
if (target.id == targetName->header.id &&
|
||||
target.adapterId.HighPart == targetName->header.adapterId.HighPart &&
|
||||
target.adapterId.LowPart == targetName->header.adapterId.LowPart)
|
||||
{
|
||||
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI;
|
||||
targetName->connectorInstance = 0;
|
||||
log_info("gitadora",
|
||||
"overriding primary monitor (id={}) to pretend to be HDMI",
|
||||
targetName->header.id);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void GitaDoraGame::attach() {
|
||||
Game::attach();
|
||||
|
||||
// arena model launches a tiny window yet backbuffer at 4k, resulting in unusable overlay
|
||||
// force scaling to make things usable
|
||||
if (!overlay::UI_SCALE_PERCENT.has_value() && is_arena_model()) {
|
||||
log_info("gitadora", "forcing UI scale to 250% for arena model");
|
||||
overlay::UI_SCALE_PERCENT = 250;
|
||||
}
|
||||
|
||||
// modules
|
||||
HMODULE sharepj_module = libutils::try_module("libshare-pj.dll");
|
||||
HMODULE bmsd2_module = libutils::try_module("libbmsd2.dll");
|
||||
@@ -269,6 +595,12 @@ namespace games::gitadora {
|
||||
#ifdef SPICE64
|
||||
// gitadora arena model
|
||||
auto aio = libutils::try_library("libaio.dll");
|
||||
|
||||
// before we start patching and hooking things, detect invalid configuration
|
||||
if (aio != nullptr && !is_arena_model()) {
|
||||
log_fatal("gitadora", "arena model i/o (libaio.dll) detected, but <spec> is not an arena model - bad prop XML files?");
|
||||
}
|
||||
|
||||
if (aio != nullptr) {
|
||||
SETUPAPI_SETTINGS settings{};
|
||||
settings.class_guid[0] = 0x86E0D1E0;
|
||||
@@ -294,39 +626,76 @@ namespace games::gitadora {
|
||||
detour::iat_try("GetDriveTypeA", GetDriveTypeA_hook, avs::game::DLL_INSTANCE);
|
||||
detour::iat_try("CreateDirectoryA", CreateDirectoryA_hook, avs::game::DLL_INSTANCE);
|
||||
|
||||
// ASIO driver redirect (XONAR -> user-configured driver)
|
||||
asio_hook_init();
|
||||
|
||||
// volume change prevention
|
||||
hooks::audio::mme::init(avs::game::DLL_INSTANCE);
|
||||
|
||||
// touch hook
|
||||
if (ARENA_SINGLE_WINDOW) {
|
||||
// fake Realtek audio injection
|
||||
// if ASIO init succeeds, game tries to look for audio device with `Realtek` in friendly name
|
||||
// if ASIO init fails, game opens default audio device
|
||||
// therefore, it's safe to enable this hook by default regardless of ASIO preference
|
||||
// (unless the user explicitly disables it, of course)
|
||||
if (ALLOW_REALTEK_AUDIO) {
|
||||
log_info(
|
||||
"gitadora",
|
||||
"fake Realtek audio injection disabled "
|
||||
"(user's real Realtek audio may be used after successful ASIO init)");
|
||||
} else {
|
||||
log_info(
|
||||
"gitadora",
|
||||
"fake Realtek audio injection enabled "
|
||||
"(create a fake Realtek audio device to prevent crashes after successful ASIO init)");
|
||||
hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true;
|
||||
}
|
||||
|
||||
// monitor/touch hooks (windowed or full screen)
|
||||
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||
// enable touch hook for subscreen overlay
|
||||
wintouchemu::FORCE = true;
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
HMODULE gdme_module = libutils::try_module("libgdme.dll");
|
||||
#if !SPICE_XP
|
||||
|
||||
if (!GRAPHICS_WINDOWED) {
|
||||
// monitor hook: always pretend to have 1 primary real monitor + 3 fake monitors
|
||||
// (LEFT / RIGHT / SMALL) so the game accepts the arena cab topology
|
||||
GetDisplayConfigBufferSizes_orig =
|
||||
detour::iat_try("GetDisplayConfigBufferSizes",
|
||||
GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE);
|
||||
QueryDisplayConfig_orig =
|
||||
detour::iat_try("QueryDisplayConfig",
|
||||
QueryDisplayConfig_hook, avs::game::DLL_INSTANCE);
|
||||
DisplayConfigGetDeviceInfo_orig =
|
||||
detour::iat_try("DisplayConfigGetDeviceInfo",
|
||||
DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE);
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// window patch
|
||||
if (GRAPHICS_WINDOWED && !replace_pattern(
|
||||
gdme_module,
|
||||
"754185ED753D8B4118BF0000CB02",
|
||||
"9090????9090??????????????12", 0, 0))
|
||||
{
|
||||
log_warning("gitadora", "windowed mode failed");
|
||||
if (!is_arena_model()) {
|
||||
HMODULE gdme_module = libutils::try_module("libgdme.dll");
|
||||
if (GRAPHICS_WINDOWED && !replace_pattern(
|
||||
gdme_module,
|
||||
"754185ED753D8B4118BF0000CB02",
|
||||
"9090????9090??????????????12", 0, 0)) {
|
||||
log_warning("gitadora", "windowed mode failed");
|
||||
}
|
||||
}
|
||||
|
||||
HMODULE bmsd_engine_module = libutils::try_module("libbmsd-engine.dll");
|
||||
HMODULE bmsd_module = libutils::try_module("libbmsd.dll");
|
||||
|
||||
// two channel mod
|
||||
if (TWOCHANNEL) {
|
||||
bmsd2_boot_orig = detour::iat_try("bmsd2_boot", bmsd2_boot_hook, bmsd_module);
|
||||
if (TWOCHANNEL && !is_arena_model()) {
|
||||
HMODULE bmsd_engine_module = libutils::try_module("libbmsd-engine.dll");
|
||||
HMODULE bmsd_module = libutils::try_module("libbmsd.dll");
|
||||
|
||||
bmsd2_boot_orig = detour::iat_try("bmsd2_boot", bmsd2_boot_hook, bmsd_module);
|
||||
if (!(replace_pattern(bmsd_engine_module, "33000000488D", "03??????????", 0, 0) ||
|
||||
replace_pattern(bmsd_engine_module, "330000000F10", "03??????????", 0, 0)))
|
||||
{
|
||||
replace_pattern(bmsd_engine_module, "330000000F10", "03??????????", 0, 0))) {
|
||||
log_warning("gitadora", "two channel mode failed");
|
||||
}
|
||||
}
|
||||
@@ -334,4 +703,18 @@ namespace games::gitadora {
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void fix_audio_channel_mask(WAVEFORMATEX *format) {
|
||||
if (!format || format->wFormatTag != WAVE_FORMAT_EXTENSIBLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto ext = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(format);
|
||||
|
||||
// fix the legacy 7.1 channel mask to the modern surround layout
|
||||
// makes it more compatible with modern audio cards
|
||||
if (ext->dwChannelMask == KSAUDIO_SPEAKER_7POINT1) {
|
||||
ext->dwChannelMask = KSAUDIO_SPEAKER_7POINT1_SURROUND;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <windows.h>
|
||||
#include <mmreg.h>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "games/game.h"
|
||||
#include "util/socd_cleaner.h"
|
||||
@@ -11,11 +14,13 @@ namespace games::gitadora {
|
||||
// settings
|
||||
extern bool TWOCHANNEL;
|
||||
extern std::optional<unsigned int> CAB_TYPE;
|
||||
extern bool ARENA_SINGLE_WINDOW;
|
||||
extern bool P1_LEFTY;
|
||||
extern bool P2_LEFTY;
|
||||
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
||||
extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
|
||||
extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
|
||||
extern std::optional<std::string> ASIO_DRIVER;
|
||||
extern bool ALLOW_REALTEK_AUDIO;
|
||||
|
||||
class GitaDoraGame : public games::Game {
|
||||
public:
|
||||
@@ -24,6 +29,8 @@ namespace games::gitadora {
|
||||
virtual void attach() override;
|
||||
};
|
||||
|
||||
void fix_audio_channel_mask(WAVEFORMATEX *format);
|
||||
|
||||
static inline bool is_drum() {
|
||||
return (
|
||||
avs::game::is_model({ "J32", "K32", "L32" }) ||
|
||||
|
||||
@@ -86,19 +86,21 @@ std::vector<Button> &games::gitadora::get_buttons() {
|
||||
std::vector<Analog> &games::gitadora::get_analogs() {
|
||||
static std::vector<Analog> analogs;
|
||||
|
||||
using namespace GameAPI::Analogs;
|
||||
|
||||
if (analogs.empty()) {
|
||||
analogs = GameAPI::Analogs::getAnalogs("GitaDora");
|
||||
|
||||
GameAPI::Analogs::sortAnalogs(&analogs,
|
||||
"Guitar P1 Wail X",
|
||||
"Guitar P1 Wail Y",
|
||||
"Guitar P1 Wail Z",
|
||||
"Guitar P1 Knob",
|
||||
"Guitar P2 Wail X",
|
||||
"Guitar P2 Wail Y",
|
||||
"Guitar P2 Wail Z",
|
||||
"Guitar P2 Knob"
|
||||
);
|
||||
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
|
||||
{"Guitar P1 Wail X", AnalogType::LinearCentered},
|
||||
{"Guitar P1 Wail Y", AnalogType::LinearCentered},
|
||||
{"Guitar P1 Wail Z", AnalogType::LinearCentered},
|
||||
{"Guitar P1 Knob", AnalogType::Circular},
|
||||
{"Guitar P2 Wail X", AnalogType::LinearCentered},
|
||||
{"Guitar P2 Wail Y", AnalogType::LinearCentered},
|
||||
{"Guitar P2 Wail Z", AnalogType::LinearCentered},
|
||||
{"Guitar P2 Knob", AnalogType::Circular}
|
||||
});
|
||||
}
|
||||
|
||||
return analogs;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "misc/eamuse.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
#include "iidx.h"
|
||||
@@ -82,7 +83,8 @@ bool games::iidx::IIDXFMSerialHandle::FMSerialDevice::parse_msg(
|
||||
}
|
||||
|
||||
// sleep - otherwise the IO thread will go too hard on the CPU
|
||||
Sleep(1);
|
||||
static thread_local timeutils::PreciseSleepTimer timer;
|
||||
timer.sleep(1);
|
||||
|
||||
// generate message
|
||||
auto msg = this->create_msg(msg_in, 0x2E);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "misc/eamuse.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
#include "iidx.h"
|
||||
@@ -82,7 +83,8 @@ bool games::iidx::BI2XSerialHandle::BI2XDevice::parse_msg(
|
||||
}
|
||||
|
||||
// sleep - otherwise the IO thread will go too hard on the CPU
|
||||
Sleep(1);
|
||||
static thread_local timeutils::PreciseSleepTimer timer;
|
||||
timer.sleep(1);
|
||||
|
||||
// generate message
|
||||
auto msg = this->create_msg(msg_in, 0x2E);
|
||||
|
||||
@@ -67,6 +67,8 @@ namespace games::iidx {
|
||||
std::optional<std::string> SOUND_OUTPUT_DEVICE = std::nullopt;
|
||||
std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt;
|
||||
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
||||
uint32_t TT_DELAY_P1 = 0;
|
||||
uint32_t TT_DELAY_P2 = 0;
|
||||
uint8_t DIGITAL_TT_SENS = 4;
|
||||
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE = std::nullopt;
|
||||
std::optional<std::string> SCREEN_MODE = std::nullopt;
|
||||
@@ -781,7 +783,7 @@ namespace games::iidx {
|
||||
}
|
||||
|
||||
// return higher 8 bit
|
||||
return (uint8_t) (ret_value >> 2);
|
||||
return (uint8_t)(ret_value >> 2);
|
||||
}
|
||||
|
||||
unsigned char get_slider(uint8_t slider) {
|
||||
@@ -891,38 +893,10 @@ namespace games::iidx {
|
||||
}
|
||||
}
|
||||
|
||||
// patch iidx32+ for asio compatibility
|
||||
// only do this if NOT wasapi (as opposed to checking if it's asio)
|
||||
// the patch is only really needed for (some) non-XONAR devices but since people sometimes disguise
|
||||
// other devices as a XONAR, don't check for the exact string (common ASIO workaround for INF)
|
||||
if (avs::game::is_ext(2024090100, INT_MAX) &&
|
||||
!(SOUND_OUTPUT_DEVICE_IN_EFFECT.has_value() &&
|
||||
SOUND_OUTPUT_DEVICE_IN_EFFECT.value() == "wasapi")) {
|
||||
|
||||
// in iidx32 final:
|
||||
// ff 50 08 call QWORD PTR [rax+0x8] ; ASIO instance AddRef
|
||||
// 48 8b 4b 08 mov rcx,QWORD PTR [rbx+0x8]
|
||||
// 48 8b 01 mov rax,QWORD PTR [rcx]
|
||||
// ff 50 08 call QWORD PTR [rax+0x8] ; ASIO instance AddRef
|
||||
|
||||
intptr_t result = replace_pattern(
|
||||
avs::game::DLL_INSTANCE,
|
||||
"FF50????????????????FF50??4533C94533C0418D51",
|
||||
"????????????????????909090??????????????????",
|
||||
0, 0);
|
||||
|
||||
if (result == 0) {
|
||||
log_warning(
|
||||
"iidx",
|
||||
"Failed to apply ASIO compatibility fix for iidx32+. "
|
||||
"Unless patches are applied, your ASIO device may hang or fail to work");
|
||||
} else {
|
||||
log_info(
|
||||
"iidx",
|
||||
"Successfully applied ASIO compatibility fix for iidx32+ using signature matching @ 0x{:x}.",
|
||||
result);
|
||||
}
|
||||
}
|
||||
// note: the iidx32+ ASIO refcount bug (a duplicate AddRef on the ASIO instance with
|
||||
// no matching Release, which leaks the driver and can hang non-XONAR devices) is now
|
||||
// handled transparently by the WrappedAsio proxy (see hooks/audio/asio_proxy.cpp),
|
||||
// so no game-DLL signature patch is needed here anymore
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -63,16 +63,17 @@ std::vector<Analog> &games::iidx::get_analogs() {
|
||||
if (analogs.empty()) {
|
||||
analogs = GameAPI::Analogs::getAnalogs("Beatmania IIDX");
|
||||
|
||||
GameAPI::Analogs::sortAnalogs(
|
||||
&analogs,
|
||||
"Turntable P1",
|
||||
"Turntable P2",
|
||||
"VEFX",
|
||||
"Low-EQ",
|
||||
"Hi-EQ",
|
||||
"Filter",
|
||||
"Play Volume"
|
||||
);
|
||||
using namespace GameAPI::Analogs;
|
||||
|
||||
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
|
||||
{ "Turntable P1", AnalogType::Circular },
|
||||
{ "Turntable P2", AnalogType::Circular },
|
||||
{ "VEFX", AnalogType::LinearPositive },
|
||||
{ "Low-EQ", AnalogType::LinearPositive },
|
||||
{ "Hi-EQ", AnalogType::LinearPositive },
|
||||
{ "Filter", AnalogType::LinearPositive },
|
||||
{ "Play Volume", AnalogType::LinearPositive }
|
||||
});
|
||||
}
|
||||
return analogs;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#if SPICE64 && !SPICE_XP
|
||||
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "util/utils.h"
|
||||
#include "mf_wrappers.h"
|
||||
|
||||
@@ -399,6 +400,7 @@ namespace games::iidx {
|
||||
void IIDXLocalCamera::CreateThread() {
|
||||
// Create thread
|
||||
m_drawThread = new std::thread([this]() {
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
|
||||
|
||||
double accumulator = 0.0;
|
||||
@@ -412,7 +414,7 @@ namespace games::iidx {
|
||||
accumulator -= 1.0;
|
||||
floorFrameTimeMicroSec += 1;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(floorFrameTimeMicroSec));
|
||||
timer.sleep(std::chrono::microseconds(floorFrameTimeMicroSec));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "touch/touch.h"
|
||||
#include "util/libutils.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
|
||||
#define POKE_NATIVE_TOUCH 0
|
||||
|
||||
@@ -200,6 +201,7 @@ namespace games::iidx::poke {
|
||||
// create new thread
|
||||
THREAD_RUNNING = true;
|
||||
THREAD = new std::thread([] {
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
// log
|
||||
log_info("poke", "enabled");
|
||||
@@ -319,7 +321,7 @@ namespace games::iidx::poke {
|
||||
}
|
||||
|
||||
// slow down
|
||||
Sleep(50);
|
||||
timer.sleep(50);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
||||
@@ -92,6 +92,9 @@ namespace games {
|
||||
games.push_back(ddr);
|
||||
buttons.insert({ ddr, ddr::get_buttons() });
|
||||
lights.insert({ ddr, ddr::get_lights() });
|
||||
buttons_help.insert({ ddr, ddr::get_buttons_help() });
|
||||
analogs_help.insert({ ddr, ddr::get_analogs_help() });
|
||||
analogs.insert({ ddr, ddr::get_analogs() });
|
||||
file_hints[ddr].push_back({"ddr.dll"});
|
||||
file_hints[ddr].push_back({"mdxja_945.dll"});
|
||||
file_hints[ddr].push_back({"arkmdxp3.dll"});
|
||||
|
||||
+86
-13
@@ -19,7 +19,7 @@
|
||||
namespace games::jb {
|
||||
|
||||
// touch stuff
|
||||
bool TOUCH_LEGACY_BOX = false;
|
||||
JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
|
||||
static bool TOUCH_ENABLE = false;
|
||||
static bool TOUCH_ATTACHED = false;
|
||||
static bool IS_PORTRAIT = true;
|
||||
@@ -78,7 +78,7 @@ namespace games::jb {
|
||||
// note that the IO code in device.cpp will correctly compensate for orientation, depending on the model.
|
||||
TOUCH_POINTS.clear();
|
||||
touch_get_points(TOUCH_POINTS);
|
||||
if (TOUCH_LEGACY_BOX) {
|
||||
if (TOUCH_ALGORITHM == Legacy) {
|
||||
auto offset = IS_PORTRAIT ? 580 : 0;
|
||||
for (auto &tp : TOUCH_POINTS) {
|
||||
|
||||
@@ -94,6 +94,17 @@ namespace games::jb {
|
||||
}
|
||||
} else {
|
||||
for (auto &tp : TOUCH_POINTS) {
|
||||
// check window out of bounds
|
||||
if (IS_PORTRAIT) {
|
||||
if (tp.x > 768 || tp.y > 1360) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (tp.x > 1360 || tp.y > 768) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
int x_relative = tp.x;
|
||||
int y_relative = tp.y;
|
||||
|
||||
@@ -110,19 +121,66 @@ namespace games::jb {
|
||||
y_relative -= 8;
|
||||
}
|
||||
|
||||
if (x_relative < 0 || y_relative < 0) {
|
||||
int x_index = -1;
|
||||
int x_hitbox = 0;
|
||||
int y_index = -1;
|
||||
int y_hitbox = 0;
|
||||
|
||||
// x_hitbox and y_hitbox is relative to top-left pixel of each button
|
||||
if (x_relative >= 0) {
|
||||
x_index = x_relative / JB_BUTTON_HITBOX;
|
||||
x_hitbox = x_relative % JB_BUTTON_HITBOX;
|
||||
}
|
||||
if (y_relative >= 0) {
|
||||
y_index = y_relative / JB_BUTTON_HITBOX;
|
||||
y_hitbox = y_relative % JB_BUTTON_HITBOX;
|
||||
}
|
||||
|
||||
// log_info("jb", "raw={}, idx={}, hitbox={}", tp.x, x_index, x_hitbox);
|
||||
|
||||
if (TOUCH_ALGORITHM == Improved) {
|
||||
if (IS_PORTRAIT) {
|
||||
// extend to left border
|
||||
if (x_relative < 0) {
|
||||
x_index = 0;
|
||||
}
|
||||
// right and bottom borders are covered by the hit box
|
||||
} else {
|
||||
// extend to top border
|
||||
if (y_relative < 0) {
|
||||
y_index = 0;
|
||||
}
|
||||
// extend to left border
|
||||
if (x_relative < 0) {
|
||||
x_index = 0;
|
||||
}
|
||||
// bottom border is covered by the hit box
|
||||
// rightmost edge - ignore them entirely
|
||||
if (x_index == 3 && JB_BUTTON_SIZE < x_hitbox) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (x_index < 0 || y_index < 0 || x_index > 3 || y_index > 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// x_hitbox and y_hitbox is relative to top-left pixel of each button
|
||||
int x_index = x_relative / JB_BUTTON_HITBOX;
|
||||
int x_hitbox = x_relative % JB_BUTTON_HITBOX;
|
||||
int y_index = y_relative / JB_BUTTON_HITBOX;
|
||||
int y_hitbox = y_relative % JB_BUTTON_HITBOX;
|
||||
|
||||
// check if the gap was touched
|
||||
if (x_hitbox > JB_BUTTON_SIZE || y_hitbox > JB_BUTTON_SIZE) {
|
||||
continue;
|
||||
if (TOUCH_ALGORITHM == AcAccurate) {
|
||||
// in ac-accurate mode, touching the gap is ignored
|
||||
if (x_hitbox > JB_BUTTON_SIZE || y_hitbox > JB_BUTTON_SIZE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
} else if (TOUCH_ALGORITHM == Improved) {
|
||||
// in improved mode, touching the gap triggers the closest button
|
||||
if (x_index <= 2 && (JB_BUTTON_SIZE + JB_BUTTON_GAP / 2) < x_hitbox) {
|
||||
x_index++;
|
||||
}
|
||||
if (y_index <= 2 && (JB_BUTTON_SIZE + JB_BUTTON_GAP / 2) < y_hitbox) {
|
||||
y_index++;
|
||||
}
|
||||
}
|
||||
|
||||
// set the corresponding state
|
||||
@@ -167,7 +225,7 @@ namespace games::jb {
|
||||
void JBGame::pre_attach() {
|
||||
if (!cfg::CONFIGURATOR_STANDALONE) {
|
||||
const auto current_path = std::filesystem::current_path();
|
||||
log_misc("jubeat", "current working directory: {}", current_path.string());
|
||||
log_misc("jubeat", "current working directory: {}", current_path);
|
||||
if (current_path.parent_path() == current_path.root_path()) {
|
||||
log_warning(
|
||||
"jubeat",
|
||||
@@ -178,7 +236,7 @@ namespace games::jb {
|
||||
" c:\\jubeat\\contents\\spice.exe <- OK\n\n"
|
||||
"To fix this, create a new directory and move ALL game files there.\n\n"
|
||||
"Your current working directory: {}\n",
|
||||
current_path.string());
|
||||
current_path);
|
||||
|
||||
log_fatal(
|
||||
"jubeat",
|
||||
@@ -193,6 +251,21 @@ namespace games::jb {
|
||||
// enable touch
|
||||
TOUCH_ENABLE = true;
|
||||
|
||||
switch (TOUCH_ALGORITHM) {
|
||||
case Legacy:
|
||||
log_info("jubeat", "using 'legacy' touch targets");
|
||||
break;
|
||||
case Improved:
|
||||
log_info("jubeat", "using 'improved' touch targets");
|
||||
break;
|
||||
case AcAccurate:
|
||||
log_info("jubeat", "using 'ac accurate' touch targets");
|
||||
break;
|
||||
default:
|
||||
log_fatal("jubeat", "unknown touch algo detected in touch_update, this is a bug");
|
||||
break;
|
||||
}
|
||||
|
||||
// enable debug logging of gftools
|
||||
HMODULE gftools = libutils::try_module("gftools.dll");
|
||||
detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc(
|
||||
|
||||
@@ -4,8 +4,14 @@
|
||||
|
||||
namespace games::jb {
|
||||
|
||||
enum JubeatTouchAlgorithm {
|
||||
Legacy,
|
||||
Improved,
|
||||
AcAccurate
|
||||
};
|
||||
|
||||
// touch stuff
|
||||
extern bool TOUCH_LEGACY_BOX;
|
||||
extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
|
||||
extern bool TOUCH_STATE[16];
|
||||
void touch_update();
|
||||
|
||||
|
||||
@@ -36,11 +36,11 @@ std::vector<Analog> &games::mga::get_analogs() {
|
||||
if (analogs.empty()) {
|
||||
analogs = GameAPI::Analogs::getAnalogs("Metal Gear");
|
||||
|
||||
GameAPI::Analogs::sortAnalogs(
|
||||
&analogs,
|
||||
"Joy X",
|
||||
"Joy Y"
|
||||
);
|
||||
using namespace GameAPI::Analogs;
|
||||
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
|
||||
{ "Joy X", AnalogType::LinearCentered },
|
||||
{ "Joy Y", AnalogType::LinearCentered }
|
||||
});
|
||||
}
|
||||
|
||||
return analogs;
|
||||
|
||||
@@ -165,37 +165,38 @@ std::vector<Analog> &games::nost::get_analogs() {
|
||||
if (analogs.empty()) {
|
||||
analogs = GameAPI::Analogs::getAnalogs("Nostalgia");
|
||||
|
||||
GameAPI::Analogs::sortAnalogs(
|
||||
&analogs,
|
||||
"Key 1",
|
||||
"Key 2",
|
||||
"Key 3",
|
||||
"Key 4",
|
||||
"Key 5",
|
||||
"Key 6",
|
||||
"Key 7",
|
||||
"Key 8",
|
||||
"Key 9",
|
||||
"Key 10",
|
||||
"Key 11",
|
||||
"Key 12",
|
||||
"Key 13",
|
||||
"Key 14",
|
||||
"Key 15",
|
||||
"Key 16",
|
||||
"Key 17",
|
||||
"Key 18",
|
||||
"Key 19",
|
||||
"Key 20",
|
||||
"Key 21",
|
||||
"Key 22",
|
||||
"Key 23",
|
||||
"Key 24",
|
||||
"Key 25",
|
||||
"Key 26",
|
||||
"Key 27",
|
||||
"Key 28"
|
||||
);
|
||||
using namespace GameAPI::Analogs;
|
||||
|
||||
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
|
||||
{ "Key 1", AnalogType::LinearPositive },
|
||||
{ "Key 2", AnalogType::LinearPositive },
|
||||
{ "Key 3", AnalogType::LinearPositive },
|
||||
{ "Key 4", AnalogType::LinearPositive },
|
||||
{ "Key 5", AnalogType::LinearPositive },
|
||||
{ "Key 6", AnalogType::LinearPositive },
|
||||
{ "Key 7", AnalogType::LinearPositive },
|
||||
{ "Key 8", AnalogType::LinearPositive },
|
||||
{ "Key 9", AnalogType::LinearPositive },
|
||||
{ "Key 10", AnalogType::LinearPositive },
|
||||
{ "Key 11", AnalogType::LinearPositive },
|
||||
{ "Key 12", AnalogType::LinearPositive },
|
||||
{ "Key 13", AnalogType::LinearPositive },
|
||||
{ "Key 14", AnalogType::LinearPositive },
|
||||
{ "Key 15", AnalogType::LinearPositive },
|
||||
{ "Key 16", AnalogType::LinearPositive },
|
||||
{ "Key 17", AnalogType::LinearPositive },
|
||||
{ "Key 18", AnalogType::LinearPositive },
|
||||
{ "Key 19", AnalogType::LinearPositive },
|
||||
{ "Key 20", AnalogType::LinearPositive },
|
||||
{ "Key 21", AnalogType::LinearPositive },
|
||||
{ "Key 22", AnalogType::LinearPositive },
|
||||
{ "Key 23", AnalogType::LinearPositive },
|
||||
{ "Key 24", AnalogType::LinearPositive },
|
||||
{ "Key 25", AnalogType::LinearPositive },
|
||||
{ "Key 26", AnalogType::LinearPositive },
|
||||
{ "Key 27", AnalogType::LinearPositive },
|
||||
{ "Key 28", AnalogType::LinearPositive }
|
||||
});
|
||||
}
|
||||
return analogs;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "misc/eamuse.h"
|
||||
#include "touch/touch.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
|
||||
namespace games::nost::poke {
|
||||
|
||||
@@ -63,6 +64,7 @@ namespace games::nost::poke {
|
||||
// create new thread
|
||||
THREAD_RUNNING = true;
|
||||
THREAD = new std::thread([] {
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
const DWORD touch_id = (DWORD)(0xFFFFFFFE);
|
||||
|
||||
const int swipe_anim_total_frames = 6;
|
||||
@@ -178,7 +180,7 @@ namespace games::nost::poke {
|
||||
}
|
||||
|
||||
// slow down
|
||||
Sleep(30);
|
||||
timer.sleep(30);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "touchpanel.h"
|
||||
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "touch/touch.h"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
@@ -44,6 +45,9 @@ bool games::onpara::TouchPanelHandle::open(LPCWSTR lpFileName) {
|
||||
}
|
||||
|
||||
int games::onpara::TouchPanelHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
|
||||
|
||||
static thread_local timeutils::PreciseSleepTimer timer;
|
||||
|
||||
DWORD i;
|
||||
auto buffer = reinterpret_cast<uint8_t *>(lpBuffer);
|
||||
|
||||
@@ -67,7 +71,7 @@ int games::onpara::TouchPanelHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesT
|
||||
enqueue_packet(report);
|
||||
|
||||
// prevent cpu bullying
|
||||
std::this_thread::sleep_for(1ms);
|
||||
timer.sleep(1);
|
||||
}
|
||||
|
||||
// copy from output queue
|
||||
@@ -133,4 +137,4 @@ bool games::onpara::TouchPanelHandle::close() {
|
||||
log_info("touchpanel", "Closed COM1 (Touch Panel)");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "util/detour.h"
|
||||
#include "util/libutils.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/sigscan.h"
|
||||
|
||||
#define OTOCA_DEBUG_VERBOSE 0
|
||||
#if OTOCA_DEBUG_VERBOSE
|
||||
@@ -116,6 +117,73 @@ namespace games::otoca {
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void patch_holo_print_hang() {
|
||||
|
||||
// arkkep.dll's print method sets the printer busy (0x66 / Printing_Busy),
|
||||
// then bails out with -1 WITHOUT printing when its holo parameter is
|
||||
// nonzero (the card path passes 0 and works). The completion callback that
|
||||
// clears busy never fires, so the page's poll loop spins forever -> hang.
|
||||
// The image band is already built, so flip the je that skips the bail-out
|
||||
// into a jmp (74 -> EB) to route holo through the print path like card.
|
||||
//
|
||||
// je issue_print ; <- patched to jmp
|
||||
// or eax, -1 ; holo: return without printing
|
||||
// ret 10h
|
||||
auto arkkep = libutils::try_module("arkkep.dll");
|
||||
if (arkkep == nullptr) {
|
||||
log_warning("otoca", "arkkep.dll not loaded; skipping holo print hang fix");
|
||||
return;
|
||||
}
|
||||
|
||||
auto result = replace_pattern(
|
||||
arkkep,
|
||||
"C7465866000000895DC0C74710080700007416",
|
||||
"C7465866000000895DC0C7471008070000EB16",
|
||||
0, 0);
|
||||
if (result) {
|
||||
log_info("otoca", "patched hologram print hang in arkkep.dll");
|
||||
} else {
|
||||
log_warning("otoca", "could not patch hologram print hang (incompatible arkkep.dll?)");
|
||||
}
|
||||
}
|
||||
|
||||
static void patch_holo_print_search() {
|
||||
|
||||
// before printing, arkkep walks the page's printer list looking for one
|
||||
// whose capability flag (byte at entry+120h) matches the job type
|
||||
// (0 = card, 1 = holo). Our emulated printer is only card-capable, so a
|
||||
// holo job finds no match and aborts without printing ("not found holo
|
||||
// printer"). The emulated printer serves both media, so force the compare
|
||||
// to always match by turning `cmp cl,[edi+120h]` into `cmp cl,cl` + NOPs,
|
||||
// which always sets ZF. Two such compares exist, so patch both.
|
||||
//
|
||||
// cmp cl, [edi+120h] ; <- patched to `cmp cl,cl` (3A 8F.. -> 3A C9..)
|
||||
// je printer_found
|
||||
auto arkkep = libutils::try_module("arkkep.dll");
|
||||
if (arkkep == nullptr) {
|
||||
log_warning("otoca", "arkkep.dll not loaded; skipping holo print search fix");
|
||||
return;
|
||||
}
|
||||
|
||||
int patched = 0;
|
||||
while (patched < 2) {
|
||||
auto result = replace_pattern(
|
||||
arkkep,
|
||||
"3A8F20010000",
|
||||
"3AC990909090",
|
||||
0, 0);
|
||||
if (!result) {
|
||||
break;
|
||||
}
|
||||
patched++;
|
||||
}
|
||||
if (patched > 0) {
|
||||
log_info("otoca", "patched hologram printer search in arkkep.dll ({} site(s))", patched);
|
||||
} else {
|
||||
log_warning("otoca", "could not patch hologram printer search (incompatible arkkep.dll?)");
|
||||
}
|
||||
}
|
||||
|
||||
void OtocaGame::attach() {
|
||||
Game::attach();
|
||||
|
||||
@@ -129,6 +197,12 @@ namespace games::otoca {
|
||||
p4io_hook();
|
||||
games::shared::printer_attach();
|
||||
|
||||
// fix hologram print hard hang in arkkep.dll
|
||||
patch_holo_print_hang();
|
||||
|
||||
// make hologram jobs find the emulated printer (else they abort unprinted)
|
||||
patch_holo_print_search();
|
||||
|
||||
if (BYPASS_CAMERA) {
|
||||
libutils::try_library("libcamera.dll");
|
||||
const auto libcamera = "libcamera.dll";
|
||||
|
||||
@@ -51,11 +51,12 @@ std::vector<Analog> &games::pc::get_analogs() {
|
||||
if (analogs.empty()) {
|
||||
analogs = GameAPI::Analogs::getAnalogs("Polaris Chord");
|
||||
|
||||
GameAPI::Analogs::sortAnalogs(
|
||||
&analogs,
|
||||
"Fader-L",
|
||||
"Fader-R"
|
||||
);
|
||||
using GameAPI::Analogs::AnalogType;
|
||||
|
||||
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
|
||||
{ "Fader-L", AnalogType::LinearCentered },
|
||||
{ "Fader-R", AnalogType::LinearCentered }
|
||||
});
|
||||
}
|
||||
|
||||
return analogs;
|
||||
|
||||
@@ -261,7 +261,7 @@ namespace games::popn {
|
||||
} else if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME) {
|
||||
const auto source = reinterpret_cast<DISPLAYCONFIG_SOURCE_DEVICE_NAME*>(requestPacket);
|
||||
// value must match WrappedIDirect3D9::GetAdapterIdentifier
|
||||
wcscpy(source->viewGdiDeviceName, L"\\\\.\\DISPLAY_SPICE_FAKE");
|
||||
wcscpy(source->viewGdiDeviceName, L"\\\\.\\DISPLAY_SPICE_FAKE_1");
|
||||
} else {
|
||||
log_fatal(
|
||||
"popn",
|
||||
|
||||
+27
-13
@@ -4,29 +4,34 @@
|
||||
#include "hooks/devicehook.h"
|
||||
#include "hooks/audio/backends/dsound/dsound_backend.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "util/logging.h"
|
||||
#include "cfg/configurator.h"
|
||||
|
||||
#include "touch.h"
|
||||
|
||||
namespace games::rb {
|
||||
uint16_t TOUCH_POLL_RATE = 0;
|
||||
}
|
||||
|
||||
static decltype(SleepEx) *SleepEx_orig;
|
||||
|
||||
static DWORD WINAPI SleepEx_hook(DWORD dwMilliseconds, BOOL bAltertable) {
|
||||
static DWORD WINAPI SleepEx_hook(DWORD dwMilliseconds, BOOL bAlertable) {
|
||||
|
||||
/*
|
||||
* Increase touch poll from ~110 FPS to ~500 FPS
|
||||
*/
|
||||
// increase touch poll rate
|
||||
if (dwMilliseconds == 8) {
|
||||
static bool initialized = false;
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
dwMilliseconds = 1000 / games::rb::TOUCH_POLL_RATE;
|
||||
}
|
||||
|
||||
// if we only sleep for 1ms we also don't need the high priority RB sets
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
|
||||
}
|
||||
dwMilliseconds = 1;
|
||||
// most calls from rb are actually non-alertable
|
||||
if (!bAlertable) {
|
||||
static thread_local timeutils::PreciseSleepTimer timer;
|
||||
timer.sleep(dwMilliseconds);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// call original
|
||||
return SleepEx_orig(dwMilliseconds, bAltertable);
|
||||
return SleepEx_orig(dwMilliseconds, bAlertable);
|
||||
}
|
||||
|
||||
games::rb::RBGame::RBGame() : Game("Reflec Beat") {
|
||||
@@ -35,6 +40,10 @@ games::rb::RBGame::RBGame() : Game("Reflec Beat") {
|
||||
void games::rb::RBGame::attach() {
|
||||
Game::attach();
|
||||
|
||||
if (1000 < games::rb::TOUCH_POLL_RATE) {
|
||||
log_fatal("rb", "-rbtouchsize is set too high; cannot exceed 1000");
|
||||
}
|
||||
|
||||
// init stuff
|
||||
devicehook_init();
|
||||
|
||||
@@ -47,7 +56,12 @@ void games::rb::RBGame::attach() {
|
||||
}
|
||||
|
||||
// hooks
|
||||
SleepEx_orig = detour::iat_try("SleepEx", SleepEx_hook, avs::game::DLL_INSTANCE);
|
||||
if (0 < TOUCH_POLL_RATE) {
|
||||
log_info("rb", "force increasing touch poll rate by hooking SleepEx ({}Hz)", TOUCH_POLL_RATE);
|
||||
SleepEx_orig = detour::iat_try("SleepEx", SleepEx_hook, avs::game::DLL_INSTANCE);
|
||||
} else {
|
||||
log_info("rb", "not changing touch poll rate (120Hz by default)");
|
||||
}
|
||||
}
|
||||
|
||||
void games::rb::RBGame::detach() {
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
namespace games::rb {
|
||||
|
||||
extern uint16_t TOUCH_SCALING;
|
||||
extern uint8_t TOUCH_SIZE;
|
||||
extern uint16_t TOUCH_POLL_RATE;
|
||||
|
||||
class RBGame : public games::Game {
|
||||
public:
|
||||
|
||||
@@ -13,6 +13,7 @@ static std::string WINDOW_TITLE = "REFLEC BEAT";
|
||||
|
||||
namespace games::rb {
|
||||
uint16_t TOUCH_SCALING = 1000;
|
||||
uint8_t TOUCH_SIZE = 1;
|
||||
}
|
||||
|
||||
games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) {
|
||||
@@ -160,16 +161,20 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
|
||||
const auto point_x = (int) ((x - window_width / 2.0) * 48.0 / 54.0 + window_width / 2.0);
|
||||
const auto point_y = (int) (y - window_height / 76);
|
||||
|
||||
// insert 9 times with offset to double the precision
|
||||
// center
|
||||
grid_insert(data, point_x, point_y);
|
||||
grid_insert(data, point_x - offset_x, point_y);
|
||||
grid_insert(data, point_x - offset_x, point_y - offset_y);
|
||||
grid_insert(data, point_x - offset_x, point_y + offset_y);
|
||||
grid_insert(data, point_x + offset_x, point_y);
|
||||
grid_insert(data, point_x + offset_x, point_y + offset_y);
|
||||
grid_insert(data, point_x + offset_x, point_y - offset_y);
|
||||
grid_insert(data, point_x, point_y - offset_y);
|
||||
grid_insert(data, point_x, point_y + offset_y);
|
||||
|
||||
// surrounding points
|
||||
if (games::rb::TOUCH_SIZE == 3) {
|
||||
grid_insert(data, point_x - offset_x, point_y); // west
|
||||
grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest
|
||||
grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest
|
||||
grid_insert(data, point_x + offset_x, point_y); // east
|
||||
grid_insert(data, point_x + offset_x, point_y + offset_y); // southeast
|
||||
grid_insert(data, point_x + offset_x, point_y - offset_y); // northeast
|
||||
grid_insert(data, point_x, point_y - offset_y); // north
|
||||
grid_insert(data, point_x, point_y + offset_y); // south
|
||||
}
|
||||
}
|
||||
|
||||
// copy data to buffer
|
||||
|
||||
@@ -35,12 +35,13 @@ std::vector<Analog> &games::rf3d::get_analogs() {
|
||||
if (analogs.empty()) {
|
||||
analogs = GameAPI::Analogs::getAnalogs("Road Fighters 3D");
|
||||
|
||||
GameAPI::Analogs::sortAnalogs(
|
||||
&analogs,
|
||||
"Wheel",
|
||||
"Accelerate",
|
||||
"Brake"
|
||||
);
|
||||
using GameAPI::Analogs::AnalogType;
|
||||
|
||||
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
|
||||
{ "Wheel", AnalogType::LinearCentered },
|
||||
{ "Accelerate", AnalogType::LinearPositive },
|
||||
{ "Brake", AnalogType::LinearPositive }
|
||||
});
|
||||
}
|
||||
|
||||
return analogs;
|
||||
|
||||
@@ -31,13 +31,14 @@ std::vector<Analog> &games::sc::get_analogs() {
|
||||
if (analogs.empty()) {
|
||||
analogs = GameAPI::Analogs::getAnalogs("Steel Chronicle");
|
||||
|
||||
GameAPI::Analogs::sortAnalogs(
|
||||
&analogs,
|
||||
"Left Stick X",
|
||||
"Left Stick Y",
|
||||
"Right Stick X",
|
||||
"Right Stick Y"
|
||||
);
|
||||
using GameAPI::Analogs::AnalogType;
|
||||
|
||||
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
|
||||
{ "Left Stick X", AnalogType::LinearCentered },
|
||||
{ "Left Stick Y", AnalogType::LinearCentered },
|
||||
{ "Right Stick X", AnalogType::LinearCentered },
|
||||
{ "Right Stick Y", AnalogType::LinearCentered }
|
||||
});
|
||||
}
|
||||
|
||||
return analogs;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "hooks/sleephook.h"
|
||||
#include "hooks/libraryhook.h"
|
||||
#include "launcher/launcher.h"
|
||||
#include "overlay/notifications.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/fileutils.h"
|
||||
#include "util/libutils.h"
|
||||
@@ -397,8 +398,14 @@ namespace games::shared {
|
||||
// logging
|
||||
if (success) {
|
||||
log_info("printer", "printer emulation has written an image to {}", image_path);
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Success,
|
||||
fmt::format("Printer: saved {}", fileutils::basename(image_path)));
|
||||
} else {
|
||||
log_warning("printer", "printer emulation failed to write image to {}", image_path);
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Error,
|
||||
fmt::format("Printer: failed to write {}", fileutils::basename(image_path)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +31,12 @@ std::vector<Analog> &games::silentscope::get_analogs() {
|
||||
if (analogs.empty()) {
|
||||
analogs = GameAPI::Analogs::getAnalogs("Silent Scope: Bone Eater");
|
||||
|
||||
GameAPI::Analogs::sortAnalogs(
|
||||
&analogs,
|
||||
"Gun X",
|
||||
"Gun Y"
|
||||
);
|
||||
using GameAPI::Analogs::AnalogType;
|
||||
|
||||
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
|
||||
{ "Gun X", AnalogType::LinearCentered },
|
||||
{ "Gun Y", AnalogType::LinearCentered }
|
||||
});
|
||||
}
|
||||
|
||||
return analogs;
|
||||
|
||||
@@ -50,13 +50,14 @@ std::vector<Analog> &games::we::get_analogs() {
|
||||
if (analogs.empty()) {
|
||||
analogs = GameAPI::Analogs::getAnalogs("Winning Eleven");
|
||||
|
||||
GameAPI::Analogs::sortAnalogs(
|
||||
&analogs,
|
||||
"Pad Stick Left X",
|
||||
"Pad Stick Left Y",
|
||||
"Pad Stick Right X",
|
||||
"Pad Stick Right Y"
|
||||
);
|
||||
using GameAPI::Analogs::AnalogType;
|
||||
|
||||
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
|
||||
{ "Pad Stick Left X", AnalogType::LinearCentered },
|
||||
{ "Pad Stick Left Y", AnalogType::LinearCentered },
|
||||
{ "Pad Stick Right X", AnalogType::LinearCentered },
|
||||
{ "Pad Stick Right Y", AnalogType::LinearCentered }
|
||||
});
|
||||
}
|
||||
|
||||
return analogs;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#include "asio_driver_scan.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "util/utils.h"
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
static constexpr char ASIO_REG_PATH[] = "software\\asio";
|
||||
static constexpr char ASIO_REG_DESC[] = "description";
|
||||
|
||||
// enumerate a single registry view, appending to entries while merging
|
||||
// duplicates discovered in another view. Drivers are matched by name (not
|
||||
// CLSID): the game's ASIO loader selects drivers by name, and some vendors
|
||||
// register the same CLSID under different 32-bit/64-bit names (e.g. "XONAR
|
||||
// SOUND CARD" vs "XONAR SOUND CARD(64)"), which are distinct user choices.
|
||||
static void scan_view(
|
||||
REGSAM wow64_flag,
|
||||
bool is_64bit,
|
||||
std::vector<AsioDriverScanEntry> &entries) {
|
||||
|
||||
HKEY hkEnum = nullptr;
|
||||
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, ASIO_REG_PATH, 0,
|
||||
KEY_READ | wow64_flag, &hkEnum) != ERROR_SUCCESS) {
|
||||
return;
|
||||
}
|
||||
|
||||
char key_name[256];
|
||||
for (DWORD index = 0;
|
||||
RegEnumKeyA(hkEnum, index, key_name, sizeof(key_name)) == ERROR_SUCCESS;
|
||||
index++) {
|
||||
|
||||
// read description (display name), fall back to the key name
|
||||
char desc[256] = { 0 };
|
||||
DWORD size = sizeof(desc);
|
||||
std::string name = key_name;
|
||||
if (RegGetValueA(hkEnum,
|
||||
key_name,
|
||||
ASIO_REG_DESC,
|
||||
RRF_RT_REG_SZ | wow64_flag,
|
||||
nullptr,
|
||||
desc,
|
||||
&size) == ERROR_SUCCESS && desc[0]) {
|
||||
name = desc;
|
||||
}
|
||||
|
||||
// merge with an existing entry from the other view (match by name)
|
||||
const std::string name_lower = strtolower(name);
|
||||
auto it = std::find_if(entries.begin(), entries.end(), [&](const auto &e) {
|
||||
return strtolower(e.name) == name_lower;
|
||||
});
|
||||
if (it == entries.end()) {
|
||||
entries.push_back({ name });
|
||||
it = entries.end() - 1;
|
||||
}
|
||||
it->found_32bit |= !is_64bit;
|
||||
it->found_64bit |= is_64bit;
|
||||
}
|
||||
|
||||
RegCloseKey(hkEnum);
|
||||
}
|
||||
|
||||
std::vector<AsioDriverScanEntry> scan_asio_drivers() {
|
||||
std::vector<AsioDriverScanEntry> entries;
|
||||
|
||||
// 64-bit view first so it wins ordering when present in both
|
||||
scan_view(KEY_WOW64_64KEY, true, entries);
|
||||
scan_view(KEY_WOW64_32KEY, false, entries);
|
||||
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
struct AsioDriverScanEntry {
|
||||
std::string name;
|
||||
bool found_32bit = false;
|
||||
bool found_64bit = false;
|
||||
};
|
||||
|
||||
std::vector<AsioDriverScanEntry> scan_asio_drivers();
|
||||
}
|
||||
@@ -0,0 +1,813 @@
|
||||
#include "asio_proxy.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include "external/asio/asiolist.h"
|
||||
#include "hooks/audio/audio.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// readable name for an ASIO sample type (e.g. "ASIOSTInt32LSB"), falling back to the
|
||||
// numeric value for unknown types
|
||||
const char *asio_sample_type_name(AsioSampleType type) {
|
||||
switch (type) {
|
||||
case ASIOSTInt16MSB: return "ASIOSTInt16MSB";
|
||||
case ASIOSTInt24MSB: return "ASIOSTInt24MSB";
|
||||
case ASIOSTInt32MSB: return "ASIOSTInt32MSB";
|
||||
case ASIOSTFloat32MSB: return "ASIOSTFloat32MSB";
|
||||
case ASIOSTFloat64MSB: return "ASIOSTFloat64MSB";
|
||||
case ASIOSTInt32MSB16: return "ASIOSTInt32MSB16";
|
||||
case ASIOSTInt32MSB18: return "ASIOSTInt32MSB18";
|
||||
case ASIOSTInt32MSB20: return "ASIOSTInt32MSB20";
|
||||
case ASIOSTInt32MSB24: return "ASIOSTInt32MSB24";
|
||||
case ASIOSTInt16LSB: return "ASIOSTInt16LSB";
|
||||
case ASIOSTInt24LSB: return "ASIOSTInt24LSB";
|
||||
case ASIOSTInt32LSB: return "ASIOSTInt32LSB";
|
||||
case ASIOSTFloat32LSB: return "ASIOSTFloat32LSB";
|
||||
case ASIOSTFloat64LSB: return "ASIOSTFloat64LSB";
|
||||
case ASIOSTInt32LSB16: return "ASIOSTInt32LSB16";
|
||||
case ASIOSTInt32LSB18: return "ASIOSTInt32LSB18";
|
||||
case ASIOSTInt32LSB20: return "ASIOSTInt32LSB20";
|
||||
case ASIOSTInt32LSB24: return "ASIOSTInt32LSB24";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// duration in milliseconds of a buffer of the given frame count at a sample rate, or a
|
||||
// negative sentinel when the frame count or sample rate is unusable
|
||||
double frames_to_ms(long frames, AsioSampleRate sample_rate) {
|
||||
if (frames < 0 || sample_rate <= 0.0) {
|
||||
return -1.0;
|
||||
}
|
||||
return (frames * 1000.0) / sample_rate;
|
||||
}
|
||||
|
||||
// scales one planar ASIO output buffer (frames samples of the given type) by gain in
|
||||
// place, clamping integer formats so a boost saturates instead of wrapping. unsupported
|
||||
// formats are left untouched. runs on the driver's realtime thread, so no allocation,
|
||||
// locking or logging here
|
||||
void apply_gain_planar(void *buffer, long frames, AsioSampleType type, float gain) {
|
||||
if (buffer == nullptr || frames <= 0) {
|
||||
return;
|
||||
}
|
||||
switch (type) {
|
||||
case ASIOSTFloat32LSB: {
|
||||
auto p = static_cast<float *>(buffer);
|
||||
for (long i = 0; i < frames; i++) {
|
||||
p[i] = std::clamp(p[i] * gain, -1.0f, 1.0f);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ASIOSTFloat64LSB: {
|
||||
auto p = static_cast<double *>(buffer);
|
||||
for (long i = 0; i < frames; i++) {
|
||||
p[i] = std::clamp(p[i] * static_cast<double>(gain), -1.0, 1.0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ASIOSTInt16LSB: {
|
||||
auto p = static_cast<int16_t *>(buffer);
|
||||
for (long i = 0; i < frames; i++) {
|
||||
p[i] = static_cast<int16_t>(
|
||||
std::clamp(std::lround(p[i] * gain), -32768L, 32767L));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ASIOSTInt24LSB: {
|
||||
// packed 24-bit little-endian, 3 bytes per sample
|
||||
auto bytes = static_cast<uint8_t *>(buffer);
|
||||
for (long i = 0; i < frames; i++) {
|
||||
uint8_t *s = bytes + i * 3;
|
||||
int32_t v = s[0] | (s[1] << 8) | (s[2] << 16);
|
||||
if (v & 0x800000) {
|
||||
v |= ~0xFFFFFF; // sign extend
|
||||
}
|
||||
int64_t scaled = std::clamp<int64_t>(
|
||||
std::llround(static_cast<double>(v) * gain), -8388608, 8388607);
|
||||
s[0] = scaled & 0xFF;
|
||||
s[1] = (scaled >> 8) & 0xFF;
|
||||
s[2] = (scaled >> 16) & 0xFF;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ASIOSTInt32LSB: {
|
||||
auto p = static_cast<int32_t *>(buffer);
|
||||
for (long i = 0; i < frames; i++) {
|
||||
p[i] = static_cast<int32_t>(std::clamp<int64_t>(
|
||||
std::llround(static_cast<double>(p[i]) * gain), INT32_MIN, INT32_MAX));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// unsupported format (MSB, aligned 32-bit, DSD): leave untouched
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// live wrappers by CLSID. ASIO drivers are single-instance, so a host that creates a
|
||||
// new instance without releasing the old one has leaked it; we track this to tear the
|
||||
// stale one down
|
||||
std::mutex g_wrappers_mutex;
|
||||
std::vector<std::pair<CLSID, WrappedAsio *>> g_wrappers;
|
||||
|
||||
// register a wrapper as the live instance for its CLSID, returning any stale wrapper it
|
||||
// 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);
|
||||
for (auto &entry : g_wrappers) {
|
||||
if (IsEqualCLSID(entry.first, clsid)) {
|
||||
WrappedAsio *stale = entry.second;
|
||||
entry.second = wrapper;
|
||||
return stale;
|
||||
}
|
||||
}
|
||||
|
||||
g_wrappers.emplace_back(clsid, wrapper);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void unregister_wrapper(WrappedAsio *wrapper) {
|
||||
std::lock_guard lock(g_wrappers_mutex);
|
||||
for (auto it = g_wrappers.begin(); it != g_wrappers.end(); ++it) {
|
||||
if (it->second == wrapper) {
|
||||
g_wrappers.erase(it);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ASIO drivers registered on this system (CLSID + registry name), scanned once
|
||||
const std::vector<std::pair<CLSID, std::string>> ®istered_asio_drivers() {
|
||||
static const std::vector<std::pair<CLSID, std::string>> drivers = [] {
|
||||
std::vector<std::pair<CLSID, std::string>> result;
|
||||
AsioDriverList driver_list;
|
||||
for (const auto &driver : driver_list.driver_list) {
|
||||
result.emplace_back(driver.clsid, driver.name);
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"registered ASIO driver: name='{}', clsid={}",
|
||||
driver.name,
|
||||
guid2s(driver.clsid));
|
||||
}
|
||||
|
||||
log_info("audio::wrappedasio", "discovered {} registered ASIO driver(s)", result.size());
|
||||
return result;
|
||||
}();
|
||||
|
||||
return drivers;
|
||||
}
|
||||
|
||||
std::string registered_asio_name(REFCLSID clsid) {
|
||||
for (const auto &driver : registered_asio_drivers()) {
|
||||
if (IsEqualCLSID(driver.first, clsid)) {
|
||||
return driver.second;
|
||||
}
|
||||
}
|
||||
|
||||
return guid2s(clsid);
|
||||
}
|
||||
}
|
||||
|
||||
namespace hooks::audio::asio {
|
||||
|
||||
bool is_asio_creation(REFCLSID rclsid, REFIID riid) {
|
||||
|
||||
// ASIO hosts request the driver using its own CLSID as the interface id
|
||||
if (!IsEqualGUID(rclsid, riid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto &driver : registered_asio_drivers()) {
|
||||
if (IsEqualCLSID(driver.first, rclsid)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma region IUnknown
|
||||
bool WrappedAsio::FORCE_TWO_CHANNELS = false;
|
||||
std::atomic<WrappedAsio *> WrappedAsio::volume_active_instance {nullptr};
|
||||
|
||||
WrappedAsio::~WrappedAsio() {
|
||||
unregister_wrapper(this);
|
||||
|
||||
this->detach_volume();
|
||||
|
||||
// our refcount is decoupled from the host's (see AddRef/Release), so pReal's count is
|
||||
// exactly one here and this releases/unloads it deterministically
|
||||
this->pReal->Release();
|
||||
|
||||
log_info("audio::wrappedasio", "destroying wrapped ASIO driver, clsid={}", guid2s(this->clsid));
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedAsio::QueryInterface(REFIID riid, void **ppv) {
|
||||
if (ppv == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
// ASIO hosts query for the driver using its own CLSID as the IID
|
||||
if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, this->clsid)) {
|
||||
this->AddRef();
|
||||
*ppv = static_cast<IAsio *>(this);
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// the host is asking for some other interface; forward to the real driver. a failure
|
||||
// here is a common reason a host discards a driver and retries
|
||||
const HRESULT ret = this->pReal->QueryInterface(riid, ppv);
|
||||
if (SUCCEEDED(ret)) {
|
||||
log_info("audio::wrappedasio", "QueryInterface({}) -> forwarded to real driver", guid2s(riid));
|
||||
} else {
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"QueryInterface({}) -> not supported by driver, hr={:#x}",
|
||||
guid2s(riid),
|
||||
static_cast<unsigned long>(ret));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE WrappedAsio::AddRef() {
|
||||
// decoupled from the real driver: we count host references on the wrapper and hold a
|
||||
// single reference on pReal for our lifetime. this neutralizes a host bug (iidx32+)
|
||||
// that takes a duplicate AddRef with no matching Release, which would leak the driver
|
||||
return ++this->ref_count;
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE WrappedAsio::Release() {
|
||||
const ULONG refs = --this->ref_count;
|
||||
if (refs == 0) {
|
||||
delete this;
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IAsio
|
||||
AsioBool __thiscall WrappedAsio::init(void *sys_handle) {
|
||||
const AsioBool result = this->pReal->init(sys_handle);
|
||||
if (result == AsioTrue) {
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"init succeeded for '{}' (driver version {})",
|
||||
this->driver_name,
|
||||
this->pReal->get_driver_version());
|
||||
} else {
|
||||
char message[128] = {};
|
||||
this->pReal->get_error_message(message);
|
||||
log_warning("audio::wrappedasio", "init failed: {}", message);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void __thiscall WrappedAsio::get_driver_name(char *name) {
|
||||
this->pReal->get_driver_name(name);
|
||||
}
|
||||
|
||||
long __thiscall WrappedAsio::get_driver_version() {
|
||||
return this->pReal->get_driver_version();
|
||||
}
|
||||
|
||||
void __thiscall WrappedAsio::get_error_message(char *string) {
|
||||
this->pReal->get_error_message(string);
|
||||
}
|
||||
|
||||
AsioError __thiscall WrappedAsio::start() {
|
||||
const AsioError result = this->pReal->start();
|
||||
if (result == ASE_OK) {
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"start succeeded, ASIO stream is now running on '{}'",
|
||||
this->driver_name);
|
||||
} else {
|
||||
log_warning("audio::wrappedasio", "start failed, err={}", static_cast<long>(result));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
AsioError __thiscall WrappedAsio::stop() {
|
||||
const AsioError result = this->pReal->stop();
|
||||
if (result == ASE_OK) {
|
||||
log_info("audio::wrappedasio", "stop succeeded, ASIO stream on '{}' halted", this->driver_name);
|
||||
} else {
|
||||
log_warning("audio::wrappedasio", "stop failed, err={}", static_cast<long>(result));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
AsioError __thiscall WrappedAsio::get_channels(long *num_input_channels, long *num_output_channels) {
|
||||
const AsioError result = this->pReal->get_channels(num_input_channels, num_output_channels);
|
||||
if (result != ASE_OK) {
|
||||
log_warning("audio::wrappedasio", "get_channels failed, err={}", static_cast<long>(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
if (FORCE_TWO_CHANNELS
|
||||
&& num_output_channels != nullptr
|
||||
&& *num_output_channels < FORCED_OUTPUT_CHANNELS)
|
||||
{
|
||||
// the device has fewer outputs than the game hardcodes; report the count it
|
||||
// expects so it proceeds to create_buffers, where we forward only the real
|
||||
// front pair and discard the rest
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"reporting output channel count as {} (device has {}) for forced two-channel",
|
||||
FORCED_OUTPUT_CHANNELS,
|
||||
*num_output_channels);
|
||||
*num_output_channels = FORCED_OUTPUT_CHANNELS;
|
||||
}
|
||||
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"get_channels -> in={}, out={}",
|
||||
num_input_channels ? *num_input_channels : -1,
|
||||
num_output_channels ? *num_output_channels : -1);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
AsioError __thiscall WrappedAsio::get_latencies(long *input_latency, long *output_latency) {
|
||||
const AsioError result = this->pReal->get_latencies(input_latency, output_latency);
|
||||
if (result == ASE_OK) {
|
||||
// include millisecond equivalents alongside the frame counts for readability
|
||||
AsioSampleRate sample_rate = 0.0;
|
||||
this->pReal->get_sample_rate(&sample_rate);
|
||||
const long in_frames = input_latency ? *input_latency : -1;
|
||||
const long out_frames = output_latency ? *output_latency : -1;
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"get_latencies -> in={} frames ({:.2f} ms), out={} frames ({:.2f} ms)",
|
||||
in_frames,
|
||||
frames_to_ms(in_frames, sample_rate),
|
||||
out_frames,
|
||||
frames_to_ms(out_frames, sample_rate));
|
||||
} else {
|
||||
log_warning("audio::wrappedasio", "get_latencies failed, err={}", static_cast<long>(result));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
AsioError __thiscall WrappedAsio::get_buffer_size(
|
||||
long *min_size,
|
||||
long *max_size,
|
||||
long *preferred_size,
|
||||
long *granularity)
|
||||
{
|
||||
const AsioError result = this->pReal->get_buffer_size(min_size, max_size, preferred_size, granularity);
|
||||
if (result != ASE_OK) {
|
||||
log_warning("audio::wrappedasio", "get_buffer_size failed, err={}", static_cast<long>(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
// include millisecond equivalents alongside the frame counts for readability
|
||||
AsioSampleRate sample_rate = 0.0;
|
||||
this->pReal->get_sample_rate(&sample_rate);
|
||||
const long min_frames = min_size ? *min_size : -1;
|
||||
const long max_frames = max_size ? *max_size : -1;
|
||||
const long preferred_frames = preferred_size ? *preferred_size : -1;
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"get_buffer_size -> min={} frames ({:.2f} ms), max={} frames ({:.2f} ms), "
|
||||
"preferred={} frames ({:.2f} ms), granularity={}",
|
||||
min_frames,
|
||||
frames_to_ms(min_frames, sample_rate),
|
||||
max_frames,
|
||||
frames_to_ms(max_frames, sample_rate),
|
||||
preferred_frames,
|
||||
frames_to_ms(preferred_frames, sample_rate),
|
||||
granularity ? *granularity : -1);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
AsioError __thiscall WrappedAsio::can_sample_rate(AsioSampleRate sample_rate) {
|
||||
const AsioError result = this->pReal->can_sample_rate(sample_rate);
|
||||
if (result == ASE_OK) {
|
||||
log_misc("audio::wrappedasio", "can_sample_rate({} Hz) -> supported", sample_rate);
|
||||
} else {
|
||||
log_misc(
|
||||
"audio::wrappedasio",
|
||||
"can_sample_rate({} Hz) -> not supported, err={}",
|
||||
sample_rate,
|
||||
static_cast<long>(result));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
AsioError __thiscall WrappedAsio::get_sample_rate(AsioSampleRate *sample_rate) {
|
||||
const AsioError result = this->pReal->get_sample_rate(sample_rate);
|
||||
if (result == ASE_OK) {
|
||||
log_misc("audio::wrappedasio", "get_sample_rate -> {} Hz", sample_rate ? *sample_rate : 0.0);
|
||||
} else {
|
||||
log_warning("audio::wrappedasio", "get_sample_rate failed, err={}", static_cast<long>(result));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
AsioError __thiscall WrappedAsio::set_sample_rate(AsioSampleRate sample_rate) {
|
||||
const AsioError result = this->pReal->set_sample_rate(sample_rate);
|
||||
if (result == ASE_OK) {
|
||||
log_info("audio::wrappedasio", "set_sample_rate({} Hz) succeeded", sample_rate);
|
||||
} else {
|
||||
log_warning(
|
||||
"audio::wrappedasio",
|
||||
"set_sample_rate({} Hz) failed, err={}",
|
||||
sample_rate,
|
||||
static_cast<long>(result));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
AsioError __thiscall WrappedAsio::get_clock_sources(ASIOClockSource *clocks, long *num_sources) {
|
||||
return this->pReal->get_clock_sources(clocks, num_sources);
|
||||
}
|
||||
|
||||
AsioError __thiscall WrappedAsio::set_clock_source(long reference) {
|
||||
return this->pReal->set_clock_source(reference);
|
||||
}
|
||||
|
||||
AsioError __thiscall WrappedAsio::get_sample_position(ASIOSamples *s_pos, ASIOTimeStamp *t_stamp) {
|
||||
return this->pReal->get_sample_position(s_pos, t_stamp);
|
||||
}
|
||||
|
||||
AsioError __thiscall WrappedAsio::get_channel_info(AsioChannelInfo *info) {
|
||||
// forced two-channel: the game probes all output channels it thinks exist, but the
|
||||
// device only has the real front pair. fabricate a plausible entry for the channels
|
||||
// beyond the device without touching the real driver - they are discarded in
|
||||
// create_buffers anyway
|
||||
long real_in = 0, real_out = 0;
|
||||
if (FORCE_TWO_CHANNELS
|
||||
&& info != nullptr
|
||||
&& info->is_input == AsioFalse
|
||||
&& this->pReal->get_channels(&real_in, &real_out) == ASE_OK
|
||||
&& info->channel >= real_out)
|
||||
{
|
||||
const long channel = info->channel;
|
||||
info->is_active = AsioTrue;
|
||||
info->channel_group = 0;
|
||||
info->type = ASIOSTInt32LSB;
|
||||
snprintf(info->name, sizeof(info->name), "Fake ASIO OUT %ld", channel);
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"get_channel_info(channel={}, dir=output) -> fake channel, type={} ({})",
|
||||
channel,
|
||||
asio_sample_type_name(info->type),
|
||||
static_cast<long>(info->type));
|
||||
|
||||
return ASE_OK;
|
||||
}
|
||||
|
||||
const AsioError result = this->pReal->get_channel_info(info);
|
||||
if (result == ASE_OK && info != nullptr) {
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"get_channel_info(channel={}, dir={}) -> active={}, group={}, type={} ({}), name='{}'",
|
||||
info->channel,
|
||||
info->is_input == AsioTrue ? "input" : "output",
|
||||
info->is_active == AsioTrue,
|
||||
info->channel_group,
|
||||
asio_sample_type_name(info->type),
|
||||
static_cast<long>(info->type),
|
||||
info->name);
|
||||
} else if (result != ASE_OK) {
|
||||
log_warning("audio::wrappedasio", "get_channel_info failed, err={}", static_cast<long>(result));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
AsioCallbacks *WrappedAsio::install_volume_callbacks(AsioCallbacks *game_callbacks) {
|
||||
const float gain = hooks::audio::VOLUME_BOOST;
|
||||
|
||||
// start from a clean slate; a previous buffer set may have left state behind
|
||||
this->volume_channels.clear();
|
||||
this->volume_active = false;
|
||||
|
||||
// no boost configured (or no callbacks to wrap): pass the game's callbacks straight
|
||||
// through and do zero realtime work, exactly as before
|
||||
if (gain == 1.0f || game_callbacks == nullptr) {
|
||||
return game_callbacks;
|
||||
}
|
||||
|
||||
this->volume_active = true;
|
||||
this->volume_gain = gain;
|
||||
this->volume_game_callbacks = *game_callbacks;
|
||||
|
||||
// wrap only the buffer-switch callbacks, where the audio data lives and we apply the
|
||||
// gain. the other two carry no data we touch, so forward the game's own pointers
|
||||
// unchanged - the driver expects them non-null and the game already owns their context
|
||||
this->volume_proxy_callbacks = {};
|
||||
this->volume_proxy_callbacks.buffer_switch = &WrappedAsio::volume_buffer_switch;
|
||||
this->volume_proxy_callbacks.sample_rate_did_change = game_callbacks->sample_rate_did_change;
|
||||
this->volume_proxy_callbacks.asio_message = game_callbacks->asio_message;
|
||||
this->volume_proxy_callbacks.buffer_switch_time_info =
|
||||
game_callbacks->buffer_switch_time_info ? &WrappedAsio::volume_buffer_switch_time_info : nullptr;
|
||||
|
||||
return &this->volume_proxy_callbacks;
|
||||
}
|
||||
|
||||
void WrappedAsio::record_volume_output_channel(const AsioBufferInfo &info) {
|
||||
if (info.is_input != AsioFalse) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ask the real driver for this channel's sample format so the realtime path knows how
|
||||
// to scale it; fall back to a sentinel that apply_gain_planar leaves untouched
|
||||
AsioChannelInfo ci {};
|
||||
ci.channel = info.channel_num;
|
||||
ci.is_input = AsioFalse;
|
||||
AsioSampleType type = ASIOSTLastEntry;
|
||||
if (this->pReal->get_channel_info(&ci) == ASE_OK) {
|
||||
type = ci.type;
|
||||
}
|
||||
|
||||
VolumeOutputChannel ch;
|
||||
ch.buffers[0] = info.buffers[0];
|
||||
ch.buffers[1] = info.buffers[1];
|
||||
ch.type = type;
|
||||
this->volume_channels.push_back(ch);
|
||||
}
|
||||
|
||||
void WrappedAsio::publish_volume(long buffer_size) {
|
||||
if (!this->volume_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
// everything the realtime thread reads is now in place; make ourselves reachable
|
||||
this->volume_buffer_size = buffer_size;
|
||||
WrappedAsio::volume_active_instance.store(this, std::memory_order_release);
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"volume boost active: gain={}, scaling {} output channel(s)",
|
||||
this->volume_gain,
|
||||
this->volume_channels.size());
|
||||
}
|
||||
|
||||
void WrappedAsio::detach_volume() {
|
||||
// stop our realtime trampolines from reaching this wrapper, but only if we are the
|
||||
// currently published instance
|
||||
WrappedAsio *expected = this;
|
||||
WrappedAsio::volume_active_instance.compare_exchange_strong(expected, nullptr);
|
||||
}
|
||||
|
||||
void WrappedAsio::apply_output_volume(long double_buffer_index) {
|
||||
if (double_buffer_index != 0 && double_buffer_index != 1) {
|
||||
return;
|
||||
}
|
||||
const float gain = this->volume_gain;
|
||||
const long frames = this->volume_buffer_size;
|
||||
for (const VolumeOutputChannel &ch : this->volume_channels) {
|
||||
apply_gain_planar(ch.buffers[double_buffer_index], frames, ch.type, gain);
|
||||
}
|
||||
}
|
||||
|
||||
void __cdecl WrappedAsio::volume_buffer_switch(long double_buffer_index, AsioBool direct_process) {
|
||||
WrappedAsio *self = WrappedAsio::volume_active_instance.load(std::memory_order_acquire);
|
||||
if (self == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// let the game write its samples into the driver buffers first, then scale them before
|
||||
// the driver plays this half on the next switch
|
||||
if (self->volume_game_callbacks.buffer_switch != nullptr) {
|
||||
self->volume_game_callbacks.buffer_switch(double_buffer_index, direct_process);
|
||||
}
|
||||
self->apply_output_volume(double_buffer_index);
|
||||
}
|
||||
|
||||
AsioTime * __cdecl WrappedAsio::volume_buffer_switch_time_info(
|
||||
AsioTime *params, long double_buffer_index, AsioBool direct_process)
|
||||
{
|
||||
WrappedAsio *self = WrappedAsio::volume_active_instance.load(std::memory_order_acquire);
|
||||
if (self == nullptr) {
|
||||
return params;
|
||||
}
|
||||
|
||||
AsioTime *ret = params;
|
||||
if (self->volume_game_callbacks.buffer_switch_time_info != nullptr) {
|
||||
ret = self->volume_game_callbacks.buffer_switch_time_info(
|
||||
params, double_buffer_index, direct_process);
|
||||
} else if (self->volume_game_callbacks.buffer_switch != nullptr) {
|
||||
self->volume_game_callbacks.buffer_switch(double_buffer_index, direct_process);
|
||||
}
|
||||
self->apply_output_volume(double_buffer_index);
|
||||
return ret;
|
||||
}
|
||||
|
||||
AsioError __thiscall WrappedAsio::create_buffers(
|
||||
AsioBufferInfo *buffer_infos,
|
||||
long num_channels,
|
||||
long buffer_size,
|
||||
AsioCallbacks *callbacks)
|
||||
{
|
||||
// swap in our buffer-switch trampolines if a volume boost is configured, so the real
|
||||
// driver calls us and we scale its output after the game fills it (no-op otherwise)
|
||||
AsioCallbacks *effective = this->install_volume_callbacks(callbacks);
|
||||
|
||||
if (FORCE_TWO_CHANNELS) {
|
||||
return this->create_buffers_front_pair(buffer_infos, num_channels, buffer_size, effective);
|
||||
}
|
||||
|
||||
const AsioError result = this->pReal->create_buffers(buffer_infos, num_channels, buffer_size, effective);
|
||||
if (result == ASE_OK) {
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"create_buffers(channels={}, size={} frames) succeeded",
|
||||
num_channels,
|
||||
buffer_size);
|
||||
|
||||
// capture the device output channels we will scale, then publish ourselves to the
|
||||
// realtime thread once everything is in place
|
||||
if (this->volume_active) {
|
||||
for (long i = 0; i < num_channels; i++) {
|
||||
this->record_volume_output_channel(buffer_infos[i]);
|
||||
}
|
||||
this->publish_volume(buffer_size);
|
||||
}
|
||||
} else {
|
||||
log_warning(
|
||||
"audio::wrappedasio",
|
||||
"create_buffers(channels={}, size={} frames) failed, err={}",
|
||||
num_channels,
|
||||
buffer_size,
|
||||
static_cast<long>(result));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
AsioError WrappedAsio::create_buffers_front_pair(
|
||||
AsioBufferInfo *buffer_infos,
|
||||
long num_channels,
|
||||
long buffer_size,
|
||||
AsioCallbacks *callbacks)
|
||||
{
|
||||
// front-pair extraction (forced two-channel ASIO): the game asks for more output
|
||||
// channels than the real device has (e.g. 8 vs 2). forward only the channels the
|
||||
// device actually provides (channel 0/1 = front L/R) and hand the game throwaway
|
||||
// buffers for the rest, so its front mix lands on the device and the surround
|
||||
// channels are discarded. the game writes directly into the driver/dummy buffers
|
||||
// from its own bufferSwitch; the only realtime work we do is the volume boost (if
|
||||
// configured), which scales the forwarded device channels via our trampolines
|
||||
long real_in = 0, real_out = 0;
|
||||
const AsioError ch_result = this->pReal->get_channels(&real_in, &real_out);
|
||||
if (ch_result != ASE_OK) {
|
||||
log_warning(
|
||||
"audio::wrappedasio",
|
||||
"create_buffers: get_channels failed, err={}",
|
||||
static_cast<long>(ch_result));
|
||||
return ch_result;
|
||||
}
|
||||
|
||||
// partition the requested channels: those the device can serve are forwarded, the rest
|
||||
// are discarded. record source indices for both so we can patch the game's array after
|
||||
std::vector<AsioBufferInfo> forwarded;
|
||||
std::vector<long> forwarded_src;
|
||||
std::vector<long> discarded_src;
|
||||
forwarded.reserve(num_channels);
|
||||
forwarded_src.reserve(num_channels);
|
||||
discarded_src.reserve(num_channels);
|
||||
for (long i = 0; i < num_channels; i++) {
|
||||
const AsioBufferInfo &bi = buffer_infos[i];
|
||||
const long limit = (bi.is_input == AsioTrue) ? real_in : real_out;
|
||||
if (bi.channel_num < limit) {
|
||||
forwarded.push_back(bi);
|
||||
forwarded_src.push_back(i);
|
||||
} else {
|
||||
discarded_src.push_back(i);
|
||||
}
|
||||
}
|
||||
|
||||
const AsioError result = this->pReal->create_buffers(
|
||||
forwarded.data(), static_cast<long>(forwarded.size()), buffer_size, callbacks);
|
||||
if (result != ASE_OK) {
|
||||
log_warning(
|
||||
"audio::wrappedasio",
|
||||
"create_buffers(forwarded={} of {}, size={} frames) failed, err={}",
|
||||
forwarded.size(),
|
||||
num_channels,
|
||||
buffer_size,
|
||||
static_cast<long>(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
// copy the real driver buffer pointers back into the game's array
|
||||
for (size_t k = 0; k < forwarded.size(); k++) {
|
||||
AsioBufferInfo &dst = buffer_infos[forwarded_src[k]];
|
||||
dst.buffers[0] = forwarded[k].buffers[0];
|
||||
dst.buffers[1] = forwarded[k].buffers[1];
|
||||
|
||||
// only the forwarded channels reach the device, so those are the ones the volume
|
||||
// boost scales (the discarded channels go to throwaway buffers below)
|
||||
if (this->volume_active) {
|
||||
this->record_volume_output_channel(dst);
|
||||
}
|
||||
}
|
||||
|
||||
this->publish_volume(buffer_size);
|
||||
|
||||
// hand throwaway double buffers to the discarded channels. sized generously at
|
||||
// 8 bytes/sample (covers every ASIO sample type) so the game can never overrun them
|
||||
// regardless of the negotiated format
|
||||
this->dummy_buffers.clear();
|
||||
this->dummy_buffers.reserve(discarded_src.size() * 2);
|
||||
const size_t dummy_bytes = static_cast<size_t>(buffer_size) * 8;
|
||||
for (const long i : discarded_src) {
|
||||
for (void *&buffer : buffer_infos[i].buffers) {
|
||||
auto buf = std::make_unique<uint8_t[]>(dummy_bytes);
|
||||
std::memset(buf.get(), 0, dummy_bytes);
|
||||
buffer = buf.get();
|
||||
this->dummy_buffers.push_back(std::move(buf));
|
||||
}
|
||||
}
|
||||
|
||||
log_info(
|
||||
"audio::wrappedasio",
|
||||
"create_buffers: front-pair extraction - forwarded {} channel(s) to device, "
|
||||
"discarded {} (requested {}, size={} frames)",
|
||||
forwarded.size(),
|
||||
discarded_src.size(),
|
||||
num_channels,
|
||||
buffer_size);
|
||||
|
||||
return ASE_OK;
|
||||
}
|
||||
|
||||
AsioError __thiscall WrappedAsio::dispose_buffers() {
|
||||
// stop our realtime trampolines from touching buffers the driver is about to free
|
||||
this->detach_volume();
|
||||
|
||||
const AsioError result = this->pReal->dispose_buffers();
|
||||
this->dummy_buffers.clear();
|
||||
this->volume_channels.clear();
|
||||
this->volume_active = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
AsioError __thiscall WrappedAsio::control_panel() {
|
||||
return this->pReal->control_panel();
|
||||
}
|
||||
|
||||
AsioError __thiscall WrappedAsio::future(long selector, void *opt) {
|
||||
return this->pReal->future(selector, opt);
|
||||
}
|
||||
|
||||
AsioError __thiscall WrappedAsio::output_ready() {
|
||||
return this->pReal->output_ready();
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
namespace hooks::audio::asio {
|
||||
|
||||
IUnknown *wrap(REFCLSID clsid, void *real) {
|
||||
log_info("audio::wrappedasio", "wrapping ASIO driver interface, clsid={}", guid2s(clsid));
|
||||
|
||||
auto *wrapper = new WrappedAsio(
|
||||
reinterpret_cast<IAsio *>(real), clsid, registered_asio_name(clsid));
|
||||
|
||||
// if the host already had a live wrapper for this CLSID it leaked the previous
|
||||
// instance (ASIO is single-instance); tear it down now so the real driver is
|
||||
// released before the reinit. works around games (iidx32+) that ref twice but
|
||||
// deref once before re-initializing.
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "external/asio/asio.h"
|
||||
#include "external/asio/iasiodrv.h"
|
||||
|
||||
namespace hooks::audio::asio {
|
||||
|
||||
// returns true if a CoCreateInstance call is instantiating a registered ASIO driver.
|
||||
// ASIO hosts pass the driver CLSID as both class id and interface id; we also validate
|
||||
// it against the system's registered ASIO drivers to avoid false positives
|
||||
bool is_asio_creation(REFCLSID rclsid, REFIID riid);
|
||||
|
||||
// wrap a real ASIO driver instance, taking ownership of the supplied reference, and
|
||||
// return a proxy that forwards every call to it
|
||||
IUnknown *wrap(REFCLSID clsid, void *real);
|
||||
}
|
||||
|
||||
// transparent proxy around a real ASIO driver; a single place to intercept ASIO traffic
|
||||
struct WrappedAsio final : IAsio {
|
||||
WrappedAsio(IAsio *real, REFCLSID clsid, std::string name)
|
||||
: pReal(real), clsid(clsid), driver_name(std::move(name)) {
|
||||
}
|
||||
|
||||
WrappedAsio(const WrappedAsio &) = delete;
|
||||
WrappedAsio &operator=(const WrappedAsio &) = delete;
|
||||
|
||||
virtual ~WrappedAsio();
|
||||
|
||||
// when set, the proxy presents the game's expected multichannel layout to the host so
|
||||
// it proceeds to create_buffers, then forwards only the device's real front pair and
|
||||
// discards the rest (see create_buffers). set once at boot, before any wrapper exists,
|
||||
// so it needs no synchronization
|
||||
static bool FORCE_TWO_CHANNELS;
|
||||
|
||||
// some games hardcode a multichannel ASIO output and bail before create_buffers if
|
||||
// get_channels reports fewer, so we report at least this many output channels when
|
||||
// FORCE_TWO_CHANNELS is active
|
||||
static constexpr long FORCED_OUTPUT_CHANNELS = 8;
|
||||
|
||||
#pragma region IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) override;
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IAsio
|
||||
AsioBool __thiscall init(void *sys_handle) override;
|
||||
void __thiscall get_driver_name(char *name) override;
|
||||
long __thiscall get_driver_version() override;
|
||||
void __thiscall get_error_message(char *string) override;
|
||||
AsioError __thiscall start() override;
|
||||
AsioError __thiscall stop() override;
|
||||
AsioError __thiscall get_channels(long *num_input_channels, long *num_output_channels) override;
|
||||
AsioError __thiscall get_latencies(long *input_latency, long *output_latency) override;
|
||||
AsioError __thiscall get_buffer_size(
|
||||
long *min_size,
|
||||
long *max_size,
|
||||
long *preferred_size,
|
||||
long *granularity) override;
|
||||
AsioError __thiscall can_sample_rate(AsioSampleRate sample_rate) override;
|
||||
AsioError __thiscall get_sample_rate(AsioSampleRate *sample_rate) override;
|
||||
AsioError __thiscall set_sample_rate(AsioSampleRate sample_rate) override;
|
||||
AsioError __thiscall get_clock_sources(ASIOClockSource *clocks, long *num_sources) override;
|
||||
AsioError __thiscall set_clock_source(long reference) override;
|
||||
AsioError __thiscall get_sample_position(ASIOSamples *s_pos, ASIOTimeStamp *t_stamp) override;
|
||||
AsioError __thiscall get_channel_info(AsioChannelInfo *info) override;
|
||||
AsioError __thiscall create_buffers(
|
||||
AsioBufferInfo *buffer_infos,
|
||||
long num_channels,
|
||||
long buffer_size,
|
||||
AsioCallbacks *callbacks) override;
|
||||
AsioError __thiscall dispose_buffers() override;
|
||||
AsioError __thiscall control_panel() override;
|
||||
AsioError __thiscall future(long selector, void *opt) override;
|
||||
AsioError __thiscall output_ready() override;
|
||||
#pragma endregion
|
||||
|
||||
private:
|
||||
// create_buffers implementation used when FORCE_TWO_CHANNELS is active: forwards only
|
||||
// the channels the real device has and hands the game throwaway buffers for the rest
|
||||
AsioError create_buffers_front_pair(
|
||||
AsioBufferInfo *buffer_infos,
|
||||
long num_channels,
|
||||
long buffer_size,
|
||||
AsioCallbacks *callbacks);
|
||||
|
||||
// if hooks::audio::VOLUME_BOOST is set, saves the game's callbacks and returns a proxy
|
||||
// callback set (our buffer-switch trampolines) to hand the real driver instead, so we
|
||||
// can scale its output buffers after the game fills them. otherwise returns the game's
|
||||
// callbacks unchanged. called at create_buffers time, before the stream starts
|
||||
AsioCallbacks *install_volume_callbacks(AsioCallbacks *game_callbacks);
|
||||
|
||||
// records a device output channel whose buffers we scale by the volume boost. queries
|
||||
// the real driver for the channel's sample format. called at create_buffers time
|
||||
void record_volume_output_channel(const AsioBufferInfo &info);
|
||||
|
||||
// publishes the captured volume state to the realtime thread once the buffers exist,
|
||||
// making our trampolines start scaling. called at the end of either create_buffers path
|
||||
void publish_volume(long buffer_size);
|
||||
|
||||
// detaches this instance from the realtime trampolines so they stop touching its
|
||||
// buffers. called from dispose_buffers and the destructor
|
||||
void detach_volume();
|
||||
|
||||
// multiplies every recorded output channel's buffer for the given double-buffer index
|
||||
// by the volume boost. runs on the driver's realtime thread from our buffer switch
|
||||
void apply_output_volume(long double_buffer_index);
|
||||
|
||||
// realtime-thread trampolines for the buffer-switch callbacks, handed to the real
|
||||
// driver in place of the game's; ASIO callbacks carry no user data, so they reach the
|
||||
// active wrapper through volume_active_instance, call the game's original, then scale.
|
||||
// the other two callbacks (sample_rate_did_change, asio_message) are forwarded as the
|
||||
// game's own pointers, so they need no trampoline
|
||||
static void __cdecl volume_buffer_switch(long double_buffer_index, AsioBool direct_process);
|
||||
static AsioTime * __cdecl volume_buffer_switch_time_info(
|
||||
AsioTime *params, long double_buffer_index, AsioBool direct_process);
|
||||
|
||||
// the single wrapper whose proxy callbacks are installed (ASIO is single-instance with
|
||||
// one running stream); read by the static trampolines to reach the right wrapper
|
||||
static std::atomic<WrappedAsio *> volume_active_instance;
|
||||
|
||||
IAsio *const pReal;
|
||||
const CLSID clsid;
|
||||
|
||||
// registry name of the driver (not get_driver_name), used in our logs as a single
|
||||
// unambiguous name; constant for our lifetime
|
||||
std::string driver_name;
|
||||
|
||||
// our own reference count; we hold one reference on pReal and release it when this
|
||||
// drops to zero
|
||||
std::atomic<ULONG> ref_count {1};
|
||||
|
||||
// throwaway double buffers handed to the channels we discard when FORCE_TWO_CHANNELS
|
||||
// is active (see create_buffers). owned for the lifetime of the buffer set and freed
|
||||
// in dispose_buffers; only read by the game from its own bufferSwitch, never by us
|
||||
std::vector<std::unique_ptr<uint8_t[]>> dummy_buffers;
|
||||
|
||||
// one device output channel scaled by the volume boost in our buffer switch
|
||||
struct VolumeOutputChannel {
|
||||
void *buffers[2];
|
||||
AsioSampleType type;
|
||||
};
|
||||
|
||||
// volume boost state, captured at create_buffers time and published to the realtime
|
||||
// thread via volume_active_instance once fully built; untouched while the stream runs.
|
||||
// volume_active gates whether we install our proxy callbacks at all
|
||||
bool volume_active = false;
|
||||
float volume_gain = 1.0f;
|
||||
long volume_buffer_size = 0;
|
||||
AsioCallbacks volume_game_callbacks {};
|
||||
AsioCallbacks volume_proxy_callbacks {};
|
||||
std::vector<VolumeOutputChannel> volume_channels;
|
||||
};
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include "audio_private.h"
|
||||
#include "acm.h"
|
||||
#include "asio_proxy.h"
|
||||
|
||||
#ifdef _MSC_VER
|
||||
DEFINE_GUID(CLSID_MMDeviceEnumerator,
|
||||
@@ -39,9 +40,14 @@ namespace hooks::audio {
|
||||
// public globals
|
||||
bool ENABLED = true;
|
||||
bool VOLUME_HOOK_ENABLED = true;
|
||||
std::optional<DownmixAlgorithm> DOWNMIX_ALGORITHM = std::nullopt;
|
||||
float VOLUME_BOOST = 1.0f;
|
||||
std::optional<uint32_t> RESAMPLE_RATE = std::nullopt;
|
||||
std::optional<uint32_t> EXCLUSIVE_BUFFER_MS = std::nullopt;
|
||||
bool USE_DUMMY = false;
|
||||
WAVEFORMATEXTENSIBLE FORMAT {};
|
||||
std::optional<Backend> BACKEND = std::nullopt;
|
||||
bool INJECT_FAKE_REALTEK_AUDIO = false;
|
||||
std::optional<size_t> ASIO_DRIVER_ID = std::nullopt;
|
||||
std::string ASIO_DRIVER_NAME = "";
|
||||
bool ASIO_FORCE_UNLOAD_ON_STOP = false;
|
||||
@@ -66,14 +72,16 @@ static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
|
||||
if (FAILED(ret)) {
|
||||
if (IsEqualCLSID(rclsid, CLSID_MMDeviceEnumerator)) {
|
||||
log_warning("audio", "CoCreateInstance failed for CLSID_MMDeviceEnumerator, hr={}", FMT_HRESULT(ret));
|
||||
}
|
||||
|
||||
// Windows 11 KB5052093 issue - reverb DMO went missing from dsdmo.dll (fails with 0x80040154)
|
||||
if (IsEqualCLSID(rclsid, CLSID_DirectSoundI3DL2ReverbDMO) && ret == (HRESULT)0x80040154) {
|
||||
log_warning(
|
||||
"audio",
|
||||
"CoCreateInstance for CLSID_DirectSoundI3DL2ReverbDMO failed with 0x80040154, swap with CLSID_DirectSoundWavesReverbDMO "
|
||||
"(workaround for Windows 11 KB5052093 issue); REVERB EX replaced with REVERB");
|
||||
} else if (IsEqualCLSID(rclsid, CLSID_DirectSoundI3DL2ReverbDMO)) {
|
||||
// deal with reverb DMO missing from dsdmo.dll
|
||||
// it usually fails with 0x80040154 (REGDB_E_CLASSNOTREG), but we have also seen 0x8007007e (ERROR_MOD_NOT_FOUND)
|
||||
static std::once_flag printed;
|
||||
std::call_once(printed, [ret]() {
|
||||
log_warning(
|
||||
"audio",
|
||||
"CoCreateInstance for CLSID_DirectSoundI3DL2ReverbDMO failed with {}, swap with CLSID_DirectSoundWavesReverbDMO...",
|
||||
FMT_HRESULT(ret));
|
||||
});
|
||||
ret = CoCreateInstance_orig(CLSID_DirectSoundWavesReverbDMO, pUnkOuter, dwClsContext, riid, ppv);
|
||||
if (FAILED(ret)) {
|
||||
log_warning("audio", "CoCreateInstance(CLSID_DirectSoundWavesReverbDMO...) failed, hr={}", FMT_HRESULT(ret));
|
||||
@@ -89,6 +97,10 @@ static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
|
||||
// wrap object
|
||||
auto mmde = reinterpret_cast<IMMDeviceEnumerator **>(ppv);
|
||||
*mmde = new WrappedIMMDeviceEnumerator(*mmde);
|
||||
} else if (ppv != nullptr && *ppv != nullptr && hooks::audio::asio::is_asio_creation(rclsid, riid)) {
|
||||
|
||||
// wrap every ASIO driver so calls pass through to the real driver
|
||||
*ppv = hooks::audio::asio::wrap(rclsid, *ppv);
|
||||
}
|
||||
|
||||
// return original result
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
@@ -16,11 +17,34 @@ namespace hooks::audio {
|
||||
WaveOut,
|
||||
};
|
||||
|
||||
// surround-to-stereo downmix algorithm
|
||||
enum class DownmixAlgorithm {
|
||||
FrontOnly, // keep only the front channels
|
||||
RearOnly, // keep only the rear/back channels
|
||||
SideOnly, // keep only the side channels
|
||||
AC4, // AC-4 stereo downmix coefficients (ETSI TS 103 190-1)
|
||||
Normalize, // all channels equally loud, LFE dropped
|
||||
};
|
||||
|
||||
extern bool ENABLED;
|
||||
extern bool VOLUME_HOOK_ENABLED;
|
||||
extern std::optional<DownmixAlgorithm> DOWNMIX_ALGORITHM;
|
||||
extern float VOLUME_BOOST;
|
||||
|
||||
// target sample rate the hooked output is resampled to, if set
|
||||
extern std::optional<uint32_t> RESAMPLE_RATE;
|
||||
|
||||
// minimum WASAPI exclusive buffer duration (milliseconds), if set. enlarges the device buffer
|
||||
// to avoid underrun crackle on endpoints that cannot service a tiny buffer in time.
|
||||
extern std::optional<uint32_t> EXCLUSIVE_BUFFER_MS;
|
||||
extern bool USE_DUMMY;
|
||||
extern WAVEFORMATEXTENSIBLE FORMAT;
|
||||
extern std::optional<Backend> BACKEND;
|
||||
|
||||
// when true, a synthetic "Realtek" render endpoint is injected into device enumeration that
|
||||
// discards all audio. used by gitadora arena, whose device search crashes when no render
|
||||
// endpoint reports a "Realtek" friendly name.
|
||||
extern bool INJECT_FAKE_REALTEK_AUDIO;
|
||||
extern std::optional<size_t> ASIO_DRIVER_ID;
|
||||
extern std::string ASIO_DRIVER_NAME;
|
||||
extern bool ASIO_FORCE_UNLOAD_ON_STOP;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "device_collection.h"
|
||||
#include "device.h"
|
||||
#include "null_device.h"
|
||||
#include "util/utils.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
@@ -28,17 +29,48 @@ ULONG STDMETHODCALLTYPE WrappedIMMDeviceCollection::Release() {
|
||||
return refs;
|
||||
}
|
||||
|
||||
bool WrappedIMMDeviceCollection::should_inject_fake_realtek() const {
|
||||
return null_render_device_enabled()
|
||||
&& (data_flow == eRender || data_flow == eAll);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceCollection::GetCount(UINT *pcDevices) {
|
||||
// when active, hide all real devices and present only the synthetic one
|
||||
if (should_inject_fake_realtek()) {
|
||||
if (pcDevices == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*pcDevices = 1;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return pReal->GetCount(pcDevices);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceCollection::Item(UINT nDevice, IMMDevice **ppDevice) {
|
||||
if (ppDevice == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
// when active, the only device in the collection is the synthetic fake Realtek
|
||||
// render device; all real devices are hidden
|
||||
if (should_inject_fake_realtek()) {
|
||||
if (nDevice != 0) {
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
log_info("audio", "WrappedIMMDeviceCollection::Item[{}] -> synthetic fake Realtek render device", nDevice);
|
||||
*ppDevice = new NullMMDevice();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
log_info("audio", "WrappedIMMDeviceCollection::Item[{}]", nDevice);
|
||||
|
||||
// call original
|
||||
const auto hr = pReal->Item(nDevice, ppDevice);
|
||||
|
||||
// wrap interface
|
||||
*ppDevice = new WrappedIMMDevice(*ppDevice);
|
||||
if (SUCCEEDED(hr) && *ppDevice != nullptr) {
|
||||
*ppDevice = new WrappedIMMDevice(*ppDevice);
|
||||
}
|
||||
return hr;
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
#include <mmdeviceapi.h>
|
||||
|
||||
struct WrappedIMMDeviceCollection : IMMDeviceCollection {
|
||||
explicit WrappedIMMDeviceCollection(IMMDeviceCollection *orig) : pReal(orig) {
|
||||
WrappedIMMDeviceCollection(IMMDeviceCollection *orig, EDataFlow dataFlow)
|
||||
: pReal(orig), data_flow(dataFlow) {
|
||||
}
|
||||
|
||||
WrappedIMMDeviceCollection(const WrappedIMMDeviceCollection &) = delete;
|
||||
@@ -24,5 +25,9 @@ struct WrappedIMMDeviceCollection : IMMDeviceCollection {
|
||||
#pragma endregion
|
||||
|
||||
private:
|
||||
// whether the synthetic fake Realtek render device should be appended to this collection
|
||||
bool should_inject_fake_realtek() const;
|
||||
|
||||
IMMDeviceCollection *const pReal;
|
||||
const EDataFlow data_flow;
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDeviceEnumerator::EnumAudioEndpoints(
|
||||
{
|
||||
const auto hr = pReal->EnumAudioEndpoints(dataFlow, dwStateMask, ppDevices);
|
||||
if (SUCCEEDED(hr) && (ppDevices != nullptr) && (*ppDevices != nullptr)) {
|
||||
*ppDevices = new WrappedIMMDeviceCollection(*ppDevices);
|
||||
*ppDevices = new WrappedIMMDeviceCollection(*ppDevices, dataFlow);
|
||||
}
|
||||
return hr;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
#include "null_device.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
|
||||
#include <audioclient.h>
|
||||
|
||||
#include "hooks/audio/audio.h"
|
||||
#include "hooks/audio/audio_private.h"
|
||||
#include "hooks/audio/backends/wasapi/dummy_audio_client.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
#include "null_discard_backend.h"
|
||||
|
||||
// friendly name reported by the synthetic device. must contain "Realtek" so the
|
||||
// gitadora arena device search matches it.
|
||||
static const wchar_t NULL_DEVICE_FRIENDLY_NAME[] = L"Realtek High Definition Audio";
|
||||
|
||||
// arbitrary identifier reported by the synthetic device.
|
||||
static const wchar_t NULL_DEVICE_ID[] = L"{spice2x-null-render-device}";
|
||||
|
||||
// PKEY_Device_FriendlyName, hardcoded to avoid pulling in functiondiscoverykeys_devpkey.h
|
||||
static const PROPERTYKEY PKEY_DEVICE_FRIENDLY_NAME_LOCAL = {
|
||||
{ 0xa45c254e, 0xdf1c, 0x4efd, { 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0 } },
|
||||
14
|
||||
};
|
||||
|
||||
bool null_render_device_enabled() {
|
||||
return hooks::audio::INJECT_FAKE_REALTEK_AUDIO;
|
||||
}
|
||||
|
||||
// duplicate a wide string into CoTaskMem so the caller can free it with
|
||||
// CoTaskMemFree / PropVariantClear as the COM API contract requires.
|
||||
static LPWSTR co_task_wcsdup(const wchar_t *src) {
|
||||
const size_t bytes = (wcslen(src) + 1) * sizeof(wchar_t);
|
||||
auto *dst = static_cast<LPWSTR>(CoTaskMemAlloc(bytes));
|
||||
if (dst != nullptr) {
|
||||
memcpy(dst, src, bytes);
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// minimal IPropertyStore that only answers PKEY_Device_FriendlyName.
|
||||
struct NullPropertyStore : IPropertyStore {
|
||||
std::atomic<ULONG> ref_cnt = 1;
|
||||
|
||||
virtual ~NullPropertyStore() = default;
|
||||
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override {
|
||||
if (ppvObj == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
if (riid == __uuidof(IUnknown) || riid == __uuidof(IPropertyStore)) {
|
||||
this->AddRef();
|
||||
*ppvObj = this;
|
||||
return S_OK;
|
||||
}
|
||||
*ppvObj = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE AddRef() override {
|
||||
return ++this->ref_cnt;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE Release() override {
|
||||
const ULONG refs = --this->ref_cnt;
|
||||
if (refs == 0) {
|
||||
delete this;
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE GetCount(DWORD *cProps) override {
|
||||
if (cProps == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*cProps = 1;
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE GetAt(DWORD iProp, PROPERTYKEY *pkey) override {
|
||||
if (pkey == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
if (iProp != 0) {
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
*pkey = PKEY_DEVICE_FRIENDLY_NAME_LOCAL;
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE GetValue(REFPROPERTYKEY key, PROPVARIANT *pv) override {
|
||||
if (pv == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
PropVariantInit(pv);
|
||||
if (key.fmtid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.fmtid
|
||||
&& key.pid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.pid) {
|
||||
pv->pwszVal = co_task_wcsdup(NULL_DEVICE_FRIENDLY_NAME);
|
||||
if (pv->pwszVal == nullptr) {
|
||||
return E_OUTOFMEMORY;
|
||||
}
|
||||
pv->vt = VT_LPWSTR;
|
||||
}
|
||||
// unknown keys are returned as VT_EMPTY / S_OK
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE SetValue(REFPROPERTYKEY, REFPROPVARIANT) override {
|
||||
return STG_E_ACCESSDENIED;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE Commit() override {
|
||||
return S_OK;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#pragma region IUnknown
|
||||
HRESULT STDMETHODCALLTYPE NullMMDevice::QueryInterface(REFIID riid, void **ppvObj) {
|
||||
if (ppvObj == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
if (riid == __uuidof(IUnknown) || riid == __uuidof(IMMDevice)) {
|
||||
this->AddRef();
|
||||
*ppvObj = this;
|
||||
return S_OK;
|
||||
}
|
||||
*ppvObj = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE NullMMDevice::AddRef() {
|
||||
return ++this->ref_cnt;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE NullMMDevice::Release() {
|
||||
const ULONG refs = --this->ref_cnt;
|
||||
if (refs == 0) {
|
||||
delete this;
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IMMDevice
|
||||
HRESULT STDMETHODCALLTYPE NullMMDevice::Activate(
|
||||
REFIID iid,
|
||||
DWORD,
|
||||
PROPVARIANT *,
|
||||
void **ppInterface)
|
||||
{
|
||||
if (ppInterface == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*ppInterface = nullptr;
|
||||
|
||||
log_info("audio::null", "NullMMDevice::Activate {}", guid2s(iid));
|
||||
|
||||
if (iid == IID_IAudioClient) {
|
||||
|
||||
// release any previously persisted client
|
||||
if (hooks::audio::CLIENT != nullptr) {
|
||||
hooks::audio::CLIENT->Release();
|
||||
}
|
||||
|
||||
auto *client = static_cast<IAudioClient *>(new DummyIAudioClient(new NullDiscardBackend()));
|
||||
*ppInterface = client;
|
||||
|
||||
// persist the audio client
|
||||
hooks::audio::CLIENT = client;
|
||||
hooks::audio::CLIENT->AddRef();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE NullMMDevice::OpenPropertyStore(DWORD, IPropertyStore **ppProperties) {
|
||||
if (ppProperties == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*ppProperties = new NullPropertyStore();
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE NullMMDevice::GetId(LPWSTR *ppstrId) {
|
||||
if (ppstrId == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*ppstrId = co_task_wcsdup(NULL_DEVICE_ID);
|
||||
return *ppstrId != nullptr ? S_OK : E_OUTOFMEMORY;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE NullMMDevice::GetState(DWORD *pdwState) {
|
||||
if (pdwState == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*pdwState = DEVICE_STATE_ACTIVE;
|
||||
return S_OK;
|
||||
}
|
||||
#pragma endregion
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include <mmdeviceapi.h>
|
||||
|
||||
// returns true when a synthetic render endpoint should be injected into device
|
||||
// enumeration. games like gitadora arena search the render endpoint list for a
|
||||
// device whose friendly name contains "Realtek" and crash with a null pointer
|
||||
// dereference when no match exists. presenting a fake match that routes to the
|
||||
// null audio backend lets the search succeed while discarding the audio.
|
||||
bool null_render_device_enabled();
|
||||
|
||||
// fake IMMDevice that reports a "Realtek" friendly name and activates straight
|
||||
// into the null audio backend, never touching real hardware.
|
||||
struct NullMMDevice : IMMDevice {
|
||||
NullMMDevice() = default;
|
||||
|
||||
NullMMDevice(const NullMMDevice &) = delete;
|
||||
NullMMDevice &operator=(const NullMMDevice &) = delete;
|
||||
|
||||
virtual ~NullMMDevice() = default;
|
||||
|
||||
#pragma region IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IMMDevice
|
||||
HRESULT STDMETHODCALLTYPE Activate(REFIID iid, DWORD dwClsCtx, PROPVARIANT *pActivationParams, void **ppInterface) override;
|
||||
HRESULT STDMETHODCALLTYPE OpenPropertyStore(DWORD stgmAccess, IPropertyStore **ppProperties) override;
|
||||
HRESULT STDMETHODCALLTYPE GetId(LPWSTR *ppstrId) override;
|
||||
HRESULT STDMETHODCALLTYPE GetState(DWORD *pdwState) override;
|
||||
#pragma endregion
|
||||
|
||||
private:
|
||||
std::atomic<ULONG> ref_cnt = 1;
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
#include "null_discard_backend.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
|
||||
#include "hooks/audio/util.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
|
||||
NullDiscardBackend::~NullDiscardBackend() {
|
||||
this->running = false;
|
||||
if (this->pacing_thread.joinable()) {
|
||||
this->pacing_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
const WAVEFORMATEXTENSIBLE &NullDiscardBackend::format() const noexcept {
|
||||
return this->format_;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_initialize(
|
||||
AUDCLNT_SHAREMODE *,
|
||||
DWORD *,
|
||||
REFERENCE_TIME *hnsBufferDuration,
|
||||
REFERENCE_TIME *,
|
||||
const WAVEFORMATEX *pFormat,
|
||||
LPCGUID)
|
||||
{
|
||||
copy_wave_format(&this->format_, pFormat);
|
||||
|
||||
// honor the game's requested buffer duration, falling back to 10 ms
|
||||
constexpr REFERENCE_TIME DEFAULT_REFTIME = 100000; // 10 ms in 100-ns units
|
||||
this->period_reftime = (hnsBufferDuration && *hnsBufferDuration > 0)
|
||||
? *hnsBufferDuration
|
||||
: DEFAULT_REFTIME;
|
||||
|
||||
this->buffer_frames = std::max<uint32_t>(1, static_cast<uint32_t>(
|
||||
static_cast<double>(this->format_.Format.nSamplesPerSec)
|
||||
* this->period_reftime / 10000000.0 + 0.5));
|
||||
|
||||
log_info("audio::null", "initializing null render device with {} channels, {} Hz, {}-bit",
|
||||
this->format_.Format.nChannels,
|
||||
this->format_.Format.nSamplesPerSec,
|
||||
this->format_.Format.wBitsPerSample);
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_get_buffer_size(uint32_t *buffer_frames) {
|
||||
*buffer_frames = this->buffer_frames;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_get_stream_latency(REFERENCE_TIME *latency) {
|
||||
*latency = this->period_reftime;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_get_current_padding(std::optional<uint32_t> &padding_frames) {
|
||||
// discarded immediately, so the buffer always reads as fully drained
|
||||
padding_frames = 0;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_is_format_supported(
|
||||
AUDCLNT_SHAREMODE *,
|
||||
const WAVEFORMATEX *,
|
||||
WAVEFORMATEX **ppClosestMatch)
|
||||
{
|
||||
if (ppClosestMatch) {
|
||||
*ppClosestMatch = nullptr;
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_get_mix_format(WAVEFORMATEX **) {
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_get_device_period(
|
||||
REFERENCE_TIME *default_device_period,
|
||||
REFERENCE_TIME *minimum_device_period)
|
||||
{
|
||||
if (default_device_period) {
|
||||
*default_device_period = this->period_reftime;
|
||||
}
|
||||
if (minimum_device_period) {
|
||||
*minimum_device_period = this->period_reftime;
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_start() {
|
||||
if (!this->running.exchange(true)) {
|
||||
this->pacing_thread = std::thread(&NullDiscardBackend::pace_loop, this);
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_stop() {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_set_event_handle(HANDLE *event_handle) {
|
||||
// keep the game's event so pace_loop() can wake it; there is no real device behind it
|
||||
this->relay_handle = *event_handle;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) {
|
||||
const size_t buffer_size =
|
||||
static_cast<size_t>(this->format_.Format.nBlockAlign) * num_frames_requested;
|
||||
if (this->scratch.size() < buffer_size) {
|
||||
this->scratch.resize(buffer_size);
|
||||
}
|
||||
*ppData = this->scratch.data();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_release_buffer(uint32_t, DWORD) {
|
||||
// discard the audio entirely
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
void NullDiscardBackend::pace_loop() {
|
||||
using namespace std::chrono;
|
||||
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
// audio is discarded, so timing precision and drift do not matter; just wake the
|
||||
// game once per buffer period to keep its render thread from blocking on the event.
|
||||
const auto period = duration_cast<steady_clock::duration>(
|
||||
duration<double>(this->period_reftime / 10000000.0));
|
||||
|
||||
while (this->running.load()) {
|
||||
if (this->relay_handle) {
|
||||
SetEvent(this->relay_handle);
|
||||
}
|
||||
timer.sleep(period);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <audioclient.h>
|
||||
|
||||
#include "hooks/audio/implementations/backend.h"
|
||||
|
||||
// discards all audio while pacing the game's event handle once per buffer period, so the game
|
||||
// keeps running normally with nothing output to any real device. routed through the shared
|
||||
// DummyIAudioClient, the same plumbing the asio backend uses.
|
||||
struct NullDiscardBackend final : AudioBackend {
|
||||
~NullDiscardBackend() final;
|
||||
|
||||
const WAVEFORMATEXTENSIBLE &format() const noexcept override;
|
||||
|
||||
HRESULT on_initialize(
|
||||
AUDCLNT_SHAREMODE *,
|
||||
DWORD *,
|
||||
REFERENCE_TIME *hnsBufferDuration,
|
||||
REFERENCE_TIME *,
|
||||
const WAVEFORMATEX *pFormat,
|
||||
LPCGUID) override;
|
||||
HRESULT on_get_buffer_size(uint32_t *buffer_frames) override;
|
||||
HRESULT on_get_stream_latency(REFERENCE_TIME *latency) override;
|
||||
HRESULT on_get_current_padding(std::optional<uint32_t> &padding_frames) override;
|
||||
HRESULT on_is_format_supported(
|
||||
AUDCLNT_SHAREMODE *,
|
||||
const WAVEFORMATEX *,
|
||||
WAVEFORMATEX **ppClosestMatch) override;
|
||||
HRESULT on_get_mix_format(WAVEFORMATEX **) override;
|
||||
HRESULT on_get_device_period(
|
||||
REFERENCE_TIME *default_device_period,
|
||||
REFERENCE_TIME *minimum_device_period) override;
|
||||
HRESULT on_start() override;
|
||||
HRESULT on_stop() override;
|
||||
HRESULT on_set_event_handle(HANDLE *event_handle) override;
|
||||
HRESULT on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) override;
|
||||
HRESULT on_release_buffer(uint32_t, DWORD) override;
|
||||
|
||||
private:
|
||||
void pace_loop();
|
||||
|
||||
WAVEFORMATEXTENSIBLE format_ {};
|
||||
uint32_t buffer_frames = 0;
|
||||
REFERENCE_TIME period_reftime = 0;
|
||||
HANDLE relay_handle = nullptr;
|
||||
std::vector<BYTE> scratch;
|
||||
std::thread pacing_thread;
|
||||
std::atomic<bool> running = false;
|
||||
};
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <ksmedia.h>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "games/gitadora/gitadora.h"
|
||||
#include "hooks/audio/audio.h"
|
||||
#include "hooks/audio/util.h"
|
||||
#include "hooks/audio/backends/wasapi/util.h"
|
||||
@@ -41,6 +42,27 @@ static void fix_rec_format(WAVEFORMATEX *pFormat) {
|
||||
pFormat->nAvgBytesPerSec = pFormat->nSamplesPerSec * pFormat->nBlockAlign;
|
||||
}
|
||||
|
||||
// decide whether the given multi-channel format should be downmixed to stereo and which algorithm
|
||||
// to use. an explicit user selection (-downmix) takes precedence; otherwise gitadora arena
|
||||
// two-channel mode defaults to the AC-4 algorithm.
|
||||
static std::optional<hooks::audio::DownmixAlgorithm> resolve_downmix(const WAVEFORMATEX *format) {
|
||||
if (format == nullptr
|
||||
|| format->nChannels <= 2
|
||||
|| format->wFormatTag != WAVE_FORMAT_EXTENSIBLE) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (hooks::audio::DOWNMIX_ALGORITHM.has_value()) {
|
||||
return hooks::audio::DOWNMIX_ALGORITHM;
|
||||
}
|
||||
|
||||
if (games::gitadora::is_arena_model() && games::gitadora::TWOCHANNEL) {
|
||||
return hooks::audio::DownmixAlgorithm::AC4;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
IAudioClient *wrap_audio_client(IAudioClient *audio_client) {
|
||||
log_misc("audio::wasapi", "wrapping IAudioClient");
|
||||
|
||||
@@ -145,13 +167,36 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
|
||||
fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat));
|
||||
}
|
||||
|
||||
// when downmixing, open the real device as stereo while the game keeps writing its native
|
||||
// multi-channel format into the scratch buffer.
|
||||
WAVEFORMATEXTENSIBLE stereo_storage = {};
|
||||
const WAVEFORMATEX *device_format = pFormat;
|
||||
|
||||
if (auto algorithm = resolve_downmix(pFormat)) {
|
||||
this->downmix.setup(pFormat, &stereo_storage, *algorithm);
|
||||
device_format = reinterpret_cast<const WAVEFORMATEX *>(&stereo_storage);
|
||||
log_info("audio::wasapi", "downmix enabled: {} channels -> 2 channels ({})",
|
||||
pFormat->nChannels, hooks::audio::Downmix::algorithm_name(*algorithm));
|
||||
} else if (games::gitadora::is_arena_model()) {
|
||||
games::gitadora::fix_audio_channel_mask(const_cast<WAVEFORMATEX *>(pFormat));
|
||||
}
|
||||
|
||||
// when resampling, open the real device at the target rate while the game keeps writing its
|
||||
// native-rate audio into the scratch buffer. this runs on whatever device_format is now: the
|
||||
// game's native format, or the stereo format produced above when downmix is also active, so
|
||||
// the two stages chain as multi-channel -> stereo -> resampled stereo.
|
||||
WAVEFORMATEXTENSIBLE resample_storage = {};
|
||||
if (auto target_rate = hooks::audio::Resampler::resolve(device_format)) {
|
||||
const uint32_t src_rate = device_format->nSamplesPerSec;
|
||||
this->resample.setup(device_format, &resample_storage, *target_rate);
|
||||
device_format = reinterpret_cast<const WAVEFORMATEX *>(&resample_storage);
|
||||
log_info("audio::wasapi", "resample enabled: {} Hz -> {} Hz{}",
|
||||
src_rate, *target_rate, this->downmix.enabled ? " (after downmix)" : "");
|
||||
}
|
||||
|
||||
// verbose output
|
||||
log_info("audio::wasapi", "IAudioClient::Initialize hook hit");
|
||||
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(ShareMode));
|
||||
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(StreamFlags));
|
||||
log_info("audio::wasapi", "... hnsBufferDuration : {}", hnsBufferDuration);
|
||||
log_info("audio::wasapi", "... hnsPeriodicity : {}", hnsPeriodicity);
|
||||
print_format(pFormat);
|
||||
print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, device_format);
|
||||
|
||||
if (this->backend) {
|
||||
SAFE_CALL("AudioBackend", "on_initialize", this->backend->on_initialize(
|
||||
@@ -163,27 +208,67 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
|
||||
AudioSessionGuid));
|
||||
|
||||
log_info("audio::wasapi", "AudioBackend::on_initialize call finished");
|
||||
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(ShareMode));
|
||||
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(StreamFlags));
|
||||
log_info("audio::wasapi", "... hnsBufferDuration : {}", hnsBufferDuration);
|
||||
log_info("audio::wasapi", "... hnsPeriodicity : {}", hnsPeriodicity);
|
||||
print_format(pFormat);
|
||||
print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, pFormat);
|
||||
}
|
||||
|
||||
// check for exclusive mode
|
||||
if (ShareMode == AUDCLNT_SHAREMODE_EXCLUSIVE) {
|
||||
this->exclusive_mode = true;
|
||||
this->frame_size = pFormat->nChannels * (pFormat->wBitsPerSample / 8);
|
||||
this->frame_size = device_format->nChannels * (device_format->wBitsPerSample / 8);
|
||||
|
||||
// optionally enlarge the exclusive buffer. games request a very small buffer (e.g. 3 ms)
|
||||
// which some endpoints (notably NVIDIA HDMI/DP display audio) cannot service in time,
|
||||
// underrunning mid-period and crackling. a larger buffer gives the device slack. exclusive
|
||||
// mode requires periodicity == buffer_duration, so raise both together; the initialize
|
||||
// paths below handle any required buffer-size realignment.
|
||||
if (hooks::audio::EXCLUSIVE_BUFFER_MS.has_value()) {
|
||||
const REFERENCE_TIME min_duration =
|
||||
(REFERENCE_TIME) hooks::audio::EXCLUSIVE_BUFFER_MS.value() * 10000;
|
||||
if (hnsBufferDuration < min_duration) {
|
||||
log_info("audio::wasapi",
|
||||
"raising exclusive buffer from {} hns to {} hns ({} ms)",
|
||||
hnsBufferDuration, min_duration, hooks::audio::EXCLUSIVE_BUFFER_MS.value());
|
||||
hnsBufferDuration = min_duration;
|
||||
if (hnsPeriodicity != 0) {
|
||||
hnsPeriodicity = min_duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// call next
|
||||
HRESULT ret = pReal->Initialize(
|
||||
ShareMode,
|
||||
StreamFlags,
|
||||
hnsBufferDuration,
|
||||
hnsPeriodicity,
|
||||
pFormat,
|
||||
AudioSessionGuid);
|
||||
// call next. the resampler owns the device interaction whenever it is active (including when
|
||||
// chained after the downmix), otherwise the downmix does, otherwise the device is opened
|
||||
// directly.
|
||||
HRESULT ret;
|
||||
if (this->resample.enabled) {
|
||||
ret = this->resample.initialize(
|
||||
pReal,
|
||||
ShareMode,
|
||||
StreamFlags,
|
||||
hnsBufferDuration,
|
||||
hnsPeriodicity,
|
||||
device_format,
|
||||
AudioSessionGuid);
|
||||
} else if (this->downmix.enabled) {
|
||||
ret = this->downmix.initialize(
|
||||
pReal,
|
||||
ShareMode,
|
||||
StreamFlags,
|
||||
hnsBufferDuration,
|
||||
hnsPeriodicity,
|
||||
device_format,
|
||||
AudioSessionGuid);
|
||||
} else {
|
||||
ret = initialize_with_alignment_retry(
|
||||
pReal,
|
||||
"audio::wasapi",
|
||||
ShareMode,
|
||||
StreamFlags,
|
||||
hnsBufferDuration,
|
||||
hnsPeriodicity,
|
||||
device_format,
|
||||
AudioSessionGuid);
|
||||
}
|
||||
|
||||
// check for failure
|
||||
if (FAILED(ret)) {
|
||||
@@ -192,7 +277,8 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
|
||||
}
|
||||
|
||||
log_info("audio::wasapi", "IAudioClient::Initialize success, hr={}", FMT_HRESULT(ret));
|
||||
copy_wave_format(&hooks::audio::FORMAT, pFormat);
|
||||
copy_wave_format(&hooks::audio::FORMAT, device_format);
|
||||
copy_wave_format(&this->device_format, device_format);
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -214,7 +300,15 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetBufferSize(UINT32 *pNumBufferF
|
||||
}
|
||||
}
|
||||
|
||||
CHECK_RESULT(pReal->GetBufferSize(pNumBufferFrames));
|
||||
HRESULT ret = pReal->GetBufferSize(pNumBufferFrames);
|
||||
|
||||
// report the buffer size at the game's native rate; the real device buffer is at the
|
||||
// resampled rate, so translate it back so the game paces its writes correctly.
|
||||
if (SUCCEEDED(ret) && this->resample.enabled && pNumBufferFrames) {
|
||||
*pNumBufferFrames = this->resample.frames_device_to_game(*pNumBufferFrames);
|
||||
}
|
||||
|
||||
CHECK_RESULT(ret);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetStreamLatency(REFERENCE_TIME *phnsLatency) {
|
||||
static std::once_flag printed;
|
||||
@@ -256,7 +350,15 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetCurrentPadding(UINT32 *pNumPad
|
||||
}
|
||||
}
|
||||
|
||||
CHECK_RESULT(pReal->GetCurrentPadding(pNumPaddingFrames));
|
||||
HRESULT ret = pReal->GetCurrentPadding(pNumPaddingFrames);
|
||||
|
||||
// the device buffer is at the resampled rate; report padding at the game's native rate so the
|
||||
// game's free-space calculation stays paced correctly.
|
||||
if (SUCCEEDED(ret) && this->resample.enabled && pNumPaddingFrames) {
|
||||
*pNumPaddingFrames = this->resample.padding_device_to_game(*pNumPaddingFrames);
|
||||
}
|
||||
|
||||
CHECK_RESULT(ret);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::IsFormatSupported(
|
||||
AUDCLNT_SHAREMODE ShareMode,
|
||||
@@ -274,6 +376,44 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::IsFormatSupported(
|
||||
fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat));
|
||||
}
|
||||
|
||||
// log the format the game is asking about
|
||||
log_info("audio::wasapi", "IAudioClient::IsFormatSupported hook hit");
|
||||
print_format(ShareMode, pFormat);
|
||||
|
||||
// when downmixing, the real device is opened as stereo, so check whether the equivalent
|
||||
// stereo format is supported instead of the multi-channel one. when resampling is also active
|
||||
// it chains onto that stereo format, so check the resampled stereo format.
|
||||
if (resolve_downmix(pFormat)) {
|
||||
WAVEFORMATEXTENSIBLE stereo_storage = {};
|
||||
hooks::audio::Downmix::make_stereo_format(pFormat, &stereo_storage);
|
||||
const WAVEFORMATEX *check_format = reinterpret_cast<const WAVEFORMATEX *>(&stereo_storage);
|
||||
|
||||
WAVEFORMATEXTENSIBLE resample_storage = {};
|
||||
if (auto target_rate = hooks::audio::Resampler::resolve(check_format)) {
|
||||
hooks::audio::Resampler::make_device_format(check_format, &resample_storage, *target_rate);
|
||||
check_format = reinterpret_cast<const WAVEFORMATEX *>(&resample_storage);
|
||||
}
|
||||
|
||||
log_info("audio::wasapi", "... checking device format instead (after downmix/resample):");
|
||||
print_format(check_format);
|
||||
|
||||
CHECK_RESULT(pReal->IsFormatSupported(ShareMode, check_format, ppClosestMatch));
|
||||
} else if (games::gitadora::is_arena_model()) {
|
||||
games::gitadora::fix_audio_channel_mask(const_cast<WAVEFORMATEX *>(pFormat));
|
||||
} else if (auto target_rate = hooks::audio::Resampler::resolve(pFormat)) {
|
||||
|
||||
// when resampling, the real device is opened at the target rate, so check whether the
|
||||
// equivalent format at that rate is supported instead of the game's native rate.
|
||||
WAVEFORMATEXTENSIBLE resample_storage = {};
|
||||
hooks::audio::Resampler::make_device_format(pFormat, &resample_storage, *target_rate);
|
||||
const auto resample_format = reinterpret_cast<const WAVEFORMATEX *>(&resample_storage);
|
||||
|
||||
log_info("audio::wasapi", "... checking device format instead (after resample):");
|
||||
print_format(resample_format);
|
||||
|
||||
CHECK_RESULT(pReal->IsFormatSupported(ShareMode, resample_format, ppClosestMatch));
|
||||
}
|
||||
|
||||
if (this->backend) {
|
||||
HRESULT ret = this->backend->on_is_format_supported(&ShareMode, pFormat, ppClosestMatch);
|
||||
|
||||
@@ -466,5 +606,6 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::InitializeSharedAudioStream(
|
||||
|
||||
log_info("audio::wasapi", "IAudioClient3::InitializeSharedAudioStream success, hr={}", FMT_HRESULT(ret));
|
||||
copy_wave_format(&hooks::audio::FORMAT, pFormat);
|
||||
copy_wave_format(&this->device_format, pFormat);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
#include "hooks/audio/audio_private.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
#include "audio_render_client.h"
|
||||
#include "downmix.h"
|
||||
#include "resample.h"
|
||||
|
||||
#include "audio_render_client.h"
|
||||
// {1FBC8530-AF3E-4128-B418-115DE72F76B6}
|
||||
static const GUID IID_WrappedIAudioClient = {
|
||||
0x1fbc8530, 0xaf3e, 0x4128, { 0xb4, 0x18, 0x11, 0x5d, 0xe7, 0x2f, 0x76, 0xb6 }
|
||||
@@ -93,4 +95,17 @@ struct WrappedIAudioClient : IAudioClient3 {
|
||||
AudioBackend *const backend;
|
||||
bool exclusive_mode = false;
|
||||
int frame_size = 0;
|
||||
|
||||
// the format the real device was opened with (after any downmix). used to scale the final
|
||||
// output buffer for the volume boost.
|
||||
WAVEFORMATEXTENSIBLE device_format = {};
|
||||
|
||||
// surround -> stereo downmix. the real device is opened as stereo while the game keeps
|
||||
// writing multi-channel audio into a scratch buffer that we downmix in the render client.
|
||||
hooks::audio::Downmix downmix;
|
||||
|
||||
// native-rate -> target-rate sample-rate conversion. the real device is opened at the target
|
||||
// rate while the game keeps writing its native-rate audio into a scratch buffer that we
|
||||
// resample in the render client.
|
||||
hooks::audio::Resampler resample;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,71 @@
|
||||
#include "audio_render_client.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "audio_client.h"
|
||||
#include "hooks/audio/audio.h"
|
||||
#include "util.h"
|
||||
#include "wasapi_private.h"
|
||||
|
||||
const char CLASS_NAME[] = "WrappedIAudioRenderClient";
|
||||
|
||||
// scale every sample of an interleaved device buffer by `gain`, clamped to the format's range.
|
||||
// supports 16/24/32-bit PCM and 32-bit float; other formats are left untouched.
|
||||
static void apply_gain(BYTE *buffer, UINT32 frames, const WAVEFORMATEXTENSIBLE &fmt, float gain) {
|
||||
const WAVEFORMATEX &f = fmt.Format;
|
||||
const size_t samples = (size_t) frames * f.nChannels;
|
||||
|
||||
bool is_float = is_ieee_float(&f);
|
||||
|
||||
if (is_float && f.wBitsPerSample == 32) {
|
||||
auto p = reinterpret_cast<float *>(buffer);
|
||||
for (size_t i = 0; i < samples; i++) {
|
||||
p[i] = std::clamp(p[i] * gain, -1.0f, 1.0f);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (f.wBitsPerSample) {
|
||||
case 16: {
|
||||
auto p = reinterpret_cast<int16_t *>(buffer);
|
||||
for (size_t i = 0; i < samples; i++) {
|
||||
p[i] = (int16_t) std::clamp((int) std::lround(p[i] * gain), -32768, 32767);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 24: {
|
||||
// packed 24-bit little-endian
|
||||
for (size_t i = 0; i < samples; i++) {
|
||||
BYTE *s = buffer + i * 3;
|
||||
int32_t v = s[0] | (s[1] << 8) | (s[2] << 16);
|
||||
if (v & 0x800000) {
|
||||
v |= ~0xFFFFFF; // sign extend
|
||||
}
|
||||
int64_t scaled = std::clamp<int64_t>(
|
||||
std::llround((double) v * gain), -8388608, 8388607);
|
||||
s[0] = scaled & 0xFF;
|
||||
s[1] = (scaled >> 8) & 0xFF;
|
||||
s[2] = (scaled >> 16) & 0xFF;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 32: {
|
||||
auto p = reinterpret_cast<int32_t *>(buffer);
|
||||
for (size_t i = 0; i < samples; i++) {
|
||||
p[i] = (int32_t) std::clamp(
|
||||
std::llround((double) p[i] * gain),
|
||||
(long long) INT32_MIN, (long long) INT32_MAX);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::QueryInterface(REFIID riid, void **ppvObj) {
|
||||
if (ppvObj == nullptr) {
|
||||
return E_POINTER;
|
||||
@@ -51,6 +112,25 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::GetBuffer(UINT32 NumFramesR
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// downmix + resample chained: the game writes its multi-channel native-rate audio into the
|
||||
// downmix scratch, which is downmixed to stereo and then resampled on release. size the
|
||||
// resampler's (stereo) input scratch now and hand the game the multi-channel downmix scratch.
|
||||
if (this->client->downmix.enabled && this->client->resample.enabled) {
|
||||
BYTE *resample_scratch = nullptr;
|
||||
this->client->resample.get_buffer(NumFramesRequested, &resample_scratch);
|
||||
CHECK_RESULT(this->client->downmix.get_scratch(NumFramesRequested, ppData));
|
||||
|
||||
// surround downmix: reserve the real (stereo) device buffer, but hand the game a
|
||||
// multi-channel scratch buffer that we downmix on release
|
||||
} else if (this->client->downmix.enabled) {
|
||||
CHECK_RESULT(this->client->downmix.get_buffer(pReal, NumFramesRequested, ppData));
|
||||
|
||||
// resample: hand the game a native-rate scratch buffer that we convert on release. the real
|
||||
// device buffer is acquired in ReleaseBuffer once the converted frame count is known.
|
||||
} else if (this->client->resample.enabled) {
|
||||
CHECK_RESULT(this->client->resample.get_buffer(NumFramesRequested, ppData));
|
||||
}
|
||||
|
||||
// call original
|
||||
HRESULT ret = pReal->GetBuffer(NumFramesRequested, ppData);
|
||||
|
||||
@@ -75,14 +155,72 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::ReleaseBuffer(UINT32 NumFra
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// fix for audio pop effect
|
||||
if (this->buffers_to_mute > 0 && this->client->frame_size > 0) {
|
||||
|
||||
// zero out = mute
|
||||
memset(this->audio_buffer, 0, NumFramesWritten * this->client->frame_size);
|
||||
|
||||
this->buffers_to_mute--;
|
||||
// downmix + resample chained: downmix the game's multi-channel scratch into the resampler's
|
||||
// stereo input scratch, then let the resampler convert and push it to the device. a silent
|
||||
// buffer skips the downmix and feeds silence straight through.
|
||||
if (this->client->downmix.enabled && this->client->resample.enabled) {
|
||||
if ((dwFlags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
|
||||
this->client->downmix.downmix_into(
|
||||
this->client->resample.input_data(), NumFramesWritten);
|
||||
}
|
||||
return this->client->resample.flush(
|
||||
pReal,
|
||||
this->client->pReal,
|
||||
NumFramesWritten,
|
||||
dwFlags,
|
||||
hooks::audio::VOLUME_BOOST);
|
||||
}
|
||||
|
||||
CHECK_RESULT(pReal->ReleaseBuffer(NumFramesWritten, dwFlags));
|
||||
// resample: convert the game's native-rate scratch and push as many output frames as the
|
||||
// device has room for, applying the volume boost to the converted output. handles acquiring
|
||||
// and releasing the real device buffer itself.
|
||||
if (this->client->resample.enabled) {
|
||||
return this->client->resample.flush(
|
||||
pReal,
|
||||
this->client->pReal,
|
||||
NumFramesWritten,
|
||||
dwFlags,
|
||||
hooks::audio::VOLUME_BOOST);
|
||||
}
|
||||
|
||||
// resolve the real device buffer for whichever path produced the audio
|
||||
BYTE *device_buffer;
|
||||
if (this->client->downmix.enabled) {
|
||||
|
||||
// downmix the game's multi-channel scratch into the real stereo buffer held since GetBuffer
|
||||
this->client->downmix.write_device_buffer(NumFramesWritten, dwFlags);
|
||||
device_buffer = this->client->downmix.current_buffer();
|
||||
} else {
|
||||
device_buffer = this->audio_buffer;
|
||||
|
||||
// mute the first few buffers to avoid a startup pop
|
||||
if (this->buffers_to_mute > 0 && this->client->frame_size > 0) {
|
||||
memset(this->audio_buffer, 0, NumFramesWritten * this->client->frame_size);
|
||||
this->buffers_to_mute--;
|
||||
}
|
||||
}
|
||||
|
||||
// boost the final output volume just before it reaches the device, layout-agnostic
|
||||
if (hooks::audio::VOLUME_BOOST != 1.0f
|
||||
&& device_buffer != nullptr
|
||||
&& (dwFlags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
|
||||
static std::once_flag boost_printed;
|
||||
std::call_once(boost_printed, []() {
|
||||
log_info("audio::wasapi", "volume boost active: gain={}", hooks::audio::VOLUME_BOOST);
|
||||
});
|
||||
apply_gain(device_buffer, NumFramesWritten, this->client->device_format,
|
||||
hooks::audio::VOLUME_BOOST);
|
||||
}
|
||||
|
||||
HRESULT ret = pReal->ReleaseBuffer(NumFramesWritten, dwFlags);
|
||||
|
||||
if (this->client->downmix.enabled) {
|
||||
this->client->downmix.buffer_released();
|
||||
}
|
||||
|
||||
if (FAILED(ret)) {
|
||||
PRINT_FAILED_RESULT(CLASS_NAME, __func__, ret);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
#include "downmix.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include <audioclient.h>
|
||||
#include <ks.h>
|
||||
#include <ksmedia.h>
|
||||
|
||||
#include "util/logging.h"
|
||||
|
||||
#include "util.h"
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float ATT_3DB = 0.70710678f;
|
||||
|
||||
// speakers routed to the left/right output; anything else (center) feeds both sides
|
||||
constexpr DWORD LEFT_SPEAKERS = SPEAKER_FRONT_LEFT | SPEAKER_BACK_LEFT | SPEAKER_SIDE_LEFT
|
||||
| SPEAKER_FRONT_LEFT_OF_CENTER | SPEAKER_TOP_FRONT_LEFT | SPEAKER_TOP_BACK_LEFT;
|
||||
constexpr DWORD RIGHT_SPEAKERS = SPEAKER_FRONT_RIGHT | SPEAKER_BACK_RIGHT | SPEAKER_SIDE_RIGHT
|
||||
| SPEAKER_FRONT_RIGHT_OF_CENTER | SPEAKER_TOP_FRONT_RIGHT | SPEAKER_TOP_BACK_RIGHT;
|
||||
|
||||
// the speaker mask is only present on WAVE_FORMAT_EXTENSIBLE formats
|
||||
DWORD read_channel_mask(const WAVEFORMATEX *fmt) {
|
||||
if (fmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE
|
||||
&& fmt->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
|
||||
return reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(fmt)->dwChannelMask;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// call visit(channel_index, speaker_bit) for each present speaker, in channel order
|
||||
template <typename F>
|
||||
void for_each_speaker(DWORD mask, int channels, F &&visit) {
|
||||
int channel = 0;
|
||||
for (int bit = 0; bit < 18 && channel < channels; bit++) {
|
||||
const DWORD speaker = 1u << bit;
|
||||
if (mask & speaker) {
|
||||
visit(channel++, speaker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Downmix::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out,
|
||||
DownmixAlgorithm algorithm) {
|
||||
this->enabled = true;
|
||||
this->algorithm = algorithm;
|
||||
this->bytes_per_sample = game_format->wBitsPerSample / 8;
|
||||
this->game_frame_size = game_format->nChannels * this->bytes_per_sample;
|
||||
|
||||
this->is_float = is_ieee_float(game_format);
|
||||
|
||||
// supported: 16/24/32-bit integer PCM and 32-bit float; anything else mixes to silence
|
||||
const bool supported = this->is_float
|
||||
? this->bytes_per_sample == 4
|
||||
: (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4);
|
||||
if (!supported) {
|
||||
log_fatal(
|
||||
"audio::downmix",
|
||||
"unsupported sample format ({}-bit {}), downmix will output silence",
|
||||
game_format->wBitsPerSample, this->is_float ? "float" : "int");
|
||||
}
|
||||
|
||||
this->left_mix.clear();
|
||||
this->right_mix.clear();
|
||||
this->build_layout_mix(game_format);
|
||||
|
||||
make_stereo_format(game_format, stereo_out);
|
||||
}
|
||||
|
||||
void Downmix::make_stereo_format(const WAVEFORMATEX *game_format,
|
||||
WAVEFORMATEXTENSIBLE *stereo_out) {
|
||||
const int bytes_per_sample = game_format->wBitsPerSample / 8;
|
||||
|
||||
memcpy(stereo_out, game_format, sizeof(WAVEFORMATEXTENSIBLE));
|
||||
stereo_out->Format.nChannels = 2;
|
||||
stereo_out->Format.nBlockAlign = 2 * bytes_per_sample;
|
||||
stereo_out->Format.nAvgBytesPerSec =
|
||||
game_format->nSamplesPerSec * stereo_out->Format.nBlockAlign;
|
||||
stereo_out->dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
|
||||
}
|
||||
|
||||
HRESULT Downmix::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
|
||||
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
|
||||
const WAVEFORMATEX *device_format, LPCGUID session_guid) {
|
||||
|
||||
// the smaller stereo buffer can end up unaligned for the device when the game sized the
|
||||
// duration for its larger multi-channel format; the helper recovers from that.
|
||||
return initialize_with_alignment_retry(real, "audio::downmix", share_mode, stream_flags,
|
||||
buffer_duration, periodicity, device_format, session_guid);
|
||||
}
|
||||
|
||||
void Downmix::add_channel(int channel, DWORD speaker, float gain) {
|
||||
if (speaker & LEFT_SPEAKERS) {
|
||||
this->left_mix.push_back({ channel, gain });
|
||||
} else if (speaker & RIGHT_SPEAKERS) {
|
||||
this->right_mix.push_back({ channel, gain });
|
||||
} else { // center: feed both sides
|
||||
this->left_mix.push_back({ channel, gain });
|
||||
this->right_mix.push_back({ channel, gain });
|
||||
}
|
||||
}
|
||||
|
||||
// AC-4 stereo downmix (ETSI TS 103 190-1): front pair at unity, everything else -3 dB, LFE dropped
|
||||
void Downmix::build_ac4_mix(DWORD mask, int channels) {
|
||||
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
|
||||
if (speaker == SPEAKER_LOW_FREQUENCY) {
|
||||
return;
|
||||
}
|
||||
const bool front_pair = speaker & (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT);
|
||||
this->add_channel(ch, speaker, front_pair ? 1.0f : ATT_3DB);
|
||||
});
|
||||
}
|
||||
|
||||
// keep only the channels in `keep` (front/rear/side), each at unity gain
|
||||
void Downmix::build_extract_mix(DWORD mask, int channels, DWORD keep) {
|
||||
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
|
||||
if (speaker & keep) {
|
||||
this->add_channel(ch, speaker, 1.0f);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// keep every channel (LFE dropped), then average each side so its gains sum to unity
|
||||
void Downmix::build_normalize_mix(DWORD mask, int channels) {
|
||||
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
|
||||
if (speaker != SPEAKER_LOW_FREQUENCY) {
|
||||
this->add_channel(ch, speaker, 1.0f);
|
||||
}
|
||||
});
|
||||
|
||||
for (auto *mix : { &this->left_mix, &this->right_mix }) {
|
||||
if (!mix->empty()) {
|
||||
const float gain = 1.0f / mix->size();
|
||||
for (auto &c : *mix) {
|
||||
c.gain = gain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback when no speaker mask is present: fold interleaved L/R pairs (even->left, odd->right)
|
||||
void Downmix::build_pairs_mix(int channels, float gain) {
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
(((ch & 1) == 0) ? this->left_mix : this->right_mix).push_back({ ch, gain });
|
||||
}
|
||||
}
|
||||
|
||||
void Downmix::build_layout_mix(const WAVEFORMATEX *game_format) {
|
||||
const int channels = game_format->nChannels;
|
||||
const DWORD mask = read_channel_mask(game_format);
|
||||
|
||||
// without a mask the layout is unknown: extract/normalize have nothing to act on, so all
|
||||
// algorithms fall back to folding L/R pairs (AC-4 still attenuates by -3 dB)
|
||||
if (mask == 0) {
|
||||
this->build_pairs_mix(channels,
|
||||
this->algorithm == DownmixAlgorithm::AC4 ? ATT_3DB : 1.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this->algorithm) {
|
||||
case DownmixAlgorithm::FrontOnly:
|
||||
this->build_extract_mix(mask, channels,
|
||||
SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT);
|
||||
break;
|
||||
case DownmixAlgorithm::RearOnly:
|
||||
this->build_extract_mix(mask, channels,
|
||||
SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | SPEAKER_BACK_CENTER);
|
||||
break;
|
||||
case DownmixAlgorithm::SideOnly:
|
||||
this->build_extract_mix(mask, channels,
|
||||
SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT);
|
||||
break;
|
||||
case DownmixAlgorithm::Normalize:
|
||||
this->build_normalize_mix(mask, channels);
|
||||
break;
|
||||
case DownmixAlgorithm::AC4:
|
||||
this->build_ac4_mix(mask, channels);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Downmix::process(BYTE *dst, const BYTE *src, UINT32 frames) const {
|
||||
const int bps = this->bytes_per_sample;
|
||||
const int src_stride = this->game_frame_size;
|
||||
const int dst_stride = 2 * bps;
|
||||
|
||||
if (dst == nullptr || src == nullptr || bps <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// sum each speaker's source channels into the matching stereo output
|
||||
for (UINT32 i = 0; i < frames; i++) {
|
||||
const BYTE *in = src + (size_t) i * src_stride;
|
||||
BYTE *out = dst + (size_t) i * dst_stride;
|
||||
|
||||
float left = 0.0f;
|
||||
float right = 0.0f;
|
||||
for (const auto &c : this->left_mix) {
|
||||
left += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain;
|
||||
}
|
||||
for (const auto &c : this->right_mix) {
|
||||
right += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain;
|
||||
}
|
||||
write_sample(out, bps, this->is_float, left);
|
||||
write_sample(out + bps, bps, this->is_float, right);
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT Downmix::get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData) {
|
||||
const size_t needed = (size_t) frames * this->game_frame_size;
|
||||
if (this->scratch.size() < needed) {
|
||||
this->scratch.resize(needed);
|
||||
}
|
||||
|
||||
HRESULT ret = real->GetBuffer(frames, &this->device_buffer);
|
||||
if (FAILED(ret)) {
|
||||
this->device_buffer = nullptr;
|
||||
return ret;
|
||||
}
|
||||
|
||||
*ppData = this->scratch.data();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT Downmix::get_scratch(UINT32 frames, BYTE **ppData) {
|
||||
const size_t needed = (size_t) frames * this->game_frame_size;
|
||||
if (this->scratch.size() < needed) {
|
||||
this->scratch.resize(needed);
|
||||
}
|
||||
|
||||
*ppData = this->scratch.data();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
void Downmix::downmix_into(BYTE *dst, UINT32 frames) const {
|
||||
this->process(dst, this->scratch.data(), frames);
|
||||
}
|
||||
|
||||
void Downmix::write_device_buffer(UINT32 frames, DWORD flags) {
|
||||
const int bps = this->bytes_per_sample;
|
||||
const int dst_stride = 2 * bps;
|
||||
|
||||
if (this->device_buffer == nullptr || frames == 0 || bps <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// mute the first few buffers to avoid a pop on stream start
|
||||
if (this->buffers_to_mute > 0) {
|
||||
memset(this->device_buffer, 0, (size_t) frames * dst_stride);
|
||||
this->buffers_to_mute--;
|
||||
} else if ((flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
|
||||
this->process(this->device_buffer, this->scratch.data(), frames);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
#include <mmreg.h>
|
||||
#include <audioclient.h>
|
||||
|
||||
#include "hooks/audio/audio.h"
|
||||
|
||||
struct IAudioClient;
|
||||
struct IAudioRenderClient;
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
// Generic WASAPI surround-to-stereo downmix. The real device is opened in stereo while the
|
||||
// game keeps writing its native multi-channel audio into a scratch buffer; on release that
|
||||
// buffer is mixed down into the two front channels.
|
||||
//
|
||||
// The mix is derived from the source format's speaker mask according to the selected
|
||||
// DownmixAlgorithm:
|
||||
// FrontOnly / RearOnly / SideOnly - keep only that group of channels, routed to their side
|
||||
// AC4 - AC-4 stereo downmix coefficients (ETSI TS 103 190-1 §6.2.17): front left/right
|
||||
// pass at 0 dB, center and surrounds fold in at -3 dB, LFE dropped
|
||||
// Normalize - every channel folded in (center to both sides) with each output side averaged
|
||||
// so its channels are equally loud, LFE dropped
|
||||
struct Downmix {
|
||||
|
||||
// a source channel routed into one output speaker at the given gain
|
||||
struct Contribution {
|
||||
int channel;
|
||||
float gain;
|
||||
};
|
||||
|
||||
// map an option value (front/rear/side/ac4/normalize) to its algorithm.
|
||||
static std::optional<DownmixAlgorithm> name_to_algorithm(const char *value) {
|
||||
if (_stricmp(value, "front") == 0) {
|
||||
return DownmixAlgorithm::FrontOnly;
|
||||
} else if (_stricmp(value, "rear") == 0) {
|
||||
return DownmixAlgorithm::RearOnly;
|
||||
} else if (_stricmp(value, "side") == 0) {
|
||||
return DownmixAlgorithm::SideOnly;
|
||||
} else if (_stricmp(value, "ac4") == 0) {
|
||||
return DownmixAlgorithm::AC4;
|
||||
} else if (_stricmp(value, "normalize") == 0) {
|
||||
return DownmixAlgorithm::Normalize;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// human-readable name of an algorithm, for logging.
|
||||
static const char *algorithm_name(DownmixAlgorithm algorithm) {
|
||||
switch (algorithm) {
|
||||
case DownmixAlgorithm::FrontOnly: return "front";
|
||||
case DownmixAlgorithm::RearOnly: return "rear";
|
||||
case DownmixAlgorithm::SideOnly: return "side";
|
||||
case DownmixAlgorithm::AC4: return "ac4";
|
||||
case DownmixAlgorithm::Normalize: return "normalize";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// whether the downmix is active for the current stream
|
||||
bool enabled = false;
|
||||
|
||||
// algorithm used to fold the multi-channel audio into stereo
|
||||
DownmixAlgorithm algorithm = DownmixAlgorithm::AC4;
|
||||
|
||||
// size in bytes of one frame of the game's multi-channel format
|
||||
int game_frame_size = 0;
|
||||
|
||||
// size in bytes of a single sample (per channel)
|
||||
int bytes_per_sample = 0;
|
||||
|
||||
// whether samples are IEEE floating point rather than integer PCM
|
||||
bool is_float = false;
|
||||
|
||||
// enable the downmix for the given game format and fill stereo_out with the equivalent
|
||||
// stereo format to open the real device with.
|
||||
void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out,
|
||||
DownmixAlgorithm algorithm);
|
||||
|
||||
// build the stereo format equivalent to game_format (same sample rate and bit depth).
|
||||
static void make_stereo_format(const WAVEFORMATEX *game_format,
|
||||
WAVEFORMATEXTENSIBLE *stereo_out);
|
||||
|
||||
// initialize the real device with the stereo format. downmixing reduces the channel count,
|
||||
// shrinking the buffer's byte size, so the duration the game sized for its multi-channel
|
||||
// format can leave the smaller stereo buffer unaligned. on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED
|
||||
// this performs the standard WASAPI realignment and retries.
|
||||
HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
|
||||
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
|
||||
const WAVEFORMATEX *device_format, LPCGUID session_guid);
|
||||
|
||||
// mix `frames` frames of multi-channel `src` down into stereo `dst`.
|
||||
void process(BYTE *dst, const BYTE *src, UINT32 frames) const;
|
||||
|
||||
// grab the real stereo device buffer and hand the game the scratch buffer to write into.
|
||||
HRESULT get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData);
|
||||
|
||||
// size the scratch and hand it to the game without acquiring a device buffer. used when a
|
||||
// later stage (the resampler) owns the device interaction.
|
||||
HRESULT get_scratch(UINT32 frames, BYTE **ppData);
|
||||
|
||||
// downmix the scratch the game wrote into the caller's stereo buffer, without touching the
|
||||
// device. used to feed the resampler when the two stages are chained.
|
||||
void downmix_into(BYTE *dst, UINT32 frames) const;
|
||||
|
||||
// mix the scratch buffer into the stereo device buffer held since get_buffer. the caller
|
||||
// owns releasing the device buffer afterwards (see current_buffer / buffer_released).
|
||||
void write_device_buffer(UINT32 frames, DWORD flags);
|
||||
|
||||
// the real device buffer currently held, or null.
|
||||
BYTE *current_buffer() const { return this->device_buffer; }
|
||||
|
||||
// forget the held device buffer once the caller has released it.
|
||||
void buffer_released() { this->device_buffer = nullptr; }
|
||||
|
||||
private:
|
||||
|
||||
// build the mix from the source speaker layout for the selected algorithm
|
||||
void build_layout_mix(const WAVEFORMATEX *game_format);
|
||||
|
||||
// per-algorithm builders, each filling left_mix / right_mix from the speaker mask
|
||||
void build_ac4_mix(DWORD mask, int channels);
|
||||
void build_extract_mix(DWORD mask, int channels, DWORD keep);
|
||||
void build_normalize_mix(DWORD mask, int channels);
|
||||
|
||||
// fallback for streams without a speaker mask: fold interleaved L/R pairs at `gain`
|
||||
void build_pairs_mix(int channels, float gain);
|
||||
|
||||
// append one source channel to the output side(s) matching its speaker, at `gain`
|
||||
void add_channel(int channel, DWORD speaker, float gain);
|
||||
|
||||
// source channels summed into each output speaker
|
||||
std::vector<Contribution> left_mix;
|
||||
std::vector<Contribution> right_mix;
|
||||
|
||||
// buffer the game writes its multi-channel audio into between get/release
|
||||
std::vector<BYTE> scratch;
|
||||
|
||||
// the real stereo device buffer currently held, or null
|
||||
BYTE *device_buffer = nullptr;
|
||||
|
||||
// leading buffers to silence to avoid a pop on stream start
|
||||
int buffers_to_mute = 16;
|
||||
};
|
||||
}
|
||||
@@ -73,11 +73,7 @@ HRESULT STDMETHODCALLTYPE DummyIAudioClient::Initialize(
|
||||
|
||||
// verbose output
|
||||
log_info("audio::wasapi", "IAudioClient::Initialize hook hit");
|
||||
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(ShareMode));
|
||||
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(StreamFlags));
|
||||
log_info("audio::wasapi", "... hnsBufferDuration : {}", hnsBufferDuration);
|
||||
log_info("audio::wasapi", "... hnsPeriodicity : {}", hnsPeriodicity);
|
||||
print_format(pFormat);
|
||||
print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, pFormat);
|
||||
|
||||
CHECK_RESULT(this->backend->on_initialize(
|
||||
&ShareMode,
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
#include "resample.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
|
||||
#include <audioclient.h>
|
||||
|
||||
#include "util/logging.h"
|
||||
|
||||
#include "util.h"
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr double PI = 3.14159265358979323846;
|
||||
|
||||
// normalized sinc: sin(pi*x) / (pi*x), with the removable singularity at 0 filled in
|
||||
inline double sinc(double x) {
|
||||
if (x == 0.0) {
|
||||
return 1.0;
|
||||
}
|
||||
const double px = PI * x;
|
||||
return std::sin(px) / px;
|
||||
}
|
||||
|
||||
// Blackman window across the kernel radius; zero at +/- radius
|
||||
inline double blackman(double x, double radius) {
|
||||
const double n = (x + radius) / (2.0 * radius);
|
||||
if (n <= 0.0 || n >= 1.0) {
|
||||
return 0.0;
|
||||
}
|
||||
return 0.42 - 0.5 * std::cos(2.0 * PI * n) + 0.08 * std::cos(4.0 * PI * n);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<uint32_t> Resampler::resolve(const WAVEFORMATEX *game_format) {
|
||||
if (game_format == nullptr || !RESAMPLE_RATE.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (game_format->nSamplesPerSec == 0
|
||||
|| game_format->nSamplesPerSec == RESAMPLE_RATE.value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return RESAMPLE_RATE;
|
||||
}
|
||||
|
||||
void Resampler::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *device_out,
|
||||
uint32_t target_rate) {
|
||||
this->enabled = true;
|
||||
this->channels = game_format->nChannels;
|
||||
this->bytes_per_sample = game_format->wBitsPerSample / 8;
|
||||
this->game_frame_size = this->channels * this->bytes_per_sample;
|
||||
|
||||
this->is_float = is_ieee_float(game_format);
|
||||
|
||||
const bool supported = this->is_float
|
||||
? this->bytes_per_sample == 4
|
||||
: (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4);
|
||||
if (!supported) {
|
||||
log_fatal(
|
||||
"audio::resample",
|
||||
"unsupported sample format ({}-bit {}) for -resample",
|
||||
game_format->wBitsPerSample, this->is_float ? "float" : "int");
|
||||
}
|
||||
|
||||
this->src_rate = game_format->nSamplesPerSec;
|
||||
this->dst_rate = target_rate;
|
||||
|
||||
// anti-alias cutoff: full bandwidth when upsampling, scaled down when decimating
|
||||
this->cutoff = std::min(1.0, (double) this->dst_rate / (double) this->src_rate);
|
||||
this->half_taps = 16;
|
||||
|
||||
// precompute the windowed-sinc kernel now that cutoff is known
|
||||
this->build_kernel();
|
||||
|
||||
// prime the queue with half a window of silence so the first outputs have left history
|
||||
this->in_queue.assign((size_t) this->half_taps * this->channels, 0.0f);
|
||||
this->in_pos = this->half_taps;
|
||||
|
||||
this->make_device_format(game_format, device_out, target_rate);
|
||||
}
|
||||
|
||||
void Resampler::make_device_format(const WAVEFORMATEX *game_format,
|
||||
WAVEFORMATEXTENSIBLE *device_out, uint32_t target_rate) {
|
||||
const size_t src_size = sizeof(WAVEFORMATEX) + game_format->cbSize;
|
||||
|
||||
memset(device_out, 0, sizeof(WAVEFORMATEXTENSIBLE));
|
||||
memcpy(device_out, game_format, std::min(src_size, sizeof(WAVEFORMATEXTENSIBLE)));
|
||||
|
||||
device_out->Format.nSamplesPerSec = target_rate;
|
||||
device_out->Format.nAvgBytesPerSec = target_rate * device_out->Format.nBlockAlign;
|
||||
}
|
||||
|
||||
HRESULT Resampler::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode,
|
||||
DWORD stream_flags, REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
|
||||
const WAVEFORMATEX *device_format, LPCGUID session_guid) {
|
||||
|
||||
// the resampler bypasses the OS mixer and talks to the device directly, so it only makes
|
||||
// sense (and only works) for exclusive streams. shared streams are already resampled by
|
||||
// the Windows audio engine, so refuse loudly rather than silently doing nothing.
|
||||
if (share_mode != AUDCLNT_SHAREMODE_EXCLUSIVE) {
|
||||
log_fatal("audio::resample",
|
||||
"-resample requires WASAPI exclusive mode, but this stream is shared "
|
||||
"(Windows already resamples shared streams)");
|
||||
}
|
||||
|
||||
// record the pacing model. event-driven streams fill the whole device buffer each period
|
||||
// (produce_exact); timer-driven streams poll padding and write variable partial chunks, so
|
||||
// they drain the pending output to the device's free space each call (flush_timer).
|
||||
this->event_driven = (stream_flags & AUDCLNT_STREAMFLAGS_EVENTCALLBACK) != 0;
|
||||
|
||||
return initialize_with_alignment_retry(real, "audio::resample", share_mode, stream_flags,
|
||||
buffer_duration, periodicity, device_format, session_guid);
|
||||
}
|
||||
|
||||
UINT32 Resampler::frames_device_to_game(UINT32 device_frames) const {
|
||||
if (this->dst_rate == 0) {
|
||||
return device_frames;
|
||||
}
|
||||
// round down so the game never believes it has more room than the device can hold
|
||||
return (UINT32) (((double) device_frames * this->src_rate) / this->dst_rate);
|
||||
}
|
||||
|
||||
UINT32 Resampler::padding_device_to_game(UINT32 device_padding) const {
|
||||
if (this->dst_rate == 0) {
|
||||
return device_padding;
|
||||
}
|
||||
// round up so the reported free space stays conservative
|
||||
return (UINT32) std::ceil(((double) device_padding * this->src_rate) / this->dst_rate);
|
||||
}
|
||||
|
||||
HRESULT Resampler::get_buffer(UINT32 frames, BYTE **ppData) {
|
||||
const size_t needed = (size_t) frames * this->game_frame_size;
|
||||
if (this->scratch.size() < needed) {
|
||||
this->scratch.resize(needed);
|
||||
}
|
||||
|
||||
*ppData = this->scratch.data();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
void Resampler::enqueue_input(UINT32 frames, bool silent) {
|
||||
const int bps = this->bytes_per_sample;
|
||||
const int ch = this->channels;
|
||||
const size_t base = this->in_queue.size();
|
||||
|
||||
this->in_queue.resize(base + (size_t) frames * ch);
|
||||
|
||||
if (silent || bps <= 0 || ch <= 0) {
|
||||
std::fill(this->in_queue.begin() + base, this->in_queue.end(), 0.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
const BYTE *src = this->scratch.data();
|
||||
for (UINT32 f = 0; f < frames; f++) {
|
||||
for (int c = 0; c < ch; c++) {
|
||||
const size_t s = (size_t) f * ch + c;
|
||||
this->in_queue[base + s] = read_sample(src + s * bps, bps, this->is_float);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Resampler::build_kernel() {
|
||||
const int taps = 2 * this->half_taps;
|
||||
const int phases = this->kernel_phases;
|
||||
const double cut = this->cutoff;
|
||||
const double radius = (double) this->half_taps;
|
||||
|
||||
// one extra row at frac == 1.0 so emit_frame can interpolate against row p + 1 safely
|
||||
this->kernel_table.resize((size_t) (phases + 1) * taps);
|
||||
|
||||
for (int p = 0; p <= phases; p++) {
|
||||
const double frac = (double) p / (double) phases;
|
||||
for (int k = 0; k < taps; k++) {
|
||||
// tap k maps to input offset t = k - (half_taps - 1), matching emit_frame
|
||||
const double x = frac - (double) (k - (this->half_taps - 1));
|
||||
this->kernel_table[(size_t) p * taps + k] =
|
||||
(float) (cut * sinc(cut * x) * blackman(x, radius));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Resampler::emit_frame() {
|
||||
const int ch = this->channels;
|
||||
const int radius = this->half_taps;
|
||||
const int taps = 2 * radius;
|
||||
const long avail = (long) (this->in_queue.size() / ch);
|
||||
const long center = (long) std::floor(this->in_pos);
|
||||
|
||||
// pick the two kernel rows bracketing this fractional position and the blend between them
|
||||
const double frac = this->in_pos - (double) center;
|
||||
const double fp = frac * (double) this->kernel_phases;
|
||||
const int p0 = (int) fp;
|
||||
const float blend = (float) (fp - (double) p0);
|
||||
const float *row0 = &this->kernel_table[(size_t) p0 * taps];
|
||||
const float *row1 = &this->kernel_table[(size_t) (p0 + 1) * taps];
|
||||
|
||||
// base input index for tap 0 (t = -(radius - 1))
|
||||
const long base = center - (radius - 1);
|
||||
|
||||
for (int c = 0; c < ch; c++) {
|
||||
double acc = 0.0;
|
||||
for (int k = 0; k < taps; k++) {
|
||||
const long idx = base + k;
|
||||
if (idx < 0 || idx >= avail) {
|
||||
continue;
|
||||
}
|
||||
const float w = row0[k] + blend * (row1[k] - row0[k]);
|
||||
acc += (double) this->in_queue[(size_t) idx * ch + c] * w;
|
||||
}
|
||||
this->out_float.push_back((float) acc);
|
||||
}
|
||||
}
|
||||
|
||||
void Resampler::drop_consumed() {
|
||||
const int ch = this->channels;
|
||||
const long drop = (long) std::floor(this->in_pos) - this->half_taps;
|
||||
if (drop > 0) {
|
||||
const size_t drop_samples = (size_t) drop * ch;
|
||||
if (drop_samples <= this->in_queue.size()) {
|
||||
this->in_queue.erase(this->in_queue.begin(),
|
||||
this->in_queue.begin() + drop_samples);
|
||||
this->in_pos -= drop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UINT32 Resampler::produce_exact(UINT32 out_frames) {
|
||||
const int ch = this->channels;
|
||||
this->out_float.clear();
|
||||
if (ch <= 0 || out_frames == 0) {
|
||||
return 0;
|
||||
}
|
||||
this->out_float.reserve((size_t) out_frames * ch);
|
||||
|
||||
// resample ratio. drive it from the buffer size actually advertised to the game rather
|
||||
// than the nominal src/dst ratio: GetBufferSize reports floor(dev_buf * src/dst) game
|
||||
// frames, so the game only ever delivers that many input frames per device period.
|
||||
// consuming at the nominal ratio would eat slightly more input than arrives on any device
|
||||
// where dev_buf * src/dst is non-integer (e.g. 144 -> 132.3, floored to 132), slowly
|
||||
// draining the queue until it underruns to permanent silence. using the advertised integer
|
||||
// ratio keeps input and output exactly balanced; the resulting pitch error is below 0.3%
|
||||
// and inaudible, and it collapses to the exact ratio when the division is integer (160 ->
|
||||
// 147 stays 147/160 = 44100/48000).
|
||||
const double step = (double) this->frames_device_to_game(this->device_buffer_frames)
|
||||
/ (double) this->device_buffer_frames;
|
||||
|
||||
// input frames the block will touch: from in_pos through the right edge of the sinc kernel
|
||||
// at the final output sample. if the queue is short of this, the kernel tail reads past the
|
||||
// end and distorts every buffer, so buffer one extra block of input before the first output
|
||||
// (emitting silence without consuming) to build a cushion the kernel can always reach into.
|
||||
const long avail = (long) (this->in_queue.size() / ch);
|
||||
const long need = (long) std::ceil(this->in_pos + step * (double) out_frames)
|
||||
+ this->half_taps;
|
||||
|
||||
if (this->priming) {
|
||||
if (avail < need + (long) out_frames) {
|
||||
this->out_float.assign((size_t) out_frames * ch, 0.0f);
|
||||
return out_frames;
|
||||
}
|
||||
this->priming = false;
|
||||
}
|
||||
|
||||
for (UINT32 o = 0; o < out_frames; o++) {
|
||||
this->emit_frame();
|
||||
this->in_pos += step;
|
||||
}
|
||||
|
||||
this->drop_consumed();
|
||||
return out_frames;
|
||||
}
|
||||
|
||||
UINT32 Resampler::produce_variable() {
|
||||
const int ch = this->channels;
|
||||
if (ch <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// input frames consumed per output frame. timer-driven streams write variable partial
|
||||
// chunks, so produce however many output frames the currently queued input can fully
|
||||
// support and leave the rest for the next call; this keeps input and output balanced at
|
||||
// the exact src/dst ratio over time without depending on the device buffer size.
|
||||
const double step = (double) this->src_rate / (double) this->dst_rate;
|
||||
const long avail = (long) (this->in_queue.size() / ch);
|
||||
|
||||
// emit only while the sinc kernel's right edge stays within the queued input. the kernel
|
||||
// reaches from in_pos out to half_taps frames ahead, so stop once that would read past the
|
||||
// end; the remaining input becomes the next block's lookahead.
|
||||
UINT32 produced = 0;
|
||||
while ((long) std::ceil(this->in_pos) + this->half_taps < avail) {
|
||||
this->emit_frame();
|
||||
this->in_pos += step;
|
||||
produced++;
|
||||
}
|
||||
|
||||
this->drop_consumed();
|
||||
return produced;
|
||||
}
|
||||
|
||||
void Resampler::write_output(BYTE *dst, UINT32 frames, float gain) const {
|
||||
const int bps = this->bytes_per_sample;
|
||||
const int ch = this->channels;
|
||||
const size_t count = (size_t) frames * ch;
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
write_sample(dst + i * bps, bps, this->is_float, this->out_float[i] * gain);
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT Resampler::flush(IAudioRenderClient *real, IAudioClient *client, UINT32 frames,
|
||||
DWORD flags, float boost) {
|
||||
if (!this->enabled) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// cache the device buffer size once
|
||||
if (this->device_buffer_frames == 0) {
|
||||
client->GetBufferSize(&this->device_buffer_frames);
|
||||
}
|
||||
if (this->device_buffer_frames == 0) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
const bool silent = (flags & AUDCLNT_BUFFERFLAGS_SILENT) != 0;
|
||||
this->enqueue_input(frames, silent);
|
||||
|
||||
// confirm once that conversion actually started producing output
|
||||
static std::once_flag active_printed;
|
||||
std::call_once(active_printed, [this]() {
|
||||
log_info("audio::resample", "resample active: {} Hz -> {} Hz ({} ch, {})",
|
||||
this->src_rate, this->dst_rate, this->channels,
|
||||
this->event_driven ? "event-driven" : "timer-driven");
|
||||
});
|
||||
|
||||
// the boost is applied here (inside write_output) rather than in the standard ReleaseBuffer
|
||||
// path, so log it once for parity with that path's "volume boost active" line.
|
||||
if (boost != 1.0f) {
|
||||
static std::once_flag boost_printed;
|
||||
std::call_once(boost_printed, [boost]() {
|
||||
log_info("audio::resample", "volume boost active (resample): gain={}", boost);
|
||||
});
|
||||
}
|
||||
|
||||
return this->event_driven
|
||||
? this->flush_event(real, boost)
|
||||
: this->flush_timer(real, client, boost);
|
||||
}
|
||||
|
||||
HRESULT Resampler::flush_event(IAudioRenderClient *real, float boost) {
|
||||
|
||||
// event-driven exclusive streams must hand the device a full buffer every period and may
|
||||
// not push partial counts. resample the whole input block into exactly the device buffer
|
||||
// size.
|
||||
const UINT32 produced = this->produce_exact(this->device_buffer_frames);
|
||||
if (produced == 0) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
BYTE *dev = nullptr;
|
||||
HRESULT ret = real->GetBuffer(produced, &dev);
|
||||
if (FAILED(ret) || dev == nullptr) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// mute the first few buffers to avoid a pop on stream start
|
||||
float gain = boost;
|
||||
if (this->buffers_to_mute > 0) {
|
||||
gain = 0.0f;
|
||||
this->buffers_to_mute--;
|
||||
}
|
||||
|
||||
this->write_output(dev, produced, gain);
|
||||
|
||||
return real->ReleaseBuffer(produced, 0);
|
||||
}
|
||||
|
||||
HRESULT Resampler::flush_timer(IAudioRenderClient *real, IAudioClient *client, float boost) {
|
||||
|
||||
// convert everything currently queued into the pending output FIFO (out_float). timer-
|
||||
// driven games write variable partial chunks, so produce only what the queued input can
|
||||
// fully support and keep the remainder for the next call.
|
||||
this->produce_variable();
|
||||
|
||||
const int ch = this->channels;
|
||||
if (ch <= 0) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
const UINT32 pending = (UINT32) (this->out_float.size() / ch);
|
||||
if (pending == 0) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// push as many frames as the device currently has free, keeping the rest queued for the
|
||||
// next call. timer-driven games poll padding and write whenever there is room, so matching
|
||||
// the device's free space here avoids overflowing the ring while staying device-paced.
|
||||
UINT32 padding = 0;
|
||||
if (FAILED(client->GetCurrentPadding(&padding))) {
|
||||
return S_OK;
|
||||
}
|
||||
const UINT32 device_free = this->device_buffer_frames > padding
|
||||
? this->device_buffer_frames - padding
|
||||
: 0;
|
||||
if (device_free == 0) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
const UINT32 to_write = std::min(pending, device_free);
|
||||
|
||||
BYTE *dev = nullptr;
|
||||
HRESULT ret = real->GetBuffer(to_write, &dev);
|
||||
if (FAILED(ret) || dev == nullptr) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// mute the first few buffers to avoid a pop on stream start
|
||||
float gain = boost;
|
||||
if (this->buffers_to_mute > 0) {
|
||||
gain = 0.0f;
|
||||
this->buffers_to_mute--;
|
||||
}
|
||||
|
||||
this->write_output(dev, to_write, gain);
|
||||
ret = real->ReleaseBuffer(to_write, 0);
|
||||
|
||||
// drop the frames just written from the front of the pending FIFO
|
||||
this->out_float.erase(this->out_float.begin(),
|
||||
this->out_float.begin() + (size_t) to_write * ch);
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
#include <mmreg.h>
|
||||
#include <audioclient.h>
|
||||
|
||||
#include "hooks/audio/audio.h"
|
||||
|
||||
struct IAudioClient;
|
||||
struct IAudioRenderClient;
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
// Streaming sample-rate converter for the WASAPI render path. The real device is opened at the
|
||||
// target rate while the game keeps writing its native-rate audio into a scratch buffer; on
|
||||
// release that buffer is converted with a windowed-sinc kernel and pushed to the device.
|
||||
// Channel count and sample format are preserved; only the sample rate changes.
|
||||
//
|
||||
// Frame counts differ between the two rates, so unlike the per-frame downmix this is stateful:
|
||||
// a fractional read position and a window of input history carry across ReleaseBuffer calls,
|
||||
// and the device buffer is only filled up to the space the device currently has free.
|
||||
struct Resampler {
|
||||
|
||||
// whether the resampler is active for the current stream
|
||||
bool enabled = false;
|
||||
|
||||
// whether the stream is event-driven (AUDCLNT_STREAMFLAGS_EVENTCALLBACK). timer-driven
|
||||
// streams instead poll padding and write variable partial chunks, so they drain the
|
||||
// pending output to the device's free space rather than pushing a full buffer per period.
|
||||
bool event_driven = true;
|
||||
|
||||
// decide whether the stream should be resampled and to which rate. returns the target rate
|
||||
// when RESAMPLE_RATE is set and differs from the game's rate, otherwise nullopt.
|
||||
static std::optional<uint32_t> resolve(const WAVEFORMATEX *game_format);
|
||||
|
||||
// enable resampling for game_format and fill device_out with the equivalent format at the
|
||||
// target rate to open the real device with.
|
||||
void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *device_out,
|
||||
uint32_t target_rate);
|
||||
|
||||
// build the device format equivalent to game_format at target_rate (same channels/depth).
|
||||
static void make_device_format(const WAVEFORMATEX *game_format,
|
||||
WAVEFORMATEXTENSIBLE *device_out, uint32_t target_rate);
|
||||
|
||||
// initialize the real device at the target rate, performing the standard WASAPI buffer
|
||||
// realignment retry on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED.
|
||||
HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
|
||||
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
|
||||
const WAVEFORMATEX *device_format, LPCGUID session_guid);
|
||||
|
||||
// translate a device-rate frame count to the equivalent game-rate count, so the buffer-size
|
||||
// and padding values reported to the game stay paced at the game's native rate.
|
||||
UINT32 frames_device_to_game(UINT32 device_frames) const;
|
||||
UINT32 padding_device_to_game(UINT32 device_padding) const;
|
||||
|
||||
// hand the game a scratch buffer sized for `frames` of its native format to write into.
|
||||
HRESULT get_buffer(UINT32 frames, BYTE **ppData);
|
||||
|
||||
// pointer to the input scratch (sized by get_buffer). when chained after the downmix, the
|
||||
// downmix writes its stereo output here for the resampler to consume on the next flush.
|
||||
BYTE *input_data() { return this->scratch.data(); }
|
||||
|
||||
// convert the `frames` the game wrote and push output to the real render client. `boost`
|
||||
// is applied to the converted output. event-driven streams fill exactly one device buffer
|
||||
// per period; timer-driven streams push as many converted frames as the device has free.
|
||||
HRESULT flush(IAudioRenderClient *real, IAudioClient *client, UINT32 frames, DWORD flags,
|
||||
float boost);
|
||||
|
||||
private:
|
||||
|
||||
// append `frames` of the scratch buffer (native format), or silence, to the input queue
|
||||
void enqueue_input(UINT32 frames, bool silent);
|
||||
|
||||
// event-driven path: produce exactly one full device buffer and push it.
|
||||
HRESULT flush_event(IAudioRenderClient *real, float boost);
|
||||
|
||||
// timer-driven path: convert all queued input into the pending output FIFO, then push as
|
||||
// many frames as the device currently has free, keeping the remainder for the next call.
|
||||
HRESULT flush_timer(IAudioRenderClient *real, IAudioClient *client, float boost);
|
||||
|
||||
// produce exactly out_frames output frames using the fixed src/dst ratio. event-driven
|
||||
// exclusive streams must fill the whole device buffer every period; a small input cushion
|
||||
// is buffered first (see priming) so the sinc kernel always has lookahead.
|
||||
UINT32 produce_exact(UINT32 out_frames);
|
||||
|
||||
// convert all input the kernel can fully support into the pending output FIFO (out_float),
|
||||
// appending without clearing. returns the number of frames produced. used by the
|
||||
// timer-driven path where output is drained to the device in device-paced chunks.
|
||||
UINT32 produce_variable();
|
||||
|
||||
// convolve the windowed-sinc kernel at the current in_pos and append the resulting frame
|
||||
// (one sample per channel) to out_float
|
||||
void emit_frame();
|
||||
|
||||
// precompute the windowed-sinc kernel sampled at kernel_phases sub-sample positions, so
|
||||
// emit_frame is a table lookup instead of recomputing sin/cos per tap (which is far too
|
||||
// expensive to run per sample on the audio callback thread and causes underrun crackle).
|
||||
void build_kernel();
|
||||
|
||||
// drop input frames that in_pos has advanced past, keeping a window of history for the
|
||||
// next block's left context
|
||||
void drop_consumed();
|
||||
|
||||
// convert the first `frames` of out_float to the device format, scaled by `gain`
|
||||
void write_output(BYTE *dst, UINT32 frames, float gain) const;
|
||||
|
||||
// sample format of the stream
|
||||
int channels = 0;
|
||||
int bytes_per_sample = 0;
|
||||
bool is_float = false;
|
||||
int game_frame_size = 0;
|
||||
|
||||
uint32_t src_rate = 0;
|
||||
uint32_t dst_rate = 0;
|
||||
|
||||
// sinc low-pass cutoff (1.0 when upsampling, dst/src when downsampling) and window radius
|
||||
double cutoff = 1.0;
|
||||
int half_taps = 16;
|
||||
|
||||
// precomputed kernel: (kernel_phases + 1) rows of 2*half_taps weights, indexed by the
|
||||
// fractional sample position (linearly interpolated between adjacent rows in emit_frame)
|
||||
std::vector<float> kernel_table;
|
||||
int kernel_phases = 1024;
|
||||
|
||||
// interleaved float input queue and the fractional read position within it (in frames)
|
||||
std::vector<float> in_queue;
|
||||
double in_pos = 0.0;
|
||||
|
||||
// emit silence until a full block of input lookahead has accumulated, so the sinc kernel
|
||||
// never reads past the end of the queue (which would distort the tail of every buffer)
|
||||
bool priming = true;
|
||||
|
||||
// interleaved float scratch for produced output
|
||||
std::vector<float> out_float;
|
||||
|
||||
// buffer the game writes its native-rate audio into between get_buffer / flush
|
||||
std::vector<BYTE> scratch;
|
||||
|
||||
// cached device buffer size (frames); a full buffer is produced every period
|
||||
UINT32 device_buffer_frames = 0;
|
||||
|
||||
// leading buffers to silence to avoid a pop on stream start
|
||||
int buffers_to_mute = 16;
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user