mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-02 06:40:42 -07:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2aae8ad9ca | |||
| 1b6d1af951 | |||
| 5c3617c05f | |||
| a9a943fbfe | |||
| b094a9e352 | |||
| 4bd18afb12 | |||
| 0b24e703a1 | |||
| 1a1be7a393 | |||
| a2d8f60d12 | |||
| 5d25fdb035 | |||
| c255e3a1db | |||
| d7c144646f | |||
| 27077aa21f | |||
| 632dcf7b41 | |||
| a25bbdc9c9 | |||
| 47d886306e | |||
| edda658fa4 | |||
| 1ab20f1b56 | |||
| 0e9093a74b | |||
| f52eaea76c | |||
| c3fcf2f417 | |||
| 8d04eba21d | |||
| 8d42c0ce77 | |||
| 829503568e | |||
| 746fdb5409 | |||
| 67da892fe9 | |||
| dc191487e0 | |||
| 5c0c405800 | |||
| 60166e6ba1 | |||
| cb0b59600a | |||
| b9f3be4df2 |
@@ -12,12 +12,12 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
working-directory: ./src/spice2x
|
working-directory: ./src/spice2x
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- name: Set ccache environment variables
|
- name: Set ccache environment variables
|
||||||
run: |
|
run: |
|
||||||
echo "CCACHE_DIR=${{ github.workspace }}/src/spice2x/.ccache" >> $GITHUB_ENV
|
echo "CCACHE_DIR=${{ github.workspace }}/src/spice2x/.ccache" >> $GITHUB_ENV
|
||||||
- name: Install ccache
|
- name: Install ccache
|
||||||
uses: hendrikmuhs/ccache-action@v1.2
|
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||||
- name: Calculate commit SHA
|
- name: Calculate commit SHA
|
||||||
id: vars
|
id: vars
|
||||||
run: |
|
run: |
|
||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
|
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
|
||||||
- name: Compile
|
- name: Compile
|
||||||
run: ./build_docker.sh
|
run: ./build_docker.sh
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
|
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
|
||||||
path: src/spice2x/bin
|
path: src/spice2x/bin
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: Draft Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Build and Draft Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./src/spice2x
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Clean leftover build artifacts
|
||||||
|
run: |
|
||||||
|
rm -rf .ccache dist bin cmake-build-*
|
||||||
|
- name: Compile
|
||||||
|
run: ./build_docker.sh
|
||||||
|
- name: Determine release name from dist filename
|
||||||
|
run: |
|
||||||
|
dist=$(basename "$(ls dist/spice2x-*.zip | grep -v -- '-full.zip')")
|
||||||
|
# strip the ".zip" to get the base name, e.g. spice2x-26-06-28
|
||||||
|
name="${dist%.zip}"
|
||||||
|
# the tag is the date portion, e.g. 26-06-28
|
||||||
|
tag="${name#spice2x-}"
|
||||||
|
echo "RELEASE_NAME=$name" >> $GITHUB_ENV
|
||||||
|
echo "RELEASE_TAG=$tag" >> $GITHUB_ENV
|
||||||
|
- name: Create draft release
|
||||||
|
uses: softprops/action-gh-release@v3
|
||||||
|
with:
|
||||||
|
draft: true
|
||||||
|
prerelease: true
|
||||||
|
tag_name: ${{ env.RELEASE_TAG }}
|
||||||
|
name: ${{ env.RELEASE_NAME }}
|
||||||
|
target_commitish: main
|
||||||
|
generate_release_notes: true
|
||||||
|
files: |
|
||||||
|
src/spice2x/dist/spice2x-*.zip
|
||||||
+7
-3
@@ -11,13 +11,17 @@ To contribute, fork the repo (just the main branch), make changes in your fork,
|
|||||||
* For a new game that was never supported (not a new version of a game, but rather a new series): please wait 1 year after official AC release in Japan.
|
* For a new game that was never supported (not a new version of a game, but rather a new series): please wait 1 year after official AC release in Japan.
|
||||||
* For new version of an already supported game: proceed with caution and use generally-accepted community guidelines.
|
* For new version of an already supported game: proceed with caution and use generally-accepted community guidelines.
|
||||||
|
|
||||||
### Forbidden "features" that will never be accepted
|
### Pull requests that will not be accepted
|
||||||
|
|
||||||
* Performing decryption of encrypted files
|
* Performing decryption of encrypted files.
|
||||||
* Including (bundling), distributing, or linking to game data, or making it easier to find/download game data
|
* Including (bundling), distributing, or linking to game data, or making it easier to find/download game data.
|
||||||
* Any features that [phone home](https://en.wikipedia.org/wiki/Phoning_home)
|
* Any features that [phone home](https://en.wikipedia.org/wiki/Phoning_home)
|
||||||
* This includes automatic updaters.
|
* This includes automatic updaters.
|
||||||
* Exception: we have allowed the "patch import from URL" feature, but this has only been done under very careful considerations and plenty of warnings to the user.
|
* Exception: we have allowed the "patch import from URL" feature, but this has only been done under very careful considerations and plenty of warnings to the user.
|
||||||
|
* Built-in EA server that saves data. They should live in a separate project.
|
||||||
|
* Any additions to built-in patches (modifications / removals are OK if we have a good reason).
|
||||||
|
* Changes to the software license terms.
|
||||||
|
* Localization / translation for UI text. It becomes too difficult to manage by the maintainers.
|
||||||
|
|
||||||
### Avoiding regressions
|
### Avoiding regressions
|
||||||
|
|
||||||
|
|||||||
@@ -412,6 +412,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
games/sdvx/io.cpp
|
games/sdvx/io.cpp
|
||||||
games/sdvx/camera.cpp
|
games/sdvx/camera.cpp
|
||||||
games/jb/jb.cpp
|
games/jb/jb.cpp
|
||||||
|
games/jb/jb_touch.cpp
|
||||||
games/jb/io.cpp
|
games/jb/io.cpp
|
||||||
games/nost/nost.cpp
|
games/nost/nost.cpp
|
||||||
games/nost/io.cpp
|
games/nost/io.cpp
|
||||||
@@ -431,6 +432,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
games/rb/rb.cpp
|
games/rb/rb.cpp
|
||||||
games/rb/io.cpp
|
games/rb/io.cpp
|
||||||
games/rb/touch.cpp
|
games/rb/touch.cpp
|
||||||
|
games/rb/touch_debug.cpp
|
||||||
games/bs/bs.cpp
|
games/bs/bs.cpp
|
||||||
games/bs/io.cpp
|
games/bs/io.cpp
|
||||||
games/rf3d/rf3d.cpp
|
games/rf3d/rf3d.cpp
|
||||||
@@ -618,6 +620,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
|
|
||||||
# rawinput
|
# rawinput
|
||||||
rawinput/rawinput.cpp
|
rawinput/rawinput.cpp
|
||||||
|
rawinput/midi.cpp
|
||||||
rawinput/sextet.cpp
|
rawinput/sextet.cpp
|
||||||
rawinput/piuio.cpp
|
rawinput/piuio.cpp
|
||||||
rawinput/touch.cpp
|
rawinput/touch.cpp
|
||||||
@@ -644,8 +647,9 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
stubs/stubs.cpp
|
stubs/stubs.cpp
|
||||||
|
|
||||||
# touch
|
# touch
|
||||||
|
touch/gdi_overlay.cpp
|
||||||
touch/touch.cpp
|
touch/touch.cpp
|
||||||
touch/touch_indicators.cpp
|
touch/touch_gestures.cpp
|
||||||
touch/win7.cpp
|
touch/win7.cpp
|
||||||
touch/win8.cpp
|
touch/win8.cpp
|
||||||
|
|
||||||
|
|||||||
@@ -214,6 +214,19 @@ time (
|
|||||||
echo "==========================="
|
echo "==========================="
|
||||||
)
|
)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Checking Win7 compatibility..."
|
||||||
|
echo "============================="
|
||||||
|
if ! command -v windows_dll_compat_checker &> /dev/null; then
|
||||||
|
echo "WARNING: windows_dll_compat_checker not found, skipping Win7 compatibility check"
|
||||||
|
else
|
||||||
|
windows_dll_compat_checker -s PREMADE/konami_win7_museca_x86_64.ini \
|
||||||
|
${BUILDDIR_64}/spicetools/64/spice64.exe
|
||||||
|
windows_dll_compat_checker -s PREMADE/konami_win7_museca_x86_64_32bit_dlls.ini \
|
||||||
|
${BUILDDIR_32}/spicetools/spicecfg.exe \
|
||||||
|
${BUILDDIR_32}/spicetools/32/spice.exe
|
||||||
|
fi
|
||||||
|
|
||||||
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
|
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
|
||||||
then
|
then
|
||||||
echo ""
|
echo ""
|
||||||
@@ -229,7 +242,7 @@ then
|
|||||||
fi
|
fi
|
||||||
if ((BUILD_XP_32 > 0))
|
if ((BUILD_XP_32 > 0))
|
||||||
then
|
then
|
||||||
windows_dll_compat_checker -s PREMADE/winxp_x86_64_32bit_dlls.ini \
|
windows_dll_compat_checker -s PREMADE/konami_winxp_jubeat_i686.ini \
|
||||||
${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe \
|
${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe \
|
||||||
${BUILDDIR_WINXP_32}/spicetools/32/spice.exe
|
${BUILDDIR_WINXP_32}/spicetools/32/spice.exe
|
||||||
fi
|
fi
|
||||||
@@ -294,6 +307,7 @@ mkdir -p ${OUTDIR_EXTRAS}/largeaddressaware
|
|||||||
mkdir -p ${OUTDIR_EXTRAS}/linux
|
mkdir -p ${OUTDIR_EXTRAS}/linux
|
||||||
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/32
|
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/32
|
||||||
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/64
|
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/64
|
||||||
|
mkdir -p ${OUTDIR_EXTRAS}/updater
|
||||||
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
|
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
|
||||||
then
|
then
|
||||||
mkdir -p ${OUTDIR_EXTRAS}/winxp
|
mkdir -p ${OUTDIR_EXTRAS}/winxp
|
||||||
@@ -354,6 +368,9 @@ mkdir ${OUTDIR_EXTRAS}/api
|
|||||||
find ./api/resources/python -name "__pycache__" -exec rm -rf {} +
|
find ./api/resources/python -name "__pycache__" -exec rm -rf {} +
|
||||||
cp -r ./api/resources/* ${OUTDIR_EXTRAS}/api
|
cp -r ./api/resources/* ${OUTDIR_EXTRAS}/api
|
||||||
|
|
||||||
|
# generate the standalone updater scripts from the shared template and drop them
|
||||||
|
bash ./build_updaters.sh ${OUTDIR_EXTRAS}/updater
|
||||||
|
|
||||||
# build distribution archive
|
# build distribution archive
|
||||||
if ((DIST_ENABLE > 0))
|
if ((DIST_ENABLE > 0))
|
||||||
then
|
then
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# builds the standalone updater scripts from the shared PowerShell logic in
|
||||||
|
# update_spice.ps1. each output is a self-contained polyglot .bat: a small batch
|
||||||
|
# header (with the release channel baked in) followed by the shared PowerShell
|
||||||
|
# body after the marker line.
|
||||||
|
#
|
||||||
|
# usage: build_updaters.sh OUT_DIR
|
||||||
|
# OUT_DIR directory to write the generated .bat files into
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if [ $# -lt 1 ]; then
|
||||||
|
echo "usage: build_updaters.sh OUT_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PS_BODY="${SRC_DIR}/update_spice.ps1"
|
||||||
|
OUT_DIR="$1"
|
||||||
|
|
||||||
|
[ -f "$PS_BODY" ] || { echo "error: $PS_BODY not found" >&2; exit 1; }
|
||||||
|
|
||||||
|
MARKER='#___PS___'
|
||||||
|
|
||||||
|
gen() {
|
||||||
|
channel="$1"; label="$2"; out="$3"
|
||||||
|
{
|
||||||
|
cat <<HEADER
|
||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
REM ============================================================================
|
||||||
|
REM spice2x updater - updates spice.exe, spice64.exe and spicecfg.exe in
|
||||||
|
REM this folder to the latest spice2x release from GitHub.
|
||||||
|
REM
|
||||||
|
REM release channel: ${label}
|
||||||
|
REM make sure spice/spicecfg are NOT running before updating.
|
||||||
|
REM ============================================================================
|
||||||
|
|
||||||
|
REM expose the script folder + channel to the embedded PowerShell, then run the
|
||||||
|
REM PowerShell part: re-read this file and execute everything after the marker.
|
||||||
|
set "SPICE_DIR=%~dp0"
|
||||||
|
set "SPICE_CHANNEL=${channel}"
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "\$m='#___'+'PS'+'___'; \$code=((Get-Content -LiteralPath '%~f0' -Raw) -split \$m)[-1]; Invoke-Expression \$code"
|
||||||
|
set "RC=%ERRORLEVEL%"
|
||||||
|
endlocal & exit /b %RC%
|
||||||
|
|
||||||
|
${MARKER}
|
||||||
|
HEADER
|
||||||
|
cat "$PS_BODY"
|
||||||
|
} | sed -e 's/\r$//' -e 's/$/\r/' > "${OUT_DIR}/${out}"
|
||||||
|
echo " generated ${OUT_DIR}/${out}"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Generating updater scripts from $(basename "$PS_BODY")..."
|
||||||
|
gen "" "stable (pre-releases excluded)" "update_spice.bat"
|
||||||
|
gen "beta" "beta (newest, pre-releases included)" "update_spice_beta.bat"
|
||||||
@@ -411,6 +411,12 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
|||||||
BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case rawinput::DESTROYED:
|
||||||
|
// device was unplugged. release the button instead of leaving
|
||||||
|
// state at getLastState(), which would latch a held button on
|
||||||
|
// forever (e.g. a MIDI note held while the device is removed)
|
||||||
|
state = BUTTON_NOT_PRESSED;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-210
@@ -3,212 +3,24 @@
|
|||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|
||||||
#include "avs/game.h"
|
|
||||||
#include "cfg/configurator.h"
|
#include "cfg/configurator.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
|
||||||
#include "touch/touch.h"
|
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/utils.h"
|
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
|
|
||||||
#define JB_BUTTON_SIZE 160
|
|
||||||
#define JB_BUTTON_GAP 37
|
|
||||||
#define JB_BUTTON_HITBOX (JB_BUTTON_SIZE + JB_BUTTON_GAP)
|
|
||||||
|
|
||||||
namespace games::jb {
|
namespace games::jb {
|
||||||
|
|
||||||
// touch stuff
|
// fixes "IP ADDR CHANGE" errors with unusual network setups (e.g. a VPN)
|
||||||
JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
|
|
||||||
static bool TOUCH_ENABLE = false;
|
|
||||||
static bool TOUCH_ATTACHED = false;
|
|
||||||
static bool IS_PORTRAIT = true;
|
|
||||||
static std::vector<TouchPoint> TOUCH_POINTS;
|
|
||||||
bool TOUCH_STATE[16];
|
|
||||||
|
|
||||||
void touch_update() {
|
|
||||||
|
|
||||||
// check if touch enabled
|
|
||||||
if (!TOUCH_ENABLE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// attach touch module
|
|
||||||
if (!TOUCH_ATTACHED) {
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Find the game window.
|
|
||||||
* We check the foreground window first, then fall back to searching for the window title
|
|
||||||
* All game versions seem to have their model first in the window title
|
|
||||||
*/
|
|
||||||
HWND wnd = GetForegroundWindow();
|
|
||||||
if (!string_begins_with(GetActiveWindowTitle(), avs::game::MODEL)) {
|
|
||||||
wnd = FindWindowBeginsWith(avs::game::MODEL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if we have a window handle
|
|
||||||
if (!wnd) {
|
|
||||||
log_warning("jubeat", "could not find window handle for touch");
|
|
||||||
TOUCH_ENABLE = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// attach touch hook
|
|
||||||
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
|
|
||||||
touch_create_wnd(wnd, true);
|
|
||||||
|
|
||||||
// show cursor
|
|
||||||
if (GRAPHICS_SHOW_CURSOR) {
|
|
||||||
ShowCursor(TRUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// earlier games use a different screen orientation
|
|
||||||
if (!avs::game::is_model("L44")) {
|
|
||||||
IS_PORTRAIT = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// set attached
|
|
||||||
TOUCH_ATTACHED = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset touch state
|
|
||||||
memset(TOUCH_STATE, 0, sizeof(TOUCH_STATE));
|
|
||||||
|
|
||||||
// check touch points
|
|
||||||
// note that the IO code in device.cpp will correctly compensate for orientation, depending on the model.
|
|
||||||
TOUCH_POINTS.clear();
|
|
||||||
touch_get_points(TOUCH_POINTS);
|
|
||||||
if (TOUCH_ALGORITHM == Legacy) {
|
|
||||||
auto offset = IS_PORTRAIT ? 580 : 0;
|
|
||||||
for (auto &tp : TOUCH_POINTS) {
|
|
||||||
|
|
||||||
// get grid coordinates
|
|
||||||
int x = tp.x * 4 / 768;
|
|
||||||
int y = (tp.y - offset) * 4 / (1360 - 580);
|
|
||||||
|
|
||||||
// set the corresponding state
|
|
||||||
int index = y * 4 + x;
|
|
||||||
if (index >= 0 && index < 16) {
|
|
||||||
TOUCH_STATE[index] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (auto &tp : TOUCH_POINTS) {
|
|
||||||
// check window out of bounds
|
|
||||||
if (IS_PORTRAIT) {
|
|
||||||
if (tp.x > 768 || tp.y > 1360) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (tp.x > 1360 || tp.y > 768) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int x_relative = tp.x;
|
|
||||||
int y_relative = tp.y;
|
|
||||||
|
|
||||||
// x_relative and y_relative are relative to the top-left pixel of the first button
|
|
||||||
if (IS_PORTRAIT) {
|
|
||||||
// which is at (8, 602) in portrait:
|
|
||||||
// X: [8...759] (752 pixels wide)
|
|
||||||
// Y: [602...1353] (752 pixels high)
|
|
||||||
x_relative -= 8;
|
|
||||||
y_relative -= 602;
|
|
||||||
} else {
|
|
||||||
// and at (8, 8) in landscape
|
|
||||||
x_relative -= 8;
|
|
||||||
y_relative -= 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
int x_index = -1;
|
|
||||||
int x_hitbox = 0;
|
|
||||||
int y_index = -1;
|
|
||||||
int y_hitbox = 0;
|
|
||||||
|
|
||||||
// x_hitbox and y_hitbox is relative to top-left pixel of each button
|
|
||||||
if (x_relative >= 0) {
|
|
||||||
x_index = x_relative / JB_BUTTON_HITBOX;
|
|
||||||
x_hitbox = x_relative % JB_BUTTON_HITBOX;
|
|
||||||
}
|
|
||||||
if (y_relative >= 0) {
|
|
||||||
y_index = y_relative / JB_BUTTON_HITBOX;
|
|
||||||
y_hitbox = y_relative % JB_BUTTON_HITBOX;
|
|
||||||
}
|
|
||||||
|
|
||||||
// log_info("jb", "raw={}, idx={}, hitbox={}", tp.x, x_index, x_hitbox);
|
|
||||||
|
|
||||||
if (TOUCH_ALGORITHM == Improved) {
|
|
||||||
if (IS_PORTRAIT) {
|
|
||||||
// extend to left border
|
|
||||||
if (x_relative < 0) {
|
|
||||||
x_index = 0;
|
|
||||||
}
|
|
||||||
// right and bottom borders are covered by the hit box
|
|
||||||
} else {
|
|
||||||
// extend to top border
|
|
||||||
if (y_relative < 0) {
|
|
||||||
y_index = 0;
|
|
||||||
}
|
|
||||||
// extend to left border
|
|
||||||
if (x_relative < 0) {
|
|
||||||
x_index = 0;
|
|
||||||
}
|
|
||||||
// bottom border is covered by the hit box
|
|
||||||
// rightmost edge - ignore them entirely
|
|
||||||
if (x_index == 3 && JB_BUTTON_SIZE < x_hitbox) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (x_index < 0 || y_index < 0 || x_index > 3 || y_index > 3) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the gap was touched
|
|
||||||
if (TOUCH_ALGORITHM == AcAccurate) {
|
|
||||||
// in ac-accurate mode, touching the gap is ignored
|
|
||||||
if (x_hitbox > JB_BUTTON_SIZE || y_hitbox > JB_BUTTON_SIZE) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (TOUCH_ALGORITHM == Improved) {
|
|
||||||
// in improved mode, touching the gap triggers the closest button
|
|
||||||
if (x_index <= 2 && (JB_BUTTON_SIZE + JB_BUTTON_GAP / 2) < x_hitbox) {
|
|
||||||
x_index++;
|
|
||||||
}
|
|
||||||
if (y_index <= 2 && (JB_BUTTON_SIZE + JB_BUTTON_GAP / 2) < y_hitbox) {
|
|
||||||
y_index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the corresponding state
|
|
||||||
int index = y_index * 4 + x_index;
|
|
||||||
if (0 <= index && index < 16) {
|
|
||||||
TOUCH_STATE[index] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* to fix "IP ADDR CHANGE" errors on boot and in-game when using weird network setups such as a VPN
|
|
||||||
*/
|
|
||||||
static BOOL __stdcall network_addr_is_changed() {
|
static BOOL __stdcall network_addr_is_changed() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
// fixes lag spikes from the periodic ping to "eamuse.konami.fun"
|
||||||
* to fix lag spikes when game tries to ping "eamuse.konami.fun" every few minutes
|
|
||||||
*/
|
|
||||||
static BOOL __stdcall network_get_network_check_info() {
|
static BOOL __stdcall network_get_network_check_info() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
// fixes network errors on non-DHCP interfaces
|
||||||
* to fix network error on non DHCP interfaces
|
|
||||||
*/
|
|
||||||
static BOOL __cdecl network_get_dhcp_result() {
|
static BOOL __cdecl network_get_dhcp_result() {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -248,23 +60,7 @@ namespace games::jb {
|
|||||||
void JBGame::attach() {
|
void JBGame::attach() {
|
||||||
Game::attach();
|
Game::attach();
|
||||||
|
|
||||||
// enable touch
|
touch_attach();
|
||||||
TOUCH_ENABLE = true;
|
|
||||||
|
|
||||||
switch (TOUCH_ALGORITHM) {
|
|
||||||
case Legacy:
|
|
||||||
log_info("jubeat", "using 'legacy' touch targets");
|
|
||||||
break;
|
|
||||||
case Improved:
|
|
||||||
log_info("jubeat", "using 'improved' touch targets");
|
|
||||||
break;
|
|
||||||
case AcAccurate:
|
|
||||||
log_info("jubeat", "using 'ac accurate' touch targets");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
log_fatal("jubeat", "unknown touch algo detected in touch_update, this is a bug");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// enable debug logging of gftools
|
// enable debug logging of gftools
|
||||||
HMODULE gftools = libutils::try_module("gftools.dll");
|
HMODULE gftools = libutils::try_module("gftools.dll");
|
||||||
@@ -284,7 +80,6 @@ namespace games::jb {
|
|||||||
void JBGame::detach() {
|
void JBGame::detach() {
|
||||||
Game::detach();
|
Game::detach();
|
||||||
|
|
||||||
// disable touch
|
touch_detach();
|
||||||
TOUCH_ENABLE = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "games/game.h"
|
#include "games/game.h"
|
||||||
|
#include "games/jb/jb_touch.h"
|
||||||
|
|
||||||
namespace games::jb {
|
namespace games::jb {
|
||||||
|
|
||||||
enum JubeatTouchAlgorithm {
|
|
||||||
Legacy,
|
|
||||||
Improved,
|
|
||||||
AcAccurate
|
|
||||||
};
|
|
||||||
|
|
||||||
// touch stuff
|
|
||||||
extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
|
|
||||||
extern bool TOUCH_STATE[16];
|
|
||||||
void touch_update();
|
|
||||||
|
|
||||||
class JBGame : public games::Game {
|
class JBGame : public games::Game {
|
||||||
public:
|
public:
|
||||||
JBGame();
|
JBGame();
|
||||||
|
|||||||
@@ -0,0 +1,470 @@
|
|||||||
|
#include "jb_touch.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <atomic>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "avs/game.h"
|
||||||
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "launcher/launcher.h"
|
||||||
|
#include "touch/touch.h"
|
||||||
|
#include "rawinput/touch.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
#include "util/time.h"
|
||||||
|
#include "util/utils.h"
|
||||||
|
|
||||||
|
// touch layout: a 4x4 grid of 160px buttons separated by 37 / 38 / 37 px gaps
|
||||||
|
// (752px across). the first button's top-left is at (8, 602) in portrait and
|
||||||
|
// (6, 8) in landscape.
|
||||||
|
#define JB_BUTTON_SIZE 160
|
||||||
|
#define JB_MAX_BUTTON_GAP 38
|
||||||
|
|
||||||
|
// improved and plus modes use this reach around each button. must be >= the
|
||||||
|
// diagonal half of the widest gap (~27px) so the grid centre still reaches a button.
|
||||||
|
#define JB_TOUCH_RADIUS 38
|
||||||
|
|
||||||
|
namespace games::jb {
|
||||||
|
|
||||||
|
static_assert(std::atomic_bool::is_always_lock_free);
|
||||||
|
static_assert(std::atomic_uint16_t::is_always_lock_free);
|
||||||
|
static_assert(std::atomic<POINT>::is_always_lock_free);
|
||||||
|
static constexpr int JB_MAX_GAP_DISTANCE = (JB_MAX_BUTTON_GAP + 1) / 2;
|
||||||
|
static_assert(JB_TOUCH_RADIUS * JB_TOUCH_RADIUS >=
|
||||||
|
2 * JB_MAX_GAP_DISTANCE * JB_MAX_GAP_DISTANCE);
|
||||||
|
|
||||||
|
// touch state
|
||||||
|
JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
|
||||||
|
JubeatTouchDebugMode TOUCH_DEBUG_OVERLAY = JbTouchDebugAuto;
|
||||||
|
uint32_t TOUCH_DEBOUNCE_MS = 0;
|
||||||
|
static std::atomic_bool TOUCH_ENABLE = false;
|
||||||
|
static bool TOUCH_ATTACHED = false;
|
||||||
|
static bool IS_PORTRAIT = true;
|
||||||
|
static std::atomic_uint16_t TOUCH_STATE = 0;
|
||||||
|
|
||||||
|
// fixed-size contact view used by the debug overlay
|
||||||
|
static const size_t JB_MAX_TOUCH_POINTS = 16;
|
||||||
|
static constexpr LONG JB_INVALID_TOUCH_COORD = std::numeric_limits<LONG>::max();
|
||||||
|
static constexpr POINT JB_INVALID_TOUCH_POINT {
|
||||||
|
JB_INVALID_TOUCH_COORD,
|
||||||
|
JB_INVALID_TOUCH_COORD
|
||||||
|
};
|
||||||
|
static std::atomic<POINT> TOUCH_POINTS[JB_MAX_TOUCH_POINTS] {};
|
||||||
|
|
||||||
|
static void clear_touch_points() {
|
||||||
|
for (auto &point : TOUCH_POINTS) {
|
||||||
|
point.store(JB_INVALID_TOUCH_POINT, std::memory_order_release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// false when a contact is younger than the debounce window and should be ignored
|
||||||
|
static bool touch_matured(const TouchPoint &tp, double now_ms, double threshold_ms) {
|
||||||
|
return now_ms - tp.down_ms >= threshold_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
// snapshot for the debug overlay: each slot atomically holds one matured contact or
|
||||||
|
// the invalid sentinel. immature and absent contacts are published as invalid, so the
|
||||||
|
// overlay never shows a suppressed tap and a single atomic per slot cannot tear
|
||||||
|
static void publish_touch_points(const std::vector<TouchPoint> &touch_points,
|
||||||
|
double now_ms, double threshold_ms) {
|
||||||
|
size_t count = touch_points.size();
|
||||||
|
if (count > JB_MAX_TOUCH_POINTS) {
|
||||||
|
count = JB_MAX_TOUCH_POINTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < JB_MAX_TOUCH_POINTS; i++) {
|
||||||
|
POINT point = JB_INVALID_TOUCH_POINT;
|
||||||
|
if (i < count && touch_matured(touch_points[i], now_ms, threshold_ms)) {
|
||||||
|
point = { touch_points[i].x, touch_points[i].y };
|
||||||
|
}
|
||||||
|
TOUCH_POINTS[i].store(point, std::memory_order_release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- touch geometry ------------------------------------------------------
|
||||||
|
// gaps between the four buttons along one axis (the middle gap is 1px wider)
|
||||||
|
static const int JB_BUTTON_GAPS[3] = { 37, JB_MAX_BUTTON_GAP, 37 };
|
||||||
|
|
||||||
|
struct AxisGeometry {
|
||||||
|
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) {
|
||||||
|
AxisGeometry g {};
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
gx = axis_geometry(6);
|
||||||
|
gy = axis_geometry(8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
if (p < button) {
|
||||||
|
return button - p;
|
||||||
|
}
|
||||||
|
if (p > end) {
|
||||||
|
return p - end;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// index (0..15) of the nearest button to (px, py) within `radius`, or -1 if none;
|
||||||
|
// shared by detection and the debug overlay so both agree which button a touch hits
|
||||||
|
static int nearest_button(
|
||||||
|
int px, int py, const AxisGeometry &gx, const AxisGeometry &gy, int radius) {
|
||||||
|
int best_index = -1;
|
||||||
|
int best_dist = 0;
|
||||||
|
for (int r = 0; r < 4; r++) {
|
||||||
|
for (int c = 0; c < 4; c++) {
|
||||||
|
int dx = axis_distance(px, gx.button[c]);
|
||||||
|
int dy = axis_distance(py, gy.button[r]);
|
||||||
|
int dist = dx * dx + dy * dy;
|
||||||
|
if (dist <= radius * radius && (best_index < 0 || dist < best_dist)) {
|
||||||
|
best_dist = dist;
|
||||||
|
best_index = r * 4 + c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// detection reach for the current algorithm (0 = register only inside a button)
|
||||||
|
static int touch_radius() {
|
||||||
|
return TOUCH_ALGORITHM == AcAccurate ? 0 : JB_TOUCH_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark the buttons a touch at (px, py) hits: only the nearest within `radius`, or
|
||||||
|
// every button within `radius` when `multi` (edge/gap presses trigger several)
|
||||||
|
static void mark_buttons(uint16_t &state, int px, int py,
|
||||||
|
const AxisGeometry &gx, const AxisGeometry &gy,
|
||||||
|
int radius, bool multi) {
|
||||||
|
if (!multi) {
|
||||||
|
int index = nearest_button(px, py, gx, gy, radius);
|
||||||
|
if (index >= 0) {
|
||||||
|
state |= uint16_t(1) << index;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int r = 0; r < 4; r++) {
|
||||||
|
for (int c = 0; c < 4; c++) {
|
||||||
|
int dx = axis_distance(px, gx.button[c]);
|
||||||
|
int dy = axis_distance(py, gy.button[r]);
|
||||||
|
if (dx * dx + dy * dy <= radius * radius) {
|
||||||
|
state |= uint16_t(1) << (r * 4 + c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::bitset<16> touch_state() {
|
||||||
|
return std::bitset<16>(TOUCH_STATE.load(std::memory_order_acquire));
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_update() {
|
||||||
|
|
||||||
|
if (!TOUCH_ENABLE.load(std::memory_order_acquire)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// one-time touch window attach
|
||||||
|
if (!TOUCH_ATTACHED) {
|
||||||
|
|
||||||
|
// find the game window: prefer the foreground window, else search by
|
||||||
|
// title (the model name prefixes the window title in every version)
|
||||||
|
HWND wnd = GetForegroundWindow();
|
||||||
|
if (!string_begins_with(GetActiveWindowTitle(), avs::game::MODEL)) {
|
||||||
|
wnd = FindWindowBeginsWith(avs::game::MODEL);
|
||||||
|
}
|
||||||
|
if (!wnd) {
|
||||||
|
log_warning("jubeat", "could not find window handle for touch");
|
||||||
|
TOUCH_ENABLE.store(false, std::memory_order_release);
|
||||||
|
TOUCH_STATE.store(0, std::memory_order_release);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
if (GRAPHICS_SHOW_CURSOR) {
|
||||||
|
ShowCursor(TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
TOUCH_ATTACHED = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate the next state locally and publish it after processing every touch
|
||||||
|
uint16_t next_state = 0;
|
||||||
|
std::vector<TouchPoint> touch_points;
|
||||||
|
touch_get_points(touch_points);
|
||||||
|
|
||||||
|
// debounce: the landing time is stamped by the touch layer, so this is
|
||||||
|
// independent of how often the game polls for input (0 = off)
|
||||||
|
double now_ms = get_performance_milliseconds();
|
||||||
|
double threshold_ms = TOUCH_DEBOUNCE_MS;
|
||||||
|
|
||||||
|
// publish every raw contact (with its maturity) for the debug overlay, then drop
|
||||||
|
// the ones still inside the debounce window so no algorithm sees them
|
||||||
|
publish_touch_points(touch_points, now_ms, threshold_ms);
|
||||||
|
std::erase_if(touch_points, [&](const TouchPoint &tp) {
|
||||||
|
return !touch_matured(tp, now_ms, threshold_ms);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (TOUCH_ALGORITHM == Legacy) {
|
||||||
|
|
||||||
|
// legacy: evenly divide the play area into a 4x4 grid
|
||||||
|
auto offset = IS_PORTRAIT ? 580 : 0;
|
||||||
|
for (auto &tp : touch_points) {
|
||||||
|
int x = tp.x * 4 / 768;
|
||||||
|
int y = (tp.y - offset) * 4 / (1360 - 580);
|
||||||
|
int index = y * 4 + x;
|
||||||
|
if (index >= 0 && index < 16) {
|
||||||
|
next_state |= uint16_t(1) << index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// accurate registers only a touch inside a button; improved snaps each touch
|
||||||
|
// to the single nearest button within reach (so a gap or centre touch still
|
||||||
|
// triggers exactly one button); plus marks every button within the same reach,
|
||||||
|
// so an edge or gap touch can trigger several at once (like the mobile game)
|
||||||
|
AxisGeometry gx, gy;
|
||||||
|
touch_geometry(gx, gy);
|
||||||
|
int radius = touch_radius();
|
||||||
|
bool multi = (TOUCH_ALGORITHM == Plus);
|
||||||
|
for (auto &tp : touch_points) {
|
||||||
|
mark_buttons(next_state, tp.x, tp.y, gx, gy, radius, multi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TOUCH_STATE.store(next_state, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
// true when any supported touch handler detects a touchscreen (checked once)
|
||||||
|
static bool touchscreen_detected() {
|
||||||
|
static const bool detected = is_touch_available("jubeat touch debug overlay");
|
||||||
|
return detected;
|
||||||
|
}
|
||||||
|
|
||||||
|
// auto draws the boxes only when a touch screen is present
|
||||||
|
static bool debug_show_boxes() {
|
||||||
|
switch (TOUCH_DEBUG_OVERLAY) {
|
||||||
|
case JbTouchDebugBox:
|
||||||
|
case JbTouchDebugAll:
|
||||||
|
return true;
|
||||||
|
case JbTouchDebugAuto:
|
||||||
|
return touchscreen_detected();
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool debug_show_taps() {
|
||||||
|
return TOUCH_DEBUG_OVERLAY == JbTouchDebugAll;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool touch_debug_overlay_enabled() {
|
||||||
|
return TOUCH_ENABLE.load(std::memory_order_acquire) &&
|
||||||
|
(debug_show_boxes() || debug_show_taps());
|
||||||
|
}
|
||||||
|
|
||||||
|
// the 16 boundary rects: legacy divides the area evenly, others use button squares
|
||||||
|
static void debug_cells(
|
||||||
|
bool legacy, const AxisGeometry &gx, const AxisGeometry &gy, RECT cells[16]) {
|
||||||
|
if (legacy) {
|
||||||
|
int offset = IS_PORTRAIT ? 580 : 0;
|
||||||
|
int x_edges[5];
|
||||||
|
int y_edges[5];
|
||||||
|
for (int i = 0; i <= 4; i++) {
|
||||||
|
x_edges[i] = i * 768 / 4;
|
||||||
|
y_edges[i] = offset + i * (1360 - 580) / 4;
|
||||||
|
}
|
||||||
|
for (int r = 0; r < 4; r++) {
|
||||||
|
for (int c = 0; c < 4; c++) {
|
||||||
|
cells[r * 4 + c] = { x_edges[c], y_edges[r], x_edges[c + 1], y_edges[r + 1] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int r = 0; r < 4; r++) {
|
||||||
|
for (int c = 0; c < 4; c++) {
|
||||||
|
cells[r * 4 + c] = {
|
||||||
|
gx.button[c], gy.button[r],
|
||||||
|
gx.button[c] + JB_BUTTON_SIZE, gy.button[r] + JB_BUTTON_SIZE
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hollow box outlines: grey idle, thick green when pressed (PS_INSIDEFRAME stays inside)
|
||||||
|
static void draw_debug_boxes(
|
||||||
|
HDC hdc, bool legacy, const AxisGeometry &gx, const AxisGeometry &gy) {
|
||||||
|
RECT cells[16];
|
||||||
|
debug_cells(legacy, gx, gy, cells);
|
||||||
|
|
||||||
|
HPEN pen_idle = CreatePen(PS_SOLID, 1, RGB(160, 160, 160));
|
||||||
|
HPEN pen_active = CreatePen(PS_INSIDEFRAME, 4, RGB(0, 200, 0));
|
||||||
|
auto state = touch_state();
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
HGDIOBJ old_pen = SelectObject(hdc, state[i] ? pen_active : pen_idle);
|
||||||
|
Rectangle(hdc, cells[i].left, cells[i].top, cells[i].right, cells[i].bottom);
|
||||||
|
SelectObject(hdc, old_pen);
|
||||||
|
}
|
||||||
|
DeleteObject(pen_idle);
|
||||||
|
DeleteObject(pen_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
// whether a touch at (px, py) presses at least one button: legacy has no gaps, plus
|
||||||
|
// reaches across gaps to nearby buttons, and the others only fire inside a button square
|
||||||
|
static bool touch_presses_button(int px, int py, const AxisGeometry &gx, const AxisGeometry &gy,
|
||||||
|
bool legacy, int radius) {
|
||||||
|
if (legacy) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
int reach = (TOUCH_ALGORITHM == Plus) ? radius : 0;
|
||||||
|
return nearest_button(px, py, gx, gy, reach) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 90-degree arc on the circle rim facing the centre of button `index`
|
||||||
|
static void draw_tap_arc(HDC hdc, int px, int py, int index,
|
||||||
|
const AxisGeometry &gx, const AxisGeometry &gy, int arc_radius) {
|
||||||
|
int c = index % 4;
|
||||||
|
int r = index / 4;
|
||||||
|
double mid = std::atan2((gy.button[r] + JB_BUTTON_SIZE / 2) - py,
|
||||||
|
(gx.button[c] + JB_BUTTON_SIZE / 2) - px);
|
||||||
|
const double quarter = 3.14159265358979323846 / 2.0;
|
||||||
|
const int segments = 16;
|
||||||
|
POINT arc[segments + 1];
|
||||||
|
for (int i = 0; i <= segments; i++) {
|
||||||
|
double a = mid - quarter / 2.0 + quarter * i / segments;
|
||||||
|
arc[i].x = px + static_cast<LONG>(std::lround(arc_radius * std::cos(a)));
|
||||||
|
arc[i].y = py + static_cast<LONG>(std::lround(arc_radius * std::sin(a)));
|
||||||
|
}
|
||||||
|
Polyline(hdc, arc, segments + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// circle at each touch: white when it presses a button, grey otherwise; in single-button
|
||||||
|
// modes a gap touch also gets a white arc facing the button it snapped to (skipped in
|
||||||
|
// plus, where a touch can trigger several buttons at once)
|
||||||
|
static void draw_debug_taps(
|
||||||
|
HDC hdc, bool legacy, const AxisGeometry &gx, const AxisGeometry &gy) {
|
||||||
|
// PS_INSIDEFRAME keeps the stroke inside the circle radius
|
||||||
|
const int stroke = 4;
|
||||||
|
HPEN pen_white = CreatePen(PS_INSIDEFRAME, stroke, RGB(255, 255, 255));
|
||||||
|
HPEN pen_gray = CreatePen(PS_INSIDEFRAME, stroke, RGB(128, 128, 128));
|
||||||
|
HGDIOBJ old_pen = SelectObject(hdc, pen_white);
|
||||||
|
|
||||||
|
// accurate has no reach, so fall back to a visible marker size when drawing the circle
|
||||||
|
int radius = touch_radius();
|
||||||
|
int draw_radius = radius > 0 ? radius : JB_TOUCH_RADIUS;
|
||||||
|
|
||||||
|
// each slot holds a matured contact or the invalid sentinel; immature and absent
|
||||||
|
// contacts were already filtered out by publish_touch_points
|
||||||
|
for (auto &touch_point : TOUCH_POINTS) {
|
||||||
|
POINT point = touch_point.load(std::memory_order_acquire);
|
||||||
|
if (point.x == JB_INVALID_TOUCH_COORD) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pressed = touch_presses_button(point.x, point.y, gx, gy, legacy, radius);
|
||||||
|
|
||||||
|
SelectObject(hdc, pressed ? pen_white : pen_gray);
|
||||||
|
Ellipse(hdc, point.x - draw_radius, point.y - draw_radius,
|
||||||
|
point.x + draw_radius, point.y + draw_radius);
|
||||||
|
|
||||||
|
// the arc points at a single snapped-to button; plus can trigger several at
|
||||||
|
// once, so skip it there
|
||||||
|
if (pressed || TOUCH_ALGORITHM == Plus) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int index = nearest_button(point.x, point.y, gx, gy, radius);
|
||||||
|
if (index >= 0) {
|
||||||
|
SelectObject(hdc, pen_white);
|
||||||
|
draw_tap_arc(hdc, point.x, point.y, index, gx, gy, draw_radius - stroke / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectObject(hdc, old_pen);
|
||||||
|
DeleteObject(pen_white);
|
||||||
|
DeleteObject(pen_gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_draw_debug_overlay(HDC hdc) {
|
||||||
|
|
||||||
|
// only draw while touch is active
|
||||||
|
if (!TOUCH_ENABLE.load(std::memory_order_acquire)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool show_boxes = debug_show_boxes();
|
||||||
|
bool show_taps = debug_show_taps();
|
||||||
|
if (!show_boxes && !show_taps) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// legacy divides the field evenly; the other algorithms use button squares
|
||||||
|
bool legacy = (TOUCH_ALGORITHM == Legacy);
|
||||||
|
AxisGeometry gx {}, gy {};
|
||||||
|
if (!legacy) {
|
||||||
|
touch_geometry(gx, gy);
|
||||||
|
}
|
||||||
|
|
||||||
|
HGDIOBJ old_brush = SelectObject(hdc, GetStockObject(NULL_BRUSH));
|
||||||
|
if (show_boxes) {
|
||||||
|
draw_debug_boxes(hdc, legacy, gx, gy);
|
||||||
|
}
|
||||||
|
if (show_taps) {
|
||||||
|
draw_debug_taps(hdc, legacy, gx, gy);
|
||||||
|
}
|
||||||
|
SelectObject(hdc, old_brush);
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_attach() {
|
||||||
|
clear_touch_points();
|
||||||
|
TOUCH_ENABLE.store(true, std::memory_order_release);
|
||||||
|
|
||||||
|
switch (TOUCH_ALGORITHM) {
|
||||||
|
case Legacy:
|
||||||
|
log_info("jubeat", "using 'legacy' touch targets");
|
||||||
|
break;
|
||||||
|
case Improved:
|
||||||
|
log_info("jubeat", "using 'improved' touch targets");
|
||||||
|
break;
|
||||||
|
case Plus:
|
||||||
|
log_info("jubeat", "using 'plus' touch targets");
|
||||||
|
break;
|
||||||
|
case AcAccurate:
|
||||||
|
log_info("jubeat", "using 'ac accurate' touch targets");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log_fatal("jubeat", "unknown touch algo, this is a bug");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_detach() {
|
||||||
|
TOUCH_ENABLE.store(false, std::memory_order_release);
|
||||||
|
TOUCH_STATE.store(0, std::memory_order_release);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <bitset>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
namespace games::jb {
|
||||||
|
|
||||||
|
// touch detection algorithm
|
||||||
|
enum JubeatTouchAlgorithm {
|
||||||
|
Legacy,
|
||||||
|
Improved,
|
||||||
|
Plus,
|
||||||
|
AcAccurate
|
||||||
|
};
|
||||||
|
|
||||||
|
// debug touch overlay mode
|
||||||
|
enum JubeatTouchDebugMode {
|
||||||
|
JbTouchDebugAuto, // boundary boxes when a touch screen is detected, else nothing
|
||||||
|
JbTouchDebugNone, // draw nothing
|
||||||
|
JbTouchDebugBox, // boundary boxes
|
||||||
|
JbTouchDebugAll, // boundary boxes and touch circles
|
||||||
|
};
|
||||||
|
|
||||||
|
// selected algorithm and debug mode (set from the launcher options)
|
||||||
|
extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
|
||||||
|
extern JubeatTouchDebugMode TOUCH_DEBUG_OVERLAY;
|
||||||
|
|
||||||
|
// minimum contact age in milliseconds before a touch registers (0 = off);
|
||||||
|
// filters momentary phantom touches from noisy panels
|
||||||
|
extern uint32_t TOUCH_DEBOUNCE_MS;
|
||||||
|
|
||||||
|
// atomically published panel state, read by the I/O layer
|
||||||
|
std::bitset<16> touch_state();
|
||||||
|
|
||||||
|
// read the current touch points and refresh the panel state
|
||||||
|
void touch_update();
|
||||||
|
|
||||||
|
// enable/disable touch handling on game attach/detach
|
||||||
|
void touch_attach();
|
||||||
|
void touch_detach();
|
||||||
|
|
||||||
|
// whether the debug overlay should currently draw anything
|
||||||
|
bool touch_debug_overlay_enabled();
|
||||||
|
|
||||||
|
// draw the debug touch overlay (boundary boxes and/or touch circles per the
|
||||||
|
// selected mode) onto the spice touch overlay window's device context
|
||||||
|
void touch_draw_debug_overlay(HDC hdc);
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ void games::rb::RBGame::attach() {
|
|||||||
Game::attach();
|
Game::attach();
|
||||||
|
|
||||||
if (1000 < games::rb::TOUCH_POLL_RATE) {
|
if (1000 < games::rb::TOUCH_POLL_RATE) {
|
||||||
log_fatal("rb", "-rbtouchsize is set too high; cannot exceed 1000");
|
log_fatal("rb", "-rbtouchhz is set too high; cannot exceed 1000");
|
||||||
}
|
}
|
||||||
|
|
||||||
// init stuff
|
// init stuff
|
||||||
@@ -60,7 +60,7 @@ void games::rb::RBGame::attach() {
|
|||||||
log_info("rb", "force increasing touch poll rate by hooking SleepEx ({}Hz)", TOUCH_POLL_RATE);
|
log_info("rb", "force increasing touch poll rate by hooking SleepEx ({}Hz)", TOUCH_POLL_RATE);
|
||||||
SleepEx_orig = detour::iat_try("SleepEx", SleepEx_hook, avs::game::DLL_INSTANCE);
|
SleepEx_orig = detour::iat_try("SleepEx", SleepEx_hook, avs::game::DLL_INSTANCE);
|
||||||
} else {
|
} else {
|
||||||
log_info("rb", "not changing touch poll rate (120Hz by default)");
|
log_info("rb", "not changing touch poll rate (~125Hz by default)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
namespace games::rb {
|
namespace games::rb {
|
||||||
|
|
||||||
extern uint16_t TOUCH_SCALING;
|
extern uint16_t TOUCH_SCALING;
|
||||||
extern uint8_t TOUCH_SIZE;
|
|
||||||
extern uint16_t TOUCH_POLL_RATE;
|
extern uint16_t TOUCH_POLL_RATE;
|
||||||
|
|
||||||
class RBGame : public games::Game {
|
class RBGame : public games::Game {
|
||||||
|
|||||||
@@ -4,7 +4,10 @@
|
|||||||
|
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "rawinput/touch.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
|
#include "touch_debug.h"
|
||||||
|
#include "touch_defs.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/time.h"
|
#include "util/time.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
@@ -12,28 +15,38 @@
|
|||||||
static std::string WINDOW_TITLE = "REFLEC BEAT";
|
static std::string WINDOW_TITLE = "REFLEC BEAT";
|
||||||
|
|
||||||
namespace games::rb {
|
namespace games::rb {
|
||||||
uint16_t TOUCH_SCALING = 1000;
|
static constexpr double TOUCH_POINT_OFFSET_DIVISOR = 3.0;
|
||||||
uint8_t TOUCH_SIZE = 1;
|
static constexpr double X_INPUT_COORDINATE_COUNT = 54.0;
|
||||||
|
|
||||||
|
uint16_t TOUCH_SCALING = TOUCH_SCALE_DEFAULT;
|
||||||
|
|
||||||
|
static void packet_set_bit(unsigned char *packet, int bit) {
|
||||||
|
packet[TOUCH_PACKET_DATA_OFFSET + bit / 8] |=
|
||||||
|
static_cast<unsigned char>(1u << (bit % 8));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) {
|
games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) : log_fps(log_fps) {
|
||||||
this->log_fps = log_fps;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void games::rb::ReflecBeatTouchDeviceHandle::grid_insert(unsigned char *data, int cursor_x, int cursor_y) {
|
void games::rb::ReflecBeatTouchDeviceHandle::grid_insert(unsigned char *data, int cursor_x, int cursor_y) {
|
||||||
|
|
||||||
// scale to grid position - there are 48 columns and 76 rows of IR sensors.
|
// read() contracts X uniformly so screen edges land within usable sensors 2..45
|
||||||
// for whatever reason, the last y row (#75) results in weird input few rows above; just drop it
|
int grid_x = CLAMP(
|
||||||
int grid_x = CLAMP((cursor_x * 48) / window_width, 0, 47);
|
(cursor_x * X_SENSOR_COUNT) / window_width,
|
||||||
int grid_y = CLAMP((cursor_y * 76) / window_height, 0, 74);
|
0,
|
||||||
|
X_SENSOR_COUNT - 1);
|
||||||
|
int grid_y = CLAMP(
|
||||||
|
(cursor_y * Y_SENSOR_COUNT) / window_height,
|
||||||
|
0,
|
||||||
|
Y_SENSOR_COUNT - 1);
|
||||||
|
|
||||||
// get bit positions
|
// get bit positions
|
||||||
int bit_x = 88 + grid_x;
|
int bit_x = X_SENSOR_FIRST_BIT + grid_x;
|
||||||
int bit_y = 74 - grid_y;
|
int bit_y = Y_SENSOR_FIRST_BIT - grid_y;
|
||||||
|
|
||||||
// insert bits
|
packet_set_bit(data, bit_x);
|
||||||
data[3 + bit_x / 8] |= (char) 1 << (bit_x % 8);
|
packet_set_bit(data, bit_y);
|
||||||
data[3 + bit_y / 8] |= (char) 1 << (bit_y % 8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
||||||
@@ -49,6 +62,9 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
|||||||
|
|
||||||
if (wnd != nullptr) {
|
if (wnd != nullptr) {
|
||||||
|
|
||||||
|
// cache the game window so read() can size against it regardless of focus
|
||||||
|
this->game_hwnd = wnd;
|
||||||
|
|
||||||
// reset window process to make the game not crash
|
// reset window process to make the game not crash
|
||||||
SetWindowLongPtr(wnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(DefWindowProc));
|
SetWindowLongPtr(wnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(DefWindowProc));
|
||||||
|
|
||||||
@@ -68,18 +84,19 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
|||||||
// update window
|
// update window
|
||||||
SetWindowPos(wnd, nullptr, 0, 0, 0, 0,
|
SetWindowPos(wnd, nullptr, 0, 0, 0, 0,
|
||||||
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER);
|
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER);
|
||||||
|
}
|
||||||
|
|
||||||
// create touch window
|
// create touch window
|
||||||
touch_create_wnd(wnd);
|
touch_create_wnd(wnd);
|
||||||
|
|
||||||
} else {
|
// show fullscreen game window because it lost focus
|
||||||
|
if (!GRAPHICS_WINDOWED) {
|
||||||
// create touch window
|
|
||||||
touch_create_wnd(wnd);
|
|
||||||
|
|
||||||
// show game window because it lost focus
|
|
||||||
ShowWindow(wnd, SW_SHOW);
|
ShowWindow(wnd, SW_SHOW);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// request automatic aspect ratio fixes
|
||||||
|
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// fallback to dx hook
|
// fallback to dx hook
|
||||||
@@ -91,18 +108,21 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
|||||||
ShowCursor(TRUE);
|
ShowCursor(TRUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
touch_debug_attach();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
|
int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
|
||||||
|
|
||||||
// check buffer size
|
// check buffer size
|
||||||
if (nNumberOfBytesToRead < 20) {
|
if (nNumberOfBytesToRead < TOUCH_PACKET_SIZE) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get window
|
// size against the game window cached in open() (not GetForegroundWindow), so
|
||||||
HWND window = GetForegroundWindow();
|
// touch stays correctly scaled and keeps working when the game loses focus
|
||||||
|
HWND window = this->game_hwnd;
|
||||||
if (window == nullptr) {
|
if (window == nullptr) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -122,17 +142,17 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create data
|
// create data
|
||||||
unsigned char data[20] {};
|
unsigned char data[TOUCH_PACKET_SIZE] {};
|
||||||
|
|
||||||
// data header
|
// data header
|
||||||
data[0] = 0x55;
|
data[0] = 0x55;
|
||||||
data[2] = 0x4C;
|
data[2] = 0x4C;
|
||||||
|
|
||||||
const auto SCALE_FACTOR = games::rb::TOUCH_SCALING / 1000.f;
|
const float scale_factor = games::rb::TOUCH_SCALING / (float) games::rb::TOUCH_SCALE_DEFAULT;
|
||||||
|
|
||||||
// iterate all touch points
|
// iterate all touch points
|
||||||
auto offset_x = (int) (window_width / 48.0 / 3.0);
|
int offset_x = (int) (window_width / (double) X_SENSOR_COUNT / TOUCH_POINT_OFFSET_DIVISOR);
|
||||||
auto offset_y = (int) (window_height / 76.0 / 3.0);
|
int offset_y = (int) (window_height / (double) Y_SENSOR_COUNT / TOUCH_POINT_OFFSET_DIVISOR);
|
||||||
std::vector<TouchPoint> touch_points;
|
std::vector<TouchPoint> touch_points;
|
||||||
touch_get_points(touch_points);
|
touch_get_points(touch_points);
|
||||||
for (auto &point : touch_points) {
|
for (auto &point : touch_points) {
|
||||||
@@ -148,24 +168,30 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
|
|||||||
}
|
}
|
||||||
|
|
||||||
// apply scaling
|
// apply scaling
|
||||||
x = x - (window_width * (1.f - SCALE_FACTOR) / 2);
|
x = x - (window_width * (1.f - scale_factor) / 2);
|
||||||
x = x / SCALE_FACTOR;
|
x = x / scale_factor;
|
||||||
y = y - (window_height * (1.f - SCALE_FACTOR) / 2);
|
y = y - (window_height * (1.f - scale_factor) / 2);
|
||||||
y = y / SCALE_FACTOR;
|
y = y / scale_factor;
|
||||||
|
|
||||||
if (x < 0 || window_width <= x || y < 0 || window_height <= y) {
|
if (x < 0 || window_width <= x || y < 0 || window_height <= y) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// point scaling
|
// point coordinates
|
||||||
const auto point_x = (int) ((x - window_width / 2.0) * 48.0 / 54.0 + window_width / 2.0);
|
// keep the verified left edge fixed while adding up to one-third of a beam
|
||||||
const auto point_y = (int) (y - window_height / 76);
|
// at the right edge, preventing the finger footprint from including sensor 44
|
||||||
|
const auto point_x = (int) (
|
||||||
|
(x - window_width / 2.0) * X_SENSOR_COUNT / X_INPUT_COORDINATE_COUNT +
|
||||||
|
window_width / 2.0 +
|
||||||
|
x / (X_SENSOR_COUNT * TOUCH_POINT_OFFSET_DIVISOR));
|
||||||
|
const auto point_y = (int) y;
|
||||||
|
|
||||||
// center
|
// model a finger as a 3x3 block of IR sensors around the touch point
|
||||||
grid_insert(data, point_x, point_y);
|
// this gives better accuracy (than just 1x1) since the logic below
|
||||||
|
// can toggle anywhere from 1x1 to 2x2, and the game engine calculates
|
||||||
// surrounding points
|
// the center point, which means by inserting up to 8 extra blocks
|
||||||
if (games::rb::TOUCH_SIZE == 3) {
|
// we are emulating a sub-"pixel" resolution
|
||||||
|
grid_insert(data, point_x, point_y); // center
|
||||||
grid_insert(data, point_x - offset_x, point_y); // west
|
grid_insert(data, point_x - offset_x, point_y); // west
|
||||||
grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest
|
grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest
|
||||||
grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest
|
grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest
|
||||||
@@ -175,10 +201,11 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
|
|||||||
grid_insert(data, point_x, point_y - offset_y); // north
|
grid_insert(data, point_x, point_y - offset_y); // north
|
||||||
grid_insert(data, point_x, point_y + offset_y); // south
|
grid_insert(data, point_x, point_y + offset_y); // south
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
touch_debug_publish(data, is_landscape);
|
||||||
|
|
||||||
// copy data to buffer
|
// copy data to buffer
|
||||||
memcpy(lpBuffer, data, 20);
|
memcpy(lpBuffer, data, TOUCH_PACKET_SIZE);
|
||||||
|
|
||||||
// update frame logging
|
// update frame logging
|
||||||
if (log_fps) {
|
if (log_fps) {
|
||||||
@@ -193,7 +220,7 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
|
|||||||
}
|
}
|
||||||
|
|
||||||
// return amount of bytes written
|
// return amount of bytes written
|
||||||
return 20;
|
return TOUCH_PACKET_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
int games::rb::ReflecBeatTouchDeviceHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) {
|
int games::rb::ReflecBeatTouchDeviceHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) {
|
||||||
@@ -207,6 +234,8 @@ int games::rb::ReflecBeatTouchDeviceHandle::device_io(DWORD dwIoControlCode, LPV
|
|||||||
|
|
||||||
bool games::rb::ReflecBeatTouchDeviceHandle::close() {
|
bool games::rb::ReflecBeatTouchDeviceHandle::close() {
|
||||||
|
|
||||||
|
touch_debug_detach();
|
||||||
|
|
||||||
// detach touch module
|
// detach touch module
|
||||||
touch_detach();
|
touch_detach();
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ namespace games::rb {
|
|||||||
int window_width = 1080;
|
int window_width = 1080;
|
||||||
int window_height = 1920;
|
int window_height = 1920;
|
||||||
|
|
||||||
|
// game window resolved in open(); read() sizes against this instead of the
|
||||||
|
// foreground window so touch stays correctly scaled and keeps working when
|
||||||
|
// the game is not the foreground window (rawinput delivers touch regardless
|
||||||
|
// of focus)
|
||||||
|
HWND game_hwnd = nullptr;
|
||||||
|
|
||||||
// logging
|
// logging
|
||||||
bool log_fps = false;
|
bool log_fps = false;
|
||||||
uint64_t log_time = 0;
|
uint64_t log_time = 0;
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
#include "touch_debug.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
|
#include <cstring>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include "external/imgui/imgui.h"
|
||||||
|
#include "games/rb/rb.h"
|
||||||
|
#include "games/rb/touch_defs.h"
|
||||||
|
|
||||||
|
namespace games::rb {
|
||||||
|
|
||||||
|
struct TouchDebugState {
|
||||||
|
std::array<unsigned char, TOUCH_PACKET_SIZE> packet {};
|
||||||
|
bool is_landscape = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::atomic_bool TOUCH_DEBUG_OVERLAY = false;
|
||||||
|
static std::atomic_bool TOUCH_ACTIVE = false;
|
||||||
|
static std::mutex TOUCH_DEBUG_STATE_M;
|
||||||
|
static TouchDebugState TOUCH_DEBUG_STATE;
|
||||||
|
|
||||||
|
static float touch_scale_factor() {
|
||||||
|
return TOUCH_SCALING / (float) TOUCH_SCALE_DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clear_touch_debug_state() {
|
||||||
|
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
|
||||||
|
TOUCH_DEBUG_STATE = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static TouchDebugState get_touch_debug_state() {
|
||||||
|
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
|
||||||
|
return TOUCH_DEBUG_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool packet_bit_active(
|
||||||
|
const std::array<unsigned char, TOUCH_PACKET_SIZE> &packet, int bit) {
|
||||||
|
return (packet[TOUCH_PACKET_DATA_OFFSET + bit / 8] & (1u << (bit % 8))) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sensor_center(int sensor, int sensor_count, int extent) {
|
||||||
|
return ((sensor * 2 + 1) * extent) / (sensor_count * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
static float sensor_span_position(int sensor, int sensor_count, int extent) {
|
||||||
|
return sensor * (extent - 1) / (float) (sensor_count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool touch_debug_overlay_enabled() {
|
||||||
|
return TOUCH_DEBUG_OVERLAY && TOUCH_ACTIVE.load(std::memory_order_acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_draw_debug_overlay() {
|
||||||
|
if (!touch_debug_overlay_enabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &io = ImGui::GetIO();
|
||||||
|
int width = static_cast<int>(io.DisplaySize.x);
|
||||||
|
int height = static_cast<int>(io.DisplaySize.y);
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float scale_factor = touch_scale_factor();
|
||||||
|
const float left = width * (1.f - scale_factor) / 2.f;
|
||||||
|
const float top = height * (1.f - scale_factor) / 2.f;
|
||||||
|
const float right = width - left;
|
||||||
|
const float bottom = height - top;
|
||||||
|
|
||||||
|
TouchDebugState state = get_touch_debug_state();
|
||||||
|
ImDrawList *draw_list = ImGui::GetBackgroundDrawList();
|
||||||
|
auto draw_line = [&](float x1, float y1, float x2, float y2) {
|
||||||
|
draw_list->AddLine(
|
||||||
|
ImVec2(x1, y1), ImVec2(x2, y2),
|
||||||
|
IM_COL32(0, 255, 64, 255), 2.f);
|
||||||
|
};
|
||||||
|
|
||||||
|
// show the valid input area when touch scaling restricts it
|
||||||
|
if (TOUCH_SCALING != TOUCH_SCALE_DEFAULT) {
|
||||||
|
draw_list->AddRect(
|
||||||
|
ImVec2(left, top), ImVec2(right, bottom),
|
||||||
|
IM_COL32(255, 255, 255, 255), 0.f, 0, 2.f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// spread the usable X sensors 2..45 from edge to edge
|
||||||
|
for (int sensor = X_SENSOR_FIRST_ACTIVE; sensor <= X_SENSOR_LAST_ACTIVE; sensor++) {
|
||||||
|
if (!packet_bit_active(state.packet, X_SENSOR_FIRST_BIT + sensor)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float position = sensor_span_position(
|
||||||
|
sensor - X_SENSOR_FIRST_ACTIVE, X_SENSOR_ACTIVE_COUNT,
|
||||||
|
state.is_landscape ? height : width);
|
||||||
|
if (state.is_landscape) {
|
||||||
|
float y = top + position * scale_factor;
|
||||||
|
draw_line(left, y, right, y);
|
||||||
|
} else {
|
||||||
|
float x = left + position * scale_factor;
|
||||||
|
draw_line(x, top, x, bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int sensor = 0; sensor < Y_SENSOR_COUNT; sensor++) {
|
||||||
|
if (!packet_bit_active(state.packet, Y_SENSOR_FIRST_BIT - sensor)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int position = sensor_center(
|
||||||
|
sensor, Y_SENSOR_COUNT,
|
||||||
|
state.is_landscape ? width : height);
|
||||||
|
if (state.is_landscape) {
|
||||||
|
float x = right - position * scale_factor;
|
||||||
|
draw_line(x, top, x, bottom);
|
||||||
|
} else {
|
||||||
|
float y = top + position * scale_factor;
|
||||||
|
draw_line(left, y, right, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_debug_attach() {
|
||||||
|
clear_touch_debug_state();
|
||||||
|
TOUCH_ACTIVE.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_debug_detach() {
|
||||||
|
TOUCH_ACTIVE.store(false, std::memory_order_release);
|
||||||
|
clear_touch_debug_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_debug_publish(const unsigned char *data, bool is_landscape) {
|
||||||
|
if (!TOUCH_DEBUG_OVERLAY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
|
||||||
|
memcpy(TOUCH_DEBUG_STATE.packet.data(), data, TOUCH_PACKET_SIZE);
|
||||||
|
TOUCH_DEBUG_STATE.is_landscape = is_landscape;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
namespace games::rb {
|
||||||
|
extern std::atomic_bool TOUCH_DEBUG_OVERLAY;
|
||||||
|
|
||||||
|
bool touch_debug_overlay_enabled();
|
||||||
|
void touch_draw_debug_overlay();
|
||||||
|
|
||||||
|
void touch_debug_attach();
|
||||||
|
void touch_debug_detach();
|
||||||
|
void touch_debug_publish(const unsigned char *data, bool is_landscape);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace games::rb {
|
||||||
|
inline constexpr int TOUCH_SCALE_DEFAULT = 1000;
|
||||||
|
inline constexpr int TOUCH_PACKET_SIZE = 20;
|
||||||
|
inline constexpr int TOUCH_PACKET_DATA_OFFSET = 3;
|
||||||
|
|
||||||
|
inline constexpr int X_SENSOR_COUNT = 48;
|
||||||
|
inline constexpr int X_SENSOR_FIRST_ACTIVE = 2;
|
||||||
|
inline constexpr int X_SENSOR_LAST_ACTIVE = 45;
|
||||||
|
inline constexpr int X_SENSOR_ACTIVE_COUNT =
|
||||||
|
X_SENSOR_LAST_ACTIVE - X_SENSOR_FIRST_ACTIVE + 1;
|
||||||
|
inline constexpr int X_SENSOR_FIRST_BIT = 88;
|
||||||
|
|
||||||
|
inline constexpr int Y_SENSOR_COUNT = 76;
|
||||||
|
inline constexpr int Y_SENSOR_FIRST_BIT = 75;
|
||||||
|
}
|
||||||
@@ -32,19 +32,29 @@ namespace hooks::audio {
|
|||||||
RegEnumKeyA(hkEnum, index, key_name, sizeof(key_name)) == ERROR_SUCCESS;
|
RegEnumKeyA(hkEnum, index, key_name, sizeof(key_name)) == ERROR_SUCCESS;
|
||||||
index++) {
|
index++) {
|
||||||
|
|
||||||
// read description (display name), fall back to the key name
|
// read description (display name), fall back to the key name.
|
||||||
|
// RegOpenKeyExA + RegQueryValueExA is used instead of RegGetValueA
|
||||||
|
// because the latter is unavailable on Windows XP.
|
||||||
char desc[256] = { 0 };
|
char desc[256] = { 0 };
|
||||||
DWORD size = sizeof(desc);
|
DWORD size = sizeof(desc);
|
||||||
std::string name = key_name;
|
std::string name = key_name;
|
||||||
if (RegGetValueA(hkEnum,
|
HKEY hkDriver = nullptr;
|
||||||
key_name,
|
if (RegOpenKeyExA(hkEnum, key_name, 0,
|
||||||
|
KEY_QUERY_VALUE | wow64_flag, &hkDriver) == ERROR_SUCCESS) {
|
||||||
|
DWORD type = 0;
|
||||||
|
if (RegQueryValueExA(hkDriver,
|
||||||
ASIO_REG_DESC,
|
ASIO_REG_DESC,
|
||||||
RRF_RT_REG_SZ | wow64_flag,
|
|
||||||
nullptr,
|
nullptr,
|
||||||
desc,
|
&type,
|
||||||
&size) == ERROR_SUCCESS && desc[0]) {
|
reinterpret_cast<LPBYTE>(desc),
|
||||||
|
&size) == ERROR_SUCCESS
|
||||||
|
&& type == REG_SZ && desc[0]) {
|
||||||
|
// ensure null termination
|
||||||
|
desc[sizeof(desc) - 1] = '\0';
|
||||||
name = desc;
|
name = desc;
|
||||||
}
|
}
|
||||||
|
RegCloseKey(hkDriver);
|
||||||
|
}
|
||||||
|
|
||||||
// merge with an existing entry from the other view (match by name)
|
// merge with an existing entry from the other view (match by name)
|
||||||
const std::string name_lower = strtolower(name);
|
const std::string name_lower = strtolower(name);
|
||||||
|
|||||||
@@ -154,36 +154,30 @@ namespace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// live wrappers by CLSID. ASIO drivers are single-instance, so a host that creates a
|
// one wrapper per CLSID, kept alive for the process lifetime. ASIO drivers are
|
||||||
// new instance without releasing the old one has leaked it; we track this to tear the
|
// single-instance, and some hardware drivers (e.g. Neva Uno) crash if their COM object
|
||||||
// stale one down
|
// is destroyed and re-created within a process - which the host triggers by leaking and
|
||||||
|
// re-instantiating the driver during startup probing. so we build the real driver and
|
||||||
|
// its wrapper once, hold a reference so it survives the host's Release calls, and hand
|
||||||
|
// the same wrapper back for every later CoCreate. as a result the real driver is created
|
||||||
|
// and initialized exactly once
|
||||||
std::mutex g_wrappers_mutex;
|
std::mutex g_wrappers_mutex;
|
||||||
std::vector<std::pair<CLSID, WrappedAsio *>> g_wrappers;
|
std::vector<std::pair<CLSID, WrappedAsio *>> g_wrappers;
|
||||||
|
|
||||||
// register a wrapper as the live instance for its CLSID, returning any stale wrapper it
|
WrappedAsio *find_wrapper(REFCLSID clsid) {
|
||||||
// replaces so the caller can tear it down outside the lock
|
|
||||||
WrappedAsio *register_wrapper(REFCLSID clsid, WrappedAsio *wrapper) {
|
|
||||||
std::lock_guard lock(g_wrappers_mutex);
|
std::lock_guard lock(g_wrappers_mutex);
|
||||||
for (auto &entry : g_wrappers) {
|
for (auto &entry : g_wrappers) {
|
||||||
if (IsEqualCLSID(entry.first, clsid)) {
|
if (IsEqualCLSID(entry.first, clsid)) {
|
||||||
WrappedAsio *stale = entry.second;
|
return entry.second;
|
||||||
entry.second = wrapper;
|
|
||||||
return stale;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
g_wrappers.emplace_back(clsid, wrapper);
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void unregister_wrapper(WrappedAsio *wrapper) {
|
void store_wrapper(REFCLSID clsid, WrappedAsio *wrapper) {
|
||||||
std::lock_guard lock(g_wrappers_mutex);
|
std::lock_guard lock(g_wrappers_mutex);
|
||||||
for (auto it = g_wrappers.begin(); it != g_wrappers.end(); ++it) {
|
g_wrappers.emplace_back(clsid, wrapper);
|
||||||
if (it->second == wrapper) {
|
|
||||||
g_wrappers.erase(it);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ASIO drivers registered on this system (CLSID + registry name), scanned once
|
// ASIO drivers registered on this system (CLSID + registry name), scanned once
|
||||||
@@ -256,12 +250,11 @@ WrappedAsio::StereoDownmix WrappedAsio::name_to_stereo_downmix(const char *name)
|
|||||||
}
|
}
|
||||||
|
|
||||||
WrappedAsio::~WrappedAsio() {
|
WrappedAsio::~WrappedAsio() {
|
||||||
unregister_wrapper(this);
|
|
||||||
|
|
||||||
this->detach_post_process();
|
this->detach_post_process();
|
||||||
|
|
||||||
// our refcount is decoupled from the host's (see AddRef/Release), so pReal's count is
|
// never runs mid-run: wrap() pins the wrapper so the refcount stays above zero until
|
||||||
// exactly one here and this releases/unloads it deterministically
|
// release_all_wrappers() drops the pin at shutdown - the only point this can fire, and
|
||||||
|
// only once the host has released its own references. tears down the real driver
|
||||||
this->pReal->Release();
|
this->pReal->Release();
|
||||||
|
|
||||||
log_info("audio::wrappedasio", "destroying wrapped ASIO driver, clsid={}", guid2s(this->clsid));
|
log_info("audio::wrappedasio", "destroying wrapped ASIO driver, clsid={}", guid2s(this->clsid));
|
||||||
@@ -315,8 +308,17 @@ ULONG STDMETHODCALLTYPE WrappedAsio::Release() {
|
|||||||
|
|
||||||
#pragma region IAsio
|
#pragma region IAsio
|
||||||
AsioBool __thiscall WrappedAsio::init(void *sys_handle) {
|
AsioBool __thiscall WrappedAsio::init(void *sys_handle) {
|
||||||
|
// the real driver is single-instance and kept alive; initialize it exactly once. the
|
||||||
|
// host re-calls init() on each CoCreate during probing, but re-initializing a live
|
||||||
|
// driver crashes some hardware drivers, so once we have a live driver we report success
|
||||||
|
if (this->initialized) {
|
||||||
|
log_misc("audio::wrappedasio", "init skipped, '{}' already initialized", this->driver_name);
|
||||||
|
return AsioTrue;
|
||||||
|
}
|
||||||
|
|
||||||
const AsioBool result = this->pReal->init(sys_handle);
|
const AsioBool result = this->pReal->init(sys_handle);
|
||||||
if (result == AsioTrue) {
|
if (result == AsioTrue) {
|
||||||
|
this->initialized = true;
|
||||||
log_info(
|
log_info(
|
||||||
"audio::wrappedasio",
|
"audio::wrappedasio",
|
||||||
"init succeeded for '{}' (driver version {})",
|
"init succeeded for '{}' (driver version {})",
|
||||||
@@ -346,6 +348,7 @@ void __thiscall WrappedAsio::get_error_message(char *string) {
|
|||||||
AsioError __thiscall WrappedAsio::start() {
|
AsioError __thiscall WrappedAsio::start() {
|
||||||
const AsioError result = this->pReal->start();
|
const AsioError result = this->pReal->start();
|
||||||
if (result == ASE_OK) {
|
if (result == ASE_OK) {
|
||||||
|
this->started = true;
|
||||||
log_info(
|
log_info(
|
||||||
"audio::wrappedasio",
|
"audio::wrappedasio",
|
||||||
"start succeeded, ASIO stream is now running on '{}'",
|
"start succeeded, ASIO stream is now running on '{}'",
|
||||||
@@ -360,6 +363,7 @@ AsioError __thiscall WrappedAsio::start() {
|
|||||||
AsioError __thiscall WrappedAsio::stop() {
|
AsioError __thiscall WrappedAsio::stop() {
|
||||||
const AsioError result = this->pReal->stop();
|
const AsioError result = this->pReal->stop();
|
||||||
if (result == ASE_OK) {
|
if (result == ASE_OK) {
|
||||||
|
this->started = false;
|
||||||
log_info("audio::wrappedasio", "stop succeeded, ASIO stream on '{}' halted", this->driver_name);
|
log_info("audio::wrappedasio", "stop succeeded, ASIO stream on '{}' halted", this->driver_name);
|
||||||
} else {
|
} else {
|
||||||
log_warning("audio::wrappedasio", "stop failed, err={}", static_cast<long>(result));
|
log_warning("audio::wrappedasio", "stop failed, err={}", static_cast<long>(result));
|
||||||
@@ -740,6 +744,27 @@ void WrappedAsio::detach_post_process() {
|
|||||||
WrappedAsio::active_instance.compare_exchange_strong(expected, nullptr);
|
WrappedAsio::active_instance.compare_exchange_strong(expected, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WrappedAsio::quiesce_for_reuse() {
|
||||||
|
// a previous abandoned probing cycle may have left a running stream and live buffers on
|
||||||
|
// the real driver without calling stop/dispose; tear that down now (without destroying
|
||||||
|
// the driver) so the host's next create_buffers starts clean. stop() guarantees no
|
||||||
|
// further buffer_switch, and dispose_buffers detaches our trampolines
|
||||||
|
if (this->started) {
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"reuse: stopping leftover stream on '{}' before handing the driver back",
|
||||||
|
this->driver_name);
|
||||||
|
this->stop();
|
||||||
|
}
|
||||||
|
if (this->buffers_created) {
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"reuse: disposing leftover buffers on '{}' before handing the driver back",
|
||||||
|
this->driver_name);
|
||||||
|
this->dispose_buffers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void WrappedAsio::apply_output_volume(long double_buffer_index) {
|
void WrappedAsio::apply_output_volume(long double_buffer_index) {
|
||||||
if (double_buffer_index != 0 && double_buffer_index != 1) {
|
if (double_buffer_index != 0 && double_buffer_index != 1) {
|
||||||
return;
|
return;
|
||||||
@@ -838,6 +863,7 @@ AsioError __thiscall WrappedAsio::create_buffers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this->publish_post_process(buffer_size);
|
this->publish_post_process(buffer_size);
|
||||||
|
this->buffers_created = true;
|
||||||
} else {
|
} else {
|
||||||
log_warning(
|
log_warning(
|
||||||
"audio::wrappedasio",
|
"audio::wrappedasio",
|
||||||
@@ -940,6 +966,7 @@ AsioError WrappedAsio::create_buffers_front_pair(
|
|||||||
// device's 2.0 output. then publish ourselves to the realtime thread
|
// device's 2.0 output. then publish ourselves to the realtime thread
|
||||||
this->record_downmix_channels(buffer_infos, num_channels, buffer_size);
|
this->record_downmix_channels(buffer_infos, num_channels, buffer_size);
|
||||||
this->publish_post_process(buffer_size);
|
this->publish_post_process(buffer_size);
|
||||||
|
this->buffers_created = true;
|
||||||
|
|
||||||
log_info(
|
log_info(
|
||||||
"audio::wrappedasio",
|
"audio::wrappedasio",
|
||||||
@@ -962,6 +989,7 @@ AsioError __thiscall WrappedAsio::dispose_buffers() {
|
|||||||
this->volume_channels.clear();
|
this->volume_channels.clear();
|
||||||
this->volume_active = false;
|
this->volume_active = false;
|
||||||
this->downmix_active = false;
|
this->downmix_active = false;
|
||||||
|
this->buffers_created = false;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -986,29 +1014,54 @@ namespace hooks::audio::asio {
|
|||||||
auto *wrapper = new WrappedAsio(
|
auto *wrapper = new WrappedAsio(
|
||||||
reinterpret_cast<IAsio *>(real), clsid, registered_asio_name(clsid));
|
reinterpret_cast<IAsio *>(real), clsid, registered_asio_name(clsid));
|
||||||
|
|
||||||
// if the host already had a live wrapper for this CLSID it leaked the previous
|
// pin the wrapper (and the real driver it owns) for the process lifetime; later
|
||||||
// instance (ASIO is single-instance); tear it down now so the real driver is
|
// CoCreate calls reuse this same instance via wrap_existing
|
||||||
// released before the reinit. works around games (iidx32+) that ref twice but
|
wrapper->AddRef();
|
||||||
// deref once before re-initializing.
|
store_wrapper(clsid, wrapper);
|
||||||
//
|
|
||||||
// FlexASIO 1.9 and many DAC ASIO drivers can't handle this; FlexASIO 1.10 and
|
|
||||||
// Xonar AE can
|
|
||||||
if (WrappedAsio *stale = register_wrapper(clsid, wrapper)) {
|
|
||||||
log_info(
|
|
||||||
"audio::wrappedasio",
|
|
||||||
"host did not release previous instance for clsid={}, forcing teardown",
|
|
||||||
guid2s(clsid));
|
|
||||||
|
|
||||||
// the stale instance may still have a running stream on the driver's realtime
|
|
||||||
// thread. stop and dispose it before deleting so no in-flight buffer switch
|
|
||||||
// (e.g. our volume trampoline) touches buffers we are about to free. ASIO
|
|
||||||
// guarantees no further buffer_switch once stop() returns, and dispose_buffers
|
|
||||||
// detaches our trampolines, fully quiescing the realtime path before delete
|
|
||||||
stale->stop();
|
|
||||||
stale->dispose_buffers();
|
|
||||||
delete stale;
|
|
||||||
}
|
|
||||||
|
|
||||||
return static_cast<IAsio *>(wrapper);
|
return static_cast<IAsio *>(wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IUnknown *wrap_existing(REFCLSID clsid) {
|
||||||
|
WrappedAsio *wrapper = find_wrapper(clsid);
|
||||||
|
if (wrapper == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_misc(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"reusing cached ASIO driver instance, clsid={}",
|
||||||
|
guid2s(clsid));
|
||||||
|
|
||||||
|
wrapper->quiesce_for_reuse();
|
||||||
|
|
||||||
|
// hand the host another reference to the one instance we keep alive
|
||||||
|
wrapper->AddRef();
|
||||||
|
return static_cast<IAsio *>(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
void release_all_wrappers() {
|
||||||
|
// collect the pinned wrappers under the lock, then drop our references outside it so
|
||||||
|
// a final Release (which can run the driver's own teardown) never happens while the
|
||||||
|
// lock is held
|
||||||
|
std::vector<std::pair<CLSID, WrappedAsio *>> wrappers;
|
||||||
|
{
|
||||||
|
std::lock_guard lock(g_wrappers_mutex);
|
||||||
|
wrappers = g_wrappers;
|
||||||
|
g_wrappers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto &entry : wrappers) {
|
||||||
|
// drop only the pin wrap() took. if the host has already released its own
|
||||||
|
// references (the normal case at shutdown) this destroys the wrapper and releases
|
||||||
|
// the real driver; if the host is still using it, the refcount stays above zero
|
||||||
|
// and we leave its live stream/buffers untouched
|
||||||
|
log_misc(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"releasing cached ASIO driver instance, clsid={}",
|
||||||
|
guid2s(entry.first));
|
||||||
|
entry.second->Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,20 @@ namespace hooks::audio::asio {
|
|||||||
bool is_asio_creation(REFCLSID rclsid, REFIID riid);
|
bool is_asio_creation(REFCLSID rclsid, REFIID riid);
|
||||||
|
|
||||||
// wrap a real ASIO driver instance, taking ownership of the supplied reference, and
|
// wrap a real ASIO driver instance, taking ownership of the supplied reference, and
|
||||||
// return a proxy that forwards every call to it
|
// return a proxy that forwards every call to it. also records it as the cached
|
||||||
|
// instance for its CLSID so later CoCreate calls can reuse it (see wrap_existing)
|
||||||
IUnknown *wrap(REFCLSID clsid, void *real);
|
IUnknown *wrap(REFCLSID clsid, void *real);
|
||||||
|
|
||||||
|
// if a cached wrapper already exists for this CLSID, return it (with an added
|
||||||
|
// reference); otherwise nullptr to signal the caller to create the real driver and
|
||||||
|
// wrap it. lets the host reuse one driver instance instead of re-instantiating it
|
||||||
|
IUnknown *wrap_existing(REFCLSID clsid);
|
||||||
|
|
||||||
|
// drop the process-lifetime references taken by wrap() so cached drivers can be released
|
||||||
|
// at shutdown. only relinquishes our pin, so a real driver is torn down once the host
|
||||||
|
// has released its own references too. call from a controlled shutdown point, never from
|
||||||
|
// a static destructor (the driver DLL may already be unloaded)
|
||||||
|
void release_all_wrappers();
|
||||||
}
|
}
|
||||||
|
|
||||||
// transparent proxy around a real ASIO driver; a single place to intercept ASIO traffic
|
// transparent proxy around a real ASIO driver; a single place to intercept ASIO traffic
|
||||||
@@ -103,6 +115,10 @@ struct WrappedAsio final : IAsio {
|
|||||||
AsioError __thiscall output_ready() override;
|
AsioError __thiscall output_ready() override;
|
||||||
#pragma endregion
|
#pragma endregion
|
||||||
|
|
||||||
|
// quiesces any leftover stream/buffer state before the cached wrapper is handed back
|
||||||
|
// for reuse, without destroying the real driver (see wrap_existing)
|
||||||
|
void quiesce_for_reuse();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// create_buffers implementation used when a stereo extraction is active: forwards only
|
// create_buffers implementation used when a stereo extraction is active: forwards only
|
||||||
// the channels the real device has and hands the game throwaway buffers for the rest
|
// the channels the real device has and hands the game throwaway buffers for the rest
|
||||||
@@ -170,6 +186,14 @@ private:
|
|||||||
// unambiguous name; constant for our lifetime
|
// unambiguous name; constant for our lifetime
|
||||||
std::string driver_name;
|
std::string driver_name;
|
||||||
|
|
||||||
|
// the real driver is initialized exactly once; repeat init() calls are a no-op success
|
||||||
|
bool initialized = false;
|
||||||
|
|
||||||
|
// whether the real driver currently has a buffer set / running stream. used to quiesce
|
||||||
|
// leftover state when the cached wrapper is reused (see quiesce_for_reuse)
|
||||||
|
bool buffers_created = false;
|
||||||
|
bool started = false;
|
||||||
|
|
||||||
// our own reference count; we hold one reference on pReal and release it when this
|
// our own reference count; we hold one reference on pReal and release it when this
|
||||||
// drops to zero
|
// drops to zero
|
||||||
std::atomic<ULONG> ref_count {1};
|
std::atomic<ULONG> ref_count {1};
|
||||||
|
|||||||
@@ -68,6 +68,15 @@ static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
|
|||||||
|
|
||||||
HRESULT ret;
|
HRESULT ret;
|
||||||
|
|
||||||
|
// ASIO: if we already hold a cached instance for this CLSID, hand back a fresh
|
||||||
|
// wrapper over it instead of re-creating the real driver (see asio_proxy.cpp)
|
||||||
|
if (ppv != nullptr && hooks::audio::asio::is_asio_creation(rclsid, riid)) {
|
||||||
|
if (IUnknown *reused = hooks::audio::asio::wrap_existing(rclsid)) {
|
||||||
|
*ppv = reused;
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// call original
|
// call original
|
||||||
ret = CoCreateInstance_orig(rclsid, pUnkOuter, dwClsContext, riid, ppv);
|
ret = CoCreateInstance_orig(rclsid, pUnkOuter, dwClsContext, riid, ppv);
|
||||||
if (FAILED(ret)) {
|
if (FAILED(ret)) {
|
||||||
@@ -130,5 +139,9 @@ namespace hooks::audio {
|
|||||||
CLIENT = nullptr;
|
CLIENT = nullptr;
|
||||||
}
|
}
|
||||||
stop_low_latency();
|
stop_low_latency();
|
||||||
|
|
||||||
|
// release the ASIO drivers we kept pinned for the process lifetime now that we are
|
||||||
|
// at a controlled shutdown point (see asio_proxy.cpp)
|
||||||
|
hooks::audio::asio::release_all_wrappers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
#include "launcher/shutdown.h"
|
#include "launcher/shutdown.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
#include "touch/touch_indicators.h"
|
#include "touch/touch_gestures.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/fileutils.h"
|
#include "util/fileutils.h"
|
||||||
@@ -627,7 +627,7 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disable_touch_indicators(result);
|
disable_touch_gestures(result);
|
||||||
log_misc(
|
log_misc(
|
||||||
"graphics",
|
"graphics",
|
||||||
"CreateWindowExA returned {}, {}",
|
"CreateWindowExA returned {}, {}",
|
||||||
@@ -722,7 +722,7 @@ static HWND WINAPI CreateWindowExW_hook(DWORD dwExStyle, LPCWSTR lpClassName, LP
|
|||||||
fmt::ptr(result),
|
fmt::ptr(result),
|
||||||
lpWindowName ? ws2s(lpWindowName) : "(null)");
|
lpWindowName ? ws2s(lpWindowName) : "(null)");
|
||||||
|
|
||||||
disable_touch_indicators(result);
|
disable_touch_gestures(result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
#include "games/popn/popn.h"
|
#include "games/popn/popn.h"
|
||||||
#include "games/qma/qma.h"
|
#include "games/qma/qma.h"
|
||||||
#include "games/rb/rb.h"
|
#include "games/rb/rb.h"
|
||||||
|
#include "games/rb/touch_debug.h"
|
||||||
#include "games/rf3d/rf3d.h"
|
#include "games/rf3d/rf3d.h"
|
||||||
#include "games/sc/sc.h"
|
#include "games/sc/sc.h"
|
||||||
#include "games/scotto/scotto.h"
|
#include "games/scotto/scotto.h"
|
||||||
@@ -1064,6 +1065,15 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::InvertTouchCoordinates].value_bool()) {
|
if (options[launcher::Options::InvertTouchCoordinates].value_bool()) {
|
||||||
rawinput::touch::INVERTED = true;
|
rawinput::touch::INVERTED = true;
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::RawInputTouchAspectRatio].is_active()) {
|
||||||
|
auto mode = options[launcher::Options::RawInputTouchAspectRatio].value_text();
|
||||||
|
if (mode == "on") {
|
||||||
|
rawinput::touch::ASPECT_COMPENSATION_MODE = rawinput::touch::AspectMode::On;
|
||||||
|
} else if (mode == "off") {
|
||||||
|
rawinput::touch::ASPECT_COMPENSATION_MODE = rawinput::touch::AspectMode::Off;
|
||||||
|
}
|
||||||
|
// "auto" leaves the default (per-game)
|
||||||
|
}
|
||||||
// DisableTouchCardInsert is no longer honored in spice2x
|
// DisableTouchCardInsert is no longer honored in spice2x
|
||||||
// if (options[launcher::Options::DisableTouchCardInsert].value_bool()) {
|
// if (options[launcher::Options::DisableTouchCardInsert].value_bool()) {
|
||||||
// SPICETOUCH_CARD_DISABLE = true;
|
// SPICETOUCH_CARD_DISABLE = true;
|
||||||
@@ -1105,6 +1115,9 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::DisableOverlay].value_bool()) {
|
if (options[launcher::Options::DisableOverlay].value_bool()) {
|
||||||
overlay::ENABLED = false;
|
overlay::ENABLED = false;
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::OverlayKeyboardNavigation].value_bool()) {
|
||||||
|
overlay::ENABLE_KEYBOARD_NAVIGATION = true;
|
||||||
|
}
|
||||||
if (options[launcher::Options::OverlayScaling].is_active() && !cfg::CONFIGURATOR_STANDALONE && !cfg_run) {
|
if (options[launcher::Options::OverlayScaling].is_active() && !cfg::CONFIGURATOR_STANDALONE && !cfg_run) {
|
||||||
const auto val = options[launcher::Options::OverlayScaling].value_uint32();
|
const auto val = options[launcher::Options::OverlayScaling].value_uint32();
|
||||||
if (10 <= val && val <= 400 && val != 100) {
|
if (10 <= val && val <= 400 && val != 100) {
|
||||||
@@ -1305,21 +1318,36 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::AcAccurate;
|
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::AcAccurate;
|
||||||
} else if (options[launcher::Options::JubeatTouchAlgo].value_text() == "legacy") {
|
} else if (options[launcher::Options::JubeatTouchAlgo].value_text() == "legacy") {
|
||||||
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Legacy;
|
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Legacy;
|
||||||
|
} else if (options[launcher::Options::JubeatTouchAlgo].value_text() == "plus") {
|
||||||
|
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Plus;
|
||||||
} else {
|
} else {
|
||||||
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Improved;
|
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Improved;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::JubeatTouchDebug].is_active()) {
|
||||||
|
auto mode = options[launcher::Options::JubeatTouchDebug].value_text();
|
||||||
|
if (mode == "none") {
|
||||||
|
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugNone;
|
||||||
|
} else if (mode == "box") {
|
||||||
|
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugBox;
|
||||||
|
} else if (mode == "all") {
|
||||||
|
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugAll;
|
||||||
|
} else {
|
||||||
|
games::jb::TOUCH_DEBUG_OVERLAY = games::jb::JubeatTouchDebugMode::JbTouchDebugAuto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options[launcher::Options::JubeatTouchDebounce].is_active()) {
|
||||||
|
games::jb::TOUCH_DEBOUNCE_MS =
|
||||||
|
options[launcher::Options::JubeatTouchDebounce].value_uint32();
|
||||||
|
}
|
||||||
|
|
||||||
// reflec beat touch emulation
|
// reflec beat touch emulation
|
||||||
|
games::rb::TOUCH_DEBUG_OVERLAY = options[launcher::Options::RBTouchDebug].value_bool();
|
||||||
if (options[launcher::Options::spice2x_RBTouchScale].is_active()) {
|
if (options[launcher::Options::spice2x_RBTouchScale].is_active()) {
|
||||||
games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32();
|
games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32();
|
||||||
}
|
}
|
||||||
if (options[launcher::Options::RBTouchSize].is_active()) {
|
// -rbtouchsize is deprecated and ignored; touch emulation always uses the 3x3 model
|
||||||
const auto text = options[launcher::Options::RBTouchSize].value_text();
|
|
||||||
if (text == "3") {
|
|
||||||
games::rb::TOUCH_SIZE = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (options[launcher::Options::RBTouchPollRate].is_active()) {
|
if (options[launcher::Options::RBTouchPollRate].is_active()) {
|
||||||
games::rb::TOUCH_POLL_RATE = options[launcher::Options::RBTouchPollRate].value_uint32();
|
games::rb::TOUCH_POLL_RATE = options[launcher::Options::RBTouchPollRate].value_uint32();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -434,18 +434,23 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.category = "Advanced Network",
|
.category = "Advanced Network",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "Preferred NetAdapter IP",
|
.title = "Preferred Network Adapter's IP",
|
||||||
.name = "network",
|
.name = "network",
|
||||||
.desc = "This is NOT the EA service URL; use -url for that. "
|
.display_name = "netadapterip",
|
||||||
"Force the use of an adapter with the specified network. Must also provide -subnet.",
|
.aliases = "netadapterip",
|
||||||
|
.desc = "Instead of using the default network adapter, force the usage of another network adapter "
|
||||||
|
"with the specified IP address. You must also set -netadaptersubnet.",
|
||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.category = "Advanced Network",
|
.category = "Advanced Network",
|
||||||
.sensitive = true,
|
.sensitive = true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "Preferred NetAdapter Subnet",
|
.title = "Preferred Network Adapter's Subnet",
|
||||||
.name = "subnet",
|
.name = "subnet",
|
||||||
.desc = "Force the use of an adapter with the specified subnet. Must also provide -network.",
|
.display_name = "netadaptersubnet",
|
||||||
|
.aliases = "netadaptersubnet",
|
||||||
|
.desc = "Instead of using the default network adapter, force the usage of another network adapter "
|
||||||
|
"with the specified subnet. You must also set -netadapterip.",
|
||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.category = "Advanced Network",
|
.category = "Advanced Network",
|
||||||
},
|
},
|
||||||
@@ -505,6 +510,13 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "General Overlay",
|
.category = "General Overlay",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.title = "Overlay Keyboard Navigation",
|
||||||
|
.name = "keyboardnav",
|
||||||
|
.desc = "Enables keyboard navigation in the in-game overlay.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.category = "General Overlay",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// OverlayScaling
|
// OverlayScaling
|
||||||
.title = "Spice Overlay UI Scale %",
|
.title = "Spice Overlay UI Scale %",
|
||||||
@@ -1031,16 +1043,16 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.title = "SDVX Disable Live2D (EXPERIMENTAL)",
|
.title = "SDVX Disable Live2D (EXPERIMENTAL)",
|
||||||
.name = "sdvxnolive2d",
|
.name = "sdvxnolive2d",
|
||||||
.desc = "Skip rendering the SDVX Live2D graphics to save GPU.\n\n"
|
.desc = "Skip rendering the SDVX Live2D graphics to save GPU.\n\n"
|
||||||
"Off: leave Live2D as-is.\n"
|
"off: leave Live2D as-is.\n"
|
||||||
"Always: never render Live2D (also removes the menu navigator).\n"
|
"always: hide Live2D (also hides menu navigator).\n"
|
||||||
"During songs: only skip Live2D during a song; keeps the menu navigator. Scene detection relies on game logging.",
|
"ingame: only hide Live2D during a song; keeps the menu navigator. Scene detection relies on game logging.",
|
||||||
.type = OptionType::Enum,
|
.type = OptionType::Enum,
|
||||||
.game_name = "Sound Voltex",
|
.game_name = "Sound Voltex",
|
||||||
.category = "Advanced Game Options",
|
.category = "Advanced Game Options",
|
||||||
.elements = {
|
.elements = {
|
||||||
{"off", "Show Live2D"},
|
{"off", "Show Live2D"},
|
||||||
{"always", "Never show Live2D"},
|
{"always", "Hide Live2D"},
|
||||||
{"ingame", "Show navigators only"},
|
{"ingame", "Hide during songs"},
|
||||||
},
|
},
|
||||||
.quick_setting_category = "Game",
|
.quick_setting_category = "Game",
|
||||||
},
|
},
|
||||||
@@ -1772,11 +1784,11 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.category = "I/O Modules",
|
.category = "I/O Modules",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.title = "Disable Raw Input Touch",
|
.title = "Touch Compatibility Mode (Disable Raw Input Touch)",
|
||||||
.name = "wintouch",
|
.name = "wintouch",
|
||||||
.desc = "For touch screen input, disable usage of Raw Input API and instead use "
|
.desc = "For touch screen input, disable usage of Raw Input API and instead use "
|
||||||
"Win8 Pointer API or Win7 Touch API. Only enable this if you have trouble "
|
"Win8 Pointer API or Win7 Touch API. Results in better compatibility with some "
|
||||||
"using the default (raw input) touch input, as Raw Input performs better.",
|
"touch screens, but may result in worse performance and higher latency.",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "Touch Parameters",
|
.category = "Touch Parameters",
|
||||||
},
|
},
|
||||||
@@ -1796,6 +1808,24 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "Touch Parameters",
|
.category = "Touch Parameters",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// RawInputTouchAspectRatio
|
||||||
|
.title = "Raw Input Touch Fix Aspect Ratio",
|
||||||
|
.name = "rawtouchaspect",
|
||||||
|
.desc = "Compensate for letterboxing/pillarboxing when the game runs at a display mode with a "
|
||||||
|
"different aspect ratio than the touch panel's native resolution. Only affects the default "
|
||||||
|
"raw input touch handler. This is automatically enabled, only force on or off if you have trouble.\n\n"
|
||||||
|
"auto (default): automatically enable on a per-game basis.\n"
|
||||||
|
"on: forced on\n"
|
||||||
|
"off: forced off",
|
||||||
|
.type = OptionType::Enum,
|
||||||
|
.category = "Touch Parameters",
|
||||||
|
.elements = {
|
||||||
|
{"auto", ""},
|
||||||
|
{"on", ""},
|
||||||
|
{"off", ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// DisableTouchCardInsert
|
// DisableTouchCardInsert
|
||||||
.title = "Disable Touch Card Insert (DEPRECATED - use -touchcard instead)",
|
.title = "Disable Touch Card Insert (DEPRECATED - use -touchcard instead)",
|
||||||
@@ -1807,12 +1837,14 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// spice2x_TouchCardInsert
|
// spice2x_TouchCardInsert
|
||||||
.title = "Show Insert Card button",
|
.title = "Show Insert Card button (DEPRECATED)",
|
||||||
.name = "sp2x-touchcard",
|
.name = "sp2x-touchcard",
|
||||||
.display_name = "touchcard",
|
.display_name = "touchcard",
|
||||||
.aliases= "touchcard",
|
.aliases= "touchcard",
|
||||||
.desc = "Show Insert Card touch button on main display.",
|
.desc = "Show Insert Card touch button on main display. "
|
||||||
|
"DEPRECATED - only works in very specific situations (jubeat + wintouch)",
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
|
.hidden = true,
|
||||||
.category = "Touch Parameters",
|
.category = "Touch Parameters",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1884,7 +1916,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.title = "HID SmartCard Fix UID",
|
.title = "HID SmartCard Fix UID",
|
||||||
.name = "scardfix",
|
.name = "scardfix",
|
||||||
.desc = "Modify behavior of SmartCard UID logic.\n\n"
|
.desc = "Modify behavior of SmartCard UID logic.\n\n"
|
||||||
"legacy: Preserve buggy old behavior (Default)\n\n"
|
"legacy (default): Preserve buggy old behavior\n\n"
|
||||||
"fix: Add E00401 to non-FeliCa cards, scan FeliCa cards as-is (Recommended for most users)\n\n"
|
"fix: Add E00401 to non-FeliCa cards, scan FeliCa cards as-is (Recommended for most users)\n\n"
|
||||||
"all: Add E00401 to all cards, including FeliCa cards (For really old games only)\n\n"
|
"all: Add E00401 to all cards, including FeliCa cards (For really old games only)\n\n"
|
||||||
"The algorithm is simple; the prefix is applied, the scanned card number is appended up to 8 bytes, and remaining digits are discarded. "
|
"The algorithm is simple; the prefix is applied, the scanned card number is appended up to 8 bytes, and remaining digits are discarded. "
|
||||||
@@ -2668,6 +2700,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.desc = "For touch screen players: choose the touch algorithm to use.\n\n"
|
.desc = "For touch screen players: choose the touch algorithm to use.\n\n"
|
||||||
"legacy - evenly divide the grid into 16 squares; old spicetools behavior, slightly inaccurate in gaps\n\n"
|
"legacy - evenly divide the grid into 16 squares; old spicetools behavior, slightly inaccurate in gaps\n\n"
|
||||||
"improved (default) - squares register as-is, gaps will trigger the closest square\n\n"
|
"improved (default) - squares register as-is, gaps will trigger the closest square\n\n"
|
||||||
|
"plus - like improved, but gaps can trigger multiple buttons (like the mobile game)\n\n"
|
||||||
"accurate - only touches within squares will trigger; gaps do nothing (for AC size touch screens)",
|
"accurate - only touches within squares will trigger; gaps do nothing (for AC size touch screens)",
|
||||||
.type = OptionType::Enum,
|
.type = OptionType::Enum,
|
||||||
.game_name = "Jubeat",
|
.game_name = "Jubeat",
|
||||||
@@ -2675,10 +2708,58 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.elements = {
|
.elements = {
|
||||||
{"legacy", ""},
|
{"legacy", ""},
|
||||||
{"improved", ""},
|
{"improved", ""},
|
||||||
|
{"plus", ""},
|
||||||
{"accurate", ""},
|
{"accurate", ""},
|
||||||
},
|
},
|
||||||
.quick_setting_category = "Game",
|
.quick_setting_category = "Game",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// JubeatTouchDebug
|
||||||
|
.title = "JB Touch Debug Overlay",
|
||||||
|
.name = "jubeattouchdebug",
|
||||||
|
.desc = "For touch screen players: draw a debug overlay on the main display. "
|
||||||
|
"Requires the Spice Overlay to be enabled.\n\n"
|
||||||
|
"auto (default) - show boundary boxes when a touch screen is detected; otherwise, none\n\n"
|
||||||
|
"none - draw nothing\n\n"
|
||||||
|
"box - show the 4x4 touch boundary boxes\n\n"
|
||||||
|
"all - show both the boxes and the touch circles",
|
||||||
|
.type = OptionType::Enum,
|
||||||
|
.game_name = "Jubeat",
|
||||||
|
.category = "Game Options",
|
||||||
|
.elements = {
|
||||||
|
{"auto", ""},
|
||||||
|
{"none", ""},
|
||||||
|
{"box", ""},
|
||||||
|
{"all", ""},
|
||||||
|
},
|
||||||
|
.quick_setting_category = "Game",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// JubeatTouchDebounce
|
||||||
|
.title = "JB Touch Debounce",
|
||||||
|
.name = "jubeattouchdebounce",
|
||||||
|
.desc = "For touch screen players: ignore extremely quick touches by requiring a "
|
||||||
|
"touch to be held for at least this many milliseconds before it registers.\n\n"
|
||||||
|
"Useful for filtering phantom touches on a noisy touch screen. A higher value "
|
||||||
|
"adds input latency, so keep it small. Default: off (0).",
|
||||||
|
.type = OptionType::Integer,
|
||||||
|
.setting_name = "8",
|
||||||
|
.game_name = "Jubeat",
|
||||||
|
.category = "Game Options",
|
||||||
|
.quick_setting_category = "Game",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// RBTouchDebug
|
||||||
|
.title = "RB Touch Debug Overlay",
|
||||||
|
.name = "rbtouchdebug",
|
||||||
|
.desc = "Draw lines to show IR sensor emulation state.\n\n"
|
||||||
|
"Note: lines will not perfectly align with touches; this is by design, it's showing the "
|
||||||
|
"IR sensor state, before the game applies its own touch detection algorithm.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.game_name = "Reflec Beat",
|
||||||
|
.category = "Game Options",
|
||||||
|
.quick_setting_category = "Game",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// spice2x_RBTouchScale
|
// spice2x_RBTouchScale
|
||||||
.title = "RB Touch Emulation Scale",
|
.title = "RB Touch Emulation Scale",
|
||||||
@@ -2693,14 +2774,15 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// RBTouchSize
|
// RBTouchSize
|
||||||
.title = "RB Touch Emulation Size",
|
.title = "RB Touch Emulation Size (DEPRECATED - no longer has any effect)",
|
||||||
.name = "rbtouchsize",
|
.name = "rbtouchsize",
|
||||||
.desc = "Size of the touch area; how many IR sensors a single finger activates. Default: 1 (1x1).",
|
.desc = "This option is deprecated and no longer has any effect. "
|
||||||
|
"Reflec Beat touch emulation always uses the 3x3 sensor model.",
|
||||||
.type = OptionType::Enum,
|
.type = OptionType::Enum,
|
||||||
|
.hidden = true,
|
||||||
.game_name = "Reflec Beat",
|
.game_name = "Reflec Beat",
|
||||||
.category = "Game Options",
|
.category = "Game Options",
|
||||||
.elements = {
|
.elements = {
|
||||||
{"1", "1x1"},
|
|
||||||
{"3", "3x3"},
|
{"3", "3x3"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -2708,9 +2790,10 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
// RBTouchPollRate
|
// RBTouchPollRate
|
||||||
.title = "RB Touch Emulation Poll Hz",
|
.title = "RB Touch Emulation Poll Hz",
|
||||||
.name = "rbtouchhz",
|
.name = "rbtouchhz",
|
||||||
.desc = "By default, the game polls for touch at 120Hz. "
|
.desc = "By default, the game polls for touch at ~125Hz. "
|
||||||
"This option overrides that rate; enter a number betwen 1 and 1000.\n\n"
|
"This option overrides that rate; enter a number between 1 and 1000.\n\n"
|
||||||
"It should be noted that higher poll does not necessarily improve accuracy or performance.",
|
"Higher rates reduce input latency (the game sees a fresher touch position) "
|
||||||
|
"but do NOT improve spatial accuracy, and very high rates just waste CPU.",
|
||||||
.type = OptionType::Integer,
|
.type = OptionType::Integer,
|
||||||
.setting_name = "250",
|
.setting_name = "250",
|
||||||
.game_name = "Reflec Beat",
|
.game_name = "Reflec Beat",
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ namespace launcher {
|
|||||||
SOFTID,
|
SOFTID,
|
||||||
VREnable,
|
VREnable,
|
||||||
DisableOverlay,
|
DisableOverlay,
|
||||||
|
OverlayKeyboardNavigation,
|
||||||
OverlayScaling,
|
OverlayScaling,
|
||||||
NotificationPosition,
|
NotificationPosition,
|
||||||
spice2x_FpsAutoShow,
|
spice2x_FpsAutoShow,
|
||||||
@@ -179,6 +180,7 @@ namespace launcher {
|
|||||||
ForceWinTouch,
|
ForceWinTouch,
|
||||||
ForceTouchEmulation,
|
ForceTouchEmulation,
|
||||||
InvertTouchCoordinates,
|
InvertTouchCoordinates,
|
||||||
|
RawInputTouchAspectRatio,
|
||||||
DisableTouchCardInsert,
|
DisableTouchCardInsert,
|
||||||
spice2x_TouchCardInsert,
|
spice2x_TouchCardInsert,
|
||||||
ICCAReaderPort,
|
ICCAReaderPort,
|
||||||
@@ -262,6 +264,9 @@ namespace launcher {
|
|||||||
IIDXWindowedSubscreenAlwaysOnTop,
|
IIDXWindowedSubscreenAlwaysOnTop,
|
||||||
spice2x_JubeatLegacyTouch,
|
spice2x_JubeatLegacyTouch,
|
||||||
JubeatTouchAlgo,
|
JubeatTouchAlgo,
|
||||||
|
JubeatTouchDebug,
|
||||||
|
JubeatTouchDebounce,
|
||||||
|
RBTouchDebug,
|
||||||
spice2x_RBTouchScale,
|
spice2x_RBTouchScale,
|
||||||
RBTouchSize,
|
RBTouchSize,
|
||||||
RBTouchPollRate,
|
RBTouchPollRate,
|
||||||
|
|||||||
+34
-32
@@ -366,6 +366,7 @@ static void __cdecl device_update() {
|
|||||||
|
|
||||||
// update touch
|
// update touch
|
||||||
games::jb::touch_update();
|
games::jb::touch_update();
|
||||||
|
auto touched = games::jb::touch_state();
|
||||||
|
|
||||||
// get buttons
|
// get buttons
|
||||||
auto &buttons = games::jb::get_buttons();
|
auto &buttons = games::jb::get_buttons();
|
||||||
@@ -382,52 +383,52 @@ static void __cdecl device_update() {
|
|||||||
if (Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::CoinMech))) {
|
if (Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::CoinMech))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 29;
|
DEVICE_INPUT_STATE |= 1 << 29;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
|
if (touched[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 13;
|
DEVICE_INPUT_STATE |= 1 << 13;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
|
if (touched[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 9;
|
DEVICE_INPUT_STATE |= 1 << 9;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
|
if (touched[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 21;
|
DEVICE_INPUT_STATE |= 1 << 21;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
|
if (touched[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 17;
|
DEVICE_INPUT_STATE |= 1 << 17;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
|
if (touched[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 14;
|
DEVICE_INPUT_STATE |= 1 << 14;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
|
if (touched[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 10;
|
DEVICE_INPUT_STATE |= 1 << 10;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
|
if (touched[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 22;
|
DEVICE_INPUT_STATE |= 1 << 22;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
|
if (touched[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 18;
|
DEVICE_INPUT_STATE |= 1 << 18;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
|
if (touched[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 15;
|
DEVICE_INPUT_STATE |= 1 << 15;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
|
if (touched[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 11;
|
DEVICE_INPUT_STATE |= 1 << 11;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
|
if (touched[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 23;
|
DEVICE_INPUT_STATE |= 1 << 23;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
|
if (touched[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 19;
|
DEVICE_INPUT_STATE |= 1 << 19;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
|
if (touched[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 24;
|
DEVICE_INPUT_STATE |= 1 << 24;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
|
if (touched[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 12;
|
DEVICE_INPUT_STATE |= 1 << 12;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
|
if (touched[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 26;
|
DEVICE_INPUT_STATE |= 1 << 26;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
|
if (touched[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 20;
|
DEVICE_INPUT_STATE |= 1 << 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,6 +440,7 @@ static void __cdecl device_update() {
|
|||||||
|
|
||||||
// update touch
|
// update touch
|
||||||
games::jb::touch_update();
|
games::jb::touch_update();
|
||||||
|
auto touched = games::jb::touch_state();
|
||||||
|
|
||||||
// get buttons
|
// get buttons
|
||||||
auto &buttons = games::jb::get_buttons();
|
auto &buttons = games::jb::get_buttons();
|
||||||
@@ -455,52 +457,52 @@ static void __cdecl device_update() {
|
|||||||
if (Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::CoinMech))) {
|
if (Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::CoinMech))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 24;
|
DEVICE_INPUT_STATE |= 1 << 24;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
|
if (touched[0] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button1))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 5;
|
DEVICE_INPUT_STATE |= 1 << 5;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
|
if (touched[1] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button2))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 1;
|
DEVICE_INPUT_STATE |= 1 << 1;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
|
if (touched[2] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button3))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 13;
|
DEVICE_INPUT_STATE |= 1 << 13;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
|
if (touched[3] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button4))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 9;
|
DEVICE_INPUT_STATE |= 1 << 9;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
|
if (touched[4] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button5))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 6;
|
DEVICE_INPUT_STATE |= 1 << 6;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
|
if (touched[5] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button6))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 2;
|
DEVICE_INPUT_STATE |= 1 << 2;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
|
if (touched[6] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button7))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 14;
|
DEVICE_INPUT_STATE |= 1 << 14;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
|
if (touched[7] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button8))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 10;
|
DEVICE_INPUT_STATE |= 1 << 10;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
|
if (touched[8] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button9))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 7;
|
DEVICE_INPUT_STATE |= 1 << 7;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
|
if (touched[9] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button10))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 3;
|
DEVICE_INPUT_STATE |= 1 << 3;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
|
if (touched[10] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button11))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 15;
|
DEVICE_INPUT_STATE |= 1 << 15;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
|
if (touched[11] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button12))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 11;
|
DEVICE_INPUT_STATE |= 1 << 11;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
|
if (touched[12] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button13))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 16;
|
DEVICE_INPUT_STATE |= 1 << 16;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
|
if (touched[13] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button14))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 4;
|
DEVICE_INPUT_STATE |= 1 << 4;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
|
if (touched[14] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button15))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 20;
|
DEVICE_INPUT_STATE |= 1 << 20;
|
||||||
}
|
}
|
||||||
if (games::jb::TOUCH_STATE[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
|
if (touched[15] || Buttons::getState(RI_MGR, buttons.at(games::jb::Buttons::Button16))) {
|
||||||
DEVICE_INPUT_STATE |= 1 << 12;
|
DEVICE_INPUT_STATE |= 1 << 12;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ static cardunit_card_cardnumber_t cardunit_card_cardnumber = nullptr;
|
|||||||
static HINSTANCE SCIUNIT_INSTANCE;
|
static HINSTANCE SCIUNIT_INSTANCE;
|
||||||
static std::string SCIUNIT_INSTANCE_NAME = "sciunit.dll";
|
static std::string SCIUNIT_INSTANCE_NAME = "sciunit.dll";
|
||||||
static bool SCIUNIT_INITIALIZED = false;
|
static bool SCIUNIT_INITIALIZED = false;
|
||||||
|
static int SCIUNIT_STATUS = 0;
|
||||||
static bool CARD_IN = false;
|
static bool CARD_IN = false;
|
||||||
static bool CARD_PRESSED = false;
|
static bool CARD_PRESSED = false;
|
||||||
static uint8_t CARD_UID[8];
|
static uint8_t CARD_UID[8];
|
||||||
@@ -150,11 +151,11 @@ static int __cdecl sciunit_finalize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int __cdecl sciunit_get_errorunit() {
|
static int __cdecl sciunit_get_errorunit() {
|
||||||
return 0;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int __cdecl sciunit_get_stat() {
|
static int __cdecl sciunit_get_stat() {
|
||||||
return 0;
|
return SCIUNIT_STATUS;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int __cdecl sciunit_get_version(int a1, int a2) {
|
static int __cdecl sciunit_get_version(int a1, int a2) {
|
||||||
@@ -163,6 +164,7 @@ static int __cdecl sciunit_get_version(int a1, int a2) {
|
|||||||
|
|
||||||
static int __cdecl sciunit_initialize() {
|
static int __cdecl sciunit_initialize() {
|
||||||
SCIUNIT_INITIALIZED = true;
|
SCIUNIT_INITIALIZED = true;
|
||||||
|
SCIUNIT_STATUS = 1;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,8 +225,12 @@ static int __cdecl sciunit_reset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int __cdecl sciunit_update() {
|
static int __cdecl sciunit_update() {
|
||||||
if (SCIUNIT_INITIALIZED)
|
if (SCIUNIT_INITIALIZED) {
|
||||||
|
if (SCIUNIT_STATUS == 1) {
|
||||||
|
SCIUNIT_STATUS = 2;
|
||||||
|
}
|
||||||
update_card();
|
update_card();
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "games/gitadora/gitadora.h"
|
#include "games/gitadora/gitadora.h"
|
||||||
#include "games/iidx/iidx.h"
|
#include "games/iidx/iidx.h"
|
||||||
#include "games/popn/popn.h"
|
#include "games/popn/popn.h"
|
||||||
|
#include "games/rb/touch_debug.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
@@ -66,6 +67,7 @@ namespace overlay {
|
|||||||
bool AUTO_SHOW_IOPANEL = false;
|
bool AUTO_SHOW_IOPANEL = false;
|
||||||
bool AUTO_SHOW_KEYPAD_P1 = false;
|
bool AUTO_SHOW_KEYPAD_P1 = false;
|
||||||
bool AUTO_SHOW_KEYPAD_P2 = false;
|
bool AUTO_SHOW_KEYPAD_P2 = false;
|
||||||
|
bool ENABLE_KEYBOARD_NAVIGATION = false;
|
||||||
bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = false;
|
bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = false;
|
||||||
FpsLocation FPS_LOCATION = FpsLocation::TopRight;
|
FpsLocation FPS_LOCATION = FpsLocation::TopRight;
|
||||||
std::optional<uint32_t> UI_SCALE_PERCENT;
|
std::optional<uint32_t> UI_SCALE_PERCENT;
|
||||||
@@ -311,6 +313,9 @@ void overlay::SpiceOverlay::init() {
|
|||||||
auto &io = ImGui::GetIO();
|
auto &io = ImGui::GetIO();
|
||||||
io.UserData = this;
|
io.UserData = this;
|
||||||
io.ConfigFlags = ImGuiConfigFlags_None;
|
io.ConfigFlags = ImGuiConfigFlags_None;
|
||||||
|
if (cfg::CONFIGURATOR_STANDALONE || ENABLE_KEYBOARD_NAVIGATION) {
|
||||||
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||||
|
}
|
||||||
if (!cfg::CONFIGURATOR_STANDALONE) {
|
if (!cfg::CONFIGURATOR_STANDALONE) {
|
||||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||||
}
|
}
|
||||||
@@ -551,9 +556,13 @@ void overlay::SpiceOverlay::new_frame() {
|
|||||||
const bool draw_fps_persistent = this->renderer != OverlayRenderer::SOFTWARE
|
const bool draw_fps_persistent = this->renderer != OverlayRenderer::SOFTWARE
|
||||||
&& this->window_fps->get_active();
|
&& this->window_fps->get_active();
|
||||||
|
|
||||||
|
// draw RB touch diagnostics
|
||||||
|
const bool draw_rb_touch_debug = this->renderer != OverlayRenderer::SOFTWARE
|
||||||
|
&& games::rb::touch_debug_overlay_enabled();
|
||||||
|
|
||||||
// check if there is nothing to draw
|
// check if there is nothing to draw
|
||||||
this->has_pending_frame = false;
|
this->has_pending_frame = false;
|
||||||
if (!this->active && !draw_notifications && !draw_fps_persistent) {
|
if (!this->active && !draw_notifications && !draw_fps_persistent && !draw_rb_touch_debug) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,6 +587,10 @@ void overlay::SpiceOverlay::new_frame() {
|
|||||||
ImGui::NewFrame();
|
ImGui::NewFrame();
|
||||||
this->has_pending_frame = true;
|
this->has_pending_frame = true;
|
||||||
|
|
||||||
|
if (draw_rb_touch_debug) {
|
||||||
|
games::rb::touch_draw_debug_overlay();
|
||||||
|
}
|
||||||
|
|
||||||
// build windows only when the overlay itself is active
|
// build windows only when the overlay itself is active
|
||||||
if (this->active) {
|
if (this->active) {
|
||||||
for (auto &window : this->windows) {
|
for (auto &window : this->windows) {
|
||||||
@@ -595,6 +608,7 @@ void overlay::SpiceOverlay::new_frame() {
|
|||||||
if (draw_fps_persistent) {
|
if (draw_fps_persistent) {
|
||||||
this->window_fps->build();
|
this->window_fps->build();
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw notifications last so they paint on top of any overlay windows
|
// draw notifications last so they paint on top of any overlay windows
|
||||||
if (draw_notifications) {
|
if (draw_notifications) {
|
||||||
overlay::notifications::draw();
|
overlay::notifications::draw();
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ namespace overlay {
|
|||||||
extern bool AUTO_SHOW_IOPANEL;
|
extern bool AUTO_SHOW_IOPANEL;
|
||||||
extern bool AUTO_SHOW_KEYPAD_P1;
|
extern bool AUTO_SHOW_KEYPAD_P1;
|
||||||
extern bool AUTO_SHOW_KEYPAD_P2;
|
extern bool AUTO_SHOW_KEYPAD_P2;
|
||||||
|
extern bool ENABLE_KEYBOARD_NAVIGATION;
|
||||||
extern bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT;
|
extern bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT;
|
||||||
extern FpsLocation FPS_LOCATION;
|
extern FpsLocation FPS_LOCATION;
|
||||||
extern bool SHOW_DEBUG_LOG_WINDOW;
|
extern bool SHOW_DEBUG_LOG_WINDOW;
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ namespace rawinput {
|
|||||||
switch (hdr->dbch_devicetype) {
|
switch (hdr->dbch_devicetype) {
|
||||||
case DBT_DEVTYP_DEVICEINTERFACE: {
|
case DBT_DEVTYP_DEVICEINTERFACE: {
|
||||||
auto dev = (const DEV_BROADCAST_DEVICEINTERFACE *) hdr;
|
auto dev = (const DEV_BROADCAST_DEVICEINTERFACE *) hdr;
|
||||||
this->ri_mgr->devices_scan_midi();
|
this->ri_mgr->midi_scan_start();
|
||||||
|
|
||||||
// check if class is not HID
|
// check if class is not HID
|
||||||
if (memcmp(&dev->dbcc_classguid, &GUID_HID, sizeof(GUID_HID)) != 0)
|
if (memcmp(&dev->dbcc_classguid, &GUID_HID, sizeof(GUID_HID)) != 0)
|
||||||
@@ -112,6 +112,7 @@ namespace rawinput {
|
|||||||
std::string name(dev->dbcc_name);
|
std::string name(dev->dbcc_name);
|
||||||
|
|
||||||
// destruct device
|
// destruct device
|
||||||
|
log_misc("hotplug", "device interface removal: {}", name);
|
||||||
this->ri_mgr->devices_remove(name);
|
this->ri_mgr->devices_remove(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,8 +121,11 @@ namespace rawinput {
|
|||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
case DBT_DEVNODES_CHANGED: {
|
case DBT_DEVNODES_CHANGED: {
|
||||||
// TODO: this can be a little noisy as it gets called on every device
|
// catch-all device-tree change: MIDI and XInput have no targeted
|
||||||
this->ri_mgr->devices_scan_midi();
|
// arrival/removal notification, so this is our only hook to detect them
|
||||||
|
// can be a little noisy as it gets called on every device
|
||||||
|
log_misc("hotplug", "device tree changed, rescanning MIDI + XInput");
|
||||||
|
this->ri_mgr->midi_scan_start();
|
||||||
this->ri_mgr->devices_scan_xinput();
|
this->ri_mgr->devices_scan_xinput();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,727 @@
|
|||||||
|
#include "rawinput.h"
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#include "util/logging.h"
|
||||||
|
#include "util/time.h"
|
||||||
|
#include "util/utils.h"
|
||||||
|
|
||||||
|
namespace rawinput {
|
||||||
|
|
||||||
|
static MidiNoteAlgorithm MIDI_NOTE_ALGORITHM = MidiNoteAlgorithm::V2;
|
||||||
|
}
|
||||||
|
|
||||||
|
rawinput::MidiNoteAlgorithm rawinput::get_midi_algorithm() {
|
||||||
|
return rawinput::MIDI_NOTE_ALGORITHM;
|
||||||
|
}
|
||||||
|
|
||||||
|
void rawinput::set_midi_algorithm(rawinput::MidiNoteAlgorithm new_algo) {
|
||||||
|
rawinput::MIDI_NOTE_ALGORITHM = new_algo;
|
||||||
|
std::string s = "Unknown";
|
||||||
|
switch (new_algo) {
|
||||||
|
case rawinput::MidiNoteAlgorithm::LEGACY:
|
||||||
|
s = "legacy";
|
||||||
|
break;
|
||||||
|
case rawinput::MidiNoteAlgorithm::V2:
|
||||||
|
s = "v2";
|
||||||
|
break;
|
||||||
|
case rawinput::MidiNoteAlgorithm::V2_DRUM:
|
||||||
|
s = "v2_drum";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log_info("rawinput", "assert failed: invalid midi algorithm");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
log_info("rawinput", "using MIDI algorithm: {}", s);
|
||||||
|
}
|
||||||
|
|
||||||
|
void rawinput::RawInputManager::midi_scan_start() {
|
||||||
|
|
||||||
|
// single-flight: only one scan runs at a time. if one is already running, set
|
||||||
|
// the pending flag so it rescans once more when it finishes - MIDI hotplug
|
||||||
|
// events fire while the slow enumeration is still going and must not be lost.
|
||||||
|
// the scheduler mutex makes this check-and-set atomic with the worker's
|
||||||
|
// exit-or-rescan decision below, so a request set while a scan is running is
|
||||||
|
// never dropped
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(this->midi_scan_m);
|
||||||
|
if (this->midi_scan_active) {
|
||||||
|
this->midi_scan_pending = true;
|
||||||
|
log_misc("rawinput", "MIDI scan already running, queued rescan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->midi_scan_active = true;
|
||||||
|
this->midi_scan_pending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean up the previous (already finished) scan thread handle
|
||||||
|
this->midi_scan_join();
|
||||||
|
|
||||||
|
// run the (potentially slow) MIDI enumeration on its own thread so callers are
|
||||||
|
// not blocked while the Windows MIDI subsystem starts up. rescan if a request
|
||||||
|
// arrived while we were scanning
|
||||||
|
log_misc("rawinput", "starting async MIDI scan thread");
|
||||||
|
this->midi_thread = new std::thread([this]() {
|
||||||
|
for (;;) {
|
||||||
|
this->devices_scan_midi();
|
||||||
|
|
||||||
|
// decide whether to exit under the scheduler lock, atomically with any
|
||||||
|
// concurrent midi_scan_start(): if a rescan was requested, consume it
|
||||||
|
// and loop; otherwise clear active and exit. because both sides take
|
||||||
|
// the same lock, a request set while active is true is never lost, so
|
||||||
|
// we never strand a hotplug event waiting for a future one
|
||||||
|
std::lock_guard<std::mutex> lock(this->midi_scan_m);
|
||||||
|
if (!this->midi_scan_pending) {
|
||||||
|
this->midi_scan_active = false;
|
||||||
|
log_misc("rawinput", "async MIDI scan thread finished");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->midi_scan_pending = false;
|
||||||
|
log_misc("rawinput", "async MIDI scan rescanning (event arrived during scan)");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void rawinput::RawInputManager::midi_scan_join() {
|
||||||
|
if (this->midi_thread) {
|
||||||
|
if (this->midi_thread->joinable()) {
|
||||||
|
|
||||||
|
// this blocks until the scan worker returns. if it ever hangs here the
|
||||||
|
// worker is stuck - most likely in midi_close_deferred_flush() waiting
|
||||||
|
// on a WinMM close. a missing "joined" line pinpoints the hang
|
||||||
|
log_misc("rawinput", "joining MIDI scan thread...");
|
||||||
|
this->midi_thread->join();
|
||||||
|
log_misc("rawinput", "MIDI scan thread joined");
|
||||||
|
}
|
||||||
|
delete this->midi_thread;
|
||||||
|
this->midi_thread = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void rawinput::RawInputManager::midi_close_deferred_flush() {
|
||||||
|
|
||||||
|
// take the queued handles under the lock, then close them without it. WinMM
|
||||||
|
// midiInReset/midiInClose block until in-flight input_midi_proc callbacks
|
||||||
|
// return, and those callbacks take devices_mutex, so closing under the lock
|
||||||
|
// would deadlock
|
||||||
|
std::vector<HMIDIIN> handles;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||||
|
handles.swap(this->midi_close_deferred);
|
||||||
|
}
|
||||||
|
if (handles.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if a hang is ever reported here it is the classic WinMM deadlock: an
|
||||||
|
// in-flight input_midi_proc callback is blocked on devices_mutex while
|
||||||
|
// midiInReset/midiInClose waits for that callback to return. the per-handle
|
||||||
|
// log below pinpoints exactly which close did not come back
|
||||||
|
log_misc("rawinput", "closing {} deferred MIDI handle(s)", handles.size());
|
||||||
|
for (size_t i = 0; i < handles.size(); i++) {
|
||||||
|
log_misc("rawinput", "closing deferred MIDI handle {}/{}", i + 1, handles.size());
|
||||||
|
midiInReset(handles[i]);
|
||||||
|
midiInClose(handles[i]);
|
||||||
|
}
|
||||||
|
log_misc("rawinput", "deferred MIDI handles closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
void rawinput::RawInputManager::devices_scan_midi() {
|
||||||
|
log_misc("rawinput", "scan MIDI devices...");
|
||||||
|
|
||||||
|
// note: the WinMM MIDI calls below (midiInGetNumDevs / midiInGetDevCaps /
|
||||||
|
// midiInOpen / midiInStart) can block for seconds while the Windows MIDI
|
||||||
|
// subsystem starts up, so they must NOT run under devices_mutex. only the
|
||||||
|
// list mutation at the end of each iteration is guarded.
|
||||||
|
|
||||||
|
// identifiers of every MIDI device seen in this scan; used below to
|
||||||
|
// tombstone devices that have since been unplugged
|
||||||
|
std::vector<std::string> present_identifiers;
|
||||||
|
|
||||||
|
// add midi devices
|
||||||
|
auto midi_device_count = midiInGetNumDevs();
|
||||||
|
for (size_t midi_device_id = 0; midi_device_id < midi_device_count; midi_device_id++) {
|
||||||
|
|
||||||
|
// get dev caps
|
||||||
|
MIDIINCAPS midi_device_caps{};
|
||||||
|
if (midiInGetDevCaps(midi_device_id, &midi_device_caps, sizeof(MIDIINCAPS)) != MMSYSERR_NOERROR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_misc("rawinput", "found MIDI device: id {}, name {}, mid {}, pid {}",
|
||||||
|
midi_device_id, midi_device_caps.szPname, midi_device_caps.wMid, midi_device_caps.wPid);
|
||||||
|
|
||||||
|
// build identifier for MIDI
|
||||||
|
// ;MIDI; format is now set in stone (in other parts of the code base and in the config xml file)
|
||||||
|
// so it should never be changed
|
||||||
|
std::ostringstream midi_identifier_stream;
|
||||||
|
midi_identifier_stream << ";" << "MIDI";
|
||||||
|
midi_identifier_stream << ";" << midi_device_id;
|
||||||
|
midi_identifier_stream << ";" << midi_device_caps.szPname;
|
||||||
|
midi_identifier_stream << ";" << midi_device_caps.wMid;
|
||||||
|
midi_identifier_stream << ";" << midi_device_caps.wPid;
|
||||||
|
const auto midi_identifier = midi_identifier_stream.str();
|
||||||
|
|
||||||
|
// record that this device is currently present
|
||||||
|
present_identifiers.push_back(midi_identifier);
|
||||||
|
|
||||||
|
// if already open, leave it alone: hotplug fires many change events, and
|
||||||
|
// reopening on every rescan would drop the WinMM handle (and its input).
|
||||||
|
// only (re)open when the device is missing or a destroyed tombstone
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||||
|
bool already_open = false;
|
||||||
|
for (auto &device : this->devices) {
|
||||||
|
if (device.type == MIDI && device.name == midi_identifier) {
|
||||||
|
already_open = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (already_open) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// open device
|
||||||
|
HMIDIIN midi_device_handle;
|
||||||
|
if (midiInOpen(&midi_device_handle,
|
||||||
|
(UINT) midi_device_id,
|
||||||
|
(DWORD_PTR) &input_midi_proc,
|
||||||
|
(DWORD_PTR) this,
|
||||||
|
CALLBACK_FUNCTION) != MMSYSERR_NOERROR)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// start input
|
||||||
|
if (midiInStart(midi_device_handle) != MMSYSERR_NOERROR) {
|
||||||
|
|
||||||
|
// close the handle we just opened so it does not leak on repeated rescans
|
||||||
|
midiInClose(midi_device_handle);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// device info
|
||||||
|
DeviceInfo midi_device_info {};
|
||||||
|
|
||||||
|
// device midi info
|
||||||
|
auto midi_device_midi_info = new DeviceMIDIInfo();
|
||||||
|
midi_device_midi_info->states = std::vector<bool>(16 * 128);
|
||||||
|
midi_device_midi_info->states_events = std::vector<uint8_t>(16 * 128);
|
||||||
|
midi_device_midi_info->bind_states = std::vector<bool>(16 * 128);
|
||||||
|
midi_device_midi_info->v2_last_on_time = std::vector<double>(16 * 128);
|
||||||
|
midi_device_midi_info->v2_last_off_time = std::vector<double>(16 * 128);
|
||||||
|
midi_device_midi_info->v2_velocity_threshold = std::vector<uint8_t>(16 * 128);
|
||||||
|
midi_device_midi_info->v2_velocity_threshold_set_on_device = std::vector<bool>(16 * 128);
|
||||||
|
midi_device_midi_info->velocity = std::vector<uint8_t>(16 * 128);
|
||||||
|
midi_device_midi_info->freeze = false;
|
||||||
|
midi_device_midi_info->controls_precision = std::vector<uint16_t>(16 * 32);
|
||||||
|
midi_device_midi_info->controls_precision_bind = std::vector<uint16_t>(16 * 32);
|
||||||
|
midi_device_midi_info->controls_precision_msb = std::vector<bool>(16 * 32);
|
||||||
|
midi_device_midi_info->controls_precision_lsb = std::vector<bool>(16 * 32);
|
||||||
|
midi_device_midi_info->controls_precision_set = std::vector<bool>(16 * 32);
|
||||||
|
midi_device_midi_info->controls_single = std::vector<uint8_t>(16 * 44);
|
||||||
|
midi_device_midi_info->controls_single_bind = std::vector<uint8_t>(16 * 44);
|
||||||
|
midi_device_midi_info->controls_single_set = std::vector<bool>(16 * 44);
|
||||||
|
midi_device_midi_info->controls_onoff = std::vector<bool>(16 * 6);
|
||||||
|
midi_device_midi_info->controls_onoff_bind = std::vector<bool>(16 * 6);
|
||||||
|
midi_device_midi_info->controls_onoff_set = std::vector<bool>(16 * 6);
|
||||||
|
midi_device_midi_info->v2_controls_onoff_last_on_time = std::vector<double>(16 * 6);
|
||||||
|
midi_device_midi_info->v2_controls_onoff_last_off_time = std::vector<double>(16 * 6);
|
||||||
|
midi_device_midi_info->pitch_bend = std::vector<int16_t>(16 * 6);
|
||||||
|
midi_device_midi_info->pitch_bend_set = std::vector<bool>(16 * 6);
|
||||||
|
|
||||||
|
// build device
|
||||||
|
Device midi_device {};
|
||||||
|
midi_device.type = MIDI;
|
||||||
|
midi_device.handle = midi_device_handle;
|
||||||
|
midi_device.name = midi_identifier;
|
||||||
|
midi_device.desc = to_string(midi_device_caps.szPname);
|
||||||
|
midi_device.info = midi_device_info;
|
||||||
|
midi_device.mutex = new std::mutex();
|
||||||
|
midi_device.mutex_out = new std::mutex();
|
||||||
|
midi_device.midiInfo = midi_device_midi_info;
|
||||||
|
|
||||||
|
// mutate the shared device list under lock (the slow WinMM calls above
|
||||||
|
// ran without it so other threads were not blocked)
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||||
|
midi_device.id = devices.size() + 1;
|
||||||
|
|
||||||
|
// reuse a previously destroyed tombstone with the same identifier, if any.
|
||||||
|
// (a live device with this identifier was already skipped above)
|
||||||
|
bool replaced = false;
|
||||||
|
for (auto &device : this->devices) {
|
||||||
|
if (device.name == midi_identifier) {
|
||||||
|
|
||||||
|
// carry over ID
|
||||||
|
midi_device.id = device.id;
|
||||||
|
|
||||||
|
// destruct and replace, reusing the slot's existing mutexes
|
||||||
|
this->devices_destruct(&device);
|
||||||
|
reuse_device_mutexes(midi_device, device);
|
||||||
|
device = midi_device;
|
||||||
|
|
||||||
|
// notify change
|
||||||
|
for (auto &cb : this->callback_change) {
|
||||||
|
cb.f(cb.data, &device);
|
||||||
|
}
|
||||||
|
|
||||||
|
replaced = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (replaced) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add device to list
|
||||||
|
auto &device = this->devices.emplace_back(midi_device);
|
||||||
|
|
||||||
|
// notify add
|
||||||
|
for (auto &cb : this->callback_add) {
|
||||||
|
cb.f(cb.data, &device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tombstone MIDI devices that were open but are no longer present (unplugged).
|
||||||
|
// otherwise a replugged device matches the stale live entry in the skip check
|
||||||
|
// above and never gets reopened, silently losing its input
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||||
|
for (auto &device : this->devices) {
|
||||||
|
if (device.type != MIDI) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bool present = false;
|
||||||
|
for (const auto &identifier : present_identifiers) {
|
||||||
|
if (identifier == device.name) {
|
||||||
|
present = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!present) {
|
||||||
|
log_info("rawinput", "MIDI device unplugged, releasing: {}", device.desc);
|
||||||
|
this->devices_destruct(&device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// close the MIDI handles detached above, now that devices_mutex is released
|
||||||
|
this->midi_close_deferred_flush();
|
||||||
|
|
||||||
|
log_misc("rawinput", "scan MIDI devices done ({} enumerated)", (unsigned) midi_device_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CALLBACK rawinput::RawInputManager::input_midi_proc(HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance,
|
||||||
|
DWORD_PTR dwParam1, DWORD_PTR dwParam2) {
|
||||||
|
// get instance
|
||||||
|
auto ri_mgr = reinterpret_cast<RawInputManager *>(dwInstance);
|
||||||
|
|
||||||
|
// handle message
|
||||||
|
switch (wMsg) {
|
||||||
|
case MIM_OPEN:
|
||||||
|
case MIM_CLOSE:
|
||||||
|
break;
|
||||||
|
case MIM_MOREDATA:
|
||||||
|
case MIM_DATA: {
|
||||||
|
|
||||||
|
// lock the device list so a concurrent scan can't mutate it while we iterate
|
||||||
|
std::lock_guard<std::recursive_mutex> devices_lock(ri_mgr->devices_mutex);
|
||||||
|
|
||||||
|
// param mapping
|
||||||
|
auto dwMidiMessage = dwParam1;
|
||||||
|
//auto dwTimestamp = dwParam2;
|
||||||
|
|
||||||
|
// message unpacking
|
||||||
|
auto midi_status = LOBYTE(LOWORD(dwMidiMessage));
|
||||||
|
auto midi_status_command = (midi_status & 0xF0u) >> 4u;
|
||||||
|
auto midi_status_channel = (midi_status & 0x0Fu);
|
||||||
|
auto midi_byte1 = HIBYTE(LOWORD(dwMidiMessage));
|
||||||
|
auto midi_byte2 = LOBYTE(HIWORD(dwMidiMessage));
|
||||||
|
|
||||||
|
// callbacks
|
||||||
|
for (auto &callback : ri_mgr->callback_midi) {
|
||||||
|
|
||||||
|
// find device
|
||||||
|
for (auto &device : ri_mgr->devices_get()) {
|
||||||
|
if (device.type == MIDI && device.handle == hMidiIn) {
|
||||||
|
|
||||||
|
// call function
|
||||||
|
callback.f(callback.data, &device,
|
||||||
|
midi_status_command, midi_status_channel,
|
||||||
|
midi_byte1, midi_byte2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip unused messages types early for performance
|
||||||
|
bool skip = false;
|
||||||
|
switch (midi_status_command) {
|
||||||
|
case 0xA: // POLYPHONIC PRESSURE
|
||||||
|
case 0xC: // PROGRAM CHANGE
|
||||||
|
case 0xD: // CHANNEL PRESSURE
|
||||||
|
case 0xF: // SYSTEM EXCLUSIVE
|
||||||
|
skip = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (skip) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find device
|
||||||
|
for (auto &device : ri_mgr->devices_get()) {
|
||||||
|
|
||||||
|
// filter non MIDI devices
|
||||||
|
if (device.type != MIDI) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter wrong handles
|
||||||
|
if (device.handle != hMidiIn) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get input time
|
||||||
|
const auto input_time = get_performance_seconds();
|
||||||
|
|
||||||
|
// lock device
|
||||||
|
std::lock_guard<std::mutex> lock(*device.mutex);
|
||||||
|
|
||||||
|
// update hz
|
||||||
|
auto diff_time = input_time - device.input_time;
|
||||||
|
if (diff_time > 0.0001) {
|
||||||
|
device.input_hz = 1.f / diff_time;
|
||||||
|
device.input_hz_max = MAX(device.input_hz_max, device.input_hz);
|
||||||
|
device.input_time = input_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
// command logic
|
||||||
|
switch (midi_status_command) {
|
||||||
|
case 0x8: { // NOTE OFF
|
||||||
|
|
||||||
|
// param mapping
|
||||||
|
const auto midi_note = midi_byte1 & 127u;
|
||||||
|
|
||||||
|
// log_misc("midi", "[{}] OFF", midi_note);
|
||||||
|
|
||||||
|
// get index
|
||||||
|
const auto midi_index = midi_status_channel * 128 + midi_note;
|
||||||
|
if (midi_index < 16 * 128) {
|
||||||
|
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::LEGACY) {
|
||||||
|
// update velocity
|
||||||
|
device.midiInfo->velocity[midi_index] = 0;
|
||||||
|
// disable note
|
||||||
|
if (device.midiInfo->states_events[midi_index]) {
|
||||||
|
device.midiInfo->states[midi_index] = false;
|
||||||
|
}
|
||||||
|
device.updated = true;
|
||||||
|
} else {
|
||||||
|
// v2 logic
|
||||||
|
// exactly the same as NOTE ON with 0 velocity
|
||||||
|
// velocity is kept; api will ignore it if button is not pressed
|
||||||
|
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::V2) {
|
||||||
|
device.midiInfo->v2_last_off_time[midi_index] = get_performance_milliseconds();
|
||||||
|
device.updated = true;
|
||||||
|
}
|
||||||
|
// for v2_drum, NOTE OFF is ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x9: { // NOTE ON
|
||||||
|
|
||||||
|
// param mapping
|
||||||
|
const auto midi_note = midi_byte1 & 127u;
|
||||||
|
|
||||||
|
// per MIDI spec, if NOTE ON is sent with 0 velocity, it's the same thing as NOTE OFF.
|
||||||
|
const auto midi_velocity = midi_byte2 & 127u;
|
||||||
|
|
||||||
|
// log_misc("midi", "[{}] ON v={}", midi_note, midi_velocity);
|
||||||
|
|
||||||
|
// get index
|
||||||
|
const auto midi_index = midi_status_channel * 128 + midi_note;
|
||||||
|
if (midi_index < 16 * 128) {
|
||||||
|
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::LEGACY) {
|
||||||
|
// update velocity
|
||||||
|
device.midiInfo->velocity[midi_index] = (uint8_t) midi_velocity;
|
||||||
|
|
||||||
|
if (midi_velocity) {
|
||||||
|
// update events (for legacy logic)
|
||||||
|
// how does this work? see the comment in api.cpp around the check for
|
||||||
|
// get_midi_algorithm() for an explanation
|
||||||
|
|
||||||
|
// so currently it's meant to be turned on
|
||||||
|
device.midiInfo->states[midi_index] = true;
|
||||||
|
|
||||||
|
// if its already on just increase it by one to turn it off
|
||||||
|
if (device.midiInfo->states_events[midi_index] % 2)
|
||||||
|
device.midiInfo->states_events[midi_index]++;
|
||||||
|
else
|
||||||
|
device.midiInfo->states_events[midi_index] += 2;
|
||||||
|
|
||||||
|
} else if (!device.midiInfo->freeze) {
|
||||||
|
// velocity 0 means turn it off
|
||||||
|
device.midiInfo->states[midi_index] = false;
|
||||||
|
}
|
||||||
|
device.updated = true;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// v2 logic
|
||||||
|
const auto now = get_performance_milliseconds();
|
||||||
|
auto threshold = device.midiInfo->v2_velocity_threshold[midi_index];
|
||||||
|
// when device is frozen (binding is happening) ignore the velocity threshold
|
||||||
|
// this allows users to bind keys even if the midi note is set to high threshold at
|
||||||
|
// rawinput layer, either from a previous binding that was cleared, or existing binding
|
||||||
|
// for another button
|
||||||
|
if (device.midiInfo->freeze) {
|
||||||
|
threshold = 0;
|
||||||
|
}
|
||||||
|
if (threshold < midi_velocity) {
|
||||||
|
device.midiInfo->velocity[midi_index] = (uint8_t)midi_velocity;
|
||||||
|
device.midiInfo->v2_last_on_time[midi_index] = now;
|
||||||
|
|
||||||
|
// disable holds and release all notes immediately
|
||||||
|
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::V2_DRUM) {
|
||||||
|
device.midiInfo->v2_last_off_time[midi_index] = now;
|
||||||
|
}
|
||||||
|
device.updated = true;
|
||||||
|
} else {
|
||||||
|
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::V2) {
|
||||||
|
// insufficient velocity ON == exactly the same as NOTE OFF
|
||||||
|
device.midiInfo->v2_last_off_time[midi_index] = now;
|
||||||
|
device.updated = true;
|
||||||
|
}
|
||||||
|
// for v2_drum, NOTE ON with insufficient velocity is ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0xA: // POLYPHONIC PRESSURE
|
||||||
|
break; // skipped above (!)
|
||||||
|
case 0xB: { // CONTROL CHANGE
|
||||||
|
|
||||||
|
// param mapping
|
||||||
|
auto midi_control = midi_byte1 & 127;
|
||||||
|
auto midi_value = midi_byte2 & 127u;
|
||||||
|
|
||||||
|
// get index
|
||||||
|
auto channel_offset = midi_status_channel * 128;
|
||||||
|
auto midi_index = channel_offset + midi_control;
|
||||||
|
if (midi_index < 16 * 128) {
|
||||||
|
|
||||||
|
// continuous controller MSB
|
||||||
|
if (midi_control >= 0x00 && midi_control <= 0x1F) {
|
||||||
|
|
||||||
|
// update index
|
||||||
|
midi_index = midi_status_channel * 32 + midi_control;
|
||||||
|
device.midiInfo->controls_precision_set[midi_index] = true;
|
||||||
|
|
||||||
|
// check if MSB wasn't sent yet
|
||||||
|
if (!device.midiInfo->controls_precision_msb[midi_index]) {
|
||||||
|
device.midiInfo->controls_precision_msb[midi_index] = true;
|
||||||
|
|
||||||
|
// move LSB value to actual position
|
||||||
|
device.midiInfo->controls_precision[midi_index] >>= 7u;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update MSB
|
||||||
|
auto tmp = device.midiInfo->controls_precision[midi_index];
|
||||||
|
tmp = (tmp & 127u) | midi_value << 7u;
|
||||||
|
if (!device.midiInfo->controls_precision_lsb[midi_index])
|
||||||
|
tmp = (tmp & (127u << 7u)) | midi_value;
|
||||||
|
if (device.midiInfo->controls_precision[midi_index] != tmp) {
|
||||||
|
device.midiInfo->controls_precision[midi_index] = tmp;
|
||||||
|
device.updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// continuous controller LSB
|
||||||
|
else if (midi_control >= 0x20 && midi_control <= 0x3F) {
|
||||||
|
|
||||||
|
// update index
|
||||||
|
midi_index = midi_status_channel * 32 + midi_control - 0x20;
|
||||||
|
device.midiInfo->controls_precision_set[midi_index] = true;
|
||||||
|
device.midiInfo->controls_precision_lsb[midi_index] = true;
|
||||||
|
|
||||||
|
// check for MSB flag
|
||||||
|
if (device.midiInfo->controls_precision_msb[midi_index]) {
|
||||||
|
|
||||||
|
// update LSB only
|
||||||
|
auto tmp = device.midiInfo->controls_precision[midi_index];
|
||||||
|
tmp &= 127u << 7u;
|
||||||
|
tmp |= midi_value;
|
||||||
|
if (device.midiInfo->controls_precision[midi_index] != tmp) {
|
||||||
|
device.midiInfo->controls_precision[midi_index] = tmp;
|
||||||
|
device.updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// cast to MSB
|
||||||
|
if (device.midiInfo->controls_precision[midi_index] != midi_value << 7u) {
|
||||||
|
device.midiInfo->controls_precision[midi_index] = midi_value << 7u | midi_value;
|
||||||
|
device.updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// on/off controls
|
||||||
|
else if (midi_control >= 0x40 && midi_control <= 0x45) {
|
||||||
|
|
||||||
|
// update index
|
||||||
|
midi_index = midi_status_channel * 6 + midi_control - 0x40;
|
||||||
|
device.midiInfo->controls_onoff_set[midi_index] = true;
|
||||||
|
|
||||||
|
// get on/off state
|
||||||
|
const auto onoff_state = midi_value >= 64;
|
||||||
|
|
||||||
|
// update device
|
||||||
|
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::LEGACY) {
|
||||||
|
if (device.midiInfo->controls_onoff[midi_index] != onoff_state) {
|
||||||
|
device.midiInfo->controls_onoff[midi_index] = onoff_state;
|
||||||
|
device.updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// v2 and v2_drum:
|
||||||
|
// unlike notes (drum pads), controls can send continuous ON signal
|
||||||
|
// therefore, check for rising and falling edges
|
||||||
|
const auto now = get_performance_milliseconds();
|
||||||
|
const auto previous_value = device.midiInfo->controls_onoff[midi_index];
|
||||||
|
if (!previous_value && onoff_state) {
|
||||||
|
device.midiInfo->v2_controls_onoff_last_on_time[midi_index] = now;
|
||||||
|
device.updated = true;
|
||||||
|
} else if (previous_value && !onoff_state) {
|
||||||
|
device.midiInfo->v2_controls_onoff_last_off_time[midi_index] = now;
|
||||||
|
device.updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
device.midiInfo->controls_onoff[midi_index] = onoff_state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// single byte controllers
|
||||||
|
else if (midi_control >= 0x46 && midi_control <= 0x5F) {
|
||||||
|
|
||||||
|
// update index
|
||||||
|
midi_index = midi_status_channel * 44 + midi_control - 0x46;
|
||||||
|
device.midiInfo->controls_single_set[midi_index] = true;
|
||||||
|
|
||||||
|
// update device
|
||||||
|
if (device.midiInfo->controls_single[midi_index] != midi_value) {
|
||||||
|
device.midiInfo->controls_single[midi_index] = midi_value;
|
||||||
|
device.updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// increment/decrement and parameter numbers
|
||||||
|
else if (midi_control >= 0x60 && midi_control <= 0x65) {
|
||||||
|
// skip
|
||||||
|
}
|
||||||
|
|
||||||
|
// undefined single-byte controllers
|
||||||
|
else if (midi_control >= 0x66 && midi_control <= 0x77) {
|
||||||
|
|
||||||
|
// update index
|
||||||
|
auto sbc_count = 0x5F - 0x46 + 1;
|
||||||
|
midi_index = midi_status_channel * 44 + midi_control - 0x66 + sbc_count;
|
||||||
|
device.midiInfo->controls_single_set[midi_index] = true;
|
||||||
|
|
||||||
|
// update device
|
||||||
|
if (device.midiInfo->controls_single[midi_index] != midi_value) {
|
||||||
|
device.midiInfo->controls_single[midi_index] = midi_value;
|
||||||
|
device.updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// channel mode messages
|
||||||
|
else if (midi_control >= 0x78 && midi_control <= 0x7F) {
|
||||||
|
switch (midi_control) {
|
||||||
|
case 0x78: // all sound off
|
||||||
|
break;
|
||||||
|
case 0x79: { // reset all controllers
|
||||||
|
for (int i = 0; i < 32; i++)
|
||||||
|
device.midiInfo->controls_precision[midi_status_channel * 32 + i] = 0;
|
||||||
|
for (int i = 0; i < 44; i++)
|
||||||
|
device.midiInfo->controls_single[midi_status_channel * 44 + i] = 0;
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
const auto index = midi_status_channel * 6 + i;
|
||||||
|
device.midiInfo->controls_onoff[index] = false;
|
||||||
|
device.midiInfo->v2_controls_onoff_last_on_time[index] = 0;
|
||||||
|
device.midiInfo->v2_controls_onoff_last_off_time[index] = 0;
|
||||||
|
}
|
||||||
|
device.updated = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0x7A: // local control on/off
|
||||||
|
break;
|
||||||
|
case 0x7B: // all notes off
|
||||||
|
case 0x7C: // omni mode off + all notes off
|
||||||
|
case 0x7D: // omni mode on + all notes off
|
||||||
|
case 0x7E: // mono mode on + poly off + all notes off
|
||||||
|
case 0x7F: // poly mode on + mono off + all notes off
|
||||||
|
for (int i = 0; i < 128; i++) {
|
||||||
|
// common
|
||||||
|
device.midiInfo->velocity[channel_offset + i] = 0;
|
||||||
|
device.midiInfo->bind_states[channel_offset + i] = false;
|
||||||
|
|
||||||
|
// legacy
|
||||||
|
device.midiInfo->states[channel_offset + i] = false;
|
||||||
|
device.midiInfo->states_events[channel_offset + i] = 0;
|
||||||
|
|
||||||
|
// v2
|
||||||
|
device.midiInfo->v2_last_off_time[channel_offset + i] = 0.0;
|
||||||
|
device.midiInfo->v2_last_on_time[channel_offset + i] = 0.0;
|
||||||
|
}
|
||||||
|
device.updated = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0xC: // PROGRAM CHANGE
|
||||||
|
break; // skipped above (!)
|
||||||
|
case 0xD: // CHANNEL PRESSURE
|
||||||
|
break; // skipped above (!)
|
||||||
|
case 0xE: { // PITCH BENDING
|
||||||
|
|
||||||
|
// raw values range from [0, 0x3FFF] (16383)
|
||||||
|
// build value, centered around zero [-8192, 8191]
|
||||||
|
int16_t value = ((midi_byte1) | (midi_byte2 << 7u)) - 0x2000;
|
||||||
|
|
||||||
|
// update device
|
||||||
|
if (device.midiInfo->pitch_bend[midi_status_channel] != value) {
|
||||||
|
device.midiInfo->pitch_bend[midi_status_channel] = value;
|
||||||
|
device.midiInfo->pitch_bend_set[midi_status_channel] = true;
|
||||||
|
device.updated = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 0xF: // SYSTEM EXCLUSIVE
|
||||||
|
break; // skipped above (!)
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't iterate through the other devices
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MIM_LONGDATA:
|
||||||
|
case MIM_ERROR:
|
||||||
|
case MIM_LONGERROR:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
+123
-574
@@ -39,35 +39,23 @@ namespace rawinput {
|
|||||||
// often than 60Hz
|
// often than 60Hz
|
||||||
uint32_t MIDI_NOTE_SUSTAIN = 20;
|
uint32_t MIDI_NOTE_SUSTAIN = 20;
|
||||||
|
|
||||||
static MidiNoteAlgorithm MIDI_NOTE_ALGORITHM = MidiNoteAlgorithm::V2;
|
|
||||||
|
|
||||||
// the price we pay for making spice overlay consume from raw input
|
// the price we pay for making spice overlay consume from raw input
|
||||||
// making focus detection a nightmare
|
// making focus detection a nightmare
|
||||||
bool OS_WINDOW_ACTIVE = false;
|
bool OS_WINDOW_ACTIVE = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
rawinput::MidiNoteAlgorithm rawinput::get_midi_algorithm() {
|
// when replacing a device slot in place, keep the old slot's per-device mutexes
|
||||||
return rawinput::MIDI_NOTE_ALGORITHM;
|
// instead of the freshly allocated pair on `replacement`. their addresses stay
|
||||||
}
|
// stable, so a thread still holding a snapshot pointer to the slot (e.g. the
|
||||||
|
// output thread blocked on mutex_out, which is taken without devices_mutex)
|
||||||
void rawinput::set_midi_algorithm(rawinput::MidiNoteAlgorithm new_algo) {
|
// never locks freed memory. the freshly allocated pair is freed here rather
|
||||||
rawinput::MIDI_NOTE_ALGORITHM = new_algo;
|
// than leaking the old one. the slot must already be destructed so both
|
||||||
std::string s = "Unknown";
|
// mutexes are unlocked
|
||||||
switch (new_algo) {
|
void rawinput::RawInputManager::reuse_device_mutexes(Device &replacement, const Device &existing) {
|
||||||
case rawinput::MidiNoteAlgorithm::LEGACY:
|
delete replacement.mutex;
|
||||||
s = "legacy";
|
delete replacement.mutex_out;
|
||||||
break;
|
replacement.mutex = existing.mutex;
|
||||||
case rawinput::MidiNoteAlgorithm::V2:
|
replacement.mutex_out = existing.mutex_out;
|
||||||
s = "v2";
|
|
||||||
break;
|
|
||||||
case rawinput::MidiNoteAlgorithm::V2_DRUM:
|
|
||||||
s = "v2_drum";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
log_info("rawinput", "assert failed: invalid midi algorithm");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
log_info("rawinput", "using MIDI algorithm: {}", s);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rawinput::RawInputManager::RawInputManager() {
|
rawinput::RawInputManager::RawInputManager() {
|
||||||
@@ -100,6 +88,9 @@ void rawinput::RawInputManager::stop() {
|
|||||||
this->hotplug = nullptr;
|
this->hotplug = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wait for any in-flight async MIDI scan before tearing down devices
|
||||||
|
this->midi_scan_join();
|
||||||
|
|
||||||
// unregister device messages
|
// unregister device messages
|
||||||
this->devices_unregister();
|
this->devices_unregister();
|
||||||
|
|
||||||
@@ -182,12 +173,18 @@ void rawinput::RawInputManager::input_hwnd_destroy() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void rawinput::RawInputManager::devices_reload() {
|
void rawinput::RawInputManager::devices_reload() {
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||||
this->devices_destruct();
|
this->devices_destruct();
|
||||||
log_info("rawinput", "reloading devices...");
|
log_info("rawinput", "reloading devices...");
|
||||||
|
|
||||||
// scan for devices
|
// scan for devices
|
||||||
this->devices_scan_rawinput();
|
this->devices_scan_rawinput();
|
||||||
this->devices_scan_midi();
|
|
||||||
|
// MIDI enumeration can block for ~10s while the Windows MIDI subsystem starts
|
||||||
|
// up, so run it off the init path instead of gating startup on it. it locks
|
||||||
|
// devices_mutex only for the list mutation, so it is safe to run concurrently
|
||||||
|
this->midi_scan_start();
|
||||||
|
|
||||||
this->devices_scan_piuio();
|
this->devices_scan_piuio();
|
||||||
|
|
||||||
if (ENABLE_SMX_STAGE) {
|
if (ENABLE_SMX_STAGE) {
|
||||||
@@ -208,6 +205,7 @@ void rawinput::RawInputManager::devices_reload() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void rawinput::RawInputManager::devices_scan_rawinput(const std::string &device_name) {
|
void rawinput::RawInputManager::devices_scan_rawinput(const std::string &device_name) {
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||||
log_misc("rawinput", "scan rawinput devices...");
|
log_misc("rawinput", "scan rawinput devices...");
|
||||||
|
|
||||||
// get number of devices
|
// get number of devices
|
||||||
@@ -239,6 +237,7 @@ void rawinput::RawInputManager::devices_scan_rawinput(const std::string &device_
|
|||||||
}
|
}
|
||||||
|
|
||||||
void rawinput::RawInputManager::devices_scan_rawinput(RAWINPUTDEVICELIST *device, bool log) {
|
void rawinput::RawInputManager::devices_scan_rawinput(RAWINPUTDEVICELIST *device, bool log) {
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||||
|
|
||||||
// get device name
|
// get device name
|
||||||
std::string device_name = rawinput_get_device_name(device->hDevice);
|
std::string device_name = rawinput_get_device_name(device->hDevice);
|
||||||
@@ -262,11 +261,13 @@ void rawinput::RawInputManager::devices_scan_rawinput(RAWINPUTDEVICELIST *device
|
|||||||
|
|
||||||
// check for duplicate handle
|
// check for duplicate handle
|
||||||
size_t unique_id = devices.size() + 1;
|
size_t unique_id = devices.size() + 1;
|
||||||
for (size_t i = 0; i < this->devices.size(); i++) {
|
size_t i = 0;
|
||||||
if (this->devices[i].name == device_name) {
|
for (const auto &existing : this->devices) {
|
||||||
|
if (existing.name == device_name) {
|
||||||
unique_id = i;
|
unique_id = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// build device
|
// build device
|
||||||
@@ -804,8 +805,9 @@ void rawinput::RawInputManager::devices_scan_rawinput(RAWINPUTDEVICELIST *device
|
|||||||
// carry over old device ID
|
// carry over old device ID
|
||||||
new_device.id = prev_device.id;
|
new_device.id = prev_device.id;
|
||||||
|
|
||||||
// destruct and replace
|
// destruct and replace, reusing the slot's existing mutexes
|
||||||
this->devices_destruct(&prev_device);
|
this->devices_destruct(&prev_device);
|
||||||
|
reuse_device_mutexes(new_device, prev_device);
|
||||||
prev_device = new_device;
|
prev_device = new_device;
|
||||||
|
|
||||||
// notify change
|
// notify change
|
||||||
@@ -829,123 +831,11 @@ void rawinput::RawInputManager::devices_scan_rawinput(RAWINPUTDEVICELIST *device
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void rawinput::RawInputManager::devices_scan_midi() {
|
|
||||||
log_misc("rawinput", "scan MIDI devices...");
|
|
||||||
|
|
||||||
// add midi devices
|
|
||||||
auto midi_device_count = midiInGetNumDevs();
|
|
||||||
for (size_t midi_device_id = 0; midi_device_id < midi_device_count; midi_device_id++) {
|
|
||||||
|
|
||||||
// get dev caps
|
|
||||||
MIDIINCAPS midi_device_caps{};
|
|
||||||
if (midiInGetDevCaps(midi_device_id, &midi_device_caps, sizeof(MIDIINCAPS)) != MMSYSERR_NOERROR) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
log_misc("rawinput", "found MIDI device: id {}, name {}, mid {}, pid {}",
|
|
||||||
midi_device_id, midi_device_caps.szPname, midi_device_caps.wMid, midi_device_caps.wPid);
|
|
||||||
|
|
||||||
// open device
|
|
||||||
HMIDIIN midi_device_handle;
|
|
||||||
if (midiInOpen(&midi_device_handle,
|
|
||||||
(UINT) midi_device_id,
|
|
||||||
(DWORD_PTR) &input_midi_proc,
|
|
||||||
(DWORD_PTR) this,
|
|
||||||
CALLBACK_FUNCTION) != MMSYSERR_NOERROR)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// start input
|
|
||||||
if (midiInStart(midi_device_handle) != MMSYSERR_NOERROR) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// device info
|
|
||||||
DeviceInfo midi_device_info {};
|
|
||||||
|
|
||||||
// device midi info
|
|
||||||
auto midi_device_midi_info = new DeviceMIDIInfo();
|
|
||||||
midi_device_midi_info->states = std::vector<bool>(16 * 128);
|
|
||||||
midi_device_midi_info->states_events = std::vector<uint8_t>(16 * 128);
|
|
||||||
midi_device_midi_info->bind_states = std::vector<bool>(16 * 128);
|
|
||||||
midi_device_midi_info->v2_last_on_time = std::vector<double>(16 * 128);
|
|
||||||
midi_device_midi_info->v2_last_off_time = std::vector<double>(16 * 128);
|
|
||||||
midi_device_midi_info->v2_velocity_threshold = std::vector<uint8_t>(16 * 128);
|
|
||||||
midi_device_midi_info->v2_velocity_threshold_set_on_device = std::vector<bool>(16 * 128);
|
|
||||||
midi_device_midi_info->velocity = std::vector<uint8_t>(16 * 128);
|
|
||||||
midi_device_midi_info->freeze = false;
|
|
||||||
midi_device_midi_info->controls_precision = std::vector<uint16_t>(16 * 32);
|
|
||||||
midi_device_midi_info->controls_precision_bind = std::vector<uint16_t>(16 * 32);
|
|
||||||
midi_device_midi_info->controls_precision_msb = std::vector<bool>(16 * 32);
|
|
||||||
midi_device_midi_info->controls_precision_lsb = std::vector<bool>(16 * 32);
|
|
||||||
midi_device_midi_info->controls_precision_set = std::vector<bool>(16 * 32);
|
|
||||||
midi_device_midi_info->controls_single = std::vector<uint8_t>(16 * 44);
|
|
||||||
midi_device_midi_info->controls_single_bind = std::vector<uint8_t>(16 * 44);
|
|
||||||
midi_device_midi_info->controls_single_set = std::vector<bool>(16 * 44);
|
|
||||||
midi_device_midi_info->controls_onoff = std::vector<bool>(16 * 6);
|
|
||||||
midi_device_midi_info->controls_onoff_bind = std::vector<bool>(16 * 6);
|
|
||||||
midi_device_midi_info->controls_onoff_set = std::vector<bool>(16 * 6);
|
|
||||||
midi_device_midi_info->v2_controls_onoff_last_on_time = std::vector<double>(16 * 6);
|
|
||||||
midi_device_midi_info->v2_controls_onoff_last_off_time = std::vector<double>(16 * 6);
|
|
||||||
midi_device_midi_info->pitch_bend = std::vector<int16_t>(16 * 6);
|
|
||||||
midi_device_midi_info->pitch_bend_set = std::vector<bool>(16 * 6);
|
|
||||||
|
|
||||||
// build identifier for MIDI
|
|
||||||
// ;MIDI; format is now set in stone (in other parts of the code base and in the config xml file)
|
|
||||||
// so it should never be changed
|
|
||||||
std::ostringstream midi_identifier;
|
|
||||||
midi_identifier << ";" << "MIDI";
|
|
||||||
midi_identifier << ";" << midi_device_id;
|
|
||||||
midi_identifier << ";" << midi_device_caps.szPname;
|
|
||||||
midi_identifier << ";" << midi_device_caps.wMid;
|
|
||||||
midi_identifier << ";" << midi_device_caps.wPid;
|
|
||||||
|
|
||||||
// build device
|
|
||||||
Device midi_device {};
|
|
||||||
midi_device.id = devices.size() + 1;
|
|
||||||
midi_device.type = MIDI;
|
|
||||||
midi_device.handle = midi_device_handle;
|
|
||||||
midi_device.name = midi_identifier.str();
|
|
||||||
midi_device.desc = to_string(midi_device_caps.szPname);
|
|
||||||
midi_device.info = midi_device_info;
|
|
||||||
midi_device.mutex = new std::mutex();
|
|
||||||
midi_device.mutex_out = new std::mutex();
|
|
||||||
midi_device.midiInfo = midi_device_midi_info;
|
|
||||||
|
|
||||||
// check for duplicate handle
|
|
||||||
for (auto &device : this->devices) {
|
|
||||||
if (device.name == midi_identifier.str()) {
|
|
||||||
|
|
||||||
// carry over ID
|
|
||||||
midi_device.id = device.id;
|
|
||||||
|
|
||||||
// destruct and replace
|
|
||||||
this->devices_destruct(&device);
|
|
||||||
device = midi_device;
|
|
||||||
|
|
||||||
// notify change
|
|
||||||
for (auto &cb : this->callback_change) {
|
|
||||||
cb.f(cb.data, &device);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add device to list
|
|
||||||
auto &device = this->devices.emplace_back(midi_device);
|
|
||||||
|
|
||||||
// notify add
|
|
||||||
for (auto &cb : this->callback_add) {
|
|
||||||
cb.f(cb.data, &device);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void rawinput::RawInputManager::devices_scan_piuio() {
|
void rawinput::RawInputManager::devices_scan_piuio() {
|
||||||
log_misc("rawinput", "scan PIUIO devices...");
|
log_misc("rawinput", "scan PIUIO devices...");
|
||||||
|
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||||
|
|
||||||
// add device to vector first so pointer is valid
|
// add device to vector first so pointer is valid
|
||||||
auto *new_piuio_device = new Device();
|
auto *new_piuio_device = new Device();
|
||||||
new_piuio_device->id = this->devices.size() + 1;
|
new_piuio_device->id = this->devices.size() + 1;
|
||||||
@@ -978,6 +868,8 @@ void rawinput::RawInputManager::devices_scan_piuio() {
|
|||||||
void rawinput::RawInputManager::devices_scan_smxstage() {
|
void rawinput::RawInputManager::devices_scan_smxstage() {
|
||||||
log_misc("rawinput", "scan SMX Stage devices...");
|
log_misc("rawinput", "scan SMX Stage devices...");
|
||||||
|
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||||
|
|
||||||
auto *new_smxstage_device = new Device();
|
auto *new_smxstage_device = new Device();
|
||||||
new_smxstage_device->id = this->devices.size() + 1;
|
new_smxstage_device->id = this->devices.size() + 1;
|
||||||
new_smxstage_device->type = SMX_STAGE;
|
new_smxstage_device->type = SMX_STAGE;
|
||||||
@@ -1005,6 +897,8 @@ void rawinput::RawInputManager::devices_scan_smxstage() {
|
|||||||
void rawinput::RawInputManager::devices_scan_smxdedicab() {
|
void rawinput::RawInputManager::devices_scan_smxdedicab() {
|
||||||
log_misc("rawinput", "scan SMX Dedicated Cabinet devices...");
|
log_misc("rawinput", "scan SMX Dedicated Cabinet devices...");
|
||||||
|
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||||
|
|
||||||
auto *new_smxdedicab_device = new Device();
|
auto *new_smxdedicab_device = new Device();
|
||||||
new_smxdedicab_device->id = this->devices.size() + 1;
|
new_smxdedicab_device->id = this->devices.size() + 1;
|
||||||
new_smxdedicab_device->type = SMX_DEDICAB;
|
new_smxdedicab_device->type = SMX_DEDICAB;
|
||||||
@@ -1032,6 +926,8 @@ void rawinput::RawInputManager::devices_scan_smxdedicab() {
|
|||||||
void rawinput::RawInputManager::devices_scan_xinput() {
|
void rawinput::RawInputManager::devices_scan_xinput() {
|
||||||
log_misc("rawinput", "scan XInput devices...");
|
log_misc("rawinput", "scan XInput devices...");
|
||||||
|
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||||
|
|
||||||
const auto connected_players = XINPUT_MGR->get_available_players();
|
const auto connected_players = XINPUT_MGR->get_available_players();
|
||||||
|
|
||||||
// first, destroy missing devices
|
// first, destroy missing devices
|
||||||
@@ -1072,7 +968,11 @@ void rawinput::RawInputManager::devices_scan_xinput() {
|
|||||||
if (prev_device.type == DESTROYED) {
|
if (prev_device.type == DESTROYED) {
|
||||||
log_info("rawinput", "overwriting previously destroyed XInput device: {}", prev_device.name);
|
log_info("rawinput", "overwriting previously destroyed XInput device: {}", prev_device.name);
|
||||||
const auto old_id = prev_device.id;
|
const auto old_id = prev_device.id;
|
||||||
prev_device = create_device(player);
|
|
||||||
|
// replace in place, reusing the slot's existing mutexes
|
||||||
|
auto replacement = create_device(player);
|
||||||
|
reuse_device_mutexes(replacement, prev_device);
|
||||||
|
prev_device = replacement;
|
||||||
prev_device.id = old_id;
|
prev_device.id = old_id;
|
||||||
|
|
||||||
// notify change
|
// notify change
|
||||||
@@ -1171,11 +1071,7 @@ void rawinput::RawInputManager::output_start() {
|
|||||||
// iterate all devices
|
// iterate all devices
|
||||||
do {
|
do {
|
||||||
output_thread_ready = false;
|
output_thread_ready = false;
|
||||||
for (auto &device : this->devices) {
|
this->devices_write_output_snapshot(true);
|
||||||
|
|
||||||
// write output
|
|
||||||
device_write_output(&device, true);
|
|
||||||
}
|
|
||||||
} while (output_thread_ready);
|
} while (output_thread_ready);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1336,6 +1232,8 @@ void rawinput::RawInputManager::sextet_register(const std::string &port_name, co
|
|||||||
|
|
||||||
log_misc("rawinput", "checking for sextet-stream device on {}...", port_name);
|
log_misc("rawinput", "checking for sextet-stream device on {}...", port_name);
|
||||||
|
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||||
|
|
||||||
// check for any sextet-stream devices
|
// check for any sextet-stream devices
|
||||||
Device device {};
|
Device device {};
|
||||||
device.type = SEXTET_OUTPUT;
|
device.type = SEXTET_OUTPUT;
|
||||||
@@ -1361,6 +1259,8 @@ void rawinput::RawInputManager::sextet_register(const std::string &port_name, co
|
|||||||
}
|
}
|
||||||
|
|
||||||
void rawinput::RawInputManager::devices_remove(const std::string &name) {
|
void rawinput::RawInputManager::devices_remove(const std::string &name) {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||||
|
|
||||||
// iterate devices
|
// iterate devices
|
||||||
for (auto &device : this->devices) {
|
for (auto &device : this->devices) {
|
||||||
@@ -1370,9 +1270,15 @@ void rawinput::RawInputManager::devices_remove(const std::string &name) {
|
|||||||
|
|
||||||
// remove device
|
// remove device
|
||||||
this->devices_destruct(&device);
|
this->devices_destruct(&device);
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// close any MIDI handle detached above, now that devices_mutex is released.
|
||||||
|
// removing a MIDI device queues its handle for deferred close; flush it here
|
||||||
|
// since we cannot rely on a later scan happening to do it
|
||||||
|
this->midi_close_deferred_flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
void rawinput::RawInputManager::devices_register() {
|
void rawinput::RawInputManager::devices_register() {
|
||||||
@@ -1509,21 +1415,25 @@ void rawinput::RawInputManager::devices_unregister() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void rawinput::RawInputManager::devices_destruct() {
|
void rawinput::RawInputManager::devices_destruct() {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||||
|
|
||||||
// check if there's something to destruct
|
// dispose devices (if there is anything to dispose)
|
||||||
if (this->devices.empty()) {
|
if (!this->devices.empty()) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// dispose devices
|
|
||||||
log_info("rawinput", "disposing devices");
|
log_info("rawinput", "disposing devices");
|
||||||
for (auto &device : this->devices) {
|
for (auto &device : this->devices) {
|
||||||
this->devices_destruct(&device, false);
|
this->devices_destruct(&device, false);
|
||||||
delete device.mutex;
|
delete device.mutex;
|
||||||
|
delete device.mutex_out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// empty array
|
// empty array
|
||||||
this->devices.clear();
|
this->devices.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// close any MIDI handles detached above, now that devices_mutex is released
|
||||||
|
this->midi_close_deferred_flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
void rawinput::RawInputManager::devices_destruct(Device *device, bool log) {
|
void rawinput::RawInputManager::devices_destruct(Device *device, bool log) {
|
||||||
@@ -1538,6 +1448,12 @@ void rawinput::RawInputManager::devices_destruct(Device *device, bool log) {
|
|||||||
log_info("rawinput", "destroying device: {} / {}", device->desc, device->name);
|
log_info("rawinput", "destroying device: {} / {}", device->desc, device->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hold the output mutex for the whole teardown, taken before we flip the type
|
||||||
|
// or free anything. device_write_output() locks mutex_out while dereferencing
|
||||||
|
// type-specific state (hidInfo/sextetInfo/...) on a snapshot taken outside
|
||||||
|
// devices_mutex, so mutex_out is what actually serializes it against us
|
||||||
|
std::lock_guard<std::mutex> lock_out(*device->mutex_out);
|
||||||
|
|
||||||
// mark as destroyed
|
// mark as destroyed
|
||||||
auto device_type = device->type;
|
auto device_type = device->type;
|
||||||
device->type = DESTROYED;
|
device->type = DESTROYED;
|
||||||
@@ -1564,8 +1480,15 @@ void rawinput::RawInputManager::devices_destruct(Device *device, bool log) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case MIDI:
|
case MIDI:
|
||||||
midiInReset((HMIDIIN) device->handle);
|
// never call midiInReset/midiInClose here: this runs under devices_mutex
|
||||||
midiInClose((HMIDIIN) device->handle);
|
// and those WinMM calls block until in-flight input_midi_proc callbacks
|
||||||
|
// return, callbacks that also take devices_mutex. detach the handle and
|
||||||
|
// let midi_close_deferred_flush() close it once the lock is released
|
||||||
|
if (device->handle != (HANDLE) INVALID_HANDLE_VALUE) {
|
||||||
|
log_misc("rawinput", "deferring MIDI handle close for: {}", device->desc);
|
||||||
|
this->midi_close_deferred.push_back((HMIDIIN) device->handle);
|
||||||
|
device->handle = (HANDLE) INVALID_HANDLE_VALUE;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case SEXTET_OUTPUT:
|
case SEXTET_OUTPUT:
|
||||||
device->sextetInfo->disconnect();
|
device->sextetInfo->disconnect();
|
||||||
@@ -1589,7 +1512,10 @@ void rawinput::RawInputManager::devices_destruct(Device *device, bool log) {
|
|||||||
device->smxstageInfo = nullptr;
|
device->smxstageInfo = nullptr;
|
||||||
delete device->smxdedicabInfo;
|
delete device->smxdedicabInfo;
|
||||||
device->smxdedicabInfo = nullptr;
|
device->smxdedicabInfo = nullptr;
|
||||||
// TODO: check if mutex can be deleted
|
|
||||||
|
// note: mutex and mutex_out are intentionally left alive here. other threads
|
||||||
|
// may still hold a snapshot pointer to this device and block on them, so they
|
||||||
|
// are only freed during full teardown; on reuse the slot keeps the same pair
|
||||||
}
|
}
|
||||||
|
|
||||||
LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
|
LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
|
||||||
@@ -1635,6 +1561,8 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
|
|||||||
|
|
||||||
// find device
|
// find device
|
||||||
HANDLE device_handle = data->header.hDevice;
|
HANDLE device_handle = data->header.hDevice;
|
||||||
|
// lock the device list so a concurrent scan can't mutate it while we iterate
|
||||||
|
std::lock_guard<std::recursive_mutex> devices_lock(ref->devices_mutex);
|
||||||
for (auto &device : ref->devices_get()) {
|
for (auto &device : ref->devices_get()) {
|
||||||
|
|
||||||
// skip if this is the wrong device
|
// skip if this is the wrong device
|
||||||
@@ -1836,6 +1764,16 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
|
|||||||
// get HID data
|
// get HID data
|
||||||
auto &data_hid = data->data.hid;
|
auto &data_hid = data->data.hid;
|
||||||
|
|
||||||
|
// a single WM_INPUT may carry more than one HID report from the same
|
||||||
|
// device: bRawData holds dwCount reports of dwSizeHid bytes each (the
|
||||||
|
// buffer size is dwSizeHid * dwCount). parse every report instead of
|
||||||
|
// only the first one.
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-rawhid
|
||||||
|
const DWORD hid_report_count = data_hid.dwCount > 0 ? data_hid.dwCount : 1;
|
||||||
|
for (DWORD hid_report_index = 0; hid_report_index < hid_report_count; hid_report_index++) {
|
||||||
|
auto *report_data = reinterpret_cast<BYTE *>(data_hid.bRawData)
|
||||||
|
+ (size_t) hid_report_index * data_hid.dwSizeHid;
|
||||||
|
|
||||||
// parse reports
|
// parse reports
|
||||||
for (const auto &pair : device.hidInfo->button_usage_pages) {
|
for (const auto &pair : device.hidInfo->button_usage_pages) {
|
||||||
const auto usage_page = pair.first.first;
|
const auto usage_page = pair.first.first;
|
||||||
@@ -1851,7 +1789,7 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
|
|||||||
usages.data(),
|
usages.data(),
|
||||||
&usages_length,
|
&usages_length,
|
||||||
reinterpret_cast<PHIDP_PREPARSED_DATA>(device.hidInfo->preparsed_data.get()),
|
reinterpret_cast<PHIDP_PREPARSED_DATA>(device.hidInfo->preparsed_data.get()),
|
||||||
reinterpret_cast<PCHAR>(data_hid.bRawData),
|
reinterpret_cast<PCHAR>(report_data),
|
||||||
data_hid.dwSizeHid) != HIDP_STATUS_SUCCESS) {
|
data_hid.dwSizeHid) != HIDP_STATUS_SUCCESS) {
|
||||||
|
|
||||||
// log_warning(
|
// log_warning(
|
||||||
@@ -1928,7 +1866,7 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
|
|||||||
value_caps.Range.UsageMin,
|
value_caps.Range.UsageMin,
|
||||||
reinterpret_cast<ULONG *>(&value_raw),
|
reinterpret_cast<ULONG *>(&value_raw),
|
||||||
reinterpret_cast<PHIDP_PREPARSED_DATA>(device.hidInfo->preparsed_data.get()),
|
reinterpret_cast<PHIDP_PREPARSED_DATA>(device.hidInfo->preparsed_data.get()),
|
||||||
reinterpret_cast<CHAR *>(data_hid.bRawData),
|
reinterpret_cast<CHAR *>(report_data),
|
||||||
data_hid.dwSizeHid) != HIDP_STATUS_SUCCESS)
|
data_hid.dwSizeHid) != HIDP_STATUS_SUCCESS)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -1991,6 +1929,7 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
|
|||||||
|
|
||||||
// touch screen
|
// touch screen
|
||||||
rawinput::touch::update_input(&device);
|
rawinput::touch::update_input(&device);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -2032,417 +1971,6 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
|
|||||||
return DefWindowProc(hWnd, msg, wparam, lParam);
|
return DefWindowProc(hWnd, msg, wparam, lParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CALLBACK rawinput::RawInputManager::input_midi_proc(HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance,
|
|
||||||
DWORD_PTR dwParam1, DWORD_PTR dwParam2) {
|
|
||||||
// get instance
|
|
||||||
auto ri_mgr = reinterpret_cast<RawInputManager *>(dwInstance);
|
|
||||||
|
|
||||||
// handle message
|
|
||||||
switch (wMsg) {
|
|
||||||
case MIM_OPEN:
|
|
||||||
case MIM_CLOSE:
|
|
||||||
break;
|
|
||||||
case MIM_MOREDATA:
|
|
||||||
case MIM_DATA: {
|
|
||||||
|
|
||||||
// param mapping
|
|
||||||
auto dwMidiMessage = dwParam1;
|
|
||||||
//auto dwTimestamp = dwParam2;
|
|
||||||
|
|
||||||
// message unpacking
|
|
||||||
auto midi_status = LOBYTE(LOWORD(dwMidiMessage));
|
|
||||||
auto midi_status_command = (midi_status & 0xF0u) >> 4u;
|
|
||||||
auto midi_status_channel = (midi_status & 0x0Fu);
|
|
||||||
auto midi_byte1 = HIBYTE(LOWORD(dwMidiMessage));
|
|
||||||
auto midi_byte2 = LOBYTE(HIWORD(dwMidiMessage));
|
|
||||||
|
|
||||||
// callbacks
|
|
||||||
for (auto &callback : ri_mgr->callback_midi) {
|
|
||||||
|
|
||||||
// find device
|
|
||||||
for (auto &device : ri_mgr->devices_get()) {
|
|
||||||
if (device.type == MIDI && device.handle == hMidiIn) {
|
|
||||||
|
|
||||||
// call function
|
|
||||||
callback.f(callback.data, &device,
|
|
||||||
midi_status_command, midi_status_channel,
|
|
||||||
midi_byte1, midi_byte2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip unused messages types early for performance
|
|
||||||
bool skip = false;
|
|
||||||
switch (midi_status_command) {
|
|
||||||
case 0xA: // POLYPHONIC PRESSURE
|
|
||||||
case 0xC: // PROGRAM CHANGE
|
|
||||||
case 0xD: // CHANNEL PRESSURE
|
|
||||||
case 0xF: // SYSTEM EXCLUSIVE
|
|
||||||
skip = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (skip) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// find device
|
|
||||||
for (auto &device : ri_mgr->devices_get()) {
|
|
||||||
|
|
||||||
// filter non MIDI devices
|
|
||||||
if (device.type != MIDI) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter wrong handles
|
|
||||||
if (device.handle != hMidiIn) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get input time
|
|
||||||
const auto input_time = get_performance_seconds();
|
|
||||||
|
|
||||||
// lock device
|
|
||||||
std::lock_guard<std::mutex> lock(*device.mutex);
|
|
||||||
|
|
||||||
// update hz
|
|
||||||
auto diff_time = input_time - device.input_time;
|
|
||||||
if (diff_time > 0.0001) {
|
|
||||||
device.input_hz = 1.f / diff_time;
|
|
||||||
device.input_hz_max = MAX(device.input_hz_max, device.input_hz);
|
|
||||||
device.input_time = input_time;
|
|
||||||
}
|
|
||||||
|
|
||||||
// command logic
|
|
||||||
switch (midi_status_command) {
|
|
||||||
case 0x8: { // NOTE OFF
|
|
||||||
|
|
||||||
// param mapping
|
|
||||||
const auto midi_note = midi_byte1 & 127u;
|
|
||||||
|
|
||||||
// log_misc("midi", "[{}] OFF", midi_note);
|
|
||||||
|
|
||||||
// get index
|
|
||||||
const auto midi_index = midi_status_channel * 128 + midi_note;
|
|
||||||
if (midi_index < 16 * 128) {
|
|
||||||
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::LEGACY) {
|
|
||||||
// update velocity
|
|
||||||
device.midiInfo->velocity[midi_index] = 0;
|
|
||||||
// disable note
|
|
||||||
if (device.midiInfo->states_events[midi_index]) {
|
|
||||||
device.midiInfo->states[midi_index] = false;
|
|
||||||
}
|
|
||||||
device.updated = true;
|
|
||||||
} else {
|
|
||||||
// v2 logic
|
|
||||||
// exactly the same as NOTE ON with 0 velocity
|
|
||||||
// velocity is kept; api will ignore it if button is not pressed
|
|
||||||
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::V2) {
|
|
||||||
device.midiInfo->v2_last_off_time[midi_index] = get_performance_milliseconds();
|
|
||||||
device.updated = true;
|
|
||||||
}
|
|
||||||
// for v2_drum, NOTE OFF is ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 0x9: { // NOTE ON
|
|
||||||
|
|
||||||
// param mapping
|
|
||||||
const auto midi_note = midi_byte1 & 127u;
|
|
||||||
|
|
||||||
// per MIDI spec, if NOTE ON is sent with 0 velocity, it's the same thing as NOTE OFF.
|
|
||||||
const auto midi_velocity = midi_byte2 & 127u;
|
|
||||||
|
|
||||||
// log_misc("midi", "[{}] ON v={}", midi_note, midi_velocity);
|
|
||||||
|
|
||||||
// get index
|
|
||||||
const auto midi_index = midi_status_channel * 128 + midi_note;
|
|
||||||
if (midi_index < 16 * 128) {
|
|
||||||
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::LEGACY) {
|
|
||||||
// update velocity
|
|
||||||
device.midiInfo->velocity[midi_index] = (uint8_t) midi_velocity;
|
|
||||||
|
|
||||||
if (midi_velocity) {
|
|
||||||
// update events (for legacy logic)
|
|
||||||
// how does this work? see the comment in api.cpp around the check for
|
|
||||||
// get_midi_algorithm() for an explanation
|
|
||||||
|
|
||||||
// so currently it's meant to be turned on
|
|
||||||
device.midiInfo->states[midi_index] = true;
|
|
||||||
|
|
||||||
// if its already on just increase it by one to turn it off
|
|
||||||
if (device.midiInfo->states_events[midi_index] % 2)
|
|
||||||
device.midiInfo->states_events[midi_index]++;
|
|
||||||
else
|
|
||||||
device.midiInfo->states_events[midi_index] += 2;
|
|
||||||
|
|
||||||
} else if (!device.midiInfo->freeze) {
|
|
||||||
// velocity 0 means turn it off
|
|
||||||
device.midiInfo->states[midi_index] = false;
|
|
||||||
}
|
|
||||||
device.updated = true;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// v2 logic
|
|
||||||
const auto now = get_performance_milliseconds();
|
|
||||||
auto threshold = device.midiInfo->v2_velocity_threshold[midi_index];
|
|
||||||
// when device is frozen (binding is happening) ignore the velocity threshold
|
|
||||||
// this allows users to bind keys even if the midi note is set to high threshold at
|
|
||||||
// rawinput layer, either from a previous binding that was cleared, or existing binding
|
|
||||||
// for another button
|
|
||||||
if (device.midiInfo->freeze) {
|
|
||||||
threshold = 0;
|
|
||||||
}
|
|
||||||
if (threshold < midi_velocity) {
|
|
||||||
device.midiInfo->velocity[midi_index] = (uint8_t)midi_velocity;
|
|
||||||
device.midiInfo->v2_last_on_time[midi_index] = now;
|
|
||||||
|
|
||||||
// disable holds and release all notes immediately
|
|
||||||
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::V2_DRUM) {
|
|
||||||
device.midiInfo->v2_last_off_time[midi_index] = now;
|
|
||||||
}
|
|
||||||
device.updated = true;
|
|
||||||
} else {
|
|
||||||
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::V2) {
|
|
||||||
// insufficient velocity ON == exactly the same as NOTE OFF
|
|
||||||
device.midiInfo->v2_last_off_time[midi_index] = now;
|
|
||||||
device.updated = true;
|
|
||||||
}
|
|
||||||
// for v2_drum, NOTE ON with insufficient velocity is ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 0xA: // POLYPHONIC PRESSURE
|
|
||||||
break; // skipped above (!)
|
|
||||||
case 0xB: { // CONTROL CHANGE
|
|
||||||
|
|
||||||
// param mapping
|
|
||||||
auto midi_control = midi_byte1 & 127;
|
|
||||||
auto midi_value = midi_byte2 & 127u;
|
|
||||||
|
|
||||||
// get index
|
|
||||||
auto channel_offset = midi_status_channel * 128;
|
|
||||||
auto midi_index = channel_offset + midi_control;
|
|
||||||
if (midi_index < 16 * 128) {
|
|
||||||
|
|
||||||
// continuous controller MSB
|
|
||||||
if (midi_control >= 0x00 && midi_control <= 0x1F) {
|
|
||||||
|
|
||||||
// update index
|
|
||||||
midi_index = midi_status_channel * 32 + midi_control;
|
|
||||||
device.midiInfo->controls_precision_set[midi_index] = true;
|
|
||||||
|
|
||||||
// check if MSB wasn't sent yet
|
|
||||||
if (!device.midiInfo->controls_precision_msb[midi_index]) {
|
|
||||||
device.midiInfo->controls_precision_msb[midi_index] = true;
|
|
||||||
|
|
||||||
// move LSB value to actual position
|
|
||||||
device.midiInfo->controls_precision[midi_index] >>= 7u;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update MSB
|
|
||||||
auto tmp = device.midiInfo->controls_precision[midi_index];
|
|
||||||
tmp = (tmp & 127u) | midi_value << 7u;
|
|
||||||
if (!device.midiInfo->controls_precision_lsb[midi_index])
|
|
||||||
tmp = (tmp & (127u << 7u)) | midi_value;
|
|
||||||
if (device.midiInfo->controls_precision[midi_index] != tmp) {
|
|
||||||
device.midiInfo->controls_precision[midi_index] = tmp;
|
|
||||||
device.updated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// continuous controller LSB
|
|
||||||
else if (midi_control >= 0x20 && midi_control <= 0x3F) {
|
|
||||||
|
|
||||||
// update index
|
|
||||||
midi_index = midi_status_channel * 32 + midi_control - 0x20;
|
|
||||||
device.midiInfo->controls_precision_set[midi_index] = true;
|
|
||||||
device.midiInfo->controls_precision_lsb[midi_index] = true;
|
|
||||||
|
|
||||||
// check for MSB flag
|
|
||||||
if (device.midiInfo->controls_precision_msb[midi_index]) {
|
|
||||||
|
|
||||||
// update LSB only
|
|
||||||
auto tmp = device.midiInfo->controls_precision[midi_index];
|
|
||||||
tmp &= 127u << 7u;
|
|
||||||
tmp |= midi_value;
|
|
||||||
if (device.midiInfo->controls_precision[midi_index] != tmp) {
|
|
||||||
device.midiInfo->controls_precision[midi_index] = tmp;
|
|
||||||
device.updated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// cast to MSB
|
|
||||||
if (device.midiInfo->controls_precision[midi_index] != midi_value << 7u) {
|
|
||||||
device.midiInfo->controls_precision[midi_index] = midi_value << 7u | midi_value;
|
|
||||||
device.updated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// on/off controls
|
|
||||||
else if (midi_control >= 0x40 && midi_control <= 0x45) {
|
|
||||||
|
|
||||||
// update index
|
|
||||||
midi_index = midi_status_channel * 6 + midi_control - 0x40;
|
|
||||||
device.midiInfo->controls_onoff_set[midi_index] = true;
|
|
||||||
|
|
||||||
// get on/off state
|
|
||||||
const auto onoff_state = midi_value >= 64;
|
|
||||||
|
|
||||||
// update device
|
|
||||||
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::LEGACY) {
|
|
||||||
if (device.midiInfo->controls_onoff[midi_index] != onoff_state) {
|
|
||||||
device.midiInfo->controls_onoff[midi_index] = onoff_state;
|
|
||||||
device.updated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// v2 and v2_drum:
|
|
||||||
// unlike notes (drum pads), controls can send continuous ON signal
|
|
||||||
// therefore, check for rising and falling edges
|
|
||||||
const auto now = get_performance_milliseconds();
|
|
||||||
const auto previous_value = device.midiInfo->controls_onoff[midi_index];
|
|
||||||
if (!previous_value && onoff_state) {
|
|
||||||
device.midiInfo->v2_controls_onoff_last_on_time[midi_index] = now;
|
|
||||||
device.updated = true;
|
|
||||||
} else if (previous_value && !onoff_state) {
|
|
||||||
device.midiInfo->v2_controls_onoff_last_off_time[midi_index] = now;
|
|
||||||
device.updated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
device.midiInfo->controls_onoff[midi_index] = onoff_state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// single byte controllers
|
|
||||||
else if (midi_control >= 0x46 && midi_control <= 0x5F) {
|
|
||||||
|
|
||||||
// update index
|
|
||||||
midi_index = midi_status_channel * 44 + midi_control - 0x46;
|
|
||||||
device.midiInfo->controls_single_set[midi_index] = true;
|
|
||||||
|
|
||||||
// update device
|
|
||||||
if (device.midiInfo->controls_single[midi_index] != midi_value) {
|
|
||||||
device.midiInfo->controls_single[midi_index] = midi_value;
|
|
||||||
device.updated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// increment/decrement and parameter numbers
|
|
||||||
else if (midi_control >= 0x60 && midi_control <= 0x65) {
|
|
||||||
// skip
|
|
||||||
}
|
|
||||||
|
|
||||||
// undefined single-byte controllers
|
|
||||||
else if (midi_control >= 0x66 && midi_control <= 0x77) {
|
|
||||||
|
|
||||||
// update index
|
|
||||||
auto sbc_count = 0x5F - 0x46 + 1;
|
|
||||||
midi_index = midi_status_channel * 44 + midi_control - 0x66 + sbc_count;
|
|
||||||
device.midiInfo->controls_single_set[midi_index] = true;
|
|
||||||
|
|
||||||
// update device
|
|
||||||
if (device.midiInfo->controls_single[midi_index] != midi_value) {
|
|
||||||
device.midiInfo->controls_single[midi_index] = midi_value;
|
|
||||||
device.updated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// channel mode messages
|
|
||||||
else if (midi_control >= 0x78 && midi_control <= 0x7F) {
|
|
||||||
switch (midi_control) {
|
|
||||||
case 0x78: // all sound off
|
|
||||||
break;
|
|
||||||
case 0x79: { // reset all controllers
|
|
||||||
for (int i = 0; i < 32; i++)
|
|
||||||
device.midiInfo->controls_precision[midi_status_channel * 32 + i] = 0;
|
|
||||||
for (int i = 0; i < 44; i++)
|
|
||||||
device.midiInfo->controls_single[midi_status_channel * 44 + i] = 0;
|
|
||||||
for (int i = 0; i < 6; i++) {
|
|
||||||
const auto index = midi_status_channel * 6 + i;
|
|
||||||
device.midiInfo->controls_onoff[index] = false;
|
|
||||||
device.midiInfo->v2_controls_onoff_last_on_time[index] = 0;
|
|
||||||
device.midiInfo->v2_controls_onoff_last_off_time[index] = 0;
|
|
||||||
}
|
|
||||||
device.updated = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 0x7A: // local control on/off
|
|
||||||
break;
|
|
||||||
case 0x7B: // all notes off
|
|
||||||
case 0x7C: // omni mode off + all notes off
|
|
||||||
case 0x7D: // omni mode on + all notes off
|
|
||||||
case 0x7E: // mono mode on + poly off + all notes off
|
|
||||||
case 0x7F: // poly mode on + mono off + all notes off
|
|
||||||
for (int i = 0; i < 128; i++) {
|
|
||||||
// common
|
|
||||||
device.midiInfo->velocity[channel_offset + i] = 0;
|
|
||||||
device.midiInfo->bind_states[channel_offset + i] = false;
|
|
||||||
|
|
||||||
// legacy
|
|
||||||
device.midiInfo->states[channel_offset + i] = false;
|
|
||||||
device.midiInfo->states_events[channel_offset + i] = 0;
|
|
||||||
|
|
||||||
// v2
|
|
||||||
device.midiInfo->v2_last_off_time[channel_offset + i] = 0.0;
|
|
||||||
device.midiInfo->v2_last_on_time[channel_offset + i] = 0.0;
|
|
||||||
}
|
|
||||||
device.updated = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 0xC: // PROGRAM CHANGE
|
|
||||||
break; // skipped above (!)
|
|
||||||
case 0xD: // CHANNEL PRESSURE
|
|
||||||
break; // skipped above (!)
|
|
||||||
case 0xE: { // PITCH BENDING
|
|
||||||
|
|
||||||
// raw values range from [0, 0x3FFF] (16383)
|
|
||||||
// build value, centered around zero [-8192, 8191]
|
|
||||||
int16_t value = ((midi_byte1) | (midi_byte2 << 7u)) - 0x2000;
|
|
||||||
|
|
||||||
// update device
|
|
||||||
if (device.midiInfo->pitch_bend[midi_status_channel] != value) {
|
|
||||||
device.midiInfo->pitch_bend[midi_status_channel] = value;
|
|
||||||
device.midiInfo->pitch_bend_set[midi_status_channel] = true;
|
|
||||||
device.updated = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 0xF: // SYSTEM EXCLUSIVE
|
|
||||||
break; // skipped above (!)
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't iterate through the other devices
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case MIM_LONGDATA:
|
|
||||||
case MIM_ERROR:
|
|
||||||
case MIM_LONGERROR:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void rawinput::RawInputManager::device_write_output(Device *device, bool only_updated) {
|
void rawinput::RawInputManager::device_write_output(Device *device, bool only_updated) {
|
||||||
|
|
||||||
// check if output is enabled
|
// check if output is enabled
|
||||||
@@ -2696,10 +2224,24 @@ void rawinput::RawInputManager::devices_flush_output(bool optimized) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// blocking routine
|
// blocking routine
|
||||||
for (auto &device : this->devices) {
|
this->devices_write_output_snapshot(false);
|
||||||
|
}
|
||||||
|
|
||||||
// write output
|
void rawinput::RawInputManager::devices_write_output_snapshot(bool only_updated) {
|
||||||
device_write_output(&device, false);
|
|
||||||
|
// snapshot device pointers under devices_mutex, then write without holding it.
|
||||||
|
// the std::list keeps addresses stable, so the (potentially blocking) writes
|
||||||
|
// below don't hold devices_mutex against input
|
||||||
|
std::vector<Device *> snapshot;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||||
|
snapshot.reserve(this->devices.size());
|
||||||
|
for (auto &device : this->devices) {
|
||||||
|
snapshot.push_back(&device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto *device : snapshot) {
|
||||||
|
device_write_output(device, only_updated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2712,6 +2254,9 @@ void rawinput::RawInputManager::devices_print() {
|
|||||||
|
|
||||||
bool touchscreen_found = false;
|
bool touchscreen_found = false;
|
||||||
|
|
||||||
|
// lock the device list while iterating
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||||
|
|
||||||
// iterate devices
|
// iterate devices
|
||||||
log_info("rawinput", "printing list of detected devices");
|
log_info("rawinput", "printing list of detected devices");
|
||||||
log_info("rawinput", "detected device count: {}", devices.size());
|
log_info("rawinput", "detected device count: {}", devices.size());
|
||||||
@@ -2905,6 +2450,10 @@ rawinput::Device *rawinput::RawInputManager::devices_get(const std::string &name
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lock the device list so a concurrent scan can't mutate it while we search.
|
||||||
|
// the returned pointer stays valid after unlock because devices is a std::list
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||||
|
|
||||||
// check if caller wants only updated devices
|
// check if caller wants only updated devices
|
||||||
if (updated) {
|
if (updated) {
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
|
#include <list>
|
||||||
|
#include <mutex>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
@@ -63,10 +65,38 @@ namespace rawinput {
|
|||||||
class RawInputManager {
|
class RawInputManager {
|
||||||
private:
|
private:
|
||||||
|
|
||||||
HotplugManager *hotplug;
|
HotplugManager *hotplug = nullptr;
|
||||||
std::vector<Device> devices;
|
|
||||||
|
// std::list (not std::vector) so adding/removing a device never invalidates
|
||||||
|
// pointers to other elements. devices_get() hands out pointers that escape the
|
||||||
|
// lock and are dereferenced later by pollers, so a concurrent scan must not
|
||||||
|
// relocate existing devices
|
||||||
|
std::list<Device> devices;
|
||||||
|
std::recursive_mutex devices_mutex;
|
||||||
|
|
||||||
WNDCLASSEX input_hwnd_class {};
|
WNDCLASSEX input_hwnd_class {};
|
||||||
std::thread *input_thread = nullptr;
|
std::thread *input_thread = nullptr;
|
||||||
|
std::thread *midi_thread = nullptr;
|
||||||
|
|
||||||
|
// guards the MIDI scan scheduler flags below. only held around the flag
|
||||||
|
// checks, never across the (slow) enumeration, so it makes "is a scan
|
||||||
|
// running?" and the worker's "exit or rescan?" a single atomic decision -
|
||||||
|
// a request can never slip into the gap between the two
|
||||||
|
std::mutex midi_scan_m;
|
||||||
|
|
||||||
|
// true while a scan worker thread is running. only one runs at a time
|
||||||
|
bool midi_scan_active = false;
|
||||||
|
|
||||||
|
// set when a scan is requested while one is already running, so the worker
|
||||||
|
// rescans once instead of dropping the request (events that arrive during
|
||||||
|
// the slow initial scan would otherwise be lost)
|
||||||
|
bool midi_scan_pending = false;
|
||||||
|
|
||||||
|
// MIDI handles pending close. devices_destruct() runs under devices_mutex but
|
||||||
|
// midiInReset/midiInClose must not (they block on the MIDI callback, which
|
||||||
|
// also takes devices_mutex), so handles are queued here and closed by
|
||||||
|
// midi_close_deferred_flush() once the lock is released
|
||||||
|
std::vector<HMIDIIN> midi_close_deferred;
|
||||||
std::thread *flush_thread = nullptr;
|
std::thread *flush_thread = nullptr;
|
||||||
bool flush_thread_running = false;
|
bool flush_thread_running = false;
|
||||||
std::mutex flush_thread_m;
|
std::mutex flush_thread_m;
|
||||||
@@ -83,6 +113,8 @@ namespace rawinput {
|
|||||||
void input_hwnd_create();
|
void input_hwnd_create();
|
||||||
void input_hwnd_destroy();
|
void input_hwnd_destroy();
|
||||||
void devices_reload();
|
void devices_reload();
|
||||||
|
void midi_scan_join();
|
||||||
|
void midi_close_deferred_flush();
|
||||||
void devices_scan_rawinput(RAWINPUTDEVICELIST *device, bool log = true);
|
void devices_scan_rawinput(RAWINPUTDEVICELIST *device, bool log = true);
|
||||||
void devices_scan_piuio();
|
void devices_scan_piuio();
|
||||||
void devices_scan_smxstage();
|
void devices_scan_smxstage();
|
||||||
@@ -93,6 +125,7 @@ namespace rawinput {
|
|||||||
void flush_stop();
|
void flush_stop();
|
||||||
void output_start();
|
void output_start();
|
||||||
void output_stop();
|
void output_stop();
|
||||||
|
void devices_write_output_snapshot(bool only_updated);
|
||||||
|
|
||||||
static std::string rawinput_get_device_name(HANDLE hDevice);
|
static std::string rawinput_get_device_name(HANDLE hDevice);
|
||||||
static std::string rawinput_get_device_description(const DeviceInfo& info, const std::string &device_name);
|
static std::string rawinput_get_device_description(const DeviceInfo& info, const std::string &device_name);
|
||||||
@@ -101,6 +134,11 @@ namespace rawinput {
|
|||||||
static void CALLBACK input_midi_proc(HMIDIIN, UINT, DWORD_PTR, DWORD_PTR, DWORD_PTR);
|
static void CALLBACK input_midi_proc(HMIDIIN, UINT, DWORD_PTR, DWORD_PTR, DWORD_PTR);
|
||||||
static DeviceInfo get_device_info(const std::string &device_name);
|
static DeviceInfo get_device_info(const std::string &device_name);
|
||||||
|
|
||||||
|
// shared by the rawinput / xinput / midi device scans when replacing a slot
|
||||||
|
// in place; keeps the existing slot's mutex pair so snapshot pointers held by
|
||||||
|
// other threads stay valid (see the definition in rawinput.cpp)
|
||||||
|
static void reuse_device_mutexes(Device &replacement, const Device &existing);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
HWND input_hwnd = nullptr;
|
HWND input_hwnd = nullptr;
|
||||||
@@ -118,6 +156,7 @@ namespace rawinput {
|
|||||||
|
|
||||||
void devices_scan_rawinput(const std::string &device_name = "");
|
void devices_scan_rawinput(const std::string &device_name = "");
|
||||||
void devices_scan_midi();
|
void devices_scan_midi();
|
||||||
|
void midi_scan_start();
|
||||||
void devices_scan_xinput();
|
void devices_scan_xinput();
|
||||||
void devices_remove(const std::string &name);
|
void devices_remove(const std::string &name);
|
||||||
|
|
||||||
@@ -130,11 +169,12 @@ namespace rawinput {
|
|||||||
void __stdcall devices_print();
|
void __stdcall devices_print();
|
||||||
Device *devices_get(const std::string &name, bool updated = false);
|
Device *devices_get(const std::string &name, bool updated = false);
|
||||||
|
|
||||||
inline std::vector<Device> &devices_get() {
|
inline std::list<Device> &devices_get() {
|
||||||
return devices;
|
return devices;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline std::vector<Device *> devices_get_updated() {
|
inline std::vector<Device *> devices_get_updated() {
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(devices_mutex);
|
||||||
std::vector<Device *> updated;
|
std::vector<Device *> updated;
|
||||||
for (auto &device : devices_get()) {
|
for (auto &device : devices_get()) {
|
||||||
device.mutex->lock();
|
device.mutex->lock();
|
||||||
@@ -150,6 +190,7 @@ namespace rawinput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline void devices_midi_freeze(bool freeze) {
|
inline void devices_midi_freeze(bool freeze) {
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(devices_mutex);
|
||||||
for (auto &device : devices_get()) {
|
for (auto &device : devices_get()) {
|
||||||
if (device.type == MIDI) {
|
if (device.type == MIDI) {
|
||||||
device.midiInfo->freeze = freeze;
|
device.midiInfo->freeze = freeze;
|
||||||
|
|||||||
+157
-15
@@ -24,13 +24,116 @@ namespace rawinput::touch {
|
|||||||
// settings
|
// settings
|
||||||
bool DISABLED = false;
|
bool DISABLED = false;
|
||||||
bool INVERTED = false;
|
bool INVERTED = false;
|
||||||
|
AspectMode ASPECT_COMPENSATION_MODE = AspectMode::Auto;
|
||||||
|
bool ASPECT_COMPENSATION_GAME = false;
|
||||||
|
|
||||||
// state
|
// state
|
||||||
DWORD DISPLAY_ORIENTATION = DMDO_DEFAULT;
|
DWORD DISPLAY_ORIENTATION = DMDO_DEFAULT;
|
||||||
long DISPLAY_SIZE_X = 1920L;
|
long DISPLAY_SIZE_X = 1920L;
|
||||||
long DISPLAY_SIZE_Y = 1080L;
|
long DISPLAY_SIZE_Y = 1080L;
|
||||||
|
long DISPLAY_NATIVE_X = 0L;
|
||||||
|
long DISPLAY_NATIVE_Y = 0L;
|
||||||
bool DISPLAY_INITIALIZED = false;
|
bool DISPLAY_INITIALIZED = false;
|
||||||
|
|
||||||
|
bool aspect_compensation_enabled() {
|
||||||
|
switch (ASPECT_COMPENSATION_MODE) {
|
||||||
|
case AspectMode::On:
|
||||||
|
return true;
|
||||||
|
case AspectMode::Off:
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return ASPECT_COMPENSATION_GAME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// compute the centered letterbox inset (fraction cropped from each side) that results
|
||||||
|
// from showing the current display mode on a panel with a different native aspect
|
||||||
|
// ratio. only one of inset_x / inset_y is ever non-zero.
|
||||||
|
static void compute_letterbox_inset(double &inset_x, double &inset_y) {
|
||||||
|
inset_x = 0.0;
|
||||||
|
inset_y = 0.0;
|
||||||
|
|
||||||
|
// native resolution in the current display orientation
|
||||||
|
long native_x = DISPLAY_NATIVE_X;
|
||||||
|
long native_y = DISPLAY_NATIVE_Y;
|
||||||
|
if ((DISPLAY_SIZE_Y >= DISPLAY_SIZE_X) != (native_y >= native_x)) {
|
||||||
|
long tmp = native_x;
|
||||||
|
native_x = native_y;
|
||||||
|
native_y = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// compare the displayed aspect ratio against the panel's native one; ignore
|
||||||
|
// tiny mismatches (e.g. 1366x768 vs true 16:9) that would only add a sub-pixel
|
||||||
|
// inset, so near-matched panels stay an exact no-op
|
||||||
|
double current_aspect = (double) DISPLAY_SIZE_X / (double) DISPLAY_SIZE_Y;
|
||||||
|
double native_aspect = (double) native_x / (double) native_y;
|
||||||
|
constexpr double aspect_epsilon = 0.01;
|
||||||
|
if (current_aspect < native_aspect - aspect_epsilon) {
|
||||||
|
|
||||||
|
// image is narrower than the panel -> bars on left/right
|
||||||
|
inset_x = (1.0 - current_aspect / native_aspect) / 2.0;
|
||||||
|
} else if (current_aspect > native_aspect + aspect_epsilon) {
|
||||||
|
|
||||||
|
// image is wider than the panel -> bars on top/bottom
|
||||||
|
inset_y = (1.0 - native_aspect / current_aspect) / 2.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// undo letterboxing: remap the normalized (0.0 - 1.0) digitizer coordinates, which span
|
||||||
|
// the whole physical panel, into the displayed image region. returns false when the
|
||||||
|
// touch lands in the black bars (outside the image) and should be ignored.
|
||||||
|
static bool remap_aspect_compensation(double &norm_x, double &norm_y) {
|
||||||
|
if (DISPLAY_NATIVE_X <= 0 || DISPLAY_NATIVE_Y <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
double inset_x, inset_y;
|
||||||
|
compute_letterbox_inset(inset_x, inset_y);
|
||||||
|
|
||||||
|
// log once the first time the compensation actually engages (non-zero inset)
|
||||||
|
static bool logged_active = false;
|
||||||
|
if (!logged_active && (inset_x > 0.0 || inset_y > 0.0)) {
|
||||||
|
logged_active = true;
|
||||||
|
log_info("rawinput", "aspect compensation active: display {}x{}, native {}x{}, inset x={:.4f} y={:.4f}",
|
||||||
|
(int) DISPLAY_SIZE_X, (int) DISPLAY_SIZE_Y,
|
||||||
|
(int) DISPLAY_NATIVE_X, (int) DISPLAY_NATIVE_Y,
|
||||||
|
inset_x, inset_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore touches inside the black bars (outside the displayed image)
|
||||||
|
if ((inset_x > 0.0 && (norm_x < inset_x || norm_x > 1.0 - inset_x)) ||
|
||||||
|
(inset_y > 0.0 && (norm_y < inset_y || norm_y > 1.0 - inset_y))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remap the displayed image region back to the full 0.0 - 1.0 range
|
||||||
|
if (inset_x > 0.0) {
|
||||||
|
norm_x = (norm_x - inset_x) / (1.0 - 2.0 * inset_x);
|
||||||
|
}
|
||||||
|
if (inset_y > 0.0) {
|
||||||
|
norm_y = (norm_y - inset_y) / (1.0 - 2.0 * inset_y);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine the native (maximum) resolution advertised by the primary display device.
|
||||||
|
// used to detect and undo letterboxing when a non-native display mode is in use.
|
||||||
|
// falls back to the current display size if enumeration yields nothing larger.
|
||||||
|
static void detect_native_resolution() {
|
||||||
|
DISPLAY_NATIVE_X = DISPLAY_SIZE_X;
|
||||||
|
DISPLAY_NATIVE_Y = DISPLAY_SIZE_Y;
|
||||||
|
|
||||||
|
DEVMODE native_mode{};
|
||||||
|
native_mode.dmSize = sizeof(native_mode);
|
||||||
|
for (int i = 0; EnumDisplaySettings(nullptr, i, &native_mode); i++) {
|
||||||
|
if ((long) native_mode.dmPelsWidth * (long) native_mode.dmPelsHeight >
|
||||||
|
DISPLAY_NATIVE_X * DISPLAY_NATIVE_Y) {
|
||||||
|
DISPLAY_NATIVE_X = (long) native_mode.dmPelsWidth;
|
||||||
|
DISPLAY_NATIVE_Y = (long) native_mode.dmPelsHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool is_touchscreen(Device *device) {
|
bool is_touchscreen(Device *device) {
|
||||||
|
|
||||||
// check if disabled
|
// check if disabled
|
||||||
@@ -322,6 +425,18 @@ namespace rawinput::touch {
|
|||||||
std::vector<DWORD> touch_removes;
|
std::vector<DWORD> touch_removes;
|
||||||
std::vector<TouchPoint> touch_writes;
|
std::vector<TouchPoint> touch_writes;
|
||||||
std::vector<DWORD> touch_modifications;
|
std::vector<DWORD> touch_modifications;
|
||||||
|
|
||||||
|
// drop every touch point with the given id (marking it as released)
|
||||||
|
auto remove_touch_point = [&] (DWORD id) {
|
||||||
|
touch_removes.push_back(id);
|
||||||
|
touch.touch_points.erase(std::remove_if(
|
||||||
|
touch.touch_points.begin(),
|
||||||
|
touch.touch_points.end(),
|
||||||
|
[id] (const HIDTouchPoint &x) {
|
||||||
|
return x.id == id;
|
||||||
|
}), touch.touch_points.end());
|
||||||
|
};
|
||||||
|
|
||||||
for (auto &hid_tp : touch_points) {
|
for (auto &hid_tp : touch_points) {
|
||||||
|
|
||||||
// check if existing
|
// check if existing
|
||||||
@@ -353,23 +468,29 @@ namespace rawinput::touch {
|
|||||||
|
|
||||||
// only remove if it exists
|
// only remove if it exists
|
||||||
if (existing != touch.touch_points.end()) {
|
if (existing != touch.touch_points.end()) {
|
||||||
|
remove_touch_point(hid_tp.id);
|
||||||
// remove all touch points with this ID
|
|
||||||
touch_removes.push_back(hid_tp.id);
|
|
||||||
touch.touch_points.erase(std::remove_if(
|
|
||||||
touch.touch_points.begin(),
|
|
||||||
touch.touch_points.end(),
|
|
||||||
[hid_tp] (const HIDTouchPoint &x) {
|
|
||||||
return x.id == hid_tp.id;
|
|
||||||
}), touch.touch_points.end());
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// write touch point
|
// start from the normalized (0.0 - 1.0) digitizer coordinates
|
||||||
|
double norm_x = hid_tp.x;
|
||||||
|
double norm_y = hid_tp.y;
|
||||||
|
|
||||||
|
// undo letterboxing so raw input touches line up with the displayed
|
||||||
|
// image; touches that land in the black bars are treated as released
|
||||||
|
if (aspect_compensation_enabled() && !remap_aspect_compensation(norm_x, norm_y)) {
|
||||||
|
if (existing != touch.touch_points.end()) {
|
||||||
|
remove_touch_point(hid_tp.id);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// scale the normalized coordinates to the monitor to obtain the
|
||||||
|
// physical screen position of the touch
|
||||||
TouchPoint tp {
|
TouchPoint tp {
|
||||||
.id = hid_tp.id,
|
.id = hid_tp.id,
|
||||||
.x = (long) (hid_tp.x * DISPLAY_SIZE_X),
|
.x = (long) (norm_x * DISPLAY_SIZE_X),
|
||||||
.y = (long) (hid_tp.y * DISPLAY_SIZE_Y),
|
.y = (long) (norm_y * DISPLAY_SIZE_Y),
|
||||||
.mouse = false,
|
.mouse = false,
|
||||||
};
|
};
|
||||||
touch_writes.push_back(tp);
|
touch_writes.push_back(tp);
|
||||||
@@ -390,13 +511,28 @@ namespace rawinput::touch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set TTL and last report time
|
// set TTL and last report time
|
||||||
if (!touch.elements_x.empty() && !touch_modifications.empty()) {
|
if (!touch.elements_x.empty()) {
|
||||||
|
|
||||||
|
// a HID digitizer reports only a fixed number of contacts per report and
|
||||||
|
// cycles the remaining ones through following reports, so a contact can be
|
||||||
|
// absent for up to this many reports before it comes back around
|
||||||
auto ttl = touch.touch_points.size() / touch.elements_x.size() + 1;
|
auto ttl = touch.touch_points.size() / touch.elements_x.size() + 1;
|
||||||
auto system_time_ms = get_system_milliseconds();
|
auto system_time_ms = get_system_milliseconds();
|
||||||
|
|
||||||
|
// extend every tracked contact to the current cycle length without ever
|
||||||
|
// shrinking it; otherwise a rising contact count (e.g. rapid taps) lengthens
|
||||||
|
// the cycle and can starve a stationary held contact into being reaped before
|
||||||
|
// it is reported again. the 50ms last-report timeout remains the hard backstop
|
||||||
|
for (auto &hid_tp : touch.touch_points) {
|
||||||
|
if (hid_tp.ttl < ttl) {
|
||||||
|
hid_tp.ttl = ttl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// only contacts actually seen this report refresh their last-report time
|
||||||
for (auto &hid_tp_id : touch_modifications) {
|
for (auto &hid_tp_id : touch_modifications) {
|
||||||
for (auto &hid_tp : touch.touch_points) {
|
for (auto &hid_tp : touch.touch_points) {
|
||||||
if (hid_tp.id == hid_tp_id) {
|
if (hid_tp.id == hid_tp_id) {
|
||||||
hid_tp.ttl = ttl;
|
|
||||||
hid_tp.last_report = system_time_ms;
|
hid_tp.last_report = system_time_ms;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -513,7 +649,13 @@ namespace rawinput::touch {
|
|||||||
GetWindowRect(GetDesktopWindow(), &display_rect);
|
GetWindowRect(GetDesktopWindow(), &display_rect);
|
||||||
DISPLAY_SIZE_X = display_rect.right - display_rect.left;
|
DISPLAY_SIZE_X = display_rect.right - display_rect.left;
|
||||||
DISPLAY_SIZE_Y = display_rect.bottom - display_rect.top;
|
DISPLAY_SIZE_Y = display_rect.bottom - display_rect.top;
|
||||||
log_info("rawinput", "display size: {}x{}", (int) DISPLAY_SIZE_X, (int) DISPLAY_SIZE_Y);
|
log_info("rawinput", "primary display size: {}x{}", (int) DISPLAY_SIZE_X, (int) DISPLAY_SIZE_Y);
|
||||||
|
|
||||||
|
// determine the native (maximum) resolution of the primary display; used to
|
||||||
|
// detect and undo letterboxing when a non-native display mode is in use
|
||||||
|
detect_native_resolution();
|
||||||
|
log_info("rawinput", "primary display native size: {}x{}",
|
||||||
|
(int) DISPLAY_NATIVE_X, (int) DISPLAY_NATIVE_Y);
|
||||||
|
|
||||||
// determine monitor orientation
|
// determine monitor orientation
|
||||||
DEVMODE display_mode{};
|
DEVMODE display_mode{};
|
||||||
|
|||||||
@@ -9,6 +9,25 @@ namespace rawinput::touch {
|
|||||||
extern bool DISABLED;
|
extern bool DISABLED;
|
||||||
extern bool INVERTED;
|
extern bool INVERTED;
|
||||||
|
|
||||||
|
// aspect-ratio compensation: when the current display mode has a different aspect
|
||||||
|
// ratio than the panel's native resolution, the image is letterboxed/pillarboxed
|
||||||
|
// on the physical panel while the digitizer still spans the whole panel. this
|
||||||
|
// remaps the touch into the displayed image region.
|
||||||
|
enum class AspectMode {
|
||||||
|
Auto, // decided per game via ASPECT_COMPENSATION_GAME
|
||||||
|
On, // always compensate
|
||||||
|
Off, // never compensate
|
||||||
|
};
|
||||||
|
|
||||||
|
// global override, driven by the -rawtouchaspect launcher option
|
||||||
|
extern AspectMode ASPECT_COMPENSATION_MODE;
|
||||||
|
|
||||||
|
// per-game request, honored only when ASPECT_COMPENSATION_MODE is Auto
|
||||||
|
extern bool ASPECT_COMPENSATION_GAME;
|
||||||
|
|
||||||
|
// effective decision combining the global mode and the per-game request
|
||||||
|
bool aspect_compensation_enabled();
|
||||||
|
|
||||||
// global state
|
// global state
|
||||||
extern DWORD DISPLAY_ORIENTATION;
|
extern DWORD DISPLAY_ORIENTATION;
|
||||||
extern long DISPLAY_SIZE_X;
|
extern long DISPLAY_SIZE_X;
|
||||||
|
|||||||
@@ -179,11 +179,27 @@ XInputSetState(
|
|||||||
|
|
||||||
XInputManager::XInputManager() {
|
XInputManager::XInputManager() {
|
||||||
log_info("xinput", "initialize...");
|
log_info("xinput", "initialize...");
|
||||||
this->xinput_lib = LoadLibraryA("xinput1_3.dll");
|
|
||||||
|
// many systems ship only a newer XInput runtime, so try the known
|
||||||
|
// DLL names in order of preference until one loads
|
||||||
|
static const char *xinput_dll_names[] = {
|
||||||
|
"xinput1_4.dll",
|
||||||
|
"xinput1_3.dll",
|
||||||
|
"xinput9_1_0.dll",
|
||||||
|
};
|
||||||
|
const char *loaded_name = nullptr;
|
||||||
|
for (const char *name : xinput_dll_names) {
|
||||||
|
this->xinput_lib = LoadLibraryA(name);
|
||||||
|
if (this->xinput_lib) {
|
||||||
|
loaded_name = name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!this->xinput_lib) {
|
if (!this->xinput_lib) {
|
||||||
log_warning("xinput", "failed to load xinput1_3.dll");
|
log_warning("xinput", "failed to load any XInput DLL");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
log_info("xinput", "loaded {}", loaded_name);
|
||||||
XInputGetState_addr = reinterpret_cast<decltype(XInputGetState) *>(
|
XInputGetState_addr = reinterpret_cast<decltype(XInputGetState) *>(
|
||||||
GetProcAddress(this->xinput_lib, "XInputGetState"));
|
GetProcAddress(this->xinput_lib, "XInputGetState"));
|
||||||
if (!XInputGetState_addr) {
|
if (!XInputGetState_addr) {
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
#include "gdi_overlay.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// the back buffer matches the window DC; the software buffer has a fixed 32-bit layout
|
||||||
|
enum class BufferType {
|
||||||
|
TargetCompatible,
|
||||||
|
Bgra32,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GdiBuffer {
|
||||||
|
HDC dc = nullptr;
|
||||||
|
HBITMAP bitmap = nullptr;
|
||||||
|
// bitmap originally selected into the memory DC, restored before cleanup
|
||||||
|
HGDIOBJ old_bitmap = nullptr;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// back buffer holds the complete frame; overlay buffer holds ImGui software pixels
|
||||||
|
GdiBuffer BACK_BUFFER;
|
||||||
|
GdiBuffer OVERLAY_BUFFER;
|
||||||
|
|
||||||
|
void release_buffer(GdiBuffer &buffer) {
|
||||||
|
// destroy the DC before the bitmap so cleanup is safe even if restoration fails
|
||||||
|
if (buffer.dc != nullptr) {
|
||||||
|
if (buffer.old_bitmap != nullptr && buffer.old_bitmap != HGDI_ERROR) {
|
||||||
|
SelectObject(buffer.dc, buffer.old_bitmap);
|
||||||
|
}
|
||||||
|
DeleteDC(buffer.dc);
|
||||||
|
}
|
||||||
|
if (buffer.bitmap != nullptr) {
|
||||||
|
DeleteObject(buffer.bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensures the buffer has a memory DC with a bitmap of the requested size and type
|
||||||
|
// selected into it. a matching allocation is reused; otherwise the old resources are
|
||||||
|
// released and recreated. returns false if the dimensions or any GDI operation fail.
|
||||||
|
bool ensure_buffer(
|
||||||
|
GdiBuffer &buffer,
|
||||||
|
HDC target_dc,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
BufferType type,
|
||||||
|
const char *name) {
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (buffer.dc != nullptr && buffer.bitmap != nullptr &&
|
||||||
|
buffer.width == width && buffer.height == height) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep allocations across frames and recreate only after a size change
|
||||||
|
release_buffer(buffer);
|
||||||
|
buffer.dc = CreateCompatibleDC(target_dc);
|
||||||
|
if (buffer.dc == nullptr) {
|
||||||
|
log_warning("touch", "failed to create {} DC: {}", name, GetLastError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// compatible bitmaps are fast presentation targets; BGRA bitmaps accept raw pixels
|
||||||
|
if (type == BufferType::TargetCompatible) {
|
||||||
|
buffer.bitmap = CreateCompatibleBitmap(target_dc, width, height);
|
||||||
|
} else {
|
||||||
|
buffer.bitmap = CreateBitmap(width, height, 1, sizeof(uint32_t) * 8, nullptr);
|
||||||
|
}
|
||||||
|
if (buffer.bitmap == nullptr) {
|
||||||
|
log_warning("touch", "failed to create {} bitmap: {}", name, GetLastError());
|
||||||
|
release_buffer(buffer);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.old_bitmap = SelectObject(buffer.dc, buffer.bitmap);
|
||||||
|
if (buffer.old_bitmap == nullptr || buffer.old_bitmap == HGDI_ERROR) {
|
||||||
|
log_warning("touch", "failed to select {} bitmap: {}", name, GetLastError());
|
||||||
|
release_buffer(buffer);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.width = width;
|
||||||
|
buffer.height = height;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool update_overlay_buffer(
|
||||||
|
HDC target_dc,
|
||||||
|
const uint32_t *pixels,
|
||||||
|
bool pixels_dirty,
|
||||||
|
int width,
|
||||||
|
int height) {
|
||||||
|
if (pixels == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool needs_update = pixels_dirty || OVERLAY_BUFFER.bitmap == nullptr ||
|
||||||
|
OVERLAY_BUFFER.width != width || OVERLAY_BUFFER.height != height;
|
||||||
|
if (!ensure_buffer(
|
||||||
|
OVERLAY_BUFFER,
|
||||||
|
target_dc,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
BufferType::Bgra32,
|
||||||
|
"software overlay")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!needs_update) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDIBits requires the destination bitmap not to be selected into a DC
|
||||||
|
HGDIOBJ overlay_bitmap =
|
||||||
|
SelectObject(OVERLAY_BUFFER.dc, OVERLAY_BUFFER.old_bitmap);
|
||||||
|
if (overlay_bitmap == nullptr || overlay_bitmap == HGDI_ERROR) {
|
||||||
|
log_warning("touch", "failed to deselect software overlay bitmap: {}", GetLastError());
|
||||||
|
release_buffer(OVERLAY_BUFFER);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
BITMAPINFO bitmap_info {};
|
||||||
|
bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||||
|
bitmap_info.bmiHeader.biWidth = width;
|
||||||
|
bitmap_info.bmiHeader.biHeight = -height;
|
||||||
|
bitmap_info.bmiHeader.biPlanes = 1;
|
||||||
|
bitmap_info.bmiHeader.biBitCount = sizeof(uint32_t) * 8;
|
||||||
|
bitmap_info.bmiHeader.biCompression = BI_RGB;
|
||||||
|
|
||||||
|
int copied_lines = SetDIBits(
|
||||||
|
target_dc,
|
||||||
|
OVERLAY_BUFFER.bitmap,
|
||||||
|
0,
|
||||||
|
height,
|
||||||
|
pixels,
|
||||||
|
&bitmap_info,
|
||||||
|
DIB_RGB_COLORS);
|
||||||
|
|
||||||
|
HGDIOBJ old_bitmap = SelectObject(OVERLAY_BUFFER.dc, OVERLAY_BUFFER.bitmap);
|
||||||
|
if (old_bitmap == nullptr || old_bitmap == HGDI_ERROR) {
|
||||||
|
log_warning("touch", "failed to reselect software overlay bitmap: {}", GetLastError());
|
||||||
|
release_buffer(OVERLAY_BUFFER);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
OVERLAY_BUFFER.old_bitmap = old_bitmap;
|
||||||
|
|
||||||
|
if (copied_lines != height) {
|
||||||
|
log_warning("touch", "failed to update software overlay bitmap: {} of {} lines copied",
|
||||||
|
copied_lines, height);
|
||||||
|
release_buffer(OVERLAY_BUFFER);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HDC touch_gdi_overlay_begin_frame(
|
||||||
|
HDC target_dc,
|
||||||
|
HBRUSH background_brush,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
const uint32_t *overlay_pixels,
|
||||||
|
bool overlay_pixels_dirty,
|
||||||
|
int overlay_width,
|
||||||
|
int overlay_height) {
|
||||||
|
if (!ensure_buffer(
|
||||||
|
BACK_BUFFER,
|
||||||
|
target_dc,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
BufferType::TargetCompatible,
|
||||||
|
"overlay back buffer")) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
HDC draw_dc = BACK_BUFFER.dc;
|
||||||
|
SetBkMode(draw_dc, TRANSPARENT);
|
||||||
|
|
||||||
|
// start each frame from the transparent color-key background
|
||||||
|
RECT buffer_rect {0, 0, width, height};
|
||||||
|
FillRect(draw_dc, &buffer_rect, background_brush);
|
||||||
|
|
||||||
|
if (update_overlay_buffer(
|
||||||
|
target_dc,
|
||||||
|
overlay_pixels,
|
||||||
|
overlay_pixels_dirty,
|
||||||
|
overlay_width,
|
||||||
|
overlay_height) &&
|
||||||
|
!BitBlt(draw_dc, 0, 0, overlay_width, overlay_height,
|
||||||
|
OVERLAY_BUFFER.dc, 0, 0, SRCCOPY)) {
|
||||||
|
log_warning("touch", "failed to draw software overlay bitmap: {}", GetLastError());
|
||||||
|
}
|
||||||
|
|
||||||
|
return draw_dc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_gdi_overlay_present(HDC target_dc) {
|
||||||
|
// one full-window blit exposes the completed frame without an intermediate erase
|
||||||
|
if (!BitBlt(target_dc, 0, 0, BACK_BUFFER.width, BACK_BUFFER.height,
|
||||||
|
BACK_BUFFER.dc, 0, 0, SRCCOPY)) {
|
||||||
|
log_warning("touch", "failed to present overlay back buffer: {}", GetLastError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch_gdi_overlay_release() {
|
||||||
|
release_buffer(BACK_BUFFER);
|
||||||
|
release_buffer(OVERLAY_BUFFER);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
// prepares a complete offscreen frame and returns its drawing DC; returns null on failure
|
||||||
|
HDC touch_gdi_overlay_begin_frame(
|
||||||
|
HDC target_dc,
|
||||||
|
HBRUSH background_brush,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
const uint32_t *overlay_pixels,
|
||||||
|
bool overlay_pixels_dirty,
|
||||||
|
int overlay_width,
|
||||||
|
int overlay_height);
|
||||||
|
|
||||||
|
// presents the frame prepared by the most recent successful begin call
|
||||||
|
void touch_gdi_overlay_present(HDC target_dc);
|
||||||
|
|
||||||
|
// releases all cached GDI resources
|
||||||
|
void touch_gdi_overlay_release();
|
||||||
+86
-52
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "external/imgui/imgui.h"
|
#include "external/imgui/imgui.h"
|
||||||
|
#include "games/jb/jb.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
@@ -19,9 +20,12 @@
|
|||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "util/time.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
|
||||||
|
#include "gdi_overlay.h"
|
||||||
#include "handler.h"
|
#include "handler.h"
|
||||||
|
#include "touch_gestures.h"
|
||||||
#include "win7.h"
|
#include "win7.h"
|
||||||
#include "win8.h"
|
#include "win8.h"
|
||||||
|
|
||||||
@@ -35,6 +39,16 @@ static const int TOUCH_EVENT_BUFFER_SIZE = 1024 * 4;
|
|||||||
static const int TOUCH_EVENT_BUFFER_THRESHOLD1 = 1024 * 2;
|
static const int TOUCH_EVENT_BUFFER_THRESHOLD1 = 1024 * 2;
|
||||||
static const int TOUCH_EVENT_BUFFER_THRESHOLD2 = 1024 * 3;
|
static const int TOUCH_EVENT_BUFFER_THRESHOLD2 = 1024 * 3;
|
||||||
|
|
||||||
|
// timer id for the overlay repaint tick
|
||||||
|
static const UINT_PTR SPICETOUCH_OVERLAY_TIMER_ID = 1;
|
||||||
|
|
||||||
|
// overlay repaint interval; the WinXP-compat build stays at 30 FPS
|
||||||
|
#if !SPICE_XP
|
||||||
|
static const int SPICETOUCH_OVERLAY_TIMER_MS = 1000 / 60;
|
||||||
|
#else
|
||||||
|
static const int SPICETOUCH_OVERLAY_TIMER_MS = 1000 / 30;
|
||||||
|
#endif // !SPICE_XP
|
||||||
|
|
||||||
// in mainline spicetools, this was false (show by default)
|
// in mainline spicetools, this was false (show by default)
|
||||||
// in spice2x, this is true (hide by default)
|
// in spice2x, this is true (hide by default)
|
||||||
bool SPICETOUCH_CARD_DISABLE = true;
|
bool SPICETOUCH_CARD_DISABLE = true;
|
||||||
@@ -247,18 +261,17 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
|
|||||||
if (!SPICETOUCH_REGISTERED_TOUCH) {
|
if (!SPICETOUCH_REGISTERED_TOUCH) {
|
||||||
SPICETOUCH_REGISTERED_TOUCH = true;
|
SPICETOUCH_REGISTERED_TOUCH = true;
|
||||||
|
|
||||||
// check if touch is available
|
// register the handler when a touch screen is present
|
||||||
if (is_touch_available("SpiceTouchWndProc")) {
|
if (is_touch_available("SpiceTouchWndProc")) {
|
||||||
|
|
||||||
// notify the handler of our window
|
|
||||||
TOUCH_HANDLER->window_register(hWnd);
|
TOUCH_HANDLER->window_register(hWnd);
|
||||||
|
}
|
||||||
|
|
||||||
// enable card unless the feature is disabled
|
// enable the card button whenever the option is set, even without a touch
|
||||||
|
// screen (mouse clicks are handled as touch input and can trigger it)
|
||||||
if (!SPICETOUCH_CARD_DISABLE) {
|
if (!SPICETOUCH_CARD_DISABLE) {
|
||||||
SPICETOUCH_CARD_ENABLED = true;
|
SPICETOUCH_CARD_ENABLED = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// update rawinput touch display resolution
|
// update rawinput touch display resolution
|
||||||
if (msg == WM_DISPLAYCHANGE) {
|
if (msg == WM_DISPLAYCHANGE) {
|
||||||
@@ -307,7 +320,10 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case WM_TIMER: {
|
case WM_TIMER: {
|
||||||
InvalidateRect(hWnd, NULL, TRUE);
|
|
||||||
|
// request a repaint; the frame is composed into an offscreen buffer, so no
|
||||||
|
// background erase is needed (bErase = FALSE avoids a transparent flash)
|
||||||
|
InvalidateRect(hWnd, NULL, FALSE);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case WM_PAINT: {
|
case WM_PAINT: {
|
||||||
@@ -339,52 +355,52 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
|
|||||||
SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE);
|
SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw overlay
|
// render the software overlay before BeginPaint so only GDI composition
|
||||||
|
// and the final blit happen while the window is being painted
|
||||||
|
int overlay_width = 0, overlay_height = 0;
|
||||||
|
uint32_t *overlay_pixels = nullptr;
|
||||||
|
bool overlay_pixels_dirty = false;
|
||||||
if (overlay_enabled) {
|
if (overlay_enabled) {
|
||||||
|
|
||||||
// update and render
|
|
||||||
overlay::OVERLAY->update();
|
overlay::OVERLAY->update();
|
||||||
overlay::OVERLAY->new_frame();
|
overlay::OVERLAY->new_frame();
|
||||||
overlay::OVERLAY->render();
|
overlay::OVERLAY->render();
|
||||||
|
|
||||||
// get pixel data
|
overlay_pixels = overlay::OVERLAY.get()->sw_get_pixel_data(
|
||||||
int width, height;
|
&overlay_width, &overlay_height);
|
||||||
uint32_t *pixel_data = overlay::OVERLAY.get()->sw_get_pixel_data(&width, &height);
|
overlay_pixels_dirty = overlay::OVERLAY->sw_pixels_dirty;
|
||||||
if (width > 0 && height > 0) {
|
}
|
||||||
|
bool overlay_active = overlay_enabled && overlay::OVERLAY->get_active();
|
||||||
|
|
||||||
// create bitmap
|
// compose the whole frame into an offscreen back buffer and present it with a
|
||||||
HBITMAP bitmap = CreateBitmap(width, height, 1, 8 * sizeof(uint32_t), pixel_data);
|
// single blit; the window never shows a half-erased (transparent) surface
|
||||||
|
// mid-paint, which is what caused the occasional flicker at higher frame rates
|
||||||
// prepare paint
|
|
||||||
PAINTSTRUCT paint {};
|
PAINTSTRUCT paint {};
|
||||||
HDC hdc = BeginPaint(hWnd, &paint);
|
HDC hdc = BeginPaint(hWnd, &paint);
|
||||||
HDC hdcMem = CreateCompatibleDC(hdc);
|
|
||||||
SetBkMode(hdc, TRANSPARENT);
|
|
||||||
|
|
||||||
/*
|
RECT bufferRect {};
|
||||||
* draw bitmap
|
GetClientRect(hWnd, &bufferRect);
|
||||||
* - this currently sets the background to black because of SRCCOPY
|
int buffer_width = bufferRect.right - bufferRect.left;
|
||||||
* - SRCPAINT will blend but colors are wrong
|
int buffer_height = bufferRect.bottom - bufferRect.top;
|
||||||
* - once this is figured out we could also try hooking WM_PAINT and
|
|
||||||
* draw directly to the game window
|
|
||||||
*/
|
|
||||||
SelectObject(hdcMem, bitmap);
|
|
||||||
BitBlt(hdc, 0, 0, width, height, hdcMem, 0, 0, SRCCOPY);
|
|
||||||
|
|
||||||
// clean up
|
HBRUSH color_key_brush =
|
||||||
DeleteObject(bitmap);
|
(HBRUSH) GetClassLongPtr(hWnd, GCLP_HBRBACKGROUND);
|
||||||
DeleteDC(hdcMem);
|
HDC draw_dc = touch_gdi_overlay_begin_frame(
|
||||||
|
hdc,
|
||||||
|
color_key_brush,
|
||||||
|
buffer_width,
|
||||||
|
buffer_height,
|
||||||
|
overlay_pixels,
|
||||||
|
overlay_pixels_dirty,
|
||||||
|
overlay_width,
|
||||||
|
overlay_height);
|
||||||
|
if (draw_dc == nullptr) {
|
||||||
EndPaint(hWnd, &paint);
|
EndPaint(hWnd, &paint);
|
||||||
}
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw card input
|
// draw the insert-card button below the jubeat debug overlay; it is
|
||||||
if (SPICETOUCH_CARD_ENABLED && (SPICETOUCH_FONT != nullptr)) {
|
// hidden while the overlay is active
|
||||||
|
if (SPICETOUCH_CARD_ENABLED && SPICETOUCH_FONT != nullptr && !overlay_active) {
|
||||||
// prepare paint
|
|
||||||
PAINTSTRUCT paint {};
|
|
||||||
HDC hdc = BeginPaint(hWnd, &paint);
|
|
||||||
SetBkMode(hdc, TRANSPARENT);
|
|
||||||
|
|
||||||
// create brushes
|
// create brushes
|
||||||
HBRUSH brushBorder = CreateSolidBrush(RGB(0, 196, 0));
|
HBRUSH brushBorder = CreateSolidBrush(RGB(0, 196, 0));
|
||||||
@@ -414,7 +430,7 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
|
|||||||
SPICETOUCH_CARD_RECT = boxRect;
|
SPICETOUCH_CARD_RECT = boxRect;
|
||||||
|
|
||||||
// draw borders
|
// draw borders
|
||||||
FillRect(hdc, &boxRect, brushBorder);
|
FillRect(draw_dc, &boxRect, brushBorder);
|
||||||
|
|
||||||
// modify box rect
|
// modify box rect
|
||||||
boxRect.left += 1;
|
boxRect.left += 1;
|
||||||
@@ -423,7 +439,7 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
|
|||||||
boxRect.bottom -= 1;
|
boxRect.bottom -= 1;
|
||||||
|
|
||||||
// fill box
|
// fill box
|
||||||
FillRect(hdc, &boxRect, brushFill);
|
FillRect(draw_dc, &boxRect, brushFill);
|
||||||
|
|
||||||
// modify box rect
|
// modify box rect
|
||||||
if (should_rotate) {
|
if (should_rotate) {
|
||||||
@@ -435,21 +451,27 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
|
|||||||
}
|
}
|
||||||
|
|
||||||
// draw text
|
// draw text
|
||||||
SelectObject(hdc, SPICETOUCH_FONT);
|
SelectObject(draw_dc, SPICETOUCH_FONT);
|
||||||
SetTextColor(hdc, RGB(0, 196, 0));
|
SetTextColor(draw_dc, RGB(0, 196, 0));
|
||||||
DrawText(hdc, INSERT_CARD_TEXT, -1, &boxRect, DT_LEFT | DT_BOTTOM | DT_NOCLIP);
|
DrawText(draw_dc, INSERT_CARD_TEXT, -1, &boxRect, DT_LEFT | DT_BOTTOM | DT_NOCLIP);
|
||||||
|
|
||||||
// delete objects
|
// delete objects
|
||||||
DeleteObject(brushFill);
|
DeleteObject(brushFill);
|
||||||
DeleteObject(brushBorder);
|
DeleteObject(brushBorder);
|
||||||
|
|
||||||
// end paint
|
|
||||||
EndPaint(hWnd, &paint);
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// call default window procedure
|
#if !SPICE_XP
|
||||||
return DefWindowProc(hWnd, msg, wParam, lParam);
|
// draw the jubeat debug overlay on top (hidden while the overlay is active)
|
||||||
|
if (overlay_enabled && !overlay_active && games::jb::touch_debug_overlay_enabled()) {
|
||||||
|
games::jb::touch_draw_debug_overlay(draw_dc);
|
||||||
|
}
|
||||||
|
#endif // !SPICE_XP
|
||||||
|
|
||||||
|
// present the composed frame in a single blit
|
||||||
|
touch_gdi_overlay_present(hdc);
|
||||||
|
|
||||||
|
EndPaint(hWnd, &paint);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
case WM_CREATE: {
|
case WM_CREATE: {
|
||||||
|
|
||||||
@@ -479,6 +501,11 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
case WM_DESTROY: {
|
case WM_DESTROY: {
|
||||||
|
touch_gdi_overlay_release();
|
||||||
|
if (SPICETOUCH_FONT != nullptr) {
|
||||||
|
DeleteObject(SPICETOUCH_FONT);
|
||||||
|
SPICETOUCH_FONT = nullptr;
|
||||||
|
}
|
||||||
PostQuitMessage(0);
|
PostQuitMessage(0);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -538,6 +565,7 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
|
|||||||
.x = GET_X_LPARAM(lParam),
|
.x = GET_X_LPARAM(lParam),
|
||||||
.y = GET_Y_LPARAM(lParam),
|
.y = GET_Y_LPARAM(lParam),
|
||||||
.mouse = true,
|
.mouse = true,
|
||||||
|
.down_ms = get_performance_milliseconds(),
|
||||||
};
|
};
|
||||||
TOUCH_POINTS.push_back(tp);
|
TOUCH_POINTS.push_back(tp);
|
||||||
|
|
||||||
@@ -751,6 +779,9 @@ void touch_create_wnd(HWND hWnd, bool overlay) {
|
|||||||
ShowWindow(touch_window, SW_SHOWNOACTIVATE);
|
ShowWindow(touch_window, SW_SHOWNOACTIVATE);
|
||||||
UpdateWindow(touch_window);
|
UpdateWindow(touch_window);
|
||||||
|
|
||||||
|
// disable the OS touch contact visualization for our own touch window
|
||||||
|
disable_touch_gestures(touch_window);
|
||||||
|
|
||||||
// register
|
// register
|
||||||
touch_register_window(touch_window);
|
touch_register_window(touch_window);
|
||||||
|
|
||||||
@@ -763,8 +794,8 @@ void touch_create_wnd(HWND hWnd, bool overlay) {
|
|||||||
// create instance
|
// create instance
|
||||||
overlay::OVERLAY.reset(new overlay::SpiceOverlay(touch_window));
|
overlay::OVERLAY.reset(new overlay::SpiceOverlay(touch_window));
|
||||||
|
|
||||||
// draw overlay in 30 FPS
|
// draw overlay repaint timer (30 FPS on WinXP, 60 FPS otherwise)
|
||||||
SetTimer(touch_window, 1, 1000 / 30, NULL);
|
SetTimer(touch_window, SPICETOUCH_OVERLAY_TIMER_ID, SPICETOUCH_OVERLAY_TIMER_MS, NULL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -848,6 +879,9 @@ void touch_write_points(std::vector<TouchPoint> *touch_points) {
|
|||||||
// create new touch point when not found
|
// create new touch point when not found
|
||||||
if (!found) {
|
if (!found) {
|
||||||
|
|
||||||
|
// stamp the landing time so debounce can measure the contact's age
|
||||||
|
tp.down_ms = get_performance_milliseconds();
|
||||||
|
|
||||||
// add touch point
|
// add touch point
|
||||||
TOUCH_POINTS.push_back(tp);
|
TOUCH_POINTS.push_back(tp);
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ struct TouchPoint {
|
|||||||
DWORD id;
|
DWORD id;
|
||||||
LONG x, y;
|
LONG x, y;
|
||||||
bool mouse;
|
bool mouse;
|
||||||
|
double down_ms = 0.0; // performance-counter milliseconds when the contact first landed
|
||||||
};
|
};
|
||||||
enum TouchEventType {
|
enum TouchEventType {
|
||||||
TOUCH_DOWN,
|
TOUCH_DOWN,
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
|
||||||
|
// set version to Windows 8 to enable Windows 8 touch functions
|
||||||
|
#define _WIN32_WINNT 0x0602
|
||||||
|
|
||||||
|
#include <propsys.h>
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include "touch_gestures.h"
|
||||||
|
#include "util/libutils.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
// tablet/pen service flags (MicrosoftTabletPenServiceProperty atom)
|
||||||
|
// these are not present in the mingw headers
|
||||||
|
#ifndef TABLET_DISABLE_PRESSANDHOLD
|
||||||
|
#define TABLET_DISABLE_PRESSANDHOLD 0x00000001
|
||||||
|
#define TABLET_DISABLE_PENTAPFEEDBACK 0x00000008
|
||||||
|
#define TABLET_DISABLE_PENBARRELFEEDBACK 0x00000010
|
||||||
|
#define TABLET_DISABLE_TOUCHUIFORCEON 0x00000100
|
||||||
|
#define TABLET_DISABLE_TOUCHUIFORCEOFF 0x00000200
|
||||||
|
#define TABLET_DISABLE_TOUCHSWITCH 0x00008000
|
||||||
|
#define TABLET_DISABLE_FLICKS 0x00010000
|
||||||
|
#define TABLET_DISABLE_SMOOTHSCROLLING 0x00080000
|
||||||
|
#define TABLET_DISABLE_FLICKFALLBACKKEYS 0x00100000
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static const char TABLET_ATOM_NAME[] = "MicrosoftTabletPenServiceProperty";
|
||||||
|
|
||||||
|
// PKEY_EdgeGesture_DisableTouchWhenFullscreen format GUID
|
||||||
|
// (not defined in the mingw headers); defined inline to avoid an initguid.h
|
||||||
|
// symbol clash with touch/win8.cpp which declares the same name
|
||||||
|
// {32CE38B2-2C9A-41B1-9BC5-B3784394AA44}
|
||||||
|
static const GUID EDGEGESTURE_DISABLE_FMT =
|
||||||
|
{ 0x32CE38B2, 0x2C9A, 0x41B1, { 0x9B, 0xC5, 0xB3, 0x78, 0x43, 0x94, 0xAA, 0x44 } };
|
||||||
|
|
||||||
|
static HINSTANCE USER32_INSTANCE = nullptr;
|
||||||
|
typedef BOOL (WINAPI *SetWindowFeedbackSetting_t)(HWND, FEEDBACK_TYPE, DWORD, UINT32, const VOID *);
|
||||||
|
static SetWindowFeedbackSetting_t pSetWindowFeedbackSetting = nullptr;
|
||||||
|
|
||||||
|
static HINSTANCE SHELL32_INSTANCE = nullptr;
|
||||||
|
typedef HRESULT (WINAPI *SHGetPropertyStoreForWindow_t)(HWND, REFIID, void **);
|
||||||
|
static SHGetPropertyStoreForWindow_t pSHGetPropertyStoreForWindow = nullptr;
|
||||||
|
|
||||||
|
static std::once_flag INIT_FLAG;
|
||||||
|
|
||||||
|
// resolve the libraries and entry points once; disable_touch_gestures may be
|
||||||
|
// called concurrently from the CreateWindowEx hooks and the touch thread
|
||||||
|
static void init_procs() {
|
||||||
|
std::call_once(INIT_FLAG, []() {
|
||||||
|
USER32_INSTANCE = libutils::load_library("user32.dll");
|
||||||
|
if (USER32_INSTANCE != nullptr) {
|
||||||
|
pSetWindowFeedbackSetting = libutils::try_proc<SetWindowFeedbackSetting_t>(
|
||||||
|
USER32_INSTANCE, "SetWindowFeedbackSetting");
|
||||||
|
}
|
||||||
|
|
||||||
|
SHELL32_INSTANCE = libutils::try_library("shell32.dll");
|
||||||
|
if (SHELL32_INSTANCE != nullptr) {
|
||||||
|
pSHGetPropertyStoreForWindow = libutils::try_proc<SHGetPropertyStoreForWindow_t>(
|
||||||
|
SHELL32_INSTANCE, "SHGetPropertyStoreForWindow");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void disable_feedback_visuals(HWND hwnd) {
|
||||||
|
|
||||||
|
if (pSetWindowFeedbackSetting == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL enabled = FALSE;
|
||||||
|
pSetWindowFeedbackSetting(
|
||||||
|
hwnd,
|
||||||
|
FEEDBACK_TOUCH_CONTACTVISUALIZATION,
|
||||||
|
0, sizeof(enabled), &enabled);
|
||||||
|
pSetWindowFeedbackSetting(
|
||||||
|
hwnd,
|
||||||
|
FEEDBACK_TOUCH_TAP,
|
||||||
|
0, sizeof(enabled), &enabled);
|
||||||
|
pSetWindowFeedbackSetting(
|
||||||
|
hwnd,
|
||||||
|
FEEDBACK_TOUCH_DOUBLETAP,
|
||||||
|
0, sizeof(enabled), &enabled);
|
||||||
|
pSetWindowFeedbackSetting(
|
||||||
|
hwnd,
|
||||||
|
FEEDBACK_TOUCH_PRESSANDHOLD,
|
||||||
|
0, sizeof(enabled), &enabled);
|
||||||
|
pSetWindowFeedbackSetting(
|
||||||
|
hwnd,
|
||||||
|
FEEDBACK_TOUCH_RIGHTTAP,
|
||||||
|
0, sizeof(enabled), &enabled);
|
||||||
|
pSetWindowFeedbackSetting(
|
||||||
|
hwnd,
|
||||||
|
FEEDBACK_GESTURE_PRESSANDTAP,
|
||||||
|
0, sizeof(enabled), &enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void disable_gesture_behaviors(HWND hwnd) {
|
||||||
|
|
||||||
|
// the tablet/pen service reads this window property to decide which
|
||||||
|
// touch/pen gestures to suppress for the window. this covers:
|
||||||
|
// - press-and-hold (touch right-click / long-press ring)
|
||||||
|
// - pen tap/barrel feedback
|
||||||
|
// - flicks (edge/directional flick navigation gestures)
|
||||||
|
// - the touch keyboard invocation UI
|
||||||
|
// - smooth scrolling / flick fallback keys
|
||||||
|
DWORD tablet_flags = TABLET_DISABLE_PRESSANDHOLD |
|
||||||
|
TABLET_DISABLE_PENTAPFEEDBACK |
|
||||||
|
TABLET_DISABLE_PENBARRELFEEDBACK |
|
||||||
|
TABLET_DISABLE_FLICKS |
|
||||||
|
TABLET_DISABLE_TOUCHUIFORCEOFF |
|
||||||
|
TABLET_DISABLE_TOUCHSWITCH |
|
||||||
|
TABLET_DISABLE_SMOOTHSCROLLING |
|
||||||
|
TABLET_DISABLE_FLICKFALLBACKKEYS;
|
||||||
|
|
||||||
|
ATOM atom_id = GlobalAddAtomA(TABLET_ATOM_NAME);
|
||||||
|
if (atom_id > 0) {
|
||||||
|
SetPropA(hwnd, TABLET_ATOM_NAME, (HANDLE) ((ULONG_PTR) tablet_flags));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void disable_edge_gestures(HWND hwnd) {
|
||||||
|
|
||||||
|
// suppress the touch edge swipes (charms/back/app bar) for the window while
|
||||||
|
// it is fullscreen. this is normally done by the touch handler's
|
||||||
|
// window_register(), but that is not called for every handler (e.g. the
|
||||||
|
// rawinput handler is a no-op), so apply it here as well.
|
||||||
|
if (pSHGetPropertyStoreForWindow == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IPropertyStore *ps = nullptr;
|
||||||
|
HRESULT hr = pSHGetPropertyStoreForWindow(hwnd, IID_IPropertyStore, (void **) &ps);
|
||||||
|
if (SUCCEEDED(hr) && ps != nullptr) {
|
||||||
|
PROPERTYKEY key = { EDGEGESTURE_DISABLE_FMT, 2 };
|
||||||
|
|
||||||
|
PROPVARIANT var {};
|
||||||
|
var.vt = VT_BOOL;
|
||||||
|
var.boolVal = VARIANT_TRUE;
|
||||||
|
|
||||||
|
hr = ps->SetValue(key, var);
|
||||||
|
ps->Release();
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
log_warning("touch_gestures",
|
||||||
|
"failed to disable edge gestures on HWND={}", fmt::ptr(hwnd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void disable_touch_gestures(HWND hwnd) {
|
||||||
|
|
||||||
|
init_procs();
|
||||||
|
if (USER32_INSTANCE == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_misc("touch_gestures",
|
||||||
|
"disable visual feedback and gestures for touch events for HWND={}", fmt::ptr(hwnd));
|
||||||
|
|
||||||
|
// disable the visual feedback (contact circles, tap/double-tap stars, etc.)
|
||||||
|
disable_feedback_visuals(hwnd);
|
||||||
|
|
||||||
|
// disable the actual gesture behaviors (press-and-hold, flicks, etc.)
|
||||||
|
disable_gesture_behaviors(hwnd);
|
||||||
|
|
||||||
|
// disable the fullscreen touch edge swipes
|
||||||
|
disable_edge_gestures(hwnd);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
// disable the OS touch UX for a window: visual feedback (contact circles,
|
||||||
|
// tap/press-and-hold indicators) and gesture behaviors (press-and-hold
|
||||||
|
// right-click, flicks, etc.)
|
||||||
|
void disable_touch_gestures(HWND hwnd);
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
|
|
||||||
// set version to Windows 8 to enable Windows 8 touch functions
|
|
||||||
#define _WIN32_WINNT 0x0602
|
|
||||||
|
|
||||||
#include "touch_indicators.h"
|
|
||||||
#include "util/libutils.h"
|
|
||||||
#include "util/logging.h"
|
|
||||||
|
|
||||||
static HINSTANCE USER32_INSTANCE = nullptr;
|
|
||||||
typedef BOOL (WINAPI *SetWindowFeedbackSetting_t)(HWND, FEEDBACK_TYPE, DWORD, UINT32, const VOID *);
|
|
||||||
static SetWindowFeedbackSetting_t pSetWindowFeedbackSetting = nullptr;
|
|
||||||
|
|
||||||
void disable_touch_indicators(HWND hwnd) {
|
|
||||||
|
|
||||||
if (USER32_INSTANCE == nullptr) {
|
|
||||||
USER32_INSTANCE = libutils::load_library("user32.dll");
|
|
||||||
}
|
|
||||||
if (USER32_INSTANCE == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pSetWindowFeedbackSetting == nullptr) {
|
|
||||||
pSetWindowFeedbackSetting = libutils::try_proc<SetWindowFeedbackSetting_t>(
|
|
||||||
USER32_INSTANCE, "SetWindowFeedbackSetting");
|
|
||||||
}
|
|
||||||
if (pSetWindowFeedbackSetting == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log_info("touch_indicators", "disable visual feedback for touch events for HWND={}", fmt::ptr(hwnd));
|
|
||||||
|
|
||||||
BOOL enabled = FALSE;
|
|
||||||
pSetWindowFeedbackSetting(
|
|
||||||
hwnd,
|
|
||||||
FEEDBACK_TOUCH_CONTACTVISUALIZATION,
|
|
||||||
0, sizeof(enabled), &enabled);
|
|
||||||
pSetWindowFeedbackSetting(
|
|
||||||
hwnd,
|
|
||||||
FEEDBACK_TOUCH_TAP,
|
|
||||||
0, sizeof(enabled), &enabled);
|
|
||||||
pSetWindowFeedbackSetting(
|
|
||||||
hwnd,
|
|
||||||
FEEDBACK_TOUCH_DOUBLETAP,
|
|
||||||
0, sizeof(enabled), &enabled);
|
|
||||||
pSetWindowFeedbackSetting(
|
|
||||||
hwnd,
|
|
||||||
FEEDBACK_TOUCH_PRESSANDHOLD,
|
|
||||||
0, sizeof(enabled), &enabled);
|
|
||||||
pSetWindowFeedbackSetting(
|
|
||||||
hwnd,
|
|
||||||
FEEDBACK_TOUCH_RIGHTTAP,
|
|
||||||
0, sizeof(enabled), &enabled);
|
|
||||||
pSetWindowFeedbackSetting(
|
|
||||||
hwnd,
|
|
||||||
FEEDBACK_GESTURE_PRESSANDTAP,
|
|
||||||
0, sizeof(enabled), &enabled);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <windows.h>
|
|
||||||
|
|
||||||
void disable_touch_indicators(HWND hwnd);
|
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "util/time.h"
|
||||||
#include "rawinput/touch.h"
|
#include "rawinput/touch.h"
|
||||||
|
|
||||||
// mingw issue #2205 workaround
|
// mingw issue #2205 workaround
|
||||||
@@ -177,6 +178,7 @@ void Win7Handler::handle_message(msg_handler_result &result, HWND hWnd, UINT msg
|
|||||||
.x = point.x,
|
.x = point.x,
|
||||||
.y = point.y,
|
.y = point.y,
|
||||||
.mouse = false,
|
.mouse = false,
|
||||||
|
.down_ms = get_performance_milliseconds(),
|
||||||
};
|
};
|
||||||
TOUCH_POINTS.push_back(tp);
|
TOUCH_POINTS.push_back(tp);
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "util/time.h"
|
||||||
#include "rawinput/touch.h"
|
#include "rawinput/touch.h"
|
||||||
|
|
||||||
// mingw does not seem to have this either
|
// mingw does not seem to have this either
|
||||||
@@ -114,10 +115,16 @@ bool Win8Handler::is_available() {
|
|||||||
bool Win8Handler::window_register(HWND hWnd) {
|
bool Win8Handler::window_register(HWND hWnd) {
|
||||||
|
|
||||||
// atom settings
|
// atom settings
|
||||||
|
// keep this in sync with touch/touch_gestures.cpp so registering a touch
|
||||||
|
// window does not downgrade the flag set applied there
|
||||||
DWORD dwHwndTabletProperty = TABLET_DISABLE_PRESSANDHOLD |
|
DWORD dwHwndTabletProperty = TABLET_DISABLE_PRESSANDHOLD |
|
||||||
TABLET_DISABLE_PENTAPFEEDBACK |
|
TABLET_DISABLE_PENTAPFEEDBACK |
|
||||||
TABLET_DISABLE_PENBARRELFEEDBACK |
|
TABLET_DISABLE_PENBARRELFEEDBACK |
|
||||||
TABLET_DISABLE_FLICKS;
|
TABLET_DISABLE_FLICKS |
|
||||||
|
TABLET_DISABLE_TOUCHUIFORCEOFF |
|
||||||
|
TABLET_DISABLE_TOUCHSWITCH |
|
||||||
|
TABLET_DISABLE_SMOOTHSCROLLING |
|
||||||
|
TABLET_DISABLE_FLICKFALLBACKKEYS;
|
||||||
|
|
||||||
// get atom ID
|
// get atom ID
|
||||||
ATOM atomID = GlobalAddAtom(ATOM_NAME);
|
ATOM atomID = GlobalAddAtom(ATOM_NAME);
|
||||||
@@ -214,6 +221,7 @@ void Win8Handler::handle_message(msg_handler_result &result, HWND hWnd, UINT msg
|
|||||||
.x = point.x,
|
.x = point.x,
|
||||||
.y = point.y,
|
.y = point.y,
|
||||||
.mouse = false,
|
.mouse = false,
|
||||||
|
.down_ms = get_performance_milliseconds(),
|
||||||
};
|
};
|
||||||
TOUCH_POINTS.push_back(tp);
|
TOUCH_POINTS.push_back(tp);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# --- configuration ----------------------------------------------------------
|
||||||
|
$scriptDir = if ($env:SPICE_DIR) { $env:SPICE_DIR } elseif ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
|
||||||
|
$repo = 'spice2x/spice2x.github.io'
|
||||||
|
$targets = @('spice.exe', 'spice64.exe', 'spicecfg.exe')
|
||||||
|
$beta = ($env:SPICE_CHANNEL -match 'beta')
|
||||||
|
$rc = 0
|
||||||
|
|
||||||
|
$title = if ($beta) { '=== spice2x updater (beta channel) ===' } else { '=== spice2x updater ===' }
|
||||||
|
Write-Host "`n$title"
|
||||||
|
Write-Host "Target folder: $scriptDir`n"
|
||||||
|
|
||||||
|
try {
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
|
$headers = @{ 'User-Agent' = 'spice2x-updater'; 'Accept' = 'application/vnd.github+json' }
|
||||||
|
|
||||||
|
# --- find the newest release (beta = include pre-releases) --------------
|
||||||
|
Write-Host 'Querying latest release...'
|
||||||
|
$url = if ($beta) { "https://api.github.com/repos/$repo/releases?per_page=1" } else { "https://api.github.com/repos/$repo/releases/latest" }
|
||||||
|
$rel = Invoke-RestMethod -Headers $headers -Uri $url | Select-Object -First 1
|
||||||
|
if (-not $rel) { throw 'No releases found.' }
|
||||||
|
|
||||||
|
# --- pick the distribution zip (spice2x-<date>.zip, not the -full one) --
|
||||||
|
$asset = $rel.assets | Where-Object { $_.name -like 'spice2x-*.zip' -and $_.name -notlike '*-full.zip' } | Select-Object -First 1
|
||||||
|
if (-not $asset) { throw 'No .zip asset found in the release.' }
|
||||||
|
Write-Host "Latest release: $($rel.tag_name)$(if ($rel.prerelease) { ' [pre-release]' }) (asset: $($asset.name))"
|
||||||
|
|
||||||
|
# --- download the zip into memory ---------------------------------------
|
||||||
|
# note: on Windows PowerShell 5.1 .Content is a String (empty for binary
|
||||||
|
# responses), so read the raw byte stream instead
|
||||||
|
Write-Host 'Downloading...'
|
||||||
|
$bytes = (Invoke-WebRequest -Headers $headers -Uri $asset.browser_download_url -UseBasicParsing).RawContentStream.ToArray()
|
||||||
|
|
||||||
|
# --- extract just the three executables straight into this folder -------
|
||||||
|
# PS 5.1 needs these assemblies loaded; PS 7 already has the types (and the
|
||||||
|
# FileSystem assembly name no longer resolves there), so only load if missing
|
||||||
|
if (-not ('System.IO.Compression.ZipFile' -as [type])) {
|
||||||
|
Add-Type -AssemblyName System.IO.Compression, System.IO.Compression.FileSystem
|
||||||
|
}
|
||||||
|
$zip = [IO.Compression.ZipArchive]::new([IO.MemoryStream]::new([byte[]]$bytes))
|
||||||
|
try {
|
||||||
|
$updated = 0
|
||||||
|
foreach ($name in $targets) {
|
||||||
|
$entry = $zip.Entries | Where-Object { $_.Name -eq $name } | Select-Object -First 1
|
||||||
|
if (-not $entry) { Write-Warning " $name not found in the archive"; continue }
|
||||||
|
try {
|
||||||
|
[IO.Compression.ZipFileExtensions]::ExtractToFile($entry, (Join-Path $scriptDir $name), $true)
|
||||||
|
Write-Host " updated $name"; $updated++
|
||||||
|
} catch {
|
||||||
|
Write-Warning " FAILED to write $name (running / read-only?): $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally { $zip.Dispose() }
|
||||||
|
|
||||||
|
Write-Host "`nDone. $updated of $($targets.Count) executables updated to $($rel.tag_name)."
|
||||||
|
Write-Host "Only the .exe files are updated; if you copied any DLL stubs, they were not changed."
|
||||||
|
if ($updated -ne $targets.Count) { $rc = 1 }
|
||||||
|
} catch {
|
||||||
|
Write-Host ''; Write-Error $_.Exception.Message; $rc = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- result ------------------------------------------------------------------
|
||||||
|
Write-Host $(if ($rc) { "`nUpdate FAILED. See the error above." } else { "`nUpdate finished successfully." })
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
exit $rc
|
||||||
Reference in New Issue
Block a user