Compare commits

...

30 Commits

Author SHA1 Message Date
bicarus ece03cabba cfg: asio driver selector must list both 32-bit and 64-bit drivers (#734)
## Link to GitHub Issue or related Pull Request, if one exists
#730 

## Description of change
spicecfg is a 32-bit application. When it uses the ASIO SDK it only saw
the 32-bit drivers.

There are some ASIO drivers that have different names under 32-bit and
64-bit (Xonar AE is one of them)... so we have to manually scan the
registry and surface both the WOW32 and WOW64 nodes.

## Testing
<img width="329" height="309" alt="image"
src="https://github.com/user-attachments/assets/7abecf76-6835-4df5-8c2e-e7b425130f4c"
/>

Checked both 32-bit and 64-bit configurator, identical results.
2026-06-05 02:05:03 -07:00
bicarus 4b7f68f920 gitadora: (arena model) flip realtek option (#733)
## Link to GitHub Issue or related Pull Request, if one exists
#730 

## Description of change
Flip the option - enable the Realtek hack by default, unless the user
chooses not to.

## Testing
Tested ASIO path and WASAPI path. WASAPI path should be unaffected as
the game just picks the default audio device.
2026-06-05 01:38:46 -07:00
bicarus 2dae86a6f2 gitadora: (arena model) simulate Realtek device, fix asio redirect hooks (#732)
## Link to GitHub Issue or related Pull Request, if one exists
#730, #718

## Description of change

Part 1)

When ASIO is in use, the game also looks for a Realtek device so that it
can open a WASAPI Exclusive mode stream for headphones.

Add an option to fake that, in case the user doesn't have a Realtek
device.

Part 2)

`-gdaasio` wasn't workign properly - fix the logic.

## Testing
Seems to work with FlexASIO, and Xonar with no real Realtek device.
2026-06-05 00:08:52 -07:00
bicarus 85058c2156 audio: create a wrapper for asio drivers (#731)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Create `WrappedAsio`, similar to how we wrap `IAudioEndpoint`

So far, the wrapper does these things:

1. logging (for diagnosis, since iidx and gfdm don't produce any logs
when asio succeeds)
2. iidx32+ hack to work around refcount mismatch issue
3. sdvx valk cab hack to force 2-channel audio ("downmixing" by taking
only the front channels)
4. honor volume boost

As a result of `#2` the mempatch was removed from `iidx.cpp` since we
can tackle it cleanly in the hook. `#3` also removes the need for manual
patches.

Real downmixing is hard & expensive on the CPU so it was not
implemented.

## Testing
Tested iidx/sdvx/gfdm with xonar and flexasio
2026-06-04 09:24:44 -07:00
bicarus ea2f4c5572 gitadora: (arena model) complain loudly about bad prop files (#729)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Refuse to load if `libaio.dll` exists but `<spec>` is set incorrectly
(almost certainly because the user copied old prop files from downlevel
version)

## Testing
2026-06-02 17:47:51 -07:00
bicarus 48033816a8 audio: WASAPI exclusive resampling, buffer size increase options (#727)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change

Resampler:
Implement resampler for exclusive mode streams, as we are seeing more
and more devices - not just laptops but onboard audio devices - that
only support 48khz and not 44.1khz. Should work with volume boost (gain
calculated inside resample) and also downmixer (hands off intermediate
scratch buffers).

Buffer size increase:
By default many of these games request a tiny buffer when in shared mode
(TDJ uses 3ms). On some audio setup this results in crackling due to
underflow. Add an option to forcibly increase the buffer size.

## Testing
With resampler set to 48kHz and buffer set to 20ms I can reliably boot
and play IIDX on my display port monitor's speakers; previously this
wasn't possible. IIDX is event-driven.

Tested SDVX7 as well at 48kHz, which opens timer-driven streams.
2026-06-02 01:55:50 -07:00
bicarus-dev b517ef3182 update options 2026-06-01 02:29:12 -07:00
bicarus 6fef16353e otoca: patch up game for printing holo cards (#726)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #154

## Description of change
Patch the game to skip over special holo printing logic and redirect to
the normal printer, resolving the soft lock + printer failure.

## Testing
Tested `NCG-2019012900`. Other versions might not work though since this
is patch-based not hook-based.
2026-05-31 23:08:17 -07:00
bicarus 84ea3f9e8e audio: stereo downmix, volume boost options (#722)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #717, fixes #647

## Description of change
Adds an option to downmix surround sound (5.1, 7.1, etc) down to stereo
(2 speakers). This can be used in most WASAPI games, including Gitadora
and FTT.

How it works: when the game tries to open surround format (say, 7.1) we
create a fake buffer and tell the game that it is supported. In reality
we open a 2-channel stream with the real sound card. When the stream
begins, we downmix the channels down to two (using one of many
algorithms) and output to the sound card.

While we're here, implement an option to boost the game volume by some
decibel value, which is a feature often requested by SDVX players. This
was needed since downmixing can sometimes result in quieter audio.

## Testing
Tested GW Delta and FTT.

Downmix doesn't work on IIDX (32 bits) but volume boost works.
2026-05-31 20:49:22 -07:00
Azalea 6bb6b0a301 [O] Filter GetIpAddrTable to selected network adapter (#724)
## Description of change
Filter GetIpAddrTable to selected network adapter. 

Fixes the IP address error (5-1506-0000) on gitadora gw delta when
multiple network adapters are present.

## Testing
Tested on Gitadora GW Delta with both WiFi and Tailscale adapters
present.
2026-05-31 16:17:47 -07:00
drmext 465d6c6c18 cfg: fix alt+f4 exit (#723)
## Link to GitHub Issue or related Pull Request, if one exists
#720

## Description of change
Pass `VK_F4` sys-key messages through to `DefWindowProc` before ImGui
ingestion, restoring the normal Alt+F4 -> `WM_CLOSE` ->
`launcher::shutdown()` path.

## Testing
- [x] Launch `spicecfg` (D3D9 default path) and press Alt+F4; window
closes and process exits
- [x] Launch `spicecfg` (software path) and press Alt+F4; window closes
and process exits
- [x] Close via title-bar X; still works
2026-05-30 23:59:54 -07:00
Selundine ef384e85dd Add GITADORA Arena windowed monitor layout support, allows the SMALL touch window to be resized (#716)
## Link to GitHub Issue or related Pull Request, if one exists
N/A

## Description of change
Adds GITADORA Arena windowed monitor layout options for the GITADORA,
LEFT, RIGHT, and SMALL windows.

This also applies the selected window border style to all four GITADORA
Arena windows, and allows the SMALL touch window to be resized in
windowed mode while keeping touch coordinates updated.

## Testing
- Manually tested GITADORA Arena windowed mode:
  - default, resizable, and borderless window styles
  - per-window monitor placement
  - SMALL resize
  - SMALL touch coordinate mapping after resize
2026-05-30 23:09:06 -07:00
bicarus 7ee04879e2 overlay: option to force software renderer in configurator (#721)
## Link to GitHub Issue or related Pull Request, if one exists
chicken bit for #720
2026-05-30 15:24:07 -07:00
drmext 4c73200f58 cfg: d3d9 standalone configurator (#720)
## Description of change
**Configurator (spicecfg.exe)**

- Attempts to create a D3D9 device on startup and uses the hardware
ImGui DX9 path when successful; falls back to the existing software
rasterizer automatically if D3D9 init fails.
- Rewrites the configurator window loop: refresh-rate-aware render
timer, minimize/resize/`WM_DISPLAYCHANGE` handling, direct Win32 ->
ImGui input (keyboard, mouse, wheel), and optimized software painting
via `SetDIBitsToDevice` instead of per-frame GDI `HBITMAP` allocation.

**Overlay / input (scoped to standalone configurator)**

- Skips expensive per-frame rawinput polling in
`ImGui_ImplSpice_NewFrame` when `CONFIGURATOR_STANDALONE` is set (Win32
messages drive input instead).
- Adds software-renderer idle-frame detection (`sw_pixels_dirty` +
draw-data hash) so spicecfg only repaints when pixels actually change.
- Adds `ImGuiWindowFlags_NoScrollWithMouse` on the configurator root
window to prevent scroll jolt on non-scrollable child widgets.
2026-05-30 12:52:35 -07:00
bicarus-dev 31aabe9786 fix option name 2026-05-30 01:22:09 -07:00
bicarus 4ea55a61b8 gitadora: (arena model) asio option (#718)
## Link to GitHub Issue or related Pull Request, if one exists
#717 

## Description of change
Add an option to override the ASIO device.

By default the game will try to use any ASIO driver that contains the
string `XONAR` in it.

## Testing
Tested with FlexASIO.
2026-05-30 00:03:06 -07:00
bicarus 0209b80a22 graphics: address perf issues with DX9 image resize logic (#712)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Move resize logic from `EndScene` to `Present`/`PresentEx` (resolves
Live2D frame drops in some extreme settings)

Plug small memory leak

Remove unnecessary surface Lock/Unlock

## Testing
Tested IIDX, SDVX, DDR.
2026-05-29 09:33:31 -07:00
drmext 0511dfb6ca rawinput: fix spicecfg close delay (#715)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Closing spicecfg was blocked for up to ~495 ms. Runtime behavior is
unchanged: same flush interval and same output path during normal
operation. Only shutdown teardown is faster.

## Testing
Opened and closed spicecfg.exe repeatedly; the window closes immediately
with no hang
2026-05-29 09:33:08 -07:00
drmext b558df3340 cfg: optimize saving config (#714)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Save config xml once instead of ~33 times

## Testing
xml is still saved properly
2026-05-29 09:09:02 -07:00
bicarus 89602cfcab misc: clean up eamuse_get_game (#713)
No need to allocate a new `std::string` every call...
2026-05-29 09:08:07 -07:00
drmext b4557993c9 cfg: optimize saving patches to file on disk (#711)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Saving patches to file used to rewrite the file repeatedly, now it loads
and saves only once. Original file timestamp is now preserved on the
backup.

## Testing
Saved a very large patch set that used to take 10+ seconds; now it is
instant. Confirmed hex diffs are still accurate.
2026-05-29 02:20:39 -07:00
bicarus f119d7c988 overlay: fix z-ordering of subscreen windows (#710)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Subscreen overlay window used to draw above the main game surface, but
below any other ImGui widgets. Fix this so that the subscreen image is
drawn at the same z-level as the sub window itself.

This has been a long-standing issue - at least since 2023 when spice2x
first forked...

## Testing
Tested TDJ in windowed mode.
2026-05-29 02:03:31 -07:00
bicarus b3d8d0aea9 overlay: implement dx11 backend (#707)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #134 

## Description of change
Add ImGui DX11 backend implementation, and integrate into the spice
overlay.

The tricky part is that some of the Unity games:

1. Launch multiple windows, and
2. Can be resized (even full screen games launch at a certain resolution
and then expand to fit desktop resolution)
3. lazy load DX11 DLLs

This PR also adds screenshot functionality, but screen resize is not
implemented.

## Testing

Seems to be working for most games. Need to do final tests for:

- [x] CCJ (attract movie plays as well)
- [x] Busou Shinki (title movie plays)
- [x] MFG
- [x] QuizKnock
- [x] Polaris Chord
- [x] check dx9 for regression
2026-05-29 00:47:28 -07:00
drmext a977cba772 overlay: prevent save button shifting patches text (#709)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
When opening the patches tab by pressing F4 in game, the Save button
doesn't appear until after initially selecting/deselecting a patch, then
it shifts all of the patch text below. To solve this annoyance, this
keeps the Save button in a fixed position (starts greyed out until a
selection) and no longer shifts the text.

## Testing
Confirmed saving selected patches still works properly in the overlay
and standalone cfg
2026-05-28 23:58:09 -07:00
bicarus 489d40d87d utils: dump memory info on crash (#708)
For diagnosing out of memory errors / malloc failures.
2026-05-28 23:57:34 -07:00
bicarus-dev 3a468a9a3b fix up option name 2026-05-27 21:06:05 -07:00
bicarus a2e220d3f4 gitadora: (arena model) unify window layout options (#706)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Combine the two layout options into one

## Testing

- [x] windowed, x4
- [x] windowed, x2
- [x] windowed, x1 with sub
- [x] fs
2026-05-27 21:04:23 -07:00
Azalea febc02b50b [+] Hide sides on gitadora 𝛿 (#705)
## Description of change
Added an option to only disable the left/right screens of gitadora gw
delta while exposing the touch panel screen.

## Testing
Tested by hand on CachyOS + hyprland + gamescope + wine + vnc + phone +
cat 🐈‍⬛

<img width="2560" height="1707" alt="image"
src="https://github.com/user-attachments/assets/a2ef0172-68e2-43c2-89c3-4863698f2f00"
/>
2026-05-27 18:57:10 -07:00
bicarus 311404f56b overlay: toast notifications (#704)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Adds toast notifications to the overlay.

Toast notifications are transient windows (shown for about 4 seconds).
These are drawn on top of the game even when the overlay is inactive
(i.e., when no window is open), and does not cause any input sink
behavior.

Add toasts for some common user-driven actions that could use a
notification, such as:

* screenshot
* card insert
* PIN macro
* API client connect/disconnect
* virtual printer
* screen resize toggle / scene switch

Add an option to change where the toasts are displayed, or to turn it
off entirely.

Add SDK function for it, and add it to the samples.

## Testing
Tested DDR (shown at bottom right, as is for most games) and RB (shown
top right by default).
2026-05-27 00:45:27 -07:00
bicarus-dev d929cdf7c8 clean up for release 2026-05-25 17:30:38 -07:00
89 changed files with 7975 additions and 445 deletions
+29 -2
View File
@@ -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
@@ -486,6 +494,8 @@ set(SOURCE_FILES ${SOURCE_FILES}
# hooks # hooks
hooks/audio/acm.cpp hooks/audio/acm.cpp
hooks/audio/audio.cpp hooks/audio/audio.cpp
hooks/audio/asio_driver_scan.cpp
hooks/audio/asio_proxy.cpp
hooks/audio/buffer.cpp hooks/audio/buffer.cpp
hooks/audio/mme.cpp hooks/audio/mme.cpp
hooks/audio/util.cpp hooks/audio/util.cpp
@@ -494,8 +504,12 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/audio/backends/mmdevice/device.cpp hooks/audio/backends/mmdevice/device.cpp
hooks/audio/backends/mmdevice/device_collection.cpp hooks/audio/backends/mmdevice/device_collection.cpp
hooks/audio/backends/mmdevice/device_enumerator.cpp hooks/audio/backends/mmdevice/device_enumerator.cpp
hooks/audio/backends/mmdevice/null_device.cpp
hooks/audio/backends/mmdevice/null_discard_backend.cpp
hooks/audio/backends/wasapi/audio_client.cpp hooks/audio/backends/wasapi/audio_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/resample.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,6 +531,11 @@ 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
@@ -558,6 +577,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
@@ -713,6 +733,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")
@@ -732,6 +756,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")
@@ -744,7 +771,7 @@ set(SOURCE_FILES ${SOURCE_FILES} launcher/options.h launcher/options.cpp)
set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc) 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 "")
@@ -762,7 +789,7 @@ set(SOURCE_FILES ${SOURCE_FILES} launcher/options.h launcher/options.cpp)
set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc) 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")
+8
View File
@@ -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();
+7
View File
@@ -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());
+2 -2
View File
@@ -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 ""
+9 -2
View File
@@ -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");
@@ -486,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
// and flushes once at the end via Config::save())
if (save) {
this->saveConfigFile(); 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
+6 -1
View File
@@ -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);
+73 -2
View File
@@ -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;
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::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;
+1
View File
@@ -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:
+429 -40
View File
@@ -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();
}
if (use_d3d9) {
const HRESULT cl = state->device->TestCooperativeLevel();
if (cl == D3DERR_DEVICELOST) {
break;
}
if (cl == D3DERR_DEVICENOTRESET) {
if (overlay::OVERLAY) {
overlay::OVERLAY->reset_invalidate();
}
const HRESULT reset_hr = state->device->Reset(&state->pp);
if (SUCCEEDED(reset_hr) && overlay::OVERLAY) {
overlay::OVERLAY->reset_recreate();
}
break;
}
if (FAILED(cl)) {
break;
}
if (overlay::OVERLAY) {
overlay::OVERLAY->render(); overlay::OVERLAY->render();
} }
// repaint window // skip Clear/Present when ImGui draw data is unchanged; the last
InvalidateRect(hWnd, nullptr, TRUE); // 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) {
// get pixel data
int width, height;
uint32_t *pixel_data = overlay::OVERLAY->sw_get_pixel_data(&width, &height);
if (width > 0 && height > 0) {
// create bitmap
HBITMAP bitmap = CreateBitmap(width, height, 1, 8 * sizeof(uint32_t), pixel_data);
// prepare paint
PAINTSTRUCT paint{};
HDC hdc = BeginPaint(hWnd, &paint);
HDC hdcMem = CreateCompatibleDC(hdc);
SetBkMode(hdc, TRANSPARENT);
// draw bitmap
SelectObject(hdcMem, bitmap);
BitBlt(hdc, paint.rcPaint.left, paint.rcPaint.top,
paint.rcPaint.right - paint.rcPaint.left,
paint.rcPaint.bottom - paint.rcPaint.top,
hdcMem, paint.rcPaint.left, paint.rcPaint.top, SRCCOPY);
// delete bitmap
DeleteObject(bitmap);
// clean up
DeleteDC(hdcMem);
EndPaint(hWnd, &paint);
} else {
return DefWindowProc(hWnd, uMsg, wParam, lParam);
} }
case WM_ACTIVATE: {
const WORD activation = LOWORD(wParam);
if (activation == WA_INACTIVE) {
// dropping any held mouse buttons so we don't get stuck in a
// "button still pressed" state when we come back.
auto &io = ImGui::GetIO();
io.AddMouseButtonEvent(ImGuiMouseButton_Left, false);
io.AddMouseButtonEvent(ImGuiMouseButton_Right, false);
io.AddMouseButtonEvent(ImGuiMouseButton_Middle, false);
} else { } 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);
} }
+31 -1
View File
@@ -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);
}; };
} }
+7
View File
@@ -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})
+697
View File
@@ -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
+52
View File
@@ -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
+171
View File
@@ -0,0 +1,171 @@
#include "asio.h"
#include <windows.h>
#include <cstring>
#include "avs/game.h"
#include "gitadora.h"
#include "util/detour.h"
#include "util/logging.h"
namespace games::gitadora {
// Redirects the game's hard-coded "XONAR" ASIO driver lookup to the
// driver name in ASIO_DRIVER by intercepting registry calls to
// HKLM\SOFTWARE\ASIO. Sentinel HKEY values mark the redirected handles
// so we can recognise them on subsequent reg* calls.
static const HKEY PARENT_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4001);
static const HKEY DEVICE_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4002);
static const char *FAKE_ASIO_DEVICE_NAME = "XONAR";
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
static decltype(RegOpenKeyA) *RegOpenKeyA_orig = nullptr;
static decltype(RegOpenKeyExA) *RegOpenKeyExA_orig = nullptr;
static decltype(RegQueryValueExA) *RegQueryValueExA_orig = nullptr;
static HKEY real_asio_reg_handle = nullptr;
static HKEY real_asio_device_reg_handle = nullptr;
static LONG WINAPI RegOpenKeyExA_hook(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired,
PHKEY phkResult)
{
if (ASIO_DRIVER.has_value() &&
lpSubKey != nullptr &&
phkResult != nullptr &&
hKey == PARENT_ASIO_REG_HANDLE &&
_stricmp(lpSubKey, FAKE_ASIO_DEVICE_NAME) == 0) {
*phkResult = DEVICE_ASIO_REG_HANDLE;
log_info("gitadora::asio", "replacing '{}' with '{}'", lpSubKey, ASIO_DRIVER.value());
const auto result = RegOpenKeyExA_orig(
real_asio_reg_handle,
ASIO_DRIVER.value().c_str(),
ulOptions,
samDesired,
&real_asio_device_reg_handle);
if (result != ERROR_SUCCESS) {
log_warning(
"gitadora::asio",
"failed to open registry subkey '{}', error=0x{:x}",
ASIO_DRIVER.value(), result);
log_warning(
"gitadora::asio",
"due to improper ASIO setting, audio init will fail");
}
return result;
}
return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, phkResult);
}
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
if (ASIO_DRIVER.has_value() &&
lpSubKey != nullptr &&
phkResult != nullptr &&
hKey == HKEY_LOCAL_MACHINE &&
_stricmp(lpSubKey, "software\\asio") == 0)
{
*phkResult = PARENT_ASIO_REG_HANDLE;
return RegOpenKeyA_orig(hKey, lpSubKey, &real_asio_reg_handle);
}
return RegOpenKeyA_orig(hKey, lpSubKey, phkResult);
}
static LONG WINAPI RegEnumKeyA_hook(HKEY hKey, DWORD dwIndex, LPSTR lpName, DWORD cchName) {
if (hKey == PARENT_ASIO_REG_HANDLE && ASIO_DRIVER.has_value()) {
if (dwIndex == 0) {
// forward to real handle just to verify the key exists; we
// overwrite the name with our fake driver string regardless
auto ret = RegEnumKeyA_orig(real_asio_reg_handle, dwIndex, lpName, cchName);
if (ret == ERROR_SUCCESS && lpName != nullptr && cchName > 0) {
log_info("gitadora::asio", "stubbing '{}' with '{}'", lpName, FAKE_ASIO_DEVICE_NAME);
strncpy(lpName, FAKE_ASIO_DEVICE_NAME, cchName);
lpName[cchName - 1] = '\0';
}
return ret;
} else {
return ERROR_NO_MORE_ITEMS;
}
}
return RegEnumKeyA_orig(hKey, dwIndex, lpName, cchName);
}
static LONG WINAPI RegQueryValueExA_hook(HKEY hKey, LPCSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType,
LPBYTE lpData, LPDWORD lpcbData)
{
HKEY target = hKey;
if (ASIO_DRIVER.has_value() &&
lpValueName != nullptr &&
lpData != nullptr &&
lpcbData != nullptr &&
hKey == DEVICE_ASIO_REG_HANDLE) {
if (_stricmp(lpValueName, "Description") == 0) {
// engine may verify the driver name after open; ensure it still
// sees something containing "XONAR" so the substring check passes
const size_t len = strlen(FAKE_ASIO_DEVICE_NAME) + 1;
if (*lpcbData < len) {
*lpcbData = static_cast<DWORD>(len);
return ERROR_MORE_DATA;
}
memcpy(lpData, FAKE_ASIO_DEVICE_NAME, len);
*lpcbData = static_cast<DWORD>(len);
if (lpType != nullptr) {
*lpType = REG_SZ;
}
return ERROR_SUCCESS;
}
// for everything else (CLSID etc.) defer to the real driver subkey
target = real_asio_device_reg_handle;
}
return RegQueryValueExA_orig(target, lpValueName, lpReserved, lpType, lpData, lpcbData);
}
static LONG WINAPI RegCloseKey_hook(HKEY hKey) {
if (hKey == PARENT_ASIO_REG_HANDLE) {
if (real_asio_reg_handle != nullptr) {
RegCloseKey_orig(real_asio_reg_handle);
real_asio_reg_handle = nullptr;
}
return ERROR_SUCCESS;
}
if (hKey == DEVICE_ASIO_REG_HANDLE) {
if (real_asio_device_reg_handle != nullptr) {
RegCloseKey_orig(real_asio_device_reg_handle);
real_asio_device_reg_handle = nullptr;
}
return ERROR_SUCCESS;
}
return RegCloseKey_orig(hKey);
}
void asio_hook_init() {
if (!ASIO_DRIVER.has_value()) {
return;
}
log_info("gitadora::asio", "installing ASIO driver redirect: XONAR -> {}", ASIO_DRIVER.value());
RegCloseKey_orig = detour::iat_try(
"RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE);
RegEnumKeyA_orig = detour::iat_try(
"RegEnumKeyA", RegEnumKeyA_hook, avs::game::DLL_INSTANCE);
RegOpenKeyA_orig = detour::iat_try(
"RegOpenKeyA", RegOpenKeyA_hook, avs::game::DLL_INSTANCE);
RegOpenKeyExA_orig = detour::iat_try(
"RegOpenKeyExA", RegOpenKeyExA_hook, avs::game::DLL_INSTANCE);
RegQueryValueExA_orig = detour::iat_try(
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
}
}
+12
View File
@@ -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();
}
+98 -23
View File
@@ -1,8 +1,14 @@
#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/audio.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"
@@ -27,6 +33,9 @@ namespace games::gitadora {
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;
bool ALLOW_REALTEK_AUDIO = false;
/* /*
* Prevent GitaDora from creating folders on F drive * Prevent GitaDora from creating folders on F drive
@@ -223,12 +232,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() && !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;
@@ -241,14 +244,46 @@ namespace games::gitadora {
#if SPICE64 && !SPICE_XP #if SPICE64 && !SPICE_XP
if (is_arena_model() && !GRAPHICS_WINDOWED && !GRAPHICS_FORCE_SINGLE_ADAPTER) { if (is_arena_model()) {
const auto &monitors = sysutils::enumerate_monitors(); // in full screen, if single-adapter option is checked, it's functionally
const size_t active_count = monitors.size(); // the same as forcing a single monitor
log_info("gitadora", "arena model: detected {} active monitor(s)", active_count); if (!GRAPHICS_WINDOWED && GRAPHICS_FORCE_SINGLE_ADAPTER) {
if (active_count < 4) { ARENA_WINDOW_COUNT = 1;
log_info("gitadora", "arena model: enable single monitor mode due to insufficient monitors"); }
// 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_FORCE_SINGLE_ADAPTER = true;
GRAPHICS_PREVENT_SECONDARY_WINDOW = 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);
} }
} }
@@ -527,6 +562,13 @@ namespace games::gitadora {
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");
@@ -553,6 +595,12 @@ namespace games::gitadora {
#ifdef SPICE64 #ifdef SPICE64
// gitadora arena model // gitadora arena model
auto aio = libutils::try_library("libaio.dll"); auto aio = libutils::try_library("libaio.dll");
// before we start patching and hooking things, detect invalid configuration
if (aio != nullptr && !is_arena_model()) {
log_fatal("gitadora", "arena model i/o (libaio.dll) detected, but <spec> is not an arena model - bad prop XML files?");
}
if (aio != nullptr) { if (aio != nullptr) {
SETUPAPI_SETTINGS settings{}; SETUPAPI_SETTINGS settings{};
settings.class_guid[0] = 0x86E0D1E0; settings.class_guid[0] = 0x86E0D1E0;
@@ -578,11 +626,32 @@ 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);
// fake Realtek audio injection
// if ASIO init succeeds, game tries to look for audio device with `Realtek` in friendly name
// if ASIO init fails, game opens default audio device
// therefore, it's safe to enable this hook by default regardless of ASIO preference
// (unless the user explicitly disables it, of course)
if (ALLOW_REALTEK_AUDIO) {
log_info(
"gitadora",
"fake Realtek audio injection disabled "
"(user's real Realtek audio may be used after successful ASIO init)");
} else {
log_info(
"gitadora",
"fake Realtek audio injection enabled "
"(create a fake Realtek audio device to prevent crashes after successful ASIO init)");
hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true;
}
// monitor/touch hooks (windowed or full screen) // monitor/touch hooks (windowed or full screen)
if (GRAPHICS_FORCE_SINGLE_ADAPTER || GRAPHICS_PREVENT_SECONDARY_WINDOW) { if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
// enable touch hook for subscreen overlay // 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;
@@ -620,14 +689,7 @@ namespace games::gitadora {
} }
// two channel mod // two channel mod
if (TWOCHANNEL) { if (TWOCHANNEL && !is_arena_model()) {
if (is_arena_model()) {
log_warning("gitadora", "two channel audio (-2ch) is not supported on Arena Model - use a patch instead");
deferredlogs::defer_error_messages({
"two channel audio (-2ch) is not supported on Arena Model - use a patch instead",
});
} else {
HMODULE bmsd_engine_module = libutils::try_module("libbmsd-engine.dll"); HMODULE bmsd_engine_module = libutils::try_module("libbmsd-engine.dll");
HMODULE bmsd_module = libutils::try_module("libbmsd.dll"); HMODULE bmsd_module = libutils::try_module("libbmsd.dll");
@@ -637,9 +699,22 @@ namespace games::gitadora {
log_warning("gitadora", "two channel mode failed"); log_warning("gitadora", "two channel mode failed");
} }
} }
}
#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;
}
}
} }
+8
View File
@@ -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"
@@ -15,6 +18,9 @@ namespace games::gitadora {
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;
extern bool ALLOW_REALTEK_AUDIO;
class GitaDoraGame : public games::Game { class GitaDoraGame : public games::Game {
public: public:
@@ -23,6 +29,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" }) ||
+4 -32
View File
@@ -893,38 +893,10 @@ namespace games::iidx {
} }
} }
// patch iidx32+ for asio compatibility // note: the iidx32+ ASIO refcount bug (a duplicate AddRef on the ASIO instance with
// only do this if NOT wasapi (as opposed to checking if it's asio) // no matching Release, which leaks the driver and can hang non-XONAR devices) is now
// the patch is only really needed for (some) non-XONAR devices but since people sometimes disguise // handled transparently by the WrappedAsio proxy (see hooks/audio/asio_proxy.cpp),
// other devices as a XONAR, don't check for the exact string (common ASIO workaround for INF) // so no game-DLL signature patch is needed here anymore
if (avs::game::is_ext(2024090100, INT_MAX) &&
!(SOUND_OUTPUT_DEVICE_IN_EFFECT.has_value() &&
SOUND_OUTPUT_DEVICE_IN_EFFECT.value() == "wasapi")) {
// in iidx32 final:
// ff 50 08 call QWORD PTR [rax+0x8] ; ASIO instance AddRef
// 48 8b 4b 08 mov rcx,QWORD PTR [rbx+0x8]
// 48 8b 01 mov rax,QWORD PTR [rcx]
// ff 50 08 call QWORD PTR [rax+0x8] ; ASIO instance AddRef
intptr_t result = replace_pattern(
avs::game::DLL_INSTANCE,
"FF50????????????????FF50??4533C94533C0418D51",
"????????????????????909090??????????????????",
0, 0);
if (result == 0) {
log_warning(
"iidx",
"Failed to apply ASIO compatibility fix for iidx32+. "
"Unless patches are applied, your ASIO device may hang or fail to work");
} else {
log_info(
"iidx",
"Successfully applied ASIO compatibility fix for iidx32+ using signature matching @ 0x{:x}.",
result);
}
}
#endif #endif
+74
View File
@@ -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";
+7
View File
@@ -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)));
} }
} }
} }
@@ -0,0 +1,74 @@
#include "asio_driver_scan.h"
#include <algorithm>
#include <windows.h>
#include "util/utils.h"
namespace hooks::audio {
static constexpr char ASIO_REG_PATH[] = "software\\asio";
static constexpr char ASIO_REG_DESC[] = "description";
// enumerate a single registry view, appending to entries while merging
// duplicates discovered in another view. Drivers are matched by name (not
// CLSID): the game's ASIO loader selects drivers by name, and some vendors
// register the same CLSID under different 32-bit/64-bit names (e.g. "XONAR
// SOUND CARD" vs "XONAR SOUND CARD(64)"), which are distinct user choices.
static void scan_view(
REGSAM wow64_flag,
bool is_64bit,
std::vector<AsioDriverScanEntry> &entries) {
HKEY hkEnum = nullptr;
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, ASIO_REG_PATH, 0,
KEY_READ | wow64_flag, &hkEnum) != ERROR_SUCCESS) {
return;
}
char key_name[256];
for (DWORD index = 0;
RegEnumKeyA(hkEnum, index, key_name, sizeof(key_name)) == ERROR_SUCCESS;
index++) {
// read description (display name), fall back to the key name
char desc[256] = { 0 };
DWORD size = sizeof(desc);
std::string name = key_name;
if (RegGetValueA(hkEnum,
key_name,
ASIO_REG_DESC,
RRF_RT_REG_SZ | wow64_flag,
nullptr,
desc,
&size) == ERROR_SUCCESS && desc[0]) {
name = desc;
}
// merge with an existing entry from the other view (match by name)
const std::string name_lower = strtolower(name);
auto it = std::find_if(entries.begin(), entries.end(), [&](const auto &e) {
return strtolower(e.name) == name_lower;
});
if (it == entries.end()) {
entries.push_back({ name });
it = entries.end() - 1;
}
it->found_32bit |= !is_64bit;
it->found_64bit |= is_64bit;
}
RegCloseKey(hkEnum);
}
std::vector<AsioDriverScanEntry> scan_asio_drivers() {
std::vector<AsioDriverScanEntry> entries;
// 64-bit view first so it wins ordering when present in both
scan_view(KEY_WOW64_64KEY, true, entries);
scan_view(KEY_WOW64_32KEY, false, entries);
return entries;
}
}
@@ -0,0 +1,15 @@
#pragma once
#include <string>
#include <vector>
namespace hooks::audio {
struct AsioDriverScanEntry {
std::string name;
bool found_32bit = false;
bool found_64bit = false;
};
std::vector<AsioDriverScanEntry> scan_asio_drivers();
}
+813
View File
@@ -0,0 +1,813 @@
#include "asio_proxy.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <mutex>
#include <vector>
#include "external/asio/asiolist.h"
#include "hooks/audio/audio.h"
#include "util/logging.h"
#include "util/utils.h"
namespace {
// readable name for an ASIO sample type (e.g. "ASIOSTInt32LSB"), falling back to the
// numeric value for unknown types
const char *asio_sample_type_name(AsioSampleType type) {
switch (type) {
case ASIOSTInt16MSB: return "ASIOSTInt16MSB";
case ASIOSTInt24MSB: return "ASIOSTInt24MSB";
case ASIOSTInt32MSB: return "ASIOSTInt32MSB";
case ASIOSTFloat32MSB: return "ASIOSTFloat32MSB";
case ASIOSTFloat64MSB: return "ASIOSTFloat64MSB";
case ASIOSTInt32MSB16: return "ASIOSTInt32MSB16";
case ASIOSTInt32MSB18: return "ASIOSTInt32MSB18";
case ASIOSTInt32MSB20: return "ASIOSTInt32MSB20";
case ASIOSTInt32MSB24: return "ASIOSTInt32MSB24";
case ASIOSTInt16LSB: return "ASIOSTInt16LSB";
case ASIOSTInt24LSB: return "ASIOSTInt24LSB";
case ASIOSTInt32LSB: return "ASIOSTInt32LSB";
case ASIOSTFloat32LSB: return "ASIOSTFloat32LSB";
case ASIOSTFloat64LSB: return "ASIOSTFloat64LSB";
case ASIOSTInt32LSB16: return "ASIOSTInt32LSB16";
case ASIOSTInt32LSB18: return "ASIOSTInt32LSB18";
case ASIOSTInt32LSB20: return "ASIOSTInt32LSB20";
case ASIOSTInt32LSB24: return "ASIOSTInt32LSB24";
default: return "unknown";
}
}
// duration in milliseconds of a buffer of the given frame count at a sample rate, or a
// negative sentinel when the frame count or sample rate is unusable
double frames_to_ms(long frames, AsioSampleRate sample_rate) {
if (frames < 0 || sample_rate <= 0.0) {
return -1.0;
}
return (frames * 1000.0) / sample_rate;
}
// scales one planar ASIO output buffer (frames samples of the given type) by gain in
// place, clamping integer formats so a boost saturates instead of wrapping. unsupported
// formats are left untouched. runs on the driver's realtime thread, so no allocation,
// locking or logging here
void apply_gain_planar(void *buffer, long frames, AsioSampleType type, float gain) {
if (buffer == nullptr || frames <= 0) {
return;
}
switch (type) {
case ASIOSTFloat32LSB: {
auto p = static_cast<float *>(buffer);
for (long i = 0; i < frames; i++) {
p[i] = std::clamp(p[i] * gain, -1.0f, 1.0f);
}
break;
}
case ASIOSTFloat64LSB: {
auto p = static_cast<double *>(buffer);
for (long i = 0; i < frames; i++) {
p[i] = std::clamp(p[i] * static_cast<double>(gain), -1.0, 1.0);
}
break;
}
case ASIOSTInt16LSB: {
auto p = static_cast<int16_t *>(buffer);
for (long i = 0; i < frames; i++) {
p[i] = static_cast<int16_t>(
std::clamp(std::lround(p[i] * gain), -32768L, 32767L));
}
break;
}
case ASIOSTInt24LSB: {
// packed 24-bit little-endian, 3 bytes per sample
auto bytes = static_cast<uint8_t *>(buffer);
for (long i = 0; i < frames; i++) {
uint8_t *s = bytes + i * 3;
int32_t v = s[0] | (s[1] << 8) | (s[2] << 16);
if (v & 0x800000) {
v |= ~0xFFFFFF; // sign extend
}
int64_t scaled = std::clamp<int64_t>(
std::llround(static_cast<double>(v) * gain), -8388608, 8388607);
s[0] = scaled & 0xFF;
s[1] = (scaled >> 8) & 0xFF;
s[2] = (scaled >> 16) & 0xFF;
}
break;
}
case ASIOSTInt32LSB: {
auto p = static_cast<int32_t *>(buffer);
for (long i = 0; i < frames; i++) {
p[i] = static_cast<int32_t>(std::clamp<int64_t>(
std::llround(static_cast<double>(p[i]) * gain), INT32_MIN, INT32_MAX));
}
break;
}
default:
// unsupported format (MSB, aligned 32-bit, DSD): leave untouched
break;
}
}
// live wrappers by CLSID. ASIO drivers are single-instance, so a host that creates a
// new instance without releasing the old one has leaked it; we track this to tear the
// stale one down
std::mutex g_wrappers_mutex;
std::vector<std::pair<CLSID, WrappedAsio *>> g_wrappers;
// register a wrapper as the live instance for its CLSID, returning any stale wrapper it
// replaces so the caller can tear it down outside the lock
WrappedAsio *register_wrapper(REFCLSID clsid, WrappedAsio *wrapper) {
std::lock_guard lock(g_wrappers_mutex);
for (auto &entry : g_wrappers) {
if (IsEqualCLSID(entry.first, clsid)) {
WrappedAsio *stale = entry.second;
entry.second = wrapper;
return stale;
}
}
g_wrappers.emplace_back(clsid, wrapper);
return nullptr;
}
void unregister_wrapper(WrappedAsio *wrapper) {
std::lock_guard lock(g_wrappers_mutex);
for (auto it = g_wrappers.begin(); it != g_wrappers.end(); ++it) {
if (it->second == wrapper) {
g_wrappers.erase(it);
return;
}
}
}
// ASIO drivers registered on this system (CLSID + registry name), scanned once
const std::vector<std::pair<CLSID, std::string>> &registered_asio_drivers() {
static const std::vector<std::pair<CLSID, std::string>> drivers = [] {
std::vector<std::pair<CLSID, std::string>> result;
AsioDriverList driver_list;
for (const auto &driver : driver_list.driver_list) {
result.emplace_back(driver.clsid, driver.name);
log_info(
"audio::wrappedasio",
"registered ASIO driver: name='{}', clsid={}",
driver.name,
guid2s(driver.clsid));
}
log_info("audio::wrappedasio", "discovered {} registered ASIO driver(s)", result.size());
return result;
}();
return drivers;
}
std::string registered_asio_name(REFCLSID clsid) {
for (const auto &driver : registered_asio_drivers()) {
if (IsEqualCLSID(driver.first, clsid)) {
return driver.second;
}
}
return guid2s(clsid);
}
}
namespace hooks::audio::asio {
bool is_asio_creation(REFCLSID rclsid, REFIID riid) {
// ASIO hosts request the driver using its own CLSID as the interface id
if (!IsEqualGUID(rclsid, riid)) {
return false;
}
for (const auto &driver : registered_asio_drivers()) {
if (IsEqualCLSID(driver.first, rclsid)) {
return true;
}
}
return false;
}
}
#pragma region IUnknown
bool WrappedAsio::FORCE_TWO_CHANNELS = false;
std::atomic<WrappedAsio *> WrappedAsio::volume_active_instance {nullptr};
WrappedAsio::~WrappedAsio() {
unregister_wrapper(this);
this->detach_volume();
// our refcount is decoupled from the host's (see AddRef/Release), so pReal's count is
// exactly one here and this releases/unloads it deterministically
this->pReal->Release();
log_info("audio::wrappedasio", "destroying wrapped ASIO driver, clsid={}", guid2s(this->clsid));
}
HRESULT STDMETHODCALLTYPE WrappedAsio::QueryInterface(REFIID riid, void **ppv) {
if (ppv == nullptr) {
return E_POINTER;
}
// ASIO hosts query for the driver using its own CLSID as the IID
if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, this->clsid)) {
this->AddRef();
*ppv = static_cast<IAsio *>(this);
return S_OK;
}
// the host is asking for some other interface; forward to the real driver. a failure
// here is a common reason a host discards a driver and retries
const HRESULT ret = this->pReal->QueryInterface(riid, ppv);
if (SUCCEEDED(ret)) {
log_info("audio::wrappedasio", "QueryInterface({}) -> forwarded to real driver", guid2s(riid));
} else {
log_info(
"audio::wrappedasio",
"QueryInterface({}) -> not supported by driver, hr={:#x}",
guid2s(riid),
static_cast<unsigned long>(ret));
}
return ret;
}
ULONG STDMETHODCALLTYPE WrappedAsio::AddRef() {
// decoupled from the real driver: we count host references on the wrapper and hold a
// single reference on pReal for our lifetime. this neutralizes a host bug (iidx32+)
// that takes a duplicate AddRef with no matching Release, which would leak the driver
return ++this->ref_count;
}
ULONG STDMETHODCALLTYPE WrappedAsio::Release() {
const ULONG refs = --this->ref_count;
if (refs == 0) {
delete this;
}
return refs;
}
#pragma endregion
#pragma region IAsio
AsioBool __thiscall WrappedAsio::init(void *sys_handle) {
const AsioBool result = this->pReal->init(sys_handle);
if (result == AsioTrue) {
log_info(
"audio::wrappedasio",
"init succeeded for '{}' (driver version {})",
this->driver_name,
this->pReal->get_driver_version());
} else {
char message[128] = {};
this->pReal->get_error_message(message);
log_warning("audio::wrappedasio", "init failed: {}", message);
}
return result;
}
void __thiscall WrappedAsio::get_driver_name(char *name) {
this->pReal->get_driver_name(name);
}
long __thiscall WrappedAsio::get_driver_version() {
return this->pReal->get_driver_version();
}
void __thiscall WrappedAsio::get_error_message(char *string) {
this->pReal->get_error_message(string);
}
AsioError __thiscall WrappedAsio::start() {
const AsioError result = this->pReal->start();
if (result == ASE_OK) {
log_info(
"audio::wrappedasio",
"start succeeded, ASIO stream is now running on '{}'",
this->driver_name);
} else {
log_warning("audio::wrappedasio", "start failed, err={}", static_cast<long>(result));
}
return result;
}
AsioError __thiscall WrappedAsio::stop() {
const AsioError result = this->pReal->stop();
if (result == ASE_OK) {
log_info("audio::wrappedasio", "stop succeeded, ASIO stream on '{}' halted", this->driver_name);
} else {
log_warning("audio::wrappedasio", "stop failed, err={}", static_cast<long>(result));
}
return result;
}
AsioError __thiscall WrappedAsio::get_channels(long *num_input_channels, long *num_output_channels) {
const AsioError result = this->pReal->get_channels(num_input_channels, num_output_channels);
if (result != ASE_OK) {
log_warning("audio::wrappedasio", "get_channels failed, err={}", static_cast<long>(result));
return result;
}
if (FORCE_TWO_CHANNELS
&& num_output_channels != nullptr
&& *num_output_channels < FORCED_OUTPUT_CHANNELS)
{
// the device has fewer outputs than the game hardcodes; report the count it
// expects so it proceeds to create_buffers, where we forward only the real
// front pair and discard the rest
log_info(
"audio::wrappedasio",
"reporting output channel count as {} (device has {}) for forced two-channel",
FORCED_OUTPUT_CHANNELS,
*num_output_channels);
*num_output_channels = FORCED_OUTPUT_CHANNELS;
}
log_info(
"audio::wrappedasio",
"get_channels -> in={}, out={}",
num_input_channels ? *num_input_channels : -1,
num_output_channels ? *num_output_channels : -1);
return result;
}
AsioError __thiscall WrappedAsio::get_latencies(long *input_latency, long *output_latency) {
const AsioError result = this->pReal->get_latencies(input_latency, output_latency);
if (result == ASE_OK) {
// include millisecond equivalents alongside the frame counts for readability
AsioSampleRate sample_rate = 0.0;
this->pReal->get_sample_rate(&sample_rate);
const long in_frames = input_latency ? *input_latency : -1;
const long out_frames = output_latency ? *output_latency : -1;
log_info(
"audio::wrappedasio",
"get_latencies -> in={} frames ({:.2f} ms), out={} frames ({:.2f} ms)",
in_frames,
frames_to_ms(in_frames, sample_rate),
out_frames,
frames_to_ms(out_frames, sample_rate));
} else {
log_warning("audio::wrappedasio", "get_latencies failed, err={}", static_cast<long>(result));
}
return result;
}
AsioError __thiscall WrappedAsio::get_buffer_size(
long *min_size,
long *max_size,
long *preferred_size,
long *granularity)
{
const AsioError result = this->pReal->get_buffer_size(min_size, max_size, preferred_size, granularity);
if (result != ASE_OK) {
log_warning("audio::wrappedasio", "get_buffer_size failed, err={}", static_cast<long>(result));
return result;
}
// include millisecond equivalents alongside the frame counts for readability
AsioSampleRate sample_rate = 0.0;
this->pReal->get_sample_rate(&sample_rate);
const long min_frames = min_size ? *min_size : -1;
const long max_frames = max_size ? *max_size : -1;
const long preferred_frames = preferred_size ? *preferred_size : -1;
log_info(
"audio::wrappedasio",
"get_buffer_size -> min={} frames ({:.2f} ms), max={} frames ({:.2f} ms), "
"preferred={} frames ({:.2f} ms), granularity={}",
min_frames,
frames_to_ms(min_frames, sample_rate),
max_frames,
frames_to_ms(max_frames, sample_rate),
preferred_frames,
frames_to_ms(preferred_frames, sample_rate),
granularity ? *granularity : -1);
return result;
}
AsioError __thiscall WrappedAsio::can_sample_rate(AsioSampleRate sample_rate) {
const AsioError result = this->pReal->can_sample_rate(sample_rate);
if (result == ASE_OK) {
log_misc("audio::wrappedasio", "can_sample_rate({} Hz) -> supported", sample_rate);
} else {
log_misc(
"audio::wrappedasio",
"can_sample_rate({} Hz) -> not supported, err={}",
sample_rate,
static_cast<long>(result));
}
return result;
}
AsioError __thiscall WrappedAsio::get_sample_rate(AsioSampleRate *sample_rate) {
const AsioError result = this->pReal->get_sample_rate(sample_rate);
if (result == ASE_OK) {
log_misc("audio::wrappedasio", "get_sample_rate -> {} Hz", sample_rate ? *sample_rate : 0.0);
} else {
log_warning("audio::wrappedasio", "get_sample_rate failed, err={}", static_cast<long>(result));
}
return result;
}
AsioError __thiscall WrappedAsio::set_sample_rate(AsioSampleRate sample_rate) {
const AsioError result = this->pReal->set_sample_rate(sample_rate);
if (result == ASE_OK) {
log_info("audio::wrappedasio", "set_sample_rate({} Hz) succeeded", sample_rate);
} else {
log_warning(
"audio::wrappedasio",
"set_sample_rate({} Hz) failed, err={}",
sample_rate,
static_cast<long>(result));
}
return result;
}
AsioError __thiscall WrappedAsio::get_clock_sources(ASIOClockSource *clocks, long *num_sources) {
return this->pReal->get_clock_sources(clocks, num_sources);
}
AsioError __thiscall WrappedAsio::set_clock_source(long reference) {
return this->pReal->set_clock_source(reference);
}
AsioError __thiscall WrappedAsio::get_sample_position(ASIOSamples *s_pos, ASIOTimeStamp *t_stamp) {
return this->pReal->get_sample_position(s_pos, t_stamp);
}
AsioError __thiscall WrappedAsio::get_channel_info(AsioChannelInfo *info) {
// forced two-channel: the game probes all output channels it thinks exist, but the
// device only has the real front pair. fabricate a plausible entry for the channels
// beyond the device without touching the real driver - they are discarded in
// create_buffers anyway
long real_in = 0, real_out = 0;
if (FORCE_TWO_CHANNELS
&& info != nullptr
&& info->is_input == AsioFalse
&& this->pReal->get_channels(&real_in, &real_out) == ASE_OK
&& info->channel >= real_out)
{
const long channel = info->channel;
info->is_active = AsioTrue;
info->channel_group = 0;
info->type = ASIOSTInt32LSB;
snprintf(info->name, sizeof(info->name), "Fake ASIO OUT %ld", channel);
log_info(
"audio::wrappedasio",
"get_channel_info(channel={}, dir=output) -> fake channel, type={} ({})",
channel,
asio_sample_type_name(info->type),
static_cast<long>(info->type));
return ASE_OK;
}
const AsioError result = this->pReal->get_channel_info(info);
if (result == ASE_OK && info != nullptr) {
log_info(
"audio::wrappedasio",
"get_channel_info(channel={}, dir={}) -> active={}, group={}, type={} ({}), name='{}'",
info->channel,
info->is_input == AsioTrue ? "input" : "output",
info->is_active == AsioTrue,
info->channel_group,
asio_sample_type_name(info->type),
static_cast<long>(info->type),
info->name);
} else if (result != ASE_OK) {
log_warning("audio::wrappedasio", "get_channel_info failed, err={}", static_cast<long>(result));
}
return result;
}
AsioCallbacks *WrappedAsio::install_volume_callbacks(AsioCallbacks *game_callbacks) {
const float gain = hooks::audio::VOLUME_BOOST;
// start from a clean slate; a previous buffer set may have left state behind
this->volume_channels.clear();
this->volume_active = false;
// no boost configured (or no callbacks to wrap): pass the game's callbacks straight
// through and do zero realtime work, exactly as before
if (gain == 1.0f || game_callbacks == nullptr) {
return game_callbacks;
}
this->volume_active = true;
this->volume_gain = gain;
this->volume_game_callbacks = *game_callbacks;
// wrap only the buffer-switch callbacks, where the audio data lives and we apply the
// gain. the other two carry no data we touch, so forward the game's own pointers
// unchanged - the driver expects them non-null and the game already owns their context
this->volume_proxy_callbacks = {};
this->volume_proxy_callbacks.buffer_switch = &WrappedAsio::volume_buffer_switch;
this->volume_proxy_callbacks.sample_rate_did_change = game_callbacks->sample_rate_did_change;
this->volume_proxy_callbacks.asio_message = game_callbacks->asio_message;
this->volume_proxy_callbacks.buffer_switch_time_info =
game_callbacks->buffer_switch_time_info ? &WrappedAsio::volume_buffer_switch_time_info : nullptr;
return &this->volume_proxy_callbacks;
}
void WrappedAsio::record_volume_output_channel(const AsioBufferInfo &info) {
if (info.is_input != AsioFalse) {
return;
}
// ask the real driver for this channel's sample format so the realtime path knows how
// to scale it; fall back to a sentinel that apply_gain_planar leaves untouched
AsioChannelInfo ci {};
ci.channel = info.channel_num;
ci.is_input = AsioFalse;
AsioSampleType type = ASIOSTLastEntry;
if (this->pReal->get_channel_info(&ci) == ASE_OK) {
type = ci.type;
}
VolumeOutputChannel ch;
ch.buffers[0] = info.buffers[0];
ch.buffers[1] = info.buffers[1];
ch.type = type;
this->volume_channels.push_back(ch);
}
void WrappedAsio::publish_volume(long buffer_size) {
if (!this->volume_active) {
return;
}
// everything the realtime thread reads is now in place; make ourselves reachable
this->volume_buffer_size = buffer_size;
WrappedAsio::volume_active_instance.store(this, std::memory_order_release);
log_info(
"audio::wrappedasio",
"volume boost active: gain={}, scaling {} output channel(s)",
this->volume_gain,
this->volume_channels.size());
}
void WrappedAsio::detach_volume() {
// stop our realtime trampolines from reaching this wrapper, but only if we are the
// currently published instance
WrappedAsio *expected = this;
WrappedAsio::volume_active_instance.compare_exchange_strong(expected, nullptr);
}
void WrappedAsio::apply_output_volume(long double_buffer_index) {
if (double_buffer_index != 0 && double_buffer_index != 1) {
return;
}
const float gain = this->volume_gain;
const long frames = this->volume_buffer_size;
for (const VolumeOutputChannel &ch : this->volume_channels) {
apply_gain_planar(ch.buffers[double_buffer_index], frames, ch.type, gain);
}
}
void __cdecl WrappedAsio::volume_buffer_switch(long double_buffer_index, AsioBool direct_process) {
WrappedAsio *self = WrappedAsio::volume_active_instance.load(std::memory_order_acquire);
if (self == nullptr) {
return;
}
// let the game write its samples into the driver buffers first, then scale them before
// the driver plays this half on the next switch
if (self->volume_game_callbacks.buffer_switch != nullptr) {
self->volume_game_callbacks.buffer_switch(double_buffer_index, direct_process);
}
self->apply_output_volume(double_buffer_index);
}
AsioTime * __cdecl WrappedAsio::volume_buffer_switch_time_info(
AsioTime *params, long double_buffer_index, AsioBool direct_process)
{
WrappedAsio *self = WrappedAsio::volume_active_instance.load(std::memory_order_acquire);
if (self == nullptr) {
return params;
}
AsioTime *ret = params;
if (self->volume_game_callbacks.buffer_switch_time_info != nullptr) {
ret = self->volume_game_callbacks.buffer_switch_time_info(
params, double_buffer_index, direct_process);
} else if (self->volume_game_callbacks.buffer_switch != nullptr) {
self->volume_game_callbacks.buffer_switch(double_buffer_index, direct_process);
}
self->apply_output_volume(double_buffer_index);
return ret;
}
AsioError __thiscall WrappedAsio::create_buffers(
AsioBufferInfo *buffer_infos,
long num_channels,
long buffer_size,
AsioCallbacks *callbacks)
{
// swap in our buffer-switch trampolines if a volume boost is configured, so the real
// driver calls us and we scale its output after the game fills it (no-op otherwise)
AsioCallbacks *effective = this->install_volume_callbacks(callbacks);
if (FORCE_TWO_CHANNELS) {
return this->create_buffers_front_pair(buffer_infos, num_channels, buffer_size, effective);
}
const AsioError result = this->pReal->create_buffers(buffer_infos, num_channels, buffer_size, effective);
if (result == ASE_OK) {
log_info(
"audio::wrappedasio",
"create_buffers(channels={}, size={} frames) succeeded",
num_channels,
buffer_size);
// capture the device output channels we will scale, then publish ourselves to the
// realtime thread once everything is in place
if (this->volume_active) {
for (long i = 0; i < num_channels; i++) {
this->record_volume_output_channel(buffer_infos[i]);
}
this->publish_volume(buffer_size);
}
} else {
log_warning(
"audio::wrappedasio",
"create_buffers(channels={}, size={} frames) failed, err={}",
num_channels,
buffer_size,
static_cast<long>(result));
}
return result;
}
AsioError WrappedAsio::create_buffers_front_pair(
AsioBufferInfo *buffer_infos,
long num_channels,
long buffer_size,
AsioCallbacks *callbacks)
{
// front-pair extraction (forced two-channel ASIO): the game asks for more output
// channels than the real device has (e.g. 8 vs 2). forward only the channels the
// device actually provides (channel 0/1 = front L/R) and hand the game throwaway
// buffers for the rest, so its front mix lands on the device and the surround
// channels are discarded. the game writes directly into the driver/dummy buffers
// from its own bufferSwitch; the only realtime work we do is the volume boost (if
// configured), which scales the forwarded device channels via our trampolines
long real_in = 0, real_out = 0;
const AsioError ch_result = this->pReal->get_channels(&real_in, &real_out);
if (ch_result != ASE_OK) {
log_warning(
"audio::wrappedasio",
"create_buffers: get_channels failed, err={}",
static_cast<long>(ch_result));
return ch_result;
}
// partition the requested channels: those the device can serve are forwarded, the rest
// are discarded. record source indices for both so we can patch the game's array after
std::vector<AsioBufferInfo> forwarded;
std::vector<long> forwarded_src;
std::vector<long> discarded_src;
forwarded.reserve(num_channels);
forwarded_src.reserve(num_channels);
discarded_src.reserve(num_channels);
for (long i = 0; i < num_channels; i++) {
const AsioBufferInfo &bi = buffer_infos[i];
const long limit = (bi.is_input == AsioTrue) ? real_in : real_out;
if (bi.channel_num < limit) {
forwarded.push_back(bi);
forwarded_src.push_back(i);
} else {
discarded_src.push_back(i);
}
}
const AsioError result = this->pReal->create_buffers(
forwarded.data(), static_cast<long>(forwarded.size()), buffer_size, callbacks);
if (result != ASE_OK) {
log_warning(
"audio::wrappedasio",
"create_buffers(forwarded={} of {}, size={} frames) failed, err={}",
forwarded.size(),
num_channels,
buffer_size,
static_cast<long>(result));
return result;
}
// copy the real driver buffer pointers back into the game's array
for (size_t k = 0; k < forwarded.size(); k++) {
AsioBufferInfo &dst = buffer_infos[forwarded_src[k]];
dst.buffers[0] = forwarded[k].buffers[0];
dst.buffers[1] = forwarded[k].buffers[1];
// only the forwarded channels reach the device, so those are the ones the volume
// boost scales (the discarded channels go to throwaway buffers below)
if (this->volume_active) {
this->record_volume_output_channel(dst);
}
}
this->publish_volume(buffer_size);
// hand throwaway double buffers to the discarded channels. sized generously at
// 8 bytes/sample (covers every ASIO sample type) so the game can never overrun them
// regardless of the negotiated format
this->dummy_buffers.clear();
this->dummy_buffers.reserve(discarded_src.size() * 2);
const size_t dummy_bytes = static_cast<size_t>(buffer_size) * 8;
for (const long i : discarded_src) {
for (void *&buffer : buffer_infos[i].buffers) {
auto buf = std::make_unique<uint8_t[]>(dummy_bytes);
std::memset(buf.get(), 0, dummy_bytes);
buffer = buf.get();
this->dummy_buffers.push_back(std::move(buf));
}
}
log_info(
"audio::wrappedasio",
"create_buffers: front-pair extraction - forwarded {} channel(s) to device, "
"discarded {} (requested {}, size={} frames)",
forwarded.size(),
discarded_src.size(),
num_channels,
buffer_size);
return ASE_OK;
}
AsioError __thiscall WrappedAsio::dispose_buffers() {
// stop our realtime trampolines from touching buffers the driver is about to free
this->detach_volume();
const AsioError result = this->pReal->dispose_buffers();
this->dummy_buffers.clear();
this->volume_channels.clear();
this->volume_active = false;
return result;
}
AsioError __thiscall WrappedAsio::control_panel() {
return this->pReal->control_panel();
}
AsioError __thiscall WrappedAsio::future(long selector, void *opt) {
return this->pReal->future(selector, opt);
}
AsioError __thiscall WrappedAsio::output_ready() {
return this->pReal->output_ready();
}
#pragma endregion
namespace hooks::audio::asio {
IUnknown *wrap(REFCLSID clsid, void *real) {
log_info("audio::wrappedasio", "wrapping ASIO driver interface, clsid={}", guid2s(clsid));
auto *wrapper = new WrappedAsio(
reinterpret_cast<IAsio *>(real), clsid, registered_asio_name(clsid));
// if the host already had a live wrapper for this CLSID it leaked the previous
// instance (ASIO is single-instance); tear it down now so the real driver is
// released before the reinit. works around games (iidx32+) that ref twice but
// deref once before re-initializing.
//
// FlexASIO 1.9 and many DAC ASIO drivers can't handle this; FlexASIO 1.10 and
// Xonar AE can
if (WrappedAsio *stale = register_wrapper(clsid, wrapper)) {
log_info(
"audio::wrappedasio",
"host did not release previous instance for clsid={}, forcing teardown",
guid2s(clsid));
// the stale instance may still have a running stream on the driver's realtime
// thread. stop and dispose it before deleting so no in-flight buffer switch
// (e.g. our volume trampoline) touches buffers we are about to free. ASIO
// guarantees no further buffer_switch once stop() returns, and dispose_buffers
// detaches our trampolines, fully quiescing the realtime path before delete
stale->stop();
stale->dispose_buffers();
delete stale;
}
return static_cast<IAsio *>(wrapper);
}
}
+160
View File
@@ -0,0 +1,160 @@
#pragma once
#include <atomic>
#include <memory>
#include <string>
#include <vector>
#include <windows.h>
#include "external/asio/asio.h"
#include "external/asio/iasiodrv.h"
namespace hooks::audio::asio {
// returns true if a CoCreateInstance call is instantiating a registered ASIO driver.
// ASIO hosts pass the driver CLSID as both class id and interface id; we also validate
// it against the system's registered ASIO drivers to avoid false positives
bool is_asio_creation(REFCLSID rclsid, REFIID riid);
// wrap a real ASIO driver instance, taking ownership of the supplied reference, and
// return a proxy that forwards every call to it
IUnknown *wrap(REFCLSID clsid, void *real);
}
// transparent proxy around a real ASIO driver; a single place to intercept ASIO traffic
struct WrappedAsio final : IAsio {
WrappedAsio(IAsio *real, REFCLSID clsid, std::string name)
: pReal(real), clsid(clsid), driver_name(std::move(name)) {
}
WrappedAsio(const WrappedAsio &) = delete;
WrappedAsio &operator=(const WrappedAsio &) = delete;
virtual ~WrappedAsio();
// when set, the proxy presents the game's expected multichannel layout to the host so
// it proceeds to create_buffers, then forwards only the device's real front pair and
// discards the rest (see create_buffers). set once at boot, before any wrapper exists,
// so it needs no synchronization
static bool FORCE_TWO_CHANNELS;
// some games hardcode a multichannel ASIO output and bail before create_buffers if
// get_channels reports fewer, so we report at least this many output channels when
// FORCE_TWO_CHANNELS is active
static constexpr long FORCED_OUTPUT_CHANNELS = 8;
#pragma region IUnknown
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) override;
ULONG STDMETHODCALLTYPE AddRef() override;
ULONG STDMETHODCALLTYPE Release() override;
#pragma endregion
#pragma region IAsio
AsioBool __thiscall init(void *sys_handle) override;
void __thiscall get_driver_name(char *name) override;
long __thiscall get_driver_version() override;
void __thiscall get_error_message(char *string) override;
AsioError __thiscall start() override;
AsioError __thiscall stop() override;
AsioError __thiscall get_channels(long *num_input_channels, long *num_output_channels) override;
AsioError __thiscall get_latencies(long *input_latency, long *output_latency) override;
AsioError __thiscall get_buffer_size(
long *min_size,
long *max_size,
long *preferred_size,
long *granularity) override;
AsioError __thiscall can_sample_rate(AsioSampleRate sample_rate) override;
AsioError __thiscall get_sample_rate(AsioSampleRate *sample_rate) override;
AsioError __thiscall set_sample_rate(AsioSampleRate sample_rate) override;
AsioError __thiscall get_clock_sources(ASIOClockSource *clocks, long *num_sources) override;
AsioError __thiscall set_clock_source(long reference) override;
AsioError __thiscall get_sample_position(ASIOSamples *s_pos, ASIOTimeStamp *t_stamp) override;
AsioError __thiscall get_channel_info(AsioChannelInfo *info) override;
AsioError __thiscall create_buffers(
AsioBufferInfo *buffer_infos,
long num_channels,
long buffer_size,
AsioCallbacks *callbacks) override;
AsioError __thiscall dispose_buffers() override;
AsioError __thiscall control_panel() override;
AsioError __thiscall future(long selector, void *opt) override;
AsioError __thiscall output_ready() override;
#pragma endregion
private:
// create_buffers implementation used when FORCE_TWO_CHANNELS is active: forwards only
// the channels the real device has and hands the game throwaway buffers for the rest
AsioError create_buffers_front_pair(
AsioBufferInfo *buffer_infos,
long num_channels,
long buffer_size,
AsioCallbacks *callbacks);
// if hooks::audio::VOLUME_BOOST is set, saves the game's callbacks and returns a proxy
// callback set (our buffer-switch trampolines) to hand the real driver instead, so we
// can scale its output buffers after the game fills them. otherwise returns the game's
// callbacks unchanged. called at create_buffers time, before the stream starts
AsioCallbacks *install_volume_callbacks(AsioCallbacks *game_callbacks);
// records a device output channel whose buffers we scale by the volume boost. queries
// the real driver for the channel's sample format. called at create_buffers time
void record_volume_output_channel(const AsioBufferInfo &info);
// publishes the captured volume state to the realtime thread once the buffers exist,
// making our trampolines start scaling. called at the end of either create_buffers path
void publish_volume(long buffer_size);
// detaches this instance from the realtime trampolines so they stop touching its
// buffers. called from dispose_buffers and the destructor
void detach_volume();
// multiplies every recorded output channel's buffer for the given double-buffer index
// by the volume boost. runs on the driver's realtime thread from our buffer switch
void apply_output_volume(long double_buffer_index);
// realtime-thread trampolines for the buffer-switch callbacks, handed to the real
// driver in place of the game's; ASIO callbacks carry no user data, so they reach the
// active wrapper through volume_active_instance, call the game's original, then scale.
// the other two callbacks (sample_rate_did_change, asio_message) are forwarded as the
// game's own pointers, so they need no trampoline
static void __cdecl volume_buffer_switch(long double_buffer_index, AsioBool direct_process);
static AsioTime * __cdecl volume_buffer_switch_time_info(
AsioTime *params, long double_buffer_index, AsioBool direct_process);
// the single wrapper whose proxy callbacks are installed (ASIO is single-instance with
// one running stream); read by the static trampolines to reach the right wrapper
static std::atomic<WrappedAsio *> volume_active_instance;
IAsio *const pReal;
const CLSID clsid;
// registry name of the driver (not get_driver_name), used in our logs as a single
// unambiguous name; constant for our lifetime
std::string driver_name;
// our own reference count; we hold one reference on pReal and release it when this
// drops to zero
std::atomic<ULONG> ref_count {1};
// throwaway double buffers handed to the channels we discard when FORCE_TWO_CHANNELS
// is active (see create_buffers). owned for the lifetime of the buffer set and freed
// in dispose_buffers; only read by the game from its own bufferSwitch, never by us
std::vector<std::unique_ptr<uint8_t[]>> dummy_buffers;
// one device output channel scaled by the volume boost in our buffer switch
struct VolumeOutputChannel {
void *buffers[2];
AsioSampleType type;
};
// volume boost state, captured at create_buffers time and published to the realtime
// thread via volume_active_instance once fully built; untouched while the stream runs.
// volume_active gates whether we install our proxy callbacks at all
bool volume_active = false;
float volume_gain = 1.0f;
long volume_buffer_size = 0;
AsioCallbacks volume_game_callbacks {};
AsioCallbacks volume_proxy_callbacks {};
std::vector<VolumeOutputChannel> volume_channels;
};
+10
View File
@@ -14,6 +14,7 @@
#include "audio_private.h" #include "audio_private.h"
#include "acm.h" #include "acm.h"
#include "asio_proxy.h"
#ifdef _MSC_VER #ifdef _MSC_VER
DEFINE_GUID(CLSID_MMDeviceEnumerator, DEFINE_GUID(CLSID_MMDeviceEnumerator,
@@ -39,9 +40,14 @@ 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;
std::optional<uint32_t> RESAMPLE_RATE = std::nullopt;
std::optional<uint32_t> EXCLUSIVE_BUFFER_MS = std::nullopt;
bool USE_DUMMY = false; bool USE_DUMMY = false;
WAVEFORMATEXTENSIBLE FORMAT {}; WAVEFORMATEXTENSIBLE FORMAT {};
std::optional<Backend> BACKEND = std::nullopt; std::optional<Backend> BACKEND = std::nullopt;
bool INJECT_FAKE_REALTEK_AUDIO = false;
std::optional<size_t> ASIO_DRIVER_ID = std::nullopt; std::optional<size_t> ASIO_DRIVER_ID = std::nullopt;
std::string ASIO_DRIVER_NAME = ""; std::string ASIO_DRIVER_NAME = "";
bool ASIO_FORCE_UNLOAD_ON_STOP = false; bool ASIO_FORCE_UNLOAD_ON_STOP = false;
@@ -91,6 +97,10 @@ static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
// wrap object // wrap object
auto mmde = reinterpret_cast<IMMDeviceEnumerator **>(ppv); auto mmde = reinterpret_cast<IMMDeviceEnumerator **>(ppv);
*mmde = new WrappedIMMDeviceEnumerator(*mmde); *mmde = new WrappedIMMDeviceEnumerator(*mmde);
} else if (ppv != nullptr && *ppv != nullptr && hooks::audio::asio::is_asio_creation(rclsid, riid)) {
// wrap every ASIO driver so calls pass through to the real driver
*ppv = hooks::audio::asio::wrap(rclsid, *ppv);
} }
// return original result // return original result
+24
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <cstdint>
#include <optional> #include <optional>
#include <string> #include <string>
@@ -16,11 +17,34 @@ 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;
// target sample rate the hooked output is resampled to, if set
extern std::optional<uint32_t> RESAMPLE_RATE;
// minimum WASAPI exclusive buffer duration (milliseconds), if set. enlarges the device buffer
// to avoid underrun crackle on endpoints that cannot service a tiny buffer in time.
extern std::optional<uint32_t> EXCLUSIVE_BUFFER_MS;
extern bool USE_DUMMY; extern bool USE_DUMMY;
extern WAVEFORMATEXTENSIBLE FORMAT; extern WAVEFORMATEXTENSIBLE FORMAT;
extern std::optional<Backend> BACKEND; extern std::optional<Backend> BACKEND;
// when true, a synthetic "Realtek" render endpoint is injected into device enumeration that
// discards all audio. used by gitadora arena, whose device search crashes when no render
// endpoint reports a "Realtek" friendly name.
extern bool INJECT_FAKE_REALTEK_AUDIO;
extern std::optional<size_t> ASIO_DRIVER_ID; extern std::optional<size_t> ASIO_DRIVER_ID;
extern std::string ASIO_DRIVER_NAME; extern std::string ASIO_DRIVER_NAME;
extern bool ASIO_FORCE_UNLOAD_ON_STOP; extern bool ASIO_FORCE_UNLOAD_ON_STOP;
@@ -1,5 +1,6 @@
#include "device_collection.h" #include "device_collection.h"
#include "device.h" #include "device.h"
#include "null_device.h"
#include "util/utils.h" #include "util/utils.h"
#include "util/logging.h" #include "util/logging.h"
@@ -28,17 +29,48 @@ ULONG STDMETHODCALLTYPE WrappedIMMDeviceCollection::Release() {
return refs; return refs;
} }
bool WrappedIMMDeviceCollection::should_inject_fake_realtek() const {
return null_render_device_enabled()
&& (data_flow == eRender || data_flow == eAll);
}
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceCollection::GetCount(UINT *pcDevices) { HRESULT STDMETHODCALLTYPE WrappedIMMDeviceCollection::GetCount(UINT *pcDevices) {
// when active, hide all real devices and present only the synthetic one
if (should_inject_fake_realtek()) {
if (pcDevices == nullptr) {
return E_POINTER;
}
*pcDevices = 1;
return S_OK;
}
return pReal->GetCount(pcDevices); return pReal->GetCount(pcDevices);
} }
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceCollection::Item(UINT nDevice, IMMDevice **ppDevice) { HRESULT STDMETHODCALLTYPE WrappedIMMDeviceCollection::Item(UINT nDevice, IMMDevice **ppDevice) {
if (ppDevice == nullptr) {
return E_POINTER;
}
// when active, the only device in the collection is the synthetic fake Realtek
// render device; all real devices are hidden
if (should_inject_fake_realtek()) {
if (nDevice != 0) {
return E_INVALIDARG;
}
log_info("audio", "WrappedIMMDeviceCollection::Item[{}] -> synthetic fake Realtek render device", nDevice);
*ppDevice = new NullMMDevice();
return S_OK;
}
log_info("audio", "WrappedIMMDeviceCollection::Item[{}]", nDevice); log_info("audio", "WrappedIMMDeviceCollection::Item[{}]", nDevice);
// call original // call original
const auto hr = pReal->Item(nDevice, ppDevice); const auto hr = pReal->Item(nDevice, ppDevice);
// wrap interface // wrap interface
if (SUCCEEDED(hr) && *ppDevice != nullptr) {
*ppDevice = new WrappedIMMDevice(*ppDevice); *ppDevice = new WrappedIMMDevice(*ppDevice);
}
return hr; return hr;
} }
@@ -4,7 +4,8 @@
#include <mmdeviceapi.h> #include <mmdeviceapi.h>
struct WrappedIMMDeviceCollection : IMMDeviceCollection { struct WrappedIMMDeviceCollection : IMMDeviceCollection {
explicit WrappedIMMDeviceCollection(IMMDeviceCollection *orig) : pReal(orig) { WrappedIMMDeviceCollection(IMMDeviceCollection *orig, EDataFlow dataFlow)
: pReal(orig), data_flow(dataFlow) {
} }
WrappedIMMDeviceCollection(const WrappedIMMDeviceCollection &) = delete; WrappedIMMDeviceCollection(const WrappedIMMDeviceCollection &) = delete;
@@ -24,5 +25,9 @@ struct WrappedIMMDeviceCollection : IMMDeviceCollection {
#pragma endregion #pragma endregion
private: private:
// whether the synthetic fake Realtek render device should be appended to this collection
bool should_inject_fake_realtek() const;
IMMDeviceCollection *const pReal; IMMDeviceCollection *const pReal;
const EDataFlow data_flow;
}; };
@@ -45,7 +45,7 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDeviceEnumerator::EnumAudioEndpoints(
{ {
const auto hr = pReal->EnumAudioEndpoints(dataFlow, dwStateMask, ppDevices); const auto hr = pReal->EnumAudioEndpoints(dataFlow, dwStateMask, ppDevices);
if (SUCCEEDED(hr) && (ppDevices != nullptr) && (*ppDevices != nullptr)) { if (SUCCEEDED(hr) && (ppDevices != nullptr) && (*ppDevices != nullptr)) {
*ppDevices = new WrappedIMMDeviceCollection(*ppDevices); *ppDevices = new WrappedIMMDeviceCollection(*ppDevices, dataFlow);
} }
return hr; return hr;
} }
@@ -0,0 +1,195 @@
#include "null_device.h"
#include <atomic>
#include <cstring>
#include <audioclient.h>
#include "hooks/audio/audio.h"
#include "hooks/audio/audio_private.h"
#include "hooks/audio/backends/wasapi/dummy_audio_client.h"
#include "util/logging.h"
#include "util/utils.h"
#include "null_discard_backend.h"
// friendly name reported by the synthetic device. must contain "Realtek" so the
// gitadora arena device search matches it.
static const wchar_t NULL_DEVICE_FRIENDLY_NAME[] = L"Realtek High Definition Audio";
// arbitrary identifier reported by the synthetic device.
static const wchar_t NULL_DEVICE_ID[] = L"{spice2x-null-render-device}";
// PKEY_Device_FriendlyName, hardcoded to avoid pulling in functiondiscoverykeys_devpkey.h
static const PROPERTYKEY PKEY_DEVICE_FRIENDLY_NAME_LOCAL = {
{ 0xa45c254e, 0xdf1c, 0x4efd, { 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0 } },
14
};
bool null_render_device_enabled() {
return hooks::audio::INJECT_FAKE_REALTEK_AUDIO;
}
// duplicate a wide string into CoTaskMem so the caller can free it with
// CoTaskMemFree / PropVariantClear as the COM API contract requires.
static LPWSTR co_task_wcsdup(const wchar_t *src) {
const size_t bytes = (wcslen(src) + 1) * sizeof(wchar_t);
auto *dst = static_cast<LPWSTR>(CoTaskMemAlloc(bytes));
if (dst != nullptr) {
memcpy(dst, src, bytes);
}
return dst;
}
namespace {
// minimal IPropertyStore that only answers PKEY_Device_FriendlyName.
struct NullPropertyStore : IPropertyStore {
std::atomic<ULONG> ref_cnt = 1;
virtual ~NullPropertyStore() = default;
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override {
if (ppvObj == nullptr) {
return E_POINTER;
}
if (riid == __uuidof(IUnknown) || riid == __uuidof(IPropertyStore)) {
this->AddRef();
*ppvObj = this;
return S_OK;
}
*ppvObj = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override {
return ++this->ref_cnt;
}
ULONG STDMETHODCALLTYPE Release() override {
const ULONG refs = --this->ref_cnt;
if (refs == 0) {
delete this;
}
return refs;
}
HRESULT STDMETHODCALLTYPE GetCount(DWORD *cProps) override {
if (cProps == nullptr) {
return E_POINTER;
}
*cProps = 1;
return S_OK;
}
HRESULT STDMETHODCALLTYPE GetAt(DWORD iProp, PROPERTYKEY *pkey) override {
if (pkey == nullptr) {
return E_POINTER;
}
if (iProp != 0) {
return E_INVALIDARG;
}
*pkey = PKEY_DEVICE_FRIENDLY_NAME_LOCAL;
return S_OK;
}
HRESULT STDMETHODCALLTYPE GetValue(REFPROPERTYKEY key, PROPVARIANT *pv) override {
if (pv == nullptr) {
return E_POINTER;
}
PropVariantInit(pv);
if (key.fmtid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.fmtid
&& key.pid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.pid) {
pv->pwszVal = co_task_wcsdup(NULL_DEVICE_FRIENDLY_NAME);
if (pv->pwszVal == nullptr) {
return E_OUTOFMEMORY;
}
pv->vt = VT_LPWSTR;
}
// unknown keys are returned as VT_EMPTY / S_OK
return S_OK;
}
HRESULT STDMETHODCALLTYPE SetValue(REFPROPERTYKEY, REFPROPVARIANT) override {
return STG_E_ACCESSDENIED;
}
HRESULT STDMETHODCALLTYPE Commit() override {
return S_OK;
}
};
}
#pragma region IUnknown
HRESULT STDMETHODCALLTYPE NullMMDevice::QueryInterface(REFIID riid, void **ppvObj) {
if (ppvObj == nullptr) {
return E_POINTER;
}
if (riid == __uuidof(IUnknown) || riid == __uuidof(IMMDevice)) {
this->AddRef();
*ppvObj = this;
return S_OK;
}
*ppvObj = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE NullMMDevice::AddRef() {
return ++this->ref_cnt;
}
ULONG STDMETHODCALLTYPE NullMMDevice::Release() {
const ULONG refs = --this->ref_cnt;
if (refs == 0) {
delete this;
}
return refs;
}
#pragma endregion
#pragma region IMMDevice
HRESULT STDMETHODCALLTYPE NullMMDevice::Activate(
REFIID iid,
DWORD,
PROPVARIANT *,
void **ppInterface)
{
if (ppInterface == nullptr) {
return E_POINTER;
}
*ppInterface = nullptr;
log_info("audio::null", "NullMMDevice::Activate {}", guid2s(iid));
if (iid == IID_IAudioClient) {
// release any previously persisted client
if (hooks::audio::CLIENT != nullptr) {
hooks::audio::CLIENT->Release();
}
auto *client = static_cast<IAudioClient *>(new DummyIAudioClient(new NullDiscardBackend()));
*ppInterface = client;
// persist the audio client
hooks::audio::CLIENT = client;
hooks::audio::CLIENT->AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
HRESULT STDMETHODCALLTYPE NullMMDevice::OpenPropertyStore(DWORD, IPropertyStore **ppProperties) {
if (ppProperties == nullptr) {
return E_POINTER;
}
*ppProperties = new NullPropertyStore();
return S_OK;
}
HRESULT STDMETHODCALLTYPE NullMMDevice::GetId(LPWSTR *ppstrId) {
if (ppstrId == nullptr) {
return E_POINTER;
}
*ppstrId = co_task_wcsdup(NULL_DEVICE_ID);
return *ppstrId != nullptr ? S_OK : E_OUTOFMEMORY;
}
HRESULT STDMETHODCALLTYPE NullMMDevice::GetState(DWORD *pdwState) {
if (pdwState == nullptr) {
return E_POINTER;
}
*pdwState = DEVICE_STATE_ACTIVE;
return S_OK;
}
#pragma endregion
@@ -0,0 +1,39 @@
#pragma once
#include <atomic>
#include <mmdeviceapi.h>
// returns true when a synthetic render endpoint should be injected into device
// enumeration. games like gitadora arena search the render endpoint list for a
// device whose friendly name contains "Realtek" and crash with a null pointer
// dereference when no match exists. presenting a fake match that routes to the
// null audio backend lets the search succeed while discarding the audio.
bool null_render_device_enabled();
// fake IMMDevice that reports a "Realtek" friendly name and activates straight
// into the null audio backend, never touching real hardware.
struct NullMMDevice : IMMDevice {
NullMMDevice() = default;
NullMMDevice(const NullMMDevice &) = delete;
NullMMDevice &operator=(const NullMMDevice &) = delete;
virtual ~NullMMDevice() = default;
#pragma region IUnknown
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
ULONG STDMETHODCALLTYPE AddRef() override;
ULONG STDMETHODCALLTYPE Release() override;
#pragma endregion
#pragma region IMMDevice
HRESULT STDMETHODCALLTYPE Activate(REFIID iid, DWORD dwClsCtx, PROPVARIANT *pActivationParams, void **ppInterface) override;
HRESULT STDMETHODCALLTYPE OpenPropertyStore(DWORD stgmAccess, IPropertyStore **ppProperties) override;
HRESULT STDMETHODCALLTYPE GetId(LPWSTR *ppstrId) override;
HRESULT STDMETHODCALLTYPE GetState(DWORD *pdwState) override;
#pragma endregion
private:
std::atomic<ULONG> ref_cnt = 1;
};
@@ -0,0 +1,141 @@
#include "null_discard_backend.h"
#include <algorithm>
#include <chrono>
#include "hooks/audio/util.h"
#include "util/logging.h"
#include "util/precise_timer.h"
NullDiscardBackend::~NullDiscardBackend() {
this->running = false;
if (this->pacing_thread.joinable()) {
this->pacing_thread.join();
}
}
const WAVEFORMATEXTENSIBLE &NullDiscardBackend::format() const noexcept {
return this->format_;
}
HRESULT NullDiscardBackend::on_initialize(
AUDCLNT_SHAREMODE *,
DWORD *,
REFERENCE_TIME *hnsBufferDuration,
REFERENCE_TIME *,
const WAVEFORMATEX *pFormat,
LPCGUID)
{
copy_wave_format(&this->format_, pFormat);
// honor the game's requested buffer duration, falling back to 10 ms
constexpr REFERENCE_TIME DEFAULT_REFTIME = 100000; // 10 ms in 100-ns units
this->period_reftime = (hnsBufferDuration && *hnsBufferDuration > 0)
? *hnsBufferDuration
: DEFAULT_REFTIME;
this->buffer_frames = std::max<uint32_t>(1, static_cast<uint32_t>(
static_cast<double>(this->format_.Format.nSamplesPerSec)
* this->period_reftime / 10000000.0 + 0.5));
log_info("audio::null", "initializing null render device with {} channels, {} Hz, {}-bit",
this->format_.Format.nChannels,
this->format_.Format.nSamplesPerSec,
this->format_.Format.wBitsPerSample);
return S_OK;
}
HRESULT NullDiscardBackend::on_get_buffer_size(uint32_t *buffer_frames) {
*buffer_frames = this->buffer_frames;
return S_OK;
}
HRESULT NullDiscardBackend::on_get_stream_latency(REFERENCE_TIME *latency) {
*latency = this->period_reftime;
return S_OK;
}
HRESULT NullDiscardBackend::on_get_current_padding(std::optional<uint32_t> &padding_frames) {
// discarded immediately, so the buffer always reads as fully drained
padding_frames = 0;
return S_OK;
}
HRESULT NullDiscardBackend::on_is_format_supported(
AUDCLNT_SHAREMODE *,
const WAVEFORMATEX *,
WAVEFORMATEX **ppClosestMatch)
{
if (ppClosestMatch) {
*ppClosestMatch = nullptr;
}
return S_OK;
}
HRESULT NullDiscardBackend::on_get_mix_format(WAVEFORMATEX **) {
return E_NOTIMPL;
}
HRESULT NullDiscardBackend::on_get_device_period(
REFERENCE_TIME *default_device_period,
REFERENCE_TIME *minimum_device_period)
{
if (default_device_period) {
*default_device_period = this->period_reftime;
}
if (minimum_device_period) {
*minimum_device_period = this->period_reftime;
}
return S_OK;
}
HRESULT NullDiscardBackend::on_start() {
if (!this->running.exchange(true)) {
this->pacing_thread = std::thread(&NullDiscardBackend::pace_loop, this);
}
return S_OK;
}
HRESULT NullDiscardBackend::on_stop() {
return S_OK;
}
HRESULT NullDiscardBackend::on_set_event_handle(HANDLE *event_handle) {
// keep the game's event so pace_loop() can wake it; there is no real device behind it
this->relay_handle = *event_handle;
return S_OK;
}
HRESULT NullDiscardBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) {
const size_t buffer_size =
static_cast<size_t>(this->format_.Format.nBlockAlign) * num_frames_requested;
if (this->scratch.size() < buffer_size) {
this->scratch.resize(buffer_size);
}
*ppData = this->scratch.data();
return S_OK;
}
HRESULT NullDiscardBackend::on_release_buffer(uint32_t, DWORD) {
// discard the audio entirely
return S_OK;
}
void NullDiscardBackend::pace_loop() {
using namespace std::chrono;
timeutils::PreciseSleepTimer timer;
// audio is discarded, so timing precision and drift do not matter; just wake the
// game once per buffer period to keep its render thread from blocking on the event.
const auto period = duration_cast<steady_clock::duration>(
duration<double>(this->period_reftime / 10000000.0));
while (this->running.load()) {
if (this->relay_handle) {
SetEvent(this->relay_handle);
}
timer.sleep(period);
}
}
@@ -0,0 +1,54 @@
#pragma once
#include <atomic>
#include <optional>
#include <thread>
#include <vector>
#include <audioclient.h>
#include "hooks/audio/implementations/backend.h"
// discards all audio while pacing the game's event handle once per buffer period, so the game
// keeps running normally with nothing output to any real device. routed through the shared
// DummyIAudioClient, the same plumbing the asio backend uses.
struct NullDiscardBackend final : AudioBackend {
~NullDiscardBackend() final;
const WAVEFORMATEXTENSIBLE &format() const noexcept override;
HRESULT on_initialize(
AUDCLNT_SHAREMODE *,
DWORD *,
REFERENCE_TIME *hnsBufferDuration,
REFERENCE_TIME *,
const WAVEFORMATEX *pFormat,
LPCGUID) override;
HRESULT on_get_buffer_size(uint32_t *buffer_frames) override;
HRESULT on_get_stream_latency(REFERENCE_TIME *latency) override;
HRESULT on_get_current_padding(std::optional<uint32_t> &padding_frames) override;
HRESULT on_is_format_supported(
AUDCLNT_SHAREMODE *,
const WAVEFORMATEX *,
WAVEFORMATEX **ppClosestMatch) override;
HRESULT on_get_mix_format(WAVEFORMATEX **) override;
HRESULT on_get_device_period(
REFERENCE_TIME *default_device_period,
REFERENCE_TIME *minimum_device_period) override;
HRESULT on_start() override;
HRESULT on_stop() override;
HRESULT on_set_event_handle(HANDLE *event_handle) override;
HRESULT on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) override;
HRESULT on_release_buffer(uint32_t, DWORD) override;
private:
void pace_loop();
WAVEFORMATEXTENSIBLE format_ {};
uint32_t buffer_frames = 0;
REFERENCE_TIME period_reftime = 0;
HANDLE relay_handle = nullptr;
std::vector<BYTE> scratch;
std::thread pacing_thread;
std::atomic<bool> running = false;
};
@@ -4,6 +4,7 @@
#include <ksmedia.h> #include <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,36 @@ 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));
}
// when resampling, open the real device at the target rate while the game keeps writing its
// native-rate audio into the scratch buffer. this runs on whatever device_format is now: the
// game's native format, or the stereo format produced above when downmix is also active, so
// the two stages chain as multi-channel -> stereo -> resampled stereo.
WAVEFORMATEXTENSIBLE resample_storage = {};
if (auto target_rate = hooks::audio::Resampler::resolve(device_format)) {
const uint32_t src_rate = device_format->nSamplesPerSec;
this->resample.setup(device_format, &resample_storage, *target_rate);
device_format = reinterpret_cast<const WAVEFORMATEX *>(&resample_storage);
log_info("audio::wasapi", "resample enabled: {} Hz -> {} Hz{}",
src_rate, *target_rate, this->downmix.enabled ? " (after downmix)" : "");
}
// verbose output // 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)); print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, device_format);
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(StreamFlags));
log_info("audio::wasapi", "... hnsBufferDuration : {}", hnsBufferDuration);
log_info("audio::wasapi", "... hnsPeriodicity : {}", hnsPeriodicity);
print_format(pFormat);
if (this->backend) { if (this->backend) {
SAFE_CALL("AudioBackend", "on_initialize", this->backend->on_initialize( SAFE_CALL("AudioBackend", "on_initialize", this->backend->on_initialize(
@@ -163,27 +208,67 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
AudioSessionGuid)); AudioSessionGuid));
log_info("audio::wasapi", "AudioBackend::on_initialize call finished"); log_info("audio::wasapi", "AudioBackend::on_initialize call finished");
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(ShareMode)); print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, pFormat);
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(StreamFlags));
log_info("audio::wasapi", "... hnsBufferDuration : {}", hnsBufferDuration);
log_info("audio::wasapi", "... hnsPeriodicity : {}", hnsPeriodicity);
print_format(pFormat);
} }
// 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);
// optionally enlarge the exclusive buffer. games request a very small buffer (e.g. 3 ms)
// which some endpoints (notably NVIDIA HDMI/DP display audio) cannot service in time,
// underrunning mid-period and crackling. a larger buffer gives the device slack. exclusive
// mode requires periodicity == buffer_duration, so raise both together; the initialize
// paths below handle any required buffer-size realignment.
if (hooks::audio::EXCLUSIVE_BUFFER_MS.has_value()) {
const REFERENCE_TIME min_duration =
(REFERENCE_TIME) hooks::audio::EXCLUSIVE_BUFFER_MS.value() * 10000;
if (hnsBufferDuration < min_duration) {
log_info("audio::wasapi",
"raising exclusive buffer from {} hns to {} hns ({} ms)",
hnsBufferDuration, min_duration, hooks::audio::EXCLUSIVE_BUFFER_MS.value());
hnsBufferDuration = min_duration;
if (hnsPeriodicity != 0) {
hnsPeriodicity = min_duration;
}
}
}
} }
// call next // call next. the resampler owns the device interaction whenever it is active (including when
HRESULT ret = pReal->Initialize( // chained after the downmix), otherwise the downmix does, otherwise the device is opened
// directly.
HRESULT ret;
if (this->resample.enabled) {
ret = this->resample.initialize(
pReal,
ShareMode, ShareMode,
StreamFlags, StreamFlags,
hnsBufferDuration, hnsBufferDuration,
hnsPeriodicity, hnsPeriodicity,
pFormat, device_format,
AudioSessionGuid); AudioSessionGuid);
} else if (this->downmix.enabled) {
ret = this->downmix.initialize(
pReal,
ShareMode,
StreamFlags,
hnsBufferDuration,
hnsPeriodicity,
device_format,
AudioSessionGuid);
} else {
ret = initialize_with_alignment_retry(
pReal,
"audio::wasapi",
ShareMode,
StreamFlags,
hnsBufferDuration,
hnsPeriodicity,
device_format,
AudioSessionGuid);
}
// check for failure // check for failure
if (FAILED(ret)) { if (FAILED(ret)) {
@@ -192,7 +277,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;
} }
@@ -214,7 +300,15 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetBufferSize(UINT32 *pNumBufferF
} }
} }
CHECK_RESULT(pReal->GetBufferSize(pNumBufferFrames)); HRESULT ret = pReal->GetBufferSize(pNumBufferFrames);
// report the buffer size at the game's native rate; the real device buffer is at the
// resampled rate, so translate it back so the game paces its writes correctly.
if (SUCCEEDED(ret) && this->resample.enabled && pNumBufferFrames) {
*pNumBufferFrames = this->resample.frames_device_to_game(*pNumBufferFrames);
}
CHECK_RESULT(ret);
} }
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetStreamLatency(REFERENCE_TIME *phnsLatency) { HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetStreamLatency(REFERENCE_TIME *phnsLatency) {
static std::once_flag printed; static std::once_flag printed;
@@ -256,7 +350,15 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetCurrentPadding(UINT32 *pNumPad
} }
} }
CHECK_RESULT(pReal->GetCurrentPadding(pNumPaddingFrames)); HRESULT ret = pReal->GetCurrentPadding(pNumPaddingFrames);
// the device buffer is at the resampled rate; report padding at the game's native rate so the
// game's free-space calculation stays paced correctly.
if (SUCCEEDED(ret) && this->resample.enabled && pNumPaddingFrames) {
*pNumPaddingFrames = this->resample.padding_device_to_game(*pNumPaddingFrames);
}
CHECK_RESULT(ret);
} }
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::IsFormatSupported( HRESULT STDMETHODCALLTYPE WrappedIAudioClient::IsFormatSupported(
AUDCLNT_SHAREMODE ShareMode, AUDCLNT_SHAREMODE ShareMode,
@@ -274,6 +376,44 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::IsFormatSupported(
fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat)); fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat));
} }
// log the format the game is asking about
log_info("audio::wasapi", "IAudioClient::IsFormatSupported hook hit");
print_format(ShareMode, pFormat);
// when downmixing, the real device is opened as stereo, so check whether the equivalent
// stereo format is supported instead of the multi-channel one. when resampling is also active
// it chains onto that stereo format, so check the resampled stereo format.
if (resolve_downmix(pFormat)) {
WAVEFORMATEXTENSIBLE stereo_storage = {};
hooks::audio::Downmix::make_stereo_format(pFormat, &stereo_storage);
const WAVEFORMATEX *check_format = reinterpret_cast<const WAVEFORMATEX *>(&stereo_storage);
WAVEFORMATEXTENSIBLE resample_storage = {};
if (auto target_rate = hooks::audio::Resampler::resolve(check_format)) {
hooks::audio::Resampler::make_device_format(check_format, &resample_storage, *target_rate);
check_format = reinterpret_cast<const WAVEFORMATEX *>(&resample_storage);
}
log_info("audio::wasapi", "... checking device format instead (after downmix/resample):");
print_format(check_format);
CHECK_RESULT(pReal->IsFormatSupported(ShareMode, check_format, ppClosestMatch));
} else if (games::gitadora::is_arena_model()) {
games::gitadora::fix_audio_channel_mask(const_cast<WAVEFORMATEX *>(pFormat));
} else if (auto target_rate = hooks::audio::Resampler::resolve(pFormat)) {
// when resampling, the real device is opened at the target rate, so check whether the
// equivalent format at that rate is supported instead of the game's native rate.
WAVEFORMATEXTENSIBLE resample_storage = {};
hooks::audio::Resampler::make_device_format(pFormat, &resample_storage, *target_rate);
const auto resample_format = reinterpret_cast<const WAVEFORMATEX *>(&resample_storage);
log_info("audio::wasapi", "... checking device format instead (after resample):");
print_format(resample_format);
CHECK_RESULT(pReal->IsFormatSupported(ShareMode, resample_format, ppClosestMatch));
}
if (this->backend) { 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 +606,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,8 +8,10 @@
#include "hooks/audio/audio_private.h" #include "hooks/audio/audio_private.h"
#include "util/logging.h" #include "util/logging.h"
#include "audio_render_client.h" #include "downmix.h"
#include "resample.h"
#include "audio_render_client.h"
// {1FBC8530-AF3E-4128-B418-115DE72F76B6} // {1FBC8530-AF3E-4128-B418-115DE72F76B6}
static const GUID IID_WrappedIAudioClient = { static const GUID IID_WrappedIAudioClient = {
0x1fbc8530, 0xaf3e, 0x4128, { 0xb4, 0x18, 0x11, 0x5d, 0xe7, 0x2f, 0x76, 0xb6 } 0x1fbc8530, 0xaf3e, 0x4128, { 0xb4, 0x18, 0x11, 0x5d, 0xe7, 0x2f, 0x76, 0xb6 }
@@ -93,4 +95,17 @@ 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;
// native-rate -> target-rate sample-rate conversion. the real device is opened at the target
// rate while the game keeps writing its native-rate audio into a scratch buffer that we
// resample in the render client.
hooks::audio::Resampler resample;
}; };
@@ -1,10 +1,71 @@
#include "audio_render_client.h" #include "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 "util.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;
bool is_float = is_ieee_float(&f);
if (is_float && f.wBitsPerSample == 32) {
auto p = reinterpret_cast<float *>(buffer);
for (size_t i = 0; i < samples; i++) {
p[i] = std::clamp(p[i] * gain, -1.0f, 1.0f);
}
return;
}
switch (f.wBitsPerSample) {
case 16: {
auto p = reinterpret_cast<int16_t *>(buffer);
for (size_t i = 0; i < samples; i++) {
p[i] = (int16_t) std::clamp((int) std::lround(p[i] * gain), -32768, 32767);
}
break;
}
case 24: {
// packed 24-bit little-endian
for (size_t i = 0; i < samples; i++) {
BYTE *s = buffer + i * 3;
int32_t v = s[0] | (s[1] << 8) | (s[2] << 16);
if (v & 0x800000) {
v |= ~0xFFFFFF; // sign extend
}
int64_t scaled = std::clamp<int64_t>(
std::llround((double) v * gain), -8388608, 8388607);
s[0] = scaled & 0xFF;
s[1] = (scaled >> 8) & 0xFF;
s[2] = (scaled >> 16) & 0xFF;
}
break;
}
case 32: {
auto p = reinterpret_cast<int32_t *>(buffer);
for (size_t i = 0; i < samples; i++) {
p[i] = (int32_t) std::clamp(
std::llround((double) p[i] * gain),
(long long) INT32_MIN, (long long) INT32_MAX);
}
break;
}
default:
break;
}
}
HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::QueryInterface(REFIID riid, void **ppvObj) { HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::QueryInterface(REFIID riid, void **ppvObj) {
if (ppvObj == nullptr) { if (ppvObj == nullptr) {
return E_POINTER; return E_POINTER;
@@ -51,6 +112,25 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::GetBuffer(UINT32 NumFramesR
return S_OK; return S_OK;
} }
// downmix + resample chained: the game writes its multi-channel native-rate audio into the
// downmix scratch, which is downmixed to stereo and then resampled on release. size the
// resampler's (stereo) input scratch now and hand the game the multi-channel downmix scratch.
if (this->client->downmix.enabled && this->client->resample.enabled) {
BYTE *resample_scratch = nullptr;
this->client->resample.get_buffer(NumFramesRequested, &resample_scratch);
CHECK_RESULT(this->client->downmix.get_scratch(NumFramesRequested, ppData));
// surround downmix: reserve the real (stereo) device buffer, but hand the game a
// multi-channel scratch buffer that we downmix on release
} else if (this->client->downmix.enabled) {
CHECK_RESULT(this->client->downmix.get_buffer(pReal, NumFramesRequested, ppData));
// resample: hand the game a native-rate scratch buffer that we convert on release. the real
// device buffer is acquired in ReleaseBuffer once the converted frame count is known.
} else if (this->client->resample.enabled) {
CHECK_RESULT(this->client->resample.get_buffer(NumFramesRequested, ppData));
}
// call original // call original
HRESULT ret = pReal->GetBuffer(NumFramesRequested, ppData); HRESULT ret = pReal->GetBuffer(NumFramesRequested, ppData);
@@ -75,14 +155,72 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::ReleaseBuffer(UINT32 NumFra
return S_OK; return S_OK;
} }
// fix for audio pop effect // downmix + resample chained: downmix the game's multi-channel scratch into the resampler's
if (this->buffers_to_mute > 0 && this->client->frame_size > 0) { // stereo input scratch, then let the resampler convert and push it to the device. a silent
// buffer skips the downmix and feeds silence straight through.
// zero out = mute if (this->client->downmix.enabled && this->client->resample.enabled) {
memset(this->audio_buffer, 0, NumFramesWritten * this->client->frame_size); if ((dwFlags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
this->client->downmix.downmix_into(
this->buffers_to_mute--; this->client->resample.input_data(), NumFramesWritten);
}
return this->client->resample.flush(
pReal,
this->client->pReal,
NumFramesWritten,
dwFlags,
hooks::audio::VOLUME_BOOST);
} }
CHECK_RESULT(pReal->ReleaseBuffer(NumFramesWritten, dwFlags)); // resample: convert the game's native-rate scratch and push as many output frames as the
// device has room for, applying the volume boost to the converted output. handles acquiring
// and releasing the real device buffer itself.
if (this->client->resample.enabled) {
return this->client->resample.flush(
pReal,
this->client->pReal,
NumFramesWritten,
dwFlags,
hooks::audio::VOLUME_BOOST);
}
// resolve the real device buffer for whichever path produced the audio
BYTE *device_buffer;
if (this->client->downmix.enabled) {
// downmix the game's multi-channel scratch into the real stereo buffer held since GetBuffer
this->client->downmix.write_device_buffer(NumFramesWritten, dwFlags);
device_buffer = this->client->downmix.current_buffer();
} else {
device_buffer = this->audio_buffer;
// mute the first few buffers to avoid a startup pop
if (this->buffers_to_mute > 0 && this->client->frame_size > 0) {
memset(this->audio_buffer, 0, NumFramesWritten * this->client->frame_size);
this->buffers_to_mute--;
}
}
// boost the final output volume just before it reaches the device, layout-agnostic
if (hooks::audio::VOLUME_BOOST != 1.0f
&& device_buffer != nullptr
&& (dwFlags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
static std::once_flag boost_printed;
std::call_once(boost_printed, []() {
log_info("audio::wasapi", "volume boost active: gain={}", hooks::audio::VOLUME_BOOST);
});
apply_gain(device_buffer, NumFramesWritten, this->client->device_format,
hooks::audio::VOLUME_BOOST);
}
HRESULT ret = pReal->ReleaseBuffer(NumFramesWritten, dwFlags);
if (this->client->downmix.enabled) {
this->client->downmix.buffer_released();
}
if (FAILED(ret)) {
PRINT_FAILED_RESULT(CLASS_NAME, __func__, ret);
}
return ret;
} }
@@ -0,0 +1,264 @@
#include "downmix.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <audioclient.h>
#include <ks.h>
#include <ksmedia.h>
#include "util/logging.h"
#include "util.h"
namespace hooks::audio {
namespace {
constexpr float ATT_3DB = 0.70710678f;
// speakers routed to the left/right output; anything else (center) feeds both sides
constexpr DWORD LEFT_SPEAKERS = SPEAKER_FRONT_LEFT | SPEAKER_BACK_LEFT | SPEAKER_SIDE_LEFT
| SPEAKER_FRONT_LEFT_OF_CENTER | SPEAKER_TOP_FRONT_LEFT | SPEAKER_TOP_BACK_LEFT;
constexpr DWORD RIGHT_SPEAKERS = SPEAKER_FRONT_RIGHT | SPEAKER_BACK_RIGHT | SPEAKER_SIDE_RIGHT
| SPEAKER_FRONT_RIGHT_OF_CENTER | SPEAKER_TOP_FRONT_RIGHT | SPEAKER_TOP_BACK_RIGHT;
// the speaker mask is only present on WAVE_FORMAT_EXTENSIBLE formats
DWORD read_channel_mask(const WAVEFORMATEX *fmt) {
if (fmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE
&& fmt->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
return reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(fmt)->dwChannelMask;
}
return 0;
}
// call visit(channel_index, speaker_bit) for each present speaker, in channel order
template <typename F>
void for_each_speaker(DWORD mask, int channels, F &&visit) {
int channel = 0;
for (int bit = 0; bit < 18 && channel < channels; bit++) {
const DWORD speaker = 1u << bit;
if (mask & speaker) {
visit(channel++, speaker);
}
}
}
}
void Downmix::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out,
DownmixAlgorithm algorithm) {
this->enabled = true;
this->algorithm = algorithm;
this->bytes_per_sample = game_format->wBitsPerSample / 8;
this->game_frame_size = game_format->nChannels * this->bytes_per_sample;
this->is_float = is_ieee_float(game_format);
// supported: 16/24/32-bit integer PCM and 32-bit float; anything else mixes to silence
const bool supported = this->is_float
? this->bytes_per_sample == 4
: (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4);
if (!supported) {
log_fatal(
"audio::downmix",
"unsupported sample format ({}-bit {}), downmix will output silence",
game_format->wBitsPerSample, this->is_float ? "float" : "int");
}
this->left_mix.clear();
this->right_mix.clear();
this->build_layout_mix(game_format);
make_stereo_format(game_format, stereo_out);
}
void Downmix::make_stereo_format(const WAVEFORMATEX *game_format,
WAVEFORMATEXTENSIBLE *stereo_out) {
const int bytes_per_sample = game_format->wBitsPerSample / 8;
memcpy(stereo_out, game_format, sizeof(WAVEFORMATEXTENSIBLE));
stereo_out->Format.nChannels = 2;
stereo_out->Format.nBlockAlign = 2 * bytes_per_sample;
stereo_out->Format.nAvgBytesPerSec =
game_format->nSamplesPerSec * stereo_out->Format.nBlockAlign;
stereo_out->dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
}
HRESULT Downmix::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
const WAVEFORMATEX *device_format, LPCGUID session_guid) {
// the smaller stereo buffer can end up unaligned for the device when the game sized the
// duration for its larger multi-channel format; the helper recovers from that.
return initialize_with_alignment_retry(real, "audio::downmix", share_mode, stream_flags,
buffer_duration, periodicity, device_format, session_guid);
}
void Downmix::add_channel(int channel, DWORD speaker, float gain) {
if (speaker & LEFT_SPEAKERS) {
this->left_mix.push_back({ channel, gain });
} else if (speaker & RIGHT_SPEAKERS) {
this->right_mix.push_back({ channel, gain });
} else { // center: feed both sides
this->left_mix.push_back({ channel, gain });
this->right_mix.push_back({ channel, gain });
}
}
// AC-4 stereo downmix (ETSI TS 103 190-1): front pair at unity, everything else -3 dB, LFE dropped
void Downmix::build_ac4_mix(DWORD mask, int channels) {
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
if (speaker == SPEAKER_LOW_FREQUENCY) {
return;
}
const bool front_pair = speaker & (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT);
this->add_channel(ch, speaker, front_pair ? 1.0f : ATT_3DB);
});
}
// keep only the channels in `keep` (front/rear/side), each at unity gain
void Downmix::build_extract_mix(DWORD mask, int channels, DWORD keep) {
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
if (speaker & keep) {
this->add_channel(ch, speaker, 1.0f);
}
});
}
// keep every channel (LFE dropped), then average each side so its gains sum to unity
void Downmix::build_normalize_mix(DWORD mask, int channels) {
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
if (speaker != SPEAKER_LOW_FREQUENCY) {
this->add_channel(ch, speaker, 1.0f);
}
});
for (auto *mix : { &this->left_mix, &this->right_mix }) {
if (!mix->empty()) {
const float gain = 1.0f / mix->size();
for (auto &c : *mix) {
c.gain = gain;
}
}
}
}
// fallback when no speaker mask is present: fold interleaved L/R pairs (even->left, odd->right)
void Downmix::build_pairs_mix(int channels, float gain) {
for (int ch = 0; ch < channels; ch++) {
(((ch & 1) == 0) ? this->left_mix : this->right_mix).push_back({ ch, gain });
}
}
void Downmix::build_layout_mix(const WAVEFORMATEX *game_format) {
const int channels = game_format->nChannels;
const DWORD mask = read_channel_mask(game_format);
// without a mask the layout is unknown: extract/normalize have nothing to act on, so all
// algorithms fall back to folding L/R pairs (AC-4 still attenuates by -3 dB)
if (mask == 0) {
this->build_pairs_mix(channels,
this->algorithm == DownmixAlgorithm::AC4 ? ATT_3DB : 1.0f);
return;
}
switch (this->algorithm) {
case DownmixAlgorithm::FrontOnly:
this->build_extract_mix(mask, channels,
SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT);
break;
case DownmixAlgorithm::RearOnly:
this->build_extract_mix(mask, channels,
SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | SPEAKER_BACK_CENTER);
break;
case DownmixAlgorithm::SideOnly:
this->build_extract_mix(mask, channels,
SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT);
break;
case DownmixAlgorithm::Normalize:
this->build_normalize_mix(mask, channels);
break;
case DownmixAlgorithm::AC4:
this->build_ac4_mix(mask, channels);
break;
}
}
void Downmix::process(BYTE *dst, const BYTE *src, UINT32 frames) const {
const int bps = this->bytes_per_sample;
const int src_stride = this->game_frame_size;
const int dst_stride = 2 * bps;
if (dst == nullptr || src == nullptr || bps <= 0) {
return;
}
// sum each speaker's source channels into the matching stereo output
for (UINT32 i = 0; i < frames; i++) {
const BYTE *in = src + (size_t) i * src_stride;
BYTE *out = dst + (size_t) i * dst_stride;
float left = 0.0f;
float right = 0.0f;
for (const auto &c : this->left_mix) {
left += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain;
}
for (const auto &c : this->right_mix) {
right += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain;
}
write_sample(out, bps, this->is_float, left);
write_sample(out + bps, bps, this->is_float, right);
}
}
HRESULT Downmix::get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData) {
const size_t needed = (size_t) frames * this->game_frame_size;
if (this->scratch.size() < needed) {
this->scratch.resize(needed);
}
HRESULT ret = real->GetBuffer(frames, &this->device_buffer);
if (FAILED(ret)) {
this->device_buffer = nullptr;
return ret;
}
*ppData = this->scratch.data();
return S_OK;
}
HRESULT Downmix::get_scratch(UINT32 frames, BYTE **ppData) {
const size_t needed = (size_t) frames * this->game_frame_size;
if (this->scratch.size() < needed) {
this->scratch.resize(needed);
}
*ppData = this->scratch.data();
return S_OK;
}
void Downmix::downmix_into(BYTE *dst, UINT32 frames) const {
this->process(dst, this->scratch.data(), frames);
}
void Downmix::write_device_buffer(UINT32 frames, DWORD flags) {
const int bps = this->bytes_per_sample;
const int dst_stride = 2 * bps;
if (this->device_buffer == nullptr || frames == 0 || bps <= 0) {
return;
}
// mute the first few buffers to avoid a pop on stream start
if (this->buffers_to_mute > 0) {
memset(this->device_buffer, 0, (size_t) frames * dst_stride);
this->buffers_to_mute--;
} else if ((flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
this->process(this->device_buffer, this->scratch.data(), frames);
}
}
}
@@ -0,0 +1,150 @@
#pragma once
#include <optional>
#include <vector>
#include <windows.h>
#include <mmreg.h>
#include <audioclient.h>
#include "hooks/audio/audio.h"
struct IAudioClient;
struct IAudioRenderClient;
namespace hooks::audio {
// Generic WASAPI surround-to-stereo downmix. The real device is opened in stereo while the
// game keeps writing its native multi-channel audio into a scratch buffer; on release that
// buffer is mixed down into the two front channels.
//
// The mix is derived from the source format's speaker mask according to the selected
// DownmixAlgorithm:
// FrontOnly / RearOnly / SideOnly - keep only that group of channels, routed to their side
// AC4 - AC-4 stereo downmix coefficients (ETSI TS 103 190-1 §6.2.17): front left/right
// pass at 0 dB, center and surrounds fold in at -3 dB, LFE dropped
// Normalize - every channel folded in (center to both sides) with each output side averaged
// so its channels are equally loud, LFE dropped
struct Downmix {
// a source channel routed into one output speaker at the given gain
struct Contribution {
int channel;
float gain;
};
// map an option value (front/rear/side/ac4/normalize) to its algorithm.
static std::optional<DownmixAlgorithm> name_to_algorithm(const char *value) {
if (_stricmp(value, "front") == 0) {
return DownmixAlgorithm::FrontOnly;
} else if (_stricmp(value, "rear") == 0) {
return DownmixAlgorithm::RearOnly;
} else if (_stricmp(value, "side") == 0) {
return DownmixAlgorithm::SideOnly;
} else if (_stricmp(value, "ac4") == 0) {
return DownmixAlgorithm::AC4;
} else if (_stricmp(value, "normalize") == 0) {
return DownmixAlgorithm::Normalize;
}
return std::nullopt;
}
// human-readable name of an algorithm, for logging.
static const char *algorithm_name(DownmixAlgorithm algorithm) {
switch (algorithm) {
case DownmixAlgorithm::FrontOnly: return "front";
case DownmixAlgorithm::RearOnly: return "rear";
case DownmixAlgorithm::SideOnly: return "side";
case DownmixAlgorithm::AC4: return "ac4";
case DownmixAlgorithm::Normalize: return "normalize";
default: return "unknown";
}
}
// whether the downmix is active for the current stream
bool enabled = false;
// algorithm used to fold the multi-channel audio into stereo
DownmixAlgorithm algorithm = DownmixAlgorithm::AC4;
// size in bytes of one frame of the game's multi-channel format
int game_frame_size = 0;
// size in bytes of a single sample (per channel)
int bytes_per_sample = 0;
// whether samples are IEEE floating point rather than integer PCM
bool is_float = false;
// enable the downmix for the given game format and fill stereo_out with the equivalent
// stereo format to open the real device with.
void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out,
DownmixAlgorithm algorithm);
// build the stereo format equivalent to game_format (same sample rate and bit depth).
static void make_stereo_format(const WAVEFORMATEX *game_format,
WAVEFORMATEXTENSIBLE *stereo_out);
// initialize the real device with the stereo format. downmixing reduces the channel count,
// shrinking the buffer's byte size, so the duration the game sized for its multi-channel
// format can leave the smaller stereo buffer unaligned. on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED
// this performs the standard WASAPI realignment and retries.
HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
const WAVEFORMATEX *device_format, LPCGUID session_guid);
// mix `frames` frames of multi-channel `src` down into stereo `dst`.
void process(BYTE *dst, const BYTE *src, UINT32 frames) const;
// grab the real stereo device buffer and hand the game the scratch buffer to write into.
HRESULT get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData);
// size the scratch and hand it to the game without acquiring a device buffer. used when a
// later stage (the resampler) owns the device interaction.
HRESULT get_scratch(UINT32 frames, BYTE **ppData);
// downmix the scratch the game wrote into the caller's stereo buffer, without touching the
// device. used to feed the resampler when the two stages are chained.
void downmix_into(BYTE *dst, UINT32 frames) const;
// mix the scratch buffer into the stereo device buffer held since get_buffer. the caller
// owns releasing the device buffer afterwards (see current_buffer / buffer_released).
void write_device_buffer(UINT32 frames, DWORD flags);
// the real device buffer currently held, or null.
BYTE *current_buffer() const { return this->device_buffer; }
// forget the held device buffer once the caller has released it.
void buffer_released() { this->device_buffer = nullptr; }
private:
// build the mix from the source speaker layout for the selected algorithm
void build_layout_mix(const WAVEFORMATEX *game_format);
// per-algorithm builders, each filling left_mix / right_mix from the speaker mask
void build_ac4_mix(DWORD mask, int channels);
void build_extract_mix(DWORD mask, int channels, DWORD keep);
void build_normalize_mix(DWORD mask, int channels);
// fallback for streams without a speaker mask: fold interleaved L/R pairs at `gain`
void build_pairs_mix(int channels, float gain);
// append one source channel to the output side(s) matching its speaker, at `gain`
void add_channel(int channel, DWORD speaker, float gain);
// source channels summed into each output speaker
std::vector<Contribution> left_mix;
std::vector<Contribution> right_mix;
// buffer the game writes its multi-channel audio into between get/release
std::vector<BYTE> scratch;
// the real stereo device buffer currently held, or null
BYTE *device_buffer = nullptr;
// leading buffers to silence to avoid a pop on stream start
int buffers_to_mute = 16;
};
}
@@ -73,11 +73,7 @@ HRESULT STDMETHODCALLTYPE DummyIAudioClient::Initialize(
// verbose output // 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)); print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, pFormat);
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(StreamFlags));
log_info("audio::wasapi", "... hnsBufferDuration : {}", hnsBufferDuration);
log_info("audio::wasapi", "... hnsPeriodicity : {}", hnsPeriodicity);
print_format(pFormat);
CHECK_RESULT(this->backend->on_initialize( CHECK_RESULT(this->backend->on_initialize(
&ShareMode, &ShareMode,
@@ -0,0 +1,437 @@
#include "resample.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <mutex>
#include <audioclient.h>
#include "util/logging.h"
#include "util.h"
namespace hooks::audio {
namespace {
constexpr double PI = 3.14159265358979323846;
// normalized sinc: sin(pi*x) / (pi*x), with the removable singularity at 0 filled in
inline double sinc(double x) {
if (x == 0.0) {
return 1.0;
}
const double px = PI * x;
return std::sin(px) / px;
}
// Blackman window across the kernel radius; zero at +/- radius
inline double blackman(double x, double radius) {
const double n = (x + radius) / (2.0 * radius);
if (n <= 0.0 || n >= 1.0) {
return 0.0;
}
return 0.42 - 0.5 * std::cos(2.0 * PI * n) + 0.08 * std::cos(4.0 * PI * n);
}
}
std::optional<uint32_t> Resampler::resolve(const WAVEFORMATEX *game_format) {
if (game_format == nullptr || !RESAMPLE_RATE.has_value()) {
return std::nullopt;
}
if (game_format->nSamplesPerSec == 0
|| game_format->nSamplesPerSec == RESAMPLE_RATE.value()) {
return std::nullopt;
}
return RESAMPLE_RATE;
}
void Resampler::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *device_out,
uint32_t target_rate) {
this->enabled = true;
this->channels = game_format->nChannels;
this->bytes_per_sample = game_format->wBitsPerSample / 8;
this->game_frame_size = this->channels * this->bytes_per_sample;
this->is_float = is_ieee_float(game_format);
const bool supported = this->is_float
? this->bytes_per_sample == 4
: (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4);
if (!supported) {
log_fatal(
"audio::resample",
"unsupported sample format ({}-bit {}) for -resample",
game_format->wBitsPerSample, this->is_float ? "float" : "int");
}
this->src_rate = game_format->nSamplesPerSec;
this->dst_rate = target_rate;
// anti-alias cutoff: full bandwidth when upsampling, scaled down when decimating
this->cutoff = std::min(1.0, (double) this->dst_rate / (double) this->src_rate);
this->half_taps = 16;
// precompute the windowed-sinc kernel now that cutoff is known
this->build_kernel();
// prime the queue with half a window of silence so the first outputs have left history
this->in_queue.assign((size_t) this->half_taps * this->channels, 0.0f);
this->in_pos = this->half_taps;
this->make_device_format(game_format, device_out, target_rate);
}
void Resampler::make_device_format(const WAVEFORMATEX *game_format,
WAVEFORMATEXTENSIBLE *device_out, uint32_t target_rate) {
const size_t src_size = sizeof(WAVEFORMATEX) + game_format->cbSize;
memset(device_out, 0, sizeof(WAVEFORMATEXTENSIBLE));
memcpy(device_out, game_format, std::min(src_size, sizeof(WAVEFORMATEXTENSIBLE)));
device_out->Format.nSamplesPerSec = target_rate;
device_out->Format.nAvgBytesPerSec = target_rate * device_out->Format.nBlockAlign;
}
HRESULT Resampler::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode,
DWORD stream_flags, REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
const WAVEFORMATEX *device_format, LPCGUID session_guid) {
// the resampler bypasses the OS mixer and talks to the device directly, so it only makes
// sense (and only works) for exclusive streams. shared streams are already resampled by
// the Windows audio engine, so refuse loudly rather than silently doing nothing.
if (share_mode != AUDCLNT_SHAREMODE_EXCLUSIVE) {
log_fatal("audio::resample",
"-resample requires WASAPI exclusive mode, but this stream is shared "
"(Windows already resamples shared streams)");
}
// record the pacing model. event-driven streams fill the whole device buffer each period
// (produce_exact); timer-driven streams poll padding and write variable partial chunks, so
// they drain the pending output to the device's free space each call (flush_timer).
this->event_driven = (stream_flags & AUDCLNT_STREAMFLAGS_EVENTCALLBACK) != 0;
return initialize_with_alignment_retry(real, "audio::resample", share_mode, stream_flags,
buffer_duration, periodicity, device_format, session_guid);
}
UINT32 Resampler::frames_device_to_game(UINT32 device_frames) const {
if (this->dst_rate == 0) {
return device_frames;
}
// round down so the game never believes it has more room than the device can hold
return (UINT32) (((double) device_frames * this->src_rate) / this->dst_rate);
}
UINT32 Resampler::padding_device_to_game(UINT32 device_padding) const {
if (this->dst_rate == 0) {
return device_padding;
}
// round up so the reported free space stays conservative
return (UINT32) std::ceil(((double) device_padding * this->src_rate) / this->dst_rate);
}
HRESULT Resampler::get_buffer(UINT32 frames, BYTE **ppData) {
const size_t needed = (size_t) frames * this->game_frame_size;
if (this->scratch.size() < needed) {
this->scratch.resize(needed);
}
*ppData = this->scratch.data();
return S_OK;
}
void Resampler::enqueue_input(UINT32 frames, bool silent) {
const int bps = this->bytes_per_sample;
const int ch = this->channels;
const size_t base = this->in_queue.size();
this->in_queue.resize(base + (size_t) frames * ch);
if (silent || bps <= 0 || ch <= 0) {
std::fill(this->in_queue.begin() + base, this->in_queue.end(), 0.0f);
return;
}
const BYTE *src = this->scratch.data();
for (UINT32 f = 0; f < frames; f++) {
for (int c = 0; c < ch; c++) {
const size_t s = (size_t) f * ch + c;
this->in_queue[base + s] = read_sample(src + s * bps, bps, this->is_float);
}
}
}
void Resampler::build_kernel() {
const int taps = 2 * this->half_taps;
const int phases = this->kernel_phases;
const double cut = this->cutoff;
const double radius = (double) this->half_taps;
// one extra row at frac == 1.0 so emit_frame can interpolate against row p + 1 safely
this->kernel_table.resize((size_t) (phases + 1) * taps);
for (int p = 0; p <= phases; p++) {
const double frac = (double) p / (double) phases;
for (int k = 0; k < taps; k++) {
// tap k maps to input offset t = k - (half_taps - 1), matching emit_frame
const double x = frac - (double) (k - (this->half_taps - 1));
this->kernel_table[(size_t) p * taps + k] =
(float) (cut * sinc(cut * x) * blackman(x, radius));
}
}
}
void Resampler::emit_frame() {
const int ch = this->channels;
const int radius = this->half_taps;
const int taps = 2 * radius;
const long avail = (long) (this->in_queue.size() / ch);
const long center = (long) std::floor(this->in_pos);
// pick the two kernel rows bracketing this fractional position and the blend between them
const double frac = this->in_pos - (double) center;
const double fp = frac * (double) this->kernel_phases;
const int p0 = (int) fp;
const float blend = (float) (fp - (double) p0);
const float *row0 = &this->kernel_table[(size_t) p0 * taps];
const float *row1 = &this->kernel_table[(size_t) (p0 + 1) * taps];
// base input index for tap 0 (t = -(radius - 1))
const long base = center - (radius - 1);
for (int c = 0; c < ch; c++) {
double acc = 0.0;
for (int k = 0; k < taps; k++) {
const long idx = base + k;
if (idx < 0 || idx >= avail) {
continue;
}
const float w = row0[k] + blend * (row1[k] - row0[k]);
acc += (double) this->in_queue[(size_t) idx * ch + c] * w;
}
this->out_float.push_back((float) acc);
}
}
void Resampler::drop_consumed() {
const int ch = this->channels;
const long drop = (long) std::floor(this->in_pos) - this->half_taps;
if (drop > 0) {
const size_t drop_samples = (size_t) drop * ch;
if (drop_samples <= this->in_queue.size()) {
this->in_queue.erase(this->in_queue.begin(),
this->in_queue.begin() + drop_samples);
this->in_pos -= drop;
}
}
}
UINT32 Resampler::produce_exact(UINT32 out_frames) {
const int ch = this->channels;
this->out_float.clear();
if (ch <= 0 || out_frames == 0) {
return 0;
}
this->out_float.reserve((size_t) out_frames * ch);
// resample ratio. drive it from the buffer size actually advertised to the game rather
// than the nominal src/dst ratio: GetBufferSize reports floor(dev_buf * src/dst) game
// frames, so the game only ever delivers that many input frames per device period.
// consuming at the nominal ratio would eat slightly more input than arrives on any device
// where dev_buf * src/dst is non-integer (e.g. 144 -> 132.3, floored to 132), slowly
// draining the queue until it underruns to permanent silence. using the advertised integer
// ratio keeps input and output exactly balanced; the resulting pitch error is below 0.3%
// and inaudible, and it collapses to the exact ratio when the division is integer (160 ->
// 147 stays 147/160 = 44100/48000).
const double step = (double) this->frames_device_to_game(this->device_buffer_frames)
/ (double) this->device_buffer_frames;
// input frames the block will touch: from in_pos through the right edge of the sinc kernel
// at the final output sample. if the queue is short of this, the kernel tail reads past the
// end and distorts every buffer, so buffer one extra block of input before the first output
// (emitting silence without consuming) to build a cushion the kernel can always reach into.
const long avail = (long) (this->in_queue.size() / ch);
const long need = (long) std::ceil(this->in_pos + step * (double) out_frames)
+ this->half_taps;
if (this->priming) {
if (avail < need + (long) out_frames) {
this->out_float.assign((size_t) out_frames * ch, 0.0f);
return out_frames;
}
this->priming = false;
}
for (UINT32 o = 0; o < out_frames; o++) {
this->emit_frame();
this->in_pos += step;
}
this->drop_consumed();
return out_frames;
}
UINT32 Resampler::produce_variable() {
const int ch = this->channels;
if (ch <= 0) {
return 0;
}
// input frames consumed per output frame. timer-driven streams write variable partial
// chunks, so produce however many output frames the currently queued input can fully
// support and leave the rest for the next call; this keeps input and output balanced at
// the exact src/dst ratio over time without depending on the device buffer size.
const double step = (double) this->src_rate / (double) this->dst_rate;
const long avail = (long) (this->in_queue.size() / ch);
// emit only while the sinc kernel's right edge stays within the queued input. the kernel
// reaches from in_pos out to half_taps frames ahead, so stop once that would read past the
// end; the remaining input becomes the next block's lookahead.
UINT32 produced = 0;
while ((long) std::ceil(this->in_pos) + this->half_taps < avail) {
this->emit_frame();
this->in_pos += step;
produced++;
}
this->drop_consumed();
return produced;
}
void Resampler::write_output(BYTE *dst, UINT32 frames, float gain) const {
const int bps = this->bytes_per_sample;
const int ch = this->channels;
const size_t count = (size_t) frames * ch;
for (size_t i = 0; i < count; i++) {
write_sample(dst + i * bps, bps, this->is_float, this->out_float[i] * gain);
}
}
HRESULT Resampler::flush(IAudioRenderClient *real, IAudioClient *client, UINT32 frames,
DWORD flags, float boost) {
if (!this->enabled) {
return S_OK;
}
// cache the device buffer size once
if (this->device_buffer_frames == 0) {
client->GetBufferSize(&this->device_buffer_frames);
}
if (this->device_buffer_frames == 0) {
return S_OK;
}
const bool silent = (flags & AUDCLNT_BUFFERFLAGS_SILENT) != 0;
this->enqueue_input(frames, silent);
// confirm once that conversion actually started producing output
static std::once_flag active_printed;
std::call_once(active_printed, [this]() {
log_info("audio::resample", "resample active: {} Hz -> {} Hz ({} ch, {})",
this->src_rate, this->dst_rate, this->channels,
this->event_driven ? "event-driven" : "timer-driven");
});
// the boost is applied here (inside write_output) rather than in the standard ReleaseBuffer
// path, so log it once for parity with that path's "volume boost active" line.
if (boost != 1.0f) {
static std::once_flag boost_printed;
std::call_once(boost_printed, [boost]() {
log_info("audio::resample", "volume boost active (resample): gain={}", boost);
});
}
return this->event_driven
? this->flush_event(real, boost)
: this->flush_timer(real, client, boost);
}
HRESULT Resampler::flush_event(IAudioRenderClient *real, float boost) {
// event-driven exclusive streams must hand the device a full buffer every period and may
// not push partial counts. resample the whole input block into exactly the device buffer
// size.
const UINT32 produced = this->produce_exact(this->device_buffer_frames);
if (produced == 0) {
return S_OK;
}
BYTE *dev = nullptr;
HRESULT ret = real->GetBuffer(produced, &dev);
if (FAILED(ret) || dev == nullptr) {
return ret;
}
// mute the first few buffers to avoid a pop on stream start
float gain = boost;
if (this->buffers_to_mute > 0) {
gain = 0.0f;
this->buffers_to_mute--;
}
this->write_output(dev, produced, gain);
return real->ReleaseBuffer(produced, 0);
}
HRESULT Resampler::flush_timer(IAudioRenderClient *real, IAudioClient *client, float boost) {
// convert everything currently queued into the pending output FIFO (out_float). timer-
// driven games write variable partial chunks, so produce only what the queued input can
// fully support and keep the remainder for the next call.
this->produce_variable();
const int ch = this->channels;
if (ch <= 0) {
return S_OK;
}
const UINT32 pending = (UINT32) (this->out_float.size() / ch);
if (pending == 0) {
return S_OK;
}
// push as many frames as the device currently has free, keeping the rest queued for the
// next call. timer-driven games poll padding and write whenever there is room, so matching
// the device's free space here avoids overflowing the ring while staying device-paced.
UINT32 padding = 0;
if (FAILED(client->GetCurrentPadding(&padding))) {
return S_OK;
}
const UINT32 device_free = this->device_buffer_frames > padding
? this->device_buffer_frames - padding
: 0;
if (device_free == 0) {
return S_OK;
}
const UINT32 to_write = std::min(pending, device_free);
BYTE *dev = nullptr;
HRESULT ret = real->GetBuffer(to_write, &dev);
if (FAILED(ret) || dev == nullptr) {
return ret;
}
// mute the first few buffers to avoid a pop on stream start
float gain = boost;
if (this->buffers_to_mute > 0) {
gain = 0.0f;
this->buffers_to_mute--;
}
this->write_output(dev, to_write, gain);
ret = real->ReleaseBuffer(to_write, 0);
// drop the frames just written from the front of the pending FIFO
this->out_float.erase(this->out_float.begin(),
this->out_float.begin() + (size_t) to_write * ch);
return ret;
}
}
@@ -0,0 +1,149 @@
#pragma once
#include <cstdint>
#include <optional>
#include <vector>
#include <windows.h>
#include <mmreg.h>
#include <audioclient.h>
#include "hooks/audio/audio.h"
struct IAudioClient;
struct IAudioRenderClient;
namespace hooks::audio {
// Streaming sample-rate converter for the WASAPI render path. The real device is opened at the
// target rate while the game keeps writing its native-rate audio into a scratch buffer; on
// release that buffer is converted with a windowed-sinc kernel and pushed to the device.
// Channel count and sample format are preserved; only the sample rate changes.
//
// Frame counts differ between the two rates, so unlike the per-frame downmix this is stateful:
// a fractional read position and a window of input history carry across ReleaseBuffer calls,
// and the device buffer is only filled up to the space the device currently has free.
struct Resampler {
// whether the resampler is active for the current stream
bool enabled = false;
// whether the stream is event-driven (AUDCLNT_STREAMFLAGS_EVENTCALLBACK). timer-driven
// streams instead poll padding and write variable partial chunks, so they drain the
// pending output to the device's free space rather than pushing a full buffer per period.
bool event_driven = true;
// decide whether the stream should be resampled and to which rate. returns the target rate
// when RESAMPLE_RATE is set and differs from the game's rate, otherwise nullopt.
static std::optional<uint32_t> resolve(const WAVEFORMATEX *game_format);
// enable resampling for game_format and fill device_out with the equivalent format at the
// target rate to open the real device with.
void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *device_out,
uint32_t target_rate);
// build the device format equivalent to game_format at target_rate (same channels/depth).
static void make_device_format(const WAVEFORMATEX *game_format,
WAVEFORMATEXTENSIBLE *device_out, uint32_t target_rate);
// initialize the real device at the target rate, performing the standard WASAPI buffer
// realignment retry on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED.
HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
const WAVEFORMATEX *device_format, LPCGUID session_guid);
// translate a device-rate frame count to the equivalent game-rate count, so the buffer-size
// and padding values reported to the game stay paced at the game's native rate.
UINT32 frames_device_to_game(UINT32 device_frames) const;
UINT32 padding_device_to_game(UINT32 device_padding) const;
// hand the game a scratch buffer sized for `frames` of its native format to write into.
HRESULT get_buffer(UINT32 frames, BYTE **ppData);
// pointer to the input scratch (sized by get_buffer). when chained after the downmix, the
// downmix writes its stereo output here for the resampler to consume on the next flush.
BYTE *input_data() { return this->scratch.data(); }
// convert the `frames` the game wrote and push output to the real render client. `boost`
// is applied to the converted output. event-driven streams fill exactly one device buffer
// per period; timer-driven streams push as many converted frames as the device has free.
HRESULT flush(IAudioRenderClient *real, IAudioClient *client, UINT32 frames, DWORD flags,
float boost);
private:
// append `frames` of the scratch buffer (native format), or silence, to the input queue
void enqueue_input(UINT32 frames, bool silent);
// event-driven path: produce exactly one full device buffer and push it.
HRESULT flush_event(IAudioRenderClient *real, float boost);
// timer-driven path: convert all queued input into the pending output FIFO, then push as
// many frames as the device currently has free, keeping the remainder for the next call.
HRESULT flush_timer(IAudioRenderClient *real, IAudioClient *client, float boost);
// produce exactly out_frames output frames using the fixed src/dst ratio. event-driven
// exclusive streams must fill the whole device buffer every period; a small input cushion
// is buffered first (see priming) so the sinc kernel always has lookahead.
UINT32 produce_exact(UINT32 out_frames);
// convert all input the kernel can fully support into the pending output FIFO (out_float),
// appending without clearing. returns the number of frames produced. used by the
// timer-driven path where output is drained to the device in device-paced chunks.
UINT32 produce_variable();
// convolve the windowed-sinc kernel at the current in_pos and append the resulting frame
// (one sample per channel) to out_float
void emit_frame();
// precompute the windowed-sinc kernel sampled at kernel_phases sub-sample positions, so
// emit_frame is a table lookup instead of recomputing sin/cos per tap (which is far too
// expensive to run per sample on the audio callback thread and causes underrun crackle).
void build_kernel();
// drop input frames that in_pos has advanced past, keeping a window of history for the
// next block's left context
void drop_consumed();
// convert the first `frames` of out_float to the device format, scaled by `gain`
void write_output(BYTE *dst, UINT32 frames, float gain) const;
// sample format of the stream
int channels = 0;
int bytes_per_sample = 0;
bool is_float = false;
int game_frame_size = 0;
uint32_t src_rate = 0;
uint32_t dst_rate = 0;
// sinc low-pass cutoff (1.0 when upsampling, dst/src when downsampling) and window radius
double cutoff = 1.0;
int half_taps = 16;
// precomputed kernel: (kernel_phases + 1) rows of 2*half_taps weights, indexed by the
// fractional sample position (linearly interpolated between adjacent rows in emit_frame)
std::vector<float> kernel_table;
int kernel_phases = 1024;
// interleaved float input queue and the fractional read position within it (in frames)
std::vector<float> in_queue;
double in_pos = 0.0;
// emit silence until a full block of input lookahead has accumulated, so the sinc kernel
// never reads past the end of the queue (which would distort the tail of every buffer)
bool priming = true;
// interleaved float scratch for produced output
std::vector<float> out_float;
// buffer the game writes its native-rate audio into between get_buffer / flush
std::vector<BYTE> scratch;
// cached device buffer size (frames); a full buffer is produced every period
UINT32 device_buffer_frames = 0;
// leading buffers to silence to avoid a pop on stream start
int buffers_to_mute = 16;
};
}
@@ -2,7 +2,9 @@
#include <audioclient.h> #include <audioclient.h>
#include "hooks/audio/util.h"
#include "util/flags_helper.h" #include "util/flags_helper.h"
#include "util/logging.h"
#include "defs.h" #include "defs.h"
@@ -18,3 +20,46 @@ std::string stream_flags_str(DWORD flags) {
FLAG(flags, AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY); FLAG(flags, AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY);
FLAGS_END(flags); FLAGS_END(flags);
} }
void print_format(AUDCLNT_SHAREMODE share_mode, DWORD stream_flags, REFERENCE_TIME buffer_duration,
REFERENCE_TIME periodicity, const WAVEFORMATEX *device_format) {
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(share_mode));
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(stream_flags));
log_info("audio::wasapi", "... hnsBufferDuration : {} ({:.3f} ms)",
buffer_duration, buffer_duration / 10000.0);
log_info("audio::wasapi", "... hnsPeriodicity : {} ({:.3f} ms)",
periodicity, periodicity / 10000.0);
print_format(device_format);
}
void print_format(AUDCLNT_SHAREMODE share_mode, const WAVEFORMATEX *device_format) {
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(share_mode));
print_format(device_format);
}
HRESULT initialize_with_alignment_retry(IAudioClient *client, const char *log_group,
AUDCLNT_SHAREMODE share_mode, DWORD stream_flags, REFERENCE_TIME buffer_duration,
REFERENCE_TIME periodicity, const WAVEFORMATEX *device_format, LPCGUID session_guid) {
HRESULT ret = client->Initialize(share_mode, stream_flags, buffer_duration, periodicity,
device_format, session_guid);
// the requested buffer size can end up unaligned for the device; recover by asking 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(client->GetBufferSize(&aligned_frames)) && aligned_frames > 0) {
REFERENCE_TIME aligned_duration = (REFERENCE_TIME)
(10000.0 * 1000 / device_format->nSamplesPerSec * aligned_frames + 0.5);
log_info(log_group, "buffer not aligned, retrying with {} frames ({} hns)",
aligned_frames, aligned_duration);
ret = client->Initialize(share_mode, stream_flags, aligned_duration,
periodicity != 0 ? aligned_duration : 0, device_format, session_guid);
}
}
return ret;
}
@@ -1,8 +1,100 @@
#pragma once #pragma once
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <string> #include <string>
#include <windows.h> #include <windows.h>
#include <mmreg.h> #include <mmreg.h>
#include <audioclient.h>
std::string stream_flags_str(DWORD flags); std::string stream_flags_str(DWORD flags);
// log the stream parameters (share mode, flags, buffer duration, periodicity) followed by the wave
// format, matching the block printed at the top of IAudioClient::Initialize.
void print_format(AUDCLNT_SHAREMODE share_mode, DWORD stream_flags, REFERENCE_TIME buffer_duration,
REFERENCE_TIME periodicity, const WAVEFORMATEX *device_format);
// log the share mode followed by the wave format, for paths that only have a share mode (e.g.
// IAudioClient::IsFormatSupported).
void print_format(AUDCLNT_SHAREMODE share_mode, const WAVEFORMATEX *device_format);
// detect IEEE float samples: WAVE_FORMAT_IEEE_FLOAT, or WAVE_FORMAT_EXTENSIBLE whose SubFormat is
// KSDATAFORMAT_SUBTYPE_IEEE_FLOAT (Data1 == 3; _PCM has Data1 == 1)
inline bool is_ieee_float(const WAVEFORMATEX *fmt) {
return fmt->wFormatTag == WAVE_FORMAT_IEEE_FLOAT
|| (fmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE
&& reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(fmt)->SubFormat.Data1 == 0x00000003);
}
// read one sample at `p` as a normalized float in [-1, 1]
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
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;
}
}
// initialize the real audio client, recovering from AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED by asking the
// device for the next aligned buffer size and re-initializing with a matching duration. log_group
// names the subsystem in the retry log line.
HRESULT initialize_with_alignment_retry(IAudioClient *client, const char *log_group,
AUDCLNT_SHAREMODE share_mode, DWORD stream_flags, REFERENCE_TIME buffer_duration,
REFERENCE_TIME periodicity, const WAVEFORMATEX *device_format, LPCGUID session_guid);
+11 -11
View File
@@ -48,33 +48,33 @@ void copy_wave_format(WAVEFORMATEXTENSIBLE *destination, const WAVEFORMATEX *sou
} }
void print_format(const WAVEFORMATEX *pFormat) { void print_format(const WAVEFORMATEX *pFormat) {
log_info("audio", "Wave Format:"); log_info("audio::wasapi", "Wave Format:");
// format specific // format specific
if (pFormat->wFormatTag == WAVE_FORMAT_EXTENSIBLE) { if (pFormat->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
auto format = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(pFormat); auto format = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(pFormat);
log_info("audio", "... SubFormat : {}", guid2s(format->SubFormat)); log_info("audio::wasapi", "... SubFormat : {}", guid2s(format->SubFormat));
} else { } else {
log_info("audio", "... wFormatTag : {}", pFormat->wFormatTag); log_info("audio::wasapi", "... wFormatTag : {}", pFormat->wFormatTag);
} }
// generic // generic
log_info("audio", "... nChannels : {}", pFormat->nChannels); log_info("audio::wasapi", "... nChannels : {}", pFormat->nChannels);
log_info("audio", "... nSamplesPerSec : {}", pFormat->nSamplesPerSec); log_info("audio::wasapi", "... nSamplesPerSec : {}", pFormat->nSamplesPerSec);
log_info("audio", "... nAvgBytesPerSec : {}", pFormat->nAvgBytesPerSec); log_info("audio::wasapi", "... nAvgBytesPerSec : {}", pFormat->nAvgBytesPerSec);
log_info("audio", "... nBlockAlign : {}", pFormat->nBlockAlign); log_info("audio::wasapi", "... nBlockAlign : {}", pFormat->nBlockAlign);
log_info("audio", "... wBitsPerSample : {}", pFormat->wBitsPerSample); log_info("audio::wasapi", "... wBitsPerSample : {}", pFormat->wBitsPerSample);
// format specific // format specific
if (pFormat->wFormatTag == WAVE_FORMAT_EXTENSIBLE) { if (pFormat->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
auto format = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(pFormat); auto format = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(pFormat);
if (pFormat->wBitsPerSample == 0) { if (pFormat->wBitsPerSample == 0) {
log_info("audio", "... wSamplesPerBlock : {}", format->Samples.wSamplesPerBlock); log_info("audio::wasapi", "... wSamplesPerBlock : {}", format->Samples.wSamplesPerBlock);
} else { } else {
log_info("audio", "... wValidBitsPerSample : {}", format->Samples.wValidBitsPerSample); log_info("audio::wasapi", "... wValidBitsPerSample : {}", format->Samples.wValidBitsPerSample);
} }
log_info("audio", "... dwChannelMask : {}", channel_mask_str(format->dwChannelMask)); log_info("audio::wasapi", "... dwChannelMask : {}", channel_mask_str(format->dwChannelMask));
} }
} }
@@ -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"
@@ -716,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 {
@@ -1434,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");
} }
@@ -1449,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.
@@ -1471,9 +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 = games::gitadora::is_arena_model() && const bool is_gfdm_arena = games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS;
(GRAPHICS_FORCE_SINGLE_ADAPTER || GRAPHICS_PREVENT_SECONDARY_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);
@@ -253,15 +253,15 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
{ {
WRAP_VERBOSE; WRAP_VERBOSE;
HRESULT hr = pReal->CreateAdditionalSwapChain(pPresentationParameters, ppSwapChain);
int index = 0; 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() &&
(GRAPHICS_FORCE_SINGLE_ADAPTER || GRAPHICS_PREVENT_SECONDARY_WINDOW)) { (GRAPHICS_PREVENT_SECONDARY_WINDOWS || GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS)) {
if (pPresentationParameters->BackBufferWidth == 800) { if (pPresentationParameters->BackBufferWidth == 800) {
// SMALL (subscreen) // SMALL (subscreen)
create_swap_chain = true; create_swap_chain = true;
@@ -273,11 +273,32 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
if (sub_swapchain[index] || fake_sub_swapchain[index]) { if (sub_swapchain[index] || fake_sub_swapchain[index]) {
index = 2; index = 2;
} }
create_fake_swap_chain = GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS &&
!GRAPHICS_PREVENT_SECONDARY_WINDOWS;
} else { } else {
log_warning("graphics::d3d9", "unknown swap chain detected in CreateAdditionalSwapChain"); 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) {
log_misc( log_misc(
"graphics::d3d9", "graphics::d3d9",
@@ -739,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(&param);
// 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
@@ -758,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(&param);
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,
@@ -772,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
@@ -791,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.
@@ -856,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.
@@ -931,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;
@@ -940,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",
@@ -952,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) {
+223 -11
View File
@@ -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;
@@ -76,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;
@@ -121,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) {
@@ -282,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);
} }
@@ -345,25 +483,37 @@ 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();
// update style / ex-style // update style / ex-style
if (is_tdj_sub_window || 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) {
// hide maximize button (prevent misaligned touches) // hide maximize button (prevent misaligned touches)
if (!allow_gfdm_small_resize) {
dwStyle &= ~(WS_MAXIMIZEBOX); dwStyle &= ~(WS_MAXIMIZEBOX);
}
// mouse clicks become misaligned when resized // mouse clicks become misaligned when resized
if (!allow_gfdm_small_resize) {
dwStyle &= ~(WS_SIZEBOX); dwStyle &= ~(WS_SIZEBOX);
}
// borderless // borderless
if (GRAPHICS_WINDOWED && GRAPHICS_WSUB_BORDERLESS) { if (GRAPHICS_WINDOWED && GRAPHICS_WSUB_BORDERLESS) {
@@ -377,6 +527,13 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
} }
} }
if (allow_gfdm_small_resize) {
gitadora_apply_small_resize_style(dwStyle);
}
if (is_gfdm_window) {
graphics_gitadora_apply_window_style(dwStyle, dwExStyle);
}
if (is_sdvx_sub_window) { if (is_sdvx_sub_window) {
graphics_load_windowed_subscreen_parameters(); graphics_load_windowed_subscreen_parameters();
if (GRAPHICS_WSUB_SIZE.has_value()) { if (GRAPHICS_WSUB_SIZE.has_value()) {
@@ -395,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);
} }
@@ -421,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 && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_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);
} }
} }
@@ -613,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);
} }
@@ -666,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) {
@@ -677,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,
@@ -689,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() &&
@@ -702,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() &&
GRAPHICS_PREVENT_SECONDARY_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;
@@ -841,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);
+16 -1
View File
@@ -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,6 +120,16 @@ 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( void update_monitor_on_boot(
@@ -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;
+130
View File
@@ -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"
@@ -18,12 +19,14 @@
// 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;
@@ -43,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
@@ -183,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__
@@ -243,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
*/ */
+91 -6
View File
@@ -3,6 +3,8 @@
#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 <windows.h>
@@ -70,6 +72,8 @@
#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/asio_proxy.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"
@@ -95,6 +99,7 @@
#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"
@@ -374,7 +379,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) {
@@ -492,6 +497,9 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::spice2x_SDVXAsioDriver].is_active()) { if (options[launcher::Options::spice2x_SDVXAsioDriver].is_active()) {
games::sdvx::ASIO_DRIVER = options[launcher::Options::spice2x_SDVXAsioDriver].value_text(); games::sdvx::ASIO_DRIVER = options[launcher::Options::spice2x_SDVXAsioDriver].value_text();
} }
if (options[launcher::Options::SDVXAsioTwoChannel].value_bool()) {
WrappedAsio::FORCE_TWO_CHANNELS = true;
}
if (options[launcher::Options::spice2x_SDVXSubPos].is_active()) { if (options[launcher::Options::spice2x_SDVXSubPos].is_active()) {
auto txt = options[launcher::Options::spice2x_SDVXSubPos].value_text(); auto txt = options[launcher::Options::spice2x_SDVXSubPos].value_text();
if (txt == "top") { if (txt == "top") {
@@ -627,7 +635,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();
@@ -644,12 +652,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();
} }
// gitadora arena layout
if (options[launcher::Options::GitaDoraArenaSingleWindow].value_bool()) { if (options[launcher::Options::GitaDoraArenaSingleWindow].value_bool()) {
// for full screen games::gitadora::ARENA_WINDOW_COUNT = 1;
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
// for windowed
GRAPHICS_PREVENT_SECONDARY_WINDOW = true;
} }
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();
} }
@@ -666,6 +684,12 @@ 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::GitaDoraArenaRealtekAccess].value_bool()) {
games::gitadora::ALLOW_REALTEK_AUDIO = true;
}
if (options[launcher::Options::LoadNostalgiaModule].value_bool()) { if (options[launcher::Options::LoadNostalgiaModule].value_bool()) {
attach_nostalgia = true; attach_nostalgia = true;
} }
@@ -1078,6 +1102,29 @@ 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::AudioResample].is_active()) {
const uint32_t rate = options[launcher::Options::AudioResample].value_uint32();
if (rate > 0) {
hooks::audio::RESAMPLE_RATE = rate;
}
}
if (options[launcher::Options::AudioExclusiveBuffer].is_active()) {
const uint32_t ms = options[launcher::Options::AudioExclusiveBuffer].value_uint32();
if (ms > 0) {
hooks::audio::EXCLUSIVE_BUFFER_MS = ms;
}
}
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();
@@ -1168,6 +1215,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()) {
@@ -1344,6 +1407,10 @@ int main_implementation(int argc, char *argv[]) {
timeutils::TIMER_HACKS_DISABLE = true; 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);
@@ -2467,6 +2534,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();
+240 -37
View File
@@ -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",
@@ -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",
@@ -911,6 +948,15 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "Game Options", .category = "Game Options",
.picker = OptionPickerType::AsioDriver, .picker = OptionPickerType::AsioDriver,
}, },
{
// SDVXAsioTwoChannel
.title = "SDVX ASIO Two Channel Audio",
.name = "sdvxasio2ch",
.desc = "Force the game to use two channels for ASIO output.",
.type = OptionType::Bool,
.game_name = "Sound Voltex",
.category = "Game Options",
},
{ {
// spice2x_SDVXSubPos // spice2x_SDVXSubPos
.title = "SDVX Subscreen Overlay Position", .title = "SDVX Subscreen Overlay Position",
@@ -1038,10 +1084,12 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "Game Options (Advanced)", .category = "Game Options (Advanced)",
}, },
{ {
.title = "GitaDora Two Channel Audio (DX/SD only)", .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"
"This option does nothing for Arena Model (GW Delta and above); need a patch for that.", "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",
@@ -1055,18 +1103,6 @@ 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 Disable Subscreens (EXPERIMENTAL)",
.name = "gdaonewindow",
.desc = "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,
.game_name = "GitaDora",
.category = "Game Options",
},
{ {
// GitaDoraLefty // GitaDoraLefty
.title = "GitaDora Lefty Guitar (for Digital Wailing)", .title = "GitaDora Lefty Guitar (for Digital Wailing)",
@@ -1132,6 +1168,64 @@ 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.\n\n"
"If the game crashes after successful ASIO init, try enabling -gdafakerealtek.",
.type = OptionType::Text,
.game_name = "GitaDora",
.category = "Game Options",
.picker = OptionPickerType::AsioDriver,
},
{
// GitaDoraArenaRealtekAccess
.title = "GitaDora Arena ASIO Allow Headphones",
.name = "gdarealtek",
.desc = "For Arena Model: allow the game to access the Realtek audio for headphone output.\n\n"
"After opening ASIO device, the game then tries to look for audio devices named Realtek to open "
"a second audio stream. For compatibility, spice prevents the game from doing this, but enabling "
"this option will allow the game to access the Realtek audio device again.",
.type = OptionType::Bool,
.game_name = "GitaDora",
.category = "Game Options",
},
{ {
.title = "Force Load Jubeat Module", .title = "Force Load Jubeat Module",
.name = "jb", .name = "jb",
@@ -1869,6 +1963,80 @@ 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/ASIO 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"},
},
},
{
// AudioResample
.title = "WASAPI Exclusive Mode Resampling",
.name = "resample",
.desc = "Resamples the hooked audio output to a fixed sample rate before it reaches the "
"device. Useful when a game requests a sample rate the device cannot output in "
"exclusive mode (e.g. the game wants 44100 Hz but the device is locked to 48000 Hz).\n\n"
"Select the TARGET sample rate (one that your audio card supports).\n\n"
"Will result in couple milliseconds of latency and increased CPU usage when active.",
.type = OptionType::Enum,
.category = "Audio",
.elements = {
{"44100", "44.1 kHz"},
{"48000", "48 kHz"},
{"88200", "88.2 kHz"},
{"96000", "96 kHz"},
{"176400", "176.4 kHz"},
{"192000", "192 kHz"},
},
},
{
// AudioExclusiveBuffer
.title = "WASAPI Exclusive Buffer Size",
.name = "exclusivebuffer",
.desc = "Enlarges the WASAPI exclusive-mode device buffer to the specified duration in milliseconds.\n\n"
"Try setting this to 10 or even 20 if you hear crackling or glitches "
"(at the cost of slightly increased latency).",
.type = OptionType::Integer,
.setting_name = "16",
.category = "Audio",
},
{ {
// DelayBy5Seconds // DelayBy5Seconds
.title = "Delay by 5 Seconds (DEPRECATED - use -sleepduration instead)", .title = "Delay by 5 Seconds (DEPRECATED - use -sleepduration instead)",
@@ -2250,6 +2418,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",
@@ -2490,28 +2706,6 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{"both", ""}, {"both", ""},
}, },
}, },
{
// AutoPinMacroTrigger0
.title = "Auto PIN Macro Trigger Log String (P1)",
.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 = "Auto PIN Macro Trigger Log String (P2)",
.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)",
},
{ {
// spice2x_LowLatencySharedAudio // spice2x_LowLatencySharedAudio
.title = "Low Latency Shared Audio", .title = "Low Latency Shared Audio",
@@ -2879,6 +3073,15 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{"user", ""}, {"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) {
+17 -3
View File
@@ -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,
@@ -91,6 +94,7 @@ namespace launcher {
spice2x_SDVXDigitalKnobSensitivity, spice2x_SDVXDigitalKnobSensitivity,
SDVXDigitalKnobSocd, SDVXDigitalKnobSocd,
spice2x_SDVXAsioDriver, spice2x_SDVXAsioDriver,
SDVXAsioTwoChannel,
spice2x_SDVXSubPos, spice2x_SDVXSubPos,
SDVXSubMonitorOverride, SDVXSubMonitorOverride,
LoadDDRModule, LoadDDRModule,
@@ -106,11 +110,14 @@ namespace launcher {
LoadGitaDoraModule, LoadGitaDoraModule,
GitaDoraTwoChannelAudio, GitaDoraTwoChannelAudio,
GitaDoraCabinetType, GitaDoraCabinetType,
GitaDoraArenaSingleWindow,
GitaDoraLefty, GitaDoraLefty,
GitaDoraWailHold, GitaDoraWailHold,
GitaDoraPickAlgo, GitaDoraPickAlgo,
GitaDoraSubOverlaySize, GitaDoraSubOverlaySize,
GitaDoraArenaSingleWindow,
GitaDoraArenaWindowLayout,
GitaDoraArenaAsioDriver,
GitaDoraArenaRealtekAccess,
LoadJubeatModule, LoadJubeatModule,
LoadReflecBeatModule, LoadReflecBeatModule,
LoadShogikaiModule, LoadShogikaiModule,
@@ -194,6 +201,10 @@ namespace launcher {
AsioDriverId, AsioDriverId,
AsioDriverName, AsioDriverName,
AudioDummy, AudioDummy,
DownmixAudioToStereo,
VolumeBoost,
AudioResample,
AudioExclusiveBuffer,
DelayBy5Seconds, DelayBy5Seconds,
spice2x_DelayByNSeconds, spice2x_DelayByNSeconds,
LoadStubs, LoadStubs,
@@ -235,6 +246,10 @@ namespace launcher {
spice2x_WindowAlwaysOnTop, spice2x_WindowAlwaysOnTop,
WindowForceScaling, WindowForceScaling,
WindowDisableRoundedCorners, WindowDisableRoundedCorners,
GitaDoraWindowedMainMonitor,
GitaDoraWindowedLeftMonitor,
GitaDoraWindowedRightMonitor,
GitaDoraWindowedSmallMonitor,
spice2x_IIDXWindowedSubscreenSize, spice2x_IIDXWindowedSubscreenSize,
spice2x_IIDXWindowedSubscreenPosition, spice2x_IIDXWindowedSubscreenPosition,
IIDXWindowedSubscreenBorderless, IIDXWindowedSubscreenBorderless,
@@ -255,8 +270,6 @@ namespace launcher {
IIDXSubMonitorOverride, IIDXSubMonitorOverride,
spice2x_IIDXEmulateSubscreenKeypadTouch, spice2x_IIDXEmulateSubscreenKeypadTouch,
spice2x_AutoCard, spice2x_AutoCard,
AutoPinMacroTrigger0,
AutoPinMacroTrigger1,
spice2x_LowLatencySharedAudio, spice2x_LowLatencySharedAudio,
spice2x_TapeLedAlgorithm, spice2x_TapeLedAlgorithm,
spice2x_NoNVAPI, spice2x_NoNVAPI,
@@ -288,6 +301,7 @@ namespace launcher {
DisableHighResTimer, DisableHighResTimer,
EnableICMPHook, EnableICMPHook,
AutoElevate, AutoElevate,
CfgForceSoftwareRender
}; };
enum class OptionsCategory { enum class OptionsCategory {
+5
View File
@@ -8,6 +8,7 @@
#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"
@@ -26,6 +27,10 @@ namespace launcher {
sdk::fini_sdk_modules(); 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();
+4
View File
@@ -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();
+38 -2
View File
@@ -14,14 +14,16 @@
#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;
@@ -119,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;
} }
} }
@@ -149,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;
} }
@@ -160,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;
} }
@@ -179,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;
} }
} }
@@ -251,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;
} }
@@ -394,6 +426,10 @@ void eamuse_pin_macro_start_thread() {
log_info("eamuse", "AUTO_PIN_MACRO_REQUEST detected for P{}", unit+1); log_info("eamuse", "AUTO_PIN_MACRO_REQUEST detected for P{}", unit+1);
} }
if (key_press || auto_request) { 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;
@@ -595,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;
} }
+1 -1
View File
@@ -78,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();
+1 -1
View File
@@ -416,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() && GRAPHICS_PREVENT_SECONDARY_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 {
+38 -6
View File
@@ -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
@@ -354,6 +369,14 @@ void ImGui_ImplSpice_NewFrame() {
const auto overlay_visible = overlay::OVERLAY && overlay::OVERLAY->get_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; 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;
for (size_t i = 0; i < VKEY_MAX; i++) { for (size_t i = 0; i < VKEY_MAX; i++) {
@@ -363,11 +386,13 @@ void ImGui_ImplSpice_NewFrame() {
const auto MouseDownOld = g_MouseDown; const auto MouseDownOld = g_MouseDown;
// reset keys state // reset keys state
if (drive_input_from_rawinput) {
g_MouseDown.fill(false); g_MouseDown.fill(false);
g_KeysDown.fill(false); g_KeysDown.fill(false);
}
// apply windows mouse buttons // apply windows mouse buttons
if (accept_new_input) { if (accept_new_input && drive_input_from_rawinput) {
g_MouseDown[ImGuiMouseButton_Left] |= get_async_primary_mouse(); g_MouseDown[ImGuiMouseButton_Left] |= get_async_primary_mouse();
g_MouseDown[ImGuiMouseButton_Right] |= get_async_secondary_mouse(); g_MouseDown[ImGuiMouseButton_Right] |= get_async_secondary_mouse();
g_MouseDown[ImGuiMouseButton_Middle] |= (GetAsyncKeyState(VK_MBUTTON) & 0x8000) != 0; g_MouseDown[ImGuiMouseButton_Middle] |= (GetAsyncKeyState(VK_MBUTTON) & 0x8000) != 0;
@@ -376,7 +401,7 @@ void ImGui_ImplSpice_NewFrame() {
// 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) {
@@ -432,7 +457,10 @@ 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).
// Skipped entirely in standalone configurator mode where Win32 messages already
// drive io.AddKeyEvent / io.AddInputCharacter directly.
if (drive_input_from_rawinput) {
for (size_t vKey = 0; vKey < VKEY_MAX; vKey++) { for (size_t vKey = 0; vKey < VKEY_MAX; vKey++) {
const bool state = g_KeysDown[vKey]; const bool state = g_KeysDown[vKey];
const auto imgui_key = get_imgui_key(vKey); const auto imgui_key = get_imgui_key(vKey);
@@ -471,29 +499,33 @@ 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
if (drive_input_from_rawinput) {
for (size_t i = 0; i < g_MouseDown.size(); i++) { for (size_t i = 0; i < g_MouseDown.size(); i++) {
if (MouseDownOld[i] != g_MouseDown[i]) { if (MouseDownOld[i] != g_MouseDown[i]) {
io.AddMouseButtonEvent(i, g_MouseDown[i]); io.AddMouseButtonEvent(i, g_MouseDown[i]);
log_debug("imgui_impl_spice", "mouse button {} event", g_MouseDown[i]); log_debug("imgui_impl_spice", "mouse button {} event", g_MouseDown[i]);
} }
} }
}
// automatically hide cursor // automatically hide cursor
if (g_MouseCursorAutoHide) { if (g_MouseCursorAutoHide) {
+1
View File
@@ -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();
+254
View File
@@ -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)
}
}
+55
View File
@@ -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();
}
+251 -9
View File
@@ -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;
} }
@@ -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,13 +555,24 @@ 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
if (this->active) {
for (auto &window : this->windows) { for (auto &window : this->windows) {
window->build(); window->build();
} }
@@ -489,15 +580,72 @@ void overlay::SpiceOverlay::new_frame() {
if (SHOW_DEBUG_LOG_WINDOW) { if (SHOW_DEBUG_LOG_WINDOW) {
ImGui::ShowDebugLogWindow(&SHOW_DEBUG_LOG_WINDOW); ImGui::ShowDebugLogWindow(&SHOW_DEBUG_LOG_WINDOW);
} }
}
// draw notifications last so they paint on top of any overlay windows
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; 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;
#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() {
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(); 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() {
if (!overlay::OVERLAY) {
return;
}
switch (overlay::OVERLAY->renderer) {
case OverlayRenderer::D3D9:
ImGui_ImplDX9_CreateDeviceObjects(); 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;
+59
View File
@@ -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);
} }
+20 -20
View File
@@ -12,9 +12,9 @@
#include "build/resource.h" #include "build/resource.h"
#include "cfg/config.h" #include "cfg/config.h"
#include "cfg/configurator.h" #include "cfg/configurator.h"
#include "external/asio/asiolist.h"
#include "external/imgui/imgui_internal.h" #include "external/imgui/imgui_internal.h"
#include "external/imgui/misc/cpp/imgui_stdlib.h" #include "external/imgui/misc/cpp/imgui_stdlib.h"
#include "hooks/audio/asio_driver_scan.h"
#include "games/io.h" #include "games/io.h"
#include "games/sdvx/sdvx.h" #include "games/sdvx/sdvx.h"
#include "games/popn/popn.h" #include "games/popn/popn.h"
@@ -63,7 +63,7 @@ namespace overlay::windows {
constexpr ImVec4 TEXT_COLOR_RED(1.f, 0.f, 0.f, 1.f); constexpr ImVec4 TEXT_COLOR_RED(1.f, 0.f, 0.f, 1.f);
constexpr uint32_t OPTION_INPUT_TEXT_WIDTH = 512; constexpr uint32_t OPTION_INPUT_TEXT_WIDTH = 512;
std::unique_ptr<AsioDriverList> asio_driver_list; std::optional<std::vector<hooks::audio::AsioDriverScanEntry>> asio_driver_list;
Config::Config(overlay::SpiceOverlay *overlay) : Window(overlay) { Config::Config(overlay::SpiceOverlay *overlay) : Window(overlay) {
this->title = "Configuration"; this->title = "Configuration";
@@ -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();
@@ -4420,30 +4426,24 @@ namespace overlay::windows {
void Config::build_option_value_picker(Option& option) { void Config::build_option_value_picker(Option& option) {
auto &definition = option.get_definition(); auto &definition = option.get_definition();
if (definition.picker == OptionPickerType::AsioDriver) { if (definition.picker == OptionPickerType::AsioDriver) {
if (asio_driver_list == nullptr) { if (!asio_driver_list.has_value()) {
asio_driver_list = std::make_unique<AsioDriverList>(); asio_driver_list = hooks::audio::scan_asio_drivers();
} }
ImGui::TextUnformatted("If your ASIO driver is not shown here, close this"); if (asio_driver_list->empty()) {
ImGui::TextUnformatted("popup and enter the driver name manually.");
ImGui::SameLine();
ImGui::HelpMarker(
"This list is populated by scanning the registry for ASIO drivers.\n\n"
"If your driver is not showing up, it may be because it is not properly registered in the system.\n\n"
"For 64-bit games, check in HKLM\\SOFTWARE\\ASIO\\.\n\n"
"For 32-bit games on 64-bit Windows, check in HKLM\\SOFTWARE\\WOW6432Node\\ASIO\\.\n\n"
"spicecfg runs in 32-bit, so it may not see 64-bit-only drivers.");
ImGui::TextUnformatted("");
if (asio_driver_list->driver_list.empty()) {
ImGui::TextUnformatted("No ASIO drivers found."); ImGui::TextUnformatted("No ASIO drivers found.");
} else { } else {
ImGui::TextUnformatted("Pick from ASIO drivers:"); ImGui::TextUnformatted("Pick from ASIO drivers:");
ImGui::SetNextItemWidth(300.f); ImGui::SetNextItemWidth(300.f);
if (ImGui::BeginListBox("##asiodrivers")) { if (ImGui::BeginListBox("##asiodrivers")) {
for (const auto &driver : asio_driver_list->driver_list) { for (const auto &driver : *asio_driver_list) {
const bool is_selected = option.value == std::string(driver.name); const bool is_selected = option.value == driver.name;
if (ImGui::Selectable(fmt::format("[{}] {}", driver.id, driver.name).c_str(), is_selected)) { const char *arch = (driver.found_32bit && driver.found_64bit)
? "32/64-bit"
: (driver.found_64bit ? "64-bit" : "32-bit");
if (ImGui::Selectable(
fmt::format("{} ({})", driver.name, arch).c_str(),
is_selected)) {
option.value = driver.name; option.value = driver.name;
} }
} }
+6 -2
View File
@@ -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);
+1 -1
View File
@@ -11,7 +11,7 @@ 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_WINDOW) { } else if (!GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
if (GRAPHICS_WINDOWED) { if (GRAPHICS_WINDOWED) {
this->disabled_message = "Use the dedicated subscreen window!"; this->disabled_message = "Use the dedicated subscreen window!";
} else { } else {
+139 -34
View File
@@ -3,6 +3,8 @@
#include <chrono> #include <chrono>
#include <thread> #include <thread>
#include <fstream> #include <fstream>
#include <memory>
#include <unordered_set>
#include <shellapi.h> #include <shellapi.h>
#include <winhttp.h> #include <winhttp.h>
#include <psapi.h> #include <psapi.h>
@@ -365,21 +367,24 @@ namespace overlay::windows {
config_dirty = true; config_dirty = true;
} }
// check for dirty state // handle dirty state
if (config_dirty) {
if (cfg::CONFIGURATOR_STANDALONE) { if (cfg::CONFIGURATOR_STANDALONE) {
// auto save for configurator version // auto save for configurator version
if (config_dirty) {
this->config_save(); this->config_save();
}
} else { } else {
// manual save for live version // manual save for live version: always render the row so the rest
// of the panel doesn't shift when config_dirty flips. Disable the
// button when there is nothing to save.
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
ImGui::HelpMarker("Save current patch state to the configuration file."); ImGui::HelpMarker("Save current patch state to the configuration file.");
ImGui::SameLine(); ImGui::SameLine();
ImGui::BeginDisabled(!config_dirty);
if (ImGui::Button("Save")) { if (ImGui::Button("Save")) {
this->config_save(); this->config_save();
} }
} ImGui::EndDisabled();
} }
bool disable_all_patches = false; bool disable_all_patches = false;
@@ -883,23 +888,54 @@ namespace overlay::windows {
void PatchManager::hard_apply_patches() { void PatchManager::hard_apply_patches() {
std::vector<std::filesystem::path> written_list; std::vector<std::filesystem::path> written_list;
for (auto& patch : patches) {
// dll_name -> in-memory image; populated lazily, written back once at
// the end. Avoids re-reading and re-writing the full DLL per patch,
// which used to dominate the cost of "Overwrite game files" on games
// with large patch lists (and leaked a vector per call via bin_read's
// raw new'd return).
robin_hood::unordered_map<std::string, std::unique_ptr<std::vector<uint8_t>>> dll_cache;
std::unordered_set<std::string> dirty;
auto load_dll = [&](const std::string &dll_name) -> std::vector<uint8_t> * {
auto it = dll_cache.find(dll_name);
if (it != dll_cache.end()) {
return it->second ? it->second.get() : nullptr;
}
const auto dll_path = MODULE_PATH / dll_name;
create_dll_backup(written_list, dll_path);
std::unique_ptr<std::vector<uint8_t>> owned(fileutils::bin_read(dll_path));
if (!owned || owned->empty()) {
// remember the miss so subsequent patches don't re-read/backup
dll_cache.emplace(dll_name, nullptr);
return nullptr;
}
auto *ptr = owned.get();
dll_cache.emplace(dll_name, std::move(owned));
return ptr;
};
for (auto &patch : patches) {
switch (patch.type) { switch (patch.type) {
case PatchType::Memory: case PatchType::Memory:
for (auto& memory_patch : patch.patches_memory) { for (auto &memory_patch : patch.patches_memory) {
auto dll_path = MODULE_PATH / memory_patch.dll_name; auto *dll_data = load_dll(memory_patch.dll_name);
create_dll_backup(written_list, dll_path); if (!dll_data) {
auto dll_data = fileutils::bin_read(dll_path); continue;
if (dll_data) { }
auto max_len = std::max(memory_patch.data_disabled_len, memory_patch.data_enabled_len); const auto max_len = std::max(
memory_patch.data_disabled_len, memory_patch.data_enabled_len);
if (memory_patch.data_offset + max_len <= dll_data->size()) { if (memory_patch.data_offset + max_len <= dll_data->size()) {
if (patch.enabled) { if (patch.enabled) {
memcpy(dll_data->data() + memory_patch.data_offset, memory_patch.data_enabled.get(), memory_patch.data_enabled_len); memcpy(dll_data->data() + memory_patch.data_offset,
memory_patch.data_enabled.get(),
memory_patch.data_enabled_len);
} else { } else {
memcpy(dll_data->data() + memory_patch.data_offset, memory_patch.data_disabled.get(), memory_patch.data_disabled_len); memcpy(dll_data->data() + memory_patch.data_offset,
} memory_patch.data_disabled.get(),
fileutils::bin_write(dll_path, dll_data->data(), dll_data->size()); memory_patch.data_disabled_len);
} }
dirty.insert(memory_patch.dll_name);
} }
} }
break; break;
@@ -907,45 +943,51 @@ namespace overlay::windows {
if (!patch.enabled) { if (!patch.enabled) {
break; break;
} }
for (auto& union_patch : patch.patches_union) { for (auto &union_patch : patch.patches_union) {
if (union_patch.name == patch.selected_union_name) { if (union_patch.name == patch.selected_union_name) {
auto dll_path = MODULE_PATH / union_patch.dll_name; auto *dll_data = load_dll(union_patch.dll_name);
create_dll_backup(written_list, dll_path); if (!dll_data) {
auto dll_data = fileutils::bin_read(dll_path); break;
if (dll_data) { }
if (union_patch.offset + union_patch.data_len <= dll_data->size()) { if (union_patch.offset + union_patch.data_len <= dll_data->size()) {
memcpy(dll_data->data() + union_patch.offset, union_patch.data.get(), union_patch.data_len); memcpy(dll_data->data() + union_patch.offset,
fileutils::bin_write(dll_path, dll_data->data(), dll_data->size()); union_patch.data.get(), union_patch.data_len);
} dirty.insert(union_patch.dll_name);
} }
break; break;
} }
} }
break; break;
case PatchType::Integer: case PatchType::Integer: {
{
if (!patch.enabled) { if (!patch.enabled) {
break; break;
} }
auto& numpatch = patch.patch_number; auto &numpatch = patch.patch_number;
auto dll_path = MODULE_PATH / numpatch.dll_name; auto *dll_data = load_dll(numpatch.dll_name);
create_dll_backup(written_list, dll_path); if (!dll_data) {
auto dll_data = fileutils::bin_read(dll_path); break;
if (dll_data) { }
if (numpatch.data_offset + numpatch.size_in_bytes <= dll_data->size()) { if (numpatch.data_offset + numpatch.size_in_bytes <= dll_data->size()) {
int_to_little_endian_bytes( int_to_little_endian_bytes(
numpatch.value, numpatch.value,
dll_data->data() + numpatch.data_offset, dll_data->data() + numpatch.data_offset,
numpatch.size_in_bytes); numpatch.size_in_bytes);
fileutils::bin_write(dll_path, dll_data->data(), dll_data->size()); dirty.insert(numpatch.dll_name);
}
}
} }
break; break;
}
default: default:
break; break;
} }
} }
// single write-back per dirty DLL
for (const auto &dll_name : dirty) {
const auto &data = dll_cache.at(dll_name);
if (data) {
fileutils::bin_write(MODULE_PATH / dll_name, data->data(), data->size());
}
}
} }
// match on model and ext, ignoring dest/spec/rev // match on model and ext, ignoring dest/spec/rev
@@ -3027,6 +3069,50 @@ namespace overlay::windows {
return file; return file;
} }
static bool get_file_times(
const std::filesystem::path& path,
FILETIME* creation,
FILETIME* access,
FILETIME* write) {
const auto handle = CreateFileW(
path.c_str(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (handle == INVALID_HANDLE_VALUE) {
return false;
}
const bool ok = GetFileTime(handle, creation, access, write) != 0;
CloseHandle(handle);
return ok;
}
static bool set_file_times(
const std::filesystem::path& path,
const FILETIME* creation,
const FILETIME* access,
const FILETIME* write) {
const auto handle = CreateFileW(
path.c_str(),
FILE_WRITE_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (handle == INVALID_HANDLE_VALUE) {
return false;
}
const bool ok = SetFileTime(handle, creation, access, write) != 0;
CloseHandle(handle);
return ok;
}
void create_dll_backup( void create_dll_backup(
std::vector<std::filesystem::path>& written_list, const std::filesystem::path& dll_path) { std::vector<std::filesystem::path>& written_list, const std::filesystem::path& dll_path) {
@@ -3037,7 +3123,26 @@ namespace overlay::windows {
dll_bak_path += ".bak"; dll_bak_path += ".bak";
try { try {
if (!fileutils::file_exists(dll_bak_path)) { if (!fileutils::file_exists(dll_bak_path)) {
FILETIME creation {};
FILETIME access {};
FILETIME write {};
const bool have_times =
get_file_times(dll_path, &creation, &access, &write);
if (!have_times) {
log_warning(
"patchmanager",
"could not read timestamps before DLL backup for: {}",
dll_path);
}
std::filesystem::copy(dll_path, dll_bak_path); std::filesystem::copy(dll_path, dll_bak_path);
if (have_times && !set_file_times(dll_bak_path, &creation, &access, &write)) {
log_warning(
"patchmanager",
"could not restore timestamps on DLL backup for: {}",
dll_bak_path);
}
} }
log_info("patchmanager", "created DLL backup for: {}", dll_path); log_info("patchmanager", "created DLL backup for: {}", dll_path);
} catch (const std::filesystem::filesystem_error& e) { } catch (const std::filesystem::filesystem_error& e) {
+1 -1
View File
@@ -15,7 +15,7 @@ namespace overlay::windows {
this->disabled_message = "Game did not launch as Pikapika Pop-kun (invalid <spec>)!"; this->disabled_message = "Game did not launch as Pikapika Pop-kun (invalid <spec>)!";
} else if (games::popn::SHOW_PIKA_MONITOR_WARNING) { } else if (games::popn::SHOW_PIKA_MONITOR_WARNING) {
this->disabled_message = "Subscreen overlay is not compatible with -dxmainadapter option, use -mainmonitor instead"; this->disabled_message = "Subscreen overlay is not compatible with -dxmainadapter option, use -mainmonitor instead";
} else if (GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOW) { } else if (GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
this->disabled_message = "Subscren overlay was not enabled in spicecfg. Use the second window (ALT+TAB)."; this->disabled_message = "Subscren overlay was not enabled in spicecfg. Use the second window (ALT+TAB).";
} }
+14 -11
View File
@@ -5,6 +5,7 @@
#include "cfg/screen_resize.h" #include "cfg/screen_resize.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "overlay/imgui/extensions.h" #include "overlay/imgui/extensions.h"
#include "overlay/notifications.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/utils.h" #include "util/utils.h"
@@ -102,19 +103,10 @@ namespace overlay::windows {
void ScreenResize::build_fullscreen_config() { void ScreenResize::build_fullscreen_config() {
if (avs::game::is_model("KFC")) {
ImGui::TextColored(ImVec4(1, 0.5f, 0.5f, 1.f), ImGui::TextColored(ImVec4(1, 0.5f, 0.5f, 1.f),
"Warning: Enabling Image Resize uses more GPU\n" "Warning: Enabling Image Resize uses more GPU\n"
"resources and may significantly lower framerate\n" "resources and may lower framerate on some\n"
"for songs with Live2D! Results may vary, use at\n" "lower-end PCs.");
"your own risk.");
} else {
ImGui::TextColored(ImVec4(1, 0.5f, 0.5f, 1.f),
"Warning: Enabling Image Resize uses more GPU\n"
"resources and may significantly lower framerate\n"
"in some situations! Results may vary, use at\n"
"your own risk.");
}
// enable checkbox // enable checkbox
ImGui::Checkbox("Enable", &cfg::SCREENRESIZE->enable_screen_resize); ImGui::Checkbox("Enable", &cfg::SCREENRESIZE->enable_screen_resize);
@@ -323,6 +315,11 @@ namespace overlay::windows {
if (toggle_screen_resize_new && !this->toggle_screen_resize_state) { if (toggle_screen_resize_new && !this->toggle_screen_resize_state) {
cfg::SCREENRESIZE->enable_screen_resize = !cfg::SCREENRESIZE->enable_screen_resize; cfg::SCREENRESIZE->enable_screen_resize = !cfg::SCREENRESIZE->enable_screen_resize;
overlay::notifications::add(
overlay::notifications::Severity::Info,
cfg::SCREENRESIZE->enable_screen_resize
? "Screen resize enabled"
: "Screen resize disabled");
} }
this->toggle_screen_resize_state = toggle_screen_resize_new; this->toggle_screen_resize_state = toggle_screen_resize_new;
} }
@@ -347,10 +344,16 @@ namespace overlay::windows {
cfg::SCREENRESIZE->enable_screen_resize) { cfg::SCREENRESIZE->enable_screen_resize) {
// this scene is already active, turn scaling off // this scene is already active, turn scaling off
cfg::SCREENRESIZE->enable_screen_resize = false; cfg::SCREENRESIZE->enable_screen_resize = false;
overlay::notifications::add(
overlay::notifications::Severity::Info,
"Screen resize disabled");
} else { } else {
// switch to scene // switch to scene
cfg::SCREENRESIZE->enable_screen_resize = true; cfg::SCREENRESIZE->enable_screen_resize = true;
cfg::SCREENRESIZE->screen_resize_current_scene = i; cfg::SCREENRESIZE->screen_resize_current_scene = i;
overlay::notifications::add(
overlay::notifications::Severity::Info,
fmt::format("Screen resize: scene {}", i + 1));
} }
break; break;
} }
+1 -1
View File
@@ -15,7 +15,7 @@ namespace overlay::windows {
} else if (games::sdvx::SHOW_VM_MONITOR_WARNING) { } else if (games::sdvx::SHOW_VM_MONITOR_WARNING) {
this->disabled_message = "VM mode subscreen overlay is not compatible with -dxmainadapter option, use -mainmonitor instead"; this->disabled_message = "VM mode subscreen overlay is not compatible with -dxmainadapter option, use -mainmonitor instead";
} else if (GRAPHICS_WINDOWED) { } else if (GRAPHICS_WINDOWED) {
if (GRAPHICS_PREVENT_SECONDARY_WINDOW) { if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
this->disabled_message = "Subscreen has been disabled by the user (-sdvxnosub)."; this->disabled_message = "Subscreen has been disabled by the user (-sdvxnosub).";
} else { } else {
this->disabled_message = "Overlay unavailable in windowed mode! Use the second window instead. (ALT+TAB)"; this->disabled_message = "Overlay unavailable in windowed mode! Use the second window instead. (ALT+TAB)";
+16 -2
View File
@@ -1112,7 +1112,16 @@ void rawinput::RawInputManager::flush_start() {
* to button based lighting. * to button based lighting.
*/ */
this->devices_flush_output(false); this->devices_flush_output(false);
Sleep(495);
// wait up to ~500ms, but wake immediately if flush_stop()
// flips the running flag. Without the CV the in-flight
// Sleep() forced launcher::shutdown() to block for the full
// remaining sleep window on every close.
std::unique_lock<std::mutex> lock(this->flush_thread_m);
this->flush_thread_cv.wait_for(
lock,
std::chrono::milliseconds(495),
[this] { return !this->flush_thread_running; });
} }
}); });
} }
@@ -1120,8 +1129,13 @@ void rawinput::RawInputManager::flush_start() {
void rawinput::RawInputManager::flush_stop() { void rawinput::RawInputManager::flush_stop() {
// set stop flag // set stop flag and wake the flush thread immediately so shutdown
// isn't blocked by the in-progress wait inside the loop above.
{
std::lock_guard<std::mutex> lock(this->flush_thread_m);
this->flush_thread_running = false; this->flush_thread_running = false;
}
this->flush_thread_cv.notify_all();
// check if thread is set // check if thread is set
if (this->flush_thread) { if (this->flush_thread) {
+2
View File
@@ -69,6 +69,8 @@ namespace rawinput {
std::thread *input_thread = nullptr; std::thread *input_thread = nullptr;
std::thread *flush_thread = nullptr; std::thread *flush_thread = nullptr;
bool flush_thread_running = false; bool flush_thread_running = false;
std::mutex flush_thread_m;
std::condition_variable flush_thread_cv;
std::thread *output_thread = nullptr; std::thread *output_thread = nullptr;
std::mutex output_thread_m; std::mutex output_thread_m;
bool output_thread_ready = false; bool output_thread_ready = false;
+20
View File
@@ -41,6 +41,13 @@ typedef enum SPICE_SDK_LOG_LEVEL {
SPICE_SDK_LOG_LEVEL_FATAL = 3, SPICE_SDK_LOG_LEVEL_FATAL = 3,
} SPICE_SDK_LOG_LEVEL; } SPICE_SDK_LOG_LEVEL;
typedef enum SPICE_SDK_TOAST_SEVERITY {
SPICE_SDK_TOAST_LEVEL_INFO = 0,
SPICE_SDK_TOAST_LEVEL_SUCCESS = 1,
SPICE_SDK_TOAST_LEVEL_WARNING = 2,
SPICE_SDK_TOAST_LEVEL_ERROR = 3,
} SPICE_SDK_TOAST_SEVERITY;
typedef struct SPICE_SDK_TOUCH_POINT { typedef struct SPICE_SDK_TOUCH_POINT {
uint32_t id; uint32_t id;
int x; int x;
@@ -219,6 +226,17 @@ typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_set_keypad_func)(
char key char key
); );
// add_toast (v0.2 and up)
// adds an overlay toast notification
//
// severity: see SPICE_SDK_TOAST_SEVERITY; controls the accent color (purely cosmetic)
// text: null-terminated UTF-8 message to display; wraps inside the toast
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_add_toast_func)(
SPICE_SDK_TOAST_SEVERITY severity,
const char *text
);
typedef struct SPICE_SDK_V0 { typedef struct SPICE_SDK_V0 {
uint32_t size; uint32_t size;
@@ -242,6 +260,8 @@ typedef struct SPICE_SDK_V0 {
spice_sdk_insert_card_func *insert_card; spice_sdk_insert_card_func *insert_card;
spice_sdk_set_keypad_func *set_keypad; spice_sdk_set_keypad_func *set_keypad;
spice_sdk_add_toast_func *add_toast;
} SPICE_SDK_V0; } SPICE_SDK_V0;
typedef void (__cdecl spice_sdk_destroy_callback_func)( typedef void (__cdecl spice_sdk_destroy_callback_func)(
+8
View File
@@ -84,7 +84,15 @@ static void worker_thread_main(std::stop_token stop_token) {
spice.set_button(arrow.button, true, 1.f); spice.set_button(arrow.button, true, 1.f);
if (!arrow.previous_state) { if (!arrow.previous_state) {
if (spice.add_toast) {
spice.add_toast(
SPICE_SDK_TOAST_LEVEL_INFO,
std::format("let me hear you say: {}", arrow.name).c_str());
} else {
LOG_INFO(std::format("let me hear you say: {}", arrow.name).c_str()); LOG_INFO(std::format("let me hear you say: {}", arrow.name).c_str());
}
arrow.previous_state = true; arrow.previous_state = true;
} }
} else { } else {
+14 -14
View File
@@ -108,59 +108,59 @@ static unsigned __stdcall worker_thread(void *arg) {
phase += 1; phase += 1;
switch (phase) { switch (phase) {
case 1: case 1:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "get buttons..."); spice.add_toast(SPICE_SDK_TOAST_LEVEL_INFO, "get buttons...");
get_buttons(); get_buttons();
break; break;
case 2: case 2:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "set buttons..."); spice.add_toast(SPICE_SDK_TOAST_LEVEL_WARNING, "set buttons...");
set_buttons(); set_buttons();
break; break;
case 3: case 3:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "clear buttons..."); spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear buttons...");
clear_buttons(); clear_buttons();
break; break;
case 4: case 4:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "get analogs..."); spice.add_toast(SPICE_SDK_TOAST_LEVEL_INFO, "get analogs...");
get_analogs(); get_analogs();
break; break;
case 5: case 5:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "set analogs..."); spice.add_toast(SPICE_SDK_TOAST_LEVEL_WARNING, "set analogs...");
set_analogs(); set_analogs();
break; break;
case 6: case 6:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "clear analogs..."); spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear analogs...");
clear_analogs(); clear_analogs();
break; break;
case 7: case 7:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "get lights..."); spice.add_toast(SPICE_SDK_TOAST_LEVEL_INFO, "get lights...");
get_lights(); get_lights();
break; break;
case 8: case 8:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "set lights..."); spice.add_toast(SPICE_SDK_TOAST_LEVEL_WARNING, "set lights...");
set_lights(); set_lights();
break; break;
case 9: case 9:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "clear lights..."); spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear lights...");
clear_lights(); clear_lights();
break; break;
case 10: case 10:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "set touch..."); spice.add_toast(SPICE_SDK_TOAST_LEVEL_INFO, "set touch...");
set_touch(); set_touch();
break; break;
case 11: case 11:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "clear touch..."); spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear touch...");
clear_touch(); clear_touch();
break; break;
case 12: case 12:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "insert card..."); spice.add_toast(SPICE_SDK_TOAST_LEVEL_SUCCESS, "insert card...");
insert_card(); insert_card();
break; break;
case 13: case 13:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "set keypad..."); spice.add_toast(SPICE_SDK_TOAST_LEVEL_SUCCESS, "set keypad...");
set_keypad(); set_keypad();
break; break;
case 14: case 14:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "clear keypad..."); spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear keypad...");
clear_keypad(); clear_keypad();
break; break;
default: default:
+46
View File
@@ -7,6 +7,7 @@
#include "games/io.h" #include "games/io.h"
#include "launcher/launcher.h" #include "launcher/launcher.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "overlay/notifications.h"
#include "sdk/include/spicesdk.h" #include "sdk/include/spicesdk.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "util/logging.h" #include "util/logging.h"
@@ -29,6 +30,7 @@ static spice_sdk_set_touch_func sdk_set_touch;
static spice_sdk_clear_touch_func sdk_clear_touch; static spice_sdk_clear_touch_func sdk_clear_touch;
static spice_sdk_insert_card_func sdk_insert_card; static spice_sdk_insert_card_func sdk_insert_card;
static spice_sdk_set_keypad_func sdk_set_keypad; static spice_sdk_set_keypad_func sdk_set_keypad;
static spice_sdk_add_toast_func sdk_add_toast;
struct SdkModule { struct SdkModule {
std::string dll; std::string dll;
@@ -169,6 +171,12 @@ sdk_init(
v0->insert_card = sdk_insert_card; v0->insert_card = sdk_insert_card;
v0->set_keypad = sdk_set_keypad; v0->set_keypad = sdk_set_keypad;
// end of 0.1 // end of 0.1
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, add_toast)) {
v0->add_toast = sdk_add_toast;
}
// end of 0.2
// any newer minor iterations will need to check the size // any newer minor iterations will need to check the size
{ {
@@ -594,5 +602,43 @@ sdk_set_keypad(
return SPICE_SDK_STATUS_SUCCESS; return SPICE_SDK_STATUS_SUCCESS;
} }
SPICE_SDK_STATUS_CODE
__cdecl
sdk_add_toast(
SPICE_SDK_TOAST_SEVERITY severity,
const char *text
)
{
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
if (!text) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
}
overlay::notifications::Severity sev;
switch (severity) {
case SPICE_SDK_TOAST_LEVEL_INFO:
sev = overlay::notifications::Severity::Info;
break;
case SPICE_SDK_TOAST_LEVEL_SUCCESS:
sev = overlay::notifications::Severity::Success;
break;
case SPICE_SDK_TOAST_LEVEL_WARNING:
sev = overlay::notifications::Severity::Warning;
break;
case SPICE_SDK_TOAST_LEVEL_ERROR:
sev = overlay::notifications::Severity::Error;
break;
default:
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
}
overlay::notifications::add(sev, text);
return SPICE_SDK_STATUS_SUCCESS;
}
} // namespace sdk } // namespace sdk
+8
View File
@@ -26,6 +26,14 @@ bool fileutils::file_exists(const std::filesystem::path &file_path) {
return file_exists(file_path.c_str()); return file_exists(file_path.c_str());
} }
std::string fileutils::basename(std::string_view path) {
const auto slash = path.find_last_of("\\/");
if (slash == std::string_view::npos) {
return std::string(path);
}
return std::string(path.substr(slash + 1));
}
bool fileutils::verify_header_pe(const std::filesystem::path &file_path) { bool fileutils::verify_header_pe(const std::filesystem::path &file_path) {
if (!file_exists(file_path)) { if (!file_exists(file_path)) {
return false; return false;
+2
View File
@@ -35,4 +35,6 @@ namespace fileutils {
std::filesystem::path get_config_file_path(const std::string module, const std::string filename, bool* file_exists=nullptr); std::filesystem::path get_config_file_path(const std::string module, const std::string filename, bool* file_exists=nullptr);
bool write_config_file(const std::string_view &module, const std::filesystem::path path, std::string text); bool write_config_file(const std::string_view &module, const std::filesystem::path path, std::string text);
std::string basename(std::string_view path);
} }
+55
View File
@@ -44,6 +44,61 @@ namespace memutils {
return get_mem_counters().PrivateUsage; return get_mem_counters().PrivateUsage;
} }
void show_available_memory() {
constexpr double MB = 1024.0 * 1024.0;
auto mem_status = get_mem_status();
const auto pct = [](DWORDLONG used, DWORDLONG total) -> double {
return total ? (100.0 * static_cast<double>(used) / static_cast<double>(total)) : 0.0;
};
log_info("memutils",
"system physical memory: {:.1f} / {:.1f} MiB used ({:.1f}%)",
mem_total_used() / MB, mem_total() / MB,
pct(mem_total_used(), mem_total()));
log_info("memutils",
"system page file: {:.1f} / {:.1f} MiB used ({:.1f}%, {:.1f} MiB commit headroom)",
vmem_total_used() / MB, vmem_total() / MB,
pct(vmem_total_used(), vmem_total()),
mem_status.ullAvailPageFile / MB);
// process virtual address space - 32-bit processes die at ~2 GiB
// (or ~4 GiB with /LARGEADDRESSAWARE) regardless of how much RAM is free
const DWORDLONG va_used = mem_status.ullTotalVirtual - mem_status.ullAvailVirtual;
log_info("memutils",
"process virtual address space: {:.1f} / {:.1f} MiB used ({:.1f}%)",
va_used / MB, mem_status.ullTotalVirtual / MB,
pct(va_used, mem_status.ullTotalVirtual));
// largest contiguous free block in the process address space
// (large allocations can fail due to fragmentation even with plenty available)
MEMORY_BASIC_INFORMATION mbi{};
SIZE_T largest_free = 0;
uintptr_t addr = 0;
const uintptr_t max_addr = static_cast<uintptr_t>(mem_status.ullTotalVirtual);
while (addr < max_addr &&
VirtualQuery(reinterpret_cast<LPCVOID>(addr), &mbi, sizeof(mbi)) == sizeof(mbi)) {
if (mbi.State == MEM_FREE && mbi.RegionSize > largest_free) {
largest_free = mbi.RegionSize;
}
const uintptr_t next = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize;
if (next <= addr) {
break;
}
addr = next;
}
log_info("memutils",
"process largest free virtual block: {:.1f} MiB ({:.1f}% of total VA)",
largest_free / MB,
pct(largest_free, mem_status.ullTotalVirtual));
log_info("memutils",
"process working set: {:.1f} MiB, private bytes: {:.1f} MiB",
mem_used() / MB, vmem_used() / MB);
}
VProtectGuard::VProtectGuard(void *addr, size_t size, DWORD mode, bool reset) VProtectGuard::VProtectGuard(void *addr, size_t size, DWORD mode, bool reset)
: addr(addr), reset(reset), size(size) : addr(addr), reset(reset), size(size)
{ {
+2
View File
@@ -14,6 +14,8 @@ namespace memutils {
DWORDLONG vmem_total_used(); DWORDLONG vmem_total_used();
DWORDLONG vmem_used(); DWORDLONG vmem_used();
void show_available_memory();
/* /*
* Helper class to unprotect/reprotect memory safely. * Helper class to unprotect/reprotect memory safely.
* It will free it's mode override on destruction. * It will free it's mode override on destruction.