mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-02 23:00:42 -07:00
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f955ab984c | |||
| 435b9ce9aa | |||
| 1ec7528dac | |||
| e7822aad8e | |||
| 8140e83e46 | |||
| cc0899290e | |||
| 3012139028 | |||
| c590b87cff | |||
| 5cabd026ae | |||
| 7ccd938f9b | |||
| 88a210e82e | |||
| 74e619df37 | |||
| 10a97b9c63 | |||
| 159043803c | |||
| bd2fbfcb67 | |||
| 64600826b7 | |||
| 80deb4bbbd | |||
| 00c8bb8ce6 | |||
| 09ed8b948c | |||
| e2daefbb6a | |||
| 39bf17aafe | |||
| 4836cd94ab | |||
| 9d41ca2515 | |||
| 18421ffbfe | |||
| 3f0921d983 | |||
| ed7318c270 | |||
| 623e1e3998 | |||
| 558e3d0cb3 | |||
| d0db547dd0 | |||
| be4bb00390 | |||
| a8cc85531c | |||
| 2dea0d27bf | |||
| 61a0220c1a | |||
| c080bbe301 | |||
| e1d1b39567 | |||
| 2aae8ad9ca | |||
| 1b6d1af951 | |||
| 5c3617c05f | |||
| a9a943fbfe | |||
| b094a9e352 | |||
| 4bd18afb12 | |||
| 0b24e703a1 | |||
| 1a1be7a393 | |||
| a2d8f60d12 | |||
| 5d25fdb035 | |||
| c255e3a1db | |||
| d7c144646f | |||
| 27077aa21f | |||
| 632dcf7b41 | |||
| a25bbdc9c9 | |||
| 47d886306e | |||
| edda658fa4 | |||
| 1ab20f1b56 | |||
| 0e9093a74b | |||
| f52eaea76c | |||
| c3fcf2f417 | |||
| 8d04eba21d | |||
| 8d42c0ce77 | |||
| 829503568e | |||
| 746fdb5409 | |||
| 67da892fe9 | |||
| dc191487e0 | |||
| 5c0c405800 | |||
| 60166e6ba1 | |||
| cb0b59600a | |||
| b9f3be4df2 | |||
| cf86fbd238 | |||
| 3fdb369128 | |||
| db7defff5a | |||
| 451cbec0b9 | |||
| 5c69e295ab | |||
| 5065a92d55 | |||
| 5483e8e2c0 | |||
| e8949a2612 | |||
| db71a0b24d | |||
| d3d5422768 | |||
| 5185f753e5 |
@@ -12,12 +12,12 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
working-directory: ./src/spice2x
|
working-directory: ./src/spice2x
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- name: Set ccache environment variables
|
- name: Set ccache environment variables
|
||||||
run: |
|
run: |
|
||||||
echo "CCACHE_DIR=${{ github.workspace }}/src/spice2x/.ccache" >> $GITHUB_ENV
|
echo "CCACHE_DIR=${{ github.workspace }}/src/spice2x/.ccache" >> $GITHUB_ENV
|
||||||
- name: Install ccache
|
- name: Install ccache
|
||||||
uses: hendrikmuhs/ccache-action@v1.2
|
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||||
- name: Calculate commit SHA
|
- name: Calculate commit SHA
|
||||||
id: vars
|
id: vars
|
||||||
run: |
|
run: |
|
||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
|
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
|
||||||
- name: Compile
|
- name: Compile
|
||||||
run: ./build_docker.sh
|
run: ./build_docker.sh
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
|
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
|
||||||
path: src/spice2x/bin
|
path: src/spice2x/bin
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: Draft Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Build and Draft Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./src/spice2x
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Clean leftover build artifacts
|
||||||
|
run: |
|
||||||
|
rm -rf .ccache dist bin cmake-build-*
|
||||||
|
- name: Compile
|
||||||
|
run: ./build_docker.sh
|
||||||
|
- name: Determine release name from dist filename
|
||||||
|
run: |
|
||||||
|
dist=$(basename "$(ls dist/spice2x-*.zip | grep -v -- '-full.zip')")
|
||||||
|
# strip the ".zip" to get the base name, e.g. spice2x-26-06-28
|
||||||
|
name="${dist%.zip}"
|
||||||
|
# the tag is the date portion, e.g. 26-06-28
|
||||||
|
tag="${name#spice2x-}"
|
||||||
|
echo "RELEASE_NAME=$name" >> $GITHUB_ENV
|
||||||
|
echo "RELEASE_TAG=$tag" >> $GITHUB_ENV
|
||||||
|
- name: Create draft release
|
||||||
|
uses: softprops/action-gh-release@v3
|
||||||
|
with:
|
||||||
|
draft: true
|
||||||
|
prerelease: true
|
||||||
|
tag_name: ${{ env.RELEASE_TAG }}
|
||||||
|
name: ${{ env.RELEASE_NAME }}
|
||||||
|
target_commitish: main
|
||||||
|
generate_release_notes: true
|
||||||
|
files: |
|
||||||
|
src/spice2x/dist/spice2x-*.zip
|
||||||
+7
-3
@@ -11,13 +11,17 @@ To contribute, fork the repo (just the main branch), make changes in your fork,
|
|||||||
* For a new game that was never supported (not a new version of a game, but rather a new series): please wait 1 year after official AC release in Japan.
|
* For a new game that was never supported (not a new version of a game, but rather a new series): please wait 1 year after official AC release in Japan.
|
||||||
* For new version of an already supported game: proceed with caution and use generally-accepted community guidelines.
|
* For new version of an already supported game: proceed with caution and use generally-accepted community guidelines.
|
||||||
|
|
||||||
### Forbidden "features" that will never be accepted
|
### Pull requests that will not be accepted
|
||||||
|
|
||||||
* Performing decryption of encrypted files
|
* Performing decryption of encrypted files.
|
||||||
* Including (bundling), distributing, or linking to game data, or making it easier to find/download game data
|
* Including (bundling), distributing, or linking to game data, or making it easier to find/download game data.
|
||||||
* Any features that [phone home](https://en.wikipedia.org/wiki/Phoning_home)
|
* Any features that [phone home](https://en.wikipedia.org/wiki/Phoning_home)
|
||||||
* This includes automatic updaters.
|
* This includes automatic updaters.
|
||||||
* Exception: we have allowed the "patch import from URL" feature, but this has only been done under very careful considerations and plenty of warnings to the user.
|
* Exception: we have allowed the "patch import from URL" feature, but this has only been done under very careful considerations and plenty of warnings to the user.
|
||||||
|
* Built-in EA server that saves data. They should live in a separate project.
|
||||||
|
* Any additions to built-in patches (modifications / removals are OK if we have a good reason).
|
||||||
|
* Changes to the software license terms.
|
||||||
|
* Localization / translation for UI text. It becomes too difficult to manage by the maintainers.
|
||||||
|
|
||||||
### Avoiding regressions
|
### Avoiding regressions
|
||||||
|
|
||||||
|
|||||||
@@ -408,13 +408,17 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
games/iidx/mf_wrappers.cpp
|
games/iidx/mf_wrappers.cpp
|
||||||
games/sdvx/bi2x_hook.cpp
|
games/sdvx/bi2x_hook.cpp
|
||||||
games/sdvx/sdvx.cpp
|
games/sdvx/sdvx.cpp
|
||||||
|
games/sdvx/sdvx_live2d.cpp
|
||||||
games/sdvx/io.cpp
|
games/sdvx/io.cpp
|
||||||
games/sdvx/camera.cpp
|
games/sdvx/camera.cpp
|
||||||
|
games/jb/bi2x_hook.cpp
|
||||||
games/jb/jb.cpp
|
games/jb/jb.cpp
|
||||||
|
games/jb/jb_touch.cpp
|
||||||
games/jb/io.cpp
|
games/jb/io.cpp
|
||||||
games/nost/nost.cpp
|
games/nost/nost.cpp
|
||||||
games/nost/io.cpp
|
games/nost/io.cpp
|
||||||
games/nost/poke.cpp
|
games/nost/poke.cpp
|
||||||
|
games/nost/touch_mode.cpp
|
||||||
games/gitadora/gitadora.cpp
|
games/gitadora/gitadora.cpp
|
||||||
games/gitadora/asio.cpp
|
games/gitadora/asio.cpp
|
||||||
games/gitadora/io.cpp
|
games/gitadora/io.cpp
|
||||||
@@ -430,6 +434,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
games/rb/rb.cpp
|
games/rb/rb.cpp
|
||||||
games/rb/io.cpp
|
games/rb/io.cpp
|
||||||
games/rb/touch.cpp
|
games/rb/touch.cpp
|
||||||
|
games/rb/touch_debug.cpp
|
||||||
games/bs/bs.cpp
|
games/bs/bs.cpp
|
||||||
games/bs/io.cpp
|
games/bs/io.cpp
|
||||||
games/rf3d/rf3d.cpp
|
games/rf3d/rf3d.cpp
|
||||||
@@ -499,6 +504,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
hooks/audio/buffer.cpp
|
hooks/audio/buffer.cpp
|
||||||
hooks/audio/mme.cpp
|
hooks/audio/mme.cpp
|
||||||
hooks/audio/util.cpp
|
hooks/audio/util.cpp
|
||||||
|
hooks/audio/xact.cpp
|
||||||
hooks/audio/backends/dsound/dsound_backend.cpp
|
hooks/audio/backends/dsound/dsound_backend.cpp
|
||||||
hooks/audio/backends/mmdevice/audio_endpoint_volume.cpp
|
hooks/audio/backends/mmdevice/audio_endpoint_volume.cpp
|
||||||
hooks/audio/backends/mmdevice/device.cpp
|
hooks/audio/backends/mmdevice/device.cpp
|
||||||
@@ -525,10 +531,13 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
hooks/devicehook.cpp
|
hooks/devicehook.cpp
|
||||||
hooks/graphics/graphics.cpp
|
hooks/graphics/graphics.cpp
|
||||||
hooks/graphics/graphics_windowed.cpp
|
hooks/graphics/graphics_windowed.cpp
|
||||||
|
hooks/graphics/nvapi_impl.cpp
|
||||||
hooks/graphics/nvapi_hook.cpp
|
hooks/graphics/nvapi_hook.cpp
|
||||||
hooks/graphics/nvenc_hook.cpp
|
hooks/graphics/nvenc_hook.cpp
|
||||||
hooks/graphics/backends/d3d9/d3d9_backend.cpp
|
hooks/graphics/backends/d3d9/d3d9_backend.cpp
|
||||||
hooks/graphics/backends/d3d9/d3d9_device.cpp
|
hooks/graphics/backends/d3d9/d3d9_device.cpp
|
||||||
|
hooks/graphics/backends/d3d9/d3d9_gfdm.cpp
|
||||||
|
hooks/graphics/backends/d3d9/d3d9_live2d.cpp
|
||||||
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
|
||||||
@@ -567,7 +576,6 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
misc/device.cpp
|
misc/device.cpp
|
||||||
misc/eamuse.cpp
|
misc/eamuse.cpp
|
||||||
misc/extdev.cpp
|
misc/extdev.cpp
|
||||||
misc/nativetouchhook.cpp
|
|
||||||
misc/sciunit.cpp
|
misc/sciunit.cpp
|
||||||
misc/sde.cpp
|
misc/sde.cpp
|
||||||
misc/wintouchemu.cpp
|
misc/wintouchemu.cpp
|
||||||
@@ -605,12 +613,27 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
overlay/windows/keypad.cpp
|
overlay/windows/keypad.cpp
|
||||||
overlay/windows/log.cpp
|
overlay/windows/log.cpp
|
||||||
overlay/windows/midi.cpp
|
overlay/windows/midi.cpp
|
||||||
|
overlay/windows/nostalgia_touch_piano.cpp
|
||||||
|
overlay/windows/obs.cpp
|
||||||
|
overlay/windows/obs_websocket.cpp
|
||||||
overlay/windows/patch_manager.cpp
|
overlay/windows/patch_manager.cpp
|
||||||
overlay/windows/popn_sub.cpp
|
overlay/windows/popn_sub.cpp
|
||||||
overlay/windows/wnd_manager.cpp
|
overlay/windows/wnd_manager.cpp
|
||||||
|
|
||||||
|
# patcher
|
||||||
|
patcher/config.cpp
|
||||||
|
patcher/lifecycle.cpp
|
||||||
|
patcher/loader.cpp
|
||||||
|
patcher/loader_group.cpp
|
||||||
|
patcher/runtime.cpp
|
||||||
|
|
||||||
|
# external
|
||||||
|
external/easywsclient/easywsclient.cpp
|
||||||
|
|
||||||
# rawinput
|
# rawinput
|
||||||
rawinput/rawinput.cpp
|
rawinput/rawinput.cpp
|
||||||
|
rawinput/rawinput_handles.cpp
|
||||||
|
rawinput/midi.cpp
|
||||||
rawinput/sextet.cpp
|
rawinput/sextet.cpp
|
||||||
rawinput/piuio.cpp
|
rawinput/piuio.cpp
|
||||||
rawinput/touch.cpp
|
rawinput/touch.cpp
|
||||||
@@ -637,8 +660,14 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
stubs/stubs.cpp
|
stubs/stubs.cpp
|
||||||
|
|
||||||
# touch
|
# touch
|
||||||
|
touch/gdi_overlay.cpp
|
||||||
|
touch/native/inject.cpp
|
||||||
|
touch/native/inject_mouse.cpp
|
||||||
|
touch/native/inject_synthetic.cpp
|
||||||
|
touch/native/nativetouchhook.cpp
|
||||||
|
touch/native/transform.cpp
|
||||||
touch/touch.cpp
|
touch/touch.cpp
|
||||||
touch/touch_indicators.cpp
|
touch/touch_gestures.cpp
|
||||||
touch/win7.cpp
|
touch/win7.cpp
|
||||||
touch/win8.cpp
|
touch/win8.cpp
|
||||||
|
|
||||||
@@ -671,6 +700,33 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
|
|
||||||
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "Source Files" FILES ${SOURCE_FILES})
|
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "Source Files" FILES ${SOURCE_FILES})
|
||||||
|
|
||||||
|
# guard against statically importing DLLs that must always be loaded dynamically:
|
||||||
|
# * DLLs users override via the modules directory (e.g. DXVK's d3d9.dll) - a
|
||||||
|
# static import loads the system copy at startup and preempts the override
|
||||||
|
# (issue #779).
|
||||||
|
# * Media Foundation DLLs (mf/mfplat/mfreadwrite) - a static import breaks
|
||||||
|
# Unity games.
|
||||||
|
# the check runs objdump on each produced binary and fails the build if any
|
||||||
|
# forbidden DLL is imported.
|
||||||
|
set(SPICE_FORBIDDEN_STATIC_IMPORTS
|
||||||
|
d3d8.dll d3d9.dll d3d10core.dll d3d11.dll dxgi.dll opengl32.dll
|
||||||
|
mf.dll mfplat.dll mfreadwrite.dll)
|
||||||
|
|
||||||
|
function(spice_guard_dll_imports target)
|
||||||
|
if(MSVC)
|
||||||
|
# objdump-based parsing assumes a GNU/LLVM toolchain
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
add_custom_command(TARGET ${target} POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND}
|
||||||
|
-DOBJDUMP=${CMAKE_OBJDUMP}
|
||||||
|
-DTARGET_FILE=$<TARGET_FILE:${target}>
|
||||||
|
"-DFORBIDDEN=${SPICE_FORBIDDEN_STATIC_IMPORTS}"
|
||||||
|
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_no_static_dll_imports.cmake
|
||||||
|
VERBATIM
|
||||||
|
COMMENT "Checking ${target} for forbidden static DLL imports")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
# spice.exe / spice_laa.exe shared objects
|
# spice.exe / spice_laa.exe shared objects
|
||||||
###########################################
|
###########################################
|
||||||
# spice.exe and spice_laa.exe are compiled identically; the only difference is
|
# spice.exe and spice_laa.exe are compiled identically; the only difference is
|
||||||
@@ -955,3 +1011,16 @@ set_target_properties(spicetools_spice64 spicetools_spice64_linux spicetools_stu
|
|||||||
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive64"
|
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive64"
|
||||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64"
|
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64"
|
||||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64")
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64")
|
||||||
|
|
||||||
|
# forbidden static DLL import guard
|
||||||
|
###################################
|
||||||
|
# apply the check to every executable and DLL produced by this project.
|
||||||
|
get_property(spice_all_targets DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY BUILDSYSTEM_TARGETS)
|
||||||
|
foreach(spice_target IN LISTS spice_all_targets)
|
||||||
|
get_target_property(spice_target_type ${spice_target} TYPE)
|
||||||
|
if(spice_target_type STREQUAL "EXECUTABLE"
|
||||||
|
OR spice_target_type STREQUAL "SHARED_LIBRARY"
|
||||||
|
OR spice_target_type STREQUAL "MODULE_LIBRARY")
|
||||||
|
spice_guard_dll_imports(${spice_target})
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
|
#include "touch/native/nativetouchhook.h"
|
||||||
|
|
||||||
// static stuff
|
// static stuff
|
||||||
static int ACIO_WARMUP = 0;
|
static int ACIO_WARMUP = 0;
|
||||||
@@ -140,8 +141,9 @@ static int __cdecl ac_io_update(int a1) {
|
|||||||
// flush device output
|
// flush device output
|
||||||
RI_MGR->devices_flush_output();
|
RI_MGR->devices_flush_output();
|
||||||
|
|
||||||
// update wintouchemu
|
// update touch emulation
|
||||||
wintouchemu::update();
|
wintouchemu::update();
|
||||||
|
nativetouch::refresh_contact_lifetime();
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
#include "games/nost/io.h"
|
#include "games/nost/io.h"
|
||||||
#include "games/nost/nost.h"
|
#include "games/nost/nost.h"
|
||||||
|
#include "games/nost/touch_mode.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ using namespace GameAPI;
|
|||||||
// static stuff
|
// static stuff
|
||||||
static uint8_t STATUS_BUFFER[277];
|
static uint8_t STATUS_BUFFER[277];
|
||||||
static bool STATUS_BUFFER_FREEZE = false;
|
static bool STATUS_BUFFER_FREEZE = false;
|
||||||
|
static constexpr uint8_t NOST_TOUCH_PIANO_VELOCITY = 11;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Implementations
|
* Implementations
|
||||||
@@ -226,6 +228,8 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
|
|||||||
games::nost::Analogs::Key27, games::nost::Analogs::Key28,
|
games::nost::Analogs::Key27, games::nost::Analogs::Key28,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const auto touch_key_state = games::nost::touch_mode::piano_key_state();
|
||||||
|
|
||||||
// iterate pairs of keys
|
// iterate pairs of keys
|
||||||
for (size_t key_pair = 0; key_pair < 28 / 2; key_pair++) {
|
for (size_t key_pair = 0; key_pair < 28 / 2; key_pair++) {
|
||||||
|
|
||||||
@@ -257,6 +261,10 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
|
|||||||
if (button0 > 0) {
|
if (button0 > 0) {
|
||||||
state0 = button0;
|
state0 = button0;
|
||||||
}
|
}
|
||||||
|
if ((touch_key_state & (UINT32_C(1) << (key_pair * 2))) &&
|
||||||
|
state0 < NOST_TOUCH_PIANO_VELOCITY) {
|
||||||
|
state0 = NOST_TOUCH_PIANO_VELOCITY;
|
||||||
|
}
|
||||||
|
|
||||||
const auto button1 = panb_get_button_velocity(
|
const auto button1 = panb_get_button_velocity(
|
||||||
buttons.at(button_mapping[key_pair * 2 + 1]),
|
buttons.at(button_mapping[key_pair * 2 + 1]),
|
||||||
@@ -267,6 +275,10 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
|
|||||||
if (button1 > 0) {
|
if (button1 > 0) {
|
||||||
state1 = button1;
|
state1 = button1;
|
||||||
}
|
}
|
||||||
|
if ((touch_key_state & (UINT32_C(1) << (key_pair * 2 + 1))) &&
|
||||||
|
state1 < NOST_TOUCH_PIANO_VELOCITY) {
|
||||||
|
state1 = NOST_TOUCH_PIANO_VELOCITY;
|
||||||
|
}
|
||||||
|
|
||||||
// build value
|
// build value
|
||||||
uint8_t value = 0;
|
uint8_t value = 0;
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ bool ICCADevice::parse_msg(MessageData *msg_in,
|
|||||||
// SDVX Old cabinet mode
|
// SDVX Old cabinet mode
|
||||||
if (avs::game::is_model("KFC") && avs::game::SPEC[0] != 'G' && avs::game::SPEC[0] != 'H')
|
if (avs::game::is_model("KFC") && avs::game::SPEC[0] != 'G' && avs::game::SPEC[0] != 'H')
|
||||||
answer_type = 1;
|
answer_type = 1;
|
||||||
if (avs::game::is_model("L44"))
|
if (avs::game::is_model({ "L44", "T44" }))
|
||||||
answer_type = 2;
|
answer_type = 2;
|
||||||
|
|
||||||
// check answer type
|
// check answer type
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
#include "keypads.h"
|
#include "keypads.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "external/rapidjson/document.h"
|
#include "external/rapidjson/document.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "util/precise_timer.h"
|
|
||||||
|
|
||||||
using namespace std::placeholders;
|
using namespace std::placeholders;
|
||||||
using namespace rapidjson;
|
using namespace rapidjson;
|
||||||
@@ -59,7 +60,6 @@ namespace api::modules {
|
|||||||
// get params
|
// get params
|
||||||
auto keypad = req.params[0].GetUint();
|
auto keypad = req.params[0].GetUint();
|
||||||
auto input = std::string(req.params[1].GetString());
|
auto input = std::string(req.params[1].GetString());
|
||||||
timeutils::PreciseSleepTimer timer;
|
|
||||||
|
|
||||||
// process all chars
|
// process all chars
|
||||||
for (auto c : input) {
|
for (auto c : input) {
|
||||||
@@ -93,11 +93,11 @@ namespace api::modules {
|
|||||||
|
|
||||||
// set
|
// set
|
||||||
eamuse_set_keypad_overrides(keypad, state);
|
eamuse_set_keypad_overrides(keypad, state);
|
||||||
timer.sleep(sleep_time);
|
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
|
||||||
|
|
||||||
// unset
|
// unset
|
||||||
eamuse_set_keypad_overrides(keypad, 0);
|
eamuse_set_keypad_overrides(keypad, 0);
|
||||||
timer.sleep(sleep_time);
|
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "touch.h"
|
#include "touch.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include "external/rapidjson/document.h"
|
#include "external/rapidjson/document.h"
|
||||||
@@ -8,6 +10,8 @@
|
|||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
|
#include "touch/native/inject.h"
|
||||||
|
#include "touch/native/nativetouchhook.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
#include "games/iidx/iidx.h"
|
#include "games/iidx/iidx.h"
|
||||||
|
|
||||||
@@ -17,6 +21,25 @@ using namespace rapidjson;
|
|||||||
|
|
||||||
namespace api::modules {
|
namespace api::modules {
|
||||||
|
|
||||||
|
static bool inject_native_touch(
|
||||||
|
const std::vector<TouchPoint> &touch_points, int canvas_w, int canvas_h) {
|
||||||
|
if (touch_points.empty()) {
|
||||||
|
// no active touches remain; release the synthetic contact
|
||||||
|
return nativetouch::inject::inject_synthetic_touch({ 0, 0 }, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// only a single synthetic contact is supported, so just use the first touch point
|
||||||
|
const auto &touch = touch_points.front();
|
||||||
|
POINT position { touch.x, touch.y };
|
||||||
|
if (canvas_w > 0 && canvas_h > 0) {
|
||||||
|
return nativetouch::inject::inject_synthetic_touch_from_canvas(
|
||||||
|
position,
|
||||||
|
{ canvas_w, canvas_h },
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
return nativetouch::inject::inject_synthetic_touch(position, true);
|
||||||
|
}
|
||||||
|
|
||||||
Touch::Touch() : Module("touch") {
|
Touch::Touch() : Module("touch") {
|
||||||
is_sdvx = avs::game::is_model("KFC");
|
is_sdvx = avs::game::is_model("KFC");
|
||||||
|
|
||||||
@@ -26,6 +49,25 @@ namespace api::modules {
|
|||||||
is_tdj_fhd = false;
|
is_tdj_fhd = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use_native = nativetouch::is_hooked();
|
||||||
|
|
||||||
|
// resolution the API/companion sends native touch coordinates in (after errata)
|
||||||
|
native_canvas_w = 0;
|
||||||
|
native_canvas_h = 0;
|
||||||
|
if (is_sdvx) {
|
||||||
|
// exceed gear subscreen, portrait after the rotation applied in apply_touch_errata
|
||||||
|
native_canvas_w = 1080;
|
||||||
|
native_canvas_h = 1920;
|
||||||
|
} else if (avs::game::is_model("LDJ")) {
|
||||||
|
// TDJ subscreen; FHD models are upscaled to 1080p by apply_touch_errata
|
||||||
|
native_canvas_w = is_tdj_fhd ? 1920 : 1280;
|
||||||
|
native_canvas_h = is_tdj_fhd ? 1080 : 720;
|
||||||
|
} else if (avs::game::is_model("M39")) {
|
||||||
|
// pop'n music API touch surface
|
||||||
|
native_canvas_w = 1280;
|
||||||
|
native_canvas_h = 800;
|
||||||
|
}
|
||||||
|
|
||||||
functions["read"] = std::bind(&Touch::read, this, _1, _2);
|
functions["read"] = std::bind(&Touch::read, this, _1, _2);
|
||||||
functions["write"] = std::bind(&Touch::write, this, _1, _2);
|
functions["write"] = std::bind(&Touch::write, this, _1, _2);
|
||||||
functions["write_reset"] = std::bind(&Touch::write_reset, this, _1, _2);
|
functions["write_reset"] = std::bind(&Touch::write_reset, this, _1, _2);
|
||||||
@@ -99,8 +141,12 @@ namespace api::modules {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// apply touch points
|
// apply touch points
|
||||||
|
if (use_native) {
|
||||||
|
write_native(touch_points);
|
||||||
|
} else {
|
||||||
touch_write_points(&touch_points);
|
touch_write_points(&touch_points);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* write_reset(id: uint, ...)
|
* write_reset(id: uint, ...)
|
||||||
@@ -123,8 +169,44 @@ namespace api::modules {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// remove all IDs
|
// remove all IDs
|
||||||
|
if (use_native) {
|
||||||
|
write_reset_native(touch_point_ids);
|
||||||
|
} else {
|
||||||
touch_remove_points(&touch_point_ids);
|
touch_remove_points(&touch_point_ids);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Touch::write_native(const std::vector<TouchPoint> &touch_points) {
|
||||||
|
if (touch_points.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(native_touch_mutex);
|
||||||
|
const auto touch_id = touch_points.front().id;
|
||||||
|
if (native_touch_id && *native_touch_id != touch_id) {
|
||||||
|
if (!inject_native_touch({}, native_canvas_w, native_canvas_h)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
native_touch_id.reset();
|
||||||
|
}
|
||||||
|
if (inject_native_touch(touch_points, native_canvas_w, native_canvas_h)) {
|
||||||
|
native_touch_id = touch_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Touch::write_reset_native(const std::vector<DWORD> &touch_point_ids) {
|
||||||
|
std::lock_guard<std::mutex> lock(native_touch_mutex);
|
||||||
|
if (!native_touch_id ||
|
||||||
|
std::find(
|
||||||
|
touch_point_ids.begin(),
|
||||||
|
touch_point_ids.end(),
|
||||||
|
*native_touch_id) == touch_point_ids.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (inject_native_touch({}, native_canvas_w, native_canvas_h)) {
|
||||||
|
native_touch_id.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Touch::apply_touch_errata(int &x, int &y) {
|
void Touch::apply_touch_errata(int &x, int &y) {
|
||||||
int x_raw = x;
|
int x_raw = x;
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "api/module.h"
|
#include "api/module.h"
|
||||||
#include "api/request.h"
|
#include "api/request.h"
|
||||||
|
#include "touch/touch.h"
|
||||||
|
|
||||||
namespace api::modules {
|
namespace api::modules {
|
||||||
|
|
||||||
@@ -13,11 +17,21 @@ namespace api::modules {
|
|||||||
private:
|
private:
|
||||||
bool is_sdvx;
|
bool is_sdvx;
|
||||||
bool is_tdj_fhd;
|
bool is_tdj_fhd;
|
||||||
|
bool use_native;
|
||||||
|
std::mutex native_touch_mutex;
|
||||||
|
std::optional<uint32_t> native_touch_id;
|
||||||
|
|
||||||
|
// resolution the API/companion sends native touch coordinates in (after errata);
|
||||||
|
// used to normalize before mapping into the native injection window
|
||||||
|
int native_canvas_w;
|
||||||
|
int native_canvas_h;
|
||||||
|
|
||||||
// function definitions
|
// function definitions
|
||||||
void read(Request &req, Response &res);
|
void read(Request &req, Response &res);
|
||||||
void write(Request &req, Response &res);
|
void write(Request &req, Response &res);
|
||||||
void write_reset(Request &req, Response &res);
|
void write_reset(Request &req, Response &res);
|
||||||
|
void write_native(const std::vector<TouchPoint> &touch_points);
|
||||||
|
void write_reset_native(const std::vector<DWORD> &touch_point_ids);
|
||||||
void apply_touch_errata(int &x, int &y);
|
void apply_touch_errata(int &x, int &y);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "serial.h"
|
#include "serial.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/precise_timer.h"
|
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
|
||||||
|
|
||||||
@@ -17,7 +18,6 @@ namespace api {
|
|||||||
controller->init_state(this->state);
|
controller->init_state(this->state);
|
||||||
this->thread = new std::thread([this] () {
|
this->thread = new std::thread([this] () {
|
||||||
log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud);
|
log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud);
|
||||||
timeutils::PreciseSleepTimer timer;
|
|
||||||
|
|
||||||
// read buffer
|
// read buffer
|
||||||
uint8_t read_buffer[16*1024];
|
uint8_t read_buffer[16*1024];
|
||||||
@@ -162,7 +162,7 @@ namespace api {
|
|||||||
|
|
||||||
// slow down on reconnect
|
// slow down on reconnect
|
||||||
if (this->running) {
|
if (this->running) {
|
||||||
timer.sleep(retry_time);
|
std::this_thread::sleep_for(std::chrono::milliseconds(retry_time));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1114,6 +1114,7 @@ namespace avs {
|
|||||||
#endif
|
#endif
|
||||||
// jubeat
|
// jubeat
|
||||||
{"jubeat.dll", 0x2000000},
|
{"jubeat.dll", 0x2000000},
|
||||||
|
{"jubeat2019.dll", 0x10000000},
|
||||||
|
|
||||||
// MUSECA
|
// MUSECA
|
||||||
{"museca.dll", 0xC000000},
|
{"museca.dll", 0xC000000},
|
||||||
@@ -1924,7 +1925,7 @@ namespace avs {
|
|||||||
#if !SPICE64
|
#if !SPICE64
|
||||||
// sdvx4 bad log config fix
|
// sdvx4 bad log config fix
|
||||||
if (avs::game::DLL_NAME == "soundvoltex.dll" && // it's too early for avs::game::is_model
|
if (avs::game::DLL_NAME == "soundvoltex.dll" && // it's too early for avs::game::is_model
|
||||||
property_search_safe(config, config_node, "/log/enable_console")) {
|
property_search(config, config_node, "/log/enable_console")) {
|
||||||
log_info("avs-core", "applying SDVX4 avs-config.xml fix for <log><enable_console>");
|
log_info("avs-core", "applying SDVX4 avs-config.xml fix for <log><enable_console>");
|
||||||
property_search_remove_safe(config, config_node, "/log/enable_console");
|
property_search_remove_safe(config, config_node, "/log/enable_console");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -591,6 +591,7 @@ namespace avs {
|
|||||||
// for proper reporting of Omnimix and other song packs
|
// for proper reporting of Omnimix and other song packs
|
||||||
if (_stricmp(EA3_MODEL, "LDJ") == 0 ||
|
if (_stricmp(EA3_MODEL, "LDJ") == 0 ||
|
||||||
_stricmp(EA3_MODEL, "L44") == 0 ||
|
_stricmp(EA3_MODEL, "L44") == 0 ||
|
||||||
|
_stricmp(EA3_MODEL, "T44") == 0 ||
|
||||||
_stricmp(EA3_MODEL, "M39") == 0 ||
|
_stricmp(EA3_MODEL, "M39") == 0 ||
|
||||||
_stricmp(EA3_MODEL, "KFC") == 0)
|
_stricmp(EA3_MODEL, "KFC") == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -214,6 +214,19 @@ time (
|
|||||||
echo "==========================="
|
echo "==========================="
|
||||||
)
|
)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Checking Win7 compatibility..."
|
||||||
|
echo "============================="
|
||||||
|
if ! command -v windows_dll_compat_checker &> /dev/null; then
|
||||||
|
echo "WARNING: windows_dll_compat_checker not found, skipping Win7 compatibility check"
|
||||||
|
else
|
||||||
|
windows_dll_compat_checker -s PREMADE/konami_win7_museca_x86_64.ini \
|
||||||
|
${BUILDDIR_64}/spicetools/64/spice64.exe
|
||||||
|
windows_dll_compat_checker -s PREMADE/konami_win7_museca_x86_64_32bit_dlls.ini \
|
||||||
|
${BUILDDIR_32}/spicetools/spicecfg.exe \
|
||||||
|
${BUILDDIR_32}/spicetools/32/spice.exe
|
||||||
|
fi
|
||||||
|
|
||||||
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
|
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
|
||||||
then
|
then
|
||||||
echo ""
|
echo ""
|
||||||
@@ -229,7 +242,7 @@ then
|
|||||||
fi
|
fi
|
||||||
if ((BUILD_XP_32 > 0))
|
if ((BUILD_XP_32 > 0))
|
||||||
then
|
then
|
||||||
windows_dll_compat_checker -s PREMADE/winxp_x86_64_32bit_dlls.ini \
|
windows_dll_compat_checker -s PREMADE/konami_winxp_jubeat_i686.ini \
|
||||||
${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe \
|
${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe \
|
||||||
${BUILDDIR_WINXP_32}/spicetools/32/spice.exe
|
${BUILDDIR_WINXP_32}/spicetools/32/spice.exe
|
||||||
fi
|
fi
|
||||||
@@ -294,6 +307,7 @@ mkdir -p ${OUTDIR_EXTRAS}/largeaddressaware
|
|||||||
mkdir -p ${OUTDIR_EXTRAS}/linux
|
mkdir -p ${OUTDIR_EXTRAS}/linux
|
||||||
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/32
|
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/32
|
||||||
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/64
|
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/64
|
||||||
|
mkdir -p ${OUTDIR_EXTRAS}/updater
|
||||||
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
|
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
|
||||||
then
|
then
|
||||||
mkdir -p ${OUTDIR_EXTRAS}/winxp
|
mkdir -p ${OUTDIR_EXTRAS}/winxp
|
||||||
@@ -354,6 +368,9 @@ mkdir ${OUTDIR_EXTRAS}/api
|
|||||||
find ./api/resources/python -name "__pycache__" -exec rm -rf {} +
|
find ./api/resources/python -name "__pycache__" -exec rm -rf {} +
|
||||||
cp -r ./api/resources/* ${OUTDIR_EXTRAS}/api
|
cp -r ./api/resources/* ${OUTDIR_EXTRAS}/api
|
||||||
|
|
||||||
|
# generate the standalone updater scripts from the shared template and drop them
|
||||||
|
bash ./build_updaters.sh ${OUTDIR_EXTRAS}/updater
|
||||||
|
|
||||||
# build distribution archive
|
# build distribution archive
|
||||||
if ((DIST_ENABLE > 0))
|
if ((DIST_ENABLE > 0))
|
||||||
then
|
then
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# builds the standalone updater scripts from the shared PowerShell logic in
|
||||||
|
# update_spice.ps1. each output is a self-contained polyglot .bat: a small batch
|
||||||
|
# header (with the release channel baked in) followed by the shared PowerShell
|
||||||
|
# body after the marker line.
|
||||||
|
#
|
||||||
|
# usage: build_updaters.sh OUT_DIR
|
||||||
|
# OUT_DIR directory to write the generated .bat files into
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if [ $# -lt 1 ]; then
|
||||||
|
echo "usage: build_updaters.sh OUT_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PS_BODY="${SRC_DIR}/update_spice.ps1"
|
||||||
|
OUT_DIR="$1"
|
||||||
|
|
||||||
|
[ -f "$PS_BODY" ] || { echo "error: $PS_BODY not found" >&2; exit 1; }
|
||||||
|
|
||||||
|
MARKER='#___PS___'
|
||||||
|
|
||||||
|
gen() {
|
||||||
|
channel="$1"; label="$2"; out="$3"
|
||||||
|
{
|
||||||
|
cat <<HEADER
|
||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
REM ============================================================================
|
||||||
|
REM spice2x updater - updates spice.exe, spice64.exe and spicecfg.exe in
|
||||||
|
REM this folder to the latest spice2x release from GitHub.
|
||||||
|
REM
|
||||||
|
REM release channel: ${label}
|
||||||
|
REM make sure spice/spicecfg are NOT running before updating.
|
||||||
|
REM ============================================================================
|
||||||
|
|
||||||
|
REM expose the script folder + channel to the embedded PowerShell, then run the
|
||||||
|
REM PowerShell part: re-read this file and execute everything after the marker.
|
||||||
|
set "SPICE_DIR=%~dp0"
|
||||||
|
set "SPICE_CHANNEL=${channel}"
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "\$m='#___'+'PS'+'___'; \$code=((Get-Content -LiteralPath '%~f0' -Raw) -split \$m)[-1]; Invoke-Expression \$code"
|
||||||
|
set "RC=%ERRORLEVEL%"
|
||||||
|
endlocal & exit /b %RC%
|
||||||
|
|
||||||
|
${MARKER}
|
||||||
|
HEADER
|
||||||
|
cat "$PS_BODY"
|
||||||
|
} | sed -e 's/\r$//' -e 's/$/\r/' > "${OUT_DIR}/${out}"
|
||||||
|
echo " generated ${OUT_DIR}/${out}"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Generating updater scripts from $(basename "$PS_BODY")..."
|
||||||
|
gen "" "stable (pre-releases excluded)" "update_spice.bat"
|
||||||
|
gen "beta" "beta (newest, pre-releases included)" "update_spice_beta.bat"
|
||||||
+72
-11
@@ -3,7 +3,9 @@
|
|||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
|
#include "games/io.h"
|
||||||
#include "launcher/superexit.h"
|
#include "launcher/superexit.h"
|
||||||
|
#include "misc/eamuse.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
#include "rawinput/piuio.h"
|
#include "rawinput/piuio.h"
|
||||||
#include "util/time.h"
|
#include "util/time.h"
|
||||||
@@ -62,7 +64,43 @@ std::vector<Button> GameAPI::Buttons::sortButtons(
|
|||||||
return sorted;
|
return sorted;
|
||||||
}
|
}
|
||||||
|
|
||||||
GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *manager, Button &_button, bool check_alts) {
|
namespace GameAPI::Buttons {
|
||||||
|
static State get_button_state(
|
||||||
|
rawinput::RawInputManager *manager,
|
||||||
|
Button &button,
|
||||||
|
bool check_alts,
|
||||||
|
bool check_modifiers);
|
||||||
|
|
||||||
|
static bool modifiers_pressed(rawinput::RawInputManager *manager, Button &button);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameAPI::Buttons::modifiers_pressed(rawinput::RawInputManager *manager, Button &button) {
|
||||||
|
const auto modifier_mask = button.getModifierMask();
|
||||||
|
if (modifier_mask == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *modifier_buttons = games::get_buttons_modifiers(eamuse_get_game());
|
||||||
|
if (!modifier_buttons) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint8_t index = 0; index < games::ModifierButtons::Size; index++) {
|
||||||
|
if ((modifier_mask & (UINT8_C(1) << index)) != 0 &&
|
||||||
|
(index >= modifier_buttons->size() ||
|
||||||
|
get_button_state(manager, modifier_buttons->at(index), true, false) !=
|
||||||
|
GameAPI::Buttons::BUTTON_PRESSED)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameAPI::Buttons::State GameAPI::Buttons::get_button_state(
|
||||||
|
rawinput::RawInputManager *manager,
|
||||||
|
Button &_button,
|
||||||
|
bool check_alts,
|
||||||
|
bool check_modifiers) {
|
||||||
|
|
||||||
// check override
|
// check override
|
||||||
if (_button.override_enabled) {
|
if (_button.override_enabled) {
|
||||||
@@ -76,6 +114,23 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
|||||||
std::optional<bool> window_has_focus;
|
std::optional<bool> window_has_focus;
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
||||||
|
// skip bindings whose required modifiers are not held
|
||||||
|
//
|
||||||
|
// note that modifiers cannot process MIDI as the logic is written
|
||||||
|
// below, since MIDI buttons are event-based (on event, off event, etc)
|
||||||
|
// and cannot be correctly handled by a simple early return
|
||||||
|
// there is no explicit check for MIDI here, but the UI should have
|
||||||
|
// prevented it
|
||||||
|
if (check_modifiers && !modifiers_pressed(manager, *current_button)) {
|
||||||
|
button_count++;
|
||||||
|
if (!alternatives || alternatives->empty() ||
|
||||||
|
button_count - 1 >= alternatives->size()) {
|
||||||
|
return BUTTON_NOT_PRESSED;
|
||||||
|
}
|
||||||
|
current_button = &alternatives->at(button_count - 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// naive behavior
|
// naive behavior
|
||||||
if (current_button->isNaive()) {
|
if (current_button->isNaive()) {
|
||||||
GameAPI::Buttons::State state;
|
GameAPI::Buttons::State state;
|
||||||
@@ -411,6 +466,12 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
|||||||
BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case rawinput::DESTROYED:
|
||||||
|
// device was unplugged. release the button instead of leaving
|
||||||
|
// state at getLastState(), which would latch a held button on
|
||||||
|
// forever (e.g. a MIDI note held while the device is removed)
|
||||||
|
state = BUTTON_NOT_PRESSED;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -463,6 +524,10 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Buttons::State Buttons::getState(rawinput::RawInputManager *manager, Button &button, bool check_alts) {
|
||||||
|
return get_button_state(manager, button, check_alts, true);
|
||||||
|
}
|
||||||
|
|
||||||
Buttons::State Buttons::getState(std::unique_ptr<rawinput::RawInputManager> &manager, Button &button, bool check_alts) {
|
Buttons::State Buttons::getState(std::unique_ptr<rawinput::RawInputManager> &manager, Button &button, bool check_alts) {
|
||||||
if (manager) {
|
if (manager) {
|
||||||
return getState(manager.get(), button, check_alts);
|
return getState(manager.get(), button, check_alts);
|
||||||
@@ -478,17 +543,13 @@ static float getVelocityHelper(rawinput::RawInputManager *manager, Button &butto
|
|||||||
return button.override_velocity;
|
return button.override_velocity;
|
||||||
}
|
}
|
||||||
|
|
||||||
// naive behavior
|
|
||||||
if (button.isNaive()) {
|
|
||||||
if (button.getInvert()) {
|
|
||||||
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 0.f : 1.f;
|
|
||||||
} else {
|
|
||||||
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 1.f : 0.f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// get button state
|
// get button state
|
||||||
Buttons::State button_state = Buttons::getState(manager, button, false);
|
const auto button_state = Buttons::getState(manager, button, false);
|
||||||
|
|
||||||
|
// naive bindings report their digital state as full or zero velocity
|
||||||
|
if (button.isNaive()) {
|
||||||
|
return button_state == Buttons::BUTTON_PRESSED ? 1.f : 0.f;
|
||||||
|
}
|
||||||
|
|
||||||
// check if button isn't being pressed
|
// check if button isn't being pressed
|
||||||
if (button_state != Buttons::BUTTON_PRESSED) {
|
if (button_state != Buttons::BUTTON_PRESSED) {
|
||||||
|
|||||||
@@ -299,7 +299,27 @@ std::string Button::getMidiNoteString() {
|
|||||||
return fmt::format("{}{}", note_names[index % 12], ((index / 12) - 1));
|
return fmt::format("{}{}", note_names[index % 12], ((index / 12) - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Button::getDisplayString(rawinput::RawInputManager* manager) {
|
std::string Button::getDisplayString(rawinput::RawInputManager *manager) {
|
||||||
|
auto binding_display = this->getBindingDisplayString(manager);
|
||||||
|
auto modifiers = this->getModifierMask();
|
||||||
|
if (binding_display.empty() || modifiers == 0) {
|
||||||
|
return binding_display;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string modifier_display;
|
||||||
|
for (unsigned int index = 1; modifiers != 0; index++, modifiers >>= 1) {
|
||||||
|
if ((modifiers & UINT8_C(1)) == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!modifier_display.empty()) {
|
||||||
|
modifier_display += "+";
|
||||||
|
}
|
||||||
|
modifier_display += fmt::format("Mod{}", index);
|
||||||
|
}
|
||||||
|
return modifier_display + "+" + binding_display;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Button::getBindingDisplayString(rawinput::RawInputManager *manager) {
|
||||||
|
|
||||||
// get VKey string
|
// get VKey string
|
||||||
auto vKey = (uint16_t) this->getVKey();
|
auto vKey = (uint16_t) this->getVKey();
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ private:
|
|||||||
std::string name;
|
std::string name;
|
||||||
std::string device_identifier = "";
|
std::string device_identifier = "";
|
||||||
unsigned short vKey = INVALID_VKEY;
|
unsigned short vKey = INVALID_VKEY;
|
||||||
|
uint8_t modifier_mask = 0;
|
||||||
|
|
||||||
// default bindings are always naive
|
// default bindings are always naive
|
||||||
unsigned short vKey_default = INVALID_VKEY;
|
unsigned short vKey_default = INVALID_VKEY;
|
||||||
@@ -65,6 +66,7 @@ private:
|
|||||||
unsigned short velocity_threshold = 0;
|
unsigned short velocity_threshold = 0;
|
||||||
|
|
||||||
std::string getMidiNoteString();
|
std::string getMidiNoteString();
|
||||||
|
std::string getBindingDisplayString(rawinput::RawInputManager *manager);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
std::string getVKeyString();
|
std::string getVKeyString();
|
||||||
@@ -111,6 +113,7 @@ public:
|
|||||||
device_identifier = "";
|
device_identifier = "";
|
||||||
analog_type = BAT_NONE;
|
analog_type = BAT_NONE;
|
||||||
bat_threshold = 0;
|
bat_threshold = 0;
|
||||||
|
modifier_mask = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string getDisplayString(rawinput::RawInputManager* manager);
|
std::string getDisplayString(rawinput::RawInputManager* manager);
|
||||||
@@ -123,6 +126,14 @@ public:
|
|||||||
return this->name;
|
return this->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline uint8_t getModifierMask() const {
|
||||||
|
return this->modifier_mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void setModifierMask(uint8_t new_modifier_mask) {
|
||||||
|
this->modifier_mask = new_modifier_mask & UINT8_C(0x0F);
|
||||||
|
}
|
||||||
|
|
||||||
inline const std::string &getDeviceIdentifier() const {
|
inline const std::string &getDeviceIdentifier() const {
|
||||||
return this->device_identifier;
|
return this->device_identifier;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ bool Config::addGame(Game &game, bool save) {
|
|||||||
int bat_threshold = 0;
|
int bat_threshold = 0;
|
||||||
int velocity_threshold = 0;
|
int velocity_threshold = 0;
|
||||||
bool invert = false;
|
bool invert = false;
|
||||||
|
unsigned int modifier_mask = 0;
|
||||||
tinyxml2::XMLError attrError = gameButtonNode->QueryIntAttribute("vkey", &vKey);
|
tinyxml2::XMLError attrError = gameButtonNode->QueryIntAttribute("vkey", &vKey);
|
||||||
const char *devid = gameButtonNode->Attribute("devid");
|
const char *devid = gameButtonNode->Attribute("devid");
|
||||||
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
|
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
|
||||||
@@ -173,6 +174,7 @@ bool Config::addGame(Game &game, bool save) {
|
|||||||
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
|
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
|
||||||
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
|
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
|
||||||
gameButtonNode->QueryBoolAttribute("invert", &invert);
|
gameButtonNode->QueryBoolAttribute("invert", &invert);
|
||||||
|
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
|
||||||
if (attrError != tinyxml2::XMLError::XML_SUCCESS) {
|
if (attrError != tinyxml2::XMLError::XML_SUCCESS) {
|
||||||
gameButtonsNode->DeleteChild(gameButtonNode);
|
gameButtonsNode->DeleteChild(gameButtonNode);
|
||||||
gameButtonNode = this->configFile.NewElement("button");
|
gameButtonNode = this->configFile.NewElement("button");
|
||||||
@@ -185,6 +187,7 @@ bool Config::addGame(Game &game, bool save) {
|
|||||||
gameButtonNode->SetAttribute("bat_threshold", bat_threshold);
|
gameButtonNode->SetAttribute("bat_threshold", bat_threshold);
|
||||||
gameButtonNode->SetAttribute("velocity_threshold", velocity_threshold);
|
gameButtonNode->SetAttribute("velocity_threshold", velocity_threshold);
|
||||||
gameButtonNode->SetAttribute("invert", invert);
|
gameButtonNode->SetAttribute("invert", invert);
|
||||||
|
gameButtonNode->SetAttribute("modifiers", button->getModifierMask());
|
||||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||||
} else {
|
} else {
|
||||||
button->setVKey(static_cast<unsigned short int>(vKey));
|
button->setVKey(static_cast<unsigned short int>(vKey));
|
||||||
@@ -194,6 +197,7 @@ bool Config::addGame(Game &game, bool save) {
|
|||||||
button->setBatThreshold(bat_threshold);
|
button->setBatThreshold(bat_threshold);
|
||||||
button->setVelocityThreshold(velocity_threshold);
|
button->setVelocityThreshold(velocity_threshold);
|
||||||
button->setInvert(invert);
|
button->setInvert(invert);
|
||||||
|
button->setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||||
if (devid) {
|
if (devid) {
|
||||||
button->setDeviceIdentifier(devid);
|
button->setDeviceIdentifier(devid);
|
||||||
}
|
}
|
||||||
@@ -214,6 +218,7 @@ bool Config::addGame(Game &game, bool save) {
|
|||||||
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
|
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
|
||||||
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
|
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
|
||||||
gameButtonNode->SetAttribute("invert", it.getInvert());
|
gameButtonNode->SetAttribute("invert", it.getInvert());
|
||||||
|
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
|
||||||
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
|
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
|
||||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||||
}
|
}
|
||||||
@@ -440,6 +445,7 @@ bool Config::addGame(Game &game, bool save) {
|
|||||||
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
|
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
|
||||||
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
|
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
|
||||||
gameButtonNode->SetAttribute("invert", it.getInvert());
|
gameButtonNode->SetAttribute("invert", it.getInvert());
|
||||||
|
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
|
||||||
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
|
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
|
||||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||||
}
|
}
|
||||||
@@ -552,6 +558,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
|
|||||||
gameButtonNode->SetAttribute("velocity_threshold", button.getVelocityThreshold());
|
gameButtonNode->SetAttribute("velocity_threshold", button.getVelocityThreshold());
|
||||||
gameButtonNode->SetAttribute("invert", button.getInvert());
|
gameButtonNode->SetAttribute("invert", button.getInvert());
|
||||||
gameButtonNode->SetAttribute("devid", button.getDeviceIdentifier().c_str());
|
gameButtonNode->SetAttribute("devid", button.getDeviceIdentifier().c_str());
|
||||||
|
gameButtonNode->SetAttribute("modifiers", button.getModifierMask());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -569,6 +576,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
|
|||||||
gameButtonNode->SetAttribute("velocity_threshold", 0);
|
gameButtonNode->SetAttribute("velocity_threshold", 0);
|
||||||
gameButtonNode->SetAttribute("invert", false);
|
gameButtonNode->SetAttribute("invert", false);
|
||||||
gameButtonNode->SetAttribute("devid", "");
|
gameButtonNode->SetAttribute("devid", "");
|
||||||
|
gameButtonNode->SetAttribute("modifiers", 0);
|
||||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -964,6 +972,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
|||||||
int bat_threshold = 0;
|
int bat_threshold = 0;
|
||||||
int velocity_threshold = 0;
|
int velocity_threshold = 0;
|
||||||
bool invert = false;
|
bool invert = false;
|
||||||
|
unsigned int modifier_mask = 0;
|
||||||
gameButtonNode->QueryIntAttribute("vkey", &vKey);
|
gameButtonNode->QueryIntAttribute("vkey", &vKey);
|
||||||
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
|
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
|
||||||
gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up);
|
gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up);
|
||||||
@@ -971,6 +980,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
|||||||
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
|
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
|
||||||
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
|
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
|
||||||
gameButtonNode->QueryBoolAttribute("invert", &invert);
|
gameButtonNode->QueryBoolAttribute("invert", &invert);
|
||||||
|
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
|
||||||
const char *devid = gameButtonNode->Attribute("devid");
|
const char *devid = gameButtonNode->Attribute("devid");
|
||||||
|
|
||||||
// find alternative
|
// find alternative
|
||||||
@@ -986,6 +996,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
|||||||
alt.setBatThreshold(bat_threshold);
|
alt.setBatThreshold(bat_threshold);
|
||||||
alt.setVelocityThreshold(velocity_threshold);
|
alt.setVelocityThreshold(velocity_threshold);
|
||||||
alt.setInvert(invert);
|
alt.setInvert(invert);
|
||||||
|
alt.setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||||
if (devid) {
|
if (devid) {
|
||||||
alt.setDeviceIdentifier(std::string(devid));
|
alt.setDeviceIdentifier(std::string(devid));
|
||||||
}
|
}
|
||||||
@@ -1005,6 +1016,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
|||||||
button.setBatThreshold(bat_threshold);
|
button.setBatThreshold(bat_threshold);
|
||||||
button.setVelocityThreshold(velocity_threshold);
|
button.setVelocityThreshold(velocity_threshold);
|
||||||
button.setInvert(invert);
|
button.setInvert(invert);
|
||||||
|
button.setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||||
if (devid) {
|
if (devid) {
|
||||||
button.setDeviceIdentifier(devid);
|
button.setDeviceIdentifier(devid);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <d3d9.h>
|
#include <d3d9.h>
|
||||||
|
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
|
#include "util/libutils.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
|
||||||
namespace cfg {
|
namespace cfg {
|
||||||
@@ -35,7 +36,26 @@ namespace cfg {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
IDirect3D9 *d3d = Direct3DCreate9(D3D_SDK_VERSION);
|
// load d3d9.dll dynamically rather than linking against it statically.
|
||||||
|
// a static import would force the system d3d9.dll to load at process
|
||||||
|
// startup for the main spice executable too (this file is shared with
|
||||||
|
// spice.exe), which loads system32\d3d9.dll before the modules directory
|
||||||
|
// is added to the DLL search path - preventing a user-supplied DXVK
|
||||||
|
// d3d9.dll in modules from ever loading for the game.
|
||||||
|
typedef IDirect3D9 *(WINAPI *Direct3DCreate9_t)(UINT);
|
||||||
|
HMODULE d3d9_module = libutils::try_library("d3d9.dll");
|
||||||
|
if (d3d9_module == nullptr) {
|
||||||
|
log_warning("configurator", "could not load d3d9.dll, falling back to software renderer");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto Direct3DCreate9_fn =
|
||||||
|
reinterpret_cast<Direct3DCreate9_t>(libutils::try_proc(d3d9_module, "Direct3DCreate9"));
|
||||||
|
if (Direct3DCreate9_fn == nullptr) {
|
||||||
|
log_warning("configurator", "could not find Direct3DCreate9, falling back to software renderer");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
IDirect3D9 *d3d = Direct3DCreate9_fn(D3D_SDK_VERSION);
|
||||||
if (d3d == nullptr) {
|
if (d3d == nullptr) {
|
||||||
log_warning("configurator", "Direct3DCreate9 returned NULL, falling back to software renderer");
|
log_warning("configurator", "Direct3DCreate9 returned NULL, falling back to software renderer");
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ static const char *CLASS_NAME = "ConfiguratorWindow";
|
|||||||
static std::string WINDOW_TITLE;
|
static std::string WINDOW_TITLE;
|
||||||
static int WINDOW_SIZE_X = 800;
|
static int WINDOW_SIZE_X = 800;
|
||||||
static int WINDOW_SIZE_Y = 600;
|
static int WINDOW_SIZE_Y = 600;
|
||||||
|
static const int WINDOW_MIN_SIZE_X = 540;
|
||||||
|
static const int WINDOW_MIN_SIZE_Y = 300;
|
||||||
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;
|
static const UINT_PTR RENDER_TIMER_ID = 1;
|
||||||
@@ -85,6 +87,23 @@ static cfg::ConfiguratorWindow *get_state(HWND hWnd) {
|
|||||||
GetWindowLongPtrW(hWnd, GWLP_USERDATA));
|
GetWindowLongPtrW(hWnd, GWLP_USERDATA));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// computes the top-left position that centers a window of the given size on
|
||||||
|
// the primary monitor's work area (the desktop minus the taskbar); falls back
|
||||||
|
// to (0, 0) if the monitor info can't be queried.
|
||||||
|
static POINT center_on_primary_monitor(int width, int height) {
|
||||||
|
POINT pos = { 0, 0 };
|
||||||
|
HMONITOR mon = MonitorFromPoint(pos, MONITOR_DEFAULTTOPRIMARY);
|
||||||
|
MONITORINFO mi {};
|
||||||
|
mi.cbSize = sizeof(mi);
|
||||||
|
if (GetMonitorInfo(mon, &mi)) {
|
||||||
|
const int work_w = mi.rcWork.right - mi.rcWork.left;
|
||||||
|
const int work_h = mi.rcWork.bottom - mi.rcWork.top;
|
||||||
|
pos.x = mi.rcWork.left + (work_w - width) / 2;
|
||||||
|
pos.y = mi.rcWork.top + (work_h - height) / 2;
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
// Returns the refresh rate (Hz) of the monitor the window currently lives on,
|
// 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
|
// 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").
|
// looks invalid (DEVMODE may report 0 or 1 to mean "use hardware default").
|
||||||
@@ -243,8 +262,10 @@ cfg::ConfiguratorWindow::~ConfiguratorWindow() {
|
|||||||
|
|
||||||
void cfg::ConfiguratorWindow::run() {
|
void cfg::ConfiguratorWindow::run() {
|
||||||
|
|
||||||
|
const POINT pos = center_on_primary_monitor(WINDOW_SIZE_X, WINDOW_SIZE_Y);
|
||||||
|
SetWindowPos(this->hWnd, HWND_TOP, pos.x, pos.y, WINDOW_SIZE_X, WINDOW_SIZE_Y, 0);
|
||||||
|
|
||||||
// show window
|
// show window
|
||||||
SetWindowPos(this->hWnd, HWND_TOP, 0, 0, WINDOW_SIZE_X, WINDOW_SIZE_Y, 0);
|
|
||||||
ShowWindow(this->hWnd, SW_SHOWNORMAL);
|
ShowWindow(this->hWnd, SW_SHOWNORMAL);
|
||||||
UpdateWindow(this->hWnd);
|
UpdateWindow(this->hWnd);
|
||||||
|
|
||||||
@@ -290,6 +311,22 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case WM_GETMINMAXINFO: {
|
||||||
|
|
||||||
|
// enforce a minimum window size so the UI can't be shrunk to a
|
||||||
|
// point where the tabs/controls become unusable. The minimum is
|
||||||
|
// expressed in client-area pixels and converted to a full window
|
||||||
|
// size that accounts for the current frame/title-bar style.
|
||||||
|
RECT rc = { 0, 0, WINDOW_MIN_SIZE_X, WINDOW_MIN_SIZE_Y };
|
||||||
|
DWORD style = static_cast<DWORD>(GetWindowLongPtrW(hWnd, GWL_STYLE));
|
||||||
|
DWORD ex_style = static_cast<DWORD>(GetWindowLongPtrW(hWnd, GWL_EXSTYLE));
|
||||||
|
if (AdjustWindowRectEx(&rc, style, FALSE, ex_style)) {
|
||||||
|
auto *mmi = reinterpret_cast<MINMAXINFO *>(lParam);
|
||||||
|
mmi->ptMinTrackSize.x = rc.right - rc.left;
|
||||||
|
mmi->ptMinTrackSize.y = rc.bottom - rc.top;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case WM_CREATE: {
|
case WM_CREATE: {
|
||||||
|
|
||||||
// set user data of window to class pointer
|
// set user data of window to class pointer
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ namespace overlay::windows {
|
|||||||
el->SetAttribute("debounce_down", entry.debounce_down);
|
el->SetAttribute("debounce_down", entry.debounce_down);
|
||||||
el->SetAttribute("bat_threshold", entry.bat_threshold);
|
el->SetAttribute("bat_threshold", entry.bat_threshold);
|
||||||
el->SetAttribute("velocity_threshold", entry.velocity_threshold);
|
el->SetAttribute("velocity_threshold", entry.velocity_threshold);
|
||||||
|
el->SetAttribute("modifiers", entry.modifier_mask);
|
||||||
parent->InsertEndChild(el);
|
parent->InsertEndChild(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +55,10 @@ namespace overlay::windows {
|
|||||||
el->QueryIntAttribute("bat_threshold", &bat);
|
el->QueryIntAttribute("bat_threshold", &bat);
|
||||||
entry.bat_threshold = bat;
|
entry.bat_threshold = bat;
|
||||||
|
|
||||||
|
unsigned int modifier_mask = 0;
|
||||||
|
el->QueryUnsignedAttribute("modifiers", &modifier_mask);
|
||||||
|
entry.modifier_mask = static_cast<uint8_t>(modifier_mask & UINT8_C(0x0F));
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +165,28 @@ namespace overlay::windows {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// modifier buttons
|
||||||
|
auto *mod_el = tmpl_el->FirstChildElement("modifier_buttons");
|
||||||
|
if (mod_el) {
|
||||||
|
auto *btn_el = mod_el->FirstChildElement("button");
|
||||||
|
while (btn_el) {
|
||||||
|
const char *btn_name = btn_el->Attribute("name");
|
||||||
|
std::string btn_name_str = btn_name ? btn_name : "";
|
||||||
|
|
||||||
|
TemplateButtonBinding *binding = nullptr;
|
||||||
|
for (auto &b : tmpl.modifier_buttons) {
|
||||||
|
if (b.name == btn_name_str) { binding = &b; break; }
|
||||||
|
}
|
||||||
|
if (!binding) {
|
||||||
|
tmpl.modifier_buttons.push_back(
|
||||||
|
{btn_name_str, read_button_entry(btn_el), {}});
|
||||||
|
} else {
|
||||||
|
binding->alternatives.push_back(read_button_entry(btn_el));
|
||||||
|
}
|
||||||
|
btn_el = btn_el->NextSiblingElement("button");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// keypad buttons
|
// keypad buttons
|
||||||
auto *kp_el = tmpl_el->FirstChildElement("keypad_buttons");
|
auto *kp_el = tmpl_el->FirstChildElement("keypad_buttons");
|
||||||
if (kp_el) {
|
if (kp_el) {
|
||||||
@@ -244,6 +271,7 @@ namespace overlay::windows {
|
|||||||
bool save_user_template(
|
bool save_user_template(
|
||||||
const ControllerTemplate &tmpl,
|
const ControllerTemplate &tmpl,
|
||||||
const bool save_buttons,
|
const bool save_buttons,
|
||||||
|
const bool save_modifiers,
|
||||||
const bool save_keypads,
|
const bool save_keypads,
|
||||||
const bool save_analogs,
|
const bool save_analogs,
|
||||||
const bool save_lights) {
|
const bool save_lights) {
|
||||||
@@ -300,6 +328,24 @@ namespace overlay::windows {
|
|||||||
}
|
}
|
||||||
tmpl_el->InsertEndChild(buttons_el);
|
tmpl_el->InsertEndChild(buttons_el);
|
||||||
|
|
||||||
|
// modifier buttons - skip unbound entries
|
||||||
|
if (!tmpl.modifier_buttons.empty()) {
|
||||||
|
auto *mod_el = doc.NewElement("modifier_buttons");
|
||||||
|
if (save_modifiers) {
|
||||||
|
for (auto &btn : tmpl.modifier_buttons) {
|
||||||
|
if (!btn.primary.is_unbound()) {
|
||||||
|
write_button_entry(doc, mod_el, btn.name, btn.primary);
|
||||||
|
}
|
||||||
|
for (auto &alt : btn.alternatives) {
|
||||||
|
if (!alt.is_unbound()) {
|
||||||
|
write_button_entry(doc, mod_el, btn.name, alt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tmpl_el->InsertEndChild(mod_el);
|
||||||
|
}
|
||||||
|
|
||||||
// keypad buttons — skip unbound entries
|
// keypad buttons — skip unbound entries
|
||||||
if (!tmpl.keypad_buttons.empty()) {
|
if (!tmpl.keypad_buttons.empty()) {
|
||||||
auto *kp_el = doc.NewElement("keypad_buttons");
|
auto *kp_el = doc.NewElement("keypad_buttons");
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# fails the build if a PE binary statically imports a forbidden DLL.
|
||||||
|
#
|
||||||
|
# some DLLs must never end up in spice's static import table, for two reasons:
|
||||||
|
#
|
||||||
|
# 1. user-overridable DLLs (e.g. DXVK's d3d9.dll): users drop their own copy
|
||||||
|
# into the modules directory to replace the system one. a static import
|
||||||
|
# forces the loader to load the SYSTEM copy at process startup - before the
|
||||||
|
# modules directory is added to the DLL search path and before the game DLL
|
||||||
|
# loads - so the user-supplied override never takes effect (see issue #779).
|
||||||
|
#
|
||||||
|
# 2. DLLs that break games when present (e.g. Media Foundation: mf/mfplat/
|
||||||
|
# mfreadwrite): a static import loads them eagerly and breaks Unity games.
|
||||||
|
#
|
||||||
|
# in both cases the DLL must instead be loaded dynamically (libutils::try_library
|
||||||
|
# / GetProcAddress / delay load) so it is only pulled in when actually needed.
|
||||||
|
#
|
||||||
|
# invoked via `cmake -P` from a POST_BUILD step. required -D variables:
|
||||||
|
# OBJDUMP - path to objdump (CMAKE_OBJDUMP)
|
||||||
|
# TARGET_FILE - path to the PE binary to inspect
|
||||||
|
# FORBIDDEN - semicolon-separated list of lowercase DLL names to reject
|
||||||
|
|
||||||
|
if(NOT OBJDUMP OR NOT EXISTS "${OBJDUMP}")
|
||||||
|
message(WARNING
|
||||||
|
"check_no_static_dll_imports: objdump not found, skipping import check for ${TARGET_FILE}")
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND "${OBJDUMP}" -p "${TARGET_FILE}"
|
||||||
|
OUTPUT_VARIABLE dump_output
|
||||||
|
RESULT_VARIABLE dump_result
|
||||||
|
ERROR_VARIABLE dump_error)
|
||||||
|
|
||||||
|
if(NOT dump_result EQUAL 0)
|
||||||
|
message(WARNING
|
||||||
|
"check_no_static_dll_imports: objdump failed for ${TARGET_FILE}: ${dump_error}")
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# both GNU objdump and llvm-objdump print one "DLL Name: <name>" line per
|
||||||
|
# statically imported DLL in their PE private-header dump.
|
||||||
|
string(REGEX MATCHALL "DLL Name:[ \t]*[^\n\r]+" dll_lines "${dump_output}")
|
||||||
|
|
||||||
|
set(violations "")
|
||||||
|
foreach(line IN LISTS dll_lines)
|
||||||
|
string(REGEX REPLACE "DLL Name:[ \t]*" "" dll_name "${line}")
|
||||||
|
string(STRIP "${dll_name}" dll_name)
|
||||||
|
string(TOLOWER "${dll_name}" dll_name_lower)
|
||||||
|
if(dll_name_lower IN_LIST FORBIDDEN)
|
||||||
|
list(APPEND violations "${dll_name}")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
if(violations)
|
||||||
|
list(REMOVE_DUPLICATES violations)
|
||||||
|
string(REPLACE ";" ", " violations_str "${violations}")
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"static DLL import check FAILED for ${TARGET_FILE}\n"
|
||||||
|
" forbidden static imports found: ${violations_str}\n"
|
||||||
|
"\n"
|
||||||
|
" these DLLs must never be statically imported by spice:\n"
|
||||||
|
" * user-overridable DLLs (e.g. DXVK d3d9.dll) - a static import loads the\n"
|
||||||
|
" system copy at startup and preempts the modules override (issue #779).\n"
|
||||||
|
" * Media Foundation DLLs (mf/mfplat/mfreadwrite) - a static import breaks\n"
|
||||||
|
" Unity games.\n"
|
||||||
|
"\n"
|
||||||
|
" fix: load the DLL dynamically instead - replace the direct API call with a\n"
|
||||||
|
" libutils::try_library() + libutils::try_proc() lookup (or a delay load), then\n"
|
||||||
|
" call through the resolved function pointer.")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
message(STATUS "static DLL import check passed for ${TARGET_FILE}")
|
||||||
+552
@@ -0,0 +1,552 @@
|
|||||||
|
// This code comes from:
|
||||||
|
// https://github.com/dhbaird/easywsclient
|
||||||
|
//
|
||||||
|
// To get the latest version:
|
||||||
|
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.hpp
|
||||||
|
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp
|
||||||
|
|
||||||
|
#include "easywsclient.hpp"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#ifndef WIN32_LEAN_AND_MEAN
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#endif
|
||||||
|
#include <winsock2.h>
|
||||||
|
#include <ws2tcpip.h>
|
||||||
|
#pragma comment( lib, "ws2_32" )
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
typedef SOCKET socket_t;
|
||||||
|
#ifndef _SSIZE_T_DEFINED
|
||||||
|
typedef int ssize_t;
|
||||||
|
#define _SSIZE_T_DEFINED
|
||||||
|
#endif
|
||||||
|
#ifndef _SOCKET_T_DEFINED
|
||||||
|
typedef SOCKET socket_t;
|
||||||
|
#define _SOCKET_T_DEFINED
|
||||||
|
#endif
|
||||||
|
#if defined(_MSC_VER) && !defined(snprintf)
|
||||||
|
#define snprintf _snprintf_s
|
||||||
|
#endif
|
||||||
|
#include <stdint.h>
|
||||||
|
#define socketerrno WSAGetLastError()
|
||||||
|
#define SOCKET_EAGAIN_EINPROGRESS WSAEINPROGRESS
|
||||||
|
#define SOCKET_EWOULDBLOCK WSAEWOULDBLOCK
|
||||||
|
#else
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
#include <netinet/tcp.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <errno.h>
|
||||||
|
typedef int socket_t;
|
||||||
|
#ifndef INVALID_SOCKET
|
||||||
|
#define INVALID_SOCKET (-1)
|
||||||
|
#endif
|
||||||
|
#ifndef SOCKET_ERROR
|
||||||
|
#define SOCKET_ERROR (-1)
|
||||||
|
#endif
|
||||||
|
#define closesocket(s) ::close(s)
|
||||||
|
#include <errno.h>
|
||||||
|
#define socketerrno errno
|
||||||
|
#define SOCKET_EAGAIN_EINPROGRESS EAGAIN
|
||||||
|
#define SOCKET_EWOULDBLOCK EWOULDBLOCK
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <stdarg.h>
|
||||||
|
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
// When false, easywsclient's diagnostics are suppressed. The host application
|
||||||
|
// opts in by setting this flag (see obs_websocket.cpp).
|
||||||
|
bool EASYWSCLIENT_LOGGING_ENABLED = false;
|
||||||
|
|
||||||
|
// Route easywsclient's diagnostic output through the project logger instead of
|
||||||
|
// writing to stderr, without editing the upstream source lines below: these two
|
||||||
|
// macros redirect fprintf(stderr, ...) / fputs(..., stderr) to log_misc.
|
||||||
|
// Only emitted when EASYWSCLIENT_LOGGING_ENABLED is set.
|
||||||
|
static inline void easywsclient_logf(const char *fmt, ...) {
|
||||||
|
if (!EASYWSCLIENT_LOGGING_ENABLED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char buf[1024];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
vsnprintf(buf, sizeof(buf), fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
// trim trailing newline(s); the logger appends its own
|
||||||
|
size_t len = strlen(buf);
|
||||||
|
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) {
|
||||||
|
buf[--len] = '\0';
|
||||||
|
}
|
||||||
|
log_misc("easywsclient", "{}", buf);
|
||||||
|
}
|
||||||
|
#define fprintf(stream, ...) easywsclient_logf(__VA_ARGS__)
|
||||||
|
#define fputs(str, stream) easywsclient_logf("%s", str)
|
||||||
|
|
||||||
|
using namespace easywsclient;
|
||||||
|
|
||||||
|
namespace { // private module-only namespace
|
||||||
|
|
||||||
|
socket_t hostname_connect(const std::string& hostname, int port) {
|
||||||
|
struct addrinfo hints;
|
||||||
|
struct addrinfo *result;
|
||||||
|
struct addrinfo *p;
|
||||||
|
int ret;
|
||||||
|
socket_t sockfd = INVALID_SOCKET;
|
||||||
|
char sport[16];
|
||||||
|
memset(&hints, 0, sizeof(hints));
|
||||||
|
hints.ai_family = AF_UNSPEC;
|
||||||
|
hints.ai_socktype = SOCK_STREAM;
|
||||||
|
snprintf(sport, 16, "%d", port);
|
||||||
|
if ((ret = getaddrinfo(hostname.c_str(), sport, &hints, &result)) != 0)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
for(p = result; p != NULL; p = p->ai_next)
|
||||||
|
{
|
||||||
|
sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
|
||||||
|
if (sockfd == INVALID_SOCKET) { continue; }
|
||||||
|
if (connect(sockfd, p->ai_addr, p->ai_addrlen) != SOCKET_ERROR) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
closesocket(sockfd);
|
||||||
|
sockfd = INVALID_SOCKET;
|
||||||
|
}
|
||||||
|
freeaddrinfo(result);
|
||||||
|
return sockfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyWebSocket : public easywsclient::WebSocket
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void poll(int timeout) { }
|
||||||
|
void send(const std::string& message) { }
|
||||||
|
void sendBinary(const std::string& message) { }
|
||||||
|
void sendBinary(const std::vector<uint8_t>& message) { }
|
||||||
|
void sendPing() { }
|
||||||
|
void close() { }
|
||||||
|
readyStateValues getReadyState() const { return CLOSED; }
|
||||||
|
void _dispatch(Callback_Imp & callable) { }
|
||||||
|
void _dispatchBinary(BytesCallback_Imp& callable) { }
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class _RealWebSocket : public easywsclient::WebSocket
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// http://tools.ietf.org/html/rfc6455#section-5.2 Base Framing Protocol
|
||||||
|
//
|
||||||
|
// 0 1 2 3
|
||||||
|
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
|
// +-+-+-+-+-------+-+-------------+-------------------------------+
|
||||||
|
// |F|R|R|R| opcode|M| Payload len | Extended payload length |
|
||||||
|
// |I|S|S|S| (4) |A| (7) | (16/64) |
|
||||||
|
// |N|V|V|V| |S| | (if payload len==126/127) |
|
||||||
|
// | |1|2|3| |K| | |
|
||||||
|
// +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|
||||||
|
// | Extended payload length continued, if payload len == 127 |
|
||||||
|
// + - - - - - - - - - - - - - - - +-------------------------------+
|
||||||
|
// | |Masking-key, if MASK set to 1 |
|
||||||
|
// +-------------------------------+-------------------------------+
|
||||||
|
// | Masking-key (continued) | Payload Data |
|
||||||
|
// +-------------------------------- - - - - - - - - - - - - - - - +
|
||||||
|
// : Payload Data continued ... :
|
||||||
|
// + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|
||||||
|
// | Payload Data continued ... |
|
||||||
|
// +---------------------------------------------------------------+
|
||||||
|
|
||||||
|
struct wsheader_type {
|
||||||
|
unsigned header_size;
|
||||||
|
bool fin;
|
||||||
|
bool mask;
|
||||||
|
enum opcode_type {
|
||||||
|
CONTINUATION = 0x0,
|
||||||
|
TEXT_FRAME = 0x1,
|
||||||
|
BINARY_FRAME = 0x2,
|
||||||
|
CLOSE = 8,
|
||||||
|
PING = 9,
|
||||||
|
PONG = 0xa,
|
||||||
|
} opcode;
|
||||||
|
int N0;
|
||||||
|
uint64_t N;
|
||||||
|
uint8_t masking_key[4];
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<uint8_t> rxbuf;
|
||||||
|
std::vector<uint8_t> txbuf;
|
||||||
|
std::vector<uint8_t> receivedData;
|
||||||
|
|
||||||
|
socket_t sockfd;
|
||||||
|
readyStateValues readyState;
|
||||||
|
bool useMask;
|
||||||
|
bool isRxBad;
|
||||||
|
|
||||||
|
_RealWebSocket(socket_t sockfd, bool useMask) : sockfd(sockfd), readyState(OPEN), useMask(useMask), isRxBad(false) {
|
||||||
|
}
|
||||||
|
|
||||||
|
readyStateValues getReadyState() const {
|
||||||
|
return readyState;
|
||||||
|
}
|
||||||
|
|
||||||
|
void poll(int timeout) { // timeout in milliseconds
|
||||||
|
if (readyState == CLOSED) {
|
||||||
|
if (timeout > 0) {
|
||||||
|
timeval tv = { timeout/1000, (timeout%1000) * 1000 };
|
||||||
|
select(0, NULL, NULL, NULL, &tv);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (timeout != 0) {
|
||||||
|
fd_set rfds;
|
||||||
|
fd_set wfds;
|
||||||
|
timeval tv = { timeout/1000, (timeout%1000) * 1000 };
|
||||||
|
FD_ZERO(&rfds);
|
||||||
|
FD_ZERO(&wfds);
|
||||||
|
FD_SET(sockfd, &rfds);
|
||||||
|
if (txbuf.size()) { FD_SET(sockfd, &wfds); }
|
||||||
|
select(sockfd + 1, &rfds, &wfds, NULL, timeout > 0 ? &tv : NULL);
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
// FD_ISSET(0, &rfds) will be true
|
||||||
|
int N = rxbuf.size();
|
||||||
|
ssize_t ret;
|
||||||
|
rxbuf.resize(N + 1500);
|
||||||
|
ret = recv(sockfd, (char*)&rxbuf[0] + N, 1500, 0);
|
||||||
|
if (false) { }
|
||||||
|
else if (ret < 0 && (socketerrno == SOCKET_EWOULDBLOCK || socketerrno == SOCKET_EAGAIN_EINPROGRESS)) {
|
||||||
|
rxbuf.resize(N);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (ret <= 0) {
|
||||||
|
rxbuf.resize(N);
|
||||||
|
closesocket(sockfd);
|
||||||
|
readyState = CLOSED;
|
||||||
|
fputs(ret < 0 ? "Connection error!\n" : "Connection closed!\n", stderr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
rxbuf.resize(N + ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (txbuf.size()) {
|
||||||
|
int ret = ::send(sockfd, (char*)&txbuf[0], txbuf.size(), 0);
|
||||||
|
if (false) { } // ??
|
||||||
|
else if (ret < 0 && (socketerrno == SOCKET_EWOULDBLOCK || socketerrno == SOCKET_EAGAIN_EINPROGRESS)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (ret <= 0) {
|
||||||
|
closesocket(sockfd);
|
||||||
|
readyState = CLOSED;
|
||||||
|
fputs(ret < 0 ? "Connection error!\n" : "Connection closed!\n", stderr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
txbuf.erase(txbuf.begin(), txbuf.begin() + ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!txbuf.size() && readyState == CLOSING) {
|
||||||
|
closesocket(sockfd);
|
||||||
|
readyState = CLOSED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void _dispatch(Callback_Imp & callable) {
|
||||||
|
struct CallbackAdapter : public BytesCallback_Imp
|
||||||
|
// Adapt void(const std::string<uint8_t>&) to void(const std::string&)
|
||||||
|
{
|
||||||
|
Callback_Imp& callable;
|
||||||
|
CallbackAdapter(Callback_Imp& callable) : callable(callable) { }
|
||||||
|
void operator()(const std::vector<uint8_t>& message) {
|
||||||
|
std::string stringMessage(message.begin(), message.end());
|
||||||
|
callable(stringMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
CallbackAdapter bytesCallback(callable);
|
||||||
|
_dispatchBinary(bytesCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void _dispatchBinary(BytesCallback_Imp& callable) {
|
||||||
|
// TODO: consider acquiring a lock on rxbuf...
|
||||||
|
while (true) {
|
||||||
|
wsheader_type ws;
|
||||||
|
if (rxbuf.size() < 2) { return; /* Need at least 2 */ }
|
||||||
|
const uint8_t * data = (uint8_t *) &rxbuf[0]; // peek, but don't consume
|
||||||
|
ws.fin = (data[0] & 0x80) == 0x80;
|
||||||
|
ws.opcode = (wsheader_type::opcode_type) (data[0] & 0x0f);
|
||||||
|
ws.mask = (data[1] & 0x80) == 0x80;
|
||||||
|
ws.N0 = (data[1] & 0x7f);
|
||||||
|
ws.header_size = 2 + (ws.N0 == 126? 2 : 0) + (ws.N0 == 127? 8 : 0) + (ws.mask? 4 : 0);
|
||||||
|
if (rxbuf.size() < ws.header_size) { return; /* Need: ws.header_size - rxbuf.size() */ }
|
||||||
|
int i = 0;
|
||||||
|
if (ws.N0 < 126) {
|
||||||
|
ws.N = ws.N0;
|
||||||
|
i = 2;
|
||||||
|
}
|
||||||
|
else if (ws.N0 == 126) {
|
||||||
|
ws.N = 0;
|
||||||
|
ws.N |= ((uint64_t) data[2]) << 8;
|
||||||
|
ws.N |= ((uint64_t) data[3]) << 0;
|
||||||
|
i = 4;
|
||||||
|
}
|
||||||
|
else if (ws.N0 == 127) {
|
||||||
|
ws.N = 0;
|
||||||
|
ws.N |= ((uint64_t) data[2]) << 56;
|
||||||
|
ws.N |= ((uint64_t) data[3]) << 48;
|
||||||
|
ws.N |= ((uint64_t) data[4]) << 40;
|
||||||
|
ws.N |= ((uint64_t) data[5]) << 32;
|
||||||
|
ws.N |= ((uint64_t) data[6]) << 24;
|
||||||
|
ws.N |= ((uint64_t) data[7]) << 16;
|
||||||
|
ws.N |= ((uint64_t) data[8]) << 8;
|
||||||
|
ws.N |= ((uint64_t) data[9]) << 0;
|
||||||
|
i = 10;
|
||||||
|
}
|
||||||
|
if (ws.mask) {
|
||||||
|
ws.masking_key[0] = ((uint8_t) data[i+0]) << 0;
|
||||||
|
ws.masking_key[1] = ((uint8_t) data[i+1]) << 0;
|
||||||
|
ws.masking_key[2] = ((uint8_t) data[i+2]) << 0;
|
||||||
|
ws.masking_key[3] = ((uint8_t) data[i+3]) << 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ws.masking_key[0] = 0;
|
||||||
|
ws.masking_key[1] = 0;
|
||||||
|
ws.masking_key[2] = 0;
|
||||||
|
ws.masking_key[3] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: The checks above should hopefully ensure this addition
|
||||||
|
// cannot overflow:
|
||||||
|
if (rxbuf.size() < ws.header_size+ws.N) { return; /* Need: ws.header_size+ws.N - rxbuf.size() */ }
|
||||||
|
|
||||||
|
// We got a whole message, now do something with it:
|
||||||
|
if (false) { }
|
||||||
|
else if (
|
||||||
|
ws.opcode == wsheader_type::TEXT_FRAME
|
||||||
|
|| ws.opcode == wsheader_type::BINARY_FRAME
|
||||||
|
|| ws.opcode == wsheader_type::CONTINUATION
|
||||||
|
) {
|
||||||
|
if (ws.mask) { for (size_t i = 0; i != ws.N; ++i) { rxbuf[i+ws.header_size] ^= ws.masking_key[i&0x3]; } }
|
||||||
|
receivedData.insert(receivedData.end(), rxbuf.begin()+ws.header_size, rxbuf.begin()+ws.header_size+(size_t)ws.N);// just feed
|
||||||
|
if (ws.fin) {
|
||||||
|
callable((const std::vector<uint8_t>) receivedData);
|
||||||
|
receivedData.erase(receivedData.begin(), receivedData.end());
|
||||||
|
std::vector<uint8_t> ().swap(receivedData);// free memory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (ws.opcode == wsheader_type::PING) {
|
||||||
|
if (ws.mask) { for (size_t i = 0; i != ws.N; ++i) { rxbuf[i+ws.header_size] ^= ws.masking_key[i&0x3]; } }
|
||||||
|
std::string data(rxbuf.begin()+ws.header_size, rxbuf.begin()+ws.header_size+(size_t)ws.N);
|
||||||
|
sendData(wsheader_type::PONG, data.size(), data.begin(), data.end());
|
||||||
|
}
|
||||||
|
else if (ws.opcode == wsheader_type::PONG) { }
|
||||||
|
else if (ws.opcode == wsheader_type::CLOSE) { close(); }
|
||||||
|
else { fprintf(stderr, "ERROR: Got unexpected WebSocket message.\n"); close(); }
|
||||||
|
|
||||||
|
rxbuf.erase(rxbuf.begin(), rxbuf.begin() + ws.header_size+(size_t)ws.N);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendPing() {
|
||||||
|
std::string empty;
|
||||||
|
sendData(wsheader_type::PING, empty.size(), empty.begin(), empty.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
void send(const std::string& message) {
|
||||||
|
sendData(wsheader_type::TEXT_FRAME, message.size(), message.begin(), message.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendBinary(const std::string& message) {
|
||||||
|
sendData(wsheader_type::BINARY_FRAME, message.size(), message.begin(), message.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendBinary(const std::vector<uint8_t>& message) {
|
||||||
|
sendData(wsheader_type::BINARY_FRAME, message.size(), message.begin(), message.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class Iterator>
|
||||||
|
void sendData(wsheader_type::opcode_type type, uint64_t message_size, Iterator message_begin, Iterator message_end) {
|
||||||
|
// TODO:
|
||||||
|
// Masking key should (must) be derived from a high quality random
|
||||||
|
// number generator, to mitigate attacks on non-WebSocket friendly
|
||||||
|
// middleware:
|
||||||
|
const uint8_t masking_key[4] = { 0x12, 0x34, 0x56, 0x78 };
|
||||||
|
// TODO: consider acquiring a lock on txbuf...
|
||||||
|
if (readyState == CLOSING || readyState == CLOSED) { return; }
|
||||||
|
std::vector<uint8_t> header;
|
||||||
|
header.assign(2 + (message_size >= 126 ? 2 : 0) + (message_size >= 65536 ? 6 : 0) + (useMask ? 4 : 0), 0);
|
||||||
|
header[0] = 0x80 | type;
|
||||||
|
if (false) { }
|
||||||
|
else if (message_size < 126) {
|
||||||
|
header[1] = (message_size & 0xff) | (useMask ? 0x80 : 0);
|
||||||
|
if (useMask) {
|
||||||
|
header[2] = masking_key[0];
|
||||||
|
header[3] = masking_key[1];
|
||||||
|
header[4] = masking_key[2];
|
||||||
|
header[5] = masking_key[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (message_size < 65536) {
|
||||||
|
header[1] = 126 | (useMask ? 0x80 : 0);
|
||||||
|
header[2] = (message_size >> 8) & 0xff;
|
||||||
|
header[3] = (message_size >> 0) & 0xff;
|
||||||
|
if (useMask) {
|
||||||
|
header[4] = masking_key[0];
|
||||||
|
header[5] = masking_key[1];
|
||||||
|
header[6] = masking_key[2];
|
||||||
|
header[7] = masking_key[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else { // TODO: run coverage testing here
|
||||||
|
header[1] = 127 | (useMask ? 0x80 : 0);
|
||||||
|
header[2] = (message_size >> 56) & 0xff;
|
||||||
|
header[3] = (message_size >> 48) & 0xff;
|
||||||
|
header[4] = (message_size >> 40) & 0xff;
|
||||||
|
header[5] = (message_size >> 32) & 0xff;
|
||||||
|
header[6] = (message_size >> 24) & 0xff;
|
||||||
|
header[7] = (message_size >> 16) & 0xff;
|
||||||
|
header[8] = (message_size >> 8) & 0xff;
|
||||||
|
header[9] = (message_size >> 0) & 0xff;
|
||||||
|
if (useMask) {
|
||||||
|
header[10] = masking_key[0];
|
||||||
|
header[11] = masking_key[1];
|
||||||
|
header[12] = masking_key[2];
|
||||||
|
header[13] = masking_key[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// N.B. - txbuf will keep growing until it can be transmitted over the socket:
|
||||||
|
txbuf.insert(txbuf.end(), header.begin(), header.end());
|
||||||
|
txbuf.insert(txbuf.end(), message_begin, message_end);
|
||||||
|
if (useMask) {
|
||||||
|
size_t message_offset = txbuf.size() - message_size;
|
||||||
|
for (size_t i = 0; i != message_size; ++i) {
|
||||||
|
txbuf[message_offset + i] ^= masking_key[i&0x3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() {
|
||||||
|
if(readyState == CLOSING || readyState == CLOSED) { return; }
|
||||||
|
readyState = CLOSING;
|
||||||
|
uint8_t closeFrame[6] = {0x88, 0x80, 0x00, 0x00, 0x00, 0x00}; // last 4 bytes are a masking key
|
||||||
|
std::vector<uint8_t> header(closeFrame, closeFrame+6);
|
||||||
|
txbuf.insert(txbuf.end(), header.begin(), header.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
easywsclient::WebSocket::pointer from_url(const std::string& url, bool useMask, const std::string& origin) {
|
||||||
|
char host[512];
|
||||||
|
int port;
|
||||||
|
char path[512];
|
||||||
|
if (url.size() >= 512) {
|
||||||
|
fprintf(stderr, "ERROR: url size limit exceeded: %s\n", url.c_str());
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (origin.size() >= 200) {
|
||||||
|
fprintf(stderr, "ERROR: origin size limit exceeded: %s\n", origin.c_str());
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (false) { }
|
||||||
|
else if (sscanf(url.c_str(), "ws://%[^:/]:%d/%s", host, &port, path) == 3) {
|
||||||
|
}
|
||||||
|
else if (sscanf(url.c_str(), "ws://%[^:/]/%s", host, path) == 2) {
|
||||||
|
port = 80;
|
||||||
|
}
|
||||||
|
else if (sscanf(url.c_str(), "ws://%[^:/]:%d", host, &port) == 2) {
|
||||||
|
path[0] = '\0';
|
||||||
|
}
|
||||||
|
else if (sscanf(url.c_str(), "ws://%[^:/]", host) == 1) {
|
||||||
|
port = 80;
|
||||||
|
path[0] = '\0';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
fprintf(stderr, "ERROR: Could not parse WebSocket url: %s\n", url.c_str());
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
//fprintf(stderr, "easywsclient: connecting: host=%s port=%d path=/%s\n", host, port, path);
|
||||||
|
socket_t sockfd = hostname_connect(host, port);
|
||||||
|
if (sockfd == INVALID_SOCKET) {
|
||||||
|
fprintf(stderr, "Unable to connect to %s:%d\n", host, port);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// XXX: this should be done non-blocking,
|
||||||
|
char line[1024];
|
||||||
|
int status;
|
||||||
|
int i;
|
||||||
|
snprintf(line, 1024, "GET /%s HTTP/1.1\r\n", path); ::send(sockfd, line, strlen(line), 0);
|
||||||
|
if (port == 80) {
|
||||||
|
snprintf(line, 1024, "Host: %s\r\n", host); ::send(sockfd, line, strlen(line), 0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
snprintf(line, 1024, "Host: %s:%d\r\n", host, port); ::send(sockfd, line, strlen(line), 0);
|
||||||
|
}
|
||||||
|
snprintf(line, 1024, "Upgrade: websocket\r\n"); ::send(sockfd, line, strlen(line), 0);
|
||||||
|
snprintf(line, 1024, "Connection: Upgrade\r\n"); ::send(sockfd, line, strlen(line), 0);
|
||||||
|
if (!origin.empty()) {
|
||||||
|
snprintf(line, 1024, "Origin: %s\r\n", origin.c_str()); ::send(sockfd, line, strlen(line), 0);
|
||||||
|
}
|
||||||
|
snprintf(line, 1024, "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n"); ::send(sockfd, line, strlen(line), 0);
|
||||||
|
snprintf(line, 1024, "Sec-WebSocket-Version: 13\r\n"); ::send(sockfd, line, strlen(line), 0);
|
||||||
|
snprintf(line, 1024, "\r\n"); ::send(sockfd, line, strlen(line), 0);
|
||||||
|
for (i = 0; i < 2 || (i < 1023 && line[i-2] != '\r' && line[i-1] != '\n'); ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } }
|
||||||
|
line[i] = 0;
|
||||||
|
if (i == 1023) { fprintf(stderr, "ERROR: Got invalid status line connecting to: %s\n", url.c_str()); return NULL; }
|
||||||
|
if (sscanf(line, "HTTP/1.1 %d", &status) != 1 || status != 101) { fprintf(stderr, "ERROR: Got bad status connecting to %s: %s", url.c_str(), line); return NULL; }
|
||||||
|
// TODO: verify response headers,
|
||||||
|
while (true) {
|
||||||
|
for (i = 0; i < 2 || (i < 1023 && line[i-2] != '\r' && line[i-1] != '\n'); ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } }
|
||||||
|
if (line[0] == '\r' && line[1] == '\n') { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int flag = 1;
|
||||||
|
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (char*) &flag, sizeof(flag)); // Disable Nagle's algorithm
|
||||||
|
#ifdef _WIN32
|
||||||
|
u_long on = 1;
|
||||||
|
ioctlsocket(sockfd, FIONBIO, &on);
|
||||||
|
#else
|
||||||
|
fcntl(sockfd, F_SETFL, O_NONBLOCK);
|
||||||
|
#endif
|
||||||
|
//fprintf(stderr, "Connected to: %s\n", url.c_str());
|
||||||
|
return easywsclient::WebSocket::pointer(new _RealWebSocket(sockfd, useMask));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // end of module-only namespace
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
namespace easywsclient {
|
||||||
|
|
||||||
|
WebSocket::pointer WebSocket::create_dummy() {
|
||||||
|
static pointer dummy = pointer(new _DummyWebSocket);
|
||||||
|
return dummy;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
WebSocket::pointer WebSocket::from_url(const std::string& url, const std::string& origin) {
|
||||||
|
return ::from_url(url, true, origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocket::pointer WebSocket::from_url_no_mask(const std::string& url, const std::string& origin) {
|
||||||
|
return ::from_url(url, false, origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} // namespace easywsclient
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#ifndef EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD
|
||||||
|
#define EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD
|
||||||
|
|
||||||
|
// This code comes from:
|
||||||
|
// https://github.com/dhbaird/easywsclient
|
||||||
|
//
|
||||||
|
// To get the latest version:
|
||||||
|
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.hpp
|
||||||
|
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace easywsclient {
|
||||||
|
|
||||||
|
struct Callback_Imp { virtual void operator()(const std::string& message) = 0; };
|
||||||
|
struct BytesCallback_Imp { virtual void operator()(const std::vector<uint8_t>& message) = 0; };
|
||||||
|
|
||||||
|
class WebSocket {
|
||||||
|
public:
|
||||||
|
typedef WebSocket * pointer;
|
||||||
|
typedef enum readyStateValues { CLOSING, CLOSED, CONNECTING, OPEN } readyStateValues;
|
||||||
|
|
||||||
|
// Factories:
|
||||||
|
static pointer create_dummy();
|
||||||
|
static pointer from_url(const std::string& url, const std::string& origin = std::string());
|
||||||
|
static pointer from_url_no_mask(const std::string& url, const std::string& origin = std::string());
|
||||||
|
|
||||||
|
// Interfaces:
|
||||||
|
virtual ~WebSocket() { }
|
||||||
|
virtual void poll(int timeout = 0) = 0; // timeout in milliseconds
|
||||||
|
virtual void send(const std::string& message) = 0;
|
||||||
|
virtual void sendBinary(const std::string& message) = 0;
|
||||||
|
virtual void sendBinary(const std::vector<uint8_t>& message) = 0;
|
||||||
|
virtual void sendPing() = 0;
|
||||||
|
virtual void close() = 0;
|
||||||
|
virtual readyStateValues getReadyState() const = 0;
|
||||||
|
|
||||||
|
template<class Callable>
|
||||||
|
void dispatch(Callable callable)
|
||||||
|
// For callbacks that accept a string argument.
|
||||||
|
{ // N.B. this is compatible with both C++11 lambdas, functors and C function pointers
|
||||||
|
struct _Callback : public Callback_Imp {
|
||||||
|
Callable& callable;
|
||||||
|
_Callback(Callable& callable) : callable(callable) { }
|
||||||
|
void operator()(const std::string& message) { callable(message); }
|
||||||
|
};
|
||||||
|
_Callback callback(callable);
|
||||||
|
_dispatch(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class Callable>
|
||||||
|
void dispatchBinary(Callable callable)
|
||||||
|
// For callbacks that accept a std::vector<uint8_t> argument.
|
||||||
|
{ // N.B. this is compatible with both C++11 lambdas, functors and C function pointers
|
||||||
|
struct _Callback : public BytesCallback_Imp {
|
||||||
|
Callable& callable;
|
||||||
|
_Callback(Callable& callable) : callable(callable) { }
|
||||||
|
void operator()(const std::vector<uint8_t>& message) { callable(message); }
|
||||||
|
};
|
||||||
|
_Callback callback(callable);
|
||||||
|
_dispatchBinary(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void _dispatch(Callback_Imp& callable) = 0;
|
||||||
|
virtual void _dispatchBinary(BytesCallback_Imp& callable) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace easywsclient
|
||||||
|
|
||||||
|
#endif /* EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD */
|
||||||
+9
-1
@@ -878,7 +878,15 @@ inline void ImGui::FileBrowser::UpdateFileRecords()
|
|||||||
|
|
||||||
inline void ImGui::FileBrowser::SetPwdUncatched(const std::filesystem::path &pwd)
|
inline void ImGui::FileBrowser::SetPwdUncatched(const std::filesystem::path &pwd)
|
||||||
{
|
{
|
||||||
pwd_ = absolute(pwd);
|
// spice2x
|
||||||
|
// avoid MinGW std::filesystem::absolute() duplicating UNC server/share
|
||||||
|
// prefixes, which makes directory_iterator() throw during startup
|
||||||
|
// spice2x
|
||||||
|
const auto &nativePwd = pwd.native();
|
||||||
|
const bool hasUncPrefix = nativePwd.size() >= 2 &&
|
||||||
|
(nativePwd[0] == '\\' || nativePwd[0] == '/') &&
|
||||||
|
(nativePwd[1] == '\\' || nativePwd[1] == '/');
|
||||||
|
pwd_ = hasUncPrefix ? pwd : absolute(pwd);
|
||||||
UpdateFileRecords();
|
UpdateFileRecords();
|
||||||
selectedFilenames_.clear();
|
selectedFilenames_.clear();
|
||||||
(*inputNameBuf_)[0] = '\0';
|
(*inputNameBuf_)[0] = '\0';
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "acioemu/handle.h"
|
#include "acioemu/handle.h"
|
||||||
#include "misc/wintouchemu.h"
|
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "bi2x_hook.h"
|
#include "bi2x_hook.h"
|
||||||
#include "trackball.h"
|
#include "trackball.h"
|
||||||
|
|||||||
@@ -33,13 +33,14 @@ namespace games::ccj {
|
|||||||
static UINT WINAPI GetRawInputDeviceList_hook(PRAWINPUTDEVICELIST pRawInputDeviceList, PUINT puiNumDevices,
|
static UINT WINAPI GetRawInputDeviceList_hook(PRAWINPUTDEVICELIST pRawInputDeviceList, PUINT puiNumDevices,
|
||||||
UINT cbSize) {
|
UINT cbSize) {
|
||||||
auto result = GetRawInputDeviceList_orig(pRawInputDeviceList, puiNumDevices, cbSize);
|
auto result = GetRawInputDeviceList_orig(pRawInputDeviceList, puiNumDevices, cbSize);
|
||||||
if (result == 0xFFFFFFFF)
|
if (result == 0xFFFFFFFF) {
|
||||||
return result;
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
if (pRawInputDeviceList == NULL) {
|
if (pRawInputDeviceList == NULL) {
|
||||||
(*puiNumDevices)++;
|
(*puiNumDevices)++;
|
||||||
} else if (result < *puiNumDevices) {
|
} else if (result < *puiNumDevices) {
|
||||||
pRawInputDeviceList[result] = {fakeHandle, 0};
|
pRawInputDeviceList[result] = { fakeHandle, 0 };
|
||||||
result++;
|
result++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,8 +48,9 @@ namespace games::ccj {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static UINT WINAPI GetRawInputDeviceInfoW_hook(HANDLE hDevice, UINT uiCommand, LPVOID pData, PUINT pcbSize) {
|
static UINT WINAPI GetRawInputDeviceInfoW_hook(HANDLE hDevice, UINT uiCommand, LPVOID pData, PUINT pcbSize) {
|
||||||
if (hDevice != fakeHandle || uiCommand != RIDI_DEVICENAME)
|
if (hDevice != fakeHandle || uiCommand != RIDI_DEVICENAME) {
|
||||||
return GetRawInputDeviceInfoW_orig(hDevice, uiCommand, pData, pcbSize);
|
return GetRawInputDeviceInfoW_orig(hDevice, uiCommand, pData, pcbSize);
|
||||||
|
}
|
||||||
|
|
||||||
const auto requiredLen = (wcslen(fakeDeviceName) + 1) * sizeof(wchar_t);
|
const auto requiredLen = (wcslen(fakeDeviceName) + 1) * sizeof(wchar_t);
|
||||||
|
|
||||||
@@ -68,23 +70,157 @@ namespace games::ccj {
|
|||||||
|
|
||||||
static LONG_PTR WINAPI SetWindowLongPtrW_hook(HWND _hWnd, int nIndex, LONG_PTR dwNewLong) {
|
static LONG_PTR WINAPI SetWindowLongPtrW_hook(HWND _hWnd, int nIndex, LONG_PTR dwNewLong) {
|
||||||
wchar_t buffer[256];
|
wchar_t buffer[256];
|
||||||
if (nIndex != GWLP_WNDPROC || GetWindowTextW(_hWnd, buffer, 256) == 0 || !wcswcs(buffer, windowName))
|
if (nIndex != GWLP_WNDPROC || GetWindowTextW(_hWnd, buffer, 256) == 0 || !wcswcs(buffer, windowName)) {
|
||||||
return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong);
|
return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong);
|
||||||
|
}
|
||||||
|
|
||||||
hWnd = _hWnd;
|
hWnd = _hWnd;
|
||||||
wndProc = (WNDPROC)dwNewLong;
|
wndProc = (WNDPROC)dwNewLong;
|
||||||
return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong);
|
return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compute the cursor wrap region in client coordinates. The cursor is confined to the
|
||||||
|
// monitor, so if the window's client area extends past a screen edge (window larger than /
|
||||||
|
// offset off the monitor) the cursor can never reach that far client edge. Clamp the region
|
||||||
|
// to the on-screen portion of the client area so the right/bottom edges wrap as reliably as
|
||||||
|
// the left/top edges.
|
||||||
|
static RECT trackball_wrap_bounds(HWND wnd, const RECT &client) {
|
||||||
|
RECT bounds = client;
|
||||||
|
|
||||||
|
POINT origin = { 0, 0 };
|
||||||
|
ClientToScreen(wnd, &origin);
|
||||||
|
RECT clientScreen = {
|
||||||
|
origin.x,
|
||||||
|
origin.y,
|
||||||
|
origin.x + client.right,
|
||||||
|
origin.y + client.bottom
|
||||||
|
};
|
||||||
|
|
||||||
|
MONITORINFO mi = {};
|
||||||
|
mi.cbSize = sizeof(mi);
|
||||||
|
RECT usable;
|
||||||
|
if (GetMonitorInfo(MonitorFromWindow(wnd, MONITOR_DEFAULTTONEAREST), &mi)
|
||||||
|
&& IntersectRect(&usable, &clientScreen, &mi.rcMonitor)) {
|
||||||
|
bounds.left = usable.left - origin.x;
|
||||||
|
bounds.top = usable.top - origin.y;
|
||||||
|
bounds.right = usable.right - origin.x;
|
||||||
|
bounds.bottom = usable.bottom - origin.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// drive the trackball from the physical mouse cursor, wrapping it at the window edges so it
|
||||||
|
// can spin indefinitely. gated by the secondary-mouse button (hold or debounced toggle).
|
||||||
|
static void trackball_mouse_input(RAWMOUSE &rawMouse) {
|
||||||
|
static bool active = false;
|
||||||
|
static bool lastState = false;
|
||||||
|
static std::chrono::steady_clock::time_point lastModified = std::chrono::steady_clock::now();
|
||||||
|
static const std::chrono::milliseconds debounceDuration(100);
|
||||||
|
|
||||||
|
const auto currentTime = std::chrono::steady_clock::now();
|
||||||
|
const bool pressed = get_async_secondary_mouse();
|
||||||
|
const bool focused = GetForegroundWindow() == hWnd;
|
||||||
|
|
||||||
|
if (focused && MOUSE_TRACKBALL_USE_TOGGLE && pressed && (currentTime - lastModified > debounceDuration)) {
|
||||||
|
active = !active;
|
||||||
|
lastModified = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool engaged = focused
|
||||||
|
&& ((MOUSE_TRACKBALL_USE_TOGGLE && active) || (!MOUSE_TRACKBALL_USE_TOGGLE && pressed));
|
||||||
|
if (!engaged) {
|
||||||
|
if (lastState && !active) {
|
||||||
|
lastState = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
POINT cursor;
|
||||||
|
RECT client;
|
||||||
|
GetClientRect(hWnd, &client);
|
||||||
|
GetCursorPos(&cursor);
|
||||||
|
ScreenToClient(hWnd, &cursor);
|
||||||
|
|
||||||
|
const RECT bounds = trackball_wrap_bounds(hWnd, client);
|
||||||
|
|
||||||
|
static int lastX = cursor.x;
|
||||||
|
static int lastY = cursor.y;
|
||||||
|
if (!lastState) {
|
||||||
|
lastX = cursor.x;
|
||||||
|
lastY = cursor.y;
|
||||||
|
lastState = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
|
||||||
|
rawMouse.lLastX = (int)((float)(cursor.x - lastX) * (float)TRACKBALL_SENSITIVITY / 20.0f);
|
||||||
|
rawMouse.lLastY = (int)((float)(lastY - cursor.y) * (float)TRACKBALL_SENSITIVITY / 20.0f);
|
||||||
|
|
||||||
|
// wrap the cursor to the opposite edge once it reaches a boundary, so the trackball
|
||||||
|
// can keep spinning past the screen edge.
|
||||||
|
bool updateCursor = false;
|
||||||
|
auto wrap = [&updateCursor](LONG value, LONG lo, LONG hi) -> LONG {
|
||||||
|
if (value <= lo) {
|
||||||
|
updateCursor = true;
|
||||||
|
return hi - 5;
|
||||||
|
}
|
||||||
|
if (value >= hi - 1) {
|
||||||
|
updateCursor = true;
|
||||||
|
return lo + 5;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
cursor.x = wrap(cursor.x, bounds.left, bounds.right);
|
||||||
|
cursor.y = wrap(cursor.y, bounds.top, bounds.bottom);
|
||||||
|
|
||||||
|
lastX = cursor.x;
|
||||||
|
lastY = cursor.y;
|
||||||
|
|
||||||
|
if (updateCursor) {
|
||||||
|
ClientToScreen(hWnd, &cursor);
|
||||||
|
SetCursorPos(cursor.x, cursor.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// drive the trackball from the configured analog axes / direction buttons.
|
||||||
|
static void trackball_mapped_input(RAWMOUSE &rawMouse) {
|
||||||
|
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
|
||||||
|
|
||||||
|
auto &analogs = get_analogs();
|
||||||
|
if (analogs[Analogs::Trackball_DX].isSet() || analogs[Analogs::Trackball_DY].isSet()) {
|
||||||
|
float x = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DX]) * 2.0f - 1.0f;
|
||||||
|
float y = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DY]) * 2.0f - 1.0f;
|
||||||
|
rawMouse.lLastX = (long) (x * (float) TRACKBALL_SENSITIVITY);
|
||||||
|
rawMouse.lLastY = (long) (-y * (float) TRACKBALL_SENSITIVITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto &buttons = get_buttons();
|
||||||
|
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Up])) {
|
||||||
|
rawMouse.lLastY = TRACKBALL_SENSITIVITY;
|
||||||
|
}
|
||||||
|
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Down])) {
|
||||||
|
rawMouse.lLastY = -TRACKBALL_SENSITIVITY;
|
||||||
|
}
|
||||||
|
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Left])) {
|
||||||
|
rawMouse.lLastX = -TRACKBALL_SENSITIVITY;
|
||||||
|
}
|
||||||
|
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Right])) {
|
||||||
|
rawMouse.lLastX = TRACKBALL_SENSITIVITY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static UINT WINAPI GetRawInputData_hook(HRAWINPUT hRawInput, UINT uiCommand, LPVOID pData, PUINT pcbSize, UINT cbSizeHeader) {
|
static UINT WINAPI GetRawInputData_hook(HRAWINPUT hRawInput, UINT uiCommand, LPVOID pData, PUINT pcbSize, UINT cbSizeHeader) {
|
||||||
if (hRawInput != fakeHandle)
|
if (hRawInput != fakeHandle) {
|
||||||
return GetRawInputData_orig(hRawInput, uiCommand, pData, pcbSize, cbSizeHeader);
|
return GetRawInputData_orig(hRawInput, uiCommand, pData, pcbSize, cbSizeHeader);
|
||||||
|
}
|
||||||
|
|
||||||
if (pData == NULL) {
|
if (pData == NULL) {
|
||||||
if (uiCommand == RID_HEADER)
|
if (uiCommand == RID_HEADER) {
|
||||||
*pcbSize = sizeof(RAWINPUTHEADER);
|
*pcbSize = sizeof(RAWINPUTHEADER);
|
||||||
else
|
} else {
|
||||||
*pcbSize = sizeof(RAWINPUT);
|
*pcbSize = sizeof(RAWINPUT);
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,92 +243,9 @@ namespace games::ccj {
|
|||||||
RAWMOUSE rawMouse {};
|
RAWMOUSE rawMouse {};
|
||||||
|
|
||||||
if (MOUSE_TRACKBALL) {
|
if (MOUSE_TRACKBALL) {
|
||||||
static bool active = false;
|
trackball_mouse_input(rawMouse);
|
||||||
static bool lastState = false;
|
|
||||||
|
|
||||||
static std::chrono::time_point<std::chrono::steady_clock> lastModified = std::chrono::steady_clock::now();
|
|
||||||
static std::chrono::milliseconds debounceDuration(100);
|
|
||||||
auto currentTime = std::chrono::steady_clock::now();
|
|
||||||
bool pressed = get_async_secondary_mouse();
|
|
||||||
bool focused = GetForegroundWindow() == hWnd;
|
|
||||||
|
|
||||||
if (focused && MOUSE_TRACKBALL_USE_TOGGLE && pressed && (currentTime - lastModified > debounceDuration)) {
|
|
||||||
active = !active;
|
|
||||||
lastModified = currentTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (focused && ((MOUSE_TRACKBALL_USE_TOGGLE && active) || (!MOUSE_TRACKBALL_USE_TOGGLE && pressed))) {
|
|
||||||
POINT cursor;
|
|
||||||
RECT client;
|
|
||||||
|
|
||||||
GetClientRect(hWnd, &client);
|
|
||||||
int sx = client.right - client.left;
|
|
||||||
int sy = client.bottom - client.top;
|
|
||||||
|
|
||||||
GetCursorPos(&cursor);
|
|
||||||
ScreenToClient(hWnd, &cursor);
|
|
||||||
|
|
||||||
static int lastX = cursor.x;
|
|
||||||
static int lastY = cursor.y;
|
|
||||||
|
|
||||||
if (!lastState) {
|
|
||||||
lastX = cursor.x;
|
|
||||||
lastY = cursor.y;
|
|
||||||
lastState = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
|
|
||||||
rawMouse.lLastX = (int)((float)(cursor.x - lastX) * (float)TRACKBALL_SENSITIVITY / 20.0f);
|
|
||||||
rawMouse.lLastY = (int)((float)(lastY - cursor.y) * (float)TRACKBALL_SENSITIVITY / 20.0f);
|
|
||||||
|
|
||||||
bool updateCursor = false;
|
|
||||||
|
|
||||||
if (cursor.x <= 0) {
|
|
||||||
updateCursor = true;
|
|
||||||
cursor.x = sx - 5;
|
|
||||||
} else if (cursor.x >= sx - 1) {
|
|
||||||
updateCursor = true;
|
|
||||||
cursor.x = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cursor.y <= 0) {
|
|
||||||
updateCursor = true;
|
|
||||||
cursor.y = sy - 5;
|
|
||||||
} else if (cursor.y >= sy - 1) {
|
|
||||||
updateCursor = true;
|
|
||||||
cursor.y = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastX = cursor.x;
|
|
||||||
lastY = cursor.y;
|
|
||||||
|
|
||||||
if (updateCursor) {
|
|
||||||
ClientToScreen(hWnd, &cursor);
|
|
||||||
SetCursorPos(cursor.x, cursor.y);
|
|
||||||
}
|
|
||||||
} else if (lastState && !active) {
|
|
||||||
lastState = false;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
|
trackball_mapped_input(rawMouse);
|
||||||
|
|
||||||
auto &analogs = get_analogs();
|
|
||||||
if (analogs[Analogs::Trackball_DX].isSet() || analogs[Analogs::Trackball_DY].isSet()) {
|
|
||||||
float x = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DX]) * 2.0f - 1.0f;
|
|
||||||
float y = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DY]) * 2.0f - 1.0f;
|
|
||||||
rawMouse.lLastX = (long) (x * (float) TRACKBALL_SENSITIVITY);
|
|
||||||
rawMouse.lLastY = (long) (-y * (float) TRACKBALL_SENSITIVITY);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto &buttons = get_buttons();
|
|
||||||
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Up]))
|
|
||||||
rawMouse.lLastY = TRACKBALL_SENSITIVITY;
|
|
||||||
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Down]))
|
|
||||||
rawMouse.lLastY = -TRACKBALL_SENSITIVITY;
|
|
||||||
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Left]))
|
|
||||||
rawMouse.lLastX = -TRACKBALL_SENSITIVITY;
|
|
||||||
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Right]))
|
|
||||||
rawMouse.lLastX = TRACKBALL_SENSITIVITY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*((RAWINPUT*)pData) = { header, { rawMouse } };
|
*((RAWINPUT*)pData) = { header, { rawMouse } };
|
||||||
@@ -224,9 +277,8 @@ namespace games::ccj {
|
|||||||
static bool initialized = false;
|
static bool initialized = false;
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
return;
|
return;
|
||||||
} else {
|
|
||||||
initialized = true;
|
|
||||||
}
|
}
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
// announce
|
// announce
|
||||||
log_info("trackball", "init");
|
log_info("trackball", "init");
|
||||||
@@ -246,8 +298,6 @@ namespace games::ccj {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void trackball_thread_start() {
|
void trackball_thread_start() {
|
||||||
using namespace std::chrono_literals;
|
|
||||||
|
|
||||||
tbThreadRunning = true;
|
tbThreadRunning = true;
|
||||||
|
|
||||||
log_info("trackball", "thread start, use mouse: {}, toggle: {}", MOUSE_TRACKBALL, MOUSE_TRACKBALL_USE_TOGGLE);
|
log_info("trackball", "thread start, use mouse: {}, toggle: {}", MOUSE_TRACKBALL, MOUSE_TRACKBALL_USE_TOGGLE);
|
||||||
@@ -259,8 +309,9 @@ namespace games::ccj {
|
|||||||
wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle);
|
wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tbThreadRunning)
|
if (!tbThreadRunning) {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
timer.sleep(10);
|
timer.sleep(10);
|
||||||
}
|
}
|
||||||
@@ -269,8 +320,9 @@ namespace games::ccj {
|
|||||||
|
|
||||||
void trackball_thread_stop() {
|
void trackball_thread_stop() {
|
||||||
tbThreadRunning = false;
|
tbThreadRunning = false;
|
||||||
if (tbThread)
|
if (tbThread) {
|
||||||
tbThread->join();
|
tbThread->join();
|
||||||
|
}
|
||||||
|
|
||||||
log_info("trackball", "thread stop");
|
log_info("trackball", "thread stop");
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,20 @@
|
|||||||
#include "asio.h"
|
#include "asio.h"
|
||||||
#include "handle.h"
|
#include "handle.h"
|
||||||
#include "bi2x_hook.h"
|
#include "bi2x_hook.h"
|
||||||
|
#include <span>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "cfg/configurator.h"
|
||||||
|
#include "cfg/screen_resize.h"
|
||||||
|
|
||||||
#include <ks.h>
|
#include <ks.h>
|
||||||
#include <ksmedia.h>
|
#include <ksmedia.h>
|
||||||
|
|
||||||
#include "cfg/configurator.h"
|
|
||||||
#include "hooks/audio/audio.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"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
|
#include "touch/native/nativetouchhook.h"
|
||||||
#include "util/cpuutils.h"
|
#include "util/cpuutils.h"
|
||||||
#include "util/deferlog.h"
|
#include "util/deferlog.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
@@ -28,12 +31,14 @@ namespace games::gitadora {
|
|||||||
|
|
||||||
// settings
|
// settings
|
||||||
bool TWOCHANNEL = false;
|
bool TWOCHANNEL = false;
|
||||||
|
bool DISABLE_FRAME_LIMITER = false;
|
||||||
std::optional<unsigned int> CAB_TYPE = std::nullopt;
|
std::optional<unsigned int> CAB_TYPE = std::nullopt;
|
||||||
bool P1_LEFTY = false;
|
bool P1_LEFTY = false;
|
||||||
bool P2_LEFTY = false;
|
bool P2_LEFTY = false;
|
||||||
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
||||||
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
|
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
|
||||||
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
|
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
|
||||||
|
bool ARENA_TWO_HEAD_EXCLUSIVE = false;
|
||||||
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
||||||
bool ALLOW_REALTEK_AUDIO = false;
|
bool ALLOW_REALTEK_AUDIO = false;
|
||||||
|
|
||||||
@@ -61,6 +66,46 @@ namespace games::gitadora {
|
|||||||
|
|
||||||
return CreateDirectoryA(lpPathName, lpSecurityAttributes);
|
return CreateDirectoryA(lpPathName, lpSecurityAttributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// libshare-pj paces mainloop with separate 12 ms and 16 ms waits. these waits
|
||||||
|
// interfere with the game's normal display synchronization on modern Windows
|
||||||
|
// and can hold a nominal 60 FPS game near 58 FPS. locate the instruction
|
||||||
|
// sequences at runtime so the fix does not depend on per-version file offsets.
|
||||||
|
static void disable_mainloop_frame_limiter(HMODULE sharepj_module) {
|
||||||
|
if (!sharepj_module) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// both limiters have the same shape, only the millisecond target xx differs
|
||||||
|
// (0Ch for the 12 ms limiter, 10h for the 16 ms one):
|
||||||
|
//
|
||||||
|
// 48 83 F8 xx: cmp rax, xx; compare elapsed frame time with the target
|
||||||
|
// 73 10: jae +10h; skip the wait once the target has elapsed
|
||||||
|
// B9 xx 00 00 00: mov ecx, xx; load the target
|
||||||
|
// 48 2B C8: sub rcx, rax; calculate the remaining wait time
|
||||||
|
// 74 06: je +6h; skip the following six-byte Sleep call if no wait remains
|
||||||
|
//
|
||||||
|
// changing jae (73h) to jmp (EBh) makes each block always take its existing
|
||||||
|
// skip path, which bypasses only the associated Sleep call and leaves the 10h
|
||||||
|
// branch displacement and every other wait untouched.
|
||||||
|
const auto limiter_12ms_disabled = replace_pattern(
|
||||||
|
sharepj_module,
|
||||||
|
"4883F80C7310B90C000000482BC87406",
|
||||||
|
"????????EB??????????????????????", 0, 0);
|
||||||
|
const auto limiter_16ms_disabled = replace_pattern(
|
||||||
|
sharepj_module,
|
||||||
|
"4883F8107310B910000000482BC87406",
|
||||||
|
"????????EB??????????????????????", 0, 0);
|
||||||
|
|
||||||
|
if (!limiter_12ms_disabled || !limiter_16ms_disabled) {
|
||||||
|
log_fatal(
|
||||||
|
"gitadora",
|
||||||
|
"failed to disable libshare-pj mainloop frame limiter (-gdnoframelimiter), ensure patch is not already applied");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info("gitadora", "successfully disabled libshare-pj mainloop frame limiter (-gdnoframelimiter)");
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -270,9 +315,21 @@ namespace games::gitadora {
|
|||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
if (!GRAPHICS_WINDOWED) {
|
if (!GRAPHICS_WINDOWED) {
|
||||||
|
if (D3D9_ADAPTER.has_value()) {
|
||||||
log_fatal(
|
log_fatal(
|
||||||
"gitadora",
|
"gitadora",
|
||||||
"arena model: 2-window mode is not supported in fullscreen, choose 1 or 4");
|
"arena model: fullscreen two-window mode cannot use -monitor; "
|
||||||
|
"use -w for borderless windows instead");
|
||||||
|
}
|
||||||
|
ARENA_TWO_HEAD_EXCLUSIVE = true;
|
||||||
|
log_info(
|
||||||
|
"gitadora",
|
||||||
|
"arena model: native two-head fullscreen adapter-group mode "
|
||||||
|
"(MAIN + SMALL; LEFT/RIGHT are virtual)");
|
||||||
|
} else {
|
||||||
|
log_info(
|
||||||
|
"gitadora",
|
||||||
|
"arena model: two-window mode uses windowed rendering");
|
||||||
}
|
}
|
||||||
log_info("gitadora", "arena model: two-window mode");
|
log_info("gitadora", "arena model: two-window mode");
|
||||||
GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS = true;
|
GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS = true;
|
||||||
@@ -298,10 +355,18 @@ namespace games::gitadora {
|
|||||||
static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr;
|
static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr;
|
||||||
static decltype(DisplayConfigGetDeviceInfo) *DisplayConfigGetDeviceInfo_orig = nullptr;
|
static decltype(DisplayConfigGetDeviceInfo) *DisplayConfigGetDeviceInfo_orig = nullptr;
|
||||||
|
|
||||||
// cached primary real monitor: its path + source/target mode entries.
|
// Cached real monitor paths and their source/target mode entries. modeInfoIdx values are
|
||||||
// modeInfoIdx values on the path are renumbered to 0 / 1 so the cache is self-contained.
|
// renumbered so the cache is self-contained. The normal single-head emulation keeps only
|
||||||
|
// MAIN; the two-head adapter-group mode keeps MAIN and SMALL as real heads.
|
||||||
static DISPLAYCONFIG_PATH_INFO real_primary_path = {};
|
static DISPLAYCONFIG_PATH_INFO real_primary_path = {};
|
||||||
static DISPLAYCONFIG_MODE_INFO real_primary_modes[2] = {}; // [0]=source, [1]=target
|
static DISPLAYCONFIG_MODE_INFO real_primary_modes[2] = {}; // [0]=source, [1]=target
|
||||||
|
static DISPLAYCONFIG_PATH_INFO real_small_path = {};
|
||||||
|
static DISPLAYCONFIG_MODE_INFO real_small_modes[2] = {}; // [0]=source, [1]=target
|
||||||
|
static bool real_small_path_available = false;
|
||||||
|
|
||||||
|
static bool is_two_head_exclusive() {
|
||||||
|
return ARENA_TWO_HEAD_EXCLUSIVE && !GRAPHICS_WINDOWED;
|
||||||
|
}
|
||||||
|
|
||||||
// fake monitors appended after the real ones. the game classifies monitors
|
// fake monitors appended after the real ones. the game classifies monitors
|
||||||
// by outputTechnology + connectorInstance:
|
// by outputTechnology + connectorInstance:
|
||||||
@@ -325,12 +390,31 @@ namespace games::gitadora {
|
|||||||
// adapter to a swap chain role via its DisplayConfig connector instance,
|
// adapter to a swap chain role via its DisplayConfig connector instance,
|
||||||
// so the entries here must be listed in the same order the wrapper enumerates
|
// so the entries here must be listed in the same order the wrapper enumerates
|
||||||
// them: id=1 -> adapter 1 -> left, id=2 -> adapter 2 -> right, id=3 -> adapter 3 -> small.
|
// them: id=1 -> adapter 1 -> left, id=2 -> adapter 2 -> right, id=3 -> adapter 3 -> small.
|
||||||
static constexpr FakeMonitor FAKE_MONITORS[] = {
|
// Normal single-head emulation: every non-MAIN role is fake.
|
||||||
|
static constexpr FakeMonitor FAKE_MONITORS_SINGLE_HEAD[] = {
|
||||||
{ 1, 1080, 1920, -100000, -100000, 0 }, // left (DP connector instance 0)
|
{ 1, 1080, 1920, -100000, -100000, 0 }, // left (DP connector instance 0)
|
||||||
{ 2, 1080, 1920, -200000, -200000, 1 }, // right (DP connector instance 1)
|
{ 2, 1080, 1920, -200000, -200000, 1 }, // right (DP connector instance 1)
|
||||||
{ 3, 800, 1280, -300000, -300000, 2 }, // small (DP connector instance 2, touch)
|
{ 3, 800, 1280, -300000, -300000, 2 }, // small (DP connector instance 2, touch)
|
||||||
};
|
};
|
||||||
static constexpr UINT32 FAKE_MONITOR_COUNT = static_cast<UINT32>(std::size(FAKE_MONITORS));
|
|
||||||
|
// Two-head fullscreen: adapters 0/1 are the real MAIN/SMALL group heads.
|
||||||
|
// Keep only LEFT/RIGHT fake, with their adapter ids matching the D3D9 wrapper.
|
||||||
|
static constexpr FakeMonitor FAKE_MONITORS_TWO_HEAD[] = {
|
||||||
|
{ 2, 1080, 1920, -200000, -200000, 0 }, // left (DP connector instance 0)
|
||||||
|
{ 3, 1080, 1920, -300000, -300000, 1 }, // right (DP connector instance 1)
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::span<const FakeMonitor> get_fake_monitors() {
|
||||||
|
if (is_two_head_exclusive()) {
|
||||||
|
return FAKE_MONITORS_TWO_HEAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FAKE_MONITORS_SINGLE_HEAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
static UINT32 real_monitor_count() {
|
||||||
|
return is_two_head_exclusive() ? 2 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
// call QueryDisplayConfig once, keep only the primary monitor's path and its
|
// call QueryDisplayConfig once, keep only the primary monitor's path and its
|
||||||
// two referenced modes (source + target). modeInfoIdx values are rewritten to
|
// two referenced modes (source + target). modeInfoIdx values are rewritten to
|
||||||
@@ -368,14 +452,56 @@ namespace games::gitadora {
|
|||||||
log_fatal("gitadora", "cache_primary_monitor_info: no primary monitor found");
|
log_fatal("gitadora", "cache_primary_monitor_info: no primary monitor found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (primary->targetInfo.modeInfoIdx >= all_modes.size()) {
|
||||||
|
log_fatal("gitadora", "cache_primary_monitor_info: primary target mode is missing");
|
||||||
|
}
|
||||||
|
|
||||||
real_primary_modes[0] = all_modes[primary->sourceInfo.modeInfoIdx];
|
real_primary_modes[0] = all_modes[primary->sourceInfo.modeInfoIdx];
|
||||||
real_primary_modes[1] = all_modes[primary->targetInfo.modeInfoIdx];
|
real_primary_modes[1] = all_modes[primary->targetInfo.modeInfoIdx];
|
||||||
real_primary_path = *primary;
|
real_primary_path = *primary;
|
||||||
real_primary_path.sourceInfo.modeInfoIdx = 0;
|
real_primary_path.sourceInfo.modeInfoIdx = 0;
|
||||||
real_primary_path.targetInfo.modeInfoIdx = 1;
|
real_primary_path.targetInfo.modeInfoIdx = 1;
|
||||||
|
|
||||||
|
real_small_path_available = false;
|
||||||
|
if (is_two_head_exclusive()) {
|
||||||
|
if (all_paths.size() != 2) {
|
||||||
|
log_fatal(
|
||||||
|
"gitadora",
|
||||||
|
"two-head fullscreen mode requires exactly two active display paths, found {}",
|
||||||
|
all_paths.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto secondary = std::find_if(all_paths.begin(), all_paths.end(),
|
||||||
|
[&](const auto &p) {
|
||||||
|
return &p != &*primary;
|
||||||
|
});
|
||||||
|
if (secondary == all_paths.end() ||
|
||||||
|
secondary->sourceInfo.modeInfoIdx >= all_modes.size() ||
|
||||||
|
secondary->targetInfo.modeInfoIdx >= all_modes.size()) {
|
||||||
|
log_fatal("gitadora", "could not identify the physical SMALL monitor");
|
||||||
|
}
|
||||||
|
const LUID primary_adapter = primary->sourceInfo.adapterId;
|
||||||
|
const LUID secondary_adapter = secondary->sourceInfo.adapterId;
|
||||||
|
const bool same_adapter = primary_adapter.HighPart == secondary_adapter.HighPart &&
|
||||||
|
primary_adapter.LowPart == secondary_adapter.LowPart;
|
||||||
|
if (!same_adapter) {
|
||||||
|
log_fatal(
|
||||||
|
"gitadora",
|
||||||
|
"MAIN and SMALL must use the same display adapter");
|
||||||
|
}
|
||||||
|
|
||||||
|
real_small_modes[0] = all_modes[secondary->sourceInfo.modeInfoIdx];
|
||||||
|
real_small_modes[1] = all_modes[secondary->targetInfo.modeInfoIdx];
|
||||||
|
real_small_path = *secondary;
|
||||||
|
real_small_path.sourceInfo.modeInfoIdx = 2;
|
||||||
|
real_small_path.targetInfo.modeInfoIdx = 3;
|
||||||
|
real_small_path_available = true;
|
||||||
|
|
||||||
|
log_info("gitadora", "cache_primary_monitor_info: cached real MAIN and SMALL monitors");
|
||||||
|
} else {
|
||||||
log_info("gitadora", "cache_primary_monitor_info: cached primary monitor");
|
log_info("gitadora", "cache_primary_monitor_info: cached primary monitor");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
LONG
|
LONG
|
||||||
@@ -389,33 +515,36 @@ namespace games::gitadora {
|
|||||||
static std::once_flag populate_once;
|
static std::once_flag populate_once;
|
||||||
std::call_once(populate_once, cache_primary_monitor_info);
|
std::call_once(populate_once, cache_primary_monitor_info);
|
||||||
|
|
||||||
// always report exactly 1 real + N fake monitors
|
const auto fake_monitors = get_fake_monitors();
|
||||||
*pNumPathArrayElements = 1 + FAKE_MONITOR_COUNT;
|
const auto fake_count = static_cast<UINT32>(fake_monitors.size());
|
||||||
*pNumModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
|
const UINT32 real_count = real_monitor_count();
|
||||||
|
*pNumPathArrayElements = real_count + fake_count;
|
||||||
|
*pNumModeInfoArrayElements = (real_count + fake_count) * 2;
|
||||||
|
|
||||||
log_info(
|
log_info(
|
||||||
"gitadora",
|
"gitadora",
|
||||||
"GetDisplayConfigBufferSizes: 1 real path + {} fake monitor(s)",
|
"GetDisplayConfigBufferSizes: {} real path(s) + {} fake monitor(s)",
|
||||||
FAKE_MONITOR_COUNT);
|
real_count,
|
||||||
|
fake_count);
|
||||||
|
|
||||||
return ERROR_SUCCESS;
|
return ERROR_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// write fake monitor i into the caller's path/mode arrays. layout (single-monitor
|
// Write a fake monitor after the real paths. The cache is packed as [source, target]
|
||||||
// assumption): index 0 in both arrays holds the cached primary real monitor, so
|
// mode pairs, so each path consumes two mode entries.
|
||||||
// fake i occupies path slot (1 + i) and mode slots (2 + i*2) / (2 + i*2 + 1).
|
|
||||||
static void insert_fake_monitor(
|
static void insert_fake_monitor(
|
||||||
DISPLAYCONFIG_PATH_INFO *paths,
|
DISPLAYCONFIG_PATH_INFO *paths,
|
||||||
DISPLAYCONFIG_MODE_INFO *modes,
|
DISPLAYCONFIG_MODE_INFO *modes,
|
||||||
UINT32 i)
|
const FakeMonitor &m,
|
||||||
|
UINT32 path_index,
|
||||||
|
UINT32 source_mode_index)
|
||||||
{
|
{
|
||||||
const FakeMonitor &m = FAKE_MONITORS[i];
|
const UINT32 src_idx = source_mode_index;
|
||||||
const UINT32 src_idx = 2 + i * 2;
|
|
||||||
const UINT32 tgt_idx = src_idx + 1;
|
const UINT32 tgt_idx = src_idx + 1;
|
||||||
const LUID adapter_id { .LowPart = static_cast<DWORD>(-m.id), .HighPart = -m.id };
|
const LUID adapter_id { .LowPart = static_cast<DWORD>(-m.id), .HighPart = -m.id };
|
||||||
const UINT32 uid = static_cast<UINT32>(-m.id);
|
const UINT32 uid = static_cast<UINT32>(-m.id);
|
||||||
|
|
||||||
paths[1 + i] = {
|
paths[path_index] = {
|
||||||
.sourceInfo = {
|
.sourceInfo = {
|
||||||
.adapterId = adapter_id,
|
.adapterId = adapter_id,
|
||||||
.id = uid,
|
.id = uid,
|
||||||
@@ -473,21 +602,35 @@ namespace games::gitadora {
|
|||||||
DISPLAYCONFIG_MODE_INFO* modeInfoArray,
|
DISPLAYCONFIG_MODE_INFO* modeInfoArray,
|
||||||
DISPLAYCONFIG_TOPOLOGY_ID* currentTopologyId)
|
DISPLAYCONFIG_TOPOLOGY_ID* currentTopologyId)
|
||||||
{
|
{
|
||||||
// copy cached primary real monitor into caller buffers at index 0
|
// Copy cached real monitor paths into the caller buffers.
|
||||||
pathArray[0] = real_primary_path;
|
pathArray[0] = real_primary_path;
|
||||||
modeInfoArray[0] = real_primary_modes[0];
|
modeInfoArray[0] = real_primary_modes[0];
|
||||||
modeInfoArray[1] = real_primary_modes[1];
|
modeInfoArray[1] = real_primary_modes[1];
|
||||||
*numPathArrayElements = 1 + FAKE_MONITOR_COUNT;
|
if (is_two_head_exclusive() && real_small_path_available) {
|
||||||
*numModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
|
pathArray[1] = real_small_path;
|
||||||
|
modeInfoArray[2] = real_small_modes[0];
|
||||||
|
modeInfoArray[3] = real_small_modes[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto fake_monitors = get_fake_monitors();
|
||||||
|
const auto fake_count = static_cast<UINT32>(fake_monitors.size());
|
||||||
|
const UINT32 real_count = real_monitor_count();
|
||||||
|
*numPathArrayElements = real_count + fake_count;
|
||||||
|
*numModeInfoArrayElements = (real_count + fake_count) * 2;
|
||||||
if (currentTopologyId != nullptr) {
|
if (currentTopologyId != nullptr) {
|
||||||
*currentTopologyId = DISPLAYCONFIG_TOPOLOGY_EXTEND;
|
*currentTopologyId = DISPLAYCONFIG_TOPOLOGY_EXTEND;
|
||||||
}
|
}
|
||||||
|
|
||||||
log_misc("gitadora", "QueryDisplayConfig returning fake monitor paths and modes");
|
log_misc("gitadora", "QueryDisplayConfig returning fake monitor paths and modes");
|
||||||
|
|
||||||
// append fake monitors after the real one
|
// Append fake monitors after the real path(s).
|
||||||
for (UINT32 i = 0; i < FAKE_MONITOR_COUNT; i++) {
|
for (UINT32 i = 0; i < fake_count; i++) {
|
||||||
insert_fake_monitor(pathArray, modeInfoArray, i);
|
insert_fake_monitor(
|
||||||
|
pathArray,
|
||||||
|
modeInfoArray,
|
||||||
|
fake_monitors[i],
|
||||||
|
real_count + i,
|
||||||
|
real_count * 2 + i * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ERROR_SUCCESS;
|
return ERROR_SUCCESS;
|
||||||
@@ -518,7 +661,8 @@ namespace games::gitadora {
|
|||||||
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
|
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
|
||||||
const LONG fake_id = -id;
|
const LONG fake_id = -id;
|
||||||
UINT32 conn_inst = 0xff;
|
UINT32 conn_inst = 0xff;
|
||||||
for (const auto& f : FAKE_MONITORS) {
|
const auto fake_monitors = get_fake_monitors();
|
||||||
|
for (const auto &f : fake_monitors) {
|
||||||
if (f.id == fake_id) {
|
if (f.id == fake_id) {
|
||||||
conn_inst = f.connector_instance;
|
conn_inst = f.connector_instance;
|
||||||
break;
|
break;
|
||||||
@@ -541,18 +685,27 @@ namespace games::gitadora {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
// override the cached primary real monitor target info to look like HDMI/0
|
// Override MAIN to HDMI/0 and retag the real second head as the DP/2 SMALL
|
||||||
|
// display expected by the cabinet software in two-head fullscreen mode.
|
||||||
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
|
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
|
||||||
const auto& target = real_primary_path.targetInfo;
|
const auto target_matches = [&](const DISPLAYCONFIG_PATH_TARGET_INFO &target) {
|
||||||
if (target.id == targetName->header.id &&
|
return target.id == targetName->header.id &&
|
||||||
target.adapterId.HighPart == targetName->header.adapterId.HighPart &&
|
target.adapterId.HighPart == targetName->header.adapterId.HighPart &&
|
||||||
target.adapterId.LowPart == targetName->header.adapterId.LowPart)
|
target.adapterId.LowPart == targetName->header.adapterId.LowPart;
|
||||||
{
|
};
|
||||||
|
if (target_matches(real_primary_path.targetInfo)) {
|
||||||
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI;
|
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI;
|
||||||
targetName->connectorInstance = 0;
|
targetName->connectorInstance = 0;
|
||||||
log_info("gitadora",
|
log_info("gitadora",
|
||||||
"overriding primary monitor (id={}) to pretend to be HDMI",
|
"overriding primary monitor (id={}) to pretend to be HDMI",
|
||||||
targetName->header.id);
|
targetName->header.id);
|
||||||
|
} else if (is_two_head_exclusive() && real_small_path_available &&
|
||||||
|
target_matches(real_small_path.targetInfo)) {
|
||||||
|
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL;
|
||||||
|
targetName->connectorInstance = 2;
|
||||||
|
log_info("gitadora",
|
||||||
|
"overriding secondary monitor (id={}) to pretend to be SMALL DP/2",
|
||||||
|
targetName->header.id);
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -575,6 +728,12 @@ namespace games::gitadora {
|
|||||||
HMODULE system_module = libutils::try_module("libsystem.dll");
|
HMODULE system_module = libutils::try_module("libsystem.dll");
|
||||||
|
|
||||||
// patches
|
// patches
|
||||||
|
#ifdef SPICE64
|
||||||
|
if (DISABLE_FRAME_LIMITER && !is_arena_model()) {
|
||||||
|
disable_mainloop_frame_limiter(sharepj_module);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
detour::inline_hook((void *) eam_network_detected_ip_change, libutils::try_proc(
|
detour::inline_hook((void *) eam_network_detected_ip_change, libutils::try_proc(
|
||||||
sharepj_module, "eam_network_detected_ip_change"));
|
sharepj_module, "eam_network_detected_ip_change"));
|
||||||
detour::inline_hook((void *) eam_network_settings_conflict, libutils::try_proc(
|
detour::inline_hook((void *) eam_network_settings_conflict, libutils::try_proc(
|
||||||
@@ -650,18 +809,28 @@ namespace games::gitadora {
|
|||||||
hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true;
|
hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// monitor/touch hooks (windowed or full screen)
|
// Single-window mode needs touch injection for its overlay. Two-head fullscreen
|
||||||
|
// mode uses the real SMALL output, so it only needs the display-topology shim.
|
||||||
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
// enable touch hook for subscreen overlay
|
// enable touch hook for subscreen overlay
|
||||||
|
const auto native_touch_ready = !wintouchemu::FORCE &&
|
||||||
|
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||||
|
if (!native_touch_ready) {
|
||||||
wintouchemu::FORCE = true;
|
wintouchemu::FORCE = true;
|
||||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||||
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
|
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if !SPICE_XP
|
#if !SPICE_XP
|
||||||
|
|
||||||
if (!GRAPHICS_WINDOWED) {
|
if (!GRAPHICS_WINDOWED &&
|
||||||
// monitor hook: always pretend to have 1 primary real monitor + 3 fake monitors
|
(GRAPHICS_PREVENT_SECONDARY_WINDOWS || ARENA_TWO_HEAD_EXCLUSIVE)) {
|
||||||
// (LEFT / RIGHT / SMALL) so the game accepts the arena cab topology
|
if (ARENA_TWO_HEAD_EXCLUSIVE) {
|
||||||
|
log_info(
|
||||||
|
"gitadora",
|
||||||
|
"exposing physical MAIN/SMALL and virtual LEFT/RIGHT");
|
||||||
|
}
|
||||||
GetDisplayConfigBufferSizes_orig =
|
GetDisplayConfigBufferSizes_orig =
|
||||||
detour::iat_try("GetDisplayConfigBufferSizes",
|
detour::iat_try("GetDisplayConfigBufferSizes",
|
||||||
GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE);
|
GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE);
|
||||||
@@ -673,8 +842,6 @@ namespace games::gitadora {
|
|||||||
DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE);
|
DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// window patch
|
// window patch
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ namespace games::gitadora {
|
|||||||
|
|
||||||
// settings
|
// settings
|
||||||
extern bool TWOCHANNEL;
|
extern bool TWOCHANNEL;
|
||||||
|
extern bool DISABLE_FRAME_LIMITER;
|
||||||
extern std::optional<unsigned int> CAB_TYPE;
|
extern std::optional<unsigned int> CAB_TYPE;
|
||||||
extern bool P1_LEFTY;
|
extern bool P1_LEFTY;
|
||||||
extern bool P2_LEFTY;
|
extern bool P2_LEFTY;
|
||||||
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
||||||
extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
|
extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
|
||||||
extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
|
extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
|
||||||
|
extern bool ARENA_TWO_HEAD_EXCLUSIVE;
|
||||||
extern std::optional<std::string> ASIO_DRIVER;
|
extern std::optional<std::string> ASIO_DRIVER;
|
||||||
extern bool ALLOW_REALTEK_AUDIO;
|
extern bool ALLOW_REALTEK_AUDIO;
|
||||||
|
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ namespace games::iidx {
|
|||||||
if (mediaTypePointer && mediaTypePointer->IsString()) {
|
if (mediaTypePointer && mediaTypePointer->IsString()) {
|
||||||
std::string mediaType = mediaTypePointer->GetString();
|
std::string mediaType = mediaTypePointer->GetString();
|
||||||
if (mediaType.length() > 0) {
|
if (mediaType.length() > 0) {
|
||||||
camera->m_selectedMediaTypeDescription = mediaType;
|
camera->SetSelectedMediaTypeDescription(mediaType);
|
||||||
camera->m_useAutoMediaType = false;
|
camera->m_useAutoMediaType = false;
|
||||||
} else {
|
} else {
|
||||||
camera->m_useAutoMediaType = true;
|
camera->m_useAutoMediaType = true;
|
||||||
@@ -547,7 +547,7 @@ namespace games::iidx {
|
|||||||
std::string drawModeString = drawModePointer->GetString();
|
std::string drawModeString = drawModePointer->GetString();
|
||||||
for (int j = 0; j < DRAW_MODE_SIZE; j++) {
|
for (int j = 0; j < DRAW_MODE_SIZE; j++) {
|
||||||
if (DRAW_MODE_LABELS[j].compare(drawModeString) == 0) {
|
if (DRAW_MODE_LABELS[j].compare(drawModeString) == 0) {
|
||||||
camera->m_drawMode = (LocalCameraDrawMode) j;
|
camera->m_drawMode.store((LocalCameraDrawMode) j);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -556,12 +556,12 @@ namespace games::iidx {
|
|||||||
// Flip
|
// Flip
|
||||||
auto flipHorizontalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipHorizontal").Get(doc);
|
auto flipHorizontalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipHorizontal").Get(doc);
|
||||||
if (flipHorizontalPointer && flipHorizontalPointer->IsBool()) {
|
if (flipHorizontalPointer && flipHorizontalPointer->IsBool()) {
|
||||||
camera->m_flipHorizontal = flipHorizontalPointer->GetBool();
|
camera->m_flipHorizontal.store(flipHorizontalPointer->GetBool());
|
||||||
}
|
}
|
||||||
|
|
||||||
auto flipVerticalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipVertical").Get(doc);
|
auto flipVerticalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipVertical").Get(doc);
|
||||||
if (flipVerticalPointer && flipVerticalPointer->IsBool()) {
|
if (flipVerticalPointer && flipVerticalPointer->IsBool()) {
|
||||||
camera->m_flipVertical = flipVerticalPointer->GetBool();
|
camera->m_flipVertical.store(flipVerticalPointer->GetBool());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow manual control
|
// Allow manual control
|
||||||
@@ -625,15 +625,17 @@ namespace games::iidx {
|
|||||||
if (camera->m_useAutoMediaType) {
|
if (camera->m_useAutoMediaType) {
|
||||||
rapidjson::Pointer(root + "MediaType").Set(doc, "");
|
rapidjson::Pointer(root + "MediaType").Set(doc, "");
|
||||||
} else {
|
} else {
|
||||||
rapidjson::Pointer(root + "MediaType").Set(doc, camera->m_selectedMediaTypeDescription);
|
const auto mediaTypeDescription = camera->GetSelectedMediaTypeDescription();
|
||||||
|
rapidjson::Pointer(root + "MediaType").Set(doc, mediaTypeDescription);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw Mode
|
// Draw Mode
|
||||||
rapidjson::Pointer(root + "DrawMode").Set(doc, DRAW_MODE_LABELS[camera->m_drawMode]);
|
const auto drawMode = camera->m_drawMode.load();
|
||||||
|
rapidjson::Pointer(root + "DrawMode").Set(doc, DRAW_MODE_LABELS[drawMode]);
|
||||||
|
|
||||||
// Flip
|
// Flip
|
||||||
rapidjson::Pointer(root + "FlipHorizontal").Set(doc, camera->m_flipHorizontal);
|
rapidjson::Pointer(root + "FlipHorizontal").Set(doc, camera->m_flipHorizontal.load());
|
||||||
rapidjson::Pointer(root + "FlipVertical").Set(doc, camera->m_flipVertical);
|
rapidjson::Pointer(root + "FlipVertical").Set(doc, camera->m_flipVertical.load());
|
||||||
|
|
||||||
// Manual control
|
// Manual control
|
||||||
rapidjson::Pointer(root + "AllowManualControl").Set(doc, camera->m_allowManualControl);
|
rapidjson::Pointer(root + "AllowManualControl").Set(doc, camera->m_allowManualControl);
|
||||||
|
|||||||
@@ -20,9 +20,9 @@
|
|||||||
#include "hooks/sleephook.h"
|
#include "hooks/sleephook.h"
|
||||||
#include "launcher/options.h"
|
#include "launcher/options.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
#include "misc/nativetouchhook.h"
|
#include "touch/native/nativetouchhook.h"
|
||||||
#include "misc/wintouchemu.h"
|
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
|
#include "misc/wintouchemu.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/deferlog.h"
|
#include "util/deferlog.h"
|
||||||
#include "util/fileutils.h"
|
#include "util/fileutils.h"
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
#include "bi2x_hook.h"
|
#include "bi2x_hook.h"
|
||||||
#include "ezusb.h"
|
#include "ezusb.h"
|
||||||
#include "io.h"
|
#include "io.h"
|
||||||
|
#include "poke.h"
|
||||||
|
|
||||||
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
|
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
|
||||||
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
|
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
|
||||||
@@ -63,7 +64,8 @@ namespace games::iidx {
|
|||||||
bool TDJ_MODE = false;
|
bool TDJ_MODE = false;
|
||||||
bool FORCE_720P = false;
|
bool FORCE_720P = false;
|
||||||
bool DISABLE_ESPEC_IO = false;
|
bool DISABLE_ESPEC_IO = false;
|
||||||
bool NATIVE_TOUCH = false;
|
bool NATIVE_TOUCH = true;
|
||||||
|
bool ENABLE_POKE = false;
|
||||||
std::optional<std::string> SOUND_OUTPUT_DEVICE = std::nullopt;
|
std::optional<std::string> SOUND_OUTPUT_DEVICE = std::nullopt;
|
||||||
std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt;
|
std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt;
|
||||||
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
||||||
@@ -350,12 +352,15 @@ namespace games::iidx {
|
|||||||
// need to hook `avs2-core.dll` so AVS win32fs operations go through rom hook
|
// need to hook `avs2-core.dll` so AVS win32fs operations go through rom hook
|
||||||
devicehook_init(avs::core::DLL_INSTANCE);
|
devicehook_init(avs::core::DLL_INSTANCE);
|
||||||
|
|
||||||
if (NATIVE_TOUCH) {
|
NATIVE_TOUCH = !wintouchemu::FORCE &&
|
||||||
nativetouchhook::hook(avs::game::DLL_INSTANCE);
|
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||||
} else {
|
if (!NATIVE_TOUCH) {
|
||||||
wintouchemu::FORCE = true;
|
wintouchemu::FORCE = true;
|
||||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||||
wintouchemu::hook_title_ends("beatmania IIDX", "main", avs::game::DLL_INSTANCE);
|
wintouchemu::hook_title_ends(
|
||||||
|
"beatmania IIDX",
|
||||||
|
"main",
|
||||||
|
avs::game::DLL_INSTANCE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,6 +538,12 @@ namespace games::iidx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void IIDXGame::post_attach() {
|
||||||
|
if (ENABLE_POKE) {
|
||||||
|
poke::enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void IIDXGame::detach() {
|
void IIDXGame::detach() {
|
||||||
Game::detach();
|
Game::detach();
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ namespace games::iidx {
|
|||||||
extern bool FORCE_720P;
|
extern bool FORCE_720P;
|
||||||
extern bool DISABLE_ESPEC_IO;
|
extern bool DISABLE_ESPEC_IO;
|
||||||
extern bool NATIVE_TOUCH;
|
extern bool NATIVE_TOUCH;
|
||||||
|
extern bool ENABLE_POKE;
|
||||||
extern std::optional<std::string> SOUND_OUTPUT_DEVICE;
|
extern std::optional<std::string> SOUND_OUTPUT_DEVICE;
|
||||||
extern std::optional<std::string> ASIO_DRIVER;
|
extern std::optional<std::string> ASIO_DRIVER;
|
||||||
extern uint8_t DIGITAL_TT_SENS;
|
extern uint8_t DIGITAL_TT_SENS;
|
||||||
@@ -53,6 +54,7 @@ namespace games::iidx {
|
|||||||
|
|
||||||
virtual void attach() override;
|
virtual void attach() override;
|
||||||
virtual void pre_attach() override;
|
virtual void pre_attach() override;
|
||||||
|
virtual void post_attach() override;
|
||||||
virtual void detach() override;
|
virtual void detach() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#if SPICE64 && !SPICE_XP
|
#if SPICE64 && !SPICE_XP
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <d3d9.h>
|
#include <d3d9.h>
|
||||||
#include <dxva2api.h>
|
#include <dxva2api.h>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -66,6 +67,8 @@ extern std::string CAMERA_CONTROL_LABELS[];
|
|||||||
extern std::string DRAW_MODE_LABELS[];
|
extern std::string DRAW_MODE_LABELS[];
|
||||||
|
|
||||||
namespace games::iidx {
|
namespace games::iidx {
|
||||||
|
class IIDXCameraSourceReaderCallback;
|
||||||
|
|
||||||
namespace Camera {
|
namespace Camera {
|
||||||
struct PlayVideoCamera {
|
struct PlayVideoCamera {
|
||||||
IDirect3DTexture9** d3d9_texture(const uintptr_t offset) {
|
IDirect3DTexture9** d3d9_texture(const uintptr_t offset) {
|
||||||
@@ -87,7 +90,7 @@ namespace games::iidx {
|
|||||||
|
|
||||||
class IIDXLocalCamera {
|
class IIDXLocalCamera {
|
||||||
protected:
|
protected:
|
||||||
virtual ~IIDXLocalCamera() {};
|
virtual ~IIDXLocalCamera();
|
||||||
|
|
||||||
LONG m_nRefCount;
|
LONG m_nRefCount;
|
||||||
CRITICAL_SECTION m_critsec;
|
CRITICAL_SECTION m_critsec;
|
||||||
@@ -101,9 +104,14 @@ namespace games::iidx {
|
|||||||
// For reading frames from Camera
|
// For reading frames from Camera
|
||||||
IMFMediaSource *m_pSource = nullptr;
|
IMFMediaSource *m_pSource = nullptr;
|
||||||
IMFSourceReader *m_pSourceReader = nullptr;
|
IMFSourceReader *m_pSourceReader = nullptr;
|
||||||
|
IMFSourceReaderEx *m_pSourceReaderEx = nullptr;
|
||||||
|
IIDXCameraSourceReaderCallback *m_pSourceReaderCallback = nullptr;
|
||||||
|
std::mutex m_mediaTypeMutex;
|
||||||
|
IMFMediaType *m_pendingMediaType = nullptr;
|
||||||
|
int m_selectedMediaTypeIndex = 0;
|
||||||
|
std::string m_selectedMediaTypeDescription = "";
|
||||||
|
|
||||||
// Camera Format information
|
// Camera Format information
|
||||||
double m_frameRate = 0;
|
|
||||||
LONG m_cameraWidth;
|
LONG m_cameraWidth;
|
||||||
LONG m_cameraHeight;
|
LONG m_cameraHeight;
|
||||||
|
|
||||||
@@ -129,6 +137,12 @@ namespace games::iidx {
|
|||||||
LPDIRECT3DTEXTURE9 m_conversionTexture = nullptr;
|
LPDIRECT3DTEXTURE9 m_conversionTexture = nullptr;
|
||||||
IDirect3DSurface9 *m_pConversionSurf = nullptr;
|
IDirect3DSurface9 *m_pConversionSurf = nullptr;
|
||||||
|
|
||||||
|
// upload surface for decoded camera frames returned in system memory
|
||||||
|
IDirect3DSurface9 *m_pDecodedSurf = nullptr;
|
||||||
|
GUID m_decodedSubtype = GUID_NULL;
|
||||||
|
GUID m_outputSubtype = GUID_NULL;
|
||||||
|
bool m_drawErrorLogged = false;
|
||||||
|
|
||||||
// Texture for custom transform (e.g. horizontal flip)
|
// Texture for custom transform (e.g. horizontal flip)
|
||||||
LPDIRECT3DTEXTURE9 m_transformTexture = nullptr;
|
LPDIRECT3DTEXTURE9 m_transformTexture = nullptr;
|
||||||
IDirect3DSurface9 *m_pTransformSurf = nullptr;
|
IDirect3DSurface9 *m_pTransformSurf = nullptr;
|
||||||
@@ -153,21 +167,19 @@ namespace games::iidx {
|
|||||||
BOOL m_initialized = false;
|
BOOL m_initialized = false;
|
||||||
|
|
||||||
// True if all the setup steps succeeded
|
// True if all the setup steps succeeded
|
||||||
BOOL m_active = false;
|
std::atomic_bool m_active = false;
|
||||||
|
|
||||||
// Media type select
|
// Media type select
|
||||||
std::vector<MediaTypeInfo> m_mediaTypeInfos = {};
|
std::vector<MediaTypeInfo> m_mediaTypeInfos = {};
|
||||||
int m_selectedMediaTypeIndex = 0;
|
|
||||||
bool m_useAutoMediaType = true;
|
bool m_useAutoMediaType = true;
|
||||||
IMFMediaType *m_pAutoMediaType = nullptr;
|
IMFMediaType *m_pAutoMediaType = nullptr;
|
||||||
std::string m_selectedMediaTypeDescription = "";
|
|
||||||
bool m_allowManualControl = false;
|
bool m_allowManualControl = false;
|
||||||
|
|
||||||
LocalCameraDrawMode m_drawMode = DrawModeCrop4_3;
|
std::atomic<LocalCameraDrawMode> m_drawMode = DrawModeCrop4_3;
|
||||||
|
|
||||||
// Render processing
|
// Render processing
|
||||||
bool m_flipHorizontal = false;
|
std::atomic_bool m_flipHorizontal = false;
|
||||||
bool m_flipVertical = false;
|
std::atomic_bool m_flipVertical = false;
|
||||||
|
|
||||||
IIDXLocalCamera(
|
IIDXLocalCamera(
|
||||||
std::string name,
|
std::string name,
|
||||||
@@ -184,11 +196,14 @@ namespace games::iidx {
|
|||||||
HRESULT GetCameraControlProp(int index, CameraControlProp *pProp);
|
HRESULT GetCameraControlProp(int index, CameraControlProp *pProp);
|
||||||
HRESULT SetCameraControlProp(int index, long value, long flags);
|
HRESULT SetCameraControlProp(int index, long value, long flags);
|
||||||
HRESULT ResetCameraControlProps();
|
HRESULT ResetCameraControlProps();
|
||||||
HRESULT FlushDrawCommands();
|
|
||||||
std::string GetName();
|
std::string GetName();
|
||||||
std::string GetFriendlyName();
|
std::string GetFriendlyName();
|
||||||
std::string GetSymLink();
|
std::string GetSymLink();
|
||||||
|
int GetSelectedMediaTypeIndex();
|
||||||
|
std::string GetSelectedMediaTypeDescription();
|
||||||
|
void SetSelectedMediaTypeDescription(const std::string &description);
|
||||||
HRESULT ChangeMediaType(IMFMediaType *pType);
|
HRESULT ChangeMediaType(IMFMediaType *pType);
|
||||||
|
void RequestMediaType(IMFMediaType *pType);
|
||||||
HRESULT StartCapture();
|
HRESULT StartCapture();
|
||||||
void UpdateDrawRect();
|
void UpdateDrawRect();
|
||||||
|
|
||||||
@@ -197,12 +212,22 @@ namespace games::iidx {
|
|||||||
void CreateThread();
|
void CreateThread();
|
||||||
MediaTypeInfo GetMediaTypeInfo(IMFMediaType *pType);
|
MediaTypeInfo GetMediaTypeInfo(IMFMediaType *pType);
|
||||||
std::string GetVideoFormatName(GUID subtype);
|
std::string GetVideoFormatName(GUID subtype);
|
||||||
HRESULT TryMediaType(IMFMediaType *pType, UINT32 *pBestWidth, double *pBestFrameRate);
|
static bool CompareMediaTypes(const MediaTypeInfo &a, const MediaTypeInfo &b);
|
||||||
|
bool MatchesPreferredAspect(const MediaTypeInfo &info) const;
|
||||||
|
static bool IsBetterAutoType(const MediaTypeInfo &candidate, const MediaTypeInfo ¤t);
|
||||||
|
IMFMediaType *FindBestNativeAutoType(bool requirePreferredAspect) const;
|
||||||
|
IMFMediaType *FindBestAutoType(const GUID &subtype, bool requirePreferredAspect) const;
|
||||||
|
HRESULT ValidateMediaType(IMFMediaType *pType);
|
||||||
HRESULT InitTargetTexture();
|
HRESULT InitTargetTexture();
|
||||||
|
HRESULT EnsureTransformTextures();
|
||||||
HRESULT InitCameraControl();
|
HRESULT InitCameraControl();
|
||||||
|
void SetSelectedMediaType(int index, const std::string &description);
|
||||||
|
bool HasPendingMediaType();
|
||||||
|
HRESULT ApplyPendingMediaType();
|
||||||
|
HRESULT UploadDecodedSample(IMFMediaBuffer *pSrcBuffer);
|
||||||
HRESULT DrawSample(IMFMediaBuffer *pSrcBuffer);
|
HRESULT DrawSample(IMFMediaBuffer *pSrcBuffer);
|
||||||
HRESULT ReadSample();
|
HRESULT ReadSample();
|
||||||
LPDIRECT3DTEXTURE9 Render();
|
HRESULT Render();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ namespace games::iidx {
|
|||||||
_In_ UINT32 cInitialSize
|
_In_ UINT32 cInitialSize
|
||||||
);
|
);
|
||||||
|
|
||||||
|
typedef HRESULT (__stdcall * MFCreateMediaType_t)(
|
||||||
|
_Out_ IMFMediaType** ppMFType
|
||||||
|
);
|
||||||
|
|
||||||
typedef HRESULT (__stdcall * MFEnumDeviceSources_t)(
|
typedef HRESULT (__stdcall * MFEnumDeviceSources_t)(
|
||||||
_In_ IMFAttributes* pAttributes,
|
_In_ IMFAttributes* pAttributes,
|
||||||
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
||||||
@@ -35,6 +39,7 @@ namespace games::iidx {
|
|||||||
);
|
);
|
||||||
|
|
||||||
static MFCreateAttributes_t MFCreateAttributes = nullptr;
|
static MFCreateAttributes_t MFCreateAttributes = nullptr;
|
||||||
|
static MFCreateMediaType_t MFCreateMediaType = nullptr;
|
||||||
static MFEnumDeviceSources_t MFEnumDeviceSources = nullptr;
|
static MFEnumDeviceSources_t MFEnumDeviceSources = nullptr;
|
||||||
static MFCreateSourceReaderFromMediaSource_t MFCreateSourceReaderFromMediaSource = nullptr;
|
static MFCreateSourceReaderFromMediaSource_t MFCreateSourceReaderFromMediaSource = nullptr;
|
||||||
static MFGetService_t MFGetService = nullptr;
|
static MFGetService_t MFGetService = nullptr;
|
||||||
@@ -66,6 +71,12 @@ namespace games::iidx {
|
|||||||
log_fatal("mf_wrappers", "MFCreateAttributes failed to hook");
|
log_fatal("mf_wrappers", "MFCreateAttributes failed to hook");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MFCreateMediaType = (MFCreateMediaType_t)
|
||||||
|
libutils::get_proc(mfplat_dll, "MFCreateMediaType");
|
||||||
|
if (!MFCreateMediaType) {
|
||||||
|
log_fatal("mf_wrappers", "MFCreateMediaType failed to hook");
|
||||||
|
}
|
||||||
|
|
||||||
MFEnumDeviceSources = (MFEnumDeviceSources_t)
|
MFEnumDeviceSources = (MFEnumDeviceSources_t)
|
||||||
libutils::get_proc(mf_dll, "MFEnumDeviceSources");
|
libutils::get_proc(mf_dll, "MFEnumDeviceSources");
|
||||||
if (!MFEnumDeviceSources) {
|
if (!MFEnumDeviceSources) {
|
||||||
@@ -92,6 +103,11 @@ namespace games::iidx {
|
|||||||
return MFCreateAttributes(ppMFAttributes, cInitialSize);
|
return MFCreateAttributes(ppMFAttributes, cInitialSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HRESULT WrappedMFCreateMediaType (
|
||||||
|
_Out_ IMFMediaType** ppMFType) {
|
||||||
|
return MFCreateMediaType(ppMFType);
|
||||||
|
}
|
||||||
|
|
||||||
HRESULT WrappedMFEnumDeviceSources (
|
HRESULT WrappedMFEnumDeviceSources (
|
||||||
_In_ IMFAttributes* pAttributes,
|
_In_ IMFAttributes* pAttributes,
|
||||||
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ namespace games::iidx {
|
|||||||
_Out_ IMFAttributes** ppMFAttributes,
|
_Out_ IMFAttributes** ppMFAttributes,
|
||||||
_In_ UINT32 cInitialSize);
|
_In_ UINT32 cInitialSize);
|
||||||
|
|
||||||
|
HRESULT WrappedMFCreateMediaType (
|
||||||
|
_Out_ IMFMediaType** ppMFType);
|
||||||
|
|
||||||
HRESULT WrappedMFEnumDeviceSources (
|
HRESULT WrappedMFEnumDeviceSources (
|
||||||
_In_ IMFAttributes* pAttributes,
|
_In_ IMFAttributes* pAttributes,
|
||||||
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
||||||
|
|||||||
+25
-146
@@ -1,31 +1,17 @@
|
|||||||
#include "poke.h"
|
#include "poke.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
#include "windows.h"
|
#include "windows.h"
|
||||||
#include "cfg/screen_resize.h"
|
|
||||||
#include "games/io.h"
|
#include "games/io.h"
|
||||||
#include "games/iidx/iidx.h"
|
#include "games/iidx/iidx.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "launcher/shutdown.h"
|
#include "launcher/shutdown.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "overlay/overlay.h"
|
#include "touch/native/inject.h"
|
||||||
#include "overlay/windows/generic_sub.h"
|
|
||||||
#include "rawinput/rawinput.h"
|
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
#include "util/libutils.h"
|
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/precise_timer.h"
|
|
||||||
|
|
||||||
#define POKE_NATIVE_TOUCH 0
|
|
||||||
|
|
||||||
#if POKE_NATIVE_TOUCH
|
|
||||||
static HINSTANCE USER32_INSTANCE = nullptr;
|
|
||||||
typedef BOOL (WINAPI *InitializeTouchInjection_t)(UINT32, DWORD);
|
|
||||||
typedef BOOL (WINAPI *InjectTouchInput_t)(UINT32, POINTER_TOUCH_INFO*);
|
|
||||||
static InitializeTouchInjection_t pInitializeTouchInjection = nullptr;
|
|
||||||
static InjectTouchInput_t pInjectTouchInput = nullptr;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace games::iidx::poke {
|
namespace games::iidx::poke {
|
||||||
|
|
||||||
@@ -116,80 +102,13 @@ namespace games::iidx::poke {
|
|||||||
{"1/D", 50},
|
{"1/D", 50},
|
||||||
};
|
};
|
||||||
|
|
||||||
#if POKE_NATIVE_TOUCH
|
static void inject_native_touch_points(const std::vector<TouchPoint> &touch_points, bool down) {
|
||||||
void initialize_native_touch_library() {
|
if (touch_points.empty()) {
|
||||||
if (USER32_INSTANCE == nullptr) {
|
|
||||||
USER32_INSTANCE = libutils::load_library("user32.dll");
|
|
||||||
}
|
|
||||||
|
|
||||||
pInitializeTouchInjection = libutils::try_proc<InitializeTouchInjection_t>(
|
|
||||||
USER32_INSTANCE, "InitializeTouchInjection");
|
|
||||||
pInjectTouchInput = libutils::try_proc<InjectTouchInput_t>(
|
|
||||||
USER32_INSTANCE, "InjectTouchInput");
|
|
||||||
}
|
|
||||||
|
|
||||||
void emulate_native_touch(TouchPoint tp, bool is_down) {
|
|
||||||
if (pInjectTouchInput == nullptr) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
POINTER_TOUCH_INFO contact;
|
const auto &touch = touch_points.front();
|
||||||
BOOL bRet = TRUE;
|
nativetouch::inject::inject_synthetic_touch({ touch.x, touch.y }, down);
|
||||||
const int contact_offset = 2;
|
|
||||||
|
|
||||||
memset(&contact, 0, sizeof(POINTER_TOUCH_INFO));
|
|
||||||
|
|
||||||
contact.pointerInfo.pointerType = PT_TOUCH;
|
|
||||||
contact.pointerInfo.pointerId = 0;
|
|
||||||
contact.pointerInfo.ptPixelLocation.x = tp.x;
|
|
||||||
contact.pointerInfo.ptPixelLocation.y = tp.y;
|
|
||||||
if (is_down) {
|
|
||||||
contact.pointerInfo.pointerFlags = POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
|
|
||||||
} else {
|
|
||||||
contact.pointerInfo.pointerFlags = POINTER_FLAG_UP;
|
|
||||||
}
|
|
||||||
|
|
||||||
contact.pointerInfo.dwTime = 0;
|
|
||||||
contact.pointerInfo.PerformanceCount = 0;
|
|
||||||
|
|
||||||
contact.touchFlags = TOUCH_FLAG_NONE;
|
|
||||||
contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | TOUCH_MASK_PRESSURE;
|
|
||||||
contact.orientation = 0;
|
|
||||||
contact.pressure = 1024;
|
|
||||||
|
|
||||||
contact.rcContact.top = tp.y - contact_offset;
|
|
||||||
contact.rcContact.bottom = tp.y + contact_offset;
|
|
||||||
contact.rcContact.left = tp.x - contact_offset;
|
|
||||||
contact.rcContact.right = tp.x + contact_offset;
|
|
||||||
|
|
||||||
bRet = InjectTouchInput(1, &contact);
|
|
||||||
if (!bRet) {
|
|
||||||
log_warning("poke", "error injecting native touch {}: ({}, {}) error: {}", is_down ? "down" : "up", tp.x, tp.y, GetLastError());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void emulate_native_touch_points(std::vector<TouchPoint> *touch_points) {
|
|
||||||
int i = 0;
|
|
||||||
for (auto &touch : *touch_points) {
|
|
||||||
emulate_native_touch(touch, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void clear_native_touch_points(std::vector<TouchPoint> *touch_points) {
|
|
||||||
for (auto &touch : *touch_points) {
|
|
||||||
emulate_native_touch(touch, false);
|
|
||||||
}
|
|
||||||
touch_points->clear();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
void clear_touch_points(std::vector<TouchPoint> *touch_points) {
|
|
||||||
std::vector<DWORD> touch_ids;
|
|
||||||
for (auto &touch : *touch_points) {
|
|
||||||
touch_ids.emplace_back(touch.id);
|
|
||||||
}
|
|
||||||
touch_remove_points(&touch_ids);
|
|
||||||
touch_points->clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void enable() {
|
void enable() {
|
||||||
@@ -198,11 +117,14 @@ namespace games::iidx::poke {
|
|||||||
if (THREAD)
|
if (THREAD)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (!games::iidx::NATIVE_TOUCH) {
|
||||||
|
log_warning("poke", "keypad touch emulation requires native touch; disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// create new thread
|
// create new thread
|
||||||
THREAD_RUNNING = true;
|
THREAD_RUNNING = true;
|
||||||
THREAD = new std::thread([] {
|
THREAD = new std::thread([] {
|
||||||
timeutils::PreciseSleepTimer timer;
|
|
||||||
|
|
||||||
// log
|
// log
|
||||||
log_info("poke", "enabled");
|
log_info("poke", "enabled");
|
||||||
|
|
||||||
@@ -210,35 +132,13 @@ namespace games::iidx::poke {
|
|||||||
std::vector<TouchPoint> touch_points;
|
std::vector<TouchPoint> touch_points;
|
||||||
std::vector<uint16_t> last_keypad_state = {0, 0};
|
std::vector<uint16_t> last_keypad_state = {0, 0};
|
||||||
|
|
||||||
#if POKE_NATIVE_TOUCH
|
|
||||||
bool use_native = games::iidx::NATIVE_TOUCH;
|
|
||||||
|
|
||||||
if (use_native) {
|
|
||||||
initialize_native_touch_library();
|
|
||||||
|
|
||||||
if (pInitializeTouchInjection != nullptr) {
|
|
||||||
pInitializeTouchInjection(1, TOUCH_FEEDBACK_NONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#else
|
|
||||||
bool use_native = false;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// set variable to false to stop
|
// set variable to false to stop
|
||||||
while (THREAD_RUNNING) {
|
while (THREAD_RUNNING) {
|
||||||
|
|
||||||
// clean up touch from last frame
|
// clean up touch from last frame
|
||||||
if (touch_points.size() > 0) {
|
if (touch_points.size() > 0) {
|
||||||
#if POKE_NATIVE_TOUCH
|
inject_native_touch_points(touch_points, false);
|
||||||
if (use_native) {
|
touch_points.clear();
|
||||||
clear_native_touch_points(&touch_points);
|
|
||||||
} else {
|
|
||||||
clear_touch_points(&touch_points);
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
clear_touch_points(&touch_points);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int unit = 0; unit < 2; unit++) {
|
for (int unit = 0; unit < 2; unit++) {
|
||||||
@@ -263,32 +163,15 @@ namespace games::iidx::poke {
|
|||||||
|
|
||||||
float x = x_iter->second / 1920.0;
|
float x = x_iter->second / 1920.0;
|
||||||
float y = y_iter->second / 1080.0;
|
float y = y_iter->second / 1080.0;
|
||||||
if (use_native) {
|
|
||||||
x *= rawinput::TOUCHSCREEN_RANGE_X;
|
|
||||||
y *= rawinput::TOUCHSCREEN_RANGE_Y;
|
|
||||||
} else if (GRAPHICS_IIDX_WSUB) {
|
|
||||||
// Scale to windowed subscreen
|
|
||||||
x *= GRAPHICS_WSUB_WIDTH;
|
|
||||||
y *= GRAPHICS_WSUB_HEIGHT;
|
|
||||||
} else if (GENERIC_SUB_WINDOW_FULLSIZE || !overlay::OVERLAY->get_active()) {
|
|
||||||
// Overlay is not present, scale to main screen
|
|
||||||
if (GRAPHICS_WINDOWED) {
|
if (GRAPHICS_WINDOWED) {
|
||||||
x *= SPICETOUCH_TOUCH_WIDTH;
|
if (SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
|
||||||
y *= SPICETOUCH_TOUCH_HEIGHT;
|
continue;
|
||||||
} else {
|
|
||||||
x *= ImGui::GetIO().DisplaySize.x;
|
|
||||||
y *= ImGui::GetIO().DisplaySize.y;
|
|
||||||
}
|
}
|
||||||
|
x = SPICETOUCH_TOUCH_X + x * SPICETOUCH_TOUCH_WIDTH;
|
||||||
|
y = SPICETOUCH_TOUCH_Y + y * SPICETOUCH_TOUCH_HEIGHT;
|
||||||
} else {
|
} else {
|
||||||
// Overlay subscreen
|
x = x_iter->second;
|
||||||
x = (GENERIC_SUB_WINDOW_X + x * GENERIC_SUB_WINDOW_WIDTH);
|
y = y_iter->second;
|
||||||
y = (GENERIC_SUB_WINDOW_Y + y * GENERIC_SUB_WINDOW_HEIGHT);
|
|
||||||
|
|
||||||
// Scale to window size ratio
|
|
||||||
if (GRAPHICS_WINDOWED) {
|
|
||||||
x *= SPICETOUCH_TOUCH_WIDTH / ImGui::GetIO().DisplaySize.x;
|
|
||||||
y *= SPICETOUCH_TOUCH_HEIGHT / ImGui::GetIO().DisplaySize.y;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TouchPoint tp {
|
TouchPoint tp {
|
||||||
@@ -309,19 +192,15 @@ namespace games::iidx::poke {
|
|||||||
} // for all units
|
} // for all units
|
||||||
|
|
||||||
if (touch_points.size() > 0) {
|
if (touch_points.size() > 0) {
|
||||||
#if POKE_NATIVE_TOUCH
|
inject_native_touch_points(touch_points, true);
|
||||||
if (use_native) {
|
|
||||||
emulate_native_touch_points(&touch_points);
|
|
||||||
} else {
|
|
||||||
touch_write_points(&touch_points);
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
touch_write_points(&touch_points);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// slow down
|
// slow down
|
||||||
timer.sleep(50);
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!touch_points.empty()) {
|
||||||
|
inject_native_touch_points(touch_points, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ namespace games {
|
|||||||
static bool IO_INITIALIZED = false;
|
static bool IO_INITIALIZED = false;
|
||||||
static std::vector<std::string> games;
|
static std::vector<std::string> games;
|
||||||
static robin_hood::unordered_map<std::string, std::vector<Button> &> buttons;
|
static robin_hood::unordered_map<std::string, std::vector<Button> &> buttons;
|
||||||
|
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_modifiers;
|
||||||
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_keypads;
|
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_keypads;
|
||||||
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_overlay;
|
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_overlay;
|
||||||
static robin_hood::unordered_map<std::string, std::string> buttons_help;
|
static robin_hood::unordered_map<std::string, std::string> buttons_help;
|
||||||
@@ -132,6 +133,7 @@ namespace games {
|
|||||||
buttons_help.insert({ jb, jb::get_buttons_help() });
|
buttons_help.insert({ jb, jb::get_buttons_help() });
|
||||||
lights.insert({ jb, jb::get_lights() });
|
lights.insert({ jb, jb::get_lights() });
|
||||||
file_hints[jb].push_back({"jubeat.dll"});
|
file_hints[jb].push_back({"jubeat.dll"});
|
||||||
|
file_hints[jb].push_back({"jubeat2019.dll"});
|
||||||
|
|
||||||
// mga
|
// mga
|
||||||
const std::string mga("Metal Gear");
|
const std::string mga("Metal Gear");
|
||||||
@@ -365,6 +367,37 @@ namespace games {
|
|||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static std::vector<Button> gen_buttons_modifiers(const std::string &game) {
|
||||||
|
auto modifier_buttons = GameAPI::Buttons::getButtons(game);
|
||||||
|
const std::vector<std::string> names {
|
||||||
|
"Modifier 1",
|
||||||
|
"Modifier 2",
|
||||||
|
"Modifier 3",
|
||||||
|
"Modifier 4",
|
||||||
|
};
|
||||||
|
modifier_buttons = GameAPI::Buttons::sortButtons(modifier_buttons, names);
|
||||||
|
for (auto &modifier : modifier_buttons) {
|
||||||
|
modifier.setModifierMask(0);
|
||||||
|
for (auto &alternative : modifier.getAlternatives()) {
|
||||||
|
alternative.setModifierMask(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modifier_buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Button> *get_buttons_modifiers(const std::string &game) {
|
||||||
|
initialize();
|
||||||
|
auto it = buttons_modifiers.find(game);
|
||||||
|
if (it == buttons_modifiers.end()) {
|
||||||
|
if (game.empty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
buttons_modifiers[game] = gen_buttons_modifiers(game);
|
||||||
|
return &buttons_modifiers[game];
|
||||||
|
}
|
||||||
|
return &it->second;
|
||||||
|
}
|
||||||
|
|
||||||
static std::vector<Button> gen_buttons_keypads(const std::string &game) {
|
static std::vector<Button> gen_buttons_keypads(const std::string &game) {
|
||||||
auto buttons = GameAPI::Buttons::getButtons(game);
|
auto buttons = GameAPI::Buttons::getButtons(game);
|
||||||
std::vector<std::string> names;
|
std::vector<std::string> names;
|
||||||
@@ -474,6 +507,8 @@ namespace games {
|
|||||||
vkey_defaults.push_back(VK_F12);
|
vkey_defaults.push_back(VK_F12);
|
||||||
names.emplace_back("Toggle Camera Control");
|
names.emplace_back("Toggle Camera Control");
|
||||||
vkey_defaults.push_back(0xFF);
|
vkey_defaults.push_back(0xFF);
|
||||||
|
names.emplace_back("Toggle OBS Control");
|
||||||
|
vkey_defaults.push_back(0xFF);
|
||||||
names.emplace_back("Player 1 PIN Macro");
|
names.emplace_back("Player 1 PIN Macro");
|
||||||
vkey_defaults.push_back(0xFF);
|
vkey_defaults.push_back(0xFF);
|
||||||
names.emplace_back("Player 2 PIN Macro");
|
names.emplace_back("Player 2 PIN Macro");
|
||||||
|
|||||||
@@ -5,6 +5,16 @@
|
|||||||
|
|
||||||
namespace games {
|
namespace games {
|
||||||
|
|
||||||
|
namespace ModifierButtons {
|
||||||
|
enum {
|
||||||
|
Modifier1,
|
||||||
|
Modifier2,
|
||||||
|
Modifier3,
|
||||||
|
Modifier4,
|
||||||
|
Size,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
namespace OverlayButtons {
|
namespace OverlayButtons {
|
||||||
enum {
|
enum {
|
||||||
Screenshot,
|
Screenshot,
|
||||||
@@ -23,6 +33,7 @@ namespace games {
|
|||||||
ToggleScreenResize,
|
ToggleScreenResize,
|
||||||
ToggleFps,
|
ToggleFps,
|
||||||
ToggleCameraControl,
|
ToggleCameraControl,
|
||||||
|
ToggleOBSControl,
|
||||||
TriggerPinMacroP1,
|
TriggerPinMacroP1,
|
||||||
TriggerPinMacroP2,
|
TriggerPinMacroP2,
|
||||||
ScreenResize,
|
ScreenResize,
|
||||||
@@ -58,6 +69,7 @@ namespace games {
|
|||||||
|
|
||||||
const std::vector<std::string> &get_games();
|
const std::vector<std::string> &get_games();
|
||||||
std::vector<Button> *get_buttons(const std::string &game);
|
std::vector<Button> *get_buttons(const std::string &game);
|
||||||
|
std::vector<Button> *get_buttons_modifiers(const std::string &game);
|
||||||
std::string get_buttons_help(const std::string &game);
|
std::string get_buttons_help(const std::string &game);
|
||||||
std::string get_analogs_help(const std::string &game);
|
std::string get_analogs_help(const std::string &game);
|
||||||
std::vector<Button> *get_buttons_keypads(const std::string &game);
|
std::vector<Button> *get_buttons_keypads(const std::string &game);
|
||||||
|
|||||||
@@ -0,0 +1,336 @@
|
|||||||
|
#include "bi2x_hook.h"
|
||||||
|
|
||||||
|
#if SPICE64
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "games/io.h"
|
||||||
|
#include "games/jb/jb_touch.h"
|
||||||
|
#include "io.h"
|
||||||
|
#include "misc/eamuse.h"
|
||||||
|
#include "rawinput/rawinput.h"
|
||||||
|
#include "util/detour.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
namespace games::jb {
|
||||||
|
|
||||||
|
struct AIO_SCI_COMM {
|
||||||
|
std::array<uint8_t, 0x100> data;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AIO_NMGR_IOB2;
|
||||||
|
|
||||||
|
struct AIO_NMGR_IOB2_VTABLE {
|
||||||
|
std::array<void *, 10> unused;
|
||||||
|
void (__fastcall *begin_manage)(AIO_NMGR_IOB2 *node_mgr);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AIO_NMGR_IOB2 {
|
||||||
|
AIO_NMGR_IOB2_VTABLE *vtable;
|
||||||
|
std::array<uint8_t, 0x78> data;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AIO_IOB2_BI2X_T44 {
|
||||||
|
std::array<uint8_t, 0x80> data;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AIO_IOB2_BI2X_T44_DEVSTATUS {
|
||||||
|
std::array<uint8_t, 0x140> data;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AIO_IOB2_BI2X_WRFIRM {
|
||||||
|
uint8_t data;
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(AIO_NMGR_IOB2) == 0x80);
|
||||||
|
static_assert(sizeof(AIO_IOB2_BI2X_T44_DEVSTATUS) == 0x140);
|
||||||
|
|
||||||
|
using aioIob2Bi2xT44_Create_t = AIO_IOB2_BI2X_T44 *(__fastcall *)(AIO_NMGR_IOB2 *node_mgr,
|
||||||
|
uint32_t device_id);
|
||||||
|
using aioIob2Bi2xT44_GetDeviceStatus_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node,
|
||||||
|
AIO_IOB2_BI2X_T44_DEVSTATUS *status);
|
||||||
|
using aioIob2Bi2xT44_IoReset_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node, uint32_t reset);
|
||||||
|
using aioIob2Bi2xT44_SetWatchDogTimer_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node, uint8_t count);
|
||||||
|
using aioIob2Bi2xT44_ControlCoinBlocker_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node,
|
||||||
|
uint32_t slot, bool open);
|
||||||
|
using aioIob2Bi2xT44_AddCounter_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node,
|
||||||
|
uint32_t counter, uint32_t count);
|
||||||
|
using aioIob2Bi2xT44_SetIccrLed_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node, uint32_t color);
|
||||||
|
using aioIob2Bi2xT44_SetTapeLedData_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node,
|
||||||
|
uint32_t tape, const void *data);
|
||||||
|
using aioIob2Bi2x_OpenSciUsbCdc_t = AIO_SCI_COMM *(__fastcall *)(uint32_t serial_number);
|
||||||
|
using aioIob2Bi2x_CreateWriteFirmContext_t = AIO_IOB2_BI2X_WRFIRM *(__fastcall *)(
|
||||||
|
uint32_t serial_number, uint32_t iob_mask);
|
||||||
|
using aioIob2Bi2x_DestroyWriteFirmContext_t = void (__fastcall *)(AIO_IOB2_BI2X_WRFIRM *context);
|
||||||
|
using aioIob2Bi2x_WriteFirmGetState_t = int32_t (__fastcall *)(AIO_IOB2_BI2X_WRFIRM *context);
|
||||||
|
using aioIob2Bi2x_WriteFirmIsCompleted_t = bool (__fastcall *)(int32_t state);
|
||||||
|
using aioIob2Bi2x_WriteFirmIsError_t = bool (__fastcall *)(int32_t state);
|
||||||
|
using aioNMgrIob2_Create_t = AIO_NMGR_IOB2 *(__fastcall *)(AIO_SCI_COMM *sci, uint32_t mode);
|
||||||
|
using aioSci_Destroy_t = void (__fastcall *)(AIO_SCI_COMM *sci);
|
||||||
|
using aioNodeMgr_Destroy_t = void (__fastcall *)(AIO_NMGR_IOB2 *node_mgr);
|
||||||
|
using aioNodeCtl_Destroy_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node);
|
||||||
|
using aioNodeCtl_UpdateDevicesStatus_t = void (__fastcall *)();
|
||||||
|
|
||||||
|
static aioIob2Bi2xT44_Create_t aioIob2Bi2xT44_Create_orig = nullptr;
|
||||||
|
static aioIob2Bi2xT44_GetDeviceStatus_t aioIob2Bi2xT44_GetDeviceStatus_orig = nullptr;
|
||||||
|
static aioIob2Bi2xT44_IoReset_t aioIob2Bi2xT44_IoReset_orig = nullptr;
|
||||||
|
static aioIob2Bi2xT44_SetWatchDogTimer_t aioIob2Bi2xT44_SetWatchDogTimer_orig = nullptr;
|
||||||
|
static aioIob2Bi2xT44_ControlCoinBlocker_t aioIob2Bi2xT44_ControlCoinBlocker_orig = nullptr;
|
||||||
|
static aioIob2Bi2xT44_AddCounter_t aioIob2Bi2xT44_AddCounter_orig = nullptr;
|
||||||
|
static aioIob2Bi2xT44_SetIccrLed_t aioIob2Bi2xT44_SetIccrLed_orig = nullptr;
|
||||||
|
static aioIob2Bi2xT44_SetTapeLedData_t aioIob2Bi2xT44_SetTapeLedData_orig = nullptr;
|
||||||
|
static aioIob2Bi2x_OpenSciUsbCdc_t aioIob2Bi2x_OpenSciUsbCdc_orig = nullptr;
|
||||||
|
static aioIob2Bi2x_CreateWriteFirmContext_t aioIob2Bi2x_CreateWriteFirmContext_orig = nullptr;
|
||||||
|
static aioIob2Bi2x_DestroyWriteFirmContext_t aioIob2Bi2x_DestroyWriteFirmContext_orig = nullptr;
|
||||||
|
static aioIob2Bi2x_WriteFirmGetState_t aioIob2Bi2x_WriteFirmGetState_orig = nullptr;
|
||||||
|
static aioIob2Bi2x_WriteFirmIsCompleted_t aioIob2Bi2x_WriteFirmIsCompleted_orig = nullptr;
|
||||||
|
static aioIob2Bi2x_WriteFirmIsError_t aioIob2Bi2x_WriteFirmIsError_orig = nullptr;
|
||||||
|
static aioNMgrIob2_Create_t aioNMgrIob2_Create_orig = nullptr;
|
||||||
|
static aioSci_Destroy_t aioSci_Destroy_orig = nullptr;
|
||||||
|
static aioNodeMgr_Destroy_t aioNodeMgr_Destroy_orig = nullptr;
|
||||||
|
static aioNodeCtl_Destroy_t aioNodeCtl_Destroy_orig = nullptr;
|
||||||
|
static aioNodeCtl_UpdateDevicesStatus_t aioNodeCtl_UpdateDevicesStatus_orig = nullptr;
|
||||||
|
|
||||||
|
static AIO_SCI_COMM *aio_sci_comm = nullptr;
|
||||||
|
static AIO_NMGR_IOB2 *aio_node_mgr = nullptr;
|
||||||
|
static AIO_IOB2_BI2X_T44 *aio_t44 = nullptr;
|
||||||
|
static AIO_IOB2_BI2X_WRFIRM *aio_write_firm = nullptr;
|
||||||
|
static uint8_t input_counter = 0;
|
||||||
|
|
||||||
|
static void __fastcall aioNMgrIob_BeginManage(AIO_NMGR_IOB2 *) {
|
||||||
|
}
|
||||||
|
|
||||||
|
static AIO_NMGR_IOB2_VTABLE aio_node_mgr_vtable {
|
||||||
|
{},
|
||||||
|
aioNMgrIob_BeginManage,
|
||||||
|
};
|
||||||
|
|
||||||
|
static AIO_SCI_COMM *__fastcall aioIob2Bi2x_OpenSciUsbCdc(uint32_t) {
|
||||||
|
aio_sci_comm = new AIO_SCI_COMM {};
|
||||||
|
return aio_sci_comm;
|
||||||
|
}
|
||||||
|
|
||||||
|
static AIO_NMGR_IOB2 *__fastcall aioNMgrIob2_Create(AIO_SCI_COMM *sci, uint32_t mode) {
|
||||||
|
if (sci != aio_sci_comm) {
|
||||||
|
return aioNMgrIob2_Create_orig(sci, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
aio_node_mgr = new AIO_NMGR_IOB2 {};
|
||||||
|
aio_node_mgr->vtable = &aio_node_mgr_vtable;
|
||||||
|
return aio_node_mgr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static AIO_IOB2_BI2X_T44 *__fastcall aioIob2Bi2xT44_Create(
|
||||||
|
AIO_NMGR_IOB2 *node_mgr, uint32_t device_id) {
|
||||||
|
if (node_mgr != aio_node_mgr) {
|
||||||
|
return aioIob2Bi2xT44_Create_orig(node_mgr, device_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info("jb::bi2x", "T44 node created");
|
||||||
|
aio_t44 = new AIO_IOB2_BI2X_T44 {};
|
||||||
|
return aio_t44;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioIob2Bi2xT44_GetDeviceStatus(
|
||||||
|
AIO_IOB2_BI2X_T44 *node, AIO_IOB2_BI2X_T44_DEVSTATUS *status) {
|
||||||
|
if (node != aio_t44) {
|
||||||
|
return aioIob2Bi2xT44_GetDeviceStatus_orig(node, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
RI_MGR->devices_flush_output();
|
||||||
|
std::memset(status, 0, sizeof(*status));
|
||||||
|
|
||||||
|
status->data[0x00] = input_counter;
|
||||||
|
status->data[0x03] = input_counter++;
|
||||||
|
status->data[0x0A] = static_cast<uint8_t>(eamuse_coin_get_stock());
|
||||||
|
|
||||||
|
games::jb::touch_update();
|
||||||
|
const auto touched = games::jb::touch_state();
|
||||||
|
auto &buttons = get_buttons();
|
||||||
|
|
||||||
|
status->data[0x04] = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Test]) ? 0xFF : 0;
|
||||||
|
status->data[0x05] = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Service]) ? 0xFF : 0;
|
||||||
|
status->data[0x06] = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::CoinMech]) ? 0xFF : 0;
|
||||||
|
|
||||||
|
static constexpr size_t PANEL_ORDER[16] = {
|
||||||
|
3, 7, 11, 15,
|
||||||
|
2, 6, 10, 14,
|
||||||
|
1, 5, 9, 13,
|
||||||
|
0, 4, 8, 12,
|
||||||
|
};
|
||||||
|
for (size_t status_index = 0; status_index < std::size(PANEL_ORDER); status_index++) {
|
||||||
|
const auto panel_index = PANEL_ORDER[status_index];
|
||||||
|
if (touched[panel_index] || GameAPI::Buttons::getState(
|
||||||
|
RI_MGR, buttons[Buttons::Button1 + panel_index])) {
|
||||||
|
status->data[0x0F + status_index] = 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioIob2Bi2xT44_IoReset(AIO_IOB2_BI2X_T44 *node, uint32_t reset) {
|
||||||
|
if (node != aio_t44) {
|
||||||
|
return aioIob2Bi2xT44_IoReset_orig(node, reset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioIob2Bi2xT44_SetWatchDogTimer(AIO_IOB2_BI2X_T44 *node, uint8_t count) {
|
||||||
|
if (node != aio_t44) {
|
||||||
|
return aioIob2Bi2xT44_SetWatchDogTimer_orig(node, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioIob2Bi2xT44_ControlCoinBlocker(
|
||||||
|
AIO_IOB2_BI2X_T44 *node, uint32_t slot, bool open) {
|
||||||
|
if (node != aio_t44) {
|
||||||
|
return aioIob2Bi2xT44_ControlCoinBlocker_orig(node, slot, open);
|
||||||
|
}
|
||||||
|
|
||||||
|
eamuse_coin_set_block(!open);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioIob2Bi2xT44_AddCounter(
|
||||||
|
AIO_IOB2_BI2X_T44 *node, uint32_t counter, uint32_t count) {
|
||||||
|
if (node != aio_t44) {
|
||||||
|
return aioIob2Bi2xT44_AddCounter_orig(node, counter, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioIob2Bi2xT44_SetIccrLed(AIO_IOB2_BI2X_T44 *node, uint32_t color) {
|
||||||
|
if (node != aio_t44) {
|
||||||
|
return aioIob2Bi2xT44_SetIccrLed_orig(node, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioIob2Bi2xT44_SetTapeLedData(
|
||||||
|
AIO_IOB2_BI2X_T44 *node, uint32_t tape, const void *data) {
|
||||||
|
if (node != aio_t44) {
|
||||||
|
return aioIob2Bi2xT44_SetTapeLedData_orig(node, tape, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static AIO_IOB2_BI2X_WRFIRM *__fastcall aioIob2Bi2x_CreateWriteFirmContext(uint32_t, uint32_t) {
|
||||||
|
aio_write_firm = new AIO_IOB2_BI2X_WRFIRM {};
|
||||||
|
return aio_write_firm;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioIob2Bi2x_DestroyWriteFirmContext(AIO_IOB2_BI2X_WRFIRM *context) {
|
||||||
|
if (context != aio_write_firm) {
|
||||||
|
return aioIob2Bi2x_DestroyWriteFirmContext_orig(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete aio_write_firm;
|
||||||
|
aio_write_firm = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int32_t __fastcall aioIob2Bi2x_WriteFirmGetState(AIO_IOB2_BI2X_WRFIRM *context) {
|
||||||
|
if (context != aio_write_firm) {
|
||||||
|
return aioIob2Bi2x_WriteFirmGetState_orig(context);
|
||||||
|
}
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool __fastcall aioIob2Bi2x_WriteFirmIsCompleted(int32_t state) {
|
||||||
|
if (aio_write_firm) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return aioIob2Bi2x_WriteFirmIsCompleted_orig(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool __fastcall aioIob2Bi2x_WriteFirmIsError(int32_t state) {
|
||||||
|
if (aio_write_firm) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return aioIob2Bi2x_WriteFirmIsError_orig(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioSci_Destroy(AIO_SCI_COMM *sci) {
|
||||||
|
if (sci != aio_sci_comm) {
|
||||||
|
return aioSci_Destroy_orig(sci);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete aio_sci_comm;
|
||||||
|
aio_sci_comm = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioNodeMgr_Destroy(AIO_NMGR_IOB2 *node_mgr) {
|
||||||
|
if (node_mgr != aio_node_mgr) {
|
||||||
|
return aioNodeMgr_Destroy_orig(node_mgr);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete aio_node_mgr;
|
||||||
|
aio_node_mgr = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioNodeCtl_Destroy(AIO_IOB2_BI2X_T44 *node) {
|
||||||
|
if (node != aio_t44) {
|
||||||
|
return aioNodeCtl_Destroy_orig(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete aio_t44;
|
||||||
|
aio_t44 = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __fastcall aioNodeCtl_UpdateDevicesStatus() {
|
||||||
|
}
|
||||||
|
|
||||||
|
void bi2x_hook_init() {
|
||||||
|
static bool initialized = false;
|
||||||
|
if (initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
log_info("jb::bi2x", "initializing T44 hooks");
|
||||||
|
|
||||||
|
const auto libaio_iob2_video = "libaio-iob2_video.dll";
|
||||||
|
detour::trampoline_try(libaio_iob2_video, "aioIob2Bi2xT44_Create",
|
||||||
|
aioIob2Bi2xT44_Create, &aioIob2Bi2xT44_Create_orig);
|
||||||
|
detour::trampoline_try(libaio_iob2_video,
|
||||||
|
"?GetDeviceStatus@AIO_IOB2_BI2X_T44@@QEBAXAEAUDEVSTATUS@1@@Z",
|
||||||
|
aioIob2Bi2xT44_GetDeviceStatus, &aioIob2Bi2xT44_GetDeviceStatus_orig);
|
||||||
|
detour::trampoline_try(libaio_iob2_video, "?IoReset@AIO_IOB2_BI2X_T44@@QEAAXI@Z",
|
||||||
|
aioIob2Bi2xT44_IoReset, &aioIob2Bi2xT44_IoReset_orig);
|
||||||
|
detour::trampoline_try(libaio_iob2_video,
|
||||||
|
"?SetWatchDogTimer@AIO_IOB2_BI2X_T44@@QEAAXE@Z",
|
||||||
|
aioIob2Bi2xT44_SetWatchDogTimer, &aioIob2Bi2xT44_SetWatchDogTimer_orig);
|
||||||
|
detour::trampoline_try(libaio_iob2_video,
|
||||||
|
"?ControlCoinBlocker@AIO_IOB2_BI2X_T44@@QEAAXI_N@Z",
|
||||||
|
aioIob2Bi2xT44_ControlCoinBlocker, &aioIob2Bi2xT44_ControlCoinBlocker_orig);
|
||||||
|
detour::trampoline_try(libaio_iob2_video, "?AddCounter@AIO_IOB2_BI2X_T44@@QEAAXII@Z",
|
||||||
|
aioIob2Bi2xT44_AddCounter, &aioIob2Bi2xT44_AddCounter_orig);
|
||||||
|
detour::trampoline_try(libaio_iob2_video, "?SetIccrLed@AIO_IOB2_BI2X_T44@@QEAAXI@Z",
|
||||||
|
aioIob2Bi2xT44_SetIccrLed, &aioIob2Bi2xT44_SetIccrLed_orig);
|
||||||
|
detour::trampoline_try(libaio_iob2_video,
|
||||||
|
"?SetTapeLedData@AIO_IOB2_BI2X_T44@@QEAAXIPEBX@Z",
|
||||||
|
aioIob2Bi2xT44_SetTapeLedData, &aioIob2Bi2xT44_SetTapeLedData_orig);
|
||||||
|
|
||||||
|
const auto libaio_iob = "libaio-iob.dll";
|
||||||
|
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_OpenSciUsbCdc",
|
||||||
|
aioIob2Bi2x_OpenSciUsbCdc, &aioIob2Bi2x_OpenSciUsbCdc_orig);
|
||||||
|
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_CreateWriteFirmContext",
|
||||||
|
aioIob2Bi2x_CreateWriteFirmContext, &aioIob2Bi2x_CreateWriteFirmContext_orig);
|
||||||
|
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_DestroyWriteFirmContext",
|
||||||
|
aioIob2Bi2x_DestroyWriteFirmContext, &aioIob2Bi2x_DestroyWriteFirmContext_orig);
|
||||||
|
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_WriteFirmGetState",
|
||||||
|
aioIob2Bi2x_WriteFirmGetState, &aioIob2Bi2x_WriteFirmGetState_orig);
|
||||||
|
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_WriteFirmIsCompleted",
|
||||||
|
aioIob2Bi2x_WriteFirmIsCompleted, &aioIob2Bi2x_WriteFirmIsCompleted_orig);
|
||||||
|
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_WriteFirmIsError",
|
||||||
|
aioIob2Bi2x_WriteFirmIsError, &aioIob2Bi2x_WriteFirmIsError_orig);
|
||||||
|
detour::trampoline_try(libaio_iob, "aioNMgrIob2_Create",
|
||||||
|
aioNMgrIob2_Create, &aioNMgrIob2_Create_orig);
|
||||||
|
|
||||||
|
const auto libaio = "libaio.dll";
|
||||||
|
detour::trampoline_try(libaio, "aioSci_Destroy", aioSci_Destroy, &aioSci_Destroy_orig);
|
||||||
|
detour::trampoline_try(libaio, "aioNodeMgr_Destroy",
|
||||||
|
aioNodeMgr_Destroy, &aioNodeMgr_Destroy_orig);
|
||||||
|
detour::trampoline_try(libaio, "aioNodeCtl_Destroy",
|
||||||
|
aioNodeCtl_Destroy, &aioNodeCtl_Destroy_orig);
|
||||||
|
detour::trampoline_try(libaio, "aioNodeCtl_UpdateDevicesStatus",
|
||||||
|
aioNodeCtl_UpdateDevicesStatus, &aioNodeCtl_UpdateDevicesStatus_orig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#if SPICE64
|
||||||
|
|
||||||
|
namespace games::jb {
|
||||||
|
void bi2x_hook_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
+20
-207
@@ -4,211 +4,27 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
|
#include "bi2x_hook.h"
|
||||||
#include "cfg/configurator.h"
|
#include "cfg/configurator.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
|
||||||
#include "touch/touch.h"
|
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/utils.h"
|
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
|
|
||||||
#define JB_BUTTON_SIZE 160
|
|
||||||
#define JB_BUTTON_GAP 37
|
|
||||||
#define JB_BUTTON_HITBOX (JB_BUTTON_SIZE + JB_BUTTON_GAP)
|
|
||||||
|
|
||||||
namespace games::jb {
|
namespace games::jb {
|
||||||
|
|
||||||
// touch stuff
|
#if !SPICE64
|
||||||
JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
|
|
||||||
static bool TOUCH_ENABLE = false;
|
|
||||||
static bool TOUCH_ATTACHED = false;
|
|
||||||
static bool IS_PORTRAIT = true;
|
|
||||||
static std::vector<TouchPoint> TOUCH_POINTS;
|
|
||||||
bool TOUCH_STATE[16];
|
|
||||||
|
|
||||||
void touch_update() {
|
// fixes "IP ADDR CHANGE" errors with unusual network setups (e.g. a VPN)
|
||||||
|
|
||||||
// check if touch enabled
|
|
||||||
if (!TOUCH_ENABLE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// attach touch module
|
|
||||||
if (!TOUCH_ATTACHED) {
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Find the game window.
|
|
||||||
* We check the foreground window first, then fall back to searching for the window title
|
|
||||||
* All game versions seem to have their model first in the window title
|
|
||||||
*/
|
|
||||||
HWND wnd = GetForegroundWindow();
|
|
||||||
if (!string_begins_with(GetActiveWindowTitle(), avs::game::MODEL)) {
|
|
||||||
wnd = FindWindowBeginsWith(avs::game::MODEL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if we have a window handle
|
|
||||||
if (!wnd) {
|
|
||||||
log_warning("jubeat", "could not find window handle for touch");
|
|
||||||
TOUCH_ENABLE = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// attach touch hook
|
|
||||||
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
|
|
||||||
touch_create_wnd(wnd, true);
|
|
||||||
|
|
||||||
// show cursor
|
|
||||||
if (GRAPHICS_SHOW_CURSOR) {
|
|
||||||
ShowCursor(TRUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// earlier games use a different screen orientation
|
|
||||||
if (!avs::game::is_model("L44")) {
|
|
||||||
IS_PORTRAIT = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// set attached
|
|
||||||
TOUCH_ATTACHED = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset touch state
|
|
||||||
memset(TOUCH_STATE, 0, sizeof(TOUCH_STATE));
|
|
||||||
|
|
||||||
// check touch points
|
|
||||||
// note that the IO code in device.cpp will correctly compensate for orientation, depending on the model.
|
|
||||||
TOUCH_POINTS.clear();
|
|
||||||
touch_get_points(TOUCH_POINTS);
|
|
||||||
if (TOUCH_ALGORITHM == Legacy) {
|
|
||||||
auto offset = IS_PORTRAIT ? 580 : 0;
|
|
||||||
for (auto &tp : TOUCH_POINTS) {
|
|
||||||
|
|
||||||
// get grid coordinates
|
|
||||||
int x = tp.x * 4 / 768;
|
|
||||||
int y = (tp.y - offset) * 4 / (1360 - 580);
|
|
||||||
|
|
||||||
// set the corresponding state
|
|
||||||
int index = y * 4 + x;
|
|
||||||
if (index >= 0 && index < 16) {
|
|
||||||
TOUCH_STATE[index] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (auto &tp : TOUCH_POINTS) {
|
|
||||||
// check window out of bounds
|
|
||||||
if (IS_PORTRAIT) {
|
|
||||||
if (tp.x > 768 || tp.y > 1360) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (tp.x > 1360 || tp.y > 768) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int x_relative = tp.x;
|
|
||||||
int y_relative = tp.y;
|
|
||||||
|
|
||||||
// x_relative and y_relative are relative to the top-left pixel of the first button
|
|
||||||
if (IS_PORTRAIT) {
|
|
||||||
// which is at (8, 602) in portrait:
|
|
||||||
// X: [8...759] (752 pixels wide)
|
|
||||||
// Y: [602...1353] (752 pixels high)
|
|
||||||
x_relative -= 8;
|
|
||||||
y_relative -= 602;
|
|
||||||
} else {
|
|
||||||
// and at (8, 8) in landscape
|
|
||||||
x_relative -= 8;
|
|
||||||
y_relative -= 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
int x_index = -1;
|
|
||||||
int x_hitbox = 0;
|
|
||||||
int y_index = -1;
|
|
||||||
int y_hitbox = 0;
|
|
||||||
|
|
||||||
// x_hitbox and y_hitbox is relative to top-left pixel of each button
|
|
||||||
if (x_relative >= 0) {
|
|
||||||
x_index = x_relative / JB_BUTTON_HITBOX;
|
|
||||||
x_hitbox = x_relative % JB_BUTTON_HITBOX;
|
|
||||||
}
|
|
||||||
if (y_relative >= 0) {
|
|
||||||
y_index = y_relative / JB_BUTTON_HITBOX;
|
|
||||||
y_hitbox = y_relative % JB_BUTTON_HITBOX;
|
|
||||||
}
|
|
||||||
|
|
||||||
// log_info("jb", "raw={}, idx={}, hitbox={}", tp.x, x_index, x_hitbox);
|
|
||||||
|
|
||||||
if (TOUCH_ALGORITHM == Improved) {
|
|
||||||
if (IS_PORTRAIT) {
|
|
||||||
// extend to left border
|
|
||||||
if (x_relative < 0) {
|
|
||||||
x_index = 0;
|
|
||||||
}
|
|
||||||
// right and bottom borders are covered by the hit box
|
|
||||||
} else {
|
|
||||||
// extend to top border
|
|
||||||
if (y_relative < 0) {
|
|
||||||
y_index = 0;
|
|
||||||
}
|
|
||||||
// extend to left border
|
|
||||||
if (x_relative < 0) {
|
|
||||||
x_index = 0;
|
|
||||||
}
|
|
||||||
// bottom border is covered by the hit box
|
|
||||||
// rightmost edge - ignore them entirely
|
|
||||||
if (x_index == 3 && JB_BUTTON_SIZE < x_hitbox) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (x_index < 0 || y_index < 0 || x_index > 3 || y_index > 3) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the gap was touched
|
|
||||||
if (TOUCH_ALGORITHM == AcAccurate) {
|
|
||||||
// in ac-accurate mode, touching the gap is ignored
|
|
||||||
if (x_hitbox > JB_BUTTON_SIZE || y_hitbox > JB_BUTTON_SIZE) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (TOUCH_ALGORITHM == Improved) {
|
|
||||||
// in improved mode, touching the gap triggers the closest button
|
|
||||||
if (x_index <= 2 && (JB_BUTTON_SIZE + JB_BUTTON_GAP / 2) < x_hitbox) {
|
|
||||||
x_index++;
|
|
||||||
}
|
|
||||||
if (y_index <= 2 && (JB_BUTTON_SIZE + JB_BUTTON_GAP / 2) < y_hitbox) {
|
|
||||||
y_index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the corresponding state
|
|
||||||
int index = y_index * 4 + x_index;
|
|
||||||
if (0 <= index && index < 16) {
|
|
||||||
TOUCH_STATE[index] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* to fix "IP ADDR CHANGE" errors on boot and in-game when using weird network setups such as a VPN
|
|
||||||
*/
|
|
||||||
static BOOL __stdcall network_addr_is_changed() {
|
static BOOL __stdcall network_addr_is_changed() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
// fixes lag spikes from the periodic ping to "eamuse.konami.fun"
|
||||||
* to fix lag spikes when game tries to ping "eamuse.konami.fun" every few minutes
|
|
||||||
*/
|
|
||||||
static BOOL __stdcall network_get_network_check_info() {
|
static BOOL __stdcall network_get_network_check_info() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
// fixes network errors on non-DHCP interfaces
|
||||||
* to fix network error on non DHCP interfaces
|
|
||||||
*/
|
|
||||||
static BOOL __cdecl network_get_dhcp_result() {
|
static BOOL __cdecl network_get_dhcp_result() {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -219,6 +35,8 @@ namespace games::jb {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
JBGame::JBGame() : Game("Jubeat") {
|
JBGame::JBGame() : Game("Jubeat") {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,24 +66,18 @@ namespace games::jb {
|
|||||||
void JBGame::attach() {
|
void JBGame::attach() {
|
||||||
Game::attach();
|
Game::attach();
|
||||||
|
|
||||||
// enable touch
|
#if SPICE64
|
||||||
TOUCH_ENABLE = true;
|
if (avs::game::DLL_NAME == "jubeat2019.dll") {
|
||||||
|
libutils::load_library("libaio.dll");
|
||||||
switch (TOUCH_ALGORITHM) {
|
libutils::load_library("libaio-iob.dll");
|
||||||
case Legacy:
|
libutils::load_library("libaio-iob2_video.dll");
|
||||||
log_info("jubeat", "using 'legacy' touch targets");
|
bi2x_hook_init();
|
||||||
break;
|
|
||||||
case Improved:
|
|
||||||
log_info("jubeat", "using 'improved' touch targets");
|
|
||||||
break;
|
|
||||||
case AcAccurate:
|
|
||||||
log_info("jubeat", "using 'ac accurate' touch targets");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
log_fatal("jubeat", "unknown touch algo detected in touch_update, this is a bug");
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
touch_attach();
|
||||||
|
|
||||||
|
#if !SPICE64
|
||||||
// enable debug logging of gftools
|
// enable debug logging of gftools
|
||||||
HMODULE gftools = libutils::try_module("gftools.dll");
|
HMODULE gftools = libutils::try_module("gftools.dll");
|
||||||
detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc(
|
detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc(
|
||||||
@@ -279,12 +91,13 @@ namespace games::jb {
|
|||||||
network, "network_get_network_check_info"));
|
network, "network_get_network_check_info"));
|
||||||
detour::inline_hook((void *) network_get_dhcp_result, libutils::try_proc(
|
detour::inline_hook((void *) network_get_dhcp_result, libutils::try_proc(
|
||||||
network, "network_get_dhcp_result"));
|
network, "network_get_dhcp_result"));
|
||||||
|
#endif
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void JBGame::detach() {
|
void JBGame::detach() {
|
||||||
Game::detach();
|
Game::detach();
|
||||||
|
|
||||||
// disable touch
|
touch_detach();
|
||||||
TOUCH_ENABLE = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "games/game.h"
|
#include "games/game.h"
|
||||||
|
#include "games/jb/jb_touch.h"
|
||||||
|
|
||||||
namespace games::jb {
|
namespace games::jb {
|
||||||
|
|
||||||
enum JubeatTouchAlgorithm {
|
|
||||||
Legacy,
|
|
||||||
Improved,
|
|
||||||
AcAccurate
|
|
||||||
};
|
|
||||||
|
|
||||||
// touch stuff
|
|
||||||
extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
|
|
||||||
extern bool TOUCH_STATE[16];
|
|
||||||
void touch_update();
|
|
||||||
|
|
||||||
class JBGame : public games::Game {
|
class JBGame : public games::Game {
|
||||||
public:
|
public:
|
||||||
JBGame();
|
JBGame();
|
||||||
|
|||||||
@@ -0,0 +1,485 @@
|
|||||||
|
#include "jb_touch.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <atomic>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "avs/game.h"
|
||||||
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "launcher/launcher.h"
|
||||||
|
#include "touch/touch.h"
|
||||||
|
#include "rawinput/touch.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
#include "util/time.h"
|
||||||
|
#include "util/utils.h"
|
||||||
|
|
||||||
|
// touch layout: a 4x4 grid of 160px buttons separated by 37 / 38 / 37 px gaps
|
||||||
|
// (752px across). the first button's top-left is at (8, 602) in portrait and
|
||||||
|
// (6, 8) in landscape.
|
||||||
|
#define JB_BUTTON_SIZE 160
|
||||||
|
#define JB_MAX_BUTTON_GAP 38
|
||||||
|
#define JB_T44_BUTTON_SIZE 224
|
||||||
|
#define JB_T44_BUTTON_GAP 33
|
||||||
|
|
||||||
|
// improved and plus modes use this reach around each button. must be >= the
|
||||||
|
// diagonal half of the widest gap (~27px) so the grid centre still reaches a button.
|
||||||
|
#define JB_TOUCH_RADIUS 38
|
||||||
|
|
||||||
|
namespace games::jb {
|
||||||
|
|
||||||
|
static_assert(std::atomic_bool::is_always_lock_free);
|
||||||
|
static_assert(std::atomic_uint16_t::is_always_lock_free);
|
||||||
|
static_assert(std::atomic<POINT>::is_always_lock_free);
|
||||||
|
static constexpr int JB_MAX_GAP_DISTANCE = (JB_MAX_BUTTON_GAP + 1) / 2;
|
||||||
|
static_assert(JB_TOUCH_RADIUS * JB_TOUCH_RADIUS >=
|
||||||
|
2 * JB_MAX_GAP_DISTANCE * JB_MAX_GAP_DISTANCE);
|
||||||
|
|
||||||
|
// touch state
|
||||||
|
JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
|
||||||
|
JubeatTouchDebugMode TOUCH_DEBUG_OVERLAY = JbTouchDebugAuto;
|
||||||
|
uint32_t TOUCH_DEBOUNCE_MS = 0;
|
||||||
|
static std::atomic_bool TOUCH_ENABLE = false;
|
||||||
|
static bool TOUCH_ATTACHED = false;
|
||||||
|
static bool IS_PORTRAIT = true;
|
||||||
|
static bool IS_T44 = false;
|
||||||
|
static std::atomic_uint16_t TOUCH_STATE = 0;
|
||||||
|
|
||||||
|
// fixed-size contact view used by the debug overlay
|
||||||
|
static const size_t JB_MAX_TOUCH_POINTS = 16;
|
||||||
|
static constexpr LONG JB_INVALID_TOUCH_COORD = std::numeric_limits<LONG>::max();
|
||||||
|
static constexpr POINT JB_INVALID_TOUCH_POINT {
|
||||||
|
JB_INVALID_TOUCH_COORD,
|
||||||
|
JB_INVALID_TOUCH_COORD
|
||||||
|
};
|
||||||
|
static std::atomic<POINT> TOUCH_POINTS[JB_MAX_TOUCH_POINTS] {};
|
||||||
|
|
||||||
|
static void clear_touch_points() {
|
||||||
|
for (auto &point : TOUCH_POINTS) {
|
||||||
|
point.store(JB_INVALID_TOUCH_POINT, std::memory_order_release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// false when a contact is younger than the debounce window and should be ignored
|
||||||
|
static bool touch_matured(const TouchPoint &tp, double now_ms, double threshold_ms) {
|
||||||
|
return now_ms - tp.down_ms >= threshold_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
// snapshot for the debug overlay: each slot atomically holds one matured contact or
|
||||||
|
// the invalid sentinel. immature and absent contacts are published as invalid, so the
|
||||||
|
// overlay never shows a suppressed tap and a single atomic per slot cannot tear
|
||||||
|
static void publish_touch_points(const std::vector<TouchPoint> &touch_points,
|
||||||
|
double now_ms, double threshold_ms) {
|
||||||
|
size_t count = touch_points.size();
|
||||||
|
if (count > JB_MAX_TOUCH_POINTS) {
|
||||||
|
count = JB_MAX_TOUCH_POINTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < JB_MAX_TOUCH_POINTS; i++) {
|
||||||
|
POINT point = JB_INVALID_TOUCH_POINT;
|
||||||
|
if (i < count && touch_matured(touch_points[i], now_ms, threshold_ms)) {
|
||||||
|
point = { touch_points[i].x, touch_points[i].y };
|
||||||
|
}
|
||||||
|
TOUCH_POINTS[i].store(point, std::memory_order_release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- touch geometry ------------------------------------------------------
|
||||||
|
// gaps between the four buttons along one axis (the middle gap is 1px wider)
|
||||||
|
static const int JB_BUTTON_GAPS[3] = { 37, JB_MAX_BUTTON_GAP, 37 };
|
||||||
|
static const int JB_T44_BUTTON_GAPS[3] = {
|
||||||
|
JB_T44_BUTTON_GAP,
|
||||||
|
JB_T44_BUTTON_GAP,
|
||||||
|
JB_T44_BUTTON_GAP,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AxisGeometry {
|
||||||
|
int size;
|
||||||
|
int button[4]; // left/top edge of each button
|
||||||
|
};
|
||||||
|
|
||||||
|
// left/top edges of the four buttons along one axis, starting at `first`
|
||||||
|
static AxisGeometry axis_geometry(int first, int size, const int gaps[3]) {
|
||||||
|
AxisGeometry g {};
|
||||||
|
g.size = size;
|
||||||
|
g.button[0] = first;
|
||||||
|
for (int i = 1; i < 4; i++) {
|
||||||
|
g.button[i] = g.button[i - 1] + size + gaps[i - 1];
|
||||||
|
}
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
// button edges for the current orientation
|
||||||
|
static void touch_geometry(AxisGeometry &gx, AxisGeometry &gy) {
|
||||||
|
if (IS_T44) {
|
||||||
|
gx = axis_geometry(37, JB_T44_BUTTON_SIZE, JB_T44_BUTTON_GAPS);
|
||||||
|
gy = axis_geometry(864, JB_T44_BUTTON_SIZE, JB_T44_BUTTON_GAPS);
|
||||||
|
} else if (IS_PORTRAIT) {
|
||||||
|
gx = axis_geometry(8, JB_BUTTON_SIZE, JB_BUTTON_GAPS);
|
||||||
|
gy = axis_geometry(602, JB_BUTTON_SIZE, JB_BUTTON_GAPS);
|
||||||
|
} else {
|
||||||
|
gx = axis_geometry(6, JB_BUTTON_SIZE, JB_BUTTON_GAPS);
|
||||||
|
gy = axis_geometry(8, JB_BUTTON_SIZE, JB_BUTTON_GAPS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// distance from `p` to a button along one axis (0 when inside)
|
||||||
|
static int axis_distance(int p, int button, int size) {
|
||||||
|
int end = button + size - 1;
|
||||||
|
if (p < button) {
|
||||||
|
return button - p;
|
||||||
|
}
|
||||||
|
if (p > end) {
|
||||||
|
return p - end;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// index (0..15) of the nearest button to (px, py) within `radius`, or -1 if none;
|
||||||
|
// shared by detection and the debug overlay so both agree which button a touch hits
|
||||||
|
static int nearest_button(
|
||||||
|
int px, int py, const AxisGeometry &gx, const AxisGeometry &gy, int radius) {
|
||||||
|
int best_index = -1;
|
||||||
|
int best_dist = 0;
|
||||||
|
for (int r = 0; r < 4; r++) {
|
||||||
|
for (int c = 0; c < 4; c++) {
|
||||||
|
int dx = axis_distance(px, gx.button[c], gx.size);
|
||||||
|
int dy = axis_distance(py, gy.button[r], gy.size);
|
||||||
|
int dist = dx * dx + dy * dy;
|
||||||
|
if (dist <= radius * radius && (best_index < 0 || dist < best_dist)) {
|
||||||
|
best_dist = dist;
|
||||||
|
best_index = r * 4 + c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// detection reach for the current algorithm (0 = register only inside a button)
|
||||||
|
static int touch_radius() {
|
||||||
|
return TOUCH_ALGORITHM == AcAccurate || (IS_T44 && TOUCH_ALGORITHM == Legacy) ?
|
||||||
|
0 : JB_TOUCH_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark the buttons a touch at (px, py) hits: only the nearest within `radius`, or
|
||||||
|
// every button within `radius` when `multi` (edge/gap presses trigger several)
|
||||||
|
static void mark_buttons(uint16_t &state, int px, int py,
|
||||||
|
const AxisGeometry &gx, const AxisGeometry &gy,
|
||||||
|
int radius, bool multi) {
|
||||||
|
if (!multi) {
|
||||||
|
int index = nearest_button(px, py, gx, gy, radius);
|
||||||
|
if (index >= 0) {
|
||||||
|
state |= uint16_t(1) << index;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int r = 0; r < 4; r++) {
|
||||||
|
for (int c = 0; c < 4; c++) {
|
||||||
|
int dx = axis_distance(px, gx.button[c], gx.size);
|
||||||
|
int dy = axis_distance(py, gy.button[r], gy.size);
|
||||||
|
if (dx * dx + dy * dy <= radius * radius) {
|
||||||
|
state |= uint16_t(1) << (r * 4 + c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::bitset<16> touch_state() {
|
||||||
|
return std::bitset<16>(TOUCH_STATE.load(std::memory_order_acquire));
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_update() {
|
||||||
|
|
||||||
|
if (!TOUCH_ENABLE.load(std::memory_order_acquire)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// one-time touch window attach
|
||||||
|
if (!TOUCH_ATTACHED) {
|
||||||
|
|
||||||
|
IS_T44 = avs::game::is_model("T44");
|
||||||
|
IS_PORTRAIT = IS_T44 || avs::game::is_model("L44");
|
||||||
|
|
||||||
|
// find the game window: prefer the foreground window, else search by
|
||||||
|
// title (T44 uses a fixed title instead of the model prefix)
|
||||||
|
const char *window_title = IS_T44 ? "jubeat 10 main" : avs::game::MODEL;
|
||||||
|
HWND wnd = GetForegroundWindow();
|
||||||
|
if (!string_begins_with(GetActiveWindowTitle(), window_title)) {
|
||||||
|
wnd = FindWindowBeginsWith(window_title);
|
||||||
|
}
|
||||||
|
if (!wnd) {
|
||||||
|
log_warning("jubeat", "could not find window handle for touch");
|
||||||
|
TOUCH_ENABLE.store(false, std::memory_order_release);
|
||||||
|
TOUCH_STATE.store(0, std::memory_order_release);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
|
||||||
|
|
||||||
|
// let the rawinput stack correct the aspect ratio
|
||||||
|
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
|
||||||
|
|
||||||
|
touch_create_wnd(wnd, true);
|
||||||
|
|
||||||
|
if (GRAPHICS_SHOW_CURSOR) {
|
||||||
|
ShowCursor(TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
TOUCH_ATTACHED = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate the next state locally and publish it after processing every touch
|
||||||
|
uint16_t next_state = 0;
|
||||||
|
std::vector<TouchPoint> touch_points;
|
||||||
|
touch_get_points(touch_points);
|
||||||
|
|
||||||
|
// debounce: the landing time is stamped by the touch layer, so this is
|
||||||
|
// independent of how often the game polls for input (0 = off)
|
||||||
|
double now_ms = get_performance_milliseconds();
|
||||||
|
double threshold_ms = TOUCH_DEBOUNCE_MS;
|
||||||
|
|
||||||
|
// publish every raw contact (with its maturity) for the debug overlay, then drop
|
||||||
|
// the ones still inside the debounce window so no algorithm sees them
|
||||||
|
publish_touch_points(touch_points, now_ms, threshold_ms);
|
||||||
|
std::erase_if(touch_points, [&](const TouchPoint &tp) {
|
||||||
|
return !touch_matured(tp, now_ms, threshold_ms);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (TOUCH_ALGORITHM == Legacy && !IS_T44) {
|
||||||
|
|
||||||
|
// legacy: evenly divide the play area into a 4x4 grid
|
||||||
|
auto offset = IS_PORTRAIT ? 580 : 0;
|
||||||
|
for (auto &tp : touch_points) {
|
||||||
|
int x = tp.x * 4 / 768;
|
||||||
|
int y = (tp.y - offset) * 4 / (1360 - 580);
|
||||||
|
int index = y * 4 + x;
|
||||||
|
if (index >= 0 && index < 16) {
|
||||||
|
next_state |= uint16_t(1) << index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// accurate registers only a touch inside a button; improved snaps each touch
|
||||||
|
// to the single nearest button within reach (so a gap or centre touch still
|
||||||
|
// triggers exactly one button); plus marks every button within the same reach,
|
||||||
|
// so an edge or gap touch can trigger several at once (like the mobile game)
|
||||||
|
AxisGeometry gx, gy;
|
||||||
|
touch_geometry(gx, gy);
|
||||||
|
int radius = touch_radius();
|
||||||
|
bool multi = (TOUCH_ALGORITHM == Plus);
|
||||||
|
for (auto &tp : touch_points) {
|
||||||
|
mark_buttons(next_state, tp.x, tp.y, gx, gy, radius, multi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TOUCH_STATE.store(next_state, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
// true when any supported touch handler detects a touchscreen (checked once)
|
||||||
|
static bool touchscreen_detected() {
|
||||||
|
static const bool detected = is_touch_available("jubeat touch debug overlay");
|
||||||
|
return detected;
|
||||||
|
}
|
||||||
|
|
||||||
|
// auto draws the boxes only when a touch screen is present
|
||||||
|
static bool debug_show_boxes() {
|
||||||
|
switch (TOUCH_DEBUG_OVERLAY) {
|
||||||
|
case JbTouchDebugBox:
|
||||||
|
case JbTouchDebugAll:
|
||||||
|
return true;
|
||||||
|
case JbTouchDebugAuto:
|
||||||
|
return touchscreen_detected();
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool debug_show_taps() {
|
||||||
|
return TOUCH_DEBUG_OVERLAY == JbTouchDebugAll;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool touch_debug_overlay_enabled() {
|
||||||
|
return TOUCH_ENABLE.load(std::memory_order_acquire) &&
|
||||||
|
(debug_show_boxes() || debug_show_taps());
|
||||||
|
}
|
||||||
|
|
||||||
|
// the 16 boundary rects: legacy divides the area evenly, others use button squares
|
||||||
|
static void debug_cells(
|
||||||
|
bool legacy, const AxisGeometry &gx, const AxisGeometry &gy, RECT cells[16]) {
|
||||||
|
if (legacy) {
|
||||||
|
int offset = IS_PORTRAIT ? 580 : 0;
|
||||||
|
int x_edges[5];
|
||||||
|
int y_edges[5];
|
||||||
|
for (int i = 0; i <= 4; i++) {
|
||||||
|
x_edges[i] = i * 768 / 4;
|
||||||
|
y_edges[i] = offset + i * (1360 - 580) / 4;
|
||||||
|
}
|
||||||
|
for (int r = 0; r < 4; r++) {
|
||||||
|
for (int c = 0; c < 4; c++) {
|
||||||
|
cells[r * 4 + c] = { x_edges[c], y_edges[r], x_edges[c + 1], y_edges[r + 1] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int r = 0; r < 4; r++) {
|
||||||
|
for (int c = 0; c < 4; c++) {
|
||||||
|
cells[r * 4 + c] = {
|
||||||
|
gx.button[c], gy.button[r],
|
||||||
|
gx.button[c] + gx.size, gy.button[r] + gy.size
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hollow box outlines: grey idle, thick green when pressed (PS_INSIDEFRAME stays inside)
|
||||||
|
static void draw_debug_boxes(
|
||||||
|
HDC hdc, bool legacy, const AxisGeometry &gx, const AxisGeometry &gy) {
|
||||||
|
RECT cells[16];
|
||||||
|
debug_cells(legacy, gx, gy, cells);
|
||||||
|
|
||||||
|
HPEN pen_idle = CreatePen(PS_SOLID, 1, RGB(160, 160, 160));
|
||||||
|
HPEN pen_active = CreatePen(PS_INSIDEFRAME, 4, RGB(0, 200, 0));
|
||||||
|
auto state = touch_state();
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
HGDIOBJ old_pen = SelectObject(hdc, state[i] ? pen_active : pen_idle);
|
||||||
|
Rectangle(hdc, cells[i].left, cells[i].top, cells[i].right, cells[i].bottom);
|
||||||
|
SelectObject(hdc, old_pen);
|
||||||
|
}
|
||||||
|
DeleteObject(pen_idle);
|
||||||
|
DeleteObject(pen_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
// whether a touch at (px, py) presses at least one button: legacy has no gaps, plus
|
||||||
|
// reaches across gaps to nearby buttons, and the others only fire inside a button square
|
||||||
|
static bool touch_presses_button(int px, int py, const AxisGeometry &gx, const AxisGeometry &gy,
|
||||||
|
bool legacy, int radius) {
|
||||||
|
if (legacy) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
int reach = (TOUCH_ALGORITHM == Plus) ? radius : 0;
|
||||||
|
return nearest_button(px, py, gx, gy, reach) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 90-degree arc on the circle rim facing the centre of button `index`
|
||||||
|
static void draw_tap_arc(HDC hdc, int px, int py, int index,
|
||||||
|
const AxisGeometry &gx, const AxisGeometry &gy, int arc_radius) {
|
||||||
|
int c = index % 4;
|
||||||
|
int r = index / 4;
|
||||||
|
double mid = std::atan2((gy.button[r] + gy.size / 2) - py,
|
||||||
|
(gx.button[c] + gx.size / 2) - px);
|
||||||
|
const double quarter = 3.14159265358979323846 / 2.0;
|
||||||
|
const int segments = 16;
|
||||||
|
POINT arc[segments + 1];
|
||||||
|
for (int i = 0; i <= segments; i++) {
|
||||||
|
double a = mid - quarter / 2.0 + quarter * i / segments;
|
||||||
|
arc[i].x = px + static_cast<LONG>(std::lround(arc_radius * std::cos(a)));
|
||||||
|
arc[i].y = py + static_cast<LONG>(std::lround(arc_radius * std::sin(a)));
|
||||||
|
}
|
||||||
|
Polyline(hdc, arc, segments + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// circle at each touch: white when it presses a button, grey otherwise; in single-button
|
||||||
|
// modes a gap touch also gets a white arc facing the button it snapped to (skipped in
|
||||||
|
// plus, where a touch can trigger several buttons at once)
|
||||||
|
static void draw_debug_taps(
|
||||||
|
HDC hdc, bool legacy, const AxisGeometry &gx, const AxisGeometry &gy) {
|
||||||
|
// PS_INSIDEFRAME keeps the stroke inside the circle radius
|
||||||
|
const int stroke = 4;
|
||||||
|
HPEN pen_white = CreatePen(PS_INSIDEFRAME, stroke, RGB(255, 255, 255));
|
||||||
|
HPEN pen_gray = CreatePen(PS_INSIDEFRAME, stroke, RGB(128, 128, 128));
|
||||||
|
HGDIOBJ old_pen = SelectObject(hdc, pen_white);
|
||||||
|
|
||||||
|
// accurate has no reach, so fall back to a visible marker size when drawing the circle
|
||||||
|
int radius = touch_radius();
|
||||||
|
int draw_radius = radius > 0 ? radius : JB_TOUCH_RADIUS;
|
||||||
|
|
||||||
|
// each slot holds a matured contact or the invalid sentinel; immature and absent
|
||||||
|
// contacts were already filtered out by publish_touch_points
|
||||||
|
for (auto &touch_point : TOUCH_POINTS) {
|
||||||
|
POINT point = touch_point.load(std::memory_order_acquire);
|
||||||
|
if (point.x == JB_INVALID_TOUCH_COORD) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pressed = touch_presses_button(point.x, point.y, gx, gy, legacy, radius);
|
||||||
|
|
||||||
|
SelectObject(hdc, pressed ? pen_white : pen_gray);
|
||||||
|
Ellipse(hdc, point.x - draw_radius, point.y - draw_radius,
|
||||||
|
point.x + draw_radius, point.y + draw_radius);
|
||||||
|
|
||||||
|
// the arc points at a single snapped-to button; plus can trigger several at
|
||||||
|
// once, so skip it there
|
||||||
|
if (pressed || TOUCH_ALGORITHM == Plus) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int index = nearest_button(point.x, point.y, gx, gy, radius);
|
||||||
|
if (index >= 0) {
|
||||||
|
SelectObject(hdc, pen_white);
|
||||||
|
draw_tap_arc(hdc, point.x, point.y, index, gx, gy, draw_radius - stroke / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectObject(hdc, old_pen);
|
||||||
|
DeleteObject(pen_white);
|
||||||
|
DeleteObject(pen_gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_draw_debug_overlay(HDC hdc) {
|
||||||
|
|
||||||
|
// only draw while touch is active
|
||||||
|
if (!TOUCH_ENABLE.load(std::memory_order_acquire)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool show_boxes = debug_show_boxes();
|
||||||
|
bool show_taps = debug_show_taps();
|
||||||
|
if (!show_boxes && !show_taps) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// legacy divides the field evenly; the other algorithms use button squares
|
||||||
|
bool legacy = TOUCH_ALGORITHM == Legacy && !IS_T44;
|
||||||
|
AxisGeometry gx {}, gy {};
|
||||||
|
if (!legacy) {
|
||||||
|
touch_geometry(gx, gy);
|
||||||
|
}
|
||||||
|
|
||||||
|
HGDIOBJ old_brush = SelectObject(hdc, GetStockObject(NULL_BRUSH));
|
||||||
|
if (show_boxes) {
|
||||||
|
draw_debug_boxes(hdc, legacy, gx, gy);
|
||||||
|
}
|
||||||
|
if (show_taps) {
|
||||||
|
draw_debug_taps(hdc, legacy, gx, gy);
|
||||||
|
}
|
||||||
|
SelectObject(hdc, old_brush);
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_attach() {
|
||||||
|
clear_touch_points();
|
||||||
|
TOUCH_ENABLE.store(true, std::memory_order_release);
|
||||||
|
|
||||||
|
switch (TOUCH_ALGORITHM) {
|
||||||
|
case Legacy:
|
||||||
|
log_info("jubeat", "using 'legacy' touch targets");
|
||||||
|
break;
|
||||||
|
case Improved:
|
||||||
|
log_info("jubeat", "using 'improved' touch targets");
|
||||||
|
break;
|
||||||
|
case Plus:
|
||||||
|
log_info("jubeat", "using 'plus' touch targets");
|
||||||
|
break;
|
||||||
|
case AcAccurate:
|
||||||
|
log_info("jubeat", "using 'ac accurate' touch targets");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log_fatal("jubeat", "unknown touch algo, this is a bug");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_detach() {
|
||||||
|
TOUCH_ENABLE.store(false, std::memory_order_release);
|
||||||
|
TOUCH_STATE.store(0, std::memory_order_release);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <bitset>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
namespace games::jb {
|
||||||
|
|
||||||
|
// touch detection algorithm
|
||||||
|
enum JubeatTouchAlgorithm {
|
||||||
|
Legacy,
|
||||||
|
Improved,
|
||||||
|
Plus,
|
||||||
|
AcAccurate
|
||||||
|
};
|
||||||
|
|
||||||
|
// debug touch overlay mode
|
||||||
|
enum JubeatTouchDebugMode {
|
||||||
|
JbTouchDebugAuto, // boundary boxes when a touch screen is detected, else nothing
|
||||||
|
JbTouchDebugNone, // draw nothing
|
||||||
|
JbTouchDebugBox, // boundary boxes
|
||||||
|
JbTouchDebugAll, // boundary boxes and touch circles
|
||||||
|
};
|
||||||
|
|
||||||
|
// selected algorithm and debug mode (set from the launcher options)
|
||||||
|
extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
|
||||||
|
extern JubeatTouchDebugMode TOUCH_DEBUG_OVERLAY;
|
||||||
|
|
||||||
|
// minimum contact age in milliseconds before a touch registers (0 = off);
|
||||||
|
// filters momentary phantom touches from noisy panels
|
||||||
|
extern uint32_t TOUCH_DEBOUNCE_MS;
|
||||||
|
|
||||||
|
// atomically published panel state, read by the I/O layer
|
||||||
|
std::bitset<16> touch_state();
|
||||||
|
|
||||||
|
// read the current touch points and refresh the panel state
|
||||||
|
void touch_update();
|
||||||
|
|
||||||
|
// enable/disable touch handling on game attach/detach
|
||||||
|
void touch_attach();
|
||||||
|
void touch_detach();
|
||||||
|
|
||||||
|
// whether the debug overlay should currently draw anything
|
||||||
|
bool touch_debug_overlay_enabled();
|
||||||
|
|
||||||
|
// draw the debug touch overlay (boundary boxes and/or touch circles per the
|
||||||
|
// selected mode) onto the spice touch overlay window's device context
|
||||||
|
void touch_draw_debug_overlay(HDC hdc);
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
#include "util/execexe.h"
|
#include "util/execexe.h"
|
||||||
#include "acioemu/handle.h"
|
#include "acioemu/handle.h"
|
||||||
#include "misc/wintouchemu.h"
|
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "bi2a_hook.h"
|
#include "bi2a_hook.h"
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
// enable touch functions - set version to windows 7
|
|
||||||
// mingw otherwise doesn't load touch stuff
|
|
||||||
#define _WIN32_WINNT 0x0601
|
|
||||||
|
|
||||||
#include "nost.h"
|
#include "nost.h"
|
||||||
#include "poke.h"
|
#include "poke.h"
|
||||||
|
#include "touch_mode.h"
|
||||||
#include "hooks/setupapihook.h"
|
#include "hooks/setupapihook.h"
|
||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
|
#include "touch/native/nativetouchhook.h"
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
|
|
||||||
namespace games::nost {
|
namespace games::nost {
|
||||||
|
|
||||||
bool ENABLE_POKE = false;
|
bool ENABLE_POKE = false;
|
||||||
|
bool ENABLE_TOUCH_MODE = false;
|
||||||
|
|
||||||
NostGame::NostGame() : Game("Nostalgia") {
|
NostGame::NostGame() : Game("Nostalgia") {
|
||||||
}
|
}
|
||||||
@@ -43,9 +42,16 @@ namespace games::nost {
|
|||||||
setupapihook_init(avs::game::DLL_INSTANCE);
|
setupapihook_init(avs::game::DLL_INSTANCE);
|
||||||
setupapihook_add(touch_settings);
|
setupapihook_add(touch_settings);
|
||||||
|
|
||||||
// custom touch
|
const auto native_touch_ready = !wintouchemu::FORCE &&
|
||||||
// nostalgia crashes if you inject touch events too early
|
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||||
|
if (!native_touch_ready) {
|
||||||
|
wintouchemu::FORCE = true;
|
||||||
wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20);
|
wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20);
|
||||||
|
} else {
|
||||||
|
if (ENABLE_TOUCH_MODE) {
|
||||||
|
touch_mode::enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void NostGame::post_attach() {
|
void NostGame::post_attach() {
|
||||||
@@ -55,6 +61,9 @@ namespace games::nost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NostGame::detach() {
|
void NostGame::detach() {
|
||||||
|
if (ENABLE_TOUCH_MODE) {
|
||||||
|
touch_mode::disable();
|
||||||
|
}
|
||||||
if (ENABLE_POKE) {
|
if (ENABLE_POKE) {
|
||||||
poke::disable();
|
poke::disable();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
namespace games::nost {
|
namespace games::nost {
|
||||||
|
|
||||||
extern bool ENABLE_POKE;
|
extern bool ENABLE_POKE;
|
||||||
|
extern bool ENABLE_TOUCH_MODE;
|
||||||
|
|
||||||
class NostGame : public games::Game {
|
class NostGame : public games::Game {
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
#include "poke.h"
|
#include "poke.h"
|
||||||
|
|
||||||
#include <thread>
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
#include "games/nost/io.h"
|
#include "games/nost/io.h"
|
||||||
#include "cfg/api.h"
|
#include "cfg/api.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/native/inject.h"
|
||||||
|
#include "touch/native/nativetouchhook.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/precise_timer.h"
|
|
||||||
|
|
||||||
namespace games::nost::poke {
|
namespace games::nost::poke {
|
||||||
|
|
||||||
static std::thread *THREAD = nullptr;
|
static std::thread *THREAD = nullptr;
|
||||||
static volatile bool THREAD_RUNNING = false;
|
static std::atomic<bool> THREAD_RUNNING { false };
|
||||||
|
|
||||||
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_NUM {
|
// positions use game-client pixels, matching the legacy client-sized touch window
|
||||||
|
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_KEYPAD {
|
||||||
{EAM_IO_KEYPAD_0, {760, 440}},
|
{EAM_IO_KEYPAD_0, {760, 440}},
|
||||||
{EAM_IO_KEYPAD_1, {760, 385}},
|
{EAM_IO_KEYPAD_1, {760, 385}},
|
||||||
{EAM_IO_KEYPAD_2, {820, 385}},
|
{EAM_IO_KEYPAD_2, {820, 385}},
|
||||||
@@ -31,7 +34,8 @@ namespace games::nost::poke {
|
|||||||
{EAM_IO_KEYPAD_DECIMAL, {880, 440}} // also erase button
|
{EAM_IO_KEYPAD_DECIMAL, {880, 440}} // also erase button
|
||||||
};
|
};
|
||||||
|
|
||||||
static const std::unordered_map<games::nost::Buttons::Button, std::pair<LONG, LONG>> NOST_POKE {
|
static const std::unordered_map<
|
||||||
|
games::nost::Buttons::Button, std::pair<LONG, LONG>> NOST_POKE_BUTTONS {
|
||||||
{games::nost::Buttons::Button::PokeConfirm, {1100, 525}},
|
{games::nost::Buttons::Button::PokeConfirm, {1100, 525}},
|
||||||
{games::nost::Buttons::Button::PokeBack, {340, 100}},
|
{games::nost::Buttons::Button::PokeBack, {340, 100}},
|
||||||
{games::nost::Buttons::Button::PokeSong1, {450, 190}},
|
{games::nost::Buttons::Button::PokeSong1, {450, 190}},
|
||||||
@@ -46,13 +50,8 @@ namespace games::nost::poke {
|
|||||||
{games::nost::Buttons::Button::PokeDifficulty4, {820, 490}},
|
{games::nost::Buttons::Button::PokeDifficulty4, {820, 490}},
|
||||||
};
|
};
|
||||||
|
|
||||||
void clear_touch_points(std::vector<TouchPoint> *touch_points) {
|
static bool inject_poke_position(POINT position, bool down) {
|
||||||
std::vector<DWORD> touch_ids;
|
return nativetouch::inject::inject_synthetic_touch(position, down);
|
||||||
for (auto &touch : *touch_points) {
|
|
||||||
touch_ids.emplace_back(touch.id);
|
|
||||||
}
|
|
||||||
touch_remove_points(&touch_ids);
|
|
||||||
touch_points->clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void enable() {
|
void enable() {
|
||||||
@@ -61,126 +60,143 @@ namespace games::nost::poke {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!nativetouch::is_hooked()) {
|
||||||
|
log_warning("poke", "Nostalgia poke requires native touch; disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// create new thread
|
// create new thread
|
||||||
THREAD_RUNNING = true;
|
THREAD_RUNNING = true;
|
||||||
THREAD = new std::thread([] {
|
THREAD = new std::thread([] {
|
||||||
timeutils::PreciseSleepTimer timer;
|
|
||||||
const DWORD touch_id = (DWORD)(0xFFFFFFFE);
|
|
||||||
|
|
||||||
const int swipe_anim_total_frames = 6;
|
const int swipe_anim_total_frames = 6;
|
||||||
const int swipe_anim_y = 300;
|
const int swipe_anim_y = 300;
|
||||||
|
|
||||||
const int swipe_next_page_x_begin = 1120;
|
const int swipe_next_page_x_begin = 1120;
|
||||||
const int swipe_next_page_x_end = 1120-120;
|
const int swipe_next_page_x_end = 1120 - 120;
|
||||||
|
|
||||||
const int swipe_prev_page_x_begin = 280-120;
|
const int swipe_prev_page_x_begin = 280 - 120;
|
||||||
const int swipe_prev_page_x_end = 280;
|
const int swipe_prev_page_x_end = 280;
|
||||||
|
|
||||||
int next_page_anim_index = -1;
|
int next_page_anim_index = -1;
|
||||||
int prev_page_anim_index = -1;
|
int prev_page_anim_index = -1;
|
||||||
std::vector<TouchPoint> touch_points;
|
bool contact_active = false;
|
||||||
|
bool release_pending = false;
|
||||||
bool touch_release = false;
|
POINT last_position {};
|
||||||
|
std::unordered_map<games::nost::Buttons::Button, bool> button_states;
|
||||||
|
uint16_t last_keypad_state = 0;
|
||||||
|
|
||||||
// log
|
// log
|
||||||
log_info("poke", "enabled");
|
log_info("poke", "enabled");
|
||||||
|
|
||||||
// set variable to false to stop
|
// set variable to false to stop
|
||||||
while (THREAD_RUNNING) {
|
while (THREAD_RUNNING) {
|
||||||
// clean up touch from last frame
|
if (release_pending) {
|
||||||
if (!touch_points.empty()) {
|
if (contact_active) {
|
||||||
if (touch_release) {
|
inject_poke_position(last_position, false);
|
||||||
clear_touch_points(&touch_points);
|
|
||||||
touch_release = false;
|
|
||||||
} else {
|
|
||||||
touch_points.clear();
|
|
||||||
}
|
}
|
||||||
|
contact_active = false;
|
||||||
|
release_pending = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto &buttons = games::nost::get_buttons();
|
auto &buttons = games::nost::get_buttons();
|
||||||
|
std::optional<POINT> touch_position;
|
||||||
|
bool release_after_touch = false;
|
||||||
|
|
||||||
|
const auto button_triggered = [&](games::nost::Buttons::Button button) {
|
||||||
|
const auto pressed =
|
||||||
|
GameAPI::Buttons::getState(RI_MGR, buttons.at(button));
|
||||||
|
const auto triggered = pressed && !button_states[button];
|
||||||
|
button_states[button] = pressed;
|
||||||
|
return triggered;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::optional<POINT> triggered_touch_position;
|
||||||
|
for (const auto& it : NOST_POKE_BUTTONS) {
|
||||||
|
if (button_triggered(it.first) && !triggered_touch_position.has_value()) {
|
||||||
|
triggered_touch_position = POINT {
|
||||||
|
it.second.first,
|
||||||
|
it.second.second,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto next_triggered =
|
||||||
|
button_triggered(games::nost::Buttons::PokeNextPage);
|
||||||
|
const auto prev_triggered =
|
||||||
|
button_triggered(games::nost::Buttons::PokePrevPage);
|
||||||
|
|
||||||
|
const auto keypad_state = eamuse_get_keypad_state(0);
|
||||||
|
if (!triggered_touch_position.has_value()) {
|
||||||
|
for (const auto& it : NOST_POKE_KEYPAD) {
|
||||||
|
const auto mask = static_cast<uint16_t>(1 << it.first);
|
||||||
|
if ((keypad_state & mask) && !(last_keypad_state & mask)) {
|
||||||
|
triggered_touch_position = POINT {
|
||||||
|
it.second.first,
|
||||||
|
it.second.second,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last_keypad_state = keypad_state;
|
||||||
|
|
||||||
if (0 <= next_page_anim_index) {
|
if (0 <= next_page_anim_index) {
|
||||||
const auto delta =
|
const auto delta =
|
||||||
(swipe_next_page_x_end - swipe_next_page_x_begin)
|
(swipe_next_page_x_end - swipe_next_page_x_begin)
|
||||||
* (swipe_anim_total_frames - next_page_anim_index) / swipe_anim_total_frames;
|
* (swipe_anim_total_frames - next_page_anim_index) / swipe_anim_total_frames;
|
||||||
|
|
||||||
TouchPoint tp {
|
touch_position = POINT {
|
||||||
.id = touch_id,
|
swipe_next_page_x_begin + delta,
|
||||||
.x = (LONG)swipe_next_page_x_begin + delta,
|
swipe_anim_y,
|
||||||
.y = (LONG)swipe_anim_y,
|
|
||||||
.mouse = true,
|
|
||||||
};
|
};
|
||||||
touch_points.emplace_back(tp);
|
|
||||||
next_page_anim_index--;
|
next_page_anim_index--;
|
||||||
if (next_page_anim_index < 0) {
|
if (next_page_anim_index < 0) {
|
||||||
touch_release = true;
|
release_after_touch = true;
|
||||||
}
|
}
|
||||||
} else if (0 <= prev_page_anim_index) {
|
} else if (0 <= prev_page_anim_index) {
|
||||||
const auto delta =
|
const auto delta =
|
||||||
(swipe_prev_page_x_end - swipe_prev_page_x_begin)
|
(swipe_prev_page_x_end - swipe_prev_page_x_begin)
|
||||||
* (swipe_anim_total_frames - prev_page_anim_index) / swipe_anim_total_frames;
|
* (swipe_anim_total_frames - prev_page_anim_index) / swipe_anim_total_frames;
|
||||||
|
|
||||||
TouchPoint tp {
|
touch_position = POINT {
|
||||||
.id = touch_id,
|
swipe_prev_page_x_begin + delta,
|
||||||
.x = (LONG)swipe_prev_page_x_begin + delta,
|
swipe_anim_y,
|
||||||
.y = (LONG)swipe_anim_y,
|
|
||||||
.mouse = true,
|
|
||||||
};
|
};
|
||||||
touch_points.emplace_back(tp);
|
|
||||||
prev_page_anim_index--;
|
prev_page_anim_index--;
|
||||||
if (prev_page_anim_index < 0) {
|
if (prev_page_anim_index < 0) {
|
||||||
touch_release = true;
|
release_after_touch = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const auto& it : NOST_POKE) {
|
touch_position = triggered_touch_position;
|
||||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(it.first))) {
|
release_after_touch = touch_position.has_value();
|
||||||
TouchPoint tp {
|
|
||||||
.id = touch_id,
|
|
||||||
.x = it.second.first,
|
|
||||||
.y = it.second.second,
|
|
||||||
.mouse = true,
|
|
||||||
};
|
|
||||||
touch_points.emplace_back(tp);
|
|
||||||
touch_release = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!touch_release) {
|
|
||||||
const auto state = eamuse_get_keypad_state(0);
|
|
||||||
if (state) {
|
|
||||||
for (const auto& it : NOST_POKE_NUM) {
|
|
||||||
if (state & (1 << it.first)) {
|
|
||||||
TouchPoint tp {
|
|
||||||
.id = touch_id,
|
|
||||||
.x = it.second.first,
|
|
||||||
.y = it.second.second,
|
|
||||||
.mouse = true,
|
|
||||||
};
|
|
||||||
touch_points.emplace_back(tp);
|
|
||||||
touch_release = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// start animations for next frame
|
// start animations for next frame
|
||||||
if (!touch_release) {
|
if (!touch_position.has_value()) {
|
||||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokeNextPage))) {
|
if (next_triggered) {
|
||||||
next_page_anim_index = swipe_anim_total_frames;
|
next_page_anim_index = swipe_anim_total_frames;
|
||||||
}
|
}
|
||||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokePrevPage))) {
|
|
||||||
|
if (prev_triggered) {
|
||||||
prev_page_anim_index = swipe_anim_total_frames;
|
prev_page_anim_index = swipe_anim_total_frames;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!touch_points.empty()) {
|
if (touch_position.has_value() &&
|
||||||
touch_write_points(&touch_points);
|
inject_poke_position(*touch_position, true)) {
|
||||||
|
contact_active = true;
|
||||||
|
last_position = *touch_position;
|
||||||
|
}
|
||||||
|
if (release_after_touch && contact_active) {
|
||||||
|
release_pending = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// slow down
|
// slow down
|
||||||
timer.sleep(30);
|
std::this_thread::sleep_for(std::chrono::milliseconds(30));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contact_active) {
|
||||||
|
inject_poke_position(last_position, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
#include "touch_mode.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "touch/native/nativetouchhook.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
namespace games::nost::touch_mode {
|
||||||
|
|
||||||
|
// native contact positions feed piano input directly. native events reach the game
|
||||||
|
// in nav mode and are suppressed in piano mode. mode-button contacts are always
|
||||||
|
// suppressed and change that routing after release.
|
||||||
|
static constexpr LONG PIANO_LEFT_GAP = 11;
|
||||||
|
static constexpr LONG PIANO_RIGHT_GAP = 10;
|
||||||
|
static constexpr uint32_t PIANO_KEY_COUNT = 28;
|
||||||
|
|
||||||
|
struct TouchGeometry {
|
||||||
|
HWND window = nullptr;
|
||||||
|
RECT mode_button {};
|
||||||
|
LONG client_width = 0;
|
||||||
|
LONG client_height = 0;
|
||||||
|
|
||||||
|
bool valid() const {
|
||||||
|
return window != nullptr && client_width > 0 && client_height > 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NativeContact {
|
||||||
|
POINT position {};
|
||||||
|
|
||||||
|
// position contains game-client coordinates
|
||||||
|
bool client_position_valid = false;
|
||||||
|
|
||||||
|
// down began on the mode switch button; remains true through up
|
||||||
|
bool mode_button = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::atomic_bool accept_events { false };
|
||||||
|
static std::atomic<Mode> current_mode_state { Mode::Nav };
|
||||||
|
static std::mutex state_mutex;
|
||||||
|
static TouchGeometry touch_geometry;
|
||||||
|
|
||||||
|
// native contacts are kept by ID so each contact contributes exactly one position
|
||||||
|
static std::unordered_map<DWORD, NativeContact> active_contacts;
|
||||||
|
|
||||||
|
// a hardware button release requests one change after all contacts are released
|
||||||
|
static bool mode_change_pending = false;
|
||||||
|
|
||||||
|
static void reset_state_locked() {
|
||||||
|
current_mode_state.store(Mode::Nav, std::memory_order_release);
|
||||||
|
touch_geometry = {};
|
||||||
|
active_contacts.clear();
|
||||||
|
mode_change_pending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// hardware contacts arrive in screen coordinates
|
||||||
|
static bool native_touch_in_button(const nativetouch::NativeTouchEvent &event) {
|
||||||
|
if (!touch_geometry.valid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
POINT position { event.x, event.y };
|
||||||
|
if (!ScreenToClient(touch_geometry.window, &position)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return PtInRect(&touch_geometry.mode_button, position) != FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool update_touch_state(const nativetouch::NativeTouchEvent &event) {
|
||||||
|
std::lock_guard<std::mutex> lock(state_mutex);
|
||||||
|
|
||||||
|
// first, process down / move events
|
||||||
|
if (event.down || event.move) {
|
||||||
|
auto contact = active_contacts.try_emplace(event.id).first;
|
||||||
|
|
||||||
|
// keep track of IDs that began as a down on the mode switch button
|
||||||
|
if (event.down) {
|
||||||
|
contact->second.mode_button = native_touch_in_button(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for valid position
|
||||||
|
POINT position { event.x, event.y };
|
||||||
|
if (touch_geometry.window != nullptr &&
|
||||||
|
ScreenToClient(touch_geometry.window, &position)) {
|
||||||
|
contact->second.position = position;
|
||||||
|
contact->second.client_position_valid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto contact = active_contacts.find(event.id);
|
||||||
|
const bool mode_button_contact = contact != active_contacts.end() &&
|
||||||
|
contact->second.mode_button;
|
||||||
|
|
||||||
|
// process up events
|
||||||
|
if (event.up) {
|
||||||
|
active_contacts.erase(event.id);
|
||||||
|
|
||||||
|
// if a contact that began down event on the mode switch button has
|
||||||
|
// been released, a mode switch is now pending
|
||||||
|
if (mode_button_contact) {
|
||||||
|
mode_change_pending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply the change on the final hardware up. switching earlier would
|
||||||
|
// split another contact's down and up events across different modes
|
||||||
|
if (mode_change_pending && active_contacts.empty()) {
|
||||||
|
mode_change_pending = false;
|
||||||
|
const auto next_mode = current_mode() == Mode::Nav ? Mode::Piano : Mode::Nav;
|
||||||
|
current_mode_state.store(next_mode, std::memory_order_release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mode_button_contact;
|
||||||
|
}
|
||||||
|
|
||||||
|
// install the Nostalgia-specific native touch interception
|
||||||
|
void enable() {
|
||||||
|
if (accept_events.exchange(true, std::memory_order_acq_rel)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(state_mutex);
|
||||||
|
reset_state_locked();
|
||||||
|
}
|
||||||
|
|
||||||
|
nativetouch::set_input_filter(filter_native_touch);
|
||||||
|
log_info("nost::touch", "enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
void disable() {
|
||||||
|
if (!accept_events.exchange(false, std::memory_order_acq_rel)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nativetouch::set_input_filter(nullptr);
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(state_mutex);
|
||||||
|
reset_state_locked();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool enabled() {
|
||||||
|
return accept_events.load(std::memory_order_acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mode current_mode() {
|
||||||
|
return current_mode_state.load(std::memory_order_acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
// publish the rendered overlay button rectangle in game-client pixels
|
||||||
|
void publish_button_bounds(HWND window, const RECT &client_bounds) {
|
||||||
|
TouchGeometry next {};
|
||||||
|
RECT client_rect {};
|
||||||
|
if (window != nullptr && GetClientRect(window, &client_rect) &&
|
||||||
|
client_rect.right > 0 && client_rect.bottom > 0) {
|
||||||
|
next.window = window;
|
||||||
|
next.mode_button = client_bounds;
|
||||||
|
next.client_width = client_rect.right;
|
||||||
|
next.client_height = client_rect.bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(state_mutex);
|
||||||
|
touch_geometry = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the active 28-key piano bitfield for the PANB input update
|
||||||
|
uint32_t piano_key_state() {
|
||||||
|
if (!enabled() || current_mode() != Mode::Piano) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(state_mutex);
|
||||||
|
if (current_mode() != Mode::Piano || !touch_geometry.valid()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t state = 0;
|
||||||
|
for (const auto &contact : active_contacts) {
|
||||||
|
// invalid position or mode-button contact; ignore these contacts
|
||||||
|
if (!contact.second.client_position_valid || contact.second.mode_button) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &position = contact.second.position;
|
||||||
|
|
||||||
|
// outside the client area or on the mode button; ignore these contacts
|
||||||
|
if (position.x < 0 || position.x >= touch_geometry.client_width ||
|
||||||
|
position.y < 0 || position.y >= touch_geometry.client_height ||
|
||||||
|
PtInRect(&touch_geometry.mode_button, position)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// divide the inset width evenly; touches in either side gap clamp to
|
||||||
|
// the nearest outer key so the physical screen edges remain playable
|
||||||
|
const auto piano_width =
|
||||||
|
touch_geometry.client_width - PIANO_LEFT_GAP - PIANO_RIGHT_GAP;
|
||||||
|
uint32_t key = 0;
|
||||||
|
if (position.x >= touch_geometry.client_width - PIANO_RIGHT_GAP) {
|
||||||
|
key = PIANO_KEY_COUNT - 1;
|
||||||
|
} else if (position.x >= PIANO_LEFT_GAP && piano_width > 0) {
|
||||||
|
key = static_cast<uint32_t>(
|
||||||
|
(position.x - PIANO_LEFT_GAP) * PIANO_KEY_COUNT / piano_width);
|
||||||
|
}
|
||||||
|
state |= UINT32_C(1) << key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update native contacts and report whether this event should be hidden from the game
|
||||||
|
bool filter_native_touch(const nativetouch::NativeTouchEvent &event) {
|
||||||
|
// synthetic events are outside this hardware-only feature
|
||||||
|
if (!enabled() || event.synthetic) {
|
||||||
|
// false leaves the event visible to the game
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// snapshot routing before an up event can commit a pending mode switch
|
||||||
|
const bool piano_mode_before_update = current_mode() == Mode::Piano;
|
||||||
|
|
||||||
|
// update the contact lifetime and commit any pending switch when safe
|
||||||
|
const bool mode_button_contact = update_touch_state(event);
|
||||||
|
|
||||||
|
// hide every event in a contact that began on the mode switch button
|
||||||
|
if (mode_button_contact) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// piano mode consumes hardware events; nav mode forwards them to the game
|
||||||
|
return piano_mode_before_update;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "touch/native/nativetouchhook.h"
|
||||||
|
|
||||||
|
namespace games::nost::touch_mode {
|
||||||
|
|
||||||
|
// nav mode forwards contacts to the game; piano mode converts them into piano keys
|
||||||
|
enum class Mode {
|
||||||
|
Nav,
|
||||||
|
Piano,
|
||||||
|
};
|
||||||
|
|
||||||
|
void enable();
|
||||||
|
void disable();
|
||||||
|
bool enabled();
|
||||||
|
|
||||||
|
Mode current_mode();
|
||||||
|
|
||||||
|
void publish_button_bounds(HWND window, const RECT &client_bounds);
|
||||||
|
|
||||||
|
uint32_t piano_key_state();
|
||||||
|
|
||||||
|
bool filter_native_touch(const nativetouch::NativeTouchEvent &event);
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@
|
|||||||
#include "util/unity_player.h"
|
#include "util/unity_player.h"
|
||||||
#include "util/execexe.h"
|
#include "util/execexe.h"
|
||||||
#include "acioemu/handle.h"
|
#include "acioemu/handle.h"
|
||||||
#include "misc/wintouchemu.h"
|
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
#include "util/socd_cleaner.h"
|
#include "util/socd_cleaner.h"
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#include "acio/icca/icca.h"
|
#include "acio/icca/icca.h"
|
||||||
#include "io.h"
|
#include "io.h"
|
||||||
#include "misc/wintouchemu.h"
|
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
|
|||||||
@@ -16,16 +16,16 @@
|
|||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
#include "launcher/logger.h"
|
#include "launcher/logger.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
|
#include "misc/wintouchemu.h"
|
||||||
#include "util/sysutils.h"
|
#include "util/sysutils.h"
|
||||||
#include "io.h"
|
#include "io.h"
|
||||||
#include "util/deferlog.h"
|
#include "util/deferlog.h"
|
||||||
#include "misc/nativetouchhook.h"
|
#include "touch/native/nativetouchhook.h"
|
||||||
#include "misc/wintouchemu.h"
|
|
||||||
|
|
||||||
namespace games::popn {
|
namespace games::popn {
|
||||||
|
|
||||||
bool SHOW_PIKA_MONITOR_WARNING = false;
|
bool SHOW_PIKA_MONITOR_WARNING = false;
|
||||||
bool NATIVE_TOUCH = false;
|
bool NATIVE_TOUCH = true;
|
||||||
|
|
||||||
#if SPICE64 && !SPICE_XP
|
#if SPICE64 && !SPICE_XP
|
||||||
|
|
||||||
@@ -715,13 +715,16 @@ namespace games::popn {
|
|||||||
// 00000100 0000000B 00000001 (button 9)
|
// 00000100 0000000B 00000001 (button 9)
|
||||||
// set third column to 0 and it will work with BIO2
|
// set third column to 0 and it will work with BIO2
|
||||||
|
|
||||||
if (!GRAPHICS_WINDOWED) {
|
NATIVE_TOUCH = !wintouchemu::FORCE &&
|
||||||
if (NATIVE_TOUCH) {
|
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||||
nativetouchhook::hook(avs::game::DLL_INSTANCE);
|
if (!NATIVE_TOUCH) {
|
||||||
} else {
|
|
||||||
wintouchemu::FORCE = true;
|
wintouchemu::FORCE = true;
|
||||||
|
if (!GRAPHICS_WINDOWED || GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||||
wintouchemu::hook_title_ends("", "Main Screen", avs::game::DLL_INSTANCE);
|
wintouchemu::hook_title_ends(
|
||||||
|
"",
|
||||||
|
"Main Screen",
|
||||||
|
avs::game::DLL_INSTANCE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ void games::rb::RBGame::attach() {
|
|||||||
Game::attach();
|
Game::attach();
|
||||||
|
|
||||||
if (1000 < games::rb::TOUCH_POLL_RATE) {
|
if (1000 < games::rb::TOUCH_POLL_RATE) {
|
||||||
log_fatal("rb", "-rbtouchsize is set too high; cannot exceed 1000");
|
log_fatal("rb", "-rbtouchhz is set too high; cannot exceed 1000");
|
||||||
}
|
}
|
||||||
|
|
||||||
// init stuff
|
// init stuff
|
||||||
@@ -60,7 +60,7 @@ void games::rb::RBGame::attach() {
|
|||||||
log_info("rb", "force increasing touch poll rate by hooking SleepEx ({}Hz)", TOUCH_POLL_RATE);
|
log_info("rb", "force increasing touch poll rate by hooking SleepEx ({}Hz)", TOUCH_POLL_RATE);
|
||||||
SleepEx_orig = detour::iat_try("SleepEx", SleepEx_hook, avs::game::DLL_INSTANCE);
|
SleepEx_orig = detour::iat_try("SleepEx", SleepEx_hook, avs::game::DLL_INSTANCE);
|
||||||
} else {
|
} else {
|
||||||
log_info("rb", "not changing touch poll rate (120Hz by default)");
|
log_info("rb", "not changing touch poll rate (~125Hz by default)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
namespace games::rb {
|
namespace games::rb {
|
||||||
|
|
||||||
extern uint16_t TOUCH_SCALING;
|
extern uint16_t TOUCH_SCALING;
|
||||||
extern uint8_t TOUCH_SIZE;
|
|
||||||
extern uint16_t TOUCH_POLL_RATE;
|
extern uint16_t TOUCH_POLL_RATE;
|
||||||
|
|
||||||
class RBGame : public games::Game {
|
class RBGame : public games::Game {
|
||||||
|
|||||||
@@ -4,7 +4,10 @@
|
|||||||
|
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "rawinput/touch.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
|
#include "touch_debug.h"
|
||||||
|
#include "touch_defs.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/time.h"
|
#include "util/time.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
@@ -12,28 +15,38 @@
|
|||||||
static std::string WINDOW_TITLE = "REFLEC BEAT";
|
static std::string WINDOW_TITLE = "REFLEC BEAT";
|
||||||
|
|
||||||
namespace games::rb {
|
namespace games::rb {
|
||||||
uint16_t TOUCH_SCALING = 1000;
|
static constexpr double TOUCH_POINT_OFFSET_DIVISOR = 3.0;
|
||||||
uint8_t TOUCH_SIZE = 1;
|
static constexpr double X_INPUT_COORDINATE_COUNT = 54.0;
|
||||||
|
|
||||||
|
uint16_t TOUCH_SCALING = TOUCH_SCALE_DEFAULT;
|
||||||
|
|
||||||
|
static void packet_set_bit(unsigned char *packet, int bit) {
|
||||||
|
packet[TOUCH_PACKET_DATA_OFFSET + bit / 8] |=
|
||||||
|
static_cast<unsigned char>(1u << (bit % 8));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) {
|
games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) : log_fps(log_fps) {
|
||||||
this->log_fps = log_fps;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void games::rb::ReflecBeatTouchDeviceHandle::grid_insert(unsigned char *data, int cursor_x, int cursor_y) {
|
void games::rb::ReflecBeatTouchDeviceHandle::grid_insert(unsigned char *data, int cursor_x, int cursor_y) {
|
||||||
|
|
||||||
// scale to grid position - there are 48 columns and 76 rows of IR sensors.
|
// read() contracts X uniformly so screen edges land within usable sensors 2..45
|
||||||
// for whatever reason, the last y row (#75) results in weird input few rows above; just drop it
|
int grid_x = CLAMP(
|
||||||
int grid_x = CLAMP((cursor_x * 48) / window_width, 0, 47);
|
(cursor_x * X_SENSOR_COUNT) / window_width,
|
||||||
int grid_y = CLAMP((cursor_y * 76) / window_height, 0, 74);
|
0,
|
||||||
|
X_SENSOR_COUNT - 1);
|
||||||
|
int grid_y = CLAMP(
|
||||||
|
(cursor_y * Y_SENSOR_COUNT) / window_height,
|
||||||
|
0,
|
||||||
|
Y_SENSOR_COUNT - 1);
|
||||||
|
|
||||||
// get bit positions
|
// get bit positions
|
||||||
int bit_x = 88 + grid_x;
|
int bit_x = X_SENSOR_FIRST_BIT + grid_x;
|
||||||
int bit_y = 74 - grid_y;
|
int bit_y = Y_SENSOR_FIRST_BIT - grid_y;
|
||||||
|
|
||||||
// insert bits
|
packet_set_bit(data, bit_x);
|
||||||
data[3 + bit_x / 8] |= (char) 1 << (bit_x % 8);
|
packet_set_bit(data, bit_y);
|
||||||
data[3 + bit_y / 8] |= (char) 1 << (bit_y % 8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
||||||
@@ -49,6 +62,9 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
|||||||
|
|
||||||
if (wnd != nullptr) {
|
if (wnd != nullptr) {
|
||||||
|
|
||||||
|
// cache the game window so read() can size against it regardless of focus
|
||||||
|
this->game_hwnd = wnd;
|
||||||
|
|
||||||
// reset window process to make the game not crash
|
// reset window process to make the game not crash
|
||||||
SetWindowLongPtr(wnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(DefWindowProc));
|
SetWindowLongPtr(wnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(DefWindowProc));
|
||||||
|
|
||||||
@@ -68,18 +84,19 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
|||||||
// update window
|
// update window
|
||||||
SetWindowPos(wnd, nullptr, 0, 0, 0, 0,
|
SetWindowPos(wnd, nullptr, 0, 0, 0, 0,
|
||||||
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER);
|
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// request automatic aspect ratio fixes
|
||||||
|
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
|
||||||
|
|
||||||
// create touch window
|
// create touch window
|
||||||
touch_create_wnd(wnd);
|
touch_create_wnd(wnd);
|
||||||
|
|
||||||
} else {
|
// show fullscreen game window because it lost focus
|
||||||
|
if (!GRAPHICS_WINDOWED) {
|
||||||
// create touch window
|
|
||||||
touch_create_wnd(wnd);
|
|
||||||
|
|
||||||
// show game window because it lost focus
|
|
||||||
ShowWindow(wnd, SW_SHOW);
|
ShowWindow(wnd, SW_SHOW);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// fallback to dx hook
|
// fallback to dx hook
|
||||||
@@ -91,18 +108,21 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
|||||||
ShowCursor(TRUE);
|
ShowCursor(TRUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
touch_debug_attach();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
|
int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
|
||||||
|
|
||||||
// check buffer size
|
// check buffer size
|
||||||
if (nNumberOfBytesToRead < 20) {
|
if (nNumberOfBytesToRead < TOUCH_PACKET_SIZE) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get window
|
// size against the game window cached in open() (not GetForegroundWindow), so
|
||||||
HWND window = GetForegroundWindow();
|
// touch stays correctly scaled and keeps working when the game loses focus
|
||||||
|
HWND window = this->game_hwnd;
|
||||||
if (window == nullptr) {
|
if (window == nullptr) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -122,17 +142,17 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create data
|
// create data
|
||||||
unsigned char data[20] {};
|
unsigned char data[TOUCH_PACKET_SIZE] {};
|
||||||
|
|
||||||
// data header
|
// data header
|
||||||
data[0] = 0x55;
|
data[0] = 0x55;
|
||||||
data[2] = 0x4C;
|
data[2] = 0x4C;
|
||||||
|
|
||||||
const auto SCALE_FACTOR = games::rb::TOUCH_SCALING / 1000.f;
|
const float scale_factor = games::rb::TOUCH_SCALING / (float) games::rb::TOUCH_SCALE_DEFAULT;
|
||||||
|
|
||||||
// iterate all touch points
|
// iterate all touch points
|
||||||
auto offset_x = (int) (window_width / 48.0 / 3.0);
|
int offset_x = (int) (window_width / (double) X_SENSOR_COUNT / TOUCH_POINT_OFFSET_DIVISOR);
|
||||||
auto offset_y = (int) (window_height / 76.0 / 3.0);
|
int offset_y = (int) (window_height / (double) Y_SENSOR_COUNT / TOUCH_POINT_OFFSET_DIVISOR);
|
||||||
std::vector<TouchPoint> touch_points;
|
std::vector<TouchPoint> touch_points;
|
||||||
touch_get_points(touch_points);
|
touch_get_points(touch_points);
|
||||||
for (auto &point : touch_points) {
|
for (auto &point : touch_points) {
|
||||||
@@ -148,24 +168,30 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
|
|||||||
}
|
}
|
||||||
|
|
||||||
// apply scaling
|
// apply scaling
|
||||||
x = x - (window_width * (1.f - SCALE_FACTOR) / 2);
|
x = x - (window_width * (1.f - scale_factor) / 2);
|
||||||
x = x / SCALE_FACTOR;
|
x = x / scale_factor;
|
||||||
y = y - (window_height * (1.f - SCALE_FACTOR) / 2);
|
y = y - (window_height * (1.f - scale_factor) / 2);
|
||||||
y = y / SCALE_FACTOR;
|
y = y / scale_factor;
|
||||||
|
|
||||||
if (x < 0 || window_width <= x || y < 0 || window_height <= y) {
|
if (x < 0 || window_width <= x || y < 0 || window_height <= y) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// point scaling
|
// point coordinates
|
||||||
const auto point_x = (int) ((x - window_width / 2.0) * 48.0 / 54.0 + window_width / 2.0);
|
// keep the verified left edge fixed while adding up to one-third of a beam
|
||||||
const auto point_y = (int) (y - window_height / 76);
|
// at the right edge, preventing the finger footprint from including sensor 44
|
||||||
|
const auto point_x = (int) (
|
||||||
|
(x - window_width / 2.0) * X_SENSOR_COUNT / X_INPUT_COORDINATE_COUNT +
|
||||||
|
window_width / 2.0 +
|
||||||
|
x / (X_SENSOR_COUNT * TOUCH_POINT_OFFSET_DIVISOR));
|
||||||
|
const auto point_y = (int) y;
|
||||||
|
|
||||||
// center
|
// model a finger as a 3x3 block of IR sensors around the touch point
|
||||||
grid_insert(data, point_x, point_y);
|
// this gives better accuracy (than just 1x1) since the logic below
|
||||||
|
// can toggle anywhere from 1x1 to 2x2, and the game engine calculates
|
||||||
// surrounding points
|
// the center point, which means by inserting up to 8 extra blocks
|
||||||
if (games::rb::TOUCH_SIZE == 3) {
|
// we are emulating a sub-"pixel" resolution
|
||||||
|
grid_insert(data, point_x, point_y); // center
|
||||||
grid_insert(data, point_x - offset_x, point_y); // west
|
grid_insert(data, point_x - offset_x, point_y); // west
|
||||||
grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest
|
grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest
|
||||||
grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest
|
grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest
|
||||||
@@ -175,10 +201,11 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
|
|||||||
grid_insert(data, point_x, point_y - offset_y); // north
|
grid_insert(data, point_x, point_y - offset_y); // north
|
||||||
grid_insert(data, point_x, point_y + offset_y); // south
|
grid_insert(data, point_x, point_y + offset_y); // south
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
touch_debug_publish(data, is_landscape);
|
||||||
|
|
||||||
// copy data to buffer
|
// copy data to buffer
|
||||||
memcpy(lpBuffer, data, 20);
|
memcpy(lpBuffer, data, TOUCH_PACKET_SIZE);
|
||||||
|
|
||||||
// update frame logging
|
// update frame logging
|
||||||
if (log_fps) {
|
if (log_fps) {
|
||||||
@@ -193,7 +220,7 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
|
|||||||
}
|
}
|
||||||
|
|
||||||
// return amount of bytes written
|
// return amount of bytes written
|
||||||
return 20;
|
return TOUCH_PACKET_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
int games::rb::ReflecBeatTouchDeviceHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) {
|
int games::rb::ReflecBeatTouchDeviceHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) {
|
||||||
@@ -207,6 +234,8 @@ int games::rb::ReflecBeatTouchDeviceHandle::device_io(DWORD dwIoControlCode, LPV
|
|||||||
|
|
||||||
bool games::rb::ReflecBeatTouchDeviceHandle::close() {
|
bool games::rb::ReflecBeatTouchDeviceHandle::close() {
|
||||||
|
|
||||||
|
touch_debug_detach();
|
||||||
|
|
||||||
// detach touch module
|
// detach touch module
|
||||||
touch_detach();
|
touch_detach();
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ namespace games::rb {
|
|||||||
int window_width = 1080;
|
int window_width = 1080;
|
||||||
int window_height = 1920;
|
int window_height = 1920;
|
||||||
|
|
||||||
|
// game window resolved in open(); read() sizes against this instead of the
|
||||||
|
// foreground window so touch stays correctly scaled and keeps working when
|
||||||
|
// the game is not the foreground window (rawinput delivers touch regardless
|
||||||
|
// of focus)
|
||||||
|
HWND game_hwnd = nullptr;
|
||||||
|
|
||||||
// logging
|
// logging
|
||||||
bool log_fps = false;
|
bool log_fps = false;
|
||||||
uint64_t log_time = 0;
|
uint64_t log_time = 0;
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
#include "touch_debug.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
|
#include <cstring>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include "external/imgui/imgui.h"
|
||||||
|
#include "games/rb/rb.h"
|
||||||
|
#include "games/rb/touch_defs.h"
|
||||||
|
|
||||||
|
namespace games::rb {
|
||||||
|
|
||||||
|
struct TouchDebugState {
|
||||||
|
std::array<unsigned char, TOUCH_PACKET_SIZE> packet {};
|
||||||
|
bool is_landscape = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::atomic_bool TOUCH_DEBUG_OVERLAY = false;
|
||||||
|
static std::atomic_bool TOUCH_ACTIVE = false;
|
||||||
|
static std::mutex TOUCH_DEBUG_STATE_M;
|
||||||
|
static TouchDebugState TOUCH_DEBUG_STATE;
|
||||||
|
|
||||||
|
static float touch_scale_factor() {
|
||||||
|
return TOUCH_SCALING / (float) TOUCH_SCALE_DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clear_touch_debug_state() {
|
||||||
|
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
|
||||||
|
TOUCH_DEBUG_STATE = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static TouchDebugState get_touch_debug_state() {
|
||||||
|
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
|
||||||
|
return TOUCH_DEBUG_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool packet_bit_active(
|
||||||
|
const std::array<unsigned char, TOUCH_PACKET_SIZE> &packet, int bit) {
|
||||||
|
return (packet[TOUCH_PACKET_DATA_OFFSET + bit / 8] & (1u << (bit % 8))) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sensor_center(int sensor, int sensor_count, int extent) {
|
||||||
|
return ((sensor * 2 + 1) * extent) / (sensor_count * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
static float sensor_span_position(int sensor, int sensor_count, int extent) {
|
||||||
|
return sensor * (extent - 1) / (float) (sensor_count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool touch_debug_overlay_enabled() {
|
||||||
|
return TOUCH_DEBUG_OVERLAY && TOUCH_ACTIVE.load(std::memory_order_acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_draw_debug_overlay() {
|
||||||
|
if (!touch_debug_overlay_enabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &io = ImGui::GetIO();
|
||||||
|
int width = static_cast<int>(io.DisplaySize.x);
|
||||||
|
int height = static_cast<int>(io.DisplaySize.y);
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float scale_factor = touch_scale_factor();
|
||||||
|
const float left = width * (1.f - scale_factor) / 2.f;
|
||||||
|
const float top = height * (1.f - scale_factor) / 2.f;
|
||||||
|
const float right = width - left;
|
||||||
|
const float bottom = height - top;
|
||||||
|
|
||||||
|
TouchDebugState state = get_touch_debug_state();
|
||||||
|
ImDrawList *draw_list = ImGui::GetBackgroundDrawList();
|
||||||
|
auto draw_line = [&](float x1, float y1, float x2, float y2) {
|
||||||
|
draw_list->AddLine(
|
||||||
|
ImVec2(x1, y1), ImVec2(x2, y2),
|
||||||
|
IM_COL32(0, 255, 64, 255), 2.f);
|
||||||
|
};
|
||||||
|
|
||||||
|
// show the valid input area when touch scaling restricts it
|
||||||
|
if (TOUCH_SCALING != TOUCH_SCALE_DEFAULT) {
|
||||||
|
draw_list->AddRect(
|
||||||
|
ImVec2(left, top), ImVec2(right, bottom),
|
||||||
|
IM_COL32(255, 255, 255, 255), 0.f, 0, 2.f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// spread the usable X sensors 2..45 from edge to edge
|
||||||
|
for (int sensor = X_SENSOR_FIRST_ACTIVE; sensor <= X_SENSOR_LAST_ACTIVE; sensor++) {
|
||||||
|
if (!packet_bit_active(state.packet, X_SENSOR_FIRST_BIT + sensor)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float position = sensor_span_position(
|
||||||
|
sensor - X_SENSOR_FIRST_ACTIVE, X_SENSOR_ACTIVE_COUNT,
|
||||||
|
state.is_landscape ? height : width);
|
||||||
|
if (state.is_landscape) {
|
||||||
|
float y = top + position * scale_factor;
|
||||||
|
draw_line(left, y, right, y);
|
||||||
|
} else {
|
||||||
|
float x = left + position * scale_factor;
|
||||||
|
draw_line(x, top, x, bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int sensor = 0; sensor < Y_SENSOR_COUNT; sensor++) {
|
||||||
|
if (!packet_bit_active(state.packet, Y_SENSOR_FIRST_BIT - sensor)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int position = sensor_center(
|
||||||
|
sensor, Y_SENSOR_COUNT,
|
||||||
|
state.is_landscape ? width : height);
|
||||||
|
if (state.is_landscape) {
|
||||||
|
float x = right - position * scale_factor;
|
||||||
|
draw_line(x, top, x, bottom);
|
||||||
|
} else {
|
||||||
|
float y = top + position * scale_factor;
|
||||||
|
draw_line(left, y, right, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_debug_attach() {
|
||||||
|
clear_touch_debug_state();
|
||||||
|
TOUCH_ACTIVE.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_debug_detach() {
|
||||||
|
TOUCH_ACTIVE.store(false, std::memory_order_release);
|
||||||
|
clear_touch_debug_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_debug_publish(const unsigned char *data, bool is_landscape) {
|
||||||
|
if (!TOUCH_DEBUG_OVERLAY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
|
||||||
|
memcpy(TOUCH_DEBUG_STATE.packet.data(), data, TOUCH_PACKET_SIZE);
|
||||||
|
TOUCH_DEBUG_STATE.is_landscape = is_landscape;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
namespace games::rb {
|
||||||
|
extern std::atomic_bool TOUCH_DEBUG_OVERLAY;
|
||||||
|
|
||||||
|
bool touch_debug_overlay_enabled();
|
||||||
|
void touch_draw_debug_overlay();
|
||||||
|
|
||||||
|
void touch_debug_attach();
|
||||||
|
void touch_debug_detach();
|
||||||
|
void touch_debug_publish(const unsigned char *data, bool is_landscape);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace games::rb {
|
||||||
|
inline constexpr int TOUCH_SCALE_DEFAULT = 1000;
|
||||||
|
inline constexpr int TOUCH_PACKET_SIZE = 20;
|
||||||
|
inline constexpr int TOUCH_PACKET_DATA_OFFSET = 3;
|
||||||
|
|
||||||
|
inline constexpr int X_SENSOR_COUNT = 48;
|
||||||
|
inline constexpr int X_SENSOR_FIRST_ACTIVE = 2;
|
||||||
|
inline constexpr int X_SENSOR_LAST_ACTIVE = 45;
|
||||||
|
inline constexpr int X_SENSOR_ACTIVE_COUNT =
|
||||||
|
X_SENSOR_LAST_ACTIVE - X_SENSOR_FIRST_ACTIVE + 1;
|
||||||
|
inline constexpr int X_SENSOR_FIRST_BIT = 88;
|
||||||
|
|
||||||
|
inline constexpr int Y_SENSOR_COUNT = 76;
|
||||||
|
inline constexpr int Y_SENSOR_FIRST_BIT = 75;
|
||||||
|
}
|
||||||
@@ -8,6 +8,9 @@
|
|||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "hooks/devicehook.h"
|
#include "hooks/devicehook.h"
|
||||||
#include "hooks/libraryhook.h"
|
#include "hooks/libraryhook.h"
|
||||||
|
#ifdef SPICE64
|
||||||
|
#include "hooks/graphics/nvapi_impl.h"
|
||||||
|
#endif
|
||||||
#include "hooks/graphics/nvapi_hook.h"
|
#include "hooks/graphics/nvapi_hook.h"
|
||||||
#include "hooks/powrprof.h"
|
#include "hooks/powrprof.h"
|
||||||
#include "hooks/sleephook.h"
|
#include "hooks/sleephook.h"
|
||||||
@@ -23,8 +26,8 @@
|
|||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
#include "util/sysutils.h"
|
#include "util/sysutils.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "misc/nativetouchhook.h"
|
|
||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
|
#include "touch/native/nativetouchhook.h"
|
||||||
#include "bi2x_hook.h"
|
#include "bi2x_hook.h"
|
||||||
#include "camera.h"
|
#include "camera.h"
|
||||||
#include "io.h"
|
#include "io.h"
|
||||||
@@ -46,7 +49,6 @@ namespace games::sdvx {
|
|||||||
const char *ORIGINAL_ASIO_DEVICE_NAME = "XONAR SOUND CARD(64)";
|
const char *ORIGINAL_ASIO_DEVICE_NAME = "XONAR SOUND CARD(64)";
|
||||||
|
|
||||||
// settings
|
// settings
|
||||||
bool NATIVETOUCH = false;
|
|
||||||
uint8_t DIGITAL_KNOB_SENS = 16;
|
uint8_t DIGITAL_KNOB_SENS = 16;
|
||||||
SdvxOverlayPosition OVERLAY_POS = SDVX_OVERLAY_BOTTOM;
|
SdvxOverlayPosition OVERLAY_POS = SDVX_OVERLAY_BOTTOM;
|
||||||
bool ENABLE_COM_PORT_SCAN_HOOK = false;
|
bool ENABLE_COM_PORT_SCAN_HOOK = false;
|
||||||
@@ -428,29 +430,42 @@ namespace games::sdvx {
|
|||||||
if (aio != nullptr) {
|
if (aio != nullptr) {
|
||||||
|
|
||||||
// enable 9on12 for AMD
|
// enable 9on12 for AMD
|
||||||
if (!libutils::try_library("nvapi64.dll")) {
|
const auto nvapi = libutils::try_library("nvapi64.dll");
|
||||||
|
if (nvapi == nullptr) {
|
||||||
log_info(
|
log_info(
|
||||||
"sdvx",
|
"sdvx",
|
||||||
"nvapi64.dll not found; for non-NVIDIA GPUs, requesting 9on12 to be enabled");
|
"nvapi64.dll not found; for non-NVIDIA GPUs, requesting 9on12 to be enabled");
|
||||||
GRAPHICS_9_ON_12_REQUESTED_BY_GAME = true;
|
GRAPHICS_9_ON_12_REQUESTED_BY_GAME = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nvapi_hook::BYPASS_NVAPI || nvapi == nullptr) {
|
||||||
|
const uint32_t main_refresh_hz = GRAPHICS_FORCE_REFRESH > 0 ?
|
||||||
|
GRAPHICS_FORCE_REFRESH : (is_valkyrie_model() ? 120 : 60);
|
||||||
|
const uint32_t sub_refresh_hz = GRAPHICS_FORCE_REFRESH_SUB.value_or(60);
|
||||||
|
if (!nvapi_impl::initialize(
|
||||||
|
avs::game::DLL_INSTANCE,
|
||||||
|
main_refresh_hz,
|
||||||
|
sub_refresh_hz)) {
|
||||||
|
log_warning("sdvx", "failed to initialize synthetic NVAPI");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// don't let nvapi mess with display settings
|
// don't let nvapi mess with display settings
|
||||||
nvapi_hook::initialize(avs::game::DLL_INSTANCE);
|
nvapi_hook::initialize(avs::game::DLL_INSTANCE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_valkyrie_model()) {
|
if (is_valkyrie_model()) {
|
||||||
if (NATIVETOUCH) {
|
const auto native_touch_ready = !wintouchemu::FORCE &&
|
||||||
nativetouchhook::hook(avs::game::DLL_INSTANCE);
|
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||||
} else if (!NATIVETOUCH && !GRAPHICS_WINDOWED) {
|
if (!native_touch_ready) {
|
||||||
// hook touch window
|
|
||||||
// in windowed mode, game can accept mouse input on the second screen
|
|
||||||
wintouchemu::FORCE = true;
|
wintouchemu::FORCE = true;
|
||||||
|
if (!GRAPHICS_WINDOWED) {
|
||||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||||
wintouchemu::hook_title_ends(
|
wintouchemu::hook_title_ends(
|
||||||
"SOUND VOLTEX",
|
"SOUND VOLTEX",
|
||||||
"Main Screen",
|
"Main Screen",
|
||||||
avs::game::DLL_INSTANCE);
|
avs::game::DLL_INSTANCE);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// insert BI2X hooks
|
// insert BI2X hooks
|
||||||
bi2x_hook_init();
|
bi2x_hook_init();
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ namespace games::sdvx {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// settings
|
// settings
|
||||||
extern bool NATIVETOUCH;
|
|
||||||
extern uint8_t DIGITAL_KNOB_SENS;
|
extern uint8_t DIGITAL_KNOB_SENS;
|
||||||
extern std::optional<std::string> ASIO_DRIVER;
|
extern std::optional<std::string> ASIO_DRIVER;
|
||||||
extern SdvxOverlayPosition OVERLAY_POS;
|
extern SdvxOverlayPosition OVERLAY_POS;
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#include "sdvx_live2d.h"
|
||||||
|
|
||||||
|
// only the Live2D-capable SDVX versions are 64-bit, so the whole feature is
|
||||||
|
// compiled out of 32-bit builds.
|
||||||
|
#ifdef SPICE64
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "launcher/logger.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
namespace games::sdvx {
|
||||||
|
|
||||||
|
// Live2D in-game scene detection (for the -sdvxnolive2d "ingame" option).
|
||||||
|
//
|
||||||
|
// the game logs scene transitions as "I:Attach: in <SCENE>" / "I:Detach: in
|
||||||
|
// <SCENE>". several scenes correspond to in-song gameplay (with the heavy
|
||||||
|
// Live2D rendering); we watch those log lines and keep the shared flag
|
||||||
|
// the d3d9 backend reads up to date. the hook never alters the log output
|
||||||
|
// (always returns false).
|
||||||
|
static bool live2d_scene_log_hook(
|
||||||
|
void *user, const std::string &data, logger::Style style, std::string &out) {
|
||||||
|
// any of these scenes counts as in-song gameplay (different play modes)
|
||||||
|
static const char *const gameplay_scenes[] = {
|
||||||
|
"in ALTERNATIVE_GAME_SCENE",
|
||||||
|
"in MEGAMIX_GAME_SCENE",
|
||||||
|
"in MEGAMIX_BATTLE",
|
||||||
|
"in BATTLE_GAME_SCENE",
|
||||||
|
"in AUTOMATION_GAME_SCENE",
|
||||||
|
"in ARENA_GAME_SCENE",
|
||||||
|
};
|
||||||
|
bool in_gameplay_scene = false;
|
||||||
|
for (const auto *scene : gameplay_scenes) {
|
||||||
|
if (data.find(scene) != std::string::npos) {
|
||||||
|
in_gameplay_scene = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!in_gameplay_scene) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// note: log messages here must NOT contain any matched scene token, else
|
||||||
|
// this hook would re-enter itself when the message is pushed.
|
||||||
|
if (data.find("I:Attach: in ") != std::string::npos) {
|
||||||
|
if (!GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(true, std::memory_order_relaxed)) {
|
||||||
|
log_info("sdvx", "Live2D skip: entering gameplay");
|
||||||
|
}
|
||||||
|
} else if (data.find("I:Detach: in ") != std::string::npos) {
|
||||||
|
if (GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(false, std::memory_order_relaxed)) {
|
||||||
|
log_info("sdvx", "Live2D skip: leaving gameplay");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void live2d_scene_detection_init() {
|
||||||
|
static bool installed = false;
|
||||||
|
if (installed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installed = true;
|
||||||
|
// the logger's hook list is a persistent static, so registering here is
|
||||||
|
// safe even though this runs before logger::start(). we intentionally do
|
||||||
|
// NOT log a confirmation now: at this point the log file isn't open yet
|
||||||
|
// and the message would be dropped. the entering/leaving-gameplay lines
|
||||||
|
// above provide runtime confirmation once the logger is running.
|
||||||
|
logger::hook_add(live2d_scene_log_hook, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // SPICE64
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace games::sdvx {
|
||||||
|
|
||||||
|
#ifdef SPICE64
|
||||||
|
// installs the Live2D in-game scene-detection log hook used by the
|
||||||
|
// -sdvxnolive2d "ingame" option. does not require the SDVX game module to
|
||||||
|
// be attached, so it can be enabled purely from the launcher option.
|
||||||
|
// only the Live2D-capable SDVX versions are 64-bit, so this is compiled out
|
||||||
|
// of 32-bit builds.
|
||||||
|
void live2d_scene_detection_init();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
}
|
||||||
@@ -32,19 +32,29 @@ namespace hooks::audio {
|
|||||||
RegEnumKeyA(hkEnum, index, key_name, sizeof(key_name)) == ERROR_SUCCESS;
|
RegEnumKeyA(hkEnum, index, key_name, sizeof(key_name)) == ERROR_SUCCESS;
|
||||||
index++) {
|
index++) {
|
||||||
|
|
||||||
// read description (display name), fall back to the key name
|
// read description (display name), fall back to the key name.
|
||||||
|
// RegOpenKeyExA + RegQueryValueExA is used instead of RegGetValueA
|
||||||
|
// because the latter is unavailable on Windows XP.
|
||||||
char desc[256] = { 0 };
|
char desc[256] = { 0 };
|
||||||
DWORD size = sizeof(desc);
|
DWORD size = sizeof(desc);
|
||||||
std::string name = key_name;
|
std::string name = key_name;
|
||||||
if (RegGetValueA(hkEnum,
|
HKEY hkDriver = nullptr;
|
||||||
key_name,
|
if (RegOpenKeyExA(hkEnum, key_name, 0,
|
||||||
|
KEY_QUERY_VALUE | wow64_flag, &hkDriver) == ERROR_SUCCESS) {
|
||||||
|
DWORD type = 0;
|
||||||
|
if (RegQueryValueExA(hkDriver,
|
||||||
ASIO_REG_DESC,
|
ASIO_REG_DESC,
|
||||||
RRF_RT_REG_SZ | wow64_flag,
|
|
||||||
nullptr,
|
nullptr,
|
||||||
desc,
|
&type,
|
||||||
&size) == ERROR_SUCCESS && desc[0]) {
|
reinterpret_cast<LPBYTE>(desc),
|
||||||
|
&size) == ERROR_SUCCESS
|
||||||
|
&& type == REG_SZ && desc[0]) {
|
||||||
|
// ensure null termination
|
||||||
|
desc[sizeof(desc) - 1] = '\0';
|
||||||
name = desc;
|
name = desc;
|
||||||
}
|
}
|
||||||
|
RegCloseKey(hkDriver);
|
||||||
|
}
|
||||||
|
|
||||||
// merge with an existing entry from the other view (match by name)
|
// merge with an existing entry from the other view (match by name)
|
||||||
const std::string name_lower = strtolower(name);
|
const std::string name_lower = strtolower(name);
|
||||||
|
|||||||
@@ -154,36 +154,30 @@ namespace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// live wrappers by CLSID. ASIO drivers are single-instance, so a host that creates a
|
// one wrapper per CLSID, kept alive for the process lifetime. ASIO drivers are
|
||||||
// new instance without releasing the old one has leaked it; we track this to tear the
|
// single-instance, and some hardware drivers (e.g. Neva Uno) crash if their COM object
|
||||||
// stale one down
|
// is destroyed and re-created within a process - which the host triggers by leaking and
|
||||||
|
// re-instantiating the driver during startup probing. so we build the real driver and
|
||||||
|
// its wrapper once, hold a reference so it survives the host's Release calls, and hand
|
||||||
|
// the same wrapper back for every later CoCreate. as a result the real driver is created
|
||||||
|
// and initialized exactly once
|
||||||
std::mutex g_wrappers_mutex;
|
std::mutex g_wrappers_mutex;
|
||||||
std::vector<std::pair<CLSID, WrappedAsio *>> g_wrappers;
|
std::vector<std::pair<CLSID, WrappedAsio *>> g_wrappers;
|
||||||
|
|
||||||
// register a wrapper as the live instance for its CLSID, returning any stale wrapper it
|
WrappedAsio *find_wrapper(REFCLSID clsid) {
|
||||||
// replaces so the caller can tear it down outside the lock
|
|
||||||
WrappedAsio *register_wrapper(REFCLSID clsid, WrappedAsio *wrapper) {
|
|
||||||
std::lock_guard lock(g_wrappers_mutex);
|
std::lock_guard lock(g_wrappers_mutex);
|
||||||
for (auto &entry : g_wrappers) {
|
for (auto &entry : g_wrappers) {
|
||||||
if (IsEqualCLSID(entry.first, clsid)) {
|
if (IsEqualCLSID(entry.first, clsid)) {
|
||||||
WrappedAsio *stale = entry.second;
|
return entry.second;
|
||||||
entry.second = wrapper;
|
|
||||||
return stale;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
g_wrappers.emplace_back(clsid, wrapper);
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void unregister_wrapper(WrappedAsio *wrapper) {
|
void store_wrapper(REFCLSID clsid, WrappedAsio *wrapper) {
|
||||||
std::lock_guard lock(g_wrappers_mutex);
|
std::lock_guard lock(g_wrappers_mutex);
|
||||||
for (auto it = g_wrappers.begin(); it != g_wrappers.end(); ++it) {
|
g_wrappers.emplace_back(clsid, wrapper);
|
||||||
if (it->second == wrapper) {
|
|
||||||
g_wrappers.erase(it);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ASIO drivers registered on this system (CLSID + registry name), scanned once
|
// ASIO drivers registered on this system (CLSID + registry name), scanned once
|
||||||
@@ -256,12 +250,11 @@ WrappedAsio::StereoDownmix WrappedAsio::name_to_stereo_downmix(const char *name)
|
|||||||
}
|
}
|
||||||
|
|
||||||
WrappedAsio::~WrappedAsio() {
|
WrappedAsio::~WrappedAsio() {
|
||||||
unregister_wrapper(this);
|
|
||||||
|
|
||||||
this->detach_post_process();
|
this->detach_post_process();
|
||||||
|
|
||||||
// our refcount is decoupled from the host's (see AddRef/Release), so pReal's count is
|
// never runs mid-run: wrap() pins the wrapper so the refcount stays above zero until
|
||||||
// exactly one here and this releases/unloads it deterministically
|
// release_all_wrappers() drops the pin at shutdown - the only point this can fire, and
|
||||||
|
// only once the host has released its own references. tears down the real driver
|
||||||
this->pReal->Release();
|
this->pReal->Release();
|
||||||
|
|
||||||
log_info("audio::wrappedasio", "destroying wrapped ASIO driver, clsid={}", guid2s(this->clsid));
|
log_info("audio::wrappedasio", "destroying wrapped ASIO driver, clsid={}", guid2s(this->clsid));
|
||||||
@@ -315,8 +308,17 @@ ULONG STDMETHODCALLTYPE WrappedAsio::Release() {
|
|||||||
|
|
||||||
#pragma region IAsio
|
#pragma region IAsio
|
||||||
AsioBool __thiscall WrappedAsio::init(void *sys_handle) {
|
AsioBool __thiscall WrappedAsio::init(void *sys_handle) {
|
||||||
|
// the real driver is single-instance and kept alive; initialize it exactly once. the
|
||||||
|
// host re-calls init() on each CoCreate during probing, but re-initializing a live
|
||||||
|
// driver crashes some hardware drivers, so once we have a live driver we report success
|
||||||
|
if (this->initialized) {
|
||||||
|
log_misc("audio::wrappedasio", "init skipped, '{}' already initialized", this->driver_name);
|
||||||
|
return AsioTrue;
|
||||||
|
}
|
||||||
|
|
||||||
const AsioBool result = this->pReal->init(sys_handle);
|
const AsioBool result = this->pReal->init(sys_handle);
|
||||||
if (result == AsioTrue) {
|
if (result == AsioTrue) {
|
||||||
|
this->initialized = true;
|
||||||
log_info(
|
log_info(
|
||||||
"audio::wrappedasio",
|
"audio::wrappedasio",
|
||||||
"init succeeded for '{}' (driver version {})",
|
"init succeeded for '{}' (driver version {})",
|
||||||
@@ -346,6 +348,7 @@ void __thiscall WrappedAsio::get_error_message(char *string) {
|
|||||||
AsioError __thiscall WrappedAsio::start() {
|
AsioError __thiscall WrappedAsio::start() {
|
||||||
const AsioError result = this->pReal->start();
|
const AsioError result = this->pReal->start();
|
||||||
if (result == ASE_OK) {
|
if (result == ASE_OK) {
|
||||||
|
this->started = true;
|
||||||
log_info(
|
log_info(
|
||||||
"audio::wrappedasio",
|
"audio::wrappedasio",
|
||||||
"start succeeded, ASIO stream is now running on '{}'",
|
"start succeeded, ASIO stream is now running on '{}'",
|
||||||
@@ -360,6 +363,7 @@ AsioError __thiscall WrappedAsio::start() {
|
|||||||
AsioError __thiscall WrappedAsio::stop() {
|
AsioError __thiscall WrappedAsio::stop() {
|
||||||
const AsioError result = this->pReal->stop();
|
const AsioError result = this->pReal->stop();
|
||||||
if (result == ASE_OK) {
|
if (result == ASE_OK) {
|
||||||
|
this->started = false;
|
||||||
log_info("audio::wrappedasio", "stop succeeded, ASIO stream on '{}' halted", this->driver_name);
|
log_info("audio::wrappedasio", "stop succeeded, ASIO stream on '{}' halted", this->driver_name);
|
||||||
} else {
|
} else {
|
||||||
log_warning("audio::wrappedasio", "stop failed, err={}", static_cast<long>(result));
|
log_warning("audio::wrappedasio", "stop failed, err={}", static_cast<long>(result));
|
||||||
@@ -740,6 +744,27 @@ void WrappedAsio::detach_post_process() {
|
|||||||
WrappedAsio::active_instance.compare_exchange_strong(expected, nullptr);
|
WrappedAsio::active_instance.compare_exchange_strong(expected, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WrappedAsio::quiesce_for_reuse() {
|
||||||
|
// a previous abandoned probing cycle may have left a running stream and live buffers on
|
||||||
|
// the real driver without calling stop/dispose; tear that down now (without destroying
|
||||||
|
// the driver) so the host's next create_buffers starts clean. stop() guarantees no
|
||||||
|
// further buffer_switch, and dispose_buffers detaches our trampolines
|
||||||
|
if (this->started) {
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"reuse: stopping leftover stream on '{}' before handing the driver back",
|
||||||
|
this->driver_name);
|
||||||
|
this->stop();
|
||||||
|
}
|
||||||
|
if (this->buffers_created) {
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"reuse: disposing leftover buffers on '{}' before handing the driver back",
|
||||||
|
this->driver_name);
|
||||||
|
this->dispose_buffers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void WrappedAsio::apply_output_volume(long double_buffer_index) {
|
void WrappedAsio::apply_output_volume(long double_buffer_index) {
|
||||||
if (double_buffer_index != 0 && double_buffer_index != 1) {
|
if (double_buffer_index != 0 && double_buffer_index != 1) {
|
||||||
return;
|
return;
|
||||||
@@ -838,6 +863,7 @@ AsioError __thiscall WrappedAsio::create_buffers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this->publish_post_process(buffer_size);
|
this->publish_post_process(buffer_size);
|
||||||
|
this->buffers_created = true;
|
||||||
} else {
|
} else {
|
||||||
log_warning(
|
log_warning(
|
||||||
"audio::wrappedasio",
|
"audio::wrappedasio",
|
||||||
@@ -940,6 +966,7 @@ AsioError WrappedAsio::create_buffers_front_pair(
|
|||||||
// device's 2.0 output. then publish ourselves to the realtime thread
|
// device's 2.0 output. then publish ourselves to the realtime thread
|
||||||
this->record_downmix_channels(buffer_infos, num_channels, buffer_size);
|
this->record_downmix_channels(buffer_infos, num_channels, buffer_size);
|
||||||
this->publish_post_process(buffer_size);
|
this->publish_post_process(buffer_size);
|
||||||
|
this->buffers_created = true;
|
||||||
|
|
||||||
log_info(
|
log_info(
|
||||||
"audio::wrappedasio",
|
"audio::wrappedasio",
|
||||||
@@ -962,6 +989,7 @@ AsioError __thiscall WrappedAsio::dispose_buffers() {
|
|||||||
this->volume_channels.clear();
|
this->volume_channels.clear();
|
||||||
this->volume_active = false;
|
this->volume_active = false;
|
||||||
this->downmix_active = false;
|
this->downmix_active = false;
|
||||||
|
this->buffers_created = false;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -986,29 +1014,54 @@ namespace hooks::audio::asio {
|
|||||||
auto *wrapper = new WrappedAsio(
|
auto *wrapper = new WrappedAsio(
|
||||||
reinterpret_cast<IAsio *>(real), clsid, registered_asio_name(clsid));
|
reinterpret_cast<IAsio *>(real), clsid, registered_asio_name(clsid));
|
||||||
|
|
||||||
// if the host already had a live wrapper for this CLSID it leaked the previous
|
// pin the wrapper (and the real driver it owns) for the process lifetime; later
|
||||||
// instance (ASIO is single-instance); tear it down now so the real driver is
|
// CoCreate calls reuse this same instance via wrap_existing
|
||||||
// released before the reinit. works around games (iidx32+) that ref twice but
|
wrapper->AddRef();
|
||||||
// deref once before re-initializing.
|
store_wrapper(clsid, wrapper);
|
||||||
//
|
|
||||||
// FlexASIO 1.9 and many DAC ASIO drivers can't handle this; FlexASIO 1.10 and
|
|
||||||
// Xonar AE can
|
|
||||||
if (WrappedAsio *stale = register_wrapper(clsid, wrapper)) {
|
|
||||||
log_info(
|
|
||||||
"audio::wrappedasio",
|
|
||||||
"host did not release previous instance for clsid={}, forcing teardown",
|
|
||||||
guid2s(clsid));
|
|
||||||
|
|
||||||
// the stale instance may still have a running stream on the driver's realtime
|
|
||||||
// thread. stop and dispose it before deleting so no in-flight buffer switch
|
|
||||||
// (e.g. our volume trampoline) touches buffers we are about to free. ASIO
|
|
||||||
// guarantees no further buffer_switch once stop() returns, and dispose_buffers
|
|
||||||
// detaches our trampolines, fully quiescing the realtime path before delete
|
|
||||||
stale->stop();
|
|
||||||
stale->dispose_buffers();
|
|
||||||
delete stale;
|
|
||||||
}
|
|
||||||
|
|
||||||
return static_cast<IAsio *>(wrapper);
|
return static_cast<IAsio *>(wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IUnknown *wrap_existing(REFCLSID clsid) {
|
||||||
|
WrappedAsio *wrapper = find_wrapper(clsid);
|
||||||
|
if (wrapper == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_misc(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"reusing cached ASIO driver instance, clsid={}",
|
||||||
|
guid2s(clsid));
|
||||||
|
|
||||||
|
wrapper->quiesce_for_reuse();
|
||||||
|
|
||||||
|
// hand the host another reference to the one instance we keep alive
|
||||||
|
wrapper->AddRef();
|
||||||
|
return static_cast<IAsio *>(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
void release_all_wrappers() {
|
||||||
|
// collect the pinned wrappers under the lock, then drop our references outside it so
|
||||||
|
// a final Release (which can run the driver's own teardown) never happens while the
|
||||||
|
// lock is held
|
||||||
|
std::vector<std::pair<CLSID, WrappedAsio *>> wrappers;
|
||||||
|
{
|
||||||
|
std::lock_guard lock(g_wrappers_mutex);
|
||||||
|
wrappers = g_wrappers;
|
||||||
|
g_wrappers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto &entry : wrappers) {
|
||||||
|
// drop only the pin wrap() took. if the host has already released its own
|
||||||
|
// references (the normal case at shutdown) this destroys the wrapper and releases
|
||||||
|
// the real driver; if the host is still using it, the refcount stays above zero
|
||||||
|
// and we leave its live stream/buffers untouched
|
||||||
|
log_misc(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"releasing cached ASIO driver instance, clsid={}",
|
||||||
|
guid2s(entry.first));
|
||||||
|
entry.second->Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,20 @@ namespace hooks::audio::asio {
|
|||||||
bool is_asio_creation(REFCLSID rclsid, REFIID riid);
|
bool is_asio_creation(REFCLSID rclsid, REFIID riid);
|
||||||
|
|
||||||
// wrap a real ASIO driver instance, taking ownership of the supplied reference, and
|
// wrap a real ASIO driver instance, taking ownership of the supplied reference, and
|
||||||
// return a proxy that forwards every call to it
|
// return a proxy that forwards every call to it. also records it as the cached
|
||||||
|
// instance for its CLSID so later CoCreate calls can reuse it (see wrap_existing)
|
||||||
IUnknown *wrap(REFCLSID clsid, void *real);
|
IUnknown *wrap(REFCLSID clsid, void *real);
|
||||||
|
|
||||||
|
// if a cached wrapper already exists for this CLSID, return it (with an added
|
||||||
|
// reference); otherwise nullptr to signal the caller to create the real driver and
|
||||||
|
// wrap it. lets the host reuse one driver instance instead of re-instantiating it
|
||||||
|
IUnknown *wrap_existing(REFCLSID clsid);
|
||||||
|
|
||||||
|
// drop the process-lifetime references taken by wrap() so cached drivers can be released
|
||||||
|
// at shutdown. only relinquishes our pin, so a real driver is torn down once the host
|
||||||
|
// has released its own references too. call from a controlled shutdown point, never from
|
||||||
|
// a static destructor (the driver DLL may already be unloaded)
|
||||||
|
void release_all_wrappers();
|
||||||
}
|
}
|
||||||
|
|
||||||
// transparent proxy around a real ASIO driver; a single place to intercept ASIO traffic
|
// transparent proxy around a real ASIO driver; a single place to intercept ASIO traffic
|
||||||
@@ -103,6 +115,10 @@ struct WrappedAsio final : IAsio {
|
|||||||
AsioError __thiscall output_ready() override;
|
AsioError __thiscall output_ready() override;
|
||||||
#pragma endregion
|
#pragma endregion
|
||||||
|
|
||||||
|
// quiesces any leftover stream/buffer state before the cached wrapper is handed back
|
||||||
|
// for reuse, without destroying the real driver (see wrap_existing)
|
||||||
|
void quiesce_for_reuse();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// create_buffers implementation used when a stereo extraction is active: forwards only
|
// create_buffers implementation used when a stereo extraction is active: forwards only
|
||||||
// the channels the real device has and hands the game throwaway buffers for the rest
|
// the channels the real device has and hands the game throwaway buffers for the rest
|
||||||
@@ -170,6 +186,14 @@ private:
|
|||||||
// unambiguous name; constant for our lifetime
|
// unambiguous name; constant for our lifetime
|
||||||
std::string driver_name;
|
std::string driver_name;
|
||||||
|
|
||||||
|
// the real driver is initialized exactly once; repeat init() calls are a no-op success
|
||||||
|
bool initialized = false;
|
||||||
|
|
||||||
|
// whether the real driver currently has a buffer set / running stream. used to quiesce
|
||||||
|
// leftover state when the cached wrapper is reused (see quiesce_for_reuse)
|
||||||
|
bool buffers_created = false;
|
||||||
|
bool started = false;
|
||||||
|
|
||||||
// our own reference count; we hold one reference on pReal and release it when this
|
// our own reference count; we hold one reference on pReal and release it when this
|
||||||
// drops to zero
|
// drops to zero
|
||||||
std::atomic<ULONG> ref_count {1};
|
std::atomic<ULONG> ref_count {1};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <audioclient.h>
|
#include <audioclient.h>
|
||||||
#include <mmdeviceapi.h>
|
#include <mmdeviceapi.h>
|
||||||
|
|
||||||
|
#include "avs/game.h"
|
||||||
#include "hooks/audio/backends/mmdevice/device_enumerator.h"
|
#include "hooks/audio/backends/mmdevice/device_enumerator.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
#include "audio_private.h"
|
#include "audio_private.h"
|
||||||
#include "acm.h"
|
#include "acm.h"
|
||||||
#include "asio_proxy.h"
|
#include "asio_proxy.h"
|
||||||
|
#include "xact.h"
|
||||||
|
|
||||||
#ifdef _MSC_VER
|
#ifdef _MSC_VER
|
||||||
DEFINE_GUID(CLSID_MMDeviceEnumerator,
|
DEFINE_GUID(CLSID_MMDeviceEnumerator,
|
||||||
@@ -53,9 +55,39 @@ namespace hooks::audio {
|
|||||||
std::string ASIO_DRIVER_NAME = "";
|
std::string ASIO_DRIVER_NAME = "";
|
||||||
bool ASIO_FORCE_UNLOAD_ON_STOP = false;
|
bool ASIO_FORCE_UNLOAD_ON_STOP = false;
|
||||||
|
|
||||||
// private globals
|
|
||||||
IAudioClient *CLIENT = nullptr;
|
|
||||||
std::mutex INITIALIZE_LOCK; // for asio
|
std::mutex INITIALIZE_LOCK; // for asio
|
||||||
|
|
||||||
|
IAudioClient *CLIENT = nullptr;
|
||||||
|
static std::mutex CLIENT_LOCK;
|
||||||
|
static bool CLIENT_STOPPING = false;
|
||||||
|
|
||||||
|
void set_active_client(IAudioClient *client, const char *source) {
|
||||||
|
IAudioClient *previous_client = nullptr;
|
||||||
|
{
|
||||||
|
std::lock_guard lock(CLIENT_LOCK);
|
||||||
|
|
||||||
|
if (CLIENT_STOPPING || CLIENT == client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (client) {
|
||||||
|
client->AddRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
previous_client = CLIENT;
|
||||||
|
CLIENT = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous_client) {
|
||||||
|
previous_client->Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client) {
|
||||||
|
log_info(
|
||||||
|
"audio",
|
||||||
|
"active client selected by {} after successful initialization",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
|
static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
|
||||||
@@ -68,6 +100,15 @@ static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
|
|||||||
|
|
||||||
HRESULT ret;
|
HRESULT ret;
|
||||||
|
|
||||||
|
// ASIO: if we already hold a cached instance for this CLSID, hand back a fresh
|
||||||
|
// wrapper over it instead of re-creating the real driver (see asio_proxy.cpp)
|
||||||
|
if (ppv != nullptr && hooks::audio::asio::is_asio_creation(rclsid, riid)) {
|
||||||
|
if (IUnknown *reused = hooks::audio::asio::wrap_existing(rclsid)) {
|
||||||
|
*ppv = reused;
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// call original
|
// call original
|
||||||
ret = CoCreateInstance_orig(rclsid, pUnkOuter, dwClsContext, riid, ppv);
|
ret = CoCreateInstance_orig(rclsid, pUnkOuter, dwClsContext, riid, ppv);
|
||||||
if (FAILED(ret)) {
|
if (FAILED(ret)) {
|
||||||
@@ -120,15 +161,28 @@ namespace hooks::audio {
|
|||||||
|
|
||||||
// general hooks
|
// general hooks
|
||||||
CoCreateInstance_orig = detour::iat_try("CoCreateInstance", CoCreateInstance_hook);
|
CoCreateInstance_orig = detour::iat_try("CoCreateInstance", CoCreateInstance_hook);
|
||||||
|
if (avs::game::is_model("PAN")) {
|
||||||
|
hooks::audio::xact::init();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
log_info("audio", "stopping");
|
log_info("audio", "stopping");
|
||||||
if (CLIENT) {
|
IAudioClient *client = nullptr;
|
||||||
CLIENT->Stop();
|
{
|
||||||
CLIENT->Release();
|
std::lock_guard lock(CLIENT_LOCK);
|
||||||
|
CLIENT_STOPPING = true;
|
||||||
|
client = CLIENT;
|
||||||
CLIENT = nullptr;
|
CLIENT = nullptr;
|
||||||
}
|
}
|
||||||
|
if (client) {
|
||||||
|
client->Stop();
|
||||||
|
client->Release();
|
||||||
|
}
|
||||||
stop_low_latency();
|
stop_low_latency();
|
||||||
|
|
||||||
|
// release the ASIO drivers we kept pinned for the process lifetime now that we are
|
||||||
|
// at a controlled shutdown point (see asio_proxy.cpp)
|
||||||
|
hooks::audio::asio::release_all_wrappers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,4 +14,6 @@ namespace hooks::audio {
|
|||||||
extern std::mutex INITIALIZE_LOCK;
|
extern std::mutex INITIALIZE_LOCK;
|
||||||
extern bool VOLUME_HOOK_ENABLED;
|
extern bool VOLUME_HOOK_ENABLED;
|
||||||
extern LowLatencyAudioClient *LOW_LATENCY_CLIENT;
|
extern LowLatencyAudioClient *LOW_LATENCY_CLIENT;
|
||||||
|
|
||||||
|
void set_active_client(IAudioClient *client, const char *source);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,11 +95,6 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDevice::Activate(
|
|||||||
}
|
}
|
||||||
std::lock_guard initialize_guard(hooks::audio::INITIALIZE_LOCK, std::adopt_lock);
|
std::lock_guard initialize_guard(hooks::audio::INITIALIZE_LOCK, std::adopt_lock);
|
||||||
|
|
||||||
// release old audio client if initialized
|
|
||||||
if (hooks::audio::CLIENT) {
|
|
||||||
hooks::audio::CLIENT->Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
IAudioClient *client = nullptr;
|
IAudioClient *client = nullptr;
|
||||||
if (iid == IID_IAudioClient) {
|
if (iid == IID_IAudioClient) {
|
||||||
client = wrap_audio_client(reinterpret_cast<IAudioClient *>(*ppInterface));
|
client = wrap_audio_client(reinterpret_cast<IAudioClient *>(*ppInterface));
|
||||||
@@ -107,9 +102,6 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDevice::Activate(
|
|||||||
client = wrap_audio_client3(reinterpret_cast<IAudioClient3 *>(*ppInterface));
|
client = wrap_audio_client3(reinterpret_cast<IAudioClient3 *>(*ppInterface));
|
||||||
}
|
}
|
||||||
*ppInterface = client;
|
*ppInterface = client;
|
||||||
// persist the audio client
|
|
||||||
hooks::audio::CLIENT = client;
|
|
||||||
hooks::audio::CLIENT->AddRef();
|
|
||||||
|
|
||||||
} else if (iid == __uuidof(IAudioEndpointVolume) && hooks::audio::VOLUME_HOOK_ENABLED) {
|
} else if (iid == __uuidof(IAudioEndpointVolume) && hooks::audio::VOLUME_HOOK_ENABLED) {
|
||||||
*ppInterface = new WrappedIAudioEndpointVolume(reinterpret_cast<IAudioEndpointVolume *>(*ppInterface));
|
*ppInterface = new WrappedIAudioEndpointVolume(reinterpret_cast<IAudioEndpointVolume *>(*ppInterface));
|
||||||
|
|||||||
@@ -153,19 +153,9 @@ HRESULT STDMETHODCALLTYPE NullMMDevice::Activate(
|
|||||||
log_info("audio::null", "NullMMDevice::Activate {}", guid2s(iid));
|
log_info("audio::null", "NullMMDevice::Activate {}", guid2s(iid));
|
||||||
|
|
||||||
if (iid == IID_IAudioClient) {
|
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()));
|
auto *client = static_cast<IAudioClient *>(new DummyIAudioClient(new NullDiscardBackend()));
|
||||||
*ppInterface = client;
|
*ppInterface = client;
|
||||||
|
|
||||||
// persist the audio client
|
|
||||||
hooks::audio::CLIENT = client;
|
|
||||||
hooks::audio::CLIENT->AddRef();
|
|
||||||
|
|
||||||
return S_OK;
|
return S_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
#include "hooks/audio/util.h"
|
#include "hooks/audio/util.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/precise_timer.h"
|
|
||||||
|
|
||||||
NullDiscardBackend::~NullDiscardBackend() {
|
NullDiscardBackend::~NullDiscardBackend() {
|
||||||
this->running = false;
|
this->running = false;
|
||||||
@@ -125,8 +125,6 @@ HRESULT NullDiscardBackend::on_release_buffer(uint32_t, DWORD) {
|
|||||||
void NullDiscardBackend::pace_loop() {
|
void NullDiscardBackend::pace_loop() {
|
||||||
using namespace std::chrono;
|
using namespace std::chrono;
|
||||||
|
|
||||||
timeutils::PreciseSleepTimer timer;
|
|
||||||
|
|
||||||
// audio is discarded, so timing precision and drift do not matter; just wake the
|
// 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.
|
// game once per buffer period to keep its render thread from blocking on the event.
|
||||||
const auto period = duration_cast<steady_clock::duration>(
|
const auto period = duration_cast<steady_clock::duration>(
|
||||||
@@ -136,6 +134,6 @@ void NullDiscardBackend::pace_loop() {
|
|||||||
if (this->relay_handle) {
|
if (this->relay_handle) {
|
||||||
SetEvent(this->relay_handle);
|
SetEvent(this->relay_handle);
|
||||||
}
|
}
|
||||||
timer.sleep(period);
|
std::this_thread::sleep_for(period);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
|
|||||||
device_format->nChannels * (device_format->wBitsPerSample / 8));
|
device_format->nChannels * (device_format->wBitsPerSample / 8));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hooks::audio::set_active_client(this, "WrappedIAudioClient::Initialize");
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetBufferSize(UINT32 *pNumBufferFrames) {
|
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetBufferSize(UINT32 *pNumBufferFrames) {
|
||||||
@@ -651,5 +652,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);
|
copy_wave_format(&this->device_format, pFormat);
|
||||||
|
hooks::audio::set_active_client(this, "WrappedIAudioClient::InitializeSharedAudioStream");
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,13 +75,17 @@ HRESULT STDMETHODCALLTYPE DummyIAudioClient::Initialize(
|
|||||||
log_info("audio::wasapi", "IAudioClient::Initialize hook hit");
|
log_info("audio::wasapi", "IAudioClient::Initialize hook hit");
|
||||||
print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, pFormat);
|
print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, pFormat);
|
||||||
|
|
||||||
CHECK_RESULT(this->backend->on_initialize(
|
HRESULT ret = this->backend->on_initialize(
|
||||||
&ShareMode,
|
&ShareMode,
|
||||||
&StreamFlags,
|
&StreamFlags,
|
||||||
&hnsBufferDuration,
|
&hnsBufferDuration,
|
||||||
&hnsPeriodicity,
|
&hnsPeriodicity,
|
||||||
pFormat,
|
pFormat,
|
||||||
AudioSessionGuid));
|
AudioSessionGuid);
|
||||||
|
if (SUCCEEDED(ret)) {
|
||||||
|
hooks::audio::set_active_client(this, "DummyIAudioClient::Initialize");
|
||||||
|
}
|
||||||
|
CHECK_RESULT(ret);
|
||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE DummyIAudioClient::GetBufferSize(UINT32 *pNumBufferFrames) {
|
HRESULT STDMETHODCALLTYPE DummyIAudioClient::GetBufferSize(UINT32 *pNumBufferFrames) {
|
||||||
static std::once_flag printed;
|
static std::once_flag printed;
|
||||||
|
|||||||
@@ -0,0 +1,393 @@
|
|||||||
|
#include "xact.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <initguid.h>
|
||||||
|
#include <mmreg.h>
|
||||||
|
#include <objbase.h>
|
||||||
|
|
||||||
|
#include "util/deferlog.h"
|
||||||
|
#include "util/detour.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
#include "util/utils.h"
|
||||||
|
|
||||||
|
namespace hooks::audio::xact {
|
||||||
|
|
||||||
|
// XAudio 2.7 is a COM API. Newer Windows SDKs expose a different IXAudio2
|
||||||
|
// layout, so keep this proxy pinned to the legacy ABI used by libxact.
|
||||||
|
struct XAudio2DeviceDetails {
|
||||||
|
WCHAR device_id[256];
|
||||||
|
WCHAR display_name[256];
|
||||||
|
DWORD role;
|
||||||
|
WAVEFORMATEXTENSIBLE output_format;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct XAudio2EffectChain {
|
||||||
|
UINT32 effect_count;
|
||||||
|
const void *effect_descriptors;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct IXAudio2_27 {
|
||||||
|
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **object) = 0;
|
||||||
|
virtual ULONG STDMETHODCALLTYPE AddRef() = 0;
|
||||||
|
virtual ULONG STDMETHODCALLTYPE Release() = 0;
|
||||||
|
virtual HRESULT STDMETHODCALLTYPE GetDeviceCount(UINT32 *device_count) = 0;
|
||||||
|
virtual HRESULT STDMETHODCALLTYPE GetDeviceDetails(
|
||||||
|
UINT32 device_index,
|
||||||
|
XAudio2DeviceDetails *device_details) = 0;
|
||||||
|
virtual HRESULT STDMETHODCALLTYPE Initialize(UINT32 flags, UINT32 processor) = 0;
|
||||||
|
virtual HRESULT STDMETHODCALLTYPE RegisterForCallbacks(void *callback) = 0;
|
||||||
|
virtual void STDMETHODCALLTYPE UnregisterForCallbacks(void *callback) = 0;
|
||||||
|
virtual HRESULT STDMETHODCALLTYPE CreateSourceVoice(
|
||||||
|
void **source_voice,
|
||||||
|
const WAVEFORMATEX *source_format,
|
||||||
|
UINT32 flags,
|
||||||
|
float max_frequency_ratio,
|
||||||
|
void *callback,
|
||||||
|
const void *send_list,
|
||||||
|
const XAudio2EffectChain *effect_chain) = 0;
|
||||||
|
virtual HRESULT STDMETHODCALLTYPE CreateSubmixVoice(
|
||||||
|
void **submix_voice,
|
||||||
|
UINT32 input_channels,
|
||||||
|
UINT32 input_sample_rate,
|
||||||
|
UINT32 flags,
|
||||||
|
UINT32 processing_stage,
|
||||||
|
const void *send_list,
|
||||||
|
const XAudio2EffectChain *effect_chain) = 0;
|
||||||
|
virtual HRESULT STDMETHODCALLTYPE CreateMasteringVoice(
|
||||||
|
void **mastering_voice,
|
||||||
|
UINT32 input_channels,
|
||||||
|
UINT32 input_sample_rate,
|
||||||
|
UINT32 flags,
|
||||||
|
UINT32 device_index,
|
||||||
|
const XAudio2EffectChain *effect_chain) = 0;
|
||||||
|
virtual HRESULT STDMETHODCALLTYPE StartEngine() = 0;
|
||||||
|
virtual void STDMETHODCALLTYPE StopEngine() = 0;
|
||||||
|
virtual HRESULT STDMETHODCALLTYPE CommitChanges(UINT32 operation_set) = 0;
|
||||||
|
virtual void STDMETHODCALLTYPE GetPerformanceData(void *performance_data) = 0;
|
||||||
|
virtual void STDMETHODCALLTYPE SetDebugConfiguration(
|
||||||
|
const void *debug_configuration,
|
||||||
|
void *reserved) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// XAudio2 2.7 COM class and interface.
|
||||||
|
DEFINE_GUID(CLSID_XAudio2_7_LEGACY,
|
||||||
|
0x5a508685, 0xa254, 0x4fba,
|
||||||
|
0x9b, 0x82, 0x9a, 0x24, 0xb0, 0x03, 0x06, 0xaf);
|
||||||
|
DEFINE_GUID(IID_IXAudio2_7_LEGACY,
|
||||||
|
0x8bcf1f58, 0x9fe7, 0x4583,
|
||||||
|
0x8a, 0xc6, 0xe2, 0xad, 0xc4, 0x65, 0xc8, 0xbb);
|
||||||
|
|
||||||
|
static decltype(CoCreateInstance) *CoCreateInstance_orig = nullptr;
|
||||||
|
|
||||||
|
using CreateFX_t = HRESULT (WINAPI *)(REFCLSID, IUnknown **, const void *, UINT32);
|
||||||
|
static CreateFX_t CreateFX_orig = nullptr;
|
||||||
|
|
||||||
|
static std::string describe_wave_format(const WAVEFORMATEX *format) {
|
||||||
|
if (format == nullptr) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD channel_mask = 0;
|
||||||
|
if (format->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
|
||||||
|
format->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
|
||||||
|
channel_mask = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(format)->dwChannelMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt::format(
|
||||||
|
"tag=0x{:04x}, channels={}, rate={} Hz, bits={}, valid_block={} B, avg={} B/s, mask=0x{:08x}",
|
||||||
|
format->wFormatTag,
|
||||||
|
format->nChannels,
|
||||||
|
format->nSamplesPerSec,
|
||||||
|
format->wBitsPerSample,
|
||||||
|
format->nBlockAlign,
|
||||||
|
format->nAvgBytesPerSec,
|
||||||
|
channel_mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <size_t Size>
|
||||||
|
static std::string narrow_fixed(const WCHAR (&value)[Size]) {
|
||||||
|
size_t length = 0;
|
||||||
|
while (length < Size && value[length] != L'\0') {
|
||||||
|
length++;
|
||||||
|
}
|
||||||
|
return ws2s(std::wstring(value, length));
|
||||||
|
}
|
||||||
|
|
||||||
|
class WrappedXAudio2 final : public IXAudio2_27 {
|
||||||
|
public:
|
||||||
|
explicit WrappedXAudio2(IXAudio2_27 *real) : real(real) {
|
||||||
|
log_info("audio::xaudio2", "wrapping IXAudio2 2.7 engine {}", static_cast<void *>(real));
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **object) override {
|
||||||
|
if (object == nullptr) {
|
||||||
|
return E_POINTER;
|
||||||
|
}
|
||||||
|
if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IXAudio2_7_LEGACY)) {
|
||||||
|
*object = this;
|
||||||
|
AddRef();
|
||||||
|
log_info("audio::xaudio2", "IXAudio2::QueryInterface({}) -> proxy", guid2s(riid));
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto result = real->QueryInterface(riid, object);
|
||||||
|
log_info(
|
||||||
|
"audio::xaudio2",
|
||||||
|
"IXAudio2::QueryInterface({}) -> {}, object={}",
|
||||||
|
guid2s(riid),
|
||||||
|
FMT_HRESULT(result),
|
||||||
|
object != nullptr ? *object : nullptr);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
ULONG STDMETHODCALLTYPE AddRef() override {
|
||||||
|
return ++ref_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
ULONG STDMETHODCALLTYPE Release() override {
|
||||||
|
const auto remaining = --ref_count;
|
||||||
|
if (remaining == 0) {
|
||||||
|
log_info("audio::xaudio2", "destroying IXAudio2 2.7 proxy");
|
||||||
|
real->Release();
|
||||||
|
delete this;
|
||||||
|
}
|
||||||
|
return remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE GetDeviceCount(UINT32 *device_count) override {
|
||||||
|
const auto result = real->GetDeviceCount(device_count);
|
||||||
|
log_info(
|
||||||
|
"audio::xaudio2",
|
||||||
|
"IXAudio2::GetDeviceCount -> {}, count={}",
|
||||||
|
FMT_HRESULT(result),
|
||||||
|
SUCCEEDED(result) && device_count != nullptr ? *device_count : 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE GetDeviceDetails(
|
||||||
|
UINT32 device_index,
|
||||||
|
XAudio2DeviceDetails *device_details) override {
|
||||||
|
const auto result = real->GetDeviceDetails(device_index, device_details);
|
||||||
|
if (SUCCEEDED(result) && device_details != nullptr) {
|
||||||
|
const auto device_name = narrow_fixed(device_details->display_name);
|
||||||
|
if (!device_details_logged.exchange(true, std::memory_order_relaxed)) {
|
||||||
|
log_info(
|
||||||
|
"audio::xaudio2",
|
||||||
|
"IXAudio2::GetDeviceDetails({}) -> {}, id='{}', name='{}', role=0x{:08x}, {}",
|
||||||
|
device_index,
|
||||||
|
FMT_HRESULT(result),
|
||||||
|
narrow_fixed(device_details->device_id),
|
||||||
|
device_name,
|
||||||
|
device_details->role,
|
||||||
|
describe_wave_format(&device_details->output_format.Format));
|
||||||
|
}
|
||||||
|
const auto channels = device_details->output_format.Format.nChannels;
|
||||||
|
if (channels != 2 && channels != 6 &&
|
||||||
|
!channel_warning_logged.exchange(true, std::memory_order_relaxed)) {
|
||||||
|
log_warning(
|
||||||
|
"audio::xaudio2",
|
||||||
|
"output device '{}' has {} channels; Nostalgia requires stereo or 5.1 output",
|
||||||
|
device_name,
|
||||||
|
channels);
|
||||||
|
deferredlogs::defer_error_messages({
|
||||||
|
"unsupported audio output channel count detected!",
|
||||||
|
fmt::format(" device: {}", device_name),
|
||||||
|
fmt::format(" detected {} channels; Nostalgia requires 2 (stereo) or 6 (5.1)", channels),
|
||||||
|
" * configure the default Windows playback device for stereo or 5.1 output",
|
||||||
|
" * disable 7.1 surround sound or spatial audio for this device",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_warning(
|
||||||
|
"audio::xaudio2",
|
||||||
|
"IXAudio2::GetDeviceDetails({}) -> {}",
|
||||||
|
device_index,
|
||||||
|
FMT_HRESULT(result));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE Initialize(UINT32 flags, UINT32 processor) override {
|
||||||
|
const auto result = real->Initialize(flags, processor);
|
||||||
|
log_info(
|
||||||
|
"audio::xaudio2",
|
||||||
|
"IXAudio2::Initialize(flags=0x{:08x}, processor=0x{:08x}) -> {}",
|
||||||
|
flags,
|
||||||
|
processor,
|
||||||
|
FMT_HRESULT(result));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE RegisterForCallbacks(void *callback) override {
|
||||||
|
const auto result = real->RegisterForCallbacks(callback);
|
||||||
|
log_info(
|
||||||
|
"audio::xaudio2",
|
||||||
|
"IXAudio2::RegisterForCallbacks({}) -> {}",
|
||||||
|
callback,
|
||||||
|
FMT_HRESULT(result));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void STDMETHODCALLTYPE UnregisterForCallbacks(void *callback) override {
|
||||||
|
log_info("audio::xaudio2", "IXAudio2::UnregisterForCallbacks({})", callback);
|
||||||
|
real->UnregisterForCallbacks(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE CreateSourceVoice(
|
||||||
|
void **source_voice,
|
||||||
|
const WAVEFORMATEX *source_format,
|
||||||
|
UINT32 flags,
|
||||||
|
float max_frequency_ratio,
|
||||||
|
void *callback,
|
||||||
|
const void *send_list,
|
||||||
|
const XAudio2EffectChain *effect_chain) override {
|
||||||
|
return real->CreateSourceVoice(
|
||||||
|
source_voice,
|
||||||
|
source_format,
|
||||||
|
flags,
|
||||||
|
max_frequency_ratio,
|
||||||
|
callback,
|
||||||
|
send_list,
|
||||||
|
effect_chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE CreateSubmixVoice(
|
||||||
|
void **submix_voice,
|
||||||
|
UINT32 input_channels,
|
||||||
|
UINT32 input_sample_rate,
|
||||||
|
UINT32 flags,
|
||||||
|
UINT32 processing_stage,
|
||||||
|
const void *send_list,
|
||||||
|
const XAudio2EffectChain *effect_chain) override {
|
||||||
|
return real->CreateSubmixVoice(
|
||||||
|
submix_voice,
|
||||||
|
input_channels,
|
||||||
|
input_sample_rate,
|
||||||
|
flags,
|
||||||
|
processing_stage,
|
||||||
|
send_list,
|
||||||
|
effect_chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE CreateMasteringVoice(
|
||||||
|
void **mastering_voice,
|
||||||
|
UINT32 input_channels,
|
||||||
|
UINT32 input_sample_rate,
|
||||||
|
UINT32 flags,
|
||||||
|
UINT32 device_index,
|
||||||
|
const XAudio2EffectChain *effect_chain) override {
|
||||||
|
const auto result = real->CreateMasteringVoice(
|
||||||
|
mastering_voice,
|
||||||
|
input_channels,
|
||||||
|
input_sample_rate,
|
||||||
|
flags,
|
||||||
|
device_index,
|
||||||
|
effect_chain);
|
||||||
|
log_info(
|
||||||
|
"audio::xaudio2",
|
||||||
|
"IXAudio2::CreateMasteringVoice(channels={}, rate={} Hz, flags=0x{:08x}, device={}, effects={}) -> {}, voice={}",
|
||||||
|
input_channels,
|
||||||
|
input_sample_rate,
|
||||||
|
flags,
|
||||||
|
device_index,
|
||||||
|
effect_chain != nullptr ? effect_chain->effect_count : 0,
|
||||||
|
FMT_HRESULT(result),
|
||||||
|
mastering_voice != nullptr ? *mastering_voice : nullptr);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE StartEngine() override {
|
||||||
|
const auto result = real->StartEngine();
|
||||||
|
log_info("audio::xaudio2", "IXAudio2::StartEngine -> {}", FMT_HRESULT(result));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void STDMETHODCALLTYPE StopEngine() override {
|
||||||
|
log_info("audio::xaudio2", "IXAudio2::StopEngine");
|
||||||
|
real->StopEngine();
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE CommitChanges(UINT32 operation_set) override {
|
||||||
|
return real->CommitChanges(operation_set);
|
||||||
|
}
|
||||||
|
|
||||||
|
void STDMETHODCALLTYPE GetPerformanceData(void *performance_data) override {
|
||||||
|
real->GetPerformanceData(performance_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void STDMETHODCALLTYPE SetDebugConfiguration(
|
||||||
|
const void *debug_configuration,
|
||||||
|
void *reserved) override {
|
||||||
|
log_info("audio::xaudio2", "IXAudio2::SetDebugConfiguration({})", debug_configuration);
|
||||||
|
real->SetDebugConfiguration(debug_configuration, reserved);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::atomic<ULONG> ref_count = 1;
|
||||||
|
std::atomic_bool device_details_logged = false;
|
||||||
|
std::atomic_bool channel_warning_logged = false;
|
||||||
|
IXAudio2_27 *real;
|
||||||
|
};
|
||||||
|
|
||||||
|
static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
|
||||||
|
REFCLSID clsid,
|
||||||
|
LPUNKNOWN outer,
|
||||||
|
DWORD class_context,
|
||||||
|
REFIID iid,
|
||||||
|
LPVOID *object) {
|
||||||
|
const auto result = CoCreateInstance_orig(clsid, outer, class_context, iid, object);
|
||||||
|
log_info(
|
||||||
|
"audio::xact",
|
||||||
|
"CoCreateInstance(clsid={}, iid={}, context=0x{:08x}) -> {}, object={}",
|
||||||
|
guid2s(clsid),
|
||||||
|
guid2s(iid),
|
||||||
|
class_context,
|
||||||
|
FMT_HRESULT(result),
|
||||||
|
object != nullptr ? *object : nullptr);
|
||||||
|
|
||||||
|
if (SUCCEEDED(result) && object != nullptr && *object != nullptr &&
|
||||||
|
IsEqualCLSID(clsid, CLSID_XAudio2_7_LEGACY) &&
|
||||||
|
IsEqualIID(iid, IID_IXAudio2_7_LEGACY)) {
|
||||||
|
*object = static_cast<IXAudio2_27 *>(
|
||||||
|
new WrappedXAudio2(static_cast<IXAudio2_27 *>(*object)));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static HRESULT WINAPI CreateFX_hook(
|
||||||
|
REFCLSID clsid,
|
||||||
|
IUnknown **effect,
|
||||||
|
const void *init_data,
|
||||||
|
UINT32 init_data_size) {
|
||||||
|
const auto result = CreateFX_orig(clsid, effect, init_data, init_data_size);
|
||||||
|
log_info(
|
||||||
|
"audio::xapofx",
|
||||||
|
"CreateFX(clsid={}, init_data={}, size={}) -> {}, effect={}",
|
||||||
|
guid2s(clsid),
|
||||||
|
init_data,
|
||||||
|
init_data_size,
|
||||||
|
FMT_HRESULT(result),
|
||||||
|
effect != nullptr ? static_cast<void *>(*effect) : nullptr);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void init() {
|
||||||
|
const auto libxact = GetModuleHandleW(L"libxact.dll");
|
||||||
|
if (libxact == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoCreateInstance_orig = detour::iat_try(
|
||||||
|
"CoCreateInstance", CoCreateInstance_hook, libxact);
|
||||||
|
CreateFX_orig = detour::iat_try("CreateFX", CreateFX_hook, libxact);
|
||||||
|
|
||||||
|
log_info(
|
||||||
|
"audio::xact",
|
||||||
|
"libxact hooks installed: CoCreateInstance={}, CreateFX={}",
|
||||||
|
CoCreateInstance_orig != nullptr,
|
||||||
|
CreateFX_orig != nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace hooks::audio::xact {
|
||||||
|
void init();
|
||||||
|
}
|
||||||
@@ -113,7 +113,10 @@ static Direct3DCreate9On12Ex_t Direct3DCreate9On12Ex_orig = nullptr;
|
|||||||
static bool ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE = false;
|
static bool ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE = false;
|
||||||
static IDirect3DSwapChain9 *SUB_SWAP_CHAIN = nullptr;
|
static IDirect3DSwapChain9 *SUB_SWAP_CHAIN = nullptr;
|
||||||
|
|
||||||
static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRESENT_PARAMETERS *present_params);
|
static void graphics_d3d9_ldj_init_sub_screen(
|
||||||
|
IDirect3DDevice9Ex *device,
|
||||||
|
D3DPRESENT_PARAMETERS *present_params,
|
||||||
|
UINT gfdm_small_swapchain);
|
||||||
|
|
||||||
static std::string behavior2s(DWORD behavior_flags) {
|
static std::string behavior2s(DWORD behavior_flags) {
|
||||||
FLAGS_START(behavior_flags);
|
FLAGS_START(behavior_flags);
|
||||||
@@ -409,7 +412,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::QueryInterface(
|
|||||||
return E_POINTER;
|
return E_POINTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (riid == IID_WrappedIDirect3D9 ||
|
if ((riid == IID_IUnknown && gfdm_two_head_exclusive()) ||
|
||||||
|
riid == IID_WrappedIDirect3D9 ||
|
||||||
riid == IID_IDirect3D9 ||
|
riid == IID_IDirect3D9 ||
|
||||||
riid == IID_IDirect3D9Ex)
|
riid == IID_IDirect3D9Ex)
|
||||||
{
|
{
|
||||||
@@ -467,6 +471,18 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::RegisterSoftwareDevice(void *pIniti
|
|||||||
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterCount() {
|
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterCount() {
|
||||||
UINT result = pReal->GetAdapterCount();
|
UINT result = pReal->GetAdapterCount();
|
||||||
|
|
||||||
|
if (gfdm_two_head_exclusive()) {
|
||||||
|
if (result == 2) {
|
||||||
|
FAKE_SUBSCREEN_ADAPTER = true;
|
||||||
|
return GFDM_LOGICAL_HEAD_COUNT;
|
||||||
|
}
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head mode requires two native adapters; found {}",
|
||||||
|
result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
if (!FAKE_SUBSCREEN_ADAPTER) {
|
if (!FAKE_SUBSCREEN_ADAPTER) {
|
||||||
if (games::popn::is_pikapika_model() && result == 1) {
|
if (games::popn::is_pikapika_model() && result == 1) {
|
||||||
FAKE_SUBSCREEN_ADAPTER = true;
|
FAKE_SUBSCREEN_ADAPTER = true;
|
||||||
@@ -493,7 +509,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier(
|
|||||||
D3DADAPTER_IDENTIFIER9 *pIdentifier)
|
D3DADAPTER_IDENTIFIER9 *pIdentifier)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0 && pIdentifier) {
|
if (is_fake_subscreen_adapter(Adapter) && pIdentifier) {
|
||||||
*pIdentifier = {};
|
*pIdentifier = {};
|
||||||
const std::string adapter_name = fmt::format("\\\\.\\DISPLAY_SPICE_FAKE_{}", Adapter);
|
const std::string adapter_name = fmt::format("\\\\.\\DISPLAY_SPICE_FAKE_{}", Adapter);
|
||||||
strcpy(pIdentifier->DeviceName, adapter_name.c_str());
|
strcpy(pIdentifier->DeviceName, adapter_name.c_str());
|
||||||
@@ -509,7 +525,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier(
|
|||||||
}
|
}
|
||||||
|
|
||||||
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCount(UINT Adapter, D3DFORMAT Format) {
|
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCount(UINT Adapter, D3DFORMAT Format) {
|
||||||
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
|
if (is_fake_subscreen_adapter(Adapter)) {
|
||||||
log_misc("graphics::d3d9", "GetAdapterModeCount called for fake subscreen adapter {}", Adapter);
|
log_misc("graphics::d3d9", "GetAdapterModeCount called for fake subscreen adapter {}", Adapter);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -523,13 +539,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes(
|
|||||||
UINT Mode,
|
UINT Mode,
|
||||||
D3DDISPLAYMODE *pMode)
|
D3DDISPLAYMODE *pMode)
|
||||||
{
|
{
|
||||||
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
|
if (is_fake_subscreen_adapter(Adapter)) {
|
||||||
log_misc("graphics::d3d9", "EnumAdapterModes called for fake subscreen adapter {}", Adapter);
|
log_misc("graphics::d3d9", "EnumAdapterModes called for fake subscreen adapter {}", Adapter);
|
||||||
if (Mode == 0 && pMode) {
|
if (Mode == 0 && pMode) {
|
||||||
pMode->Width = 1280;
|
get_fake_subscreen_display_mode(Adapter, pMode);
|
||||||
pMode->Height = 800;
|
|
||||||
pMode->RefreshRate = 60;
|
|
||||||
pMode->Format = D3DFMT_X8R8G8B8;
|
|
||||||
return S_OK;
|
return S_OK;
|
||||||
} else {
|
} else {
|
||||||
return D3DERR_INVALIDCALL;
|
return D3DERR_INVALIDCALL;
|
||||||
@@ -611,6 +624,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterDisplayMode(UINT Adapter, D3DDISPLAYMODE *pMode) {
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterDisplayMode(UINT Adapter, D3DDISPLAYMODE *pMode) {
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
|
||||||
|
if (pMode == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
get_fake_subscreen_display_mode(Adapter, pMode);
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->GetAdapterDisplayMode(Adapter, pMode));
|
CHECK_RESULT(pReal->GetAdapterDisplayMode(Adapter, pMode));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -621,6 +642,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceType(
|
|||||||
D3DFORMAT BackBufferFormat,
|
D3DFORMAT BackBufferFormat,
|
||||||
BOOL bWindowed)
|
BOOL bWindowed)
|
||||||
{
|
{
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(iAdapter)) {
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->CheckDeviceType(iAdapter, DevType, DisplayFormat, BackBufferFormat, bWindowed));
|
CHECK_RESULT(pReal->CheckDeviceType(iAdapter, DevType, DisplayFormat, BackBufferFormat, bWindowed));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,7 +657,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceFormat(
|
|||||||
D3DRESOURCETYPE RType,
|
D3DRESOURCETYPE RType,
|
||||||
D3DFORMAT CheckFormat)
|
D3DFORMAT CheckFormat)
|
||||||
{
|
{
|
||||||
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
|
if (is_fake_subscreen_adapter(Adapter)) {
|
||||||
log_misc("graphics::d3d9", "CheckDeviceFormat called for fake subscreen adapter {}", Adapter);
|
log_misc("graphics::d3d9", "CheckDeviceFormat called for fake subscreen adapter {}", Adapter);
|
||||||
return D3D_OK;
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
@@ -648,6 +673,13 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceMultiSampleType(
|
|||||||
D3DMULTISAMPLE_TYPE MultiSampleType,
|
D3DMULTISAMPLE_TYPE MultiSampleType,
|
||||||
DWORD *pQualityLevels)
|
DWORD *pQualityLevels)
|
||||||
{
|
{
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
|
||||||
|
if (pQualityLevels != nullptr) {
|
||||||
|
*pQualityLevels = 1;
|
||||||
|
}
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->CheckDeviceMultiSampleType(
|
CHECK_RESULT(pReal->CheckDeviceMultiSampleType(
|
||||||
Adapter,
|
Adapter,
|
||||||
DeviceType,
|
DeviceType,
|
||||||
@@ -664,6 +696,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDepthStencilMatch(
|
|||||||
D3DFORMAT RenderTargetFormat,
|
D3DFORMAT RenderTargetFormat,
|
||||||
D3DFORMAT DepthStencilFormat)
|
D3DFORMAT DepthStencilFormat)
|
||||||
{
|
{
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->CheckDepthStencilMatch(
|
CHECK_RESULT(pReal->CheckDepthStencilMatch(
|
||||||
Adapter,
|
Adapter,
|
||||||
DeviceType,
|
DeviceType,
|
||||||
@@ -678,6 +714,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceFormatConversion(
|
|||||||
D3DFORMAT SourceFormat,
|
D3DFORMAT SourceFormat,
|
||||||
D3DFORMAT TargetFormat)
|
D3DFORMAT TargetFormat)
|
||||||
{
|
{
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->CheckDeviceFormatConversion(Adapter, DeviceType, SourceFormat, TargetFormat));
|
CHECK_RESULT(pReal->CheckDeviceFormatConversion(Adapter, DeviceType, SourceFormat, TargetFormat));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,6 +729,20 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetDeviceCaps(UINT Adapter, D3DDEVT
|
|||||||
return D3DERR_INVALIDCALL;
|
return D3DERR_INVALIDCALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (gfdm_two_head_exclusive() && Adapter < GFDM_LOGICAL_HEAD_COUNT) {
|
||||||
|
HRESULT ret = this->pReal->GetDeviceCaps(
|
||||||
|
Adapter >= 2 ? 0 : Adapter,
|
||||||
|
DeviceType,
|
||||||
|
pCaps);
|
||||||
|
if (SUCCEEDED(ret)) {
|
||||||
|
pCaps->NumberOfAdaptersInGroup =
|
||||||
|
Adapter == 0 ? GFDM_LOGICAL_HEAD_COUNT : 0;
|
||||||
|
pCaps->MasterAdapterOrdinal = 0;
|
||||||
|
pCaps->AdapterOrdinalInGroup = Adapter;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
// SDVX uses `NumberOfAdaptersInGroup` to allocate a vector and the Microsoft documentation states:
|
// SDVX uses `NumberOfAdaptersInGroup` to allocate a vector and the Microsoft documentation states:
|
||||||
// "The value will be 0 for a subordinate adapter of a multihead card. Each card can have at most one
|
// "The value will be 0 for a subordinate adapter of a multihead card. Each card can have at most one
|
||||||
// master, but may have many subordinates." Therefore, this must point to the master adapter.
|
// master, but may have many subordinates." Therefore, this must point to the master adapter.
|
||||||
@@ -741,6 +795,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetDeviceCaps(UINT Adapter, D3DDEVT
|
|||||||
}
|
}
|
||||||
|
|
||||||
HMONITOR STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterMonitor(UINT Adapter) {
|
HMONITOR STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterMonitor(UINT Adapter) {
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
|
||||||
|
return pReal->GetAdapterMonitor(0);
|
||||||
|
}
|
||||||
|
|
||||||
return pReal->GetAdapterMonitor(Adapter);
|
return pReal->GetAdapterMonitor(Adapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -861,7 +919,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
|
|||||||
"either use -graphics-force-refresh option or change the desktop resolution beforehand.");
|
"either use -graphics-force-refresh option or change the desktop resolution beforehand.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!GRAPHICS_WINDOWED && num_adapters >= 2 && GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
|
if (!GRAPHICS_WINDOWED && num_adapters >= 2
|
||||||
|
&& GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
|
||||||
log_info("graphics::d3d9", "force sub refresh rate: {} => {} Hz (-graphics-force-refresh-sub option)",
|
log_info("graphics::d3d9", "force sub refresh rate: {} => {} Hz (-graphics-force-refresh-sub option)",
|
||||||
pPresentationParameters[1].FullScreen_RefreshRateInHz,
|
pPresentationParameters[1].FullScreen_RefreshRateInHz,
|
||||||
GRAPHICS_FORCE_REFRESH_SUB.value());
|
GRAPHICS_FORCE_REFRESH_SUB.value());
|
||||||
@@ -902,7 +961,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
|
|||||||
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
|
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
|
||||||
graphics_hook_window(hFocusWindow, pPresentationParameters);
|
graphics_hook_window(hFocusWindow, pPresentationParameters);
|
||||||
|
|
||||||
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface);
|
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(
|
||||||
|
hFocusWindow,
|
||||||
|
*ppReturnedDeviceInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
// return result
|
// return result
|
||||||
@@ -916,6 +977,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
|
|||||||
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCountEx(UINT Adapter, const D3DDISPLAYMODEFILTER *pFilter) {
|
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCountEx(UINT Adapter, const D3DDISPLAYMODEFILTER *pFilter) {
|
||||||
assert(is_d3d9ex);
|
assert(is_d3d9ex);
|
||||||
|
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
return static_cast<IDirect3D9Ex *>(pReal)->GetAdapterModeCountEx(Adapter, pFilter);
|
return static_cast<IDirect3D9Ex *>(pReal)->GetAdapterModeCountEx(Adapter, pFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -926,6 +991,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModesEx(
|
|||||||
D3DDISPLAYMODEEX *pMode)
|
D3DDISPLAYMODEEX *pMode)
|
||||||
{
|
{
|
||||||
assert(is_d3d9ex);
|
assert(is_d3d9ex);
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
|
||||||
|
if (Mode != 0 || pMode == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
get_fake_subscreen_display_mode_ex(Adapter, pMode);
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->EnumAdapterModesEx(Adapter, pFilter, Mode, pMode));
|
CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->EnumAdapterModesEx(Adapter, pFilter, Mode, pMode));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -935,6 +1008,17 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterDisplayModeEx(
|
|||||||
D3DDISPLAYROTATION *pRotation)
|
D3DDISPLAYROTATION *pRotation)
|
||||||
{
|
{
|
||||||
assert(is_d3d9ex);
|
assert(is_d3d9ex);
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
|
||||||
|
if (pMode == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
get_fake_subscreen_display_mode_ex(Adapter, pMode);
|
||||||
|
if (pRotation != nullptr) {
|
||||||
|
*pRotation = D3DDISPLAYROTATION_IDENTITY;
|
||||||
|
}
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->GetAdapterDisplayModeEx(Adapter, pMode, pRotation));
|
CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->GetAdapterDisplayModeEx(Adapter, pMode, pRotation));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -963,6 +1047,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
|
|
||||||
return D3DERR_INVALIDCALL;
|
return D3DERR_INVALIDCALL;
|
||||||
}
|
}
|
||||||
|
if (gfdm_two_head_exclusive() && GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"-forceressub is unavailable; SMALL must remain {}x{}",
|
||||||
|
GFDM_SMALL_WIDTH,
|
||||||
|
GFDM_SMALL_HEIGHT);
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
DWORD orig_behavior_flags = BehaviorFlags;
|
DWORD orig_behavior_flags = BehaviorFlags;
|
||||||
size_t num_adapters = 1;
|
size_t num_adapters = 1;
|
||||||
@@ -989,6 +1081,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
if (SUCCEEDED(this->pReal->GetDeviceCaps(Adapter, DeviceType, &device_caps))) {
|
if (SUCCEEDED(this->pReal->GetDeviceCaps(Adapter, DeviceType, &device_caps))) {
|
||||||
num_adapters = device_caps.NumberOfAdaptersInGroup;
|
num_adapters = device_caps.NumberOfAdaptersInGroup;
|
||||||
}
|
}
|
||||||
|
if (gfdm_two_head_exclusive()) {
|
||||||
|
num_adapters = GFDM_LOGICAL_HEAD_COUNT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (size_t i = 0; i < num_adapters; i++) {
|
for (size_t i = 0; i < num_adapters; i++) {
|
||||||
@@ -1105,7 +1200,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!GRAPHICS_WINDOWED && num_adapters >= 2 && GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
|
if (!GRAPHICS_WINDOWED && !gfdm_two_head_exclusive()
|
||||||
|
&& num_adapters >= 2 && GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
|
||||||
log_info("graphics::d3d9", "force sub refresh rate: {} => {} Hz (-graphics-force-refresh-sub option)",
|
log_info("graphics::d3d9", "force sub refresh rate: {} => {} Hz (-graphics-force-refresh-sub option)",
|
||||||
pPresentationParameters[1].FullScreen_RefreshRateInHz,
|
pPresentationParameters[1].FullScreen_RefreshRateInHz,
|
||||||
GRAPHICS_FORCE_REFRESH_SUB.value());
|
GRAPHICS_FORCE_REFRESH_SUB.value());
|
||||||
@@ -1131,16 +1227,93 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
pPresentationParameters->BackBufferCount = GRAPHICS_FORCE_VSYNC_BUFFER.value();
|
pPresentationParameters->BackBufferCount = GRAPHICS_FORCE_VSYNC_BUFFER.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GfdmTwoHeadDeviceState gfdm_parameters(
|
||||||
|
pPresentationParameters,
|
||||||
|
pFullscreenDisplayMode);
|
||||||
|
if (gfdm_two_head_exclusive()) {
|
||||||
|
if (!(BehaviorFlags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"CreateDeviceEx did not request an adapter-group device");
|
||||||
|
return D3DERR_NOTAVAILABLE;
|
||||||
|
}
|
||||||
|
HRESULT select = graphics_d3d9_gfdm_select_two_head_group_parameters(
|
||||||
|
pPresentationParameters,
|
||||||
|
pFullscreenDisplayMode,
|
||||||
|
gfdm_parameters.native_presentation_parameters.data(),
|
||||||
|
gfdm_parameters.native_fullscreen_display_modes.data(),
|
||||||
|
&gfdm_parameters.logical_small_swapchain,
|
||||||
|
"CreateDeviceEx");
|
||||||
|
if (FAILED(select)) {
|
||||||
|
return select;
|
||||||
|
}
|
||||||
|
if (GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
|
||||||
|
gfdm_parameters.native_presentation_parameters[1]
|
||||||
|
.FullScreen_RefreshRateInHz =
|
||||||
|
GRAPHICS_FORCE_REFRESH_SUB.value();
|
||||||
|
if (pFullscreenDisplayMode != nullptr) {
|
||||||
|
gfdm_parameters.native_fullscreen_display_modes[1].RefreshRate =
|
||||||
|
GRAPHICS_FORCE_REFRESH_SUB.value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HRESULT remap = graphics_d3d9_gfdm_remap_two_head_group_parameters(
|
||||||
|
gfdm_parameters.native_presentation_parameters.data(),
|
||||||
|
pFullscreenDisplayMode != nullptr
|
||||||
|
? gfdm_parameters.native_fullscreen_display_modes.data()
|
||||||
|
: nullptr,
|
||||||
|
"CreateDeviceEx");
|
||||||
|
if (FAILED(remap)) {
|
||||||
|
return remap;
|
||||||
|
}
|
||||||
|
if (pFullscreenDisplayMode != nullptr) {
|
||||||
|
graphics_d3d9_gfdm_align_two_head_refresh_to_desktop(
|
||||||
|
static_cast<IDirect3D9Ex *>(this->pReal),
|
||||||
|
Adapter,
|
||||||
|
gfdm_parameters.native_presentation_parameters.data(),
|
||||||
|
gfdm_parameters.native_fullscreen_display_modes.data(),
|
||||||
|
"CreateDeviceEx");
|
||||||
|
}
|
||||||
|
if (!graphics_gitadora_prepare_two_head_device_window(
|
||||||
|
gfdm_parameters.native_presentation_parameters[1].hDeviceWindow,
|
||||||
|
this->pReal->GetAdapterMonitor(Adapter + 1),
|
||||||
|
gfdm_parameters.native_presentation_parameters[1].BackBufferWidth,
|
||||||
|
gfdm_parameters.native_presentation_parameters[1].BackBufferHeight))
|
||||||
|
{
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
HRESULT validation = validate_gfdm_two_head_exclusive(
|
||||||
|
this->pReal,
|
||||||
|
Adapter,
|
||||||
|
DeviceType,
|
||||||
|
BehaviorFlags,
|
||||||
|
gfdm_parameters.native_presentation_parameters.data());
|
||||||
|
if (FAILED(validation)) {
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
|
gfdm_parameters.use_native_parameters();
|
||||||
|
}
|
||||||
|
|
||||||
// call original
|
// call original
|
||||||
HRESULT result = static_cast<IDirect3D9Ex *>(this->pReal)->CreateDeviceEx(
|
HRESULT result = static_cast<IDirect3D9Ex *>(this->pReal)->CreateDeviceEx(
|
||||||
Adapter,
|
Adapter,
|
||||||
DeviceType,
|
DeviceType,
|
||||||
hFocusWindow,
|
hFocusWindow,
|
||||||
BehaviorFlags,
|
BehaviorFlags,
|
||||||
pPresentationParameters,
|
gfdm_parameters.presentation_parameters,
|
||||||
pFullscreenDisplayMode,
|
gfdm_parameters.fullscreen_display_modes,
|
||||||
ppReturnedDeviceInterface);
|
ppReturnedDeviceInterface);
|
||||||
|
|
||||||
|
if (SUCCEEDED(result) && gfdm_two_head_exclusive()) {
|
||||||
|
pPresentationParameters[0] = gfdm_parameters.presentation_parameters[0];
|
||||||
|
pPresentationParameters[gfdm_parameters.logical_small_swapchain] =
|
||||||
|
gfdm_parameters.presentation_parameters[1];
|
||||||
|
if (pFullscreenDisplayMode != nullptr) {
|
||||||
|
pFullscreenDisplayMode[0] = gfdm_parameters.fullscreen_display_modes[0];
|
||||||
|
pFullscreenDisplayMode[gfdm_parameters.logical_small_swapchain] =
|
||||||
|
gfdm_parameters.fullscreen_display_modes[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// check for error
|
// check for error
|
||||||
if (result != D3D_OK) {
|
if (result != D3D_OK) {
|
||||||
|
|
||||||
@@ -1151,17 +1324,28 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
|
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
|
||||||
graphics_hook_window(hFocusWindow, pPresentationParameters);
|
graphics_hook_window(hFocusWindow, pPresentationParameters);
|
||||||
|
|
||||||
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface);
|
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(
|
||||||
|
hFocusWindow,
|
||||||
|
*ppReturnedDeviceInterface,
|
||||||
|
gfdm_parameters.logical_small_swapchain,
|
||||||
|
gfdm_two_head_exclusive() ? static_cast<IDirect3D9 *>(this) : nullptr,
|
||||||
|
gfdm_two_head_exclusive() ? pPresentationParameters : nullptr);
|
||||||
|
|
||||||
// initialize sub screen if the game requested a multi-head context
|
// initialize sub screen if the game requested a multi-head context
|
||||||
if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) &&
|
if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) &&
|
||||||
(orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
|
(orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
|
||||||
|
|
||||||
UINT i = 1;
|
UINT i = games::gitadora::is_arena_model()
|
||||||
if (games::gitadora::is_arena_model()) {
|
? gfdm_parameters.logical_small_swapchain
|
||||||
i = 2;
|
: 1;
|
||||||
}
|
D3DPRESENT_PARAMETERS *subscreen_parameters =
|
||||||
graphics_d3d9_ldj_init_sub_screen(*ppReturnedDeviceInterface, &pPresentationParameters[i]);
|
gfdm_two_head_exclusive()
|
||||||
|
? &gfdm_parameters.presentation_parameters[1]
|
||||||
|
: &pPresentationParameters[i];
|
||||||
|
graphics_d3d9_ldj_init_sub_screen(
|
||||||
|
*ppReturnedDeviceInterface,
|
||||||
|
subscreen_parameters,
|
||||||
|
i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1170,6 +1354,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterLUID(UINT Adapter, LUID *pLUID) {
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterLUID(UINT Adapter, LUID *pLUID) {
|
||||||
assert(is_d3d9ex);
|
assert(is_d3d9ex);
|
||||||
|
if (gfdm_two_head_exclusive() && is_fake_subscreen_adapter(Adapter)) {
|
||||||
|
if (pLUID == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
pLUID->LowPart = static_cast<DWORD>(-static_cast<LONG>(Adapter));
|
||||||
|
pLUID->HighPart = -static_cast<LONG>(Adapter);
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->GetAdapterLUID(Adapter, pLUID));
|
CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->GetAdapterLUID(Adapter, pLUID));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1178,7 +1370,11 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterLUID(UINT Adapter, LUID *
|
|||||||
// The sub screen swap chain should be created if:
|
// The sub screen swap chain should be created if:
|
||||||
// - Running windowed with `NumberOfAdaptersInGroup >= 2` (game expects implicit swap chain to exist)
|
// - Running windowed with `NumberOfAdaptersInGroup >= 2` (game expects implicit swap chain to exist)
|
||||||
// - Running fullscreen with `NumberOfAdaptersInGroup < 2` (overridden `GetDeviceCaps` structure)
|
// - Running fullscreen with `NumberOfAdaptersInGroup < 2` (overridden `GetDeviceCaps` structure)
|
||||||
static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRESENT_PARAMETERS *present_params) {
|
static void graphics_d3d9_ldj_init_sub_screen(
|
||||||
|
IDirect3DDevice9Ex *device,
|
||||||
|
D3DPRESENT_PARAMETERS *present_params,
|
||||||
|
UINT gfdm_small_swapchain)
|
||||||
|
{
|
||||||
D3DCAPS9 caps {};
|
D3DCAPS9 caps {};
|
||||||
HRESULT hr = device->GetDeviceCaps(&caps);
|
HRESULT hr = device->GetDeviceCaps(&caps);
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
@@ -1212,10 +1408,9 @@ static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRE
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
int swapchain = 1;
|
const int swapchain = games::gitadora::is_arena_model()
|
||||||
if (games::gitadora::is_arena_model()) {
|
? static_cast<int>(gfdm_small_swapchain)
|
||||||
swapchain = 2;
|
: 1;
|
||||||
}
|
|
||||||
|
|
||||||
hr = device->GetSwapChain(swapchain, &SUB_SWAP_CHAIN);
|
hr = device->GetSwapChain(swapchain, &SUB_SWAP_CHAIN);
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
@@ -1273,9 +1468,11 @@ static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
|
|||||||
wintouchemu::update();
|
wintouchemu::update();
|
||||||
|
|
||||||
// newer versions of exceed gear needs SUBSCREEN_FORCE_REDRAW
|
// newer versions of exceed gear needs SUBSCREEN_FORCE_REDRAW
|
||||||
// (when enabled on older versions of EG, you end up with graphical glitches on the subscreen
|
// (when enabled on older versions of EG, you end up with graphical glitches on the subscreen)
|
||||||
// early versions of popn HC needs this as well, otherwise the subscreen doesn't update at all
|
//
|
||||||
if (GRAPHICS_WINDOWED || SUBSCREEN_FORCE_REDRAW || games::popn::is_pikapika_model()) {
|
// early versions of popn HC needs this as well, but not on by default as it can cause
|
||||||
|
// graphical glitches on some GPUs
|
||||||
|
if (GRAPHICS_WINDOWED || SUBSCREEN_FORCE_REDRAW) {
|
||||||
SUB_SWAP_CHAIN->Present(nullptr, nullptr, nullptr, nullptr, 0);
|
SUB_SWAP_CHAIN->Present(nullptr, nullptr, nullptr, nullptr, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#include <d3d9.h>
|
#include <d3d9.h>
|
||||||
|
|
||||||
|
#include "d3d9_gfdm.h"
|
||||||
|
|
||||||
// {EEE9CCF6-53D6-4326-9AE5-60921B3DB394}
|
// {EEE9CCF6-53D6-4326-9AE5-60921B3DB394}
|
||||||
static const GUID IID_WrappedIDirect3D9 = {
|
static const GUID IID_WrappedIDirect3D9 = {
|
||||||
0xeee9ccf6, 0x53d6, 0x4326, { 0x9a, 0xe5, 0x60, 0x92, 0x1b, 0x3d, 0xb3, 0x94 }
|
0xeee9ccf6, 0x53d6, 0x4326, { 0x9a, 0xe5, 0x60, 0x92, 0x1b, 0x3d, 0xb3, 0x94 }
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
#include "d3d9_device.h"
|
#include "d3d9_device.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
#include <climits>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "games/gitadora/gitadora.h"
|
#include "games/gitadora/gitadora.h"
|
||||||
@@ -12,6 +16,7 @@
|
|||||||
#include "cfg/screen_resize.h"
|
#include "cfg/screen_resize.h"
|
||||||
|
|
||||||
#include "d3d9_backend.h"
|
#include "d3d9_backend.h"
|
||||||
|
#include "d3d9_live2d.h"
|
||||||
#include "d3d9_texture.h"
|
#include "d3d9_texture.h"
|
||||||
|
|
||||||
#ifndef SPICE64
|
#ifndef SPICE64
|
||||||
@@ -36,7 +41,6 @@
|
|||||||
constexpr bool CUSTOM_RESET = false;
|
constexpr bool CUSTOM_RESET = false;
|
||||||
|
|
||||||
constexpr D3DFORMAT D3DFMT_DF24 = static_cast<D3DFORMAT>(MAKEFOURCC('D', 'F', '2', '4'));
|
constexpr D3DFORMAT D3DFMT_DF24 = static_cast<D3DFORMAT>(MAKEFOURCC('D', 'F', '2', '4'));
|
||||||
|
|
||||||
auto format_as(D3DPOOL f) {
|
auto format_as(D3DPOOL f) {
|
||||||
return fmt::underlying(f);
|
return fmt::underlying(f);
|
||||||
}
|
}
|
||||||
@@ -80,7 +84,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::QueryInterface(
|
|||||||
return E_POINTER;
|
return E_POINTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (riid == IID_WrappedIDirect3DDevice9 ||
|
if ((riid == IID_IUnknown && is_gfdm_two_head_exclusive()) ||
|
||||||
|
riid == IID_WrappedIDirect3DDevice9 ||
|
||||||
riid == IID_IDirect3DDevice9 ||
|
riid == IID_IDirect3DDevice9 ||
|
||||||
riid == IID_IDirect3DDevice9Ex)
|
riid == IID_IDirect3DDevice9Ex)
|
||||||
{
|
{
|
||||||
@@ -197,6 +202,15 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDirect3D(
|
|||||||
IDirect3D9 **ppD3D9)
|
IDirect3D9 **ppD3D9)
|
||||||
{
|
{
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
|
if (is_gfdm_two_head_exclusive() && gfdm_parent_d3d != nullptr) {
|
||||||
|
if (ppD3D9 == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
gfdm_parent_d3d->AddRef();
|
||||||
|
*ppD3D9 = gfdm_parent_d3d;
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->GetDirect3D(ppD3D9));
|
CHECK_RESULT(pReal->GetDirect3D(ppD3D9));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +218,13 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDeviceCaps(
|
|||||||
D3DCAPS9 *pCaps)
|
D3DCAPS9 *pCaps)
|
||||||
{
|
{
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
CHECK_RESULT(pReal->GetDeviceCaps(pCaps));
|
HRESULT result = pReal->GetDeviceCaps(pCaps);
|
||||||
|
if (SUCCEEDED(result) && is_gfdm_two_head_exclusive() && pCaps != nullptr) {
|
||||||
|
pCaps->NumberOfAdaptersInGroup = GFDM_LOGICAL_HEAD_COUNT;
|
||||||
|
pCaps->MasterAdapterOrdinal = 0;
|
||||||
|
pCaps->AdapterOrdinalInGroup = 0;
|
||||||
|
}
|
||||||
|
CHECK_RESULT(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDisplayMode(
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDisplayMode(
|
||||||
@@ -212,6 +232,17 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDisplayMode(
|
|||||||
D3DDISPLAYMODE *pMode)
|
D3DDISPLAYMODE *pMode)
|
||||||
{
|
{
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_side_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
return ensure_gfdm_hidden_side_swapchain(iSwapChain)->GetDisplayMode(pMode);
|
||||||
|
}
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_small_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
CHECK_RESULT(pReal->GetDisplayMode(GFDM_NATIVE_SMALL_SWAPCHAIN, pMode));
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->GetDisplayMode(iSwapChain, pMode));
|
CHECK_RESULT(pReal->GetDisplayMode(iSwapChain, pMode));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +284,51 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
|
|||||||
{
|
{
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
|
|
||||||
|
if (pPresentationParameters == nullptr || ppSwapChain == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_gfdm_two_head_exclusive()) {
|
||||||
|
if (pPresentationParameters->BackBufferWidth == 800) {
|
||||||
|
return GetSwapChain(gfdm_logical_small_swapchain, ppSwapChain);
|
||||||
|
}
|
||||||
|
if (pPresentationParameters->BackBufferWidth != 1080) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: rejecting unexpected additional swap chain {}x{}",
|
||||||
|
pPresentationParameters->BackBufferWidth,
|
||||||
|
pPresentationParameters->BackBufferHeight);
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t index = 3;
|
||||||
|
for (UINT logical_swapchain = 1;
|
||||||
|
logical_swapchain < GFDM_LOGICAL_HEAD_COUNT;
|
||||||
|
logical_swapchain++)
|
||||||
|
{
|
||||||
|
if (!is_gfdm_logical_side_swapchain(logical_swapchain)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const size_t candidate =
|
||||||
|
gfdm_hidden_side_swapchain_slot(logical_swapchain);
|
||||||
|
if (fake_sub_swapchain[candidate] == nullptr) {
|
||||||
|
index = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index == 3) {
|
||||||
|
return D3DERR_OUTOFVIDEOMEMORY;
|
||||||
|
}
|
||||||
|
fake_sub_swapchain[index] = new FakeIDirect3DSwapChain9(
|
||||||
|
this,
|
||||||
|
pPresentationParameters,
|
||||||
|
is_d3d9ex,
|
||||||
|
true);
|
||||||
|
fake_sub_swapchain[index]->AddRef();
|
||||||
|
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[index]);
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
int index = 0;
|
int index = 0;
|
||||||
bool create_swap_chain = false;
|
bool create_swap_chain = false;
|
||||||
bool create_fake_swap_chain = false;
|
bool create_fake_swap_chain = false;
|
||||||
@@ -266,6 +342,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
|
|||||||
// SMALL (subscreen)
|
// SMALL (subscreen)
|
||||||
create_swap_chain = true;
|
create_swap_chain = true;
|
||||||
index = 0;
|
index = 0;
|
||||||
|
|
||||||
} else if (pPresentationParameters->BackBufferWidth == 1080) {
|
} else if (pPresentationParameters->BackBufferWidth == 1080) {
|
||||||
// LEFT/RIGHT
|
// LEFT/RIGHT
|
||||||
create_swap_chain = true;
|
create_swap_chain = true;
|
||||||
@@ -330,6 +407,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
|
|||||||
{
|
{
|
||||||
WRAP_VERBOSE_FMT("GetSwapChain({})", iSwapChain);
|
WRAP_VERBOSE_FMT("GetSwapChain({})", iSwapChain);
|
||||||
|
|
||||||
|
if (ppSwapChain == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
if (iSwapChain == 0) {
|
if (iSwapChain == 0) {
|
||||||
if (!main_swapchain) {
|
if (!main_swapchain) {
|
||||||
HRESULT ret = pReal->GetSwapChain(iSwapChain, ppSwapChain);
|
HRESULT ret = pReal->GetSwapChain(iSwapChain, ppSwapChain);
|
||||||
@@ -339,7 +420,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
main_swapchain = new WrappedIDirect3DSwapChain9(this, *ppSwapChain);
|
main_swapchain = new WrappedIDirect3DSwapChain9(
|
||||||
|
this,
|
||||||
|
*ppSwapChain,
|
||||||
|
is_gfdm_two_head_exclusive()
|
||||||
|
? WrappedIDirect3DSwapChain9::NativeGroupHead::Main
|
||||||
|
: WrappedIDirect3DSwapChain9::NativeGroupHead::Unknown);
|
||||||
}
|
}
|
||||||
|
|
||||||
main_swapchain->AddRef();
|
main_swapchain->AddRef();
|
||||||
@@ -349,6 +435,39 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
|
|||||||
return D3D_OK;
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (is_gfdm_two_head_exclusive()) {
|
||||||
|
if (is_gfdm_logical_small_swapchain(iSwapChain)) {
|
||||||
|
if (!sub_swapchain[0]) {
|
||||||
|
IDirect3DSwapChain9 *real_swapchain = nullptr;
|
||||||
|
HRESULT ret = pReal->GetSwapChain(
|
||||||
|
GFDM_NATIVE_SMALL_SWAPCHAIN,
|
||||||
|
&real_swapchain);
|
||||||
|
if (FAILED(ret)) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
sub_swapchain[0] = new WrappedIDirect3DSwapChain9(
|
||||||
|
this,
|
||||||
|
real_swapchain,
|
||||||
|
WrappedIDirect3DSwapChain9::NativeGroupHead::Small);
|
||||||
|
sub_swapchain[0]->should_run_hooks = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub_swapchain[0]->AddRef();
|
||||||
|
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(sub_swapchain[0]);
|
||||||
|
graphics_screens_register(iSwapChain);
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
if (is_gfdm_logical_side_swapchain(iSwapChain)) {
|
||||||
|
FakeIDirect3DSwapChain9 *swapchain =
|
||||||
|
ensure_gfdm_hidden_side_swapchain(iSwapChain);
|
||||||
|
swapchain->AddRef();
|
||||||
|
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(swapchain);
|
||||||
|
graphics_screens_register(iSwapChain);
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
bool swap = false;
|
bool swap = false;
|
||||||
if (iSwapChain == 1 && avs::game::is_model({"LDJ", "KFC", "M39"})) {
|
if (iSwapChain == 1 && avs::game::is_model({"LDJ", "KFC", "M39"})) {
|
||||||
swap = true;
|
swap = true;
|
||||||
@@ -385,6 +504,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
|
|||||||
UINT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetNumberOfSwapChains() {
|
UINT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetNumberOfSwapChains() {
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
|
|
||||||
|
if (is_gfdm_two_head_exclusive()) {
|
||||||
|
return GFDM_LOGICAL_HEAD_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
UINT n = pReal->GetNumberOfSwapChains();
|
UINT n = pReal->GetNumberOfSwapChains();
|
||||||
|
|
||||||
if (sub_swapchain[0] && avs::game::is_model({"LDJ", "KFC", "M39"})) {
|
if (sub_swapchain[0] && avs::game::is_model({"LDJ", "KFC", "M39"})) {
|
||||||
@@ -448,6 +571,24 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetBackBuffer(
|
|||||||
IDirect3DSurface9 **ppBackBuffer)
|
IDirect3DSurface9 **ppBackBuffer)
|
||||||
{
|
{
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_side_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
return ensure_gfdm_hidden_side_swapchain(iSwapChain)->GetBackBuffer(
|
||||||
|
iBackBuffer,
|
||||||
|
Type,
|
||||||
|
ppBackBuffer);
|
||||||
|
}
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_small_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
CHECK_RESULT(pReal->GetBackBuffer(
|
||||||
|
GFDM_NATIVE_SMALL_SWAPCHAIN,
|
||||||
|
iBackBuffer,
|
||||||
|
Type,
|
||||||
|
ppBackBuffer));
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->GetBackBuffer(iSwapChain, iBackBuffer, Type, ppBackBuffer));
|
CHECK_RESULT(pReal->GetBackBuffer(iSwapChain, iBackBuffer, Type, ppBackBuffer));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,6 +597,20 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetRasterStatus(
|
|||||||
D3DRASTER_STATUS *pRasterStatus)
|
D3DRASTER_STATUS *pRasterStatus)
|
||||||
{
|
{
|
||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_side_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
return ensure_gfdm_hidden_side_swapchain(iSwapChain)->GetRasterStatus(
|
||||||
|
pRasterStatus);
|
||||||
|
}
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_small_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
CHECK_RESULT(pReal->GetRasterStatus(
|
||||||
|
GFDM_NATIVE_SMALL_SWAPCHAIN,
|
||||||
|
pRasterStatus));
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->GetRasterStatus(iSwapChain, pRasterStatus));
|
CHECK_RESULT(pReal->GetRasterStatus(iSwapChain, pRasterStatus));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,6 +627,18 @@ void STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetGammaRamp(
|
|||||||
const D3DGAMMARAMP *pRamp)
|
const D3DGAMMARAMP *pRamp)
|
||||||
{
|
{
|
||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_side_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_small_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
pReal->SetGammaRamp(GFDM_NATIVE_SMALL_SWAPCHAIN, Flags, pRamp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
pReal->SetGammaRamp(iSwapChain, Flags, pRamp);
|
pReal->SetGammaRamp(iSwapChain, Flags, pRamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,6 +647,21 @@ void STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetGammaRamp(
|
|||||||
D3DGAMMARAMP *pRamp)
|
D3DGAMMARAMP *pRamp)
|
||||||
{
|
{
|
||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_side_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
if (pRamp != nullptr) {
|
||||||
|
*pRamp = {};
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_small_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
pReal->GetGammaRamp(GFDM_NATIVE_SMALL_SWAPCHAIN, pRamp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
pReal->GetGammaRamp(iSwapChain, pRamp);
|
pReal->GetGammaRamp(iSwapChain, pRamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,6 +862,19 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetFrontBufferData(
|
|||||||
IDirect3DSurface9 *pDestSurface)
|
IDirect3DSurface9 *pDestSurface)
|
||||||
{
|
{
|
||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_side_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_small_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
CHECK_RESULT(pReal->GetFrontBufferData(
|
||||||
|
GFDM_NATIVE_SMALL_SWAPCHAIN,
|
||||||
|
pDestSurface));
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->GetFrontBufferData(iSwapChain, pDestSurface));
|
CHECK_RESULT(pReal->GetFrontBufferData(iSwapChain, pDestSurface));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1044,7 +1239,36 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetViewport(
|
|||||||
D3DVIEWPORT9 *pViewport)
|
D3DVIEWPORT9 *pViewport)
|
||||||
{
|
{
|
||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
CHECK_RESULT(pReal->GetViewport(pViewport));
|
const auto result = pReal->GetViewport(pViewport);
|
||||||
|
|
||||||
|
// SDVX landscape mode
|
||||||
|
// without this, the game calculates the camera angle incorrectly and
|
||||||
|
// ends up with zoomed out view of the lanes
|
||||||
|
const auto landscape_width = GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ?
|
||||||
|
GRAPHICS_FS_CUSTOM_RESOLUTION.value().first : GRAPHICS_FS_ORIGINAL_HEIGHT;
|
||||||
|
const auto landscape_height = GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ?
|
||||||
|
GRAPHICS_FS_CUSTOM_RESOLUTION.value().second : GRAPHICS_FS_ORIGINAL_WIDTH;
|
||||||
|
if (SUCCEEDED(result) &&
|
||||||
|
GRAPHICS_FS_ORIENTATION_SWAP &&
|
||||||
|
avs::game::is_model("KFC") &&
|
||||||
|
pViewport != nullptr &&
|
||||||
|
pViewport->Width == landscape_width &&
|
||||||
|
pViewport->Height == landscape_height) {
|
||||||
|
|
||||||
|
static std::once_flag log_once;
|
||||||
|
std::call_once(log_once, [pViewport]() {
|
||||||
|
log_info(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"SDVX landscape viewport fix: {}x{} => {}x{}",
|
||||||
|
pViewport->Width,
|
||||||
|
pViewport->Height,
|
||||||
|
pViewport->Height,
|
||||||
|
pViewport->Width);
|
||||||
|
});
|
||||||
|
std::swap(pViewport->Width, pViewport->Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetMaterial(
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetMaterial(
|
||||||
@@ -1301,6 +1525,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawPrimitive(
|
|||||||
UINT PrimitiveCount)
|
UINT PrimitiveCount)
|
||||||
{
|
{
|
||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
|
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
CHECK_RESULT(pReal->DrawPrimitive(PrimitiveType, StartVertex, PrimitiveCount));
|
CHECK_RESULT(pReal->DrawPrimitive(PrimitiveType, StartVertex, PrimitiveCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1313,6 +1540,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawIndexedPrimitive(
|
|||||||
UINT PrimitiveCount)
|
UINT PrimitiveCount)
|
||||||
{
|
{
|
||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
|
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
CHECK_RESULT(pReal->DrawIndexedPrimitive(
|
CHECK_RESULT(pReal->DrawIndexedPrimitive(
|
||||||
PrimitiveType, BaseVertexIndex,
|
PrimitiveType, BaseVertexIndex,
|
||||||
MinVertexIndex, NumVertices,
|
MinVertexIndex, NumVertices,
|
||||||
@@ -1328,6 +1558,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawPrimitiveUP(
|
|||||||
WRAP_DEBUG_FMT("DrawPrimitiveUP({}, {}, {}, {})",
|
WRAP_DEBUG_FMT("DrawPrimitiveUP({}, {}, {}, {})",
|
||||||
PrimitiveType, PrimitiveCount,
|
PrimitiveType, PrimitiveCount,
|
||||||
fmt::ptr(pVertexStreamZeroData), VertexStreamZeroStride);
|
fmt::ptr(pVertexStreamZeroData), VertexStreamZeroStride);
|
||||||
|
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
CHECK_RESULT(pReal->DrawPrimitiveUP(
|
CHECK_RESULT(pReal->DrawPrimitiveUP(
|
||||||
PrimitiveType, PrimitiveCount,
|
PrimitiveType, PrimitiveCount,
|
||||||
pVertexStreamZeroData, VertexStreamZeroStride));
|
pVertexStreamZeroData, VertexStreamZeroStride));
|
||||||
@@ -1344,6 +1577,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawIndexedPrimitiveUP(
|
|||||||
UINT VertexStreamZeroStride)
|
UINT VertexStreamZeroStride)
|
||||||
{
|
{
|
||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
|
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
CHECK_RESULT(pReal->DrawIndexedPrimitiveUP(
|
CHECK_RESULT(pReal->DrawIndexedPrimitiveUP(
|
||||||
PrimitiveType, MinVertexIndex, NumVertices, PrimitiveCount, pIndexData,
|
PrimitiveType, MinVertexIndex, NumVertices, PrimitiveCount, pIndexData,
|
||||||
IndexDataFormat, pVertexStreamZeroData, VertexStreamZeroStride));
|
IndexDataFormat, pVertexStreamZeroData, VertexStreamZeroStride));
|
||||||
@@ -1403,7 +1639,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateVertexShader(
|
|||||||
IDirect3DVertexShader9 **ppShader)
|
IDirect3DVertexShader9 **ppShader)
|
||||||
{
|
{
|
||||||
WRAP_VERBOSE_FMT("CreateVertexShader({})", fmt::ptr(pFunction));
|
WRAP_VERBOSE_FMT("CreateVertexShader({})", fmt::ptr(pFunction));
|
||||||
CHECK_RESULT(pReal->CreateVertexShader(pFunction, ppShader));
|
HRESULT ret = pReal->CreateVertexShader(pFunction, ppShader);
|
||||||
|
if (SUCCEEDED(ret) && ppShader != nullptr) {
|
||||||
|
d3d9_live2d::on_create_vertex_shader(*ppShader, pFunction);
|
||||||
|
}
|
||||||
|
if (GRAPHICS_LOG_HRESULT && FAILED(ret)) [[unlikely]] {
|
||||||
|
log_warning("graphics::d3d9", "{} failed, hr={}", __FUNCTION__, FMT_HRESULT(ret));
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetVertexShader(
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetVertexShader(
|
||||||
@@ -1411,6 +1654,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetVertexShader(
|
|||||||
{
|
{
|
||||||
WRAP_DEBUG_FMT("SetVertexShader({})", fmt::ptr(pShader));
|
WRAP_DEBUG_FMT("SetVertexShader({})", fmt::ptr(pShader));
|
||||||
|
|
||||||
|
d3d9_live2d::on_set_vertex_shader(pShader);
|
||||||
|
|
||||||
#ifndef SPICE64
|
#ifndef SPICE64
|
||||||
|
|
||||||
// diagonal line fix
|
// diagonal line fix
|
||||||
@@ -1557,13 +1802,21 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreatePixelShader(
|
|||||||
IDirect3DPixelShader9 **ppShader)
|
IDirect3DPixelShader9 **ppShader)
|
||||||
{
|
{
|
||||||
WRAP_VERBOSE_FMT("CreatePixelShader({})", fmt::ptr(pFunction));
|
WRAP_VERBOSE_FMT("CreatePixelShader({})", fmt::ptr(pFunction));
|
||||||
CHECK_RESULT(pReal->CreatePixelShader(pFunction, ppShader));
|
HRESULT ret = pReal->CreatePixelShader(pFunction, ppShader);
|
||||||
|
if (SUCCEEDED(ret) && ppShader != nullptr) {
|
||||||
|
d3d9_live2d::on_create_pixel_shader(*ppShader, pFunction);
|
||||||
|
}
|
||||||
|
if (GRAPHICS_LOG_HRESULT && FAILED(ret)) [[unlikely]] {
|
||||||
|
log_warning("graphics::d3d9", "{} failed, hr={}", __FUNCTION__, FMT_HRESULT(ret));
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetPixelShader(
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetPixelShader(
|
||||||
IDirect3DPixelShader9 *pShader)
|
IDirect3DPixelShader9 *pShader)
|
||||||
{
|
{
|
||||||
WRAP_DEBUG_FMT("SetPixelShader({})", fmt::ptr(pShader));
|
WRAP_DEBUG_FMT("SetPixelShader({})", fmt::ptr(pShader));
|
||||||
|
d3d9_live2d::on_set_pixel_shader(pShader);
|
||||||
CHECK_RESULT(pReal->SetPixelShader(pShader));
|
CHECK_RESULT(pReal->SetPixelShader(pShader));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1745,6 +1998,18 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::WaitForVBlank(
|
|||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
|
|
||||||
assert(is_d3d9ex);
|
assert(is_d3d9ex);
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_side_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_small_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->WaitForVBlank(
|
||||||
|
GFDM_NATIVE_SMALL_SWAPCHAIN));
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->WaitForVBlank(iSwapChain));
|
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->WaitForVBlank(iSwapChain));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1848,6 +2113,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
|
|||||||
|
|
||||||
log_misc("graphics::d3d9", "WrappedIDirect3DDevice9::ResetEx");
|
log_misc("graphics::d3d9", "WrappedIDirect3DDevice9::ResetEx");
|
||||||
|
|
||||||
|
if (is_gfdm_two_head_exclusive()) {
|
||||||
|
gfdm_disarm_present_mode_recovery();
|
||||||
|
if (pPresentationParameters == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
release_gfdm_hidden_side_swapchains();
|
||||||
|
}
|
||||||
|
|
||||||
if (GRAPHICS_WINDOWED) {
|
if (GRAPHICS_WINDOWED) {
|
||||||
if (pPresentationParameters) {
|
if (pPresentationParameters) {
|
||||||
pPresentationParameters->Windowed = true;
|
pPresentationParameters->Windowed = true;
|
||||||
@@ -1864,13 +2137,115 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GfdmTwoHeadDeviceState gfdm_parameters(
|
||||||
|
pPresentationParameters,
|
||||||
|
pFullscreenDisplayMode,
|
||||||
|
gfdm_logical_small_swapchain);
|
||||||
|
if (is_gfdm_two_head_exclusive()) {
|
||||||
|
HRESULT result = graphics_d3d9_gfdm_select_two_head_group_parameters(
|
||||||
|
pPresentationParameters,
|
||||||
|
pFullscreenDisplayMode,
|
||||||
|
gfdm_parameters.native_presentation_parameters.data(),
|
||||||
|
pFullscreenDisplayMode != nullptr
|
||||||
|
? gfdm_parameters.native_fullscreen_display_modes.data()
|
||||||
|
: nullptr,
|
||||||
|
&gfdm_parameters.logical_small_swapchain,
|
||||||
|
"ResetEx");
|
||||||
|
if (FAILED(result)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
set_gfdm_logical_group_parameters(pPresentationParameters);
|
||||||
|
gfdm_parameters.use_native_parameters();
|
||||||
|
if (gfdm_parameters.native_presentation_parameters[0].Windowed
|
||||||
|
|| gfdm_parameters.native_presentation_parameters[1].Windowed)
|
||||||
|
{
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
if (GRAPHICS_FORCE_REFRESH_SUB.has_value()) {
|
||||||
|
gfdm_parameters.native_presentation_parameters[1]
|
||||||
|
.FullScreen_RefreshRateInHz =
|
||||||
|
GRAPHICS_FORCE_REFRESH_SUB.value();
|
||||||
|
if (gfdm_parameters.fullscreen_display_modes != nullptr) {
|
||||||
|
gfdm_parameters.fullscreen_display_modes[1].RefreshRate =
|
||||||
|
GRAPHICS_FORCE_REFRESH_SUB.value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = graphics_d3d9_gfdm_remap_two_head_group_parameters(
|
||||||
|
gfdm_parameters.native_presentation_parameters.data(),
|
||||||
|
gfdm_parameters.fullscreen_display_modes,
|
||||||
|
"ResetEx");
|
||||||
|
if (FAILED(result)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (!graphics_gitadora_prepare_two_head_device_window(
|
||||||
|
gfdm_parameters.native_presentation_parameters[1].hDeviceWindow,
|
||||||
|
nullptr,
|
||||||
|
gfdm_parameters.native_presentation_parameters[1].BackBufferWidth,
|
||||||
|
gfdm_parameters.native_presentation_parameters[1].BackBufferHeight))
|
||||||
|
{
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
if (gfdm_parameters.fullscreen_display_modes != nullptr) {
|
||||||
|
IDirect3D9 *raw_d3d = nullptr;
|
||||||
|
IDirect3D9Ex *raw_d3d_ex = nullptr;
|
||||||
|
D3DDEVICE_CREATION_PARAMETERS creation {};
|
||||||
|
HRESULT raw_result = pReal->GetDirect3D(&raw_d3d);
|
||||||
|
if (SUCCEEDED(raw_result) && raw_d3d != nullptr) {
|
||||||
|
raw_result = raw_d3d->QueryInterface(IID_PPV_ARGS(&raw_d3d_ex));
|
||||||
|
}
|
||||||
|
if (SUCCEEDED(raw_result)) {
|
||||||
|
raw_result = pReal->GetCreationParameters(&creation);
|
||||||
|
}
|
||||||
|
if (SUCCEEDED(raw_result) && raw_d3d_ex != nullptr) {
|
||||||
|
graphics_d3d9_gfdm_align_two_head_refresh_to_desktop(
|
||||||
|
raw_d3d_ex,
|
||||||
|
creation.AdapterOrdinal,
|
||||||
|
gfdm_parameters.native_presentation_parameters.data(),
|
||||||
|
gfdm_parameters.fullscreen_display_modes,
|
||||||
|
"ResetEx");
|
||||||
|
}
|
||||||
|
if (raw_d3d_ex != nullptr) {
|
||||||
|
raw_d3d_ex->Release();
|
||||||
|
}
|
||||||
|
if (raw_d3d != nullptr) {
|
||||||
|
raw_d3d->Release();
|
||||||
|
}
|
||||||
|
gfdm_parameters.recovery_parameters =
|
||||||
|
gfdm_parameters.native_presentation_parameters;
|
||||||
|
gfdm_parameters.recovery_modes =
|
||||||
|
gfdm_parameters.native_fullscreen_display_modes;
|
||||||
|
gfdm_parameters.recovery_candidate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// reset overlay
|
// reset overlay
|
||||||
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal)) {
|
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal)) {
|
||||||
overlay::OVERLAY->reset_invalidate();
|
overlay::OVERLAY->reset_invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
HRESULT res = static_cast<IDirect3DDevice9Ex *>(pReal)->ResetEx(
|
HRESULT res = static_cast<IDirect3DDevice9Ex *>(pReal)->ResetEx(
|
||||||
pPresentationParameters, pFullscreenDisplayMode);
|
gfdm_parameters.presentation_parameters,
|
||||||
|
gfdm_parameters.fullscreen_display_modes);
|
||||||
|
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& SUCCEEDED(res)
|
||||||
|
&& gfdm_parameters.recovery_candidate)
|
||||||
|
{
|
||||||
|
gfdm_arm_present_mode_recovery(
|
||||||
|
gfdm_parameters.recovery_parameters.data(),
|
||||||
|
gfdm_parameters.recovery_modes.data());
|
||||||
|
}
|
||||||
|
if (is_gfdm_two_head_exclusive() && SUCCEEDED(res)) {
|
||||||
|
gfdm_logical_small_swapchain = gfdm_parameters.logical_small_swapchain;
|
||||||
|
pPresentationParameters[0] = gfdm_parameters.presentation_parameters[0];
|
||||||
|
pPresentationParameters[gfdm_parameters.logical_small_swapchain] =
|
||||||
|
gfdm_parameters.presentation_parameters[1];
|
||||||
|
if (pFullscreenDisplayMode != nullptr) {
|
||||||
|
pFullscreenDisplayMode[0] = gfdm_parameters.fullscreen_display_modes[0];
|
||||||
|
pFullscreenDisplayMode[gfdm_parameters.logical_small_swapchain] =
|
||||||
|
gfdm_parameters.fullscreen_display_modes[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// recreate overlay
|
// recreate overlay
|
||||||
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal) && SUCCEEDED(res)) {
|
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal) && SUCCEEDED(res)) {
|
||||||
@@ -1888,6 +2263,22 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetDisplayModeEx(
|
|||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
|
|
||||||
assert(is_d3d9ex);
|
assert(is_d3d9ex);
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_side_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
return ensure_gfdm_hidden_side_swapchain(iSwapChain)->GetDisplayModeEx(
|
||||||
|
pMode,
|
||||||
|
pRotation);
|
||||||
|
}
|
||||||
|
if (is_gfdm_two_head_exclusive()
|
||||||
|
&& is_gfdm_logical_small_swapchain(iSwapChain))
|
||||||
|
{
|
||||||
|
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->GetDisplayModeEx(
|
||||||
|
GFDM_NATIVE_SMALL_SWAPCHAIN,
|
||||||
|
pMode,
|
||||||
|
pRotation));
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->GetDisplayModeEx(
|
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->GetDisplayModeEx(
|
||||||
iSwapChain, pMode, pRotation));
|
iSwapChain, pMode, pRotation));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
#include <initguid.h>
|
#include <initguid.h>
|
||||||
#include <d3d9.h>
|
#include <d3d9.h>
|
||||||
@@ -40,23 +42,54 @@ static const GUID IID_WrappedIDirect3DDevice9 = {
|
|||||||
void SurfaceHook(IDirect3DDevice9 *pReal);
|
void SurfaceHook(IDirect3DDevice9 *pReal);
|
||||||
|
|
||||||
struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
|
struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
|
||||||
explicit WrappedIDirect3DDevice9(HWND hFocusWindow, IDirect3DDevice9 *orig)
|
explicit WrappedIDirect3DDevice9(
|
||||||
: hFocusWindow(hFocusWindow), pReal(orig), is_d3d9ex(false) {
|
HWND hFocusWindow,
|
||||||
|
IDirect3DDevice9 *orig,
|
||||||
|
UINT gfdm_logical_small_swapchain = 2,
|
||||||
|
IDirect3D9 *gfdm_parent_d3d = nullptr,
|
||||||
|
const D3DPRESENT_PARAMETERS *gfdm_logical_group_parameters = nullptr)
|
||||||
|
: hFocusWindow(hFocusWindow),
|
||||||
|
pReal(orig),
|
||||||
|
is_d3d9ex(false),
|
||||||
|
gfdm_logical_small_swapchain(gfdm_logical_small_swapchain),
|
||||||
|
gfdm_parent_d3d(gfdm_parent_d3d) {
|
||||||
|
if (this->gfdm_parent_d3d != nullptr) {
|
||||||
|
this->gfdm_parent_d3d->AddRef();
|
||||||
|
}
|
||||||
IDirect3DDevice9Ex *device = nullptr;
|
IDirect3DDevice9Ex *device = nullptr;
|
||||||
|
|
||||||
// attempt to upgrade handle
|
// attempt to upgrade handle
|
||||||
if (SUCCEEDED(this->QueryInterface(IID_PPV_ARGS(&device))) && device != nullptr) {
|
if (SUCCEEDED(this->QueryInterface(IID_PPV_ARGS(&device))) && device != nullptr) {
|
||||||
device->Release();
|
device->Release();
|
||||||
}
|
}
|
||||||
|
set_gfdm_logical_group_parameters(gfdm_logical_group_parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
explicit WrappedIDirect3DDevice9(HWND hFocusWindow, IDirect3DDevice9Ex *orig)
|
explicit WrappedIDirect3DDevice9(
|
||||||
: hFocusWindow(hFocusWindow), pReal(orig), is_d3d9ex(true) {}
|
HWND hFocusWindow,
|
||||||
|
IDirect3DDevice9Ex *orig,
|
||||||
|
UINT gfdm_logical_small_swapchain = 2,
|
||||||
|
IDirect3D9 *gfdm_parent_d3d = nullptr,
|
||||||
|
const D3DPRESENT_PARAMETERS *gfdm_logical_group_parameters = nullptr)
|
||||||
|
: hFocusWindow(hFocusWindow),
|
||||||
|
pReal(orig),
|
||||||
|
is_d3d9ex(true),
|
||||||
|
gfdm_logical_small_swapchain(gfdm_logical_small_swapchain),
|
||||||
|
gfdm_parent_d3d(gfdm_parent_d3d) {
|
||||||
|
if (this->gfdm_parent_d3d != nullptr) {
|
||||||
|
this->gfdm_parent_d3d->AddRef();
|
||||||
|
}
|
||||||
|
set_gfdm_logical_group_parameters(gfdm_logical_group_parameters);
|
||||||
|
}
|
||||||
|
|
||||||
WrappedIDirect3DDevice9(const WrappedIDirect3DDevice9 &) = delete;
|
WrappedIDirect3DDevice9(const WrappedIDirect3DDevice9 &) = delete;
|
||||||
WrappedIDirect3DDevice9 &operator=(const WrappedIDirect3DDevice9 &) = delete;
|
WrappedIDirect3DDevice9 &operator=(const WrappedIDirect3DDevice9 &) = delete;
|
||||||
|
|
||||||
virtual ~WrappedIDirect3DDevice9() = default;
|
virtual ~WrappedIDirect3DDevice9() {
|
||||||
|
if (gfdm_parent_d3d != nullptr) {
|
||||||
|
gfdm_parent_d3d->Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#pragma region IUnknown
|
#pragma region IUnknown
|
||||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
|
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
|
||||||
@@ -201,6 +234,27 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
|
|||||||
virtual HRESULT STDMETHODCALLTYPE GetDisplayModeEx(UINT iSwapChain, D3DDISPLAYMODEEX *pMode, D3DDISPLAYROTATION *pRotation) override;
|
virtual HRESULT STDMETHODCALLTYPE GetDisplayModeEx(UINT iSwapChain, D3DDISPLAYMODEEX *pMode, D3DDISPLAYROTATION *pRotation) override;
|
||||||
#pragma endregion
|
#pragma endregion
|
||||||
|
|
||||||
|
bool is_gfdm_two_head_exclusive() const;
|
||||||
|
bool is_gfdm_logical_small_swapchain(UINT swapchain) const;
|
||||||
|
bool is_gfdm_logical_side_swapchain(UINT swapchain) const;
|
||||||
|
size_t gfdm_hidden_side_swapchain_slot(UINT swapchain) const;
|
||||||
|
void set_gfdm_logical_group_parameters(
|
||||||
|
const D3DPRESENT_PARAMETERS *presentation_parameters);
|
||||||
|
FakeIDirect3DSwapChain9 *ensure_gfdm_hidden_side_swapchain(UINT iSwapChain);
|
||||||
|
void release_gfdm_hidden_side_swapchains();
|
||||||
|
|
||||||
|
enum class GfdmPresentModeRecoveryResult {
|
||||||
|
NotAttempted,
|
||||||
|
ReadyToRetry,
|
||||||
|
Failed,
|
||||||
|
};
|
||||||
|
void gfdm_disarm_present_mode_recovery();
|
||||||
|
void gfdm_arm_present_mode_recovery(
|
||||||
|
const D3DPRESENT_PARAMETERS *native_presentation_parameters,
|
||||||
|
const D3DDISPLAYMODEEX *native_fullscreen_display_modes);
|
||||||
|
GfdmPresentModeRecoveryResult gfdm_recover_present_mode_change(
|
||||||
|
HRESULT *failure_result);
|
||||||
|
|
||||||
HWND const hFocusWindow;
|
HWND const hFocusWindow;
|
||||||
IDirect3DDevice9 *pReal;
|
IDirect3DDevice9 *pReal;
|
||||||
bool is_d3d9ex = false;
|
bool is_d3d9ex = false;
|
||||||
@@ -211,5 +265,17 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
|
|||||||
WrappedIDirect3DSwapChain9 *sub_swapchain[3] = { nullptr, nullptr, nullptr };
|
WrappedIDirect3DSwapChain9 *sub_swapchain[3] = { nullptr, nullptr, nullptr };
|
||||||
FakeIDirect3DSwapChain9 *fake_sub_swapchain[3] = { nullptr, nullptr, nullptr };
|
FakeIDirect3DSwapChain9 *fake_sub_swapchain[3] = { nullptr, nullptr, nullptr };
|
||||||
|
|
||||||
|
UINT gfdm_logical_small_swapchain = 2;
|
||||||
|
std::array<D3DPRESENT_PARAMETERS, 4> gfdm_logical_group_parameters {};
|
||||||
|
bool gfdm_logical_group_parameters_valid = false;
|
||||||
|
IDirect3D9 *gfdm_parent_d3d = nullptr;
|
||||||
|
|
||||||
|
std::mutex gfdm_recovery_mutex;
|
||||||
|
std::array<D3DPRESENT_PARAMETERS, 2> gfdm_recovery_parameters {};
|
||||||
|
std::array<D3DDISPLAYMODEEX, 2> gfdm_recovery_modes {};
|
||||||
|
bool gfdm_recovery_armed = false;
|
||||||
|
std::atomic_bool gfdm_recovery_consumed {false};
|
||||||
|
const DWORD gfdm_device_creation_thread_id = GetCurrentThreadId();
|
||||||
|
|
||||||
IDirect3DVertexShader9 *vertex_shader = nullptr;
|
IDirect3DVertexShader9 *vertex_shader = nullptr;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,7 +59,12 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::Present(const RECT *pSourceRe
|
|||||||
log_misc("graphics::d3d9", "FakeIDirect3DSwapChain9::Present");
|
log_misc("graphics::d3d9", "FakeIDirect3DSwapChain9::Present");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!is_hidden) {
|
||||||
return D3DERR_INVALIDCALL;
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
present_count.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetFrontBufferData(IDirect3DSurface9 *pDestSurface) {
|
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetFrontBufferData(IDirect3DSurface9 *pDestSurface) {
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
@@ -86,11 +91,32 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetBackBuffer(UINT iBackBuffe
|
|||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetRasterStatus(D3DRASTER_STATUS *pRasterStatus) {
|
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetRasterStatus(D3DRASTER_STATUS *pRasterStatus) {
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
|
if (!is_hidden) {
|
||||||
return D3DERR_INVALIDCALL;
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pRasterStatus == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
*pRasterStatus = {};
|
||||||
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDisplayMode(D3DDISPLAYMODE *pMode) {
|
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDisplayMode(D3DDISPLAYMODE *pMode) {
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
|
if (!is_hidden) {
|
||||||
return D3DERR_INVALIDCALL;
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pMode == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
pMode->Width = present_params.BackBufferWidth;
|
||||||
|
pMode->Height = present_params.BackBufferHeight;
|
||||||
|
pMode->RefreshRate = present_params.FullScreen_RefreshRateInHz;
|
||||||
|
pMode->Format = present_params.BackBufferFormat;
|
||||||
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDevice(IDirect3DDevice9 **ppDevice) {
|
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDevice(IDirect3DDevice9 **ppDevice) {
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
@@ -108,17 +134,46 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetPresentParameters(
|
|||||||
D3DPRESENT_PARAMETERS *pPresentationParameters)
|
D3DPRESENT_PARAMETERS *pPresentationParameters)
|
||||||
{
|
{
|
||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
|
if (!is_hidden) {
|
||||||
return D3DERR_INVALIDCALL;
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pPresentationParameters == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
*pPresentationParameters = present_params;
|
||||||
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDirect3DSwapChain9Ex
|
// IDirect3DSwapChain9Ex
|
||||||
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetLastPresentCount(UINT *pLastPresentCount) {
|
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetLastPresentCount(UINT *pLastPresentCount) {
|
||||||
assert(is_d3d9ex);
|
assert(is_d3d9ex);
|
||||||
|
if (!is_hidden) {
|
||||||
return D3DERR_INVALIDCALL;
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pLastPresentCount == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
*pLastPresentCount = present_count.load(std::memory_order_relaxed);
|
||||||
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetPresentStats(D3DPRESENTSTATS *pPresentationStatistics) {
|
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetPresentStats(D3DPRESENTSTATS *pPresentationStatistics) {
|
||||||
assert(is_d3d9ex);
|
assert(is_d3d9ex);
|
||||||
|
if (!is_hidden) {
|
||||||
return D3DERR_INVALIDCALL;
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pPresentationStatistics == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
*pPresentationStatistics = {};
|
||||||
|
pPresentationStatistics->PresentCount =
|
||||||
|
present_count.load(std::memory_order_relaxed);
|
||||||
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDisplayModeEx(D3DDISPLAYMODEEX *pMode,
|
HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDisplayModeEx(D3DDISPLAYMODEEX *pMode,
|
||||||
D3DDISPLAYROTATION *pRotation)
|
D3DDISPLAYROTATION *pRotation)
|
||||||
@@ -126,5 +181,22 @@ HRESULT STDMETHODCALLTYPE FakeIDirect3DSwapChain9::GetDisplayModeEx(D3DDISPLAYMO
|
|||||||
WRAP_VERBOSE;
|
WRAP_VERBOSE;
|
||||||
|
|
||||||
assert(is_d3d9ex);
|
assert(is_d3d9ex);
|
||||||
|
if (!is_hidden) {
|
||||||
return D3DERR_INVALIDCALL;
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pMode == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
pMode->Size = sizeof(*pMode);
|
||||||
|
pMode->Width = present_params.BackBufferWidth;
|
||||||
|
pMode->Height = present_params.BackBufferHeight;
|
||||||
|
pMode->RefreshRate = present_params.FullScreen_RefreshRateInHz;
|
||||||
|
pMode->Format = present_params.BackBufferFormat;
|
||||||
|
pMode->ScanLineOrdering = D3DSCANLINEORDERING_PROGRESSIVE;
|
||||||
|
if (pRotation != nullptr) {
|
||||||
|
*pRotation = D3DDISPLAYROTATION_IDENTITY;
|
||||||
|
}
|
||||||
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,19 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <d3d9.h>
|
#include <d3d9.h>
|
||||||
|
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
|
||||||
struct FakeIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
|
struct FakeIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
|
||||||
FakeIDirect3DSwapChain9(IDirect3DDevice9 *pDev, D3DPRESENT_PARAMETERS *present_params, bool is_d3d9ex) :
|
FakeIDirect3DSwapChain9(
|
||||||
pDev(pDev), is_d3d9ex(is_d3d9ex)
|
IDirect3DDevice9 *pDev,
|
||||||
|
D3DPRESENT_PARAMETERS *present_params,
|
||||||
|
bool is_d3d9ex,
|
||||||
|
bool is_hidden = false) :
|
||||||
|
pDev(pDev), is_d3d9ex(is_d3d9ex), is_hidden(is_hidden)
|
||||||
{
|
{
|
||||||
// copy presentation parameters
|
// copy presentation parameters
|
||||||
memcpy(&this->present_params, present_params, sizeof(this->present_params));
|
memcpy(&this->present_params, present_params, sizeof(this->present_params));
|
||||||
@@ -78,8 +83,10 @@ struct FakeIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
|
|||||||
|
|
||||||
IDirect3DDevice9 *const pDev;
|
IDirect3DDevice9 *const pDev;
|
||||||
bool is_d3d9ex;
|
bool is_d3d9ex;
|
||||||
|
bool is_hidden;
|
||||||
|
|
||||||
std::atomic<ULONG> ref_cnt = 1;
|
std::atomic<ULONG> ref_cnt = 1;
|
||||||
|
std::atomic<UINT> present_count = 0;
|
||||||
|
|
||||||
D3DPRESENT_PARAMETERS present_params {};
|
D3DPRESENT_PARAMETERS present_params {};
|
||||||
std::vector<IDirect3DSurface9 *> render_targets;
|
std::vector<IDirect3DSurface9 *> render_targets;
|
||||||
|
|||||||
@@ -0,0 +1,715 @@
|
|||||||
|
#include "d3d9_gfdm.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include "games/gitadora/gitadora.h"
|
||||||
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
#include "d3d9_device.h"
|
||||||
|
|
||||||
|
bool gfdm_two_head_exclusive() {
|
||||||
|
return games::gitadora::is_arena_model()
|
||||||
|
&& games::gitadora::ARENA_TWO_HEAD_EXCLUSIVE
|
||||||
|
&& !GRAPHICS_WINDOWED;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT graphics_d3d9_gfdm_select_two_head_group_parameters(
|
||||||
|
const D3DPRESENT_PARAMETERS *logical_presentation_parameters,
|
||||||
|
const D3DDISPLAYMODEEX *logical_fullscreen_display_modes,
|
||||||
|
D3DPRESENT_PARAMETERS *native_presentation_parameters,
|
||||||
|
D3DDISPLAYMODEEX *native_fullscreen_display_modes,
|
||||||
|
UINT *logical_small_swapchain,
|
||||||
|
const char *operation)
|
||||||
|
{
|
||||||
|
if (!gfdm_two_head_exclusive()
|
||||||
|
|| logical_presentation_parameters == nullptr
|
||||||
|
|| native_presentation_parameters == nullptr
|
||||||
|
|| logical_small_swapchain == nullptr)
|
||||||
|
{
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
UINT small_slot = GFDM_LOGICAL_HEAD_COUNT;
|
||||||
|
for (UINT i = 1; i < GFDM_LOGICAL_HEAD_COUNT; i++) {
|
||||||
|
const auto ¶ms = logical_presentation_parameters[i];
|
||||||
|
if (params.BackBufferWidth != GFDM_SMALL_WIDTH
|
||||||
|
|| params.BackBufferHeight != GFDM_SMALL_HEIGHT)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (small_slot != GFDM_LOGICAL_HEAD_COUNT) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} found multiple SMALL slots: {} and {}",
|
||||||
|
operation,
|
||||||
|
small_slot,
|
||||||
|
i);
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
small_slot = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (small_slot == GFDM_LOGICAL_HEAD_COUNT) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} could not find an 800x1280 logical SMALL descriptor",
|
||||||
|
operation);
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logical_fullscreen_display_modes != nullptr
|
||||||
|
&& (logical_fullscreen_display_modes[small_slot].Width != GFDM_SMALL_WIDTH
|
||||||
|
|| logical_fullscreen_display_modes[small_slot].Height != GFDM_SMALL_HEIGHT))
|
||||||
|
{
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} SMALL slot {} mode is {}x{}, expected {}x{}",
|
||||||
|
operation,
|
||||||
|
small_slot,
|
||||||
|
logical_fullscreen_display_modes[small_slot].Width,
|
||||||
|
logical_fullscreen_display_modes[small_slot].Height,
|
||||||
|
GFDM_SMALL_WIDTH,
|
||||||
|
GFDM_SMALL_HEIGHT);
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
native_presentation_parameters[0] = logical_presentation_parameters[0];
|
||||||
|
native_presentation_parameters[1] = logical_presentation_parameters[small_slot];
|
||||||
|
if (logical_fullscreen_display_modes != nullptr
|
||||||
|
&& native_fullscreen_display_modes != nullptr)
|
||||||
|
{
|
||||||
|
native_fullscreen_display_modes[0] = logical_fullscreen_display_modes[0];
|
||||||
|
native_fullscreen_display_modes[1] = logical_fullscreen_display_modes[small_slot];
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} selected logical SMALL slot {} -> native group head 1{}",
|
||||||
|
operation,
|
||||||
|
small_slot,
|
||||||
|
logical_presentation_parameters[small_slot].hDeviceWindow == GFDM_SUBSCREEN_WINDOW
|
||||||
|
? " (named SMALL host)"
|
||||||
|
: "");
|
||||||
|
*logical_small_swapchain = small_slot;
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_fake_subscreen_adapter(UINT adapter) {
|
||||||
|
// MAIN and SMALL remain native D3D heads; LEFT/RIGHT are virtual.
|
||||||
|
if (gfdm_two_head_exclusive()) {
|
||||||
|
return adapter >= 2 && adapter < GFDM_LOGICAL_HEAD_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FAKE_SUBSCREEN_ADAPTER && adapter > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void get_fake_subscreen_display_mode(
|
||||||
|
UINT adapter,
|
||||||
|
D3DDISPLAYMODE *mode)
|
||||||
|
{
|
||||||
|
(void) adapter;
|
||||||
|
if (gfdm_two_head_exclusive()) {
|
||||||
|
mode->Width = GFDM_SIDE_WIDTH;
|
||||||
|
mode->Height = GFDM_SIDE_HEIGHT;
|
||||||
|
} else {
|
||||||
|
mode->Width = 1280;
|
||||||
|
mode->Height = 800;
|
||||||
|
}
|
||||||
|
mode->RefreshRate = 60;
|
||||||
|
mode->Format = D3DFMT_X8R8G8B8;
|
||||||
|
}
|
||||||
|
|
||||||
|
void get_fake_subscreen_display_mode_ex(
|
||||||
|
UINT adapter,
|
||||||
|
D3DDISPLAYMODEEX *mode)
|
||||||
|
{
|
||||||
|
D3DDISPLAYMODE legacy_mode {};
|
||||||
|
get_fake_subscreen_display_mode(adapter, &legacy_mode);
|
||||||
|
|
||||||
|
mode->Size = sizeof(*mode);
|
||||||
|
mode->Width = legacy_mode.Width;
|
||||||
|
mode->Height = legacy_mode.Height;
|
||||||
|
mode->RefreshRate = legacy_mode.RefreshRate;
|
||||||
|
mode->Format = legacy_mode.Format;
|
||||||
|
mode->ScanLineOrdering = D3DSCANLINEORDERING_PROGRESSIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT graphics_d3d9_gfdm_remap_two_head_group_parameters(
|
||||||
|
D3DPRESENT_PARAMETERS *presentation_parameters,
|
||||||
|
D3DDISPLAYMODEEX *fullscreen_display_modes,
|
||||||
|
const char *operation)
|
||||||
|
{
|
||||||
|
if (!gfdm_two_head_exclusive()) {
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
if (presentation_parameters == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto &secondary = presentation_parameters[1];
|
||||||
|
if (secondary.BackBufferWidth != GFDM_SMALL_WIDTH
|
||||||
|
|| secondary.BackBufferHeight != GFDM_SMALL_HEIGHT)
|
||||||
|
{
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} expected physical SMALL {}x{} at group slot 1, got {}x{}",
|
||||||
|
operation,
|
||||||
|
GFDM_SMALL_WIDTH,
|
||||||
|
GFDM_SMALL_HEIGHT,
|
||||||
|
secondary.BackBufferWidth,
|
||||||
|
secondary.BackBufferHeight);
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GFDM_SUBSCREEN_WINDOW == nullptr || !IsWindow(GFDM_SUBSCREEN_WINDOW)) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} cannot use the named SMALL window as physical group head 1",
|
||||||
|
operation);
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
secondary.hDeviceWindow = GFDM_SUBSCREEN_WINDOW;
|
||||||
|
|
||||||
|
if (fullscreen_display_modes != nullptr
|
||||||
|
&& (fullscreen_display_modes[1].Width != GFDM_SMALL_WIDTH
|
||||||
|
|| fullscreen_display_modes[1].Height != GFDM_SMALL_HEIGHT))
|
||||||
|
{
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} SMALL head mode is {}x{}, expected 800x1280",
|
||||||
|
operation,
|
||||||
|
fullscreen_display_modes[1].Width,
|
||||||
|
fullscreen_display_modes[1].Height);
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void graphics_d3d9_gfdm_align_two_head_refresh_to_desktop(
|
||||||
|
IDirect3D9Ex *d3d,
|
||||||
|
UINT master_adapter,
|
||||||
|
D3DPRESENT_PARAMETERS *presentation_parameters,
|
||||||
|
D3DDISPLAYMODEEX *fullscreen_display_modes,
|
||||||
|
const char *operation)
|
||||||
|
{
|
||||||
|
if (d3d == nullptr
|
||||||
|
|| presentation_parameters == nullptr
|
||||||
|
|| fullscreen_display_modes == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (UINT head = 0; head < 2; head++) {
|
||||||
|
const bool explicitly_forced =
|
||||||
|
(head == 0 && GRAPHICS_FORCE_REFRESH > 0)
|
||||||
|
|| (head == 1 && GRAPHICS_FORCE_REFRESH_SUB.has_value());
|
||||||
|
if (explicitly_forced) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
D3DDISPLAYMODEEX desktop_mode {};
|
||||||
|
desktop_mode.Size = sizeof(desktop_mode);
|
||||||
|
D3DDISPLAYROTATION rotation = D3DDISPLAYROTATION_IDENTITY;
|
||||||
|
const HRESULT result = d3d->GetAdapterDisplayModeEx(
|
||||||
|
master_adapter + head,
|
||||||
|
&desktop_mode,
|
||||||
|
&rotation);
|
||||||
|
if (FAILED(result)) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} could not query native head {} desktop refresh, hr={}",
|
||||||
|
operation,
|
||||||
|
head,
|
||||||
|
FMT_HRESULT(result));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto ¶meters = presentation_parameters[head];
|
||||||
|
if (desktop_mode.Width != parameters.BackBufferWidth
|
||||||
|
|| desktop_mode.Height != parameters.BackBufferHeight
|
||||||
|
|| desktop_mode.Format != parameters.BackBufferFormat
|
||||||
|
|| desktop_mode.RefreshRate == 0)
|
||||||
|
{
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} head {} mode differs from the desktop",
|
||||||
|
operation,
|
||||||
|
head);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameters.FullScreen_RefreshRateInHz != desktop_mode.RefreshRate
|
||||||
|
|| fullscreen_display_modes[head].RefreshRate != desktop_mode.RefreshRate)
|
||||||
|
{
|
||||||
|
log_info(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} head {} refresh {} / {} -> {} (rotation {})",
|
||||||
|
operation,
|
||||||
|
head,
|
||||||
|
parameters.FullScreen_RefreshRateInHz,
|
||||||
|
fullscreen_display_modes[head].RefreshRate,
|
||||||
|
desktop_mode.RefreshRate,
|
||||||
|
static_cast<UINT>(rotation));
|
||||||
|
presentation_parameters[head].FullScreen_RefreshRateInHz =
|
||||||
|
desktop_mode.RefreshRate;
|
||||||
|
fullscreen_display_modes[head].RefreshRate =
|
||||||
|
desktop_mode.RefreshRate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT validate_gfdm_two_head_exclusive(
|
||||||
|
IDirect3D9 *d3d,
|
||||||
|
UINT adapter,
|
||||||
|
D3DDEVTYPE device_type,
|
||||||
|
DWORD behavior_flags,
|
||||||
|
const D3DPRESENT_PARAMETERS *presentation_parameters)
|
||||||
|
{
|
||||||
|
if (!(behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head mode requires D3DCREATE_ADAPTERGROUP_DEVICE");
|
||||||
|
return D3DERR_NOTAVAILABLE;
|
||||||
|
}
|
||||||
|
if (adapter != 0) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive mode currently requires master adapter 0, game requested {}",
|
||||||
|
adapter);
|
||||||
|
return D3DERR_NOTAVAILABLE;
|
||||||
|
}
|
||||||
|
if (presentation_parameters == nullptr) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
const UINT native_adapter_count = d3d->GetAdapterCount();
|
||||||
|
if (native_adapter_count != 2) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head mode requires two native D3D9 adapters; found {}",
|
||||||
|
native_adapter_count);
|
||||||
|
return D3DERR_NOTAVAILABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
D3DCAPS9 caps {};
|
||||||
|
const HRESULT caps_result = d3d->GetDeviceCaps(adapter, device_type, &caps);
|
||||||
|
if (FAILED(caps_result)) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive mode could not query native adapter caps, hr={}",
|
||||||
|
FMT_HRESULT(caps_result));
|
||||||
|
return caps_result;
|
||||||
|
}
|
||||||
|
if (caps.MasterAdapterOrdinal != adapter
|
||||||
|
|| caps.AdapterOrdinalInGroup != 0
|
||||||
|
|| caps.NumberOfAdaptersInGroup != 2)
|
||||||
|
{
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"invalid adapter group: master={}, index={}, size={}",
|
||||||
|
caps.MasterAdapterOrdinal,
|
||||||
|
caps.AdapterOrdinalInGroup,
|
||||||
|
caps.NumberOfAdaptersInGroup);
|
||||||
|
return D3DERR_NOTAVAILABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &main = presentation_parameters[0];
|
||||||
|
const auto &small_params = presentation_parameters[1];
|
||||||
|
if (main.Windowed || small_params.Windowed) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive mode requires both group heads to be fullscreen");
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
if (small_params.BackBufferWidth != GFDM_SMALL_WIDTH
|
||||||
|
|| small_params.BackBufferHeight != GFDM_SMALL_HEIGHT)
|
||||||
|
{
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"SMALL head must be {}x{}; got {}x{}",
|
||||||
|
GFDM_SMALL_WIDTH,
|
||||||
|
GFDM_SMALL_HEIGHT,
|
||||||
|
small_params.BackBufferWidth,
|
||||||
|
small_params.BackBufferHeight);
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
if (main.EnableAutoDepthStencil != small_params.EnableAutoDepthStencil) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head mode requires matching automatic depth-stencil settings");
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
if (main.EnableAutoDepthStencil
|
||||||
|
&& (main.AutoDepthStencilFormat != small_params.AutoDepthStencilFormat
|
||||||
|
|| main.BackBufferWidth != small_params.BackBufferWidth
|
||||||
|
|| main.BackBufferHeight != small_params.BackBufferHeight
|
||||||
|
|| main.BackBufferFormat != small_params.BackBufferFormat))
|
||||||
|
{
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head mode cannot use unequal sizes with automatic depth-stencil");
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool gfdm_find_alternate_small_mode(
|
||||||
|
IDirect3D9Ex *d3d,
|
||||||
|
UINT adapter,
|
||||||
|
const D3DDISPLAYMODEEX &desired,
|
||||||
|
D3DDISPLAYMODEEX *alternate)
|
||||||
|
{
|
||||||
|
if (d3d == nullptr || alternate == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
D3DDISPLAYMODEFILTER filter {};
|
||||||
|
filter.Size = sizeof(filter);
|
||||||
|
filter.Format = desired.Format;
|
||||||
|
filter.ScanLineOrdering = D3DSCANLINEORDERING_UNKNOWN;
|
||||||
|
const UINT count = d3d->GetAdapterModeCountEx(adapter, &filter);
|
||||||
|
const bool portrait = desired.Width < desired.Height;
|
||||||
|
|
||||||
|
for (UINT index = 0; index < count; index++) {
|
||||||
|
D3DDISPLAYMODEEX mode {};
|
||||||
|
mode.Size = sizeof(mode);
|
||||||
|
if (FAILED(d3d->EnumAdapterModesEx(adapter, &filter, index, &mode))
|
||||||
|
|| mode.Width != desired.Width
|
||||||
|
|| mode.Height != desired.Height
|
||||||
|
|| mode.Format != desired.Format
|
||||||
|
|| (mode.RefreshRate == desired.RefreshRate
|
||||||
|
&& mode.ScanLineOrdering == desired.ScanLineOrdering))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
*alternate = mode;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int preserve_orientation = 1; preserve_orientation >= 0; preserve_orientation--) {
|
||||||
|
for (int preserve_refresh = 1; preserve_refresh >= 0; preserve_refresh--) {
|
||||||
|
for (UINT index = 0; index < count; index++) {
|
||||||
|
D3DDISPLAYMODEEX mode {};
|
||||||
|
mode.Size = sizeof(mode);
|
||||||
|
if (FAILED(d3d->EnumAdapterModesEx(adapter, &filter, index, &mode))
|
||||||
|
|| (mode.Width == desired.Width && mode.Height == desired.Height)
|
||||||
|
|| mode.Format != desired.Format)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (preserve_orientation && (mode.Width < mode.Height) != portrait) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (preserve_refresh && mode.RefreshRate != desired.RefreshRate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
*alternate = mode;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool gfdm_wait_for_small_mode(
|
||||||
|
HWND window,
|
||||||
|
const D3DDISPLAYMODEEX &expected)
|
||||||
|
{
|
||||||
|
const HMONITOR monitor =
|
||||||
|
window != nullptr ? MonitorFromWindow(window, MONITOR_DEFAULTTONULL) : nullptr;
|
||||||
|
MONITORINFOEXA monitor_info {};
|
||||||
|
monitor_info.cbSize = sizeof(monitor_info);
|
||||||
|
if (monitor == nullptr || !GetMonitorInfoA(monitor, &monitor_info)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (UINT poll = 0; poll < 50; poll++) {
|
||||||
|
DEVMODEA current {};
|
||||||
|
current.dmSize = sizeof(current);
|
||||||
|
if (EnumDisplaySettingsExA(
|
||||||
|
monitor_info.szDevice,
|
||||||
|
ENUM_CURRENT_SETTINGS,
|
||||||
|
¤t,
|
||||||
|
0)
|
||||||
|
&& current.dmPelsWidth == expected.Width
|
||||||
|
&& current.dmPelsHeight == expected.Height
|
||||||
|
&& (expected.RefreshRate == 0
|
||||||
|
|| current.dmDisplayFrequency == expected.RefreshRate))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Sleep(10);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT graphics_d3d9_gfdm_recover_two_head_present_mode(
|
||||||
|
IDirect3D9Ex *d3d,
|
||||||
|
IDirect3DDevice9Ex *device,
|
||||||
|
UINT master_adapter,
|
||||||
|
const D3DPRESENT_PARAMETERS *desired_parameters,
|
||||||
|
const D3DDISPLAYMODEEX *desired_modes)
|
||||||
|
{
|
||||||
|
if (d3d == nullptr
|
||||||
|
|| device == nullptr
|
||||||
|
|| desired_parameters == nullptr
|
||||||
|
|| desired_modes == nullptr)
|
||||||
|
{
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
D3DDISPLAYMODEEX alternate_small {};
|
||||||
|
if (!gfdm_find_alternate_small_mode(
|
||||||
|
d3d,
|
||||||
|
master_adapter + 1,
|
||||||
|
desired_modes[1],
|
||||||
|
&alternate_small))
|
||||||
|
{
|
||||||
|
log_warning("graphics::d3d9", "no alternate SMALL mode is available for recovery");
|
||||||
|
return D3DERR_NOTAVAILABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
D3DPRESENT_PARAMETERS temporary_parameters[2] {
|
||||||
|
desired_parameters[0],
|
||||||
|
desired_parameters[1]};
|
||||||
|
D3DDISPLAYMODEEX temporary_modes[2] {
|
||||||
|
desired_modes[0],
|
||||||
|
desired_modes[1]};
|
||||||
|
temporary_parameters[1].BackBufferWidth = alternate_small.Width;
|
||||||
|
temporary_parameters[1].BackBufferHeight = alternate_small.Height;
|
||||||
|
temporary_parameters[1].BackBufferFormat = alternate_small.Format;
|
||||||
|
temporary_parameters[1].FullScreen_RefreshRateInHz = alternate_small.RefreshRate;
|
||||||
|
temporary_modes[1] = alternate_small;
|
||||||
|
|
||||||
|
HRESULT temporary_result = device->ResetEx(temporary_parameters, temporary_modes);
|
||||||
|
const bool temporary_settled =
|
||||||
|
temporary_result == D3D_OK
|
||||||
|
&& gfdm_wait_for_small_mode(desired_parameters[1].hDeviceWindow, alternate_small);
|
||||||
|
|
||||||
|
D3DPRESENT_PARAMETERS restore_parameters[2] {
|
||||||
|
desired_parameters[0],
|
||||||
|
desired_parameters[1]};
|
||||||
|
D3DDISPLAYMODEEX restore_modes[2] {
|
||||||
|
desired_modes[0],
|
||||||
|
desired_modes[1]};
|
||||||
|
HRESULT restore_result = device->ResetEx(restore_parameters, restore_modes);
|
||||||
|
const bool restore_settled =
|
||||||
|
restore_result == D3D_OK
|
||||||
|
&& gfdm_wait_for_small_mode(desired_parameters[1].hDeviceWindow, desired_modes[1]);
|
||||||
|
|
||||||
|
if (temporary_result != D3D_OK) {
|
||||||
|
return temporary_result;
|
||||||
|
}
|
||||||
|
if (restore_result != D3D_OK) {
|
||||||
|
return restore_result;
|
||||||
|
}
|
||||||
|
if (!temporary_settled || !restore_settled) {
|
||||||
|
return D3DERR_NOTAVAILABLE;
|
||||||
|
}
|
||||||
|
return device->CheckDeviceState(desired_parameters[0].hDeviceWindow) == D3D_OK
|
||||||
|
? D3D_OK
|
||||||
|
: D3DERR_NOTAVAILABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WrappedIDirect3DDevice9::is_gfdm_two_head_exclusive() const {
|
||||||
|
return ::gfdm_two_head_exclusive();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WrappedIDirect3DDevice9::is_gfdm_logical_small_swapchain(
|
||||||
|
UINT swapchain) const
|
||||||
|
{
|
||||||
|
return swapchain == gfdm_logical_small_swapchain;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WrappedIDirect3DDevice9::is_gfdm_logical_side_swapchain(
|
||||||
|
UINT swapchain) const
|
||||||
|
{
|
||||||
|
return swapchain > 0
|
||||||
|
&& swapchain < GFDM_LOGICAL_HEAD_COUNT
|
||||||
|
&& swapchain != gfdm_logical_small_swapchain;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t WrappedIDirect3DDevice9::gfdm_hidden_side_swapchain_slot(
|
||||||
|
UINT swapchain) const
|
||||||
|
{
|
||||||
|
assert(is_gfdm_logical_side_swapchain(swapchain));
|
||||||
|
return swapchain < gfdm_logical_small_swapchain
|
||||||
|
? swapchain
|
||||||
|
: swapchain - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WrappedIDirect3DDevice9::set_gfdm_logical_group_parameters(
|
||||||
|
const D3DPRESENT_PARAMETERS *presentation_parameters)
|
||||||
|
{
|
||||||
|
if (presentation_parameters == nullptr) {
|
||||||
|
gfdm_logical_group_parameters_valid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::copy_n(
|
||||||
|
presentation_parameters,
|
||||||
|
gfdm_logical_group_parameters.size(),
|
||||||
|
gfdm_logical_group_parameters.begin());
|
||||||
|
gfdm_logical_group_parameters_valid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FakeIDirect3DSwapChain9 *
|
||||||
|
WrappedIDirect3DDevice9::ensure_gfdm_hidden_side_swapchain(UINT iSwapChain) {
|
||||||
|
assert(is_gfdm_logical_side_swapchain(iSwapChain));
|
||||||
|
|
||||||
|
const size_t index = gfdm_hidden_side_swapchain_slot(iSwapChain);
|
||||||
|
if (fake_sub_swapchain[index] == nullptr) {
|
||||||
|
D3DPRESENT_PARAMETERS params {};
|
||||||
|
if (gfdm_logical_group_parameters_valid) {
|
||||||
|
params = gfdm_logical_group_parameters[iSwapChain];
|
||||||
|
} else {
|
||||||
|
params.BackBufferWidth = 1080;
|
||||||
|
params.BackBufferHeight = 1920;
|
||||||
|
params.BackBufferFormat = D3DFMT_X8R8G8B8;
|
||||||
|
params.BackBufferCount = 1;
|
||||||
|
params.MultiSampleType = D3DMULTISAMPLE_NONE;
|
||||||
|
params.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||||
|
params.Windowed = FALSE;
|
||||||
|
params.FullScreen_RefreshRateInHz = 60;
|
||||||
|
params.EnableAutoDepthStencil = FALSE;
|
||||||
|
params.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
|
||||||
|
}
|
||||||
|
fake_sub_swapchain[index] = new FakeIDirect3DSwapChain9(
|
||||||
|
this,
|
||||||
|
¶ms,
|
||||||
|
is_d3d9ex,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
return fake_sub_swapchain[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
void WrappedIDirect3DDevice9::release_gfdm_hidden_side_swapchains() {
|
||||||
|
for (UINT i = 1; i < GFDM_LOGICAL_HEAD_COUNT; i++) {
|
||||||
|
if (!is_gfdm_logical_side_swapchain(i)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const size_t index = gfdm_hidden_side_swapchain_slot(i);
|
||||||
|
if (fake_sub_swapchain[index] != nullptr) {
|
||||||
|
fake_sub_swapchain[index]->Release();
|
||||||
|
fake_sub_swapchain[index] = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WrappedIDirect3DDevice9::gfdm_disarm_present_mode_recovery() {
|
||||||
|
std::lock_guard<std::mutex> lock(gfdm_recovery_mutex);
|
||||||
|
gfdm_recovery_armed = false;
|
||||||
|
gfdm_recovery_consumed.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WrappedIDirect3DDevice9::gfdm_arm_present_mode_recovery(
|
||||||
|
const D3DPRESENT_PARAMETERS *native_presentation_parameters,
|
||||||
|
const D3DDISPLAYMODEEX *native_fullscreen_display_modes)
|
||||||
|
{
|
||||||
|
if (native_presentation_parameters == nullptr
|
||||||
|
|| native_fullscreen_display_modes == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(gfdm_recovery_mutex);
|
||||||
|
gfdm_recovery_parameters[0] = native_presentation_parameters[0];
|
||||||
|
gfdm_recovery_parameters[1] = native_presentation_parameters[1];
|
||||||
|
gfdm_recovery_modes[0] = native_fullscreen_display_modes[0];
|
||||||
|
gfdm_recovery_modes[1] = native_fullscreen_display_modes[1];
|
||||||
|
if (gfdm_recovery_modes[0].Size == 0) {
|
||||||
|
gfdm_recovery_modes[0].Size = sizeof(D3DDISPLAYMODEEX);
|
||||||
|
}
|
||||||
|
if (gfdm_recovery_modes[1].Size == 0) {
|
||||||
|
gfdm_recovery_modes[1].Size = sizeof(D3DDISPLAYMODEEX);
|
||||||
|
}
|
||||||
|
gfdm_recovery_armed =
|
||||||
|
!gfdm_recovery_parameters[0].Windowed
|
||||||
|
&& !gfdm_recovery_parameters[1].Windowed
|
||||||
|
&& IsWindow(gfdm_recovery_parameters[1].hDeviceWindow);
|
||||||
|
gfdm_recovery_consumed.store(
|
||||||
|
!gfdm_recovery_armed,
|
||||||
|
std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
WrappedIDirect3DDevice9::GfdmPresentModeRecoveryResult
|
||||||
|
WrappedIDirect3DDevice9::gfdm_recover_present_mode_change(
|
||||||
|
HRESULT *failure_result)
|
||||||
|
{
|
||||||
|
if (failure_result != nullptr) {
|
||||||
|
*failure_result = D3D_OK;
|
||||||
|
}
|
||||||
|
if (!is_gfdm_two_head_exclusive() || !is_d3d9ex
|
||||||
|
|| gfdm_recovery_consumed.exchange(
|
||||||
|
true,
|
||||||
|
std::memory_order_acq_rel))
|
||||||
|
{
|
||||||
|
return GfdmPresentModeRecoveryResult::NotAttempted;
|
||||||
|
}
|
||||||
|
if (GetCurrentThreadId() != gfdm_device_creation_thread_id) {
|
||||||
|
if (failure_result != nullptr) {
|
||||||
|
*failure_result = D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
return GfdmPresentModeRecoveryResult::Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<D3DPRESENT_PARAMETERS, 2> parameters {};
|
||||||
|
std::array<D3DDISPLAYMODEEX, 2> modes {};
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(gfdm_recovery_mutex);
|
||||||
|
if (!gfdm_recovery_armed) {
|
||||||
|
return GfdmPresentModeRecoveryResult::NotAttempted;
|
||||||
|
}
|
||||||
|
parameters = gfdm_recovery_parameters;
|
||||||
|
modes = gfdm_recovery_modes;
|
||||||
|
}
|
||||||
|
|
||||||
|
IDirect3D9 *raw_d3d = nullptr;
|
||||||
|
HRESULT result = pReal->GetDirect3D(&raw_d3d);
|
||||||
|
IDirect3D9Ex *raw_d3d_ex = nullptr;
|
||||||
|
if (SUCCEEDED(result)) {
|
||||||
|
result = raw_d3d != nullptr
|
||||||
|
? raw_d3d->QueryInterface(IID_PPV_ARGS(&raw_d3d_ex))
|
||||||
|
: E_FAIL;
|
||||||
|
}
|
||||||
|
D3DDEVICE_CREATION_PARAMETERS creation {};
|
||||||
|
if (SUCCEEDED(result)) {
|
||||||
|
result = pReal->GetCreationParameters(&creation);
|
||||||
|
}
|
||||||
|
if (FAILED(result) || raw_d3d_ex == nullptr) {
|
||||||
|
if (raw_d3d_ex != nullptr) {
|
||||||
|
raw_d3d_ex->Release();
|
||||||
|
}
|
||||||
|
if (raw_d3d != nullptr) {
|
||||||
|
raw_d3d->Release();
|
||||||
|
}
|
||||||
|
if (failure_result != nullptr) {
|
||||||
|
*failure_result = result;
|
||||||
|
}
|
||||||
|
return GfdmPresentModeRecoveryResult::Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = graphics_d3d9_gfdm_recover_two_head_present_mode(
|
||||||
|
raw_d3d_ex,
|
||||||
|
static_cast<IDirect3DDevice9Ex *>(pReal),
|
||||||
|
creation.AdapterOrdinal,
|
||||||
|
parameters.data(),
|
||||||
|
modes.data());
|
||||||
|
raw_d3d_ex->Release();
|
||||||
|
raw_d3d->Release();
|
||||||
|
|
||||||
|
if (result == D3DERR_NOTAVAILABLE) {
|
||||||
|
return GfdmPresentModeRecoveryResult::NotAttempted;
|
||||||
|
}
|
||||||
|
if (FAILED(result)) {
|
||||||
|
if (failure_result != nullptr) {
|
||||||
|
*failure_result = result;
|
||||||
|
}
|
||||||
|
return GfdmPresentModeRecoveryResult::Failed;
|
||||||
|
}
|
||||||
|
return GfdmPresentModeRecoveryResult::ReadyToRetry;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
#include <d3d9.h>
|
||||||
|
|
||||||
|
inline constexpr UINT GFDM_SIDE_WIDTH = 1080;
|
||||||
|
inline constexpr UINT GFDM_SIDE_HEIGHT = 1920;
|
||||||
|
inline constexpr UINT GFDM_SMALL_WIDTH = 800;
|
||||||
|
inline constexpr UINT GFDM_SMALL_HEIGHT = 1280;
|
||||||
|
inline constexpr UINT GFDM_LOGICAL_HEAD_COUNT = 4;
|
||||||
|
inline constexpr UINT GFDM_NATIVE_SMALL_SWAPCHAIN = 1;
|
||||||
|
|
||||||
|
struct GfdmTwoHeadDeviceState {
|
||||||
|
explicit GfdmTwoHeadDeviceState(
|
||||||
|
D3DPRESENT_PARAMETERS *presentation_parameters,
|
||||||
|
D3DDISPLAYMODEEX *fullscreen_display_modes,
|
||||||
|
UINT logical_small_swapchain = 2)
|
||||||
|
: presentation_parameters(presentation_parameters),
|
||||||
|
fullscreen_display_modes(fullscreen_display_modes),
|
||||||
|
logical_small_swapchain(logical_small_swapchain) {}
|
||||||
|
|
||||||
|
void use_native_parameters() {
|
||||||
|
presentation_parameters = native_presentation_parameters.data();
|
||||||
|
fullscreen_display_modes = source_fullscreen_display_modes != nullptr
|
||||||
|
? native_fullscreen_display_modes.data()
|
||||||
|
: nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<D3DPRESENT_PARAMETERS, 2> native_presentation_parameters {};
|
||||||
|
std::array<D3DDISPLAYMODEEX, 2> native_fullscreen_display_modes {};
|
||||||
|
std::array<D3DPRESENT_PARAMETERS, 2> recovery_parameters {};
|
||||||
|
std::array<D3DDISPLAYMODEEX, 2> recovery_modes {};
|
||||||
|
D3DPRESENT_PARAMETERS *presentation_parameters;
|
||||||
|
D3DDISPLAYMODEEX *fullscreen_display_modes;
|
||||||
|
D3DDISPLAYMODEEX *const source_fullscreen_display_modes = fullscreen_display_modes;
|
||||||
|
UINT logical_small_swapchain;
|
||||||
|
bool recovery_candidate = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool gfdm_two_head_exclusive();
|
||||||
|
bool is_fake_subscreen_adapter(UINT adapter);
|
||||||
|
void get_fake_subscreen_display_mode(UINT adapter, D3DDISPLAYMODE *mode);
|
||||||
|
void get_fake_subscreen_display_mode_ex(UINT adapter, D3DDISPLAYMODEEX *mode);
|
||||||
|
|
||||||
|
HRESULT graphics_d3d9_gfdm_select_two_head_group_parameters(
|
||||||
|
const D3DPRESENT_PARAMETERS *logical_presentation_parameters,
|
||||||
|
const D3DDISPLAYMODEEX *logical_fullscreen_display_modes,
|
||||||
|
D3DPRESENT_PARAMETERS *native_presentation_parameters,
|
||||||
|
D3DDISPLAYMODEEX *native_fullscreen_display_modes,
|
||||||
|
UINT *logical_small_swapchain,
|
||||||
|
const char *operation);
|
||||||
|
|
||||||
|
HRESULT graphics_d3d9_gfdm_remap_two_head_group_parameters(
|
||||||
|
D3DPRESENT_PARAMETERS *presentation_parameters,
|
||||||
|
D3DDISPLAYMODEEX *fullscreen_display_modes,
|
||||||
|
const char *operation);
|
||||||
|
|
||||||
|
void graphics_d3d9_gfdm_align_two_head_refresh_to_desktop(
|
||||||
|
IDirect3D9Ex *d3d,
|
||||||
|
UINT master_adapter,
|
||||||
|
D3DPRESENT_PARAMETERS *presentation_parameters,
|
||||||
|
D3DDISPLAYMODEEX *fullscreen_display_modes,
|
||||||
|
const char *operation);
|
||||||
|
|
||||||
|
HRESULT validate_gfdm_two_head_exclusive(
|
||||||
|
IDirect3D9 *d3d,
|
||||||
|
UINT adapter,
|
||||||
|
D3DDEVTYPE device_type,
|
||||||
|
DWORD behavior_flags,
|
||||||
|
const D3DPRESENT_PARAMETERS *presentation_parameters);
|
||||||
|
|
||||||
|
HRESULT graphics_d3d9_gfdm_recover_two_head_present_mode(
|
||||||
|
IDirect3D9Ex *d3d,
|
||||||
|
IDirect3DDevice9Ex *device,
|
||||||
|
UINT master_adapter,
|
||||||
|
const D3DPRESENT_PARAMETERS *desired_parameters,
|
||||||
|
const D3DDISPLAYMODEEX *desired_modes);
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
#include "d3d9_live2d.h"
|
||||||
|
|
||||||
|
// only the Live2D-capable SDVX versions are 64-bit, so the entire implementation
|
||||||
|
// is compiled out of 32-bit builds (the header supplies inline no-op stubs there).
|
||||||
|
#ifdef SPICE64
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
#include "hooks/graphics/graphics.h"
|
||||||
|
|
||||||
|
// how the Live2D draw filtering works
|
||||||
|
// ------------------------------------
|
||||||
|
// SDVX draws its Live2D characters with a small, fixed set of pixel and
|
||||||
|
// vertex shaders. to skip those draws (and save GPU) we have to recognise them at
|
||||||
|
// the exact moment the game issues a draw call. the d3d9 device hooks feed three
|
||||||
|
// kinds of events into this module:
|
||||||
|
//
|
||||||
|
// 1. shader creation (on_create_pixel_shader / on_create_vertex_shader)
|
||||||
|
// the game compiles its shaders once at load. we can't trust the shader
|
||||||
|
// *object pointer* to identify a shader (it's just a heap address that
|
||||||
|
// varies per run and can be recycled), so instead we hash the shader's
|
||||||
|
// D3D9 *bytecode* - that fingerprint is stable across runs because the
|
||||||
|
// game ships the same shaders. if the hash matches a known Live2D shader
|
||||||
|
// we remember that object pointer in g_live2d_shaders.
|
||||||
|
//
|
||||||
|
// 2. shader binding (on_set_pixel_shader / on_set_vertex_shader)
|
||||||
|
// whenever the game binds a shader we look it up in that set once and cache
|
||||||
|
// the yes/no answer in g_cur_ps_is_live2d / g_cur_vs_is_live2d. binds happen
|
||||||
|
// far less often than draws, so this is where the lookup cost lives.
|
||||||
|
//
|
||||||
|
// 3. draw call (should_skip_draw, called from every Draw* hook)
|
||||||
|
// the per-draw question "is this a Live2D draw?" is then just reading those
|
||||||
|
// two cached bools - no hashing, no map lookups. if the skip is currently
|
||||||
|
// active (see graphics_sdvx_live2d_should_skip) and either bound shader is
|
||||||
|
// Live2D, the Draw* hook drops the call instead of forwarding it.
|
||||||
|
//
|
||||||
|
// everything is gated on the feature being enabled (mode != Off); when it's Off
|
||||||
|
// every entry point is a single predicted-not-taken branch. d3d9 rendering for a
|
||||||
|
// device is single-threaded, so none of this state needs locking.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// shader state is tracked whenever the feature might act (mode != Off) so the
|
||||||
|
// known-shader set is populated before a song starts. when Off, every entry
|
||||||
|
// point is a single cheap branch.
|
||||||
|
bool tracking_enabled() {
|
||||||
|
return GRAPHICS_SDVX_LIVE2D_MODE != SdvxLive2dMode::Off;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the set of shader objects (pixel or vertex) whose bytecode matched a known
|
||||||
|
// Live2D fingerprint. only matching shaders are stored, so this stays tiny.
|
||||||
|
std::unordered_set<void *> g_live2d_shaders;
|
||||||
|
|
||||||
|
// whether the currently-bound shaders are known Live2D shaders. cached at set
|
||||||
|
// time so the per-draw check is just two bool reads.
|
||||||
|
bool g_cur_ps_is_live2d = false;
|
||||||
|
bool g_cur_vs_is_live2d = false;
|
||||||
|
|
||||||
|
// FNV-1a 64 over a D3D9 shader token stream (ends with D3DSIO_END = 0x0000FFFF)
|
||||||
|
uint64_t bytecode_hash(const DWORD *func) {
|
||||||
|
if (func == nullptr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const DWORD *p = func;
|
||||||
|
const DWORD *cap = func + 65536; // safety bound
|
||||||
|
while (p < cap && *p != 0x0000FFFF) {
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
const size_t n_bytes = ((size_t)(p - func) + 1) * sizeof(DWORD);
|
||||||
|
uint64_t h = 1469598103934665603ULL;
|
||||||
|
const auto *bytes = reinterpret_cast<const uint8_t *>(func);
|
||||||
|
for (size_t i = 0; i < n_bytes; i++) {
|
||||||
|
h ^= bytes[i];
|
||||||
|
h *= 1099511628211ULL;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// known SDVX Live2D shader bytecode hashes (4 pixel + 3 vertex). stable
|
||||||
|
// across runs because the game ships fixed shaders. the two sets are disjoint so
|
||||||
|
// a single shader can be classified by its own hash alone.
|
||||||
|
bool hash_is_live2d(uint64_t hash) {
|
||||||
|
switch (hash) {
|
||||||
|
case 0x75c89951817421a4ULL: // pixel: dominant model draw (~4.9M prims/120f in-song)
|
||||||
|
case 0x2d7ce428c6b4775dULL: // pixel: masked model draw
|
||||||
|
case 0x3ce00cc6111c10e7ULL: // pixel: mask generation
|
||||||
|
case 0x8bb3a2f37150ac34ULL: // pixel: mask generation (variant)
|
||||||
|
case 0xe9cf898c331e2a51ULL: // vertex
|
||||||
|
case 0x94dc84e7b7c0f437ULL: // vertex
|
||||||
|
case 0xc872937c5cc04309ULL: // vertex
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// classify a shader at creation time and record it if it is Live2D. erasing on a
|
||||||
|
// miss keeps the set correct if the runtime reuses a freed shader pointer.
|
||||||
|
void classify_shader(void *shader, const DWORD *func) {
|
||||||
|
if (hash_is_live2d(bytecode_hash(func))) {
|
||||||
|
g_live2d_shaders.insert(shader);
|
||||||
|
} else {
|
||||||
|
g_live2d_shaders.erase(shader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace d3d9_live2d {
|
||||||
|
|
||||||
|
// stage 1: fingerprint each shader as the game creates it
|
||||||
|
void on_create_vertex_shader(IDirect3DVertexShader9 *shader, const DWORD *func) {
|
||||||
|
if (tracking_enabled() && shader != nullptr) [[unlikely]] {
|
||||||
|
classify_shader(shader, func);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_create_pixel_shader(IDirect3DPixelShader9 *shader, const DWORD *func) {
|
||||||
|
if (tracking_enabled() && shader != nullptr) [[unlikely]] {
|
||||||
|
classify_shader(shader, func);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stage 2: remember whether the just-bound shader is a Live2D one
|
||||||
|
void on_set_vertex_shader(IDirect3DVertexShader9 *shader) {
|
||||||
|
if (tracking_enabled()) [[unlikely]] {
|
||||||
|
g_cur_vs_is_live2d = g_live2d_shaders.count(shader) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_set_pixel_shader(IDirect3DPixelShader9 *shader) {
|
||||||
|
if (tracking_enabled()) [[unlikely]] {
|
||||||
|
g_cur_ps_is_live2d = g_live2d_shaders.count(shader) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stage 3: drop the draw if the skip is active and a Live2D shader is bound
|
||||||
|
bool should_skip_draw() {
|
||||||
|
return graphics_sdvx_live2d_should_skip() && (g_cur_ps_is_live2d || g_cur_vs_is_live2d);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace d3d9_live2d
|
||||||
|
|
||||||
|
#endif // SPICE64
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <d3d9.h>
|
||||||
|
|
||||||
|
// SDVX Live2D draw-skip support for the D3D9 backend.
|
||||||
|
//
|
||||||
|
// SDVX renders its Live2D navigator / in-song character through a fixed set of
|
||||||
|
// shaders. when the skip is active (see graphics_sdvx_live2d_should_skip)
|
||||||
|
// the matching draw calls are dropped to save GPU. shaders are identified by a
|
||||||
|
// stable hash of their D3D9 bytecode (object pointers vary per run, the bytecode
|
||||||
|
// does not). the hashes were captured with the draw-call fingerprinting tool.
|
||||||
|
//
|
||||||
|
// every entry point is a no-op unless the feature is enabled (mode != Off), and
|
||||||
|
// d3d9 rendering for a device is single-threaded, so none of this needs locking.
|
||||||
|
namespace d3d9_live2d {
|
||||||
|
|
||||||
|
#ifdef SPICE64
|
||||||
|
|
||||||
|
// record a shader's bytecode fingerprint at creation time
|
||||||
|
void on_create_vertex_shader(IDirect3DVertexShader9 *shader, const DWORD *func);
|
||||||
|
void on_create_pixel_shader(IDirect3DPixelShader9 *shader, const DWORD *func);
|
||||||
|
|
||||||
|
// remember the currently-bound shaders
|
||||||
|
void on_set_vertex_shader(IDirect3DVertexShader9 *shader);
|
||||||
|
void on_set_pixel_shader(IDirect3DPixelShader9 *shader);
|
||||||
|
|
||||||
|
// true if the current draw call should be dropped (skip active AND the bound
|
||||||
|
// shaders identify it as SDVX Live2D)
|
||||||
|
bool should_skip_draw();
|
||||||
|
|
||||||
|
#else // !SPICE64
|
||||||
|
|
||||||
|
// only the Live2D-capable SDVX versions are 64-bit; on 32-bit every entry point
|
||||||
|
// compiles away to nothing, so the d3d9 device hooks need no #ifdefs at their
|
||||||
|
// call sites.
|
||||||
|
inline void on_create_vertex_shader(IDirect3DVertexShader9 *, const DWORD *) {}
|
||||||
|
inline void on_create_pixel_shader(IDirect3DPixelShader9 *, const DWORD *) {}
|
||||||
|
inline void on_set_vertex_shader(IDirect3DVertexShader9 *) {}
|
||||||
|
inline void on_set_pixel_shader(IDirect3DPixelShader9 *) {}
|
||||||
|
inline bool should_skip_draw() { return false; }
|
||||||
|
|
||||||
|
#endif // SPICE64
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::QueryInterface(REFIID riid
|
|||||||
return E_POINTER;
|
return E_POINTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (//riid == __uuidof(IUnknown) || Ignore IUnknown, it's often queried to test object equality between different interfaces
|
if ((riid == IID_IUnknown && pDev->is_gfdm_two_head_exclusive()) ||
|
||||||
riid == IID_IDirect3DSwapChain9 ||
|
riid == IID_IDirect3DSwapChain9 ||
|
||||||
riid == IID_IDirect3DSwapChain9Ex)
|
riid == IID_IDirect3DSwapChain9Ex)
|
||||||
{
|
{
|
||||||
@@ -89,7 +89,42 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::Present(const RECT *pSourc
|
|||||||
graphics_d3d9_on_present(pDev->hFocusWindow, pDev->pReal, pDev);
|
graphics_d3d9_on_present(pDev->hFocusWindow, pDev->pReal, pDev);
|
||||||
}
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->Present(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion, dwFlags));
|
HRESULT result = pReal->Present(
|
||||||
|
pSourceRect,
|
||||||
|
pDestRect,
|
||||||
|
hDestWindowOverride,
|
||||||
|
pDirtyRegion,
|
||||||
|
dwFlags);
|
||||||
|
|
||||||
|
// Some drivers report S_PRESENT_MODE_CHANGED after Windows moves the portrait SMALL
|
||||||
|
// head into the requested exclusive mode, leaving both group heads black. Toggle SMALL
|
||||||
|
// through another supported mode and restore it once, then retry the interrupted MAIN
|
||||||
|
// present. Recovery is restricted to the creation thread and consumes its armed state,
|
||||||
|
// so subsequent presents cannot repeat the mode switch.
|
||||||
|
if (pDev->is_gfdm_two_head_exclusive()
|
||||||
|
&& native_group_head == NativeGroupHead::Main
|
||||||
|
&& result == S_PRESENT_MODE_CHANGED)
|
||||||
|
{
|
||||||
|
HRESULT recovery_failure = D3D_OK;
|
||||||
|
const auto recovery = pDev->gfdm_recover_present_mode_change(
|
||||||
|
&recovery_failure);
|
||||||
|
if (recovery
|
||||||
|
== WrappedIDirect3DDevice9::GfdmPresentModeRecoveryResult::ReadyToRetry)
|
||||||
|
{
|
||||||
|
result = pReal->Present(
|
||||||
|
pSourceRect,
|
||||||
|
pDestRect,
|
||||||
|
hDestWindowOverride,
|
||||||
|
pDirtyRegion,
|
||||||
|
dwFlags);
|
||||||
|
} else if (recovery
|
||||||
|
== WrappedIDirect3DDevice9::GfdmPresentModeRecoveryResult::Failed
|
||||||
|
&& FAILED(recovery_failure))
|
||||||
|
{
|
||||||
|
result = recovery_failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CHECK_RESULT(result);
|
||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetFrontBufferData(IDirect3DSurface9 *pDestSurface) {
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetFrontBufferData(IDirect3DSurface9 *pDestSurface) {
|
||||||
CHECK_RESULT(pReal->GetFrontBufferData(pDestSurface));
|
CHECK_RESULT(pReal->GetFrontBufferData(pDestSurface));
|
||||||
|
|||||||
@@ -5,8 +5,13 @@
|
|||||||
interface WrappedIDirect3DDevice9;
|
interface WrappedIDirect3DDevice9;
|
||||||
|
|
||||||
struct WrappedIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
|
struct WrappedIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
|
||||||
WrappedIDirect3DSwapChain9(WrappedIDirect3DDevice9 *dev, IDirect3DSwapChain9 *orig) :
|
enum class NativeGroupHead { Unknown, Main, Small };
|
||||||
pDev(dev), pReal(orig), is_d3d9ex(false)
|
|
||||||
|
WrappedIDirect3DSwapChain9(
|
||||||
|
WrappedIDirect3DDevice9 *dev,
|
||||||
|
IDirect3DSwapChain9 *orig,
|
||||||
|
NativeGroupHead native_group_head = NativeGroupHead::Unknown) :
|
||||||
|
pDev(dev), pReal(orig), is_d3d9ex(false), native_group_head(native_group_head)
|
||||||
{
|
{
|
||||||
IDirect3DSwapChain9Ex *swapchain = nullptr;
|
IDirect3DSwapChain9Ex *swapchain = nullptr;
|
||||||
|
|
||||||
@@ -16,8 +21,11 @@ struct WrappedIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WrappedIDirect3DSwapChain9(WrappedIDirect3DDevice9 *dev, IDirect3DSwapChain9Ex *orig) :
|
WrappedIDirect3DSwapChain9(
|
||||||
pDev(dev), pReal(orig), is_d3d9ex(true)
|
WrappedIDirect3DDevice9 *dev,
|
||||||
|
IDirect3DSwapChain9Ex *orig,
|
||||||
|
NativeGroupHead native_group_head = NativeGroupHead::Unknown) :
|
||||||
|
pDev(dev), pReal(orig), is_d3d9ex(true), native_group_head(native_group_head)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +61,7 @@ struct WrappedIDirect3DSwapChain9 : IDirect3DSwapChain9Ex {
|
|||||||
|
|
||||||
IDirect3DSwapChain9 *pReal;
|
IDirect3DSwapChain9 *pReal;
|
||||||
bool is_d3d9ex = false;
|
bool is_d3d9ex = false;
|
||||||
|
NativeGroupHead native_group_head = NativeGroupHead::Unknown;
|
||||||
|
|
||||||
bool should_run_hooks = true;
|
bool should_run_hooks = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,18 +17,20 @@
|
|||||||
#include "games/ddr/ddr.h"
|
#include "games/ddr/ddr.h"
|
||||||
#include "games/gitadora/gitadora.h"
|
#include "games/gitadora/gitadora.h"
|
||||||
#include "games/iidx/iidx.h"
|
#include "games/iidx/iidx.h"
|
||||||
|
#include "games/sdvx/sdvx.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 "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"
|
||||||
#include "touch/touch_indicators.h"
|
#include "touch/touch_gestures.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/fileutils.h"
|
#include "util/fileutils.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
|
#include "touch/native/inject.h"
|
||||||
#include "util/time.h"
|
#include "util/time.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
|
|
||||||
@@ -43,6 +45,8 @@ HWND SDVX_SUBSCREEN_WINDOW = nullptr;
|
|||||||
HWND GFDM_SUBSCREEN_WINDOW = nullptr;
|
HWND GFDM_SUBSCREEN_WINDOW = nullptr;
|
||||||
static HWND GFDM_LEFT_WINDOW = nullptr;
|
static HWND GFDM_LEFT_WINDOW = nullptr;
|
||||||
static HWND GFDM_RIGHT_WINDOW = nullptr;
|
static HWND GFDM_RIGHT_WINDOW = nullptr;
|
||||||
|
static HMONITOR GFDM_TWO_HEAD_SMALL_MONITOR = nullptr;
|
||||||
|
static HWND GFDM_TWO_HEAD_SMALL_WINDOW = nullptr;
|
||||||
HWND POPN_SUBSCREEN_WINDOW = nullptr;
|
HWND POPN_SUBSCREEN_WINDOW = nullptr;
|
||||||
bool FAKE_SUBSCREEN_ADAPTER = false;
|
bool FAKE_SUBSCREEN_ADAPTER = false;
|
||||||
|
|
||||||
@@ -86,6 +90,10 @@ static bool monitor_layout_needs_reset = false;
|
|||||||
bool GRAPHICS_CAPTURE_CURSOR = false;
|
bool GRAPHICS_CAPTURE_CURSOR = false;
|
||||||
bool GRAPHICS_LOG_HRESULT = false;
|
bool GRAPHICS_LOG_HRESULT = false;
|
||||||
bool GRAPHICS_SDVX_FORCE_720 = false;
|
bool GRAPHICS_SDVX_FORCE_720 = false;
|
||||||
|
#ifdef SPICE64
|
||||||
|
SdvxLive2dMode GRAPHICS_SDVX_LIVE2D_MODE = SdvxLive2dMode::Off;
|
||||||
|
std::atomic<bool> GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY = false;
|
||||||
|
#endif // SPICE64
|
||||||
bool GRAPHICS_SHOW_CURSOR = false;
|
bool GRAPHICS_SHOW_CURSOR = false;
|
||||||
bool GRAPHICS_WINDOWED = false;
|
bool GRAPHICS_WINDOWED = false;
|
||||||
std::vector<HWND> GRAPHICS_WINDOWS;
|
std::vector<HWND> GRAPHICS_WINDOWS;
|
||||||
@@ -178,17 +186,26 @@ static bool is_gfdm_known_window(HWND hWnd) {
|
|||||||
return gitadora_window_name_for_hwnd(hWnd) != nullptr;
|
return gitadora_window_name_for_hwnd(hWnd) != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool is_gfdm_two_head_small_window(HWND hWnd) {
|
||||||
|
return games::gitadora::is_arena_model() &&
|
||||||
|
games::gitadora::ARENA_TWO_HEAD_EXCLUSIVE &&
|
||||||
|
hWnd != nullptr &&
|
||||||
|
hWnd == GFDM_TWO_HEAD_SMALL_WINDOW;
|
||||||
|
}
|
||||||
|
|
||||||
static bool gitadora_should_block_game_window_placement(HWND hWnd) {
|
static bool gitadora_should_block_game_window_placement(HWND hWnd) {
|
||||||
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) {
|
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto window_name = gitadora_window_name_for_hwnd(hWnd);
|
const auto window_name = gitadora_window_name_for_hwnd(hWnd);
|
||||||
return window_name != nullptr && graphics_gitadora_has_window_monitor(window_name);
|
return window_name != nullptr && graphics_gitadora_has_window_override(window_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void gitadora_remember_window(HWND hWnd, const std::string &window_name) {
|
static void gitadora_remember_window(HWND hWnd, const std::string &window_name) {
|
||||||
if (window_name == "LEFT") {
|
if (window_name == "GITADORA") {
|
||||||
|
GRAPHICS_HOOKED_WINDOW = hWnd;
|
||||||
|
} else if (window_name == "LEFT") {
|
||||||
GFDM_LEFT_WINDOW = hWnd;
|
GFDM_LEFT_WINDOW = hWnd;
|
||||||
} else if (window_name == "RIGHT") {
|
} else if (window_name == "RIGHT") {
|
||||||
GFDM_RIGHT_WINDOW = hWnd;
|
GFDM_RIGHT_WINDOW = hWnd;
|
||||||
@@ -197,6 +214,95 @@ static void gitadora_remember_window(HWND hWnd, const std::string &window_name)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool graphics_gitadora_prepare_two_head_device_window(
|
||||||
|
HWND hWnd, HMONITOR target_monitor, UINT desired_width, UINT desired_height) {
|
||||||
|
if (hWnd == nullptr || !IsWindow(hWnd)) {
|
||||||
|
log_warning(
|
||||||
|
"graphics",
|
||||||
|
"two-head exclusive: native secondary device window is invalid: {}",
|
||||||
|
fmt::ptr(hWnd));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ShowWindow_orig == nullptr || SetWindowLongA_orig == nullptr ||
|
||||||
|
SetWindowPos_orig == nullptr) {
|
||||||
|
log_warning(
|
||||||
|
"graphics",
|
||||||
|
"two-head exclusive: window hooks are not initialized for {}",
|
||||||
|
fmt::ptr(hWnd));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT rect_before {};
|
||||||
|
GetWindowRect(hWnd, &rect_before);
|
||||||
|
const DWORD style_before = static_cast<DWORD>(GetWindowLongA(hWnd, GWL_STYLE));
|
||||||
|
const BOOL visible_before = IsWindowVisible(hWnd);
|
||||||
|
|
||||||
|
if (target_monitor != nullptr) {
|
||||||
|
GFDM_TWO_HEAD_SMALL_MONITOR = target_monitor;
|
||||||
|
}
|
||||||
|
const HMONITOR monitor = target_monitor != nullptr
|
||||||
|
? target_monitor
|
||||||
|
: (GFDM_TWO_HEAD_SMALL_MONITOR != nullptr
|
||||||
|
? GFDM_TWO_HEAD_SMALL_MONITOR
|
||||||
|
: MonitorFromWindow(hWnd, MONITOR_DEFAULTTONULL));
|
||||||
|
MONITORINFO monitor_info {};
|
||||||
|
monitor_info.cbSize = sizeof(monitor_info);
|
||||||
|
if (monitor == nullptr || !GetMonitorInfoA(monitor, &monitor_info)) {
|
||||||
|
log_warning(
|
||||||
|
"graphics",
|
||||||
|
"two-head exclusive: could not find the SMALL monitor for {}",
|
||||||
|
fmt::ptr(hWnd));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arena requests D3DCREATE_NOWINDOWCHANGES, so place its SMALL window on
|
||||||
|
// the second head before D3D9 takes ownership of it.
|
||||||
|
const DWORD fullscreen_style = (style_before & ~WS_OVERLAPPEDWINDOW) | WS_POPUP;
|
||||||
|
const bool style_changed = fullscreen_style != style_before;
|
||||||
|
if (style_changed) {
|
||||||
|
SetWindowLongA_orig(hWnd, GWL_STYLE, static_cast<LONG>(fullscreen_style));
|
||||||
|
}
|
||||||
|
const RECT &monitor_rect = monitor_info.rcMonitor;
|
||||||
|
const int host_width = desired_width != 0
|
||||||
|
? static_cast<int>(desired_width)
|
||||||
|
: monitor_rect.right - monitor_rect.left;
|
||||||
|
const int host_height = desired_height != 0
|
||||||
|
? static_cast<int>(desired_height)
|
||||||
|
: monitor_rect.bottom - monitor_rect.top;
|
||||||
|
const bool rect_changed = rect_before.left != monitor_rect.left ||
|
||||||
|
rect_before.top != monitor_rect.top ||
|
||||||
|
rect_before.right != monitor_rect.left + host_width ||
|
||||||
|
rect_before.bottom != monitor_rect.top + host_height;
|
||||||
|
if ((style_changed || rect_changed) && !SetWindowPos_orig(
|
||||||
|
hWnd,
|
||||||
|
HWND_TOP,
|
||||||
|
monitor_rect.left,
|
||||||
|
monitor_rect.top,
|
||||||
|
host_width,
|
||||||
|
host_height,
|
||||||
|
SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_FRAMECHANGED)) {
|
||||||
|
log_warning(
|
||||||
|
"graphics",
|
||||||
|
"two-head exclusive: failed to place SMALL window, error={}",
|
||||||
|
GetLastError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!visible_before) {
|
||||||
|
ShowWindow_orig(hWnd, SW_SHOWNOACTIVATE);
|
||||||
|
}
|
||||||
|
if (MonitorFromWindow(hWnd, MONITOR_DEFAULTTONULL) != monitor) {
|
||||||
|
log_warning(
|
||||||
|
"graphics",
|
||||||
|
"two-head exclusive: SMALL window {} is on the wrong monitor",
|
||||||
|
fmt::ptr(hWnd));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GFDM_TWO_HEAD_SMALL_WINDOW = hWnd;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static bool gitadora_should_allow_small_resize() {
|
static bool gitadora_should_allow_small_resize() {
|
||||||
return GRAPHICS_WINDOWED &&
|
return GRAPHICS_WINDOWED &&
|
||||||
games::gitadora::is_arena_model() &&
|
games::gitadora::is_arena_model() &&
|
||||||
@@ -585,6 +691,12 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
|||||||
hWndParent, hMenu, hInstance, lpParam);
|
hWndParent, hMenu, hInstance, lpParam);
|
||||||
GRAPHICS_WINDOWS.push_back(result);
|
GRAPHICS_WINDOWS.push_back(result);
|
||||||
|
|
||||||
|
// remember these windows now because full capture happens later during D3D
|
||||||
|
// initialization; placement calls before then can undo creation-time overrides
|
||||||
|
if (result != nullptr && is_gfdm_window && !is_gfdm_sub_window) {
|
||||||
|
gitadora_remember_window(result, gfdm_window_name);
|
||||||
|
}
|
||||||
|
|
||||||
// theme the native title bar (dark/light)
|
// theme the native title bar (dark/light)
|
||||||
set_window_dark_titlebar(result);
|
set_window_dark_titlebar(result);
|
||||||
|
|
||||||
@@ -608,10 +720,14 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
|||||||
if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") {
|
if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") {
|
||||||
gitadora_remember_window(result, gfdm_window_name);
|
gitadora_remember_window(result, gfdm_window_name);
|
||||||
}
|
}
|
||||||
if (is_gfdm_sub_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
if (is_gfdm_sub_window &&
|
||||||
|
((GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) ||
|
||||||
|
games::gitadora::ARENA_TWO_HEAD_EXCLUSIVE)) {
|
||||||
gitadora_remember_window(result, gfdm_window_name);
|
gitadora_remember_window(result, gfdm_window_name);
|
||||||
|
if (GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
|
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (is_gfdm_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
if (is_gfdm_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||||
gitadora_force_window_style(result);
|
gitadora_force_window_style(result);
|
||||||
}
|
}
|
||||||
@@ -623,7 +739,7 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disable_touch_indicators(result);
|
disable_touch_gestures(result);
|
||||||
log_misc(
|
log_misc(
|
||||||
"graphics",
|
"graphics",
|
||||||
"CreateWindowExA returned {}, {}",
|
"CreateWindowExA returned {}, {}",
|
||||||
@@ -718,7 +834,7 @@ static HWND WINAPI CreateWindowExW_hook(DWORD dwExStyle, LPCWSTR lpClassName, LP
|
|||||||
fmt::ptr(result),
|
fmt::ptr(result),
|
||||||
lpWindowName ? ws2s(lpWindowName) : "(null)");
|
lpWindowName ? ws2s(lpWindowName) : "(null)");
|
||||||
|
|
||||||
disable_touch_indicators(result);
|
disable_touch_gestures(result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -757,6 +873,10 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei
|
|||||||
nHeight,
|
nHeight,
|
||||||
bRepaint);
|
bRepaint);
|
||||||
|
|
||||||
|
if (is_gfdm_two_head_small_window(hWnd)) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
// sound voltex windowed mode adjustments
|
// sound voltex windowed mode adjustments
|
||||||
if (GRAPHICS_WINDOWED && GRAPHICS_SDVX_FORCE_720 && avs::game::is_model("KFC")) {
|
if (GRAPHICS_WINDOWED && GRAPHICS_SDVX_FORCE_720 && avs::game::is_model("KFC")) {
|
||||||
RECT rect {};
|
RECT rect {};
|
||||||
@@ -793,7 +913,11 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei
|
|||||||
nWidth = rect.right - rect.left;
|
nWidth = rect.right - rect.left;
|
||||||
nHeight = rect.bottom - rect.top;
|
nHeight = rect.bottom - rect.top;
|
||||||
|
|
||||||
|
if (games::iidx::NATIVE_TOUCH) {
|
||||||
|
nativetouch::inject::register_and_attach_window(TDJ_SUBSCREEN_WINDOW);
|
||||||
|
} else {
|
||||||
touch_attach_wnd(TDJ_SUBSCREEN_WINDOW);
|
touch_attach_wnd(TDJ_SUBSCREEN_WINDOW);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Existing behaviour: suppress subscreen window and prompt user to use overlay instead
|
// Existing behaviour: suppress subscreen window and prompt user to use overlay instead
|
||||||
log_info(
|
log_info(
|
||||||
@@ -860,6 +984,11 @@ static HCURSOR WINAPI SetCursor_hook(HCURSOR hCursor) {
|
|||||||
|
|
||||||
static LONG WINAPI SetWindowLongA_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
|
static LONG WINAPI SetWindowLongA_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
|
||||||
|
|
||||||
|
if (is_gfdm_two_head_small_window(hWnd) &&
|
||||||
|
(nIndex == GWL_STYLE || nIndex == GWL_EXSTYLE)) {
|
||||||
|
return GetWindowLongA(hWnd, nIndex);
|
||||||
|
}
|
||||||
|
|
||||||
// DDR window style fix
|
// DDR window style fix
|
||||||
if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) {
|
if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) {
|
||||||
dwNewLong |= WS_OVERLAPPEDWINDOW;
|
dwNewLong |= WS_OVERLAPPEDWINDOW;
|
||||||
@@ -880,6 +1009,11 @@ static LONG WINAPI SetWindowLongA_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
|
|||||||
|
|
||||||
static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
|
static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
|
||||||
|
|
||||||
|
if (is_gfdm_two_head_small_window(hWnd) &&
|
||||||
|
(nIndex == GWL_STYLE || nIndex == GWL_EXSTYLE)) {
|
||||||
|
return GetWindowLongW(hWnd, nIndex);
|
||||||
|
}
|
||||||
|
|
||||||
// DDR overlapped window fix
|
// DDR overlapped window fix
|
||||||
if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) {
|
if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) {
|
||||||
dwNewLong |= WS_OVERLAPPEDWINDOW;
|
dwNewLong |= WS_OVERLAPPEDWINDOW;
|
||||||
@@ -901,6 +1035,12 @@ static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
|
|||||||
static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
|
static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
|
||||||
int X, int Y, int cx, int cy, UINT uFlags) {
|
int X, int Y, int cx, int cy, UINT uFlags) {
|
||||||
|
|
||||||
|
if (is_gfdm_two_head_small_window(hWnd) &&
|
||||||
|
((uFlags & SWP_HIDEWINDOW) ||
|
||||||
|
(uFlags & (SWP_NOMOVE | SWP_NOSIZE)) != (SWP_NOMOVE | SWP_NOSIZE))) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
// windowed mode adjustments
|
// windowed mode adjustments
|
||||||
if (GRAPHICS_WINDOWED && (avs::game::is_model("LMA") || avs::game::is_model("MDX"))) {
|
if (GRAPHICS_WINDOWED && (avs::game::is_model("LMA") || avs::game::is_model("MDX"))) {
|
||||||
return TRUE;
|
return TRUE;
|
||||||
@@ -925,6 +1065,13 @@ 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 (is_gfdm_two_head_small_window(hWnd) &&
|
||||||
|
(nCmdShow == SW_HIDE || nCmdShow == SW_MINIMIZE ||
|
||||||
|
nCmdShow == SW_SHOWMINIMIZED || nCmdShow == SW_SHOWMINNOACTIVE ||
|
||||||
|
nCmdShow == SW_FORCEMINIMIZE)) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
if (games::gitadora::is_arena_model() &&
|
if (games::gitadora::is_arena_model() &&
|
||||||
GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
|
GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
|
||||||
hWnd != GRAPHICS_HOOKED_WINDOW) {
|
hWnd != GRAPHICS_HOOKED_WINDOW) {
|
||||||
@@ -946,6 +1093,13 @@ static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (games::iidx::TDJ_MODE &&
|
||||||
|
GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
|
||||||
|
hWnd == TDJ_SUBSCREEN_WINDOW) {
|
||||||
|
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// call original
|
// call original
|
||||||
return ShowWindow_orig(hWnd, nCmdShow);
|
return ShowWindow_orig(hWnd, nCmdShow);
|
||||||
}
|
}
|
||||||
@@ -1159,6 +1313,14 @@ void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParamet
|
|||||||
WNDPROC_ORIG = reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hWnd, GWLP_WNDPROC));
|
WNDPROC_ORIG = reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hWnd, GWLP_WNDPROC));
|
||||||
SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WindowProc));
|
SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WindowProc));
|
||||||
|
|
||||||
|
const bool native_touch_overlay =
|
||||||
|
(games::iidx::NATIVE_TOUCH && games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) ||
|
||||||
|
(games::popn::NATIVE_TOUCH &&
|
||||||
|
games::popn::is_pikapika_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS);
|
||||||
|
if (native_touch_overlay) {
|
||||||
|
nativetouch::inject::register_and_attach_window(hWnd);
|
||||||
|
}
|
||||||
|
|
||||||
// NOLEGACY causes WM_CHAR to be not received
|
// NOLEGACY causes WM_CHAR to be not received
|
||||||
// reflec beat game engine does not pass WM_CHAR through for some reason (unrelated to SpiceTouch)
|
// reflec beat game engine does not pass WM_CHAR through for some reason (unrelated to SpiceTouch)
|
||||||
if (!rawinput::NOLEGACY && !(avs::game::is_model({"KBR", "LBR", "MBR"}))) {
|
if (!rawinput::NOLEGACY && !(avs::game::is_model({"KBR", "LBR", "MBR"}))) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
@@ -27,6 +28,33 @@ enum graphics_dx9on12_state {
|
|||||||
DX9ON12_FORCE_ON,
|
DX9ON12_FORCE_ON,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// SDVX Live2D suppression policy. the mode comes from the launcher
|
||||||
|
// option; the in-gameplay flag is maintained by the SDVX scene-detection hook in
|
||||||
|
// games/sdvx (which does not require the SDVX game module to be enabled). the
|
||||||
|
// d3d9 backend reads graphics_sdvx_live2d_should_skip() on every draw.
|
||||||
|
// only the Live2D-capable SDVX versions are 64-bit, so the whole feature is
|
||||||
|
// compiled out of 32-bit builds.
|
||||||
|
#ifdef SPICE64
|
||||||
|
enum class SdvxLive2dMode {
|
||||||
|
Off, // leave Live2D untouched (default)
|
||||||
|
Always, // always skip Live2D draws (also removes the menu navigator)
|
||||||
|
InGame, // skip Live2D draws only during a song
|
||||||
|
};
|
||||||
|
extern SdvxLive2dMode GRAPHICS_SDVX_LIVE2D_MODE;
|
||||||
|
extern std::atomic<bool> GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY;
|
||||||
|
|
||||||
|
inline bool graphics_sdvx_live2d_should_skip() {
|
||||||
|
switch (GRAPHICS_SDVX_LIVE2D_MODE) {
|
||||||
|
case SdvxLive2dMode::Always:
|
||||||
|
return true;
|
||||||
|
case SdvxLive2dMode::InGame:
|
||||||
|
return GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.load(std::memory_order_relaxed);
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // SPICE64
|
||||||
|
|
||||||
// flag settings
|
// flag settings
|
||||||
extern bool GRAPHICS_CAPTURE_CURSOR;
|
extern bool GRAPHICS_CAPTURE_CURSOR;
|
||||||
extern bool GRAPHICS_LOG_HRESULT;
|
extern bool GRAPHICS_LOG_HRESULT;
|
||||||
@@ -60,6 +88,8 @@ extern std::string GRAPHICS_GITADORA_MAIN_MONITOR;
|
|||||||
extern std::string GRAPHICS_GITADORA_LEFT_MONITOR;
|
extern std::string GRAPHICS_GITADORA_LEFT_MONITOR;
|
||||||
extern std::string GRAPHICS_GITADORA_RIGHT_MONITOR;
|
extern std::string GRAPHICS_GITADORA_RIGHT_MONITOR;
|
||||||
extern std::string GRAPHICS_GITADORA_SMALL_MONITOR;
|
extern std::string GRAPHICS_GITADORA_SMALL_MONITOR;
|
||||||
|
extern std::optional<std::string> GRAPHICS_GITADORA_SMALL_SIZE;
|
||||||
|
extern std::optional<std::string> GRAPHICS_GITADORA_SMALL_POS;
|
||||||
|
|
||||||
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;
|
||||||
@@ -89,6 +119,15 @@ extern bool D3D9_DEVICE_HOOK_DISABLE;
|
|||||||
|
|
||||||
void graphics_init();
|
void graphics_init();
|
||||||
void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters);
|
void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters);
|
||||||
|
// The native GITADORA two-head D3D9 group uses the game's named SMALL
|
||||||
|
// device window for the native physical SMALL head. The game requests
|
||||||
|
// D3DCREATE_NOWINDOWCHANGES, so this host must be made borderless and sized
|
||||||
|
// to the requested native SMALL mode before the native fullscreen device is
|
||||||
|
// created. During the adapter-group startup warmup, the monitor can briefly
|
||||||
|
// report a different mode; preserve its origin but not that temporary size.
|
||||||
|
bool graphics_gitadora_prepare_two_head_device_window(
|
||||||
|
HWND hWnd, HMONITOR target_monitor = nullptr, UINT desired_width = 0,
|
||||||
|
UINT desired_height = 0);
|
||||||
void graphics_add_wnd_proc(WNDPROC wndProc);
|
void graphics_add_wnd_proc(WNDPROC wndProc);
|
||||||
void graphics_remove_wnd_proc(WNDPROC wndProc);
|
void graphics_remove_wnd_proc(WNDPROC wndProc);
|
||||||
void graphics_hook_subscreen_window(HWND hWnd);
|
void graphics_hook_subscreen_window(HWND hWnd);
|
||||||
@@ -129,7 +168,7 @@ bool graphics_gitadora_apply_window_monitor(
|
|||||||
int &width,
|
int &width,
|
||||||
int &height,
|
int &height,
|
||||||
bool log_change);
|
bool log_change);
|
||||||
bool graphics_gitadora_has_window_monitor(const std::string &window_name);
|
bool graphics_gitadora_has_window_override(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(
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ std::string GRAPHICS_GITADORA_MAIN_MONITOR;
|
|||||||
std::string GRAPHICS_GITADORA_LEFT_MONITOR;
|
std::string GRAPHICS_GITADORA_LEFT_MONITOR;
|
||||||
std::string GRAPHICS_GITADORA_RIGHT_MONITOR;
|
std::string GRAPHICS_GITADORA_RIGHT_MONITOR;
|
||||||
std::string GRAPHICS_GITADORA_SMALL_MONITOR;
|
std::string GRAPHICS_GITADORA_SMALL_MONITOR;
|
||||||
|
std::optional<std::string> GRAPHICS_GITADORA_SMALL_SIZE;
|
||||||
|
std::optional<std::string> GRAPHICS_GITADORA_SMALL_POS;
|
||||||
|
|
||||||
// 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;
|
||||||
@@ -89,9 +91,12 @@ static const std::string &graphics_gitadora_monitor_for_window_name(
|
|||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool graphics_gitadora_has_window_monitor(const std::string &window_name) {
|
bool graphics_gitadora_has_window_override(const std::string &window_name) {
|
||||||
const auto &monitor_name = graphics_gitadora_monitor_for_window_name(window_name);
|
const auto &monitor_name = graphics_gitadora_monitor_for_window_name(window_name);
|
||||||
return !monitor_name.empty();
|
return !monitor_name.empty() ||
|
||||||
|
(window_name == "SMALL" &&
|
||||||
|
(GRAPHICS_GITADORA_SMALL_SIZE.has_value() ||
|
||||||
|
GRAPHICS_GITADORA_SMALL_POS.has_value()));
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool graphics_monitor_rect_from_name(
|
static bool graphics_monitor_rect_from_name(
|
||||||
@@ -212,7 +217,11 @@ bool graphics_gitadora_apply_window_monitor(
|
|||||||
int &width,
|
int &width,
|
||||||
int &height,
|
int &height,
|
||||||
bool log_change) {
|
bool log_change) {
|
||||||
return graphics_gitadora_apply_monitor_rect(
|
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool applied = graphics_gitadora_apply_monitor_rect(
|
||||||
graphics_gitadora_monitor_for_window_name(window_name),
|
graphics_gitadora_monitor_for_window_name(window_name),
|
||||||
window_name,
|
window_name,
|
||||||
x,
|
x,
|
||||||
@@ -220,6 +229,42 @@ bool graphics_gitadora_apply_window_monitor(
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
log_change);
|
log_change);
|
||||||
|
|
||||||
|
if (window_name != "SMALL") {
|
||||||
|
return applied;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<uint32_t, uint32_t> result;
|
||||||
|
if (GRAPHICS_GITADORA_SMALL_SIZE.has_value()) {
|
||||||
|
if (parse_width_height(GRAPHICS_GITADORA_SMALL_SIZE.value(), result)) {
|
||||||
|
width = result.first;
|
||||||
|
height = result.second;
|
||||||
|
applied = true;
|
||||||
|
} else {
|
||||||
|
log_fatal("graphics-windowed", "failed to parse -gdwsmallsize");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (GRAPHICS_GITADORA_SMALL_POS.has_value()) {
|
||||||
|
if (parse_width_height(GRAPHICS_GITADORA_SMALL_POS.value(), result)) {
|
||||||
|
x = result.first;
|
||||||
|
y = result.second;
|
||||||
|
applied = true;
|
||||||
|
} else {
|
||||||
|
log_fatal("graphics-windowed", "failed to parse -gdwsmallpos");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applied && log_change &&
|
||||||
|
(GRAPHICS_GITADORA_SMALL_SIZE.has_value() || GRAPHICS_GITADORA_SMALL_POS.has_value())) {
|
||||||
|
log_info(
|
||||||
|
"graphics-windowed",
|
||||||
|
"GITADORA SMALL custom override: pos=({}, {}), size={}x{}",
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height);
|
||||||
|
}
|
||||||
|
return applied;
|
||||||
}
|
}
|
||||||
|
|
||||||
void graphics_capture_initial_window(HWND hWnd) {
|
void graphics_capture_initial_window(HWND hWnd) {
|
||||||
@@ -254,7 +299,7 @@ void graphics_capture_initial_window(HWND hWnd) {
|
|||||||
cfg::SCREENRESIZE->init_client_width = client_w;
|
cfg::SCREENRESIZE->init_client_width = client_w;
|
||||||
cfg::SCREENRESIZE->init_client_height = client_h;
|
cfg::SCREENRESIZE->init_client_height = client_h;
|
||||||
cfg::SCREENRESIZE->init_client_aspect_ratio = (float)client_w / (float)client_h;
|
cfg::SCREENRESIZE->init_client_aspect_ratio = (float)client_w / (float)client_h;
|
||||||
log_debug(
|
log_misc(
|
||||||
"graphics-windowed",
|
"graphics-windowed",
|
||||||
"[{}] graphics_capture_initial_window initial window size {}x{}, ratio {}",
|
"[{}] graphics_capture_initial_window initial window size {}x{}, ratio {}",
|
||||||
fmt::ptr(hWnd),
|
fmt::ptr(hWnd),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "external/nvapi/nvapi.h"
|
#include "external/nvapi/nvapi.h"
|
||||||
#include "external/nvapi/NvApiDriverSettings.h"
|
#include "external/nvapi/NvApiDriverSettings.h"
|
||||||
|
#include "avs/game.h"
|
||||||
#include "hooks/libraryhook.h"
|
#include "hooks/libraryhook.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
@@ -12,14 +13,21 @@ namespace nvapi_hook {
|
|||||||
bool BYPASS_NVAPI = false;
|
bool BYPASS_NVAPI = false;
|
||||||
|
|
||||||
typedef uintptr_t *(*NvAPI_QueryInterface_t)(unsigned int);
|
typedef uintptr_t *(*NvAPI_QueryInterface_t)(unsigned int);
|
||||||
|
|
||||||
static NvAPI_QueryInterface_t NvAPI_QueryInterface_orig = nullptr;
|
static NvAPI_QueryInterface_t NvAPI_QueryInterface_orig = nullptr;
|
||||||
|
static decltype(NvAPI_GPU_GetConnectedDisplayIds) *NvAPI_GPU_GetConnectedDisplayIds_orig = nullptr;
|
||||||
|
static decltype(NvAPI_DISP_GetGDIPrimaryDisplayId) *NvAPI_DISP_GetGDIPrimaryDisplayId_orig = nullptr;
|
||||||
|
|
||||||
static uintptr_t* __cdecl NvAPI_QueryInterface_hook(unsigned int func_code);
|
static uintptr_t* __cdecl NvAPI_QueryInterface_hook(unsigned int func_code);
|
||||||
|
static NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_hook(
|
||||||
|
NvPhysicalGpuHandle hPhysicalGpu,
|
||||||
|
NV_GPU_DISPLAYIDS *pDisplayIds,
|
||||||
|
NvU32 *pDisplayIdCount,
|
||||||
|
NvU32 flags);
|
||||||
static NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_hook(
|
static NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_hook(
|
||||||
NvU32 pathInfoCount, NV_DISPLAYCONFIG_PATH_INFO *pathInfo, NvU32 flags);
|
NvU32 pathInfoCount, NV_DISPLAYCONFIG_PATH_INFO *pathInfo, NvU32 flags);
|
||||||
|
|
||||||
void initialize(HINSTANCE dll) {
|
void initialize(HINSTANCE dll) {
|
||||||
|
|
||||||
#ifdef SPICE64
|
#ifdef SPICE64
|
||||||
std::string nvapi_dll = "nvapi64.dll";
|
std::string nvapi_dll = "nvapi64.dll";
|
||||||
#else
|
#else
|
||||||
@@ -40,6 +48,18 @@ namespace nvapi_hook {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// spoof display connector types only for SDVX
|
||||||
|
if (func_code == 0x0078DBA2 && avs::game::is_model("KFC")) {
|
||||||
|
NvAPI_GPU_GetConnectedDisplayIds_orig =
|
||||||
|
reinterpret_cast<decltype(NvAPI_GPU_GetConnectedDisplayIds) *>(
|
||||||
|
NvAPI_QueryInterface_orig(func_code));
|
||||||
|
NvAPI_DISP_GetGDIPrimaryDisplayId_orig =
|
||||||
|
reinterpret_cast<decltype(NvAPI_DISP_GetGDIPrimaryDisplayId) *>(
|
||||||
|
NvAPI_QueryInterface_orig(0x1E9D8A31));
|
||||||
|
log_misc("nvapi_hook", "NvAPI_QueryInterface(NvAPI_GPU_GetConnectedDisplayIds) - hooked");
|
||||||
|
return (uintptr_t *)NvAPI_GPU_GetConnectedDisplayIds_hook;
|
||||||
|
}
|
||||||
|
|
||||||
// NvAPI_DISP_SetDisplayConfig
|
// NvAPI_DISP_SetDisplayConfig
|
||||||
if (func_code == 0x5D8CF8DE) {
|
if (func_code == 0x5D8CF8DE) {
|
||||||
log_misc("nvapi_hook", "NvAPI_QueryInterface(NvAPI_DISP_SetDisplayConfig) - hooked");
|
log_misc("nvapi_hook", "NvAPI_QueryInterface(NvAPI_DISP_SetDisplayConfig) - hooked");
|
||||||
@@ -51,8 +71,53 @@ namespace nvapi_hook {
|
|||||||
return NvAPI_QueryInterface_orig(func_code);
|
return NvAPI_QueryInterface_orig(func_code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_hook(
|
||||||
|
NvPhysicalGpuHandle hPhysicalGpu,
|
||||||
|
NV_GPU_DISPLAYIDS *pDisplayIds,
|
||||||
|
NvU32 *pDisplayIdCount,
|
||||||
|
NvU32 flags) {
|
||||||
|
|
||||||
|
if (NvAPI_GPU_GetConnectedDisplayIds_orig == nullptr) {
|
||||||
|
return NVAPI_NO_IMPLEMENTATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NvAPI_Status status = NvAPI_GPU_GetConnectedDisplayIds_orig(
|
||||||
|
hPhysicalGpu,
|
||||||
|
pDisplayIds,
|
||||||
|
pDisplayIdCount,
|
||||||
|
flags);
|
||||||
|
|
||||||
|
if (status != NVAPI_OK || pDisplayIds == nullptr || pDisplayIdCount == nullptr ||
|
||||||
|
NvAPI_DISP_GetGDIPrimaryDisplayId_orig == nullptr) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
NvU32 primary_display_id = 0;
|
||||||
|
if (NvAPI_DISP_GetGDIPrimaryDisplayId_orig(&primary_display_id) != NVAPI_OK) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// report the primary display adapter as DP and all other displays as HDMI
|
||||||
|
// (the game expects this since the VM cab is like that)
|
||||||
|
//
|
||||||
|
// this doesn't actually affect gameplay; only how
|
||||||
|
// NvDisplayConfig: MainDisplay={}hz / SubDisplay={}hz
|
||||||
|
// gets printed.
|
||||||
|
//
|
||||||
|
// for the actual value that matters for gameplay, it's this:
|
||||||
|
// NvDisplayConfig: GetMainDisplayRefreshRate = {}hz
|
||||||
|
for (NvU32 index = 0; index < *pDisplayIdCount; index++) {
|
||||||
|
auto &display = pDisplayIds[index];
|
||||||
|
display.connectorType = display.displayId == primary_display_id ?
|
||||||
|
NV_MONITOR_CONN_TYPE_DP : NV_MONITOR_CONN_TYPE_HDMI;
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_hook(
|
NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_hook(
|
||||||
NvU32 pathInfoCount, NV_DISPLAYCONFIG_PATH_INFO *pathInfo, NvU32 flags) {
|
NvU32 pathInfoCount, NV_DISPLAYCONFIG_PATH_INFO *pathInfo, NvU32 flags) {
|
||||||
|
// do not let the game apply its emulated cabinet topology to Windows
|
||||||
log_misc("nvapi_hook", "NvAPI_DISP_SetDisplayConfig_hook - do nothing and return");
|
log_misc("nvapi_hook", "NvAPI_DISP_SetDisplayConfig_hook - do nothing and return");
|
||||||
return NVAPI_OK;
|
return NVAPI_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,480 @@
|
|||||||
|
#include "nvapi_impl.h"
|
||||||
|
|
||||||
|
#ifdef SPICE64
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "external/nvapi/nvapi.h"
|
||||||
|
#include "hooks/libraryhook.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
#include "util/sysutils.h"
|
||||||
|
|
||||||
|
namespace nvapi_impl {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr unsigned int NVAPI_INITIALIZE_ID = 0x0150E828;
|
||||||
|
constexpr unsigned int NVAPI_INITIALIZE_EX_ID = 0xAD298D3F;
|
||||||
|
constexpr unsigned int NVAPI_UNLOAD_ID = 0xD22BDD7E;
|
||||||
|
constexpr unsigned int NVAPI_ENUM_PHYSICAL_GPUS_ID = 0xE5AC921F;
|
||||||
|
constexpr unsigned int NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID = 0x0078DBA2;
|
||||||
|
constexpr unsigned int NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID = 0x1E9D8A31;
|
||||||
|
constexpr unsigned int NVAPI_DISP_GET_DISPLAY_CONFIG_ID = 0x11ABCCF8;
|
||||||
|
constexpr unsigned int NVAPI_DISP_SET_DISPLAY_CONFIG_ID = 0x5D8CF8DE;
|
||||||
|
|
||||||
|
constexpr char NVAPI_DLL_NAME_A[] = "nvapi64.dll";
|
||||||
|
|
||||||
|
struct SyntheticDisplay {
|
||||||
|
NvU32 display_id;
|
||||||
|
NvU32 width;
|
||||||
|
NvU32 height;
|
||||||
|
NvU32 color_depth;
|
||||||
|
NvS32 x;
|
||||||
|
NvS32 y;
|
||||||
|
NvU32 refresh_rate_1k;
|
||||||
|
NV_ROTATE rotation;
|
||||||
|
bool primary;
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool provider_initialized = false;
|
||||||
|
static bool nvapi_initialized = false;
|
||||||
|
static int gpu_handle_storage = 0;
|
||||||
|
// snapshot of the Win32 display state exposed through synthetic NVAPI
|
||||||
|
static std::vector<SyntheticDisplay> displays;
|
||||||
|
|
||||||
|
static NvPhysicalGpuHandle get_gpu_handle() {
|
||||||
|
return reinterpret_cast<NvPhysicalGpuHandle>(&gpu_handle_storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
static NV_ROTATE get_rotation(DWORD orientation) {
|
||||||
|
switch (orientation) {
|
||||||
|
case DMDO_90:
|
||||||
|
return NV_ROTATE_90;
|
||||||
|
case DMDO_180:
|
||||||
|
return NV_ROTATE_180;
|
||||||
|
case DMDO_270:
|
||||||
|
return NV_ROTATE_270;
|
||||||
|
default:
|
||||||
|
return NV_ROTATE_0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<SyntheticDisplay> enumerate_displays(
|
||||||
|
uint32_t main_refresh_hz,
|
||||||
|
uint32_t sub_refresh_hz) {
|
||||||
|
|
||||||
|
std::vector<SyntheticDisplay> result;
|
||||||
|
|
||||||
|
// reuse the active monitor list, then read live modes after -mainmonitor changes
|
||||||
|
for (const auto &monitor : sysutils::enumerate_monitors()) {
|
||||||
|
DEVMODEA mode {};
|
||||||
|
mode.dmSize = sizeof(mode);
|
||||||
|
if (!EnumDisplaySettingsExA(
|
||||||
|
monitor.display_name.c_str(),
|
||||||
|
ENUM_CURRENT_SETTINGS,
|
||||||
|
&mode,
|
||||||
|
0)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool primary = mode.dmPosition.x == 0 && mode.dmPosition.y == 0;
|
||||||
|
result.push_back({
|
||||||
|
.display_id = 0,
|
||||||
|
.width = mode.dmPelsWidth,
|
||||||
|
.height = mode.dmPelsHeight,
|
||||||
|
.color_depth = mode.dmBitsPerPel > 0 ? mode.dmBitsPerPel : 32,
|
||||||
|
.x = mode.dmPosition.x,
|
||||||
|
.y = mode.dmPosition.y,
|
||||||
|
.refresh_rate_1k = 0,
|
||||||
|
.rotation = get_rotation(mode.dmDisplayOrientation),
|
||||||
|
.primary = primary,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stable_sort(result.begin(), result.end(), [](const auto &left, const auto &right) {
|
||||||
|
return left.primary && !right.primary;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.size() > 2) {
|
||||||
|
result.resize(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.empty()) {
|
||||||
|
result.push_back({
|
||||||
|
.display_id = 0,
|
||||||
|
.width = 1920,
|
||||||
|
.height = 1080,
|
||||||
|
.color_depth = 32,
|
||||||
|
.x = 0,
|
||||||
|
.y = 0,
|
||||||
|
.refresh_rate_1k = 0,
|
||||||
|
.rotation = NV_ROTATE_0,
|
||||||
|
.primary = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t index = 0; index < result.size(); index++) {
|
||||||
|
auto &display = result[index];
|
||||||
|
display.primary = index == 0;
|
||||||
|
display.display_id = 0x80000000u | static_cast<NvU32>(index + 1);
|
||||||
|
const uint32_t refresh_hz = index == 0 ? main_refresh_hz : sub_refresh_hz;
|
||||||
|
display.refresh_rate_1k = refresh_hz * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// initializes NVAPI for the calling process.
|
||||||
|
// marks the synthetic provider initialized without contacting a driver.
|
||||||
|
static NvAPI_Status __cdecl NvAPI_Initialize_impl() {
|
||||||
|
log_misc("nvapi_impl", "NvAPI_Initialize");
|
||||||
|
nvapi_initialized = true;
|
||||||
|
return NVAPI_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// initializes NVAPI with additional client flags.
|
||||||
|
// accepts the flags and marks the synthetic provider initialized.
|
||||||
|
static NvAPI_Status __cdecl NvAPI_InitializeEx_impl(NvU32 flags) {
|
||||||
|
log_misc("nvapi_impl", "NvAPI_InitializeEx(flags={:#x})", flags);
|
||||||
|
nvapi_initialized = true;
|
||||||
|
return NVAPI_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// releases NVAPI state held for the calling process.
|
||||||
|
// clears the synthetic initialization state while leaving the provider installed.
|
||||||
|
static NvAPI_Status __cdecl NvAPI_Unload_impl() {
|
||||||
|
log_misc("nvapi_impl", "NvAPI_Unload");
|
||||||
|
nvapi_initialized = false;
|
||||||
|
return NVAPI_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// enumerates physical GPU handles managed by the NVIDIA driver.
|
||||||
|
// returns one stable synthetic GPU containing all exposed displays.
|
||||||
|
static NvAPI_Status __cdecl NvAPI_EnumPhysicalGPUs_impl(
|
||||||
|
NvPhysicalGpuHandle gpu_handles[NVAPI_MAX_PHYSICAL_GPUS],
|
||||||
|
NvU32 *gpu_count) {
|
||||||
|
|
||||||
|
log_misc(
|
||||||
|
"nvapi_impl",
|
||||||
|
"NvAPI_EnumPhysicalGPUs(handles={}, count={})",
|
||||||
|
fmt::ptr(gpu_handles),
|
||||||
|
fmt::ptr(gpu_count));
|
||||||
|
|
||||||
|
if (!nvapi_initialized) {
|
||||||
|
return NVAPI_API_NOT_INITIALIZED;
|
||||||
|
}
|
||||||
|
if (gpu_handles == nullptr || gpu_count == nullptr) {
|
||||||
|
return NVAPI_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
gpu_handles[0] = get_gpu_handle();
|
||||||
|
*gpu_count = 1;
|
||||||
|
log_misc(
|
||||||
|
"nvapi_impl",
|
||||||
|
"NvAPI_EnumPhysicalGPUs - gpu={}, count={}",
|
||||||
|
fmt::ptr(gpu_handles[0]),
|
||||||
|
*gpu_count);
|
||||||
|
return NVAPI_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns connected display descriptors for a physical GPU.
|
||||||
|
// exposes the monitor snapshot as DP primary and HDMI secondary displays.
|
||||||
|
static NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_impl(
|
||||||
|
NvPhysicalGpuHandle gpu_handle,
|
||||||
|
NV_GPU_DISPLAYIDS *display_ids,
|
||||||
|
NvU32 *display_id_count,
|
||||||
|
NvU32 flags) {
|
||||||
|
|
||||||
|
const NvU32 input_count = display_id_count != nullptr ? *display_id_count : 0;
|
||||||
|
log_misc(
|
||||||
|
"nvapi_impl",
|
||||||
|
"NvAPI_GPU_GetConnectedDisplayIds(gpu={}, ids={}, count={}, flags={:#x})",
|
||||||
|
fmt::ptr(gpu_handle),
|
||||||
|
fmt::ptr(display_ids),
|
||||||
|
input_count,
|
||||||
|
flags);
|
||||||
|
|
||||||
|
if (!nvapi_initialized) {
|
||||||
|
return NVAPI_API_NOT_INITIALIZED;
|
||||||
|
}
|
||||||
|
if (gpu_handle != get_gpu_handle()) {
|
||||||
|
return NVAPI_EXPECTED_PHYSICAL_GPU_HANDLE;
|
||||||
|
}
|
||||||
|
if (display_id_count == nullptr) {
|
||||||
|
return NVAPI_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NvU32 required_count = static_cast<NvU32>(displays.size());
|
||||||
|
if (display_ids == nullptr) {
|
||||||
|
*display_id_count = required_count;
|
||||||
|
log_misc(
|
||||||
|
"nvapi_impl",
|
||||||
|
"NvAPI_GPU_GetConnectedDisplayIds - required_count={}",
|
||||||
|
required_count);
|
||||||
|
return NVAPI_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NvU32 capacity = *display_id_count;
|
||||||
|
*display_id_count = required_count;
|
||||||
|
if (capacity < required_count) {
|
||||||
|
return NVAPI_INSUFFICIENT_BUFFER;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (NvU32 index = 0; index < required_count; index++) {
|
||||||
|
const auto &source = displays[index];
|
||||||
|
auto &destination = display_ids[index];
|
||||||
|
destination = {};
|
||||||
|
destination.version = NV_GPU_DISPLAYIDS_VER;
|
||||||
|
destination.connectorType = source.primary ?
|
||||||
|
NV_MONITOR_CONN_TYPE_DP : NV_MONITOR_CONN_TYPE_HDMI;
|
||||||
|
destination.displayId = source.display_id;
|
||||||
|
destination.isActive = 1;
|
||||||
|
destination.isOSVisible = 1;
|
||||||
|
destination.isConnected = 1;
|
||||||
|
destination.isPhysicallyConnected = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_misc(
|
||||||
|
"nvapi_impl",
|
||||||
|
"NvAPI_GPU_GetConnectedDisplayIds - returned_count={}",
|
||||||
|
required_count);
|
||||||
|
return NVAPI_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the NVAPI display ID associated with the Windows GDI primary.
|
||||||
|
// returns the first synthetic display, ordered from the live desktop origin.
|
||||||
|
static NvAPI_Status __cdecl NvAPI_DISP_GetGDIPrimaryDisplayId_impl(NvU32 *display_id) {
|
||||||
|
log_misc(
|
||||||
|
"nvapi_impl",
|
||||||
|
"NvAPI_DISP_GetGDIPrimaryDisplayId(display_id={})",
|
||||||
|
fmt::ptr(display_id));
|
||||||
|
|
||||||
|
if (!nvapi_initialized) {
|
||||||
|
return NVAPI_API_NOT_INITIALIZED;
|
||||||
|
}
|
||||||
|
if (display_id == nullptr || displays.empty()) {
|
||||||
|
return NVAPI_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
*display_id = displays.front().display_id;
|
||||||
|
log_misc(
|
||||||
|
"nvapi_impl",
|
||||||
|
"NvAPI_DISP_GetGDIPrimaryDisplayId - display_id={:#x}",
|
||||||
|
*display_id);
|
||||||
|
return NVAPI_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fill_source_mode(
|
||||||
|
NV_DISPLAYCONFIG_SOURCE_MODE_INFO *destination,
|
||||||
|
const SyntheticDisplay &source) {
|
||||||
|
|
||||||
|
if (destination == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
*destination = {};
|
||||||
|
destination->resolution.width = source.width;
|
||||||
|
destination->resolution.height = source.height;
|
||||||
|
destination->resolution.colorDepth = source.color_depth;
|
||||||
|
destination->colorFormat = NV_FORMAT_A8R8G8B8;
|
||||||
|
destination->position.x = source.x;
|
||||||
|
destination->position.y = source.y;
|
||||||
|
destination->spanningOrientation = NV_DISPLAYCONFIG_SPAN_NONE;
|
||||||
|
destination->bGDIPrimary = source.primary ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NvAPI_Status fill_target(
|
||||||
|
NV_DISPLAYCONFIG_PATH_TARGET_INFO *destination,
|
||||||
|
const SyntheticDisplay &source,
|
||||||
|
NvU32 target_id) {
|
||||||
|
|
||||||
|
if (destination == nullptr) {
|
||||||
|
return NVAPI_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *details = destination->details;
|
||||||
|
destination->displayId = source.display_id;
|
||||||
|
destination->targetId = target_id;
|
||||||
|
|
||||||
|
if (details == nullptr) {
|
||||||
|
return NVAPI_OK;
|
||||||
|
}
|
||||||
|
if (details->version != NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER) {
|
||||||
|
return NVAPI_INCOMPATIBLE_STRUCT_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
*details = {};
|
||||||
|
details->version = NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER;
|
||||||
|
details->rotation = source.rotation;
|
||||||
|
details->scaling = NV_SCALING_DEFAULT;
|
||||||
|
details->refreshRate1K = source.refresh_rate_1k;
|
||||||
|
details->timingOverride = NV_TIMING_OVERRIDE_CURRENT;
|
||||||
|
return NVAPI_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// retrieves the current global display topology through NVAPI's three-pass contract.
|
||||||
|
// fills caller-owned buffers from the synthetic monitor snapshot and configured rates.
|
||||||
|
static NvAPI_Status __cdecl NvAPI_DISP_GetDisplayConfig_impl(
|
||||||
|
NvU32 *path_info_count,
|
||||||
|
NV_DISPLAYCONFIG_PATH_INFO *path_info) {
|
||||||
|
|
||||||
|
const NvU32 input_count = path_info_count != nullptr ? *path_info_count : 0;
|
||||||
|
log_misc(
|
||||||
|
"nvapi_impl",
|
||||||
|
"NvAPI_DISP_GetDisplayConfig(count={}, paths={})",
|
||||||
|
input_count,
|
||||||
|
fmt::ptr(path_info));
|
||||||
|
|
||||||
|
if (!nvapi_initialized) {
|
||||||
|
return NVAPI_API_NOT_INITIALIZED;
|
||||||
|
}
|
||||||
|
if (path_info_count == nullptr) {
|
||||||
|
return NVAPI_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NvU32 required_count = static_cast<NvU32>(displays.size());
|
||||||
|
if (path_info == nullptr) {
|
||||||
|
*path_info_count = required_count;
|
||||||
|
log_misc(
|
||||||
|
"nvapi_impl",
|
||||||
|
"NvAPI_DISP_GetDisplayConfig - required_count={}",
|
||||||
|
required_count);
|
||||||
|
return NVAPI_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NvU32 capacity = *path_info_count;
|
||||||
|
*path_info_count = required_count;
|
||||||
|
if (capacity < required_count) {
|
||||||
|
return NVAPI_INSUFFICIENT_BUFFER;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (NvU32 index = 0; index < required_count; index++) {
|
||||||
|
auto &path = path_info[index];
|
||||||
|
if (path.version != NV_DISPLAYCONFIG_PATH_INFO_VER2) {
|
||||||
|
return NVAPI_INCOMPATIBLE_STRUCT_VERSION;
|
||||||
|
}
|
||||||
|
if (path.targetInfo != nullptr && path.targetInfoCount < 1) {
|
||||||
|
return NVAPI_INSUFFICIENT_BUFFER;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &display = displays[index];
|
||||||
|
path.sourceId = index;
|
||||||
|
path.targetInfoCount = 1;
|
||||||
|
path.IsNonNVIDIAAdapter = 0;
|
||||||
|
path.pOSAdapterID = nullptr;
|
||||||
|
fill_source_mode(path.sourceModeInfo, display);
|
||||||
|
|
||||||
|
const NvAPI_Status status = fill_target(path.targetInfo, display, index);
|
||||||
|
if (status != NVAPI_OK) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log_misc(
|
||||||
|
"nvapi_impl",
|
||||||
|
"NvAPI_DISP_GetDisplayConfig - returned_count={}",
|
||||||
|
required_count);
|
||||||
|
return NVAPI_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// applies a supplied global display topology through the NVIDIA driver.
|
||||||
|
// accepts the cabinet topology without making any changes to Windows.
|
||||||
|
static NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_impl(
|
||||||
|
NvU32 path_info_count,
|
||||||
|
NV_DISPLAYCONFIG_PATH_INFO *path_info,
|
||||||
|
NvU32 flags) {
|
||||||
|
|
||||||
|
log_misc(
|
||||||
|
"nvapi_impl",
|
||||||
|
"NvAPI_DISP_SetDisplayConfig(count={}, paths={}, flags={:#x})",
|
||||||
|
path_info_count,
|
||||||
|
fmt::ptr(path_info),
|
||||||
|
flags);
|
||||||
|
|
||||||
|
if (!nvapi_initialized) {
|
||||||
|
return NVAPI_API_NOT_INITIALIZED;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_misc("nvapi_impl", "NvAPI_DISP_SetDisplayConfig - return synthetic success");
|
||||||
|
return NVAPI_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
static uintptr_t *query_result(T function) {
|
||||||
|
return reinterpret_cast<uintptr_t *>(function);
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolves an NVAPI function ID to its implementation address.
|
||||||
|
// exposes only the synthetic entry points used by KFC and rejects all others.
|
||||||
|
static uintptr_t *__cdecl NvAPI_QueryInterface_impl(unsigned int function_id) {
|
||||||
|
uintptr_t *result = nullptr;
|
||||||
|
switch (function_id) {
|
||||||
|
case NVAPI_INITIALIZE_ID:
|
||||||
|
result = query_result(NvAPI_Initialize_impl);
|
||||||
|
break;
|
||||||
|
case NVAPI_INITIALIZE_EX_ID:
|
||||||
|
result = query_result(NvAPI_InitializeEx_impl);
|
||||||
|
break;
|
||||||
|
case NVAPI_UNLOAD_ID:
|
||||||
|
result = query_result(NvAPI_Unload_impl);
|
||||||
|
break;
|
||||||
|
case NVAPI_ENUM_PHYSICAL_GPUS_ID:
|
||||||
|
result = query_result(NvAPI_EnumPhysicalGPUs_impl);
|
||||||
|
break;
|
||||||
|
case NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID:
|
||||||
|
result = query_result(NvAPI_GPU_GetConnectedDisplayIds_impl);
|
||||||
|
break;
|
||||||
|
case NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID:
|
||||||
|
result = query_result(NvAPI_DISP_GetGDIPrimaryDisplayId_impl);
|
||||||
|
break;
|
||||||
|
case NVAPI_DISP_GET_DISPLAY_CONFIG_ID:
|
||||||
|
result = query_result(NvAPI_DISP_GetDisplayConfig_impl);
|
||||||
|
break;
|
||||||
|
case NVAPI_DISP_SET_DISPLAY_CONFIG_ID:
|
||||||
|
result = query_result(NvAPI_DISP_SetDisplayConfig_impl);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_misc(
|
||||||
|
"nvapi_impl",
|
||||||
|
"NvAPI_QueryInterface(0x{:x}) - {}",
|
||||||
|
function_id,
|
||||||
|
result != nullptr ? "implemented" : "unsupported");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz) {
|
||||||
|
if (provider_initialized) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (dll == nullptr) {
|
||||||
|
log_warning("nvapi_impl", "invalid synthetic module handle");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
displays = enumerate_displays(main_refresh_hz, sub_refresh_hz);
|
||||||
|
libraryhook_hook_library(NVAPI_DLL_NAME_A, dll);
|
||||||
|
libraryhook_hook_proc("nvapi_QueryInterface", NvAPI_QueryInterface_impl);
|
||||||
|
libraryhook_enable();
|
||||||
|
|
||||||
|
provider_initialized = true;
|
||||||
|
log_info(
|
||||||
|
"nvapi_impl",
|
||||||
|
"synthetic {} enabled with {} display(s), main={} Hz, sub={} Hz",
|
||||||
|
NVAPI_DLL_NAME_A,
|
||||||
|
displays.size(),
|
||||||
|
main_refresh_hz,
|
||||||
|
sub_refresh_hz);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef SPICE64
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
namespace nvapi_impl {
|
||||||
|
|
||||||
|
bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -296,8 +296,9 @@ void hooks::lang::early_init() {
|
|||||||
&GetLocaleInfoA_orig);
|
&GetLocaleInfoA_orig);
|
||||||
}
|
}
|
||||||
|
|
||||||
// for TDJ subscreen search keyboard
|
// for TDJ subscreen search keyboard and T44 narrow-string handling
|
||||||
if (avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) {
|
if ((avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) ||
|
||||||
|
avs::game::is_model("T44")) {
|
||||||
log_info("hooks::lang", "hooking IsDBCSLeadByte");
|
log_info("hooks::lang", "hooking IsDBCSLeadByte");
|
||||||
detour::trampoline_try(
|
detour::trampoline_try(
|
||||||
"kernel32.dll",
|
"kernel32.dll",
|
||||||
@@ -306,7 +307,7 @@ void hooks::lang::early_init() {
|
|||||||
&IsDBCSLeadByte_orig);
|
&IsDBCSLeadByte_orig);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (games::gitadora::is_arena_model()) {
|
if (games::gitadora::is_arena_model() || avs::game::is_model("T44")) {
|
||||||
log_info("hooks::lang", "hooking WideCharToMultiByte");
|
log_info("hooks::lang", "hooking WideCharToMultiByte");
|
||||||
detour::trampoline_try(
|
detour::trampoline_try(
|
||||||
"kernel32.dll",
|
"kernel32.dll",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ static robin_hood::unordered_map<std::string, FARPROC> PROCS;
|
|||||||
|
|
||||||
static decltype(LoadLibraryA) *LoadLibraryA_orig = nullptr;
|
static decltype(LoadLibraryA) *LoadLibraryA_orig = nullptr;
|
||||||
static decltype(LoadLibraryW) *LoadLibraryW_orig = nullptr;
|
static decltype(LoadLibraryW) *LoadLibraryW_orig = nullptr;
|
||||||
|
static decltype(LoadLibraryExW) *LoadLibraryExW_orig = nullptr;
|
||||||
static decltype(GetModuleHandleA) *GetModuleHandleA_orig = nullptr;
|
static decltype(GetModuleHandleA) *GetModuleHandleA_orig = nullptr;
|
||||||
static decltype(GetModuleHandleW) *GetModuleHandleW_orig = nullptr;
|
static decltype(GetModuleHandleW) *GetModuleHandleW_orig = nullptr;
|
||||||
static decltype(GetProcAddress) *GetProcAddress_orig = nullptr;
|
static decltype(GetProcAddress) *GetProcAddress_orig = nullptr;
|
||||||
@@ -44,6 +45,20 @@ static HMODULE WINAPI LoadLibraryW_hook(LPCWSTR lpFileName) {
|
|||||||
return LoadLibraryW_orig(lpFileName);
|
return LoadLibraryW_orig(lpFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static HMODULE WINAPI LoadLibraryExW_hook(LPCWSTR lpFileName, HANDLE hFile, DWORD dwFlags) {
|
||||||
|
|
||||||
|
// check hooks
|
||||||
|
if (lpFileName) {
|
||||||
|
auto module = LIBRARIES_W.find(lpFileName);
|
||||||
|
if (module != LIBRARIES_W.end()) {
|
||||||
|
return module->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback
|
||||||
|
return LoadLibraryExW_orig(lpFileName, hFile, dwFlags);
|
||||||
|
}
|
||||||
|
|
||||||
static HMODULE WINAPI GetModuleHandleA_hook(LPCSTR lpModuleName) {
|
static HMODULE WINAPI GetModuleHandleA_hook(LPCSTR lpModuleName) {
|
||||||
|
|
||||||
// check hooks
|
// check hooks
|
||||||
@@ -103,6 +118,7 @@ void libraryhook_enable(HMODULE module) {
|
|||||||
// detour
|
// detour
|
||||||
detour::trampoline_try("kernel32.dll", "LoadLibraryA", LoadLibraryA_hook, &LoadLibraryA_orig);
|
detour::trampoline_try("kernel32.dll", "LoadLibraryA", LoadLibraryA_hook, &LoadLibraryA_orig);
|
||||||
detour::trampoline_try("kernel32.dll", "LoadLibraryW", LoadLibraryW_hook, &LoadLibraryW_orig);
|
detour::trampoline_try("kernel32.dll", "LoadLibraryW", LoadLibraryW_hook, &LoadLibraryW_orig);
|
||||||
|
detour::trampoline_try("kernel32.dll", "LoadLibraryExW", LoadLibraryExW_hook, &LoadLibraryExW_orig);
|
||||||
detour::trampoline_try("kernel32.dll", "GetModuleHandleA", GetModuleHandleA_hook, &GetModuleHandleA_orig);
|
detour::trampoline_try("kernel32.dll", "GetModuleHandleA", GetModuleHandleA_hook, &GetModuleHandleA_orig);
|
||||||
detour::trampoline_try("kernel32.dll", "GetModuleHandleW", GetModuleHandleW_hook, &GetModuleHandleW_orig);
|
detour::trampoline_try("kernel32.dll", "GetModuleHandleW", GetModuleHandleW_hook, &GetModuleHandleW_orig);
|
||||||
detour::trampoline_try("kernel32.dll", "GetProcAddress", GetProcAddress_hook, &GetProcAddress_orig);
|
detour::trampoline_try("kernel32.dll", "GetProcAddress", GetProcAddress_hook, &GetProcAddress_orig);
|
||||||
|
|||||||
@@ -38,10 +38,10 @@
|
|||||||
#include "games/gitadora/gitadora.h"
|
#include "games/gitadora/gitadora.h"
|
||||||
#include "games/hpm/hpm.h"
|
#include "games/hpm/hpm.h"
|
||||||
#include "games/iidx/iidx.h"
|
#include "games/iidx/iidx.h"
|
||||||
|
#include "games/iidx/poke.h"
|
||||||
#ifdef SPICE64
|
#ifdef SPICE64
|
||||||
#include "games/iidx/camera.h"
|
#include "games/iidx/camera.h"
|
||||||
#endif
|
#endif
|
||||||
#include "games/iidx/poke.h"
|
|
||||||
#include "games/jb/jb.h"
|
#include "games/jb/jb.h"
|
||||||
#include "games/mga/mga.h"
|
#include "games/mga/mga.h"
|
||||||
#include "games/nost/nost.h"
|
#include "games/nost/nost.h"
|
||||||
@@ -49,10 +49,12 @@
|
|||||||
#include "games/popn/popn.h"
|
#include "games/popn/popn.h"
|
||||||
#include "games/qma/qma.h"
|
#include "games/qma/qma.h"
|
||||||
#include "games/rb/rb.h"
|
#include "games/rb/rb.h"
|
||||||
|
#include "games/rb/touch_debug.h"
|
||||||
#include "games/rf3d/rf3d.h"
|
#include "games/rf3d/rf3d.h"
|
||||||
#include "games/sc/sc.h"
|
#include "games/sc/sc.h"
|
||||||
#include "games/scotto/scotto.h"
|
#include "games/scotto/scotto.h"
|
||||||
#include "games/sdvx/sdvx.h"
|
#include "games/sdvx/sdvx.h"
|
||||||
|
#include "games/sdvx/sdvx_live2d.h"
|
||||||
#include "games/shared/printer.h"
|
#include "games/shared/printer.h"
|
||||||
#include "games/silentscope/silentscope.h"
|
#include "games/silentscope/silentscope.h"
|
||||||
#include "games/mfc/mfc.h"
|
#include "games/mfc/mfc.h"
|
||||||
@@ -100,8 +102,9 @@
|
|||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
#include "overlay/notifications.h"
|
#include "overlay/notifications.h"
|
||||||
#include "overlay/windows/patch_manager.h"
|
#include "patcher/patch_manager.h"
|
||||||
#include "overlay/windows/iidx_seg.h"
|
#include "overlay/windows/iidx_seg.h"
|
||||||
|
#include "overlay/windows/obs.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
#include "rawinput/touch.h"
|
#include "rawinput/touch.h"
|
||||||
#include "reader/reader.h"
|
#include "reader/reader.h"
|
||||||
@@ -376,6 +379,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
if (options[launcher::Options::spice2x_IIDXNoSub].value_bool()) {
|
if (options[launcher::Options::spice2x_IIDXNoSub].value_bool()) {
|
||||||
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
|
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
|
||||||
|
GRAPHICS_PREVENT_SECONDARY_WINDOWS = true;
|
||||||
}
|
}
|
||||||
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;
|
||||||
@@ -425,9 +429,6 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
|
|
||||||
if (options[launcher::Options::spice2x_NoD3D9DeviceHook].value_bool()) {
|
if (options[launcher::Options::spice2x_NoD3D9DeviceHook].value_bool()) {
|
||||||
D3D9_DEVICE_HOOK_DISABLE = true;
|
D3D9_DEVICE_HOOK_DISABLE = true;
|
||||||
// touch emulation gets disabled, might as well turn these on
|
|
||||||
games::iidx::NATIVE_TOUCH = true;
|
|
||||||
games::sdvx::NATIVETOUCH = true;
|
|
||||||
// not strictly necessary as it will fail to init anyway, but cleaner to just disable it now
|
// not strictly necessary as it will fail to init anyway, but cleaner to just disable it now
|
||||||
overlay::ENABLED = false;
|
overlay::ENABLED = false;
|
||||||
// leaving these on without dx9hooks result in torn state and therefore failure to draw
|
// leaving these on without dx9hooks result in torn state and therefore failure to draw
|
||||||
@@ -487,9 +488,6 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::LoadSoundVoltexModule].value_bool()) {
|
if (options[launcher::Options::LoadSoundVoltexModule].value_bool()) {
|
||||||
attach_sdvx = true;
|
attach_sdvx = true;
|
||||||
}
|
}
|
||||||
if (options[launcher::Options::SDVXNativeTouch].value_bool()) {
|
|
||||||
games::sdvx::NATIVETOUCH = true;
|
|
||||||
}
|
|
||||||
if (options[launcher::Options::spice2x_SDVXDigitalKnobSensitivity].is_active()) {
|
if (options[launcher::Options::spice2x_SDVXDigitalKnobSensitivity].is_active()) {
|
||||||
games::sdvx::DIGITAL_KNOB_SENS = (uint8_t)
|
games::sdvx::DIGITAL_KNOB_SENS = (uint8_t)
|
||||||
options[launcher::Options::spice2x_SDVXDigitalKnobSensitivity].value_uint32();
|
options[launcher::Options::spice2x_SDVXDigitalKnobSensitivity].value_uint32();
|
||||||
@@ -590,9 +588,6 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::spice2x_IIDXNoESpec].value_bool()) {
|
if (options[launcher::Options::spice2x_IIDXNoESpec].value_bool()) {
|
||||||
games::iidx::DISABLE_ESPEC_IO = true;
|
games::iidx::DISABLE_ESPEC_IO = true;
|
||||||
}
|
}
|
||||||
if (options[launcher::Options::spice2x_IIDXNativeTouch].value_bool()) {
|
|
||||||
games::iidx::NATIVE_TOUCH = true;
|
|
||||||
}
|
|
||||||
// should come later since this will override a few settings
|
// should come later since this will override a few settings
|
||||||
if (options[launcher::Options::spice2x_IIDXWindowedTDJ].value_bool() ||
|
if (options[launcher::Options::spice2x_IIDXWindowedTDJ].value_bool() ||
|
||||||
(options[launcher::Options::IIDXTDJMode].value_bool() && GRAPHICS_WINDOWED)) {
|
(options[launcher::Options::IIDXTDJMode].value_bool() && GRAPHICS_WINDOWED)) {
|
||||||
@@ -640,8 +635,8 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
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();
|
||||||
}
|
}
|
||||||
if (options[launcher::Options::PopnNativeTouch].value_bool()) {
|
if (options[launcher::Options::PopnSubRedraw].value_bool()) {
|
||||||
games::popn::NATIVE_TOUCH = true;
|
SUBSCREEN_FORCE_REDRAW = true;
|
||||||
}
|
}
|
||||||
if (options[launcher::Options::LoadMetalGearArcadeModule].value_bool()) {
|
if (options[launcher::Options::LoadMetalGearArcadeModule].value_bool()) {
|
||||||
attach_mga = true;
|
attach_mga = true;
|
||||||
@@ -649,6 +644,9 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::LoadGitaDoraModule].value_bool()) {
|
if (options[launcher::Options::LoadGitaDoraModule].value_bool()) {
|
||||||
attach_gitadora = true;
|
attach_gitadora = true;
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::GitaDoraDisableFrameLimiter].value_bool()) {
|
||||||
|
games::gitadora::DISABLE_FRAME_LIMITER = true;
|
||||||
|
}
|
||||||
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();
|
||||||
}
|
}
|
||||||
@@ -883,7 +881,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
options[launcher::Options::ScreenResizeConfigPath].value_text();
|
options[launcher::Options::ScreenResizeConfigPath].value_text();
|
||||||
}
|
}
|
||||||
if (options[launcher::Options::PatchManagerConfigPath].is_active()) {
|
if (options[launcher::Options::PatchManagerConfigPath].is_active()) {
|
||||||
overlay::windows::PATCH_MANAGER_CFG_PATH_OVERRIDE =
|
patcher::PATCH_MANAGER_CFG_PATH_OVERRIDE =
|
||||||
options[launcher::Options::PatchManagerConfigPath].value_text();
|
options[launcher::Options::PatchManagerConfigPath].value_text();
|
||||||
}
|
}
|
||||||
if (options[launcher::Options::PathToAppConfig].is_active()) {
|
if (options[launcher::Options::PathToAppConfig].is_active()) {
|
||||||
@@ -1042,9 +1040,32 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::SDVXForce720p].value_bool()) {
|
if (options[launcher::Options::SDVXForce720p].value_bool()) {
|
||||||
GRAPHICS_SDVX_FORCE_720 = true;
|
GRAPHICS_SDVX_FORCE_720 = true;
|
||||||
}
|
}
|
||||||
|
#ifdef SPICE64
|
||||||
|
// only the Live2D-capable SDVX versions are 64-bit, so this whole feature is
|
||||||
|
// gated out of 32-bit builds and only armed for SDVX (model KFC).
|
||||||
|
if (avs::game::is_model("KFC")) {
|
||||||
|
auto live2d = options[launcher::Options::SDVXDisableLive2D].value_text();
|
||||||
|
if (live2d == "always") {
|
||||||
|
GRAPHICS_SDVX_LIVE2D_MODE = SdvxLive2dMode::Always;
|
||||||
|
} else if (live2d == "ingame") {
|
||||||
|
GRAPHICS_SDVX_LIVE2D_MODE = SdvxLive2dMode::InGame;
|
||||||
|
// scene detection runs independently of the SDVX game module
|
||||||
|
games::sdvx::live2d_scene_detection_init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // SPICE64
|
||||||
if (options[launcher::Options::InvertTouchCoordinates].value_bool()) {
|
if (options[launcher::Options::InvertTouchCoordinates].value_bool()) {
|
||||||
rawinput::touch::INVERTED = true;
|
rawinput::touch::INVERTED = true;
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::RawInputTouchAspectRatio].is_active()) {
|
||||||
|
auto mode = options[launcher::Options::RawInputTouchAspectRatio].value_text();
|
||||||
|
if (mode == "on") {
|
||||||
|
rawinput::touch::ASPECT_COMPENSATION_MODE = rawinput::touch::AspectMode::On;
|
||||||
|
} else if (mode == "off") {
|
||||||
|
rawinput::touch::ASPECT_COMPENSATION_MODE = rawinput::touch::AspectMode::Off;
|
||||||
|
}
|
||||||
|
// "auto" leaves the default (per-game)
|
||||||
|
}
|
||||||
// DisableTouchCardInsert is no longer honored in spice2x
|
// DisableTouchCardInsert is no longer honored in spice2x
|
||||||
// if (options[launcher::Options::DisableTouchCardInsert].value_bool()) {
|
// if (options[launcher::Options::DisableTouchCardInsert].value_bool()) {
|
||||||
// SPICETOUCH_CARD_DISABLE = true;
|
// SPICETOUCH_CARD_DISABLE = true;
|
||||||
@@ -1086,6 +1107,9 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::DisableOverlay].value_bool()) {
|
if (options[launcher::Options::DisableOverlay].value_bool()) {
|
||||||
overlay::ENABLED = false;
|
overlay::ENABLED = false;
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::OverlayKeyboardNavigation].value_bool()) {
|
||||||
|
overlay::ENABLE_KEYBOARD_NAVIGATION = true;
|
||||||
|
}
|
||||||
if (options[launcher::Options::OverlayScaling].is_active() && !cfg::CONFIGURATOR_STANDALONE && !cfg_run) {
|
if (options[launcher::Options::OverlayScaling].is_active() && !cfg::CONFIGURATOR_STANDALONE && !cfg_run) {
|
||||||
const auto val = options[launcher::Options::OverlayScaling].value_uint32();
|
const auto val = options[launcher::Options::OverlayScaling].value_uint32();
|
||||||
if (10 <= val && val <= 400 && val != 100) {
|
if (10 <= val && val <= 400 && val != 100) {
|
||||||
@@ -1254,6 +1278,14 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
GRAPHICS_GITADORA_SMALL_MONITOR =
|
GRAPHICS_GITADORA_SMALL_MONITOR =
|
||||||
options[launcher::Options::GitaDoraWindowedSmallMonitor].value_text();
|
options[launcher::Options::GitaDoraWindowedSmallMonitor].value_text();
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::GitaDoraWindowedSmallSize].is_active()) {
|
||||||
|
GRAPHICS_GITADORA_SMALL_SIZE =
|
||||||
|
options[launcher::Options::GitaDoraWindowedSmallSize].value_text();
|
||||||
|
}
|
||||||
|
if (options[launcher::Options::GitaDoraWindowedSmallPosition].is_active()) {
|
||||||
|
GRAPHICS_GITADORA_SMALL_POS =
|
||||||
|
options[launcher::Options::GitaDoraWindowedSmallPosition].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()) {
|
||||||
@@ -1286,21 +1318,36 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::AcAccurate;
|
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::AcAccurate;
|
||||||
} else if (options[launcher::Options::JubeatTouchAlgo].value_text() == "legacy") {
|
} else if (options[launcher::Options::JubeatTouchAlgo].value_text() == "legacy") {
|
||||||
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Legacy;
|
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Legacy;
|
||||||
|
} else if (options[launcher::Options::JubeatTouchAlgo].value_text() == "plus") {
|
||||||
|
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Plus;
|
||||||
} else {
|
} else {
|
||||||
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Improved;
|
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Improved;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::JubeatTouchDebug].is_active()) {
|
||||||
|
auto mode = options[launcher::Options::JubeatTouchDebug].value_text();
|
||||||
|
if (mode == "none") {
|
||||||
|
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugNone;
|
||||||
|
} else if (mode == "box") {
|
||||||
|
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugBox;
|
||||||
|
} else if (mode == "all") {
|
||||||
|
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugAll;
|
||||||
|
} else {
|
||||||
|
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugAuto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options[launcher::Options::JubeatTouchDebounce].is_active()) {
|
||||||
|
games::jb::TOUCH_DEBOUNCE_MS =
|
||||||
|
options[launcher::Options::JubeatTouchDebounce].value_uint32();
|
||||||
|
}
|
||||||
|
|
||||||
// reflec beat touch emulation
|
// reflec beat touch emulation
|
||||||
|
games::rb::TOUCH_DEBUG_OVERLAY = options[launcher::Options::RBTouchDebug].value_bool();
|
||||||
if (options[launcher::Options::spice2x_RBTouchScale].is_active()) {
|
if (options[launcher::Options::spice2x_RBTouchScale].is_active()) {
|
||||||
games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32();
|
games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32();
|
||||||
}
|
}
|
||||||
if (options[launcher::Options::RBTouchSize].is_active()) {
|
// -rbtouchsize is deprecated and ignored; touch emulation always uses the 3x3 model
|
||||||
const auto text = options[launcher::Options::RBTouchSize].value_text();
|
|
||||||
if (text == "3") {
|
|
||||||
games::rb::TOUCH_SIZE = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (options[launcher::Options::RBTouchPollRate].is_active()) {
|
if (options[launcher::Options::RBTouchPollRate].is_active()) {
|
||||||
games::rb::TOUCH_POLL_RATE = options[launcher::Options::RBTouchPollRate].value_uint32();
|
games::rb::TOUCH_POLL_RATE = options[launcher::Options::RBTouchPollRate].value_uint32();
|
||||||
}
|
}
|
||||||
@@ -1434,6 +1481,26 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
cfg::CONFIGURATOR_FORCE_SOFTWARE_RENDER = true;
|
cfg::CONFIGURATOR_FORCE_SOFTWARE_RENDER = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OBS WebSocket overlay settings
|
||||||
|
if (options[launcher::Options::OBSWebSocketEnabled].value_bool()) {
|
||||||
|
overlay::windows::OBS_CONTROL_ENABLED = true;
|
||||||
|
}
|
||||||
|
if (options[launcher::Options::OBSWebSocketHost].is_active()) {
|
||||||
|
overlay::windows::OBS_CONTROL_HOST = options[launcher::Options::OBSWebSocketHost].value_text();
|
||||||
|
}
|
||||||
|
if (options[launcher::Options::OBSWebSocketPort].is_active()) {
|
||||||
|
const auto obs_port = options[launcher::Options::OBSWebSocketPort].value_uint32();
|
||||||
|
if (obs_port > 0 && obs_port <= 65535) {
|
||||||
|
overlay::windows::OBS_CONTROL_PORT = static_cast<uint16_t>(obs_port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options[launcher::Options::OBSWebSocketPassword].is_active()) {
|
||||||
|
overlay::windows::OBS_CONTROL_PASSWORD = options[launcher::Options::OBSWebSocketPassword].value_text();
|
||||||
|
}
|
||||||
|
if (options[launcher::Options::OBSWebSocketDebug].value_bool()) {
|
||||||
|
overlay::windows::OBS_CONTROL_DEBUG = 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);
|
||||||
@@ -1546,8 +1613,19 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
|
|
||||||
// print out conflicts
|
// print out conflicts
|
||||||
size_t conflicts = 0;
|
size_t conflicts = 0;
|
||||||
for (const auto &option : options) {
|
for (size_t i = 0; i < options.size(); i++) {
|
||||||
if (option.conflicting && option.get_definition().type != OptionType::Bool) {
|
// InjectHook / EarlyInjectHook accept multiple values, so command line and
|
||||||
|
// spicecfg entries are merged rather than conflicting; don't warn about them
|
||||||
|
if (i == (size_t) launcher::Options::InjectHook ||
|
||||||
|
i == (size_t) launcher::Options::EarlyInjectHook) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto &option = options[i];
|
||||||
|
// ignore Boolean values
|
||||||
|
if (option.get_definition().type == OptionType::Bool) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (option.conflicting) {
|
||||||
conflicts += 1;
|
conflicts += 1;
|
||||||
const auto& value = option.get_definition().sensitive ? "*****" : option.value;
|
const auto& value = option.get_definition().sensitive ? "*****" : option.value;
|
||||||
if (launcher::USE_CMD_OVERRIDE) {
|
if (launcher::USE_CMD_OVERRIDE) {
|
||||||
@@ -1627,18 +1705,13 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SDVXFullscreenLandscape disabled due to it messing with in-game camera angle
|
if (options[launcher::Options::SDVXFullscreenLandscape].value_bool() && !GRAPHICS_WINDOWED) {
|
||||||
// FullscreenOrientationFlip continues to live on, however.
|
#if SPICE64
|
||||||
if (options[launcher::Options::SDVXFullscreenLandscape].value_bool() && !cfg::CONFIGURATOR_STANDALONE) {
|
GRAPHICS_FS_ORIENTATION_SWAP = true;
|
||||||
log_fatal("launcher", "-sdvxlandscape removed due to a camera bug in SDVX!");
|
#else
|
||||||
|
log_warning("launcher", "-sdvxlandscape is not supported in 32-bit SDVX, ignoring...");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
// if (options[launcher::Options::SDVXFullscreenLandscape].value_bool() && !GRAPHICS_WINDOWED) {
|
|
||||||
// #if SPICE64
|
|
||||||
// GRAPHICS_FS_ORIENTATION_SWAP = true;
|
|
||||||
// #else
|
|
||||||
// log_warning("launcher", "-sdvxlandscape is not supported in 32-bit SDVX, ignoring...");
|
|
||||||
// #endif
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (options[launcher::Options::FullscreenOrientationFlip].value_bool() && !GRAPHICS_WINDOWED) {
|
if (options[launcher::Options::FullscreenOrientationFlip].value_bool() && !GRAPHICS_WINDOWED) {
|
||||||
GRAPHICS_FS_ORIENTATION_SWAP = true;
|
GRAPHICS_FS_ORIENTATION_SWAP = true;
|
||||||
@@ -1708,10 +1781,19 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
|
|
||||||
// enable subscreen touch emulation
|
// enable subscreen touch emulation
|
||||||
if (options[launcher::Options::spice2x_IIDXEmulateSubscreenKeypadTouch].is_active()) {
|
if (options[launcher::Options::spice2x_IIDXEmulateSubscreenKeypadTouch].is_active()) {
|
||||||
games::iidx::poke::enable();
|
games::iidx::ENABLE_POKE = true;
|
||||||
}
|
}
|
||||||
if (options[launcher::Options::NostalgiaPoke].is_active()) {
|
if (options[launcher::Options::NostalgiaPoke].is_active()) {
|
||||||
games::nost::ENABLE_POKE = TRUE;
|
games::nost::ENABLE_POKE = true;
|
||||||
|
}
|
||||||
|
if (options[launcher::Options::NostalgiaTouchMode].is_active()) {
|
||||||
|
if (overlay::ENABLED) {
|
||||||
|
games::nost::ENABLE_TOUCH_MODE = true;
|
||||||
|
} else {
|
||||||
|
log_warning(
|
||||||
|
"launcher",
|
||||||
|
"Nostalgia Touch Mode requires the global overlay; ignoring -nosttouch");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1771,6 +1853,13 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (check_dll("jubeat2019.dll")) {
|
||||||
|
avs::game::DLL_NAME = "jubeat2019.dll";
|
||||||
|
attach_io = true;
|
||||||
|
attach_jb = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// RB
|
// RB
|
||||||
if (check_dll("reflecbeat.dll")) {
|
if (check_dll("reflecbeat.dll")) {
|
||||||
avs::game::DLL_NAME = "reflecbeat.dll";
|
avs::game::DLL_NAME = "reflecbeat.dll";
|
||||||
@@ -2199,7 +2288,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
games.push_back(new games::sdvx::SDVXGame());
|
games.push_back(new games::sdvx::SDVXGame());
|
||||||
}
|
}
|
||||||
if (attach_jb) {
|
if (attach_jb) {
|
||||||
avs::core::set_default_heap_size("jubeat.dll");
|
avs::core::set_default_heap_size(avs::game::DLL_NAME);
|
||||||
games.push_back(new games::jb::JBGame());
|
games.push_back(new games::jb::JBGame());
|
||||||
}
|
}
|
||||||
if (attach_nostalgia) {
|
if (attach_nostalgia) {
|
||||||
@@ -2439,10 +2528,8 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
// copy defaults to nvram
|
// copy defaults to nvram
|
||||||
avs::core::copy_defaults();
|
avs::core::copy_defaults();
|
||||||
|
|
||||||
// prepare patches
|
// prepare patches (registers the DLL-load notification before the game DLL loads)
|
||||||
{
|
patcher::init();
|
||||||
overlay::windows::PatchManager patch_manager(nullptr, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// load game
|
// load game
|
||||||
avs::game::load_dll();
|
avs::game::load_dll();
|
||||||
@@ -2570,9 +2657,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// apply patches
|
// apply patches
|
||||||
{
|
patcher::apply_patches_on_start();
|
||||||
overlay::windows::PatchManager patch_manager(nullptr, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// load AVS-EA3
|
// load AVS-EA3
|
||||||
avs::ea3::boot(easrv_port, easrv_maint, easrv_smart);
|
avs::ea3::boot(easrv_port, easrv_maint, easrv_smart);
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ static const std::vector<std::string> CATEGORY_ORDER_NETWORK = {
|
|||||||
static const std::vector<std::string> CATEGORY_ORDER_OVERLAY = {
|
static const std::vector<std::string> CATEGORY_ORDER_OVERLAY = {
|
||||||
"General Overlay",
|
"General Overlay",
|
||||||
"Game Overlay",
|
"Game Overlay",
|
||||||
|
"OBS Control",
|
||||||
};
|
};
|
||||||
|
|
||||||
static const std::vector<std::string> CATEGORY_ORDER_ADVANCED = {
|
static const std::vector<std::string> CATEGORY_ORDER_ADVANCED = {
|
||||||
@@ -329,9 +330,8 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.desc =
|
.desc =
|
||||||
"Allows you to play portrait games in landscape (and vice versa) by transposing resolution and applying image scaling.\n\n"
|
"Allows you to play portrait games in landscape (and vice versa) by transposing resolution and applying image scaling.\n\n"
|
||||||
"Works great for some games, but can COMPLETELY BREAK other games - YMMV!\n\n"
|
"Works great for some games, but can COMPLETELY BREAK other games - YMMV!\n\n"
|
||||||
"Strongly consider combining this with -forceres option to render at monitor native resolution\n\n"
|
"Strongly consider combining this with -forceres option to render at monitor native resolution.\n\n"
|
||||||
"WARNING: for SDVX, this messes with camera angle for note lanes, causing it to be zoomed out, and causes performance issues "
|
"For SDVX, use the dedicated -sdvxlandscape option.",
|
||||||
"with Live2D!",
|
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.hidden = true,
|
.hidden = true,
|
||||||
.category = "Full Screen Settings"
|
.category = "Full Screen Settings"
|
||||||
@@ -433,18 +433,23 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.category = "Advanced Network",
|
.category = "Advanced Network",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "Preferred NetAdapter IP",
|
.title = "Preferred Network Adapter's IP",
|
||||||
.name = "network",
|
.name = "network",
|
||||||
.desc = "This is NOT the EA service URL; use -url for that. "
|
.display_name = "netadapterip",
|
||||||
"Force the use of an adapter with the specified network. Must also provide -subnet.",
|
.aliases = "netadapterip",
|
||||||
|
.desc = "Instead of using the default network adapter, force the usage of another network adapter "
|
||||||
|
"with the specified IP address. You must also set -netadaptersubnet.",
|
||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.category = "Advanced Network",
|
.category = "Advanced Network",
|
||||||
.sensitive = true,
|
.sensitive = true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "Preferred NetAdapter Subnet",
|
.title = "Preferred Network Adapter's Subnet",
|
||||||
.name = "subnet",
|
.name = "subnet",
|
||||||
.desc = "Force the use of an adapter with the specified subnet. Must also provide -network.",
|
.display_name = "netadaptersubnet",
|
||||||
|
.aliases = "netadaptersubnet",
|
||||||
|
.desc = "Instead of using the default network adapter, force the usage of another network adapter "
|
||||||
|
"with the specified subnet. You must also set -netadapterip.",
|
||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.category = "Advanced Network",
|
.category = "Advanced Network",
|
||||||
},
|
},
|
||||||
@@ -504,6 +509,13 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "General Overlay",
|
.category = "General Overlay",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.title = "Overlay Keyboard Navigation",
|
||||||
|
.name = "keyboardnav",
|
||||||
|
.desc = "Enables keyboard navigation in the in-game overlay.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.category = "General Overlay",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// OverlayScaling
|
// OverlayScaling
|
||||||
.title = "Spice Overlay UI Scale %",
|
.title = "Spice Overlay UI Scale %",
|
||||||
@@ -949,13 +961,12 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.category = "Cab Peripherals",
|
.category = "Cab Peripherals",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "SDVX FS Subscreen Native Touch Handling",
|
.title = "SDVX Native Touch (DEPRECATED - no longer needed)",
|
||||||
.name = "sdvxnativetouch",
|
.name = "sdvxnativetouch",
|
||||||
.desc = "Disables touch hooks and lets the game access a touch screen directly. "
|
.desc = "This option does nothing.\n\n"
|
||||||
"Requires a touch screen to be connected as a secondary monitor. "
|
"Native touch handling is now enabled by default and this option is no longer needed.",
|
||||||
"Touch input must be routed to the primary screen via Windows Tablet PC settings. "
|
|
||||||
"Enable this when you get duplicate touch inputs from an actual touch screen.",
|
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
|
.hidden = true,
|
||||||
.game_name = "Sound Voltex",
|
.game_name = "Sound Voltex",
|
||||||
.category = "Advanced Game Options",
|
.category = "Advanced Game Options",
|
||||||
},
|
},
|
||||||
@@ -1025,6 +1036,24 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.category = "Game Options",
|
.category = "Game Options",
|
||||||
.quick_setting_category = "Game",
|
.quick_setting_category = "Game",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// SDVXDisableLive2D
|
||||||
|
.title = "SDVX Disable Live2D (EXPERIMENTAL)",
|
||||||
|
.name = "sdvxnolive2d",
|
||||||
|
.desc = "Skip rendering the SDVX Live2D graphics to save GPU.\n\n"
|
||||||
|
"off: leave Live2D as-is.\n"
|
||||||
|
"always: hide Live2D (also hides menu navigator).\n"
|
||||||
|
"ingame: only hide Live2D during a song; keeps the menu navigator. Scene detection relies on game logging.",
|
||||||
|
.type = OptionType::Enum,
|
||||||
|
.game_name = "Sound Voltex",
|
||||||
|
.category = "Advanced Game Options",
|
||||||
|
.elements = {
|
||||||
|
{"off", "Show Live2D"},
|
||||||
|
{"always", "Hide Live2D"},
|
||||||
|
{"ingame", "Hide during songs"},
|
||||||
|
},
|
||||||
|
.quick_setting_category = "Game",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// spice2x_SDVXSubPos
|
// spice2x_SDVXSubPos
|
||||||
.title = "SDVX Subscreen Overlay Position",
|
.title = "SDVX Subscreen Overlay Position",
|
||||||
@@ -1128,15 +1157,25 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// PopnNativeTouch
|
// PopnNativeTouch
|
||||||
.title = "Pop'n Music PikaPika Native Touch Handling",
|
.title = "Pop'n Music Native Touch (DEPRECATED - no longer needed)",
|
||||||
.name = "popnnativetouch",
|
.name = "popnnativetouch",
|
||||||
.desc = "Disables touch hooks and lets the game access a touch screen directly. "
|
.desc = "This option does nothing.\n\n"
|
||||||
"Requires a touch screen to be connected as a secondary monitor. "
|
"Native touch handling is now enabled by default and this option is no longer needed.",
|
||||||
"Touch input must be routed to the primary screen via Windows Tablet PC settings. "
|
.type = OptionType::Bool,
|
||||||
"Enable this when you get duplicate touch inputs from an actual touch screen.",
|
.hidden = true,
|
||||||
|
.game_name = "Pop'n Music",
|
||||||
|
.category = "Advanced Game Options",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// PopnSubRedraw
|
||||||
|
.title = "Pop'n Music PikaPika Subscreen Force Redraw",
|
||||||
|
.name = "popnsubredraw",
|
||||||
|
.desc = "Check if submonitor in fullscreen mode appears stuck; "
|
||||||
|
"this option forces subscreen to redraw every frame.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.game_name = "Pop'n Music",
|
.game_name = "Pop'n Music",
|
||||||
.category = "Advanced Game Options",
|
.category = "Advanced Game Options",
|
||||||
|
.quick_setting_category = "Game",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "Force Load HELLO! Pop'n Music Module",
|
.title = "Force Load HELLO! Pop'n Music Module",
|
||||||
@@ -1163,6 +1202,19 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.category = "Game Options",
|
.category = "Game Options",
|
||||||
.quick_setting_category = "Game",
|
.quick_setting_category = "Game",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.title = "GitaDora Disable Frame Limiter (EXPERIMENTAL)",
|
||||||
|
.name = "gdnoframelimiter",
|
||||||
|
.desc = "Only for GITADORA to GALAXY WAVE; not for XG series or Arena Model.\n\n"
|
||||||
|
"Game engine's built-in frame limiter can result in unstable frame pacing "
|
||||||
|
"(around 56-58 FPS) on modern Windows. Turning this on disables that and "
|
||||||
|
"forces the game to rely on V-Sync instead, resulting in a stable FPS.\n\n"
|
||||||
|
"Ensure your monitors are set to 60Hz.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.game_name = "GitaDora",
|
||||||
|
.category = "Game Options",
|
||||||
|
.quick_setting_category = "Game",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.title = "GitaDora Cabinet Type",
|
.title = "GitaDora Cabinet Type",
|
||||||
.name = "gdcabtype",
|
.name = "gdcabtype",
|
||||||
@@ -1259,7 +1311,9 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.name = "gdalayout",
|
.name = "gdalayout",
|
||||||
.desc = "For Arena Model: select how many windows to create.\n\n"
|
.desc = "For Arena Model: select how many windows to create.\n\n"
|
||||||
"1 window: main only; use the subscreen overlay for SMALL.\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"
|
"2 windows: main + SMALL. Without -w, this uses native D3D9 adapter-group fullscreen "
|
||||||
|
"with physical MAIN/SMALL and virtual LEFT/RIGHT. It requires two active displays on "
|
||||||
|
"one adapter group. With -w, it uses borderless windows instead.\n\n"
|
||||||
"4 windows: main + LEFT + RIGHT + SMALL. Fullscreen requires exactly 4 monitors in the "
|
"4 windows: main + LEFT + RIGHT + SMALL. Fullscreen requires exactly 4 monitors in the "
|
||||||
"right resolution; see wiki for details.",
|
"right resolution; see wiki for details.",
|
||||||
.type = OptionType::Enum,
|
.type = OptionType::Enum,
|
||||||
@@ -1742,19 +1796,19 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.category = "I/O Modules",
|
.category = "I/O Modules",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "Disable Raw Input Touch",
|
.title = "Touch Compatibility Mode (Disable Raw Input Touch)",
|
||||||
.name = "wintouch",
|
.name = "wintouch",
|
||||||
.desc = "For touch screen input, disable usage of Raw Input API and instead use "
|
.desc = "For touch screen input, disable usage of Raw Input API and instead use "
|
||||||
"Win8 Pointer API or Win7 Touch API. Only enable this if you have trouble "
|
"Win8 Pointer API or Win7 Touch API. Results in better compatibility with some "
|
||||||
"using the default (raw input) touch input, as Raw Input performs better.",
|
"touch screens, but may result in worse performance and higher latency.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "Touch Parameters",
|
.category = "Touch Parameters",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "Force Touch Emulation",
|
.title = "Force Legacy Touch Emulation",
|
||||||
.name = "touchemuforce",
|
.name = "touchemuforce",
|
||||||
.desc = "Force enable hook for GetTouchInputInfo API and inject WM_TOUCH events. "
|
.desc = "Force enable hook for GetTouchInputInfo API and inject WM_TOUCH events. "
|
||||||
"This is automatically enabled when needed and is typically not required. Do not enable this unless you have trouble.",
|
"Do not enable this unless you have trouble with using a mouse to emulate touch input.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "Touch Parameters",
|
.category = "Touch Parameters",
|
||||||
},
|
},
|
||||||
@@ -1766,6 +1820,24 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "Touch Parameters",
|
.category = "Touch Parameters",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// RawInputTouchAspectRatio
|
||||||
|
.title = "Raw Input Touch Fix Aspect Ratio",
|
||||||
|
.name = "rawtouchaspect",
|
||||||
|
.desc = "Compensate for letterboxing/pillarboxing when the game runs at a display mode with a "
|
||||||
|
"different aspect ratio than the touch panel's native resolution. Only affects the default "
|
||||||
|
"raw input touch handler. This is automatically enabled, only force on or off if you have trouble.\n\n"
|
||||||
|
"auto (default): automatically enable on a per-game basis.\n"
|
||||||
|
"on: forced on\n"
|
||||||
|
"off: forced off",
|
||||||
|
.type = OptionType::Enum,
|
||||||
|
.category = "Touch Parameters",
|
||||||
|
.elements = {
|
||||||
|
{"auto", ""},
|
||||||
|
{"on", ""},
|
||||||
|
{"off", ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// DisableTouchCardInsert
|
// DisableTouchCardInsert
|
||||||
.title = "Disable Touch Card Insert (DEPRECATED - use -touchcard instead)",
|
.title = "Disable Touch Card Insert (DEPRECATED - use -touchcard instead)",
|
||||||
@@ -1777,12 +1849,14 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// spice2x_TouchCardInsert
|
// spice2x_TouchCardInsert
|
||||||
.title = "Show Insert Card button",
|
.title = "Show Insert Card button (DEPRECATED)",
|
||||||
.name = "sp2x-touchcard",
|
.name = "sp2x-touchcard",
|
||||||
.display_name = "touchcard",
|
.display_name = "touchcard",
|
||||||
.aliases= "touchcard",
|
.aliases= "touchcard",
|
||||||
.desc = "Show Insert Card touch button on main display.",
|
.desc = "Show Insert Card touch button on main display. "
|
||||||
|
"DEPRECATED - only works in very specific situations (jubeat + wintouch)",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
|
.hidden = true,
|
||||||
.category = "Touch Parameters",
|
.category = "Touch Parameters",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1854,7 +1928,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.title = "HID SmartCard Fix UID",
|
.title = "HID SmartCard Fix UID",
|
||||||
.name = "scardfix",
|
.name = "scardfix",
|
||||||
.desc = "Modify behavior of SmartCard UID logic.\n\n"
|
.desc = "Modify behavior of SmartCard UID logic.\n\n"
|
||||||
"legacy: Preserve buggy old behavior (Default)\n\n"
|
"legacy (default): Preserve buggy old behavior\n\n"
|
||||||
"fix: Add E00401 to non-FeliCa cards, scan FeliCa cards as-is (Recommended for most users)\n\n"
|
"fix: Add E00401 to non-FeliCa cards, scan FeliCa cards as-is (Recommended for most users)\n\n"
|
||||||
"all: Add E00401 to all cards, including FeliCa cards (For really old games only)\n\n"
|
"all: Add E00401 to all cards, including FeliCa cards (For really old games only)\n\n"
|
||||||
"The algorithm is simple; the prefix is applied, the scanned card number is appended up to 8 bytes, and remaining digits are discarded. "
|
"The algorithm is simple; the prefix is applied, the scanned card number is appended up to 8 bytes, and remaining digits are discarded. "
|
||||||
@@ -2105,7 +2179,6 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
{"25", "+25 dB"},
|
{"25", "+25 dB"},
|
||||||
{"30", "+30 dB"},
|
{"30", "+30 dB"},
|
||||||
},
|
},
|
||||||
.quick_setting_category = "Audio",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// AudioResample
|
// AudioResample
|
||||||
@@ -2582,6 +2655,26 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.category = "Game Windowed Settings",
|
.category = "Game Windowed Settings",
|
||||||
.picker = OptionPickerType::Monitor,
|
.picker = OptionPickerType::Monitor,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// GitaDoraWindowedSmallSize
|
||||||
|
.title = "GitaDora Windowed SMALL Size",
|
||||||
|
.name = "gdwsmallsize",
|
||||||
|
.desc = "Size of the GITADORA Arena SMALL touch window. Defaults to (800,1280).",
|
||||||
|
.type = OptionType::Text,
|
||||||
|
.setting_name = "800,1280",
|
||||||
|
.game_name = "GitaDora",
|
||||||
|
.category = "Game Windowed Settings",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// GitaDoraWindowedSmallPosition
|
||||||
|
.title = "GitaDora Windowed SMALL Position",
|
||||||
|
.name = "gdwsmallpos",
|
||||||
|
.desc = "Initial position of the GITADORA Arena SMALL touch window. Defaults to (0,0).",
|
||||||
|
.type = OptionType::Text,
|
||||||
|
.setting_name = "0,0",
|
||||||
|
.game_name = "GitaDora",
|
||||||
|
.category = "Game Windowed Settings",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// spice2x_IIDXWindowedSubscreenSize
|
// spice2x_IIDXWindowedSubscreenSize
|
||||||
.title = "IIDX Windowed Subscreen Size",
|
.title = "IIDX Windowed Subscreen Size",
|
||||||
@@ -2638,6 +2731,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.desc = "For touch screen players: choose the touch algorithm to use.\n\n"
|
.desc = "For touch screen players: choose the touch algorithm to use.\n\n"
|
||||||
"legacy - evenly divide the grid into 16 squares; old spicetools behavior, slightly inaccurate in gaps\n\n"
|
"legacy - evenly divide the grid into 16 squares; old spicetools behavior, slightly inaccurate in gaps\n\n"
|
||||||
"improved (default) - squares register as-is, gaps will trigger the closest square\n\n"
|
"improved (default) - squares register as-is, gaps will trigger the closest square\n\n"
|
||||||
|
"plus - like improved, but gaps can trigger multiple buttons (like the mobile game)\n\n"
|
||||||
"accurate - only touches within squares will trigger; gaps do nothing (for AC size touch screens)",
|
"accurate - only touches within squares will trigger; gaps do nothing (for AC size touch screens)",
|
||||||
.type = OptionType::Enum,
|
.type = OptionType::Enum,
|
||||||
.game_name = "Jubeat",
|
.game_name = "Jubeat",
|
||||||
@@ -2645,10 +2739,58 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.elements = {
|
.elements = {
|
||||||
{"legacy", ""},
|
{"legacy", ""},
|
||||||
{"improved", ""},
|
{"improved", ""},
|
||||||
|
{"plus", ""},
|
||||||
{"accurate", ""},
|
{"accurate", ""},
|
||||||
},
|
},
|
||||||
.quick_setting_category = "Game",
|
.quick_setting_category = "Game",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// JubeatTouchDebug
|
||||||
|
.title = "JB Touch Debug Overlay",
|
||||||
|
.name = "jubeattouchdebug",
|
||||||
|
.desc = "For touch screen players: draw a debug overlay on the main display. "
|
||||||
|
"Requires the Spice Overlay to be enabled.\n\n"
|
||||||
|
"auto (default) - show boundary boxes when a touch screen is detected; otherwise, none\n\n"
|
||||||
|
"none - draw nothing\n\n"
|
||||||
|
"box - show the 4x4 touch boundary boxes\n\n"
|
||||||
|
"all - show both the boxes and the touch circles",
|
||||||
|
.type = OptionType::Enum,
|
||||||
|
.game_name = "Jubeat",
|
||||||
|
.category = "Game Options",
|
||||||
|
.elements = {
|
||||||
|
{"auto", ""},
|
||||||
|
{"none", ""},
|
||||||
|
{"box", ""},
|
||||||
|
{"all", ""},
|
||||||
|
},
|
||||||
|
.quick_setting_category = "Game",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// JubeatTouchDebounce
|
||||||
|
.title = "JB Touch Debounce",
|
||||||
|
.name = "jubeattouchdebounce",
|
||||||
|
.desc = "For touch screen players: ignore extremely quick touches by requiring a "
|
||||||
|
"touch to be held for at least this many milliseconds before it registers.\n\n"
|
||||||
|
"Useful for filtering phantom touches on a noisy touch screen. A higher value "
|
||||||
|
"adds input latency, so keep it small. Default: off (0).",
|
||||||
|
.type = OptionType::Integer,
|
||||||
|
.setting_name = "8",
|
||||||
|
.game_name = "Jubeat",
|
||||||
|
.category = "Game Options",
|
||||||
|
.quick_setting_category = "Game",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// RBTouchDebug
|
||||||
|
.title = "RB Touch Debug Overlay",
|
||||||
|
.name = "rbtouchdebug",
|
||||||
|
.desc = "Draw lines to show IR sensor emulation state.\n\n"
|
||||||
|
"Note: lines will not perfectly align with touches; this is by design, it's showing the "
|
||||||
|
"IR sensor state, before the game applies its own touch detection algorithm.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.game_name = "Reflec Beat",
|
||||||
|
.category = "Game Options",
|
||||||
|
.quick_setting_category = "Game",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// spice2x_RBTouchScale
|
// spice2x_RBTouchScale
|
||||||
.title = "RB Touch Emulation Scale",
|
.title = "RB Touch Emulation Scale",
|
||||||
@@ -2663,14 +2805,15 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// RBTouchSize
|
// RBTouchSize
|
||||||
.title = "RB Touch Emulation Size",
|
.title = "RB Touch Emulation Size (DEPRECATED - no longer has any effect)",
|
||||||
.name = "rbtouchsize",
|
.name = "rbtouchsize",
|
||||||
.desc = "Size of the touch area; how many IR sensors a single finger activates. Default: 1 (1x1).",
|
.desc = "This option is deprecated and no longer has any effect. "
|
||||||
|
"Reflec Beat touch emulation always uses the 3x3 sensor model.",
|
||||||
.type = OptionType::Enum,
|
.type = OptionType::Enum,
|
||||||
|
.hidden = true,
|
||||||
.game_name = "Reflec Beat",
|
.game_name = "Reflec Beat",
|
||||||
.category = "Game Options",
|
.category = "Game Options",
|
||||||
.elements = {
|
.elements = {
|
||||||
{"1", "1x1"},
|
|
||||||
{"3", "3x3"},
|
{"3", "3x3"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -2678,9 +2821,10 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
// RBTouchPollRate
|
// RBTouchPollRate
|
||||||
.title = "RB Touch Emulation Poll Hz",
|
.title = "RB Touch Emulation Poll Hz",
|
||||||
.name = "rbtouchhz",
|
.name = "rbtouchhz",
|
||||||
.desc = "By default, the game polls for touch at 120Hz. "
|
.desc = "By default, the game polls for touch at ~125Hz. "
|
||||||
"This option overrides that rate; enter a number betwen 1 and 1000.\n\n"
|
"This option overrides that rate; enter a number between 1 and 1000.\n\n"
|
||||||
"It should be noted that higher poll does not necessarily improve accuracy or performance.",
|
"Higher rates reduce input latency (the game sees a fresher touch position) "
|
||||||
|
"but do NOT improve spatial accuracy, and very high rates just waste CPU.",
|
||||||
.type = OptionType::Integer,
|
.type = OptionType::Integer,
|
||||||
.setting_name = "250",
|
.setting_name = "250",
|
||||||
.game_name = "Reflec Beat",
|
.game_name = "Reflec Beat",
|
||||||
@@ -2757,15 +2901,14 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// spice2x_IIDXNativeTouch
|
// spice2x_IIDXNativeTouch
|
||||||
.title = "IIDX TDJ Subscreen Native Touch Handling",
|
.title = "IIDX Native Touch (DEPRECATED - no longer needed)",
|
||||||
.name = "sp2x-iidxnativetouch",
|
.name = "sp2x-iidxnativetouch",
|
||||||
.display_name = "iidxnativetouch",
|
.display_name = "iidxnativetouch",
|
||||||
.aliases= "iidxnativetouch",
|
.aliases= "iidxnativetouch",
|
||||||
.desc = "Disables touch hooks and lets the game access a touch screen directly. "
|
.desc = "This option does nothing.\n\n"
|
||||||
"Requires a touch screen to be connected as a secondary monitor. "
|
"Native touch handling is now enabled by default and this option is no longer needed.",
|
||||||
"Touch input must be routed to the primary screen via Windows Tablet PC settings. "
|
|
||||||
"Enable this when you get duplicate touch inputs from an actual touch screen.",
|
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
|
.hidden = true,
|
||||||
.game_name = "Beatmania IIDX",
|
.game_name = "Beatmania IIDX",
|
||||||
.category = "Advanced Game Options",
|
.category = "Advanced Game Options",
|
||||||
},
|
},
|
||||||
@@ -2882,17 +3025,17 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// SDVXFullscreenLandscape
|
// SDVXFullscreenLandscape
|
||||||
.title = "(DISABLED) SDVX Landscape Mode (SDVX5+)",
|
.title = "SDVX Full Screen Landscape Mode (SDVX5+, EXPERIMENTAL)",
|
||||||
.name = "sdvxlandscape",
|
.name = "sdvxlandscape",
|
||||||
.desc =
|
.desc =
|
||||||
"Option HIDDEN and DISABLED due to it affecting in-game camera angle for displaying lanes.\n\n"
|
|
||||||
"Allows you to play in landscape by transposing resolution and applying image scaling.\n\n"
|
"Allows you to play in landscape by transposing resolution and applying image scaling.\n\n"
|
||||||
"Works only for SDVX5 and above! This is identical to -forceresswap.\n\n"
|
"Corrects the in-game perspective camera for the transposed display aspect ratio.\n\n"
|
||||||
|
"Works only for SDVX5 and above.\n\n"
|
||||||
"Will launch at 1080p by default; strongly consider combining this with -forceres option to render at monitor native resolution.",
|
"Will launch at 1080p by default; strongly consider combining this with -forceres option to render at monitor native resolution.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.hidden = true,
|
|
||||||
.game_name = "Sound Voltex",
|
.game_name = "Sound Voltex",
|
||||||
.category = "Game Options",
|
.category = "Game Options",
|
||||||
|
.quick_setting_category = "Game",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// spice2x_EnableSMXStage
|
// spice2x_EnableSMXStage
|
||||||
@@ -3019,6 +3162,21 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.game_name = "Nostalgia",
|
.game_name = "Nostalgia",
|
||||||
.category = "Game Options",
|
.category = "Game Options",
|
||||||
|
.quick_setting_category = "Game",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// NostalgiaTouchMode
|
||||||
|
.title = "Nostalgia Touch Piano",
|
||||||
|
.name = "nosttouch",
|
||||||
|
.desc =
|
||||||
|
"Allows you to play the piano by touching the screen instead of a controller. "
|
||||||
|
"Use the mode switch button to toggle between interacting with the menu and playing the piano.\n\n"
|
||||||
|
"Velocity sensitivity is not supported. Touch targets will be pixel-perfect aligned with the "
|
||||||
|
"judgement line, key beams, and notes.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.game_name = "Nostalgia",
|
||||||
|
.category = "Game Options",
|
||||||
|
.quick_setting_category = "Game",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// ForceBackBufferCount
|
// ForceBackBufferCount
|
||||||
@@ -3190,6 +3348,51 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "Development",
|
.category = "Development",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// OBSWebSocketEnabled
|
||||||
|
.title = "OBS WebSocket Enable",
|
||||||
|
.name = "obsenable",
|
||||||
|
.desc = "Enables the in-game OBS Control overlay and its connection to the OBS Studio "
|
||||||
|
"obs-websocket (v5) server.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.category = "OBS Control",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// OBSWebSocketHost
|
||||||
|
.title = "OBS WebSocket Host",
|
||||||
|
.name = "obshost",
|
||||||
|
.desc = "Host name or IP address of the OBS Studio obs-websocket (v5) server used by "
|
||||||
|
"the in-game OBS Control overlay. Defaults to 127.0.0.1 when left empty.",
|
||||||
|
.type = OptionType::Text,
|
||||||
|
.category = "OBS Control",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// OBSWebSocketPort
|
||||||
|
.title = "OBS WebSocket Port",
|
||||||
|
.name = "obsport",
|
||||||
|
.desc = "Port of the OBS Studio obs-websocket (v5) server. Defaults to 4455 when left empty.",
|
||||||
|
.type = OptionType::Integer,
|
||||||
|
.category = "OBS Control",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// OBSWebSocketPassword
|
||||||
|
.title = "OBS WebSocket Password",
|
||||||
|
.name = "obspass",
|
||||||
|
.desc = "Password for the OBS Studio obs-websocket (v5) server. Leave empty if "
|
||||||
|
"authentication is disabled in OBS.",
|
||||||
|
.type = OptionType::Text,
|
||||||
|
.category = "OBS Control",
|
||||||
|
.sensitive = true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// OBSWebSocketDebug
|
||||||
|
.title = "OBS WebSocket Debug",
|
||||||
|
.name = "obsdebug",
|
||||||
|
.desc = "Writes the OBS WebSocket client's internal connection diagnostics to the log. "
|
||||||
|
"Only enable this when troubleshooting connection problems.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.category = "OBS Control",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) {
|
const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user