Compare commits

..

355 Commits

Author SHA1 Message Date
bicarus 74e619df37 sdvx: fix check for enable_console in avs_config.xml (#838)
## Link to GitHub Issue or related Pull Request, if one exists
Regressed by #657 

## Description of change
Workaround for SDVX4 was applied too broadly and caused sdvx 1/2/3 to
not boot, depending on contents of avs-config.xml.

`property_search_safe` throws a fatal error if the node is not present.
What we wanted to do was to check for presence.

## Testing
sanity checked sdvx 1/2/3/4
2026-07-26 16:16:25 -07:00
bicarus 10a97b9c63 signal: dump exception context, log dump creation failure (#837)
More diagnostics info for game crashes.
2026-07-26 15:25:24 -07:00
bicarus 159043803c imgui: fix crash when launching from UNC path (network shares) (#836)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
ImGui filebrowser extension crashes due to MinGW quirk about UNC path
handling.

## Testing
2026-07-26 04:30:06 -07:00
bicarus bd2fbfcb67 touch: restore wintouchemu, fall back to it when native touch hooks fail (#834)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #833

## Description of change
Last couple PRs - such as #820 #827 #828 - made the native touch hook &
touch injection using `InjectTouchInput` the default path, since it
performs much more reliably with both real touch screens and mouse (or
any other synthetic source).

However, user has reported that WINE lacks `InjectTouchInput` which
means this won't work.

As a fix, revive the old wintouchemu code. Native touch is still the
default, but under following circumstances:

1. if `-touchemuforce` is set, or
2. if any of the required Windows touch APIs are unavailable

then we fail over from native touch to wintouchemu code. 

For Linux, condition #2 would be hit during init, and gracefully switch
over.

Caveat: the poke code for IIDX and Nost will continue to require native
touch, I do not want to maintain two paths for this. This means that
iidx poke will stop working on Linux, unfortunately.

## Testing
Tested on Windows with `-touchemuforce` set. This is mostly reverting
Linux code path back to where we were last release, so this should just
work with wine.
2026-07-26 04:29:45 -07:00
bicarus 64600826b7 nost: touch piano mode (#830)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Touch piano mode. Allows piano keys to be played with the touch screen.
Since the touch screen is still needed for menu navigation (including
during songs) a toggle switch is added so that the user can flip between
navigation mode and piano mode.

The touch targets are pixel-perfect. Don't let the game's piano
animations fool you.

## Testing
2026-07-23 15:49:50 -07:00
bicarus 80deb4bbbd iidx: hide tdj sub screen in fullscreen single monitor mode (#829)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Prevent the game from launching a blank window when launching fullscreen
with sub disabled.

## Testing
2026-07-22 14:37:29 -07:00
bicarus 00c8bb8ce6 gitadora: use native touch for single-window mode (#828)
## Link to GitHub Issue or related Pull Request, if one exists
#814 

## Description of change
Make gitadora arena model use native touch instead of wintouchemu. Only
single window mode with overlay is affected.

This removes the last consumer of `INJECT_MOUSE_AS_WM_TOUCH` so all of
that code can be deleted.

## Testing
Tested touch screen and mouse interaction with single window mode.
2026-07-22 01:36:06 -07:00
bicarus 09ed8b948c nost: move nostalgia from wintouchemu to native touch hook (#827)
## Link to GitHub Issue or related Pull Request, if one exists
#814

## Description of change
Switch over Nostalgia from wintouchemu to native touch hook.

Nostalgia has some strict timing requirements (touches must be updated
on every acio poll) so this takes a slightly different path to maintain
mouse holds.

Only a handful of consumers of wintouchemu remain:

1. beatstream - but it's off by default, only enabled as errata for
buggy touchscreens, or if the user forces it on (for Show Cursor)
2. gitadora single-window overlay - this is just for mouse so not a big
deal.
3. MFC HG mode - not a priority to fix.

## Testing
Tested full screen and windowed mode with touch / mouse / poke.
2026-07-22 00:56:03 -07:00
bicarus e2daefbb6a overlay: can't press enter to select sub-tab in Controller tab (#826)
## Link to GitHub Issue or related Pull Request, if one exists
Another bug reported by a sea creature

## Description of change
Fix keyboard navigation in Controllers sub tabs.

## Testing
2026-07-21 21:13:18 -07:00
bicarus 39bf17aafe patcher: fix plus minus buttons for number patches (#825)
## Link to GitHub Issue or related Pull Request, if one exists
Reported by a certain sea reptile

## Description of change
Fix the + / - buttons for number patches not working. Regressed by one
of the ImGui version updates.

## Testing
seems fine now
2026-07-21 17:46:18 -07:00
bicarus 4836cd94ab touch: inject touches from spiceapi to native touch hooks (#824)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #814

## Description of change
Touches via the API wasn't being delivered to native touch hook;
implement this, which closes the last gap we have vs. wintouchemu.

While we're here, implement popn subscreen support for companion as
well.

## Testing
Tested for iidx/sdvx/popn
2026-07-21 16:57:11 -07:00
bicarus 9d41ca2515 nost: xact audio hook (#823)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #822

## Description of change
Add hooks for `libxact.dll` which only Nostalgia uses for audio. Detect
cases where the game would fail to launch (wrong number of channels) and
log a warning / deferred log.

## Testing
tested 2024102200
2026-07-21 15:33:30 -07:00
bicarus 18421ffbfe touch: make native touch the default for iidx/sdvx/popn (#820)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #814

## Description of change
Remove iidx/sdvx/popon `native touch` options.

Remove dead code in wintouchemu that deals with these games.

Native touch is now at feature-parity with wintouchemu. Native touch
options work better with touch screens, while rawinput-based wintouchemu
has compat issues on some touchscreens. For these subscreen games
deprecate the usage of wintouchemu and make them use native touch stack
by default.

### For future consideration
Gitadora - not sure what to do with this one. We only use wintouchemu
for the single window case. For all other configurations, the game
accepts mouse and touch input without any hooks, so perhaps nothing
needs to be done here.

Nostalgia - consider moving away from wintouchemu and adopt native touch
hook here as well.

## Testing
WIP
2026-07-21 02:15:56 -07:00
bicarus 3f0921d983 gitadora: more windowed mode fixes (#819)
## Link to GitHub Issue or related Pull Request, if one exists
User reported

## Description of change

Sometimes the game moved the main window after `CreateWindowExA`
returned but before D3D initialization called
`graphics_capture_initial_window`. During this gap,
`GRAPHICS_HOOKED_WINDOW` was not set, so the placement hooks could not
identify the main window and preserve its monitor override. GITADORA
main, LEFT, and RIGHT windows are now registered immediately after
creation so later placement calls cannot undo their overrides.

This also adds `-gdwsmallsize` and `-gdwsmallpos` for explicitly setting
the SMALL window size and position. Explicit values override the
corresponding monitor-derived geometry, allowing the SMALL window's
10:16 aspect ratio to be preserved instead of stretching it to fill a
monitor.

## Testing
2026-07-21 00:54:09 -07:00
bicarus ed7318c270 rawinput: modifiers (key combinations) (#813)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #635

## Description of change
Adds optional **button modifiers**, allowing a binding to require one or
more "modifier" buttons to be held before it activates.

- New "Modifiers" controller page in the overlay config to bind the
Modifier 1–4 source buttons.
- Each binding gains a 4-bit modifier_mask (Modifier 1-4). It is
persisted as an optional modifiers XML attribute on button nodes and
controller-preset entries. The attribute is optional and defaults to 0,
so existing config files and presets load unchanged and the feature is
off unless the user opts in.
- The button Edit properties popup gains a "Modifiers" dropdown to pick
which modifiers a binding requires. Doesn't apply to MIDI though.
- Input evaluation skips a binding whose required modifiers are not
held, falling through to its alternatives; velocity reporting uses the
same gated path.
- Controller-preset templates gain a "Modifiers" group for importing and
exporting.

## Testing
2026-07-21 00:26:36 -07:00
bicarus 623e1e3998 touch: inject mouse and poke into native touch modes (#815)
## Link to GitHub Issue or related Pull Request, if one exists
Part 1 of many fixes for #814.

## Description of change
For the native touch hook - add code to inject synthetic touches using
Windows API (`InjectTouchInput`).

Note that this is Windows 8+ only, but we can make an assumption that we
are running on Win10+ for these games (TDJ/UFC/High Cheers) since the
cabs assume Win10.

Detect mouse events and allow IIDX poke to call into this to inject
synthetic touches.

Also, update the nativetouchhook to independently calculate window size
and rotation, instead of relying on rawinput layer.

## Testing
Tested to work with native touch option on IIDX, SDVX, POPN, all full
screen / windowed / sub on/off combinations.

Edge cases:

* sdvx main monitor rotated 270 deg
* touch invert option
* windowed mode resize / moved

All seem to work.
2026-07-21 00:04:38 -07:00
bicarus 558e3d0cb3 rawinput: avoid enumerationg adapter modes on first touch (#817)
## Link to GitHub Issue or related Pull Request, if one exists
Regressed by #790

## Description of change
Avoid enumerating every display mode with `EnumDisplaySettings` during
the first raw touchscreen report, as it can take a couple hundred
milliseconds.

The latency-sensitive path now refreshes only the current display size
using `GetWindowRect(GetDesktopWindow())` and orientation using
`EnumDisplaySettingsEx(..., ENUM_CURRENT_SETTINGS, ..., EDS_RAWMODE)`.
Full `EnumDisplaySettings` enumeration runs only when native resolution
is needed for aspect compensation.

Jubeat and Reflec Beat now enable aspect compensation before
initializing touch input.

## Testing
Sanity checked rb/jb on laptops with non-16:9 ratio
2026-07-20 03:00:34 -07:00
bicarus d0db547dd0 sdvx: implement NVAPI for non-NVIDIA GPUs (#816)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change

In summary: for SDVX VM mode, this PR eliminates the need for `Note FPS
Target 120Hz` / `Game FPS Target 120Hz` patches typically used for AMD
GPU via synthetic responses provided via a fake NVAPI layer.

Before this change, playing on AMD GPU required patches to force the
game to render at 60Hz (See #107 for relevant discussion). Basically, if
NVAPI query fails, the game shows

`I:NvDisplayConfig: GetMainDisplayRefreshRate = 60.000hz (DUMMY)`

the game renders at 120Hz but the notes render at 60Hz, which would look
stuttery.

After this change, even without a real `nvapi64.dll`, spice will provide
a stub implementation, just enough to convince the game that the main
display is 120Hz, which causes notes to render at 120Hz as well.

This can be overridden by `-graphics-force-refresh` option.

Important note: if user has the `Note FPS Target 60Hz` patch but running
the game at 120Hz, before this change the notes would render at 60Hz,
but after this they will be at 120Hz. I don't know why anyone would
intentionally lower the notes framerate, so this is not going to be
fixed. If the user wants to run the game at 60Hz they can do so via
`-graphics-force-refresh`.

Another change - `-nonvapi` will always force down this synthetic nvapi
path, even if the system has the real `nvapi64.dll`. Even on NVDIA GPU
this option can be used to force this.

## Testing
2026-07-19 23:43:14 -07:00
bicarus be4bb00390 patcher: patch groups (#812)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #245

## Description of change

Introduce patch groups. JSON schema now allows for a `"type": "group"`:

```json
    {
        "type": "group",
        "gameCode": "LDJ",
        "id": "timer-freeze",
        "name": "Timer Freeze Patches",
        "description": "Freezes various timers in the game."
    },
    {
        "name": "Standard/Menu Timer Freeze",
        "description": "Freezes all non-premium area timers.",
        "caution": "",
        "gameCode": "LDJ",
        "type": "memory",
        "group": "timer-freeze",
        "patches": [
            {
                "offset": 9962743,
                "dllName": "bm2dx.dll",
                "dataDisabled": "0F84",
                "dataEnabled": "90E9"
            }
        ]
    },
    {
        "name": "Premium Free Timer Freeze",
        "description": "Freezes all premium area timers.",
        "caution": "",
        "gameCode": "LDJ",
        "type": "memory",
        "group": "timer-freeze",
        "patches": [
            {
                "offset": 9111965,
                "dllName": "bm2dx.dll",
                "dataDisabled": "7E",
                "dataEnabled": "EB"
            }
        ]
    },
```

In the UI, these will show up as tree nodes. The parent is not actually
treated as a patch; e.g., its state is not saved to the patch manager
config file; only the child patches states are managed, the parent's
state only bubble up only in the UI.

In downlevel versions of spice, group parents will show up as a patch of
invalid type. Child patches will still show up as individual patches.

## Testing
*how was the code tested?*
2026-07-19 02:10:13 -07:00
bicarus a8cc85531c overlay: disable subscreen overlay if "native touch" option is on (#811)
## Link to GitHub Issue or related Pull Request, if one exists
#345 

## Description of change
If IIDX/SDVX/POPN "native touch" option is enabled, show an error
message if user toggles subscreen window.

Those options are meant for users with touch screens, and disables mouse
interactions. Users who like to randomly turn on options often latch on
to this option and get confused when clicks don't register.

## Testing
2026-07-18 21:49:50 -07:00
bicarus 2dea0d27bf rawinput: fix hotplug for keyboard and mouse (#810)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Fix a long-standing issue of hotplug not working for keyboards and mice.
The code only registered for HID notifications; we also need to do it
for the keyboard GUID and mouse GUID.

## Testing
Confirmed that keyboards and mice can be hot unplugged and plugged.
2026-07-18 17:38:59 -07:00
bicarus 61a0220c1a audio: retain WASAPI clients only after initialization (#807)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #782 

## Description of change

This addresses an issue only seen on Linux + WINE.

Defer retaining WASAPI clients until `Initialize` succeeds.

Before this PR, temporary clients created during DirectShow device
capability probing were retained as active audio clients, causing
Wine/Linux crashes in DDR and popn.

Also covers IAudioClient3, dummy, ASIO, WaveOut, and null-device
initialization paths, for consistency.

## Testing
WIP
2026-07-18 03:09:37 -07:00
bicarus c080bbe301 rawinput: avoid deadlock when binding analog axis as a button (#809)
## Link to GitHub Issue or related Pull Request, if one exists
Regressed by #793

## Description of change
Due to lock inversion, when binding an analog axis as a button, spice
deadlocks. Fix that.

Also create a separate `unordered_map` that keeps track of device
handles so that `WM_INPUT` handle can look up devices without having to
acquire the larger `devices_mutex` which could be held by (potentially)
lengthy operations like hotplug.

Fix more synchronization issues around hotplug. Latent bug exposed by
MIDI 2.0 issues.

## Testing
2026-07-18 03:09:27 -07:00
bicarus e1d1b39567 patcher: refactor (#808)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Refactor patch manager.

This one file had the UI logic, config save/load, parsing patches JSON,
applying memory patches, everything all in one cpp file. Refactor and
separate out the layers.

## Testing
Sanity checked patch workflows.
2026-07-17 03:48:01 -07:00
bicarus-dev 2aae8ad9ca move updater scripts 2026-07-15 02:14:19 -07:00
bicarus 1b6d1af951 jb, shogikai: double buffer overlay, run at ~60fps (#805)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Make small tweaks to how scaling works.

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

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

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

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

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

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

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

## Description of change
Two changes:

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

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

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

This was causing two issues:

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

Address both.

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

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

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

Fixes #697

## Description of change

Adds aspect-ratio compensation for raw input touch handler.

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

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

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

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

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

Notes:

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

## Testing

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

## Description of change
Updates usage of `windows_dll_compat_checker`

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

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

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

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

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

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

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

## Testing
Worked fine for FlexASIO / Xonar AE / Realtek ASIO though none of these
drivers repro the crashing behavior.
2026-06-28 22:37:48 -07:00
bicarus cf86fbd238 graphics: remove static import of d3d9 (#780)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #779
Regressed by #720 

## Description of change
#720 introduced a static dependency on DX9 which caused `d3d9.dll` to be
loaded at boot from `system32`. Some third party hooks (like
ifs_layeredfs and dxvk) rely on supplying a custom `d3d9.dll` in the DLL
search path (usually in modules) but this change caused Windows to skip
that check.

Remove the hard dependency on d3d9 and add a CMake check to ensure that
compiled binaries do not accidentally introduce new static imports in
the future.

## Testing
Confirmed that dxvk runs again.
2026-06-27 15:23:59 -07:00
bicarus 3fdb369128 sdvx: option to disable Live2D (#777)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Adds an option to disable Live2D. Can be disabled everywhere, or just
during a song.

## Testing
Tested EG final and recent Nabla.
2026-06-27 02:34:19 -07:00
bicarus db7defff5a overlay: fix OS cursor showing up in some games (#776)
## Link to GitHub Issue or related Pull Request, if one exists
regressed by #766, fixes #775 

## Description of change
Some games like DDR leave `ShowCursor` ref count at non-negative number,
so when spice handles `WM_SETCURSOR` to change the cursor shape, it ends
up showing the OS cursor.

## Testing
2026-06-25 15:30:59 -07:00
bicarus 451cbec0b9 overlay: OBS control window via WebSocket API (#773)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Adds an overlay window widget for controlling OBS over WebSocket.

Add new lines to the FPS widget that shows recording / streaming timers.

Show notifications for major state changes (stream started/stopped,
recording started/paused/resumed/stopped)

## Testing
2026-06-24 02:44:36 -07:00
bicarus 5c69e295ab ccj: fix mouse trackball cursor wrap (#774)
I don't have a repro but a user reported that the cursor doesn't wrap
around on one side. Correctly account for windows bleeding over to other
monitors.
2026-06-21 17:03:44 -07:00
bicarus 5065a92d55 launcher: don't warn about hook option conflicts (#771)
DLL hook options (-k and -z) allow multiple flags being specified in
both spicecfg and via command line; they never conflict as they are
additive. Remove the warning about option conflicts.
2026-06-20 15:46:45 -07:00
bicarus 5483e8e2c0 ponp: make Force Sub Redraw an option (#770)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Similar to how it is on SDVX, turning this on can cause graphical
glitches depending on the GPU, so it needs to be an option that users
can toggle.

## Testing
2026-06-19 22:30:08 -07:00
bicarus e8949a2612 overlay: fix highlights, alignment in presets table (#769)
* Fix table row highlight hover detection in Options tab when row has
two lines of text
* Fix text alignment in controller presets table
2026-06-19 00:40:54 -07:00
bicarus db71a0b24d cfg: center window on launch (#768) 2026-06-18 23:02:23 -07:00
bicarus d3d5422768 cfg: enforce minimum window size (#767) 2026-06-18 21:10:42 -07:00
bicarus-dev 5185f753e5 fix truncated text 2026-06-18 17:32:59 -07:00
bicarus eb897d0f1b overlay: fix cursor visibility (#766)
After #739 there was a regression where the cursor visibility became
inconsistent if the FPS display was shown and then another overlay
window was toggled. Fix the imgui cursor toggle logic to only check for
the overlay layer visibility.
2026-06-17 20:12:56 -07:00
bicarus 50fc80a1c3 graphics: respect Windows dark mode (#765)
Only affects the caption bar.

Should work for:

- [x] standalone configurator
- [x] dx9 games, including multi-window games like gitadora
- [x] Unity games
2026-06-17 11:51:31 -07:00
bicarus 301e15c44d overlay: make tabs bigger (#764) 2026-06-17 01:14:50 -07:00
bicarus-dev b6f886a7c5 fix option text for -iidxtdjsubsize 2026-06-16 01:47:47 -07:00
bicarus 0b085286d3 cfg: ignore nolegacy (#763)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #762

## Description of change
Fix -nolegacy causing input to be not processed in spicecfg

Need to fix `run_cfg` and `CONFIGURATOR_STANDALONE` for real soon.

## Testing
2026-06-15 14:48:18 -07:00
bicarus 29d3d9bb52 overlay: add sorting to patches table (#761) 2026-06-15 14:47:15 -07:00
bicarus-dev bf05a3ef30 update crash message 2026-06-15 12:55:15 -07:00
bicarus 6280ac3b0a imgui: update imgui to head of docking branch (v1.92.8) (#756)
## Tested:

- [x] LDJ with sub
- [x] sdvx
- [x] dx11 game
2026-06-15 10:40:38 -07:00
bicarus-dev cc9df27569 change ea url template 2026-06-15 10:33:34 -07:00
bicarus ac51acdafe cfg: fix config save atomicity (#760)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Fully flush `tmp` file and then use `MoveFileExW` instead of `CopyFileW`
which guarantees atomic operation on NTFS.

## Testing
2026-06-15 02:06:04 -07:00
bicarus 6fec169347 overlay: toggle main menu independently (#759)
Recently there was a change to make the main menu also toggle the
overlay; revert that.
2026-06-15 01:45:02 -07:00
bicarus 4af006e869 wasapi: address buffer overflow when using -wasapishared (#758)
## Link to GitHub Issue or related Pull Request, if one exists
Building on #745 

## Description of change
With `-wasapishared`, some users see `AUDCLNT_E_BUFFER_TOO_LARGE`
(`0x88890006`).

The existing one-device-period buffer clamp wasn't enough on endpoints
with a small shared buffer; the shared buffer is double-buffered, so a
full-buffer write often exceeds the free space.

### Fix

Added a FIFO bridge to `SharedRedirect` that decouples the game's
per-event writes from the shared engine's clock:

- **`GetBuffer`** hands the game a pointer into the FIFO tail to write
in place.
- **`ReleaseBuffer`** commits the write and drains `min(pending,
device_free)` frames to the device - so a write can never exceed what
the device accepts, structurally preventing the overflow on any
endpoint.
- **`GetCurrentPadding`** reports the FIFO fill level (capped to the
reported buffer size) so poll/timer-driven games pace correctly against
the virtual buffer.

## Testing
2026-06-14 19:57:28 -07:00
bicarus-dev 256ef4d341 update -wasapishared option text 2026-06-14 17:53:01 -07:00
bicarus 698f2ffc2c overlay: add vertical row padding (#757)
Slightly decrease information density by adding a couple pixels of
vertical padding to each table row in the configurator.
2026-06-14 12:29:32 -07:00
bicarus 4879a35eb8 overlay: remove keyboard / gamepad tab navigation (#755)
Tired of constantly fighting unintentional keyboard navigation behavior.

Also, remove `ImGuiConfigFlags_IsTouchScreen` which literally does
nothing.
2026-06-12 21:28:34 -07:00
bicarus fd7e850789 ccj: fix mouse wheel input in overlay (#754)
## Link to GitHub Issue or related Pull Request, if one exists
#251 

## Description of change
CCJ registers for rawinput mouse. We allowed the rawinput registration
to go through to the OS, which prevents spice's rawinput stack from
using the mouse. This caused the overlay to not accept mouse input. Fix
it by not letting the rawinput registration through. Luckily, mouse
input continues to work properly in CCJ (for touch).

Also, fix a bug in DX11 overlay when toggling the main menu.

## Testing
2026-06-12 03:42:59 -07:00
bicarus 0cf07c38da overlay: left nav for cards tab (#753)
Enable left nav for Card tab, splitting the single page into multiple
sub tabs.

Card Manager is now embedded in this UI (but can still be launched as a
separate window)
2026-06-12 00:57:52 -07:00
bicarus 1f073b2d75 overlay: combine buttons/analogs/overlay/lights (#752)
Same idea as #747 except for input/output binding.
2026-06-11 23:38:09 -07:00
jessemarthin 3037214932 Fix capture.get_jpg blocking during game load for Companion Mirror (#750)
Add a timeout and skip signalling to the D3D9 capture path so the API
thread no longer waits indefinitely when Present stops during loading.
Return the last successful JPEG frame as a fallback.

## Link to GitHub Issue or related Pull Request, if one exists
#746

## Description of change
* Fix `capture.get_jpg` blocking indefinitely when D3D9 screen capture
cannot
  complete during game loading (e.g. no Present calls).
* Add a 2-second timeout to `graphics_capture_receive_jpeg()` and cancel
  pending capture requests on timeout.
* Signal capture skip from D3D9 failure paths via
`graphics_capture_skip()`.
* Cache the last successful JPEG per screen and return it as a fallback
  when a new capture fails.
Tested with **stock iOS Spice Companion** only. I have not tested
Android or
other Companion clients. The iOS client implementation differs from the
others; this fix is server-side only.
No config file, CLI option, or API schema changes.

## Testing
* Built locally with llvm-mingw cross-compiler (WSL).
* Docker build (`src/spice2x/build_docker.sh`): Pending
* Tested with stock iOS Spice Companion on iPad: Mirror remains
connected
  during game loading instead of returning to KeyPad.
2026-06-11 19:14:47 -07:00
drmext 18d6d9783a overlay: fix Options tab contents jitter (#751)
## Link to GitHub Issue or related Pull Request, if one exists
#747

## Description of change
The old approach called ImGui’s `SetScrollHereY()` while the options
list was still being laid out. That caused one-frame scroll corrections
and edge-snap jitter near scroll boundaries.

The new approach in `build_options_tab()`:

1. **On click** - set a flag (`options_scroll_pending` for categories,
`options_scroll_top` for group headers).
2. **During layout** - when building the target category, record its
content position with `GetCursorPosY()` (don’t scroll yet).
3. **After all options are built** - apply scroll once by setting
`window->Scroll.y` directly, and only if the position actually needs to
change (0.5px threshold).

That deferred, single-pass scroll avoids the shake while still
supporting scroll-up, scroll-down, and re-click-to-recenter.


## Testing
In spicecfg.exe and in-game overlay, Options tab:

Open groups with many categories

Scroll content down manually, then click a higher category in the left
nav - content scrolls up to that section

Re-click the same category after manual scroll - jumps back without
shake

Rapid re-clicks when already aligned - no jitter

Switch group headers - still scrolls to top; re-click at top - no shake
2026-06-11 14:03:33 -07:00
bicarus 267add3227 iidx: set SOUND_OUTPUT_DEVICE even when -iidx is not enabled (#749)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
For cab usage, when `-exec bm2dx.dll` is used without `-iidx` then
SOUND_OUTPUT_DEVICE never gets set.

Change this so that `SOUND_OUTPUT_DEVICE` gets set unconditionally if
`-iidxsounddevice` is set.

## Testing
2026-06-11 00:40:53 -07:00
bicarus a170627f52 overlay: redesign Options tab(s) (#747)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Combine API/Options/Advanced/Development/Search tabs into one Options
tab and add a left nav for navigation categories.

Reduce information density for options table by making each row double
the height and slightly increasing widget sizes.

## Testing
2026-06-11 00:17:54 -07:00
drmext 5c244eaca7 improve E004 card generation and placeholder pcbid (#748)
## Link to GitHub Issue or related Pull Request, if one exists
#73

## Description of change
Generated cards now start with E0040100 to fully match real cards. The
placeholder example `E004010000000000` is replaced with
`E0040100FFFFFFFF`.

The invalid placeholder/fallback PCBID `04040000000000000000` is
replaced with `01201000000000010101`, which contains a valid
header/checksum, making it closer resemble a real PCBID while still
clearly being an example.
2026-06-10 23:24:43 -07:00
bicarus 6bcadccf09 audio: WASAPI Force Shared Mode option (#745)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
This new option, `-wasapishared`, detects when the game opens WASAPI
Exclusive stream and forcibly opens a shared mode stream instead on the
real device.

#### Benefits of this:

1. No need for `force wasapi shared` patches
2. No need to change sample rate of audio devices before starting up
games
3. Adds ability to boot in shared mode even for games that do not have
shared wasapi changes (old GITADORA, popn HC)

As a result, this option is strictly better than `shared wasapi` patches
available for many games.

#### Downsides:
1. OS resampling will add a small latency, but not big enough to be
noticeable.
2. Using shared mode instead of exclusive mode adds latency, of course.

(If you cared about latency, you would use the low latency option, or
use exclusive or asio)

Basically it's the "make things work" button for audio that should work
for almost all games that we support.

The option has no effect if the game opens in shared mode.

## Testing
SDVX7 - ok
GITADORA GW - ok
popn HC - ok
IIDX - ok
2026-06-10 00:20:59 -07:00
bicarus-dev ccb3bac48c defer error for -exec 2026-06-09 00:09:00 -07:00
bicarus 0a7ecf82a9 build: produce shared objects for spice.exe / spice_laa.exe, disable 64-bit winxp builds (#744)
to speed up clean builds. GitHub CI time went from 20 minutes -> 10
minutes.
2026-06-08 17:42:41 -07:00
bicarus-dev 1c6929dd84 Move full screen Monitor options to Graphics (Full Screen) 2026-06-08 15:53:54 -07:00
bicarus 1d3d9040f2 Update bug_report.md 2026-06-08 00:51:48 -07:00
bicarus 8f1069628e Update bug_report.md 2026-06-08 00:50:33 -07:00
bicarus-dev f8c585a9d1 update main menu layout 2026-06-08 00:27:42 -07:00
bicarus 9651051990 overlay: make overlay toggle behavior more consistent (#743)
Clean up how various overlay toggle shortcuts work with the main menu
visible.
2026-06-07 21:56:06 -07:00
bicarus a54a62d0d7 overlay: fix dummy marker spacing (#741)
Fix alignment in patches tab
2026-06-07 19:14:43 -07:00
bicarus 100caa0f5c overlay: refactor overlay layering (#739)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change

Create three distinct layers for the overlay:

1. Bottommost persistent layer - non-interactable layer that is always
on. This was only for notifications, but now the FPS widget lives here
as well.
2. Overlay windows layer - most interactable windows go here.
3. Topmost main menu - this is reserved for the main menu (escape key)
and this is a modal dialog that occludes the layers below.

Why? 

- `toggle overlay` behavior with FPS widget *also* toggling on/off was a
bit confusing (now they're two separate keys)
- FPS widget is popular, but it caused the entire overlay to be active,
which affects how input is handled
- the main menu being a standalone window was a little awkward (now it's
a modal)

## Testing
2026-06-07 17:23:08 -07:00
bicarus-dev bb87aa2944 add placeholder text for ea url option 2026-06-07 01:48:52 -07:00
bicarus 2e2968d1bc overlay: remove dependency on d3dcompiler (#738)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #737, regressed by #707

## Description of change
ImGui backend for DX11 pulled in dependency on a specific version of
`d3dcompiler_47.dll` which is not present on stock Win7 OS image.

Change this to a runtime load of the DLL and disable the overlay if we
can't find one.

## Testing
- [x] Win11, DX11 overlay
- [x] Win11, DX9 overlay
- [x] Win7, DX9 overlay
2026-06-06 14:06:37 -07:00
bicarus 0457262472 audio: asio "downmix" 7.1 to stereo (#736)
## Link to GitHub Issue or related Pull Request, if one exists
#730 

## Description of change
Add an option that extracts channels from 7.1 ASIO and presents to 2-ch
ASIO. Same idea as the SDVX 2ch fix except for gitadora we are
duplicating the center channel to front.

## Testing
Tested SDVX and GFDM Arena.
2026-06-06 03:11:40 -07:00
bicarus-dev f23c359114 minify patches json again 2026-06-05 18:05:48 -07:00
bicarus-dev 81e74dcb97 remove museca difficulty patches 2026-06-05 17:45:36 -07:00
bicarus-dev 8ead941c6c format patches json 2026-06-05 17:22:38 -07:00
bicarus ece03cabba cfg: asio driver selector must list both 32-bit and 64-bit drivers (#734)
## Link to GitHub Issue or related Pull Request, if one exists
#730 

## Description of change
spicecfg is a 32-bit application. When it uses the ASIO SDK it only saw
the 32-bit drivers.

There are some ASIO drivers that have different names under 32-bit and
64-bit (Xonar AE is one of them)... so we have to manually scan the
registry and surface both the WOW32 and WOW64 nodes.

## Testing
<img width="329" height="309" alt="image"
src="https://github.com/user-attachments/assets/7abecf76-6835-4df5-8c2e-e7b425130f4c"
/>

Checked both 32-bit and 64-bit configurator, identical results.
2026-06-05 02:05:03 -07:00
bicarus 4b7f68f920 gitadora: (arena model) flip realtek option (#733)
## Link to GitHub Issue or related Pull Request, if one exists
#730 

## Description of change
Flip the option - enable the Realtek hack by default, unless the user
chooses not to.

## Testing
Tested ASIO path and WASAPI path. WASAPI path should be unaffected as
the game just picks the default audio device.
2026-06-05 01:38:46 -07:00
bicarus 2dae86a6f2 gitadora: (arena model) simulate Realtek device, fix asio redirect hooks (#732)
## Link to GitHub Issue or related Pull Request, if one exists
#730, #718

## Description of change

Part 1)

When ASIO is in use, the game also looks for a Realtek device so that it
can open a WASAPI Exclusive mode stream for headphones.

Add an option to fake that, in case the user doesn't have a Realtek
device.

Part 2)

`-gdaasio` wasn't workign properly - fix the logic.

## Testing
Seems to work with FlexASIO, and Xonar with no real Realtek device.
2026-06-05 00:08:52 -07:00
bicarus 85058c2156 audio: create a wrapper for asio drivers (#731)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Create `WrappedAsio`, similar to how we wrap `IAudioEndpoint`

So far, the wrapper does these things:

1. logging (for diagnosis, since iidx and gfdm don't produce any logs
when asio succeeds)
2. iidx32+ hack to work around refcount mismatch issue
3. sdvx valk cab hack to force 2-channel audio ("downmixing" by taking
only the front channels)
4. honor volume boost

As a result of `#2` the mempatch was removed from `iidx.cpp` since we
can tackle it cleanly in the hook. `#3` also removes the need for manual
patches.

Real downmixing is hard & expensive on the CPU so it was not
implemented.

## Testing
Tested iidx/sdvx/gfdm with xonar and flexasio
2026-06-04 09:24:44 -07:00
bicarus ea2f4c5572 gitadora: (arena model) complain loudly about bad prop files (#729)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Refuse to load if `libaio.dll` exists but `<spec>` is set incorrectly
(almost certainly because the user copied old prop files from downlevel
version)

## Testing
2026-06-02 17:47:51 -07:00
bicarus 48033816a8 audio: WASAPI exclusive resampling, buffer size increase options (#727)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change

Resampler:
Implement resampler for exclusive mode streams, as we are seeing more
and more devices - not just laptops but onboard audio devices - that
only support 48khz and not 44.1khz. Should work with volume boost (gain
calculated inside resample) and also downmixer (hands off intermediate
scratch buffers).

Buffer size increase:
By default many of these games request a tiny buffer when in shared mode
(TDJ uses 3ms). On some audio setup this results in crackling due to
underflow. Add an option to forcibly increase the buffer size.

## Testing
With resampler set to 48kHz and buffer set to 20ms I can reliably boot
and play IIDX on my display port monitor's speakers; previously this
wasn't possible. IIDX is event-driven.

Tested SDVX7 as well at 48kHz, which opens timer-driven streams.
2026-06-02 01:55:50 -07:00
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
bicarus-dev 15bfffa1d3 Merge branch 'main' of https://github.com/spice2x/spice2x.github.io 2026-04-18 23:07:10 -07:00
bicarus-dev 18a9a47c33 remove dead code 2026-04-18 23:07:05 -07:00
bicarus 6838e69ebe popn: rest of the new cab lights (#651)
## Link to GitHub Issue or related Pull Request, if one exists
#618 

## Description of change
Add rest of the LED strip lights.

## Testing
Tested U region.
2026-04-18 22:17:20 -07:00
bicarus 26b3b64794 rawinput, touch: discard invalid touch points from device in hybrid mode (#649)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #648

## Description of change
Deal with devices in hybrid packet mode that send garbage data in second
packet when it's out of bounds (outside the number of contact points).
MSDN says they're supposed to be (0, 0), but some devices don't follow
the spec.

## Testing
I don't have a device that reports garbage values but this should fix it
in theory.

I tested on my Dell touch monitor which is in hybrid mode & sends 5
touches at a time, but can split across two packets to send up to 10
touches. It sends (0, 0) for the remaining slots when there are 6-9
touch points, but confirmed that there was no regression.
2026-04-18 16:22:00 -07:00
bicarus aadd2bdd59 cfg: deprecate -sdvx720, add new category for camera/printer options (#650) 2026-04-18 15:56:15 -07:00
Emma d0e24e31ca ftt: allow windowed mode resize options, add title (#646)
## Description of change
This code adds a window title to FutureTomTom when running in windowed
mode and allows the windowed graphics hooks to take effect.

## Testing
I ran the game and made sure fullscreen mode still worked as well as all
window move/resize functionality.
2026-04-16 14:32:06 -07:00
bicarus 4fb7f20c7b iidx: fix crash when ASIO compatibility fix patching fails (#645)
Logging method was crashing when the patching failed.
2026-04-16 00:42:40 -07:00
bicarus 6ddae70c5a util: fix dangling pointer issues with improper std::stringstream usage (#644)
## Description of change
`std::ostringstream.str()` returns a temporary string so it's invalid to
obtain c_str from it and pass it around.

## Testing
wip
2026-04-15 23:25:38 -07:00
bicarus b38160d6c2 popn: native touch option, keypad warning (#640)
#639 

the usual
2026-04-15 19:31:26 -07:00
Emma 231c998cc0 acio: implement ICCA slotted reader emulation for iidx18 (#642)
## Link to GitHub Issue or related Pull Request, if one exists
#121

## Description of change
This implements a seperate ICCA card workflow for the slotted readers
acting how Resort Anthem expects, enabling cards to be inserted.
Additionally this fixes a bug where both card units would be treated the
same (for keypad reads) under Resort Anthem.

The slotted reader workflow was implemented as a seperate function for
two reasons - it would be nice to not mess with any existing code to
avoid breaking all the functional games, and the way the slotted readers
are handled by RA seemed different enough to how everything was already
designed to where it didn't make sense to keep adding special edge cases
for RA.

## Testing
IIDX18 was tested and cards could be inserted on both P1 and P2 side.
IIDX19 through 22 was tested to make sure nothing had broken for
wavepass emulation.

## Known Issues
In IIDX18, when a card isn't set but the insert key is pressed, after
the card timeout the game will error out with an invalid state error.
2026-04-15 18:53:21 -07:00
Emma 127d31a85c graphics: add option to disable win11 rounded corners on windows (#641)
## Description of change
This adds a toggleable option to remove the rounded window corners when
running a game, or a subscreen, in a window on Windows 11 and upwards.

## Testing
On Windows 11: Resort Anthem, MÚSECA and EPOLIS were tested and the
window corners were removed from both main windows and the TDJ subscreen
when running in windowed mode, with no change in fullscreen mode.

On Windows 7: IIDX18 was tested with no change, the call to
`DwmSetWindowAttribute` is ignored as `DWMWA_WINDOW_CORNER_PREFERENCE`
doesn't exist.

For XP builds, the call is compiled out to avoid linking against dwmapi
and windows_dll_compat_checker returns successfully when testing against
the XP Embedded image. I can't test it on XP, but it ran on Vista.
2026-04-15 18:51:53 -07:00
bicarus a8419bbaa0 grpahics: fix borderless for ddr/gitadora (#638)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Allow DDR to be launched in borderless window (honor the spicecfg
option).

Prevent Gitadora arena model from changing the window decoration once
launched as it hangs the game.

## Testing
Tested 32/64bit ddr, gitadora arena.
2026-04-14 18:11:42 -07:00
bicarus 0fc430ceb3 popn: add io emulation for more lights and coin (#637)
## Link to GitHub Issue or related Pull Request, if one exists
#618 

## Description of change
Wire up coin and more lights to bi3a_hook.

## Testing
Tested on U region
2026-04-14 09:46:13 -07:00
bicarus e59a50b64c popn: boot with 3 monitors (#636)
## Link to GitHub Issue or related Pull Request, if one exists
#618 

## Description of change
Take the monitor hooks added for IIDX/SDVX #612 and apply it to popn as
well now that popn has a subscreen.

There is a debug option in game that actually uses three monitors, which
can be activated via hex edits. This will prevent that from working.

## Testing
Tested with 3 monitors, fs and windowed.
2026-04-13 23:02:56 -07:00
bicarus 2042b629e9 popn: allow game to launch on PCs with only one monitor (#633)
## Link to GitHub Issue or related Pull Request, if one exists
#618 

## Description of change
The game insists on having two monitors. Hook various DX9 and Win32
monitor APIs to fake a monitor if the user only has one. This correctly
convinces the game that it needs to draw two screens instead of just
one.

## Testing
WIP
2026-04-13 03:00:47 -07:00
bicarus 440b9ed749 overlay: fix how input is handled when window loses focus (#632)
## Link to GitHub Issue or related Pull Request, if one exists
#286 

## Description of change
When we migrated to event-based I/O for ImGui, it introduced an issue
where a button would be held down even when the window is not in focus,
because we didn't notify ImGui that the button was released (after the
window lost focus).

Fix the ImGui custom I/O handler so that it immediately releases all
buttons when the window focus is lost.

## Testing
tested on 32-bit windowed mode
2026-04-12 04:44:46 -07:00
bicarus 9936088286 popn: subscreen overlay (#631)
## Link to GitHub Issue or related Pull Request, if one exists
#618 

## Description of change

Add overlay for subscreen.

Add an option for no subscreen window.

Use the same "force redraw subscreen" logic we used in SDVX to fix the
same issue.

Known issues:

* still crashing when running windowed if there is only one monitor
* having only one monitor causes sunscreen overlay to not work
(NumberOfAdaptersInGroup issue?)

## Testing
2026-04-12 02:11:24 -07:00
bicarus 4d8a6c1952 graphics: fix window position if out of bounds (#630)
## Link to GitHub Issue or related Pull Request, if one exists
#618 

## Description of change
Before calling `CreateWindowEx[A/W]`, check dimensions of the window
about to be created, and check if it will be out of bounds (completely
or partially).

If it's completely out of bounds, move it to `(0, 0)` to prevent crashes
or rendering issues.

## Testing
Tested popn and sdvx.
2026-04-11 20:12:43 -07:00
bicarus af69cf9251 popn: new cab lights (#629)
## Link to GitHub Issue or related Pull Request, if one exists
#618 

## Description of change
Basic support for lights - subwoofer RGB and button lights.

## Testing
Tested on title screen and in-game.

Tested beat pop as well since that lights up the colors in white/blue.
2026-04-11 18:08:45 -07:00
bicarus 1d5550b1e8 popn: implement bi3a-hook (bio3) (#626)
## Link to GitHub Issue or related Pull Request, if one exists
#618 

## Description of change
Turns out running this with bi2x doesn't process buttons properly, so we
have to use bi3a.

## Testing
Seems playable now.
2026-04-11 14:43:45 -07:00
bicarus ec3f6639b7 ccj: fix locale issues with carding in (#628)
## Link to GitHub Issue or related Pull Request, if one exists
#624 

## Description of change
See bug. Add locale hooks specifically for CCJ.

also, fix y axis direction for XInput, it was flipped because XInput
uses opposite values from what RawInput does

## Testing
Tested card in / play one round with Windows regional format set to
French (France) and other language set to English.
2026-04-11 14:42:51 -07:00
bicarus 727b99effd rawinput: fix ID for rawinput devices (#627)
## Link to GitHub Issue or related Pull Request, if one exists
#616 

## Description of change
The IDs don't really get used anywhere if it's not a real HID or MIDI
device, but putting them for completeness since they show up in Controls
overlay menu.

## Testing
2026-04-11 12:04:12 -07:00
bicarus 8afff3aaca popn: implement bi2x hook (#623)
## Link to GitHub Issue or related Pull Request, if one exists
#618 

## Description of change
Implement BI2X hook I/O for service/test/coin/headphones and the deka
pop-kuns.
2026-04-10 17:46:12 -07:00
bicarus b281517429 popn: hook DisplayConfigGetDeviceInfo to allow for any GPU port to be used for monitor, get past I/O check (#622)
## Link to GitHub Issue or related Pull Request, if one exists
#618 

## Description of change
The game expects the main display to be *not* HDMI, and at port 2.

Hooking DisplayConfigGetDeviceInfo allows us to boot the game on any
monitor.

Add basic I/O hook. It gets stuck in `USB I/O Checking...` for a couple
minutes but it'll eventually skip and boot to title screen.

## Testing
ugh.
2026-04-10 02:56:32 -07:00
bicarus 54ec9f272e popn: new cab windowed mode (#621)
## Link to GitHub Issue or related Pull Request, if one exists
#618 

## Description of change
Get windowed mode to work.

## Testing
tbd... i/o doesn't work right now but it launches two windows at
least...
2026-04-10 01:05:44 -07:00
bicarus a159691097 xinput: bind UI, vibration, hotplug (#620)
## Link to GitHub Issue or related Pull Request, if one exists
#616 

## Description of change
`Naive` button is now called `Naive/XInput` and detects XInput
controllers

Add vibration output as "Lights"

Add proper hotplug support and multiple controllers

Fix misc bugs on x86

## Testing
I tested with two xbox controllers.
2026-04-10 01:04:20 -07:00
bicarus ca70f7b19c popn: distinguish between hpm and new popn (#619)
## Link to GitHub Issue or related Pull Request, if one exists
#618

## Description of change
Enhance game detection logic.

Fix heap size.
2026-04-09 19:54:27 -07:00
bicarus 7b4227d2a9 rawinput: add basic support for xinput (#617)
## Link to GitHub Issue or related Pull Request, if one exists
#616 

## Description of change
Adds preliminary support for XInput controllers. Enough to manually bind
buttons and map analogs.
2026-04-09 02:49:20 -07:00
bicarus f89efa2558 build: flatten file structure in release archives (#615)
Before:

```
.\spice2x\spice.exe
.\spice2x\spicecfg.exe
.\spice2x\spice64.exe
.\spice2x\stubs\..
.\spice2x\extras\..
```

after:

```
.\spice.exe
.\spicecfg.exe
.\spice64.exe
.\stubs\..
.\extras\..
```
2026-04-07 16:48:31 -07:00
bicarus 6f4824b648 misc: improve monitor logging (#614) 2026-04-06 22:22:34 -07:00
bicarus 45debeb5c9 iidx, sdvx: booting fullscreen with three or more monitors (#612)
## Link to GitHub Issue or related Pull Request, if one exists
#345 

Hoping that #180 is also addressed by this change, but I can't test it.

## Description of change

IIDX (TDJ) and SDVX (Valk) will now be able to boot in full screen even
with 3 or more monitors attached.

spice will "hide" from the game any monitors other than the primary
monitor and one other monitor for subscreen, allowing it to boot without
crashing - accomplished by clearing `DISPLAY_DEVICE_ATTACHED_TO_DESKTOP`
flag when monitors are enumerated over `EnumDisplayDevicesA`, and
fortunately both games seem to respect that flag.

Add options (one for IIDX, another for SDVX) that lets you specify the
monitor ID of the second monitor.

When the option is set, user-specified monitor will be used as the
subscreen. When the option is not set, spice will just pick the
lowest-indexed monitor as the subscreen, not counting the primary one of
course.

## Testing
Tested both games with three monitors. Should probably work for four or
more.
2026-04-06 02:59:12 -07:00
bicarus-dev a01f452eba fix build script 2026-04-06 02:37:40 -07:00
Will e452734bc1 Build a WinXP compatible version of Spice (#611)
This was a lot more background work than expected, but that is the
nature of unnecessary hobbies I guess. Most things in here are obvious,
so here's the non-obvious thoughts and notes:

1. All the XP stuff is optional, if the toolchain isn't available it
won't even try
2. The docker build is the "known good" build and now has all the
required tools for XP and checking binaries
a. If you're OK with it, can add the XP_MUST_BUILD flag to
build_docker.sh
4. New `SPICE_XP` flag to comment out blocks
5. We could technically build using the Docker image I've made for my XP
compatible LLVM toolchain, but combining the binaries into a single zip
at the end of it is somewhat tedious, so I've chosen to grab the
binaries from my release
6. I also wrote a DLL checker to make sure the final binaries are
_actually_ XP compatible, which found some issues on x86_64 (I don't
know a single game that runs on 64 bit XP, but it wasn't too much effort
so whatever). This also won't run if missing, just to save the trouble.
7. The build script is still pretty shit, I've made some minor
improvements
  a. `/usr/bin/env bash` instead of `/bin/bash` as it's more correct
  b. Adding `-it` to the docker build, so Ctrl+C actually halts it
c. Main issue is that failures don't halt, because of the custom error
catching function. This has caused a lot of frustration, maybe we can
change that

## Testing
It runs on 32 and 64 bit installs of XP. My jubeat cab is on Win10 these
days and I haven't got around to testing an actual game, but I've
distributed test builds of this to people who *have* confirmed it works
as expected.
2026-04-06 01:14:29 -07:00
bicarus d1779b93fa overlay: remove tab stop from invisible selectable used in options tab (#613)
#598 added extra tab stops to the invisible Selectable widgets, which is
only used for detecting mouse hover
2026-04-06 01:02:56 -07:00
bicarus fe9aac7e29 overlay: fix cosmetic issue in options tab when there are no options for the game (#610)
Number of columns was modified in #598 but it didn't correctly account
for the case where a game doesn't have any game-specific options.
2026-04-05 19:01:09 -07:00
bicarus-dev f3d887ebcf update strings 2026-04-05 17:03:23 -07:00
bicarus 336500c8ae ccj: fix D3D10Warp.dll_unloaded issue (#609)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Fix random crashes with CCJ.

This should remove the need to add `d3d10warp.dll` to `
preload_libraries` in `ee-config.xml`.

## Testing
Tried a couple games and didn't run into the crash..
2026-04-05 16:18:19 -07:00
bicarus ee3fa58bfa graphics: option to change primary monitor before launching game (#608)
## Description of change

Adds `-mainmonitor` option which changes the monitor layout before
launching the game. This is much more reliable than the old `-monitor`
option which operated at DX9 level. This works for TDJ/UFC subscreens,
for example.

Rename `-monitor` option to `-dx9mainadapter` to discourage use.
`-monitor` will continue to work, of course.

Changing of primary monitor happens first before any orientation changes
/ refresh rate changes, which means those options will result in the
logically correct configuration.

Create a new option category for `Monitor` (rotate, refresh rate, etc).

Add a monitor selection widget for `-mainmonitor` option in the
configurator.

Improve how we log names of monitors in the log during boot - should be
less of "Generic PnP Monitor" after this.
2026-04-04 23:26:38 -07:00
bicarus a4c3eddc2f graphics: rewrite auto-rotate and refresh rate options to be more reliable (#607)
## Description of change

Rotate monitor & change the refresh rate **well before** the game is
launched.

This is much faster and more reliable compared to what we do now, which
is trying to do monitor changes during CreateWindow hooks. For example,
SDVX makes a bunch of OS / NVAPI calls to figure out monitor geometry
while it's booting, and by the time CreateWindow is called, the game
already made up its mind, which leads to weird graphical issues.

This affects both windowed mode and full screen.

In addition, add options for normal orientation (landscape) and upside
down (landscape, flipped) options. This can be used if the monitor is
normally rotated to portrait, but the game needs to be in landscape.
2026-04-04 03:23:28 -07:00
bicarus e0f6cb2cbd api: dump key binding strings (#606)
Print out human readable text for vKey when dumping bindings.
2026-04-03 20:22:24 -07:00
bicarus 1ad6a9412e imgui: update imgui to v1.92.7, update dx9 backend to latest (#605) 2026-04-03 01:51:08 -07:00
bicarus 5af3e0d494 imgui: migrate to new event-based IO (#604)
## Link to GitHub Issue or related Pull Request, if one exists
#287

## Description of change
Switch over to new event-based I/O system in ImGui so that we can keep
up with ImGui updates.

Unlike #287, this change does not modify how mouse position is handled.
That continues to be supported by newer versions of ImGui.

## Testing
wip
2026-04-02 21:00:42 -07:00
bicarus-dev b1ee828457 add some logging to device reload 2026-04-01 02:51:03 -07:00
bicarus 0d60649715 overlay: small tweaks to cards tab (#602)
## Link to GitHub Issue or related Pull Request, if one exists
#593 

## Description of change
* Display a confirmation dialog when clicking on `Generate` for the
first time (to prevent accidental loss of card number)
* If `cardN.txt` contents are read back successfully, show card number
in green to indicate success, and red in case of failure.
2026-03-31 00:07:34 -07:00
bicarus 220b27159b ea3: don't treat failure from dll_entry_init as fatal (#601)
## Link to GitHub Issue or related Pull Request, if one exists
fixes #600
2026-03-30 02:31:08 -07:00
bicarus b06a0a7d47 overlay: card override input should be disabled when overridden with cmd line (#599) 2026-03-29 20:36:42 -07:00
bicarus d66af8e16b overlay: options tab(s) clean up (#598)
## Description of change
Merge option name and command line parameter columns.
Expand the size of widget column.
In Search tab, add a new column to display the category.
Add a highlight effect to rows when hovered over - apply this to all
tables in the configurator UI.
2026-03-28 18:21:53 -07:00
bicarus-dev ad366ecdd0 update fatal crash message 2026-03-27 18:46:08 -07:00
llm96 22cb4d689c utils: print DLL info for early hooks (#597)
## Link to GitHub Issue or related Pull Request, if one exists
#596

## Description of change
Adds call to `libutils::print_dll_info` for early hooks too.

## Testing
```
[2026/03/27 09:51:16] I:launcher: SpiceTools Bootstrap (x64) (spice2x)
[2026/03/27 09:51:16] I:launcher: 1.0-V-2026-03-27T07:46:10
[2026/03/27 09:51:16] I:launcher: spice2x is free & open source; if you paid money for it, you got scammed
[2026/03/27 09:51:16] I:launcher: visit https://spice2x.github.io to download the latest version for free
[2026/03/27 09:51:16] I:launcher: arguments:
                                      [...]
                                      -z C:\Users\Noe\AppData\Local\Programs\SpiceTools\hooks\64\mempatcher64.dll
                                      [...]
[2026/03/27 09:51:16] I:launcher: loading early hook DLL C:\Users\Noe\AppData\Local\Programs\SpiceTools\hooks\64\mempatcher64.dll
[2026/03/27 09:51:16] I:superexit: enabled
[2026/03/27 09:51:16] I:libutils: DLL info for mempatcher64.dll: CompanyName = aixxe, ProductName = mempatcher, Version = 4975386c637a86e8daa89d479a0c67ab28d792ec
```
2026-03-27 12:19:32 -07:00
bicarus b38ea463aa utils: print out DLL metadata (#596)
## Link to GitHub Issue or related Pull Request, if one exists
#592 

## Description of change
* when custom DLLs are detected (`d3d9.dll`) print out some basic data
from the PE header
* for our own NVIDIA DLL stubs, add manifest files to identify ourselves
* as a bonus, when DLL hooks are loaded from `-k`, print DLL info there
as well

This is mainly to detect when DXVK is in use.
2026-03-27 02:34:56 -07:00
bicarus 056d1b2d90 ea3: warn about missing ea3 config (#595)
Show better warning message when `ea3-config.xml` / `eamuse-config.xml`
is missing. Right now we just say `failed to load prop/eamuseconfig.xml`
which is really confusing when people look for `eamuse-config.xml` that
never existed in the first place.
2026-03-27 01:56:32 -07:00
bicarus 37693916d9 overlay: show warning when binding analog axis as a button for TT/knobs (#594)
## Link to GitHub Issue or related Pull Request, if one exists
#345 

## Description of change
When X or Y axis is being bound to any button labeled `TT` or `Knob` or
`VOL-`, show a warning and ask if the user really meant to do that,
encouraging them to use the Analog tab instead.
2026-03-26 21:17:51 -07:00
bicarus 889e8b43eb overlay: redesign Cards tab (#593)
* add `-card0` `-card1` options at the top of Cards tab to clearly
indicate that they take precedence over card files
* reorganize UI / clean up
* remove ability to directly edit the contents of `card0.txt` /
`card1.txt` as that wasn't very intuitive; instead, encourage users to
use overrides directly or use an external text editor (`Edit` button
opens notepad)
2026-03-26 01:32:48 -07:00
bicarus 9db2db2340 presets: clear out applied status when switching profiles (#591)
## Link to GitHub Issue or related Pull Request, if one exists
#581 

## Description of change
Ensure "applied" status is not persisted through.
2026-03-24 22:46:38 -07:00
bicarus 1bb612f3c9 overlay: tweak imgui theme (#590)
## Link to GitHub Issue or related Pull Request, if one exists
#589
2026-03-24 08:57:58 -07:00
bicarus e886ab77af overlay: update imgui theme (#589)
Switch to CrimsonVesuvius theme from
`https://github.com/ocornut/imgui/issues/707#issuecomment-4107169777`
2026-03-24 01:59:13 -07:00
bicarus-dev fbb3c1d4c3 fix option 2026-03-24 00:42:50 -07:00
bicarus 73a69b2e8f touch: native touch hooks to honor Invert Touch option (#588)
## Link to GitHub Issue or related Pull Request, if one exists
For #587 

## Description of change
`Invert Touch` option is now honored by the IIDX/SDVX touch handler.
2026-03-23 20:42:55 -07:00
bicarus 63a0acac24 presets: minor UI fixes, add new controller profiles (#584)
## Link to GitHub Issue or related Pull Request, if one exists
#579 

## Description of change

* Fix profiles being loaded twice on tab switch
* Add more default profiles for some controllers
* Remove borders from tables since they look broken in software renderer
* Add check boxes to import/export only certain parts of of a profile
* Show device identifier string in drop down when applying profiles
since some controllers have multiple identically named interfaces
* Show completion message when action buttons are clicked (`Apply`
button, `Clear all bindings` button both become disabled & shows `done`
message)
2026-03-21 19:43:17 -07:00
ichijyo-hotaru 75ba49ff4a fix qma directory create CreateFileW_Hook to prevent recursive calls (#585)
## Link to GitHub Issue or related Pull Request, if one exists

N/A

## Description of change

Move `dirs_created` flag assignment before `dir_create_recursive` calls
in `CreateFileW_Hook` to prevent stack overflow. `dir_create_recursive`
calls `_wstat64` (ucrtbase), which internally calls `CreateFileW`,
re-entering the hook before the flag is set. GCC 13 preserved the source
order, but GCC 15 reorders the flag store after the function calls,
causing infinite recursion.

Note: I'm not fully confident the root cause is specific to GCC version
differences. Also, I cannot determine whether this directory creation
hook is truly necessary. Without these directories the game boots but
quizzes won't load, making gameplay impossible. It may be worth removing
this hook entirely, but for now this fix is needed to prevent the crash.

## Testing

Verified title screen transition on LMA-2019022700. Crashes on the
latest build without this fix.
2026-03-21 14:23:49 -07:00
ichijyo-hotaru b31ddb1ffc Add controller presets management (#581)
## Link to GitHub Issue or related Pull Request, if one exists
Implements #579.

## Description of change

### Controller Presets

Add a new "Presets" tab to spicecfg that allows users to save, load, and
manage controller binding presets.

#### Save Presets
- Capture all current button, analog, and light bindings as a preset
- "Assign Labels" dialog prompts the user to name each source device
(e.g. "Player 1", "Player 2") before saving
- Device IDs are replaced with these labels, making presets portable
across different machines
- Presets are stored in `%APPDATA%/spice2x/spicetools_presets.xml`

#### Load / Apply Presets
- Preset list shows name, type (Built-in / User), and binding counts
- Apply dialog maps each preset source label to a connected device or
keyboard
- Bindings are applied per-source, supporting multi-device setups (e.g.
two PHOENIXWAN controllers)

#### Edit / Delete Presets
- Rename source labels in saved presets (propagates to all bindings)
- Delete user presets

#### Built-in Presets
- PHOENIXWAN preset for Beatmania IIDX: P1/P2 buttons (1-7, Start,
EFFECT, VEFX), turntable analogs, and button lights

## Testing

* [x] Open spicecfg, go to Buttons tab, verify Presets section loads
* [x] Verify PHOENIXWAN builtin preset appears for Beatmania IIDX
* [x] Save a new preset: confirm "Assign Labels" dialog appears with
device descriptions as defaults
* [x] Apply a preset: confirm source column shows labels, popup does not
shrink
* [x] Edit labels on a saved preset: confirm rename propagates to all
bindings
* [x] Hover over a device source tooltip: confirm vKey shows as number
2026-03-20 16:46:17 -07:00
skogaby 458a1495c8 Fix stale frame returned by capture API on rapid successive calls (#583)
Fix stale frame returned by capture API on rapid successive calls

> [!NOTE]
> Before submitting code changes... 
> * Please do note that this is a GPL v3.0 open source project.
> * Please read the
[CONTRIBUTING](https://github.com/spice2x/spice2x.github.io/blob/main/CONTRIBUTING.md)
guide.
> * Maintainers reserve the right to reject or modify your submission
without reason.
> * No new compiler warnings must be introduced. Check the CI build
results.
> 
> Feel free to remove this section after you have read it.

## Link to GitHub Issue or related Pull Request, if one exists
https://github.com/spice2x/spice2x.github.io/issues/582

## Description of change
When using the SpiceAPI `capture` endpoint for taking screenshots, if
you call multiple times in a row in a short period, there's a good
chance for duplicate / stale data to be returned, and you end up with
multiple identical screenshots. This is due to a race condition in the
graphics capture pipeline because the capture data is never reset to
null between invocations.

`graphics_capture_receive_jpeg()` never resets the capture buffer's data
pointer to `nullptr` after consuming it. The condition variable wait
predicate checks `data != nullptr`, so subsequent calls find the
predicate already satisfied and return immediately with the previous
frame instead of waiting for the render thread to produce a fresh one.

Fix: null out `capture.data` after copying the shared_ptr (which keeps
the pixel data alive via refcount) but before releasing the lock.

## Testing
Tested this locally and invoked the capture API 10+ times in a row
without any duplicates. Previously, every other invocation would be a
duplicate, basically.
2026-03-20 14:43:20 -07:00
bicarus eb029faf2e Update CONTRIBUTING.md 2026-03-19 02:59:55 -07:00
bicarus d65eb6ed4f Update CONTRIBUTING.md 2026-03-19 02:59:23 -07:00
bicarus 97ec2e5d37 overlay: minor UI fixes for clear all / reset all (#580)
## Link to GitHub Issue or related Pull Request, if one exists
#578 

## Description of change
Use `Clear All` when there are no defaults, and `Reset All` when there
are defaults.

Clearing analogs should reset values to 0.5, not 0.
2026-03-18 02:51:02 -07:00
bicarus 97d65b137a overlay: "reset all" buttons for buttons/overlay/analog tabs (#578)
## Link to GitHub Issue or related Pull Request, if one exists
#576 

## Description of change
Add `Reset All` buttons to these tabs which clears out bindings to none
/ default.

For keypads, introduce a special `Use Preset` button which lets users
pick between numpad and top row number keys.

Update UI-visible string used for vKeys, especially the numpad ones
(e.g., `.` is now `Numpad .` to distinguish from the regular `.`
period).

Breaking change: if the user never had `P1 Keypad 00` set, the default
will change from `Enter` to `Numpad -` since both enter keys trigger the
same. This will affect a small number of popn/ddr players. They can just
change it back.

## Testing
*how was the code tested?*
2026-03-17 18:47:54 -07:00
bicarus a9f0e86aa3 overlay: categorize lights for different cab types (#576)
## Link to GitHub Issue or related Pull Request, if one exists
#574 

## Description of change
Refactor so that grouping of lights are in common I/O code. Add this to
IIDX/SDVX/DDR/GitaDora.

## Testing
2026-03-17 02:14:48 -07:00
bicarus 88679b7cb2 sdvx, troubleshooter: detect layer missing error (#577)
Detect `W:BM2D: CreateLayer() 指定したレイヤーは存在しません` and add to deferred log.
2026-03-16 11:22:08 -07:00
bicarus 9ec62a61ac overlay: small tweaks to auto light matching (#575)
## Link to GitHub Issue or related Pull Request, if one exists
#574 

## Description of change

* Add binds for Nostroller
* Update matching logic to ignore common device-side output names like
"Button" "Light" and "LED" (e.g., `Button 1` from a device now matches
on `P1 1`)
* For RGB matching, in addition to `Light R` on the device also try
`LightR` (Nostroller needs this)
* Add a confirmation dialog for `Clear All`
* Address minor bugs

## Testing
Seems to work on arcin, and Nostroller. Still need to retest Faucetwo.
Hopefully I didn't break Phoenixwan.
2026-03-15 19:04:36 -07:00
Horo 44befb7e9a cfg: auto light binding, sdvx light formatting, all light test/clear binds (#574)
<img width="784" height="561" alt="image"
src="https://github.com/user-attachments/assets/3ac6bf6d-2ca8-40e5-80ea-b0fd9e080d7b"
/>

## Link to GitHub Issue or related Pull Request, if one exists

n/a

## Description of change

I've always been annoyed with how long it takes to bind controller
lights, made a few improvements

1) added a button+window for automatically matching device lights to
game lights with the same name
2) added button(s) to cycle through all bound lights to visually confirm
location/function
3) added button to clear all bound lights
4) split SDVX lights into sections (Buttons, Valkyrie, Nemsys, Other)
with formatting, sorted based on current game spec

## Testing
Tested with multiple games and controllers (Faucetwo, custom con) and
matching always works if the descriptor strings are labeled correctly.

Not every controller labels their LEDs the same was as spice, but for
those that do this saves a ton of time clicking every box and testing
one by one.

The SDVX table split/formatting is important because myself (and several
friends) have tried binding to Wing/Controller lights and wondered why
they weren't working in game, when they are legacy Nemsys lights instead
of the strip lights handled by Valk/bi2x. I separated the sections
visually, with tooltips, plus sort the two sections based on active spec
to further minimize the chance of someone trying to use the wrong lights
for their game.
2026-03-15 02:19:24 -07:00
bicarus be388f7b49 otoca: add cam hook, fix printer rotation (#573)
## Link to GitHub Issue or related Pull Request, if one exists
related to #154 though this PR doesn't fix it

## Description of change

1. Add an option for cam hook so that the game can be launched without
any camera
1. Rotate image printed out by print-to-file so that it's right side up
(portrait orientation)

## Testing
Able to boot the game without any cameras, and tested printing in test
menu.

"Holo" printing remains broken.
2026-03-09 02:54:04 -07:00
bicarus 34ef034345 overlay: automatically hide mouse cursor when idle (#571)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Hide the ImGui-rendered cursor in the overlay when the mouse is idle for
more than 2 seconds.

## Testing
Tested windowed and fullscreen.
2026-03-08 20:06:18 -07:00
bicarus ba4623b009 patchmanager: various UI fixes (#572)
## Description of change
* Show more descriptive error when connection fails (for
`ERROR_WINHTTP_NAME_NOT_RESOLVED`)
* ensure patches are reloaded even if importing fails, since existing
checkboxes get reset
2026-03-08 19:43:15 -07:00
Will a0a7b86508 ddr p3io: fix neon pulse mapping (#566)
Logic was wrong; caught by compiling with a newer clang and it raising a
warning about the useless expression (since the second clause always
eval'd as true).

Changed to the intended functionality, but I have no ddr3 with p3io so I
can't test it. But it seems nobody ever really noticed this.
2026-03-06 19:05:44 -08:00
Will 6d7927927c Final changes for clang (#570)
- Allow overriding the toolchains so I can avoid changes in my working
tree
- Squash warnings that the deps use, can't just add them as PRIVATE
compile flags because it's in the headers :(
- Squash those d3d9 warnings with a suppression instead of trying to
rewrite 400 lines of code
2026-03-06 15:18:38 -08:00
Will 837c8a46d6 add some more _WIN32_WINNT defines to hook Win vista+ APIs when compiler is targeting XP (#568)
There's already a bunch of these in the codebase, this just catches a
few locations where they were missed when the compiler is targeting XP.
2026-03-06 14:21:55 -08:00
Will 22aeb64ff9 fix various minor issues causing compilation failure with clang (#567)
- Include order and forward decls were breaking button.h because a
declared but not defined struct can't be initiated (honestly this file
was cooked, crazy include order).
- `MAXINT` is GCC specific, `INT_MAX` is portable.
- InterlockedDecrement takes a LONG, not ULONG
- an imgui printf-style call using a direct string instead of %s (let's
ignore the fact that it's actually safe based on how the str is
constructed)
- missing `override` on a virtual subclass
- `CALLBACK` on a lambda threw me for a loop, but I believe moving it
after the arg list is the correct approach (see if it builds on gcc I
guess)
- `std::result_of` was removed in C++20, which the project is built with
2026-03-06 14:21:10 -08:00
bicarus e4b08064b7 rawinput: fix incorrect usage of HidP_GetUsages when descriptor is not range (#565)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #563 

## Description of change
When buttons are presented as NotRange and instead an array of single
buttons, we are calling `HidP_GetUsages` expecting at most one result
back, but in reality we need to expect all possible buttons on that
usage page since `HidP_GetUsages` returns all buttons in the usage page
+ link collection which would be the entire array of buttons - see
documentation for `HidP_GetUsages` on MSDN.

## Testing
Tested with my own controller modifying USB descriptors... more
controller testing is needed.
2026-03-05 20:41:47 -08:00
bicarus-dev 1e6b4c16cc asio bit difference message 2026-03-04 03:59:03 -08:00
bicarus-dev d6eb8f7125 fix duplicated text 2026-03-04 02:50:02 -08:00
bicarus dc6850e479 overlay: helper modal dialog for picking option values (#561)
## Link to GitHub Issue or related Pull Request, if one exists

## Description of change
Introduce a new modal dialog for picking out some of the options to
improve UX.

Implemented a few for now:

1. UI for picking ASIO drivers from a list (for -iidxasio, -sdvxasio,
-asioconvert, etc)
2. Button to generate card numbers for -card0 and card1
3. Processor Affinity selector (very similar to what's in Windows Task
Manager, list of checkboxes)
4. file selector (file open dialog for various file path overrides like
log file)
5. folder selector (folder override)

I wrote a selector for COM ports as well but how we use the value was
inconsistent (\\\\.\\COMx vs. COMx) and didn't see a huge point in it so
I didn't check it in.

## Testing
2026-03-02 12:08:20 -08:00
bicarus 2d623e179b audio: simplify ASIO backend conversion (#560)
## Link to GitHub Issue or related Pull Request, if one exists

## Description of change
Deprecate backend conversion options and replace with one unified option
for enabling WASAPI exclusive conversion to ASIO.

The new option takes in a string (ASIO driver name) and when filled out,
it automatically enables the ASIO backend.

This is to avoid confusion that was caused by the previous option - the
fact that it was split into two (one to enable the conversion, another
to optionally pick which ASIO driver to use) and that user needed to
specify an integer as opposed to the driver name.

## Testing
Tested on IIDX33. Old options should continue to work.
2026-03-01 13:06:18 -08:00
bicarus 884d665c1c sdvx, touch: account for 270 degree monitor rotation in wintouchemu (#559)
## Link to GitHub Issue or related Pull Request, if one exists
#558 

## Description of change
Same as #558 but this PR fixes the non-native touch cases (when
wintouchemu is active).

## Testing
Tested 90 and 270 degree rotation, 1080p and 1440p.
2026-02-27 17:17:00 -08:00
bicarus 2842fe7ec6 Update pull_request_template.md 2026-02-27 16:56:03 -08:00
bicarus 954e6022d9 sdvx, touch: hook Windows touch API when main monitor is rotated 270 degrees (portrait flipped) (#558)
## Link to GitHub Issue, if one exists
n/a

## Description of change
Game expects primary monitor to be in 90 deg rotation, and does the
touch calculation accordingly.

If the user does 270 deg rotation ("portrait, flipped") then the game's
touch calculation is flipped upside down. Detect this and provide the
fixed up values in the touch hook.

## Testing
Tested with SDVX in 90 deg, 270 deg rotation, and 1080p, forced 1440p.
2026-02-27 14:22:56 -08:00
Emma 955c50a9f3 api: fix for setting drs touch state (#557)
## Link to GitHub Issue, if one exists
N/A

## Description of change
Changes the array size used for collecting touch inputs from the API to
be dynamic according to the size of the input data, rather than fixed.
This fixes a crash.

## Testing
An API client was made sending touch states, both valid and invalid
(oversized). The valid states are handled identically in the test menu
while the oversized states no longer crash.
2026-02-26 10:47:08 -08:00
bicarus dc82c980b1 iidx: show error message for emulated I/O mismatch in LDJ/TDJ sub overlay (#555)
## Link to GitHub Issue, if one exists
#345 

## Description of change
When DLL is configured to use TDJ I/O but spice is in LDJ mode, show an
error in LED ticker suggesting the user to turn on `-iidxtdj`.

When DLL is configured to use LDJ I/O but spice is in TDJ mode, show an
error in TDJ overaly suggesting the user to fix the DLL.

Fix a small bug that caused a white window to be created when `-w` is
enabled even when `-iidxtdj` is not, but DLL is using TDJ I/O.
2026-02-15 19:49:07 -08:00
bicarus 408ed17521 cfg: turn a handful of parameter parsing errors into fatal (#554)
For parameters that require two values (`width, height` or `x, y`), if
the string fails to parse, turn into runtime failure.
2026-02-13 03:22:20 -08:00
bicarus f4050e9adb graphics: custom resolution for subscreen (#553)
## Link to GitHub Issue, if one exists
n/a

## Description of change
Allow user to specify a custom resolution for the sub monitor, in
addition to the existing refresh rate option

This would allow practically any touch monitor (including ones with
weird resolution / aspect ratio / refresh rate) to be used with
IIDX/SDVX.

## Testing
Tested with various resolution / refresh rate combinations on IIDX /
SDVX.
2026-02-12 17:51:38 -08:00
din badae01357 bt5api fix (#552)
## Description of change
use case: user with two ICCA readers in a daisy chain configuration (as
is wired in og IIDX/DDR) wanted to play DDR using their existing
equipment.

Existing `reader/` implementation is only designed for a single reader
per com port vs two readers on one daisy chained com port. This can be
implemented and improved at a future date.

Due to time constraints `eamio-icca.dll` from btools did support both
wavepass and slotted in a daisy chain configuration, and spice does
support bt5api calls.

However, the implementation was not sufficient to allow use of both
modules correctly, often stalling, not did it allow the ability to eject
the card.

I have cleaned up the implementation such that it matches the btools
`eamiotest` implementation and allows the user to use decimal to eject
the card as desired.

As well as properly staging the requests for state of the two individual
readers, like `eamio-icca` expects.

## Testing
Tested with both wavepass and slotted (ICCA and ICCB) modules and in
combination with released `eamio-icca.dll` binaries from btools 5.49 as
released by their github using `MDX` as a test subject.

Works as expected and allows end users to use real readers (if they have
them) as virtual readers in spice's emulation layer.
2026-02-04 19:34:25 -08:00
bicarus 38d9939eb7 cfg: fix early game detection for Polaris Chord and MFG (#551)
## Link to GitHub Issue, if one exists


## Description of change
Some parts of the UI (e.g., Card tab) rely on early auto detection of
the game, which relies on file hints and not the game code. Two games
(PC and MFG) rely on directory presence and not file presence.
2026-02-04 11:40:07 -08:00
bicarus f5a3caf536 signal: avoid spamming log with hook hit messages (#550)
## Link to GitHub Issue, if one exists
n/a

## Description of change
Some people have weird DLLs that keep calling
`AddVectoredExceptionHandler` which ends up spamming the log.

## Testing
Tested IIDX with error + signaldisable to confirm the warning message
being shown, ran Polaris Chord once to check "hook hit" message.
2026-02-03 20:08:15 -08:00
bicarus 027445afb1 overlay: add context menu for overflow operations (#547)
Remove the green + button, remove the bottom checkbox row, and instead
put extra operations like `Add alternate` and `Bind many` to a context
menu.
2026-01-31 23:57:08 -08:00
bicarus 63a3900b79 overlay: add skip button to bind many (#546) 2026-01-31 20:58:15 -08:00
bicarus f542a78347 overlay: add hover delay to tooltip (#545)
Clean up flags used for tooltip display, which adds a tiny delay by
default.
2026-01-31 15:47:23 -08:00
bicarus 4d32dde83e overlay: remove multiple pages from Buttons/Overlay/Lights tabs, replace with tree layout (#541)
The old pages UI was not intuitive and often led to questions like:

* How do I bind multiple controllers to a single input?? (failure to
discover)
* I have a ghost input that won't go away, how do I fix this? Is this a
bug? (non-intuitive UI - there are too many pages and finding out where
each binding is potentially requires scrolling through 100 pages)
* Potential perf issue - user binds too many by accident, or puts
bindings all the way in page 99, requiring us to walk each element in
the vector, wasting precious CPU cycles during an input poll.

This PR removes the multiple pages, and puts everything on a single
page, showing multiple binds in a tree structure.

It also puts a hard limit on how many alternate bindings can be made (8
for buttons, 16 for lights), though if the user has configured more in
older version they will be respected.
2026-01-31 15:16:19 -08:00
bicarus 927170ce73 api: fix unbounded access in getting analog state (#544)
## Link to GitHub Issue, if one exists
#0

## Description of change
Occasionally we would read a bad "vKey" for an analog axis and end up
accessing invalid memory (beyond the size of a vector) and potentially
crash or read junk values.

## Testing
Fixed based on minidump, this was a rare repro though.
2026-01-30 20:53:13 -08:00
bicarus 875a0f0765 overlay: address small polish issues (#543)
* Make a custom widget for truncated text that shows a tooltip only when
truncated
* Use the above widget more consistently in various places
* Ensure tooltips still show even when the preceding widget is disabled
2026-01-30 19:17:44 -08:00
bicarus 3ad88ef15c overlay: fixes for naive binding (#542)
## Description of change
Three bugs:

* Some keys are IME-dependent. Right Alt can be IME mode switch for
Japanese / Korean, and Right Ctrl is often the Kanji button.
* If you press modifiers like Ctrl during bind-many-naive, it will skip
over a button, because of broken for loop logic that ends up calling
`inc_buttons_many_index` too many times
* trying to cancel out of bind-many-naive with left and right mouse
buttons swap doesn't work

## Testing
Validating various IME. I don't have a keyboard in another language
though..
2026-01-30 13:16:14 -08:00
bicarus 7a6cadb176 overlay: refactor code for lights (#539)
No functional change.
2026-01-29 16:13:56 -08:00
Will 4feca23240 Build scripts: support paths with spaces (#540)
Doing dumb things and building inside a path that contains spaces.
Quoted the relevant bits in the build script (and a couple more
shellcheck brought up that realistically won't ever be an issue, but the
squigglies annoy me.
2026-01-29 16:12:23 -08:00
bicarus 2e52bebd3d overlay: bring back "-" placeholder for unbound io, clean up code (#538) 2026-01-29 11:53:16 -08:00
bicarus aa218d5be7 overlay: fix colors of new extension widgets (#537) 2026-01-29 01:58:43 -08:00
bicarus 94f009991a overlay: simplify various input controls by introducing new custom ImGUI widgets (#536)
## Description of change

* Add a red `x` button to replace `Clear` button in binding tabs
(Buttons/Analog/Lights).
* Add a gray `x` button to replace `Clear` button in options tabs next
to input widgets
* Add helper for truncating text and adding ellipsis (...) to deal with
text overflow in tables.
* Reword some UI text for clarity
2026-01-28 02:17:40 -08:00
bicarus c3b9ce3fdc cfg: fix mouse names out of bounds (#535)
## Link to GitHub Issue, if one exists
n/a

## Description of change
Improper bounds check resulting in crash when binding mouse buttons
beyond Mouse 5.

## Testing
Tested with `Edit` function.
2026-01-26 23:42:12 -08:00
bicarus 1517cf9a7c overlay: refactor UI code for I/O binding tabs (#533)
No functional changes, refactor of code only.
2026-01-26 22:24:14 -08:00
bicarus f0f46296f2 cfg: clean up stale buttons and lights alt bindings on save (#534)
## Link to GitHub Issue, if one exists
n/a

## Description of change
For buttons and lights, it's possible to have multiple alternatives.

When you create alternatives and then clear then, the config files
continue to have stale entries. This is because alternatives are stored
in a vector and elements are never deleted when unbound; they get
written out to the XML on modification.

These stale XML entries live forever, and get loaded on next startup of
spice.

This PR fixes that by deleting stale entries - basically, XML nodes will
be kept only until the last valid node (properly bound button or light),
trimming the trailing invalid entries.

## Testing
Manual testing, validating XML.
2026-01-26 22:12:04 -08:00
bicarus 1d1981fcde rb: prevent window moves and resizes in windowed mode (#532)
## Link to GitHub Issue, if one exists
n/a

## Description of change
Disable all window options that change dimensions or position as they
cause problems with touch overlay in windowed mode. Always On Top is the
only thing that's still possible.

## Testing
2026-01-24 18:49:16 -08:00
bicarus 2f538fe2e4 gitadora: fix order of drums in Buttons tab (#531)
## Link to GitHub Issue, if one exists
n/a

## Description of change
Before this PR, drums were in the order they appear in test menu.

This is really confusing. Change it so that they appear in the order
they do in default drummania layout.

## Testing
validated manually in gw
2026-01-23 10:22:24 -08:00
bicarus-dev 8e01b2e5c8 update gitadora help text 2026-01-23 03:03:18 -08:00
bicarus cd14ca800e graphics: fix window resize not working properly in some games (#530)
## Link to GitHub Issue, if one exists
n/a

## Description of change
Need to filter out system-created windows (such as `CicMarshalWnd`) when
looking up which window to mess with when user interacts with F11 menu.

While I'm here, clean up the hack for gfdm arena model that remembers
the main window.

## Testing
Tested:

DDR
gfdm arena 4 / 1 window modes
iidx (windowed mode, with/without nosub option)
sdvx with sub windows

Need to check:
full screen games
2026-01-22 19:06:31 -08:00
bicarus cb59fe14e9 gitadora: prevent window resize (#529)
## Link to GitHub Issue, if one exists
n/a

## Description of change
Gitadora (pre-Delta) tends to soft lock, have graphical issues (menu bg
render as black), or take a long time (minutes) between scene
transitions when window is resized.
2026-01-22 00:51:12 -08:00
bicarus 9fd9290cd3 gitadora: option for guitar picking algorithm (#528)
## Link to GitHub Issue, if one exists
n/a

## Description of change
Adds an option to pick from four algorithms for guitar picking input.

The default behavior is being switched from legacy (rising edge only,
always 1 frame of input) to SOCD "prefer recent" algorithm. This makes
it slightly easier to navigate the menu with the guitar.

## Testing
Validated gw, gwdelta, and sdvx just in case.
2026-01-21 21:14:28 -08:00
bicarus-dev 1dd99aac74 update error message for module auto detection failure 2026-01-20 18:04:54 -08:00
bicarus 9e376f373e patcher: load auto-enable settings even if dest/spec/rev don't match (#527)
## Link to GitHub Issue, if one exists
n/a

## Description of change
Before this change, if user changes `dest`/`spec`/`rev`, then the "auto
enable patches" settings doesn't get loaded. This is because the patch
manager config file relies on having an exact match of
`model+dest+spec+rev+ext`.

This causes a lot of confusion for:

* people who edit prop files - e.g., to switch between sdvx nemsys/valk
cab modes - and forget to re-enable patches
* people who use batch scripts to change xml files on launch - e.g., for
gitadora, omnimix, etc - even if they checked auto-apply in spicecfg,
when they use the batch script to launch the game, the setting won't be
loaded

This PR solves this by just ignoring the `dest`/`spec`/`rev` fields when
checking if patches should be auto applied on launch. Yes, this may
cause false positive cases, but probably very unlikely, and it's pretty
harmless anyway.

For future consideration: should we fix up gitadora XG disparity (K32 vs
K33 etc)?

## Testing
Manually validated
2026-01-20 04:00:15 -08:00
bicarus d4e51b77d1 overlay: options tab tooltip formatting consistency (#526) 2026-01-20 00:24:40 -08:00
bicarus 61181836b0 nvapi: set refresh rate / framerate settings (#525) 2026-01-20 00:23:14 -08:00
bicarus 4e26c8afa5 drs: rgb camera hook (#524)
## Link to GitHub Issue, if one exists
n/a

## Description of change
Add plug&play hooks for RGB camera in DRS.

## Testing
Seems to work with my crappy Logitech C270

Camera options (brightness etc) seem to work in test menu as well).

Tested by another user with realsense camera + logitech camera, both
cams were detected correctly.
2026-01-19 02:54:01 -08:00
bicarus 7684028f99 io: fix bi2x_hook implementation in various games (#522)
## Link to GitHub Issue, if one exists
similar issue as #499

## Description of change
Fix broken bi2x_hook implementations that did not properly allocate I/O
buffers with correct sizes.

People keep copy-pasting other implementation that allocate 0 bytes of
memory, but we run the risk of memory corruption or/or game crashes if
we keep doing this.

We've seen some mystery crashes with CCJ for example, maybe this will
fix that.

While I'm here, implement SOCD cleaner for Polaris Chord.

## Testing
Tested I/O menu with CCJ, play through tutorial (didn't test matches)
Tested GWDelta (test menu and a few songs in guitar)
Tested PC (played a credit)
Tested QKS (still can't get past the title screen)
2026-01-18 18:06:15 -08:00
bicarus-dev 1d6be8fe4b fix print message 2026-01-17 20:24:18 -08:00
bicarus c70cf40af3 graphics: fix monitor auto-rotate for reflec beat (#521)
## Link to GitHub Issue, if one exists
Fixes #341

## Description of change
fix monitor auto-rotate for reflec beat

## Testing
Tested KBR, LBR, MBR (volzza2), MBR (reflesia)
2026-01-17 19:57:47 -08:00
bicarus 5715d4e605 gitadora: (arena model) override app-config (#520)
## Link to GitHub Issue, if one exists
#477 

## Description of change
Just like the old `sys_code_get_cmdline`, override command line
parameters in `app-config*` so that we can control windowed mode / full
screen.

## Testing
Tested both GF and DM.

Technically both games can boot into the correct mode using `-d -DM` but
we'll try to use the right one just in case.
2026-01-17 17:49:50 -08:00
bicarus 821a705a7d gitadora: (arena model) single window mode + subscreen overlay (#486)
## Link to GitHub Issue, if one exists
#477 

## Description of change
This is a work in progress.

What works:
- [x] subscreen overlay shows the touch screen image (in windowed mode)
- [x] touch works in the overlay
- [x] only the main window is shown, other windows are not launched

What doesn't:
- [ ] game occasionally crashes when entering test menu?
- [ ] full screen mode is not tested at all due to monitor requirements
- [ ] need more polish (various options to control the behavior)
2026-01-17 02:17:33 -08:00
bicarus-dev 86f2a1fd39 remove asio failure message 2026-01-14 10:05:54 -08:00
bicarus-dev 86f218329a fix typo 2026-01-14 01:07:46 -08:00
Isaac Drew 66094de38a Add coin support for SDVX BI2X (#518)
## Description of change
Add coin handling to SDVX BI2X. I also tried to populate the structs as
best I could.

## Testing
Tested via running spice2x on EG final, using F1 to insert coins.
2026-01-13 09:59:49 -08:00
bicarus c00cff2afd patcher: print patch identifier for each DLL (#517) 2026-01-13 04:21:22 -08:00
bicarus b2cc8d404c gitadora: digital wailing sensitivity (#516)
## Link to GitHub Issue, if one exists
#512 

## Description of change
Add an option to adjust the hold period for digital wailing.
2026-01-13 04:19:55 -08:00
bicarus a0a04ab62f gitadora: various fixes for wailing (#513)
## Link to GitHub Issue, if one exists
Fixes #512

## Description of change
Fix digital wailing not being recognized consistently.

Also address the fact that wailing was completely broken when lefty mode
was on. This requires a new option that the user needs to check off in
the configurator since we can't magically guess if the user is holding
the guitar in lefty mode.

- [x] fix downward wail
- [x] digital lefty mode
- [x] implement all of this for arena model i/o
- [x] test dx cab 2p
- [x] lefty toggle in overlay
- [x] fix analog not working for 2p guitar
- [ ] analog lefty mode?

## Testing
Checked GW and GW Delta
2026-01-12 21:52:48 -08:00
bicarus a3a59e689e ftt: clean up i/o (#515)
No functional change, just a code clean up.
2026-01-12 19:32:24 -08:00
bicarus a6f29d6a5a audio: implement wrapped IAudioClient3 (#511)
## Link to GitHub Issue, if one exists
Fixes #509

## Description of change
Implement `WrappedIAudioClient3`.

For now, we have only seen SDVX use it (EG final) and only when the
shared mode patch is enabled. Exclusive still goes through version 1.
This means that for V3 we can forego implementing backend conversion
logic (for now).

Only significant addition is `InitializeSharedAudioStream` and this IS
called by SDVX when shared mode patch is enabled.

## Testing
Tested EG final, tested year 1 data, also IIDX shared/exclusive wasapi.
2026-01-10 17:41:10 -08:00
bicarus 9be618d791 utils: log primary display adapter (#508)
## Description of change
Log a message so you can tell which display adapter is primary.
2026-01-10 03:03:08 -08:00
bicarus 6bf7ae9074 graphics: submonitor refresh rate option (#507)
## Description of change
Add an option that forces requested refresh rate value for the second
monitor; i.e., instead of 60Hz allow the user to override the adapter to
be 70Hz.

Useful for people who have a touch screen that can't do 60Hz.

I don't know if this would cause any timing issues in IIDX or SDVX, but
it seems to boot and monitor check is unaffected.

## Testing
Tested TDJ And UFC mode with a submonitor that can only do 61Hz.
2026-01-09 19:50:07 -08:00
bicarus e3d63c65c1 overlay: fps window clean up (#506)
## Link to GitHub Issue, if one exists
n/a

## Description of change
Use fmt library for formatting time

Round down sub-second units so that the FPS counter only updates once
per second (instead of Time and Uptime ticking independently)

clean up usage of `localtime` in `logging.cpp`
2026-01-08 21:52:13 -08:00
bicarus 5d2a9fa1cf audio: update logging for low latency option (#505)
## Description of change
Spit out an error if the minimum latency is not any lower than the
default.
2026-01-08 13:56:10 -08:00
bicarus 0078b72b03 graphics: address performance regression with Image Resize (#504)
## Link to GitHub Issue, if one exists
Regressed by #401 

## Description of change
Two things from testing SDVX Live2D -

1. ColorFill is almost always too expensive and will lead to drops
1. Scale factor of 0.5 causes GPU to sync and causes drops

(ARGB/XRGB doesn't matter, linear filtering doesn't matter)

1 is fixed by removing ColorFill calls on every frame - instead only do
this once when the user changes the duplicate option (on the next frame)

2 is fixed by updating the UI (limit the slider) though people can
ctrl+click to override these slider limits.

The warning label will be kept with slightly reworded text.

## Testing
tested in sdvx eg final (sudden death) - on my RTX 4070, my FPS is rock
solid 120 fps even with image resize on now.
2026-01-07 16:35:08 -08:00
bicarus b1ee37a066 troubleshooter: diagnose ddr codec issues (#503)
## Link to GitHub Issue, if one exists
#345 

## Description of change
Turn existing warnings for DDR codec issues into deferred error
messages.

## Testing
Tested DDR World with faked codec issues.
2026-01-06 23:20:05 -08:00
bicarus ebf6382e7f sdvx: add warning about Image Resize feature affecting Live2D performance (#502)
## Link to GitHub Issue, if one exists
n/a

## Description of change
Turning on Image Resize feature completely tanks performance of Live2D
in some SDVX songs. Add a giant warning next to the UI.

On my system with RTX 4070, Sudden Death is normally a rock solid
120FPS, but drops to 30-40FPS when resizing is on.

* linear filter does not affect this at all
* window resize options also have no effect

It's really just rendering on a larger surface + calling `StretchRect`
that does this.

Additionally, hide the resolution swap option since it sees very low
usage & has potential gotchas likes this.
2026-01-06 16:40:54 -08:00
bicarus 8e23ef224a Update CONTRIBUTING.md 2026-01-05 18:59:06 -08:00
bicarus 6706ffc321 Fix wording in CONTRIBUTING.md for clarity 2026-01-05 17:26:29 -08:00
bicarus e755b06e35 Update CONTRIBUTING.md 2026-01-05 17:25:10 -08:00
bicarus 9ef22712c0 Update CONTRIBUTING.md 2026-01-05 17:24:13 -08:00
bicarus 7b903d35e2 iidx, sdvx: SOCD cleaner for knobs and turntables (#497)
## Link to GitHub Issue, if one exists
n/a

## Description of change
Apply SOCD cleaner algorithm to deal with the case where both
counter-clockwise and clockwise buttons are pressed at the same time.

In SDVX, default algorithm is to prefer the most recently pressed
direction. This is better for dealing with slams in rapid succession,
for example, as you don't have to completely let go of one direction to
hit the other now.

In IIDX, default algorithm is neutral (same behavior as before). Using
last or first algorithm results in double scratches which means you'll
likely end up with an excessive poor after hitting a scratch, so neutral
is actually beneficial. Besides, if you are serious about playing on the
keyboard, you should be using `TT +/-` and `TT +/- Alternate`.

## Testing
Tested SDVX 3, EG (old cab), EG valk cab modes, IIDX 24, 27, 33
(tdj/ldj)
2026-01-04 19:50:11 -08:00
bicarus 7f281a50c5 sdvx: clean up valk cab check (#498) 2026-01-04 19:49:55 -08:00
bicarus 1354d63b85 iidx, sdvx: bi2x hook fix (attempt 2) (#499)
## Link to GitHub Issue, if one exists
see my previous attempt #496, which has been reverted by #500

## Description of change
Two issues:

1. the original implementation of bi2x_hook for iidx misunderstood what
`aioNMgrIob2_Create` does. In the original I/O code it returns a pointer
to a C++ class instance, which would also have a vptr (pointer to a
vtable) in the beginning. The original author of this code of the hook
in spice thought that `aioNMgrIob2_Create` returns a pointer to a
pointer. This may happen to work but it's not 100% accurate, so this has
been fixed by separating out the class and the vtable allocation.
Without this, the game could have accessed memory outside this buffer.

2. And then the SDVX implementation blindly copy-pasted the IIDX
`bi2x_hook`. Along with the issue above, they also forgot to check the
I/O DLLs to see how much memory needs to be allocated and change that.
Instead, they kept the same value as IIDX, which was too small. Fix that
as well.

## Testing
Doing some extensive testing with IIDX (27-33) and SDVX versions (EG).
2026-01-04 19:10:50 -08:00
bicarus e745cdcac8 Revert "iidx, sdvx: bi2x hook fix" (#500)
Reverts spice2x/spice2x.github.io#496
2026-01-04 17:50:21 -08:00
bicarus bf7666939a iidx, sdvx: bi2x hook fix (#496)
## Link to GitHub Issue, if one exists
n/a

## Description of change
Both games take the address returned by `aioNMgrIob2_Create` and reads
values offset from that address as part of the I/O routine. This means
that the I/O routines of these games have been accessing random
variables in data/bss segments for years, they just happened to get a
value that doesn't lead to a crash.

Stumbled upon this while trying to add a new feature that required
adding more globals, but suddenly the game stopped polling I/O when I
changed a value of my global. Scary.

## Testing
Tested both games I/O
2026-01-03 19:27:53 -08:00
bicarus-dev 7e89037db3 update github download url 2026-01-03 05:30:50 -08:00
bicarus 6ffaa77f58 overlay: UI scaling option (#495)
## Link to GitHub Issue, if one exists
#477 

## Description of change
Add a new option for scaling the overlay.

Gitadora Arena will receive 250% by default.

Various overlay windows have been updated to scale properly, not perfect
but they are at least usable.
2026-01-02 21:54:25 -08:00
bicarus 7a1f46d1be gitadora: (arena model) fix window settings not working (#494)
## Link to GitHub Issue, if one exists
#477 

## Description of change
F11 menu (window settings) did not work for arena model because the game
engine races to create many windows, so our logic of grabbing the first
window created doesn't always get the main window.

## Testing
2026-01-02 15:07:32 -08:00
bicarus cf59ae5f89 gitadora: (arena model) lights (#492)
## Link to GitHub Issue, if one exists
#477 

## Description of change
Add lights for J region cab. I can't figure out how the extra lights on
U region cab are wired up.

## Testing
Tested guitar and drums, J and U regions.
2026-01-02 03:49:01 -08:00
bicarus 9c06dd36a0 graphics: tone down adapter mode removal logging (#493)
## Link to GitHub Issue, if one exists
n/a

## Description of change
Get rid of log messages when adapter modes are removed, since these have
never been all that useful.
2026-01-02 03:48:28 -08:00
bicarus d5d082ec37 troubleshooter: deferred message for fatal errors (#491)
## Description of change
When the game logs a fatal error message, queue up a deferred message
for the troubleshooter.
2025-12-31 05:44:55 -08:00
bicarus 17723d290a troubleshooter: update formatting (#490)
## Link to GitHub Issue, if one exists
n/a

## Description of change
Make the text color cyan to make the messages stand out.

## Testing
Verified in terminal.
2025-12-31 01:15:19 -08:00
bicarus 8de7e5a9e4 sdvx: prevent the game from trying a random ASIO driver (#488)
## Link to GitHub Issue, if one exists
n/a

## Description of change
SDVX EG final started to pick an ASIO driver at random and tries to open
it, which causes crashes / hangs if the driver is not compatible
(ASIO4ALL in particular)

Only allow the game to access `HKLM\SOFTWARE\ASIO` if the user has a
Xonar AE, or if ASIO was configured using -sdvxasio.

## Testing
Tested year 1 data and EG final.
2025-12-30 17:58:28 -08:00
bicarus b8e99e75e5 gitadora: (arena model) prevent the small subscreen from being closed (#487)
## Link to GitHub Issue, if one exists
#477 

## Description of change
Ensure the small subscreen can't be resized or closed by accident.
2025-12-30 14:30:59 -08:00
bicarus 46dc22dd63 gitadora: (arena model) headphones i/o (#485)
## Link to GitHub Issue, if one exists
#477 

## Description of change
Plumb through headphone jack detection. Touch screen menu for headphone
volume lights up.

## Testing
Tested guitar.
2025-12-30 14:30:33 -08:00
oleg238948234 69d78e0a87 gitadora: (arena model) fix j32d encoding & use velocity (#483)
## Link to GitHub Issue, if one exists
N/A

## Description of change
in current implementation j32d analog values are not encoded properly,
values are supposed to go up to 1023, but they show up as 771 in game

## Testing
tested with M32-007-2025092400, used DM PAD CALIBRATION menu
2025-12-30 00:29:43 -08:00
oleg238948234 ab3d41432a gitadora: (arena model) F: drive fix (#484)
## Link to GitHub Issue, if one exists
N/A

## Description of change
prevents the game from creating folders on F: drive

## Testing
tested with M32-007-2025092400
2025-12-30 00:26:47 -08:00
bicarus 73e10cac0c gitadora: (arena model) text encoding hook (#482)
## Link to GitHub Issue, if one exists
#477 

## Description of change
Fix Japanese text broken in the game UI when system ACP is not
SHIFT-JIS.

## Testing
Checked song select and in-game title display.
2025-12-29 23:27:15 -08:00
bicarus c6242233a7 gitadora: (arena model) implement wailing, improve picking (#481)
## Link to GitHub Issue, if one exists
#477 

## Description of change
Add analog and digital wail for the arena model guitar.

Fix the picking code to be the same algorithm as extdev - detect rising
edges and insert 1 frame of input.

## Testing
Tested x/y/z and digital y up/down. Only Y gets used in game though.
2025-12-29 23:26:44 -08:00
bicarus 6b8335780f ddr: fix up init code when using old I/O with gold cab mode (#480)
## Link to GitHub Issue, if one exists
Fixes #479 

## Description of change
When p3io / p4io is in use with Gold Cab spec, use the old init code.

## Testing
Verified p3io / p4io / bio2 booting with 2024040200.
2025-12-29 16:26:30 -08:00
bicarus-dev 21f469b260 gitadora: arena cab volume change prevention (#478)
## Link to GitHub Issue, if one exists
#477 

## Description of change
Arena cab has two sound cards and it uses `WrappedIMMDeviceCollection`
to set the volume to 100% on boot on both, instead of the typical
`GetDefaultAudioEndpoint`.

## Testing
Testing Delta, testing other games as well as they are affected.
2025-12-29 03:23:58 -08:00
zxcwdta 07a0df5fdc gitadora: Added support for GITADORA Delta (#476)
## Link to GitHub Issue, if one exists
None

## Description of change
- Added bi2x_hook
- Added J32D (drum) and J33I (guitar) emulations
- AMI2000 card reader (this game uses this instead of ICCA)

## Testing
Played through a 15-minute session on Drummania.
Guitar mode works with keyboard input.
2025-12-28 16:17:06 -08:00
zxcwdta d4f1768bbd avs: Increased heap size for later gitadora games (#475)
## Link to GitHub Issue, if one exists
None

## Description of change
Increase the heap size for GITADORA games.

Later releases of GITADORA attempt to allocate a block of memory (size
of 0x7ECF78) for notes_info.xml but fail due to heap size being too
small, leading to HDD ERROR 5-1502-0008.

## Testing
Tested with current and previous GITADORA releases.
2025-12-26 23:00:57 -08:00
bicarus-dev 96e7e4949f iidx, linux: cache lstat result for movie thumbnails (#474)
## Link to GitHub Issue, if one exists
https://codeberg.org/nixac/spicetools/issues/4

## Description of change
Inspired by the Linux issue above in the fork, cache lstat calls for
thumbnail. IIDX 32 and 33 make a call for this every frame which can
cause significant overhead on Linux.

This is being enabled for all versions and not just the Linux variant of
the executables since it provides some marginal performance gain on
Windows.

## Testing
Tested IIDX 32/33, with the option on/off, and with AVS verbose logging
on/off.

Tested with ifs_layeredfs hook as well just to verify functionality.
2025-12-26 20:17:20 -08:00
bicarus-dev 24d3062c66 update superstep audio error troubleshooter message 2025-12-24 21:10:26 -08:00
bicarus-dev 6cd212aac2 iidx,sdvx: disable subscreen overlay if -monitor is in use (#471)
## Link to GitHub Issue, if one exists
#345 

## Description of change
If TDJ / VM mode is active AND user is enabled `-monitor` option (and
the value isn't 0, the primary monitor)

then disable the subscreen overlay, show an error message instead.

We're doing this because both games cannot accept emulated touch input
if `-monitor` option is in use. There is not much point in displaying
the second screen's image, other than to REALLY confuse people when
nothing happens when they click on it.

## Testing
Tested both games with `-monitor`.
2025-12-24 17:35:15 -08:00
bicarus-dev 064031bc12 Update CONTRIBUTING.md 2025-12-22 19:20:47 -08:00
bicarus-dev 4c8d4ebab6 iidx: detect ldj/tdj I/O mismatch (#470)
## Link to GitHub Issue, if one exists
#345 

## Description of change
For IIDX31+, detect if BI2A or BI2X I/O emulation becomes active, and
see if there is a mismatch between the -iidxtdj setting.

Right now a mismatch results in warning in log + deferred error log, but
in the future we could turn this into a fatal error and crash the game
to make it more obvious.

## Testing
Tested 4 combinations:

1. DLL in LDJ mode + -iidxtdj off (pass)
2. DLL in LDJ mode + -iidxtdj on (error)
3. DLL in TDJ mode + -iidxtdj off (error)
4. DLL in TDJ mode + -iidxtdj on (pass)
2025-12-22 18:52:37 -08:00
bicarus-dev 462a07680b Update CONTRIBUTING.md 2025-12-22 18:51:15 -08:00
bicarus-dev 081def30db overlay: clarify subscreen types (#469)
## Link to GitHub Issue, if one exists
#345 

## Description of change
In the main menu (Esc key), instead of just saying `Show subscreen`,
detect game and say `Show TDJ Subscreen` `Show LDJ LED Ticker` etc.

For the LED ticker window, show the title bar by default, and add an
option to hide it back.

This is to help with cases where people get stuck with being in LDJ mode
when they meant to turn on TDJ.

## Testing
Tested all configurations (IIDX LDJ/TDJ, SDVX UFC, DRS)
2025-12-22 15:59:31 -08:00
cchike 6776b08bce MDX: Refactored p4io.cpp (#467)
## Link to GitHub Issue, if one exists
#452 

## Description of change
Added `Sleep(4)` call to regulate how fast the `device_io` function will
call `getState` for each button. The panel buttons were commented out
because those are handled by the MDXF Control Status Buffer functions
anyways, so the work was redundant. It's also why the timing precision
from this function doesn't matter, so it can afford a longer sleep

## Testing
- Verified IO still functions properly
- Verified decrease in overall CPU usage
2025-12-21 20:49:39 -08:00
bicarus-dev 6cb507f4fa Update CONTRIBUTING.md 2025-12-21 18:57:42 -08:00
bicarus-dev de05af052f build: turn warnings into errors for gcc (#466)
for CI
2025-12-21 03:52:30 -08:00
391 changed files with 60645 additions and 17647 deletions
+5 -3
View File
@@ -11,9 +11,7 @@ assignees: ''
*name of game, version of game*
## Version of spice2x
*version of spice, this can be seen in the About tab, and in log.txt*
*please remember to test the LATEST beta version before posting a bug about it**
*please remember to test the LATEST beta version before posting a bug about it*
## Describe the issue
*what's the issue?*
@@ -22,3 +20,7 @@ assignees: ''
> [!WARNING]
> Please make sure you remove any personally identifiable information from the log file.
> Please set `-loglevel` to `all` before launching the game for more verbose logs.
## A note on issue management
`Closed as not planned` means exactly that - it's closed, because it's not on our plans to address them any time soon. It doesn't mean your issue is invalid, it just means it's not a priority, and we can always revisit them later.
+3 -3
View File
@@ -12,12 +12,12 @@ jobs:
run:
working-directory: ./src/spice2x
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set ccache environment variables
run: |
echo "CCACHE_DIR=${{ github.workspace }}/src/spice2x/.ccache" >> $GITHUB_ENV
- name: Install ccache
uses: hendrikmuhs/ccache-action@v1.2
uses: hendrikmuhs/ccache-action@v1.2.23
- name: Calculate commit SHA
id: vars
run: |
@@ -25,7 +25,7 @@ jobs:
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
- name: Compile
run: ./build_docker.sh
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v6
with:
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
path: src/spice2x/bin
+49
View File
@@ -0,0 +1,49 @@
name: Draft Release
on:
workflow_dispatch:
permissions:
contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
release:
name: Build and Draft Release
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./src/spice2x
steps:
- uses: actions/checkout@v5
with:
ref: main
fetch-depth: 0
- name: Clean leftover build artifacts
run: |
rm -rf .ccache dist bin cmake-build-*
- name: Compile
run: ./build_docker.sh
- name: Determine release name from dist filename
run: |
dist=$(basename "$(ls dist/spice2x-*.zip | grep -v -- '-full.zip')")
# strip the ".zip" to get the base name, e.g. spice2x-26-06-28
name="${dist%.zip}"
# the tag is the date portion, e.g. 26-06-28
tag="${name#spice2x-}"
echo "RELEASE_NAME=$name" >> $GITHUB_ENV
echo "RELEASE_TAG=$tag" >> $GITHUB_ENV
- name: Create draft release
uses: softprops/action-gh-release@v3
with:
draft: true
prerelease: true
tag_name: ${{ env.RELEASE_TAG }}
name: ${{ env.RELEASE_NAME }}
target_commitish: main
generate_release_notes: true
files: |
src/spice2x/dist/spice2x-*.zip
+17 -3
View File
@@ -1,8 +1,7 @@
## Contributing
Baseline rules for patch submissions are as follows. Any patches violating the rules below will not be accepted.
### New as of March 2025
Pull requests in GitHub are open. You don't need to bother with patch file submissions anymore. To contribute, fork the repo (just the main branch), make changes in your fork, and contribute to upstream by opening a pull request to the main repo.
To contribute, fork the repo (just the main branch), make changes in your fork, and contribute to upstream by opening a pull request to the main repo.
### Adding support for games
@@ -12,6 +11,18 @@ Pull requests in GitHub are open. You don't need to bother with patch file submi
* For a new game that was never supported (not a new version of a game, but rather a new series): please wait 1 year after official AC release in Japan.
* For new version of an already supported game: proceed with caution and use generally-accepted community guidelines.
### Pull requests that will not be accepted
* Performing decryption of encrypted files.
* Including (bundling), distributing, or linking to game data, or making it easier to find/download game data.
* Any features that [phone home](https://en.wikipedia.org/wiki/Phoning_home)
* This includes automatic updaters.
* Exception: we have allowed the "patch import from URL" feature, but this has only been done under very careful considerations and plenty of warnings to the user.
* Built-in EA server that saves data. They should live in a separate project.
* Any additions to built-in patches (modifications / removals are OK if we have a good reason).
* Changes to the software license terms.
* Localization / translation for UI text. It becomes too difficult to manage by the maintainers.
### Avoiding regressions
The biggest risk in making code changes to spice is the risk of regressions. There is a **lot** of shared code used by many different games, reaching back game versions that are more than a decade old. It is practically impossible to test all supported games and versions.
@@ -22,7 +33,7 @@ Therefore, when making code changes, please be extremely careful about containin
#### Config file compatibility
Do not change the names of options, buttons binds, analogs, etc - since they are used as unique identifiers in config files. If they are changed, you will introduce an incompatibility with previous versions of config file.
Do not change the names of buttons binds, analogs, strings for command line parameters, etc - since they are used as unique identifiers in config files. If they are changed, you will introduce an incompatibility with previous versions of config file.
### Code quality requirements
@@ -49,6 +60,9 @@ Watch out for legacy OS compatibility. Currently, the minimum support floor is W
### Code style requirements
* Indents are four spaces.
* Try to keep under 100 characters per line. This includes comments and string literals.
* Avoid making unnecessary formatting or whitespace changes in a PR; e.g., watch out for text editor automatically stripping trailing whitespace. It pollutes the diff and slows down code review.
* For signed/unsigned integer types, prefer `cstdint` types; e.g., use `int32_t`, `uint64_t` and not `int` and `unsigned long long`.
* Always use \{ curly braces \} when appropriate; do not omit them even when it's optional; such as `for` `if` `else`, etc.
OK:
+1 -1
View File
@@ -7,7 +7,7 @@
>
> Feel free to remove this section after you have read it.
## Link to GitHub Issue, if one exists
## Link to GitHub Issue or related Pull Request, if one exists
#0
## Description of change
+196 -25
View File
@@ -109,6 +109,12 @@ elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
# RapidJSON does this
add_compile_definitions(_SILENCE_CXX17_ITERATOR_BASE_CLASS_DEPRECATION_WARNING)
add_compile_options("-Wno-nontrivial-memcall") # imgui, rapidjson
add_compile_options("-Wno-deprecated-builtins") # robin_hood
add_compile_options("-Wno-deprecated-declarations") # rapidjson
add_compile_options("-Wno-inconsistent-missing-override") # headsocket
add_compile_options("-Wno-microsoft-exception-spec") # gcc/clang have different noexcept specifiers on d3d9 COM interfaces
# warnings
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wunknown-warning-option")
@@ -118,6 +124,9 @@ elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -static")
else()
# warnings as errors
set(CMAKE_COMPILE_WARNING_AS_ERROR ON)
# warnings
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra") # enable stuff
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-pointer-arith") # but we love pointer arithmetic :)
@@ -167,8 +176,8 @@ else()
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -DNDEBUG")
# file prefix map for relative working directory
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -ffile-prefix-map=${CMAKE_SOURCE_DIR}=.")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -ffile-prefix-map=${CMAKE_SOURCE_DIR}=.")
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -ffile-prefix-map=\"${CMAKE_SOURCE_DIR}=.\"")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -ffile-prefix-map=\"${CMAKE_SOURCE_DIR}=.\"")
endif()
# release with debug info flags
@@ -226,6 +235,13 @@ add_compile_definitions(
_WIN32_IE=0x0400
)
# SPICE_XP: set by build_all.sh for the WinXP-compat toolchains. Gates out
# the DX11 overlay backend
option(SPICE_XP "Build for the WinXP-compat toolchain" OFF)
if(SPICE_XP)
add_compile_definitions(SPICE_XP=1)
endif()
# acioemu log
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DACIOEMU_LOG")
@@ -257,6 +273,7 @@ set_source_files_properties(cfg/manifest.rc PROPERTIES LANGUAGE RC)
set_source_files_properties(cfg/icon.rc PROPERTIES LANGUAGE RC)
set_source_files_properties(cfg/Win32D.rc PROPERTIES LANGUAGE RC)
set_source_files_properties(build/manifest64.rc PROPERTIES LANGUAGE RC)
set_source_files_properties(stubs/manifest.rc PROPERTIES LANGUAGE RC)
# sources
#########
@@ -343,6 +360,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
cfg/configurator.cpp
cfg/configurator_wnd.cpp
cfg/screen_resize.cpp
cfg/controller_presets.cpp
# easrv
easrv/easrv.cpp
@@ -372,6 +390,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/shared/twtouch.cpp
games/popn/popn.cpp
games/popn/io.cpp
games/popn/bi3a_hook.cpp
games/bbc/bbc.cpp
games/bbc/io.cpp
games/hpm/hpm.cpp
@@ -389,15 +408,23 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/iidx/mf_wrappers.cpp
games/sdvx/bi2x_hook.cpp
games/sdvx/sdvx.cpp
games/sdvx/sdvx_live2d.cpp
games/sdvx/io.cpp
games/sdvx/camera.cpp
games/jb/jb.cpp
games/jb/jb_touch.cpp
games/jb/io.cpp
games/nost/nost.cpp
games/nost/io.cpp
games/nost/poke.cpp
games/nost/touch_mode.cpp
games/gitadora/gitadora.cpp
games/gitadora/asio.cpp
games/gitadora/io.cpp
games/gitadora/handle.cpp
games/gitadora/j32d.cpp
games/gitadora/j33i.cpp
games/gitadora/bi2x_hook.cpp
games/mga/mga.cpp
games/mga/io.cpp
games/mga/gunio.cpp
@@ -406,6 +433,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/rb/rb.cpp
games/rb/io.cpp
games/rb/touch.cpp
games/rb/touch_debug.cpp
games/bs/bs.cpp
games/bs/io.cpp
games/rf3d/rf3d.cpp
@@ -434,6 +462,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/scotto/io.cpp
games/drs/drs.cpp
games/drs/io.cpp
games/drs/rgb_cam.cpp
games/we/we.cpp
games/we/io.cpp
games/we/touchpanel.cpp
@@ -469,14 +498,24 @@ set(SOURCE_FILES ${SOURCE_FILES}
# hooks
hooks/audio/acm.cpp
hooks/audio/audio.cpp
hooks/audio/asio_driver_scan.cpp
hooks/audio/asio_proxy.cpp
hooks/audio/buffer.cpp
hooks/audio/mme.cpp
hooks/audio/util.cpp
hooks/audio/xact.cpp
hooks/audio/backends/dsound/dsound_backend.cpp
hooks/audio/backends/mmdevice/audio_endpoint_volume.cpp
hooks/audio/backends/mmdevice/device.cpp
hooks/audio/backends/mmdevice/device_collection.cpp
hooks/audio/backends/mmdevice/device_enumerator.cpp
hooks/audio/backends/mmdevice/null_device.cpp
hooks/audio/backends/mmdevice/null_discard_backend.cpp
hooks/audio/backends/wasapi/audio_client.cpp
hooks/audio/backends/wasapi/audio_render_client.cpp
hooks/audio/backends/wasapi/downmix.cpp
hooks/audio/backends/wasapi/resample.cpp
hooks/audio/backends/wasapi/shared.cpp
hooks/audio/backends/wasapi/dummy_audio_client.cpp
hooks/audio/backends/wasapi/dummy_audio_clock.cpp
hooks/audio/backends/wasapi/dummy_audio_render_client.cpp
@@ -491,19 +530,28 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/devicehook.cpp
hooks/graphics/graphics.cpp
hooks/graphics/graphics_windowed.cpp
hooks/graphics/nvapi_impl.cpp
hooks/graphics/nvapi_hook.cpp
hooks/graphics/nvenc_hook.cpp
hooks/graphics/backends/d3d9/d3d9_backend.cpp
hooks/graphics/backends/d3d9/d3d9_device.cpp
hooks/graphics/backends/d3d9/d3d9_live2d.cpp
hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp
hooks/graphics/backends/d3d9/d3d9_swapchain.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_device.cpp
hooks/input/dinput8/hook.cpp
hooks/lang.cpp
hooks/libraryhook.cpp
hooks/networkhook.cpp
hooks/icmphook_net.cpp
hooks/icmphook_iphlpapi.cpp
hooks/powrprof.cpp
#hooks/rom.cpp
hooks/setupapihook.cpp
@@ -526,16 +574,17 @@ set(SOURCE_FILES ${SOURCE_FILES}
misc/device.cpp
misc/eamuse.cpp
misc/extdev.cpp
misc/nativetouchhook.cpp
misc/sciunit.cpp
misc/sde.cpp
misc/wintouchemu.cpp
misc/ami2000.cpp
# nvapi
nvapi/nvapi.cpp
# overlay
overlay/overlay.cpp
overlay/notifications.cpp
overlay/window.cpp
overlay/imgui/extensions.cpp
overlay/imgui/impl_spice.cpp
@@ -544,9 +593,11 @@ set(SOURCE_FILES ${SOURCE_FILES}
overlay/windows/camera_control.cpp
overlay/windows/card_manager.cpp
overlay/windows/drs_dancefloor.cpp
overlay/windows/gfdm_sub.cpp
overlay/windows/screen_resize.cpp
overlay/windows/sdvx_sub.cpp
overlay/windows/config.cpp
overlay/windows/controller_presets.cpp
overlay/windows/control.cpp
overlay/windows/eadev.cpp
overlay/windows/fps.cpp
@@ -560,11 +611,27 @@ set(SOURCE_FILES ${SOURCE_FILES}
overlay/windows/keypad.cpp
overlay/windows/log.cpp
overlay/windows/midi.cpp
overlay/windows/nostalgia_touch_piano.cpp
overlay/windows/obs.cpp
overlay/windows/obs_websocket.cpp
overlay/windows/patch_manager.cpp
overlay/windows/popn_sub.cpp
overlay/windows/wnd_manager.cpp
# patcher
patcher/config.cpp
patcher/lifecycle.cpp
patcher/loader.cpp
patcher/loader_group.cpp
patcher/runtime.cpp
# external
external/easywsclient/easywsclient.cpp
# rawinput
rawinput/rawinput.cpp
rawinput/rawinput_handles.cpp
rawinput/midi.cpp
rawinput/sextet.cpp
rawinput/piuio.cpp
rawinput/touch.cpp
@@ -575,6 +642,8 @@ set(SOURCE_FILES ${SOURCE_FILES}
rawinput/smxstage.h
rawinput/smxdedicab.cpp
rawinput/smxdedicab.h
rawinput/xinput.cpp
rawinput/xinput.h
# reader
reader/reader.cpp
@@ -582,12 +651,21 @@ set(SOURCE_FILES ${SOURCE_FILES}
reader/structuredmessage.cpp
reader/crypt.cpp
# sdk
sdk/sdk.cpp
# stubs
stubs/stubs.cpp
# touch
touch/gdi_overlay.cpp
touch/native/inject.cpp
touch/native/inject_mouse.cpp
touch/native/inject_synthetic.cpp
touch/native/nativetouchhook.cpp
touch/native/transform.cpp
touch/touch.cpp
touch/touch_indicators.cpp
touch/touch_gestures.cpp
touch/win7.cpp
touch/win8.cpp
@@ -608,47 +686,86 @@ set(SOURCE_FILES ${SOURCE_FILES}
util/time.cpp
util/cpuutils.cpp
util/netutils.cpp
util/precise_timer.cpp
util/sysutils.cpp
util/lz77.cpp
util/tapeled.cpp
util/execexe.cpp
util/dependencies.cpp
util/deferlog.cpp
util/socd_cleaner.cpp
)
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "Source Files" FILES ${SOURCE_FILES})
# guard against statically importing DLLs that must always be loaded dynamically:
# * DLLs users override via the modules directory (e.g. DXVK's d3d9.dll) - a
# static import loads the system copy at startup and preempts the override
# (issue #779).
# * Media Foundation DLLs (mf/mfplat/mfreadwrite) - a static import breaks
# Unity games.
# the check runs objdump on each produced binary and fails the build if any
# forbidden DLL is imported.
set(SPICE_FORBIDDEN_STATIC_IMPORTS
d3d8.dll d3d9.dll d3d10core.dll d3d11.dll dxgi.dll opengl32.dll
mf.dll mfplat.dll mfreadwrite.dll)
function(spice_guard_dll_imports target)
if(MSVC)
# objdump-based parsing assumes a GNU/LLVM toolchain
return()
endif()
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND}
-DOBJDUMP=${CMAKE_OBJDUMP}
-DTARGET_FILE=$<TARGET_FILE:${target}>
"-DFORBIDDEN=${SPICE_FORBIDDEN_STATIC_IMPORTS}"
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_no_static_dll_imports.cmake
VERBATIM
COMMENT "Checking ${target} for forbidden static DLL imports")
endfunction()
# spice.exe / spice_laa.exe shared objects
###########################################
# spice.exe and spice_laa.exe are compiled identically; the only difference is
# the large-address-aware bit, which is set at link time. Compile the sources
# once into a shared OBJECT library so spice_laa.exe only costs an extra link
# instead of a full recompile.
add_library(spicetools_spice_objs OBJECT ${SOURCE_FILES})
target_link_libraries(spicetools_spice_objs
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
target_link_libraries(spicetools_spice_objs PUBLIC winscard)
if(NOT MSVC)
set_target_properties(spicetools_spice_objs PROPERTIES COMPILE_FLAGS "-m32")
endif()
# spice.exe
###########
set(RESOURCE_FILES build/manifest.manifest build/manifest.rc build/icon.rc cfg/Win32D.rc)
add_executable(spicetools_spice ${SOURCE_FILES} ${RESOURCE_FILES})
target_link_libraries(spicetools_spice
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
target_link_libraries(spicetools_spice PUBLIC winscard)
add_executable(spicetools_spice ${RESOURCE_FILES})
target_link_libraries(spicetools_spice PRIVATE spicetools_spice_objs)
set_target_properties(spicetools_spice PROPERTIES PREFIX "")
set_target_properties(spicetools_spice PROPERTIES OUTPUT_NAME "spice")
IF(NOT MSVC)
set_target_properties(spicetools_spice PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32")
set_target_properties(spicetools_spice PROPERTIES LINK_FLAGS "-m32")
endif()
# spice_laa.exe
###########
set(RESOURCE_FILES build/manifest.manifest build/manifest.rc build/icon.rc cfg/Win32D.rc)
add_executable(spicetools_spice_laa ${SOURCE_FILES} ${RESOURCE_FILES})
target_link_libraries(spicetools_spice_laa
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
target_link_libraries(spicetools_spice_laa PUBLIC winscard)
add_executable(spicetools_spice_laa ${RESOURCE_FILES})
target_link_libraries(spicetools_spice_laa PRIVATE spicetools_spice_objs)
set_target_properties(spicetools_spice_laa PROPERTIES PREFIX "")
set_target_properties(spicetools_spice_laa PROPERTIES OUTPUT_NAME "spice_laa")
target_compile_definitions(spicetools_spice_laa PRIVATE SPICE32_LARGE_ADDRESS_AWARE=1)
IF(NOT MSVC)
set_target_properties(spicetools_spice_laa PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32 -Wl,--large-address-aware")
set_target_properties(spicetools_spice_laa PROPERTIES LINK_FLAGS "-m32 -Wl,--large-address-aware")
endif()
# spice_linux.exe
@@ -681,6 +798,10 @@ target_link_libraries(spicetools_spice64 PUBLIC winscard)
set_target_properties(spicetools_spice64 PROPERTIES PREFIX "")
set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64")
target_compile_definitions(spicetools_spice64 PRIVATE SPICE64=1)
if(NOT SPICE_XP)
target_link_libraries(spicetools_spice64 PUBLIC d3d11 dxgi)
target_compile_definitions(spicetools_spice64 PRIVATE SPICE_D3D11=1)
endif()
IF(NOT MSVC)
set_target_properties(spicetools_spice64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
@@ -700,6 +821,9 @@ set_target_properties(spicetools_spice64_linux PROPERTIES PREFIX "")
set_target_properties(spicetools_spice64_linux PROPERTIES OUTPUT_NAME "spice64_linux")
target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE64=1)
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)
target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE_D3D11=1)
IF(NOT MSVC)
set_target_properties(spicetools_spice64_linux PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
@@ -712,7 +836,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)
add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
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)
target_link_libraries(spicetools_cfg PUBLIC winscard)
set_target_properties(spicetools_cfg PROPERTIES PREFIX "")
@@ -730,7 +854,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)
add_executable(spicetools_cfg_linux WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
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)
set_target_properties(spicetools_cfg_linux PROPERTIES PREFIX "")
set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux")
@@ -792,7 +916,8 @@ endif()
# nvcuda.dll
set(SOURCE_FILES stubs/nvcuda.cpp)
add_library(spicetools_stubs_nvcuda SHARED ${SOURCE_FILES} stubs/nvcuda.def)
set(RESOURCE_FILES stubs/manifest.rc)
add_library(spicetools_stubs_nvcuda SHARED ${SOURCE_FILES} ${RESOURCE_FILES} stubs/nvcuda.def)
set_target_properties(spicetools_stubs_nvcuda PROPERTIES PREFIX "")
set_target_properties(spicetools_stubs_nvcuda PROPERTIES OUTPUT_NAME "nvcuda")
@@ -802,7 +927,8 @@ endif()
# nvcuvid.dll
set(SOURCE_FILES stubs/nvcuvid.cpp)
add_library(spicetools_stubs_nvcuvid SHARED ${SOURCE_FILES} stubs/nvcuvid.def)
set(RESOURCE_FILES stubs/manifest.rc)
add_library(spicetools_stubs_nvcuvid SHARED ${SOURCE_FILES} ${RESOURCE_FILES} stubs/nvcuvid.def)
set_target_properties(spicetools_stubs_nvcuvid PROPERTIES PREFIX "")
set_target_properties(spicetools_stubs_nvcuvid PROPERTIES OUTPUT_NAME "nvcuvid")
@@ -812,7 +938,8 @@ endif()
# nvEncodeAPI64.dll
set(SOURCE_FILES stubs/nvEncodeAPI64.cpp)
add_library(spicetools_stubs_nvEncodeAPI64 SHARED ${SOURCE_FILES} stubs/nvEncodeAPI64.def)
set(RESOURCE_FILES stubs/manifest.rc)
add_library(spicetools_stubs_nvEncodeAPI64 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} stubs/nvEncodeAPI64.def)
set_target_properties(spicetools_stubs_nvEncodeAPI64 PROPERTIES PREFIX "")
set_target_properties(spicetools_stubs_nvEncodeAPI64 PROPERTIES OUTPUT_NAME "nvEncodeAPI64")
@@ -822,7 +949,8 @@ endif()
# cpusbxpkm.dll (32 bit)
set(SOURCE_FILES stubs/cpusbxpkm.cpp)
add_library(spicetools_stubs_cpusbxpkm SHARED ${SOURCE_FILES} stubs/cpusbxpkm.def)
set(RESOURCE_FILES stubs/manifest.rc)
add_library(spicetools_stubs_cpusbxpkm SHARED ${SOURCE_FILES} ${RESOURCE_FILES} stubs/cpusbxpkm.def)
set_target_properties(spicetools_stubs_cpusbxpkm PROPERTIES PREFIX "")
set_target_properties(spicetools_stubs_cpusbxpkm PROPERTIES OUTPUT_NAME "cpusbxpkm")
@@ -830,6 +958,36 @@ if(NOT MSVC)
set_target_properties(spicetools_stubs_cpusbxpkm PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32")
endif()
# sdk_sample_v0_flat_c.dll (32 bit)
set(SOURCE_FILES sdk/sample/v0/flat_c/v0_flat_c.c)
add_library(spicetools_sdk_sample_v0_flat_c_32 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/flat_c/v0_flat_c.def)
set_target_properties(spicetools_sdk_sample_v0_flat_c_32 PROPERTIES PREFIX "")
set_target_properties(spicetools_sdk_sample_v0_flat_c_32 PROPERTIES OUTPUT_NAME "sdk_sample_v0_flat_c")
if(NOT MSVC)
set_target_properties(spicetools_sdk_sample_v0_flat_c_32 PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32")
endif()
# sdk_sample_v0_flat_c.dll (64 bit)
set(SOURCE_FILES sdk/sample/v0/flat_c/v0_flat_c.c)
add_library(spicetools_sdk_sample_v0_flat_c_64 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/flat_c/v0_flat_c.def)
set_target_properties(spicetools_sdk_sample_v0_flat_c_64 PROPERTIES PREFIX "")
set_target_properties(spicetools_sdk_sample_v0_flat_c_64 PROPERTIES OUTPUT_NAME "sdk_sample_v0_flat_c")
if(NOT MSVC)
set_target_properties(spicetools_sdk_sample_v0_flat_c_64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
endif()
# sdk_sample_v0_cpp.dll (64 bit)
set(SOURCE_FILES sdk/sample/v0/cpp/v0_cpp.cpp)
add_library(spicetools_sdk_sample_v0_cpp_64 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/cpp/v0_cpp.def)
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES PREFIX "")
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES OUTPUT_NAME "sdk_sample_v0_cpp")
if(NOT MSVC)
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES COMPILE_FLAGS "-m64" LINK_FLAGS "-m64")
endif()
# output directories
####################
@@ -839,15 +997,28 @@ set_target_properties(spicetools_cfg spicetools_cfg_linux
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools")
# output 32bit
set_target_properties(spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_kbt spicetools_stubs_kld spicetools_stubs_cpusbxpkm
set_target_properties(spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_kbt spicetools_stubs_kld spicetools_stubs_cpusbxpkm spicetools_sdk_sample_v0_flat_c_32
PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive32"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/32")
# output 64bit
set_target_properties(spicetools_spice64 spicetools_spice64_linux spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvcuda spicetools_stubs_nvcuvid spicetools_stubs_nvEncodeAPI64
set_target_properties(spicetools_spice64 spicetools_spice64_linux spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvcuda spicetools_stubs_nvcuvid spicetools_stubs_nvEncodeAPI64 spicetools_sdk_sample_v0_flat_c_64 spicetools_sdk_sample_v0_cpp_64
PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive64"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64")
# forbidden static DLL import guard
###################################
# apply the check to every executable and DLL produced by this project.
get_property(spice_all_targets DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY BUILDSYSTEM_TARGETS)
foreach(spice_target IN LISTS spice_all_targets)
get_target_property(spice_target_type ${spice_target} TYPE)
if(spice_target_type STREQUAL "EXECUTABLE"
OR spice_target_type STREQUAL "SHARED_LIBRARY"
OR spice_target_type STREQUAL "MODULE_LIBRARY")
spice_guard_dll_imports(${spice_target})
endif()
endforeach()
+10
View File
@@ -283,6 +283,16 @@ Python is the only one that is fully spec compliant.
Other libraries may be missing features and contain bugs; please feel free to
contribute code to fill the gaps if you work on a project using these libraries.
## Spice SDK (for DLL hooks / plugins)
As an alternative to Spice API, Spice SDK is available.
This is a flat C, header-only library that can be included in your DLL. Once
initialized, you can call directly into helper routines provided by Spicetools
executable, making low-latency and high-throughput interactions possible.
Check the documentation on the wiki for more details.
## License
Unless otherwise noted, all files are licensed under the GPLv3.
See the LICENSE file for the full license text.
+30 -6
View File
@@ -9,11 +9,22 @@
#include "games/drs/drs.h"
#include "misc/eamuse.h"
#include "util/logging.h"
#include "util/socd_cleaner.h"
#include "util/time.h"
#include "util/utils.h"
#include "util/tapeled.h"
using namespace GameAPI;
#define DEBUG_VERBOSE 0
#if DEBUG_VERBOSE
#define log_debug(module, format_str, ...) logger::push( \
LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY)
#else
#define log_debug(module, format_str, ...)
#endif
// state
static uint8_t STATUS_BUFFER[272] {};
static bool STATUS_BUFFER_FREEZE = false;
@@ -94,18 +105,25 @@ static bool __cdecl ac_io_bi2a_update_control_status_buffer() {
}
// volume left
if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Left))) {
const auto now = get_performance_milliseconds();
const auto vol_l_state = socd::socd_clean(0,
Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Left)),
Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Right)),
now);
if (vol_l_state == socd::SocdCCW) {
BI2A_VOLL = (BI2A_VOLL - games::sdvx::DIGITAL_KNOB_SENS) & 1023;
}
if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Right))) {
} else if (vol_l_state == socd::SocdCW) {
BI2A_VOLL = (BI2A_VOLL + games::sdvx::DIGITAL_KNOB_SENS) & 1023;
}
// volume right
if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Left))) {
const auto vol_r_state = socd::socd_clean(1,
Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Left)),
Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Right)),
now);
if (vol_r_state == socd::SocdCCW) {
BI2A_VOLR = (BI2A_VOLR - games::sdvx::DIGITAL_KNOB_SENS) & 1023;
}
if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Right))) {
} else if (vol_r_state == socd::SocdCW) {
BI2A_VOLR = (BI2A_VOLR + games::sdvx::DIGITAL_KNOB_SENS) & 1023;
}
@@ -127,6 +145,12 @@ static bool __cdecl ac_io_bi2a_update_control_status_buffer() {
// save volumes in buffer
*((uint16_t*) &STATUS_BUFFER[17]) = (uint16_t) ((vol_left) << 2);
*((uint16_t*) &STATUS_BUFFER[19]) = (uint16_t) ((vol_right) << 2);
log_debug(
"bi2a",
"knobs = {} {}",
*((uint16_t*) &STATUS_BUFFER[17]),
*((uint16_t*) &STATUS_BUFFER[19]));
}
// DanceDanceRevolution
+3 -1
View File
@@ -4,6 +4,7 @@
#include "launcher/launcher.h"
#include "misc/wintouchemu.h"
#include "rawinput/rawinput.h"
#include "touch/native/nativetouchhook.h"
// static stuff
static int ACIO_WARMUP = 0;
@@ -140,8 +141,9 @@ static int __cdecl ac_io_update(int a1) {
// flush device output
RI_MGR->devices_flush_output();
// update wintouchemu
// update touch emulation
wintouchemu::update();
nativetouch::refresh_contact_lifetime();
return 1;
}
+122 -20
View File
@@ -35,24 +35,33 @@ struct ICCA_STATUS_LA9 {
static_assert(sizeof(struct ICCA_STATUS) == 24, "ICCA_STATUS must be 24 bytes");
enum ICCA_WORKFLOW {
STEP,
SLEEP,
START,
INIT,
READY,
GET_USERID,
ACTIVE,
EJECT,
EJECT_CHECK,
END,
CLOSE_EJECT,
CLOSE_E_CHK,
CLOSE_END,
ERR_GETUID = -2
STEP = 0,
SLEEP = 1,
START = 2,
INIT = 3,
READY = 4,
GET_USERID = 5,
ACTIVE = 6,
EJECT = 7,
EJECT_CHECK = 8,
END = 9,
CLOSE_EJECT = 10,
CLOSE_E_CHK = 11,
CLOSE_END = 12,
ERR_GETUID = -2,
ERR_REMOVED = -11
};
// used by RA as the status_code
enum ICCA_STATUS_CODE {
EMPTY = 1,
ENGAGED = 2
};
struct ICCA_UNIT {
struct ICCA_STATUS status {};
enum ICCA_WORKFLOW state = STEP;
enum ICCA_WORKFLOW state_jdz = SLEEP;
bool card_cmd_pressed = false;
bool card_in = false;
double card_in_time = 0.0;
@@ -75,6 +84,11 @@ static inline int icca_get_active_count() {
}
static inline int icca_get_unit_id(int unit_id) {
// on RA the unit_id is zero-indexed
if (avs::game::is_model("JDZ")) {
return unit_id;
}
if (icca_get_active_count() < 2)
return 1;
else {
@@ -122,9 +136,15 @@ static inline void update_card(int unit_id) {
IS_LAST_CARD_FELICA = is_card_uid_felica(unit->status.uid);
unit->state = acio::ICCA_COMPAT_ACTIVE ? START : ACTIVE;
unit->state_jdz = ACTIVE;
unit->status.error = 0;
unit->status.front_sensor = 1;
unit->status.rear_sensor = 1;
} else {
unit->state = ERR_GETUID;
unit->state_jdz = ERR_GETUID;
unit->status.front_sensor = 1;
unit->status.rear_sensor = 1;
memset(unit->status.uid, 0, 8);
}
unit->card_in = true;
@@ -132,10 +152,25 @@ static inline void update_card(int unit_id) {
} else if (unit->state == EJECT_CHECK) {
unit->state = SLEEP;
unit->card_in = false;
unit->status.front_sensor = 0;
unit->status.rear_sensor = 0;
} else if (unit->state_jdz == READY) {
unit->state = GET_USERID;
unit->state_jdz = GET_USERID;
unit->status.error = 0;
unit->status.front_sensor = 1;
unit->status.rear_sensor = 1;
unit->card_in = true;
unit->card_in_time = get_performance_seconds();
}
} else {
unit->state = acio::ICCA_COMPAT_ACTIVE ? START : ACTIVE;
if (unit->state_jdz == READY) {
unit->state_jdz = GET_USERID;
}
unit->status.error = 0;
unit->status.front_sensor = 1;
unit->status.rear_sensor = 1;
unit->card_in = true;
unit->card_in_time = get_performance_seconds();
}
@@ -144,6 +179,12 @@ static inline void update_card(int unit_id) {
unit->state = CLOSE_EJECT;
if (fabs(get_performance_seconds() - unit->card_in_time) > CARD_TIMEOUT) {
unit->card_in = false;
memset(unit->status.uid, 0, 8);
if (unit->state_jdz == ACTIVE || unit->state_jdz == GET_USERID) {
unit->state_jdz = ERR_REMOVED;
}
unit->status.front_sensor = 0;
unit->status.rear_sensor = 0;
}
}
@@ -302,7 +343,11 @@ static char __cdecl ac_io_icca_get_status(void *a1, void *a2) {
// copy state to output buffer
ICCA_UNIT *unit = &ICCA_UNITS[unit_id];
unit->status.status_code = unit->state;
if (avs::game::is_model("JDZ")) {
unit->status.status_code = unit->card_in ? ENGAGED : EMPTY;
} else {
unit->status.status_code = unit->state;
}
memcpy(status, &unit->status, sizeof(struct ICCA_STATUS));
// funny workaround
@@ -387,6 +432,11 @@ static char __cdecl ac_io_icca_req_uid(int unit_id) {
static int __cdecl ac_io_icca_req_uid_isfinished(int unit_id, DWORD *read_state) {
unit_id = icca_get_unit_id(unit_id);
ICCA_UNIT *unit = &ICCA_UNITS[unit_id];
if (avs::game::is_model("JDZ")) {
// hack for RA - only ever seen it as this
*read_state = 8;
return 1;
}
if (unit->card_in) {
if (fabs(get_performance_seconds() - unit->card_in_time) < CARD_TIMEOUT) {
unit->state = END;
@@ -403,16 +453,61 @@ static int __cdecl ac_io_icca_send_keep_alive_packet(int a1, int a2, int a3) {
return 0;
}
static int __cdecl ac_io_icca_workflow_jdz(int workflow, int unit_id) {
unit_id = icca_get_unit_id(unit_id);
ICCA_UNIT *unit = &ICCA_UNITS[unit_id];
switch (workflow) {
// RA uses "STEP" as more of a polling thing it seems
case STEP:
switch (unit->state_jdz) {
case CLOSE_EJECT:
unit->state_jdz = CLOSE_E_CHK;
break;
case CLOSE_E_CHK:
unit->state_jdz = CLOSE_END;
break;
case CLOSE_END:
unit->state_jdz = SLEEP;
break;
default:
break;
}
break;
case SLEEP:
unit->state = SLEEP;
break;
case START:
unit->state_jdz = INIT;
break;
case INIT:
case READY:
unit->state_jdz = READY;
break;
case CLOSE_EJECT:
unit->state_jdz = CLOSE_EJECT;
return CLOSE_E_CHK;
case CLOSE_END:
unit->state_jdz = SLEEP;
break;
default:
// should probably log a warning here
break;
}
return unit->state_jdz;
}
static int __cdecl ac_io_icca_workflow(int workflow, int unit_id) {
// RA uses a different workflow process for the slotted readers; treat it differently
if (avs::game::is_model("JDZ")) {
return ac_io_icca_workflow_jdz(workflow, unit_id);
}
unit_id = icca_get_unit_id(unit_id);
ICCA_UNIT *unit = &ICCA_UNITS[unit_id];
switch (workflow) {
case STEP:
if (avs::game::is_model("JDZ"))
unit->state = SLEEP;
else
unit->state = STEP;
unit->state = STEP;
break;
case SLEEP:
unit->state = SLEEP;
@@ -441,7 +536,6 @@ static int __cdecl ac_io_icca_workflow(int workflow, int unit_id) {
default:
break;
}
return unit->state;
}
@@ -467,6 +561,14 @@ acio::ICCAModule::ICCAModule(HMODULE module, acio::HookMode hookMode) : ACIOModu
void acio::ICCAModule::attach() {
ACIOModule::attach();
// workaround for RA; ac_io_icca_cardunit_init is never called so treat like both are initialized
if (avs::game::is_model("JDZ")) {
ICCA_UNITS[0].initialized = true;
ICCA_UNITS[1].initialized = true;
CARD_TIMEOUT = 10.0; // keep the card in the slot for longer
}
// hooks
ACIO_MODULE_HOOK(ac_io_icca_cardunit_init);
ACIO_MODULE_HOOK(ac_io_icca_cardunit_init_isfinished);
+5 -5
View File
@@ -52,7 +52,7 @@ static bool __cdecl ac_io_j32d_update_control_status_buffer(size_t a1) {
float pad3_vel = Buttons::getVelocity(RI_MGR, buttons.at(games::ftt::Buttons::Pad3));
float pad4_vel = Buttons::getVelocity(RI_MGR, buttons.at(games::ftt::Buttons::Pad4));
// FIXME(felix): this logic seems wrong for analog handling but correct for digital inputs
if (pad1_vel > 0.f) {
if (Buttons::getState(RI_MGR, buttons.at(games::ftt::Buttons::Pad1))) {
STATUS_BUFFER[6] = (int) (51.f * pad1_vel + 0.5f);
}
if (Buttons::getState(RI_MGR, buttons.at(games::ftt::Buttons::Pad2))) {
@@ -67,10 +67,10 @@ static bool __cdecl ac_io_j32d_update_control_status_buffer(size_t a1) {
// process analogs
auto &analogs = games::ftt::get_analogs();
auto pad1_analog = analogs.at(games::ftt::Analogs::Pad1);
auto pad2_analog = analogs.at(games::ftt::Analogs::Pad2);
auto pad3_analog = analogs.at(games::ftt::Analogs::Pad3);
auto pad4_analog = analogs.at(games::ftt::Analogs::Pad4);
auto &pad1_analog = analogs.at(games::ftt::Analogs::Pad1);
auto &pad2_analog = analogs.at(games::ftt::Analogs::Pad2);
auto &pad3_analog = analogs.at(games::ftt::Analogs::Pad3);
auto &pad4_analog = analogs.at(games::ftt::Analogs::Pad4);
if (pad1_analog.isSet()) {
auto val = (uint32_t) (51.f * Analogs::getState(RI_MGR, pad1_analog) + 0.5f);
STATUS_BUFFER[6] = MAX(STATUS_BUFFER[6], val);
+26 -6
View File
@@ -8,10 +8,21 @@
#include "games/sdvx/io.h"
#include "misc/eamuse.h"
#include "rawinput/rawinput.h"
#include "util/socd_cleaner.h"
#include "util/time.h"
#include "util/utils.h"
using namespace GameAPI;
#define DEBUG_VERBOSE 0
#if DEBUG_VERBOSE
#define log_debug(module, format_str, ...) logger::push( \
LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY)
#else
#define log_debug(module, format_str, ...)
#endif
// globals
uint8_t KFCA_VOL_SOUND = 96;
uint8_t KFCA_VOL_HEADPHONE = 96;
@@ -342,18 +353,25 @@ static char __cdecl ac_io_kfca_update_control_status_buffer() {
}
// volume left
if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Left))) {
const auto now = get_performance_milliseconds();
const auto vol_l_state = socd::socd_clean(0,
Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Left)),
Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Right)),
now);
if (vol_l_state == socd::SocdCCW) {
KFCA_VOLL = (KFCA_VOLL - games::sdvx::DIGITAL_KNOB_SENS) & 1023;
}
if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Right))) {
} else if (vol_l_state == socd::SocdCW) {
KFCA_VOLL = (KFCA_VOLL + games::sdvx::DIGITAL_KNOB_SENS) & 1023;
}
// volume right
if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Left))) {
const auto vol_r_state = socd::socd_clean(1,
Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Left)),
Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Right)),
now);
if (vol_r_state == socd::SocdCCW) {
KFCA_VOLR = (KFCA_VOLR - games::sdvx::DIGITAL_KNOB_SENS) & 1023;
}
if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Right))) {
} else if (vol_r_state == socd::SocdCW) {
KFCA_VOLR = (KFCA_VOLR + games::sdvx::DIGITAL_KNOB_SENS) & 1023;
}
@@ -372,6 +390,8 @@ static char __cdecl ac_io_kfca_update_control_status_buffer() {
vol_left %= 1024;
vol_right %= 1024;
log_debug("kfca", "knobs = {} {}", vol_left, vol_right);
// save volumes in buffer
STATUS_BUFFER[input_offset + 16 + 0] |= (unsigned char) ((vol_left << 6) & 0xFF);
STATUS_BUFFER[input_offset + 16 + 1] |= (unsigned char) ((vol_left >> 2) & 0xFF);
+22 -7
View File
@@ -1,13 +1,16 @@
#include "mdxf.h"
#include "mdxf_poll.h"
#include <mutex>
#include "avs/game.h"
#include "games/ddr/ddr.h"
#include "games/ddr/io.h"
#include "launcher/launcher.h"
#include "rawinput/rawinput.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include <mutex>
#define MDFX_DEBUG_VERBOSE 0
@@ -108,9 +111,10 @@ static void mdxf_thread_start() {
MDXF_THREAD = std::thread([] {
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
timeutils::PreciseSleepTimer timer;
while (MDXF_THREAD_RUNNING.load(std::memory_order_acquire)) {
mdxf_poll(false);
std::this_thread::sleep_for(THREAD_PERIOD);
timer.sleep(THREAD_PERIOD);
}
});
}
@@ -357,14 +361,17 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFP
// decide on button map
const size_t *button_map = nullptr;
int player = 0;
switch (node) {
case 17:
case 25:
button_map = &buttons_p1[0];
player = 1;
break;
case 18:
case 26:
button_map = &buttons_p2[0];
player = 2;
break;
}
@@ -374,19 +381,27 @@ static bool __cdecl ac_io_mdxf_update_control_status_buffer_impl(int node, MDXFP
if (source == EXTERNAL_POLL) {
// 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 left_right = 0;
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[0]))) {
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(button_map[0])) || analog_up) {
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;
}
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;
}
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;
}
current_state = (uint16_t(up_down) << 8) | left_right;
+12
View File
@@ -3,6 +3,7 @@
#include "rawinput/rawinput.h"
#include "games/nost/io.h"
#include "games/nost/nost.h"
#include "games/nost/touch_mode.h"
#include "util/logging.h"
#include "avs/game.h"
@@ -16,6 +17,7 @@ using namespace GameAPI;
// static stuff
static uint8_t STATUS_BUFFER[277];
static bool STATUS_BUFFER_FREEZE = false;
static constexpr uint8_t NOST_TOUCH_PIANO_VELOCITY = 11;
/*
* Implementations
@@ -226,6 +228,8 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
games::nost::Analogs::Key27, games::nost::Analogs::Key28,
};
const auto touch_key_state = games::nost::touch_mode::piano_key_state();
// iterate pairs of keys
for (size_t key_pair = 0; key_pair < 28 / 2; key_pair++) {
@@ -257,6 +261,10 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
if (button0 > 0) {
state0 = button0;
}
if ((touch_key_state & (UINT32_C(1) << (key_pair * 2))) &&
state0 < NOST_TOUCH_PIANO_VELOCITY) {
state0 = NOST_TOUCH_PIANO_VELOCITY;
}
const auto button1 = panb_get_button_velocity(
buttons.at(button_mapping[key_pair * 2 + 1]),
@@ -267,6 +275,10 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
if (button1 > 0) {
state1 = button1;
}
if ((touch_key_state & (UINT32_C(1) << (key_pair * 2 + 1))) &&
state1 < NOST_TOUCH_PIANO_VELOCITY) {
state1 = NOST_TOUCH_PIANO_VELOCITY;
}
// build value
uint8_t value = 0;
+1
View File
@@ -1,5 +1,6 @@
#pragma once
#include <cstdint>
#include <string>
#include "util/circular_buffer.h"
+8 -12
View File
@@ -2,8 +2,10 @@
#include "acio/icca/icca.h"
#include "avs/game.h"
#include "games/sdvx/sdvx.h"
#include "misc/eamuse.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
using namespace acioemu;
@@ -45,11 +47,12 @@ ICCADevice::ICCADevice(bool flip_order, bool keypad_thread, uint8_t node_count)
// keypad thread for faster polling
if (keypad_thread) {
this->keypad_thread = new std::thread([this]() {
timeutils::PreciseSleepTimer timer;
while (this->cards) {
for (int unit = 0; unit < this->node_count; unit++) {
this->update_keypad(unit, false);
}
Sleep(7);
timer.sleep(7);
}
});
}
@@ -101,13 +104,10 @@ bool ICCADevice::parse_msg(MessageData *msg_in,
// send version data
auto msg = this->create_msg(msg_in, MSG_VERSION_SIZE);
if (
avs::game::is_model({"LDJ", "TBS", "UJK", "XIF"}) ||
// SDVX Valkyrie cabinet mode
(avs::game::is_model("KFC") && (avs::game::SPEC[0] == 'G' || avs::game::SPEC[0] == 'H'))
) {
if (avs::game::is_model({"LDJ", "TBS", "UJK", "XIF"}) ||
games::sdvx::is_valkyrie_model()) {
this->set_version(msg, 0x3, 0, 1, 7, 0, "ICCA");
} else if (avs::game::is_model({"VFG"})) {
} else if (avs::game::is_model("VFG")) {
this->set_version(msg, 0x3, 0, 1, 7, 0, "ICCB");
} else {
this->set_version(msg, 0x3, 0, 1, 6, 0, "ICCA");
@@ -520,11 +520,7 @@ void ICCADevice::update_status(int unit) {
buffer[1] = felica ? 0x01 : 0x00;
buffer[10] = felica ? 0x01 : 0x00;
} else if (
avs::game::is_model({"LDJ", "TBS", "XIF"}) ||
// SDVX Valkyrie cabinet mode
(avs::game::is_model("KFC") && (avs::game::SPEC[0] == 'G' || avs::game::SPEC[0] == 'H'))) {
} else if (avs::game::is_model({"LDJ", "TBS", "XIF"}) || games::sdvx::is_valkyrie_model()) {
// set status to 0 otherwise reader power on fails
buffer[0] = 0x00;
} else {
+8
View File
@@ -11,6 +11,8 @@
#include "util/logging.h"
#include "util/utils.h"
#include "overlay/notifications.h"
#include "module.h"
#include "modules/analogs.h"
#include "modules/buttons.h"
@@ -197,6 +199,9 @@ void Controller::connection_handler(api::ClientState client_state) {
// log connection
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.emplace_back(&client_state);
client_states_m.unlock();
@@ -277,6 +282,9 @@ void Controller::connection_handler(api::ClientState client_state) {
// log disconnect
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.erase(std::remove(client_states.begin(), client_states.end(), &client_state));
client_states_m.unlock();
+4
View File
@@ -6,6 +6,7 @@
#include "launcher/launcher.h"
#include "games/io.h"
#include "util/utils.h"
#include "acio/mdxf/mdxf_poll.h"
using namespace std::placeholders;
using namespace rapidjson;
@@ -104,6 +105,7 @@ namespace api::modules {
for (auto &analog : *this->analogs) {
analog.override_enabled = false;
}
mdxf_poll(true);
}
return;
}
@@ -148,6 +150,7 @@ namespace api::modules {
if (analog.getName() == name) {
analog.override_state = CLAMP(state, 0.f, 1.f);
analog.override_enabled = true;
mdxf_poll(true);
return true;
}
}
@@ -167,6 +170,7 @@ namespace api::modules {
for (auto &analog : *this->analogs) {
if (analog.getName() == name) {
analog.override_enabled = false;
mdxf_poll(true);
return true;
}
}
+4
View File
@@ -6,6 +6,7 @@
#include "launcher/launcher.h"
#include "games/io.h"
#include "util/utils.h"
#include "acio/mdxf/mdxf_poll.h"
using namespace std::placeholders;
using namespace rapidjson;
@@ -107,6 +108,7 @@ namespace api::modules {
for (auto &button : *this->buttons) {
button.override_enabled = false;
}
mdxf_poll(true);
}
return;
}
@@ -153,6 +155,7 @@ namespace api::modules {
GameAPI::Buttons::BUTTON_PRESSED : GameAPI::Buttons::BUTTON_NOT_PRESSED;
button.override_velocity = CLAMP(state, 0.f, 1.f);
button.override_enabled = true;
mdxf_poll(true);
return true;
}
}
@@ -172,6 +175,7 @@ namespace api::modules {
for (auto &button : *this->buttons) {
if (button.getName() == name) {
button.override_enabled = false;
mdxf_poll(true);
return true;
}
}
+59 -15
View File
@@ -1,5 +1,7 @@
#include "capture.h"
#include <functional>
#include <mutex>
#include <unordered_map>
#include "external/rapidjson/document.h"
#include "hooks/graphics/graphics.h"
#include "util/crypt.h"
@@ -14,6 +16,56 @@ namespace api::modules {
static thread_local std::vector<uint8_t> CAPTURE_BUFFER;
struct CachedFrame {
std::vector<uint8_t> jpeg;
uint64_t timestamp = 0;
int width = 0;
int height = 0;
};
static std::mutex FRAME_CACHE_M;
static std::unordered_map<int, CachedFrame> FRAME_CACHE;
static void add_jpeg_response(
int screen,
uint64_t timestamp,
int width,
int height,
const std::vector<uint8_t> &jpeg,
Response &res) {
auto encoded = crypt::base64_encode(jpeg.data(), jpeg.size());
Value data;
data.SetString(encoded.c_str(), encoded.length(), res.doc()->GetAllocator());
res.add_data(timestamp);
res.add_data(width);
res.add_data(height);
res.add_data(data);
std::lock_guard<std::mutex> lock(FRAME_CACHE_M);
FRAME_CACHE[screen] = {jpeg, timestamp, width, height};
}
static bool try_cached_response(int screen, Response &res) {
std::lock_guard<std::mutex> lock(FRAME_CACHE_M);
const auto pos = FRAME_CACHE.find(screen);
if (pos == FRAME_CACHE.end() || pos->second.jpeg.empty()) {
return false;
}
const auto &cached = pos->second;
auto encoded = crypt::base64_encode(cached.jpeg.data(), cached.jpeg.size());
Value data;
data.SetString(encoded.c_str(), encoded.length(), res.doc()->GetAllocator());
res.add_data(cached.timestamp);
res.add_data(cached.width);
res.add_data(cached.height);
res.add_data(data);
return true;
}
Capture::Capture() : Module("capture") {
functions["get_screens"] = std::bind(&Capture::get_screens, this, _1, _2);
functions["get_jpg"] = std::bind(&Capture::get_jpg, this, _1, _2);
@@ -41,6 +93,7 @@ namespace api::modules {
* reduce: uint for dividing image size
*/
void Capture::get_jpg(Request &req, Response &res) {
CAPTURE_BUFFER.clear();
CAPTURE_BUFFER.reserve(1024 * 128);
// settings
@@ -71,24 +124,15 @@ namespace api::modules {
bool success = graphics_capture_receive_jpeg(screen, [] (uint8_t byte) {
CAPTURE_BUFFER.push_back(byte);
}, true, quality, true, divide, &timestamp, &width, &height);
if (!success) {
if (success) {
add_jpeg_response(screen, timestamp, width, height, CAPTURE_BUFFER, res);
CAPTURE_BUFFER.clear();
return;
}
// encode to base64
auto encoded = crypt::base64_encode(
CAPTURE_BUFFER.data(),
CAPTURE_BUFFER.size());
// clear buffer
// fall back to the last successful frame while the game is busy loading
CAPTURE_BUFFER.clear();
// add data to response
Value data;
data.SetString(encoded.c_str(), encoded.length(), res.doc()->GetAllocator());
res.add_data(timestamp);
res.add_data(width);
res.add_data(height);
res.add_data(data);
try_cached_response(screen, res);
}
}
+2 -2
View File
@@ -34,7 +34,7 @@ namespace api::modules {
void DRS::touch_set(Request &req, Response &res) {
// get all touch points
games::drs::drs_touch_t touches[16];
auto touches = std::make_unique<games::drs::drs_touch_t[]>(req.params.Size());
size_t i = 0;
for (Value &param : req.params.GetArray()) {
@@ -86,6 +86,6 @@ namespace api::modules {
}
// apply touch points
games::drs::fire_touches(touches, i);
games::drs::fire_touches(touches.get(), i);
}
}
+4 -2
View File
@@ -7,6 +7,7 @@
#include "avs/game.h"
#include "external/rapidjson/document.h"
#include "misc/eamuse.h"
#include "util/precise_timer.h"
using namespace std::placeholders;
using namespace rapidjson;
@@ -58,6 +59,7 @@ namespace api::modules {
// get params
auto keypad = req.params[0].GetUint();
auto input = std::string(req.params[1].GetString());
timeutils::PreciseSleepTimer timer;
// process all chars
for (auto c : input) {
@@ -91,11 +93,11 @@ namespace api::modules {
// set
eamuse_set_keypad_overrides(keypad, state);
Sleep(sleep_time);
timer.sleep(sleep_time);
// unset
eamuse_set_keypad_overrides(keypad, 0);
Sleep(sleep_time);
timer.sleep(sleep_time);
}
}
+3 -3
View File
@@ -58,7 +58,7 @@ namespace api::modules {
// check if file exists in modules
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
@@ -118,7 +118,7 @@ namespace api::modules {
// check if file exists in modules
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
@@ -196,7 +196,7 @@ namespace api::modules {
// check if file exists in modules
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
+84 -2
View File
@@ -1,5 +1,7 @@
#include "touch.h"
#include <algorithm>
#include <cstdint>
#include <functional>
#include "external/rapidjson/document.h"
@@ -8,6 +10,8 @@
#include "misc/eamuse.h"
#include "launcher/launcher.h"
#include "touch/touch.h"
#include "touch/native/inject.h"
#include "touch/native/nativetouchhook.h"
#include "util/utils.h"
#include "games/iidx/iidx.h"
@@ -17,6 +21,25 @@ using namespace rapidjson;
namespace api::modules {
static bool inject_native_touch(
const std::vector<TouchPoint> &touch_points, int canvas_w, int canvas_h) {
if (touch_points.empty()) {
// no active touches remain; release the synthetic contact
return nativetouch::inject::inject_synthetic_touch({ 0, 0 }, false);
}
// only a single synthetic contact is supported, so just use the first touch point
const auto &touch = touch_points.front();
POINT position { touch.x, touch.y };
if (canvas_w > 0 && canvas_h > 0) {
return nativetouch::inject::inject_synthetic_touch_from_canvas(
position,
{ canvas_w, canvas_h },
true);
}
return nativetouch::inject::inject_synthetic_touch(position, true);
}
Touch::Touch() : Module("touch") {
is_sdvx = avs::game::is_model("KFC");
@@ -26,6 +49,25 @@ namespace api::modules {
is_tdj_fhd = false;
}
use_native = nativetouch::is_hooked();
// resolution the API/companion sends native touch coordinates in (after errata)
native_canvas_w = 0;
native_canvas_h = 0;
if (is_sdvx) {
// exceed gear subscreen, portrait after the rotation applied in apply_touch_errata
native_canvas_w = 1080;
native_canvas_h = 1920;
} else if (avs::game::is_model("LDJ")) {
// TDJ subscreen; FHD models are upscaled to 1080p by apply_touch_errata
native_canvas_w = is_tdj_fhd ? 1920 : 1280;
native_canvas_h = is_tdj_fhd ? 1080 : 720;
} else if (avs::game::is_model("M39")) {
// pop'n music API touch surface
native_canvas_w = 1280;
native_canvas_h = 800;
}
functions["read"] = std::bind(&Touch::read, this, _1, _2);
functions["write"] = std::bind(&Touch::write, this, _1, _2);
functions["write_reset"] = std::bind(&Touch::write_reset, this, _1, _2);
@@ -99,7 +141,11 @@ namespace api::modules {
}
// apply touch points
touch_write_points(&touch_points);
if (use_native) {
write_native(touch_points);
} else {
touch_write_points(&touch_points);
}
}
/**
@@ -123,7 +169,43 @@ namespace api::modules {
}
// remove all IDs
touch_remove_points(&touch_point_ids);
if (use_native) {
write_reset_native(touch_point_ids);
} else {
touch_remove_points(&touch_point_ids);
}
}
void Touch::write_native(const std::vector<TouchPoint> &touch_points) {
if (touch_points.empty()) {
return;
}
std::lock_guard<std::mutex> lock(native_touch_mutex);
const auto touch_id = touch_points.front().id;
if (native_touch_id && *native_touch_id != touch_id) {
if (!inject_native_touch({}, native_canvas_w, native_canvas_h)) {
return;
}
native_touch_id.reset();
}
if (inject_native_touch(touch_points, native_canvas_w, native_canvas_h)) {
native_touch_id = touch_id;
}
}
void Touch::write_reset_native(const std::vector<DWORD> &touch_point_ids) {
std::lock_guard<std::mutex> lock(native_touch_mutex);
if (!native_touch_id ||
std::find(
touch_point_ids.begin(),
touch_point_ids.end(),
*native_touch_id) == touch_point_ids.end()) {
return;
}
if (inject_native_touch({}, native_canvas_w, native_canvas_h)) {
native_touch_id.reset();
}
}
void Touch::apply_touch_errata(int &x, int &y) {
+14
View File
@@ -1,8 +1,12 @@
#pragma once
#include <cstdint>
#include <mutex>
#include <optional>
#include <vector>
#include "api/module.h"
#include "api/request.h"
#include "touch/touch.h"
namespace api::modules {
@@ -13,11 +17,21 @@ namespace api::modules {
private:
bool is_sdvx;
bool is_tdj_fhd;
bool use_native;
std::mutex native_touch_mutex;
std::optional<uint32_t> native_touch_id;
// resolution the API/companion sends native touch coordinates in (after errata);
// used to normalize before mapping into the native injection window
int native_canvas_w;
int native_canvas_h;
// function definitions
void read(Request &req, Response &res);
void write(Request &req, Response &res);
void write_reset(Request &req, Response &res);
void write_native(const std::vector<TouchPoint> &touch_points);
void write_reset_native(const std::vector<DWORD> &touch_point_ids);
void apply_touch_errata(int &x, int &y);
};
+3 -1
View File
@@ -5,6 +5,7 @@
#include <utility>
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
@@ -16,6 +17,7 @@ namespace api {
controller->init_state(this->state);
this->thread = new std::thread([this] () {
log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud);
timeutils::PreciseSleepTimer timer;
// read buffer
uint8_t read_buffer[16*1024];
@@ -160,7 +162,7 @@ namespace api {
// slow down on reconnect
if (this->running) {
Sleep(retry_time);
timer.sleep(retry_time);
}
}
});
+7
View File
@@ -5,6 +5,7 @@
#include "util/utils.h"
#include "util/rc4.h"
#include "util/logging.h"
#include "overlay/notifications.h"
#include "controller.h"
using namespace headsocket;
@@ -91,12 +92,18 @@ namespace api {
// log connection
log_info("api::websocket", "client connected");
overlay::notifications::add(
overlay::notifications::Severity::Success,
"API websocket client connected");
}
void WebSocketClient::on_disconnect() {
// log disconnection
log_info("api::websocket", "client disconnected");
overlay::notifications::add(
overlay::notifications::Severity::Info,
"API websocket client disconnected");
// get pointer to server
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
+28 -5
View File
@@ -7,6 +7,7 @@
#include <stdint.h>
#include "external/robin_hood.h"
#include "games/popn/popn.h"
#include "launcher/logger.h"
#include "launcher/signal.h"
#include "util/deferlog.h"
@@ -1108,6 +1109,8 @@ namespace avs {
// SOUND VOLTEX
{"soundvoltex.dll", 0x10000000},
// GITADORA
{"gdxg.dll", 0x2000000},
#endif
// jubeat
{"jubeat.dll", 0x2000000},
@@ -1177,6 +1180,15 @@ namespace avs {
return;
}
// hack, both hello! popn and popn hc using popn.dll
if (games::popn::is_pikapika_model()) {
auto old_size = HEAP_SIZE;
HEAP_SIZE = 0x20000000;
DEFAULT_HEAP_SIZE_SET = true;
log_info("avs-core", "updated heap size: {} -> {}", old_size, avs::core::HEAP_SIZE);
return;
}
if (HEAP_SIZE_DEFAULTS.find(dll_name) != HEAP_SIZE_DEFAULTS.end()) {
auto old_size = HEAP_SIZE;
@@ -1232,6 +1244,7 @@ namespace avs {
break;
case 'F':
new_style = logger::Style::RED;
deferredlogs::report_fatal_message();
break;
default:
position = 0;
@@ -1300,6 +1313,7 @@ namespace avs {
break;
case 'F':
style = logger::Style::RED;
deferredlogs::report_fatal_message();
break;
case 'W':
style = logger::Style::YELLOW;
@@ -1502,7 +1516,7 @@ namespace avs {
" * 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"
"\n"
, DLL_NAME, MODULE_PATH.string()) };
, DLL_NAME, MODULE_PATH) };
log_warning("avs-ea3", "{}", info_str);
log_fatal("avs-ea3", "Failed to find critical avs DLL on disk (avs2-core.dll OR {})", DLL_NAME);
}
@@ -1743,8 +1757,8 @@ namespace avs {
{
deferredlogs::defer_error_messages({
"AVS filesystem initialization failure was previously detected during boot!",
fmt::format(" ERROR: directory could not be created: {}", src_path.string().c_str()),
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(" 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),
" fix the XML file and try again"
});
}
@@ -1766,14 +1780,14 @@ namespace avs {
auto created = std::filesystem::create_directories(real_path, err);
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) {
avs_dir_err(real_path);
log_warning("avs-core", "failed to create '{}' folder at '{}': {}",
avs_path,
real_path.string(),
real_path,
err.message());
}
}
@@ -1907,6 +1921,15 @@ namespace avs {
// create nvram and raw directories if possible for mounttable configurations
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(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
if (!LOG_LEVEL_CUSTOM.empty()) {
property_search_remove_safe(config, config_node, "/log/level");
+59 -8
View File
@@ -5,8 +5,9 @@
#include "build/defs.h"
#include "easrv/easrv.h"
#include "easrv/smartea.h"
#include "games/mfc/mfc.h"
#include "games/gitadora/gitadora.h"
#include "hooks/avshook.h"
#include "hooks/graphics/graphics.h"
#include "util/detour.h"
#include "util/deferlog.h"
#include "util/fileutils.h"
@@ -150,7 +151,7 @@ namespace avs {
" * 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"
"\n"
, DLL_NAME, MODULE_PATH.string()) };
, DLL_NAME, MODULE_PATH) };
log_warning("avs-ea3", "{}", info_str);
log_fatal("avs-ea3", "Failed to find critical ea3 DLL on disk (avs2-ea3.dll OR {})", DLL_NAME);
@@ -216,7 +217,25 @@ namespace avs {
ea3_config_name = "prop/eamuse-config.xml";
}
log_info("avs-ea3", "booting (using {})", ea3_config_name);
if (fileutils::file_exists(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 {
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-cfg.xml");
if (avs::game::DLL_NAME == "beatstream1.dll") {
log_warning("avs-ea3", " * prop/ea3-config-1.xml");
}
if (avs::game::DLL_NAME == "beatstream2.dll") {
log_warning("avs-ea3", " * prop/ea3-config-2.xml");
}
log_warning("avs-ea3", " * prop/eamuse-config.xml");
log_warning("avs-ea3", "none of the files above were found, game will fail to boot");
log_warning("avs-ea3", "usually, this means your game data is incomplete");
log_fatal("avs-ea3", "no ea3 config file found in prop directory; check log messages");
}
// remember new config path
CFG_PATH = ea3_config_name;
@@ -371,8 +390,8 @@ namespace avs {
// fall back to default PCBID if node is not found
if (!EA3_PCBID[0] && PCBID_CUSTOM.empty()) {
log_warning("avs-ea3", "no PCBID set, falling back to default PCBID value (04040000000000000000)");
PCBID_CUSTOM = "04040000000000000000";
log_warning("avs-ea3", "no PCBID set, falling back to default PCBID value (01201000000000010101)");
PCBID_CUSTOM = "01201000000000010101";
}
// custom PCBID
@@ -494,7 +513,9 @@ namespace avs {
std::ostringstream init_code;
init_code << EA3_MODEL;
init_code << EA3_DEST;
if (strcmp(EA3_MODEL, "MDX") == 0 && strcmp(EA3_SPEC, "I") == 0) {
if (avs::game::DLL_NAME == "arkmdxbio2.dll" &&
strcmp(EA3_MODEL, "MDX") == 0 && strcmp(EA3_SPEC, "I") == 0) {
// ddr gold cab
init_code << "G";
init_code << "B";
} else {
@@ -526,12 +547,42 @@ namespace avs {
}
}
// update -WINDOW in app-config
if (games::gitadora::is_arena_model()) {
avs::core::property_search_remove_safe(app_config, nullptr, "/param/cmdline");
std::string cmdline{};
if (games::gitadora::is_drum()) {
if (GRAPHICS_WINDOWED) {
cmdline = "-d -DM -WINDOW";
} else {
cmdline = "-d -DM";
}
} else {
if (GRAPHICS_WINDOWED) {
cmdline = "-g -GF -WINDOW";
} else {
cmdline = "-g -GF";
}
}
log_info("avs-ea3", "using cmdline: `{}`", cmdline);
avs::core::property_node_create(app_config, nullptr,
avs::core::NODE_TYPE_str, "/param/cmdline", cmdline.c_str());
}
auto app_param = avs::core::property_search_safe(app_config, nullptr, "/param");
// call the game init
log_info("avs-ea3", "calling entry init");
log_info("avs-ea3", "calling dll_entry_init (init code = {})", init_code_str);
if (!avs::game::entry_init(init_code_str.data(), app_param)) {
log_fatal("avs-ea3", "entry init failed :(");
for (size_t i = 0; i < 255; i++) {
log_warning("avs-ea3", "dll_entry_init failed :(");
}
deferredlogs::defer_error_messages({
"dll_entry_init failed",
" init code: " + init_code_str,
" this is a fatal error that causes boot failure",
" most likely, your prop files have errors"
});
}
// accommodate changes to soft id code
+4 -5
View File
@@ -73,11 +73,10 @@ namespace avs {
// load game instance
const auto dll_path = MODULE_PATH / DLL_NAME;
const auto dll_path_s = dll_path.string();
log_info("avs-game", "DLL path: {}", dll_path_s.c_str());
log_info("avs-game", "DLL path: {}", dll_path);
// 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(
"avs-game",
"PATH TOO LONG WARNING\n\n"
@@ -93,7 +92,7 @@ namespace avs {
"long, often resulting in random crashes. Move the game contents to\n"
"a directory with shorter path.\n"
"-------------------------------------------------------------------\n\n",
dll_path_s, dll_path_s.length());
dll_path, dll_path_len);
}
// ddr gamemdx.dll user error
@@ -112,7 +111,7 @@ namespace avs {
// file not found on disk
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");
}
+327
View File
@@ -0,0 +1,327 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<!-- PHOENIXWAN controller preset for Beatmania IIDX
Button indices 0-6: keys 1-7
Button indices 8-11: E1 (Start for Deluxe Model Style), E2 (Effect), E3 (VEFX), E4 (P1 Start for Lightning Model Style)
P1 Side P2 Side
8 9 10 11 8 9 10 11
2 4 6 2 4 6
TT 1 3 5 7 1 3 5 7 TT -->
<template name="DJ DAO PHOENIXWAN" game="Beatmania IIDX">
<buttons>
<button name="P1 1" vkey="0" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 2" vkey="1" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 3" vkey="2" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 4" vkey="3" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 5" vkey="4" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 6" vkey="5" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 7" vkey="6" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P2 1" vkey="0" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P2 2" vkey="1" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P2 3" vkey="2" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P2 4" vkey="3" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P2 5" vkey="4" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P2 6" vkey="5" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P2 7" vkey="6" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<!-- P1 Start: E1 primary (index 8), E4 alternative (index 11) -->
<button name="P1 Start" vkey="8" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 Start" vkey="11" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<!-- P2 Start: E1 primary (index 8), E4 alternative (index 11) -->
<button name="P2 Start" vkey="8" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P2 Start" vkey="11" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<!-- EFFECT: E2 (index 9), primary P2 side, alternative P1 side -->
<button name="EFFECT" vkey="9" analogtype="0" devid="Player 2" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="EFFECT" vkey="9" analogtype="0" devid="Player 1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<!-- VEFX: E3 (index 10), primary P2 side, alternative P1 side -->
<button name="VEFX" vkey="10" analogtype="0" devid="Player 2" 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>
<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_ms="0"/>
<analog name="Turntable P2" devid="Player 2" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs>
<lights>
<light name="P1 1" devid="Player 1" index="0"/>
<light name="P1 2" devid="Player 1" index="1"/>
<light name="P1 3" devid="Player 1" index="2"/>
<light name="P1 4" devid="Player 1" index="3"/>
<light name="P1 5" devid="Player 1" index="4"/>
<light name="P1 6" devid="Player 1" index="5"/>
<light name="P1 7" devid="Player 1" index="6"/>
<light name="P2 1" devid="Player 2" index="0"/>
<light name="P2 2" devid="Player 2" index="1"/>
<light name="P2 3" devid="Player 2" index="2"/>
<light name="P2 4" devid="Player 2" index="3"/>
<light name="P2 5" devid="Player 2" index="4"/>
<light name="P2 6" devid="Player 2" index="5"/>
<light name="P2 7" devid="Player 2" index="6"/>
<!-- Start buttons bind both E1 (index 8) and E4 (index 11), but lights use
Lightning Model style only (one per side) to avoid both lighting up at once -->
<light name="P1 Start" devid="Player 1" index="10"/>
<light name="P2 Start" devid="Player 2" index="7"/>
<light name="VEFX" devid="Player 1" index="8"/>
<light name="VEFX" devid="Player 2" index="8"/>
<light name="Effect" devid="Player 1" index="9"/>
<light name="Effect" devid="Player 2" index="9"/>
</lights>
</template>
<!-- Common "double scratch" BMS playstyle: LSHIFT A S D SPACE L ; ' RSHIFT -->
<!-- Uses Up/Down arrows for turntable navigation -->
<template name="Keyboard (linear, double scratch, QWERTY)" game="Beatmania IIDX">
<buttons>
<button name="P1 1" vkey="65" analogtype="0" devid="" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 2" vkey="83" analogtype="0" devid="" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 3" vkey="68" analogtype="0" devid="" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 4" vkey="32" analogtype="0" devid="" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 5" vkey="76" analogtype="0" devid="" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 6" vkey="186" analogtype="0" devid="" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 7" vkey="222" analogtype="0" devid="" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 TT+" vkey="40" analogtype="0" devid="" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 TT-" vkey="38" analogtype="0" devid="" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 TT+/-" vkey="160" analogtype="0" devid="" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 TT+/- Alternate" vkey="161" analogtype="0" devid="" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 Start" vkey="13" analogtype="0" devid="" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P2 Start" vkey="69" analogtype="0" devid="" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="EFFECT" vkey="81" analogtype="0" devid="" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="VEFX" vkey="87" analogtype="0" devid="" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
</buttons>
<keypad_buttons/>
<analogs/>
<lights/>
</template>
<template name="arcin (arcin-infinitas)" game="Beatmania IIDX">
<buttons>
<button name="P1 1" vkey="0" analogtype="0" devid="arcin (1p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 2" vkey="1" analogtype="0" devid="arcin (1p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 3" vkey="2" analogtype="0" devid="arcin (1p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 4" vkey="3" analogtype="0" devid="arcin (1p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 5" vkey="4" analogtype="0" devid="arcin (1p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 6" vkey="5" analogtype="0" devid="arcin (1p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 7" vkey="6" analogtype="0" devid="arcin (1p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P1 Start" vkey="9" analogtype="0" devid="arcin (1p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P2 1" vkey="0" analogtype="0" devid="arcin (2p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P2 2" vkey="1" analogtype="0" devid="arcin (2p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P2 3" vkey="2" analogtype="0" devid="arcin (2p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P2 4" vkey="3" analogtype="0" devid="arcin (2p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P2 5" vkey="4" analogtype="0" devid="arcin (2p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P2 6" vkey="5" analogtype="0" devid="arcin (2p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P2 7" vkey="6" analogtype="0" devid="arcin (2p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="P2 Start" vkey="9" analogtype="0" devid="arcin (2p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="EFFECT" vkey="11" analogtype="0" devid="arcin (1p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="EFFECT" vkey="11" analogtype="0" devid="arcin (2p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="VEFX" vkey="10" analogtype="0" devid="arcin (1p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="VEFX" vkey="10" analogtype="0" devid="arcin (2p)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
</buttons>
<keypad_buttons/>
<!-- Assumes analog mode. Need to invert direction since this FW is designed for INF. -->
<analogs>
<analog name="Turntable P1" devid="arcin (1p)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="true" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Turntable P2" devid="arcin (2p)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="true" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs>
<lights>
<light name="P1 1" devid="arcin (1p)" index="0"/>
<light name="P1 2" devid="arcin (1p)" index="1"/>
<light name="P1 3" devid="arcin (1p)" index="2"/>
<light name="P1 4" devid="arcin (1p)" index="3"/>
<light name="P1 5" devid="arcin (1p)" index="4"/>
<light name="P1 6" devid="arcin (1p)" index="5"/>
<light name="P1 7" devid="arcin (1p)" index="6"/>
<light name="P2 1" devid="arcin (2p)" index="0"/>
<light name="P2 2" devid="arcin (2p)" index="1"/>
<light name="P2 3" devid="arcin (2p)" index="2"/>
<light name="P2 4" devid="arcin (2p)" index="3"/>
<light name="P2 5" devid="arcin (2p)" index="4"/>
<light name="P2 6" devid="arcin (2p)" index="5"/>
<light name="P2 7" devid="arcin (2p)" index="6"/>
<light name="P1 Start" devid="arcin (1p)" index="9"/>
<light name="P2 Start" devid="arcin (2p)" index="9"/>
</lights>
</template>
<!-- This controller has many modes; here, we use HID Analog mode which is meant for data -->
<template name="DJ DAO Nostroller (Analog mode)" game="Nostalgia">
<buttons/>
<keypad_buttons/>
<analogs>
<analog name="Key 1" devid="Analogs key 01-28" index="4" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 2" devid="Analogs key 01-28" index="5" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 3" devid="Analogs key 01-28" index="6" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 4" devid="Analogs key 01-28" index="7" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 5" devid="Analogs key 01-28" index="8" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 6" devid="Analogs key 01-28" index="9" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 7" devid="Analogs key 01-28" index="10" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 8" devid="Analogs key 01-28" index="11" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 9" devid="Analogs key 01-28" index="12" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 10" devid="Analogs key 01-28" index="13" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 11" devid="Analogs key 01-28" index="14" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 12" devid="Analogs key 01-28" index="15" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 13" devid="Analogs key 01-28" index="16" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 14" devid="Analogs key 01-28" index="17" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 15" devid="Analogs key 01-28" index="18" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 16" devid="Analogs key 01-28" index="19" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 17" devid="Analogs key 01-28" index="20" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 18" devid="Analogs key 01-28" index="21" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 19" devid="Analogs key 01-28" index="22" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 20" devid="Analogs key 01-28" index="23" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 21" devid="Analogs key 01-28" index="24" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 22" devid="Analogs key 01-28" index="25" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 23" devid="Analogs key 01-28" index="26" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 24" devid="Analogs key 01-28" index="27" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 25" devid="Analogs key 01-28" index="28" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 26" devid="Analogs key 01-28" index="29" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 27" devid="Analogs key 01-28" index="30" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Key 28" devid="Analogs key 01-28" index="31" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs>
<lights>
<light name="Key 1 R" devid="Lights 01-14" index="0"/>
<light name="Key 1 G" devid="Lights 01-14" index="1"/>
<light name="Key 1 B" devid="Lights 01-14" index="2"/>
<light name="Key 2 R" devid="Lights 01-14" index="3"/>
<light name="Key 2 G" devid="Lights 01-14" index="4"/>
<light name="Key 2 B" devid="Lights 01-14" index="5"/>
<light name="Key 3 R" devid="Lights 01-14" index="6"/>
<light name="Key 3 G" devid="Lights 01-14" index="7"/>
<light name="Key 3 B" devid="Lights 01-14" index="8"/>
<light name="Key 4 R" devid="Lights 01-14" index="9"/>
<light name="Key 4 G" devid="Lights 01-14" index="10"/>
<light name="Key 4 B" devid="Lights 01-14" index="11"/>
<light name="Key 5 R" devid="Lights 01-14" index="12"/>
<light name="Key 5 G" devid="Lights 01-14" index="13"/>
<light name="Key 5 B" devid="Lights 01-14" index="14"/>
<light name="Key 6 R" devid="Lights 01-14" index="15"/>
<light name="Key 6 G" devid="Lights 01-14" index="16"/>
<light name="Key 6 B" devid="Lights 01-14" index="17"/>
<light name="Key 7 R" devid="Lights 01-14" index="18"/>
<light name="Key 7 G" devid="Lights 01-14" index="19"/>
<light name="Key 7 B" devid="Lights 01-14" index="20"/>
<light name="Key 8 R" devid="Lights 01-14" index="21"/>
<light name="Key 8 G" devid="Lights 01-14" index="22"/>
<light name="Key 8 B" devid="Lights 01-14" index="23"/>
<light name="Key 9 R" devid="Lights 01-14" index="24"/>
<light name="Key 9 G" devid="Lights 01-14" index="25"/>
<light name="Key 9 B" devid="Lights 01-14" index="26"/>
<light name="Key 10 R" devid="Lights 01-14" index="27"/>
<light name="Key 10 G" devid="Lights 01-14" index="28"/>
<light name="Key 10 B" devid="Lights 01-14" index="29"/>
<light name="Key 11 R" devid="Lights 01-14" index="30"/>
<light name="Key 11 G" devid="Lights 01-14" index="31"/>
<light name="Key 11 B" devid="Lights 01-14" index="32"/>
<light name="Key 12 R" devid="Lights 01-14" index="33"/>
<light name="Key 12 G" devid="Lights 01-14" index="34"/>
<light name="Key 12 B" devid="Lights 01-14" index="35"/>
<light name="Key 13 R" devid="Lights 01-14" index="36"/>
<light name="Key 13 G" devid="Lights 01-14" index="37"/>
<light name="Key 13 B" devid="Lights 01-14" index="38"/>
<light name="Key 14 R" devid="Lights 01-14" index="39"/>
<light name="Key 14 G" devid="Lights 01-14" index="40"/>
<light name="Key 14 B" devid="Lights 01-14" index="41"/>
<light name="Key 15 R" devid="Lights 15-28" index="0"/>
<light name="Key 15 G" devid="Lights 15-28" index="1"/>
<light name="Key 15 B" devid="Lights 15-28" index="2"/>
<light name="Key 16 R" devid="Lights 15-28" index="3"/>
<light name="Key 16 G" devid="Lights 15-28" index="4"/>
<light name="Key 16 B" devid="Lights 15-28" index="5"/>
<light name="Key 17 R" devid="Lights 15-28" index="6"/>
<light name="Key 17 G" devid="Lights 15-28" index="7"/>
<light name="Key 17 B" devid="Lights 15-28" index="8"/>
<light name="Key 18 R" devid="Lights 15-28" index="9"/>
<light name="Key 18 G" devid="Lights 15-28" index="10"/>
<light name="Key 18 B" devid="Lights 15-28" index="11"/>
<light name="Key 19 R" devid="Lights 15-28" index="12"/>
<light name="Key 19 G" devid="Lights 15-28" index="13"/>
<light name="Key 19 B" devid="Lights 15-28" index="14"/>
<light name="Key 20 R" devid="Lights 15-28" index="15"/>
<light name="Key 20 G" devid="Lights 15-28" index="16"/>
<light name="Key 20 B" devid="Lights 15-28" index="17"/>
<light name="Key 21 R" devid="Lights 15-28" index="18"/>
<light name="Key 21 G" devid="Lights 15-28" index="19"/>
<light name="Key 21 B" devid="Lights 15-28" index="20"/>
<light name="Key 22 R" devid="Lights 15-28" index="21"/>
<light name="Key 22 G" devid="Lights 15-28" index="22"/>
<light name="Key 22 B" devid="Lights 15-28" index="23"/>
<light name="Key 23 R" devid="Lights 15-28" index="24"/>
<light name="Key 23 G" devid="Lights 15-28" index="25"/>
<light name="Key 23 B" devid="Lights 15-28" index="26"/>
<light name="Key 24 R" devid="Lights 15-28" index="27"/>
<light name="Key 24 G" devid="Lights 15-28" index="28"/>
<light name="Key 24 B" devid="Lights 15-28" index="29"/>
<light name="Key 25 R" devid="Lights 15-28" index="30"/>
<light name="Key 25 G" devid="Lights 15-28" index="31"/>
<light name="Key 25 B" devid="Lights 15-28" index="32"/>
<light name="Key 26 R" devid="Lights 15-28" index="33"/>
<light name="Key 26 G" devid="Lights 15-28" index="34"/>
<light name="Key 26 B" devid="Lights 15-28" index="35"/>
<light name="Key 27 R" devid="Lights 15-28" index="36"/>
<light name="Key 27 G" devid="Lights 15-28" index="37"/>
<light name="Key 27 B" devid="Lights 15-28" index="38"/>
<light name="Key 28 R" devid="Lights 15-28" index="39"/>
<light name="Key 28 G" devid="Lights 15-28" index="40"/>
<light name="Key 28 B" devid="Lights 15-28" index="41"/>
</lights>
</template>
<!-- Gitaller has two modes - digital tilt and analog axis. This binds both of them at the same time. -->
<template name="DJ DAO Gitaller" game="GitaDora">
<buttons>
<button name="Guitar P1 Start" vkey="9" analogtype="0" devid="Gitaller P1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="Guitar P1 Help" vkey="8" analogtype="0" devid="Gitaller P1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="Guitar P1 Pick Up" vkey="5" analogtype="0" devid="Gitaller P1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="Guitar P1 Pick Down" vkey="6" analogtype="0" devid="Gitaller P1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="Guitar P1 R" vkey="0" analogtype="0" devid="Gitaller P1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="Guitar P1 G" vkey="1" analogtype="0" devid="Gitaller P1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="Guitar P1 B" vkey="2" analogtype="0" devid="Gitaller P1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="Guitar P1 Y" vkey="3" analogtype="0" devid="Gitaller P1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="Guitar P1 P" vkey="4" analogtype="0" devid="Gitaller P1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="Guitar P1 Wail Up" vkey="10" analogtype="0" devid="Gitaller P1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="Guitar P1 Wail Down" vkey="11" analogtype="0" devid="Gitaller P1" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
</buttons>
<keypad_buttons/>
<analogs>
<analog name="Guitar P1 Wail X" devid="Gitaller P1" index="1" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Guitar P1 Wail Y" devid="Gitaller P1" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="Guitar P1 Wail Z" devid="Gitaller P1" index="5" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs>
<lights/>
<!-- Gen 1 FAUCETWO in HID mode; should also work for later gens -->
</template>
<template name="DJ DAO FAUCETWO" game="Sound Voltex">
<buttons>
<button name="BT-A" vkey="1" analogtype="0" devid="Buttons (MI_01)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="BT-B" vkey="2" analogtype="0" devid="Buttons (MI_01)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="BT-C" vkey="3" analogtype="0" devid="Buttons (MI_01)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="BT-D" vkey="4" analogtype="0" devid="Buttons (MI_01)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="FX-L" vkey="5" analogtype="0" devid="Buttons (MI_01)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="FX-R" vkey="6" analogtype="0" devid="Buttons (MI_01)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
<button name="Start" vkey="0" analogtype="0" devid="Buttons (MI_01)" invert="false" debounce_up="0" debounce_down="0" velocity_threshold="0"/>
</buttons>
<keypad_buttons/>
<analogs>
<analog name="VOL-L" devid="Buttons (MI_01)" index="1" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
<analog name="VOL-R" devid="Buttons (MI_01)" index="0" sensivity="1" deadzone="0" deadzone_mirror="false" invert="false" smoothing="false" multiplier="1" relative="false" delay_ms="0"/>
</analogs>
<lights>
<light name="BT-A" devid="Lights (MI_02)" index="24"/>
<light name="BT-B" devid="Lights (MI_02)" index="25"/>
<light name="BT-C" devid="Lights (MI_02)" index="26"/>
<light name="BT-D" devid="Lights (MI_02)" index="27"/>
<light name="FX-L" devid="Lights (MI_02)" index="28"/>
<light name="FX-R" devid="Lights (MI_02)" index="29"/>
<light name="Start" devid="Lights (MI_02)" index="30"/>
<light name="Upper Left Speaker Avg R" devid="Lights (MI_02)" index="0"/>
<light name="Upper Left Speaker Avg G" devid="Lights (MI_02)" index="1"/>
<light name="Upper Left Speaker Avg B" devid="Lights (MI_02)" index="2"/>
<light name="Upper Right Speaker Avg R" devid="Lights (MI_02)" index="9"/>
<light name="Upper Right Speaker Avg G" devid="Lights (MI_02)" index="10"/>
<light name="Upper Right Speaker Avg B" devid="Lights (MI_02)" index="11"/>
<light name="Lower Left Speaker Avg R" devid="Lights (MI_02)" index="3"/>
<light name="Lower Left Speaker Avg G" devid="Lights (MI_02)" index="4"/>
<light name="Lower Left Speaker Avg B" devid="Lights (MI_02)" index="5"/>
<light name="Lower Right Speaker Avg R" devid="Lights (MI_02)" index="6"/>
<light name="Lower Right Speaker Avg G" devid="Lights (MI_02)" index="7"/>
<light name="Lower Right Speaker Avg B" devid="Lights (MI_02)" index="8"/>
<light name="Control Panel Avg R" devid="Lights (MI_02)" index="15"/>
<light name="Control Panel Avg G" devid="Lights (MI_02)" index="16"/>
<light name="Control Panel Avg B" devid="Lights (MI_02)" index="17"/>
</lights>
</template>
</templates>
+1 -1
View File
@@ -23,7 +23,7 @@
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
File diff suppressed because one or more lines are too long
+152 -25
View File
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# set to EN-US for consistency
LANG=en_us_8859_1
@@ -41,12 +41,18 @@ done
# settings
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2> /dev/null || echo "none")
GIT_HEAD=$(git rev-parse HEAD || echo "none")
TOOLCHAIN_32="/usr/share/mingw/toolchain-i686-w64-mingw32.cmake"
TOOLCHAIN_64="/usr/share/mingw/toolchain-x86_64-w64-mingw32.cmake"
TOOLCHAIN_32="${TOOLCHAIN_32:-"/usr/share/mingw/toolchain-i686-w64-mingw32.cmake"}"
TOOLCHAIN_64="${TOOLCHAIN_64:-"/usr/share/mingw/toolchain-x86_64-w64-mingw32.cmake"}"
TOOLCHAIN_WINXP_32="${TOOLCHAIN_WINXP_32:-"/opt/llvm-mingw-xp/toolchain-files/cmake/i686-mingw32-clang.cmake"}"
TOOLCHAIN_WINXP_64="${TOOLCHAIN_WINXP_64:-"/opt/llvm-mingw-xp/toolchain-files/cmake/x86_64-mingw32-clang.cmake"}"
BUILDDIR_32_RELEASE="./cmake-build-release-32"
BUILDDIR_32_DEBUG="./cmake-build-debug-32"
BUILDDIR_64_RELEASE="./cmake-build-release-64"
BUILDDIR_64_DEBUG="./cmake-build-debug-64"
BUILDDIR_WINXP_32_RELEASE="./cmake-build-release-winxp-32"
BUILDDIR_WINXP_32_DEBUG="./cmake-build-debug-winxp-32"
BUILDDIR_WINXP_64_RELEASE="./cmake-build-release-winxp-64"
BUILDDIR_WINXP_64_DEBUG="./cmake-build-debug-winxp-64"
DEBUG=0
OUTDIR="./bin/spice2x"
OUTDIR_EXTRAS="./bin/spice2x/extras"
@@ -62,19 +68,42 @@ DIST_FOLDER="./dist"
DIST_NAME="spice2x-$(date +%y)-$(date +%m)-$(date +%d).zip"
DIST_NAME_EXTRAS="spice2x-$(date +%y)-$(date +%m)-$(date +%d)-full.zip"
DIST_COMMENT=${DIST_NAME}$'\n'"$GIT_BRANCH - $GIT_HEAD"$'\nThank you for playing.'
TARGETS_32="spicetools_stubs_kbt spicetools_stubs_kld spicetools_cfg spicetools_cfg_linux spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_cpusbxpkm"
TARGETS_64="spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvEncodeAPI64 spicetools_stubs_nvcuvid spicetools_stubs_nvcuda spicetools_spice64 spicetools_spice64_linux"
TARGETS_32="spicetools_stubs_kbt spicetools_stubs_kld spicetools_cfg spicetools_cfg_linux spicetools_spice spicetools_spice_laa spicetools_spice_linux spicetools_stubs_cpusbxpkm spicetools_sdk_sample_v0_flat_c_32"
TARGETS_64="spicetools_stubs_kbt64 spicetools_stubs_kld64 spicetools_stubs_nvEncodeAPI64 spicetools_stubs_nvcuvid spicetools_stubs_nvcuda spicetools_spice64 spicetools_spice64_linux spicetools_sdk_sample_v0_flat_c_64 spicetools_sdk_sample_v0_cpp_64"
TARGETS_XP32="spicetools_cfg spicetools_spice"
TARGETS_XP64="spicetools_spice64"
# determine build type
BUILD_TYPE="Release"
BUILDDIR_32=${BUILDDIR_32_RELEASE}
BUILDDIR_64=${BUILDDIR_64_RELEASE}
BUILDDIR_WINXP_32=${BUILDDIR_WINXP_32_RELEASE}
BUILDDIR_WINXP_64=${BUILDDIR_WINXP_64_RELEASE}
if ((DEBUG > 0))
then
BUILD_TYPE="Debug"
BUILDDIR_32=${BUILDDIR_32_DEBUG}
BUILDDIR_64=${BUILDDIR_64_DEBUG}
DIST_NAME=$(echo ${DIST_NAME} | sed 's/\.[^.]*$/-dbg&/')
BUILDDIR_WINXP_32=${BUILDDIR_WINXP_32_DEBUG}
BUILDDIR_WINXP_64=${BUILDDIR_WINXP_64_DEBUG}
DIST_NAME=$(echo "${DIST_NAME}" | sed 's/\.[^.]*$/-dbg&/')
fi
# is the XP-compatible toolchain installed?
XP_MUST_BUILD=0
# 64-bit WinXP builds are disabled by default; set to 1 to opt in
BUILD_XP_64_ENABLE=0
BUILD_XP_32=0
BUILD_XP_64=0
if [ -f "$TOOLCHAIN_WINXP_32" ]; then
BUILD_XP_32=1;
elif ((XP_MUST_BUILD > 0))
then
echo "WinXP 32bit toolchain not available, aborting"
exit 1
fi
if ((BUILD_XP_64_ENABLE > 0)) && [ -f "$TOOLCHAIN_WINXP_64" ]; then
BUILD_XP_64=1;
fi
# determine number of cores
@@ -90,6 +119,18 @@ echo "Git Branch: $GIT_BRANCH"
echo "Git Head: $GIT_HEAD"
echo "Toolchain for 32bit targets: $TOOLCHAIN_32"
echo "Toolchain for 64bit targets: $TOOLCHAIN_64"
if ((BUILD_XP_32 > 0))
then
echo "Toolchain for WinXP 32bit targets: $TOOLCHAIN_WINXP_32"
else
echo "WinXP 32bit toolchain not available, skipping WinXP 32bit builds"
fi
if ((BUILD_XP_64 > 0))
then
echo "Toolchain for WinXP 64bit targets: $TOOLCHAIN_WINXP_64"
else
echo "WinXP 64bit builds disabled, skipping WinXP 64bit builds"
fi
echo "Distribution Name: $DIST_NAME"
echo "Build Type: $BUILD_TYPE"
echo "Cores: $CORES"
@@ -114,7 +155,7 @@ time (
fi
mkdir -p ${BUILDDIR_32}
pushd ${BUILDDIR_32} > /dev/null
cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_32} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} $OLDPWD && ninja ${TARGETS_32}
cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_32} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} "$OLDPWD" && ninja ${TARGETS_32}
popd > /dev/null
# 64 bit
@@ -127,14 +168,87 @@ time (
fi
mkdir -p ${BUILDDIR_64}
pushd ${BUILDDIR_64} > /dev/null
cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_64} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} $OLDPWD && ninja ${TARGETS_64}
cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_64} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} "$OLDPWD" && ninja ${TARGETS_64}
popd > /dev/null
if ((BUILD_XP_32 > 0))
then
# 32 bit Windows XP
echo ""
echo "Building 32bit targets (WinXP toolchain)..."
echo "========================="
if ((CLEAN_BUILD > 0))
then
rm -rf ${BUILDDIR_WINXP_32}
fi
mkdir -p ${BUILDDIR_WINXP_32}
pushd ${BUILDDIR_WINXP_32} > /dev/null
cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_32} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DSPICE_XP=ON "$OLDPWD" && ninja ${TARGETS_XP32}
popd > /dev/null
else
echo ""
echo "Skipping WinXP 32bit builds, toolchain not specified"
fi
if ((BUILD_XP_64 > 0))
then
# 64 bit Windows XP
echo ""
echo "Building 64bit targets (WinXP toolchain)..."
echo "========================="
if ((CLEAN_BUILD > 0))
then
rm -rf ${BUILDDIR_WINXP_64}
fi
mkdir -p ${BUILDDIR_WINXP_64}
pushd ${BUILDDIR_WINXP_64} > /dev/null
cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_WINXP_64} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DSPICE_XP=ON "$OLDPWD" && ninja ${TARGETS_XP64}
popd > /dev/null
else
echo ""
echo "Skipping WinXP 64bit builds"
fi
echo ""
echo "Compilation process done :)"
echo "==========================="
)
echo ""
echo "Checking Win7 compatibility..."
echo "============================="
if ! command -v windows_dll_compat_checker &> /dev/null; then
echo "WARNING: windows_dll_compat_checker not found, skipping Win7 compatibility check"
else
windows_dll_compat_checker -s PREMADE/konami_win7_museca_x86_64.ini \
${BUILDDIR_64}/spicetools/64/spice64.exe
windows_dll_compat_checker -s PREMADE/konami_win7_museca_x86_64_32bit_dlls.ini \
${BUILDDIR_32}/spicetools/spicecfg.exe \
${BUILDDIR_32}/spicetools/32/spice.exe
fi
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
then
echo ""
echo "Checking XP compatibility..."
echo "============================="
if ! command -v windows_dll_compat_checker &> /dev/null; then
echo "WARNING: windows_dll_compat_checker not found, skipping XP compatibility check"
else
if ((BUILD_XP_64 > 0))
then
windows_dll_compat_checker -s PREMADE/winxp_x86_64.ini \
${BUILDDIR_WINXP_64}/spicetools/64/spice64.exe
fi
if ((BUILD_XP_32 > 0))
then
windows_dll_compat_checker -s PREMADE/konami_winxp_jubeat_i686.ini \
${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe \
${BUILDDIR_WINXP_32}/spicetools/32/spice.exe
fi
fi
fi
# generate PDBs
if false # ((DEBUG > 0))
then
@@ -191,6 +305,13 @@ rm -rf ${OUTDIR_EXTRAS}
mkdir -p ${OUTDIR_EXTRAS}
mkdir -p ${OUTDIR_EXTRAS}/largeaddressaware
mkdir -p ${OUTDIR_EXTRAS}/linux
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/32
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/64
mkdir -p ${OUTDIR_EXTRAS}/updater
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
then
mkdir -p ${OUTDIR_EXTRAS}/winxp
fi
if false # ((DEBUG > 0))
then
@@ -199,16 +320,8 @@ then
cp ${BUILDDIR_32}/spicetools/spicecfg-pdb.pdb ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/spice-pdb.exe ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/spice-pdb.pdb ${OUTDIR} 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/kbt.dll ${OUTDIR}/stubs/32 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/kld.dll ${OUTDIR}/stubs/32 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/spice64-pdb.exe ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/spice64-pdb.pdb ${OUTDIR} 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/kbt.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/kld.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/nvEncodeAPI64.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/nvcuda.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/nvcuvid.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/cpusbxpkm.dll ${OUTDIR}/stubs/32 2>/dev/null
else
# release files
cp ${BUILDDIR_32}/spicetools/spicecfg.exe ${OUTDIR} 2>/dev/null
@@ -216,16 +329,24 @@ else
cp ${BUILDDIR_32}/spicetools/32/spice.exe ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/spice_laa.exe ${OUTDIR_EXTRAS}/largeaddressaware/spice.exe 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/spice_linux.exe ${OUTDIR_EXTRAS}/linux/spice.exe 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/kbt.dll ${OUTDIR}/stubs/32 2>/dev/null
#cp ${BUILDDIR_32}/spicetools/32/kld.dll ${OUTDIR}/stubs/32 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/spice64.exe ${OUTDIR} 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/spice64_linux.exe ${OUTDIR_EXTRAS}/linux/spice64.exe 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/kbt.dll ${OUTDIR}/stubs/64 2>/dev/null
#cp ${BUILDDIR_64}/spicetools/64/kld.dll ${OUTDIR}/stubs/64 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/nvEncodeAPI64.dll ${OUTDIR}/stubs/64 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/nvcuda.dll ${OUTDIR}/stubs/64 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/nvcuvid.dll ${OUTDIR}/stubs/64 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/cpusbxpkm.dll ${OUTDIR}/stubs/32 2>/dev/null
cp ${BUILDDIR_32}/spicetools/32/sdk_sample_v0_flat_c.dll ${OUTDIR_EXTRAS}/sdk/samples/32/v0_flat_c.dll 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/sdk_sample_v0_flat_c.dll ${OUTDIR_EXTRAS}/sdk/samples/64/v0_flat_c.dll 2>/dev/null
cp ${BUILDDIR_64}/spicetools/64/sdk_sample_v0_cpp.dll ${OUTDIR_EXTRAS}/sdk/samples/64/v0_cpp.dll 2>/dev/null
if ((BUILD_XP_32 > 0))
then
cp ${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe ${OUTDIR_EXTRAS}/winxp 2>/dev/null
cp ${BUILDDIR_WINXP_32}/spicetools/32/spice.exe ${OUTDIR_EXTRAS}/winxp 2>/dev/null
fi
if ((BUILD_XP_64 > 0))
then
cp ${BUILDDIR_WINXP_64}/spicetools/64/spice64.exe ${OUTDIR_EXTRAS}/winxp 2>/dev/null
fi
fi
# pack source files to output directory
@@ -234,26 +355,32 @@ mkdir -p ${OUTDIR}/src
if ((INCLUDE_SRC > 0))
then
echo "Generating source file archive..."
git archive --format tar.gz --prefix=spice2x/ HEAD ./ > ${OUTDIR}/src/spice2x-${GIT_BRANCH}.tar.gz 2>/dev/null || \
git archive --format tar.gz --prefix=spice2x/ HEAD ./ > "${OUTDIR}/src/spice2x-${GIT_BRANCH}.tar.gz" 2>/dev/null || \
echo "WARNING: Couldn't get git to create the archive. Is this a git repository?"
fi
# sdk headers
cp -r ./sdk/include ${OUTDIR_EXTRAS}/sdk
# copy resources
rm -rf ${OUTDIR_EXTRAS}/api
mkdir ${OUTDIR_EXTRAS}/api
find ./api/resources/python -name "__pycache__" -exec rm -rf {} +
cp -r ./api/resources/* ${OUTDIR_EXTRAS}/api
# generate the standalone updater scripts from the shared template and drop them
bash ./build_updaters.sh ${OUTDIR_EXTRAS}/updater
# build distribution archive
if ((DIST_ENABLE > 0))
then
echo "Building dist..."
mkdir -p ${DIST_FOLDER}
rm -rf ${DIST_FOLDER}/${DIST_NAME}
pushd ${OUTDIR}/.. > /dev/null
zip -qrXT9 $OLDPWD/${DIST_FOLDER}/${DIST_NAME} spice2x -x "spice2x/extras/*" -z <<< "$DIST_COMMENT"
rm -rf "${DIST_FOLDER}/${DIST_NAME}"
pushd ${OUTDIR} > /dev/null
zip -qrXT9 "$OLDPWD/${DIST_FOLDER}/${DIST_NAME}" . -x "extras/*" -z <<< "$DIST_COMMENT"
echo "Building extras..."
zip -qrXT9 $OLDPWD/${DIST_FOLDER}/${DIST_NAME_EXTRAS} spice2x -z <<< "$DIST_COMMENT"
zip -qrXT9 "$OLDPWD/${DIST_FOLDER}/${DIST_NAME_EXTRAS}" . -z <<< "$DIST_COMMENT"
popd > /dev/null
fi
+1 -1
View File
@@ -1,6 +1,6 @@
docker build --pull external/docker -t spicetools/deps
docker build --build-context gitroot=%cd%/../../.git . -t spicetools/spice
docker run --rm -it -v %cd%/dist:/src/src/spice2x/dist -v %cd%/bin:/src/src/spice2x/bin -v %cd%/.ccache:/src/src/spice2x/.ccache spicetools/spice %*
docker run --rm -i -v %cd%/dist:/src/src/spice2x/dist -v %cd%/bin:/src/src/spice2x/bin -v %cd%/.ccache:/src/src/spice2x/.ccache spicetools/spice %*
@REM to generate PDBs, set DEBUG to 1 in build_all.sh, place cv2pdb in external\cv2pdb, and run below
@REM external\cv2pdb\cv2pdb.exe bin\spice2x\spicecfg.exe bin\spice2x\spicecfg-pdb.exe bin\spice2x\spicecfg-pdb.pdb
@REM external\cv2pdb\cv2pdb.exe bin\spice2x\spice.exe bin\spice2x\spice-pdb.exe bin\spice2x\spice-pdb.pdb
+12 -4
View File
@@ -1,4 +1,12 @@
#!/bin/bash
docker build --pull $PWD/external/docker -t spicetools/deps --platform linux/x86_64
docker build --build-context gitroot=$PWD/../../.git . -t spicetools/spice:latest
docker run --rm -v $PWD/dist:/src/src/spice2x/dist -v $PWD/bin:/src/src/spice2x/bin -v $PWD/.ccache:/src/src/spice2x/.ccache spicetools/spice "$@"
#!/usr/bin/env bash
set -eu
docker build --pull "$PWD/external/docker" -t spicetools/deps --platform linux/x86_64
docker build --build-context gitroot="$PWD/../../.git" . -t spicetools/spice:latest
# Interactive TTY if available, so docker build can be Ctrl+C'd
DOCKER_FLAGS=""
[ -t 0 ] && DOCKER_FLAGS="-it"
docker run $DOCKER_FLAGS --rm -v "$PWD/dist:/src/src/spice2x/dist" -v "$PWD/bin:/src/src/spice2x/bin" -v "$PWD/.ccache:/src/src/spice2x/.ccache" spicetools/spice "$@"
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# builds the standalone updater scripts from the shared PowerShell logic in
# update_spice.ps1. each output is a self-contained polyglot .bat: a small batch
# header (with the release channel baked in) followed by the shared PowerShell
# body after the marker line.
#
# usage: build_updaters.sh OUT_DIR
# OUT_DIR directory to write the generated .bat files into
set -eu
if [ $# -lt 1 ]; then
echo "usage: build_updaters.sh OUT_DIR" >&2
exit 1
fi
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PS_BODY="${SRC_DIR}/update_spice.ps1"
OUT_DIR="$1"
[ -f "$PS_BODY" ] || { echo "error: $PS_BODY not found" >&2; exit 1; }
MARKER='#___PS___'
gen() {
channel="$1"; label="$2"; out="$3"
{
cat <<HEADER
@echo off
setlocal
REM ============================================================================
REM spice2x updater - updates spice.exe, spice64.exe and spicecfg.exe in
REM this folder to the latest spice2x release from GitHub.
REM
REM release channel: ${label}
REM make sure spice/spicecfg are NOT running before updating.
REM ============================================================================
REM expose the script folder + channel to the embedded PowerShell, then run the
REM PowerShell part: re-read this file and execute everything after the marker.
set "SPICE_DIR=%~dp0"
set "SPICE_CHANNEL=${channel}"
powershell -NoProfile -ExecutionPolicy Bypass -Command "\$m='#___'+'PS'+'___'; \$code=((Get-Content -LiteralPath '%~f0' -Raw) -split \$m)[-1]; Invoke-Expression \$code"
set "RC=%ERRORLEVEL%"
endlocal & exit /b %RC%
${MARKER}
HEADER
cat "$PS_BODY"
} | sed -e 's/\r$//' -e 's/$/\r/' > "${OUT_DIR}/${out}"
echo " generated ${OUT_DIR}/${out}"
}
echo "Generating updater scripts from $(basename "$PS_BODY")..."
gen "" "stable (pre-releases excluded)" "update_spice.bat"
gen "beta" "beta (newest, pre-releases included)" "update_spice_beta.bat"
+1
View File
@@ -13,5 +13,6 @@ IDR_LICENSES RCDATA "../licenses.txt"
IDR_PATCHES RCDATA "../build/patches.json"
IDR_README RCDATA "../readme.txt"
IDR_DSEGFONT RCDATA "../external/dseg/DSEG14Classic-Italic.ttf"
IDR_CONTROLLER_PRESETS_BUILTIN RCDATA "../build/controller_presets.xml"
#endif
+105 -14
View File
@@ -90,6 +90,10 @@ std::string Analog::getDisplayString(rawinput::RawInputManager *manager) {
return "MIDI Unknown Index " + indexString + " (" + device->desc + ")";
}
}
case rawinput::XINPUT_GAMEPAD:
return fmt::format("{} ({})",
xinput::get_analog_string(static_cast<xinput::XInputAnalogEnum>(index)),
device->desc);
case rawinput::DESTROYED:
return "Device unplugged (" + indexString + ")";
default:
@@ -203,22 +207,38 @@ float Analog::applyMultiplier(float value) {
}
float Analog::normalizeAnalogValue(float value) {
// effectively the same as fmodf(value, 1.f)
// for small values, this is MUCH faster than fmodf.
float new_value = value;
while (new_value > 1.f) {
new_value -= 1.f;
if (getType() == GameAPI::Analogs::AnalogType::Circular) {
// effectively the same as fmodf(value, 1.f)
// for small values, this is MUCH faster than fmodf.
float new_value = value;
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 value = raw_value;
const auto deadzone = this->getDeadzone();
if (deadzone > 0) {
auto deadzone = this->getDeadzone();
// 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
const auto delta = value - 0.5f;
@@ -251,7 +271,7 @@ float Analog::applyDeadzone(float raw_value) {
}
}
} else if (deadzone < 0) {
} else {
// invert for mirror
if (this->getDeadzoneMirror()) {
@@ -259,8 +279,8 @@ float Analog::applyDeadzone(float raw_value) {
}
// deadzone from minimum value
if (deadzone > -1 && value > -deadzone) {
value = std::min(1.f, (value + deadzone) / (1.f + deadzone));
if (deadzone < 1.f && deadzone < value) {
value = std::max(0.f, (value - deadzone) / (1.f - deadzone));
} else {
value = 0.f;
}
@@ -272,3 +292,74 @@ float Analog::applyDeadzone(float raw_value) {
}
return value;
}
float Analog::getRelativeModeValue(float raw_value) {
const auto now = get_performance_seconds();
auto delta_time = now - this->rel_mode_last_read_time_s;
if (this->rel_mode_last_read_time_s != 0.f) {
// some heuristics to prevent huge jumps:
// * if we went for more than 250ms without polls, discard it (e.g., during loading screens)
// * cap the delta at 100ms to prevent huge jumps in case of very infrequent polling
if (delta_time < 0.f || 0.25f < delta_time) {
delta_time = 0.f;
} else if (delta_time > 0.1f) {
delta_time = 0.1f;
}
// scale [0, 1] to [-1, 1] to simplify calculations
const auto delta_raw_value = (raw_value - 0.5f) * 2.f;
// target is one revolution per second at max speed at 1.0 sensitivity
auto adjusted_delta_value = delta_raw_value * delta_time;
// multiplier / divisor
if (this->getMultiplier() > 1) {
adjusted_delta_value *= this->getMultiplier();
} else if (this->getMultiplier() < -1) {
adjusted_delta_value /= -this->getMultiplier();
}
// sensitivity
if (this->isSensitivitySet()) {
adjusted_delta_value *= this->getSensitivity();
}
// calculate the new absolute value
this->rel_mode_absolute_value += adjusted_delta_value;
}
// update for next poll
this->rel_mode_last_read_time_s = now;
this->rel_mode_absolute_value = normalizeAnalogValue(this->rel_mode_absolute_value);
return this->rel_mode_absolute_value;
}
float Analog::getDelayedValue(float raw_value) {
const double delay_ms = static_cast<double>(this->getDelayMs());
if (delay_ms == 0.0) {
return raw_value;
}
// always push a new value
const auto now = get_performance_milliseconds();
this->delayed_inputs.emplace(now, raw_value);
// drain the queue down to reasonable length to prevent unconstrained growth
// this would accommodate 1 second at ~1000Hz which is overkill
// (UI only allows for 250ms of delay)
while (this->delayed_inputs.size() > 1024) {
this->delayed_inputs.pop();
}
// pop until we find the oldest value still inside the delay window
while (this->delayed_inputs.size() > 1) {
const auto delta_t = now - this->delayed_inputs.front().time_in_ms;
if (delta_t <= delay_ms) {
break;
}
this->delayed_inputs.pop();
}
return this->delayed_inputs.front().value;
}
+47 -18
View File
@@ -1,5 +1,6 @@
#pragma once
#include <cstdint>
#include <array>
#include <string>
#include <cmath>
@@ -13,12 +14,32 @@ namespace rawinput {
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 {
double time_in_ms;
float sine;
float cosine;
};
struct AnalogDelayEntry {
double time_in_ms;
float value;
};
class Analog {
private:
std::string name;
@@ -31,10 +52,11 @@ private:
float last_state = 0.5f;
bool sensitivity_set = false;
bool deadzone_set = false;
GameAPI::Analogs::AnalogType type = GameAPI::Analogs::AnalogType::Circular;
// smoothing function
bool smoothing = false;
std::array<AnalogMovingAverage, ANALOG_HISTORY_CNT> vector_history;
std::array<AnalogMovingAverage, ANALOG_HISTORY_CNT> vector_history = {};
int vector_history_index = 0;
float smoothed_last_state = 0.f;
@@ -48,12 +70,13 @@ private:
unsigned short divisor_region = 0;
// 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;
// circular buffer (delayed input)
int delay_buffer_depth = 0;
std::queue<float> delay_buffer;
// delay
uint32_t delay_ms = 0;
std::queue<AnalogDelayEntry> delayed_inputs;
float calculateAngularDifference(float old_rads, float new_rads);
float normalizeAngle(float rads);
@@ -66,7 +89,8 @@ public:
float override_state = 0.5f;
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);
@@ -74,6 +98,8 @@ public:
float applyAngularSensitivity(float raw_rads);
float applyMultiplier(float raw_value);
float applyDeadzone(float raw_value);
float getRelativeModeValue(float raw_value);
float getDelayedValue(float raw_value);
inline bool isSet() {
if (this->override_enabled) {
@@ -90,7 +116,8 @@ public:
deadzone_mirror = false;
setMultiplier(1);
setRelativeMode(false);
setDelayBufferDepth(0);
setLastState(0.5f);
setDelayMs(0);
}
inline void clearBindings() {
@@ -192,25 +219,27 @@ public:
}
inline void setRelativeMode(bool relative_mode) {
if (relative_mode) {
this->smoothing = false;
}
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) {
this->absolute_value_for_rel_mode =
normalizeAnalogValue(this->absolute_value_for_rel_mode + relative_delta);
return this->absolute_value_for_rel_mode;
inline GameAPI::Analogs::AnalogType getType() const {
return this->type;
}
inline int getDelayBufferDepth() const {
return this->delay_buffer_depth;
inline void setType(GameAPI::Analogs::AnalogType type) {
this->type = type;
}
inline void setDelayBufferDepth(int depth) {
this->delay_buffer_depth = depth;
inline uint32_t getDelayMs() const {
return this->delay_ms;
}
inline std::queue<float> &getDelayBuffer() {
return this->delay_buffer;
inline void setDelayMs(uint32_t delay_ms) {
this->delay_ms = delay_ms;
}
};
+283 -79
View File
@@ -1,8 +1,11 @@
#include "api.h"
#include <cassert>
#include <optional>
#include "games/io.h"
#include "launcher/superexit.h"
#include "misc/eamuse.h"
#include "rawinput/rawinput.h"
#include "rawinput/piuio.h"
#include "util/time.h"
@@ -37,7 +40,11 @@ std::vector<Button> GameAPI::Buttons::sortButtons(
for (auto &bt : buttons) {
if (name == bt.getName()) {
button_found = true;
sorted.push_back(bt);
Button button_new = bt;
if (vkey_defaults) {
button_new.setVKeyDefault(vkey_defaults->at(index));
}
sorted.push_back(button_new);
break;
}
}
@@ -47,6 +54,7 @@ std::vector<Button> GameAPI::Buttons::sortButtons(
if (vkey_defaults) {
button.setVKey(vkey_defaults->at(index));
button.setVKeyDefault(vkey_defaults->at(index));
}
}
@@ -56,7 +64,43 @@ std::vector<Button> GameAPI::Buttons::sortButtons(
return sorted;
}
GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *manager, Button &_button, bool check_alts) {
namespace GameAPI::Buttons {
static State get_button_state(
rawinput::RawInputManager *manager,
Button &button,
bool check_alts,
bool check_modifiers);
static bool modifiers_pressed(rawinput::RawInputManager *manager, Button &button);
}
bool GameAPI::Buttons::modifiers_pressed(rawinput::RawInputManager *manager, Button &button) {
const auto modifier_mask = button.getModifierMask();
if (modifier_mask == 0) {
return true;
}
auto *modifier_buttons = games::get_buttons_modifiers(eamuse_get_game());
if (!modifier_buttons) {
return false;
}
for (uint8_t index = 0; index < games::ModifierButtons::Size; index++) {
if ((modifier_mask & (UINT8_C(1) << index)) != 0 &&
(index >= modifier_buttons->size() ||
get_button_state(manager, modifier_buttons->at(index), true, false) !=
GameAPI::Buttons::BUTTON_PRESSED)) {
return false;
}
}
return true;
}
GameAPI::Buttons::State GameAPI::Buttons::get_button_state(
rawinput::RawInputManager *manager,
Button &_button,
bool check_alts,
bool check_modifiers) {
// check override
if (_button.override_enabled) {
@@ -70,6 +114,23 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
std::optional<bool> window_has_focus;
while (true) {
// skip bindings whose required modifiers are not held
//
// note that modifiers cannot process MIDI as the logic is written
// below, since MIDI buttons are event-based (on event, off event, etc)
// and cannot be correctly handled by a simple early return
// there is no explicit check for MIDI here, but the UI should have
// prevented it
if (check_modifiers && !modifiers_pressed(manager, *current_button)) {
button_count++;
if (!alternatives || alternatives->empty() ||
button_count - 1 >= alternatives->size()) {
return BUTTON_NOT_PRESSED;
}
current_button = &alternatives->at(button_count - 1);
continue;
}
// naive behavior
if (current_button->isNaive()) {
GameAPI::Buttons::State state;
@@ -193,9 +254,19 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
if (vKey < value_states->size()) {
auto value = value_states->at(vKey);
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) {
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 {
state = value > 0.01f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
}
@@ -384,7 +455,23 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
}
case rawinput::PIUIO_DEVICE: {
state = device->piuioDev->IsPressed(vKey) ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
break;
}
case rawinput::XINPUT_GAMEPAD: {
assert(reinterpret_cast<uintptr_t>(device->handle) < XUSER_MAX_COUNT);
const auto player = static_cast<uint8_t>(reinterpret_cast<uintptr_t>(device->handle));
state = manager->XINPUT_MGR->is_button_pressed(
player,
static_cast<xinput::XInputButtonEnum>(vKey)) ?
BUTTON_PRESSED : BUTTON_NOT_PRESSED;
break;
}
case rawinput::DESTROYED:
// device was unplugged. release the button instead of leaving
// state at getLastState(), which would latch a held button on
// forever (e.g. a MIDI note held while the device is removed)
state = BUTTON_NOT_PRESSED;
break;
default:
break;
}
@@ -437,6 +524,10 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
}
}
Buttons::State Buttons::getState(rawinput::RawInputManager *manager, Button &button, bool check_alts) {
return get_button_state(manager, button, check_alts, true);
}
Buttons::State Buttons::getState(std::unique_ptr<rawinput::RawInputManager> &manager, Button &button, bool check_alts) {
if (manager) {
return getState(manager.get(), button, check_alts);
@@ -452,17 +543,13 @@ static float getVelocityHelper(rawinput::RawInputManager *manager, Button &butto
return button.override_velocity;
}
// naive behavior
if (button.isNaive()) {
if (button.getInvert()) {
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 0.f : 1.f;
} else {
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 1.f : 0.f;
}
}
// get button state
Buttons::State button_state = Buttons::getState(manager, button, false);
const auto button_state = Buttons::getState(manager, button, false);
// naive bindings report their digital state as full or zero velocity
if (button.isNaive()) {
return button_state == Buttons::BUTTON_PRESSED ? 1.f : 0.f;
}
// check if button isn't being pressed
if (button_state != Buttons::BUTTON_PRESSED) {
@@ -581,7 +668,7 @@ float Buttons::getVelocity(std::unique_ptr<rawinput::RawInputManager> &manager,
}
}
float GameAPI::Analogs::getState(rawinput::Device *device, Analog &analog) {
float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, rawinput::Device *device, Analog &analog) {
float value = 0.5f;
if (!device) {
return value;
@@ -644,88 +731,107 @@ float GameAPI::Analogs::getState(rawinput::Device *device, Analog &analog) {
case rawinput::HID: {
// get value
if (inverted) {
value = 1.f - device->hidInfo->value_states[index];
} else {
value = device->hidInfo->value_states[index];
if (index < device->hidInfo->value_states.size()) {
if (inverted) {
value = 1.f - device->hidInfo->value_states[index];
} else {
value = device->hidInfo->value_states[index];
}
}
// 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);
}
if (analog.isRelativeMode()) {
float relative_delta = value - 0.5f;
// built-in scaling to make values reasonable
relative_delta /= 80.f;
if (analog.getType() == AnalogType::Circular) {
// integer multiplier/divisor
const auto mult = analog.getMultiplier();
if (mult < -1) {
relative_delta /= -mult;
} else if (1 < mult) {
relative_delta *= mult;
}
if (analog.isRelativeMode()) {
value = analog.getRelativeModeValue(value);
// sensitivity (ranges from 0.0 to 4.0)
if (analog.isSensitivitySet()) {
relative_delta *= analog.getSensitivity();
}
} else {
// integer multiplier
value = analog.applyMultiplier(value);
// translate relative movement to absolute value
value = analog.getAbsoluteValue(relative_delta);
// smoothing/sensitivity
if (analog.getSmoothing() || analog.isSensitivitySet()) {
float rads = value * (float) M_TAU;
} else {
// integer multiplier
value = analog.applyMultiplier(value);
// smoothing
if (analog.getSmoothing()) {
// smoothing/sensitivity
if (analog.getSmoothing() || analog.isSensitivitySet()) {
float rads = value * (float) M_TAU;
// preserve direction
if (rads >= M_TAU) {
rads -= 0.0001f;
}
// smoothing
if (analog.getSmoothing()) {
// preserve direction
if (rads >= M_TAU) {
rads -= 0.0001f;
// calculate angle
rads = analog.getSmoothedValue(rads);
}
// calculate angle
rads = analog.getSmoothedValue(rads);
// sensitivity
if (analog.isSensitivitySet()) {
rads = analog.applyAngularSensitivity(rads);
}
// 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
if (analog.isSensitivitySet()) {
rads = analog.applyAngularSensitivity(rads);
value = std::clamp(value, 0.f, 1.f);
}
// 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 = rads * (float) M_1_TAU;
value = std::clamp(value, 0.f, 1.f);
} 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
if (0 < analog.getDelayBufferDepth()) {
auto& queue = analog.getDelayBuffer();
// 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();
}
if (analog.getDelayMs() > 0) {
value = analog.getDelayedValue(value);
}
break;
@@ -760,6 +866,18 @@ float GameAPI::Analogs::getState(rawinput::Device *device, Analog &analog) {
}
break;
}
case rawinput::XINPUT_GAMEPAD: {
assert(reinterpret_cast<uintptr_t>(device->handle) < XUSER_MAX_COUNT);
const auto player = static_cast<uint8_t>(reinterpret_cast<uintptr_t>(device->handle));
value = manager->XINPUT_MGR->get_analog_state(
player,
static_cast<xinput::XInputAnalogEnum>(index));
// invert value
if (inverted) {
value = 1.f - value;
}
break;
}
default:
break;
}
@@ -799,6 +917,42 @@ std::vector<Analog> GameAPI::Analogs::sortAnalogs(
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) {
// check override
@@ -815,7 +969,7 @@ float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, Analog &ana
return analog.getLastState();
}
float state = getState(device, analog);
float state = getState(manager, device, analog);
analog.setLastState(state);
return state;
@@ -859,7 +1013,45 @@ std::vector<Light> GameAPI::Lights::sortLights(
return sorted;
}
void GameAPI::Lights::writeLight(rawinput::Device *device, int index, float value) {
static std::vector<Light> sortLightsWithCategoryInternal(
const std::vector<Light> &lights,
const std::initializer_list<GameAPI::Lights::LightAndCategory> list)
{
std::vector<Light> sorted;
bool light_found;
for (auto &name : list) {
light_found = false;
for (auto &light : lights) {
if (name.light_name == light.getName()) {
light_found = true;
Light light_new = light;
light_new.setCategory(name.category_name);
sorted.push_back(light_new);
break;
}
}
if (!light_found) {
sorted.emplace_back(name.light_name, name.category_name);
}
}
return sorted;
}
void GameAPI::Lights::sortLightsWithCategory(
std::vector<Light> *lights,
const std::initializer_list<LightAndCategory> list) {
if (lights) {
*lights = sortLightsWithCategoryInternal(*lights, list);
}
}
void GameAPI::Lights::writeLight(rawinput::RawInputManager *manager, rawinput::Device *device, int index, float value) {
// check device
if (!device) {
@@ -946,6 +1138,18 @@ void GameAPI::Lights::writeLight(rawinput::Device *device, int index, float valu
}
break;
}
case rawinput::XINPUT_GAMEPAD: {
assert(reinterpret_cast<uintptr_t>(device->handle) < XUSER_MAX_COUNT);
const auto player = static_cast<uint8_t>(reinterpret_cast<uintptr_t>(device->handle));
if (index < static_cast<int>(xinput::XInputOutputEnum::COUNT)) {
manager->XINPUT_MGR->set_output_state(
player, static_cast<xinput::XInputOutputEnum>(index), value);
// no need to set output_pending; xinput output handled immediately
} else {
log_warning("api", "invalid xinput light index: {}", index);
}
break;
}
default:
break;
}
@@ -971,9 +1175,9 @@ void GameAPI::Lights::writeLight(rawinput::RawInputManager *manager, Light &ligh
// write state
if (light.override_enabled) {
writeLight(device, light.getIndex(), light.override_state);
writeLight(manager, device, light.getIndex(), light.override_state);
} else {
writeLight(device, light.getIndex(), value);
writeLight(manager, device, light.getIndex(), value);
}
}
+25 -30
View File
@@ -6,24 +6,20 @@
#include <windows.h>
#include "option.h"
#include "button.h"
#include "analog.h"
#include "light.h"
namespace rawinput {
class RawInputManager;
struct Device;
}
class Button;
class Analog;
class Light;
class Game;
class Option;
namespace GameAPI {
namespace Buttons {
enum State {
BUTTON_PRESSED = true,
BUTTON_NOT_PRESSED = false
};
/**
* Parses the config and returns the buttons set by the user.
*
@@ -89,14 +85,15 @@ namespace GameAPI {
const std::vector<Analog> &analogs,
const std::vector<std::string> &analog_names);
template<typename T>
void sortAnalogs(std::vector<Analog> *analogs, T t) {
const std::vector<std::string> analog_names { t };
struct AnalogWithType {
const std::string name;
const AnalogType type;
if (analogs) {
*analogs = GameAPI::Analogs::sortAnalogs(*analogs, analog_names);
}
}
AnalogWithType(std::string name, AnalogType type) :
name(std::move(name)), type(type) {}
};
void sortAnalogsWithType(std::vector<Analog> *analogs, const std::initializer_list<AnalogWithType> list);
template<typename T, typename... Rest>
void sortAnalogs(std::vector<Analog> *analogs, T t, Rest... rest) {
@@ -107,27 +104,19 @@ namespace GameAPI {
}
}
float getState(rawinput::Device *device, Analog &analog);
float getState(rawinput::RawInputManager *manager, rawinput::Device *device, Analog &analog);
float getState(rawinput::RawInputManager *manager, Analog &analog);
float getState(std::unique_ptr<rawinput::RawInputManager> &manager, Analog &analog);
}
namespace Lights {
std::vector<Light> getLights(const std::string &game_name);
std::vector<Light> sortLights(
const std::vector<Light> &lights,
const std::vector<std::string> &light_names);
template<typename T>
void sortLights(std::vector<Light> *lights, T t) {
const std::vector<std::string> light_names { t };
if (lights) {
*lights = GameAPI::Lights::sortLights(*lights, light_names);
}
}
template<typename T, typename... Rest>
void sortLights(std::vector<Light> *lights, T t, Rest... rest) {
const std::vector<std::string> light_names { t, rest... };
@@ -137,7 +126,17 @@ namespace GameAPI {
}
}
void writeLight(rawinput::Device *device, int index, float value);
struct LightAndCategory {
const std::string category_name;
const std::string light_name;
LightAndCategory(std::string category_name, std::string light_name) :
category_name(std::move(category_name)), light_name(std::move(light_name)) {}
};
void sortLightsWithCategory(std::vector<Light> *lights, const std::initializer_list<LightAndCategory> list);
void writeLight(rawinput::RawInputManager *manager, rawinput::Device *device, int index, float value);
void writeLight(rawinput::RawInputManager *manager, Light &light, float value);
void writeLight(std::unique_ptr<rawinput::RawInputManager> &manager, Light &light, float value);
@@ -152,7 +151,3 @@ namespace GameAPI {
void sortOptions(std::vector<Option> &, const std::vector<OptionDefinition> &);
}
}
#include "button.h"
#include "analog.h"
#include "light.h"
+81 -32
View File
@@ -44,7 +44,7 @@ std::string Button::getVKeyString() {
case 0x0C:
return "Clear";
case 0x0D:
return "Enter";
return "Enter/Return";
case 0x10:
return "Shift";
case 0x11:
@@ -58,8 +58,28 @@ std::string Button::getVKeyString() {
return "Pause";
case 0x14:
return "Caps Lock";
case 0x15:
return "IME Kana/Hangul";
case 0x16:
return "IME On";
case 0x17:
return "IME Junja";
case 0x18:
return "IME Final";
case 0x19:
return "IME Hanja/Kanji";
case 0x1A:
return "IME Off";
case 0x1B:
return "Escape";
case 0x1C:
return "IME Convert";
case 0x1D:
return "IME Nonconvert";
case 0x1E:
return "IME Accept";
case 0x1F:
return "IME Mode Change";
case 0x20:
return "Space";
case 0x21:
@@ -163,37 +183,37 @@ std::string Button::getVKeyString() {
case 0x5D:
return "Apps";
case 0x60:
return "Num 0";
return "Numpad 0";
case 0x61:
return "Num 1";
return "Numpad 1";
case 0x62:
return "Num 2";
return "Numpad 2";
case 0x63:
return "Num 3";
return "Numpad 3";
case 0x64:
return "Num 4";
return "Numpad 4";
case 0x65:
return "Num 5";
return "Numpad 5";
case 0x66:
return "Num 6";
return "Numpad 6";
case 0x67:
return "Num 7";
return "Numpad 7";
case 0x68:
return "Num 8";
return "Numpad 8";
case 0x69:
return "Num 9";
return "Numpad 9";
case 0x6A:
return "*";
return "Numpad *";
case 0x6B:
return "+";
return "Numpad +";
case 0x6C:
return "Seperator";
return "Numpad Seperator";
case 0x6D:
return "-";
return "Numpad -";
case 0x6E:
return ".";
return "Numpad .";
case 0x6F:
return "/";
return "Numpad /";
case 0x70:
return "F1";
case 0x71:
@@ -255,9 +275,11 @@ std::string Button::getVKeyString() {
case 0xA3:
return "Right Control";
case 0xA4:
return "Left Menu";
return "Left Alt";
case 0xA5:
return "Right Menu";
return "Right Alt";
case INVALID_VKEY:
return "Invalid";
default:
// check win API
@@ -277,14 +299,37 @@ std::string Button::getMidiNoteString() {
return fmt::format("{}{}", note_names[index % 12], ((index / 12) - 1));
}
std::string Button::getDisplayString(rawinput::RawInputManager* manager) {
std::string Button::getDisplayString(rawinput::RawInputManager *manager) {
auto binding_display = this->getBindingDisplayString(manager);
auto modifiers = this->getModifierMask();
if (binding_display.empty() || modifiers == 0) {
return binding_display;
}
std::string modifier_display;
for (unsigned int index = 1; modifiers != 0; index++, modifiers >>= 1) {
if ((modifiers & UINT8_C(1)) == 0) {
continue;
}
if (!modifier_display.empty()) {
modifier_display += "+";
}
modifier_display += fmt::format("Mod{}", index);
}
return modifier_display + "+" + binding_display;
}
std::string Button::getBindingDisplayString(rawinput::RawInputManager *manager) {
// get VKey string
auto vKey = (uint16_t) this->getVKey();
std::string vKeyString = fmt::format("{:#x}", vKey);
// 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 "";
}
@@ -301,17 +346,17 @@ std::string Button::getDisplayString(rawinput::RawInputManager* manager) {
switch (device->type) {
case rawinput::MOUSE: {
const char *btn = "Unknown";
static const char *MOUSE_NAMES[] = {
"Left Mouse",
"Right Mouse",
"Middle Mouse",
"Mouse 1",
"Mouse 2",
"Mouse 3",
"Mouse 4",
"Mouse 5",
};
if (vKey < sizeof(MOUSE_NAMES)) {
static const std::array<const char *, 8>MOUSE_NAMES = {
"Left Mouse",
"Right Mouse",
"Middle Mouse",
"Mouse 1",
"Mouse 2",
"Mouse 3",
"Mouse 4",
"Mouse 5",
};
if (vKey < MOUSE_NAMES.size()) {
btn = MOUSE_NAMES[vKey];
}
return fmt::format("{} ({})", btn, device->desc);
@@ -395,6 +440,10 @@ std::string Button::getDisplayString(rawinput::RawInputManager* manager) {
}
case rawinput::PIUIO_DEVICE:
return "PIUIO " + vKeyString;
case rawinput::XINPUT_GAMEPAD:
return fmt::format("{} ({})",
xinput::get_button_string(static_cast<xinput::XInputButtonEnum>(vKey)),
device->desc);
case rawinput::DESTROYED:
return "Device unplugged (" + vKeyString + ")";
default:
+76 -4
View File
@@ -1,15 +1,23 @@
#pragma once
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
#include "api.h"
namespace rawinput {
class RawInputManager;
}
namespace GameAPI {
namespace Buttons {
enum State {
BUTTON_PRESSED = true,
BUTTON_NOT_PRESSED = false
};
}
}
enum ButtonAnalogType {
BAT_NONE = 0,
BAT_POSITIVE = 1,
@@ -41,19 +49,27 @@ private:
std::string name;
std::string device_identifier = "";
unsigned short vKey = INVALID_VKEY;
uint8_t modifier_mask = 0;
// default bindings are always naive
unsigned short vKey_default = INVALID_VKEY;
ButtonAnalogType analog_type = BAT_NONE;
double debounce_up = 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 is_temporary = false;
GameAPI::Buttons::State last_state = GameAPI::Buttons::BUTTON_NOT_PRESSED;
float last_velocity = 0.f;
unsigned short velocity_threshold = 0;
std::string getVKeyString();
std::string getMidiNoteString();
std::string getBindingDisplayString(rawinput::RawInputManager *manager);
public:
std::string getVKeyString();
// overrides
bool override_enabled = false;
@@ -70,12 +86,20 @@ public:
if (this->override_enabled) {
return true;
}
// has a device
if (!this->device_identifier.empty()) {
return true;
}
// naive, and has a valid vKey
if (this->vKey != INVALID_VKEY) {
return true;
}
for (auto &alternative : this->alternatives) {
if (alternative.vKey != INVALID_VKEY) {
if (!alternative.getDeviceIdentifier().empty() ||
alternative.getVKey() != INVALID_VKEY) {
return true;
}
}
@@ -88,6 +112,8 @@ public:
alternatives.clear();
device_identifier = "";
analog_type = BAT_NONE;
bat_threshold = 0;
modifier_mask = 0;
}
std::string getDisplayString(rawinput::RawInputManager* manager);
@@ -100,6 +126,14 @@ public:
return this->name;
}
inline uint8_t getModifierMask() const {
return this->modifier_mask;
}
inline void setModifierMask(uint8_t new_modifier_mask) {
this->modifier_mask = new_modifier_mask & UINT8_C(0x0F);
}
inline const std::string &getDeviceIdentifier() const {
return this->device_identifier;
}
@@ -116,6 +150,14 @@ public:
this->vKey = vKey;
}
inline unsigned short getVKeyDefault() const {
return this->vKey_default;
}
inline void setVKeyDefault(unsigned short vKey_default) {
this->vKey_default = vKey_default;
}
inline ButtonAnalogType getAnalogType() const {
return this->analog_type;
}
@@ -140,6 +182,14 @@ public:
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 {
return this->invert;
}
@@ -164,6 +214,14 @@ public:
this->last_velocity = last_velocity;
}
inline bool isTemporary() const {
return this->is_temporary;
}
inline void setTemporary(bool is_temporary) {
this->is_temporary = is_temporary;
}
inline unsigned short getVelocityThreshold() const {
return this->velocity_threshold;
}
@@ -172,6 +230,20 @@ public:
this->velocity_threshold = velocity_threshold;
}
inline bool isValid() {
// temporarily created by UI
if (isTemporary()) {
return true;
}
// nothing is set
if (getDeviceIdentifier().empty() && getVKey() == INVALID_VKEY) {
return false;
}
return true;
}
void getMidiVKey(int& channel, int& index);
void setMidiVKey(rawinput::RawInputManager* manager, bool is_note, int channel, int index);
+146 -25
View File
@@ -1,5 +1,6 @@
#include "config.h"
#include "util/logging.h"
#include "cfg/button.h"
/*
* This code absolutely sucks.
@@ -10,6 +11,14 @@
// settings
std::string CONFIG_PATH_OVERRIDE = "";
#define CFG_DEBUG_VERBOSE 0
#if CFG_DEBUG_VERBOSE
#define log_debug(module, format_str, ...) logger::push( \
LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY)
#else
#define log_debug(module, format_str, ...)
#endif
///////////////////
/// Constructor ///
@@ -19,7 +28,7 @@ Config::Config() {
this->status = false;
if (CONFIG_PATH_OVERRIDE.length() > 0) {
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 {
this->configLocation = std::filesystem::path(_wgetenv(L"APPDATA")) / L"spicetools.xml";
// avoids logging the expanded appdata path as it contains user name
@@ -48,7 +57,7 @@ Config::Config() {
this->firstFillConfigFile();
break;
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;
case tinyxml2::XMLError::XML_ERROR_FILE_NOT_FOUND:
this->createConfigFile();
@@ -63,7 +72,7 @@ Config::Config() {
case tinyxml2::XMLError::XML_ERROR_PARSING_UNKNOWN:
case tinyxml2::XMLError::XML_ERROR_MISMATCHED_ELEMENT:
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->firstFillConfigFile();
break;
@@ -89,7 +98,7 @@ bool Config::getStatus() {
return this->status;
}
bool Config::addGame(Game &game) {
bool Config::addGame(Game &game, bool save) {
tinyxml2::XMLNode *rootNode = this->configFile.LastChild();
tinyxml2::XMLElement *gameNodes = rootNode->FirstChildElement("game");
@@ -153,15 +162,19 @@ bool Config::addGame(Game &game) {
auto analogType = (int) BAT_NONE;
double debounce_up = 0.0;
double debounce_down = 0.0;
int bat_threshold = 0;
int velocity_threshold = 0;
bool invert = false;
unsigned int modifier_mask = 0;
tinyxml2::XMLError attrError = gameButtonNode->QueryIntAttribute("vkey", &vKey);
const char *devid = gameButtonNode->Attribute("devid");
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up);
gameButtonNode->QueryDoubleAttribute("debounce_down", &debounce_down);
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
gameButtonNode->QueryBoolAttribute("invert", &invert);
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
if (attrError != tinyxml2::XMLError::XML_SUCCESS) {
gameButtonsNode->DeleteChild(gameButtonNode);
gameButtonNode = this->configFile.NewElement("button");
@@ -171,16 +184,20 @@ bool Config::addGame(Game &game) {
gameButtonNode->SetAttribute("devid", button->getDeviceIdentifier().c_str());
gameButtonNode->SetAttribute("debounce_up", debounce_up);
gameButtonNode->SetAttribute("debounce_down", debounce_down);
gameButtonNode->SetAttribute("bat_threshold", bat_threshold);
gameButtonNode->SetAttribute("velocity_threshold", velocity_threshold);
gameButtonNode->SetAttribute("invert", invert);
gameButtonNode->SetAttribute("modifiers", button->getModifierMask());
gameButtonsNode->InsertEndChild(gameButtonNode);
} else {
button->setVKey(static_cast<unsigned short int>(vKey));
button->setAnalogType((ButtonAnalogType) analogType);
button->setDebounceUp(debounce_up);
button->setDebounceDown(debounce_down);
button->setBatThreshold(bat_threshold);
button->setVelocityThreshold(velocity_threshold);
button->setInvert(invert);
button->setModifierMask(static_cast<uint8_t>(modifier_mask));
if (devid) {
button->setDeviceIdentifier(devid);
}
@@ -198,8 +215,10 @@ bool Config::addGame(Game &game) {
gameButtonNode->SetAttribute("analogtype", (int) it.getAnalogType());
gameButtonNode->SetAttribute("debounce_up", it.getDebounceUp());
gameButtonNode->SetAttribute("debounce_down", it.getDebounceDown());
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
gameButtonNode->SetAttribute("invert", it.getInvert());
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
gameButtonsNode->InsertEndChild(gameButtonNode);
}
@@ -239,7 +258,7 @@ bool Config::addGame(Game &game) {
bool smoothing = false;
int multiplier = 1;
bool relative_mode = false;
int delay_buffer_depth = 0;
uint32_t delay = 0;
tinyxml2::XMLError err1 = gameAnalogNode->QueryIntAttribute("index", &index);
gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity);
gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone);
@@ -248,7 +267,7 @@ bool Config::addGame(Game &game) {
gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing);
gameAnalogNode->QueryIntAttribute("multiplier", &multiplier);
gameAnalogNode->QueryBoolAttribute("relative", &relative_mode);
gameAnalogNode->QueryIntAttribute("delay", &delay_buffer_depth);
gameAnalogNode->QueryUnsignedAttribute("delay_ms", &delay);
const char *devid = gameAnalogNode->Attribute("devid");
if (err1 != tinyxml2::XMLError::XML_SUCCESS || !devid) {
@@ -264,7 +283,7 @@ bool Config::addGame(Game &game) {
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
gameAnalogNode->SetAttribute("delay", it.getDelayBufferDepth());
gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
gameAnalogsNode->InsertEndChild(gameAnalogNode);
} else {
it.setIndex(static_cast<unsigned short int>(index));
@@ -276,7 +295,7 @@ bool Config::addGame(Game &game) {
it.setSmoothing(smoothing);
it.setMultiplier(multiplier);
it.setRelativeMode(relative_mode);
it.setDelayBufferDepth(delay_buffer_depth);
it.setDelayMs(delay);
}
} else {
gameAnalogNode = this->configFile.NewElement("analog");
@@ -289,7 +308,7 @@ bool Config::addGame(Game &game) {
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
gameAnalogNode->SetAttribute("delay", it.getDelayBufferDepth());
gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
gameAnalogNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
gameAnalogsNode->InsertEndChild(gameAnalogNode);
}
@@ -423,8 +442,10 @@ bool Config::addGame(Game &game) {
gameButtonNode->SetAttribute("analogtype", it.getAnalogType());
gameButtonNode->SetAttribute("debounce_up", it.getDebounceUp());
gameButtonNode->SetAttribute("debounce_down", it.getDebounceDown());
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
gameButtonNode->SetAttribute("invert", it.getInvert());
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
gameButtonsNode->InsertEndChild(gameButtonNode);
}
@@ -443,7 +464,7 @@ bool Config::addGame(Game &game) {
gameAnalogNode->SetAttribute("smoothing", it.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", it.getMultiplier());
gameAnalogNode->SetAttribute("relative", it.isRelativeMode());
gameAnalogNode->SetAttribute("delay", it.getDelayBufferDepth());
gameAnalogNode->SetAttribute("delay_ms", it.getDelayMs());
gameAnalogsNode->InsertEndChild(gameAnalogNode);
}
@@ -471,13 +492,20 @@ bool Config::addGame(Game &game) {
rootNode->InsertEndChild(gameNode);
}
// save config
this->saveConfigFile();
// save config (skipped when caller batches multiple addGame calls
// and flushes once at the end via Config::save())
if (save) {
this->saveConfigFile();
}
// return success
return true;
}
void Config::save() {
this->saveConfigFile();
}
bool Config::updateBinding(const Game &game, const Button &button, int alternative) {
// get root node
@@ -510,8 +538,10 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
return false;
}
tinyxml2::XMLElement *gameButtonNode = nullptr;
// iterate button nodes
tinyxml2::XMLElement *gameButtonNode = gameButtonsNode->FirstChildElement("button");
gameButtonNode = gameButtonsNode->FirstChildElement("button");
int button_count = 0;
while (gameButtonNode != nullptr) {
const char *buttonNodeName = gameButtonNode->Attribute("name");
@@ -524,9 +554,11 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
gameButtonNode->SetAttribute("analogtype", (int) button.getAnalogType());
gameButtonNode->SetAttribute("debounce_up", button.getDebounceUp());
gameButtonNode->SetAttribute("debounce_down", button.getDebounceDown());
gameButtonNode->SetAttribute("bat_threshold", button.getBatThreshold());
gameButtonNode->SetAttribute("velocity_threshold", button.getVelocityThreshold());
gameButtonNode->SetAttribute("invert", button.getInvert());
gameButtonNode->SetAttribute("devid", button.getDeviceIdentifier().c_str());
gameButtonNode->SetAttribute("modifiers", button.getModifierMask());
break;
}
}
@@ -540,9 +572,11 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
gameButtonNode->SetAttribute("analogtype", 0);
gameButtonNode->SetAttribute("debounce_up", 0.0);
gameButtonNode->SetAttribute("debounce_down", 0.0);
gameButtonNode->SetAttribute("bat_threshold", 0);
gameButtonNode->SetAttribute("velocity_threshold", 0);
gameButtonNode->SetAttribute("invert", false);
gameButtonNode->SetAttribute("devid", "");
gameButtonNode->SetAttribute("modifiers", 0);
gameButtonsNode->InsertEndChild(gameButtonNode);
}
}
@@ -574,6 +608,39 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
}
}
// trim the trailing invalid (unbound) alternatives
tinyxml2::XMLElement *to_delete = nullptr;
gameButtonNode = gameButtonsNode->LastChildElement("button");
while (gameButtonNode != nullptr) {
const char *name = gameButtonNode->Attribute("name");
const char *devid = gameButtonNode->Attribute("devid");
int vkey = INVALID_VKEY;
gameButtonNode->QueryIntAttribute("vkey", &vkey);
// wrong button, ignore and keep iterating backwards
if (name == nullptr || std::string(name) != button.getName()) {
gameButtonNode = gameButtonNode->PreviousSiblingElement("button");
continue;
}
// found a node for this button, which means to_delete is not the first one
// delete the node for to_delete
if (to_delete != nullptr) {
log_debug("cfg", "deleting excessive button binding for {}", name);
gameButtonsNode->DeleteChild(to_delete);
to_delete = nullptr;
}
if ((devid == nullptr || strlen(devid) == 0) && vkey == INVALID_VKEY) {
// unbound button with no device ID, make a note of it so it can be deleted later
to_delete = gameButtonNode;
gameButtonNode = gameButtonNode->PreviousSiblingElement("button");
} else {
// valid binding found; stop iterating
break;
}
}
// check if button was not found
if (button_count == 0) {
return false;
@@ -640,7 +707,7 @@ bool Config::updateBinding(const Game &game, const Analog &analog) {
gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier());
gameAnalogNode->SetAttribute("relative", analog.isRelativeMode());
gameAnalogNode->SetAttribute("delay", analog.getDelayBufferDepth());
gameAnalogNode->SetAttribute("delay_ms", analog.getDelayMs());
gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str());
} else {
gameAnalogNode = this->configFile.NewElement("analog");
@@ -652,7 +719,7 @@ bool Config::updateBinding(const Game &game, const Analog &analog) {
gameAnalogNode->SetAttribute("smoothing", analog.getSmoothing());
gameAnalogNode->SetAttribute("multiplier", analog.getMultiplier());
gameAnalogNode->SetAttribute("relative", analog.isRelativeMode());
gameAnalogNode->SetAttribute("delay", analog.getDelayBufferDepth());
gameAnalogNode->SetAttribute("delay_ms", analog.getDelayMs());
gameAnalogNode->SetAttribute("devid", analog.getDeviceIdentifier().c_str());
gameAnalogsNode->InsertEndChild(gameAnalogNode);
}
@@ -760,6 +827,37 @@ bool Config::updateBinding(const Game &game, const Light &light, int alternative
}
}
// trim the trailing invalid (unbound) alternatives
tinyxml2::XMLElement *to_delete = nullptr;
gameLightNode = gameLightsNode->LastChildElement("light");
while (gameLightNode != nullptr) {
const char *name = gameLightNode->Attribute("name");
const char *devid = gameLightNode->Attribute("devid");
// wrong light, ignore and keep iterating backwards
if (name == nullptr || std::string(name) != light.getName()) {
gameLightNode = gameLightNode->PreviousSiblingElement("light");
continue;
}
// found a node for this button, which means to_delete is not the first one
// delete the node for to_delete
if (to_delete != nullptr) {
log_debug("cfg", "deleting excessive light binding for {}", name);
gameLightsNode->DeleteChild(to_delete);
to_delete = nullptr;
}
if (devid == nullptr || strlen(devid) == 0) {
// unbound light, make a note of it so it can be deleted later
to_delete = gameLightNode;
gameLightNode = gameLightNode->PreviousSiblingElement("light");
} else {
// valid binding found; stop iterating
break;
}
}
// check if light was not found
if (light_count == 0) {
return false;
@@ -871,14 +969,18 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
auto analogType = (int) BAT_NONE;
double debounce_up = 0.0;
double debounce_down = 0.0;
int bat_threshold = 0;
int velocity_threshold = 0;
bool invert = false;
unsigned int modifier_mask = 0;
gameButtonNode->QueryIntAttribute("vkey", &vKey);
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up);
gameButtonNode->QueryDoubleAttribute("debounce_down", &debounce_down);
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
gameButtonNode->QueryBoolAttribute("invert", &invert);
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
const char *devid = gameButtonNode->Attribute("devid");
// find alternative
@@ -891,8 +993,10 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
alt.setAnalogType((ButtonAnalogType) analogType);
alt.setDebounceUp(debounce_up);
alt.setDebounceDown(debounce_down);
alt.setBatThreshold(bat_threshold);
alt.setVelocityThreshold(velocity_threshold);
alt.setInvert(invert);
alt.setModifierMask(static_cast<uint8_t>(modifier_mask));
if (devid) {
alt.setDeviceIdentifier(std::string(devid));
}
@@ -909,8 +1013,10 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
button.setAnalogType((ButtonAnalogType) analogType);
button.setDebounceUp(debounce_up);
button.setDebounceDown(debounce_down);
button.setBatThreshold(bat_threshold);
button.setVelocityThreshold(velocity_threshold);
button.setInvert(invert);
button.setModifierMask(static_cast<uint8_t>(modifier_mask));
if (devid) {
button.setDeviceIdentifier(devid);
}
@@ -1055,7 +1161,7 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
bool smoothing = false;
int multiplier = 1;
bool relative_mode = false;
int delay_buffer_depth = 0;
uint32_t delay = 0;
gameAnalogNode->QueryIntAttribute("index", &index);
gameAnalogNode->QueryFloatAttribute("sensivity", &sensitivity);
gameAnalogNode->QueryFloatAttribute("deadzone", &deadzone);
@@ -1064,7 +1170,7 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
gameAnalogNode->QueryBoolAttribute("smoothing", &smoothing);
gameAnalogNode->QueryIntAttribute("multiplier", &multiplier);
gameAnalogNode->QueryBoolAttribute("relative", &relative_mode);
gameAnalogNode->QueryIntAttribute("delay", &delay_buffer_depth);
gameAnalogNode->QueryUnsignedAttribute("delay_ms", &delay);
const char *devid = gameAnalogNode->Attribute("devid");
// create analog and add to list
@@ -1077,7 +1183,7 @@ std::vector<Analog> Config::getAnalogs(const std::string &gameName) {
analog.setSmoothing(smoothing);
analog.setMultiplier(multiplier);
analog.setRelativeMode(relative_mode);
analog.setDelayBufferDepth(delay_buffer_depth);
analog.setDelayMs(delay);
if (devid) {
analog.setDeviceIdentifier(devid);
}
@@ -1237,17 +1343,32 @@ bool Config::firstFillConfigFile() {
}
void Config::saveConfigFile() {
// create a .tmp file and write to it...
// write the new config to a .tmp file first...
const auto xml_result = this->configFile.SaveFile(this->configLocationTemp.c_str(), false);
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;
}
// copy the .tmp file to the main file...
if (CopyFileW(this->configLocationTemp.c_str(), this->configLocation.c_str(), false) == 0) {
log_warning("cfg", "CopyFileA failed: 0x{:08x}", GetLastError());
// ...flush the .tmp file to disk so a crash/power loss can't leave it half-written...
HANDLE tmp_handle = CreateFileW(
this->configLocationTemp.c_str(),
GENERIC_WRITE, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (tmp_handle != INVALID_HANDLE_VALUE) {
FlushFileBuffers(tmp_handle);
CloseHandle(tmp_handle);
}
// ...then atomically replace the real config with the .tmp file.
// Unlike CopyFile (which truncates and rewrites the destination in place),
// an NTFS rename is atomic: the existing config is never left half-overwritten,
// so an interrupted save can't corrupt it. MoveFileEx also removes the .tmp on success.
if (MoveFileExW(
this->configLocationTemp.c_str(),
this->configLocation.c_str(),
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) == 0) {
log_warning("cfg", "MoveFileExW failed: 0x{:08x}", GetLastError());
return;
}
// delete the .tmp file (not critical if this fails)
DeleteFileW(this->configLocationTemp.c_str());
}
+6 -1
View File
@@ -22,7 +22,12 @@ public:
bool getStatus();
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 Analog &analog);
+94 -4
View File
@@ -1,11 +1,16 @@
#include "configurator.h"
#include "configurator.h"
#include <d3d9.h>
#include "overlay/overlay.h"
#include "util/libutils.h"
#include "util/logging.h"
namespace cfg {
// globals
bool CONFIGURATOR_STANDALONE = false;
bool CONFIGURATOR_FORCE_SOFTWARE_RENDER = false;
ConfigType CONFIGURATOR_TYPE = ConfigType::Config;
Configurator::Configurator() {
@@ -16,13 +21,98 @@ namespace cfg {
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;
}
// load d3d9.dll dynamically rather than linking against it statically.
// a static import would force the system d3d9.dll to load at process
// startup for the main spice executable too (this file is shared with
// spice.exe), which loads system32\d3d9.dll before the modules directory
// is added to the DLL search path - preventing a user-supplied DXVK
// d3d9.dll in modules from ever loading for the game.
typedef IDirect3D9 *(WINAPI *Direct3DCreate9_t)(UINT);
HMODULE d3d9_module = libutils::try_library("d3d9.dll");
if (d3d9_module == nullptr) {
log_warning("configurator", "could not load d3d9.dll, falling back to software renderer");
return false;
}
auto Direct3DCreate9_fn =
reinterpret_cast<Direct3DCreate9_t>(libutils::try_proc(d3d9_module, "Direct3DCreate9"));
if (Direct3DCreate9_fn == nullptr) {
log_warning("configurator", "could not find Direct3DCreate9, falling back to software renderer");
return false;
}
IDirect3D9 *d3d = Direct3DCreate9_fn(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() {
// 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::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->hotkeys_enable = false;
ImGui::GetIO().MouseDrawCursor = false;
// run window
+1
View File
@@ -11,6 +11,7 @@ namespace cfg {
// globals
extern bool CONFIGURATOR_STANDALONE;
extern ConfigType CONFIGURATOR_TYPE;
extern bool CONFIGURATOR_FORCE_SOFTWARE_RENDER;
class Configurator {
private:
+472 -43
View File
@@ -1,11 +1,18 @@
#include "configurator_wnd.h"
#include "configurator_wnd.h"
#include <algorithm>
#include <cstring>
#include <windows.h>
#include <windowsx.h>
#include "build/defs.h"
#include "external/imgui/imgui.h"
#include "launcher/shutdown.h"
#include "overlay/overlay.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include "cfg/configurator.h"
#include "icon.h"
@@ -14,8 +21,177 @@ static const char *CLASS_NAME = "ConfiguratorWindow";
static std::string WINDOW_TITLE;
static int WINDOW_SIZE_X = 800;
static int WINDOW_SIZE_Y = 600;
static const int WINDOW_MIN_SIZE_X = 540;
static const int WINDOW_MIN_SIZE_Y = 300;
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));
}
// computes the top-left position that centers a window of the given size on
// the primary monitor's work area (the desktop minus the taskbar); falls back
// to (0, 0) if the monitor info can't be queried.
static POINT center_on_primary_monitor(int width, int height) {
POINT pos = { 0, 0 };
HMONITOR mon = MonitorFromPoint(pos, MONITOR_DEFAULTTOPRIMARY);
MONITORINFO mi {};
mi.cbSize = sizeof(mi);
if (GetMonitorInfo(mon, &mi)) {
const int work_w = mi.rcWork.right - mi.rcWork.left;
const int work_h = mi.rcWork.bottom - mi.rcWork.top;
pos.x = mi.rcWork.left + (work_w - width) / 2;
pos.y = mi.rcWork.top + (work_h - height) / 2;
}
return pos;
}
// 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() {
// register the window class
@@ -27,12 +203,22 @@ cfg::ConfiguratorWindow::ConfiguratorWindow() {
wc.hIcon = WINDOW_ICON;
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
if (cfg::CONFIGURATOR_TYPE == cfg::ConfigType::Config) {
WINDOW_TITLE = "spice2x config (" + to_string(VERSION_STRING_CFG) + ")";
WINDOW_SIZE_X = 800;
WINDOW_SIZE_Y = 600;
}
this->client_width = WINDOW_SIZE_X;
this->client_height = WINDOW_SIZE_Y;
// open window
this->hWnd = CreateWindowEx(
@@ -48,27 +234,58 @@ cfg::ConfiguratorWindow::ConfiguratorWindow() {
if (this->hWnd) {
overlay::USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = true;
// force dark title bar
set_window_dark_titlebar(this->hWnd, true);
}
}
cfg::ConfiguratorWindow::~ConfiguratorWindow() {
this->stop_timer();
// close window
DestroyWindow(this->hWnd);
// unregister class
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() {
const POINT pos = center_on_primary_monitor(WINDOW_SIZE_X, WINDOW_SIZE_Y);
SetWindowPos(this->hWnd, HWND_TOP, pos.x, pos.y, WINDOW_SIZE_X, WINDOW_SIZE_Y, 0);
// show window
SetWindowPos(this->hWnd, HWND_TOP, 0, 0, WINDOW_SIZE_X, WINDOW_SIZE_Y, 0);
ShowWindow(this->hWnd, SW_SHOWNORMAL);
UpdateWindow(this->hWnd);
// draw overlay in 60 FPS
SetTimer(this->hWnd, 1, 1000 / 60, nullptr);
// SW_SHOWNORMAL usually activates a top-level window, but not always (e.g.,
// 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
BOOL ret;
@@ -94,6 +311,22 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
}
break;
}
case WM_GETMINMAXINFO: {
// enforce a minimum window size so the UI can't be shrunk to a
// point where the tabs/controls become unusable. The minimum is
// expressed in client-area pixels and converted to a full window
// size that accounts for the current frame/title-bar style.
RECT rc = { 0, 0, WINDOW_MIN_SIZE_X, WINDOW_MIN_SIZE_Y };
DWORD style = static_cast<DWORD>(GetWindowLongPtrW(hWnd, GWL_STYLE));
DWORD ex_style = static_cast<DWORD>(GetWindowLongPtrW(hWnd, GWL_EXSTYLE));
if (AdjustWindowRectEx(&rc, style, FALSE, ex_style)) {
auto *mmi = reinterpret_cast<MINMAXINFO *>(lParam);
mmi->ptMinTrackSize.x = rc.right - rc.left;
mmi->ptMinTrackSize.y = rc.bottom - rc.top;
}
break;
}
case WM_CREATE: {
// set user data of window to class pointer
@@ -102,6 +335,72 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
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_DESTROY: {
@@ -110,62 +409,192 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
break;
}
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) {
overlay::OVERLAY->update();
overlay::OVERLAY->set_active(true);
overlay::OVERLAY->new_frame();
overlay::OVERLAY->render();
}
// repaint window
InvalidateRect(hWnd, nullptr, TRUE);
if (use_d3d9) {
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;
}
case WM_ERASEBKGND: {
return 1;
}
case WM_PAINT: {
// render overlay
if (overlay::OVERLAY) {
// get pixel data
int width, height;
uint32_t *pixel_data = overlay::OVERLAY->sw_get_pixel_data(&width, &height);
if (width > 0 && height > 0) {
// create bitmap
HBITMAP bitmap = CreateBitmap(width, height, 1, 8 * sizeof(uint32_t), pixel_data);
// 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);
}
paint_software(hWnd);
break;
}
case WM_ACTIVATE: {
const WORD activation = LOWORD(wParam);
if (activation == WA_INACTIVE) {
// dropping any held mouse buttons so we don't get stuck in a
// "button still pressed" state when we come back.
auto &io = ImGui::GetIO();
io.AddMouseButtonEvent(ImGuiMouseButton_Left, false);
io.AddMouseButtonEvent(ImGuiMouseButton_Right, false);
io.AddMouseButtonEvent(ImGuiMouseButton_Middle, false);
} 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;
}
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:
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
+31 -1
View File
@@ -1,6 +1,9 @@
#pragma once
#pragma once
#include <cstdint>
#include <windows.h>
#include <d3d9.h>
namespace cfg {
@@ -9,11 +12,38 @@ namespace cfg {
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();
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);
};
}
+508
View File
@@ -0,0 +1,508 @@
#include "overlay/windows/controller_presets.h"
#include <filesystem>
#include "cfg/resource.h"
#include "external/tinyxml2/tinyxml2.h"
#include "util/fileutils.h"
#include "util/logging.h"
#include "util/resutils.h"
namespace overlay::windows {
static std::filesystem::path get_templates_path() {
return fileutils::get_config_file_path("ControllerPresets", "spicetools_presets.xml");
}
static void write_button_entry(tinyxml2::XMLDocument &doc, tinyxml2::XMLElement *parent,
const std::string &name, const TemplateButtonEntry &entry) {
auto *el = doc.NewElement("button");
el->SetAttribute("name", name.c_str());
el->SetAttribute("vkey", entry.vKey);
el->SetAttribute("analogtype", (int)entry.analog_type);
el->SetAttribute("devid", entry.device_identifier.c_str());
el->SetAttribute("invert", entry.invert);
el->SetAttribute("debounce_up", entry.debounce_up);
el->SetAttribute("debounce_down", entry.debounce_down);
el->SetAttribute("bat_threshold", entry.bat_threshold);
el->SetAttribute("velocity_threshold", entry.velocity_threshold);
el->SetAttribute("modifiers", entry.modifier_mask);
parent->InsertEndChild(el);
}
static TemplateButtonEntry read_button_entry(tinyxml2::XMLElement *el) {
TemplateButtonEntry entry;
int vkey = INVALID_VKEY;
el->QueryIntAttribute("vkey", &vkey);
entry.vKey = (unsigned short)vkey;
int atype = 0;
el->QueryIntAttribute("analogtype", &atype);
entry.analog_type = (ButtonAnalogType)atype;
const char *devid = el->Attribute("devid");
entry.device_identifier = devid ? devid : "";
el->QueryBoolAttribute("invert", &entry.invert);
el->QueryDoubleAttribute("debounce_up", &entry.debounce_up);
el->QueryDoubleAttribute("debounce_down", &entry.debounce_down);
int vel = 0;
el->QueryIntAttribute("velocity_threshold", &vel);
entry.velocity_threshold = (unsigned short)vel;
int bat = 0;
el->QueryIntAttribute("bat_threshold", &bat);
entry.bat_threshold = bat;
unsigned int modifier_mask = 0;
el->QueryUnsignedAttribute("modifiers", &modifier_mask);
entry.modifier_mask = static_cast<uint8_t>(modifier_mask & UINT8_C(0x0F));
return entry;
}
static void write_analog(tinyxml2::XMLDocument &doc, tinyxml2::XMLElement *parent,
const TemplateAnalogBinding &analog) {
auto *el = doc.NewElement("analog");
el->SetAttribute("name", analog.name.c_str());
el->SetAttribute("devid", analog.device_identifier.c_str());
el->SetAttribute("index", analog.index);
el->SetAttribute("sensivity", analog.sensitivity); // keep typo for compat
el->SetAttribute("deadzone", analog.deadzone);
el->SetAttribute("deadzone_mirror", analog.deadzone_mirror);
el->SetAttribute("invert", analog.invert);
el->SetAttribute("smoothing", analog.smoothing);
el->SetAttribute("multiplier", analog.multiplier);
el->SetAttribute("relative", analog.relative_mode);
el->SetAttribute("delay_ms", analog.delay_ms);
parent->InsertEndChild(el);
}
static TemplateAnalogBinding read_analog(tinyxml2::XMLElement *el) {
TemplateAnalogBinding a;
const char *name = el->Attribute("name");
a.name = name ? name : "";
const char *devid = el->Attribute("devid");
a.device_identifier = devid ? devid : "";
int idx = 0xFF;
el->QueryIntAttribute("index", &idx);
a.index = (unsigned short)idx;
el->QueryFloatAttribute("sensivity", &a.sensitivity); // keep typo
el->QueryFloatAttribute("deadzone", &a.deadzone);
el->QueryBoolAttribute("deadzone_mirror", &a.deadzone_mirror);
el->QueryBoolAttribute("invert", &a.invert);
el->QueryBoolAttribute("smoothing", &a.smoothing);
el->QueryIntAttribute("multiplier", &a.multiplier);
el->QueryBoolAttribute("relative", &a.relative_mode);
el->QueryUnsignedAttribute("delay_ms", &a.delay_ms);
return a;
}
static void write_light_entry(tinyxml2::XMLDocument &doc, tinyxml2::XMLElement *parent,
const std::string &name, const TemplateLightEntry &entry) {
auto *el = doc.NewElement("light");
el->SetAttribute("name", name.c_str());
el->SetAttribute("devid", entry.device_identifier.c_str());
el->SetAttribute("index", entry.index);
parent->InsertEndChild(el);
}
static TemplateLightEntry read_light_entry(tinyxml2::XMLElement *el) {
TemplateLightEntry entry;
const char *devid = el->Attribute("devid");
entry.device_identifier = devid ? devid : "";
int idx = 0;
el->QueryIntAttribute("index", &idx);
entry.index = (unsigned int)idx;
return entry;
}
// Parse all templates from an already-loaded XMLDocument.
// is_builtin controls the is_builtin flag on each returned template.
static std::vector<ControllerTemplate> load_templates_from_doc(
tinyxml2::XMLDocument &doc, bool is_builtin) {
std::vector<ControllerTemplate> templates;
auto *root = doc.RootElement();
if (!root) {
return templates;
}
auto *tmpl_el = root->FirstChildElement("template");
while (tmpl_el) {
ControllerTemplate tmpl;
const char *name = tmpl_el->Attribute("name");
const char *game = tmpl_el->Attribute("game");
tmpl.name = name ? name : "";
tmpl.game_name = game ? game : "";
tmpl.is_builtin = is_builtin;
// buttons
auto *buttons_el = tmpl_el->FirstChildElement("buttons");
if (buttons_el) {
auto *btn_el = buttons_el->FirstChildElement("button");
while (btn_el) {
const char *btn_name = btn_el->Attribute("name");
std::string btn_name_str = btn_name ? btn_name : "";
TemplateButtonBinding *binding = nullptr;
for (auto &b : tmpl.buttons) {
if (b.name == btn_name_str) { binding = &b; break; }
}
if (!binding) {
tmpl.buttons.push_back({btn_name_str, read_button_entry(btn_el), {}});
} else {
binding->alternatives.push_back(read_button_entry(btn_el));
}
btn_el = btn_el->NextSiblingElement("button");
}
}
// modifier buttons
auto *mod_el = tmpl_el->FirstChildElement("modifier_buttons");
if (mod_el) {
auto *btn_el = mod_el->FirstChildElement("button");
while (btn_el) {
const char *btn_name = btn_el->Attribute("name");
std::string btn_name_str = btn_name ? btn_name : "";
TemplateButtonBinding *binding = nullptr;
for (auto &b : tmpl.modifier_buttons) {
if (b.name == btn_name_str) { binding = &b; break; }
}
if (!binding) {
tmpl.modifier_buttons.push_back(
{btn_name_str, read_button_entry(btn_el), {}});
} else {
binding->alternatives.push_back(read_button_entry(btn_el));
}
btn_el = btn_el->NextSiblingElement("button");
}
}
// keypad buttons
auto *kp_el = tmpl_el->FirstChildElement("keypad_buttons");
if (kp_el) {
auto *btn_el = kp_el->FirstChildElement("button");
while (btn_el) {
const char *btn_name = btn_el->Attribute("name");
std::string btn_name_str = btn_name ? btn_name : "";
TemplateButtonBinding *binding = nullptr;
for (auto &b : tmpl.keypad_buttons) {
if (b.name == btn_name_str) { binding = &b; break; }
}
if (!binding) {
tmpl.keypad_buttons.push_back({btn_name_str, read_button_entry(btn_el), {}});
} else {
binding->alternatives.push_back(read_button_entry(btn_el));
}
btn_el = btn_el->NextSiblingElement("button");
}
}
// analogs
auto *analogs_el = tmpl_el->FirstChildElement("analogs");
if (analogs_el) {
auto *analog_el = analogs_el->FirstChildElement("analog");
while (analog_el) {
tmpl.analogs.push_back(read_analog(analog_el));
analog_el = analog_el->NextSiblingElement("analog");
}
}
// lights
auto *lights_el = tmpl_el->FirstChildElement("lights");
if (lights_el) {
auto *light_el = lights_el->FirstChildElement("light");
while (light_el) {
const char *light_name = light_el->Attribute("name");
std::string light_name_str = light_name ? light_name : "";
TemplateLightBinding *binding = nullptr;
for (auto &l : tmpl.lights) {
if (l.name == light_name_str) { binding = &l; break; }
}
if (!binding) {
tmpl.lights.push_back({light_name_str, read_light_entry(light_el), {}});
} else {
binding->alternatives.push_back(read_light_entry(light_el));
}
light_el = light_el->NextSiblingElement("light");
}
}
templates.push_back(std::move(tmpl));
tmpl_el = tmpl_el->NextSiblingElement("template");
}
return templates;
}
static std::vector<ControllerTemplate> load_builtin_templates() {
auto xml = resutil::load_file_string(IDR_CONTROLLER_PRESETS_BUILTIN);
tinyxml2::XMLDocument doc;
auto err = doc.Parse(xml.c_str());
if (err != tinyxml2::XML_SUCCESS) {
log_warning("templates", "failed to parse builtin templates XML: {}", (int)err);
return {};
}
return load_templates_from_doc(doc, true);
}
std::vector<ControllerTemplate> load_user_templates() {
std::vector<ControllerTemplate> templates;
auto path = get_templates_path();
tinyxml2::XMLDocument doc;
if (doc.LoadFile(path.c_str()) != tinyxml2::XML_SUCCESS) {
return templates;
}
return load_templates_from_doc(doc, false);
}
bool save_user_template(
const ControllerTemplate &tmpl,
const bool save_buttons,
const bool save_modifiers,
const bool save_keypads,
const bool save_analogs,
const bool save_lights) {
auto path = get_templates_path();
auto path_tmp = path;
path_tmp.replace_extension(L"tmp");
tinyxml2::XMLDocument doc;
if (doc.LoadFile(path.c_str()) != tinyxml2::XML_SUCCESS) {
auto *decl = doc.NewDeclaration();
doc.InsertFirstChild(decl);
auto *root = doc.NewElement("templates");
doc.InsertEndChild(root);
}
auto *root = doc.RootElement();
if (!root) {
root = doc.NewElement("templates");
doc.InsertEndChild(root);
}
// remove existing template with same name and game
auto *existing = root->FirstChildElement("template");
while (existing) {
auto *next = existing->NextSiblingElement("template");
const char *ename = existing->Attribute("name");
const char *egame = existing->Attribute("game");
if (ename && egame &&
std::string(ename) == tmpl.name &&
std::string(egame) == tmpl.game_name) {
root->DeleteChild(existing);
}
existing = next;
}
auto *tmpl_el = doc.NewElement("template");
tmpl_el->SetAttribute("name", tmpl.name.c_str());
tmpl_el->SetAttribute("game", tmpl.game_name.c_str());
// buttons — skip unbound entries
auto *buttons_el = doc.NewElement("buttons");
if (save_buttons) {
for (auto &btn : tmpl.buttons) {
if (!btn.primary.is_unbound()) {
write_button_entry(doc, buttons_el, btn.name, btn.primary);
}
for (auto &alt : btn.alternatives) {
if (!alt.is_unbound()) {
write_button_entry(doc, buttons_el, btn.name, alt);
}
}
}
}
tmpl_el->InsertEndChild(buttons_el);
// modifier buttons - skip unbound entries
if (!tmpl.modifier_buttons.empty()) {
auto *mod_el = doc.NewElement("modifier_buttons");
if (save_modifiers) {
for (auto &btn : tmpl.modifier_buttons) {
if (!btn.primary.is_unbound()) {
write_button_entry(doc, mod_el, btn.name, btn.primary);
}
for (auto &alt : btn.alternatives) {
if (!alt.is_unbound()) {
write_button_entry(doc, mod_el, btn.name, alt);
}
}
}
}
tmpl_el->InsertEndChild(mod_el);
}
// keypad buttons — skip unbound entries
if (!tmpl.keypad_buttons.empty()) {
auto *kp_el = doc.NewElement("keypad_buttons");
if (save_keypads) {
for (auto &btn : tmpl.keypad_buttons) {
if (!btn.primary.is_unbound()) {
write_button_entry(doc, kp_el, btn.name, btn.primary);
}
for (auto &alt : btn.alternatives) {
if (!alt.is_unbound()) {
write_button_entry(doc, kp_el, btn.name, alt);
}
}
}
}
tmpl_el->InsertEndChild(kp_el);
}
// analogs — skip unbound entries
auto *analogs_el = doc.NewElement("analogs");
if (save_analogs) {
for (auto &a : tmpl.analogs) {
if (!a.is_unbound()) {
write_analog(doc, analogs_el, a);
}
}
}
tmpl_el->InsertEndChild(analogs_el);
// lights — skip unbound entries
auto *lights_el = doc.NewElement("lights");
if (save_lights) {
for (auto &l : tmpl.lights) {
if (!l.primary.is_unbound()) {
write_light_entry(doc, lights_el, l.name, l.primary);
}
for (auto &alt : l.alternatives) {
if (!alt.is_unbound()) {
write_light_entry(doc, lights_el, l.name, alt);
}
}
}
}
tmpl_el->InsertEndChild(lights_el);
root->InsertEndChild(tmpl_el);
// ensure directory exists
if (!path.parent_path().empty() && !std::filesystem::exists(path.parent_path())) {
fileutils::dir_create_recursive(path.parent_path());
}
// save atomically via temp file
auto save_err = doc.SaveFile(path_tmp.c_str());
if (save_err != tinyxml2::XML_SUCCESS) {
log_warning("templates", "failed to save templates file: {}", (int)save_err);
return false;
}
if (MoveFileExW(path_tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) == 0) {
log_warning("templates", "failed to rename templates file: 0x{:08x}", GetLastError());
return false;
}
log_info("templates", "templates file saved successfully");
return true;
}
bool delete_user_template(const std::string &game_name, const std::string &template_name) {
auto path = get_templates_path();
tinyxml2::XMLDocument doc;
if (doc.LoadFile(path.c_str()) != tinyxml2::XML_SUCCESS) return false;
auto *root = doc.RootElement();
if (!root) return false;
bool found = false;
auto *tmpl_el = root->FirstChildElement("template");
while (tmpl_el) {
auto *next = tmpl_el->NextSiblingElement("template");
const char *ename = tmpl_el->Attribute("name");
const char *egame = tmpl_el->Attribute("game");
if (ename && egame &&
std::string(ename) == template_name &&
std::string(egame) == game_name) {
root->DeleteChild(tmpl_el);
found = true;
}
tmpl_el = next;
}
if (found) {
auto path_tmp = path;
path_tmp.replace_extension(L"tmp");
if (doc.SaveFile(path_tmp.c_str()) == tinyxml2::XML_SUCCESS) {
MoveFileExW(path_tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH);
}
}
return found;
}
bool rename_user_template(const std::string &game_name,
const std::string &old_name, const std::string &new_name) {
auto path = get_templates_path();
tinyxml2::XMLDocument doc;
if (doc.LoadFile(path.c_str()) != tinyxml2::XML_SUCCESS) return false;
auto *root = doc.RootElement();
if (!root) return false;
bool found = false;
auto *tmpl_el = root->FirstChildElement("template");
while (tmpl_el) {
const char *ename = tmpl_el->Attribute("name");
const char *egame = tmpl_el->Attribute("game");
if (ename && egame &&
std::string(ename) == old_name &&
std::string(egame) == game_name) {
tmpl_el->SetAttribute("name", new_name.c_str());
found = true;
break;
}
tmpl_el = tmpl_el->NextSiblingElement("template");
}
if (found) {
auto path_tmp = path;
path_tmp.replace_extension(L"tmp");
if (doc.SaveFile(path_tmp.c_str()) == tinyxml2::XML_SUCCESS) {
MoveFileExW(path_tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH);
}
}
return found;
}
std::vector<ControllerTemplate> get_templates_for_game(const std::string &game_name) {
std::vector<ControllerTemplate> result;
log_misc("templates", "loading built-in templates for {}", game_name);
for (auto &t : load_builtin_templates()) {
if (t.game_name == game_name) {
result.push_back(std::move(t));
}
}
log_misc("templates", "loading user templates for {}", game_name);
for (auto &t : load_user_templates()) {
if (t.game_name == game_name) {
result.push_back(std::move(t));
}
}
return result;
}
}
+9
View File
@@ -85,6 +85,15 @@ std::string Light::getDisplayString(rawinput::RawInputManager* manager) {
return "Invalid SMX Dedicab Light (" + index_string + ")";
}
case rawinput::XINPUT_GAMEPAD: {
if (index < static_cast<size_t>(xinput::XInputOutputEnum::COUNT)) {
return fmt::format("{} ({})",
xinput::get_output_string(static_cast<xinput::XInputOutputEnum>(index)),
device->desc);
}
return "Invalid XInput Gamepad Light (" + index_string + ")";
}
case rawinput::DESTROYED:
return "Unplugged device (" + index_string + ")";
default:
+34
View File
@@ -11,8 +11,10 @@ class Light {
private:
std::vector<Light> alternatives;
std::string lightName;
std::string lightCategory;
std::string deviceIdentifier = "";
unsigned int index = 0;
bool is_temporary = false;
public:
float last_state = 0.f;
@@ -22,6 +24,8 @@ public:
float override_state = 0.f;
explicit Light(std::string lightName) : lightName(std::move(lightName)) {};
explicit Light(std::string lightName, std::string lightCategory) :
lightName(std::move(lightName)), lightCategory(std::move(lightCategory)) {};
std::string getDisplayString(rawinput::RawInputManager* manager);
@@ -50,6 +54,14 @@ public:
return this->lightName;
}
inline const std::string &getCategory() const {
return this->lightCategory;
}
inline void setCategory(std::string category) {
this->lightCategory = std::move(category);
}
inline const std::string &getDeviceIdentifier() const {
return this->deviceIdentifier;
}
@@ -65,4 +77,26 @@ public:
inline void setIndex(unsigned int index) {
this->index = index;
}
inline bool isTemporary() const {
return this->is_temporary;
}
inline void setTemporary(bool is_temporary) {
this->is_temporary = is_temporary;
}
inline bool isValid() {
// temporarily created by UI
if (isTemporary()) {
return true;
}
// nothing is set
if (getDeviceIdentifier().empty()) {
return false;
}
return true;
}
};
+5 -5
View File
@@ -20,10 +20,10 @@
</dependentAssembly>
</dependency>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
+16
View File
@@ -13,6 +13,16 @@ enum class OptionType {
Hex,
};
enum class OptionPickerType {
None,
AsioDriver,
EACard,
CpuAffinity,
FilePath,
DirectoryPath,
Monitor,
};
struct OptionDefinition {
std::string title;
// unique identifier used for flag matching but also stored in config files
@@ -32,6 +42,12 @@ struct OptionDefinition {
bool sensitive = false;
std::vector<std::pair<std::string, std::string>> elements = {};
bool disabled = false;
OptionPickerType picker = OptionPickerType::None;
// for OptionPickerType::FilePath
std::string file_extension = "";
std::string quick_setting_category = "";
};
class Option {
+1
View File
@@ -4,3 +4,4 @@
#define IDR_PATCHES 132
#define IDR_README 133
#define IDR_DSEGFONT 134
#define IDR_CONTROLLER_PRESETS_BUILTIN 135
+7 -1
View File
@@ -1,9 +1,11 @@
#include "screen_resize.h"
#include "avs/game.h"
#include "external/rapidjson/document.h"
#include "external/rapidjson/pointer.h"
#include "external/rapidjson/prettywriter.h"
#include "misc/eamuse.h"
#include "util/deferlog.h"
#include "util/utils.h"
#include "util/fileutils.h"
#include "hooks/graphics/graphics.h"
@@ -19,7 +21,7 @@ namespace cfg {
if (SCREEN_RESIZE_CFG_PATH_OVERRIDE.has_value()) {
this->config_path = SCREEN_RESIZE_CFG_PATH_OVERRIDE.value();
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;
}
} else {
@@ -78,6 +80,10 @@ namespace cfg {
root);
load_bool_value(doc, root + "enable_screen_resize", this->enable_screen_resize);
if (this->enable_screen_resize) {
log_misc("ScreenResize", "enabled by config file");
}
load_bool_value(doc, root + "enable_linear_filter", this->enable_linear_filter);
for (size_t i = 0; i < std::size(this->scene_settings); i++) {
auto& scene = this->scene_settings[i];
+6
View File
@@ -1,5 +1,6 @@
#pragma once
#include <atomic>
#include <memory>
#include <string>
#include <optional>
@@ -51,11 +52,15 @@ namespace cfg {
bool enable_linear_filter = true;
fullscreen_setting scene_settings[4];
// state
std::atomic<bool> need_surface_clean = false;
// windowed mode sizing
// Windows terminology:
// window = rectangle including the frame
// client = just the content area without frames.
bool window_always_on_top = false;
bool window_disable_round_corners = false;
bool client_keep_aspect_ratio = true;
bool enable_window_resize = false;
int window_decoration = 0; // enum type WindowDecorationMode
@@ -69,6 +74,7 @@ namespace cfg {
uint32_t init_client_height = 0;
float init_client_aspect_ratio = 1.f;
uint32_t init_window_style = 0;
uint32_t init_window_style_ex = 0;
uint32_t window_deco_width = 0;
uint32_t window_deco_height = 0;
@@ -0,0 +1,72 @@
# fails the build if a PE binary statically imports a forbidden DLL.
#
# some DLLs must never end up in spice's static import table, for two reasons:
#
# 1. user-overridable DLLs (e.g. DXVK's d3d9.dll): users drop their own copy
# into the modules directory to replace the system one. a static import
# forces the loader to load the SYSTEM copy at process startup - before the
# modules directory is added to the DLL search path and before the game DLL
# loads - so the user-supplied override never takes effect (see issue #779).
#
# 2. DLLs that break games when present (e.g. Media Foundation: mf/mfplat/
# mfreadwrite): a static import loads them eagerly and breaks Unity games.
#
# in both cases the DLL must instead be loaded dynamically (libutils::try_library
# / GetProcAddress / delay load) so it is only pulled in when actually needed.
#
# invoked via `cmake -P` from a POST_BUILD step. required -D variables:
# OBJDUMP - path to objdump (CMAKE_OBJDUMP)
# TARGET_FILE - path to the PE binary to inspect
# FORBIDDEN - semicolon-separated list of lowercase DLL names to reject
if(NOT OBJDUMP OR NOT EXISTS "${OBJDUMP}")
message(WARNING
"check_no_static_dll_imports: objdump not found, skipping import check for ${TARGET_FILE}")
return()
endif()
execute_process(
COMMAND "${OBJDUMP}" -p "${TARGET_FILE}"
OUTPUT_VARIABLE dump_output
RESULT_VARIABLE dump_result
ERROR_VARIABLE dump_error)
if(NOT dump_result EQUAL 0)
message(WARNING
"check_no_static_dll_imports: objdump failed for ${TARGET_FILE}: ${dump_error}")
return()
endif()
# both GNU objdump and llvm-objdump print one "DLL Name: <name>" line per
# statically imported DLL in their PE private-header dump.
string(REGEX MATCHALL "DLL Name:[ \t]*[^\n\r]+" dll_lines "${dump_output}")
set(violations "")
foreach(line IN LISTS dll_lines)
string(REGEX REPLACE "DLL Name:[ \t]*" "" dll_name "${line}")
string(STRIP "${dll_name}" dll_name)
string(TOLOWER "${dll_name}" dll_name_lower)
if(dll_name_lower IN_LIST FORBIDDEN)
list(APPEND violations "${dll_name}")
endif()
endforeach()
if(violations)
list(REMOVE_DUPLICATES violations)
string(REPLACE ";" ", " violations_str "${violations}")
message(FATAL_ERROR
"static DLL import check FAILED for ${TARGET_FILE}\n"
" forbidden static imports found: ${violations_str}\n"
"\n"
" these DLLs must never be statically imported by spice:\n"
" * user-overridable DLLs (e.g. DXVK d3d9.dll) - a static import loads the\n"
" system copy at startup and preempts the modules override (issue #779).\n"
" * Media Foundation DLLs (mf/mfplat/mfreadwrite) - a static import breaks\n"
" Unity games.\n"
"\n"
" fix: load the DLL dynamically instead - replace the direct API call with a\n"
" libutils::try_library() + libutils::try_proc() lookup (or a delay load), then\n"
" call through the resolved function pointer.")
endif()
message(STATUS "static DLL import check passed for ${TARGET_FILE}")
+8
View File
@@ -15,3 +15,11 @@ RUN pacman --noconfirm -Syu git \
ccache
RUN useradd user -m && echo "ALL ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
RUN su user -c "cd /tmp/ && git clone https://aur.archlinux.org/yay-bin.git --depth=1 && cd yay-bin && makepkg --noconfirm -si && yay --noconfirm -S mingw-w64-cmake"
RUN mkdir -p /opt/llvm-mingw-xp \
&& curl -fsSL "https://github.com/mon/llvm-mingw-xp/releases/download/llvm-mingw-xp-22.1.0/llvm-mingw-llvm-mingw-xp-22.1.0-msvcrt-ubuntu-22.04-x86_64.tar.xz" \
| tar -xJ --strip-components=1 -C /opt/llvm-mingw-xp
ENV PATH="$PATH:/opt/llvm-mingw-xp/bin"
RUN curl -fsSL "https://github.com/mon/windows-dll-compat-checker/releases/download/v1.3/windows_dll_compat_checker-linux-x86_64.tar.xz" \
| tar -xJ -C /usr/local/bin
+552
View File
@@ -0,0 +1,552 @@
// This code comes from:
// https://github.com/dhbaird/easywsclient
//
// To get the latest version:
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.hpp
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp
#include "easywsclient.hpp"
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment( lib, "ws2_32" )
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <vector>
#include <string>
typedef SOCKET socket_t;
#ifndef _SSIZE_T_DEFINED
typedef int ssize_t;
#define _SSIZE_T_DEFINED
#endif
#ifndef _SOCKET_T_DEFINED
typedef SOCKET socket_t;
#define _SOCKET_T_DEFINED
#endif
#if defined(_MSC_VER) && !defined(snprintf)
#define snprintf _snprintf_s
#endif
#include <stdint.h>
#define socketerrno WSAGetLastError()
#define SOCKET_EAGAIN_EINPROGRESS WSAEINPROGRESS
#define SOCKET_EWOULDBLOCK WSAEWOULDBLOCK
#else
#include <fcntl.h>
#include <netdb.h>
#include <netinet/tcp.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <string>
#include <vector>
#include <errno.h>
typedef int socket_t;
#ifndef INVALID_SOCKET
#define INVALID_SOCKET (-1)
#endif
#ifndef SOCKET_ERROR
#define SOCKET_ERROR (-1)
#endif
#define closesocket(s) ::close(s)
#include <errno.h>
#define socketerrno errno
#define SOCKET_EAGAIN_EINPROGRESS EAGAIN
#define SOCKET_EWOULDBLOCK EWOULDBLOCK
#endif
#include <vector>
#include <string>
#include <stdarg.h>
#include "util/logging.h"
// When false, easywsclient's diagnostics are suppressed. The host application
// opts in by setting this flag (see obs_websocket.cpp).
bool EASYWSCLIENT_LOGGING_ENABLED = false;
// Route easywsclient's diagnostic output through the project logger instead of
// writing to stderr, without editing the upstream source lines below: these two
// macros redirect fprintf(stderr, ...) / fputs(..., stderr) to log_misc.
// Only emitted when EASYWSCLIENT_LOGGING_ENABLED is set.
static inline void easywsclient_logf(const char *fmt, ...) {
if (!EASYWSCLIENT_LOGGING_ENABLED) {
return;
}
char buf[1024];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
// trim trailing newline(s); the logger appends its own
size_t len = strlen(buf);
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) {
buf[--len] = '\0';
}
log_misc("easywsclient", "{}", buf);
}
#define fprintf(stream, ...) easywsclient_logf(__VA_ARGS__)
#define fputs(str, stream) easywsclient_logf("%s", str)
using namespace easywsclient;
namespace { // private module-only namespace
socket_t hostname_connect(const std::string& hostname, int port) {
struct addrinfo hints;
struct addrinfo *result;
struct addrinfo *p;
int ret;
socket_t sockfd = INVALID_SOCKET;
char sport[16];
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
snprintf(sport, 16, "%d", port);
if ((ret = getaddrinfo(hostname.c_str(), sport, &hints, &result)) != 0)
{
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
return 1;
}
for(p = result; p != NULL; p = p->ai_next)
{
sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (sockfd == INVALID_SOCKET) { continue; }
if (connect(sockfd, p->ai_addr, p->ai_addrlen) != SOCKET_ERROR) {
break;
}
closesocket(sockfd);
sockfd = INVALID_SOCKET;
}
freeaddrinfo(result);
return sockfd;
}
class _DummyWebSocket : public easywsclient::WebSocket
{
public:
void poll(int timeout) { }
void send(const std::string& message) { }
void sendBinary(const std::string& message) { }
void sendBinary(const std::vector<uint8_t>& message) { }
void sendPing() { }
void close() { }
readyStateValues getReadyState() const { return CLOSED; }
void _dispatch(Callback_Imp & callable) { }
void _dispatchBinary(BytesCallback_Imp& callable) { }
};
class _RealWebSocket : public easywsclient::WebSocket
{
public:
// http://tools.ietf.org/html/rfc6455#section-5.2 Base Framing Protocol
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-------+-+-------------+-------------------------------+
// |F|R|R|R| opcode|M| Payload len | Extended payload length |
// |I|S|S|S| (4) |A| (7) | (16/64) |
// |N|V|V|V| |S| | (if payload len==126/127) |
// | |1|2|3| |K| | |
// +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
// | Extended payload length continued, if payload len == 127 |
// + - - - - - - - - - - - - - - - +-------------------------------+
// | |Masking-key, if MASK set to 1 |
// +-------------------------------+-------------------------------+
// | Masking-key (continued) | Payload Data |
// +-------------------------------- - - - - - - - - - - - - - - - +
// : Payload Data continued ... :
// + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
// | Payload Data continued ... |
// +---------------------------------------------------------------+
struct wsheader_type {
unsigned header_size;
bool fin;
bool mask;
enum opcode_type {
CONTINUATION = 0x0,
TEXT_FRAME = 0x1,
BINARY_FRAME = 0x2,
CLOSE = 8,
PING = 9,
PONG = 0xa,
} opcode;
int N0;
uint64_t N;
uint8_t masking_key[4];
};
std::vector<uint8_t> rxbuf;
std::vector<uint8_t> txbuf;
std::vector<uint8_t> receivedData;
socket_t sockfd;
readyStateValues readyState;
bool useMask;
bool isRxBad;
_RealWebSocket(socket_t sockfd, bool useMask) : sockfd(sockfd), readyState(OPEN), useMask(useMask), isRxBad(false) {
}
readyStateValues getReadyState() const {
return readyState;
}
void poll(int timeout) { // timeout in milliseconds
if (readyState == CLOSED) {
if (timeout > 0) {
timeval tv = { timeout/1000, (timeout%1000) * 1000 };
select(0, NULL, NULL, NULL, &tv);
}
return;
}
if (timeout != 0) {
fd_set rfds;
fd_set wfds;
timeval tv = { timeout/1000, (timeout%1000) * 1000 };
FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_SET(sockfd, &rfds);
if (txbuf.size()) { FD_SET(sockfd, &wfds); }
select(sockfd + 1, &rfds, &wfds, NULL, timeout > 0 ? &tv : NULL);
}
while (true) {
// FD_ISSET(0, &rfds) will be true
int N = rxbuf.size();
ssize_t ret;
rxbuf.resize(N + 1500);
ret = recv(sockfd, (char*)&rxbuf[0] + N, 1500, 0);
if (false) { }
else if (ret < 0 && (socketerrno == SOCKET_EWOULDBLOCK || socketerrno == SOCKET_EAGAIN_EINPROGRESS)) {
rxbuf.resize(N);
break;
}
else if (ret <= 0) {
rxbuf.resize(N);
closesocket(sockfd);
readyState = CLOSED;
fputs(ret < 0 ? "Connection error!\n" : "Connection closed!\n", stderr);
break;
}
else {
rxbuf.resize(N + ret);
}
}
while (txbuf.size()) {
int ret = ::send(sockfd, (char*)&txbuf[0], txbuf.size(), 0);
if (false) { } // ??
else if (ret < 0 && (socketerrno == SOCKET_EWOULDBLOCK || socketerrno == SOCKET_EAGAIN_EINPROGRESS)) {
break;
}
else if (ret <= 0) {
closesocket(sockfd);
readyState = CLOSED;
fputs(ret < 0 ? "Connection error!\n" : "Connection closed!\n", stderr);
break;
}
else {
txbuf.erase(txbuf.begin(), txbuf.begin() + ret);
}
}
if (!txbuf.size() && readyState == CLOSING) {
closesocket(sockfd);
readyState = CLOSED;
}
}
virtual void _dispatch(Callback_Imp & callable) {
struct CallbackAdapter : public BytesCallback_Imp
// Adapt void(const std::string<uint8_t>&) to void(const std::string&)
{
Callback_Imp& callable;
CallbackAdapter(Callback_Imp& callable) : callable(callable) { }
void operator()(const std::vector<uint8_t>& message) {
std::string stringMessage(message.begin(), message.end());
callable(stringMessage);
}
};
CallbackAdapter bytesCallback(callable);
_dispatchBinary(bytesCallback);
}
virtual void _dispatchBinary(BytesCallback_Imp& callable) {
// TODO: consider acquiring a lock on rxbuf...
while (true) {
wsheader_type ws;
if (rxbuf.size() < 2) { return; /* Need at least 2 */ }
const uint8_t * data = (uint8_t *) &rxbuf[0]; // peek, but don't consume
ws.fin = (data[0] & 0x80) == 0x80;
ws.opcode = (wsheader_type::opcode_type) (data[0] & 0x0f);
ws.mask = (data[1] & 0x80) == 0x80;
ws.N0 = (data[1] & 0x7f);
ws.header_size = 2 + (ws.N0 == 126? 2 : 0) + (ws.N0 == 127? 8 : 0) + (ws.mask? 4 : 0);
if (rxbuf.size() < ws.header_size) { return; /* Need: ws.header_size - rxbuf.size() */ }
int i = 0;
if (ws.N0 < 126) {
ws.N = ws.N0;
i = 2;
}
else if (ws.N0 == 126) {
ws.N = 0;
ws.N |= ((uint64_t) data[2]) << 8;
ws.N |= ((uint64_t) data[3]) << 0;
i = 4;
}
else if (ws.N0 == 127) {
ws.N = 0;
ws.N |= ((uint64_t) data[2]) << 56;
ws.N |= ((uint64_t) data[3]) << 48;
ws.N |= ((uint64_t) data[4]) << 40;
ws.N |= ((uint64_t) data[5]) << 32;
ws.N |= ((uint64_t) data[6]) << 24;
ws.N |= ((uint64_t) data[7]) << 16;
ws.N |= ((uint64_t) data[8]) << 8;
ws.N |= ((uint64_t) data[9]) << 0;
i = 10;
}
if (ws.mask) {
ws.masking_key[0] = ((uint8_t) data[i+0]) << 0;
ws.masking_key[1] = ((uint8_t) data[i+1]) << 0;
ws.masking_key[2] = ((uint8_t) data[i+2]) << 0;
ws.masking_key[3] = ((uint8_t) data[i+3]) << 0;
}
else {
ws.masking_key[0] = 0;
ws.masking_key[1] = 0;
ws.masking_key[2] = 0;
ws.masking_key[3] = 0;
}
// Note: The checks above should hopefully ensure this addition
// cannot overflow:
if (rxbuf.size() < ws.header_size+ws.N) { return; /* Need: ws.header_size+ws.N - rxbuf.size() */ }
// We got a whole message, now do something with it:
if (false) { }
else if (
ws.opcode == wsheader_type::TEXT_FRAME
|| ws.opcode == wsheader_type::BINARY_FRAME
|| ws.opcode == wsheader_type::CONTINUATION
) {
if (ws.mask) { for (size_t i = 0; i != ws.N; ++i) { rxbuf[i+ws.header_size] ^= ws.masking_key[i&0x3]; } }
receivedData.insert(receivedData.end(), rxbuf.begin()+ws.header_size, rxbuf.begin()+ws.header_size+(size_t)ws.N);// just feed
if (ws.fin) {
callable((const std::vector<uint8_t>) receivedData);
receivedData.erase(receivedData.begin(), receivedData.end());
std::vector<uint8_t> ().swap(receivedData);// free memory
}
}
else if (ws.opcode == wsheader_type::PING) {
if (ws.mask) { for (size_t i = 0; i != ws.N; ++i) { rxbuf[i+ws.header_size] ^= ws.masking_key[i&0x3]; } }
std::string data(rxbuf.begin()+ws.header_size, rxbuf.begin()+ws.header_size+(size_t)ws.N);
sendData(wsheader_type::PONG, data.size(), data.begin(), data.end());
}
else if (ws.opcode == wsheader_type::PONG) { }
else if (ws.opcode == wsheader_type::CLOSE) { close(); }
else { fprintf(stderr, "ERROR: Got unexpected WebSocket message.\n"); close(); }
rxbuf.erase(rxbuf.begin(), rxbuf.begin() + ws.header_size+(size_t)ws.N);
}
}
void sendPing() {
std::string empty;
sendData(wsheader_type::PING, empty.size(), empty.begin(), empty.end());
}
void send(const std::string& message) {
sendData(wsheader_type::TEXT_FRAME, message.size(), message.begin(), message.end());
}
void sendBinary(const std::string& message) {
sendData(wsheader_type::BINARY_FRAME, message.size(), message.begin(), message.end());
}
void sendBinary(const std::vector<uint8_t>& message) {
sendData(wsheader_type::BINARY_FRAME, message.size(), message.begin(), message.end());
}
template<class Iterator>
void sendData(wsheader_type::opcode_type type, uint64_t message_size, Iterator message_begin, Iterator message_end) {
// TODO:
// Masking key should (must) be derived from a high quality random
// number generator, to mitigate attacks on non-WebSocket friendly
// middleware:
const uint8_t masking_key[4] = { 0x12, 0x34, 0x56, 0x78 };
// TODO: consider acquiring a lock on txbuf...
if (readyState == CLOSING || readyState == CLOSED) { return; }
std::vector<uint8_t> header;
header.assign(2 + (message_size >= 126 ? 2 : 0) + (message_size >= 65536 ? 6 : 0) + (useMask ? 4 : 0), 0);
header[0] = 0x80 | type;
if (false) { }
else if (message_size < 126) {
header[1] = (message_size & 0xff) | (useMask ? 0x80 : 0);
if (useMask) {
header[2] = masking_key[0];
header[3] = masking_key[1];
header[4] = masking_key[2];
header[5] = masking_key[3];
}
}
else if (message_size < 65536) {
header[1] = 126 | (useMask ? 0x80 : 0);
header[2] = (message_size >> 8) & 0xff;
header[3] = (message_size >> 0) & 0xff;
if (useMask) {
header[4] = masking_key[0];
header[5] = masking_key[1];
header[6] = masking_key[2];
header[7] = masking_key[3];
}
}
else { // TODO: run coverage testing here
header[1] = 127 | (useMask ? 0x80 : 0);
header[2] = (message_size >> 56) & 0xff;
header[3] = (message_size >> 48) & 0xff;
header[4] = (message_size >> 40) & 0xff;
header[5] = (message_size >> 32) & 0xff;
header[6] = (message_size >> 24) & 0xff;
header[7] = (message_size >> 16) & 0xff;
header[8] = (message_size >> 8) & 0xff;
header[9] = (message_size >> 0) & 0xff;
if (useMask) {
header[10] = masking_key[0];
header[11] = masking_key[1];
header[12] = masking_key[2];
header[13] = masking_key[3];
}
}
// N.B. - txbuf will keep growing until it can be transmitted over the socket:
txbuf.insert(txbuf.end(), header.begin(), header.end());
txbuf.insert(txbuf.end(), message_begin, message_end);
if (useMask) {
size_t message_offset = txbuf.size() - message_size;
for (size_t i = 0; i != message_size; ++i) {
txbuf[message_offset + i] ^= masking_key[i&0x3];
}
}
}
void close() {
if(readyState == CLOSING || readyState == CLOSED) { return; }
readyState = CLOSING;
uint8_t closeFrame[6] = {0x88, 0x80, 0x00, 0x00, 0x00, 0x00}; // last 4 bytes are a masking key
std::vector<uint8_t> header(closeFrame, closeFrame+6);
txbuf.insert(txbuf.end(), header.begin(), header.end());
}
};
easywsclient::WebSocket::pointer from_url(const std::string& url, bool useMask, const std::string& origin) {
char host[512];
int port;
char path[512];
if (url.size() >= 512) {
fprintf(stderr, "ERROR: url size limit exceeded: %s\n", url.c_str());
return NULL;
}
if (origin.size() >= 200) {
fprintf(stderr, "ERROR: origin size limit exceeded: %s\n", origin.c_str());
return NULL;
}
if (false) { }
else if (sscanf(url.c_str(), "ws://%[^:/]:%d/%s", host, &port, path) == 3) {
}
else if (sscanf(url.c_str(), "ws://%[^:/]/%s", host, path) == 2) {
port = 80;
}
else if (sscanf(url.c_str(), "ws://%[^:/]:%d", host, &port) == 2) {
path[0] = '\0';
}
else if (sscanf(url.c_str(), "ws://%[^:/]", host) == 1) {
port = 80;
path[0] = '\0';
}
else {
fprintf(stderr, "ERROR: Could not parse WebSocket url: %s\n", url.c_str());
return NULL;
}
//fprintf(stderr, "easywsclient: connecting: host=%s port=%d path=/%s\n", host, port, path);
socket_t sockfd = hostname_connect(host, port);
if (sockfd == INVALID_SOCKET) {
fprintf(stderr, "Unable to connect to %s:%d\n", host, port);
return NULL;
}
{
// XXX: this should be done non-blocking,
char line[1024];
int status;
int i;
snprintf(line, 1024, "GET /%s HTTP/1.1\r\n", path); ::send(sockfd, line, strlen(line), 0);
if (port == 80) {
snprintf(line, 1024, "Host: %s\r\n", host); ::send(sockfd, line, strlen(line), 0);
}
else {
snprintf(line, 1024, "Host: %s:%d\r\n", host, port); ::send(sockfd, line, strlen(line), 0);
}
snprintf(line, 1024, "Upgrade: websocket\r\n"); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 1024, "Connection: Upgrade\r\n"); ::send(sockfd, line, strlen(line), 0);
if (!origin.empty()) {
snprintf(line, 1024, "Origin: %s\r\n", origin.c_str()); ::send(sockfd, line, strlen(line), 0);
}
snprintf(line, 1024, "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n"); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 1024, "Sec-WebSocket-Version: 13\r\n"); ::send(sockfd, line, strlen(line), 0);
snprintf(line, 1024, "\r\n"); ::send(sockfd, line, strlen(line), 0);
for (i = 0; i < 2 || (i < 1023 && line[i-2] != '\r' && line[i-1] != '\n'); ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } }
line[i] = 0;
if (i == 1023) { fprintf(stderr, "ERROR: Got invalid status line connecting to: %s\n", url.c_str()); return NULL; }
if (sscanf(line, "HTTP/1.1 %d", &status) != 1 || status != 101) { fprintf(stderr, "ERROR: Got bad status connecting to %s: %s", url.c_str(), line); return NULL; }
// TODO: verify response headers,
while (true) {
for (i = 0; i < 2 || (i < 1023 && line[i-2] != '\r' && line[i-1] != '\n'); ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } }
if (line[0] == '\r' && line[1] == '\n') { break; }
}
}
int flag = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (char*) &flag, sizeof(flag)); // Disable Nagle's algorithm
#ifdef _WIN32
u_long on = 1;
ioctlsocket(sockfd, FIONBIO, &on);
#else
fcntl(sockfd, F_SETFL, O_NONBLOCK);
#endif
//fprintf(stderr, "Connected to: %s\n", url.c_str());
return easywsclient::WebSocket::pointer(new _RealWebSocket(sockfd, useMask));
}
} // end of module-only namespace
namespace easywsclient {
WebSocket::pointer WebSocket::create_dummy() {
static pointer dummy = pointer(new _DummyWebSocket);
return dummy;
}
WebSocket::pointer WebSocket::from_url(const std::string& url, const std::string& origin) {
return ::from_url(url, true, origin);
}
WebSocket::pointer WebSocket::from_url_no_mask(const std::string& url, const std::string& origin) {
return ::from_url(url, false, origin);
}
} // namespace easywsclient
+73
View File
@@ -0,0 +1,73 @@
#ifndef EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD
#define EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD
// This code comes from:
// https://github.com/dhbaird/easywsclient
//
// To get the latest version:
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.hpp
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp
#include <string>
#include <vector>
#include <cstdint>
namespace easywsclient {
struct Callback_Imp { virtual void operator()(const std::string& message) = 0; };
struct BytesCallback_Imp { virtual void operator()(const std::vector<uint8_t>& message) = 0; };
class WebSocket {
public:
typedef WebSocket * pointer;
typedef enum readyStateValues { CLOSING, CLOSED, CONNECTING, OPEN } readyStateValues;
// Factories:
static pointer create_dummy();
static pointer from_url(const std::string& url, const std::string& origin = std::string());
static pointer from_url_no_mask(const std::string& url, const std::string& origin = std::string());
// Interfaces:
virtual ~WebSocket() { }
virtual void poll(int timeout = 0) = 0; // timeout in milliseconds
virtual void send(const std::string& message) = 0;
virtual void sendBinary(const std::string& message) = 0;
virtual void sendBinary(const std::vector<uint8_t>& message) = 0;
virtual void sendPing() = 0;
virtual void close() = 0;
virtual readyStateValues getReadyState() const = 0;
template<class Callable>
void dispatch(Callable callable)
// For callbacks that accept a string argument.
{ // N.B. this is compatible with both C++11 lambdas, functors and C function pointers
struct _Callback : public Callback_Imp {
Callable& callable;
_Callback(Callable& callable) : callable(callable) { }
void operator()(const std::string& message) { callable(message); }
};
_Callback callback(callable);
_dispatch(callback);
}
template<class Callable>
void dispatchBinary(Callable callable)
// For callbacks that accept a std::vector<uint8_t> argument.
{ // N.B. this is compatible with both C++11 lambdas, functors and C function pointers
struct _Callback : public BytesCallback_Imp {
Callable& callable;
_Callback(Callable& callable) : callable(callable) { }
void operator()(const std::vector<uint8_t>& message) { callable(message); }
};
_Callback callback(callable);
_dispatchBinary(callback);
}
protected:
virtual void _dispatch(Callback_Imp& callable) = 0;
virtual void _dispatchBinary(BytesCallback_Imp& callable) = 0;
};
} // namespace easywsclient
#endif /* EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD */
+1
View File
@@ -20,6 +20,7 @@ Usage:
#include <memory>
#include <string>
#include <map>
#include <cstdint>
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+6
View File
@@ -21,5 +21,11 @@ set(IMGUI_SOURCES
misc/cpp/imgui_stdlib.cpp
)
# spice2x: DX11 backend is only built for non-XP toolchains
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})
target_include_directories(imgui PRIVATE ${PROJECT_SOURCE_DIR})
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2014-2023 Omar Cornut
Copyright (c) 2014-2026 Omar Cornut
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+894
View File
@@ -0,0 +1,894 @@
// 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'.
// [X] Renderer: Multi-viewport support (multiple windows). Enable with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'.
// 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-XX-XX: Platform: Added support for multiple windows via the ImGuiPlatformIO interface.
// 2026-04-23: DirectX11: Added support for standard draw callbacks (in platform_io): DrawCallback_ResetRenderState, DrawCallback_SetSamplerLinear, DrawCallback_SetSamplerNearest. Obsoleting samplers from ImGui_ImplDX11_RenderState. (#9378)
// 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-02-24: [Docking] Added undocumented ImGui_ImplDX11_SetSwapChainDescs() to configure swap chain creation for secondary viewports.
// 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
// spice2x: resolve D3DCompile at runtime rather than static-linking d3dcompiler,
// which would hardwire an import on d3dcompiler_47.dll (absent on stock Win7).
static pD3DCompile ImGui_ImplDX11_GetD3DCompile()
{
static pD3DCompile fn = []() -> pD3DCompile {
static const char *dlls[] = {
"d3dcompiler_47.dll",
"d3dcompiler_46.dll",
"d3dcompiler_43.dll" };
for (const char *dll : dlls) {
HMODULE mod = GetModuleHandleA(dll);
if (!mod) {
mod = LoadLibraryA(dll);
}
if (mod) {
if (auto p = (pD3DCompile) GetProcAddress(mod, "D3DCompile")) {
return p;
}
}
}
return nullptr;
}();
return fn;
}
// 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_RenderState* RenderState; // == (ImGui_ImplDX11_RenderState*)ImGui::GetPlatformIO().Renderer_RenderState during rendering.
ImVector<DXGI_SWAP_CHAIN_DESC> SwapChainDescsForViewports;
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;
}
// Forward Declarations
static void ImGui_ImplDX11_InitMultiViewportSupport();
static void ImGui_ImplDX11_ShutdownMultiViewportSupport();
// 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);
}
// Draw callbacks
static void ImGui_ImplDX11_DrawCallback_ResetRenderState(const ImDrawList*, const ImDrawCmd*) {} // Intentionally empty. Used as an identifier for rendering loop to call its code. Simpler to implement this way.
static void ImGui_ImplDX11_DrawCallback_SetSamplerLinear(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData(); bd->RenderState->DeviceContext->PSSetSamplers(0, 1, &bd->pTexSamplerLinear); }
static void ImGui_ImplDX11_DrawCallback_SetSamplerNearest(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData(); bd->RenderState->DeviceContext->PSSetSamplers(0, 1, &bd->pTexSamplerNearest); }
// 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 = (UINT)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 = (UINT)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.VertexConstantBuffer = bd->pVertexConstantBuffer;
platform_io.Renderer_RenderState = bd->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()
if (pcmd->UserCallback == ImGui_ImplDX11_DrawCallback_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 = bd->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.
// spice2x: dynamically resolved; see ImGui_ImplDX11_GetD3DCompile.
pD3DCompile D3DCompile_fn = ImGui_ImplDX11_GetD3DCompile();
if (!D3DCompile_fn) {
return false;
}
// 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_fn(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_fn(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.
io.BackendFlags |= ImGuiBackendFlags_RendererHasViewports; // We can create multi-viewports on the Renderer side (optional)
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
platform_io.Renderer_TextureMaxWidth = platform_io.Renderer_TextureMaxHeight = D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION;
platform_io.DrawCallback_ResetRenderState = ImGui_ImplDX11_DrawCallback_ResetRenderState;
platform_io.DrawCallback_SetSamplerLinear = ImGui_ImplDX11_DrawCallback_SetSamplerLinear;
platform_io.DrawCallback_SetSamplerNearest = ImGui_ImplDX11_DrawCallback_SetSamplerNearest;
// 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();
ImGui_ImplDX11_InitMultiViewportSupport();
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_ShutdownMultiViewportSupport();
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 | ImGuiBackendFlags_RendererHasViewports);
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!");
}
//--------------------------------------------------------------------------------------------------------
// MULTI-VIEWPORT / PLATFORM INTERFACE SUPPORT
// This is an _advanced_ and _optional_ feature, allowing the backend to create and handle multiple viewports simultaneously.
// If you are new to dear imgui or creating a new binding for dear imgui, it is recommended that you completely ignore this section first..
//--------------------------------------------------------------------------------------------------------
// Helper structure we store in the void* RendererUserData field of each ImGuiViewport to easily retrieve our backend data.
struct ImGui_ImplDX11_ViewportData
{
IDXGISwapChain* SwapChain;
ID3D11RenderTargetView* RTView;
ImGui_ImplDX11_ViewportData() { SwapChain = nullptr; RTView = nullptr; }
~ImGui_ImplDX11_ViewportData() { IM_ASSERT(SwapChain == nullptr && RTView == nullptr); }
};
// Multi-Viewports: configure templates used when creating swapchains for secondary viewports. Will try them in order.
// This is intentionally not declared in the .h file yet, so you will need to copy this declaration:
void ImGui_ImplDX11_SetSwapChainDescs(const DXGI_SWAP_CHAIN_DESC* desc_templates, int desc_templates_count);
void ImGui_ImplDX11_SetSwapChainDescs(const DXGI_SWAP_CHAIN_DESC* desc_templates, int desc_templates_count)
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
bd->SwapChainDescsForViewports.resize(desc_templates_count);
memcpy(bd->SwapChainDescsForViewports.Data, desc_templates, sizeof(DXGI_SWAP_CHAIN_DESC));
}
static void ImGui_ImplDX11_CreateWindow(ImGuiViewport* viewport)
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
ImGui_ImplDX11_ViewportData* vd = IM_NEW(ImGui_ImplDX11_ViewportData)();
viewport->RendererUserData = vd;
// PlatformHandleRaw should always be a HWND, whereas PlatformHandle might be a higher-level handle (e.g. GLFWWindow*, SDL's WindowID).
// Some backends will leave PlatformHandleRaw == 0, in which case we assume PlatformHandle will contain the HWND.
HWND hwnd = viewport->PlatformHandleRaw ? (HWND)viewport->PlatformHandleRaw : (HWND)viewport->PlatformHandle;
IM_ASSERT(hwnd != 0);
IM_ASSERT(vd->SwapChain == nullptr && vd->RTView == nullptr);
// Create swap chain
HRESULT hr = DXGI_ERROR_UNSUPPORTED;
for (const DXGI_SWAP_CHAIN_DESC& sd_template : bd->SwapChainDescsForViewports)
{
IM_ASSERT(sd_template.BufferDesc.Width == 0 && sd_template.BufferDesc.Height == 0 && sd_template.OutputWindow == nullptr);
DXGI_SWAP_CHAIN_DESC sd = sd_template;
sd.BufferDesc.Width = (UINT)viewport->Size.x;
sd.BufferDesc.Height = (UINT)viewport->Size.y;
sd.OutputWindow = hwnd;
hr = bd->pFactory->CreateSwapChain(bd->pd3dDevice, &sd, &vd->SwapChain);
if (SUCCEEDED(hr))
break;
}
IM_ASSERT(SUCCEEDED(hr));
bd->pFactory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_WINDOW_CHANGES); // Disable e.g. Alt+Enter
// Create the render target
if (vd->SwapChain != nullptr)
{
ID3D11Texture2D* pBackBuffer;
vd->SwapChain->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer));
bd->pd3dDevice->CreateRenderTargetView(pBackBuffer, nullptr, &vd->RTView);
pBackBuffer->Release();
}
}
static void ImGui_ImplDX11_DestroyWindow(ImGuiViewport* viewport)
{
// The main viewport (owned by the application) will always have RendererUserData == nullptr since we didn't create the data for it.
if (ImGui_ImplDX11_ViewportData* vd = (ImGui_ImplDX11_ViewportData*)viewport->RendererUserData)
{
if (vd->SwapChain)
vd->SwapChain->Release();
vd->SwapChain = nullptr;
if (vd->RTView)
vd->RTView->Release();
vd->RTView = nullptr;
IM_DELETE(vd);
}
viewport->RendererUserData = nullptr;
}
static void ImGui_ImplDX11_SetWindowSize(ImGuiViewport* viewport, ImVec2 size)
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
ImGui_ImplDX11_ViewportData* vd = (ImGui_ImplDX11_ViewportData*)viewport->RendererUserData;
if (vd->RTView)
{
vd->RTView->Release();
vd->RTView = nullptr;
}
if (vd->SwapChain)
{
ID3D11Texture2D* pBackBuffer = nullptr;
vd->SwapChain->ResizeBuffers(0, (UINT)size.x, (UINT)size.y, DXGI_FORMAT_UNKNOWN, 0);
vd->SwapChain->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer));
if (pBackBuffer == nullptr) { fprintf(stderr, "ImGui_ImplDX11_SetWindowSize() failed creating buffers.\n"); return; }
bd->pd3dDevice->CreateRenderTargetView(pBackBuffer, nullptr, &vd->RTView);
pBackBuffer->Release();
}
}
static void ImGui_ImplDX11_RenderWindow(ImGuiViewport* viewport, void*)
{
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
ImGui_ImplDX11_ViewportData* vd = (ImGui_ImplDX11_ViewportData*)viewport->RendererUserData;
ImVec4 clear_color = ImVec4(0.0f, 0.0f, 0.0f, 1.0f);
bd->pd3dDeviceContext->OMSetRenderTargets(1, &vd->RTView, nullptr);
if (!(viewport->Flags & ImGuiViewportFlags_NoRendererClear))
bd->pd3dDeviceContext->ClearRenderTargetView(vd->RTView, (float*)&clear_color);
ImGui_ImplDX11_RenderDrawData(viewport->DrawData);
}
static void ImGui_ImplDX11_SwapBuffers(ImGuiViewport* viewport, void*)
{
ImGui_ImplDX11_ViewportData* vd = (ImGui_ImplDX11_ViewportData*)viewport->RendererUserData;
if (vd->SwapChain)
vd->SwapChain->Present(0, 0); // Present without vsync
}
static void ImGui_ImplDX11_InitMultiViewportSupport()
{
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
platform_io.Renderer_CreateWindow = ImGui_ImplDX11_CreateWindow;
platform_io.Renderer_DestroyWindow = ImGui_ImplDX11_DestroyWindow;
platform_io.Renderer_SetWindowSize = ImGui_ImplDX11_SetWindowSize;
platform_io.Renderer_RenderWindow = ImGui_ImplDX11_RenderWindow;
platform_io.Renderer_SwapBuffers = ImGui_ImplDX11_SwapBuffers;
// Default swapchain format
DXGI_SWAP_CHAIN_DESC sd;
ZeroMemory(&sd, sizeof(sd));
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
sd.SampleDesc.Count = 1;
sd.SampleDesc.Quality = 0;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.BufferCount = 1;
sd.Windowed = TRUE;
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
sd.Flags = 0;
ImGui_ImplDX11_SetSwapChainDescs(&sd, 1);
}
static void ImGui_ImplDX11_ShutdownMultiViewportSupport()
{
ImGui::DestroyPlatformWindows();
}
//-----------------------------------------------------------------------------
#endif // #ifndef IMGUI_DISABLE
+53
View File
@@ -0,0 +1,53 @@
// 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'.
// [X] Renderer: Multi-viewport support (multiple windows). Enable with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'.
// 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;
ID3D11Buffer* VertexConstantBuffer;
//ID3D11SamplerState* SamplerLinear; // Use ImDrawList::AddCallback(ImGui::GetPlatform().DrawCallback_SetSamplerLinear)
//ID3D11SamplerState* SamplerNearest; // Use ImDrawList::AddCallback(ImGui::GetPlatform().DrawCallback_SetSamplerNearest)
};
#endif // #ifndef IMGUI_DISABLE
File diff suppressed because it is too large Load Diff
+25 -12
View File
@@ -1,25 +1,38 @@
// dear imgui: Renderer for DirectX9
// This needs to be used along with a Platform Binding (e.g. Win32)
// dear imgui: Renderer Backend for DirectX9
// This needs to be used along with a Platform Backend (e.g. Win32)
// Implemented features:
// [X] Renderer: User texture binding. Use 'LPDIRECT3DTEXTURE9' as ImTextureID. Read the FAQ about ImTextureID in imgui.cpp.
// [X] Renderer: Support for large meshes (64k+ vertices) with 16-bits indices.
// [X] Renderer: User texture binding. Use 'LPDIRECT3DTEXTURE9' 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: IMGUI_USE_BGRA_PACKED_COLOR support, as this is the optimal color encoding for DirectX9.
// [X] Renderer: Multi-viewport support (multiple windows). Enable with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'.
// You can copy and use unmodified imgui_impl_* files in your project. See main.cpp for an example of using this.
// If you are new to dear imgui, read examples/README.txt and read the documentation at the top of imgui.cpp.
// https://github.com/ocornut/imgui
// 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"
#include "imgui.h" // IMGUI_IMPL_API
#ifndef IMGUI_DISABLE
struct IDirect3DDevice9;
IMGUI_IMPL_API bool ImGui_ImplDX9_Init(IDirect3DDevice9 *device);
// Follow "Getting Started" link and check examples/ folder to learn about using backends!
IMGUI_IMPL_API bool ImGui_ImplDX9_Init(IDirect3DDevice9* device);
IMGUI_IMPL_API void ImGui_ImplDX9_Shutdown();
IMGUI_IMPL_API void ImGui_ImplDX9_NewFrame();
IMGUI_IMPL_API void ImGui_ImplDX9_RenderDrawData(ImDrawData *draw_data);
IMGUI_IMPL_API void ImGui_ImplDX9_RenderDrawData(ImDrawData* draw_data);
// Use if you want to reset your rendering device without losing ImGui state.
// Use if you want to reset your rendering device without losing Dear ImGui state.
IMGUI_IMPL_API bool ImGui_ImplDX9_CreateDeviceObjects();
IMGUI_IMPL_API void ImGui_ImplDX9_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_ImplDX9_UpdateTexture(ImTextureData* tex);
#endif // #ifndef IMGUI_DISABLE
+13 -5
View File
@@ -15,7 +15,8 @@
#pragma once
//---- Define assertion handler. Defaults to calling assert().
// If your macro uses multiple statements, make sure is enclosed in a 'do { .. } while (0)' block so it can be used as a single statement.
// - If your macro uses multiple statements, make sure is enclosed in a 'do { .. } while (0)' block so it can be used as a single statement.
// - Compiling with NDEBUG will usually strip out assert() to nothing, which is NOT recommended because we use asserts to notify of programmer mistakes.
//#define IM_ASSERT(_EXPR) MyAssert(_EXPR)
#define IM_ASSERT(_EXPR) ((void)(_EXPR)) // Disable asserts
@@ -29,7 +30,6 @@
//---- Don't define obsolete functions/enums/behaviors. Consider enabling from time to time after updating to clean your code of obsolete function/names.
//#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS
//#define IMGUI_DISABLE_OBSOLETE_KEYIO // 1.87+ disable legacy io.KeyMap[]+io.KeysDown[] in favor io.AddKeyEvent(). This is automatically done by IMGUI_DISABLE_OBSOLETE_FUNCTIONS.
//---- Disable all of Dear ImGui or don't implement standard windows/tools.
// It is very strongly recommended to NOT disable the demo windows and debug tool during development. They are extremely useful in day to day work. Please read comments in imgui_demo.cpp.
@@ -49,6 +49,7 @@
//#define IMGUI_DISABLE_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle at all (replace them with dummies)
//#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle so you can implement them yourself if you don't want to link with fopen/fclose/fread/fwrite. This will also disable the LogToTTY() function.
#define IMGUI_DISABLE_DEFAULT_ALLOCATORS // Don't implement default allocators calling malloc()/free() to avoid linking with them. You will need to call ImGui::SetAllocatorFunctions().
//#define IMGUI_DISABLE_DEFAULT_FONT // Disable default embedded fonts (ProggyClean/ProggyForever), remove ~9 KB + ~14 KB from output binary. AddFontDefaultXXX() functions will assert.
//#define IMGUI_DISABLE_SSE // Disable use of SSE intrinsics even if available
//---- Enable Test Engine / Automation features.
@@ -59,12 +60,15 @@
//#define IMGUI_INCLUDE_IMGUI_USER_H
//#define IMGUI_USER_H_FILENAME "my_folder/my_imgui_user.h"
//---- Pack colors to BGRA8 instead of RGBA8 (to avoid converting from one to another)
//---- Pack vertex colors as BGRA8 instead of RGBA8 (to avoid converting from one to another). Need dedicated backend support.
#define IMGUI_USE_BGRA_PACKED_COLOR
//---- Performance optimization for DX9
#define IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT struct ImDrawVert { ImVec2 pos; float z; ImU32 col; ImVec2 uv; }
//---- Use legacy CRC32-adler tables (used before 1.91.6), in order to preserve old .ini data that you cannot afford to invalidate.
//#define IMGUI_USE_LEGACY_CRC32_ADLER
//---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...)
//#define IMGUI_USE_WCHAR32
@@ -83,13 +87,13 @@
//---- Use FreeType to build and rasterize the font atlas (instead of stb_truetype which is embedded by default in Dear ImGui)
// Requires FreeType headers to be available in the include path. Requires program to be compiled with 'misc/freetype/imgui_freetype.cpp' (in this repository) + the FreeType library (not provided).
// Note that imgui_freetype.cpp may be used _without_ this define, if you manually call ImFontAtlas::SetFontLoader(). The define is simply a convenience.
// On Windows you may use vcpkg with 'vcpkg install freetype --triplet=x64-windows' + 'vcpkg integrate install'.
//#define IMGUI_ENABLE_FREETYPE
//---- Use FreeType + plutosvg or lunasvg to render OpenType SVG fonts (SVGinOT)
// Only works in combination with IMGUI_ENABLE_FREETYPE.
// - lunasvg is currently easier to acquire/install, as e.g. it is part of vcpkg.
// - plutosvg will support more fonts and may load them faster. It currently requires to be built manually but it is fairly easy. See misc/freetype/README for instructions.
// - plutosvg is currently easier to install, as e.g. it is part of vcpkg. It will support more fonts and may load them faster. See misc/freetype/README for instructions.
// - Both require headers to be available in the include path + program to be linked with the library code (not provided).
// - (note: lunasvg implementation is based on Freetype's rsvg-port.c which is licensed under CeCILL-C Free Software License Agreement)
//#define IMGUI_ENABLE_FREETYPE_PLUTOSVG
@@ -130,6 +134,10 @@
//#define IM_DEBUG_BREAK IM_ASSERT(0)
//#define IM_DEBUG_BREAK __debugbreak()
//---- Debug Tools: Enable highlight ID conflicts _before_ hovering items. When io.ConfigDebugHighlightIdConflicts is set.
// (THIS WILL SLOW DOWN DEAR IMGUI. Only use occasionally and disable after use)
//#define IMGUI_DEBUG_HIGHLIGHT_ALL_ID_CONFLICTS
//---- Debug Tools: Enable slower asserts
//#define IMGUI_DEBUG_PARANOID
+3607 -2027
View File
File diff suppressed because it is too large Load Diff
+1081 -543
View File
File diff suppressed because it is too large Load Diff
+2964 -2093
View File
File diff suppressed because it is too large Load Diff
+3341 -1210
View File
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -878,7 +878,15 @@ inline void ImGui::FileBrowser::UpdateFileRecords()
inline void ImGui::FileBrowser::SetPwdUncatched(const std::filesystem::path &pwd)
{
pwd_ = absolute(pwd);
// spice2x
// avoid MinGW std::filesystem::absolute() duplicating UNC server/share
// prefixes, which makes directory_iterator() throw during startup
// spice2x
const auto &nativePwd = pwd.native();
const bool hasUncPrefix = nativePwd.size() >= 2 &&
(nativePwd[0] == '\\' || nativePwd[0] == '/') &&
(nativePwd[1] == '\\' || nativePwd[1] == '/');
pwd_ = hasUncPrefix ? pwd : absolute(pwd);
UpdateFileRecords();
selectedFilenames_.clear();
(*inputNameBuf_)[0] = '\0';
File diff suppressed because it is too large Load Diff
+258 -164
View File
@@ -1,6 +1,7 @@
// Mini memory editor for Dear ImGui (to embed in your game/tools)
// Get latest version at http://www.github.com/ocornut/imgui_club
//
// Licensed under The MIT License (MIT)
// Right-click anywhere to access the Options menu!
// You can adjust the keyboard repeat delay/rate in ImGuiIO.
// The code assume a mono-space font for simplicity!
@@ -39,10 +40,22 @@
// - v0.43 (2021/03/12): added OptFooterExtraHeight to allow for custom drawing at the bottom of the editor [@leiradel]
// - v0.44 (2021/03/12): use ImGuiInputTextFlags_AlwaysOverwrite in 1.82 + fix hardcoded width.
// - v0.50 (2021/11/12): various fixes for recent dear imgui versions (fixed misuse of clipper, relying on SetKeyboardFocusHere() handling scrolling from 1.85). added default size.
// - v0.51 (2024/02/22): fix for layout change in 1.89 when using IMGUI_DISABLE_OBSOLETE_FUNCTIONS. (#34)
// - v0.52 (2024/03/08): removed unnecessary GetKeyIndex() calls, they are a no-op since 1.87.
// - v0.53 (2024/05/27): fixed right-click popup from not appearing when using DrawContents(). warning fixes. (#35)
// - v0.54 (2024/07/29): allow ReadOnly mode to still select and preview data. (#46) [@DeltaGW2])
// - v0.55 (2024/08/19): added BgColorFn to allow setting background colors independently from highlighted selection. (#27) [@StrikerX3]
// added MouseHoveredAddr public readable field. (#47, #27) [@StrikerX3]
// fixed a data preview crash with 1.91.0 WIP. fixed contiguous highlight color when using data preview.
// *BREAKING* added UserData field passed to all optional function handlers: ReadFn, WriteFn, HighlightFn, BgColorFn. (#50) [@silverweed]
// - v0.56 (2024/11/04): fixed MouseHovered, MouseHoveredAddr not being set when hovering a byte being edited. (#54)
// - v0.57 (2025/03/26): fixed warnings. using ImGui's ImSXX/ImUXX types instead of e.g. int32_t/uint32_t. (#56)
// - v0.58 (2025/03/31): fixed extraneous footer spacing (added in 0.51) breaking vertical auto-resize. (#53)
// - v0.59 (2025/04/08): fixed GotoAddrAndHighlight() not working if OptShowOptions is disabled.
//
// Todo/Bugs:
// TODO:
// - This is generally old/crappy code, it should work but isn't very good.. to be rewritten some day.
// - PageUp/PageDown are supported because we use _NoNav. This is a good test scenario for working out idioms of how to mix natural nav and our own...
// - PageUp/PageDown are not supported because we use _NoNav. This is a good test scenario for working out idioms of how to mix natural nav and our own...
// - Arrows are being sent to the InputText() about to disappear which for LeftArrow makes the text cursor appear at position 1 for one frame.
// - Using InputText() is awkward and maybe overkill here, consider implementing something custom.
@@ -51,15 +64,18 @@
#include <stdio.h> // sprintf, scanf
#include <stdint.h> // uint8_t, etc.
#ifdef _MSC_VER
#define _PRISizeT "I"
#if defined(_MSC_VER) && !defined(snprintf)
#define ImSnprintf _snprintf
#else
#define _PRISizeT "z"
#define ImSnprintf snprintf
#endif
#if defined(_MSC_VER) && !defined(__clang__)
#define _PRISizeT "I"
#else
#define _PRISizeT "z"
#endif
#ifdef _MSC_VER
#if defined(_MSC_VER) || defined(_UCRT)
#pragma warning (push)
#pragma warning (disable: 4996) // warning C4996: 'sprintf': This function or variable may be unsafe.
#endif
@@ -88,9 +104,17 @@ struct MemoryEditor
int OptAddrDigitsCount; // = 0 // number of addr digits to display (default calculated based on maximum displayed addr).
float OptFooterExtraHeight; // = 0 // space to reserve at the bottom of the widget to add custom widgets
ImU32 HighlightColor; // // background color of highlighted bytes.
ImU8 (*ReadFn)(const ImU8* data, size_t off); // = 0 // optional handler to read bytes.
void (*WriteFn)(ImU8* data, size_t off, ImU8 d); // = 0 // optional handler to write bytes.
bool (*HighlightFn)(const ImU8* data, size_t off);//= 0 // optional handler to return Highlight property (to support non-contiguous highlighting).
// Function handlers
ImU8 (*ReadFn)(const ImU8* mem, size_t off, void* user_data); // = 0 // optional handler to read bytes.
void (*WriteFn)(ImU8* mem, size_t off, ImU8 d, void* user_data); // = 0 // optional handler to write bytes.
bool (*HighlightFn)(const ImU8* mem, size_t off, void* user_data); // = 0 // optional handler to return Highlight property (to support non-contiguous highlighting).
ImU32 (*BgColorFn)(const ImU8* mem, size_t off, void* user_data); // = 0 // optional handler to return custom background color of individual bytes.
void* UserData; // = NULL // user data forwarded to the function handlers
// Public read-only data
bool MouseHovered; // set when mouse is hovering a value.
size_t MouseHoveredAddr; // the address currently being hovered if MouseHovered is set.
// [Internal State]
bool ContentsWidthChanged;
@@ -101,7 +125,7 @@ struct MemoryEditor
char AddrInputBuf[32];
size_t GotoAddr;
size_t HighlightMin, HighlightMax;
int PreviewEndianess;
int PreviewEndianness;
ImGuiDataType PreviewDataType;
MemoryEditor()
@@ -120,9 +144,11 @@ struct MemoryEditor
OptAddrDigitsCount = 0;
OptFooterExtraHeight = 0.0f;
HighlightColor = IM_COL32(255, 255, 255, 50);
ReadFn = NULL;
WriteFn = NULL;
HighlightFn = NULL;
ReadFn = nullptr;
WriteFn = nullptr;
HighlightFn = nullptr;
BgColorFn = nullptr;
UserData = nullptr;
// State/Internals
ContentsWidthChanged = false;
@@ -131,8 +157,10 @@ struct MemoryEditor
memset(DataInputBuf, 0, sizeof(DataInputBuf));
memset(AddrInputBuf, 0, sizeof(AddrInputBuf));
GotoAddr = (size_t)-1;
MouseHovered = false;
MouseHoveredAddr = 0;
HighlightMin = HighlightMax = (size_t)-1;
PreviewEndianess = 0;
PreviewEndianness = 0;
PreviewDataType = ImGuiDataType_S32;
}
@@ -145,16 +173,16 @@ struct MemoryEditor
struct Sizes
{
int AddrDigitsCount;
float LineHeight;
float GlyphWidth;
float HexCellWidth;
float SpacingBetweenMidCols;
float PosHexStart;
float PosHexEnd;
float PosAsciiStart;
float PosAsciiEnd;
float WindowWidth;
int AddrDigitsCount; // Number of digits required to represent maximum address.
float LineHeight; // Height of each line (no spacing).
float GlyphWidth; // Glyph width (assume mono-space).
float HexCellWidth; // Width of a hex edit cell ~2.5f * GlypHWidth.
float SpacingBetweenMidCols; // Spacing between each columns section (OptMidColsCount).
float OffsetHexMinX;
float OffsetHexMaxX;
float OffsetAsciiMinX;
float OffsetAsciiMaxX;
float WindowWidth; // Ideal window width.
Sizes() { memset(this, 0, sizeof(*this)); }
};
@@ -170,17 +198,17 @@ struct MemoryEditor
s.GlyphWidth = ImGui::CalcTextSize("F").x + 1; // We assume the font is mono-space
s.HexCellWidth = (float)(int)(s.GlyphWidth * 2.5f); // "FF " we include trailing space in the width to easily catch clicks everywhere
s.SpacingBetweenMidCols = (float)(int)(s.HexCellWidth * 0.25f); // Every OptMidColsCount columns we add a bit of extra spacing
s.PosHexStart = (s.AddrDigitsCount + 2) * s.GlyphWidth;
s.PosHexEnd = s.PosHexStart + (s.HexCellWidth * Cols);
s.PosAsciiStart = s.PosAsciiEnd = s.PosHexEnd;
s.OffsetHexMinX = (s.AddrDigitsCount + 2) * s.GlyphWidth;
s.OffsetHexMaxX = s.OffsetHexMinX + (s.HexCellWidth * Cols);
s.OffsetAsciiMinX = s.OffsetAsciiMaxX = s.OffsetHexMaxX;
if (OptShowAscii)
{
s.PosAsciiStart = s.PosHexEnd + s.GlyphWidth * 1;
s.OffsetAsciiMinX = s.OffsetHexMaxX + s.GlyphWidth * 1;
if (OptMidColsCount > 0)
s.PosAsciiStart += (float)((Cols + OptMidColsCount - 1) / OptMidColsCount) * s.SpacingBetweenMidCols;
s.PosAsciiEnd = s.PosAsciiStart + Cols * s.GlyphWidth;
s.OffsetAsciiMinX += (float)((Cols + OptMidColsCount - 1) / OptMidColsCount) * s.SpacingBetweenMidCols;
s.OffsetAsciiMaxX = s.OffsetAsciiMinX + Cols * s.GlyphWidth;
}
s.WindowWidth = s.PosAsciiEnd + style.ScrollbarSize + style.WindowPadding.x * 2 + s.GlyphWidth;
s.WindowWidth = s.OffsetAsciiMaxX + style.ScrollbarSize + style.WindowPadding.x * 2 + s.GlyphWidth;
}
// Standalone Memory Editor window
@@ -194,8 +222,6 @@ struct MemoryEditor
Open = true;
if (ImGui::Begin(title, &Open, ImGuiWindowFlags_NoScrollbar))
{
if (ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows) && ImGui::IsMouseReleased(ImGuiMouseButton_Right))
ImGui::OpenPopup("context");
DrawContents(mem_data, mem_size, base_display_addr);
if (ContentsWidthChanged)
{
@@ -217,6 +243,8 @@ struct MemoryEditor
CalcSizes(s, mem_size, base_display_addr);
ImGuiStyle& style = ImGui::GetStyle();
const ImVec2 contents_pos_start = ImGui::GetCursorScreenPos();
// We begin into our scrolling region with the 'ImGuiWindowFlags_NoMove' in order to prevent click from moving the window.
// This is used as a facility since our main click detection code doesn't assign an ActiveId so the click would normally be caught as a window-move.
const float height_separator = style.ItemSpacing.y;
@@ -225,20 +253,21 @@ struct MemoryEditor
footer_height += height_separator + ImGui::GetFrameHeightWithSpacing() * 1;
if (OptShowDataPreview)
footer_height += height_separator + ImGui::GetFrameHeightWithSpacing() * 1 + ImGui::GetTextLineHeightWithSpacing() * 3;
ImGui::BeginChild("##scrolling", ImVec2(0, -footer_height), false, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoNav);
ImGui::BeginChild("##scrolling", ImVec2(-FLT_MIN, -footer_height), ImGuiChildFlags_None, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoNav);
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
// We are not really using the clipper API correctly here, because we rely on visible_start_addr/visible_end_addr for our scrolling function.
const ImVec2 avail_size = ImGui::GetContentRegionAvail();
const int line_total_count = (int)((mem_size + Cols - 1) / Cols);
ImGuiListClipper clipper;
clipper.Begin(line_total_count, s.LineHeight);
bool data_next = false;
if (ReadOnly || DataEditingAddr >= mem_size)
if (DataEditingAddr >= mem_size)
DataEditingAddr = (size_t)-1;
if (DataPreviewAddr >= mem_size)
DataPreviewAddr = (size_t)-1;
@@ -249,16 +278,16 @@ struct MemoryEditor
if (DataEditingAddr != (size_t)-1)
{
// Move cursor but only apply on next frame so scrolling with be synchronized (because currently we can't change the scrolling while the window is being rendered)
if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_UpArrow)) && (ptrdiff_t)DataEditingAddr >= (ptrdiff_t)Cols) { data_editing_addr_next = DataEditingAddr - Cols; }
else if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_DownArrow)) && (ptrdiff_t)DataEditingAddr < (ptrdiff_t)mem_size - Cols) { data_editing_addr_next = DataEditingAddr + Cols; }
else if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_LeftArrow)) && (ptrdiff_t)DataEditingAddr > (ptrdiff_t)0) { data_editing_addr_next = DataEditingAddr - 1; }
else if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_RightArrow)) && (ptrdiff_t)DataEditingAddr < (ptrdiff_t)mem_size - 1) { data_editing_addr_next = DataEditingAddr + 1; }
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) && (ptrdiff_t)DataEditingAddr >= (ptrdiff_t)Cols) { data_editing_addr_next = DataEditingAddr - Cols; }
else if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) && (ptrdiff_t)DataEditingAddr < (ptrdiff_t)mem_size - Cols){ data_editing_addr_next = DataEditingAddr + Cols; }
else if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow) && (ptrdiff_t)DataEditingAddr > (ptrdiff_t)0) { data_editing_addr_next = DataEditingAddr - 1; }
else if (ImGui::IsKeyPressed(ImGuiKey_RightArrow) && (ptrdiff_t)DataEditingAddr < (ptrdiff_t)mem_size - 1) { data_editing_addr_next = DataEditingAddr + 1; }
}
// Draw vertical separator
ImVec2 window_pos = ImGui::GetWindowPos();
if (OptShowAscii)
draw_list->AddLine(ImVec2(window_pos.x + s.PosAsciiStart - s.GlyphWidth, window_pos.y), ImVec2(window_pos.x + s.PosAsciiStart - s.GlyphWidth, window_pos.y + 9999), ImGui::GetColorU32(ImGuiCol_Border));
draw_list->AddLine(ImVec2(window_pos.x + s.OffsetAsciiMinX - s.GlyphWidth, window_pos.y), ImVec2(window_pos.x + s.OffsetAsciiMinX - s.GlyphWidth, window_pos.y + 9999), ImGui::GetColorU32(ImGuiCol_Border));
const ImU32 color_text = ImGui::GetColorU32(ImGuiCol_Text);
const ImU32 color_disabled = OptGreyOutZeroes ? ImGui::GetColorU32(ImGuiCol_TextDisabled) : color_text;
@@ -268,36 +297,51 @@ struct MemoryEditor
const char* format_byte = OptUpperCaseHex ? "%02X" : "%02x";
const char* format_byte_space = OptUpperCaseHex ? "%02X " : "%02x ";
MouseHovered = false;
MouseHoveredAddr = 0;
while (clipper.Step())
for (int line_i = clipper.DisplayStart; line_i < clipper.DisplayEnd; line_i++) // display only visible lines
{
size_t addr = (size_t)(line_i * Cols);
size_t addr = (size_t)line_i * Cols;
ImGui::Text(format_address, s.AddrDigitsCount, base_display_addr + addr);
// Draw Hexadecimal
for (int n = 0; n < Cols && addr < mem_size; n++, addr++)
{
float byte_pos_x = s.PosHexStart + s.HexCellWidth * n;
float byte_pos_x = s.OffsetHexMinX + s.HexCellWidth * n;
if (OptMidColsCount > 0)
byte_pos_x += (float)(n / OptMidColsCount) * s.SpacingBetweenMidCols;
ImGui::SameLine(byte_pos_x);
// Draw highlight
bool is_highlight_from_user_range = (addr >= HighlightMin && addr < HighlightMax);
bool is_highlight_from_user_func = (HighlightFn && HighlightFn(mem_data, addr));
bool is_highlight_from_preview = (addr >= DataPreviewAddr && addr < DataPreviewAddr + preview_data_type_size);
// Draw highlight or custom background color
const bool is_highlight_from_user_range = (addr >= HighlightMin && addr < HighlightMax);
const bool is_highlight_from_user_func = (HighlightFn && HighlightFn(mem_data, addr, UserData));
const bool is_highlight_from_preview = (addr >= DataPreviewAddr && addr < DataPreviewAddr + preview_data_type_size);
ImU32 bg_color = 0;
bool is_next_byte_highlighted = false;
if (is_highlight_from_user_range || is_highlight_from_user_func || is_highlight_from_preview)
{
ImVec2 pos = ImGui::GetCursorScreenPos();
float highlight_width = s.GlyphWidth * 2;
bool is_next_byte_highlighted = (addr + 1 < mem_size) && ((HighlightMax != (size_t)-1 && addr + 1 < HighlightMax) || (HighlightFn && HighlightFn(mem_data, addr + 1)));
is_next_byte_highlighted = (addr + 1 < mem_size) && ((HighlightMax != (size_t)-1 && addr + 1 < HighlightMax) || (HighlightFn && HighlightFn(mem_data, addr + 1, UserData)) || (addr + 1 < DataPreviewAddr + preview_data_type_size));
bg_color = HighlightColor;
}
else if (BgColorFn != nullptr)
{
is_next_byte_highlighted = (addr + 1 < mem_size) && ((BgColorFn(mem_data, addr + 1, UserData) & IM_COL32_A_MASK) != 0);
bg_color = BgColorFn(mem_data, addr, UserData);
}
if (bg_color != 0)
{
float bg_width = s.GlyphWidth * 2;
if (is_next_byte_highlighted || (n + 1 == Cols))
{
highlight_width = s.HexCellWidth;
bg_width = s.HexCellWidth;
if (OptMidColsCount > 0 && n > 0 && (n + 1) < Cols && ((n + 1) % OptMidColsCount) == 0)
highlight_width += s.SpacingBetweenMidCols;
bg_width += s.SpacingBetweenMidCols;
}
draw_list->AddRectFilled(pos, ImVec2(pos.x + highlight_width, pos.y + s.LineHeight), HighlightColor);
ImVec2 pos = ImGui::GetCursorScreenPos();
draw_list->AddRectFilled(pos, ImVec2(pos.x + bg_width, pos.y + s.LineHeight), bg_color);
}
if (DataEditingAddr == addr)
@@ -308,17 +352,21 @@ struct MemoryEditor
if (DataEditingTakeFocus)
{
ImGui::SetKeyboardFocusHere(0);
sprintf(AddrInputBuf, format_data, s.AddrDigitsCount, base_display_addr + addr);
sprintf(DataInputBuf, format_byte, ReadFn ? ReadFn(mem_data, addr) : mem_data[addr]);
ImSnprintf(AddrInputBuf, 32, format_data, s.AddrDigitsCount, base_display_addr + addr);
ImSnprintf(DataInputBuf, 32, format_byte, ReadFn ? ReadFn(mem_data, addr, UserData) : mem_data[addr]);
}
struct UserData
struct InputTextUserData
{
// FIXME: We should have a way to retrieve the text edit cursor position more easily in the API, this is rather tedious. This is such a ugly mess we may be better off not using InputText() at all here.
static int Callback(ImGuiInputTextCallbackData* data)
{
UserData* user_data = (UserData*)data->UserData;
InputTextUserData* user_data = (InputTextUserData*)data->UserData;
if (!data->HasSelection())
user_data->CursorPos = data->CursorPos;
#if IMGUI_VERSION_NUM < 19102
if (data->Flags & ImGuiInputTextFlags_ReadOnly)
return 0;
#endif
if (data->SelectionStart == 0 && data->SelectionEnd == data->BufTextLen)
{
// When not editing a byte, always refresh its InputText content pulled from underlying memory data
@@ -334,39 +382,42 @@ struct MemoryEditor
char CurrentBufOverwrite[3]; // Input
int CursorPos; // Output
};
UserData user_data;
user_data.CursorPos = -1;
sprintf(user_data.CurrentBufOverwrite, format_byte, ReadFn ? ReadFn(mem_data, addr) : mem_data[addr]);
InputTextUserData input_text_user_data;
input_text_user_data.CursorPos = -1;
ImSnprintf(input_text_user_data.CurrentBufOverwrite, 3, format_byte, ReadFn ? ReadFn(mem_data, addr, UserData) : mem_data[addr]);
ImGuiInputTextFlags flags = ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_NoHorizontalScroll | ImGuiInputTextFlags_CallbackAlways;
#if IMGUI_VERSION_NUM >= 18104
flags |= ImGuiInputTextFlags_AlwaysOverwrite;
#else
flags |= ImGuiInputTextFlags_AlwaysInsertMode;
#endif
if (ReadOnly)
flags |= ImGuiInputTextFlags_ReadOnly;
flags |= ImGuiInputTextFlags_AlwaysOverwrite; // was ImGuiInputTextFlags_AlwaysInsertMode
ImGui::SetNextItemWidth(s.GlyphWidth * 2);
if (ImGui::InputText("##data", DataInputBuf, IM_ARRAYSIZE(DataInputBuf), flags, UserData::Callback, &user_data))
if (ImGui::InputText("##data", DataInputBuf, IM_ARRAYSIZE(DataInputBuf), flags, InputTextUserData::Callback, &input_text_user_data))
data_write = data_next = true;
else if (!DataEditingTakeFocus && !ImGui::IsItemActive())
DataEditingAddr = data_editing_addr_next = (size_t)-1;
DataEditingTakeFocus = false;
if (user_data.CursorPos >= 2)
if (input_text_user_data.CursorPos >= 2)
data_write = data_next = true;
if (data_editing_addr_next != (size_t)-1)
data_write = data_next = false;
unsigned int data_input_value = 0;
if (data_write && sscanf(DataInputBuf, "%X", &data_input_value) == 1)
if (!ReadOnly && data_write && sscanf(DataInputBuf, "%X", &data_input_value) == 1)
{
if (WriteFn)
WriteFn(mem_data, addr, (ImU8)data_input_value);
WriteFn(mem_data, addr, (ImU8)data_input_value, UserData);
else
mem_data[addr] = (ImU8)data_input_value;
}
if (ImGui::IsItemHovered())
{
MouseHovered = true;
MouseHoveredAddr = addr;
}
ImGui::PopID();
}
else
{
// NB: The trailing space is not visible but ensure there's no gap that the mouse cannot click on.
ImU8 b = ReadFn ? ReadFn(mem_data, addr) : mem_data[addr];
ImU8 b = ReadFn ? ReadFn(mem_data, addr, UserData) : mem_data[addr];
if (OptShowHexII)
{
@@ -386,10 +437,15 @@ struct MemoryEditor
else
ImGui::Text(format_byte_space, b);
}
if (!ReadOnly && ImGui::IsItemHovered() && ImGui::IsMouseClicked(0))
if (ImGui::IsItemHovered())
{
DataEditingTakeFocus = true;
data_editing_addr_next = addr;
MouseHovered = true;
MouseHoveredAddr = addr;
if (ImGui::IsMouseClicked(0))
{
DataEditingTakeFocus = true;
data_editing_addr_next = addr;
}
}
}
}
@@ -397,15 +453,24 @@ struct MemoryEditor
if (OptShowAscii)
{
// Draw ASCII values
ImGui::SameLine(s.PosAsciiStart);
ImGui::SameLine(s.OffsetAsciiMinX);
ImVec2 pos = ImGui::GetCursorScreenPos();
addr = line_i * Cols;
addr = (size_t)line_i * Cols;
const float mouse_off_x = ImGui::GetIO().MousePos.x - pos.x;
const size_t mouse_addr = (mouse_off_x >= 0.0f && mouse_off_x < s.OffsetAsciiMaxX - s.OffsetAsciiMinX) ? addr + (size_t)(mouse_off_x / s.GlyphWidth) : (size_t)-1;
ImGui::PushID(line_i);
if (ImGui::InvisibleButton("ascii", ImVec2(s.PosAsciiEnd - s.PosAsciiStart, s.LineHeight)))
if (ImGui::InvisibleButton("ascii", ImVec2(s.OffsetAsciiMaxX - s.OffsetAsciiMinX, s.LineHeight)))
{
DataEditingAddr = DataPreviewAddr = addr + (size_t)((ImGui::GetIO().MousePos.x - pos.x) / s.GlyphWidth);
DataEditingAddr = DataPreviewAddr = mouse_addr;
DataEditingTakeFocus = true;
}
if (ImGui::IsItemHovered())
{
MouseHovered = true;
MouseHoveredAddr = mouse_addr;
}
ImGui::PopID();
for (int n = 0; n < Cols && addr < mem_size; n++, addr++)
{
@@ -414,7 +479,11 @@ struct MemoryEditor
draw_list->AddRectFilled(pos, ImVec2(pos.x + s.GlyphWidth, pos.y + s.LineHeight), ImGui::GetColorU32(ImGuiCol_FrameBg));
draw_list->AddRectFilled(pos, ImVec2(pos.x + s.GlyphWidth, pos.y + s.LineHeight), ImGui::GetColorU32(ImGuiCol_TextSelectedBg));
}
unsigned char c = ReadFn ? ReadFn(mem_data, addr) : mem_data[addr];
else if (BgColorFn)
{
draw_list->AddRectFilled(pos, ImVec2(pos.x + s.GlyphWidth, pos.y + s.LineHeight), BgColorFn(mem_data, addr, UserData));
}
unsigned char c = ReadFn ? ReadFn(mem_data, addr, UserData) : mem_data[addr];
char display_c = (c < 32 || c >= 128) ? '.' : c;
draw_list->AddText(pos, (display_c == c) ? color_text : color_disabled, &display_c, &display_c + 1);
pos.x += s.GlyphWidth;
@@ -422,10 +491,14 @@ struct MemoryEditor
}
}
ImGui::PopStyleVar(2);
const float child_width = ImGui::GetWindowSize().x;
ImGui::EndChild();
// Notify the main window of our ideal child content size (FIXME: we are missing an API to get the contents size from the child)
ImVec2 backup_pos = ImGui::GetCursorScreenPos();
ImGui::SetCursorPosX(s.WindowWidth);
ImGui::Dummy(ImVec2(0.0f, 0.0f));
ImGui::SetCursorScreenPos(backup_pos);
if (data_next && DataEditingAddr + 1 < mem_size)
{
@@ -450,18 +523,28 @@ struct MemoryEditor
ImGui::Separator();
DrawPreviewLine(s, mem_data, mem_size, base_display_addr);
}
}
void DrawOptionsLine(const Sizes& s, void* mem_data, size_t mem_size, size_t base_display_addr)
{
IM_UNUSED(mem_data);
ImGuiStyle& style = ImGui::GetStyle();
const char* format_range = OptUpperCaseHex ? "Range %0*" _PRISizeT "X..%0*" _PRISizeT "X" : "Range %0*" _PRISizeT "x..%0*" _PRISizeT "x";
if (GotoAddr != (size_t)-1)
{
if (GotoAddr < mem_size)
{
ImGui::BeginChild("##scrolling");
ImGui::SetScrollY((GotoAddr / Cols) * ImGui::GetTextLineHeight() - avail_size.y * 0.5f);
ImGui::EndChild();
DataEditingAddr = DataPreviewAddr = GotoAddr;
DataEditingTakeFocus = true;
}
GotoAddr = (size_t)-1;
}
// Options menu
if (ImGui::Button("Options"))
ImGui::OpenPopup("context");
if (ImGui::BeginPopup("context"))
const ImVec2 contents_pos_end(contents_pos_start.x + child_width, ImGui::GetCursorScreenPos().y);
//ImGui::GetForegroundDrawList()->AddRect(contents_pos_start, contents_pos_end, IM_COL32(255, 0, 0, 255));
if (OptShowOptions)
if (ImGui::IsMouseHoveringRect(contents_pos_start, contents_pos_end))
if (ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows) && ImGui::IsMouseReleased(ImGuiMouseButton_Right))
ImGui::OpenPopup("OptionsPopup");
if (ImGui::BeginPopup("OptionsPopup"))
{
ImGui::SetNextItemWidth(s.GlyphWidth * 7 + style.FramePadding.x * 2.0f);
if (ImGui::DragInt("##cols", &Cols, 0.2f, 4, 32, "%d cols")) { ContentsWidthChanged = true; if (Cols < 1) Cols = 1; }
@@ -473,6 +556,17 @@ struct MemoryEditor
ImGui::EndPopup();
}
}
void DrawOptionsLine(const Sizes& s, void* mem_data, size_t mem_size, size_t base_display_addr)
{
IM_UNUSED(mem_data);
ImGuiStyle& style = ImGui::GetStyle();
const char* format_range = OptUpperCaseHex ? "Range %0*" _PRISizeT "X..%0*" _PRISizeT "X" : "Range %0*" _PRISizeT "x..%0*" _PRISizeT "x";
// Options menu
if (ImGui::Button("Options"))
ImGui::OpenPopup("OptionsPopup");
ImGui::SameLine();
ImGui::Text(format_range, s.AddrDigitsCount, base_display_addr, s.AddrDigitsCount, base_display_addr + mem_size - 1);
@@ -488,18 +582,11 @@ struct MemoryEditor
}
}
if (GotoAddr != (size_t)-1)
{
if (GotoAddr < mem_size)
{
ImGui::BeginChild("##scrolling");
ImGui::SetScrollFromPosY(ImGui::GetCursorStartPos().y + (GotoAddr / Cols) * ImGui::GetTextLineHeight());
ImGui::EndChild();
DataEditingAddr = DataPreviewAddr = GotoAddr;
DataEditingTakeFocus = true;
}
GotoAddr = (size_t)-1;
}
//if (MouseHovered)
//{
// ImGui::SameLine();
// ImGui::Text("Hovered: %p", MouseHoveredAddr);
//}
}
void DrawPreviewLine(const Sizes& s, void* mem_data_void, size_t mem_size, size_t base_display_addr)
@@ -511,16 +598,21 @@ struct MemoryEditor
ImGui::Text("Preview as:");
ImGui::SameLine();
ImGui::SetNextItemWidth((s.GlyphWidth * 10.0f) + style.FramePadding.x * 2.0f + style.ItemInnerSpacing.x);
static const ImGuiDataType supported_data_types[] = { ImGuiDataType_S8, ImGuiDataType_U8, ImGuiDataType_S16, ImGuiDataType_U16, ImGuiDataType_S32, ImGuiDataType_U32, ImGuiDataType_S64, ImGuiDataType_U64, ImGuiDataType_Float, ImGuiDataType_Double };
if (ImGui::BeginCombo("##combo_type", DataTypeGetDesc(PreviewDataType), ImGuiComboFlags_HeightLargest))
{
for (int n = 0; n < ImGuiDataType_COUNT; n++)
if (ImGui::Selectable(DataTypeGetDesc((ImGuiDataType)n), PreviewDataType == n))
PreviewDataType = (ImGuiDataType)n;
for (int n = 0; n < IM_ARRAYSIZE(supported_data_types); n++)
{
ImGuiDataType data_type = supported_data_types[n];
if (ImGui::Selectable(DataTypeGetDesc(data_type), PreviewDataType == data_type))
PreviewDataType = data_type;
}
ImGui::EndCombo();
}
ImGui::SameLine();
ImGui::SetNextItemWidth((s.GlyphWidth * 6.0f) + style.FramePadding.x * 2.0f + style.ItemInnerSpacing.x);
ImGui::Combo("##combo_endianess", &PreviewEndianess, "LE\0BE\0\0");
ImGui::Combo("##combo_endianness", &PreviewEndianness, "LE\0BE\0\0");
char buf[128] = "";
float x = s.GlyphWidth * 6.0f;
@@ -537,18 +629,19 @@ struct MemoryEditor
ImGui::Text("Bin"); ImGui::SameLine(x); ImGui::TextUnformatted(has_value ? buf : "N/A");
}
// Utilities for Data Preview
// Utilities for Data Preview (since we don't access imgui_internal.h)
// FIXME: This technically depends on ImGuiDataType order.
const char* DataTypeGetDesc(ImGuiDataType data_type) const
{
const char* descs[] = { "Int8", "Uint8", "Int16", "Uint16", "Int32", "Uint32", "Int64", "Uint64", "Float", "Double" };
IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT);
IM_ASSERT(data_type >= 0 && data_type < IM_ARRAYSIZE(descs));
return descs[data_type];
}
size_t DataTypeGetSize(ImGuiDataType data_type) const
{
const size_t sizes[] = { 1, 1, 2, 2, 4, 4, 8, 8, sizeof(float), sizeof(double) };
IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT);
IM_ASSERT(data_type >= 0 && data_type < IM_ARRAYSIZE(sizes));
return sizes[data_type];
}
@@ -561,18 +654,18 @@ struct MemoryEditor
bool IsBigEndian() const
{
uint16_t x = 1;
ImU16 x = 1;
char c[2];
memcpy(c, &x, 2);
return c[0] != 0;
}
static void* EndianessCopyBigEndian(void* _dst, void* _src, size_t s, int is_little_endian)
static void* EndiannessCopyBigEndian(void* _dst, void* _src, size_t s, int is_little_endian)
{
if (is_little_endian)
{
uint8_t* dst = (uint8_t*)_dst;
uint8_t* src = (uint8_t*)_src + s - 1;
ImU8* dst = (ImU8*)_dst;
ImU8* src = (ImU8*)_src + s - 1;
for (int i = 0, n = (int)s; i < n; ++i)
memcpy(dst++, src--, 1);
return _dst;
@@ -583,7 +676,7 @@ struct MemoryEditor
}
}
static void* EndianessCopyLittleEndian(void* _dst, void* _src, size_t s, int is_little_endian)
static void* EndiannessCopyLittleEndian(void* _dst, void* _src, size_t s, int is_little_endian)
{
if (is_little_endian)
{
@@ -591,23 +684,23 @@ struct MemoryEditor
}
else
{
uint8_t* dst = (uint8_t*)_dst;
uint8_t* src = (uint8_t*)_src + s - 1;
ImU8* dst = (ImU8*)_dst;
ImU8* src = (ImU8*)_src + s - 1;
for (int i = 0, n = (int)s; i < n; ++i)
memcpy(dst++, src--, 1);
return _dst;
}
}
void* EndianessCopy(void* dst, void* src, size_t size) const
void* EndiannessCopy(void* dst, void* src, size_t size) const
{
static void* (*fp)(void*, void*, size_t, int) = NULL;
if (fp == NULL)
fp = IsBigEndian() ? EndianessCopyBigEndian : EndianessCopyLittleEndian;
return fp(dst, src, size, PreviewEndianess);
static void* (*fp)(void*, void*, size_t, int) = nullptr;
if (fp == nullptr)
fp = IsBigEndian() ? EndiannessCopyBigEndian : EndiannessCopyLittleEndian;
return fp(dst, src, size, PreviewEndianness);
}
const char* FormatBinary(const uint8_t* buf, int width) const
const char* FormatBinary(const ImU8* buf, int width) const
{
IM_ASSERT(width <= 64);
size_t out_n = 0;
@@ -627,19 +720,19 @@ struct MemoryEditor
// [Internal]
void DrawPreviewData(size_t addr, const ImU8* mem_data, size_t mem_size, ImGuiDataType data_type, DataFormat data_format, char* out_buf, size_t out_buf_size) const
{
uint8_t buf[8];
ImU8 buf[8];
size_t elem_size = DataTypeGetSize(data_type);
size_t size = addr + elem_size > mem_size ? mem_size - addr : elem_size;
if (ReadFn)
for (int i = 0, n = (int)size; i < n; ++i)
buf[i] = ReadFn(mem_data, addr + i);
buf[i] = ReadFn(mem_data, addr + i, UserData);
else
memcpy(buf, mem_data + addr, size);
if (data_format == DataFormat_Bin)
{
uint8_t binbuf[8];
EndianessCopy(binbuf, buf, size);
ImU8 binbuf[8];
EndiannessCopy(binbuf, buf, size);
ImSnprintf(out_buf, out_buf_size, "%s", FormatBinary(binbuf, (int)size * 8));
return;
}
@@ -649,84 +742,85 @@ struct MemoryEditor
{
case ImGuiDataType_S8:
{
int8_t int8 = 0;
EndianessCopy(&int8, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%hhd", int8); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%02x", int8 & 0xFF); return; }
ImS8 data = 0;
EndiannessCopy(&data, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%hhd", data); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%02x", data & 0xFF); return; }
break;
}
case ImGuiDataType_U8:
{
uint8_t uint8 = 0;
EndianessCopy(&uint8, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%hhu", uint8); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%02x", uint8 & 0XFF); return; }
ImU8 data = 0;
EndiannessCopy(&data, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%hhu", data); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%02x", data & 0XFF); return; }
break;
}
case ImGuiDataType_S16:
{
int16_t int16 = 0;
EndianessCopy(&int16, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%hd", int16); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%04x", int16 & 0xFFFF); return; }
ImS16 data = 0;
EndiannessCopy(&data, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%hd", data); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%04x", data & 0xFFFF); return; }
break;
}
case ImGuiDataType_U16:
{
uint16_t uint16 = 0;
EndianessCopy(&uint16, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%hu", uint16); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%04x", uint16 & 0xFFFF); return; }
ImU16 data = 0;
EndiannessCopy(&data, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%hu", data); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%04x", data & 0xFFFF); return; }
break;
}
case ImGuiDataType_S32:
{
int32_t int32 = 0;
EndianessCopy(&int32, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%d", int32); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%08x", int32); return; }
ImS32 data = 0;
EndiannessCopy(&data, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%d", data); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%08x", data); return; }
break;
}
case ImGuiDataType_U32:
{
uint32_t uint32 = 0;
EndianessCopy(&uint32, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%u", uint32); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%08x", uint32); return; }
ImU32 data = 0;
EndiannessCopy(&data, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%u", data); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%08x", data); return; }
break;
}
case ImGuiDataType_S64:
{
int64_t int64 = 0;
EndianessCopy(&int64, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%lld", (long long)int64); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%016llx", (long long)int64); return; }
ImS64 data = 0;
EndiannessCopy(&data, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%lld", (long long)data); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%016llx", (long long)data); return; }
break;
}
case ImGuiDataType_U64:
{
uint64_t uint64 = 0;
EndianessCopy(&uint64, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%llu", (long long)uint64); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%016llx", (long long)uint64); return; }
ImU64 data = 0;
EndiannessCopy(&data, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%llu", (long long)data); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%016llx", (long long)data); return; }
break;
}
case ImGuiDataType_Float:
{
float float32 = 0.0f;
EndianessCopy(&float32, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%f", float32); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "%a", float32); return; }
float data = 0.0f;
EndiannessCopy(&data, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%f", data); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "%a", data); return; }
break;
}
case ImGuiDataType_Double:
{
double float64 = 0.0;
EndianessCopy(&float64, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%f", float64); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "%a", float64); return; }
double data = 0.0;
EndiannessCopy(&data, buf, size);
if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%f", data); return; }
if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "%a", data); return; }
break;
}
default:
case ImGuiDataType_COUNT:
break;
} // Switch
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -315,7 +315,7 @@ static int stbrp__skyline_find_min_y(stbrp_context *c, stbrp_node *first, int x0
if (node->y > min_y) {
// raise min_y higher.
// we've accounted for all waste up to min_y,
// but we'll now add more waste for everything we've visted
// but we'll now add more waste for everything we've visited
waste_area += visited_width * (node->y - min_y);
min_y = node->y;
// the first time through, visited_width might be reduced
@@ -470,7 +470,7 @@ static stbrp__findresult stbrp__skyline_pack_rectangle(stbrp_context *context, i
// insert the new node into the right starting point, and
// let 'cur' point to the remaining nodes needing to be
// stiched back in
// stitched back in
cur = *res.prev_link;
if (cur->x < res.x) {
+113 -55
View File
@@ -5,7 +5,8 @@
// - Fix in stb_textedit_find_charpos to handle last line (see https://github.com/ocornut/imgui/issues/6000 + #6783)
// - Added name to struct or it may be forward declared in our code.
// - Added UTF-8 support (see https://github.com/nothings/stb/issues/188 + https://github.com/ocornut/imgui/pull/7925)
// Grep for [DEAR IMGUI] to find the changes.
// - Changed STB_TEXTEDIT_INSERTCHARS() to return inserted count (instead of 0/1 bool), allowing partial insertion.
// Grep for [DEAR IMGUI] to find some changes.
// - Also renamed macros used or defined outside of IMSTB_TEXTEDIT_IMPLEMENTATION block from STB_TEXTEDIT_* to IMSTB_TEXTEDIT_*
// stb_textedit.h - v1.14 - public domain - Sean Barrett
@@ -39,6 +40,7 @@
//
// VERSION HISTORY
//
// !!!! (2025-10-23) changed STB_TEXTEDIT_INSERTCHARS() to return inserted count (instead of 0/1 bool), allowing partial insertion.
// 1.14 (2021-07-11) page up/down, various fixes
// 1.13 (2019-02-07) fix bug in undo size management
// 1.12 (2018-01-29) user can change STB_TEXTEDIT_KEYTYPE, fix redo to avoid crash
@@ -141,12 +143,14 @@
// with previous char)
// STB_TEXTEDIT_KEYTOTEXT(k) maps a keyboard input to an insertable character
// (return type is int, -1 means not valid to insert)
// (not supported if you want to use UTF-8, see below)
// STB_TEXTEDIT_GETCHAR(obj,i) returns the i'th character of obj, 0-based
// STB_TEXTEDIT_NEWLINE the character returned by _GETCHAR() we recognize
// as manually wordwrapping for end-of-line positioning
//
// STB_TEXTEDIT_DELETECHARS(obj,i,n) delete n characters starting at i
// STB_TEXTEDIT_INSERTCHARS(obj,i,c*,n) insert n characters at i (pointed to by STB_TEXTEDIT_CHARTYPE*)
// STB_TEXTEDIT_INSERTCHARS(obj,i,c*,n) try to insert n characters at i (pointed to by STB_TEXTEDIT_CHARTYPE*)
// returns number of characters actually inserted. [DEAR IMGUI]
//
// STB_TEXTEDIT_K_SHIFT a power of two that is or'd in to a keyboard input to represent the shift key
//
@@ -178,6 +182,13 @@
// STB_TEXTEDIT_K_TEXTSTART2 secondary keyboard input to move cursor to start of text
// STB_TEXTEDIT_K_TEXTEND2 secondary keyboard input to move cursor to end of text
//
// To support UTF-8:
//
// STB_TEXTEDIT_GETPREVCHARINDEX returns index of previous character
// STB_TEXTEDIT_GETNEXTCHARINDEX returns index of next character
// Do NOT define STB_TEXTEDIT_KEYTOTEXT.
// Instead, call stb_textedit_text() directly for text contents.
//
// Keyboard input must be encoded as a single integer value; e.g. a character code
// and some bitflags that represent shift states. to simplify the interface, SHIFT must
// be a bitflag, so we can test the shifted state of cursor movements to allow selection,
@@ -250,8 +261,10 @@
// if the STB_TEXTEDIT_KEYTOTEXT function is defined, selected keys are
// transformed into text and stb_textedit_text() is automatically called.
//
// text: [DEAR IMGUI] added 2024-09
// call this to text inputs sent to the textfield.
// text: (added 2025)
// call this to directly send text input the textfield, which is required
// for UTF-8 support, because stb_textedit_key() + STB_TEXTEDIT_KEYTOTEXT()
// cannot infer text length.
//
//
// When rendering, you can read the cursor position and selection state from
@@ -400,6 +413,16 @@ typedef struct
#define IMSTB_TEXTEDIT_memmove memmove
#endif
// [DEAR IMGUI]
// Functions must be implemented for UTF8 support
// Code in this file that uses those functions is modified for [DEAR IMGUI] and deviates from the original stb_textedit.
// There is not necessarily a '[DEAR IMGUI]' at the usage sites.
#ifndef IMSTB_TEXTEDIT_GETPREVCHARINDEX
#define IMSTB_TEXTEDIT_GETPREVCHARINDEX(OBJ, IDX) ((IDX) - 1)
#endif
#ifndef IMSTB_TEXTEDIT_GETNEXTCHARINDEX
#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX(OBJ, IDX) ((IDX) + 1)
#endif
/////////////////////////////////////////////////////////////////////////////
//
@@ -407,7 +430,7 @@ typedef struct
//
// traverse the layout to locate the nearest character to a display position
static int stb_text_locate_coord(IMSTB_TEXTEDIT_STRING *str, float x, float y)
static int stb_text_locate_coord(IMSTB_TEXTEDIT_STRING *str, float x, float y, int* out_side_on_line)
{
StbTexteditRow r;
int n = STB_TEXTEDIT_STRINGLEN(str);
@@ -417,6 +440,7 @@ static int stb_text_locate_coord(IMSTB_TEXTEDIT_STRING *str, float x, float y)
r.x0 = r.x1 = 0;
r.ymin = r.ymax = 0;
r.num_chars = 0;
*out_side_on_line = 0;
// search rows to find one that straddles 'y'
while (i < n) {
@@ -436,7 +460,10 @@ static int stb_text_locate_coord(IMSTB_TEXTEDIT_STRING *str, float x, float y)
// below all text, return 'after' last character
if (i >= n)
{
*out_side_on_line = 1;
return n;
}
// check if it's before the beginning of the line
if (x < r.x0)
@@ -449,6 +476,7 @@ static int stb_text_locate_coord(IMSTB_TEXTEDIT_STRING *str, float x, float y)
for (k=0; k < r.num_chars; k = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, i + k) - i) {
float w = STB_TEXTEDIT_GETWIDTH(str, i, k);
if (x < prev_x+w) {
*out_side_on_line = (k == 0) ? 0 : 1;
if (x < prev_x+w/2)
return k+i;
else
@@ -460,6 +488,7 @@ static int stb_text_locate_coord(IMSTB_TEXTEDIT_STRING *str, float x, float y)
}
// if the last character is a newline, return that. otherwise return 'after' the last character
*out_side_on_line = 1;
if (STB_TEXTEDIT_GETCHAR(str, i+r.num_chars-1) == STB_TEXTEDIT_NEWLINE)
return i+r.num_chars-1;
else
@@ -471,6 +500,7 @@ static void stb_textedit_click(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *st
{
// In single-line mode, just always make y = 0. This lets the drag keep working if the mouse
// goes off the top or bottom of the text
int side_on_line;
if( state->single_line )
{
StbTexteditRow r;
@@ -478,16 +508,18 @@ static void stb_textedit_click(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *st
y = r.ymin;
}
state->cursor = stb_text_locate_coord(str, x, y);
state->cursor = stb_text_locate_coord(str, x, y, &side_on_line);
state->select_start = state->cursor;
state->select_end = state->cursor;
state->has_preferred_x = 0;
str->LastMoveDirectionLR = (ImS8)(side_on_line ? ImGuiDir_Right : ImGuiDir_Left);
}
// API drag: on mouse drag, move the cursor and selection endpoint to the clicked location
static void stb_textedit_drag(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y)
{
int p = 0;
int side_on_line;
// In single-line mode, just always make y = 0. This lets the drag keep working if the mouse
// goes off the top or bottom of the text
@@ -501,8 +533,9 @@ static void stb_textedit_drag(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *sta
if (state->select_start == state->select_end)
state->select_start = state->cursor;
p = stb_text_locate_coord(str, x, y);
p = stb_text_locate_coord(str, x, y, &side_on_line);
state->cursor = state->select_end = p;
str->LastMoveDirectionLR = (ImS8)(side_on_line ? ImGuiDir_Right : ImGuiDir_Left);
}
/////////////////////////////////////////////////////////////////////////////
@@ -552,6 +585,8 @@ static void stb_textedit_find_charpos(StbFindState *find, IMSTB_TEXTEDIT_STRING
STB_TEXTEDIT_LAYOUTROW(&r, str, i);
if (n < i + r.num_chars)
break;
if (str->LastMoveDirectionLR == ImGuiDir_Right && str->Stb->cursor > 0 && str->Stb->cursor == i + r.num_chars && STB_TEXTEDIT_GETCHAR(str, i + r.num_chars - 1) != STB_TEXTEDIT_NEWLINE) // [DEAR IMGUI] Wrapping point handling
break;
if (i + r.num_chars == z && z > 0 && STB_TEXTEDIT_GETCHAR(str, z - 1) != STB_TEXTEDIT_NEWLINE) // [DEAR IMGUI] special handling for last line
break; // [DEAR IMGUI]
prev_start = i;
@@ -648,15 +683,33 @@ static void stb_textedit_move_to_last(IMSTB_TEXTEDIT_STRING *str, STB_TexteditSt
}
}
// [DEAR IMGUI]
// Functions must be implemented for UTF8 support
// Code in this file that uses those functions is modified for [DEAR IMGUI] and deviates from the original stb_textedit.
// There is not necessarily a '[DEAR IMGUI]' at the usage sites.
#ifndef IMSTB_TEXTEDIT_GETPREVCHARINDEX
#define IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx) (idx - 1)
// [DEAR IMGUI] Extracted this function so we can more easily add support for word-wrapping.
#ifndef STB_TEXTEDIT_MOVELINESTART
static int stb_textedit_move_line_start(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int cursor)
{
if (state->single_line)
return 0;
while (cursor > 0) {
int prev = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, cursor);
if (STB_TEXTEDIT_GETCHAR(str, prev) == STB_TEXTEDIT_NEWLINE)
break;
cursor = prev;
}
return cursor;
}
#define STB_TEXTEDIT_MOVELINESTART stb_textedit_move_line_start
#endif
#ifndef IMSTB_TEXTEDIT_GETNEXTCHARINDEX
#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx) (idx + 1)
#ifndef STB_TEXTEDIT_MOVELINEEND
static int stb_textedit_move_line_end(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int cursor)
{
int n = STB_TEXTEDIT_STRINGLEN(str);
if (state->single_line)
return n;
while (cursor < n && STB_TEXTEDIT_GETCHAR(str, cursor) != STB_TEXTEDIT_NEWLINE)
cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, cursor);
return cursor;
}
#define STB_TEXTEDIT_MOVELINEEND stb_textedit_move_line_end
#endif
#ifdef STB_TEXTEDIT_IS_SPACE
@@ -668,9 +721,9 @@ static int is_word_boundary( IMSTB_TEXTEDIT_STRING *str, int idx )
#ifndef STB_TEXTEDIT_MOVEWORDLEFT
static int stb_textedit_move_to_word_previous( IMSTB_TEXTEDIT_STRING *str, int c )
{
--c; // always move at least one character
while( c >= 0 && !is_word_boundary( str, c ) )
--c;
c = IMSTB_TEXTEDIT_GETPREVCHARINDEX( str, c ); // always move at least one character
while (c >= 0 && !is_word_boundary(str, c))
c = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, c);
if( c < 0 )
c = 0;
@@ -684,9 +737,9 @@ static int stb_textedit_move_to_word_previous( IMSTB_TEXTEDIT_STRING *str, int c
static int stb_textedit_move_to_word_next( IMSTB_TEXTEDIT_STRING *str, int c )
{
const int len = STB_TEXTEDIT_STRINGLEN(str);
++c; // always move at least one character
c = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, c); // always move at least one character
while( c < len && !is_word_boundary( str, c ) )
++c;
c = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, c);
if( c > len )
c = len;
@@ -725,7 +778,8 @@ static int stb_textedit_paste_internal(IMSTB_TEXTEDIT_STRING *str, STB_TexteditS
stb_textedit_clamp(str, state);
stb_textedit_delete_selection(str,state);
// try to insert the characters
if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, text, len)) {
len = STB_TEXTEDIT_INSERTCHARS(str, state->cursor, text, len);
if (len) {
stb_text_makeundo_insert(state, state->cursor, len);
state->cursor += len;
state->has_preferred_x = 0;
@@ -739,6 +793,7 @@ static int stb_textedit_paste_internal(IMSTB_TEXTEDIT_STRING *str, STB_TexteditS
#define STB_TEXTEDIT_KEYTYPE int
#endif
// API key: process text input
// [DEAR IMGUI] Added stb_textedit_text(), extracted out and called by stb_textedit_key() for backward compatibility.
static void stb_textedit_text(IMSTB_TEXTEDIT_STRING* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len)
{
@@ -749,14 +804,15 @@ static void stb_textedit_text(IMSTB_TEXTEDIT_STRING* str, STB_TexteditState* sta
if (state->insert_mode && !STB_TEXT_HAS_SELECTION(state) && state->cursor < STB_TEXTEDIT_STRINGLEN(str)) {
stb_text_makeundo_replace(str, state, state->cursor, 1, 1);
STB_TEXTEDIT_DELETECHARS(str, state->cursor, 1);
if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, text, text_len)) {
text_len = STB_TEXTEDIT_INSERTCHARS(str, state->cursor, text, text_len);
if (text_len) {
state->cursor += text_len;
state->has_preferred_x = 0;
}
}
else {
} else {
stb_textedit_delete_selection(str, state); // implicitly clamps
if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, text, text_len)) {
text_len = STB_TEXTEDIT_INSERTCHARS(str, state->cursor, text, text_len);
if (text_len) {
stb_text_makeundo_insert(state, state->cursor, text_len);
state->cursor += text_len;
state->has_preferred_x = 0;
@@ -771,6 +827,7 @@ retry:
switch (key) {
default: {
#ifdef STB_TEXTEDIT_KEYTOTEXT
// This is not suitable for UTF-8 support.
int c = STB_TEXTEDIT_KEYTOTEXT(key);
if (c > 0) {
IMSTB_TEXTEDIT_CHARTYPE ch = (IMSTB_TEXTEDIT_CHARTYPE)c;
@@ -911,15 +968,16 @@ retry:
// [DEAR IMGUI]
// going down while being on the last line shouldn't bring us to that line end
if (STB_TEXTEDIT_GETCHAR(str, find.first_char + find.length - 1) != STB_TEXTEDIT_NEWLINE)
break;
//if (STB_TEXTEDIT_GETCHAR(str, find.first_char + find.length - 1) != STB_TEXTEDIT_NEWLINE)
// break;
// now find character position down a row
state->cursor = start;
STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor);
x = row.x0;
for (i=0; i < row.num_chars; ++i) {
for (i=0; i < row.num_chars; ) {
float dx = STB_TEXTEDIT_GETWIDTH(str, start, i);
int next = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor);
#ifdef IMSTB_TEXTEDIT_GETWIDTH_NEWLINE
if (dx == IMSTB_TEXTEDIT_GETWIDTH_NEWLINE)
break;
@@ -927,10 +985,13 @@ retry:
x += dx;
if (x > goal_x)
break;
state->cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor);
i += next - state->cursor;
state->cursor = next;
}
stb_textedit_clamp(str, state);
if (state->cursor == find.first_char + find.length)
str->LastMoveDirectionLR = ImGuiDir_Left;
state->has_preferred_x = 1;
state->preferred_x = goal_x;
@@ -980,8 +1041,9 @@ retry:
state->cursor = find.prev_first;
STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor);
x = row.x0;
for (i=0; i < row.num_chars; ++i) {
for (i=0; i < row.num_chars; ) {
float dx = STB_TEXTEDIT_GETWIDTH(str, find.prev_first, i);
int next = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor);
#ifdef IMSTB_TEXTEDIT_GETWIDTH_NEWLINE
if (dx == IMSTB_TEXTEDIT_GETWIDTH_NEWLINE)
break;
@@ -989,10 +1051,15 @@ retry:
x += dx;
if (x > goal_x)
break;
state->cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor);
i += next - state->cursor;
state->cursor = next;
}
stb_textedit_clamp(str, state);
if (state->cursor == find.first_char)
str->LastMoveDirectionLR = ImGuiDir_Right;
else if (state->cursor == find.prev_first)
str->LastMoveDirectionLR = ImGuiDir_Left;
state->has_preferred_x = 1;
state->preferred_x = goal_x;
@@ -1002,10 +1069,15 @@ retry:
// go to previous line
// (we need to scan previous line the hard way. maybe we could expose this as a new API function?)
prev_scan = find.prev_first > 0 ? find.prev_first - 1 : 0;
while (prev_scan > 0 && STB_TEXTEDIT_GETCHAR(str, prev_scan - 1) != STB_TEXTEDIT_NEWLINE)
--prev_scan;
while (prev_scan > 0)
{
int prev = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, prev_scan);
if (STB_TEXTEDIT_GETCHAR(str, prev) == STB_TEXTEDIT_NEWLINE)
break;
prev_scan = prev;
}
find.first_char = find.prev_first;
find.prev_first = prev_scan;
find.prev_first = STB_TEXTEDIT_MOVELINESTART(str, state, prev_scan);
}
break;
}
@@ -1079,10 +1151,7 @@ retry:
case STB_TEXTEDIT_K_LINESTART:
stb_textedit_clamp(str, state);
stb_textedit_move_to_first(state);
if (state->single_line)
state->cursor = 0;
else while (state->cursor > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor-1) != STB_TEXTEDIT_NEWLINE)
--state->cursor;
state->cursor = STB_TEXTEDIT_MOVELINESTART(str, state, state->cursor);
state->has_preferred_x = 0;
break;
@@ -1090,13 +1159,9 @@ retry:
case STB_TEXTEDIT_K_LINEEND2:
#endif
case STB_TEXTEDIT_K_LINEEND: {
int n = STB_TEXTEDIT_STRINGLEN(str);
stb_textedit_clamp(str, state);
stb_textedit_move_to_first(state);
if (state->single_line)
state->cursor = n;
else while (state->cursor < n && STB_TEXTEDIT_GETCHAR(str, state->cursor) != STB_TEXTEDIT_NEWLINE)
++state->cursor;
stb_textedit_move_to_last(str, state);
state->cursor = STB_TEXTEDIT_MOVELINEEND(str, state, state->cursor);
state->has_preferred_x = 0;
break;
}
@@ -1107,10 +1172,7 @@ retry:
case STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT:
stb_textedit_clamp(str, state);
stb_textedit_prep_selection_at_cursor(state);
if (state->single_line)
state->cursor = 0;
else while (state->cursor > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor-1) != STB_TEXTEDIT_NEWLINE)
--state->cursor;
state->cursor = STB_TEXTEDIT_MOVELINESTART(str, state, state->cursor);
state->select_end = state->cursor;
state->has_preferred_x = 0;
break;
@@ -1119,13 +1181,9 @@ retry:
case STB_TEXTEDIT_K_LINEEND2 | STB_TEXTEDIT_K_SHIFT:
#endif
case STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT: {
int n = STB_TEXTEDIT_STRINGLEN(str);
stb_textedit_clamp(str, state);
stb_textedit_prep_selection_at_cursor(state);
if (state->single_line)
state->cursor = n;
else while (state->cursor < n && STB_TEXTEDIT_GETCHAR(str, state->cursor) != STB_TEXTEDIT_NEWLINE)
++state->cursor;
state->cursor = STB_TEXTEDIT_MOVELINEEND(str, state, state->cursor);
state->select_end = state->cursor;
state->has_preferred_x = 0;
break;
@@ -1300,7 +1358,7 @@ static void stb_text_undo(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state)
// check type of recorded action:
if (u.insert_length) {
// easy case: was a deletion, so we need to insert n characters
STB_TEXTEDIT_INSERTCHARS(str, u.where, &s->undo_char[u.char_storage], u.insert_length);
u.insert_length = STB_TEXTEDIT_INSERTCHARS(str, u.where, &s->undo_char[u.char_storage], u.insert_length);
s->undo_char_point -= u.insert_length;
}
@@ -1351,7 +1409,7 @@ static void stb_text_redo(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state)
if (r.insert_length) {
// easy case: need to insert n characters
STB_TEXTEDIT_INSERTCHARS(str, r.where, &s->undo_char[r.char_storage], r.insert_length);
r.insert_length = STB_TEXTEDIT_INSERTCHARS(str, r.where, &s->undo_char[r.char_storage], r.insert_length);
s->redo_char_point += r.insert_length;
}
+3 -3
View File
@@ -886,7 +886,7 @@ STBTT_DEF unsigned char *stbtt_GetCodepointBitmap(const stbtt_fontinfo *info, fl
// xoff/yoff are the offset it pixel space from the glyph origin to the top-left of the bitmap
STBTT_DEF unsigned char *stbtt_GetCodepointBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint, int *width, int *height, int *xoff, int *yoff);
// the same as stbtt_GetCodepoitnBitmap, but you can specify a subpixel
// the same as stbtt_GetCodepointBitmap, but you can specify a subpixel
// shift for the character
STBTT_DEF void stbtt_MakeCodepointBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int codepoint);
@@ -4516,8 +4516,8 @@ static int stbtt__compute_crossings_x(float x, float y, int nverts, stbtt_vertex
q2[0] = (float)x2;
q2[1] = (float)y2;
if (equal(q0,q1) || equal(q1,q2)) {
x0 = (int)verts[i-1].x;
y0 = (int)verts[i-1].y;
x0 = (int)verts[i-1].x; //-V1048
y0 = (int)verts[i-1].y; //-V1048
x1 = (int)verts[i ].x;
y1 = (int)verts[i ].y;
if (y > STBTT_min(y0,y1) && y < STBTT_max(y0,y1) && x > STBTT_min(x0,x1)) {
+1
View File
@@ -39,6 +39,7 @@
#define ROBIN_HOOD_VERSION_PATCH 2 // for backwards-compatible bug fixes
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <functional>
+6 -5
View File
@@ -38,11 +38,12 @@ std::vector<Analog> &games::bc::get_analogs() {
if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Busou Shinki: Armored Princess Battle Conductor");
GameAPI::Analogs::sortAnalogs(
&analogs,
"Stick X",
"Stick Y"
);
using GameAPI::Analogs::AnalogType;
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
{ "Stick X", AnalogType::LinearCentered },
{ "Stick Y", AnalogType::LinearCentered }
});
}
return analogs;
+14
View File
@@ -1,5 +1,7 @@
#include "bi2x_hook.h"
#if SPICE64
#include <cstdint>
#include "util/detour.h"
#include "util/logging.h"
@@ -16,15 +18,19 @@ namespace games::ccj {
*/
struct AIO_SCI_COMM {
uint8_t data[0xf8];
};
struct AIO_NMGR_IOB2 {
uint8_t data[0xe30];
};
struct AIO_IOB2_BI2X_TBS {
uint8_t data[0x4680];
};
struct AIO_IOB2_BI2X_WRFIRM {
uint8_t data[0x20f48];
};
struct AIO_NMGR_IOB__NODEINFO {
@@ -72,6 +78,12 @@ namespace games::ccj {
AIO_IOB2_BI2X_AC1__OUTPUTDATA OutputData;
};
// checked with UJK-001-2024060400
static_assert(sizeof(AIO_SCI_COMM) == 0xf8);
static_assert(sizeof(AIO_NMGR_IOB2) == 0xe30);
static_assert(sizeof(AIO_IOB2_BI2X_TBS) == 0x4680);
static_assert(sizeof(AIO_IOB2_BI2X_WRFIRM) == 0x20f48);
static void write_iccr_led(Lights::ccj_lights_t light, uint8_t value);
/*
@@ -622,3 +634,5 @@ namespace games::ccj {
aioNodeCtl_IsError, &aioNodeCtl_IsError_orig);
}
}
#endif
+4
View File
@@ -1,5 +1,9 @@
#pragma once
#if SPICE64
namespace games::ccj {
void bi2x_hook_init();
}
#endif
+8 -1
View File
@@ -7,7 +7,6 @@
#include "util/utils.h"
#include "util/detour.h"
#include "acioemu/handle.h"
#include "misc/wintouchemu.h"
#include "hooks/graphics/graphics.h"
#include "bi2x_hook.h"
#include "trackball.h"
@@ -65,12 +64,20 @@ namespace games::ccj {
libutils::load_library("libaio-iob2_video.dll");
libutils::load_library("win10actlog.dll");
// we do this to prevent this unexpectedly being unloaded
// which leads to D3D10Warp.dll_unloaded crash
libutils::load_library("d3d10warp.dll");
detour::trampoline_try("execexe.dll", MAKEINTRESOURCE(11),
(void*)execexe_CreateFileW_hook,(void**)&execexe_CreateFileW_orig);
#if SPICE64
// insert BI2X hooks
bi2x_hook_init();
#endif
// insert trackball hooks
trackball_hook_init();
+8 -7
View File
@@ -45,13 +45,14 @@ std::vector<Analog> &games::ccj::get_analogs() {
if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Chase Chase Jokers");
GameAPI::Analogs::sortAnalogs(
&analogs,
"Joystick X",
"Joystick Y",
"Trackball DX",
"Trackball DY"
);
using GameAPI::Analogs::AnalogType;
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
{ "Joystick X", AnalogType::LinearCentered },
{ "Joystick Y", AnalogType::LinearCentered },
{ "Trackball DX", AnalogType::LinearCentered },
{ "Trackball DY", AnalogType::LinearCentered }
});
}
return analogs;
+165 -102
View File
@@ -6,6 +6,7 @@
#include "util/logging.h"
#include "rawinput/rawinput.h"
#include "games/io.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include "io.h"
@@ -32,13 +33,14 @@ namespace games::ccj {
static UINT WINAPI GetRawInputDeviceList_hook(PRAWINPUTDEVICELIST pRawInputDeviceList, PUINT puiNumDevices,
UINT cbSize) {
auto result = GetRawInputDeviceList_orig(pRawInputDeviceList, puiNumDevices, cbSize);
if (result == 0xFFFFFFFF)
if (result == 0xFFFFFFFF) {
return result;
}
if (pRawInputDeviceList == NULL) {
(*puiNumDevices)++;
} else if (result < *puiNumDevices) {
pRawInputDeviceList[result] = {fakeHandle, 0};
pRawInputDeviceList[result] = { fakeHandle, 0 };
result++;
}
@@ -46,8 +48,9 @@ namespace games::ccj {
}
static UINT WINAPI GetRawInputDeviceInfoW_hook(HANDLE hDevice, UINT uiCommand, LPVOID pData, PUINT pcbSize) {
if (hDevice != fakeHandle || uiCommand != RIDI_DEVICENAME)
if (hDevice != fakeHandle || uiCommand != RIDI_DEVICENAME) {
return GetRawInputDeviceInfoW_orig(hDevice, uiCommand, pData, pcbSize);
}
const auto requiredLen = (wcslen(fakeDeviceName) + 1) * sizeof(wchar_t);
@@ -67,23 +70,157 @@ namespace games::ccj {
static LONG_PTR WINAPI SetWindowLongPtrW_hook(HWND _hWnd, int nIndex, LONG_PTR dwNewLong) {
wchar_t buffer[256];
if (nIndex != GWLP_WNDPROC || GetWindowTextW(_hWnd, buffer, 256) == 0 || !wcswcs(buffer, windowName))
if (nIndex != GWLP_WNDPROC || GetWindowTextW(_hWnd, buffer, 256) == 0 || !wcswcs(buffer, windowName)) {
return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong);
}
hWnd = _hWnd;
wndProc = (WNDPROC)dwNewLong;
return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong);
}
// compute the cursor wrap region in client coordinates. The cursor is confined to the
// monitor, so if the window's client area extends past a screen edge (window larger than /
// offset off the monitor) the cursor can never reach that far client edge. Clamp the region
// to the on-screen portion of the client area so the right/bottom edges wrap as reliably as
// the left/top edges.
static RECT trackball_wrap_bounds(HWND wnd, const RECT &client) {
RECT bounds = client;
POINT origin = { 0, 0 };
ClientToScreen(wnd, &origin);
RECT clientScreen = {
origin.x,
origin.y,
origin.x + client.right,
origin.y + client.bottom
};
MONITORINFO mi = {};
mi.cbSize = sizeof(mi);
RECT usable;
if (GetMonitorInfo(MonitorFromWindow(wnd, MONITOR_DEFAULTTONEAREST), &mi)
&& IntersectRect(&usable, &clientScreen, &mi.rcMonitor)) {
bounds.left = usable.left - origin.x;
bounds.top = usable.top - origin.y;
bounds.right = usable.right - origin.x;
bounds.bottom = usable.bottom - origin.y;
}
return bounds;
}
// drive the trackball from the physical mouse cursor, wrapping it at the window edges so it
// can spin indefinitely. gated by the secondary-mouse button (hold or debounced toggle).
static void trackball_mouse_input(RAWMOUSE &rawMouse) {
static bool active = false;
static bool lastState = false;
static std::chrono::steady_clock::time_point lastModified = std::chrono::steady_clock::now();
static const std::chrono::milliseconds debounceDuration(100);
const auto currentTime = std::chrono::steady_clock::now();
const bool pressed = get_async_secondary_mouse();
const bool focused = GetForegroundWindow() == hWnd;
if (focused && MOUSE_TRACKBALL_USE_TOGGLE && pressed && (currentTime - lastModified > debounceDuration)) {
active = !active;
lastModified = currentTime;
}
const bool engaged = focused
&& ((MOUSE_TRACKBALL_USE_TOGGLE && active) || (!MOUSE_TRACKBALL_USE_TOGGLE && pressed));
if (!engaged) {
if (lastState && !active) {
lastState = false;
}
return;
}
POINT cursor;
RECT client;
GetClientRect(hWnd, &client);
GetCursorPos(&cursor);
ScreenToClient(hWnd, &cursor);
const RECT bounds = trackball_wrap_bounds(hWnd, client);
static int lastX = cursor.x;
static int lastY = cursor.y;
if (!lastState) {
lastX = cursor.x;
lastY = cursor.y;
lastState = true;
}
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
rawMouse.lLastX = (int)((float)(cursor.x - lastX) * (float)TRACKBALL_SENSITIVITY / 20.0f);
rawMouse.lLastY = (int)((float)(lastY - cursor.y) * (float)TRACKBALL_SENSITIVITY / 20.0f);
// wrap the cursor to the opposite edge once it reaches a boundary, so the trackball
// can keep spinning past the screen edge.
bool updateCursor = false;
auto wrap = [&updateCursor](LONG value, LONG lo, LONG hi) -> LONG {
if (value <= lo) {
updateCursor = true;
return hi - 5;
}
if (value >= hi - 1) {
updateCursor = true;
return lo + 5;
}
return value;
};
cursor.x = wrap(cursor.x, bounds.left, bounds.right);
cursor.y = wrap(cursor.y, bounds.top, bounds.bottom);
lastX = cursor.x;
lastY = cursor.y;
if (updateCursor) {
ClientToScreen(hWnd, &cursor);
SetCursorPos(cursor.x, cursor.y);
}
}
// drive the trackball from the configured analog axes / direction buttons.
static void trackball_mapped_input(RAWMOUSE &rawMouse) {
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
auto &analogs = get_analogs();
if (analogs[Analogs::Trackball_DX].isSet() || analogs[Analogs::Trackball_DY].isSet()) {
float x = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DX]) * 2.0f - 1.0f;
float y = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DY]) * 2.0f - 1.0f;
rawMouse.lLastX = (long) (x * (float) TRACKBALL_SENSITIVITY);
rawMouse.lLastY = (long) (-y * (float) TRACKBALL_SENSITIVITY);
}
auto &buttons = get_buttons();
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Up])) {
rawMouse.lLastY = TRACKBALL_SENSITIVITY;
}
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Down])) {
rawMouse.lLastY = -TRACKBALL_SENSITIVITY;
}
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Left])) {
rawMouse.lLastX = -TRACKBALL_SENSITIVITY;
}
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Right])) {
rawMouse.lLastX = TRACKBALL_SENSITIVITY;
}
}
static UINT WINAPI GetRawInputData_hook(HRAWINPUT hRawInput, UINT uiCommand, LPVOID pData, PUINT pcbSize, UINT cbSizeHeader) {
if (hRawInput != fakeHandle)
if (hRawInput != fakeHandle) {
return GetRawInputData_orig(hRawInput, uiCommand, pData, pcbSize, cbSizeHeader);
}
if (pData == NULL) {
if (uiCommand == RID_HEADER)
if (uiCommand == RID_HEADER) {
*pcbSize = sizeof(RAWINPUTHEADER);
else
} else {
*pcbSize = sizeof(RAWINPUT);
}
return 0;
}
@@ -106,92 +243,9 @@ namespace games::ccj {
RAWMOUSE rawMouse {};
if (MOUSE_TRACKBALL) {
static bool active = false;
static bool lastState = false;
static std::chrono::time_point<std::chrono::steady_clock> lastModified = std::chrono::steady_clock::now();
static std::chrono::milliseconds debounceDuration(100);
auto currentTime = std::chrono::steady_clock::now();
bool pressed = get_async_secondary_mouse() & 0x8000;
bool focused = GetForegroundWindow() == hWnd;
if (focused && MOUSE_TRACKBALL_USE_TOGGLE && pressed && (currentTime - lastModified > debounceDuration)) {
active = !active;
lastModified = currentTime;
}
if (focused && ((MOUSE_TRACKBALL_USE_TOGGLE && active) || (!MOUSE_TRACKBALL_USE_TOGGLE && pressed))) {
POINT cursor;
RECT client;
GetClientRect(hWnd, &client);
int sx = client.right - client.left;
int sy = client.bottom - client.top;
GetCursorPos(&cursor);
ScreenToClient(hWnd, &cursor);
static int lastX = cursor.x;
static int lastY = cursor.y;
if (!lastState) {
lastX = cursor.x;
lastY = cursor.y;
lastState = true;
}
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
rawMouse.lLastX = (int)((float)(cursor.x - lastX) * (float)TRACKBALL_SENSITIVITY / 20.0f);
rawMouse.lLastY = (int)((float)(lastY - cursor.y) * (float)TRACKBALL_SENSITIVITY / 20.0f);
bool updateCursor = false;
if (cursor.x <= 0) {
updateCursor = true;
cursor.x = sx - 5;
} else if (cursor.x >= sx - 1) {
updateCursor = true;
cursor.x = 5;
}
if (cursor.y <= 0) {
updateCursor = true;
cursor.y = sy - 5;
} else if (cursor.y >= sy - 1) {
updateCursor = true;
cursor.y = 5;
}
lastX = cursor.x;
lastY = cursor.y;
if (updateCursor) {
ClientToScreen(hWnd, &cursor);
SetCursorPos(cursor.x, cursor.y);
}
} else if (lastState && !active) {
lastState = false;
}
trackball_mouse_input(rawMouse);
} else {
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
auto &analogs = get_analogs();
if (analogs[Analogs::Trackball_DX].isSet() || analogs[Analogs::Trackball_DY].isSet()) {
float x = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DX]) * 2.0f - 1.0f;
float y = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DY]) * 2.0f - 1.0f;
rawMouse.lLastX = (long) (x * (float) TRACKBALL_SENSITIVITY);
rawMouse.lLastY = (long) (-y * (float) TRACKBALL_SENSITIVITY);
}
auto &buttons = get_buttons();
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Up]))
rawMouse.lLastY = TRACKBALL_SENSITIVITY;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Down]))
rawMouse.lLastY = -TRACKBALL_SENSITIVITY;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Left]))
rawMouse.lLastX = -TRACKBALL_SENSITIVITY;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Right]))
rawMouse.lLastX = TRACKBALL_SENSITIVITY;
trackball_mapped_input(rawMouse);
}
*((RAWINPUT*)pData) = { header, { rawMouse } };
@@ -202,10 +256,19 @@ namespace games::ccj {
}
static BOOL WINAPI RegisterRawInputDevices_hook(PCRAWINPUTDEVICE pRawInputDevices, UINT uiNumDevices, UINT cbSize) {
if (uiNumDevices == 2 && pRawInputDevices[1].usUsage == HID_USAGE_GENERIC_GAMEPAD)
uiNumDevices = 1;
return RegisterRawInputDevices_orig(pRawInputDevices, uiNumDevices, cbSize);
// if the caller is spice itself, then pass through.
if (pRawInputDevices &&
(uiNumDevices > 0) &&
(pRawInputDevices[0].hwndTarget == RI_MGR->input_hwnd)) {
return RegisterRawInputDevices_orig(pRawInputDevices, uiNumDevices, cbSize);
}
// otherwise, it must be the game; prevent the game from registering for raw input
// and hijacking WM_INPUT messages; we need that for rawinput to work.
// even if we drop this, trackball emulation and mouse-as-touch input still work
return TRUE;
}
@@ -214,9 +277,8 @@ namespace games::ccj {
static bool initialized = false;
if (initialized) {
return;
} else {
initialized = true;
}
initialized = true;
// announce
log_info("trackball", "init");
@@ -236,30 +298,31 @@ namespace games::ccj {
}
void trackball_thread_start() {
using namespace std::chrono_literals;
tbThreadRunning = true;
log_info("trackball", "thread start, use mouse: {}, toggle: {}", MOUSE_TRACKBALL, MOUSE_TRACKBALL_USE_TOGGLE);
tbThread = new std::thread([&] {
timeutils::PreciseSleepTimer timer;
while (tbThreadRunning) {
if (hWnd && wndProc) {
wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle);
}
if (!tbThreadRunning)
if (!tbThreadRunning) {
break;
}
std::this_thread::sleep_for(10ms);
timer.sleep(10);
}
});
}
void trackball_thread_stop() {
tbThreadRunning = false;
if (tbThread)
if (tbThread) {
tbThread->join();
}
log_info("trackball", "thread stop");
+81 -19
View File
@@ -8,6 +8,7 @@
#include "hooks/setupapihook.h"
#include "hooks/sleephook.h"
#include "hooks/input/dinput8/hook.h"
#include "util/deferlog.h"
#include "util/utils.h"
#include "util/libutils.h"
#include "util/fileutils.h"
@@ -22,6 +23,7 @@
#include "p3io/usbmem.h"
#include "p4io/p4io.h"
#include <cstring>
using namespace acioemu;
@@ -52,9 +54,9 @@ namespace games::ddr {
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) {
if (static_cast<unsigned char>(c) > 127) {
if (c > 127) {
return false;
}
}
@@ -79,24 +81,24 @@ namespace games::ddr {
}
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 {
log_info("ddr", "codecs directory not found: {}", dir.string());
log_info("ddr", "codecs directory not found: {}", dir);
return;
}
for (const auto &file : std::filesystem::directory_iterator(dir)) {
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;
}
log_info("ddr", "found DLL: {}, size: {} bytes", filename.string(), file.file_size());
if (filename == "k-clvsd.dll" || filename.string().find("xactengine") == 0) {
log_info("ddr", "found DLL: {}, size: {} bytes", filename, file.file_size());
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::string cmd = "regsvr32.exe /s \"" + file.path().string() + "\"";
const std::string cmd = fmt::format("regsvr32.exe /s \"{}\"", file.path());
int result = 0;
std::thread t([wcmd, &result]() {
@@ -108,18 +110,32 @@ namespace games::ddr {
log_info("ddr", "`{}` returned {}", cmd, result);
} else {
log_warning("ddr", "`{}` failed, returned {}", cmd, result);
deferredlogs::defer_error_messages({
"DDR codec registration failure",
fmt::format(" {} failed to register with error {}", filename, result)});
}
if (!contains_only_ascii(file.path().string())) {
log_warning(
"ddr",
"BAD PATH ERROR\n\n\n"
"!!! !!!\n"
"!!! filesystem path to codec contains non-ASCII characters! !!!\n"
"!!! this may cause the game to crash! !!!\n"
"!!! !!!\n"
);
}
static std::once_flag printed;
std::call_once(printed, [file]() {
if (!contains_only_ascii(file.path().wstring())) {
log_warning(
"ddr",
"BAD PATH ERROR\n\n"
"!!! !!!\n"
"!!! filesystem path to codec contains non-ASCII characters! !!!\n"
"!!! this may cause failures when loading codecs, and !!!\n"
"!!! potentially leading the game to crash! !!!\n"
"!!! !!!\n"
);
deferredlogs::defer_error_messages({
"path to DDR codecs contain non-Latin characters",
" this may lead to movies not playing, white screen issue,",
" game hanging, or potentially a crash",
" move the game to a directory that does not contain non-Latin characters",
" if you encountered any issues"});
}
});
}
}
}
@@ -275,4 +291,50 @@ namespace games::ddr {
// dispose device hook
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:
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);
}
+158 -110
View File
@@ -35,142 +35,190 @@ std::vector<Button> &games::ddr::get_buttons() {
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() {
static std::vector<Light> lights;
if (lights.empty()) {
lights = GameAPI::Lights::getLights("Dance Dance Revolution");
GameAPI::Lights::sortLights(
&lights,
"P1 Foot Left",
"P1 Foot Up",
"P1 Foot Right",
"P1 Foot Down",
"P2 Foot Left",
"P2 Foot Up",
"P2 Foot Right",
"P2 Foot Down",
"Spot Red",
"Spot Blue",
"Top Spot Red",
"Top Spot Blue",
"P1 Halogen Upper",
"P1 Halogen Lower",
"P2 Halogen Upper",
"P2 Halogen Lower",
"P1 Button",
"P2 Button",
"Neon",
"HD P1 Start",
"HD P1 Menu Left-Right",
"HD P1 Menu Up-Down",
"HD P2 Start",
"HD P2 Menu Left-Right",
"HD P2 Menu Up-Down",
"HD P1 Speaker F R",
"HD P1 Speaker F G",
"HD P1 Speaker F B",
"HD P1 Speaker W R",
"HD P1 Speaker W G",
"HD P1 Speaker W B",
"HD P2 Speaker F R",
"HD P2 Speaker F G",
"HD P2 Speaker F B",
"HD P2 Speaker W R",
"HD P2 Speaker W G",
"HD P2 Speaker W B",
GameAPI::Lights::sortLightsWithCategory(
&lights,
{
{"General", "P1 Foot Left"},
{"General", "P1 Foot Up"},
{"General", "P1 Foot Right"},
{"General", "P1 Foot Down"},
{"General", "P2 Foot Left"},
{"General", "P2 Foot Up"},
{"General", "P2 Foot Right"},
{"General", "P2 Foot Down"},
"WHITE Speaker Top R",
"WHITE Speaker Top G",
"WHITE Speaker Top B",
"WHITE Speaker Bottom R",
"WHITE Speaker Bottom G",
"WHITE Speaker Bottom B",
"WHITE Woofer R",
"WHITE Woofer G",
"WHITE Woofer B",
{"SD", "Spot Red"},
{"SD", "Spot Blue"},
{"SD", "Top Spot Red"},
{"SD", "Top Spot Blue"},
{"SD", "P1 Halogen Upper"},
{"SD", "P1 Halogen Lower"},
{"SD", "P2 Halogen Upper"},
{"SD", "P2 Halogen Lower"},
{"SD", "P1 Button"},
{"SD", "P2 Button"},
{"SD", "Neon"},
"GOLD P1 Menu Start",
"GOLD P1 Menu Up",
"GOLD P1 Menu Down",
"GOLD P1 Menu Left",
"GOLD P1 Menu Right",
{"HD", "HD P1 Start"},
{"HD", "HD P1 Menu Left-Right"},
{"HD", "HD P1 Menu Up-Down"},
{"HD", "HD P2 Start"},
{"HD", "HD P2 Menu Left-Right"},
{"HD", "HD P2 Menu Up-Down"},
{"HD", "HD P1 Speaker F R"},
{"HD", "HD P1 Speaker F G"},
{"HD", "HD P1 Speaker F B"},
{"HD", "HD P1 Speaker W R"},
{"HD", "HD P1 Speaker W G"},
{"HD", "HD P1 Speaker W B"},
{"HD", "HD P2 Speaker F R"},
{"HD", "HD P2 Speaker F G"},
{"HD", "HD P2 Speaker F B"},
{"HD", "HD P2 Speaker W R"},
{"HD", "HD P2 Speaker W G"},
{"HD", "HD P2 Speaker W B"},
"GOLD P2 Menu Start",
"GOLD P2 Menu Up",
"GOLD P2 Menu Down",
"GOLD P2 Menu Left",
"GOLD P2 Menu Right",
{"WHITE", "WHITE Speaker Top R"},
{"WHITE", "WHITE Speaker Top G"},
{"WHITE", "WHITE Speaker Top B"},
{"WHITE", "WHITE Speaker Bottom R"},
{"WHITE", "WHITE Speaker Bottom G"},
{"WHITE", "WHITE Speaker Bottom B"},
{"WHITE", "WHITE Woofer R"},
{"WHITE", "WHITE Woofer G"},
{"WHITE", "WHITE Woofer B"},
"GOLD Title Panel Left",
"GOLD Title Panel Center",
"GOLD Title Panel Right",
{"GOLD", "GOLD P1 Menu Start"},
{"GOLD", "GOLD P1 Menu Up"},
{"GOLD", "GOLD P1 Menu Down"},
{"GOLD", "GOLD P1 Menu Left"},
{"GOLD", "GOLD P1 Menu Right"},
"GOLD P1 Woofer Corner",
"GOLD P2 Woofer Corner",
{"GOLD", "GOLD P2 Menu Start"},
{"GOLD", "GOLD P2 Menu Up"},
{"GOLD", "GOLD P2 Menu Down"},
{"GOLD", "GOLD P2 Menu Left"},
{"GOLD", "GOLD P2 Menu Right"},
"GOLD P1 Card Unit R",
"GOLD P1 Card Unit G",
"GOLD P1 Card Unit B",
{"GOLD", "GOLD Title Panel Left"},
{"GOLD", "GOLD Title Panel Center"},
{"GOLD", "GOLD Title Panel Right"},
"GOLD P2 Card Unit R",
"GOLD P2 Card Unit G",
"GOLD P2 Card Unit B",
{"GOLD", "GOLD P1 Woofer Corner"},
{"GOLD", "GOLD P2 Woofer Corner"},
"GOLD Top Panel Avg R",
"GOLD Top Panel Avg G",
"GOLD Top Panel Avg B",
{"GOLD", "GOLD P1 Card Unit R"},
{"GOLD", "GOLD P1 Card Unit G"},
{"GOLD", "GOLD P1 Card Unit B"},
"GOLD Monitor Side Left Avg R",
"GOLD Monitor Side Left Avg G",
"GOLD Monitor Side Left Avg B",
{"GOLD", "GOLD P2 Card Unit R"},
{"GOLD", "GOLD P2 Card Unit G"},
{"GOLD", "GOLD P2 Card Unit B"},
"GOLD Monitor Side Right Avg R",
"GOLD Monitor Side Right Avg G",
"GOLD Monitor Side Right Avg B",
{"GOLD", "GOLD Top Panel Avg R"},
{"GOLD", "GOLD Top Panel Avg G"},
{"GOLD", "GOLD Top Panel Avg B"},
"GOLD P1 Foot Up Avg R",
"GOLD P1 Foot Up Avg G",
"GOLD P1 Foot Up Avg B",
{"GOLD", "GOLD Monitor Side Left Avg R"},
{"GOLD", "GOLD Monitor Side Left Avg G"},
{"GOLD", "GOLD Monitor Side Left Avg B"},
"GOLD P1 Foot Down Avg R",
"GOLD P1 Foot Down Avg G",
"GOLD P1 Foot Down Avg B",
{"GOLD", "GOLD Monitor Side Right Avg R"},
{"GOLD", "GOLD Monitor Side Right Avg G"},
{"GOLD", "GOLD Monitor Side Right Avg B"},
"GOLD P1 Foot Left Avg R",
"GOLD P1 Foot Left Avg G",
"GOLD P1 Foot Left Avg B",
{"GOLD", "GOLD P1 Foot Up Avg R"},
{"GOLD", "GOLD P1 Foot Up Avg G"},
{"GOLD", "GOLD P1 Foot Up Avg B"},
"GOLD P1 Foot Right Avg R",
"GOLD P1 Foot Right Avg G",
"GOLD P1 Foot Right Avg B",
{"GOLD", "GOLD P1 Foot Down Avg R"},
{"GOLD", "GOLD P1 Foot Down Avg G"},
{"GOLD", "GOLD P1 Foot Down Avg B"},
"GOLD P2 Foot Up Avg R",
"GOLD P2 Foot Up Avg G",
"GOLD P2 Foot Up Avg B",
{"GOLD", "GOLD P1 Foot Left Avg R"},
{"GOLD", "GOLD P1 Foot Left Avg G"},
{"GOLD", "GOLD P1 Foot Left Avg B"},
"GOLD P2 Foot Down Avg R",
"GOLD P2 Foot Down Avg G",
"GOLD P2 Foot Down Avg B",
{"GOLD", "GOLD P1 Foot Right Avg R"},
{"GOLD", "GOLD P1 Foot Right Avg G"},
{"GOLD", "GOLD P1 Foot Right Avg B"},
"GOLD P2 Foot Left Avg R",
"GOLD P2 Foot Left Avg G",
"GOLD P2 Foot Left Avg B",
{"GOLD", "GOLD P2 Foot Up Avg R"},
{"GOLD", "GOLD P2 Foot Up Avg G"},
{"GOLD", "GOLD P2 Foot Up Avg B"},
"GOLD P2 Foot Right Avg R",
"GOLD P2 Foot Right Avg G",
"GOLD P2 Foot Right Avg B",
{"GOLD", "GOLD P2 Foot Down Avg R"},
{"GOLD", "GOLD P2 Foot Down Avg G"},
{"GOLD", "GOLD P2 Foot Down Avg B"},
"GOLD P1 Stage Corner Up-Left",
"GOLD P1 Stage Corner Up-Right",
"GOLD P1 Stage Corner Down-Left",
"GOLD P1 Stage Corner Down-Right",
{"GOLD", "GOLD P2 Foot Left Avg R"},
{"GOLD", "GOLD P2 Foot Left Avg G"},
{"GOLD", "GOLD P2 Foot Left Avg B"},
"GOLD P2 Stage Corner Up-Left",
"GOLD P2 Stage Corner Up-Right",
"GOLD P2 Stage Corner Down-Left",
"GOLD P2 Stage Corner Down-Right"
{"GOLD", "GOLD P2 Foot Right Avg R"},
{"GOLD", "GOLD P2 Foot Right Avg G"},
{"GOLD", "GOLD P2 Foot Right Avg B"},
{"GOLD", "GOLD P1 Stage Corner Up-Left"},
{"GOLD", "GOLD P1 Stage Corner Up-Right"},
{"GOLD", "GOLD P1 Stage Corner Down-Left"},
{"GOLD", "GOLD P1 Stage Corner Down-Right"},
{"GOLD", "GOLD P2 Stage Corner Up-Left"},
{"GOLD", "GOLD P2 Stage Corner Up-Right"},
{"GOLD", "GOLD P2 Stage Corner Down-Left"},
{"GOLD", "GOLD P2 Stage Corner Down-Right"}
}
);
}
+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
namespace Lights {
enum {
@@ -177,5 +187,8 @@ namespace games::ddr {
// getters
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();
}
+53 -3
View File
@@ -3,6 +3,7 @@
#include "cfg/api.h"
#include "rawinput/rawinput.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include "../ddr.h"
@@ -282,7 +283,7 @@ int games::ddr::DDRP3IOHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWri
}
// use both sat spots for a neon pulse
float value_neon = (light_bits & hd_mapping_bits[0] && hd_mapping_bits[1]) ? 1.f : 0.f;
float value_neon = (light_bits & hd_mapping_bits[0] && light_bits & hd_mapping_bits[1]) ? 1.f : 0.f;
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::NEON], value_neon);
}
@@ -459,7 +460,8 @@ int games::ddr::DDRP3IOHandle::device_io(
if (nOutBufferSize >= 4) {
// cool down
Sleep(1);
static thread_local timeutils::PreciseSleepTimer timer;
timer.sleep(1);
// get controls as single variable (4 bytes)
auto &controls = *(uint32_t*) lpOutBuffer;
@@ -498,32 +500,80 @@ int games::ddr::DDRP3IOHandle::device_io(
// 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[] = {
Buttons::SERVICE,
Buttons::TEST,
Buttons::COIN_MECH,
Buttons::P1_START,
Buttons::P1_PANEL_UP,
Buttons::P1_PANEL_DOWN,
Buttons::P1_PANEL_LEFT,
Buttons::P1_PANEL_RIGHT,
Buttons::P1_MENU_UP,
Buttons::P1_MENU_DOWN,
Buttons::P1_MENU_LEFT,
Buttons::P1_MENU_RIGHT,
Buttons::P2_START,
Buttons::P2_PANEL_UP,
Buttons::P2_PANEL_DOWN,
Buttons::P2_PANEL_LEFT,
Buttons::P2_PANEL_RIGHT,
Buttons::P2_MENU_UP,
Buttons::P2_MENU_DOWN,
Buttons::P2_MENU_LEFT,
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
auto &buttons = get_buttons();
size_t count = 0;
+7 -8
View File
@@ -2,6 +2,7 @@
#include "rawinput/rawinput.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
#include "../io.h"
@@ -253,28 +254,26 @@ namespace games::ddr {
if(((dwIoControlCode>>16)&0xFFFF) == FILE_DEVICE_UNKNOWN) {
switch((dwIoControlCode>>2)&0x3FFF) {
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
static thread_local timeutils::PreciseSleepTimer timer;
timer.sleep(4);
memset(lpOutBuffer, 0, 16);
auto controls = (uint32_t*) lpOutBuffer;
const std::pair<size_t, uint8_t> buttonMapping[] =
{
// Panel input comes from MDXF Control Status Buffer functions
{Buttons::SERVICE, 25}, //Good
{Buttons::TEST, 28}, //Good
{Buttons::COIN_MECH, 24}, //Good
{Buttons::P1_START, 0}, //Good
{Buttons::P1_PANEL_UP, 5}, //Good
{Buttons::P1_PANEL_DOWN, 6}, //Good
{Buttons::P1_PANEL_LEFT, 7}, //Goood
{Buttons::P1_PANEL_RIGHT, 16}, //Good
{Buttons::P1_MENU_UP, 1}, //Good
{Buttons::P1_MENU_DOWN, 2}, //Good
{Buttons::P1_MENU_LEFT, 3}, //Good
{Buttons::P1_MENU_RIGHT, 4}, //Good
{Buttons::P2_START, 8}, //Good
{Buttons::P2_PANEL_UP, 13}, //Good
{Buttons::P2_PANEL_DOWN, 14}, //Good
{Buttons::P2_PANEL_LEFT, 15}, //Good
{Buttons::P2_PANEL_RIGHT, 20}, //Good
{Buttons::P2_MENU_UP, 9}, //Good
{Buttons::P2_MENU_DOWN, 10}, //Good
{Buttons::P2_MENU_LEFT, 11}, //Good
+9 -1
View File
@@ -7,7 +7,9 @@
#include "games/game.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/memutils.h"
#include "rgb_cam.h"
#pragma pack(push)
@@ -150,6 +152,7 @@ namespace games::drs {
char DRS_TAPELED[DRS_TAPELED_COLS_TOTAL][3] {};
bool DISABLE_TOUCH = false;
bool TRANSPOSE_TOUCH = false;
bool RGB_CAMERA_HOOK = false;
std::vector<TouchEvent> TOUCH_EVENTS;
@@ -219,10 +222,11 @@ namespace games::drs {
void start_touch() {
std::thread t([] {
log_info("drs", "starting touch input thread");
timeutils::PreciseSleepTimer timer;
// main loop
while (TRUE) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
timer.sleep(1);
TOUCH_EVENTS.clear();
touch_get_events(TOUCH_EVENTS);
@@ -313,6 +317,10 @@ namespace games::drs {
} else {
log_info("drs", "no native input method detected");
}
if (RGB_CAMERA_HOOK) {
init_rgb_camera_hook();
}
}
void DRSGame::detach() {
+1
View File
@@ -31,6 +31,7 @@ namespace games::drs {
extern std::vector<TouchEvent> TOUCH_EVENTS;
extern bool DISABLE_TOUCH;
extern bool TRANSPOSE_TOUCH;
extern bool RGB_CAMERA_HOOK;
void fire_touches(drs_touch_t *events, size_t event_count);
void start_touch();
+170
View File
@@ -0,0 +1,170 @@
#include "games/drs/rgb_cam.h"
// set version to Windows 7 to enable Media Foundation functions
#ifdef _WIN32_WINNT
#undef _WIN32_WINNT
#endif
#define _WIN32_WINNT 0x0601
// turn on C-style COM objects
#define CINTERFACE
#include <mfobjects.h>
#include <mfidl.h>
#include <string>
#include "avs/game.h"
#include "cfg/configurator.h"
#include "hooks/cfgmgr32hook.h"
#include "util/detour.h"
#include "util/memutils.h"
#include "util/logging.h"
#include "util/utils.h"
static VTBL_TYPE(IMFActivate, GetAllocatedString) GetAllocatedString_orig = nullptr;
static decltype(MFEnumDeviceSources) *MFEnumDeviceSources_orig = nullptr;
namespace games::drs {
/*
* Camera related stuff
*/
static std::wstring CAMERA0_ID;
static HRESULT WINAPI GetAllocatedString_hook(IMFActivate* This, REFGUID guidKey, LPWSTR *ppwszValue,
UINT32 *pcchLength)
{
// call the original
HRESULT result = GetAllocatedString_orig(This, guidKey, ppwszValue, pcchLength);
// log the cam name
log_misc("drs::rgbcam", "obtained {}", ws2s(*ppwszValue));
// try first camera
wchar_t *pwc = nullptr;
if (CAMERA0_ID.length() == 23) {
pwc = wcsstr(*ppwszValue, CAMERA0_ID.c_str());
}
// check if camera could be identified
if (pwc) {
// fake the USB IDs
pwc[4] = L'2';
pwc[5] = L'8';
pwc[6] = L'8';
pwc[7] = L'c';
pwc[13] = L'0';
pwc[14] = L'0';
pwc[15] = L'0';
pwc[16] = L'2';
pwc[21] = L'0';
pwc[22] = L'0';
log_misc("drs::rgbcam", "replaced {}", ws2s(*ppwszValue));
}
// return original result
return result;
}
static void hook_camera(std::wstring camera_id, std::string camera_instance) {
// don't hook if camera 0 is already hooked
if (CAMERA0_ID.length() > 0) {
return;
}
// save the camera ID
CAMERA0_ID = camera_id;
// cfgmgr hook
CFGMGR32_HOOK_SETTING camera_setting;
camera_setting.device_instance = 0xDEADBEEF;
camera_setting.parent_instance = ~camera_setting.device_instance;
camera_setting.device_id = "USB\\VEN_8086&DEV_8C2D";
camera_setting.device_node_id = "USB\\VID_288C&PID_0002&MI_00\\?&????????&?&????";
if (camera_instance.length() == 17) {
for (int i = 0; i < 17; i++) {
camera_setting.device_node_id[28 + i] = camera_instance[i];
}
}
cfgmgr32hook_add(camera_setting);
log_info("drs::rgbcam", "hooked camera @ {}", ws2s(camera_id));
}
static void hook_camera_vtable(IMFActivate *camera) {
// hook allocated string method for camera identification
memutils::VProtectGuard camera_guard(camera->lpVtbl);
camera->lpVtbl->GetAllocatedString = GetAllocatedString_hook;
}
static HRESULT WINAPI MFEnumDeviceSources_hook(IMFAttributes *pAttributes, IMFActivate ***pppSourceActivate,
UINT32 *pcSourceActivate) {
// call original function
HRESULT result_orig = MFEnumDeviceSources_orig(pAttributes, pppSourceActivate, pcSourceActivate);
// check for capture devices
if (FAILED(result_orig) || !*pcSourceActivate) {
return result_orig;
}
// get camera
IMFActivate *camera = **pppSourceActivate;
// save original method for later use
if (GetAllocatedString_orig == nullptr) {
GetAllocatedString_orig = camera->lpVtbl->GetAllocatedString;
}
// hook allocated string method for camera identification
hook_camera_vtable(camera);
// get camera link
LPWSTR camera_link_lpwstr;
UINT32 camera_link_length;
if (SUCCEEDED(GetAllocatedString_orig(
camera,
MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK,
&camera_link_lpwstr,
&camera_link_length))) {
// cut name to make ID
std::wstring camera_link_ws = std::wstring(camera_link_lpwstr);
std::wstring camera_id = camera_link_ws.substr(8, 23);
log_info("drs::rgbcam", "found video capture device: {}", ws2s(camera_link_ws));
// only support cameras that start with \\?\usb
if (!string_begins_with(camera_link_ws, L"\\\\?\\usb")) {
return result_orig;
}
// get camera instance
std::string camera_link = ws2s(camera_link_ws);
std::string camera_instance = camera_link.substr(32, 17);
// hook the camera
hook_camera(camera_id, camera_instance);
} else {
log_warning("drs::rgbcam", "failed to open camera");
}
// return result
return result_orig;
}
void init_rgb_camera_hook() {
log_info("drs::rgbcam", "initializing camera hook");
cfgmgr32hook_init(avs::game::DLL_INSTANCE);
// camera media framework hook
MFEnumDeviceSources_orig = detour::iat_try(
"MFEnumDeviceSources", MFEnumDeviceSources_hook, avs::game::DLL_INSTANCE);
}
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
namespace games::drs {
void init_rgb_camera_hook();
}
+8 -7
View File
@@ -35,13 +35,14 @@ std::vector<Analog> &games::ftt::get_analogs() {
if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("FutureTomTom");
GameAPI::Analogs::sortAnalogs(
&analogs,
"Pad 1",
"Pad 2",
"Pad 3",
"Pad 4"
);
using GameAPI::Analogs::AnalogType;
GameAPI::Analogs::sortAnalogsWithType(&analogs, {
{ "Pad 1", AnalogType::LinearPositive },
{ "Pad 2", AnalogType::LinearPositive },
{ "Pad 3", AnalogType::LinearPositive },
{ "Pad 4", AnalogType::LinearPositive }
});
}
return analogs;

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