Compare commits

...

69 Commits

Author SHA1 Message Date
bicarus-dev b517ef3182 update options 2026-06-01 02:29:12 -07:00
bicarus 6fef16353e otoca: patch up game for printing holo cards (#726)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #154

## Description of change
Patch the game to skip over special holo printing logic and redirect to
the normal printer, resolving the soft lock + printer failure.

## Testing
Tested `NCG-2019012900`. Other versions might not work though since this
is patch-based not hook-based.
2026-05-31 23:08:17 -07:00
bicarus 84ea3f9e8e audio: stereo downmix, volume boost options (#722)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #717, fixes #647

## Description of change
Adds an option to downmix surround sound (5.1, 7.1, etc) down to stereo
(2 speakers). This can be used in most WASAPI games, including Gitadora
and FTT.

How it works: when the game tries to open surround format (say, 7.1) we
create a fake buffer and tell the game that it is supported. In reality
we open a 2-channel stream with the real sound card. When the stream
begins, we downmix the channels down to two (using one of many
algorithms) and output to the sound card.

While we're here, implement an option to boost the game volume by some
decibel value, which is a feature often requested by SDVX players. This
was needed since downmixing can sometimes result in quieter audio.

## Testing
Tested GW Delta and FTT.

Downmix doesn't work on IIDX (32 bits) but volume boost works.
2026-05-31 20:49:22 -07:00
Azalea 6bb6b0a301 [O] Filter GetIpAddrTable to selected network adapter (#724)
## Description of change
Filter GetIpAddrTable to selected network adapter. 

Fixes the IP address error (5-1506-0000) on gitadora gw delta when
multiple network adapters are present.

## Testing
Tested on Gitadora GW Delta with both WiFi and Tailscale adapters
present.
2026-05-31 16:17:47 -07:00
drmext 465d6c6c18 cfg: fix alt+f4 exit (#723)
## Link to GitHub Issue or related Pull Request, if one exists
#720

## Description of change
Pass `VK_F4` sys-key messages through to `DefWindowProc` before ImGui
ingestion, restoring the normal Alt+F4 -> `WM_CLOSE` ->
`launcher::shutdown()` path.

## Testing
- [x] Launch `spicecfg` (D3D9 default path) and press Alt+F4; window
closes and process exits
- [x] Launch `spicecfg` (software path) and press Alt+F4; window closes
and process exits
- [x] Close via title-bar X; still works
2026-05-30 23:59:54 -07:00
Selundine ef384e85dd Add GITADORA Arena windowed monitor layout support, allows the SMALL touch window to be resized (#716)
## Link to GitHub Issue or related Pull Request, if one exists
N/A

## Description of change
Adds GITADORA Arena windowed monitor layout options for the GITADORA,
LEFT, RIGHT, and SMALL windows.

This also applies the selected window border style to all four GITADORA
Arena windows, and allows the SMALL touch window to be resized in
windowed mode while keeping touch coordinates updated.

## Testing
- Manually tested GITADORA Arena windowed mode:
  - default, resizable, and borderless window styles
  - per-window monitor placement
  - SMALL resize
  - SMALL touch coordinate mapping after resize
2026-05-30 23:09:06 -07:00
bicarus 7ee04879e2 overlay: option to force software renderer in configurator (#721)
## Link to GitHub Issue or related Pull Request, if one exists
chicken bit for #720
2026-05-30 15:24:07 -07:00
drmext 4c73200f58 cfg: d3d9 standalone configurator (#720)
## Description of change
**Configurator (spicecfg.exe)**

- Attempts to create a D3D9 device on startup and uses the hardware
ImGui DX9 path when successful; falls back to the existing software
rasterizer automatically if D3D9 init fails.
- Rewrites the configurator window loop: refresh-rate-aware render
timer, minimize/resize/`WM_DISPLAYCHANGE` handling, direct Win32 ->
ImGui input (keyboard, mouse, wheel), and optimized software painting
via `SetDIBitsToDevice` instead of per-frame GDI `HBITMAP` allocation.

**Overlay / input (scoped to standalone configurator)**

- Skips expensive per-frame rawinput polling in
`ImGui_ImplSpice_NewFrame` when `CONFIGURATOR_STANDALONE` is set (Win32
messages drive input instead).
- Adds software-renderer idle-frame detection (`sw_pixels_dirty` +
draw-data hash) so spicecfg only repaints when pixels actually change.
- Adds `ImGuiWindowFlags_NoScrollWithMouse` on the configurator root
window to prevent scroll jolt on non-scrollable child widgets.
2026-05-30 12:52:35 -07:00
bicarus-dev 31aabe9786 fix option name 2026-05-30 01:22:09 -07:00
bicarus 4ea55a61b8 gitadora: (arena model) asio option (#718)
## Link to GitHub Issue or related Pull Request, if one exists
#717 

## Description of change
Add an option to override the ASIO device.

By default the game will try to use any ASIO driver that contains the
string `XONAR` in it.

## Testing
Tested with FlexASIO.
2026-05-30 00:03:06 -07:00
bicarus 0209b80a22 graphics: address perf issues with DX9 image resize logic (#712)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Move resize logic from `EndScene` to `Present`/`PresentEx` (resolves
Live2D frame drops in some extreme settings)

Plug small memory leak

Remove unnecessary surface Lock/Unlock

## Testing
Tested IIDX, SDVX, DDR.
2026-05-29 09:33:31 -07:00
drmext 0511dfb6ca rawinput: fix spicecfg close delay (#715)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Closing spicecfg was blocked for up to ~495 ms. Runtime behavior is
unchanged: same flush interval and same output path during normal
operation. Only shutdown teardown is faster.

## Testing
Opened and closed spicecfg.exe repeatedly; the window closes immediately
with no hang
2026-05-29 09:33:08 -07:00
drmext b558df3340 cfg: optimize saving config (#714)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Save config xml once instead of ~33 times

## Testing
xml is still saved properly
2026-05-29 09:09:02 -07:00
bicarus 89602cfcab misc: clean up eamuse_get_game (#713)
No need to allocate a new `std::string` every call...
2026-05-29 09:08:07 -07:00
drmext b4557993c9 cfg: optimize saving patches to file on disk (#711)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Saving patches to file used to rewrite the file repeatedly, now it loads
and saves only once. Original file timestamp is now preserved on the
backup.

## Testing
Saved a very large patch set that used to take 10+ seconds; now it is
instant. Confirmed hex diffs are still accurate.
2026-05-29 02:20:39 -07:00
bicarus f119d7c988 overlay: fix z-ordering of subscreen windows (#710)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Subscreen overlay window used to draw above the main game surface, but
below any other ImGui widgets. Fix this so that the subscreen image is
drawn at the same z-level as the sub window itself.

This has been a long-standing issue - at least since 2023 when spice2x
first forked...

## Testing
Tested TDJ in windowed mode.
2026-05-29 02:03:31 -07:00
bicarus b3d8d0aea9 overlay: implement dx11 backend (#707)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #134 

## Description of change
Add ImGui DX11 backend implementation, and integrate into the spice
overlay.

The tricky part is that some of the Unity games:

1. Launch multiple windows, and
2. Can be resized (even full screen games launch at a certain resolution
and then expand to fit desktop resolution)
3. lazy load DX11 DLLs

This PR also adds screenshot functionality, but screen resize is not
implemented.

## Testing

Seems to be working for most games. Need to do final tests for:

- [x] CCJ (attract movie plays as well)
- [x] Busou Shinki (title movie plays)
- [x] MFG
- [x] QuizKnock
- [x] Polaris Chord
- [x] check dx9 for regression
2026-05-29 00:47:28 -07:00
drmext a977cba772 overlay: prevent save button shifting patches text (#709)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
When opening the patches tab by pressing F4 in game, the Save button
doesn't appear until after initially selecting/deselecting a patch, then
it shifts all of the patch text below. To solve this annoyance, this
keeps the Save button in a fixed position (starts greyed out until a
selection) and no longer shifts the text.

## Testing
Confirmed saving selected patches still works properly in the overlay
and standalone cfg
2026-05-28 23:58:09 -07:00
bicarus 489d40d87d utils: dump memory info on crash (#708)
For diagnosing out of memory errors / malloc failures.
2026-05-28 23:57:34 -07:00
bicarus-dev 3a468a9a3b fix up option name 2026-05-27 21:06:05 -07:00
bicarus a2e220d3f4 gitadora: (arena model) unify window layout options (#706)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Combine the two layout options into one

## Testing

- [x] windowed, x4
- [x] windowed, x2
- [x] windowed, x1 with sub
- [x] fs
2026-05-27 21:04:23 -07:00
Azalea febc02b50b [+] Hide sides on gitadora 𝛿 (#705)
## Description of change
Added an option to only disable the left/right screens of gitadora gw
delta while exposing the touch panel screen.

## Testing
Tested by hand on CachyOS + hyprland + gamescope + wine + vnc + phone +
cat 🐈‍⬛

<img width="2560" height="1707" alt="image"
src="https://github.com/user-attachments/assets/a2ef0172-68e2-43c2-89c3-4863698f2f00"
/>
2026-05-27 18:57:10 -07:00
bicarus 311404f56b overlay: toast notifications (#704)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Adds toast notifications to the overlay.

Toast notifications are transient windows (shown for about 4 seconds).
These are drawn on top of the game even when the overlay is inactive
(i.e., when no window is open), and does not cause any input sink
behavior.

Add toasts for some common user-driven actions that could use a
notification, such as:

* screenshot
* card insert
* PIN macro
* API client connect/disconnect
* virtual printer
* screen resize toggle / scene switch

Add an option to change where the toasts are displayed, or to turn it
off entirely.

Add SDK function for it, and add it to the samples.

## Testing
Tested DDR (shown at bottom right, as is for most games) and RB (shown
top right by default).
2026-05-27 00:45:27 -07:00
bicarus-dev d929cdf7c8 clean up for release 2026-05-25 17:30:38 -07:00
bicarus 2b930ad95f gitadora: warn about 2ch audio option not working on arena model (#702)
shrug
2026-05-25 17:00:49 -07:00
bicarus 3c60f3966b rb: more touch options (#699)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Add two options:

1. option to adjust poll rate
2. option to change emulation size

Also, the default configuration is changing from:

* ~1000Hz ish to 120Hz (default)
* 3x3 point to 1x1 point

In theory this does not make a meaningful change to how the game plays.
But if people want the old behavior they can change it.

## Testing
Tested on Volzza2 and Reflesia.
2026-05-25 16:02:17 -07:00
Biduleman c59d399ab8 Automated PIN Macro after auto-inserting a card (#698)
## Description of change
Adds a way to automatically use the PIN Macro for P1, P2 or both when
Auto Card Insert is used.

### Launcher changes: 
 - Added a checkbox to enable/disable the feature
 - Added a textbox for each player to set what to look for in the logs.

### How it works:
The feature requires Auto Card Insert and PIN Macro to be enabled and
set.
It uses the Auto Card Insert value to enable the feature for P1, P2 or
both, with a "master" checkbox to completely disable the feature. It
then relies on the current PIN Macro code to send the PIN to the games.

The games logs send a message when a new scene is loaded with the name
of the scene. By hooking on the logs, we can see when a PIN is required
and send the relevant macro. Games have different scene names, and
sometimes one for each player, so the field is configurable per game per
player. These could be hard-coded in a lookup table but I didn't want to
recompile the project every time I wanted to test, and I don't have all
the games to get this info from.

The PIN Macro thread is used to hook to the logs and check if the
defined string appears, which triggers the PIN macro just like a button
press would.

Setting nothing for the log trigger disables the feature for that
player. It's useful if someone wants to have Auto Card Insert enabled
for both players but only Auto Pin on 1 player, but is mainly there so
the check won't happen if nothing is set.

## Testing
I used SDVX and DDR to have a single and a two player test with the
feature both enabled and disabled. I also tested multiple combinations
of PIN Macro settings and Auto Card Insert settings with no apparent
issues. The PIN is automatically entered as soon as the game requests
it.

## Discussion

I know that hooking to the logs might not be the best solution. I'm very
open to discussions about this, and understand that this might be a deal
breaker for this feature. I made this to use with my setup and thought
I'd try opening this PR to see if the feature would be useful for
others.

I haven't found another way to make sure we're on the correct screen to
launch the PIN Macro, and sending the macro every time the Auto Card
Insert finishes had sync issues where the end of the PIN would actually
be registered by the game.

It would also be much more convenient for the users to hardcode a lookup
table for every games' log-trigger but I don't have the games currently
so I can't check the logs to see what would be best.

---------

Co-authored-by: Maxime St-Pierre <maxime.stpierre@ticketmaster.com>
2026-05-25 16:00:06 -07:00
bicarus f69e40fa26 gitadora: (arena model) booting fullscreen with one monitor (#696)
## Link to GitHub Issue or related Pull Request, if one exists
#477 

## Description of change
By default the game needs 4 monitors.

With the new option to disable subscreens, you can now boot fullscreen
with a single 4K monitor, with subscreen overlay enabled. Side monitors
are not shown in the overlay; only the touch screen is emulated.

## Future work

- [ ] custom resolution (-forceres) doesn't work properly (hooking
ResetEx does allow the game to boot but will render incorrectly;
probably needs hex edits)
- [x] enable this automatically if the user has fewer than 4 monitors
- [ ] ability to boot with a 4k main screen + small subscreen (with no
side screens which do nothing gameplay wise), but this whole display
emulation is a nightmare to work with & test thoroughly so it'll have to
be in the future.


## Testing
Checklist:

- [x] fs on 1 monitor with option set
- [x] fs on 2 monitor with option set
- [x] windowed - default (4 windows, no overlay)
- [x] windowed - with option set (1 window, overlay)
- [x] fs with 4 monitors?
- [x] check popn hc for regression
2026-05-18 15:28:12 -07:00
bicarus e9dcc5717e launcher: option to run as user without elevation (#695)
## Link to GitHub Issue or related Pull Request, if one exists
#694 

## Description of change
Before this change, we have it set in the executable manifest to always
ask for UAC elevation.

This change removes that and instead attempts to prompt for elevation
when we launch, depending on the new developer option that is being
added here to run as current user instead of elevating.

The default behavior continues to run as admin. This is still the safest
in terms of regression risk.

A bit of history on this: while spicecfg never technically needed to
elevate, we continue to do so to avoid difference in behavior when the
game is launched. For example, if the user is using an input remapper
running without elevation, it would inject input into spicecfg running
as user but fail to do so if the game runs elevated.

WinXP builds are unaffected (there is no UAC).

Some features will straight up fail without elevation so this will
remain as a debug option. To name a few (not a complete list):

* reboot/shutdown of PC
* running at realtime process priority
* `-nvprofile` option
* ...

## Testing
2026-05-16 16:27:59 -07:00
drmext 5bec3d5db7 network: icmp emulation (#694)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Add `-icmphook` option under spicecfg Development/Network for emulating
keepalive ping replies in user mode, so games do not need to open
privileged raw ICMP sockets. This makes connecting to networks possible
on macOS under Whisky, and on Windows with ICMP firewalled or spice
running without Administrator privileges.

## Testing
Tested DDR and SDVX on Windows 10 and macOS Tahoe. Network check status
in test menu shows `CONNECTED` when -icmphook is enabled, and `NOT
CONNECTED` when it is disabled. Actual network functionality works as
expected (under previously impossible conditions).
2026-05-14 00:44:14 -07:00
bicarus-dev 6ec2b47345 fix option text 2026-05-13 17:54:25 -07:00
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
bicarus 5f94ef4478 overlay: fix scroll bar jumping around when regaining focus (#673)
## Link to GitHub Issue or related Pull Request, if one exists
Regression caused by #604, probably

## Description of change
When spicecfg or the game loses focus and regains it, scroll bar jumps
around. This was because we weren't keeping proper track of mouse wheel
position while not in focus, so when we gain it back, it's as if the
scroll bar was moved a huge distance.

## Testing
Tested spicecfg and in overlay.
2026-05-03 03:49:52 -07:00
bicarus c4193eddc5 iidx: millisecond-based delay option (#672)
## Link to GitHub Issue or related Pull Request, if one exists
#181 

## Description of change
Use millisecond-based calculation to add artificial delay to the TT
input.

## Testing
Tested 64 bit TDJ and LDJ.
2026-05-03 01:23:45 -07:00
bicarus c8962a0e77 overlay: setting button as always-on (#671)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change

Add a checkbox to the buttons edit dialog that lets you set a device as
"always on", accomplished by setting the device to `Naive`, vkey to
`0xff` (invalid), and `Invert`.

No backend rawinput changes.

## Testing
2026-05-02 22:42:36 -07:00
din b6a749e5db add snek to lights match map (#670)
## Description of change
add snek board to lights mapping

## Testing
with a snek board


<img width="490" height="477"
alt="DeepinScreenshot_select-area_20260502164249"
src="https://github.com/user-attachments/assets/a86affff-74c6-4d5d-b657-fab12c40aaa4"
/>
2026-05-02 15:49:19 -07:00
bicarus-dev 7dbbe9809e update iidx tt delay option text 2026-05-02 02:50:29 -07:00
bicarus 79a3e32d5d iidx: delay logic for iidx tt (#669)
## Link to GitHub Issue or related Pull Request, if one exists
Re-implements fix for #181 as it was removed by #668

## Description of change
Add two options to add artificial delay to turntable input. These
operate directly on the input logic (`get_tt`) which is called by I/O
emulation code.

Functionally, it's **identical** to the `Delay (experimental)` option we
had in the analog tab, just moved to Options tab.

## Testing
64 bit tdj: ok
64 bit ldj bi2a: ok
32 bit ldj ezusb: ok

Worth noting that TDJ bi2x_hook polls at 120Hz, bi2a at ~500Hz, and
ezusb at ~170Hz
2026-05-02 02:17:51 -07:00
bicarus c5a4e954f9 rawinput: remove experimental analog features (delay and relative axis mode) (#668)
## Link to GitHub Issue or related Pull Request, if one exists
Related to #181 

## Description of change
This PR removes two features marked as "experimental" for analog axis -
`Delay` and `Relative Axis`.

They have been in spice as experimental option for almost 2 years.. but
they have a fundamental problem of being tied to how often the analog is
polled. Not just how often the game's I/O emulation code polls it, but
literally any source (spice API, overlay, etc).

For `Delay`, the only real usage I'm aware of is for #181 which is
delaying IIDX turntable input. For this, I will follow up with a
separate PR that adds back the delay option specifically for IIDX.

For `Relative Axis` - I'm not quite sure if any one actually uses this.
This could be implemented per-game if some game demands it. Polaris
Chord already has one, for example.

## Testing
2026-05-02 00:51:21 -07:00
bicarus 7b862768b0 overlay: show name of analog control in popup title (#667) 2026-05-02 00:01:33 -07:00
bicarus af8d8dae9f rawinput: distinguish between circular and linear analog input (#665)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Currently, all analogs are treated as circular values that wrap around.
This works for knobs and turntables, but can get weird for linear values
(like tilt sensors and sliders) especially once analog options are
enabled, such as sensitivity.

This PR groups analogs into 3 buckets: circular, linear (centered), and
linear (positive). Linear types get different algorithms applied to it
when options are set (e.g., sensitivity caps out at 0, 1 instead of
wrapping around).

In linear mode:
* Sensitivity value applies an exponential response curve (power curve)
to the original value.
* Multiplier/divisor serves as a linear cut-off.
* Relative mode works as you expect, but of course, values don't wrap
around.
* Invert and deadzone work as you expect (since they applied to the
controller input anyway)
* Smoothing is removed from the UI and has no effect.
* Delay is unaffected.

Also, fix a small bug with -/+ button not working for analog `Delay`
option.

## Testing
WIP
2026-05-01 19:02:32 -07:00
llm96 d9d5823fdb rawinput: add missing nullptr check (#666)
## Link to GitHub Issue or related Pull Request, if one exists
N/A

## Description of change

Fixes a crash when opening the properties modal on a Naive button bind.


https://github.com/spice2x/spice2x.github.io/blob/82dda3380039bbaa995ee99e2d3effebbcb58082/src/spice2x/overlay/windows/config.cpp#L2081

Binds:
<img width="800" height="600" alt="image"
src="https://github.com/user-attachments/assets/18d1fcfb-085f-4a2b-8fce-36809a4bbb5a"
/>

Stack trace from Debug build:
```
overlay::windows::Config::edit_button_popup(const std::string &, const std::string &, Button *, float, int) config.cpp:2081
overlay::windows::Config::build_button(const std::string &, Button &, Button *, int, int, int) config.cpp:1050
overlay::windows::Config::build_buttons(const std::string &, std::vector<…> *, int, int) config.cpp:718
overlay::windows::Config::build_content() config.cpp:283
overlay::Window::build() window.cpp:96
overlay::SpiceOverlay::new_frame() overlay.cpp:486
graphics_d3d9_on_present(HWND__ *, IDirect3DDevice9 *, IDirect3DDevice9 *) d3d9_backend.cpp:1447
__ZN23WrappedIDirect3DDevice97PresentEPK7tagRECTS2_P6HWND__PK8_RGNDATA@20 d3d9_device.cpp:396
main_implementation(int, char **) launcher.cpp:2453
main launcher.cpp:2671
```

Relevant snippet from error log:
```
[2026/05/01 18:50:17] M:config: tab selection changed from 0 to 1
[2026/05/01 18:50:48] W:signal: exception raised: EXCEPTION_ACCESS_VIOLATION
[2026/05/01 18:50:48] W:troubleshooter: 

/-------------------------- spice2x auto-troubleshooter -----------------------\

  spice2x version: 2026-04-23
  game version: LDJ:J:A:A:2013090900

  the game has crashed
    * share this entire log file with someone for troubleshooting (log.txt)
    * spice will also attempt to create a minidump (minidump.dmp)
        minidump should only be shared with people you trust as it may contain
        sensitive data (PCBID, card ID, etc)

  log level is set to `disable` (either in avs-config.xml or using -loglevel)
      this log file may have omitted important error messages from the game
      if you are troubleshooting crashes or failures, it is recommended that you
      set AVS Log Level (-loglevel) option to `all`

  unsure what to do next?
    * update to the latest version:
        https://github.com/spice2x/spice2x.github.io/releases
    * check the FAQ:
        https://github.com/spice2x/spice2x.github.io/wiki/Known-issues

\------------------------- spice2x auto-troubleshooter ------------------------/

[2026/05/01 18:50:48] I:signal: printing callstack
[2026/05/01 18:50:48] I:stackwalker: 006A7001 (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 0068DF8C (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 0069A7C1 (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 0069B857 (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 0066461F (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 00663B7A (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 0045B548 (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 004FE1CE (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 1008AD13 (bm2dx): (unknown): dll_entry_main
[2026/05/01 18:50:48] I:stackwalker: 100934A7 (bm2dx): (unknown): dll_entry_main
[2026/05/01 18:50:48] I:stackwalker: 10088D24 (bm2dx): (unknown): dll_entry_main
[2026/05/01 18:50:48] I:stackwalker: 0044B8A6 (spice): (unknown): (unknown)
[2026/05/01 18:50:48] I:stackwalker: 77290CB7 (ntdll): (unknown): RtlGetSystemTimeAndBias
[2026/05/01 18:50:53] F:signal: end
[2026/05/01 18:50:53] I:launcher: stopping subsystems
[2026/05/01 18:50:53] I:logger: stop
[2026/05/01 18:50:53] I:audio: stopping
[2026/05/01 18:50:54] I:rawinput: disposing devices
[2026/05/01 18:50:54] I:xinput: destroyed
```

## Testing
Replicated on upstream build from `spice2x-26-04-23-full.zip`
2026-05-01 14:42:45 -07:00
bicarus 82dda33800 graphics: restore monitor settings when fullscreen is lost then restored (#664)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #663 

## Description of change
Apply monitor rotation and refresh rate again when fullscreen focus is
regained.

## Testing
Tested with sdvx7
2026-04-28 16:04:52 -07:00
bicarus 3876e38636 xinput: small fixes (#661)
## Link to GitHub Issue or related Pull Request, if one exists
#616 

## Description of change
Fix UI labels being wrong for analog stick up/down (they were reversed).

Adjust deadzone so that when sticks are bound as digital buttons, they
are not as sensitive.

Small refactoring of code.

## Testing
Tested via spicecfg using my xbox one controller.
2026-04-27 22:19:23 -07:00
bicarus 6bc1357d6a ddr: add support for analog axis-only pads and handle left+right / up+down input (#659)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Some crappy DDR soft mats (either USB-based or console pads with crappy
adapters that lack a dedicated "DDR pad mode") use X and Y analog axis
for arrows, which means they cannot cleanly report Up+Down or Left+Right
input.

According to some ancient Stepmania code, a lot of these report a middle
value (value between center and right, for example, for left+right).
Borrow this hack from Stepmania and implement it in spice as analog
input for DDR.

Also, for rawinput/xinput automatic bind scenario, update the logic so
that face buttons are always detected before analog axis / dpads for
consistency. This would help with binding any DDR pads that
simultaneously input face buttons / dpad / analog axis, so that we can
prefer face buttons when detected.

## Testing
I don't have any soft mats that have this, but it was tested with an
xbox controller.
2026-04-27 20:17:54 -07:00
Will 37218e7fe0 Try and preserve the wideness of std::filesystem::path more (#660)
As noted in #567, a filesystem path that contains non-ascii will break a
lot if using a clang toolchain.

Luckily, fmtlib has a lossy utf8 convert when you use it to print a path
(after including `fmt/std.h`). The vast majority of this diff is just
removing `.string()` from paths inside loggings calls.

There are some callsites I _didn't_ touch, mainly the options, because
it would be an ABI break to change those to be wide strings and I cbf
looking into settings upgrades. There are also some spots (avs mountpath
remapping, for example) where the path is guaranteed to be ascii, so I
didn't modify them.

ImGui doesn't appear to easily support wide strings (I mean, surely it
does, but I'm not gonna look too far into it) so I mostly just left
those alone too, with a few spots modified to re-use fmtlib's lossy
utf8.

Some of the changes are basically never gonna be hit IRL, like who would
put a file with a non-ascii _extension_ along with their modules? But
the diff is (I hope) pretty easy to validate as OK.

Testing has been somewhat minimal, I fired up the GCC build of spice2x
in a dodgy folder name, got mojibake (running via wine in linux so take
that as you will), ran the unmodified clang spice and crashed the same
way the reporter did. After modification, I get the exact same mojibake
so I assume if the terminal enjoys utf8 it'll display OK.

Claude (only used for review) thinks the commit is fine but is annoyed
that I use `fmt::detail` in the appdata censoring, which is part of the
private API; personally I don't care because it's pretty stable.
2026-04-27 20:07:15 -07:00
bicarus 678e11eade rawinput: fix up devices that report invalid max values (#658)
## Link to GitHub Issue or related Pull Request, if one exists
regression caused by #653

## Description of change
Some controllers - like the Xbox One controller I have - reports
0xffffffff as the max range value for analog, despite reporting 16-bit
width. This causes us to consider 0xffffffff as -1 so the range became
[0, -1] which is invalid.

Our automatic calibration does eventually fix this (-1 will fix itself
to become the highest actual value reported, which would be 0xffff).

This seems to be a common issue with XInput-based controllers in
general: see `Correct_Axis_Max` in
https://github.com/argonlefou/DemulShooter/blob/master/DsCore/RawInput/RawInputController.cs

#653 removed the fix that was masking this issue. Put in a new fix that
fixes these up.

## Testing
Tested with Xbox One controller.
2026-04-27 01:41:44 -07:00
bicarus f72313fe45 sdvx: work around bad logger configuration (#657)
## Link to GitHub Issue or related Pull Request, if one exists
#345 

## Description of change
SDVX4 data from certain private forum has bad logger configuration and
crashes on boot. Add an explicit workaround to fix up that broken
`avs-config.xml` file, scoped to `soundvoltex.dll` + 32-bit.

## Testing
Tested HH.
2026-04-23 01:33:49 -07:00
bicarus-dev 06374ef78a cosmetic fix to build script 2026-04-22 23:14:03 -07:00
bicarus-dev bdde1ec6b8 update ifs_hook missing warning message 2026-04-22 22:56:55 -07:00
bicarus-dev d2176fe4f0 clear out button properties when switching devices in edit dialog 2026-04-22 22:48:29 -07:00
bicarus fb9eef904a popn: block keypad overlay window in new cab i/o (#656)
Redirect users to the subscreen overlay when keypad window is open.
2026-04-22 22:21:18 -07:00
bicarus 48903998a5 ea3: fix boot failure when -e is specified to override ea3-config path (#655)
## Link to GitHub Issue or related Pull Request, if one exists
Regression introduced by #595 

## Description of change
`avs::core::file_exists` fails if you use `\` as path separator... what
I meant to use was `fileutils::file_exists`.

This would have caused failures - mostly for gitadora players - if they
used `prop\ea3-config2.xml` for example.

## Testing
2026-04-22 01:55:13 -07:00
bicarus f2dca0265f rawinput: custom threshold for BAT (button-analog type) (#654)
## Link to GitHub Issue or related Pull Request, if one exists
#653 

## Description of change
By default, BAT Positive / Negative binds trigger when the stick is
tilted 10% off the center.

This needs a bit more customization since:

1. User may want to customize the center point (e.g., guitar controller
neutral position)
2. User may want to adjust the sensitivity (e.g., guitar controller tilt
angle to trigger wailing).

## Testing
WIP
2026-04-22 01:14:15 -07:00
bicarus 1147ed9858 rawinput: fix signed range values (#653)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Official GuitarFreaks controller has the motion sensor values with
`valuemin=-127` to `valuemax=127` and we don't deal with negative values
correctly.

## Testing
tbd
2026-04-21 22:56:27 -07:00
bicarus 19d93f4ffd audio: deal with missing DirectSoundI3DL2ReverbDMO better (#652)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
On some Win11 systems,
`CoCreateInstance(CLSID_DirectSoundI3DL2ReverbDMO)` started to fail with
`ERROR_MOD_NOT_FOUND` instead of `REGDB_E_CLASSNOTREG` for some reason.

## Testing
User tested.
2026-04-21 18:42:21 -07:00
161 changed files with 12239 additions and 1245 deletions
+62 -4
View File
@@ -235,6 +235,13 @@ add_compile_definitions(
_WIN32_IE=0x0400 _WIN32_IE=0x0400
) )
# SPICE_XP: set by build_all.sh for the WinXP-compat toolchains. Gates out
# the DX11 overlay backend (WinXP lacks D3DCOMPILER_47.dll).
option(SPICE_XP "Build for the WinXP-compat toolchain" OFF)
if(SPICE_XP)
add_compile_definitions(SPICE_XP=1)
endif()
# acioemu log # acioemu log
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DACIOEMU_LOG") #set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DACIOEMU_LOG")
@@ -409,6 +416,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/nost/io.cpp games/nost/io.cpp
games/nost/poke.cpp games/nost/poke.cpp
games/gitadora/gitadora.cpp games/gitadora/gitadora.cpp
games/gitadora/asio.cpp
games/gitadora/io.cpp games/gitadora/io.cpp
games/gitadora/handle.cpp games/gitadora/handle.cpp
games/gitadora/j32d.cpp games/gitadora/j32d.cpp
@@ -496,6 +504,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/audio/backends/mmdevice/device_enumerator.cpp hooks/audio/backends/mmdevice/device_enumerator.cpp
hooks/audio/backends/wasapi/audio_client.cpp hooks/audio/backends/wasapi/audio_client.cpp
hooks/audio/backends/wasapi/audio_render_client.cpp hooks/audio/backends/wasapi/audio_render_client.cpp
hooks/audio/backends/wasapi/downmix.cpp
hooks/audio/backends/wasapi/dummy_audio_client.cpp hooks/audio/backends/wasapi/dummy_audio_client.cpp
hooks/audio/backends/wasapi/dummy_audio_clock.cpp hooks/audio/backends/wasapi/dummy_audio_clock.cpp
hooks/audio/backends/wasapi/dummy_audio_render_client.cpp hooks/audio/backends/wasapi/dummy_audio_render_client.cpp
@@ -517,12 +526,19 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp
hooks/graphics/backends/d3d9/d3d9_swapchain.cpp hooks/graphics/backends/d3d9/d3d9_swapchain.cpp
hooks/graphics/backends/d3d9/d3d9_texture.cpp hooks/graphics/backends/d3d9/d3d9_texture.cpp
hooks/graphics/backends/d3d11/d3d11_backend.cpp
hooks/graphics/backends/d3d11/d3d11_swapchain.cpp
hooks/graphics/backends/d3d11/d3d11_factory.cpp
hooks/graphics/backends/d3d11/d3d11_vtable_capture.cpp
hooks/graphics/backends/d3d11/d3d11_screenshot.cpp
hooks/input/dinput8/fake_backend.cpp hooks/input/dinput8/fake_backend.cpp
hooks/input/dinput8/fake_device.cpp hooks/input/dinput8/fake_device.cpp
hooks/input/dinput8/hook.cpp hooks/input/dinput8/hook.cpp
hooks/lang.cpp hooks/lang.cpp
hooks/libraryhook.cpp hooks/libraryhook.cpp
hooks/networkhook.cpp hooks/networkhook.cpp
hooks/icmphook_net.cpp
hooks/icmphook_iphlpapi.cpp
hooks/powrprof.cpp hooks/powrprof.cpp
#hooks/rom.cpp #hooks/rom.cpp
hooks/setupapihook.cpp hooks/setupapihook.cpp
@@ -556,6 +572,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
# overlay # overlay
overlay/overlay.cpp overlay/overlay.cpp
overlay/notifications.cpp
overlay/window.cpp overlay/window.cpp
overlay/imgui/extensions.cpp overlay/imgui/extensions.cpp
overlay/imgui/impl_spice.cpp overlay/imgui/impl_spice.cpp
@@ -607,6 +624,9 @@ set(SOURCE_FILES ${SOURCE_FILES}
reader/structuredmessage.cpp reader/structuredmessage.cpp
reader/crypt.cpp reader/crypt.cpp
# sdk
sdk/sdk.cpp
# stubs # stubs
stubs/stubs.cpp stubs/stubs.cpp
@@ -633,6 +653,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
util/time.cpp util/time.cpp
util/cpuutils.cpp util/cpuutils.cpp
util/netutils.cpp util/netutils.cpp
util/precise_timer.cpp
util/sysutils.cpp util/sysutils.cpp
util/lz77.cpp util/lz77.cpp
util/tapeled.cpp util/tapeled.cpp
@@ -707,6 +728,10 @@ target_link_libraries(spicetools_spice64 PUBLIC winscard)
set_target_properties(spicetools_spice64 PROPERTIES PREFIX "") set_target_properties(spicetools_spice64 PROPERTIES PREFIX "")
set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64") set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64")
target_compile_definitions(spicetools_spice64 PRIVATE SPICE64=1) target_compile_definitions(spicetools_spice64 PRIVATE SPICE64=1)
if(NOT SPICE_XP)
target_link_libraries(spicetools_spice64 PUBLIC d3d11 dxgi d3dcompiler)
target_compile_definitions(spicetools_spice64 PRIVATE SPICE_D3D11=1)
endif()
IF(NOT MSVC) IF(NOT MSVC)
set_target_properties(spicetools_spice64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64") set_target_properties(spicetools_spice64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
@@ -726,6 +751,9 @@ set_target_properties(spicetools_spice64_linux PROPERTIES PREFIX "")
set_target_properties(spicetools_spice64_linux PROPERTIES OUTPUT_NAME "spice64_linux") set_target_properties(spicetools_spice64_linux PROPERTIES OUTPUT_NAME "spice64_linux")
target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE64=1) target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE64=1)
target_compile_definitions(spicetools_spice64_linux PRIVATE NO_SCARD=1 PRIVATE SPICE_LINUX=1) target_compile_definitions(spicetools_spice64_linux PRIVATE NO_SCARD=1 PRIVATE SPICE_LINUX=1)
# spice64_linux is never built under the WinXP toolchain, so dx11 is always on.
target_link_libraries(spicetools_spice64_linux PUBLIC d3d11 dxgi d3dcompiler)
target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE_D3D11=1)
IF(NOT MSVC) IF(NOT MSVC)
set_target_properties(spicetools_spice64_linux PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64") set_target_properties(spicetools_spice64_linux PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
@@ -738,7 +766,7 @@ set(SOURCE_FILES ${SOURCE_FILES} launcher/options.h launcher/options.cpp)
set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc) set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc)
add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES}) add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
target_link_libraries(spicetools_cfg target_link_libraries(spicetools_cfg
PUBLIC ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features) PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
target_link_libraries(spicetools_cfg PUBLIC winscard) target_link_libraries(spicetools_cfg PUBLIC winscard)
set_target_properties(spicetools_cfg PROPERTIES PREFIX "") set_target_properties(spicetools_cfg PROPERTIES PREFIX "")
@@ -756,7 +784,7 @@ set(SOURCE_FILES ${SOURCE_FILES} launcher/options.h launcher/options.cpp)
set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc) set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc)
add_executable(spicetools_cfg_linux WIN32 ${SOURCE_FILES} ${RESOURCE_FILES}) add_executable(spicetools_cfg_linux WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
target_link_libraries(spicetools_cfg_linux target_link_libraries(spicetools_cfg_linux
PUBLIC ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features) PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
set_target_properties(spicetools_cfg_linux PROPERTIES PREFIX "") set_target_properties(spicetools_cfg_linux PROPERTIES PREFIX "")
set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux") set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux")
@@ -860,6 +888,36 @@ if(NOT MSVC)
set_target_properties(spicetools_stubs_cpusbxpkm PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32") set_target_properties(spicetools_stubs_cpusbxpkm PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32")
endif() 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 # output directories
#################### ####################
@@ -869,14 +927,14 @@ set_target_properties(spicetools_cfg spicetools_cfg_linux
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools") RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools")
# output 32bit # 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 PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive32" ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive32"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32" LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32") RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32")
# output 64bit # 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 PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive64" ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive64"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64" LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64"
+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 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. 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 ## License
Unless otherwise noted, all files are licensed under the GPLv3. Unless otherwise noted, all files are licensed under the GPLv3.
See the LICENSE file for the full license text. See the LICENSE file for the full license text.
+22 -7
View File
@@ -1,13 +1,16 @@
#include "mdxf.h" #include "mdxf.h"
#include "mdxf_poll.h" #include "mdxf_poll.h"
#include <mutex>
#include "avs/game.h" #include "avs/game.h"
#include "games/ddr/ddr.h"
#include "games/ddr/io.h" #include "games/ddr/io.h"
#include "launcher/launcher.h" #include "launcher/launcher.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
#include <mutex>
#define MDFX_DEBUG_VERBOSE 0 #define MDFX_DEBUG_VERBOSE 0
@@ -108,9 +111,10 @@ static void mdxf_thread_start() {
MDXF_THREAD = std::thread([] { MDXF_THREAD = std::thread([] {
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL); SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
timeutils::PreciseSleepTimer timer;
while (MDXF_THREAD_RUNNING.load(std::memory_order_acquire)) { while (MDXF_THREAD_RUNNING.load(std::memory_order_acquire)) {
mdxf_poll(false); mdxf_poll(false);
std::this_thread::sleep_for(THREAD_PERIOD); timer.sleep(THREAD_PERIOD);
} }
}); });
} }
@@ -357,14 +361,17 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFP
// decide on button map // decide on button map
const size_t *button_map = nullptr; const size_t *button_map = nullptr;
int player = 0;
switch (node) { switch (node) {
case 17: case 17:
case 25: case 25:
button_map = &buttons_p1[0]; button_map = &buttons_p1[0];
player = 1;
break; break;
case 18: case 18:
case 26: case 26:
button_map = &buttons_p2[0]; button_map = &buttons_p2[0];
player = 2;
break; break;
} }
@@ -374,19 +381,27 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFP
if (source == EXTERNAL_POLL) { if (source == EXTERNAL_POLL) {
// get buttons // get buttons
auto &buttons = games::ddr::get_buttons(); auto &buttons = games::ddr::get_buttons();
// get analogs
bool analog_left = false;
bool analog_right = false;
games::ddr::get_analog_x_axis(player, analog_left, analog_right);
bool analog_up = false;
bool analog_down = false;
games::ddr::get_analog_y_axis(player, analog_up, analog_down);
uint8_t up_down = 0; uint8_t up_down = 0;
uint8_t left_right = 0; uint8_t left_right = 0;
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[0])) || analog_up) {
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[0]))) {
up_down |= 0xF0; up_down |= 0xF0;
} }
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[1]))) { if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[1])) || analog_down) {
up_down |= 0x0F; up_down |= 0x0F;
} }
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[2]))) { if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[2])) || analog_left) {
left_right |= 0xF0; left_right |= 0xF0;
} }
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[3]))) { if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[3])) || analog_right) {
left_right |= 0x0F; left_right |= 0x0F;
} }
current_state = (uint16_t(up_down) << 8) | left_right; current_state = (uint16_t(up_down) << 8) | left_right;
+1
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <cstdint>
#include <string> #include <string>
#include "util/circular_buffer.h" #include "util/circular_buffer.h"
+3 -1
View File
@@ -5,6 +5,7 @@
#include "games/sdvx/sdvx.h" #include "games/sdvx/sdvx.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
using namespace acioemu; using namespace acioemu;
@@ -46,11 +47,12 @@ ICCADevice::ICCADevice(bool flip_order, bool keypad_thread, uint8_t node_count)
// keypad thread for faster polling // keypad thread for faster polling
if (keypad_thread) { if (keypad_thread) {
this->keypad_thread = new std::thread([this]() { this->keypad_thread = new std::thread([this]() {
timeutils::PreciseSleepTimer timer;
while (this->cards) { while (this->cards) {
for (int unit = 0; unit < this->node_count; unit++) { for (int unit = 0; unit < this->node_count; unit++) {
this->update_keypad(unit, false); this->update_keypad(unit, false);
} }
Sleep(7); timer.sleep(7);
} }
}); });
} }
+8
View File
@@ -11,6 +11,8 @@
#include "util/logging.h" #include "util/logging.h"
#include "util/utils.h" #include "util/utils.h"
#include "overlay/notifications.h"
#include "module.h" #include "module.h"
#include "modules/analogs.h" #include "modules/analogs.h"
#include "modules/buttons.h" #include "modules/buttons.h"
@@ -197,6 +199,9 @@ void Controller::connection_handler(api::ClientState client_state) {
// log connection // log connection
log_info("api", "client connected: {}", client_address_str); log_info("api", "client connected: {}", client_address_str);
overlay::notifications::add(
overlay::notifications::Severity::Success,
fmt::format("API client connected ({})", client_address_str));
client_states_m.lock(); client_states_m.lock();
client_states.emplace_back(&client_state); client_states.emplace_back(&client_state);
client_states_m.unlock(); client_states_m.unlock();
@@ -277,6 +282,9 @@ void Controller::connection_handler(api::ClientState client_state) {
// log disconnect // log disconnect
log_info("api", "client disconnected: {}", client_address_str); log_info("api", "client disconnected: {}", client_address_str);
overlay::notifications::add(
overlay::notifications::Severity::Info,
fmt::format("API client disconnected ({})", client_address_str));
client_states_m.lock(); client_states_m.lock();
client_states.erase(std::remove(client_states.begin(), client_states.end(), &client_state)); client_states.erase(std::remove(client_states.begin(), client_states.end(), &client_state));
client_states_m.unlock(); client_states_m.unlock();
+4
View File
@@ -6,6 +6,7 @@
#include "launcher/launcher.h" #include "launcher/launcher.h"
#include "games/io.h" #include "games/io.h"
#include "util/utils.h" #include "util/utils.h"
#include "acio/mdxf/mdxf_poll.h"
using namespace std::placeholders; using namespace std::placeholders;
using namespace rapidjson; using namespace rapidjson;
@@ -104,6 +105,7 @@ namespace api::modules {
for (auto &analog : *this->analogs) { for (auto &analog : *this->analogs) {
analog.override_enabled = false; analog.override_enabled = false;
} }
mdxf_poll(true);
} }
return; return;
} }
@@ -148,6 +150,7 @@ namespace api::modules {
if (analog.getName() == name) { if (analog.getName() == name) {
analog.override_state = CLAMP(state, 0.f, 1.f); analog.override_state = CLAMP(state, 0.f, 1.f);
analog.override_enabled = true; analog.override_enabled = true;
mdxf_poll(true);
return true; return true;
} }
} }
@@ -167,6 +170,7 @@ namespace api::modules {
for (auto &analog : *this->analogs) { for (auto &analog : *this->analogs) {
if (analog.getName() == name) { if (analog.getName() == name) {
analog.override_enabled = false; analog.override_enabled = false;
mdxf_poll(true);
return true; return true;
} }
} }
+4
View File
@@ -6,6 +6,7 @@
#include "launcher/launcher.h" #include "launcher/launcher.h"
#include "games/io.h" #include "games/io.h"
#include "util/utils.h" #include "util/utils.h"
#include "acio/mdxf/mdxf_poll.h"
using namespace std::placeholders; using namespace std::placeholders;
using namespace rapidjson; using namespace rapidjson;
@@ -107,6 +108,7 @@ namespace api::modules {
for (auto &button : *this->buttons) { for (auto &button : *this->buttons) {
button.override_enabled = false; button.override_enabled = false;
} }
mdxf_poll(true);
} }
return; return;
} }
@@ -153,6 +155,7 @@ namespace api::modules {
GameAPI::Buttons::BUTTON_PRESSED : GameAPI::Buttons::BUTTON_NOT_PRESSED; GameAPI::Buttons::BUTTON_PRESSED : GameAPI::Buttons::BUTTON_NOT_PRESSED;
button.override_velocity = CLAMP(state, 0.f, 1.f); button.override_velocity = CLAMP(state, 0.f, 1.f);
button.override_enabled = true; button.override_enabled = true;
mdxf_poll(true);
return true; return true;
} }
} }
@@ -172,6 +175,7 @@ namespace api::modules {
for (auto &button : *this->buttons) { for (auto &button : *this->buttons) {
if (button.getName() == name) { if (button.getName() == name) {
button.override_enabled = false; button.override_enabled = false;
mdxf_poll(true);
return true; return true;
} }
} }
+4 -2
View File
@@ -7,6 +7,7 @@
#include "avs/game.h" #include "avs/game.h"
#include "external/rapidjson/document.h" #include "external/rapidjson/document.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "util/precise_timer.h"
using namespace std::placeholders; using namespace std::placeholders;
using namespace rapidjson; using namespace rapidjson;
@@ -58,6 +59,7 @@ namespace api::modules {
// get params // get params
auto keypad = req.params[0].GetUint(); auto keypad = req.params[0].GetUint();
auto input = std::string(req.params[1].GetString()); auto input = std::string(req.params[1].GetString());
timeutils::PreciseSleepTimer timer;
// process all chars // process all chars
for (auto c : input) { for (auto c : input) {
@@ -91,11 +93,11 @@ namespace api::modules {
// set // set
eamuse_set_keypad_overrides(keypad, state); eamuse_set_keypad_overrides(keypad, state);
Sleep(sleep_time); timer.sleep(sleep_time);
// unset // unset
eamuse_set_keypad_overrides(keypad, 0); eamuse_set_keypad_overrides(keypad, 0);
Sleep(sleep_time); timer.sleep(sleep_time);
} }
} }
+3 -3
View File
@@ -58,7 +58,7 @@ namespace api::modules {
// check if file exists in modules // check if file exists in modules
if (!fileutils::file_exists(dll_path)) { if (!fileutils::file_exists(dll_path)) {
return error(res, "Couldn't find " + dll_path.string()); return error(res, fmt::format("Couldn't find {}", dll_path));
} }
// get module // get module
@@ -118,7 +118,7 @@ namespace api::modules {
// check if file exists in modules // check if file exists in modules
if (!fileutils::file_exists(dll_path)) { if (!fileutils::file_exists(dll_path)) {
return error(res, "Couldn't find " + dll_path.string()); return error(res, fmt::format("Couldn't find {}", dll_path));
} }
// get module // get module
@@ -196,7 +196,7 @@ namespace api::modules {
// check if file exists in modules // check if file exists in modules
if (!fileutils::file_exists(dll_path)) { if (!fileutils::file_exists(dll_path)) {
return error(res, "Couldn't find " + dll_path.string()); return error(res, fmt::format("Couldn't find {}", dll_path));
} }
// get module // get module
+3 -1
View File
@@ -5,6 +5,7 @@
#include <utility> #include <utility>
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
@@ -16,6 +17,7 @@ namespace api {
controller->init_state(this->state); controller->init_state(this->state);
this->thread = new std::thread([this] () { this->thread = new std::thread([this] () {
log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud); log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud);
timeutils::PreciseSleepTimer timer;
// read buffer // read buffer
uint8_t read_buffer[16*1024]; uint8_t read_buffer[16*1024];
@@ -160,7 +162,7 @@ namespace api {
// slow down on reconnect // slow down on reconnect
if (this->running) { if (this->running) {
Sleep(retry_time); timer.sleep(retry_time);
} }
} }
}); });
+7
View File
@@ -5,6 +5,7 @@
#include "util/utils.h" #include "util/utils.h"
#include "util/rc4.h" #include "util/rc4.h"
#include "util/logging.h" #include "util/logging.h"
#include "overlay/notifications.h"
#include "controller.h" #include "controller.h"
using namespace headsocket; using namespace headsocket;
@@ -91,12 +92,18 @@ namespace api {
// log connection // log connection
log_info("api::websocket", "client connected"); log_info("api::websocket", "client connected");
overlay::notifications::add(
overlay::notifications::Severity::Success,
"API websocket client connected");
} }
void WebSocketClient::on_disconnect() { void WebSocketClient::on_disconnect() {
// log disconnection // log disconnection
log_info("api::websocket", "client disconnected"); log_info("api::websocket", "client disconnected");
overlay::notifications::add(
overlay::notifications::Severity::Info,
"API websocket client disconnected");
// get pointer to server // get pointer to server
auto srv = reinterpret_cast<WebSocketServer *>(server().get()); auto srv = reinterpret_cast<WebSocketServer *>(server().get());
+14 -5
View File
@@ -1516,7 +1516,7 @@ namespace avs {
" * It's also possible that you have incomplete game data\n" " * It's also possible that you have incomplete game data\n"
" * Do NOT copy over random DLLs from another game installation; DLL must match game version\n" " * Do NOT copy over random DLLs from another game installation; DLL must match game version\n"
"\n" "\n"
, DLL_NAME, MODULE_PATH.string()) }; , DLL_NAME, MODULE_PATH) };
log_warning("avs-ea3", "{}", info_str); log_warning("avs-ea3", "{}", info_str);
log_fatal("avs-ea3", "Failed to find critical avs DLL on disk (avs2-core.dll OR {})", DLL_NAME); log_fatal("avs-ea3", "Failed to find critical avs DLL on disk (avs2-core.dll OR {})", DLL_NAME);
} }
@@ -1757,8 +1757,8 @@ namespace avs {
{ {
deferredlogs::defer_error_messages({ deferredlogs::defer_error_messages({
"AVS filesystem initialization failure was previously detected during boot!", "AVS filesystem initialization failure was previously detected during boot!",
fmt::format(" ERROR: directory could not be created: {}", src_path.string().c_str()), fmt::format(" ERROR: directory could not be created: {}", src_path),
fmt::format(" if you see a crash, it may have been caused by bad <mounttable> contents in {}", avs::core::CFG_PATH.c_str()), fmt::format(" if you see a crash, it may have been caused by bad <mounttable> contents in {}", avs::core::CFG_PATH),
" fix the XML file and try again" " fix the XML file and try again"
}); });
} }
@@ -1780,14 +1780,14 @@ namespace avs {
auto created = std::filesystem::create_directories(real_path, err); auto created = std::filesystem::create_directories(real_path, err);
if (created) { if (created) {
log_info("avs-core", "created '{}' at '{}'", avs_path, real_path.string()); log_info("avs-core", "created '{}' at '{}'", avs_path, real_path);
} }
if (err) { if (err) {
avs_dir_err(real_path); avs_dir_err(real_path);
log_warning("avs-core", "failed to create '{}' folder at '{}': {}", log_warning("avs-core", "failed to create '{}' folder at '{}': {}",
avs_path, avs_path,
real_path.string(), real_path,
err.message()); err.message());
} }
} }
@@ -1921,6 +1921,15 @@ namespace avs {
// create nvram and raw directories if possible for mounttable configurations // create nvram and raw directories if possible for mounttable configurations
create_avs_config_fs_table(config, config_node); create_avs_config_fs_table(config, config_node);
#if !SPICE64
// sdvx4 bad log config fix
if (avs::game::DLL_NAME == "soundvoltex.dll" && // it's too early for avs::game::is_model
property_search_safe(config, config_node, "/log/enable_console")) {
log_info("avs-core", "applying SDVX4 avs-config.xml fix for <log><enable_console>");
property_search_remove_safe(config, config_node, "/log/enable_console");
}
#endif
// set log level // set log level
if (!LOG_LEVEL_CUSTOM.empty()) { if (!LOG_LEVEL_CUSTOM.empty()) {
property_search_remove_safe(config, config_node, "/log/level"); property_search_remove_safe(config, config_node, "/log/level");
+5 -3
View File
@@ -151,7 +151,7 @@ namespace avs {
" * It's also possible that you have incomplete game data\n" " * It's also possible that you have incomplete game data\n"
" * Do NOT copy over random DLLs from another game installation; DLL must match game version\n" " * Do NOT copy over random DLLs from another game installation; DLL must match game version\n"
"\n" "\n"
, DLL_NAME, MODULE_PATH.string()) }; , DLL_NAME, MODULE_PATH) };
log_warning("avs-ea3", "{}", info_str); log_warning("avs-ea3", "{}", info_str);
log_fatal("avs-ea3", "Failed to find critical ea3 DLL on disk (avs2-ea3.dll OR {})", DLL_NAME); log_fatal("avs-ea3", "Failed to find critical ea3 DLL on disk (avs2-ea3.dll OR {})", DLL_NAME);
@@ -217,8 +217,10 @@ namespace avs {
ea3_config_name = "prop/eamuse-config.xml"; ea3_config_name = "prop/eamuse-config.xml";
} }
if (avs::core::file_exists(ea3_config_name)) { if (fileutils::file_exists(ea3_config_name)) {
log_info("avs-ea3", "booting (using {})", ea3_config_name); log_info("avs-ea3", "found {} on disk", ea3_config_name);
} else if (CFG_PATH.size()) {
log_fatal("avs-ea3", "user-specified ea3 config file is missing: {}", ea3_config_name);
} else { } else {
log_warning("avs-ea3", "looked for the following files in order:"); log_warning("avs-ea3", "looked for the following files in order:");
log_warning("avs-ea3", " * prop/ea3-config.xml"); log_warning("avs-ea3", " * prop/ea3-config.xml");
+4 -5
View File
@@ -73,11 +73,10 @@ namespace avs {
// load game instance // load game instance
const auto dll_path = MODULE_PATH / DLL_NAME; const auto dll_path = MODULE_PATH / DLL_NAME;
const auto dll_path_s = dll_path.string(); log_info("avs-game", "DLL path: {}", dll_path);
log_info("avs-game", "DLL path: {}", dll_path_s.c_str());
// MAX_PATH is 260 // MAX_PATH is 260
if (130 <= dll_path_s.length()) { if (const auto dll_path_len = dll_path.wstring().length(); 130 <= dll_path_len) {
log_warning( log_warning(
"avs-game", "avs-game",
"PATH TOO LONG WARNING\n\n" "PATH TOO LONG WARNING\n\n"
@@ -93,7 +92,7 @@ namespace avs {
"long, often resulting in random crashes. Move the game contents to\n" "long, often resulting in random crashes. Move the game contents to\n"
"a directory with shorter path.\n" "a directory with shorter path.\n"
"-------------------------------------------------------------------\n\n", "-------------------------------------------------------------------\n\n",
dll_path_s, dll_path_s.length()); dll_path, dll_path_len);
} }
// ddr gamemdx.dll user error // ddr gamemdx.dll user error
@@ -112,7 +111,7 @@ namespace avs {
// file not found on disk // file not found on disk
if (!fileutils::file_exists(dll_path)) { if (!fileutils::file_exists(dll_path)) {
log_warning("avs-game", "game DLL could not be found on disk: {}", dll_path.string().c_str()); log_warning("avs-game", "game DLL could not be found on disk: {}", dll_path);
log_warning("avs-game", "double check -exec and -modules parameters; unless you know what you're doing, leave them blank"); log_warning("avs-game", "double check -exec and -modules parameters; unless you know what you're doing, leave them blank");
} }
+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"/> <button name="VEFX" vkey="10" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
</buttons> </buttons>
<analogs> <analogs>
<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="0"/> <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="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> </analogs>
<lights> <lights>
<light name="P1 1" devid="Player 1" index="0"/> <light name="P1 1" devid="Player 1" index="0"/>
@@ -117,8 +117,8 @@
<keypad_buttons/> <keypad_buttons/>
<!-- Assumes analog mode. Need to invert direction since this FW is designed for INF. --> <!-- Assumes analog mode. Need to invert direction since this FW is designed for INF. -->
<analogs> <analogs>
<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="0"/> <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="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> </analogs>
<lights> <lights>
<light name="P1 1" devid="arcin (1p)" index="0"/> <light name="P1 1" devid="arcin (1p)" index="0"/>
@@ -144,34 +144,34 @@
<buttons/> <buttons/>
<keypad_buttons/> <keypad_buttons/>
<analogs> <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" relative="false" delay="0"/> <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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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> </analogs>
<lights> <lights>
<light name="Key 1 R" devid="Lights 01-14" index="0"/> <light name="Key 1 R" devid="Lights 01-14" index="0"/>
@@ -277,9 +277,9 @@
</buttons> </buttons>
<keypad_buttons/> <keypad_buttons/>
<analogs> <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" relative="false" delay="0"/> <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="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="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> </analogs>
<lights/> <lights/>
<!-- Gen 1 FAUCETWO in HID mode; should also work for later gens --> <!-- Gen 1 FAUCETWO in HID mode; should also work for later gens -->
@@ -296,8 +296,8 @@
</buttons> </buttons>
<keypad_buttons/> <keypad_buttons/>
<analogs> <analogs>
<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="0"/> <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="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> </analogs>
<lights> <lights>
<light name="BT-A" devid="Lights (MI_02)" index="24"/> <light name="BT-A" devid="Lights (MI_02)" index="24"/>
+1 -1
View File
@@ -23,7 +23,7 @@
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security> <security>
<requestedPrivileges> <requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/> <requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges> </requestedPrivileges>
</security> </security>
</trustInfo> </trustInfo>
+14 -16
View File
@@ -68,8 +68,8 @@ DIST_FOLDER="./dist"
DIST_NAME="spice2x-$(date +%y)-$(date +%m)-$(date +%d).zip" DIST_NAME="spice2x-$(date +%y)-$(date +%m)-$(date +%d).zip"
DIST_NAME_EXTRAS="spice2x-$(date +%y)-$(date +%m)-$(date +%d)-full.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.' 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_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" 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_XP32="spicetools_cfg spicetools_spice"
TARGETS_XP64="spicetools_spice64" TARGETS_XP64="spicetools_spice64"
@@ -163,6 +163,7 @@ time (
if ((BUILD_XP > 0)) if ((BUILD_XP > 0))
then then
# 32 bit Windows XP # 32 bit Windows XP
echo ""
echo "Building 32bit targets (WinXP toolchain)..." echo "Building 32bit targets (WinXP toolchain)..."
echo "=========================" echo "========================="
if ((CLEAN_BUILD > 0)) if ((CLEAN_BUILD > 0))
@@ -171,7 +172,7 @@ time (
fi fi
mkdir -p ${BUILDDIR_WINXP_32} mkdir -p ${BUILDDIR_WINXP_32}
pushd ${BUILDDIR_WINXP_32} > /dev/null pushd ${BUILDDIR_WINXP_32} > /dev/null
CXXFLAGS="$CXXFLAGS -DSPICE_XP=1" cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_32} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} "$OLDPWD" && ninja ${TARGETS_XP32} cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_32} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DSPICE_XP=ON "$OLDPWD" && ninja ${TARGETS_XP32}
popd > /dev/null popd > /dev/null
# 64 bit Windows XP # 64 bit Windows XP
@@ -184,9 +185,10 @@ time (
fi fi
mkdir -p ${BUILDDIR_WINXP_64} mkdir -p ${BUILDDIR_WINXP_64}
pushd ${BUILDDIR_WINXP_64} > /dev/null pushd ${BUILDDIR_WINXP_64} > /dev/null
CXXFLAGS="$CXXFLAGS -DSPICE_XP=1" cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_64} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} "$OLDPWD" && ninja ${TARGETS_XP64} cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_64} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DSPICE_XP=ON "$OLDPWD" && ninja ${TARGETS_XP64}
popd > /dev/null popd > /dev/null
else else
echo ""
echo "Skipping WinXP builds, toolchain not specified" echo "Skipping WinXP builds, toolchain not specified"
fi fi
@@ -267,6 +269,8 @@ rm -rf ${OUTDIR_EXTRAS}
mkdir -p ${OUTDIR_EXTRAS} mkdir -p ${OUTDIR_EXTRAS}
mkdir -p ${OUTDIR_EXTRAS}/largeaddressaware mkdir -p ${OUTDIR_EXTRAS}/largeaddressaware
mkdir -p ${OUTDIR_EXTRAS}/linux mkdir -p ${OUTDIR_EXTRAS}/linux
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/32
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/64
if ((BUILD_XP > 0)) if ((BUILD_XP > 0))
then then
mkdir -p ${OUTDIR_EXTRAS}/winxp mkdir -p ${OUTDIR_EXTRAS}/winxp
@@ -279,16 +283,8 @@ then
cp ${BUILDDIR_32}/spicetools/spicecfg-pdb.pdb ${OUTDIR} 2>/dev/null 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.exe ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/spice-pdb.pdb ${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.exe ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/spice64-pdb.pdb ${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 else
# release files # release files
cp ${BUILDDIR_32}/spicetools/spicecfg.exe ${OUTDIR} 2>/dev/null cp ${BUILDDIR_32}/spicetools/spicecfg.exe ${OUTDIR} 2>/dev/null
@@ -296,16 +292,15 @@ else
cp ${BUILDDIR_32}/spicetools/32/spice.exe ${OUTDIR} 2>/dev/null 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_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/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.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/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/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/nvcuda.dll ${OUTDIR}/stubs/64 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/nvcuvid.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/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)) if ((BUILD_XP > 0))
then then
cp ${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe ${OUTDIR_EXTRAS}/winxp 2>/dev/null cp ${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe ${OUTDIR_EXTRAS}/winxp 2>/dev/null
@@ -324,6 +319,9 @@ then
echo "WARNING: Couldn't get git to create the archive. Is this a git repository?" echo "WARNING: Couldn't get git to create the archive. Is this a git repository?"
fi fi
# sdk headers
cp -r ./sdk/include ${OUTDIR_EXTRAS}/sdk
# copy resources # copy resources
rm -rf ${OUTDIR_EXTRAS}/api rm -rf ${OUTDIR_EXTRAS}/api
mkdir ${OUTDIR_EXTRAS}/api mkdir ${OUTDIR_EXTRAS}/api
+1 -1
View File
@@ -1,6 +1,6 @@
docker build --pull external/docker -t spicetools/deps docker build --pull external/docker -t spicetools/deps
docker build --build-context gitroot=%cd%/../../.git . -t spicetools/spice 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 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\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\spice.exe bin\spice2x\spice-pdb.exe bin\spice2x\spice-pdb.pdb
+101 -14
View File
@@ -207,22 +207,38 @@ float Analog::applyMultiplier(float value) {
} }
float Analog::normalizeAnalogValue(float value) { float Analog::normalizeAnalogValue(float value) {
// effectively the same as fmodf(value, 1.f) if (getType() == GameAPI::Analogs::AnalogType::Circular) {
// for small values, this is MUCH faster than fmodf. // effectively the same as fmodf(value, 1.f)
float new_value = value; // for small values, this is MUCH faster than fmodf.
while (new_value > 1.f) { float new_value = value;
new_value -= 1.f; while (new_value > 1.f) {
new_value -= 1.f;
}
while (new_value < 0.f) {
new_value += 1.f;
}
return new_value;
} else {
// clamp to [0, 1] range
return std::clamp(value, 0.f, 1.f);
} }
while (new_value < 0.f) {
new_value += 1.f;
}
return new_value;
} }
float Analog::applyDeadzone(float raw_value) { float Analog::applyDeadzone(float raw_value) {
float value = raw_value; float value = raw_value;
const auto deadzone = this->getDeadzone(); auto deadzone = this->getDeadzone();
if (deadzone > 0) {
// in the past, positive deadzone applied in the center, negative deadzone applied to 0
// after each analog value received a type (circular/linear) this has been simpliifed to
// positive values only since we can figure out where the rest value is
// for back compat, treat negative value as positive
if (deadzone < 0.f) {
deadzone = -deadzone;
}
// 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 // calculate values
const auto delta = value - 0.5f; const auto delta = value - 0.5f;
@@ -255,7 +271,7 @@ float Analog::applyDeadzone(float raw_value) {
} }
} }
} else if (deadzone < 0) { } else {
// invert for mirror // invert for mirror
if (this->getDeadzoneMirror()) { if (this->getDeadzoneMirror()) {
@@ -263,8 +279,8 @@ float Analog::applyDeadzone(float raw_value) {
} }
// deadzone from minimum value // deadzone from minimum value
if (deadzone > -1 && value > -deadzone) { if (deadzone < 1.f && deadzone < value) {
value = std::min(1.f, (value + deadzone) / (1.f + deadzone)); value = std::max(0.f, (value - deadzone) / (1.f - deadzone));
} else { } else {
value = 0.f; value = 0.f;
} }
@@ -276,3 +292,74 @@ float Analog::applyDeadzone(float raw_value) {
} }
return 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;
}
+46 -18
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <cstdint>
#include <array> #include <array>
#include <string> #include <string>
#include <cmath> #include <cmath>
@@ -13,12 +14,32 @@ namespace rawinput {
class RawInputManager; class RawInputManager;
} }
namespace GameAPI::Analogs {
enum class AnalogType {
// default; values wrap around (below 0 turns into 1, over 1 is 0)
// knobs, turntables
Circular = 0,
// typical joystick that rests at the center and caps at [0, 1]
LinearCentered = 1,
// one-directional value (sliders and instruments like piano/drum velocity)
// starts at 0 and goes up to 1
LinearPositive = 2,
};
}
struct AnalogMovingAverage { struct AnalogMovingAverage {
double time_in_ms; double time_in_ms;
float sine; float sine;
float cosine; float cosine;
}; };
struct AnalogDelayEntry {
double time_in_ms;
float value;
};
class Analog { class Analog {
private: private:
std::string name; std::string name;
@@ -31,10 +52,11 @@ private:
float last_state = 0.5f; float last_state = 0.5f;
bool sensitivity_set = false; bool sensitivity_set = false;
bool deadzone_set = false; bool deadzone_set = false;
GameAPI::Analogs::AnalogType type = GameAPI::Analogs::AnalogType::Circular;
// smoothing function // smoothing function
bool smoothing = false; bool smoothing = false;
std::array<AnalogMovingAverage, ANALOG_HISTORY_CNT> vector_history; std::array<AnalogMovingAverage, ANALOG_HISTORY_CNT> vector_history = {};
int vector_history_index = 0; int vector_history_index = 0;
float smoothed_last_state = 0.f; float smoothed_last_state = 0.f;
@@ -48,12 +70,13 @@ private:
unsigned short divisor_region = 0; unsigned short divisor_region = 0;
// relative input mode // relative input mode
float absolute_value_for_rel_mode = 0.5f; float rel_mode_absolute_value = 0.5f;
float rel_mode_last_read_time_s = 0.f;
bool relative_mode = false; bool relative_mode = false;
// circular buffer (delayed input) // delay
int delay_buffer_depth = 0; uint32_t delay_ms = 0;
std::queue<float> delay_buffer; std::queue<AnalogDelayEntry> delayed_inputs;
float calculateAngularDifference(float old_rads, float new_rads); float calculateAngularDifference(float old_rads, float new_rads);
float normalizeAngle(float rads); float normalizeAngle(float rads);
@@ -66,7 +89,8 @@ public:
float override_state = 0.5f; float override_state = 0.5f;
explicit Analog(std::string name) : name(std::move(name)) { explicit Analog(std::string name) : name(std::move(name)) {
vector_history.fill({0.0, 0.f, 0.f}); };
explicit Analog(std::string name, GameAPI::Analogs::AnalogType type) : name(std::move(name)), type(type) {
}; };
std::string getDisplayString(rawinput::RawInputManager* manager); std::string getDisplayString(rawinput::RawInputManager* manager);
@@ -74,6 +98,8 @@ public:
float applyAngularSensitivity(float raw_rads); float applyAngularSensitivity(float raw_rads);
float applyMultiplier(float raw_value); float applyMultiplier(float raw_value);
float applyDeadzone(float raw_value); float applyDeadzone(float raw_value);
float getRelativeModeValue(float raw_value);
float getDelayedValue(float raw_value);
inline bool isSet() { inline bool isSet() {
if (this->override_enabled) { if (this->override_enabled) {
@@ -90,8 +116,8 @@ public:
deadzone_mirror = false; deadzone_mirror = false;
setMultiplier(1); setMultiplier(1);
setRelativeMode(false); setRelativeMode(false);
setDelayBufferDepth(0);
setLastState(0.5f); setLastState(0.5f);
setDelayMs(0);
} }
inline void clearBindings() { inline void clearBindings() {
@@ -193,25 +219,27 @@ public:
} }
inline void setRelativeMode(bool relative_mode) { inline void setRelativeMode(bool relative_mode) {
if (relative_mode) {
this->smoothing = false;
}
this->relative_mode = relative_mode; this->relative_mode = relative_mode;
this->absolute_value_for_rel_mode = 0.5f; this->rel_mode_absolute_value = 0.5f;
this->rel_mode_last_read_time_s = 0.f;
} }
inline float getAbsoluteValue(float relative_delta) { inline GameAPI::Analogs::AnalogType getType() const {
this->absolute_value_for_rel_mode = return this->type;
normalizeAnalogValue(this->absolute_value_for_rel_mode + relative_delta);
return this->absolute_value_for_rel_mode;
} }
inline int getDelayBufferDepth() const { inline void setType(GameAPI::Analogs::AnalogType type) {
return this->delay_buffer_depth; this->type = type;
} }
inline void setDelayBufferDepth(int depth) { inline uint32_t getDelayMs() const {
this->delay_buffer_depth = depth; return this->delay_ms;
} }
inline std::queue<float> &getDelayBuffer() { inline void setDelayMs(uint32_t delay_ms) {
return this->delay_buffer; this->delay_ms = delay_ms;
} }
}; };
+121 -58
View File
@@ -199,9 +199,19 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
if (vKey < value_states->size()) { if (vKey < value_states->size()) {
auto value = value_states->at(vKey); auto value = value_states->at(vKey);
if (current_button->getAnalogType() == BAT_POSITIVE) { if (current_button->getAnalogType() == BAT_POSITIVE) {
state = value > 0.6f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED; float threshold = 0.6f;
if (current_button->getBatThreshold() > 0) {
threshold = std::clamp(current_button->getBatThreshold() / 100.f, 0.01f, 0.99f);
}
state = value > threshold ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
} else if (current_button->getAnalogType() == BAT_NEGATIVE) { } else if (current_button->getAnalogType() == BAT_NEGATIVE) {
state = value < 0.4f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED; float threshold = 0.4f;
if (current_button->getBatThreshold() > 0) {
threshold = std::clamp((100 - current_button->getBatThreshold()) / 100.f, 0.01f, 0.99f);
}
state = value < threshold ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
} else { } else {
state = value > 0.01f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED; state = value > 0.01f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
} }
@@ -669,81 +679,98 @@ float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, rawinput::D
} }
// deadzone // deadzone
if (analog.isDeadzoneSet()) { // 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); value = analog.applyDeadzone(value);
} }
if (analog.isRelativeMode()) { if (analog.getType() == AnalogType::Circular) {
float relative_delta = value - 0.5f;
// built-in scaling to make values reasonable
relative_delta /= 80.f;
// integer multiplier/divisor if (analog.isRelativeMode()) {
const auto mult = analog.getMultiplier(); value = analog.getRelativeModeValue(value);
if (mult < -1) {
relative_delta /= -mult;
} else if (1 < mult) {
relative_delta *= mult;
}
// sensitivity (ranges from 0.0 to 4.0) } else {
if (analog.isSensitivitySet()) { // integer multiplier
relative_delta *= analog.getSensitivity(); value = analog.applyMultiplier(value);
}
// translate relative movement to absolute value // smoothing/sensitivity
value = analog.getAbsoluteValue(relative_delta); if (analog.getSmoothing() || analog.isSensitivitySet()) {
float rads = value * (float) M_TAU;
} else { // smoothing
// integer multiplier if (analog.getSmoothing()) {
value = analog.applyMultiplier(value);
// smoothing/sensitivity // preserve direction
if (analog.getSmoothing() || analog.isSensitivitySet()) { if (rads >= M_TAU) {
float rads = value * (float) M_TAU; rads -= 0.0001f;
}
// smoothing // calculate angle
if (analog.getSmoothing()) { rads = analog.getSmoothedValue(rads);
// preserve direction
if (rads >= M_TAU) {
rads -= 0.0001f;
} }
// calculate angle // sensitivity
rads = analog.getSmoothedValue(rads); if (analog.isSensitivitySet()) {
rads = analog.applyAngularSensitivity(rads);
}
// apply to value
value = rads * (float) M_1_TAU;
}
}
} else {
// sensitivity
if (analog.isSensitivitySet()) {
// adjust curve
// values < 1.f : less sensitive around neutral
// values > 1.f : more sensitive around neutral
float curve = analog.getSensitivity();
if (curve <= 0.f) {
curve = 0.01f;
}
curve = 1.f / curve;
if (analog.getType() == AnalogType::LinearCentered) {
// convert 0.0..1.0 to -1.0..+1.0
float signed_raw = (value - 0.5f) * 2.0f;
// apply curve
float sign = signed_raw < 0.0f ? -1.0f : 1.0f;
float magnitude = fabsf(signed_raw);
float curved = sign * powf(magnitude, curve);
// convert back to 0.0..1.0
value = curved * 0.5f + 0.5f;
} else {
value = powf(value, curve);
} }
// sensitivity value = std::clamp(value, 0.f, 1.f);
if (analog.isSensitivitySet()) { }
rads = analog.applyAngularSensitivity(rads);
// multiplier / divisor
if (analog.getMultiplier() < -1) {
if (analog.getType() == AnalogType::LinearCentered) {
value = (value - 0.5f) / (-analog.getMultiplier()) + 0.5f;
} else {
value /= -analog.getMultiplier();
} }
// apply to value value = std::clamp(value, 0.f, 1.f);
value = rads * (float) M_1_TAU;
} else if (analog.getMultiplier() > 1) {
if (analog.getType() == AnalogType::LinearCentered) {
value = (value - 0.5f) * analog.getMultiplier() + 0.5f;
} else {
value *= analog.getMultiplier();
}
value = std::clamp(value, 0.f, 1.f);
} }
} }
// delay // delay
if (0 < analog.getDelayBufferDepth()) { if (analog.getDelayMs() > 0) {
auto& queue = analog.getDelayBuffer(); value = analog.getDelayedValue(value);
// ensure the queue isn't too long; drop old values
while (analog.getDelayBufferDepth() <= (int)queue.size()) {
queue.pop();
}
// always push new value
queue.push(value);
// get a new value to return
if ((int)queue.size() < analog.getDelayBufferDepth()) {
// not enough in the queue, stall for now, shouldn't happen often
value = analog.getLastState();
} else {
value = queue.front();
queue.pop();
}
} }
break; break;
@@ -829,6 +856,42 @@ std::vector<Analog> GameAPI::Analogs::sortAnalogs(
return sorted; return sorted;
} }
static std::vector<Analog> sortAnalogsWithTypeInternal(
std::vector<Analog> &analogs,
const std::initializer_list<GameAPI::Analogs::AnalogWithType> list) {
std::vector<Analog> sorted;
bool analog_found;
for (auto &a : list) {
analog_found = false;
for (auto &analog : analogs) {
if (a.name == analog.getName()) {
analog_found = true;
analog.setType(a.type);
sorted.push_back(analog);
break;
}
}
if (!analog_found) {
sorted.emplace_back(a.name, a.type);
}
}
return sorted;
}
void GameAPI::Analogs::sortAnalogsWithType(
std::vector<Analog> *analogs,
const std::initializer_list<AnalogWithType> list) {
if (analogs) {
*analogs = sortAnalogsWithTypeInternal(*analogs, list);
}
}
float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, Analog &analog) { float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, Analog &analog) {
// check override // check override
+8 -7
View File
@@ -85,14 +85,15 @@ namespace GameAPI {
const std::vector<Analog> &analogs, const std::vector<Analog> &analogs,
const std::vector<std::string> &analog_names); const std::vector<std::string> &analog_names);
template<typename T> struct AnalogWithType {
void sortAnalogs(std::vector<Analog> *analogs, T t) { const std::string name;
const std::vector<std::string> analog_names { t }; const AnalogType type;
if (analogs) { AnalogWithType(std::string name, AnalogType type) :
*analogs = GameAPI::Analogs::sortAnalogs(*analogs, analog_names); name(std::move(name)), type(type) {}
} };
}
void sortAnalogsWithType(std::vector<Analog> *analogs, const std::initializer_list<AnalogWithType> list);
template<typename T, typename... Rest> template<typename T, typename... Rest>
void sortAnalogs(std::vector<Analog> *analogs, T t, Rest... rest) { void sortAnalogs(std::vector<Analog> *analogs, T t, Rest... rest) {
+4 -1
View File
@@ -306,7 +306,10 @@ std::string Button::getDisplayString(rawinput::RawInputManager* manager) {
std::string vKeyString = fmt::format("{:#x}", vKey); std::string vKeyString = fmt::format("{:#x}", vKey);
// device must be existing // device must be existing
if (this->device_identifier.empty() && vKey == INVALID_VKEY) { if (this->isNaive() && vKey == INVALID_VKEY) {
if (this->getInvert()) {
return "(always on)";
}
return ""; return "";
} }
+10
View File
@@ -56,6 +56,7 @@ private:
ButtonAnalogType analog_type = BAT_NONE; ButtonAnalogType analog_type = BAT_NONE;
double debounce_up = 0.0; double debounce_up = 0.0;
double debounce_down = 0.0; double debounce_down = 0.0;
int bat_threshold = 0; // for positive/negative only, value in percentage, zero is default
bool invert = false; bool invert = false;
bool is_temporary = false; bool is_temporary = false;
@@ -109,6 +110,7 @@ public:
alternatives.clear(); alternatives.clear();
device_identifier = ""; device_identifier = "";
analog_type = BAT_NONE; analog_type = BAT_NONE;
bat_threshold = 0;
} }
std::string getDisplayString(rawinput::RawInputManager* manager); std::string getDisplayString(rawinput::RawInputManager* manager);
@@ -169,6 +171,14 @@ public:
this->debounce_down = debounce_time_down; this->debounce_down = debounce_time_down;
} }
inline void setBatThreshold(int bat_threshold) {
this->bat_threshold = bat_threshold;
}
inline int getBatThreshold() const {
return this->bat_threshold;
}
inline bool getInvert() const { inline bool getInvert() const {
return this->invert; return this->invert;
} }
+37 -18
View File
@@ -28,7 +28,7 @@ Config::Config() {
this->status = false; this->status = false;
if (CONFIG_PATH_OVERRIDE.length() > 0) { if (CONFIG_PATH_OVERRIDE.length() > 0) {
this->configLocation = CONFIG_PATH_OVERRIDE; this->configLocation = CONFIG_PATH_OVERRIDE;
log_info("cfg", "using custom config file: {}", this->configLocation.string()); log_info("cfg", "using custom config file: {}", this->configLocation);
} else { } else {
this->configLocation = std::filesystem::path(_wgetenv(L"APPDATA")) / L"spicetools.xml"; this->configLocation = std::filesystem::path(_wgetenv(L"APPDATA")) / L"spicetools.xml";
// avoids logging the expanded appdata path as it contains user name // avoids logging the expanded appdata path as it contains user name
@@ -57,7 +57,7 @@ Config::Config() {
this->firstFillConfigFile(); this->firstFillConfigFile();
break; break;
case tinyxml2::XMLError::XML_ERROR_FILE_COULD_NOT_BE_OPENED: case tinyxml2::XMLError::XML_ERROR_FILE_COULD_NOT_BE_OPENED:
log_fatal("cfg", "could not open config file: {}", this->configLocation.string()); log_fatal("cfg", "could not open config file: {}", this->configLocation);
break; break;
case tinyxml2::XMLError::XML_ERROR_FILE_NOT_FOUND: case tinyxml2::XMLError::XML_ERROR_FILE_NOT_FOUND:
this->createConfigFile(); this->createConfigFile();
@@ -72,7 +72,7 @@ Config::Config() {
case tinyxml2::XMLError::XML_ERROR_PARSING_UNKNOWN: case tinyxml2::XMLError::XML_ERROR_PARSING_UNKNOWN:
case tinyxml2::XMLError::XML_ERROR_MISMATCHED_ELEMENT: case tinyxml2::XMLError::XML_ERROR_MISMATCHED_ELEMENT:
case tinyxml2::XMLError::XML_ERROR_PARSING: case tinyxml2::XMLError::XML_ERROR_PARSING:
log_warning("cfg", "Couldn't read config file: {}", this->configLocation.string()); log_warning("cfg", "Couldn't read config file: {}", this->configLocation);
this->createConfigFile(); this->createConfigFile();
this->firstFillConfigFile(); this->firstFillConfigFile();
break; break;
@@ -98,7 +98,7 @@ bool Config::getStatus() {
return this->status; return this->status;
} }
bool Config::addGame(Game &game) { bool Config::addGame(Game &game, bool save) {
tinyxml2::XMLNode *rootNode = this->configFile.LastChild(); tinyxml2::XMLNode *rootNode = this->configFile.LastChild();
tinyxml2::XMLElement *gameNodes = rootNode->FirstChildElement("game"); tinyxml2::XMLElement *gameNodes = rootNode->FirstChildElement("game");
@@ -162,6 +162,7 @@ bool Config::addGame(Game &game) {
auto analogType = (int) BAT_NONE; auto analogType = (int) BAT_NONE;
double debounce_up = 0.0; double debounce_up = 0.0;
double debounce_down = 0.0; double debounce_down = 0.0;
int bat_threshold = 0;
int velocity_threshold = 0; int velocity_threshold = 0;
bool invert = false; bool invert = false;
tinyxml2::XMLError attrError = gameButtonNode->QueryIntAttribute("vkey", &vKey); tinyxml2::XMLError attrError = gameButtonNode->QueryIntAttribute("vkey", &vKey);
@@ -169,6 +170,7 @@ bool Config::addGame(Game &game) {
gameButtonNode->QueryIntAttribute("analogtype", &analogType); gameButtonNode->QueryIntAttribute("analogtype", &analogType);
gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up); gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up);
gameButtonNode->QueryDoubleAttribute("debounce_down", &debounce_down); gameButtonNode->QueryDoubleAttribute("debounce_down", &debounce_down);
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold); gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
gameButtonNode->QueryBoolAttribute("invert", &invert); gameButtonNode->QueryBoolAttribute("invert", &invert);
if (attrError != tinyxml2::XMLError::XML_SUCCESS) { if (attrError != tinyxml2::XMLError::XML_SUCCESS) {
@@ -180,6 +182,7 @@ bool Config::addGame(Game &game) {
gameButtonNode->SetAttribute("devid", button->getDeviceIdentifier().c_str()); gameButtonNode->SetAttribute("devid", button->getDeviceIdentifier().c_str());
gameButtonNode->SetAttribute("debounce_up", debounce_up); gameButtonNode->SetAttribute("debounce_up", debounce_up);
gameButtonNode->SetAttribute("debounce_down", debounce_down); gameButtonNode->SetAttribute("debounce_down", debounce_down);
gameButtonNode->SetAttribute("bat_threshold", bat_threshold);
gameButtonNode->SetAttribute("velocity_threshold", velocity_threshold); gameButtonNode->SetAttribute("velocity_threshold", velocity_threshold);
gameButtonNode->SetAttribute("invert", invert); gameButtonNode->SetAttribute("invert", invert);
gameButtonsNode->InsertEndChild(gameButtonNode); gameButtonsNode->InsertEndChild(gameButtonNode);
@@ -188,6 +191,7 @@ bool Config::addGame(Game &game) {
button->setAnalogType((ButtonAnalogType) analogType); button->setAnalogType((ButtonAnalogType) analogType);
button->setDebounceUp(debounce_up); button->setDebounceUp(debounce_up);
button->setDebounceDown(debounce_down); button->setDebounceDown(debounce_down);
button->setBatThreshold(bat_threshold);
button->setVelocityThreshold(velocity_threshold); button->setVelocityThreshold(velocity_threshold);
button->setInvert(invert); button->setInvert(invert);
if (devid) { if (devid) {
@@ -207,6 +211,7 @@ bool Config::addGame(Game &game) {
gameButtonNode->SetAttribute("analogtype", (int) it.getAnalogType()); gameButtonNode->SetAttribute("analogtype", (int) it.getAnalogType());
gameButtonNode->SetAttribute("debounce_up", it.getDebounceUp()); gameButtonNode->SetAttribute("debounce_up", it.getDebounceUp());
gameButtonNode->SetAttribute("debounce_down", it.getDebounceDown()); gameButtonNode->SetAttribute("debounce_down", it.getDebounceDown());
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold()); gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
gameButtonNode->SetAttribute("invert", it.getInvert()); gameButtonNode->SetAttribute("invert", it.getInvert());
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str()); gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
@@ -248,7 +253,7 @@ bool Config::addGame(Game &game) {
bool smoothing = false; bool smoothing = false;
int multiplier = 1; int multiplier = 1;
bool relative_mode = false; bool relative_mode = false;
int delay_buffer_depth = 0; uint32_t delay = 0;
tinyxml2::XMLError err1 = gameAnalogNode->QueryIntAttribute("index", &index); tinyxml2::XMLError err1 = gameAnalogNode->QueryIntAttribute("index", &index);
gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity); gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity);
gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone); gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone);
@@ -257,7 +262,7 @@ bool Config::addGame(Game &game) {
gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing); gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing);
gameAnalogNode->QueryIntAttribute("multiplier", &multiplier); gameAnalogNode->QueryIntAttribute("multiplier", &multiplier);
gameAnalogNode->QueryBoolAttribute("relative", &relative_mode); gameAnalogNode->QueryBoolAttribute("relative", &relative_mode);
gameAnalogNode->QueryIntAttribute("delay", &delay_buffer_depth); gameAnalogNode->QueryUnsignedAttribute("delay_ms", &delay);
const char *devid = gameAnalogNode->Attribute("devid"); const char *devid = gameAnalogNode->Attribute("devid");
if (err1 != tinyxml2::XMLError::XML_SUCCESS || !devid) { if (err1 != tinyxml2::XMLError::XML_SUCCESS || !devid) {
@@ -273,7 +278,7 @@ bool Config::addGame(Game &game) {
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing()); gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier()); gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
gameAnalogNode->SetAttribute("relative", it.isRelativeMode()); gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
gameAnalogNode->SetAttribute("delay", it.getDelayBufferDepth()); gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
gameAnalogsNode->InsertEndChild(gameAnalogNode); gameAnalogsNode->InsertEndChild(gameAnalogNode);
} else { } else {
it.setIndex(static_cast<unsigned short int>(index)); it.setIndex(static_cast<unsigned short int>(index));
@@ -285,7 +290,7 @@ bool Config::addGame(Game &game) {
it.setSmoothing(smoothing); it.setSmoothing(smoothing);
it.setMultiplier(multiplier); it.setMultiplier(multiplier);
it.setRelativeMode(relative_mode); it.setRelativeMode(relative_mode);
it.setDelayBufferDepth(delay_buffer_depth); it.setDelayMs(delay);
} }
} else { } else {
gameAnalogNode = this->configFile.NewElement("analog"); gameAnalogNode = this->configFile.NewElement("analog");
@@ -298,7 +303,7 @@ bool Config::addGame(Game &game) {
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing()); gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier()); gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
gameAnalogNode->SetAttribute("relative", it.isRelativeMode()); gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
gameAnalogNode->SetAttribute("delay", it.getDelayBufferDepth()); gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
gameAnalogNode->SetAttribute("devid", it.getDeviceIdentifier().c_str()); gameAnalogNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
gameAnalogsNode->InsertEndChild(gameAnalogNode); gameAnalogsNode->InsertEndChild(gameAnalogNode);
} }
@@ -432,6 +437,7 @@ bool Config::addGame(Game &game) {
gameButtonNode->SetAttribute("analogtype", it.getAnalogType()); gameButtonNode->SetAttribute("analogtype", it.getAnalogType());
gameButtonNode->SetAttribute("debounce_up", it.getDebounceUp()); gameButtonNode->SetAttribute("debounce_up", it.getDebounceUp());
gameButtonNode->SetAttribute("debounce_down", it.getDebounceDown()); gameButtonNode->SetAttribute("debounce_down", it.getDebounceDown());
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold()); gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
gameButtonNode->SetAttribute("invert", it.getInvert()); gameButtonNode->SetAttribute("invert", it.getInvert());
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str()); gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
@@ -452,7 +458,7 @@ bool Config::addGame(Game &game) {
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing()); gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier()); gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
gameAnalogNode->SetAttribute("relative", it.isRelativeMode()); gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
gameAnalogNode->SetAttribute("delay", it.getDelayBufferDepth()); gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
gameAnalogsNode->InsertEndChild(gameAnalogNode); gameAnalogsNode->InsertEndChild(gameAnalogNode);
} }
@@ -480,13 +486,20 @@ bool Config::addGame(Game &game) {
rootNode->InsertEndChild(gameNode); rootNode->InsertEndChild(gameNode);
} }
// save config // save config (skipped when caller batches multiple addGame calls
this->saveConfigFile(); // and flushes once at the end via Config::save())
if (save) {
this->saveConfigFile();
}
// return success // return success
return true; return true;
} }
void Config::save() {
this->saveConfigFile();
}
bool Config::updateBinding(const Game &game, const Button &button, int alternative) { bool Config::updateBinding(const Game &game, const Button &button, int alternative) {
// get root node // get root node
@@ -535,6 +548,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
gameButtonNode->SetAttribute("analogtype", (int) button.getAnalogType()); gameButtonNode->SetAttribute("analogtype", (int) button.getAnalogType());
gameButtonNode->SetAttribute("debounce_up", button.getDebounceUp()); gameButtonNode->SetAttribute("debounce_up", button.getDebounceUp());
gameButtonNode->SetAttribute("debounce_down", button.getDebounceDown()); gameButtonNode->SetAttribute("debounce_down", button.getDebounceDown());
gameButtonNode->SetAttribute("bat_threshold", button.getBatThreshold());
gameButtonNode->SetAttribute("velocity_threshold", button.getVelocityThreshold()); gameButtonNode->SetAttribute("velocity_threshold", button.getVelocityThreshold());
gameButtonNode->SetAttribute("invert", button.getInvert()); gameButtonNode->SetAttribute("invert", button.getInvert());
gameButtonNode->SetAttribute("devid", button.getDeviceIdentifier().c_str()); gameButtonNode->SetAttribute("devid", button.getDeviceIdentifier().c_str());
@@ -551,6 +565,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
gameButtonNode->SetAttribute("analogtype", 0); gameButtonNode->SetAttribute("analogtype", 0);
gameButtonNode->SetAttribute("debounce_up", 0.0); gameButtonNode->SetAttribute("debounce_up", 0.0);
gameButtonNode->SetAttribute("debounce_down", 0.0); gameButtonNode->SetAttribute("debounce_down", 0.0);
gameButtonNode->SetAttribute("bat_threshold", 0);
gameButtonNode->SetAttribute("velocity_threshold", 0); gameButtonNode->SetAttribute("velocity_threshold", 0);
gameButtonNode->SetAttribute("invert", false); gameButtonNode->SetAttribute("invert", false);
gameButtonNode->SetAttribute("devid", ""); gameButtonNode->SetAttribute("devid", "");
@@ -684,7 +699,7 @@ bool Config::updateBinding(const Game &game, const Analog &analog) {
gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing()); gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier()); gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier());
gameAnalogNode->SetAttribute("relative", analog.isRelativeMode()); gameAnalogNode->SetAttribute("relative", analog.isRelativeMode());
gameAnalogNode->SetAttribute("delay", analog.getDelayBufferDepth()); gameAnalogNode->SetAttribute("delay_ms", analog.getDelayMs());
gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str()); gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str());
} else { } else {
gameAnalogNode = this->configFile.NewElement("analog"); gameAnalogNode = this->configFile.NewElement("analog");
@@ -696,7 +711,7 @@ bool Config::updateBinding(const Game &game, const Analog &analog) {
gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing()); gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier()); gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier());
gameAnalogNode->SetAttribute("relative", analog.isRelativeMode()); gameAnalogNode->SetAttribute("relative", analog.isRelativeMode());
gameAnalogNode->SetAttribute("delay", analog.getDelayBufferDepth()); gameAnalogNode->SetAttribute("delay_ms", analog.getDelayMs());
gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str()); gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str());
gameAnalogsNode->InsertEndChild(gameAnalogNode); gameAnalogsNode->InsertEndChild(gameAnalogNode);
} }
@@ -946,12 +961,14 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
auto analogType = (int) BAT_NONE; auto analogType = (int) BAT_NONE;
double debounce_up = 0.0; double debounce_up = 0.0;
double debounce_down = 0.0; double debounce_down = 0.0;
int bat_threshold = 0;
int velocity_threshold = 0; int velocity_threshold = 0;
bool invert = false; bool invert = false;
gameButtonNode->QueryIntAttribute("vkey", &vKey); gameButtonNode->QueryIntAttribute("vkey", &vKey);
gameButtonNode->QueryIntAttribute("analogtype", &analogType); gameButtonNode->QueryIntAttribute("analogtype", &analogType);
gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up); gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up);
gameButtonNode->QueryDoubleAttribute("debounce_down", &debounce_down); gameButtonNode->QueryDoubleAttribute("debounce_down", &debounce_down);
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold); gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
gameButtonNode->QueryBoolAttribute("invert", &invert); gameButtonNode->QueryBoolAttribute("invert", &invert);
const char *devid = gameButtonNode->Attribute("devid"); const char *devid = gameButtonNode->Attribute("devid");
@@ -966,6 +983,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
alt.setAnalogType((ButtonAnalogType) analogType); alt.setAnalogType((ButtonAnalogType) analogType);
alt.setDebounceUp(debounce_up); alt.setDebounceUp(debounce_up);
alt.setDebounceDown(debounce_down); alt.setDebounceDown(debounce_down);
alt.setBatThreshold(bat_threshold);
alt.setVelocityThreshold(velocity_threshold); alt.setVelocityThreshold(velocity_threshold);
alt.setInvert(invert); alt.setInvert(invert);
if (devid) { if (devid) {
@@ -984,6 +1002,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
button.setAnalogType((ButtonAnalogType) analogType); button.setAnalogType((ButtonAnalogType) analogType);
button.setDebounceUp(debounce_up); button.setDebounceUp(debounce_up);
button.setDebounceDown(debounce_down); button.setDebounceDown(debounce_down);
button.setBatThreshold(bat_threshold);
button.setVelocityThreshold(velocity_threshold); button.setVelocityThreshold(velocity_threshold);
button.setInvert(invert); button.setInvert(invert);
if (devid) { if (devid) {
@@ -1130,7 +1149,7 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
bool smoothing = false; bool smoothing = false;
int multiplier = 1; int multiplier = 1;
bool relative_mode = false; bool relative_mode = false;
int delay_buffer_depth = 0; uint32_t delay = 0;
gameAnalogNode->QueryIntAttribute("index", &index); gameAnalogNode->QueryIntAttribute("index", &index);
gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity); gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity);
gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone); gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone);
@@ -1139,7 +1158,7 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing); gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing);
gameAnalogNode->QueryIntAttribute("multiplier", &multiplier); gameAnalogNode->QueryIntAttribute("multiplier", &multiplier);
gameAnalogNode->QueryBoolAttribute("relative", &relative_mode); gameAnalogNode->QueryBoolAttribute("relative", &relative_mode);
gameAnalogNode->QueryIntAttribute("delay", &delay_buffer_depth); gameAnalogNode->QueryUnsignedAttribute("delay_ms", &delay);
const char *devid = gameAnalogNode->Attribute("devid"); const char *devid = gameAnalogNode->Attribute("devid");
// create analog and add to list // create analog and add to list
@@ -1152,7 +1171,7 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
analog.setSmoothing(smoothing); analog.setSmoothing(smoothing);
analog.setMultiplier(multiplier); analog.setMultiplier(multiplier);
analog.setRelativeMode(relative_mode); analog.setRelativeMode(relative_mode);
analog.setDelayBufferDepth(delay_buffer_depth); analog.setDelayMs(delay);
if (devid) { if (devid) {
analog.setDeviceIdentifier(devid); analog.setDeviceIdentifier(devid);
} }
@@ -1315,7 +1334,7 @@ void Config::saveConfigFile() {
// create a .tmp file and write to it... // create a .tmp file and write to it...
const auto xml_result = this->configFile.SaveFile(this->configLocationTemp.c_str(), false); const auto xml_result = this->configFile.SaveFile(this->configLocationTemp.c_str(), false);
if (xml_result != tinyxml2::XMLError::XML_SUCCESS) { if (xml_result != tinyxml2::XMLError::XML_SUCCESS) {
log_info("cfg", "failed to write file: {}", this->configLocationTemp.string()); log_info("cfg", "failed to write file: {}", this->configLocationTemp);
return; return;
} }
// copy the .tmp file to the main file... // copy the .tmp file to the main file...
+6 -1
View File
@@ -22,7 +22,12 @@ public:
bool getStatus(); bool getStatus();
bool createConfigFile(); bool createConfigFile();
bool addGame(Game &game); bool addGame(Game &game, bool save = true);
// flush pending in-memory changes to disk; intended to be paired with
// addGame(game, false) in bulk-init paths so we write the XML once instead
// of once per game
void save();
bool updateBinding(const Game &game, const Button &button, int alternative); bool updateBinding(const Game &game, const Button &button, int alternative);
bool updateBinding(const Game &game, const Analog &analog); bool updateBinding(const Game &game, const Analog &analog);
+74 -3
View File
@@ -1,11 +1,15 @@
#include "configurator.h" #include "configurator.h"
#include <d3d9.h>
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "util/logging.h"
namespace cfg { namespace cfg {
// globals // globals
bool CONFIGURATOR_STANDALONE = false; bool CONFIGURATOR_STANDALONE = false;
bool CONFIGURATOR_FORCE_SOFTWARE_RENDER = false;
ConfigType CONFIGURATOR_TYPE = ConfigType::Config; ConfigType CONFIGURATOR_TYPE = ConfigType::Config;
Configurator::Configurator() { Configurator::Configurator() {
@@ -16,11 +20,78 @@ namespace cfg {
CONFIGURATOR_STANDALONE = false; CONFIGURATOR_STANDALONE = false;
} }
// Attempt to bring up a D3D9 device backing the configurator's window so the
// overlay can use the hardware-accelerated imgui_impl_dx9 path instead of
// the CPU rasterizer. Returns true if both Direct3DCreate9 and CreateDevice
// succeed; the caller falls back to overlay::create_software() otherwise so
// that environments without D3D9 (Wine without dxvk, headless test boxes,
// GPUs whose drivers reject HAL) still get a working configurator.
static bool try_init_d3d9(ConfiguratorWindow &wnd) {
if (cfg::CONFIGURATOR_FORCE_SOFTWARE_RENDER) {
return false;
}
if (wnd.hWnd == nullptr) {
return false;
}
IDirect3D9 *d3d = Direct3DCreate9(D3D_SDK_VERSION);
if (d3d == nullptr) {
log_warning("configurator", "Direct3DCreate9 returned NULL, falling back to software renderer");
return false;
}
D3DPRESENT_PARAMETERS pp {};
pp.Windowed = TRUE;
pp.SwapEffect = D3DSWAPEFFECT_DISCARD;
// D3DFMT_UNKNOWN -> driver picks the current desktop format
pp.BackBufferFormat = D3DFMT_UNKNOWN;
pp.BackBufferWidth = static_cast<UINT>(wnd.client_width);
pp.BackBufferHeight = static_cast<UINT>(wnd.client_height);
pp.hDeviceWindow = wnd.hWnd;
pp.EnableAutoDepthStencil = FALSE;
pp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
IDirect3DDevice9 *device = nullptr;
// SOFTWARE_VERTEXPROCESSING is the most compatible behavior flag; the UI
// is tiny so we don't need hardware T&L. FPU_PRESERVE keeps our floating
// point environment intact in case other spice code relies on it.
const DWORD behavior_flags = D3DCREATE_SOFTWARE_VERTEXPROCESSING | D3DCREATE_FPU_PRESERVE;
HRESULT hr = d3d->CreateDevice(
D3DADAPTER_DEFAULT,
D3DDEVTYPE_HAL,
wnd.hWnd,
behavior_flags,
&pp,
&device);
if (FAILED(hr) || device == nullptr) {
log_warning("configurator",
"D3D9 CreateDevice failed (hr={:#x}), falling back to software renderer",
static_cast<unsigned int>(hr));
d3d->Release();
return false;
}
wnd.d3d = d3d;
wnd.device = device;
wnd.pp = pp;
wnd.use_d3d9 = true;
return true;
}
void Configurator::run() { void Configurator::run() {
// create instance // bring up the overlay against either a real D3D9 device or the software
// rasterizer. The choice is one-shot - the in-game overlay always uses
// the same renderer the configurator picked here.
overlay::ENABLED = true; overlay::ENABLED = true;
overlay::create_software(this->wnd.hWnd); if (try_init_d3d9(this->wnd)) {
log_info("configurator", "using D3D9 hardware-accelerated renderer");
overlay::create_d3d9(this->wnd.hWnd, this->wnd.d3d, this->wnd.device);
} else {
log_info("configurator", "using software renderer");
overlay::create_software(this->wnd.hWnd);
}
overlay::OVERLAY->set_active(true); overlay::OVERLAY->set_active(true);
overlay::OVERLAY->hotkeys_enable = false; overlay::OVERLAY->hotkeys_enable = false;
ImGui::GetIO().MouseDrawCursor = false; ImGui::GetIO().MouseDrawCursor = false;
+1
View File
@@ -11,6 +11,7 @@ namespace cfg {
// globals // globals
extern bool CONFIGURATOR_STANDALONE; extern bool CONFIGURATOR_STANDALONE;
extern ConfigType CONFIGURATOR_TYPE; extern ConfigType CONFIGURATOR_TYPE;
extern bool CONFIGURATOR_FORCE_SOFTWARE_RENDER;
class Configurator { class Configurator {
private: private:
+431 -42
View File
@@ -1,11 +1,17 @@
#include "configurator_wnd.h" #include "configurator_wnd.h"
#include <algorithm>
#include <cstring>
#include <windows.h> #include <windows.h>
#include <windowsx.h>
#include "build/defs.h" #include "build/defs.h"
#include "external/imgui/imgui.h"
#include "launcher/shutdown.h" #include "launcher/shutdown.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "cfg/configurator.h" #include "cfg/configurator.h"
#include "icon.h" #include "icon.h"
@@ -16,6 +22,156 @@ static int WINDOW_SIZE_X = 800;
static int WINDOW_SIZE_Y = 600; static int WINDOW_SIZE_Y = 600;
static HICON WINDOW_ICON = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(MAINICON)); static HICON WINDOW_ICON = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(MAINICON));
static const UINT_PTR RENDER_TIMER_ID = 1;
// Map a virtual key code to the matching ImGuiKey. Mirrors the table used by
// the in-game ImGui spice backend so navigation keys behave identically when
// the configurator runs standalone and drives ImGui from Win32 messages.
static ImGuiKey vk_to_imgui_key(WPARAM vkey) {
switch (vkey) {
case VK_TAB: return ImGuiKey_Tab;
case VK_LEFT: return ImGuiKey_LeftArrow;
case VK_RIGHT: return ImGuiKey_RightArrow;
case VK_UP: return ImGuiKey_UpArrow;
case VK_DOWN: return ImGuiKey_DownArrow;
case VK_PRIOR: return ImGuiKey_PageUp;
case VK_NEXT: return ImGuiKey_PageDown;
case VK_HOME: return ImGuiKey_Home;
case VK_END: return ImGuiKey_End;
case VK_INSERT: return ImGuiKey_Insert;
case VK_DELETE: return ImGuiKey_Delete;
case VK_BACK: return ImGuiKey_Backspace;
case VK_SPACE: return ImGuiKey_Space;
case VK_RETURN: return ImGuiKey_Enter;
case VK_ESCAPE: return ImGuiKey_Escape;
case VK_LSHIFT: return ImGuiKey_LeftShift;
case VK_RSHIFT: return ImGuiKey_RightShift;
case VK_SHIFT: return ImGuiKey_LeftShift;
case VK_LCONTROL: return ImGuiKey_LeftCtrl;
case VK_RCONTROL: return ImGuiKey_RightCtrl;
case VK_CONTROL: return ImGuiKey_LeftCtrl;
case 'A': return ImGuiKey_A;
case 'C': return ImGuiKey_C;
case 'V': return ImGuiKey_V;
case 'X': return ImGuiKey_X;
case 'Y': return ImGuiKey_Y;
case 'Z': return ImGuiKey_Z;
default: return ImGuiKey_None;
}
}
static ImGuiKey vk_to_imgui_mod_key(WPARAM vkey) {
switch (vkey) {
case VK_SHIFT:
case VK_LSHIFT:
case VK_RSHIFT:
return ImGuiMod_Shift;
case VK_CONTROL:
case VK_LCONTROL:
case VK_RCONTROL:
return ImGuiMod_Ctrl;
case VK_MENU:
case VK_LMENU:
case VK_RMENU:
return ImGuiMod_Alt;
default:
return ImGuiMod_None;
}
}
static cfg::ConfiguratorWindow *get_state(HWND hWnd) {
return reinterpret_cast<cfg::ConfiguratorWindow *>(
GetWindowLongPtrW(hWnd, GWLP_USERDATA));
}
// Returns the refresh rate (Hz) of the monitor the window currently lives on,
// clamped to a sane range. Falls back to 60 if detection fails or the value
// looks invalid (DEVMODE may report 0 or 1 to mean "use hardware default").
static UINT detect_monitor_refresh_hz(HWND hWnd) {
UINT hz = 0;
HMONITOR mon = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST);
if (mon) {
MONITORINFOEXW mi {};
mi.cbSize = sizeof(mi);
if (GetMonitorInfoW(mon, reinterpret_cast<LPMONITORINFO>(&mi))) {
DEVMODEW dm {};
dm.dmSize = sizeof(dm);
if (EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm)) {
hz = dm.dmDisplayFrequency;
}
}
}
if (hz <= 1) {
HDC hdc = GetDC(hWnd);
if (hdc) {
int v = GetDeviceCaps(hdc, VREFRESH);
if (v > 1) {
hz = static_cast<UINT>(v);
}
ReleaseDC(hWnd, hdc);
}
}
if (hz < 30 || hz > 240) {
hz = 60;
}
return hz;
}
// Software-path WM_PAINT: blit the overlay's pixel buffer to the window using
// SetDIBitsToDevice. Avoids creating/destroying a GDI HBITMAP every frame.
static void paint_software(HWND hWnd) {
if (!overlay::OVERLAY) {
PAINTSTRUCT ps {};
BeginPaint(hWnd, &ps);
EndPaint(hWnd, &ps);
return;
}
int width = 0;
int height = 0;
uint32_t *pixel_data = overlay::OVERLAY->sw_get_pixel_data(&width, &height);
PAINTSTRUCT paint {};
HDC hdc = BeginPaint(hWnd, &paint);
if (pixel_data && width > 0 && height > 0) {
BITMAPINFO bmi {};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = width;
// negative height -> top-down DIB, matching how ImGui packs pixels
bmi.bmiHeader.biHeight = -height;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
SetDIBitsToDevice(
hdc,
0, 0,
width, height,
0, 0,
0, height,
pixel_data,
&bmi,
DIB_RGB_COLORS);
}
EndPaint(hWnd, &paint);
}
void cfg::ConfiguratorWindow::start_timer() {
if (!this->timer_running && this->hWnd) {
SetTimer(this->hWnd, RENDER_TIMER_ID, this->timer_interval_ms, nullptr);
this->timer_running = true;
}
}
void cfg::ConfiguratorWindow::stop_timer() {
if (this->timer_running && this->hWnd) {
KillTimer(this->hWnd, RENDER_TIMER_ID);
this->timer_running = false;
}
}
cfg::ConfiguratorWindow::ConfiguratorWindow() { cfg::ConfiguratorWindow::ConfiguratorWindow() {
// register the window class // register the window class
@@ -27,12 +183,22 @@ cfg::ConfiguratorWindow::ConfiguratorWindow() {
wc.hIcon = WINDOW_ICON; wc.hIcon = WINDOW_ICON;
RegisterClass(&wc); RegisterClass(&wc);
// raise SetTimer resolution so high refresh rates (120/144Hz) are actually
// achievable. Without this, USER timers quantize to ~15.6ms and cap us
// near ~64 FPS. Uses the shared helper so it respects -notimerhacks
// (Use Legacy Timers) and the Win11 timer-throttling opt-out. The helper
// intentionally never calls timeEndPeriod; the kernel releases the request
// when spicecfg exits.
timeutils::set_timer_resolution();
// determine window title // determine window title
if (cfg::CONFIGURATOR_TYPE == cfg::ConfigType::Config) { if (cfg::CONFIGURATOR_TYPE == cfg::ConfigType::Config) {
WINDOW_TITLE = "spice2x config (" + to_string(VERSION_STRING_CFG) + ")"; WINDOW_TITLE = "spice2x config (" + to_string(VERSION_STRING_CFG) + ")";
WINDOW_SIZE_X = 800; WINDOW_SIZE_X = 800;
WINDOW_SIZE_Y = 600; WINDOW_SIZE_Y = 600;
} }
this->client_width = WINDOW_SIZE_X;
this->client_height = WINDOW_SIZE_Y;
// open window // open window
this->hWnd = CreateWindowEx( this->hWnd = CreateWindowEx(
@@ -53,11 +219,23 @@ cfg::ConfiguratorWindow::ConfiguratorWindow() {
cfg::ConfiguratorWindow::~ConfiguratorWindow() { cfg::ConfiguratorWindow::~ConfiguratorWindow() {
this->stop_timer();
// close window // close window
DestroyWindow(this->hWnd); DestroyWindow(this->hWnd);
// unregister class // unregister class
UnregisterClass(CLASS_NAME, GetModuleHandle(NULL)); UnregisterClass(CLASS_NAME, GetModuleHandle(NULL));
// release D3D9 resources if any
if (this->device) {
this->device->Release();
this->device = nullptr;
}
if (this->d3d) {
this->d3d->Release();
this->d3d = nullptr;
}
} }
void cfg::ConfiguratorWindow::run() { void cfg::ConfiguratorWindow::run() {
@@ -67,8 +245,23 @@ void cfg::ConfiguratorWindow::run() {
ShowWindow(this->hWnd, SW_SHOWNORMAL); ShowWindow(this->hWnd, SW_SHOWNORMAL);
UpdateWindow(this->hWnd); UpdateWindow(this->hWnd);
// draw overlay in 60 FPS // SW_SHOWNORMAL usually activates a top-level window, but not always (e.g.,
SetTimer(this->hWnd, 1, 1000 / 60, nullptr); // when the launching process doesn't have foreground rights to transfer).
// Force foreground+focus explicitly so WM_MOUSEWHEEL is routed to us from
// the very first frame; the Win32 default routes wheel events to the
// keyboard-focused window.
SetForegroundWindow(this->hWnd);
SetFocus(this->hWnd);
// match the render timer to the monitor refresh rate so scrolling stays smooth
// on 60/120/144 Hz panels. Idle CPU is bounded by overlay::sw_pixels_dirty
// (software path) and overlay::d3d9_frame_dirty (D3D9 path); the timer is also
// paused entirely when the window is minimized (see WM_SIZE handler).
const UINT hz = detect_monitor_refresh_hz(this->hWnd);
this->timer_interval_ms = std::max<UINT>(1, 1000 / hz);
log_info("configurator", "render timer {} ms ({} Hz)",
this->timer_interval_ms, hz);
this->start_timer();
// window loop // window loop
BOOL ret; BOOL ret;
@@ -102,6 +295,72 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
break; break;
} }
case WM_SIZE: {
// pause/resume the render timer based on visibility
auto *state = get_state(hWnd);
if (state) {
if (wParam == SIZE_MINIMIZED) {
state->window_minimized = true;
state->stop_timer();
} else {
if (state->window_minimized) {
state->window_minimized = false;
// force a full repaint on the next frame
state->has_valid_draw_hash = false;
state->start_timer();
}
const int new_w = LOWORD(lParam);
const int new_h = HIWORD(lParam);
if (new_w > 0 && new_h > 0
&& (new_w != state->client_width || new_h != state->client_height)) {
state->client_width = new_w;
state->client_height = new_h;
// reset the D3D9 device with the new back-buffer size so the
// hardware-accelerated path matches the window dimensions.
if (state->use_d3d9 && state->device) {
if (overlay::OVERLAY) {
overlay::OVERLAY->reset_invalidate();
}
state->pp.BackBufferWidth = static_cast<UINT>(new_w);
state->pp.BackBufferHeight = static_cast<UINT>(new_h);
HRESULT hr = state->device->Reset(&state->pp);
if (FAILED(hr)) {
log_warning("configurator", "D3D9 device Reset failed, hr={:#x}",
static_cast<unsigned int>(hr));
}
if (overlay::OVERLAY) {
overlay::OVERLAY->reset_recreate();
}
}
// force a full repaint after resize
state->has_valid_draw_hash = false;
}
}
}
break;
}
case WM_DISPLAYCHANGE: {
// monitor refresh rate may have changed (or the window moved to a
// different monitor); re-detect and re-arm the render timer.
auto *state = get_state(hWnd);
if (state && state->timer_running) {
const UINT hz = detect_monitor_refresh_hz(hWnd);
const UINT new_interval = std::max<UINT>(1, 1000 / hz);
if (new_interval != state->timer_interval_ms) {
state->stop_timer();
state->timer_interval_ms = new_interval;
state->start_timer();
log_info("configurator",
"WM_DISPLAYCHANGE: render timer {} ms ({} Hz)",
new_interval, hz);
}
}
break;
}
case WM_CLOSE: case WM_CLOSE:
case WM_DESTROY: { case WM_DESTROY: {
@@ -110,62 +369,192 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
break; break;
} }
case WM_TIMER: { case WM_TIMER: {
if (wParam != RENDER_TIMER_ID) {
break;
}
// update overlay auto *state = get_state(hWnd);
// skip rendering when the window is minimized or hidden - the timer is
// already paused on minimize, but defensively skip here as well.
if (state && (state->window_minimized || !IsWindowVisible(hWnd))) {
break;
}
const bool use_d3d9 = state && state->use_d3d9 && state->device;
// build the imgui frame (input is already buffered via WM_* messages)
if (overlay::OVERLAY) { if (overlay::OVERLAY) {
overlay::OVERLAY->update(); overlay::OVERLAY->update();
overlay::OVERLAY->set_active(true); overlay::OVERLAY->set_active(true);
overlay::OVERLAY->new_frame(); overlay::OVERLAY->new_frame();
overlay::OVERLAY->render();
} }
// repaint window if (use_d3d9) {
InvalidateRect(hWnd, nullptr, TRUE); const HRESULT cl = state->device->TestCooperativeLevel();
if (cl == D3DERR_DEVICELOST) {
break;
}
if (cl == D3DERR_DEVICENOTRESET) {
if (overlay::OVERLAY) {
overlay::OVERLAY->reset_invalidate();
}
const HRESULT reset_hr = state->device->Reset(&state->pp);
if (SUCCEEDED(reset_hr) && overlay::OVERLAY) {
overlay::OVERLAY->reset_recreate();
}
break;
}
if (FAILED(cl)) {
break;
}
if (overlay::OVERLAY) {
overlay::OVERLAY->render();
}
// skip Clear/Present when ImGui draw data is unchanged; the last
// presented frame stays visible (same fast path as sw_pixels_dirty).
const bool dirty = overlay::OVERLAY && overlay::OVERLAY->d3d9_frame_dirty;
const bool force_present = state && !state->has_valid_draw_hash;
if (dirty || force_present) {
if (state) {
state->has_valid_draw_hash = true;
}
state->device->Clear(0, nullptr, D3DCLEAR_TARGET,
D3DCOLOR_RGBA(20, 18, 18, 255), 1.0f, 0);
if (SUCCEEDED(state->device->BeginScene())) {
if (overlay::OVERLAY) {
overlay::OVERLAY->d3d9_render_draw(force_present);
}
state->device->EndScene();
}
const HRESULT hr = state->device->Present(nullptr, nullptr, nullptr, nullptr);
if (hr == D3DERR_DEVICELOST) {
break;
}
}
} else {
if (overlay::OVERLAY) {
overlay::OVERLAY->render();
}
// software path: the overlay's renderer already skipped the costly
// paint_imgui call when the draw data was unchanged. Only invalidate
// the window when those pixels actually changed, or on the first
// frame / after a resize when we lost the previously cached state.
const bool dirty = overlay::OVERLAY && overlay::OVERLAY->sw_pixels_dirty;
const bool force_repaint = state && !state->has_valid_draw_hash;
if (dirty || force_repaint) {
if (state) {
state->has_valid_draw_hash = true;
}
// pass FALSE for bErase - WM_ERASEBKGND already short-circuits, so
// skipping the erase region avoids one extra Win32 round trip.
InvalidateRect(hWnd, nullptr, FALSE);
}
}
break; break;
} }
case WM_ERASEBKGND: { case WM_ERASEBKGND: {
return 1; return 1;
} }
case WM_PAINT: { case WM_PAINT: {
paint_software(hWnd);
// render overlay break;
if (overlay::OVERLAY) { }
case WM_ACTIVATE: {
// get pixel data const WORD activation = LOWORD(wParam);
int width, height; if (activation == WA_INACTIVE) {
uint32_t *pixel_data = overlay::OVERLAY->sw_get_pixel_data(&width, &height); // dropping any held mouse buttons so we don't get stuck in a
if (width > 0 && height > 0) { // "button still pressed" state when we come back.
auto &io = ImGui::GetIO();
// create bitmap io.AddMouseButtonEvent(ImGuiMouseButton_Left, false);
HBITMAP bitmap = CreateBitmap(width, height, 1, 8 * sizeof(uint32_t), pixel_data); io.AddMouseButtonEvent(ImGuiMouseButton_Right, false);
io.AddMouseButtonEvent(ImGuiMouseButton_Middle, false);
// prepare paint
PAINTSTRUCT paint{};
HDC hdc = BeginPaint(hWnd, &paint);
HDC hdcMem = CreateCompatibleDC(hdc);
SetBkMode(hdc, TRANSPARENT);
// draw bitmap
SelectObject(hdcMem, bitmap);
BitBlt(hdc, paint.rcPaint.left, paint.rcPaint.top,
paint.rcPaint.right - paint.rcPaint.left,
paint.rcPaint.bottom - paint.rcPaint.top,
hdcMem, paint.rcPaint.left, paint.rcPaint.top, SRCCOPY);
// delete bitmap
DeleteObject(bitmap);
// clean up
DeleteDC(hdcMem);
EndPaint(hWnd, &paint);
} else {
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
} else { } else {
return DefWindowProc(hWnd, uMsg, wParam, lParam); // WA_ACTIVE or WA_CLICKACTIVE: make sure the keyboard focus is
// actually on this window so WM_MOUSEWHEEL gets routed to us
// again. Without this, scrolling silently stops working after
// the user alt-tabs back to spicecfg.
SetFocus(hWnd);
} }
break; break;
} }
case WM_KILLFOCUS: {
// mirror the WA_INACTIVE mouse-button drop for the keyboard -
// without this, a key held during alt-tab stays "down" in ImGui
// state until the user presses and releases it again.
ImGui::GetIO().ClearInputKeys();
break;
}
// input messages routed straight into ImGui IO. This replaces the previous
// per-frame 256-VK rawinput scan in ImGui_ImplSpice_NewFrame for the
// standalone configurator (see CONFIGURATOR_STANDALONE branches there).
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
case WM_KEYUP:
case WM_SYSKEYUP: {
if (wParam == VK_F4) {
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
const bool down = (uMsg == WM_KEYDOWN) || (uMsg == WM_SYSKEYDOWN);
auto &io = ImGui::GetIO();
const ImGuiKey key = vk_to_imgui_key(wParam);
if (key != ImGuiKey_None) {
io.AddKeyEvent(key, down);
}
const ImGuiKey mod = vk_to_imgui_mod_key(wParam);
if (mod != ImGuiMod_None) {
io.AddKeyEvent(mod, down);
}
break;
}
case WM_MOUSEMOVE: {
auto &io = ImGui::GetIO();
io.AddMousePosEvent(static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
break;
}
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
case WM_MBUTTONDOWN:
case WM_MBUTTONUP: {
int button = 0;
bool down = false;
switch (uMsg) {
case WM_LBUTTONDOWN: button = ImGuiMouseButton_Left; down = true; break;
case WM_LBUTTONUP: button = ImGuiMouseButton_Left; down = false; break;
case WM_RBUTTONDOWN: button = ImGuiMouseButton_Right; down = true; break;
case WM_RBUTTONUP: button = ImGuiMouseButton_Right; down = false; break;
case WM_MBUTTONDOWN: button = ImGuiMouseButton_Middle; down = true; break;
case WM_MBUTTONUP: button = ImGuiMouseButton_Middle; down = false; break;
}
auto &io = ImGui::GetIO();
io.AddMouseButtonEvent(button, down);
if (down) {
SetCapture(hWnd);
} else {
ReleaseCapture();
}
break;
}
case WM_MOUSEWHEEL: {
const float delta = static_cast<float>(GET_WHEEL_DELTA_WPARAM(wParam))
/ static_cast<float>(WHEEL_DELTA);
ImGui::GetIO().AddMouseWheelEvent(0.0f, delta);
break;
}
#if !SPICE_XP
case WM_MOUSEHWHEEL: {
const float delta = static_cast<float>(GET_WHEEL_DELTA_WPARAM(wParam))
/ static_cast<float>(WHEEL_DELTA);
ImGui::GetIO().AddMouseWheelEvent(delta, 0.0f);
break;
}
#endif
default: default:
return DefWindowProc(hWnd, uMsg, wParam, lParam); return DefWindowProc(hWnd, uMsg, wParam, lParam);
} }
+31 -1
View File
@@ -1,6 +1,9 @@
#pragma once #pragma once
#include <cstdint>
#include <windows.h> #include <windows.h>
#include <d3d9.h>
namespace cfg { namespace cfg {
@@ -9,11 +12,38 @@ namespace cfg {
HWND hWnd; HWND hWnd;
// optional D3D9 device backing the configurator window; nullptr when running
// the software-rendered path. Owned by ConfiguratorWindow when set.
IDirect3D9 *d3d = nullptr;
IDirect3DDevice9 *device = nullptr;
D3DPRESENT_PARAMETERS pp {};
bool use_d3d9 = false;
// throttling / pause state. timer_interval_ms is the default until run()
// refines it to the actual monitor refresh rate (see detect_monitor_refresh_hz).
UINT timer_interval_ms = 1000 / 60;
bool timer_running = false;
bool window_minimized = false;
// tracks whether we've issued at least one InvalidateRect since window
// creation/resize. The overlay's per-frame "pixels changed" flag suppresses
// idle blits; this flag forces the very first blit on startup or after
// a resize so the window doesn't show garbage until the user moves the mouse.
bool has_valid_draw_hash = false;
// dimensions of the configurator client area (kept in sync with WM_SIZE)
int client_width = 0;
int client_height = 0;
ConfiguratorWindow(); ConfiguratorWindow();
~ConfiguratorWindow(); ~ConfiguratorWindow();
void run(); void run();
// start/stop the render timer based on visibility state
void start_timer();
void stop_timer();
static LRESULT CALLBACK window_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); static LRESULT CALLBACK window_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
}; };
} }
+7 -2
View File
@@ -24,6 +24,7 @@ namespace overlay::windows {
el->SetAttribute("invert", entry.invert); el->SetAttribute("invert", entry.invert);
el->SetAttribute("debounce_up", entry.debounce_up); el->SetAttribute("debounce_up", entry.debounce_up);
el->SetAttribute("debounce_down", entry.debounce_down); el->SetAttribute("debounce_down", entry.debounce_down);
el->SetAttribute("bat_threshold", entry.bat_threshold);
el->SetAttribute("velocity_threshold", entry.velocity_threshold); el->SetAttribute("velocity_threshold", entry.velocity_threshold);
parent->InsertEndChild(el); parent->InsertEndChild(el);
} }
@@ -49,6 +50,10 @@ namespace overlay::windows {
el->QueryIntAttribute("velocity_threshold", &vel); el->QueryIntAttribute("velocity_threshold", &vel);
entry.velocity_threshold = (unsigned short)vel; entry.velocity_threshold = (unsigned short)vel;
int bat = 0;
el->QueryIntAttribute("bat_threshold", &bat);
entry.bat_threshold = bat;
return entry; return entry;
} }
@@ -65,7 +70,7 @@ namespace overlay::windows {
el->SetAttribute("smoothing", analog.smoothing); el->SetAttribute("smoothing", analog.smoothing);
el->SetAttribute("multiplier", analog.multiplier); el->SetAttribute("multiplier", analog.multiplier);
el->SetAttribute("relative", analog.relative_mode); el->SetAttribute("relative", analog.relative_mode);
el->SetAttribute("delay", analog.delay_buffer_depth); el->SetAttribute("delay_ms", analog.delay_ms);
parent->InsertEndChild(el); parent->InsertEndChild(el);
} }
@@ -88,7 +93,7 @@ namespace overlay::windows {
el->QueryBoolAttribute("smoothing", &a.smoothing); el->QueryBoolAttribute("smoothing", &a.smoothing);
el->QueryIntAttribute("multiplier", &a.multiplier); el->QueryIntAttribute("multiplier", &a.multiplier);
el->QueryBoolAttribute("relative", &a.relative_mode); el->QueryBoolAttribute("relative", &a.relative_mode);
el->QueryIntAttribute("delay", &a.delay_buffer_depth); el->QueryUnsignedAttribute("delay_ms", &a.delay_ms);
return a; return a;
} }
+5 -5
View File
@@ -20,10 +20,10 @@
</dependentAssembly> </dependentAssembly>
</dependency> </dependency>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security> <security>
<requestedPrivileges> <requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/> <requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges> </requestedPrivileges>
</security> </security>
</trustInfo> </trustInfo>
</assembly> </assembly>
+1 -1
View File
@@ -21,7 +21,7 @@ namespace cfg {
if (SCREEN_RESIZE_CFG_PATH_OVERRIDE.has_value()) { if (SCREEN_RESIZE_CFG_PATH_OVERRIDE.has_value()) {
this->config_path = SCREEN_RESIZE_CFG_PATH_OVERRIDE.value(); this->config_path = SCREEN_RESIZE_CFG_PATH_OVERRIDE.value();
if (fileutils::file_exists(this->config_path)) { if (fileutils::file_exists(this->config_path)) {
log_info("ScreenResize", "loading config from: {}", this->config_path.string()); log_info("ScreenResize", "loading config from: {}", this->config_path);
file_exists = true; file_exists = true;
} }
} else { } else {
+1
View File
@@ -20,6 +20,7 @@ Usage:
#include <memory> #include <memory>
#include <string> #include <string>
#include <map> #include <map>
#include <cstdint>
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+7
View File
@@ -21,5 +21,12 @@ set(IMGUI_SOURCES
misc/cpp/imgui_stdlib.cpp misc/cpp/imgui_stdlib.cpp
) )
# spice2x: DX11 backend is only built for non-XP toolchains. WinXP lacks
# D3DCOMPILER_47.dll, and including this TU would pull in D3DCompile imports.
if(NOT SPICE_XP)
list(APPEND IMGUI_HEADERS backends/imgui_impl_dx11.h)
list(APPEND IMGUI_SOURCES backends/imgui_impl_dx11.cpp)
endif()
add_library(imgui STATIC ${IMGUI_HEADERS} ${IMGUI_SOURCES}) add_library(imgui STATIC ${IMGUI_HEADERS} ${IMGUI_SOURCES})
target_include_directories(imgui PRIVATE ${PROJECT_SOURCE_DIR}) target_include_directories(imgui PRIVATE ${PROJECT_SOURCE_DIR})
+697
View File
@@ -0,0 +1,697 @@
// dear imgui: Renderer Backend for DirectX11
// This needs to be used along with a Platform Backend (e.g. Win32)
// Implemented features:
// [X] Renderer: User texture binding. Use 'ID3D11ShaderResourceView*' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef!
// [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset).
// [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures).
// [X] Renderer: Expose selected render state for draw callbacks to use. Access in '(ImGui_ImplXXXX_RenderState*)GetPlatformIO().Renderer_RenderState'.
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
// Learn about Dear ImGui:
// - FAQ https://dearimgui.com/faq
// - Getting Started https://dearimgui.com/getting-started
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
// - Introduction, links and more at the top of imgui.cpp
// CHANGELOG
// (minor and older changes stripped away, please see git history for details)
// 2026-01-19: DirectX11: Added 'SamplerNearest' in ImGui_ImplDX11_RenderState. Renamed 'SamplerDefault' to 'SamplerLinear'.
// 2025-09-18: Call platform_io.ClearRendererHandlers() on shutdown.
// 2025-06-11: DirectX11: Added support for ImGuiBackendFlags_RendererHasTextures, for dynamic font atlas.
// 2025-05-07: DirectX11: Honor draw_data->FramebufferScale to allow for custom backends and experiment using it (consistently with other renderer backends, even though in normal condition it is not set under Windows).
// 2025-01-06: DirectX11: Expose VertexConstantBuffer in ImGui_ImplDX11_RenderState. Reset projection matrix in ImDrawCallback_ResetRenderState handler.
// 2024-10-07: DirectX11: Changed default texture sampler to Clamp instead of Repeat/Wrap.
// 2024-10-07: DirectX11: Expose selected render state in ImGui_ImplDX11_RenderState, which you can access in 'void* platform_io.Renderer_RenderState' during draw callbacks.
// 2022-10-11: Using 'nullptr' instead of 'NULL' as per our switch to C++11.
// 2021-06-29: Reorganized backend to pull data from a single structure to facilitate usage with multiple-contexts (all g_XXXX access changed to bd->XXXX).
// 2021-05-19: DirectX11: Replaced direct access to ImDrawCmd::TextureId with a call to ImDrawCmd::GetTexID(). (will become a requirement)
// 2021-02-18: DirectX11: Change blending equation to preserve alpha in output buffer.
// 2019-08-01: DirectX11: Fixed code querying the Geometry Shader state (would generally error with Debug layer enabled).
// 2019-07-21: DirectX11: Backup, clear and restore Geometry Shader is any is bound when calling ImGui_ImplDX11_RenderDrawData. Clearing Hull/Domain/Compute shaders without backup/restore.
// 2019-05-29: DirectX11: Added support for large mesh (64K+ vertices), enable ImGuiBackendFlags_RendererHasVtxOffset flag.
// 2019-04-30: DirectX11: Added support for special ImDrawCallback_ResetRenderState callback to reset render state.
// 2018-12-03: Misc: Added #pragma comment statement to automatically link with d3dcompiler.lib when using D3DCompile().
// 2018-11-30: Misc: Setting up io.BackendRendererName so it can be displayed in the About Window.
// 2018-08-01: DirectX11: Querying for IDXGIFactory instead of IDXGIFactory1 to increase compatibility.
// 2018-07-13: DirectX11: Fixed unreleased resources in Init and Shutdown functions.
// 2018-06-08: Misc: Extracted imgui_impl_dx11.cpp/.h away from the old combined DX11+Win32 example.
// 2018-06-08: DirectX11: Use draw_data->DisplayPos and draw_data->DisplaySize to setup projection matrix and clipping rectangle.
// 2018-02-16: Misc: Obsoleted the io.RenderDrawListsFn callback and exposed ImGui_ImplDX11_RenderDrawData() in the .h file so you can call it yourself.
// 2018-02-06: Misc: Removed call to ImGui::Shutdown() which is not available from 1.60 WIP, user needs to call CreateContext/DestroyContext themselves.
// 2016-05-07: DirectX11: Disabling depth-write.
#include "imgui.h"
#ifndef IMGUI_DISABLE
#include "imgui_impl_dx11.h"
// DirectX
#include <stdio.h>
#include <d3d11.h>
#include <d3dcompiler.h>
#ifdef _MSC_VER
#pragma comment(lib, "d3dcompiler") // Automatically link with d3dcompiler.lib as we are using D3DCompile() below.
#endif
// Clang/GCC warnings with -Weverything
#if defined(__clang__)
#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse.
#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness
#endif
// DirectX11 data
struct ImGui_ImplDX11_Texture
{
ID3D11Texture2D* pTexture;
ID3D11ShaderResourceView* pTextureView;
};
struct ImGui_ImplDX11_Data
{
ID3D11Device* pd3dDevice;
ID3D11DeviceContext* pd3dDeviceContext;
IDXGIFactory* pFactory;
ID3D11Buffer* pVB;
ID3D11Buffer* pIB;
ID3D11VertexShader* pVertexShader;
ID3D11InputLayout* pInputLayout;
ID3D11Buffer* pVertexConstantBuffer;
ID3D11PixelShader* pPixelShader;
ID3D11SamplerState* pTexSamplerLinear;
ID3D11SamplerState* pTexSamplerNearest;
ID3D11RasterizerState* pRasterizerState;
ID3D11BlendState* pBlendState;
ID3D11DepthStencilState* pDepthStencilState;
int VertexBufferSize;
int IndexBufferSize;
ImGui_ImplDX11_Data() { memset((void*)this, 0, sizeof(*this)); VertexBufferSize = 5000; IndexBufferSize = 10000; }
};
struct VERTEX_CONSTANT_BUFFER_DX11
{
float mvp[4][4];
};
// Backend data stored in io.BackendRendererUserData to allow support for multiple Dear ImGui contexts
// It is STRONGLY preferred that you use docking branch with multi-viewports (== single Dear ImGui context + multiple windows) instead of multiple Dear ImGui contexts.
static ImGui_ImplDX11_Data* ImGui_ImplDX11_GetBackendData()
{
return ImGui::GetCurrentContext() ? (ImGui_ImplDX11_Data*)ImGui::GetIO().BackendRendererUserData : nullptr;
}
// Functions
static void ImGui_ImplDX11_SetupRenderState(const ImDrawData* draw_data, ID3D11DeviceContext* device_ctx)
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
// Setup viewport
D3D11_VIEWPORT vp = {};
vp.Width = draw_data->DisplaySize.x * draw_data->FramebufferScale.x;
vp.Height = draw_data->DisplaySize.y * draw_data->FramebufferScale.y;
vp.MinDepth = 0.0f;
vp.MaxDepth = 1.0f;
vp.TopLeftX = vp.TopLeftY = 0;
device_ctx->RSSetViewports(1, &vp);
// Setup orthographic projection matrix into our constant buffer
// Our visible imgui space lies from draw_data->DisplayPos (top left) to draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayPos is (0,0) for single viewport apps.
D3D11_MAPPED_SUBRESOURCE mapped_resource;
if (device_ctx->Map(bd->pVertexConstantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped_resource) == S_OK)
{
VERTEX_CONSTANT_BUFFER_DX11* constant_buffer = (VERTEX_CONSTANT_BUFFER_DX11*)mapped_resource.pData;
float L = draw_data->DisplayPos.x;
float R = draw_data->DisplayPos.x + draw_data->DisplaySize.x;
float T = draw_data->DisplayPos.y;
float B = draw_data->DisplayPos.y + draw_data->DisplaySize.y;
float mvp[4][4] =
{
{ 2.0f/(R-L), 0.0f, 0.0f, 0.0f },
{ 0.0f, 2.0f/(T-B), 0.0f, 0.0f },
{ 0.0f, 0.0f, 0.5f, 0.0f },
{ (R+L)/(L-R), (T+B)/(B-T), 0.5f, 1.0f },
};
memcpy(&constant_buffer->mvp, mvp, sizeof(mvp));
device_ctx->Unmap(bd->pVertexConstantBuffer, 0);
}
// Setup shader and vertex buffers
unsigned int stride = sizeof(ImDrawVert);
unsigned int offset = 0;
device_ctx->IASetInputLayout(bd->pInputLayout);
device_ctx->IASetVertexBuffers(0, 1, &bd->pVB, &stride, &offset);
device_ctx->IASetIndexBuffer(bd->pIB, sizeof(ImDrawIdx) == 2 ? DXGI_FORMAT_R16_UINT : DXGI_FORMAT_R32_UINT, 0);
device_ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
device_ctx->VSSetShader(bd->pVertexShader, nullptr, 0);
device_ctx->VSSetConstantBuffers(0, 1, &bd->pVertexConstantBuffer);
device_ctx->PSSetShader(bd->pPixelShader, nullptr, 0);
device_ctx->PSSetSamplers(0, 1, &bd->pTexSamplerLinear);
device_ctx->GSSetShader(nullptr, nullptr, 0);
device_ctx->HSSetShader(nullptr, nullptr, 0); // In theory we should backup and restore this as well.. very infrequently used..
device_ctx->DSSetShader(nullptr, nullptr, 0); // In theory we should backup and restore this as well.. very infrequently used..
device_ctx->CSSetShader(nullptr, nullptr, 0); // In theory we should backup and restore this as well.. very infrequently used..
// Setup render state
const float blend_factor[4] = { 0.f, 0.f, 0.f, 0.f };
device_ctx->OMSetBlendState(bd->pBlendState, blend_factor, 0xffffffff);
device_ctx->OMSetDepthStencilState(bd->pDepthStencilState, 0);
device_ctx->RSSetState(bd->pRasterizerState);
}
// Render function
void ImGui_ImplDX11_RenderDrawData(ImDrawData* draw_data)
{
// Avoid rendering when minimized
if (draw_data->DisplaySize.x <= 0.0f || draw_data->DisplaySize.y <= 0.0f)
return;
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
ID3D11DeviceContext* device = bd->pd3dDeviceContext;
// Catch up with texture updates. Most of the times, the list will have 1 element with an OK status, aka nothing to do.
// (This almost always points to ImGui::GetPlatformIO().Textures[] but is part of ImDrawData to allow overriding or disabling texture updates).
if (draw_data->Textures != nullptr)
for (ImTextureData* tex : *draw_data->Textures)
if (tex->Status != ImTextureStatus_OK)
ImGui_ImplDX11_UpdateTexture(tex);
// Create and grow vertex/index buffers if needed
if (!bd->pVB || bd->VertexBufferSize < draw_data->TotalVtxCount)
{
if (bd->pVB) { bd->pVB->Release(); bd->pVB = nullptr; }
bd->VertexBufferSize = draw_data->TotalVtxCount + 5000;
D3D11_BUFFER_DESC desc = {};
desc.Usage = D3D11_USAGE_DYNAMIC;
desc.ByteWidth = bd->VertexBufferSize * sizeof(ImDrawVert);
desc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
desc.MiscFlags = 0;
if (bd->pd3dDevice->CreateBuffer(&desc, nullptr, &bd->pVB) < 0)
return;
}
if (!bd->pIB || bd->IndexBufferSize < draw_data->TotalIdxCount)
{
if (bd->pIB) { bd->pIB->Release(); bd->pIB = nullptr; }
bd->IndexBufferSize = draw_data->TotalIdxCount + 10000;
D3D11_BUFFER_DESC desc = {};
desc.Usage = D3D11_USAGE_DYNAMIC;
desc.ByteWidth = bd->IndexBufferSize * sizeof(ImDrawIdx);
desc.BindFlags = D3D11_BIND_INDEX_BUFFER;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
if (bd->pd3dDevice->CreateBuffer(&desc, nullptr, &bd->pIB) < 0)
return;
}
// Upload vertex/index data into a single contiguous GPU buffer
D3D11_MAPPED_SUBRESOURCE vtx_resource, idx_resource;
if (device->Map(bd->pVB, 0, D3D11_MAP_WRITE_DISCARD, 0, &vtx_resource) != S_OK)
return;
if (device->Map(bd->pIB, 0, D3D11_MAP_WRITE_DISCARD, 0, &idx_resource) != S_OK)
return;
ImDrawVert* vtx_dst = (ImDrawVert*)vtx_resource.pData;
ImDrawIdx* idx_dst = (ImDrawIdx*)idx_resource.pData;
for (const ImDrawList* draw_list : draw_data->CmdLists)
{
memcpy(vtx_dst, draw_list->VtxBuffer.Data, draw_list->VtxBuffer.Size * sizeof(ImDrawVert));
memcpy(idx_dst, draw_list->IdxBuffer.Data, draw_list->IdxBuffer.Size * sizeof(ImDrawIdx));
vtx_dst += draw_list->VtxBuffer.Size;
idx_dst += draw_list->IdxBuffer.Size;
}
device->Unmap(bd->pVB, 0);
device->Unmap(bd->pIB, 0);
// Backup DX state that will be modified to restore it afterwards (unfortunately this is very ugly looking and verbose. Close your eyes!)
struct BACKUP_DX11_STATE
{
UINT ScissorRectsCount, ViewportsCount;
D3D11_RECT ScissorRects[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE];
D3D11_VIEWPORT Viewports[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE];
ID3D11RasterizerState* RS;
ID3D11BlendState* BlendState;
FLOAT BlendFactor[4];
UINT SampleMask;
UINT StencilRef;
ID3D11DepthStencilState* DepthStencilState;
ID3D11ShaderResourceView* PSShaderResource;
ID3D11SamplerState* PSSampler;
ID3D11PixelShader* PS;
ID3D11VertexShader* VS;
ID3D11GeometryShader* GS;
UINT PSInstancesCount, VSInstancesCount, GSInstancesCount;
ID3D11ClassInstance *PSInstances[256], *VSInstances[256], *GSInstances[256]; // 256 is max according to PSSetShader documentation
D3D11_PRIMITIVE_TOPOLOGY PrimitiveTopology;
ID3D11Buffer* IndexBuffer, *VertexBuffer, *VSConstantBuffer;
UINT IndexBufferOffset, VertexBufferStride, VertexBufferOffset;
DXGI_FORMAT IndexBufferFormat;
ID3D11InputLayout* InputLayout;
};
BACKUP_DX11_STATE old = {};
old.ScissorRectsCount = old.ViewportsCount = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE;
device->RSGetScissorRects(&old.ScissorRectsCount, old.ScissorRects);
device->RSGetViewports(&old.ViewportsCount, old.Viewports);
device->RSGetState(&old.RS);
device->OMGetBlendState(&old.BlendState, old.BlendFactor, &old.SampleMask);
device->OMGetDepthStencilState(&old.DepthStencilState, &old.StencilRef);
device->PSGetShaderResources(0, 1, &old.PSShaderResource);
device->PSGetSamplers(0, 1, &old.PSSampler);
old.PSInstancesCount = old.VSInstancesCount = old.GSInstancesCount = 256;
device->PSGetShader(&old.PS, old.PSInstances, &old.PSInstancesCount);
device->VSGetShader(&old.VS, old.VSInstances, &old.VSInstancesCount);
device->VSGetConstantBuffers(0, 1, &old.VSConstantBuffer);
device->GSGetShader(&old.GS, old.GSInstances, &old.GSInstancesCount);
device->IAGetPrimitiveTopology(&old.PrimitiveTopology);
device->IAGetIndexBuffer(&old.IndexBuffer, &old.IndexBufferFormat, &old.IndexBufferOffset);
device->IAGetVertexBuffers(0, 1, &old.VertexBuffer, &old.VertexBufferStride, &old.VertexBufferOffset);
device->IAGetInputLayout(&old.InputLayout);
// Setup desired DX state
ImGui_ImplDX11_SetupRenderState(draw_data, device);
// Setup render state structure (for callbacks and custom texture bindings)
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
ImGui_ImplDX11_RenderState render_state;
render_state.Device = bd->pd3dDevice;
render_state.DeviceContext = bd->pd3dDeviceContext;
render_state.SamplerLinear = bd->pTexSamplerLinear;
render_state.SamplerNearest = bd->pTexSamplerNearest;
render_state.VertexConstantBuffer = bd->pVertexConstantBuffer;
platform_io.Renderer_RenderState = &render_state;
// Render command lists
// (Because we merged all buffers into a single one, we maintain our own offset into them)
int global_idx_offset = 0;
int global_vtx_offset = 0;
ImVec2 clip_off = draw_data->DisplayPos;
ImVec2 clip_scale = draw_data->FramebufferScale;
for (const ImDrawList* draw_list : draw_data->CmdLists)
{
for (int cmd_i = 0; cmd_i < draw_list->CmdBuffer.Size; cmd_i++)
{
const ImDrawCmd* pcmd = &draw_list->CmdBuffer[cmd_i];
if (pcmd->UserCallback != nullptr)
{
// User callback, registered via ImDrawList::AddCallback()
// (ImDrawCallback_ResetRenderState is a special callback value used by the user to request the renderer to reset render state.)
if (pcmd->UserCallback == ImDrawCallback_ResetRenderState)
ImGui_ImplDX11_SetupRenderState(draw_data, device);
else
pcmd->UserCallback(draw_list, pcmd);
}
else
{
// Project scissor/clipping rectangles into framebuffer space
ImVec2 clip_min((pcmd->ClipRect.x - clip_off.x) * clip_scale.x, (pcmd->ClipRect.y - clip_off.y) * clip_scale.y);
ImVec2 clip_max((pcmd->ClipRect.z - clip_off.x) * clip_scale.x, (pcmd->ClipRect.w - clip_off.y) * clip_scale.y);
if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y)
continue;
// Apply scissor/clipping rectangle
const D3D11_RECT r = { (LONG)clip_min.x, (LONG)clip_min.y, (LONG)clip_max.x, (LONG)clip_max.y };
device->RSSetScissorRects(1, &r);
// Bind texture, Draw
ID3D11ShaderResourceView* texture_srv = (ID3D11ShaderResourceView*)pcmd->GetTexID();
device->PSSetShaderResources(0, 1, &texture_srv);
device->DrawIndexed(pcmd->ElemCount, pcmd->IdxOffset + global_idx_offset, pcmd->VtxOffset + global_vtx_offset);
}
}
global_idx_offset += draw_list->IdxBuffer.Size;
global_vtx_offset += draw_list->VtxBuffer.Size;
}
platform_io.Renderer_RenderState = nullptr;
// Restore modified DX state
device->RSSetScissorRects(old.ScissorRectsCount, old.ScissorRects);
device->RSSetViewports(old.ViewportsCount, old.Viewports);
device->RSSetState(old.RS); if (old.RS) old.RS->Release();
device->OMSetBlendState(old.BlendState, old.BlendFactor, old.SampleMask); if (old.BlendState) old.BlendState->Release();
device->OMSetDepthStencilState(old.DepthStencilState, old.StencilRef); if (old.DepthStencilState) old.DepthStencilState->Release();
device->PSSetShaderResources(0, 1, &old.PSShaderResource); if (old.PSShaderResource) old.PSShaderResource->Release();
device->PSSetSamplers(0, 1, &old.PSSampler); if (old.PSSampler) old.PSSampler->Release();
device->PSSetShader(old.PS, old.PSInstances, old.PSInstancesCount); if (old.PS) old.PS->Release();
for (UINT i = 0; i < old.PSInstancesCount; i++) if (old.PSInstances[i]) old.PSInstances[i]->Release();
device->VSSetShader(old.VS, old.VSInstances, old.VSInstancesCount); if (old.VS) old.VS->Release();
device->VSSetConstantBuffers(0, 1, &old.VSConstantBuffer); if (old.VSConstantBuffer) old.VSConstantBuffer->Release();
device->GSSetShader(old.GS, old.GSInstances, old.GSInstancesCount); if (old.GS) old.GS->Release();
for (UINT i = 0; i < old.VSInstancesCount; i++) if (old.VSInstances[i]) old.VSInstances[i]->Release();
device->IASetPrimitiveTopology(old.PrimitiveTopology);
device->IASetIndexBuffer(old.IndexBuffer, old.IndexBufferFormat, old.IndexBufferOffset); if (old.IndexBuffer) old.IndexBuffer->Release();
device->IASetVertexBuffers(0, 1, &old.VertexBuffer, &old.VertexBufferStride, &old.VertexBufferOffset); if (old.VertexBuffer) old.VertexBuffer->Release();
device->IASetInputLayout(old.InputLayout); if (old.InputLayout) old.InputLayout->Release();
}
static void ImGui_ImplDX11_DestroyTexture(ImTextureData* tex)
{
if (ImGui_ImplDX11_Texture* backend_tex = (ImGui_ImplDX11_Texture*)tex->BackendUserData)
{
IM_ASSERT(backend_tex->pTextureView == (ID3D11ShaderResourceView*)(intptr_t)tex->TexID);
backend_tex->pTextureView->Release();
backend_tex->pTexture->Release();
IM_DELETE(backend_tex);
// Clear identifiers and mark as destroyed (in order to allow e.g. calling InvalidateDeviceObjects while running)
tex->SetTexID(ImTextureID_Invalid);
tex->BackendUserData = nullptr;
}
tex->SetStatus(ImTextureStatus_Destroyed);
}
void ImGui_ImplDX11_UpdateTexture(ImTextureData* tex)
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
if (tex->Status == ImTextureStatus_WantCreate)
{
// Create and upload new texture to graphics system
//IMGUI_DEBUG_LOG("UpdateTexture #%03d: WantCreate %dx%d\n", tex->UniqueID, tex->Width, tex->Height);
IM_ASSERT(tex->TexID == ImTextureID_Invalid && tex->BackendUserData == nullptr);
IM_ASSERT(tex->Format == ImTextureFormat_RGBA32);
unsigned int* pixels = (unsigned int*)tex->GetPixels();
ImGui_ImplDX11_Texture* backend_tex = IM_NEW(ImGui_ImplDX11_Texture)();
// Create texture
D3D11_TEXTURE2D_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.Width = (UINT)tex->Width;
desc.Height = (UINT)tex->Height;
desc.MipLevels = 1;
desc.ArraySize = 1;
#ifdef IMGUI_USE_BGRA_PACKED_COLOR
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
#else
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
#endif
desc.SampleDesc.Count = 1;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
desc.CPUAccessFlags = 0;
D3D11_SUBRESOURCE_DATA subResource;
subResource.pSysMem = pixels;
subResource.SysMemPitch = desc.Width * 4;
subResource.SysMemSlicePitch = 0;
bd->pd3dDevice->CreateTexture2D(&desc, &subResource, &backend_tex->pTexture);
IM_ASSERT(backend_tex->pTexture != nullptr && "Backend failed to create texture!");
// Create texture view
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
ZeroMemory(&srvDesc, sizeof(srvDesc));
#ifdef IMGUI_USE_BGRA_PACKED_COLOR
srvDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
#else
srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
#endif
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MipLevels = desc.MipLevels;
srvDesc.Texture2D.MostDetailedMip = 0;
bd->pd3dDevice->CreateShaderResourceView(backend_tex->pTexture, &srvDesc, &backend_tex->pTextureView);
IM_ASSERT(backend_tex->pTextureView != nullptr && "Backend failed to create texture!");
// Store identifiers
tex->SetTexID((ImTextureID)(intptr_t)backend_tex->pTextureView);
tex->SetStatus(ImTextureStatus_OK);
tex->BackendUserData = backend_tex;
}
else if (tex->Status == ImTextureStatus_WantUpdates)
{
// Update selected blocks. We only ever write to textures regions which have never been used before!
// This backend choose to use tex->Updates[] but you can use tex->UpdateRect to upload a single region.
ImGui_ImplDX11_Texture* backend_tex = (ImGui_ImplDX11_Texture*)tex->BackendUserData;
IM_ASSERT(backend_tex->pTextureView == (ID3D11ShaderResourceView*)(intptr_t)tex->TexID);
for (ImTextureRect& r : tex->Updates)
{
D3D11_BOX box = { (UINT)r.x, (UINT)r.y, (UINT)0, (UINT)(r.x + r.w), (UINT)(r.y + r .h), (UINT)1 };
bd->pd3dDeviceContext->UpdateSubresource(backend_tex->pTexture, 0, &box, tex->GetPixelsAt(r.x, r.y), (UINT)tex->GetPitch(), 0);
}
tex->SetStatus(ImTextureStatus_OK);
}
if (tex->Status == ImTextureStatus_WantDestroy && tex->UnusedFrames > 0)
ImGui_ImplDX11_DestroyTexture(tex);
}
bool ImGui_ImplDX11_CreateDeviceObjects()
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
if (!bd->pd3dDevice)
return false;
ImGui_ImplDX11_InvalidateDeviceObjects();
// By using D3DCompile() from <d3dcompiler.h> / d3dcompiler.lib, we introduce a dependency to a given version of d3dcompiler_XX.dll (see D3DCOMPILER_DLL_A)
// If you would like to use this DX11 sample code but remove this dependency you can:
// 1) compile once, save the compiled shader blobs into a file or source code and pass them to CreateVertexShader()/CreatePixelShader() [preferred solution]
// 2) use code to detect any version of the DLL and grab a pointer to D3DCompile from the DLL.
// See https://github.com/ocornut/imgui/pull/638 for sources and details.
// Create the vertex shader
{
static const char* vertexShader =
"cbuffer vertexBuffer : register(b0) \
{\
float4x4 ProjectionMatrix; \
};\
struct VS_INPUT\
{\
float2 pos : POSITION;\
float4 col : COLOR0;\
float2 uv : TEXCOORD0;\
};\
\
struct PS_INPUT\
{\
float4 pos : SV_POSITION;\
float4 col : COLOR0;\
float2 uv : TEXCOORD0;\
};\
\
PS_INPUT main(VS_INPUT input)\
{\
PS_INPUT output;\
output.pos = mul( ProjectionMatrix, float4(input.pos.xy, 0.f, 1.f));\
output.col = input.col;\
output.uv = input.uv;\
return output;\
}";
ID3DBlob* vertexShaderBlob;
if (FAILED(D3DCompile(vertexShader, strlen(vertexShader), nullptr, nullptr, nullptr, "main", "vs_4_0", 0, 0, &vertexShaderBlob, nullptr)))
return false; // NB: Pass ID3DBlob* pErrorBlob to D3DCompile() to get error showing in (const char*)pErrorBlob->GetBufferPointer(). Make sure to Release() the blob!
if (bd->pd3dDevice->CreateVertexShader(vertexShaderBlob->GetBufferPointer(), vertexShaderBlob->GetBufferSize(), nullptr, &bd->pVertexShader) != S_OK)
{
vertexShaderBlob->Release();
return false;
}
// Create the input layout
D3D11_INPUT_ELEMENT_DESC local_layout[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (UINT)offsetof(ImDrawVert, pos), D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (UINT)offsetof(ImDrawVert, uv), D3D11_INPUT_PER_VERTEX_DATA, 0 },
#ifdef IMGUI_USE_BGRA_PACKED_COLOR
{ "COLOR", 0, DXGI_FORMAT_B8G8R8A8_UNORM, 0, (UINT)offsetof(ImDrawVert, col), D3D11_INPUT_PER_VERTEX_DATA, 0 },
#else
{ "COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, (UINT)offsetof(ImDrawVert, col), D3D11_INPUT_PER_VERTEX_DATA, 0 },
#endif
};
if (bd->pd3dDevice->CreateInputLayout(local_layout, 3, vertexShaderBlob->GetBufferPointer(), vertexShaderBlob->GetBufferSize(), &bd->pInputLayout) != S_OK)
{
vertexShaderBlob->Release();
return false;
}
vertexShaderBlob->Release();
// Create the constant buffer
{
D3D11_BUFFER_DESC desc = {};
desc.ByteWidth = sizeof(VERTEX_CONSTANT_BUFFER_DX11);
desc.Usage = D3D11_USAGE_DYNAMIC;
desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
desc.MiscFlags = 0;
bd->pd3dDevice->CreateBuffer(&desc, nullptr, &bd->pVertexConstantBuffer);
}
}
// Create the pixel shader
{
static const char* pixelShader =
"struct PS_INPUT\
{\
float4 pos : SV_POSITION;\
float4 col : COLOR0;\
float2 uv : TEXCOORD0;\
};\
sampler sampler0;\
Texture2D texture0;\
\
float4 main(PS_INPUT input) : SV_Target\
{\
float4 out_col = input.col * texture0.Sample(sampler0, input.uv); \
return out_col; \
}";
ID3DBlob* pixelShaderBlob;
if (FAILED(D3DCompile(pixelShader, strlen(pixelShader), nullptr, nullptr, nullptr, "main", "ps_4_0", 0, 0, &pixelShaderBlob, nullptr)))
return false; // NB: Pass ID3DBlob* pErrorBlob to D3DCompile() to get error showing in (const char*)pErrorBlob->GetBufferPointer(). Make sure to Release() the blob!
if (bd->pd3dDevice->CreatePixelShader(pixelShaderBlob->GetBufferPointer(), pixelShaderBlob->GetBufferSize(), nullptr, &bd->pPixelShader) != S_OK)
{
pixelShaderBlob->Release();
return false;
}
pixelShaderBlob->Release();
}
// Create the blending setup
{
D3D11_BLEND_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.AlphaToCoverageEnable = false;
desc.RenderTarget[0].BlendEnable = true;
desc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;
desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA;
desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
bd->pd3dDevice->CreateBlendState(&desc, &bd->pBlendState);
}
// Create the rasterizer state
{
D3D11_RASTERIZER_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.FillMode = D3D11_FILL_SOLID;
desc.CullMode = D3D11_CULL_NONE;
desc.ScissorEnable = true;
desc.DepthClipEnable = true;
bd->pd3dDevice->CreateRasterizerState(&desc, &bd->pRasterizerState);
}
// Create depth-stencil State
{
D3D11_DEPTH_STENCIL_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.DepthEnable = false;
desc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
desc.DepthFunc = D3D11_COMPARISON_ALWAYS;
desc.StencilEnable = false;
desc.FrontFace.StencilFailOp = desc.FrontFace.StencilDepthFailOp = desc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
desc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
desc.BackFace = desc.FrontFace;
bd->pd3dDevice->CreateDepthStencilState(&desc, &bd->pDepthStencilState);
}
// Create texture sampler
// (Bilinear sampling is required by default. Set 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow point/nearest sampling)
{
D3D11_SAMPLER_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
desc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
desc.MipLODBias = 0.f;
desc.ComparisonFunc = D3D11_COMPARISON_ALWAYS;
desc.MinLOD = 0.f;
desc.MaxLOD = 0.f;
bd->pd3dDevice->CreateSamplerState(&desc, &bd->pTexSamplerLinear);
desc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT;
bd->pd3dDevice->CreateSamplerState(&desc, &bd->pTexSamplerNearest);
}
return true;
}
void ImGui_ImplDX11_InvalidateDeviceObjects()
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
if (!bd->pd3dDevice)
return;
// Destroy all textures
for (ImTextureData* tex : ImGui::GetPlatformIO().Textures)
if (tex->RefCount == 1)
ImGui_ImplDX11_DestroyTexture(tex);
if (bd->pTexSamplerLinear) { bd->pTexSamplerLinear->Release(); bd->pTexSamplerLinear = nullptr; }
if (bd->pTexSamplerNearest) { bd->pTexSamplerNearest->Release(); bd->pTexSamplerNearest = nullptr; }
if (bd->pIB) { bd->pIB->Release(); bd->pIB = nullptr; }
if (bd->pVB) { bd->pVB->Release(); bd->pVB = nullptr; }
if (bd->pBlendState) { bd->pBlendState->Release(); bd->pBlendState = nullptr; }
if (bd->pDepthStencilState) { bd->pDepthStencilState->Release(); bd->pDepthStencilState = nullptr; }
if (bd->pRasterizerState) { bd->pRasterizerState->Release(); bd->pRasterizerState = nullptr; }
if (bd->pPixelShader) { bd->pPixelShader->Release(); bd->pPixelShader = nullptr; }
if (bd->pVertexConstantBuffer) { bd->pVertexConstantBuffer->Release(); bd->pVertexConstantBuffer = nullptr; }
if (bd->pInputLayout) { bd->pInputLayout->Release(); bd->pInputLayout = nullptr; }
if (bd->pVertexShader) { bd->pVertexShader->Release(); bd->pVertexShader = nullptr; }
}
bool ImGui_ImplDX11_Init(ID3D11Device* device, ID3D11DeviceContext* device_context)
{
ImGuiIO& io = ImGui::GetIO();
IMGUI_CHECKVERSION();
IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!");
// Setup backend capabilities flags
ImGui_ImplDX11_Data* bd = IM_NEW(ImGui_ImplDX11_Data)();
io.BackendRendererUserData = (void*)bd;
io.BackendRendererName = "imgui_impl_dx11";
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
io.BackendFlags |= ImGuiBackendFlags_RendererHasTextures; // We can honor ImGuiPlatformIO::Textures[] requests during render.
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
platform_io.Renderer_TextureMaxWidth = platform_io.Renderer_TextureMaxHeight = D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION;
// Get factory from device
IDXGIDevice* pDXGIDevice = nullptr;
IDXGIAdapter* pDXGIAdapter = nullptr;
IDXGIFactory* pFactory = nullptr;
if (device->QueryInterface(IID_PPV_ARGS(&pDXGIDevice)) == S_OK)
if (pDXGIDevice->GetParent(IID_PPV_ARGS(&pDXGIAdapter)) == S_OK)
if (pDXGIAdapter->GetParent(IID_PPV_ARGS(&pFactory)) == S_OK)
{
bd->pd3dDevice = device;
bd->pd3dDeviceContext = device_context;
bd->pFactory = pFactory;
}
if (pDXGIDevice) pDXGIDevice->Release();
if (pDXGIAdapter) pDXGIAdapter->Release();
bd->pd3dDevice->AddRef();
bd->pd3dDeviceContext->AddRef();
return true;
}
void ImGui_ImplDX11_Shutdown()
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?");
ImGuiIO& io = ImGui::GetIO();
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
ImGui_ImplDX11_InvalidateDeviceObjects();
if (bd->pFactory) { bd->pFactory->Release(); }
if (bd->pd3dDevice) { bd->pd3dDevice->Release(); }
if (bd->pd3dDeviceContext) { bd->pd3dDeviceContext->Release(); }
io.BackendRendererName = nullptr;
io.BackendRendererUserData = nullptr;
io.BackendFlags &= ~(ImGuiBackendFlags_RendererHasVtxOffset | ImGuiBackendFlags_RendererHasTextures);
platform_io.ClearRendererHandlers();
IM_DELETE(bd);
}
void ImGui_ImplDX11_NewFrame()
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
IM_ASSERT(bd != nullptr && "Context or backend not initialized! Did you call ImGui_ImplDX11_Init()?");
if (!bd->pVertexShader)
if (!ImGui_ImplDX11_CreateDeviceObjects())
IM_ASSERT(0 && "ImGui_ImplDX11_CreateDeviceObjects() failed!");
}
//-----------------------------------------------------------------------------
#endif // #ifndef IMGUI_DISABLE
+52
View File
@@ -0,0 +1,52 @@
// dear imgui: Renderer Backend for DirectX11
// This needs to be used along with a Platform Backend (e.g. Win32)
// Implemented features:
// [X] Renderer: User texture binding. Use 'ID3D11ShaderResourceView*' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef!
// [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset).
// [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures).
// [X] Renderer: Expose selected render state for draw callbacks to use. Access in '(ImGui_ImplXXXX_RenderState*)GetPlatformIO().Renderer_RenderState'.
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
// Learn about Dear ImGui:
// - FAQ https://dearimgui.com/faq
// - Getting Started https://dearimgui.com/getting-started
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
// - Introduction, links and more at the top of imgui.cpp
#pragma once
#include "imgui.h" // IMGUI_IMPL_API
#ifndef IMGUI_DISABLE
struct ID3D11Device;
struct ID3D11DeviceContext;
struct ID3D11SamplerState;
struct ID3D11Buffer;
// Follow "Getting Started" link and check examples/ folder to learn about using backends!
IMGUI_IMPL_API bool ImGui_ImplDX11_Init(ID3D11Device* device, ID3D11DeviceContext* device_context);
IMGUI_IMPL_API void ImGui_ImplDX11_Shutdown();
IMGUI_IMPL_API void ImGui_ImplDX11_NewFrame();
IMGUI_IMPL_API void ImGui_ImplDX11_RenderDrawData(ImDrawData* draw_data);
// Use if you want to reset your rendering device without losing Dear ImGui state.
IMGUI_IMPL_API bool ImGui_ImplDX11_CreateDeviceObjects();
IMGUI_IMPL_API void ImGui_ImplDX11_InvalidateDeviceObjects();
// (Advanced) Use e.g. if you need to precisely control the timing of texture updates (e.g. for staged rendering), by setting ImDrawData::Textures = nullptr to handle this manually.
IMGUI_IMPL_API void ImGui_ImplDX11_UpdateTexture(ImTextureData* tex);
// [BETA] Selected render state data shared with callbacks.
// This is temporarily stored in GetPlatformIO().Renderer_RenderState during the ImGui_ImplDX11_RenderDrawData() call.
// (Please open an issue if you feel you need access to more data)
struct ImGui_ImplDX11_RenderState
{
ID3D11Device* Device;
ID3D11DeviceContext* DeviceContext;
ID3D11SamplerState* SamplerLinear;
ID3D11SamplerState* SamplerNearest;
ID3D11Buffer* VertexConstantBuffer;
};
#endif // #ifndef IMGUI_DISABLE
+1
View File
@@ -39,6 +39,7 @@
#define ROBIN_HOOD_VERSION_PATCH 2 // for backwards-compatible bug fixes #define ROBIN_HOOD_VERSION_PATCH 2 // for backwards-compatible bug fixes
#include <algorithm> #include <algorithm>
#include <cstdint>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <functional> #include <functional>
+6 -5
View File
@@ -38,11 +38,12 @@ std::vector<Analog> &games::bc::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Busou Shinki: Armored Princess Battle Conductor"); analogs = GameAPI::Analogs::getAnalogs("Busou Shinki: Armored Princess Battle Conductor");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Stick X", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Stick Y" { "Stick X", AnalogType::LinearCentered },
); { "Stick Y", AnalogType::LinearCentered }
});
} }
return analogs; return analogs;
+8 -7
View File
@@ -45,13 +45,14 @@ std::vector<Analog> &games::ccj::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Chase Chase Jokers"); analogs = GameAPI::Analogs::getAnalogs("Chase Chase Jokers");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Joystick X", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Joystick Y", { "Joystick X", AnalogType::LinearCentered },
"Trackball DX", { "Joystick Y", AnalogType::LinearCentered },
"Trackball DY" { "Trackball DX", AnalogType::LinearCentered },
); { "Trackball DY", AnalogType::LinearCentered }
});
} }
return analogs; return analogs;
+4 -2
View File
@@ -6,6 +6,7 @@
#include "util/logging.h" #include "util/logging.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "games/io.h" #include "games/io.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
#include "io.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::time_point<std::chrono::steady_clock> lastModified = std::chrono::steady_clock::now();
static std::chrono::milliseconds debounceDuration(100); static std::chrono::milliseconds debounceDuration(100);
auto currentTime = std::chrono::steady_clock::now(); auto currentTime = std::chrono::steady_clock::now();
bool pressed = get_async_secondary_mouse() & 0x8000; bool pressed = get_async_secondary_mouse();
bool focused = GetForegroundWindow() == hWnd; bool focused = GetForegroundWindow() == hWnd;
if (focused && MOUSE_TRACKBALL_USE_TOGGLE && pressed && (currentTime - lastModified > debounceDuration)) { 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); log_info("trackball", "thread start, use mouse: {}, toggle: {}", MOUSE_TRACKBALL, MOUSE_TRACKBALL_USE_TOGGLE);
tbThread = new std::thread([&] { tbThread = new std::thread([&] {
timeutils::PreciseSleepTimer timer;
while (tbThreadRunning) { while (tbThreadRunning) {
if (hWnd && wndProc) { if (hWnd && wndProc) {
wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle); wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle);
@@ -251,7 +253,7 @@ namespace games::ccj {
if (!tbThreadRunning) if (!tbThreadRunning)
break; break;
std::this_thread::sleep_for(10ms); timer.sleep(10);
} }
}); });
} }
+58 -11
View File
@@ -23,6 +23,7 @@
#include "p3io/usbmem.h" #include "p3io/usbmem.h"
#include "p4io/p4io.h" #include "p4io/p4io.h"
#include <cstring>
using namespace acioemu; using namespace acioemu;
@@ -53,9 +54,9 @@ namespace games::ddr {
return SendMessage_real(hWnd, Msg, wParam, lParam); return SendMessage_real(hWnd, Msg, wParam, lParam);
} }
bool contains_only_ascii(const std::string& str) { bool contains_only_ascii(const std::wstring& str) {
for (auto c: str) { for (auto c: str) {
if (static_cast<unsigned char>(c) > 127) { if (c > 127) {
return false; return false;
} }
} }
@@ -80,24 +81,24 @@ namespace games::ddr {
} }
if (fileutils::dir_exists(dir)) { if (fileutils::dir_exists(dir)) {
log_info("ddr", "looking for codecs in this directory: {}", dir.string()); log_info("ddr", "looking for codecs in this directory: {}", dir);
} else { } else {
log_info("ddr", "codecs directory not found: {}", dir.string()); log_info("ddr", "codecs directory not found: {}", dir);
return; return;
} }
for (const auto &file : std::filesystem::directory_iterator(dir)) { for (const auto &file : std::filesystem::directory_iterator(dir)) {
const auto &filename = file.path().filename(); const auto &filename = file.path().filename();
const auto extension = strtolower(filename.extension().string()); const auto extension = filename.extension().wstring();
if (extension != ".dll") { if (wcsicmp(extension.c_str(), L".dll") != 0) {
continue; continue;
} }
log_info("ddr", "found DLL: {}, size: {} bytes", filename.string(), file.file_size()); log_info("ddr", "found DLL: {}, size: {} bytes", filename, file.file_size());
if (filename == "k-clvsd.dll" || filename.string().find("xactengine") == 0) { if (filename == "k-clvsd.dll" || filename.wstring().find(L"xactengine") == 0) {
const std::wstring wcmd = L"regsvr32.exe /s \"" + file.path().wstring() + L"\""; const std::wstring wcmd = L"regsvr32.exe /s \"" + file.path().wstring() + L"\"";
const std::string cmd = "regsvr32.exe /s \"" + file.path().string() + "\""; const std::string cmd = fmt::format("regsvr32.exe /s \"{}\"", file.path());
int result = 0; int result = 0;
std::thread t([wcmd, &result]() { std::thread t([wcmd, &result]() {
@@ -111,12 +112,12 @@ namespace games::ddr {
log_warning("ddr", "`{}` failed, returned {}", cmd, result); log_warning("ddr", "`{}` failed, returned {}", cmd, result);
deferredlogs::defer_error_messages({ deferredlogs::defer_error_messages({
"DDR codec registration failure", "DDR codec registration failure",
fmt::format(" {} failed to register with error {}", filename.string(), result)}); fmt::format(" {} failed to register with error {}", filename, result)});
} }
static std::once_flag printed; static std::once_flag printed;
std::call_once(printed, [file]() { std::call_once(printed, [file]() {
if (!contains_only_ascii(file.path().string())) { if (!contains_only_ascii(file.path().wstring())) {
log_warning( log_warning(
"ddr", "ddr",
"BAD PATH ERROR\n\n" "BAD PATH ERROR\n\n"
@@ -290,4 +291,50 @@ namespace games::ddr {
// dispose device hook // dispose device hook
devicehook_dispose(); devicehook_dispose();
} }
static void get_analog_xy_axis(Analog &analog, bool &less, bool &more) {
if (!analog.isSet()) {
return;
}
const auto value = GameAPI::Analogs::getState(RI_MGR, analog);
// stepmania linux source:
// https://github.com/stepmania/stepmania/blob/d55acb1ba26f1c5b5e3048d6d6c0bd116625216f/src/arch/InputHandler/InputHandler_Linux_Event.cpp#L410
// for example, value cap with range [0, 255]:
// 127 = 0.498, neutral
// 128 = 0.502, both
// apparently some adapters report values like above; this obviously makes a lot of
// assumptions about bit width of the HID value cap and how the adapter reports
// neutral/both values, but this is good enough for parity with stepmania and its many forks
if (0.5001f < value && value < 0.75f) {
less |= true;
more |= true;
} else if (value <= 0.25f) {
less |= true;
} else if (value >= 0.75f) {
more |= true;
}
}
void get_analog_x_axis(int player, bool &left, bool &right) {
if (player != 1 && player != 2) {
log_fatal("ddr", "invalid player number: {}, expected 1 or 2", player);
return;
}
auto &analog = get_analogs().at(player == 1 ? Analogs::P1_LEFT_RIGHT : Analogs::P2_LEFT_RIGHT);
get_analog_xy_axis(analog, left, right);
}
void get_analog_y_axis(int player, bool &up, bool &down) {
if (player != 1 && player != 2) {
log_fatal("ddr", "invalid player number: {}, expected 1 or 2", player);
return;
}
auto &analog = get_analogs().at(player == 1 ? Analogs::P1_UP_DOWN : Analogs::P2_UP_DOWN);
get_analog_xy_axis(analog, up, down);
}
} }
+3
View File
@@ -23,4 +23,7 @@ namespace games::ddr {
private: private:
void register_codecs(); void register_codecs();
}; };
void get_analog_x_axis(int player, bool &left, bool &right);
void get_analog_y_axis(int player, bool &up, bool &down);
} }
+44
View File
@@ -35,6 +35,50 @@ std::vector<Button> &games::ddr::get_buttons() {
return analogs; return analogs;
} }
std::string games::ddr::get_buttons_help() {
// keep to max 100 characters wide
return
"For DDR pad arrows, double check that simultaneous Left+Right and Up+Down can be detected.\n"
"You should boot the game, enter test menu, and use Foot Panel Check.\n\n"
"When mapping arrows, try the following in order:\n\n"
" 1. If your controller uses face buttons (A/B/X/Y), bind them here.\n"
" 2. If your controller supports XInput, use that.\n"
" 3. If your controller uses analog axis for arrows, try Analogs tab.\n"
" 4. Otherwise, you will need to use remapping software or get a better adapter."
;
}
std::vector<Analog> &games::ddr::get_analogs() {
static std::vector<Analog> analogs;
if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Dance Dance Revolution");
using namespace GameAPI::Analogs;
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
{ "P1 Left-Right (Axis Fix)", AnalogType::LinearCentered },
{ "P1 Up-Down (Axis Fix)", AnalogType::LinearCentered },
{ "P2 Left-Right (Axis Fix)", AnalogType::LinearCentered },
{ "P2 Up-Down (Axis Fix)", AnalogType::LinearCentered }
});
}
return analogs;
}
std::string games::ddr::get_analogs_help() {
// keep to max 100 characters wide
return
"Only use this if your DDR pad outputs analog axis for arrows.\n\n"
"If the pad uses face buttons (A/B/X/Y), use Buttons tab instead.\n\n"
"Spice will treat values <=25% as Left, >=75% as Right, ~=50% as neutral,\n"
"and value between 50% and 75% as both arrows.\n\n"
"This is the classic Stepmania \"Axis Fix\" which may or may not work with\n"
"your dance pad or your adapter."
;
}
std::vector<Light> &games::ddr::get_lights() { std::vector<Light> &games::ddr::get_lights() {
static std::vector<Light> lights; static std::vector<Light> lights;
+13
View File
@@ -33,6 +33,16 @@ namespace games::ddr {
}; };
} }
// all analogs in correct order
namespace Analogs {
enum {
P1_LEFT_RIGHT,
P1_UP_DOWN,
P2_LEFT_RIGHT,
P2_UP_DOWN
};
}
// all lights in correct order // all lights in correct order
namespace Lights { namespace Lights {
enum { enum {
@@ -177,5 +187,8 @@ namespace games::ddr {
// getters // getters
std::vector<Button> &get_buttons(); std::vector<Button> &get_buttons();
std::string get_buttons_help();
std::string get_analogs_help();
std::vector<Analog> &get_analogs();
std::vector<Light> &get_lights(); std::vector<Light> &get_lights();
} }
+52 -2
View File
@@ -3,6 +3,7 @@
#include "cfg/api.h" #include "cfg/api.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
#include "../ddr.h" #include "../ddr.h"
@@ -459,7 +460,8 @@ int games::ddr::DDRP3IOHandle::device_io(
if (nOutBufferSize >= 4) { if (nOutBufferSize >= 4) {
// cool down // cool down
Sleep(1); static thread_local timeutils::PreciseSleepTimer timer;
timer.sleep(1);
// get controls as single variable (4 bytes) // get controls as single variable (4 bytes)
auto &controls = *(uint32_t*) lpOutBuffer; auto &controls = *(uint32_t*) lpOutBuffer;
@@ -498,32 +500,80 @@ int games::ddr::DDRP3IOHandle::device_io(
// shift table // shift table
static size_t shift_table[] = { static size_t shift_table[] = {
30, 28, 29, 8, 9, 10, 11, 12, 24, 25, 14, 15, 16, 17, 18, 19, 20, 26, 27, 22, 23 30, 28, 29, // service, test, coin
8, // p1 start
9, 10, 11, 12, // p1 panel
24, 25, 14, 15, // p1 menu
16, // p2 start
17, 18, 19, 20, // p2 panel
26, 27, 22, 23 // p2 menu
}; };
static size_t button_table[] = { static size_t button_table[] = {
Buttons::SERVICE, Buttons::SERVICE,
Buttons::TEST, Buttons::TEST,
Buttons::COIN_MECH, Buttons::COIN_MECH,
Buttons::P1_START, Buttons::P1_START,
Buttons::P1_PANEL_UP, Buttons::P1_PANEL_UP,
Buttons::P1_PANEL_DOWN, Buttons::P1_PANEL_DOWN,
Buttons::P1_PANEL_LEFT, Buttons::P1_PANEL_LEFT,
Buttons::P1_PANEL_RIGHT, Buttons::P1_PANEL_RIGHT,
Buttons::P1_MENU_UP, Buttons::P1_MENU_UP,
Buttons::P1_MENU_DOWN, Buttons::P1_MENU_DOWN,
Buttons::P1_MENU_LEFT, Buttons::P1_MENU_LEFT,
Buttons::P1_MENU_RIGHT, Buttons::P1_MENU_RIGHT,
Buttons::P2_START, Buttons::P2_START,
Buttons::P2_PANEL_UP, Buttons::P2_PANEL_UP,
Buttons::P2_PANEL_DOWN, Buttons::P2_PANEL_DOWN,
Buttons::P2_PANEL_LEFT, Buttons::P2_PANEL_LEFT,
Buttons::P2_PANEL_RIGHT, Buttons::P2_PANEL_RIGHT,
Buttons::P2_MENU_UP, Buttons::P2_MENU_UP,
Buttons::P2_MENU_DOWN, Buttons::P2_MENU_DOWN,
Buttons::P2_MENU_LEFT, Buttons::P2_MENU_LEFT,
Buttons::P2_MENU_RIGHT, Buttons::P2_MENU_RIGHT,
}; };
// get analogs
struct {
bool up;
bool down;
bool left;
bool right;
} analog_values[2] = {};
games::ddr::get_analog_x_axis(1, analog_values[0].left, analog_values[0].right);
games::ddr::get_analog_y_axis(1, analog_values[0].up, analog_values[0].down);
games::ddr::get_analog_x_axis(2, analog_values[1].left, analog_values[1].right);
games::ddr::get_analog_y_axis(2, analog_values[1].up, analog_values[1].down);
if (analog_values[0].up) {
controls |= 1 << 9;
}
if (analog_values[0].down) {
controls |= 1 << 10;
}
if (analog_values[0].left) {
controls |= 1 << 11;
}
if (analog_values[0].right) {
controls |= 1 << 12;
}
if (analog_values[1].up) {
controls |= 1 << 17;
}
if (analog_values[1].down) {
controls |= 1 << 18;
}
if (analog_values[1].left) {
controls |= 1 << 19;
}
if (analog_values[1].right) {
controls |= 1 << 20;
}
// update states // update states
auto &buttons = get_buttons(); auto &buttons = get_buttons();
size_t count = 0; size_t count = 0;
+3 -1
View File
@@ -2,6 +2,7 @@
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
#include "../io.h" #include "../io.h"
@@ -255,7 +256,8 @@ namespace games::ddr {
case P4IO_IOCTL_GET_INPUTS: { 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 // 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); memset(lpOutBuffer, 0, 16);
auto controls = (uint32_t*) lpOutBuffer; auto controls = (uint32_t*) lpOutBuffer;
+3 -1
View File
@@ -7,6 +7,7 @@
#include "games/game.h" #include "games/game.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "util/memutils.h" #include "util/memutils.h"
#include "rgb_cam.h" #include "rgb_cam.h"
@@ -221,10 +222,11 @@ namespace games::drs {
void start_touch() { void start_touch() {
std::thread t([] { std::thread t([] {
log_info("drs", "starting touch input thread"); log_info("drs", "starting touch input thread");
timeutils::PreciseSleepTimer timer;
// main loop // main loop
while (TRUE) { while (TRUE) {
std::this_thread::sleep_for(std::chrono::milliseconds(1)); timer.sleep(1);
TOUCH_EVENTS.clear(); TOUCH_EVENTS.clear();
touch_get_events(TOUCH_EVENTS); touch_get_events(TOUCH_EVENTS);
+8 -7
View File
@@ -35,13 +35,14 @@ std::vector<Analog> &games::ftt::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("FutureTomTom"); analogs = GameAPI::Analogs::getAnalogs("FutureTomTom");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Pad 1", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Pad 2", { "Pad 1", AnalogType::LinearPositive },
"Pad 3", { "Pad 2", AnalogType::LinearPositive },
"Pad 4" { "Pad 3", AnalogType::LinearPositive },
); { "Pad 4", AnalogType::LinearPositive }
});
} }
return analogs; return analogs;
+166
View File
@@ -0,0 +1,166 @@
#include "asio.h"
#include <windows.h>
#include <cstring>
#include "avs/game.h"
#include "gitadora.h"
#include "util/detour.h"
#include "util/logging.h"
namespace games::gitadora {
// Redirects the game's hard-coded "XONAR" ASIO driver lookup to the
// driver name in ASIO_DRIVER by intercepting registry calls to
// HKLM\SOFTWARE\ASIO. Sentinel HKEY values mark the redirected handles
// so we can recognise them on subsequent reg* calls.
static const HKEY PARENT_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4001);
static const HKEY DEVICE_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4002);
static const char *FAKE_ASIO_DEVICE_NAME = "XONAR";
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
static decltype(RegOpenKeyExA) *RegOpenKeyExA_orig = nullptr;
static decltype(RegQueryValueExA) *RegQueryValueExA_orig = nullptr;
static HKEY real_asio_reg_handle = nullptr;
static HKEY real_asio_device_reg_handle = nullptr;
static LONG WINAPI RegOpenKeyExA_hook(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired,
PHKEY phkResult)
{
// ASIO\XONAR redirect to ASIO\<configured>
if (ASIO_DRIVER.has_value() &&
lpSubKey != nullptr &&
phkResult != nullptr &&
hKey == PARENT_ASIO_REG_HANDLE &&
_stricmp(lpSubKey, FAKE_ASIO_DEVICE_NAME) == 0) {
*phkResult = DEVICE_ASIO_REG_HANDLE;
log_info("gitadora::asio", "replacing '{}' with '{}'", lpSubKey, ASIO_DRIVER.value());
const auto result = RegOpenKeyExA_orig(
real_asio_reg_handle,
ASIO_DRIVER.value().c_str(),
ulOptions,
samDesired,
&real_asio_device_reg_handle);
if (result != ERROR_SUCCESS) {
log_warning(
"gitadora::asio",
"failed to open registry subkey '{}', error=0x{:x}",
ASIO_DRIVER.value(), result);
log_warning(
"gitadora::asio",
"due to improper ASIO setting, audio init will fail");
}
return result;
}
// open of the ASIO root: hand back a sentinel
if (ASIO_DRIVER.has_value() &&
lpSubKey != nullptr &&
phkResult != nullptr &&
hKey == HKEY_LOCAL_MACHINE &&
_stricmp(lpSubKey, "software\\asio") == 0)
{
*phkResult = PARENT_ASIO_REG_HANDLE;
return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, &real_asio_reg_handle);
}
return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, phkResult);
}
static LONG WINAPI RegEnumKeyA_hook(HKEY hKey, DWORD dwIndex, LPSTR lpName, DWORD cchName) {
if (hKey == PARENT_ASIO_REG_HANDLE && ASIO_DRIVER.has_value()) {
if (dwIndex == 0) {
// forward to real handle just to verify the key exists; we
// overwrite the name with our fake driver string regardless
auto ret = RegEnumKeyA_orig(real_asio_reg_handle, dwIndex, lpName, cchName);
if (ret == ERROR_SUCCESS && lpName != nullptr && cchName > 0) {
log_info("gitadora::asio", "stubbing '{}' with '{}'", lpName, FAKE_ASIO_DEVICE_NAME);
strncpy(lpName, FAKE_ASIO_DEVICE_NAME, cchName);
lpName[cchName - 1] = '\0';
}
return ret;
} else {
return ERROR_NO_MORE_ITEMS;
}
}
return RegEnumKeyA_orig(hKey, dwIndex, lpName, cchName);
}
static LONG WINAPI RegQueryValueExA_hook(HKEY hKey, LPCSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType,
LPBYTE lpData, LPDWORD lpcbData)
{
HKEY target = hKey;
if (ASIO_DRIVER.has_value() &&
lpValueName != nullptr &&
lpData != nullptr &&
lpcbData != nullptr &&
hKey == DEVICE_ASIO_REG_HANDLE) {
if (_stricmp(lpValueName, "Description") == 0) {
// engine may verify the driver name after open; ensure it still
// sees something containing "XONAR" so the substring check passes
const size_t len = strlen(FAKE_ASIO_DEVICE_NAME) + 1;
if (*lpcbData < len) {
*lpcbData = static_cast<DWORD>(len);
return ERROR_MORE_DATA;
}
memcpy(lpData, FAKE_ASIO_DEVICE_NAME, len);
*lpcbData = static_cast<DWORD>(len);
if (lpType != nullptr) {
*lpType = REG_SZ;
}
return ERROR_SUCCESS;
}
// for everything else (CLSID etc.) defer to the real driver subkey
target = real_asio_device_reg_handle;
}
return RegQueryValueExA_orig(target, lpValueName, lpReserved, lpType, lpData, lpcbData);
}
static LONG WINAPI RegCloseKey_hook(HKEY hKey) {
if (hKey == PARENT_ASIO_REG_HANDLE) {
if (real_asio_reg_handle != nullptr) {
RegCloseKey_orig(real_asio_reg_handle);
real_asio_reg_handle = nullptr;
}
return ERROR_SUCCESS;
}
if (hKey == DEVICE_ASIO_REG_HANDLE) {
if (real_asio_device_reg_handle != nullptr) {
RegCloseKey_orig(real_asio_device_reg_handle);
real_asio_device_reg_handle = nullptr;
}
return ERROR_SUCCESS;
}
return RegCloseKey_orig(hKey);
}
void asio_hook_init() {
if (!ASIO_DRIVER.has_value()) {
return;
}
log_info("gitadora::asio", "installing ASIO driver redirect: XONAR -> {}", ASIO_DRIVER.value());
RegCloseKey_orig = detour::iat_try(
"RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE);
RegEnumKeyA_orig = detour::iat_try(
"RegEnumKeyA", RegEnumKeyA_hook, avs::game::DLL_INSTANCE);
RegOpenKeyExA_orig = detour::iat_try(
"RegOpenKeyExA", RegOpenKeyExA_hook, avs::game::DLL_INSTANCE);
RegQueryValueExA_orig = detour::iat_try(
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
}
}
+12
View File
@@ -0,0 +1,12 @@
#pragma once
namespace games::gitadora {
// installs IAT registry hooks in gfdm.dll that redirect the game's
// ASIO driver lookup (hard-coded "XONAR" substring) to a user-chosen
// driver name read from games::gitadora::ASIO_DRIVER.
//
// safe to call unconditionally; if ASIO_DRIVER is unset the hooks
// forward every call straight through to advapi32.
void asio_hook_init();
}
+384 -27
View File
@@ -1,18 +1,26 @@
#include "gitadora.h" #include "gitadora.h"
#include "asio.h"
#include "handle.h" #include "handle.h"
#include "bi2x_hook.h" #include "bi2x_hook.h"
#include <unordered_map> #include <unordered_map>
#include <ks.h>
#include <ksmedia.h>
#include "cfg/configurator.h" #include "cfg/configurator.h"
#include "hooks/audio/mme.h" #include "hooks/audio/mme.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "misc/wintouchemu.h" #include "misc/wintouchemu.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "util/cpuutils.h" #include "util/cpuutils.h"
#include "util/deferlog.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/libutils.h" #include "util/libutils.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/sigscan.h" #include "util/sigscan.h"
#include "util/socd_cleaner.h" #include "util/socd_cleaner.h"
#include "util/sysutils.h"
#include "util/utils.h"
#include "hooks/setupapihook.h" #include "hooks/setupapihook.h"
namespace games::gitadora { namespace games::gitadora {
@@ -20,11 +28,12 @@ namespace games::gitadora {
// settings // settings
bool TWOCHANNEL = false; bool TWOCHANNEL = false;
std::optional<unsigned int> CAB_TYPE = std::nullopt; std::optional<unsigned int> CAB_TYPE = std::nullopt;
bool ARENA_SINGLE_WINDOW = false;
bool P1_LEFTY = false; bool P1_LEFTY = false;
bool P2_LEFTY = false; bool P2_LEFTY = false;
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE; std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent; std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
std::optional<std::string> ASIO_DRIVER = std::nullopt;
/* /*
* Prevent GitaDora from creating folders on F drive * Prevent GitaDora from creating folders on F drive
@@ -221,13 +230,6 @@ namespace games::gitadora {
} }
#endif #endif
// arena model launches a tiny window yet backbuffer at 4k, resulting in unusable overlay
// force scaling to make things usable
if (!overlay::UI_SCALE_PERCENT.has_value() && is_arena_model() &&
GRAPHICS_WINDOWED && !cfg::CONFIGURATOR_STANDALONE) {
overlay::UI_SCALE_PERCENT = 250;
}
// for guitar wail SOCD cleaning // for guitar wail SOCD cleaning
socd::ALGORITHM = socd::SocdAlgorithm::PreferRecent; socd::ALGORITHM = socd::SocdAlgorithm::PreferRecent;
@@ -237,12 +239,334 @@ namespace games::gitadora {
} else { } else {
log_info("gitadora", "guitar pick SOCD algorithm: legacy"); log_info("gitadora", "guitar pick SOCD algorithm: legacy");
} }
#if SPICE64 && !SPICE_XP
if (is_arena_model()) {
// in full screen, if single-adapter option is checked, it's functionally
// the same as forcing a single monitor
if (!GRAPHICS_WINDOWED && GRAPHICS_FORCE_SINGLE_ADAPTER) {
ARENA_WINDOW_COUNT = 1;
}
// figure out default settings if user didn't provide one
if (!ARENA_WINDOW_COUNT.has_value()) {
if (!GRAPHICS_WINDOWED && sysutils::enumerate_monitors().size() < 4) {
log_info("gitadora", "arena model: <4 monitors, defaulting to single window mode");
ARENA_WINDOW_COUNT = 1;
} else {
ARENA_WINDOW_COUNT = 4;
}
}
const int count = ARENA_WINDOW_COUNT.value();
switch (count) {
case 1:
log_info("gitadora", "arena model: single-window mode");
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
GRAPHICS_PREVENT_SECONDARY_WINDOWS = true;
break;
case 2:
if (!GRAPHICS_WINDOWED) {
log_fatal(
"gitadora",
"arena model: 2-window mode is not supported in fullscreen, choose 1 or 4");
}
log_info("gitadora", "arena model: two-window mode");
GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS = true;
break;
case 4:
log_info("gitadora", "arena model: four-window mode");
break;
default:
log_fatal(
"gitadora",
"arena model: unsupported window count: {}", count);
}
}
#endif
} }
} }
#if SPICE64 && !SPICE_XP
static decltype(GetDisplayConfigBufferSizes) *GetDisplayConfigBufferSizes_orig = nullptr;
static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr;
static decltype(DisplayConfigGetDeviceInfo) *DisplayConfigGetDeviceInfo_orig = nullptr;
// cached primary real monitor: its path + source/target mode entries.
// modeInfoIdx values on the path are renumbered to 0 / 1 so the cache is self-contained.
static DISPLAYCONFIG_PATH_INFO real_primary_path = {};
static DISPLAYCONFIG_MODE_INFO real_primary_modes[2] = {}; // [0]=source, [1]=target
// fake monitors appended after the real ones. the game classifies monitors
// by outputTechnology + connectorInstance:
// HDMI -> main 4k monitor (real primary)
// DP connInstance 0 -> left
// DP connInstance 1 -> right
// DP connInstance 2 -> small (sub/touch)
// ids are negated on the fake monitor headers so they can be distinguished
// from real ones in DisplayConfigGetDeviceInfo.
struct FakeMonitor {
LONG id;
int width;
int height;
int offset_x;
int offset_y;
UINT32 connector_instance;
};
// ORDERING MATTERS: the d3d9 wrapper (FAKE_SUBSCREEN_ADAPTER) hands out
// adapters as "\\.\DISPLAY_SPICE_FAKE_{N}" for N=1,2,3. The game maps each
// adapter to a swap chain role via its DisplayConfig connector instance,
// so the entries here must be listed in the same order the wrapper enumerates
// them: id=1 -> adapter 1 -> left, id=2 -> adapter 2 -> right, id=3 -> adapter 3 -> small.
static constexpr FakeMonitor FAKE_MONITORS[] = {
{ 1, 1080, 1920, -100000, -100000, 0 }, // left (DP connector instance 0)
{ 2, 1080, 1920, -200000, -200000, 1 }, // right (DP connector instance 1)
{ 3, 800, 1280, -300000, -300000, 2 }, // small (DP connector instance 2, touch)
};
static constexpr UINT32 FAKE_MONITOR_COUNT = static_cast<UINT32>(std::size(FAKE_MONITORS));
// call QueryDisplayConfig once, keep only the primary monitor's path and its
// two referenced modes (source + target). modeInfoIdx values are rewritten to
// 0 and 1 so the cache is self-consistent.
static void cache_primary_monitor_info() {
UINT32 path_count = 0;
UINT32 mode_count = 0;
if (GetDisplayConfigBufferSizes_orig(
QDC_ONLY_ACTIVE_PATHS, &path_count, &mode_count) != ERROR_SUCCESS) {
log_fatal("gitadora", "cache_primary_monitor_info: GetDisplayConfigBufferSizes failed");
}
std::vector<DISPLAYCONFIG_PATH_INFO> all_paths(path_count);
std::vector<DISPLAYCONFIG_MODE_INFO> all_modes(mode_count);
if (QueryDisplayConfig_orig(
QDC_ONLY_ACTIVE_PATHS,
&path_count, all_paths.data(),
&mode_count, all_modes.data(),
nullptr) != ERROR_SUCCESS) {
log_fatal("gitadora", "cache_primary_monitor_info: QueryDisplayConfig failed");
}
all_paths.resize(path_count);
all_modes.resize(mode_count);
// pick the primary monitor: source mode at (0, 0)
auto primary = std::find_if(all_paths.begin(), all_paths.end(),
[&](const auto &p) {
const auto idx = p.sourceInfo.modeInfoIdx;
return idx < all_modes.size() &&
all_modes[idx].infoType == DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE &&
all_modes[idx].sourceMode.position.x == 0 &&
all_modes[idx].sourceMode.position.y == 0;
});
if (primary == all_paths.end()) {
log_fatal("gitadora", "cache_primary_monitor_info: no primary monitor found");
}
real_primary_modes[0] = all_modes[primary->sourceInfo.modeInfoIdx];
real_primary_modes[1] = all_modes[primary->targetInfo.modeInfoIdx];
real_primary_path = *primary;
real_primary_path.sourceInfo.modeInfoIdx = 0;
real_primary_path.targetInfo.modeInfoIdx = 1;
log_info("gitadora", "cache_primary_monitor_info: cached primary monitor");
}
static
LONG
WINAPI
GetDisplayConfigBufferSizes_hook(
UINT32 Flags,
UINT32 *pNumPathArrayElements,
UINT32 *pNumModeInfoArrayElements)
{
// populate cached primary real monitor on the first call
static std::once_flag populate_once;
std::call_once(populate_once, cache_primary_monitor_info);
// always report exactly 1 real + N fake monitors
*pNumPathArrayElements = 1 + FAKE_MONITOR_COUNT;
*pNumModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
log_info(
"gitadora",
"GetDisplayConfigBufferSizes: 1 real path + {} fake monitor(s)",
FAKE_MONITOR_COUNT);
return ERROR_SUCCESS;
}
// write fake monitor i into the caller's path/mode arrays. layout (single-monitor
// assumption): index 0 in both arrays holds the cached primary real monitor, so
// fake i occupies path slot (1 + i) and mode slots (2 + i*2) / (2 + i*2 + 1).
static void insert_fake_monitor(
DISPLAYCONFIG_PATH_INFO *paths,
DISPLAYCONFIG_MODE_INFO *modes,
UINT32 i)
{
const FakeMonitor &m = FAKE_MONITORS[i];
const UINT32 src_idx = 2 + i * 2;
const UINT32 tgt_idx = src_idx + 1;
const LUID adapter_id { .LowPart = static_cast<DWORD>(-m.id), .HighPart = -m.id };
const UINT32 uid = static_cast<UINT32>(-m.id);
paths[1 + i] = {
.sourceInfo = {
.adapterId = adapter_id,
.id = uid,
.modeInfoIdx = src_idx,
.statusFlags = DISPLAYCONFIG_SOURCE_IN_USE,
},
.targetInfo = {
.adapterId = adapter_id,
.id = uid,
.modeInfoIdx = tgt_idx,
.outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL,
.rotation = DISPLAYCONFIG_ROTATION_IDENTITY,
.scaling = DISPLAYCONFIG_SCALING_IDENTITY,
.refreshRate = { .Numerator = 60000, .Denominator = 1000 },
.scanLineOrdering = DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE,
.targetAvailable = TRUE,
.statusFlags = DISPLAYCONFIG_TARGET_IN_USE,
},
.flags = DISPLAYCONFIG_PATH_ACTIVE,
};
modes[src_idx] = {
.infoType = DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE,
.id = uid,
.adapterId = adapter_id,
.sourceMode = {
.width = static_cast<UINT32>(m.width),
.height = static_cast<UINT32>(m.height),
.pixelFormat = DISPLAYCONFIG_PIXELFORMAT_32BPP,
.position = { .x = m.offset_x, .y = m.offset_y },
},
};
modes[tgt_idx] = {
.infoType = DISPLAYCONFIG_MODE_INFO_TYPE_TARGET,
.id = uid,
.adapterId = adapter_id,
.targetMode = {},
};
log_misc(
"gitadora",
"inserted fake monitor: id={}, width={}, height={}, offset_x={}, offset_y={}",
m.id, m.width, m.height, m.offset_x, m.offset_y);
}
static
LONG
WINAPI
QueryDisplayConfig_hook(
UINT32 flags,
UINT32* numPathArrayElements,
DISPLAYCONFIG_PATH_INFO* pathArray,
UINT32* numModeInfoArrayElements,
DISPLAYCONFIG_MODE_INFO* modeInfoArray,
DISPLAYCONFIG_TOPOLOGY_ID* currentTopologyId)
{
// copy cached primary real monitor into caller buffers at index 0
pathArray[0] = real_primary_path;
modeInfoArray[0] = real_primary_modes[0];
modeInfoArray[1] = real_primary_modes[1];
*numPathArrayElements = 1 + FAKE_MONITOR_COUNT;
*numModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
if (currentTopologyId != nullptr) {
*currentTopologyId = DISPLAYCONFIG_TOPOLOGY_EXTEND;
}
log_misc("gitadora", "QueryDisplayConfig returning fake monitor paths and modes");
// append fake monitors after the real one
for (UINT32 i = 0; i < FAKE_MONITOR_COUNT; i++) {
insert_fake_monitor(pathArray, modeInfoArray, i);
}
return ERROR_SUCCESS;
}
static
LONG
WINAPI
DisplayConfigGetDeviceInfo_hook(DISPLAYCONFIG_DEVICE_INFO_HEADER* requestPacket)
{
if (requestPacket == nullptr) {
return DisplayConfigGetDeviceInfo_orig(requestPacket);
}
// handle fake monitors (negative id) directly without calling orig
const auto id = static_cast<int>(requestPacket->id);
if (id < 0) {
if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME) {
const auto sourceName = reinterpret_cast<DISPLAYCONFIG_SOURCE_DEVICE_NAME*>(requestPacket);
// name must match WrappedIDirect3D9::GetAdapterIdentifier
const std::string adapter_name = fmt::format("\\\\.\\DISPLAY_SPICE_FAKE_{}", -id);
wcscpy(sourceName->viewGdiDeviceName, s2ws(adapter_name).c_str());
log_misc("gitadora",
"DisplayConfigGetDeviceInfo: fake source id={} name={}", id, adapter_name);
return ERROR_SUCCESS;
}
if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME) {
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
const LONG fake_id = -id;
UINT32 conn_inst = 0xff;
for (const auto& f : FAKE_MONITORS) {
if (f.id == fake_id) {
conn_inst = f.connector_instance;
break;
}
}
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL;
targetName->connectorInstance = conn_inst;
wcscpy(targetName->monitorFriendlyDeviceName, L"Spice Fake Monitor");
wcscpy(targetName->monitorDevicePath, L"\\\\?\\SpiceFakeMonitor");
log_misc("gitadora",
"DisplayConfigGetDeviceInfo: fake target id={} -> DP connInst {}",
id, targetName->connectorInstance);
return ERROR_SUCCESS;
}
}
const auto ret = DisplayConfigGetDeviceInfo_orig(requestPacket);
if (ret != ERROR_SUCCESS ||
requestPacket->type != DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME) {
return ret;
}
// override the cached primary real monitor target info to look like HDMI/0
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
const auto& target = real_primary_path.targetInfo;
if (target.id == targetName->header.id &&
target.adapterId.HighPart == targetName->header.adapterId.HighPart &&
target.adapterId.LowPart == targetName->header.adapterId.LowPart)
{
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI;
targetName->connectorInstance = 0;
log_info("gitadora",
"overriding primary monitor (id={}) to pretend to be HDMI",
targetName->header.id);
}
return ret;
}
#endif
void GitaDoraGame::attach() { void GitaDoraGame::attach() {
Game::attach(); Game::attach();
// arena model launches a tiny window yet backbuffer at 4k, resulting in unusable overlay
// force scaling to make things usable
if (!overlay::UI_SCALE_PERCENT.has_value() && is_arena_model()) {
log_info("gitadora", "forcing UI scale to 250% for arena model");
overlay::UI_SCALE_PERCENT = 250;
}
// modules // modules
HMODULE sharepj_module = libutils::try_module("libshare-pj.dll"); HMODULE sharepj_module = libutils::try_module("libshare-pj.dll");
HMODULE bmsd2_module = libutils::try_module("libbmsd2.dll"); HMODULE bmsd2_module = libutils::try_module("libbmsd2.dll");
@@ -294,39 +618,58 @@ namespace games::gitadora {
detour::iat_try("GetDriveTypeA", GetDriveTypeA_hook, avs::game::DLL_INSTANCE); detour::iat_try("GetDriveTypeA", GetDriveTypeA_hook, avs::game::DLL_INSTANCE);
detour::iat_try("CreateDirectoryA", CreateDirectoryA_hook, avs::game::DLL_INSTANCE); detour::iat_try("CreateDirectoryA", CreateDirectoryA_hook, avs::game::DLL_INSTANCE);
// ASIO driver redirect (XONAR -> user-configured driver)
asio_hook_init();
// volume change prevention // volume change prevention
hooks::audio::mme::init(avs::game::DLL_INSTANCE); hooks::audio::mme::init(avs::game::DLL_INSTANCE);
// touch hook // monitor/touch hooks (windowed or full screen)
if (ARENA_SINGLE_WINDOW) { if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
// enable touch hook for subscreen overlay
wintouchemu::FORCE = true; wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE); wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
}
return;
}
HMODULE gdme_module = libutils::try_module("libgdme.dll"); #if !SPICE_XP
if (!GRAPHICS_WINDOWED) {
// monitor hook: always pretend to have 1 primary real monitor + 3 fake monitors
// (LEFT / RIGHT / SMALL) so the game accepts the arena cab topology
GetDisplayConfigBufferSizes_orig =
detour::iat_try("GetDisplayConfigBufferSizes",
GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE);
QueryDisplayConfig_orig =
detour::iat_try("QueryDisplayConfig",
QueryDisplayConfig_hook, avs::game::DLL_INSTANCE);
DisplayConfigGetDeviceInfo_orig =
detour::iat_try("DisplayConfigGetDeviceInfo",
DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE);
}
#endif
}
}
// window patch // window patch
if (GRAPHICS_WINDOWED && !replace_pattern( if (!is_arena_model()) {
gdme_module, HMODULE gdme_module = libutils::try_module("libgdme.dll");
"754185ED753D8B4118BF0000CB02", if (GRAPHICS_WINDOWED && !replace_pattern(
"9090????9090??????????????12", 0, 0)) gdme_module,
{ "754185ED753D8B4118BF0000CB02",
log_warning("gitadora", "windowed mode failed"); "9090????9090??????????????12", 0, 0)) {
log_warning("gitadora", "windowed mode failed");
}
} }
HMODULE bmsd_engine_module = libutils::try_module("libbmsd-engine.dll");
HMODULE bmsd_module = libutils::try_module("libbmsd.dll");
// two channel mod // two channel mod
if (TWOCHANNEL) { if (TWOCHANNEL && !is_arena_model()) {
bmsd2_boot_orig = detour::iat_try("bmsd2_boot", bmsd2_boot_hook, bmsd_module); HMODULE bmsd_engine_module = libutils::try_module("libbmsd-engine.dll");
HMODULE bmsd_module = libutils::try_module("libbmsd.dll");
bmsd2_boot_orig = detour::iat_try("bmsd2_boot", bmsd2_boot_hook, bmsd_module);
if (!(replace_pattern(bmsd_engine_module, "33000000488D", "03??????????", 0, 0) || if (!(replace_pattern(bmsd_engine_module, "33000000488D", "03??????????", 0, 0) ||
replace_pattern(bmsd_engine_module, "330000000F10", "03??????????", 0, 0))) replace_pattern(bmsd_engine_module, "330000000F10", "03??????????", 0, 0))) {
{
log_warning("gitadora", "two channel mode failed"); log_warning("gitadora", "two channel mode failed");
} }
} }
@@ -334,4 +677,18 @@ namespace games::gitadora {
#endif #endif
} }
void fix_audio_channel_mask(WAVEFORMATEX *format) {
if (!format || format->wFormatTag != WAVE_FORMAT_EXTENSIBLE) {
return;
}
auto ext = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(format);
// fix the legacy 7.1 channel mask to the modern surround layout
// makes it more compatible with modern audio cards
if (ext->dwChannelMask == KSAUDIO_SPEAKER_7POINT1) {
ext->dwChannelMask = KSAUDIO_SPEAKER_7POINT1_SURROUND;
}
}
} }
+7 -1
View File
@@ -2,6 +2,9 @@
#include <optional> #include <optional>
#include <windows.h>
#include <mmreg.h>
#include "avs/game.h" #include "avs/game.h"
#include "games/game.h" #include "games/game.h"
#include "util/socd_cleaner.h" #include "util/socd_cleaner.h"
@@ -11,11 +14,12 @@ namespace games::gitadora {
// settings // settings
extern bool TWOCHANNEL; extern bool TWOCHANNEL;
extern std::optional<unsigned int> CAB_TYPE; extern std::optional<unsigned int> CAB_TYPE;
extern bool ARENA_SINGLE_WINDOW;
extern bool P1_LEFTY; extern bool P1_LEFTY;
extern bool P2_LEFTY; extern bool P2_LEFTY;
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE; extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
extern std::optional<socd::SocdAlgorithm> PICK_ALGO; extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
extern std::optional<std::string> ASIO_DRIVER;
class GitaDoraGame : public games::Game { class GitaDoraGame : public games::Game {
public: public:
@@ -24,6 +28,8 @@ namespace games::gitadora {
virtual void attach() override; virtual void attach() override;
}; };
void fix_audio_channel_mask(WAVEFORMATEX *format);
static inline bool is_drum() { static inline bool is_drum() {
return ( return (
avs::game::is_model({ "J32", "K32", "L32" }) || avs::game::is_model({ "J32", "K32", "L32" }) ||
+12 -10
View File
@@ -86,19 +86,21 @@ std::vector<Button> &games::gitadora::get_buttons() {
std::vector<Analog> &games::gitadora::get_analogs() { std::vector<Analog> &games::gitadora::get_analogs() {
static std::vector<Analog> analogs; static std::vector<Analog> analogs;
using namespace GameAPI::Analogs;
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("GitaDora"); analogs = GameAPI::Analogs::getAnalogs("GitaDora");
GameAPI::Analogs::sortAnalogs(&analogs, GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Guitar P1 Wail X", {"Guitar P1 Wail X", AnalogType::LinearCentered},
"Guitar P1 Wail Y", {"Guitar P1 Wail Y", AnalogType::LinearCentered},
"Guitar P1 Wail Z", {"Guitar P1 Wail Z", AnalogType::LinearCentered},
"Guitar P1 Knob", {"Guitar P1 Knob", AnalogType::Circular},
"Guitar P2 Wail X", {"Guitar P2 Wail X", AnalogType::LinearCentered},
"Guitar P2 Wail Y", {"Guitar P2 Wail Y", AnalogType::LinearCentered},
"Guitar P2 Wail Z", {"Guitar P2 Wail Z", AnalogType::LinearCentered},
"Guitar P2 Knob" {"Guitar P2 Knob", AnalogType::Circular}
); });
} }
return analogs; return analogs;
+3 -1
View File
@@ -2,6 +2,7 @@
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
#include "iidx.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 - otherwise the IO thread will go too hard on the CPU
Sleep(1); static thread_local timeutils::PreciseSleepTimer timer;
timer.sleep(1);
// generate message // generate message
auto msg = this->create_msg(msg_in, 0x2E); auto msg = this->create_msg(msg_in, 0x2E);
+3 -1
View File
@@ -2,6 +2,7 @@
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
#include "iidx.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 - otherwise the IO thread will go too hard on the CPU
Sleep(1); static thread_local timeutils::PreciseSleepTimer timer;
timer.sleep(1);
// generate message // generate message
auto msg = this->create_msg(msg_in, 0x2E); auto msg = this->create_msg(msg_in, 0x2E);
+3 -1
View File
@@ -67,6 +67,8 @@ namespace games::iidx {
std::optional<std::string> SOUND_OUTPUT_DEVICE = std::nullopt; std::optional<std::string> SOUND_OUTPUT_DEVICE = std::nullopt;
std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt; std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt;
std::optional<std::string> ASIO_DRIVER = std::nullopt; std::optional<std::string> ASIO_DRIVER = std::nullopt;
uint32_t TT_DELAY_P1 = 0;
uint32_t TT_DELAY_P2 = 0;
uint8_t DIGITAL_TT_SENS = 4; uint8_t DIGITAL_TT_SENS = 4;
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE = std::nullopt; std::optional<std::string> SUBSCREEN_OVERLAY_SIZE = std::nullopt;
std::optional<std::string> SCREEN_MODE = std::nullopt; std::optional<std::string> SCREEN_MODE = std::nullopt;
@@ -781,7 +783,7 @@ namespace games::iidx {
} }
// return higher 8 bit // return higher 8 bit
return (uint8_t) (ret_value >> 2); return (uint8_t)(ret_value >> 2);
} }
unsigned char get_slider(uint8_t slider) { unsigned char get_slider(uint8_t slider) {
+11 -10
View File
@@ -63,16 +63,17 @@ std::vector<Analog> &games::iidx::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Beatmania IIDX"); analogs = GameAPI::Analogs::getAnalogs("Beatmania IIDX");
GameAPI::Analogs::sortAnalogs( using namespace GameAPI::Analogs;
&analogs,
"Turntable P1", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Turntable P2", { "Turntable P1", AnalogType::Circular },
"VEFX", { "Turntable P2", AnalogType::Circular },
"Low-EQ", { "VEFX", AnalogType::LinearPositive },
"Hi-EQ", { "Low-EQ", AnalogType::LinearPositive },
"Filter", { "Hi-EQ", AnalogType::LinearPositive },
"Play Volume" { "Filter", AnalogType::LinearPositive },
); { "Play Volume", AnalogType::LinearPositive }
});
} }
return analogs; return analogs;
} }
+3 -1
View File
@@ -3,6 +3,7 @@
#if SPICE64 && !SPICE_XP #if SPICE64 && !SPICE_XP
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h" #include "util/utils.h"
#include "mf_wrappers.h" #include "mf_wrappers.h"
@@ -399,6 +400,7 @@ namespace games::iidx {
void IIDXLocalCamera::CreateThread() { void IIDXLocalCamera::CreateThread() {
// Create thread // Create thread
m_drawThread = new std::thread([this]() { m_drawThread = new std::thread([this]() {
timeutils::PreciseSleepTimer timer;
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL); SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
double accumulator = 0.0; double accumulator = 0.0;
@@ -412,7 +414,7 @@ namespace games::iidx {
accumulator -= 1.0; accumulator -= 1.0;
floorFrameTimeMicroSec += 1; 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 "touch/touch.h"
#include "util/libutils.h" #include "util/libutils.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#define POKE_NATIVE_TOUCH 0 #define POKE_NATIVE_TOUCH 0
@@ -200,6 +201,7 @@ namespace games::iidx::poke {
// create new thread // create new thread
THREAD_RUNNING = true; THREAD_RUNNING = true;
THREAD = new std::thread([] { THREAD = new std::thread([] {
timeutils::PreciseSleepTimer timer;
// log // log
log_info("poke", "enabled"); log_info("poke", "enabled");
@@ -319,7 +321,7 @@ namespace games::iidx::poke {
} }
// slow down // slow down
Sleep(50); timer.sleep(50);
} }
return nullptr; return nullptr;
+3
View File
@@ -92,6 +92,9 @@ namespace games {
games.push_back(ddr); games.push_back(ddr);
buttons.insert({ ddr, ddr::get_buttons() }); buttons.insert({ ddr, ddr::get_buttons() });
lights.insert({ ddr, ddr::get_lights() }); lights.insert({ ddr, ddr::get_lights() });
buttons_help.insert({ ddr, ddr::get_buttons_help() });
analogs_help.insert({ ddr, ddr::get_analogs_help() });
analogs.insert({ ddr, ddr::get_analogs() });
file_hints[ddr].push_back({"ddr.dll"}); file_hints[ddr].push_back({"ddr.dll"});
file_hints[ddr].push_back({"mdxja_945.dll"}); file_hints[ddr].push_back({"mdxja_945.dll"});
file_hints[ddr].push_back({"arkmdxp3.dll"}); file_hints[ddr].push_back({"arkmdxp3.dll"});
+86 -13
View File
@@ -19,7 +19,7 @@
namespace games::jb { namespace games::jb {
// touch stuff // touch stuff
bool TOUCH_LEGACY_BOX = false; JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
static bool TOUCH_ENABLE = false; static bool TOUCH_ENABLE = false;
static bool TOUCH_ATTACHED = false; static bool TOUCH_ATTACHED = false;
static bool IS_PORTRAIT = true; 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. // note that the IO code in device.cpp will correctly compensate for orientation, depending on the model.
TOUCH_POINTS.clear(); TOUCH_POINTS.clear();
touch_get_points(TOUCH_POINTS); touch_get_points(TOUCH_POINTS);
if (TOUCH_LEGACY_BOX) { if (TOUCH_ALGORITHM == Legacy) {
auto offset = IS_PORTRAIT ? 580 : 0; auto offset = IS_PORTRAIT ? 580 : 0;
for (auto &tp : TOUCH_POINTS) { for (auto &tp : TOUCH_POINTS) {
@@ -94,6 +94,17 @@ namespace games::jb {
} }
} else { } else {
for (auto &tp : TOUCH_POINTS) { 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 x_relative = tp.x;
int y_relative = tp.y; int y_relative = tp.y;
@@ -110,19 +121,66 @@ namespace games::jb {
y_relative -= 8; 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; 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 // check if the gap was touched
if (x_hitbox > JB_BUTTON_SIZE || y_hitbox > JB_BUTTON_SIZE) { if (TOUCH_ALGORITHM == AcAccurate) {
continue; // 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 // set the corresponding state
@@ -167,7 +225,7 @@ namespace games::jb {
void JBGame::pre_attach() { void JBGame::pre_attach() {
if (!cfg::CONFIGURATOR_STANDALONE) { if (!cfg::CONFIGURATOR_STANDALONE) {
const auto current_path = std::filesystem::current_path(); const auto current_path = std::filesystem::current_path();
log_misc("jubeat", "current working directory: {}", current_path.string()); log_misc("jubeat", "current working directory: {}", current_path);
if (current_path.parent_path() == current_path.root_path()) { if (current_path.parent_path() == current_path.root_path()) {
log_warning( log_warning(
"jubeat", "jubeat",
@@ -178,7 +236,7 @@ namespace games::jb {
" c:\\jubeat\\contents\\spice.exe <- OK\n\n" " c:\\jubeat\\contents\\spice.exe <- OK\n\n"
"To fix this, create a new directory and move ALL game files there.\n\n" "To fix this, create a new directory and move ALL game files there.\n\n"
"Your current working directory: {}\n", "Your current working directory: {}\n",
current_path.string()); current_path);
log_fatal( log_fatal(
"jubeat", "jubeat",
@@ -193,6 +251,21 @@ namespace games::jb {
// enable touch // enable touch
TOUCH_ENABLE = true; TOUCH_ENABLE = true;
switch (TOUCH_ALGORITHM) {
case Legacy:
log_info("jubeat", "using 'legacy' touch targets");
break;
case Improved:
log_info("jubeat", "using 'improved' touch targets");
break;
case AcAccurate:
log_info("jubeat", "using 'ac accurate' touch targets");
break;
default:
log_fatal("jubeat", "unknown touch algo detected in touch_update, this is a bug");
break;
}
// enable debug logging of gftools // enable debug logging of gftools
HMODULE gftools = libutils::try_module("gftools.dll"); HMODULE gftools = libutils::try_module("gftools.dll");
detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc( detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc(
+7 -1
View File
@@ -4,8 +4,14 @@
namespace games::jb { namespace games::jb {
enum JubeatTouchAlgorithm {
Legacy,
Improved,
AcAccurate
};
// touch stuff // touch stuff
extern bool TOUCH_LEGACY_BOX; extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
extern bool TOUCH_STATE[16]; extern bool TOUCH_STATE[16];
void touch_update(); void touch_update();
+5 -5
View File
@@ -36,11 +36,11 @@ std::vector<Analog> &games::mga::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Metal Gear"); analogs = GameAPI::Analogs::getAnalogs("Metal Gear");
GameAPI::Analogs::sortAnalogs( using namespace GameAPI::Analogs;
&analogs, GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Joy X", { "Joy X", AnalogType::LinearCentered },
"Joy Y" { "Joy Y", AnalogType::LinearCentered }
); });
} }
return analogs; return analogs;
+32 -31
View File
@@ -165,37 +165,38 @@ std::vector<Analog> &games::nost::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Nostalgia"); analogs = GameAPI::Analogs::getAnalogs("Nostalgia");
GameAPI::Analogs::sortAnalogs( using namespace GameAPI::Analogs;
&analogs,
"Key 1", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Key 2", { "Key 1", AnalogType::LinearPositive },
"Key 3", { "Key 2", AnalogType::LinearPositive },
"Key 4", { "Key 3", AnalogType::LinearPositive },
"Key 5", { "Key 4", AnalogType::LinearPositive },
"Key 6", { "Key 5", AnalogType::LinearPositive },
"Key 7", { "Key 6", AnalogType::LinearPositive },
"Key 8", { "Key 7", AnalogType::LinearPositive },
"Key 9", { "Key 8", AnalogType::LinearPositive },
"Key 10", { "Key 9", AnalogType::LinearPositive },
"Key 11", { "Key 10", AnalogType::LinearPositive },
"Key 12", { "Key 11", AnalogType::LinearPositive },
"Key 13", { "Key 12", AnalogType::LinearPositive },
"Key 14", { "Key 13", AnalogType::LinearPositive },
"Key 15", { "Key 14", AnalogType::LinearPositive },
"Key 16", { "Key 15", AnalogType::LinearPositive },
"Key 17", { "Key 16", AnalogType::LinearPositive },
"Key 18", { "Key 17", AnalogType::LinearPositive },
"Key 19", { "Key 18", AnalogType::LinearPositive },
"Key 20", { "Key 19", AnalogType::LinearPositive },
"Key 21", { "Key 20", AnalogType::LinearPositive },
"Key 22", { "Key 21", AnalogType::LinearPositive },
"Key 23", { "Key 22", AnalogType::LinearPositive },
"Key 24", { "Key 23", AnalogType::LinearPositive },
"Key 25", { "Key 24", AnalogType::LinearPositive },
"Key 26", { "Key 25", AnalogType::LinearPositive },
"Key 27", { "Key 26", AnalogType::LinearPositive },
"Key 28" { "Key 27", AnalogType::LinearPositive },
); { "Key 28", AnalogType::LinearPositive }
});
} }
return analogs; return analogs;
} }
+3 -1
View File
@@ -9,6 +9,7 @@
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
namespace games::nost::poke { namespace games::nost::poke {
@@ -63,6 +64,7 @@ namespace games::nost::poke {
// create new thread // create new thread
THREAD_RUNNING = true; THREAD_RUNNING = true;
THREAD = new std::thread([] { THREAD = new std::thread([] {
timeutils::PreciseSleepTimer timer;
const DWORD touch_id = (DWORD)(0xFFFFFFFE); const DWORD touch_id = (DWORD)(0xFFFFFFFE);
const int swipe_anim_total_frames = 6; const int swipe_anim_total_frames = 6;
@@ -178,7 +180,7 @@ namespace games::nost::poke {
} }
// slow down // slow down
Sleep(30); timer.sleep(30);
} }
return nullptr; return nullptr;
+5 -1
View File
@@ -4,6 +4,7 @@
#include "touchpanel.h" #include "touchpanel.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "touch/touch.h" #include "touch/touch.h"
using namespace std::chrono_literals; 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) { int games::onpara::TouchPanelHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
static thread_local timeutils::PreciseSleepTimer timer;
DWORD i; DWORD i;
auto buffer = reinterpret_cast<uint8_t *>(lpBuffer); auto buffer = reinterpret_cast<uint8_t *>(lpBuffer);
@@ -67,7 +71,7 @@ int games::onpara::TouchPanelHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesT
enqueue_packet(report); enqueue_packet(report);
// prevent cpu bullying // prevent cpu bullying
std::this_thread::sleep_for(1ms); timer.sleep(1);
} }
// copy from output queue // copy from output queue
+74
View File
@@ -4,6 +4,7 @@
#include "util/detour.h" #include "util/detour.h"
#include "util/libutils.h" #include "util/libutils.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/sigscan.h"
#define OTOCA_DEBUG_VERBOSE 0 #define OTOCA_DEBUG_VERBOSE 0
#if OTOCA_DEBUG_VERBOSE #if OTOCA_DEBUG_VERBOSE
@@ -116,6 +117,73 @@ namespace games::otoca {
return ret; return ret;
} }
static void patch_holo_print_hang() {
// arkkep.dll's print method sets the printer busy (0x66 / Printing_Busy),
// then bails out with -1 WITHOUT printing when its holo parameter is
// nonzero (the card path passes 0 and works). The completion callback that
// clears busy never fires, so the page's poll loop spins forever -> hang.
// The image band is already built, so flip the je that skips the bail-out
// into a jmp (74 -> EB) to route holo through the print path like card.
//
// je issue_print ; <- patched to jmp
// or eax, -1 ; holo: return without printing
// ret 10h
auto arkkep = libutils::try_module("arkkep.dll");
if (arkkep == nullptr) {
log_warning("otoca", "arkkep.dll not loaded; skipping holo print hang fix");
return;
}
auto result = replace_pattern(
arkkep,
"C7465866000000895DC0C74710080700007416",
"C7465866000000895DC0C7471008070000EB16",
0, 0);
if (result) {
log_info("otoca", "patched hologram print hang in arkkep.dll");
} else {
log_warning("otoca", "could not patch hologram print hang (incompatible arkkep.dll?)");
}
}
static void patch_holo_print_search() {
// before printing, arkkep walks the page's printer list looking for one
// whose capability flag (byte at entry+120h) matches the job type
// (0 = card, 1 = holo). Our emulated printer is only card-capable, so a
// holo job finds no match and aborts without printing ("not found holo
// printer"). The emulated printer serves both media, so force the compare
// to always match by turning `cmp cl,[edi+120h]` into `cmp cl,cl` + NOPs,
// which always sets ZF. Two such compares exist, so patch both.
//
// cmp cl, [edi+120h] ; <- patched to `cmp cl,cl` (3A 8F.. -> 3A C9..)
// je printer_found
auto arkkep = libutils::try_module("arkkep.dll");
if (arkkep == nullptr) {
log_warning("otoca", "arkkep.dll not loaded; skipping holo print search fix");
return;
}
int patched = 0;
while (patched < 2) {
auto result = replace_pattern(
arkkep,
"3A8F20010000",
"3AC990909090",
0, 0);
if (!result) {
break;
}
patched++;
}
if (patched > 0) {
log_info("otoca", "patched hologram printer search in arkkep.dll ({} site(s))", patched);
} else {
log_warning("otoca", "could not patch hologram printer search (incompatible arkkep.dll?)");
}
}
void OtocaGame::attach() { void OtocaGame::attach() {
Game::attach(); Game::attach();
@@ -129,6 +197,12 @@ namespace games::otoca {
p4io_hook(); p4io_hook();
games::shared::printer_attach(); games::shared::printer_attach();
// fix hologram print hard hang in arkkep.dll
patch_holo_print_hang();
// make hologram jobs find the emulated printer (else they abort unprinted)
patch_holo_print_search();
if (BYPASS_CAMERA) { if (BYPASS_CAMERA) {
libutils::try_library("libcamera.dll"); libutils::try_library("libcamera.dll");
const auto libcamera = "libcamera.dll"; const auto libcamera = "libcamera.dll";
+6 -5
View File
@@ -51,11 +51,12 @@ std::vector<Analog> &games::pc::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Polaris Chord"); analogs = GameAPI::Analogs::getAnalogs("Polaris Chord");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Fader-L", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Fader-R" { "Fader-L", AnalogType::LinearCentered },
); { "Fader-R", AnalogType::LinearCentered }
});
} }
return analogs; return analogs;
+1 -1
View File
@@ -261,7 +261,7 @@ namespace games::popn {
} else if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME) { } else if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME) {
const auto source = reinterpret_cast<DISPLAYCONFIG_SOURCE_DEVICE_NAME*>(requestPacket); const auto source = reinterpret_cast<DISPLAYCONFIG_SOURCE_DEVICE_NAME*>(requestPacket);
// value must match WrappedIDirect3D9::GetAdapterIdentifier // value must match WrappedIDirect3D9::GetAdapterIdentifier
wcscpy(source->viewGdiDeviceName, L"\\\\.\\DISPLAY_SPICE_FAKE"); wcscpy(source->viewGdiDeviceName, L"\\\\.\\DISPLAY_SPICE_FAKE_1");
} else { } else {
log_fatal( log_fatal(
"popn", "popn",
+27 -13
View File
@@ -4,29 +4,34 @@
#include "hooks/devicehook.h" #include "hooks/devicehook.h"
#include "hooks/audio/backends/dsound/dsound_backend.h" #include "hooks/audio/backends/dsound/dsound_backend.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/precise_timer.h"
#include "util/logging.h"
#include "cfg/configurator.h"
#include "touch.h" #include "touch.h"
namespace games::rb {
uint16_t TOUCH_POLL_RATE = 0;
}
static decltype(SleepEx) *SleepEx_orig; static decltype(SleepEx) *SleepEx_orig;
static DWORD WINAPI SleepEx_hook(DWORD dwMilliseconds, BOOL bAltertable) { static DWORD WINAPI SleepEx_hook(DWORD dwMilliseconds, BOOL bAlertable) {
/* // increase touch poll rate
* Increase touch poll from ~110 FPS to ~500 FPS
*/
if (dwMilliseconds == 8) { if (dwMilliseconds == 8) {
static bool initialized = false; dwMilliseconds = 1000 / games::rb::TOUCH_POLL_RATE;
if (!initialized) { }
initialized = true;
// if we only sleep for 1ms we also don't need the high priority RB sets // most calls from rb are actually non-alertable
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL); if (!bAlertable) {
} static thread_local timeutils::PreciseSleepTimer timer;
dwMilliseconds = 1; timer.sleep(dwMilliseconds);
return 0;
} }
// call original // call original
return SleepEx_orig(dwMilliseconds, bAltertable); return SleepEx_orig(dwMilliseconds, bAlertable);
} }
games::rb::RBGame::RBGame() : Game("Reflec Beat") { games::rb::RBGame::RBGame() : Game("Reflec Beat") {
@@ -35,6 +40,10 @@ games::rb::RBGame::RBGame() : Game("Reflec Beat") {
void games::rb::RBGame::attach() { void games::rb::RBGame::attach() {
Game::attach(); Game::attach();
if (1000 < games::rb::TOUCH_POLL_RATE) {
log_fatal("rb", "-rbtouchsize is set too high; cannot exceed 1000");
}
// init stuff // init stuff
devicehook_init(); devicehook_init();
@@ -47,7 +56,12 @@ void games::rb::RBGame::attach() {
} }
// hooks // hooks
SleepEx_orig = detour::iat_try("SleepEx", SleepEx_hook, avs::game::DLL_INSTANCE); if (0 < TOUCH_POLL_RATE) {
log_info("rb", "force increasing touch poll rate by hooking SleepEx ({}Hz)", TOUCH_POLL_RATE);
SleepEx_orig = detour::iat_try("SleepEx", SleepEx_hook, avs::game::DLL_INSTANCE);
} else {
log_info("rb", "not changing touch poll rate (120Hz by default)");
}
} }
void games::rb::RBGame::detach() { void games::rb::RBGame::detach() {
+2
View File
@@ -7,6 +7,8 @@
namespace games::rb { namespace games::rb {
extern uint16_t TOUCH_SCALING; extern uint16_t TOUCH_SCALING;
extern uint8_t TOUCH_SIZE;
extern uint16_t TOUCH_POLL_RATE;
class RBGame : public games::Game { class RBGame : public games::Game {
public: public:
+14 -9
View File
@@ -13,6 +13,7 @@ static std::string WINDOW_TITLE = "REFLEC BEAT";
namespace games::rb { namespace games::rb {
uint16_t TOUCH_SCALING = 1000; uint16_t TOUCH_SCALING = 1000;
uint8_t TOUCH_SIZE = 1;
} }
games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) { games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) {
@@ -160,16 +161,20 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
const auto point_x = (int) ((x - window_width / 2.0) * 48.0 / 54.0 + window_width / 2.0); const auto point_x = (int) ((x - window_width / 2.0) * 48.0 / 54.0 + window_width / 2.0);
const auto point_y = (int) (y - window_height / 76); const auto point_y = (int) (y - window_height / 76);
// insert 9 times with offset to double the precision // center
grid_insert(data, point_x, point_y); grid_insert(data, point_x, point_y);
grid_insert(data, point_x - offset_x, point_y);
grid_insert(data, point_x - offset_x, point_y - offset_y); // surrounding points
grid_insert(data, point_x - offset_x, point_y + offset_y); if (games::rb::TOUCH_SIZE == 3) {
grid_insert(data, point_x + offset_x, point_y); grid_insert(data, point_x - offset_x, point_y); // west
grid_insert(data, point_x + offset_x, point_y + offset_y); grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest
grid_insert(data, point_x + offset_x, point_y - offset_y); grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest
grid_insert(data, point_x, point_y - offset_y); grid_insert(data, point_x + offset_x, point_y); // east
grid_insert(data, point_x, point_y + offset_y); grid_insert(data, point_x + offset_x, point_y + offset_y); // southeast
grid_insert(data, point_x + offset_x, point_y - offset_y); // northeast
grid_insert(data, point_x, point_y - offset_y); // north
grid_insert(data, point_x, point_y + offset_y); // south
}
} }
// copy data to buffer // copy data to buffer
+7 -6
View File
@@ -35,12 +35,13 @@ std::vector<Analog> &games::rf3d::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Road Fighters 3D"); analogs = GameAPI::Analogs::getAnalogs("Road Fighters 3D");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Wheel", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Accelerate", { "Wheel", AnalogType::LinearCentered },
"Brake" { "Accelerate", AnalogType::LinearPositive },
); { "Brake", AnalogType::LinearPositive }
});
} }
return analogs; return analogs;
+8 -7
View File
@@ -31,13 +31,14 @@ std::vector<Analog> &games::sc::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Steel Chronicle"); analogs = GameAPI::Analogs::getAnalogs("Steel Chronicle");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Left Stick X", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Left Stick Y", { "Left Stick X", AnalogType::LinearCentered },
"Right Stick X", { "Left Stick Y", AnalogType::LinearCentered },
"Right Stick Y" { "Right Stick X", AnalogType::LinearCentered },
); { "Right Stick Y", AnalogType::LinearCentered }
});
} }
return analogs; return analogs;
+7
View File
@@ -7,6 +7,7 @@
#include "hooks/sleephook.h" #include "hooks/sleephook.h"
#include "hooks/libraryhook.h" #include "hooks/libraryhook.h"
#include "launcher/launcher.h" #include "launcher/launcher.h"
#include "overlay/notifications.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/fileutils.h" #include "util/fileutils.h"
#include "util/libutils.h" #include "util/libutils.h"
@@ -397,8 +398,14 @@ namespace games::shared {
// logging // logging
if (success) { if (success) {
log_info("printer", "printer emulation has written an image to {}", image_path); log_info("printer", "printer emulation has written an image to {}", image_path);
overlay::notifications::add(
overlay::notifications::Severity::Success,
fmt::format("Printer: saved {}", fileutils::basename(image_path)));
} else { } else {
log_warning("printer", "printer emulation failed to write image to {}", image_path); log_warning("printer", "printer emulation failed to write image to {}", image_path);
overlay::notifications::add(
overlay::notifications::Severity::Error,
fmt::format("Printer: failed to write {}", fileutils::basename(image_path)));
} }
} }
} }
+6 -5
View File
@@ -31,11 +31,12 @@ std::vector<Analog> &games::silentscope::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Silent Scope: Bone Eater"); analogs = GameAPI::Analogs::getAnalogs("Silent Scope: Bone Eater");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Gun X", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Gun Y" { "Gun X", AnalogType::LinearCentered },
); { "Gun Y", AnalogType::LinearCentered }
});
} }
return analogs; return analogs;
+8 -7
View File
@@ -50,13 +50,14 @@ std::vector<Analog> &games::we::get_analogs() {
if (analogs.empty()) { if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Winning Eleven"); analogs = GameAPI::Analogs::getAnalogs("Winning Eleven");
GameAPI::Analogs::sortAnalogs( using GameAPI::Analogs::AnalogType;
&analogs,
"Pad Stick Left X", GameAPI::Analogs::sortAnalogsWithType(&analogs, {
"Pad Stick Left Y", { "Pad Stick Left X", AnalogType::LinearCentered },
"Pad Stick Right X", { "Pad Stick Left Y", AnalogType::LinearCentered },
"Pad Stick Right Y" { "Pad Stick Right X", AnalogType::LinearCentered },
); { "Pad Stick Right Y", AnalogType::LinearCentered }
});
} }
return analogs; return analogs;
+12 -8
View File
@@ -39,6 +39,8 @@ namespace hooks::audio {
// public globals // public globals
bool ENABLED = true; bool ENABLED = true;
bool VOLUME_HOOK_ENABLED = true; bool VOLUME_HOOK_ENABLED = true;
std::optional<DownmixAlgorithm> DOWNMIX_ALGORITHM = std::nullopt;
float VOLUME_BOOST = 1.0f;
bool USE_DUMMY = false; bool USE_DUMMY = false;
WAVEFORMATEXTENSIBLE FORMAT {}; WAVEFORMATEXTENSIBLE FORMAT {};
std::optional<Backend> BACKEND = std::nullopt; std::optional<Backend> BACKEND = std::nullopt;
@@ -66,14 +68,16 @@ static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
if (FAILED(ret)) { if (FAILED(ret)) {
if (IsEqualCLSID(rclsid, CLSID_MMDeviceEnumerator)) { if (IsEqualCLSID(rclsid, CLSID_MMDeviceEnumerator)) {
log_warning("audio", "CoCreateInstance failed for CLSID_MMDeviceEnumerator, hr={}", FMT_HRESULT(ret)); log_warning("audio", "CoCreateInstance failed for CLSID_MMDeviceEnumerator, hr={}", FMT_HRESULT(ret));
} } else if (IsEqualCLSID(rclsid, CLSID_DirectSoundI3DL2ReverbDMO)) {
// deal with reverb DMO missing from dsdmo.dll
// Windows 11 KB5052093 issue - reverb DMO went missing from dsdmo.dll (fails with 0x80040154) // it usually fails with 0x80040154 (REGDB_E_CLASSNOTREG), but we have also seen 0x8007007e (ERROR_MOD_NOT_FOUND)
if (IsEqualCLSID(rclsid, CLSID_DirectSoundI3DL2ReverbDMO) && ret == (HRESULT)0x80040154) { static std::once_flag printed;
log_warning( std::call_once(printed, [ret]() {
"audio", log_warning(
"CoCreateInstance for CLSID_DirectSoundI3DL2ReverbDMO failed with 0x80040154, swap with CLSID_DirectSoundWavesReverbDMO " "audio",
"(workaround for Windows 11 KB5052093 issue); REVERB EX replaced with REVERB"); "CoCreateInstance for CLSID_DirectSoundI3DL2ReverbDMO failed with {}, swap with CLSID_DirectSoundWavesReverbDMO...",
FMT_HRESULT(ret));
});
ret = CoCreateInstance_orig(CLSID_DirectSoundWavesReverbDMO, pUnkOuter, dwClsContext, riid, ppv); ret = CoCreateInstance_orig(CLSID_DirectSoundWavesReverbDMO, pUnkOuter, dwClsContext, riid, ppv);
if (FAILED(ret)) { if (FAILED(ret)) {
log_warning("audio", "CoCreateInstance(CLSID_DirectSoundWavesReverbDMO...) failed, hr={}", FMT_HRESULT(ret)); log_warning("audio", "CoCreateInstance(CLSID_DirectSoundWavesReverbDMO...) failed, hr={}", FMT_HRESULT(ret));
+11
View File
@@ -16,8 +16,19 @@ namespace hooks::audio {
WaveOut, WaveOut,
}; };
// surround-to-stereo downmix algorithm
enum class DownmixAlgorithm {
FrontOnly, // keep only the front channels
RearOnly, // keep only the rear/back channels
SideOnly, // keep only the side channels
AC4, // AC-4 stereo downmix coefficients (ETSI TS 103 190-1)
Normalize, // all channels equally loud, LFE dropped
};
extern bool ENABLED; extern bool ENABLED;
extern bool VOLUME_HOOK_ENABLED; extern bool VOLUME_HOOK_ENABLED;
extern std::optional<DownmixAlgorithm> DOWNMIX_ALGORITHM;
extern float VOLUME_BOOST;
extern bool USE_DUMMY; extern bool USE_DUMMY;
extern WAVEFORMATEXTENSIBLE FORMAT; extern WAVEFORMATEXTENSIBLE FORMAT;
extern std::optional<Backend> BACKEND; extern std::optional<Backend> BACKEND;
@@ -4,6 +4,7 @@
#include <ksmedia.h> #include <ksmedia.h>
#include "avs/game.h" #include "avs/game.h"
#include "games/gitadora/gitadora.h"
#include "hooks/audio/audio.h" #include "hooks/audio/audio.h"
#include "hooks/audio/util.h" #include "hooks/audio/util.h"
#include "hooks/audio/backends/wasapi/util.h" #include "hooks/audio/backends/wasapi/util.h"
@@ -41,6 +42,27 @@ static void fix_rec_format(WAVEFORMATEX *pFormat) {
pFormat->nAvgBytesPerSec = pFormat->nSamplesPerSec * pFormat->nBlockAlign; pFormat->nAvgBytesPerSec = pFormat->nSamplesPerSec * pFormat->nBlockAlign;
} }
// decide whether the given multi-channel format should be downmixed to stereo and which algorithm
// to use. an explicit user selection (-downmix) takes precedence; otherwise gitadora arena
// two-channel mode defaults to the AC-4 algorithm.
static std::optional<hooks::audio::DownmixAlgorithm> resolve_downmix(const WAVEFORMATEX *format) {
if (format == nullptr
|| format->nChannels <= 2
|| format->wFormatTag != WAVE_FORMAT_EXTENSIBLE) {
return std::nullopt;
}
if (hooks::audio::DOWNMIX_ALGORITHM.has_value()) {
return hooks::audio::DOWNMIX_ALGORITHM;
}
if (games::gitadora::is_arena_model() && games::gitadora::TWOCHANNEL) {
return hooks::audio::DownmixAlgorithm::AC4;
}
return std::nullopt;
}
IAudioClient *wrap_audio_client(IAudioClient *audio_client) { IAudioClient *wrap_audio_client(IAudioClient *audio_client) {
log_misc("audio::wasapi", "wrapping IAudioClient"); log_misc("audio::wasapi", "wrapping IAudioClient");
@@ -145,13 +167,27 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat)); fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat));
} }
// when downmixing, open the real device as stereo while the game keeps writing its native
// multi-channel format into the scratch buffer.
WAVEFORMATEXTENSIBLE stereo_storage = {};
const WAVEFORMATEX *device_format = pFormat;
if (auto algorithm = resolve_downmix(pFormat)) {
this->downmix.setup(pFormat, &stereo_storage, *algorithm);
device_format = reinterpret_cast<const WAVEFORMATEX *>(&stereo_storage);
log_info("audio::wasapi", "downmix enabled: {} channels -> 2 channels ({})",
pFormat->nChannels, hooks::audio::Downmix::algorithm_name(*algorithm));
} else if (games::gitadora::is_arena_model()) {
games::gitadora::fix_audio_channel_mask(const_cast<WAVEFORMATEX *>(pFormat));
}
// verbose output // verbose output
log_info("audio::wasapi", "IAudioClient::Initialize hook hit"); log_info("audio::wasapi", "IAudioClient::Initialize hook hit");
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(ShareMode)); log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(ShareMode));
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(StreamFlags)); log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(StreamFlags));
log_info("audio::wasapi", "... hnsBufferDuration : {}", hnsBufferDuration); log_info("audio::wasapi", "... hnsBufferDuration : {}", hnsBufferDuration);
log_info("audio::wasapi", "... hnsPeriodicity : {}", hnsPeriodicity); log_info("audio::wasapi", "... hnsPeriodicity : {}", hnsPeriodicity);
print_format(pFormat); print_format(device_format);
if (this->backend) { if (this->backend) {
SAFE_CALL("AudioBackend", "on_initialize", this->backend->on_initialize( SAFE_CALL("AudioBackend", "on_initialize", this->backend->on_initialize(
@@ -173,17 +209,29 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
// check for exclusive mode // check for exclusive mode
if (ShareMode == AUDCLNT_SHAREMODE_EXCLUSIVE) { if (ShareMode == AUDCLNT_SHAREMODE_EXCLUSIVE) {
this->exclusive_mode = true; this->exclusive_mode = true;
this->frame_size = pFormat->nChannels * (pFormat->wBitsPerSample / 8); this->frame_size = device_format->nChannels * (device_format->wBitsPerSample / 8);
} }
// call next // call next
HRESULT ret = pReal->Initialize( HRESULT ret;
ShareMode, if (this->downmix.enabled) {
StreamFlags, ret = this->downmix.initialize(
hnsBufferDuration, pReal,
hnsPeriodicity, ShareMode,
pFormat, StreamFlags,
AudioSessionGuid); hnsBufferDuration,
hnsPeriodicity,
device_format,
AudioSessionGuid);
} else {
ret = pReal->Initialize(
ShareMode,
StreamFlags,
hnsBufferDuration,
hnsPeriodicity,
device_format,
AudioSessionGuid);
}
// check for failure // check for failure
if (FAILED(ret)) { if (FAILED(ret)) {
@@ -192,7 +240,8 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
} }
log_info("audio::wasapi", "IAudioClient::Initialize success, hr={}", FMT_HRESULT(ret)); log_info("audio::wasapi", "IAudioClient::Initialize success, hr={}", FMT_HRESULT(ret));
copy_wave_format(&hooks::audio::FORMAT, pFormat); copy_wave_format(&hooks::audio::FORMAT, device_format);
copy_wave_format(&this->device_format, device_format);
return ret; return ret;
} }
@@ -274,6 +323,18 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::IsFormatSupported(
fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat)); fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat));
} }
// when downmixing, the real device is opened as stereo, so check whether the equivalent
// stereo format is supported instead of the multi-channel one.
if (resolve_downmix(pFormat)) {
WAVEFORMATEXTENSIBLE stereo_storage = {};
hooks::audio::Downmix::make_stereo_format(pFormat, &stereo_storage);
const auto stereo_format = reinterpret_cast<const WAVEFORMATEX *>(&stereo_storage);
CHECK_RESULT(pReal->IsFormatSupported(ShareMode, stereo_format, ppClosestMatch));
} else if (games::gitadora::is_arena_model()) {
games::gitadora::fix_audio_channel_mask(const_cast<WAVEFORMATEX *>(pFormat));
}
if (this->backend) { if (this->backend) {
HRESULT ret = this->backend->on_is_format_supported(&ShareMode, pFormat, ppClosestMatch); HRESULT ret = this->backend->on_is_format_supported(&ShareMode, pFormat, ppClosestMatch);
@@ -466,5 +527,6 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioClient::InitializeSharedAudioStream(
log_info("audio::wasapi", "IAudioClient3::InitializeSharedAudioStream success, hr={}", FMT_HRESULT(ret)); log_info("audio::wasapi", "IAudioClient3::InitializeSharedAudioStream success, hr={}", FMT_HRESULT(ret));
copy_wave_format(&hooks::audio::FORMAT, pFormat); copy_wave_format(&hooks::audio::FORMAT, pFormat);
copy_wave_format(&this->device_format, pFormat);
return ret; return ret;
} }
@@ -8,6 +8,8 @@
#include "hooks/audio/audio_private.h" #include "hooks/audio/audio_private.h"
#include "util/logging.h" #include "util/logging.h"
#include "downmix.h"
#include "audio_render_client.h" #include "audio_render_client.h"
// {1FBC8530-AF3E-4128-B418-115DE72F76B6} // {1FBC8530-AF3E-4128-B418-115DE72F76B6}
@@ -93,4 +95,12 @@ struct WrappedIAudioClient : IAudioClient3 {
AudioBackend *const backend; AudioBackend *const backend;
bool exclusive_mode = false; bool exclusive_mode = false;
int frame_size = 0; int frame_size = 0;
// the format the real device was opened with (after any downmix). used to scale the final
// output buffer for the volume boost.
WAVEFORMATEXTENSIBLE device_format = {};
// surround -> stereo downmix. the real device is opened as stereo while the game keeps
// writing multi-channel audio into a scratch buffer that we downmix in the render client.
hooks::audio::Downmix downmix;
}; };
@@ -1,10 +1,72 @@
#include "audio_render_client.h" #include "audio_render_client.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include "audio_client.h" #include "audio_client.h"
#include "hooks/audio/audio.h"
#include "wasapi_private.h" #include "wasapi_private.h"
const char CLASS_NAME[] = "WrappedIAudioRenderClient"; const char CLASS_NAME[] = "WrappedIAudioRenderClient";
// scale every sample of an interleaved device buffer by `gain`, clamped to the format's range.
// supports 16/24/32-bit PCM and 32-bit float; other formats are left untouched.
static void apply_gain(BYTE *buffer, UINT32 frames, const WAVEFORMATEXTENSIBLE &fmt, float gain) {
const WAVEFORMATEX &f = fmt.Format;
const size_t samples = (size_t) frames * f.nChannels;
// KSDATAFORMAT_SUBTYPE_IEEE_FLOAT has Data1 == 3, _PCM has Data1 == 1
bool is_float = f.wFormatTag == WAVE_FORMAT_IEEE_FLOAT
|| (f.wFormatTag == WAVE_FORMAT_EXTENSIBLE && fmt.SubFormat.Data1 == 0x00000003);
if (is_float && f.wBitsPerSample == 32) {
auto p = reinterpret_cast<float *>(buffer);
for (size_t i = 0; i < samples; i++) {
p[i] = std::clamp(p[i] * gain, -1.0f, 1.0f);
}
return;
}
switch (f.wBitsPerSample) {
case 16: {
auto p = reinterpret_cast<int16_t *>(buffer);
for (size_t i = 0; i < samples; i++) {
p[i] = (int16_t) std::clamp((int) std::lround(p[i] * gain), -32768, 32767);
}
break;
}
case 24: {
// packed 24-bit little-endian
for (size_t i = 0; i < samples; i++) {
BYTE *s = buffer + i * 3;
int32_t v = s[0] | (s[1] << 8) | (s[2] << 16);
if (v & 0x800000) {
v |= ~0xFFFFFF; // sign extend
}
int64_t scaled = std::clamp<int64_t>(
std::llround((double) v * gain), -8388608, 8388607);
s[0] = scaled & 0xFF;
s[1] = (scaled >> 8) & 0xFF;
s[2] = (scaled >> 16) & 0xFF;
}
break;
}
case 32: {
auto p = reinterpret_cast<int32_t *>(buffer);
for (size_t i = 0; i < samples; i++) {
p[i] = (int32_t) std::clamp(
std::llround((double) p[i] * gain),
(long long) INT32_MIN, (long long) INT32_MAX);
}
break;
}
default:
break;
}
}
HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::QueryInterface(REFIID riid, void **ppvObj) { HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::QueryInterface(REFIID riid, void **ppvObj) {
if (ppvObj == nullptr) { if (ppvObj == nullptr) {
return E_POINTER; return E_POINTER;
@@ -51,6 +113,12 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::GetBuffer(UINT32 NumFramesR
return S_OK; return S_OK;
} }
// surround downmix: reserve the real (stereo) device buffer, but hand the game a
// multi-channel scratch buffer that we downmix on release
if (this->client->downmix.enabled) {
CHECK_RESULT(this->client->downmix.get_buffer(pReal, NumFramesRequested, ppData));
}
// call original // call original
HRESULT ret = pReal->GetBuffer(NumFramesRequested, ppData); HRESULT ret = pReal->GetBuffer(NumFramesRequested, ppData);
@@ -75,14 +143,44 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::ReleaseBuffer(UINT32 NumFra
return S_OK; return S_OK;
} }
// fix for audio pop effect // resolve the real device buffer for whichever path produced the audio
if (this->buffers_to_mute > 0 && this->client->frame_size > 0) { BYTE *device_buffer;
if (this->client->downmix.enabled) {
// zero out = mute // downmix the game's multi-channel scratch into the real stereo buffer held since GetBuffer
memset(this->audio_buffer, 0, NumFramesWritten * this->client->frame_size); this->client->downmix.write_device_buffer(NumFramesWritten, dwFlags);
device_buffer = this->client->downmix.current_buffer();
} else {
device_buffer = this->audio_buffer;
this->buffers_to_mute--; // mute the first few buffers to avoid a startup pop
if (this->buffers_to_mute > 0 && this->client->frame_size > 0) {
memset(this->audio_buffer, 0, NumFramesWritten * this->client->frame_size);
this->buffers_to_mute--;
}
} }
CHECK_RESULT(pReal->ReleaseBuffer(NumFramesWritten, dwFlags)); // boost the final output volume just before it reaches the device, layout-agnostic
if (hooks::audio::VOLUME_BOOST != 1.0f
&& device_buffer != nullptr
&& (dwFlags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
static std::once_flag boost_printed;
std::call_once(boost_printed, []() {
log_info("audio::wasapi", "volume boost active: gain={}", hooks::audio::VOLUME_BOOST);
});
apply_gain(device_buffer, NumFramesWritten, this->client->device_format,
hooks::audio::VOLUME_BOOST);
}
HRESULT ret = pReal->ReleaseBuffer(NumFramesWritten, dwFlags);
if (this->client->downmix.enabled) {
this->client->downmix.buffer_released();
}
if (FAILED(ret)) {
PRINT_FAILED_RESULT(CLASS_NAME, __func__, ret);
}
return ret;
} }
@@ -0,0 +1,332 @@
#include "downmix.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <audioclient.h>
#include <ks.h>
#include <ksmedia.h>
#include "util/logging.h"
namespace hooks::audio {
namespace {
constexpr float ATT_3DB = 0.70710678f;
// speakers routed to the left/right output; anything else (center) feeds both sides
constexpr DWORD LEFT_SPEAKERS = SPEAKER_FRONT_LEFT | SPEAKER_BACK_LEFT | SPEAKER_SIDE_LEFT
| SPEAKER_FRONT_LEFT_OF_CENTER | SPEAKER_TOP_FRONT_LEFT | SPEAKER_TOP_BACK_LEFT;
constexpr DWORD RIGHT_SPEAKERS = SPEAKER_FRONT_RIGHT | SPEAKER_BACK_RIGHT | SPEAKER_SIDE_RIGHT
| SPEAKER_FRONT_RIGHT_OF_CENTER | SPEAKER_TOP_FRONT_RIGHT | SPEAKER_TOP_BACK_RIGHT;
// the speaker mask is only present on WAVE_FORMAT_EXTENSIBLE formats
DWORD read_channel_mask(const WAVEFORMATEX *fmt) {
if (fmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE
&& fmt->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
return reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(fmt)->dwChannelMask;
}
return 0;
}
// call visit(channel_index, speaker_bit) for each present speaker, in channel order
template <typename F>
void for_each_speaker(DWORD mask, int channels, F &&visit) {
int channel = 0;
for (int bit = 0; bit < 18 && channel < channels; bit++) {
const DWORD speaker = 1u << bit;
if (mask & speaker) {
visit(channel++, speaker);
}
}
}
}
// read one sample at `p` as a normalized float in [-1, 1]
static inline float read_sample(const BYTE *p, int bytes, bool is_float) {
if (is_float) {
float v;
memcpy(&v, p, sizeof(float));
return v;
}
switch (bytes) {
case 2: {
int16_t v;
memcpy(&v, p, sizeof(v));
return v * (1.0f / 32768.0f);
}
case 3: {
int32_t v = p[0] | (p[1] << 8) | (p[2] << 16);
if (v & 0x800000) {
v |= ~0xFFFFFF; // sign extend
}
return v * (1.0f / 8388608.0f);
}
case 4: {
int32_t v;
memcpy(&v, p, sizeof(v));
return (float) (v * (1.0 / 2147483648.0));
}
default:
return 0.0f;
}
}
// write the normalized float `value` to the sample at `p`, clamping to the format's range
static inline void write_sample(BYTE *p, int bytes, bool is_float, float value) {
if (is_float) {
float v = std::clamp(value, -1.0f, 1.0f);
memcpy(p, &v, sizeof(v));
return;
}
switch (bytes) {
case 2: {
int16_t v = (int16_t) std::clamp(
(int) std::lround(value * 32768.0f), -32768, 32767);
memcpy(p, &v, sizeof(v));
break;
}
case 3: {
int32_t v = (int32_t) std::clamp<int64_t>(
std::llround((double) value * 8388608.0), -8388608, 8388607);
p[0] = v & 0xFF;
p[1] = (v >> 8) & 0xFF;
p[2] = (v >> 16) & 0xFF;
break;
}
case 4: {
int32_t v = (int32_t) std::clamp<int64_t>(
std::llround((double) value * 2147483648.0), INT32_MIN, INT32_MAX);
memcpy(p, &v, sizeof(v));
break;
}
default:
break;
}
}
void Downmix::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out,
DownmixAlgorithm algorithm) {
this->enabled = true;
this->algorithm = algorithm;
this->bytes_per_sample = game_format->wBitsPerSample / 8;
this->game_frame_size = game_format->nChannels * this->bytes_per_sample;
// KSDATAFORMAT_SUBTYPE_IEEE_FLOAT has Data1 == 3 (matches apply_gain detection)
this->is_float = game_format->wFormatTag == WAVE_FORMAT_IEEE_FLOAT
|| (game_format->wFormatTag == WAVE_FORMAT_EXTENSIBLE
&& reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(game_format)
->SubFormat.Data1 == 0x00000003);
// supported: 16/24/32-bit integer PCM and 32-bit float; anything else mixes to silence
const bool supported = this->is_float
? this->bytes_per_sample == 4
: (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4);
if (!supported) {
log_fatal(
"audio::downmix",
"unsupported sample format ({}-bit {}), downmix will output silence",
game_format->wBitsPerSample, this->is_float ? "float" : "int");
}
this->left_mix.clear();
this->right_mix.clear();
this->build_layout_mix(game_format);
make_stereo_format(game_format, stereo_out);
}
void Downmix::make_stereo_format(const WAVEFORMATEX *game_format,
WAVEFORMATEXTENSIBLE *stereo_out) {
const int bytes_per_sample = game_format->wBitsPerSample / 8;
memcpy(stereo_out, game_format, sizeof(WAVEFORMATEXTENSIBLE));
stereo_out->Format.nChannels = 2;
stereo_out->Format.nBlockAlign = 2 * bytes_per_sample;
stereo_out->Format.nAvgBytesPerSec =
game_format->nSamplesPerSec * stereo_out->Format.nBlockAlign;
stereo_out->dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
}
HRESULT Downmix::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
const WAVEFORMATEX *device_format, LPCGUID session_guid) {
HRESULT ret = real->Initialize(share_mode, stream_flags, buffer_duration, periodicity,
device_format, session_guid);
// the smaller stereo buffer can end up unaligned for the device when the game sized the
// duration for its larger multi-channel format. recover by asking the device for the next
// aligned buffer size and re-initializing with a matching duration.
if (ret == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED) {
UINT32 aligned_frames = 0;
if (SUCCEEDED(real->GetBufferSize(&aligned_frames)) && aligned_frames > 0) {
REFERENCE_TIME aligned_duration = (REFERENCE_TIME)
(10000.0 * 1000 / device_format->nSamplesPerSec * aligned_frames + 0.5);
log_info("audio::downmix",
"buffer not aligned, retrying with {} frames ({} hns)",
aligned_frames, aligned_duration);
ret = real->Initialize(share_mode, stream_flags, aligned_duration,
periodicity != 0 ? aligned_duration : 0, device_format, session_guid);
}
}
return ret;
}
void Downmix::add_channel(int channel, DWORD speaker, float gain) {
if (speaker & LEFT_SPEAKERS) {
this->left_mix.push_back({ channel, gain });
} else if (speaker & RIGHT_SPEAKERS) {
this->right_mix.push_back({ channel, gain });
} else { // center: feed both sides
this->left_mix.push_back({ channel, gain });
this->right_mix.push_back({ channel, gain });
}
}
// AC-4 stereo downmix (ETSI TS 103 190-1): front pair at unity, everything else -3 dB, LFE dropped
void Downmix::build_ac4_mix(DWORD mask, int channels) {
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
if (speaker == SPEAKER_LOW_FREQUENCY) {
return;
}
const bool front_pair = speaker & (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT);
this->add_channel(ch, speaker, front_pair ? 1.0f : ATT_3DB);
});
}
// keep only the channels in `keep` (front/rear/side), each at unity gain
void Downmix::build_extract_mix(DWORD mask, int channels, DWORD keep) {
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
if (speaker & keep) {
this->add_channel(ch, speaker, 1.0f);
}
});
}
// keep every channel (LFE dropped), then average each side so its gains sum to unity
void Downmix::build_normalize_mix(DWORD mask, int channels) {
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
if (speaker != SPEAKER_LOW_FREQUENCY) {
this->add_channel(ch, speaker, 1.0f);
}
});
for (auto *mix : { &this->left_mix, &this->right_mix }) {
if (!mix->empty()) {
const float gain = 1.0f / mix->size();
for (auto &c : *mix) {
c.gain = gain;
}
}
}
}
// fallback when no speaker mask is present: fold interleaved L/R pairs (even->left, odd->right)
void Downmix::build_pairs_mix(int channels, float gain) {
for (int ch = 0; ch < channels; ch++) {
(((ch & 1) == 0) ? this->left_mix : this->right_mix).push_back({ ch, gain });
}
}
void Downmix::build_layout_mix(const WAVEFORMATEX *game_format) {
const int channels = game_format->nChannels;
const DWORD mask = read_channel_mask(game_format);
// without a mask the layout is unknown: extract/normalize have nothing to act on, so all
// algorithms fall back to folding L/R pairs (AC-4 still attenuates by -3 dB)
if (mask == 0) {
this->build_pairs_mix(channels,
this->algorithm == DownmixAlgorithm::AC4 ? ATT_3DB : 1.0f);
return;
}
switch (this->algorithm) {
case DownmixAlgorithm::FrontOnly:
this->build_extract_mix(mask, channels,
SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT);
break;
case DownmixAlgorithm::RearOnly:
this->build_extract_mix(mask, channels,
SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | SPEAKER_BACK_CENTER);
break;
case DownmixAlgorithm::SideOnly:
this->build_extract_mix(mask, channels,
SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT);
break;
case DownmixAlgorithm::Normalize:
this->build_normalize_mix(mask, channels);
break;
case DownmixAlgorithm::AC4:
this->build_ac4_mix(mask, channels);
break;
}
}
void Downmix::process(BYTE *dst, const BYTE *src, UINT32 frames) const {
const int bps = this->bytes_per_sample;
const int src_stride = this->game_frame_size;
const int dst_stride = 2 * bps;
if (dst == nullptr || src == nullptr || bps <= 0) {
return;
}
// sum each speaker's source channels into the matching stereo output
for (UINT32 i = 0; i < frames; i++) {
const BYTE *in = src + (size_t) i * src_stride;
BYTE *out = dst + (size_t) i * dst_stride;
float left = 0.0f;
float right = 0.0f;
for (const auto &c : this->left_mix) {
left += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain;
}
for (const auto &c : this->right_mix) {
right += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain;
}
write_sample(out, bps, this->is_float, left);
write_sample(out + bps, bps, this->is_float, right);
}
}
HRESULT Downmix::get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData) {
const size_t needed = (size_t) frames * this->game_frame_size;
if (this->scratch.size() < needed) {
this->scratch.resize(needed);
}
HRESULT ret = real->GetBuffer(frames, &this->device_buffer);
if (FAILED(ret)) {
this->device_buffer = nullptr;
return ret;
}
*ppData = this->scratch.data();
return S_OK;
}
void Downmix::write_device_buffer(UINT32 frames, DWORD flags) {
const int bps = this->bytes_per_sample;
const int dst_stride = 2 * bps;
if (this->device_buffer == nullptr || frames == 0 || bps <= 0) {
return;
}
// mute the first few buffers to avoid a pop on stream start
if (this->buffers_to_mute > 0) {
memset(this->device_buffer, 0, (size_t) frames * dst_stride);
this->buffers_to_mute--;
} else if ((flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
this->process(this->device_buffer, this->scratch.data(), frames);
}
}
}
@@ -0,0 +1,142 @@
#pragma once
#include <optional>
#include <vector>
#include <windows.h>
#include <mmreg.h>
#include <audioclient.h>
#include "hooks/audio/audio.h"
struct IAudioClient;
struct IAudioRenderClient;
namespace hooks::audio {
// Generic WASAPI surround-to-stereo downmix. The real device is opened in stereo while the
// game keeps writing its native multi-channel audio into a scratch buffer; on release that
// buffer is mixed down into the two front channels.
//
// The mix is derived from the source format's speaker mask according to the selected
// DownmixAlgorithm:
// FrontOnly / RearOnly / SideOnly - keep only that group of channels, routed to their side
// AC4 - AC-4 stereo downmix coefficients (ETSI TS 103 190-1 §6.2.17): front left/right
// pass at 0 dB, center and surrounds fold in at -3 dB, LFE dropped
// Normalize - every channel folded in (center to both sides) with each output side averaged
// so its channels are equally loud, LFE dropped
struct Downmix {
// a source channel routed into one output speaker at the given gain
struct Contribution {
int channel;
float gain;
};
// map an option value (front/rear/side/ac4/normalize) to its algorithm.
static std::optional<DownmixAlgorithm> name_to_algorithm(const char *value) {
if (_stricmp(value, "front") == 0) {
return DownmixAlgorithm::FrontOnly;
} else if (_stricmp(value, "rear") == 0) {
return DownmixAlgorithm::RearOnly;
} else if (_stricmp(value, "side") == 0) {
return DownmixAlgorithm::SideOnly;
} else if (_stricmp(value, "ac4") == 0) {
return DownmixAlgorithm::AC4;
} else if (_stricmp(value, "normalize") == 0) {
return DownmixAlgorithm::Normalize;
}
return std::nullopt;
}
// human-readable name of an algorithm, for logging.
static const char *algorithm_name(DownmixAlgorithm algorithm) {
switch (algorithm) {
case DownmixAlgorithm::FrontOnly: return "front";
case DownmixAlgorithm::RearOnly: return "rear";
case DownmixAlgorithm::SideOnly: return "side";
case DownmixAlgorithm::AC4: return "ac4";
case DownmixAlgorithm::Normalize: return "normalize";
default: return "unknown";
}
}
// whether the downmix is active for the current stream
bool enabled = false;
// algorithm used to fold the multi-channel audio into stereo
DownmixAlgorithm algorithm = DownmixAlgorithm::AC4;
// size in bytes of one frame of the game's multi-channel format
int game_frame_size = 0;
// size in bytes of a single sample (per channel)
int bytes_per_sample = 0;
// whether samples are IEEE floating point rather than integer PCM
bool is_float = false;
// enable the downmix for the given game format and fill stereo_out with the equivalent
// stereo format to open the real device with.
void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out,
DownmixAlgorithm algorithm);
// build the stereo format equivalent to game_format (same sample rate and bit depth).
static void make_stereo_format(const WAVEFORMATEX *game_format,
WAVEFORMATEXTENSIBLE *stereo_out);
// initialize the real device with the stereo format. downmixing reduces the channel count,
// shrinking the buffer's byte size, so the duration the game sized for its multi-channel
// format can leave the smaller stereo buffer unaligned. on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED
// this performs the standard WASAPI realignment and retries.
HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
const WAVEFORMATEX *device_format, LPCGUID session_guid);
// mix `frames` frames of multi-channel `src` down into stereo `dst`.
void process(BYTE *dst, const BYTE *src, UINT32 frames) const;
// grab the real stereo device buffer and hand the game the scratch buffer to write into.
HRESULT get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData);
// mix the scratch buffer into the stereo device buffer held since get_buffer. the caller
// owns releasing the device buffer afterwards (see current_buffer / buffer_released).
void write_device_buffer(UINT32 frames, DWORD flags);
// the real device buffer currently held, or null.
BYTE *current_buffer() const { return this->device_buffer; }
// forget the held device buffer once the caller has released it.
void buffer_released() { this->device_buffer = nullptr; }
private:
// build the mix from the source speaker layout for the selected algorithm
void build_layout_mix(const WAVEFORMATEX *game_format);
// per-algorithm builders, each filling left_mix / right_mix from the speaker mask
void build_ac4_mix(DWORD mask, int channels);
void build_extract_mix(DWORD mask, int channels, DWORD keep);
void build_normalize_mix(DWORD mask, int channels);
// fallback for streams without a speaker mask: fold interleaved L/R pairs at `gain`
void build_pairs_mix(int channels, float gain);
// append one source channel to the output side(s) matching its speaker, at `gain`
void add_channel(int channel, DWORD speaker, float gain);
// source channels summed into each output speaker
std::vector<Contribution> left_mix;
std::vector<Contribution> right_mix;
// buffer the game writes its multi-channel audio into between get/release
std::vector<BYTE> scratch;
// the real stereo device buffer currently held, or null
BYTE *device_buffer = nullptr;
// leading buffers to silence to avoid a pop on stream start
int buffers_to_mute = 16;
};
}
@@ -3,6 +3,7 @@
#include "hooks/audio/audio.h" #include "hooks/audio/audio.h"
#include "hooks/audio/backends/wasapi/audio_client.h" #include "hooks/audio/backends/wasapi/audio_client.h"
#include "hooks/audio/backends/wasapi/defs.h" #include "hooks/audio/backends/wasapi/defs.h"
#include "util/precise_timer.h"
static REFERENCE_TIME WASAPI_TARGET_REFTIME = TARGET_REFTIME; 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) { HRESULT WaveOutBackend::on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) {
bool written = false; bool written = false;
timeutils::PreciseSleepTimer timer;
// reset the dispatcher event // reset the dispatcher event
ResetEvent(this->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 // avoid pegging the CPU
if (!written) { if (!written) {
Sleep(1); timer.sleep(1);
} }
} }
+2 -2
View File
@@ -291,7 +291,7 @@ static int avs_fs_mount(const char *mountpoint, const char *fsroot, const char *
std::filesystem::create_directories(mapped_path, err); std::filesystem::create_directories(mapped_path, err);
if (err) { if (err) {
log_warning("hooks::avs", "failed to create '{}': {}", mapped_path.string(), err.message()); log_warning("hooks::avs", "failed to create '{}': {}", mapped_path, err.message());
} else { } else {
// if this is the `e:\`, then create the special directories // if this is the `e:\`, then create the special directories
@@ -305,7 +305,7 @@ static int avs_fs_mount(const char *mountpoint, const char *fsroot, const char *
log_misc("hooks::avs", "source directory '{}' remapped to '{}'", log_misc("hooks::avs", "source directory '{}' remapped to '{}'",
fsroot, fsroot,
mapped_path.string()); mapped_path);
} }
new_fs_root = mapped_path.string(); new_fs_root = mapped_path.string();
@@ -0,0 +1,275 @@
// dx11 / dxgi hook entrypoint. trampolines d3d11.dll / dxgi.dll exports
// the moment those DLLs appear (LDR notification + poll-thread fallback),
// then drives proactive vtable capture so we don't lose the race against
// the execexe loader. per-vtable hook implementations live in the sibling
// files (d3d11_swapchain / d3d11_factory / d3d11_vtable_capture /
// d3d11_screenshot).
//
// note: never LoadLibrary d3d11/dxgi -- execexe pre-loads them itself and
// fails (error 0xa) if they're already in the loader's module list.
//
// 64-bit only.
#include "d3d11_backend.h"
#ifndef SPICE_D3D11
void graphics_d3d11_init() {}
void graphics_d3d11_shutdown() {}
#else
#include <atomic>
#include <thread>
#include <chrono>
#include <cwchar>
#include <mutex>
#include <windows.h>
#include <d3d11.h>
#include <dxgi.h>
#include <dxgi1_2.h>
#include "d3d11_internal.h"
#include "util/nt_loader.h"
namespace {
using D3D11CreateDeviceAndSwapChain_t = HRESULT(WINAPI *)(
IDXGIAdapter *, D3D_DRIVER_TYPE, HMODULE, UINT,
const D3D_FEATURE_LEVEL *, UINT, UINT,
const DXGI_SWAP_CHAIN_DESC *, IDXGISwapChain **,
ID3D11Device **, D3D_FEATURE_LEVEL *, ID3D11DeviceContext **);
using CreateDXGIFactory_t = HRESULT(WINAPI *)(REFIID, void **);
using CreateDXGIFactory1_t = HRESULT(WINAPI *)(REFIID, void **);
using CreateDXGIFactory2_t = HRESULT(WINAPI *)(UINT, REFIID, void **);
D3D11CreateDeviceAndSwapChain_t D3D11CreateDeviceAndSwapChain_orig = nullptr;
CreateDXGIFactory_t CreateDXGIFactory_orig = nullptr;
CreateDXGIFactory1_t CreateDXGIFactory1_orig = nullptr;
CreateDXGIFactory2_t CreateDXGIFactory2_orig = nullptr;
std::atomic<bool> g_d3d11_exports_hooked { false };
std::atomic<bool> g_dxgi_exports_hooked { false };
// ----------------------------------------------------------------------
// top-level export hooks
HRESULT WINAPI D3D11CreateDeviceAndSwapChain_hook(
IDXGIAdapter *pAdapter, D3D_DRIVER_TYPE DriverType, HMODULE Software, UINT Flags,
const D3D_FEATURE_LEVEL *pFeatureLevels, UINT FeatureLevels, UINT SDKVersion,
const DXGI_SWAP_CHAIN_DESC *pSwapChainDesc, IDXGISwapChain **ppSwapChain,
ID3D11Device **ppDevice, D3D_FEATURE_LEVEL *pFeatureLevel,
ID3D11DeviceContext **ppImmediateContext)
{
HRESULT res = D3D11CreateDeviceAndSwapChain_orig(
pAdapter, DriverType, Software, Flags,
pFeatureLevels, FeatureLevels, SDKVersion,
pSwapChainDesc, ppSwapChain, ppDevice, pFeatureLevel, ppImmediateContext);
if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) {
if (pSwapChainDesc) {
d3d11_hooks::note_main_hwnd(pSwapChainDesc->OutputWindow);
}
d3d11_hooks::install_swapchain_hooks(*ppSwapChain);
}
return res;
}
#define DEFINE_FACTORY_HOOK(NAME, SIG_PARAMS, ORIG_ARGS) \
HRESULT WINAPI NAME##_hook SIG_PARAMS { \
HRESULT res = NAME##_orig ORIG_ARGS; \
if (SUCCEEDED(res) && ppFactory && *ppFactory) { \
d3d11_hooks::install_factory_hooks( \
reinterpret_cast<IUnknown *>(*ppFactory)); \
} \
return res; \
}
DEFINE_FACTORY_HOOK(CreateDXGIFactory,
(REFIID riid, void **ppFactory),
(riid, ppFactory))
DEFINE_FACTORY_HOOK(CreateDXGIFactory1,
(REFIID riid, void **ppFactory),
(riid, ppFactory))
DEFINE_FACTORY_HOOK(CreateDXGIFactory2,
(UINT Flags, REFIID riid, void **ppFactory),
(Flags, riid, ppFactory))
#undef DEFINE_FACTORY_HOOK
// ----------------------------------------------------------------------
// export trampoline plumbing
// serializes trampoline_export() so the LDR notification callback and the
// poll thread don't race each other into MinHook against the same target.
std::mutex g_export_mutex;
bool trampoline_export(const char *dll, const char *name, void *hook, void **orig) {
std::lock_guard<std::mutex> lock(g_export_mutex);
if (*orig) {
return true;
}
HMODULE mod = GetModuleHandleA(dll);
if (!mod) {
return false;
}
void *addr = reinterpret_cast<void *>(GetProcAddress(mod, name));
if (!addr) {
return false;
}
*orig = addr; // trampoline_try reads *orig before overwriting it.
if (!detour::trampoline_try(addr, hook, orig)) {
*orig = nullptr;
return false;
}
log_info("graphics::d3d11", "trampolined {}!{}", dll, name);
return true;
}
void try_install_d3d11_exports() {
if (g_d3d11_exports_hooked) {
return;
}
if (trampoline_export("d3d11.dll", "D3D11CreateDeviceAndSwapChain",
(void *) D3D11CreateDeviceAndSwapChain_hook,
(void **) &D3D11CreateDeviceAndSwapChain_orig)) {
g_d3d11_exports_hooked = true;
}
}
void try_install_dxgi_exports() {
if (g_dxgi_exports_hooked) {
return;
}
struct entry { const char *name; void *hook; void **orig; };
const entry entries[] = {
{ "CreateDXGIFactory", (void *) CreateDXGIFactory_hook,
(void **) &CreateDXGIFactory_orig },
{ "CreateDXGIFactory1", (void *) CreateDXGIFactory1_hook,
(void **) &CreateDXGIFactory1_orig },
{ "CreateDXGIFactory2", (void *) CreateDXGIFactory2_hook,
(void **) &CreateDXGIFactory2_orig },
};
bool any = false;
for (auto &e : entries) {
any |= trampoline_export("dxgi.dll", e.name, e.hook, e.orig);
}
if (any) {
g_dxgi_exports_hooked = true;
}
}
void try_capture_if_ready() {
if (g_d3d11_exports_hooked && g_dxgi_exports_hooked) {
d3d11_hooks::try_capture_vtables();
}
}
// ----------------------------------------------------------------------
// LDR notification + polling fallback
bool dll_name_ends_with(PCUNICODE_STRING name, const wchar_t *suffix) {
if (!name || !name->Buffer) {
return false;
}
const size_t n = name->Length / sizeof(WCHAR);
const size_t s = wcslen(suffix);
return n >= s && _wcsnicmp(name->Buffer + n - s, suffix, s) == 0;
}
VOID CALLBACK ldr_dll_notification(
ULONG reason, PCLDR_DLL_NOTIFICATION_DATA data, PVOID /*context*/)
{
if (reason != LDR_DLL_NOTIFICATION_REASON_LOADED || !data) {
return;
}
if (dll_name_ends_with(data->Loaded.BaseDllName, L"d3d11.dll")) {
try_install_d3d11_exports();
} else if (dll_name_ends_with(data->Loaded.BaseDllName, L"dxgi.dll")) {
try_install_dxgi_exports();
}
}
// execexe maps d3d11/dxgi via a path that bypasses LdrLoadDll, so the
// notification above never fires for those DLLs and we have to poll.
std::atomic<bool> g_stop { false };
std::thread g_poll_thread;
std::mutex g_init_mutex;
PVOID g_ldr_cookie = nullptr;
void poll_thread() {
using namespace std::chrono_literals;
for (int32_t i = 0; i < 120 && !g_stop.load(); ++i) {
try_install_d3d11_exports();
try_install_dxgi_exports();
if (g_d3d11_exports_hooked && g_dxgi_exports_hooked) {
d3d11_hooks::try_capture_vtables();
return;
}
// sliced so shutdown doesn't have to wait a full second.
for (int32_t s = 0; s < 10 && !g_stop.load(); ++s) {
std::this_thread::sleep_for(100ms);
}
}
}
} // namespace
void graphics_d3d11_init() {
// dx11 titles always run under execexe. skipping on pure-dx9 games keeps
// their startup path completely untouched (no exports patched, no poll
// thread, no LDR callback).
if (!GetModuleHandleW(L"execexe.dll")) {
return;
}
std::lock_guard<std::mutex> lock(g_init_mutex);
if (g_poll_thread.joinable()) {
return; // already initialized
}
log_info("graphics::d3d11", "initializing");
// trampoline now if either DLL is already in the PEB.
try_install_d3d11_exports();
try_install_dxgi_exports();
try_capture_if_ready();
// catches standard LdrLoadDll loads.
auto reg = reinterpret_cast<decltype(&LdrRegisterDllNotification)>(
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrRegisterDllNotification"));
if (reg) {
NTSTATUS st = reg(0, ldr_dll_notification, nullptr, &g_ldr_cookie);
if (NT_SUCCESS(st)) {
log_info("graphics::d3d11", "registered LDR DLL notification");
} else {
g_ldr_cookie = nullptr;
log_warning("graphics::d3d11",
"LdrRegisterDllNotification failed: {:#x}", (unsigned long)st);
}
}
// catches the execexe loader path that bypasses LdrLoadDll.
g_poll_thread = std::thread(poll_thread);
}
void graphics_d3d11_shutdown() {
std::lock_guard<std::mutex> lock(g_init_mutex);
// unregister first so the callback can't fire mid-teardown.
if (g_ldr_cookie) {
auto unreg = reinterpret_cast<decltype(&LdrUnregisterDllNotification)>(
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrUnregisterDllNotification"));
if (unreg) {
unreg(g_ldr_cookie);
}
g_ldr_cookie = nullptr;
}
g_stop.store(true);
if (g_poll_thread.joinable()) {
g_poll_thread.join();
}
}
#endif // SPICE_D3D11
@@ -0,0 +1,24 @@
#pragma once
#include "overlay/overlay.h"
void graphics_d3d11_init();
void graphics_d3d11_shutdown();
#ifdef SPICE_D3D11
struct ID3D11Device;
struct ID3D11DeviceContext;
struct ID3D11RenderTargetView;
struct IDXGISwapChain;
namespace overlay::d3d11 {
void render(ID3D11Device *device,
ID3D11DeviceContext *context,
IDXGISwapChain *swapchain,
ID3D11RenderTargetView **rtv);
}
#endif
@@ -0,0 +1,102 @@
// dx11 factory vtable hooks. patches CreateSwapChain / CreateSwapChainForHwnd
// so we can install_swapchain_hooks against every newly-created swapchain.
#include "d3d11_backend.h"
#ifdef SPICE_D3D11
#include <mutex>
#include <windows.h>
#include <d3d11.h>
#include <dxgi.h>
#include <dxgi1_2.h>
#include "d3d11_internal.h"
namespace {
using CreateSwapChain_t = HRESULT(STDMETHODCALLTYPE *)(
IDXGIFactory *, IUnknown *, DXGI_SWAP_CHAIN_DESC *, IDXGISwapChain **);
using CreateSwapChainForHwnd_t = HRESULT(STDMETHODCALLTYPE *)(
IDXGIFactory2 *, IUnknown *, HWND,
const DXGI_SWAP_CHAIN_DESC1 *,
const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *,
IDXGIOutput *, IDXGISwapChain1 **);
CreateSwapChain_t CreateSwapChain_orig = nullptr;
CreateSwapChainForHwnd_t CreateSwapChainForHwnd_orig = nullptr;
bool g_factory_hooked = false;
bool g_factory2_hooked = false;
std::mutex g_hook_mutex;
HRESULT STDMETHODCALLTYPE CreateSwapChain_hook(
IDXGIFactory *factory, IUnknown *pDevice,
DXGI_SWAP_CHAIN_DESC *pDesc, IDXGISwapChain **ppSwapChain)
{
HRESULT res = CreateSwapChain_orig(factory, pDevice, pDesc, ppSwapChain);
if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) {
if (pDesc) {
d3d11_hooks::note_main_hwnd(pDesc->OutputWindow);
}
d3d11_hooks::install_swapchain_hooks(*ppSwapChain);
}
return res;
}
HRESULT STDMETHODCALLTYPE CreateSwapChainForHwnd_hook(
IDXGIFactory2 *factory, IUnknown *pDevice, HWND hWnd,
const DXGI_SWAP_CHAIN_DESC1 *pDesc,
const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *pFullscreenDesc,
IDXGIOutput *pRestrictToOutput, IDXGISwapChain1 **ppSwapChain)
{
HRESULT res = CreateSwapChainForHwnd_orig(
factory, pDevice, hWnd, pDesc, pFullscreenDesc, pRestrictToOutput, ppSwapChain);
if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) {
d3d11_hooks::note_main_hwnd(hWnd);
d3d11_hooks::install_swapchain_hooks(*ppSwapChain);
}
return res;
}
// QI-and-hook helper: dedupes the IDXGIFactory / IDXGIFactory2 install paths.
template<typename Iface>
void install_on(IUnknown *factory, bool &flag,
size_t vtbl_index, void *hook, void **orig, const char *name)
{
if (flag) {
return;
}
Iface *f = nullptr;
if (FAILED(factory->QueryInterface(IID_PPV_ARGS(&f))) || !f) {
return;
}
if (d3d11_hooks::hook_vtbl(f, vtbl_index, hook, orig, name)) {
flag = true;
}
f->Release();
}
} // namespace
namespace d3d11_hooks {
void install_factory_hooks(IUnknown *factory) {
if (!factory) {
return;
}
std::lock_guard<std::mutex> lock(g_hook_mutex);
install_on<IDXGIFactory>(factory, g_factory_hooked, 10,
(void *) CreateSwapChain_hook, (void **) &CreateSwapChain_orig,
"IDXGIFactory::CreateSwapChain");
install_on<IDXGIFactory2>(factory, g_factory2_hooked, 15,
(void *) CreateSwapChainForHwnd_hook, (void **) &CreateSwapChainForHwnd_orig,
"IDXGIFactory2::CreateSwapChainForHwnd");
}
}
#endif // SPICE_D3D11
@@ -0,0 +1,59 @@
#pragma once
// internal glue for the dx11 backend. all symbols gated on SPICE_D3D11.
#include "overlay/overlay.h"
#ifdef SPICE_D3D11
#include <memory>
#include "util/detour.h"
#include "util/logging.h"
struct HWND__; typedef HWND__ *HWND;
struct IUnknown;
struct IDXGISwapChain;
namespace d3d11_hooks {
void install_swapchain_hooks(IDXGISwapChain *swapchain);
void install_factory_hooks(IUnknown *factory);
void try_capture_vtables();
// first non-null swapchain HWND wins; later ones (sub-screens, IME
// helpers) are ignored. the dummy capture window is exempted via
// ignore_hwnd.
void note_main_hwnd(HWND hwnd);
HWND main_hwnd();
void ignore_hwnd(HWND hwnd);
// capture backbuffer to PNG if a screenshot was requested.
void try_screenshot(IDXGISwapChain *swapchain);
// trampoline a virtual method by vtable index. on failure *orig is null.
inline bool hook_vtbl(void *iface, size_t index,
void *hook, void **orig, const char *name)
{
void **vtbl = *reinterpret_cast<void ***>(iface);
void *target = vtbl[index];
// trampoline_try reads *orig before overwriting it.
*orig = target;
if (!detour::trampoline_try(target, hook, orig)) {
*orig = nullptr;
log_warning("graphics::d3d11", "failed to hook {}", name);
return false;
}
log_info("graphics::d3d11", "hooked {}", name);
return true;
}
// minimal COM RAII used by capture / screenshot paths.
struct com_release {
void operator()(IUnknown *p) const { if (p) p->Release(); }
};
template<typename T> using com_ptr = std::unique_ptr<T, com_release>;
}
#endif
@@ -0,0 +1,165 @@
// dx11 screenshot capture. mirrors the d3d9 backend: copy the current
// backbuffer into a staging texture, force alpha=255, write PNG via
// stb_image_write, push to clipboard and notify.
#include "d3d11_backend.h"
#ifdef SPICE_D3D11
#include <vector>
#include <windows.h>
#include <d3d11.h>
#include <dxgi.h>
#include "d3d11_internal.h"
#include "external/stb_image_write.h"
#include "hooks/graphics/graphics.h"
#include "misc/clipboard.h"
#include "overlay/notifications.h"
#include "util/fileutils.h"
using d3d11_hooks::com_ptr;
namespace {
// copy the swapchain backbuffer into a CPU-readable staging texture and
// flatten it into an RGBA8 buffer (BGRA backbuffers are swizzled,
// alpha is forced to 255).
bool copy_backbuffer_to_rgba(IDXGISwapChain *swapchain,
ID3D11Device *device,
ID3D11DeviceContext *context,
std::vector<uint8_t> &out,
uint32_t &out_w, uint32_t &out_h)
{
ID3D11Texture2D *raw_bb = nullptr;
if (FAILED(swapchain->GetBuffer(0, IID_PPV_ARGS(&raw_bb))) || !raw_bb) {
return false;
}
com_ptr<ID3D11Texture2D> backbuffer(raw_bb);
D3D11_TEXTURE2D_DESC desc {};
backbuffer->GetDesc(&desc);
// MSAA backbuffers can't be CopyResource'd into a non-MS staging target.
com_ptr<ID3D11Texture2D> resolved;
ID3D11Texture2D *source = backbuffer.get();
if (desc.SampleDesc.Count > 1) {
D3D11_TEXTURE2D_DESC rd = desc;
rd.SampleDesc.Count = 1;
rd.SampleDesc.Quality = 0;
rd.Usage = D3D11_USAGE_DEFAULT;
rd.BindFlags = D3D11_BIND_RENDER_TARGET;
rd.CPUAccessFlags = 0;
rd.MiscFlags = 0;
ID3D11Texture2D *r = nullptr;
if (FAILED(device->CreateTexture2D(&rd, nullptr, &r)) || !r) {
return false;
}
resolved.reset(r);
context->ResolveSubresource(resolved.get(), 0, backbuffer.get(), 0, desc.Format);
source = resolved.get();
}
D3D11_TEXTURE2D_DESC sd {};
sd.Width = desc.Width;
sd.Height = desc.Height;
sd.MipLevels = 1;
sd.ArraySize = 1;
sd.Format = desc.Format;
sd.SampleDesc.Count = 1;
sd.Usage = D3D11_USAGE_STAGING;
sd.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
ID3D11Texture2D *raw_staging = nullptr;
if (FAILED(device->CreateTexture2D(&sd, nullptr, &raw_staging)) || !raw_staging) {
return false;
}
com_ptr<ID3D11Texture2D> staging(raw_staging);
context->CopyResource(staging.get(), source);
D3D11_MAPPED_SUBRESOURCE mapped {};
if (FAILED(context->Map(staging.get(), 0, D3D11_MAP_READ, 0, &mapped))) {
return false;
}
// backbuffers from GetDesc are always fully-typed (never _TYPELESS).
const bool is_bgra = desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM
|| desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM_SRGB;
out.resize(static_cast<size_t>(desc.Width) * desc.Height * 4);
const uint8_t *src_base = reinterpret_cast<const uint8_t *>(mapped.pData);
for (uint32_t y = 0; y < desc.Height; ++y) {
const uint8_t *row = src_base + static_cast<size_t>(y) * mapped.RowPitch;
uint8_t *dst = out.data() + static_cast<size_t>(y) * desc.Width * 4;
for (uint32_t x = 0; x < desc.Width; ++x) {
dst[x * 4 + 0] = row[x * 4 + (is_bgra ? 2 : 0)];
dst[x * 4 + 1] = row[x * 4 + 1];
dst[x * 4 + 2] = row[x * 4 + (is_bgra ? 0 : 2)];
dst[x * 4 + 3] = 255;
}
}
context->Unmap(staging.get(), 0);
out_w = desc.Width;
out_h = desc.Height;
return true;
}
} // namespace
namespace d3d11_hooks {
void try_screenshot(IDXGISwapChain *swapchain) {
if (!swapchain || !graphics_screenshot_consume()) {
return;
}
auto file_path = graphics_screenshot_genpath();
if (file_path.empty()) {
return;
}
ID3D11Device *raw_device = nullptr;
if (FAILED(swapchain->GetDevice(IID_PPV_ARGS(&raw_device))) || !raw_device) {
return;
}
com_ptr<ID3D11Device> device(raw_device);
ID3D11DeviceContext *raw_ctx = nullptr;
device->GetImmediateContext(&raw_ctx);
if (!raw_ctx) {
return;
}
com_ptr<ID3D11DeviceContext> context(raw_ctx);
std::vector<uint8_t> pixels;
uint32_t w = 0, h = 0;
if (!copy_backbuffer_to_rgba(swapchain, device.get(), context.get(), pixels, w, h)) {
log_warning("graphics::d3d11", "screenshot: failed to capture backbuffer");
overlay::notifications::add(
overlay::notifications::Severity::Error,
"Screenshot failed to capture");
return;
}
log_info("graphics::d3d11", "saving screenshot to {}", file_path);
if (stbi_write_png(file_path.c_str(), (int) w, (int) h, 4,
pixels.data(), (int) w * 4))
{
clipboard::copy_image(file_path);
overlay::notifications::add(
overlay::notifications::Severity::Success,
fmt::format("Screenshot saved: {}", fileutils::basename(file_path)));
} else {
log_warning("graphics::d3d11", "screenshot: stbi_write_png failed");
overlay::notifications::add(
overlay::notifications::Severity::Error,
"Screenshot failed to save");
}
}
}
#endif // SPICE_D3D11
@@ -0,0 +1,289 @@
// dx11 swapchain vtable hooks + per-frame overlay pump.
//
// dxgi shares vtables across swapchain instances, so we only need to patch
// Present / Present1 / ResizeBuffers once on the first instance we see.
// each frame we lazily attach the overlay to whichever swapchain is
// presenting, then drive its imgui update / new_frame / render cycle.
#include "d3d11_backend.h"
#ifdef SPICE_D3D11
#include <atomic>
#include <mutex>
#include <windows.h>
#include <d3d11.h>
#include <dxgi.h>
#include <dxgi1_2.h>
#include "d3d11_internal.h"
#include "external/imgui/imgui.h"
#include "external/imgui/backends/imgui_impl_dx11.h"
#include "overlay/imgui/impl_spice.h"
#include "games/io.h"
#include "hooks/graphics/graphics.h"
#include "launcher/launcher.h"
#include "misc/eamuse.h"
// --------------------------------------------------------------------------
// overlay render bridge
namespace overlay::d3d11 {
// sRGB backbuffers need a UNORM view: ImGui vertex colors are already
// sRGB-encoded, so an extra linear->sRGB conversion would wash the
// overlay out white.
static DXGI_FORMAT to_unorm_view(DXGI_FORMAT fmt) {
switch (fmt) {
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM;
case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM;
default: return fmt;
}
}
static void ensure_rtv(ID3D11Device *device,
IDXGISwapChain *swapchain,
ID3D11RenderTargetView **rtv)
{
if (*rtv || !device || !swapchain) {
return;
}
ID3D11Texture2D *backbuffer = nullptr;
if (FAILED(swapchain->GetBuffer(0, IID_PPV_ARGS(&backbuffer))) || !backbuffer) {
return;
}
D3D11_TEXTURE2D_DESC td {};
backbuffer->GetDesc(&td);
const DXGI_FORMAT view_fmt = to_unorm_view(td.Format);
if (view_fmt != td.Format) {
D3D11_RENDER_TARGET_VIEW_DESC rtvd {};
rtvd.Format = view_fmt;
rtvd.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
device->CreateRenderTargetView(backbuffer, &rtvd, rtv);
} else {
device->CreateRenderTargetView(backbuffer, nullptr, rtv);
}
backbuffer->Release();
}
// bind the backbuffer (lazily creating the RTV) and draw the imgui
// frame on top. reset_invalidate releases *rtv on ResizeBuffers.
void render(ID3D11Device *device,
ID3D11DeviceContext *context,
IDXGISwapChain *swapchain,
ID3D11RenderTargetView **rtv)
{
ensure_rtv(device, swapchain, rtv);
if (!*rtv || !context) {
return;
}
// present happens immediately after, so no need to save the previous
// RT binding (flip-model resets it anyway).
context->OMSetRenderTargets(1, rtv, nullptr);
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
}
}
// --------------------------------------------------------------------------
// file-local state + per-frame helpers
namespace {
using Present_t = HRESULT(STDMETHODCALLTYPE *)(
IDXGISwapChain *, UINT, UINT);
using ResizeBuffers_t = HRESULT(STDMETHODCALLTYPE *)(
IDXGISwapChain *, UINT, UINT, UINT, DXGI_FORMAT, UINT);
using Present1_t = HRESULT(STDMETHODCALLTYPE *)(
IDXGISwapChain1 *, UINT, UINT, const DXGI_PRESENT_PARAMETERS *);
Present_t Present_orig = nullptr;
ResizeBuffers_t ResizeBuffers_orig = nullptr;
Present1_t Present1_orig = nullptr;
bool g_swapchain_hooked = false;
bool g_swapchain1_hooked = false;
void try_create_overlay(IDXGISwapChain *swapchain) {
if (!swapchain || overlay::OVERLAY) {
return;
}
DXGI_SWAP_CHAIN_DESC desc {};
if (FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
return;
}
// only attach to the main game window; ignore sub-screens / IME helpers.
HWND main = d3d11_hooks::main_hwnd();
if (main && desc.OutputWindow != main) {
return;
}
ID3D11Device *device = nullptr;
if (FAILED(swapchain->GetDevice(IID_PPV_ARGS(&device))) || !device) {
return;
}
ID3D11DeviceContext *context = nullptr;
device->GetImmediateContext(&context);
if (context) {
overlay::create_d3d11(desc.OutputWindow, device, context, swapchain);
RECT cr {};
::GetClientRect(desc.OutputWindow, &cr);
log_info("graphics::d3d11",
"attached overlay to swapchain hwnd=0x{:x} backbuffer={}x{} client={}x{}",
(uintptr_t) desc.OutputWindow,
desc.BufferDesc.Width, desc.BufferDesc.Height,
cr.right - cr.left, cr.bottom - cr.top);
context->Release();
}
device->Release();
}
// rising-edge screenshot hotkey poll (mirrors d3d9 backend behaviour).
void poll_screenshot_hotkey() {
static bool s_down = false;
auto buttons = games::get_buttons_overlay(eamuse_get_game());
const bool pressed = buttons
&& (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered())
&& GameAPI::Buttons::getState(RI_MGR,
buttons->at(games::OverlayButtons::Screenshot));
if (pressed && !s_down) {
graphics_screenshot_trigger();
}
s_down = pressed;
}
void pump_overlay(IDXGISwapChain *swapchain) {
if (!overlay::OVERLAY || !overlay::OVERLAY->uses_swapchain(swapchain)) {
return;
}
poll_screenshot_hotkey();
// size imgui to the backbuffer (not window client). dxgi may upscale
// a small backbuffer into a larger client rect; without this override
// imgui would draw past the RTV and the mouse mapping would be off.
DXGI_SWAP_CHAIN_DESC desc {};
if (SUCCEEDED(swapchain->GetDesc(&desc))) {
ImGui_ImplSpice_SetDisplaySizeOverride(
(float) desc.BufferDesc.Width,
(float) desc.BufferDesc.Height);
}
overlay::OVERLAY->update();
overlay::OVERLAY->new_frame();
overlay::OVERLAY->render();
// after overlay render so toasts/menus end up in the saved image.
d3d11_hooks::try_screenshot(swapchain);
}
// ----------------------------------------------------------------------
// swapchain method hooks
HRESULT STDMETHODCALLTYPE Present_hook(
IDXGISwapChain *swapchain, UINT SyncInterval, UINT Flags)
{
try_create_overlay(swapchain);
pump_overlay(swapchain);
return Present_orig(swapchain, SyncInterval, Flags);
}
HRESULT STDMETHODCALLTYPE Present1_hook(
IDXGISwapChain1 *swapchain, UINT SyncInterval, UINT Flags,
const DXGI_PRESENT_PARAMETERS *pParams)
{
try_create_overlay(swapchain);
pump_overlay(swapchain);
return Present1_orig(swapchain, SyncInterval, Flags, pParams);
}
HRESULT STDMETHODCALLTYPE ResizeBuffers_hook(
IDXGISwapChain *swapchain, UINT BufferCount, UINT Width, UINT Height,
DXGI_FORMAT NewFormat, UINT SwapChainFlags)
{
const bool ours = overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain);
if (ours) {
log_info("graphics::d3d11", "ResizeBuffers {}x{} fmt={}",
Width, Height, (int32_t) NewFormat);
overlay::OVERLAY->reset_invalidate();
}
HRESULT res = ResizeBuffers_orig(
swapchain, BufferCount, Width, Height, NewFormat, SwapChainFlags);
if (ours && SUCCEEDED(res)) {
overlay::OVERLAY->reset_recreate();
}
return res;
}
} // namespace
// --------------------------------------------------------------------------
// d3d11_hooks public surface: main-window tracking + vtable install.
namespace d3d11_hooks {
namespace {
std::atomic<HWND> g_main_hwnd { nullptr };
std::atomic<HWND> g_ignored_hwnd { nullptr };
}
void note_main_hwnd(HWND hwnd) {
if (!hwnd || hwnd == g_ignored_hwnd.load()) {
return;
}
HWND expected = nullptr;
if (g_main_hwnd.compare_exchange_strong(expected, hwnd)) {
log_info("graphics::d3d11", "main hwnd recorded: 0x{:x}",
(uintptr_t) hwnd);
}
}
HWND main_hwnd() {
return g_main_hwnd.load();
}
void ignore_hwnd(HWND hwnd) {
g_ignored_hwnd.store(hwnd);
}
// patch IDXGISwapChain::Present + ResizeBuffers and (if implemented)
// IDXGISwapChain1::Present1. idempotent; flag is set only after success
// so failed attempts can be retried on the next swapchain.
void install_swapchain_hooks(IDXGISwapChain *swapchain) {
if (!swapchain) {
return;
}
static std::mutex s_hook_mutex;
std::lock_guard<std::mutex> lock(s_hook_mutex);
if (!g_swapchain_hooked) {
const bool a = hook_vtbl(swapchain, 8, (void *) Present_hook,
(void **) &Present_orig, "IDXGISwapChain::Present");
const bool b = hook_vtbl(swapchain, 13, (void *) ResizeBuffers_hook,
(void **) &ResizeBuffers_orig, "IDXGISwapChain::ResizeBuffers");
if (a && b) {
g_swapchain_hooked = true;
}
}
if (!g_swapchain1_hooked) {
IDXGISwapChain1 *sc1 = nullptr;
if (SUCCEEDED(swapchain->QueryInterface(IID_PPV_ARGS(&sc1))) && sc1) {
if (hook_vtbl(sc1, 22, (void *) Present1_hook,
(void **) &Present1_orig, "IDXGISwapChain1::Present1")) {
g_swapchain1_hooked = true;
}
sc1->Release();
}
}
}
}
#endif // SPICE_D3D11
@@ -0,0 +1,175 @@
// proactive vtable capture for the dx11 backend.
//
// titles under the execexe loader routinely race past our export-level
// trampolines, so the game's first real swapchain never goes through us.
// we sidestep that by creating a throwaway device + swapchain ourselves
// the moment d3d11.dll + dxgi.dll appear, which patches the shared
// IDXGISwapChain[1] / IDXGIFactory[2] vtables ahead of the game.
#include "d3d11_backend.h"
#ifdef SPICE_D3D11
#include <atomic>
#include <memory>
#include <windows.h>
#include <d3d11.h>
#include <dxgi.h>
#include <dxgi1_2.h>
#include "d3d11_internal.h"
using d3d11_hooks::com_ptr;
namespace {
using D3D11CreateDevice_t = HRESULT(WINAPI *)(
IDXGIAdapter *, D3D_DRIVER_TYPE, HMODULE, UINT,
const D3D_FEATURE_LEVEL *, UINT, UINT,
ID3D11Device **, D3D_FEATURE_LEVEL *, ID3D11DeviceContext **);
using CreateDXGIFactory1_t = HRESULT(WINAPI *)(REFIID, void **);
using CreateDXGIFactory2_t = HRESULT(WINAPI *)(UINT, REFIID, void **);
std::atomic<bool> g_vtables_captured { false };
template<typename Fn>
Fn resolve(HMODULE mod, const char *name) {
return reinterpret_cast<Fn>(GetProcAddress(mod, name));
}
com_ptr<IDXGIFactory2> create_factory2(CreateDXGIFactory2_t f2,
CreateDXGIFactory1_t f1)
{
IDXGIFactory2 *raw = nullptr;
if (f2 && SUCCEEDED(f2(0, IID_PPV_ARGS(&raw))) && raw) {
return com_ptr<IDXGIFactory2>(raw);
}
IDXGIFactory1 *factory1 = nullptr;
if (f1 && SUCCEEDED(f1(IID_PPV_ARGS(&factory1))) && factory1) {
factory1->QueryInterface(IID_PPV_ARGS(&raw));
factory1->Release();
}
return com_ptr<IDXGIFactory2>(raw);
}
bool create_dummy_device(D3D11CreateDevice_t create,
com_ptr<ID3D11Device> &device,
com_ptr<ID3D11DeviceContext> &context)
{
static constexpr D3D_FEATURE_LEVEL levels[] = {
D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0,
};
// hardware first, then WARP so headless / unusual configs still work.
for (auto type : { D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP }) {
ID3D11Device *d = nullptr;
ID3D11DeviceContext *c = nullptr;
D3D_FEATURE_LEVEL got;
if (SUCCEEDED(create(nullptr, type, nullptr, 0,
levels, ARRAYSIZE(levels), D3D11_SDK_VERSION,
&d, &got, &c)) && d) {
device.reset(d);
context.reset(c);
return true;
}
}
return false;
}
} // namespace
namespace d3d11_hooks {
// create a throwaway device + swapchain to patch the shared vtables before
// the game's loader races past our export trampolines. safe to call
// repeatedly; runs at most once.
void try_capture_vtables() {
if (g_vtables_captured.load()) {
return;
}
HMODULE d3d11 = GetModuleHandleW(L"d3d11.dll");
HMODULE dxgi = GetModuleHandleW(L"dxgi.dll");
if (!d3d11 || !dxgi) {
return;
}
auto create_device = resolve<D3D11CreateDevice_t>(d3d11, "D3D11CreateDevice");
auto f2 = resolve<CreateDXGIFactory2_t>(dxgi, "CreateDXGIFactory2");
auto f1 = resolve<CreateDXGIFactory1_t>(dxgi, "CreateDXGIFactory1");
if (!create_device || (!f1 && !f2)) {
return;
}
// serialize concurrent calls (poll thread + LDR notification). only
// flip g_vtables_captured after success so failed attempts remain
// retriable on the next tick.
static std::atomic<bool> in_progress { false };
if (in_progress.exchange(true)) {
return;
}
struct scope_clear {
std::atomic<bool> &flag;
~scope_clear() { flag.store(false); }
} clear { in_progress };
// hidden message-only window; STATIC is always registered by user32.
HWND dummy_hwnd = CreateWindowExW(
0, L"STATIC", L"", 0, 0, 0, 1, 1,
HWND_MESSAGE, nullptr, GetModuleHandleW(nullptr), nullptr);
if (!dummy_hwnd) {
log_warning("graphics::d3d11",
"vtable capture: CreateWindowExW failed (gle={})", (unsigned long)GetLastError());
return;
}
auto destroy_hwnd = std::unique_ptr<HWND__, decltype(&DestroyWindow)>(
dummy_hwnd, &DestroyWindow);
// if the game's CreateDXGIFactory_hook already raced us, our
// CreateSwapChainForHwnd call below would trip the hook and try to
// record dummy_hwnd as the main window. block that.
ignore_hwnd(dummy_hwnd);
auto factory2 = create_factory2(f2, f1);
if (!factory2) {
log_warning("graphics::d3d11", "vtable capture: CreateDXGIFactory* failed");
return;
}
com_ptr<ID3D11Device> device;
com_ptr<ID3D11DeviceContext> context;
if (!create_dummy_device(create_device, device, context)) {
log_warning("graphics::d3d11", "vtable capture: D3D11CreateDevice failed");
return;
}
DXGI_SWAP_CHAIN_DESC1 desc {};
desc.Width = 1;
desc.Height = 1;
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
desc.SampleDesc.Count = 1;
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
desc.BufferCount = 2;
desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
IDXGISwapChain1 *raw_sc = nullptr;
HRESULT hr = factory2->CreateSwapChainForHwnd(
device.get(), dummy_hwnd, &desc, nullptr, nullptr, &raw_sc);
if (FAILED(hr) || !raw_sc) {
log_warning("graphics::d3d11",
"vtable capture: CreateSwapChainForHwnd failed (hr={:#x})", (unsigned long)hr);
return;
}
com_ptr<IDXGISwapChain1> swapchain(raw_sc);
install_swapchain_hooks(swapchain.get());
install_factory_hooks(factory2.get());
g_vtables_captured.store(true);
log_info("graphics::d3d11", "vtable capture complete (via dummy swapchain)");
}
}
#endif // SPICE_D3D11
@@ -28,8 +28,10 @@
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "misc/wintouchemu.h" #include "misc/wintouchemu.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "overlay/notifications.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/deferlog.h" #include "util/deferlog.h"
#include "util/fileutils.h"
#include "util/flags_helper.h" #include "util/flags_helper.h"
#include "util/libutils.h" #include "util/libutils.h"
#include "util/logging.h" #include "util/logging.h"
@@ -465,15 +467,21 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::RegisterSoftwareDevice(void *pIniti
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterCount() { UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterCount() {
UINT result = pReal->GetAdapterCount(); UINT result = pReal->GetAdapterCount();
if (!FAKE_SUBSCREEN_ADAPTER && games::popn::is_pikapika_model() && result == 1) { if (!FAKE_SUBSCREEN_ADAPTER) {
FAKE_SUBSCREEN_ADAPTER = true; if (games::popn::is_pikapika_model() && result == 1) {
log_info( FAKE_SUBSCREEN_ADAPTER = true;
"graphics::d3d9", log_info(
"GetAdapterCount returned 1, popn needs 2 adapters - enabliing fake submonitor mode"); "graphics::d3d9",
} "GetAdapterCount returned {}, popn needs 2 adapters - enabling fake submonitor mode", result);
return 2;
if (FAKE_SUBSCREEN_ADAPTER) { } else if (games::gitadora::is_arena_model() && !GRAPHICS_WINDOWED && result < 4) {
return 2; FAKE_SUBSCREEN_ADAPTER = true;
log_info(
"graphics::d3d9",
"GetAdapterCount returned {}, gfdm needs 4 adapters - enabling fake submonitor mode", result);
return 4;
}
} }
return result; return result;
@@ -485,12 +493,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier(
D3DADAPTER_IDENTIFIER9 *pIdentifier) D3DADAPTER_IDENTIFIER9 *pIdentifier)
{ {
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1 && pIdentifier) { if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0 && pIdentifier) {
*pIdentifier = {}; *pIdentifier = {};
strcpy(pIdentifier->DeviceName, "\\\\.\\DISPLAY_SPICE_FAKE"); const std::string adapter_name = fmt::format("\\\\.\\DISPLAY_SPICE_FAKE_{}", Adapter);
strcpy(pIdentifier->DeviceName, adapter_name.c_str());
log_misc( log_misc(
"graphics::d3d9", "graphics::d3d9",
"GetAdapterIdentifier called for fake subscreen adapter 1: {}", "GetAdapterIdentifier called for fake subscreen adapter {}: {}",
Adapter,
pIdentifier->DeviceName); pIdentifier->DeviceName);
return S_OK; return S_OK;
} }
@@ -499,8 +509,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier(
} }
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCount(UINT Adapter, D3DFORMAT Format) { UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCount(UINT Adapter, D3DFORMAT Format) {
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1) { if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
log_misc("graphics::d3d9", "GetAdapterModeCount called for fake subscreen adapter"); log_misc("graphics::d3d9", "GetAdapterModeCount called for fake subscreen adapter {}", Adapter);
return 1; return 1;
} }
@@ -513,8 +523,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes(
UINT Mode, UINT Mode,
D3DDISPLAYMODE *pMode) D3DDISPLAYMODE *pMode)
{ {
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1) { if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
log_misc("graphics::d3d9", "EnumAdapterModes called for fake subscreen adapter"); log_misc("graphics::d3d9", "EnumAdapterModes called for fake subscreen adapter {}", Adapter);
if (Mode == 0 && pMode) { if (Mode == 0 && pMode) {
pMode->Width = 1280; pMode->Width = 1280;
pMode->Height = 800; pMode->Height = 800;
@@ -622,8 +632,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceFormat(
D3DRESOURCETYPE RType, D3DRESOURCETYPE RType,
D3DFORMAT CheckFormat) D3DFORMAT CheckFormat)
{ {
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1) { if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
log_misc("graphics::d3d9", "CheckDeviceFormat called for fake subscreen adapter"); log_misc("graphics::d3d9", "CheckDeviceFormat called for fake subscreen adapter {}", Adapter);
return D3D_OK; return D3D_OK;
} }
@@ -708,7 +718,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetDeviceCaps(UINT Adapter, D3DDEVT
} }
// in windowed mode, LDJ will always launch two windows, no special handling needed here // in windowed mode, LDJ will always launch two windows, no special handling needed here
} else if (avs::game::is_model("KFC")) { } else if (avs::game::is_model("KFC")) {
if (GRAPHICS_WINDOWED && GRAPHICS_PREVENT_SECONDARY_WINDOW) { if (GRAPHICS_WINDOWED && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
// user wants windowed mode but does not want subscreen at all // user wants windowed mode but does not want subscreen at all
pCaps->NumberOfAdaptersInGroup = 1; pCaps->NumberOfAdaptersInGroup = 1;
} else { } else {
@@ -1143,10 +1153,15 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface); *ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface);
// initialize sub screen if IIDX/SDVX requested a multi-head context // initialize sub screen if the game requested a multi-head context
if (avs::game::is_model({"LDJ", "KFC", "M39"}) && if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) &&
(orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) { (orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
graphics_d3d9_ldj_init_sub_screen(*ppReturnedDeviceInterface, &pPresentationParameters[1]);
UINT i = 1;
if (games::gitadora::is_arena_model()) {
i = 2;
}
graphics_d3d9_ldj_init_sub_screen(*ppReturnedDeviceInterface, &pPresentationParameters[i]);
} }
} }
@@ -1196,7 +1211,13 @@ static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRE
log_info("graphics::d3d9", "created additional swap chain for windowed mode"); log_info("graphics::d3d9", "created additional swap chain for windowed mode");
} }
} else { } else {
hr = device->GetSwapChain(1, &SUB_SWAP_CHAIN);
int swapchain = 1;
if (games::gitadora::is_arena_model()) {
swapchain = 2;
}
hr = device->GetSwapChain(swapchain, &SUB_SWAP_CHAIN);
if (FAILED(hr)) { if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to acquire fullscreen sub swap chain, hr={}", FMT_HRESULT(hr)); log_warning("graphics::d3d9", "failed to acquire fullscreen sub swap chain, hr={}", FMT_HRESULT(hr));
} else { } else {
@@ -1233,7 +1254,7 @@ static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
// iidx/sdvx // iidx/sdvx
int swapchain = 1; int swapchain = 1;
if (games::gitadora::is_arena_model()) { if (games::gitadora::is_arena_model()) {
swapchain = 3; swapchain = 2;
} }
if (!ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE && SUB_SWAP_CHAIN == nullptr) { if (!ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE && SUB_SWAP_CHAIN == nullptr) {
@@ -1415,11 +1436,18 @@ static void save_screenshot(const std::string &file_path, UINT height, IDirect3D
if (FAILED(hr)) { if (FAILED(hr)) {
log_warning("graphics::d3d9", "Failed to save screenshot"); log_warning("graphics::d3d9", "Failed to save screenshot");
overlay::notifications::add(
overlay::notifications::Severity::Error,
"Screenshot failed to save");
return; return;
} }
// save to clipboard // save to clipboard
clipboard::copy_image(file_path); clipboard::copy_image(file_path);
overlay::notifications::add(
overlay::notifications::Severity::Success,
fmt::format("Screenshot saved: {}", fileutils::basename(file_path)));
} else { } else {
log_warning("graphics::d3d9", "Direct3D save helper function not available"); log_warning("graphics::d3d9", "Direct3D save helper function not available");
} }
@@ -1430,6 +1458,15 @@ void graphics_d3d9_on_present(
IDirect3DDevice9 *device, IDirect3DDevice9 *device,
IDirect3DDevice9 *wrapped_device) { IDirect3DDevice9 *wrapped_device) {
// image resize / orientation swap. run here (the present path) rather than from `EndScene`,
// which may fire several times per frame on multi-pass / render-to-texture games. this is the
// single point guaranteed to be after the game's last `EndScene` and before the real `Present`,
// so the back buffer is fully drawn and the expensive StretchRect work happens exactly once per
// frame. it must run before the overlay is rendered so the overlay isn't scaled with the image.
if (cfg::SCREENRESIZE->enable_screen_resize || GRAPHICS_FS_ORIENTATION_SWAP) {
SurfaceHook(device);
}
// Do overlay init as many d3d9 hooks create a dummy instance to get vtable offsets and never // Do overlay init as many d3d9 hooks create a dummy instance to get vtable offsets and never
// call `Present`. This avoids race conditions on `IDirect3D9::CreateDevice` like with // call `Present`. This avoids race conditions on `IDirect3D9::CreateDevice` like with
// `dx9osd.dll` for pfreepanic. // `dx9osd.dll` for pfreepanic.
@@ -1452,8 +1489,7 @@ void graphics_d3d9_on_present(
// for IIDX TDJ / SDVX UFC, handle subscreen // for IIDX TDJ / SDVX UFC, handle subscreen
const bool is_vm = games::sdvx::is_valkyrie_model(); const bool is_vm = games::sdvx::is_valkyrie_model();
const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE; const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE;
const bool is_gfdm_arena = const bool is_gfdm_arena = games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS;
games::gitadora::is_arena_model() && games::gitadora::ARENA_SINGLE_WINDOW;
const bool is_pika = games::popn::is_pikapika_model(); const bool is_pika = games::popn::is_pikapika_model();
if (is_vm || is_tdj || is_gfdm_arena || is_pika) { if (is_vm || is_tdj || is_gfdm_arena || is_pika) {
graphics_d3d9_ldj_on_present(wrapped_device); graphics_d3d9_ldj_on_present(wrapped_device);
@@ -129,13 +129,17 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() {
this->main_swapchain->Release(); this->main_swapchain->Release();
this->main_swapchain = nullptr; this->main_swapchain = nullptr;
} }
if (this->sub_swapchain) { for (auto &sc : this->sub_swapchain) {
this->sub_swapchain->Release(); if (sc) {
this->sub_swapchain = nullptr; sc->Release();
sc = nullptr;
}
} }
if (this->fake_sub_swapchain) { for (auto &sc : this->fake_sub_swapchain) {
this->fake_sub_swapchain->Release(); if (sc) {
this->fake_sub_swapchain = nullptr; sc->Release();
sc = nullptr;
}
} }
if (overlay::ENABLED) { if (overlay::ENABLED) {
@@ -249,28 +253,69 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
{ {
WRAP_VERBOSE; WRAP_VERBOSE;
HRESULT hr = pReal->CreateAdditionalSwapChain(pPresentationParameters, ppSwapChain); int index = 0;
bool create_swap_chain = false; bool create_swap_chain = false;
bool create_fake_swap_chain = false;
if (avs::game::is_model({"LDJ", "KFC", "M39"})) { if (avs::game::is_model({"LDJ", "KFC", "M39"})) {
create_swap_chain = true; create_swap_chain = true;
} else if (games::gitadora::is_arena_model() && } else if (games::gitadora::is_arena_model() &&
games::gitadora::ARENA_SINGLE_WINDOW && (GRAPHICS_PREVENT_SECONDARY_WINDOWS || GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS)) {
pPresentationParameters->BackBufferWidth == 800) {
create_swap_chain = true; if (pPresentationParameters->BackBufferWidth == 800) {
// SMALL (subscreen)
create_swap_chain = true;
index = 0;
} else if (pPresentationParameters->BackBufferWidth == 1080) {
// LEFT/RIGHT
create_swap_chain = true;
index = 1;
if (sub_swapchain[index] || fake_sub_swapchain[index]) {
index = 2;
}
create_fake_swap_chain = GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS &&
!GRAPHICS_PREVENT_SECONDARY_WINDOWS;
} else {
log_warning("graphics::d3d9", "unknown swap chain detected in CreateAdditionalSwapChain");
}
} }
if (create_fake_swap_chain) {
if (!fake_sub_swapchain[index]) {
log_info(
"graphics::d3d9",
"CreateAdditionalSwapChain called for hidden GITADORA side swap chain {}, "
"using fake swap chain",
index);
fake_sub_swapchain[index] =
new FakeIDirect3DSwapChain9(this, pPresentationParameters, false);
}
fake_sub_swapchain[index]->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[index]);
return D3D_OK;
}
HRESULT hr = pReal->CreateAdditionalSwapChain(pPresentationParameters, ppSwapChain);
if (create_swap_chain) { if (create_swap_chain) {
if (SUCCEEDED(hr) && !sub_swapchain) { log_misc(
sub_swapchain = new WrappedIDirect3DSwapChain9(this, *ppSwapChain); "graphics::d3d9",
sub_swapchain->should_run_hooks = false; "CreateAdditionalSwapChain called for swap chain {}, creating swap chain",
} else if (FAILED(hr) && !fake_sub_swapchain) { index);
if (SUCCEEDED(hr) && !sub_swapchain[index]) {
sub_swapchain[index] = new WrappedIDirect3DSwapChain9(this, *ppSwapChain);
sub_swapchain[index]->should_run_hooks = false;
} else if (FAILED(hr) && !fake_sub_swapchain[index]) {
log_warning("graphics::d3d9", log_warning("graphics::d3d9",
"failed to create sub swap chain, hr={}, using fake swap chain", "failed to create sub swap chain, hr={}, using fake swap chain",
FMT_HRESULT(hr)); FMT_HRESULT(hr));
fake_sub_swapchain = new FakeIDirect3DSwapChain9(this, pPresentationParameters, false); fake_sub_swapchain[index] = new FakeIDirect3DSwapChain9(this, pPresentationParameters, false);
fake_sub_swapchain->AddRef(); fake_sub_swapchain[index]->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain); *ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[index]);
return D3D_OK; return D3D_OK;
} }
@@ -308,22 +353,20 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
if (iSwapChain == 1 && avs::game::is_model({"LDJ", "KFC", "M39"})) { if (iSwapChain == 1 && avs::game::is_model({"LDJ", "KFC", "M39"})) {
swap = true; swap = true;
} }
if (games::gitadora::is_arena_model() && if (games::gitadora::is_arena_model() && iSwapChain == 2) {
games::gitadora::ARENA_SINGLE_WINDOW &&
iSwapChain == 3) {
swap = true; swap = true;
} }
if (swap) { if (swap) {
if (sub_swapchain) { if (sub_swapchain[0]) {
sub_swapchain->AddRef(); sub_swapchain[0]->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(sub_swapchain); *ppSwapChain = static_cast<IDirect3DSwapChain9 *>(sub_swapchain[0]);
graphics_screens_register(iSwapChain); graphics_screens_register(iSwapChain);
return D3D_OK; return D3D_OK;
} else if (fake_sub_swapchain) { } else if (fake_sub_swapchain[0]) {
fake_sub_swapchain->AddRef(); fake_sub_swapchain[0]->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain); *ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[0]);
graphics_screens_register(iSwapChain); graphics_screens_register(iSwapChain);
return D3D_OK; return D3D_OK;
@@ -344,7 +387,7 @@ UINT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetNumberOfSwapChains() {
UINT n = pReal->GetNumberOfSwapChains(); UINT n = pReal->GetNumberOfSwapChains();
if (sub_swapchain && avs::game::is_model({"LDJ", "KFC", "M39"})) { if (sub_swapchain[0] && avs::game::is_model({"LDJ", "KFC", "M39"})) {
n += 1; n += 1;
} }
@@ -717,18 +760,15 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::BeginScene() {
static IDirect3DSurface9 *topSurface = nullptr; static IDirect3DSurface9 *topSurface = nullptr;
static IDirect3DSurface9 *backbuffer = nullptr; static IDirect3DSurface9 *backbuffer = nullptr;
static LPDIRECT3DSWAPCHAIN9 mSwapChain = nullptr; static IDirect3DTexture9 *tex = nullptr;
static IDirect3DTexture9* tex;
static UINT topSurface_width = 0; static UINT topSurface_width = 0;
static UINT topSurface_height = 0; static UINT topSurface_height = 0;
static float topSurface_aspect_ratio = 1.f; static float topSurface_aspect_ratio = 1.f;
static UINT backbuffer_width = 0;
static UINT backbuffer_height = 0;
void SurfaceHook(IDirect3DDevice9 *pReal) { void SurfaceHook(IDirect3DDevice9 *pReal) {
D3DPRESENT_PARAMETERS param {};
pReal->GetSwapChain(0, &mSwapChain);
mSwapChain->GetPresentParameters(&param);
// phase 0 - create a surface that has the same aspect ratio as the original back buffer // phase 0 - create a surface that has the same aspect ratio as the original back buffer
// but larger in size. this is needed to ensure that when the user zooms out, we have // but larger in size. this is needed to ensure that when the user zooms out, we have
@@ -736,8 +776,20 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
// //
// (only done once) // (only done once)
if (!topSurface) { if (!topSurface) {
topSurface_width = param.BackBufferWidth * 3; // GetSwapChain/GetBackBuffer AddRef their out parameters; the swap chain is only
topSurface_height = param.BackBufferHeight * 3; // needed here (during one-time init), so release it after use to avoid a per-frame
// ref leak. The backbuffer ref is intentionally retained for the lifetime of the
// cached resources.
LPDIRECT3DSWAPCHAIN9 swapChain = nullptr;
D3DPRESENT_PARAMETERS param {};
pReal->GetSwapChain(0, &swapChain);
swapChain->GetPresentParameters(&param);
backbuffer_width = param.BackBufferWidth;
backbuffer_height = param.BackBufferHeight;
topSurface_width = backbuffer_width * 3;
topSurface_height = backbuffer_height * 3;
topSurface_aspect_ratio = (float)topSurface_width / (float)topSurface_height; topSurface_aspect_ratio = (float)topSurface_width / (float)topSurface_height;
if (pReal->CreateTexture(topSurface_width, topSurface_height, 1, if (pReal->CreateTexture(topSurface_width, topSurface_height, 1,
D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8, D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8,
@@ -750,16 +802,18 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
param.BackBufferWidth, param.BackBufferHeight, param.BackBufferCount); param.BackBufferWidth, param.BackBufferHeight, param.BackBufferCount);
tex->GetSurfaceLevel(0, &topSurface); tex->GetSurfaceLevel(0, &topSurface);
if (mSwapChain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &backbuffer) != D3D_OK) { if (swapChain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &backbuffer) != D3D_OK) {
log_warning("graphics::d3d9", "SurfaceHook - GetBackBuffer failed"); log_warning("graphics::d3d9", "SurfaceHook - GetBackBuffer failed");
} }
swapChain->Release();
} }
// pre-calculate dimensions used for phase 1 // pre-calculate dimensions used for phase 1
const int rectLeft = param.BackBufferWidth; const int rectLeft = backbuffer_width;
const int rectTop = param.BackBufferHeight; const int rectTop = backbuffer_height;
const int w = param.BackBufferWidth; const int w = backbuffer_width;
const int h = param.BackBufferHeight; const int h = backbuffer_height;
// this code used to clear the surface using ColorFill on every call, but // this code used to clear the surface using ColorFill on every call, but
// this turned out to be very expensive, leading to major frame drops in // this turned out to be very expensive, leading to major frame drops in
@@ -769,9 +823,7 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
cfg::SCREENRESIZE->need_surface_clean = false; cfg::SCREENRESIZE->need_surface_clean = false;
} }
D3DLOCKED_RECT rect;
HRESULT hr; HRESULT hr;
topSurface->LockRect(&rect, NULL, D3DLOCK_DONOTWAIT);
// phase 1 - copy the original back buffer onto the new surface. // phase 1 - copy the original back buffer onto the new surface.
// we draw it 1:1 in the center of the surface. // we draw it 1:1 in the center of the surface.
@@ -834,7 +886,6 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
originRect2.left, originRect2.top, originRect2.right, originRect2.bottom); originRect2.left, originRect2.top, originRect2.right, originRect2.bottom);
} }
} }
topSurface->UnlockRect();
// phase 2 - viewport calculation - do the actual zoom / offset math // phase 2 - viewport calculation - do the actual zoom / offset math
// figure out what region of the surface to copy back to the back buffer. // figure out what region of the surface to copy back to the back buffer.
@@ -909,7 +960,6 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
// targetRect.left, targetRect.top, targetRect.right, targetRect.bottom); // targetRect.left, targetRect.top, targetRect.right, targetRect.bottom);
// phase 3 - draw the surface to back buffer // phase 3 - draw the surface to back buffer
backbuffer->LockRect(&rect, NULL, D3DLOCK_DONOTWAIT);
bool use_linear_filter = true; bool use_linear_filter = true;
if (cfg::SCREENRESIZE->enable_screen_resize) { if (cfg::SCREENRESIZE->enable_screen_resize) {
use_linear_filter = cfg::SCREENRESIZE->enable_linear_filter; use_linear_filter = cfg::SCREENRESIZE->enable_linear_filter;
@@ -918,7 +968,6 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
topSurface, &targetRect, topSurface, &targetRect,
backbuffer, nullptr, backbuffer, nullptr,
use_linear_filter ? D3DTEXF_LINEAR : D3DTEXF_NONE); use_linear_filter ? D3DTEXF_LINEAR : D3DTEXF_NONE);
backbuffer->UnlockRect();
if (hr != D3D_OK) { if (hr != D3D_OK) {
log_warning( log_warning(
"graphics::d3d9", "graphics::d3d9",
@@ -930,10 +979,6 @@ void SurfaceHook(IDirect3DDevice9 *pReal) {
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::EndScene() { HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::EndScene() {
WRAP_DEBUG; WRAP_DEBUG;
if (cfg::SCREENRESIZE->enable_screen_resize || GRAPHICS_FS_ORIENTATION_SWAP) {
SurfaceHook(pReal);
}
static std::once_flag printed; static std::once_flag printed;
std::call_once(printed, []() { std::call_once(printed, []() {
log_misc("graphics::d3d9", "WrappedIDirect3DDevice9::EndScene"); log_misc("graphics::d3d9", "WrappedIDirect3DDevice9::EndScene");
@@ -35,6 +35,10 @@ static const GUID IID_WrappedIDirect3DDevice9 = {
0x6dec0d40, 0x1339, 0x4bda, { 0xa5, 0xf2, 0x22, 0x31, 0xd4, 0x1, 0xf, 0xd1 } 0x6dec0d40, 0x1339, 0x4bda, { 0xa5, 0xf2, 0x22, 0x31, 0xd4, 0x1, 0xf, 0xd1 }
}; };
// applies the image resize / orientation swap by copying the back buffer through an
// oversized intermediate surface and back. expects the real (unwrapped) device.
void SurfaceHook(IDirect3DDevice9 *pReal);
struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex { struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
explicit WrappedIDirect3DDevice9(HWND hFocusWindow, IDirect3DDevice9 *orig) explicit WrappedIDirect3DDevice9(HWND hFocusWindow, IDirect3DDevice9 *orig)
: hFocusWindow(hFocusWindow), pReal(orig), is_d3d9ex(false) { : hFocusWindow(hFocusWindow), pReal(orig), is_d3d9ex(false) {
@@ -204,7 +208,8 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
std::atomic_ulong refs = 1; std::atomic_ulong refs = 1;
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr; WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
WrappedIDirect3DSwapChain9 *sub_swapchain = nullptr; WrappedIDirect3DSwapChain9 *sub_swapchain[3] = { nullptr, nullptr, nullptr };
FakeIDirect3DSwapChain9 *fake_sub_swapchain = nullptr; FakeIDirect3DSwapChain9 *fake_sub_swapchain[3] = { nullptr, nullptr, nullptr };
IDirect3DVertexShader9 *vertex_shader = nullptr; IDirect3DVertexShader9 *vertex_shader = nullptr;
}; };
+359 -35
View File
@@ -18,6 +18,7 @@
#include "games/iidx/iidx.h" #include "games/iidx/iidx.h"
#include "games/popn/popn.h" #include "games/popn/popn.h"
#include "hooks/graphics/backends/d3d9/d3d9_backend.h" #include "hooks/graphics/backends/d3d9/d3d9_backend.h"
#include "hooks/graphics/backends/d3d11/d3d11_backend.h"
#include "launcher/shutdown.h" #include "launcher/shutdown.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "touch/touch.h" #include "touch/touch.h"
@@ -39,6 +40,8 @@ struct CaptureData {
HWND TDJ_SUBSCREEN_WINDOW = nullptr; HWND TDJ_SUBSCREEN_WINDOW = nullptr;
HWND SDVX_SUBSCREEN_WINDOW = nullptr; HWND SDVX_SUBSCREEN_WINDOW = nullptr;
HWND GFDM_SUBSCREEN_WINDOW = nullptr; HWND GFDM_SUBSCREEN_WINDOW = nullptr;
static HWND GFDM_LEFT_WINDOW = nullptr;
static HWND GFDM_RIGHT_WINDOW = nullptr;
HWND POPN_SUBSCREEN_WINDOW = nullptr; HWND POPN_SUBSCREEN_WINDOW = nullptr;
bool FAKE_SUBSCREEN_ADAPTER = false; bool FAKE_SUBSCREEN_ADAPTER = false;
@@ -59,6 +62,12 @@ static CaptureData GRAPHICS_CAPTURE_BUFFER[GRAPHICS_CAPTURE_SCREEN_NO] {};
static std::mutex GRAPHICS_CAPTURE_BUFFER_M[GRAPHICS_CAPTURE_SCREEN_NO] {}; static std::mutex GRAPHICS_CAPTURE_BUFFER_M[GRAPHICS_CAPTURE_SCREEN_NO] {};
static std::condition_variable GRAPHICS_CAPTURE_CV[GRAPHICS_CAPTURE_SCREEN_NO] {}; 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;
// flag settings // flag settings
bool GRAPHICS_CAPTURE_CURSOR = false; bool GRAPHICS_CAPTURE_CURSOR = false;
bool GRAPHICS_LOG_HRESULT = false; bool GRAPHICS_LOG_HRESULT = false;
@@ -70,7 +79,8 @@ UINT GRAPHICS_FORCE_REFRESH = 0;
std::optional<uint32_t> GRAPHICS_FORCE_REFRESH_SUB; std::optional<uint32_t> GRAPHICS_FORCE_REFRESH_SUB;
std::optional<int> GRAPHICS_FORCE_VSYNC_BUFFER; std::optional<int> GRAPHICS_FORCE_VSYNC_BUFFER;
bool GRAPHICS_FORCE_SINGLE_ADAPTER = false; bool GRAPHICS_FORCE_SINGLE_ADAPTER = false;
bool GRAPHICS_PREVENT_SECONDARY_WINDOW = false; bool GRAPHICS_PREVENT_SECONDARY_WINDOWS = false;
bool GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS = false;
graphics_dx9on12_state GRAPHICS_9_ON_12_STATE = DX9ON12_AUTO; graphics_dx9on12_state GRAPHICS_9_ON_12_STATE = DX9ON12_AUTO;
bool GRAPHICS_9_ON_12_REQUESTED_BY_GAME = false; bool GRAPHICS_9_ON_12_REQUESTED_BY_GAME = false;
bool SUBSCREEN_FORCE_REDRAW = false; bool SUBSCREEN_FORCE_REDRAW = false;
@@ -104,6 +114,7 @@ static decltype(SetWindowLongA) *SetWindowLongA_orig = nullptr;
static decltype(SetWindowLongW) *SetWindowLongW_orig = nullptr; static decltype(SetWindowLongW) *SetWindowLongW_orig = nullptr;
static decltype(SetWindowPos) *SetWindowPos_orig = nullptr; static decltype(SetWindowPos) *SetWindowPos_orig = nullptr;
static decltype(ShowWindow) *ShowWindow_orig = nullptr; static decltype(ShowWindow) *ShowWindow_orig = nullptr;
static decltype(SetDisplayConfig) *SetDisplayConfig_addr = nullptr;
static void reset_window_hook(HWND hWnd) { static void reset_window_hook(HWND hWnd) {
overlay::destroy(hWnd); overlay::destroy(hWnd);
@@ -114,6 +125,128 @@ static void reset_window_hook(HWND hWnd) {
} }
} }
static std::string gitadora_canonical_window_name(const std::string &window_name) {
if (window_name == "GITADORA") {
return "GITADORA";
}
if (window_name.ends_with("LEFT")) {
return "LEFT";
}
if (window_name.ends_with("RIGHT")) {
return "RIGHT";
}
if (window_name.ends_with("SMALL")) {
return "SMALL";
}
return "";
}
static const char *gitadora_window_name_for_hwnd(HWND hWnd) {
if (hWnd == nullptr) {
return nullptr;
}
if (GRAPHICS_HOOKED_WINDOW.has_value() && hWnd == GRAPHICS_HOOKED_WINDOW.value()) {
return "GITADORA";
}
if (hWnd == GFDM_LEFT_WINDOW) {
return "LEFT";
}
if (hWnd == GFDM_RIGHT_WINDOW) {
return "RIGHT";
}
if (hWnd == GFDM_SUBSCREEN_WINDOW) {
return "SMALL";
}
return nullptr;
}
static bool is_gfdm_known_window(HWND hWnd) {
return gitadora_window_name_for_hwnd(hWnd) != nullptr;
}
static bool gitadora_should_block_game_window_placement(HWND hWnd) {
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model()) {
return false;
}
const auto window_name = gitadora_window_name_for_hwnd(hWnd);
return window_name != nullptr && graphics_gitadora_has_window_monitor(window_name);
}
static void gitadora_remember_window(HWND hWnd, const std::string &window_name) {
if (window_name == "LEFT") {
GFDM_LEFT_WINDOW = hWnd;
} else if (window_name == "RIGHT") {
GFDM_RIGHT_WINDOW = hWnd;
} else if (window_name == "SMALL") {
GFDM_SUBSCREEN_WINDOW = hWnd;
}
}
static bool gitadora_should_allow_small_resize() {
return GRAPHICS_WINDOWED &&
games::gitadora::is_arena_model() &&
!graphics_gitadora_is_borderless_windowed();
}
static void gitadora_apply_small_resize_style(DWORD &style) {
if (!gitadora_should_allow_small_resize()) {
return;
}
style |= WS_SIZEBOX;
style |= WS_MAXIMIZEBOX;
style |= WS_SYSMENU;
}
static void gitadora_force_window_style(HWND hWnd) {
if (!GRAPHICS_WINDOWED || !games::gitadora::is_arena_model() || hWnd == nullptr) {
return;
}
DWORD style = GetWindowLongA(hWnd, GWL_STYLE);
DWORD style_ex = GetWindowLongA(hWnd, GWL_EXSTYLE);
const DWORD style_orig = style;
const DWORD style_ex_orig = style_ex;
graphics_gitadora_apply_window_style(style, style_ex);
if (hWnd == GFDM_SUBSCREEN_WINDOW) {
gitadora_apply_small_resize_style(style);
}
if (style == style_orig && style_ex == style_ex_orig) {
return;
}
if (SetWindowLongA_orig != nullptr) {
SetWindowLongA_orig(hWnd, GWL_STYLE, static_cast<LONG>(style));
SetWindowLongA_orig(hWnd, GWL_EXSTYLE, static_cast<LONG>(style_ex));
} else {
SetWindowLongA(hWnd, GWL_STYLE, static_cast<LONG>(style));
SetWindowLongA(hWnd, GWL_EXSTYLE, static_cast<LONG>(style_ex));
}
const UINT flags =
SWP_NOMOVE |
SWP_NOSIZE |
SWP_NOZORDER |
SWP_NOACTIVATE |
SWP_FRAMECHANGED;
if (SetWindowPos_orig != nullptr) {
SetWindowPos_orig(hWnd, nullptr, 0, 0, 0, 0, flags);
} else {
SetWindowPos(hWnd, nullptr, 0, 0, 0, 0, flags);
}
log_misc(
"graphics",
"GITADORA window style override: hwnd={}, style 0x{:x}->0x{:x}, ex 0x{:x}->0x{:x}",
fmt::ptr(hWnd),
style_orig,
style,
style_ex_orig,
style_ex);
}
// window procedure // window procedure
static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
@@ -157,6 +290,7 @@ static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
if (wintouchemu::INJECT_MOUSE_AS_WM_TOUCH) { if (wintouchemu::INJECT_MOUSE_AS_WM_TOUCH) {
// drop mouse inputs since only wintouches should be used // drop mouse inputs since only wintouches should be used
switch (uMsg) { switch (uMsg) {
case WM_MOUSEMOVE:
case WM_LBUTTONDOWN: case WM_LBUTTONDOWN:
case WM_LBUTTONUP: case WM_LBUTTONUP:
case WM_MBUTTONDOWN: case WM_MBUTTONDOWN:
@@ -232,7 +366,7 @@ static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
switch (uMsg) { switch (uMsg) {
case WM_MOVE: case WM_MOVE:
case WM_SIZE: { case WM_SIZE:
// Update SPICETOUCH space when the main window changes size or moves. // Update SPICETOUCH space when the main window changes size or moves.
// The update happens regardless of whether the "fake" spicetouch window is present or not. // The update happens regardless of whether the "fake" spicetouch window is present or not.
// This allows touches received on subscreen window to be translated correctly. // This allows touches received on subscreen window to be translated correctly.
@@ -249,7 +383,16 @@ static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
SPICETOUCH_TOUCH_WIDTH, SPICETOUCH_TOUCH_HEIGHT, SPICETOUCH_TOUCH_WIDTH, SPICETOUCH_TOUCH_HEIGHT,
SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE); SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE);
} }
} break;
case WM_ACTIVATEAPP:
if (wParam) {
// regained focus
// this *can* get called twice in a row when restoring, but update_monitor_at_runtime
// is idempotent (checks current display settings to see if changes are needed),
// so it shouldn't cause any issues
update_monitor_at_runtime();
}
break;
default: default:
break; break;
} }
@@ -265,6 +408,18 @@ static LRESULT CALLBACK WsubWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPAR
log_misc("graphics", "ignore WM_CLOSE for subscreen window"); log_misc("graphics", "ignore WM_CLOSE for subscreen window");
return false; return false;
} }
if (hWnd == GFDM_SUBSCREEN_WINDOW && (uMsg == WM_MOVE || uMsg == WM_SIZE)) {
update_spicetouch_window_dimensions(hWnd);
if (SPICETOUCH_TOUCH_HWND) {
SetWindowPos(
SPICETOUCH_TOUCH_HWND, HWND_TOP,
SPICETOUCH_TOUCH_X, SPICETOUCH_TOUCH_Y,
SPICETOUCH_TOUCH_WIDTH, SPICETOUCH_TOUCH_HEIGHT,
SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE);
}
}
return CallWindowProcA(WSUB_WNDPROC_ORIG, hWnd, uMsg, wParam, lParam); return CallWindowProcA(WSUB_WNDPROC_ORIG, hWnd, uMsg, wParam, lParam);
} }
@@ -328,28 +483,55 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
fmt::ptr(lpParam)); fmt::ptr(lpParam));
// gfdm // gfdm
std::string effective_window_name = window_name;
if (avs::game::is_model({"J32", "J33", "K32", "K33", "L32", "L33", "M32"})) { if (avs::game::is_model({"J32", "J33", "K32", "K33", "L32", "L33", "M32"})) {
// set window name // set window name
if (!lpWindowName) { if (!lpWindowName) {
lpWindowName = "GITADORA"; lpWindowName = "GITADORA";
effective_window_name = "GITADORA";
} }
} }
bool is_tdj_sub_window = avs::game::is_model("LDJ") && window_name.ends_with(" sub"); bool is_tdj_sub_window = avs::game::is_model("LDJ") && window_name.ends_with(" sub");
bool is_sdvx_sub_window = avs::game::is_model("KFC") && window_name.ends_with(" Sub Screen"); bool is_sdvx_sub_window = avs::game::is_model("KFC") && window_name.ends_with(" Sub Screen");
bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen"); 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"); const std::string gfdm_window_name = games::gitadora::is_arena_model()
? gitadora_canonical_window_name(effective_window_name)
: "";
const bool is_gfdm_window = !gfdm_window_name.empty();
const bool is_gfdm_sub_window = gfdm_window_name == "SMALL";
const bool allow_gfdm_small_resize =
is_gfdm_sub_window && gitadora_should_allow_small_resize();
// hide maximize button (prevent misaligned touches) // update style / ex-style
if ((is_tdj_sub_window && GRAPHICS_IIDX_WSUB) || is_sdvx_sub_window || is_gfdm_sub_window || is_popn_sub_window) { if (is_tdj_sub_window || is_sdvx_sub_window || is_gfdm_sub_window || is_popn_sub_window) {
dwStyle &= ~(WS_MAXIMIZEBOX); // hide maximize button (prevent misaligned touches)
if (!allow_gfdm_small_resize) {
dwStyle &= ~(WS_MAXIMIZEBOX);
}
// mouse clicks become misaligned when resized
if (!allow_gfdm_small_resize) {
dwStyle &= ~(WS_SIZEBOX);
}
// 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;
}
} }
// mouse clicks become misaligned when resized
if (is_gfdm_sub_window) { if (allow_gfdm_small_resize) {
dwStyle &= ~(WS_SIZEBOX); gitadora_apply_small_resize_style(dwStyle);
} }
if ((is_tdj_sub_window || is_sdvx_sub_window || is_popn_sub_window) && GRAPHICS_WSUB_BORDERLESS) { if (is_gfdm_window) {
dwStyle &= ~(WS_OVERLAPPEDWINDOW); graphics_gitadora_apply_window_style(dwStyle, dwExStyle);
} }
if (is_sdvx_sub_window) { if (is_sdvx_sub_window) {
@@ -370,6 +552,16 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
} }
} }
if (is_gfdm_window) {
graphics_gitadora_apply_window_monitor(
gfdm_window_name,
x,
y,
nWidth,
nHeight,
true);
}
if (GRAPHICS_WINDOWED) { if (GRAPHICS_WINDOWED) {
graphics_window_check_bounds_before_creation(x, y, nWidth, nHeight); graphics_window_check_bounds_before_creation(x, y, nWidth, nHeight);
} }
@@ -396,14 +588,20 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
} }
// only hook touch window if multiple windows are allowed // only hook touch window if multiple windows are allowed
if (is_gfdm_sub_window && !games::gitadora::ARENA_SINGLE_WINDOW) { if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") {
GFDM_SUBSCREEN_WINDOW = result; gitadora_remember_window(result, gfdm_window_name);
}
if (is_gfdm_sub_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
gitadora_remember_window(result, gfdm_window_name);
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW); graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
} }
if (is_gfdm_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
gitadora_force_window_style(result);
}
if (is_popn_sub_window) { if (is_popn_sub_window) {
POPN_SUBSCREEN_WINDOW = result; POPN_SUBSCREEN_WINDOW = result;
if (!GRAPHICS_PREVENT_SECONDARY_WINDOW) { if (!GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
graphics_hook_subscreen_window(POPN_SUBSCREEN_WINDOW); graphics_hook_subscreen_window(POPN_SUBSCREEN_WINDOW);
} }
} }
@@ -588,6 +786,12 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei
} }
} }
// Monitor overrides are applied at creation time. Suppress later game
// placement calls instead of resizing again during scene transitions.
if (gitadora_should_block_game_window_placement(hWnd)) {
return TRUE;
}
// call original // call original
return MoveWindow_orig(hWnd, X, Y, nWidth, nHeight, bRepaint); return MoveWindow_orig(hWnd, X, Y, nWidth, nHeight, bRepaint);
} }
@@ -641,8 +845,17 @@ static LONG WINAPI SetWindowLongA_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
dwNewLong |= WS_OVERLAPPEDWINDOW; dwNewLong |= WS_OVERLAPPEDWINDOW;
} }
// call original const bool force_gfdm_style =
return SetWindowLongA_orig(hWnd, nIndex, dwNewLong); GRAPHICS_WINDOWED &&
games::gitadora::is_arena_model() &&
is_gfdm_known_window(hWnd) &&
(nIndex == GWL_STYLE || nIndex == GWL_EXSTYLE);
const auto result = SetWindowLongA_orig(hWnd, nIndex, dwNewLong);
if (force_gfdm_style) {
gitadora_force_window_style(hWnd);
}
return result;
} }
static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) { static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
@@ -652,8 +865,17 @@ static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
dwNewLong |= WS_OVERLAPPEDWINDOW; dwNewLong |= WS_OVERLAPPEDWINDOW;
} }
// call original const bool force_gfdm_style =
return SetWindowLongW_orig(hWnd, nIndex, dwNewLong); GRAPHICS_WINDOWED &&
games::gitadora::is_arena_model() &&
is_gfdm_known_window(hWnd) &&
(nIndex == GWL_STYLE || nIndex == GWL_EXSTYLE);
const auto result = SetWindowLongW_orig(hWnd, nIndex, dwNewLong);
if (force_gfdm_style) {
gitadora_force_window_style(hWnd);
}
return result;
} }
static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter, static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
@@ -664,6 +886,13 @@ static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
return TRUE; return TRUE;
} }
// Monitor overrides are applied at creation time. Suppress later game
// placement calls instead of resizing again during scene transitions.
if (gitadora_should_block_game_window_placement(hWnd) &&
(uFlags & (SWP_NOMOVE | SWP_NOSIZE)) != (SWP_NOMOVE | SWP_NOSIZE)) {
return TRUE;
}
// prevent gitadora arena model from shifting windows around if the user has preferences // prevent gitadora arena model from shifting windows around if the user has preferences
if (GRAPHICS_WINDOWED && games::gitadora::is_arena_model() && if (GRAPHICS_WINDOWED && games::gitadora::is_arena_model() &&
GRAPHICS_HOOKED_WINDOW.has_value() && hWnd == GRAPHICS_HOOKED_WINDOW.value() && GRAPHICS_HOOKED_WINDOW.has_value() && hWnd == GRAPHICS_HOOKED_WINDOW.value() &&
@@ -677,14 +906,21 @@ static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) { static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
if (games::gitadora::is_arena_model() && if (games::gitadora::is_arena_model() &&
games::gitadora::ARENA_SINGLE_WINDOW && GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
hWnd != GRAPHICS_HOOKED_WINDOW) { hWnd != GRAPHICS_HOOKED_WINDOW) {
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd)); log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
return true; return true;
} }
if (games::gitadora::is_arena_model() &&
GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS &&
(hWnd == GFDM_LEFT_WINDOW || hWnd == GFDM_RIGHT_WINDOW)) {
log_info("graphics", "ShowWindow_hook - hiding GITADORA side window {}", fmt::ptr(hWnd));
return true;
}
if (games::popn::is_pikapika_model() && if (games::popn::is_pikapika_model() &&
GRAPHICS_PREVENT_SECONDARY_WINDOW && GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
hWnd == POPN_SUBSCREEN_WINDOW) { hWnd == POPN_SUBSCREEN_WINDOW) {
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd)); log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
return true; return true;
@@ -816,6 +1052,7 @@ void graphics_init() {
// init backends // init backends
graphics_d3d9_init(); graphics_d3d9_init();
graphics_d3d11_init();
// general hooks // general hooks
ChangeDisplaySettingsA_orig = detour::iat_try("ChangeDisplaySettingsA", ChangeDisplaySettingsA_hook); ChangeDisplaySettingsA_orig = detour::iat_try("ChangeDisplaySettingsA", ChangeDisplaySettingsA_hook);
@@ -1142,7 +1379,7 @@ void change_primary_monitor(const std::string &monitor_name) {
const auto DisplayConfigGetDeviceInfo_addr = const auto DisplayConfigGetDeviceInfo_addr =
reinterpret_cast<decltype(DisplayConfigGetDeviceInfo) *>( reinterpret_cast<decltype(DisplayConfigGetDeviceInfo) *>(
GetProcAddress(user32, "DisplayConfigGetDeviceInfo")); GetProcAddress(user32, "DisplayConfigGetDeviceInfo"));
const auto SetDisplayConfig_addr = SetDisplayConfig_addr =
reinterpret_cast<decltype(SetDisplayConfig) *>( reinterpret_cast<decltype(SetDisplayConfig) *>(
GetProcAddress(user32, "SetDisplayConfig")); GetProcAddress(user32, "SetDisplayConfig"));
if (GetDisplayConfigBufferSizes_addr == nullptr || QueryDisplayConfig_addr == nullptr || if (GetDisplayConfigBufferSizes_addr == nullptr || QueryDisplayConfig_addr == nullptr ||
@@ -1257,12 +1494,19 @@ void change_primary_monitor(const std::string &monitor_name) {
log_fatal("graphics", "SetDisplayConfig failed, check -mainmonitor option: {}", status); log_fatal("graphics", "SetDisplayConfig failed, check -mainmonitor option: {}", status);
} }
monitor_layout_needs_reset = true;
// a little extra time for windows to settle and redraw things // a little extra time for windows to settle and redraw things
Sleep(2000); Sleep(2000);
} }
void update_monitor_on_boot(std::optional<graphics_orientation> target_orientation, UINT target_refresh_rate) { void update_monitor(
// note: all of this is only being done for the primary motnior 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 // get current settings
DEVMODEA dm = {}; DEVMODEA dm = {};
@@ -1330,7 +1574,7 @@ void update_monitor_on_boot(std::optional<graphics_orientation> target_orientati
} }
// update refresh rate // update refresh rate
if (target_refresh_rate > 0) { if (target_refresh_rate > 0 && target_refresh_rate != dm.dmDisplayFrequency) {
log_misc("graphics", log_misc("graphics",
"current refresh rate {} => desired refresh rate {}", "current refresh rate {} => desired refresh rate {}",
dm.dmDisplayFrequency, target_refresh_rate); dm.dmDisplayFrequency, target_refresh_rate);
@@ -1340,27 +1584,107 @@ void update_monitor_on_boot(std::optional<graphics_orientation> target_orientati
needs_update = true; 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) { if (!needs_update) {
// nothing to do // nothing to do
log_misc("graphics", "display settings are already up to date, no changes needed");
return; return;
} }
const auto result = ChangeDisplaySettings(&dm, CDS_FULLSCREEN); const auto result = ChangeDisplaySettings(&dm, CDS_FULLSCREEN);
if (result != DISP_CHANGE_SUCCESSFUL) { if (result != DISP_CHANGE_SUCCESSFUL) {
log_fatal( if (is_boot) {
"graphics", log_fatal(
"failed to update display settings ({}px x {}px @ {}Hz): error {}, double check options", "graphics",
dm.dmPelsWidth, "failed to update display settings ({}px x {}px @ {}Hz): error {}, double check options",
dm.dmPelsHeight, dm.dmPelsWidth,
dm.dmDisplayFrequency, dm.dmPelsHeight,
result); dm.dmDisplayFrequency,
result);
} else {
log_warning(
"graphics",
"failed to update display settings ({}px x {}px @ {}Hz): error {}, double check options",
dm.dmPelsWidth,
dm.dmPelsHeight,
dm.dmDisplayFrequency,
result);
}
} else { } else {
monitor_settings_changed = true;
// sleep for a little bit after changing monitor settings to delay game launch/resume
Sleep(1000);
log_info("graphics", "display settings updated successfully ({}px x {}px @ {}Hz)", log_info("graphics", "display settings updated successfully ({}px x {}px @ {}Hz)",
dm.dmPelsWidth, dm.dmPelsWidth,
dm.dmPelsHeight, dm.dmPelsHeight,
dm.dmDisplayFrequency); dm.dmDisplayFrequency);
} }
}
// sleep for a little bit after changing monitor settings to delay game launch
Sleep(1000); 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,
target_resolution);
}
void update_monitor_at_runtime() {
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,
target_resolution_on_boot);
}
}
void reset_monitor_on_exit() {
// while CDS_FULLSCREEN is *supposed* to be temporary & the OS attempts to
// restore the original settings on exit, it can sometimes fail to do that;
// therefore, we try our best to clean things up on the way out
if (monitor_settings_changed) {
monitor_settings_changed = false;
log_misc("graphics", "resetting monitor settings on exit...");
ChangeDisplaySettingsW(nullptr, 0);
}
// same for this one.
if (monitor_layout_needs_reset) {
monitor_layout_needs_reset = false;
log_misc("graphics", "restoring primary monitor on exit...");
if (SetDisplayConfig_addr != nullptr) {
SetDisplayConfig_addr(
0,
nullptr,
0,
nullptr,
SDC_APPLY | SDC_USE_DATABASE_CURRENT);
}
}
} }
+23 -2
View File
@@ -38,7 +38,8 @@ extern UINT GRAPHICS_FORCE_REFRESH;
extern std::optional<uint32_t> GRAPHICS_FORCE_REFRESH_SUB; extern std::optional<uint32_t> GRAPHICS_FORCE_REFRESH_SUB;
extern std::optional<int> GRAPHICS_FORCE_VSYNC_BUFFER; extern std::optional<int> GRAPHICS_FORCE_VSYNC_BUFFER;
extern bool GRAPHICS_FORCE_SINGLE_ADAPTER; extern bool GRAPHICS_FORCE_SINGLE_ADAPTER;
extern bool GRAPHICS_PREVENT_SECONDARY_WINDOW; extern bool GRAPHICS_PREVENT_SECONDARY_WINDOWS;
extern bool GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS;
extern std::optional<std::pair<uint32_t, uint32_t>> GRAPHICS_FS_CUSTOM_RESOLUTION; extern std::optional<std::pair<uint32_t, uint32_t>> GRAPHICS_FS_CUSTOM_RESOLUTION;
extern std::optional<std::pair<uint32_t, uint32_t>> GRAPHICS_FS_CUSTOM_RESOLUTION_SUB; extern std::optional<std::pair<uint32_t, uint32_t>> GRAPHICS_FS_CUSTOM_RESOLUTION_SUB;
extern bool GRAPHICS_FS_ORIENTATION_SWAP; extern bool GRAPHICS_FS_ORIENTATION_SWAP;
@@ -55,6 +56,10 @@ extern bool GRAPHICS_WINDOW_ALWAYS_ON_TOP;
extern bool GRAPHICS_WINDOW_BACKBUFFER_SCALE; extern bool GRAPHICS_WINDOW_BACKBUFFER_SCALE;
extern bool GRAPHICS_WINDOW_DISABLE_ROUNDED_CORNERS; extern bool GRAPHICS_WINDOW_DISABLE_ROUNDED_CORNERS;
extern std::optional<HWND> GRAPHICS_HOOKED_WINDOW; extern std::optional<HWND> GRAPHICS_HOOKED_WINDOW;
extern std::string GRAPHICS_GITADORA_MAIN_MONITOR;
extern std::string GRAPHICS_GITADORA_LEFT_MONITOR;
extern std::string GRAPHICS_GITADORA_RIGHT_MONITOR;
extern std::string GRAPHICS_GITADORA_SMALL_MONITOR;
extern bool GRAPHICS_IIDX_WSUB; extern bool GRAPHICS_IIDX_WSUB;
extern std::optional<std::string> GRAPHICS_WSUB_SIZE; extern std::optional<std::string> GRAPHICS_WSUB_SIZE;
@@ -115,6 +120,22 @@ bool graphics_window_resize_breaks_game();
bool graphics_window_move_and_resize_breaks_game(); bool graphics_window_move_and_resize_breaks_game();
void graphics_load_windowed_subscreen_parameters(); void graphics_load_windowed_subscreen_parameters();
void graphics_window_check_bounds_before_creation(int &x, int &y, const int width, const int height); void graphics_window_check_bounds_before_creation(int &x, int &y, const int width, const int height);
bool graphics_gitadora_is_borderless_windowed();
void graphics_gitadora_apply_window_style(DWORD &style, DWORD &style_ex);
bool graphics_gitadora_apply_window_monitor(
const std::string &window_name,
int &x,
int &y,
int &width,
int &height,
bool log_change);
bool graphics_gitadora_has_window_monitor(const std::string &window_name);
void change_primary_monitor(const std::string &monitor_name); void change_primary_monitor(const std::string &monitor_name);
void update_monitor_on_boot(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();

Some files were not shown because too many files have changed in this diff Show More