mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-02 06:40:42 -07:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -235,6 +235,13 @@ add_compile_definitions(
|
|||||||
_WIN32_IE=0x0400
|
_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
|
# acioemu log
|
||||||
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DACIOEMU_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/io.cpp
|
||||||
games/nost/poke.cpp
|
games/nost/poke.cpp
|
||||||
games/gitadora/gitadora.cpp
|
games/gitadora/gitadora.cpp
|
||||||
|
games/gitadora/asio.cpp
|
||||||
games/gitadora/io.cpp
|
games/gitadora/io.cpp
|
||||||
games/gitadora/handle.cpp
|
games/gitadora/handle.cpp
|
||||||
games/gitadora/j32d.cpp
|
games/gitadora/j32d.cpp
|
||||||
@@ -496,6 +504,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
hooks/audio/backends/mmdevice/device_enumerator.cpp
|
hooks/audio/backends/mmdevice/device_enumerator.cpp
|
||||||
hooks/audio/backends/wasapi/audio_client.cpp
|
hooks/audio/backends/wasapi/audio_client.cpp
|
||||||
hooks/audio/backends/wasapi/audio_render_client.cpp
|
hooks/audio/backends/wasapi/audio_render_client.cpp
|
||||||
|
hooks/audio/backends/wasapi/downmix.cpp
|
||||||
hooks/audio/backends/wasapi/dummy_audio_client.cpp
|
hooks/audio/backends/wasapi/dummy_audio_client.cpp
|
||||||
hooks/audio/backends/wasapi/dummy_audio_clock.cpp
|
hooks/audio/backends/wasapi/dummy_audio_clock.cpp
|
||||||
hooks/audio/backends/wasapi/dummy_audio_render_client.cpp
|
hooks/audio/backends/wasapi/dummy_audio_render_client.cpp
|
||||||
@@ -517,12 +526,19 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp
|
hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp
|
||||||
hooks/graphics/backends/d3d9/d3d9_swapchain.cpp
|
hooks/graphics/backends/d3d9/d3d9_swapchain.cpp
|
||||||
hooks/graphics/backends/d3d9/d3d9_texture.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_backend.cpp
|
||||||
hooks/input/dinput8/fake_device.cpp
|
hooks/input/dinput8/fake_device.cpp
|
||||||
hooks/input/dinput8/hook.cpp
|
hooks/input/dinput8/hook.cpp
|
||||||
hooks/lang.cpp
|
hooks/lang.cpp
|
||||||
hooks/libraryhook.cpp
|
hooks/libraryhook.cpp
|
||||||
hooks/networkhook.cpp
|
hooks/networkhook.cpp
|
||||||
|
hooks/icmphook_net.cpp
|
||||||
|
hooks/icmphook_iphlpapi.cpp
|
||||||
hooks/powrprof.cpp
|
hooks/powrprof.cpp
|
||||||
#hooks/rom.cpp
|
#hooks/rom.cpp
|
||||||
hooks/setupapihook.cpp
|
hooks/setupapihook.cpp
|
||||||
@@ -556,6 +572,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
|
|
||||||
# overlay
|
# overlay
|
||||||
overlay/overlay.cpp
|
overlay/overlay.cpp
|
||||||
|
overlay/notifications.cpp
|
||||||
overlay/window.cpp
|
overlay/window.cpp
|
||||||
overlay/imgui/extensions.cpp
|
overlay/imgui/extensions.cpp
|
||||||
overlay/imgui/impl_spice.cpp
|
overlay/imgui/impl_spice.cpp
|
||||||
@@ -607,6 +624,9 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
reader/structuredmessage.cpp
|
reader/structuredmessage.cpp
|
||||||
reader/crypt.cpp
|
reader/crypt.cpp
|
||||||
|
|
||||||
|
# sdk
|
||||||
|
sdk/sdk.cpp
|
||||||
|
|
||||||
# stubs
|
# stubs
|
||||||
stubs/stubs.cpp
|
stubs/stubs.cpp
|
||||||
|
|
||||||
@@ -633,6 +653,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
util/time.cpp
|
util/time.cpp
|
||||||
util/cpuutils.cpp
|
util/cpuutils.cpp
|
||||||
util/netutils.cpp
|
util/netutils.cpp
|
||||||
|
util/precise_timer.cpp
|
||||||
util/sysutils.cpp
|
util/sysutils.cpp
|
||||||
util/lz77.cpp
|
util/lz77.cpp
|
||||||
util/tapeled.cpp
|
util/tapeled.cpp
|
||||||
@@ -707,6 +728,10 @@ target_link_libraries(spicetools_spice64 PUBLIC winscard)
|
|||||||
set_target_properties(spicetools_spice64 PROPERTIES PREFIX "")
|
set_target_properties(spicetools_spice64 PROPERTIES PREFIX "")
|
||||||
set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64")
|
set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64")
|
||||||
target_compile_definitions(spicetools_spice64 PRIVATE SPICE64=1)
|
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)
|
IF(NOT MSVC)
|
||||||
set_target_properties(spicetools_spice64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
|
set_target_properties(spicetools_spice64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
|
||||||
@@ -726,6 +751,9 @@ set_target_properties(spicetools_spice64_linux PROPERTIES PREFIX "")
|
|||||||
set_target_properties(spicetools_spice64_linux PROPERTIES OUTPUT_NAME "spice64_linux")
|
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 SPICE64=1)
|
||||||
target_compile_definitions(spicetools_spice64_linux PRIVATE NO_SCARD=1 PRIVATE SPICE_LINUX=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)
|
IF(NOT MSVC)
|
||||||
set_target_properties(spicetools_spice64_linux PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
|
set_target_properties(spicetools_spice64_linux PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
|
||||||
@@ -738,7 +766,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)
|
set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc)
|
||||||
add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
|
add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||||
target_link_libraries(spicetools_cfg
|
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)
|
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
||||||
target_link_libraries(spicetools_cfg PUBLIC winscard)
|
target_link_libraries(spicetools_cfg PUBLIC winscard)
|
||||||
set_target_properties(spicetools_cfg PROPERTIES PREFIX "")
|
set_target_properties(spicetools_cfg PROPERTIES PREFIX "")
|
||||||
@@ -756,7 +784,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)
|
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})
|
add_executable(spicetools_cfg_linux WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||||
target_link_libraries(spicetools_cfg_linux
|
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)
|
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 PREFIX "")
|
||||||
set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux")
|
set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux")
|
||||||
@@ -860,6 +888,36 @@ if(NOT MSVC)
|
|||||||
set_target_properties(spicetools_stubs_cpusbxpkm PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32")
|
set_target_properties(spicetools_stubs_cpusbxpkm PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32")
|
||||||
endif()
|
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
|
# output directories
|
||||||
####################
|
####################
|
||||||
|
|
||||||
@@ -869,14 +927,14 @@ set_target_properties(spicetools_cfg spicetools_cfg_linux
|
|||||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools")
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools")
|
||||||
|
|
||||||
# output 32bit
|
# 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
|
PROPERTIES
|
||||||
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive32"
|
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive32"
|
||||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32"
|
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32"
|
||||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32")
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32")
|
||||||
|
|
||||||
# output 64bit
|
# 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
|
PROPERTIES
|
||||||
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive64"
|
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive64"
|
||||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64"
|
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
|
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.
|
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
|
## License
|
||||||
Unless otherwise noted, all files are licensed under the GPLv3.
|
Unless otherwise noted, all files are licensed under the GPLv3.
|
||||||
See the LICENSE file for the full license text.
|
See the LICENSE file for the full license text.
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
#include "mdxf.h"
|
#include "mdxf.h"
|
||||||
#include "mdxf_poll.h"
|
#include "mdxf_poll.h"
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "games/ddr/ddr.h"
|
#include "games/ddr/ddr.h"
|
||||||
#include "games/ddr/io.h"
|
#include "games/ddr/io.h"
|
||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
#include <mutex>
|
|
||||||
|
|
||||||
#define MDFX_DEBUG_VERBOSE 0
|
#define MDFX_DEBUG_VERBOSE 0
|
||||||
|
|
||||||
@@ -109,9 +111,10 @@ static void mdxf_thread_start() {
|
|||||||
MDXF_THREAD = std::thread([] {
|
MDXF_THREAD = std::thread([] {
|
||||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
|
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
|
||||||
|
|
||||||
|
timeutils::PreciseSleepTimer timer;
|
||||||
while (MDXF_THREAD_RUNNING.load(std::memory_order_acquire)) {
|
while (MDXF_THREAD_RUNNING.load(std::memory_order_acquire)) {
|
||||||
mdxf_poll(false);
|
mdxf_poll(false);
|
||||||
std::this_thread::sleep_for(THREAD_PERIOD);
|
timer.sleep(THREAD_PERIOD);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -512,4 +515,4 @@ acio::MDXFModule::~MDXFModule() {
|
|||||||
if (IS_THREAD_NEEDED) {
|
if (IS_THREAD_NEEDED) {
|
||||||
mdxf_thread_stop();
|
mdxf_thread_stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "util/circular_buffer.h"
|
#include "util/circular_buffer.h"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "games/sdvx/sdvx.h"
|
#include "games/sdvx/sdvx.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
|
||||||
using namespace acioemu;
|
using namespace acioemu;
|
||||||
@@ -46,11 +47,12 @@ ICCADevice::ICCADevice(bool flip_order, bool keypad_thread, uint8_t node_count)
|
|||||||
// keypad thread for faster polling
|
// keypad thread for faster polling
|
||||||
if (keypad_thread) {
|
if (keypad_thread) {
|
||||||
this->keypad_thread = new std::thread([this]() {
|
this->keypad_thread = new std::thread([this]() {
|
||||||
|
timeutils::PreciseSleepTimer timer;
|
||||||
while (this->cards) {
|
while (this->cards) {
|
||||||
for (int unit = 0; unit < this->node_count; unit++) {
|
for (int unit = 0; unit < this->node_count; unit++) {
|
||||||
this->update_keypad(unit, false);
|
this->update_keypad(unit, false);
|
||||||
}
|
}
|
||||||
Sleep(7);
|
timer.sleep(7);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
|
||||||
|
#include "overlay/notifications.h"
|
||||||
|
|
||||||
#include "module.h"
|
#include "module.h"
|
||||||
#include "modules/analogs.h"
|
#include "modules/analogs.h"
|
||||||
#include "modules/buttons.h"
|
#include "modules/buttons.h"
|
||||||
@@ -197,6 +199,9 @@ void Controller::connection_handler(api::ClientState client_state) {
|
|||||||
|
|
||||||
// log connection
|
// log connection
|
||||||
log_info("api", "client connected: {}", client_address_str);
|
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_m.lock();
|
||||||
client_states.emplace_back(&client_state);
|
client_states.emplace_back(&client_state);
|
||||||
client_states_m.unlock();
|
client_states_m.unlock();
|
||||||
@@ -277,6 +282,9 @@ void Controller::connection_handler(api::ClientState client_state) {
|
|||||||
|
|
||||||
// log disconnect
|
// log disconnect
|
||||||
log_info("api", "client disconnected: {}", client_address_str);
|
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_m.lock();
|
||||||
client_states.erase(std::remove(client_states.begin(), client_states.end(), &client_state));
|
client_states.erase(std::remove(client_states.begin(), client_states.end(), &client_state));
|
||||||
client_states_m.unlock();
|
client_states_m.unlock();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
#include "games/io.h"
|
#include "games/io.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
#include "acio/mdxf/mdxf_poll.h"
|
||||||
|
|
||||||
using namespace std::placeholders;
|
using namespace std::placeholders;
|
||||||
using namespace rapidjson;
|
using namespace rapidjson;
|
||||||
@@ -104,6 +105,7 @@ namespace api::modules {
|
|||||||
for (auto &analog : *this->analogs) {
|
for (auto &analog : *this->analogs) {
|
||||||
analog.override_enabled = false;
|
analog.override_enabled = false;
|
||||||
}
|
}
|
||||||
|
mdxf_poll(true);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -148,6 +150,7 @@ namespace api::modules {
|
|||||||
if (analog.getName() == name) {
|
if (analog.getName() == name) {
|
||||||
analog.override_state = CLAMP(state, 0.f, 1.f);
|
analog.override_state = CLAMP(state, 0.f, 1.f);
|
||||||
analog.override_enabled = true;
|
analog.override_enabled = true;
|
||||||
|
mdxf_poll(true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,6 +170,7 @@ namespace api::modules {
|
|||||||
for (auto &analog : *this->analogs) {
|
for (auto &analog : *this->analogs) {
|
||||||
if (analog.getName() == name) {
|
if (analog.getName() == name) {
|
||||||
analog.override_enabled = false;
|
analog.override_enabled = false;
|
||||||
|
mdxf_poll(true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
#include "games/io.h"
|
#include "games/io.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
#include "acio/mdxf/mdxf_poll.h"
|
||||||
|
|
||||||
using namespace std::placeholders;
|
using namespace std::placeholders;
|
||||||
using namespace rapidjson;
|
using namespace rapidjson;
|
||||||
@@ -107,6 +108,7 @@ namespace api::modules {
|
|||||||
for (auto &button : *this->buttons) {
|
for (auto &button : *this->buttons) {
|
||||||
button.override_enabled = false;
|
button.override_enabled = false;
|
||||||
}
|
}
|
||||||
|
mdxf_poll(true);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -153,6 +155,7 @@ namespace api::modules {
|
|||||||
GameAPI::Buttons::BUTTON_PRESSED : GameAPI::Buttons::BUTTON_NOT_PRESSED;
|
GameAPI::Buttons::BUTTON_PRESSED : GameAPI::Buttons::BUTTON_NOT_PRESSED;
|
||||||
button.override_velocity = CLAMP(state, 0.f, 1.f);
|
button.override_velocity = CLAMP(state, 0.f, 1.f);
|
||||||
button.override_enabled = true;
|
button.override_enabled = true;
|
||||||
|
mdxf_poll(true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,6 +175,7 @@ namespace api::modules {
|
|||||||
for (auto &button : *this->buttons) {
|
for (auto &button : *this->buttons) {
|
||||||
if (button.getName() == name) {
|
if (button.getName() == name) {
|
||||||
button.override_enabled = false;
|
button.override_enabled = false;
|
||||||
|
mdxf_poll(true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "external/rapidjson/document.h"
|
#include "external/rapidjson/document.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
|
|
||||||
using namespace std::placeholders;
|
using namespace std::placeholders;
|
||||||
using namespace rapidjson;
|
using namespace rapidjson;
|
||||||
@@ -58,6 +59,7 @@ namespace api::modules {
|
|||||||
// get params
|
// get params
|
||||||
auto keypad = req.params[0].GetUint();
|
auto keypad = req.params[0].GetUint();
|
||||||
auto input = std::string(req.params[1].GetString());
|
auto input = std::string(req.params[1].GetString());
|
||||||
|
timeutils::PreciseSleepTimer timer;
|
||||||
|
|
||||||
// process all chars
|
// process all chars
|
||||||
for (auto c : input) {
|
for (auto c : input) {
|
||||||
@@ -91,11 +93,11 @@ namespace api::modules {
|
|||||||
|
|
||||||
// set
|
// set
|
||||||
eamuse_set_keypad_overrides(keypad, state);
|
eamuse_set_keypad_overrides(keypad, state);
|
||||||
Sleep(sleep_time);
|
timer.sleep(sleep_time);
|
||||||
|
|
||||||
// unset
|
// unset
|
||||||
eamuse_set_keypad_overrides(keypad, 0);
|
eamuse_set_keypad_overrides(keypad, 0);
|
||||||
Sleep(sleep_time);
|
timer.sleep(sleep_time);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ namespace api {
|
|||||||
controller->init_state(this->state);
|
controller->init_state(this->state);
|
||||||
this->thread = new std::thread([this] () {
|
this->thread = new std::thread([this] () {
|
||||||
log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud);
|
log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud);
|
||||||
|
timeutils::PreciseSleepTimer timer;
|
||||||
|
|
||||||
// read buffer
|
// read buffer
|
||||||
uint8_t read_buffer[16*1024];
|
uint8_t read_buffer[16*1024];
|
||||||
@@ -160,7 +162,7 @@ namespace api {
|
|||||||
|
|
||||||
// slow down on reconnect
|
// slow down on reconnect
|
||||||
if (this->running) {
|
if (this->running) {
|
||||||
Sleep(retry_time);
|
timer.sleep(retry_time);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
#include "util/rc4.h"
|
#include "util/rc4.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "overlay/notifications.h"
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
|
|
||||||
using namespace headsocket;
|
using namespace headsocket;
|
||||||
@@ -91,12 +92,18 @@ namespace api {
|
|||||||
|
|
||||||
// log connection
|
// log connection
|
||||||
log_info("api::websocket", "client connected");
|
log_info("api::websocket", "client connected");
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Success,
|
||||||
|
"API websocket client connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocketClient::on_disconnect() {
|
void WebSocketClient::on_disconnect() {
|
||||||
|
|
||||||
// log disconnection
|
// log disconnection
|
||||||
log_info("api::websocket", "client disconnected");
|
log_info("api::websocket", "client disconnected");
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Info,
|
||||||
|
"API websocket client disconnected");
|
||||||
|
|
||||||
// get pointer to server
|
// get pointer to server
|
||||||
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
|
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
|
||||||
|
|||||||
@@ -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"/>
|
<button name="VEFX" vkey="10" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
|
||||||
</buttons>
|
</buttons>
|
||||||
<analogs>
|
<analogs>
|
||||||
<analog name="Turntable P1" devid="Player 1" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
|
<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"/>
|
<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>
|
</analogs>
|
||||||
<lights>
|
<lights>
|
||||||
<light name="P1 1" devid="Player 1" index="0"/>
|
<light name="P1 1" devid="Player 1" index="0"/>
|
||||||
@@ -117,8 +117,8 @@
|
|||||||
<keypad_buttons/>
|
<keypad_buttons/>
|
||||||
<!-- Assumes analog mode. Need to invert direction since this FW is designed for INF. -->
|
<!-- Assumes analog mode. Need to invert direction since this FW is designed for INF. -->
|
||||||
<analogs>
|
<analogs>
|
||||||
<analog name="Turntable P1" devid="arcin (1p)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="true" smoothing="false" multiplier="1"/>
|
<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"/>
|
<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>
|
</analogs>
|
||||||
<lights>
|
<lights>
|
||||||
<light name="P1 1" devid="arcin (1p)" index="0"/>
|
<light name="P1 1" devid="arcin (1p)" index="0"/>
|
||||||
@@ -144,34 +144,34 @@
|
|||||||
<buttons/>
|
<buttons/>
|
||||||
<keypad_buttons/>
|
<keypad_buttons/>
|
||||||
<analogs>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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>
|
</analogs>
|
||||||
<lights>
|
<lights>
|
||||||
<light name="Key 1 R" devid="Lights 01-14" index="0"/>
|
<light name="Key 1 R" devid="Lights 01-14" index="0"/>
|
||||||
@@ -277,9 +277,9 @@
|
|||||||
</buttons>
|
</buttons>
|
||||||
<keypad_buttons/>
|
<keypad_buttons/>
|
||||||
<analogs>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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>
|
</analogs>
|
||||||
<lights/>
|
<lights/>
|
||||||
<!-- Gen 1 FAUCETWO in HID mode; should also work for later gens -->
|
<!-- Gen 1 FAUCETWO in HID mode; should also work for later gens -->
|
||||||
@@ -296,8 +296,8 @@
|
|||||||
</buttons>
|
</buttons>
|
||||||
<keypad_buttons/>
|
<keypad_buttons/>
|
||||||
<analogs>
|
<analogs>
|
||||||
<analog name="VOL-L" devid="Buttons (MI_01)" index="1" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
|
<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"/>
|
<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>
|
</analogs>
|
||||||
<lights>
|
<lights>
|
||||||
<light name="BT-A" devid="Lights (MI_02)" index="24"/>
|
<light name="BT-A" devid="Lights (MI_02)" index="24"/>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||||
<security>
|
<security>
|
||||||
<requestedPrivileges>
|
<requestedPrivileges>
|
||||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
|
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||||
</requestedPrivileges>
|
</requestedPrivileges>
|
||||||
</security>
|
</security>
|
||||||
</trustInfo>
|
</trustInfo>
|
||||||
|
|||||||
+12
-16
@@ -68,8 +68,8 @@ DIST_FOLDER="./dist"
|
|||||||
DIST_NAME="spice2x-$(date +%y)-$(date +%m)-$(date +%d).zip"
|
DIST_NAME="spice2x-$(date +%y)-$(date +%m)-$(date +%d).zip"
|
||||||
DIST_NAME_EXTRAS="spice2x-$(date +%y)-$(date +%m)-$(date +%d)-full.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.'
|
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_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"
|
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_XP32="spicetools_cfg spicetools_spice"
|
||||||
TARGETS_XP64="spicetools_spice64"
|
TARGETS_XP64="spicetools_spice64"
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ time (
|
|||||||
fi
|
fi
|
||||||
mkdir -p ${BUILDDIR_WINXP_32}
|
mkdir -p ${BUILDDIR_WINXP_32}
|
||||||
pushd ${BUILDDIR_WINXP_32} > /dev/null
|
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
|
popd > /dev/null
|
||||||
|
|
||||||
# 64 bit Windows XP
|
# 64 bit Windows XP
|
||||||
@@ -185,7 +185,7 @@ time (
|
|||||||
fi
|
fi
|
||||||
mkdir -p ${BUILDDIR_WINXP_64}
|
mkdir -p ${BUILDDIR_WINXP_64}
|
||||||
pushd ${BUILDDIR_WINXP_64} > /dev/null
|
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
|
popd > /dev/null
|
||||||
else
|
else
|
||||||
echo ""
|
echo ""
|
||||||
@@ -269,6 +269,8 @@ rm -rf ${OUTDIR_EXTRAS}
|
|||||||
mkdir -p ${OUTDIR_EXTRAS}
|
mkdir -p ${OUTDIR_EXTRAS}
|
||||||
mkdir -p ${OUTDIR_EXTRAS}/largeaddressaware
|
mkdir -p ${OUTDIR_EXTRAS}/largeaddressaware
|
||||||
mkdir -p ${OUTDIR_EXTRAS}/linux
|
mkdir -p ${OUTDIR_EXTRAS}/linux
|
||||||
|
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/32
|
||||||
|
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/64
|
||||||
if ((BUILD_XP > 0))
|
if ((BUILD_XP > 0))
|
||||||
then
|
then
|
||||||
mkdir -p ${OUTDIR_EXTRAS}/winxp
|
mkdir -p ${OUTDIR_EXTRAS}/winxp
|
||||||
@@ -281,16 +283,8 @@ then
|
|||||||
cp ${BUILDDIR_32}/spicetools/spicecfg-pdb.pdb ${OUTDIR} 2>/dev/null
|
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.exe ${OUTDIR} 2>/dev/null
|
||||||
cp ${BUILDDIR_32}/spicetools/32/spice-pdb.pdb ${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.exe ${OUTDIR} 2>/dev/null
|
||||||
cp ${BUILDDIR_64}/spicetools/64/spice64-pdb.pdb ${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
|
else
|
||||||
# release files
|
# release files
|
||||||
cp ${BUILDDIR_32}/spicetools/spicecfg.exe ${OUTDIR} 2>/dev/null
|
cp ${BUILDDIR_32}/spicetools/spicecfg.exe ${OUTDIR} 2>/dev/null
|
||||||
@@ -298,16 +292,15 @@ else
|
|||||||
cp ${BUILDDIR_32}/spicetools/32/spice.exe ${OUTDIR} 2>/dev/null
|
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_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/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.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/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/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/nvcuda.dll ${OUTDIR}/stubs/64 2>/dev/null
|
||||||
cp ${BUILDDIR_64}/spicetools/64/nvcuvid.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/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))
|
if ((BUILD_XP > 0))
|
||||||
then
|
then
|
||||||
cp ${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe ${OUTDIR_EXTRAS}/winxp 2>/dev/null
|
cp ${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe ${OUTDIR_EXTRAS}/winxp 2>/dev/null
|
||||||
@@ -326,6 +319,9 @@ then
|
|||||||
echo "WARNING: Couldn't get git to create the archive. Is this a git repository?"
|
echo "WARNING: Couldn't get git to create the archive. Is this a git repository?"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# sdk headers
|
||||||
|
cp -r ./sdk/include ${OUTDIR_EXTRAS}/sdk
|
||||||
|
|
||||||
# copy resources
|
# copy resources
|
||||||
rm -rf ${OUTDIR_EXTRAS}/api
|
rm -rf ${OUTDIR_EXTRAS}/api
|
||||||
mkdir ${OUTDIR_EXTRAS}/api
|
mkdir ${OUTDIR_EXTRAS}/api
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
docker build --pull external/docker -t spicetools/deps
|
docker build --pull external/docker -t spicetools/deps
|
||||||
docker build --build-context gitroot=%cd%/../../.git . -t spicetools/spice
|
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 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\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\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
|
||||||
|
|||||||
@@ -237,7 +237,8 @@ float Analog::applyDeadzone(float raw_value) {
|
|||||||
deadzone = -deadzone;
|
deadzone = -deadzone;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getType() != GameAPI::Analogs::AnalogType::LinearPositive) {
|
// 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
|
// calculate values
|
||||||
const auto delta = value - 0.5f;
|
const auto delta = value - 0.5f;
|
||||||
@@ -290,4 +291,75 @@ float Analog::applyDeadzone(float raw_value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -34,6 +35,11 @@ struct AnalogMovingAverage {
|
|||||||
float cosine;
|
float cosine;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct AnalogDelayEntry {
|
||||||
|
double time_in_ms;
|
||||||
|
float value;
|
||||||
|
};
|
||||||
|
|
||||||
class Analog {
|
class Analog {
|
||||||
private:
|
private:
|
||||||
std::string name;
|
std::string name;
|
||||||
@@ -63,6 +69,15 @@ private:
|
|||||||
float divisor_previous_value = 0.5f;
|
float divisor_previous_value = 0.5f;
|
||||||
unsigned short divisor_region = 0;
|
unsigned short divisor_region = 0;
|
||||||
|
|
||||||
|
// relative input mode
|
||||||
|
float rel_mode_absolute_value = 0.5f;
|
||||||
|
float rel_mode_last_read_time_s = 0.f;
|
||||||
|
bool relative_mode = false;
|
||||||
|
|
||||||
|
// delay
|
||||||
|
uint32_t delay_ms = 0;
|
||||||
|
std::queue<AnalogDelayEntry> delayed_inputs;
|
||||||
|
|
||||||
float calculateAngularDifference(float old_rads, float new_rads);
|
float calculateAngularDifference(float old_rads, float new_rads);
|
||||||
float normalizeAngle(float rads);
|
float normalizeAngle(float rads);
|
||||||
float normalizeAnalogValue(float value);
|
float normalizeAnalogValue(float value);
|
||||||
@@ -83,6 +98,8 @@ public:
|
|||||||
float applyAngularSensitivity(float raw_rads);
|
float applyAngularSensitivity(float raw_rads);
|
||||||
float applyMultiplier(float raw_value);
|
float applyMultiplier(float raw_value);
|
||||||
float applyDeadzone(float raw_value);
|
float applyDeadzone(float raw_value);
|
||||||
|
float getRelativeModeValue(float raw_value);
|
||||||
|
float getDelayedValue(float raw_value);
|
||||||
|
|
||||||
inline bool isSet() {
|
inline bool isSet() {
|
||||||
if (this->override_enabled) {
|
if (this->override_enabled) {
|
||||||
@@ -98,7 +115,9 @@ public:
|
|||||||
smoothing = false;
|
smoothing = false;
|
||||||
deadzone_mirror = false;
|
deadzone_mirror = false;
|
||||||
setMultiplier(1);
|
setMultiplier(1);
|
||||||
|
setRelativeMode(false);
|
||||||
setLastState(0.5f);
|
setLastState(0.5f);
|
||||||
|
setDelayMs(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void clearBindings() {
|
inline void clearBindings() {
|
||||||
@@ -195,6 +214,19 @@ public:
|
|||||||
this->last_state = last_state;
|
this->last_state = last_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline bool isRelativeMode() const {
|
||||||
|
return this->relative_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void setRelativeMode(bool relative_mode) {
|
||||||
|
if (relative_mode) {
|
||||||
|
this->smoothing = false;
|
||||||
|
}
|
||||||
|
this->relative_mode = relative_mode;
|
||||||
|
this->rel_mode_absolute_value = 0.5f;
|
||||||
|
this->rel_mode_last_read_time_s = 0.f;
|
||||||
|
}
|
||||||
|
|
||||||
inline GameAPI::Analogs::AnalogType getType() const {
|
inline GameAPI::Analogs::AnalogType getType() const {
|
||||||
return this->type;
|
return this->type;
|
||||||
}
|
}
|
||||||
@@ -202,4 +234,12 @@ public:
|
|||||||
inline void setType(GameAPI::Analogs::AnalogType type) {
|
inline void setType(GameAPI::Analogs::AnalogType type) {
|
||||||
this->type = type;
|
this->type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline uint32_t getDelayMs() const {
|
||||||
|
return this->delay_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void setDelayMs(uint32_t delay_ms) {
|
||||||
|
this->delay_ms = delay_ms;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+34
-21
@@ -679,38 +679,45 @@ float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, rawinput::D
|
|||||||
}
|
}
|
||||||
|
|
||||||
// deadzone
|
// deadzone
|
||||||
// do not apply deadzone to circular analogs since it doesn't make sense
|
// do not apply deadzone to circular analogs since it doesn't make sense (except in relative mode)
|
||||||
if (analog.isDeadzoneSet() && analog.getType() != AnalogType::Circular) {
|
if (analog.isDeadzoneSet() &&
|
||||||
|
(analog.getType() != AnalogType::Circular || analog.isRelativeMode())) {
|
||||||
value = analog.applyDeadzone(value);
|
value = analog.applyDeadzone(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (analog.getType() == AnalogType::Circular) {
|
if (analog.getType() == AnalogType::Circular) {
|
||||||
// integer multiplier
|
|
||||||
value = analog.applyMultiplier(value);
|
|
||||||
|
|
||||||
// smoothing/sensitivity
|
if (analog.isRelativeMode()) {
|
||||||
if (analog.getSmoothing() || analog.isSensitivitySet()) {
|
value = analog.getRelativeModeValue(value);
|
||||||
float rads = value * (float) M_TAU;
|
|
||||||
|
|
||||||
// smoothing
|
} else {
|
||||||
if (analog.getSmoothing()) {
|
// integer multiplier
|
||||||
|
value = analog.applyMultiplier(value);
|
||||||
|
|
||||||
// preserve direction
|
// smoothing/sensitivity
|
||||||
if (rads >= M_TAU) {
|
if (analog.getSmoothing() || analog.isSensitivitySet()) {
|
||||||
rads -= 0.0001f;
|
float rads = value * (float) M_TAU;
|
||||||
|
|
||||||
|
// smoothing
|
||||||
|
if (analog.getSmoothing()) {
|
||||||
|
|
||||||
|
// preserve direction
|
||||||
|
if (rads >= M_TAU) {
|
||||||
|
rads -= 0.0001f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate angle
|
||||||
|
rads = analog.getSmoothedValue(rads);
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculate angle
|
// sensitivity
|
||||||
rads = analog.getSmoothedValue(rads);
|
if (analog.isSensitivitySet()) {
|
||||||
}
|
rads = analog.applyAngularSensitivity(rads);
|
||||||
|
}
|
||||||
|
|
||||||
// sensitivity
|
// apply to value
|
||||||
if (analog.isSensitivitySet()) {
|
value = rads * (float) M_1_TAU;
|
||||||
rads = analog.applyAngularSensitivity(rads);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply to value
|
|
||||||
value = rads * (float) M_1_TAU;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// sensitivity
|
// sensitivity
|
||||||
@@ -760,6 +767,12 @@ float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, rawinput::D
|
|||||||
value = std::clamp(value, 0.f, 1.f);
|
value = std::clamp(value, 0.f, 1.f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delay
|
||||||
|
if (analog.getDelayMs() > 0) {
|
||||||
|
value = analog.getDelayedValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case rawinput::MIDI: {
|
case rawinput::MIDI: {
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ bool Config::getStatus() {
|
|||||||
return this->status;
|
return this->status;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Config::addGame(Game &game) {
|
bool Config::addGame(Game &game, bool save) {
|
||||||
tinyxml2::XMLNode *rootNode = this->configFile.LastChild();
|
tinyxml2::XMLNode *rootNode = this->configFile.LastChild();
|
||||||
tinyxml2::XMLElement *gameNodes = rootNode->FirstChildElement("game");
|
tinyxml2::XMLElement *gameNodes = rootNode->FirstChildElement("game");
|
||||||
|
|
||||||
@@ -252,6 +252,8 @@ bool Config::addGame(Game &game) {
|
|||||||
bool invert = false;
|
bool invert = false;
|
||||||
bool smoothing = false;
|
bool smoothing = false;
|
||||||
int multiplier = 1;
|
int multiplier = 1;
|
||||||
|
bool relative_mode = false;
|
||||||
|
uint32_t delay = 0;
|
||||||
tinyxml2::XMLError err1 = gameAnalogNode->QueryIntAttribute("index", &index);
|
tinyxml2::XMLError err1 = gameAnalogNode->QueryIntAttribute("index", &index);
|
||||||
gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity);
|
gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity);
|
||||||
gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone);
|
gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone);
|
||||||
@@ -259,6 +261,8 @@ bool Config::addGame(Game &game) {
|
|||||||
gameAnalogNode->QueryBoolAttribute("invert", &invert);
|
gameAnalogNode->QueryBoolAttribute("invert", &invert);
|
||||||
gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing);
|
gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing);
|
||||||
gameAnalogNode->QueryIntAttribute("multiplier", &multiplier);
|
gameAnalogNode->QueryIntAttribute("multiplier", &multiplier);
|
||||||
|
gameAnalogNode->QueryBoolAttribute("relative", &relative_mode);
|
||||||
|
gameAnalogNode->QueryUnsignedAttribute("delay_ms", &delay);
|
||||||
const char *devid = gameAnalogNode->Attribute("devid");
|
const char *devid = gameAnalogNode->Attribute("devid");
|
||||||
|
|
||||||
if (err1 != tinyxml2::XMLError::XML_SUCCESS || !devid) {
|
if (err1 != tinyxml2::XMLError::XML_SUCCESS || !devid) {
|
||||||
@@ -273,6 +277,8 @@ bool Config::addGame(Game &game) {
|
|||||||
gameAnalogNode->SetAttribute("invert", it.getInvert());
|
gameAnalogNode->SetAttribute("invert", it.getInvert());
|
||||||
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
|
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
|
||||||
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
|
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
|
||||||
|
gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
|
||||||
|
gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
|
||||||
gameAnalogsNode->InsertEndChild(gameAnalogNode);
|
gameAnalogsNode->InsertEndChild(gameAnalogNode);
|
||||||
} else {
|
} else {
|
||||||
it.setIndex(static_cast<unsigned short int>(index));
|
it.setIndex(static_cast<unsigned short int>(index));
|
||||||
@@ -283,6 +289,8 @@ bool Config::addGame(Game &game) {
|
|||||||
it.setInvert(invert);
|
it.setInvert(invert);
|
||||||
it.setSmoothing(smoothing);
|
it.setSmoothing(smoothing);
|
||||||
it.setMultiplier(multiplier);
|
it.setMultiplier(multiplier);
|
||||||
|
it.setRelativeMode(relative_mode);
|
||||||
|
it.setDelayMs(delay);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
gameAnalogNode = this->configFile.NewElement("analog");
|
gameAnalogNode = this->configFile.NewElement("analog");
|
||||||
@@ -294,6 +302,8 @@ bool Config::addGame(Game &game) {
|
|||||||
gameAnalogNode->SetAttribute("invert", it.getInvert());
|
gameAnalogNode->SetAttribute("invert", it.getInvert());
|
||||||
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
|
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
|
||||||
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
|
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
|
||||||
|
gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
|
||||||
|
gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
|
||||||
gameAnalogNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
|
gameAnalogNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
|
||||||
gameAnalogsNode->InsertEndChild(gameAnalogNode);
|
gameAnalogsNode->InsertEndChild(gameAnalogNode);
|
||||||
}
|
}
|
||||||
@@ -447,6 +457,8 @@ bool Config::addGame(Game &game) {
|
|||||||
gameAnalogNode->SetAttribute("invert", it.getInvert());
|
gameAnalogNode->SetAttribute("invert", it.getInvert());
|
||||||
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
|
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
|
||||||
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
|
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
|
||||||
|
gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
|
||||||
|
gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
|
||||||
gameAnalogsNode->InsertEndChild(gameAnalogNode);
|
gameAnalogsNode->InsertEndChild(gameAnalogNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,13 +486,20 @@ bool Config::addGame(Game &game) {
|
|||||||
rootNode->InsertEndChild(gameNode);
|
rootNode->InsertEndChild(gameNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// save config
|
// save config (skipped when caller batches multiple addGame calls
|
||||||
this->saveConfigFile();
|
// and flushes once at the end via Config::save())
|
||||||
|
if (save) {
|
||||||
|
this->saveConfigFile();
|
||||||
|
}
|
||||||
|
|
||||||
// return success
|
// return success
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Config::save() {
|
||||||
|
this->saveConfigFile();
|
||||||
|
}
|
||||||
|
|
||||||
bool Config::updateBinding(const Game &game, const Button &button, int alternative) {
|
bool Config::updateBinding(const Game &game, const Button &button, int alternative) {
|
||||||
|
|
||||||
// get root node
|
// get root node
|
||||||
@@ -679,6 +698,8 @@ bool Config::updateBinding(const Game &game, const Analog &analog) {
|
|||||||
gameAnalogNode->SetAttribute("invert", analog.getInvert());
|
gameAnalogNode->SetAttribute("invert", analog.getInvert());
|
||||||
gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing());
|
gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing());
|
||||||
gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier());
|
gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier());
|
||||||
|
gameAnalogNode->SetAttribute("relative", analog.isRelativeMode());
|
||||||
|
gameAnalogNode->SetAttribute("delay_ms", analog.getDelayMs());
|
||||||
gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str());
|
gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str());
|
||||||
} else {
|
} else {
|
||||||
gameAnalogNode = this->configFile.NewElement("analog");
|
gameAnalogNode = this->configFile.NewElement("analog");
|
||||||
@@ -689,6 +710,8 @@ bool Config::updateBinding(const Game &game, const Analog &analog) {
|
|||||||
gameAnalogNode->SetAttribute("invert", analog.getInvert());
|
gameAnalogNode->SetAttribute("invert", analog.getInvert());
|
||||||
gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing());
|
gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing());
|
||||||
gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier());
|
gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier());
|
||||||
|
gameAnalogNode->SetAttribute("relative", analog.isRelativeMode());
|
||||||
|
gameAnalogNode->SetAttribute("delay_ms", analog.getDelayMs());
|
||||||
gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str());
|
gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str());
|
||||||
gameAnalogsNode->InsertEndChild(gameAnalogNode);
|
gameAnalogsNode->InsertEndChild(gameAnalogNode);
|
||||||
}
|
}
|
||||||
@@ -1125,6 +1148,8 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
|
|||||||
bool invert = false;
|
bool invert = false;
|
||||||
bool smoothing = false;
|
bool smoothing = false;
|
||||||
int multiplier = 1;
|
int multiplier = 1;
|
||||||
|
bool relative_mode = false;
|
||||||
|
uint32_t delay = 0;
|
||||||
gameAnalogNode->QueryIntAttribute("index", &index);
|
gameAnalogNode->QueryIntAttribute("index", &index);
|
||||||
gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity);
|
gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity);
|
||||||
gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone);
|
gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone);
|
||||||
@@ -1132,6 +1157,8 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
|
|||||||
gameAnalogNode->QueryBoolAttribute("invert", &invert);
|
gameAnalogNode->QueryBoolAttribute("invert", &invert);
|
||||||
gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing);
|
gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing);
|
||||||
gameAnalogNode->QueryIntAttribute("multiplier", &multiplier);
|
gameAnalogNode->QueryIntAttribute("multiplier", &multiplier);
|
||||||
|
gameAnalogNode->QueryBoolAttribute("relative", &relative_mode);
|
||||||
|
gameAnalogNode->QueryUnsignedAttribute("delay_ms", &delay);
|
||||||
const char *devid = gameAnalogNode->Attribute("devid");
|
const char *devid = gameAnalogNode->Attribute("devid");
|
||||||
|
|
||||||
// create analog and add to list
|
// create analog and add to list
|
||||||
@@ -1143,6 +1170,8 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
|
|||||||
analog.setInvert(invert);
|
analog.setInvert(invert);
|
||||||
analog.setSmoothing(smoothing);
|
analog.setSmoothing(smoothing);
|
||||||
analog.setMultiplier(multiplier);
|
analog.setMultiplier(multiplier);
|
||||||
|
analog.setRelativeMode(relative_mode);
|
||||||
|
analog.setDelayMs(delay);
|
||||||
if (devid) {
|
if (devid) {
|
||||||
analog.setDeviceIdentifier(devid);
|
analog.setDeviceIdentifier(devid);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ public:
|
|||||||
bool getStatus();
|
bool getStatus();
|
||||||
bool createConfigFile();
|
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 Button &button, int alternative);
|
||||||
bool updateBinding(const Game &game, const Analog &analog);
|
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 "overlay/overlay.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
namespace cfg {
|
namespace cfg {
|
||||||
|
|
||||||
// globals
|
// globals
|
||||||
bool CONFIGURATOR_STANDALONE = false;
|
bool CONFIGURATOR_STANDALONE = false;
|
||||||
|
bool CONFIGURATOR_FORCE_SOFTWARE_RENDER = false;
|
||||||
ConfigType CONFIGURATOR_TYPE = ConfigType::Config;
|
ConfigType CONFIGURATOR_TYPE = ConfigType::Config;
|
||||||
|
|
||||||
Configurator::Configurator() {
|
Configurator::Configurator() {
|
||||||
@@ -16,11 +20,78 @@ namespace cfg {
|
|||||||
CONFIGURATOR_STANDALONE = false;
|
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() {
|
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::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->set_active(true);
|
||||||
overlay::OVERLAY->hotkeys_enable = false;
|
overlay::OVERLAY->hotkeys_enable = false;
|
||||||
ImGui::GetIO().MouseDrawCursor = false;
|
ImGui::GetIO().MouseDrawCursor = false;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace cfg {
|
|||||||
// globals
|
// globals
|
||||||
extern bool CONFIGURATOR_STANDALONE;
|
extern bool CONFIGURATOR_STANDALONE;
|
||||||
extern ConfigType CONFIGURATOR_TYPE;
|
extern ConfigType CONFIGURATOR_TYPE;
|
||||||
|
extern bool CONFIGURATOR_FORCE_SOFTWARE_RENDER;
|
||||||
|
|
||||||
class Configurator {
|
class Configurator {
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
#include "configurator_wnd.h"
|
#include "configurator_wnd.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
#include <windowsx.h>
|
||||||
|
|
||||||
#include "build/defs.h"
|
#include "build/defs.h"
|
||||||
|
#include "external/imgui/imgui.h"
|
||||||
#include "launcher/shutdown.h"
|
#include "launcher/shutdown.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
#include "cfg/configurator.h"
|
#include "cfg/configurator.h"
|
||||||
|
|
||||||
#include "icon.h"
|
#include "icon.h"
|
||||||
@@ -16,6 +22,156 @@ static int WINDOW_SIZE_X = 800;
|
|||||||
static int WINDOW_SIZE_Y = 600;
|
static int WINDOW_SIZE_Y = 600;
|
||||||
static HICON WINDOW_ICON = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(MAINICON));
|
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() {
|
cfg::ConfiguratorWindow::ConfiguratorWindow() {
|
||||||
|
|
||||||
// register the window class
|
// register the window class
|
||||||
@@ -27,12 +183,22 @@ cfg::ConfiguratorWindow::ConfiguratorWindow() {
|
|||||||
wc.hIcon = WINDOW_ICON;
|
wc.hIcon = WINDOW_ICON;
|
||||||
RegisterClass(&wc);
|
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
|
// determine window title
|
||||||
if (cfg::CONFIGURATOR_TYPE == cfg::ConfigType::Config) {
|
if (cfg::CONFIGURATOR_TYPE == cfg::ConfigType::Config) {
|
||||||
WINDOW_TITLE = "spice2x config (" + to_string(VERSION_STRING_CFG) + ")";
|
WINDOW_TITLE = "spice2x config (" + to_string(VERSION_STRING_CFG) + ")";
|
||||||
WINDOW_SIZE_X = 800;
|
WINDOW_SIZE_X = 800;
|
||||||
WINDOW_SIZE_Y = 600;
|
WINDOW_SIZE_Y = 600;
|
||||||
}
|
}
|
||||||
|
this->client_width = WINDOW_SIZE_X;
|
||||||
|
this->client_height = WINDOW_SIZE_Y;
|
||||||
|
|
||||||
// open window
|
// open window
|
||||||
this->hWnd = CreateWindowEx(
|
this->hWnd = CreateWindowEx(
|
||||||
@@ -53,11 +219,23 @@ cfg::ConfiguratorWindow::ConfiguratorWindow() {
|
|||||||
|
|
||||||
cfg::ConfiguratorWindow::~ConfiguratorWindow() {
|
cfg::ConfiguratorWindow::~ConfiguratorWindow() {
|
||||||
|
|
||||||
|
this->stop_timer();
|
||||||
|
|
||||||
// close window
|
// close window
|
||||||
DestroyWindow(this->hWnd);
|
DestroyWindow(this->hWnd);
|
||||||
|
|
||||||
// unregister class
|
// unregister class
|
||||||
UnregisterClass(CLASS_NAME, GetModuleHandle(NULL));
|
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() {
|
void cfg::ConfiguratorWindow::run() {
|
||||||
@@ -67,8 +245,23 @@ void cfg::ConfiguratorWindow::run() {
|
|||||||
ShowWindow(this->hWnd, SW_SHOWNORMAL);
|
ShowWindow(this->hWnd, SW_SHOWNORMAL);
|
||||||
UpdateWindow(this->hWnd);
|
UpdateWindow(this->hWnd);
|
||||||
|
|
||||||
// draw overlay in 60 FPS
|
// SW_SHOWNORMAL usually activates a top-level window, but not always (e.g.,
|
||||||
SetTimer(this->hWnd, 1, 1000 / 60, nullptr);
|
// 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
|
// window loop
|
||||||
BOOL ret;
|
BOOL ret;
|
||||||
@@ -102,6 +295,72 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
|
|||||||
|
|
||||||
break;
|
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_CLOSE:
|
||||||
case WM_DESTROY: {
|
case WM_DESTROY: {
|
||||||
|
|
||||||
@@ -110,62 +369,192 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case WM_TIMER: {
|
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) {
|
if (overlay::OVERLAY) {
|
||||||
overlay::OVERLAY->update();
|
overlay::OVERLAY->update();
|
||||||
overlay::OVERLAY->set_active(true);
|
overlay::OVERLAY->set_active(true);
|
||||||
overlay::OVERLAY->new_frame();
|
overlay::OVERLAY->new_frame();
|
||||||
overlay::OVERLAY->render();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// repaint window
|
if (use_d3d9) {
|
||||||
InvalidateRect(hWnd, nullptr, TRUE);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case WM_ERASEBKGND: {
|
case WM_ERASEBKGND: {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
case WM_PAINT: {
|
case WM_PAINT: {
|
||||||
|
paint_software(hWnd);
|
||||||
// render overlay
|
break;
|
||||||
if (overlay::OVERLAY) {
|
}
|
||||||
|
case WM_ACTIVATE: {
|
||||||
// get pixel data
|
const WORD activation = LOWORD(wParam);
|
||||||
int width, height;
|
if (activation == WA_INACTIVE) {
|
||||||
uint32_t *pixel_data = overlay::OVERLAY->sw_get_pixel_data(&width, &height);
|
// dropping any held mouse buttons so we don't get stuck in a
|
||||||
if (width > 0 && height > 0) {
|
// "button still pressed" state when we come back.
|
||||||
|
auto &io = ImGui::GetIO();
|
||||||
// create bitmap
|
io.AddMouseButtonEvent(ImGuiMouseButton_Left, false);
|
||||||
HBITMAP bitmap = CreateBitmap(width, height, 1, 8 * sizeof(uint32_t), pixel_data);
|
io.AddMouseButtonEvent(ImGuiMouseButton_Right, false);
|
||||||
|
io.AddMouseButtonEvent(ImGuiMouseButton_Middle, false);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
} else {
|
} 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;
|
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:
|
default:
|
||||||
return DefWindowProc(hWnd, uMsg, wParam, lParam);
|
return DefWindowProc(hWnd, uMsg, wParam, lParam);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
#include <d3d9.h>
|
||||||
|
|
||||||
namespace cfg {
|
namespace cfg {
|
||||||
|
|
||||||
@@ -9,11 +12,38 @@ namespace cfg {
|
|||||||
|
|
||||||
HWND hWnd;
|
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();
|
||||||
~ConfiguratorWindow();
|
~ConfiguratorWindow();
|
||||||
|
|
||||||
void run();
|
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);
|
static LRESULT CALLBACK window_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ namespace overlay::windows {
|
|||||||
el->SetAttribute("invert", analog.invert);
|
el->SetAttribute("invert", analog.invert);
|
||||||
el->SetAttribute("smoothing", analog.smoothing);
|
el->SetAttribute("smoothing", analog.smoothing);
|
||||||
el->SetAttribute("multiplier", analog.multiplier);
|
el->SetAttribute("multiplier", analog.multiplier);
|
||||||
|
el->SetAttribute("relative", analog.relative_mode);
|
||||||
|
el->SetAttribute("delay_ms", analog.delay_ms);
|
||||||
parent->InsertEndChild(el);
|
parent->InsertEndChild(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +92,8 @@ namespace overlay::windows {
|
|||||||
el->QueryBoolAttribute("invert", &a.invert);
|
el->QueryBoolAttribute("invert", &a.invert);
|
||||||
el->QueryBoolAttribute("smoothing", &a.smoothing);
|
el->QueryBoolAttribute("smoothing", &a.smoothing);
|
||||||
el->QueryIntAttribute("multiplier", &a.multiplier);
|
el->QueryIntAttribute("multiplier", &a.multiplier);
|
||||||
|
el->QueryBoolAttribute("relative", &a.relative_mode);
|
||||||
|
el->QueryUnsignedAttribute("delay_ms", &a.delay_ms);
|
||||||
|
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,10 @@
|
|||||||
</dependentAssembly>
|
</dependentAssembly>
|
||||||
</dependency>
|
</dependency>
|
||||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||||
<security>
|
<security>
|
||||||
<requestedPrivileges>
|
<requestedPrivileges>
|
||||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
|
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||||
</requestedPrivileges>
|
</requestedPrivileges>
|
||||||
</security>
|
</security>
|
||||||
</trustInfo>
|
</trustInfo>
|
||||||
</assembly>
|
</assembly>
|
||||||
Vendored
+1
@@ -20,6 +20,7 @@ Usage:
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|||||||
+7
@@ -21,5 +21,12 @@ set(IMGUI_SOURCES
|
|||||||
misc/cpp/imgui_stdlib.cpp
|
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})
|
add_library(imgui STATIC ${IMGUI_HEADERS} ${IMGUI_SOURCES})
|
||||||
target_include_directories(imgui PRIVATE ${PROJECT_SOURCE_DIR})
|
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
|
#define ROBIN_HOOD_VERSION_PATCH 2 // for backwards-compatible bug fixes
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
#include "games/io.h"
|
#include "games/io.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
#include "io.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::time_point<std::chrono::steady_clock> lastModified = std::chrono::steady_clock::now();
|
||||||
static std::chrono::milliseconds debounceDuration(100);
|
static std::chrono::milliseconds debounceDuration(100);
|
||||||
auto currentTime = std::chrono::steady_clock::now();
|
auto currentTime = std::chrono::steady_clock::now();
|
||||||
bool pressed = get_async_secondary_mouse() & 0x8000;
|
bool pressed = get_async_secondary_mouse();
|
||||||
bool focused = GetForegroundWindow() == hWnd;
|
bool focused = GetForegroundWindow() == hWnd;
|
||||||
|
|
||||||
if (focused && MOUSE_TRACKBALL_USE_TOGGLE && pressed && (currentTime - lastModified > debounceDuration)) {
|
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);
|
log_info("trackball", "thread start, use mouse: {}, toggle: {}", MOUSE_TRACKBALL, MOUSE_TRACKBALL_USE_TOGGLE);
|
||||||
|
|
||||||
tbThread = new std::thread([&] {
|
tbThread = new std::thread([&] {
|
||||||
|
timeutils::PreciseSleepTimer timer;
|
||||||
while (tbThreadRunning) {
|
while (tbThreadRunning) {
|
||||||
if (hWnd && wndProc) {
|
if (hWnd && wndProc) {
|
||||||
wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle);
|
wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle);
|
||||||
@@ -251,7 +253,7 @@ namespace games::ccj {
|
|||||||
if (!tbThreadRunning)
|
if (!tbThreadRunning)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
std::this_thread::sleep_for(10ms);
|
timer.sleep(10);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "cfg/api.h"
|
#include "cfg/api.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
|
||||||
#include "../ddr.h"
|
#include "../ddr.h"
|
||||||
@@ -459,7 +460,8 @@ int games::ddr::DDRP3IOHandle::device_io(
|
|||||||
if (nOutBufferSize >= 4) {
|
if (nOutBufferSize >= 4) {
|
||||||
|
|
||||||
// cool down
|
// cool down
|
||||||
Sleep(1);
|
static thread_local timeutils::PreciseSleepTimer timer;
|
||||||
|
timer.sleep(1);
|
||||||
|
|
||||||
// get controls as single variable (4 bytes)
|
// get controls as single variable (4 bytes)
|
||||||
auto &controls = *(uint32_t*) lpOutBuffer;
|
auto &controls = *(uint32_t*) lpOutBuffer;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
|
||||||
#include "../io.h"
|
#include "../io.h"
|
||||||
@@ -255,7 +256,8 @@ namespace games::ddr {
|
|||||||
case P4IO_IOCTL_GET_INPUTS: {
|
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
|
// 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);
|
memset(lpOutBuffer, 0, 16);
|
||||||
auto controls = (uint32_t*) lpOutBuffer;
|
auto controls = (uint32_t*) lpOutBuffer;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "games/game.h"
|
#include "games/game.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
#include "util/memutils.h"
|
#include "util/memutils.h"
|
||||||
#include "rgb_cam.h"
|
#include "rgb_cam.h"
|
||||||
|
|
||||||
@@ -221,10 +222,11 @@ namespace games::drs {
|
|||||||
void start_touch() {
|
void start_touch() {
|
||||||
std::thread t([] {
|
std::thread t([] {
|
||||||
log_info("drs", "starting touch input thread");
|
log_info("drs", "starting touch input thread");
|
||||||
|
timeutils::PreciseSleepTimer timer;
|
||||||
|
|
||||||
// main loop
|
// main loop
|
||||||
while (TRUE) {
|
while (TRUE) {
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
timer.sleep(1);
|
||||||
|
|
||||||
TOUCH_EVENTS.clear();
|
TOUCH_EVENTS.clear();
|
||||||
touch_get_events(TOUCH_EVENTS);
|
touch_get_events(TOUCH_EVENTS);
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
#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(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)
|
||||||
|
{
|
||||||
|
// ASIO\XONAR redirect to ASIO\<configured>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// open of the ASIO root: hand back a sentinel
|
||||||
|
if (ASIO_DRIVER.has_value() &&
|
||||||
|
lpSubKey != nullptr &&
|
||||||
|
phkResult != nullptr &&
|
||||||
|
hKey == HKEY_LOCAL_MACHINE &&
|
||||||
|
_stricmp(lpSubKey, "software\\asio") == 0)
|
||||||
|
{
|
||||||
|
*phkResult = PARENT_ASIO_REG_HANDLE;
|
||||||
|
return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, &real_asio_reg_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, 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);
|
||||||
|
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,26 @@
|
|||||||
#include "gitadora.h"
|
#include "gitadora.h"
|
||||||
|
#include "asio.h"
|
||||||
#include "handle.h"
|
#include "handle.h"
|
||||||
#include "bi2x_hook.h"
|
#include "bi2x_hook.h"
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <ks.h>
|
||||||
|
#include <ksmedia.h>
|
||||||
|
|
||||||
#include "cfg/configurator.h"
|
#include "cfg/configurator.h"
|
||||||
#include "hooks/audio/mme.h"
|
#include "hooks/audio/mme.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
#include "util/cpuutils.h"
|
#include "util/cpuutils.h"
|
||||||
|
#include "util/deferlog.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/sigscan.h"
|
#include "util/sigscan.h"
|
||||||
#include "util/socd_cleaner.h"
|
#include "util/socd_cleaner.h"
|
||||||
|
#include "util/sysutils.h"
|
||||||
|
#include "util/utils.h"
|
||||||
#include "hooks/setupapihook.h"
|
#include "hooks/setupapihook.h"
|
||||||
|
|
||||||
namespace games::gitadora {
|
namespace games::gitadora {
|
||||||
@@ -20,11 +28,12 @@ namespace games::gitadora {
|
|||||||
// settings
|
// settings
|
||||||
bool TWOCHANNEL = false;
|
bool TWOCHANNEL = false;
|
||||||
std::optional<unsigned int> CAB_TYPE = std::nullopt;
|
std::optional<unsigned int> CAB_TYPE = std::nullopt;
|
||||||
bool ARENA_SINGLE_WINDOW = false;
|
|
||||||
bool P1_LEFTY = false;
|
bool P1_LEFTY = false;
|
||||||
bool P2_LEFTY = false;
|
bool P2_LEFTY = false;
|
||||||
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
||||||
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
|
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;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Prevent GitaDora from creating folders on F drive
|
* Prevent GitaDora from creating folders on F drive
|
||||||
@@ -221,13 +230,6 @@ namespace games::gitadora {
|
|||||||
}
|
}
|
||||||
#endif
|
#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
|
// for guitar wail SOCD cleaning
|
||||||
socd::ALGORITHM = socd::SocdAlgorithm::PreferRecent;
|
socd::ALGORITHM = socd::SocdAlgorithm::PreferRecent;
|
||||||
|
|
||||||
@@ -237,12 +239,334 @@ namespace games::gitadora {
|
|||||||
} else {
|
} else {
|
||||||
log_info("gitadora", "guitar pick SOCD algorithm: legacy");
|
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() {
|
void GitaDoraGame::attach() {
|
||||||
Game::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
|
// modules
|
||||||
HMODULE sharepj_module = libutils::try_module("libshare-pj.dll");
|
HMODULE sharepj_module = libutils::try_module("libshare-pj.dll");
|
||||||
HMODULE bmsd2_module = libutils::try_module("libbmsd2.dll");
|
HMODULE bmsd2_module = libutils::try_module("libbmsd2.dll");
|
||||||
@@ -294,39 +618,58 @@ namespace games::gitadora {
|
|||||||
detour::iat_try("GetDriveTypeA", GetDriveTypeA_hook, avs::game::DLL_INSTANCE);
|
detour::iat_try("GetDriveTypeA", GetDriveTypeA_hook, avs::game::DLL_INSTANCE);
|
||||||
detour::iat_try("CreateDirectoryA", CreateDirectoryA_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
|
// volume change prevention
|
||||||
hooks::audio::mme::init(avs::game::DLL_INSTANCE);
|
hooks::audio::mme::init(avs::game::DLL_INSTANCE);
|
||||||
|
|
||||||
// touch hook
|
// monitor/touch hooks (windowed or full screen)
|
||||||
if (ARENA_SINGLE_WINDOW) {
|
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
|
// enable touch hook for subscreen overlay
|
||||||
wintouchemu::FORCE = true;
|
wintouchemu::FORCE = true;
|
||||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||||
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
|
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
|
// window patch
|
||||||
if (GRAPHICS_WINDOWED && !replace_pattern(
|
if (!is_arena_model()) {
|
||||||
gdme_module,
|
HMODULE gdme_module = libutils::try_module("libgdme.dll");
|
||||||
"754185ED753D8B4118BF0000CB02",
|
if (GRAPHICS_WINDOWED && !replace_pattern(
|
||||||
"9090????9090??????????????12", 0, 0))
|
gdme_module,
|
||||||
{
|
"754185ED753D8B4118BF0000CB02",
|
||||||
log_warning("gitadora", "windowed mode failed");
|
"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
|
// two channel mod
|
||||||
if (TWOCHANNEL) {
|
if (TWOCHANNEL && !is_arena_model()) {
|
||||||
bmsd2_boot_orig = detour::iat_try("bmsd2_boot", bmsd2_boot_hook, bmsd_module);
|
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) ||
|
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");
|
log_warning("gitadora", "two channel mode failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,4 +677,18 @@ namespace games::gitadora {
|
|||||||
#endif
|
#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 <optional>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <mmreg.h>
|
||||||
|
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "games/game.h"
|
#include "games/game.h"
|
||||||
#include "util/socd_cleaner.h"
|
#include "util/socd_cleaner.h"
|
||||||
@@ -11,11 +14,12 @@ namespace games::gitadora {
|
|||||||
// settings
|
// settings
|
||||||
extern bool TWOCHANNEL;
|
extern bool TWOCHANNEL;
|
||||||
extern std::optional<unsigned int> CAB_TYPE;
|
extern std::optional<unsigned int> CAB_TYPE;
|
||||||
extern bool ARENA_SINGLE_WINDOW;
|
|
||||||
extern bool P1_LEFTY;
|
extern bool P1_LEFTY;
|
||||||
extern bool P2_LEFTY;
|
extern bool P2_LEFTY;
|
||||||
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
||||||
extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
|
extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
|
||||||
|
extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
|
||||||
|
extern std::optional<std::string> ASIO_DRIVER;
|
||||||
|
|
||||||
class GitaDoraGame : public games::Game {
|
class GitaDoraGame : public games::Game {
|
||||||
public:
|
public:
|
||||||
@@ -24,6 +28,8 @@ namespace games::gitadora {
|
|||||||
virtual void attach() override;
|
virtual void attach() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void fix_audio_channel_mask(WAVEFORMATEX *format);
|
||||||
|
|
||||||
static inline bool is_drum() {
|
static inline bool is_drum() {
|
||||||
return (
|
return (
|
||||||
avs::game::is_model({ "J32", "K32", "L32" }) ||
|
avs::game::is_model({ "J32", "K32", "L32" }) ||
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
|
||||||
#include "iidx.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 - otherwise the IO thread will go too hard on the CPU
|
||||||
Sleep(1);
|
static thread_local timeutils::PreciseSleepTimer timer;
|
||||||
|
timer.sleep(1);
|
||||||
|
|
||||||
// generate message
|
// generate message
|
||||||
auto msg = this->create_msg(msg_in, 0x2E);
|
auto msg = this->create_msg(msg_in, 0x2E);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
|
||||||
#include "iidx.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 - otherwise the IO thread will go too hard on the CPU
|
||||||
Sleep(1);
|
static thread_local timeutils::PreciseSleepTimer timer;
|
||||||
|
timer.sleep(1);
|
||||||
|
|
||||||
// generate message
|
// generate message
|
||||||
auto msg = this->create_msg(msg_in, 0x2E);
|
auto msg = this->create_msg(msg_in, 0x2E);
|
||||||
|
|||||||
@@ -783,42 +783,7 @@ namespace games::iidx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// return higher 8 bit
|
// return higher 8 bit
|
||||||
uint8_t result = (uint8_t) (ret_value >> 2);
|
return (uint8_t)(ret_value >> 2);
|
||||||
|
|
||||||
// delay
|
|
||||||
if ((player == 0 && TT_DELAY_P1 > 0) ||
|
|
||||||
(player == 1 && TT_DELAY_P2 > 0)) {
|
|
||||||
|
|
||||||
static std::queue<std::pair<double, uint8_t>> delay_queue[2];
|
|
||||||
auto &queue = delay_queue[player];
|
|
||||||
|
|
||||||
const auto max_delta_ms =
|
|
||||||
static_cast<double>((player == 0) ? TT_DELAY_P1 : TT_DELAY_P2);
|
|
||||||
|
|
||||||
// always push a new value
|
|
||||||
const auto now = get_performance_milliseconds();
|
|
||||||
queue.push(std::make_pair(now, result));
|
|
||||||
|
|
||||||
// drain the queue down to reasonable length to prevent unconstrained growth
|
|
||||||
// this would accommodate 1 second at ~1000Hz
|
|
||||||
// (in reality all three iidx I/O emulation runs well under 500Hz)
|
|
||||||
while (queue.size() > 1024) {
|
|
||||||
queue.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// pop until we find one that falls just under the time threshold
|
|
||||||
while (!queue.empty()) {
|
|
||||||
const auto delta_t = now - queue.front().first;
|
|
||||||
if (delta_t <= max_delta_ms) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
queue.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
result = queue.front().second;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned char get_slider(uint8_t slider) {
|
unsigned char get_slider(uint8_t slider) {
|
||||||
|
|||||||
@@ -31,8 +31,6 @@ namespace games::iidx {
|
|||||||
extern bool NATIVE_TOUCH;
|
extern bool NATIVE_TOUCH;
|
||||||
extern std::optional<std::string> SOUND_OUTPUT_DEVICE;
|
extern std::optional<std::string> SOUND_OUTPUT_DEVICE;
|
||||||
extern std::optional<std::string> ASIO_DRIVER;
|
extern std::optional<std::string> ASIO_DRIVER;
|
||||||
extern uint32_t TT_DELAY_P1;
|
|
||||||
extern uint32_t TT_DELAY_P2;
|
|
||||||
extern uint8_t DIGITAL_TT_SENS;
|
extern uint8_t DIGITAL_TT_SENS;
|
||||||
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
||||||
extern std::optional<std::string> SCREEN_MODE;
|
extern std::optional<std::string> SCREEN_MODE;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#if SPICE64 && !SPICE_XP
|
#if SPICE64 && !SPICE_XP
|
||||||
|
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
#include "mf_wrappers.h"
|
#include "mf_wrappers.h"
|
||||||
|
|
||||||
@@ -399,6 +400,7 @@ namespace games::iidx {
|
|||||||
void IIDXLocalCamera::CreateThread() {
|
void IIDXLocalCamera::CreateThread() {
|
||||||
// Create thread
|
// Create thread
|
||||||
m_drawThread = new std::thread([this]() {
|
m_drawThread = new std::thread([this]() {
|
||||||
|
timeutils::PreciseSleepTimer timer;
|
||||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
|
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
|
||||||
|
|
||||||
double accumulator = 0.0;
|
double accumulator = 0.0;
|
||||||
@@ -412,7 +414,7 @@ namespace games::iidx {
|
|||||||
accumulator -= 1.0;
|
accumulator -= 1.0;
|
||||||
floorFrameTimeMicroSec += 1;
|
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 "touch/touch.h"
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
|
|
||||||
#define POKE_NATIVE_TOUCH 0
|
#define POKE_NATIVE_TOUCH 0
|
||||||
|
|
||||||
@@ -200,6 +201,7 @@ namespace games::iidx::poke {
|
|||||||
// create new thread
|
// create new thread
|
||||||
THREAD_RUNNING = true;
|
THREAD_RUNNING = true;
|
||||||
THREAD = new std::thread([] {
|
THREAD = new std::thread([] {
|
||||||
|
timeutils::PreciseSleepTimer timer;
|
||||||
|
|
||||||
// log
|
// log
|
||||||
log_info("poke", "enabled");
|
log_info("poke", "enabled");
|
||||||
@@ -319,7 +321,7 @@ namespace games::iidx::poke {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// slow down
|
// slow down
|
||||||
Sleep(50);
|
timer.sleep(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|||||||
+84
-11
@@ -19,7 +19,7 @@
|
|||||||
namespace games::jb {
|
namespace games::jb {
|
||||||
|
|
||||||
// touch stuff
|
// touch stuff
|
||||||
bool TOUCH_LEGACY_BOX = false;
|
JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
|
||||||
static bool TOUCH_ENABLE = false;
|
static bool TOUCH_ENABLE = false;
|
||||||
static bool TOUCH_ATTACHED = false;
|
static bool TOUCH_ATTACHED = false;
|
||||||
static bool IS_PORTRAIT = true;
|
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.
|
// note that the IO code in device.cpp will correctly compensate for orientation, depending on the model.
|
||||||
TOUCH_POINTS.clear();
|
TOUCH_POINTS.clear();
|
||||||
touch_get_points(TOUCH_POINTS);
|
touch_get_points(TOUCH_POINTS);
|
||||||
if (TOUCH_LEGACY_BOX) {
|
if (TOUCH_ALGORITHM == Legacy) {
|
||||||
auto offset = IS_PORTRAIT ? 580 : 0;
|
auto offset = IS_PORTRAIT ? 580 : 0;
|
||||||
for (auto &tp : TOUCH_POINTS) {
|
for (auto &tp : TOUCH_POINTS) {
|
||||||
|
|
||||||
@@ -94,6 +94,17 @@ namespace games::jb {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (auto &tp : TOUCH_POINTS) {
|
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 x_relative = tp.x;
|
||||||
int y_relative = tp.y;
|
int y_relative = tp.y;
|
||||||
|
|
||||||
@@ -110,19 +121,66 @@ namespace games::jb {
|
|||||||
y_relative -= 8;
|
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;
|
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
|
// check if the gap was touched
|
||||||
if (x_hitbox > JB_BUTTON_SIZE || y_hitbox > JB_BUTTON_SIZE) {
|
if (TOUCH_ALGORITHM == AcAccurate) {
|
||||||
continue;
|
// 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
|
// set the corresponding state
|
||||||
@@ -193,6 +251,21 @@ namespace games::jb {
|
|||||||
// enable touch
|
// enable touch
|
||||||
TOUCH_ENABLE = true;
|
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
|
// enable debug logging of gftools
|
||||||
HMODULE gftools = libutils::try_module("gftools.dll");
|
HMODULE gftools = libutils::try_module("gftools.dll");
|
||||||
detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc(
|
detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc(
|
||||||
|
|||||||
@@ -4,8 +4,14 @@
|
|||||||
|
|
||||||
namespace games::jb {
|
namespace games::jb {
|
||||||
|
|
||||||
|
enum JubeatTouchAlgorithm {
|
||||||
|
Legacy,
|
||||||
|
Improved,
|
||||||
|
AcAccurate
|
||||||
|
};
|
||||||
|
|
||||||
// touch stuff
|
// touch stuff
|
||||||
extern bool TOUCH_LEGACY_BOX;
|
extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
|
||||||
extern bool TOUCH_STATE[16];
|
extern bool TOUCH_STATE[16];
|
||||||
void touch_update();
|
void touch_update();
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
|
|
||||||
namespace games::nost::poke {
|
namespace games::nost::poke {
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ namespace games::nost::poke {
|
|||||||
// create new thread
|
// create new thread
|
||||||
THREAD_RUNNING = true;
|
THREAD_RUNNING = true;
|
||||||
THREAD = new std::thread([] {
|
THREAD = new std::thread([] {
|
||||||
|
timeutils::PreciseSleepTimer timer;
|
||||||
const DWORD touch_id = (DWORD)(0xFFFFFFFE);
|
const DWORD touch_id = (DWORD)(0xFFFFFFFE);
|
||||||
|
|
||||||
const int swipe_anim_total_frames = 6;
|
const int swipe_anim_total_frames = 6;
|
||||||
@@ -178,7 +180,7 @@ namespace games::nost::poke {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// slow down
|
// slow down
|
||||||
Sleep(30);
|
timer.sleep(30);
|
||||||
}
|
}
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "touchpanel.h"
|
#include "touchpanel.h"
|
||||||
|
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
|
|
||||||
using namespace std::chrono_literals;
|
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) {
|
int games::onpara::TouchPanelHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
|
||||||
|
|
||||||
|
static thread_local timeutils::PreciseSleepTimer timer;
|
||||||
|
|
||||||
DWORD i;
|
DWORD i;
|
||||||
auto buffer = reinterpret_cast<uint8_t *>(lpBuffer);
|
auto buffer = reinterpret_cast<uint8_t *>(lpBuffer);
|
||||||
|
|
||||||
@@ -67,7 +71,7 @@ int games::onpara::TouchPanelHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesT
|
|||||||
enqueue_packet(report);
|
enqueue_packet(report);
|
||||||
|
|
||||||
// prevent cpu bullying
|
// prevent cpu bullying
|
||||||
std::this_thread::sleep_for(1ms);
|
timer.sleep(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy from output queue
|
// copy from output queue
|
||||||
@@ -133,4 +137,4 @@ bool games::onpara::TouchPanelHandle::close() {
|
|||||||
log_info("touchpanel", "Closed COM1 (Touch Panel)");
|
log_info("touchpanel", "Closed COM1 (Touch Panel)");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "util/sigscan.h"
|
||||||
|
|
||||||
#define OTOCA_DEBUG_VERBOSE 0
|
#define OTOCA_DEBUG_VERBOSE 0
|
||||||
#if OTOCA_DEBUG_VERBOSE
|
#if OTOCA_DEBUG_VERBOSE
|
||||||
@@ -116,6 +117,73 @@ namespace games::otoca {
|
|||||||
return ret;
|
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() {
|
void OtocaGame::attach() {
|
||||||
Game::attach();
|
Game::attach();
|
||||||
|
|
||||||
@@ -129,6 +197,12 @@ namespace games::otoca {
|
|||||||
p4io_hook();
|
p4io_hook();
|
||||||
games::shared::printer_attach();
|
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) {
|
if (BYPASS_CAMERA) {
|
||||||
libutils::try_library("libcamera.dll");
|
libutils::try_library("libcamera.dll");
|
||||||
const auto libcamera = "libcamera.dll";
|
const auto libcamera = "libcamera.dll";
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ namespace games::popn {
|
|||||||
} else if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME) {
|
} else if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME) {
|
||||||
const auto source = reinterpret_cast<DISPLAYCONFIG_SOURCE_DEVICE_NAME*>(requestPacket);
|
const auto source = reinterpret_cast<DISPLAYCONFIG_SOURCE_DEVICE_NAME*>(requestPacket);
|
||||||
// value must match WrappedIDirect3D9::GetAdapterIdentifier
|
// value must match WrappedIDirect3D9::GetAdapterIdentifier
|
||||||
wcscpy(source->viewGdiDeviceName, L"\\\\.\\DISPLAY_SPICE_FAKE");
|
wcscpy(source->viewGdiDeviceName, L"\\\\.\\DISPLAY_SPICE_FAKE_1");
|
||||||
} else {
|
} else {
|
||||||
log_fatal(
|
log_fatal(
|
||||||
"popn",
|
"popn",
|
||||||
|
|||||||
+27
-13
@@ -4,29 +4,34 @@
|
|||||||
#include "hooks/devicehook.h"
|
#include "hooks/devicehook.h"
|
||||||
#include "hooks/audio/backends/dsound/dsound_backend.h"
|
#include "hooks/audio/backends/dsound/dsound_backend.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
#include "cfg/configurator.h"
|
||||||
|
|
||||||
#include "touch.h"
|
#include "touch.h"
|
||||||
|
|
||||||
|
namespace games::rb {
|
||||||
|
uint16_t TOUCH_POLL_RATE = 0;
|
||||||
|
}
|
||||||
|
|
||||||
static decltype(SleepEx) *SleepEx_orig;
|
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 rate
|
||||||
* Increase touch poll from ~110 FPS to ~500 FPS
|
|
||||||
*/
|
|
||||||
if (dwMilliseconds == 8) {
|
if (dwMilliseconds == 8) {
|
||||||
static bool initialized = false;
|
dwMilliseconds = 1000 / games::rb::TOUCH_POLL_RATE;
|
||||||
if (!initialized) {
|
}
|
||||||
initialized = true;
|
|
||||||
|
|
||||||
// if we only sleep for 1ms we also don't need the high priority RB sets
|
// most calls from rb are actually non-alertable
|
||||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
|
if (!bAlertable) {
|
||||||
}
|
static thread_local timeutils::PreciseSleepTimer timer;
|
||||||
dwMilliseconds = 1;
|
timer.sleep(dwMilliseconds);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// call original
|
// call original
|
||||||
return SleepEx_orig(dwMilliseconds, bAltertable);
|
return SleepEx_orig(dwMilliseconds, bAlertable);
|
||||||
}
|
}
|
||||||
|
|
||||||
games::rb::RBGame::RBGame() : Game("Reflec Beat") {
|
games::rb::RBGame::RBGame() : Game("Reflec Beat") {
|
||||||
@@ -35,6 +40,10 @@ games::rb::RBGame::RBGame() : Game("Reflec Beat") {
|
|||||||
void games::rb::RBGame::attach() {
|
void games::rb::RBGame::attach() {
|
||||||
Game::attach();
|
Game::attach();
|
||||||
|
|
||||||
|
if (1000 < games::rb::TOUCH_POLL_RATE) {
|
||||||
|
log_fatal("rb", "-rbtouchsize is set too high; cannot exceed 1000");
|
||||||
|
}
|
||||||
|
|
||||||
// init stuff
|
// init stuff
|
||||||
devicehook_init();
|
devicehook_init();
|
||||||
|
|
||||||
@@ -47,7 +56,12 @@ void games::rb::RBGame::attach() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// hooks
|
// 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() {
|
void games::rb::RBGame::detach() {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
namespace games::rb {
|
namespace games::rb {
|
||||||
|
|
||||||
extern uint16_t TOUCH_SCALING;
|
extern uint16_t TOUCH_SCALING;
|
||||||
|
extern uint8_t TOUCH_SIZE;
|
||||||
|
extern uint16_t TOUCH_POLL_RATE;
|
||||||
|
|
||||||
class RBGame : public games::Game {
|
class RBGame : public games::Game {
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ static std::string WINDOW_TITLE = "REFLEC BEAT";
|
|||||||
|
|
||||||
namespace games::rb {
|
namespace games::rb {
|
||||||
uint16_t TOUCH_SCALING = 1000;
|
uint16_t TOUCH_SCALING = 1000;
|
||||||
|
uint8_t TOUCH_SIZE = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) {
|
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_x = (int) ((x - window_width / 2.0) * 48.0 / 54.0 + window_width / 2.0);
|
||||||
const auto point_y = (int) (y - window_height / 76);
|
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, point_y);
|
||||||
grid_insert(data, point_x - offset_x, point_y);
|
|
||||||
grid_insert(data, point_x - offset_x, point_y - offset_y);
|
// surrounding points
|
||||||
grid_insert(data, point_x - offset_x, point_y + offset_y);
|
if (games::rb::TOUCH_SIZE == 3) {
|
||||||
grid_insert(data, point_x + offset_x, point_y);
|
grid_insert(data, point_x - offset_x, point_y); // west
|
||||||
grid_insert(data, point_x + offset_x, point_y + offset_y);
|
grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest
|
||||||
grid_insert(data, point_x + offset_x, point_y - offset_y);
|
grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest
|
||||||
grid_insert(data, point_x, point_y - offset_y);
|
grid_insert(data, point_x + offset_x, point_y); // east
|
||||||
grid_insert(data, point_x, point_y + offset_y);
|
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
|
// copy data to buffer
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "hooks/sleephook.h"
|
#include "hooks/sleephook.h"
|
||||||
#include "hooks/libraryhook.h"
|
#include "hooks/libraryhook.h"
|
||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
|
#include "overlay/notifications.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/fileutils.h"
|
#include "util/fileutils.h"
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
@@ -397,8 +398,14 @@ namespace games::shared {
|
|||||||
// logging
|
// logging
|
||||||
if (success) {
|
if (success) {
|
||||||
log_info("printer", "printer emulation has written an image to {}", image_path);
|
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 {
|
} else {
|
||||||
log_warning("printer", "printer emulation failed to write image to {}", image_path);
|
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)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ namespace hooks::audio {
|
|||||||
// public globals
|
// public globals
|
||||||
bool ENABLED = true;
|
bool ENABLED = true;
|
||||||
bool VOLUME_HOOK_ENABLED = true;
|
bool VOLUME_HOOK_ENABLED = true;
|
||||||
|
std::optional<DownmixAlgorithm> DOWNMIX_ALGORITHM = std::nullopt;
|
||||||
|
float VOLUME_BOOST = 1.0f;
|
||||||
bool USE_DUMMY = false;
|
bool USE_DUMMY = false;
|
||||||
WAVEFORMATEXTENSIBLE FORMAT {};
|
WAVEFORMATEXTENSIBLE FORMAT {};
|
||||||
std::optional<Backend> BACKEND = std::nullopt;
|
std::optional<Backend> BACKEND = std::nullopt;
|
||||||
|
|||||||
@@ -16,8 +16,19 @@ namespace hooks::audio {
|
|||||||
WaveOut,
|
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 ENABLED;
|
||||||
extern bool VOLUME_HOOK_ENABLED;
|
extern bool VOLUME_HOOK_ENABLED;
|
||||||
|
extern std::optional<DownmixAlgorithm> DOWNMIX_ALGORITHM;
|
||||||
|
extern float VOLUME_BOOST;
|
||||||
extern bool USE_DUMMY;
|
extern bool USE_DUMMY;
|
||||||
extern WAVEFORMATEXTENSIBLE FORMAT;
|
extern WAVEFORMATEXTENSIBLE FORMAT;
|
||||||
extern std::optional<Backend> BACKEND;
|
extern std::optional<Backend> BACKEND;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <ksmedia.h>
|
#include <ksmedia.h>
|
||||||
|
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
|
#include "games/gitadora/gitadora.h"
|
||||||
#include "hooks/audio/audio.h"
|
#include "hooks/audio/audio.h"
|
||||||
#include "hooks/audio/util.h"
|
#include "hooks/audio/util.h"
|
||||||
#include "hooks/audio/backends/wasapi/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;
|
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) {
|
IAudioClient *wrap_audio_client(IAudioClient *audio_client) {
|
||||||
log_misc("audio::wasapi", "wrapping IAudioClient");
|
log_misc("audio::wasapi", "wrapping IAudioClient");
|
||||||
|
|
||||||
@@ -145,13 +167,27 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
|
|||||||
fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat));
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
// verbose output
|
// verbose output
|
||||||
log_info("audio::wasapi", "IAudioClient::Initialize hook hit");
|
log_info("audio::wasapi", "IAudioClient::Initialize hook hit");
|
||||||
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(ShareMode));
|
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(ShareMode));
|
||||||
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(StreamFlags));
|
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(StreamFlags));
|
||||||
log_info("audio::wasapi", "... hnsBufferDuration : {}", hnsBufferDuration);
|
log_info("audio::wasapi", "... hnsBufferDuration : {}", hnsBufferDuration);
|
||||||
log_info("audio::wasapi", "... hnsPeriodicity : {}", hnsPeriodicity);
|
log_info("audio::wasapi", "... hnsPeriodicity : {}", hnsPeriodicity);
|
||||||
print_format(pFormat);
|
print_format(device_format);
|
||||||
|
|
||||||
if (this->backend) {
|
if (this->backend) {
|
||||||
SAFE_CALL("AudioBackend", "on_initialize", this->backend->on_initialize(
|
SAFE_CALL("AudioBackend", "on_initialize", this->backend->on_initialize(
|
||||||
@@ -173,17 +209,29 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
|
|||||||
// check for exclusive mode
|
// check for exclusive mode
|
||||||
if (ShareMode == AUDCLNT_SHAREMODE_EXCLUSIVE) {
|
if (ShareMode == AUDCLNT_SHAREMODE_EXCLUSIVE) {
|
||||||
this->exclusive_mode = true;
|
this->exclusive_mode = true;
|
||||||
this->frame_size = pFormat->nChannels * (pFormat->wBitsPerSample / 8);
|
this->frame_size = device_format->nChannels * (device_format->wBitsPerSample / 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
// call next
|
// call next
|
||||||
HRESULT ret = pReal->Initialize(
|
HRESULT ret;
|
||||||
ShareMode,
|
if (this->downmix.enabled) {
|
||||||
StreamFlags,
|
ret = this->downmix.initialize(
|
||||||
hnsBufferDuration,
|
pReal,
|
||||||
hnsPeriodicity,
|
ShareMode,
|
||||||
pFormat,
|
StreamFlags,
|
||||||
AudioSessionGuid);
|
hnsBufferDuration,
|
||||||
|
hnsPeriodicity,
|
||||||
|
device_format,
|
||||||
|
AudioSessionGuid);
|
||||||
|
} else {
|
||||||
|
ret = pReal->Initialize(
|
||||||
|
ShareMode,
|
||||||
|
StreamFlags,
|
||||||
|
hnsBufferDuration,
|
||||||
|
hnsPeriodicity,
|
||||||
|
device_format,
|
||||||
|
AudioSessionGuid);
|
||||||
|
}
|
||||||
|
|
||||||
// check for failure
|
// check for failure
|
||||||
if (FAILED(ret)) {
|
if (FAILED(ret)) {
|
||||||
@@ -192,7 +240,8 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
|
|||||||
}
|
}
|
||||||
|
|
||||||
log_info("audio::wasapi", "IAudioClient::Initialize success, hr={}", FMT_HRESULT(ret));
|
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;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -274,6 +323,18 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::IsFormatSupported(
|
|||||||
fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat));
|
fix_rec_format(const_cast<WAVEFORMATEX *>(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.
|
||||||
|
if (resolve_downmix(pFormat)) {
|
||||||
|
WAVEFORMATEXTENSIBLE stereo_storage = {};
|
||||||
|
hooks::audio::Downmix::make_stereo_format(pFormat, &stereo_storage);
|
||||||
|
const auto stereo_format = reinterpret_cast<const WAVEFORMATEX *>(&stereo_storage);
|
||||||
|
|
||||||
|
CHECK_RESULT(pReal->IsFormatSupported(ShareMode, stereo_format, ppClosestMatch));
|
||||||
|
} else if (games::gitadora::is_arena_model()) {
|
||||||
|
games::gitadora::fix_audio_channel_mask(const_cast<WAVEFORMATEX *>(pFormat));
|
||||||
|
}
|
||||||
|
|
||||||
if (this->backend) {
|
if (this->backend) {
|
||||||
HRESULT ret = this->backend->on_is_format_supported(&ShareMode, pFormat, ppClosestMatch);
|
HRESULT ret = this->backend->on_is_format_supported(&ShareMode, pFormat, ppClosestMatch);
|
||||||
|
|
||||||
@@ -466,5 +527,6 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::InitializeSharedAudioStream(
|
|||||||
|
|
||||||
log_info("audio::wasapi", "IAudioClient3::InitializeSharedAudioStream success, hr={}", FMT_HRESULT(ret));
|
log_info("audio::wasapi", "IAudioClient3::InitializeSharedAudioStream success, hr={}", FMT_HRESULT(ret));
|
||||||
copy_wave_format(&hooks::audio::FORMAT, pFormat);
|
copy_wave_format(&hooks::audio::FORMAT, pFormat);
|
||||||
|
copy_wave_format(&this->device_format, pFormat);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include "hooks/audio/audio_private.h"
|
#include "hooks/audio/audio_private.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
#include "downmix.h"
|
||||||
|
|
||||||
#include "audio_render_client.h"
|
#include "audio_render_client.h"
|
||||||
|
|
||||||
// {1FBC8530-AF3E-4128-B418-115DE72F76B6}
|
// {1FBC8530-AF3E-4128-B418-115DE72F76B6}
|
||||||
@@ -93,4 +95,12 @@ struct WrappedIAudioClient : IAudioClient3 {
|
|||||||
AudioBackend *const backend;
|
AudioBackend *const backend;
|
||||||
bool exclusive_mode = false;
|
bool exclusive_mode = false;
|
||||||
int frame_size = 0;
|
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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,72 @@
|
|||||||
#include "audio_render_client.h"
|
#include "audio_render_client.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
#include "audio_client.h"
|
#include "audio_client.h"
|
||||||
|
#include "hooks/audio/audio.h"
|
||||||
#include "wasapi_private.h"
|
#include "wasapi_private.h"
|
||||||
|
|
||||||
const char CLASS_NAME[] = "WrappedIAudioRenderClient";
|
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;
|
||||||
|
|
||||||
|
// KSDATAFORMAT_SUBTYPE_IEEE_FLOAT has Data1 == 3, _PCM has Data1 == 1
|
||||||
|
bool is_float = f.wFormatTag == WAVE_FORMAT_IEEE_FLOAT
|
||||||
|
|| (f.wFormatTag == WAVE_FORMAT_EXTENSIBLE && fmt.SubFormat.Data1 == 0x00000003);
|
||||||
|
|
||||||
|
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) {
|
HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::QueryInterface(REFIID riid, void **ppvObj) {
|
||||||
if (ppvObj == nullptr) {
|
if (ppvObj == nullptr) {
|
||||||
return E_POINTER;
|
return E_POINTER;
|
||||||
@@ -51,6 +113,12 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::GetBuffer(UINT32 NumFramesR
|
|||||||
return S_OK;
|
return S_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// surround downmix: reserve the real (stereo) device buffer, but hand the game a
|
||||||
|
// multi-channel scratch buffer that we downmix on release
|
||||||
|
if (this->client->downmix.enabled) {
|
||||||
|
CHECK_RESULT(this->client->downmix.get_buffer(pReal, NumFramesRequested, ppData));
|
||||||
|
}
|
||||||
|
|
||||||
// call original
|
// call original
|
||||||
HRESULT ret = pReal->GetBuffer(NumFramesRequested, ppData);
|
HRESULT ret = pReal->GetBuffer(NumFramesRequested, ppData);
|
||||||
|
|
||||||
@@ -75,14 +143,44 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::ReleaseBuffer(UINT32 NumFra
|
|||||||
return S_OK;
|
return S_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// fix for audio pop effect
|
// resolve the real device buffer for whichever path produced the audio
|
||||||
if (this->buffers_to_mute > 0 && this->client->frame_size > 0) {
|
BYTE *device_buffer;
|
||||||
|
if (this->client->downmix.enabled) {
|
||||||
|
|
||||||
// zero out = mute
|
// downmix the game's multi-channel scratch into the real stereo buffer held since GetBuffer
|
||||||
memset(this->audio_buffer, 0, NumFramesWritten * this->client->frame_size);
|
this->client->downmix.write_device_buffer(NumFramesWritten, dwFlags);
|
||||||
|
device_buffer = this->client->downmix.current_buffer();
|
||||||
|
} else {
|
||||||
|
device_buffer = this->audio_buffer;
|
||||||
|
|
||||||
this->buffers_to_mute--;
|
// 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--;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->ReleaseBuffer(NumFramesWritten, dwFlags));
|
// 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,332 @@
|
|||||||
|
#include "downmix.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include <audioclient.h>
|
||||||
|
#include <ks.h>
|
||||||
|
#include <ksmedia.h>
|
||||||
|
|
||||||
|
#include "util/logging.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// read one sample at `p` as a normalized float in [-1, 1]
|
||||||
|
static inline float read_sample(const BYTE *p, int bytes, bool is_float) {
|
||||||
|
if (is_float) {
|
||||||
|
float v;
|
||||||
|
memcpy(&v, p, sizeof(float));
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
switch (bytes) {
|
||||||
|
case 2: {
|
||||||
|
int16_t v;
|
||||||
|
memcpy(&v, p, sizeof(v));
|
||||||
|
return v * (1.0f / 32768.0f);
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
int32_t v = p[0] | (p[1] << 8) | (p[2] << 16);
|
||||||
|
if (v & 0x800000) {
|
||||||
|
v |= ~0xFFFFFF; // sign extend
|
||||||
|
}
|
||||||
|
return v * (1.0f / 8388608.0f);
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
|
int32_t v;
|
||||||
|
memcpy(&v, p, sizeof(v));
|
||||||
|
return (float) (v * (1.0 / 2147483648.0));
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the normalized float `value` to the sample at `p`, clamping to the format's range
|
||||||
|
static inline void write_sample(BYTE *p, int bytes, bool is_float, float value) {
|
||||||
|
if (is_float) {
|
||||||
|
float v = std::clamp(value, -1.0f, 1.0f);
|
||||||
|
memcpy(p, &v, sizeof(v));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (bytes) {
|
||||||
|
case 2: {
|
||||||
|
int16_t v = (int16_t) std::clamp(
|
||||||
|
(int) std::lround(value * 32768.0f), -32768, 32767);
|
||||||
|
memcpy(p, &v, sizeof(v));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
int32_t v = (int32_t) std::clamp<int64_t>(
|
||||||
|
std::llround((double) value * 8388608.0), -8388608, 8388607);
|
||||||
|
p[0] = v & 0xFF;
|
||||||
|
p[1] = (v >> 8) & 0xFF;
|
||||||
|
p[2] = (v >> 16) & 0xFF;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
|
int32_t v = (int32_t) std::clamp<int64_t>(
|
||||||
|
std::llround((double) value * 2147483648.0), INT32_MIN, INT32_MAX);
|
||||||
|
memcpy(p, &v, sizeof(v));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// KSDATAFORMAT_SUBTYPE_IEEE_FLOAT has Data1 == 3 (matches apply_gain detection)
|
||||||
|
this->is_float = game_format->wFormatTag == WAVE_FORMAT_IEEE_FLOAT
|
||||||
|
|| (game_format->wFormatTag == WAVE_FORMAT_EXTENSIBLE
|
||||||
|
&& reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(game_format)
|
||||||
|
->SubFormat.Data1 == 0x00000003);
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
|
||||||
|
HRESULT ret = real->Initialize(share_mode, stream_flags, buffer_duration, periodicity,
|
||||||
|
device_format, 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. recover by asking the device for the next
|
||||||
|
// aligned buffer size and re-initializing with a matching duration.
|
||||||
|
if (ret == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED) {
|
||||||
|
UINT32 aligned_frames = 0;
|
||||||
|
if (SUCCEEDED(real->GetBufferSize(&aligned_frames)) && aligned_frames > 0) {
|
||||||
|
REFERENCE_TIME aligned_duration = (REFERENCE_TIME)
|
||||||
|
(10000.0 * 1000 / device_format->nSamplesPerSec * aligned_frames + 0.5);
|
||||||
|
|
||||||
|
log_info("audio::downmix",
|
||||||
|
"buffer not aligned, retrying with {} frames ({} hns)",
|
||||||
|
aligned_frames, aligned_duration);
|
||||||
|
|
||||||
|
ret = real->Initialize(share_mode, stream_flags, aligned_duration,
|
||||||
|
periodicity != 0 ? aligned_duration : 0, device_format, session_guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,142 @@
|
|||||||
|
#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);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "hooks/audio/audio.h"
|
#include "hooks/audio/audio.h"
|
||||||
#include "hooks/audio/backends/wasapi/audio_client.h"
|
#include "hooks/audio/backends/wasapi/audio_client.h"
|
||||||
#include "hooks/audio/backends/wasapi/defs.h"
|
#include "hooks/audio/backends/wasapi/defs.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
|
|
||||||
static REFERENCE_TIME WASAPI_TARGET_REFTIME = TARGET_REFTIME;
|
static REFERENCE_TIME WASAPI_TARGET_REFTIME = TARGET_REFTIME;
|
||||||
|
|
||||||
@@ -185,6 +186,7 @@ HRESULT WaveOutBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppDa
|
|||||||
}
|
}
|
||||||
HRESULT WaveOutBackend::on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) {
|
HRESULT WaveOutBackend::on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) {
|
||||||
bool written = false;
|
bool written = false;
|
||||||
|
timeutils::PreciseSleepTimer timer;
|
||||||
|
|
||||||
// reset the dispatcher event
|
// reset the dispatcher event
|
||||||
ResetEvent(this->dispatcher_event);
|
ResetEvent(this->dispatcher_event);
|
||||||
@@ -209,7 +211,7 @@ HRESULT WaveOutBackend::on_release_buffer(uint32_t num_frames_written, DWORD dwF
|
|||||||
|
|
||||||
// avoid pegging the CPU
|
// avoid pegging the CPU
|
||||||
if (!written) {
|
if (!written) {
|
||||||
Sleep(1);
|
timer.sleep(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
// dx11 / dxgi hook entrypoint. trampolines d3d11.dll / dxgi.dll exports
|
||||||
|
// the moment those DLLs appear (LDR notification + poll-thread fallback),
|
||||||
|
// then drives proactive vtable capture so we don't lose the race against
|
||||||
|
// the execexe loader. per-vtable hook implementations live in the sibling
|
||||||
|
// files (d3d11_swapchain / d3d11_factory / d3d11_vtable_capture /
|
||||||
|
// d3d11_screenshot).
|
||||||
|
//
|
||||||
|
// note: never LoadLibrary d3d11/dxgi -- execexe pre-loads them itself and
|
||||||
|
// fails (error 0xa) if they're already in the loader's module list.
|
||||||
|
//
|
||||||
|
// 64-bit only.
|
||||||
|
|
||||||
|
#include "d3d11_backend.h"
|
||||||
|
|
||||||
|
#ifndef SPICE_D3D11
|
||||||
|
|
||||||
|
void graphics_d3d11_init() {}
|
||||||
|
void graphics_d3d11_shutdown() {}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <thread>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cwchar>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <d3d11.h>
|
||||||
|
#include <dxgi.h>
|
||||||
|
#include <dxgi1_2.h>
|
||||||
|
|
||||||
|
#include "d3d11_internal.h"
|
||||||
|
#include "util/nt_loader.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using D3D11CreateDeviceAndSwapChain_t = HRESULT(WINAPI *)(
|
||||||
|
IDXGIAdapter *, D3D_DRIVER_TYPE, HMODULE, UINT,
|
||||||
|
const D3D_FEATURE_LEVEL *, UINT, UINT,
|
||||||
|
const DXGI_SWAP_CHAIN_DESC *, IDXGISwapChain **,
|
||||||
|
ID3D11Device **, D3D_FEATURE_LEVEL *, ID3D11DeviceContext **);
|
||||||
|
using CreateDXGIFactory_t = HRESULT(WINAPI *)(REFIID, void **);
|
||||||
|
using CreateDXGIFactory1_t = HRESULT(WINAPI *)(REFIID, void **);
|
||||||
|
using CreateDXGIFactory2_t = HRESULT(WINAPI *)(UINT, REFIID, void **);
|
||||||
|
|
||||||
|
D3D11CreateDeviceAndSwapChain_t D3D11CreateDeviceAndSwapChain_orig = nullptr;
|
||||||
|
CreateDXGIFactory_t CreateDXGIFactory_orig = nullptr;
|
||||||
|
CreateDXGIFactory1_t CreateDXGIFactory1_orig = nullptr;
|
||||||
|
CreateDXGIFactory2_t CreateDXGIFactory2_orig = nullptr;
|
||||||
|
|
||||||
|
std::atomic<bool> g_d3d11_exports_hooked { false };
|
||||||
|
std::atomic<bool> g_dxgi_exports_hooked { false };
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// top-level export hooks
|
||||||
|
|
||||||
|
HRESULT WINAPI D3D11CreateDeviceAndSwapChain_hook(
|
||||||
|
IDXGIAdapter *pAdapter, D3D_DRIVER_TYPE DriverType, HMODULE Software, UINT Flags,
|
||||||
|
const D3D_FEATURE_LEVEL *pFeatureLevels, UINT FeatureLevels, UINT SDKVersion,
|
||||||
|
const DXGI_SWAP_CHAIN_DESC *pSwapChainDesc, IDXGISwapChain **ppSwapChain,
|
||||||
|
ID3D11Device **ppDevice, D3D_FEATURE_LEVEL *pFeatureLevel,
|
||||||
|
ID3D11DeviceContext **ppImmediateContext)
|
||||||
|
{
|
||||||
|
HRESULT res = D3D11CreateDeviceAndSwapChain_orig(
|
||||||
|
pAdapter, DriverType, Software, Flags,
|
||||||
|
pFeatureLevels, FeatureLevels, SDKVersion,
|
||||||
|
pSwapChainDesc, ppSwapChain, ppDevice, pFeatureLevel, ppImmediateContext);
|
||||||
|
if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) {
|
||||||
|
if (pSwapChainDesc) {
|
||||||
|
d3d11_hooks::note_main_hwnd(pSwapChainDesc->OutputWindow);
|
||||||
|
}
|
||||||
|
d3d11_hooks::install_swapchain_hooks(*ppSwapChain);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define DEFINE_FACTORY_HOOK(NAME, SIG_PARAMS, ORIG_ARGS) \
|
||||||
|
HRESULT WINAPI NAME##_hook SIG_PARAMS { \
|
||||||
|
HRESULT res = NAME##_orig ORIG_ARGS; \
|
||||||
|
if (SUCCEEDED(res) && ppFactory && *ppFactory) { \
|
||||||
|
d3d11_hooks::install_factory_hooks( \
|
||||||
|
reinterpret_cast<IUnknown *>(*ppFactory)); \
|
||||||
|
} \
|
||||||
|
return res; \
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINE_FACTORY_HOOK(CreateDXGIFactory,
|
||||||
|
(REFIID riid, void **ppFactory),
|
||||||
|
(riid, ppFactory))
|
||||||
|
DEFINE_FACTORY_HOOK(CreateDXGIFactory1,
|
||||||
|
(REFIID riid, void **ppFactory),
|
||||||
|
(riid, ppFactory))
|
||||||
|
DEFINE_FACTORY_HOOK(CreateDXGIFactory2,
|
||||||
|
(UINT Flags, REFIID riid, void **ppFactory),
|
||||||
|
(Flags, riid, ppFactory))
|
||||||
|
|
||||||
|
#undef DEFINE_FACTORY_HOOK
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// export trampoline plumbing
|
||||||
|
|
||||||
|
// serializes trampoline_export() so the LDR notification callback and the
|
||||||
|
// poll thread don't race each other into MinHook against the same target.
|
||||||
|
std::mutex g_export_mutex;
|
||||||
|
|
||||||
|
bool trampoline_export(const char *dll, const char *name, void *hook, void **orig) {
|
||||||
|
std::lock_guard<std::mutex> lock(g_export_mutex);
|
||||||
|
if (*orig) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
HMODULE mod = GetModuleHandleA(dll);
|
||||||
|
if (!mod) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
void *addr = reinterpret_cast<void *>(GetProcAddress(mod, name));
|
||||||
|
if (!addr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*orig = addr; // trampoline_try reads *orig before overwriting it.
|
||||||
|
if (!detour::trampoline_try(addr, hook, orig)) {
|
||||||
|
*orig = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log_info("graphics::d3d11", "trampolined {}!{}", dll, name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void try_install_d3d11_exports() {
|
||||||
|
if (g_d3d11_exports_hooked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (trampoline_export("d3d11.dll", "D3D11CreateDeviceAndSwapChain",
|
||||||
|
(void *) D3D11CreateDeviceAndSwapChain_hook,
|
||||||
|
(void **) &D3D11CreateDeviceAndSwapChain_orig)) {
|
||||||
|
g_d3d11_exports_hooked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void try_install_dxgi_exports() {
|
||||||
|
if (g_dxgi_exports_hooked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
struct entry { const char *name; void *hook; void **orig; };
|
||||||
|
const entry entries[] = {
|
||||||
|
{ "CreateDXGIFactory", (void *) CreateDXGIFactory_hook,
|
||||||
|
(void **) &CreateDXGIFactory_orig },
|
||||||
|
{ "CreateDXGIFactory1", (void *) CreateDXGIFactory1_hook,
|
||||||
|
(void **) &CreateDXGIFactory1_orig },
|
||||||
|
{ "CreateDXGIFactory2", (void *) CreateDXGIFactory2_hook,
|
||||||
|
(void **) &CreateDXGIFactory2_orig },
|
||||||
|
};
|
||||||
|
bool any = false;
|
||||||
|
for (auto &e : entries) {
|
||||||
|
any |= trampoline_export("dxgi.dll", e.name, e.hook, e.orig);
|
||||||
|
}
|
||||||
|
if (any) {
|
||||||
|
g_dxgi_exports_hooked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void try_capture_if_ready() {
|
||||||
|
if (g_d3d11_exports_hooked && g_dxgi_exports_hooked) {
|
||||||
|
d3d11_hooks::try_capture_vtables();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// LDR notification + polling fallback
|
||||||
|
|
||||||
|
bool dll_name_ends_with(PCUNICODE_STRING name, const wchar_t *suffix) {
|
||||||
|
if (!name || !name->Buffer) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const size_t n = name->Length / sizeof(WCHAR);
|
||||||
|
const size_t s = wcslen(suffix);
|
||||||
|
return n >= s && _wcsnicmp(name->Buffer + n - s, suffix, s) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
VOID CALLBACK ldr_dll_notification(
|
||||||
|
ULONG reason, PCLDR_DLL_NOTIFICATION_DATA data, PVOID /*context*/)
|
||||||
|
{
|
||||||
|
if (reason != LDR_DLL_NOTIFICATION_REASON_LOADED || !data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dll_name_ends_with(data->Loaded.BaseDllName, L"d3d11.dll")) {
|
||||||
|
try_install_d3d11_exports();
|
||||||
|
} else if (dll_name_ends_with(data->Loaded.BaseDllName, L"dxgi.dll")) {
|
||||||
|
try_install_dxgi_exports();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// execexe maps d3d11/dxgi via a path that bypasses LdrLoadDll, so the
|
||||||
|
// notification above never fires for those DLLs and we have to poll.
|
||||||
|
std::atomic<bool> g_stop { false };
|
||||||
|
std::thread g_poll_thread;
|
||||||
|
std::mutex g_init_mutex;
|
||||||
|
PVOID g_ldr_cookie = nullptr;
|
||||||
|
|
||||||
|
void poll_thread() {
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
for (int32_t i = 0; i < 120 && !g_stop.load(); ++i) {
|
||||||
|
try_install_d3d11_exports();
|
||||||
|
try_install_dxgi_exports();
|
||||||
|
if (g_d3d11_exports_hooked && g_dxgi_exports_hooked) {
|
||||||
|
d3d11_hooks::try_capture_vtables();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// sliced so shutdown doesn't have to wait a full second.
|
||||||
|
for (int32_t s = 0; s < 10 && !g_stop.load(); ++s) {
|
||||||
|
std::this_thread::sleep_for(100ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void graphics_d3d11_init() {
|
||||||
|
// dx11 titles always run under execexe. skipping on pure-dx9 games keeps
|
||||||
|
// their startup path completely untouched (no exports patched, no poll
|
||||||
|
// thread, no LDR callback).
|
||||||
|
if (!GetModuleHandleW(L"execexe.dll")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(g_init_mutex);
|
||||||
|
if (g_poll_thread.joinable()) {
|
||||||
|
return; // already initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info("graphics::d3d11", "initializing");
|
||||||
|
|
||||||
|
// trampoline now if either DLL is already in the PEB.
|
||||||
|
try_install_d3d11_exports();
|
||||||
|
try_install_dxgi_exports();
|
||||||
|
try_capture_if_ready();
|
||||||
|
|
||||||
|
// catches standard LdrLoadDll loads.
|
||||||
|
auto reg = reinterpret_cast<decltype(&LdrRegisterDllNotification)>(
|
||||||
|
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrRegisterDllNotification"));
|
||||||
|
if (reg) {
|
||||||
|
NTSTATUS st = reg(0, ldr_dll_notification, nullptr, &g_ldr_cookie);
|
||||||
|
if (NT_SUCCESS(st)) {
|
||||||
|
log_info("graphics::d3d11", "registered LDR DLL notification");
|
||||||
|
} else {
|
||||||
|
g_ldr_cookie = nullptr;
|
||||||
|
log_warning("graphics::d3d11",
|
||||||
|
"LdrRegisterDllNotification failed: {:#x}", (unsigned long)st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// catches the execexe loader path that bypasses LdrLoadDll.
|
||||||
|
g_poll_thread = std::thread(poll_thread);
|
||||||
|
}
|
||||||
|
|
||||||
|
void graphics_d3d11_shutdown() {
|
||||||
|
std::lock_guard<std::mutex> lock(g_init_mutex);
|
||||||
|
|
||||||
|
// unregister first so the callback can't fire mid-teardown.
|
||||||
|
if (g_ldr_cookie) {
|
||||||
|
auto unreg = reinterpret_cast<decltype(&LdrUnregisterDllNotification)>(
|
||||||
|
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrUnregisterDllNotification"));
|
||||||
|
if (unreg) {
|
||||||
|
unreg(g_ldr_cookie);
|
||||||
|
}
|
||||||
|
g_ldr_cookie = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_stop.store(true);
|
||||||
|
if (g_poll_thread.joinable()) {
|
||||||
|
g_poll_thread.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // SPICE_D3D11
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "overlay/overlay.h"
|
||||||
|
|
||||||
|
void graphics_d3d11_init();
|
||||||
|
void graphics_d3d11_shutdown();
|
||||||
|
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
|
||||||
|
struct ID3D11Device;
|
||||||
|
struct ID3D11DeviceContext;
|
||||||
|
struct ID3D11RenderTargetView;
|
||||||
|
struct IDXGISwapChain;
|
||||||
|
|
||||||
|
namespace overlay::d3d11 {
|
||||||
|
|
||||||
|
void render(ID3D11Device *device,
|
||||||
|
ID3D11DeviceContext *context,
|
||||||
|
IDXGISwapChain *swapchain,
|
||||||
|
ID3D11RenderTargetView **rtv);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
// dx11 factory vtable hooks. patches CreateSwapChain / CreateSwapChainForHwnd
|
||||||
|
// so we can install_swapchain_hooks against every newly-created swapchain.
|
||||||
|
|
||||||
|
#include "d3d11_backend.h"
|
||||||
|
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <d3d11.h>
|
||||||
|
#include <dxgi.h>
|
||||||
|
#include <dxgi1_2.h>
|
||||||
|
|
||||||
|
#include "d3d11_internal.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using CreateSwapChain_t = HRESULT(STDMETHODCALLTYPE *)(
|
||||||
|
IDXGIFactory *, IUnknown *, DXGI_SWAP_CHAIN_DESC *, IDXGISwapChain **);
|
||||||
|
using CreateSwapChainForHwnd_t = HRESULT(STDMETHODCALLTYPE *)(
|
||||||
|
IDXGIFactory2 *, IUnknown *, HWND,
|
||||||
|
const DXGI_SWAP_CHAIN_DESC1 *,
|
||||||
|
const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *,
|
||||||
|
IDXGIOutput *, IDXGISwapChain1 **);
|
||||||
|
|
||||||
|
CreateSwapChain_t CreateSwapChain_orig = nullptr;
|
||||||
|
CreateSwapChainForHwnd_t CreateSwapChainForHwnd_orig = nullptr;
|
||||||
|
|
||||||
|
bool g_factory_hooked = false;
|
||||||
|
bool g_factory2_hooked = false;
|
||||||
|
std::mutex g_hook_mutex;
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE CreateSwapChain_hook(
|
||||||
|
IDXGIFactory *factory, IUnknown *pDevice,
|
||||||
|
DXGI_SWAP_CHAIN_DESC *pDesc, IDXGISwapChain **ppSwapChain)
|
||||||
|
{
|
||||||
|
HRESULT res = CreateSwapChain_orig(factory, pDevice, pDesc, ppSwapChain);
|
||||||
|
if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) {
|
||||||
|
if (pDesc) {
|
||||||
|
d3d11_hooks::note_main_hwnd(pDesc->OutputWindow);
|
||||||
|
}
|
||||||
|
d3d11_hooks::install_swapchain_hooks(*ppSwapChain);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE CreateSwapChainForHwnd_hook(
|
||||||
|
IDXGIFactory2 *factory, IUnknown *pDevice, HWND hWnd,
|
||||||
|
const DXGI_SWAP_CHAIN_DESC1 *pDesc,
|
||||||
|
const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *pFullscreenDesc,
|
||||||
|
IDXGIOutput *pRestrictToOutput, IDXGISwapChain1 **ppSwapChain)
|
||||||
|
{
|
||||||
|
HRESULT res = CreateSwapChainForHwnd_orig(
|
||||||
|
factory, pDevice, hWnd, pDesc, pFullscreenDesc, pRestrictToOutput, ppSwapChain);
|
||||||
|
if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) {
|
||||||
|
d3d11_hooks::note_main_hwnd(hWnd);
|
||||||
|
d3d11_hooks::install_swapchain_hooks(*ppSwapChain);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// QI-and-hook helper: dedupes the IDXGIFactory / IDXGIFactory2 install paths.
|
||||||
|
template<typename Iface>
|
||||||
|
void install_on(IUnknown *factory, bool &flag,
|
||||||
|
size_t vtbl_index, void *hook, void **orig, const char *name)
|
||||||
|
{
|
||||||
|
if (flag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Iface *f = nullptr;
|
||||||
|
if (FAILED(factory->QueryInterface(IID_PPV_ARGS(&f))) || !f) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (d3d11_hooks::hook_vtbl(f, vtbl_index, hook, orig, name)) {
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
f->Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace d3d11_hooks {
|
||||||
|
|
||||||
|
void install_factory_hooks(IUnknown *factory) {
|
||||||
|
if (!factory) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::lock_guard<std::mutex> lock(g_hook_mutex);
|
||||||
|
|
||||||
|
install_on<IDXGIFactory>(factory, g_factory_hooked, 10,
|
||||||
|
(void *) CreateSwapChain_hook, (void **) &CreateSwapChain_orig,
|
||||||
|
"IDXGIFactory::CreateSwapChain");
|
||||||
|
|
||||||
|
install_on<IDXGIFactory2>(factory, g_factory2_hooked, 15,
|
||||||
|
(void *) CreateSwapChainForHwnd_hook, (void **) &CreateSwapChainForHwnd_orig,
|
||||||
|
"IDXGIFactory2::CreateSwapChainForHwnd");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // SPICE_D3D11
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// internal glue for the dx11 backend. all symbols gated on SPICE_D3D11.
|
||||||
|
|
||||||
|
#include "overlay/overlay.h"
|
||||||
|
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "util/detour.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
struct HWND__; typedef HWND__ *HWND;
|
||||||
|
struct IUnknown;
|
||||||
|
struct IDXGISwapChain;
|
||||||
|
|
||||||
|
namespace d3d11_hooks {
|
||||||
|
|
||||||
|
void install_swapchain_hooks(IDXGISwapChain *swapchain);
|
||||||
|
void install_factory_hooks(IUnknown *factory);
|
||||||
|
void try_capture_vtables();
|
||||||
|
|
||||||
|
// first non-null swapchain HWND wins; later ones (sub-screens, IME
|
||||||
|
// helpers) are ignored. the dummy capture window is exempted via
|
||||||
|
// ignore_hwnd.
|
||||||
|
void note_main_hwnd(HWND hwnd);
|
||||||
|
HWND main_hwnd();
|
||||||
|
void ignore_hwnd(HWND hwnd);
|
||||||
|
|
||||||
|
// capture backbuffer to PNG if a screenshot was requested.
|
||||||
|
void try_screenshot(IDXGISwapChain *swapchain);
|
||||||
|
|
||||||
|
// trampoline a virtual method by vtable index. on failure *orig is null.
|
||||||
|
inline bool hook_vtbl(void *iface, size_t index,
|
||||||
|
void *hook, void **orig, const char *name)
|
||||||
|
{
|
||||||
|
void **vtbl = *reinterpret_cast<void ***>(iface);
|
||||||
|
void *target = vtbl[index];
|
||||||
|
// trampoline_try reads *orig before overwriting it.
|
||||||
|
*orig = target;
|
||||||
|
if (!detour::trampoline_try(target, hook, orig)) {
|
||||||
|
*orig = nullptr;
|
||||||
|
log_warning("graphics::d3d11", "failed to hook {}", name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log_info("graphics::d3d11", "hooked {}", name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// minimal COM RAII used by capture / screenshot paths.
|
||||||
|
struct com_release {
|
||||||
|
void operator()(IUnknown *p) const { if (p) p->Release(); }
|
||||||
|
};
|
||||||
|
template<typename T> using com_ptr = std::unique_ptr<T, com_release>;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
// dx11 screenshot capture. mirrors the d3d9 backend: copy the current
|
||||||
|
// backbuffer into a staging texture, force alpha=255, write PNG via
|
||||||
|
// stb_image_write, push to clipboard and notify.
|
||||||
|
|
||||||
|
#include "d3d11_backend.h"
|
||||||
|
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <d3d11.h>
|
||||||
|
#include <dxgi.h>
|
||||||
|
|
||||||
|
#include "d3d11_internal.h"
|
||||||
|
|
||||||
|
#include "external/stb_image_write.h"
|
||||||
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "misc/clipboard.h"
|
||||||
|
#include "overlay/notifications.h"
|
||||||
|
#include "util/fileutils.h"
|
||||||
|
|
||||||
|
using d3d11_hooks::com_ptr;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// copy the swapchain backbuffer into a CPU-readable staging texture and
|
||||||
|
// flatten it into an RGBA8 buffer (BGRA backbuffers are swizzled,
|
||||||
|
// alpha is forced to 255).
|
||||||
|
bool copy_backbuffer_to_rgba(IDXGISwapChain *swapchain,
|
||||||
|
ID3D11Device *device,
|
||||||
|
ID3D11DeviceContext *context,
|
||||||
|
std::vector<uint8_t> &out,
|
||||||
|
uint32_t &out_w, uint32_t &out_h)
|
||||||
|
{
|
||||||
|
ID3D11Texture2D *raw_bb = nullptr;
|
||||||
|
if (FAILED(swapchain->GetBuffer(0, IID_PPV_ARGS(&raw_bb))) || !raw_bb) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
com_ptr<ID3D11Texture2D> backbuffer(raw_bb);
|
||||||
|
|
||||||
|
D3D11_TEXTURE2D_DESC desc {};
|
||||||
|
backbuffer->GetDesc(&desc);
|
||||||
|
|
||||||
|
// MSAA backbuffers can't be CopyResource'd into a non-MS staging target.
|
||||||
|
com_ptr<ID3D11Texture2D> resolved;
|
||||||
|
ID3D11Texture2D *source = backbuffer.get();
|
||||||
|
if (desc.SampleDesc.Count > 1) {
|
||||||
|
D3D11_TEXTURE2D_DESC rd = desc;
|
||||||
|
rd.SampleDesc.Count = 1;
|
||||||
|
rd.SampleDesc.Quality = 0;
|
||||||
|
rd.Usage = D3D11_USAGE_DEFAULT;
|
||||||
|
rd.BindFlags = D3D11_BIND_RENDER_TARGET;
|
||||||
|
rd.CPUAccessFlags = 0;
|
||||||
|
rd.MiscFlags = 0;
|
||||||
|
ID3D11Texture2D *r = nullptr;
|
||||||
|
if (FAILED(device->CreateTexture2D(&rd, nullptr, &r)) || !r) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
resolved.reset(r);
|
||||||
|
context->ResolveSubresource(resolved.get(), 0, backbuffer.get(), 0, desc.Format);
|
||||||
|
source = resolved.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
D3D11_TEXTURE2D_DESC sd {};
|
||||||
|
sd.Width = desc.Width;
|
||||||
|
sd.Height = desc.Height;
|
||||||
|
sd.MipLevels = 1;
|
||||||
|
sd.ArraySize = 1;
|
||||||
|
sd.Format = desc.Format;
|
||||||
|
sd.SampleDesc.Count = 1;
|
||||||
|
sd.Usage = D3D11_USAGE_STAGING;
|
||||||
|
sd.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
||||||
|
|
||||||
|
ID3D11Texture2D *raw_staging = nullptr;
|
||||||
|
if (FAILED(device->CreateTexture2D(&sd, nullptr, &raw_staging)) || !raw_staging) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
com_ptr<ID3D11Texture2D> staging(raw_staging);
|
||||||
|
context->CopyResource(staging.get(), source);
|
||||||
|
|
||||||
|
D3D11_MAPPED_SUBRESOURCE mapped {};
|
||||||
|
if (FAILED(context->Map(staging.get(), 0, D3D11_MAP_READ, 0, &mapped))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// backbuffers from GetDesc are always fully-typed (never _TYPELESS).
|
||||||
|
const bool is_bgra = desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM
|
||||||
|
|| desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM_SRGB;
|
||||||
|
|
||||||
|
out.resize(static_cast<size_t>(desc.Width) * desc.Height * 4);
|
||||||
|
const uint8_t *src_base = reinterpret_cast<const uint8_t *>(mapped.pData);
|
||||||
|
for (uint32_t y = 0; y < desc.Height; ++y) {
|
||||||
|
const uint8_t *row = src_base + static_cast<size_t>(y) * mapped.RowPitch;
|
||||||
|
uint8_t *dst = out.data() + static_cast<size_t>(y) * desc.Width * 4;
|
||||||
|
for (uint32_t x = 0; x < desc.Width; ++x) {
|
||||||
|
dst[x * 4 + 0] = row[x * 4 + (is_bgra ? 2 : 0)];
|
||||||
|
dst[x * 4 + 1] = row[x * 4 + 1];
|
||||||
|
dst[x * 4 + 2] = row[x * 4 + (is_bgra ? 0 : 2)];
|
||||||
|
dst[x * 4 + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context->Unmap(staging.get(), 0);
|
||||||
|
|
||||||
|
out_w = desc.Width;
|
||||||
|
out_h = desc.Height;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace d3d11_hooks {
|
||||||
|
|
||||||
|
void try_screenshot(IDXGISwapChain *swapchain) {
|
||||||
|
if (!swapchain || !graphics_screenshot_consume()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto file_path = graphics_screenshot_genpath();
|
||||||
|
if (file_path.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ID3D11Device *raw_device = nullptr;
|
||||||
|
if (FAILED(swapchain->GetDevice(IID_PPV_ARGS(&raw_device))) || !raw_device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
com_ptr<ID3D11Device> device(raw_device);
|
||||||
|
ID3D11DeviceContext *raw_ctx = nullptr;
|
||||||
|
device->GetImmediateContext(&raw_ctx);
|
||||||
|
if (!raw_ctx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
com_ptr<ID3D11DeviceContext> context(raw_ctx);
|
||||||
|
|
||||||
|
std::vector<uint8_t> pixels;
|
||||||
|
uint32_t w = 0, h = 0;
|
||||||
|
if (!copy_backbuffer_to_rgba(swapchain, device.get(), context.get(), pixels, w, h)) {
|
||||||
|
log_warning("graphics::d3d11", "screenshot: failed to capture backbuffer");
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Error,
|
||||||
|
"Screenshot failed to capture");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info("graphics::d3d11", "saving screenshot to {}", file_path);
|
||||||
|
if (stbi_write_png(file_path.c_str(), (int) w, (int) h, 4,
|
||||||
|
pixels.data(), (int) w * 4))
|
||||||
|
{
|
||||||
|
clipboard::copy_image(file_path);
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Success,
|
||||||
|
fmt::format("Screenshot saved: {}", fileutils::basename(file_path)));
|
||||||
|
} else {
|
||||||
|
log_warning("graphics::d3d11", "screenshot: stbi_write_png failed");
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Error,
|
||||||
|
"Screenshot failed to save");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // SPICE_D3D11
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
// dx11 swapchain vtable hooks + per-frame overlay pump.
|
||||||
|
//
|
||||||
|
// dxgi shares vtables across swapchain instances, so we only need to patch
|
||||||
|
// Present / Present1 / ResizeBuffers once on the first instance we see.
|
||||||
|
// each frame we lazily attach the overlay to whichever swapchain is
|
||||||
|
// presenting, then drive its imgui update / new_frame / render cycle.
|
||||||
|
|
||||||
|
#include "d3d11_backend.h"
|
||||||
|
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <d3d11.h>
|
||||||
|
#include <dxgi.h>
|
||||||
|
#include <dxgi1_2.h>
|
||||||
|
|
||||||
|
#include "d3d11_internal.h"
|
||||||
|
|
||||||
|
#include "external/imgui/imgui.h"
|
||||||
|
#include "external/imgui/backends/imgui_impl_dx11.h"
|
||||||
|
#include "overlay/imgui/impl_spice.h"
|
||||||
|
|
||||||
|
#include "games/io.h"
|
||||||
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "launcher/launcher.h"
|
||||||
|
#include "misc/eamuse.h"
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// overlay render bridge
|
||||||
|
|
||||||
|
namespace overlay::d3d11 {
|
||||||
|
|
||||||
|
// sRGB backbuffers need a UNORM view: ImGui vertex colors are already
|
||||||
|
// sRGB-encoded, so an extra linear->sRGB conversion would wash the
|
||||||
|
// overlay out white.
|
||||||
|
static DXGI_FORMAT to_unorm_view(DXGI_FORMAT fmt) {
|
||||||
|
switch (fmt) {
|
||||||
|
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||||
|
case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM;
|
||||||
|
default: return fmt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ensure_rtv(ID3D11Device *device,
|
||||||
|
IDXGISwapChain *swapchain,
|
||||||
|
ID3D11RenderTargetView **rtv)
|
||||||
|
{
|
||||||
|
if (*rtv || !device || !swapchain) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ID3D11Texture2D *backbuffer = nullptr;
|
||||||
|
if (FAILED(swapchain->GetBuffer(0, IID_PPV_ARGS(&backbuffer))) || !backbuffer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
D3D11_TEXTURE2D_DESC td {};
|
||||||
|
backbuffer->GetDesc(&td);
|
||||||
|
const DXGI_FORMAT view_fmt = to_unorm_view(td.Format);
|
||||||
|
if (view_fmt != td.Format) {
|
||||||
|
D3D11_RENDER_TARGET_VIEW_DESC rtvd {};
|
||||||
|
rtvd.Format = view_fmt;
|
||||||
|
rtvd.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
|
||||||
|
device->CreateRenderTargetView(backbuffer, &rtvd, rtv);
|
||||||
|
} else {
|
||||||
|
device->CreateRenderTargetView(backbuffer, nullptr, rtv);
|
||||||
|
}
|
||||||
|
backbuffer->Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
// bind the backbuffer (lazily creating the RTV) and draw the imgui
|
||||||
|
// frame on top. reset_invalidate releases *rtv on ResizeBuffers.
|
||||||
|
void render(ID3D11Device *device,
|
||||||
|
ID3D11DeviceContext *context,
|
||||||
|
IDXGISwapChain *swapchain,
|
||||||
|
ID3D11RenderTargetView **rtv)
|
||||||
|
{
|
||||||
|
ensure_rtv(device, swapchain, rtv);
|
||||||
|
if (!*rtv || !context) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// present happens immediately after, so no need to save the previous
|
||||||
|
// RT binding (flip-model resets it anyway).
|
||||||
|
context->OMSetRenderTargets(1, rtv, nullptr);
|
||||||
|
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// file-local state + per-frame helpers
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using Present_t = HRESULT(STDMETHODCALLTYPE *)(
|
||||||
|
IDXGISwapChain *, UINT, UINT);
|
||||||
|
using ResizeBuffers_t = HRESULT(STDMETHODCALLTYPE *)(
|
||||||
|
IDXGISwapChain *, UINT, UINT, UINT, DXGI_FORMAT, UINT);
|
||||||
|
using Present1_t = HRESULT(STDMETHODCALLTYPE *)(
|
||||||
|
IDXGISwapChain1 *, UINT, UINT, const DXGI_PRESENT_PARAMETERS *);
|
||||||
|
|
||||||
|
Present_t Present_orig = nullptr;
|
||||||
|
ResizeBuffers_t ResizeBuffers_orig = nullptr;
|
||||||
|
Present1_t Present1_orig = nullptr;
|
||||||
|
|
||||||
|
bool g_swapchain_hooked = false;
|
||||||
|
bool g_swapchain1_hooked = false;
|
||||||
|
|
||||||
|
void try_create_overlay(IDXGISwapChain *swapchain) {
|
||||||
|
if (!swapchain || overlay::OVERLAY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DXGI_SWAP_CHAIN_DESC desc {};
|
||||||
|
if (FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// only attach to the main game window; ignore sub-screens / IME helpers.
|
||||||
|
HWND main = d3d11_hooks::main_hwnd();
|
||||||
|
if (main && desc.OutputWindow != main) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ID3D11Device *device = nullptr;
|
||||||
|
if (FAILED(swapchain->GetDevice(IID_PPV_ARGS(&device))) || !device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ID3D11DeviceContext *context = nullptr;
|
||||||
|
device->GetImmediateContext(&context);
|
||||||
|
|
||||||
|
if (context) {
|
||||||
|
overlay::create_d3d11(desc.OutputWindow, device, context, swapchain);
|
||||||
|
RECT cr {};
|
||||||
|
::GetClientRect(desc.OutputWindow, &cr);
|
||||||
|
log_info("graphics::d3d11",
|
||||||
|
"attached overlay to swapchain hwnd=0x{:x} backbuffer={}x{} client={}x{}",
|
||||||
|
(uintptr_t) desc.OutputWindow,
|
||||||
|
desc.BufferDesc.Width, desc.BufferDesc.Height,
|
||||||
|
cr.right - cr.left, cr.bottom - cr.top);
|
||||||
|
context->Release();
|
||||||
|
}
|
||||||
|
device->Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
// rising-edge screenshot hotkey poll (mirrors d3d9 backend behaviour).
|
||||||
|
void poll_screenshot_hotkey() {
|
||||||
|
static bool s_down = false;
|
||||||
|
auto buttons = games::get_buttons_overlay(eamuse_get_game());
|
||||||
|
const bool pressed = buttons
|
||||||
|
&& (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered())
|
||||||
|
&& GameAPI::Buttons::getState(RI_MGR,
|
||||||
|
buttons->at(games::OverlayButtons::Screenshot));
|
||||||
|
if (pressed && !s_down) {
|
||||||
|
graphics_screenshot_trigger();
|
||||||
|
}
|
||||||
|
s_down = pressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pump_overlay(IDXGISwapChain *swapchain) {
|
||||||
|
if (!overlay::OVERLAY || !overlay::OVERLAY->uses_swapchain(swapchain)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
poll_screenshot_hotkey();
|
||||||
|
|
||||||
|
// size imgui to the backbuffer (not window client). dxgi may upscale
|
||||||
|
// a small backbuffer into a larger client rect; without this override
|
||||||
|
// imgui would draw past the RTV and the mouse mapping would be off.
|
||||||
|
DXGI_SWAP_CHAIN_DESC desc {};
|
||||||
|
if (SUCCEEDED(swapchain->GetDesc(&desc))) {
|
||||||
|
ImGui_ImplSpice_SetDisplaySizeOverride(
|
||||||
|
(float) desc.BufferDesc.Width,
|
||||||
|
(float) desc.BufferDesc.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay::OVERLAY->update();
|
||||||
|
overlay::OVERLAY->new_frame();
|
||||||
|
overlay::OVERLAY->render();
|
||||||
|
|
||||||
|
// after overlay render so toasts/menus end up in the saved image.
|
||||||
|
d3d11_hooks::try_screenshot(swapchain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// swapchain method hooks
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE Present_hook(
|
||||||
|
IDXGISwapChain *swapchain, UINT SyncInterval, UINT Flags)
|
||||||
|
{
|
||||||
|
try_create_overlay(swapchain);
|
||||||
|
pump_overlay(swapchain);
|
||||||
|
return Present_orig(swapchain, SyncInterval, Flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE Present1_hook(
|
||||||
|
IDXGISwapChain1 *swapchain, UINT SyncInterval, UINT Flags,
|
||||||
|
const DXGI_PRESENT_PARAMETERS *pParams)
|
||||||
|
{
|
||||||
|
try_create_overlay(swapchain);
|
||||||
|
pump_overlay(swapchain);
|
||||||
|
return Present1_orig(swapchain, SyncInterval, Flags, pParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE ResizeBuffers_hook(
|
||||||
|
IDXGISwapChain *swapchain, UINT BufferCount, UINT Width, UINT Height,
|
||||||
|
DXGI_FORMAT NewFormat, UINT SwapChainFlags)
|
||||||
|
{
|
||||||
|
const bool ours = overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain);
|
||||||
|
if (ours) {
|
||||||
|
log_info("graphics::d3d11", "ResizeBuffers {}x{} fmt={}",
|
||||||
|
Width, Height, (int32_t) NewFormat);
|
||||||
|
overlay::OVERLAY->reset_invalidate();
|
||||||
|
}
|
||||||
|
HRESULT res = ResizeBuffers_orig(
|
||||||
|
swapchain, BufferCount, Width, Height, NewFormat, SwapChainFlags);
|
||||||
|
if (ours && SUCCEEDED(res)) {
|
||||||
|
overlay::OVERLAY->reset_recreate();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// d3d11_hooks public surface: main-window tracking + vtable install.
|
||||||
|
|
||||||
|
namespace d3d11_hooks {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::atomic<HWND> g_main_hwnd { nullptr };
|
||||||
|
std::atomic<HWND> g_ignored_hwnd { nullptr };
|
||||||
|
}
|
||||||
|
|
||||||
|
void note_main_hwnd(HWND hwnd) {
|
||||||
|
if (!hwnd || hwnd == g_ignored_hwnd.load()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HWND expected = nullptr;
|
||||||
|
if (g_main_hwnd.compare_exchange_strong(expected, hwnd)) {
|
||||||
|
log_info("graphics::d3d11", "main hwnd recorded: 0x{:x}",
|
||||||
|
(uintptr_t) hwnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND main_hwnd() {
|
||||||
|
return g_main_hwnd.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ignore_hwnd(HWND hwnd) {
|
||||||
|
g_ignored_hwnd.store(hwnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// patch IDXGISwapChain::Present + ResizeBuffers and (if implemented)
|
||||||
|
// IDXGISwapChain1::Present1. idempotent; flag is set only after success
|
||||||
|
// so failed attempts can be retried on the next swapchain.
|
||||||
|
void install_swapchain_hooks(IDXGISwapChain *swapchain) {
|
||||||
|
if (!swapchain) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
static std::mutex s_hook_mutex;
|
||||||
|
std::lock_guard<std::mutex> lock(s_hook_mutex);
|
||||||
|
|
||||||
|
if (!g_swapchain_hooked) {
|
||||||
|
const bool a = hook_vtbl(swapchain, 8, (void *) Present_hook,
|
||||||
|
(void **) &Present_orig, "IDXGISwapChain::Present");
|
||||||
|
const bool b = hook_vtbl(swapchain, 13, (void *) ResizeBuffers_hook,
|
||||||
|
(void **) &ResizeBuffers_orig, "IDXGISwapChain::ResizeBuffers");
|
||||||
|
if (a && b) {
|
||||||
|
g_swapchain_hooked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!g_swapchain1_hooked) {
|
||||||
|
IDXGISwapChain1 *sc1 = nullptr;
|
||||||
|
if (SUCCEEDED(swapchain->QueryInterface(IID_PPV_ARGS(&sc1))) && sc1) {
|
||||||
|
if (hook_vtbl(sc1, 22, (void *) Present1_hook,
|
||||||
|
(void **) &Present1_orig, "IDXGISwapChain1::Present1")) {
|
||||||
|
g_swapchain1_hooked = true;
|
||||||
|
}
|
||||||
|
sc1->Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // SPICE_D3D11
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
// proactive vtable capture for the dx11 backend.
|
||||||
|
//
|
||||||
|
// titles under the execexe loader routinely race past our export-level
|
||||||
|
// trampolines, so the game's first real swapchain never goes through us.
|
||||||
|
// we sidestep that by creating a throwaway device + swapchain ourselves
|
||||||
|
// the moment d3d11.dll + dxgi.dll appear, which patches the shared
|
||||||
|
// IDXGISwapChain[1] / IDXGIFactory[2] vtables ahead of the game.
|
||||||
|
|
||||||
|
#include "d3d11_backend.h"
|
||||||
|
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <d3d11.h>
|
||||||
|
#include <dxgi.h>
|
||||||
|
#include <dxgi1_2.h>
|
||||||
|
|
||||||
|
#include "d3d11_internal.h"
|
||||||
|
|
||||||
|
using d3d11_hooks::com_ptr;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using D3D11CreateDevice_t = HRESULT(WINAPI *)(
|
||||||
|
IDXGIAdapter *, D3D_DRIVER_TYPE, HMODULE, UINT,
|
||||||
|
const D3D_FEATURE_LEVEL *, UINT, UINT,
|
||||||
|
ID3D11Device **, D3D_FEATURE_LEVEL *, ID3D11DeviceContext **);
|
||||||
|
using CreateDXGIFactory1_t = HRESULT(WINAPI *)(REFIID, void **);
|
||||||
|
using CreateDXGIFactory2_t = HRESULT(WINAPI *)(UINT, REFIID, void **);
|
||||||
|
|
||||||
|
std::atomic<bool> g_vtables_captured { false };
|
||||||
|
|
||||||
|
template<typename Fn>
|
||||||
|
Fn resolve(HMODULE mod, const char *name) {
|
||||||
|
return reinterpret_cast<Fn>(GetProcAddress(mod, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
com_ptr<IDXGIFactory2> create_factory2(CreateDXGIFactory2_t f2,
|
||||||
|
CreateDXGIFactory1_t f1)
|
||||||
|
{
|
||||||
|
IDXGIFactory2 *raw = nullptr;
|
||||||
|
if (f2 && SUCCEEDED(f2(0, IID_PPV_ARGS(&raw))) && raw) {
|
||||||
|
return com_ptr<IDXGIFactory2>(raw);
|
||||||
|
}
|
||||||
|
IDXGIFactory1 *factory1 = nullptr;
|
||||||
|
if (f1 && SUCCEEDED(f1(IID_PPV_ARGS(&factory1))) && factory1) {
|
||||||
|
factory1->QueryInterface(IID_PPV_ARGS(&raw));
|
||||||
|
factory1->Release();
|
||||||
|
}
|
||||||
|
return com_ptr<IDXGIFactory2>(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool create_dummy_device(D3D11CreateDevice_t create,
|
||||||
|
com_ptr<ID3D11Device> &device,
|
||||||
|
com_ptr<ID3D11DeviceContext> &context)
|
||||||
|
{
|
||||||
|
static constexpr D3D_FEATURE_LEVEL levels[] = {
|
||||||
|
D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0,
|
||||||
|
D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0,
|
||||||
|
};
|
||||||
|
// hardware first, then WARP so headless / unusual configs still work.
|
||||||
|
for (auto type : { D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP }) {
|
||||||
|
ID3D11Device *d = nullptr;
|
||||||
|
ID3D11DeviceContext *c = nullptr;
|
||||||
|
D3D_FEATURE_LEVEL got;
|
||||||
|
if (SUCCEEDED(create(nullptr, type, nullptr, 0,
|
||||||
|
levels, ARRAYSIZE(levels), D3D11_SDK_VERSION,
|
||||||
|
&d, &got, &c)) && d) {
|
||||||
|
device.reset(d);
|
||||||
|
context.reset(c);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace d3d11_hooks {
|
||||||
|
|
||||||
|
// create a throwaway device + swapchain to patch the shared vtables before
|
||||||
|
// the game's loader races past our export trampolines. safe to call
|
||||||
|
// repeatedly; runs at most once.
|
||||||
|
void try_capture_vtables() {
|
||||||
|
if (g_vtables_captured.load()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HMODULE d3d11 = GetModuleHandleW(L"d3d11.dll");
|
||||||
|
HMODULE dxgi = GetModuleHandleW(L"dxgi.dll");
|
||||||
|
if (!d3d11 || !dxgi) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto create_device = resolve<D3D11CreateDevice_t>(d3d11, "D3D11CreateDevice");
|
||||||
|
auto f2 = resolve<CreateDXGIFactory2_t>(dxgi, "CreateDXGIFactory2");
|
||||||
|
auto f1 = resolve<CreateDXGIFactory1_t>(dxgi, "CreateDXGIFactory1");
|
||||||
|
if (!create_device || (!f1 && !f2)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// serialize concurrent calls (poll thread + LDR notification). only
|
||||||
|
// flip g_vtables_captured after success so failed attempts remain
|
||||||
|
// retriable on the next tick.
|
||||||
|
static std::atomic<bool> in_progress { false };
|
||||||
|
if (in_progress.exchange(true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
struct scope_clear {
|
||||||
|
std::atomic<bool> &flag;
|
||||||
|
~scope_clear() { flag.store(false); }
|
||||||
|
} clear { in_progress };
|
||||||
|
|
||||||
|
// hidden message-only window; STATIC is always registered by user32.
|
||||||
|
HWND dummy_hwnd = CreateWindowExW(
|
||||||
|
0, L"STATIC", L"", 0, 0, 0, 1, 1,
|
||||||
|
HWND_MESSAGE, nullptr, GetModuleHandleW(nullptr), nullptr);
|
||||||
|
if (!dummy_hwnd) {
|
||||||
|
log_warning("graphics::d3d11",
|
||||||
|
"vtable capture: CreateWindowExW failed (gle={})", (unsigned long)GetLastError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto destroy_hwnd = std::unique_ptr<HWND__, decltype(&DestroyWindow)>(
|
||||||
|
dummy_hwnd, &DestroyWindow);
|
||||||
|
|
||||||
|
// if the game's CreateDXGIFactory_hook already raced us, our
|
||||||
|
// CreateSwapChainForHwnd call below would trip the hook and try to
|
||||||
|
// record dummy_hwnd as the main window. block that.
|
||||||
|
ignore_hwnd(dummy_hwnd);
|
||||||
|
|
||||||
|
auto factory2 = create_factory2(f2, f1);
|
||||||
|
if (!factory2) {
|
||||||
|
log_warning("graphics::d3d11", "vtable capture: CreateDXGIFactory* failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
com_ptr<ID3D11Device> device;
|
||||||
|
com_ptr<ID3D11DeviceContext> context;
|
||||||
|
if (!create_dummy_device(create_device, device, context)) {
|
||||||
|
log_warning("graphics::d3d11", "vtable capture: D3D11CreateDevice failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DXGI_SWAP_CHAIN_DESC1 desc {};
|
||||||
|
desc.Width = 1;
|
||||||
|
desc.Height = 1;
|
||||||
|
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
|
||||||
|
desc.SampleDesc.Count = 1;
|
||||||
|
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
|
||||||
|
desc.BufferCount = 2;
|
||||||
|
desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
|
||||||
|
|
||||||
|
IDXGISwapChain1 *raw_sc = nullptr;
|
||||||
|
HRESULT hr = factory2->CreateSwapChainForHwnd(
|
||||||
|
device.get(), dummy_hwnd, &desc, nullptr, nullptr, &raw_sc);
|
||||||
|
if (FAILED(hr) || !raw_sc) {
|
||||||
|
log_warning("graphics::d3d11",
|
||||||
|
"vtable capture: CreateSwapChainForHwnd failed (hr={:#x})", (unsigned long)hr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
com_ptr<IDXGISwapChain1> swapchain(raw_sc);
|
||||||
|
|
||||||
|
install_swapchain_hooks(swapchain.get());
|
||||||
|
install_factory_hooks(factory2.get());
|
||||||
|
|
||||||
|
g_vtables_captured.store(true);
|
||||||
|
log_info("graphics::d3d11", "vtable capture complete (via dummy swapchain)");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // SPICE_D3D11
|
||||||
@@ -28,8 +28,10 @@
|
|||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
|
#include "overlay/notifications.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/deferlog.h"
|
#include "util/deferlog.h"
|
||||||
|
#include "util/fileutils.h"
|
||||||
#include "util/flags_helper.h"
|
#include "util/flags_helper.h"
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
@@ -465,15 +467,21 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::RegisterSoftwareDevice(void *pIniti
|
|||||||
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterCount() {
|
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterCount() {
|
||||||
UINT result = pReal->GetAdapterCount();
|
UINT result = pReal->GetAdapterCount();
|
||||||
|
|
||||||
if (!FAKE_SUBSCREEN_ADAPTER && games::popn::is_pikapika_model() && result == 1) {
|
if (!FAKE_SUBSCREEN_ADAPTER) {
|
||||||
FAKE_SUBSCREEN_ADAPTER = true;
|
if (games::popn::is_pikapika_model() && result == 1) {
|
||||||
log_info(
|
FAKE_SUBSCREEN_ADAPTER = true;
|
||||||
"graphics::d3d9",
|
log_info(
|
||||||
"GetAdapterCount returned 1, popn needs 2 adapters - enabliing fake submonitor mode");
|
"graphics::d3d9",
|
||||||
}
|
"GetAdapterCount returned {}, popn needs 2 adapters - enabling fake submonitor mode", result);
|
||||||
|
return 2;
|
||||||
|
|
||||||
if (FAKE_SUBSCREEN_ADAPTER) {
|
} else if (games::gitadora::is_arena_model() && !GRAPHICS_WINDOWED && result < 4) {
|
||||||
return 2;
|
FAKE_SUBSCREEN_ADAPTER = true;
|
||||||
|
log_info(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"GetAdapterCount returned {}, gfdm needs 4 adapters - enabling fake submonitor mode", result);
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -485,12 +493,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier(
|
|||||||
D3DADAPTER_IDENTIFIER9 *pIdentifier)
|
D3DADAPTER_IDENTIFIER9 *pIdentifier)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1 && pIdentifier) {
|
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0 && pIdentifier) {
|
||||||
*pIdentifier = {};
|
*pIdentifier = {};
|
||||||
strcpy(pIdentifier->DeviceName, "\\\\.\\DISPLAY_SPICE_FAKE");
|
const std::string adapter_name = fmt::format("\\\\.\\DISPLAY_SPICE_FAKE_{}", Adapter);
|
||||||
|
strcpy(pIdentifier->DeviceName, adapter_name.c_str());
|
||||||
log_misc(
|
log_misc(
|
||||||
"graphics::d3d9",
|
"graphics::d3d9",
|
||||||
"GetAdapterIdentifier called for fake subscreen adapter 1: {}",
|
"GetAdapterIdentifier called for fake subscreen adapter {}: {}",
|
||||||
|
Adapter,
|
||||||
pIdentifier->DeviceName);
|
pIdentifier->DeviceName);
|
||||||
return S_OK;
|
return S_OK;
|
||||||
}
|
}
|
||||||
@@ -499,8 +509,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier(
|
|||||||
}
|
}
|
||||||
|
|
||||||
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCount(UINT Adapter, D3DFORMAT Format) {
|
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCount(UINT Adapter, D3DFORMAT Format) {
|
||||||
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1) {
|
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
|
||||||
log_misc("graphics::d3d9", "GetAdapterModeCount called for fake subscreen adapter");
|
log_misc("graphics::d3d9", "GetAdapterModeCount called for fake subscreen adapter {}", Adapter);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,8 +523,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes(
|
|||||||
UINT Mode,
|
UINT Mode,
|
||||||
D3DDISPLAYMODE *pMode)
|
D3DDISPLAYMODE *pMode)
|
||||||
{
|
{
|
||||||
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1) {
|
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
|
||||||
log_misc("graphics::d3d9", "EnumAdapterModes called for fake subscreen adapter");
|
log_misc("graphics::d3d9", "EnumAdapterModes called for fake subscreen adapter {}", Adapter);
|
||||||
if (Mode == 0 && pMode) {
|
if (Mode == 0 && pMode) {
|
||||||
pMode->Width = 1280;
|
pMode->Width = 1280;
|
||||||
pMode->Height = 800;
|
pMode->Height = 800;
|
||||||
@@ -622,8 +632,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceFormat(
|
|||||||
D3DRESOURCETYPE RType,
|
D3DRESOURCETYPE RType,
|
||||||
D3DFORMAT CheckFormat)
|
D3DFORMAT CheckFormat)
|
||||||
{
|
{
|
||||||
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1) {
|
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
|
||||||
log_misc("graphics::d3d9", "CheckDeviceFormat called for fake subscreen adapter");
|
log_misc("graphics::d3d9", "CheckDeviceFormat called for fake subscreen adapter {}", Adapter);
|
||||||
return D3D_OK;
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -708,7 +718,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetDeviceCaps(UINT Adapter, D3DDEVT
|
|||||||
}
|
}
|
||||||
// in windowed mode, LDJ will always launch two windows, no special handling needed here
|
// in windowed mode, LDJ will always launch two windows, no special handling needed here
|
||||||
} else if (avs::game::is_model("KFC")) {
|
} else if (avs::game::is_model("KFC")) {
|
||||||
if (GRAPHICS_WINDOWED && GRAPHICS_PREVENT_SECONDARY_WINDOW) {
|
if (GRAPHICS_WINDOWED && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
// user wants windowed mode but does not want subscreen at all
|
// user wants windowed mode but does not want subscreen at all
|
||||||
pCaps->NumberOfAdaptersInGroup = 1;
|
pCaps->NumberOfAdaptersInGroup = 1;
|
||||||
} else {
|
} else {
|
||||||
@@ -1143,10 +1153,15 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
|
|
||||||
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface);
|
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface);
|
||||||
|
|
||||||
// initialize sub screen if IIDX/SDVX requested a multi-head context
|
// initialize sub screen if the game requested a multi-head context
|
||||||
if (avs::game::is_model({"LDJ", "KFC", "M39"}) &&
|
if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) &&
|
||||||
(orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
|
(orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
|
||||||
graphics_d3d9_ldj_init_sub_screen(*ppReturnedDeviceInterface, &pPresentationParameters[1]);
|
|
||||||
|
UINT i = 1;
|
||||||
|
if (games::gitadora::is_arena_model()) {
|
||||||
|
i = 2;
|
||||||
|
}
|
||||||
|
graphics_d3d9_ldj_init_sub_screen(*ppReturnedDeviceInterface, &pPresentationParameters[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1196,7 +1211,13 @@ static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRE
|
|||||||
log_info("graphics::d3d9", "created additional swap chain for windowed mode");
|
log_info("graphics::d3d9", "created additional swap chain for windowed mode");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hr = device->GetSwapChain(1, &SUB_SWAP_CHAIN);
|
|
||||||
|
int swapchain = 1;
|
||||||
|
if (games::gitadora::is_arena_model()) {
|
||||||
|
swapchain = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = device->GetSwapChain(swapchain, &SUB_SWAP_CHAIN);
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
log_warning("graphics::d3d9", "failed to acquire fullscreen sub swap chain, hr={}", FMT_HRESULT(hr));
|
log_warning("graphics::d3d9", "failed to acquire fullscreen sub swap chain, hr={}", FMT_HRESULT(hr));
|
||||||
} else {
|
} else {
|
||||||
@@ -1233,7 +1254,7 @@ static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
|
|||||||
// iidx/sdvx
|
// iidx/sdvx
|
||||||
int swapchain = 1;
|
int swapchain = 1;
|
||||||
if (games::gitadora::is_arena_model()) {
|
if (games::gitadora::is_arena_model()) {
|
||||||
swapchain = 3;
|
swapchain = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE && SUB_SWAP_CHAIN == nullptr) {
|
if (!ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE && SUB_SWAP_CHAIN == nullptr) {
|
||||||
@@ -1415,11 +1436,18 @@ static void save_screenshot(const std::string &file_path, UINT height, IDirect3D
|
|||||||
|
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
log_warning("graphics::d3d9", "Failed to save screenshot");
|
log_warning("graphics::d3d9", "Failed to save screenshot");
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Error,
|
||||||
|
"Screenshot failed to save");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// save to clipboard
|
// save to clipboard
|
||||||
clipboard::copy_image(file_path);
|
clipboard::copy_image(file_path);
|
||||||
|
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Success,
|
||||||
|
fmt::format("Screenshot saved: {}", fileutils::basename(file_path)));
|
||||||
} else {
|
} else {
|
||||||
log_warning("graphics::d3d9", "Direct3D save helper function not available");
|
log_warning("graphics::d3d9", "Direct3D save helper function not available");
|
||||||
}
|
}
|
||||||
@@ -1430,6 +1458,15 @@ void graphics_d3d9_on_present(
|
|||||||
IDirect3DDevice9 *device,
|
IDirect3DDevice9 *device,
|
||||||
IDirect3DDevice9 *wrapped_device) {
|
IDirect3DDevice9 *wrapped_device) {
|
||||||
|
|
||||||
|
// image resize / orientation swap. run here (the present path) rather than from `EndScene`,
|
||||||
|
// which may fire several times per frame on multi-pass / render-to-texture games. this is the
|
||||||
|
// single point guaranteed to be after the game's last `EndScene` and before the real `Present`,
|
||||||
|
// so the back buffer is fully drawn and the expensive StretchRect work happens exactly once per
|
||||||
|
// frame. it must run before the overlay is rendered so the overlay isn't scaled with the image.
|
||||||
|
if (cfg::SCREENRESIZE->enable_screen_resize || GRAPHICS_FS_ORIENTATION_SWAP) {
|
||||||
|
SurfaceHook(device);
|
||||||
|
}
|
||||||
|
|
||||||
// Do overlay init as many d3d9 hooks create a dummy instance to get vtable offsets and never
|
// Do overlay init as many d3d9 hooks create a dummy instance to get vtable offsets and never
|
||||||
// call `Present`. This avoids race conditions on `IDirect3D9::CreateDevice` like with
|
// call `Present`. This avoids race conditions on `IDirect3D9::CreateDevice` like with
|
||||||
// `dx9osd.dll` for pfreepanic.
|
// `dx9osd.dll` for pfreepanic.
|
||||||
@@ -1452,8 +1489,7 @@ void graphics_d3d9_on_present(
|
|||||||
// for IIDX TDJ / SDVX UFC, handle subscreen
|
// for IIDX TDJ / SDVX UFC, handle subscreen
|
||||||
const bool is_vm = games::sdvx::is_valkyrie_model();
|
const bool is_vm = games::sdvx::is_valkyrie_model();
|
||||||
const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE;
|
const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE;
|
||||||
const bool is_gfdm_arena =
|
const bool is_gfdm_arena = games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS;
|
||||||
games::gitadora::is_arena_model() && games::gitadora::ARENA_SINGLE_WINDOW;
|
|
||||||
const bool is_pika = games::popn::is_pikapika_model();
|
const bool is_pika = games::popn::is_pikapika_model();
|
||||||
if (is_vm || is_tdj || is_gfdm_arena || is_pika) {
|
if (is_vm || is_tdj || is_gfdm_arena || is_pika) {
|
||||||
graphics_d3d9_ldj_on_present(wrapped_device);
|
graphics_d3d9_ldj_on_present(wrapped_device);
|
||||||
|
|||||||
@@ -129,13 +129,17 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() {
|
|||||||
this->main_swapchain->Release();
|
this->main_swapchain->Release();
|
||||||
this->main_swapchain = nullptr;
|
this->main_swapchain = nullptr;
|
||||||
}
|
}
|
||||||
if (this->sub_swapchain) {
|
for (auto &sc : this->sub_swapchain) {
|
||||||
this->sub_swapchain->Release();
|
if (sc) {
|
||||||
this->sub_swapchain = nullptr;
|
sc->Release();
|
||||||
|
sc = nullptr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (this->fake_sub_swapchain) {
|
for (auto &sc : this->fake_sub_swapchain) {
|
||||||
this->fake_sub_swapchain->Release();
|
if (sc) {
|
||||||
this->fake_sub_swapchain = nullptr;
|
sc->Release();
|
||||||
|
sc = nullptr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (overlay::ENABLED) {
|
if (overlay::ENABLED) {
|
||||||
@@ -249,28 +253,69 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
|
|||||||
{
|
{
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
|
|
||||||
HRESULT hr = pReal->CreateAdditionalSwapChain(pPresentationParameters, ppSwapChain);
|
int index = 0;
|
||||||
bool create_swap_chain = false;
|
bool create_swap_chain = false;
|
||||||
|
bool create_fake_swap_chain = false;
|
||||||
if (avs::game::is_model({"LDJ", "KFC", "M39"})) {
|
if (avs::game::is_model({"LDJ", "KFC", "M39"})) {
|
||||||
create_swap_chain = true;
|
create_swap_chain = true;
|
||||||
|
|
||||||
} else if (games::gitadora::is_arena_model() &&
|
} else if (games::gitadora::is_arena_model() &&
|
||||||
games::gitadora::ARENA_SINGLE_WINDOW &&
|
(GRAPHICS_PREVENT_SECONDARY_WINDOWS || GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS)) {
|
||||||
pPresentationParameters->BackBufferWidth == 800) {
|
|
||||||
create_swap_chain = true;
|
if (pPresentationParameters->BackBufferWidth == 800) {
|
||||||
|
// SMALL (subscreen)
|
||||||
|
create_swap_chain = true;
|
||||||
|
index = 0;
|
||||||
|
} else if (pPresentationParameters->BackBufferWidth == 1080) {
|
||||||
|
// LEFT/RIGHT
|
||||||
|
create_swap_chain = true;
|
||||||
|
index = 1;
|
||||||
|
if (sub_swapchain[index] || fake_sub_swapchain[index]) {
|
||||||
|
index = 2;
|
||||||
|
}
|
||||||
|
create_fake_swap_chain = GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS &&
|
||||||
|
!GRAPHICS_PREVENT_SECONDARY_WINDOWS;
|
||||||
|
} else {
|
||||||
|
log_warning("graphics::d3d9", "unknown swap chain detected in CreateAdditionalSwapChain");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (create_fake_swap_chain) {
|
||||||
|
if (!fake_sub_swapchain[index]) {
|
||||||
|
log_info(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"CreateAdditionalSwapChain called for hidden GITADORA side swap chain {}, "
|
||||||
|
"using fake swap chain",
|
||||||
|
index);
|
||||||
|
|
||||||
|
fake_sub_swapchain[index] =
|
||||||
|
new FakeIDirect3DSwapChain9(this, pPresentationParameters, false);
|
||||||
|
}
|
||||||
|
fake_sub_swapchain[index]->AddRef();
|
||||||
|
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[index]);
|
||||||
|
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT hr = pReal->CreateAdditionalSwapChain(pPresentationParameters, ppSwapChain);
|
||||||
|
|
||||||
if (create_swap_chain) {
|
if (create_swap_chain) {
|
||||||
if (SUCCEEDED(hr) && !sub_swapchain) {
|
log_misc(
|
||||||
sub_swapchain = new WrappedIDirect3DSwapChain9(this, *ppSwapChain);
|
"graphics::d3d9",
|
||||||
sub_swapchain->should_run_hooks = false;
|
"CreateAdditionalSwapChain called for swap chain {}, creating swap chain",
|
||||||
} else if (FAILED(hr) && !fake_sub_swapchain) {
|
index);
|
||||||
|
|
||||||
|
if (SUCCEEDED(hr) && !sub_swapchain[index]) {
|
||||||
|
sub_swapchain[index] = new WrappedIDirect3DSwapChain9(this, *ppSwapChain);
|
||||||
|
sub_swapchain[index]->should_run_hooks = false;
|
||||||
|
} else if (FAILED(hr) && !fake_sub_swapchain[index]) {
|
||||||
log_warning("graphics::d3d9",
|
log_warning("graphics::d3d9",
|
||||||
"failed to create sub swap chain, hr={}, using fake swap chain",
|
"failed to create sub swap chain, hr={}, using fake swap chain",
|
||||||
FMT_HRESULT(hr));
|
FMT_HRESULT(hr));
|
||||||
|
|
||||||
fake_sub_swapchain = new FakeIDirect3DSwapChain9(this, pPresentationParameters, false);
|
fake_sub_swapchain[index] = new FakeIDirect3DSwapChain9(this, pPresentationParameters, false);
|
||||||
fake_sub_swapchain->AddRef();
|
fake_sub_swapchain[index]->AddRef();
|
||||||
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain);
|
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[index]);
|
||||||
|
|
||||||
return D3D_OK;
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
@@ -308,22 +353,20 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
|
|||||||
if (iSwapChain == 1 && avs::game::is_model({"LDJ", "KFC", "M39"})) {
|
if (iSwapChain == 1 && avs::game::is_model({"LDJ", "KFC", "M39"})) {
|
||||||
swap = true;
|
swap = true;
|
||||||
}
|
}
|
||||||
if (games::gitadora::is_arena_model() &&
|
if (games::gitadora::is_arena_model() && iSwapChain == 2) {
|
||||||
games::gitadora::ARENA_SINGLE_WINDOW &&
|
|
||||||
iSwapChain == 3) {
|
|
||||||
swap = true;
|
swap = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (swap) {
|
if (swap) {
|
||||||
if (sub_swapchain) {
|
if (sub_swapchain[0]) {
|
||||||
sub_swapchain->AddRef();
|
sub_swapchain[0]->AddRef();
|
||||||
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(sub_swapchain);
|
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(sub_swapchain[0]);
|
||||||
|
|
||||||
graphics_screens_register(iSwapChain);
|
graphics_screens_register(iSwapChain);
|
||||||
return D3D_OK;
|
return D3D_OK;
|
||||||
} else if (fake_sub_swapchain) {
|
} else if (fake_sub_swapchain[0]) {
|
||||||
fake_sub_swapchain->AddRef();
|
fake_sub_swapchain[0]->AddRef();
|
||||||
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain);
|
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[0]);
|
||||||
|
|
||||||
graphics_screens_register(iSwapChain);
|
graphics_screens_register(iSwapChain);
|
||||||
return D3D_OK;
|
return D3D_OK;
|
||||||
@@ -344,7 +387,7 @@ UINT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetNumberOfSwapChains() {
|
|||||||
|
|
||||||
UINT n = pReal->GetNumberOfSwapChains();
|
UINT n = pReal->GetNumberOfSwapChains();
|
||||||
|
|
||||||
if (sub_swapchain && avs::game::is_model({"LDJ", "KFC", "M39"})) {
|
if (sub_swapchain[0] && avs::game::is_model({"LDJ", "KFC", "M39"})) {
|
||||||
n += 1;
|
n += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -717,18 +760,15 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::BeginScene() {
|
|||||||
|
|
||||||
static IDirect3DSurface9 *topSurface = nullptr;
|
static IDirect3DSurface9 *topSurface = nullptr;
|
||||||
static IDirect3DSurface9 *backbuffer = nullptr;
|
static IDirect3DSurface9 *backbuffer = nullptr;
|
||||||
static LPDIRECT3DSWAPCHAIN9 mSwapChain = nullptr;
|
static IDirect3DTexture9 *tex = nullptr;
|
||||||
static IDirect3DTexture9* tex;
|
|
||||||
|
|
||||||
static UINT topSurface_width = 0;
|
static UINT topSurface_width = 0;
|
||||||
static UINT topSurface_height = 0;
|
static UINT topSurface_height = 0;
|
||||||
static float topSurface_aspect_ratio = 1.f;
|
static float topSurface_aspect_ratio = 1.f;
|
||||||
|
static UINT backbuffer_width = 0;
|
||||||
|
static UINT backbuffer_height = 0;
|
||||||
|
|
||||||
void SurfaceHook(IDirect3DDevice9 *pReal) {
|
void SurfaceHook(IDirect3DDevice9 *pReal) {
|
||||||
D3DPRESENT_PARAMETERS param {};
|
|
||||||
|
|
||||||
pReal->GetSwapChain(0, &mSwapChain);
|
|
||||||
mSwapChain->GetPresentParameters(¶m);
|
|
||||||
|
|
||||||
// phase 0 - create a surface that has the same aspect ratio as the original back buffer
|
// phase 0 - create a surface that has the same aspect ratio as the original back buffer
|
||||||
// but larger in size. this is needed to ensure that when the user zooms out, we have
|
// but larger in size. this is needed to ensure that when the user zooms out, we have
|
||||||
@@ -736,8 +776,20 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
|
|||||||
//
|
//
|
||||||
// (only done once)
|
// (only done once)
|
||||||
if (!topSurface) {
|
if (!topSurface) {
|
||||||
topSurface_width = param.BackBufferWidth * 3;
|
// GetSwapChain/GetBackBuffer AddRef their out parameters; the swap chain is only
|
||||||
topSurface_height = param.BackBufferHeight * 3;
|
// needed here (during one-time init), so release it after use to avoid a per-frame
|
||||||
|
// ref leak. The backbuffer ref is intentionally retained for the lifetime of the
|
||||||
|
// cached resources.
|
||||||
|
LPDIRECT3DSWAPCHAIN9 swapChain = nullptr;
|
||||||
|
D3DPRESENT_PARAMETERS param {};
|
||||||
|
|
||||||
|
pReal->GetSwapChain(0, &swapChain);
|
||||||
|
swapChain->GetPresentParameters(¶m);
|
||||||
|
|
||||||
|
backbuffer_width = param.BackBufferWidth;
|
||||||
|
backbuffer_height = param.BackBufferHeight;
|
||||||
|
topSurface_width = backbuffer_width * 3;
|
||||||
|
topSurface_height = backbuffer_height * 3;
|
||||||
topSurface_aspect_ratio = (float)topSurface_width / (float)topSurface_height;
|
topSurface_aspect_ratio = (float)topSurface_width / (float)topSurface_height;
|
||||||
if (pReal->CreateTexture(topSurface_width, topSurface_height, 1,
|
if (pReal->CreateTexture(topSurface_width, topSurface_height, 1,
|
||||||
D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8,
|
D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8,
|
||||||
@@ -750,16 +802,18 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
|
|||||||
param.BackBufferWidth, param.BackBufferHeight, param.BackBufferCount);
|
param.BackBufferWidth, param.BackBufferHeight, param.BackBufferCount);
|
||||||
|
|
||||||
tex->GetSurfaceLevel(0, &topSurface);
|
tex->GetSurfaceLevel(0, &topSurface);
|
||||||
if (mSwapChain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &backbuffer) != D3D_OK) {
|
if (swapChain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &backbuffer) != D3D_OK) {
|
||||||
log_warning("graphics::d3d9", "SurfaceHook - GetBackBuffer failed");
|
log_warning("graphics::d3d9", "SurfaceHook - GetBackBuffer failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
swapChain->Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
// pre-calculate dimensions used for phase 1
|
// pre-calculate dimensions used for phase 1
|
||||||
const int rectLeft = param.BackBufferWidth;
|
const int rectLeft = backbuffer_width;
|
||||||
const int rectTop = param.BackBufferHeight;
|
const int rectTop = backbuffer_height;
|
||||||
const int w = param.BackBufferWidth;
|
const int w = backbuffer_width;
|
||||||
const int h = param.BackBufferHeight;
|
const int h = backbuffer_height;
|
||||||
|
|
||||||
// this code used to clear the surface using ColorFill on every call, but
|
// this code used to clear the surface using ColorFill on every call, but
|
||||||
// this turned out to be very expensive, leading to major frame drops in
|
// this turned out to be very expensive, leading to major frame drops in
|
||||||
@@ -769,9 +823,7 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
|
|||||||
cfg::SCREENRESIZE->need_surface_clean = false;
|
cfg::SCREENRESIZE->need_surface_clean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
D3DLOCKED_RECT rect;
|
|
||||||
HRESULT hr;
|
HRESULT hr;
|
||||||
topSurface->LockRect(&rect, NULL, D3DLOCK_DONOTWAIT);
|
|
||||||
|
|
||||||
// phase 1 - copy the original back buffer onto the new surface.
|
// phase 1 - copy the original back buffer onto the new surface.
|
||||||
// we draw it 1:1 in the center of the surface.
|
// we draw it 1:1 in the center of the surface.
|
||||||
@@ -834,7 +886,6 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
|
|||||||
originRect2.left, originRect2.top, originRect2.right, originRect2.bottom);
|
originRect2.left, originRect2.top, originRect2.right, originRect2.bottom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
topSurface->UnlockRect();
|
|
||||||
|
|
||||||
// phase 2 - viewport calculation - do the actual zoom / offset math
|
// phase 2 - viewport calculation - do the actual zoom / offset math
|
||||||
// figure out what region of the surface to copy back to the back buffer.
|
// figure out what region of the surface to copy back to the back buffer.
|
||||||
@@ -909,7 +960,6 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
|
|||||||
// targetRect.left, targetRect.top, targetRect.right, targetRect.bottom);
|
// targetRect.left, targetRect.top, targetRect.right, targetRect.bottom);
|
||||||
|
|
||||||
// phase 3 - draw the surface to back buffer
|
// phase 3 - draw the surface to back buffer
|
||||||
backbuffer->LockRect(&rect, NULL, D3DLOCK_DONOTWAIT);
|
|
||||||
bool use_linear_filter = true;
|
bool use_linear_filter = true;
|
||||||
if (cfg::SCREENRESIZE->enable_screen_resize) {
|
if (cfg::SCREENRESIZE->enable_screen_resize) {
|
||||||
use_linear_filter = cfg::SCREENRESIZE->enable_linear_filter;
|
use_linear_filter = cfg::SCREENRESIZE->enable_linear_filter;
|
||||||
@@ -918,7 +968,6 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
|
|||||||
topSurface, &targetRect,
|
topSurface, &targetRect,
|
||||||
backbuffer, nullptr,
|
backbuffer, nullptr,
|
||||||
use_linear_filter ? D3DTEXF_LINEAR : D3DTEXF_NONE);
|
use_linear_filter ? D3DTEXF_LINEAR : D3DTEXF_NONE);
|
||||||
backbuffer->UnlockRect();
|
|
||||||
if (hr != D3D_OK) {
|
if (hr != D3D_OK) {
|
||||||
log_warning(
|
log_warning(
|
||||||
"graphics::d3d9",
|
"graphics::d3d9",
|
||||||
@@ -930,10 +979,6 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
|
|||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::EndScene() {
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::EndScene() {
|
||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
|
|
||||||
if (cfg::SCREENRESIZE->enable_screen_resize || GRAPHICS_FS_ORIENTATION_SWAP) {
|
|
||||||
SurfaceHook(pReal);
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::once_flag printed;
|
static std::once_flag printed;
|
||||||
std::call_once(printed, []() {
|
std::call_once(printed, []() {
|
||||||
log_misc("graphics::d3d9", "WrappedIDirect3DDevice9::EndScene");
|
log_misc("graphics::d3d9", "WrappedIDirect3DDevice9::EndScene");
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ static const GUID IID_WrappedIDirect3DDevice9 = {
|
|||||||
0x6dec0d40, 0x1339, 0x4bda, { 0xa5, 0xf2, 0x22, 0x31, 0xd4, 0x1, 0xf, 0xd1 }
|
0x6dec0d40, 0x1339, 0x4bda, { 0xa5, 0xf2, 0x22, 0x31, 0xd4, 0x1, 0xf, 0xd1 }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// applies the image resize / orientation swap by copying the back buffer through an
|
||||||
|
// oversized intermediate surface and back. expects the real (unwrapped) device.
|
||||||
|
void SurfaceHook(IDirect3DDevice9 *pReal);
|
||||||
|
|
||||||
struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
|
struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
|
||||||
explicit WrappedIDirect3DDevice9(HWND hFocusWindow, IDirect3DDevice9 *orig)
|
explicit WrappedIDirect3DDevice9(HWND hFocusWindow, IDirect3DDevice9 *orig)
|
||||||
: hFocusWindow(hFocusWindow), pReal(orig), is_d3d9ex(false) {
|
: hFocusWindow(hFocusWindow), pReal(orig), is_d3d9ex(false) {
|
||||||
@@ -204,7 +208,8 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
|
|||||||
std::atomic_ulong refs = 1;
|
std::atomic_ulong refs = 1;
|
||||||
|
|
||||||
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
|
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
|
||||||
WrappedIDirect3DSwapChain9 *sub_swapchain = nullptr;
|
WrappedIDirect3DSwapChain9 *sub_swapchain[3] = { nullptr, nullptr, nullptr };
|
||||||
FakeIDirect3DSwapChain9 *fake_sub_swapchain = nullptr;
|
FakeIDirect3DSwapChain9 *fake_sub_swapchain[3] = { nullptr, nullptr, nullptr };
|
||||||
|
|
||||||
IDirect3DVertexShader9 *vertex_shader = nullptr;
|
IDirect3DVertexShader9 *vertex_shader = nullptr;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "games/iidx/iidx.h"
|
#include "games/iidx/iidx.h"
|
||||||
#include "games/popn/popn.h"
|
#include "games/popn/popn.h"
|
||||||
#include "hooks/graphics/backends/d3d9/d3d9_backend.h"
|
#include "hooks/graphics/backends/d3d9/d3d9_backend.h"
|
||||||
|
#include "hooks/graphics/backends/d3d11/d3d11_backend.h"
|
||||||
#include "launcher/shutdown.h"
|
#include "launcher/shutdown.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
@@ -39,6 +40,8 @@ struct CaptureData {
|
|||||||
HWND TDJ_SUBSCREEN_WINDOW = nullptr;
|
HWND TDJ_SUBSCREEN_WINDOW = nullptr;
|
||||||
HWND SDVX_SUBSCREEN_WINDOW = nullptr;
|
HWND SDVX_SUBSCREEN_WINDOW = nullptr;
|
||||||
HWND GFDM_SUBSCREEN_WINDOW = nullptr;
|
HWND GFDM_SUBSCREEN_WINDOW = nullptr;
|
||||||
|
static HWND GFDM_LEFT_WINDOW = nullptr;
|
||||||
|
static HWND GFDM_RIGHT_WINDOW = nullptr;
|
||||||
HWND POPN_SUBSCREEN_WINDOW = nullptr;
|
HWND POPN_SUBSCREEN_WINDOW = nullptr;
|
||||||
bool FAKE_SUBSCREEN_ADAPTER = false;
|
bool FAKE_SUBSCREEN_ADAPTER = false;
|
||||||
|
|
||||||
@@ -61,6 +64,7 @@ static std::condition_variable GRAPHICS_CAPTURE_CV[GRAPHICS_CAPTURE_SCREEN_NO] {
|
|||||||
|
|
||||||
static std::optional<graphics_orientation> target_orientation_on_boot;
|
static std::optional<graphics_orientation> target_orientation_on_boot;
|
||||||
static UINT target_refresh_rate_on_boot = 0;
|
static UINT target_refresh_rate_on_boot = 0;
|
||||||
|
static std::optional<std::pair<uint32_t, uint32_t>> target_resolution_on_boot;
|
||||||
static bool monitor_settings_changed = false;
|
static bool monitor_settings_changed = false;
|
||||||
static bool monitor_layout_needs_reset = false;
|
static bool monitor_layout_needs_reset = false;
|
||||||
|
|
||||||
@@ -75,7 +79,8 @@ UINT GRAPHICS_FORCE_REFRESH = 0;
|
|||||||
std::optional<uint32_t> GRAPHICS_FORCE_REFRESH_SUB;
|
std::optional<uint32_t> GRAPHICS_FORCE_REFRESH_SUB;
|
||||||
std::optional<int> GRAPHICS_FORCE_VSYNC_BUFFER;
|
std::optional<int> GRAPHICS_FORCE_VSYNC_BUFFER;
|
||||||
bool GRAPHICS_FORCE_SINGLE_ADAPTER = false;
|
bool GRAPHICS_FORCE_SINGLE_ADAPTER = false;
|
||||||
bool GRAPHICS_PREVENT_SECONDARY_WINDOW = false;
|
bool GRAPHICS_PREVENT_SECONDARY_WINDOWS = false;
|
||||||
|
bool GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS = false;
|
||||||
graphics_dx9on12_state GRAPHICS_9_ON_12_STATE = DX9ON12_AUTO;
|
graphics_dx9on12_state GRAPHICS_9_ON_12_STATE = DX9ON12_AUTO;
|
||||||
bool GRAPHICS_9_ON_12_REQUESTED_BY_GAME = false;
|
bool GRAPHICS_9_ON_12_REQUESTED_BY_GAME = false;
|
||||||
bool SUBSCREEN_FORCE_REDRAW = false;
|
bool SUBSCREEN_FORCE_REDRAW = false;
|
||||||
@@ -120,6 +125,128 @@ static void reset_window_hook(HWND hWnd) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static std::string gitadora_canonical_window_name(const std::string &window_name) {
|
||||||
|
if (window_name == "GITADORA") {
|
||||||
|
return "GITADORA";
|
||||||
|
}
|
||||||
|
if (window_name.ends_with("LEFT")) {
|
||||||
|
return "LEFT";
|
||||||
|
}
|
||||||
|
if (window_name.ends_with("RIGHT")) {
|
||||||
|
return "RIGHT";
|
||||||
|
}
|
||||||
|
if (window_name.ends_with("SMALL")) {
|
||||||
|
return "SMALL";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *gitadora_window_name_for_hwnd(HWND hWnd) {
|
||||||
|
if (hWnd == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (GRAPHICS_HOOKED_WINDOW.has_value() && hWnd == GRAPHICS_HOOKED_WINDOW.value()) {
|
||||||
|
return "GITADORA";
|
||||||
|
}
|
||||||
|
if (hWnd == GFDM_LEFT_WINDOW) {
|
||||||
|
return "LEFT";
|
||||||
|
}
|
||||||
|
if (hWnd == GFDM_RIGHT_WINDOW) {
|
||||||
|
return "RIGHT";
|
||||||
|
}
|
||||||
|
if (hWnd == GFDM_SUBSCREEN_WINDOW) {
|
||||||
|
return "SMALL";
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool is_gfdm_known_window(HWND hWnd) {
|
||||||
|
return gitadora_window_name_for_hwnd(hWnd) != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool gitadora_should_block_game_window_placement(HWND hWnd) {
|
||||||
|
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto window_name = gitadora_window_name_for_hwnd(hWnd);
|
||||||
|
return window_name != nullptr && graphics_gitadora_has_window_monitor(window_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void gitadora_remember_window(HWND hWnd, const std::string &window_name) {
|
||||||
|
if (window_name == "LEFT") {
|
||||||
|
GFDM_LEFT_WINDOW = hWnd;
|
||||||
|
} else if (window_name == "RIGHT") {
|
||||||
|
GFDM_RIGHT_WINDOW = hWnd;
|
||||||
|
} else if (window_name == "SMALL") {
|
||||||
|
GFDM_SUBSCREEN_WINDOW = hWnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool gitadora_should_allow_small_resize() {
|
||||||
|
return GRAPHICS_WINDOWED &&
|
||||||
|
games::gitadora::is_arena_model() &&
|
||||||
|
!graphics_gitadora_is_borderless_windowed();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void gitadora_apply_small_resize_style(DWORD &style) {
|
||||||
|
if (!gitadora_should_allow_small_resize()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
style |= WS_SIZEBOX;
|
||||||
|
style |= WS_MAXIMIZEBOX;
|
||||||
|
style |= WS_SYSMENU;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void gitadora_force_window_style(HWND hWnd) {
|
||||||
|
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model() || hWnd == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD style = GetWindowLongA(hWnd, GWL_STYLE);
|
||||||
|
DWORD style_ex = GetWindowLongA(hWnd, GWL_EXSTYLE);
|
||||||
|
const DWORD style_orig = style;
|
||||||
|
const DWORD style_ex_orig = style_ex;
|
||||||
|
graphics_gitadora_apply_window_style(style, style_ex);
|
||||||
|
if (hWnd == GFDM_SUBSCREEN_WINDOW) {
|
||||||
|
gitadora_apply_small_resize_style(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style == style_orig && style_ex == style_ex_orig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SetWindowLongA_orig != nullptr) {
|
||||||
|
SetWindowLongA_orig(hWnd, GWL_STYLE, static_cast<LONG>(style));
|
||||||
|
SetWindowLongA_orig(hWnd, GWL_EXSTYLE, static_cast<LONG>(style_ex));
|
||||||
|
} else {
|
||||||
|
SetWindowLongA(hWnd, GWL_STYLE, static_cast<LONG>(style));
|
||||||
|
SetWindowLongA(hWnd, GWL_EXSTYLE, static_cast<LONG>(style_ex));
|
||||||
|
}
|
||||||
|
|
||||||
|
const UINT flags =
|
||||||
|
SWP_NOMOVE |
|
||||||
|
SWP_NOSIZE |
|
||||||
|
SWP_NOZORDER |
|
||||||
|
SWP_NOACTIVATE |
|
||||||
|
SWP_FRAMECHANGED;
|
||||||
|
if (SetWindowPos_orig != nullptr) {
|
||||||
|
SetWindowPos_orig(hWnd, nullptr, 0, 0, 0, 0, flags);
|
||||||
|
} else {
|
||||||
|
SetWindowPos(hWnd, nullptr, 0, 0, 0, 0, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_misc(
|
||||||
|
"graphics",
|
||||||
|
"GITADORA window style override: hwnd={}, style 0x{:x}->0x{:x}, ex 0x{:x}->0x{:x}",
|
||||||
|
fmt::ptr(hWnd),
|
||||||
|
style_orig,
|
||||||
|
style,
|
||||||
|
style_ex_orig,
|
||||||
|
style_ex);
|
||||||
|
}
|
||||||
|
|
||||||
// window procedure
|
// window procedure
|
||||||
static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
|
static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
|
||||||
|
|
||||||
@@ -163,6 +290,7 @@ static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
|
|||||||
if (wintouchemu::INJECT_MOUSE_AS_WM_TOUCH) {
|
if (wintouchemu::INJECT_MOUSE_AS_WM_TOUCH) {
|
||||||
// drop mouse inputs since only wintouches should be used
|
// drop mouse inputs since only wintouches should be used
|
||||||
switch (uMsg) {
|
switch (uMsg) {
|
||||||
|
case WM_MOUSEMOVE:
|
||||||
case WM_LBUTTONDOWN:
|
case WM_LBUTTONDOWN:
|
||||||
case WM_LBUTTONUP:
|
case WM_LBUTTONUP:
|
||||||
case WM_MBUTTONDOWN:
|
case WM_MBUTTONDOWN:
|
||||||
@@ -280,6 +408,18 @@ static LRESULT CALLBACK WsubWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPAR
|
|||||||
log_misc("graphics", "ignore WM_CLOSE for subscreen window");
|
log_misc("graphics", "ignore WM_CLOSE for subscreen window");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hWnd == GFDM_SUBSCREEN_WINDOW && (uMsg == WM_MOVE || uMsg == WM_SIZE)) {
|
||||||
|
update_spicetouch_window_dimensions(hWnd);
|
||||||
|
if (SPICETOUCH_TOUCH_HWND) {
|
||||||
|
SetWindowPos(
|
||||||
|
SPICETOUCH_TOUCH_HWND, HWND_TOP,
|
||||||
|
SPICETOUCH_TOUCH_X, SPICETOUCH_TOUCH_Y,
|
||||||
|
SPICETOUCH_TOUCH_WIDTH, SPICETOUCH_TOUCH_HEIGHT,
|
||||||
|
SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return CallWindowProcA(WSUB_WNDPROC_ORIG, hWnd, uMsg, wParam, lParam);
|
return CallWindowProcA(WSUB_WNDPROC_ORIG, hWnd, uMsg, wParam, lParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,28 +483,55 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
|||||||
fmt::ptr(lpParam));
|
fmt::ptr(lpParam));
|
||||||
|
|
||||||
// gfdm
|
// gfdm
|
||||||
|
std::string effective_window_name = window_name;
|
||||||
if (avs::game::is_model({"J32", "J33", "K32", "K33", "L32", "L33", "M32"})) {
|
if (avs::game::is_model({"J32", "J33", "K32", "K33", "L32", "L33", "M32"})) {
|
||||||
// set window name
|
// set window name
|
||||||
if (!lpWindowName) {
|
if (!lpWindowName) {
|
||||||
lpWindowName = "GITADORA";
|
lpWindowName = "GITADORA";
|
||||||
|
effective_window_name = "GITADORA";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool is_tdj_sub_window = avs::game::is_model("LDJ") && window_name.ends_with(" sub");
|
bool is_tdj_sub_window = avs::game::is_model("LDJ") && window_name.ends_with(" sub");
|
||||||
bool is_sdvx_sub_window = avs::game::is_model("KFC") && window_name.ends_with(" Sub Screen");
|
bool is_sdvx_sub_window = avs::game::is_model("KFC") && window_name.ends_with(" Sub Screen");
|
||||||
bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen");
|
bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen");
|
||||||
bool is_gfdm_sub_window = games::gitadora::is_arena_model() && window_name.ends_with("SMALL");
|
const std::string gfdm_window_name = games::gitadora::is_arena_model()
|
||||||
|
? gitadora_canonical_window_name(effective_window_name)
|
||||||
|
: "";
|
||||||
|
const bool is_gfdm_window = !gfdm_window_name.empty();
|
||||||
|
const bool is_gfdm_sub_window = gfdm_window_name == "SMALL";
|
||||||
|
const bool allow_gfdm_small_resize =
|
||||||
|
is_gfdm_sub_window && gitadora_should_allow_small_resize();
|
||||||
|
|
||||||
// hide maximize button (prevent misaligned touches)
|
// update style / ex-style
|
||||||
if ((is_tdj_sub_window && GRAPHICS_IIDX_WSUB) || is_sdvx_sub_window || is_gfdm_sub_window || is_popn_sub_window) {
|
if (is_tdj_sub_window || is_sdvx_sub_window || is_gfdm_sub_window || is_popn_sub_window) {
|
||||||
dwStyle &= ~(WS_MAXIMIZEBOX);
|
// hide maximize button (prevent misaligned touches)
|
||||||
|
if (!allow_gfdm_small_resize) {
|
||||||
|
dwStyle &= ~(WS_MAXIMIZEBOX);
|
||||||
|
}
|
||||||
|
|
||||||
|
// mouse clicks become misaligned when resized
|
||||||
|
if (!allow_gfdm_small_resize) {
|
||||||
|
dwStyle &= ~(WS_SIZEBOX);
|
||||||
|
}
|
||||||
|
|
||||||
|
// borderless
|
||||||
|
if (GRAPHICS_WINDOWED && GRAPHICS_WSUB_BORDERLESS) {
|
||||||
|
dwStyle &= ~(WS_OVERLAPPEDWINDOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't show the sub window on task bar / alt-tab targets
|
||||||
|
if (!GRAPHICS_WINDOWED) {
|
||||||
|
dwExStyle &= ~(WS_EX_APPWINDOW);
|
||||||
|
dwExStyle |= WS_EX_TOOLWINDOW;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// mouse clicks become misaligned when resized
|
|
||||||
if (is_gfdm_sub_window) {
|
if (allow_gfdm_small_resize) {
|
||||||
dwStyle &= ~(WS_SIZEBOX);
|
gitadora_apply_small_resize_style(dwStyle);
|
||||||
}
|
}
|
||||||
if ((is_tdj_sub_window || is_sdvx_sub_window || is_popn_sub_window) && GRAPHICS_WSUB_BORDERLESS) {
|
if (is_gfdm_window) {
|
||||||
dwStyle &= ~(WS_OVERLAPPEDWINDOW);
|
graphics_gitadora_apply_window_style(dwStyle, dwExStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_sdvx_sub_window) {
|
if (is_sdvx_sub_window) {
|
||||||
@@ -385,6 +552,16 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (is_gfdm_window) {
|
||||||
|
graphics_gitadora_apply_window_monitor(
|
||||||
|
gfdm_window_name,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
nWidth,
|
||||||
|
nHeight,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
if (GRAPHICS_WINDOWED) {
|
if (GRAPHICS_WINDOWED) {
|
||||||
graphics_window_check_bounds_before_creation(x, y, nWidth, nHeight);
|
graphics_window_check_bounds_before_creation(x, y, nWidth, nHeight);
|
||||||
}
|
}
|
||||||
@@ -411,14 +588,20 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
|||||||
}
|
}
|
||||||
|
|
||||||
// only hook touch window if multiple windows are allowed
|
// only hook touch window if multiple windows are allowed
|
||||||
if (is_gfdm_sub_window && !games::gitadora::ARENA_SINGLE_WINDOW) {
|
if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") {
|
||||||
GFDM_SUBSCREEN_WINDOW = result;
|
gitadora_remember_window(result, gfdm_window_name);
|
||||||
|
}
|
||||||
|
if (is_gfdm_sub_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
|
gitadora_remember_window(result, gfdm_window_name);
|
||||||
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
|
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
|
||||||
}
|
}
|
||||||
|
if (is_gfdm_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
|
gitadora_force_window_style(result);
|
||||||
|
}
|
||||||
|
|
||||||
if (is_popn_sub_window) {
|
if (is_popn_sub_window) {
|
||||||
POPN_SUBSCREEN_WINDOW = result;
|
POPN_SUBSCREEN_WINDOW = result;
|
||||||
if (!GRAPHICS_PREVENT_SECONDARY_WINDOW) {
|
if (!GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
graphics_hook_subscreen_window(POPN_SUBSCREEN_WINDOW);
|
graphics_hook_subscreen_window(POPN_SUBSCREEN_WINDOW);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -603,6 +786,12 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Monitor overrides are applied at creation time. Suppress later game
|
||||||
|
// placement calls instead of resizing again during scene transitions.
|
||||||
|
if (gitadora_should_block_game_window_placement(hWnd)) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
// call original
|
// call original
|
||||||
return MoveWindow_orig(hWnd, X, Y, nWidth, nHeight, bRepaint);
|
return MoveWindow_orig(hWnd, X, Y, nWidth, nHeight, bRepaint);
|
||||||
}
|
}
|
||||||
@@ -656,8 +845,17 @@ static LONG WINAPI SetWindowLongA_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
|
|||||||
dwNewLong |= WS_OVERLAPPEDWINDOW;
|
dwNewLong |= WS_OVERLAPPEDWINDOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
// call original
|
const bool force_gfdm_style =
|
||||||
return SetWindowLongA_orig(hWnd, nIndex, dwNewLong);
|
GRAPHICS_WINDOWED &&
|
||||||
|
games::gitadora::is_arena_model() &&
|
||||||
|
is_gfdm_known_window(hWnd) &&
|
||||||
|
(nIndex == GWL_STYLE || nIndex == GWL_EXSTYLE);
|
||||||
|
|
||||||
|
const auto result = SetWindowLongA_orig(hWnd, nIndex, dwNewLong);
|
||||||
|
if (force_gfdm_style) {
|
||||||
|
gitadora_force_window_style(hWnd);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
|
static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
|
||||||
@@ -667,8 +865,17 @@ static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
|
|||||||
dwNewLong |= WS_OVERLAPPEDWINDOW;
|
dwNewLong |= WS_OVERLAPPEDWINDOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
// call original
|
const bool force_gfdm_style =
|
||||||
return SetWindowLongW_orig(hWnd, nIndex, dwNewLong);
|
GRAPHICS_WINDOWED &&
|
||||||
|
games::gitadora::is_arena_model() &&
|
||||||
|
is_gfdm_known_window(hWnd) &&
|
||||||
|
(nIndex == GWL_STYLE || nIndex == GWL_EXSTYLE);
|
||||||
|
|
||||||
|
const auto result = SetWindowLongW_orig(hWnd, nIndex, dwNewLong);
|
||||||
|
if (force_gfdm_style) {
|
||||||
|
gitadora_force_window_style(hWnd);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
|
static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
|
||||||
@@ -679,6 +886,13 @@ static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
|
|||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Monitor overrides are applied at creation time. Suppress later game
|
||||||
|
// placement calls instead of resizing again during scene transitions.
|
||||||
|
if (gitadora_should_block_game_window_placement(hWnd) &&
|
||||||
|
(uFlags & (SWP_NOMOVE | SWP_NOSIZE)) != (SWP_NOMOVE | SWP_NOSIZE)) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
// prevent gitadora arena model from shifting windows around if the user has preferences
|
// prevent gitadora arena model from shifting windows around if the user has preferences
|
||||||
if (GRAPHICS_WINDOWED && games::gitadora::is_arena_model() &&
|
if (GRAPHICS_WINDOWED && games::gitadora::is_arena_model() &&
|
||||||
GRAPHICS_HOOKED_WINDOW.has_value() && hWnd == GRAPHICS_HOOKED_WINDOW.value() &&
|
GRAPHICS_HOOKED_WINDOW.has_value() && hWnd == GRAPHICS_HOOKED_WINDOW.value() &&
|
||||||
@@ -692,14 +906,21 @@ static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
|
|||||||
|
|
||||||
static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
|
static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
|
||||||
if (games::gitadora::is_arena_model() &&
|
if (games::gitadora::is_arena_model() &&
|
||||||
games::gitadora::ARENA_SINGLE_WINDOW &&
|
GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
|
||||||
hWnd != GRAPHICS_HOOKED_WINDOW) {
|
hWnd != GRAPHICS_HOOKED_WINDOW) {
|
||||||
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
|
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (games::gitadora::is_arena_model() &&
|
||||||
|
GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS &&
|
||||||
|
(hWnd == GFDM_LEFT_WINDOW || hWnd == GFDM_RIGHT_WINDOW)) {
|
||||||
|
log_info("graphics", "ShowWindow_hook - hiding GITADORA side window {}", fmt::ptr(hWnd));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (games::popn::is_pikapika_model() &&
|
if (games::popn::is_pikapika_model() &&
|
||||||
GRAPHICS_PREVENT_SECONDARY_WINDOW &&
|
GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
|
||||||
hWnd == POPN_SUBSCREEN_WINDOW) {
|
hWnd == POPN_SUBSCREEN_WINDOW) {
|
||||||
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
|
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
|
||||||
return true;
|
return true;
|
||||||
@@ -831,6 +1052,7 @@ void graphics_init() {
|
|||||||
|
|
||||||
// init backends
|
// init backends
|
||||||
graphics_d3d9_init();
|
graphics_d3d9_init();
|
||||||
|
graphics_d3d11_init();
|
||||||
|
|
||||||
// general hooks
|
// general hooks
|
||||||
ChangeDisplaySettingsA_orig = detour::iat_try("ChangeDisplaySettingsA", ChangeDisplaySettingsA_hook);
|
ChangeDisplaySettingsA_orig = detour::iat_try("ChangeDisplaySettingsA", ChangeDisplaySettingsA_hook);
|
||||||
@@ -1278,8 +1500,13 @@ void change_primary_monitor(const std::string &monitor_name) {
|
|||||||
Sleep(2000);
|
Sleep(2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
void update_monitor(bool is_boot, std::optional<graphics_orientation> target_orientation, UINT target_refresh_rate) {
|
void update_monitor(
|
||||||
// note: all of this is only being done for the primary motnior
|
bool is_boot,
|
||||||
|
std::optional<graphics_orientation> target_orientation,
|
||||||
|
UINT target_refresh_rate,
|
||||||
|
std::optional<std::pair<uint32_t, uint32_t>> target_resolution) {
|
||||||
|
|
||||||
|
// note: all of this is only being done for the primary monitor
|
||||||
|
|
||||||
// get current settings
|
// get current settings
|
||||||
DEVMODEA dm = {};
|
DEVMODEA dm = {};
|
||||||
@@ -1357,6 +1584,22 @@ void update_monitor(bool is_boot, std::optional<graphics_orientation> target_ori
|
|||||||
needs_update = true;
|
needs_update = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (target_resolution.has_value()) {
|
||||||
|
if (dm.dmPelsWidth != target_resolution.value().first ||
|
||||||
|
dm.dmPelsHeight != target_resolution.value().second) {
|
||||||
|
|
||||||
|
log_misc(
|
||||||
|
"graphics",
|
||||||
|
"current resolution {}, {} => desired resolution {}, {}",
|
||||||
|
dm.dmPelsWidth, dm.dmPelsHeight,
|
||||||
|
target_resolution.value().first, target_resolution.value().second);
|
||||||
|
|
||||||
|
dm.dmPelsWidth = target_resolution.value().first;
|
||||||
|
dm.dmPelsHeight = target_resolution.value().second;
|
||||||
|
needs_update = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!needs_update) {
|
if (!needs_update) {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
log_misc("graphics", "display settings are already up to date, no changes needed");
|
log_misc("graphics", "display settings are already up to date, no changes needed");
|
||||||
@@ -1393,17 +1636,30 @@ void update_monitor(bool is_boot, std::optional<graphics_orientation> target_ori
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void update_monitor_on_boot(std::optional<graphics_orientation> target_orientation, UINT target_refresh_rate) {
|
void update_monitor_on_boot(
|
||||||
|
std::optional<graphics_orientation> target_orientation,
|
||||||
|
UINT target_refresh_rate,
|
||||||
|
std::optional<std::pair<uint32_t, uint32_t>> target_resolution) {
|
||||||
|
|
||||||
target_orientation_on_boot = target_orientation;
|
target_orientation_on_boot = target_orientation;
|
||||||
target_refresh_rate_on_boot = target_refresh_rate;
|
target_refresh_rate_on_boot = target_refresh_rate;
|
||||||
|
target_resolution_on_boot = target_resolution;
|
||||||
log_misc("graphics", "applying monitor updates at boot...");
|
log_misc("graphics", "applying monitor updates at boot...");
|
||||||
update_monitor(true, target_orientation, target_refresh_rate);
|
update_monitor(
|
||||||
|
true,
|
||||||
|
target_orientation,
|
||||||
|
target_refresh_rate,
|
||||||
|
target_resolution);
|
||||||
}
|
}
|
||||||
|
|
||||||
void update_monitor_at_runtime() {
|
void update_monitor_at_runtime() {
|
||||||
if (monitor_settings_changed) {
|
if (!GRAPHICS_WINDOWED && monitor_settings_changed) {
|
||||||
log_misc("graphics", "applying monitor updates at runtime as window regained focus...");
|
log_misc("graphics", "applying monitor updates at runtime as window regained focus...");
|
||||||
update_monitor(false, target_orientation_on_boot, target_refresh_rate_on_boot);
|
update_monitor(
|
||||||
|
false,
|
||||||
|
target_orientation_on_boot,
|
||||||
|
target_refresh_rate_on_boot,
|
||||||
|
target_resolution_on_boot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ extern UINT GRAPHICS_FORCE_REFRESH;
|
|||||||
extern std::optional<uint32_t> GRAPHICS_FORCE_REFRESH_SUB;
|
extern std::optional<uint32_t> GRAPHICS_FORCE_REFRESH_SUB;
|
||||||
extern std::optional<int> GRAPHICS_FORCE_VSYNC_BUFFER;
|
extern std::optional<int> GRAPHICS_FORCE_VSYNC_BUFFER;
|
||||||
extern bool GRAPHICS_FORCE_SINGLE_ADAPTER;
|
extern bool GRAPHICS_FORCE_SINGLE_ADAPTER;
|
||||||
extern bool GRAPHICS_PREVENT_SECONDARY_WINDOW;
|
extern bool GRAPHICS_PREVENT_SECONDARY_WINDOWS;
|
||||||
|
extern bool GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS;
|
||||||
extern std::optional<std::pair<uint32_t, uint32_t>> GRAPHICS_FS_CUSTOM_RESOLUTION;
|
extern std::optional<std::pair<uint32_t, uint32_t>> GRAPHICS_FS_CUSTOM_RESOLUTION;
|
||||||
extern std::optional<std::pair<uint32_t, uint32_t>> GRAPHICS_FS_CUSTOM_RESOLUTION_SUB;
|
extern std::optional<std::pair<uint32_t, uint32_t>> GRAPHICS_FS_CUSTOM_RESOLUTION_SUB;
|
||||||
extern bool GRAPHICS_FS_ORIENTATION_SWAP;
|
extern bool GRAPHICS_FS_ORIENTATION_SWAP;
|
||||||
@@ -55,6 +56,10 @@ extern bool GRAPHICS_WINDOW_ALWAYS_ON_TOP;
|
|||||||
extern bool GRAPHICS_WINDOW_BACKBUFFER_SCALE;
|
extern bool GRAPHICS_WINDOW_BACKBUFFER_SCALE;
|
||||||
extern bool GRAPHICS_WINDOW_DISABLE_ROUNDED_CORNERS;
|
extern bool GRAPHICS_WINDOW_DISABLE_ROUNDED_CORNERS;
|
||||||
extern std::optional<HWND> GRAPHICS_HOOKED_WINDOW;
|
extern std::optional<HWND> GRAPHICS_HOOKED_WINDOW;
|
||||||
|
extern std::string GRAPHICS_GITADORA_MAIN_MONITOR;
|
||||||
|
extern std::string GRAPHICS_GITADORA_LEFT_MONITOR;
|
||||||
|
extern std::string GRAPHICS_GITADORA_RIGHT_MONITOR;
|
||||||
|
extern std::string GRAPHICS_GITADORA_SMALL_MONITOR;
|
||||||
|
|
||||||
extern bool GRAPHICS_IIDX_WSUB;
|
extern bool GRAPHICS_IIDX_WSUB;
|
||||||
extern std::optional<std::string> GRAPHICS_WSUB_SIZE;
|
extern std::optional<std::string> GRAPHICS_WSUB_SIZE;
|
||||||
@@ -115,8 +120,22 @@ bool graphics_window_resize_breaks_game();
|
|||||||
bool graphics_window_move_and_resize_breaks_game();
|
bool graphics_window_move_and_resize_breaks_game();
|
||||||
void graphics_load_windowed_subscreen_parameters();
|
void graphics_load_windowed_subscreen_parameters();
|
||||||
void graphics_window_check_bounds_before_creation(int &x, int &y, const int width, const int height);
|
void graphics_window_check_bounds_before_creation(int &x, int &y, const int width, const int height);
|
||||||
|
bool graphics_gitadora_is_borderless_windowed();
|
||||||
|
void graphics_gitadora_apply_window_style(DWORD &style, DWORD &style_ex);
|
||||||
|
bool graphics_gitadora_apply_window_monitor(
|
||||||
|
const std::string &window_name,
|
||||||
|
int &x,
|
||||||
|
int &y,
|
||||||
|
int &width,
|
||||||
|
int &height,
|
||||||
|
bool log_change);
|
||||||
|
bool graphics_gitadora_has_window_monitor(const std::string &window_name);
|
||||||
|
|
||||||
void change_primary_monitor(const std::string &monitor_name);
|
void change_primary_monitor(const std::string &monitor_name);
|
||||||
void update_monitor_on_boot(std::optional<graphics_orientation> target_orientation, UINT target_refresh_rate);
|
void update_monitor_on_boot(
|
||||||
|
std::optional<graphics_orientation> target_orientation,
|
||||||
|
UINT target_refresh_rate,
|
||||||
|
std::optional<std::pair<uint32_t, uint32_t>> target_resolution);
|
||||||
|
|
||||||
void update_monitor_at_runtime();
|
void update_monitor_at_runtime();
|
||||||
void reset_monitor_on_exit();
|
void reset_monitor_on_exit();
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ bool GRAPHICS_WINDOW_ALWAYS_ON_TOP = false;
|
|||||||
bool GRAPHICS_WINDOW_BACKBUFFER_SCALE = false;
|
bool GRAPHICS_WINDOW_BACKBUFFER_SCALE = false;
|
||||||
bool GRAPHICS_WINDOW_DISABLE_ROUNDED_CORNERS = false;
|
bool GRAPHICS_WINDOW_DISABLE_ROUNDED_CORNERS = false;
|
||||||
std::optional<HWND> GRAPHICS_HOOKED_WINDOW;
|
std::optional<HWND> GRAPHICS_HOOKED_WINDOW;
|
||||||
|
std::string GRAPHICS_GITADORA_MAIN_MONITOR;
|
||||||
|
std::string GRAPHICS_GITADORA_LEFT_MONITOR;
|
||||||
|
std::string GRAPHICS_GITADORA_RIGHT_MONITOR;
|
||||||
|
std::string GRAPHICS_GITADORA_SMALL_MONITOR;
|
||||||
|
|
||||||
// IIDX Windowed Subscreen - starts out as false, enabled by IIDX module on pre-attach as needed
|
// IIDX Windowed Subscreen - starts out as false, enabled by IIDX module on pre-attach as needed
|
||||||
bool GRAPHICS_IIDX_WSUB = false;
|
bool GRAPHICS_IIDX_WSUB = false;
|
||||||
@@ -51,6 +55,173 @@ static const DWORD SETWINDOWPOS_NOOP =
|
|||||||
SWP_NOZORDER |
|
SWP_NOZORDER |
|
||||||
SWP_ASYNCWINDOWPOS;
|
SWP_ASYNCWINDOWPOS;
|
||||||
|
|
||||||
|
static int graphics_effective_window_decoration() {
|
||||||
|
if (GRAPHICS_WINDOW_STYLE.has_value()) {
|
||||||
|
return GRAPHICS_WINDOW_STYLE.value();
|
||||||
|
}
|
||||||
|
if (cfg::SCREENRESIZE != nullptr) {
|
||||||
|
return cfg::SCREENRESIZE->window_decoration;
|
||||||
|
}
|
||||||
|
return cfg::WindowDecorationMode::Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool graphics_gitadora_is_borderless_windowed() {
|
||||||
|
return GRAPHICS_WINDOWED &&
|
||||||
|
graphics_effective_window_decoration() == cfg::WindowDecorationMode::Borderless;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const std::string &graphics_gitadora_monitor_for_window_name(
|
||||||
|
const std::string &window_name) {
|
||||||
|
if (window_name == "GITADORA") {
|
||||||
|
return GRAPHICS_GITADORA_MAIN_MONITOR;
|
||||||
|
}
|
||||||
|
if (window_name == "LEFT") {
|
||||||
|
return GRAPHICS_GITADORA_LEFT_MONITOR;
|
||||||
|
}
|
||||||
|
if (window_name == "RIGHT") {
|
||||||
|
return GRAPHICS_GITADORA_RIGHT_MONITOR;
|
||||||
|
}
|
||||||
|
if (window_name == "SMALL") {
|
||||||
|
return GRAPHICS_GITADORA_SMALL_MONITOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const std::string empty;
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool graphics_gitadora_has_window_monitor(const std::string &window_name) {
|
||||||
|
const auto &monitor_name = graphics_gitadora_monitor_for_window_name(window_name);
|
||||||
|
return !monitor_name.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool graphics_monitor_rect_from_name(
|
||||||
|
const std::string &monitor_name,
|
||||||
|
RECT &rect,
|
||||||
|
bool log_change) {
|
||||||
|
DEVMODEA devmode {};
|
||||||
|
devmode.dmSize = sizeof(devmode);
|
||||||
|
if (!EnumDisplaySettingsA(monitor_name.c_str(), ENUM_CURRENT_SETTINGS, &devmode)) {
|
||||||
|
if (log_change) {
|
||||||
|
log_warning(
|
||||||
|
"graphics-windowed",
|
||||||
|
"failed to get monitor settings for {}",
|
||||||
|
monitor_name);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rect.left = devmode.dmPosition.x;
|
||||||
|
rect.top = devmode.dmPosition.y;
|
||||||
|
rect.right = rect.left + static_cast<LONG>(devmode.dmPelsWidth);
|
||||||
|
rect.bottom = rect.top + static_cast<LONG>(devmode.dmPelsHeight);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool graphics_gitadora_apply_monitor_rect(
|
||||||
|
const std::string &monitor_name,
|
||||||
|
const std::string &window_name,
|
||||||
|
int &x,
|
||||||
|
int &y,
|
||||||
|
int &width,
|
||||||
|
int &height,
|
||||||
|
bool log_change) {
|
||||||
|
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (monitor_name.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT rect {};
|
||||||
|
if (!graphics_monitor_rect_from_name(monitor_name, rect, log_change)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
x = rect.left;
|
||||||
|
y = rect.top;
|
||||||
|
width = rect.right - rect.left;
|
||||||
|
height = rect.bottom - rect.top;
|
||||||
|
|
||||||
|
if (log_change) {
|
||||||
|
log_info(
|
||||||
|
"graphics-windowed",
|
||||||
|
"GITADORA {} monitor override: {} => pos=({}, {}), size={}x{}",
|
||||||
|
window_name,
|
||||||
|
monitor_name,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool graphics_gitadora_apply_main_monitor_to_config() {
|
||||||
|
if (cfg::SCREENRESIZE == nullptr || !graphics_gitadora_is_borderless_windowed()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
if (!graphics_gitadora_apply_monitor_rect(
|
||||||
|
GRAPHICS_GITADORA_MAIN_MONITOR,
|
||||||
|
"GITADORA",
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
false)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg::SCREENRESIZE->enable_window_resize = true;
|
||||||
|
cfg::SCREENRESIZE->client_keep_aspect_ratio = false;
|
||||||
|
cfg::SCREENRESIZE->window_offset_x = x;
|
||||||
|
cfg::SCREENRESIZE->window_offset_y = y;
|
||||||
|
cfg::SCREENRESIZE->client_width = width;
|
||||||
|
cfg::SCREENRESIZE->client_height = height;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void graphics_gitadora_apply_window_style(DWORD &style, DWORD &style_ex) {
|
||||||
|
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (graphics_effective_window_decoration()) {
|
||||||
|
case cfg::WindowDecorationMode::Borderless:
|
||||||
|
style &= ~WS_OVERLAPPEDWINDOW;
|
||||||
|
style_ex &= ~WS_EX_CLIENTEDGE;
|
||||||
|
style_ex &= ~WS_EX_WINDOWEDGE;
|
||||||
|
break;
|
||||||
|
case cfg::WindowDecorationMode::ResizableFrame:
|
||||||
|
style |= WS_OVERLAPPEDWINDOW;
|
||||||
|
break;
|
||||||
|
case cfg::WindowDecorationMode::Default:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool graphics_gitadora_apply_window_monitor(
|
||||||
|
const std::string &window_name,
|
||||||
|
int &x,
|
||||||
|
int &y,
|
||||||
|
int &width,
|
||||||
|
int &height,
|
||||||
|
bool log_change) {
|
||||||
|
return graphics_gitadora_apply_monitor_rect(
|
||||||
|
graphics_gitadora_monitor_for_window_name(window_name),
|
||||||
|
window_name,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
log_change);
|
||||||
|
}
|
||||||
|
|
||||||
void graphics_capture_initial_window(HWND hWnd) {
|
void graphics_capture_initial_window(HWND hWnd) {
|
||||||
if (!GRAPHICS_WINDOWED) {
|
if (!GRAPHICS_WINDOWED) {
|
||||||
return;
|
return;
|
||||||
@@ -181,6 +352,8 @@ void graphics_load_windowed_parameters() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
graphics_gitadora_apply_main_monitor_to_config();
|
||||||
|
|
||||||
// only override if true; don't stomp on user setting
|
// only override if true; don't stomp on user setting
|
||||||
if (GRAPHICS_WINDOW_ALWAYS_ON_TOP) {
|
if (GRAPHICS_WINDOW_ALWAYS_ON_TOP) {
|
||||||
cfg::SCREENRESIZE->window_always_on_top = true;
|
cfg::SCREENRESIZE->window_always_on_top = true;
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#include <winsock2.h>
|
||||||
|
#include <windows.h>
|
||||||
|
#include <iptypes.h>
|
||||||
|
#include <iphlpapi.h>
|
||||||
|
#include <icmpapi.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "util/detour.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
struct IcmpHdr {
|
||||||
|
uint8_t type;
|
||||||
|
uint8_t code;
|
||||||
|
uint16_t checksum_be;
|
||||||
|
};
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
decltype(IcmpSendEcho) *IcmpSendEcho_orig = nullptr;
|
||||||
|
|
||||||
|
DWORD WINAPI IcmpSendEcho_hook(
|
||||||
|
HANDLE IcmpHandle,
|
||||||
|
IPAddr DestinationAddress,
|
||||||
|
LPVOID RequestData,
|
||||||
|
WORD RequestSize,
|
||||||
|
PIP_OPTION_INFORMATION RequestOptions,
|
||||||
|
LPVOID ReplyBuffer,
|
||||||
|
DWORD ReplySize,
|
||||||
|
DWORD Timeout) {
|
||||||
|
if (!IcmpSendEcho_orig) {
|
||||||
|
SetLastError(ERROR_INVALID_FUNCTION);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
DWORD n = IcmpSendEcho_orig(
|
||||||
|
IcmpHandle,
|
||||||
|
DestinationAddress,
|
||||||
|
RequestData,
|
||||||
|
RequestSize,
|
||||||
|
RequestOptions,
|
||||||
|
ReplyBuffer,
|
||||||
|
ReplySize,
|
||||||
|
Timeout);
|
||||||
|
if (n != 0) {
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
if (ReplyBuffer == nullptr || ReplySize < sizeof(ICMP_ECHO_REPLY) || RequestData == nullptr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (RequestSize < sizeof(IcmpHdr)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const auto *req = reinterpret_cast<const uint8_t *>(RequestData);
|
||||||
|
const auto *ih = reinterpret_cast<const IcmpHdr *>(req);
|
||||||
|
if (ih->type != 8 || ih->code != 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(ReplyBuffer, 0, ReplySize);
|
||||||
|
auto *reply = reinterpret_cast<ICMP_ECHO_REPLY *>(ReplyBuffer);
|
||||||
|
const size_t data_off = offsetof(ICMP_ECHO_REPLY, Data);
|
||||||
|
const size_t avail = ReplySize > data_off ? ReplySize - data_off : 0;
|
||||||
|
const size_t copy_len = (std::min)(static_cast<size_t>(RequestSize), avail);
|
||||||
|
reply->Address = DestinationAddress;
|
||||||
|
reply->Status = IP_SUCCESS;
|
||||||
|
reply->RoundTripTime = 1;
|
||||||
|
reply->DataSize = static_cast<USHORT>(copy_len);
|
||||||
|
if (copy_len > 0) {
|
||||||
|
memcpy(reinterpret_cast<uint8_t *>(ReplyBuffer) + data_off, RequestData, copy_len);
|
||||||
|
}
|
||||||
|
SetLastError(ERROR_SUCCESS);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void icmphook_iphlpapi_install() {
|
||||||
|
static bool done = false;
|
||||||
|
if (done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
done = true;
|
||||||
|
if (!detour::trampoline_try(
|
||||||
|
"iphlpapi.dll",
|
||||||
|
"IcmpSendEcho",
|
||||||
|
(void *) IcmpSendEcho_hook,
|
||||||
|
(void **) &IcmpSendEcho_orig)) {
|
||||||
|
log_warning("network", "ICMP emulation: IcmpSendEcho hook was not installed");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,692 @@
|
|||||||
|
#include "icmphook_net.h"
|
||||||
|
|
||||||
|
#include "util/detour.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
#include <iphlpapi.h>
|
||||||
|
#include <mutex>
|
||||||
|
#include <deque>
|
||||||
|
#include <vector>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstring>
|
||||||
|
#include <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
void icmphook_iphlpapi_install();
|
||||||
|
|
||||||
|
#ifndef SPICE64
|
||||||
|
using socket_uint = unsigned int;
|
||||||
|
#else
|
||||||
|
using socket_uint = unsigned long long;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace icmphook_internal {
|
||||||
|
|
||||||
|
constexpr uint32_t k_loopback_host = 0x7f000001u;
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
struct IcmpHeaderView {
|
||||||
|
uint8_t type;
|
||||||
|
uint8_t code;
|
||||||
|
uint16_t checksum_be;
|
||||||
|
};
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
bool icmp_packet_valid(const uint8_t *icmp, size_t len) {
|
||||||
|
if (len < sizeof(IcmpHeaderView)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
unsigned sum = 0;
|
||||||
|
for (size_t i = 0; i + 1 < len; i += 2) {
|
||||||
|
sum += (static_cast<unsigned>(icmp[i]) << 8) | icmp[i + 1];
|
||||||
|
sum = (sum >> 16) + (sum & 0xFFFF);
|
||||||
|
}
|
||||||
|
if (len & 1) {
|
||||||
|
sum += static_cast<unsigned>(icmp[len - 1]) << 8;
|
||||||
|
sum = (sum >> 16) + (sum & 0xFFFF);
|
||||||
|
}
|
||||||
|
sum = (sum >> 16) + (sum & 0xFFFF);
|
||||||
|
return sum == 0xFFFFu || sum == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool icmp_build_echo_reply(std::vector<uint8_t> &out, const uint8_t *req, size_t req_len) {
|
||||||
|
if (req_len < sizeof(IcmpHeaderView)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.resize(req_len);
|
||||||
|
memcpy(out.data(), req, req_len);
|
||||||
|
auto *hdr = reinterpret_cast<IcmpHeaderView *>(out.data());
|
||||||
|
hdr->type = 0;
|
||||||
|
hdr->code = 0;
|
||||||
|
hdr->checksum_be = 0;
|
||||||
|
unsigned sum = 0;
|
||||||
|
for (size_t i = 0; i + 1 < out.size(); i += 2) {
|
||||||
|
sum += (static_cast<unsigned>(out[i]) << 8) | out[i + 1];
|
||||||
|
sum = (sum >> 16) + (sum & 0xFFFF);
|
||||||
|
}
|
||||||
|
if (out.size() & 1) {
|
||||||
|
sum += static_cast<unsigned>(out[out.size() - 1]) << 8;
|
||||||
|
sum = (sum >> 16) + (sum & 0xFFFF);
|
||||||
|
}
|
||||||
|
sum = (sum >> 16) + (sum & 0xFFFF);
|
||||||
|
uint16_t csum = static_cast<uint16_t>(~sum);
|
||||||
|
hdr->checksum_be = htons(csum);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool icmp_fix_pointer(
|
||||||
|
const uint8_t *base,
|
||||||
|
size_t total_len,
|
||||||
|
const uint8_t **icmp_start,
|
||||||
|
size_t *icmp_len) {
|
||||||
|
if (total_len >= 20 && (base[0] >> 4) == 4) {
|
||||||
|
size_t ihl = (base[0] & 0x0Fu) * 4;
|
||||||
|
if (ihl < 20 || ihl > total_len || base[9] != IPPROTO_ICMP) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*icmp_start = base + ihl;
|
||||||
|
*icmp_len = total_len - ihl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
*icmp_start = base;
|
||||||
|
*icmp_len = total_len;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ipv4_encode_datagram(
|
||||||
|
std::vector<uint8_t> &out,
|
||||||
|
uint32_t src_host_order,
|
||||||
|
uint32_t dst_host_order,
|
||||||
|
const uint8_t *payload,
|
||||||
|
size_t payload_len) {
|
||||||
|
const size_t hdr_len = 20;
|
||||||
|
if (payload_len > 0xFFFFu - hdr_len) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto total_len = static_cast<uint16_t>(hdr_len + payload_len);
|
||||||
|
out.resize(hdr_len + payload_len);
|
||||||
|
uint8_t *p = out.data();
|
||||||
|
memset(p, 0, hdr_len);
|
||||||
|
p[0] = 0x45;
|
||||||
|
p[1] = 0;
|
||||||
|
p[2] = static_cast<uint8_t>(total_len >> 8);
|
||||||
|
p[3] = static_cast<uint8_t>(total_len & 0xFF);
|
||||||
|
p[6] = 0x40;
|
||||||
|
p[8] = 1;
|
||||||
|
p[9] = IPPROTO_ICMP;
|
||||||
|
uint32_t src_wire = htonl(src_host_order);
|
||||||
|
uint32_t dst_wire = htonl(dst_host_order);
|
||||||
|
memcpy(p + 12, &src_wire, 4);
|
||||||
|
memcpy(p + 16, &dst_wire, 4);
|
||||||
|
unsigned sum = 0;
|
||||||
|
for (size_t i = 0; i < hdr_len; i += 2) {
|
||||||
|
sum += (static_cast<unsigned>(p[i]) << 8) | p[i + 1];
|
||||||
|
while (sum >> 16) {
|
||||||
|
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uint16_t hdr_csum = static_cast<uint16_t>(~sum);
|
||||||
|
p[10] = static_cast<uint8_t>(hdr_csum >> 8);
|
||||||
|
p[11] = static_cast<uint8_t>(hdr_csum & 0xFF);
|
||||||
|
memcpy(p + hdr_len, payload, payload_len);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cached adapter list. Populated once per process: adapter changes after the
|
||||||
|
// first ICMP send are intentionally not picked up, in exchange for avoiding
|
||||||
|
// repeated IP Helper calls on every emulated send.
|
||||||
|
std::once_flag g_adapters_cache_init;
|
||||||
|
std::vector<uint8_t> g_adapters_cache;
|
||||||
|
|
||||||
|
void fill_adapters_cache() {
|
||||||
|
std::call_once(g_adapters_cache_init, []() {
|
||||||
|
ULONG buf_len = 0;
|
||||||
|
DWORD r = GetAdaptersInfo(nullptr, &buf_len);
|
||||||
|
if (r != ERROR_BUFFER_OVERFLOW && r != ERROR_SUCCESS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (buf_len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_adapters_cache.resize(buf_len);
|
||||||
|
auto *info = reinterpret_cast<PIP_ADAPTER_INFO>(g_adapters_cache.data());
|
||||||
|
if (GetAdaptersInfo(info, &buf_len) != ERROR_SUCCESS) {
|
||||||
|
g_adapters_cache.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t local_ipv4_for_peer(
|
||||||
|
uint32_t peer_host_order,
|
||||||
|
uint32_t bind_pref_host_order,
|
||||||
|
bool has_bind) {
|
||||||
|
if (peer_host_order == k_loopback_host) {
|
||||||
|
return k_loopback_host;
|
||||||
|
}
|
||||||
|
if (has_bind && bind_pref_host_order != 0) {
|
||||||
|
return bind_pref_host_order;
|
||||||
|
}
|
||||||
|
fill_adapters_cache();
|
||||||
|
if (g_adapters_cache.empty()) {
|
||||||
|
return peer_host_order;
|
||||||
|
}
|
||||||
|
auto *info = reinterpret_cast<PIP_ADAPTER_INFO>(g_adapters_cache.data());
|
||||||
|
for (PIP_ADAPTER_INFO a = info; a != nullptr; a = a->Next) {
|
||||||
|
uint32_t ip = ntohl(inet_addr(a->IpAddressList.IpAddress.String));
|
||||||
|
uint32_t mask = ntohl(inet_addr(a->IpAddressList.IpMask.String));
|
||||||
|
if (mask == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((peer_host_order & mask) == (ip & mask)) {
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return peer_host_order;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EmuSock {
|
||||||
|
std::deque<std::vector<uint8_t>> queue;
|
||||||
|
std::condition_variable_any cv;
|
||||||
|
DWORD rx_timeout_ms = INFINITE;
|
||||||
|
bool nonblocking = false;
|
||||||
|
bool has_bind = false;
|
||||||
|
uint32_t bind_ip_host = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::recursive_mutex g_mu;
|
||||||
|
std::unordered_map<SOCKET, std::unique_ptr<EmuSock>> g_socks;
|
||||||
|
std::atomic<uint32_t> g_seq{1};
|
||||||
|
|
||||||
|
// Fast-path flag so the bind() hook in networkhook.cpp can short-circuit
|
||||||
|
// without taking g_mu when the ICMP feature was never enabled.
|
||||||
|
std::atomic<bool> g_installed{false};
|
||||||
|
|
||||||
|
decltype(socket) *socket_orig = nullptr;
|
||||||
|
decltype(WSASocketW) *WSASocketW_orig = nullptr;
|
||||||
|
decltype(WSASocketA) *WSASocketA_orig = nullptr;
|
||||||
|
decltype(closesocket) *closesocket_orig = nullptr;
|
||||||
|
decltype(bind) *bind_trampoline_orig = nullptr;
|
||||||
|
decltype(sendto) *sendto_orig = nullptr;
|
||||||
|
decltype(recvfrom) *recvfrom_orig = nullptr;
|
||||||
|
decltype(WSASendTo) *WSASendTo_orig = nullptr;
|
||||||
|
decltype(WSARecvFrom) *WSARecvFrom_orig = nullptr;
|
||||||
|
decltype(ioctlsocket) *ioctlsocket_orig = nullptr;
|
||||||
|
decltype(setsockopt) *setsockopt_orig = nullptr;
|
||||||
|
|
||||||
|
EmuSock *lookup(SOCKET s) {
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(g_mu);
|
||||||
|
auto it = g_socks.find(s);
|
||||||
|
if (it == g_socks.end()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return it->second.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
SOCKET alloc_icmp_socket() {
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(g_mu);
|
||||||
|
for (;;) {
|
||||||
|
uint32_t x = g_seq.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
SOCKET s = (SOCKET)(socket_uint)(0xE0000000u | (x & 0x0FFFFFFFu));
|
||||||
|
if (s == INVALID_SOCKET) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (g_socks.find(s) == g_socks.end()) {
|
||||||
|
g_socks[s] = std::make_unique<EmuSock>();
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void release_icmp_socket(SOCKET s) {
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(g_mu);
|
||||||
|
auto it = g_socks.find(s);
|
||||||
|
if (it != g_socks.end()) {
|
||||||
|
it->second->cv.notify_all();
|
||||||
|
g_socks.erase(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool process_icmp_send(SOCKET s, const uint8_t *buf, int len, const sockaddr *to, int tolen) {
|
||||||
|
if (len <= 0 || tolen < (int) sizeof(sockaddr_in) || !buf || !to) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
auto *sin = reinterpret_cast<const sockaddr_in *>(to);
|
||||||
|
if (sin->sin_family != AF_INET) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
uint32_t peer_host = ntohl(sin->sin_addr.s_addr);
|
||||||
|
|
||||||
|
const uint8_t *icmp_ptr = nullptr;
|
||||||
|
size_t icmp_len = 0;
|
||||||
|
const auto *raw = reinterpret_cast<const uint8_t *>(buf);
|
||||||
|
if (!icmp_fix_pointer(raw, static_cast<size_t>(len), &icmp_ptr, &icmp_len)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!icmp_packet_valid(icmp_ptr, icmp_len)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (icmp_len < sizeof(IcmpHeaderView)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const auto *ih = reinterpret_cast<const IcmpHeaderView *>(icmp_ptr);
|
||||||
|
if (ih->type != 8 || ih->code != 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> icmp_reply;
|
||||||
|
if (!icmp_build_echo_reply(icmp_reply, icmp_ptr, icmp_len)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> packet;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(g_mu);
|
||||||
|
auto it = g_socks.find(s);
|
||||||
|
if (it == g_socks.end()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
EmuSock *es = it->second.get();
|
||||||
|
uint32_t local_host = local_ipv4_for_peer(peer_host, es->bind_ip_host, es->has_bind);
|
||||||
|
if (!ipv4_encode_datagram(
|
||||||
|
packet, peer_host, local_host, icmp_reply.data(), icmp_reply.size())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
es->queue.push_back(std::move(packet));
|
||||||
|
es->cv.notify_all();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SOCKET WINAPI socket_hook(int af, int type, int protocol) {
|
||||||
|
if (af == AF_INET && type == SOCK_RAW && protocol == IPPROTO_ICMP) {
|
||||||
|
SOCKET s = alloc_icmp_socket();
|
||||||
|
log_info("network", "ICMP emulation: allocated raw socket {}", (socket_uint) s);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return socket_orig(af, type, protocol);
|
||||||
|
}
|
||||||
|
|
||||||
|
SOCKET WINAPI WSASocketW_hook(
|
||||||
|
int af,
|
||||||
|
int type,
|
||||||
|
int protocol,
|
||||||
|
LPWSAPROTOCOL_INFOW lpProtocolInfo,
|
||||||
|
GROUP g,
|
||||||
|
DWORD dwFlags) {
|
||||||
|
const bool is_raw_icmp =
|
||||||
|
af == AF_INET && type == SOCK_RAW && protocol == IPPROTO_ICMP;
|
||||||
|
if (lpProtocolInfo == nullptr && is_raw_icmp) {
|
||||||
|
SOCKET s = alloc_icmp_socket();
|
||||||
|
log_info("network", "ICMP emulation: allocated raw WSASocketW {}", (socket_uint) s);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return WSASocketW_orig(af, type, protocol, lpProtocolInfo, g, dwFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
SOCKET WINAPI WSASocketA_hook(
|
||||||
|
int af,
|
||||||
|
int type,
|
||||||
|
int protocol,
|
||||||
|
LPWSAPROTOCOL_INFOA lpProtocolInfo,
|
||||||
|
GROUP g,
|
||||||
|
DWORD dwFlags) {
|
||||||
|
const bool is_raw_icmp =
|
||||||
|
af == AF_INET && type == SOCK_RAW && protocol == IPPROTO_ICMP;
|
||||||
|
if (lpProtocolInfo == nullptr && is_raw_icmp) {
|
||||||
|
SOCKET s = alloc_icmp_socket();
|
||||||
|
log_info("network", "ICMP emulation: allocated raw WSASocketA {}", (socket_uint) s);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return WSASocketA_orig(af, type, protocol, lpProtocolInfo, g, dwFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
int WINAPI closesocket_hook(SOCKET s) {
|
||||||
|
if (lookup(s)) {
|
||||||
|
release_icmp_socket(s);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return closesocket_orig(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
int WINAPI bind_hook_ws2(SOCKET s, const sockaddr *name, int namelen) {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(g_mu);
|
||||||
|
auto it = g_socks.find(s);
|
||||||
|
if (it != g_socks.end() && name && namelen >= (int) sizeof(sockaddr_in)) {
|
||||||
|
auto *in = reinterpret_cast<const sockaddr_in *>(name);
|
||||||
|
if (in->sin_family != AF_INET) {
|
||||||
|
WSASetLastError(WSAEAFNOSUPPORT);
|
||||||
|
return SOCKET_ERROR;
|
||||||
|
}
|
||||||
|
it->second->has_bind = true;
|
||||||
|
it->second->bind_ip_host = ntohl(in->sin_addr.s_addr);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bind_trampoline_orig(s, name, namelen);
|
||||||
|
}
|
||||||
|
|
||||||
|
int WINAPI sendto_hook(
|
||||||
|
SOCKET s, const char *buf, int len, int flags, const sockaddr *to, int tolen) {
|
||||||
|
EmuSock *es = lookup(s);
|
||||||
|
if (!es) {
|
||||||
|
return sendto_orig(s, buf, len, flags, to, tolen);
|
||||||
|
}
|
||||||
|
if (!buf || !to) {
|
||||||
|
WSASetLastError(WSAEFAULT);
|
||||||
|
return SOCKET_ERROR;
|
||||||
|
}
|
||||||
|
process_icmp_send(s, reinterpret_cast<const uint8_t *>(buf), len, to, tolen);
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
int WINAPI WSASendTo_hook(
|
||||||
|
SOCKET s,
|
||||||
|
LPWSABUF lpBuffers,
|
||||||
|
DWORD dwBufferCount,
|
||||||
|
LPDWORD lpNumberOfBytesSent,
|
||||||
|
DWORD dwFlags,
|
||||||
|
const sockaddr *lpTo,
|
||||||
|
int iTolen,
|
||||||
|
LPWSAOVERLAPPED lpOverlapped,
|
||||||
|
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine) {
|
||||||
|
EmuSock *es = lookup(s);
|
||||||
|
if (!es) {
|
||||||
|
return WSASendTo_orig(
|
||||||
|
s,
|
||||||
|
lpBuffers,
|
||||||
|
dwBufferCount,
|
||||||
|
lpNumberOfBytesSent,
|
||||||
|
dwFlags,
|
||||||
|
lpTo,
|
||||||
|
iTolen,
|
||||||
|
lpOverlapped,
|
||||||
|
lpCompletionRoutine);
|
||||||
|
}
|
||||||
|
if (lpOverlapped != nullptr) {
|
||||||
|
WSASetLastError(WSAEOPNOTSUPP);
|
||||||
|
return SOCKET_ERROR;
|
||||||
|
}
|
||||||
|
if (dwBufferCount != 1 || !lpBuffers || !lpNumberOfBytesSent) {
|
||||||
|
WSASetLastError(WSAEINVAL);
|
||||||
|
return SOCKET_ERROR;
|
||||||
|
}
|
||||||
|
int len = (int) lpBuffers[0].len;
|
||||||
|
process_icmp_send(
|
||||||
|
s,
|
||||||
|
reinterpret_cast<const uint8_t *>(lpBuffers[0].buf),
|
||||||
|
len,
|
||||||
|
lpTo,
|
||||||
|
iTolen);
|
||||||
|
*lpNumberOfBytesSent = static_cast<DWORD>(len);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dequeue one datagram for an emulated ICMP socket into the caller-provided
|
||||||
|
// buffer. If out_full_dgram_bytes is non-null, it receives the full packet
|
||||||
|
// size (before truncation to len) so callers can signal WSAEMSGSIZE.
|
||||||
|
int emu_recv_dequeue(
|
||||||
|
SOCKET s, char *buf, int len, sockaddr *from, int *fromlen,
|
||||||
|
size_t *out_full_dgram_bytes) {
|
||||||
|
if (!buf || len <= 0) {
|
||||||
|
WSASetLastError(WSAEINVAL);
|
||||||
|
return SOCKET_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> pkt;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::recursive_mutex> lock(g_mu);
|
||||||
|
auto it = g_socks.find(s);
|
||||||
|
if (it == g_socks.end()) {
|
||||||
|
WSASetLastError(WSAENOTSOCK);
|
||||||
|
return SOCKET_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto wait_pred = [&] {
|
||||||
|
auto j = g_socks.find(s);
|
||||||
|
return j == g_socks.end() || !j->second->queue.empty();
|
||||||
|
};
|
||||||
|
|
||||||
|
EmuSock *es = it->second.get();
|
||||||
|
if (es->nonblocking) {
|
||||||
|
if (es->queue.empty()) {
|
||||||
|
WSASetLastError(WSAEWOULDBLOCK);
|
||||||
|
return SOCKET_ERROR;
|
||||||
|
}
|
||||||
|
} else if (es->rx_timeout_ms == INFINITE || es->rx_timeout_ms == 0) {
|
||||||
|
it->second->cv.wait(lock, wait_pred);
|
||||||
|
} else {
|
||||||
|
const auto timeout = std::chrono::milliseconds(es->rx_timeout_ms);
|
||||||
|
if (!it->second->cv.wait_for(lock, timeout, wait_pred)) {
|
||||||
|
WSASetLastError(WSAETIMEDOUT);
|
||||||
|
return SOCKET_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it = g_socks.find(s);
|
||||||
|
if (it == g_socks.end()) {
|
||||||
|
WSASetLastError(WSAENOTSOCK);
|
||||||
|
return SOCKET_ERROR;
|
||||||
|
}
|
||||||
|
es = it->second.get();
|
||||||
|
if (es->queue.empty()) {
|
||||||
|
WSASetLastError(WSAENOTSOCK);
|
||||||
|
return SOCKET_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt = std::move(es->queue.front());
|
||||||
|
es->queue.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out_full_dgram_bytes) {
|
||||||
|
*out_full_dgram_bytes = pkt.size();
|
||||||
|
}
|
||||||
|
const int take = (len < (int) pkt.size()) ? len : (int) pkt.size();
|
||||||
|
memcpy(buf, pkt.data(), static_cast<size_t>(take));
|
||||||
|
if (from && fromlen && *fromlen >= (int) sizeof(sockaddr_in)) {
|
||||||
|
auto *out = reinterpret_cast<sockaddr_in *>(from);
|
||||||
|
memset(out, 0, sizeof(*out));
|
||||||
|
out->sin_family = AF_INET;
|
||||||
|
if (pkt.size() >= 20) {
|
||||||
|
memcpy(&out->sin_addr, pkt.data() + 12, 4);
|
||||||
|
} else {
|
||||||
|
out->sin_addr.s_addr = htonl(k_loopback_host);
|
||||||
|
}
|
||||||
|
*fromlen = sizeof(sockaddr_in);
|
||||||
|
}
|
||||||
|
return take;
|
||||||
|
}
|
||||||
|
|
||||||
|
int WINAPI recvfrom_hook(
|
||||||
|
SOCKET s, char *buf, int len, int flags, sockaddr *from, int *fromlen) {
|
||||||
|
if (!lookup(s)) {
|
||||||
|
return recvfrom_orig(s, buf, len, flags, from, fromlen);
|
||||||
|
}
|
||||||
|
return emu_recv_dequeue(s, buf, len, from, fromlen, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
int WINAPI WSARecvFrom_hook(
|
||||||
|
SOCKET s,
|
||||||
|
LPWSABUF lpBuffers,
|
||||||
|
DWORD dwBufferCount,
|
||||||
|
LPDWORD lpNumberOfBytesRecvd,
|
||||||
|
LPDWORD lpFlags,
|
||||||
|
sockaddr *lpFrom,
|
||||||
|
LPINT lpFromlen,
|
||||||
|
LPWSAOVERLAPPED lpOverlapped,
|
||||||
|
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine) {
|
||||||
|
if (!lookup(s)) {
|
||||||
|
return WSARecvFrom_orig(
|
||||||
|
s,
|
||||||
|
lpBuffers,
|
||||||
|
dwBufferCount,
|
||||||
|
lpNumberOfBytesRecvd,
|
||||||
|
lpFlags,
|
||||||
|
lpFrom,
|
||||||
|
lpFromlen,
|
||||||
|
lpOverlapped,
|
||||||
|
lpCompletionRoutine);
|
||||||
|
}
|
||||||
|
if (lpOverlapped != nullptr) {
|
||||||
|
WSASetLastError(WSAEOPNOTSUPP);
|
||||||
|
return SOCKET_ERROR;
|
||||||
|
}
|
||||||
|
if (dwBufferCount != 1 || !lpBuffers || !lpNumberOfBytesRecvd) {
|
||||||
|
WSASetLastError(WSAEINVAL);
|
||||||
|
return SOCKET_ERROR;
|
||||||
|
}
|
||||||
|
sockaddr_in from_tmp{};
|
||||||
|
int fromlen_tmp = sizeof(from_tmp);
|
||||||
|
size_t full_bytes = 0;
|
||||||
|
int r = emu_recv_dequeue(
|
||||||
|
s,
|
||||||
|
lpBuffers[0].buf,
|
||||||
|
(int) lpBuffers[0].len,
|
||||||
|
reinterpret_cast<sockaddr *>(&from_tmp),
|
||||||
|
&fromlen_tmp,
|
||||||
|
&full_bytes);
|
||||||
|
if (r == SOCKET_ERROR) {
|
||||||
|
return SOCKET_ERROR;
|
||||||
|
}
|
||||||
|
if (full_bytes > lpBuffers[0].len) {
|
||||||
|
WSASetLastError(WSAEMSGSIZE);
|
||||||
|
return SOCKET_ERROR;
|
||||||
|
}
|
||||||
|
*lpNumberOfBytesRecvd = static_cast<DWORD>(r);
|
||||||
|
if (lpFlags) {
|
||||||
|
*lpFlags = 0;
|
||||||
|
}
|
||||||
|
if (lpFrom && lpFromlen && *lpFromlen >= (int) sizeof(sockaddr_in)) {
|
||||||
|
*reinterpret_cast<sockaddr_in *>(lpFrom) = from_tmp;
|
||||||
|
*lpFromlen = sizeof(sockaddr_in);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int WINAPI ioctlsocket_hook(SOCKET s, long cmd, u_long *argp) {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(g_mu);
|
||||||
|
auto it = g_socks.find(s);
|
||||||
|
if (it != g_socks.end()) {
|
||||||
|
EmuSock *es = it->second.get();
|
||||||
|
if (argp && static_cast<u_long>(cmd) == FIONBIO) {
|
||||||
|
es->nonblocking = (*argp != 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
WSASetLastError(WSAEOPNOTSUPP);
|
||||||
|
return SOCKET_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ioctlsocket_orig(s, cmd, argp);
|
||||||
|
}
|
||||||
|
|
||||||
|
int WINAPI setsockopt_hook(
|
||||||
|
SOCKET s, int level, int optname, const char *optval, int optlen) {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(g_mu);
|
||||||
|
auto it = g_socks.find(s);
|
||||||
|
if (it != g_socks.end()) {
|
||||||
|
const bool deref_optval = level == SOL_SOCKET && optlen >= (int) sizeof(DWORD);
|
||||||
|
if (deref_optval && optval) {
|
||||||
|
EmuSock *es = it->second.get();
|
||||||
|
auto v = reinterpret_cast<const DWORD *>(optval);
|
||||||
|
if (optname == SO_RCVTIMEO) {
|
||||||
|
DWORD ms = *v;
|
||||||
|
es->rx_timeout_ms = (ms == 0) ? INFINITE : ms;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (optname == SO_SNDTIMEO) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
WSASetLastError(WSAEOPNOTSUPP);
|
||||||
|
return SOCKET_ERROR;
|
||||||
|
}
|
||||||
|
if (!deref_optval) {
|
||||||
|
WSASetLastError(WSAEOPNOTSUPP);
|
||||||
|
return SOCKET_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return setsockopt_orig(s, level, optname, optval, optlen);
|
||||||
|
}
|
||||||
|
|
||||||
|
void install_icmphook_hooks() {
|
||||||
|
static bool done = false;
|
||||||
|
if (done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
done = true;
|
||||||
|
|
||||||
|
bool ok = true;
|
||||||
|
ok &= detour::trampoline_try(
|
||||||
|
"ws2_32.dll", "socket",
|
||||||
|
(void *) socket_hook, (void **) &socket_orig);
|
||||||
|
ok &= detour::trampoline_try(
|
||||||
|
"ws2_32.dll", "WSASocketW",
|
||||||
|
(void *) WSASocketW_hook, (void **) &WSASocketW_orig);
|
||||||
|
ok &= detour::trampoline_try(
|
||||||
|
"ws2_32.dll", "WSASocketA",
|
||||||
|
(void *) WSASocketA_hook, (void **) &WSASocketA_orig);
|
||||||
|
ok &= detour::trampoline_try(
|
||||||
|
"ws2_32.dll", "closesocket",
|
||||||
|
(void *) closesocket_hook, (void **) &closesocket_orig);
|
||||||
|
ok &= detour::trampoline_try(
|
||||||
|
"ws2_32.dll", "bind",
|
||||||
|
(void *) bind_hook_ws2, (void **) &bind_trampoline_orig);
|
||||||
|
ok &= detour::trampoline_try(
|
||||||
|
"ws2_32.dll", "sendto",
|
||||||
|
(void *) sendto_hook, (void **) &sendto_orig);
|
||||||
|
ok &= detour::trampoline_try(
|
||||||
|
"ws2_32.dll", "recvfrom",
|
||||||
|
(void *) recvfrom_hook, (void **) &recvfrom_orig);
|
||||||
|
ok &= detour::trampoline_try(
|
||||||
|
"ws2_32.dll", "WSASendTo",
|
||||||
|
(void *) WSASendTo_hook, (void **) &WSASendTo_orig);
|
||||||
|
ok &= detour::trampoline_try(
|
||||||
|
"ws2_32.dll", "WSARecvFrom",
|
||||||
|
(void *) WSARecvFrom_hook, (void **) &WSARecvFrom_orig);
|
||||||
|
ok &= detour::trampoline_try(
|
||||||
|
"ws2_32.dll", "ioctlsocket",
|
||||||
|
(void *) ioctlsocket_hook, (void **) &ioctlsocket_orig);
|
||||||
|
ok &= detour::trampoline_try(
|
||||||
|
"ws2_32.dll", "setsockopt",
|
||||||
|
(void *) setsockopt_hook, (void **) &setsockopt_orig);
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
log_warning(
|
||||||
|
"network",
|
||||||
|
"ICMP emulation: one or more ws2_32 hooks failed to install");
|
||||||
|
} else {
|
||||||
|
log_info("network", "ICMP emulation hooks installed (raw ICMP sockets)");
|
||||||
|
}
|
||||||
|
|
||||||
|
g_installed.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace icmphook_internal
|
||||||
|
|
||||||
|
bool icmphook_is_emulated_socket(SOCKET s) {
|
||||||
|
if (!icmphook_internal::g_installed.load(std::memory_order_acquire)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(icmphook_internal::g_mu);
|
||||||
|
return icmphook_internal::g_socks.find(s) != icmphook_internal::g_socks.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool icmphook_try_bind(SOCKET s, const struct sockaddr *name, int namelen, int *out_result) {
|
||||||
|
if (!icmphook_internal::g_installed.load(std::memory_order_acquire)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(icmphook_internal::g_mu);
|
||||||
|
if (icmphook_internal::g_socks.find(s) == icmphook_internal::g_socks.end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*out_result = icmphook_internal::bind_hook_ws2(s, name, namelen);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void icmphook_net_init() {
|
||||||
|
icmphook_internal::install_icmphook_hooks();
|
||||||
|
icmphook_iphlpapi_install();
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <winsock2.h>
|
||||||
|
|
||||||
|
bool icmphook_is_emulated_socket(SOCKET s);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Handle bind() for emulated ICMP sockets: records interface address and succeeds without kernel bind.
|
||||||
|
* Returns true if this socket was handled (caller should return 0).
|
||||||
|
*/
|
||||||
|
bool icmphook_try_bind(SOCKET s, const struct sockaddr *name, int namelen, int *out_result);
|
||||||
|
|
||||||
|
void icmphook_net_init();
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
#include "avs/core.h"
|
#include "avs/core.h"
|
||||||
#include "avs/ea3.h"
|
#include "avs/ea3.h"
|
||||||
@@ -14,15 +15,18 @@
|
|||||||
#include "util/fileutils.h"
|
#include "util/fileutils.h"
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
#include "util/deferlog.h"
|
#include "util/deferlog.h"
|
||||||
|
#include "hooks/icmphook_net.h"
|
||||||
|
|
||||||
// hooking related stuff
|
// hooking related stuff
|
||||||
static decltype(GetAdaptersInfo) *GetAdaptersInfo_orig = nullptr;
|
static decltype(GetAdaptersInfo) *GetAdaptersInfo_orig = nullptr;
|
||||||
|
static decltype(GetIpAddrTable) *GetIpAddrTable_orig = nullptr;
|
||||||
static decltype(bind) *bind_orig = nullptr;
|
static decltype(bind) *bind_orig = nullptr;
|
||||||
|
|
||||||
// settings
|
// settings
|
||||||
std::string NETWORK_ADDRESS = "10.9.0.0";
|
std::string NETWORK_ADDRESS = "10.9.0.0";
|
||||||
std::string NETWORK_SUBNET = "255.255.0.0";
|
std::string NETWORK_SUBNET = "255.255.0.0";
|
||||||
static bool GetAdaptersInfo_log = true;
|
static bool GetAdaptersInfo_log = true;
|
||||||
|
static bool GetIpAddrTable_log = true;
|
||||||
|
|
||||||
// network structs
|
// network structs
|
||||||
static struct in_addr network;
|
static struct in_addr network;
|
||||||
@@ -42,6 +46,80 @@ static void defer_network_adapter_error() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool is_valid_ipaddr_row(const MIB_IPADDRROW &row) {
|
||||||
|
static const auto loopback = inet_addr("127.0.0.1");
|
||||||
|
|
||||||
|
return row.dwAddr != 0 && row.dwAddr != loopback;
|
||||||
|
}
|
||||||
|
|
||||||
|
static MIB_IPADDRROW *find_preferred_ipaddr_row(PMIB_IPADDRTABLE table) {
|
||||||
|
|
||||||
|
if (table == nullptr || table->dwNumEntries == 0) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefer the row matching -adapternetwork/-adaptersubnet
|
||||||
|
for (DWORD i = 0; i < table->dwNumEntries; i++) {
|
||||||
|
auto &row = table->table[i];
|
||||||
|
if (!is_valid_ipaddr_row(row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto row_prefix = row.dwAddr & row.dwMask;
|
||||||
|
if (row_prefix == prefix.s_addr && row.dwMask == subnet.s_addr) {
|
||||||
|
return &row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fall back to the interface Windows would route through by default
|
||||||
|
PMIB_IPFORWARDTABLE pIpForwardTable = (MIB_IPFORWARDTABLE *) malloc(sizeof(MIB_IPFORWARDTABLE));
|
||||||
|
DWORD dwSize = 0;
|
||||||
|
if (GetIpForwardTable(pIpForwardTable, &dwSize, TRUE) == ERROR_INSUFFICIENT_BUFFER) {
|
||||||
|
free(pIpForwardTable);
|
||||||
|
pIpForwardTable = (MIB_IPFORWARDTABLE *) malloc(dwSize);
|
||||||
|
}
|
||||||
|
if (GetIpForwardTable(pIpForwardTable, &dwSize, TRUE) != NO_ERROR || pIpForwardTable->dwNumEntries == 0) {
|
||||||
|
free(pIpForwardTable);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD best = pIpForwardTable->table[0].dwForwardIfIndex;
|
||||||
|
free(pIpForwardTable);
|
||||||
|
|
||||||
|
for (DWORD i = 0; i < table->dwNumEntries; i++) {
|
||||||
|
auto &row = table->table[i];
|
||||||
|
if (row.dwIndex == best && is_valid_ipaddr_row(row)) {
|
||||||
|
return &row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// last resort: keep a deterministic valid row instead of exposing all adapters
|
||||||
|
for (DWORD i = 0; i < table->dwNumEntries; i++) {
|
||||||
|
auto &row = table->table[i];
|
||||||
|
if (is_valid_ipaddr_row(row)) {
|
||||||
|
return &row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void keep_only_ipaddr_row(PMIB_IPADDRTABLE table, MIB_IPADDRROW *row) {
|
||||||
|
if (table == nullptr || row == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GetIpAddrTable_log) {
|
||||||
|
in_addr addr {};
|
||||||
|
addr.s_addr = row->dwAddr;
|
||||||
|
log_info("network", "Using preferred IP address row: {}", inet_ntoa(addr));
|
||||||
|
}
|
||||||
|
|
||||||
|
table->table[0] = *row;
|
||||||
|
table->dwNumEntries = 1;
|
||||||
|
GetIpAddrTable_log = false;
|
||||||
|
}
|
||||||
|
|
||||||
static ULONG WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO pAdapterInfo, PULONG pOutBufLen) {
|
static ULONG WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO pAdapterInfo, PULONG pOutBufLen) {
|
||||||
|
|
||||||
// call orig
|
// call orig
|
||||||
@@ -182,6 +260,50 @@ static ULONG WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO pAdapterInfo, PULONG p
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static DWORD WINAPI GetIpAddrTable_hook(PMIB_IPADDRTABLE pIpAddrTable, PULONG pdwSize, BOOL bOrder) {
|
||||||
|
|
||||||
|
auto input_size = pdwSize != nullptr ? *pdwSize : 0;
|
||||||
|
auto ret = GetIpAddrTable_orig(pIpAddrTable, pdwSize, bOrder);
|
||||||
|
|
||||||
|
if (ret == NO_ERROR) {
|
||||||
|
auto row = find_preferred_ipaddr_row(pIpAddrTable);
|
||||||
|
if (row != nullptr) {
|
||||||
|
keep_only_ipaddr_row(pIpAddrTable, row);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ret != ERROR_INSUFFICIENT_BUFFER || pIpAddrTable == nullptr || pdwSize == nullptr) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the caller's buffer is large enough for the filtered single-row table,
|
||||||
|
// satisfy the call even when Windows needed more room for all adapters.
|
||||||
|
const auto one_row_size = offsetof(MIB_IPADDRTABLE, table) + sizeof(MIB_IPADDRROW);
|
||||||
|
if (input_size < one_row_size) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto full_size = *pdwSize;
|
||||||
|
auto table = (PMIB_IPADDRTABLE) malloc(full_size);
|
||||||
|
if (table == nullptr) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto full_ret = GetIpAddrTable_orig(table, &full_size, bOrder);
|
||||||
|
if (full_ret == NO_ERROR) {
|
||||||
|
auto row = find_preferred_ipaddr_row(table);
|
||||||
|
if (row != nullptr) {
|
||||||
|
keep_only_ipaddr_row(pIpAddrTable, row);
|
||||||
|
*pdwSize = one_row_size;
|
||||||
|
ret = NO_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(table);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
static int WINAPI bind_hook(SOCKET s, const struct sockaddr *name, int namelen) {
|
static int WINAPI bind_hook(SOCKET s, const struct sockaddr *name, int namelen) {
|
||||||
|
|
||||||
#ifdef __clang__
|
#ifdef __clang__
|
||||||
@@ -189,6 +311,11 @@ static int WINAPI bind_hook(SOCKET s, const struct sockaddr *name, int namelen)
|
|||||||
#pragma ide diagnostic ignored "OCDFAInspection"
|
#pragma ide diagnostic ignored "OCDFAInspection"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
int icmp_bind_result = 0;
|
||||||
|
if (icmphook_try_bind(s, name, namelen, &icmp_bind_result)) {
|
||||||
|
return icmp_bind_result;
|
||||||
|
}
|
||||||
|
|
||||||
// cast to sockaddr_in
|
// cast to sockaddr_in
|
||||||
struct sockaddr_in *in_name = (struct sockaddr_in *) name;
|
struct sockaddr_in *in_name = (struct sockaddr_in *) name;
|
||||||
|
|
||||||
@@ -237,6 +364,15 @@ void networkhook_init() {
|
|||||||
GetAdaptersInfo_orig = orig_addr;
|
GetAdaptersInfo_orig = orig_addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIpAddrTable hook
|
||||||
|
auto ip_addr_table_orig_addr = detour::iat_try(
|
||||||
|
"GetIpAddrTable", GetIpAddrTable_hook, nullptr);
|
||||||
|
if (!ip_addr_table_orig_addr) {
|
||||||
|
log_warning("network", "Could not hook GetIpAddrTable");
|
||||||
|
} else if (GetIpAddrTable_orig == nullptr) {
|
||||||
|
GetIpAddrTable_orig = ip_addr_table_orig_addr;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Bind Hook
|
* Bind Hook
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,8 +3,11 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <shlwapi.h>
|
#include <shlwapi.h>
|
||||||
|
#include <windows.h>
|
||||||
#include <cfg/configurator.h>
|
#include <cfg/configurator.h>
|
||||||
|
|
||||||
#include "api/modules/capture.h"
|
#include "api/modules/capture.h"
|
||||||
@@ -69,6 +72,7 @@
|
|||||||
#include "games/museca/museca.h"
|
#include "games/museca/museca.h"
|
||||||
#include "hooks/avshook.h"
|
#include "hooks/avshook.h"
|
||||||
#include "hooks/audio/audio.h"
|
#include "hooks/audio/audio.h"
|
||||||
|
#include "hooks/audio/backends/wasapi/downmix.h"
|
||||||
#include "hooks/debughook.h"
|
#include "hooks/debughook.h"
|
||||||
#include "hooks/devicehook.h"
|
#include "hooks/devicehook.h"
|
||||||
#include "hooks/graphics/nvenc_hook.h"
|
#include "hooks/graphics/nvenc_hook.h"
|
||||||
@@ -76,6 +80,7 @@
|
|||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "hooks/lang.h"
|
#include "hooks/lang.h"
|
||||||
#include "hooks/networkhook.h"
|
#include "hooks/networkhook.h"
|
||||||
|
#include "hooks/icmphook_net.h"
|
||||||
#include "hooks/unisintrhook.h"
|
#include "hooks/unisintrhook.h"
|
||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
#include "launcher/logger.h"
|
#include "launcher/logger.h"
|
||||||
@@ -93,11 +98,13 @@
|
|||||||
#include "misc/sde.h"
|
#include "misc/sde.h"
|
||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
|
#include "overlay/notifications.h"
|
||||||
#include "overlay/windows/patch_manager.h"
|
#include "overlay/windows/patch_manager.h"
|
||||||
#include "overlay/windows/iidx_seg.h"
|
#include "overlay/windows/iidx_seg.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
#include "rawinput/touch.h"
|
#include "rawinput/touch.h"
|
||||||
#include "reader/reader.h"
|
#include "reader/reader.h"
|
||||||
|
#include "sdk/sdk.h"
|
||||||
#include "stubs/stubs.h"
|
#include "stubs/stubs.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
#include "util/cpuutils.h"
|
#include "util/cpuutils.h"
|
||||||
@@ -107,6 +114,7 @@
|
|||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/peb.h"
|
#include "util/peb.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
#include "util/socd_cleaner.h"
|
#include "util/socd_cleaner.h"
|
||||||
#include "util/sysutils.h"
|
#include "util/sysutils.h"
|
||||||
#include "util/tapeled.h"
|
#include "util/tapeled.h"
|
||||||
@@ -242,6 +250,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
bool easrv_smart = false;
|
bool easrv_smart = false;
|
||||||
bool load_stubs = false;
|
bool load_stubs = false;
|
||||||
bool netfix_disable = false;
|
bool netfix_disable = false;
|
||||||
|
bool icmphook_enable = false;
|
||||||
bool lang_disable = false;
|
bool lang_disable = false;
|
||||||
std::string process_priority_str = "high";
|
std::string process_priority_str = "high";
|
||||||
bool cardio_enabled = false;
|
bool cardio_enabled = false;
|
||||||
@@ -297,6 +306,26 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
auto &options = *options_ptr;
|
auto &options = *options_ptr;
|
||||||
|
|
||||||
|
#if !SPICE_XP
|
||||||
|
|
||||||
|
{
|
||||||
|
// skip elevation only if AutoElevate is explicitly set to "user"
|
||||||
|
const bool skip_elevation = options[launcher::Options::AutoElevate].is_active() &&
|
||||||
|
options[launcher::Options::AutoElevate].value_text() == "user";
|
||||||
|
if (!skip_elevation && !sysutils::is_running_as_admin()) {
|
||||||
|
log_info("launcher", "relaunching with administrator privileges");
|
||||||
|
if (sysutils::relaunch_as_admin()) {
|
||||||
|
exit(0);
|
||||||
|
} else {
|
||||||
|
// elevation failed or was denied by the user
|
||||||
|
log_fatal("launcher", "failed to launch with administrator privileges");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // !SPICE_XP
|
||||||
|
|
||||||
// check options
|
// check options
|
||||||
// TODO: get rid of some booleans here and make use of the options directly
|
// TODO: get rid of some booleans here and make use of the options directly
|
||||||
if (options[launcher::Options::OpenConfigurator].value_bool()) {
|
if (options[launcher::Options::OpenConfigurator].value_bool()) {
|
||||||
@@ -349,7 +378,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
if (options[launcher::Options::spice2x_SDVXNoSub].value_bool()) {
|
if (options[launcher::Options::spice2x_SDVXNoSub].value_bool()) {
|
||||||
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
|
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
|
||||||
GRAPHICS_PREVENT_SECONDARY_WINDOW = true;
|
GRAPHICS_PREVENT_SECONDARY_WINDOWS = true;
|
||||||
}
|
}
|
||||||
if (options[launcher::Options::DXDisplayAdapter].is_active() &&
|
if (options[launcher::Options::DXDisplayAdapter].is_active() &&
|
||||||
options[launcher::Options::DXDisplayAdapter].value_uint32() != D3DADAPTER_DEFAULT) {
|
options[launcher::Options::DXDisplayAdapter].value_uint32() != D3DADAPTER_DEFAULT) {
|
||||||
@@ -383,6 +412,16 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
monitor_orientation = ORIENTATION_CW;
|
monitor_orientation = ORIENTATION_CW;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<std::pair<uint32_t, uint32_t>> monitor_resolution;
|
||||||
|
if (options[launcher::Options::ChangeResolution].is_active()) {
|
||||||
|
std::pair<uint32_t, uint32_t> result;
|
||||||
|
if (parse_width_height(options[launcher::Options::ChangeResolution].value_text(), result)) {
|
||||||
|
monitor_resolution = result;
|
||||||
|
} else if (!cfg_run && !cfg::CONFIGURATOR_STANDALONE) {
|
||||||
|
log_fatal("launcher", "failed to parse -changeres");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (options[launcher::Options::spice2x_NoD3D9DeviceHook].value_bool()) {
|
if (options[launcher::Options::spice2x_NoD3D9DeviceHook].value_bool()) {
|
||||||
D3D9_DEVICE_HOOK_DISABLE = true;
|
D3D9_DEVICE_HOOK_DISABLE = true;
|
||||||
// touch emulation gets disabled, might as well turn these on
|
// touch emulation gets disabled, might as well turn these on
|
||||||
@@ -518,12 +557,6 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::IIDXTDJMode].value_bool()) {
|
if (options[launcher::Options::IIDXTDJMode].value_bool()) {
|
||||||
games::iidx::TDJ_MODE = true;
|
games::iidx::TDJ_MODE = true;
|
||||||
}
|
}
|
||||||
if (options[launcher::Options::IIDXTTDelayP1].is_active()) {
|
|
||||||
games::iidx::TT_DELAY_P1 = options[launcher::Options::IIDXTTDelayP1].value_uint32();
|
|
||||||
}
|
|
||||||
if (options[launcher::Options::IIDXTTDelayP2].is_active()) {
|
|
||||||
games::iidx::TT_DELAY_P2 = options[launcher::Options::IIDXTTDelayP2].value_uint32();
|
|
||||||
}
|
|
||||||
if (options[launcher::Options::spice2x_IIDXDigitalTTSensitivity].is_active()) {
|
if (options[launcher::Options::spice2x_IIDXDigitalTTSensitivity].is_active()) {
|
||||||
games::iidx::DIGITAL_TT_SENS = (uint8_t)
|
games::iidx::DIGITAL_TT_SENS = (uint8_t)
|
||||||
options[launcher::Options::spice2x_IIDXDigitalTTSensitivity].value_uint32();
|
options[launcher::Options::spice2x_IIDXDigitalTTSensitivity].value_uint32();
|
||||||
@@ -598,7 +631,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
if (options[launcher::Options::PopnNoSub].value_bool()) {
|
if (options[launcher::Options::PopnNoSub].value_bool()) {
|
||||||
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
|
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
|
||||||
GRAPHICS_PREVENT_SECONDARY_WINDOW = true;
|
GRAPHICS_PREVENT_SECONDARY_WINDOWS = true;
|
||||||
}
|
}
|
||||||
if (options[launcher::Options::PopnSubMonitorOverride].is_active()) {
|
if (options[launcher::Options::PopnSubMonitorOverride].is_active()) {
|
||||||
sysutils::SECOND_MONITOR_OVERRIDE = options[launcher::Options::PopnSubMonitorOverride].value_text();
|
sysutils::SECOND_MONITOR_OVERRIDE = options[launcher::Options::PopnSubMonitorOverride].value_text();
|
||||||
@@ -615,9 +648,22 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::GitaDoraCabinetType].is_active()) {
|
if (options[launcher::Options::GitaDoraCabinetType].is_active()) {
|
||||||
games::gitadora::CAB_TYPE = options[launcher::Options::GitaDoraCabinetType].value_uint32();
|
games::gitadora::CAB_TYPE = options[launcher::Options::GitaDoraCabinetType].value_uint32();
|
||||||
}
|
}
|
||||||
if (options[launcher::Options::GitaDoraArenaSingleWindow].value_bool() && GRAPHICS_WINDOWED) {
|
|
||||||
games::gitadora::ARENA_SINGLE_WINDOW = true;
|
// gitadora arena layout
|
||||||
|
if (options[launcher::Options::GitaDoraArenaSingleWindow].value_bool()) {
|
||||||
|
games::gitadora::ARENA_WINDOW_COUNT = 1;
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::GitaDoraArenaWindowLayout].is_active()) {
|
||||||
|
const auto window_count = options[launcher::Options::GitaDoraArenaWindowLayout].value_text();
|
||||||
|
if (window_count == "1") {
|
||||||
|
games::gitadora::ARENA_WINDOW_COUNT = 1;
|
||||||
|
} else if (window_count == "2") {
|
||||||
|
games::gitadora::ARENA_WINDOW_COUNT = 2;
|
||||||
|
} else if (window_count == "4") {
|
||||||
|
games::gitadora::ARENA_WINDOW_COUNT = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (options[launcher::Options::GitaDoraWailHold].is_active()) {
|
if (options[launcher::Options::GitaDoraWailHold].is_active()) {
|
||||||
socd::TILT_HOLD_MS = options[launcher::Options::GitaDoraWailHold].value_uint32();
|
socd::TILT_HOLD_MS = options[launcher::Options::GitaDoraWailHold].value_uint32();
|
||||||
}
|
}
|
||||||
@@ -634,6 +680,9 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::GitaDoraSubOverlaySize].is_active()) {
|
if (options[launcher::Options::GitaDoraSubOverlaySize].is_active()) {
|
||||||
games::gitadora::SUBSCREEN_OVERLAY_SIZE = options[launcher::Options::GitaDoraSubOverlaySize].value_text();
|
games::gitadora::SUBSCREEN_OVERLAY_SIZE = options[launcher::Options::GitaDoraSubOverlaySize].value_text();
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::GitaDoraArenaAsioDriver].is_active()) {
|
||||||
|
games::gitadora::ASIO_DRIVER = options[launcher::Options::GitaDoraArenaAsioDriver].value_text();
|
||||||
|
}
|
||||||
if (options[launcher::Options::LoadNostalgiaModule].value_bool()) {
|
if (options[launcher::Options::LoadNostalgiaModule].value_bool()) {
|
||||||
attach_nostalgia = true;
|
attach_nostalgia = true;
|
||||||
}
|
}
|
||||||
@@ -726,6 +775,9 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::DisableNetworkFixes].value_bool()) {
|
if (options[launcher::Options::DisableNetworkFixes].value_bool()) {
|
||||||
netfix_disable = true;
|
netfix_disable = true;
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::EnableICMPHook].value_bool()) {
|
||||||
|
icmphook_enable = true;
|
||||||
|
}
|
||||||
if (options[launcher::Options::DisableACPHook].value_bool()) {
|
if (options[launcher::Options::DisableACPHook].value_bool()) {
|
||||||
lang_disable = true;
|
lang_disable = true;
|
||||||
}
|
}
|
||||||
@@ -883,6 +935,12 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
PIN_MACRO_ENABLED = true;
|
PIN_MACRO_ENABLED = true;
|
||||||
PIN_MACRO_VALUES[1] = options[launcher::Options::Player2PinMacro].value_text();
|
PIN_MACRO_VALUES[1] = options[launcher::Options::Player2PinMacro].value_text();
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::AutoPinMacroTrigger0].is_active()) {
|
||||||
|
AUTO_PIN_MACRO_TRIGGER[0] = options[launcher::Options::AutoPinMacroTrigger0].value_text();
|
||||||
|
}
|
||||||
|
if (options[launcher::Options::AutoPinMacroTrigger1].is_active()) {
|
||||||
|
AUTO_PIN_MACRO_TRIGGER[1] = options[launcher::Options::AutoPinMacroTrigger1].value_text();
|
||||||
|
}
|
||||||
|
|
||||||
for (auto &reader : options[launcher::Options::ICCAReaderPort].values_text()) {
|
for (auto &reader : options[launcher::Options::ICCAReaderPort].values_text()) {
|
||||||
static int reader_id = 0;
|
static int reader_id = 0;
|
||||||
@@ -1037,6 +1095,17 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::spice2x_DisableVolumeHook].value_bool()) {
|
if (options[launcher::Options::spice2x_DisableVolumeHook].value_bool()) {
|
||||||
hooks::audio::VOLUME_HOOK_ENABLED = false;
|
hooks::audio::VOLUME_HOOK_ENABLED = false;
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::DownmixAudioToStereo].is_active()) {
|
||||||
|
auto &name = options[launcher::Options::DownmixAudioToStereo].value_text();
|
||||||
|
hooks::audio::DOWNMIX_ALGORITHM = hooks::audio::Downmix::name_to_algorithm(name.c_str());
|
||||||
|
}
|
||||||
|
if (options[launcher::Options::VolumeBoost].is_active()) {
|
||||||
|
const double decibels = std::strtod(
|
||||||
|
options[launcher::Options::VolumeBoost].value_text().c_str(), nullptr);
|
||||||
|
if (decibels > 0.0) {
|
||||||
|
hooks::audio::VOLUME_BOOST = (float) std::pow(10.0, decibels / 20.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (options[launcher::Options::AudioBackend].is_active()) {
|
if (options[launcher::Options::AudioBackend].is_active()) {
|
||||||
auto &name = options[launcher::Options::AudioBackend].value_text();
|
auto &name = options[launcher::Options::AudioBackend].value_text();
|
||||||
@@ -1127,6 +1196,22 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
GRAPHICS_WINDOW_ALWAYS_ON_TOP = options[launcher::Options::spice2x_WindowAlwaysOnTop].value_bool();
|
GRAPHICS_WINDOW_ALWAYS_ON_TOP = options[launcher::Options::spice2x_WindowAlwaysOnTop].value_bool();
|
||||||
GRAPHICS_WINDOW_BACKBUFFER_SCALE = options[launcher::Options::WindowForceScaling].value_bool();
|
GRAPHICS_WINDOW_BACKBUFFER_SCALE = options[launcher::Options::WindowForceScaling].value_bool();
|
||||||
GRAPHICS_WINDOW_DISABLE_ROUNDED_CORNERS = options[launcher::Options::WindowDisableRoundedCorners].value_bool();
|
GRAPHICS_WINDOW_DISABLE_ROUNDED_CORNERS = options[launcher::Options::WindowDisableRoundedCorners].value_bool();
|
||||||
|
if (options[launcher::Options::GitaDoraWindowedMainMonitor].is_active()) {
|
||||||
|
GRAPHICS_GITADORA_MAIN_MONITOR =
|
||||||
|
options[launcher::Options::GitaDoraWindowedMainMonitor].value_text();
|
||||||
|
}
|
||||||
|
if (options[launcher::Options::GitaDoraWindowedLeftMonitor].is_active()) {
|
||||||
|
GRAPHICS_GITADORA_LEFT_MONITOR =
|
||||||
|
options[launcher::Options::GitaDoraWindowedLeftMonitor].value_text();
|
||||||
|
}
|
||||||
|
if (options[launcher::Options::GitaDoraWindowedRightMonitor].is_active()) {
|
||||||
|
GRAPHICS_GITADORA_RIGHT_MONITOR =
|
||||||
|
options[launcher::Options::GitaDoraWindowedRightMonitor].value_text();
|
||||||
|
}
|
||||||
|
if (options[launcher::Options::GitaDoraWindowedSmallMonitor].is_active()) {
|
||||||
|
GRAPHICS_GITADORA_SMALL_MONITOR =
|
||||||
|
options[launcher::Options::GitaDoraWindowedSmallMonitor].value_text();
|
||||||
|
}
|
||||||
|
|
||||||
// IIDX/SDVX Windowed Subscreen
|
// IIDX/SDVX Windowed Subscreen
|
||||||
if (options[launcher::Options::spice2x_IIDXWindowedSubscreenSize].is_active()) {
|
if (options[launcher::Options::spice2x_IIDXWindowedSubscreenSize].is_active()) {
|
||||||
@@ -1152,11 +1237,32 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options[launcher::Options::spice2x_JubeatLegacyTouch].value_bool()) {
|
if (options[launcher::Options::spice2x_JubeatLegacyTouch].value_bool()) {
|
||||||
games::jb::TOUCH_LEGACY_BOX = true;
|
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Legacy;
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::JubeatTouchAlgo].is_active()) {
|
||||||
|
if (options[launcher::Options::JubeatTouchAlgo].value_text() == "accurate") {
|
||||||
|
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::AcAccurate;
|
||||||
|
} else if (options[launcher::Options::JubeatTouchAlgo].value_text() == "legacy") {
|
||||||
|
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Legacy;
|
||||||
|
} else {
|
||||||
|
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Improved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reflec beat touch emulation
|
||||||
if (options[launcher::Options::spice2x_RBTouchScale].is_active()) {
|
if (options[launcher::Options::spice2x_RBTouchScale].is_active()) {
|
||||||
games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32();
|
games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32();
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::RBTouchSize].is_active()) {
|
||||||
|
const auto text = options[launcher::Options::RBTouchSize].value_text();
|
||||||
|
if (text == "3") {
|
||||||
|
games::rb::TOUCH_SIZE = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options[launcher::Options::RBTouchPollRate].is_active()) {
|
||||||
|
games::rb::TOUCH_POLL_RATE = options[launcher::Options::RBTouchPollRate].value_uint32();
|
||||||
|
}
|
||||||
|
|
||||||
if (options[launcher::Options::spice2x_AsioForceUnload].value_bool()) {
|
if (options[launcher::Options::spice2x_AsioForceUnload].value_bool()) {
|
||||||
hooks::audio::ASIO_FORCE_UNLOAD_ON_STOP = true;
|
hooks::audio::ASIO_FORCE_UNLOAD_ON_STOP = true;
|
||||||
}
|
}
|
||||||
@@ -1278,6 +1384,14 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
games::loveplus::CAMERA_ENABLE = true;
|
games::loveplus::CAMERA_ENABLE = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options[launcher::Options::DisableHighResTimer].value_bool()) {
|
||||||
|
timeutils::TIMER_HACKS_DISABLE = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options[launcher::Options::CfgForceSoftwareRender].value_bool()) {
|
||||||
|
cfg::CONFIGURATOR_FORCE_SOFTWARE_RENDER = true;
|
||||||
|
}
|
||||||
|
|
||||||
// API debugging
|
// API debugging
|
||||||
if (api_debug && !cfg::CONFIGURATOR_STANDALONE) {
|
if (api_debug && !cfg::CONFIGURATOR_STANDALONE) {
|
||||||
API_CONTROLLER = std::make_unique<api::Controller>(api_port, api_pass, api_pretty);
|
API_CONTROLLER = std::make_unique<api::Controller>(api_port, api_pass, api_pretty);
|
||||||
@@ -1537,16 +1651,18 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// early hooks
|
// early hooks
|
||||||
for (auto &hook : early_hooks) {
|
if (!cfg_run) {
|
||||||
log_info("launcher", "loading early hook DLL {}", hook);
|
for (auto &hook : early_hooks) {
|
||||||
libutils::print_dll_info(hook);
|
log_info("launcher", "loading early hook DLL {}", hook);
|
||||||
HMODULE module;
|
libutils::print_dll_info(hook);
|
||||||
if (!(module = libutils::try_library(hook))) {
|
HMODULE module;
|
||||||
log_warning("launcher", "failed to load early hook {}: {}", hook, get_last_error_string());
|
if (!(module = libutils::try_library(hook))) {
|
||||||
deferredlogs::defer_error_messages({
|
log_warning("launcher", "failed to load early hook {}: {}", hook, get_last_error_string());
|
||||||
fmt::format("failed to load early hook {}:", hook),
|
deferredlogs::defer_error_messages({
|
||||||
fmt::format(" {}", get_last_error_string())
|
fmt::format("failed to load early hook {}:", hook),
|
||||||
});
|
fmt::format(" {}", get_last_error_string())
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2177,7 +2293,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::PrimaryMonitor].is_active()) {
|
if (options[launcher::Options::PrimaryMonitor].is_active()) {
|
||||||
change_primary_monitor(options[launcher::Options::PrimaryMonitor].value_text());
|
change_primary_monitor(options[launcher::Options::PrimaryMonitor].value_text());
|
||||||
}
|
}
|
||||||
update_monitor_on_boot(monitor_orientation, GRAPHICS_FORCE_REFRESH);
|
update_monitor_on_boot(monitor_orientation, GRAPHICS_FORCE_REFRESH, monitor_resolution);
|
||||||
|
|
||||||
// initialize raw input
|
// initialize raw input
|
||||||
RI_MGR = std::make_unique<rawinput::RawInputManager>();
|
RI_MGR = std::make_unique<rawinput::RawInputManager>();
|
||||||
@@ -2230,10 +2346,18 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
hooks::lang::early_init();
|
hooks::lang::early_init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// use high resolution timer for this process
|
||||||
|
timeutils::set_timer_resolution();
|
||||||
|
|
||||||
// load DLLs
|
// load DLLs
|
||||||
avs::core::load_dll();
|
avs::core::load_dll();
|
||||||
avs::ea3::load_dll();
|
avs::ea3::load_dll();
|
||||||
|
|
||||||
|
// ICMP emulation (opt-in; before games open raw ICMP sockets)
|
||||||
|
if (icmphook_enable) {
|
||||||
|
icmphook_net_init();
|
||||||
|
}
|
||||||
|
|
||||||
// net fix
|
// net fix
|
||||||
if (!netfix_disable) {
|
if (!netfix_disable) {
|
||||||
networkhook_init();
|
networkhook_init();
|
||||||
@@ -2364,6 +2488,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
bt5api_hook(module);
|
bt5api_hook(module);
|
||||||
|
sdk::register_sdk_hooks(hook, module);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2390,6 +2515,24 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
// eamuse init
|
// eamuse init
|
||||||
eamuse_autodetect_game();
|
eamuse_autodetect_game();
|
||||||
|
|
||||||
|
// notification position: apply per-game default first, then the explicit
|
||||||
|
// user option (if set) wins over the default.
|
||||||
|
overlay::notifications::apply_game_default_position(eamuse_get_game());
|
||||||
|
if (options[launcher::Options::NotificationPosition].is_active()) {
|
||||||
|
const auto txt = options[launcher::Options::NotificationPosition].value_text();
|
||||||
|
if (txt == "topleft") {
|
||||||
|
overlay::notifications::POSITION = overlay::notifications::Position::TopLeft;
|
||||||
|
} else if (txt == "topright") {
|
||||||
|
overlay::notifications::POSITION = overlay::notifications::Position::TopRight;
|
||||||
|
} else if (txt == "bottomleft") {
|
||||||
|
overlay::notifications::POSITION = overlay::notifications::Position::BottomLeft;
|
||||||
|
} else if (txt == "bottomright") {
|
||||||
|
overlay::notifications::POSITION = overlay::notifications::Position::BottomRight;
|
||||||
|
} else if (txt == "off") {
|
||||||
|
overlay::notifications::ENABLED = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// unis device hook
|
// unis device hook
|
||||||
unisintrhook_init();
|
unisintrhook_init();
|
||||||
|
|
||||||
@@ -2398,6 +2541,9 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
bt5api_init();
|
bt5api_init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// init SDK
|
||||||
|
sdk::init_sdk_modules();
|
||||||
|
|
||||||
// API
|
// API
|
||||||
if (api_enable || std::min(api_serial_port.size(), api_serial_baud.size()) > 0) {
|
if (api_enable || std::min(api_serial_port.size(), api_serial_baud.size()) > 0) {
|
||||||
API_CONTROLLER = std::make_unique<api::Controller>(api_port, api_pass, api_pretty);
|
API_CONTROLLER = std::make_unique<api::Controller>(api_port, api_pass, api_pretty);
|
||||||
@@ -2505,6 +2651,8 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
bt5api_dispose();
|
bt5api_dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sdk::fini_sdk_modules();
|
||||||
|
|
||||||
// stop raw input
|
// stop raw input
|
||||||
RI_MGR.reset();
|
RI_MGR.reset();
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,28 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.category = "Network (Advanced)",
|
.category = "Network (Advanced)",
|
||||||
.sensitive = true,
|
.sensitive = true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// AutoPinMacroTrigger0
|
||||||
|
.title = "Player 1 PIN Macro Auto Trigger on Log",
|
||||||
|
.name = "autopinmacrotrigger0",
|
||||||
|
.desc =
|
||||||
|
"Substring matched against game log output to detect when Player 1's PIN entry "
|
||||||
|
"screen is ready. When the substring appears in any log line, the PIN macro is "
|
||||||
|
"typed for Player 1 only. Leave blank to disable auto-trigger for P1.",
|
||||||
|
.type = OptionType::Text,
|
||||||
|
.category = "Network (Advanced)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// AutoPinMacroTrigger1
|
||||||
|
.title = "Player 2 PIN Macro Auto Trigger on Log",
|
||||||
|
.name = "autopinmacrotrigger1",
|
||||||
|
.desc =
|
||||||
|
"Substring matched against game log output to detect when Player 2's PIN entry "
|
||||||
|
"screen is ready. When the substring appears in any log line, the PIN macro is "
|
||||||
|
"typed for Player 2 only. Leave blank to disable auto-trigger for P2.",
|
||||||
|
.type = OptionType::Text,
|
||||||
|
.category = "Network (Advanced)",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.title = "Windowed Mode",
|
.title = "Windowed Mode",
|
||||||
.name = "w",
|
.name = "w",
|
||||||
@@ -203,7 +225,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
{
|
{
|
||||||
.title = "Show Cursor & Touch Emulation Enable",
|
.title = "Show Cursor & Touch Emulation Enable",
|
||||||
.name = "s",
|
.name = "s",
|
||||||
.desc = "Shows the cursor in the game window; also turns on touch emulation. Do not use if you use a real touch screen.",
|
.desc = "Shows the cursor in the game window; also turns on touch emulation. Do not enable this if you have a real touch screen.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "Common",
|
.category = "Common",
|
||||||
},
|
},
|
||||||
@@ -265,7 +287,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.title = "Full Screen Orientation Swap (EXPERIMENTAL)",
|
.title = "Full Screen Orientation Swap (EXPERIMENTAL)",
|
||||||
.name = "forceresswap",
|
.name = "forceresswap",
|
||||||
.desc =
|
.desc =
|
||||||
"Allows you to play portrait games in in landscape (and vice versa) by transposing resolution and applying image scaling.\n\n"
|
"Allows you to play portrait games in landscape (and vice versa) by transposing resolution and applying image scaling.\n\n"
|
||||||
"Works great for some games, but can COMPLETELY BREAK other games - YMMV!\n\n"
|
"Works great for some games, but can COMPLETELY BREAK other games - YMMV!\n\n"
|
||||||
"Strongly consider combining this with -forceres option to render at monitor native resolution\n\n"
|
"Strongly consider combining this with -forceres option to render at monitor native resolution\n\n"
|
||||||
"WARNING: for SDVX, this messes with camera angle for note lanes, causing it to be zoomed out, and causes performance issues "
|
"WARNING: for SDVX, this messes with camera angle for note lanes, causing it to be zoomed out, and causes performance issues "
|
||||||
@@ -312,7 +334,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.name = "sp2x-dx9on12",
|
.name = "sp2x-dx9on12",
|
||||||
.display_name = "dx9on12",
|
.display_name = "dx9on12",
|
||||||
.aliases= "dx9on12",
|
.aliases= "dx9on12",
|
||||||
.desc = "Use D3D9On12 wrapper library, requires Windows 10. Has no effect games on that don't use DX9. Can cause some games to crash.\n\n"
|
.desc = "Use D3D9On12 wrapper library, requires Windows 10. Has no effect on games that don't use DX9. Can cause some games to crash.\n\n"
|
||||||
"Default: auto (use DX9 for most games, but turn on 9on12 for games that require it on non-NVIDIA GPUs).",
|
"Default: auto (use DX9 for most games, but turn on 9on12 for games that require it on non-NVIDIA GPUs).",
|
||||||
.type = OptionType::Enum,
|
.type = OptionType::Enum,
|
||||||
.category = "Graphics (Common)",
|
.category = "Graphics (Common)",
|
||||||
@@ -446,6 +468,21 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.setting_name = "200",
|
.setting_name = "200",
|
||||||
.category = "Overlay",
|
.category = "Overlay",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// NotificationPosition
|
||||||
|
.title = "Notifications",
|
||||||
|
.name = "toast",
|
||||||
|
.desc = "Select where notifications will appear on the screen.",
|
||||||
|
.type = OptionType::Enum,
|
||||||
|
.category = "Overlay",
|
||||||
|
.elements = {
|
||||||
|
{"off", ""},
|
||||||
|
{"topleft", ""},
|
||||||
|
{"topright", ""},
|
||||||
|
{"bottomleft", ""},
|
||||||
|
{"bottomright", ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// spice2x_FpsAutoShow
|
// spice2x_FpsAutoShow
|
||||||
.title = "Auto Show FPS/Clock",
|
.title = "Auto Show FPS/Clock",
|
||||||
@@ -655,30 +692,6 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.game_name = "Beatmania IIDX",
|
.game_name = "Beatmania IIDX",
|
||||||
.category = "Game Options",
|
.category = "Game Options",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
// IIDXTTDelayP1
|
|
||||||
.title = "IIDX TT Delay ms (Player 1)",
|
|
||||||
.name = "iidxttdelayp1",
|
|
||||||
.desc = "Delays turntable by number of milliseconds. "
|
|
||||||
"Delay will be *at most* the specified period; maximum error is dependent on the game's poll rate. "
|
|
||||||
"As usual, changing any option requires a restart. Default: 0 (no delay).",
|
|
||||||
.type = OptionType::Integer,
|
|
||||||
.setting_name = "(0-500)",
|
|
||||||
.game_name = "Beatmania IIDX",
|
|
||||||
.category = "Game Options (Advanced)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// IIDXTTDelayP2
|
|
||||||
.title = "IIDX TT Delay ms (Player 2)",
|
|
||||||
.name = "iidxttdelayp2",
|
|
||||||
.desc = "Delays turntable by number of milliseconds. "
|
|
||||||
"Delay will be *at most* the specified period; maximum error is dependent on the game's poll rate. "
|
|
||||||
"As usual, changing any option requires a restart. Default: 0 (no delay).",
|
|
||||||
.type = OptionType::Integer,
|
|
||||||
.setting_name = "(0-500)",
|
|
||||||
.game_name = "Beatmania IIDX",
|
|
||||||
.category = "Game Options (Advanced)",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
// spice2x_IIDXDigitalTTSensitivity
|
// spice2x_IIDXDigitalTTSensitivity
|
||||||
.title = "IIDX Digital TT Sensitivity",
|
.title = "IIDX Digital TT Sensitivity",
|
||||||
@@ -1064,7 +1077,10 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
{
|
{
|
||||||
.title = "GitaDora Two Channel Audio",
|
.title = "GitaDora Two Channel Audio",
|
||||||
.name = "2ch",
|
.name = "2ch",
|
||||||
.desc = "Attempt to reduce audio channels down to just two channels.",
|
.desc = "Attempt to reduce audio channels down to just two channels.\n\n"
|
||||||
|
"Arena Model: downmixes 7.1 to stereo using the AC-4 stereo downmix coefficients "
|
||||||
|
"(ETSI TS 103 190-1). The WASAPI Stereo Downmix option, if set, overrides this "
|
||||||
|
"algorithm.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.game_name = "GitaDora",
|
.game_name = "GitaDora",
|
||||||
.category = "Game Options",
|
.category = "Game Options",
|
||||||
@@ -1078,23 +1094,13 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.category = "Game Options",
|
.category = "Game Options",
|
||||||
.elements = {{"1", "DX"}, {"2", "SD"}, {"3", "SD2 - white cab"}},
|
.elements = {{"1", "DX"}, {"2", "SD"}, {"3", "SD2 - white cab"}},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
// GitaDoraArenaSingleWindow
|
|
||||||
.title = "GitaDora Arena Single Window (EXPERIMENTAL)",
|
|
||||||
.name = "gdaonewindow",
|
|
||||||
.desc = "For Arena Model, when combined with windowed mode, only show the main game window; enables subscreen overlay.",
|
|
||||||
.type = OptionType::Bool,
|
|
||||||
.game_name = "GitaDora",
|
|
||||||
.category = "Game Options",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
// GitaDoraLefty
|
// GitaDoraLefty
|
||||||
.title = "GitaDora Lefty Guitar (for Digital Wailing)",
|
.title = "GitaDora Lefty Guitar (for Digital Wailing)",
|
||||||
.name = "gdlefty",
|
.name = "gdlefty",
|
||||||
.desc = "Enables lefty mode, flipping motion sensor directions. Default: off.\n\n"
|
.desc = "Enables lefty mode, flipping motion sensor directions. Default: off.\n\n"
|
||||||
"Without this option, enabling LEFT in the game option will continuously trigger UP wail.\n\n"
|
"Without this option, enabling LEFT in the game option will continuously trigger UP wail.\n\n"
|
||||||
"Has no effect if you are using analog bindings for X/Y axis; expectation is that your controller "
|
"Has no effect if you are using analog bindings for X/Y axis; most controllers do not handle this correctly.\n\n"
|
||||||
"handles this correctly (most do not, however).\n\n"
|
|
||||||
"As always, remember to restart the game after changing options. If you need to change in the game, "
|
"As always, remember to restart the game after changing options. If you need to change in the game, "
|
||||||
"use the I/O panel overlay window.",
|
"use the I/O panel overlay window.",
|
||||||
.type = OptionType::Enum,
|
.type = OptionType::Enum,
|
||||||
@@ -1153,6 +1159,51 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
{"large", ""}
|
{"large", ""}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// GitaDoraArenaSingleWindow
|
||||||
|
.title = "GitaDora Arena Disable Subscreens (DEPRECATED - use -gdalayout)",
|
||||||
|
.name = "gdaonewindow",
|
||||||
|
.desc = "DEPRECATED - use -gdalayout.\n\n"
|
||||||
|
"For Arena Model:\n\n"
|
||||||
|
"Windowed mode: instead of 4 windows, create 1 window.\n\n"
|
||||||
|
"Fullscreen mode: instead of requiring 4 monitors, use only the primary monitor. WARNING: requires 4K monitor.\n\n"
|
||||||
|
"To access the subscreen, use the subscreen overlay.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.hidden = true,
|
||||||
|
.game_name = "GitaDora",
|
||||||
|
.category = "Game Options",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// GitaDoraArenaWindowLayout
|
||||||
|
.title = "GitaDora Arena Layout (EXPERIMENTAL)",
|
||||||
|
.name = "gdalayout",
|
||||||
|
.desc = "For Arena Model: select how many windows to create.\n\n"
|
||||||
|
"1 window: main only; use the subscreen overlay for SMALL.\n\n"
|
||||||
|
"2 windows: main + SMALL. Note: currently only works for windowed mode, broken for fullscreen.\n\n"
|
||||||
|
"4 windows: main + LEFT + RIGHT + SMALL. Fullscreen requires exactly 4 monitors in the "
|
||||||
|
"right resolution; see wiki for details.",
|
||||||
|
.type = OptionType::Enum,
|
||||||
|
.game_name = "GitaDora",
|
||||||
|
.category = "Game Options",
|
||||||
|
.elements = {
|
||||||
|
{"1", "1 window"},
|
||||||
|
{"2", "2 windows"},
|
||||||
|
{"4", "4 windows"}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// GitaDoraArenaAsioDriver
|
||||||
|
.title = "GitaDora Arena ASIO driver",
|
||||||
|
.name = "gdaasio",
|
||||||
|
.desc = "For Arena Model: ASIO driver name to use in place of XONAR. "
|
||||||
|
"String should match a subkey under HKLM\\SOFTWARE\\ASIO\\\n\n"
|
||||||
|
"Requires 7.1 @ 48kHz; if the game rejects your ASIO device, it will automatically "
|
||||||
|
"fall back to using WASAPI.",
|
||||||
|
.type = OptionType::Text,
|
||||||
|
.game_name = "GitaDora",
|
||||||
|
.category = "Game Options",
|
||||||
|
.picker = OptionPickerType::AsioDriver,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.title = "Force Load Jubeat Module",
|
.title = "Force Load Jubeat Module",
|
||||||
.name = "jb",
|
.name = "jb",
|
||||||
@@ -1292,7 +1343,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
{
|
{
|
||||||
.title = "Force Load LovePlus Module",
|
.title = "Force Load LovePlus Module",
|
||||||
.name = "loveplus",
|
.name = "loveplus",
|
||||||
.desc = "manually enable LovePlus module.",
|
.desc = "Manually enable LovePlus module.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.game_name = "LovePlus",
|
.game_name = "LovePlus",
|
||||||
.category = "Game Options (Advanced)",
|
.category = "Game Options (Advanced)",
|
||||||
@@ -1300,7 +1351,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
{
|
{
|
||||||
.title = "Force Load Charge Machine Module",
|
.title = "Force Load Charge Machine Module",
|
||||||
.name = "pcm",
|
.name = "pcm",
|
||||||
.desc = "manually enable Charge Machine module.",
|
.desc = "Manually enable Charge Machine module.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.game_name = "Charge Machine",
|
.game_name = "Charge Machine",
|
||||||
.category = "Game Options (Advanced)",
|
.category = "Game Options (Advanced)",
|
||||||
@@ -1308,7 +1359,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
{
|
{
|
||||||
.title = "Force Load Ongaku Paradise Module",
|
.title = "Force Load Ongaku Paradise Module",
|
||||||
.name = "onpara",
|
.name = "onpara",
|
||||||
.desc = "manually enable Ongaku Paradise module.",
|
.desc = "Manually enable Ongaku Paradise module.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.game_name = "Ongaku Paradise",
|
.game_name = "Ongaku Paradise",
|
||||||
.category = "Game Options (Advanced)",
|
.category = "Game Options (Advanced)",
|
||||||
@@ -1316,7 +1367,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
{
|
{
|
||||||
.title = "Force Load Busou Shinki Module",
|
.title = "Force Load Busou Shinki Module",
|
||||||
.name = "busou",
|
.name = "busou",
|
||||||
.desc = "manually enable Busou Shinki module.",
|
.desc = "Manually enable Busou Shinki module.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.game_name = "Busou Shinki: Armored Princess Battle Conductor",
|
.game_name = "Busou Shinki: Armored Princess Battle Conductor",
|
||||||
.category = "Game Options (Advanced)",
|
.category = "Game Options (Advanced)",
|
||||||
@@ -1325,7 +1376,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
// LoadCCJModule
|
// LoadCCJModule
|
||||||
.title = "Force Load Chase Chase Jokers Module",
|
.title = "Force Load Chase Chase Jokers Module",
|
||||||
.name = "ccj",
|
.name = "ccj",
|
||||||
.desc = "manually enable Chase Chase Jokers module.",
|
.desc = "Manually enable Chase Chase Jokers module.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.game_name = "Chase Chase Jokers",
|
.game_name = "Chase Chase Jokers",
|
||||||
.category = "Game Options (Advanced)",
|
.category = "Game Options (Advanced)",
|
||||||
@@ -1334,7 +1385,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
// LoadQKSModule
|
// LoadQKSModule
|
||||||
.title = "Force Load QuizKnock STADIUM Module",
|
.title = "Force Load QuizKnock STADIUM Module",
|
||||||
.name = "qks",
|
.name = "qks",
|
||||||
.desc = "manually enable QuizKnock STADIUM module",
|
.desc = "Manually enable QuizKnock STADIUM module.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.game_name = "QuizKnock STADIUM",
|
.game_name = "QuizKnock STADIUM",
|
||||||
.category = "Game Options (Advanced)",
|
.category = "Game Options (Advanced)",
|
||||||
@@ -1343,7 +1394,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
// LoadMFGModule
|
// LoadMFGModule
|
||||||
.title = "Force Load Mahjong Fight Girl Module",
|
.title = "Force Load Mahjong Fight Girl Module",
|
||||||
.name = "mfg",
|
.name = "mfg",
|
||||||
.desc = "manually enable Mahjong Fight Girl module.",
|
.desc = "Manually enable Mahjong Fight Girl module.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.game_name = "Mahjong Fight Girl",
|
.game_name = "Mahjong Fight Girl",
|
||||||
.category = "Game Options (Advanced)",
|
.category = "Game Options (Advanced)",
|
||||||
@@ -1352,7 +1403,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
// LoadPCModule
|
// LoadPCModule
|
||||||
.title = "Force Load Polaris Chord Module",
|
.title = "Force Load Polaris Chord Module",
|
||||||
.name = "pc",
|
.name = "pc",
|
||||||
.desc = "manually enable Polaris Chord module.",
|
.desc = "Manually enable Polaris Chord module.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.game_name = "Polaris Chord",
|
.game_name = "Polaris Chord",
|
||||||
.category = "Game Options (Advanced)",
|
.category = "Game Options (Advanced)",
|
||||||
@@ -1361,7 +1412,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
// LoadMusecaModule
|
// LoadMusecaModule
|
||||||
.title = "Force Load Museca Module",
|
.title = "Force Load Museca Module",
|
||||||
.name = "museca",
|
.name = "museca",
|
||||||
.desc = "manually enable Museca module.",
|
.desc = "Manually enable Museca module.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.game_name = "Museca",
|
.game_name = "Museca",
|
||||||
.category = "Game Options (Advanced)",
|
.category = "Game Options (Advanced)",
|
||||||
@@ -1485,7 +1536,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
{
|
{
|
||||||
.title = "API Verbose Logging",
|
.title = "API Verbose Logging",
|
||||||
.name = "apilogging",
|
.name = "apilogging",
|
||||||
.desc = "verbose logging of API activity.",
|
.desc = "Verbose logging of API activity.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "API (Dev)",
|
.category = "API (Dev)",
|
||||||
},
|
},
|
||||||
@@ -1608,8 +1659,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.title = "Force Touch Emulation",
|
.title = "Force Touch Emulation",
|
||||||
.name = "touchemuforce",
|
.name = "touchemuforce",
|
||||||
.desc = "Force enable hook for GetTouchInputInfo API and inject WM_TOUCH events. "
|
.desc = "Force enable hook for GetTouchInputInfo API and inject WM_TOUCH events. "
|
||||||
"This is automatically turned on when needed, so it is not typically required to "
|
"This is automatically enabled when needed and is typically not required. Do not enable this unless you have trouble.",
|
||||||
"turn it on manually. Do not enable this unless you have trouble.",
|
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "Touch Parameters",
|
.category = "Touch Parameters",
|
||||||
},
|
},
|
||||||
@@ -1712,7 +1762,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
"legacy: Preserve buggy old behavior (Default)\n\n"
|
"legacy: Preserve buggy old behavior (Default)\n\n"
|
||||||
"fix: Add E00401 to non-FeliCa cards, scan FeliCa cards as-is (Recommended for most users)\n\n"
|
"fix: Add E00401 to non-FeliCa cards, scan FeliCa cards as-is (Recommended for most users)\n\n"
|
||||||
"all: Add E00401 to all cards, including FeliCa cards (For really old games only)\n\n"
|
"all: Add E00401 to all cards, including FeliCa cards (For really old games only)\n\n"
|
||||||
"The algorithm is simple; the prefix is applied, the scanned card number is appended upto 8 bytes, and remaining digits are discarded. "
|
"The algorithm is simple; the prefix is applied, the scanned card number is appended up to 8 bytes, and remaining digits are discarded. "
|
||||||
"For example, abcd56780123 is converted to e00401abcd567801 on card in. This results in different card numbers so "
|
"For example, abcd56780123 is converted to e00401abcd567801 on card in. This results in different card numbers so "
|
||||||
"be careful when connecting to servers!",
|
"be careful when connecting to servers!",
|
||||||
.type = OptionType::Enum,
|
.type = OptionType::Enum,
|
||||||
@@ -1743,7 +1793,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
{
|
{
|
||||||
.title = "Realtime Process Priority (DEPRECATED - use -processpriority instead)",
|
.title = "Realtime Process Priority (DEPRECATED - use -processpriority instead)",
|
||||||
.name = "realtime",
|
.name = "realtime",
|
||||||
.desc = "Sets the process priority to realtime, can help with odd lag spikes.",
|
.desc = "Sets the process priority to realtime; can help with odd lag spikes.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.hidden = true,
|
.hidden = true,
|
||||||
.category = "Performance",
|
.category = "Performance",
|
||||||
@@ -1754,7 +1804,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.name = "sp2x-processpriority",
|
.name = "sp2x-processpriority",
|
||||||
.display_name = "processpriority",
|
.display_name = "processpriority",
|
||||||
.aliases= "processpriority",
|
.aliases= "processpriority",
|
||||||
.desc = "Sets the process priority, can help with odd lag spikes. Default: high.",
|
.desc = "Sets the process priority, which can help with odd lag spikes. Default: high.",
|
||||||
.type = OptionType::Enum,
|
.type = OptionType::Enum,
|
||||||
.category = "Performance",
|
.category = "Performance",
|
||||||
.elements = {
|
.elements = {
|
||||||
@@ -1831,7 +1881,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.desc = "Disables all audio hooks, including device initialization and volume hooks.\n\n"
|
.desc = "Disables all audio hooks, including device initialization and volume hooks.\n\n"
|
||||||
"Default: off (allow Spice to hook IMMDeviceEnumerator for and optionally let "
|
"Default: off (allow Spice to hook IMMDeviceEnumerator for and optionally let "
|
||||||
"you use a different backend instead of exclusive WASAPI).\n\n"
|
"you use a different backend instead of exclusive WASAPI).\n\n"
|
||||||
"Check this if you want games to natively access your audio device.",
|
"Check this to allow games to natively access your audio device.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "Audio (Hacks)",
|
.category = "Audio (Hacks)",
|
||||||
},
|
},
|
||||||
@@ -1843,7 +1893,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.aliases= "volumehookdisable",
|
.aliases= "volumehookdisable",
|
||||||
.desc = "Disables audio volume hook.\n\n"
|
.desc = "Disables audio volume hook.\n\n"
|
||||||
"Default: off (prevent games from changing audio volume by hooking IAudioEndpointVolume).\n\n"
|
"Default: off (prevent games from changing audio volume by hooking IAudioEndpointVolume).\n\n"
|
||||||
"Check this if you want games to freely change your volume.",
|
"Check this to allow games to freely change your volume.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "Audio (Hacks)",
|
.category = "Audio (Hacks)",
|
||||||
},
|
},
|
||||||
@@ -1891,6 +1941,49 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.hidden = true,
|
.hidden = true,
|
||||||
.category = "Audio (Hacks)",
|
.category = "Audio (Hacks)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// DownmixAudioToStereo
|
||||||
|
.title = "WASAPI Stereo Downmix",
|
||||||
|
.name = "downmix",
|
||||||
|
.desc = "Downmixes multi-channel (surround) audio output to stereo.\n\n"
|
||||||
|
"Note that in all of the options, subwoofer (LFE) is dropped.\n\n"
|
||||||
|
"ac4 (recommended): AC-4 algorithm (ETSI TS 103 190-1) - front left/right pass "
|
||||||
|
"through, center and surrounds fold in at -3 dB.\n\n"
|
||||||
|
"normalize: all channels equally loud.\n\n"
|
||||||
|
"front: front channels only.\n\n"
|
||||||
|
"rear: rear channels only.\n\n"
|
||||||
|
"side: side channels only.",
|
||||||
|
.type = OptionType::Enum,
|
||||||
|
.category = "Audio",
|
||||||
|
.elements = {
|
||||||
|
{"ac4", "AC-4 downmix"},
|
||||||
|
{"normalize", "All channels"},
|
||||||
|
{"front", "Front channels only"},
|
||||||
|
{"rear", "Rear channels only"},
|
||||||
|
{"side", "Side channels only"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// VolumeBoost
|
||||||
|
.title = "WASAPI Boost Audio Volume",
|
||||||
|
.name = "volumeboost",
|
||||||
|
.desc = "Artificially amplifies the hooked audio output by the selected amount, applied "
|
||||||
|
"right before the audio reaches the device. Works regardless of channel layout or "
|
||||||
|
"downmixing.\n\n"
|
||||||
|
"Louder settings may cause clipping/distortion, use caution.",
|
||||||
|
.type = OptionType::Enum,
|
||||||
|
.category = "Audio",
|
||||||
|
.elements = {
|
||||||
|
{"3", "+3 dB"},
|
||||||
|
{"6", "+6 dB"},
|
||||||
|
{"9", "+9 dB"},
|
||||||
|
{"12", "+12 dB"},
|
||||||
|
{"15", "+15 dB"},
|
||||||
|
{"20", "+20 dB"},
|
||||||
|
{"25", "+25 dB"},
|
||||||
|
{"30", "+30 dB"},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// DelayBy5Seconds
|
// DelayBy5Seconds
|
||||||
.title = "Delay by 5 Seconds (DEPRECATED - use -sleepduration instead)",
|
.title = "Delay by 5 Seconds (DEPRECATED - use -sleepduration instead)",
|
||||||
@@ -1944,6 +2037,15 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
{"3", "Landscape, Flipped"}
|
{"3", "Landscape, Flipped"}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// ChangeResolution
|
||||||
|
.title = "Change Monitor Resolution",
|
||||||
|
.name = "changeres",
|
||||||
|
.desc = "Changes monitor resolution before booting the game.",
|
||||||
|
.type = OptionType::Text,
|
||||||
|
.setting_name = "1280,720",
|
||||||
|
.category = "Monitor"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.title = "AVS Log Level",
|
.title = "AVS Log Level",
|
||||||
.name = "loglevel",
|
.name = "loglevel",
|
||||||
@@ -1976,7 +2078,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
{
|
{
|
||||||
.title = "Blocking Logger",
|
.title = "Blocking Logger",
|
||||||
.name = "logblock",
|
.name = "logblock",
|
||||||
.desc = "Slower but safer logging used for debugging.",
|
.desc = "Slower but safer logging for debugging.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "Debug Log",
|
.category = "Debug Log",
|
||||||
},
|
},
|
||||||
@@ -2263,6 +2365,54 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "Graphics (Windowed)",
|
.category = "Graphics (Windowed)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// GitaDoraWindowedMainMonitor
|
||||||
|
.title = "GitaDora Windowed Main Monitor",
|
||||||
|
.name = "gdwmainmonitor",
|
||||||
|
.desc = "For GITADORA Arena windowed mode: place and size the "
|
||||||
|
"main window to fill this monitor.",
|
||||||
|
.type = OptionType::Text,
|
||||||
|
.setting_name = "\\\\.\\DISPLAY1",
|
||||||
|
.game_name = "GitaDora",
|
||||||
|
.category = "Graphics (Windowed)",
|
||||||
|
.picker = OptionPickerType::Monitor,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// GitaDoraWindowedLeftMonitor
|
||||||
|
.title = "GitaDora Windowed LEFT Monitor",
|
||||||
|
.name = "gdwleftmonitor",
|
||||||
|
.desc = "For GITADORA Arena windowed mode: place and size the "
|
||||||
|
"LEFT window to fill this monitor.",
|
||||||
|
.type = OptionType::Text,
|
||||||
|
.setting_name = "\\\\.\\DISPLAY2",
|
||||||
|
.game_name = "GitaDora",
|
||||||
|
.category = "Graphics (Windowed)",
|
||||||
|
.picker = OptionPickerType::Monitor,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// GitaDoraWindowedRightMonitor
|
||||||
|
.title = "GitaDora Windowed RIGHT Monitor",
|
||||||
|
.name = "gdwrightmonitor",
|
||||||
|
.desc = "For GITADORA Arena windowed mode: place and size the "
|
||||||
|
"RIGHT window to fill this monitor.",
|
||||||
|
.type = OptionType::Text,
|
||||||
|
.setting_name = "\\\\.\\DISPLAY3",
|
||||||
|
.game_name = "GitaDora",
|
||||||
|
.category = "Graphics (Windowed)",
|
||||||
|
.picker = OptionPickerType::Monitor,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// GitaDoraWindowedSmallMonitor
|
||||||
|
.title = "GitaDora Windowed SMALL Monitor",
|
||||||
|
.name = "gdwsmallmonitor",
|
||||||
|
.desc = "For GITADORA Arena windowed mode: place and size the "
|
||||||
|
"SMALL touch window to fill this monitor.",
|
||||||
|
.type = OptionType::Text,
|
||||||
|
.setting_name = "\\\\.\\DISPLAY4",
|
||||||
|
.game_name = "GitaDora",
|
||||||
|
.category = "Graphics (Windowed)",
|
||||||
|
.picker = OptionPickerType::Monitor,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// spice2x_IIDXWindowedSubscreenSize
|
// spice2x_IIDXWindowedSubscreenSize
|
||||||
.title = "IIDX Windowed Subscreen Size",
|
.title = "IIDX Windowed Subscreen Size",
|
||||||
@@ -2303,19 +2453,35 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// spice2x_JubeatLegacyTouch
|
// spice2x_JubeatLegacyTouch
|
||||||
.title = "JB Legacy Touch Targets",
|
.title = "JB Legacy Touch Targets (Deprecated - use -jubeattouchalgo instead)",
|
||||||
.name = "sp2x-jubeatlegacytouch",
|
.name = "sp2x-jubeatlegacytouch",
|
||||||
.display_name = "jubeatlegacytouch",
|
.display_name = "jubeatlegacytouch",
|
||||||
.aliases= "jubeatlegacytouch",
|
.aliases= "jubeatlegacytouch",
|
||||||
.desc = "For touch screen players - use the legacy & less accurate grid-based layout for touch recognition, "
|
.desc = "Deprecated - use -jubeattouchalgo instead.",
|
||||||
"instead of the new & more accurate touch targets. Default: off.",
|
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
|
.hidden = true,
|
||||||
.game_name = "Jubeat",
|
.game_name = "Jubeat",
|
||||||
.category = "Game Options",
|
.category = "Game Options",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.title = "JB Touch Algorithm",
|
||||||
|
.name = "jubeattouchalgo",
|
||||||
|
.desc = "For touch screen players: choose the touch algorithm to use.\n\n"
|
||||||
|
"legacy - evenly divide the grid into 16 squares; old spicetools behavior, slightly inaccurate in gaps\n\n"
|
||||||
|
"improved (default) - squares register as-is, gaps will trigger the closest square\n\n"
|
||||||
|
"accurate - only touches within squares will trigger; gaps do nothing (for AC size touch screens)",
|
||||||
|
.type = OptionType::Enum,
|
||||||
|
.game_name = "Jubeat",
|
||||||
|
.category = "Game Options",
|
||||||
|
.elements = {
|
||||||
|
{"legacy", ""},
|
||||||
|
{"improved", ""},
|
||||||
|
{"accurate", ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// spice2x_RBTouchScale
|
// spice2x_RBTouchScale
|
||||||
.title = "RB Scale Touch Input",
|
.title = "RB Touch Emulation Scale",
|
||||||
.name = "sp2x-rbscaletouch",
|
.name = "sp2x-rbscaletouch",
|
||||||
.display_name = "rbscaletouch",
|
.display_name = "rbscaletouch",
|
||||||
.aliases= "rbscaletouch",
|
.aliases= "rbscaletouch",
|
||||||
@@ -2325,6 +2491,31 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.game_name = "Reflec Beat",
|
.game_name = "Reflec Beat",
|
||||||
.category = "Game Options",
|
.category = "Game Options",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// RBTouchSize
|
||||||
|
.title = "RB Touch Emulation Size",
|
||||||
|
.name = "rbtouchsize",
|
||||||
|
.desc = "Size of the touch area; how many IR sensors a single finger activates. Default: 1 (1x1).",
|
||||||
|
.type = OptionType::Enum,
|
||||||
|
.game_name = "Reflec Beat",
|
||||||
|
.category = "Game Options",
|
||||||
|
.elements = {
|
||||||
|
{"1", "1x1"},
|
||||||
|
{"3", "3x3"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// RBTouchPollRate
|
||||||
|
.title = "RB Touch Emulation Poll Hz",
|
||||||
|
.name = "rbtouchhz",
|
||||||
|
.desc = "By default, the game polls for touch at 120Hz. "
|
||||||
|
"This option overrides that rate; enter a number betwen 1 and 1000.\n\n"
|
||||||
|
"It should be noted that higher poll does not necessarily improve accuracy or performance.",
|
||||||
|
.type = OptionType::Integer,
|
||||||
|
.setting_name = "250",
|
||||||
|
.game_name = "Reflec Beat",
|
||||||
|
.category = "Game Options",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// spice2x_AsioForceUnload
|
// spice2x_AsioForceUnload
|
||||||
.title = "ASIO Force Unload On Stop",
|
.title = "ASIO Force Unload On Stop",
|
||||||
@@ -2501,7 +2692,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.aliases= "nonvapi",
|
.aliases= "nonvapi",
|
||||||
.desc = "Completely block the game from accessing NVAPI. Can be used if NVAPI is returning undesirable "
|
.desc = "Completely block the game from accessing NVAPI. Can be used if NVAPI is returning undesirable "
|
||||||
"results to the game (e.g., wrong values from NvDisplayConfig for SDVX). You may need to apply additional "
|
"results to the game (e.g., wrong values from NvDisplayConfig for SDVX). You may need to apply additional "
|
||||||
"hex edits in order to boot the game correctly.",
|
"hex edits to boot the game correctly.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "Graphics (Common)",
|
.category = "Graphics (Common)",
|
||||||
},
|
},
|
||||||
@@ -2549,7 +2740,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.name = "sp2x-smx-stage",
|
.name = "sp2x-smx-stage",
|
||||||
.display_name = "smxstage",
|
.display_name = "smxstage",
|
||||||
.aliases= "smxstage",
|
.aliases= "smxstage",
|
||||||
.desc = "StepmaniaX stage will show up as a device that can recieve lighting output. "
|
.desc = "StepmaniaX stage will show up as a device that can receive lighting output. "
|
||||||
"For configurator, restart spicecfg.exe after enabling this to have the device show up for binding.",
|
"For configurator, restart spicecfg.exe after enabling this to have the device show up for binding.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "I/O Options",
|
.category = "I/O Options",
|
||||||
@@ -2560,7 +2751,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.name = "sp2x-smx-dedicab",
|
.name = "sp2x-smx-dedicab",
|
||||||
.display_name = "smxdedicab",
|
.display_name = "smxdedicab",
|
||||||
.aliases = "smxdedicab",
|
.aliases = "smxdedicab",
|
||||||
.desc = "StepManiaX Dedicated Cabinet will show up as a device that can recieve lighting output. "
|
.desc = "StepManiaX Dedicated Cabinet will show up as a device that can receive lighting output. "
|
||||||
"For configurator, restart spicecfg.exe after enabling this to have the device show up for binding.",
|
"For configurator, restart spicecfg.exe after enabling this to have the device show up for binding.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "I/O Options"
|
.category = "I/O Options"
|
||||||
@@ -2664,7 +2855,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
"Enables emulation of touch gestures to make it easier to navigate the game without a touchscreen.\n\n"
|
"Enables emulation of touch gestures to make it easier to navigate the game without a touchscreen.\n\n"
|
||||||
"This enables two things:\n"
|
"This enables two things:\n"
|
||||||
" 1. Swipe/Touch bindings in Buttons tab (Swipe Next Page, etc)\n"
|
" 1. Swipe/Touch bindings in Buttons tab (Swipe Next Page, etc)\n"
|
||||||
" 2. PIN pad bindings (make sure Unscramble Keypad patch is applied)).",
|
" 2. PIN pad bindings (make sure Unscramble Keypad patch is applied).",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.game_name = "Nostalgia",
|
.game_name = "Nostalgia",
|
||||||
.category = "Game Options",
|
.category = "Game Options",
|
||||||
@@ -2793,7 +2984,51 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.game_name = "Otoca D'or",
|
.game_name = "Otoca D'or",
|
||||||
.category = "Game Options (Peripherals)",
|
.category = "Game Options (Peripherals)",
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
// DisableHighResTimer
|
||||||
|
.title = "Use Legacy Timers",
|
||||||
|
.name = "notimerhacks",
|
||||||
|
.desc = "Disables high resolution timers, reverting to legacy behavior. "
|
||||||
|
"Recommended that you leave this OFF for optimal input latency, "
|
||||||
|
"unless you're on a resource-constrained system (e.g., old cabinet PC)",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.category = "Development"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// EnableICMPHook
|
||||||
|
.title = "Enable ICMP Emulation",
|
||||||
|
.name = "icmphook",
|
||||||
|
.desc = "Emulate keepalive ping replies in user mode, so the game does not need to "
|
||||||
|
"open privileged raw ICMP sockets.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.category = "Network (Development)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// AutoElevate
|
||||||
|
.title = "Run as",
|
||||||
|
.name = "runas",
|
||||||
|
.desc = "Controls whether spice will automatically re-launch with administrator privileges at startup.\n\n"
|
||||||
|
"admin (default): automatically re-launch with administrator privileges, "
|
||||||
|
"needed for most games to function properly\n\n"
|
||||||
|
"user: skip elevation, run with current user privileges; game may fail to launch, "
|
||||||
|
"crash, or critical features may silently fail; use at your own risk.",
|
||||||
|
.type = OptionType::Enum,
|
||||||
|
.category = "Development",
|
||||||
|
.elements = {
|
||||||
|
{"admin", ""},
|
||||||
|
{"user", ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// CfgForceSoftwareRender
|
||||||
|
.title = "Configurator Force Software Rendering",
|
||||||
|
.name = "forcesoftware",
|
||||||
|
.desc = "Forces the use of software rendering instead of hardware acceleration for "
|
||||||
|
"the configurator.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.category = "Development",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) {
|
const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ namespace launcher {
|
|||||||
Player2Card,
|
Player2Card,
|
||||||
Player1PinMacro,
|
Player1PinMacro,
|
||||||
Player2PinMacro,
|
Player2PinMacro,
|
||||||
|
AutoPinMacroTrigger0,
|
||||||
|
AutoPinMacroTrigger1,
|
||||||
WindowedMode,
|
WindowedMode,
|
||||||
InjectHook,
|
InjectHook,
|
||||||
EarlyInjectHook,
|
EarlyInjectHook,
|
||||||
@@ -51,6 +53,7 @@ namespace launcher {
|
|||||||
VREnable,
|
VREnable,
|
||||||
DisableOverlay,
|
DisableOverlay,
|
||||||
OverlayScaling,
|
OverlayScaling,
|
||||||
|
NotificationPosition,
|
||||||
spice2x_FpsAutoShow,
|
spice2x_FpsAutoShow,
|
||||||
spice2x_FpsOpposite,
|
spice2x_FpsOpposite,
|
||||||
spice2x_SubScreenAutoShow,
|
spice2x_SubScreenAutoShow,
|
||||||
@@ -69,8 +72,6 @@ namespace launcher {
|
|||||||
IIDXAsioDriver,
|
IIDXAsioDriver,
|
||||||
IIDXBIO2FW,
|
IIDXBIO2FW,
|
||||||
IIDXTDJMode,
|
IIDXTDJMode,
|
||||||
IIDXTTDelayP1,
|
|
||||||
IIDXTTDelayP2,
|
|
||||||
spice2x_IIDXDigitalTTSensitivity,
|
spice2x_IIDXDigitalTTSensitivity,
|
||||||
IIDXDigitalTTSocd,
|
IIDXDigitalTTSocd,
|
||||||
spice2x_IIDXLDJForce720p,
|
spice2x_IIDXLDJForce720p,
|
||||||
@@ -108,11 +109,13 @@ namespace launcher {
|
|||||||
LoadGitaDoraModule,
|
LoadGitaDoraModule,
|
||||||
GitaDoraTwoChannelAudio,
|
GitaDoraTwoChannelAudio,
|
||||||
GitaDoraCabinetType,
|
GitaDoraCabinetType,
|
||||||
GitaDoraArenaSingleWindow,
|
|
||||||
GitaDoraLefty,
|
GitaDoraLefty,
|
||||||
GitaDoraWailHold,
|
GitaDoraWailHold,
|
||||||
GitaDoraPickAlgo,
|
GitaDoraPickAlgo,
|
||||||
GitaDoraSubOverlaySize,
|
GitaDoraSubOverlaySize,
|
||||||
|
GitaDoraArenaSingleWindow,
|
||||||
|
GitaDoraArenaWindowLayout,
|
||||||
|
GitaDoraArenaAsioDriver,
|
||||||
LoadJubeatModule,
|
LoadJubeatModule,
|
||||||
LoadReflecBeatModule,
|
LoadReflecBeatModule,
|
||||||
LoadShogikaiModule,
|
LoadShogikaiModule,
|
||||||
@@ -196,11 +199,14 @@ namespace launcher {
|
|||||||
AsioDriverId,
|
AsioDriverId,
|
||||||
AsioDriverName,
|
AsioDriverName,
|
||||||
AudioDummy,
|
AudioDummy,
|
||||||
|
DownmixAudioToStereo,
|
||||||
|
VolumeBoost,
|
||||||
DelayBy5Seconds,
|
DelayBy5Seconds,
|
||||||
spice2x_DelayByNSeconds,
|
spice2x_DelayByNSeconds,
|
||||||
LoadStubs,
|
LoadStubs,
|
||||||
AdjustOrientation,
|
AdjustOrientation,
|
||||||
spice2x_AutoOrientation,
|
spice2x_AutoOrientation,
|
||||||
|
ChangeResolution,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
EAAutomap,
|
EAAutomap,
|
||||||
EANetdump,
|
EANetdump,
|
||||||
@@ -236,12 +242,19 @@ namespace launcher {
|
|||||||
spice2x_WindowAlwaysOnTop,
|
spice2x_WindowAlwaysOnTop,
|
||||||
WindowForceScaling,
|
WindowForceScaling,
|
||||||
WindowDisableRoundedCorners,
|
WindowDisableRoundedCorners,
|
||||||
|
GitaDoraWindowedMainMonitor,
|
||||||
|
GitaDoraWindowedLeftMonitor,
|
||||||
|
GitaDoraWindowedRightMonitor,
|
||||||
|
GitaDoraWindowedSmallMonitor,
|
||||||
spice2x_IIDXWindowedSubscreenSize,
|
spice2x_IIDXWindowedSubscreenSize,
|
||||||
spice2x_IIDXWindowedSubscreenPosition,
|
spice2x_IIDXWindowedSubscreenPosition,
|
||||||
IIDXWindowedSubscreenBorderless,
|
IIDXWindowedSubscreenBorderless,
|
||||||
IIDXWindowedSubscreenAlwaysOnTop,
|
IIDXWindowedSubscreenAlwaysOnTop,
|
||||||
spice2x_JubeatLegacyTouch,
|
spice2x_JubeatLegacyTouch,
|
||||||
|
JubeatTouchAlgo,
|
||||||
spice2x_RBTouchScale,
|
spice2x_RBTouchScale,
|
||||||
|
RBTouchSize,
|
||||||
|
RBTouchPollRate,
|
||||||
spice2x_AsioForceUnload,
|
spice2x_AsioForceUnload,
|
||||||
spice2x_IIDXNoESpec,
|
spice2x_IIDXNoESpec,
|
||||||
spice2x_IIDXWindowedTDJ,
|
spice2x_IIDXWindowedTDJ,
|
||||||
@@ -281,6 +294,10 @@ namespace launcher {
|
|||||||
LovePlusPrinterJPGQuality,
|
LovePlusPrinterJPGQuality,
|
||||||
OptionConflictResolution,
|
OptionConflictResolution,
|
||||||
OtocaCamHook,
|
OtocaCamHook,
|
||||||
|
DisableHighResTimer,
|
||||||
|
EnableICMPHook,
|
||||||
|
AutoElevate,
|
||||||
|
CfgForceSoftwareRender
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class OptionsCategory {
|
enum class OptionsCategory {
|
||||||
|
|||||||
@@ -8,12 +8,14 @@
|
|||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
#include "hooks/audio/audio.h"
|
#include "hooks/audio/audio.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "hooks/graphics/backends/d3d11/d3d11_backend.h"
|
||||||
#include "util/deferlog.h"
|
#include "util/deferlog.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
|
||||||
#include "launcher.h"
|
#include "launcher.h"
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "nvapi/nvapi.h"
|
#include "nvapi/nvapi.h"
|
||||||
|
#include "sdk/sdk.h"
|
||||||
|
|
||||||
namespace launcher {
|
namespace launcher {
|
||||||
|
|
||||||
@@ -23,6 +25,12 @@ namespace launcher {
|
|||||||
// therefore, subsystems need to be guarded against multiple unload attempts
|
// therefore, subsystems need to be guarded against multiple unload attempts
|
||||||
log_info("launcher", "stopping subsystems");
|
log_info("launcher", "stopping subsystems");
|
||||||
|
|
||||||
|
sdk::fini_sdk_modules();
|
||||||
|
|
||||||
|
// stop dx11 background workers (poll thread, LDR notification)
|
||||||
|
// before anything else, so they can't race against the teardown.
|
||||||
|
graphics_d3d11_shutdown();
|
||||||
|
|
||||||
// reset monitor settings
|
// reset monitor settings
|
||||||
reset_monitor_on_exit();
|
reset_monitor_on_exit();
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "util/memutils.h"
|
||||||
#include "cfg/configurator.h"
|
#include "cfg/configurator.h"
|
||||||
|
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
@@ -175,6 +176,9 @@ static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *Exception
|
|||||||
log_warning("signal", "minidump creation function not available, skipping");
|
log_warning("signal", "minidump creation function not available, skipping");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dump memory information
|
||||||
|
memutils::show_available_memory();
|
||||||
|
|
||||||
// this will stall all UI threads for this process
|
// this will stall all UI threads for this process
|
||||||
show_popup_for_crash();
|
show_popup_for_crash();
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ namespace superexit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool async_key_exit = GetAsyncKeyState(VK_MENU) && GetAsyncKeyState(VK_F4);
|
bool async_key_exit =
|
||||||
|
(GetAsyncKeyState(VK_MENU) & 0x8000) != 0 &&
|
||||||
|
(GetAsyncKeyState(VK_F4) & 0x8000) != 0;
|
||||||
|
|
||||||
bool overlay_exit = false;
|
bool overlay_exit = false;
|
||||||
auto buttons = games::get_buttons_overlay(eamuse_get_game());
|
auto buttons = games::get_buttons_overlay(eamuse_get_game());
|
||||||
|
|||||||
+93
-11
@@ -1,5 +1,6 @@
|
|||||||
#include "eamuse.h"
|
#include "eamuse.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
@@ -9,17 +10,20 @@
|
|||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
#include "games/sdvx/sdvx.h"
|
#include "games/sdvx/sdvx.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
#include "util/time.h"
|
#include "util/time.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
|
#include "overlay/notifications.h"
|
||||||
|
|
||||||
#include "bt5api.h"
|
#include "bt5api.h"
|
||||||
|
|
||||||
// state
|
// state
|
||||||
|
static constexpr double NOTIFICATION_THROTTLE_SECONDS = 3.0;
|
||||||
static bool CARD_INSERT[2] = {false, false};
|
static bool CARD_INSERT[2] = {false, false};
|
||||||
static double CARD_INSERT_TIME[2] = {0, 0};
|
static double CARD_INSERT_TIME[2] = {0, 0};
|
||||||
static double CARD_INSERT_TIMEOUT = 2.0;
|
static double CARD_INSERT_TIMEOUT = 2.0;
|
||||||
static char CARD_INSERT_UID[2][8];
|
static char CARD_INSERT_UID[2][8] = {{0}, {0}};
|
||||||
static char CARD_INSERT_UID_ENABLE[2] = {false, false};
|
static char CARD_INSERT_UID_ENABLE[2] = {false, false};
|
||||||
static int COIN_STOCK = 0;
|
static int COIN_STOCK = 0;
|
||||||
static bool COIN_BLOCK = false;
|
static bool COIN_BLOCK = false;
|
||||||
@@ -43,6 +47,9 @@ static uint8_t AUTO_INSERT_CARD_CACHED_DATA[2][8];
|
|||||||
// pin macro
|
// pin macro
|
||||||
bool PIN_MACRO_ENABLED = false;
|
bool PIN_MACRO_ENABLED = false;
|
||||||
std::string PIN_MACRO_VALUES[2] = {"", ""};
|
std::string PIN_MACRO_VALUES[2] = {"", ""};
|
||||||
|
std::string AUTO_PIN_MACRO_TRIGGER[2];
|
||||||
|
static std::atomic_bool AUTO_PIN_MACRO_REQUEST[2] {false, false};
|
||||||
|
static bool AUTO_PIN_MACRO_PLAYER_ACTIVE[2] = {false, false};
|
||||||
static std::thread *PIN_MACRO_THREAD = nullptr;
|
static std::thread *PIN_MACRO_THREAD = nullptr;
|
||||||
static bool PIN_MACRO_THREAD_ACTIVE = false;
|
static bool PIN_MACRO_THREAD_ACTIVE = false;
|
||||||
static uint16_t PIN_MACRO_TRIGGER_KEYS[2] = {
|
static uint16_t PIN_MACRO_TRIGGER_KEYS[2] = {
|
||||||
@@ -50,6 +57,18 @@ static uint16_t PIN_MACRO_TRIGGER_KEYS[2] = {
|
|||||||
games::OverlayButtons::TriggerPinMacroP2
|
games::OverlayButtons::TriggerPinMacroP2
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static bool pin_macro_log_hook(void *, const std::string &data, logger::Style, std::string &) {
|
||||||
|
for (int unit = 0; unit < 2; unit++) {
|
||||||
|
if (!AUTO_PIN_MACRO_PLAYER_ACTIVE[unit]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (data.find(AUTO_PIN_MACRO_TRIGGER[unit]) != std::string::npos) {
|
||||||
|
AUTO_PIN_MACRO_REQUEST[unit].store(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool eamuse_get_card(int active_count, int unit_id, uint8_t *card) {
|
bool eamuse_get_card(int active_count, int unit_id, uint8_t *card) {
|
||||||
|
|
||||||
// get unit index
|
// get unit index
|
||||||
@@ -102,6 +121,12 @@ bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int index
|
|||||||
"{} card override contains an invalid character sequence at byte {} (16 characters, 0-9/A-F only)",
|
"{} card override contains an invalid character sequence at byte {} (16 characters, 0-9/A-F only)",
|
||||||
card_override, n);
|
card_override, n);
|
||||||
|
|
||||||
|
overlay::notifications::add_throttled(
|
||||||
|
overlay::notifications::Severity::Error,
|
||||||
|
fmt::format("eamuse.card_override_error.p{}", index + 1),
|
||||||
|
NOTIFICATION_THROTTLE_SECONDS,
|
||||||
|
fmt::format("[P{}] invalid card override", index + 1));
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,6 +157,11 @@ bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card,
|
|||||||
std::ifstream f(path);
|
std::ifstream f(path);
|
||||||
if (!f) {
|
if (!f) {
|
||||||
log_warning("eamuse", "{} can not be opened!", path);
|
log_warning("eamuse", "{} can not be opened!", path);
|
||||||
|
overlay::notifications::add_throttled(
|
||||||
|
overlay::notifications::Severity::Error,
|
||||||
|
fmt::format("eamuse.card_file_error.p{}", index + 1),
|
||||||
|
NOTIFICATION_THROTTLE_SECONDS,
|
||||||
|
fmt::format("[P{}] can't open card file", index + 1));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +173,12 @@ bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card,
|
|||||||
// check size
|
// check size
|
||||||
if (length < 16) {
|
if (length < 16) {
|
||||||
log_warning("eamuse", "{} is too small (must be at least 16 characters)", path);
|
log_warning("eamuse", "{} is too small (must be at least 16 characters)", path);
|
||||||
|
overlay::notifications::add_throttled(
|
||||||
|
overlay::notifications::Severity::Error,
|
||||||
|
fmt::format("eamuse.card_file_error.p{}", index + 1),
|
||||||
|
NOTIFICATION_THROTTLE_SECONDS,
|
||||||
|
fmt::format("[P{}] card file error", index + 1));
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +198,11 @@ bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card,
|
|||||||
"{} contains an invalid character sequence at byte {} (16 characters, 0-9/A-F only)",
|
"{} contains an invalid character sequence at byte {} (16 characters, 0-9/A-F only)",
|
||||||
path, n);
|
path, n);
|
||||||
|
|
||||||
|
overlay::notifications::add_throttled(
|
||||||
|
overlay::notifications::Severity::Error,
|
||||||
|
fmt::format("eamuse.card_file_error.p{}", index + 1),
|
||||||
|
NOTIFICATION_THROTTLE_SECONDS,
|
||||||
|
fmt::format("[P{}] card file error", index + 1));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,8 +275,16 @@ bool eamuse_card_insert_consume(int active_count, int unit_id) {
|
|||||||
auto offset = unit_id * games::KeypadButtons::Size;
|
auto offset = unit_id * games::KeypadButtons::Size;
|
||||||
if ((CARD_INSERT[index] && fabs(get_performance_seconds() - CARD_INSERT_TIME[index]) < CARD_INSERT_TIMEOUT)
|
if ((CARD_INSERT[index] && fabs(get_performance_seconds() - CARD_INSERT_TIME[index]) < CARD_INSERT_TIMEOUT)
|
||||||
|| GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::InsertCard + offset))) {
|
|| GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::InsertCard + offset))) {
|
||||||
|
|
||||||
log_info("eamuse", "[P{}] Card insert on reader (total active count: {})", unit_id+1, active_count);
|
log_info("eamuse", "[P{}] Card insert on reader (total active count: {})", unit_id+1, active_count);
|
||||||
CARD_INSERT[index] = false;
|
CARD_INSERT[index] = false;
|
||||||
|
|
||||||
|
overlay::notifications::add_throttled(
|
||||||
|
overlay::notifications::Severity::Info,
|
||||||
|
fmt::format("eamuse.card_inserted.p{}", unit_id + 1),
|
||||||
|
NOTIFICATION_THROTTLE_SECONDS,
|
||||||
|
fmt::format("[P{}] card inserted", unit_id + 1));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,6 +335,7 @@ void eamuse_coin_start_thread() {
|
|||||||
COIN_INPUT_THREAD = new std::thread([]() {
|
COIN_INPUT_THREAD = new std::thread([]() {
|
||||||
auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game());
|
auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game());
|
||||||
static bool COIN_INPUT_KEY_STATE = false;
|
static bool COIN_INPUT_KEY_STATE = false;
|
||||||
|
timeutils::PreciseSleepTimer timer;
|
||||||
while (COIN_INPUT_THREAD_ACTIVE) {
|
while (COIN_INPUT_THREAD_ACTIVE) {
|
||||||
|
|
||||||
// check input key
|
// check input key
|
||||||
@@ -305,7 +355,7 @@ void eamuse_coin_start_thread() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// once every two frames
|
// once every two frames
|
||||||
Sleep(1000 / 30);
|
timer.sleep(1000 / 30);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -322,6 +372,25 @@ void eamuse_pin_macro_start_thread() {
|
|||||||
// set active
|
// set active
|
||||||
PIN_MACRO_THREAD_ACTIVE = true;
|
PIN_MACRO_THREAD_ACTIVE = true;
|
||||||
|
|
||||||
|
// a unit is eligible for auto-trigger only if all the static prerequisites are
|
||||||
|
// satisfied at startup; precomputing this lets the log hook skip per-line checks
|
||||||
|
// on values that never change at runtime
|
||||||
|
for (int unit = 0; unit < 2; unit++) {
|
||||||
|
AUTO_PIN_MACRO_PLAYER_ACTIVE[unit] =
|
||||||
|
!AUTO_PIN_MACRO_TRIGGER[unit].empty() &&
|
||||||
|
!PIN_MACRO_VALUES[unit].empty();
|
||||||
|
if(!AUTO_PIN_MACRO_TRIGGER[unit].empty() && PIN_MACRO_VALUES[unit].empty()) {
|
||||||
|
log_warning("eamuse", "Configuration error; Pin Macro empty for P{}.", unit+1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// register scene log hook so the macro fires on the per-game trigger string,
|
||||||
|
// but only if at least one player is eligible
|
||||||
|
if (AUTO_PIN_MACRO_PLAYER_ACTIVE[0] || AUTO_PIN_MACRO_PLAYER_ACTIVE[1]) {
|
||||||
|
logger::hook_add(pin_macro_log_hook, nullptr);
|
||||||
|
log_info("eamuse", "AUTO_PIN_MACRO enabled");
|
||||||
|
}
|
||||||
|
|
||||||
// create thread
|
// create thread
|
||||||
PIN_MACRO_THREAD = new std::thread([]() {
|
PIN_MACRO_THREAD = new std::thread([]() {
|
||||||
uint16_t keypad_overrides[] = {
|
uint16_t keypad_overrides[] = {
|
||||||
@@ -340,17 +409,27 @@ void eamuse_pin_macro_start_thread() {
|
|||||||
size_t pin_index[2] = {PIN_MACRO_VALUES[0].length(), PIN_MACRO_VALUES[1].length()};
|
size_t pin_index[2] = {PIN_MACRO_VALUES[0].length(), PIN_MACRO_VALUES[1].length()};
|
||||||
|
|
||||||
std::optional<uint8_t> active_unit = std::nullopt;
|
std::optional<uint8_t> active_unit = std::nullopt;
|
||||||
|
timeutils::PreciseSleepTimer timer;
|
||||||
|
|
||||||
while (PIN_MACRO_THREAD_ACTIVE) {
|
while (PIN_MACRO_THREAD_ACTIVE) {
|
||||||
// wait for key press
|
// wait for key press or auto-trigger
|
||||||
if (!active_unit.has_value()) {
|
if (!active_unit.has_value()) {
|
||||||
for (int unit = 0; unit < 2; unit++) {
|
for (int unit = 0; unit < 2; unit++) {
|
||||||
if (PIN_MACRO_VALUES[unit].empty()) {
|
if (PIN_MACRO_VALUES[unit].empty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (overlay_buttons &&
|
bool key_press = overlay_buttons &&
|
||||||
(!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) &&
|
(!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) &&
|
||||||
GameAPI::Buttons::getState(RI_MGR, overlay_buttons->at(PIN_MACRO_TRIGGER_KEYS[unit]))) {
|
GameAPI::Buttons::getState(RI_MGR, overlay_buttons->at(PIN_MACRO_TRIGGER_KEYS[unit]));
|
||||||
|
bool auto_request = AUTO_PIN_MACRO_REQUEST[unit].exchange(false);
|
||||||
|
if (auto_request) {
|
||||||
|
log_info("eamuse", "AUTO_PIN_MACRO_REQUEST detected for P{}", unit+1);
|
||||||
|
}
|
||||||
|
if (key_press || auto_request) {
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Info,
|
||||||
|
fmt::format("[P{}] PIN macro fired ({})",
|
||||||
|
unit + 1, auto_request ? "auto" : "manual"));
|
||||||
active_unit = unit;
|
active_unit = unit;
|
||||||
// Reset key index
|
// Reset key index
|
||||||
pin_index[unit] = 0;
|
pin_index[unit] = 0;
|
||||||
@@ -359,7 +438,7 @@ void eamuse_pin_macro_start_thread() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!active_unit.has_value()) {
|
if (!active_unit.has_value()) {
|
||||||
Sleep(20);
|
timer.sleep(20);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -375,22 +454,22 @@ void eamuse_pin_macro_start_thread() {
|
|||||||
eamuse_set_keypad_overrides(unit, keypad_overrides[char_index]);
|
eamuse_set_keypad_overrides(unit, keypad_overrides[char_index]);
|
||||||
}
|
}
|
||||||
pin_index[unit]++;
|
pin_index[unit]++;
|
||||||
Sleep(100);
|
timer.sleep(100);
|
||||||
|
|
||||||
// clear
|
// clear
|
||||||
eamuse_set_keypad_overrides(unit, 0);
|
eamuse_set_keypad_overrides(unit, 0);
|
||||||
Sleep(50);
|
timer.sleep(50);
|
||||||
|
|
||||||
// end of PIN
|
// end of PIN
|
||||||
if (pin_index[unit] == PIN_MACRO_VALUES[unit].length()) {
|
if (pin_index[unit] == PIN_MACRO_VALUES[unit].length()) {
|
||||||
active_unit = std::nullopt;
|
active_unit = std::nullopt;
|
||||||
Sleep(120);
|
timer.sleep(120);
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Sleep(200);
|
timer.sleep(200);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -402,6 +481,9 @@ void eamuse_pin_macro_stop_thread() {
|
|||||||
delete PIN_MACRO_THREAD;
|
delete PIN_MACRO_THREAD;
|
||||||
PIN_MACRO_THREAD = nullptr;
|
PIN_MACRO_THREAD = nullptr;
|
||||||
}
|
}
|
||||||
|
if (AUTO_PIN_MACRO_PLAYER_ACTIVE[0] || AUTO_PIN_MACRO_PLAYER_ACTIVE[1]) {
|
||||||
|
logger::hook_remove(pin_macro_log_hook, nullptr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void eamuse_set_keypad_overrides(size_t unit, uint16_t keypad_state) {
|
void eamuse_set_keypad_overrides(size_t unit, uint16_t keypad_state) {
|
||||||
@@ -549,7 +631,7 @@ void eamuse_update_keypad_bindings() {
|
|||||||
KEYPAD_BINDINGS = Config::getInstance().getKeypadBindings(EAMUSE_GAME_NAME);
|
KEYPAD_BINDINGS = Config::getInstance().getKeypadBindings(EAMUSE_GAME_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string eamuse_get_game() {
|
const std::string &eamuse_get_game() {
|
||||||
return EAMUSE_GAME_NAME;
|
return EAMUSE_GAME_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ extern float AUTO_INSERT_CARD_COOLDOWN;
|
|||||||
|
|
||||||
extern bool PIN_MACRO_ENABLED;
|
extern bool PIN_MACRO_ENABLED;
|
||||||
extern std::string PIN_MACRO_VALUES[2];
|
extern std::string PIN_MACRO_VALUES[2];
|
||||||
|
extern std::string AUTO_PIN_MACRO_TRIGGER[2];
|
||||||
|
|
||||||
bool eamuse_get_card(int active_count, int unit_id, uint8_t *card);
|
bool eamuse_get_card(int active_count, int unit_id, uint8_t *card);
|
||||||
bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int unit_id);
|
bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int unit_id);
|
||||||
@@ -77,7 +78,7 @@ bool eamuse_keypad_state_naive();
|
|||||||
|
|
||||||
void eamuse_set_game(std::string game);
|
void eamuse_set_game(std::string game);
|
||||||
|
|
||||||
std::string eamuse_get_game();
|
const std::string &eamuse_get_game();
|
||||||
int eamuse_get_game_keypads();
|
int eamuse_get_game_keypads();
|
||||||
int eamuse_get_game_keypads_name();
|
int eamuse_get_game_keypads_name();
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ namespace wintouchemu {
|
|||||||
bool INJECT_MOUSE_AS_WM_TOUCH = false;
|
bool INJECT_MOUSE_AS_WM_TOUCH = false;
|
||||||
bool LOG_FPS = false;
|
bool LOG_FPS = false;
|
||||||
bool ADD_TOUCH_FLAG_PRIMARY = false;
|
bool ADD_TOUCH_FLAG_PRIMARY = false;
|
||||||
|
|
||||||
|
// state
|
||||||
|
double last_touch_event = 0.0;
|
||||||
|
|
||||||
static inline bool is_emu_enabled() {
|
static inline bool is_emu_enabled() {
|
||||||
return FORCE || !is_touch_available("wintouchemu::is_emu_enabled") || GRAPHICS_SHOW_CURSOR;
|
return FORCE || !is_touch_available("wintouchemu::is_emu_enabled") || GRAPHICS_SHOW_CURSOR;
|
||||||
@@ -248,7 +251,10 @@ namespace wintouchemu {
|
|||||||
touch_input->y -= SPICETOUCH_TOUCH_Y;
|
touch_input->y -= SPICETOUCH_TOUCH_Y;
|
||||||
}
|
}
|
||||||
|
|
||||||
// log_misc("wintouchemu", "mouse state ({}, {})", to_string(touch_input->x), to_string(touch_input->y));
|
// log_misc(
|
||||||
|
// "wintouchemu",
|
||||||
|
// "mouse state ({}, {}) event={}",
|
||||||
|
// to_string(touch_input->x), to_string(touch_input->y), mouse_state.touch_event);
|
||||||
|
|
||||||
auto valid = true;
|
auto valid = true;
|
||||||
if (overlay::OVERLAY) {
|
if (overlay::OVERLAY) {
|
||||||
@@ -410,7 +416,7 @@ namespace wintouchemu {
|
|||||||
// same as iidx case above
|
// same as iidx case above
|
||||||
log_info("wintouchemu", "use mouse cursor API for popn overlay subscreen");
|
log_info("wintouchemu", "use mouse cursor API for popn overlay subscreen");
|
||||||
USE_MOUSE = true;
|
USE_MOUSE = true;
|
||||||
} else if (games::gitadora::is_arena_model() && games::gitadora::ARENA_SINGLE_WINDOW) {
|
} else if (games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
log_info("wintouchemu", "use mouse cursor API for gitadora overlay subscreen");
|
log_info("wintouchemu", "use mouse cursor API for gitadora overlay subscreen");
|
||||||
USE_MOUSE = true;
|
USE_MOUSE = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -439,6 +445,8 @@ namespace wintouchemu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto now = get_performance_milliseconds();
|
||||||
|
|
||||||
// update touch events
|
// update touch events
|
||||||
if (hWnd != nullptr) {
|
if (hWnd != nullptr) {
|
||||||
|
|
||||||
@@ -458,6 +466,7 @@ namespace wintouchemu {
|
|||||||
|
|
||||||
// check if new events are available
|
// check if new events are available
|
||||||
if (event_count > 0) {
|
if (event_count > 0) {
|
||||||
|
last_touch_event = now;
|
||||||
|
|
||||||
// send fake event to make the game update it's touch inputs
|
// send fake event to make the game update it's touch inputs
|
||||||
auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC);
|
auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC);
|
||||||
@@ -484,7 +493,12 @@ namespace wintouchemu {
|
|||||||
// value from GetTouchInputInfo or fail to read dwFlags for valid events, so it's not OK to
|
// value from GetTouchInputInfo or fail to read dwFlags for valid events, so it's not OK to
|
||||||
// send empty events when the mouse button is not clicked/released
|
// send empty events when the mouse button is not clicked/released
|
||||||
if (hWnd != nullptr && USE_MOUSE) {
|
if (hWnd != nullptr && USE_MOUSE) {
|
||||||
bool button_pressed = ((GetKeyState(VK_LBUTTON) & 0x100) != 0);
|
bool button_pressed = get_async_primary_mouse();
|
||||||
|
|
||||||
|
// if there was a touch event in the last 500 ms, don't insert new button presses
|
||||||
|
if (button_pressed && (now - last_touch_event) < 500) {
|
||||||
|
button_pressed = false;
|
||||||
|
}
|
||||||
|
|
||||||
// figure out what kind of touch event to simulate
|
// figure out what kind of touch event to simulate
|
||||||
if (button_pressed && !mouse_state.last_button_pressed) {
|
if (button_pressed && !mouse_state.last_button_pressed) {
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ static INT64 g_TicksPerSecond = 0;
|
|||||||
static ImGuiMouseCursor g_LastMouseCursor = ImGuiMouseCursor_COUNT;
|
static ImGuiMouseCursor g_LastMouseCursor = ImGuiMouseCursor_COUNT;
|
||||||
static double g_LastMouseMovement = 0.f;
|
static double g_LastMouseMovement = 0.f;
|
||||||
static bool g_MouseCursorAutoHide = false;
|
static bool g_MouseCursorAutoHide = false;
|
||||||
|
static float g_DisplaySizeOverrideW = 0.0f;
|
||||||
|
static float g_DisplaySizeOverrideH = 0.0f;
|
||||||
|
|
||||||
constexpr size_t VKEY_MAX = 255;
|
constexpr size_t VKEY_MAX = 255;
|
||||||
static std::array<bool, VKEY_MAX> g_KeysDown;
|
static std::array<bool, VKEY_MAX> g_KeysDown;
|
||||||
@@ -179,12 +181,25 @@ void ImGui_ImplSpice_Shutdown() {
|
|||||||
|
|
||||||
void ImGui_ImplSpice_UpdateDisplaySize() {
|
void ImGui_ImplSpice_UpdateDisplaySize() {
|
||||||
|
|
||||||
|
// backends that render to a surface different from the window's client
|
||||||
|
// area (e.g. dx11 swapchain backbuffer) can override the size here so
|
||||||
|
// ImGui's viewport matches the actual render target.
|
||||||
|
if (g_DisplaySizeOverrideW > 0.0f && g_DisplaySizeOverrideH > 0.0f) {
|
||||||
|
ImGui::GetIO().DisplaySize = ImVec2(g_DisplaySizeOverrideW, g_DisplaySizeOverrideH);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// get display size
|
// get display size
|
||||||
RECT rect;
|
RECT rect;
|
||||||
::GetClientRect(g_hWnd, &rect);
|
::GetClientRect(g_hWnd, &rect);
|
||||||
ImGui::GetIO().DisplaySize = ImVec2((float)(rect.right - rect.left), (float)(rect.bottom - rect.top));
|
ImGui::GetIO().DisplaySize = ImVec2((float)(rect.right - rect.left), (float)(rect.bottom - rect.top));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ImGui_ImplSpice_SetDisplaySizeOverride(float w, float h) {
|
||||||
|
g_DisplaySizeOverrideW = w;
|
||||||
|
g_DisplaySizeOverrideH = h;
|
||||||
|
}
|
||||||
|
|
||||||
bool ImGui_ImplSpice_UpdateMouseCursor() {
|
bool ImGui_ImplSpice_UpdateMouseCursor() {
|
||||||
|
|
||||||
// check if cursor should be changed
|
// check if cursor should be changed
|
||||||
@@ -351,7 +366,16 @@ void ImGui_ImplSpice_NewFrame() {
|
|||||||
|
|
||||||
// only process new input from devices if window is in focus
|
// only process new input from devices if window is in focus
|
||||||
// (when not in focus, all mouse/keyboard events are treated as released)
|
// (when not in focus, all mouse/keyboard events are treated as released)
|
||||||
const auto accept_new_input = superexit::has_focus() && !rawinput::OS_WINDOW_ACTIVE;
|
const auto overlay_visible = overlay::OVERLAY && overlay::OVERLAY->get_active();
|
||||||
|
const auto accept_new_input = superexit::has_focus() && !rawinput::OS_WINDOW_ACTIVE && overlay_visible;
|
||||||
|
|
||||||
|
// when running as standalone spicecfg.exe the configurator window proc feeds
|
||||||
|
// ImGui directly via WM_KEY*/WM_MOUSE*/WM_MOUSEWHEEL messages. The per-frame
|
||||||
|
// rawinput device walk and the 256-VK scan below are pure CPU waste in that
|
||||||
|
// case (and the dominant per-frame cost on low-end PCs), so short-circuit
|
||||||
|
// them entirely. Rebind dialogs that explicitly need fresh device state call
|
||||||
|
// RI_MGR->devices_get_updated() themselves; see overlay/windows/config.cpp.
|
||||||
|
const bool drive_input_from_rawinput = !cfg::CONFIGURATOR_STANDALONE;
|
||||||
|
|
||||||
// remember old state
|
// remember old state
|
||||||
std::array<BYTE, VKEY_MAX> KeysDownOld;
|
std::array<BYTE, VKEY_MAX> KeysDownOld;
|
||||||
@@ -362,18 +386,22 @@ void ImGui_ImplSpice_NewFrame() {
|
|||||||
const auto MouseDownOld = g_MouseDown;
|
const auto MouseDownOld = g_MouseDown;
|
||||||
|
|
||||||
// reset keys state
|
// reset keys state
|
||||||
g_MouseDown.fill(false);
|
if (drive_input_from_rawinput) {
|
||||||
g_KeysDown.fill(false);
|
g_MouseDown.fill(false);
|
||||||
|
g_KeysDown.fill(false);
|
||||||
|
}
|
||||||
|
|
||||||
// apply windows mouse buttons
|
// apply windows mouse buttons
|
||||||
g_MouseDown[ImGuiMouseButton_Left] |= (get_async_primary_mouse()) != 0;
|
if (accept_new_input && drive_input_from_rawinput) {
|
||||||
g_MouseDown[ImGuiMouseButton_Right] |= (get_async_secondary_mouse()) != 0;
|
g_MouseDown[ImGuiMouseButton_Left] |= get_async_primary_mouse();
|
||||||
g_MouseDown[ImGuiMouseButton_Middle] |= (GetAsyncKeyState(VK_MBUTTON)) != 0;
|
g_MouseDown[ImGuiMouseButton_Right] |= get_async_secondary_mouse();
|
||||||
|
g_MouseDown[ImGuiMouseButton_Middle] |= (GetAsyncKeyState(VK_MBUTTON) & 0x8000) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
// read new keys state
|
// read new keys state
|
||||||
static long mouse_wheel_last = 0;
|
static long mouse_wheel_last = 0;
|
||||||
long mouse_wheel = 0;
|
long mouse_wheel = 0;
|
||||||
if (RI_MGR != nullptr) {
|
if (drive_input_from_rawinput && RI_MGR != nullptr) {
|
||||||
auto devices = RI_MGR->devices_get();
|
auto devices = RI_MGR->devices_get();
|
||||||
for (auto &device : devices) {
|
for (auto &device : devices) {
|
||||||
switch (device.type) {
|
switch (device.type) {
|
||||||
@@ -429,41 +457,45 @@ void ImGui_ImplSpice_NewFrame() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// process keyboard input from all keyboard collapsed into one state (g_KeysDown)
|
// process keyboard input from all keyboards collapsed into one state (g_KeysDown).
|
||||||
for (size_t vKey = 0; vKey < VKEY_MAX; vKey++) {
|
// Skipped entirely in standalone configurator mode where Win32 messages already
|
||||||
const bool state = g_KeysDown[vKey];
|
// drive io.AddKeyEvent / io.AddInputCharacter directly.
|
||||||
const auto imgui_key = get_imgui_key(vKey);
|
if (drive_input_from_rawinput) {
|
||||||
const auto changed =
|
for (size_t vKey = 0; vKey < VKEY_MAX; vKey++) {
|
||||||
(state && !KeysDownOld[vKey]) ||
|
const bool state = g_KeysDown[vKey];
|
||||||
(!state && KeysDownOld[vKey]);
|
const auto imgui_key = get_imgui_key(vKey);
|
||||||
|
const auto changed =
|
||||||
|
(state && !KeysDownOld[vKey]) ||
|
||||||
|
(!state && KeysDownOld[vKey]);
|
||||||
|
|
||||||
if (imgui_key != ImGuiKey_None && changed) {
|
if (imgui_key != ImGuiKey_None && changed) {
|
||||||
io.AddKeyEvent(imgui_key, state);
|
io.AddKeyEvent(imgui_key, state);
|
||||||
log_debug("imgui_impl_spice", "vkey {:#x} added as navigation event, state: {}", static_cast<uint64_t>(vKey), state);
|
log_debug("imgui_impl_spice", "vkey {:#x} added as navigation event, state: {}", static_cast<uint64_t>(vKey), state);
|
||||||
|
|
||||||
// mod key must also be processed separately
|
// mod key must also be processed separately
|
||||||
const auto imgui_mod_key = get_imgui_mod_key(vKey);
|
const auto imgui_mod_key = get_imgui_mod_key(vKey);
|
||||||
if (imgui_mod_key != ImGuiMod_None) {
|
if (imgui_mod_key != ImGuiMod_None) {
|
||||||
io.AddKeyEvent(imgui_mod_key, state);
|
io.AddKeyEvent(imgui_mod_key, state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// generate character input, but only if WM_CHAR didn't take over the functionality
|
// generate character input, but only if WM_CHAR didn't take over the functionality
|
||||||
// only detecting rising edges here - this means holding a key won't work
|
// only detecting rising edges here - this means holding a key won't work
|
||||||
// (it's better than repeating a character input every frame - cost we pay for providing input
|
// (it's better than repeating a character input every frame - cost we pay for providing input
|
||||||
// on top of rawinput instead of WM_CHAR)
|
// on top of rawinput instead of WM_CHAR)
|
||||||
if (!overlay::USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT && !KeysDownOld[vKey] && state) {
|
if (!overlay::USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT && !KeysDownOld[vKey] && state) {
|
||||||
UCHAR buf[2];
|
UCHAR buf[2];
|
||||||
auto ret = ToAscii(
|
auto ret = ToAscii(
|
||||||
static_cast<UINT>(vKey),
|
static_cast<UINT>(vKey),
|
||||||
0,
|
0,
|
||||||
static_cast<const BYTE *>(KeysDownOld.data()),
|
static_cast<const BYTE *>(KeysDownOld.data()),
|
||||||
reinterpret_cast<LPWORD>(buf),
|
reinterpret_cast<LPWORD>(buf),
|
||||||
0);
|
0);
|
||||||
if (ret > 0) {
|
if (ret > 0) {
|
||||||
for (int i = 0; i < ret; i++) {
|
for (int i = 0; i < ret; i++) {
|
||||||
overlay::OVERLAY->input_char(buf[i]);
|
overlay::OVERLAY->input_char(buf[i]);
|
||||||
log_debug("imgui_impl_spice", "vkey {:#x} inputted as character", vKey);
|
log_debug("imgui_impl_spice", "vkey {:#x} inputted as character", vKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -472,23 +504,26 @@ void ImGui_ImplSpice_NewFrame() {
|
|||||||
// set mouse wheel
|
// set mouse wheel
|
||||||
long wheel_diff = mouse_wheel - mouse_wheel_last;
|
long wheel_diff = mouse_wheel - mouse_wheel_last;
|
||||||
mouse_wheel_last = mouse_wheel;
|
mouse_wheel_last = mouse_wheel;
|
||||||
if (wheel_diff != 0 && accept_new_input) {
|
if (wheel_diff != 0 && accept_new_input && drive_input_from_rawinput) {
|
||||||
io.AddMouseWheelEvent(0, wheel_diff);
|
io.AddMouseWheelEvent(0, wheel_diff);
|
||||||
}
|
}
|
||||||
|
|
||||||
// update OS mouse position
|
// update OS mouse position. The standalone configurator gets mouse position
|
||||||
|
// straight from WM_MOUSEMOVE, so skip the per-frame cursor poll there.
|
||||||
const auto old_mouse_pos = io.MousePos;
|
const auto old_mouse_pos = io.MousePos;
|
||||||
if (accept_new_input) {
|
if (accept_new_input && drive_input_from_rawinput) {
|
||||||
ImGui_ImplSpice_UpdateMousePos();
|
ImGui_ImplSpice_UpdateMousePos();
|
||||||
}
|
}
|
||||||
const auto new_mouse_pos = io.MousePos;
|
const auto new_mouse_pos = io.MousePos;
|
||||||
|
|
||||||
// update mouse buttons
|
// update mouse buttons
|
||||||
// doing this after ImGui_ImplSpice_UpdateMousePos since it can set g_MouseDown for touch input
|
// doing this after ImGui_ImplSpice_UpdateMousePos since it can set g_MouseDown for touch input
|
||||||
for (size_t i = 0; i < g_MouseDown.size(); i++) {
|
if (drive_input_from_rawinput) {
|
||||||
if (MouseDownOld[i] != g_MouseDown[i]) {
|
for (size_t i = 0; i < g_MouseDown.size(); i++) {
|
||||||
io.AddMouseButtonEvent(i, g_MouseDown[i]);
|
if (MouseDownOld[i] != g_MouseDown[i]) {
|
||||||
log_debug("imgui_impl_spice", "mouse button {} event", g_MouseDown[i]);
|
io.AddMouseButtonEvent(i, g_MouseDown[i]);
|
||||||
|
log_debug("imgui_impl_spice", "mouse button {} event", g_MouseDown[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,5 +6,6 @@
|
|||||||
IMGUI_IMPL_API bool ImGui_ImplSpice_Init(HWND hWnd);
|
IMGUI_IMPL_API bool ImGui_ImplSpice_Init(HWND hWnd);
|
||||||
IMGUI_IMPL_API void ImGui_ImplSpice_Shutdown();
|
IMGUI_IMPL_API void ImGui_ImplSpice_Shutdown();
|
||||||
IMGUI_IMPL_API void ImGui_ImplSpice_UpdateDisplaySize();
|
IMGUI_IMPL_API void ImGui_ImplSpice_UpdateDisplaySize();
|
||||||
|
IMGUI_IMPL_API void ImGui_ImplSpice_SetDisplaySizeOverride(float w, float h);
|
||||||
IMGUI_IMPL_API bool ImGui_ImplSpice_UpdateMouseCursor();
|
IMGUI_IMPL_API bool ImGui_ImplSpice_UpdateMouseCursor();
|
||||||
IMGUI_IMPL_API void ImGui_ImplSpice_NewFrame();
|
IMGUI_IMPL_API void ImGui_ImplSpice_NewFrame();
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
#include "notifications.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <deque>
|
||||||
|
#include <mutex>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "external/imgui/imgui.h"
|
||||||
|
#include "external/imgui/imgui_internal.h"
|
||||||
|
#include "external/fmt/include/fmt/format.h"
|
||||||
|
|
||||||
|
#include "overlay/overlay.h"
|
||||||
|
#include "util/time.h"
|
||||||
|
|
||||||
|
namespace overlay::notifications {
|
||||||
|
|
||||||
|
bool ENABLED = true;
|
||||||
|
Position POSITION = Position::BottomRight;
|
||||||
|
|
||||||
|
struct Notification {
|
||||||
|
uint64_t id;
|
||||||
|
std::string text;
|
||||||
|
Severity severity;
|
||||||
|
double created_ms;
|
||||||
|
float duration_s;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::mutex g_mutex;
|
||||||
|
static std::deque<Notification> g_items;
|
||||||
|
static std::atomic<uint64_t> g_next_id { 1 };
|
||||||
|
static std::atomic<size_t> g_count { 0 };
|
||||||
|
|
||||||
|
// duration in seconds each notification stays visible
|
||||||
|
static constexpr float DURATION_S = 3.0f;
|
||||||
|
|
||||||
|
// maximum number of notifications kept in the queue (oldest dropped beyond this)
|
||||||
|
static constexpr size_t MAX_NOTIFICATIONS = 6;
|
||||||
|
|
||||||
|
// time (ms) over which a toast fades out at the end of its lifetime
|
||||||
|
static constexpr float FADE_OUT_MS = 400.0f;
|
||||||
|
|
||||||
|
// fixed width of each toast window, in unscaled pixels
|
||||||
|
static constexpr float TOAST_WIDTH = 320.0f;
|
||||||
|
|
||||||
|
// gap between the toast stack and the screen edges (right + bottom)
|
||||||
|
static constexpr float TOAST_MARGIN = 20.0f;
|
||||||
|
|
||||||
|
// vertical gap between stacked toasts
|
||||||
|
static constexpr float TOAST_SPACING = 8.0f;
|
||||||
|
|
||||||
|
// inner padding inside a toast window (horizontal / vertical)
|
||||||
|
static constexpr float TOAST_PAD_X = 10.0f;
|
||||||
|
static constexpr float TOAST_PAD_Y = 8.0f;
|
||||||
|
|
||||||
|
// width of the colored severity accent bar drawn on the left edge
|
||||||
|
static constexpr float TOAST_ACCENT_W = 6.0f;
|
||||||
|
|
||||||
|
// base opacity of the toast background (0..1), multiplied by the fade alpha
|
||||||
|
static constexpr float TOAST_BG_ALPHA = 0.85f;
|
||||||
|
|
||||||
|
static constexpr ImGuiWindowFlags TOAST_FLAGS =
|
||||||
|
ImGuiWindowFlags_NoDecoration
|
||||||
|
| ImGuiWindowFlags_NoInputs
|
||||||
|
| ImGuiWindowFlags_NoNav
|
||||||
|
| ImGuiWindowFlags_NoMove
|
||||||
|
| ImGuiWindowFlags_NoSavedSettings
|
||||||
|
| ImGuiWindowFlags_NoFocusOnAppearing
|
||||||
|
| ImGuiWindowFlags_NoBringToFrontOnFocus
|
||||||
|
| ImGuiWindowFlags_AlwaysAutoResize;
|
||||||
|
|
||||||
|
static ImU32 severity_accent(Severity sev) {
|
||||||
|
switch (sev) {
|
||||||
|
case Severity::Success: return IM_COL32(80, 200, 120, 255);
|
||||||
|
case Severity::Warning: return IM_COL32(230, 180, 60, 255);
|
||||||
|
case Severity::Error: return IM_COL32(220, 60, 60, 255);
|
||||||
|
case Severity::Info:
|
||||||
|
default: return IM_COL32(90, 160, 230, 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool is_expired(const Notification &n, double now_ms) {
|
||||||
|
return (now_ms - n.created_ms) >= (n.duration_s * 1000.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns 0.0 .. 1.0 fade alpha based on time remaining
|
||||||
|
static float compute_alpha(const Notification &n, double now_ms) {
|
||||||
|
const double remaining_ms = (n.duration_s * 1000.0) - (now_ms - n.created_ms);
|
||||||
|
if (remaining_ms >= FADE_OUT_MS) {
|
||||||
|
return 1.0f;
|
||||||
|
}
|
||||||
|
if (remaining_ms <= 0.0) {
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
return static_cast<float>(remaining_ms / FADE_OUT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// drop expired items and copy the rest under a single lock acquisition
|
||||||
|
static std::vector<Notification> snapshot_and_prune(double now_ms) {
|
||||||
|
std::vector<Notification> snapshot;
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
for (auto it = g_items.begin(); it != g_items.end();) {
|
||||||
|
if (is_expired(*it, now_ms)) {
|
||||||
|
it = g_items.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_count.store(g_items.size(), std::memory_order_release);
|
||||||
|
snapshot.assign(g_items.begin(), g_items.end());
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
// is the configured anchor on the right edge of the screen?
|
||||||
|
static bool position_is_right(Position p) {
|
||||||
|
return p == Position::BottomRight || p == Position::TopRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// is the configured anchor on the bottom edge of the screen?
|
||||||
|
static bool position_is_bottom(Position p) {
|
||||||
|
return p == Position::BottomRight || p == Position::BottomLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw a single toast anchored to the configured corner; `cursor_y` is the
|
||||||
|
// y-coordinate of the toast edge nearest the anchor (top edge for Top* anchors,
|
||||||
|
// bottom edge for Bottom* anchors). returns its height in pixels.
|
||||||
|
static float draw_toast(const Notification &n, float cursor_y, float alpha) {
|
||||||
|
const float toast_width = apply_scaling(TOAST_WIDTH);
|
||||||
|
const float margin = apply_scaling(TOAST_MARGIN);
|
||||||
|
const ImVec2 &display = ImGui::GetIO().DisplaySize;
|
||||||
|
const Position pos = POSITION;
|
||||||
|
|
||||||
|
const auto window_id = fmt::format("##spice_notif_{}", n.id);
|
||||||
|
|
||||||
|
// anchor x/pivot.x select the screen edge; pivot.y matches cursor_y semantics
|
||||||
|
const float anchor_x = position_is_right(pos) ? (display.x - margin) : margin;
|
||||||
|
const float pivot_x = position_is_right(pos) ? 1.0f : 0.0f;
|
||||||
|
const float pivot_y = position_is_bottom(pos) ? 1.0f : 0.0f;
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(anchor_x, cursor_y),
|
||||||
|
ImGuiCond_Always, ImVec2(pivot_x, pivot_y));
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(toast_width, 0.f), ImGuiCond_Always);
|
||||||
|
ImGui::SetNextWindowBgAlpha(TOAST_BG_ALPHA * alpha);
|
||||||
|
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,
|
||||||
|
ImVec2(apply_scaling(TOAST_PAD_X), apply_scaling(TOAST_PAD_Y)));
|
||||||
|
|
||||||
|
float height = 0.f;
|
||||||
|
if (ImGui::Begin(window_id.c_str(), nullptr, TOAST_FLAGS)) {
|
||||||
|
const ImVec2 win_pos = ImGui::GetWindowPos();
|
||||||
|
const ImVec2 win_size = ImGui::GetWindowSize();
|
||||||
|
|
||||||
|
// accent bar on the left edge of the window
|
||||||
|
const ImU32 accent = severity_accent(n.severity);
|
||||||
|
const ImU32 accent_faded =
|
||||||
|
(accent & 0x00FFFFFFu) | (static_cast<ImU32>(alpha * 255.0f) << 24);
|
||||||
|
ImGui::GetWindowDrawList()->AddRectFilled(
|
||||||
|
win_pos,
|
||||||
|
ImVec2(win_pos.x + apply_scaling(TOAST_ACCENT_W), win_pos.y + win_size.y),
|
||||||
|
accent_faded);
|
||||||
|
|
||||||
|
// small gutter past the accent bar, then wrapped text
|
||||||
|
ImGui::Dummy(ImVec2(apply_scaling(2.0f), 0.f));
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::PushTextWrapPos(win_pos.x + win_size.x - apply_scaling(TOAST_PAD_X));
|
||||||
|
ImGui::TextUnformatted(n.text.c_str());
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
|
||||||
|
height = ImGui::GetWindowSize().y;
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
|
||||||
|
ImGui::PopStyleVar(2);
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t add(Severity severity, std::string text) {
|
||||||
|
if (!ENABLED || !overlay::ENABLED || overlay::OVERLAY == nullptr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Notification n {
|
||||||
|
.id = g_next_id.fetch_add(1, std::memory_order_relaxed),
|
||||||
|
.text = std::move(text),
|
||||||
|
.severity = severity,
|
||||||
|
.created_ms = get_performance_milliseconds(),
|
||||||
|
.duration_s = DURATION_S,
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
g_items.push_back(std::move(n));
|
||||||
|
while (g_items.size() > MAX_NOTIFICATIONS) {
|
||||||
|
g_items.pop_front();
|
||||||
|
}
|
||||||
|
g_count.store(g_items.size(), std::memory_order_release);
|
||||||
|
}
|
||||||
|
return n.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t add_throttled(Severity severity, const std::string &key,
|
||||||
|
double cooldown_seconds, std::string text) {
|
||||||
|
if (!ENABLED || !overlay::ENABLED || overlay::OVERLAY == nullptr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// per-key last-emit timestamps live behind their own lock so we don't
|
||||||
|
// hold g_mutex across the map lookup.
|
||||||
|
static std::mutex throttle_mutex;
|
||||||
|
static std::unordered_map<std::string, double> last_emit_ms;
|
||||||
|
const double now_ms = get_performance_milliseconds();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(throttle_mutex);
|
||||||
|
auto it = last_emit_ms.find(key);
|
||||||
|
if (it != last_emit_ms.end()
|
||||||
|
&& (now_ms - it->second) < (cooldown_seconds * 1000.0)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
last_emit_ms[key] = now_ms;
|
||||||
|
}
|
||||||
|
return add(severity, std::move(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_pending() {
|
||||||
|
return g_count.load(std::memory_order_acquire) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw() {
|
||||||
|
const double now_ms = get_performance_milliseconds();
|
||||||
|
const auto snapshot = snapshot_and_prune(now_ms);
|
||||||
|
if (snapshot.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// stack from the anchored edge with newest toast at the anchor.
|
||||||
|
// Bottom* anchors stack upward; Top* anchors stack downward.
|
||||||
|
const float spacing = apply_scaling(TOAST_SPACING);
|
||||||
|
const float margin = apply_scaling(TOAST_MARGIN);
|
||||||
|
const bool bottom = position_is_bottom(POSITION);
|
||||||
|
float cursor_y = bottom
|
||||||
|
? (ImGui::GetIO().DisplaySize.y - margin)
|
||||||
|
: margin;
|
||||||
|
for (auto it = snapshot.rbegin(); it != snapshot.rend(); ++it) {
|
||||||
|
const float alpha = compute_alpha(*it, now_ms);
|
||||||
|
const float height = draw_toast(*it, cursor_y, alpha);
|
||||||
|
cursor_y += bottom ? -(height + spacing) : (height + spacing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_game_default_position(const std::string &game_name) {
|
||||||
|
if (game_name == "Reflec Beat") {
|
||||||
|
POSITION = Position::TopRight;
|
||||||
|
}
|
||||||
|
// others keep the default (BottomRight)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace overlay::notifications {
|
||||||
|
|
||||||
|
// master switch for the notification system; when false, add() is a no-op.
|
||||||
|
// controlled by selecting "none" for the -notifypos launcher option.
|
||||||
|
extern bool ENABLED;
|
||||||
|
|
||||||
|
enum class Severity {
|
||||||
|
Info,
|
||||||
|
Success,
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
// screen anchor for the toast stack. toasts stack away from the anchored edge.
|
||||||
|
enum class Position {
|
||||||
|
BottomRight,
|
||||||
|
BottomLeft,
|
||||||
|
TopRight,
|
||||||
|
TopLeft,
|
||||||
|
};
|
||||||
|
|
||||||
|
// current toast anchor. defaults to BottomRight; may be reassigned by
|
||||||
|
// apply_game_default_position() or by the user via -notifypos.
|
||||||
|
extern Position POSITION;
|
||||||
|
|
||||||
|
// apply the default toast position appropriate for a game (by display name,
|
||||||
|
// as returned by eamuse_get_game()). called once after game autodetect, before
|
||||||
|
// any user -notifypos override is applied.
|
||||||
|
void apply_game_default_position(const std::string &game_name);
|
||||||
|
|
||||||
|
// add a notification (thread-safe). returns the assigned id, or 0 if the
|
||||||
|
// notification was dropped (overlay disabled or notifications disabled).
|
||||||
|
uint64_t add(Severity severity, std::string text);
|
||||||
|
|
||||||
|
// rate-limited variant of add(). suppresses the toast if another call with
|
||||||
|
// the same `key` succeeded within the last `cooldown_seconds`. useful for
|
||||||
|
// events that can fire every frame (e.g. a button held down). returns the
|
||||||
|
// assigned id, or 0 if the toast was suppressed or dropped. thread-safe.
|
||||||
|
uint64_t add_throttled(Severity severity, const std::string &key,
|
||||||
|
double cooldown_seconds, std::string text);
|
||||||
|
|
||||||
|
// true if there is at least one notification that still needs to be drawn.
|
||||||
|
// safe to call from the render thread without locking the underlying store.
|
||||||
|
bool has_pending();
|
||||||
|
|
||||||
|
// draw all active notifications and prune expired ones.
|
||||||
|
// must be called from the ImGui render thread inside a NewFrame/EndFrame pair.
|
||||||
|
void draw();
|
||||||
|
}
|
||||||
+258
-16
@@ -15,8 +15,18 @@
|
|||||||
#include "build/resource.h"
|
#include "build/resource.h"
|
||||||
|
|
||||||
#include "external/imgui/backends/imgui_impl_dx9.h"
|
#include "external/imgui/backends/imgui_impl_dx9.h"
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
#include "external/imgui/backends/imgui_impl_dx11.h"
|
||||||
|
#include "hooks/graphics/backends/d3d11/d3d11_backend.h"
|
||||||
|
#endif
|
||||||
#include "overlay/imgui/impl_spice.h"
|
#include "overlay/imgui/impl_spice.h"
|
||||||
#include "overlay/imgui/impl_sw.h"
|
#include "overlay/imgui/impl_sw.h"
|
||||||
|
#include "overlay/notifications.h"
|
||||||
|
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
#include <d3d11.h>
|
||||||
|
#include <dxgi.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "window.h"
|
#include "window.h"
|
||||||
#ifdef SPICE64
|
#ifdef SPICE64
|
||||||
@@ -104,6 +114,21 @@ void overlay::create_d3d9(HWND hWnd, IDirect3D9 *d3d, IDirect3DDevice9 *device)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
void overlay::create_d3d11(HWND hWnd, ID3D11Device *device, ID3D11DeviceContext *context,
|
||||||
|
IDXGISwapChain *swapchain) {
|
||||||
|
if (!overlay::ENABLED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::lock_guard<std::mutex> lock(OVERLAY_MUTEX);
|
||||||
|
|
||||||
|
if (!overlay::OVERLAY) {
|
||||||
|
overlay::OVERLAY = std::make_unique<overlay::SpiceOverlay>(hWnd, device, context, swapchain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void overlay::create_software(HWND hWnd) {
|
void overlay::create_software(HWND hWnd) {
|
||||||
if (!overlay::ENABLED) {
|
if (!overlay::ENABLED) {
|
||||||
return;
|
return;
|
||||||
@@ -164,6 +189,27 @@ overlay::SpiceOverlay::SpiceOverlay(HWND hWnd)
|
|||||||
this->init();
|
this->init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
overlay::SpiceOverlay::SpiceOverlay(HWND hWnd, ID3D11Device *d3d11_device,
|
||||||
|
ID3D11DeviceContext *d3d11_context,
|
||||||
|
IDXGISwapChain *d3d11_swapchain)
|
||||||
|
: renderer(OverlayRenderer::D3D11),
|
||||||
|
hWnd(hWnd),
|
||||||
|
d3d11_device(d3d11_device),
|
||||||
|
d3d11_context(d3d11_context),
|
||||||
|
d3d11_swapchain(d3d11_swapchain) {
|
||||||
|
log_info("overlay", "initializing (D3D11)");
|
||||||
|
|
||||||
|
// increment reference counts
|
||||||
|
this->d3d11_device->AddRef();
|
||||||
|
this->d3d11_context->AddRef();
|
||||||
|
this->d3d11_swapchain->AddRef();
|
||||||
|
|
||||||
|
// init
|
||||||
|
this->init();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void overlay::SpiceOverlay::init() {
|
void overlay::SpiceOverlay::init() {
|
||||||
|
|
||||||
// init imgui
|
// init imgui
|
||||||
@@ -281,8 +327,9 @@ void overlay::SpiceOverlay::init() {
|
|||||||
// disable config
|
// disable config
|
||||||
io.IniFilename = nullptr;
|
io.IniFilename = nullptr;
|
||||||
|
|
||||||
// allow CTRL+WHEEL scaling
|
// allow CTRL+WHEEL scaling in the in-game overlay; disable for the standalone
|
||||||
io.FontAllowUserScaling = true;
|
// configurator so the mouse wheel always scrolls the configuration window.
|
||||||
|
io.FontAllowUserScaling = !cfg::CONFIGURATOR_STANDALONE;
|
||||||
|
|
||||||
// add default font
|
// add default font
|
||||||
io.Fonts->AddFontDefaultBitmap();
|
io.Fonts->AddFontDefaultBitmap();
|
||||||
@@ -316,6 +363,11 @@ void overlay::SpiceOverlay::init() {
|
|||||||
case OverlayRenderer::D3D9:
|
case OverlayRenderer::D3D9:
|
||||||
ImGui_ImplDX9_Init(this->device);
|
ImGui_ImplDX9_Init(this->device);
|
||||||
break;
|
break;
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
case OverlayRenderer::D3D11:
|
||||||
|
ImGui_ImplDX11_Init(this->d3d11_device, this->d3d11_context);
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
case OverlayRenderer::SOFTWARE:
|
case OverlayRenderer::SOFTWARE:
|
||||||
imgui_sw::bind_imgui_painting();
|
imgui_sw::bind_imgui_painting();
|
||||||
break;
|
break;
|
||||||
@@ -326,6 +378,11 @@ void overlay::SpiceOverlay::init() {
|
|||||||
case OverlayRenderer::D3D9:
|
case OverlayRenderer::D3D9:
|
||||||
ImGui_ImplDX9_NewFrame();
|
ImGui_ImplDX9_NewFrame();
|
||||||
break;
|
break;
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
case OverlayRenderer::D3D11:
|
||||||
|
ImGui_ImplDX11_NewFrame();
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
case OverlayRenderer::SOFTWARE:
|
case OverlayRenderer::SOFTWARE:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -415,7 +472,7 @@ void overlay::SpiceOverlay::init() {
|
|||||||
window_sub = new overlay::windows::DRSDanceFloorDisplay(this);
|
window_sub = new overlay::windows::DRSDanceFloorDisplay(this);
|
||||||
} else if (avs::game::is_model("KFC")) {
|
} else if (avs::game::is_model("KFC")) {
|
||||||
window_sub = new overlay::windows::SDVXSubScreen(this);
|
window_sub = new overlay::windows::SDVXSubScreen(this);
|
||||||
} else if (games::gitadora::is_arena_model() && games::gitadora::ARENA_SINGLE_WINDOW) {
|
} else if (games::gitadora::is_arena_model()) {
|
||||||
window_sub = new overlay::windows::GitaDoraSubScreen(this);
|
window_sub = new overlay::windows::GitaDoraSubScreen(this);
|
||||||
} else if (games::popn::is_pikapika_model()) {
|
} else if (games::popn::is_pikapika_model()) {
|
||||||
window_sub = new overlay::windows::PopnSubScreen(this);
|
window_sub = new overlay::windows::PopnSubScreen(this);
|
||||||
@@ -448,6 +505,21 @@ overlay::SpiceOverlay::~SpiceOverlay() {
|
|||||||
this->d3d->Release();
|
this->d3d->Release();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
case OverlayRenderer::D3D11:
|
||||||
|
if (this->d3d11_rtv) {
|
||||||
|
this->d3d11_rtv->Release();
|
||||||
|
this->d3d11_rtv = nullptr;
|
||||||
|
}
|
||||||
|
ImGui_ImplDX11_Shutdown();
|
||||||
|
|
||||||
|
// drop references
|
||||||
|
this->d3d11_swapchain->Release();
|
||||||
|
this->d3d11_context->Release();
|
||||||
|
this->d3d11_device->Release();
|
||||||
|
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
case OverlayRenderer::SOFTWARE:
|
case OverlayRenderer::SOFTWARE:
|
||||||
imgui_sw::unbind_imgui_painting();
|
imgui_sw::unbind_imgui_painting();
|
||||||
break;
|
break;
|
||||||
@@ -465,8 +537,16 @@ void overlay::SpiceOverlay::new_frame() {
|
|||||||
ImGui_ImplSpice_NewFrame();
|
ImGui_ImplSpice_NewFrame();
|
||||||
this->total_elapsed += ImGui::GetIO().DeltaTime;
|
this->total_elapsed += ImGui::GetIO().DeltaTime;
|
||||||
|
|
||||||
// check if inactive
|
// notifications draw on top of the game without flipping `active`, so the input gates
|
||||||
if (!this->active) {
|
// (see touch/touch.cpp WndProc, touch_get_points/events, graphics.cpp WM_CHAR) stay
|
||||||
|
// disabled and input continues to flow to the game.
|
||||||
|
// SOFTWARE renderer (spicecfg / configurator) never gets notifications - no room.
|
||||||
|
const bool draw_notifications = this->renderer != OverlayRenderer::SOFTWARE
|
||||||
|
&& overlay::notifications::has_pending();
|
||||||
|
|
||||||
|
// check if there is nothing to draw
|
||||||
|
this->has_pending_frame = false;
|
||||||
|
if (!this->active && !draw_notifications) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,29 +555,97 @@ void overlay::SpiceOverlay::new_frame() {
|
|||||||
case OverlayRenderer::D3D9:
|
case OverlayRenderer::D3D9:
|
||||||
ImGui_ImplDX9_NewFrame();
|
ImGui_ImplDX9_NewFrame();
|
||||||
break;
|
break;
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
case OverlayRenderer::D3D11:
|
||||||
|
// refresh DisplaySize each frame; the dx11 swapchain hook pushes
|
||||||
|
// the current backbuffer dimensions into impl_spice via the
|
||||||
|
// size override so ImGui's viewport tracks ResizeBuffers calls.
|
||||||
|
ImGui_ImplSpice_UpdateDisplaySize();
|
||||||
|
ImGui_ImplDX11_NewFrame();
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
case OverlayRenderer::SOFTWARE:
|
case OverlayRenderer::SOFTWARE:
|
||||||
ImGui_ImplSpice_UpdateDisplaySize();
|
ImGui_ImplSpice_UpdateDisplaySize();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
ImGui::NewFrame();
|
ImGui::NewFrame();
|
||||||
|
this->has_pending_frame = true;
|
||||||
|
|
||||||
// build windows
|
// build windows only when the overlay itself is active
|
||||||
for (auto &window : this->windows) {
|
if (this->active) {
|
||||||
window->build();
|
for (auto &window : this->windows) {
|
||||||
|
window->build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SHOW_DEBUG_LOG_WINDOW) {
|
||||||
|
ImGui::ShowDebugLogWindow(&SHOW_DEBUG_LOG_WINDOW);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SHOW_DEBUG_LOG_WINDOW) {
|
// draw notifications last so they paint on top of any overlay windows
|
||||||
ImGui::ShowDebugLogWindow(&SHOW_DEBUG_LOG_WINDOW);
|
if (draw_notifications) {
|
||||||
|
overlay::notifications::draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
// end frame
|
// end frame
|
||||||
ImGui::EndFrame();
|
ImGui::EndFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FNV-1a 64-bit hash helper used by the software renderer to detect idle frames.
|
||||||
|
static inline uint64_t overlay_fnv1a64(uint64_t h, const void *data, size_t len) {
|
||||||
|
const auto *p = static_cast<const uint8_t *>(data);
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
h ^= p[i];
|
||||||
|
h *= 0x100000001B3ULL;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute a hash over the visual content of ImDrawData. Only the geometry the
|
||||||
|
// software rasterizer actually consumes (vertex/index buffers + display size)
|
||||||
|
// feeds into the hash, so frames where ImGui produces identical draw data
|
||||||
|
// (i.e. nothing animated this tick) can skip the full pixel rasterization.
|
||||||
|
static uint64_t overlay_hash_draw_data(const ImDrawData *draw_data) {
|
||||||
|
uint64_t h = 0xcbf29ce484222325ULL;
|
||||||
|
if (draw_data == nullptr) {
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
const float display[4] = {
|
||||||
|
draw_data->DisplayPos.x,
|
||||||
|
draw_data->DisplayPos.y,
|
||||||
|
draw_data->DisplaySize.x,
|
||||||
|
draw_data->DisplaySize.y,
|
||||||
|
};
|
||||||
|
h = overlay_fnv1a64(h, display, sizeof(display));
|
||||||
|
|
||||||
|
const int cmd_lists = draw_data->CmdListsCount;
|
||||||
|
h = overlay_fnv1a64(h, &cmd_lists, sizeof(cmd_lists));
|
||||||
|
|
||||||
|
for (int i = 0; i < draw_data->CmdListsCount; i++) {
|
||||||
|
const ImDrawList *cmd_list = draw_data->CmdLists[i];
|
||||||
|
if (cmd_list == nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const int vtx_count = cmd_list->VtxBuffer.Size;
|
||||||
|
const int idx_count = cmd_list->IdxBuffer.Size;
|
||||||
|
h = overlay_fnv1a64(h, &vtx_count, sizeof(vtx_count));
|
||||||
|
h = overlay_fnv1a64(h, &idx_count, sizeof(idx_count));
|
||||||
|
if (vtx_count > 0) {
|
||||||
|
h = overlay_fnv1a64(h, cmd_list->VtxBuffer.Data,
|
||||||
|
static_cast<size_t>(vtx_count) * sizeof(ImDrawVert));
|
||||||
|
}
|
||||||
|
if (idx_count > 0) {
|
||||||
|
h = overlay_fnv1a64(h, cmd_list->IdxBuffer.Data,
|
||||||
|
static_cast<size_t>(idx_count) * sizeof(ImDrawIdx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
void overlay::SpiceOverlay::render() {
|
void overlay::SpiceOverlay::render() {
|
||||||
|
|
||||||
// check if inactive
|
// skip if new_frame() didn't begin a frame this tick
|
||||||
if (!this->active) {
|
if (!this->has_pending_frame) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,8 +655,35 @@ void overlay::SpiceOverlay::render() {
|
|||||||
// implementation render
|
// implementation render
|
||||||
switch (this->renderer) {
|
switch (this->renderer) {
|
||||||
case OverlayRenderer::D3D9:
|
case OverlayRenderer::D3D9:
|
||||||
ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
||||||
|
const auto *draw_data = ImGui::GetDrawData();
|
||||||
|
const uint64_t draw_hash = overlay_hash_draw_data(draw_data);
|
||||||
|
const auto &io = ImGui::GetIO();
|
||||||
|
const int display_w = static_cast<int>(std::ceil(io.DisplaySize.x));
|
||||||
|
const int display_h = static_cast<int>(std::ceil(io.DisplaySize.y));
|
||||||
|
const bool size_matches = (this->d3d9_last_display_w == display_w
|
||||||
|
&& this->d3d9_last_display_h == display_h);
|
||||||
|
if (this->d3d9_has_last_draw_hash
|
||||||
|
&& draw_hash == this->d3d9_last_draw_hash
|
||||||
|
&& size_matches) {
|
||||||
|
this->d3d9_frame_dirty = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this->d3d9_last_draw_hash = draw_hash;
|
||||||
|
this->d3d9_has_last_draw_hash = true;
|
||||||
|
this->d3d9_last_display_w = display_w;
|
||||||
|
this->d3d9_last_display_h = display_h;
|
||||||
|
this->d3d9_frame_dirty = true;
|
||||||
|
} else {
|
||||||
|
ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
case OverlayRenderer::D3D11:
|
||||||
|
overlay::d3d11::render(this->d3d11_device, this->d3d11_context,
|
||||||
|
this->d3d11_swapchain, &this->d3d11_rtv);
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
case OverlayRenderer::SOFTWARE: {
|
case OverlayRenderer::SOFTWARE: {
|
||||||
|
|
||||||
// get display metrics
|
// get display metrics
|
||||||
@@ -517,6 +692,23 @@ void overlay::SpiceOverlay::render() {
|
|||||||
auto height = static_cast<size_t>(std::ceil(io.DisplaySize.y));
|
auto height = static_cast<size_t>(std::ceil(io.DisplaySize.y));
|
||||||
auto pixels = width * height;
|
auto pixels = width * height;
|
||||||
|
|
||||||
|
// skip the (expensive) full software rasterization when the draw data
|
||||||
|
// is byte-identical to the previous frame and the existing pixel
|
||||||
|
// buffer still matches the current display size.
|
||||||
|
const auto *draw_data = ImGui::GetDrawData();
|
||||||
|
const uint64_t draw_hash = overlay_hash_draw_data(draw_data);
|
||||||
|
const bool size_matches = (this->pixel_data_width == width
|
||||||
|
&& this->pixel_data_height == height
|
||||||
|
&& this->pixel_data.size() >= pixels);
|
||||||
|
if (this->sw_has_last_draw_hash
|
||||||
|
&& draw_hash == this->sw_last_draw_hash
|
||||||
|
&& size_matches) {
|
||||||
|
this->sw_pixels_dirty = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this->sw_last_draw_hash = draw_hash;
|
||||||
|
this->sw_has_last_draw_hash = true;
|
||||||
|
|
||||||
// make sure buffer is big enough
|
// make sure buffer is big enough
|
||||||
if (this->pixel_data.size() < pixels) {
|
if (this->pixel_data.size() < pixels) {
|
||||||
this->pixel_data.resize(pixels, 0);
|
this->pixel_data.resize(pixels, 0);
|
||||||
@@ -533,6 +725,7 @@ void overlay::SpiceOverlay::render() {
|
|||||||
imgui_sw::paint_imgui(&this->pixel_data[0], width, height, options);
|
imgui_sw::paint_imgui(&this->pixel_data[0], width, height, options);
|
||||||
pixel_data_width = width;
|
pixel_data_width = width;
|
||||||
pixel_data_height = height;
|
pixel_data_height = height;
|
||||||
|
this->sw_pixels_dirty = true;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -541,6 +734,18 @@ void overlay::SpiceOverlay::render() {
|
|||||||
for (auto &window : this->windows) {
|
for (auto &window : this->windows) {
|
||||||
window->after_render();
|
window->after_render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this->has_pending_frame = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void overlay::SpiceOverlay::d3d9_render_draw(const bool force_submit) {
|
||||||
|
if (this->renderer != OverlayRenderer::D3D9) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!force_submit && !this->d3d9_frame_dirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
|
||||||
}
|
}
|
||||||
|
|
||||||
void overlay::SpiceOverlay::update() {
|
void overlay::SpiceOverlay::update() {
|
||||||
@@ -686,11 +891,48 @@ bool overlay::SpiceOverlay::hotkeys_triggered() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void overlay::SpiceOverlay::reset_invalidate() {
|
void overlay::SpiceOverlay::reset_invalidate() {
|
||||||
ImGui_ImplDX9_InvalidateDeviceObjects();
|
if (!overlay::OVERLAY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (overlay::OVERLAY->renderer) {
|
||||||
|
case OverlayRenderer::D3D9:
|
||||||
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
||||||
|
overlay::OVERLAY->d3d9_has_last_draw_hash = false;
|
||||||
|
overlay::OVERLAY->d3d9_frame_dirty = true;
|
||||||
|
}
|
||||||
|
ImGui_ImplDX9_InvalidateDeviceObjects();
|
||||||
|
break;
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
case OverlayRenderer::D3D11:
|
||||||
|
// for DX11 a ResizeBuffers only invalidates the backbuffer RTV; the imgui
|
||||||
|
// device objects (shaders, buffers, textures) remain valid.
|
||||||
|
if (overlay::OVERLAY->d3d11_rtv) {
|
||||||
|
overlay::OVERLAY->d3d11_rtv->Release();
|
||||||
|
overlay::OVERLAY->d3d11_rtv = nullptr;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
case OverlayRenderer::SOFTWARE:
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void overlay::SpiceOverlay::reset_recreate() {
|
void overlay::SpiceOverlay::reset_recreate() {
|
||||||
ImGui_ImplDX9_CreateDeviceObjects();
|
if (!overlay::OVERLAY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (overlay::OVERLAY->renderer) {
|
||||||
|
case OverlayRenderer::D3D9:
|
||||||
|
ImGui_ImplDX9_CreateDeviceObjects();
|
||||||
|
break;
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
case OverlayRenderer::D3D11:
|
||||||
|
// RTV is lazily recreated on the next render()
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
case OverlayRenderer::SOFTWARE:
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void overlay::SpiceOverlay::input_char(unsigned int c) {
|
void overlay::SpiceOverlay::input_char(unsigned int c) {
|
||||||
@@ -700,7 +942,7 @@ void overlay::SpiceOverlay::input_char(unsigned int c) {
|
|||||||
|
|
||||||
uint32_t *overlay::SpiceOverlay::sw_get_pixel_data(int *width, int *height) {
|
uint32_t *overlay::SpiceOverlay::sw_get_pixel_data(int *width, int *height) {
|
||||||
|
|
||||||
// check if active
|
// check if active (notifications never draw in software renderer, so no extra gate here)
|
||||||
if (!this->active) {
|
if (!this->active) {
|
||||||
*width = 0;
|
*width = 0;
|
||||||
*height = 0;
|
*height = 0;
|
||||||
|
|||||||
@@ -10,11 +10,22 @@
|
|||||||
|
|
||||||
#include "external/imgui/imgui.h"
|
#include "external/imgui/imgui.h"
|
||||||
|
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
// forward decls for D3D11 (avoid pulling d3d11.h into every TU)
|
||||||
|
struct ID3D11Device;
|
||||||
|
struct ID3D11DeviceContext;
|
||||||
|
struct ID3D11RenderTargetView;
|
||||||
|
struct IDXGISwapChain;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace overlay {
|
namespace overlay {
|
||||||
class Window;
|
class Window;
|
||||||
|
|
||||||
enum class OverlayRenderer {
|
enum class OverlayRenderer {
|
||||||
D3D9,
|
D3D9,
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
D3D11,
|
||||||
|
#endif
|
||||||
SOFTWARE,
|
SOFTWARE,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,12 +73,19 @@ namespace overlay {
|
|||||||
Window *window_log = nullptr;
|
Window *window_log = nullptr;
|
||||||
|
|
||||||
explicit SpiceOverlay(HWND hWnd, IDirect3D9 *d3d, IDirect3DDevice9 *device);
|
explicit SpiceOverlay(HWND hWnd, IDirect3D9 *d3d, IDirect3DDevice9 *device);
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
explicit SpiceOverlay(HWND hWnd, ID3D11Device *d3d11_device,
|
||||||
|
ID3D11DeviceContext *d3d11_context, IDXGISwapChain *d3d11_swapchain);
|
||||||
|
#endif
|
||||||
explicit SpiceOverlay(HWND hWnd);
|
explicit SpiceOverlay(HWND hWnd);
|
||||||
~SpiceOverlay();
|
~SpiceOverlay();
|
||||||
|
|
||||||
void window_add(Window *wnd);
|
void window_add(Window *wnd);
|
||||||
void new_frame();
|
void new_frame();
|
||||||
void render();
|
void render();
|
||||||
|
// configurator-only (spicecfg D3D9): submit ImGui draw data inside BeginScene
|
||||||
|
// after render() when the frame is being presented. In-game overlay draws in render().
|
||||||
|
void d3d9_render_draw(bool force_submit = false);
|
||||||
void update();
|
void update();
|
||||||
void toggle_active(bool overlay_key = false);
|
void toggle_active(bool overlay_key = false);
|
||||||
void show_main_menu();
|
void show_main_menu();
|
||||||
@@ -92,6 +110,14 @@ namespace overlay {
|
|||||||
inline bool uses_device(IDirect3DDevice9 *other) {
|
inline bool uses_device(IDirect3DDevice9 *other) {
|
||||||
return this->device == other;
|
return this->device == other;
|
||||||
}
|
}
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
inline bool uses_device(ID3D11Device *other) {
|
||||||
|
return this->d3d11_device == other;
|
||||||
|
}
|
||||||
|
inline bool uses_swapchain(IDXGISwapChain *other) {
|
||||||
|
return this->d3d11_swapchain == other;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
inline IDirect3DDevice9 *get_device() {
|
inline IDirect3DDevice9 *get_device() {
|
||||||
return this->device;
|
return this->device;
|
||||||
}
|
}
|
||||||
@@ -116,6 +142,16 @@ namespace overlay {
|
|||||||
OverlayRenderer renderer;
|
OverlayRenderer renderer;
|
||||||
float total_elapsed = 0.f;
|
float total_elapsed = 0.f;
|
||||||
|
|
||||||
|
// set to true by the SOFTWARE renderer when the pixel buffer was updated
|
||||||
|
// in the most recent render() call. Allows the configurator's WM_PAINT
|
||||||
|
// driver to skip its full-window InvalidateRect on idle frames where the
|
||||||
|
// ImGui draw data is bitwise-identical to the previous frame.
|
||||||
|
bool sw_pixels_dirty = false;
|
||||||
|
|
||||||
|
// configurator-only (spicecfg D3D9): set by render() when ImGui draw data
|
||||||
|
// changed since the last submitted frame; configurator skips Clear/Present when false.
|
||||||
|
bool d3d9_frame_dirty = false;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
HWND hWnd = nullptr;
|
HWND hWnd = nullptr;
|
||||||
@@ -124,10 +160,25 @@ namespace overlay {
|
|||||||
IDirect3D9 *d3d = nullptr;
|
IDirect3D9 *d3d = nullptr;
|
||||||
IDirect3DDevice9 *device = nullptr;
|
IDirect3DDevice9 *device = nullptr;
|
||||||
|
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
// D3D11
|
||||||
|
ID3D11Device *d3d11_device = nullptr;
|
||||||
|
ID3D11DeviceContext *d3d11_context = nullptr;
|
||||||
|
IDXGISwapChain *d3d11_swapchain = nullptr;
|
||||||
|
ID3D11RenderTargetView *d3d11_rtv = nullptr;
|
||||||
|
#endif
|
||||||
|
|
||||||
// software
|
// software
|
||||||
std::vector<uint32_t> pixel_data;
|
std::vector<uint32_t> pixel_data;
|
||||||
size_t pixel_data_width = 0;
|
size_t pixel_data_width = 0;
|
||||||
size_t pixel_data_height = 0;
|
size_t pixel_data_height = 0;
|
||||||
|
uint64_t sw_last_draw_hash = 0;
|
||||||
|
bool sw_has_last_draw_hash = false;
|
||||||
|
|
||||||
|
uint64_t d3d9_last_draw_hash = 0;
|
||||||
|
bool d3d9_has_last_draw_hash = false;
|
||||||
|
int d3d9_last_display_w = 0;
|
||||||
|
int d3d9_last_display_h = 0;
|
||||||
|
|
||||||
std::vector<std::unique_ptr<Window>> windows;
|
std::vector<std::unique_ptr<Window>> windows;
|
||||||
|
|
||||||
@@ -141,6 +192,10 @@ namespace overlay {
|
|||||||
bool hotkey_toggle = false;
|
bool hotkey_toggle = false;
|
||||||
bool hotkey_toggle_last = false;
|
bool hotkey_toggle_last = false;
|
||||||
|
|
||||||
|
// true between new_frame()'s ImGui::NewFrame() and render()'s ImGui::Render(),
|
||||||
|
// so render() never runs without a matching NewFrame.
|
||||||
|
bool has_pending_frame = false;
|
||||||
|
|
||||||
void init();
|
void init();
|
||||||
void add_font(const char* font, ImFontConfig* config, const ImWchar* glyphs);
|
void add_font(const char* font, ImFontConfig* config, const ImWchar* glyphs);
|
||||||
};
|
};
|
||||||
@@ -152,6 +207,10 @@ namespace overlay {
|
|||||||
|
|
||||||
// synchronized helpers
|
// synchronized helpers
|
||||||
void create_d3d9(HWND hWnd, IDirect3D9 *d3d, IDirect3DDevice9 *device);
|
void create_d3d9(HWND hWnd, IDirect3D9 *d3d, IDirect3DDevice9 *device);
|
||||||
|
#ifdef SPICE_D3D11
|
||||||
|
void create_d3d11(HWND hWnd, ID3D11Device *device, ID3D11DeviceContext *context,
|
||||||
|
IDXGISwapChain *swapchain);
|
||||||
|
#endif
|
||||||
void create_software(HWND hWnd);
|
void create_software(HWND hWnd);
|
||||||
void destroy(HWND hWnd = nullptr);
|
void destroy(HWND hWnd = nullptr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ namespace overlay::windows {
|
|||||||
this->flags |= ImGuiWindowFlags_NoTitleBar;
|
this->flags |= ImGuiWindowFlags_NoTitleBar;
|
||||||
this->flags |= ImGuiWindowFlags_NoCollapse;
|
this->flags |= ImGuiWindowFlags_NoCollapse;
|
||||||
this->flags |= ImGuiWindowFlags_NoDecoration;
|
this->flags |= ImGuiWindowFlags_NoDecoration;
|
||||||
|
// prevent the parent window from absorbing wheel events when a hovered
|
||||||
|
// child has no scrollable content (otherwise the parent's tiny overflow
|
||||||
|
// causes the whole UI to jolt by a few pixels each tick)
|
||||||
|
this->flags |= ImGuiWindowFlags_NoScrollWithMouse;
|
||||||
}
|
}
|
||||||
this->flags |= ImGuiWindowFlags_MenuBar;
|
this->flags |= ImGuiWindowFlags_MenuBar;
|
||||||
|
|
||||||
@@ -154,12 +158,14 @@ namespace overlay::windows {
|
|||||||
// add games to the config and window
|
// add games to the config and window
|
||||||
auto &config = ::Config::getInstance();
|
auto &config = ::Config::getInstance();
|
||||||
for (auto &game : games_list) {
|
for (auto &game : games_list) {
|
||||||
config.addGame(game);
|
config.addGame(game, /* save */ false);
|
||||||
|
|
||||||
if (!config.getStatus()) {
|
if (!config.getStatus()) {
|
||||||
log_warning("config", "failure adding game: {}", game.getGameName());
|
log_warning("config", "failure adding game: {}", game.getGameName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// single flush in place of ~33 redundant writes during the loop above
|
||||||
|
config.save();
|
||||||
|
|
||||||
// read card numbers
|
// read card numbers
|
||||||
read_card();
|
read_card();
|
||||||
@@ -948,7 +954,7 @@ namespace overlay::windows {
|
|||||||
|
|
||||||
// grab current keyboard state
|
// grab current keyboard state
|
||||||
for (unsigned short int i = 0x01; i < 0xFF; i++) {
|
for (unsigned short int i = 0x01; i < 0xFF; i++) {
|
||||||
buttons_keyboard_state[i] = GetAsyncKeyState(i) != 0;
|
buttons_keyboard_state[i] = (GetAsyncKeyState(i) & 0x8000) != 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered(ImGui::TOOLTIP_FLAGS)) {
|
if (ImGui::IsItemHovered(ImGui::TOOLTIP_FLAGS)) {
|
||||||
@@ -1615,7 +1621,7 @@ namespace overlay::windows {
|
|||||||
// use care when iterating over the result (could result in torn reads)
|
// use care when iterating over the result (could result in torn reads)
|
||||||
bool keyboard_state_new[sizeof(buttons_keyboard_state)];
|
bool keyboard_state_new[sizeof(buttons_keyboard_state)];
|
||||||
for (size_t i = 0; i < sizeof(keyboard_state_new); i++) {
|
for (size_t i = 0; i < sizeof(keyboard_state_new); i++) {
|
||||||
keyboard_state_new[i] = GetAsyncKeyState(i) != 0;
|
keyboard_state_new[i] = (GetAsyncKeyState(i) & 0x8000) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// detect key presses
|
// detect key presses
|
||||||
@@ -2539,9 +2545,9 @@ namespace overlay::windows {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// hide deadzone for circular analog since it doesn't make any sense
|
// hide deadzone for circular analog since it doesn't make any sense (unless in relative mode)
|
||||||
if ((device->type == rawinput::HID || device->type == rawinput::MIDI) &&
|
if ((device->type == rawinput::HID || device->type == rawinput::MIDI) &&
|
||||||
analog.getType() != GameAPI::Analogs::AnalogType::Circular) {
|
((analog.getType() != GameAPI::Analogs::AnalogType::Circular) || analog.isRelativeMode())) {
|
||||||
auto deadzone = analog.getDeadzone();
|
auto deadzone = analog.getDeadzone();
|
||||||
|
|
||||||
// for back compat (before each analog had a type)
|
// for back compat (before each analog had a type)
|
||||||
@@ -2585,16 +2591,46 @@ namespace overlay::windows {
|
|||||||
if (analog.getType() == GameAPI::Analogs::AnalogType::Circular) {
|
if (analog.getType() == GameAPI::Analogs::AnalogType::Circular) {
|
||||||
// smoothing
|
// smoothing
|
||||||
bool smoothing = analog.getSmoothing();
|
bool smoothing = analog.getSmoothing();
|
||||||
|
ImGui::BeginDisabled(analog.isRelativeMode());
|
||||||
ImGui::Checkbox("Smooth Axis (adds latency)", &smoothing);
|
ImGui::Checkbox("Smooth Axis (adds latency)", &smoothing);
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::HelpMarker(
|
ImGui::HelpMarker(
|
||||||
"Apply a moving average algorithm; intended for angular input (knobs, turntables). "
|
"Apply a moving average algorithm; intended for angular input (knobs, turntables). "
|
||||||
"Adds a slight bit of latency to input as the algorithm averages out recent input. "
|
"Adds a slight bit of latency to input as the algorithm averages out recent input. "
|
||||||
"Only use in dire situations where the input is too jittery for the game.");
|
"Only use in dire situations where the input is too jittery for the game.");
|
||||||
|
ImGui::EndDisabled();
|
||||||
if (smoothing != analog.getSmoothing()) {
|
if (smoothing != analog.getSmoothing()) {
|
||||||
analog.setSmoothing(smoothing);
|
analog.setSmoothing(smoothing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// relative input mode
|
||||||
|
bool relative_analog = analog.isRelativeMode();
|
||||||
|
ImGui::Checkbox("Relative Axis", &relative_analog);
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::HelpMarker(
|
||||||
|
"Use relative directional input instead of positional values. "
|
||||||
|
"Can be used to translate analog sticks to knob input.\n\n"
|
||||||
|
"At default settings, max speed is one revolution per second.\n\n"
|
||||||
|
"Adjust sensitivity to increase/decrease max speed, and deadzone to prevent jitter.");
|
||||||
|
if (relative_analog != analog.isRelativeMode()) {
|
||||||
|
analog.setRelativeMode(relative_analog);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delay
|
||||||
|
int delay = analog.getDelayMs();
|
||||||
|
if (ImGui::InputInt("Delay (ms)", &delay, 1, 1)) {
|
||||||
|
delay = std::clamp(delay, 0, 250);
|
||||||
|
analog.setDelayMs(delay);
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::HelpMarker(
|
||||||
|
"Adds an artificial delay, in milliseconds.\n\n"
|
||||||
|
"Value read by the game engine will be delayed AT MOST this amount of time (could be slightly less).\n\n"
|
||||||
|
"To minimize quantization error, delay value should be a multiple of the game's poll rate, "
|
||||||
|
"add 0.5ms of buffer, and then round up to the nearest millisecond.\n\n"
|
||||||
|
"If you aren't sure, assume the game polls for input at the same rate as the display FPS."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5406,6 +5442,8 @@ namespace overlay::windows {
|
|||||||
a.setInvert(ta.invert);
|
a.setInvert(ta.invert);
|
||||||
a.setSmoothing(ta.smoothing);
|
a.setSmoothing(ta.smoothing);
|
||||||
a.setMultiplier(ta.multiplier);
|
a.setMultiplier(ta.multiplier);
|
||||||
|
a.setRelativeMode(ta.relative_mode);
|
||||||
|
a.setDelayMs(ta.delay_ms);
|
||||||
::Config::getInstance().updateBinding(game, a);
|
::Config::getInstance().updateBinding(game, a);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ namespace overlay::windows {
|
|||||||
bool invert = false;
|
bool invert = false;
|
||||||
bool smoothing = false;
|
bool smoothing = false;
|
||||||
int multiplier = 1;
|
int multiplier = 1;
|
||||||
|
bool relative_mode = false;
|
||||||
|
uint32_t delay_ms = 0;
|
||||||
|
|
||||||
bool is_device() const { return !device_identifier.empty(); }
|
bool is_device() const { return !device_identifier.empty(); }
|
||||||
bool is_unbound() const { return device_identifier.empty() && index == 0xFF; }
|
bool is_unbound() const { return device_identifier.empty() && index == 0xFF; }
|
||||||
@@ -87,6 +89,8 @@ namespace overlay::windows {
|
|||||||
invert = a.getInvert();
|
invert = a.getInvert();
|
||||||
smoothing = a.getSmoothing();
|
smoothing = a.getSmoothing();
|
||||||
multiplier = a.getMultiplier();
|
multiplier = a.getMultiplier();
|
||||||
|
relative_mode = a.isRelativeMode();
|
||||||
|
delay_ms = a.getDelayMs();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ namespace overlay::windows {
|
|||||||
sub = "Show Valkyrie Subscreen";
|
sub = "Show Valkyrie Subscreen";
|
||||||
} else if (avs::game::is_model("REC")) {
|
} else if (avs::game::is_model("REC")) {
|
||||||
sub = "Show DRS Dance Floor";
|
sub = "Show DRS Dance Floor";
|
||||||
} else if (games::gitadora::is_arena_model() && games::gitadora::ARENA_SINGLE_WINDOW) {
|
} else if (games::gitadora::is_arena_model()) {
|
||||||
sub = "Show GITADORA Subscreen";
|
sub = "Show GITADORA Subscreen";
|
||||||
} else if (games::popn::is_pikapika_model()) {
|
} else if (games::popn::is_pikapika_model()) {
|
||||||
sub = "Show Pop'n Subscreen";
|
sub = "Show Pop'n Subscreen";
|
||||||
|
|||||||
@@ -231,11 +231,15 @@ namespace overlay::windows {
|
|||||||
surface->Release();
|
surface->Release();
|
||||||
texture_surface->Release();
|
texture_surface->Release();
|
||||||
|
|
||||||
// draw the subscreen (this draws *under* ImGui windows, over the game surface)
|
// draw the subscreen into the current window's draw list so it participates in
|
||||||
|
// normal ImGui z-ordering (i.e. other ImGui windows can appear above it)
|
||||||
auto bottom_right = overlay_content_size;
|
auto bottom_right = overlay_content_size;
|
||||||
bottom_right.x += overlay_content_top_left.x;
|
bottom_right.x += overlay_content_top_left.x;
|
||||||
bottom_right.y += overlay_content_top_left.y;
|
bottom_right.y += overlay_content_top_left.y;
|
||||||
ImGui::GetBackgroundDrawList()->AddImage(
|
auto draw_list = this->draws_window
|
||||||
|
? ImGui::GetWindowDrawList()
|
||||||
|
: ImGui::GetBackgroundDrawList();
|
||||||
|
draw_list->AddImage(
|
||||||
reinterpret_cast<ImTextureID>(this->texture),
|
reinterpret_cast<ImTextureID>(this->texture),
|
||||||
overlay_content_top_left,
|
overlay_content_top_left,
|
||||||
bottom_right);
|
bottom_right);
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ namespace overlay::windows {
|
|||||||
|
|
||||||
if (!games::gitadora::is_arena_model()) {
|
if (!games::gitadora::is_arena_model()) {
|
||||||
this->disabled_message = "Requires Arena Model!";
|
this->disabled_message = "Requires Arena Model!";
|
||||||
|
} else if (!GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
|
if (GRAPHICS_WINDOWED) {
|
||||||
|
this->disabled_message = "Use the dedicated subscreen window!";
|
||||||
|
} else {
|
||||||
|
this->disabled_message = "Subscreen overlay disabled by user; enable single window mode for overlay.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this->resize_callback = keep_10_by_16;
|
this->resize_callback = keep_10_by_16;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user