Compare commits

...

31 Commits

Author SHA1 Message Date
bicarus-dev 2aae8ad9ca move updater scripts 2026-07-15 02:14:19 -07:00
bicarus 1b6d1af951 jb, shogikai: double buffer overlay, run at ~60fps (#805)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
GDI bit blit overlay used exclusively for jubeat & shogikai was running
at 30fps, which caused the Jubeat touch debug UI to not render fast
enough. Run it at 60 fps, and also perform double buffering to avoid
flickering.

This could have been scoped to JB but I made the change for both. XP
builds will keep running at 30fps since those people run on cabs with
ancient hardware.

## Testing
Tested jubeat and shogikai, overlay renders without flickering at 60 fps
2026-07-15 01:31:29 -07:00
bicarus 5c3617c05f rawinput: fix ttl logic in raw touch (#806)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Fixes held touch contacts briefly dropping under rapid multi-touch (e.g.
holding several jubeat buttons and tapping elsewhere many times would
make the holds briefly release and the hold again).

A HID digitizer reports a fixed number of contact slots per report and
cycles the rest through later reports, so the touch layer keeps an
unreported contact alive for a few reports (a per-contact TTL). The TTL
was only set for contacts in the current report and had no slack, so
when the contact count rose (from the rapid taps) the reporting cycle
got longer and stationary holds were expired before the device cycled
back to them.

Now, the cycle length is recomputed every report and every tracked
contact's TTL is extended to it (never shrunk), so a rising contact
count can't starve a currently held contact. The 50 ms last-report
timeout still removes genuinely lifted contacts.

## Testing
Tested on jb, rb.
2026-07-14 23:46:17 -07:00
bicarus a9a943fbfe jb: fix card not detected on title screen (#804)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Fix a long standing issue with card reader behavior in jubeat.

sciunit emulation has a bug - when on the title screen / demo loop,
inserting a card did not generate an interrupt. Users always had to
press the game start button and card in. Fix this by reversing what the
actual sciunit does and mirroring the state machine behavior.

## Testing
Tested L44 and K44.
2026-07-14 17:06:00 -07:00
bicarus-dev b094a9e352 deprecate insert card button 2026-07-14 02:49:46 -07:00
bicarus 4bd18afb12 rb: fix alignment of vertical lines in debug view (#803)
they were off by third of a column
2026-07-14 00:18:55 -07:00
bicarus 0b24e703a1 overlay: re-enable keyboard navigation (#802)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #800 

## Description of change
Restores keyboard navigation by default in spicecfg, and add an option
to optionally enable it in the overlay.

## Testing
2026-07-14 00:18:21 -07:00
bicarus 1a1be7a393 rb: touch debug overlay, tweak scaling (#801)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Similar to #797, draw an overlay to debug touch issues for reflec beat.

For rb, we draw on the ImGui surface. Draw horizontal and vertical line
for triggered sensors, and also a bounding box if scaling is in use.

Make small tweaks to how scaling works.

## Testing
Verified volzza 2 / reflesia / 1st game (landscape KBR)
2026-07-13 23:30:41 -07:00
bicarus a2d8f60d12 jb: only show debug overlay for jubeat (#799) 2026-07-13 19:42:27 -07:00
bicarus 5d25fdb035 jb: touch debounce (#798)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Debouce option for touch input. Applies to all touch handlers (rawinput,
win8, win7) since it sits at the touch layer, and then consumed by jb
touch logic.

## Testing
2026-07-13 02:06:54 -07:00
bicarus c255e3a1db jb: option to draw debug graphics to show touch interactions (#797)
## Link to GitHub Issue or related Pull Request, if one exists
related to #697 

## Description of change
Add a new option that paints touch targets and touch points for jubeat;
gets enabled if there is a touch screen connected. This is to help
explain / debug issues when users have their touch screen issues, which
is usually due to misalignment.

Fix the touch targets being off by 1 pixel (center gap is 1px wider in
both directions).

Also, simplify the `improved` algorithm to simulate a finger and trigger
nearest-neighbor for the AC dimensions. In terms of touch performance
this is near identical to what we had before, but it allows us to draw a
nice debug overlay instead of just grids.

## Testing
Tested portrait and landscape versions of jb.
2026-07-13 00:31:05 -07:00
bicarus d7c144646f rawinput: deal with high poll rate devices, use 3x3 for default for rb touch emu (#796)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Two changes:

1. Handle all HID events in `WM_INPUT` and not just the first one. This
only really affects 4000Hz / 8000Hz HID devices.
2. Go back to `3x3` as the default for rb IR touch emulation and remove
`1x1` as an option as it does not perform as well as I expected.

## Testing
2026-07-12 10:32:29 -07:00
bicarus-dev 27077aa21f bump action-gh-release version 2026-07-12 01:45:16 -07:00
bicarus-dev 632dcf7b41 clean up option text for consistency 2026-07-12 00:00:31 -07:00
bicarus a25bbdc9c9 rawinput: refactor midi (#795)
pure movement of code only, no functional changes
2026-07-11 23:59:34 -07:00
bicarus 47d886306e rawinput: handle midi scanning asynchronously, fix midi hotplug (#793)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #603

## Description of change
Scanning for midi devices can take a while on Windows 11 with MIDI 2.0
service. Sometimes it'll take a couple seconds. Sometimes, on a PC with
zero MIDI devices, it takes 10-11 seconds.

This was causing two issues:

1. slow startup time
2. crash on invalid memory access due to a race condition

Address both.

Also fix a bug: when a device change event fires, we do a MIDI scan, and
invalidated all existing MIDI devices in favor of creating new handles.
Stop doing this, and instead check for duplicates by matching the ID and
keep existing device handles alone. Properly clean up devices on unplug.

Also: fix MIDI buttons being stuck on when unplugged while holding a
key.

## Testing
Tested with Nostroller in MIDI mode. rtpMIDI works too.
2026-07-11 21:57:55 -07:00
bicarus-dev edda658fa4 update rawtouchaspect text 2026-07-11 21:16:34 -07:00
bicarus-dev 1ab20f1b56 update wintouch option text 2026-07-11 21:06:29 -07:00
bicarus 0e9093a74b xinput: allow for newer xinput DLL (#794)
Fixes #792
2026-07-11 03:04:49 -07:00
bicarus f52eaea76c touch: fix indicators showing in touch overlay, disable additional gestures (#791) 2026-07-11 00:24:36 -07:00
bicarus c3fcf2f417 touch: rawinput touch to compensate for letterboxing (#790)
## Link to GitHub Issue or related Pull Request, if one exists

Fixes #697

## Description of change

Adds aspect-ratio compensation for raw input touch handler.

This addresses the problem with playing rawinput touch games - namely
jubeat and reflec beat - on a display that is not 16:9, and user chose
to preserve the aspect ratio. This happens a lot on:

* 16:10 touch screens
* touch screen laptops (which range from 3:2, 16:10, and so on).

Root cause is a **Windows Raw Input limitation**. Unlike
`WM_POINTER`/`WM_TOUCH`, where the OS already maps digitizer coordinates
to screen space (applying the display's calibration/scaling), Raw Input
delivers the HID digitizer's *raw* logical coordinates, spanning the
whole physical panel, with **no calibration or display-mapping data
attached**. We have to reconstruct that mapping ourselves.

When a game runs at a display mode whose aspect ratio differs from the
panel's native resolution, Windows scales the image with
letterboxing/pillarboxing (black bars). That is standard OS/GPU
behavior. Because Raw Input gives us no mapping to account for those
bars, touches land off-target. This change detects the mismatch and
remaps each touch from panel-space into the displayed image region;
touches inside the black bars are treated as released.

Adds new launcher option **`-rawtouchaspect`**.

Notes:

- Native resolution is the largest-area mode from `EnumDisplaySettings`
on the **primary** display, consistent with the existing primary-only
touch pipeline.
- Math assumes centered, aspect-preserving scaling; only one axis is
ever inset.
- A small epsilon (0.01) makes near-matched panels (e.g. 1366×768 vs
true 16:9) a no-op.
- If the user forces a stretched image, then the touches will be off
again. In this case, the user needs to use the `force off` setting.

## Testing

On my tiny Surface Go tablet, 
* checked that jubeat squares align correctly
* good enough to full combo level 11 for reflec beat
2026-07-10 23:23:45 -07:00
bicarus 8d04eba21d build: check for win7 compat, fix 32-bit xp check (#789)
## Link to GitHub Issue or related Pull Request, if one exists
#787 

## Description of change
Updates usage of `windows_dll_compat_checker`

## Testing
it builds
2026-07-10 01:51:04 -07:00
bicarus 8d42c0ce77 asio: fix XP build; remove call to RegGetValueA (#788)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #787 

## Description of change
Remove the call to `RegGetValueA` as it is Vista and up. Replace with
`RegOpenKeyExA` + `RegQueryValueExA`

## Testing
2026-07-10 00:26:03 -07:00
bicarus-dev 829503568e fix UI text for -sdvxnolive2d 2026-07-06 01:27:18 -07:00
bicarus 746fdb5409 build: manual update scripts (#786)
people asked for this often enough
2026-07-02 23:48:52 -07:00
bicarus 67da892fe9 cfg: aliases for -network and -subnet (#785)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
`-network` and `-subnet` are poorly named. Add new names for them that
are more explicit.

Old command line parameters will continue to work, this is an additive
change.

## Testing
2026-07-01 02:41:14 -07:00
bicarus dc191487e0 Update CONTRIBUTING.md 2026-06-30 17:45:23 -07:00
bicarus 5c0c405800 Update CONTRIBUTING.md 2026-06-30 14:32:30 -07:00
bicarus-dev 60166e6ba1 update gh actions 2026-06-30 02:29:44 -07:00
bicarus cb0b59600a build: GitHub action for creating a draft relase (#783) 2026-06-30 00:53:08 -07:00
bicarus b9f3be4df2 asio: cache result from CoCreateInstance to work around buggy asio drivers (#781)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
IIDX likes to call `CoCreateInstance` and `init` multiple times on ASIO
drivers, but some ASIO drivers like `Neva Uno` really don't like that
and ends up crashing. In our wrapper, cache the instances and try to
reuse them for better compatibility.

## Testing
Worked fine for FlexASIO / Xonar AE / Realtek ASIO though none of these
drivers repro the crashing behavior.
2026-06-28 22:37:48 -07:00
48 changed files with 3103 additions and 1258 deletions
+3 -3
View File
@@ -12,12 +12,12 @@ jobs:
run:
working-directory: ./src/spice2x
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set ccache environment variables
run: |
echo "CCACHE_DIR=${{ github.workspace }}/src/spice2x/.ccache" >> $GITHUB_ENV
- name: Install ccache
uses: hendrikmuhs/ccache-action@v1.2
uses: hendrikmuhs/ccache-action@v1.2.23
- name: Calculate commit SHA
id: vars
run: |
@@ -25,7 +25,7 @@ jobs:
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
- name: Compile
run: ./build_docker.sh
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v6
with:
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
path: src/spice2x/bin
+49
View File
@@ -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
View File
@@ -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 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
* Including (bundling), distributing, or linking to game data, or making it easier to find/download game data
* Performing decryption of encrypted files.
* 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)
* 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.
* 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
+5 -1
View File
@@ -412,6 +412,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/sdvx/io.cpp
games/sdvx/camera.cpp
games/jb/jb.cpp
games/jb/jb_touch.cpp
games/jb/io.cpp
games/nost/nost.cpp
games/nost/io.cpp
@@ -431,6 +432,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/rb/rb.cpp
games/rb/io.cpp
games/rb/touch.cpp
games/rb/touch_debug.cpp
games/bs/bs.cpp
games/bs/io.cpp
games/rf3d/rf3d.cpp
@@ -618,6 +620,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
# rawinput
rawinput/rawinput.cpp
rawinput/midi.cpp
rawinput/sextet.cpp
rawinput/piuio.cpp
rawinput/touch.cpp
@@ -644,8 +647,9 @@ set(SOURCE_FILES ${SOURCE_FILES}
stubs/stubs.cpp
# touch
touch/gdi_overlay.cpp
touch/touch.cpp
touch/touch_indicators.cpp
touch/touch_gestures.cpp
touch/win7.cpp
touch/win8.cpp
+18 -1
View File
@@ -214,6 +214,19 @@ time (
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))
then
echo ""
@@ -229,7 +242,7 @@ then
fi
if ((BUILD_XP_32 > 0))
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/32/spice.exe
fi
@@ -294,6 +307,7 @@ mkdir -p ${OUTDIR_EXTRAS}/largeaddressaware
mkdir -p ${OUTDIR_EXTRAS}/linux
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/32
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/64
mkdir -p ${OUTDIR_EXTRAS}/updater
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
then
mkdir -p ${OUTDIR_EXTRAS}/winxp
@@ -354,6 +368,9 @@ mkdir ${OUTDIR_EXTRAS}/api
find ./api/resources/python -name "__pycache__" -exec rm -rf {} +
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
if ((DIST_ENABLE > 0))
then
+57
View File
@@ -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"
+6
View File
@@ -411,6 +411,12 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
BUTTON_PRESSED : BUTTON_NOT_PRESSED;
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:
break;
}
+5 -210
View File
@@ -3,212 +3,24 @@
#include <windows.h>
#include <filesystem>
#include "avs/game.h"
#include "cfg/configurator.h"
#include "hooks/graphics/graphics.h"
#include "touch/touch.h"
#include "util/logging.h"
#include "util/utils.h"
#include "util/detour.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 {
// touch stuff
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
*/
// fixes "IP ADDR CHANGE" errors with unusual network setups (e.g. a VPN)
static BOOL __stdcall network_addr_is_changed() {
return 0;
}
/*
* to fix lag spikes when game tries to ping "eamuse.konami.fun" every few minutes
*/
// fixes lag spikes from the periodic ping to "eamuse.konami.fun"
static BOOL __stdcall network_get_network_check_info() {
return 0;
}
/*
* to fix network error on non DHCP interfaces
*/
// fixes network errors on non-DHCP interfaces
static BOOL __cdecl network_get_dhcp_result() {
return 1;
}
@@ -248,23 +60,7 @@ namespace games::jb {
void JBGame::attach() {
Game::attach();
// enable touch
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;
}
touch_attach();
// enable debug logging of gftools
HMODULE gftools = libutils::try_module("gftools.dll");
@@ -284,7 +80,6 @@ namespace games::jb {
void JBGame::detach() {
Game::detach();
// disable touch
TOUCH_ENABLE = false;
touch_detach();
}
}
+1 -11
View File
@@ -1,20 +1,10 @@
#pragma once
#include "games/game.h"
#include "games/jb/jb_touch.h"
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 {
public:
JBGame();
+470
View File
@@ -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);
}
}
+49
View File
@@ -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);
}
+2 -2
View File
@@ -41,7 +41,7 @@ void games::rb::RBGame::attach() {
Game::attach();
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
@@ -60,7 +60,7 @@ void games::rb::RBGame::attach() {
log_info("rb", "force increasing touch poll rate by hooking SleepEx ({}Hz)", TOUCH_POLL_RATE);
SleepEx_orig = detour::iat_try("SleepEx", SleepEx_hook, avs::game::DLL_INSTANCE);
} else {
log_info("rb", "not changing touch poll rate (120Hz by default)");
log_info("rb", "not changing touch poll rate (~125Hz by default)");
}
}
-1
View File
@@ -7,7 +7,6 @@
namespace games::rb {
extern uint16_t TOUCH_SCALING;
extern uint8_t TOUCH_SIZE;
extern uint16_t TOUCH_POLL_RATE;
class RBGame : public games::Game {
+80 -51
View File
@@ -4,7 +4,10 @@
#include "avs/game.h"
#include "hooks/graphics/graphics.h"
#include "rawinput/touch.h"
#include "touch/touch.h"
#include "touch_debug.h"
#include "touch_defs.h"
#include "util/logging.h"
#include "util/time.h"
#include "util/utils.h"
@@ -12,28 +15,38 @@
static std::string WINDOW_TITLE = "REFLEC BEAT";
namespace games::rb {
uint16_t TOUCH_SCALING = 1000;
uint8_t TOUCH_SIZE = 1;
static constexpr double TOUCH_POINT_OFFSET_DIVISOR = 3.0;
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) {
this->log_fps = log_fps;
games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) : log_fps(log_fps) {
}
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.
// for whatever reason, the last y row (#75) results in weird input few rows above; just drop it
int grid_x = CLAMP((cursor_x * 48) / window_width, 0, 47);
int grid_y = CLAMP((cursor_y * 76) / window_height, 0, 74);
// read() contracts X uniformly so screen edges land within usable sensors 2..45
int grid_x = CLAMP(
(cursor_x * X_SENSOR_COUNT) / window_width,
0,
X_SENSOR_COUNT - 1);
int grid_y = CLAMP(
(cursor_y * Y_SENSOR_COUNT) / window_height,
0,
Y_SENSOR_COUNT - 1);
// get bit positions
int bit_x = 88 + grid_x;
int bit_y = 74 - grid_y;
int bit_x = X_SENSOR_FIRST_BIT + grid_x;
int bit_y = Y_SENSOR_FIRST_BIT - grid_y;
// insert bits
data[3 + bit_x / 8] |= (char) 1 << (bit_x % 8);
data[3 + bit_y / 8] |= (char) 1 << (bit_y % 8);
packet_set_bit(data, bit_x);
packet_set_bit(data, bit_y);
}
bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
@@ -49,6 +62,9 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
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
SetWindowLongPtr(wnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(DefWindowProc));
@@ -68,18 +84,19 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
// update window
SetWindowPos(wnd, nullptr, 0, 0, 0, 0,
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER);
}
// create touch window
touch_create_wnd(wnd);
// create touch window
touch_create_wnd(wnd);
} else {
// create touch window
touch_create_wnd(wnd);
// show game window because it lost focus
// show fullscreen game window because it lost focus
if (!GRAPHICS_WINDOWED) {
ShowWindow(wnd, SW_SHOW);
}
// request automatic aspect ratio fixes
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
} else {
// fallback to dx hook
@@ -91,18 +108,21 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
ShowCursor(TRUE);
}
touch_debug_attach();
return true;
}
int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
// check buffer size
if (nNumberOfBytesToRead < 20) {
if (nNumberOfBytesToRead < TOUCH_PACKET_SIZE) {
return 0;
}
// get window
HWND window = GetForegroundWindow();
// size against the game window cached in open() (not GetForegroundWindow), so
// touch stays correctly scaled and keeps working when the game loses focus
HWND window = this->game_hwnd;
if (window == nullptr) {
return 0;
}
@@ -122,17 +142,17 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
}
// create data
unsigned char data[20] {};
unsigned char data[TOUCH_PACKET_SIZE] {};
// data header
data[0] = 0x55;
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
auto offset_x = (int) (window_width / 48.0 / 3.0);
auto offset_y = (int) (window_height / 76.0 / 3.0);
int offset_x = (int) (window_width / (double) X_SENSOR_COUNT / TOUCH_POINT_OFFSET_DIVISOR);
int offset_y = (int) (window_height / (double) Y_SENSOR_COUNT / TOUCH_POINT_OFFSET_DIVISOR);
std::vector<TouchPoint> touch_points;
touch_get_points(touch_points);
for (auto &point : touch_points) {
@@ -148,37 +168,44 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
}
// apply scaling
x = x - (window_width * (1.f - SCALE_FACTOR) / 2);
x = x / SCALE_FACTOR;
y = y - (window_height * (1.f - SCALE_FACTOR) / 2);
y = y / SCALE_FACTOR;
x = x - (window_width * (1.f - scale_factor) / 2);
x = x / scale_factor;
y = y - (window_height * (1.f - scale_factor) / 2);
y = y / scale_factor;
if (x < 0 || window_width <= x || y < 0 || window_height <= y) {
continue;
}
// point scaling
const auto point_x = (int) ((x - window_width / 2.0) * 48.0 / 54.0 + window_width / 2.0);
const auto point_y = (int) (y - window_height / 76);
// point coordinates
// keep the verified left edge fixed while adding up to one-third of a beam
// 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
grid_insert(data, point_x, point_y);
// surrounding points
if (games::rb::TOUCH_SIZE == 3) {
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); // southwest
grid_insert(data, point_x + offset_x, point_y); // east
grid_insert(data, point_x + offset_x, point_y + offset_y); // southeast
grid_insert(data, point_x + offset_x, point_y - offset_y); // northeast
grid_insert(data, point_x, point_y - offset_y); // north
grid_insert(data, point_x, point_y + offset_y); // south
}
// model a finger as a 3x3 block of IR sensors around the touch point
// this gives better accuracy (than just 1x1) since the logic below
// can toggle anywhere from 1x1 to 2x2, and the game engine calculates
// the center point, which means by inserting up to 8 extra blocks
// 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 - offset_y); // northwest
grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest
grid_insert(data, point_x + offset_x, point_y); // east
grid_insert(data, point_x + offset_x, point_y + offset_y); // southeast
grid_insert(data, point_x + offset_x, point_y - offset_y); // northeast
grid_insert(data, point_x, point_y - offset_y); // north
grid_insert(data, point_x, point_y + offset_y); // south
}
touch_debug_publish(data, is_landscape);
// copy data to buffer
memcpy(lpBuffer, data, 20);
memcpy(lpBuffer, data, TOUCH_PACKET_SIZE);
// update frame logging
if (log_fps) {
@@ -193,7 +220,7 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
}
// return amount of bytes written
return 20;
return TOUCH_PACKET_SIZE;
}
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() {
touch_debug_detach();
// detach touch module
touch_detach();
+6
View File
@@ -12,6 +12,12 @@ namespace games::rb {
int window_width = 1080;
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
bool log_fps = false;
uint64_t log_time = 0;
+143
View File
@@ -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;
}
}
+14
View File
@@ -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);
}
+17
View File
@@ -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;
}
+19 -9
View File
@@ -32,18 +32,28 @@ namespace hooks::audio {
RegEnumKeyA(hkEnum, index, key_name, sizeof(key_name)) == ERROR_SUCCESS;
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 };
DWORD size = sizeof(desc);
std::string name = key_name;
if (RegGetValueA(hkEnum,
key_name,
ASIO_REG_DESC,
RRF_RT_REG_SZ | wow64_flag,
nullptr,
desc,
&size) == ERROR_SUCCESS && desc[0]) {
name = desc;
HKEY hkDriver = nullptr;
if (RegOpenKeyExA(hkEnum, key_name, 0,
KEY_QUERY_VALUE | wow64_flag, &hkDriver) == ERROR_SUCCESS) {
DWORD type = 0;
if (RegQueryValueExA(hkDriver,
ASIO_REG_DESC,
nullptr,
&type,
reinterpret_cast<LPBYTE>(desc),
&size) == ERROR_SUCCESS
&& type == REG_SZ && desc[0]) {
// ensure null termination
desc[sizeof(desc) - 1] = '\0';
name = desc;
}
RegCloseKey(hkDriver);
}
// merge with an existing entry from the other view (match by name)
+96 -43
View File
@@ -154,36 +154,30 @@ namespace {
}
}
// live wrappers by CLSID. ASIO drivers are single-instance, so a host that creates a
// new instance without releasing the old one has leaked it; we track this to tear the
// stale one down
// one wrapper per CLSID, kept alive for the process lifetime. ASIO drivers are
// single-instance, and some hardware drivers (e.g. Neva Uno) crash if their COM object
// 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::vector<std::pair<CLSID, WrappedAsio *>> g_wrappers;
// register a wrapper as the live instance for its CLSID, returning any stale wrapper it
// replaces so the caller can tear it down outside the lock
WrappedAsio *register_wrapper(REFCLSID clsid, WrappedAsio *wrapper) {
WrappedAsio *find_wrapper(REFCLSID clsid) {
std::lock_guard lock(g_wrappers_mutex);
for (auto &entry : g_wrappers) {
if (IsEqualCLSID(entry.first, clsid)) {
WrappedAsio *stale = entry.second;
entry.second = wrapper;
return stale;
return entry.second;
}
}
g_wrappers.emplace_back(clsid, wrapper);
return nullptr;
}
void unregister_wrapper(WrappedAsio *wrapper) {
void store_wrapper(REFCLSID clsid, WrappedAsio *wrapper) {
std::lock_guard lock(g_wrappers_mutex);
for (auto it = g_wrappers.begin(); it != g_wrappers.end(); ++it) {
if (it->second == wrapper) {
g_wrappers.erase(it);
return;
}
}
g_wrappers.emplace_back(clsid, wrapper);
}
// 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() {
unregister_wrapper(this);
this->detach_post_process();
// our refcount is decoupled from the host's (see AddRef/Release), so pReal's count is
// exactly one here and this releases/unloads it deterministically
// never runs mid-run: wrap() pins the wrapper so the refcount stays above zero until
// 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();
log_info("audio::wrappedasio", "destroying wrapped ASIO driver, clsid={}", guid2s(this->clsid));
@@ -315,8 +308,17 @@ ULONG STDMETHODCALLTYPE WrappedAsio::Release() {
#pragma region IAsio
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);
if (result == AsioTrue) {
this->initialized = true;
log_info(
"audio::wrappedasio",
"init succeeded for '{}' (driver version {})",
@@ -346,6 +348,7 @@ void __thiscall WrappedAsio::get_error_message(char *string) {
AsioError __thiscall WrappedAsio::start() {
const AsioError result = this->pReal->start();
if (result == ASE_OK) {
this->started = true;
log_info(
"audio::wrappedasio",
"start succeeded, ASIO stream is now running on '{}'",
@@ -360,6 +363,7 @@ AsioError __thiscall WrappedAsio::start() {
AsioError __thiscall WrappedAsio::stop() {
const AsioError result = this->pReal->stop();
if (result == ASE_OK) {
this->started = false;
log_info("audio::wrappedasio", "stop succeeded, ASIO stream on '{}' halted", this->driver_name);
} else {
log_warning("audio::wrappedasio", "stop failed, err={}", static_cast<long>(result));
@@ -740,6 +744,27 @@ void WrappedAsio::detach_post_process() {
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) {
if (double_buffer_index != 0 && double_buffer_index != 1) {
return;
@@ -838,6 +863,7 @@ AsioError __thiscall WrappedAsio::create_buffers(
}
}
this->publish_post_process(buffer_size);
this->buffers_created = true;
} else {
log_warning(
"audio::wrappedasio",
@@ -940,6 +966,7 @@ AsioError WrappedAsio::create_buffers_front_pair(
// device's 2.0 output. then publish ourselves to the realtime thread
this->record_downmix_channels(buffer_infos, num_channels, buffer_size);
this->publish_post_process(buffer_size);
this->buffers_created = true;
log_info(
"audio::wrappedasio",
@@ -962,6 +989,7 @@ AsioError __thiscall WrappedAsio::dispose_buffers() {
this->volume_channels.clear();
this->volume_active = false;
this->downmix_active = false;
this->buffers_created = false;
return result;
}
@@ -986,29 +1014,54 @@ namespace hooks::audio::asio {
auto *wrapper = new WrappedAsio(
reinterpret_cast<IAsio *>(real), clsid, registered_asio_name(clsid));
// if the host already had a live wrapper for this CLSID it leaked the previous
// instance (ASIO is single-instance); tear it down now so the real driver is
// released before the reinit. works around games (iidx32+) that ref twice but
// deref once before re-initializing.
//
// FlexASIO 1.9 and many DAC ASIO drivers can't handle this; FlexASIO 1.10 and
// Xonar AE can
if (WrappedAsio *stale = register_wrapper(clsid, wrapper)) {
log_info(
"audio::wrappedasio",
"host did not release previous instance for clsid={}, forcing teardown",
guid2s(clsid));
// the stale instance may still have a running stream on the driver's realtime
// thread. stop and dispose it before deleting so no in-flight buffer switch
// (e.g. our volume trampoline) touches buffers we are about to free. ASIO
// guarantees no further buffer_switch once stop() returns, and dispose_buffers
// detaches our trampolines, fully quiescing the realtime path before delete
stale->stop();
stale->dispose_buffers();
delete stale;
}
// pin the wrapper (and the real driver it owns) for the process lifetime; later
// CoCreate calls reuse this same instance via wrap_existing
wrapper->AddRef();
store_wrapper(clsid, 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();
}
}
}
+25 -1
View File
@@ -18,8 +18,20 @@ namespace hooks::audio::asio {
bool is_asio_creation(REFCLSID rclsid, REFIID riid);
// wrap a real ASIO driver instance, taking ownership of the supplied reference, and
// return a proxy that forwards every call to it
// return a proxy that forwards every call to it. also records it as the cached
// instance for its CLSID so later CoCreate calls can reuse it (see wrap_existing)
IUnknown *wrap(REFCLSID clsid, void *real);
// if a cached wrapper already exists for this CLSID, return it (with an added
// reference); otherwise nullptr to signal the caller to create the real driver and
// wrap it. lets the host reuse one driver instance instead of re-instantiating it
IUnknown *wrap_existing(REFCLSID clsid);
// drop the process-lifetime references taken by wrap() so cached drivers can be released
// at shutdown. only relinquishes our pin, so a real driver is torn down once the host
// has released its own references too. call from a controlled shutdown point, never from
// a static destructor (the driver DLL may already be unloaded)
void release_all_wrappers();
}
// transparent proxy around a real ASIO driver; a single place to intercept ASIO traffic
@@ -103,6 +115,10 @@ struct WrappedAsio final : IAsio {
AsioError __thiscall output_ready() override;
#pragma endregion
// quiesces any leftover stream/buffer state before the cached wrapper is handed back
// for reuse, without destroying the real driver (see wrap_existing)
void quiesce_for_reuse();
private:
// create_buffers implementation used when a stereo extraction is active: forwards only
// the channels the real device has and hands the game throwaway buffers for the rest
@@ -170,6 +186,14 @@ private:
// unambiguous name; constant for our lifetime
std::string driver_name;
// the real driver is initialized exactly once; repeat init() calls are a no-op success
bool initialized = false;
// whether the real driver currently has a buffer set / running stream. used to quiesce
// leftover state when the cached wrapper is reused (see quiesce_for_reuse)
bool buffers_created = false;
bool started = false;
// our own reference count; we hold one reference on pReal and release it when this
// drops to zero
std::atomic<ULONG> ref_count {1};
+13
View File
@@ -68,6 +68,15 @@ static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
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
ret = CoCreateInstance_orig(rclsid, pUnkOuter, dwClsContext, riid, ppv);
if (FAILED(ret)) {
@@ -130,5 +139,9 @@ namespace hooks::audio {
CLIENT = nullptr;
}
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();
}
}
+3 -3
View File
@@ -23,7 +23,7 @@
#include "launcher/shutdown.h"
#include "overlay/overlay.h"
#include "touch/touch.h"
#include "touch/touch_indicators.h"
#include "touch/touch_gestures.h"
#include "util/detour.h"
#include "util/logging.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(
"graphics",
"CreateWindowExA returned {}, {}",
@@ -722,7 +722,7 @@ static HWND WINAPI CreateWindowExW_hook(DWORD dwExStyle, LPCWSTR lpClassName, LP
fmt::ptr(result),
lpWindowName ? ws2s(lpWindowName) : "(null)");
disable_touch_indicators(result);
disable_touch_gestures(result);
return result;
}
+34 -6
View File
@@ -49,6 +49,7 @@
#include "games/popn/popn.h"
#include "games/qma/qma.h"
#include "games/rb/rb.h"
#include "games/rb/touch_debug.h"
#include "games/rf3d/rf3d.h"
#include "games/sc/sc.h"
#include "games/scotto/scotto.h"
@@ -1064,6 +1065,15 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::InvertTouchCoordinates].value_bool()) {
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
// if (options[launcher::Options::DisableTouchCardInsert].value_bool()) {
// SPICETOUCH_CARD_DISABLE = true;
@@ -1105,6 +1115,9 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::DisableOverlay].value_bool()) {
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) {
const auto val = options[launcher::Options::OverlayScaling].value_uint32();
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;
} else if (options[launcher::Options::JubeatTouchAlgo].value_text() == "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 {
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
games::rb::TOUCH_DEBUG_OVERLAY = options[launcher::Options::RBTouchDebug].value_bool();
if (options[launcher::Options::spice2x_RBTouchScale].is_active()) {
games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32();
}
if (options[launcher::Options::RBTouchSize].is_active()) {
const auto text = options[launcher::Options::RBTouchSize].value_text();
if (text == "3") {
games::rb::TOUCH_SIZE = 3;
}
}
// -rbtouchsize is deprecated and ignored; touch emulation always uses the 3x3 model
if (options[launcher::Options::RBTouchPollRate].is_active()) {
games::rb::TOUCH_POLL_RATE = options[launcher::Options::RBTouchPollRate].value_uint32();
}
+105 -22
View File
@@ -434,18 +434,23 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "Advanced Network",
},
{
.title = "Preferred NetAdapter IP",
.title = "Preferred Network Adapter's IP",
.name = "network",
.desc = "This is NOT the EA service URL; use -url for that. "
"Force the use of an adapter with the specified network. Must also provide -subnet.",
.display_name = "netadapterip",
.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,
.category = "Advanced Network",
.sensitive = true,
},
{
.title = "Preferred NetAdapter Subnet",
.title = "Preferred Network Adapter's 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,
.category = "Advanced Network",
},
@@ -505,6 +510,13 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Bool,
.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
.title = "Spice Overlay UI Scale %",
@@ -1031,16 +1043,16 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.title = "SDVX Disable Live2D (EXPERIMENTAL)",
.name = "sdvxnolive2d",
.desc = "Skip rendering the SDVX Live2D graphics to save GPU.\n\n"
"Off: leave Live2D as-is.\n"
"Always: never render Live2D (also removes the menu navigator).\n"
"During songs: only skip Live2D during a song; keeps the menu navigator. Scene detection relies on game logging.",
"off: leave Live2D as-is.\n"
"always: hide Live2D (also hides menu navigator).\n"
"ingame: only hide Live2D during a song; keeps the menu navigator. Scene detection relies on game logging.",
.type = OptionType::Enum,
.game_name = "Sound Voltex",
.category = "Advanced Game Options",
.elements = {
{"off", "Show Live2D"},
{"always", "Never show Live2D"},
{"ingame", "Show navigators only"},
{"always", "Hide Live2D"},
{"ingame", "Hide during songs"},
},
.quick_setting_category = "Game",
},
@@ -1772,11 +1784,11 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "I/O Modules",
},
{
.title = "Disable Raw Input Touch",
.title = "Touch Compatibility Mode (Disable Raw Input Touch)",
.name = "wintouch",
.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 "
"using the default (raw input) touch input, as Raw Input performs better.",
"Win8 Pointer API or Win7 Touch API. Results in better compatibility with some "
"touch screens, but may result in worse performance and higher latency.",
.type = OptionType::Bool,
.category = "Touch Parameters",
},
@@ -1796,6 +1808,24 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Bool,
.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
.title = "Disable Touch Card Insert (DEPRECATED - use -touchcard instead)",
@@ -1807,12 +1837,14 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
},
{
// spice2x_TouchCardInsert
.title = "Show Insert Card button",
.title = "Show Insert Card button (DEPRECATED)",
.name = "sp2x-touchcard",
.display_name = "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,
.hidden = true,
.category = "Touch Parameters",
},
{
@@ -1884,7 +1916,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.title = "HID SmartCard Fix UID",
.name = "scardfix",
.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"
"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. "
@@ -2668,6 +2700,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.desc = "For touch screen players: choose the touch algorithm to use.\n\n"
"legacy - evenly divide the grid into 16 squares; old spicetools behavior, slightly inaccurate in gaps\n\n"
"improved (default) - squares register as-is, gaps will trigger the closest square\n\n"
"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)",
.type = OptionType::Enum,
.game_name = "Jubeat",
@@ -2675,10 +2708,58 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.elements = {
{"legacy", ""},
{"improved", ""},
{"plus", ""},
{"accurate", ""},
},
.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
.title = "RB Touch Emulation Scale",
@@ -2693,14 +2774,15 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
},
{
// RBTouchSize
.title = "RB Touch Emulation Size",
.title = "RB Touch Emulation Size (DEPRECATED - no longer has any effect)",
.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,
.hidden = true,
.game_name = "Reflec Beat",
.category = "Game Options",
.elements = {
{"1", "1x1"},
{"3", "3x3"},
},
},
@@ -2708,9 +2790,10 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
// RBTouchPollRate
.title = "RB Touch Emulation Poll Hz",
.name = "rbtouchhz",
.desc = "By default, the game polls for touch at 120Hz. "
"This option overrides that rate; enter a number betwen 1 and 1000.\n\n"
"It should be noted that higher poll does not necessarily improve accuracy or performance.",
.desc = "By default, the game polls for touch at ~125Hz. "
"This option overrides that rate; enter a number between 1 and 1000.\n\n"
"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,
.setting_name = "250",
.game_name = "Reflec Beat",
+5
View File
@@ -53,6 +53,7 @@ namespace launcher {
SOFTID,
VREnable,
DisableOverlay,
OverlayKeyboardNavigation,
OverlayScaling,
NotificationPosition,
spice2x_FpsAutoShow,
@@ -179,6 +180,7 @@ namespace launcher {
ForceWinTouch,
ForceTouchEmulation,
InvertTouchCoordinates,
RawInputTouchAspectRatio,
DisableTouchCardInsert,
spice2x_TouchCardInsert,
ICCAReaderPort,
@@ -262,6 +264,9 @@ namespace launcher {
IIDXWindowedSubscreenAlwaysOnTop,
spice2x_JubeatLegacyTouch,
JubeatTouchAlgo,
JubeatTouchDebug,
JubeatTouchDebounce,
RBTouchDebug,
spice2x_RBTouchScale,
RBTouchSize,
RBTouchPollRate,
+34 -32
View File
@@ -366,6 +366,7 @@ static void __cdecl device_update() {
// update touch
games::jb::touch_update();
auto touched = games::jb::touch_state();
// 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))) {
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
@@ -439,6 +440,7 @@ static void __cdecl device_update() {
// update touch
games::jb::touch_update();
auto touched = games::jb::touch_state();
// 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))) {
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
}
+9 -3
View File
@@ -20,6 +20,7 @@ static cardunit_card_cardnumber_t cardunit_card_cardnumber = nullptr;
static HINSTANCE SCIUNIT_INSTANCE;
static std::string SCIUNIT_INSTANCE_NAME = "sciunit.dll";
static bool SCIUNIT_INITIALIZED = false;
static int SCIUNIT_STATUS = 0;
static bool CARD_IN = false;
static bool CARD_PRESSED = false;
static uint8_t CARD_UID[8];
@@ -150,11 +151,11 @@ static int __cdecl sciunit_finalize() {
}
static int __cdecl sciunit_get_errorunit() {
return 0;
return -1;
}
static int __cdecl sciunit_get_stat() {
return 0;
return SCIUNIT_STATUS;
}
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() {
SCIUNIT_INITIALIZED = true;
SCIUNIT_STATUS = 1;
return 0;
}
@@ -223,8 +225,12 @@ static int __cdecl sciunit_reset() {
}
static int __cdecl sciunit_update() {
if (SCIUNIT_INITIALIZED)
if (SCIUNIT_INITIALIZED) {
if (SCIUNIT_STATUS == 1) {
SCIUNIT_STATUS = 2;
}
update_card();
}
return 0;
}
+15 -1
View File
@@ -6,6 +6,7 @@
#include "games/gitadora/gitadora.h"
#include "games/iidx/iidx.h"
#include "games/popn/popn.h"
#include "games/rb/touch_debug.h"
#include "hooks/graphics/graphics.h"
#include "misc/eamuse.h"
#include "touch/touch.h"
@@ -66,6 +67,7 @@ namespace overlay {
bool AUTO_SHOW_IOPANEL = false;
bool AUTO_SHOW_KEYPAD_P1 = false;
bool AUTO_SHOW_KEYPAD_P2 = false;
bool ENABLE_KEYBOARD_NAVIGATION = false;
bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = false;
FpsLocation FPS_LOCATION = FpsLocation::TopRight;
std::optional<uint32_t> UI_SCALE_PERCENT;
@@ -311,6 +313,9 @@ void overlay::SpiceOverlay::init() {
auto &io = ImGui::GetIO();
io.UserData = this;
io.ConfigFlags = ImGuiConfigFlags_None;
if (cfg::CONFIGURATOR_STANDALONE || ENABLE_KEYBOARD_NAVIGATION) {
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
}
if (!cfg::CONFIGURATOR_STANDALONE) {
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
}
@@ -551,9 +556,13 @@ void overlay::SpiceOverlay::new_frame() {
const bool draw_fps_persistent = this->renderer != OverlayRenderer::SOFTWARE
&& 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
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;
}
@@ -578,6 +587,10 @@ void overlay::SpiceOverlay::new_frame() {
ImGui::NewFrame();
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
if (this->active) {
for (auto &window : this->windows) {
@@ -595,6 +608,7 @@ void overlay::SpiceOverlay::new_frame() {
if (draw_fps_persistent) {
this->window_fps->build();
}
// draw notifications last so they paint on top of any overlay windows
if (draw_notifications) {
overlay::notifications::draw();
+1
View File
@@ -44,6 +44,7 @@ namespace overlay {
extern bool AUTO_SHOW_IOPANEL;
extern bool AUTO_SHOW_KEYPAD_P1;
extern bool AUTO_SHOW_KEYPAD_P2;
extern bool ENABLE_KEYBOARD_NAVIGATION;
extern bool USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT;
extern FpsLocation FPS_LOCATION;
extern bool SHOW_DEBUG_LOG_WINDOW;
+7 -3
View File
@@ -54,7 +54,7 @@ namespace rawinput {
switch (hdr->dbch_devicetype) {
case DBT_DEVTYP_DEVICEINTERFACE: {
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
if (memcmp(&dev->dbcc_classguid, &GUID_HID, sizeof(GUID_HID)) != 0)
@@ -112,6 +112,7 @@ namespace rawinput {
std::string name(dev->dbcc_name);
// destruct device
log_misc("hotplug", "device interface removal: {}", name);
this->ri_mgr->devices_remove(name);
}
}
@@ -120,8 +121,11 @@ namespace rawinput {
return TRUE;
}
case DBT_DEVNODES_CHANGED: {
// TODO: this can be a little noisy as it gets called on every device
this->ri_mgr->devices_scan_midi();
// catch-all device-tree change: MIDI and XInput have no targeted
// 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();
break;
}
+727
View File
@@ -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;
}
}
File diff suppressed because it is too large Load Diff
+44 -3
View File
@@ -3,6 +3,8 @@
#include <functional>
#include <thread>
#include <condition_variable>
#include <list>
#include <mutex>
#include <vector>
#include <windows.h>
@@ -63,10 +65,38 @@ namespace rawinput {
class RawInputManager {
private:
HotplugManager *hotplug;
std::vector<Device> devices;
HotplugManager *hotplug = nullptr;
// 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 {};
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;
bool flush_thread_running = false;
std::mutex flush_thread_m;
@@ -83,6 +113,8 @@ namespace rawinput {
void input_hwnd_create();
void input_hwnd_destroy();
void devices_reload();
void midi_scan_join();
void midi_close_deferred_flush();
void devices_scan_rawinput(RAWINPUTDEVICELIST *device, bool log = true);
void devices_scan_piuio();
void devices_scan_smxstage();
@@ -93,6 +125,7 @@ namespace rawinput {
void flush_stop();
void output_start();
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_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 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:
HWND input_hwnd = nullptr;
@@ -118,6 +156,7 @@ namespace rawinput {
void devices_scan_rawinput(const std::string &device_name = "");
void devices_scan_midi();
void midi_scan_start();
void devices_scan_xinput();
void devices_remove(const std::string &name);
@@ -130,11 +169,12 @@ namespace rawinput {
void __stdcall devices_print();
Device *devices_get(const std::string &name, bool updated = false);
inline std::vector<Device> &devices_get() {
inline std::list<Device> &devices_get() {
return devices;
}
inline std::vector<Device *> devices_get_updated() {
std::lock_guard<std::recursive_mutex> lock(devices_mutex);
std::vector<Device *> updated;
for (auto &device : devices_get()) {
device.mutex->lock();
@@ -150,6 +190,7 @@ namespace rawinput {
}
inline void devices_midi_freeze(bool freeze) {
std::lock_guard<std::recursive_mutex> lock(devices_mutex);
for (auto &device : devices_get()) {
if (device.type == MIDI) {
device.midiInfo->freeze = freeze;
+157 -15
View File
@@ -24,13 +24,116 @@ namespace rawinput::touch {
// settings
bool DISABLED = false;
bool INVERTED = false;
AspectMode ASPECT_COMPENSATION_MODE = AspectMode::Auto;
bool ASPECT_COMPENSATION_GAME = false;
// state
DWORD DISPLAY_ORIENTATION = DMDO_DEFAULT;
long DISPLAY_SIZE_X = 1920L;
long DISPLAY_SIZE_Y = 1080L;
long DISPLAY_NATIVE_X = 0L;
long DISPLAY_NATIVE_Y = 0L;
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) {
// check if disabled
@@ -322,6 +425,18 @@ namespace rawinput::touch {
std::vector<DWORD> touch_removes;
std::vector<TouchPoint> touch_writes;
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) {
// check if existing
@@ -353,23 +468,29 @@ namespace rawinput::touch {
// only remove if it exists
if (existing != touch.touch_points.end()) {
// 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());
remove_touch_point(hid_tp.id);
}
} 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 {
.id = hid_tp.id,
.x = (long) (hid_tp.x * DISPLAY_SIZE_X),
.y = (long) (hid_tp.y * DISPLAY_SIZE_Y),
.x = (long) (norm_x * DISPLAY_SIZE_X),
.y = (long) (norm_y * DISPLAY_SIZE_Y),
.mouse = false,
};
touch_writes.push_back(tp);
@@ -390,13 +511,28 @@ namespace rawinput::touch {
}
// 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 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 : touch.touch_points) {
if (hid_tp.id == hid_tp_id) {
hid_tp.ttl = ttl;
hid_tp.last_report = system_time_ms;
}
}
@@ -513,7 +649,13 @@ namespace rawinput::touch {
GetWindowRect(GetDesktopWindow(), &display_rect);
DISPLAY_SIZE_X = display_rect.right - display_rect.left;
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
DEVMODE display_mode{};
+19
View File
@@ -9,6 +9,25 @@ namespace rawinput::touch {
extern bool DISABLED;
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
extern DWORD DISPLAY_ORIENTATION;
extern long DISPLAY_SIZE_X;
+18 -2
View File
@@ -179,11 +179,27 @@ XInputSetState(
XInputManager::XInputManager() {
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) {
log_warning("xinput", "failed to load xinput1_3.dll");
log_warning("xinput", "failed to load any XInput DLL");
return;
}
log_info("xinput", "loaded {}", loaded_name);
XInputGetState_addr = reinterpret_cast<decltype(XInputGetState) *>(
GetProcAddress(this->xinput_lib, "XInputGetState"));
if (!XInputGetState_addr) {
+213
View File
@@ -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);
}
+21
View File
@@ -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();
+91 -57
View File
@@ -11,6 +11,7 @@
#include "avs/game.h"
#include "external/imgui/imgui.h"
#include "games/jb/jb.h"
#include "hooks/graphics/graphics.h"
#include "misc/eamuse.h"
#include "overlay/overlay.h"
@@ -19,9 +20,12 @@
#include "util/detour.h"
#include "util/libutils.h"
#include "util/logging.h"
#include "util/time.h"
#include "util/utils.h"
#include "gdi_overlay.h"
#include "handler.h"
#include "touch_gestures.h"
#include "win7.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_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 spice2x, this is true (hide by default)
bool SPICETOUCH_CARD_DISABLE = true;
@@ -247,16 +261,15 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
if (!SPICETOUCH_REGISTERED_TOUCH) {
SPICETOUCH_REGISTERED_TOUCH = true;
// check if touch is available
// register the handler when a touch screen is present
if (is_touch_available("SpiceTouchWndProc")) {
// notify the handler of our window
TOUCH_HANDLER->window_register(hWnd);
}
// enable card unless the feature is disabled
if (!SPICETOUCH_CARD_DISABLE) {
SPICETOUCH_CARD_ENABLED = true;
}
// 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) {
SPICETOUCH_CARD_ENABLED = true;
}
}
@@ -307,7 +320,10 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
break;
}
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;
}
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);
}
// 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) {
// update and render
overlay::OVERLAY->update();
overlay::OVERLAY->new_frame();
overlay::OVERLAY->render();
// get pixel data
int width, height;
uint32_t *pixel_data = overlay::OVERLAY.get()->sw_get_pixel_data(&width, &height);
if (width > 0 && height > 0) {
overlay_pixels = overlay::OVERLAY.get()->sw_get_pixel_data(
&overlay_width, &overlay_height);
overlay_pixels_dirty = overlay::OVERLAY->sw_pixels_dirty;
}
bool overlay_active = overlay_enabled && overlay::OVERLAY->get_active();
// create bitmap
HBITMAP bitmap = CreateBitmap(width, height, 1, 8 * sizeof(uint32_t), pixel_data);
// compose the whole frame into an offscreen back buffer and present it with a
// single blit; the window never shows a half-erased (transparent) surface
// mid-paint, which is what caused the occasional flicker at higher frame rates
PAINTSTRUCT paint {};
HDC hdc = BeginPaint(hWnd, &paint);
// prepare paint
PAINTSTRUCT paint {};
HDC hdc = BeginPaint(hWnd, &paint);
HDC hdcMem = CreateCompatibleDC(hdc);
SetBkMode(hdc, TRANSPARENT);
RECT bufferRect {};
GetClientRect(hWnd, &bufferRect);
int buffer_width = bufferRect.right - bufferRect.left;
int buffer_height = bufferRect.bottom - bufferRect.top;
/*
* draw bitmap
* - this currently sets the background to black because of SRCCOPY
* - SRCPAINT will blend but colors are wrong
* - 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
DeleteObject(bitmap);
DeleteDC(hdcMem);
EndPaint(hWnd, &paint);
}
HBRUSH color_key_brush =
(HBRUSH) GetClassLongPtr(hWnd, GCLP_HBRBACKGROUND);
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);
return 0;
}
// draw card input
if (SPICETOUCH_CARD_ENABLED && (SPICETOUCH_FONT != nullptr)) {
// prepare paint
PAINTSTRUCT paint {};
HDC hdc = BeginPaint(hWnd, &paint);
SetBkMode(hdc, TRANSPARENT);
// draw the insert-card button below the jubeat debug overlay; it is
// hidden while the overlay is active
if (SPICETOUCH_CARD_ENABLED && SPICETOUCH_FONT != nullptr && !overlay_active) {
// create brushes
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;
// draw borders
FillRect(hdc, &boxRect, brushBorder);
FillRect(draw_dc, &boxRect, brushBorder);
// modify box rect
boxRect.left += 1;
@@ -423,7 +439,7 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
boxRect.bottom -= 1;
// fill box
FillRect(hdc, &boxRect, brushFill);
FillRect(draw_dc, &boxRect, brushFill);
// modify box rect
if (should_rotate) {
@@ -435,21 +451,27 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
}
// draw text
SelectObject(hdc, SPICETOUCH_FONT);
SetTextColor(hdc, RGB(0, 196, 0));
DrawText(hdc, INSERT_CARD_TEXT, -1, &boxRect, DT_LEFT | DT_BOTTOM | DT_NOCLIP);
SelectObject(draw_dc, SPICETOUCH_FONT);
SetTextColor(draw_dc, RGB(0, 196, 0));
DrawText(draw_dc, INSERT_CARD_TEXT, -1, &boxRect, DT_LEFT | DT_BOTTOM | DT_NOCLIP);
// delete objects
DeleteObject(brushFill);
DeleteObject(brushBorder);
// end paint
EndPaint(hWnd, &paint);
return 0;
}
// call default window procedure
return DefWindowProc(hWnd, msg, wParam, lParam);
#if !SPICE_XP
// 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: {
@@ -479,6 +501,11 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
return 0;
}
case WM_DESTROY: {
touch_gdi_overlay_release();
if (SPICETOUCH_FONT != nullptr) {
DeleteObject(SPICETOUCH_FONT);
SPICETOUCH_FONT = nullptr;
}
PostQuitMessage(0);
return 0;
}
@@ -538,6 +565,7 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
.x = GET_X_LPARAM(lParam),
.y = GET_Y_LPARAM(lParam),
.mouse = true,
.down_ms = get_performance_milliseconds(),
};
TOUCH_POINTS.push_back(tp);
@@ -751,6 +779,9 @@ void touch_create_wnd(HWND hWnd, bool overlay) {
ShowWindow(touch_window, SW_SHOWNOACTIVATE);
UpdateWindow(touch_window);
// disable the OS touch contact visualization for our own touch window
disable_touch_gestures(touch_window);
// register
touch_register_window(touch_window);
@@ -763,8 +794,8 @@ void touch_create_wnd(HWND hWnd, bool overlay) {
// create instance
overlay::OVERLAY.reset(new overlay::SpiceOverlay(touch_window));
// draw overlay in 30 FPS
SetTimer(touch_window, 1, 1000 / 30, NULL);
// draw overlay repaint timer (30 FPS on WinXP, 60 FPS otherwise)
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
if (!found) {
// stamp the landing time so debounce can measure the contact's age
tp.down_ms = get_performance_milliseconds();
// add touch point
TOUCH_POINTS.push_back(tp);
+1
View File
@@ -7,6 +7,7 @@ struct TouchPoint {
DWORD id;
LONG x, y;
bool mouse;
double down_ms = 0.0; // performance-counter milliseconds when the contact first landed
};
enum TouchEventType {
TOUCH_DOWN,
+168
View File
@@ -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);
}
+8
View File
@@ -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);
-57
View File
@@ -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);
}
-5
View File
@@ -1,5 +0,0 @@
#pragma once
#include <windows.h>
void disable_touch_indicators(HWND hwnd);
+2
View File
@@ -11,6 +11,7 @@
#include "util/libutils.h"
#include "util/logging.h"
#include "util/time.h"
#include "rawinput/touch.h"
// mingw issue #2205 workaround
@@ -177,6 +178,7 @@ void Win7Handler::handle_message(msg_handler_result &result, HWND hWnd, UINT msg
.x = point.x,
.y = point.y,
.mouse = false,
.down_ms = get_performance_milliseconds(),
};
TOUCH_POINTS.push_back(tp);
+9 -1
View File
@@ -12,6 +12,7 @@
#include "util/libutils.h"
#include "util/logging.h"
#include "util/time.h"
#include "rawinput/touch.h"
// mingw does not seem to have this either
@@ -114,10 +115,16 @@ bool Win8Handler::is_available() {
bool Win8Handler::window_register(HWND hWnd) {
// 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 |
TABLET_DISABLE_PENTAPFEEDBACK |
TABLET_DISABLE_PENBARRELFEEDBACK |
TABLET_DISABLE_FLICKS;
TABLET_DISABLE_FLICKS |
TABLET_DISABLE_TOUCHUIFORCEOFF |
TABLET_DISABLE_TOUCHSWITCH |
TABLET_DISABLE_SMOOTHSCROLLING |
TABLET_DISABLE_FLICKFALLBACKKEYS;
// get atom ID
ATOM atomID = GlobalAddAtom(ATOM_NAME);
@@ -214,6 +221,7 @@ void Win8Handler::handle_message(msg_handler_result &result, HWND hWnd, UINT msg
.x = point.x,
.y = point.y,
.mouse = false,
.down_ms = get_performance_milliseconds(),
};
TOUCH_POINTS.push_back(tp);
+66
View File
@@ -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