mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-10-05 01:38:12 -07:00
Compare commits
56 Commits
26-07-15
...
7c50fcc79e
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c50fcc79e | |||
| 0f4ab63101 | |||
| 8b2f38307b | |||
| 3863d5a4ed | |||
| 94574c485a | |||
| f857926ec3 | |||
| adf4cccd4a | |||
| bf8e194685 | |||
| a2e508208c | |||
| f5888609a8 | |||
| c6cd72c528 | |||
| 82e0c053d0 | |||
| 3b29227dbc | |||
| ce2f93d234 | |||
| 13a171f199 | |||
| 3da352488e | |||
| 2386047c2f | |||
| baa550037a | |||
| b53447bed5 | |||
| 4959a58de3 | |||
| 9141ff453b | |||
| 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 |
@@ -0,0 +1,46 @@
|
||||
# Normalize every text file to LF in the repository.
|
||||
# The build runs under Linux/MinGW containers, so LF is also used in the
|
||||
# working tree; Windows editors and toolchains handle LF fine.
|
||||
* text=auto eol=lf
|
||||
|
||||
# Windows-only files that must keep CRLF in the working tree.
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.sln text eol=crlf
|
||||
*.vcproj text eol=crlf
|
||||
*.vcxproj text eol=crlf
|
||||
*.props text eol=crlf
|
||||
*.filters text eol=crlf
|
||||
|
||||
# Files that must keep LF even if a Windows editor rewrites them.
|
||||
*.sh text eol=lf
|
||||
*.in text eol=lf
|
||||
*.cmake text eol=lf
|
||||
*.mk text eol=lf
|
||||
Makefile text eol=lf
|
||||
Dockerfile text eol=lf
|
||||
|
||||
# Binary files - never touch the contents.
|
||||
*.bin binary
|
||||
*.ico binary
|
||||
*.ttf binary
|
||||
*.otf binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.bmp binary
|
||||
*.zip binary
|
||||
*.7z binary
|
||||
*.gz binary
|
||||
*.dll binary
|
||||
*.exe binary
|
||||
*.lib binary
|
||||
*.a binary
|
||||
*.o binary
|
||||
*.obj binary
|
||||
*.pdb binary
|
||||
|
||||
# Vendored code is stored and checked out byte-for-byte as upstream ships it,
|
||||
# so re-importing a library never produces line-ending-only diffs.
|
||||
src/spice2x/external/** -text
|
||||
+31
-31
@@ -1,32 +1,32 @@
|
||||
on: [push, pull_request]
|
||||
name: Continuous Integration
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
fw-ci:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./src/spice2x
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set ccache environment variables
|
||||
run: |
|
||||
echo "CCACHE_DIR=${{ github.workspace }}/src/spice2x/.ccache" >> $GITHUB_ENV
|
||||
- name: Install ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
- name: Calculate commit SHA
|
||||
id: vars
|
||||
run: |
|
||||
calculatedSha=$(git rev-parse --short ${{ github.sha }})
|
||||
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
|
||||
- name: Compile
|
||||
run: ./build_docker.sh
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
|
||||
path: src/spice2x/bin
|
||||
on: [push, pull_request]
|
||||
name: Continuous Integration
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
fw-ci:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./src/spice2x
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set ccache environment variables
|
||||
run: |
|
||||
echo "CCACHE_DIR=${{ github.workspace }}/src/spice2x/.ccache" >> $GITHUB_ENV
|
||||
- name: Install ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
- name: Calculate commit SHA
|
||||
id: vars
|
||||
run: |
|
||||
calculatedSha=$(git rev-parse --short ${{ github.sha }})
|
||||
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
|
||||
- name: Compile
|
||||
run: ./build_docker.sh
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
|
||||
path: src/spice2x/bin
|
||||
if-no-files-found: error
|
||||
@@ -1,49 +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
|
||||
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
|
||||
|
||||
@@ -256,6 +256,35 @@ add_subdirectory(external/imgui EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(external/minhook EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(external/cpu_features EXCLUDE_FROM_ALL)
|
||||
|
||||
# libjpeg-turbo, prebuilt into the deps image. The WinXP toolchains have their own
|
||||
# sysroot and cannot see it, so those targets build without JPEG support.
|
||||
add_library(spice_jpeg INTERFACE)
|
||||
if(NOT SPICE_XP)
|
||||
# search static archives only: the mingw package also ships an import library,
|
||||
# and linking that one would pull in a libjpeg DLL at runtime
|
||||
set(SPICE_JPEG_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES})
|
||||
set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_STATIC_LIBRARY_SUFFIX})
|
||||
find_package(JPEG)
|
||||
set(CMAKE_FIND_LIBRARY_SUFFIXES ${SPICE_JPEG_SUFFIXES})
|
||||
|
||||
if(JPEG_FOUND)
|
||||
target_link_libraries(spice_jpeg INTERFACE JPEG::JPEG)
|
||||
target_compile_definitions(spice_jpeg INTERFACE SPICE_JPEG=1)
|
||||
else()
|
||||
message(WARNING
|
||||
"libjpeg-turbo not found: screen capture over the API is disabled")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# fpng's SIMD needs the whole unit built for SSE4.1, which its runtime CPU check
|
||||
# cannot undo, so keep it scalar rather than raising the CPU baseline
|
||||
set_source_files_properties(external/fpng/fpng.cpp PROPERTIES
|
||||
COMPILE_DEFINITIONS "FPNG_NO_SSE=1")
|
||||
if(NOT MSVC)
|
||||
set_source_files_properties(external/fpng/fpng.cpp PROPERTIES
|
||||
COMPILE_OPTIONS "-fno-strict-aliasing")
|
||||
endif()
|
||||
|
||||
# set link time optimizations (disabled for Debug builds for speed, disabled
|
||||
# for RelWithDebInfo builds due to "lto1: error: two or more sections for"
|
||||
# errors)
|
||||
@@ -332,6 +361,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
api/modules/control.cpp
|
||||
api/modules/touch.cpp
|
||||
api/modules/iidx.cpp
|
||||
api/modules/sdvx.cpp
|
||||
api/serial.cpp
|
||||
api/modules/drs.cpp
|
||||
api/modules/lcd.cpp
|
||||
@@ -379,7 +409,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
external/tinyxml2/tinyxml2.cpp
|
||||
external/http-parser/http_parser.c
|
||||
external/usbhidusage/usb-hid-usage.c
|
||||
external/toojpeg/toojpeg.cpp
|
||||
external/fpng/fpng.cpp
|
||||
external/scard/scard.cpp
|
||||
|
||||
# games
|
||||
@@ -411,12 +441,14 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
games/sdvx/sdvx_live2d.cpp
|
||||
games/sdvx/io.cpp
|
||||
games/sdvx/camera.cpp
|
||||
games/jb/bi2x_hook.cpp
|
||||
games/jb/jb.cpp
|
||||
games/jb/jb_touch.cpp
|
||||
games/jb/io.cpp
|
||||
games/nost/nost.cpp
|
||||
games/nost/io.cpp
|
||||
games/nost/poke.cpp
|
||||
games/nost/touch_mode.cpp
|
||||
games/gitadora/gitadora.cpp
|
||||
games/gitadora/asio.cpp
|
||||
games/gitadora/io.cpp
|
||||
@@ -502,6 +534,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
hooks/audio/buffer.cpp
|
||||
hooks/audio/mme.cpp
|
||||
hooks/audio/util.cpp
|
||||
hooks/audio/xact.cpp
|
||||
hooks/audio/backends/dsound/dsound_backend.cpp
|
||||
hooks/audio/backends/mmdevice/audio_endpoint_volume.cpp
|
||||
hooks/audio/backends/mmdevice/device.cpp
|
||||
@@ -528,10 +561,15 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
hooks/devicehook.cpp
|
||||
hooks/graphics/graphics.cpp
|
||||
hooks/graphics/graphics_windowed.cpp
|
||||
hooks/graphics/jpeg_encoder.cpp
|
||||
hooks/graphics/nvapi_impl.cpp
|
||||
hooks/graphics/nvapi_hook.cpp
|
||||
hooks/graphics/nvenc_hook.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_backend.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_readback.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_screenshot.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_swapchain.cpp
|
||||
@@ -571,7 +609,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
misc/device.cpp
|
||||
misc/eamuse.cpp
|
||||
misc/extdev.cpp
|
||||
misc/nativetouchhook.cpp
|
||||
misc/hotkeys.cpp
|
||||
misc/sciunit.cpp
|
||||
misc/sde.cpp
|
||||
misc/wintouchemu.cpp
|
||||
@@ -609,17 +647,26 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
overlay/windows/keypad.cpp
|
||||
overlay/windows/log.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/popn_sub.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.cpp
|
||||
rawinput/rawinput_handles.cpp
|
||||
rawinput/midi.cpp
|
||||
rawinput/sextet.cpp
|
||||
rawinput/piuio.cpp
|
||||
@@ -648,6 +695,11 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
|
||||
# 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_gestures.cpp
|
||||
touch/win7.cpp
|
||||
@@ -719,7 +771,7 @@ endfunction()
|
||||
add_library(spicetools_spice_objs OBJECT ${SOURCE_FILES})
|
||||
target_link_libraries(spicetools_spice_objs
|
||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg)
|
||||
target_link_libraries(spicetools_spice_objs PUBLIC winscard)
|
||||
|
||||
if(NOT MSVC)
|
||||
@@ -759,7 +811,7 @@ set(RESOURCE_FILES build/manifest.manifest build/manifest.rc build/icon.rc cfg/W
|
||||
add_executable(spicetools_spice_linux ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||
target_link_libraries(spicetools_spice_linux
|
||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg)
|
||||
set_target_properties(spicetools_spice_linux PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_spice_linux PROPERTIES OUTPUT_NAME "spice_linux")
|
||||
target_compile_definitions(spicetools_spice_linux PRIVATE NO_SCARD=1 PRIVATE SPICE_LINUX=1)
|
||||
@@ -777,7 +829,7 @@ add_executable(spicetools_spice64 ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
|
||||
target_link_libraries(spicetools_spice64
|
||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg)
|
||||
target_link_libraries(spicetools_spice64 PUBLIC winscard)
|
||||
set_target_properties(spicetools_spice64 PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64")
|
||||
@@ -800,7 +852,7 @@ add_executable(spicetools_spice64_linux ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
|
||||
target_link_libraries(spicetools_spice64_linux
|
||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg)
|
||||
set_target_properties(spicetools_spice64_linux PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_spice64_linux PROPERTIES OUTPUT_NAME "spice64_linux")
|
||||
target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE64=1)
|
||||
@@ -821,7 +873,7 @@ set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.
|
||||
add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||
target_link_libraries(spicetools_cfg
|
||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg)
|
||||
target_link_libraries(spicetools_cfg PUBLIC winscard)
|
||||
set_target_properties(spicetools_cfg PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_cfg PROPERTIES OUTPUT_NAME "spicecfg")
|
||||
@@ -839,7 +891,7 @@ set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.
|
||||
add_executable(spicetools_cfg_linux WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||
target_link_libraries(spicetools_cfg_linux
|
||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg)
|
||||
set_target_properties(spicetools_cfg_linux PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux")
|
||||
target_compile_definitions(spicetools_cfg_linux PRIVATE SPICETOOLS_SPICECFG_STANDALONE=1)
|
||||
|
||||
@@ -267,6 +267,20 @@ which also means that your hex edits are applicable directly.
|
||||
- `Side Panel Right Inner`
|
||||
- `Side Panel Right`
|
||||
|
||||
#### SDVX
|
||||
- tapeled_get(name: str, ...)
|
||||
- returns a list containing a dict of the current tape LED states. The dict keys are:
|
||||
- `Title`
|
||||
- `Upper Left Speaker`
|
||||
- `Upper Right Speaker`
|
||||
- `Left Wing`
|
||||
- `Right Wing`
|
||||
- `Control Panel`
|
||||
- `Lower Left Speaker`
|
||||
- `Lower Right Speaker`
|
||||
- `Woofer`
|
||||
- `V Unit`
|
||||
|
||||
#### LCD
|
||||
- info()
|
||||
- returns information about the serial LCD controller some games use
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "launcher/launcher.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
|
||||
// static stuff
|
||||
static int ACIO_WARMUP = 0;
|
||||
@@ -140,8 +141,9 @@ static int __cdecl ac_io_update(int a1) {
|
||||
// flush device output
|
||||
RI_MGR->devices_flush_output();
|
||||
|
||||
// update wintouchemu
|
||||
// update touch emulation
|
||||
wintouchemu::update();
|
||||
nativetouch::refresh_contact_lifetime();
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "games/nost/io.h"
|
||||
#include "games/nost/nost.h"
|
||||
#include "games/nost/touch_mode.h"
|
||||
#include "util/logging.h"
|
||||
#include "avs/game.h"
|
||||
|
||||
@@ -16,6 +17,7 @@ using namespace GameAPI;
|
||||
// static stuff
|
||||
static uint8_t STATUS_BUFFER[277];
|
||||
static bool STATUS_BUFFER_FREEZE = false;
|
||||
static constexpr uint8_t NOST_TOUCH_PIANO_VELOCITY = 11;
|
||||
|
||||
/*
|
||||
* Implementations
|
||||
@@ -226,6 +228,8 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
|
||||
games::nost::Analogs::Key27, games::nost::Analogs::Key28,
|
||||
};
|
||||
|
||||
const auto touch_key_state = games::nost::touch_mode::piano_key_state();
|
||||
|
||||
// iterate pairs of keys
|
||||
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) {
|
||||
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(
|
||||
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) {
|
||||
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
|
||||
uint8_t value = 0;
|
||||
|
||||
@@ -239,7 +239,7 @@ bool ICCADevice::parse_msg(MessageData *msg_in,
|
||||
// SDVX Old cabinet mode
|
||||
if (avs::game::is_model("KFC") && avs::game::SPEC[0] != 'G' && avs::game::SPEC[0] != 'H')
|
||||
answer_type = 1;
|
||||
if (avs::game::is_model("L44"))
|
||||
if (avs::game::is_model({ "L44", "T44" }))
|
||||
answer_type = 2;
|
||||
|
||||
// check answer type
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
namespace api {
|
||||
|
||||
extern std::atomic_uint32_t CLIENT_COUNT;
|
||||
|
||||
inline bool has_clients() {
|
||||
return CLIENT_COUNT.load(std::memory_order_relaxed) > 0;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "client.h"
|
||||
#include "cfg/configurator.h"
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "util/crypt.h"
|
||||
@@ -28,6 +29,7 @@
|
||||
#include "modules/lcd.h"
|
||||
#include "modules/lights.h"
|
||||
#include "modules/memory.h"
|
||||
#include "modules/sdvx.h"
|
||||
#include "modules/touch.h"
|
||||
#include "modules/resize.h"
|
||||
#include "request.h"
|
||||
@@ -36,6 +38,8 @@
|
||||
using namespace rapidjson;
|
||||
using namespace api;
|
||||
|
||||
std::atomic_uint32_t api::CLIENT_COUNT = 0;
|
||||
|
||||
Controller::Controller(unsigned short port, std::string password, bool pretty)
|
||||
: port(port), password(std::move(password)), pretty(pretty)
|
||||
{
|
||||
@@ -411,8 +415,11 @@ void Controller::init_state(api::ClientState *state) {
|
||||
state->modules.push_back(new modules::LCD());
|
||||
state->modules.push_back(new modules::Lights());
|
||||
state->modules.push_back(new modules::Memory());
|
||||
state->modules.push_back(new modules::SDVX());
|
||||
state->modules.push_back(new modules::Touch());
|
||||
state->modules.push_back(new modules::Resize());
|
||||
|
||||
CLIENT_COUNT.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void Controller::free_state(api::ClientState *state) {
|
||||
@@ -424,6 +431,8 @@ void Controller::free_state(api::ClientState *state) {
|
||||
|
||||
// free cipher
|
||||
delete state->cipher;
|
||||
|
||||
CLIENT_COUNT.fetch_sub(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void Controller::free_socket() {
|
||||
|
||||
@@ -93,8 +93,6 @@ namespace api::modules {
|
||||
* reduce: uint for dividing image size
|
||||
*/
|
||||
void Capture::get_jpg(Request &req, Response &res) {
|
||||
CAPTURE_BUFFER.clear();
|
||||
CAPTURE_BUFFER.reserve(1024 * 128);
|
||||
|
||||
// settings
|
||||
int screen = 0;
|
||||
@@ -121,9 +119,8 @@ namespace api::modules {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
graphics_capture_trigger(screen);
|
||||
bool success = graphics_capture_receive_jpeg(screen, [] (uint8_t byte) {
|
||||
CAPTURE_BUFFER.push_back(byte);
|
||||
}, true, quality, true, divide, ×tamp, &width, &height);
|
||||
bool success = graphics_capture_receive_jpeg(
|
||||
screen, CAPTURE_BUFFER, quality, divide, ×tamp, &width, &height);
|
||||
|
||||
if (success) {
|
||||
add_jpeg_response(screen, timestamp, width, height, CAPTURE_BUFFER, res);
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
#include "ddr.h"
|
||||
#include <functional>
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "games/ddr/ddr.h"
|
||||
|
||||
using namespace std::placeholders;
|
||||
using namespace rapidjson;
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
DDR::DDR() : Module("ddr") {
|
||||
functions["tapeled_get"] = std::bind(&DDR::tapeled_get, this, _1, _2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows fetching of the RGB LED strips that are gold cabinets, via SpiceAPI
|
||||
*/
|
||||
void DDR::tapeled_get(Request &req, Response &res) {
|
||||
static const char* device_names[11] = {
|
||||
"p1_foot_up",
|
||||
"p1_foot_right",
|
||||
"p1_foot_left",
|
||||
"p1_foot_down",
|
||||
"p2_foot_up",
|
||||
"p2_foot_right",
|
||||
"p2_foot_left",
|
||||
"p2_foot_down",
|
||||
"top_panel",
|
||||
"monitor_left",
|
||||
"monitor_right"
|
||||
};
|
||||
|
||||
Value response_object(kObjectType);
|
||||
|
||||
// Iterate through each device and dump its lights data into the response
|
||||
for (size_t device = 0; device < 11; device++) {
|
||||
size_t num_leds = 25;
|
||||
if (device > 7)
|
||||
num_leds = 50;
|
||||
|
||||
Value light_state(kArrayType);
|
||||
light_state.Reserve(num_leds * 3, res.doc()->GetAllocator());
|
||||
for (size_t led = 0; led < num_leds; led++) {
|
||||
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][0], res.doc()->GetAllocator());
|
||||
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][1], res.doc()->GetAllocator());
|
||||
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][2], res.doc()->GetAllocator());
|
||||
}
|
||||
|
||||
response_object.AddMember(StringRef(device_names[device]), light_state, res.doc()->GetAllocator());
|
||||
}
|
||||
|
||||
res.add_data(response_object);
|
||||
}
|
||||
}
|
||||
#include "ddr.h"
|
||||
#include <functional>
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "games/ddr/ddr.h"
|
||||
|
||||
using namespace std::placeholders;
|
||||
using namespace rapidjson;
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
DDR::DDR() : Module("ddr") {
|
||||
functions["tapeled_get"] = std::bind(&DDR::tapeled_get, this, _1, _2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows fetching of the RGB LED strips that are gold cabinets, via SpiceAPI
|
||||
*/
|
||||
void DDR::tapeled_get(Request &req, Response &res) {
|
||||
static const char* device_names[11] = {
|
||||
"p1_foot_up",
|
||||
"p1_foot_right",
|
||||
"p1_foot_left",
|
||||
"p1_foot_down",
|
||||
"p2_foot_up",
|
||||
"p2_foot_right",
|
||||
"p2_foot_left",
|
||||
"p2_foot_down",
|
||||
"top_panel",
|
||||
"monitor_left",
|
||||
"monitor_right"
|
||||
};
|
||||
|
||||
Value response_object(kObjectType);
|
||||
|
||||
// Iterate through each device and dump its lights data into the response
|
||||
for (size_t device = 0; device < 11; device++) {
|
||||
size_t num_leds = 25;
|
||||
if (device > 7)
|
||||
num_leds = 50;
|
||||
|
||||
Value light_state(kArrayType);
|
||||
light_state.Reserve(num_leds * 3, res.doc()->GetAllocator());
|
||||
for (size_t led = 0; led < num_leds; led++) {
|
||||
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][0], res.doc()->GetAllocator());
|
||||
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][1], res.doc()->GetAllocator());
|
||||
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][2], res.doc()->GetAllocator());
|
||||
}
|
||||
|
||||
response_object.AddMember(StringRef(device_names[device]), light_state, res.doc()->GetAllocator());
|
||||
}
|
||||
|
||||
res.add_data(response_object);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include "api/module.h"
|
||||
#include "api/request.h"
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
class DDR : public Module {
|
||||
public:
|
||||
DDR();
|
||||
|
||||
private:
|
||||
// function definitions
|
||||
void tapeled_get(Request &req, Response &res);
|
||||
};
|
||||
}
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include "api/module.h"
|
||||
#include "api/request.h"
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
class DDR : public Module {
|
||||
public:
|
||||
DDR();
|
||||
|
||||
private:
|
||||
// function definitions
|
||||
void tapeled_get(Request &req, Response &res);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -106,14 +106,16 @@ namespace api::modules {
|
||||
void IIDX::copy_tapeled_data(Response &res, Value &response_object, const tapeledutils::tape_led &mapping) {
|
||||
// Create an array for the light state
|
||||
Value light_state(kArrayType);
|
||||
light_state.Reserve(mapping.data.capacity() * 3, res.doc()->GetAllocator());
|
||||
light_state.Reserve(
|
||||
static_cast<SizeType>(mapping.data.size() * 3),
|
||||
res.doc()->GetAllocator());
|
||||
for (const auto [r, g, b] : mapping.data) {
|
||||
light_state.PushBack(r, res.doc()->GetAllocator());
|
||||
light_state.PushBack(g, res.doc()->GetAllocator());
|
||||
light_state.PushBack(b, res.doc()->GetAllocator());
|
||||
}
|
||||
|
||||
// Can't use StringRef here, turns some strings partially into null bytes for some reason
|
||||
// can't use StringRef here, turns some strings partially into null bytes for some reason
|
||||
Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator());
|
||||
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#include "keypads.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "util/precise_timer.h"
|
||||
|
||||
using namespace std::placeholders;
|
||||
using namespace rapidjson;
|
||||
@@ -59,7 +60,6 @@ namespace api::modules {
|
||||
// get params
|
||||
auto keypad = req.params[0].GetUint();
|
||||
auto input = std::string(req.params[1].GetString());
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
// process all chars
|
||||
for (auto c : input) {
|
||||
@@ -93,11 +93,11 @@ namespace api::modules {
|
||||
|
||||
// set
|
||||
eamuse_set_keypad_overrides(keypad, state);
|
||||
timer.sleep(sleep_time);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
|
||||
|
||||
// unset
|
||||
eamuse_set_keypad_overrides(keypad, 0);
|
||||
timer.sleep(sleep_time);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#include "sdvx.h"
|
||||
|
||||
#include <functional>
|
||||
|
||||
using namespace std::placeholders;
|
||||
using namespace rapidjson;
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
SDVX::SDVX() : Module("sdvx") {
|
||||
functions["tapeled_get"] = std::bind(&SDVX::tapeled_get, this, _1, _2);
|
||||
|
||||
for (auto &light : games::sdvx::TAPELED_MAPPING) {
|
||||
lights_by_names.emplace(light.lightName, light);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* tapeled_get()
|
||||
* tapeled_get(name: str, ...)
|
||||
*/
|
||||
void SDVX::tapeled_get(Request &req, Response &res) {
|
||||
Value response_object(kObjectType);
|
||||
|
||||
// all tape leds
|
||||
if (req.params.Size() == 0) {
|
||||
// iterate through each device and dump its lights data into the response
|
||||
for (const auto &mapping : games::sdvx::TAPELED_MAPPING) {
|
||||
copy_tapeled_data(res, response_object, mapping);
|
||||
}
|
||||
} else {
|
||||
// specified light names
|
||||
for (Value ¶m : req.params.GetArray()) {
|
||||
// check params
|
||||
if (!param.IsString()) {
|
||||
error_type(res, "name", "string");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto name = param.GetString();
|
||||
if (const auto &it = lights_by_names.find(name); it != lights_by_names.end()) {
|
||||
copy_tapeled_data(res, response_object, it->second.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.add_data(response_object);
|
||||
}
|
||||
|
||||
void SDVX::copy_tapeled_data(Response &res, Value &response_object,
|
||||
const tapeledutils::tape_led &mapping)
|
||||
{
|
||||
Value light_state(kArrayType);
|
||||
light_state.Reserve(
|
||||
static_cast<SizeType>(mapping.data.size() * 3),
|
||||
res.doc()->GetAllocator());
|
||||
for (const auto [r, g, b] : mapping.data) {
|
||||
light_state.PushBack(r, res.doc()->GetAllocator());
|
||||
light_state.PushBack(g, res.doc()->GetAllocator());
|
||||
light_state.PushBack(b, res.doc()->GetAllocator());
|
||||
}
|
||||
|
||||
// can't use StringRef here, turns some strings partially into null bytes for some reason
|
||||
Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator());
|
||||
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "api/module.h"
|
||||
#include "api/request.h"
|
||||
#include "external/robin_hood.h"
|
||||
#include "games/sdvx/sdvx.h"
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
class SDVX : public Module {
|
||||
public:
|
||||
SDVX();
|
||||
|
||||
private:
|
||||
robin_hood::unordered_map<std::string, std::reference_wrapper<tapeledutils::tape_led>> lights_by_names;
|
||||
|
||||
void tapeled_get(Request &req, Response &res);
|
||||
void copy_tapeled_data(Response &res, rapidjson::Value &response_object,
|
||||
const tapeledutils::tape_led &mapping);
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "touch.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
#include "external/rapidjson/document.h"
|
||||
@@ -8,7 +10,11 @@
|
||||
#include "misc/eamuse.h"
|
||||
#include "launcher/launcher.h"
|
||||
#include "touch/touch.h"
|
||||
#include "touch/native/inject.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "touch/native/transform.h"
|
||||
#include "util/utils.h"
|
||||
#include "games/gitadora/gitadora.h"
|
||||
#include "games/iidx/iidx.h"
|
||||
|
||||
using namespace std::placeholders;
|
||||
@@ -17,6 +23,54 @@ using namespace rapidjson;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// map API coordinates onto the touch space SDVX reads, which depends on how it is displayed
|
||||
static void sdvx_touch_errata(
|
||||
int &x, int &y, bool use_native, int canvas_w, int canvas_h) {
|
||||
|
||||
// windowed coordinates already match the sub screen window they land on
|
||||
if (GRAPHICS_WINDOWED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// landscape mode: native injection hands the game these coordinates
|
||||
// unchanged, so apply the rotation the touchscreen gets, while wintouchemu instead
|
||||
// rotates them later through the subscreen overlay
|
||||
if (GRAPHICS_FS_ORIENTATION_SWAP) {
|
||||
if (use_native) {
|
||||
POINT position { x, y };
|
||||
if (nativetouch::transform::sdvx_landscape_rotate(&position, canvas_w, canvas_h)) {
|
||||
x = position.x;
|
||||
y = position.y;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// rotate into the portrait touch space
|
||||
const int x_raw = x;
|
||||
x = canvas_w - y;
|
||||
y = x_raw;
|
||||
}
|
||||
|
||||
Touch::Touch() : Module("touch") {
|
||||
is_sdvx = avs::game::is_model("KFC");
|
||||
|
||||
@@ -26,6 +80,32 @@ namespace api::modules {
|
||||
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) {
|
||||
// windowed API coordinates land on the sub screen window as-is; fullscreen
|
||||
// coordinates are rotated into the game's touch space by apply_touch_errata
|
||||
const bool landscape_coordinates =
|
||||
GRAPHICS_WINDOWED || GRAPHICS_FS_ORIENTATION_SWAP;
|
||||
native_canvas_w = landscape_coordinates ? 1920 : 1080;
|
||||
native_canvas_h = landscape_coordinates ? 1080 : 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;
|
||||
} else if (games::gitadora::is_arena_model()) {
|
||||
// GITADORA arena SMALL subscreen, either in its own window or in the overlay
|
||||
native_canvas_w = games::gitadora::ARENA_SUBSCREEN_WIDTH;
|
||||
native_canvas_h = games::gitadora::ARENA_SUBSCREEN_HEIGHT;
|
||||
}
|
||||
|
||||
functions["read"] = std::bind(&Touch::read, this, _1, _2);
|
||||
functions["write"] = std::bind(&Touch::write, this, _1, _2);
|
||||
functions["write_reset"] = std::bind(&Touch::write_reset, this, _1, _2);
|
||||
@@ -99,7 +179,11 @@ namespace api::modules {
|
||||
}
|
||||
|
||||
// apply touch points
|
||||
touch_write_points(&touch_points);
|
||||
if (use_native) {
|
||||
write_native(touch_points);
|
||||
} else {
|
||||
touch_write_points(&touch_points);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,23 +207,54 @@ namespace api::modules {
|
||||
}
|
||||
|
||||
// remove all IDs
|
||||
touch_remove_points(&touch_point_ids);
|
||||
if (use_native) {
|
||||
write_reset_native(touch_point_ids);
|
||||
} else {
|
||||
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) {
|
||||
int x_raw = x;
|
||||
int y_raw = y;
|
||||
|
||||
if (is_tdj_fhd) {
|
||||
// deal with TDJ FHD resolution mismatch (upgrade 720p to 1080p)
|
||||
// we don't know what screen is being shown on the companion and the API doesn't specify
|
||||
// the target of the touch events so just assume it's the sub screen
|
||||
x = x_raw * 1920 / 1280;
|
||||
y = y_raw * 1080 / 720;
|
||||
x = x * 1920 / 1280;
|
||||
y = y * 1080 / 720;
|
||||
} else if (is_sdvx) {
|
||||
// for exceed gear, they are both 1080p screens, but need to apply transformation
|
||||
x = 1080 - y_raw;
|
||||
y = x_raw;
|
||||
sdvx_touch_errata(x, y, use_native, native_canvas_w, native_canvas_h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include "api/module.h"
|
||||
#include "api/request.h"
|
||||
#include "touch/touch.h"
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
@@ -13,11 +17,21 @@ namespace api::modules {
|
||||
private:
|
||||
bool is_sdvx;
|
||||
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
|
||||
void read(Request &req, Response &res);
|
||||
void write(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);
|
||||
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ from .coin import *
|
||||
from .control import *
|
||||
from .exceptions import *
|
||||
from .iidx import *
|
||||
from .sdvx import *
|
||||
from .info import *
|
||||
from .keypads import *
|
||||
from .lights import *
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from .connection import Connection
|
||||
from .request import Request
|
||||
|
||||
|
||||
def sdvx_tapeled_get(con: Connection, *light_names):
|
||||
req = Request("sdvx", "tapeled_get")
|
||||
|
||||
for light_name in light_names:
|
||||
req.add_param(light_name)
|
||||
|
||||
res = con.request(req)
|
||||
return res.get_data()
|
||||
@@ -1,11 +1,12 @@
|
||||
#include "controller.h"
|
||||
#include "serial.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
|
||||
@@ -17,7 +18,6 @@ namespace api {
|
||||
controller->init_state(this->state);
|
||||
this->thread = new std::thread([this] () {
|
||||
log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud);
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
// read buffer
|
||||
uint8_t read_buffer[16*1024];
|
||||
@@ -162,7 +162,7 @@ namespace api {
|
||||
|
||||
// slow down on reconnect
|
||||
if (this->running) {
|
||||
timer.sleep(retry_time);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(retry_time));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1114,6 +1114,7 @@ namespace avs {
|
||||
#endif
|
||||
// jubeat
|
||||
{"jubeat.dll", 0x2000000},
|
||||
{"jubeat2019.dll", 0x10000000},
|
||||
|
||||
// MUSECA
|
||||
{"museca.dll", 0xC000000},
|
||||
@@ -1924,7 +1925,7 @@ namespace avs {
|
||||
#if !SPICE64
|
||||
// sdvx4 bad log config fix
|
||||
if (avs::game::DLL_NAME == "soundvoltex.dll" && // it's too early for avs::game::is_model
|
||||
property_search_safe(config, config_node, "/log/enable_console")) {
|
||||
property_search(config, config_node, "/log/enable_console")) {
|
||||
log_info("avs-core", "applying SDVX4 avs-config.xml fix for <log><enable_console>");
|
||||
property_search_remove_safe(config, config_node, "/log/enable_console");
|
||||
}
|
||||
|
||||
@@ -591,6 +591,7 @@ namespace avs {
|
||||
// for proper reporting of Omnimix and other song packs
|
||||
if (_stricmp(EA3_MODEL, "LDJ") == 0 ||
|
||||
_stricmp(EA3_MODEL, "L44") == 0 ||
|
||||
_stricmp(EA3_MODEL, "T44") == 0 ||
|
||||
_stricmp(EA3_MODEL, "M39") == 0 ||
|
||||
_stricmp(EA3_MODEL, "KFC") == 0)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
<dpiAware>true</dpiAware>
|
||||
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">Legacy</activeCodePage>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
<dependency>
|
||||
|
||||
+66
-11
@@ -3,7 +3,9 @@
|
||||
#include <cassert>
|
||||
#include <optional>
|
||||
|
||||
#include "games/io.h"
|
||||
#include "launcher/superexit.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "rawinput/piuio.h"
|
||||
#include "util/time.h"
|
||||
@@ -62,7 +64,43 @@ std::vector<Button> GameAPI::Buttons::sortButtons(
|
||||
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
|
||||
if (_button.override_enabled) {
|
||||
@@ -76,6 +114,23 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
||||
std::optional<bool> window_has_focus;
|
||||
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
|
||||
if (current_button->isNaive()) {
|
||||
GameAPI::Buttons::State state;
|
||||
@@ -469,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) {
|
||||
if (manager) {
|
||||
return getState(manager.get(), button, check_alts);
|
||||
@@ -484,17 +543,13 @@ static float getVelocityHelper(rawinput::RawInputManager *manager, Button &butto
|
||||
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
|
||||
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
|
||||
if (button_state != Buttons::BUTTON_PRESSED) {
|
||||
|
||||
@@ -299,7 +299,27 @@ std::string Button::getMidiNoteString() {
|
||||
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
|
||||
auto vKey = (uint16_t) this->getVKey();
|
||||
|
||||
@@ -49,6 +49,7 @@ private:
|
||||
std::string name;
|
||||
std::string device_identifier = "";
|
||||
unsigned short vKey = INVALID_VKEY;
|
||||
uint8_t modifier_mask = 0;
|
||||
|
||||
// default bindings are always naive
|
||||
unsigned short vKey_default = INVALID_VKEY;
|
||||
@@ -65,6 +66,7 @@ private:
|
||||
unsigned short velocity_threshold = 0;
|
||||
|
||||
std::string getMidiNoteString();
|
||||
std::string getBindingDisplayString(rawinput::RawInputManager *manager);
|
||||
|
||||
public:
|
||||
std::string getVKeyString();
|
||||
@@ -111,6 +113,7 @@ public:
|
||||
device_identifier = "";
|
||||
analog_type = BAT_NONE;
|
||||
bat_threshold = 0;
|
||||
modifier_mask = 0;
|
||||
}
|
||||
|
||||
std::string getDisplayString(rawinput::RawInputManager* manager);
|
||||
@@ -123,6 +126,14 @@ public:
|
||||
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 {
|
||||
return this->device_identifier;
|
||||
}
|
||||
|
||||
@@ -165,6 +165,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
int bat_threshold = 0;
|
||||
int velocity_threshold = 0;
|
||||
bool invert = false;
|
||||
unsigned int modifier_mask = 0;
|
||||
tinyxml2::XMLError attrError = gameButtonNode->QueryIntAttribute("vkey", &vKey);
|
||||
const char *devid = gameButtonNode->Attribute("devid");
|
||||
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
|
||||
@@ -173,6 +174,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
|
||||
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
|
||||
gameButtonNode->QueryBoolAttribute("invert", &invert);
|
||||
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
|
||||
if (attrError != tinyxml2::XMLError::XML_SUCCESS) {
|
||||
gameButtonsNode->DeleteChild(gameButtonNode);
|
||||
gameButtonNode = this->configFile.NewElement("button");
|
||||
@@ -185,6 +187,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->SetAttribute("bat_threshold", bat_threshold);
|
||||
gameButtonNode->SetAttribute("velocity_threshold", velocity_threshold);
|
||||
gameButtonNode->SetAttribute("invert", invert);
|
||||
gameButtonNode->SetAttribute("modifiers", button->getModifierMask());
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
} else {
|
||||
button->setVKey(static_cast<unsigned short int>(vKey));
|
||||
@@ -194,6 +197,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
button->setBatThreshold(bat_threshold);
|
||||
button->setVelocityThreshold(velocity_threshold);
|
||||
button->setInvert(invert);
|
||||
button->setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||
if (devid) {
|
||||
button->setDeviceIdentifier(devid);
|
||||
}
|
||||
@@ -214,6 +218,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
|
||||
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
|
||||
gameButtonNode->SetAttribute("invert", it.getInvert());
|
||||
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
|
||||
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
}
|
||||
@@ -440,6 +445,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
|
||||
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
|
||||
gameButtonNode->SetAttribute("invert", it.getInvert());
|
||||
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
|
||||
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
|
||||
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("invert", button.getInvert());
|
||||
gameButtonNode->SetAttribute("devid", button.getDeviceIdentifier().c_str());
|
||||
gameButtonNode->SetAttribute("modifiers", button.getModifierMask());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -569,6 +576,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
|
||||
gameButtonNode->SetAttribute("velocity_threshold", 0);
|
||||
gameButtonNode->SetAttribute("invert", false);
|
||||
gameButtonNode->SetAttribute("devid", "");
|
||||
gameButtonNode->SetAttribute("modifiers", 0);
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
}
|
||||
}
|
||||
@@ -964,6 +972,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
int bat_threshold = 0;
|
||||
int velocity_threshold = 0;
|
||||
bool invert = false;
|
||||
unsigned int modifier_mask = 0;
|
||||
gameButtonNode->QueryIntAttribute("vkey", &vKey);
|
||||
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
|
||||
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("velocity_threshold", &velocity_threshold);
|
||||
gameButtonNode->QueryBoolAttribute("invert", &invert);
|
||||
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
|
||||
const char *devid = gameButtonNode->Attribute("devid");
|
||||
|
||||
// find alternative
|
||||
@@ -986,6 +996,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
alt.setBatThreshold(bat_threshold);
|
||||
alt.setVelocityThreshold(velocity_threshold);
|
||||
alt.setInvert(invert);
|
||||
alt.setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||
if (devid) {
|
||||
alt.setDeviceIdentifier(std::string(devid));
|
||||
}
|
||||
@@ -1005,6 +1016,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
button.setBatThreshold(bat_threshold);
|
||||
button.setVelocityThreshold(velocity_threshold);
|
||||
button.setInvert(invert);
|
||||
button.setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||
if (devid) {
|
||||
button.setDeviceIdentifier(devid);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace overlay::windows {
|
||||
el->SetAttribute("debounce_down", entry.debounce_down);
|
||||
el->SetAttribute("bat_threshold", entry.bat_threshold);
|
||||
el->SetAttribute("velocity_threshold", entry.velocity_threshold);
|
||||
el->SetAttribute("modifiers", entry.modifier_mask);
|
||||
parent->InsertEndChild(el);
|
||||
}
|
||||
|
||||
@@ -54,6 +55,10 @@ namespace overlay::windows {
|
||||
el->QueryIntAttribute("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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
auto *kp_el = tmpl_el->FirstChildElement("keypad_buttons");
|
||||
if (kp_el) {
|
||||
@@ -244,6 +271,7 @@ namespace overlay::windows {
|
||||
bool save_user_template(
|
||||
const ControllerTemplate &tmpl,
|
||||
const bool save_buttons,
|
||||
const bool save_modifiers,
|
||||
const bool save_keypads,
|
||||
const bool save_analogs,
|
||||
const bool save_lights) {
|
||||
@@ -300,6 +328,24 @@ namespace overlay::windows {
|
||||
}
|
||||
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
|
||||
if (!tmpl.keypad_buttons.empty()) {
|
||||
auto *kp_el = doc.NewElement("keypad_buttons");
|
||||
|
||||
@@ -1,72 +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}")
|
||||
# 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}")
|
||||
|
||||
+5
@@ -7,6 +7,7 @@ RUN pacman --noconfirm -Syu git \
|
||||
ninja \
|
||||
cmake \
|
||||
unzip \
|
||||
nasm \
|
||||
mingw-w64-crt \
|
||||
mingw-w64-winpthreads \
|
||||
mingw-w64-gcc \
|
||||
@@ -23,3 +24,7 @@ ENV PATH="$PATH:/opt/llvm-mingw-xp/bin"
|
||||
|
||||
RUN curl -fsSL "https://github.com/mon/windows-dll-compat-checker/releases/download/v1.3/windows_dll_compat_checker-linux-x86_64.tar.xz" \
|
||||
| tar -xJ -C /usr/local/bin
|
||||
|
||||
# libjpeg-turbo for JPEG encoding. Only the mingw-w64 toolchains get it;
|
||||
# the WinXP targets build without JPEG support entirely.
|
||||
RUN su user -c "yay --noconfirm -S mingw-w64-libjpeg-turbo"
|
||||
+95
-95
@@ -1,95 +1,95 @@
|
||||
Copyright (c) 2017, keshikan (http://www.keshikan.net),
|
||||
with Reserved Font Name "DSEG".
|
||||
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Copyright (c) 2017, keshikan (http://www.keshikan.net),
|
||||
with Reserved Font Name "DSEG".
|
||||
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
+11023
-11023
File diff suppressed because one or more lines are too long
+552
-552
File diff suppressed because it is too large
Load Diff
+73
-73
@@ -1,73 +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 */
|
||||
#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 */
|
||||
|
||||
Vendored
+3222
File diff suppressed because it is too large
Load Diff
Vendored
+122
@@ -0,0 +1,122 @@
|
||||
// fpng.h - unlicense (see end of fpng.cpp)
|
||||
#pragma once
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <vector>
|
||||
|
||||
#ifndef FPNG_TRAIN_HUFFMAN_TABLES
|
||||
// Set to 1 when using the -t (training) option in fpng_test to generate new opaque/alpha Huffman tables for the single pass encoder.
|
||||
#define FPNG_TRAIN_HUFFMAN_TABLES (0)
|
||||
#endif
|
||||
|
||||
namespace fpng
|
||||
{
|
||||
// ---- Library initialization - call once to identify if the processor supports SSE.
|
||||
// Otherwise you'll only get scalar fallbacks.
|
||||
void fpng_init();
|
||||
|
||||
// ---- Useful Utilities
|
||||
|
||||
// Returns true if the CPU supports SSE 4.1, and SSE support wasn't disabled by setting FPNG_NO_SSE=1.
|
||||
// fpng_init() must have been called first, or it'll assert and return false.
|
||||
bool fpng_cpu_supports_sse41();
|
||||
|
||||
// Fast CRC-32 SSE4.1+pclmul or a scalar fallback (slice by 4)
|
||||
const uint32_t FPNG_CRC32_INIT = 0;
|
||||
uint32_t fpng_crc32(const void* pData, size_t size, uint32_t prev_crc32 = FPNG_CRC32_INIT);
|
||||
|
||||
// Fast Adler32 SSE4.1 Adler-32 with a scalar fallback.
|
||||
const uint32_t FPNG_ADLER32_INIT = 1;
|
||||
uint32_t fpng_adler32(const void* pData, size_t size, uint32_t adler = FPNG_ADLER32_INIT);
|
||||
|
||||
// ---- Compression
|
||||
enum
|
||||
{
|
||||
// Enables computing custom Huffman tables for each file, instead of using the custom global tables.
|
||||
// Results in roughly 6% smaller files on average, but compression is around 40% slower.
|
||||
FPNG_ENCODE_SLOWER = 1,
|
||||
|
||||
// Only use raw Deflate blocks (no compression at all). Intended for testing.
|
||||
FPNG_FORCE_UNCOMPRESSED = 2,
|
||||
};
|
||||
|
||||
// Fast PNG encoding. The resulting file can be decoded either using a standard PNG decoder or by the fpng_decode_memory() function below.
|
||||
// pImage: pointer to RGB or RGBA image pixels, R first in memory, B/A last.
|
||||
// w/h - image dimensions. Image's row pitch in bytes must is w*num_chans.
|
||||
// num_chans must be 3 or 4.
|
||||
bool fpng_encode_image_to_memory(const void* pImage, uint32_t w, uint32_t h, uint32_t num_chans, std::vector<uint8_t>& out_buf, uint32_t flags = 0);
|
||||
|
||||
#ifndef FPNG_NO_STDIO
|
||||
// Fast PNG encoding to the specified file.
|
||||
bool fpng_encode_image_to_file(const char* pFilename, const void* pImage, uint32_t w, uint32_t h, uint32_t num_chans, uint32_t flags = 0);
|
||||
#endif
|
||||
|
||||
// ---- Decompression
|
||||
|
||||
enum
|
||||
{
|
||||
FPNG_DECODE_SUCCESS = 0, // file is a valid PNG file and written by FPNG and the decode succeeded
|
||||
|
||||
FPNG_DECODE_NOT_FPNG, // file is a valid PNG file, but it wasn't written by FPNG so you should try decoding it with a general purpose PNG decoder
|
||||
|
||||
FPNG_DECODE_INVALID_ARG, // invalid function parameter
|
||||
|
||||
FPNG_DECODE_FAILED_NOT_PNG, // file cannot be a PNG file
|
||||
FPNG_DECODE_FAILED_HEADER_CRC32, // a chunk CRC32 check failed, file is likely corrupted or not PNG
|
||||
FPNG_DECODE_FAILED_INVALID_DIMENSIONS, // invalid image dimensions in IHDR chunk (0 or too large)
|
||||
FPNG_DECODE_FAILED_DIMENSIONS_TOO_LARGE, // decoding the file fully into memory would likely require too much memory (only on 32bpp builds)
|
||||
FPNG_DECODE_FAILED_CHUNK_PARSING, // failed while parsing the chunk headers, or file is corrupted
|
||||
FPNG_DECODE_FAILED_INVALID_IDAT, // IDAT data length is too small and cannot be valid, file is either corrupted or it's a bug
|
||||
|
||||
// fpng_decode_file() specific errors
|
||||
FPNG_DECODE_FILE_OPEN_FAILED,
|
||||
FPNG_DECODE_FILE_TOO_LARGE,
|
||||
FPNG_DECODE_FILE_READ_FAILED,
|
||||
FPNG_DECODE_FILE_SEEK_FAILED
|
||||
};
|
||||
|
||||
// Fast PNG decoding of files ONLY created by fpng_encode_image_to_memory() or fpng_encode_image_to_file().
|
||||
// If fpng_get_info() or fpng_decode_memory() returns FPNG_DECODE_NOT_FPNG, you should decode the PNG by falling back to a general purpose decoder.
|
||||
//
|
||||
// fpng_get_info() parses the PNG header and iterates through all chunks to determine if it's a file written by FPNG, but does not decompress the actual image data so it's relatively fast.
|
||||
//
|
||||
// pImage, image_size: Pointer to PNG image data and its size
|
||||
// width, height: output image's dimensions
|
||||
// channels_in_file: will be 3 or 4
|
||||
//
|
||||
// Returns FPNG_DECODE_SUCCESS on success, otherwise one of the failure codes above.
|
||||
// If FPNG_DECODE_NOT_FPNG is returned, you must decompress the file with a general purpose PNG decoder.
|
||||
// If another error occurs, the file is likely corrupted or invalid, but you can still try to decompress the file with another decoder (which will likely fail).
|
||||
int fpng_get_info(const void* pImage, uint32_t image_size, uint32_t& width, uint32_t& height, uint32_t& channels_in_file);
|
||||
|
||||
// fpng_decode_memory() decompresses 24/32bpp PNG files ONLY encoded by this module.
|
||||
// If the image was written by FPNG, it will decompress the image data, otherwise it will return FPNG_DECODE_NOT_FPNG in which case you should fall back to a general purpose PNG decoder (lodepng, stb_image, libpng, etc.)
|
||||
//
|
||||
// pImage, image_size: Pointer to PNG image data and its size
|
||||
// out: Output 24/32bpp image buffer
|
||||
// width, height: output image's dimensions
|
||||
// channels_in_file: will be 3 or 4
|
||||
// desired_channels: must be 3 or 4
|
||||
//
|
||||
// If the image is 24bpp and 32bpp is requested, the alpha values will be set to 0xFF.
|
||||
// If the image is 32bpp and 24bpp is requested, the alpha values will be discarded.
|
||||
//
|
||||
// Returns FPNG_DECODE_SUCCESS on success, otherwise one of the failure codes above.
|
||||
// If FPNG_DECODE_NOT_FPNG is returned, you must decompress the file with a general purpose PNG decoder.
|
||||
// If another error occurs, the file is likely corrupted or invalid, but you can still try to decompress the file with another decoder (which will likely fail).
|
||||
int fpng_decode_memory(const void* pImage, uint32_t image_size, std::vector<uint8_t>& out, uint32_t& width, uint32_t& height, uint32_t& channels_in_file, uint32_t desired_channels);
|
||||
|
||||
#ifndef FPNG_NO_STDIO
|
||||
int fpng_decode_file(const char* pFilename, std::vector<uint8_t>& out, uint32_t& width, uint32_t& height, uint32_t& channels_in_file, uint32_t desired_channels);
|
||||
#endif
|
||||
|
||||
// ---- Internal API used for Huffman table training purposes
|
||||
|
||||
#if FPNG_TRAIN_HUFFMAN_TABLES
|
||||
const uint32_t HUFF_COUNTS_SIZE = 288;
|
||||
extern uint64_t g_huff_counts[HUFF_COUNTS_SIZE];
|
||||
bool create_dynamic_block_prefix(uint64_t* pFreq, uint32_t num_chans, std::vector<uint8_t>& prefix, uint64_t& bit_buf, int& bit_buf_size, uint32_t *pCodes, uint8_t *pCodesizes);
|
||||
#endif
|
||||
|
||||
} // namespace fpng
|
||||
+9
-1
@@ -878,7 +878,15 @@ inline void ImGui::FileBrowser::UpdateFileRecords()
|
||||
|
||||
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();
|
||||
selectedFilenames_.clear();
|
||||
(*inputNameBuf_)[0] = '\0';
|
||||
|
||||
+835
-835
File diff suppressed because it is too large
Load Diff
Vendored
-10
@@ -1,10 +0,0 @@
|
||||
zlib License
|
||||
|
||||
Copyright (c) 2011-2016 Stephan Brumme
|
||||
|
||||
This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
|
||||
Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
|
||||
1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software.
|
||||
If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
-665
@@ -1,665 +0,0 @@
|
||||
// //////////////////////////////////////////////////////////
|
||||
// toojpeg.cpp
|
||||
// written by Stephan Brumme, 2018-2019
|
||||
// see https://create.stephan-brumme.com/toojpeg/
|
||||
//
|
||||
|
||||
#include "toojpeg.h"
|
||||
|
||||
// - the "official" specifications: https://www.w3.org/Graphics/JPEG/itu-t81.pdf and https://www.w3.org/Graphics/JPEG/jfif3.pdf
|
||||
// - Wikipedia has a short description of the JFIF/JPEG file format: https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format
|
||||
// - the popular STB Image library includes Jon's JPEG encoder as well: https://github.com/nothings/stb/blob/master/stb_image_write.h
|
||||
// - the most readable JPEG book (from a developer's perspective) is Miano's "Compressed Image File Formats" (1999, ISBN 0-201-60443-4),
|
||||
// used copies are really cheap nowadays and include a CD with C++ sources as well (plus great format descriptions of GIF & PNG)
|
||||
// - much more detailled is Mitchell/Pennebaker's "JPEG: Still Image Data Compression Standard" (1993, ISBN 0-442-01272-1)
|
||||
// which contains the official JPEG standard, too - fun fact: I bought a signed copy in a second-hand store without noticing
|
||||
|
||||
namespace // anonymous namespace to hide local functions / constants / etc.
|
||||
{
|
||||
// ////////////////////////////////////////
|
||||
// data types
|
||||
using uint8_t = unsigned char;
|
||||
using uint16_t = unsigned short;
|
||||
using int16_t = short;
|
||||
using int32_t = int; // at least four bytes
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// constants
|
||||
|
||||
// quantization tables from JPEG Standard, Annex K
|
||||
const uint8_t DefaultQuantLuminance[8*8] =
|
||||
{ 16, 11, 10, 16, 24, 40, 51, 61, // there are a few experts proposing slightly more efficient values,
|
||||
12, 12, 14, 19, 26, 58, 60, 55, // e.g. https://www.imagemagick.org/discourse-server/viewtopic.php?t=20333
|
||||
14, 13, 16, 24, 40, 57, 69, 56, // btw: Google's Guetzli project optimizes the quantization tables per image
|
||||
14, 17, 22, 29, 51, 87, 80, 62,
|
||||
18, 22, 37, 56, 68,109,103, 77,
|
||||
24, 35, 55, 64, 81,104,113, 92,
|
||||
49, 64, 78, 87,103,121,120,101,
|
||||
72, 92, 95, 98,112,100,103, 99 };
|
||||
const uint8_t DefaultQuantChrominance[8*8] =
|
||||
{ 17, 18, 24, 47, 99, 99, 99, 99,
|
||||
18, 21, 26, 66, 99, 99, 99, 99,
|
||||
24, 26, 56, 99, 99, 99, 99, 99,
|
||||
47, 66, 99, 99, 99, 99, 99, 99,
|
||||
99, 99, 99, 99, 99, 99, 99, 99,
|
||||
99, 99, 99, 99, 99, 99, 99, 99,
|
||||
99, 99, 99, 99, 99, 99, 99, 99,
|
||||
99, 99, 99, 99, 99, 99, 99, 99 };
|
||||
|
||||
// 8x8 blocks are processed in zig-zag order
|
||||
// most encoders use a zig-zag "forward" table, I switched to its inverse for performance reasons
|
||||
// note: ZigZagInv[ZigZag[i]] = i
|
||||
const uint8_t ZigZagInv[8*8] =
|
||||
{ 0, 1, 8,16, 9, 2, 3,10, // ZigZag[] = 0, 1, 5, 6,14,15,27,28,
|
||||
17,24,32,25,18,11, 4, 5, // 2, 4, 7,13,16,26,29,42,
|
||||
12,19,26,33,40,48,41,34, // 3, 8,12,17,25,30,41,43,
|
||||
27,20,13, 6, 7,14,21,28, // 9,11,18,24,31,40,44,53,
|
||||
35,42,49,56,57,50,43,36, // 10,19,23,32,39,45,52,54,
|
||||
29,22,15,23,30,37,44,51, // 20,22,33,38,46,51,55,60,
|
||||
58,59,52,45,38,31,39,46, // 21,34,37,47,50,56,59,61,
|
||||
53,60,61,54,47,55,62,63 }; // 35,36,48,49,57,58,62,63
|
||||
|
||||
// static Huffman code tables from JPEG standard Annex K
|
||||
// - CodesPerBitsize tables define how many Huffman codes will have a certain bitsize (plus 1 because there nothing with zero bits),
|
||||
// e.g. DcLuminanceCodesPerBitsize[2] = 5 because there are 5 Huffman codes being 2+1=3 bits long
|
||||
// - Values tables are a list of values ordered by their Huffman code bitsize,
|
||||
// e.g. AcLuminanceValues => Huffman(0x01,0x02 and 0x03) will have 2 bits, Huffman(0x00) will have 3 bits, Huffman(0x04,0x11 and 0x05) will have 4 bits, ...
|
||||
|
||||
// Huffman definitions for first DC/AC tables (luminance / Y channel)
|
||||
const uint8_t DcLuminanceCodesPerBitsize[16] = { 0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0 }; // sum = 12
|
||||
const uint8_t DcLuminanceValues [12] = { 0,1,2,3,4,5,6,7,8,9,10,11 }; // => 12 codes
|
||||
const uint8_t AcLuminanceCodesPerBitsize[16] = { 0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,125 }; // sum = 162
|
||||
const uint8_t AcLuminanceValues [162] = // => 162 codes
|
||||
{ 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07,0x22,0x71,0x14,0x32,0x81,0x91,0xA1,0x08, // 16*10+2 symbols because
|
||||
0x23,0x42,0xB1,0xC1,0x15,0x52,0xD1,0xF0,0x24,0x33,0x62,0x72,0x82,0x09,0x0A,0x16,0x17,0x18,0x19,0x1A,0x25,0x26,0x27,0x28, // upper 4 bits can be 0..F
|
||||
0x29,0x2A,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58,0x59, // while lower 4 bits can be 1..A
|
||||
0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x83,0x84,0x85,0x86,0x87,0x88,0x89, // plus two special codes 0x00 and 0xF0
|
||||
0x8A,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4,0xB5,0xB6, // order of these symbols was determined empirically by JPEG committee
|
||||
0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,0xE1,0xE2,
|
||||
0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,0xEA,0xF1,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0xFA };
|
||||
// Huffman definitions for second DC/AC tables (chrominance / Cb and Cr channels)
|
||||
const uint8_t DcChrominanceCodesPerBitsize[16] = { 0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0 }; // sum = 12
|
||||
const uint8_t DcChrominanceValues [12] = { 0,1,2,3,4,5,6,7,8,9,10,11 }; // => 12 codes (identical to DcLuminanceValues)
|
||||
const uint8_t AcChrominanceCodesPerBitsize[16] = { 0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,119 }; // sum = 162
|
||||
const uint8_t AcChrominanceValues [162] = // => 162 codes
|
||||
{ 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71,0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, // same number of symbol, just different order
|
||||
0xA1,0xB1,0xC1,0x09,0x23,0x33,0x52,0xF0,0x15,0x62,0x72,0xD1,0x0A,0x16,0x24,0x34,0xE1,0x25,0xF1,0x17,0x18,0x19,0x1A,0x26, // (which is more efficient for AC coding)
|
||||
0x27,0x28,0x29,0x2A,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58,
|
||||
0x59,0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x82,0x83,0x84,0x85,0x86,0x87,
|
||||
0x88,0x89,0x8A,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4,
|
||||
0xB5,0xB6,0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,
|
||||
0xE2,0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,0xEA,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0xFA };
|
||||
const int16_t CodeWordLimit = 2048; // +/-2^11, maximum value after DCT
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// structs
|
||||
|
||||
// represent a single Huffman code
|
||||
struct BitCode
|
||||
{
|
||||
BitCode() = default; // undefined state, must be initialized at a later time
|
||||
BitCode(uint16_t code_, uint8_t numBits_)
|
||||
: code(code_), numBits(numBits_) {}
|
||||
uint16_t code; // JPEG's Huffman codes are limited to 16 bits
|
||||
uint8_t numBits; // number of valid bits
|
||||
};
|
||||
|
||||
// wrapper for bit output operations
|
||||
struct BitWriter
|
||||
{
|
||||
// user-supplied callback that writes/stores one byte
|
||||
TooJpeg::WRITE_ONE_BYTE output;
|
||||
// initialize writer
|
||||
explicit BitWriter(TooJpeg::WRITE_ONE_BYTE output_) : output(output_) {}
|
||||
|
||||
// store the most recently encoded bits that are not written yet
|
||||
struct BitBuffer
|
||||
{
|
||||
int32_t data = 0; // actually only at most 24 bits are used
|
||||
uint8_t numBits = 0; // number of valid bits (the right-most bits)
|
||||
} buffer;
|
||||
|
||||
// write Huffman bits stored in BitCode, keep excess bits in BitBuffer
|
||||
BitWriter& operator<<(const BitCode& data)
|
||||
{
|
||||
// append the new bits to those bits leftover from previous call(s)
|
||||
buffer.numBits += data.numBits;
|
||||
buffer.data <<= data.numBits;
|
||||
buffer.data |= data.code;
|
||||
|
||||
// write all "full" bytes
|
||||
while (buffer.numBits >= 8)
|
||||
{
|
||||
// extract highest 8 bits
|
||||
buffer.numBits -= 8;
|
||||
auto oneByte = uint8_t(buffer.data >> buffer.numBits);
|
||||
output(oneByte);
|
||||
|
||||
if (oneByte == 0xFF) // 0xFF has a special meaning for JPEGs (it's a block marker)
|
||||
output(0); // therefore pad a zero to indicate "nope, this one ain't a marker, it's just a coincidence"
|
||||
|
||||
// note: I don't clear those written bits, therefore buffer.bits may contain garbage in the high bits
|
||||
// if you really want to "clean up" (e.g. for debugging purposes) then uncomment the following line
|
||||
//buffer.bits &= (1 << buffer.numBits) - 1;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// write all non-yet-written bits, fill gaps with 1s (that's a strange JPEG thing)
|
||||
void flush()
|
||||
{
|
||||
// at most seven set bits needed to "fill" the last byte: 0x7F = binary 0111 1111
|
||||
*this << BitCode(0x7F, 7); // I should set buffer.numBits = 0 but since there are no single bits written after flush() I can safely ignore it
|
||||
}
|
||||
|
||||
// NOTE: all the following BitWriter functions IGNORE the BitBuffer and write straight to output !
|
||||
// write a single byte
|
||||
BitWriter& operator<<(uint8_t oneByte)
|
||||
{
|
||||
output(oneByte);
|
||||
return *this;
|
||||
}
|
||||
|
||||
// write an array of bytes
|
||||
template <typename T, int Size>
|
||||
BitWriter& operator<<(T (&manyBytes)[Size])
|
||||
{
|
||||
for (auto c : manyBytes)
|
||||
output(c);
|
||||
return *this;
|
||||
}
|
||||
|
||||
// start a new JFIF block
|
||||
void addMarker(uint8_t id, uint16_t length)
|
||||
{
|
||||
output(0xFF); output(id); // ID, always preceded by 0xFF
|
||||
output(uint8_t(length >> 8)); // length of the block (big-endian, includes the 2 length bytes as well)
|
||||
output(uint8_t(length & 0xFF));
|
||||
}
|
||||
};
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// functions / templates
|
||||
|
||||
// same as std::min()
|
||||
template <typename Number>
|
||||
Number minimum(Number value, Number maximum)
|
||||
{
|
||||
return value <= maximum ? value : maximum;
|
||||
}
|
||||
|
||||
// restrict a value to the interval [minimum, maximum]
|
||||
template <typename Number, typename Limit>
|
||||
Number clamp(Number value, Limit minValue, Limit maxValue)
|
||||
{
|
||||
if (value <= minValue) return minValue; // never smaller than the minimum
|
||||
if (value >= maxValue) return maxValue; // never bigger than the maximum
|
||||
return value; // value was inside interval, keep it
|
||||
}
|
||||
|
||||
// convert from RGB to YCbCr, constants are similar to ITU-R, see https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion
|
||||
float rgb2y (float r, float g, float b) { return +0.299f * r +0.587f * g +0.114f * b; }
|
||||
float rgb2cb(float r, float g, float b) { return -0.16874f * r -0.33126f * g +0.5f * b; }
|
||||
float rgb2cr(float r, float g, float b) { return +0.5f * r -0.41869f * g -0.08131f * b; }
|
||||
|
||||
// forward DCT computation "in one dimension" (fast AAN algorithm by Arai, Agui and Nakajima: "A fast DCT-SQ scheme for images")
|
||||
void DCT(float block[8*8], uint8_t stride) // stride must be 1 (=horizontal) or 8 (=vertical)
|
||||
{
|
||||
const auto SqrtHalfSqrt = 1.306562965f; // sqrt((2 + sqrt(2)) / 2) = cos(pi * 1 / 8) * sqrt(2)
|
||||
const auto InvSqrt = 0.707106781f; // 1 / sqrt(2) = cos(pi * 2 / 8)
|
||||
const auto HalfSqrtSqrt = 0.382683432f; // sqrt(2 - sqrt(2)) / 2 = cos(pi * 3 / 8)
|
||||
const auto InvSqrtSqrt = 0.541196100f; // 1 / sqrt(2 - sqrt(2)) = cos(pi * 3 / 8) * sqrt(2)
|
||||
|
||||
// modify in-place
|
||||
auto& block0 = block[0 ];
|
||||
auto& block1 = block[1 * stride];
|
||||
auto& block2 = block[2 * stride];
|
||||
auto& block3 = block[3 * stride];
|
||||
auto& block4 = block[4 * stride];
|
||||
auto& block5 = block[5 * stride];
|
||||
auto& block6 = block[6 * stride];
|
||||
auto& block7 = block[7 * stride];
|
||||
|
||||
// based on https://dev.w3.org/Amaya/libjpeg/jfdctflt.c , the original variable names can be found in my comments
|
||||
auto add07 = block0 + block7; auto sub07 = block0 - block7; // tmp0, tmp7
|
||||
auto add16 = block1 + block6; auto sub16 = block1 - block6; // tmp1, tmp6
|
||||
auto add25 = block2 + block5; auto sub25 = block2 - block5; // tmp2, tmp5
|
||||
auto add34 = block3 + block4; auto sub34 = block3 - block4; // tmp3, tmp4
|
||||
|
||||
auto add0347 = add07 + add34; auto sub07_34 = add07 - add34; // tmp10, tmp13 ("even part" / "phase 2")
|
||||
auto add1256 = add16 + add25; auto sub16_25 = add16 - add25; // tmp11, tmp12
|
||||
|
||||
block0 = add0347 + add1256; block4 = add0347 - add1256; // "phase 3"
|
||||
|
||||
auto z1 = (sub16_25 + sub07_34) * InvSqrt; // all temporary z-variables kept their original names
|
||||
block2 = sub07_34 + z1; block6 = sub07_34 - z1; // "phase 5"
|
||||
|
||||
auto sub23_45 = sub25 + sub34; // tmp10 ("odd part" / "phase 2")
|
||||
auto sub12_56 = sub16 + sub25; // tmp11
|
||||
auto sub01_67 = sub16 + sub07; // tmp12
|
||||
|
||||
auto z5 = (sub23_45 - sub01_67) * HalfSqrtSqrt;
|
||||
auto z2 = sub23_45 * InvSqrtSqrt + z5;
|
||||
auto z3 = sub12_56 * InvSqrt;
|
||||
auto z4 = sub01_67 * SqrtHalfSqrt + z5;
|
||||
auto z6 = sub07 + z3; // z11 ("phase 5")
|
||||
auto z7 = sub07 - z3; // z13
|
||||
block1 = z6 + z4; block7 = z6 - z4; // "phase 6"
|
||||
block5 = z7 + z2; block3 = z7 - z2;
|
||||
}
|
||||
|
||||
// run DCT, quantize and write Huffman bit codes
|
||||
int16_t encodeBlock(BitWriter& writer, float block[8][8], const float scaled[8*8], int16_t lastDC,
|
||||
const BitCode huffmanDC[256], const BitCode huffmanAC[256], const BitCode* codewords)
|
||||
{
|
||||
// "linearize" the 8x8 block, treat it as a flat array of 64 floats
|
||||
auto block64 = (float*) block;
|
||||
|
||||
// DCT: rows
|
||||
for (auto offset = 0; offset < 8; offset++)
|
||||
DCT(block64 + offset*8, 1);
|
||||
// DCT: columns
|
||||
for (auto offset = 0; offset < 8; offset++)
|
||||
DCT(block64 + offset*1, 8);
|
||||
|
||||
// scale
|
||||
for (auto i = 0; i < 8*8; i++)
|
||||
block64[i] *= scaled[i];
|
||||
|
||||
// encode DC (the first coefficient is the "average color" of the 8x8 block)
|
||||
auto DC = int(block64[0] + (block64[0] >= 0 ? +0.5f : -0.5f)); // C++11's nearbyint() achieves a similar effect
|
||||
|
||||
// quantize and zigzag the other 63 coefficients
|
||||
auto posNonZero = 0; // find last coefficient which is not zero (because trailing zeros are encoded differently)
|
||||
int16_t quantized[8*8];
|
||||
for (auto i = 1; i < 8*8; i++) // start at 1 because block64[0]=DC was already processed
|
||||
{
|
||||
auto value = block64[ZigZagInv[i]];
|
||||
// round to nearest integer
|
||||
quantized[i] = int(value + (value >= 0 ? +0.5f : -0.5f)); // C++11's nearbyint() achieves a similar effect
|
||||
// remember offset of last non-zero coefficient
|
||||
if (quantized[i] != 0)
|
||||
posNonZero = i;
|
||||
}
|
||||
|
||||
// same "average color" as previous block ?
|
||||
auto diff = DC - lastDC;
|
||||
if (diff == 0)
|
||||
writer << huffmanDC[0x00]; // yes, write a special short symbol
|
||||
else
|
||||
{
|
||||
auto bits = codewords[diff]; // nope, encode the difference to previous block's average color
|
||||
writer << huffmanDC[bits.numBits] << bits;
|
||||
}
|
||||
|
||||
// encode ACs (quantized[1..63])
|
||||
auto offset = 0; // upper 4 bits count the number of consecutive zeros
|
||||
for (auto i = 1; i <= posNonZero; i++) // quantized[0] was already written, skip all trailing zeros, too
|
||||
{
|
||||
// zeros are encoded in a special way
|
||||
while (quantized[i] == 0) // found another zero ?
|
||||
{
|
||||
offset += 0x10; // add 1 to the upper 4 bits
|
||||
// split into blocks of at most 16 consecutive zeros
|
||||
if (offset > 0xF0) // remember, the counter is in the upper 4 bits, 0xF = 15
|
||||
{
|
||||
writer << huffmanAC[0xF0]; // 0xF0 is a special code for "16 zeros"
|
||||
offset = 0;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
auto encoded = codewords[quantized[i]];
|
||||
// combine number of zeros with the number of bits of the next non-zero value
|
||||
writer << huffmanAC[offset + encoded.numBits] << encoded; // and the value itself
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
// send end-of-block code (0x00), only needed if there are trailing zeros
|
||||
if (posNonZero < 8*8 - 1) // = 63
|
||||
writer << huffmanAC[0x00];
|
||||
|
||||
return DC;
|
||||
}
|
||||
|
||||
// Jon's code includes the pre-generated Huffman codes
|
||||
// I don't like these "magic constants" and compute them on my own :-)
|
||||
void generateHuffmanTable(const uint8_t numCodes[16], const uint8_t* values, BitCode result[256])
|
||||
{
|
||||
// process all bitsizes 1 thru 16, no JPEG Huffman code is allowed to exceed 16 bits
|
||||
auto huffmanCode = 0;
|
||||
for (auto numBits = 1; numBits <= 16; numBits++)
|
||||
{
|
||||
// ... and each code of these bitsizes
|
||||
for (auto i = 0; i < numCodes[numBits - 1]; i++) // note: numCodes array starts at zero, but smallest bitsize is 1
|
||||
result[*values++] = BitCode(huffmanCode++, numBits);
|
||||
|
||||
// next Huffman code needs to be one bit wider
|
||||
huffmanCode <<= 1;
|
||||
}
|
||||
}
|
||||
|
||||
} // end of anonymous namespace
|
||||
|
||||
// -------------------- externally visible code --------------------
|
||||
|
||||
namespace TooJpeg
|
||||
{
|
||||
// the only exported function ...
|
||||
bool writeJpeg(WRITE_ONE_BYTE output, const void* pixels_, unsigned short width, unsigned short height,
|
||||
bool isRGB, unsigned char quality_, bool downsample, const char* comment)
|
||||
{
|
||||
// reject invalid pointers
|
||||
if (output == nullptr || pixels_ == nullptr)
|
||||
return false;
|
||||
// check image format
|
||||
if (width == 0 || height == 0)
|
||||
return false;
|
||||
|
||||
// number of components
|
||||
const auto numComponents = isRGB ? 3 : 1;
|
||||
// note: if there is just one component (=grayscale), then only luminance needs to be stored in the file
|
||||
// thus everything related to chrominance need not to be written to the JPEG
|
||||
// I still compute a few things, like quantization tables to avoid a complete code mess
|
||||
|
||||
// grayscale images can't be downsampled (because there are no Cb + Cr channels)
|
||||
if (!isRGB)
|
||||
downsample = false;
|
||||
|
||||
// wrapper for all output operations
|
||||
BitWriter bitWriter(output);
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// JFIF headers
|
||||
const uint8_t HeaderJfif[2+2+16] =
|
||||
{ 0xFF,0xD8, // SOI marker (start of image)
|
||||
0xFF,0xE0, // JFIF APP0 tag
|
||||
0,16, // length: 16 bytes (14 bytes payload + 2 bytes for this length field)
|
||||
'J','F','I','F',0, // JFIF identifier, zero-terminated
|
||||
1,1, // JFIF version 1.1
|
||||
0, // no density units specified
|
||||
0,1,0,1, // density: 1 pixel "per pixel" horizontally and vertically
|
||||
0,0 }; // no thumbnail (size 0 x 0)
|
||||
bitWriter << HeaderJfif;
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// comment (optional)
|
||||
if (comment != nullptr)
|
||||
{
|
||||
// look for zero terminator
|
||||
auto length = 0; // = strlen(comment);
|
||||
while (comment[length] != 0)
|
||||
length++;
|
||||
|
||||
// write COM marker
|
||||
bitWriter.addMarker(0xFE, 2+length); // block size is number of bytes (without zero terminator) + 2 bytes for this length field
|
||||
// ... and write the comment itself
|
||||
for (auto i = 0; i < length; i++)
|
||||
bitWriter << comment[i];
|
||||
}
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// adjust quantization tables to desired quality
|
||||
|
||||
// quality level must be in 1 ... 100
|
||||
auto quality = clamp<uint16_t>(quality_, 1, 100);
|
||||
// convert to an internal JPEG quality factor, formula taken from libjpeg
|
||||
quality = quality < 50 ? 5000 / quality : 200 - quality * 2;
|
||||
|
||||
uint8_t quantLuminance [8*8];
|
||||
uint8_t quantChrominance[8*8];
|
||||
for (auto i = 0; i < 8*8; i++)
|
||||
{
|
||||
int luminance = (DefaultQuantLuminance [ZigZagInv[i]] * quality + 50) / 100;
|
||||
int chrominance = (DefaultQuantChrominance[ZigZagInv[i]] * quality + 50) / 100;
|
||||
|
||||
// clamp to 1..255
|
||||
quantLuminance [i] = clamp(luminance, 1, 255);
|
||||
quantChrominance[i] = clamp(chrominance, 1, 255);
|
||||
}
|
||||
|
||||
// write quantization tables
|
||||
bitWriter.addMarker(0xDB, 2 + (isRGB ? 2 : 1) * (1 + 8*8)); // length: 65 bytes per table + 2 bytes for this length field
|
||||
// each table has 64 entries and is preceded by an ID byte
|
||||
|
||||
bitWriter << 0x00 << quantLuminance; // first quantization table
|
||||
if (isRGB)
|
||||
bitWriter << 0x01 << quantChrominance; // second quantization table, only relevant for color images
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// write image infos (SOF0 - start of frame)
|
||||
bitWriter.addMarker(0xC0, 2+6+3*numComponents); // length: 6 bytes general info + 3 per channel + 2 bytes for this length field
|
||||
|
||||
// 8 bits per channel
|
||||
bitWriter << 0x08
|
||||
// image dimensions (big-endian)
|
||||
<< (height >> 8) << (height & 0xFF)
|
||||
<< (width >> 8) << (width & 0xFF);
|
||||
|
||||
// sampling and quantization tables for each component
|
||||
bitWriter << numComponents; // 1 component (grayscale, Y only) or 3 components (Y,Cb,Cr)
|
||||
for (auto id = 1; id <= numComponents; id++)
|
||||
bitWriter << id // component ID (Y=1, Cb=2, Cr=3)
|
||||
// bitmasks for sampling: highest 4 bits: horizontal, lowest 4 bits: vertical
|
||||
<< (id == 1 && downsample ? 0x22 : 0x11) // 0x11 is default YCbCr 4:4:4 and 0x22 stands for YCbCr 4:2:0
|
||||
<< (id == 1 ? 0 : 1); // use quantization table 0 for Y, table 1 for Cb and Cr
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// Huffman tables
|
||||
// DHT marker - define Huffman tables
|
||||
bitWriter.addMarker(0xC4, isRGB ? (2+208+208) : (2+208));
|
||||
// 2 bytes for the length field, store chrominance only if needed
|
||||
// 1+16+12 for the DC luminance
|
||||
// 1+16+162 for the AC luminance (208 = 1+16+12 + 1+16+162)
|
||||
// 1+16+12 for the DC chrominance
|
||||
// 1+16+162 for the AC chrominance (208 = 1+16+12 + 1+16+162, same as above)
|
||||
|
||||
// store luminance's DC+AC Huffman table definitions
|
||||
bitWriter << 0x00 // highest 4 bits: 0 => DC, lowest 4 bits: 0 => Y (baseline)
|
||||
<< DcLuminanceCodesPerBitsize
|
||||
<< DcLuminanceValues;
|
||||
bitWriter << 0x10 // highest 4 bits: 1 => AC, lowest 4 bits: 0 => Y (baseline)
|
||||
<< AcLuminanceCodesPerBitsize
|
||||
<< AcLuminanceValues;
|
||||
|
||||
// compute actual Huffman code tables (see Jon's code for precalculated tables)
|
||||
BitCode huffmanLuminanceDC[256];
|
||||
BitCode huffmanLuminanceAC[256];
|
||||
generateHuffmanTable(DcLuminanceCodesPerBitsize, DcLuminanceValues, huffmanLuminanceDC);
|
||||
generateHuffmanTable(AcLuminanceCodesPerBitsize, AcLuminanceValues, huffmanLuminanceAC);
|
||||
|
||||
// chrominance is only relevant for color images
|
||||
BitCode huffmanChrominanceDC[256];
|
||||
BitCode huffmanChrominanceAC[256];
|
||||
if (isRGB)
|
||||
{
|
||||
// store luminance's DC+AC Huffman table definitions
|
||||
bitWriter << 0x01 // highest 4 bits: 0 => DC, lowest 4 bits: 1 => Cr,Cb (baseline)
|
||||
<< DcChrominanceCodesPerBitsize
|
||||
<< DcChrominanceValues;
|
||||
bitWriter << 0x11 // highest 4 bits: 1 => AC, lowest 4 bits: 1 => Cr,Cb (baseline)
|
||||
<< AcChrominanceCodesPerBitsize
|
||||
<< AcChrominanceValues;
|
||||
|
||||
// compute actual Huffman code tables (see Jon's code for precalculated tables)
|
||||
generateHuffmanTable(DcChrominanceCodesPerBitsize, DcChrominanceValues, huffmanChrominanceDC);
|
||||
generateHuffmanTable(AcChrominanceCodesPerBitsize, AcChrominanceValues, huffmanChrominanceAC);
|
||||
}
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// start of scan (there is only a single scan for baseline JPEGs)
|
||||
bitWriter.addMarker(0xDA, 2+1+2*numComponents+3); // 2 bytes for the length field, 1 byte for number of components,
|
||||
// then 2 bytes for each component and 3 bytes for spectral selection
|
||||
|
||||
// assign Huffman tables to each component
|
||||
bitWriter << numComponents;
|
||||
for (auto id = 1; id <= numComponents; id++)
|
||||
// highest 4 bits: DC Huffman table, lowest 4 bits: AC Huffman table
|
||||
bitWriter << id << (id == 1 ? 0x00 : 0x11); // Y: tables 0 for DC and AC; Cb + Cr: tables 1 for DC and AC
|
||||
|
||||
// constant values for our baseline JPEGs (which have a single sequential scan)
|
||||
static const uint8_t Spectral[3] = { 0, 63, 0 }; // spectral selection: must be from 0 to 63; successive approximation must be 0
|
||||
bitWriter << Spectral;
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// adjust quantization tables with AAN scaling factors to simplify DCT
|
||||
float scaledLuminance [8*8];
|
||||
float scaledChrominance[8*8];
|
||||
for (auto i = 0; i < 8*8; i++)
|
||||
{
|
||||
auto row = ZigZagInv[i] / 8; // same as ZigZagInv[i] >> 3
|
||||
auto column = ZigZagInv[i] % 8; // same as ZigZagInv[i] & 7
|
||||
|
||||
// scaling constants for AAN DCT algorithm: AanScaleFactors[0] = 1, AanScaleFactors[k=1..7] = cos(k*PI/16) * sqrt(2)
|
||||
static const float AanScaleFactors[8] = { 1, 1.387039845f, 1.306562965f, 1.175875602f, 1, 0.785694958f, 0.541196100f, 0.275899379f };
|
||||
auto factor = 1 / (AanScaleFactors[row] * AanScaleFactors[column] * 8);
|
||||
scaledLuminance [ZigZagInv[i]] = factor / quantLuminance [i];
|
||||
scaledChrominance[ZigZagInv[i]] = factor / quantChrominance[i];
|
||||
// if you really want JPEGs that are bitwise identical to Jon Olick's code then you need slightly different formulas (note: sqrt(8) = 2.828427125f)
|
||||
//static const float aasf[] = { 1.0f * 2.828427125f, 1.387039845f * 2.828427125f, 1.306562965f * 2.828427125f, 1.175875602f * 2.828427125f, 1.0f * 2.828427125f, 0.785694958f * 2.828427125f, 0.541196100f * 2.828427125f, 0.275899379f * 2.828427125f }; // line 240 of jo_jpeg.cpp
|
||||
//scaledLuminance [ZigZagInv[i]] = 1 / (quantLuminance [i] * aasf[row] * aasf[column]); // lines 266-267 of jo_jpeg.cpp
|
||||
//scaledChrominance[ZigZagInv[i]] = 1 / (quantChrominance[i] * aasf[row] * aasf[column]);
|
||||
}
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// precompute JPEG codewords for quantized DCT
|
||||
BitCode codewordsArray[2 * CodeWordLimit]; // note: quantized[i] is found at codewordsArray[quantized[i] + CodeWordLimit]
|
||||
BitCode* codewords = &codewordsArray[CodeWordLimit]; // allow negative indices, so quantized[i] is at codewords[quantized[i]]
|
||||
uint8_t numBits = 1; // each codeword has at least one bit (value == 0 is undefined)
|
||||
int32_t mask = 1; // mask is always 2^numBits - 1, initial value 2^1-1 = 2-1 = 1
|
||||
for (int16_t value = 1; value < CodeWordLimit; value++)
|
||||
{
|
||||
// numBits = position of highest set bit (ignoring the sign)
|
||||
// mask = (2^numBits) - 1
|
||||
if (value > mask) // one more bit ?
|
||||
{
|
||||
numBits++;
|
||||
mask = (mask << 1) | 1; // append a set bit
|
||||
}
|
||||
codewords[-value] = BitCode(mask - value, numBits); // note that I use a negative index => codewords[-value] = codewordsArray[CodeWordLimit value]
|
||||
codewords[+value] = BitCode( value, numBits);
|
||||
}
|
||||
|
||||
// just convert image data from void*
|
||||
auto pixels = (const uint8_t*)pixels_;
|
||||
|
||||
// the next two variables are frequently used when checking for image borders
|
||||
const auto maxWidth = width - 1; // "last row"
|
||||
const auto maxHeight = height - 1; // "bottom line"
|
||||
|
||||
// process MCUs (minimum codes units) => image is subdivided into a grid of 8x8 or 16x16 tiles
|
||||
const auto sampling = downsample ? 2 : 1; // 1x1 or 2x2 sampling
|
||||
const auto mcuSize = 8 * sampling;
|
||||
|
||||
// average color of the previous MCU
|
||||
int16_t lastYDC = 0, lastCbDC = 0, lastCrDC = 0;
|
||||
// convert from RGB to YCbCr
|
||||
float Y[8][8], Cb[8][8], Cr[8][8];
|
||||
|
||||
for (auto mcuY = 0; mcuY < height; mcuY += mcuSize) // each step is either 8 or 16 (=mcuSize)
|
||||
for (auto mcuX = 0; mcuX < width; mcuX += mcuSize)
|
||||
{
|
||||
// YCbCr 4:4:4 format: each MCU is a 8x8 block - the same applies to grayscale images, too
|
||||
// YCbCr 4:2:0 format: each MCU represents a 16x16 block, stored as 4x 8x8 Y-blocks plus 1x 8x8 Cb and 1x 8x8 Cr block)
|
||||
for (auto blockY = 0; blockY < mcuSize; blockY += 8) // iterate once (YCbCr444 and grayscale) or twice (YCbCr420)
|
||||
for (auto blockX = 0; blockX < mcuSize; blockX += 8)
|
||||
{
|
||||
// now we finally have an 8x8 block ...
|
||||
for (auto deltaY = 0; deltaY < 8; deltaY++)
|
||||
{
|
||||
auto column = minimum(mcuX + blockX , maxWidth); // must not exceed image borders, replicate last row/column if needed
|
||||
auto row = minimum(mcuY + blockY + deltaY, maxHeight);
|
||||
for (auto deltaX = 0; deltaX < 8; deltaX++)
|
||||
{
|
||||
// find actual pixel position within the current image
|
||||
auto pixelPos = row * int(width) + column; // the cast ensures that we don't run into multiplication overflows
|
||||
if (column < maxWidth)
|
||||
column++;
|
||||
|
||||
// grayscale images have solely a Y channel which can be easily derived from the input pixel by shifting it by 128
|
||||
if (!isRGB)
|
||||
{
|
||||
Y[deltaY][deltaX] = pixels[pixelPos] - 128.f;
|
||||
continue;
|
||||
}
|
||||
|
||||
// RGB: 3 bytes per pixel (whereas grayscale images have only 1 byte per pixel)
|
||||
auto r = pixels[3 * pixelPos ];
|
||||
auto g = pixels[3 * pixelPos + 1];
|
||||
auto b = pixels[3 * pixelPos + 2];
|
||||
|
||||
Y [deltaY][deltaX] = rgb2y (r, g, b) - 128; // again, the JPEG standard requires Y to be shifted by 128
|
||||
// YCbCr444 is easy - the more complex YCbCr420 has to be computed about 20 lines below in a second pass
|
||||
if (!downsample)
|
||||
{
|
||||
Cb[deltaY][deltaX] = rgb2cb(r, g, b); // standard RGB-to-YCbCr conversion
|
||||
Cr[deltaY][deltaX] = rgb2cr(r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// encode Y channel
|
||||
lastYDC = encodeBlock(bitWriter, Y, scaledLuminance, lastYDC, huffmanLuminanceDC, huffmanLuminanceAC, codewords);
|
||||
// Cb and Cr are encoded about 50 lines below
|
||||
}
|
||||
|
||||
// grayscale images don't need any Cb and Cr information
|
||||
if (!isRGB)
|
||||
continue;
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// the following lines are only relevant for YCbCr420:
|
||||
// average/downsample chrominance of four pixels while respecting the image borders
|
||||
if (downsample)
|
||||
for (short deltaY = 7; downsample && deltaY >= 0; deltaY--) // iterating loop in reverse increases cache read efficiency
|
||||
{
|
||||
auto row = minimum(mcuY + 2*deltaY, maxHeight); // each deltaX/Y step covers a 2x2 area
|
||||
auto column = mcuX; // column is updated inside next loop
|
||||
auto pixelPos = (row * int(width) + column) * 3; // numComponents = 3
|
||||
|
||||
// deltas (in bytes) to next row / column, must not exceed image borders
|
||||
auto rowStep = (row < maxHeight) ? 3 * int(width) : 0; // always numComponents*width except for bottom line
|
||||
auto columnStep = (column < maxWidth ) ? 3 : 0; // always numComponents except for rightmost pixel
|
||||
|
||||
for (short deltaX = 0; deltaX < 8; deltaX++)
|
||||
{
|
||||
// let's add all four samples (2x2 area)
|
||||
auto right = pixelPos + columnStep;
|
||||
auto down = pixelPos + rowStep;
|
||||
auto downRight = pixelPos + columnStep + rowStep;
|
||||
|
||||
// note: cast from 8 bits to >8 bits to avoid overflows when adding
|
||||
auto r = short(pixels[pixelPos ]) + pixels[right ] + pixels[down ] + pixels[downRight ];
|
||||
auto g = short(pixels[pixelPos + 1]) + pixels[right + 1] + pixels[down + 1] + pixels[downRight + 1];
|
||||
auto b = short(pixels[pixelPos + 2]) + pixels[right + 2] + pixels[down + 2] + pixels[downRight + 2];
|
||||
|
||||
// convert to Cb and Cr
|
||||
Cb[deltaY][deltaX] = rgb2cb(r, g, b) / 4; // I still have to divide r,g,b by 4 to get their average values
|
||||
Cr[deltaY][deltaX] = rgb2cr(r, g, b) / 4; // it's a bit faster if done AFTER CbCr conversion
|
||||
|
||||
// step forward to next 2x2 area
|
||||
pixelPos += 2*3; // 2 pixels => 6 bytes (2*numComponents)
|
||||
column += 2;
|
||||
|
||||
// reached right border ?
|
||||
if (column >= maxWidth)
|
||||
{
|
||||
columnStep = 0;
|
||||
pixelPos = ((row + 1) * int(width) - 1) * 3; // same as (row * width + maxWidth) * numComponents => current's row last pixel
|
||||
}
|
||||
}
|
||||
} // end of YCbCr420 code for Cb and Cr
|
||||
|
||||
// encode Cb and Cr
|
||||
lastCbDC = encodeBlock(bitWriter, Cb, scaledChrominance, lastCbDC, huffmanChrominanceDC, huffmanChrominanceAC, codewords);
|
||||
lastCrDC = encodeBlock(bitWriter, Cr, scaledChrominance, lastCrDC, huffmanChrominanceDC, huffmanChrominanceAC, codewords);
|
||||
}
|
||||
|
||||
bitWriter.flush(); // now image is completely encoded, write any bits still left in the buffer
|
||||
|
||||
// ///////////////////////////
|
||||
// EOI marker
|
||||
bitWriter << 0xFF << 0xD9; // this marker has no length, therefore I can't use addMarker()
|
||||
return true;
|
||||
} // writeJpeg()
|
||||
} // namespace TooJpeg
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
// //////////////////////////////////////////////////////////
|
||||
// toojpeg.h
|
||||
// written by Stephan Brumme, 2018-2019
|
||||
// see https://create.stephan-brumme.com/toojpeg/
|
||||
//
|
||||
|
||||
// This is a compact baseline JPEG/JFIF writer, written in C++ (but looks like C for the most part).
|
||||
// Its interface has only one function: writeJpeg() - and that's it !
|
||||
//
|
||||
// basic example:
|
||||
// => create an image with any content you like, e.g. 1024x768, RGB = 3 bytes per pixel
|
||||
// auto pixels = new unsigned char[1024*768*3];
|
||||
// => you need to define a callback that receives the compressed data byte-by-byte from my JPEG writer
|
||||
// void myOutput(unsigned char oneByte) { fputc(oneByte, myFileHandle); } // save byte to file
|
||||
// => let's go !
|
||||
// TooJpeg::writeJpeg(myOutput, mypixels, 1024, 768);
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace TooJpeg
|
||||
{
|
||||
// write one byte (to disk, memory, ...)
|
||||
typedef void (*WRITE_ONE_BYTE)(unsigned char);
|
||||
// this callback is called for every byte generated by the encoder and behaves similar to fputc
|
||||
// if you prefer stylish C++11 syntax then it can be a lambda, too:
|
||||
// auto myOutput = [](unsigned char oneByte) { fputc(oneByte, output); };
|
||||
|
||||
// output - callback that stores a single byte (writes to disk, memory, ...)
|
||||
// pixels - stored in RGB format or grayscale, stored from upper-left to lower-right
|
||||
// width,height - image size
|
||||
// isRGB - true if RGB format (3 bytes per pixel); false if grayscale (1 byte per pixel)
|
||||
// quality - between 1 (worst) and 100 (best)
|
||||
// downsample - if true then YCbCr 4:2:0 format is used (smaller size, minor quality loss) instead of 4:4:4, not relevant for grayscale
|
||||
// comment - optional JPEG comment (0/NULL if no comment), must not contain ASCII code 0xFF
|
||||
bool writeJpeg(WRITE_ONE_BYTE output, const void* pixels, unsigned short width, unsigned short height,
|
||||
bool isRGB = true, unsigned char quality = 90, bool downsample = false, const char* comment = nullptr);
|
||||
} // namespace TooJpeg
|
||||
|
||||
// My main inspiration was Jon Olick's Minimalistic JPEG writer
|
||||
// ( https://www.jonolick.com/code.html => direct link is https://www.jonolick.com/uploads/7/9/2/1/7921194/jo_jpeg.cpp ).
|
||||
// However, his code documentation is quite sparse - probably because it wasn't written from scratch and is (quote:) "based on a javascript jpeg writer",
|
||||
// most likely Andreas Ritter's code: https://github.com/eugeneware/jpeg-js/blob/master/lib/encoder.js
|
||||
//
|
||||
// Therefore I wrote the whole lib from scratch and tried hard to add tons of comments to my code, especially describing where all those magic numbers come from.
|
||||
// And I managed to remove the need for any external includes ...
|
||||
// yes, that's right: my library has no (!) includes at all, not even #include <stdlib.h>
|
||||
// Depending on your callback WRITE_ONE_BYTE, the library writes either to disk, or in-memory, or wherever you wish.
|
||||
// Moreover, no dynamic memory allocations are performed, just a few bytes on the stack.
|
||||
//
|
||||
// In contrast to Jon's code, compression can be significantly improved in many use cases:
|
||||
// a) grayscale JPEG images need just a single Y channel, no need to save the superfluous Cb + Cr channels
|
||||
// b) YCbCr 4:2:0 downsampling is often about 20% more efficient (=smaller) than the default YCbCr 4:4:4 with only little visual loss
|
||||
//
|
||||
// TooJpeg 1.2+ compresses about twice as fast as jo_jpeg (and about half as fast as libjpeg-turbo).
|
||||
// A few benchmark numbers can be found on my website https://create.stephan-brumme.com/toojpeg/#benchmark
|
||||
//
|
||||
// Last but not least you can optionally add a JPEG comment.
|
||||
//
|
||||
// Your C++ compiler needs to support a reasonable subset of C++11 (g++ 4.7 or Visual C++ 2013 are sufficient).
|
||||
// I haven't tested the code on big-endian systems or anything that smells like an apple.
|
||||
//
|
||||
// USE AT YOUR OWN RISK. Because you are a brave soul :-)
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "util/utils.h"
|
||||
#include "util/detour.h"
|
||||
#include "acioemu/handle.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "bi2x_hook.h"
|
||||
#include "trackball.h"
|
||||
|
||||
+171
-171
@@ -1,171 +1,171 @@
|
||||
#include "asio.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <cstring>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "gitadora.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace games::gitadora {
|
||||
|
||||
// Redirects the game's hard-coded "XONAR" ASIO driver lookup to the
|
||||
// driver name in ASIO_DRIVER by intercepting registry calls to
|
||||
// HKLM\SOFTWARE\ASIO. Sentinel HKEY values mark the redirected handles
|
||||
// so we can recognise them on subsequent reg* calls.
|
||||
|
||||
static const HKEY PARENT_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4001);
|
||||
static const HKEY DEVICE_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4002);
|
||||
static const char *FAKE_ASIO_DEVICE_NAME = "XONAR";
|
||||
|
||||
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
|
||||
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
|
||||
static decltype(RegOpenKeyA) *RegOpenKeyA_orig = nullptr;
|
||||
static decltype(RegOpenKeyExA) *RegOpenKeyExA_orig = nullptr;
|
||||
static decltype(RegQueryValueExA) *RegQueryValueExA_orig = nullptr;
|
||||
|
||||
static HKEY real_asio_reg_handle = nullptr;
|
||||
static HKEY real_asio_device_reg_handle = nullptr;
|
||||
|
||||
static LONG WINAPI RegOpenKeyExA_hook(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired,
|
||||
PHKEY phkResult)
|
||||
{
|
||||
if (ASIO_DRIVER.has_value() &&
|
||||
lpSubKey != nullptr &&
|
||||
phkResult != nullptr &&
|
||||
hKey == PARENT_ASIO_REG_HANDLE &&
|
||||
_stricmp(lpSubKey, FAKE_ASIO_DEVICE_NAME) == 0) {
|
||||
|
||||
*phkResult = DEVICE_ASIO_REG_HANDLE;
|
||||
|
||||
log_info("gitadora::asio", "replacing '{}' with '{}'", lpSubKey, ASIO_DRIVER.value());
|
||||
const auto result = RegOpenKeyExA_orig(
|
||||
real_asio_reg_handle,
|
||||
ASIO_DRIVER.value().c_str(),
|
||||
ulOptions,
|
||||
samDesired,
|
||||
&real_asio_device_reg_handle);
|
||||
|
||||
if (result != ERROR_SUCCESS) {
|
||||
log_warning(
|
||||
"gitadora::asio",
|
||||
"failed to open registry subkey '{}', error=0x{:x}",
|
||||
ASIO_DRIVER.value(), result);
|
||||
log_warning(
|
||||
"gitadora::asio",
|
||||
"due to improper ASIO setting, audio init will fail");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, phkResult);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
|
||||
if (ASIO_DRIVER.has_value() &&
|
||||
lpSubKey != nullptr &&
|
||||
phkResult != nullptr &&
|
||||
hKey == HKEY_LOCAL_MACHINE &&
|
||||
_stricmp(lpSubKey, "software\\asio") == 0)
|
||||
{
|
||||
*phkResult = PARENT_ASIO_REG_HANDLE;
|
||||
return RegOpenKeyA_orig(hKey, lpSubKey, &real_asio_reg_handle);
|
||||
}
|
||||
|
||||
return RegOpenKeyA_orig(hKey, lpSubKey, phkResult);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegEnumKeyA_hook(HKEY hKey, DWORD dwIndex, LPSTR lpName, DWORD cchName) {
|
||||
if (hKey == PARENT_ASIO_REG_HANDLE && ASIO_DRIVER.has_value()) {
|
||||
if (dwIndex == 0) {
|
||||
// forward to real handle just to verify the key exists; we
|
||||
// overwrite the name with our fake driver string regardless
|
||||
auto ret = RegEnumKeyA_orig(real_asio_reg_handle, dwIndex, lpName, cchName);
|
||||
if (ret == ERROR_SUCCESS && lpName != nullptr && cchName > 0) {
|
||||
log_info("gitadora::asio", "stubbing '{}' with '{}'", lpName, FAKE_ASIO_DEVICE_NAME);
|
||||
strncpy(lpName, FAKE_ASIO_DEVICE_NAME, cchName);
|
||||
lpName[cchName - 1] = '\0';
|
||||
}
|
||||
return ret;
|
||||
} else {
|
||||
return ERROR_NO_MORE_ITEMS;
|
||||
}
|
||||
}
|
||||
|
||||
return RegEnumKeyA_orig(hKey, dwIndex, lpName, cchName);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegQueryValueExA_hook(HKEY hKey, LPCSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType,
|
||||
LPBYTE lpData, LPDWORD lpcbData)
|
||||
{
|
||||
HKEY target = hKey;
|
||||
|
||||
if (ASIO_DRIVER.has_value() &&
|
||||
lpValueName != nullptr &&
|
||||
lpData != nullptr &&
|
||||
lpcbData != nullptr &&
|
||||
hKey == DEVICE_ASIO_REG_HANDLE) {
|
||||
if (_stricmp(lpValueName, "Description") == 0) {
|
||||
// engine may verify the driver name after open; ensure it still
|
||||
// sees something containing "XONAR" so the substring check passes
|
||||
const size_t len = strlen(FAKE_ASIO_DEVICE_NAME) + 1;
|
||||
if (*lpcbData < len) {
|
||||
*lpcbData = static_cast<DWORD>(len);
|
||||
return ERROR_MORE_DATA;
|
||||
}
|
||||
memcpy(lpData, FAKE_ASIO_DEVICE_NAME, len);
|
||||
*lpcbData = static_cast<DWORD>(len);
|
||||
if (lpType != nullptr) {
|
||||
*lpType = REG_SZ;
|
||||
}
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
// for everything else (CLSID etc.) defer to the real driver subkey
|
||||
target = real_asio_device_reg_handle;
|
||||
}
|
||||
|
||||
return RegQueryValueExA_orig(target, lpValueName, lpReserved, lpType, lpData, lpcbData);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegCloseKey_hook(HKEY hKey) {
|
||||
if (hKey == PARENT_ASIO_REG_HANDLE) {
|
||||
if (real_asio_reg_handle != nullptr) {
|
||||
RegCloseKey_orig(real_asio_reg_handle);
|
||||
real_asio_reg_handle = nullptr;
|
||||
}
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
if (hKey == DEVICE_ASIO_REG_HANDLE) {
|
||||
if (real_asio_device_reg_handle != nullptr) {
|
||||
RegCloseKey_orig(real_asio_device_reg_handle);
|
||||
real_asio_device_reg_handle = nullptr;
|
||||
}
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
return RegCloseKey_orig(hKey);
|
||||
}
|
||||
|
||||
void asio_hook_init() {
|
||||
if (!ASIO_DRIVER.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
log_info("gitadora::asio", "installing ASIO driver redirect: XONAR -> {}", ASIO_DRIVER.value());
|
||||
|
||||
RegCloseKey_orig = detour::iat_try(
|
||||
"RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE);
|
||||
RegEnumKeyA_orig = detour::iat_try(
|
||||
"RegEnumKeyA", RegEnumKeyA_hook, avs::game::DLL_INSTANCE);
|
||||
RegOpenKeyA_orig = detour::iat_try(
|
||||
"RegOpenKeyA", RegOpenKeyA_hook, avs::game::DLL_INSTANCE);
|
||||
RegOpenKeyExA_orig = detour::iat_try(
|
||||
"RegOpenKeyExA", RegOpenKeyExA_hook, avs::game::DLL_INSTANCE);
|
||||
RegQueryValueExA_orig = detour::iat_try(
|
||||
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
|
||||
}
|
||||
}
|
||||
#include "asio.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <cstring>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "gitadora.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace games::gitadora {
|
||||
|
||||
// Redirects the game's hard-coded "XONAR" ASIO driver lookup to the
|
||||
// driver name in ASIO_DRIVER by intercepting registry calls to
|
||||
// HKLM\SOFTWARE\ASIO. Sentinel HKEY values mark the redirected handles
|
||||
// so we can recognise them on subsequent reg* calls.
|
||||
|
||||
static const HKEY PARENT_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4001);
|
||||
static const HKEY DEVICE_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4002);
|
||||
static const char *FAKE_ASIO_DEVICE_NAME = "XONAR";
|
||||
|
||||
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
|
||||
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
|
||||
static decltype(RegOpenKeyA) *RegOpenKeyA_orig = nullptr;
|
||||
static decltype(RegOpenKeyExA) *RegOpenKeyExA_orig = nullptr;
|
||||
static decltype(RegQueryValueExA) *RegQueryValueExA_orig = nullptr;
|
||||
|
||||
static HKEY real_asio_reg_handle = nullptr;
|
||||
static HKEY real_asio_device_reg_handle = nullptr;
|
||||
|
||||
static LONG WINAPI RegOpenKeyExA_hook(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired,
|
||||
PHKEY phkResult)
|
||||
{
|
||||
if (ASIO_DRIVER.has_value() &&
|
||||
lpSubKey != nullptr &&
|
||||
phkResult != nullptr &&
|
||||
hKey == PARENT_ASIO_REG_HANDLE &&
|
||||
_stricmp(lpSubKey, FAKE_ASIO_DEVICE_NAME) == 0) {
|
||||
|
||||
*phkResult = DEVICE_ASIO_REG_HANDLE;
|
||||
|
||||
log_info("gitadora::asio", "replacing '{}' with '{}'", lpSubKey, ASIO_DRIVER.value());
|
||||
const auto result = RegOpenKeyExA_orig(
|
||||
real_asio_reg_handle,
|
||||
ASIO_DRIVER.value().c_str(),
|
||||
ulOptions,
|
||||
samDesired,
|
||||
&real_asio_device_reg_handle);
|
||||
|
||||
if (result != ERROR_SUCCESS) {
|
||||
log_warning(
|
||||
"gitadora::asio",
|
||||
"failed to open registry subkey '{}', error=0x{:x}",
|
||||
ASIO_DRIVER.value(), result);
|
||||
log_warning(
|
||||
"gitadora::asio",
|
||||
"due to improper ASIO setting, audio init will fail");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, phkResult);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
|
||||
if (ASIO_DRIVER.has_value() &&
|
||||
lpSubKey != nullptr &&
|
||||
phkResult != nullptr &&
|
||||
hKey == HKEY_LOCAL_MACHINE &&
|
||||
_stricmp(lpSubKey, "software\\asio") == 0)
|
||||
{
|
||||
*phkResult = PARENT_ASIO_REG_HANDLE;
|
||||
return RegOpenKeyA_orig(hKey, lpSubKey, &real_asio_reg_handle);
|
||||
}
|
||||
|
||||
return RegOpenKeyA_orig(hKey, lpSubKey, phkResult);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegEnumKeyA_hook(HKEY hKey, DWORD dwIndex, LPSTR lpName, DWORD cchName) {
|
||||
if (hKey == PARENT_ASIO_REG_HANDLE && ASIO_DRIVER.has_value()) {
|
||||
if (dwIndex == 0) {
|
||||
// forward to real handle just to verify the key exists; we
|
||||
// overwrite the name with our fake driver string regardless
|
||||
auto ret = RegEnumKeyA_orig(real_asio_reg_handle, dwIndex, lpName, cchName);
|
||||
if (ret == ERROR_SUCCESS && lpName != nullptr && cchName > 0) {
|
||||
log_info("gitadora::asio", "stubbing '{}' with '{}'", lpName, FAKE_ASIO_DEVICE_NAME);
|
||||
strncpy(lpName, FAKE_ASIO_DEVICE_NAME, cchName);
|
||||
lpName[cchName - 1] = '\0';
|
||||
}
|
||||
return ret;
|
||||
} else {
|
||||
return ERROR_NO_MORE_ITEMS;
|
||||
}
|
||||
}
|
||||
|
||||
return RegEnumKeyA_orig(hKey, dwIndex, lpName, cchName);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegQueryValueExA_hook(HKEY hKey, LPCSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType,
|
||||
LPBYTE lpData, LPDWORD lpcbData)
|
||||
{
|
||||
HKEY target = hKey;
|
||||
|
||||
if (ASIO_DRIVER.has_value() &&
|
||||
lpValueName != nullptr &&
|
||||
lpData != nullptr &&
|
||||
lpcbData != nullptr &&
|
||||
hKey == DEVICE_ASIO_REG_HANDLE) {
|
||||
if (_stricmp(lpValueName, "Description") == 0) {
|
||||
// engine may verify the driver name after open; ensure it still
|
||||
// sees something containing "XONAR" so the substring check passes
|
||||
const size_t len = strlen(FAKE_ASIO_DEVICE_NAME) + 1;
|
||||
if (*lpcbData < len) {
|
||||
*lpcbData = static_cast<DWORD>(len);
|
||||
return ERROR_MORE_DATA;
|
||||
}
|
||||
memcpy(lpData, FAKE_ASIO_DEVICE_NAME, len);
|
||||
*lpcbData = static_cast<DWORD>(len);
|
||||
if (lpType != nullptr) {
|
||||
*lpType = REG_SZ;
|
||||
}
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
// for everything else (CLSID etc.) defer to the real driver subkey
|
||||
target = real_asio_device_reg_handle;
|
||||
}
|
||||
|
||||
return RegQueryValueExA_orig(target, lpValueName, lpReserved, lpType, lpData, lpcbData);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegCloseKey_hook(HKEY hKey) {
|
||||
if (hKey == PARENT_ASIO_REG_HANDLE) {
|
||||
if (real_asio_reg_handle != nullptr) {
|
||||
RegCloseKey_orig(real_asio_reg_handle);
|
||||
real_asio_reg_handle = nullptr;
|
||||
}
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
if (hKey == DEVICE_ASIO_REG_HANDLE) {
|
||||
if (real_asio_device_reg_handle != nullptr) {
|
||||
RegCloseKey_orig(real_asio_device_reg_handle);
|
||||
real_asio_device_reg_handle = nullptr;
|
||||
}
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
return RegCloseKey_orig(hKey);
|
||||
}
|
||||
|
||||
void asio_hook_init() {
|
||||
if (!ASIO_DRIVER.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
log_info("gitadora::asio", "installing ASIO driver redirect: XONAR -> {}", ASIO_DRIVER.value());
|
||||
|
||||
RegCloseKey_orig = detour::iat_try(
|
||||
"RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE);
|
||||
RegEnumKeyA_orig = detour::iat_try(
|
||||
"RegEnumKeyA", RegEnumKeyA_hook, avs::game::DLL_INSTANCE);
|
||||
RegOpenKeyA_orig = detour::iat_try(
|
||||
"RegOpenKeyA", RegOpenKeyA_hook, avs::game::DLL_INSTANCE);
|
||||
RegOpenKeyExA_orig = detour::iat_try(
|
||||
"RegOpenKeyExA", RegOpenKeyExA_hook, avs::game::DLL_INSTANCE);
|
||||
RegQueryValueExA_orig = detour::iat_try(
|
||||
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
namespace games::gitadora {
|
||||
|
||||
// installs IAT registry hooks in gfdm.dll that redirect the game's
|
||||
// ASIO driver lookup (hard-coded "XONAR" substring) to a user-chosen
|
||||
// driver name read from games::gitadora::ASIO_DRIVER.
|
||||
//
|
||||
// safe to call unconditionally; if ASIO_DRIVER is unset the hooks
|
||||
// forward every call straight through to advapi32.
|
||||
void asio_hook_init();
|
||||
}
|
||||
#pragma once
|
||||
|
||||
namespace games::gitadora {
|
||||
|
||||
// installs IAT registry hooks in gfdm.dll that redirect the game's
|
||||
// ASIO driver lookup (hard-coded "XONAR" substring) to a user-chosen
|
||||
// driver name read from games::gitadora::ASIO_DRIVER.
|
||||
//
|
||||
// safe to call unconditionally; if ASIO_DRIVER is unset the hooks
|
||||
// forward every call straight through to advapi32.
|
||||
void asio_hook_init();
|
||||
}
|
||||
|
||||
@@ -2,17 +2,20 @@
|
||||
#include "asio.h"
|
||||
#include "handle.h"
|
||||
#include "bi2x_hook.h"
|
||||
#include <span>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "cfg/configurator.h"
|
||||
#include "cfg/screen_resize.h"
|
||||
|
||||
#include <ks.h>
|
||||
#include <ksmedia.h>
|
||||
|
||||
#include "cfg/configurator.h"
|
||||
#include "hooks/audio/audio.h"
|
||||
#include "hooks/audio/mme.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "util/cpuutils.h"
|
||||
#include "util/deferlog.h"
|
||||
#include "util/detour.h"
|
||||
@@ -28,14 +31,17 @@ namespace games::gitadora {
|
||||
|
||||
// settings
|
||||
bool TWOCHANNEL = false;
|
||||
bool DISABLE_FRAME_LIMITER = false;
|
||||
std::optional<unsigned int> CAB_TYPE = std::nullopt;
|
||||
bool P1_LEFTY = false;
|
||||
bool P2_LEFTY = false;
|
||||
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
||||
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
|
||||
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
|
||||
bool ARENA_TWO_HEAD_EXCLUSIVE = false;
|
||||
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
||||
bool ALLOW_REALTEK_AUDIO = false;
|
||||
bool NATIVE_TOUCH = false;
|
||||
|
||||
/*
|
||||
* Prevent GitaDora from creating folders on F drive
|
||||
@@ -61,6 +67,46 @@ namespace games::gitadora {
|
||||
|
||||
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
|
||||
|
||||
/*
|
||||
@@ -270,9 +316,21 @@ namespace games::gitadora {
|
||||
break;
|
||||
case 2:
|
||||
if (!GRAPHICS_WINDOWED) {
|
||||
log_fatal(
|
||||
if (D3D9_ADAPTER.has_value()) {
|
||||
log_fatal(
|
||||
"gitadora",
|
||||
"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: 2-window mode is not supported in fullscreen, choose 1 or 4");
|
||||
"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");
|
||||
GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS = true;
|
||||
@@ -298,10 +356,18 @@ namespace games::gitadora {
|
||||
static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr;
|
||||
static decltype(DisplayConfigGetDeviceInfo) *DisplayConfigGetDeviceInfo_orig = nullptr;
|
||||
|
||||
// cached primary real monitor: its path + source/target mode entries.
|
||||
// modeInfoIdx values on the path are renumbered to 0 / 1 so the cache is self-contained.
|
||||
// Cached real monitor paths and their source/target mode entries. modeInfoIdx values are
|
||||
// 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_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
|
||||
// by outputTechnology + connectorInstance:
|
||||
@@ -325,12 +391,31 @@ namespace games::gitadora {
|
||||
// adapter to a swap chain role via its DisplayConfig connector instance,
|
||||
// so the entries here must be listed in the same order the wrapper enumerates
|
||||
// them: id=1 -> adapter 1 -> left, id=2 -> adapter 2 -> right, id=3 -> adapter 3 -> small.
|
||||
static constexpr FakeMonitor FAKE_MONITORS[] = {
|
||||
// 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)
|
||||
{ 2, 1080, 1920, -200000, -200000, 1 }, // right (DP connector instance 1)
|
||||
{ 3, 800, 1280, -300000, -300000, 2 }, // small (DP connector instance 2, touch)
|
||||
};
|
||||
static constexpr UINT32 FAKE_MONITOR_COUNT = static_cast<UINT32>(std::size(FAKE_MONITORS));
|
||||
|
||||
// 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
|
||||
// two referenced modes (source + target). modeInfoIdx values are rewritten to
|
||||
@@ -368,13 +453,55 @@ namespace games::gitadora {
|
||||
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[1] = all_modes[primary->targetInfo.modeInfoIdx];
|
||||
real_primary_path = *primary;
|
||||
real_primary_path.sourceInfo.modeInfoIdx = 0;
|
||||
real_primary_path.targetInfo.modeInfoIdx = 1;
|
||||
|
||||
log_info("gitadora", "cache_primary_monitor_info: cached primary monitor");
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
static
|
||||
@@ -389,33 +516,36 @@ namespace games::gitadora {
|
||||
static std::once_flag populate_once;
|
||||
std::call_once(populate_once, cache_primary_monitor_info);
|
||||
|
||||
// always report exactly 1 real + N fake monitors
|
||||
*pNumPathArrayElements = 1 + FAKE_MONITOR_COUNT;
|
||||
*pNumModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
|
||||
const auto fake_monitors = get_fake_monitors();
|
||||
const auto fake_count = static_cast<UINT32>(fake_monitors.size());
|
||||
const UINT32 real_count = real_monitor_count();
|
||||
*pNumPathArrayElements = real_count + fake_count;
|
||||
*pNumModeInfoArrayElements = (real_count + fake_count) * 2;
|
||||
|
||||
log_info(
|
||||
"gitadora",
|
||||
"GetDisplayConfigBufferSizes: 1 real path + {} fake monitor(s)",
|
||||
FAKE_MONITOR_COUNT);
|
||||
"GetDisplayConfigBufferSizes: {} real path(s) + {} fake monitor(s)",
|
||||
real_count,
|
||||
fake_count);
|
||||
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
// write fake monitor i into the caller's path/mode arrays. layout (single-monitor
|
||||
// assumption): index 0 in both arrays holds the cached primary real monitor, so
|
||||
// fake i occupies path slot (1 + i) and mode slots (2 + i*2) / (2 + i*2 + 1).
|
||||
// Write a fake monitor after the real paths. The cache is packed as [source, target]
|
||||
// mode pairs, so each path consumes two mode entries.
|
||||
static void insert_fake_monitor(
|
||||
DISPLAYCONFIG_PATH_INFO *paths,
|
||||
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 = 2 + i * 2;
|
||||
const UINT32 src_idx = source_mode_index;
|
||||
const UINT32 tgt_idx = src_idx + 1;
|
||||
const LUID adapter_id { .LowPart = static_cast<DWORD>(-m.id), .HighPart = -m.id };
|
||||
const UINT32 uid = static_cast<UINT32>(-m.id);
|
||||
|
||||
paths[1 + i] = {
|
||||
paths[path_index] = {
|
||||
.sourceInfo = {
|
||||
.adapterId = adapter_id,
|
||||
.id = uid,
|
||||
@@ -473,21 +603,35 @@ namespace games::gitadora {
|
||||
DISPLAYCONFIG_MODE_INFO* modeInfoArray,
|
||||
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;
|
||||
modeInfoArray[0] = real_primary_modes[0];
|
||||
modeInfoArray[1] = real_primary_modes[1];
|
||||
*numPathArrayElements = 1 + FAKE_MONITOR_COUNT;
|
||||
*numModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
|
||||
if (is_two_head_exclusive() && real_small_path_available) {
|
||||
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) {
|
||||
*currentTopologyId = DISPLAYCONFIG_TOPOLOGY_EXTEND;
|
||||
}
|
||||
|
||||
log_misc("gitadora", "QueryDisplayConfig returning fake monitor paths and modes");
|
||||
|
||||
// append fake monitors after the real one
|
||||
for (UINT32 i = 0; i < FAKE_MONITOR_COUNT; i++) {
|
||||
insert_fake_monitor(pathArray, modeInfoArray, i);
|
||||
// Append fake monitors after the real path(s).
|
||||
for (UINT32 i = 0; i < fake_count; i++) {
|
||||
insert_fake_monitor(
|
||||
pathArray,
|
||||
modeInfoArray,
|
||||
fake_monitors[i],
|
||||
real_count + i,
|
||||
real_count * 2 + i * 2);
|
||||
}
|
||||
|
||||
return ERROR_SUCCESS;
|
||||
@@ -518,7 +662,8 @@ namespace games::gitadora {
|
||||
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
|
||||
const LONG fake_id = -id;
|
||||
UINT32 conn_inst = 0xff;
|
||||
for (const auto& f : FAKE_MONITORS) {
|
||||
const auto fake_monitors = get_fake_monitors();
|
||||
for (const auto &f : fake_monitors) {
|
||||
if (f.id == fake_id) {
|
||||
conn_inst = f.connector_instance;
|
||||
break;
|
||||
@@ -541,18 +686,27 @@ namespace games::gitadora {
|
||||
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& target = real_primary_path.targetInfo;
|
||||
if (target.id == targetName->header.id &&
|
||||
target.adapterId.HighPart == targetName->header.adapterId.HighPart &&
|
||||
target.adapterId.LowPart == targetName->header.adapterId.LowPart)
|
||||
{
|
||||
const auto target_matches = [&](const DISPLAYCONFIG_PATH_TARGET_INFO &target) {
|
||||
return target.id == targetName->header.id &&
|
||||
target.adapterId.HighPart == targetName->header.adapterId.HighPart &&
|
||||
target.adapterId.LowPart == targetName->header.adapterId.LowPart;
|
||||
};
|
||||
if (target_matches(real_primary_path.targetInfo)) {
|
||||
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI;
|
||||
targetName->connectorInstance = 0;
|
||||
log_info("gitadora",
|
||||
"overriding primary monitor (id={}) to pretend to be HDMI",
|
||||
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;
|
||||
}
|
||||
@@ -575,6 +729,12 @@ namespace games::gitadora {
|
||||
HMODULE system_module = libutils::try_module("libsystem.dll");
|
||||
|
||||
// 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(
|
||||
sharepj_module, "eam_network_detected_ip_change"));
|
||||
detour::inline_hook((void *) eam_network_settings_conflict, libutils::try_proc(
|
||||
@@ -650,31 +810,38 @@ namespace games::gitadora {
|
||||
hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true;
|
||||
}
|
||||
|
||||
// monitor/touch hooks (windowed or full screen)
|
||||
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||
// enable touch hook for subscreen overlay
|
||||
// touch injection drives mouse-as-touch and API touch for the subscreen,
|
||||
// no matter whether it is drawn by the overlay (single-window mode) or by
|
||||
// the dedicated SMALL window
|
||||
NATIVE_TOUCH = !wintouchemu::FORCE &&
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
if (!NATIVE_TOUCH && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||
// the legacy fallback can only feed the subscreen overlay
|
||||
wintouchemu::FORCE = true;
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
|
||||
}
|
||||
|
||||
#if !SPICE_XP
|
||||
|
||||
if (!GRAPHICS_WINDOWED) {
|
||||
// monitor hook: always pretend to have 1 primary real monitor + 3 fake monitors
|
||||
// (LEFT / RIGHT / SMALL) so the game accepts the arena cab topology
|
||||
GetDisplayConfigBufferSizes_orig =
|
||||
detour::iat_try("GetDisplayConfigBufferSizes",
|
||||
GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE);
|
||||
QueryDisplayConfig_orig =
|
||||
detour::iat_try("QueryDisplayConfig",
|
||||
QueryDisplayConfig_hook, avs::game::DLL_INSTANCE);
|
||||
DisplayConfigGetDeviceInfo_orig =
|
||||
detour::iat_try("DisplayConfigGetDeviceInfo",
|
||||
DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE);
|
||||
if (!GRAPHICS_WINDOWED &&
|
||||
(GRAPHICS_PREVENT_SECONDARY_WINDOWS || ARENA_TWO_HEAD_EXCLUSIVE)) {
|
||||
if (ARENA_TWO_HEAD_EXCLUSIVE) {
|
||||
log_info(
|
||||
"gitadora",
|
||||
"exposing physical MAIN/SMALL and virtual LEFT/RIGHT");
|
||||
}
|
||||
#endif
|
||||
|
||||
GetDisplayConfigBufferSizes_orig =
|
||||
detour::iat_try("GetDisplayConfigBufferSizes",
|
||||
GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE);
|
||||
QueryDisplayConfig_orig =
|
||||
detour::iat_try("QueryDisplayConfig",
|
||||
QueryDisplayConfig_hook, avs::game::DLL_INSTANCE);
|
||||
DisplayConfigGetDeviceInfo_orig =
|
||||
detour::iat_try("DisplayConfigGetDeviceInfo",
|
||||
DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// window patch
|
||||
|
||||
@@ -13,14 +13,21 @@ namespace games::gitadora {
|
||||
|
||||
// settings
|
||||
extern bool TWOCHANNEL;
|
||||
extern bool DISABLE_FRAME_LIMITER;
|
||||
extern std::optional<unsigned int> CAB_TYPE;
|
||||
extern bool P1_LEFTY;
|
||||
extern bool P2_LEFTY;
|
||||
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
||||
extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
|
||||
extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
|
||||
extern bool ARENA_TWO_HEAD_EXCLUSIVE;
|
||||
extern std::optional<std::string> ASIO_DRIVER;
|
||||
extern bool ALLOW_REALTEK_AUDIO;
|
||||
extern bool NATIVE_TOUCH;
|
||||
|
||||
// arena SMALL subscreen (touch panel) resolution
|
||||
static constexpr int ARENA_SUBSCREEN_WIDTH = 800;
|
||||
static constexpr int ARENA_SUBSCREEN_HEIGHT = 1280;
|
||||
|
||||
class GitaDoraGame : public games::Game {
|
||||
public:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#if SPICE64
|
||||
|
||||
#include <cstdint>
|
||||
#include "api/client.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
@@ -443,10 +444,12 @@ namespace games::iidx {
|
||||
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g);
|
||||
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
|
||||
|
||||
for (unsigned int i = 0; i < data_size; ++i) {
|
||||
map.data[i].r = data[i * 3];
|
||||
map.data[i].g = data[i * 3 + 1];
|
||||
map.data[i].b = data[i * 3 + 2];
|
||||
if (api::has_clients()) {
|
||||
for (size_t i = 0; i < data_size; ++i) {
|
||||
map.data[i].r = data[i * 3];
|
||||
map.data[i].g = data[i * 3 + 1];
|
||||
map.data[i].b = data[i * 3 + 2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -534,7 +534,7 @@ namespace games::iidx {
|
||||
if (mediaTypePointer && mediaTypePointer->IsString()) {
|
||||
std::string mediaType = mediaTypePointer->GetString();
|
||||
if (mediaType.length() > 0) {
|
||||
camera->m_selectedMediaTypeDescription = mediaType;
|
||||
camera->SetSelectedMediaTypeDescription(mediaType);
|
||||
camera->m_useAutoMediaType = false;
|
||||
} else {
|
||||
camera->m_useAutoMediaType = true;
|
||||
@@ -547,7 +547,7 @@ namespace games::iidx {
|
||||
std::string drawModeString = drawModePointer->GetString();
|
||||
for (int j = 0; j < DRAW_MODE_SIZE; j++) {
|
||||
if (DRAW_MODE_LABELS[j].compare(drawModeString) == 0) {
|
||||
camera->m_drawMode = (LocalCameraDrawMode) j;
|
||||
camera->m_drawMode.store((LocalCameraDrawMode) j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -556,12 +556,12 @@ namespace games::iidx {
|
||||
// Flip
|
||||
auto flipHorizontalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipHorizontal").Get(doc);
|
||||
if (flipHorizontalPointer && flipHorizontalPointer->IsBool()) {
|
||||
camera->m_flipHorizontal = flipHorizontalPointer->GetBool();
|
||||
camera->m_flipHorizontal.store(flipHorizontalPointer->GetBool());
|
||||
}
|
||||
|
||||
auto flipVerticalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipVertical").Get(doc);
|
||||
if (flipVerticalPointer && flipVerticalPointer->IsBool()) {
|
||||
camera->m_flipVertical = flipVerticalPointer->GetBool();
|
||||
camera->m_flipVertical.store(flipVerticalPointer->GetBool());
|
||||
}
|
||||
|
||||
// Allow manual control
|
||||
@@ -625,15 +625,17 @@ namespace games::iidx {
|
||||
if (camera->m_useAutoMediaType) {
|
||||
rapidjson::Pointer(root + "MediaType").Set(doc, "");
|
||||
} else {
|
||||
rapidjson::Pointer(root + "MediaType").Set(doc, camera->m_selectedMediaTypeDescription);
|
||||
const auto mediaTypeDescription = camera->GetSelectedMediaTypeDescription();
|
||||
rapidjson::Pointer(root + "MediaType").Set(doc, mediaTypeDescription);
|
||||
}
|
||||
|
||||
// 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
|
||||
rapidjson::Pointer(root + "FlipHorizontal").Set(doc, camera->m_flipHorizontal);
|
||||
rapidjson::Pointer(root + "FlipVertical").Set(doc, camera->m_flipVertical);
|
||||
rapidjson::Pointer(root + "FlipHorizontal").Set(doc, camera->m_flipHorizontal.load());
|
||||
rapidjson::Pointer(root + "FlipVertical").Set(doc, camera->m_flipVertical.load());
|
||||
|
||||
// Manual control
|
||||
rapidjson::Pointer(root + "AllowManualControl").Set(doc, camera->m_allowManualControl);
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
#include "hooks/sleephook.h"
|
||||
#include "launcher/options.h"
|
||||
#include "touch/touch.h"
|
||||
#include "misc/nativetouchhook.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/deferlog.h"
|
||||
#include "util/fileutils.h"
|
||||
@@ -41,6 +41,7 @@
|
||||
#include "bi2x_hook.h"
|
||||
#include "ezusb.h"
|
||||
#include "io.h"
|
||||
#include "poke.h"
|
||||
|
||||
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
|
||||
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
|
||||
@@ -57,13 +58,14 @@ namespace games::iidx {
|
||||
|
||||
// settings
|
||||
bool FLIP_CAMS = false;
|
||||
std::optional<bool> DISABLE_CAMS;
|
||||
cab_camera_access_mode CAB_CAMERA_ACCESS = cab_camera_access_mode::automatic;
|
||||
bool TDJ_CAMERA = false;
|
||||
bool TDJ_CAMERA_PREFER_16_9 = true;
|
||||
bool TDJ_MODE = false;
|
||||
bool FORCE_720P = 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_IN_EFFECT = std::nullopt;
|
||||
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
||||
@@ -277,6 +279,26 @@ namespace games::iidx {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// best-effort read of a TDJ ROM file to determine if the game is running in TDJ mode
|
||||
//
|
||||
// these paths are not emulated - they hit whatever is actually mounted at that drive letter.
|
||||
// an empty optical or removable drive raises the modal "insert a disk" error (the launcher
|
||||
// clears SEM_FAILCRITICALERRORS process-wide) and a downed network drive stalls on redirector
|
||||
// timeouts, so only probe what a TDJ cabinet would actually be laid out on.
|
||||
// drive_path must be absolute and start with a drive letter.
|
||||
static bool tdj_rom_matches(const char *drive_path, const char *expected) {
|
||||
const wchar_t root[] = { (wchar_t) drive_path[0], L':', L'\\', L'\0' };
|
||||
const auto drive_type = GetDriveTypeW(root);
|
||||
|
||||
if (drive_type != DRIVE_FIXED && drive_type != DRIVE_RAMDISK) {
|
||||
log_misc("iidx", "not probing '{}' for TDJ, not a local disk (drive type {})",
|
||||
drive_path, drive_type);
|
||||
return false;
|
||||
}
|
||||
|
||||
return fileutils::text_read(drive_path) == expected;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
IIDXGame::IIDXGame() : Game("Beatmania IIDX") {
|
||||
@@ -337,8 +359,10 @@ namespace games::iidx {
|
||||
HAS_LIBAIO = true;
|
||||
|
||||
// check TDJ mode
|
||||
TDJ_MODE |= fileutils::text_read("C:\\000rom.txt") == "TDJ-JA";
|
||||
TDJ_MODE |= fileutils::text_read("D:\\001rom.txt") == "TDJ";
|
||||
if (!TDJ_MODE) {
|
||||
TDJ_MODE = tdj_rom_matches("C:\\000rom.txt", "TDJ-JA")
|
||||
|| tdj_rom_matches("D:\\001rom.txt", "TDJ");
|
||||
}
|
||||
|
||||
// force TDJ mode
|
||||
if (TDJ_MODE) {
|
||||
@@ -350,12 +374,15 @@ namespace games::iidx {
|
||||
// need to hook `avs2-core.dll` so AVS win32fs operations go through rom hook
|
||||
devicehook_init(avs::core::DLL_INSTANCE);
|
||||
|
||||
if (NATIVE_TOUCH) {
|
||||
nativetouchhook::hook(avs::game::DLL_INSTANCE);
|
||||
} else {
|
||||
NATIVE_TOUCH = !wintouchemu::FORCE &&
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
if (!NATIVE_TOUCH) {
|
||||
wintouchemu::FORCE = 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,10 +446,7 @@ namespace games::iidx {
|
||||
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
|
||||
|
||||
// check if cam hook should be enabled
|
||||
if (!DISABLE_CAMS.has_value()) {
|
||||
log_fatal("iidx", "assertion failure - DISABLE_CAMS not set during attach");
|
||||
}
|
||||
if (!DISABLE_CAMS.value()) {
|
||||
if (CAB_CAMERA_ACCESS == cab_camera_access_mode::legacy) {
|
||||
init_legacy_camera_hook(FLIP_CAMS);
|
||||
}
|
||||
|
||||
@@ -449,19 +473,10 @@ namespace games::iidx {
|
||||
SetEnvironmentVariable("SCREEN_MODE", SCREEN_MODE.value().c_str());
|
||||
}
|
||||
|
||||
// check for cab camera access for the second time (first time was in launcher.cpp)
|
||||
// this time, we are inside -iidx module hook, which means the user is likely NOT on a cab
|
||||
// therefore, start with cams OFF by default, and allow user to forcibly override to ON
|
||||
if (!games::iidx::DISABLE_CAMS.has_value()) {
|
||||
games::iidx::DISABLE_CAMS = true;
|
||||
if (options->at(launcher::Options::IIDXCabCamAccess).is_active() &&
|
||||
options->at(launcher::Options::IIDXCabCamAccess).value_text() == "on") {
|
||||
games::iidx::DISABLE_CAMS = false;
|
||||
}
|
||||
if (games::iidx::DISABLE_CAMS.value()) {
|
||||
log_misc("iidx", "CONNECT_CAMERA env var set to 0");
|
||||
SetEnvironmentVariable("CONNECT_CAMERA", "0");
|
||||
}
|
||||
// auto with iidx module means turn off camera (non-cab use)
|
||||
if (CAB_CAMERA_ACCESS == cab_camera_access_mode::automatic) {
|
||||
log_misc("iidx", "CONNECT_CAMERA env var set to 0");
|
||||
SetEnvironmentVariable("CONNECT_CAMERA", "0");
|
||||
}
|
||||
|
||||
// windowed subscreen, enabled by default, unless turned off by user
|
||||
@@ -533,6 +548,12 @@ namespace games::iidx {
|
||||
}
|
||||
}
|
||||
|
||||
void IIDXGame::post_attach() {
|
||||
if (ENABLE_POKE) {
|
||||
poke::enable();
|
||||
}
|
||||
}
|
||||
|
||||
void IIDXGame::detach() {
|
||||
Game::detach();
|
||||
|
||||
|
||||
@@ -11,6 +11,13 @@
|
||||
|
||||
namespace games::iidx {
|
||||
|
||||
enum class cab_camera_access_mode {
|
||||
automatic,
|
||||
off,
|
||||
on,
|
||||
legacy,
|
||||
};
|
||||
|
||||
enum class iidx_aio_emulation_state {
|
||||
unknown,
|
||||
bi2a_com2,
|
||||
@@ -20,7 +27,7 @@ namespace games::iidx {
|
||||
// settings
|
||||
|
||||
extern bool FLIP_CAMS;
|
||||
extern std::optional<bool> DISABLE_CAMS;
|
||||
extern cab_camera_access_mode CAB_CAMERA_ACCESS;
|
||||
extern bool TDJ_CAMERA;
|
||||
extern bool TDJ_CAMERA_PREFER_16_9;
|
||||
extern std::optional<std::string> TDJ_CAMERA_OVERRIDE;
|
||||
@@ -29,6 +36,7 @@ namespace games::iidx {
|
||||
extern bool FORCE_720P;
|
||||
extern bool DISABLE_ESPEC_IO;
|
||||
extern bool NATIVE_TOUCH;
|
||||
extern bool ENABLE_POKE;
|
||||
extern std::optional<std::string> SOUND_OUTPUT_DEVICE;
|
||||
extern std::optional<std::string> ASIO_DRIVER;
|
||||
extern uint8_t DIGITAL_TT_SENS;
|
||||
@@ -53,6 +61,7 @@ namespace games::iidx {
|
||||
|
||||
virtual void attach() override;
|
||||
virtual void pre_attach() override;
|
||||
virtual void post_attach() override;
|
||||
virtual void detach() override;
|
||||
|
||||
private:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
|
||||
#if SPICE64 && !SPICE_XP
|
||||
|
||||
#include <atomic>
|
||||
#include <d3d9.h>
|
||||
#include <dxva2api.h>
|
||||
#include <mutex>
|
||||
@@ -66,6 +67,8 @@ extern std::string CAMERA_CONTROL_LABELS[];
|
||||
extern std::string DRAW_MODE_LABELS[];
|
||||
|
||||
namespace games::iidx {
|
||||
class IIDXCameraSourceReaderCallback;
|
||||
|
||||
namespace Camera {
|
||||
struct PlayVideoCamera {
|
||||
IDirect3DTexture9** d3d9_texture(const uintptr_t offset) {
|
||||
@@ -87,7 +90,7 @@ namespace games::iidx {
|
||||
|
||||
class IIDXLocalCamera {
|
||||
protected:
|
||||
virtual ~IIDXLocalCamera() {};
|
||||
virtual ~IIDXLocalCamera();
|
||||
|
||||
LONG m_nRefCount;
|
||||
CRITICAL_SECTION m_critsec;
|
||||
@@ -101,9 +104,14 @@ namespace games::iidx {
|
||||
// For reading frames from Camera
|
||||
IMFMediaSource *m_pSource = 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
|
||||
double m_frameRate = 0;
|
||||
LONG m_cameraWidth;
|
||||
LONG m_cameraHeight;
|
||||
|
||||
@@ -129,6 +137,12 @@ namespace games::iidx {
|
||||
LPDIRECT3DTEXTURE9 m_conversionTexture = 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)
|
||||
LPDIRECT3DTEXTURE9 m_transformTexture = nullptr;
|
||||
IDirect3DSurface9 *m_pTransformSurf = nullptr;
|
||||
@@ -153,21 +167,19 @@ namespace games::iidx {
|
||||
BOOL m_initialized = false;
|
||||
|
||||
// True if all the setup steps succeeded
|
||||
BOOL m_active = false;
|
||||
std::atomic_bool m_active = false;
|
||||
|
||||
// Media type select
|
||||
std::vector<MediaTypeInfo> m_mediaTypeInfos = {};
|
||||
int m_selectedMediaTypeIndex = 0;
|
||||
bool m_useAutoMediaType = true;
|
||||
IMFMediaType *m_pAutoMediaType = nullptr;
|
||||
std::string m_selectedMediaTypeDescription = "";
|
||||
bool m_allowManualControl = false;
|
||||
|
||||
LocalCameraDrawMode m_drawMode = DrawModeCrop4_3;
|
||||
std::atomic<LocalCameraDrawMode> m_drawMode = DrawModeCrop4_3;
|
||||
|
||||
// Render processing
|
||||
bool m_flipHorizontal = false;
|
||||
bool m_flipVertical = false;
|
||||
std::atomic_bool m_flipHorizontal = false;
|
||||
std::atomic_bool m_flipVertical = false;
|
||||
|
||||
IIDXLocalCamera(
|
||||
std::string name,
|
||||
@@ -184,11 +196,14 @@ namespace games::iidx {
|
||||
HRESULT GetCameraControlProp(int index, CameraControlProp *pProp);
|
||||
HRESULT SetCameraControlProp(int index, long value, long flags);
|
||||
HRESULT ResetCameraControlProps();
|
||||
HRESULT FlushDrawCommands();
|
||||
std::string GetName();
|
||||
std::string GetFriendlyName();
|
||||
std::string GetSymLink();
|
||||
int GetSelectedMediaTypeIndex();
|
||||
std::string GetSelectedMediaTypeDescription();
|
||||
void SetSelectedMediaTypeDescription(const std::string &description);
|
||||
HRESULT ChangeMediaType(IMFMediaType *pType);
|
||||
void RequestMediaType(IMFMediaType *pType);
|
||||
HRESULT StartCapture();
|
||||
void UpdateDrawRect();
|
||||
|
||||
@@ -197,12 +212,22 @@ namespace games::iidx {
|
||||
void CreateThread();
|
||||
MediaTypeInfo GetMediaTypeInfo(IMFMediaType *pType);
|
||||
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 EnsureTransformTextures();
|
||||
HRESULT InitCameraControl();
|
||||
void SetSelectedMediaType(int index, const std::string &description);
|
||||
bool HasPendingMediaType();
|
||||
HRESULT ApplyPendingMediaType();
|
||||
HRESULT UploadDecodedSample(IMFMediaBuffer *pSrcBuffer);
|
||||
HRESULT DrawSample(IMFMediaBuffer *pSrcBuffer);
|
||||
HRESULT ReadSample();
|
||||
LPDIRECT3DTEXTURE9 Render();
|
||||
HRESULT Render();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,116 +1,132 @@
|
||||
#include "mf_wrappers.h"
|
||||
#include "util/libutils.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace games::iidx {
|
||||
|
||||
static bool INITIALIZED = false;
|
||||
|
||||
static HMODULE mf_dll = nullptr;
|
||||
static HMODULE mfreadwrite_dll = nullptr;
|
||||
static HMODULE mfplat_dll = nullptr;
|
||||
|
||||
typedef HRESULT (__stdcall * MFCreateAttributes_t)(
|
||||
_Out_ IMFAttributes** ppMFAttributes,
|
||||
_In_ UINT32 cInitialSize
|
||||
);
|
||||
|
||||
typedef HRESULT (__stdcall * MFEnumDeviceSources_t)(
|
||||
_In_ IMFAttributes* pAttributes,
|
||||
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
||||
_Out_ UINT32* pcSourceActivate
|
||||
);
|
||||
|
||||
typedef HRESULT (__stdcall * MFCreateSourceReaderFromMediaSource_t)(
|
||||
_In_ IMFMediaSource *pMediaSource,
|
||||
_In_opt_ IMFAttributes *pAttributes,
|
||||
_Out_ IMFSourceReader **ppSourceReader
|
||||
);
|
||||
|
||||
typedef HRESULT (__stdcall * MFGetService_t)(
|
||||
IUnknown* punkObject,
|
||||
REFGUID guidService,
|
||||
REFIID riid,
|
||||
_Outptr_ LPVOID* ppvObject
|
||||
);
|
||||
|
||||
static MFCreateAttributes_t MFCreateAttributes = nullptr;
|
||||
static MFEnumDeviceSources_t MFEnumDeviceSources = nullptr;
|
||||
static MFCreateSourceReaderFromMediaSource_t MFCreateSourceReaderFromMediaSource = nullptr;
|
||||
static MFGetService_t MFGetService = nullptr;
|
||||
|
||||
void init_mf_library() {
|
||||
|
||||
// why was all of this needed?
|
||||
//
|
||||
// when iidx camhook was initially implemented, we linked to mf.lib, mfreadwrite.lib, and mfplat.lib
|
||||
// this made Unity-based really unhappy, causing them to skip over the logic that loads mf library
|
||||
// causing videos to not play ("Initializing Microsoft Media Foundation failed." in the cmd prompt)
|
||||
//
|
||||
// as a result, the static linking to mf libs were removed, and we are now doing the mess that is this file
|
||||
|
||||
if (INITIALIZED) {
|
||||
return;
|
||||
}
|
||||
INITIALIZED = true;
|
||||
|
||||
log_misc("mf_wrappers", "creating delay-loaded wrappers for MF routines - BEGIN");
|
||||
|
||||
mf_dll = libutils::load_library("mf.dll", true);
|
||||
mfreadwrite_dll = libutils::load_library("mfreadwrite.dll", true);
|
||||
mfplat_dll = libutils::load_library("mfplat.dll", true);
|
||||
|
||||
MFCreateAttributes = (MFCreateAttributes_t)
|
||||
libutils::get_proc(mfplat_dll, "MFCreateAttributes");
|
||||
if (!MFCreateAttributes) {
|
||||
log_fatal("mf_wrappers", "MFCreateAttributes failed to hook");
|
||||
}
|
||||
|
||||
MFEnumDeviceSources = (MFEnumDeviceSources_t)
|
||||
libutils::get_proc(mf_dll, "MFEnumDeviceSources");
|
||||
if (!MFEnumDeviceSources) {
|
||||
log_fatal("mf_wrappers", "MFEnumDeviceSources failed to hook");
|
||||
}
|
||||
|
||||
MFCreateSourceReaderFromMediaSource = (MFCreateSourceReaderFromMediaSource_t)
|
||||
libutils::get_proc(mfreadwrite_dll, "MFCreateSourceReaderFromMediaSource");
|
||||
if (!MFCreateSourceReaderFromMediaSource) {
|
||||
log_fatal("mf_wrappers", "MFCreateSourceReaderFromMediaSource failed to hook");
|
||||
}
|
||||
|
||||
MFGetService = (MFGetService_t)libutils::get_proc(mf_dll, "MFGetService");
|
||||
if (!MFGetService) {
|
||||
log_fatal("mf_wrappers", "MFGetService failed to hook");
|
||||
}
|
||||
|
||||
log_misc("mf_wrappers", "creating delay-loaded wrappers for MF routines - DONE");
|
||||
}
|
||||
|
||||
HRESULT WrappedMFCreateAttributes (
|
||||
_Out_ IMFAttributes** ppMFAttributes,
|
||||
_In_ UINT32 cInitialSize) {
|
||||
return MFCreateAttributes(ppMFAttributes, cInitialSize);
|
||||
}
|
||||
|
||||
HRESULT WrappedMFEnumDeviceSources (
|
||||
_In_ IMFAttributes* pAttributes,
|
||||
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
||||
_Out_ UINT32* pcSourceActivate) {
|
||||
return MFEnumDeviceSources(pAttributes, pppSourceActivate, pcSourceActivate);
|
||||
}
|
||||
|
||||
HRESULT WrappedMFCreateSourceReaderFromMediaSource (
|
||||
_In_ IMFMediaSource *pMediaSource,
|
||||
_In_opt_ IMFAttributes *pAttributes,
|
||||
_Out_ IMFSourceReader **ppSourceReader) {
|
||||
return MFCreateSourceReaderFromMediaSource(pMediaSource, pAttributes, ppSourceReader);
|
||||
}
|
||||
|
||||
HRESULT WrappedMFGetService (
|
||||
IUnknown* punkObject,
|
||||
REFGUID guidService,
|
||||
REFIID riid,
|
||||
_Outptr_ LPVOID* ppvObject) {
|
||||
return MFGetService(punkObject, guidService, riid, ppvObject);
|
||||
}
|
||||
#include "mf_wrappers.h"
|
||||
#include "util/libutils.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace games::iidx {
|
||||
|
||||
static bool INITIALIZED = false;
|
||||
|
||||
static HMODULE mf_dll = nullptr;
|
||||
static HMODULE mfreadwrite_dll = nullptr;
|
||||
static HMODULE mfplat_dll = nullptr;
|
||||
|
||||
typedef HRESULT (__stdcall * MFCreateAttributes_t)(
|
||||
_Out_ IMFAttributes** ppMFAttributes,
|
||||
_In_ UINT32 cInitialSize
|
||||
);
|
||||
|
||||
typedef HRESULT (__stdcall * MFCreateMediaType_t)(
|
||||
_Out_ IMFMediaType** ppMFType
|
||||
);
|
||||
|
||||
typedef HRESULT (__stdcall * MFEnumDeviceSources_t)(
|
||||
_In_ IMFAttributes* pAttributes,
|
||||
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
||||
_Out_ UINT32* pcSourceActivate
|
||||
);
|
||||
|
||||
typedef HRESULT (__stdcall * MFCreateSourceReaderFromMediaSource_t)(
|
||||
_In_ IMFMediaSource *pMediaSource,
|
||||
_In_opt_ IMFAttributes *pAttributes,
|
||||
_Out_ IMFSourceReader **ppSourceReader
|
||||
);
|
||||
|
||||
typedef HRESULT (__stdcall * MFGetService_t)(
|
||||
IUnknown* punkObject,
|
||||
REFGUID guidService,
|
||||
REFIID riid,
|
||||
_Outptr_ LPVOID* ppvObject
|
||||
);
|
||||
|
||||
static MFCreateAttributes_t MFCreateAttributes = nullptr;
|
||||
static MFCreateMediaType_t MFCreateMediaType = nullptr;
|
||||
static MFEnumDeviceSources_t MFEnumDeviceSources = nullptr;
|
||||
static MFCreateSourceReaderFromMediaSource_t MFCreateSourceReaderFromMediaSource = nullptr;
|
||||
static MFGetService_t MFGetService = nullptr;
|
||||
|
||||
void init_mf_library() {
|
||||
|
||||
// why was all of this needed?
|
||||
//
|
||||
// when iidx camhook was initially implemented, we linked to mf.lib, mfreadwrite.lib, and mfplat.lib
|
||||
// this made Unity-based really unhappy, causing them to skip over the logic that loads mf library
|
||||
// causing videos to not play ("Initializing Microsoft Media Foundation failed." in the cmd prompt)
|
||||
//
|
||||
// as a result, the static linking to mf libs were removed, and we are now doing the mess that is this file
|
||||
|
||||
if (INITIALIZED) {
|
||||
return;
|
||||
}
|
||||
INITIALIZED = true;
|
||||
|
||||
log_misc("mf_wrappers", "creating delay-loaded wrappers for MF routines - BEGIN");
|
||||
|
||||
mf_dll = libutils::load_library("mf.dll", true);
|
||||
mfreadwrite_dll = libutils::load_library("mfreadwrite.dll", true);
|
||||
mfplat_dll = libutils::load_library("mfplat.dll", true);
|
||||
|
||||
MFCreateAttributes = (MFCreateAttributes_t)
|
||||
libutils::get_proc(mfplat_dll, "MFCreateAttributes");
|
||||
if (!MFCreateAttributes) {
|
||||
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)
|
||||
libutils::get_proc(mf_dll, "MFEnumDeviceSources");
|
||||
if (!MFEnumDeviceSources) {
|
||||
log_fatal("mf_wrappers", "MFEnumDeviceSources failed to hook");
|
||||
}
|
||||
|
||||
MFCreateSourceReaderFromMediaSource = (MFCreateSourceReaderFromMediaSource_t)
|
||||
libutils::get_proc(mfreadwrite_dll, "MFCreateSourceReaderFromMediaSource");
|
||||
if (!MFCreateSourceReaderFromMediaSource) {
|
||||
log_fatal("mf_wrappers", "MFCreateSourceReaderFromMediaSource failed to hook");
|
||||
}
|
||||
|
||||
MFGetService = (MFGetService_t)libutils::get_proc(mf_dll, "MFGetService");
|
||||
if (!MFGetService) {
|
||||
log_fatal("mf_wrappers", "MFGetService failed to hook");
|
||||
}
|
||||
|
||||
log_misc("mf_wrappers", "creating delay-loaded wrappers for MF routines - DONE");
|
||||
}
|
||||
|
||||
HRESULT WrappedMFCreateAttributes (
|
||||
_Out_ IMFAttributes** ppMFAttributes,
|
||||
_In_ UINT32 cInitialSize) {
|
||||
return MFCreateAttributes(ppMFAttributes, cInitialSize);
|
||||
}
|
||||
|
||||
HRESULT WrappedMFCreateMediaType (
|
||||
_Out_ IMFMediaType** ppMFType) {
|
||||
return MFCreateMediaType(ppMFType);
|
||||
}
|
||||
|
||||
HRESULT WrappedMFEnumDeviceSources (
|
||||
_In_ IMFAttributes* pAttributes,
|
||||
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
||||
_Out_ UINT32* pcSourceActivate) {
|
||||
return MFEnumDeviceSources(pAttributes, pppSourceActivate, pcSourceActivate);
|
||||
}
|
||||
|
||||
HRESULT WrappedMFCreateSourceReaderFromMediaSource (
|
||||
_In_ IMFMediaSource *pMediaSource,
|
||||
_In_opt_ IMFAttributes *pAttributes,
|
||||
_Out_ IMFSourceReader **ppSourceReader) {
|
||||
return MFCreateSourceReaderFromMediaSource(pMediaSource, pAttributes, ppSourceReader);
|
||||
}
|
||||
|
||||
HRESULT WrappedMFGetService (
|
||||
IUnknown* punkObject,
|
||||
REFGUID guidService,
|
||||
REFIID riid,
|
||||
_Outptr_ LPVOID* ppvObject) {
|
||||
return MFGetService(punkObject, guidService, riid, ppvObject);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,33 @@
|
||||
#include <mfapi.h>
|
||||
#include <mfidl.h>
|
||||
#include <mfreadwrite.h>
|
||||
#include <mfobjects.h>
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace games::iidx {
|
||||
void init_mf_library();
|
||||
|
||||
HRESULT WrappedMFCreateAttributes (
|
||||
_Out_ IMFAttributes** ppMFAttributes,
|
||||
_In_ UINT32 cInitialSize);
|
||||
|
||||
HRESULT WrappedMFEnumDeviceSources (
|
||||
_In_ IMFAttributes* pAttributes,
|
||||
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
||||
_Out_ UINT32* pcSourceActivate);
|
||||
|
||||
HRESULT WrappedMFCreateSourceReaderFromMediaSource (
|
||||
_In_ IMFMediaSource *pMediaSource,
|
||||
_In_opt_ IMFAttributes *pAttributes,
|
||||
_Out_ IMFSourceReader **ppSourceReader);
|
||||
|
||||
HRESULT WrappedMFGetService (
|
||||
IUnknown* punkObject,
|
||||
REFGUID guidService,
|
||||
REFIID riid,
|
||||
_Outptr_ LPVOID* ppvObject);
|
||||
#include <mfapi.h>
|
||||
#include <mfidl.h>
|
||||
#include <mfreadwrite.h>
|
||||
#include <mfobjects.h>
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace games::iidx {
|
||||
void init_mf_library();
|
||||
|
||||
HRESULT WrappedMFCreateAttributes (
|
||||
_Out_ IMFAttributes** ppMFAttributes,
|
||||
_In_ UINT32 cInitialSize);
|
||||
|
||||
HRESULT WrappedMFCreateMediaType (
|
||||
_Out_ IMFMediaType** ppMFType);
|
||||
|
||||
HRESULT WrappedMFEnumDeviceSources (
|
||||
_In_ IMFAttributes* pAttributes,
|
||||
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
|
||||
_Out_ UINT32* pcSourceActivate);
|
||||
|
||||
HRESULT WrappedMFCreateSourceReaderFromMediaSource (
|
||||
_In_ IMFMediaSource *pMediaSource,
|
||||
_In_opt_ IMFAttributes *pAttributes,
|
||||
_Out_ IMFSourceReader **ppSourceReader);
|
||||
|
||||
HRESULT WrappedMFGetService (
|
||||
IUnknown* punkObject,
|
||||
REFGUID guidService,
|
||||
REFIID riid,
|
||||
_Outptr_ LPVOID* ppvObject);
|
||||
}
|
||||
+26
-147
@@ -1,31 +1,17 @@
|
||||
#include "poke.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include "windows.h"
|
||||
#include "cfg/screen_resize.h"
|
||||
#include "games/io.h"
|
||||
#include "games/iidx/iidx.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "launcher/shutdown.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "overlay/windows/generic_sub.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "touch/native/inject.h"
|
||||
#include "touch/touch.h"
|
||||
#include "util/libutils.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 {
|
||||
|
||||
@@ -116,93 +102,29 @@ namespace games::iidx::poke {
|
||||
{"1/D", 50},
|
||||
};
|
||||
|
||||
#if POKE_NATIVE_TOUCH
|
||||
void initialize_native_touch_library() {
|
||||
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) {
|
||||
static void inject_native_touch_points(const std::vector<TouchPoint> &touch_points, bool down) {
|
||||
if (touch_points.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
POINTER_TOUCH_INFO contact;
|
||||
BOOL bRet = TRUE;
|
||||
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());
|
||||
}
|
||||
const auto &touch = touch_points.front();
|
||||
nativetouch::inject::inject_synthetic_touch({ touch.x, touch.y }, down);
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
// check if already running
|
||||
if (THREAD)
|
||||
return;
|
||||
|
||||
if (!games::iidx::NATIVE_TOUCH) {
|
||||
log_warning("poke", "keypad touch emulation requires native touch; disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
// create new thread
|
||||
THREAD_RUNNING = true;
|
||||
THREAD = new std::thread([] {
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
// log
|
||||
log_info("poke", "enabled");
|
||||
|
||||
@@ -210,35 +132,13 @@ namespace games::iidx::poke {
|
||||
std::vector<TouchPoint> touch_points;
|
||||
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
|
||||
while (THREAD_RUNNING) {
|
||||
|
||||
// clean up touch from last frame
|
||||
if (touch_points.size() > 0) {
|
||||
#if POKE_NATIVE_TOUCH
|
||||
if (use_native) {
|
||||
clear_native_touch_points(&touch_points);
|
||||
} else {
|
||||
clear_touch_points(&touch_points);
|
||||
}
|
||||
#else
|
||||
clear_touch_points(&touch_points);
|
||||
#endif
|
||||
inject_native_touch_points(touch_points, false);
|
||||
touch_points.clear();
|
||||
}
|
||||
|
||||
for (int unit = 0; unit < 2; unit++) {
|
||||
@@ -263,32 +163,15 @@ namespace games::iidx::poke {
|
||||
|
||||
float x = x_iter->second / 1920.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) {
|
||||
x *= SPICETOUCH_TOUCH_WIDTH;
|
||||
y *= SPICETOUCH_TOUCH_HEIGHT;
|
||||
} else {
|
||||
x *= ImGui::GetIO().DisplaySize.x;
|
||||
y *= ImGui::GetIO().DisplaySize.y;
|
||||
if (GRAPHICS_WINDOWED) {
|
||||
if (SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
|
||||
continue;
|
||||
}
|
||||
x = SPICETOUCH_TOUCH_X + x * SPICETOUCH_TOUCH_WIDTH;
|
||||
y = SPICETOUCH_TOUCH_Y + y * SPICETOUCH_TOUCH_HEIGHT;
|
||||
} else {
|
||||
// Overlay subscreen
|
||||
x = (GENERIC_SUB_WINDOW_X + x * GENERIC_SUB_WINDOW_WIDTH);
|
||||
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;
|
||||
}
|
||||
x = x_iter->second;
|
||||
y = y_iter->second;
|
||||
}
|
||||
|
||||
TouchPoint tp {
|
||||
@@ -309,19 +192,15 @@ namespace games::iidx::poke {
|
||||
} // for all units
|
||||
|
||||
if (touch_points.size() > 0) {
|
||||
#if POKE_NATIVE_TOUCH
|
||||
if (use_native) {
|
||||
emulate_native_touch_points(&touch_points);
|
||||
} else {
|
||||
touch_write_points(&touch_points);
|
||||
}
|
||||
#else
|
||||
touch_write_points(&touch_points);
|
||||
#endif
|
||||
inject_native_touch_points(touch_points, true);
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace games {
|
||||
static bool IO_INITIALIZED = false;
|
||||
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_modifiers;
|
||||
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::string> buttons_help;
|
||||
@@ -132,6 +133,7 @@ namespace games {
|
||||
buttons_help.insert({ jb, jb::get_buttons_help() });
|
||||
lights.insert({ jb, jb::get_lights() });
|
||||
file_hints[jb].push_back({"jubeat.dll"});
|
||||
file_hints[jb].push_back({"jubeat2019.dll"});
|
||||
|
||||
// mga
|
||||
const std::string mga("Metal Gear");
|
||||
@@ -365,6 +367,37 @@ namespace games {
|
||||
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) {
|
||||
auto buttons = GameAPI::Buttons::getButtons(game);
|
||||
std::vector<std::string> names;
|
||||
|
||||
@@ -5,6 +5,16 @@
|
||||
|
||||
namespace games {
|
||||
|
||||
namespace ModifierButtons {
|
||||
enum {
|
||||
Modifier1,
|
||||
Modifier2,
|
||||
Modifier3,
|
||||
Modifier4,
|
||||
Size,
|
||||
};
|
||||
}
|
||||
|
||||
namespace OverlayButtons {
|
||||
enum {
|
||||
Screenshot,
|
||||
@@ -59,6 +69,7 @@ namespace games {
|
||||
|
||||
const std::vector<std::string> &get_games();
|
||||
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_analogs_help(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
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <windows.h>
|
||||
#include <filesystem>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "bi2x_hook.h"
|
||||
#include "cfg/configurator.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/detour.h"
|
||||
@@ -10,6 +12,8 @@
|
||||
|
||||
namespace games::jb {
|
||||
|
||||
#if !SPICE64
|
||||
|
||||
// fixes "IP ADDR CHANGE" errors with unusual network setups (e.g. a VPN)
|
||||
static BOOL __stdcall network_addr_is_changed() {
|
||||
return 0;
|
||||
@@ -31,6 +35,8 @@ namespace games::jb {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
JBGame::JBGame() : Game("Jubeat") {
|
||||
}
|
||||
|
||||
@@ -60,8 +66,18 @@ namespace games::jb {
|
||||
void JBGame::attach() {
|
||||
Game::attach();
|
||||
|
||||
#if SPICE64
|
||||
if (avs::game::DLL_NAME == "jubeat2019.dll") {
|
||||
libutils::load_library("libaio.dll");
|
||||
libutils::load_library("libaio-iob.dll");
|
||||
libutils::load_library("libaio-iob2_video.dll");
|
||||
bi2x_hook_init();
|
||||
}
|
||||
#endif
|
||||
|
||||
touch_attach();
|
||||
|
||||
#if !SPICE64
|
||||
// enable debug logging of gftools
|
||||
HMODULE gftools = libutils::try_module("gftools.dll");
|
||||
detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc(
|
||||
@@ -75,6 +91,8 @@ namespace games::jb {
|
||||
network, "network_get_network_check_info"));
|
||||
detour::inline_hook((void *) network_get_dhcp_result, libutils::try_proc(
|
||||
network, "network_get_dhcp_result"));
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void JBGame::detach() {
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
// (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.
|
||||
@@ -41,6 +43,7 @@ namespace games::jb {
|
||||
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
|
||||
@@ -85,35 +88,45 @@ namespace games::jb {
|
||||
// --- 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) {
|
||||
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] + JB_BUTTON_SIZE + JB_BUTTON_GAPS[i - 1];
|
||||
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_PORTRAIT) {
|
||||
gx = axis_geometry(8);
|
||||
gy = axis_geometry(602);
|
||||
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);
|
||||
gy = axis_geometry(8);
|
||||
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 end = button + JB_BUTTON_SIZE - 1;
|
||||
static int axis_distance(int p, int button, int size) {
|
||||
int end = button + size - 1;
|
||||
if (p < button) {
|
||||
return button - p;
|
||||
}
|
||||
@@ -131,8 +144,8 @@ namespace games::jb {
|
||||
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]);
|
||||
int dy = axis_distance(py, gy.button[r]);
|
||||
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;
|
||||
@@ -145,7 +158,8 @@ namespace games::jb {
|
||||
|
||||
// detection reach for the current algorithm (0 = register only inside a button)
|
||||
static int touch_radius() {
|
||||
return TOUCH_ALGORITHM == AcAccurate ? 0 : JB_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
|
||||
@@ -162,8 +176,8 @@ namespace games::jb {
|
||||
}
|
||||
for (int r = 0; r < 4; r++) {
|
||||
for (int c = 0; c < 4; c++) {
|
||||
int dx = axis_distance(px, gx.button[c]);
|
||||
int dy = axis_distance(py, gy.button[r]);
|
||||
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);
|
||||
}
|
||||
@@ -184,11 +198,15 @@ namespace games::jb {
|
||||
// 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 (the model name prefixes the window title in every version)
|
||||
// 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(), avs::game::MODEL)) {
|
||||
wnd = FindWindowBeginsWith(avs::game::MODEL);
|
||||
if (!string_begins_with(GetActiveWindowTitle(), window_title)) {
|
||||
wnd = FindWindowBeginsWith(window_title);
|
||||
}
|
||||
if (!wnd) {
|
||||
log_warning("jubeat", "could not find window handle for touch");
|
||||
@@ -197,16 +215,13 @@ namespace games::jb {
|
||||
return;
|
||||
}
|
||||
|
||||
// only the L44 model runs in portrait; set this before starting the
|
||||
// touch-window thread so the renderer only observes the final value
|
||||
IS_PORTRAIT = avs::game::is_model("L44");
|
||||
|
||||
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
|
||||
touch_create_wnd(wnd, true);
|
||||
|
||||
// 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);
|
||||
}
|
||||
@@ -231,7 +246,7 @@ namespace games::jb {
|
||||
return !touch_matured(tp, now_ms, threshold_ms);
|
||||
});
|
||||
|
||||
if (TOUCH_ALGORITHM == Legacy) {
|
||||
if (TOUCH_ALGORITHM == Legacy && !IS_T44) {
|
||||
|
||||
// legacy: evenly divide the play area into a 4x4 grid
|
||||
auto offset = IS_PORTRAIT ? 580 : 0;
|
||||
@@ -310,7 +325,7 @@ namespace games::jb {
|
||||
for (int c = 0; c < 4; c++) {
|
||||
cells[r * 4 + c] = {
|
||||
gx.button[c], gy.button[r],
|
||||
gx.button[c] + JB_BUTTON_SIZE, gy.button[r] + JB_BUTTON_SIZE
|
||||
gx.button[c] + gx.size, gy.button[r] + gy.size
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -351,8 +366,8 @@ namespace games::jb {
|
||||
const AxisGeometry &gx, const AxisGeometry &gy, int arc_radius) {
|
||||
int c = index % 4;
|
||||
int r = index / 4;
|
||||
double mid = std::atan2((gy.button[r] + JB_BUTTON_SIZE / 2) - py,
|
||||
(gx.button[c] + JB_BUTTON_SIZE / 2) - px);
|
||||
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];
|
||||
@@ -424,7 +439,7 @@ namespace games::jb {
|
||||
}
|
||||
|
||||
// legacy divides the field evenly; the other algorithms use button squares
|
||||
bool legacy = (TOUCH_ALGORITHM == Legacy);
|
||||
bool legacy = TOUCH_ALGORITHM == Legacy && !IS_T44;
|
||||
AxisGeometry gx {}, gy {};
|
||||
if (!legacy) {
|
||||
touch_geometry(gx, gy);
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "util/utils.h"
|
||||
#include "util/execexe.h"
|
||||
#include "acioemu/handle.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "hooks/graphics/graphics.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 "poke.h"
|
||||
#include "touch_mode.h"
|
||||
#include "hooks/setupapihook.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "avs/game.h"
|
||||
|
||||
namespace games::nost {
|
||||
|
||||
bool ENABLE_POKE = false;
|
||||
bool ENABLE_TOUCH_MODE = false;
|
||||
|
||||
NostGame::NostGame() : Game("Nostalgia") {
|
||||
}
|
||||
@@ -43,9 +42,16 @@ namespace games::nost {
|
||||
setupapihook_init(avs::game::DLL_INSTANCE);
|
||||
setupapihook_add(touch_settings);
|
||||
|
||||
// custom touch
|
||||
// nostalgia crashes if you inject touch events too early
|
||||
wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20);
|
||||
const auto native_touch_ready = !wintouchemu::FORCE &&
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
if (!native_touch_ready) {
|
||||
wintouchemu::FORCE = true;
|
||||
wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20);
|
||||
} else {
|
||||
if (ENABLE_TOUCH_MODE) {
|
||||
touch_mode::enable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NostGame::post_attach() {
|
||||
@@ -55,6 +61,9 @@ namespace games::nost {
|
||||
}
|
||||
|
||||
void NostGame::detach() {
|
||||
if (ENABLE_TOUCH_MODE) {
|
||||
touch_mode::disable();
|
||||
}
|
||||
if (ENABLE_POKE) {
|
||||
poke::disable();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
namespace games::nost {
|
||||
|
||||
extern bool ENABLE_POKE;
|
||||
extern bool ENABLE_TOUCH_MODE;
|
||||
|
||||
class NostGame : public games::Game {
|
||||
public:
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
#include "poke.h"
|
||||
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
#include "games/nost/io.h"
|
||||
#include "cfg/api.h"
|
||||
#include "rawinput/rawinput.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/precise_timer.h"
|
||||
|
||||
namespace games::nost::poke {
|
||||
|
||||
static std::thread *THREAD = nullptr;
|
||||
static volatile bool THREAD_RUNNING = false;
|
||||
|
||||
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_NUM {
|
||||
static std::atomic<bool> THREAD_RUNNING { false };
|
||||
|
||||
// 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_1, {760, 385}},
|
||||
{EAM_IO_KEYPAD_2, {820, 385}},
|
||||
@@ -31,7 +34,8 @@ namespace games::nost::poke {
|
||||
{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::PokeBack, {340, 100}},
|
||||
{games::nost::Buttons::Button::PokeSong1, {450, 190}},
|
||||
@@ -46,13 +50,8 @@ namespace games::nost::poke {
|
||||
{games::nost::Buttons::Button::PokeDifficulty4, {820, 490}},
|
||||
};
|
||||
|
||||
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();
|
||||
static bool inject_poke_position(POINT position, bool down) {
|
||||
return nativetouch::inject::inject_synthetic_touch(position, down);
|
||||
}
|
||||
|
||||
void enable() {
|
||||
@@ -61,126 +60,143 @@ namespace games::nost::poke {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nativetouch::is_hooked()) {
|
||||
log_warning("poke", "Nostalgia poke requires native touch; disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
// create new thread
|
||||
THREAD_RUNNING = true;
|
||||
THREAD = new std::thread([] {
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
const DWORD touch_id = (DWORD)(0xFFFFFFFE);
|
||||
|
||||
const int swipe_anim_total_frames = 6;
|
||||
const int swipe_anim_y = 300;
|
||||
|
||||
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;
|
||||
|
||||
int next_page_anim_index = -1;
|
||||
int prev_page_anim_index = -1;
|
||||
std::vector<TouchPoint> touch_points;
|
||||
|
||||
bool touch_release = false;
|
||||
bool contact_active = false;
|
||||
bool release_pending = false;
|
||||
POINT last_position {};
|
||||
std::unordered_map<games::nost::Buttons::Button, bool> button_states;
|
||||
uint16_t last_keypad_state = 0;
|
||||
|
||||
// log
|
||||
log_info("poke", "enabled");
|
||||
|
||||
// set variable to false to stop
|
||||
while (THREAD_RUNNING) {
|
||||
// clean up touch from last frame
|
||||
if (!touch_points.empty()) {
|
||||
if (touch_release) {
|
||||
clear_touch_points(&touch_points);
|
||||
touch_release = false;
|
||||
} else {
|
||||
touch_points.clear();
|
||||
if (release_pending) {
|
||||
if (contact_active) {
|
||||
inject_poke_position(last_position, false);
|
||||
}
|
||||
contact_active = false;
|
||||
release_pending = false;
|
||||
}
|
||||
|
||||
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) {
|
||||
const auto delta =
|
||||
(swipe_next_page_x_end - swipe_next_page_x_begin)
|
||||
* (swipe_anim_total_frames - next_page_anim_index) / swipe_anim_total_frames;
|
||||
|
||||
TouchPoint tp {
|
||||
.id = touch_id,
|
||||
.x = (LONG)swipe_next_page_x_begin + delta,
|
||||
.y = (LONG)swipe_anim_y,
|
||||
.mouse = true,
|
||||
touch_position = POINT {
|
||||
swipe_next_page_x_begin + delta,
|
||||
swipe_anim_y,
|
||||
};
|
||||
touch_points.emplace_back(tp);
|
||||
next_page_anim_index--;
|
||||
if (next_page_anim_index < 0) {
|
||||
touch_release = true;
|
||||
release_after_touch = true;
|
||||
}
|
||||
} else if (0 <= prev_page_anim_index) {
|
||||
const auto delta =
|
||||
(swipe_prev_page_x_end - swipe_prev_page_x_begin)
|
||||
* (swipe_anim_total_frames - prev_page_anim_index) / swipe_anim_total_frames;
|
||||
|
||||
TouchPoint tp {
|
||||
.id = touch_id,
|
||||
.x = (LONG)swipe_prev_page_x_begin + delta,
|
||||
.y = (LONG)swipe_anim_y,
|
||||
.mouse = true,
|
||||
touch_position = POINT {
|
||||
swipe_prev_page_x_begin + delta,
|
||||
swipe_anim_y,
|
||||
};
|
||||
touch_points.emplace_back(tp);
|
||||
prev_page_anim_index--;
|
||||
if (prev_page_anim_index < 0) {
|
||||
touch_release = true;
|
||||
release_after_touch = true;
|
||||
}
|
||||
} else {
|
||||
for (const auto& it : NOST_POKE) {
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
touch_position = triggered_touch_position;
|
||||
release_after_touch = touch_position.has_value();
|
||||
|
||||
// start animations for next frame
|
||||
if (!touch_release) {
|
||||
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokeNextPage))) {
|
||||
if (!touch_position.has_value()) {
|
||||
if (next_triggered) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!touch_points.empty()) {
|
||||
touch_write_points(&touch_points);
|
||||
if (touch_position.has_value() &&
|
||||
inject_poke_position(*touch_position, true)) {
|
||||
contact_active = true;
|
||||
last_position = *touch_position;
|
||||
}
|
||||
if (release_after_touch && contact_active) {
|
||||
release_pending = true;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -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/execexe.h"
|
||||
#include "acioemu/handle.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "util/socd_cleaner.h"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "acio/icca/icca.h"
|
||||
#include "io.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/utils.h"
|
||||
#include "util/libutils.h"
|
||||
|
||||
@@ -16,16 +16,16 @@
|
||||
#include "launcher/launcher.h"
|
||||
#include "launcher/logger.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "util/sysutils.h"
|
||||
#include "io.h"
|
||||
#include "util/deferlog.h"
|
||||
#include "misc/nativetouchhook.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
|
||||
namespace games::popn {
|
||||
|
||||
bool SHOW_PIKA_MONITOR_WARNING = false;
|
||||
bool NATIVE_TOUCH = false;
|
||||
bool NATIVE_TOUCH = true;
|
||||
|
||||
#if SPICE64 && !SPICE_XP
|
||||
|
||||
@@ -715,13 +715,16 @@ namespace games::popn {
|
||||
// 00000100 0000000B 00000001 (button 9)
|
||||
// set third column to 0 and it will work with BIO2
|
||||
|
||||
if (!GRAPHICS_WINDOWED) {
|
||||
if (NATIVE_TOUCH) {
|
||||
nativetouchhook::hook(avs::game::DLL_INSTANCE);
|
||||
} else {
|
||||
wintouchemu::FORCE = true;
|
||||
NATIVE_TOUCH = !wintouchemu::FORCE &&
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
if (!NATIVE_TOUCH) {
|
||||
wintouchemu::FORCE = true;
|
||||
if (!GRAPHICS_WINDOWED || GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,9 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
||||
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER);
|
||||
}
|
||||
|
||||
// request automatic aspect ratio fixes
|
||||
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
|
||||
|
||||
// create touch window
|
||||
touch_create_wnd(wnd);
|
||||
|
||||
@@ -94,9 +97,6 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
||||
ShowWindow(wnd, SW_SHOW);
|
||||
}
|
||||
|
||||
// request automatic aspect ratio fixes
|
||||
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
|
||||
|
||||
} else {
|
||||
|
||||
// fallback to dx hook
|
||||
|
||||
@@ -1,143 +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;
|
||||
}
|
||||
}
|
||||
#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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +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);
|
||||
}
|
||||
#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);
|
||||
}
|
||||
|
||||
@@ -1,17 +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;
|
||||
}
|
||||
#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;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#if SPICE64
|
||||
|
||||
#include <cstdint>
|
||||
#include "api/client.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
@@ -349,43 +350,29 @@ namespace games::sdvx {
|
||||
* 9 - v unit - 258 bytes - 86 colors
|
||||
*
|
||||
* data is stored in RGB order, 3 bytes per color
|
||||
*
|
||||
* TODO: expose this data via API
|
||||
*/
|
||||
|
||||
// data mapping
|
||||
static struct TapeLedMapping {
|
||||
size_t data_size;
|
||||
int index_r, index_g, index_b;
|
||||
|
||||
TapeLedMapping(size_t data_size, int index_r, int index_g, int index_b)
|
||||
: data_size(data_size), index_r(index_r), index_g(index_g), index_b(index_b) {}
|
||||
|
||||
} mapping[] = {
|
||||
{ 74, Lights::TITLE_AVG_R, Lights::TITLE_AVG_G, Lights::TITLE_AVG_B },
|
||||
{ 12, Lights::UPPER_LEFT_SPEAKER_AVG_R, Lights::UPPER_LEFT_SPEAKER_AVG_G, Lights::UPPER_LEFT_SPEAKER_AVG_B },
|
||||
{ 12, Lights::UPPER_RIGHT_SPEAKER_AVG_R, Lights::UPPER_RIGHT_SPEAKER_AVG_G, Lights::UPPER_RIGHT_SPEAKER_AVG_B },
|
||||
{ 56, Lights::LEFT_WING_AVG_R, Lights::LEFT_WING_AVG_G, Lights::LEFT_WING_AVG_B },
|
||||
{ 56, Lights::RIGHT_WING_AVG_R, Lights::RIGHT_WING_AVG_G, Lights::RIGHT_WING_AVG_B },
|
||||
{ 94, Lights::CONTROL_PANEL_AVG_R, Lights::CONTROL_PANEL_AVG_G, Lights::CONTROL_PANEL_AVG_B },
|
||||
{ 12, Lights::LOWER_LEFT_SPEAKER_AVG_R, Lights::LOWER_LEFT_SPEAKER_AVG_G, Lights::LOWER_LEFT_SPEAKER_AVG_B },
|
||||
{ 12, Lights::LOWER_RIGHT_SPEAKER_AVG_R, Lights::LOWER_RIGHT_SPEAKER_AVG_G, Lights::LOWER_RIGHT_SPEAKER_AVG_B },
|
||||
{ 14, Lights::WOOFER_AVG_R, Lights::WOOFER_AVG_G, Lights::WOOFER_AVG_B },
|
||||
{ 86, Lights::V_UNIT_AVG_R, Lights::V_UNIT_AVG_G, Lights::V_UNIT_AVG_B },
|
||||
};
|
||||
|
||||
// check index bounds
|
||||
if (tapeledutils::is_enabled() && index < std::size(mapping)) {
|
||||
auto &map = mapping[index];
|
||||
if (tapeledutils::is_enabled() && index < std::size(TAPELED_MAPPING)) {
|
||||
auto &map = TAPELED_MAPPING[index];
|
||||
const auto data_size = map.data.size();
|
||||
|
||||
// pick a color to use
|
||||
const auto rgb = tapeledutils::pick_color_from_led_tape(data, map.data_size);
|
||||
const auto rgb = tapeledutils::pick_color_from_led_tape(data, data_size);
|
||||
|
||||
// program the lights into API
|
||||
auto &lights = get_lights();
|
||||
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_r], rgb.r);
|
||||
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g);
|
||||
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
|
||||
|
||||
if (api::has_clients()) {
|
||||
for (size_t i = 0; i < data_size; ++i) {
|
||||
map.data[i].r = data[i * 3];
|
||||
map.data[i].g = data[i * 3 + 1];
|
||||
map.data[i].b = data[i * 3 + 2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (This != custom_node) {
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "hooks/devicehook.h"
|
||||
#include "hooks/libraryhook.h"
|
||||
#ifdef SPICE64
|
||||
#include "hooks/graphics/nvapi_impl.h"
|
||||
#endif
|
||||
#include "hooks/graphics/nvapi_hook.h"
|
||||
#include "hooks/powrprof.h"
|
||||
#include "hooks/sleephook.h"
|
||||
@@ -23,8 +26,8 @@
|
||||
#include "util/libutils.h"
|
||||
#include "util/sysutils.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "misc/nativetouchhook.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "bi2x_hook.h"
|
||||
#include "camera.h"
|
||||
#include "io.h"
|
||||
@@ -46,7 +49,6 @@ namespace games::sdvx {
|
||||
const char *ORIGINAL_ASIO_DEVICE_NAME = "XONAR SOUND CARD(64)";
|
||||
|
||||
// settings
|
||||
bool NATIVETOUCH = false;
|
||||
uint8_t DIGITAL_KNOB_SENS = 16;
|
||||
SdvxOverlayPosition OVERLAY_POS = SDVX_OVERLAY_BOTTOM;
|
||||
bool ENABLE_COM_PORT_SCAN_HOOK = false;
|
||||
@@ -59,6 +61,19 @@ namespace games::sdvx {
|
||||
static HKEY real_asio_reg_handle = nullptr;
|
||||
static HKEY real_asio_device_reg_handle = nullptr;
|
||||
|
||||
tapeledutils::tape_led TAPELED_MAPPING[SDVX_TAPELED_TOTAL] = {
|
||||
{ 74, Lights::TITLE_AVG_R, Lights::TITLE_AVG_G, Lights::TITLE_AVG_B, "Title" },
|
||||
{ 12, Lights::UPPER_LEFT_SPEAKER_AVG_R, Lights::UPPER_LEFT_SPEAKER_AVG_G, Lights::UPPER_LEFT_SPEAKER_AVG_B, "Upper Left Speaker" },
|
||||
{ 12, Lights::UPPER_RIGHT_SPEAKER_AVG_R, Lights::UPPER_RIGHT_SPEAKER_AVG_G, Lights::UPPER_RIGHT_SPEAKER_AVG_B, "Upper Right Speaker" },
|
||||
{ 56, Lights::LEFT_WING_AVG_R, Lights::LEFT_WING_AVG_G, Lights::LEFT_WING_AVG_B, "Left Wing" },
|
||||
{ 56, Lights::RIGHT_WING_AVG_R, Lights::RIGHT_WING_AVG_G, Lights::RIGHT_WING_AVG_B, "Right Wing" },
|
||||
{ 94, Lights::CONTROL_PANEL_AVG_R, Lights::CONTROL_PANEL_AVG_G, Lights::CONTROL_PANEL_AVG_B, "Control Panel" },
|
||||
{ 12, Lights::LOWER_LEFT_SPEAKER_AVG_R, Lights::LOWER_LEFT_SPEAKER_AVG_G, Lights::LOWER_LEFT_SPEAKER_AVG_B, "Lower Left Speaker" },
|
||||
{ 12, Lights::LOWER_RIGHT_SPEAKER_AVG_R, Lights::LOWER_RIGHT_SPEAKER_AVG_G, Lights::LOWER_RIGHT_SPEAKER_AVG_B, "Lower Right Speaker" },
|
||||
{ 14, Lights::WOOFER_AVG_R, Lights::WOOFER_AVG_G, Lights::WOOFER_AVG_B, "Woofer" },
|
||||
{ 86, Lights::V_UNIT_AVG_R, Lights::V_UNIT_AVG_G, Lights::V_UNIT_AVG_B, "V Unit" },
|
||||
};
|
||||
|
||||
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
|
||||
if (lpSubKey != nullptr &&
|
||||
phkResult != nullptr &&
|
||||
@@ -428,28 +443,41 @@ namespace games::sdvx {
|
||||
if (aio != nullptr) {
|
||||
|
||||
// enable 9on12 for AMD
|
||||
if (!libutils::try_library("nvapi64.dll")) {
|
||||
const auto nvapi = libutils::try_library("nvapi64.dll");
|
||||
if (nvapi == nullptr) {
|
||||
log_info(
|
||||
"sdvx",
|
||||
"nvapi64.dll not found; for non-NVIDIA GPUs, requesting 9on12 to be enabled");
|
||||
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 {
|
||||
// don't let nvapi mess with display settings
|
||||
nvapi_hook::initialize(avs::game::DLL_INSTANCE);
|
||||
}
|
||||
|
||||
if (is_valkyrie_model()) {
|
||||
if (NATIVETOUCH) {
|
||||
nativetouchhook::hook(avs::game::DLL_INSTANCE);
|
||||
} else if (!NATIVETOUCH && !GRAPHICS_WINDOWED) {
|
||||
// hook touch window
|
||||
// in windowed mode, game can accept mouse input on the second screen
|
||||
const auto native_touch_ready = !wintouchemu::FORCE &&
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
if (!native_touch_ready) {
|
||||
wintouchemu::FORCE = true;
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook_title_ends(
|
||||
if (!GRAPHICS_WINDOWED) {
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook_title_ends(
|
||||
"SOUND VOLTEX",
|
||||
"Main Screen",
|
||||
avs::game::DLL_INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
// insert BI2X hooks
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "games/game.h"
|
||||
#include "util/tapeled.h"
|
||||
|
||||
namespace games::sdvx {
|
||||
|
||||
@@ -18,7 +19,6 @@ namespace games::sdvx {
|
||||
};
|
||||
|
||||
// settings
|
||||
extern bool NATIVETOUCH;
|
||||
extern uint8_t DIGITAL_KNOB_SENS;
|
||||
extern std::optional<std::string> ASIO_DRIVER;
|
||||
extern SdvxOverlayPosition OVERLAY_POS;
|
||||
@@ -26,6 +26,9 @@ namespace games::sdvx {
|
||||
// states
|
||||
extern bool SHOW_VM_MONITOR_WARNING;
|
||||
|
||||
constexpr int SDVX_TAPELED_TOTAL = 10;
|
||||
extern tapeledutils::tape_led TAPELED_MAPPING[SDVX_TAPELED_TOTAL];
|
||||
|
||||
static inline bool is_valkyrie_model() {
|
||||
return (
|
||||
avs::game::is_model("KFC") &&
|
||||
|
||||
@@ -1,72 +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
|
||||
#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
|
||||
|
||||
@@ -1,14 +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
|
||||
|
||||
}
|
||||
#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
|
||||
|
||||
}
|
||||
|
||||
@@ -1,84 +1,84 @@
|
||||
#include "asio_driver_scan.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "util/utils.h"
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
static constexpr char ASIO_REG_PATH[] = "software\\asio";
|
||||
static constexpr char ASIO_REG_DESC[] = "description";
|
||||
|
||||
// enumerate a single registry view, appending to entries while merging
|
||||
// duplicates discovered in another view. Drivers are matched by name (not
|
||||
// CLSID): the game's ASIO loader selects drivers by name, and some vendors
|
||||
// register the same CLSID under different 32-bit/64-bit names (e.g. "XONAR
|
||||
// SOUND CARD" vs "XONAR SOUND CARD(64)"), which are distinct user choices.
|
||||
static void scan_view(
|
||||
REGSAM wow64_flag,
|
||||
bool is_64bit,
|
||||
std::vector<AsioDriverScanEntry> &entries) {
|
||||
|
||||
HKEY hkEnum = nullptr;
|
||||
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, ASIO_REG_PATH, 0,
|
||||
KEY_READ | wow64_flag, &hkEnum) != ERROR_SUCCESS) {
|
||||
return;
|
||||
}
|
||||
|
||||
char key_name[256];
|
||||
for (DWORD index = 0;
|
||||
RegEnumKeyA(hkEnum, index, key_name, sizeof(key_name)) == ERROR_SUCCESS;
|
||||
index++) {
|
||||
|
||||
// read description (display name), fall back to the key name.
|
||||
// RegOpenKeyExA + RegQueryValueExA is used instead of RegGetValueA
|
||||
// because the latter is unavailable on Windows XP.
|
||||
char desc[256] = { 0 };
|
||||
DWORD size = sizeof(desc);
|
||||
std::string name = key_name;
|
||||
HKEY hkDriver = nullptr;
|
||||
if (RegOpenKeyExA(hkEnum, key_name, 0,
|
||||
KEY_QUERY_VALUE | wow64_flag, &hkDriver) == ERROR_SUCCESS) {
|
||||
DWORD type = 0;
|
||||
if (RegQueryValueExA(hkDriver,
|
||||
ASIO_REG_DESC,
|
||||
nullptr,
|
||||
&type,
|
||||
reinterpret_cast<LPBYTE>(desc),
|
||||
&size) == ERROR_SUCCESS
|
||||
&& type == REG_SZ && desc[0]) {
|
||||
// ensure null termination
|
||||
desc[sizeof(desc) - 1] = '\0';
|
||||
name = desc;
|
||||
}
|
||||
RegCloseKey(hkDriver);
|
||||
}
|
||||
|
||||
// merge with an existing entry from the other view (match by name)
|
||||
const std::string name_lower = strtolower(name);
|
||||
auto it = std::find_if(entries.begin(), entries.end(), [&](const auto &e) {
|
||||
return strtolower(e.name) == name_lower;
|
||||
});
|
||||
if (it == entries.end()) {
|
||||
entries.push_back({ name });
|
||||
it = entries.end() - 1;
|
||||
}
|
||||
it->found_32bit |= !is_64bit;
|
||||
it->found_64bit |= is_64bit;
|
||||
}
|
||||
|
||||
RegCloseKey(hkEnum);
|
||||
}
|
||||
|
||||
std::vector<AsioDriverScanEntry> scan_asio_drivers() {
|
||||
std::vector<AsioDriverScanEntry> entries;
|
||||
|
||||
// 64-bit view first so it wins ordering when present in both
|
||||
scan_view(KEY_WOW64_64KEY, true, entries);
|
||||
scan_view(KEY_WOW64_32KEY, false, entries);
|
||||
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
#include "asio_driver_scan.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "util/utils.h"
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
static constexpr char ASIO_REG_PATH[] = "software\\asio";
|
||||
static constexpr char ASIO_REG_DESC[] = "description";
|
||||
|
||||
// enumerate a single registry view, appending to entries while merging
|
||||
// duplicates discovered in another view. Drivers are matched by name (not
|
||||
// CLSID): the game's ASIO loader selects drivers by name, and some vendors
|
||||
// register the same CLSID under different 32-bit/64-bit names (e.g. "XONAR
|
||||
// SOUND CARD" vs "XONAR SOUND CARD(64)"), which are distinct user choices.
|
||||
static void scan_view(
|
||||
REGSAM wow64_flag,
|
||||
bool is_64bit,
|
||||
std::vector<AsioDriverScanEntry> &entries) {
|
||||
|
||||
HKEY hkEnum = nullptr;
|
||||
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, ASIO_REG_PATH, 0,
|
||||
KEY_READ | wow64_flag, &hkEnum) != ERROR_SUCCESS) {
|
||||
return;
|
||||
}
|
||||
|
||||
char key_name[256];
|
||||
for (DWORD index = 0;
|
||||
RegEnumKeyA(hkEnum, index, key_name, sizeof(key_name)) == ERROR_SUCCESS;
|
||||
index++) {
|
||||
|
||||
// read description (display name), fall back to the key name.
|
||||
// RegOpenKeyExA + RegQueryValueExA is used instead of RegGetValueA
|
||||
// because the latter is unavailable on Windows XP.
|
||||
char desc[256] = { 0 };
|
||||
DWORD size = sizeof(desc);
|
||||
std::string name = key_name;
|
||||
HKEY hkDriver = nullptr;
|
||||
if (RegOpenKeyExA(hkEnum, key_name, 0,
|
||||
KEY_QUERY_VALUE | wow64_flag, &hkDriver) == ERROR_SUCCESS) {
|
||||
DWORD type = 0;
|
||||
if (RegQueryValueExA(hkDriver,
|
||||
ASIO_REG_DESC,
|
||||
nullptr,
|
||||
&type,
|
||||
reinterpret_cast<LPBYTE>(desc),
|
||||
&size) == ERROR_SUCCESS
|
||||
&& type == REG_SZ && desc[0]) {
|
||||
// ensure null termination
|
||||
desc[sizeof(desc) - 1] = '\0';
|
||||
name = desc;
|
||||
}
|
||||
RegCloseKey(hkDriver);
|
||||
}
|
||||
|
||||
// merge with an existing entry from the other view (match by name)
|
||||
const std::string name_lower = strtolower(name);
|
||||
auto it = std::find_if(entries.begin(), entries.end(), [&](const auto &e) {
|
||||
return strtolower(e.name) == name_lower;
|
||||
});
|
||||
if (it == entries.end()) {
|
||||
entries.push_back({ name });
|
||||
it = entries.end() - 1;
|
||||
}
|
||||
it->found_32bit |= !is_64bit;
|
||||
it->found_64bit |= is_64bit;
|
||||
}
|
||||
|
||||
RegCloseKey(hkEnum);
|
||||
}
|
||||
|
||||
std::vector<AsioDriverScanEntry> scan_asio_drivers() {
|
||||
std::vector<AsioDriverScanEntry> entries;
|
||||
|
||||
// 64-bit view first so it wins ordering when present in both
|
||||
scan_view(KEY_WOW64_64KEY, true, entries);
|
||||
scan_view(KEY_WOW64_32KEY, false, entries);
|
||||
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
struct AsioDriverScanEntry {
|
||||
std::string name;
|
||||
bool found_32bit = false;
|
||||
bool found_64bit = false;
|
||||
};
|
||||
|
||||
std::vector<AsioDriverScanEntry> scan_asio_drivers();
|
||||
}
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
struct AsioDriverScanEntry {
|
||||
std::string name;
|
||||
bool found_32bit = false;
|
||||
bool found_64bit = false;
|
||||
};
|
||||
|
||||
std::vector<AsioDriverScanEntry> scan_asio_drivers();
|
||||
}
|
||||
|
||||
+1067
-1067
File diff suppressed because it is too large
Load Diff
@@ -1,240 +1,240 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "external/asio/asio.h"
|
||||
#include "external/asio/iasiodrv.h"
|
||||
|
||||
namespace hooks::audio::asio {
|
||||
|
||||
// returns true if a CoCreateInstance call is instantiating a registered ASIO driver.
|
||||
// ASIO hosts pass the driver CLSID as both class id and interface id; we also validate
|
||||
// it against the system's registered ASIO drivers to avoid false positives
|
||||
bool is_asio_creation(REFCLSID rclsid, REFIID riid);
|
||||
|
||||
// wrap a real ASIO driver instance, taking ownership of the supplied reference, and
|
||||
// return a proxy that forwards every call to it. 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);
|
||||
|
||||
// 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
|
||||
struct WrappedAsio final : IAsio {
|
||||
WrappedAsio(IAsio *real, REFCLSID clsid, std::string name)
|
||||
: pReal(real), clsid(clsid), driver_name(std::move(name)) {
|
||||
}
|
||||
|
||||
WrappedAsio(const WrappedAsio &) = delete;
|
||||
WrappedAsio &operator=(const WrappedAsio &) = delete;
|
||||
|
||||
virtual ~WrappedAsio();
|
||||
|
||||
// selects which source channel pair of a multichannel ASIO output reaches the device's
|
||||
// 2.0 front pair. when not None, the proxy presents the game's expected multichannel
|
||||
// layout to the host so it proceeds to create_buffers, then opens only a two-channel
|
||||
// stream on the real device and routes the selected pair onto it (see create_buffers).
|
||||
// Front is the plain "force two channel" case (forward the device's own front pair);
|
||||
// the others copy a different pair onto 0/1. assumes a standard 7.1 layout (0-indexed).
|
||||
// set once at boot, before any wrapper exists, so it needs no synchronization
|
||||
enum class StereoDownmix {
|
||||
None, // feature disabled - full multichannel passthrough
|
||||
Front, // channels 0/1 - the device front pair is forwarded as-is (no copy)
|
||||
Center, // channel 2 duplicated to both 0 and 1
|
||||
Rear, // channels 4/5 -> 0/1
|
||||
Side, // channels 6/7 -> 0/1
|
||||
};
|
||||
static StereoDownmix STEREO_DOWNMIX;
|
||||
|
||||
// true when a stereo extraction is configured, i.e. the real device should open a 2.0
|
||||
// stream and only the selected pair should reach it. the former standalone
|
||||
// FORCE_TWO_CHANNELS flag is now just the Front case of this
|
||||
static bool force_two_channels() {
|
||||
return STEREO_DOWNMIX != StereoDownmix::None;
|
||||
}
|
||||
|
||||
// some games hardcode a multichannel ASIO output and bail before create_buffers if
|
||||
// get_channels reports fewer, so we report at least this many output channels when a
|
||||
// stereo extraction is active
|
||||
static constexpr long FORCED_OUTPUT_CHANNELS = 8;
|
||||
|
||||
// maps an option string ("front", "center", "rear", "side") to a StereoDownmix value,
|
||||
// returning None for anything unrecognized
|
||||
static StereoDownmix name_to_stereo_downmix(const char *name);
|
||||
|
||||
#pragma region IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) override;
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IAsio
|
||||
AsioBool __thiscall init(void *sys_handle) override;
|
||||
void __thiscall get_driver_name(char *name) override;
|
||||
long __thiscall get_driver_version() override;
|
||||
void __thiscall get_error_message(char *string) override;
|
||||
AsioError __thiscall start() override;
|
||||
AsioError __thiscall stop() override;
|
||||
AsioError __thiscall get_channels(long *num_input_channels, long *num_output_channels) override;
|
||||
AsioError __thiscall get_latencies(long *input_latency, long *output_latency) override;
|
||||
AsioError __thiscall get_buffer_size(
|
||||
long *min_size,
|
||||
long *max_size,
|
||||
long *preferred_size,
|
||||
long *granularity) override;
|
||||
AsioError __thiscall can_sample_rate(AsioSampleRate sample_rate) override;
|
||||
AsioError __thiscall get_sample_rate(AsioSampleRate *sample_rate) override;
|
||||
AsioError __thiscall set_sample_rate(AsioSampleRate sample_rate) override;
|
||||
AsioError __thiscall get_clock_sources(ASIOClockSource *clocks, long *num_sources) override;
|
||||
AsioError __thiscall set_clock_source(long reference) override;
|
||||
AsioError __thiscall get_sample_position(ASIOSamples *s_pos, ASIOTimeStamp *t_stamp) override;
|
||||
AsioError __thiscall get_channel_info(AsioChannelInfo *info) override;
|
||||
AsioError __thiscall create_buffers(
|
||||
AsioBufferInfo *buffer_infos,
|
||||
long num_channels,
|
||||
long buffer_size,
|
||||
AsioCallbacks *callbacks) override;
|
||||
AsioError __thiscall dispose_buffers() override;
|
||||
AsioError __thiscall control_panel() override;
|
||||
AsioError __thiscall future(long selector, void *opt) override;
|
||||
AsioError __thiscall output_ready() override;
|
||||
#pragma endregion
|
||||
|
||||
// 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:
|
||||
// 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
|
||||
AsioError create_buffers_front_pair(
|
||||
AsioBufferInfo *buffer_infos,
|
||||
long num_channels,
|
||||
long buffer_size,
|
||||
AsioCallbacks *callbacks);
|
||||
|
||||
// if any post-processing effect (volume boost or stereo downmix) is active, saves the
|
||||
// game's callbacks and returns a proxy callback set (our buffer-switch trampolines) to
|
||||
// hand the real driver instead, so we can rework its output buffers after the game
|
||||
// fills them. otherwise returns the game's callbacks unchanged. called at create_buffers
|
||||
// time, before the stream starts
|
||||
AsioCallbacks *install_proxy_callbacks(AsioCallbacks *game_callbacks);
|
||||
|
||||
// records a device output channel whose buffers we scale by the volume boost. queries
|
||||
// the real driver for the channel's sample format. called at create_buffers time
|
||||
void record_volume_output_channel(const AsioBufferInfo &info);
|
||||
|
||||
// the real device's output sample format, queried from its first output channel. all
|
||||
// output channels of a device share one format, so this characterizes them all. returns
|
||||
// ASIOSTLastEntry if the device has no output channels or the query fails
|
||||
AsioSampleType device_output_sample_type();
|
||||
|
||||
// locates the destination pair (device channels 0/1) and the configured source channels
|
||||
// in the game's buffer set so the realtime path can copy the selected pair onto 0/1.
|
||||
// a no-op unless STEREO_DOWNMIX selects a non-front pair. called at create_buffers time
|
||||
void record_downmix_channels(AsioBufferInfo *buffer_infos, long num_channels, long buffer_size);
|
||||
|
||||
// publishes the captured post-process state to the realtime thread once the buffers
|
||||
// exist, making our trampolines start reworking output. called at the end of either
|
||||
// create_buffers path
|
||||
void publish_post_process(long buffer_size);
|
||||
|
||||
// detaches this instance from the realtime trampolines so they stop touching its
|
||||
// buffers. called from dispose_buffers and the destructor
|
||||
void detach_post_process();
|
||||
|
||||
// multiplies every recorded output channel's buffer for the given double-buffer index
|
||||
// by the volume boost. runs on the driver's realtime thread from our buffer switch
|
||||
void apply_output_volume(long double_buffer_index);
|
||||
|
||||
// copies the configured source channel pair onto device channels 0/1 for the given
|
||||
// double-buffer index. runs on the driver's realtime thread from our buffer switch
|
||||
void apply_downmix(long double_buffer_index);
|
||||
|
||||
// realtime-thread trampolines for the buffer-switch callbacks, handed to the real
|
||||
// driver in place of the game's; ASIO callbacks carry no user data, so they reach the
|
||||
// active wrapper through active_instance, call the game's original, then rework output.
|
||||
// the other two callbacks (sample_rate_did_change, asio_message) are forwarded as the
|
||||
// game's own pointers, so they need no trampoline
|
||||
static void __cdecl proxy_buffer_switch(long double_buffer_index, AsioBool direct_process);
|
||||
static AsioTime * __cdecl proxy_buffer_switch_time_info(
|
||||
AsioTime *params, long double_buffer_index, AsioBool direct_process);
|
||||
|
||||
// the single wrapper whose proxy callbacks are installed (ASIO is single-instance with
|
||||
// one running stream); read by the static trampolines to reach the right wrapper
|
||||
static std::atomic<WrappedAsio *> active_instance;
|
||||
|
||||
IAsio *const pReal;
|
||||
const CLSID clsid;
|
||||
|
||||
// registry name of the driver (not get_driver_name), used in our logs as a single
|
||||
// unambiguous name; constant for our lifetime
|
||||
std::string driver_name;
|
||||
|
||||
// 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
|
||||
// drops to zero
|
||||
std::atomic<ULONG> ref_count {1};
|
||||
|
||||
// throwaway double buffers handed to the channels we discard when a stereo extraction
|
||||
// is active (see create_buffers). owned for the lifetime of the buffer set and freed
|
||||
// in dispose_buffers; only read by the game from its own bufferSwitch, never by us
|
||||
std::vector<std::unique_ptr<uint8_t[]>> dummy_buffers;
|
||||
|
||||
// one device output channel scaled by the volume boost in our buffer switch
|
||||
struct VolumeOutputChannel {
|
||||
void *buffers[2];
|
||||
AsioSampleType type;
|
||||
};
|
||||
|
||||
// the game's original callbacks (captured when we install our proxy set) and the proxy
|
||||
// set we hand the real driver; the realtime trampolines reach the game's buffer_switch
|
||||
// through game_callbacks regardless of which effect is active
|
||||
AsioCallbacks game_callbacks {};
|
||||
AsioCallbacks proxy_callbacks {};
|
||||
|
||||
// volume boost state, captured at create_buffers time and published to the realtime
|
||||
// thread via active_instance once fully built; untouched while the stream runs.
|
||||
// volume_active gates whether the realtime path scales any buffers
|
||||
bool volume_active = false;
|
||||
float volume_gain = 1.0f;
|
||||
long volume_buffer_size = 0;
|
||||
std::vector<VolumeOutputChannel> volume_channels;
|
||||
|
||||
// one device channel (0 or 1) fed by a source channel during stereo downmix; both
|
||||
// buffer pointers are indexed by the ASIO double-buffer index, the same as the channels
|
||||
struct DownmixCopy {
|
||||
void *dst[2];
|
||||
void *src[2];
|
||||
};
|
||||
|
||||
// stereo downmix state, captured at create_buffers time and published alongside the
|
||||
// volume state; untouched while the stream runs. downmix_active gates whether the
|
||||
// realtime path copies the selected source pair onto device channels 0/1. copies[0]
|
||||
// feeds device channel 0, copies[1] feeds device channel 1
|
||||
bool downmix_active = false;
|
||||
DownmixCopy downmix_copies[2] {};
|
||||
size_t downmix_bytes = 0;
|
||||
};
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "external/asio/asio.h"
|
||||
#include "external/asio/iasiodrv.h"
|
||||
|
||||
namespace hooks::audio::asio {
|
||||
|
||||
// returns true if a CoCreateInstance call is instantiating a registered ASIO driver.
|
||||
// ASIO hosts pass the driver CLSID as both class id and interface id; we also validate
|
||||
// it against the system's registered ASIO drivers to avoid false positives
|
||||
bool is_asio_creation(REFCLSID rclsid, REFIID riid);
|
||||
|
||||
// wrap a real ASIO driver instance, taking ownership of the supplied reference, and
|
||||
// return a proxy that forwards every call to it. 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);
|
||||
|
||||
// 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
|
||||
struct WrappedAsio final : IAsio {
|
||||
WrappedAsio(IAsio *real, REFCLSID clsid, std::string name)
|
||||
: pReal(real), clsid(clsid), driver_name(std::move(name)) {
|
||||
}
|
||||
|
||||
WrappedAsio(const WrappedAsio &) = delete;
|
||||
WrappedAsio &operator=(const WrappedAsio &) = delete;
|
||||
|
||||
virtual ~WrappedAsio();
|
||||
|
||||
// selects which source channel pair of a multichannel ASIO output reaches the device's
|
||||
// 2.0 front pair. when not None, the proxy presents the game's expected multichannel
|
||||
// layout to the host so it proceeds to create_buffers, then opens only a two-channel
|
||||
// stream on the real device and routes the selected pair onto it (see create_buffers).
|
||||
// Front is the plain "force two channel" case (forward the device's own front pair);
|
||||
// the others copy a different pair onto 0/1. assumes a standard 7.1 layout (0-indexed).
|
||||
// set once at boot, before any wrapper exists, so it needs no synchronization
|
||||
enum class StereoDownmix {
|
||||
None, // feature disabled - full multichannel passthrough
|
||||
Front, // channels 0/1 - the device front pair is forwarded as-is (no copy)
|
||||
Center, // channel 2 duplicated to both 0 and 1
|
||||
Rear, // channels 4/5 -> 0/1
|
||||
Side, // channels 6/7 -> 0/1
|
||||
};
|
||||
static StereoDownmix STEREO_DOWNMIX;
|
||||
|
||||
// true when a stereo extraction is configured, i.e. the real device should open a 2.0
|
||||
// stream and only the selected pair should reach it. the former standalone
|
||||
// FORCE_TWO_CHANNELS flag is now just the Front case of this
|
||||
static bool force_two_channels() {
|
||||
return STEREO_DOWNMIX != StereoDownmix::None;
|
||||
}
|
||||
|
||||
// some games hardcode a multichannel ASIO output and bail before create_buffers if
|
||||
// get_channels reports fewer, so we report at least this many output channels when a
|
||||
// stereo extraction is active
|
||||
static constexpr long FORCED_OUTPUT_CHANNELS = 8;
|
||||
|
||||
// maps an option string ("front", "center", "rear", "side") to a StereoDownmix value,
|
||||
// returning None for anything unrecognized
|
||||
static StereoDownmix name_to_stereo_downmix(const char *name);
|
||||
|
||||
#pragma region IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) override;
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IAsio
|
||||
AsioBool __thiscall init(void *sys_handle) override;
|
||||
void __thiscall get_driver_name(char *name) override;
|
||||
long __thiscall get_driver_version() override;
|
||||
void __thiscall get_error_message(char *string) override;
|
||||
AsioError __thiscall start() override;
|
||||
AsioError __thiscall stop() override;
|
||||
AsioError __thiscall get_channels(long *num_input_channels, long *num_output_channels) override;
|
||||
AsioError __thiscall get_latencies(long *input_latency, long *output_latency) override;
|
||||
AsioError __thiscall get_buffer_size(
|
||||
long *min_size,
|
||||
long *max_size,
|
||||
long *preferred_size,
|
||||
long *granularity) override;
|
||||
AsioError __thiscall can_sample_rate(AsioSampleRate sample_rate) override;
|
||||
AsioError __thiscall get_sample_rate(AsioSampleRate *sample_rate) override;
|
||||
AsioError __thiscall set_sample_rate(AsioSampleRate sample_rate) override;
|
||||
AsioError __thiscall get_clock_sources(ASIOClockSource *clocks, long *num_sources) override;
|
||||
AsioError __thiscall set_clock_source(long reference) override;
|
||||
AsioError __thiscall get_sample_position(ASIOSamples *s_pos, ASIOTimeStamp *t_stamp) override;
|
||||
AsioError __thiscall get_channel_info(AsioChannelInfo *info) override;
|
||||
AsioError __thiscall create_buffers(
|
||||
AsioBufferInfo *buffer_infos,
|
||||
long num_channels,
|
||||
long buffer_size,
|
||||
AsioCallbacks *callbacks) override;
|
||||
AsioError __thiscall dispose_buffers() override;
|
||||
AsioError __thiscall control_panel() override;
|
||||
AsioError __thiscall future(long selector, void *opt) override;
|
||||
AsioError __thiscall output_ready() override;
|
||||
#pragma endregion
|
||||
|
||||
// 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:
|
||||
// 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
|
||||
AsioError create_buffers_front_pair(
|
||||
AsioBufferInfo *buffer_infos,
|
||||
long num_channels,
|
||||
long buffer_size,
|
||||
AsioCallbacks *callbacks);
|
||||
|
||||
// if any post-processing effect (volume boost or stereo downmix) is active, saves the
|
||||
// game's callbacks and returns a proxy callback set (our buffer-switch trampolines) to
|
||||
// hand the real driver instead, so we can rework its output buffers after the game
|
||||
// fills them. otherwise returns the game's callbacks unchanged. called at create_buffers
|
||||
// time, before the stream starts
|
||||
AsioCallbacks *install_proxy_callbacks(AsioCallbacks *game_callbacks);
|
||||
|
||||
// records a device output channel whose buffers we scale by the volume boost. queries
|
||||
// the real driver for the channel's sample format. called at create_buffers time
|
||||
void record_volume_output_channel(const AsioBufferInfo &info);
|
||||
|
||||
// the real device's output sample format, queried from its first output channel. all
|
||||
// output channels of a device share one format, so this characterizes them all. returns
|
||||
// ASIOSTLastEntry if the device has no output channels or the query fails
|
||||
AsioSampleType device_output_sample_type();
|
||||
|
||||
// locates the destination pair (device channels 0/1) and the configured source channels
|
||||
// in the game's buffer set so the realtime path can copy the selected pair onto 0/1.
|
||||
// a no-op unless STEREO_DOWNMIX selects a non-front pair. called at create_buffers time
|
||||
void record_downmix_channels(AsioBufferInfo *buffer_infos, long num_channels, long buffer_size);
|
||||
|
||||
// publishes the captured post-process state to the realtime thread once the buffers
|
||||
// exist, making our trampolines start reworking output. called at the end of either
|
||||
// create_buffers path
|
||||
void publish_post_process(long buffer_size);
|
||||
|
||||
// detaches this instance from the realtime trampolines so they stop touching its
|
||||
// buffers. called from dispose_buffers and the destructor
|
||||
void detach_post_process();
|
||||
|
||||
// multiplies every recorded output channel's buffer for the given double-buffer index
|
||||
// by the volume boost. runs on the driver's realtime thread from our buffer switch
|
||||
void apply_output_volume(long double_buffer_index);
|
||||
|
||||
// copies the configured source channel pair onto device channels 0/1 for the given
|
||||
// double-buffer index. runs on the driver's realtime thread from our buffer switch
|
||||
void apply_downmix(long double_buffer_index);
|
||||
|
||||
// realtime-thread trampolines for the buffer-switch callbacks, handed to the real
|
||||
// driver in place of the game's; ASIO callbacks carry no user data, so they reach the
|
||||
// active wrapper through active_instance, call the game's original, then rework output.
|
||||
// the other two callbacks (sample_rate_did_change, asio_message) are forwarded as the
|
||||
// game's own pointers, so they need no trampoline
|
||||
static void __cdecl proxy_buffer_switch(long double_buffer_index, AsioBool direct_process);
|
||||
static AsioTime * __cdecl proxy_buffer_switch_time_info(
|
||||
AsioTime *params, long double_buffer_index, AsioBool direct_process);
|
||||
|
||||
// the single wrapper whose proxy callbacks are installed (ASIO is single-instance with
|
||||
// one running stream); read by the static trampolines to reach the right wrapper
|
||||
static std::atomic<WrappedAsio *> active_instance;
|
||||
|
||||
IAsio *const pReal;
|
||||
const CLSID clsid;
|
||||
|
||||
// registry name of the driver (not get_driver_name), used in our logs as a single
|
||||
// unambiguous name; constant for our lifetime
|
||||
std::string driver_name;
|
||||
|
||||
// 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
|
||||
// drops to zero
|
||||
std::atomic<ULONG> ref_count {1};
|
||||
|
||||
// throwaway double buffers handed to the channels we discard when a stereo extraction
|
||||
// is active (see create_buffers). owned for the lifetime of the buffer set and freed
|
||||
// in dispose_buffers; only read by the game from its own bufferSwitch, never by us
|
||||
std::vector<std::unique_ptr<uint8_t[]>> dummy_buffers;
|
||||
|
||||
// one device output channel scaled by the volume boost in our buffer switch
|
||||
struct VolumeOutputChannel {
|
||||
void *buffers[2];
|
||||
AsioSampleType type;
|
||||
};
|
||||
|
||||
// the game's original callbacks (captured when we install our proxy set) and the proxy
|
||||
// set we hand the real driver; the realtime trampolines reach the game's buffer_switch
|
||||
// through game_callbacks regardless of which effect is active
|
||||
AsioCallbacks game_callbacks {};
|
||||
AsioCallbacks proxy_callbacks {};
|
||||
|
||||
// volume boost state, captured at create_buffers time and published to the realtime
|
||||
// thread via active_instance once fully built; untouched while the stream runs.
|
||||
// volume_active gates whether the realtime path scales any buffers
|
||||
bool volume_active = false;
|
||||
float volume_gain = 1.0f;
|
||||
long volume_buffer_size = 0;
|
||||
std::vector<VolumeOutputChannel> volume_channels;
|
||||
|
||||
// one device channel (0 or 1) fed by a source channel during stereo downmix; both
|
||||
// buffer pointers are indexed by the ASIO double-buffer index, the same as the channels
|
||||
struct DownmixCopy {
|
||||
void *dst[2];
|
||||
void *src[2];
|
||||
};
|
||||
|
||||
// stereo downmix state, captured at create_buffers time and published alongside the
|
||||
// volume state; untouched while the stream runs. downmix_active gates whether the
|
||||
// realtime path copies the selected source pair onto device channels 0/1. copies[0]
|
||||
// feeds device channel 0, copies[1] feeds device channel 1
|
||||
bool downmix_active = false;
|
||||
DownmixCopy downmix_copies[2] {};
|
||||
size_t downmix_bytes = 0;
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <audioclient.h>
|
||||
#include <mmdeviceapi.h>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "hooks/audio/backends/mmdevice/device_enumerator.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
@@ -15,6 +16,7 @@
|
||||
#include "audio_private.h"
|
||||
#include "acm.h"
|
||||
#include "asio_proxy.h"
|
||||
#include "xact.h"
|
||||
|
||||
#ifdef _MSC_VER
|
||||
DEFINE_GUID(CLSID_MMDeviceEnumerator,
|
||||
@@ -53,9 +55,39 @@ namespace hooks::audio {
|
||||
std::string ASIO_DRIVER_NAME = "";
|
||||
bool ASIO_FORCE_UNLOAD_ON_STOP = false;
|
||||
|
||||
// private globals
|
||||
IAudioClient *CLIENT = nullptr;
|
||||
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(
|
||||
@@ -129,15 +161,24 @@ namespace hooks::audio {
|
||||
|
||||
// general hooks
|
||||
CoCreateInstance_orig = detour::iat_try("CoCreateInstance", CoCreateInstance_hook);
|
||||
if (avs::game::is_model("PAN")) {
|
||||
hooks::audio::xact::init();
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
log_info("audio", "stopping");
|
||||
if (CLIENT) {
|
||||
CLIENT->Stop();
|
||||
CLIENT->Release();
|
||||
IAudioClient *client = nullptr;
|
||||
{
|
||||
std::lock_guard lock(CLIENT_LOCK);
|
||||
CLIENT_STOPPING = true;
|
||||
client = CLIENT;
|
||||
CLIENT = nullptr;
|
||||
}
|
||||
if (client) {
|
||||
client->Stop();
|
||||
client->Release();
|
||||
}
|
||||
stop_low_latency();
|
||||
|
||||
// release the ASIO drivers we kept pinned for the process lifetime now that we are
|
||||
|
||||
@@ -14,4 +14,6 @@ namespace hooks::audio {
|
||||
extern std::mutex INITIALIZE_LOCK;
|
||||
extern bool VOLUME_HOOK_ENABLED;
|
||||
extern LowLatencyAudioClient *LOW_LATENCY_CLIENT;
|
||||
|
||||
void set_active_client(IAudioClient *client, const char *source);
|
||||
}
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <endpointvolume.h>
|
||||
|
||||
struct WrappedIAudioEndpointVolume : IAudioEndpointVolume {
|
||||
explicit WrappedIAudioEndpointVolume(IAudioEndpointVolume *orig) : pReal(orig) {}
|
||||
|
||||
WrappedIAudioEndpointVolume(const WrappedIAudioEndpointVolume &) = delete;
|
||||
WrappedIAudioEndpointVolume &operator=(const WrappedIAudioEndpointVolume &) = delete;
|
||||
|
||||
virtual ~WrappedIAudioEndpointVolume() = default;
|
||||
|
||||
#pragma region IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IAudioEndpointVolume
|
||||
HRESULT STDMETHODCALLTYPE RegisterControlChangeNotify(IAudioEndpointVolumeCallback *pNotify) override;
|
||||
HRESULT STDMETHODCALLTYPE UnregisterControlChangeNotify(IAudioEndpointVolumeCallback *pNotify) override;
|
||||
HRESULT STDMETHODCALLTYPE GetChannelCount(uint32_t *pnChannelCount) override;
|
||||
HRESULT STDMETHODCALLTYPE SetMasterVolumeLevel(float fLevelDB, LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE SetMasterVolumeLevelScalar(float fLevel, LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE GetMasterVolumeLevel(float *fLevelDB) override;
|
||||
HRESULT STDMETHODCALLTYPE GetMasterVolumeLevelScalar(float *fLevel) override;
|
||||
HRESULT STDMETHODCALLTYPE SetChannelVolumeLevel(uint32_t nChannel, float fLevelDB, LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE SetChannelVolumeLevelScalar(uint32_t nChannel, float fLevel, LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE GetChannelVolumeLevel(uint32_t nChannel, float *fLevelDB) override;
|
||||
HRESULT STDMETHODCALLTYPE GetChannelVolumeLevelScalar(uint32_t nChannel, float *fLevel) override;
|
||||
HRESULT STDMETHODCALLTYPE SetMute(WINBOOL bMute, LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE GetMute(WINBOOL *bMute) override;
|
||||
HRESULT STDMETHODCALLTYPE GetVolumeStepInfo(uint32_t *pnStep, uint32_t *pnStepCount) override;
|
||||
HRESULT STDMETHODCALLTYPE VolumeStepUp(LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE VolumeStepDown(LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE QueryHardwareSupport(DWORD *pdwHardwareSupportMask) override;
|
||||
HRESULT STDMETHODCALLTYPE GetVolumeRange(float *pflVolumeMindB, float *pflVolumeMaxdB, float *pflVolumeIncrementdB) override;
|
||||
#pragma endregion
|
||||
|
||||
private:
|
||||
IAudioEndpointVolume *const pReal;
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <endpointvolume.h>
|
||||
|
||||
struct WrappedIAudioEndpointVolume : IAudioEndpointVolume {
|
||||
explicit WrappedIAudioEndpointVolume(IAudioEndpointVolume *orig) : pReal(orig) {}
|
||||
|
||||
WrappedIAudioEndpointVolume(const WrappedIAudioEndpointVolume &) = delete;
|
||||
WrappedIAudioEndpointVolume &operator=(const WrappedIAudioEndpointVolume &) = delete;
|
||||
|
||||
virtual ~WrappedIAudioEndpointVolume() = default;
|
||||
|
||||
#pragma region IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IAudioEndpointVolume
|
||||
HRESULT STDMETHODCALLTYPE RegisterControlChangeNotify(IAudioEndpointVolumeCallback *pNotify) override;
|
||||
HRESULT STDMETHODCALLTYPE UnregisterControlChangeNotify(IAudioEndpointVolumeCallback *pNotify) override;
|
||||
HRESULT STDMETHODCALLTYPE GetChannelCount(uint32_t *pnChannelCount) override;
|
||||
HRESULT STDMETHODCALLTYPE SetMasterVolumeLevel(float fLevelDB, LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE SetMasterVolumeLevelScalar(float fLevel, LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE GetMasterVolumeLevel(float *fLevelDB) override;
|
||||
HRESULT STDMETHODCALLTYPE GetMasterVolumeLevelScalar(float *fLevel) override;
|
||||
HRESULT STDMETHODCALLTYPE SetChannelVolumeLevel(uint32_t nChannel, float fLevelDB, LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE SetChannelVolumeLevelScalar(uint32_t nChannel, float fLevel, LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE GetChannelVolumeLevel(uint32_t nChannel, float *fLevelDB) override;
|
||||
HRESULT STDMETHODCALLTYPE GetChannelVolumeLevelScalar(uint32_t nChannel, float *fLevel) override;
|
||||
HRESULT STDMETHODCALLTYPE SetMute(WINBOOL bMute, LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE GetMute(WINBOOL *bMute) override;
|
||||
HRESULT STDMETHODCALLTYPE GetVolumeStepInfo(uint32_t *pnStep, uint32_t *pnStepCount) override;
|
||||
HRESULT STDMETHODCALLTYPE VolumeStepUp(LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE VolumeStepDown(LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE QueryHardwareSupport(DWORD *pdwHardwareSupportMask) override;
|
||||
HRESULT STDMETHODCALLTYPE GetVolumeRange(float *pflVolumeMindB, float *pflVolumeMaxdB, float *pflVolumeIncrementdB) override;
|
||||
#pragma endregion
|
||||
|
||||
private:
|
||||
IAudioEndpointVolume *const pReal;
|
||||
};
|
||||
@@ -95,11 +95,6 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDevice::Activate(
|
||||
}
|
||||
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;
|
||||
if (iid == IID_IAudioClient) {
|
||||
client = wrap_audio_client(reinterpret_cast<IAudioClient *>(*ppInterface));
|
||||
@@ -107,9 +102,6 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDevice::Activate(
|
||||
client = wrap_audio_client3(reinterpret_cast<IAudioClient3 *>(*ppInterface));
|
||||
}
|
||||
*ppInterface = client;
|
||||
// persist the audio client
|
||||
hooks::audio::CLIENT = client;
|
||||
hooks::audio::CLIENT->AddRef();
|
||||
|
||||
} else if (iid == __uuidof(IAudioEndpointVolume) && hooks::audio::VOLUME_HOOK_ENABLED) {
|
||||
*ppInterface = new WrappedIAudioEndpointVolume(reinterpret_cast<IAudioEndpointVolume *>(*ppInterface));
|
||||
|
||||
@@ -1,195 +1,185 @@
|
||||
#include "null_device.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
|
||||
#include <audioclient.h>
|
||||
|
||||
#include "hooks/audio/audio.h"
|
||||
#include "hooks/audio/audio_private.h"
|
||||
#include "hooks/audio/backends/wasapi/dummy_audio_client.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
#include "null_discard_backend.h"
|
||||
|
||||
// friendly name reported by the synthetic device. must contain "Realtek" so the
|
||||
// gitadora arena device search matches it.
|
||||
static const wchar_t NULL_DEVICE_FRIENDLY_NAME[] = L"Realtek High Definition Audio";
|
||||
|
||||
// arbitrary identifier reported by the synthetic device.
|
||||
static const wchar_t NULL_DEVICE_ID[] = L"{spice2x-null-render-device}";
|
||||
|
||||
// PKEY_Device_FriendlyName, hardcoded to avoid pulling in functiondiscoverykeys_devpkey.h
|
||||
static const PROPERTYKEY PKEY_DEVICE_FRIENDLY_NAME_LOCAL = {
|
||||
{ 0xa45c254e, 0xdf1c, 0x4efd, { 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0 } },
|
||||
14
|
||||
};
|
||||
|
||||
bool null_render_device_enabled() {
|
||||
return hooks::audio::INJECT_FAKE_REALTEK_AUDIO;
|
||||
}
|
||||
|
||||
// duplicate a wide string into CoTaskMem so the caller can free it with
|
||||
// CoTaskMemFree / PropVariantClear as the COM API contract requires.
|
||||
static LPWSTR co_task_wcsdup(const wchar_t *src) {
|
||||
const size_t bytes = (wcslen(src) + 1) * sizeof(wchar_t);
|
||||
auto *dst = static_cast<LPWSTR>(CoTaskMemAlloc(bytes));
|
||||
if (dst != nullptr) {
|
||||
memcpy(dst, src, bytes);
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// minimal IPropertyStore that only answers PKEY_Device_FriendlyName.
|
||||
struct NullPropertyStore : IPropertyStore {
|
||||
std::atomic<ULONG> ref_cnt = 1;
|
||||
|
||||
virtual ~NullPropertyStore() = default;
|
||||
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override {
|
||||
if (ppvObj == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
if (riid == __uuidof(IUnknown) || riid == __uuidof(IPropertyStore)) {
|
||||
this->AddRef();
|
||||
*ppvObj = this;
|
||||
return S_OK;
|
||||
}
|
||||
*ppvObj = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE AddRef() override {
|
||||
return ++this->ref_cnt;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE Release() override {
|
||||
const ULONG refs = --this->ref_cnt;
|
||||
if (refs == 0) {
|
||||
delete this;
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE GetCount(DWORD *cProps) override {
|
||||
if (cProps == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*cProps = 1;
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE GetAt(DWORD iProp, PROPERTYKEY *pkey) override {
|
||||
if (pkey == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
if (iProp != 0) {
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
*pkey = PKEY_DEVICE_FRIENDLY_NAME_LOCAL;
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE GetValue(REFPROPERTYKEY key, PROPVARIANT *pv) override {
|
||||
if (pv == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
PropVariantInit(pv);
|
||||
if (key.fmtid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.fmtid
|
||||
&& key.pid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.pid) {
|
||||
pv->pwszVal = co_task_wcsdup(NULL_DEVICE_FRIENDLY_NAME);
|
||||
if (pv->pwszVal == nullptr) {
|
||||
return E_OUTOFMEMORY;
|
||||
}
|
||||
pv->vt = VT_LPWSTR;
|
||||
}
|
||||
// unknown keys are returned as VT_EMPTY / S_OK
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE SetValue(REFPROPERTYKEY, REFPROPVARIANT) override {
|
||||
return STG_E_ACCESSDENIED;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE Commit() override {
|
||||
return S_OK;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#pragma region IUnknown
|
||||
HRESULT STDMETHODCALLTYPE NullMMDevice::QueryInterface(REFIID riid, void **ppvObj) {
|
||||
if (ppvObj == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
if (riid == __uuidof(IUnknown) || riid == __uuidof(IMMDevice)) {
|
||||
this->AddRef();
|
||||
*ppvObj = this;
|
||||
return S_OK;
|
||||
}
|
||||
*ppvObj = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE NullMMDevice::AddRef() {
|
||||
return ++this->ref_cnt;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE NullMMDevice::Release() {
|
||||
const ULONG refs = --this->ref_cnt;
|
||||
if (refs == 0) {
|
||||
delete this;
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IMMDevice
|
||||
HRESULT STDMETHODCALLTYPE NullMMDevice::Activate(
|
||||
REFIID iid,
|
||||
DWORD,
|
||||
PROPVARIANT *,
|
||||
void **ppInterface)
|
||||
{
|
||||
if (ppInterface == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*ppInterface = nullptr;
|
||||
|
||||
log_info("audio::null", "NullMMDevice::Activate {}", guid2s(iid));
|
||||
|
||||
if (iid == IID_IAudioClient) {
|
||||
|
||||
// release any previously persisted client
|
||||
if (hooks::audio::CLIENT != nullptr) {
|
||||
hooks::audio::CLIENT->Release();
|
||||
}
|
||||
|
||||
auto *client = static_cast<IAudioClient *>(new DummyIAudioClient(new NullDiscardBackend()));
|
||||
*ppInterface = client;
|
||||
|
||||
// persist the audio client
|
||||
hooks::audio::CLIENT = client;
|
||||
hooks::audio::CLIENT->AddRef();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE NullMMDevice::OpenPropertyStore(DWORD, IPropertyStore **ppProperties) {
|
||||
if (ppProperties == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*ppProperties = new NullPropertyStore();
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE NullMMDevice::GetId(LPWSTR *ppstrId) {
|
||||
if (ppstrId == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*ppstrId = co_task_wcsdup(NULL_DEVICE_ID);
|
||||
return *ppstrId != nullptr ? S_OK : E_OUTOFMEMORY;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE NullMMDevice::GetState(DWORD *pdwState) {
|
||||
if (pdwState == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*pdwState = DEVICE_STATE_ACTIVE;
|
||||
return S_OK;
|
||||
}
|
||||
#pragma endregion
|
||||
#include "null_device.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
|
||||
#include <audioclient.h>
|
||||
|
||||
#include "hooks/audio/audio.h"
|
||||
#include "hooks/audio/audio_private.h"
|
||||
#include "hooks/audio/backends/wasapi/dummy_audio_client.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
#include "null_discard_backend.h"
|
||||
|
||||
// friendly name reported by the synthetic device. must contain "Realtek" so the
|
||||
// gitadora arena device search matches it.
|
||||
static const wchar_t NULL_DEVICE_FRIENDLY_NAME[] = L"Realtek High Definition Audio";
|
||||
|
||||
// arbitrary identifier reported by the synthetic device.
|
||||
static const wchar_t NULL_DEVICE_ID[] = L"{spice2x-null-render-device}";
|
||||
|
||||
// PKEY_Device_FriendlyName, hardcoded to avoid pulling in functiondiscoverykeys_devpkey.h
|
||||
static const PROPERTYKEY PKEY_DEVICE_FRIENDLY_NAME_LOCAL = {
|
||||
{ 0xa45c254e, 0xdf1c, 0x4efd, { 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0 } },
|
||||
14
|
||||
};
|
||||
|
||||
bool null_render_device_enabled() {
|
||||
return hooks::audio::INJECT_FAKE_REALTEK_AUDIO;
|
||||
}
|
||||
|
||||
// duplicate a wide string into CoTaskMem so the caller can free it with
|
||||
// CoTaskMemFree / PropVariantClear as the COM API contract requires.
|
||||
static LPWSTR co_task_wcsdup(const wchar_t *src) {
|
||||
const size_t bytes = (wcslen(src) + 1) * sizeof(wchar_t);
|
||||
auto *dst = static_cast<LPWSTR>(CoTaskMemAlloc(bytes));
|
||||
if (dst != nullptr) {
|
||||
memcpy(dst, src, bytes);
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// minimal IPropertyStore that only answers PKEY_Device_FriendlyName.
|
||||
struct NullPropertyStore : IPropertyStore {
|
||||
std::atomic<ULONG> ref_cnt = 1;
|
||||
|
||||
virtual ~NullPropertyStore() = default;
|
||||
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override {
|
||||
if (ppvObj == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
if (riid == __uuidof(IUnknown) || riid == __uuidof(IPropertyStore)) {
|
||||
this->AddRef();
|
||||
*ppvObj = this;
|
||||
return S_OK;
|
||||
}
|
||||
*ppvObj = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE AddRef() override {
|
||||
return ++this->ref_cnt;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE Release() override {
|
||||
const ULONG refs = --this->ref_cnt;
|
||||
if (refs == 0) {
|
||||
delete this;
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE GetCount(DWORD *cProps) override {
|
||||
if (cProps == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*cProps = 1;
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE GetAt(DWORD iProp, PROPERTYKEY *pkey) override {
|
||||
if (pkey == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
if (iProp != 0) {
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
*pkey = PKEY_DEVICE_FRIENDLY_NAME_LOCAL;
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE GetValue(REFPROPERTYKEY key, PROPVARIANT *pv) override {
|
||||
if (pv == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
PropVariantInit(pv);
|
||||
if (key.fmtid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.fmtid
|
||||
&& key.pid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.pid) {
|
||||
pv->pwszVal = co_task_wcsdup(NULL_DEVICE_FRIENDLY_NAME);
|
||||
if (pv->pwszVal == nullptr) {
|
||||
return E_OUTOFMEMORY;
|
||||
}
|
||||
pv->vt = VT_LPWSTR;
|
||||
}
|
||||
// unknown keys are returned as VT_EMPTY / S_OK
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE SetValue(REFPROPERTYKEY, REFPROPVARIANT) override {
|
||||
return STG_E_ACCESSDENIED;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE Commit() override {
|
||||
return S_OK;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#pragma region IUnknown
|
||||
HRESULT STDMETHODCALLTYPE NullMMDevice::QueryInterface(REFIID riid, void **ppvObj) {
|
||||
if (ppvObj == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
if (riid == __uuidof(IUnknown) || riid == __uuidof(IMMDevice)) {
|
||||
this->AddRef();
|
||||
*ppvObj = this;
|
||||
return S_OK;
|
||||
}
|
||||
*ppvObj = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE NullMMDevice::AddRef() {
|
||||
return ++this->ref_cnt;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE NullMMDevice::Release() {
|
||||
const ULONG refs = --this->ref_cnt;
|
||||
if (refs == 0) {
|
||||
delete this;
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IMMDevice
|
||||
HRESULT STDMETHODCALLTYPE NullMMDevice::Activate(
|
||||
REFIID iid,
|
||||
DWORD,
|
||||
PROPVARIANT *,
|
||||
void **ppInterface)
|
||||
{
|
||||
if (ppInterface == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*ppInterface = nullptr;
|
||||
|
||||
log_info("audio::null", "NullMMDevice::Activate {}", guid2s(iid));
|
||||
|
||||
if (iid == IID_IAudioClient) {
|
||||
auto *client = static_cast<IAudioClient *>(new DummyIAudioClient(new NullDiscardBackend()));
|
||||
*ppInterface = client;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE NullMMDevice::OpenPropertyStore(DWORD, IPropertyStore **ppProperties) {
|
||||
if (ppProperties == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*ppProperties = new NullPropertyStore();
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE NullMMDevice::GetId(LPWSTR *ppstrId) {
|
||||
if (ppstrId == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*ppstrId = co_task_wcsdup(NULL_DEVICE_ID);
|
||||
return *ppstrId != nullptr ? S_OK : E_OUTOFMEMORY;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE NullMMDevice::GetState(DWORD *pdwState) {
|
||||
if (pdwState == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
*pdwState = DEVICE_STATE_ACTIVE;
|
||||
return S_OK;
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include <mmdeviceapi.h>
|
||||
|
||||
// returns true when a synthetic render endpoint should be injected into device
|
||||
// enumeration. games like gitadora arena search the render endpoint list for a
|
||||
// device whose friendly name contains "Realtek" and crash with a null pointer
|
||||
// dereference when no match exists. presenting a fake match that routes to the
|
||||
// null audio backend lets the search succeed while discarding the audio.
|
||||
bool null_render_device_enabled();
|
||||
|
||||
// fake IMMDevice that reports a "Realtek" friendly name and activates straight
|
||||
// into the null audio backend, never touching real hardware.
|
||||
struct NullMMDevice : IMMDevice {
|
||||
NullMMDevice() = default;
|
||||
|
||||
NullMMDevice(const NullMMDevice &) = delete;
|
||||
NullMMDevice &operator=(const NullMMDevice &) = delete;
|
||||
|
||||
virtual ~NullMMDevice() = default;
|
||||
|
||||
#pragma region IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IMMDevice
|
||||
HRESULT STDMETHODCALLTYPE Activate(REFIID iid, DWORD dwClsCtx, PROPVARIANT *pActivationParams, void **ppInterface) override;
|
||||
HRESULT STDMETHODCALLTYPE OpenPropertyStore(DWORD stgmAccess, IPropertyStore **ppProperties) override;
|
||||
HRESULT STDMETHODCALLTYPE GetId(LPWSTR *ppstrId) override;
|
||||
HRESULT STDMETHODCALLTYPE GetState(DWORD *pdwState) override;
|
||||
#pragma endregion
|
||||
|
||||
private:
|
||||
std::atomic<ULONG> ref_cnt = 1;
|
||||
};
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include <mmdeviceapi.h>
|
||||
|
||||
// returns true when a synthetic render endpoint should be injected into device
|
||||
// enumeration. games like gitadora arena search the render endpoint list for a
|
||||
// device whose friendly name contains "Realtek" and crash with a null pointer
|
||||
// dereference when no match exists. presenting a fake match that routes to the
|
||||
// null audio backend lets the search succeed while discarding the audio.
|
||||
bool null_render_device_enabled();
|
||||
|
||||
// fake IMMDevice that reports a "Realtek" friendly name and activates straight
|
||||
// into the null audio backend, never touching real hardware.
|
||||
struct NullMMDevice : IMMDevice {
|
||||
NullMMDevice() = default;
|
||||
|
||||
NullMMDevice(const NullMMDevice &) = delete;
|
||||
NullMMDevice &operator=(const NullMMDevice &) = delete;
|
||||
|
||||
virtual ~NullMMDevice() = default;
|
||||
|
||||
#pragma region IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IMMDevice
|
||||
HRESULT STDMETHODCALLTYPE Activate(REFIID iid, DWORD dwClsCtx, PROPVARIANT *pActivationParams, void **ppInterface) override;
|
||||
HRESULT STDMETHODCALLTYPE OpenPropertyStore(DWORD stgmAccess, IPropertyStore **ppProperties) override;
|
||||
HRESULT STDMETHODCALLTYPE GetId(LPWSTR *ppstrId) override;
|
||||
HRESULT STDMETHODCALLTYPE GetState(DWORD *pdwState) override;
|
||||
#pragma endregion
|
||||
|
||||
private:
|
||||
std::atomic<ULONG> ref_cnt = 1;
|
||||
};
|
||||
|
||||
@@ -1,141 +1,139 @@
|
||||
#include "null_discard_backend.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
|
||||
#include "hooks/audio/util.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
|
||||
NullDiscardBackend::~NullDiscardBackend() {
|
||||
this->running = false;
|
||||
if (this->pacing_thread.joinable()) {
|
||||
this->pacing_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
const WAVEFORMATEXTENSIBLE &NullDiscardBackend::format() const noexcept {
|
||||
return this->format_;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_initialize(
|
||||
AUDCLNT_SHAREMODE *,
|
||||
DWORD *,
|
||||
REFERENCE_TIME *hnsBufferDuration,
|
||||
REFERENCE_TIME *,
|
||||
const WAVEFORMATEX *pFormat,
|
||||
LPCGUID)
|
||||
{
|
||||
copy_wave_format(&this->format_, pFormat);
|
||||
|
||||
// honor the game's requested buffer duration, falling back to 10 ms
|
||||
constexpr REFERENCE_TIME DEFAULT_REFTIME = 100000; // 10 ms in 100-ns units
|
||||
this->period_reftime = (hnsBufferDuration && *hnsBufferDuration > 0)
|
||||
? *hnsBufferDuration
|
||||
: DEFAULT_REFTIME;
|
||||
|
||||
this->buffer_frames = std::max<uint32_t>(1, static_cast<uint32_t>(
|
||||
static_cast<double>(this->format_.Format.nSamplesPerSec)
|
||||
* this->period_reftime / 10000000.0 + 0.5));
|
||||
|
||||
log_info("audio::null", "initializing null render device with {} channels, {} Hz, {}-bit",
|
||||
this->format_.Format.nChannels,
|
||||
this->format_.Format.nSamplesPerSec,
|
||||
this->format_.Format.wBitsPerSample);
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_get_buffer_size(uint32_t *buffer_frames) {
|
||||
*buffer_frames = this->buffer_frames;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_get_stream_latency(REFERENCE_TIME *latency) {
|
||||
*latency = this->period_reftime;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_get_current_padding(std::optional<uint32_t> &padding_frames) {
|
||||
// discarded immediately, so the buffer always reads as fully drained
|
||||
padding_frames = 0;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_is_format_supported(
|
||||
AUDCLNT_SHAREMODE *,
|
||||
const WAVEFORMATEX *,
|
||||
WAVEFORMATEX **ppClosestMatch)
|
||||
{
|
||||
if (ppClosestMatch) {
|
||||
*ppClosestMatch = nullptr;
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_get_mix_format(WAVEFORMATEX **) {
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_get_device_period(
|
||||
REFERENCE_TIME *default_device_period,
|
||||
REFERENCE_TIME *minimum_device_period)
|
||||
{
|
||||
if (default_device_period) {
|
||||
*default_device_period = this->period_reftime;
|
||||
}
|
||||
if (minimum_device_period) {
|
||||
*minimum_device_period = this->period_reftime;
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_start() {
|
||||
if (!this->running.exchange(true)) {
|
||||
this->pacing_thread = std::thread(&NullDiscardBackend::pace_loop, this);
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_stop() {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_set_event_handle(HANDLE *event_handle) {
|
||||
// keep the game's event so pace_loop() can wake it; there is no real device behind it
|
||||
this->relay_handle = *event_handle;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) {
|
||||
const size_t buffer_size =
|
||||
static_cast<size_t>(this->format_.Format.nBlockAlign) * num_frames_requested;
|
||||
if (this->scratch.size() < buffer_size) {
|
||||
this->scratch.resize(buffer_size);
|
||||
}
|
||||
*ppData = this->scratch.data();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_release_buffer(uint32_t, DWORD) {
|
||||
// discard the audio entirely
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
void NullDiscardBackend::pace_loop() {
|
||||
using namespace std::chrono;
|
||||
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
// audio is discarded, so timing precision and drift do not matter; just wake the
|
||||
// game once per buffer period to keep its render thread from blocking on the event.
|
||||
const auto period = duration_cast<steady_clock::duration>(
|
||||
duration<double>(this->period_reftime / 10000000.0));
|
||||
|
||||
while (this->running.load()) {
|
||||
if (this->relay_handle) {
|
||||
SetEvent(this->relay_handle);
|
||||
}
|
||||
timer.sleep(period);
|
||||
}
|
||||
}
|
||||
#include "null_discard_backend.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include "hooks/audio/util.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
NullDiscardBackend::~NullDiscardBackend() {
|
||||
this->running = false;
|
||||
if (this->pacing_thread.joinable()) {
|
||||
this->pacing_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
const WAVEFORMATEXTENSIBLE &NullDiscardBackend::format() const noexcept {
|
||||
return this->format_;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_initialize(
|
||||
AUDCLNT_SHAREMODE *,
|
||||
DWORD *,
|
||||
REFERENCE_TIME *hnsBufferDuration,
|
||||
REFERENCE_TIME *,
|
||||
const WAVEFORMATEX *pFormat,
|
||||
LPCGUID)
|
||||
{
|
||||
copy_wave_format(&this->format_, pFormat);
|
||||
|
||||
// honor the game's requested buffer duration, falling back to 10 ms
|
||||
constexpr REFERENCE_TIME DEFAULT_REFTIME = 100000; // 10 ms in 100-ns units
|
||||
this->period_reftime = (hnsBufferDuration && *hnsBufferDuration > 0)
|
||||
? *hnsBufferDuration
|
||||
: DEFAULT_REFTIME;
|
||||
|
||||
this->buffer_frames = std::max<uint32_t>(1, static_cast<uint32_t>(
|
||||
static_cast<double>(this->format_.Format.nSamplesPerSec)
|
||||
* this->period_reftime / 10000000.0 + 0.5));
|
||||
|
||||
log_info("audio::null", "initializing null render device with {} channels, {} Hz, {}-bit",
|
||||
this->format_.Format.nChannels,
|
||||
this->format_.Format.nSamplesPerSec,
|
||||
this->format_.Format.wBitsPerSample);
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_get_buffer_size(uint32_t *buffer_frames) {
|
||||
*buffer_frames = this->buffer_frames;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_get_stream_latency(REFERENCE_TIME *latency) {
|
||||
*latency = this->period_reftime;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_get_current_padding(std::optional<uint32_t> &padding_frames) {
|
||||
// discarded immediately, so the buffer always reads as fully drained
|
||||
padding_frames = 0;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_is_format_supported(
|
||||
AUDCLNT_SHAREMODE *,
|
||||
const WAVEFORMATEX *,
|
||||
WAVEFORMATEX **ppClosestMatch)
|
||||
{
|
||||
if (ppClosestMatch) {
|
||||
*ppClosestMatch = nullptr;
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_get_mix_format(WAVEFORMATEX **) {
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_get_device_period(
|
||||
REFERENCE_TIME *default_device_period,
|
||||
REFERENCE_TIME *minimum_device_period)
|
||||
{
|
||||
if (default_device_period) {
|
||||
*default_device_period = this->period_reftime;
|
||||
}
|
||||
if (minimum_device_period) {
|
||||
*minimum_device_period = this->period_reftime;
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_start() {
|
||||
if (!this->running.exchange(true)) {
|
||||
this->pacing_thread = std::thread(&NullDiscardBackend::pace_loop, this);
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_stop() {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_set_event_handle(HANDLE *event_handle) {
|
||||
// keep the game's event so pace_loop() can wake it; there is no real device behind it
|
||||
this->relay_handle = *event_handle;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) {
|
||||
const size_t buffer_size =
|
||||
static_cast<size_t>(this->format_.Format.nBlockAlign) * num_frames_requested;
|
||||
if (this->scratch.size() < buffer_size) {
|
||||
this->scratch.resize(buffer_size);
|
||||
}
|
||||
*ppData = this->scratch.data();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT NullDiscardBackend::on_release_buffer(uint32_t, DWORD) {
|
||||
// discard the audio entirely
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
void NullDiscardBackend::pace_loop() {
|
||||
using namespace std::chrono;
|
||||
|
||||
// audio is discarded, so timing precision and drift do not matter; just wake the
|
||||
// game once per buffer period to keep its render thread from blocking on the event.
|
||||
const auto period = duration_cast<steady_clock::duration>(
|
||||
duration<double>(this->period_reftime / 10000000.0));
|
||||
|
||||
while (this->running.load()) {
|
||||
if (this->relay_handle) {
|
||||
SetEvent(this->relay_handle);
|
||||
}
|
||||
std::this_thread::sleep_for(period);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <audioclient.h>
|
||||
|
||||
#include "hooks/audio/implementations/backend.h"
|
||||
|
||||
// discards all audio while pacing the game's event handle once per buffer period, so the game
|
||||
// keeps running normally with nothing output to any real device. routed through the shared
|
||||
// DummyIAudioClient, the same plumbing the asio backend uses.
|
||||
struct NullDiscardBackend final : AudioBackend {
|
||||
~NullDiscardBackend() final;
|
||||
|
||||
const WAVEFORMATEXTENSIBLE &format() const noexcept override;
|
||||
|
||||
HRESULT on_initialize(
|
||||
AUDCLNT_SHAREMODE *,
|
||||
DWORD *,
|
||||
REFERENCE_TIME *hnsBufferDuration,
|
||||
REFERENCE_TIME *,
|
||||
const WAVEFORMATEX *pFormat,
|
||||
LPCGUID) override;
|
||||
HRESULT on_get_buffer_size(uint32_t *buffer_frames) override;
|
||||
HRESULT on_get_stream_latency(REFERENCE_TIME *latency) override;
|
||||
HRESULT on_get_current_padding(std::optional<uint32_t> &padding_frames) override;
|
||||
HRESULT on_is_format_supported(
|
||||
AUDCLNT_SHAREMODE *,
|
||||
const WAVEFORMATEX *,
|
||||
WAVEFORMATEX **ppClosestMatch) override;
|
||||
HRESULT on_get_mix_format(WAVEFORMATEX **) override;
|
||||
HRESULT on_get_device_period(
|
||||
REFERENCE_TIME *default_device_period,
|
||||
REFERENCE_TIME *minimum_device_period) override;
|
||||
HRESULT on_start() override;
|
||||
HRESULT on_stop() override;
|
||||
HRESULT on_set_event_handle(HANDLE *event_handle) override;
|
||||
HRESULT on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) override;
|
||||
HRESULT on_release_buffer(uint32_t, DWORD) override;
|
||||
|
||||
private:
|
||||
void pace_loop();
|
||||
|
||||
WAVEFORMATEXTENSIBLE format_ {};
|
||||
uint32_t buffer_frames = 0;
|
||||
REFERENCE_TIME period_reftime = 0;
|
||||
HANDLE relay_handle = nullptr;
|
||||
std::vector<BYTE> scratch;
|
||||
std::thread pacing_thread;
|
||||
std::atomic<bool> running = false;
|
||||
};
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <audioclient.h>
|
||||
|
||||
#include "hooks/audio/implementations/backend.h"
|
||||
|
||||
// discards all audio while pacing the game's event handle once per buffer period, so the game
|
||||
// keeps running normally with nothing output to any real device. routed through the shared
|
||||
// DummyIAudioClient, the same plumbing the asio backend uses.
|
||||
struct NullDiscardBackend final : AudioBackend {
|
||||
~NullDiscardBackend() final;
|
||||
|
||||
const WAVEFORMATEXTENSIBLE &format() const noexcept override;
|
||||
|
||||
HRESULT on_initialize(
|
||||
AUDCLNT_SHAREMODE *,
|
||||
DWORD *,
|
||||
REFERENCE_TIME *hnsBufferDuration,
|
||||
REFERENCE_TIME *,
|
||||
const WAVEFORMATEX *pFormat,
|
||||
LPCGUID) override;
|
||||
HRESULT on_get_buffer_size(uint32_t *buffer_frames) override;
|
||||
HRESULT on_get_stream_latency(REFERENCE_TIME *latency) override;
|
||||
HRESULT on_get_current_padding(std::optional<uint32_t> &padding_frames) override;
|
||||
HRESULT on_is_format_supported(
|
||||
AUDCLNT_SHAREMODE *,
|
||||
const WAVEFORMATEX *,
|
||||
WAVEFORMATEX **ppClosestMatch) override;
|
||||
HRESULT on_get_mix_format(WAVEFORMATEX **) override;
|
||||
HRESULT on_get_device_period(
|
||||
REFERENCE_TIME *default_device_period,
|
||||
REFERENCE_TIME *minimum_device_period) override;
|
||||
HRESULT on_start() override;
|
||||
HRESULT on_stop() override;
|
||||
HRESULT on_set_event_handle(HANDLE *event_handle) override;
|
||||
HRESULT on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) override;
|
||||
HRESULT on_release_buffer(uint32_t, DWORD) override;
|
||||
|
||||
private:
|
||||
void pace_loop();
|
||||
|
||||
WAVEFORMATEXTENSIBLE format_ {};
|
||||
uint32_t buffer_frames = 0;
|
||||
REFERENCE_TIME period_reftime = 0;
|
||||
HANDLE relay_handle = nullptr;
|
||||
std::vector<BYTE> scratch;
|
||||
std::thread pacing_thread;
|
||||
std::atomic<bool> running = false;
|
||||
};
|
||||
|
||||
@@ -302,6 +302,7 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
|
||||
device_format->nChannels * (device_format->wBitsPerSample / 8));
|
||||
}
|
||||
|
||||
hooks::audio::set_active_client(this, "WrappedIAudioClient::Initialize");
|
||||
return ret;
|
||||
}
|
||||
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));
|
||||
copy_wave_format(&hooks::audio::FORMAT, pFormat);
|
||||
copy_wave_format(&this->device_format, pFormat);
|
||||
hooks::audio::set_active_client(this, "WrappedIAudioClient::InitializeSharedAudioStream");
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -1,264 +1,264 @@
|
||||
#include "downmix.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include <audioclient.h>
|
||||
#include <ks.h>
|
||||
#include <ksmedia.h>
|
||||
|
||||
#include "util/logging.h"
|
||||
|
||||
#include "util.h"
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float ATT_3DB = 0.70710678f;
|
||||
|
||||
// speakers routed to the left/right output; anything else (center) feeds both sides
|
||||
constexpr DWORD LEFT_SPEAKERS = SPEAKER_FRONT_LEFT | SPEAKER_BACK_LEFT | SPEAKER_SIDE_LEFT
|
||||
| SPEAKER_FRONT_LEFT_OF_CENTER | SPEAKER_TOP_FRONT_LEFT | SPEAKER_TOP_BACK_LEFT;
|
||||
constexpr DWORD RIGHT_SPEAKERS = SPEAKER_FRONT_RIGHT | SPEAKER_BACK_RIGHT | SPEAKER_SIDE_RIGHT
|
||||
| SPEAKER_FRONT_RIGHT_OF_CENTER | SPEAKER_TOP_FRONT_RIGHT | SPEAKER_TOP_BACK_RIGHT;
|
||||
|
||||
// the speaker mask is only present on WAVE_FORMAT_EXTENSIBLE formats
|
||||
DWORD read_channel_mask(const WAVEFORMATEX *fmt) {
|
||||
if (fmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE
|
||||
&& fmt->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
|
||||
return reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(fmt)->dwChannelMask;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// call visit(channel_index, speaker_bit) for each present speaker, in channel order
|
||||
template <typename F>
|
||||
void for_each_speaker(DWORD mask, int channels, F &&visit) {
|
||||
int channel = 0;
|
||||
for (int bit = 0; bit < 18 && channel < channels; bit++) {
|
||||
const DWORD speaker = 1u << bit;
|
||||
if (mask & speaker) {
|
||||
visit(channel++, speaker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Downmix::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out,
|
||||
DownmixAlgorithm algorithm) {
|
||||
this->enabled = true;
|
||||
this->algorithm = algorithm;
|
||||
this->bytes_per_sample = game_format->wBitsPerSample / 8;
|
||||
this->game_frame_size = game_format->nChannels * this->bytes_per_sample;
|
||||
|
||||
this->is_float = is_ieee_float(game_format);
|
||||
|
||||
// supported: 16/24/32-bit integer PCM and 32-bit float; anything else mixes to silence
|
||||
const bool supported = this->is_float
|
||||
? this->bytes_per_sample == 4
|
||||
: (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4);
|
||||
if (!supported) {
|
||||
log_fatal(
|
||||
"audio::downmix",
|
||||
"unsupported sample format ({}-bit {}), downmix will output silence",
|
||||
game_format->wBitsPerSample, this->is_float ? "float" : "int");
|
||||
}
|
||||
|
||||
this->left_mix.clear();
|
||||
this->right_mix.clear();
|
||||
this->build_layout_mix(game_format);
|
||||
|
||||
make_stereo_format(game_format, stereo_out);
|
||||
}
|
||||
|
||||
void Downmix::make_stereo_format(const WAVEFORMATEX *game_format,
|
||||
WAVEFORMATEXTENSIBLE *stereo_out) {
|
||||
const int bytes_per_sample = game_format->wBitsPerSample / 8;
|
||||
|
||||
memcpy(stereo_out, game_format, sizeof(WAVEFORMATEXTENSIBLE));
|
||||
stereo_out->Format.nChannels = 2;
|
||||
stereo_out->Format.nBlockAlign = 2 * bytes_per_sample;
|
||||
stereo_out->Format.nAvgBytesPerSec =
|
||||
game_format->nSamplesPerSec * stereo_out->Format.nBlockAlign;
|
||||
stereo_out->dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
|
||||
}
|
||||
|
||||
HRESULT Downmix::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
|
||||
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
|
||||
const WAVEFORMATEX *device_format, LPCGUID session_guid) {
|
||||
|
||||
// the smaller stereo buffer can end up unaligned for the device when the game sized the
|
||||
// duration for its larger multi-channel format; the helper recovers from that.
|
||||
return initialize_with_alignment_retry(real, "audio::downmix", share_mode, stream_flags,
|
||||
buffer_duration, periodicity, device_format, session_guid);
|
||||
}
|
||||
|
||||
void Downmix::add_channel(int channel, DWORD speaker, float gain) {
|
||||
if (speaker & LEFT_SPEAKERS) {
|
||||
this->left_mix.push_back({ channel, gain });
|
||||
} else if (speaker & RIGHT_SPEAKERS) {
|
||||
this->right_mix.push_back({ channel, gain });
|
||||
} else { // center: feed both sides
|
||||
this->left_mix.push_back({ channel, gain });
|
||||
this->right_mix.push_back({ channel, gain });
|
||||
}
|
||||
}
|
||||
|
||||
// AC-4 stereo downmix (ETSI TS 103 190-1): front pair at unity, everything else -3 dB, LFE dropped
|
||||
void Downmix::build_ac4_mix(DWORD mask, int channels) {
|
||||
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
|
||||
if (speaker == SPEAKER_LOW_FREQUENCY) {
|
||||
return;
|
||||
}
|
||||
const bool front_pair = speaker & (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT);
|
||||
this->add_channel(ch, speaker, front_pair ? 1.0f : ATT_3DB);
|
||||
});
|
||||
}
|
||||
|
||||
// keep only the channels in `keep` (front/rear/side), each at unity gain
|
||||
void Downmix::build_extract_mix(DWORD mask, int channels, DWORD keep) {
|
||||
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
|
||||
if (speaker & keep) {
|
||||
this->add_channel(ch, speaker, 1.0f);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// keep every channel (LFE dropped), then average each side so its gains sum to unity
|
||||
void Downmix::build_normalize_mix(DWORD mask, int channels) {
|
||||
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
|
||||
if (speaker != SPEAKER_LOW_FREQUENCY) {
|
||||
this->add_channel(ch, speaker, 1.0f);
|
||||
}
|
||||
});
|
||||
|
||||
for (auto *mix : { &this->left_mix, &this->right_mix }) {
|
||||
if (!mix->empty()) {
|
||||
const float gain = 1.0f / mix->size();
|
||||
for (auto &c : *mix) {
|
||||
c.gain = gain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback when no speaker mask is present: fold interleaved L/R pairs (even->left, odd->right)
|
||||
void Downmix::build_pairs_mix(int channels, float gain) {
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
(((ch & 1) == 0) ? this->left_mix : this->right_mix).push_back({ ch, gain });
|
||||
}
|
||||
}
|
||||
|
||||
void Downmix::build_layout_mix(const WAVEFORMATEX *game_format) {
|
||||
const int channels = game_format->nChannels;
|
||||
const DWORD mask = read_channel_mask(game_format);
|
||||
|
||||
// without a mask the layout is unknown: extract/normalize have nothing to act on, so all
|
||||
// algorithms fall back to folding L/R pairs (AC-4 still attenuates by -3 dB)
|
||||
if (mask == 0) {
|
||||
this->build_pairs_mix(channels,
|
||||
this->algorithm == DownmixAlgorithm::AC4 ? ATT_3DB : 1.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this->algorithm) {
|
||||
case DownmixAlgorithm::FrontOnly:
|
||||
this->build_extract_mix(mask, channels,
|
||||
SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT);
|
||||
break;
|
||||
case DownmixAlgorithm::RearOnly:
|
||||
this->build_extract_mix(mask, channels,
|
||||
SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | SPEAKER_BACK_CENTER);
|
||||
break;
|
||||
case DownmixAlgorithm::SideOnly:
|
||||
this->build_extract_mix(mask, channels,
|
||||
SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT);
|
||||
break;
|
||||
case DownmixAlgorithm::Normalize:
|
||||
this->build_normalize_mix(mask, channels);
|
||||
break;
|
||||
case DownmixAlgorithm::AC4:
|
||||
this->build_ac4_mix(mask, channels);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Downmix::process(BYTE *dst, const BYTE *src, UINT32 frames) const {
|
||||
const int bps = this->bytes_per_sample;
|
||||
const int src_stride = this->game_frame_size;
|
||||
const int dst_stride = 2 * bps;
|
||||
|
||||
if (dst == nullptr || src == nullptr || bps <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// sum each speaker's source channels into the matching stereo output
|
||||
for (UINT32 i = 0; i < frames; i++) {
|
||||
const BYTE *in = src + (size_t) i * src_stride;
|
||||
BYTE *out = dst + (size_t) i * dst_stride;
|
||||
|
||||
float left = 0.0f;
|
||||
float right = 0.0f;
|
||||
for (const auto &c : this->left_mix) {
|
||||
left += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain;
|
||||
}
|
||||
for (const auto &c : this->right_mix) {
|
||||
right += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain;
|
||||
}
|
||||
write_sample(out, bps, this->is_float, left);
|
||||
write_sample(out + bps, bps, this->is_float, right);
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT Downmix::get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData) {
|
||||
const size_t needed = (size_t) frames * this->game_frame_size;
|
||||
if (this->scratch.size() < needed) {
|
||||
this->scratch.resize(needed);
|
||||
}
|
||||
|
||||
HRESULT ret = real->GetBuffer(frames, &this->device_buffer);
|
||||
if (FAILED(ret)) {
|
||||
this->device_buffer = nullptr;
|
||||
return ret;
|
||||
}
|
||||
|
||||
*ppData = this->scratch.data();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT Downmix::get_scratch(UINT32 frames, BYTE **ppData) {
|
||||
const size_t needed = (size_t) frames * this->game_frame_size;
|
||||
if (this->scratch.size() < needed) {
|
||||
this->scratch.resize(needed);
|
||||
}
|
||||
|
||||
*ppData = this->scratch.data();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
void Downmix::downmix_into(BYTE *dst, UINT32 frames) const {
|
||||
this->process(dst, this->scratch.data(), frames);
|
||||
}
|
||||
|
||||
void Downmix::write_device_buffer(UINT32 frames, DWORD flags) {
|
||||
const int bps = this->bytes_per_sample;
|
||||
const int dst_stride = 2 * bps;
|
||||
|
||||
if (this->device_buffer == nullptr || frames == 0 || bps <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// mute the first few buffers to avoid a pop on stream start
|
||||
if (this->buffers_to_mute > 0) {
|
||||
memset(this->device_buffer, 0, (size_t) frames * dst_stride);
|
||||
this->buffers_to_mute--;
|
||||
} else if ((flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
|
||||
this->process(this->device_buffer, this->scratch.data(), frames);
|
||||
}
|
||||
}
|
||||
}
|
||||
#include "downmix.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include <audioclient.h>
|
||||
#include <ks.h>
|
||||
#include <ksmedia.h>
|
||||
|
||||
#include "util/logging.h"
|
||||
|
||||
#include "util.h"
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float ATT_3DB = 0.70710678f;
|
||||
|
||||
// speakers routed to the left/right output; anything else (center) feeds both sides
|
||||
constexpr DWORD LEFT_SPEAKERS = SPEAKER_FRONT_LEFT | SPEAKER_BACK_LEFT | SPEAKER_SIDE_LEFT
|
||||
| SPEAKER_FRONT_LEFT_OF_CENTER | SPEAKER_TOP_FRONT_LEFT | SPEAKER_TOP_BACK_LEFT;
|
||||
constexpr DWORD RIGHT_SPEAKERS = SPEAKER_FRONT_RIGHT | SPEAKER_BACK_RIGHT | SPEAKER_SIDE_RIGHT
|
||||
| SPEAKER_FRONT_RIGHT_OF_CENTER | SPEAKER_TOP_FRONT_RIGHT | SPEAKER_TOP_BACK_RIGHT;
|
||||
|
||||
// the speaker mask is only present on WAVE_FORMAT_EXTENSIBLE formats
|
||||
DWORD read_channel_mask(const WAVEFORMATEX *fmt) {
|
||||
if (fmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE
|
||||
&& fmt->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
|
||||
return reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(fmt)->dwChannelMask;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// call visit(channel_index, speaker_bit) for each present speaker, in channel order
|
||||
template <typename F>
|
||||
void for_each_speaker(DWORD mask, int channels, F &&visit) {
|
||||
int channel = 0;
|
||||
for (int bit = 0; bit < 18 && channel < channels; bit++) {
|
||||
const DWORD speaker = 1u << bit;
|
||||
if (mask & speaker) {
|
||||
visit(channel++, speaker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Downmix::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out,
|
||||
DownmixAlgorithm algorithm) {
|
||||
this->enabled = true;
|
||||
this->algorithm = algorithm;
|
||||
this->bytes_per_sample = game_format->wBitsPerSample / 8;
|
||||
this->game_frame_size = game_format->nChannels * this->bytes_per_sample;
|
||||
|
||||
this->is_float = is_ieee_float(game_format);
|
||||
|
||||
// supported: 16/24/32-bit integer PCM and 32-bit float; anything else mixes to silence
|
||||
const bool supported = this->is_float
|
||||
? this->bytes_per_sample == 4
|
||||
: (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4);
|
||||
if (!supported) {
|
||||
log_fatal(
|
||||
"audio::downmix",
|
||||
"unsupported sample format ({}-bit {}), downmix will output silence",
|
||||
game_format->wBitsPerSample, this->is_float ? "float" : "int");
|
||||
}
|
||||
|
||||
this->left_mix.clear();
|
||||
this->right_mix.clear();
|
||||
this->build_layout_mix(game_format);
|
||||
|
||||
make_stereo_format(game_format, stereo_out);
|
||||
}
|
||||
|
||||
void Downmix::make_stereo_format(const WAVEFORMATEX *game_format,
|
||||
WAVEFORMATEXTENSIBLE *stereo_out) {
|
||||
const int bytes_per_sample = game_format->wBitsPerSample / 8;
|
||||
|
||||
memcpy(stereo_out, game_format, sizeof(WAVEFORMATEXTENSIBLE));
|
||||
stereo_out->Format.nChannels = 2;
|
||||
stereo_out->Format.nBlockAlign = 2 * bytes_per_sample;
|
||||
stereo_out->Format.nAvgBytesPerSec =
|
||||
game_format->nSamplesPerSec * stereo_out->Format.nBlockAlign;
|
||||
stereo_out->dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
|
||||
}
|
||||
|
||||
HRESULT Downmix::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
|
||||
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
|
||||
const WAVEFORMATEX *device_format, LPCGUID session_guid) {
|
||||
|
||||
// the smaller stereo buffer can end up unaligned for the device when the game sized the
|
||||
// duration for its larger multi-channel format; the helper recovers from that.
|
||||
return initialize_with_alignment_retry(real, "audio::downmix", share_mode, stream_flags,
|
||||
buffer_duration, periodicity, device_format, session_guid);
|
||||
}
|
||||
|
||||
void Downmix::add_channel(int channel, DWORD speaker, float gain) {
|
||||
if (speaker & LEFT_SPEAKERS) {
|
||||
this->left_mix.push_back({ channel, gain });
|
||||
} else if (speaker & RIGHT_SPEAKERS) {
|
||||
this->right_mix.push_back({ channel, gain });
|
||||
} else { // center: feed both sides
|
||||
this->left_mix.push_back({ channel, gain });
|
||||
this->right_mix.push_back({ channel, gain });
|
||||
}
|
||||
}
|
||||
|
||||
// AC-4 stereo downmix (ETSI TS 103 190-1): front pair at unity, everything else -3 dB, LFE dropped
|
||||
void Downmix::build_ac4_mix(DWORD mask, int channels) {
|
||||
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
|
||||
if (speaker == SPEAKER_LOW_FREQUENCY) {
|
||||
return;
|
||||
}
|
||||
const bool front_pair = speaker & (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT);
|
||||
this->add_channel(ch, speaker, front_pair ? 1.0f : ATT_3DB);
|
||||
});
|
||||
}
|
||||
|
||||
// keep only the channels in `keep` (front/rear/side), each at unity gain
|
||||
void Downmix::build_extract_mix(DWORD mask, int channels, DWORD keep) {
|
||||
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
|
||||
if (speaker & keep) {
|
||||
this->add_channel(ch, speaker, 1.0f);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// keep every channel (LFE dropped), then average each side so its gains sum to unity
|
||||
void Downmix::build_normalize_mix(DWORD mask, int channels) {
|
||||
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
|
||||
if (speaker != SPEAKER_LOW_FREQUENCY) {
|
||||
this->add_channel(ch, speaker, 1.0f);
|
||||
}
|
||||
});
|
||||
|
||||
for (auto *mix : { &this->left_mix, &this->right_mix }) {
|
||||
if (!mix->empty()) {
|
||||
const float gain = 1.0f / mix->size();
|
||||
for (auto &c : *mix) {
|
||||
c.gain = gain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback when no speaker mask is present: fold interleaved L/R pairs (even->left, odd->right)
|
||||
void Downmix::build_pairs_mix(int channels, float gain) {
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
(((ch & 1) == 0) ? this->left_mix : this->right_mix).push_back({ ch, gain });
|
||||
}
|
||||
}
|
||||
|
||||
void Downmix::build_layout_mix(const WAVEFORMATEX *game_format) {
|
||||
const int channels = game_format->nChannels;
|
||||
const DWORD mask = read_channel_mask(game_format);
|
||||
|
||||
// without a mask the layout is unknown: extract/normalize have nothing to act on, so all
|
||||
// algorithms fall back to folding L/R pairs (AC-4 still attenuates by -3 dB)
|
||||
if (mask == 0) {
|
||||
this->build_pairs_mix(channels,
|
||||
this->algorithm == DownmixAlgorithm::AC4 ? ATT_3DB : 1.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this->algorithm) {
|
||||
case DownmixAlgorithm::FrontOnly:
|
||||
this->build_extract_mix(mask, channels,
|
||||
SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT);
|
||||
break;
|
||||
case DownmixAlgorithm::RearOnly:
|
||||
this->build_extract_mix(mask, channels,
|
||||
SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | SPEAKER_BACK_CENTER);
|
||||
break;
|
||||
case DownmixAlgorithm::SideOnly:
|
||||
this->build_extract_mix(mask, channels,
|
||||
SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT);
|
||||
break;
|
||||
case DownmixAlgorithm::Normalize:
|
||||
this->build_normalize_mix(mask, channels);
|
||||
break;
|
||||
case DownmixAlgorithm::AC4:
|
||||
this->build_ac4_mix(mask, channels);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Downmix::process(BYTE *dst, const BYTE *src, UINT32 frames) const {
|
||||
const int bps = this->bytes_per_sample;
|
||||
const int src_stride = this->game_frame_size;
|
||||
const int dst_stride = 2 * bps;
|
||||
|
||||
if (dst == nullptr || src == nullptr || bps <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// sum each speaker's source channels into the matching stereo output
|
||||
for (UINT32 i = 0; i < frames; i++) {
|
||||
const BYTE *in = src + (size_t) i * src_stride;
|
||||
BYTE *out = dst + (size_t) i * dst_stride;
|
||||
|
||||
float left = 0.0f;
|
||||
float right = 0.0f;
|
||||
for (const auto &c : this->left_mix) {
|
||||
left += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain;
|
||||
}
|
||||
for (const auto &c : this->right_mix) {
|
||||
right += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain;
|
||||
}
|
||||
write_sample(out, bps, this->is_float, left);
|
||||
write_sample(out + bps, bps, this->is_float, right);
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT Downmix::get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData) {
|
||||
const size_t needed = (size_t) frames * this->game_frame_size;
|
||||
if (this->scratch.size() < needed) {
|
||||
this->scratch.resize(needed);
|
||||
}
|
||||
|
||||
HRESULT ret = real->GetBuffer(frames, &this->device_buffer);
|
||||
if (FAILED(ret)) {
|
||||
this->device_buffer = nullptr;
|
||||
return ret;
|
||||
}
|
||||
|
||||
*ppData = this->scratch.data();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT Downmix::get_scratch(UINT32 frames, BYTE **ppData) {
|
||||
const size_t needed = (size_t) frames * this->game_frame_size;
|
||||
if (this->scratch.size() < needed) {
|
||||
this->scratch.resize(needed);
|
||||
}
|
||||
|
||||
*ppData = this->scratch.data();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
void Downmix::downmix_into(BYTE *dst, UINT32 frames) const {
|
||||
this->process(dst, this->scratch.data(), frames);
|
||||
}
|
||||
|
||||
void Downmix::write_device_buffer(UINT32 frames, DWORD flags) {
|
||||
const int bps = this->bytes_per_sample;
|
||||
const int dst_stride = 2 * bps;
|
||||
|
||||
if (this->device_buffer == nullptr || frames == 0 || bps <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// mute the first few buffers to avoid a pop on stream start
|
||||
if (this->buffers_to_mute > 0) {
|
||||
memset(this->device_buffer, 0, (size_t) frames * dst_stride);
|
||||
this->buffers_to_mute--;
|
||||
} else if ((flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
|
||||
this->process(this->device_buffer, this->scratch.data(), frames);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,150 +1,150 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
#include <mmreg.h>
|
||||
#include <audioclient.h>
|
||||
|
||||
#include "hooks/audio/audio.h"
|
||||
|
||||
struct IAudioClient;
|
||||
struct IAudioRenderClient;
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
// Generic WASAPI surround-to-stereo downmix. The real device is opened in stereo while the
|
||||
// game keeps writing its native multi-channel audio into a scratch buffer; on release that
|
||||
// buffer is mixed down into the two front channels.
|
||||
//
|
||||
// The mix is derived from the source format's speaker mask according to the selected
|
||||
// DownmixAlgorithm:
|
||||
// FrontOnly / RearOnly / SideOnly - keep only that group of channels, routed to their side
|
||||
// AC4 - AC-4 stereo downmix coefficients (ETSI TS 103 190-1 §6.2.17): front left/right
|
||||
// pass at 0 dB, center and surrounds fold in at -3 dB, LFE dropped
|
||||
// Normalize - every channel folded in (center to both sides) with each output side averaged
|
||||
// so its channels are equally loud, LFE dropped
|
||||
struct Downmix {
|
||||
|
||||
// a source channel routed into one output speaker at the given gain
|
||||
struct Contribution {
|
||||
int channel;
|
||||
float gain;
|
||||
};
|
||||
|
||||
// map an option value (front/rear/side/ac4/normalize) to its algorithm.
|
||||
static std::optional<DownmixAlgorithm> name_to_algorithm(const char *value) {
|
||||
if (_stricmp(value, "front") == 0) {
|
||||
return DownmixAlgorithm::FrontOnly;
|
||||
} else if (_stricmp(value, "rear") == 0) {
|
||||
return DownmixAlgorithm::RearOnly;
|
||||
} else if (_stricmp(value, "side") == 0) {
|
||||
return DownmixAlgorithm::SideOnly;
|
||||
} else if (_stricmp(value, "ac4") == 0) {
|
||||
return DownmixAlgorithm::AC4;
|
||||
} else if (_stricmp(value, "normalize") == 0) {
|
||||
return DownmixAlgorithm::Normalize;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// human-readable name of an algorithm, for logging.
|
||||
static const char *algorithm_name(DownmixAlgorithm algorithm) {
|
||||
switch (algorithm) {
|
||||
case DownmixAlgorithm::FrontOnly: return "front";
|
||||
case DownmixAlgorithm::RearOnly: return "rear";
|
||||
case DownmixAlgorithm::SideOnly: return "side";
|
||||
case DownmixAlgorithm::AC4: return "ac4";
|
||||
case DownmixAlgorithm::Normalize: return "normalize";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// whether the downmix is active for the current stream
|
||||
bool enabled = false;
|
||||
|
||||
// algorithm used to fold the multi-channel audio into stereo
|
||||
DownmixAlgorithm algorithm = DownmixAlgorithm::AC4;
|
||||
|
||||
// size in bytes of one frame of the game's multi-channel format
|
||||
int game_frame_size = 0;
|
||||
|
||||
// size in bytes of a single sample (per channel)
|
||||
int bytes_per_sample = 0;
|
||||
|
||||
// whether samples are IEEE floating point rather than integer PCM
|
||||
bool is_float = false;
|
||||
|
||||
// enable the downmix for the given game format and fill stereo_out with the equivalent
|
||||
// stereo format to open the real device with.
|
||||
void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out,
|
||||
DownmixAlgorithm algorithm);
|
||||
|
||||
// build the stereo format equivalent to game_format (same sample rate and bit depth).
|
||||
static void make_stereo_format(const WAVEFORMATEX *game_format,
|
||||
WAVEFORMATEXTENSIBLE *stereo_out);
|
||||
|
||||
// initialize the real device with the stereo format. downmixing reduces the channel count,
|
||||
// shrinking the buffer's byte size, so the duration the game sized for its multi-channel
|
||||
// format can leave the smaller stereo buffer unaligned. on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED
|
||||
// this performs the standard WASAPI realignment and retries.
|
||||
HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
|
||||
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
|
||||
const WAVEFORMATEX *device_format, LPCGUID session_guid);
|
||||
|
||||
// mix `frames` frames of multi-channel `src` down into stereo `dst`.
|
||||
void process(BYTE *dst, const BYTE *src, UINT32 frames) const;
|
||||
|
||||
// grab the real stereo device buffer and hand the game the scratch buffer to write into.
|
||||
HRESULT get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData);
|
||||
|
||||
// size the scratch and hand it to the game without acquiring a device buffer. used when a
|
||||
// later stage (the resampler) owns the device interaction.
|
||||
HRESULT get_scratch(UINT32 frames, BYTE **ppData);
|
||||
|
||||
// downmix the scratch the game wrote into the caller's stereo buffer, without touching the
|
||||
// device. used to feed the resampler when the two stages are chained.
|
||||
void downmix_into(BYTE *dst, UINT32 frames) const;
|
||||
|
||||
// mix the scratch buffer into the stereo device buffer held since get_buffer. the caller
|
||||
// owns releasing the device buffer afterwards (see current_buffer / buffer_released).
|
||||
void write_device_buffer(UINT32 frames, DWORD flags);
|
||||
|
||||
// the real device buffer currently held, or null.
|
||||
BYTE *current_buffer() const { return this->device_buffer; }
|
||||
|
||||
// forget the held device buffer once the caller has released it.
|
||||
void buffer_released() { this->device_buffer = nullptr; }
|
||||
|
||||
private:
|
||||
|
||||
// build the mix from the source speaker layout for the selected algorithm
|
||||
void build_layout_mix(const WAVEFORMATEX *game_format);
|
||||
|
||||
// per-algorithm builders, each filling left_mix / right_mix from the speaker mask
|
||||
void build_ac4_mix(DWORD mask, int channels);
|
||||
void build_extract_mix(DWORD mask, int channels, DWORD keep);
|
||||
void build_normalize_mix(DWORD mask, int channels);
|
||||
|
||||
// fallback for streams without a speaker mask: fold interleaved L/R pairs at `gain`
|
||||
void build_pairs_mix(int channels, float gain);
|
||||
|
||||
// append one source channel to the output side(s) matching its speaker, at `gain`
|
||||
void add_channel(int channel, DWORD speaker, float gain);
|
||||
|
||||
// source channels summed into each output speaker
|
||||
std::vector<Contribution> left_mix;
|
||||
std::vector<Contribution> right_mix;
|
||||
|
||||
// buffer the game writes its multi-channel audio into between get/release
|
||||
std::vector<BYTE> scratch;
|
||||
|
||||
// the real stereo device buffer currently held, or null
|
||||
BYTE *device_buffer = nullptr;
|
||||
|
||||
// leading buffers to silence to avoid a pop on stream start
|
||||
int buffers_to_mute = 16;
|
||||
};
|
||||
}
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
#include <mmreg.h>
|
||||
#include <audioclient.h>
|
||||
|
||||
#include "hooks/audio/audio.h"
|
||||
|
||||
struct IAudioClient;
|
||||
struct IAudioRenderClient;
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
// Generic WASAPI surround-to-stereo downmix. The real device is opened in stereo while the
|
||||
// game keeps writing its native multi-channel audio into a scratch buffer; on release that
|
||||
// buffer is mixed down into the two front channels.
|
||||
//
|
||||
// The mix is derived from the source format's speaker mask according to the selected
|
||||
// DownmixAlgorithm:
|
||||
// FrontOnly / RearOnly / SideOnly - keep only that group of channels, routed to their side
|
||||
// AC4 - AC-4 stereo downmix coefficients (ETSI TS 103 190-1 §6.2.17): front left/right
|
||||
// pass at 0 dB, center and surrounds fold in at -3 dB, LFE dropped
|
||||
// Normalize - every channel folded in (center to both sides) with each output side averaged
|
||||
// so its channels are equally loud, LFE dropped
|
||||
struct Downmix {
|
||||
|
||||
// a source channel routed into one output speaker at the given gain
|
||||
struct Contribution {
|
||||
int channel;
|
||||
float gain;
|
||||
};
|
||||
|
||||
// map an option value (front/rear/side/ac4/normalize) to its algorithm.
|
||||
static std::optional<DownmixAlgorithm> name_to_algorithm(const char *value) {
|
||||
if (_stricmp(value, "front") == 0) {
|
||||
return DownmixAlgorithm::FrontOnly;
|
||||
} else if (_stricmp(value, "rear") == 0) {
|
||||
return DownmixAlgorithm::RearOnly;
|
||||
} else if (_stricmp(value, "side") == 0) {
|
||||
return DownmixAlgorithm::SideOnly;
|
||||
} else if (_stricmp(value, "ac4") == 0) {
|
||||
return DownmixAlgorithm::AC4;
|
||||
} else if (_stricmp(value, "normalize") == 0) {
|
||||
return DownmixAlgorithm::Normalize;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// human-readable name of an algorithm, for logging.
|
||||
static const char *algorithm_name(DownmixAlgorithm algorithm) {
|
||||
switch (algorithm) {
|
||||
case DownmixAlgorithm::FrontOnly: return "front";
|
||||
case DownmixAlgorithm::RearOnly: return "rear";
|
||||
case DownmixAlgorithm::SideOnly: return "side";
|
||||
case DownmixAlgorithm::AC4: return "ac4";
|
||||
case DownmixAlgorithm::Normalize: return "normalize";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// whether the downmix is active for the current stream
|
||||
bool enabled = false;
|
||||
|
||||
// algorithm used to fold the multi-channel audio into stereo
|
||||
DownmixAlgorithm algorithm = DownmixAlgorithm::AC4;
|
||||
|
||||
// size in bytes of one frame of the game's multi-channel format
|
||||
int game_frame_size = 0;
|
||||
|
||||
// size in bytes of a single sample (per channel)
|
||||
int bytes_per_sample = 0;
|
||||
|
||||
// whether samples are IEEE floating point rather than integer PCM
|
||||
bool is_float = false;
|
||||
|
||||
// enable the downmix for the given game format and fill stereo_out with the equivalent
|
||||
// stereo format to open the real device with.
|
||||
void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out,
|
||||
DownmixAlgorithm algorithm);
|
||||
|
||||
// build the stereo format equivalent to game_format (same sample rate and bit depth).
|
||||
static void make_stereo_format(const WAVEFORMATEX *game_format,
|
||||
WAVEFORMATEXTENSIBLE *stereo_out);
|
||||
|
||||
// initialize the real device with the stereo format. downmixing reduces the channel count,
|
||||
// shrinking the buffer's byte size, so the duration the game sized for its multi-channel
|
||||
// format can leave the smaller stereo buffer unaligned. on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED
|
||||
// this performs the standard WASAPI realignment and retries.
|
||||
HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
|
||||
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
|
||||
const WAVEFORMATEX *device_format, LPCGUID session_guid);
|
||||
|
||||
// mix `frames` frames of multi-channel `src` down into stereo `dst`.
|
||||
void process(BYTE *dst, const BYTE *src, UINT32 frames) const;
|
||||
|
||||
// grab the real stereo device buffer and hand the game the scratch buffer to write into.
|
||||
HRESULT get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData);
|
||||
|
||||
// size the scratch and hand it to the game without acquiring a device buffer. used when a
|
||||
// later stage (the resampler) owns the device interaction.
|
||||
HRESULT get_scratch(UINT32 frames, BYTE **ppData);
|
||||
|
||||
// downmix the scratch the game wrote into the caller's stereo buffer, without touching the
|
||||
// device. used to feed the resampler when the two stages are chained.
|
||||
void downmix_into(BYTE *dst, UINT32 frames) const;
|
||||
|
||||
// mix the scratch buffer into the stereo device buffer held since get_buffer. the caller
|
||||
// owns releasing the device buffer afterwards (see current_buffer / buffer_released).
|
||||
void write_device_buffer(UINT32 frames, DWORD flags);
|
||||
|
||||
// the real device buffer currently held, or null.
|
||||
BYTE *current_buffer() const { return this->device_buffer; }
|
||||
|
||||
// forget the held device buffer once the caller has released it.
|
||||
void buffer_released() { this->device_buffer = nullptr; }
|
||||
|
||||
private:
|
||||
|
||||
// build the mix from the source speaker layout for the selected algorithm
|
||||
void build_layout_mix(const WAVEFORMATEX *game_format);
|
||||
|
||||
// per-algorithm builders, each filling left_mix / right_mix from the speaker mask
|
||||
void build_ac4_mix(DWORD mask, int channels);
|
||||
void build_extract_mix(DWORD mask, int channels, DWORD keep);
|
||||
void build_normalize_mix(DWORD mask, int channels);
|
||||
|
||||
// fallback for streams without a speaker mask: fold interleaved L/R pairs at `gain`
|
||||
void build_pairs_mix(int channels, float gain);
|
||||
|
||||
// append one source channel to the output side(s) matching its speaker, at `gain`
|
||||
void add_channel(int channel, DWORD speaker, float gain);
|
||||
|
||||
// source channels summed into each output speaker
|
||||
std::vector<Contribution> left_mix;
|
||||
std::vector<Contribution> right_mix;
|
||||
|
||||
// buffer the game writes its multi-channel audio into between get/release
|
||||
std::vector<BYTE> scratch;
|
||||
|
||||
// the real stereo device buffer currently held, or null
|
||||
BYTE *device_buffer = nullptr;
|
||||
|
||||
// leading buffers to silence to avoid a pop on stream start
|
||||
int buffers_to_mute = 16;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,13 +75,17 @@ HRESULT STDMETHODCALLTYPE DummyIAudioClient::Initialize(
|
||||
log_info("audio::wasapi", "IAudioClient::Initialize hook hit");
|
||||
print_format(ShareMode, StreamFlags, hnsBufferDuration, hnsPeriodicity, pFormat);
|
||||
|
||||
CHECK_RESULT(this->backend->on_initialize(
|
||||
HRESULT ret = this->backend->on_initialize(
|
||||
&ShareMode,
|
||||
&StreamFlags,
|
||||
&hnsBufferDuration,
|
||||
&hnsPeriodicity,
|
||||
pFormat,
|
||||
AudioSessionGuid));
|
||||
AudioSessionGuid);
|
||||
if (SUCCEEDED(ret)) {
|
||||
hooks::audio::set_active_client(this, "DummyIAudioClient::Initialize");
|
||||
}
|
||||
CHECK_RESULT(ret);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClient::GetBufferSize(UINT32 *pNumBufferFrames) {
|
||||
static std::once_flag printed;
|
||||
|
||||
@@ -1,437 +1,437 @@
|
||||
#include "resample.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
|
||||
#include <audioclient.h>
|
||||
|
||||
#include "util/logging.h"
|
||||
|
||||
#include "util.h"
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr double PI = 3.14159265358979323846;
|
||||
|
||||
// normalized sinc: sin(pi*x) / (pi*x), with the removable singularity at 0 filled in
|
||||
inline double sinc(double x) {
|
||||
if (x == 0.0) {
|
||||
return 1.0;
|
||||
}
|
||||
const double px = PI * x;
|
||||
return std::sin(px) / px;
|
||||
}
|
||||
|
||||
// Blackman window across the kernel radius; zero at +/- radius
|
||||
inline double blackman(double x, double radius) {
|
||||
const double n = (x + radius) / (2.0 * radius);
|
||||
if (n <= 0.0 || n >= 1.0) {
|
||||
return 0.0;
|
||||
}
|
||||
return 0.42 - 0.5 * std::cos(2.0 * PI * n) + 0.08 * std::cos(4.0 * PI * n);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<uint32_t> Resampler::resolve(const WAVEFORMATEX *game_format) {
|
||||
if (game_format == nullptr || !RESAMPLE_RATE.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (game_format->nSamplesPerSec == 0
|
||||
|| game_format->nSamplesPerSec == RESAMPLE_RATE.value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return RESAMPLE_RATE;
|
||||
}
|
||||
|
||||
void Resampler::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *device_out,
|
||||
uint32_t target_rate) {
|
||||
this->enabled = true;
|
||||
this->channels = game_format->nChannels;
|
||||
this->bytes_per_sample = game_format->wBitsPerSample / 8;
|
||||
this->game_frame_size = this->channels * this->bytes_per_sample;
|
||||
|
||||
this->is_float = is_ieee_float(game_format);
|
||||
|
||||
const bool supported = this->is_float
|
||||
? this->bytes_per_sample == 4
|
||||
: (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4);
|
||||
if (!supported) {
|
||||
log_fatal(
|
||||
"audio::resample",
|
||||
"unsupported sample format ({}-bit {}) for -resample",
|
||||
game_format->wBitsPerSample, this->is_float ? "float" : "int");
|
||||
}
|
||||
|
||||
this->src_rate = game_format->nSamplesPerSec;
|
||||
this->dst_rate = target_rate;
|
||||
|
||||
// anti-alias cutoff: full bandwidth when upsampling, scaled down when decimating
|
||||
this->cutoff = std::min(1.0, (double) this->dst_rate / (double) this->src_rate);
|
||||
this->half_taps = 16;
|
||||
|
||||
// precompute the windowed-sinc kernel now that cutoff is known
|
||||
this->build_kernel();
|
||||
|
||||
// prime the queue with half a window of silence so the first outputs have left history
|
||||
this->in_queue.assign((size_t) this->half_taps * this->channels, 0.0f);
|
||||
this->in_pos = this->half_taps;
|
||||
|
||||
this->make_device_format(game_format, device_out, target_rate);
|
||||
}
|
||||
|
||||
void Resampler::make_device_format(const WAVEFORMATEX *game_format,
|
||||
WAVEFORMATEXTENSIBLE *device_out, uint32_t target_rate) {
|
||||
const size_t src_size = sizeof(WAVEFORMATEX) + game_format->cbSize;
|
||||
|
||||
memset(device_out, 0, sizeof(WAVEFORMATEXTENSIBLE));
|
||||
memcpy(device_out, game_format, std::min(src_size, sizeof(WAVEFORMATEXTENSIBLE)));
|
||||
|
||||
device_out->Format.nSamplesPerSec = target_rate;
|
||||
device_out->Format.nAvgBytesPerSec = target_rate * device_out->Format.nBlockAlign;
|
||||
}
|
||||
|
||||
HRESULT Resampler::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode,
|
||||
DWORD stream_flags, REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
|
||||
const WAVEFORMATEX *device_format, LPCGUID session_guid) {
|
||||
|
||||
// the resampler bypasses the OS mixer and talks to the device directly, so it only makes
|
||||
// sense (and only works) for exclusive streams. shared streams are already resampled by
|
||||
// the Windows audio engine, so refuse loudly rather than silently doing nothing.
|
||||
if (share_mode != AUDCLNT_SHAREMODE_EXCLUSIVE) {
|
||||
log_fatal("audio::resample",
|
||||
"-resample requires WASAPI exclusive mode, but this stream is shared "
|
||||
"(Windows already resamples shared streams)");
|
||||
}
|
||||
|
||||
// record the pacing model. event-driven streams fill the whole device buffer each period
|
||||
// (produce_exact); timer-driven streams poll padding and write variable partial chunks, so
|
||||
// they drain the pending output to the device's free space each call (flush_timer).
|
||||
this->event_driven = (stream_flags & AUDCLNT_STREAMFLAGS_EVENTCALLBACK) != 0;
|
||||
|
||||
return initialize_with_alignment_retry(real, "audio::resample", share_mode, stream_flags,
|
||||
buffer_duration, periodicity, device_format, session_guid);
|
||||
}
|
||||
|
||||
UINT32 Resampler::frames_device_to_game(UINT32 device_frames) const {
|
||||
if (this->dst_rate == 0) {
|
||||
return device_frames;
|
||||
}
|
||||
// round down so the game never believes it has more room than the device can hold
|
||||
return (UINT32) (((double) device_frames * this->src_rate) / this->dst_rate);
|
||||
}
|
||||
|
||||
UINT32 Resampler::padding_device_to_game(UINT32 device_padding) const {
|
||||
if (this->dst_rate == 0) {
|
||||
return device_padding;
|
||||
}
|
||||
// round up so the reported free space stays conservative
|
||||
return (UINT32) std::ceil(((double) device_padding * this->src_rate) / this->dst_rate);
|
||||
}
|
||||
|
||||
HRESULT Resampler::get_buffer(UINT32 frames, BYTE **ppData) {
|
||||
const size_t needed = (size_t) frames * this->game_frame_size;
|
||||
if (this->scratch.size() < needed) {
|
||||
this->scratch.resize(needed);
|
||||
}
|
||||
|
||||
*ppData = this->scratch.data();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
void Resampler::enqueue_input(UINT32 frames, bool silent) {
|
||||
const int bps = this->bytes_per_sample;
|
||||
const int ch = this->channels;
|
||||
const size_t base = this->in_queue.size();
|
||||
|
||||
this->in_queue.resize(base + (size_t) frames * ch);
|
||||
|
||||
if (silent || bps <= 0 || ch <= 0) {
|
||||
std::fill(this->in_queue.begin() + base, this->in_queue.end(), 0.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
const BYTE *src = this->scratch.data();
|
||||
for (UINT32 f = 0; f < frames; f++) {
|
||||
for (int c = 0; c < ch; c++) {
|
||||
const size_t s = (size_t) f * ch + c;
|
||||
this->in_queue[base + s] = read_sample(src + s * bps, bps, this->is_float);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Resampler::build_kernel() {
|
||||
const int taps = 2 * this->half_taps;
|
||||
const int phases = this->kernel_phases;
|
||||
const double cut = this->cutoff;
|
||||
const double radius = (double) this->half_taps;
|
||||
|
||||
// one extra row at frac == 1.0 so emit_frame can interpolate against row p + 1 safely
|
||||
this->kernel_table.resize((size_t) (phases + 1) * taps);
|
||||
|
||||
for (int p = 0; p <= phases; p++) {
|
||||
const double frac = (double) p / (double) phases;
|
||||
for (int k = 0; k < taps; k++) {
|
||||
// tap k maps to input offset t = k - (half_taps - 1), matching emit_frame
|
||||
const double x = frac - (double) (k - (this->half_taps - 1));
|
||||
this->kernel_table[(size_t) p * taps + k] =
|
||||
(float) (cut * sinc(cut * x) * blackman(x, radius));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Resampler::emit_frame() {
|
||||
const int ch = this->channels;
|
||||
const int radius = this->half_taps;
|
||||
const int taps = 2 * radius;
|
||||
const long avail = (long) (this->in_queue.size() / ch);
|
||||
const long center = (long) std::floor(this->in_pos);
|
||||
|
||||
// pick the two kernel rows bracketing this fractional position and the blend between them
|
||||
const double frac = this->in_pos - (double) center;
|
||||
const double fp = frac * (double) this->kernel_phases;
|
||||
const int p0 = (int) fp;
|
||||
const float blend = (float) (fp - (double) p0);
|
||||
const float *row0 = &this->kernel_table[(size_t) p0 * taps];
|
||||
const float *row1 = &this->kernel_table[(size_t) (p0 + 1) * taps];
|
||||
|
||||
// base input index for tap 0 (t = -(radius - 1))
|
||||
const long base = center - (radius - 1);
|
||||
|
||||
for (int c = 0; c < ch; c++) {
|
||||
double acc = 0.0;
|
||||
for (int k = 0; k < taps; k++) {
|
||||
const long idx = base + k;
|
||||
if (idx < 0 || idx >= avail) {
|
||||
continue;
|
||||
}
|
||||
const float w = row0[k] + blend * (row1[k] - row0[k]);
|
||||
acc += (double) this->in_queue[(size_t) idx * ch + c] * w;
|
||||
}
|
||||
this->out_float.push_back((float) acc);
|
||||
}
|
||||
}
|
||||
|
||||
void Resampler::drop_consumed() {
|
||||
const int ch = this->channels;
|
||||
const long drop = (long) std::floor(this->in_pos) - this->half_taps;
|
||||
if (drop > 0) {
|
||||
const size_t drop_samples = (size_t) drop * ch;
|
||||
if (drop_samples <= this->in_queue.size()) {
|
||||
this->in_queue.erase(this->in_queue.begin(),
|
||||
this->in_queue.begin() + drop_samples);
|
||||
this->in_pos -= drop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UINT32 Resampler::produce_exact(UINT32 out_frames) {
|
||||
const int ch = this->channels;
|
||||
this->out_float.clear();
|
||||
if (ch <= 0 || out_frames == 0) {
|
||||
return 0;
|
||||
}
|
||||
this->out_float.reserve((size_t) out_frames * ch);
|
||||
|
||||
// resample ratio. drive it from the buffer size actually advertised to the game rather
|
||||
// than the nominal src/dst ratio: GetBufferSize reports floor(dev_buf * src/dst) game
|
||||
// frames, so the game only ever delivers that many input frames per device period.
|
||||
// consuming at the nominal ratio would eat slightly more input than arrives on any device
|
||||
// where dev_buf * src/dst is non-integer (e.g. 144 -> 132.3, floored to 132), slowly
|
||||
// draining the queue until it underruns to permanent silence. using the advertised integer
|
||||
// ratio keeps input and output exactly balanced; the resulting pitch error is below 0.3%
|
||||
// and inaudible, and it collapses to the exact ratio when the division is integer (160 ->
|
||||
// 147 stays 147/160 = 44100/48000).
|
||||
const double step = (double) this->frames_device_to_game(this->device_buffer_frames)
|
||||
/ (double) this->device_buffer_frames;
|
||||
|
||||
// input frames the block will touch: from in_pos through the right edge of the sinc kernel
|
||||
// at the final output sample. if the queue is short of this, the kernel tail reads past the
|
||||
// end and distorts every buffer, so buffer one extra block of input before the first output
|
||||
// (emitting silence without consuming) to build a cushion the kernel can always reach into.
|
||||
const long avail = (long) (this->in_queue.size() / ch);
|
||||
const long need = (long) std::ceil(this->in_pos + step * (double) out_frames)
|
||||
+ this->half_taps;
|
||||
|
||||
if (this->priming) {
|
||||
if (avail < need + (long) out_frames) {
|
||||
this->out_float.assign((size_t) out_frames * ch, 0.0f);
|
||||
return out_frames;
|
||||
}
|
||||
this->priming = false;
|
||||
}
|
||||
|
||||
for (UINT32 o = 0; o < out_frames; o++) {
|
||||
this->emit_frame();
|
||||
this->in_pos += step;
|
||||
}
|
||||
|
||||
this->drop_consumed();
|
||||
return out_frames;
|
||||
}
|
||||
|
||||
UINT32 Resampler::produce_variable() {
|
||||
const int ch = this->channels;
|
||||
if (ch <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// input frames consumed per output frame. timer-driven streams write variable partial
|
||||
// chunks, so produce however many output frames the currently queued input can fully
|
||||
// support and leave the rest for the next call; this keeps input and output balanced at
|
||||
// the exact src/dst ratio over time without depending on the device buffer size.
|
||||
const double step = (double) this->src_rate / (double) this->dst_rate;
|
||||
const long avail = (long) (this->in_queue.size() / ch);
|
||||
|
||||
// emit only while the sinc kernel's right edge stays within the queued input. the kernel
|
||||
// reaches from in_pos out to half_taps frames ahead, so stop once that would read past the
|
||||
// end; the remaining input becomes the next block's lookahead.
|
||||
UINT32 produced = 0;
|
||||
while ((long) std::ceil(this->in_pos) + this->half_taps < avail) {
|
||||
this->emit_frame();
|
||||
this->in_pos += step;
|
||||
produced++;
|
||||
}
|
||||
|
||||
this->drop_consumed();
|
||||
return produced;
|
||||
}
|
||||
|
||||
void Resampler::write_output(BYTE *dst, UINT32 frames, float gain) const {
|
||||
const int bps = this->bytes_per_sample;
|
||||
const int ch = this->channels;
|
||||
const size_t count = (size_t) frames * ch;
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
write_sample(dst + i * bps, bps, this->is_float, this->out_float[i] * gain);
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT Resampler::flush(IAudioRenderClient *real, IAudioClient *client, UINT32 frames,
|
||||
DWORD flags, float boost) {
|
||||
if (!this->enabled) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// cache the device buffer size once
|
||||
if (this->device_buffer_frames == 0) {
|
||||
client->GetBufferSize(&this->device_buffer_frames);
|
||||
}
|
||||
if (this->device_buffer_frames == 0) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
const bool silent = (flags & AUDCLNT_BUFFERFLAGS_SILENT) != 0;
|
||||
this->enqueue_input(frames, silent);
|
||||
|
||||
// confirm once that conversion actually started producing output
|
||||
static std::once_flag active_printed;
|
||||
std::call_once(active_printed, [this]() {
|
||||
log_info("audio::resample", "resample active: {} Hz -> {} Hz ({} ch, {})",
|
||||
this->src_rate, this->dst_rate, this->channels,
|
||||
this->event_driven ? "event-driven" : "timer-driven");
|
||||
});
|
||||
|
||||
// the boost is applied here (inside write_output) rather than in the standard ReleaseBuffer
|
||||
// path, so log it once for parity with that path's "volume boost active" line.
|
||||
if (boost != 1.0f) {
|
||||
static std::once_flag boost_printed;
|
||||
std::call_once(boost_printed, [boost]() {
|
||||
log_info("audio::resample", "volume boost active (resample): gain={}", boost);
|
||||
});
|
||||
}
|
||||
|
||||
return this->event_driven
|
||||
? this->flush_event(real, boost)
|
||||
: this->flush_timer(real, client, boost);
|
||||
}
|
||||
|
||||
HRESULT Resampler::flush_event(IAudioRenderClient *real, float boost) {
|
||||
|
||||
// event-driven exclusive streams must hand the device a full buffer every period and may
|
||||
// not push partial counts. resample the whole input block into exactly the device buffer
|
||||
// size.
|
||||
const UINT32 produced = this->produce_exact(this->device_buffer_frames);
|
||||
if (produced == 0) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
BYTE *dev = nullptr;
|
||||
HRESULT ret = real->GetBuffer(produced, &dev);
|
||||
if (FAILED(ret) || dev == nullptr) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// mute the first few buffers to avoid a pop on stream start
|
||||
float gain = boost;
|
||||
if (this->buffers_to_mute > 0) {
|
||||
gain = 0.0f;
|
||||
this->buffers_to_mute--;
|
||||
}
|
||||
|
||||
this->write_output(dev, produced, gain);
|
||||
|
||||
return real->ReleaseBuffer(produced, 0);
|
||||
}
|
||||
|
||||
HRESULT Resampler::flush_timer(IAudioRenderClient *real, IAudioClient *client, float boost) {
|
||||
|
||||
// convert everything currently queued into the pending output FIFO (out_float). timer-
|
||||
// driven games write variable partial chunks, so produce only what the queued input can
|
||||
// fully support and keep the remainder for the next call.
|
||||
this->produce_variable();
|
||||
|
||||
const int ch = this->channels;
|
||||
if (ch <= 0) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
const UINT32 pending = (UINT32) (this->out_float.size() / ch);
|
||||
if (pending == 0) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// push as many frames as the device currently has free, keeping the rest queued for the
|
||||
// next call. timer-driven games poll padding and write whenever there is room, so matching
|
||||
// the device's free space here avoids overflowing the ring while staying device-paced.
|
||||
UINT32 padding = 0;
|
||||
if (FAILED(client->GetCurrentPadding(&padding))) {
|
||||
return S_OK;
|
||||
}
|
||||
const UINT32 device_free = this->device_buffer_frames > padding
|
||||
? this->device_buffer_frames - padding
|
||||
: 0;
|
||||
if (device_free == 0) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
const UINT32 to_write = std::min(pending, device_free);
|
||||
|
||||
BYTE *dev = nullptr;
|
||||
HRESULT ret = real->GetBuffer(to_write, &dev);
|
||||
if (FAILED(ret) || dev == nullptr) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// mute the first few buffers to avoid a pop on stream start
|
||||
float gain = boost;
|
||||
if (this->buffers_to_mute > 0) {
|
||||
gain = 0.0f;
|
||||
this->buffers_to_mute--;
|
||||
}
|
||||
|
||||
this->write_output(dev, to_write, gain);
|
||||
ret = real->ReleaseBuffer(to_write, 0);
|
||||
|
||||
// drop the frames just written from the front of the pending FIFO
|
||||
this->out_float.erase(this->out_float.begin(),
|
||||
this->out_float.begin() + (size_t) to_write * ch);
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
#include "resample.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
|
||||
#include <audioclient.h>
|
||||
|
||||
#include "util/logging.h"
|
||||
|
||||
#include "util.h"
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr double PI = 3.14159265358979323846;
|
||||
|
||||
// normalized sinc: sin(pi*x) / (pi*x), with the removable singularity at 0 filled in
|
||||
inline double sinc(double x) {
|
||||
if (x == 0.0) {
|
||||
return 1.0;
|
||||
}
|
||||
const double px = PI * x;
|
||||
return std::sin(px) / px;
|
||||
}
|
||||
|
||||
// Blackman window across the kernel radius; zero at +/- radius
|
||||
inline double blackman(double x, double radius) {
|
||||
const double n = (x + radius) / (2.0 * radius);
|
||||
if (n <= 0.0 || n >= 1.0) {
|
||||
return 0.0;
|
||||
}
|
||||
return 0.42 - 0.5 * std::cos(2.0 * PI * n) + 0.08 * std::cos(4.0 * PI * n);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<uint32_t> Resampler::resolve(const WAVEFORMATEX *game_format) {
|
||||
if (game_format == nullptr || !RESAMPLE_RATE.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (game_format->nSamplesPerSec == 0
|
||||
|| game_format->nSamplesPerSec == RESAMPLE_RATE.value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return RESAMPLE_RATE;
|
||||
}
|
||||
|
||||
void Resampler::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *device_out,
|
||||
uint32_t target_rate) {
|
||||
this->enabled = true;
|
||||
this->channels = game_format->nChannels;
|
||||
this->bytes_per_sample = game_format->wBitsPerSample / 8;
|
||||
this->game_frame_size = this->channels * this->bytes_per_sample;
|
||||
|
||||
this->is_float = is_ieee_float(game_format);
|
||||
|
||||
const bool supported = this->is_float
|
||||
? this->bytes_per_sample == 4
|
||||
: (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4);
|
||||
if (!supported) {
|
||||
log_fatal(
|
||||
"audio::resample",
|
||||
"unsupported sample format ({}-bit {}) for -resample",
|
||||
game_format->wBitsPerSample, this->is_float ? "float" : "int");
|
||||
}
|
||||
|
||||
this->src_rate = game_format->nSamplesPerSec;
|
||||
this->dst_rate = target_rate;
|
||||
|
||||
// anti-alias cutoff: full bandwidth when upsampling, scaled down when decimating
|
||||
this->cutoff = std::min(1.0, (double) this->dst_rate / (double) this->src_rate);
|
||||
this->half_taps = 16;
|
||||
|
||||
// precompute the windowed-sinc kernel now that cutoff is known
|
||||
this->build_kernel();
|
||||
|
||||
// prime the queue with half a window of silence so the first outputs have left history
|
||||
this->in_queue.assign((size_t) this->half_taps * this->channels, 0.0f);
|
||||
this->in_pos = this->half_taps;
|
||||
|
||||
this->make_device_format(game_format, device_out, target_rate);
|
||||
}
|
||||
|
||||
void Resampler::make_device_format(const WAVEFORMATEX *game_format,
|
||||
WAVEFORMATEXTENSIBLE *device_out, uint32_t target_rate) {
|
||||
const size_t src_size = sizeof(WAVEFORMATEX) + game_format->cbSize;
|
||||
|
||||
memset(device_out, 0, sizeof(WAVEFORMATEXTENSIBLE));
|
||||
memcpy(device_out, game_format, std::min(src_size, sizeof(WAVEFORMATEXTENSIBLE)));
|
||||
|
||||
device_out->Format.nSamplesPerSec = target_rate;
|
||||
device_out->Format.nAvgBytesPerSec = target_rate * device_out->Format.nBlockAlign;
|
||||
}
|
||||
|
||||
HRESULT Resampler::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode,
|
||||
DWORD stream_flags, REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
|
||||
const WAVEFORMATEX *device_format, LPCGUID session_guid) {
|
||||
|
||||
// the resampler bypasses the OS mixer and talks to the device directly, so it only makes
|
||||
// sense (and only works) for exclusive streams. shared streams are already resampled by
|
||||
// the Windows audio engine, so refuse loudly rather than silently doing nothing.
|
||||
if (share_mode != AUDCLNT_SHAREMODE_EXCLUSIVE) {
|
||||
log_fatal("audio::resample",
|
||||
"-resample requires WASAPI exclusive mode, but this stream is shared "
|
||||
"(Windows already resamples shared streams)");
|
||||
}
|
||||
|
||||
// record the pacing model. event-driven streams fill the whole device buffer each period
|
||||
// (produce_exact); timer-driven streams poll padding and write variable partial chunks, so
|
||||
// they drain the pending output to the device's free space each call (flush_timer).
|
||||
this->event_driven = (stream_flags & AUDCLNT_STREAMFLAGS_EVENTCALLBACK) != 0;
|
||||
|
||||
return initialize_with_alignment_retry(real, "audio::resample", share_mode, stream_flags,
|
||||
buffer_duration, periodicity, device_format, session_guid);
|
||||
}
|
||||
|
||||
UINT32 Resampler::frames_device_to_game(UINT32 device_frames) const {
|
||||
if (this->dst_rate == 0) {
|
||||
return device_frames;
|
||||
}
|
||||
// round down so the game never believes it has more room than the device can hold
|
||||
return (UINT32) (((double) device_frames * this->src_rate) / this->dst_rate);
|
||||
}
|
||||
|
||||
UINT32 Resampler::padding_device_to_game(UINT32 device_padding) const {
|
||||
if (this->dst_rate == 0) {
|
||||
return device_padding;
|
||||
}
|
||||
// round up so the reported free space stays conservative
|
||||
return (UINT32) std::ceil(((double) device_padding * this->src_rate) / this->dst_rate);
|
||||
}
|
||||
|
||||
HRESULT Resampler::get_buffer(UINT32 frames, BYTE **ppData) {
|
||||
const size_t needed = (size_t) frames * this->game_frame_size;
|
||||
if (this->scratch.size() < needed) {
|
||||
this->scratch.resize(needed);
|
||||
}
|
||||
|
||||
*ppData = this->scratch.data();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
void Resampler::enqueue_input(UINT32 frames, bool silent) {
|
||||
const int bps = this->bytes_per_sample;
|
||||
const int ch = this->channels;
|
||||
const size_t base = this->in_queue.size();
|
||||
|
||||
this->in_queue.resize(base + (size_t) frames * ch);
|
||||
|
||||
if (silent || bps <= 0 || ch <= 0) {
|
||||
std::fill(this->in_queue.begin() + base, this->in_queue.end(), 0.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
const BYTE *src = this->scratch.data();
|
||||
for (UINT32 f = 0; f < frames; f++) {
|
||||
for (int c = 0; c < ch; c++) {
|
||||
const size_t s = (size_t) f * ch + c;
|
||||
this->in_queue[base + s] = read_sample(src + s * bps, bps, this->is_float);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Resampler::build_kernel() {
|
||||
const int taps = 2 * this->half_taps;
|
||||
const int phases = this->kernel_phases;
|
||||
const double cut = this->cutoff;
|
||||
const double radius = (double) this->half_taps;
|
||||
|
||||
// one extra row at frac == 1.0 so emit_frame can interpolate against row p + 1 safely
|
||||
this->kernel_table.resize((size_t) (phases + 1) * taps);
|
||||
|
||||
for (int p = 0; p <= phases; p++) {
|
||||
const double frac = (double) p / (double) phases;
|
||||
for (int k = 0; k < taps; k++) {
|
||||
// tap k maps to input offset t = k - (half_taps - 1), matching emit_frame
|
||||
const double x = frac - (double) (k - (this->half_taps - 1));
|
||||
this->kernel_table[(size_t) p * taps + k] =
|
||||
(float) (cut * sinc(cut * x) * blackman(x, radius));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Resampler::emit_frame() {
|
||||
const int ch = this->channels;
|
||||
const int radius = this->half_taps;
|
||||
const int taps = 2 * radius;
|
||||
const long avail = (long) (this->in_queue.size() / ch);
|
||||
const long center = (long) std::floor(this->in_pos);
|
||||
|
||||
// pick the two kernel rows bracketing this fractional position and the blend between them
|
||||
const double frac = this->in_pos - (double) center;
|
||||
const double fp = frac * (double) this->kernel_phases;
|
||||
const int p0 = (int) fp;
|
||||
const float blend = (float) (fp - (double) p0);
|
||||
const float *row0 = &this->kernel_table[(size_t) p0 * taps];
|
||||
const float *row1 = &this->kernel_table[(size_t) (p0 + 1) * taps];
|
||||
|
||||
// base input index for tap 0 (t = -(radius - 1))
|
||||
const long base = center - (radius - 1);
|
||||
|
||||
for (int c = 0; c < ch; c++) {
|
||||
double acc = 0.0;
|
||||
for (int k = 0; k < taps; k++) {
|
||||
const long idx = base + k;
|
||||
if (idx < 0 || idx >= avail) {
|
||||
continue;
|
||||
}
|
||||
const float w = row0[k] + blend * (row1[k] - row0[k]);
|
||||
acc += (double) this->in_queue[(size_t) idx * ch + c] * w;
|
||||
}
|
||||
this->out_float.push_back((float) acc);
|
||||
}
|
||||
}
|
||||
|
||||
void Resampler::drop_consumed() {
|
||||
const int ch = this->channels;
|
||||
const long drop = (long) std::floor(this->in_pos) - this->half_taps;
|
||||
if (drop > 0) {
|
||||
const size_t drop_samples = (size_t) drop * ch;
|
||||
if (drop_samples <= this->in_queue.size()) {
|
||||
this->in_queue.erase(this->in_queue.begin(),
|
||||
this->in_queue.begin() + drop_samples);
|
||||
this->in_pos -= drop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UINT32 Resampler::produce_exact(UINT32 out_frames) {
|
||||
const int ch = this->channels;
|
||||
this->out_float.clear();
|
||||
if (ch <= 0 || out_frames == 0) {
|
||||
return 0;
|
||||
}
|
||||
this->out_float.reserve((size_t) out_frames * ch);
|
||||
|
||||
// resample ratio. drive it from the buffer size actually advertised to the game rather
|
||||
// than the nominal src/dst ratio: GetBufferSize reports floor(dev_buf * src/dst) game
|
||||
// frames, so the game only ever delivers that many input frames per device period.
|
||||
// consuming at the nominal ratio would eat slightly more input than arrives on any device
|
||||
// where dev_buf * src/dst is non-integer (e.g. 144 -> 132.3, floored to 132), slowly
|
||||
// draining the queue until it underruns to permanent silence. using the advertised integer
|
||||
// ratio keeps input and output exactly balanced; the resulting pitch error is below 0.3%
|
||||
// and inaudible, and it collapses to the exact ratio when the division is integer (160 ->
|
||||
// 147 stays 147/160 = 44100/48000).
|
||||
const double step = (double) this->frames_device_to_game(this->device_buffer_frames)
|
||||
/ (double) this->device_buffer_frames;
|
||||
|
||||
// input frames the block will touch: from in_pos through the right edge of the sinc kernel
|
||||
// at the final output sample. if the queue is short of this, the kernel tail reads past the
|
||||
// end and distorts every buffer, so buffer one extra block of input before the first output
|
||||
// (emitting silence without consuming) to build a cushion the kernel can always reach into.
|
||||
const long avail = (long) (this->in_queue.size() / ch);
|
||||
const long need = (long) std::ceil(this->in_pos + step * (double) out_frames)
|
||||
+ this->half_taps;
|
||||
|
||||
if (this->priming) {
|
||||
if (avail < need + (long) out_frames) {
|
||||
this->out_float.assign((size_t) out_frames * ch, 0.0f);
|
||||
return out_frames;
|
||||
}
|
||||
this->priming = false;
|
||||
}
|
||||
|
||||
for (UINT32 o = 0; o < out_frames; o++) {
|
||||
this->emit_frame();
|
||||
this->in_pos += step;
|
||||
}
|
||||
|
||||
this->drop_consumed();
|
||||
return out_frames;
|
||||
}
|
||||
|
||||
UINT32 Resampler::produce_variable() {
|
||||
const int ch = this->channels;
|
||||
if (ch <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// input frames consumed per output frame. timer-driven streams write variable partial
|
||||
// chunks, so produce however many output frames the currently queued input can fully
|
||||
// support and leave the rest for the next call; this keeps input and output balanced at
|
||||
// the exact src/dst ratio over time without depending on the device buffer size.
|
||||
const double step = (double) this->src_rate / (double) this->dst_rate;
|
||||
const long avail = (long) (this->in_queue.size() / ch);
|
||||
|
||||
// emit only while the sinc kernel's right edge stays within the queued input. the kernel
|
||||
// reaches from in_pos out to half_taps frames ahead, so stop once that would read past the
|
||||
// end; the remaining input becomes the next block's lookahead.
|
||||
UINT32 produced = 0;
|
||||
while ((long) std::ceil(this->in_pos) + this->half_taps < avail) {
|
||||
this->emit_frame();
|
||||
this->in_pos += step;
|
||||
produced++;
|
||||
}
|
||||
|
||||
this->drop_consumed();
|
||||
return produced;
|
||||
}
|
||||
|
||||
void Resampler::write_output(BYTE *dst, UINT32 frames, float gain) const {
|
||||
const int bps = this->bytes_per_sample;
|
||||
const int ch = this->channels;
|
||||
const size_t count = (size_t) frames * ch;
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
write_sample(dst + i * bps, bps, this->is_float, this->out_float[i] * gain);
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT Resampler::flush(IAudioRenderClient *real, IAudioClient *client, UINT32 frames,
|
||||
DWORD flags, float boost) {
|
||||
if (!this->enabled) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// cache the device buffer size once
|
||||
if (this->device_buffer_frames == 0) {
|
||||
client->GetBufferSize(&this->device_buffer_frames);
|
||||
}
|
||||
if (this->device_buffer_frames == 0) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
const bool silent = (flags & AUDCLNT_BUFFERFLAGS_SILENT) != 0;
|
||||
this->enqueue_input(frames, silent);
|
||||
|
||||
// confirm once that conversion actually started producing output
|
||||
static std::once_flag active_printed;
|
||||
std::call_once(active_printed, [this]() {
|
||||
log_info("audio::resample", "resample active: {} Hz -> {} Hz ({} ch, {})",
|
||||
this->src_rate, this->dst_rate, this->channels,
|
||||
this->event_driven ? "event-driven" : "timer-driven");
|
||||
});
|
||||
|
||||
// the boost is applied here (inside write_output) rather than in the standard ReleaseBuffer
|
||||
// path, so log it once for parity with that path's "volume boost active" line.
|
||||
if (boost != 1.0f) {
|
||||
static std::once_flag boost_printed;
|
||||
std::call_once(boost_printed, [boost]() {
|
||||
log_info("audio::resample", "volume boost active (resample): gain={}", boost);
|
||||
});
|
||||
}
|
||||
|
||||
return this->event_driven
|
||||
? this->flush_event(real, boost)
|
||||
: this->flush_timer(real, client, boost);
|
||||
}
|
||||
|
||||
HRESULT Resampler::flush_event(IAudioRenderClient *real, float boost) {
|
||||
|
||||
// event-driven exclusive streams must hand the device a full buffer every period and may
|
||||
// not push partial counts. resample the whole input block into exactly the device buffer
|
||||
// size.
|
||||
const UINT32 produced = this->produce_exact(this->device_buffer_frames);
|
||||
if (produced == 0) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
BYTE *dev = nullptr;
|
||||
HRESULT ret = real->GetBuffer(produced, &dev);
|
||||
if (FAILED(ret) || dev == nullptr) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// mute the first few buffers to avoid a pop on stream start
|
||||
float gain = boost;
|
||||
if (this->buffers_to_mute > 0) {
|
||||
gain = 0.0f;
|
||||
this->buffers_to_mute--;
|
||||
}
|
||||
|
||||
this->write_output(dev, produced, gain);
|
||||
|
||||
return real->ReleaseBuffer(produced, 0);
|
||||
}
|
||||
|
||||
HRESULT Resampler::flush_timer(IAudioRenderClient *real, IAudioClient *client, float boost) {
|
||||
|
||||
// convert everything currently queued into the pending output FIFO (out_float). timer-
|
||||
// driven games write variable partial chunks, so produce only what the queued input can
|
||||
// fully support and keep the remainder for the next call.
|
||||
this->produce_variable();
|
||||
|
||||
const int ch = this->channels;
|
||||
if (ch <= 0) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
const UINT32 pending = (UINT32) (this->out_float.size() / ch);
|
||||
if (pending == 0) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// push as many frames as the device currently has free, keeping the rest queued for the
|
||||
// next call. timer-driven games poll padding and write whenever there is room, so matching
|
||||
// the device's free space here avoids overflowing the ring while staying device-paced.
|
||||
UINT32 padding = 0;
|
||||
if (FAILED(client->GetCurrentPadding(&padding))) {
|
||||
return S_OK;
|
||||
}
|
||||
const UINT32 device_free = this->device_buffer_frames > padding
|
||||
? this->device_buffer_frames - padding
|
||||
: 0;
|
||||
if (device_free == 0) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
const UINT32 to_write = std::min(pending, device_free);
|
||||
|
||||
BYTE *dev = nullptr;
|
||||
HRESULT ret = real->GetBuffer(to_write, &dev);
|
||||
if (FAILED(ret) || dev == nullptr) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// mute the first few buffers to avoid a pop on stream start
|
||||
float gain = boost;
|
||||
if (this->buffers_to_mute > 0) {
|
||||
gain = 0.0f;
|
||||
this->buffers_to_mute--;
|
||||
}
|
||||
|
||||
this->write_output(dev, to_write, gain);
|
||||
ret = real->ReleaseBuffer(to_write, 0);
|
||||
|
||||
// drop the frames just written from the front of the pending FIFO
|
||||
this->out_float.erase(this->out_float.begin(),
|
||||
this->out_float.begin() + (size_t) to_write * ch);
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,149 +1,149 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
#include <mmreg.h>
|
||||
#include <audioclient.h>
|
||||
|
||||
#include "hooks/audio/audio.h"
|
||||
|
||||
struct IAudioClient;
|
||||
struct IAudioRenderClient;
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
// Streaming sample-rate converter for the WASAPI render path. The real device is opened at the
|
||||
// target rate while the game keeps writing its native-rate audio into a scratch buffer; on
|
||||
// release that buffer is converted with a windowed-sinc kernel and pushed to the device.
|
||||
// Channel count and sample format are preserved; only the sample rate changes.
|
||||
//
|
||||
// Frame counts differ between the two rates, so unlike the per-frame downmix this is stateful:
|
||||
// a fractional read position and a window of input history carry across ReleaseBuffer calls,
|
||||
// and the device buffer is only filled up to the space the device currently has free.
|
||||
struct Resampler {
|
||||
|
||||
// whether the resampler is active for the current stream
|
||||
bool enabled = false;
|
||||
|
||||
// whether the stream is event-driven (AUDCLNT_STREAMFLAGS_EVENTCALLBACK). timer-driven
|
||||
// streams instead poll padding and write variable partial chunks, so they drain the
|
||||
// pending output to the device's free space rather than pushing a full buffer per period.
|
||||
bool event_driven = true;
|
||||
|
||||
// decide whether the stream should be resampled and to which rate. returns the target rate
|
||||
// when RESAMPLE_RATE is set and differs from the game's rate, otherwise nullopt.
|
||||
static std::optional<uint32_t> resolve(const WAVEFORMATEX *game_format);
|
||||
|
||||
// enable resampling for game_format and fill device_out with the equivalent format at the
|
||||
// target rate to open the real device with.
|
||||
void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *device_out,
|
||||
uint32_t target_rate);
|
||||
|
||||
// build the device format equivalent to game_format at target_rate (same channels/depth).
|
||||
static void make_device_format(const WAVEFORMATEX *game_format,
|
||||
WAVEFORMATEXTENSIBLE *device_out, uint32_t target_rate);
|
||||
|
||||
// initialize the real device at the target rate, performing the standard WASAPI buffer
|
||||
// realignment retry on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED.
|
||||
HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
|
||||
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
|
||||
const WAVEFORMATEX *device_format, LPCGUID session_guid);
|
||||
|
||||
// translate a device-rate frame count to the equivalent game-rate count, so the buffer-size
|
||||
// and padding values reported to the game stay paced at the game's native rate.
|
||||
UINT32 frames_device_to_game(UINT32 device_frames) const;
|
||||
UINT32 padding_device_to_game(UINT32 device_padding) const;
|
||||
|
||||
// hand the game a scratch buffer sized for `frames` of its native format to write into.
|
||||
HRESULT get_buffer(UINT32 frames, BYTE **ppData);
|
||||
|
||||
// pointer to the input scratch (sized by get_buffer). when chained after the downmix, the
|
||||
// downmix writes its stereo output here for the resampler to consume on the next flush.
|
||||
BYTE *input_data() { return this->scratch.data(); }
|
||||
|
||||
// convert the `frames` the game wrote and push output to the real render client. `boost`
|
||||
// is applied to the converted output. event-driven streams fill exactly one device buffer
|
||||
// per period; timer-driven streams push as many converted frames as the device has free.
|
||||
HRESULT flush(IAudioRenderClient *real, IAudioClient *client, UINT32 frames, DWORD flags,
|
||||
float boost);
|
||||
|
||||
private:
|
||||
|
||||
// append `frames` of the scratch buffer (native format), or silence, to the input queue
|
||||
void enqueue_input(UINT32 frames, bool silent);
|
||||
|
||||
// event-driven path: produce exactly one full device buffer and push it.
|
||||
HRESULT flush_event(IAudioRenderClient *real, float boost);
|
||||
|
||||
// timer-driven path: convert all queued input into the pending output FIFO, then push as
|
||||
// many frames as the device currently has free, keeping the remainder for the next call.
|
||||
HRESULT flush_timer(IAudioRenderClient *real, IAudioClient *client, float boost);
|
||||
|
||||
// produce exactly out_frames output frames using the fixed src/dst ratio. event-driven
|
||||
// exclusive streams must fill the whole device buffer every period; a small input cushion
|
||||
// is buffered first (see priming) so the sinc kernel always has lookahead.
|
||||
UINT32 produce_exact(UINT32 out_frames);
|
||||
|
||||
// convert all input the kernel can fully support into the pending output FIFO (out_float),
|
||||
// appending without clearing. returns the number of frames produced. used by the
|
||||
// timer-driven path where output is drained to the device in device-paced chunks.
|
||||
UINT32 produce_variable();
|
||||
|
||||
// convolve the windowed-sinc kernel at the current in_pos and append the resulting frame
|
||||
// (one sample per channel) to out_float
|
||||
void emit_frame();
|
||||
|
||||
// precompute the windowed-sinc kernel sampled at kernel_phases sub-sample positions, so
|
||||
// emit_frame is a table lookup instead of recomputing sin/cos per tap (which is far too
|
||||
// expensive to run per sample on the audio callback thread and causes underrun crackle).
|
||||
void build_kernel();
|
||||
|
||||
// drop input frames that in_pos has advanced past, keeping a window of history for the
|
||||
// next block's left context
|
||||
void drop_consumed();
|
||||
|
||||
// convert the first `frames` of out_float to the device format, scaled by `gain`
|
||||
void write_output(BYTE *dst, UINT32 frames, float gain) const;
|
||||
|
||||
// sample format of the stream
|
||||
int channels = 0;
|
||||
int bytes_per_sample = 0;
|
||||
bool is_float = false;
|
||||
int game_frame_size = 0;
|
||||
|
||||
uint32_t src_rate = 0;
|
||||
uint32_t dst_rate = 0;
|
||||
|
||||
// sinc low-pass cutoff (1.0 when upsampling, dst/src when downsampling) and window radius
|
||||
double cutoff = 1.0;
|
||||
int half_taps = 16;
|
||||
|
||||
// precomputed kernel: (kernel_phases + 1) rows of 2*half_taps weights, indexed by the
|
||||
// fractional sample position (linearly interpolated between adjacent rows in emit_frame)
|
||||
std::vector<float> kernel_table;
|
||||
int kernel_phases = 1024;
|
||||
|
||||
// interleaved float input queue and the fractional read position within it (in frames)
|
||||
std::vector<float> in_queue;
|
||||
double in_pos = 0.0;
|
||||
|
||||
// emit silence until a full block of input lookahead has accumulated, so the sinc kernel
|
||||
// never reads past the end of the queue (which would distort the tail of every buffer)
|
||||
bool priming = true;
|
||||
|
||||
// interleaved float scratch for produced output
|
||||
std::vector<float> out_float;
|
||||
|
||||
// buffer the game writes its native-rate audio into between get_buffer / flush
|
||||
std::vector<BYTE> scratch;
|
||||
|
||||
// cached device buffer size (frames); a full buffer is produced every period
|
||||
UINT32 device_buffer_frames = 0;
|
||||
|
||||
// leading buffers to silence to avoid a pop on stream start
|
||||
int buffers_to_mute = 16;
|
||||
};
|
||||
}
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
#include <mmreg.h>
|
||||
#include <audioclient.h>
|
||||
|
||||
#include "hooks/audio/audio.h"
|
||||
|
||||
struct IAudioClient;
|
||||
struct IAudioRenderClient;
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
// Streaming sample-rate converter for the WASAPI render path. The real device is opened at the
|
||||
// target rate while the game keeps writing its native-rate audio into a scratch buffer; on
|
||||
// release that buffer is converted with a windowed-sinc kernel and pushed to the device.
|
||||
// Channel count and sample format are preserved; only the sample rate changes.
|
||||
//
|
||||
// Frame counts differ between the two rates, so unlike the per-frame downmix this is stateful:
|
||||
// a fractional read position and a window of input history carry across ReleaseBuffer calls,
|
||||
// and the device buffer is only filled up to the space the device currently has free.
|
||||
struct Resampler {
|
||||
|
||||
// whether the resampler is active for the current stream
|
||||
bool enabled = false;
|
||||
|
||||
// whether the stream is event-driven (AUDCLNT_STREAMFLAGS_EVENTCALLBACK). timer-driven
|
||||
// streams instead poll padding and write variable partial chunks, so they drain the
|
||||
// pending output to the device's free space rather than pushing a full buffer per period.
|
||||
bool event_driven = true;
|
||||
|
||||
// decide whether the stream should be resampled and to which rate. returns the target rate
|
||||
// when RESAMPLE_RATE is set and differs from the game's rate, otherwise nullopt.
|
||||
static std::optional<uint32_t> resolve(const WAVEFORMATEX *game_format);
|
||||
|
||||
// enable resampling for game_format and fill device_out with the equivalent format at the
|
||||
// target rate to open the real device with.
|
||||
void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *device_out,
|
||||
uint32_t target_rate);
|
||||
|
||||
// build the device format equivalent to game_format at target_rate (same channels/depth).
|
||||
static void make_device_format(const WAVEFORMATEX *game_format,
|
||||
WAVEFORMATEXTENSIBLE *device_out, uint32_t target_rate);
|
||||
|
||||
// initialize the real device at the target rate, performing the standard WASAPI buffer
|
||||
// realignment retry on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED.
|
||||
HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
|
||||
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
|
||||
const WAVEFORMATEX *device_format, LPCGUID session_guid);
|
||||
|
||||
// translate a device-rate frame count to the equivalent game-rate count, so the buffer-size
|
||||
// and padding values reported to the game stay paced at the game's native rate.
|
||||
UINT32 frames_device_to_game(UINT32 device_frames) const;
|
||||
UINT32 padding_device_to_game(UINT32 device_padding) const;
|
||||
|
||||
// hand the game a scratch buffer sized for `frames` of its native format to write into.
|
||||
HRESULT get_buffer(UINT32 frames, BYTE **ppData);
|
||||
|
||||
// pointer to the input scratch (sized by get_buffer). when chained after the downmix, the
|
||||
// downmix writes its stereo output here for the resampler to consume on the next flush.
|
||||
BYTE *input_data() { return this->scratch.data(); }
|
||||
|
||||
// convert the `frames` the game wrote and push output to the real render client. `boost`
|
||||
// is applied to the converted output. event-driven streams fill exactly one device buffer
|
||||
// per period; timer-driven streams push as many converted frames as the device has free.
|
||||
HRESULT flush(IAudioRenderClient *real, IAudioClient *client, UINT32 frames, DWORD flags,
|
||||
float boost);
|
||||
|
||||
private:
|
||||
|
||||
// append `frames` of the scratch buffer (native format), or silence, to the input queue
|
||||
void enqueue_input(UINT32 frames, bool silent);
|
||||
|
||||
// event-driven path: produce exactly one full device buffer and push it.
|
||||
HRESULT flush_event(IAudioRenderClient *real, float boost);
|
||||
|
||||
// timer-driven path: convert all queued input into the pending output FIFO, then push as
|
||||
// many frames as the device currently has free, keeping the remainder for the next call.
|
||||
HRESULT flush_timer(IAudioRenderClient *real, IAudioClient *client, float boost);
|
||||
|
||||
// produce exactly out_frames output frames using the fixed src/dst ratio. event-driven
|
||||
// exclusive streams must fill the whole device buffer every period; a small input cushion
|
||||
// is buffered first (see priming) so the sinc kernel always has lookahead.
|
||||
UINT32 produce_exact(UINT32 out_frames);
|
||||
|
||||
// convert all input the kernel can fully support into the pending output FIFO (out_float),
|
||||
// appending without clearing. returns the number of frames produced. used by the
|
||||
// timer-driven path where output is drained to the device in device-paced chunks.
|
||||
UINT32 produce_variable();
|
||||
|
||||
// convolve the windowed-sinc kernel at the current in_pos and append the resulting frame
|
||||
// (one sample per channel) to out_float
|
||||
void emit_frame();
|
||||
|
||||
// precompute the windowed-sinc kernel sampled at kernel_phases sub-sample positions, so
|
||||
// emit_frame is a table lookup instead of recomputing sin/cos per tap (which is far too
|
||||
// expensive to run per sample on the audio callback thread and causes underrun crackle).
|
||||
void build_kernel();
|
||||
|
||||
// drop input frames that in_pos has advanced past, keeping a window of history for the
|
||||
// next block's left context
|
||||
void drop_consumed();
|
||||
|
||||
// convert the first `frames` of out_float to the device format, scaled by `gain`
|
||||
void write_output(BYTE *dst, UINT32 frames, float gain) const;
|
||||
|
||||
// sample format of the stream
|
||||
int channels = 0;
|
||||
int bytes_per_sample = 0;
|
||||
bool is_float = false;
|
||||
int game_frame_size = 0;
|
||||
|
||||
uint32_t src_rate = 0;
|
||||
uint32_t dst_rate = 0;
|
||||
|
||||
// sinc low-pass cutoff (1.0 when upsampling, dst/src when downsampling) and window radius
|
||||
double cutoff = 1.0;
|
||||
int half_taps = 16;
|
||||
|
||||
// precomputed kernel: (kernel_phases + 1) rows of 2*half_taps weights, indexed by the
|
||||
// fractional sample position (linearly interpolated between adjacent rows in emit_frame)
|
||||
std::vector<float> kernel_table;
|
||||
int kernel_phases = 1024;
|
||||
|
||||
// interleaved float input queue and the fractional read position within it (in frames)
|
||||
std::vector<float> in_queue;
|
||||
double in_pos = 0.0;
|
||||
|
||||
// emit silence until a full block of input lookahead has accumulated, so the sinc kernel
|
||||
// never reads past the end of the queue (which would distort the tail of every buffer)
|
||||
bool priming = true;
|
||||
|
||||
// interleaved float scratch for produced output
|
||||
std::vector<float> out_float;
|
||||
|
||||
// buffer the game writes its native-rate audio into between get_buffer / flush
|
||||
std::vector<BYTE> scratch;
|
||||
|
||||
// cached device buffer size (frames); a full buffer is produced every period
|
||||
UINT32 device_buffer_frames = 0;
|
||||
|
||||
// leading buffers to silence to avoid a pop on stream start
|
||||
int buffers_to_mute = 16;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,187 +1,187 @@
|
||||
#include "shared.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <audioclient.h>
|
||||
|
||||
#include "hooks/audio/audio.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
#include "util.h"
|
||||
#include "defs.h"
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
// whether the engine's PCM converter can handle this format. PCM / float only; non-PCM
|
||||
// bitstream (AC-3 / DTS passthrough) must be left alone.
|
||||
static bool is_pcm_or_float(const WAVEFORMATEX *format) {
|
||||
if (format == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (format->wFormatTag) {
|
||||
case WAVE_FORMAT_PCM:
|
||||
case WAVE_FORMAT_IEEE_FLOAT:
|
||||
return true;
|
||||
case WAVE_FORMAT_EXTENSIBLE: {
|
||||
|
||||
// SubFormat is only valid when the extra-bytes block is large enough
|
||||
if (format->cbSize < sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
|
||||
return false;
|
||||
}
|
||||
const auto *ext = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(format);
|
||||
return ext->SubFormat == GUID_KSDATAFORMAT_SUBTYPE_PCM
|
||||
|| ext->SubFormat == GUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool SharedRedirect::wants(AUDCLNT_SHAREMODE share_mode, const WAVEFORMATEX *format) {
|
||||
|
||||
// only redirect PCM / float exclusive streams: the engine converter (AUTOCONVERTPCM) can
|
||||
// handle those, but non-PCM bitstream (AC-3 / DTS passthrough) would fail in shared mode,
|
||||
// so leave it in exclusive untouched.
|
||||
return hooks::audio::WASAPI_COMPATIBILITY_MODE
|
||||
&& share_mode == AUDCLNT_SHAREMODE_EXCLUSIVE
|
||||
&& is_pcm_or_float(format);
|
||||
}
|
||||
|
||||
void SharedRedirect::apply(AUDCLNT_SHAREMODE *share_mode, DWORD *stream_flags,
|
||||
REFERENCE_TIME *periodicity) {
|
||||
|
||||
// shared mode requires periodicity == 0; AUTOCONVERTPCM lets the engine accept the game's
|
||||
// native format (else shared Initialize returns AUDCLNT_E_UNSUPPORTED_FORMAT).
|
||||
log_info("audio::wasapi", "redirecting exclusive WASAPI to shared mode");
|
||||
*share_mode = AUDCLNT_SHAREMODE_SHARED;
|
||||
*periodicity = 0;
|
||||
*stream_flags |= AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
|
||||
this->redirected_from_exclusive = true;
|
||||
}
|
||||
|
||||
UINT32 SharedRedirect::clamp_buffer_size(IAudioClient *real, uint32_t sample_rate,
|
||||
UINT32 device_frames) {
|
||||
if (!this->redirected_from_exclusive || real == nullptr || sample_rate == 0 || device_frames == 0) {
|
||||
this->reported_frames = device_frames;
|
||||
return device_frames;
|
||||
}
|
||||
|
||||
// GetDevicePeriod returns REFERENCE_TIME units (100 ns), 10^7 per second, so
|
||||
// period_frames = period * sample_rate / 10^7.
|
||||
REFERENCE_TIME period = 0;
|
||||
if (SUCCEEDED(real->GetDevicePeriod(&period, nullptr)) && period > 0) {
|
||||
const UINT32 period_frames = (UINT32) ((period * sample_rate) / 10000000);
|
||||
if (period_frames > 0 && period_frames < device_frames) {
|
||||
this->reported_frames = period_frames;
|
||||
return period_frames;
|
||||
}
|
||||
}
|
||||
|
||||
this->reported_frames = device_frames;
|
||||
return device_frames;
|
||||
}
|
||||
|
||||
void SharedRedirect::enable_bridge(int frame_bytes) {
|
||||
if (!this->redirected_from_exclusive || frame_bytes <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->frame_bytes = frame_bytes;
|
||||
this->device_buffer_frames = 0;
|
||||
this->fifo.clear();
|
||||
|
||||
log_info("audio::wasapi", "shared-mode buffer bridge enabled (frame size {} bytes)",
|
||||
frame_bytes);
|
||||
}
|
||||
|
||||
BYTE *SharedRedirect::begin_write(UINT32 frames) {
|
||||
// reserve space at the FIFO tail and let the game write straight into it - no scratch copy.
|
||||
this->pending_write_offset = this->fifo.size();
|
||||
this->fifo.resize(this->pending_write_offset + (size_t) frames * this->frame_bytes);
|
||||
|
||||
return this->fifo.data() + this->pending_write_offset;
|
||||
}
|
||||
|
||||
void SharedRedirect::commit_write(UINT32 frames, bool silent) {
|
||||
// trim the tail reservation to the frames actually written; zero it in place if silent.
|
||||
const size_t end = this->pending_write_offset + (size_t) frames * this->frame_bytes;
|
||||
if (silent) {
|
||||
std::fill(this->fifo.begin() + this->pending_write_offset,
|
||||
this->fifo.begin() + end, (BYTE) 0);
|
||||
}
|
||||
this->fifo.resize(end);
|
||||
}
|
||||
|
||||
UINT32 SharedRedirect::pending_frames() const {
|
||||
if (this->frame_bytes <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (UINT32) (this->fifo.size() / this->frame_bytes);
|
||||
}
|
||||
|
||||
UINT32 SharedRedirect::virtual_padding() const {
|
||||
const UINT32 pending = this->pending_frames();
|
||||
return this->reported_frames > 0 ? std::min(pending, this->reported_frames) : pending;
|
||||
}
|
||||
|
||||
HRESULT SharedRedirect::drain(IAudioRenderClient *real, IAudioClient *client,
|
||||
const WAVEFORMATEXTENSIBLE &device_format, float boost) {
|
||||
if (!this->bridge_enabled()) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// cache the real device buffer size once; it is fixed for the life of the stream.
|
||||
if (this->device_buffer_frames == 0) {
|
||||
if (FAILED(client->GetBufferSize(&this->device_buffer_frames))
|
||||
|| this->device_buffer_frames == 0) {
|
||||
return S_OK;
|
||||
}
|
||||
}
|
||||
|
||||
const UINT32 pending = this->pending_frames();
|
||||
if (pending == 0) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// push only as many frames as the device currently has free, keeping the rest queued. this
|
||||
// self-paces to the engine's real consumption so a full-buffer write never overflows.
|
||||
UINT32 padding = 0;
|
||||
if (FAILED(client->GetCurrentPadding(&padding))) {
|
||||
return S_OK;
|
||||
}
|
||||
const UINT32 device_free = this->device_buffer_frames > padding
|
||||
? this->device_buffer_frames - padding
|
||||
: 0;
|
||||
if (device_free == 0) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
const UINT32 to_write = std::min(pending, device_free);
|
||||
|
||||
BYTE *dev = nullptr;
|
||||
HRESULT ret = real->GetBuffer(to_write, &dev);
|
||||
if (FAILED(ret) || dev == nullptr) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
const size_t bytes = (size_t) to_write * this->frame_bytes;
|
||||
std::copy(this->fifo.begin(), this->fifo.begin() + bytes, dev);
|
||||
|
||||
// mute the first few buffers to avoid a startup pop, then apply the volume boost.
|
||||
if (this->buffers_to_mute > 0) {
|
||||
std::fill(dev, dev + bytes, (BYTE) 0);
|
||||
this->buffers_to_mute--;
|
||||
} else if (boost != 1.0f) {
|
||||
apply_gain(dev, to_write, device_format, boost);
|
||||
}
|
||||
|
||||
ret = real->ReleaseBuffer(to_write, 0);
|
||||
|
||||
// drop the frames just handed to the device from the front of the FIFO.
|
||||
this->fifo.erase(this->fifo.begin(), this->fifo.begin() + bytes);
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
#include "shared.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <audioclient.h>
|
||||
|
||||
#include "hooks/audio/audio.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
#include "util.h"
|
||||
#include "defs.h"
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
// whether the engine's PCM converter can handle this format. PCM / float only; non-PCM
|
||||
// bitstream (AC-3 / DTS passthrough) must be left alone.
|
||||
static bool is_pcm_or_float(const WAVEFORMATEX *format) {
|
||||
if (format == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (format->wFormatTag) {
|
||||
case WAVE_FORMAT_PCM:
|
||||
case WAVE_FORMAT_IEEE_FLOAT:
|
||||
return true;
|
||||
case WAVE_FORMAT_EXTENSIBLE: {
|
||||
|
||||
// SubFormat is only valid when the extra-bytes block is large enough
|
||||
if (format->cbSize < sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
|
||||
return false;
|
||||
}
|
||||
const auto *ext = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(format);
|
||||
return ext->SubFormat == GUID_KSDATAFORMAT_SUBTYPE_PCM
|
||||
|| ext->SubFormat == GUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool SharedRedirect::wants(AUDCLNT_SHAREMODE share_mode, const WAVEFORMATEX *format) {
|
||||
|
||||
// only redirect PCM / float exclusive streams: the engine converter (AUTOCONVERTPCM) can
|
||||
// handle those, but non-PCM bitstream (AC-3 / DTS passthrough) would fail in shared mode,
|
||||
// so leave it in exclusive untouched.
|
||||
return hooks::audio::WASAPI_COMPATIBILITY_MODE
|
||||
&& share_mode == AUDCLNT_SHAREMODE_EXCLUSIVE
|
||||
&& is_pcm_or_float(format);
|
||||
}
|
||||
|
||||
void SharedRedirect::apply(AUDCLNT_SHAREMODE *share_mode, DWORD *stream_flags,
|
||||
REFERENCE_TIME *periodicity) {
|
||||
|
||||
// shared mode requires periodicity == 0; AUTOCONVERTPCM lets the engine accept the game's
|
||||
// native format (else shared Initialize returns AUDCLNT_E_UNSUPPORTED_FORMAT).
|
||||
log_info("audio::wasapi", "redirecting exclusive WASAPI to shared mode");
|
||||
*share_mode = AUDCLNT_SHAREMODE_SHARED;
|
||||
*periodicity = 0;
|
||||
*stream_flags |= AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
|
||||
this->redirected_from_exclusive = true;
|
||||
}
|
||||
|
||||
UINT32 SharedRedirect::clamp_buffer_size(IAudioClient *real, uint32_t sample_rate,
|
||||
UINT32 device_frames) {
|
||||
if (!this->redirected_from_exclusive || real == nullptr || sample_rate == 0 || device_frames == 0) {
|
||||
this->reported_frames = device_frames;
|
||||
return device_frames;
|
||||
}
|
||||
|
||||
// GetDevicePeriod returns REFERENCE_TIME units (100 ns), 10^7 per second, so
|
||||
// period_frames = period * sample_rate / 10^7.
|
||||
REFERENCE_TIME period = 0;
|
||||
if (SUCCEEDED(real->GetDevicePeriod(&period, nullptr)) && period > 0) {
|
||||
const UINT32 period_frames = (UINT32) ((period * sample_rate) / 10000000);
|
||||
if (period_frames > 0 && period_frames < device_frames) {
|
||||
this->reported_frames = period_frames;
|
||||
return period_frames;
|
||||
}
|
||||
}
|
||||
|
||||
this->reported_frames = device_frames;
|
||||
return device_frames;
|
||||
}
|
||||
|
||||
void SharedRedirect::enable_bridge(int frame_bytes) {
|
||||
if (!this->redirected_from_exclusive || frame_bytes <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->frame_bytes = frame_bytes;
|
||||
this->device_buffer_frames = 0;
|
||||
this->fifo.clear();
|
||||
|
||||
log_info("audio::wasapi", "shared-mode buffer bridge enabled (frame size {} bytes)",
|
||||
frame_bytes);
|
||||
}
|
||||
|
||||
BYTE *SharedRedirect::begin_write(UINT32 frames) {
|
||||
// reserve space at the FIFO tail and let the game write straight into it - no scratch copy.
|
||||
this->pending_write_offset = this->fifo.size();
|
||||
this->fifo.resize(this->pending_write_offset + (size_t) frames * this->frame_bytes);
|
||||
|
||||
return this->fifo.data() + this->pending_write_offset;
|
||||
}
|
||||
|
||||
void SharedRedirect::commit_write(UINT32 frames, bool silent) {
|
||||
// trim the tail reservation to the frames actually written; zero it in place if silent.
|
||||
const size_t end = this->pending_write_offset + (size_t) frames * this->frame_bytes;
|
||||
if (silent) {
|
||||
std::fill(this->fifo.begin() + this->pending_write_offset,
|
||||
this->fifo.begin() + end, (BYTE) 0);
|
||||
}
|
||||
this->fifo.resize(end);
|
||||
}
|
||||
|
||||
UINT32 SharedRedirect::pending_frames() const {
|
||||
if (this->frame_bytes <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (UINT32) (this->fifo.size() / this->frame_bytes);
|
||||
}
|
||||
|
||||
UINT32 SharedRedirect::virtual_padding() const {
|
||||
const UINT32 pending = this->pending_frames();
|
||||
return this->reported_frames > 0 ? std::min(pending, this->reported_frames) : pending;
|
||||
}
|
||||
|
||||
HRESULT SharedRedirect::drain(IAudioRenderClient *real, IAudioClient *client,
|
||||
const WAVEFORMATEXTENSIBLE &device_format, float boost) {
|
||||
if (!this->bridge_enabled()) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// cache the real device buffer size once; it is fixed for the life of the stream.
|
||||
if (this->device_buffer_frames == 0) {
|
||||
if (FAILED(client->GetBufferSize(&this->device_buffer_frames))
|
||||
|| this->device_buffer_frames == 0) {
|
||||
return S_OK;
|
||||
}
|
||||
}
|
||||
|
||||
const UINT32 pending = this->pending_frames();
|
||||
if (pending == 0) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// push only as many frames as the device currently has free, keeping the rest queued. this
|
||||
// self-paces to the engine's real consumption so a full-buffer write never overflows.
|
||||
UINT32 padding = 0;
|
||||
if (FAILED(client->GetCurrentPadding(&padding))) {
|
||||
return S_OK;
|
||||
}
|
||||
const UINT32 device_free = this->device_buffer_frames > padding
|
||||
? this->device_buffer_frames - padding
|
||||
: 0;
|
||||
if (device_free == 0) {
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
const UINT32 to_write = std::min(pending, device_free);
|
||||
|
||||
BYTE *dev = nullptr;
|
||||
HRESULT ret = real->GetBuffer(to_write, &dev);
|
||||
if (FAILED(ret) || dev == nullptr) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
const size_t bytes = (size_t) to_write * this->frame_bytes;
|
||||
std::copy(this->fifo.begin(), this->fifo.begin() + bytes, dev);
|
||||
|
||||
// mute the first few buffers to avoid a startup pop, then apply the volume boost.
|
||||
if (this->buffers_to_mute > 0) {
|
||||
std::fill(dev, dev + bytes, (BYTE) 0);
|
||||
this->buffers_to_mute--;
|
||||
} else if (boost != 1.0f) {
|
||||
apply_gain(dev, to_write, device_format, boost);
|
||||
}
|
||||
|
||||
ret = real->ReleaseBuffer(to_write, 0);
|
||||
|
||||
// drop the frames just handed to the device from the front of the FIFO.
|
||||
this->fifo.erase(this->fifo.begin(), this->fifo.begin() + bytes);
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user