Compare commits

..

14 Commits

Author SHA1 Message Date
bicarus ebda16beae rb: update reflec beat to use new timer (#692)
## Link to GitHub Issue or related Pull Request, if one exists
Continuation of #682

## Description of change
RB uses SleepEx, so it was missed during the search while working on
#682.

## Testing
Plays about the same on a 200Hz touch monitor.
2026-05-10 19:47:33 -07:00
bicarus c29a26f183 graphics: hide subscreen window from task bar and alt+tab list when in full screen (#690)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
When booting in fullscreen, hide the subscreen from the alt+tab list and
from the task bar. Alt-tabbing to it does nothing, so this is just to
avoid confusion.

No change to windowed mode.

## Testing
Tested TDJ/SDVX/Popn. Didn't test Gitadora but it's probably fine..
2026-05-10 05:33:18 -07:00
bicarus 2014e9cf75 graphics: option to change monitor resolution (#689)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Add a new option under `Monitor` group to change the monitor resolution
before booting the game, very similar to how orientation / refresh rate
options work.

This would be useful for games like Polaris Chord that boots at current
resolution but manages to have graphical issues when not in 1080p.

## Testing
2026-05-09 21:32:43 -07:00
bicarus 33f2e1d4b7 rawinput, touch: attempt to remove touchscreens injecting mouse events (#687)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #562

## Description of change

### rawinput touch fixes
In rawinput touch handler, if the mouse events (`WM_LBUTTONDOWN`,
`WM_MOUSEMOVE`, `WM_LBUTTONUP`) originated from a touch screen, drop
them on the floor instead of treating them as touch events.

This is an attempt to fix phantom touch issues we see with some screens.

Fix another issue where, if mouse is clicked inside the window, and then
dragged outside the window and button is released, touch point (id 0)
continues to be held.

### wintouchemu fixes
If we detected a touch input recently (500ms), ignore new mouse clicks.
This isn't perfect, but hopefully it helps with filtering out mouse
clicks originating from touch screens.

Also, switch over to detecting the primary mouse button correctly, not
just the left mouse button.

### notes

Future investigation: are there really touchscreens that report a mouse
HID capability & insert mouse events? We could have a way to filter them
at rawinput level but it'll be ugly.

## Testing
Tried jubeat, beatstream..
2026-05-09 17:52:43 -07:00
bicarus eb037542b4 jubeat: introduce new algorithm for touch (#686)
## Description of change
There was enough feedback that the current algorithm is not ideal for
most touch screens because most touch screens are not 24". In the
current algorithm touching the gap is no-op, so people think the game is
dropping touches.

This PR introduces a new algorithm that inspect touches on the gap and
triggers the nearest square.

## Testing
Tested landscape and portrait jubeat.
2026-05-09 04:22:29 -07:00
bicarus 039b42ad53 fix build (#684) 2026-05-08 22:46:29 -07:00
bicarus 33b4718744 overlay: fix mouse events queued up while overlay is hidden (#683)
## Link to GitHub Issue or related Pull Request, if one exists
continuation of #677

## Description of change
When overlay was hidden, when mouse buttons were clicked, they were
queued up and replayed when overlay became visible again.

This is due to improper usage of `GetAsyncKeyState`, per documentation.

## Testing
2026-05-08 03:37:46 -07:00
bicarus 71ba9b6b47 os: implement win10 high-resolution timer as replacement for Sleep() / sleep_for() (#682)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #681 

## Description of change
`Sleep` and `sleep_for()` can be very inaccurate and varies depending on
what the OS gives us...

### `timeBeginPeriod(1)`
On boot, we are now calling `timeBeginPeriod(1)`, which affects the
whole process but makes `Sleep` more accurate. There is some risk here
if any game was relying on doing things like `Sleep(1)` and expecting it
to run for 15.6ms. Most games already call `timeBeginPeriod(1)` in the
game engine, though not the whole time, so I'm hoping that this is not
too impactful.

### Opt out of Win11 power throttling
Ensure that timer resolution change above is respected even when the
window is occluded / minimized by opting out of throttling via
`PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION`.

### Use Win10 high resolution timer instead of Sleep

On Win10 1803 and above, there is a new OS-level API for high resolution
timers; if this is available, use it
(`CREATE_WAITABLE_TIMER_HIGH_RESOLUTION`). Worth noting that WINE
doesn't support this currently.

If not, fall back to `Sleep`, which is significantly better than
`sleep_for()` in my experiments.

Callers of Sleep / sleep_for were replaced with this new timer. Most of
them anyway; calls to Sleep() with more than 100ms+ was left alone.

### Add an option as a chicken bit

To opt out I'm adding a new option called `Use Legacy Timers` which will
revert to behavior before this PR. The code paths that switched from
`sleep_for` to `Sleep` will remain in place though, not affected by the
option.

## Expected changes
In some I/O emulation modules, poll threads may run more frequently,
resulting in lower latency.

It also means that spice overall may use more CPU resources and power.
If you don't like this, you can always enable the option to opt out;
e.g., if you're on old arcade cab PC.

## Testing

DDR p4io - ok
drs touch hook - ok
IIDX camera hook - ok
CCJ trackball - ok
2026-05-08 02:25:20 -07:00
bicarus 1957917270 sdk: update cpp sample for DDR, fix DDR MDXF polling when api/sdk is providing input (#679)
## Link to GitHub Issue or related Pull Request, if one exists
#676 
#452 

## Description of change

When spice SDK or API sets or clears button and analog override, we
don't tell MDXF about it, so it may fall behind. This is a special case
for DDR MDXF I/O emulation. For Naive/Xinput I think we are getting
lucky because they usually also generate WM_INPUT events.

Update the C++ SDK sample to provide more interesting example of
emulating DDR arrows with the keyboard.

## Testing
2026-05-07 18:10:14 -07:00
bicarus b9ccddf47d launcher: prevent early hook from launching in standalone configurator (#678)
## Description of change
Seems to be an oversight when it was initially implemented; hooks should
not load when launching `spicecfg` (or `spice -cfg` or `spice64 -cfg`).
2026-05-07 00:26:18 -07:00
bicarus cd0ba51b5a sdk: introduce Spice SDK (#676)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Adds a flat-C, headers-only library for writing DLL plugins with spice.

This is meant to be an alternative to writing applications over spice
API which doesn't suit some scenarios like writing custom
high-performance I/O modules.

All you have to do is:

1. Create a 32-bit or 64-bit DLL with `spice_sdk_entry_point` as a
DLLExport
1. Include `spicesdk.h`, and optionally `spicesdk_io.h` (see
`extras\sdk\include`)
1. `spice_sdk_entry_point` will be called when hooks get initialized.
Call `init` with valid parameters.
1. When `init` returns, `SPICE_SDK_V0` will be filled out with a series
of function pointers; your DLL can now start calling into it.
1. Compile your DLL and add it as a DLL hook (`-k`) in spice when
launching the game.

See `v0_cpp.cpp` for an example.

V0 implements the following functions:

```
    log;

    get_game_info;
    get_avs_info;

    get_button;
    set_button;

    get_analog;
    set_analog;

    get_light;
    set_light;

    set_touch;
    clear_touch;

    insert_card;
    set_keypad;
```

V0 is focused on providing common utility functions needed in a lot of
DLL hooks (get AVS info, log messages to file), and implementing I/O.

In the future, we could have more interesting things like:

* scanning and modifying memory 
* installing DLL hooks
* having direct hooks into I/O emulation modules
* getting full tape LEDs
* having a callback into DX9 `Present` call
* drawing an ImGui window

More detailed documentation is coming soon in form of a wiki page.

## Testing
See included sample DLL.
2026-05-06 23:29:52 -07:00
bicarus 1f23d88c7a overlay: prevent overlay from queueing up keyboard input while overlay is not visible (#677)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #675

Yet another regression from #604 

## Description of change
When overlay was hidden (not visible) it was still accepting keyboard
input, so when it became visible again the queued up keystrokes got
processed all at once.

Also, fix a bug with monitor settings update kicking in when in windowed
mode - this really should have been only for full screen.

## Testing
Tested DDR windowed / full screen, TDJ with subscreen overlay windowed /
fullscreen.
2026-05-06 01:00:52 -07:00
bicarus-dev 46e43ab09c fix typo 2026-05-04 23:57:35 -07:00
bicarus 96950b6b4e rawinput: reimplement analog relative mode and delay (#674)
## Link to GitHub Issue or related Pull Request, if one exists
#181 

## Description of change
Implement analog relative mode and delay option using a new
millisecond-based algorithm.

## Testing
WIP
2026-05-04 22:50:11 -07:00
61 changed files with 4186 additions and 298 deletions
+36 -2
View File
@@ -607,6 +607,9 @@ set(SOURCE_FILES ${SOURCE_FILES}
reader/structuredmessage.cpp
reader/crypt.cpp
# sdk
sdk/sdk.cpp
# stubs
stubs/stubs.cpp
@@ -633,6 +636,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
util/time.cpp
util/cpuutils.cpp
util/netutils.cpp
util/precise_timer.cpp
util/sysutils.cpp
util/lz77.cpp
util/tapeled.cpp
@@ -860,6 +864,36 @@ if(NOT MSVC)
set_target_properties(spicetools_stubs_cpusbxpkm PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32")
endif()
# sdk_sample_v0_flat_c.dll (32 bit)
set(SOURCE_FILES sdk/sample/v0/flat_c/v0_flat_c.c)
add_library(spicetools_sdk_sample_v0_flat_c_32 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/flat_c/v0_flat_c.def)
set_target_properties(spicetools_sdk_sample_v0_flat_c_32 PROPERTIES PREFIX "")
set_target_properties(spicetools_sdk_sample_v0_flat_c_32 PROPERTIES OUTPUT_NAME "sdk_sample_v0_flat_c")
if(NOT MSVC)
set_target_properties(spicetools_sdk_sample_v0_flat_c_32 PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32")
endif()
# sdk_sample_v0_flat_c.dll (64 bit)
set(SOURCE_FILES sdk/sample/v0/flat_c/v0_flat_c.c)
add_library(spicetools_sdk_sample_v0_flat_c_64 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/flat_c/v0_flat_c.def)
set_target_properties(spicetools_sdk_sample_v0_flat_c_64 PROPERTIES PREFIX "")
set_target_properties(spicetools_sdk_sample_v0_flat_c_64 PROPERTIES OUTPUT_NAME "sdk_sample_v0_flat_c")
if(NOT MSVC)
set_target_properties(spicetools_sdk_sample_v0_flat_c_64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
endif()
# sdk_sample_v0_cpp.dll (64 bit)
set(SOURCE_FILES sdk/sample/v0/cpp/v0_cpp.cpp)
add_library(spicetools_sdk_sample_v0_cpp_64 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/cpp/v0_cpp.def)
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES PREFIX "")
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES OUTPUT_NAME "sdk_sample_v0_cpp")
if(NOT MSVC)
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
endif()
# output directories
####################
@@ -869,14 +903,14 @@ set_target_properties(spicetools_cfg spicetools_cfg_linux
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools")
# output 32bit
set_target_properties(spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_kbt spicetools_stubs_kld spicetools_stubs_cpusbxpkm
set_target_properties(spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_kbt spicetools_stubs_kld spicetools_stubs_cpusbxpkm spicetools_sdk_sample_v0_flat_c_32
PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive32"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32")
# output 64bit
set_target_properties(spicetools_spice64 spicetools_spice64_linux spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvcuda spicetools_stubs_nvcuvid spicetools_stubs_nvEncodeAPI64
set_target_properties(spicetools_spice64 spicetools_spice64_linux spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvcuda spicetools_stubs_nvcuvid spicetools_stubs_nvEncodeAPI64 spicetools_sdk_sample_v0_flat_c_64 spicetools_sdk_sample_v0_cpp_64
PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive64"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64"
+10
View File
@@ -283,6 +283,16 @@ Python is the only one that is fully spec compliant.
Other libraries may be missing features and contain bugs; please feel free to
contribute code to fill the gaps if you work on a project using these libraries.
## Spice SDK (for DLL hooks / plugins)
As an alternative to Spice API, Spice SDK is available.
This is a flat C, header-only library that can be included in your DLL. Once
initialized, you can call directly into helper routines provided by Spicetools
executable, making low-latency and high-throughput interactions possible.
Check the documentation on the wiki for more details.
## License
Unless otherwise noted, all files are licensed under the GPLv3.
See the LICENSE file for the full license text.
+6 -3
View File
@@ -1,14 +1,16 @@
#include "mdxf.h"
#include "mdxf_poll.h"
#include <mutex>
#include "avs/game.h"
#include "games/ddr/ddr.h"
#include "games/ddr/io.h"
#include "launcher/launcher.h"
#include "rawinput/rawinput.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include <mutex>
#define MDFX_DEBUG_VERBOSE 0
@@ -109,9 +111,10 @@ static void mdxf_thread_start() {
MDXF_THREAD = std::thread([] {
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
timeutils::PreciseSleepTimer timer;
while (MDXF_THREAD_RUNNING.load(std::memory_order_acquire)) {
mdxf_poll(false);
std::this_thread::sleep_for(THREAD_PERIOD);
timer.sleep(THREAD_PERIOD);
}
});
}
@@ -512,4 +515,4 @@ acio::MDXFModule::~MDXFModule() {
if (IS_THREAD_NEEDED) {
mdxf_thread_stop();
}
}
}
+1
View File
@@ -1,5 +1,6 @@
#pragma once
#include <cstdint>
#include <string>
#include "util/circular_buffer.h"
+3 -1
View File
@@ -5,6 +5,7 @@
#include "games/sdvx/sdvx.h"
#include "misc/eamuse.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
using namespace acioemu;
@@ -46,11 +47,12 @@ ICCADevice::ICCADevice(bool flip_order, bool keypad_thread, uint8_t node_count)
// keypad thread for faster polling
if (keypad_thread) {
this->keypad_thread = new std::thread([this]() {
timeutils::PreciseSleepTimer timer;
while (this->cards) {
for (int unit = 0; unit < this->node_count; unit++) {
this->update_keypad(unit, false);
}
Sleep(7);
timer.sleep(7);
}
});
}
+4
View File
@@ -6,6 +6,7 @@
#include "launcher/launcher.h"
#include "games/io.h"
#include "util/utils.h"
#include "acio/mdxf/mdxf_poll.h"
using namespace std::placeholders;
using namespace rapidjson;
@@ -104,6 +105,7 @@ namespace api::modules {
for (auto &analog : *this->analogs) {
analog.override_enabled = false;
}
mdxf_poll(true);
}
return;
}
@@ -148,6 +150,7 @@ namespace api::modules {
if (analog.getName() == name) {
analog.override_state = CLAMP(state, 0.f, 1.f);
analog.override_enabled = true;
mdxf_poll(true);
return true;
}
}
@@ -167,6 +170,7 @@ namespace api::modules {
for (auto &analog : *this->analogs) {
if (analog.getName() == name) {
analog.override_enabled = false;
mdxf_poll(true);
return true;
}
}
+4
View File
@@ -6,6 +6,7 @@
#include "launcher/launcher.h"
#include "games/io.h"
#include "util/utils.h"
#include "acio/mdxf/mdxf_poll.h"
using namespace std::placeholders;
using namespace rapidjson;
@@ -107,6 +108,7 @@ namespace api::modules {
for (auto &button : *this->buttons) {
button.override_enabled = false;
}
mdxf_poll(true);
}
return;
}
@@ -153,6 +155,7 @@ namespace api::modules {
GameAPI::Buttons::BUTTON_PRESSED : GameAPI::Buttons::BUTTON_NOT_PRESSED;
button.override_velocity = CLAMP(state, 0.f, 1.f);
button.override_enabled = true;
mdxf_poll(true);
return true;
}
}
@@ -172,6 +175,7 @@ namespace api::modules {
for (auto &button : *this->buttons) {
if (button.getName() == name) {
button.override_enabled = false;
mdxf_poll(true);
return true;
}
}
+4 -2
View File
@@ -7,6 +7,7 @@
#include "avs/game.h"
#include "external/rapidjson/document.h"
#include "misc/eamuse.h"
#include "util/precise_timer.h"
using namespace std::placeholders;
using namespace rapidjson;
@@ -58,6 +59,7 @@ namespace api::modules {
// get params
auto keypad = req.params[0].GetUint();
auto input = std::string(req.params[1].GetString());
timeutils::PreciseSleepTimer timer;
// process all chars
for (auto c : input) {
@@ -91,11 +93,11 @@ namespace api::modules {
// set
eamuse_set_keypad_overrides(keypad, state);
Sleep(sleep_time);
timer.sleep(sleep_time);
// unset
eamuse_set_keypad_overrides(keypad, 0);
Sleep(sleep_time);
timer.sleep(sleep_time);
}
}
+3 -1
View File
@@ -5,6 +5,7 @@
#include <utility>
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
@@ -16,6 +17,7 @@ namespace api {
controller->init_state(this->state);
this->thread = new std::thread([this] () {
log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud);
timeutils::PreciseSleepTimer timer;
// read buffer
uint8_t read_buffer[16*1024];
@@ -160,7 +162,7 @@ namespace api {
// slow down on reconnect
if (this->running) {
Sleep(retry_time);
timer.sleep(retry_time);
}
}
});
+37 -37
View File
@@ -39,8 +39,8 @@
<button name="VEFX" vkey="10" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
</buttons>
<analogs>
<analog name="Turntable P1" devid="Player 1" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Turntable P2" devid="Player 2" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Turntable P1" devid="Player 1" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Turntable P2" devid="Player 2" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs>
<lights>
<light name="P1 1" devid="Player 1" index="0"/>
@@ -117,8 +117,8 @@
<keypad_buttons/>
<!-- Assumes analog mode. Need to invert direction since this FW is designed for INF. -->
<analogs>
<analog name="Turntable P1" devid="arcin (1p)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="true" smoothing="false" multiplier="1"/>
<analog name="Turntable P2" devid="arcin (2p)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="true" smoothing="false" multiplier="1"/>
<analog name="Turntable P1" devid="arcin (1p)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="true" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Turntable P2" devid="arcin (2p)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="true" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs>
<lights>
<light name="P1 1" devid="arcin (1p)" index="0"/>
@@ -144,34 +144,34 @@
<buttons/>
<keypad_buttons/>
<analogs>
<analog name="Key 1" devid="Analogs key 01-28" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 2" devid="Analogs key 01-28" index="5" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 3" devid="Analogs key 01-28" index="6" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 4" devid="Analogs key 01-28" index="7" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 5" devid="Analogs key 01-28" index="8" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 6" devid="Analogs key 01-28" index="9" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 7" devid="Analogs key 01-28" index="10" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 8" devid="Analogs key 01-28" index="11" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 9" devid="Analogs key 01-28" index="12" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 10" devid="Analogs key 01-28" index="13" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 11" devid="Analogs key 01-28" index="14" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 12" devid="Analogs key 01-28" index="15" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 13" devid="Analogs key 01-28" index="16" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 14" devid="Analogs key 01-28" index="17" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 15" devid="Analogs key 01-28" index="18" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 16" devid="Analogs key 01-28" index="19" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 17" devid="Analogs key 01-28" index="20" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 18" devid="Analogs key 01-28" index="21" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 19" devid="Analogs key 01-28" index="22" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 20" devid="Analogs key 01-28" index="23" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 21" devid="Analogs key 01-28" index="24" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 22" devid="Analogs key 01-28" index="25" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 23" devid="Analogs key 01-28" index="26" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 24" devid="Analogs key 01-28" index="27" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 25" devid="Analogs key 01-28" index="28" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 26" devid="Analogs key 01-28" index="29" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 27" devid="Analogs key 01-28" index="30" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 28" devid="Analogs key 01-28" index="31" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Key 1" devid="Analogs key 01-28" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 2" devid="Analogs key 01-28" index="5" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 3" devid="Analogs key 01-28" index="6" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 4" devid="Analogs key 01-28" index="7" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 5" devid="Analogs key 01-28" index="8" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 6" devid="Analogs key 01-28" index="9" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 7" devid="Analogs key 01-28" index="10" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 8" devid="Analogs key 01-28" index="11" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 9" devid="Analogs key 01-28" index="12" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 10" devid="Analogs key 01-28" index="13" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 11" devid="Analogs key 01-28" index="14" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 12" devid="Analogs key 01-28" index="15" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 13" devid="Analogs key 01-28" index="16" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 14" devid="Analogs key 01-28" index="17" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 15" devid="Analogs key 01-28" index="18" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 16" devid="Analogs key 01-28" index="19" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 17" devid="Analogs key 01-28" index="20" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 18" devid="Analogs key 01-28" index="21" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 19" devid="Analogs key 01-28" index="22" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 20" devid="Analogs key 01-28" index="23" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 21" devid="Analogs key 01-28" index="24" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 22" devid="Analogs key 01-28" index="25" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 23" devid="Analogs key 01-28" index="26" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 24" devid="Analogs key 01-28" index="27" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 25" devid="Analogs key 01-28" index="28" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 26" devid="Analogs key 01-28" index="29" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 27" devid="Analogs key 01-28" index="30" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 28" devid="Analogs key 01-28" index="31" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs>
<lights>
<light name="Key 1 R" devid="Lights 01-14" index="0"/>
@@ -277,9 +277,9 @@
</buttons>
<keypad_buttons/>
<analogs>
<analog name="Guitar P1 Wail X" devid="Gitaller P1" index="1" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Guitar P1 Wail Y" devid="Gitaller P1" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Guitar P1 Wail Z" devid="Gitaller P1" index="5" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="Guitar P1 Wail X" devid="Gitaller P1" index="1" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Guitar P1 Wail Y" devid="Gitaller P1" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Guitar P1 Wail Z" devid="Gitaller P1" index="5" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs>
<lights/>
<!-- Gen 1 FAUCETWO in HID mode; should also work for later gens -->
@@ -296,8 +296,8 @@
</buttons>
<keypad_buttons/>
<analogs>
<analog name="VOL-L" devid="Buttons (MI_01)" index="1" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="VOL-R" devid="Buttons (MI_01)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1"/>
<analog name="VOL-L" devid="Buttons (MI_01)" index="1" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="VOL-R" devid="Buttons (MI_01)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs>
<lights>
<light name="BT-A" devid="Lights (MI_02)" index="24"/>
+10 -14
View File
@@ -68,8 +68,8 @@ DIST_FOLDER="./dist"
DIST_NAME="spice2x-$(date +%y)-$(date +%m)-$(date +%d).zip"
DIST_NAME_EXTRAS="spice2x-$(date +%y)-$(date +%m)-$(date +%d)-full.zip"
DIST_COMMENT=${DIST_NAME}$'\n'"$GIT_BRANCH - $GIT_HEAD"$'\nThank you for playing.'
TARGETS_32="spicetools_stubs_kbt spicetools_stubs_kld spicetools_cfg spicetools_cfg_linux spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_cpusbxpkm"
TARGETS_64="spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvEncodeAPI64 spicetools_stubs_nvcuvid spicetools_stubs_nvcuda spicetools_spice64 spicetools_spice64_linux"
TARGETS_32="spicetools_stubs_kbt spicetools_stubs_kld spicetools_cfg spicetools_cfg_linux spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_cpusbxpkm spicetools_sdk_sample_v0_flat_c_32"
TARGETS_64="spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvEncodeAPI64 spicetools_stubs_nvcuvid spicetools_stubs_nvcuda spicetools_spice64 spicetools_spice64_linux spicetools_sdk_sample_v0_flat_c_64 spicetools_sdk_sample_v0_cpp_64"
TARGETS_XP32="spicetools_cfg spicetools_spice"
TARGETS_XP64="spicetools_spice64"
@@ -269,6 +269,8 @@ rm -rf ${OUTDIR_EXTRAS}
mkdir -p ${OUTDIR_EXTRAS}
mkdir -p ${OUTDIR_EXTRAS}/largeaddressaware
mkdir -p ${OUTDIR_EXTRAS}/linux
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/32
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/64
if ((BUILD_XP > 0))
then
mkdir -p ${OUTDIR_EXTRAS}/winxp
@@ -281,16 +283,8 @@ then
cp ${BUILDDIR_32}/spicetools/spicecfg-pdb.pdb ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/spice-pdb.exe ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/spice-pdb.pdb ${OUTDIR} 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/kbt.dll ${OUTDIR}/stubs/32 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/kld.dll ${OUTDIR}/stubs/32 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/spice64-pdb.exe ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/spice64-pdb.pdb ${OUTDIR} 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/kbt.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/kld.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/nvEncodeAPI64.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/nvcuda.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/nvcuvid.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/cpusbxpkm.dll ${OUTDIR}/stubs/32 2>/dev/null
else
# release files
cp ${BUILDDIR_32}/spicetools/spicecfg.exe ${OUTDIR} 2>/dev/null
@@ -298,16 +292,15 @@ else
cp ${BUILDDIR_32}/spicetools/32/spice.exe ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/spice_laa.exe ${OUTDIR_EXTRAS}/largeaddressaware/spice.exe 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/spice_linux.exe ${OUTDIR_EXTRAS}/linux/spice.exe 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/kbt.dll ${OUTDIR}/stubs/32 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/kld.dll ${OUTDIR}/stubs/32 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/spice64.exe ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/spice64_linux.exe ${OUTDIR_EXTRAS}/linux/spice64.exe 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/kbt.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/kld.dll ${OUTDIR}/stubs/64 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/nvEncodeAPI64.dll ${OUTDIR}/stubs/64 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/nvcuda.dll ${OUTDIR}/stubs/64 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/nvcuvid.dll ${OUTDIR}/stubs/64 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/cpusbxpkm.dll ${OUTDIR}/stubs/32 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/sdk_sample_v0_flat_c.dll ${OUTDIR_EXTRAS}/sdk/samples/32/v0_flat_c.dll 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/sdk_sample_v0_flat_c.dll ${OUTDIR_EXTRAS}/sdk/samples/64/v0_flat_c.dll 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/sdk_sample_v0_cpp.dll ${OUTDIR_EXTRAS}/sdk/samples/64/v0_cpp.dll 2>/dev/null
if ((BUILD_XP > 0))
then
cp ${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe ${OUTDIR_EXTRAS}/winxp 2>/dev/null
@@ -326,6 +319,9 @@ then
echo "WARNING: Couldn't get git to create the archive. Is this a git repository?"
fi
# sdk headers
cp -r ./sdk/include ${OUTDIR_EXTRAS}/sdk
# copy resources
rm -rf ${OUTDIR_EXTRAS}/api
mkdir ${OUTDIR_EXTRAS}/api
+2 -2
View File
@@ -1,7 +1,7 @@
docker build --pull external/docker -t spicetools/deps
docker build --build-context gitroot=%cd%/../../.git . -t spicetools/spice
docker run --rm -it -v %cd%/dist:/src/src/spice2x/dist -v %cd%/bin:/src/src/spice2x/bin -v %cd%/.ccache:/src/src/spice2x/.ccache spicetools/spice %*
docker run --rm -i -v %cd%/dist:/src/src/spice2x/dist -v %cd%/bin:/src/src/spice2x/bin -v %cd%/.ccache:/src/src/spice2x/.ccache spicetools/spice %*
@REM to generate PDBs, set DEBUG to 1 in build_all.sh, place cv2pdb in external\cv2pdb, and run below
@REM external\cv2pdb\cv2pdb.exe bin\spice2x\spicecfg.exe bin\spice2x\spicecfg-pdb.exe bin\spice2x\spicecfg-pdb.pdb
@REM external\cv2pdb\cv2pdb.exe bin\spice2x\spice.exe bin\spice2x\spice-pdb.exe bin\spice2x\spice-pdb.pdb
@REM external\cv2pdb\cv2pdb.exe bin\spice2x\spice64.exe bin\spice2x\spice64-pdb.exe bin\spice2x\spice64-pdb.pdb
@REM external\cv2pdb\cv2pdb.exe bin\spice2x\spice64.exe bin\spice2x\spice64-pdb.exe bin\spice2x\spice64-pdb.pdb
+74 -2
View File
@@ -237,7 +237,8 @@ float Analog::applyDeadzone(float raw_value) {
deadzone = -deadzone;
}
if (getType() != GameAPI::Analogs::AnalogType::LinearPositive) {
// relative mode assumes that user is using a stick, so center is neutral regardless of analog type
if (getType() != GameAPI::Analogs::AnalogType::LinearPositive || isRelativeMode()) {
// calculate values
const auto delta = value - 0.5f;
@@ -290,4 +291,75 @@ float Analog::applyDeadzone(float raw_value) {
}
}
return value;
}
}
float Analog::getRelativeModeValue(float raw_value) {
const auto now = get_performance_seconds();
auto delta_time = now - this->rel_mode_last_read_time_s;
if (this->rel_mode_last_read_time_s != 0.f) {
// some heuristics to prevent huge jumps:
// * if we went for more than 250ms without polls, discard it (e.g., during loading screens)
// * cap the delta at 100ms to prevent huge jumps in case of very infrequent polling
if (delta_time < 0.f || 0.25f < delta_time) {
delta_time = 0.f;
} else if (delta_time > 0.1f) {
delta_time = 0.1f;
}
// scale [0, 1] to [-1, 1] to simplify calculations
const auto delta_raw_value = (raw_value - 0.5f) * 2.f;
// target is one revolution per second at max speed at 1.0 sensitivity
auto adjusted_delta_value = delta_raw_value * delta_time;
// multiplier / divisor
if (this->getMultiplier() > 1) {
adjusted_delta_value *= this->getMultiplier();
} else if (this->getMultiplier() < -1) {
adjusted_delta_value /= -this->getMultiplier();
}
// sensitivity
if (this->isSensitivitySet()) {
adjusted_delta_value *= this->getSensitivity();
}
// calculate the new absolute value
this->rel_mode_absolute_value += adjusted_delta_value;
}
// update for next poll
this->rel_mode_last_read_time_s = now;
this->rel_mode_absolute_value = normalizeAnalogValue(this->rel_mode_absolute_value);
return this->rel_mode_absolute_value;
}
float Analog::getDelayedValue(float raw_value) {
const double delay_ms = static_cast<double>(this->getDelayMs());
if (delay_ms == 0.0) {
return raw_value;
}
// always push a new value
const auto now = get_performance_milliseconds();
this->delayed_inputs.emplace(now, raw_value);
// drain the queue down to reasonable length to prevent unconstrained growth
// this would accommodate 1 second at ~1000Hz which is overkill
// (UI only allows for 250ms of delay)
while (this->delayed_inputs.size() > 1024) {
this->delayed_inputs.pop();
}
// pop until we find the oldest value still inside the delay window
while (this->delayed_inputs.size() > 1) {
const auto delta_t = now - this->delayed_inputs.front().time_in_ms;
if (delta_t <= delay_ms) {
break;
}
this->delayed_inputs.pop();
}
return this->delayed_inputs.front().value;
}
+40
View File
@@ -1,5 +1,6 @@
#pragma once
#include <cstdint>
#include <array>
#include <string>
#include <cmath>
@@ -34,6 +35,11 @@ struct AnalogMovingAverage {
float cosine;
};
struct AnalogDelayEntry {
double time_in_ms;
float value;
};
class Analog {
private:
std::string name;
@@ -63,6 +69,15 @@ private:
float divisor_previous_value = 0.5f;
unsigned short divisor_region = 0;
// relative input mode
float rel_mode_absolute_value = 0.5f;
float rel_mode_last_read_time_s = 0.f;
bool relative_mode = false;
// delay
uint32_t delay_ms = 0;
std::queue<AnalogDelayEntry> delayed_inputs;
float calculateAngularDifference(float old_rads, float new_rads);
float normalizeAngle(float rads);
float normalizeAnalogValue(float value);
@@ -83,6 +98,8 @@ public:
float applyAngularSensitivity(float raw_rads);
float applyMultiplier(float raw_value);
float applyDeadzone(float raw_value);
float getRelativeModeValue(float raw_value);
float getDelayedValue(float raw_value);
inline bool isSet() {
if (this->override_enabled) {
@@ -98,7 +115,9 @@ public:
smoothing = false;
deadzone_mirror = false;
setMultiplier(1);
setRelativeMode(false);
setLastState(0.5f);
setDelayMs(0);
}
inline void clearBindings() {
@@ -195,6 +214,19 @@ public:
this->last_state = last_state;
}
inline bool isRelativeMode() const {
return this->relative_mode;
}
inline void setRelativeMode(bool relative_mode) {
if (relative_mode) {
this->smoothing = false;
}
this->relative_mode = relative_mode;
this->rel_mode_absolute_value = 0.5f;
this->rel_mode_last_read_time_s = 0.f;
}
inline GameAPI::Analogs::AnalogType getType() const {
return this->type;
}
@@ -202,4 +234,12 @@ public:
inline void setType(GameAPI::Analogs::AnalogType type) {
this->type = type;
}
inline uint32_t getDelayMs() const {
return this->delay_ms;
}
inline void setDelayMs(uint32_t delay_ms) {
this->delay_ms = delay_ms;
}
};
+34 -21
View File
@@ -679,38 +679,45 @@ float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, rawinput::D
}
// deadzone
// do not apply deadzone to circular analogs since it doesn't make sense
if (analog.isDeadzoneSet() && analog.getType() != AnalogType::Circular) {
// do not apply deadzone to circular analogs since it doesn't make sense (except in relative mode)
if (analog.isDeadzoneSet() &&
(analog.getType() != AnalogType::Circular || analog.isRelativeMode())) {
value = analog.applyDeadzone(value);
}
if (analog.getType() == AnalogType::Circular) {
// integer multiplier
value = analog.applyMultiplier(value);
// smoothing/sensitivity
if (analog.getSmoothing() || analog.isSensitivitySet()) {
float rads = value * (float) M_TAU;
if (analog.isRelativeMode()) {
value = analog.getRelativeModeValue(value);
// smoothing
if (analog.getSmoothing()) {
} else {
// integer multiplier
value = analog.applyMultiplier(value);
// preserve direction
if (rads >= M_TAU) {
rads -= 0.0001f;
// smoothing/sensitivity
if (analog.getSmoothing() || analog.isSensitivitySet()) {
float rads = value * (float) M_TAU;
// smoothing
if (analog.getSmoothing()) {
// preserve direction
if (rads >= M_TAU) {
rads -= 0.0001f;
}
// calculate angle
rads = analog.getSmoothedValue(rads);
}
// calculate angle
rads = analog.getSmoothedValue(rads);
}
// sensitivity
if (analog.isSensitivitySet()) {
rads = analog.applyAngularSensitivity(rads);
}
// sensitivity
if (analog.isSensitivitySet()) {
rads = analog.applyAngularSensitivity(rads);
// apply to value
value = rads * (float) M_1_TAU;
}
// apply to value
value = rads * (float) M_1_TAU;
}
} else {
// sensitivity
@@ -760,6 +767,12 @@ float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, rawinput::D
value = std::clamp(value, 0.f, 1.f);
}
}
// delay
if (analog.getDelayMs() > 0) {
value = analog.getDelayedValue(value);
}
break;
}
case rawinput::MIDI: {
+22
View File
@@ -252,6 +252,8 @@ bool Config::addGame(Game &game) {
bool invert = false;
bool smoothing = false;
int multiplier = 1;
bool relative_mode = false;
uint32_t delay = 0;
tinyxml2::XMLError err1 = gameAnalogNode->QueryIntAttribute("index", &index);
gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity);
gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone);
@@ -259,6 +261,8 @@ bool Config::addGame(Game &game) {
gameAnalogNode->QueryBoolAttribute("invert", &invert);
gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing);
gameAnalogNode->QueryIntAttribute("multiplier", &multiplier);
gameAnalogNode->QueryBoolAttribute("relative", &relative_mode);
gameAnalogNode->QueryUnsignedAttribute("delay_ms", &delay);
const char *devid = gameAnalogNode->Attribute("devid");
if (err1 != tinyxml2::XMLError::XML_SUCCESS || !devid) {
@@ -273,6 +277,8 @@ bool Config::addGame(Game &game) {
gameAnalogNode->SetAttribute("invert", it.getInvert());
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
gameAnalogsNode->InsertEndChild(gameAnalogNode);
} else {
it.setIndex(static_cast<unsigned short int>(index));
@@ -283,6 +289,8 @@ bool Config::addGame(Game &game) {
it.setInvert(invert);
it.setSmoothing(smoothing);
it.setMultiplier(multiplier);
it.setRelativeMode(relative_mode);
it.setDelayMs(delay);
}
} else {
gameAnalogNode = this->configFile.NewElement("analog");
@@ -294,6 +302,8 @@ bool Config::addGame(Game &game) {
gameAnalogNode->SetAttribute("invert", it.getInvert());
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
gameAnalogNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
gameAnalogsNode->InsertEndChild(gameAnalogNode);
}
@@ -447,6 +457,8 @@ bool Config::addGame(Game &game) {
gameAnalogNode->SetAttribute("invert", it.getInvert());
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
gameAnalogsNode->InsertEndChild(gameAnalogNode);
}
@@ -679,6 +691,8 @@ bool Config::updateBinding(const Game &game, const Analog &analog) {
gameAnalogNode->SetAttribute("invert", analog.getInvert());
gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier());
gameAnalogNode->SetAttribute("relative", analog.isRelativeMode());
gameAnalogNode->SetAttribute("delay_ms", analog.getDelayMs());
gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str());
} else {
gameAnalogNode = this->configFile.NewElement("analog");
@@ -689,6 +703,8 @@ bool Config::updateBinding(const Game &game, const Analog &analog) {
gameAnalogNode->SetAttribute("invert", analog.getInvert());
gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier());
gameAnalogNode->SetAttribute("relative", analog.isRelativeMode());
gameAnalogNode->SetAttribute("delay_ms", analog.getDelayMs());
gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str());
gameAnalogsNode->InsertEndChild(gameAnalogNode);
}
@@ -1125,6 +1141,8 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
bool invert = false;
bool smoothing = false;
int multiplier = 1;
bool relative_mode = false;
uint32_t delay = 0;
gameAnalogNode->QueryIntAttribute("index", &index);
gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity);
gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone);
@@ -1132,6 +1150,8 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
gameAnalogNode->QueryBoolAttribute("invert", &invert);
gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing);
gameAnalogNode->QueryIntAttribute("multiplier", &multiplier);
gameAnalogNode->QueryBoolAttribute("relative", &relative_mode);
gameAnalogNode->QueryUnsignedAttribute("delay_ms", &delay);
const char *devid = gameAnalogNode->Attribute("devid");
// create analog and add to list
@@ -1143,6 +1163,8 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
analog.setInvert(invert);
analog.setSmoothing(smoothing);
analog.setMultiplier(multiplier);
analog.setRelativeMode(relative_mode);
analog.setDelayMs(delay);
if (devid) {
analog.setDeviceIdentifier(devid);
}
+4
View File
@@ -69,6 +69,8 @@ namespace overlay::windows {
el->SetAttribute("invert", analog.invert);
el->SetAttribute("smoothing", analog.smoothing);
el->SetAttribute("multiplier", analog.multiplier);
el->SetAttribute("relative", analog.relative_mode);
el->SetAttribute("delay_ms", analog.delay_ms);
parent->InsertEndChild(el);
}
@@ -90,6 +92,8 @@ namespace overlay::windows {
el->QueryBoolAttribute("invert", &a.invert);
el->QueryBoolAttribute("smoothing", &a.smoothing);
el->QueryIntAttribute("multiplier", &a.multiplier);
el->QueryBoolAttribute("relative", &a.relative_mode);
el->QueryUnsignedAttribute("delay_ms", &a.delay_ms);
return a;
}
+1
View File
@@ -20,6 +20,7 @@ Usage:
#include <memory>
#include <string>
#include <map>
#include <cstdint>
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+1
View File
@@ -39,6 +39,7 @@
#define ROBIN_HOOD_VERSION_PATCH 2 // for backwards-compatible bug fixes
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <functional>
+4 -2
View File
@@ -6,6 +6,7 @@
#include "util/logging.h"
#include "rawinput/rawinput.h"
#include "games/io.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include "io.h"
@@ -112,7 +113,7 @@ namespace games::ccj {
static std::chrono::time_point<std::chrono::steady_clock> lastModified = std::chrono::steady_clock::now();
static std::chrono::milliseconds debounceDuration(100);
auto currentTime = std::chrono::steady_clock::now();
bool pressed = get_async_secondary_mouse() & 0x8000;
bool pressed = get_async_secondary_mouse();
bool focused = GetForegroundWindow() == hWnd;
if (focused && MOUSE_TRACKBALL_USE_TOGGLE && pressed && (currentTime - lastModified > debounceDuration)) {
@@ -243,6 +244,7 @@ namespace games::ccj {
log_info("trackball", "thread start, use mouse: {}, toggle: {}", MOUSE_TRACKBALL, MOUSE_TRACKBALL_USE_TOGGLE);
tbThread = new std::thread([&] {
timeutils::PreciseSleepTimer timer;
while (tbThreadRunning) {
if (hWnd && wndProc) {
wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle);
@@ -251,7 +253,7 @@ namespace games::ccj {
if (!tbThreadRunning)
break;
std::this_thread::sleep_for(10ms);
timer.sleep(10);
}
});
}
+3 -1
View File
@@ -3,6 +3,7 @@
#include "cfg/api.h"
#include "rawinput/rawinput.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include "../ddr.h"
@@ -459,7 +460,8 @@ int games::ddr::DDRP3IOHandle::device_io(
if (nOutBufferSize >= 4) {
// cool down
Sleep(1);
static thread_local timeutils::PreciseSleepTimer timer;
timer.sleep(1);
// get controls as single variable (4 bytes)
auto &controls = *(uint32_t*) lpOutBuffer;
+3 -1
View File
@@ -2,6 +2,7 @@
#include "rawinput/rawinput.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include "../io.h"
@@ -255,7 +256,8 @@ namespace games::ddr {
case P4IO_IOCTL_GET_INPUTS: {
// Prevents this function from being called at its normal 2000-2500 kHz cadence and overloading the CPU, instead reduces it to 250 Hz
Sleep(4);
static thread_local timeutils::PreciseSleepTimer timer;
timer.sleep(4);
memset(lpOutBuffer, 0, 16);
auto controls = (uint32_t*) lpOutBuffer;
+3 -1
View File
@@ -7,6 +7,7 @@
#include "games/game.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/memutils.h"
#include "rgb_cam.h"
@@ -221,10 +222,11 @@ namespace games::drs {
void start_touch() {
std::thread t([] {
log_info("drs", "starting touch input thread");
timeutils::PreciseSleepTimer timer;
// main loop
while (TRUE) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
timer.sleep(1);
TOUCH_EVENTS.clear();
touch_get_events(TOUCH_EVENTS);
+3 -1
View File
@@ -2,6 +2,7 @@
#include "misc/eamuse.h"
#include "rawinput/rawinput.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include "iidx.h"
@@ -82,7 +83,8 @@ bool games::iidx::IIDXFMSerialHandle::FMSerialDevice::parse_msg(
}
// sleep - otherwise the IO thread will go too hard on the CPU
Sleep(1);
static thread_local timeutils::PreciseSleepTimer timer;
timer.sleep(1);
// generate message
auto msg = this->create_msg(msg_in, 0x2E);
+3 -1
View File
@@ -2,6 +2,7 @@
#include "misc/eamuse.h"
#include "rawinput/rawinput.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include "iidx.h"
@@ -82,7 +83,8 @@ bool games::iidx::BI2XSerialHandle::BI2XDevice::parse_msg(
}
// sleep - otherwise the IO thread will go too hard on the CPU
Sleep(1);
static thread_local timeutils::PreciseSleepTimer timer;
timer.sleep(1);
// generate message
auto msg = this->create_msg(msg_in, 0x2E);
+1 -36
View File
@@ -783,42 +783,7 @@ namespace games::iidx {
}
// return higher 8 bit
uint8_t result = (uint8_t) (ret_value >> 2);
// delay
if ((player == 0 && TT_DELAY_P1 > 0) ||
(player == 1 && TT_DELAY_P2 > 0)) {
static std::queue<std::pair<double, uint8_t>> delay_queue[2];
auto &queue = delay_queue[player];
const auto max_delta_ms =
static_cast<double>((player == 0) ? TT_DELAY_P1 : TT_DELAY_P2);
// always push a new value
const auto now = get_performance_milliseconds();
queue.push(std::make_pair(now, result));
// drain the queue down to reasonable length to prevent unconstrained growth
// this would accommodate 1 second at ~1000Hz
// (in reality all three iidx I/O emulation runs well under 500Hz)
while (queue.size() > 1024) {
queue.pop();
}
// pop until we find one that falls just under the time threshold
while (!queue.empty()) {
const auto delta_t = now - queue.front().first;
if (delta_t <= max_delta_ms) {
break;
}
queue.pop();
}
result = queue.front().second;
}
return result;
return (uint8_t)(ret_value >> 2);
}
unsigned char get_slider(uint8_t slider) {
-2
View File
@@ -31,8 +31,6 @@ namespace games::iidx {
extern bool NATIVE_TOUCH;
extern std::optional<std::string> SOUND_OUTPUT_DEVICE;
extern std::optional<std::string> ASIO_DRIVER;
extern uint32_t TT_DELAY_P1;
extern uint32_t TT_DELAY_P2;
extern uint8_t DIGITAL_TT_SENS;
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
extern std::optional<std::string> SCREEN_MODE;
+3 -1
View File
@@ -3,6 +3,7 @@
#if SPICE64 && !SPICE_XP
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include "mf_wrappers.h"
@@ -399,6 +400,7 @@ namespace games::iidx {
void IIDXLocalCamera::CreateThread() {
// Create thread
m_drawThread = new std::thread([this]() {
timeutils::PreciseSleepTimer timer;
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
double accumulator = 0.0;
@@ -412,7 +414,7 @@ namespace games::iidx {
accumulator -= 1.0;
floorFrameTimeMicroSec += 1;
}
std::this_thread::sleep_for(std::chrono::microseconds(floorFrameTimeMicroSec));
timer.sleep(std::chrono::microseconds(floorFrameTimeMicroSec));
}
});
}
+3 -1
View File
@@ -15,6 +15,7 @@
#include "touch/touch.h"
#include "util/libutils.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#define POKE_NATIVE_TOUCH 0
@@ -200,6 +201,7 @@ namespace games::iidx::poke {
// create new thread
THREAD_RUNNING = true;
THREAD = new std::thread([] {
timeutils::PreciseSleepTimer timer;
// log
log_info("poke", "enabled");
@@ -319,7 +321,7 @@ namespace games::iidx::poke {
}
// slow down
Sleep(50);
timer.sleep(50);
}
return nullptr;
+84 -11
View File
@@ -19,7 +19,7 @@
namespace games::jb {
// touch stuff
bool TOUCH_LEGACY_BOX = false;
JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
static bool TOUCH_ENABLE = false;
static bool TOUCH_ATTACHED = false;
static bool IS_PORTRAIT = true;
@@ -78,7 +78,7 @@ namespace games::jb {
// 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_LEGACY_BOX) {
if (TOUCH_ALGORITHM == Legacy) {
auto offset = IS_PORTRAIT ? 580 : 0;
for (auto &tp : TOUCH_POINTS) {
@@ -94,6 +94,17 @@ namespace games::jb {
}
} 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;
@@ -110,19 +121,66 @@ namespace games::jb {
y_relative -= 8;
}
if (x_relative < 0 || y_relative < 0) {
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;
}
// x_hitbox and y_hitbox is relative to top-left pixel of each button
int x_index = x_relative / JB_BUTTON_HITBOX;
int x_hitbox = x_relative % JB_BUTTON_HITBOX;
int y_index = y_relative / JB_BUTTON_HITBOX;
int y_hitbox = y_relative % JB_BUTTON_HITBOX;
// check if the gap was touched
if (x_hitbox > JB_BUTTON_SIZE || y_hitbox > JB_BUTTON_SIZE) {
continue;
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
@@ -193,6 +251,21 @@ namespace games::jb {
// 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;
}
// enable debug logging of gftools
HMODULE gftools = libutils::try_module("gftools.dll");
detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc(
+7 -1
View File
@@ -4,8 +4,14 @@
namespace games::jb {
enum JubeatTouchAlgorithm {
Legacy,
Improved,
AcAccurate
};
// touch stuff
extern bool TOUCH_LEGACY_BOX;
extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
extern bool TOUCH_STATE[16];
void touch_update();
+3 -1
View File
@@ -9,6 +9,7 @@
#include "misc/eamuse.h"
#include "touch/touch.h"
#include "util/logging.h"
#include "util/precise_timer.h"
namespace games::nost::poke {
@@ -63,6 +64,7 @@ namespace games::nost::poke {
// create new thread
THREAD_RUNNING = true;
THREAD = new std::thread([] {
timeutils::PreciseSleepTimer timer;
const DWORD touch_id = (DWORD)(0xFFFFFFFE);
const int swipe_anim_total_frames = 6;
@@ -178,7 +180,7 @@ namespace games::nost::poke {
}
// slow down
Sleep(30);
timer.sleep(30);
}
return nullptr;
+6 -2
View File
@@ -4,6 +4,7 @@
#include "touchpanel.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "touch/touch.h"
using namespace std::chrono_literals;
@@ -44,6 +45,9 @@ bool games::onpara::TouchPanelHandle::open(LPCWSTR lpFileName) {
}
int games::onpara::TouchPanelHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
static thread_local timeutils::PreciseSleepTimer timer;
DWORD i;
auto buffer = reinterpret_cast<uint8_t *>(lpBuffer);
@@ -67,7 +71,7 @@ int games::onpara::TouchPanelHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesT
enqueue_packet(report);
// prevent cpu bullying
std::this_thread::sleep_for(1ms);
timer.sleep(1);
}
// copy from output queue
@@ -133,4 +137,4 @@ bool games::onpara::TouchPanelHandle::close() {
log_info("touchpanel", "Closed COM1 (Touch Panel)");
return true;
}
}
+10 -10
View File
@@ -4,6 +4,8 @@
#include "hooks/devicehook.h"
#include "hooks/audio/backends/dsound/dsound_backend.h"
#include "util/detour.h"
#include "util/precise_timer.h"
#include "util/logging.h"
#include "touch.h"
@@ -11,20 +13,18 @@ static decltype(SleepEx) *SleepEx_orig;
static DWORD WINAPI SleepEx_hook(DWORD dwMilliseconds, BOOL bAltertable) {
/*
* Increase touch poll from ~110 FPS to ~500 FPS
*/
// increase touch poll from ~110 FPS to ~500 FPS (Sleep) or ~1000 FPS (Win10 high-res timer)
if (dwMilliseconds == 8) {
static bool initialized = false;
if (!initialized) {
initialized = true;
// if we only sleep for 1ms we also don't need the high priority RB sets
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
}
dwMilliseconds = 1;
}
// most calls from rb are actually non-alertable
if (!bAltertable) {
static thread_local timeutils::PreciseSleepTimer timer;
timer.sleep(dwMilliseconds);
return 0;
}
// call original
return SleepEx_orig(dwMilliseconds, bAltertable);
}
@@ -3,6 +3,7 @@
#include "hooks/audio/audio.h"
#include "hooks/audio/backends/wasapi/audio_client.h"
#include "hooks/audio/backends/wasapi/defs.h"
#include "util/precise_timer.h"
static REFERENCE_TIME WASAPI_TARGET_REFTIME = TARGET_REFTIME;
@@ -185,6 +186,7 @@ HRESULT WaveOutBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppDa
}
HRESULT WaveOutBackend::on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) {
bool written = false;
timeutils::PreciseSleepTimer timer;
// reset the dispatcher event
ResetEvent(this->dispatcher_event);
@@ -209,7 +211,7 @@ HRESULT WaveOutBackend::on_release_buffer(uint32_t num_frames_written, DWORD dwF
// avoid pegging the CPU
if (!written) {
Sleep(1);
timer.sleep(1);
}
}
+58 -14
View File
@@ -61,6 +61,7 @@ static std::condition_variable GRAPHICS_CAPTURE_CV[GRAPHICS_CAPTURE_SCREEN_NO] {
static std::optional<graphics_orientation> target_orientation_on_boot;
static UINT target_refresh_rate_on_boot = 0;
static std::optional<std::pair<uint32_t, uint32_t>> target_resolution_on_boot;
static bool monitor_settings_changed = false;
static bool monitor_layout_needs_reset = false;
@@ -163,6 +164,7 @@ static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
if (wintouchemu::INJECT_MOUSE_AS_WM_TOUCH) {
// drop mouse inputs since only wintouches should be used
switch (uMsg) {
case WM_MOUSEMOVE:
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_MBUTTONDOWN:
@@ -355,16 +357,24 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen");
bool is_gfdm_sub_window = games::gitadora::is_arena_model() && window_name.ends_with("SMALL");
// hide maximize button (prevent misaligned touches)
if ((is_tdj_sub_window && GRAPHICS_IIDX_WSUB) || is_sdvx_sub_window || is_gfdm_sub_window || is_popn_sub_window) {
// update style / ex-style
if (is_tdj_sub_window || is_sdvx_sub_window || is_gfdm_sub_window || is_popn_sub_window) {
// hide maximize button (prevent misaligned touches)
dwStyle &= ~(WS_MAXIMIZEBOX);
}
// mouse clicks become misaligned when resized
if (is_gfdm_sub_window) {
// mouse clicks become misaligned when resized
dwStyle &= ~(WS_SIZEBOX);
}
if ((is_tdj_sub_window || is_sdvx_sub_window || is_popn_sub_window) && GRAPHICS_WSUB_BORDERLESS) {
dwStyle &= ~(WS_OVERLAPPEDWINDOW);
// borderless
if (GRAPHICS_WINDOWED && GRAPHICS_WSUB_BORDERLESS) {
dwStyle &= ~(WS_OVERLAPPEDWINDOW);
}
// don't show the sub window on task bar / alt-tab targets
if (!GRAPHICS_WINDOWED) {
dwExStyle &= ~(WS_EX_APPWINDOW);
dwExStyle |= WS_EX_TOOLWINDOW;
}
}
if (is_sdvx_sub_window) {
@@ -1278,8 +1288,13 @@ void change_primary_monitor(const std::string &monitor_name) {
Sleep(2000);
}
void update_monitor(bool is_boot, std::optional<graphics_orientation> target_orientation, UINT target_refresh_rate) {
// note: all of this is only being done for the primary motnior
void update_monitor(
bool is_boot,
std::optional<graphics_orientation> target_orientation,
UINT target_refresh_rate,
std::optional<std::pair<uint32_t, uint32_t>> target_resolution) {
// note: all of this is only being done for the primary monitor
// get current settings
DEVMODEA dm = {};
@@ -1357,6 +1372,22 @@ void update_monitor(bool is_boot, std::optional<graphics_orientation> target_ori
needs_update = true;
}
if (target_resolution.has_value()) {
if (dm.dmPelsWidth != target_resolution.value().first ||
dm.dmPelsHeight != target_resolution.value().second) {
log_misc(
"graphics",
"current resolution {}, {} => desired resolution {}, {}",
dm.dmPelsWidth, dm.dmPelsHeight,
target_resolution.value().first, target_resolution.value().second);
dm.dmPelsWidth = target_resolution.value().first;
dm.dmPelsHeight = target_resolution.value().second;
needs_update = true;
}
}
if (!needs_update) {
// nothing to do
log_misc("graphics", "display settings are already up to date, no changes needed");
@@ -1393,17 +1424,30 @@ void update_monitor(bool is_boot, std::optional<graphics_orientation> target_ori
}
}
void update_monitor_on_boot(std::optional<graphics_orientation> target_orientation, UINT target_refresh_rate) {
void update_monitor_on_boot(
std::optional<graphics_orientation> target_orientation,
UINT target_refresh_rate,
std::optional<std::pair<uint32_t, uint32_t>> target_resolution) {
target_orientation_on_boot = target_orientation;
target_refresh_rate_on_boot = target_refresh_rate;
target_resolution_on_boot = target_resolution;
log_misc("graphics", "applying monitor updates at boot...");
update_monitor(true, target_orientation, target_refresh_rate);
update_monitor(
true,
target_orientation,
target_refresh_rate,
target_resolution);
}
void update_monitor_at_runtime() {
if (monitor_settings_changed) {
if (!GRAPHICS_WINDOWED && monitor_settings_changed) {
log_misc("graphics", "applying monitor updates at runtime as window regained focus...");
update_monitor(false, target_orientation_on_boot, target_refresh_rate_on_boot);
update_monitor(
false,
target_orientation_on_boot,
target_refresh_rate_on_boot,
target_resolution_on_boot);
}
}
+5 -1
View File
@@ -117,6 +117,10 @@ void graphics_load_windowed_subscreen_parameters();
void graphics_window_check_bounds_before_creation(int &x, int &y, const int width, const int height);
void change_primary_monitor(const std::string &monitor_name);
void update_monitor_on_boot(std::optional<graphics_orientation> target_orientation, UINT target_refresh_rate);
void update_monitor_on_boot(
std::optional<graphics_orientation> target_orientation,
UINT target_refresh_rate,
std::optional<std::pair<uint32_t, uint32_t>> target_resolution);
void update_monitor_at_runtime();
void reset_monitor_on_exit();
+49 -18
View File
@@ -98,6 +98,7 @@
#include "rawinput/rawinput.h"
#include "rawinput/touch.h"
#include "reader/reader.h"
#include "sdk/sdk.h"
#include "stubs/stubs.h"
#include "touch/touch.h"
#include "util/cpuutils.h"
@@ -107,6 +108,7 @@
#include "util/libutils.h"
#include "util/logging.h"
#include "util/peb.h"
#include "util/precise_timer.h"
#include "util/socd_cleaner.h"
#include "util/sysutils.h"
#include "util/tapeled.h"
@@ -383,6 +385,16 @@ int main_implementation(int argc, char *argv[]) {
monitor_orientation = ORIENTATION_CW;
}
std::optional<std::pair<uint32_t, uint32_t>> monitor_resolution;
if (options[launcher::Options::ChangeResolution].is_active()) {
std::pair<uint32_t, uint32_t> result;
if (parse_width_height(options[launcher::Options::ChangeResolution].value_text(), result)) {
monitor_resolution = result;
} else if (!cfg_run && !cfg::CONFIGURATOR_STANDALONE) {
log_fatal("launcher", "failed to parse -changeres");
}
}
if (options[launcher::Options::spice2x_NoD3D9DeviceHook].value_bool()) {
D3D9_DEVICE_HOOK_DISABLE = true;
// touch emulation gets disabled, might as well turn these on
@@ -518,12 +530,6 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::IIDXTDJMode].value_bool()) {
games::iidx::TDJ_MODE = true;
}
if (options[launcher::Options::IIDXTTDelayP1].is_active()) {
games::iidx::TT_DELAY_P1 = options[launcher::Options::IIDXTTDelayP1].value_uint32();
}
if (options[launcher::Options::IIDXTTDelayP2].is_active()) {
games::iidx::TT_DELAY_P2 = options[launcher::Options::IIDXTTDelayP2].value_uint32();
}
if (options[launcher::Options::spice2x_IIDXDigitalTTSensitivity].is_active()) {
games::iidx::DIGITAL_TT_SENS = (uint8_t)
options[launcher::Options::spice2x_IIDXDigitalTTSensitivity].value_uint32();
@@ -1152,8 +1158,18 @@ int main_implementation(int argc, char *argv[]) {
}
if (options[launcher::Options::spice2x_JubeatLegacyTouch].value_bool()) {
games::jb::TOUCH_LEGACY_BOX = true;
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Legacy;
}
if (options[launcher::Options::JubeatTouchAlgo].is_active()) {
if (options[launcher::Options::JubeatTouchAlgo].value_text() == "accurate") {
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 {
games::jb::TOUCH_ALGORITHM = games::jb::JubeatTouchAlgorithm::Improved;
}
}
if (options[launcher::Options::spice2x_RBTouchScale].is_active()) {
games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32();
}
@@ -1278,6 +1294,10 @@ int main_implementation(int argc, char *argv[]) {
games::loveplus::CAMERA_ENABLE = true;
}
if (options[launcher::Options::DisableHighResTimer].value_bool()) {
timeutils::TIMER_HACKS_DISABLE = true;
}
// API debugging
if (api_debug && !cfg::CONFIGURATOR_STANDALONE) {
API_CONTROLLER = std::make_unique<api::Controller>(api_port, api_pass, api_pretty);
@@ -1537,16 +1557,18 @@ int main_implementation(int argc, char *argv[]) {
}
// early hooks
for (auto &hook : early_hooks) {
log_info("launcher", "loading early hook DLL {}", hook);
libutils::print_dll_info(hook);
HMODULE module;
if (!(module = libutils::try_library(hook))) {
log_warning("launcher", "failed to load early hook {}: {}", hook, get_last_error_string());
deferredlogs::defer_error_messages({
fmt::format("failed to load early hook {}:", hook),
fmt::format(" {}", get_last_error_string())
});
if (!cfg_run) {
for (auto &hook : early_hooks) {
log_info("launcher", "loading early hook DLL {}", hook);
libutils::print_dll_info(hook);
HMODULE module;
if (!(module = libutils::try_library(hook))) {
log_warning("launcher", "failed to load early hook {}: {}", hook, get_last_error_string());
deferredlogs::defer_error_messages({
fmt::format("failed to load early hook {}:", hook),
fmt::format(" {}", get_last_error_string())
});
}
}
}
@@ -2177,7 +2199,7 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::PrimaryMonitor].is_active()) {
change_primary_monitor(options[launcher::Options::PrimaryMonitor].value_text());
}
update_monitor_on_boot(monitor_orientation, GRAPHICS_FORCE_REFRESH);
update_monitor_on_boot(monitor_orientation, GRAPHICS_FORCE_REFRESH, monitor_resolution);
// initialize raw input
RI_MGR = std::make_unique<rawinput::RawInputManager>();
@@ -2230,6 +2252,9 @@ int main_implementation(int argc, char *argv[]) {
hooks::lang::early_init();
}
// use high resolution timer for this process
timeutils::set_timer_resolution();
// load DLLs
avs::core::load_dll();
avs::ea3::load_dll();
@@ -2364,6 +2389,7 @@ int main_implementation(int argc, char *argv[]) {
});
} else {
bt5api_hook(module);
sdk::register_sdk_hooks(hook, module);
}
}
@@ -2398,6 +2424,9 @@ int main_implementation(int argc, char *argv[]) {
bt5api_init();
}
// init SDK
sdk::init_sdk_modules();
// API
if (api_enable || std::min(api_serial_port.size(), api_serial_baud.size()) > 0) {
API_CONTROLLER = std::make_unique<api::Controller>(api_port, api_pass, api_pretty);
@@ -2505,6 +2534,8 @@ int main_implementation(int argc, char *argv[]) {
bt5api_dispose();
}
sdk::fini_sdk_modules();
// stop raw input
RI_MGR.reset();
+39 -28
View File
@@ -655,30 +655,6 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.game_name = "Beatmania IIDX",
.category = "Game Options",
},
{
// IIDXTTDelayP1
.title = "IIDX TT Delay ms (Player 1)",
.name = "iidxttdelayp1",
.desc = "Delays turntable by number of milliseconds. "
"Delay will be *at most* the specified period; maximum error is dependent on the game's poll rate. "
"As usual, changing any option requires a restart. Default: 0 (no delay).",
.type = OptionType::Integer,
.setting_name = "(0-500)",
.game_name = "Beatmania IIDX",
.category = "Game Options (Advanced)",
},
{
// IIDXTTDelayP2
.title = "IIDX TT Delay ms (Player 2)",
.name = "iidxttdelayp2",
.desc = "Delays turntable by number of milliseconds. "
"Delay will be *at most* the specified period; maximum error is dependent on the game's poll rate. "
"As usual, changing any option requires a restart. Default: 0 (no delay).",
.type = OptionType::Integer,
.setting_name = "(0-500)",
.game_name = "Beatmania IIDX",
.category = "Game Options (Advanced)",
},
{
// spice2x_IIDXDigitalTTSensitivity
.title = "IIDX Digital TT Sensitivity",
@@ -1944,6 +1920,15 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{"3", "Landscape, Flipped"}
},
},
{
// ChangeResolution
.title = "Change Monitor Resolution",
.name = "changeres",
.desc = "Changes monitor resolution before booting the game.",
.type = OptionType::Text,
.setting_name = "1280,720",
.category = "Monitor"
},
{
.title = "AVS Log Level",
.name = "loglevel",
@@ -2303,16 +2288,32 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
},
{
// spice2x_JubeatLegacyTouch
.title = "JB Legacy Touch Targets",
.title = "JB Legacy Touch Targets (Deprecated - use -jubeattouchalgo instead)",
.name = "sp2x-jubeatlegacytouch",
.display_name = "jubeatlegacytouch",
.aliases= "jubeatlegacytouch",
.desc = "For touch screen players - use the legacy & less accurate grid-based layout for touch recognition, "
"instead of the new & more accurate touch targets. Default: off.",
.desc = "Deprecated - use -jubeattouchalgo instead.",
.type = OptionType::Bool,
.hidden = true,
.game_name = "Jubeat",
.category = "Game Options",
},
{
.title = "JB Touch Algorithm",
.name = "jubeattouchalgo",
.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"
"accurate - only touches within squares will trigger; gaps do nothing (for AC size touch screens)",
.type = OptionType::Enum,
.game_name = "Jubeat",
.category = "Game Options",
.elements = {
{"legacy", ""},
{"improved", ""},
{"accurate", ""},
},
},
{
// spice2x_RBTouchScale
.title = "RB Scale Touch Input",
@@ -2793,7 +2794,17 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Bool,
.game_name = "Otoca D'or",
.category = "Game Options (Peripherals)",
}
},
{
// DisableHighResTimer
.title = "Use Legacy Timers",
.name = "notimerhacks",
.desc = "Disables high resolution timers, reverting to legacy behavior. "
"Recommended that you leave this OFF for optimal input latency, "
"unless you're on a resource-constrained system (e.g., old cabinet PC)",
.type = OptionType::Bool,
.category = "Development"
},
};
const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) {
+3 -2
View File
@@ -69,8 +69,6 @@ namespace launcher {
IIDXAsioDriver,
IIDXBIO2FW,
IIDXTDJMode,
IIDXTTDelayP1,
IIDXTTDelayP2,
spice2x_IIDXDigitalTTSensitivity,
IIDXDigitalTTSocd,
spice2x_IIDXLDJForce720p,
@@ -201,6 +199,7 @@ namespace launcher {
LoadStubs,
AdjustOrientation,
spice2x_AutoOrientation,
ChangeResolution,
LogLevel,
EAAutomap,
EANetdump,
@@ -241,6 +240,7 @@ namespace launcher {
IIDXWindowedSubscreenBorderless,
IIDXWindowedSubscreenAlwaysOnTop,
spice2x_JubeatLegacyTouch,
JubeatTouchAlgo,
spice2x_RBTouchScale,
spice2x_AsioForceUnload,
spice2x_IIDXNoESpec,
@@ -281,6 +281,7 @@ namespace launcher {
LovePlusPrinterJPGQuality,
OptionConflictResolution,
OtocaCamHook,
DisableHighResTimer,
};
enum class OptionsCategory {
+3
View File
@@ -14,6 +14,7 @@
#include "launcher.h"
#include "logger.h"
#include "nvapi/nvapi.h"
#include "sdk/sdk.h"
namespace launcher {
@@ -23,6 +24,8 @@ namespace launcher {
// therefore, subsystems need to be guarded against multiple unload attempts
log_info("launcher", "stopping subsystems");
sdk::fini_sdk_modules();
// reset monitor settings
reset_monitor_on_exit();
+3 -1
View File
@@ -67,7 +67,9 @@ namespace superexit {
}
}
bool async_key_exit = GetAsyncKeyState(VK_MENU) && GetAsyncKeyState(VK_F4);
bool async_key_exit =
(GetAsyncKeyState(VK_MENU) & 0x8000) != 0 &&
(GetAsyncKeyState(VK_F4) & 0x8000) != 0;
bool overlay_exit = false;
auto buttons = games::get_buttons_overlay(eamuse_get_game());
+9 -6
View File
@@ -9,6 +9,7 @@
#include "rawinput/rawinput.h"
#include "games/sdvx/sdvx.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/time.h"
#include "util/utils.h"
#include "overlay/overlay.h"
@@ -286,6 +287,7 @@ void eamuse_coin_start_thread() {
COIN_INPUT_THREAD = new std::thread([]() {
auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game());
static bool COIN_INPUT_KEY_STATE = false;
timeutils::PreciseSleepTimer timer;
while (COIN_INPUT_THREAD_ACTIVE) {
// check input key
@@ -305,7 +307,7 @@ void eamuse_coin_start_thread() {
}
// once every two frames
Sleep(1000 / 30);
timer.sleep(1000 / 30);
}
});
}
@@ -340,6 +342,7 @@ void eamuse_pin_macro_start_thread() {
size_t pin_index[2] = {PIN_MACRO_VALUES[0].length(), PIN_MACRO_VALUES[1].length()};
std::optional<uint8_t> active_unit = std::nullopt;
timeutils::PreciseSleepTimer timer;
while (PIN_MACRO_THREAD_ACTIVE) {
// wait for key press
@@ -359,7 +362,7 @@ void eamuse_pin_macro_start_thread() {
}
if (!active_unit.has_value()) {
Sleep(20);
timer.sleep(20);
continue;
}
}
@@ -375,22 +378,22 @@ void eamuse_pin_macro_start_thread() {
eamuse_set_keypad_overrides(unit, keypad_overrides[char_index]);
}
pin_index[unit]++;
Sleep(100);
timer.sleep(100);
// clear
eamuse_set_keypad_overrides(unit, 0);
Sleep(50);
timer.sleep(50);
// end of PIN
if (pin_index[unit] == PIN_MACRO_VALUES[unit].length()) {
active_unit = std::nullopt;
Sleep(120);
timer.sleep(120);
}
continue;
}
Sleep(200);
timer.sleep(200);
}
});
}
+16 -2
View File
@@ -38,6 +38,9 @@ namespace wintouchemu {
bool INJECT_MOUSE_AS_WM_TOUCH = false;
bool LOG_FPS = false;
bool ADD_TOUCH_FLAG_PRIMARY = false;
// state
double last_touch_event = 0.0;
static inline bool is_emu_enabled() {
return FORCE || !is_touch_available("wintouchemu::is_emu_enabled") || GRAPHICS_SHOW_CURSOR;
@@ -248,7 +251,10 @@ namespace wintouchemu {
touch_input->y -= SPICETOUCH_TOUCH_Y;
}
// log_misc("wintouchemu", "mouse state ({}, {})", to_string(touch_input->x), to_string(touch_input->y));
// log_misc(
// "wintouchemu",
// "mouse state ({}, {}) event={}",
// to_string(touch_input->x), to_string(touch_input->y), mouse_state.touch_event);
auto valid = true;
if (overlay::OVERLAY) {
@@ -439,6 +445,8 @@ namespace wintouchemu {
}
}
const auto now = get_performance_milliseconds();
// update touch events
if (hWnd != nullptr) {
@@ -458,6 +466,7 @@ namespace wintouchemu {
// check if new events are available
if (event_count > 0) {
last_touch_event = now;
// send fake event to make the game update it's touch inputs
auto wndProc = (WNDPROC) GetWindowLongPtr(hWnd, GWLP_WNDPROC);
@@ -484,7 +493,12 @@ namespace wintouchemu {
// value from GetTouchInputInfo or fail to read dwFlags for valid events, so it's not OK to
// send empty events when the mouse button is not clicked/released
if (hWnd != nullptr && USE_MOUSE) {
bool button_pressed = ((GetKeyState(VK_LBUTTON) & 0x100) != 0);
bool button_pressed = get_async_primary_mouse();
// if there was a touch event in the last 500 ms, don't insert new button presses
if (button_pressed && (now - last_touch_event) < 500) {
button_pressed = false;
}
// figure out what kind of touch event to simulate
if (button_pressed && !mouse_state.last_button_pressed) {
+7 -4
View File
@@ -351,7 +351,8 @@ void ImGui_ImplSpice_NewFrame() {
// only process new input from devices if window is in focus
// (when not in focus, all mouse/keyboard events are treated as released)
const auto accept_new_input = superexit::has_focus() && !rawinput::OS_WINDOW_ACTIVE;
const auto overlay_visible = overlay::OVERLAY && overlay::OVERLAY->get_active();
const auto accept_new_input = superexit::has_focus() && !rawinput::OS_WINDOW_ACTIVE && overlay_visible;
// remember old state
std::array<BYTE, VKEY_MAX> KeysDownOld;
@@ -366,9 +367,11 @@ void ImGui_ImplSpice_NewFrame() {
g_KeysDown.fill(false);
// apply windows mouse buttons
g_MouseDown[ImGuiMouseButton_Left] |= (get_async_primary_mouse()) != 0;
g_MouseDown[ImGuiMouseButton_Right] |= (get_async_secondary_mouse()) != 0;
g_MouseDown[ImGuiMouseButton_Middle] |= (GetAsyncKeyState(VK_MBUTTON)) != 0;
if (accept_new_input) {
g_MouseDown[ImGuiMouseButton_Left] |= get_async_primary_mouse();
g_MouseDown[ImGuiMouseButton_Right] |= get_async_secondary_mouse();
g_MouseDown[ImGuiMouseButton_Middle] |= (GetAsyncKeyState(VK_MBUTTON) & 0x8000) != 0;
}
// read new keys state
static long mouse_wheel_last = 0;
+36 -4
View File
@@ -948,7 +948,7 @@ namespace overlay::windows {
// grab current keyboard state
for (unsigned short int i = 0x01; i < 0xFF; i++) {
buttons_keyboard_state[i] = GetAsyncKeyState(i) != 0;
buttons_keyboard_state[i] = (GetAsyncKeyState(i) & 0x8000) != 0;
}
}
if (ImGui::IsItemHovered(ImGui::TOOLTIP_FLAGS)) {
@@ -1615,7 +1615,7 @@ namespace overlay::windows {
// use care when iterating over the result (could result in torn reads)
bool keyboard_state_new[sizeof(buttons_keyboard_state)];
for (size_t i = 0; i < sizeof(keyboard_state_new); i++) {
keyboard_state_new[i] = GetAsyncKeyState(i) != 0;
keyboard_state_new[i] = (GetAsyncKeyState(i) & 0x8000) != 0;
}
// detect key presses
@@ -2539,9 +2539,9 @@ namespace overlay::windows {
}
}
// hide deadzone for circular analog since it doesn't make any sense
// hide deadzone for circular analog since it doesn't make any sense (unless in relative mode)
if ((device->type == rawinput::HID || device->type == rawinput::MIDI) &&
analog.getType() != GameAPI::Analogs::AnalogType::Circular) {
((analog.getType() != GameAPI::Analogs::AnalogType::Circular) || analog.isRelativeMode())) {
auto deadzone = analog.getDeadzone();
// for back compat (before each analog had a type)
@@ -2585,16 +2585,46 @@ namespace overlay::windows {
if (analog.getType() == GameAPI::Analogs::AnalogType::Circular) {
// smoothing
bool smoothing = analog.getSmoothing();
ImGui::BeginDisabled(analog.isRelativeMode());
ImGui::Checkbox("Smooth Axis (adds latency)", &smoothing);
ImGui::SameLine();
ImGui::HelpMarker(
"Apply a moving average algorithm; intended for angular input (knobs, turntables). "
"Adds a slight bit of latency to input as the algorithm averages out recent input. "
"Only use in dire situations where the input is too jittery for the game.");
ImGui::EndDisabled();
if (smoothing != analog.getSmoothing()) {
analog.setSmoothing(smoothing);
}
// relative input mode
bool relative_analog = analog.isRelativeMode();
ImGui::Checkbox("Relative Axis", &relative_analog);
ImGui::SameLine();
ImGui::HelpMarker(
"Use relative directional input instead of positional values. "
"Can be used to translate analog sticks to knob input.\n\n"
"At default settings, max speed is one revolution per second.\n\n"
"Adjust sensitivity to increase/decrease max speed, and deadzone to prevent jitter.");
if (relative_analog != analog.isRelativeMode()) {
analog.setRelativeMode(relative_analog);
}
}
// delay
int delay = analog.getDelayMs();
if (ImGui::InputInt("Delay (ms)", &delay, 1, 1)) {
delay = std::clamp(delay, 0, 250);
analog.setDelayMs(delay);
}
ImGui::SameLine();
ImGui::HelpMarker(
"Adds an artificial delay, in milliseconds.\n\n"
"Value read by the game engine will be delayed AT MOST this amount of time (could be slightly less).\n\n"
"To minimize quantization error, delay value should be a multiple of the game's poll rate, "
"add 0.5ms of buffer, and then round up to the nearest millisecond.\n\n"
"If you aren't sure, assume the game polls for input at the same rate as the display FPS."
);
}
}
@@ -5406,6 +5436,8 @@ namespace overlay::windows {
a.setInvert(ta.invert);
a.setSmoothing(ta.smoothing);
a.setMultiplier(ta.multiplier);
a.setRelativeMode(ta.relative_mode);
a.setDelayMs(ta.delay_ms);
::Config::getInstance().updateBinding(game, a);
break;
}
@@ -71,6 +71,8 @@ namespace overlay::windows {
bool invert = false;
bool smoothing = false;
int multiplier = 1;
bool relative_mode = false;
uint32_t delay_ms = 0;
bool is_device() const { return !device_identifier.empty(); }
bool is_unbound() const { return device_identifier.empty() && index == 0xFF; }
@@ -87,6 +89,8 @@ namespace overlay::windows {
invert = a.getInvert();
smoothing = a.getSmoothing();
multiplier = a.getMultiplier();
relative_mode = a.isRelativeMode();
delay_ms = a.getDelayMs();
}
};
+4 -2
View File
@@ -9,6 +9,7 @@
#include "util/logging.h"
#include "external/robin_hood.h"
#include "util/precise_timer.h"
#include "util/time.h"
#include "util/utils.h"
@@ -155,8 +156,9 @@ void rawinput::RawInputManager::input_hwnd_create() {
});
// wait for window creation being done
timeutils::PreciseSleepTimer timer;
while (!this->input_hwnd) {
Sleep(1);
timer.sleep(1);
}
}
@@ -2983,4 +2985,4 @@ void rawinput::RawInputManager::remove_callback_midi(void * data, const std::fun
[data, callback](MidiCallback const &cb) {
return cb.data == data && cb.f.target<void>() == callback.target<void>();
}), this->callback_midi.end());
}
}
+9 -5
View File
@@ -7,6 +7,7 @@
#include "util/logging.h"
#include "misc/eamuse.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include "structuredmessage.h"
@@ -217,6 +218,7 @@ bool Reader::set_comm_state(DWORD BaudRate) {
}
bool Reader::wait_for_handshake() {
timeutils::PreciseSleepTimer timer;
// baud rates
DWORD baud_rates[] = { CBR_57600, CBR_38400, CBR_19200, CBR_9600 };
@@ -268,7 +270,7 @@ bool Reader::wait_for_handshake() {
}
// sleep
Sleep(50);
timer.sleep(50);
}
log_warning("reader", "{}: no handshake received for {} baud", this->port, baud_rates[i]);
@@ -405,6 +407,7 @@ void start_reader_thread(const std::string &port, int id) {
READER_THREAD_RUNNING = true;
READER_THREADS.push_back(new std::thread([port, id]() {
log_info("reader", "{}: starting reader thread", port);
timeutils::PreciseSleepTimer timer;
while (READER_THREAD_RUNNING) {
@@ -442,21 +445,22 @@ void start_reader_thread(const std::string &port, int id) {
}
if (did_read_card) {
Sleep(2500);
timer.sleep(2500);
}
Sleep(20);
timer.sleep(20);
}
}
// sleep between reader connection retries
if (READER_THREAD_RUNNING)
Sleep(5000);
timer.sleep(5000);
}
}));
// wait for thread to start
Sleep(10);
timeutils::PreciseSleepTimer timer;
timer.sleep(10);
}
void stop_reader_thread() {
+273
View File
@@ -0,0 +1,273 @@
#pragma once
#ifndef SPICE_SDK_H
#define SPICE_SDK_H
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
#define SPICE_SDK_ENTRY_POINT extern "C" __declspec(dllexport) int __cdecl
#else
#define SPICE_SDK_ENTRY_POINT __declspec(dllexport) int __cdecl
#endif
#ifdef __cplusplus
extern "C" {
#endif
typedef enum SPICE_SDK_STATUS_CODE {
SPICE_SDK_STATUS_SUCCESS = 0,
// 000: generic
SPICE_SDK_STATUS_GENERIC_ERROR = 1,
SPICE_SDK_STATUS_NOT_INITIALIZED = 2,
SPICE_SDK_STATUS_NOT_SUPPORTED = 3,
SPICE_SDK_STATUS_TOO_SMALL = 4,
SPICE_SDK_STATUS_TOO_LATE = 5,
// 1000: invalid args
SPICE_SDK_STATUS_INVALID_ARGUMENT_1 = 1001,
SPICE_SDK_STATUS_INVALID_ARGUMENT_2 = 1002,
SPICE_SDK_STATUS_INVALID_ARGUMENT_3 = 1003,
SPICE_SDK_STATUS_INVALID_ARGUMENT_4 = 1004,
SPICE_SDK_STATUS_INVALID_ARGUMENT_5 = 1005,
} SPICE_SDK_STATUS_CODE;
typedef enum SPICE_SDK_LOG_LEVEL {
SPICE_SDK_LOG_LEVEL_MISC = 0,
SPICE_SDK_LOG_LEVEL_INFO = 1,
SPICE_SDK_LOG_LEVEL_WARNING = 2,
SPICE_SDK_LOG_LEVEL_FATAL = 3,
} SPICE_SDK_LOG_LEVEL;
typedef struct SPICE_SDK_TOUCH_POINT {
uint32_t id;
int x;
int y;
} SPICE_SDK_TOUCH_POINT;
typedef struct SPICE_SDK_GAME_INFO {
char name[64]; // null-terminated
} SPICE_SDK_GAME_INFO;
typedef struct SPICE_SDK_AVS_INFO {
char model[4]; // "MDX", null-terminated
char dest; // J
char spec; // A
char rev; // A
char ext[11]; // "2025061002", null-terminated
} SPICE_SDK_AVS_INFO;
// get_game_info (v0.1 and up)
//
// get info about the currently running game
//
// info: receives game info
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_game_info_func)(
SPICE_SDK_GAME_INFO *info
);
// get_avs_info (v0.1 and up)
//
// get AVS info (model, dest, spec, rev, ext)
//
// info: receives AVS info
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_avs_info_func)(
SPICE_SDK_AVS_INFO *info
);
// log (v0.1 and up)
// logs a message to the log
// writing a FATAL message will terminate spice, only use in catastrophic failure
//
// level: see log level enum
// module: short string that identifies the facility / module / submodule
// message: the message to log
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_log_func)(
SPICE_SDK_LOG_LEVEL level,
const char *module,
const char *message
);
// get_button (v0.1 and up)
// gets the button state
//
// button_id: ID of the button; see spicesdk_io.h for named values
// pressed: (optional) is the button pressed?
// velocity: (optional) MIDI velocity of the button
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_button_func)(
uint32_t button_id,
bool *pressed,
float *velocity
);
// set_button (v0.1 and up)
// sets or clears the button override
//
// make sure to hold the button long enough for the game's I/O engine to pick up
// usually, one or two frames
//
// button_id: ID of the button; see spicesdk_io.h for named values
// pressed: true to set the button override (permanently set the button to be ON until cleared),
// false to clear the override (allow user's controller to provide input again)
// velocity: MIDI velocity of the button; only valid when pressed is true
// can be between 0.0 and 1.0, inclusive
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_set_button_func)(
uint32_t button_id,
bool pressed,
float velocity
);
// get_analog (v0.1 and up)
// gets the analog state
//
// button_id: ID of the button; see spicesdk_io.h for named values
// value: receives the state of the analog
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_analog_func)(
uint32_t analog_id,
float *value
);
// set_analog (v0.1 and up)
// sets or clears the analog override
//
// analog_id: ID of the analog; see spicesdk_io.h for named values
// override_active: true to set override (gain exclusive control)
// false to clear it (allow user's controller provide input again)
// value: value of the analog; only valid when override_active is true
// can be between 0.0 and 1.0, inclusive
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_set_analog_func)(
uint32_t analog_id,
bool override_active,
float value
);
// get_light (v0.1 and up)
// gets the last observed value of a light
//
// light_id: ID of the light; see spicesdk_io.h for named values
// value: output parameter for the light value; 0.0 to 1.0
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_light_func)(
uint32_t light_id,
float *value
);
// set_light (v0.1 and up)
// sets or clears the light override
//
// light_id: ID of the light; see spicesdk_io.h for named values
// light_value: output parameter for the light value; 0.0 to 1.0
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_set_light_func)(
uint32_t light_id,
bool override_active,
float light_value
);
// set_touch (v0.1 and up)
// adds or updates touch points
//
// points: array of touch points to add or update
// count: number of touch points in the array
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_set_touch_func)(
const SPICE_SDK_TOUCH_POINT *points,
uint32_t count
);
// clear_touch (v0.1 and up)
// clears touch points (i.e., no longer being touched)
//
// ids: array of touch point IDs to clear
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_clear_touch_func)(
const uint32_t *ids,
uint32_t count
);
// insert_card (v0.1 and up)
// simulates inserting an e-amuse card with the given ID
//
// unit: 0 for player 1, 1 for player 2
// card_id: null-terminated string of the card ID
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_insert_card_func)(
uint8_t unit,
const char *card_id
);
// set_keypad (v0.1 and up)
// sets keypad state
//
// make sure to hold the button long enough for the game to pick up
// 70ms is usually sufficient, except for DDR which needs 150ms
//
// unit: 0 for player 1, 1 for player 2
// key: '0' to '9' for numbers, 'A' for 00, 'D' for decimal point, 0 or '\0' to release all keys
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_set_keypad_func)(
uint8_t unit,
char key
);
typedef struct SPICE_SDK_V0 {
uint32_t size;
spice_sdk_log_func *log;
spice_sdk_get_game_info_func *get_game_info;
spice_sdk_get_avs_info_func *get_avs_info;
spice_sdk_get_button_func *get_button;
spice_sdk_set_button_func *set_button;
spice_sdk_get_analog_func *get_analog;
spice_sdk_set_analog_func *set_analog;
spice_sdk_get_light_func *get_light;
spice_sdk_set_light_func *set_light;
spice_sdk_set_touch_func *set_touch;
spice_sdk_clear_touch_func *clear_touch;
spice_sdk_insert_card_func *insert_card;
spice_sdk_set_keypad_func *set_keypad;
} SPICE_SDK_V0;
typedef void (__cdecl spice_sdk_destroy_callback_func)(
void
);
// init (v0.1 and up)
//
// version: supply 0
// destroy_callback: supply a function pointer that will be called when spice
// is shutting down
// sdk_functions: supply a pointer to SPICE_SDK_V0; ensure size field is initialized
// to sizeof(SPICE_SDK_V0) before calling this function
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_init_func)(
uint32_t version,
spice_sdk_destroy_callback_func *destroy_callback,
void *sdk_functions
);
typedef int (__cdecl spice_sdk_entry_point_func)(
spice_sdk_init_func *init
);
#ifdef __cplusplus
} // extern "C"
#endif
#endif // SPICE_SDK_H
File diff suppressed because it is too large Load Diff
+98
View File
@@ -0,0 +1,98 @@
#include <format>
#include <chrono>
#include <thread>
#include <windows.h>
#include "sdk/include/spicesdk.h"
#include "sdk/include/spicesdk_io.h"
// this sample assumes that the game is DDR
// convenience wrapper for logging; feel free to add other levels
#define LOG_INFO(message) spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0_cpp", message)
static SPICE_SDK_V0 spice = {};
static spice_sdk_destroy_callback_func destroy_callback;
static std::jthread worker_thread;
static void worker_thread_main(std::stop_token stop_token);
// main entry point into the DLL
// spice executable will call into this shortly after the game is partially
// initialized (but not quite fully booted just yet)
SPICE_SDK_ENTRY_POINT
spice_sdk_entry_point(
spice_sdk_init_func *init
)
{
SPICE_SDK_STATUS_CODE status;
// SPICE_SDK_V0 must be zeroed AND the size field must be set
spice.size = sizeof(spice);
// initialize the SDK - can be called any time, but we'll do it here
status = init(0, destroy_callback, &spice);
// always check for return value; failure could be usage error,
// or the spice executable doesn't support this SDK version
// check the log messages from sdk:
if (status != SPICE_SDK_STATUS_SUCCESS) {
return 0;
}
LOG_INFO("plugin loaded");
// spin up a worker thread
worker_thread = std::jthread(worker_thread_main);
return 1;
}
void
__cdecl
destroy_callback(
void
)
{
LOG_INFO("plugin unloading");
if (worker_thread.joinable()) {
worker_thread.request_stop();
worker_thread.join();
}
}
struct ArrowButton {
int key;
SPICE_SDK_DDR_BUTTONS button;
const char* name;
bool previous_state;
};
static ArrowButton arrow_buttons[] = {
{ VK_UP, DDR_Button_P1_PANEL_UP, "UP", false },
{ VK_DOWN, DDR_Button_P1_PANEL_DOWN, "DOWN", false },
{ VK_LEFT, DDR_Button_P1_PANEL_LEFT, "LEFT", false },
{ VK_RIGHT, DDR_Button_P1_PANEL_RIGHT, "RIGHT", false },
};
// worker thread for I/O
static void worker_thread_main(std::stop_token stop_token) {
while (!stop_token.stop_requested()) {
for (auto& arrow : arrow_buttons) {
// check for ctrl + arrow keys and trigger p1 pad arrows
if (((GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0) &&
((GetAsyncKeyState(arrow.key) & 0x8000) != 0)) {
spice.set_button(arrow.button, true, 1.f);
if (!arrow.previous_state) {
LOG_INFO(std::format("let me hear you say: {}", arrow.name).c_str());
arrow.previous_state = true;
}
} else {
spice.set_button(arrow.button, false, 0.f);
arrow.previous_state = false;
}
}
Sleep(1);
}
}
+4
View File
@@ -0,0 +1,4 @@
LIBRARY sdk_sample_v0_cpp
EXPORTS
spice_sdk_entry_point
@@ -0,0 +1,318 @@
#include <string.h>
#include <stdio.h>
#include <windows.h>
#include <process.h>
#include "sdk/include/spicesdk.h"
#include "sdk/include/spicesdk_io.h"
static SPICE_SDK_V0 spice;
static spice_sdk_destroy_callback_func destroy_callback;
static void test_logging();
static void test_game_info();
static void test_avs_info();
static void get_buttons();
static void set_buttons();
static void clear_buttons();
static void get_analogs();
static void set_analogs();
static void clear_analogs();
static void get_lights();
static void set_lights();
static void clear_lights();
static void set_touch();
static void clear_touch();
static void insert_card();
static void set_keypad();
static void clear_keypad();
static HANDLE worker_stop_event;
static HANDLE worker_handle;
static unsigned __stdcall worker_thread(void *arg);
// this sample assumes that the game is IIDX, but it doesn't check for it.
SPICE_SDK_ENTRY_POINT
spice_sdk_entry_point(
spice_sdk_init_func *init
)
{
SPICE_SDK_STATUS_CODE status;
memset(&spice, 0, sizeof(spice));
spice.size = sizeof(spice);
status = init(0, destroy_callback, &spice);
if (status != SPICE_SDK_STATUS_SUCCESS) {
return 0;
}
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "plugin loaded");
test_logging();
test_game_info();
test_avs_info();
worker_stop_event = CreateEventA(NULL, TRUE, FALSE, NULL);
if (!worker_stop_event) {
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", "failed to create worker stop event");
return 0;
}
worker_handle = (HANDLE)_beginthreadex(
NULL, // security
0, // stack size
worker_thread, // function
NULL, // argument
0, // flags
NULL // thread id
);
if (!worker_handle) {
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", "failed to create worker thread");
CloseHandle(worker_stop_event);
worker_stop_event = NULL;
return 0;
}
return 1;
}
void
__cdecl
destroy_callback(
void
)
{
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "plugin unloaded");
if (worker_stop_event) {
SetEvent(worker_stop_event);
}
if (worker_handle) {
WaitForSingleObject(worker_handle, INFINITE);
CloseHandle(worker_handle);
worker_handle = NULL;
}
if (worker_stop_event) {
CloseHandle(worker_stop_event);
worker_stop_event = NULL;
}
}
static unsigned __stdcall worker_thread(void *arg) {
int phase = 0;
while (WaitForSingleObject(worker_stop_event, 0) == WAIT_TIMEOUT) {
phase += 1;
switch (phase) {
case 1:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "get buttons...");
get_buttons();
break;
case 2:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "set buttons...");
set_buttons();
break;
case 3:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "clear buttons...");
clear_buttons();
break;
case 4:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "get analogs...");
get_analogs();
break;
case 5:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "set analogs...");
set_analogs();
break;
case 6:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "clear analogs...");
clear_analogs();
break;
case 7:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "get lights...");
get_lights();
break;
case 8:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "set lights...");
set_lights();
break;
case 9:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "clear lights...");
clear_lights();
break;
case 10:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "set touch...");
set_touch();
break;
case 11:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "clear touch...");
clear_touch();
break;
case 12:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "insert card...");
insert_card();
break;
case 13:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "set keypad...");
set_keypad();
break;
case 14:
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "clear keypad...");
clear_keypad();
break;
default:
phase = 0;
break;
}
if (phase != 0) {
WaitForSingleObject(worker_stop_event, 3000);
}
}
return 0;
}
static void test_logging() {
spice.log(SPICE_SDK_LOG_LEVEL_MISC, "sample_v0", "this is a misc message");
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "this is an info message");
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", "this is a warning message");
}
static void test_game_info() {
SPICE_SDK_GAME_INFO info;
SPICE_SDK_STATUS_CODE status;
status = spice.get_game_info(&info);
if (status != SPICE_SDK_STATUS_SUCCESS) {
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", "get_game_info failed");
return;
}
char log_message[128];
snprintf(log_message, sizeof(log_message), "game info - name: %s", info.name);
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", log_message);
}
static void test_avs_info() {
SPICE_SDK_AVS_INFO info;
SPICE_SDK_STATUS_CODE status;
status = spice.get_avs_info(&info);
if (status != SPICE_SDK_STATUS_SUCCESS) {
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", "get_avs_info failed");
return;
}
char log_message[128];
snprintf(
log_message,
sizeof(log_message),
"avs - model: %s, dest: %c, spec: %c, rev: %c, ext: %s",
info.model, info.dest, info.spec, info.rev, info.ext);
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", log_message);
}
static void get_buttons() {
bool pressed;
float velocity;
spice.get_button(IIDX_Button_P1_Headphone, &pressed, &velocity);
char log_message[128];
snprintf(
log_message,
sizeof(log_message),
"button P1_Headphone pressed: %s, velocity: %.2f",
pressed ? "ON" : "off", velocity);
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", log_message);
}
static void set_buttons() {
spice.set_button(IIDX_Button_P1_1, true, 0.3f);
spice.set_button(IIDX_Button_P1_3, true, 0.5f);
spice.set_button(IIDX_Button_P1_5, true, 0.7f);
}
static void clear_buttons() {
spice.set_button(IIDX_Button_P1_1, false, 0.f);
spice.set_button(IIDX_Button_P1_3, false, 0.f);
spice.set_button(IIDX_Button_P1_5, false, 0.f);
}
static void get_analogs() {
float value;
spice.get_analog(IIDX_Analog_TT_P1, &value);
char log_message[128];
snprintf(
log_message,
sizeof(log_message),
"analog TT_P1: %.2f",
value);
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", log_message);
}
static void set_analogs() {
spice.set_analog(IIDX_Analog_TT_P1, true, 0.25f);
spice.set_analog(IIDX_Analog_TT_P2, true, 0.75f);
}
static void clear_analogs() {
spice.set_analog(IIDX_Analog_TT_P1, false, 0.f);
spice.set_analog(IIDX_Analog_TT_P2, false, 0.f);
}
static void get_lights() {
float value;
SPICE_SDK_STATUS_CODE status;
status = spice.get_light(IIDX_Light_TT_P1_Resistance, &value);
if (status == SPICE_SDK_STATUS_SUCCESS) {
char log_message[64];
snprintf(log_message, sizeof(log_message), "P1 TT resistance value: %.2f", value);
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", log_message);
} else {
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", "get_light failed");
}
}
static void set_lights() {
spice.set_light(IIDX_Light_P1_Start, true, 1.f);
}
static void clear_lights() {
spice.set_light(IIDX_Light_P1_Start, false, 0.f);
}
static void set_touch() {
SPICE_SDK_TOUCH_POINT points[2] = {
{ .id = 1, .x = 100, .y = 200 },
{ .id = 2, .x = 300, .y = 400 },
};
spice.set_touch(points, 2);
}
static void clear_touch() {
uint32_t ids[2] = { 1, 2 };
spice.clear_touch(ids, 2);
}
static void insert_card() {
spice.insert_card(0, "E004010000001234");
}
static void set_keypad() {
SPICE_SDK_STATUS_CODE ret = spice.set_keypad(0, '3');
if (ret != SPICE_SDK_STATUS_SUCCESS) {
char log_message[64];
snprintf(log_message, sizeof(log_message), "set_keypad failed: %d", ret);
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", log_message);
}
}
static void clear_keypad() {
spice.set_keypad(0, 0);
}
@@ -0,0 +1,4 @@
LIBRARY sdk_sample_v0_flat_c
EXPORTS
spice_sdk_entry_point
+598
View File
@@ -0,0 +1,598 @@
#include <vector>
#include <mutex>
#include <shared_mutex>
#include "sdk.h"
#include "avs/game.h"
#include "games/io.h"
#include "launcher/launcher.h"
#include "misc/eamuse.h"
#include "sdk/include/spicesdk.h"
#include "touch/touch.h"
#include "util/logging.h"
#include "util/utils.h"
#include "acio/mdxf/mdxf_poll.h"
namespace sdk {
static spice_sdk_init_func sdk_init;
static spice_sdk_log_func sdk_log;
static spice_sdk_get_avs_info_func sdk_get_avs_info;
static spice_sdk_get_game_info_func sdk_get_game_info;
static spice_sdk_get_button_func sdk_get_button;
static spice_sdk_set_button_func sdk_set_button;
static spice_sdk_get_analog_func sdk_get_analog;
static spice_sdk_set_analog_func sdk_set_analog;
static spice_sdk_get_light_func sdk_get_light;
static spice_sdk_set_light_func sdk_set_light;
static spice_sdk_set_touch_func sdk_set_touch;
static spice_sdk_clear_touch_func sdk_clear_touch;
static spice_sdk_insert_card_func sdk_insert_card;
static spice_sdk_set_keypad_func sdk_set_keypad;
struct SdkModule {
std::string dll;
HINSTANCE module;
};
// DLLs
static int sdk_modules_count = 0;
static std::vector<SdkModule> sdk_modules_list;
static std::shared_mutex sdk_global_mutex;
// internal
static bool sdk_initialized = false;
static bool sdk_shutting_down = false;
static std::vector<Button> *buttons;
static std::vector<Analog> *analogs;
static std::vector<Light> *lights;
// callbacks
static std::mutex sdk_callback_registration_mutex;
static std::vector<spice_sdk_destroy_callback_func *> callbacks_destroy;
void register_sdk_hooks(std::string dll, HINSTANCE module) {
sdk_modules_list.emplace_back(std::move(dll), module);
sdk_modules_count += 1;
}
void init_sdk_modules() {
if (sdk_modules_count == 0) {
return;
}
// prepare
buttons = games::get_buttons(eamuse_get_game());
analogs = games::get_analogs(eamuse_get_game());
lights = games::get_lights(eamuse_get_game());
// under exclusive lock, mark the SDK helpers as being available
{
std::unique_lock lock(sdk_global_mutex);
sdk_initialized = true;
}
// without any locks, call into each DLL; this may call back into SDK functions
for (auto &module : sdk_modules_list) {
// get a pointer to dll's spice_sdk_entry_point
const auto entry_point = reinterpret_cast<spice_sdk_entry_point_func *>(
GetProcAddress(module.module, "spice_sdk_entry_point"));
if (!entry_point) {
continue;
}
// call into it on this thread
log_info("sdk", "registering SDK hooks for {}", module.dll);
const auto ret = entry_point(sdk_init);
log_info("sdk", "spice_sdk_entry_point returned {}", ret);
}
}
void fini_sdk_modules() {
// prevent multiple calls and further calls into sdk_init
{
std::unique_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return;
}
sdk_shutting_down = true;
}
// call into destroy callback of each DLL
// this may call back into SDK functions (e.g., for logging)
// so we leave sdk_initialized as-is
{
log_info("sdk", "calling destroy callbacks...");
std::lock_guard lock(sdk_callback_registration_mutex);
for (const auto& destroy : callbacks_destroy) {
destroy();
}
}
// under exclusive lock, mark the SDK functions as no longer available
{
std::unique_lock lock(sdk_global_mutex);
sdk_initialized = false;
}
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_init(
uint32_t version,
spice_sdk_destroy_callback_func *destroy_callback,
void *sdk_functions
)
{
std::shared_lock lock(sdk_global_mutex);
if (sdk_shutting_down || !sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
uint32_t size;
if (version != 0) {
log_warning("sdk", "sdk_init returning NOT_SUPPORTED due to invalid version: {}", version);
return SPICE_SDK_STATUS_NOT_SUPPORTED;
}
if (!destroy_callback) {
log_warning("sdk", "sdk_init returning INVALID_ARGUMENT_2 due to invalid destroy_callback pointer");
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
}
if (!sdk_functions) {
log_warning("sdk", "sdk_init returning INVALID_ARGUMENT_3 due to invalid sdk_functions pointer");
return SPICE_SDK_STATUS_INVALID_ARGUMENT_3;
}
auto *v0 = reinterpret_cast<SPICE_SDK_V0 *>(sdk_functions);
if (v0->size < RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, set_keypad)) {
log_warning("sdk", "sdk_init returning TOO_SMALL due to size field of SPICE_SDK_V0 not being set");
return SPICE_SDK_STATUS_TOO_SMALL;
}
// we are trusting the size passed in by the caller
size = v0->size;
memset(v0, 0, size);
v0->size = size;
v0->log = sdk_log;
v0->get_game_info = sdk_get_game_info;
v0->get_avs_info = sdk_get_avs_info;
v0->get_button = sdk_get_button;
v0->set_button = sdk_set_button;
v0->get_analog = sdk_get_analog;
v0->set_analog = sdk_set_analog;
v0->get_light = sdk_get_light;
v0->set_light = sdk_set_light;
v0->set_touch = sdk_set_touch;
v0->clear_touch = sdk_clear_touch;
v0->insert_card = sdk_insert_card;
v0->set_keypad = sdk_set_keypad;
// end of 0.1
// any newer minor iterations will need to check the size
{
// destroy callbacks are only called upon successful return from this routine
std::lock_guard lock(sdk_callback_registration_mutex);
callbacks_destroy.push_back(destroy_callback);
}
log_info("sdk", "sdk_init returning SUCCESS");
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_log(
SPICE_SDK_LOG_LEVEL level,
const char *facility,
const char *message
)
{
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
if (facility == nullptr) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
}
const std::string facility_str = fmt::format("sdk::{}", facility);
switch (level) {
case SPICE_SDK_LOG_LEVEL_MISC:
log_misc(facility_str.c_str(), "{}", message);
break;
case SPICE_SDK_LOG_LEVEL_INFO:
log_info(facility_str.c_str(), "{}", message);
break;
case SPICE_SDK_LOG_LEVEL_WARNING:
log_warning(facility_str.c_str(), "{}", message);
break;
case SPICE_SDK_LOG_LEVEL_FATAL:
log_fatal(facility_str.c_str(), "{}", message);
break;
default:
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
}
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_get_avs_info(
SPICE_SDK_AVS_INFO *info
)
{
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
if (!info) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
}
// MDXJAA2025061002
strncpy(info->model, avs::game::MODEL, sizeof(info->model));
info->dest = avs::game::DEST[0];
info->spec = avs::game::SPEC[0];
info->rev = avs::game::REV[0];
strncpy(info->ext, avs::game::EXT, sizeof(info->ext));
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_get_game_info(
SPICE_SDK_GAME_INFO *info
)
{
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
if (!info) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
}
*info = {};
const std::string game_name = eamuse_get_game();
strncpy(info->name, game_name.c_str(), sizeof(info->name));
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_get_button (
uint32_t button_id,
bool *pressed,
float *velocity
)
{
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
if (!buttons) {
return SPICE_SDK_STATUS_NOT_INITIALIZED;
}
if (button_id >= buttons->size()) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
}
Button &button = (*buttons)[button_id];
if (pressed) {
*pressed = (GameAPI::Buttons::getState(RI_MGR, button) == GameAPI::Buttons::BUTTON_PRESSED);
}
if (velocity) {
*velocity = GameAPI::Buttons::getVelocity(RI_MGR, button);
}
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_set_button (
uint32_t button_id,
bool pressed,
float velocity
)
{
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
if (!buttons) {
return SPICE_SDK_STATUS_NOT_INITIALIZED;
}
if (button_id >= buttons->size()) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
}
if (pressed) {
velocity = std::clamp(velocity, 0.f, 1.f);
}
Button &button = (*buttons)[button_id];
if (pressed) {
button.override_state = pressed ? GameAPI::Buttons::BUTTON_PRESSED : GameAPI::Buttons::BUTTON_NOT_PRESSED;
button.override_velocity = velocity;
}
button.override_enabled = pressed;
mdxf_poll(true);
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_get_analog (
uint32_t analog_id,
float *value
)
{
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
if (!analogs) {
return SPICE_SDK_STATUS_NOT_INITIALIZED;
}
if (analog_id >= analogs->size()) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
}
if (!value) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
}
Analog &analog = (*analogs)[analog_id];
*value = GameAPI::Analogs::getState(RI_MGR, analog);
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_set_analog (
uint32_t analog_id,
bool override_active,
float value
)
{
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
if (!analogs) {
return SPICE_SDK_STATUS_NOT_INITIALIZED;
}
if (analog_id >= analogs->size()) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
}
if (override_active) {
value = std::clamp(value, 0.f, 1.f);
}
Analog &analog = (*analogs)[analog_id];
if (override_active) {
analog.override_state = value;
}
analog.override_enabled = override_active;
mdxf_poll(true);
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_get_light(
uint32_t light_id,
float *value
)
{
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
if (!lights) {
return SPICE_SDK_STATUS_NOT_INITIALIZED;
}
if (light_id >= lights->size()) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
}
if (!value) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
}
Light &light = (*lights)[light_id];
*value = GameAPI::Lights::readLight(RI_MGR, light);
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_set_light(
uint32_t light_id,
bool override_active,
float value
)
{
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
if (!lights) {
return SPICE_SDK_STATUS_NOT_INITIALIZED;
}
if (light_id >= lights->size()) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
}
if (override_active) {
value = std::clamp(value, 0.f, 1.f);
}
Light &light = (*lights)[light_id];
if (override_active) {
light.override_state = value;
}
light.override_enabled = override_active;
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_set_touch(
const SPICE_SDK_TOUCH_POINT *points,
uint32_t count
)
{
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
if (count == 0 || !points) {
return SPICE_SDK_STATUS_TOO_SMALL;
}
std::vector<TouchPoint> touch_points;
for (uint32_t i = 0; i < count; i++) {
const SPICE_SDK_TOUCH_POINT &point = points[i];
touch_points.emplace_back(TouchPoint {
.id = point.id,
.x = point.x,
.y = point.y,
.mouse = false,
});
}
touch_write_points(&touch_points);
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_clear_touch(
const uint32_t *ids,
uint32_t count
)
{
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
if (count == 0 || !ids) {
return SPICE_SDK_STATUS_TOO_SMALL;
}
std::vector<DWORD> touch_point_ids;
for (uint32_t i = 0; i < count; i++) {
touch_point_ids.push_back(ids[i]);
}
touch_remove_points(&touch_point_ids);
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_insert_card(
uint8_t unit,
const char *card_id
)
{
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
if (unit >= 2) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
}
if (!card_id) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
}
if (strlen(card_id) != 16) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
}
// convert to binary
uint8_t card_bin[8] {};
if (!hex2bin(card_id, card_bin)) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
}
// insert
eamuse_card_insert(unit & 1, card_bin);
return SPICE_SDK_STATUS_SUCCESS;
}
struct KeypadMapping {
char character;
uint16_t state;
};
static KeypadMapping KEYPAD_MAPPINGS[] = {
{ '0', 1 << EAM_IO_KEYPAD_0 },
{ '1', 1 << EAM_IO_KEYPAD_1 },
{ '2', 1 << EAM_IO_KEYPAD_2 },
{ '3', 1 << EAM_IO_KEYPAD_3 },
{ '4', 1 << EAM_IO_KEYPAD_4 },
{ '5', 1 << EAM_IO_KEYPAD_5 },
{ '6', 1 << EAM_IO_KEYPAD_6 },
{ '7', 1 << EAM_IO_KEYPAD_7 },
{ '8', 1 << EAM_IO_KEYPAD_8 },
{ '9', 1 << EAM_IO_KEYPAD_9 },
{ 'A', 1 << EAM_IO_KEYPAD_00 },
{ 'D', 1 << EAM_IO_KEYPAD_DECIMAL },
};
SPICE_SDK_STATUS_CODE
__cdecl
sdk_set_keypad(
uint8_t unit,
char key
)
{
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
if (unit >= 2) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
}
if (key == 0) {
eamuse_set_keypad_overrides(unit, 0);
return SPICE_SDK_STATUS_SUCCESS;
}
// find mapping
uint16_t state = 0;
bool mapping_found = false;
for (auto &mapping : KEYPAD_MAPPINGS) {
if (mapping.character == key) {
state |= mapping.state;
mapping_found = true;
break;
}
}
// check for error
if (!mapping_found) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
}
// set
eamuse_set_keypad_overrides(unit, state);
return SPICE_SDK_STATUS_SUCCESS;
}
} // namespace sdk
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include <string>
#include <windows.h>
namespace sdk {
void register_sdk_hooks(std::string dll, HINSTANCE module);
void init_sdk_modules();
void fini_sdk_modules();
}
+66 -49
View File
@@ -67,6 +67,8 @@ static const char *LOG_MODULE_NAME = "touch";
static TouchHandler *TOUCH_HANDLER = nullptr;
static bool is_mouse_message_from_touchscreen();
TouchHandler::TouchHandler(std::string name) {
log_info("touch", "Using touch handler: {}", name);
}
@@ -215,6 +217,30 @@ void update_card_button() {
}
}
static void release_all_mouse_touch_points() {
std::lock_guard<std::mutex> lock_points(TOUCH_POINTS_M);
std::lock_guard<std::mutex> lock_events(TOUCH_EVENTS_M);
for (size_t x = 0; x < TOUCH_POINTS.size();) {
TouchPoint *tp = &TOUCH_POINTS[x];
if (tp->id == 0u) {
TouchEvent te {
.id = tp->id,
.x = tp->x,
.y = tp->y,
.type = TOUCH_UP,
.mouse = tp->mouse,
};
add_touch_event(&te);
TOUCH_POINTS.erase(TOUCH_POINTS.begin() + x);
} else {
x++;
}
}
}
static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
// check if touch was registered
@@ -490,35 +516,22 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
// parse mouse messages
switch (msg) {
case WM_LBUTTONDOWN: {
// check if mouse is enabled
if (SPICETOUCH_ENABLE_MOUSE) {
if (is_mouse_message_from_touchscreen()) {
return 0;
}
// subscribe to mouse messages even when the cursor leaves the window
SetCapture(hWnd);
// release all old events before inserting a new one
release_all_mouse_touch_points();
// lock touch points
std::lock_guard<std::mutex> lock_points(TOUCH_POINTS_M);
std::lock_guard<std::mutex> lock_events(TOUCH_EVENTS_M);
// remove all points with ID 0
for (size_t x = 0; x < TOUCH_POINTS.size(); x++) {
TouchPoint *tp = &TOUCH_POINTS[x];
if (tp->id == 0u) {
// generate touch up event
TouchEvent te {
.id = tp->id,
.x = tp->x,
.y = tp->y,
.type = TOUCH_UP,
.mouse = tp->mouse,
};
add_touch_event(&te);
// erase touch point
TOUCH_POINTS.erase(TOUCH_POINTS.begin() + x);
}
}
// create touch point
TouchPoint tp {
.id = 0,
@@ -545,9 +558,11 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
break;
}
case WM_MOUSEMOVE: {
// check if mouse is enabled
if (SPICETOUCH_ENABLE_MOUSE) {
if (is_mouse_message_from_touchscreen()) {
return 0;
}
// lock touch points
std::lock_guard<std::mutex> lock_points(TOUCH_POINTS_M);
@@ -582,40 +597,31 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP
break;
}
case WM_LBUTTONUP: {
// check if mouse is enabled
if (SPICETOUCH_ENABLE_MOUSE) {
if (is_mouse_message_from_touchscreen()) {
return 0;
}
// lock touch points
std::lock_guard<std::mutex> lock_points(TOUCH_POINTS_M);
std::lock_guard<std::mutex> lock_events(TOUCH_EVENTS_M);
// remove all points with ID 0
for (size_t x = 0; x < TOUCH_POINTS.size(); x++) {
TouchPoint *tp = &TOUCH_POINTS[x];
if (tp->id == 0u) {
// generate touch up event
TouchEvent te {
.id = tp->id,
.x = tp->x,
.y = tp->y,
.type = TOUCH_UP,
.mouse = tp->mouse,
};
add_touch_event(&te);
// remove touch point
TOUCH_POINTS.erase(TOUCH_POINTS.begin() + x);
}
release_all_mouse_touch_points();
if (GetCapture() == hWnd) {
ReleaseCapture();
}
}
break;
}
default:
case WM_CAPTURECHANGED:
case WM_CANCELMODE: {
// to deal with window losing the capture after SetCapture
release_all_mouse_touch_points();
if (msg == WM_CANCELMODE && GetCapture() == hWnd) {
ReleaseCapture();
}
break;
}
default:
// call original function
if (SPICETOUCH_CALL_OLD_PROC && SPICETOUCH_OLD_PROC != nullptr) {
return SPICETOUCH_OLD_PROC(hWnd, msg, wParam, lParam);
@@ -663,6 +669,8 @@ void touch_attach_dx_hook() {
// initialize touch handler
touch_initialize();
log_info("touch", "touch_attach_dx_hook: attaching SpiceTouchWndProc...");
// add dx hook
graphics_add_wnd_proc(SpiceTouchWndProc);
@@ -681,6 +689,8 @@ void touch_create_wnd(HWND hWnd, bool overlay) {
// initialize touch handler
touch_initialize();
log_info("touch", "touch_create_wnd: creating SPICETOUCH_TOUCH_THREAD...");
// create thread
SPICETOUCH_TOUCH_THREAD = new std::thread([hWnd, overlay]() {
@@ -956,4 +966,11 @@ void update_spicetouch_window_dimensions(HWND hWnd) {
SPICETOUCH_TOUCH_Y = topleft.y;
SPICETOUCH_TOUCH_WIDTH = bottomright.x - topleft.x;
SPICETOUCH_TOUCH_HEIGHT = bottomright.y - topleft.y;
}
}
// https://learn.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages
static bool is_mouse_message_from_touchscreen() {
constexpr ULONG_PTR MI_WP_SIGNATURE = 0xFF515700;
constexpr ULONG_PTR SIGNATURE_MASK = 0xFFFFFF00;
return (GetMessageExtraInfo() & SIGNATURE_MASK) == MI_WP_SIGNATURE;
}
+188
View File
@@ -0,0 +1,188 @@
#define _WIN32_WINNT 0x0602
#include "precise_timer.h"
#define WIN32_NO_STATUS
#include <windows.h>
#undef WIN32_NO_STATUS
#include <timeapi.h>
#if !SPICE_XP
#include <processthreadsapi.h>
#endif
#include <chrono>
#include <cmath>
#include <limits>
#include "util/logging.h"
#ifdef min
#undef min
#endif
#ifdef max
#undef max
#endif
#define PERIOD 1
#define TOLERANCE 0.02
#ifndef PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION
#define PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION 0x4
#endif
namespace timeutils {
bool TIMER_HACKS_DISABLE = false;
static TimerHandle get_precise_sleep_timer();
static void destroy_precise_sleep_timer(TimerHandle timer);
static void precise_sleep(TimerHandle timer, double seconds);
void set_timer_resolution() {
if (TIMER_HACKS_DISABLE) {
return;
}
#if !SPICE_XP
// make a call to opt out of power throttling
// (requested timer resolution being ignored when window is occluded or minimized)
// SetProcessInformation is win8+
const auto kernel32 = GetModuleHandleA("kernel32.dll");
if (kernel32) {
const auto SetProcessInformation_addr =
reinterpret_cast<decltype(SetProcessInformation) *>(
GetProcAddress(kernel32, "SetProcessInformation"));
if (SetProcessInformation_addr) {
PROCESS_POWER_THROTTLING_STATE state {};
state.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION;
state.ControlMask = PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION;
state.StateMask = 0;
const auto ret = SetProcessInformation_addr(
GetCurrentProcess(),
ProcessPowerThrottling,
&state,
sizeof(state));
log_info(
"timeutils",
"SetProcessInformation called to disable timer resolution throttling, returned {}",
ret);
}
}
#endif
const auto ret = timeBeginPeriod(PERIOD);
log_info(
"timeutils",
"timeBeginPeriod({}) called, returned {}",
PERIOD,
ret);
// and then we never call timeEndPeriod();
// the game will also call this but the OS will honor the lowest value (1 above)
}
#if !SPICE_XP
// timerSleep from https://blog.bearcats.nl/perfect-sleep-function/
static void timerSleep(HANDLE timer, double seconds) {
using namespace std::chrono;
auto t = high_resolution_clock::now();
const auto target = t + nanoseconds(int64_t(seconds * 1e9));
const int64_t maxTicks = PERIOD * 9'500;
for (;;) {
const int64_t remaining = (target - t).count();
int64_t ticks = (remaining - TOLERANCE) / 100;
if (ticks <= 0) {
break;
}
if (ticks > maxTicks) {
ticks = maxTicks;
}
LARGE_INTEGER due;
due.QuadPart = -ticks;
SetWaitableTimerEx(timer, &due, 0, NULL, NULL, NULL, 0);
WaitForSingleObject(timer, INFINITE);
t = high_resolution_clock::now();
}
while (high_resolution_clock::now() < target) {
YieldProcessor();
}
}
#endif
static TimerHandle get_precise_sleep_timer() {
TimerHandle timer = nullptr;
if (TIMER_HACKS_DISABLE) {
return timer;
}
// CreateWaitableTimerExW is Vista+
// CREATE_WAITABLE_TIMER_HIGH_RESOLUTION win10 1803+
#if !SPICE_XP
timer = CreateWaitableTimerExW(
NULL,
NULL,
CREATE_WAITABLE_TIMER_HIGH_RESOLUTION,
TIMER_ALL_ACCESS);
#endif
return timer;
}
static void destroy_precise_sleep_timer(TimerHandle timer) {
if (timer) {
CloseHandle(timer);
}
}
static void precise_sleep(TimerHandle timer, double seconds) {
if (!std::isfinite(seconds) || seconds <= 0.0) {
return;
}
#if !SPICE_XP
if (timer) {
timerSleep(timer, seconds);
return;
}
#endif
// robustSleep is too CPU heavy so we will stick to Sleep() with
// timeBeginPeriod, which has an error at most 1ms
const auto milliseconds = std::ceil(seconds * 1000.0);
if (milliseconds >= std::numeric_limits<DWORD>::max()) {
Sleep(std::numeric_limits<DWORD>::max());
return;
}
Sleep(static_cast<DWORD>(milliseconds));
}
// RAII wrapper
PreciseSleepTimer::PreciseSleepTimer() : timer(get_precise_sleep_timer()) {}
PreciseSleepTimer::~PreciseSleepTimer() {
destroy_precise_sleep_timer(timer);
}
void PreciseSleepTimer::sleep(uint32_t ms) const {
sleep(std::chrono::milliseconds(ms));
}
void PreciseSleepTimer::sleep_seconds(double seconds) const {
precise_sleep(timer, seconds);
}
}
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <chrono>
#include <cstdint>
namespace timeutils {
extern bool TIMER_HACKS_DISABLE;
void set_timer_resolution();
using TimerHandle = void *;
class PreciseSleepTimer {
public:
PreciseSleepTimer();
~PreciseSleepTimer();
PreciseSleepTimer(const PreciseSleepTimer &) = delete;
PreciseSleepTimer &operator=(const PreciseSleepTimer &) = delete;
PreciseSleepTimer(PreciseSleepTimer &&) = delete;
PreciseSleepTimer &operator=(PreciseSleepTimer &&) = delete;
void sleep(uint32_t ms) const;
template <typename Rep, typename Period>
void sleep(std::chrono::duration<Rep, Period> duration) const {
sleep_seconds(std::chrono::duration<double>(duration).count());
}
private:
void sleep_seconds(double seconds) const;
TimerHandle timer;
};
}
+4 -4
View File
@@ -319,14 +319,14 @@ bool acquire_shutdown_privs();
void generate_ea_card(char card[17]);
static inline int get_async_primary_mouse() {
static inline bool get_async_primary_mouse() {
int vk = GetSystemMetrics(SM_SWAPBUTTON) ? VK_RBUTTON : VK_LBUTTON;
return GetAsyncKeyState(vk);
return (GetAsyncKeyState(vk) & 0x8000) != 0;
}
static inline int get_async_secondary_mouse() {
static inline bool get_async_secondary_mouse() {
int vk = GetSystemMetrics(SM_SWAPBUTTON) ? VK_LBUTTON : VK_RBUTTON;
return GetAsyncKeyState(vk);
return (GetAsyncKeyState(vk) & 0x8000) != 0;
}
static inline bool parse_width_height(const std::string wh, std::pair<uint32_t, uint32_t> &result) {